Compare commits
223 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6693bbe166 | ||
|
|
1696135096 | ||
| c764182a6d | |||
|
|
a112ef2eca | ||
|
|
7e28517b82 | ||
|
|
3080ab995a | ||
|
|
7a510b315d | ||
|
|
dc701e5c49 | ||
| 233e9ca92f | |||
|
|
e1a25cdd31 | ||
|
|
5be23c0d52 | ||
|
|
de8cf11d4d | ||
|
|
666a50e163 | ||
| 0bd256cb09 | |||
| 0d46802862 | |||
| b190e25c88 | |||
| 8290f9dd23 | |||
| 22e57dc075 | |||
| aa6406f949 | |||
| 663ad01740 | |||
| 619c5975aa | |||
| 795c707a1f | |||
| 0599ec2512 | |||
| b1cb45ac7e | |||
| 274c86a820 | |||
| 3119dff6fe | |||
| f7f3532b30 | |||
| 1dd83af6aa | |||
| cea64da34d | |||
| cba9cadb41 | |||
| 9428bc05cc | |||
| 94d45c1f17 | |||
| 8be3e2ea78 | |||
| 7e33d9e5de | |||
| 923166b592 | |||
| dbfd3657a0 | |||
|
|
864175284d | ||
| a4eda81436 | |||
| 4db44733ca | |||
| 1fc43a8727 | |||
| 6cd1d829b6 | |||
| 85dc1fa0b5 | |||
| 2aa64f7360 | |||
| f3da2ac630 | |||
| d44b903b3e | |||
| c4501f1d08 | |||
| 560d1adf5f | |||
| acd6a3c11c | |||
| 2787bde8da | |||
| 6b82c26268 | |||
| 770c2f3182 | |||
| 843e6dbba2 | |||
| 3607a9da6d | |||
| 608bea5199 | |||
| 07dd91665c | |||
| 5662a2d1f1 | |||
| 72938e41ba | |||
|
|
cf6ae898c4 | ||
| 05acba8cbe | |||
| 9856272734 | |||
| 504490f593 | |||
|
|
4c109538ee | ||
|
|
1e5c4df163 | ||
| e1ebd528b8 | |||
| 708320e067 | |||
| 51298ba726 | |||
| 96e3eb613d | |||
| a2903da109 | |||
| c9fa627651 | |||
| 969cc4df63 | |||
| 2480efc345 | |||
| 8c4ec00c35 | |||
| 78a8442d07 | |||
| 95803db3a0 | |||
| d71ff014ea | |||
| aca5a79de2 | |||
| 4feb05a02e | |||
| 77a9100b2e | |||
|
|
b947037ea2 | ||
|
|
d88acf4634 | ||
|
|
fbe0e37d28 | ||
| bb03d28b7d | |||
| 2196e89208 | |||
| 4ff51c8f6f | |||
| 434eed2217 | |||
| f88e527fe4 | |||
| 40fe8ecfc6 | |||
| 13502d704e | |||
| d1e1f25162 | |||
| ac5bca2fcd | |||
| 064645d1b3 | |||
| 956c85a9f3 | |||
|
|
bee135ab48 | ||
| 42ecc91c22 | |||
| a37d4b369a | |||
| 039b1234c5 | |||
| 87b3214c84 | |||
| ad937cda8c | |||
| 899071e4d6 | |||
| 55bf8c0355 | |||
| b4a8ccf9cc | |||
| 76d3c57658 | |||
| 2490f8e69f | |||
| 6cd0152636 | |||
| 19433fdc56 | |||
| 012c75db21 | |||
| 71e2d6827e | |||
| 41b14f1ece | |||
| a2e01e74af | |||
| 8a353c357f | |||
| 9bf7033eac | |||
| 0a01490aa7 | |||
| 115452035d | |||
| b8e7ee2b3d | |||
| 3d1908d71a | |||
| ed54b666ec | |||
| a1d8dc2e7e | |||
|
|
956464659e | ||
| 9a5c487b2c | |||
| bcfcbd5c9b | |||
| 96038a4f22 | |||
| 5c4042e5f3 | |||
| c9f1bc4047 | |||
| bf13473954 | |||
| a0e7b2f96c | |||
| 848890d3cd | |||
| f8bf02df2b | |||
| 1489c27121 | |||
| 0c5f4cb430 | |||
| 9597663881 | |||
| 7ed5e7a326 | |||
| 1180ef6fd0 | |||
| 2c3292cadf | |||
| 7803b753cb | |||
|
|
bbeebc641e | ||
| 42c97924ec | |||
| 29fc201294 | |||
| 938423b832 | |||
| 54f2430b3e | |||
| 2e47df00b9 | |||
| 223ae0f2f8 | |||
| cc8bd19f85 | |||
|
|
3f5a22c85d | ||
| 12fe58fc81 | |||
|
|
fafa25a7b5 | ||
|
|
d4cfce317d | ||
| ac045fdc70 | |||
| a85a5be4cd | |||
| 1d7b46b4a4 | |||
|
|
453034100b | ||
| 9c608070ae | |||
| aa81de74a4 | |||
| d9ed893b52 | |||
| dfa774f655 | |||
| 608d8a3661 | |||
| 3c4e6b62fb | |||
| f39de71c02 | |||
| 24dbaf36bc | |||
| 43bf25a5bd | |||
| f4b8417deb | |||
| c8350722a4 | |||
| af09e02801 | |||
| 8e2a98c12b | |||
| 1cdb20eb60 | |||
|
|
c8fa509ace | ||
|
|
5a023a9e32 | ||
|
|
2763d2012a | ||
| 264aaab24c | |||
| c65dc04e8f | |||
| a1ba004efa | |||
| 514bca5257 | |||
| 9cbc35c263 | |||
| 84d7890ae4 | |||
| aa893062f1 | |||
| d4a3459adf | |||
|
|
8acfc1d10c | ||
| e9bbeffd7e | |||
| 7e3e772055 | |||
| 471982d245 | |||
| 3eec9ef8df | |||
| ff5b31929e | |||
| 12bb8b7145 | |||
| 2e005a90f2 | |||
| 843ac60aae | |||
| a42ccb0faa | |||
| c929d42ebd | |||
| 4051d1e11b | |||
| 71af64dc28 | |||
| 74f044919c | |||
| 9dc6ec461c | |||
| 1f31fe8cf2 | |||
| d56c9c3c31 | |||
| 55ed01cb40 | |||
|
|
9f299c854c | ||
|
|
35902daff6 | ||
|
|
31f657a15f | ||
|
|
7946e046e2 | ||
|
|
7ca12d064d | ||
|
|
5e85eae825 | ||
|
|
3e9e90ed86 | ||
|
|
a67697d159 | ||
|
|
ce8aa849f8 | ||
| 5c4f742745 | |||
| 7b7b82cba3 | |||
|
|
cf89722c7f | ||
|
|
44d082f8b9 | ||
|
|
9b9370fed0 | ||
|
|
2351388826 | ||
| aa41004c39 | |||
| 29df39f3b5 | |||
| de005691f1 | |||
| 050516c0bc | |||
| e63c8751eb | |||
| 2a4158303e | |||
| 1797d4eb9b | |||
| 307cda543e | |||
| de19073e11 | |||
| 18af65da10 | |||
| 45048ce62d | |||
| bc4594bea2 | |||
| e4883c62d0 | |||
| 6e5a58aa37 | |||
| d495a31ad8 |
207
.gitlab-ci.yml
207
.gitlab-ci.yml
@ -1,4 +1,4 @@
|
|||||||
# SPDX-FileCopyrightText: 2022-2023 Sarah Vaupel <sarah.vaupel@uniworx.de>, Gregor Kleen <gregor@kleen.consulting>
|
# SPDX-FileCopyrightText: 2022-2024 Sarah Vaupel <sarah.vaupel@uniworx.de>, Gregor Kleen <gregor@kleen.consulting>
|
||||||
#
|
#
|
||||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
@ -35,6 +35,7 @@ node dependencies:
|
|||||||
stage: frontend:build
|
stage: frontend:build
|
||||||
script:
|
script:
|
||||||
- nix -L build -o result ".#uniworxNodeDependencies"
|
- nix -L build -o result ".#uniworxNodeDependencies"
|
||||||
|
- nix-store --gc
|
||||||
- nix-store --export $(nix-store -qR result) | xz -T0 -2 > node-dependencies.nar.xz
|
- nix-store --export $(nix-store -qR result) | xz -T0 -2 > node-dependencies.nar.xz
|
||||||
before_script: &nix-before
|
before_script: &nix-before
|
||||||
- git config --global init.defaultBranch master
|
- git config --global init.defaultBranch master
|
||||||
@ -54,8 +55,12 @@ node dependencies:
|
|||||||
well known:
|
well known:
|
||||||
stage: frontend:build
|
stage: frontend:build
|
||||||
script:
|
script:
|
||||||
|
# - xzcat node-dependencies.nar.xz > node-dependencies-debug.nar
|
||||||
|
# - nix-shell -p util-linux --command "hexdump -C node-dependencies-debug.nar | head -n 10"
|
||||||
|
# - nix nar ls node-dependencies-debug.nar /
|
||||||
- xzcat node-dependencies.nar.xz | nix-store --import
|
- xzcat node-dependencies.nar.xz | nix-store --import
|
||||||
- nix -L build -o result ".#uniworxWellKnown"
|
- nix -L build -o result ".#uniworxWellKnown"
|
||||||
|
- nix-store --gc
|
||||||
- nix-store --export $(nix-store -qR result) | xz -T0 -2 > well-known.nar.xz
|
- nix-store --export $(nix-store -qR result) | xz -T0 -2 > well-known.nar.xz
|
||||||
before_script: *nix-before
|
before_script: *nix-before
|
||||||
needs:
|
needs:
|
||||||
@ -75,6 +80,7 @@ frontend:
|
|||||||
- xzcat node-dependencies.nar.xz | nix-store --import
|
- xzcat node-dependencies.nar.xz | nix-store --import
|
||||||
- xzcat well-known.nar.xz | nix-store --import
|
- xzcat well-known.nar.xz | nix-store --import
|
||||||
- nix -L build -o result ".#uniworxFrontend"
|
- nix -L build -o result ".#uniworxFrontend"
|
||||||
|
- nix-store --gc
|
||||||
- nix-store --export $(nix-store -qR result) | xz -T0 -2 > frontend.nar.xz
|
- nix-store --export $(nix-store -qR result) | xz -T0 -2 > frontend.nar.xz
|
||||||
before_script: *nix-before
|
before_script: *nix-before
|
||||||
needs:
|
needs:
|
||||||
@ -95,6 +101,7 @@ uniworx:lib:uniworx:
|
|||||||
script:
|
script:
|
||||||
- xzcat frontend.nar.xz | nix-store --import
|
- xzcat frontend.nar.xz | nix-store --import
|
||||||
- nix -L build -o result ".#uniworx:lib:uniworx"
|
- nix -L build -o result ".#uniworx:lib:uniworx"
|
||||||
|
- nix-store --gc
|
||||||
- nix-store --export $(nix-store -qR result) | xz -T0 -2 > uniworx:lib:uniworx.nar.xz
|
- nix-store --export $(nix-store -qR result) | xz -T0 -2 > uniworx:lib:uniworx.nar.xz
|
||||||
before_script: *nix-before
|
before_script: *nix-before
|
||||||
needs:
|
needs:
|
||||||
@ -117,6 +124,7 @@ uniworx:exe:uniworx:
|
|||||||
script:
|
script:
|
||||||
- xzcat uniworx:lib:uniworx.nar.xz | nix-store --import
|
- xzcat uniworx:lib:uniworx.nar.xz | nix-store --import
|
||||||
- nix -L build -o result ".#uniworx:exe:uniworx"
|
- nix -L build -o result ".#uniworx:exe:uniworx"
|
||||||
|
- nix-store --gc
|
||||||
- nix-store --export $(nix-store -qR result) | xz -T0 -2 > uniworx:exe:uniworx.nar.xz
|
- nix-store --export $(nix-store -qR result) | xz -T0 -2 > uniworx:exe:uniworx.nar.xz
|
||||||
before_script: *nix-before
|
before_script: *nix-before
|
||||||
needs:
|
needs:
|
||||||
@ -141,6 +149,7 @@ uniworx:exe:uniworxdb:
|
|||||||
script:
|
script:
|
||||||
- xzcat uniworx:lib:uniworx.nar.xz | nix-store --import
|
- xzcat uniworx:lib:uniworx.nar.xz | nix-store --import
|
||||||
- nix -L build -o result ".#uniworx:exe:uniworxdb"
|
- nix -L build -o result ".#uniworx:exe:uniworxdb"
|
||||||
|
- nix-store --gc
|
||||||
- nix-store --export $(nix-store -qR result) | xz -T0 -2 > uniworx:exe:uniworxdb.nar.xz
|
- nix-store --export $(nix-store -qR result) | xz -T0 -2 > uniworx:exe:uniworxdb.nar.xz
|
||||||
before_script: *nix-before
|
before_script: *nix-before
|
||||||
needs:
|
needs:
|
||||||
@ -165,6 +174,7 @@ uniworx:exe:uniworxload:
|
|||||||
script:
|
script:
|
||||||
- xzcat uniworx:lib:uniworx.nar.xz | nix-store --import
|
- xzcat uniworx:lib:uniworx.nar.xz | nix-store --import
|
||||||
- nix -L build -o result ".#uniworx:exe:uniworxload"
|
- nix -L build -o result ".#uniworx:exe:uniworxload"
|
||||||
|
- nix-store --gc
|
||||||
- nix-store --export $(nix-store -qR result) | xz -T0 -2 > uniworx:exe:uniworxload.nar.xz
|
- nix-store --export $(nix-store -qR result) | xz -T0 -2 > uniworx:exe:uniworxload.nar.xz
|
||||||
before_script: *nix-before
|
before_script: *nix-before
|
||||||
needs:
|
needs:
|
||||||
@ -207,8 +217,13 @@ container:
|
|||||||
stage: container:build
|
stage: container:build
|
||||||
script:
|
script:
|
||||||
- xzcat uniworx:exe:uniworx.nar.xz | nix-store --import
|
- xzcat uniworx:exe:uniworx.nar.xz | nix-store --import
|
||||||
- cp -pr --reflink=auto -L $(nix build --print-out-paths ".#uniworxDocker") uniworx.tar.gz
|
# - &container-remove-nodejs
|
||||||
|
# "for i in `ls /nix/store/ | grep -E -i '^[a-z0-9]+-nodejs-'` ; do ( nix store delete --ignore-liveness \"/nix/store/$i\" || ( echo \"Could not remove NodeJS from /nix/store/ due to: \" && ( nix-store --query --roots \"/nix/store/$i\" | cat ) && echo \"Removing NodeJS by brute force...\" && rm -rf \"/nix/store/$i\" ) ) ; done"
|
||||||
|
- cp -p --reflink=auto -L $(nix build --print-out-paths ".#uniworxDocker") uniworx.tar.gz
|
||||||
before_script: *nix-before
|
before_script: *nix-before
|
||||||
|
# TODO: reintroduce working version of after_script
|
||||||
|
# after_script: &container-fail-on-nodejs
|
||||||
|
# - (ls /nix/store/ | grep -E -i '^[a-z0-9]+-nodejs-') && (echo "NodeJS remainder in container /nix/store!" 1>&2; exit 1)
|
||||||
needs:
|
needs:
|
||||||
- job: node dependencies # transitive
|
- job: node dependencies # transitive
|
||||||
artifacts: false
|
artifacts: false
|
||||||
@ -235,8 +250,10 @@ test container:
|
|||||||
stage: container:build
|
stage: container:build
|
||||||
script:
|
script:
|
||||||
- xzcat uniworx:exe:uniworx.nar.xz | nix-store --import
|
- xzcat uniworx:exe:uniworx.nar.xz | nix-store --import
|
||||||
- cp -pr --reflink=auto -L $(nix build --print-out-paths ".#uniworxTestDocker") uniworx.tar.gz
|
# - *container-remove-nodejs
|
||||||
|
- cp -p --reflink=auto -L $(nix build --print-out-paths ".#uniworxTestDocker") uniworx.tar.gz
|
||||||
before_script: *nix-before
|
before_script: *nix-before
|
||||||
|
# after_script: *container-fail-on-nodejs
|
||||||
needs:
|
needs:
|
||||||
- job: node dependencies # transitive
|
- job: node dependencies # transitive
|
||||||
artifacts: false
|
artifacts: false
|
||||||
@ -259,6 +276,128 @@ test container:
|
|||||||
interruptible: true
|
interruptible: true
|
||||||
rules: &test-release-rules
|
rules: &test-release-rules
|
||||||
- if: $CI_COMMIT_TAG =~ /^t/
|
- if: $CI_COMMIT_TAG =~ /^t/
|
||||||
|
dev container:
|
||||||
|
stage: container:build
|
||||||
|
script:
|
||||||
|
- xzcat uniworx:exe:uniworx.nar.xz | nix-store --import
|
||||||
|
- cp -p --reflink=auto -L $(nix build --print-out-paths ".#uniworxDevDocker") uniworx.tar.gz
|
||||||
|
before_script: *nix-before
|
||||||
|
needs:
|
||||||
|
- job: node dependencies # transitive
|
||||||
|
artifacts: false
|
||||||
|
- job: well known # transitive
|
||||||
|
artifacts: false
|
||||||
|
- job: frontend # tranitive
|
||||||
|
artifacts: false
|
||||||
|
- job: uniworx:lib:uniworx # transitive
|
||||||
|
artifacts: false
|
||||||
|
- job: uniworx:exe:uniworx
|
||||||
|
artifacts: true
|
||||||
|
- job: check # sanity
|
||||||
|
artifacts: false
|
||||||
|
artifacts:
|
||||||
|
paths:
|
||||||
|
- uniworx.tar.gz
|
||||||
|
name: "${CI_JOB_NAME}-${CI_COMMIT_SHORT_SHA}"
|
||||||
|
expire_in: "1 day"
|
||||||
|
retry: 2
|
||||||
|
interruptible: true
|
||||||
|
rules: &dev-release-rules
|
||||||
|
- if: $CI_COMMIT_TAG =~ /^d/
|
||||||
|
|
||||||
|
sanitize container:
|
||||||
|
stage: container:build
|
||||||
|
needs:
|
||||||
|
- job: node dependencies # transitive
|
||||||
|
artifacts: false
|
||||||
|
- job: well known # transitive
|
||||||
|
artifacts: false
|
||||||
|
- job: frontend # transitive
|
||||||
|
artifacts: false
|
||||||
|
- job: uniworx:lib:uniworx # transitive
|
||||||
|
artifacts: false
|
||||||
|
- job: uniworx:exe:uniworx # transitive
|
||||||
|
artifacts: false
|
||||||
|
- job: check # sanity
|
||||||
|
artifacts: false
|
||||||
|
- job: container
|
||||||
|
artifacts: true
|
||||||
|
before_script: *nix-before
|
||||||
|
script:
|
||||||
|
- nix shell nixpkgs#perl --command ./.gitlab-ci/sanitize-docker.pl
|
||||||
|
after_script:
|
||||||
|
- tar xzvf uniworx-sanitized.tar.gz
|
||||||
|
- for i in `tar tf */layer.tar | grep 'nix/store/[0-9a-z]*-nodejs'`; do echo "NodeJS remainer found in /nix/store!"; echo "$i"; exit 1; done
|
||||||
|
artifacts:
|
||||||
|
paths:
|
||||||
|
- uniworx-sanitized.tar.gz
|
||||||
|
name: "${CI_JOB_NAME}-${CI_COMMIT_SHORT_SHA}"
|
||||||
|
expire_in: "1 day"
|
||||||
|
retry: 2
|
||||||
|
interruptible: true
|
||||||
|
rules: *release-rules
|
||||||
|
sanitize test container:
|
||||||
|
stage: container:build
|
||||||
|
needs:
|
||||||
|
- job: node dependencies # transitive
|
||||||
|
artifacts: false
|
||||||
|
- job: well known # transitive
|
||||||
|
artifacts: false
|
||||||
|
- job: frontend # transitive
|
||||||
|
artifacts: false
|
||||||
|
- job: uniworx:lib:uniworx # transitive
|
||||||
|
artifacts: false
|
||||||
|
- job: uniworx:exe:uniworx # transitive
|
||||||
|
artifacts: false
|
||||||
|
- job: check # sanity
|
||||||
|
artifacts: false
|
||||||
|
- job: test container
|
||||||
|
artifacts: true
|
||||||
|
before_script: *nix-before
|
||||||
|
script:
|
||||||
|
- nix shell nixpkgs#perl --command ./.gitlab-ci/sanitize-docker.pl
|
||||||
|
after_script:
|
||||||
|
- tar xzvf uniworx-sanitized.tar.gz
|
||||||
|
- for i in `tar tf */layer.tar | grep 'nix/store/[0-9a-z]*-nodejs'`; do echo "NodeJS remainer found in /nix/store!"; echo "$i"; exit 1; done
|
||||||
|
artifacts:
|
||||||
|
paths:
|
||||||
|
- uniworx-sanitized.tar.gz
|
||||||
|
name: "${CI_JOB_NAME}-${CI_COMMIT_SHORT_SHA}"
|
||||||
|
expire_in: "1 day"
|
||||||
|
retry: 2
|
||||||
|
interruptible: true
|
||||||
|
rules: *test-release-rules
|
||||||
|
sanitize dev container:
|
||||||
|
stage: container:build
|
||||||
|
needs:
|
||||||
|
- job: node dependencies # transitive
|
||||||
|
artifacts: false
|
||||||
|
- job: well known # transitive
|
||||||
|
artifacts: false
|
||||||
|
- job: frontend # transitive
|
||||||
|
artifacts: false
|
||||||
|
- job: uniworx:lib:uniworx # transitive
|
||||||
|
artifacts: false
|
||||||
|
- job: uniworx:exe:uniworx # transitive
|
||||||
|
artifacts: false
|
||||||
|
- job: check # sanity
|
||||||
|
artifacts: false
|
||||||
|
- job: dev container
|
||||||
|
artifacts: true
|
||||||
|
before_script: *nix-before
|
||||||
|
script:
|
||||||
|
- nix shell nixpkgs#perl --command ./.gitlab-ci/sanitize-docker.pl
|
||||||
|
after_script:
|
||||||
|
- tar xzvf uniworx-sanitized.tar.gz
|
||||||
|
- for i in `tar tf */layer.tar | grep 'nix/store/[0-9a-z]*-nodejs'`; do echo "NodeJS remainer found in /nix/store!"; echo "$i"; exit 1; done
|
||||||
|
artifacts:
|
||||||
|
paths:
|
||||||
|
- uniworx-sanitized.tar.gz
|
||||||
|
name: "${CI_JOB_NAME}-${CI_COMMIT_SHORT_SHA}"
|
||||||
|
expire_in: "1 day"
|
||||||
|
retry: 2
|
||||||
|
interruptible: true
|
||||||
|
rules: *dev-release-rules
|
||||||
|
|
||||||
parse changelog:
|
parse changelog:
|
||||||
stage: prepare release
|
stage: prepare release
|
||||||
@ -311,25 +450,27 @@ upload container:
|
|||||||
stage: release
|
stage: release
|
||||||
image: quay.io/skopeo/stable:latest
|
image: quay.io/skopeo/stable:latest
|
||||||
script:
|
script:
|
||||||
- skopeo --insecure-policy copy --dest-creds "${CI_REGISTRY_USER}:${CI_JOB_TOKEN}" docker-archive://$(pwd)/uniworx.tar.gz docker://${CI_REGISTRY_IMAGE}:${VERSION}
|
- skopeo --insecure-policy copy --dest-creds "${CI_REGISTRY_USER}:${CI_JOB_TOKEN}" docker-archive://$(pwd)/uniworx-sanitized.tar.gz docker://${CI_REGISTRY_IMAGE}:${VERSION}
|
||||||
- skopeo --insecure-policy copy --src-creds "${CI_REGISTRY_USER}:${CI_JOB_TOKEN}" --dest-creds "${CI_REGISTRY_USER}:${CI_JOB_TOKEN}" docker://${CI_REGISTRY_IMAGE}:${VERSION} docker://${CI_REGISTRY_IMAGE}:latest
|
- skopeo --insecure-policy copy --src-creds "${CI_REGISTRY_USER}:${CI_JOB_TOKEN}" --dest-creds "${CI_REGISTRY_USER}:${CI_JOB_TOKEN}" docker://${CI_REGISTRY_IMAGE}:${VERSION} docker://${CI_REGISTRY_IMAGE}:latest
|
||||||
needs:
|
needs:
|
||||||
- job: node dependencies # transitive
|
- job: node dependencies # transitive
|
||||||
artifacts: false
|
artifacts: false
|
||||||
- job: well known # transitive
|
- job: well known # transitive
|
||||||
artifacts: false
|
artifacts: false
|
||||||
- job: frontend # tranitive
|
- job: frontend # transitive
|
||||||
artifacts: false
|
artifacts: false
|
||||||
- job: uniworx:lib:uniworx # transitive
|
- job: uniworx:lib:uniworx # transitive
|
||||||
artifacts: false
|
artifacts: false
|
||||||
- job: uniworx:exe:uniworx # transitive
|
- job: uniworx:exe:uniworx # transitive
|
||||||
artifacts: false
|
artifacts: false
|
||||||
- job: container
|
- job: container # transitive
|
||||||
artifacts: true
|
artifacts: false
|
||||||
- job: parse changelog
|
- job: parse changelog
|
||||||
artifacts: true
|
artifacts: true
|
||||||
- job: check # sanity
|
- job: check # sanity
|
||||||
artifacts: false
|
artifacts: false
|
||||||
|
- job: sanitize container
|
||||||
|
artifacts: true
|
||||||
rules: *release-rules
|
rules: *release-rules
|
||||||
retry: 2
|
retry: 2
|
||||||
upload test container:
|
upload test container:
|
||||||
@ -338,27 +479,56 @@ upload test container:
|
|||||||
stage: release
|
stage: release
|
||||||
image: quay.io/skopeo/stable:latest
|
image: quay.io/skopeo/stable:latest
|
||||||
script:
|
script:
|
||||||
- skopeo --insecure-policy copy --dest-creds "${CI_REGISTRY_USER}:${CI_JOB_TOKEN}" docker-archive://$(pwd)/uniworx.tar.gz docker://${CI_REGISTRY}/fradrive/fradrive/test:${CI_COMMIT_REF_NAME}
|
- skopeo --insecure-policy copy --dest-creds "${CI_REGISTRY_USER}:${CI_JOB_TOKEN}" docker-archive://$(pwd)/uniworx-sanitized.tar.gz docker://${CI_REGISTRY}/fradrive/fradrive/test:${CI_COMMIT_REF_NAME}
|
||||||
- skopeo --insecure-policy copy --src-creds "${CI_REGISTRY_USER}:${CI_JOB_TOKEN}" --dest-creds "${CI_REGISTRY_USER}:${CI_JOB_TOKEN}" docker://${CI_REGISTRY}/fradrive/fradrive/test:${CI_COMMIT_REF_NAME} docker://${CI_REGISTRY}/fradrive/fradrive/test:latest
|
- skopeo --insecure-policy copy --src-creds "${CI_REGISTRY_USER}:${CI_JOB_TOKEN}" --dest-creds "${CI_REGISTRY_USER}:${CI_JOB_TOKEN}" docker://${CI_REGISTRY}/fradrive/fradrive/test:${CI_COMMIT_REF_NAME} docker://${CI_REGISTRY}/fradrive/fradrive/test:latest
|
||||||
needs:
|
needs:
|
||||||
- job: node dependencies # transitive
|
- job: node dependencies # transitive
|
||||||
artifacts: false
|
artifacts: false
|
||||||
- job: well known # transitive
|
- job: well known # transitive
|
||||||
artifacts: false
|
artifacts: false
|
||||||
- job: frontend # tranitive
|
- job: frontend # transitive
|
||||||
artifacts: false
|
artifacts: false
|
||||||
- job: uniworx:lib:uniworx # transitive
|
- job: uniworx:lib:uniworx # transitive
|
||||||
artifacts: false
|
artifacts: false
|
||||||
- job: uniworx:exe:uniworx # transitive
|
- job: uniworx:exe:uniworx # transitive
|
||||||
artifacts: false
|
artifacts: false
|
||||||
- job: test container
|
- job: test container # transitive
|
||||||
artifacts: true
|
artifacts: false
|
||||||
- job: parse test changelog
|
- job: parse test changelog
|
||||||
artifacts: true
|
artifacts: true
|
||||||
- job: check # sanity
|
- job: check # sanity
|
||||||
artifacts: false
|
artifacts: false
|
||||||
|
- job: sanitize test container
|
||||||
|
artifacts: true
|
||||||
rules: *test-release-rules
|
rules: *test-release-rules
|
||||||
retry: 2
|
retry: 2
|
||||||
|
upload dev container:
|
||||||
|
variables:
|
||||||
|
GIT_STRATEGY: none
|
||||||
|
stage: release
|
||||||
|
image: quay.io/skopeo/stable:latest
|
||||||
|
script:
|
||||||
|
- skopeo --insecure-policy copy --dest-creds "${CI_REGISTRY_USER}:${CI_JOB_TOKEN}" docker-archive://$(pwd)/uniworx-sanitized.tar.gz docker://${CI_REGISTRY}/fradrive/fradrive/dev:${CI_COMMIT_REF_NAME}
|
||||||
|
- skopeo --insecure-policy copy --src-creds "${CI_REGISTRY_USER}:${CI_JOB_TOKEN}" --dest-creds "${CI_REGISTRY_USER}:${CI_JOB_TOKEN}" docker://${CI_REGISTRY}/fradrive/fradrive/dev:${CI_COMMIT_REF_NAME} docker://${CI_REGISTRY}/fradrive/fradrive/dev:latest
|
||||||
|
needs:
|
||||||
|
- job: node dependencies # transitive
|
||||||
|
artifacts: false
|
||||||
|
- job: well known # transitive
|
||||||
|
artifacts: false
|
||||||
|
- job: frontend # transitive
|
||||||
|
artifacts: false
|
||||||
|
- job: uniworx:lib:uniworx # transitive
|
||||||
|
artifacts: false
|
||||||
|
- job: uniworx:exe:uniworx # transitive
|
||||||
|
artifacts: false
|
||||||
|
- job: dev container # transitive
|
||||||
|
artifacts: false
|
||||||
|
- job: check # sanity
|
||||||
|
artifacts: false
|
||||||
|
- job: sanitize dev container
|
||||||
|
artifacts: true
|
||||||
|
rules: *dev-release-rules
|
||||||
|
retry: 2
|
||||||
|
|
||||||
release:
|
release:
|
||||||
variables:
|
variables:
|
||||||
@ -394,3 +564,18 @@ test release:
|
|||||||
artifacts: false
|
artifacts: false
|
||||||
- job: parse test changelog
|
- job: parse test changelog
|
||||||
artifacts: true
|
artifacts: true
|
||||||
|
dev release:
|
||||||
|
variables:
|
||||||
|
GIT_STRATEGY: none
|
||||||
|
stage: release
|
||||||
|
image: registry.gitlab.com/gitlab-org/release-cli:latest
|
||||||
|
rules: *dev-release-rules
|
||||||
|
script:
|
||||||
|
- echo "Will create dev release ${VERSION}-dev..."
|
||||||
|
release:
|
||||||
|
name: "${VERSION}-dev"
|
||||||
|
tag_name: '$CI_COMMIT_TAG'
|
||||||
|
description: .current-changelog.md
|
||||||
|
needs:
|
||||||
|
- job: check # sanity
|
||||||
|
artifacts: false
|
||||||
|
|||||||
272
.gitlab-ci/sanitize-docker.pl
Executable file
272
.gitlab-ci/sanitize-docker.pl
Executable file
@ -0,0 +1,272 @@
|
|||||||
|
#!/usr/bin/env perl
|
||||||
|
|
||||||
|
use strict;
|
||||||
|
use warnings;
|
||||||
|
|
||||||
|
use Data::Dumper;
|
||||||
|
|
||||||
|
print "Sanitize script for node removal from container.\n";
|
||||||
|
|
||||||
|
system("pwd");
|
||||||
|
{
|
||||||
|
my @l = (".","..");
|
||||||
|
for(1..8) {
|
||||||
|
push @l, (("../" x $_)."..")
|
||||||
|
}
|
||||||
|
for(@l) {
|
||||||
|
my $cmd = "ls -ld $_";
|
||||||
|
print "running: $cmd\n";
|
||||||
|
system $cmd;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
my $tmpdir = "tmp-sanitize";
|
||||||
|
|
||||||
|
die "Has already run, abort" if -e $tmpdir;
|
||||||
|
|
||||||
|
mkdir $tmpdir;
|
||||||
|
|
||||||
|
chmodWrap(0755, $tmpdir);
|
||||||
|
chdir($tmpdir);
|
||||||
|
system("ln -s ../uniworx.tar.gz .");
|
||||||
|
system("tar xzvf uniworx.tar.gz");
|
||||||
|
chmodWrap(0755, '.'); # tar can change the rights of '.' if it contains an entry for '.' with other rights
|
||||||
|
|
||||||
|
my %truerights = ();
|
||||||
|
storeRightsMake7(".");
|
||||||
|
|
||||||
|
#print "=== Extended rights:\n";
|
||||||
|
#system("ls -l *");
|
||||||
|
#resetRights(".");
|
||||||
|
#print "=== Reset rights:\n";
|
||||||
|
#system("ls -l *");
|
||||||
|
|
||||||
|
sub chmodWrap {
|
||||||
|
my ($mode, $fn) = @_;
|
||||||
|
my $tries = 0;
|
||||||
|
die "file '$fn' does not exist; cannot change its permissions to $mode" unless -e $fn;
|
||||||
|
RIGHTS: {
|
||||||
|
chmod($mode, $fn);
|
||||||
|
my $ismode = (stat($fn))[2];
|
||||||
|
my $fm = $ismode % 512;
|
||||||
|
if($fm != $mode) {
|
||||||
|
if($tries++ > 20) {
|
||||||
|
die "Problem with file permissions, abort"
|
||||||
|
}
|
||||||
|
warn sprintf "File rights were meant to be set, but were not updated properly for file '%s', is %03o but was set to %03o; try again in 1 second";
|
||||||
|
sleep 1;
|
||||||
|
redo RIGHTS;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#
|
||||||
|
sub storeRightsMake7 {
|
||||||
|
my ($pwd) = @_;
|
||||||
|
my $dh = undef;
|
||||||
|
opendir($dh, $pwd) or die "Could not read dir '$pwd', because: $!";
|
||||||
|
while(my $fn = readdir($dh)) {
|
||||||
|
next if $fn=~m#^\.\.?$#;
|
||||||
|
#perl -le 'my $dh = undef;opendir($dh, ".");while(my $fn = readdir($dh)) { my $mode = (stat($fn))[2];my $fm = $mode % 512;my $fmo=sprintf("%03o",$fm);print "$fn -> $fmo" }'
|
||||||
|
my $fullname = "$pwd/$fn";
|
||||||
|
my $mode = (stat($fullname))[2];
|
||||||
|
my $fm = $mode % 512;
|
||||||
|
#my $fmo = sprintf("%03o",$fm);
|
||||||
|
$truerights{$fullname} = $fm;
|
||||||
|
chmodWrap(($fm | 0700), $fullname);
|
||||||
|
storeRightsMake7($fullname) if -d $fullname;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sub resetRights {
|
||||||
|
my ($pwd) = @_;
|
||||||
|
print "Resetting rights to:\n" if '.' eq $pwd;
|
||||||
|
print Data::Dumper::Dumper(\%truerights);
|
||||||
|
my $dh = undef;
|
||||||
|
opendir($dh, $pwd) or die "Could not read dir '$pwd', because: $!";
|
||||||
|
while(my $fn = readdir($dh)) {
|
||||||
|
next if $fn=~m#^\.\.?$#;
|
||||||
|
#perl -le 'my $dh = undef;opendir($dh, ".");while(my $fn = readdir($dh)) { my $mode = (stat($fn))[2];my $fm = $mode % 512;my $fmo=sprintf("%03o",$fm);print "$fn -> $fmo" }'
|
||||||
|
my $fullname = "$pwd/$fn";
|
||||||
|
printf(" set rights of '$fullname' back to %03o\n", $truerights{$fullname});
|
||||||
|
chmodWrap($truerights{$fullname}, $fullname);
|
||||||
|
resetRights($fullname) if -d $fullname;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sub renameWithRights {
|
||||||
|
my ($from, $to) = @_;
|
||||||
|
print " rename file '$from' to '$to'\n";
|
||||||
|
my %oldrights = %truerights;
|
||||||
|
%truerights = ();
|
||||||
|
while(my ($k,$v) = each %oldrights) {
|
||||||
|
$k =~ s#^\./\Q$from\E#./$to#;
|
||||||
|
$truerights{$k} = $v;
|
||||||
|
}
|
||||||
|
#my $rights = $truerights{$from};
|
||||||
|
#delete $truerights{$from};
|
||||||
|
rename($from, $to) or die "Could not rename '$from' to '$to', because $!";
|
||||||
|
my $waittimer = 20;
|
||||||
|
while(-e $from || not(-e $to) and $waittimer-- > 0) {
|
||||||
|
sleep 1
|
||||||
|
}
|
||||||
|
die "rename file from '$from' to '$to', but it is still there" if -e $from;
|
||||||
|
die "rename file from '$from' to '$to', but there is no file under the new name" unless -e $to;
|
||||||
|
#$truerights{$to} = $rights
|
||||||
|
}
|
||||||
|
|
||||||
|
print Data::Dumper::Dumper(\%truerights);
|
||||||
|
#exit 0;
|
||||||
|
|
||||||
|
# Checksummen:
|
||||||
|
# outerjson c27f -- toplevel $outerjson.json, by sha256sum $outerjson.json
|
||||||
|
# imageid d940 -- toplevel verzeichnis mit der layer darin; doc says: Each image’s ID is given by the SHA256 hash of its configuration JSON.
|
||||||
|
# we'll try as configuration "remove nodejs $oldhash"
|
||||||
|
# or we just use a random number ;)
|
||||||
|
# layertar fd3d -- doc says: Each image’s ID is given by the SHA256 hash of its configuration JSON.
|
||||||
|
#
|
||||||
|
##### FOUND
|
||||||
|
# outerjson c27f64c8de183296ef409baecc27ddac8cd4065aac760b1b512caf482ad782dd -- in manifest.json
|
||||||
|
# imageid d940253667b5ab47060e8bf537bd5b3e66a2447978f3c784a22b115a262fccbf -- in manifest.json
|
||||||
|
# imageid d940253667b5ab47060e8bf537bd5b3e66a2447978f3c784a22b115a262fccbf -- as toplevel dirname
|
||||||
|
# outerjson c27f64c8de183296ef409baecc27ddac8cd4065aac760b1b512caf482ad782dd -- as toplevel filename
|
||||||
|
# imageid d940253667b5ab47060e8bf537bd5b3e66a2447978f3c784a22b115a262fccbf -- in $layerdir/json
|
||||||
|
# layertar fd3d3cdf4ece09864ac933aa664eb5f397cf5ca28652125addd689726f8485cd -- in $outerjson.json
|
||||||
|
#
|
||||||
|
#
|
||||||
|
##### COMPUTE
|
||||||
|
# toplevel
|
||||||
|
# outerjson c27f64c8de183296ef409baecc27ddac8cd4065aac760b1b512caf482ad782dd $outerjson.json
|
||||||
|
# b21db3fcc85b23d91067a2a5834e114ca9eec0364742c8680546f040598d8cd9 manifest.json
|
||||||
|
# 238f234e3a1ddb27a034f4ee1e59735175741e5cc05673b5dd41d9a42bac2ebd uniworx.tar.gz
|
||||||
|
# in $layerdir/
|
||||||
|
# 028c1e8d9688b420f7316bb44ce0e26f4712dc21ef93c5af8000c102b1405ad4 json
|
||||||
|
# layertar fd3d3cdf4ece09864ac933aa664eb5f397cf5ca28652125addd689726f8485cd layer.tar
|
||||||
|
# d0ff5974b6aa52cf562bea5921840c032a860a91a3512f7fe8f768f6bbe005f6 VERSION
|
||||||
|
#
|
||||||
|
#
|
||||||
|
# sha256sum layer.tar fd3d3cdf4ece09864ac933aa664eb5f397cf5ca28652125addd689726f8485cd
|
||||||
|
|
||||||
|
my ($outerjson, $imageid) = ();
|
||||||
|
|
||||||
|
{
|
||||||
|
my $dirh = undef;
|
||||||
|
opendir($dirh, '.') or die "Could not read dir '.', because: $!";
|
||||||
|
while(my $fn = readdir($dirh)) {
|
||||||
|
next if $fn=~m#^\.#;
|
||||||
|
if($fn=~m#(.{16,})\.json#) { # it shall match on hash sums but not for example on manifest.json
|
||||||
|
$outerjson = $1;
|
||||||
|
next
|
||||||
|
}
|
||||||
|
if($fn=~m#^[0-9a-f]{64}$#) {
|
||||||
|
$imageid = $fn
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
die "Bad archive, could not found expected files and directories" unless defined($outerjson) and defined($imageid);
|
||||||
|
|
||||||
|
#system("pwd");
|
||||||
|
#print "will run: sha256sum $imageid/layer.tar\n";
|
||||||
|
|
||||||
|
my $oldLayerdir = qx(sha256sum $imageid/layer.tar);
|
||||||
|
#print "oldLayerdir is for now $oldLayerdir\n\n";
|
||||||
|
$oldLayerdir =~ m#^([0-9a-f]{64}).*$# or die "layer.tar not found or sha256sum not installed!";
|
||||||
|
$oldLayerdir = $1;
|
||||||
|
|
||||||
|
# tar --delete --file layer.tar nix/store/cdalbhzm3z4gz07wyg89maprdbjc4yah-nodejs-14.17.0
|
||||||
|
my $layerContent = qx(tar -tf $imageid/layer.tar);
|
||||||
|
|
||||||
|
my @rms = $layerContent=~m#^((?:\./)?nix/store/[a-z0-9]+-(?:nodejs|openjdk|ghc)-[^/]+/)$#gm;
|
||||||
|
|
||||||
|
print "rm <<$_>>\n" for @rms;
|
||||||
|
|
||||||
|
system("tar --delete --file $imageid/layer.tar '$_'") for @rms;
|
||||||
|
|
||||||
|
|
||||||
|
### Deconstruction finished, now lets put everything together again after fixing the checksums
|
||||||
|
|
||||||
|
|
||||||
|
my $newImageId = qx(echo 'remove nodejs $imageid' | sha256sum);
|
||||||
|
$newImageId =~ m#^([0-9a-f]{64}).*$# or die "sha256sum not installed!";
|
||||||
|
$newImageId = $1;
|
||||||
|
|
||||||
|
my $newLayerdir = qx(sha256sum $imageid/layer.tar);
|
||||||
|
$newLayerdir =~ m#^([0-9a-f]{64}).*$# or die "sha256sum not installed!";
|
||||||
|
$newLayerdir = $1;
|
||||||
|
|
||||||
|
# new outerjson is computed later, as we first have to change its content
|
||||||
|
|
||||||
|
sub cautionWaiter {
|
||||||
|
# some file operations give the impression that they are not instant.
|
||||||
|
# Hence, we wait here a bit to see if that fixes stuff
|
||||||
|
#sleep 5; # seems not to be the reason
|
||||||
|
}
|
||||||
|
|
||||||
|
sub replaceInFile {
|
||||||
|
my ($filename, $replacer) = @_;
|
||||||
|
return unless -e $filename;
|
||||||
|
my $fh = undef;
|
||||||
|
open($fh, '<', $filename) or die "Could not read $filename, because: $!";
|
||||||
|
my $content = join '', <$fh>;
|
||||||
|
close $fh;
|
||||||
|
keys %$replacer;
|
||||||
|
while(my ($k,$v) = each %$replacer) {
|
||||||
|
$content=~s#\Q$k\E#$v#g;
|
||||||
|
}
|
||||||
|
my $wh = undef;
|
||||||
|
open($wh, '>', $filename) or die "Could not write $filename, because: $!";
|
||||||
|
print $wh $content;
|
||||||
|
close $wh;
|
||||||
|
}
|
||||||
|
|
||||||
|
my %replacer = (
|
||||||
|
$oldLayerdir => $newLayerdir,
|
||||||
|
$imageid => $newImageId,
|
||||||
|
);
|
||||||
|
|
||||||
|
replaceInFile("$imageid/json", \%replacer);
|
||||||
|
replaceInFile("$outerjson.json", \%replacer);
|
||||||
|
|
||||||
|
cautionWaiter();
|
||||||
|
|
||||||
|
my $newOuterjson = qx(sha256sum '$outerjson.json');
|
||||||
|
$newOuterjson =~ m#^([0-9a-f]{64}).*$# or die "sha256sum not installed!";
|
||||||
|
$newOuterjson = $1;
|
||||||
|
|
||||||
|
cautionWaiter();
|
||||||
|
|
||||||
|
renameWithRights("$outerjson.json", "$newOuterjson.json");
|
||||||
|
$replacer{$outerjson} = $newOuterjson;
|
||||||
|
|
||||||
|
replaceInFile("manifest.json", \%replacer);
|
||||||
|
|
||||||
|
replaceInFile("repositories", \%replacer);
|
||||||
|
|
||||||
|
cautionWaiter();
|
||||||
|
renameWithRights($imageid, $newImageId);
|
||||||
|
|
||||||
|
cautionWaiter();
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
resetRights(".");
|
||||||
|
|
||||||
|
|
||||||
|
system("find");
|
||||||
|
|
||||||
|
unlink("uniworx.tar.gz");
|
||||||
|
|
||||||
|
system("tar czvf uniwox-rmnodejs.tar.gz *");
|
||||||
|
|
||||||
|
cautionWaiter();
|
||||||
|
print "Debug output, content of container:\n";
|
||||||
|
system("tar tzvf uniwox-rmnodejs.tar.gz");
|
||||||
|
|
||||||
|
cautionWaiter();
|
||||||
|
#unlink("../uniworx.tar.gz");
|
||||||
|
|
||||||
|
system("cp uniwox-rmnodejs.tar.gz ../uniworx-sanitized.tar.gz");
|
||||||
|
|
||||||
64
.ports/assign.hs
Normal file
64
.ports/assign.hs
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
-- SPDX-FileCopyrightText: 2024 David Mosbach <david.mosbach@uniworx.de>
|
||||||
|
--
|
||||||
|
-- SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
|
{-# Language OverloadedStrings, LambdaCase, TypeApplications #-}
|
||||||
|
|
||||||
|
import Data.Text (Text)
|
||||||
|
import qualified Data.Text as T
|
||||||
|
import System.Directory
|
||||||
|
import System.Environment
|
||||||
|
import System.IO
|
||||||
|
|
||||||
|
main :: IO ()
|
||||||
|
main = getArgs >>= \case
|
||||||
|
["--assign", offsetFile] -> parseOffsets offsetFile >>= uncurry nextOffset
|
||||||
|
["--remove", offset] -> removeOffset offset
|
||||||
|
_ -> fail "unsupported args"
|
||||||
|
|
||||||
|
parseOffsets :: FilePath -> IO (Int,Int)
|
||||||
|
parseOffsets offsetFile = do
|
||||||
|
user <- T.pack <$> getEnv "USER"
|
||||||
|
let pred x = "//" `T.isPrefixOf` x || T.null (T.strip x)
|
||||||
|
tokenise = map (filter (not . pred) . T.lines) . T.split (=='#')
|
||||||
|
extract = map tail . filter (\u -> not (null u) && user == (T.strip $ head u))
|
||||||
|
((extract . tokenise . T.pack) <$> readFile offsetFile) >>= \case
|
||||||
|
[[min,max]] -> return (read $ T.unpack min, read $ T.unpack max)
|
||||||
|
x -> print x >> fail "malformed offset file"
|
||||||
|
|
||||||
|
nextOffset :: Int -> Int -> IO ()
|
||||||
|
nextOffset min max
|
||||||
|
| min > max = nextOffset max min
|
||||||
|
| otherwise = do
|
||||||
|
home <- getEnv "HOME"
|
||||||
|
offset <- findFile [home] ".port-offsets" >>= \case
|
||||||
|
Nothing -> writeFile (home ++ "/.port-offsets") (show min) >> return min
|
||||||
|
Just path -> do
|
||||||
|
used <- (map (read @Int) . filter (not . null) . lines) <$> readFile path
|
||||||
|
o <- next min max used
|
||||||
|
appendFile path ('\n' : show o)
|
||||||
|
return o
|
||||||
|
print offset
|
||||||
|
where
|
||||||
|
next :: Int -> Int -> [Int] -> IO Int
|
||||||
|
next min max used
|
||||||
|
| min > max = fail "all offsets currently in use"
|
||||||
|
| min `elem` used = next (min+1) max used
|
||||||
|
| otherwise = return min
|
||||||
|
|
||||||
|
removeOffset :: String -> IO ()
|
||||||
|
removeOffset offset = do
|
||||||
|
home <- getEnv "HOME"
|
||||||
|
findFile [home] ".port-offsets" >>= \case
|
||||||
|
Nothing -> fail "offset file does not exist"
|
||||||
|
Just path -> do
|
||||||
|
remaining <- (filter (/= offset) . lines) <$> readFile path
|
||||||
|
run <- getEnv "XDG_RUNTIME_DIR"
|
||||||
|
(tempPath, fh) <- openTempFile run ".port-offsets"
|
||||||
|
let out = unlines remaining
|
||||||
|
hPutStr fh $ out
|
||||||
|
case T.null (T.strip $ T.pack out) of
|
||||||
|
True -> removeFile path
|
||||||
|
False -> writeFile path $ out
|
||||||
|
removeFile tempPath
|
||||||
|
|
||||||
24
.ports/offsets
Normal file
24
.ports/offsets
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2024 David Mosbach <david.mosbach@uniworx.de>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
|
# gkleen
|
||||||
|
-1000
|
||||||
|
-950
|
||||||
|
|
||||||
|
# ishka
|
||||||
|
-949
|
||||||
|
-899
|
||||||
|
|
||||||
|
# jost
|
||||||
|
-898
|
||||||
|
-848
|
||||||
|
|
||||||
|
# mosbach
|
||||||
|
-847
|
||||||
|
-797
|
||||||
|
|
||||||
|
# savau
|
||||||
|
-796
|
||||||
|
-746
|
||||||
|
|
||||||
1863
CHANGELOG.md
1863
CHANGELOG.md
File diff suppressed because it is too large
Load Diff
@ -1,4 +1,4 @@
|
|||||||
# SPDX-FileCopyrightText: 2022 Gregor Kleen <gregor.kleen@ifi.lmu.de>,Sarah Vaupel <sarah.vaupel@ifi.lmu.de>,Steffen Jost <jost@tcs.ifi.lmu.de>,Wolfgang Witt <Wolfgang.Witt@campus.lmu.de>
|
# SPDX-FileCopyrightText: 2022-2024 Sarah Vaupel <sarah.vaupel@uniworx.de>, David Mosbach <david.mosbach@uniworx.de>, Gregor Kleen <gregor.kleen@ifi.lmu.de>, Sarah Vaupel <sarah.vaupel@ifi.lmu.de>, Steffen Jost <jost@tcs.ifi.lmu.de>, Wolfgang Witt <Wolfgang.Witt@campus.lmu.de>
|
||||||
#
|
#
|
||||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
@ -24,9 +24,9 @@ mail-from:
|
|||||||
email: "_env:MAILFROM_EMAIL:uniworx@localhost"
|
email: "_env:MAILFROM_EMAIL:uniworx@localhost"
|
||||||
mail-object-domain: "_env:MAILOBJECT_DOMAIN:localhost"
|
mail-object-domain: "_env:MAILOBJECT_DOMAIN:localhost"
|
||||||
mail-use-replyto-instead-sender: "_env:MAIL_USES_REPLYTO:true"
|
mail-use-replyto-instead-sender: "_env:MAIL_USES_REPLYTO:true"
|
||||||
mail-reroute-to:
|
mail-reroute-to:
|
||||||
name: "_env:MAIL_REROUTE_TO_NAME:"
|
name: "_env:MAIL_REROUTE_TO_NAME:"
|
||||||
email: "_env:MAIL_REROUTE_TO_EMAIL:"
|
email: "_env:MAIL_REROUTE_TO_EMAIL:"
|
||||||
#mail-verp:
|
#mail-verp:
|
||||||
# separator: "_env:VERP_SEPARATOR:+"
|
# separator: "_env:VERP_SEPARATOR:+"
|
||||||
# prefix: "_env:VERP_PREFIX:bounce"
|
# prefix: "_env:VERP_PREFIX:bounce"
|
||||||
@ -45,7 +45,7 @@ legal-external:
|
|||||||
imprint: "https://www.fraport.com/de/tools/impressum.html"
|
imprint: "https://www.fraport.com/de/tools/impressum.html"
|
||||||
data-protection: "https://www.fraport.com/de/konzern/datenschutz.html"
|
data-protection: "https://www.fraport.com/de/konzern/datenschutz.html"
|
||||||
terms-of-use: "https://www.fraport.com/de/tools/disclaimer.html"
|
terms-of-use: "https://www.fraport.com/de/tools/disclaimer.html"
|
||||||
payments: "https://www.fraport.com/de/geschaeftsfelder/service/geschaeftspartner/richtlinien-und-zahlungsbedingungen.html"
|
payments: "https://www.fraport.com/de/geschaeftsfelder/service/geschaeftspartner/richtlinien-und-zahlungsbedingungen.html"
|
||||||
|
|
||||||
job-workers: "_env:JOB_WORKERS:10"
|
job-workers: "_env:JOB_WORKERS:10"
|
||||||
job-flush-interval: "_env:JOB_FLUSH:30"
|
job-flush-interval: "_env:JOB_FLUSH:30"
|
||||||
@ -66,7 +66,7 @@ keep-unreferenced-files: 86400
|
|||||||
health-check-interval:
|
health-check-interval:
|
||||||
matching-cluster-config: "_env:HEALTHCHECK_INTERVAL_MATCHING_CLUSTER_CONFIG:600"
|
matching-cluster-config: "_env:HEALTHCHECK_INTERVAL_MATCHING_CLUSTER_CONFIG:600"
|
||||||
http-reachable: "_env:HEALTHCHECK_INTERVAL_HTTP_REACHABLE:600"
|
http-reachable: "_env:HEALTHCHECK_INTERVAL_HTTP_REACHABLE:600"
|
||||||
ldap-admins: "_env:HEALTHCHECK_INTERVAL_LDAP_ADMINS:600"
|
ldap-admins: "_env:HEALTHCHECK_INTERVAL_LDAP_ADMINS:600" # TODO: either generalize over every external auth sources, or otherwise reimplement for different semantics
|
||||||
smtp-connect: "_env:HEALTHCHECK_INTERVAL_SMTP_CONNECT:600"
|
smtp-connect: "_env:HEALTHCHECK_INTERVAL_SMTP_CONNECT:600"
|
||||||
widget-memcached: "_env:HEALTHCHECK_INTERVAL_WIDGET_MEMCACHED:600"
|
widget-memcached: "_env:HEALTHCHECK_INTERVAL_WIDGET_MEMCACHED:600"
|
||||||
active-job-executors: "_env:HEALTHCHECK_INTERVAL_ACTIVE_JOB_EXECUTORS:60"
|
active-job-executors: "_env:HEALTHCHECK_INTERVAL_ACTIVE_JOB_EXECUTORS:60"
|
||||||
@ -77,14 +77,10 @@ health-check-http: "_env:HEALTHCHECK_HTTP:true" # Can we assume, that we can rea
|
|||||||
health-check-active-job-executors-timeout: "_env:HEALTHCHECK_ACTIVE_JOB_EXECUTORS_TIMEOUT:5"
|
health-check-active-job-executors-timeout: "_env:HEALTHCHECK_ACTIVE_JOB_EXECUTORS_TIMEOUT:5"
|
||||||
health-check-active-widget-memcached-timeout: "_env:HEALTHCHECK_ACTIVE_WIDGET_MEMCACHED_TIMEOUT:2"
|
health-check-active-widget-memcached-timeout: "_env:HEALTHCHECK_ACTIVE_WIDGET_MEMCACHED_TIMEOUT:2"
|
||||||
health-check-smtp-connect-timeout: "_env:HEALTHCHECK_SMTP_CONNECT_TIMEOUT:5"
|
health-check-smtp-connect-timeout: "_env:HEALTHCHECK_SMTP_CONNECT_TIMEOUT:5"
|
||||||
health-check-ldap-admins-timeout: "_env:HEALTHCHECK_LDAP_ADMINS_TIMEOUT:60"
|
health-check-ldap-admins-timeout: "_env:HEALTHCHECK_LDAP_ADMINS_TIMEOUT:60" # TODO: either generalize over every external auth sources, or otherwise reimplement for different semantics
|
||||||
health-check-http-reachable-timeout: "_env:HEALTHCHECK_HTTP_REACHABLE_TIMEOUT:2"
|
health-check-http-reachable-timeout: "_env:HEALTHCHECK_HTTP_REACHABLE_TIMEOUT:2"
|
||||||
health-check-matching-cluster-config-timeout: "_env:HEALTHCHECK_MATCHING_CLUSTER_CONFIG_TIMEOUT:2"
|
health-check-matching-cluster-config-timeout: "_env:HEALTHCHECK_MATCHING_CLUSTER_CONFIG_TIMEOUT:2"
|
||||||
|
|
||||||
synchronise-ldap-users-within: "_env:SYNCHRONISE_LDAP_WITHIN:1209600" # 14 Tage in Sekunden
|
|
||||||
synchronise-ldap-users-interval: "_env:SYNCHRONISE_LDAP_INTERVAL:3600" # jede Stunde
|
|
||||||
synchronise-ldap-users-expire: "_env:SYNCHRONISE_LDAP_EXPIRE:15897600" # halbes Jahr in Sekunden
|
|
||||||
|
|
||||||
synchronise-avs-users-within: "_env:SYNCHRONISE_AVS_WITHIN:5702400" # alle 66 Tage
|
synchronise-avs-users-within: "_env:SYNCHRONISE_AVS_WITHIN:5702400" # alle 66 Tage
|
||||||
synchronise-avs-users-interval: "_env:SYNCHRONISE_AVS_INTERVAL:21600" # alle 6 Stunden
|
synchronise-avs-users-interval: "_env:SYNCHRONISE_AVS_INTERVAL:21600" # alle 6 Stunden
|
||||||
|
|
||||||
@ -92,8 +88,8 @@ study-features-recache-relevance-within: 172800
|
|||||||
study-features-recache-relevance-interval: 293
|
study-features-recache-relevance-interval: 293
|
||||||
|
|
||||||
# Enqueue at specified hour, a few minutes later
|
# Enqueue at specified hour, a few minutes later
|
||||||
job-lms-qualifications-enqueue-hour: 16
|
# job-lms-qualifications-enqueue-hour: 15
|
||||||
job-lms-qualifications-dequeue-hour: 4
|
# job-lms-qualifications-dequeue-hour: 3
|
||||||
|
|
||||||
log-settings:
|
log-settings:
|
||||||
detailed: "_env:DETAILED_LOGGING:false"
|
detailed: "_env:DETAILED_LOGGING:false"
|
||||||
@ -130,24 +126,47 @@ database:
|
|||||||
database: "_env:PGDATABASE:uniworx"
|
database: "_env:PGDATABASE:uniworx"
|
||||||
poolsize: "_env:PGPOOLSIZE:990"
|
poolsize: "_env:PGPOOLSIZE:990"
|
||||||
|
|
||||||
auto-db-migrate: '_env:AUTO_DB_MIGRATE:true'
|
auto-db-migrate: "_env:AUTO_DB_MIGRATE:true"
|
||||||
|
|
||||||
ldap:
|
# External sources used for user authentication and userdata lookups
|
||||||
- host: "_env:LDAPHOST:"
|
user-auth:
|
||||||
tls: "_env:LDAPTLS:"
|
# mode: single-source
|
||||||
port: "_env:LDAPPORT:389"
|
protocol: azureadv2
|
||||||
user: "_env:LDAPUSER:"
|
config:
|
||||||
pass: "_env:LDAPPASS:"
|
client-id: "_env:AZURECLIENTID:00000000-0000-0000-0000-000000000000"
|
||||||
baseDN: "_env:LDAPBASE:"
|
client-secret: "_env:AZURECLIENTSECRET:''"
|
||||||
scope: "_env:LDAPSCOPE:WholeSubtree"
|
tenant-id: "_env:AZURETENANTID:00000000-0000-0000-0000-000000000000"
|
||||||
timeout: "_env:LDAPTIMEOUT:5"
|
scopes: "_env:AZURESCOPES:[ID,Profile]"
|
||||||
search-timeout: "_env:LDAPSEARCHTIME:5"
|
# protocol: "ldap"
|
||||||
pool:
|
# config:
|
||||||
stripes: "_env:LDAPSTRIPES:1"
|
# host: "_env:LDAPHOST:"
|
||||||
timeout: "_env:LDAPTIMEOUT:20"
|
# tls: "_env:LDAPTLS:"
|
||||||
limit: "_env:LDAPLIMIT:10"
|
# port: "_env:LDAPPORT:389"
|
||||||
|
# user: "_env:LDAPUSER:"
|
||||||
|
# pass: "_env:LDAPPASS:"
|
||||||
|
# baseDN: "_env:LDAPBASE:"
|
||||||
|
# scope: "_env:LDAPSCOPE:WholeSubtree"
|
||||||
|
# timeout: "_env:LDAPTIMEOUT:5"
|
||||||
|
# search-timeout: "_env:LDAPSEARCHTIME:5"
|
||||||
|
|
||||||
ldap-re-test-failover: 60
|
single-sign-on: "_env:OIDC_SSO:false"
|
||||||
|
|
||||||
|
# Automatically redirect to SSO route when not signed on
|
||||||
|
# Note: This will force authentication, thus the site will be inaccessible without external credentials. Only use this option when it is ensured that every user that should be able to access the site has valid external credentials!
|
||||||
|
auto-sign-on: "_env:AUTO_SIGN_ON:false"
|
||||||
|
|
||||||
|
# TODO: generalize for arbitrary auth protocols
|
||||||
|
# TODO: maybe use separate pools for external databases?
|
||||||
|
ldap-pool:
|
||||||
|
stripes: "_env:LDAPSTRIPES:1"
|
||||||
|
timeout: "_env:LDAPTIMEOUT:20"
|
||||||
|
limit: "_env:LDAPLIMIT:10"
|
||||||
|
|
||||||
|
# TODO: reintroduce and move into failover settings once failover mode has been reimplemented
|
||||||
|
# user-retest-failover: 60
|
||||||
|
# TODO; maybe implement syncWithin and syncInterval per auth source
|
||||||
|
user-sync-within: "_env:USER_SYNC_WITHIN:1209600" # 14 Tage in Sekunden
|
||||||
|
user-sync-interval: "_env:USER_SYNC_INTERVAL:3600" # jede Stunde
|
||||||
|
|
||||||
lms-direct:
|
lms-direct:
|
||||||
upload-header: "_env:LMSUPLOADHEADER:true"
|
upload-header: "_env:LMSUPLOADHEADER:true"
|
||||||
@ -158,17 +177,15 @@ lms-direct:
|
|||||||
deletion-days: "_env:LMSDELETIONDAYS:7"
|
deletion-days: "_env:LMSDELETIONDAYS:7"
|
||||||
|
|
||||||
avs:
|
avs:
|
||||||
host: "_env:AVSHOST:skytest.fra.fraport.de"
|
host: "_env:AVSHOST:skytest.fra.fraport.de"
|
||||||
port: "_env:AVSPORT:443"
|
port: "_env:AVSPORT:443"
|
||||||
user: "_env:AVSUSER:fradrive"
|
user: "_env:AVSUSER:fradrive"
|
||||||
pass: "_env:AVSPASS:\"0000\""
|
pass: "_env:AVSPASS:"
|
||||||
timeout: "_env:AVSTIMEOUT:42"
|
|
||||||
cache-expiry: "_env:AVSCACHEEXPIRY:420"
|
|
||||||
|
|
||||||
lpr:
|
lpr:
|
||||||
host: "_env:LPRHOST:fravm017173.fra.fraport.de"
|
host: "_env:LPRHOST:fravm017173.fra.fraport.de"
|
||||||
port: "_env:LPRPORT:515"
|
port: "_env:LPRPORT:515"
|
||||||
queue: "_env:LPRQUEUE:fradrive"
|
queue: "_env:LPRQUEUE:fradrive"
|
||||||
|
|
||||||
smtp:
|
smtp:
|
||||||
host: "_env:SMTPHOST:"
|
host: "_env:SMTPHOST:"
|
||||||
@ -191,7 +208,7 @@ widget-memcached:
|
|||||||
timeout: "_env:WIDGET_MEMCACHED_TIMEOUT:20"
|
timeout: "_env:WIDGET_MEMCACHED_TIMEOUT:20"
|
||||||
base-url: "_env:WIDGET_MEMCACHED_ROOT:"
|
base-url: "_env:WIDGET_MEMCACHED_ROOT:"
|
||||||
expiration: "_env:WIDGET_MEMCACHED_EXPIRATION:3600"
|
expiration: "_env:WIDGET_MEMCACHED_EXPIRATION:3600"
|
||||||
|
|
||||||
session-memcached:
|
session-memcached:
|
||||||
host: "_env:SESSION_MEMCACHED_HOST:localhost"
|
host: "_env:SESSION_MEMCACHED_HOST:localhost"
|
||||||
port: "_env:SESSION_MEMCACHED_PORT:11211"
|
port: "_env:SESSION_MEMCACHED_PORT:11211"
|
||||||
@ -279,8 +296,8 @@ user-defaults:
|
|||||||
max-favourites: 0
|
max-favourites: 0
|
||||||
max-favourite-terms: 2
|
max-favourite-terms: 2
|
||||||
theme: Default
|
theme: Default
|
||||||
date-time-format: "%d.%m.%Y %R"
|
date-time-format: "%d %b %y %R"
|
||||||
date-format: "%d.%m.%y"
|
date-format: "%d %b %Y"
|
||||||
time-format: "%R"
|
time-format: "%R"
|
||||||
download-files: false
|
download-files: false
|
||||||
warning-days: 1209600
|
warning-days: 1209600
|
||||||
|
|||||||
@ -1,6 +0,0 @@
|
|||||||
if [[ ! -d .stack-work-test ]]; then
|
|
||||||
mv -vT .stack-work .stack-work-test
|
|
||||||
[[ -d .stack-work-build ]] && mv -vT .stack-work-build .stack-work
|
|
||||||
else
|
|
||||||
echo "Directory .stack-work-test exists already."
|
|
||||||
fi
|
|
||||||
@ -301,7 +301,7 @@ export class ExamCorrect {
|
|||||||
users: [user],
|
users: [user],
|
||||||
status: STATUS.LOADING,
|
status: STATUS.LOADING,
|
||||||
};
|
};
|
||||||
if (results && Object.keys(results).length > 0) rowInfo.results = results;
|
if (results && results !== {}) rowInfo.results = results;
|
||||||
if (result !== undefined) rowInfo.result = result;
|
if (result !== undefined) rowInfo.result = result;
|
||||||
this._addRow(rowInfo);
|
this._addRow(rowInfo);
|
||||||
|
|
||||||
|
|||||||
@ -16,8 +16,13 @@ fi
|
|||||||
branch="$(git rev-parse --abbrev-ref HEAD)"
|
branch="$(git rev-parse --abbrev-ref HEAD)"
|
||||||
|
|
||||||
if [[ $branch != "master" && $branch != "test" ]]; then
|
if [[ $branch != "master" && $branch != "test" ]]; then
|
||||||
|
if echo $@ | grep -xqe '--dev';
|
||||||
|
then
|
||||||
|
: # dev-releases possible on any branch
|
||||||
|
else
|
||||||
echo "Not on master or test" >&2
|
echo "Not on master or test" >&2
|
||||||
exit 1
|
exit 1
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
ourHash=$(git rev-parse HEAD)
|
ourHash=$(git rev-parse HEAD)
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
# SPDX-FileCopyrightText: 2022-24 Gregor Kleen <gregor.kleen@ifi.lmu.de>,Winnie Ros <winnie.ros@campus.lmu.de>,Steffen Jost <s.jost@fraport.de>
|
# SPDX-FileCopyrightText: 2022-2024 Sarah Vaupel <sarah.vaupel@uniworx.de>, Gregor Kleen <gregor.kleen@ifi.lmu.de>,Winnie Ros <winnie.ros@campus.lmu.de>
|
||||||
#
|
#
|
||||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
@ -67,7 +67,6 @@ BearerTokenExpiresTip: Wird der Ablaufzeitpunkt überschrieben und kein Ablaufze
|
|||||||
BearerTokenOverrideStart: Startzeitpunkt
|
BearerTokenOverrideStart: Startzeitpunkt
|
||||||
BearerTokenOverrideStartTip: Wird kein Startzeitpunkt angegeben, wird bei Verwendung des Tokens nur der Ablaufzeitpunkt überprüft.
|
BearerTokenOverrideStartTip: Wird kein Startzeitpunkt angegeben, wird bei Verwendung des Tokens nur der Ablaufzeitpunkt überprüft.
|
||||||
HeadingAdminTokens: Tokens ausstellen
|
HeadingAdminTokens: Tokens ausstellen
|
||||||
UserUnknown: Unbekannter Benutzer:in
|
|
||||||
|
|
||||||
#templates adminFeautures
|
#templates adminFeautures
|
||||||
StudyFeaturesDegrees: Abschlüsse
|
StudyFeaturesDegrees: Abschlüsse
|
||||||
@ -102,7 +101,7 @@ ProblemsHeadingDrivers: Fahrberechtigungen
|
|||||||
ProblemsHeadingNotifications: Benachrichtigungen
|
ProblemsHeadingNotifications: Benachrichtigungen
|
||||||
ProblemsHeadingMisc: Allgemein
|
ProblemsHeadingMisc: Allgemein
|
||||||
ProblemsAvsProblem: Synchronisation mit AVS/MoBaKo komplett fehlgeschlagen
|
ProblemsAvsProblem: Synchronisation mit AVS/MoBaKo komplett fehlgeschlagen
|
||||||
ProblemsDriverSynch n@Int: #{n} #{pluralDE n "Diskrepanz" "Diskrepanzen"} zwischen AVS und FRADrive
|
ProblemsDriverSynch n@Int: #{tshow n} Diskrepanzen zwischen AVS und FRADrive
|
||||||
ProblemsDriverSynch0: Alle Sperrungen von Vorfeld-Fahrberechtigungen 'F' sind im AVS eingetragen
|
ProblemsDriverSynch0: Alle Sperrungen von Vorfeld-Fahrberechtigungen 'F' sind im AVS eingetragen
|
||||||
ProblemsDriverSynch1down: Alle Sperrungen von Rollfeld-Fahrberechtigungen 'R' sind im AVS eingetragen
|
ProblemsDriverSynch1down: Alle Sperrungen von Rollfeld-Fahrberechtigungen 'R' sind im AVS eingetragen
|
||||||
ProblemsDriverSynch1up: Alle gültigen Vorfeld-Fahrberechtigungen 'F' sind im AVS eingetragen
|
ProblemsDriverSynch1up: Alle gültigen Vorfeld-Fahrberechtigungen 'F' sind im AVS eingetragen
|
||||||
@ -110,11 +109,10 @@ ProblemsDriverSynch2: Alle gültigen Rollfeld-Fahrberechtigungen 'R' sind im AVS
|
|||||||
ProblemsRDriversHaveFs: Alle Inhaber einer Rollfeld-Fahrberechtigung besitzen auch eine gültige Vorfeld-Fahrberechtigung
|
ProblemsRDriversHaveFs: Alle Inhaber einer Rollfeld-Fahrberechtigung besitzen auch eine gültige Vorfeld-Fahrberechtigung
|
||||||
ProblemsDriversHaveAvsIds: Alle Inhaber einer Fahrberechtigung konnten einer AVS Identifikationsnummer zugeordnet werden
|
ProblemsDriversHaveAvsIds: Alle Inhaber einer Fahrberechtigung konnten einer AVS Identifikationsnummer zugeordnet werden
|
||||||
ProblemsUsersAreReachable: Für alle Benutzer ist eine E-Mail oder postalische Adresse bekannt
|
ProblemsUsersAreReachable: Für alle Benutzer ist eine E-Mail oder postalische Adresse bekannt
|
||||||
ProblemsNoStalePrintJobs n@Integer: Alle Briefversandaufträge #{pluralDE n "des vergangenen Tages" ("der vergangenen "<> tshow n <> " Tage")} wurden von der Druckerei bestätigt
|
ProblemsNoStalePrintJobs n@Integer: Alle Briefversandaufträge der vergangenen #{show n} Tage wurden von der Druckerei bestätigt
|
||||||
ProblemsNoBadAPCIds: Alle kürzlich empfangenen Druckauftragsbestätigungen waren gültig
|
ProblemsNoBadAPCIds: Alle kürzlich empfangenen Druckauftragsbestätigungen waren gültig
|
||||||
ProblemsUnreachableHeading: Unerreichbare Benutzer
|
ProblemsUnreachableHeading: Unerreichbare Benutzer
|
||||||
ProblemsUnreachableBody: Benutzer ohne E-Mail oder Postadresse, welche z.B. bei ablaufenden Berechtigungen nicht benachrichtigt werden können:
|
ProblemsUnreachableBody: Benutzer ohne E-Mail oder Postadresse, welche z.B. bei ablaufenden Berechtigungen nicht benachrichtigt werden können:
|
||||||
ProblemsUnreachableButtons: Synchronisation für Unerreichbare starten
|
|
||||||
ProblemsRWithoutFHeading: Fahrer mit R ohne F
|
ProblemsRWithoutFHeading: Fahrer mit R ohne F
|
||||||
ProblemsRWithoutFBody: Diese Fahrer sind wegen einer ungültigen Vorfeld-Fahrberechtigung komplett gesperrt, obwohl eine gültige Rollfeld-Fahrberechtigung besteht:
|
ProblemsRWithoutFBody: Diese Fahrer sind wegen einer ungültigen Vorfeld-Fahrberechtigung komplett gesperrt, obwohl eine gültige Rollfeld-Fahrberechtigung besteht:
|
||||||
ProblemsNoAvsIdHeading: Fahrer ohne AVS-Id
|
ProblemsNoAvsIdHeading: Fahrer ohne AVS-Id
|
||||||
@ -122,24 +120,6 @@ ProblemsNoAvsIdBody: Fahrer mit gültiger Fahrberechtigung in FRADrive, welche t
|
|||||||
ProblemsAvsSynchHeading: Synchronisation AVS Fahrberechtigungen
|
ProblemsAvsSynchHeading: Synchronisation AVS Fahrberechtigungen
|
||||||
ProblemsAvsErrorHeading: Fehlermeldungen
|
ProblemsAvsErrorHeading: Fehlermeldungen
|
||||||
ProblemsInterfaceSince: Berücksichtigt werden nur Erfolge und Fehler seit
|
ProblemsInterfaceSince: Berücksichtigt werden nur Erfolge und Fehler seit
|
||||||
ProblemAvsUsrHadR: Momentan gültiges R im AVS
|
|
||||||
|
|
||||||
AdminProblemSolved: Erledigt
|
|
||||||
AdminProblemSolver: Bearbeitet von
|
|
||||||
AdminProblemCreated: Erkannt
|
|
||||||
AdminProblemInfo: Problembeschreibung
|
|
||||||
AdminProblemsSolved n@Int: #{pluralDEeN n "Admin Problem"} als erledigt markiert
|
|
||||||
AdminProblemsReopened n@Int: #{pluralDEeN n "Admin Problem"} erneut eröffnet
|
|
||||||
AdminProblemNewCompany: Neue Firma über AVS automatisch erstellt; prüfen und ggf. Standardansprechpartner eintragen
|
|
||||||
AdminProblemSupervisorNewCompany b@Bool: Standardansprechpartner #{boolText mempty "mit Standardumleitung" b} wechselte zu neuer Firma
|
|
||||||
AdminProblemSupervisorLeftCompany b@Bool: Einziger Standardansprechpartner #{boolText mempty "mit Standardumleitung" b} dieses Fahrers wechselte zu neuer Firma
|
|
||||||
AdminProblemCompanySuperiorChange: Neuer firmenweiter Vorgesetzter.
|
|
||||||
AdminProblemCompanySuperiorNotFound t@Text: Neuer unbekannter firmenweiter Vorgesetzter mit E-Mail #{t}, keine Ansprechpartnerbeziehungen eingerichtet.
|
|
||||||
AdminProblemCompanySuperiorPrevious: Ehemaliger Vorgesetzter:
|
|
||||||
AdminProblemNewlyUnsupervised: Fahrer hat keinen Firmenansprechpartner mehr nach AVS Firmenwechsel zu Firma
|
|
||||||
AdminProblemUser: Betroffener
|
|
||||||
ProblemTableMarkSolved: Als erledigt markieren
|
|
||||||
ProblemTableMarkUnsolved: Erledigt Markierung löschen
|
|
||||||
|
|
||||||
InterfacesOk: Schnittstellen sind ok.
|
InterfacesOk: Schnittstellen sind ok.
|
||||||
InterfacesFail n@Int: #{pluralDEeN n "Schnittstellenproblem"}!
|
InterfacesFail n@Int: #{pluralDEeN n "Schnittstellenproblem"}!
|
||||||
@ -148,15 +128,8 @@ InterfaceName: Schnittstelle
|
|||||||
InterfaceLastSynch: Zuletzt
|
InterfaceLastSynch: Zuletzt
|
||||||
InterfaceSubtype: Betreffend
|
InterfaceSubtype: Betreffend
|
||||||
InterfaceWrite: Schreibend
|
InterfaceWrite: Schreibend
|
||||||
|
|
||||||
|
AdminUserPassword: Passwort
|
||||||
InterfaceSuccess: Rückmeldung
|
InterfaceSuccess: Rückmeldung
|
||||||
InterfaceInfo: Nachricht
|
InterfaceInfo: Nachricht
|
||||||
InterfaceFreshness: Maximale Zugriffsfrist
|
InterfaceFreshness: Prüfungszeitraum (h)
|
||||||
InterfaceFreshnessTooltip: Zeitspanne innerhalb der ein erneuter erfolgreicher Schnittstellenzugriff erfolgen muss, ohne Warnungen auszulösen
|
|
||||||
ConfigInterfacesHeading: Konfiguration Zugriffsfristen
|
|
||||||
|
|
||||||
IWTActAdd: Hinzufügen/Ändern
|
|
||||||
IWTActDelete: Entfernen
|
|
||||||
InterfaceWarningAdded: Schnittstellenwarnungszeit hinzugefügt oder geändert
|
|
||||||
InterfaceWarningDeleted n@Int: #{pluralDEeN n "Schnittstellenwarnungszeit"} gelöscht
|
|
||||||
InterfaceWarningDisabledEntirely: Alle Fehler ignorieren
|
|
||||||
InterfaceWarningDisabledInterval: Keine Zugriffsfrist
|
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
# SPDX-FileCopyrightText: 2022-24 Sarah Vaupel <sarah.vaupel@ifi.lmu.de>,Winnie Ros <winnie.ros@campus.lmu.de>,Steffen Jost <s.jost@fraport.de>
|
# SPDX-FileCopyrightText: 2022-2024 Sarah Vaupel <sarah.vaupel@uniworx.de>, Sarah Vaupel <sarah.vaupel@ifi.lmu.de>, Winnie Ros <winnie.ros@campus.lmu.de>
|
||||||
#
|
#
|
||||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
@ -18,10 +18,10 @@ NoNameCandidatesInferred: No new name-mappings inferred
|
|||||||
AllNameIncidencesDeleted: Successfully deleted all name observations
|
AllNameIncidencesDeleted: Successfully deleted all name observations
|
||||||
AllParentIncidencesDeleted: Successfully deleted all parent-relation observations
|
AllParentIncidencesDeleted: Successfully deleted all parent-relation observations
|
||||||
AllStandaloneIncidencesDeleted: Successfully deleted all standalone observations
|
AllStandaloneIncidencesDeleted: Successfully deleted all standalone observations
|
||||||
IncidencesDeleted n: Successfully deleted #{pluralENsN n "observation"}
|
IncidencesDeleted n: Successfully deleted #{show n} #{pluralEN n "observation" "observations"}
|
||||||
RedundantParentCandidatesRemoved n: Successfully removed #{n} rendundant #{pluralENs n "parent-candidate"}
|
RedundantParentCandidatesRemoved n: Successfully removed #{n} rendundant #{pluralEN n "parent-candidate" "parent-candidates"}
|
||||||
RedundantStandaloneCandidatesRemoved n: Successfully removed #{n} rendundant #{pluralENs n "standalone-candidate"}
|
RedundantStandaloneCandidatesRemoved n: Successfully removed #{n} rendundant #{pluralEN n "standalone-candidate" "standalone-candidates"}
|
||||||
ParentCandidatesInferred n: Successfully inferred #{n} field #{pluralENs n "parent-relation"}
|
ParentCandidatesInferred n: Successfully inferred #{n} field #{pluralEN n "parent-relation" "parent-reliations"}
|
||||||
NoParentCandidatesInferred: No new parent-relations inferred
|
NoParentCandidatesInferred: No new parent-relations inferred
|
||||||
StudyDegreeChangeSuccess: Successfully updated degrees
|
StudyDegreeChangeSuccess: Successfully updated degrees
|
||||||
StudyTermsShort: Field shorthand
|
StudyTermsShort: Field shorthand
|
||||||
@ -67,7 +67,6 @@ BearerTokenExpiresTip: If no expiration time is given, the token will not expire
|
|||||||
BearerTokenOverrideStart: Start time
|
BearerTokenOverrideStart: Start time
|
||||||
BearerTokenOverrideStartTip: If no start time is given, only the expiration time will be checked when the token is used.
|
BearerTokenOverrideStartTip: If no start time is given, only the expiration time will be checked when the token is used.
|
||||||
HeadingAdminTokens: Issue tokens
|
HeadingAdminTokens: Issue tokens
|
||||||
UserUnknown: User unknown
|
|
||||||
|
|
||||||
#templates adminfeatures
|
#templates adminfeatures
|
||||||
StudyFeaturesDegrees: Degrees
|
StudyFeaturesDegrees: Degrees
|
||||||
@ -102,7 +101,7 @@ ProblemsHeadingDrivers: Driving Licences
|
|||||||
ProblemsHeadingNotifications: User communication
|
ProblemsHeadingNotifications: User communication
|
||||||
ProblemsHeadingMisc: Miscellaneous
|
ProblemsHeadingMisc: Miscellaneous
|
||||||
ProblemsAvsProblem: Synchronisation with AVS/MoBaKo failed entirely
|
ProblemsAvsProblem: Synchronisation with AVS/MoBaKo failed entirely
|
||||||
ProblemsDriverSynch n: #{tshow n} #{pluralEN n "mismatch" "mismatches"} between AVS and FRADrive
|
ProblemsDriverSynch n: #{tshow n} mismatches between AVS and FRADrive
|
||||||
ProblemsDriverSynch0: All revocations of apron driving licences 'F' were successfully registered with AVS
|
ProblemsDriverSynch0: All revocations of apron driving licences 'F' were successfully registered with AVS
|
||||||
ProblemsDriverSynch1down: All revocations of maneuvering area driving licences 'R' were successfully registered with AVS
|
ProblemsDriverSynch1down: All revocations of maneuvering area driving licences 'R' were successfully registered with AVS
|
||||||
ProblemsDriverSynch1up: All valid apron driving licences 'F' were successfully registered with AVS
|
ProblemsDriverSynch1up: All valid apron driving licences 'F' were successfully registered with AVS
|
||||||
@ -110,36 +109,17 @@ ProblemsDriverSynch2: All valid maneuvering area driving licences 'R' were succe
|
|||||||
ProblemsRDriversHaveFs: All driving licence 'R' holders also have a valid 'F' licence
|
ProblemsRDriversHaveFs: All driving licence 'R' holders also have a valid 'F' licence
|
||||||
ProblemsDriversHaveAvsIds: All driving licence holder could be matched with their AVS id
|
ProblemsDriversHaveAvsIds: All driving licence holder could be matched with their AVS id
|
||||||
ProblemsUsersAreReachable: Either Email or postal address is known for all users
|
ProblemsUsersAreReachable: Either Email or postal address is known for all users
|
||||||
ProblemsNoStalePrintJobs n: All requests for letter mailing within the last #{pluralENsN n "day"} were acknowledged as printed by the airport printing center
|
ProblemsNoStalePrintJobs n: All requests for letter mailing within the last #{show n} days were acknowledged as printed by the airport printing center
|
||||||
ProblemsNoBadAPCIds: All recently received print job ids from Airport Print Center were legit
|
ProblemsNoBadAPCIds: All recently received print job ids from Airport Print Center were legit
|
||||||
ProblemsUnreachableHeading: Unreachable Users
|
ProblemsUnreachableHeading: Unreachable Users
|
||||||
ProblemsUnreachableBody: Users without Email nor postal address, who thus cannot be notified about expiring qualifications:
|
ProblemsUnreachableBody: Users without Email nor postal address, who thus cannot be notified about expiring qualifications:
|
||||||
ProblemsUnreachableButtons: Start synchronisation for unreachable users only
|
|
||||||
ProblemsRWithoutFHeading: Drivers having 'R' but not 'F'
|
ProblemsRWithoutFHeading: Drivers having 'R' but not 'F'
|
||||||
ProblemsRWithoutFBody: Drivers without apron driving licence are prohibited from driving, even if they own a valid maneuvering driving licence:
|
ProblemsRWithoutFBody: Drivers without apron driving licence are prohibited from driving, even if they own a valid maneuvering driving licence:
|
||||||
ProblemsNoAvsIdHeading: Drivers without AVS id
|
ProblemsNoAvsIdHeading: Drivers without AVS id
|
||||||
ProblemsNoAvsIdBody: Drivers having a valid apron driving licence within FRADrive only, but who may not drive since a missing AVS id prevents communication of the driving licence to AVS:
|
ProblemsNoAvsIdBody: Drivers having a valid apron driving licence within FRADrive only, but who may not drive since a missing AVS id prevents communication of the driving licence to AVS:
|
||||||
ProblemsAvsSynchHeading: Synchronisation AVS Driving Licences
|
ProblemsAvsSynchHeading: Synchronisation AVS Driving Licences
|
||||||
ProblemsAvsErrorHeading: Error Log
|
ProblemsAvsErrorHeading: Error Log
|
||||||
ProblemsInterfaceSince: Only considering successes and errors since
|
ProblemsInterfaceSince: Only considering successes and errors since
|
||||||
ProblemAvsUsrHadR: Currenlt R valid in AVS
|
|
||||||
|
|
||||||
AdminProblemSolved: Done
|
|
||||||
AdminProblemSolver: Solved by
|
|
||||||
AdminProblemCreated: Recognized
|
|
||||||
AdminProblemInfo: Problem
|
|
||||||
AdminProblemsSolved n: #{pluralENsN n "admin problem"} marked as solved
|
|
||||||
AdminProblemsReopened n: #{pluralENsN n "admin problem"} reopened
|
|
||||||
AdminProblemNewCompany: New company from AVS; verify and add default supervisors
|
|
||||||
AdminProblemSupervisorNewCompany b: Default company supervisor #{boolText mempty "with reroute" b} changed to new company
|
|
||||||
AdminProblemSupervisorLeftCompany b: Only default company supervisor #{boolText mempty "with reroute" b} for this user changed to new company
|
|
||||||
AdminProblemCompanySuperiorChange: New company wide superior.
|
|
||||||
AdminProblemCompanySuperiorNotFound t: Unable to set supervision for new unknown company wide superior having Email #{t}.
|
|
||||||
AdminProblemCompanySuperiorPrevious: Previous superior:
|
|
||||||
AdminProblemNewlyUnsupervised: Driver has no longer a company default supervisor after AVS update at new company
|
|
||||||
AdminProblemUser: Affected
|
|
||||||
ProblemTableMarkSolved: Mark done
|
|
||||||
ProblemTableMarkUnsolved: Reopen as undone
|
|
||||||
|
|
||||||
InterfacesOk: Interfaces are ok.
|
InterfacesOk: Interfaces are ok.
|
||||||
InterfacesFail n: #{pluralENsN n "interface problem"}!
|
InterfacesFail n: #{pluralENsN n "interface problem"}!
|
||||||
@ -148,15 +128,8 @@ InterfaceName: Interface
|
|||||||
InterfaceLastSynch: Last
|
InterfaceLastSynch: Last
|
||||||
InterfaceSubtype: Affecting
|
InterfaceSubtype: Affecting
|
||||||
InterfaceWrite: Write
|
InterfaceWrite: Write
|
||||||
|
|
||||||
|
AdminUserPassword: Password
|
||||||
InterfaceSuccess: Returned
|
InterfaceSuccess: Returned
|
||||||
InterfaceInfo: Message
|
InterfaceInfo: Message
|
||||||
InterfaceFreshness: Maximum usage period
|
InterfaceFreshness: Check hours
|
||||||
InterfaceFreshnessTooltip: Time period within which the next successful interface access must occur to avoid a warning
|
|
||||||
ConfigInterfacesHeading: Configure interface usage warnings
|
|
||||||
|
|
||||||
IWTActAdd: Add/Edit
|
|
||||||
IWTActDelete: Delete
|
|
||||||
InterfaceWarningAdded: Interface warning time added/changed
|
|
||||||
InterfaceWarningDeleted n: #{pluralENsN n "interface warning time"} deleted
|
|
||||||
InterfaceWarningDisabledEntirely: Ignore all errors
|
|
||||||
InterfaceWarningDisabledInterval: No maximum usage period
|
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
# SPDX-FileCopyrightText: 2022 Gregor Kleen <gregor.kleen@ifi.lmu.de>,Sarah Vaupel <sarah.vaupel@ifi.lmu.de>,Steffen Jost <jost@tcs.ifi.lmu.de>,Winnie Ros <winnie.ros@campus.lmu.de>
|
# SPDX-FileCopyrightText: 2022-2024 David Mosbach <david.mosbach@uniworx.de>, Sarah Vaupel <sarah.vaupel@uniworx.de>, Gregor Kleen <gregor.kleen@ifi.lmu.de>, Sarah Vaupel <sarah.vaupel@ifi.lmu.de>, Steffen Jost <jost@tcs.ifi.lmu.de>, Winnie Ros <winnie.ros@campus.lmu.de>
|
||||||
#
|
#
|
||||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
@ -72,8 +72,8 @@ UnauthorizedTutorialTutorControl: Ausbilder:innen dürfen diesen Kurs nicht edit
|
|||||||
UnauthorizedCourseTutor: Sie sind nicht Ausbilder:in für diese Kursart.
|
UnauthorizedCourseTutor: Sie sind nicht Ausbilder:in für diese Kursart.
|
||||||
UnauthorizedTutor: Sie sind nicht Ausbilder:in.
|
UnauthorizedTutor: Sie sind nicht Ausbilder:in.
|
||||||
UnauthorizedTutorialRegisterGroup: Sie sind bereits in einem Kurs mit derselben Registrierungs-Gruppe eingetragen.
|
UnauthorizedTutorialRegisterGroup: Sie sind bereits in einem Kurs mit derselben Registrierungs-Gruppe eingetragen.
|
||||||
UnauthorizedLDAP: Angegebener Nutzer/Angegebene Nutzerin meldet sich nicht mit Fraport Login an.
|
UnauthorizedExternal: Angegebene:r Benuzter:in meldet sich nicht über einen aktuell unterstützten externen Login an.
|
||||||
UnauthorizedPWHash: Angegebener Nutzer/Angegebene Nutzerin meldet sich nicht mit FRADrive-Kennung an.
|
UnauthorizedInternal: Angegebene:r Benutzer:in meldet sich nicht mit FRADrive-Kennung an.
|
||||||
UnauthorizedExternalExamListNotEmpty: Liste von externen Prüfungen ist nicht leer
|
UnauthorizedExternalExamListNotEmpty: Liste von externen Prüfungen ist nicht leer
|
||||||
UnauthorizedExternalExamLecturer: Sie sind nicht als Prüfer:in für diese externe Prüfung eingetragen
|
UnauthorizedExternalExamLecturer: Sie sind nicht als Prüfer:in für diese externe Prüfung eingetragen
|
||||||
UnauthorizedSubmissionSubmissionGroup: Sie sind nicht Mitglied in einer der registrierten Abgabegruppen, die an dieser Abgabe beteiligt sind
|
UnauthorizedSubmissionSubmissionGroup: Sie sind nicht Mitglied in einer der registrierten Abgabegruppen, die an dieser Abgabe beteiligt sind
|
||||||
@ -102,15 +102,15 @@ LDAPLoginTitle: Fraport Login für interne und externe Nutzer
|
|||||||
PWHashLoginTitle: Spezieller Funktionsnutzer Login
|
PWHashLoginTitle: Spezieller Funktionsnutzer Login
|
||||||
PWHashLoginNote: Verwenden Sie dieses Formular nur, wenn Sie explizit dazu aufgefordert wurden. Alle anderen sollten das andere Login Formular verwenden!
|
PWHashLoginNote: Verwenden Sie dieses Formular nur, wenn Sie explizit dazu aufgefordert wurden. Alle anderen sollten das andere Login Formular verwenden!
|
||||||
DummyLoginTitle: Development-Login
|
DummyLoginTitle: Development-Login
|
||||||
InternalLdapError: Interner Fehler beim Fraport Büko-Login
|
InternalLoginError: Interner Fehler beim Login
|
||||||
CampusUserInvalidIdent: Konnte anhand des Fraport Büko-Logins keine eindeutige Identifikation ermitteln
|
DecodeUserInvalidIdent: Konnte anhand des Fraport Büko-Logins keine eindeutige Identifikation ermitteln
|
||||||
CampusUserInvalidEmail: Konnte anhand des Fraport Büko-Logins keine E-Mail-Addresse ermitteln
|
DecodeUserInvalidEmail: Konnte anhand des Fraport Büko-Logins keine E-Mail-Addresse ermitteln
|
||||||
CampusUserInvalidDisplayName: Konnte anhand des Fraport Büko-Logins keinen vollen Namen ermitteln
|
DecodeUserInvalidDisplayName: Konnte anhand des Fraport Büko-Logins keinen vollen Namen ermitteln
|
||||||
CampusUserInvalidGivenName: Konnte anhand des Fraport Büko-Logins keinen Vornamen ermitteln
|
DecodeUserInvalidGivenName: Konnte anhand des Fraport Büko-Logins keinen Vornamen ermitteln
|
||||||
CampusUserInvalidSurname: Konnte anhand des Fraport Büko-Logins keinen Nachname ermitteln
|
DecodeUserInvalidSurname: Konnte anhand des Fraport Büko-Logins keinen Nachname ermitteln
|
||||||
CampusUserInvalidTitle: Konnte anhand des Fraport Büko-Logins keinen akademischen Titel ermitteln
|
DecodeUserInvalidTitle: Konnte anhand des Fraport Büko-Logins keinen akademischen Titel ermitteln
|
||||||
CampusUserInvalidFeaturesOfStudy parseErr@Text: Konnte anhand des Fraport Büko-Logins keine Studiengänge ermitteln
|
DecodeUserInvalidFeaturesOfStudy parseErr@Text: Konnte anhand des Fraport Büko-Logins keine Studiengänge ermitteln
|
||||||
CampusUserInvalidAssociatedSchools parseErr@Text: Konnte anhand des Fraport Büko-Logins keine Bereiche ermitteln
|
DecodeUserInvalidAssociatedSchools parseErr@Text: Konnte anhand des Fraport Büko-Logins keine Bereiche ermitteln
|
||||||
InvalidCredentialsADNoSuchObject: Benutzereintrag existiert nicht
|
InvalidCredentialsADNoSuchObject: Benutzereintrag existiert nicht
|
||||||
InvalidCredentialsADLogonFailure: Ungültiges Passwort
|
InvalidCredentialsADLogonFailure: Ungültiges Passwort
|
||||||
InvalidCredentialsADAccountRestriction: Beschränkungen des Fraport Accounts verhindern Login
|
InvalidCredentialsADAccountRestriction: Beschränkungen des Fraport Accounts verhindern Login
|
||||||
@ -139,3 +139,6 @@ FormHoneypotNamePlaceholder: Name
|
|||||||
FormHoneypotComment: Kommentar
|
FormHoneypotComment: Kommentar
|
||||||
FormHoneypotCommentPlaceholder: Kommentar
|
FormHoneypotCommentPlaceholder: Kommentar
|
||||||
FormHoneypotFilled: Bitte füllen Sie keines der verstecken Felder aus
|
FormHoneypotFilled: Bitte füllen Sie keines der verstecken Felder aus
|
||||||
|
|
||||||
|
Logout: Abmeldung
|
||||||
|
SingleSignOut: Abmeldung bei Azure
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
# SPDX-FileCopyrightText: 2022 Gregor Kleen <gregor.kleen@ifi.lmu.de>,Sarah Vaupel <sarah.vaupel@ifi.lmu.de>,Steffen Jost <jost@tcs.ifi.lmu.de>,Winnie Ros <winnie.ros@campus.lmu.de>
|
# SPDX-FileCopyrightText: 2022-2024 Sarah Vaupel <sarah.vaupel@uniworx.de>, David Mosbach <david.mosbach@uniworx.de>, Gregor Kleen <gregor.kleen@ifi.lmu.de>, Sarah Vaupel <sarah.vaupel@ifi.lmu.de>, Steffen Jost <jost@tcs.ifi.lmu.de>, Winnie Ros <winnie.ros@campus.lmu.de>
|
||||||
#
|
#
|
||||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
@ -72,8 +72,8 @@ UnauthorizedTutorialTutorControl: Instructors may not edit this course.
|
|||||||
UnauthorizedCourseTutor: You are no instructor for this course.
|
UnauthorizedCourseTutor: You are no instructor for this course.
|
||||||
UnauthorizedTutor: You are no instructor.
|
UnauthorizedTutor: You are no instructor.
|
||||||
UnauthorizedTutorialRegisterGroup: You are already registered for a course with the same registration group.
|
UnauthorizedTutorialRegisterGroup: You are already registered for a course with the same registration group.
|
||||||
UnauthorizedLDAP: Specified user does not log in with their Fraport password.
|
UnauthorizedExternal: Specified user does not log in with any currently supported external login.
|
||||||
UnauthorizedPWHash: Specified user does not log in with an FRADrive-account.
|
UnauthorizedInternal: Specified user does not log in with a FRADrive-account.
|
||||||
UnauthorizedExternalExamListNotEmpty: List of external exams is not empty
|
UnauthorizedExternalExamListNotEmpty: List of external exams is not empty
|
||||||
UnauthorizedExternalExamLecturer: You are not an associated person for this external exam
|
UnauthorizedExternalExamLecturer: You are not an associated person for this external exam
|
||||||
UnauthorizedSubmissionSubmissionGroup: You are not member in any of the submission groups for this submission
|
UnauthorizedSubmissionSubmissionGroup: You are not member in any of the submission groups for this submission
|
||||||
@ -103,15 +103,15 @@ LDAPLoginTitle: Fraport login for intern and extern users
|
|||||||
PWHashLoginTitle: Special function user login
|
PWHashLoginTitle: Special function user login
|
||||||
PWHashLoginNote: Only use this login form if you have received special instructions to do so. All others should use the other login field.
|
PWHashLoginNote: Only use this login form if you have received special instructions to do so. All others should use the other login field.
|
||||||
DummyLoginTitle: Development login
|
DummyLoginTitle: Development login
|
||||||
InternalLdapError: Internal error during Fraport Büko login
|
InternalLoginError: Internal error during login
|
||||||
CampusUserInvalidIdent: Could not determine unique identification during Fraport Büko login
|
DecodeUserInvalidIdent: Could not determine unique identification during Fraport Büko login
|
||||||
CampusUserInvalidEmail: Could not determine email address during Fraport Büko login
|
DecodeUserInvalidEmail: Could not determine email address during Fraport Büko login
|
||||||
CampusUserInvalidDisplayName: Could not determine display name during Fraport Büko login
|
DecodeUserInvalidDisplayName: Could not determine display name during Fraport Büko login
|
||||||
CampusUserInvalidGivenName: Could not determine given name during Fraport Büko login
|
DecodeUserInvalidGivenName: Could not determine given name during Fraport Büko login
|
||||||
CampusUserInvalidSurname: Could not determine surname during Fraport Büko login
|
DecodeUserInvalidSurname: Could not determine surname during Fraport Büko login
|
||||||
CampusUserInvalidTitle: Could not determine title during Fraport Büko login
|
DecodeUserInvalidTitle: Could not determine title during Fraport Büko login
|
||||||
CampusUserInvalidFeaturesOfStudy parseErr: Could not determine features of study during Fraport Büko login
|
DecodeUserInvalidFeaturesOfStudy parseErr: Could not determine features of study during Fraport Büko login
|
||||||
CampusUserInvalidAssociatedSchools parseErr: Could not determine associated departments during Fraport Büko login
|
DecodeUserInvalidAssociatedSchools parseErr: Could not determine associated departments during Fraport Büko login
|
||||||
InvalidCredentialsADNoSuchObject: User entry does not exist
|
InvalidCredentialsADNoSuchObject: User entry does not exist
|
||||||
InvalidCredentialsADLogonFailure: Invalid password
|
InvalidCredentialsADLogonFailure: Invalid password
|
||||||
InvalidCredentialsADAccountRestriction: Restrictions on your Fraport account prevent a login
|
InvalidCredentialsADAccountRestriction: Restrictions on your Fraport account prevent a login
|
||||||
@ -140,3 +140,6 @@ FormHoneypotNamePlaceholder !ident-ok: Name
|
|||||||
FormHoneypotComment: Comment
|
FormHoneypotComment: Comment
|
||||||
FormHoneypotCommentPlaceholder: Comment
|
FormHoneypotCommentPlaceholder: Comment
|
||||||
FormHoneypotFilled: Please do not fill in any of the hidden fields
|
FormHoneypotFilled: Please do not fill in any of the hidden fields
|
||||||
|
|
||||||
|
Logout: Logout
|
||||||
|
SingleSignOut: Azure logout
|
||||||
|
|||||||
@ -2,21 +2,17 @@
|
|||||||
#
|
#
|
||||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
AvsPersonInfo: AVS Personendaten
|
AvsPersonInfo: AVS Personendaten
|
||||||
AvsPersonId: AVS Personen Id
|
AvsPersonId: AVS Personen Id
|
||||||
AvsPersonNo: AVS Personennummer
|
AvsPersonNo: AVS Personennummer
|
||||||
AvsPersonNoNotId: AVS Personennummer dient zur menschlichen Kommunikation mit der Ausweisstelle und darf nicht verwechselt werden mit der maschinell verwendeten AVS Personen Id
|
|
||||||
AvsPersonNoMismatch: AVS Personennummer hat sich geändert und wurde in FRADrive noch nicht aktualisiert
|
|
||||||
AvsPersonNoDiffers: Es sind derzeit zwei verschiedene AVS Personennummern zugeordnet. Bitte einen Administrator kontaktieren.
|
|
||||||
AvsCardNo: Ausweiskartennummer
|
AvsCardNo: Ausweiskartennummer
|
||||||
AvsFirstName: Vorname
|
AvsFirstName: Vorname
|
||||||
AvsLastName: Nachname
|
AvsLastName: Nachname
|
||||||
AvsPrimaryCompany: Primäre Firma
|
|
||||||
AvsInternalPersonalNo: Personalnummer (nur Fraport AG)
|
AvsInternalPersonalNo: Personalnummer (nur Fraport AG)
|
||||||
AvsVersionNo: Versionsnummer
|
AvsVersionNo: Versionsnummer
|
||||||
AvsQueryNeeded: Benötigt Verbindung zum AVS.
|
|
||||||
AvsQueryEmpty: Bitte mindestens ein Anfragefeld ausfüllen!
|
AvsQueryEmpty: Bitte mindestens ein Anfragefeld ausfüllen!
|
||||||
AvsQueryStatusInvalid t@Text: Nur numerische IDs eingeben, durch Komma getrennt! Erhalten: #{show t}
|
AvsQueryStatusInvalid t@Text: Nur numerische IDs eingeben, durch Komma getrennt! Erhalten: #{show t}
|
||||||
AvsLicence: Fahrberechtigung
|
AvsLicence: Fahrberechtigung
|
||||||
|
AvsPersonNoNotId: AVS Personennummer dient zur menschlichen Kommunikation mit der Ausweisstelle und darf nicht verwechselt werden mit der maschinell verwendeten AVS Personen Id
|
||||||
AvsTitleLicenceSynch: Abgleich Fahrberechtigungen zwischen AVS und FRADrive
|
AvsTitleLicenceSynch: Abgleich Fahrberechtigungen zwischen AVS und FRADrive
|
||||||
BtnAvsRevokeUnknown: Fahrberechtigungen im AVS sofort entziehen
|
BtnAvsRevokeUnknown: Fahrberechtigungen im AVS sofort entziehen
|
||||||
BtnAvsImportUnknown: AVS Daten unbekannter Personen importieren
|
BtnAvsImportUnknown: AVS Daten unbekannter Personen importieren
|
||||||
@ -31,33 +27,13 @@ RevokeFraDriveLicences alic@AvsLicence n@Int: _{alic} in FRADrive entzogen für
|
|||||||
RevokeUnknownLicencesOk: AVS Fahrberechtigungen unbekannter Fahrer wurden gesperrt
|
RevokeUnknownLicencesOk: AVS Fahrberechtigungen unbekannter Fahrer wurden gesperrt
|
||||||
RevokeUnknownLicencesFail: Nicht alle AVS Fahrberechtigungen unbekannter Fahrer konnten entzogen werden, siehe Log für Details
|
RevokeUnknownLicencesFail: Nicht alle AVS Fahrberechtigungen unbekannter Fahrer konnten entzogen werden, siehe Log für Details
|
||||||
AvsCommunicationError: AVS Schnittstelle lieferte einen unerwarteten Fehler.
|
AvsCommunicationError: AVS Schnittstelle lieferte einen unerwarteten Fehler.
|
||||||
AvsCommunicationTimeout: AVS Schnittstelle antwortete nicht.
|
|
||||||
LicenceTableChangeAvs: Im AVS ändern
|
LicenceTableChangeAvs: Im AVS ändern
|
||||||
LicenceTableGrantFDrive: In FRADrive erteilen
|
LicenceTableGrantFDrive: In FRADrive erteilen
|
||||||
LicenceTableRevokeFDrive: In FRADrive entziehen
|
LicenceTableRevokeFDrive: In FRADrive entziehen
|
||||||
TableAvsActiveCards: Gültige Ausweise
|
TableAvsActiveCards: Gültige Ausweise
|
||||||
TableAvsCardValid: Aktuell gültig
|
|
||||||
TableAvsCardIssueDate: Ausgestellt am
|
|
||||||
TableAvsCardValidTo: Gültig bis
|
|
||||||
AvsCardAreas: Ausweiszusätze
|
|
||||||
AvsCardColor: Ausweisfarbe
|
|
||||||
AvsCardColorGreen: Grün
|
AvsCardColorGreen: Grün
|
||||||
AvsCardColorBlue: Blau
|
AvsCardColorBlue: Blau
|
||||||
AvsCardColorRed: Rot
|
AvsCardColorRed: Rot
|
||||||
AvsCardColorYellow: Gelb
|
AvsCardColorYellow: Gelb
|
||||||
LastAvsSynchronisation: Letzte AVS-Synchronisation
|
LastAvsSynchronisation: Letzte AVS-Synchronisation
|
||||||
LastAvsSyncedBefore: Letzte AVS-Synchronisation vor
|
|
||||||
LastAvsSynchError: Letzte AVS-Fehlermeldung
|
LastAvsSynchError: Letzte AVS-Fehlermeldung
|
||||||
|
|
||||||
AvsInterfaceUnavailable: AVS Schnittstelle nicht richtig konfiguriert oder antwortet nicht
|
|
||||||
AvsUserUnassociated user@UserDisplayName: AVS Id unbekannt für Nutzer #{user}
|
|
||||||
AvsUserUnknownByAvs api@AvsPersonId: AVS kennt Id #{tshow api} nicht (mehr)
|
|
||||||
AvsUserAmbiguous api@AvsPersonId: AVS Id #{tshow api} ist nicht eindeutig
|
|
||||||
AvsStatusSearchEmpty: AVS lieferte keine Ausweisinformationen
|
|
||||||
AvsPersonSearchEmpty: AVS Suche lieferte leeres Ergebnis
|
|
||||||
AvsPersonSearchAmbiguous: AVS Suche lieferte mehrere uneindeutige Ergebnisse
|
|
||||||
AvsSetLicencesFailed reason@Text: Setzen der Fahrlizenz im AVS fehlgeschlagen. Grund: #{reason}
|
|
||||||
AvsIdMismatch api1@AvsPersonId api2@AvsPersonId: AVS Suche für Id #{tshow api1} lieferte stattdessen Id #{tshow api2}
|
|
||||||
AvsUserCreationFailed api@AvsPersonId: Für AVS Id #{tshow api} konnte kein neuer Benutzer angelegt werden, da es eine gemeinsame Id (z.B. Personalnummer) mit einem existierenden, aber verschiedenen Nutzer gibt.
|
|
||||||
AvsCardsEmpty: AVS Suche lieferte keinerlei Ausweiskarten
|
|
||||||
AvsCurrentData: Alle angezeigte Daten wurden kürzlich direkt über die AVS Schnittstelle abgerufen.
|
|
||||||
@ -1,23 +1,18 @@
|
|||||||
# SPDX-FileCopyrightText: 2022 Steffen Jost <jost@tcs.ifi.lmu.de>
|
# SPDX-FileCopyrightText: 2022 Steffen Jost <jost@tcs.ifi.lmu.de>
|
||||||
#
|
#
|
||||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
AvsPersonInfo: AVS person info
|
AvsPersonInfo: AVS Person Info
|
||||||
AvsPersonId: AVS person id
|
AvsPersonId: AVS Person Id
|
||||||
AvsPersonNo: AVS person number
|
AvsPersonNo: AVS Person Number
|
||||||
AvsPersonNoNotId: AVS person number is used in human communication only and must not be mistaken for the AVS personen id used in machine communications
|
|
||||||
AvsPersonNoMismatch: AVS person number has changed and was not yet updated in FRADrive
|
|
||||||
AvsPersonNoDiffers: There are currently two differing AVS person numbers associated with this user. Please contact an administrator to resolve this.
|
|
||||||
AvsCardNo: Card number
|
AvsCardNo: Card number
|
||||||
AvsFirstName: First name
|
AvsFirstName: First name
|
||||||
AvsLastName: Last name
|
AvsLastName: Last name
|
||||||
AvsPrimaryCompany: Primary company
|
|
||||||
AvsInternalPersonalNo: Personnel number (Fraport AG only)
|
AvsInternalPersonalNo: Personnel number (Fraport AG only)
|
||||||
AvsVersionNo: Version number
|
AvsVersionNo: Version number
|
||||||
AvsQueryNeeded: AVS connection required.
|
|
||||||
AvsQueryEmpty: At least one query field must be filled!
|
AvsQueryEmpty: At least one query field must be filled!
|
||||||
AvsQueryStatusInvalid t: Numeric IDs only, comma seperated! #{show t}
|
AvsQueryStatusInvalid t: Numeric IDs only, comma seperated! #{show t}
|
||||||
AvsLicence: Driving Licence
|
AvsLicence: Driving Licence
|
||||||
|
AvsPersonNoNotId: AVS person number is used in human communication only and must not be mistaken for the AVS personen id used in machine communications
|
||||||
AvsTitleLicenceSynch: Synchronisation driving licences between AVS and FRADrive
|
AvsTitleLicenceSynch: Synchronisation driving licences between AVS and FRADrive
|
||||||
BtnAvsRevokeUnknown: Revoke AVS driving licences for unknown persons immediately
|
BtnAvsRevokeUnknown: Revoke AVS driving licences for unknown persons immediately
|
||||||
BtnAvsImportUnknown: Import AVS data for unknown persons
|
BtnAvsImportUnknown: Import AVS data for unknown persons
|
||||||
@ -32,33 +27,13 @@ RevokeFraDriveLicences alic@AvsLicence n@Int: _{alic} revoked in FRADrive for #{
|
|||||||
RevokeUnknownLicencesOk: AVS driving licences of unknown drivers revoked
|
RevokeUnknownLicencesOk: AVS driving licences of unknown drivers revoked
|
||||||
RevokeUnknownLicencesFail: Not all AVS driving licences of unknown drivers could be revoked, see log for details
|
RevokeUnknownLicencesFail: Not all AVS driving licences of unknown drivers could be revoked, see log for details
|
||||||
AvsCommunicationError: AVS interface returned an unexpected error.
|
AvsCommunicationError: AVS interface returned an unexpected error.
|
||||||
AvsCommunicationTimeout: AVS interface returned no response within timeout limit.
|
|
||||||
LicenceTableChangeAvs: Change in AVS
|
LicenceTableChangeAvs: Change in AVS
|
||||||
LicenceTableGrantFDrive: Grant in FRADrive
|
LicenceTableGrantFDrive: Grant in FRADrive
|
||||||
LicenceTableRevokeFDrive: Revoke in FRADrive
|
LicenceTableRevokeFDrive: Revoke in FRADrive
|
||||||
TableAvsActiveCards: Valid Cards
|
TableAvsActiveCards: Valid Cards
|
||||||
TableAvsCardValid: Currently valid
|
|
||||||
TableAvsCardIssueDate: Issued
|
|
||||||
TableAvsCardValidTo: Valid to
|
|
||||||
AvsCardAreas: Card areas
|
|
||||||
AvsCardColor: Color
|
|
||||||
AvsCardColorGreen: Green
|
AvsCardColorGreen: Green
|
||||||
AvsCardColorBlue: Blue
|
AvsCardColorBlue: Blue
|
||||||
AvsCardColorRed: Red
|
AvsCardColorRed: Red
|
||||||
AvsCardColorYellow: Yellow
|
AvsCardColorYellow: Yellow
|
||||||
LastAvsSynchronisation: Last AVS synchronisation
|
LastAvsSynchronisation: Last AVS synchronisation
|
||||||
LastAvsSyncedBefore: Last AVS synchronisation before
|
LastAvsSynchError: Last AVS Error
|
||||||
LastAvsSynchError: Last AVS Error
|
|
||||||
|
|
||||||
AvsInterfaceUnavailable: AVS interface was not configured correctly or does not respond
|
|
||||||
AvsUserUnassociated user: AVS id unknown for user #{user}
|
|
||||||
AvsUserUnknownByAvs api: AVS reports id #{tshow api} as unknown (or no longer known)
|
|
||||||
AvsUserAmbiguous api: Multiple matching users found for #{tshow api}
|
|
||||||
AvsStatusSearchEmpty: AVS returned no card information
|
|
||||||
AvsPersonSearchEmpty: AVS search returned empty result
|
|
||||||
AvsPersonSearchAmbiguous: AVS search returned more than one result
|
|
||||||
AvsSetLicencesFailed reason: Set driving licence within AVS failed. Reason: #{reason}
|
|
||||||
AvsIdMismatch api1 api2: AVS search for id #{tshow api1} returned id #{tshow api2} instead
|
|
||||||
AvsUserCreationFailed api@AvsPersonId: No new user could be created for AVS Id #{tshow api}, since an existing user shares at least one id presumed as unique
|
|
||||||
AvsCardsEmpty: AVS search returned no id cards
|
|
||||||
AvsCurrentData: All shown data has been recently received via the AVS interface.
|
|
||||||
@ -70,10 +70,6 @@ CourseInvalidInput: Eingaben bitte korrigieren.
|
|||||||
CourseEditTitle: Kursart editieren/anlegen
|
CourseEditTitle: Kursart editieren/anlegen
|
||||||
CourseEditOk tid@TermId ssh@SchoolId csh@CourseShorthand: Kursart #{tid}-#{ssh}-#{csh} wurde erfolgreich geändert.
|
CourseEditOk tid@TermId ssh@SchoolId csh@CourseShorthand: Kursart #{tid}-#{ssh}-#{csh} wurde erfolgreich geändert.
|
||||||
CourseEditDupShort tid@TermId ssh@SchoolId csh@CourseShorthand: Kursart #{tid}-#{ssh}-#{csh} konnte nicht geändert werden: Es gibt bereits einen andere Kursart mit dem selben Kürzel oder Titel in diesem Jahr und Bereich.
|
CourseEditDupShort tid@TermId ssh@SchoolId csh@CourseShorthand: Kursart #{tid}-#{ssh}-#{csh} konnte nicht geändert werden: Es gibt bereits einen andere Kursart mit dem selben Kürzel oder Titel in diesem Jahr und Bereich.
|
||||||
CourseEditQualificationFail: Eine Qualifikation konnte uas unbekanntem Grund nicht mit diesem Kurs assoziert werden.
|
|
||||||
CourseEditQualificationFailRights qsh@QualificationShorthand ssh@SchoolId: Qualifikation #{qsh} konnte nicht mit diesem Kurs assoziert werden, da Ihre Berechtigungen für Bereich #{ssh} dazu nicht ausreichen.
|
|
||||||
CourseEditQualificationFailExists: Diese Qualifikation ist bereits assoziert
|
|
||||||
CourseEditQualificationFailOrder: Diese Sortierpriorität existiert bereits
|
|
||||||
CourseLecturer: Kursverwalter:in
|
CourseLecturer: Kursverwalter:in
|
||||||
MailSubjectParticipantInvitation tid@TermId ssh@SchoolId csh@CourseShorthand: [#{tid}-#{ssh}-#{csh}] Einladung zur Kursartteilnahme
|
MailSubjectParticipantInvitation tid@TermId ssh@SchoolId csh@CourseShorthand: [#{tid}-#{ssh}-#{csh}] Einladung zur Kursartteilnahme
|
||||||
CourseParticipantInviteHeading courseName@Text: Einladung zum Kursartteilnahmer für #{courseName}
|
CourseParticipantInviteHeading courseName@Text: Einladung zum Kursartteilnahmer für #{courseName}
|
||||||
|
|||||||
@ -70,12 +70,8 @@ CourseInvalidInput: Invalid input
|
|||||||
CourseEditTitle: Edit/Create course
|
CourseEditTitle: Edit/Create course
|
||||||
CourseEditOk tid ssh csh: Successfully edited course type #{tid}-#{ssh}-#{csh}
|
CourseEditOk tid ssh csh: Successfully edited course type #{tid}-#{ssh}-#{csh}
|
||||||
CourseEditDupShort tid ssh csh: Could not edit course type #{tid}-#{ssh}-#{csh}. Another course type with the same shorthand or title already exists for the given year and school.
|
CourseEditDupShort tid ssh csh: Could not edit course type #{tid}-#{ssh}-#{csh}. Another course type with the same shorthand or title already exists for the given year and school.
|
||||||
CourseEditQualificationFail: A qualifikation could not be associated with this course for unknown reasons.
|
|
||||||
CourseEditQualificationFailRights qsh ssh: Qualification #{qsh} could not be associated with this course, due to your insufficient rights for department #{ssh}.
|
|
||||||
CourseEditQualificationFailExists: This qualification is already associated
|
|
||||||
CourseEditQualificationFailOrder: This sort order priority is used already
|
|
||||||
CourseLecturer: Course administrator
|
CourseLecturer: Course administrator
|
||||||
MailSubjectParticipantInvitation tid ssh csh: [#{tid}-#{ssh}-#{csh}] Invitaion to join the course
|
MailSubjectParticipantInvitation tid@TermId ssh@SchoolId csh@CourseShorthand: [#{tid}-#{ssh}-#{csh}] Invitaion to join the course
|
||||||
CourseParticipantInviteHeading courseName: Invitation to enrol for #{courseName}
|
CourseParticipantInviteHeading courseName: Invitation to enrol for #{courseName}
|
||||||
CourseParticipantInviteExplanation: You were invited to be a participant of a course.
|
CourseParticipantInviteExplanation: You were invited to be a participant of a course.
|
||||||
CourseParticipantInviteField: Email addresses to invite
|
CourseParticipantInviteField: Email addresses to invite
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
# SPDX-FileCopyrightText: 2023-24 Steffen Jost <s.jost@fraport.de>
|
# SPDX-FileCopyrightText: 2023 Steffen Jost <s.jost@fraport.de>
|
||||||
#
|
#
|
||||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
@ -7,6 +7,7 @@ FirmSuperForeign: Firmenfremde Ansprechpartner
|
|||||||
FirmSuperIrregular: Irreguläre Ansprechpartner
|
FirmSuperIrregular: Irreguläre Ansprechpartner
|
||||||
FirmAssociates: Firmenangehörige
|
FirmAssociates: Firmenangehörige
|
||||||
FirmContact: Firmenkontakt
|
FirmContact: Firmenkontakt
|
||||||
|
FirmNoContact: Keine allgemeinen Kontaktinformationen bekannt.
|
||||||
FirmEmail: Allgemeine Email
|
FirmEmail: Allgemeine Email
|
||||||
FirmAddress: Postanschrift
|
FirmAddress: Postanschrift
|
||||||
FirmDefaultPreferenceInfo: Diese Voreinstellungen gelten nur für neue Firmenangehörige
|
FirmDefaultPreferenceInfo: Diese Voreinstellungen gelten nur für neue Firmenangehörige
|
||||||
@ -15,15 +16,11 @@ FirmActionInfo: Betrifft alle Firmenangehörigen unter Ihrer Aufsicht.
|
|||||||
FirmActNotify: Mitteilung versenden
|
FirmActNotify: Mitteilung versenden
|
||||||
FirmActResetSupervision: Ansprechpartner für alle Firmenangehörigen zurücksetzen
|
FirmActResetSupervision: Ansprechpartner für alle Firmenangehörigen zurücksetzen
|
||||||
FirmActResetSuperKeep: Bisherige Ansprechpartner der Firmenangehörigen zusätzlich beibehalten?
|
FirmActResetSuperKeep: Bisherige Ansprechpartner der Firmenangehörigen zusätzlich beibehalten?
|
||||||
FirmActRemoveSupers: Alle rein firmenbezogenen Ansprechpartnerbeziehungen für diese Personen entfernen?
|
|
||||||
FirmActResetMutualSupervision: Ansprechpartner beaufsichtigen sich gegenseitig
|
FirmActResetMutualSupervision: Ansprechpartner beaufsichtigen sich gegenseitig
|
||||||
FirmActResetSupersKeepAll: Alle behalten
|
FirmActAddSupersvisors: Ansprechpartner hinzufügen
|
||||||
FirmActResetSupersRemoveAps: Nur Standardansprechpartner entfernen
|
|
||||||
FirmActResetSupersRemoveAll: Alle entfernen
|
|
||||||
FirmActAddSupervisors: Ansprechpartner hinzufügen
|
|
||||||
FirmActAddSupersEmpty: Es konnten keine Ansprechpartner hinzugefügt werden
|
FirmActAddSupersEmpty: Es konnten keine Ansprechpartner hinzugefügt werden
|
||||||
FirmActAddSupersSet n@Int64 postal@(Maybe Bool): #{n} Standardansprechpartner geändert #{maybeBoolMessage postal "" "und auf Briefversand geschaltet" "und Benachrichtigungen per Email gesetzt"}, aber nicht nicht aktiviert.
|
FirmActAddSupersSet n@Int64 postal@(Maybe Bool): #{n} Standardansprechpartner geändert #{maybeBoolMessage postal "" "und auf Briefversand geschaltet" "und Benachrichtigungen per Email gesetzt"}, aber nicht nicht aktiviert.
|
||||||
RemoveSupervisors ndef@Int64: #{ndef} Standardansprechpartner entfernt.
|
RemoveSupervisors ndef@Int64 nact@Int64: #{ndef} Standard Ansprechpartner entfernt#{bool ", aber noch nicht deaktiviert" (", " <> tshow nact <> " aktive Ansprechpartnerbeziehungen gelöscht") (nact > 0)}
|
||||||
FirmActChangeContactUser: Kontaktinformationen von allen Firmenangehörigen ändern
|
FirmActChangeContactUser: Kontaktinformationen von allen Firmenangehörigen ändern
|
||||||
FirmActChangeContactFirm: Kontaktinformationen der Firma ändern
|
FirmActChangeContactFirm: Kontaktinformationen der Firma ändern
|
||||||
FirmActChangeContactFirmInfo: Firmenkontaktinformationen werden nur für neue Firmenangehörige verwendet, für die sonst keine Kontaktinformationen vorliegen.
|
FirmActChangeContactFirmInfo: Firmenkontaktinformationen werden nur für neue Firmenangehörige verwendet, für die sonst keine Kontaktinformationen vorliegen.
|
||||||
@ -31,23 +28,17 @@ FirmActChangeContactFirmResult: Firmenkontaktinformationen geändert. Betrifft n
|
|||||||
FirmUserActNotify: Mitteilung versenden
|
FirmUserActNotify: Mitteilung versenden
|
||||||
FirmUserActResetSupervision: Ansprechpartner auf Firmenstandard zurücksetzen
|
FirmUserActResetSupervision: Ansprechpartner auf Firmenstandard zurücksetzen
|
||||||
FirmUserActSetSupervisor: Ansprechpartner ändern
|
FirmUserActSetSupervisor: Ansprechpartner ändern
|
||||||
FirmUserActChangeContact: Kontaktinformationen für ausgewählte Firmenangehörige ändern
|
|
||||||
FirmUserActChangeDetails: Firmenassoziation bearbeiten
|
|
||||||
FirmUserActRemove: Firmenassoziation entfernen
|
|
||||||
FirmUserActMkSuper: Zum Firmenansprechpartner ernennen
|
|
||||||
FirmUserActChangeDetailsResult n@Int64 t@Int64: Firmenassoziation von #{n}/#{t} #{pluralDE n "Firmenangehörigen" "Firmenangehörige"} wurden aktualisiert
|
|
||||||
FirmUserActChangeResult n@Int64 t@Int64: Benachrichtigungseinstellung für #{n}/#{t} #{pluralDE n "Firmenangehörigen" "Firmenangehörige"} wurden geändert
|
|
||||||
FirmUserActRemoveResult uc@Int64: #{uc} #{pluralDE uc "Firmenassoziation" "Firmenassoziationen"} entfernt.
|
|
||||||
FirmRemoveSupervision sup@Int64 sub@Int64: #{noneMoreDE sup "" (tshow sup <> " Ansprechpartnerbeziehungen wegen entferntem Ansprechpartner gelöscht. ")} #{noneOneMoreDE sub "Keine Ansprechpartnerbeziehung" "Eine Ansprechpartnerbeziehung" (tshow sup <> " Ansprechpartnerbeziehungen")} wegen entferntem Angesprochenem gelöscht.
|
|
||||||
FirmNewSupervisor: Neue individuelle Ansprechpartner hinzufügen
|
FirmNewSupervisor: Neue individuelle Ansprechpartner hinzufügen
|
||||||
FirmSetSupervisor: Existierende Ansprechpartner hinzufügen
|
FirmSetSupervisor: Existierende Ansprechpartner hinzufügen
|
||||||
FirmSetSupersReport nusr@Int64 nspr@Int64 nrem@Int64: Für #{nusr} Firmenangehörige wurden #{nspr} individuelle Ansprechpartner eingetragen#{bool "." (" und " <> tshow nrem <> " individuelle Ansprechpartnerbeziehungen gelöscht.") (nrem >0)}
|
FirmSetSupersReport nusr@Int64 nspr@Int64 nrem@Int64: Für #{nusr} Firmenangehörige wurden #{nspr} individuelle Ansprechpartner eingetragen#{bool "." (" und " <> tshow nrem <> " individuelle Ansprechpartnerbeziehungen gelöscht.") (nrem >0)}
|
||||||
|
FirmUserActMkSuper: Zum Firmenansprechpartner ernennen
|
||||||
|
FirmUserActChangeContact: Kontaktinformationen für ausgewählte Firmenangehörige ändern
|
||||||
FirmResetSupervision rem@Int64 set@Int64: #{tshow set} Ansprechpartner gesetzt#{bool mempty (", " <> tshow rem <> " zuvor gelöscht") (rem > 0)}
|
FirmResetSupervision rem@Int64 set@Int64: #{tshow set} Ansprechpartner gesetzt#{bool mempty (", " <> tshow rem <> " zuvor gelöscht") (rem > 0)}
|
||||||
FirmSuperActNotify: Mitteilung versenden
|
FirmSuperActNotify: Mitteilung versenden
|
||||||
FirmSuperActSwitchSuper: Standard Firmenansprechpartner abändern
|
FirmSuperActSwitchSuper: Standard Firmenansprechpartner abändern
|
||||||
FirmSuperActSwitchSuperInfo: Betrifft keine firmenfremden Ansprechpartner und ändert keine aktiven individuellen Ansprechpartnerbeziehungen. Gegebenfalls im Anschluss die Funktion "Ansprechpartner auf Firmenstandard zurücksetzen" nutzen.
|
FirmSuperActSwitchSuperInfo: Betrifft keine firmenfremden Ansprechpartner und ändert keine aktiven individuellen Ansprechpartnerbeziehungen. Gegebenfalls im Anschluss die Funktion "Ansprechpartner auf Firmenstandard zurücksetzen" nutzen.
|
||||||
FirmSuperActRMSuperDef: Firmenansprechpartner entfernen
|
FirmSuperActRMSuperDef: Firmenansprechpartner entfernen
|
||||||
FirmSuperActRMSuperActive: Aktive Ansprechpartnerbeziehungen innerhalb dieser Firma beenden?
|
FirmSuperActRMSuperActive: Auch aktive Ansprechpartnerbeziehungen innerhalb dieser Firma beenden
|
||||||
FirmsNotification: Firmen E-Mail versenden
|
FirmsNotification: Firmen E-Mail versenden
|
||||||
FirmNotification fsh@CompanyShorthand: E-Mail an #{fsh} senden
|
FirmNotification fsh@CompanyShorthand: E-Mail an #{fsh} senden
|
||||||
FirmsNotificationTitle: Firmen benachrichtigen
|
FirmsNotificationTitle: Firmen benachrichtigen
|
||||||
@ -56,23 +47,14 @@ FilterSupervisor: Hat aktiven Ansprechpartner
|
|||||||
FilterSupervisorCompany fsh@CompanyShorthand: Hat aktiven Ansprechpartner, #{fsh} der angehört
|
FilterSupervisorCompany fsh@CompanyShorthand: Hat aktiven Ansprechpartner, #{fsh} der angehört
|
||||||
FilterSupervisorForeign fsh@CompanyShorthand: Hat aktiven Ansprechpartner, der selbst nicht #{fsh} angehört
|
FilterSupervisorForeign fsh@CompanyShorthand: Hat aktiven Ansprechpartner, der selbst nicht #{fsh} angehört
|
||||||
FilterForeignSupervisor: Hat firmenfremde Ansprechpartner
|
FilterForeignSupervisor: Hat firmenfremde Ansprechpartner
|
||||||
FilterIsForeignSupervisee: Ist Ansprechpartner für Firmenfremde
|
|
||||||
FilterFirmExtern: Externe Firma
|
FilterFirmExtern: Externe Firma
|
||||||
FilterFirmExternTooltip: Hat die Firma eine Postanschrift im AVS?
|
|
||||||
FilterFirmPrimary: Ist primäre Firma in FRADrive
|
|
||||||
FilterHasQualification: Hat Firmenangehörige mit aktuell gültiger Qualifikation
|
|
||||||
FirmSupervisorOf fsh@CompanyShorthand: Ansprechpartner #{fsh} angehörig
|
FirmSupervisorOf fsh@CompanyShorthand: Ansprechpartner #{fsh} angehörig
|
||||||
FirmSupervisorIndependent: Ansprechpartner ohne jegliche Firmenzugehörigkeit
|
FirmSupervisorIndependent: Ansprechpartner ohne jegliche Firmenzugehörigkeit
|
||||||
FirmEmployeeOf fsh@CompanyShorthand: Firmenangehörige #{fsh}
|
FirmEmployeeOf fsh@CompanyShorthand: Firmenangehörige #{fsh}
|
||||||
NoCompanySelected: Bitte wählen Sie mindestens eine Firma aus.
|
NoCompanySelected: Bitte wählen Sie mindestens eine Firma aus.
|
||||||
TableIsDefaultSupervisor: Standardansprechpartner
|
TableIsDefaultSupervisor: Standardansprechpartner
|
||||||
TableSuperior: Vorgesetzter
|
|
||||||
TableIsDefaultReroute: Standardumleitung
|
TableIsDefaultReroute: Standardumleitung
|
||||||
FormFieldPostal: Benachrichtigungseinstellung
|
FormFieldPostal: Benachrichtigungseinstellung
|
||||||
FormFieldPostalTip: Gilt für alle Benachrichtigungen an diese Person, nicht nur für Umleitungen an diesen Ansprechpartner
|
FormFieldPostalTip: Gilt für alle Benachrichtigungen an diese Person, nicht nur für Umleitungen an diesen Ansprechpartner
|
||||||
FirmSupervisionKeyData: Kennzahlen Ansprechpartner
|
FirmUserChanges n@Int64: Benachrichtigungseinstellung für #{n} Firmenangehörige wurden geändert
|
||||||
CompanyUserPriority: Firmenpriorität
|
FirmSupervisionKeyData: Kennzahlen Ansprechpartner
|
||||||
CompanyUserPriorityTip: Firmenpriorität ist lediglich relativ zu anderen Firmenassoziation der Person
|
|
||||||
CompanyUserUseCompanyAddress: Verwendet Firmenkontaktaddresse
|
|
||||||
CompanyUserUseCompanyAddressTip: sofern im Benutzer keine Postanschrift hinterlegt ist
|
|
||||||
CompanyUserUseCompanyPostalError: Postalische Adresse muss leer bleiben, damit die Firmenanschrift genutzt wird!
|
|
||||||
@ -1,4 +1,4 @@
|
|||||||
# SPDX-FileCopyrightText: 2023-24 Steffen Jost <s.jost@fraport.de>
|
# SPDX-FileCopyrightText: 2023 Steffen Jost <s.jost@fraport.de>
|
||||||
#
|
#
|
||||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
@ -7,6 +7,7 @@ FirmSuperForeign: External supervisor
|
|||||||
FirmSuperIrregular: Irregular supervisor
|
FirmSuperIrregular: Irregular supervisor
|
||||||
FirmAssociates: Company associated users
|
FirmAssociates: Company associated users
|
||||||
FirmContact: Company Contact
|
FirmContact: Company Contact
|
||||||
|
FirmNoContact: No general contact information known.
|
||||||
FirmEmail: General company email
|
FirmEmail: General company email
|
||||||
FirmAddress: Postal address
|
FirmAddress: Postal address
|
||||||
FirmDefaultPreferenceInfo: Default setting for new company associates only
|
FirmDefaultPreferenceInfo: Default setting for new company associates only
|
||||||
@ -15,15 +16,11 @@ FirmActionInfo: Affects alle company associates under your supervision.
|
|||||||
FirmActNotify: Send message
|
FirmActNotify: Send message
|
||||||
FirmActResetSupervision: Reset supervisors for all company associates
|
FirmActResetSupervision: Reset supervisors for all company associates
|
||||||
FirmActResetSuperKeep: Additionally keep existing supervisors of company associates?
|
FirmActResetSuperKeep: Additionally keep existing supervisors of company associates?
|
||||||
FirmActRemoveSupers: Terminate all company related supervisonships?
|
|
||||||
FirmActResetMutualSupervision: Supervisors supervise each other
|
FirmActResetMutualSupervision: Supervisors supervise each other
|
||||||
FirmActResetSupersKeepAll: Keep all
|
FirmActAddSupersvisors: Add supervisors
|
||||||
FirmActResetSupersRemoveAps: Remove default supervisors only
|
|
||||||
FirmActResetSupersRemoveAll: Remove all
|
|
||||||
FirmActAddSupervisors: Add supervisors
|
|
||||||
FirmActAddSupersEmpty: No supervisors added
|
FirmActAddSupersEmpty: No supervisors added
|
||||||
FirmActAddSupersSet n postal: #{n} default company supervisors changed #{maybeBoolMessage postal "" "and switched to postal notifications" "and switched to email notifications"}, but not yet activated.
|
FirmActAddSupersSet n postal: #{n} default company supervisors changed #{maybeBoolMessage postal "" "and switched to postal notifications" "and switched to email notifications"}, but not yet activated.
|
||||||
RemoveSupervisors ndef: #{ndef} default supervisors removed.
|
RemoveSupervisors ndef nact: #{ndef} default supervisors removed#{bool ", but not yet deactivated" (" and " <> tshow nact <> " active supervisions terminated") (nact > 0)}
|
||||||
FirmActChangeContactUser: Change contact data for all company associates
|
FirmActChangeContactUser: Change contact data for all company associates
|
||||||
FirmActChangeContactFirm: Change company contact data
|
FirmActChangeContactFirm: Change company contact data
|
||||||
FirmActChangeContactFirmInfo: The company contact data is only used for new company associates that would habe no contact information of their own otherwise.
|
FirmActChangeContactFirmInfo: The company contact data is only used for new company associates that would habe no contact information of their own otherwise.
|
||||||
@ -31,23 +28,17 @@ FirmActChangeContactFirmResult: Company contact data changed, affecting future c
|
|||||||
FirmUserActNotify: Send message
|
FirmUserActNotify: Send message
|
||||||
FirmUserActResetSupervision: Reset supervisors to company default
|
FirmUserActResetSupervision: Reset supervisors to company default
|
||||||
FirmUserActSetSupervisor: Change supervision
|
FirmUserActSetSupervisor: Change supervision
|
||||||
FirmUserActChangeContact: Change contact data for selected company associates
|
|
||||||
FirmUserActChangeDetails: Edit company association
|
|
||||||
FirmUserActRemove: Delete company association
|
|
||||||
FirmUserActMkSuper: Mark as company supervisor
|
|
||||||
FirmUserActChangeDetailsResult n t: #{n}/#{t} #{pluralENs n "company association"} updated
|
|
||||||
FirmUserActChangeResult n t: Notification settings changed for #{n}/#{t} company #{pluralENs n "associate"}
|
|
||||||
FirmUserActRemoveResult uc: #{pluralENsN uc "Company association"} deleted.
|
|
||||||
FirmRemoveSupervision sup sub: #{noneMoreEN sup "" ((pluralENsN sup "supervision") <> " removed due to eliminated supervisors.")} #{noneMoreEN sub "No supervision" (pluralENsN sub "supervision")} removed due to eliminated supervisees.
|
|
||||||
FirmNewSupervisor: Appoint new individual supervisors
|
FirmNewSupervisor: Appoint new individual supervisors
|
||||||
FirmSetSupervisor: Add existing supervisors
|
FirmSetSupervisor: Add existing supervisors
|
||||||
FirmSetSupersReport nusr nspr nrem: #{nspr} individual supervisors set for #{nusr} company associates#{bool "." (" and " <> tshow nrem <> " other individual supervisions terminated.") (nrem >0)}
|
FirmSetSupersReport nusr@Int64 nspr@Int64 nrem@Int64: #{nspr} individal supervisors set for #{nusr} company associates#{bool "." (" and " <> tshow nrem <> " other individual supervisions terminated.") (nrem >0)}
|
||||||
FirmResetSupervision rem set: #{tshow set} supervisors set#{bool mempty (", " <> tshow rem <> " deleted before") (rem > 0)}
|
FirmResetSupervision rem set: #{tshow set} supervisors set#{bool mempty (", " <> tshow rem <> " deleted before") (rem > 0)}
|
||||||
|
FirmUserActChangeContact: Change contact data for selected company associates
|
||||||
|
FirmUserActMkSuper: Mark as company supervisor
|
||||||
FirmSuperActNotify: Send message
|
FirmSuperActNotify: Send message
|
||||||
FirmSuperActSwitchSuper: Change default company supervisor
|
FirmSuperActSwitchSuper: Change default company supervisor
|
||||||
FirmSuperActSwitchSuperInfo: Does not affect company-external supervisors and does not change any active individual supervisions. Additionally use reset action, if desired.
|
FirmSuperActSwitchSuperInfo: Does not affect company-external supervisors and does not change any active individal supervisions. Additionally use reset action, if desired.
|
||||||
FirmSuperActRMSuperDef: Remove default supervisor
|
FirmSuperActRMSuperDef: Remove default supervisor
|
||||||
FirmSuperActRMSuperActive: Terminate active supervisions within this company?
|
FirmSuperActRMSuperActive: Also remove active supervisions within this company
|
||||||
FirmsNotification: Send company notification e-mail
|
FirmsNotification: Send company notification e-mail
|
||||||
FirmNotification fsh: Send e-mail to #{fsh}
|
FirmNotification fsh: Send e-mail to #{fsh}
|
||||||
FirmsNotificationTitle: Company notification
|
FirmsNotificationTitle: Company notification
|
||||||
@ -56,23 +47,14 @@ FilterSupervisor: Has active supervisor
|
|||||||
FilterSupervisorCompany fsh: Has active company supervisor belonging to #{fsh}
|
FilterSupervisorCompany fsh: Has active company supervisor belonging to #{fsh}
|
||||||
FilterSupervisorForeign fsh: Has active supervisor not belonging to #{fsh}
|
FilterSupervisorForeign fsh: Has active supervisor not belonging to #{fsh}
|
||||||
FilterForeignSupervisor: Has company-external supervisors
|
FilterForeignSupervisor: Has company-external supervisors
|
||||||
FilterIsForeignSupervisee: Supervisor for company external users
|
|
||||||
FilterFirmExtern: External company
|
FilterFirmExtern: External company
|
||||||
FilterFirmExternTooltip: i.e. is a postal address registered within AVS?
|
|
||||||
FilterFirmPrimary: Is primary company in FRADrive
|
|
||||||
FilterHasQualification: Has company associates with currently valid qualification
|
|
||||||
FirmSupervisorOf fsh@CompanyShorthand: Supervisors belonging to #{fsh}
|
FirmSupervisorOf fsh@CompanyShorthand: Supervisors belonging to #{fsh}
|
||||||
FirmSupervisorIndependent: Independent supervisors
|
FirmSupervisorIndependent: Independent supervisors
|
||||||
FirmEmployeeOf fsh@CompanyShorthand: #{fsh} associated users
|
FirmEmployeeOf fsh@CompanyShorthand: #{fsh} associated users
|
||||||
NoCompanySelected: Select at least one company, please.
|
NoCompanySelected: Select at least one company, please.
|
||||||
TableIsDefaultSupervisor: Default supervisor
|
TableIsDefaultSupervisor: Default supervisor
|
||||||
TableSuperior: Superior
|
|
||||||
TableIsDefaultReroute: Default reroute
|
TableIsDefaultReroute: Default reroute
|
||||||
FormFieldPostal: Notification type
|
FormFieldPostal: Notification type
|
||||||
FormFieldPostalTip: Affects all notifications to this person, not just reroutes to this supervisor
|
FormFieldPostalTip: Affects all notifications to this person, not just reroutes to this supervisor
|
||||||
FirmSupervisionKeyData: Supervision key data
|
FirmUserChanges n: Notification settings changed for #{n} company associates
|
||||||
CompanyUserPriority: Company priority
|
FirmSupervisionKeyData: Supervision key data
|
||||||
CompanyUserPriorityTip: Company priority is relative to other company associations for a user
|
|
||||||
CompanyUserUseCompanyAddress: Use company postal address
|
|
||||||
CompanyUserUseCompanyAddressTip: if and only if the postal address of the user is empty
|
|
||||||
CompanyUserUseCompanyPostalError: Individual postal address must left empty for the company address to be used!
|
|
||||||
@ -18,7 +18,6 @@ PrintJobAcknowledgeFailed: Keine Druckaufträge bestätigt aufgrund zwischenzeit
|
|||||||
PrintJobAcknowledgeQuestion n@Int d@Text: #{n} #{pluralDE n "Druckauftrag" "Druckaufräge"} vom #{d} als gedruckt und versendet bestätigen?
|
PrintJobAcknowledgeQuestion n@Int d@Text: #{n} #{pluralDE n "Druckauftrag" "Druckaufräge"} vom #{d} als gedruckt und versendet bestätigen?
|
||||||
PrintJobAcknowledgements: Versanddatum von Briefen an
|
PrintJobAcknowledgements: Versanddatum von Briefen an
|
||||||
PrintRecipient: Empfänger
|
PrintRecipient: Empfänger
|
||||||
PrintAffected: Betroffener
|
|
||||||
PrintSender !ident-ok: Sender
|
PrintSender !ident-ok: Sender
|
||||||
PrintCourse: Kursarten
|
PrintCourse: Kursarten
|
||||||
PrintQualification: Qualifikation
|
PrintQualification: Qualifikation
|
||||||
@ -26,7 +25,4 @@ PrintPDF !ident-ok: PDF
|
|||||||
PrintManualRenewal: Vorfeldführerschein Renewal-Brief testweise versenden
|
PrintManualRenewal: Vorfeldführerschein Renewal-Brief testweise versenden
|
||||||
PrintLmsUser: E‑Learning Id
|
PrintLmsUser: E‑Learning Id
|
||||||
PrintJobs: Druckaufräge
|
PrintJobs: Druckaufräge
|
||||||
PrintLetterType: Brieftypkürzel
|
PrintLetterType: Brieftypkürzel
|
||||||
|
|
||||||
MCActDummy: Platzhalter
|
|
||||||
CCActDummy: Platzhalter
|
|
||||||
@ -18,7 +18,6 @@ PrintJobAcknowledgeFailed: No print-jobs acknowledged, due to intermediate chang
|
|||||||
PrintJobAcknowledgeQuestion n d: Mark #{n} #{pluralENs n "print-job"} issued on #{d} as printed and mailed already?
|
PrintJobAcknowledgeQuestion n d: Mark #{n} #{pluralENs n "print-job"} issued on #{d} as printed and mailed already?
|
||||||
PrintJobAcknowledgements: Sent-dates for Letter to
|
PrintJobAcknowledgements: Sent-dates for Letter to
|
||||||
PrintRecipient: Recipient
|
PrintRecipient: Recipient
|
||||||
PrintAffected: Affetcted
|
|
||||||
PrintSender: Sender
|
PrintSender: Sender
|
||||||
PrintCourse: Course type
|
PrintCourse: Course type
|
||||||
PrintQualification: Qualification
|
PrintQualification: Qualification
|
||||||
@ -26,7 +25,4 @@ PrintPDF: PDF
|
|||||||
PrintManualRenewal: Manual sending of an apron driver's licence renewal letter
|
PrintManualRenewal: Manual sending of an apron driver's licence renewal letter
|
||||||
PrintLmsUser: E‑learning id
|
PrintLmsUser: E‑learning id
|
||||||
PrintJobs: Print jobs
|
PrintJobs: Print jobs
|
||||||
PrintLetterType: Letter type shorthand
|
PrintLetterType: Letter type shorthand
|
||||||
|
|
||||||
MCActDummy: Placeholder
|
|
||||||
CCActDummy: Placeholder
|
|
||||||
@ -9,31 +9,23 @@ QualificationValidIndicator: Gültigkeit
|
|||||||
QualificationValidDuration: Gültigkeitsdauer
|
QualificationValidDuration: Gültigkeitsdauer
|
||||||
QualificationAuditDuration: Aufbewahrung Audit Log
|
QualificationAuditDuration: Aufbewahrung Audit Log
|
||||||
QualificationAuditDurationTooltip n@Int: Optionaler Zeitraum zur Löschung von E‑Learning Daten. Hinweis: Der E‑Learning Server kann seine anonymisierten Daten schon früher löschen, aber spätestens #{n} Tage nach Abschluss.
|
QualificationAuditDurationTooltip n@Int: Optionaler Zeitraum zur Löschung von E‑Learning Daten. Hinweis: Der E‑Learning Server kann seine anonymisierten Daten schon früher löschen, aber spätestens #{n} Tage nach Abschluss.
|
||||||
QualificationAuditDurationReuseError: Diese Qualifikation nutzt das E‑Learning einer anderen Qualifikation, für die derzeit keinen Löschzeitraum konfiguriert wurde.
|
|
||||||
QualificationRefreshWithin: Erneurerungszeitraum
|
QualificationRefreshWithin: Erneurerungszeitraum
|
||||||
QualificationRefreshWithinTooltip: Optionaler Zeitraum vor Ablauf für eine Benachrichtigung per Email. Bei aktiviertem automatischem E‑Learning wird dieses gestartet und die Benachrichtigung erfolgt per Brief oder Email.
|
QualificationRefreshWithinTooltip: Optionaler Zeitraum vor Ablauf für automatischen Start des E‑Learnings und Versand einer Benachrichtigung per Brief oder Email.
|
||||||
QualificationRefreshReminder: Zweite Erinnerung
|
QualificationRefreshReminder: 2. Erinnerung
|
||||||
QualificationRefreshReminderTooltip: Optionaler Zeitraum vor Ablauf zur Versendung einer zweiten Erinnerung per Brief oder Email mit identischen E‑Learning Zugangsdaten, sofern die Qualifikation noch gültig und das E‑Learning noch offen ist.
|
QualificationRefreshReminderTooltip: Optionaler Zeitraum vor Ablauf zur Versendung einer zweiten Erinnerung per Brief oder Email mit identischen Zugangsdaten, sofern in diesem Zeitraum vor Ablauf noch keine Ablaufbenachrichtigung versendet wurde.
|
||||||
QualificationElearningStart: Wird das E‑Learning automatisch gestartet?
|
QualificationElearningStart: Wird das E‑Learning automatisch gestartet?
|
||||||
QualificationElearningRenew: Verlängert ein erfolgreiches E‑Learning die Qualifikation automatisch um die reguläre Gültigkeitsdauer?
|
|
||||||
QualificationElearningLimit: Ist die Anzahl der E‑Learning Versuche limitiert?
|
|
||||||
QualificationElearningLimitMax n@Int: Maximal #{n} Versuche
|
|
||||||
QualificationElearningNoLimit: Nicht limitiert
|
|
||||||
QualificationExpiryNotification: Ungültigkeitsbenachrichtigung?
|
QualificationExpiryNotification: Ungültigkeitsbenachrichtigung?
|
||||||
QualificationExpiryNotificationTooltip: Nutzer werden benachrichtigt, wenn die Qualifikation ungültig wird, sofern der jeweilige Nutzer in seinen Benutzereinstellungen diese Art Benachrichtigung aktiviert hat.
|
QualificationExpiryNotificationTooltip: Nutzer werden benachrichtigt, wenn die Qualifikation ungültig wird, sofern der jeweilige Nutzer in seinen Benutzereinstellungen diese Art Benachrichtigung aktiviert hat.
|
||||||
TableQualificationCountActive: Aktive
|
TableQualificationCountActive: Aktive
|
||||||
TableQualificationCountActiveTooltip: Anzahl Personen mit momentan gültiger Qualifikation
|
TableQualificationCountActiveTooltip: Anzahl Personen mit momentan gültiger Qualifikation
|
||||||
TableQualificationCountTotal: Gesamt
|
TableQualificationCountTotal: Gesamt
|
||||||
TableQualificationLmsReuses: LMS nutzt
|
|
||||||
TableQualificationLmsReusesTooltip: Diese Qualifikation hat kein eigenes E‑Learning, sondern wird über das E‑Learning der angegebenen Qualifikation abgewickelt.
|
|
||||||
TableQualificationIsAvsLicence: AVS
|
TableQualificationIsAvsLicence: AVS
|
||||||
TableQualificationIsAvsLicenceTooltip: Unter welchem Namen wird diese Qualifikation mit dem Ausweisverwaltungssystem (AVS) synchronisiert? Betrifft nur Benutzer mit AVS PersonenID.
|
TableQualificationIsAvsLicenceTooltip: Unter welchem Namen wird diese Qualifikation mit dem Ausweisverwaltungssystem (AVS) synchronisiert? Betrifft nur Benutzer mit AVS PersonenID.
|
||||||
TableQualificationSapExport: SAP
|
TableQualificationSapExport: SAP
|
||||||
TableQualificationSapExportTooltip: Wird die Qualifikation an das SAP übermittelt? Betrifft nur Benutzer mit Fraport Personalnummer.
|
TableQualificationSapExportTooltip: Wird die Qualifikation an das SAP übermittelt? Betrifft nur Benutzer mit Fraport Personalnummer.
|
||||||
LmsQualificationValidUntil: Gültig bis
|
LmsQualificationValidUntil: Gültig bis
|
||||||
TableQualificationLastRefresh: Zuletzt erneuert
|
TableQualificationLastRefresh: Zuletzt erneuert
|
||||||
TableQualificationLastNotified: Letzte Benachrichtigung über erfolgte Gültigkeitsänderung
|
TableQualificationLastNotified: Letzte Benachrichtigung
|
||||||
TableQualificationLastNotifiedTooltip: Hier werden ausschließlich Benachrichtigungen berücksichtigt, die über einen bereits erfolgten Ablauf/Entzug/Wiedererteilung informieren. Dies ignoriert insbesondere reguläre Verlängerung, z.B. durch E-Learning.
|
|
||||||
TableQualificationFirstHeld: Erstmalig
|
TableQualificationFirstHeld: Erstmalig
|
||||||
TableQualificationBlockedDue: Entzug
|
TableQualificationBlockedDue: Entzug
|
||||||
TableQualificationBlockedTooltip: Wann wurde die Qualifikation vorübergehend außer Kraft gesetzt und warum wurde dies veranlasst?
|
TableQualificationBlockedTooltip: Wann wurde die Qualifikation vorübergehend außer Kraft gesetzt und warum wurde dies veranlasst?
|
||||||
@ -54,13 +46,11 @@ QualificationExpired: Ungültig seit
|
|||||||
LmsUser: Inhaber
|
LmsUser: Inhaber
|
||||||
LmsURL: Link E‑Learning
|
LmsURL: Link E‑Learning
|
||||||
TableLmsEmail: E‑Mail
|
TableLmsEmail: E‑Mail
|
||||||
TableLmsIdent: E‑Learning Benutzer
|
TableLmsIdent: E-Learning Benutzer
|
||||||
TableLmsElearning: E‑Learning
|
TableLmsElearning: E‑Learning
|
||||||
TableLmsElearningRenews: Automatische Verlängerung
|
|
||||||
TableLmsElearningLimit: Maximale Versuche
|
|
||||||
TableLmsPin: E‑Learning Passwort
|
TableLmsPin: E‑Learning Passwort
|
||||||
TableLmsResetPin: E‑Learning Passwort zurücksetzen?
|
TableLmsResetPin: E-Learning Passwort zurücksetzen?
|
||||||
TableLmsDatePin: E‑Learning Passwort erstellt
|
TableLmsDatePin: E-Learning Passwort erstellt
|
||||||
TableLmsDate: Datum
|
TableLmsDate: Datum
|
||||||
TableLmsDelete: Löschen?
|
TableLmsDelete: Löschen?
|
||||||
TableLmsStaff: Interner Mitarbeiter?
|
TableLmsStaff: Interner Mitarbeiter?
|
||||||
@ -98,8 +88,7 @@ LmsReportInsert: Neues LMS Ereignis
|
|||||||
LmsReportUpdate: LMS Ereignis Aktualisierung
|
LmsReportUpdate: LMS Ereignis Aktualisierung
|
||||||
LmsReportCsvExceptionDuplicatedKey: CSV-Import LmsReport fand uneindeutigen Schlüssel
|
LmsReportCsvExceptionDuplicatedKey: CSV-Import LmsReport fand uneindeutigen Schlüssel
|
||||||
LmsDirectUpload: Direkter Upload für automatisierte Systeme
|
LmsDirectUpload: Direkter Upload für automatisierte Systeme
|
||||||
LmsErrorNoRefreshElearning: Fehler: E‑Learning wird nicht automatisch gestartet, da die Zeitspanne für den Erneurerungszeitraum nicht festgelegt wurde!
|
LmsErrorNoRefreshElearning: Fehler: E‑Learning wird nicht automatisch gestartet, da die Zeitspanne für den Erneurerungszeitraum nicht festgelegt wurde.
|
||||||
LmsErrorNoRenewElearning: Fehler: Erfoglreiches E‑Learning verlängert die Qualifikation nicht automatisch, da die Gültigkeitsdauer nicht festgelegt wurde!
|
|
||||||
MailSubjectQualificationRenewal qname@Text: Qualifikation #{qname} muss demnächst erneuert werden
|
MailSubjectQualificationRenewal qname@Text: Qualifikation #{qname} muss demnächst erneuert werden
|
||||||
MailSubjectQualificationExpiry qname@Text: Qualifikation #{qname} läuft demnächst ab
|
MailSubjectQualificationExpiry qname@Text: Qualifikation #{qname} läuft demnächst ab
|
||||||
MailSubjectQualificationExpired qname@Text: Qualifikation #{qname} ist ab sofort ungültig
|
MailSubjectQualificationExpired qname@Text: Qualifikation #{qname} ist ab sofort ungültig
|
||||||
@ -117,13 +106,11 @@ QualificationActUnblock: Entzug aufheben
|
|||||||
QualificationActRenew: Qualifikation regulär verlängern
|
QualificationActRenew: Qualifikation regulär verlängern
|
||||||
QualificationActGrant: Qualifikation vergeben
|
QualificationActGrant: Qualifikation vergeben
|
||||||
QualificationActGrantWarning: Diese Funktion ist nur für seltene Ausnahmefälle vorgesehen! Ein Entzug wird ggf. aufgehoben.
|
QualificationActGrantWarning: Diese Funktion ist nur für seltene Ausnahmefälle vorgesehen! Ein Entzug wird ggf. aufgehoben.
|
||||||
QualificationActStartELearning: E‑Learning für gültige Inhaber (neu) starten
|
|
||||||
QualificationActStartELearningStatus l@QualificationShorthand n@Int m@Int: E‑Learning #{l} für #{n}/#{m} Teilnehmer (neu) gestartet. Hinweis: Es kann länger dauern, bis das LMS tatsächlich startet.
|
|
||||||
QualificationStatusBlock l@QualificationShorthand n@Int m@Int: #{n}/#{m} #{l} entzogen
|
QualificationStatusBlock l@QualificationShorthand n@Int m@Int: #{n}/#{m} #{l} entzogen
|
||||||
QualificationStatusUnblock l@QualificationShorthand n@Int m@Int: #{n}/#{m} #{l} reaktiviert
|
QualificationStatusUnblock l@QualificationShorthand n@Int m@Int: #{n}/#{m} #{l} reaktiviert
|
||||||
LmsInactive: Aktuell kein E‑Learning aktiv
|
LmsInactive: Aktuell kein E‑Learning aktiv
|
||||||
LmsRenewalInstructions: Weitere Anweisungen zur Verlängerung finden Sie im angehängten PDF. Um Missbrauch zu verhindern wurde das PDF mit dem im FRADrive hinterlegten PDF-Passwort des Prüflings verschlüsselt. Falls kein PDF-Passwort manuell hinterlegt wurde, ist das PDF-Passwort die Flughafen Ausweisnummer, inklusive Punkt und der Ziffer danach.
|
LmsRenewalInstructions: Weitere Anweisungen zur Verlängerung finden Sie im angehängten PDF. Um Missbrauch zu verhindern wurde das PDF mit dem im FRADrive hinterlegten PDF-Passwort des Prüflings verschlüsselt. Falls kein PDF-Passwort manuell hinterlegt wurde, ist das PDF-Passwort die Flughafen Ausweisnummer, inklusive Punkt und der Ziffer danach.
|
||||||
LmsNoRenewal: Leider kann diese Qualifikation nicht alleine durch E‑Learning verlängert werden. Bitte setzen Sie sich mit uns in Verbindung, wenn Sie die Qualifikation verlängern möchten und noch nicht wissen, wie Sie das tun können. Ignorieren Sie diese automatisch generierte Erinnerung, falls Sie sich bereits um die Verlängerung gekümmert haben
|
LmsNoRenewal: Leider kann diese Qualifikation nicht alleine durch E‑Learning verlängert werden.
|
||||||
LmsRenewalReminder: Erinnerung
|
LmsRenewalReminder: Erinnerung
|
||||||
LmsActNotify: Benachrichtigung E‑Learning erneut per Post oder E-Mail versenden
|
LmsActNotify: Benachrichtigung E‑Learning erneut per Post oder E-Mail versenden
|
||||||
LmsActRenewPin: Neues zufällige E‑Learning Passwort zuweisen
|
LmsActRenewPin: Neues zufällige E‑Learning Passwort zuweisen
|
||||||
@ -132,7 +119,7 @@ LmsActReset: E‑Learning Fehlversuche zurücksetzen und entsperren
|
|||||||
LmsActResetInfo: E‑Learning Login, Passwort und Fortschritt bleiben unverändert, eine neue Benachrichtigung ist nicht notwendig. Nur möglich für bereits gesperrte Lerner. Es kann bis zu 2 Stunden dauern, bis das LMS die Anfrage umgesetzt hat.
|
LmsActResetInfo: E‑Learning Login, Passwort und Fortschritt bleiben unverändert, eine neue Benachrichtigung ist nicht notwendig. Nur möglich für bereits gesperrte Lerner. Es kann bis zu 2 Stunden dauern, bis das LMS die Anfrage umgesetzt hat.
|
||||||
LmsActResetFeedback n@Int m@Int: Für #{n}/#{m} E‑Learning Nutzer wurden alle Fehlversuche zurückgesetzt.
|
LmsActResetFeedback n@Int m@Int: Für #{n}/#{m} E‑Learning Nutzer wurden alle Fehlversuche zurückgesetzt.
|
||||||
LmsActRestart: E‑Learning komplett neu starten
|
LmsActRestart: E‑Learning komplett neu starten
|
||||||
LmsActRestartWarning: Das vorhandene E‑Learning wird komplett gelöscht! Für Inhaber einer gültigen Lizenz werden später Benutzer und Passwort neu vergeben und es sollte eine neue Benachrichtigung versendet werden. Hinweis: Es kann mehrere Stunden dauern, bis das LMS diese Anfrage umgesetzt hat.
|
LmsActRestartWarning: Das vorhandene E‑Learning wird komplett gelöscht! Für Inhaber einer gültigen Fahrlizenz werden später Benutzer und Passwort neu vergeben und es sollte eine neue Benachrichtigung versendet werden. Hinweis: Es kann mehrere Stunden dauern, bis das LMS diese Anfrage umgesetzt hat.
|
||||||
LmsActRestartFeedback n@Int m@Int: #{n}/#{m} E‑Learning Nutzer wurden komplett neu gestartet mit neuem Login und Passwort.
|
LmsActRestartFeedback n@Int m@Int: #{n}/#{m} E‑Learning Nutzer wurden komplett neu gestartet mit neuem Login und Passwort.
|
||||||
LmsActRestartExtend: Gültig bis ggf. erhöhen für die nächsten # Tage
|
LmsActRestartExtend: Gültig bis ggf. erhöhen für die nächsten # Tage
|
||||||
LmsActRestartUnblock: Entzug ggf. aufheben
|
LmsActRestartUnblock: Entzug ggf. aufheben
|
||||||
|
|||||||
@ -9,31 +9,23 @@ QualificationValidIndicator: Validity
|
|||||||
QualificationValidDuration: Validity period
|
QualificationValidDuration: Validity period
|
||||||
QualificationAuditDuration: Audit log retention period
|
QualificationAuditDuration: Audit log retention period
|
||||||
QualificationAuditDurationTooltip n@Int: Optional period for deletion of e‑learning data. Note that the e‑learning server may delete its anonymised data earlier, at most #{n} days after closing.
|
QualificationAuditDurationTooltip n@Int: Optional period for deletion of e‑learning data. Note that the e‑learning server may delete its anonymised data earlier, at most #{n} days after closing.
|
||||||
QualificationAuditDurationReuseError: This qualification reuses the e‑learning from another qualification, which has no audit duration configured.
|
|
||||||
QualificationRefreshWithin: Refresh within
|
QualificationRefreshWithin: Refresh within
|
||||||
QualificationRefreshWithinTooltip: Optional period before expiry to send a notification by email. If e‑learning is set to start automatically, it will be started and e‑learning credentials are send with this notification by post or email.
|
QualificationRefreshWithinTooltip: Optional period before expiry to start e‑learning and send a notification by post or email.
|
||||||
QualificationRefreshReminder: Second reminder
|
QualificationRefreshReminder: 2. Reminder
|
||||||
QualificationRefreshReminderTooltip: Optional period before expiry to send a second notification by post or email once more, including the existing credentials, provided that the e‑learning is still undecided and the qualification has not yet expired.
|
QualificationRefreshReminderTooltip: Optional period before expiry to send a second notification by post or email once more, provided that no renewal notification was sent in this period before expiry.
|
||||||
QualificationElearningStart: Is e‑learning automatically started?
|
QualificationElearningStart: Is e‑learning automatically started?
|
||||||
QualificationElearningRenew: Does successful e‑learning automatically extend a qualification by the default validity period?
|
|
||||||
QualificationElearningLimit: Is the number of e‑learning attempts limited?
|
|
||||||
QualificationElearningLimitMax n: #{n} attempts maximum
|
|
||||||
QualificationElearningNoLimit: No limit
|
|
||||||
QualificationExpiryNotification: Invalidity notification?
|
QualificationExpiryNotification: Invalidity notification?
|
||||||
QualificationExpiryNotificationTooltip: Qualification holder are notfied upon invalidity, provided they have activated such notification in their user settings.
|
QualificationExpiryNotificationTooltip: Qualification holder are notfied upon invalidity, provided they have activated such notification in their user settings.
|
||||||
TableQualificationCountActive: Active
|
TableQualificationCountActive: Active
|
||||||
TableQualificationCountActiveTooltip: Number of currently valid qualification holders
|
TableQualificationCountActiveTooltip: Number of currently valid qualification holders
|
||||||
TableQualificationCountTotal: Total
|
TableQualificationCountTotal: Total
|
||||||
TableQualificationLmsReuses: Reuse LMS
|
|
||||||
TableQualificationLmsReusesTooltip: This qualification reuses the e‑learning of the given qualification, instead of having a separate e‑learning of its own.
|
|
||||||
TableQualificationIsAvsLicence: AVS driving license
|
TableQualificationIsAvsLicence: AVS driving license
|
||||||
TableQualificationIsAvsLicenceTooltip: Under which name is this qualification synchronized with AVS, if any? Only applies to qualification holders having an AVS PersonID.
|
TableQualificationIsAvsLicenceTooltip: Under which name is this qualification synchronized with AVS, if any? Only applies to qualification holders having an AVS PersonID.
|
||||||
TableQualificationSapExport: Sent to SAP
|
TableQualificationSapExport: Sent to SAP
|
||||||
TableQualificationSapExportTooltip: Is this qualification transmitted to SAP? Only applies to qualification holder having a Fraport AG personnel number.
|
TableQualificationSapExportTooltip: Is this qualification transmitted to SAP? Only applies to qualification holder having a Fraport AG personnel number.
|
||||||
LmsQualificationValidUntil: Valid until
|
LmsQualificationValidUntil: Valid until
|
||||||
TableQualificationLastRefresh: Last renewed
|
TableQualificationLastRefresh: Last renewed
|
||||||
TableQualificationLastNotified: Last notified about validity change
|
TableQualificationLastNotified: Last notified
|
||||||
TableQualificationLastNotifiedTooltip: The date of the last notification about any already effective change in validity due to revocation or reissue. This does not entail regular validity extensions, e.g. due to e-learning.
|
|
||||||
TableQualificationFirstHeld: First held
|
TableQualificationFirstHeld: First held
|
||||||
TableQualificationBlockedDue: Revocations
|
TableQualificationBlockedDue: Revocations
|
||||||
TableQualificationBlockedTooltip: Why and when was this qualification temporarily suspended?
|
TableQualificationBlockedTooltip: Why and when was this qualification temporarily suspended?
|
||||||
@ -57,8 +49,6 @@ TableLmsEmail: Email
|
|||||||
TableLmsIdent: E‑learning user
|
TableLmsIdent: E‑learning user
|
||||||
TableLmsPin: E‑learning password
|
TableLmsPin: E‑learning password
|
||||||
TableLmsElearning: E‑learning
|
TableLmsElearning: E‑learning
|
||||||
TableLmsElearningRenews: Automatic renewal
|
|
||||||
TableLmsElearningLimit: Max attempts
|
|
||||||
TableLmsResetPin: Reset E‑learning password?
|
TableLmsResetPin: Reset E‑learning password?
|
||||||
TableLmsDatePin: E‑learning password created
|
TableLmsDatePin: E‑learning password created
|
||||||
TableLmsDate: Date
|
TableLmsDate: Date
|
||||||
@ -98,8 +88,7 @@ LmsReportInsert: New LMS event
|
|||||||
LmsReportUpdate: Update of LMS event
|
LmsReportUpdate: Update of LMS event
|
||||||
LmsReportCsvExceptionDuplicatedKey: CSV Import LmsReport with ambiguous key
|
LmsReportCsvExceptionDuplicatedKey: CSV Import LmsReport with ambiguous key
|
||||||
LmsDirectUpload: Direct upload for automated systems
|
LmsDirectUpload: Direct upload for automated systems
|
||||||
LmsErrorNoRefreshElearning: Error: E‑learning will not be started automatically due to refresh-within time period not being set!
|
LmsErrorNoRefreshElearning: Error: E‑learning will not be started automatically due to refresh-within time period not being set.
|
||||||
LmsErrorNoRenewElearning: Error: E‑learning will not automatically extend validity due to validity duration not being set!
|
|
||||||
MailSubjectQualificationRenewal qname: Qualification #{qname} must be renewed shortly
|
MailSubjectQualificationRenewal qname: Qualification #{qname} must be renewed shortly
|
||||||
MailSubjectQualificationExpiry qname: Qualification #{qname} expires soon
|
MailSubjectQualificationExpiry qname: Qualification #{qname} expires soon
|
||||||
MailSubjectQualificationExpired qname: Qualification #{qname} is no longer valid
|
MailSubjectQualificationExpired qname: Qualification #{qname} is no longer valid
|
||||||
@ -117,13 +106,11 @@ QualificationActUnblock: Clear revocation
|
|||||||
QualificationActRenew: Renew qualification
|
QualificationActRenew: Renew qualification
|
||||||
QualificationActGrant: Grant qualification
|
QualificationActGrant: Grant qualification
|
||||||
QualificationActGrantWarning: Use with caution in rare exceptional cases only! Any revocation will be undone.
|
QualificationActGrantWarning: Use with caution in rare exceptional cases only! Any revocation will be undone.
|
||||||
QualificationActStartELearning: Manually (re)start e‑learning for valid qualification holders
|
|
||||||
QualificationActStartELearningStatus l n m: E‑learning #{l} (re)started for #{n}/#{m} users. Note: It may take a while, until the e‑learning is activated.
|
|
||||||
QualificationStatusBlock l n m: #{n}/#{m} #{l} revoked
|
QualificationStatusBlock l n m: #{n}/#{m} #{l} revoked
|
||||||
QualificationStatusUnblock l n m: #{n}/#{m} #{l} reactivated
|
QualificationStatusUnblock l n m: #{n}/#{m} #{l} reactivated
|
||||||
LmsInactive: Currently no active e‑learning
|
LmsInactive: Currently no active e‑learning
|
||||||
LmsRenewalInstructions: Instruction on how to accomplish the renewal are enclosed in the attached PDF. In order to avoid misuse, the PDF is encrypted with the FRADrive PDF-password of the examinee. If no PDF-password had been chosen yet, then the password is the Fraport id card number of the examinee, including the punctuation mark and the digit thereafter.
|
LmsRenewalInstructions: Instruction on how to accomplish the renewal are enclosed in the attached PDF. In order to avoid misuse, the PDF is encrypted with the FRADrive PDF-password of the examinee. If no PDF-password had been chosen yet, then the password is the Fraport id card number of the examinee, including the punctuation mark and the digit thereafter.
|
||||||
LmsNoRenewal: Unfortunately, this particular qualification cannot be renewed through e‑learning only. Please contact us, if you do not yet know how to renew this qualification. Ignore this automatically generated reminder email, if you have made arrangements for the renewal of this qualification already.
|
LmsNoRenewal: Unfortunately, this particular qualification cannot be renewed through e‑learning only.
|
||||||
LmsRenewalReminder: Reminder
|
LmsRenewalReminder: Reminder
|
||||||
LmsActNotify: Resend e‑learning notification by post or email
|
LmsActNotify: Resend e‑learning notification by post or email
|
||||||
LmsActRenewPin: Randomly replace e‑learning password
|
LmsActRenewPin: Randomly replace e‑learning password
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
# SPDX-FileCopyrightText: 2022 Gregor Kleen <gregor.kleen@ifi.lmu.de>,Steffen Jost <jost@tcs.ifi.lmu.de>,Winnie Ros <winnie.ros@campus.lmu.de>
|
# SPDX-FileCopyrightText: 2022-2024 Sarah Vaupel <sarah.vaupel@uniworx.de>, Gregor Kleen <gregor.kleen@ifi.lmu.de>, Steffen Jost <jost@tcs.ifi.lmu.de>, Winnie Ros <winnie.ros@campus.lmu.de>
|
||||||
#
|
#
|
||||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
@ -75,11 +75,10 @@ NotPassed: Nicht bestanden
|
|||||||
|
|
||||||
#userAuthModeUpdate.hs + templates
|
#userAuthModeUpdate.hs + templates
|
||||||
MailSubjectUserAuthModeUpdate: Ihr FRADrive-Login
|
MailSubjectUserAuthModeUpdate: Ihr FRADrive-Login
|
||||||
UserAuthModeChangedToLDAP: Sie können sich nun mit Ihrer Fraport AG Kennung (Büko) in FRADrive einloggen.
|
UserAuthPasswordEnabled: Sie können sich nun mit einer FRADrive-internen Kennung einloggen.
|
||||||
UserAuthModeChangedToPWHash: Sie können sich nun mit einer FRADrive-internen Kennung einloggen.
|
UserAuthPasswordDisabled: Sie können sich nun nicht mehr mit Ihrer FRADrive-internen Kennung einloggen.
|
||||||
UserAuthModeChangedToNoLogin: Ihr Login auf der FRADrive Webseite wurde deaktiviert, aber ihr FRADrive Konto besteht weiterhin. Gültigkeit und Verlängerungen Ihrer Qualifikationen sind dadurch nicht beeinträchtigt. Wenden Sie sich an die Fahrschuladmins, wenn der Login auf der FRADrive Webseite benötigt werden sollte.
|
AuthExternalLoginTip: Sollten Sie Zugriff zu einem von FRADrive unterstützten externen Account (Azure-Login über Fraport-Kennung, Fraport-BüKo-Login) besitzen, so können Sie sich mit Ihren externen Login-Daten in FRADrive einloggen.
|
||||||
AuthPWHashTip: Sie müssen nun das mit "FRADrive-Login" beschriftete Login-Formular verwenden. Stellen Sie bitte sicher, dass Sie ein Passwort gesetzt haben, bevor Sie versuchen sich anzumelden.
|
PasswordResetEmailIncoming: Einen Link um ihr Passwort zu setzen bzw. zu ändern bekommen Sie aus Sicherheitsgründen in einer separaten E-Mail.
|
||||||
PasswordResetEmailIncoming: Einen Link um ihr Passwort zu setzen bzw. zu ändern bekommen Sie, aus Sicherheitsgründen, in einer separaten E-Mail.
|
|
||||||
MailFradrive !ident-ok: FRADrive
|
MailFradrive !ident-ok: FRADrive
|
||||||
MailBodyFradrive: ist die Führerscheinverwaltungsapp der Fraport AG.
|
MailBodyFradrive: ist die Führerscheinverwaltungsapp der Fraport AG.
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
# SPDX-FileCopyrightText: 2022 Gregor Kleen <gregor.kleen@ifi.lmu.de>,Steffen Jost <jost@tcs.ifi.lmu.de>,Winnie Ros <winnie.ros@campus.lmu.de>
|
# SPDX-FileCopyrightText: 2022-2024 Sarah Vaupel <sarah.vaupel@uniworx.de>, Gregor Kleen <gregor.kleen@ifi.lmu.de>, Steffen Jost <jost@tcs.ifi.lmu.de>, Winnie Ros <winnie.ros@campus.lmu.de>
|
||||||
#
|
#
|
||||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
@ -75,10 +75,9 @@ NotPassed: Failed
|
|||||||
|
|
||||||
#userAuthModeUpdate.hs + templates
|
#userAuthModeUpdate.hs + templates
|
||||||
MailSubjectUserAuthModeUpdate: Your FRADrive login
|
MailSubjectUserAuthModeUpdate: Your FRADrive login
|
||||||
UserAuthModeChangedToLDAP: You can now log in to FRADrive using your Fraport AG account (Büko)
|
UserAuthPasswordEnabled: You can now log in using your FRADrive-internal account credentials.
|
||||||
UserAuthModeChangedToPWHash: You can now log in using your FRADrive-internal account
|
UserAuthPasswordDisabled: You can no longer log in using your FRADrive-internal account credentials.
|
||||||
UserAuthModeChangedToNoLogin: Your login for the FRADrive website has been deactivated, but you FRADrive account persists. This has no effect on you qualifications. Please contact the driving school admins, if you need new login credentials for the FRADrive website.
|
AuthExternalLoginTip: If you have access to an external account supported by FRADrive (Azure login via Fraport identification, Fraport-BüKo login), you can login in FRADrive using your external credentials.
|
||||||
AuthPWHashTip: You now need to use the login form labeled "FRADrive login". Please ensure that you have already set a password when you try to log in.
|
|
||||||
PasswordResetEmailIncoming: For security reasons you will receive a link to the page on which you can set and later change your password in a separate email.
|
PasswordResetEmailIncoming: For security reasons you will receive a link to the page on which you can set and later change your password in a separate email.
|
||||||
MailFradrive: FRADrive
|
MailFradrive: FRADrive
|
||||||
MailBodyFradrive: is the apron driver's licence management app of Fraport AG.
|
MailBodyFradrive: is the apron driver's licence management app of Fraport AG.
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
# SPDX-FileCopyrightText: 2022 Gregor Kleen <gregor.kleen@ifi.lmu.de>,Steffen Jost <jost@tcs.ifi.lmu.de>,Winnie Ros <winnie.ros@campus.lmu.de>
|
# SPDX-FileCopyrightText: 2022-2024 Sarah Vaupel <sarah.vaupel@uniworx.de>, Gregor Kleen <gregor.kleen@ifi.lmu.de>, Steffen Jost <jost@tcs.ifi.lmu.de>, Winnie Ros <winnie.ros@campus.lmu.de>
|
||||||
#
|
#
|
||||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
@ -45,8 +45,8 @@ AuthTagUserSubmissions: Abgaben erfolgen durch Kursartteilnehmer:innen
|
|||||||
AuthTagCorrectorSubmissions: Abgaben erfolgen durch Korrektor:innen
|
AuthTagCorrectorSubmissions: Abgaben erfolgen durch Korrektor:innen
|
||||||
AuthTagCorrectionAnonymous: Korrektur ist anonymisiert
|
AuthTagCorrectionAnonymous: Korrektur ist anonymisiert
|
||||||
AuthTagSelf: Nutzer:in greift nur auf eigene Daten zu
|
AuthTagSelf: Nutzer:in greift nur auf eigene Daten zu
|
||||||
AuthTagIsLDAP: Nutzer:in meldet sich mit Fraport AG Kennung an
|
AuthTagIsExternal: Nutzer:in meldet sich mit extern verwalteten Logindaten an
|
||||||
AuthTagIsPWHash: Nutzer:in meldet sich mit FRADrive spezifischer Kennung an
|
AuthTagIsInternal: Nutzer:in meldet sich mit FRADrive-internen Logindaten an
|
||||||
AuthTagAuthentication: Nutzer:in ist angemeldet, falls erforderlich
|
AuthTagAuthentication: Nutzer:in ist angemeldet, falls erforderlich
|
||||||
AuthTagRead: Zugriff ist nur lesend
|
AuthTagRead: Zugriff ist nur lesend
|
||||||
AuthTagWrite: Zugriff ist i.A. schreibend
|
AuthTagWrite: Zugriff ist i.A. schreibend
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
# SPDX-FileCopyrightText: 2022 Gregor Kleen <gregor.kleen@ifi.lmu.de>,Sarah Vaupel <sarah.vaupel@ifi.lmu.de>,Steffen Jost <jost@tcs.ifi.lmu.de>,Winnie Ros <winnie.ros@campus.lmu.de>
|
# SPDX-FileCopyrightText: 2022-2024 Sarah Vaupel <sarah.vaupel@uniworx.de>, Gregor Kleen <gregor.kleen@ifi.lmu.de>, Sarah Vaupel <sarah.vaupel@ifi.lmu.de>, Steffen Jost <jost@tcs.ifi.lmu.de>, Winnie Ros <winnie.ros@campus.lmu.de>
|
||||||
#
|
#
|
||||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
@ -45,8 +45,8 @@ AuthTagUserSubmissions: Submissions are made by course type participants
|
|||||||
AuthTagCorrectorSubmissions: Submissions are registered by correctors
|
AuthTagCorrectorSubmissions: Submissions are registered by correctors
|
||||||
AuthTagCorrectionAnonymous: Correction is anonymised
|
AuthTagCorrectionAnonymous: Correction is anonymised
|
||||||
AuthTagSelf: User is only accessing their only data
|
AuthTagSelf: User is only accessing their only data
|
||||||
AuthTagIsLDAP: User logs in using their Fraport AG account
|
AuthTagIsExternal: User logs in using externally managed credentials
|
||||||
AuthTagIsPWHash: User logs in using their FRADrive specific account
|
AuthTagIsInternal: User logs in using FRADrive-internal credentials
|
||||||
AuthTagAuthentication: User is authenticated
|
AuthTagAuthentication: User is authenticated
|
||||||
AuthTagRead: Access is read only
|
AuthTagRead: Access is read only
|
||||||
AuthTagWrite: Access might write
|
AuthTagWrite: Access might write
|
||||||
|
|||||||
@ -25,14 +25,10 @@ PersonalInfoTutorialsWip: Die Anzeige von Kurse, zu denen Sie angemeldet sind wi
|
|||||||
ProfileGroupSubmissionDates: Bei Gruppenabgaben wird kein Datum angezeigt, wenn Sie die Gruppenabgabe nie selbst hochgeladen haben.
|
ProfileGroupSubmissionDates: Bei Gruppenabgaben wird kein Datum angezeigt, wenn Sie die Gruppenabgabe nie selbst hochgeladen haben.
|
||||||
ProfileCorrectorRemark: Die oberhalb angezeigte Tabelle zeigt nur prinzipielle Einteilungen als Korrektor zu einem Übungsblatt. Auch ohne Einteilung können Korrekturen einzeln zugewiesen werden, welche hier dann nicht aufgeführt werden.
|
ProfileCorrectorRemark: Die oberhalb angezeigte Tabelle zeigt nur prinzipielle Einteilungen als Korrektor zu einem Übungsblatt. Auch ohne Einteilung können Korrekturen einzeln zugewiesen werden, welche hier dann nicht aufgeführt werden.
|
||||||
ProfileCorrections: Auflistung aller zugewiesenen Korrekturen
|
ProfileCorrections: Auflistung aller zugewiesenen Korrekturen
|
||||||
Remarks: Hinweis:
|
Remarks: Hinweise
|
||||||
|
|
||||||
ProfileNoSupervisor: Keine übergeordneten Ansprechpartner vorhanden
|
ProfileSupervisor: Übergeordnete Ansprechpartner
|
||||||
ProfileSupervisor n@Int m@Int: #{n} #{pluralDE n "übergeordneter" "übergeordnete"} Ansprechpartner#{noneMoreDE m "" (", davon " <> tshow m <> " mit Benachrichtigungsumleitung")}
|
ProfileSupervisee: Ist Ansprechpartner für
|
||||||
ProfileSupervisorRemark n@Int m@Int l@Int: #{m} von #{n} #{pluralDE m "übergeordneter" "übergeordnete"} Ansprechpartner mit Benachrichtigungsumleitung#{noneMoreDE l "" (", davon " <> tshow l <> " mit postalischer Benachrichtigung")}
|
|
||||||
ProfileNoSupervisee: Ist kein Ansprechpartner für irgendjemand
|
|
||||||
ProfileSupervisee n@Int m@Int: Ist Ansprechpartner für #{n} #{pluralDE n "Person" "Personen"}#{noneMoreDE m "" (", davon " <> tshow m <> " mit Benachrichtigungsumleitung")}
|
|
||||||
ProfileSuperviseeRemark n@Int m@Int: Dieser Nutzer ist Ansprechpartner für #{n} #{pluralDE n "Person" "Personen"}#{noneMoreDE m "" (", davon " <> tshow m <> " mit Benachrichtigungsumleitung")}
|
|
||||||
|
|
||||||
UserTelephone: Telefon
|
UserTelephone: Telefon
|
||||||
UserMobile: Mobiltelefon
|
UserMobile: Mobiltelefon
|
||||||
|
|||||||
@ -25,14 +25,10 @@ PersonalInfoTutorialsWip: The feature to display courses you have registered for
|
|||||||
ProfileGroupSubmissionDates: No date is shown for group submissions if you have never uploaded the submission yourself.
|
ProfileGroupSubmissionDates: No date is shown for group submissions if you have never uploaded the submission yourself.
|
||||||
ProfileCorrectorRemark: The table above only shows registration as a corrector in principle. Even without registration corrections can be assigned individually and are not listed.
|
ProfileCorrectorRemark: The table above only shows registration as a corrector in principle. Even without registration corrections can be assigned individually and are not listed.
|
||||||
ProfileCorrections: List of all assigned corrections
|
ProfileCorrections: List of all assigned corrections
|
||||||
Remarks: Remark:
|
Remarks: Remarks
|
||||||
|
|
||||||
ProfileNoSupervisor: Is not supervised by anynone
|
ProfileSupervisor: Supervised by
|
||||||
ProfileSupervisor n m: #{pluralENsN n "supervisor"} #{noneMoreEN m "" ("with " <> tshow m <> " active notification rerouting")}
|
ProfileSupervisee: Supervises
|
||||||
ProfileSupervisorRemark n@Int m@Int l@Int: #{m} of #{n} #{pluralENs m "supervisor"} with active notification rerouting#{noneMoreEN l "" (", and " <> tshow l <> "of these prefer postal notifications")}
|
|
||||||
ProfileNoSupervisee: Does not supervise anynone
|
|
||||||
ProfileSupervisee n m: Supervises #{pluralENsN n "person"} #{noneMoreEN m "" ("with " <> tshow m <> " active notification rerouting")}
|
|
||||||
ProfileSuperviseeRemark n m: This person supervises #{pluralENsN n "person"}#{noneMoreEN m "" (" with " <> tshow m <> " having active notifications rerouting to this user")}
|
|
||||||
|
|
||||||
UserTelephone: Phone
|
UserTelephone: Phone
|
||||||
UserMobile: Mobile
|
UserMobile: Mobile
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
# SPDX-FileCopyrightText: 2022 Gregor Kleen <gregor.kleen@ifi.lmu.de>,Steffen Jost <jost@cip.ifi.lmu.de>,Steffen Jost <jost@tcs.ifi.lmu.de>,Winnie Ros <winnie.ros@campus.lmu.de>
|
# SPDX-FileCopyrightText: 2022-2024 Sarah Vaupel <sarah.vaupel@uniworx.de>, Gregor Kleen <gregor.kleen@ifi.lmu.de>, Steffen Jost <jost@cip.ifi.lmu.de>, Steffen Jost <jost@tcs.ifi.lmu.de>, Winnie Ros <winnie.ros@campus.lmu.de>
|
||||||
#
|
#
|
||||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
@ -22,7 +22,6 @@ AdminUserPostAddress: Postalische Anschrift
|
|||||||
AdminUserPrefersPostal: Briefe anstatt Email bevorzugt
|
AdminUserPrefersPostal: Briefe anstatt Email bevorzugt
|
||||||
AdminUserPinPassword: Passwort zur Verschlüsselung von PDF Anhängen in Emails
|
AdminUserPinPassword: Passwort zur Verschlüsselung von PDF Anhängen in Emails
|
||||||
AdminUserNoPassword: Kein Passwort gesetzt
|
AdminUserNoPassword: Kein Passwort gesetzt
|
||||||
AdminUserPinPassNotIncluded: Hinweis: Das Passwort wird hier zur Bequemlichkeit zusätzlich angezeigt und ist selbstverständlich nicht im originalem Inhalt enthalten.
|
|
||||||
AdminUserAssimilate: Diesen Benutzer assimilieren von
|
AdminUserAssimilate: Diesen Benutzer assimilieren von
|
||||||
UserAdded: Benutzer erfolgreich angelegt
|
UserAdded: Benutzer erfolgreich angelegt
|
||||||
UserCollision: Benutzer konnte wegen Eindeutigkeit nicht angelegt werden
|
UserCollision: Benutzer konnte wegen Eindeutigkeit nicht angelegt werden
|
||||||
@ -38,10 +37,9 @@ AuthPWHashAlreadyConfigured: Nutzer:in meldet sich bereits mit FRADrive spezifis
|
|||||||
AuthPWHashConfigured: Nutzer:in meldet sich nun mit FRADrive spezifischer Kennung an
|
AuthPWHashConfigured: Nutzer:in meldet sich nun mit FRADrive spezifischer Kennung an
|
||||||
UsersCourseSchool: Bereich
|
UsersCourseSchool: Bereich
|
||||||
ActionNoUsersSelected: Keine Benutzer:innen ausgewählt
|
ActionNoUsersSelected: Keine Benutzer:innen ausgewählt
|
||||||
SynchroniseAvsUserQueued n@Int: AVS-Synchronisation von #{n} #{pluralDE n "Benutzer:in" "Benutzer:innen"} zwingend angestoßen, die Ausführung wird mehrere Minuten benötigen!
|
SynchroniseAvsUserQueued n@Int: AVS-Synchronisation von #{n} #{pluralDE n "Benutzer:in" "Benutzer:innen"} angestoßen
|
||||||
SynchroniseAvsAllUsersQueued n@Int64: AVS-Synchronisation von allen #{n} #{pluralDE n "Benutzer:in" "Benutzer:innen"} angestoßen, welche heute noch nicht synchronisiert wurden, die Ausführung wird eine Weile brauchen!
|
SynchroniseUserdbUserQueued n@Int: Benutzerdatenbank-Synchronisation von #{n} #{pluralDE n "Benutzer:in" "Benutzer:innen"} angestoßen
|
||||||
SynchroniseLdapUserQueued n@Int: LDAP-Synchronisation von #{n} #{pluralDE n "Benutzer:in" "Benutzer:innen"} angestoßen, die Ausführung wird mehrere Minuten benötigen!
|
SynchroniseUserdbAllUsersQueued: Benutzerdatenbank-Synchronisation von allen Benutzer:innen angestoßen
|
||||||
SynchroniseLdapAllUsersQueued: LDAP-Synchronisation von allen Benutzer:innen angestoßen, die Ausführung kann eine Weile brauchen!
|
|
||||||
UserListTitle: Komprehensive Benutzerliste
|
UserListTitle: Komprehensive Benutzerliste
|
||||||
AccessRightsSaved: Berechtigungen erfolgreich verändert
|
AccessRightsSaved: Berechtigungen erfolgreich verändert
|
||||||
AccessRightsNotChanged: Berechtigungen wurden nicht verändert
|
AccessRightsNotChanged: Berechtigungen wurden nicht verändert
|
||||||
@ -50,6 +48,7 @@ AuthLDAPInvalidLookup: Bestehender Nutzer/Bestehende Nutzerin konnte nicht einde
|
|||||||
AuthLDAPAlreadyConfigured: Nutzer:in meldet sich bereits per Fraport AG Kennung in FRADrive an
|
AuthLDAPAlreadyConfigured: Nutzer:in meldet sich bereits per Fraport AG Kennung in FRADrive an
|
||||||
AuthLDAPConfigured: Nutzer:in meldet sich nun per Fraport AG Kennung in FRADrive an
|
AuthLDAPConfigured: Nutzer:in meldet sich nun per Fraport AG Kennung in FRADrive an
|
||||||
AuthLDAP !ident-ok: Fraport AG Kennung
|
AuthLDAP !ident-ok: Fraport AG Kennung
|
||||||
|
AuthAzure: Azure-Account
|
||||||
AuthNoLogin: Kein Login erlaubt.
|
AuthNoLogin: Kein Login erlaubt.
|
||||||
PasswordResetQueued: Link zum Passwort-Zurücksetzen versandt
|
PasswordResetQueued: Link zum Passwort-Zurücksetzen versandt
|
||||||
UserAssimilateUser: Benutzer:in
|
UserAssimilateUser: Benutzer:in
|
||||||
@ -91,29 +90,20 @@ NewPasswordLink: Neues Passwort setzen
|
|||||||
UserAccountDeleteWarning: Achtung, dies löscht den kompletten Benutzer/die komplette Benutzerin unwiderruflich und mit allen assoziierten Daten aus der Datenbank. Prüfungsdaten müssen jedoch langfristig gespeichert bleiben!
|
UserAccountDeleteWarning: Achtung, dies löscht den kompletten Benutzer/die komplette Benutzerin unwiderruflich und mit allen assoziierten Daten aus der Datenbank. Prüfungsdaten müssen jedoch langfristig gespeichert bleiben!
|
||||||
UserAvsSync: AVS-Synchronisieren
|
UserAvsSync: AVS-Synchronisieren
|
||||||
UserLdapSync: LDAP-Synchronisieren
|
UserLdapSync: LDAP-Synchronisieren
|
||||||
|
AllUsersLdapSync: Alle LDAP-Synchronisieren
|
||||||
UserHijack: Sitzung übernehmen
|
UserHijack: Sitzung übernehmen
|
||||||
UserAddSupervisor: Ansprechpartner hinzufügen
|
UserAddSupervisor: Ansprechpartner hinzufügen
|
||||||
UserSetSupervisor: Ansprechpartner ersetzen
|
UserSetSupervisor: Ansprechpartner ersetzen
|
||||||
UserRemoveSupervisor: Alle Ansprechpartner entfernen
|
UserRemoveSupervisor: Alle Ansprechpartner entfernen
|
||||||
UserRemoveSubordinates: Alle Ansprechpartnerbeziehungen zu Untergebenen beenden
|
|
||||||
UserIsSupervisor: Ist Ansprechpartner
|
UserIsSupervisor: Ist Ansprechpartner
|
||||||
UserAvsSwitchCompany: Als Primärfirma verwenden
|
|
||||||
UserAvsSwitchCompanyField: Primärfirma auswählen
|
|
||||||
UserAvsCompanySwitched c@CompanyShorthand: Primärfirma gewechselt zu #{tshow c}
|
|
||||||
AllUsersLdapSync: Alle LDAP-Synchronisieren
|
|
||||||
AllUsersAvsSync: Alle AVS-Synchronisieren
|
|
||||||
ThisUserLdapSync: LDAP Synchronisation
|
|
||||||
ThisUserAvsSync: AVS Synchronisation
|
|
||||||
AuthKindLDAP: Fraport AG Kennung
|
|
||||||
AuthKindPWHash: FRADrive Kennung
|
|
||||||
AuthKindNoLogin: Kein Login möglich
|
|
||||||
Name !ident-ok: Name
|
Name !ident-ok: Name
|
||||||
UsersChangeSupervisorsSuccess usr@Int spr@Int: #{tshow spr} Ansprechpartner für #{tshow usr} Benutzer gesetzt.
|
UsersChangeSupervisorsSuccess usr@Int spr@Int: #{tshow spr} Ansprechpartner für #{tshow usr} Benutzer gesetzt.
|
||||||
UsersChangeSupervisorsWarning usr@Int spr@Int bad@Int: Nur _{MsgUsersChangeSupervisorsSuccess usr spr} #{tshow bad} Ansprechpartner #{pluralDE bad "wurde" "wurden"} nicht gefunden!
|
UsersChangeSupervisorsWarning usr@Int spr@Int bad@Int: Nur _{MsgUsersChangeSupervisorsSuccess usr spr} #{tshow bad} Ansprechpartner #{pluralDE bad "wurde" "wurden"} nicht gefunden!
|
||||||
UsersRemoveSupervisors usr@Int: Alle Ansprechpartner für #{tshow usr} Benutzer gelöscht.
|
UsersRemoveSupervisors usr@Int: Alle Ansprechpartner für #{tshow usr} Benutzer gelöscht.
|
||||||
UsersRemoveSubordinates usr@Int: Alle Ansprechpartnerbeziehungen für #{tshow usr} #{pluralDE usr "ehemaligen" "ehemalige"} Ansprechpartner gelöscht.
|
|
||||||
UserCompanyReason: Begründung der Firmenassoziation
|
AdminUserAuthentication: Authentification
|
||||||
UserCompanyReasonTooltip: Optionale Notiz für besondere Fälle. Kann ggf. autmatische Entfernung bei AVS Firmenwechsel verhindern.
|
AdminUserAuthLastSync: Zuletzt synchronisiert
|
||||||
UserSupervisorReason: Begründung Ansprechpartner
|
AuthKindLDAP: Fraport-AG-Kennung (LDAP)
|
||||||
UserSupervisorReasonTooltip: Optionale Notiz für besondere Fälle. Kann ggf. autmatische Entfernung bei AVS Firmenwechsel verhindern.
|
AuthKindAzure: Azure-Login
|
||||||
AdminUserAllNotifications: Alle Benachrichtigungen and diesen Benutzer
|
AuthKindPWHash: Interne FRADrive-Kennung
|
||||||
|
AuthKindNoLogin: Kein Login möglich
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
# SPDX-FileCopyrightText: 2022 Steffen Jost <jost@cip.ifi.lmu.de>,Steffen Jost <jost@tcs.ifi.lmu.de>,Winnie Ros <winnie.ros@campus.lmu.de>
|
# SPDX-FileCopyrightText: 2022-2024 Sarah Vaupel <sarah.vaupel@uniworx.de>, Steffen Jost <jost@cip.ifi.lmu.de>, Steffen Jost <jost@tcs.ifi.lmu.de>, Winnie Ros <winnie.ros@campus.lmu.de>
|
||||||
#
|
#
|
||||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
@ -22,7 +22,6 @@ AdminUserPostAddress: Postal Address
|
|||||||
AdminUserPrefersPostal: Prefers postal letters over email
|
AdminUserPrefersPostal: Prefers postal letters over email
|
||||||
AdminUserPinPassword: Password used for PDF attachments to emails
|
AdminUserPinPassword: Password used for PDF attachments to emails
|
||||||
AdminUserNoPassword: No password set
|
AdminUserNoPassword: No password set
|
||||||
AdminUserPinPassNotIncluded: Note: the password is shown here only for convenience, but is not contained in the original content, of course.
|
|
||||||
AdminUserAssimilate: Assimilate user by another user
|
AdminUserAssimilate: Assimilate user by another user
|
||||||
UserAdded: Successfully added user
|
UserAdded: Successfully added user
|
||||||
UserCollision: Could not create user due to uniqueness constraint
|
UserCollision: Could not create user due to uniqueness constraint
|
||||||
@ -38,10 +37,9 @@ AuthPWHashAlreadyConfigured: User already logs in using their FRADrive specific
|
|||||||
AuthPWHashConfigured: User now logs in using their FRADrive specific account
|
AuthPWHashConfigured: User now logs in using their FRADrive specific account
|
||||||
UsersCourseSchool: Department
|
UsersCourseSchool: Department
|
||||||
ActionNoUsersSelected: No users selected
|
ActionNoUsersSelected: No users selected
|
||||||
SynchroniseAvsUserQueued n: Triggered forced AVS synchronisation of #{n} #{pluralEN n "user" "users"}, which may take several minutes to complete.
|
SynchroniseAvsUserQueued n: Triggered AVS synchronisation of #{n} #{pluralEN n "user" "users"}.
|
||||||
SynchroniseAvsAllUsersQueued n: Triggered AVS synchronisation of all #{n} #{pluralEN n "user" "users"} that were not already synchronised today, which may take quite a while to complete.
|
SynchroniseUserdbUserQueued n: Triggered user database synchronisation of #{n} #{pluralEN n "user" "users"}.
|
||||||
SynchroniseLdapUserQueued n: Triggered LDAP synchronisation of #{n} #{pluralEN n "user" "users"}, which may take several minutes to complete.
|
SynchroniseUserdbAllUsersQueued: Triggered user database synchronisation of all users
|
||||||
SynchroniseLdapAllUsersQueued: Triggered LDAP synchronisation of all users, which may take quite a while to complete.
|
|
||||||
UserListTitle: Comprehensive list of users
|
UserListTitle: Comprehensive list of users
|
||||||
AccessRightsSaved: Successfully updated permissions
|
AccessRightsSaved: Successfully updated permissions
|
||||||
AccessRightsNotChanged: Permissions left unchanged
|
AccessRightsNotChanged: Permissions left unchanged
|
||||||
@ -50,6 +48,7 @@ AuthLDAPInvalidLookup: Existing user could not be uniquely matched with a LDAP e
|
|||||||
AuthLDAPAlreadyConfigured: User already logs in using their Fraport AG account
|
AuthLDAPAlreadyConfigured: User already logs in using their Fraport AG account
|
||||||
AuthLDAPConfigured: User now logs in using their Fraport AG account
|
AuthLDAPConfigured: User now logs in using their Fraport AG account
|
||||||
AuthLDAP: Fraport AG account
|
AuthLDAP: Fraport AG account
|
||||||
|
AuthAzure: Azure account
|
||||||
AuthNoLogin: No login allowed.
|
AuthNoLogin: No login allowed.
|
||||||
PasswordResetQueued: Sent link to reset password
|
PasswordResetQueued: Sent link to reset password
|
||||||
UserAssimilateUser: User
|
UserAssimilateUser: User
|
||||||
@ -91,29 +90,20 @@ NewPasswordLink: Set password
|
|||||||
UserAccountDeleteWarning: Caution, this permanently deletes users and all of their associated data. Exam results must be stored long term!
|
UserAccountDeleteWarning: Caution, this permanently deletes users and all of their associated data. Exam results must be stored long term!
|
||||||
UserAvsSync: Synchronise with AVS
|
UserAvsSync: Synchronise with AVS
|
||||||
UserLdapSync: Synchronise with LDAP
|
UserLdapSync: Synchronise with LDAP
|
||||||
|
AllUsersLdapSync: Synchronise all with LDAP
|
||||||
UserHijack: Hijack session
|
UserHijack: Hijack session
|
||||||
UserAddSupervisor: Add supervisor
|
UserAddSupervisor: Add supervisor
|
||||||
UserSetSupervisor: Replace supervisors
|
UserSetSupervisor: Replace supervisors
|
||||||
UserRemoveSupervisor: Set to unsupervised
|
UserRemoveSupervisor: Set to unsupervised
|
||||||
UserRemoveSubordinates: Remove all subordinates
|
|
||||||
UserIsSupervisor: Is supervisor
|
UserIsSupervisor: Is supervisor
|
||||||
UserAvsSwitchCompany: Use as primary company
|
|
||||||
UserAvsSwitchCompanyField: Select primary company
|
|
||||||
UserAvsCompanySwitched c: Primary company switched to #{tshow c}
|
|
||||||
AllUsersLdapSync: Synchronise all with LDAP
|
|
||||||
AllUsersAvsSync: Synchronise all with AVS
|
|
||||||
ThisUserLdapSync: Synchronise user with LDAP
|
|
||||||
ThisUserAvsSync: Synchronise user with AVS
|
|
||||||
AuthKindLDAP: Fraport AG account
|
|
||||||
AuthKindPWHash: FRADrive account
|
|
||||||
AuthKindNoLogin: No login
|
|
||||||
Name: Name
|
Name: Name
|
||||||
UsersChangeSupervisorsSuccess usr spr: #{pluralENsN spr "supervisor"} for #{pluralENsN usr "user"} set.
|
UsersChangeSupervisorsSuccess usr spr: #{pluralENsN spr "supervisor"} for #{pluralENsN usr "user"} set.
|
||||||
UsersChangeSupervisorsWarning usr spr bad: Only _{MsgUsersChangeSupervisorsSuccess usr spr} #{pluralENsN bad "supervisors"} could not be identified!
|
UsersChangeSupervisorsWarning usr spr bad: Only _{MsgUsersChangeSupervisorsSuccess usr spr} #{pluralENsN bad "supervisors"} could not be identified!
|
||||||
UsersRemoveSupervisors usr: Removed all supervisors for #{pluralENsN usr "user"}.
|
UsersRemoveSupervisors usr: Removed all supervisors for #{pluralENsN usr "user"}.
|
||||||
UsersRemoveSubordinates usr: Removed all subordinates for #{pluralENsN usr "previous supervisor"}.
|
|
||||||
UserCompanyReason: Reason for company association
|
AdminUserAuthentication: Authentifizierung
|
||||||
UserCompanyReasonTooltip: Optional note for special cases. In some case this may prevent automatic removel upon AVS user company changes.
|
AdminUserAuthLastSync: Last synchronised
|
||||||
UserSupervisorReason: Reason for supervision
|
AuthKindLDAP: Fraport AG account (LDAP)
|
||||||
UserSupervisorReasonTooltip: Optional note for special cases. In some case this may prevent automatic removel upon AVS user company changes.
|
AuthKindAzure: Azure account
|
||||||
AdminUserAllNotifications: All notification sent to this user
|
AuthKindPWHash: Internal FRADrive login
|
||||||
|
AuthKindNoLogin: No login
|
||||||
@ -4,31 +4,24 @@
|
|||||||
|
|
||||||
#messages or constructors that are used all over the code
|
#messages or constructors that are used all over the code
|
||||||
|
|
||||||
Logo !ident-ok: FRADrive
|
Logo !ident-ok: Uni2work
|
||||||
EmailInvitationWarning: Diese Adresse konnte keinem FRADrive-Benutzer/-Benutzerin zugeordnet werden. Es wird eine Einladung per E-Mail versandt.
|
EmailInvitationWarning: Diese Adresse konnte keinem Uni2work-Benutzer/keiner Uni2work-Benutzerin zugeordnet werden. Es wird eine Einladung per E-Mail versandt.
|
||||||
BoolIrrelevant !ident-ok: —
|
BoolIrrelevant !ident-ok: —
|
||||||
FieldPrimary: Hauptfach
|
FieldPrimary: Hauptfach
|
||||||
FieldSecondary: Nebenfach
|
FieldSecondary: Nebenfach
|
||||||
MultiEmailFieldTip: Es sind mehrere, Komma-separierte, E-Mail-Adressen möglich
|
MultiEmailFieldTip: Es sind mehrere, Komma-separierte, E-Mail-Adressen möglich
|
||||||
MultiSelectTip: Mehrfachauswahl und Abwählen mit Strg-Klick
|
MultiSelectTip: Mehrfachauswahl und Abwählen mit Strg-Klick
|
||||||
WeekDay: Wochentag
|
WeekDay: Wochentag
|
||||||
Hours: Stunden
|
|
||||||
LdapIdentificationOrEmail: Fraport AG-Kennung / E-Mail-Adresse
|
LdapIdentificationOrEmail: Fraport AG-Kennung / E-Mail-Adresse
|
||||||
Months num@Int64: #{num} #{pluralDE num "Monat" "Monate"}
|
Months num@Int64: #{num} #{pluralDE num "Monat" "Monate"}
|
||||||
Days num@Int64: #{num} #{pluralDE num "Tag" "Tage"}
|
Days num@Int64: #{num} #{pluralDE num "Tag" "Tage"}
|
||||||
NoAutomaticUpdateTip: Dieser Wert wurde manuell editiert und wird daher nicht mehr automatisch durch as AVS aktualisiert.
|
|
||||||
AddressIsLinkedTip: Verlinkte Postaddresse: Für diesen Benutzer ist keine individuelle Postadresse gespeichert, die Adresse wurde stattdessen aus der Firmenzugehörigkeit abgeleitet.
|
|
||||||
|
|
||||||
ClusterVolatileQuickActionsEnabled: Schnellzugriffsmenü aktiv
|
ClusterVolatileQuickActionsEnabled: Schnellzugriffsmenü aktiv
|
||||||
|
|
||||||
AvsNoLicence: Keine Fahrberechtigung
|
AvsNoLicence: Keine Fahrberechtigung
|
||||||
AvsLicenceVorfeld: Vorfeld Fahrberechtigung
|
AvsLicenceVorfeld: Vorfeld Fahrberechtigung
|
||||||
AvsLicenceRollfeld: Rollfeld Fahrberechtigung
|
AvsLicenceRollfeld: Rollfeld Fahrberechtigung
|
||||||
AvsNoLicenceGuest: Keine Fahrberechtigung (Gast, Fahrberechtigungserwerb nicht möglich)
|
|
||||||
|
|
||||||
PaginationSize: Einträge pro Seite
|
PaginationSize: Einträge pro Seite
|
||||||
PaginationPage: Angzeigte Seite
|
PaginationPage: Angzeigte Seite
|
||||||
PaginationError: Paginierung Parameter dürfen nicht negativ sein
|
PaginationError: Paginierung Parameter dürfen nicht negativ sein
|
||||||
|
|
||||||
NullDeletes: Zum Löschen NULL eingeben.
|
|
||||||
SortPriority: Sortierungspriorität
|
|
||||||
@ -4,31 +4,24 @@
|
|||||||
|
|
||||||
#messages or constructors that are used all over the Code
|
#messages or constructors that are used all over the Code
|
||||||
|
|
||||||
Logo: FRADrive
|
Logo: Uni2work
|
||||||
EmailInvitationWarning: This address could not be matched to any FRADrive user. An invitation will be sent via email.
|
EmailInvitationWarning: This address could not be matched to any Uni2work user. An invitation will be sent via email.
|
||||||
BoolIrrelevant: —
|
BoolIrrelevant: —
|
||||||
FieldPrimary: Major
|
FieldPrimary: Major
|
||||||
FieldSecondary: Minor
|
FieldSecondary: Minor
|
||||||
MultiEmailFieldTip: Multiple emails addresses may be specified (comma-separated)
|
MultiEmailFieldTip: Multiple emails addresses may be specified (comma-separated)
|
||||||
MultiSelectTip: Multiple selection and desection via Ctrl-Click
|
MultiSelectTip: Multiple selection and desection via Ctrl-Click
|
||||||
WeekDay: Day of the week
|
WeekDay: Day of the week
|
||||||
Hours: Hours
|
|
||||||
LdapIdentificationOrEmail: Fraport AG-Kennung / email address
|
LdapIdentificationOrEmail: Fraport AG-Kennung / email address
|
||||||
Months num: #{num} #{pluralEN num "Month" "Months"}
|
Months num: #{num} #{pluralEN num "Month" "Months"}
|
||||||
Days num: #{num} #{pluralEN num "Day" "Days"}
|
Days num: #{num} #{pluralEN num "Day" "Days"}
|
||||||
NoAutomaticUpdateTip: This particular value receives no automatic AVS updates, since it has been edited manually.
|
|
||||||
AddressIsLinkedTip: Linked postal address: No individual postal address is stored for this user, instead a postal address was inferred from the user's company association.
|
|
||||||
|
|
||||||
ClusterVolatileQuickActionsEnabled: Quick actions enabled
|
ClusterVolatileQuickActionsEnabled: Quick actions enabled
|
||||||
|
|
||||||
AvsNoLicence: No driving licence
|
AvsNoLicence: No driving licence
|
||||||
AvsLicenceVorfeld: Apron driving licence
|
AvsLicenceVorfeld: Apron driving licence
|
||||||
AvsLicenceRollfeld: Maneuvering area driving licence
|
AvsLicenceRollfeld: Maneuvering area driving licence
|
||||||
AvsNoLicenceGuest: No driving licence (Guest account, cannot acquire a diriving licence)
|
|
||||||
|
|
||||||
PaginationSize: Rows per Page
|
PaginationSize: Rows per Page
|
||||||
PaginationPage: Page to show
|
PaginationPage: Page to show
|
||||||
PaginationError: Pagination parameter must not be negative
|
PaginationError: Pagination parameter must not be negative
|
||||||
|
|
||||||
NullDeletes: Enter NULL to delete.
|
|
||||||
SortPriority: Sort order priority
|
|
||||||
@ -1,4 +1,4 @@
|
|||||||
# SPDX-FileCopyrightText: 2022 Gregor Kleen <gregor.kleen@ifi.lmu.de>,Sarah Vaupel <sarah.vaupel@ifi.lmu.de>,Steffen Jost <jost@cip.ifi.lmu.de>,Steffen Jost <jost@tcs.ifi.lmu.de>,Winnie Ros <winnie.ros@campus.lmu.de>
|
# SPDX-FileCopyrightText: 2022-2024 Sarah Vaupel <sarah.vaupel@uniworx.de>, David Mosbach <david.mosbach@uniworx.de>, Gregor Kleen <gregor.kleen@ifi.lmu.de>,Sarah Vaupel <sarah.vaupel@ifi.lmu.de>,Steffen Jost <jost@cip.ifi.lmu.de>,Steffen Jost <jost@tcs.ifi.lmu.de>,Winnie Ros <winnie.ros@campus.lmu.de>
|
||||||
#
|
#
|
||||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
@ -25,6 +25,7 @@ MenuInstance: Instanz-Identifikation
|
|||||||
MenuHealth: Instanz-Zustand
|
MenuHealth: Instanz-Zustand
|
||||||
MenuHealthInterface: Schnittstellen Zustand
|
MenuHealthInterface: Schnittstellen Zustand
|
||||||
MenuHelp: Hilfe
|
MenuHelp: Hilfe
|
||||||
|
MenuAccount: Konto
|
||||||
MenuProfile: Anpassen
|
MenuProfile: Anpassen
|
||||||
MenuLogin !ident-ok: Login
|
MenuLogin !ident-ok: Login
|
||||||
MenuLogout !ident-ok: Logout
|
MenuLogout !ident-ok: Logout
|
||||||
@ -142,19 +143,13 @@ MenuSap: SAP Schnittstelle
|
|||||||
|
|
||||||
MenuAvs: AVS Schnittstelle
|
MenuAvs: AVS Schnittstelle
|
||||||
MenuAvsSynchError: AVS Problemübersicht
|
MenuAvsSynchError: AVS Problemübersicht
|
||||||
MenuLdap: LDAP Schnittstelle
|
MenuExternalUser: Externe Benutzer
|
||||||
MenuApc: Druck
|
MenuApc: Druckerei
|
||||||
MenuPrintSend: Manueller Briefversand
|
MenuPrintSend: Manueller Briefversand
|
||||||
MenuPrintDownload: Brief herunterladen
|
MenuPrintDownload: Brief herunterladen
|
||||||
MenuPrintLog: LPR Schnittstelle
|
MenuPrintLog: LPR Schnittstelle
|
||||||
MenuPrintAck: Druckbestätigung
|
MenuPrintAck: Druckbestätigung
|
||||||
|
|
||||||
MenuCommCenter: Benachrichtigungen
|
|
||||||
MenuMailCenter: E‑Mails
|
|
||||||
MenuMailHtml !ident-ok: Html
|
|
||||||
MenuMailPlain !ident-ok: Text
|
|
||||||
MenuMailAttachment: Anhang
|
|
||||||
|
|
||||||
MenuApiDocs: API-Dokumentation (Englisch)
|
MenuApiDocs: API-Dokumentation (Englisch)
|
||||||
MenuSwagger !ident-ok: OpenAPI 2.0 (Swagger)
|
MenuSwagger !ident-ok: OpenAPI 2.0 (Swagger)
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
# SPDX-FileCopyrightText: 2022 Gregor Kleen <gregor.kleen@ifi.lmu.de>,Sarah Vaupel <sarah.vaupel@ifi.lmu.de>,Steffen Jost <jost@tcs.ifi.lmu.de>,Winnie Ros <winnie.ros@campus.lmu.de>
|
# SPDX-FileCopyrightText: 2022-2024 Sarah Vaupel <sarah.vaupel@uniworx.de>, David Mosbach <david.mosbach@uniworx.de>, Gregor Kleen <gregor.kleen@ifi.lmu.de>,Sarah Vaupel <sarah.vaupel@ifi.lmu.de>,Steffen Jost <jost@tcs.ifi.lmu.de>,Winnie Ros <winnie.ros@campus.lmu.de>
|
||||||
#
|
#
|
||||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
@ -25,6 +25,7 @@ MenuInstance: Instance identification
|
|||||||
MenuHealth: Instance health
|
MenuHealth: Instance health
|
||||||
MenuHealthInterface: Interface health
|
MenuHealthInterface: Interface health
|
||||||
MenuHelp: Support
|
MenuHelp: Support
|
||||||
|
MenuAccount: Account
|
||||||
MenuProfile: Settings
|
MenuProfile: Settings
|
||||||
MenuLogin: Login
|
MenuLogin: Login
|
||||||
MenuLogout: Logout
|
MenuLogout: Logout
|
||||||
@ -142,19 +143,13 @@ MenuSap: SAP Interface
|
|||||||
|
|
||||||
MenuAvs: AVS Interface
|
MenuAvs: AVS Interface
|
||||||
MenuAvsSynchError: AVS Problem Overview
|
MenuAvsSynchError: AVS Problem Overview
|
||||||
MenuLdap: LDAP Interface
|
MenuExternalUser: External users
|
||||||
MenuApc: Print
|
MenuApc: Printing
|
||||||
MenuPrintSend: Send Letter
|
MenuPrintSend: Send Letter
|
||||||
MenuPrintDownload: Download Letter
|
MenuPrintDownload: Download Letter
|
||||||
MenuPrintLog: LPR Interface
|
MenuPrintLog: LPR Interface
|
||||||
MenuPrintAck: Acknowledge Printing
|
MenuPrintAck: Acknowledge Printing
|
||||||
|
|
||||||
MenuCommCenter: Notifications
|
|
||||||
MenuMailCenter: Email
|
|
||||||
MenuMailHtml: Html
|
|
||||||
MenuMailPlain: Text
|
|
||||||
MenuMailAttachment: Attachment
|
|
||||||
|
|
||||||
MenuApiDocs: API documentation
|
MenuApiDocs: API documentation
|
||||||
MenuSwagger: OpenAPI 2.0 (Swagger)
|
MenuSwagger: OpenAPI 2.0 (Swagger)
|
||||||
|
|
||||||
|
|||||||
@ -73,19 +73,15 @@ TableDiffDaysTooltip: Zeitspanne nach ISO 8601. Beispiel: "P2Y3M4D" ist eine Zei
|
|||||||
TableExamOfficeLabel: Label-Name
|
TableExamOfficeLabel: Label-Name
|
||||||
TableExamOfficeLabelStatus: Label-Farbe
|
TableExamOfficeLabelStatus: Label-Farbe
|
||||||
TableExamOfficeLabelPriority: Label-Priorität
|
TableExamOfficeLabelPriority: Label-Priorität
|
||||||
TableQualification: Qualifikation
|
|
||||||
TableQualifications: Qualifikationen
|
TableQualifications: Qualifikationen
|
||||||
TableCompany: Firma
|
TableCompany: Firma
|
||||||
TableCompanyFilter: Firma oder Nummer
|
TableCompanyFilter: Firma oder Nummer
|
||||||
TableCompanyShort: Firmenkürzel
|
TableCompanyShort: Firmenkürzel
|
||||||
TableCompanies: Firmen
|
TableCompanies: Firmen
|
||||||
TablePrimeCompany: Primäre Firma
|
|
||||||
TableCompanyNo: Firmennummer
|
TableCompanyNo: Firmennummer
|
||||||
TableCompanyNos: Firmennummern
|
TableCompanyNos: Firmennummern
|
||||||
TableCompanyUser: Firmenangehöriger
|
TableCompanyUser: Firmenangehöriger
|
||||||
TableCompanyNrUsers: Firmenangehörige
|
TableCompanyNrUsers: Firmenangehörige
|
||||||
TableCompanyNrSecondaryUsers: Sekundäre Firmenangehörige
|
|
||||||
TableCompanyReason: Notiz
|
|
||||||
TableCompanyNrSupers: Ansprechpartner
|
TableCompanyNrSupers: Ansprechpartner
|
||||||
TableCompanyNrEmpSupervised: Firmenangehörige mit Ansprechpartner
|
TableCompanyNrEmpSupervised: Firmenangehörige mit Ansprechpartner
|
||||||
TableCompanyNrEmpRerouted: Firmenangehörige mit Umleitung
|
TableCompanyNrEmpRerouted: Firmenangehörige mit Umleitung
|
||||||
@ -95,11 +91,8 @@ TableCompanyNrSupersDefault: Standard Ansprechpartner
|
|||||||
TableCompanyNrForeignSupers: Firmenfremde Ansprechpartner
|
TableCompanyNrForeignSupers: Firmenfremde Ansprechpartner
|
||||||
TableCompanyNrRerouteDefault: Standard Umleitungen
|
TableCompanyNrRerouteDefault: Standard Umleitungen
|
||||||
TableCompanyNrRerouteActive: Aktive Umleitungen
|
TableCompanyNrRerouteActive: Aktive Umleitungen
|
||||||
TableRerouteActive: Umleitung
|
|
||||||
TableCompanyPostalPreference: Benachrichtigungspräferenz neue Firmenangehörige
|
TableCompanyPostalPreference: Benachrichtigungspräferenz neue Firmenangehörige
|
||||||
TableSupervisor: Ansprechpartner
|
TableSupervisor: Ansprechpartner
|
||||||
TableSupervisee: Ansprechpartner für
|
|
||||||
TableReason: Begründung
|
|
||||||
TableCreationTime: Erstellungszeit
|
TableCreationTime: Erstellungszeit
|
||||||
TableJob !ident-ok: Job
|
TableJob !ident-ok: Job
|
||||||
TableJobContent !ident-ok: Parameter
|
TableJobContent !ident-ok: Parameter
|
||||||
@ -107,11 +100,9 @@ TableJobLockTime: Bearbeitung seit
|
|||||||
TableJobLockInstance: Bearbeiter
|
TableJobLockInstance: Bearbeiter
|
||||||
TableJobCreationInstance: Ersteller
|
TableJobCreationInstance: Ersteller
|
||||||
ActJobDelete: Job entfernen
|
ActJobDelete: Job entfernen
|
||||||
ActJobDeleteForce n@Int: Auch vor #{pluralDEnN n "Minute"} gesperrte Jobs entfernen
|
|
||||||
TableJobActDeleteFeedback n@Int m@Int: #{n}/#{m} Jobs entfernt
|
TableJobActDeleteFeedback n@Int m@Int: #{n}/#{m} Jobs entfernt
|
||||||
TableFilterComma: Es können mehrere alternative Suchkriterien mit Komma getrennt angegeben werden, wovon mindestens eines erfüllt werden muss.
|
TableFilterComma: Es können mehrere alternative Suchkriterien mit Komma getrennt angegeben werden, wovon mindestens eines erfüllt werden muss.
|
||||||
TableFilterCommaPlus: Mehrere alternative Suchkriterien mit Komma trennen. Mindestens ein Suchkriterium muss erfüllt werden, zusätzlich zu allen Suchkriterien mit vorangestelltem Plus-Symbol.
|
TableFilterCommaPlus: Mehrere alternative Suchkriterien mit Komma trennen. Mindestens ein Suchkriterium muss erfüllt werden, zusätzlich zu allen Suchkriterien mit vorangestelltem Plus-Symbol.
|
||||||
TableFilterCommaPlusShort: Unterstützt mehrere Kriterien mit Komma-Plus, siehe oben.
|
|
||||||
TableFilterCommaName: Mehrere Namen mit Komma trennen.
|
TableFilterCommaName: Mehrere Namen mit Komma trennen.
|
||||||
TableFilterCommaNameNr: Mehrere Namen oder Nummern mit Komma trennen. Nummern werden nur exakt gesucht.
|
TableFilterCommaNameNr: Mehrere Namen oder Nummern mit Komma trennen. Nummern werden nur exakt gesucht.
|
||||||
TableUserEdit: Benutzer bearbeiten
|
TableUserEdit: Benutzer bearbeiten
|
||||||
|
|||||||
@ -73,19 +73,15 @@ TableDiffDaysTooltip: Duration given according to ISO 8601. Example: "P2Y3M4D" i
|
|||||||
TableExamOfficeLabel: Label name
|
TableExamOfficeLabel: Label name
|
||||||
TableExamOfficeLabelStatus: Label colour
|
TableExamOfficeLabelStatus: Label colour
|
||||||
TableExamOfficeLabelPriority: Label priority
|
TableExamOfficeLabelPriority: Label priority
|
||||||
TableQualification: Qualification
|
|
||||||
TableQualifications: Qualifications
|
TableQualifications: Qualifications
|
||||||
TableCompany: Company
|
TableCompany: Company
|
||||||
TableCompanyFilter: Company/Nr
|
TableCompanyFilter: Company/Nr
|
||||||
TableCompanyShort: Company shorthand
|
TableCompanyShort: Company shorthand
|
||||||
TableCompanies: Companies
|
TableCompanies: Companies
|
||||||
TablePrimeCompany: Primary company
|
|
||||||
TableCompanyNo: Company number
|
TableCompanyNo: Company number
|
||||||
TableCompanyNos: Company numbers
|
TableCompanyNos: Company numbers
|
||||||
TableCompanyUser: Associate
|
TableCompanyUser: Associate
|
||||||
TableCompanyNrUsers: Associates
|
TableCompanyNrUsers: Associates
|
||||||
TableCompanyNrSecondaryUsers: Secondary Associates
|
|
||||||
TableCompanyReason: Note
|
|
||||||
TableCompanyNrSupers: Supervisors
|
TableCompanyNrSupers: Supervisors
|
||||||
TableCompanyNrEmpSupervised: Supervised employees
|
TableCompanyNrEmpSupervised: Supervised employees
|
||||||
TableCompanyNrEmpRerouted: Employees having reroute
|
TableCompanyNrEmpRerouted: Employees having reroute
|
||||||
@ -95,11 +91,8 @@ TableCompanyNrSupersDefault: Default supervisors
|
|||||||
TableCompanyNrForeignSupers: External Supervisors
|
TableCompanyNrForeignSupers: External Supervisors
|
||||||
TableCompanyNrRerouteDefault: Default reroutes
|
TableCompanyNrRerouteDefault: Default reroutes
|
||||||
TableCompanyNrRerouteActive: Active reroutes
|
TableCompanyNrRerouteActive: Active reroutes
|
||||||
TableRerouteActive: Reroute
|
|
||||||
TableCompanyPostalPreference: Default notification preference
|
TableCompanyPostalPreference: Default notification preference
|
||||||
TableSupervisor: Supervisor
|
TableSupervisor: Supervisor
|
||||||
TableSupervisee: Supervisor for
|
|
||||||
TableReason: Reason
|
|
||||||
TableCreationTime: Creation
|
TableCreationTime: Creation
|
||||||
TableJob !ident-ok: Job
|
TableJob !ident-ok: Job
|
||||||
TableJobContent !ident-ok: Parameters
|
TableJobContent !ident-ok: Parameters
|
||||||
@ -107,11 +100,9 @@ TableJobLockTime: Lock time
|
|||||||
TableJobLockInstance: Worker
|
TableJobLockInstance: Worker
|
||||||
TableJobCreationInstance: Creator
|
TableJobCreationInstance: Creator
|
||||||
ActJobDelete: Delete job
|
ActJobDelete: Delete job
|
||||||
ActJobDeleteForce n: Also delete jobs locked #{pluralENsN n "minute"} ago
|
|
||||||
TableJobActDeleteFeedback n@Int m@Int: #{n}/#{m} queued jobs deleted
|
TableJobActDeleteFeedback n@Int m@Int: #{n}/#{m} queued jobs deleted
|
||||||
TableFilterComma: Separate multiple alternative filter criteria by comma, at least one of which must be fulfilled.
|
TableFilterComma: Separate multiple alternative filter criteria by comma, at least one of which must be fulfilled.
|
||||||
TableFilterCommaPlus: Separate multiple alternative filter criteria by comma, at least one of which must be fulfilled in addition to all criteria preceded by a plus symbol.
|
TableFilterCommaPlus: Separate multiple alternative filter criteria by comma, at least one of which must be fulfilled in addition to all criteria preceded by a plus symbol.
|
||||||
TableFilterCommaPlusShort: Support multiple criteria with comma/plus, see above.
|
|
||||||
TableFilterCommaName: Separate names by comma.
|
TableFilterCommaName: Separate names by comma.
|
||||||
TableFilterCommaNameNr: Separate names and numbers by comma. Numbers have to match exact.
|
TableFilterCommaNameNr: Separate names and numbers by comma. Numbers have to match exact.
|
||||||
TableUserEdit: Edit user
|
TableUserEdit: Edit user
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
# SPDX-FileCopyrightText: 2023 Steffen Jost <jost@tcs.ifi.lmu.de>,Gregor Kleen <gregor.kleen@ifi.lmu.de>,Sarah Vaupel <sarah.vaupel@ifi.lmu.de>,Winnie Ros <winnie.ros@campus.lmu.de>
|
# SPDX-FileCopyrightText: 2023-2024 Sarah Vaupel <sarah.vaupel@uniworx.de>, Steffen Jost <jost@tcs.ifi.lmu.de>, Gregor Kleen <gregor.kleen@ifi.lmu.de>, Sarah Vaupel <sarah.vaupel@ifi.lmu.de>, Winnie Ros <winnie.ros@campus.lmu.de>
|
||||||
#
|
#
|
||||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
@ -25,7 +25,6 @@ RGTutorialParticipants tutn@TutorialName: Kursteilnehmer:innen (#{tutn})
|
|||||||
RGExamRegistered examn@ExamName: Angemeldet zur Prüfung „#{examn}“
|
RGExamRegistered examn@ExamName: Angemeldet zur Prüfung „#{examn}“
|
||||||
RGSheetSubmittor shn@SheetName: Abgebende für das Übungsblatt „#{shn}“
|
RGSheetSubmittor shn@SheetName: Abgebende für das Übungsblatt „#{shn}“
|
||||||
CommSubject: Betreff
|
CommSubject: Betreff
|
||||||
CommContent: Inhalt
|
|
||||||
CommAttachments: Anhänge
|
CommAttachments: Anhänge
|
||||||
CommAttachmentsTip: Im Allgemeinen ist es vorzuziehen Dateien, die Sie mit den Empfängern teilen möchten, als Material hochzuladen (und ggf. in der Nachricht zu verlinken). So ist die Datei für die Empfänger dauerhaft abrufbar und auch Personen, die sich z.B. erst später zur Kursart anmelden, haben Zugriff auf die Datei.
|
CommAttachmentsTip: Im Allgemeinen ist es vorzuziehen Dateien, die Sie mit den Empfängern teilen möchten, als Material hochzuladen (und ggf. in der Nachricht zu verlinken). So ist die Datei für die Empfänger dauerhaft abrufbar und auch Personen, die sich z.B. erst später zur Kursart anmelden, haben Zugriff auf die Datei.
|
||||||
CommSuccess n@Int: Nachricht wurde an #{n} Empfänger versandt
|
CommSuccess n@Int: Nachricht wurde an #{n} Empfänger versandt
|
||||||
@ -83,7 +82,6 @@ MultiUserFieldInvitationExplanationAlways: Es wird an alle Adressen, die Sie hie
|
|||||||
AmbiguousEmail: E-Mail-Adresse nicht eindeutig
|
AmbiguousEmail: E-Mail-Adresse nicht eindeutig
|
||||||
InvalidEmailAddress: E-Mail-Adresse ist ungültig
|
InvalidEmailAddress: E-Mail-Adresse ist ungültig
|
||||||
InvalidEmailAddressWith e@Text: E-Mail-Adresse #{show e} ist ungültig
|
InvalidEmailAddressWith e@Text: E-Mail-Adresse #{show e} ist ungültig
|
||||||
MailFileAttachment: Dateianhang
|
|
||||||
UtilExamResultGrade: Note
|
UtilExamResultGrade: Note
|
||||||
UtilExamResultPass: Bestanden/Nicht Bestanden
|
UtilExamResultPass: Bestanden/Nicht Bestanden
|
||||||
UtilExamResultNoShow: Nicht erschienen
|
UtilExamResultNoShow: Nicht erschienen
|
||||||
@ -98,7 +96,6 @@ RoomReferenceLinkLink !ident-ok: Link
|
|||||||
RoomReferenceLinkLinkPlaceholder !ident-ok: URL
|
RoomReferenceLinkLinkPlaceholder !ident-ok: URL
|
||||||
RoomReferenceLinkInstructions: Anweisungen
|
RoomReferenceLinkInstructions: Anweisungen
|
||||||
RoomReferenceLinkInstructionsPlaceholder: Anweisungen
|
RoomReferenceLinkInstructionsPlaceholder: Anweisungen
|
||||||
UtilNoneSet: Keine angegeben
|
|
||||||
UtilEmptyChoice: Auswahl war leer
|
UtilEmptyChoice: Auswahl war leer
|
||||||
UtilEmptyNoChangeTip: Eine leere Eingabe belässt den vorherigen Wert unverändert.
|
UtilEmptyNoChangeTip: Eine leere Eingabe belässt den vorherigen Wert unverändert.
|
||||||
MultiNoSelection: Keine Auswahl
|
MultiNoSelection: Keine Auswahl
|
||||||
@ -161,4 +158,6 @@ SheetGradingPassAlways': Automatisch bestanden, sobald korrigiert
|
|||||||
SheetTypeNormal !ident-ok: Normal
|
SheetTypeNormal !ident-ok: Normal
|
||||||
SheetTypeBonus !ident-ok: Bonus
|
SheetTypeBonus !ident-ok: Bonus
|
||||||
|
|
||||||
InvalidFormAction: Keine Aktion ausgeführt wegen ungültigen Formulardaten
|
InvalidFormAction: Keine Aktion ausgeführt wegen ungültigen Formulardaten
|
||||||
|
|
||||||
|
InvalidUuid: Invalide UUID!
|
||||||
@ -1,4 +1,4 @@
|
|||||||
# SPDX-FileCopyrightText: 2023 Sarah Vaupel <sarah.vaupel@ifi.lmu.de>,Winnie Ros <winnie.ros@campus.lmu.de>,Steffen Jost <jost@tcs.ifi.lmu.de>
|
# SPDX-FileCopyrightText: 2023-2024 Sarah Vaupel <sarah.vaupel@uniworx.de>, Sarah Vaupel <sarah.vaupel@ifi.lmu.de>, Winnie Ros <winnie.ros@campus.lmu.de>, Steffen Jost <jost@tcs.ifi.lmu.de>
|
||||||
#
|
#
|
||||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
@ -25,7 +25,6 @@ RGTutorialParticipants tutn: Course participants (#{tutn})
|
|||||||
RGExamRegistered examn: Registered for exam “#{examn}”
|
RGExamRegistered examn: Registered for exam “#{examn}”
|
||||||
RGSheetSubmittor shn: Submitted for exercise sheet “#{shn}”
|
RGSheetSubmittor shn: Submitted for exercise sheet “#{shn}”
|
||||||
CommSubject: Subject
|
CommSubject: Subject
|
||||||
CommContent: Content
|
|
||||||
CommAttachments: Attachments
|
CommAttachments: Attachments
|
||||||
CommAttachmentsTip: In general it is preferable to upload files as course type material instead of sending them as attachments. You can then link to the material from the message. The file is then permanently accessable to the recipients and to persons that, for example, register for the Course type at a later date.
|
CommAttachmentsTip: In general it is preferable to upload files as course type material instead of sending them as attachments. You can then link to the material from the message. The file is then permanently accessable to the recipients and to persons that, for example, register for the Course type at a later date.
|
||||||
CommSuccess n: Message was sent to #{n} #{pluralEN n "recipient" "recipients"}
|
CommSuccess n: Message was sent to #{n} #{pluralEN n "recipient" "recipients"}
|
||||||
@ -83,7 +82,6 @@ MultiUserFieldInvitationExplanationAlways: An invitation will be sent via email
|
|||||||
AmbiguousEmail: Email address is ambiguous
|
AmbiguousEmail: Email address is ambiguous
|
||||||
InvalidEmailAddress: Email address is invalid
|
InvalidEmailAddress: Email address is invalid
|
||||||
InvalidEmailAddressWith e: Email asdress #{show e} is invalid
|
InvalidEmailAddressWith e: Email asdress #{show e} is invalid
|
||||||
MailFileAttachment: Attached file
|
|
||||||
UtilExamResultGrade: Grade
|
UtilExamResultGrade: Grade
|
||||||
UtilExamResultPass: Passed/Failed
|
UtilExamResultPass: Passed/Failed
|
||||||
UtilExamResultNoShow: Not present
|
UtilExamResultNoShow: Not present
|
||||||
@ -98,7 +96,6 @@ RoomReferenceLinkLink: Link
|
|||||||
RoomReferenceLinkLinkPlaceholder: URL
|
RoomReferenceLinkLinkPlaceholder: URL
|
||||||
RoomReferenceLinkInstructions: Instructions
|
RoomReferenceLinkInstructions: Instructions
|
||||||
RoomReferenceLinkInstructionsPlaceholder: Instructions
|
RoomReferenceLinkInstructionsPlaceholder: Instructions
|
||||||
UtilNoneSet: None set
|
|
||||||
UtilEmptyChoice: Empty selection
|
UtilEmptyChoice: Empty selection
|
||||||
UtilEmptyNoChangeTip: Existing values remain unchanged if this field is left empty.
|
UtilEmptyNoChangeTip: Existing values remain unchanged if this field is left empty.
|
||||||
MultiNoSelection: No selection
|
MultiNoSelection: No selection
|
||||||
@ -161,4 +158,6 @@ SheetGradingPassAlways': Automatically passed when corrected
|
|||||||
SheetTypeNormal: Normal
|
SheetTypeNormal: Normal
|
||||||
SheetTypeBonus: Bonus
|
SheetTypeBonus: Bonus
|
||||||
|
|
||||||
InvalidFormAction: No action taken due to invalid form data
|
InvalidFormAction: No action taken due to invalid form data
|
||||||
|
|
||||||
|
InvalidUuid: Invalid UUID!
|
||||||
@ -1,4 +1,4 @@
|
|||||||
-- SPDX-FileCopyrightText: 2022-24 Gregor Kleen <gregor.kleen@ifi.lmu.de>,Steffen Jost <s.jost@fraport.de>
|
-- SPDX-FileCopyrightText: 2022-23 Gregor Kleen <gregor.kleen@ifi.lmu.de>,Steffen Jost <s.jost@fraport.de>
|
||||||
--
|
--
|
||||||
-- SPDX-License-Identifier: AGPL-3.0-or-later
|
-- SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
@ -8,7 +8,7 @@ TransactionLog
|
|||||||
instance InstanceId
|
instance InstanceId
|
||||||
initiator UserId Maybe -- User associated with performing this action
|
initiator UserId Maybe -- User associated with performing this action
|
||||||
remote IP Maybe -- Remote party that triggered this action via HTTP
|
remote IP Maybe -- Remote party that triggered this action via HTTP
|
||||||
info Value -- JSON-encoded `Transaction`. Value allows full backwards compatibility
|
info Value -- JSON-encoded `Transaction`
|
||||||
deriving Eq Read Show Generic
|
deriving Eq Read Show Generic
|
||||||
|
|
||||||
InterfaceLog
|
InterfaceLog
|
||||||
@ -26,13 +26,6 @@ InterfaceHealth
|
|||||||
interface Text
|
interface Text
|
||||||
subtype Text Maybe
|
subtype Text Maybe
|
||||||
write Bool Maybe
|
write Bool Maybe
|
||||||
hours Int -- negative number: never expires, i.e. if the last entry is a success, this remains indefinitely
|
hours Int
|
||||||
UniqueInterfaceHealth interface subtype write !force -- Note that nullable fields must be either empty or unique
|
UniqueInterfaceHealth interface subtype write !force -- Note that nullable fields must be either empty or unique
|
||||||
deriving Eq Read Show Generic
|
deriving Eq Read Show Generic
|
||||||
|
|
||||||
ProblemLog
|
|
||||||
time UTCTime default=now()
|
|
||||||
info Value -- generic JSON Value allows maximum backwards compatibility
|
|
||||||
solved UTCTime Maybe
|
|
||||||
solver UserId Maybe -- User who marked this problem as done
|
|
||||||
deriving Eq Read Show Generic
|
|
||||||
@ -1,4 +1,4 @@
|
|||||||
-- SPDX-FileCopyrightText: 2022-24 Steffen Jost <jost@tcs.ifi.lmu.de>
|
-- SPDX-FileCopyrightText: 2022 Steffen Jost <jost@tcs.ifi.lmu.de>
|
||||||
--
|
--
|
||||||
-- SPDX-License-Identifier: AGPL-3.0-or-later
|
-- SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
@ -16,19 +16,27 @@
|
|||||||
UserAvs
|
UserAvs
|
||||||
personId AvsPersonId -- unique identifier for user throughout avs; newtype for Int
|
personId AvsPersonId -- unique identifier for user throughout avs; newtype for Int
|
||||||
user UserId
|
user UserId
|
||||||
noPerson Int default=0 -- only needed for manual communication with personnel from Ausweisverwaltungsstelle, redundant since needed for filtering
|
noPerson Int default=0 -- only needed for manual communication with personnel from Ausweisverwaltungsstelle
|
||||||
lastSynch UTCTime default=now()
|
lastSynch UTCTime default=now()
|
||||||
lastSynchError Text Maybe
|
lastSynchError Text Maybe
|
||||||
lastPersonInfo AvsPersonInfo Maybe -- just to discern field changes
|
|
||||||
lastFirmInfo AvsFirmInfo Maybe -- just to discern field changes
|
|
||||||
lastCardNo AvsFullCardNo Maybe -- just to discern changes
|
|
||||||
UniqueUserAvsUser user
|
UniqueUserAvsUser user
|
||||||
UniqueUserAvsId personId
|
UniqueUserAvsId personId
|
||||||
deriving Generic Show
|
deriving Generic Show
|
||||||
|
|
||||||
|
-- Multiple UserAvsCards per UserAvs is possible and not too uncommon.
|
||||||
|
-- Purpose of saving cards is to detect external changes in qualifications and postal addresses
|
||||||
|
-- TODO: This table will be deleted if AVS CR3 SCF-165 is implemented
|
||||||
|
UserAvsCard
|
||||||
|
personId AvsPersonId
|
||||||
|
cardNo AvsFullCardNo
|
||||||
|
card AvsDataPersonCard
|
||||||
|
lastSynch UTCTime
|
||||||
|
-- UniqueAvsCard cardNo -- Note: cardNo is not unique; invalid cardNo may be reissued to different persons
|
||||||
|
deriving Generic
|
||||||
|
|
||||||
AvsSync
|
AvsSync
|
||||||
user UserId -- Note: we need to lookup UserAvs Entity anyway, so no benefit from storing AvsPersonId here
|
user UserId -- Note: we need to lookup UserAvs Entity anyway, so no benefit from storing AvsPersonId here
|
||||||
creationTime UTCTime
|
creationTime UTCTime
|
||||||
pause Day Maybe -- Don't synch if last synch after this day, otherwise synch
|
pause Day Maybe
|
||||||
UniqueAvsSyncUser user
|
UniqueAvsSyncUser user
|
||||||
deriving Generic Show
|
deriving Generic
|
||||||
@ -1,18 +1,25 @@
|
|||||||
-- SPDX-FileCopyrightText: 2022-24 Sarah Vaupel <sarah.vaupel@ifi.lmu.de>,Steffen Jost <jost@tcs.ifi.lmu.de>
|
-- SPDX-FileCopyrightText: 2022 Sarah Vaupel <sarah.vaupel@ifi.lmu.de>,Steffen Jost <jost@tcs.ifi.lmu.de>
|
||||||
--
|
--
|
||||||
-- SPDX-License-Identifier: AGPL-3.0-or-later
|
-- SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
-- Description of companies associated with users
|
-- Description of companies associated with users
|
||||||
|
|
||||||
Company
|
Company
|
||||||
name CompanyName -- == (CI Text) -- NOTE: Fraport department name may carry additional information; use the Shorthand with respect to UserCompanyDepartment
|
name CompanyName -- == (CI Text)
|
||||||
shorthand CompanyShorthand -- == (CI Text) and CompanyKey :: CompanyShorthand -> CompanyId A change to AvsId as primary key is too much work and not strictly necessary due to Uniqueness
|
shorthand CompanyShorthand -- == (CI Text) and CompanyKey :: CompanyShorthand -> CompanyId FUTURE TODO: a shorthand will become available through the AVS interface in the future
|
||||||
avsId Int default=0 -- primary key from avs, use negative numbers for non-AVS companies
|
avsId Int default=0 -- primary key from avs
|
||||||
prefersPostal Bool default=true -- new company users prefers letters by post instead of email
|
prefersPostal Bool default=false -- new company users prefers letters by post instead of email
|
||||||
postAddress StoredMarkup Maybe -- default company postal address, including company name
|
postAddress StoredMarkup Maybe -- default company postal address
|
||||||
email UserEmail Maybe -- Case-insensitive generic company eMail address
|
email UserEmail Maybe -- Case-insensitive generic company eMail address
|
||||||
-- UniqueCompanyName name -- Should be Unique in AVS, but we do not yet need to enforce it
|
UniqueCompanyName name
|
||||||
-- UniqueCompanyShorthand shorthand -- unnecessary, since it is the primary key already
|
UniqueCompanyShorthand shorthand
|
||||||
UniqueCompanyAvsId avsId -- Should be the key, is not for historical reasons and for convenience in URLs and columns
|
-- UniqueCompanyAvsId avsId -- should be the case, unclear if enforcing works here, since we cannot query avs by company id
|
||||||
Primary shorthand -- newtype Key Company = CompanyKey { unCompanyKey :: CompanyShorthand }
|
Primary shorthand -- newtype Key Company = CompanyKey { unCompanyKey :: CompanyShorthand }
|
||||||
deriving Ord Eq Show Generic Binary
|
deriving Ord Eq Show Generic Binary
|
||||||
|
|
||||||
|
-- TODO: a way to populate this table (manually)
|
||||||
|
CompanySynonym
|
||||||
|
synonym CompanyName
|
||||||
|
canonical CompanyShorthand OnDeleteCascade OnUpdateCascade
|
||||||
|
UniqueCompanySynonym synonym
|
||||||
|
deriving Ord Eq Show Generic
|
||||||
|
|||||||
@ -20,11 +20,11 @@ CronLastExec
|
|||||||
time UTCTime -- When was the job executed
|
time UTCTime -- When was the job executed
|
||||||
instance InstanceId -- Which uni2work-instance did the work
|
instance InstanceId -- Which uni2work-instance did the work
|
||||||
UniqueCronLastExec job
|
UniqueCronLastExec job
|
||||||
deriving Generic Show
|
deriving Generic
|
||||||
|
|
||||||
TokenBucket
|
TokenBucket
|
||||||
ident TokenBucketIdent
|
ident TokenBucketIdent
|
||||||
lastValue Int64
|
lastValue Int64
|
||||||
lastAccess UTCTime
|
lastAccess UTCTime
|
||||||
Primary ident
|
Primary ident
|
||||||
deriving Generic Show
|
deriving Generic
|
||||||
@ -13,18 +13,16 @@ Qualification
|
|||||||
refreshWithin CalendarDiffDays Maybe -- notify users about renewal within this number of month/days before expiry; to be used with addGregorianDurationClip
|
refreshWithin CalendarDiffDays Maybe -- notify users about renewal within this number of month/days before expiry; to be used with addGregorianDurationClip
|
||||||
refreshReminder CalendarDiffDays Maybe -- send a second notification about renewal within this number of month/days before expiry
|
refreshReminder CalendarDiffDays Maybe -- send a second notification about renewal within this number of month/days before expiry
|
||||||
elearningStart Bool -- automatically schedule e-refresher
|
elearningStart Bool -- automatically schedule e-refresher
|
||||||
elearningRenews Bool default=true -- successful e-learing automatically increases validity automatically by validDuration
|
-- elearningOnly Bool -- successful E-learing automatically increases validity. NO!
|
||||||
elearningLimit Int Maybe -- limit of e-learning attempts, currently only for informative purposes, as it is enforced by LMS only
|
|
||||||
lmsReuses QualificationId Maybe -- if set, lms is also included within the given qualification's lms, but only for direct routes. AuditDuration is used from this Qualification instead.
|
|
||||||
expiryNotification Bool default=true -- should expiryNotification be generated for this qualification?
|
expiryNotification Bool default=true -- should expiryNotification be generated for this qualification?
|
||||||
avsLicence AvsLicence Maybe -- if set, valid QualificationUsers are synchronized to AVS as a driving licence
|
avsLicence AvsLicence Maybe -- if set, valid QualificationUsers are synchronized to AVS as a driving licence
|
||||||
sapId Text Maybe -- if set, valid QualificationUsers with userCompanyPersonalNumber are transmitted via SAP interface under this id
|
sapId Text Maybe -- if set, valid QualificationUsers with userCompanyPersonalNumber are transmitted via SAP interface under this id
|
||||||
SchoolQualificationShort school shorthand -- must be unique per school and shorthand
|
SchoolQualificationShort school shorthand -- must be unique per school and shorthand
|
||||||
SchoolQualificationName school name -- must be unique per school and name
|
SchoolQualificationName school name -- must be unique per school and name
|
||||||
-- across all schools, only one qualification may be a driving licence -- NO LONGER TRUE
|
-- across all schools, only one qualification may be a driving licence:
|
||||||
-- UniqueQualificationAvsLicence avsLicence !force -- either empty or unique
|
UniqueQualificationAvsLicence avsLicence !force -- either empty or unique
|
||||||
-- NOTE: two NULL values are not equal for the purpose of Uniqueness constraints!
|
-- NOTE: two NULL values are not equal for the purpose of Uniqueness constraints!
|
||||||
deriving Show Eq Generic
|
deriving Eq Generic
|
||||||
|
|
||||||
-- TODOs:
|
-- TODOs:
|
||||||
-- - Enstehen Kosten, wenn Teilnehmer für KnowHow eingereiht werden, aber nicht am Kurs teilnehmen?
|
-- - Enstehen Kosten, wenn Teilnehmer für KnowHow eingereiht werden, aber nicht am Kurs teilnehmen?
|
||||||
@ -43,27 +41,27 @@ Qualification
|
|||||||
-- - Flag "interner Mitarbeiter" wird von Know-How ignoriert / nicht ausgewertet (legacy)
|
-- - Flag "interner Mitarbeiter" wird von Know-How ignoriert / nicht ausgewertet (legacy)
|
||||||
|
|
||||||
-- QualificationPrecondition -- NOTE: this can only be enforced through a background job adding or removing qualifications
|
-- QualificationPrecondition -- NOTE: this can only be enforced through a background job adding or removing qualifications
|
||||||
-- qualification QualificationId OnDeleteCascade OnUpdateCascade -- AND: not unique, ie. qualification can have multiple required preconditions
|
-- qualification QualificationId OnDeleteCascade OnUpdateCascade -- AND: not unique, ie. qualification can have multiple required preconditions
|
||||||
-- required [QualificationId] -- OR : alternatives, any one will suffice -- we don't want array, since we have recursive CTEs
|
-- required [QualificationId] -- OR : alternatives, any one will suffice
|
||||||
-- continuous Bool -- expiring precondition blocks qualification
|
-- continuous Bool -- expiring precondition blocks qualification
|
||||||
-- deriving Generic Show
|
-- deriving Generic
|
||||||
|
|
||||||
-- Maybe an alternative for online qualification validity checking, transitivity through recursive CTEs? (already available in our version)
|
-- Maybe an alternative for online qualification validity checking, transitivity through recursive CTEs? (already available in our version)
|
||||||
QualificationRequirement
|
--QualificationRequirement
|
||||||
qualification QualificationId OnDeleteCascade OnUpdateCascade
|
-- qualification QualificationId OnDeleteCascade OnUpdateCascade
|
||||||
requirement QualificationId OnDeleteCascade OnUpdateCascade
|
-- requirement QualificationId OnDeleteCascade OnUpdateCascade
|
||||||
group Int -- OR: several requirements within the same group are considered equivalent; no order between groups
|
-- group Text -- OR: several requirements within the same group are considered equivalent
|
||||||
note Text -- for humans only, no semantical effect
|
-- UniqueQualificationRequirement qualification requirement
|
||||||
UniqueQualificationRequirement qualification requirement
|
-- deriving Generic
|
||||||
deriving Generic Show
|
--
|
||||||
|
|
||||||
-- TODO: connect Qualification with Exams!
|
-- TODO: connect Qualifications with Exams!?
|
||||||
|
|
||||||
QualificationEdit
|
QualificationEdit
|
||||||
user UserId
|
user UserId
|
||||||
time UTCTime
|
time UTCTime
|
||||||
qualification QualificationId OnDeleteCascade OnUpdateCascade
|
qualification QualificationId OnDeleteCascade OnUpdateCascade
|
||||||
deriving Generic Show
|
deriving Generic
|
||||||
|
|
||||||
QualificationUser
|
QualificationUser
|
||||||
user UserId OnDeleteCascade OnUpdateCascade
|
user UserId OnDeleteCascade OnUpdateCascade
|
||||||
@ -72,11 +70,11 @@ QualificationUser
|
|||||||
lastRefresh Day -- lastRefresh > validUntil possible, if Qualification^elearningOnly == False
|
lastRefresh Day -- lastRefresh > validUntil possible, if Qualification^elearningOnly == False
|
||||||
firstHeld Day -- first time the qualification was earned, should never change
|
firstHeld Day -- first time the qualification was earned, should never change
|
||||||
scheduleRenewal Bool default=true -- if false, no automatic renewal is scheduled and the qualification expires
|
scheduleRenewal Bool default=true -- if false, no automatic renewal is scheduled and the qualification expires
|
||||||
lastNotified UTCTime default=now() -- last notficiation about actual licence validity changes (does not entail e-learning notifications)
|
lastNotified UTCTime default=now() -- last notficiation about being invalid
|
||||||
-- Reasons and temporary revocations are implemented through QualificationUserBlock
|
-- Reasons and temporary revocations are implemented through QualificationUserBlock
|
||||||
-- TODO: adjust SAP interface to transmit end dates
|
-- TODO: adjust SAP interface to transmit end dates
|
||||||
UniqueQualificationUser qualification user
|
UniqueQualificationUser qualification user
|
||||||
deriving Generic Show
|
deriving Generic
|
||||||
|
|
||||||
QualificationUserBlock
|
QualificationUserBlock
|
||||||
qualificationUser QualificationUserId OnDeleteCascade OnUpdateCascade
|
qualificationUser QualificationUserId OnDeleteCascade OnUpdateCascade
|
||||||
@ -84,6 +82,7 @@ QualificationUserBlock
|
|||||||
from UTCTime
|
from UTCTime
|
||||||
reason Text
|
reason Text
|
||||||
blocker UserId Maybe
|
blocker UserId Maybe
|
||||||
|
-- precondition Bool default=false -- if true, this was due to a precondition
|
||||||
deriving Eq Ord Read Show Generic
|
deriving Eq Ord Read Show Generic
|
||||||
|
|
||||||
-- LMS Interface Tables, need regular processing by background jobs, per QualificationId:
|
-- LMS Interface Tables, need regular processing by background jobs, per QualificationId:
|
||||||
@ -133,7 +132,7 @@ LmsUser
|
|||||||
-- Primary ident -- newtype Key LmsUserId = LmsUserKey { unLmsUser :: Text } -- change LmsIdent -> Text. Do we want this? No.
|
-- Primary ident -- newtype Key LmsUserId = LmsUserKey { unLmsUser :: Text } -- change LmsIdent -> Text. Do we want this? No.
|
||||||
UniqueLmsIdent ident -- idents must be unique accross all qualifications, since idents are global within LMS!
|
UniqueLmsIdent ident -- idents must be unique accross all qualifications, since idents are global within LMS!
|
||||||
UniqueLmsQualificationUser qualification user -- each user may be enrolled at most once per course
|
UniqueLmsQualificationUser qualification user -- each user may be enrolled at most once per course
|
||||||
deriving Generic Show
|
deriving Generic
|
||||||
|
|
||||||
-- LmsUserStatus
|
-- LmsUserStatus
|
||||||
-- lmsUser LmsUserId OnDeleteCascade OnUpdateCascade
|
-- lmsUser LmsUserId OnDeleteCascade OnUpdateCascade
|
||||||
@ -151,7 +150,7 @@ LmsReport
|
|||||||
lock Bool -- (0|1)
|
lock Bool -- (0|1)
|
||||||
timestamp UTCTime default=now()
|
timestamp UTCTime default=now()
|
||||||
UniqueLmsReport qualification ident -- required by DBTable
|
UniqueLmsReport qualification ident -- required by DBTable
|
||||||
deriving Generic Show
|
deriving Generic
|
||||||
|
|
||||||
-- LmsAudit removed by commit 71cde92a
|
-- LmsAudit removed by commit 71cde92a
|
||||||
-- due to frequent transmit errors, a separate lms tranmission log is necessary again
|
-- due to frequent transmit errors, a separate lms tranmission log is necessary again
|
||||||
@ -163,4 +162,4 @@ LmsReportLog
|
|||||||
lock Bool -- (0|1)
|
lock Bool -- (0|1)
|
||||||
timestamp UTCTime default=now()
|
timestamp UTCTime default=now()
|
||||||
missing Bool default=false
|
missing Bool default=false
|
||||||
deriving Generic Show
|
deriving Generic
|
||||||
@ -10,23 +10,22 @@ PrintJob
|
|||||||
created UTCTime
|
created UTCTime
|
||||||
acknowledged UTCTime Maybe
|
acknowledged UTCTime Maybe
|
||||||
recipient UserId Maybe OnDeleteSetNull OnUpdateCascade -- optional as some letters may contain just an address
|
recipient UserId Maybe OnDeleteSetNull OnUpdateCascade -- optional as some letters may contain just an address
|
||||||
affected UserId Maybe OnDeleteSetNull OnUpdateCascade -- subject of the letter
|
|
||||||
sender UserId Maybe OnDeleteSetNull OnUpdateCascade -- senders and associations are optional
|
sender UserId Maybe OnDeleteSetNull OnUpdateCascade -- senders and associations are optional
|
||||||
course CourseId Maybe OnDeleteCascade OnUpdateCascade
|
course CourseId Maybe OnDeleteCascade OnUpdateCascade
|
||||||
qualification QualificationId Maybe OnDeleteCascade OnUpdateCascade
|
qualification QualificationId Maybe OnDeleteCascade OnUpdateCascade
|
||||||
lmsUser LmsIdent Maybe OnDeleteSetNull OnUpdateCascade -- allows tracking if recipient has been notified; must be unique
|
lmsUser LmsIdent Maybe OnDeleteSetNull OnUpdateCascade -- allows tracking if recipient has been notified; must be unique
|
||||||
-- UniquePrintJobLmsUser lmsUser -- Note that in fact multiple print jobs per LMS user are possible!
|
-- UniquePrintJobLmsUser lmsUser -- Note that in fact multiple print jobs per LMS user are possible!
|
||||||
-- UniquePrintJobApcIdent apcIdent -- TODO: not yet enforced, since LmsIdent is currently used
|
-- UniquePrintJobApcIdent apcIdent -- TODO: not yet enforced, since LmsIdent is currently used
|
||||||
deriving Generic Show
|
deriving Generic
|
||||||
|
|
||||||
PrintAcknowledge -- just to store acknowledging requests to be evaluated by a background job later on
|
PrintAcknowledge -- just to store acknowledging requests to be evaluated by a background job later on
|
||||||
apcIdent Text
|
apcIdent Text
|
||||||
timestamp UTCTime default=now()
|
timestamp UTCTime default=now()
|
||||||
processed Bool
|
processed Bool
|
||||||
deriving Generic Show
|
deriving Generic
|
||||||
|
|
||||||
PrintAckIdAlias
|
PrintAckIdAlias
|
||||||
needle Text
|
needle Text
|
||||||
replacement Text
|
replacement Text
|
||||||
priority Int
|
priority Int
|
||||||
deriving Generic Show
|
deriving Generic
|
||||||
@ -1,8 +1,8 @@
|
|||||||
-- SPDX-FileCopyrightText: 2022-24 Gregor Kleen <gregor.kleen@ifi.lmu.de>,Sarah Vaupel <sarah.vaupel@ifi.lmu.de>,Steffen Jost <jost@tcs.ifi.lmu.de>,Steffen Jost <s.jost@fraport.de>
|
-- SPDX-FileCopyrightText: 2022-2024 Sarah Vaupel <sarah.vaupel@uniworx.de>, Gregor Kleen <gregor.kleen@ifi.lmu.de>, Sarah Vaupel <sarah.vaupel@ifi.lmu.de>, Steffen Jost <jost@cip.ifi.lmu.de>, Steffen Jost <jost@tcs.ifi.lmu.de>
|
||||||
--
|
--
|
||||||
-- SPDX-License-Identifier: AGPL-3.0-or-later
|
-- SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
-- The files in /models determine t he database scheme.
|
-- The files in /models determine the database scheme.
|
||||||
-- The organisational split into several files has no operational effects.
|
-- The organisational split into several files has no operational effects.
|
||||||
-- White-space and case matters: Each SQL table is named in 1st column of this file
|
-- White-space and case matters: Each SQL table is named in 1st column of this file
|
||||||
-- Indendent lower-case lines describe the SQL-columns of the table with name, type and options
|
-- Indendent lower-case lines describe the SQL-columns of the table with name, type and options
|
||||||
@ -11,17 +11,16 @@
|
|||||||
-- Indendent upper-case lines usually impose Uniqueness constraints for rows by some columns.
|
-- Indendent upper-case lines usually impose Uniqueness constraints for rows by some columns.
|
||||||
-- Each table will also have an column storing a unique numeric row key, unless there is a row Primary columnname
|
-- Each table will also have an column storing a unique numeric row key, unless there is a row Primary columnname
|
||||||
--
|
--
|
||||||
|
|
||||||
User json -- Each Uni2work user has a corresponding row in this table; created upon first login.
|
User json -- Each Uni2work user has a corresponding row in this table; created upon first login.
|
||||||
|
ident UserIdent -- Case-insensitive user-identifier
|
||||||
|
passwordHash Text Maybe -- If specified, allows the user to login with credentials independently of external authentication
|
||||||
|
lastAuthentication UTCTime Maybe -- When did the user last authenticate?
|
||||||
surname UserSurname -- Display user names always through 'nameWidget displayName surname'
|
surname UserSurname -- Display user names always through 'nameWidget displayName surname'
|
||||||
displayName UserDisplayName
|
displayName UserDisplayName
|
||||||
displayEmail UserEmail -- Case-insensitive eMail address, used for sending; leave empty for using auto-update CompanyEmail via UserCompany
|
displayEmail UserEmail
|
||||||
email UserEmail -- Case-insensitive eMail address, used for identification and fallback for sending. Defaults to "AVSNO:dddddddd" if unknown
|
email UserEmail -- Case-insensitive eMail address, used for sending TODO: make this nullable
|
||||||
ident UserIdent -- Case-insensitive user-identifier. Defaults to "AVSID:dddddddd" if unknown
|
|
||||||
authentication AuthenticationMode -- 'AuthLDAP' or ('AuthPWHash'+password-hash)
|
|
||||||
lastAuthentication UTCTime Maybe -- last login date
|
|
||||||
created UTCTime default=now()
|
created UTCTime default=now()
|
||||||
lastLdapSynchronisation UTCTime Maybe
|
|
||||||
ldapPrimaryKey UserEduPersonPrincipalName Maybe -- Fraport Personnel Number or Email-Prefix for @fraport.de work here
|
|
||||||
tokensIssuedAfter UTCTime Maybe -- do not accept bearer tokens issued before this time (accept all tokens if null)
|
tokensIssuedAfter UTCTime Maybe -- do not accept bearer tokens issued before this time (accept all tokens if null)
|
||||||
matrikelnummer UserMatriculation Maybe -- usually a number; AVS Personalnummer; nicht Fraport Personalnummer!
|
matrikelnummer UserMatriculation Maybe -- usually a number; AVS Personalnummer; nicht Fraport Personalnummer!
|
||||||
firstName Text -- For export in tables, pre-split firstName from displayName
|
firstName Text -- For export in tables, pre-split firstName from displayName
|
||||||
@ -44,64 +43,67 @@ User json -- Each Uni2work user has a corresponding row in this table; create
|
|||||||
mobile Text Maybe
|
mobile Text Maybe
|
||||||
companyPersonalNumber Text Maybe -- Company will become a new table, but if company=fraport, some information is received via LDAP
|
companyPersonalNumber Text Maybe -- Company will become a new table, but if company=fraport, some information is received via LDAP
|
||||||
companyDepartment Text Maybe -- thus we store such information for ease of reference directly, if available
|
companyDepartment Text Maybe -- thus we store such information for ease of reference directly, if available
|
||||||
pinPassword Text Maybe -- used to encrypt pins within emails, defaults to cardno.version
|
pinPassword Text Maybe -- used to encrypt pins within emails
|
||||||
postAddress StoredMarkup Maybe -- including company name, if any, but excluding username; leave empty for using auto-update CompanyPostAddress via UserCompany
|
postAddress StoredMarkup Maybe
|
||||||
postLastUpdate UTCTime Maybe -- record postal address updates
|
postLastUpdate UTCTime Maybe -- record postal address updates
|
||||||
prefersPostal Bool default=false -- user prefers letters by post instead of email
|
prefersPostal Bool default=false -- user prefers letters by post instead of email
|
||||||
examOfficeGetSynced Bool default=true -- whether synced status should be displayed for exam results by default
|
examOfficeGetSynced Bool default=true -- whether synced status should be displayed for exam results by default
|
||||||
examOfficeGetLabels Bool default=true -- whether labels should be displayed for exam results by default
|
examOfficeGetLabels Bool default=true -- whether labels should be displayed for exam results by default
|
||||||
UniqueAuthentication ident -- Column 'ident' can be used as a row-key in this table
|
lastSync UTCTime Maybe -- When was the User data last synchronised with external sources?
|
||||||
|
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
|
UniqueEmail email -- Column 'email' can be used as a row-key in this table
|
||||||
UniqueLdapPrimaryKey ldapPrimaryKey !force -- Column 'ldapPrimaryKey' is either empty or contains a unique value
|
|
||||||
deriving Show Eq Ord Generic -- Haskell-specific settings for runtime-value representing a row in memory
|
deriving Show Eq Ord Generic -- Haskell-specific settings for runtime-value representing a row in memory
|
||||||
|
|
||||||
|
-- | User data fetched from external user sources, used for authentication and data queries
|
||||||
|
ExternalUser
|
||||||
|
user UserIdent
|
||||||
|
source AuthSourceIdent -- Identifier of the external source in the config
|
||||||
|
data Value "default='{}'::jsonb" -- Raw user data from external source -- TODO: maybe make Maybe, iff the source only ever responds with "success"?
|
||||||
|
lastSync UTCTime -- When was the external source last queried?
|
||||||
|
UniqueExternalUser user source -- At most one entry of this user per source
|
||||||
|
deriving Show Eq Ord Generic
|
||||||
|
|
||||||
UserFunction -- Administratively assigned functions (lecturer, admin, evaluation, ...)
|
UserFunction -- Administratively assigned functions (lecturer, admin, evaluation, ...)
|
||||||
user UserId
|
user UserId
|
||||||
school SchoolId
|
school SchoolId
|
||||||
function SchoolFunction
|
function SchoolFunction
|
||||||
UniqueUserFunction user school function
|
UniqueUserFunction user school function
|
||||||
deriving Generic
|
deriving Generic
|
||||||
UserSystemFunction Show
|
UserSystemFunction
|
||||||
user UserId
|
user UserId
|
||||||
function SystemFunction -- Defined in Model.Types.User
|
function SystemFunction -- Defined in Model.Types.User
|
||||||
manual Bool -- Inserted manually by Admin or automatic from LDAP
|
manual Bool -- Inserted manually by Admin or automatic from LDAP
|
||||||
isOptOut Bool -- User has currently deactivate the role for themselves
|
isOptOut Bool -- User has currently deactivate the role for themselves
|
||||||
UniqueUserSystemFunction user function
|
UniqueUserSystemFunction user function
|
||||||
deriving Generic Show
|
deriving Generic
|
||||||
UserExamOffice
|
UserExamOffice
|
||||||
user UserId
|
user UserId
|
||||||
field StudyTermsId
|
field StudyTermsId
|
||||||
UniqueUserExamOffice user field
|
UniqueUserExamOffice user field
|
||||||
deriving Generic Show
|
deriving Generic
|
||||||
UserSchool -- Managed by users themselves, encodes "schools of interest"
|
UserSchool -- Managed by users themselves, encodes "schools of interest"
|
||||||
user UserId
|
user UserId
|
||||||
school SchoolId
|
school SchoolId
|
||||||
isOptOut Bool -- true if this a marker, that the user manually deleted this entry; it should not be recreated automatically
|
isOptOut Bool -- true if this a marker, that the user manually deleted this entry; it should not be recreated automatically
|
||||||
UniqueUserSchool user school
|
UniqueUserSchool user school
|
||||||
deriving Generic Show
|
deriving Generic
|
||||||
UserGroupMember
|
UserGroupMember
|
||||||
group UserGroupName
|
group UserGroupName
|
||||||
user UserId
|
user UserId
|
||||||
primary Checkmark nullable
|
primary Checkmark nullable
|
||||||
UniquePrimaryUserGroupMember group primary !force
|
UniquePrimaryUserGroupMember group primary !force
|
||||||
UniqueUserGroupMember group user
|
UniqueUserGroupMember group user
|
||||||
deriving Generic Show
|
deriving Generic
|
||||||
UserCompany
|
UserCompany
|
||||||
user UserId
|
user UserId
|
||||||
company CompanyId OnDeleteCascade OnUpdateCascade
|
company CompanyId OnDeleteCascade OnUpdateCascade
|
||||||
supervisor Bool default=false -- should this user be made supervisor for all _new_ users associated with this company?
|
supervisor Bool default=false -- should this user be made supervisor for all _new_ users associated with this company?
|
||||||
supervisorReroute Bool default=false -- if supervisor is true, should this supervisor receive email for _new_ company users?
|
supervisorReroute Bool default=false -- if supervisor is true, should this supervisor receive email for _new_ company users?
|
||||||
priority Int default=0 -- higher number, higher priority; default=1 for Haskell-Code
|
|
||||||
useCompanyAddress Bool default=true -- if true, CompanyPostalAddress and CompanyEmail are used if UserPostalAddress/UserDisplayEmail are Nothing, respects priority
|
|
||||||
reason Text Maybe -- miscellaneous note, e.g. Superior
|
|
||||||
UniqueUserCompany user company -- a user may belong to multiple companies, but to each one only once
|
UniqueUserCompany user company -- a user may belong to multiple companies, but to each one only once
|
||||||
deriving Generic Show
|
deriving Generic
|
||||||
UserSupervisor
|
UserSupervisor
|
||||||
supervisor UserId -- multiple supervisor per trainee possible
|
supervisor UserId -- multiple supervisor per trainee possible
|
||||||
user UserId
|
user UserId
|
||||||
rerouteNotifications Bool -- User can be his own supervisor to receive notifications as well
|
rerouteNotifications Bool -- User can be his own supervisor to receive notifications as well
|
||||||
company CompanyId Maybe OnDeleteCascade OnUpdateCascade -- this supervisor was company default supervisor at time of entry
|
UniqueUserSupervisor supervisor user -- each supervisor/user combination is unique (same supervisor can superviser the same user only once)
|
||||||
reason Text Maybe -- miscellaneous reason, e.g. Winterservice supervisision
|
deriving Generic
|
||||||
UniqueUserSupervisor supervisor user -- each supervisor/user combination is unique (same supervisor can superviser the same user only once)
|
|
||||||
deriving Generic Show
|
|
||||||
|
|
||||||
|
|||||||
@ -14,8 +14,8 @@ let
|
|||||||
'');
|
'');
|
||||||
in if self ? lastModified then fromDate else "1970-01-01T00:00:01Z";
|
in if self ? lastModified then fromDate else "1970-01-01T00:00:01Z";
|
||||||
|
|
||||||
mkUniworxDocker = { isTest }: prev.dockerTools.buildImage {
|
mkUniworxDocker = { scope, isTest }: prev.dockerTools.buildImage {
|
||||||
name = "uniworx${optionalString isTest "-test"}";
|
name = "uniworx" + (if scope == null then "" else "-${scope}");
|
||||||
tag =
|
tag =
|
||||||
let
|
let
|
||||||
versionFile = if isTest then ./test-version.json else ./version.json;
|
versionFile = if isTest then ./test-version.json else ./version.json;
|
||||||
@ -31,12 +31,11 @@ let
|
|||||||
busybox # should provide a working lpr -- to be tested
|
busybox # should provide a working lpr -- to be tested
|
||||||
htop
|
htop
|
||||||
pdftk # for encrypting pdfs
|
pdftk # for encrypting pdfs
|
||||||
roboto roboto-mono
|
|
||||||
#texlive.combined.scheme-medium # too large for container in LMU build environment.
|
#texlive.combined.scheme-medium # too large for container in LMU build environment.
|
||||||
(texlive.combine {
|
(texlive.combine {
|
||||||
inherit (texlive) scheme-basic
|
inherit (texlive) scheme-basic
|
||||||
babel-german babel-english booktabs textpos
|
babel-german babel-english booktabs textpos
|
||||||
enumitem eurosym koma-script parskip xcolor roboto xkeyval
|
enumitem eurosym koma-script parskip xcolor dejavu
|
||||||
# required fro LuaTeX
|
# required fro LuaTeX
|
||||||
luatexbase lualatex-math unicode-math selnolig
|
luatexbase lualatex-math unicode-math selnolig
|
||||||
;
|
;
|
||||||
@ -112,6 +111,7 @@ let
|
|||||||
};
|
};
|
||||||
in
|
in
|
||||||
mapAttrs (_name: mkUniworxDocker) {
|
mapAttrs (_name: mkUniworxDocker) {
|
||||||
uniworxTestDocker = { isTest = true; };
|
uniworxDocker = { isTest = false; scope = null; };
|
||||||
uniworxDocker = { isTest = false; };
|
uniworxTestDocker = { isTest = false; scope = "test"; };
|
||||||
|
uniworxDevDocker = { isTest = false; scope = "dev"; };
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,3 +1,3 @@
|
|||||||
{
|
{
|
||||||
"version": "27.4.79"
|
"version": "28.1.1"
|
||||||
}
|
}
|
||||||
|
|||||||
2
package-lock.json
generated
2
package-lock.json
generated
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "uni2work",
|
"name": "uni2work",
|
||||||
"version": "27.4.79",
|
"version": "28.1.1",
|
||||||
"lockfileVersion": 1,
|
"lockfileVersion": 1,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "uni2work",
|
"name": "uni2work",
|
||||||
"version": "27.4.79",
|
"version": "28.1.1",
|
||||||
"description": "",
|
"description": "",
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
"author": "",
|
"author": "",
|
||||||
@ -25,7 +25,9 @@
|
|||||||
"i18n:test": "./missing-translations.sh",
|
"i18n:test": "./missing-translations.sh",
|
||||||
"prerelease": "./is-clean.sh && npm run test",
|
"prerelease": "./is-clean.sh && npm run test",
|
||||||
"release": "./release.sh",
|
"release": "./release.sh",
|
||||||
|
"release-dev": "./release.sh --dev",
|
||||||
"postrelease": "git push --follow-tags",
|
"postrelease": "git push --follow-tags",
|
||||||
|
"postrelease-dev": "git push --follow-tags",
|
||||||
"parse-changelog": "changelog-parser ./CHANGELOG.md > changelog.json"
|
"parse-changelog": "changelog-parser ./CHANGELOG.md > changelog.json"
|
||||||
},
|
},
|
||||||
"husky": {
|
"husky": {
|
||||||
|
|||||||
10
package.yaml
10
package.yaml
@ -1,11 +1,12 @@
|
|||||||
name: uniworx
|
name: uniworx
|
||||||
version: 27.4.79
|
version: 28.1.1
|
||||||
dependencies:
|
dependencies:
|
||||||
- base
|
- base
|
||||||
- yesod
|
- yesod
|
||||||
- yesod-core
|
- yesod-core
|
||||||
- yesod-persistent
|
- yesod-persistent
|
||||||
- yesod-auth
|
- yesod-auth
|
||||||
|
- yesod-auth-oauth2
|
||||||
- yesod-static
|
- yesod-static
|
||||||
- yesod-form
|
- yesod-form
|
||||||
- yesod-persistent
|
- yesod-persistent
|
||||||
@ -20,7 +21,6 @@ dependencies:
|
|||||||
- template-haskell
|
- template-haskell
|
||||||
- shakespeare
|
- shakespeare
|
||||||
- monad-control
|
- monad-control
|
||||||
- wai-extra
|
|
||||||
- yaml
|
- yaml
|
||||||
- http-conduit
|
- http-conduit
|
||||||
- directory
|
- directory
|
||||||
@ -30,7 +30,6 @@ dependencies:
|
|||||||
- conduit
|
- conduit
|
||||||
- monad-logger
|
- monad-logger
|
||||||
- fast-logger
|
- fast-logger
|
||||||
- wai-logger
|
|
||||||
- foreign-store
|
- foreign-store
|
||||||
- file-embed
|
- file-embed
|
||||||
- unordered-containers
|
- unordered-containers
|
||||||
@ -39,6 +38,10 @@ dependencies:
|
|||||||
- time
|
- time
|
||||||
- case-insensitive
|
- case-insensitive
|
||||||
- wai
|
- wai
|
||||||
|
- wai-cors
|
||||||
|
- wai-extra
|
||||||
|
- wai-logger
|
||||||
|
- wai-middleware-prometheus
|
||||||
- cryptonite
|
- cryptonite
|
||||||
- cryptonite-conduit
|
- cryptonite-conduit
|
||||||
- saltine
|
- saltine
|
||||||
@ -143,7 +146,6 @@ dependencies:
|
|||||||
- cookie
|
- cookie
|
||||||
- prometheus-client
|
- prometheus-client
|
||||||
- prometheus-metrics-ghc
|
- prometheus-metrics-ghc
|
||||||
- wai-middleware-prometheus
|
|
||||||
- extended-reals
|
- extended-reals
|
||||||
- rfc5051
|
- rfc5051
|
||||||
- unidecode
|
- unidecode
|
||||||
|
|||||||
11
release.sh
11
release.sh
@ -1,6 +1,6 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
# SPDX-FileCopyrightText: 2023 Sarah Vaupel <sarah.vaupel@uniworx.de>
|
# SPDX-FileCopyrightText: 2023-2024 Sarah Vaupel <sarah.vaupel@uniworx.de>
|
||||||
#
|
#
|
||||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
@ -14,7 +14,12 @@ case "$(git rev-parse --abbrev-ref HEAD)" in
|
|||||||
standard-version -a -t t
|
standard-version -a -t t
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
echo "Current branch not supported for release!"
|
if echo $@ | grep -xqe '--dev';
|
||||||
exit 1
|
then
|
||||||
|
standard-version -a -t d
|
||||||
|
else
|
||||||
|
echo "Current branch not supported for release!"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|||||||
52
routes
52
routes
@ -1,4 +1,4 @@
|
|||||||
-- SPDX-FileCopyrightText: 2022-24 Gregor Kleen <gregor.kleen@ifi.lmu.de>,Sarah Vaupel <sarah.vaupel@ifi.lmu.de>,Steffen Jost <jost@tcs.ifi.lmu.de>,Wolfgang Witt <Wolfgang.Witt@campus.lmu.de>,Steffen Jost <s.jost@fraport.de>
|
-- SPDX-FileCopyrightText: 2022-2024 Sarah Vaupel <sarah.vaupel@uniworx.de>, Gregor Kleen <gregor.kleen@ifi.lmu.de>,Sarah Vaupel <sarah.vaupel@ifi.lmu.de>,Steffen Jost <jost@tcs.ifi.lmu.de>,Wolfgang Witt <Wolfgang.Witt@campus.lmu.de>,David Mosbach <david.mosbach@uniworx.de>
|
||||||
--
|
--
|
||||||
-- SPDX-License-Identifier: AGPL-3.0-or-later
|
-- SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
@ -30,8 +30,8 @@
|
|||||||
-- !capacity -- course this route is associated with has at least one unit of participant capacity
|
-- !capacity -- course this route is associated with has at least one unit of participant capacity
|
||||||
-- !empty -- course this route is associated with has no participants whatsoever
|
-- !empty -- course this route is associated with has no participants whatsoever
|
||||||
--
|
--
|
||||||
-- !is-ldap -- user has authentication mode set to LDAP
|
-- !is-external -- user can login using external sources
|
||||||
-- !is-pw-hash -- user has authentication mode set to PWHash
|
-- !is-internal -- user can login using internal credentials
|
||||||
--
|
--
|
||||||
-- !materials -- only if course allows all materials to be free (no meaning outside of courses)
|
-- !materials -- only if course allows all materials to be free (no meaning outside of courses)
|
||||||
-- !time -- access depends on time somehow
|
-- !time -- access depends on time somehow
|
||||||
@ -46,43 +46,39 @@
|
|||||||
/static StaticR EmbeddedStatic appStatic !free
|
/static StaticR EmbeddedStatic appStatic !free
|
||||||
/auth AuthR Auth getAuth !free
|
/auth AuthR Auth getAuth !free
|
||||||
|
|
||||||
|
/logout SOutR GET !free
|
||||||
|
/logout/ssout SSOutR GET !free -- single sign-out (OIDC)
|
||||||
|
|
||||||
/metrics MetricsR GET !free -- verify if this can be free
|
/metrics MetricsR GET !free -- verify if this can be free
|
||||||
|
|
||||||
/err ErrorR GET !free
|
/err ErrorR GET !free
|
||||||
|
|
||||||
/ NewsR GET !free
|
/ NewsR GET !free
|
||||||
/users UsersR GET POST -- no tags, i.e. admins only
|
/users UsersR GET POST -- no tags, i.e. admins only
|
||||||
/users/#CryptoUUIDUser AdminUserR GET POST
|
/users/#CryptoUUIDUser AdminUserR GET POST
|
||||||
/users/#CryptoUUIDUser/delete AdminUserDeleteR POST
|
/users/#CryptoUUIDUser/delete AdminUserDeleteR POST
|
||||||
/users/#CryptoUUIDUser/hijack AdminHijackUserR GET POST !adminANDno-escalation
|
/users/#CryptoUUIDUser/hijack AdminHijackUserR GET POST !adminANDno-escalation
|
||||||
/users/#CryptoUUIDUser/notifications UserNotificationR GET POST !self
|
/users/#CryptoUUIDUser/notifications UserNotificationR GET POST !self
|
||||||
/users/#CryptoUUIDUser/password UserPasswordR GET POST !selfANDis-pw-hash
|
/users/#CryptoUUIDUser/password UserPasswordR GET POST !selfANDis-pw-hash
|
||||||
!/users/functionary-invite/new AdminNewFunctionaryInviteR GET POST
|
!/users/functionary-invite/new AdminNewFunctionaryInviteR GET POST
|
||||||
!/users/functionary-invite AdminFunctionaryInviteR GET POST
|
!/users/functionary-invite AdminFunctionaryInviteR GET POST
|
||||||
!/users/add AdminUserAddR GET POST
|
!/users/add AdminUserAddR GET POST
|
||||||
/admin AdminR GET
|
/admin AdminR GET
|
||||||
/admin/test AdminTestR GET POST
|
/admin/test AdminTestR GET POST
|
||||||
/admin/test/pdf AdminTestPdfR GET
|
/admin/test/pdf AdminTestPdfR GET
|
||||||
/admin/errMsg AdminErrMsgR GET POST
|
/admin/errMsg AdminErrMsgR GET POST
|
||||||
/admin/tokens AdminTokensR GET POST
|
/admin/tokens AdminTokensR GET POST
|
||||||
/admin/crontab AdminCrontabR GET
|
/admin/crontab AdminCrontabR GET
|
||||||
/admin/crontab/jobs AdminJobsR GET POST
|
/admin/crontab/jobs AdminJobsR GET POST
|
||||||
/admin/avs AdminAvsR GET POST
|
/admin/avs AdminAvsR GET POST
|
||||||
/admin/avs/#CryptoUUIDUser AdminAvsUserR GET POST
|
/admin/avs/#CryptoUUIDUser AdminAvsUserR GET
|
||||||
/admin/ldap AdminLdapR GET POST
|
/admin/external-user AdminExternalUserR GET POST
|
||||||
/admin/problems AdminProblemsR GET POST
|
/admin/problems AdminProblemsR GET
|
||||||
/admin/problems/no-contact ProblemUnreachableR GET POST
|
/admin/problems/no-contact ProblemUnreachableR GET
|
||||||
/admin/problems/no-avs-id ProblemWithoutAvsId GET
|
/admin/problems/no-avs-id ProblemWithoutAvsId GET
|
||||||
/admin/problems/r-without-f ProblemFbutNoR GET
|
/admin/problems/r-without-f ProblemFbutNoR GET
|
||||||
/admin/problems/avs ProblemAvsSynchR GET POST
|
/admin/problems/avs ProblemAvsSynchR GET POST
|
||||||
/admin/problems/avs/errors ProblemAvsErrorR GET
|
/admin/problems/avs/errors ProblemAvsErrorR GET
|
||||||
/admin/config/interfaces ConfigInterfacesR GET POST
|
|
||||||
|
|
||||||
/comm CommCenterR GET
|
|
||||||
/comm/email MailCenterR GET POST
|
|
||||||
/comm/email/html/#CryptoUUIDSentMail MailHtmlR GET
|
|
||||||
/comm/email/plain/#CryptoUUIDSentMail MailPlainR GET
|
|
||||||
/comm/email/attachment/#CryptoUUIDSentMail/#Text MailAttachmentR GET
|
|
||||||
|
|
||||||
/print PrintCenterR GET POST !system-printer
|
/print PrintCenterR GET POST !system-printer
|
||||||
/print/acknowledge/#Day/#Int/#Int PrintAckR GET POST !system-printer
|
/print/acknowledge/#Day/#Int/#Int PrintAckR GET POST !system-printer
|
||||||
|
|||||||
47
shell.nix
47
shell.nix
@ -1,4 +1,4 @@
|
|||||||
# SPDX-FileCopyrightText: 2022-2023 Gregor Kleen <gregor@kleen.consulting>, Sarah Vaupel <sarah.vaupel@uniworx.de>, Steffen Jost <jost@tcs.ifi.lmu.de>
|
# SPDX-FileCopyrightText: 2022-2024 Gregor Kleen <gregor@kleen.consulting>, Sarah Vaupel <sarah.vaupel@uniworx.de>, Steffen Jost <jost@tcs.ifi.lmu.de>, David Mosbach <david.mosbach@uniworx.de>
|
||||||
#
|
#
|
||||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
@ -9,6 +9,13 @@ let
|
|||||||
|
|
||||||
haskellPackages = pkgs.haskellPackages;
|
haskellPackages = pkgs.haskellPackages;
|
||||||
|
|
||||||
|
oauth2Flake = (builtins.getFlake "git+https://gitlab.uniworx.de/mosbach/oauth2-mock-server/?rev=7b995e6cffa963a24eb5d0373b2d29089533284f&ref=main").packages.x86_64-linux;
|
||||||
|
|
||||||
|
|
||||||
|
oauth2MockServer = oauth2Flake.default;
|
||||||
|
mkOauth2DB = oauth2Flake.mkOauth2DB;
|
||||||
|
killOauth2DB = oauth2Flake.killOauth2DB;
|
||||||
|
|
||||||
postgresSchema = pkgs.writeText "schema.sql" ''
|
postgresSchema = pkgs.writeText "schema.sql" ''
|
||||||
CREATE USER uniworx WITH SUPERUSER;
|
CREATE USER uniworx WITH SUPERUSER;
|
||||||
CREATE DATABASE uniworx_test;
|
CREATE DATABASE uniworx_test;
|
||||||
@ -21,6 +28,17 @@ let
|
|||||||
local all all trust
|
local all all trust
|
||||||
'';
|
'';
|
||||||
|
|
||||||
|
oauth2Schema = pkgs.writeText "oauth2_schema.sql" ''
|
||||||
|
CREATE USER oauth2mock WITH SUPERUSER;
|
||||||
|
CREATE DATABASE test_users;
|
||||||
|
GRANT ALL ON DATABASE test_users TO oauth2mock;
|
||||||
|
'';
|
||||||
|
|
||||||
|
oauth2Hba = pkgs.writeText "oauth2_hba_file" ''
|
||||||
|
local all all trust
|
||||||
|
'';
|
||||||
|
|
||||||
|
|
||||||
develop = pkgs.writeScriptBin "develop" ''
|
develop = pkgs.writeScriptBin "develop" ''
|
||||||
#!${pkgs.zsh}/bin/zsh -e
|
#!${pkgs.zsh}/bin/zsh -e
|
||||||
|
|
||||||
@ -44,6 +62,9 @@ let
|
|||||||
type cleanup_cache_memcached &>/dev/null && cleanup_cache_memcached
|
type cleanup_cache_memcached &>/dev/null && cleanup_cache_memcached
|
||||||
type cleanup_minio &>/dev/null && cleanup_minio
|
type cleanup_minio &>/dev/null && cleanup_minio
|
||||||
type cleanup_maildev &>/dev/null && cleanup_maildev
|
type cleanup_maildev &>/dev/null && cleanup_maildev
|
||||||
|
[[ -z "$OAUTH2_PGDIR" ]] || source ${killOauth2DB}/bin/killOauth2DB
|
||||||
|
[[ -z "$OAUTH2_PGHOST" ]] || pkill oauth2-mock-ser
|
||||||
|
[[ -z "$PORT_OFFSET" ]] || runghc .ports/assign.hs --remove $PORT_OFFSET
|
||||||
|
|
||||||
[ -f "''${basePath}/.develop.env" ] && rm -vf "''${basePath}/.develop.env"
|
[ -f "''${basePath}/.develop.env" ] && rm -vf "''${basePath}/.develop.env"
|
||||||
set +x
|
set +x
|
||||||
@ -51,7 +72,17 @@ let
|
|||||||
|
|
||||||
trap cleanup EXIT
|
trap cleanup EXIT
|
||||||
|
|
||||||
export PORT_OFFSET=$(((16#$(sha256sum <<<"$(hostname -f):''${basePath}" | head -c 16)) % 1000))
|
export PORT_OFFSET=$(runghc .ports/assign.hs --assign .ports/offsets)
|
||||||
|
# export PORT_OFFSET=$(((16#$(sha256sum <<<"$(hostname -f):''${basePath}" | head -c 16)) % 1000))
|
||||||
|
|
||||||
|
if [[ -z "$OAUTH2_PGHOST" ]]; then
|
||||||
|
set -xe
|
||||||
|
export OAUTH2_SERVER_PORT=$((9443 + $PORT_OFFSET))
|
||||||
|
export OAUTH2_DB_PORT=$((9444 + $PORT_OFFSET))
|
||||||
|
source ${mkOauth2DB}/bin/mkOauth2DB
|
||||||
|
${oauth2MockServer}/bin/oauth2-mock-server&
|
||||||
|
set +xe
|
||||||
|
fi
|
||||||
|
|
||||||
if [[ -z "$PGHOST" ]]; then
|
if [[ -z "$PGHOST" ]]; then
|
||||||
set -xe
|
set -xe
|
||||||
@ -197,9 +228,9 @@ let
|
|||||||
UPLOAD_S3_KEY_ID=''${MINIO_ACCESS_KEY}
|
UPLOAD_S3_KEY_ID=''${MINIO_ACCESS_KEY}
|
||||||
UPLOAD_S3_KEY=''${MINIO_SECRET_KEY}
|
UPLOAD_S3_KEY=''${MINIO_SECRET_KEY}
|
||||||
|
|
||||||
# SMTPHOST=''${SMTPHOST}
|
SMTPHOST=''${SMTPHOST}
|
||||||
# SMTPPORT=''${SMTPPORT}
|
SMTPPORT=''${SMTPPORT}
|
||||||
# SMTPSSL=''${SMTPSSL}
|
SMTPSSL=''${SMTPSSL}
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
set +xe
|
set +xe
|
||||||
@ -271,6 +302,9 @@ in pkgs.mkShell {
|
|||||||
|
|
||||||
export CHROME_BIN=${pkgs.chromium}/bin/chromium
|
export CHROME_BIN=${pkgs.chromium}/bin/chromium
|
||||||
'';
|
'';
|
||||||
|
OAUTH2_HBA = oauth2Hba;
|
||||||
|
OAUTH2_DB_SCHEMA = oauth2Schema;
|
||||||
|
OAUTH2_TEST_USERS = ./test/Database/test-users.yaml;
|
||||||
nativeBuildInputs = [develop inDevelop killallUni2work diffRunning]
|
nativeBuildInputs = [develop inDevelop killallUni2work diffRunning]
|
||||||
++ (with pkgs;
|
++ (with pkgs;
|
||||||
[ stack nodejs-14_x postgresql_12 openldap exiftool memcached minio minio-client
|
[ stack nodejs-14_x postgresql_12 openldap exiftool memcached minio minio-client
|
||||||
@ -279,14 +313,13 @@ in pkgs.mkShell {
|
|||||||
# busybox # for print services, but interferes with build commands in develop-shell
|
# busybox # for print services, but interferes with build commands in develop-shell
|
||||||
htop
|
htop
|
||||||
pdftk # pdftk just for testing pdf-passwords
|
pdftk # pdftk just for testing pdf-passwords
|
||||||
roboto roboto-mono
|
|
||||||
# texlive.combined.scheme-full # works
|
# texlive.combined.scheme-full # works
|
||||||
# texlive.combined.scheme-medium
|
# texlive.combined.scheme-medium
|
||||||
# texlive.combined.scheme-small
|
# texlive.combined.scheme-small
|
||||||
(texlive.combine {
|
(texlive.combine {
|
||||||
inherit (texlive) scheme-basic
|
inherit (texlive) scheme-basic
|
||||||
babel-german babel-english booktabs textpos
|
babel-german babel-english booktabs textpos
|
||||||
enumitem eurosym koma-script parskip xcolor roboto xkeyval
|
enumitem eurosym koma-script parskip xcolor dejavu
|
||||||
luatexbase lualatex-math unicode-math selnolig # required for LuaTeX
|
luatexbase lualatex-math unicode-math selnolig # required for LuaTeX
|
||||||
;
|
;
|
||||||
})
|
})
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
-- SPDX-FileCopyrightText: 2022 Gregor Kleen <gregor.kleen@ifi.lmu.de>,Sarah Vaupel <sarah.vaupel@ifi.lmu.de>,Sarah Vaupel <vaupel.sarah@campus.lmu.de>,Steffen Jost <jost@tcs.ifi.lmu.de>
|
-- SPDX-FileCopyrightText: 2022-2024 Sarah Vaupel <sarah.vaupel@uniworx.de>, Gregor Kleen <gregor.kleen@ifi.lmu.de>, Sarah Vaupel <sarah.vaupel@ifi.lmu.de>, Sarah Vaupel <vaupel.sarah@campus.lmu.de>, Steffen Jost <jost@tcs.ifi.lmu.de>, David Mosbach <david.mosbach@uniworx.de>
|
||||||
--
|
--
|
||||||
-- SPDX-License-Identifier: AGPL-3.0-or-later
|
-- SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
@ -6,10 +6,8 @@
|
|||||||
|
|
||||||
module Application
|
module Application
|
||||||
( getAppSettings, getAppDevSettings
|
( getAppSettings, getAppDevSettings
|
||||||
, appMain
|
, appMain, develMain
|
||||||
, develMain
|
|
||||||
, makeFoundation
|
, makeFoundation
|
||||||
, makeMiddleware
|
|
||||||
-- * for DevelMain
|
-- * for DevelMain
|
||||||
, foundationStoreNum
|
, foundationStoreNum
|
||||||
, getApplicationRepl
|
, getApplicationRepl
|
||||||
@ -20,111 +18,98 @@ module Application
|
|||||||
, addPWEntry
|
, addPWEntry
|
||||||
) where
|
) where
|
||||||
|
|
||||||
import Control.Monad.Logger (liftLoc, LoggingT(..), MonadLoggerIO(..))
|
import Import hiding (cancel, respond)
|
||||||
|
|
||||||
|
import Handler.Utils (runAppLoggingT)
|
||||||
|
import Handler.Utils.Memcached (manageMemcachedLocalInvalidations)
|
||||||
|
|
||||||
|
import Jobs
|
||||||
|
|
||||||
|
import Middleware
|
||||||
|
|
||||||
|
import Utils.Avs
|
||||||
|
import qualified Utils.Pool as Custom
|
||||||
|
import Utils.Postgresql
|
||||||
|
|
||||||
|
|
||||||
|
import Control.Concurrent.STM.Delay
|
||||||
|
import Control.Monad.Logger (liftLoc, LoggingT(..), MonadLoggerIO(..))
|
||||||
|
import Control.Monad.Trans.Cont (runContT, callCC)
|
||||||
|
import Control.Monad.Trans.Resource
|
||||||
|
|
||||||
|
import qualified Data.Acid.Memory as Acid
|
||||||
|
import qualified Data.Aeson as Aeson
|
||||||
|
import qualified Data.ByteString.Lazy as LBS
|
||||||
|
import qualified Data.IntervalMap.Strict as IntervalMap
|
||||||
|
import qualified Data.Map as Map
|
||||||
|
import Data.Ratio ((%))
|
||||||
|
import qualified Data.Set as Set
|
||||||
|
import Data.Streaming.Network (bindPortTCP)
|
||||||
|
import qualified Data.Text.Encoding as Text
|
||||||
|
import qualified Data.UUID as UUID
|
||||||
|
import qualified Data.UUID.V4 as UUID
|
||||||
|
|
||||||
|
import qualified Database.Memcached.Binary.IO as Memcached
|
||||||
import Database.Persist.Postgresql ( openSimpleConn, pgConnStr, pgPoolIdleTimeout
|
import Database.Persist.Postgresql ( openSimpleConn, pgConnStr, pgPoolIdleTimeout
|
||||||
, pgPoolSize
|
, pgPoolSize
|
||||||
)
|
)
|
||||||
import Database.Persist.SqlBackend.Internal ( connClose )
|
import Database.Persist.SqlBackend.Internal ( connClose )
|
||||||
import qualified Database.PostgreSQL.Simple as PG
|
import qualified Database.PostgreSQL.Simple as PG
|
||||||
import Import hiding (cancel, respond)
|
|
||||||
import Language.Haskell.TH.Syntax (qLocation)
|
|
||||||
import Network.Wai (Middleware)
|
|
||||||
import qualified Network.Wai as Wai
|
|
||||||
import Network.Wai.Handler.Warp (Settings, defaultSettings,
|
|
||||||
defaultShouldDisplayException,
|
|
||||||
runSettings, runSettingsSocket, setHost,
|
|
||||||
setBeforeMainLoop,
|
|
||||||
setOnException, setPort, getPort)
|
|
||||||
import Network.Connection (settingDisableCertificateValidation)
|
|
||||||
import Data.Streaming.Network (bindPortTCP)
|
|
||||||
import Network.Wai.Middleware.RequestLogger (Destination (Logger),
|
|
||||||
IPAddrSource (..),
|
|
||||||
OutputFormat (..), destination,
|
|
||||||
mkRequestLogger, outputFormat)
|
|
||||||
import System.Log.FastLogger ( defaultBufSize, newStderrLoggerSet, newStdoutLoggerSet, newFileLoggerSet
|
|
||||||
, toLogStr, rmLoggerSet
|
|
||||||
)
|
|
||||||
|
|
||||||
import Handler.Utils (runAppLoggingT)
|
|
||||||
|
|
||||||
import Foreign.Store
|
import Foreign.Store
|
||||||
|
|
||||||
import Web.Cookie
|
import GHC.RTS.Flags (getRTSFlags)
|
||||||
import Network.HTTP.Types.Header (hSetCookie)
|
|
||||||
|
|
||||||
import qualified Data.UUID as UUID
|
import Language.Haskell.TH.Syntax (qLocation)
|
||||||
import qualified Data.UUID.V4 as UUID
|
|
||||||
|
|
||||||
import System.Directory
|
import qualified Ldap.Client as Ldap (Host(Plain,Tls))
|
||||||
|
|
||||||
import Jobs
|
|
||||||
|
|
||||||
import qualified Data.Text.Encoding as Text
|
|
||||||
|
|
||||||
import Yesod.Auth.Util.PasswordStore
|
|
||||||
|
|
||||||
import qualified Data.ByteString.Lazy as LBS
|
|
||||||
|
|
||||||
|
import Network.Connection (settingDisableCertificateValidation)
|
||||||
import Network.HaskellNet.SSL hiding (Settings)
|
import Network.HaskellNet.SSL hiding (Settings)
|
||||||
import Network.HaskellNet.SMTP.SSL as SMTP hiding (Settings)
|
import Network.HaskellNet.SMTP.SSL as SMTP hiding (Settings)
|
||||||
|
import Network.HTTP.Client.TLS (mkManagerSettings)
|
||||||
|
import qualified Network.Minio as Minio
|
||||||
|
import Network.Socket (socketPort, Socket, PortNumber)
|
||||||
|
import qualified Network.Socket as Socket (close)
|
||||||
|
import Network.Wai.Handler.Warp ( Settings
|
||||||
|
, defaultSettings
|
||||||
|
, defaultShouldDisplayException
|
||||||
|
, runSettings, runSettingsSocket
|
||||||
|
, getPort, setPort
|
||||||
|
, setHost, setBeforeMainLoop, setOnException
|
||||||
|
)
|
||||||
|
|
||||||
|
import qualified Prometheus
|
||||||
|
|
||||||
|
import qualified System.Clock as Clock
|
||||||
|
import System.Directory
|
||||||
|
import System.Environment (lookupEnv)
|
||||||
|
import System.Exit
|
||||||
|
import System.Log.FastLogger ( defaultBufSize
|
||||||
|
, newStderrLoggerSet, newStdoutLoggerSet, newFileLoggerSet
|
||||||
|
, toLogStr, rmLoggerSet
|
||||||
|
)
|
||||||
|
import System.Log.FastLogger.Date
|
||||||
|
import System.Posix.Process (getProcessID)
|
||||||
|
import System.Posix.Signals (SignalInfo(..), installHandler, sigTERM, sigINT)
|
||||||
|
import qualified System.Posix.Signals as Signals (Handler(..))
|
||||||
|
import qualified System.Systemd.Daemon as Systemd
|
||||||
|
|
||||||
import UnliftIO.Concurrent
|
import UnliftIO.Concurrent
|
||||||
import UnliftIO.Pool
|
import UnliftIO.Pool
|
||||||
|
|
||||||
import Control.Monad.Trans.Resource
|
|
||||||
|
|
||||||
import System.Log.FastLogger.Date
|
|
||||||
import qualified Yesod.Core.Types as Yesod (Logger(..))
|
|
||||||
|
|
||||||
import qualified Data.HashMap.Strict as HashMap
|
|
||||||
|
|
||||||
import qualified Data.Aeson as Aeson
|
|
||||||
|
|
||||||
import System.Exit
|
|
||||||
|
|
||||||
import qualified Database.Memcached.Binary.IO as Memcached
|
|
||||||
|
|
||||||
import qualified System.Systemd.Daemon as Systemd
|
|
||||||
import System.Environment (lookupEnv)
|
|
||||||
import System.Posix.Process (getProcessID)
|
|
||||||
import System.Posix.Signals (SignalInfo(..), installHandler, sigTERM, sigINT)
|
|
||||||
import qualified System.Posix.Signals as Signals (Handler(..))
|
|
||||||
|
|
||||||
import Network.Socket (socketPort, Socket, PortNumber)
|
|
||||||
import qualified Network.Socket as Socket (close)
|
|
||||||
|
|
||||||
import Control.Concurrent.STM.Delay
|
|
||||||
import Control.Monad.Trans.Cont (runContT, callCC)
|
|
||||||
|
|
||||||
import Data.Ratio ((%))
|
|
||||||
import qualified Data.Set as Set
|
|
||||||
import qualified Data.Map as Map
|
|
||||||
|
|
||||||
import Handler.Utils.Routes (classifyHandler)
|
|
||||||
|
|
||||||
import qualified Data.Acid.Memory as Acid
|
|
||||||
import qualified Web.ServerSession.Backend.Acid as Acid
|
import qualified Web.ServerSession.Backend.Acid as Acid
|
||||||
|
|
||||||
import qualified Ldap.Client as Ldap (Host(Plain, Tls))
|
|
||||||
|
|
||||||
import qualified Network.Minio as Minio
|
|
||||||
|
|
||||||
import Web.ServerSession.Core (StorageException(..))
|
import Web.ServerSession.Core (StorageException(..))
|
||||||
|
|
||||||
import GHC.RTS.Flags (getRTSFlags)
|
import Yesod.Auth.OAuth2.AzureAD (oauth2AzureADScoped)
|
||||||
|
import Yesod.Auth.Util.PasswordStore
|
||||||
|
import qualified Yesod.Core.Types as Yesod (Logger(..))
|
||||||
|
|
||||||
import qualified Prometheus
|
#ifdef DEVELOPMENT
|
||||||
|
import Data.Maybe (fromJust)
|
||||||
|
import Auth.OAuth2 (azureMockServer)
|
||||||
|
#endif
|
||||||
|
|
||||||
import qualified Data.IntervalMap.Strict as IntervalMap
|
|
||||||
|
|
||||||
import qualified Utils.Pool as Custom
|
|
||||||
|
|
||||||
import Utils.Postgresql
|
|
||||||
import Handler.Utils.Memcached (manageMemcachedLocalInvalidations)
|
|
||||||
|
|
||||||
import qualified System.Clock as Clock
|
|
||||||
|
|
||||||
import Utils.Avs (mkAvsQuery)
|
|
||||||
|
|
||||||
-- Import all relevant handler modules here.
|
-- Import all relevant handler modules here.
|
||||||
-- (HPack takes care to add new modules to our cabal file nowadays.)
|
-- (HPack takes care to add new modules to our cabal file nowadays.)
|
||||||
@ -157,17 +142,15 @@ import Handler.Upload
|
|||||||
import Handler.Qualification
|
import Handler.Qualification
|
||||||
import Handler.LMS
|
import Handler.LMS
|
||||||
import Handler.SAP
|
import Handler.SAP
|
||||||
import Handler.CommCenter
|
|
||||||
import Handler.MailCenter
|
|
||||||
import Handler.PrintCenter
|
import Handler.PrintCenter
|
||||||
import Handler.ApiDocs
|
import Handler.ApiDocs
|
||||||
import Handler.Swagger
|
import Handler.Swagger
|
||||||
import Handler.Firm
|
import Handler.Firm
|
||||||
|
import Handler.SingleSignOut
|
||||||
|
|
||||||
import ServantApi () -- YesodSubDispatch instances
|
import ServantApi () -- YesodSubDispatch instances
|
||||||
import Servant.API
|
import Servant.API
|
||||||
import Servant.Client
|
import Servant.Client
|
||||||
import Network.HTTP.Client.TLS (mkManagerSettings)
|
|
||||||
|
|
||||||
|
|
||||||
-- This line actually creates our YesodDispatch instance. It is the second half
|
-- This line actually creates our YesodDispatch instance. It is the second half
|
||||||
@ -238,7 +221,7 @@ makeFoundation appSettings''@AppSettings{..} = do
|
|||||||
-- from there, and then create the real foundation.
|
-- from there, and then create the real foundation.
|
||||||
let
|
let
|
||||||
mkFoundation :: _ -> (forall m. MonadIO m => Custom.Pool' m DBConnLabel DBConnUseState SqlBackend) -> _
|
mkFoundation :: _ -> (forall m. MonadIO m => Custom.Pool' m DBConnLabel DBConnUseState SqlBackend) -> _
|
||||||
mkFoundation appSettings' appConnPool appSmtpPool appLdapPool appCryptoIDKey appSessionStore appSecretBoxKey appWidgetMemcached appJSONWebKeySet appClusterID appMemcached appMemcachedLocal appUploadCache appVerpSecret appAuthKey appPersonalisedSheetFilesSeedKey appVolatileClusterSettingsCache appAvsQuery = UniWorX{..}
|
mkFoundation appSettings' appConnPool appSmtpPool appLdapPool appCryptoIDKey appSessionStore appSecretBoxKey appWidgetMemcached appJSONWebKeySet appClusterID appMemcached appMemcachedLocal appUploadCache appVerpSecret appAuthKey appAuthPlugins appPersonalisedSheetFilesSeedKey appVolatileClusterSettingsCache appAvsQuery = UniWorX{..}
|
||||||
tempFoundation = mkFoundation
|
tempFoundation = mkFoundation
|
||||||
(error "appSettings' forced in tempFoundation")
|
(error "appSettings' forced in tempFoundation")
|
||||||
(error "connPool forced in tempFoundation")
|
(error "connPool forced in tempFoundation")
|
||||||
@ -255,10 +238,12 @@ makeFoundation appSettings''@AppSettings{..} = do
|
|||||||
(error "MinioConn forced in tempFoundation")
|
(error "MinioConn forced in tempFoundation")
|
||||||
(error "VerpSecret forced in tempFoundation")
|
(error "VerpSecret forced in tempFoundation")
|
||||||
(error "AuthKey forced in tempFoundation")
|
(error "AuthKey forced in tempFoundation")
|
||||||
|
(error "AuthPlugins forced in tempFoundation")
|
||||||
(error "PersonalisedSheetFilesSeedKey forced in tempFoundation")
|
(error "PersonalisedSheetFilesSeedKey forced in tempFoundation")
|
||||||
(error "VolatileClusterSettingsCache forced in tempFoundation")
|
(error "VolatileClusterSettingsCache forced in tempFoundation")
|
||||||
(error "AvsQuery forced in tempFoundation")
|
(error "AvsQuery forced in tempFoundation")
|
||||||
|
|
||||||
|
|
||||||
runAppLoggingT tempFoundation $ do
|
runAppLoggingT tempFoundation $ do
|
||||||
$logInfoS "InstanceID" $ UUID.toText appInstanceID
|
$logInfoS "InstanceID" $ UUID.toText appInstanceID
|
||||||
$logInfoS "Configuration" $ tshowCrop appSettings''
|
$logInfoS "Configuration" $ tshowCrop appSettings''
|
||||||
@ -293,13 +278,32 @@ makeFoundation appSettings''@AppSettings{..} = do
|
|||||||
sqlPool = Custom.hoistPool (liftIO . flip runLoggingT logFunc) sqlPool'
|
sqlPool = Custom.hoistPool (liftIO . flip runLoggingT logFunc) sqlPool'
|
||||||
void . Prometheus.register . poolMetrics PoolDatabaseConnections $ sqlPool @IO
|
void . Prometheus.register . poolMetrics PoolDatabaseConnections $ sqlPool @IO
|
||||||
|
|
||||||
ldapPool <- traverse mkFailoverLabeled <=< forOf (traverse . traverse) appLdapConf $ \conf@LdapConf{..} -> do
|
-- ldapPool <- traverse mkFailoverLabeled <=< forOf (traverse . traverse) appUserDbConf $ \conf -> if
|
||||||
let ldapLabel = case ldapHost of
|
-- | UserDbSingleSource{..} <- conf
|
||||||
Ldap.Plain str -> pack str <> ":" <> tshow ldapPort
|
-- , UserDbLdap LdapConf{..} <- userdbSingleSource
|
||||||
Ldap.Tls str _ -> pack str <> ":" <> tshow ldapPort
|
-- , Just ResourcePoolConf{..} <- userdbPoolConf
|
||||||
$logDebugS "setup" $ "LDAP-Pool " <> ldapLabel
|
-- -> do
|
||||||
(ldapLabel,) . (conf,) <$> createLdapPool ldapHost ldapPort (poolStripes ldapPool) (poolTimeout ldapPool) ldapTimeout (poolLimit ldapPool)
|
-- let ldapLabel = case ldapHost of
|
||||||
forM_ ldapPool $ registerFailoverMetrics "ldap"
|
-- Ldap.Plain str -> pack str <> ":" <> tshow ldapPort
|
||||||
|
-- Ldap.Tls str _ -> pack str <> ":" <> tshow ldapPort
|
||||||
|
-- $logDebugS "setup" $ "LDAP-Pool " <> ldapLabel
|
||||||
|
-- (ldapLabel,) . (conf,) <$> createLdapPool ldapHost ldapPort poolStripes poolTimeout ldapTimeout poolLimit
|
||||||
|
-- | otherwise
|
||||||
|
-- -> return mempty
|
||||||
|
-- forM_ ldapPool $ registerFailoverMetrics "ldap"
|
||||||
|
|
||||||
|
-- TODO: reintroduce failover once UserDbFailover is implemented (see above)
|
||||||
|
ldapPool <- fmap join . forM appLdapPoolConf $ \ResourcePoolConf{..} -> if
|
||||||
|
| UserAuthConfSingleSource{..} <- appUserAuthConf
|
||||||
|
, AuthSourceConfLdap conf@LdapConf{..} <- userAuthConfSingleSource
|
||||||
|
-> do -- set up a singleton ldap pool with no failover
|
||||||
|
let ldapLabel = case ldapConfHost of
|
||||||
|
Ldap.Plain str -> pack str <> ":" <> tshow ldapConfPort
|
||||||
|
Ldap.Tls str _ -> pack str <> ":" <> tshow ldapConfPort
|
||||||
|
$logDebugS "setup" $ "LDAP-Pool " <> ldapLabel
|
||||||
|
Just . (conf,) <$> createLdapPool ldapConfHost ldapConfPort poolStripes poolTimeout ldapConfTimeout poolLimit
|
||||||
|
| otherwise -- No LDAP pool to be initialized
|
||||||
|
-> return Nothing
|
||||||
|
|
||||||
-- Perform database migration using our application's logging settings.
|
-- Perform database migration using our application's logging settings.
|
||||||
flip runReaderT tempFoundation $
|
flip runReaderT tempFoundation $
|
||||||
@ -320,6 +324,34 @@ makeFoundation appSettings''@AppSettings{..} = do
|
|||||||
appAuthKey <- clusterSetting (Proxy :: Proxy 'ClusterAuthKey) `customRunSqlPool` sqlPool
|
appAuthKey <- clusterSetting (Proxy :: Proxy 'ClusterAuthKey) `customRunSqlPool` sqlPool
|
||||||
appPersonalisedSheetFilesSeedKey <- clusterSetting (Proxy :: Proxy 'ClusterPersonalisedSheetFilesSeedKey) `customRunSqlPool` sqlPool
|
appPersonalisedSheetFilesSeedKey <- clusterSetting (Proxy :: Proxy 'ClusterPersonalisedSheetFilesSeedKey) `customRunSqlPool` sqlPool
|
||||||
|
|
||||||
|
-- TODO: use scopes from Settings
|
||||||
|
#ifdef DEVELOPMENT
|
||||||
|
oauth2Plugins <- liftIO $ sequence
|
||||||
|
[ (azureMockServer . fromJust) <$> lookupEnv "OAUTH2_SERVER_PORT"
|
||||||
|
, return $ oauth2AzureADScoped ["openid", "profile", "offline_access"] "42" "shhh"
|
||||||
|
]
|
||||||
|
#else
|
||||||
|
let -- Auth Plugins
|
||||||
|
-- loadPlugin p prefix = do -- Loads given YesodAuthPlugin
|
||||||
|
-- mID <- fmap Text.pack <$> appUserAuthConf ^? _UserAuthConfSingleSource . _AuthSourceConfAzureAdV2 . _azureConfClientId
|
||||||
|
-- mSecret <- fmap Text.pack <$> appUserAuthConf ^? _UserAuthConfSingleSource . _AuthSourceConfAzureAdV2 . _azureConfClientSecret
|
||||||
|
-- let mArgs = (,) <$> mID <*> mSecret
|
||||||
|
-- guard $ isJust mArgs
|
||||||
|
-- return . uncurry p $ fromJust mArgs
|
||||||
|
-- tenantID = case appUserAuthConf of
|
||||||
|
-- UserAuthConfSingleSource (AuthSourceConfAzureAdV2 AzureConf{..})
|
||||||
|
-- -> tshow azureConfTenantId
|
||||||
|
-- _other
|
||||||
|
-- -> error "Tenant ID missing!"
|
||||||
|
oauth2Plugins
|
||||||
|
| UserAuthConfSingleSource (AuthSourceConfAzureAdV2 AzureConf{..}) <- appUserAuthConf
|
||||||
|
= singleton $ oauth2AzureADScoped (Set.toList azureConfScopes) (tshow azureConfClientId) azureConfClientSecret
|
||||||
|
| otherwise
|
||||||
|
= mempty
|
||||||
|
#endif
|
||||||
|
let appAuthPlugins = oauth2Plugins
|
||||||
|
|
||||||
|
|
||||||
let appVolatileClusterSettingsCacheTime' = Clock.fromNanoSecs ns
|
let appVolatileClusterSettingsCacheTime' = Clock.fromNanoSecs ns
|
||||||
where (MkFixed ns :: Nano) = realToFrac appVolatileClusterSettingsCacheTime
|
where (MkFixed ns :: Nano) = realToFrac appVolatileClusterSettingsCacheTime
|
||||||
appVolatileClusterSettingsCache <- newTVarIO $ mkVolatileClusterSettingsCache appVolatileClusterSettingsCacheTime'
|
appVolatileClusterSettingsCache <- newTVarIO $ mkVolatileClusterSettingsCache appVolatileClusterSettingsCacheTime'
|
||||||
@ -354,15 +386,15 @@ makeFoundation appSettings''@AppSettings{..} = do
|
|||||||
handleIf isBucketExists (const $ return ()) $ Minio.makeBucket appUploadTmpBucket Nothing
|
handleIf isBucketExists (const $ return ()) $ Minio.makeBucket appUploadTmpBucket Nothing
|
||||||
return conn
|
return conn
|
||||||
|
|
||||||
appAvsQuery <- case appAvsConf of
|
appAvsQuery <- case appAvsConf of
|
||||||
Nothing -> do
|
Nothing -> do
|
||||||
$logErrorS "avsPrepare" "appAvsConfig is empty, i.e. invalid AVS configuration settings."
|
$logErrorS "avsPrepare" "appAvsConfig is empty, i.e. invalid AVS configuration settings."
|
||||||
return Nothing
|
return Nothing
|
||||||
-- error "AvsConfig is empty, i.e. invalid AVS configuration settings."
|
-- error "AvsConfig is empty, i.e. invalid AVS configuration settings."
|
||||||
|
|
||||||
Just avsConf -> do
|
Just avsConf -> do
|
||||||
manager <- newManagerSettings $ mkManagerSettings (def { settingDisableCertificateValidation = True }) Nothing
|
manager <- newManagerSettings $ mkManagerSettings (def { settingDisableCertificateValidation = True }) Nothing
|
||||||
let avsServer = BaseUrl
|
let avsServer = BaseUrl
|
||||||
{ baseUrlScheme = Https
|
{ baseUrlScheme = Https
|
||||||
, baseUrlHost = avsHost avsConf
|
, baseUrlHost = avsHost avsConf
|
||||||
, baseUrlPort = avsPort avsConf
|
, baseUrlPort = avsPort avsConf
|
||||||
@ -379,7 +411,8 @@ makeFoundation appSettings''@AppSettings{..} = do
|
|||||||
|
|
||||||
$logDebugS "Runtime configuration" $ tshowCrop appSettings'
|
$logDebugS "Runtime configuration" $ tshowCrop appSettings'
|
||||||
|
|
||||||
let foundation = mkFoundation appSettings' sqlPool smtpPool ldapPool appCryptoIDKey appSessionStore appSecretBoxKey appWidgetMemcached appJSONWebKeySet appClusterID appMemcached appMemcachedLocal appUploadCache appVerpSecret appAuthKey appPersonalisedSheetFilesSeedKey appVolatileClusterSettingsCache appAvsQuery
|
-- TODO: reimplement user db failover
|
||||||
|
let foundation = mkFoundation appSettings' sqlPool smtpPool ldapPool appCryptoIDKey appSessionStore appSecretBoxKey appWidgetMemcached appJSONWebKeySet appClusterID appMemcached appMemcachedLocal appUploadCache appVerpSecret appAuthKey appAuthPlugins appPersonalisedSheetFilesSeedKey appVolatileClusterSettingsCache appAvsQuery
|
||||||
|
|
||||||
-- Return the foundation
|
-- Return the foundation
|
||||||
$logInfoS "setup" "*** DONE ***"
|
$logInfoS "setup" "*** DONE ***"
|
||||||
@ -478,66 +511,6 @@ createMemcached MemcachedConf{memcachedConnectInfo} = snd <$> allocate (Memcache
|
|||||||
makeApplication :: MonadIO m => UniWorX -> m Application
|
makeApplication :: MonadIO m => UniWorX -> m Application
|
||||||
makeApplication foundation = liftIO $ makeMiddleware foundation <*> toWaiAppPlain foundation
|
makeApplication foundation = liftIO $ makeMiddleware foundation <*> toWaiAppPlain foundation
|
||||||
|
|
||||||
makeMiddleware :: MonadIO m => UniWorX -> m Middleware
|
|
||||||
makeMiddleware app = do
|
|
||||||
logWare <- makeLogWare
|
|
||||||
return $ observeHTTPRequestLatency classifyHandler . logWare . normalizeCookies . defaultMiddlewaresNoLogging
|
|
||||||
where
|
|
||||||
makeLogWare = do
|
|
||||||
logWareMap <- liftIO $ newTVarIO HashMap.empty
|
|
||||||
|
|
||||||
let
|
|
||||||
mkLogWare ls@LogSettings{..} = do
|
|
||||||
logger <- readTVarIO . snd $ appLogger app
|
|
||||||
logWare <- mkRequestLogger def
|
|
||||||
{ outputFormat = bool
|
|
||||||
(Apache . bool FromSocket FromHeader $ app ^. _appIpFromHeader)
|
|
||||||
(Detailed True)
|
|
||||||
logDetailed
|
|
||||||
, destination = Logger $ loggerSet logger
|
|
||||||
}
|
|
||||||
atomically . modifyTVar' logWareMap $ HashMap.insert ls logWare
|
|
||||||
return logWare
|
|
||||||
|
|
||||||
void. liftIO $
|
|
||||||
mkLogWare =<< readTVarIO (appLogSettings app)
|
|
||||||
|
|
||||||
return $ \wai req fin -> do
|
|
||||||
lookupRes <- atomically $ do
|
|
||||||
ls <- readTVar $ appLogSettings app
|
|
||||||
existing <- HashMap.lookup ls <$> readTVar logWareMap
|
|
||||||
return $ maybe (Left ls) Right existing
|
|
||||||
logWare <- either mkLogWare return lookupRes
|
|
||||||
logWare wai req fin
|
|
||||||
|
|
||||||
normalizeCookies :: Wai.Middleware
|
|
||||||
normalizeCookies waiApp req respond = waiApp req $ \res -> do
|
|
||||||
resHdrs' <- go $ Wai.responseHeaders res
|
|
||||||
respond $ Wai.mapResponseHeaders (const resHdrs') res
|
|
||||||
where parseSetCookie' :: ByteString -> IO (Maybe SetCookie)
|
|
||||||
parseSetCookie' = fmap (either (\(_ :: SomeException) -> Nothing) Just) . try . evaluate . force . parseSetCookie
|
|
||||||
|
|
||||||
go [] = return []
|
|
||||||
go (hdr@(hdrName, hdrValue) : hdrs)
|
|
||||||
| hdrName == hSetCookie = do
|
|
||||||
mcookieHdr <- parseSetCookie' hdrValue
|
|
||||||
case mcookieHdr of
|
|
||||||
Nothing -> (hdr :) <$> go hdrs
|
|
||||||
Just cookieHdr -> do
|
|
||||||
let cookieHdrMatches hdrValue' = maybeT (return False) $ do
|
|
||||||
cookieHdr' <- MaybeT $ parseSetCookie' hdrValue'
|
|
||||||
-- See https://tools.ietf.org/html/rfc6265
|
|
||||||
guard $ setCookiePath cookieHdr' == setCookiePath cookieHdr
|
|
||||||
guard $ setCookieName cookieHdr' == setCookieName cookieHdr
|
|
||||||
guard $ setCookieDomain cookieHdr' == setCookieDomain cookieHdr
|
|
||||||
return True
|
|
||||||
others <- filterM (\(hdrName', hdrValue') -> and2M (pure $ hdrName' == hSetCookie) (cookieHdrMatches hdrValue')) hdrs
|
|
||||||
if | null others -> (hdr :) <$> go hdrs
|
|
||||||
| otherwise -> go hdrs
|
|
||||||
| otherwise = (hdr :) <$> go hdrs
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
-- | Warp settings for the given foundation value.
|
-- | Warp settings for the given foundation value.
|
||||||
warpSettings :: UniWorX -> Settings
|
warpSettings :: UniWorX -> Settings
|
||||||
warpSettings foundation = defaultSettings
|
warpSettings foundation = defaultSettings
|
||||||
@ -618,6 +591,8 @@ appMain = runResourceT $ do
|
|||||||
foundation <- makeFoundation settings
|
foundation <- makeFoundation settings
|
||||||
|
|
||||||
runAppLoggingT foundation $ do
|
runAppLoggingT foundation $ do
|
||||||
|
$logDebugS "AppSettings" $ tshow settings
|
||||||
|
|
||||||
$logInfoS "setup" "Job-Handling"
|
$logInfoS "setup" "Job-Handling"
|
||||||
handleJobs foundation
|
handleJobs foundation
|
||||||
|
|
||||||
@ -659,7 +634,7 @@ appMain = runResourceT $ do
|
|||||||
notifyWatchdog = forever' Nothing $ \pResults -> do
|
notifyWatchdog = forever' Nothing $ \pResults -> do
|
||||||
let delay = floor $ wInterval % 4
|
let delay = floor $ wInterval % 4
|
||||||
d <- liftIO $ newDelay delay
|
d <- liftIO $ newDelay delay
|
||||||
|
|
||||||
$logDebugS "Notify" $ "Waiting up to " <> tshow delay <> "µs..."
|
$logDebugS "Notify" $ "Waiting up to " <> tshow delay <> "µs..."
|
||||||
mResults <- atomically $ asum
|
mResults <- atomically $ asum
|
||||||
[ pResults <$ waitDelay d
|
[ pResults <$ waitDelay d
|
||||||
@ -734,7 +709,7 @@ shutdownApp app = do
|
|||||||
liftIO $ do
|
liftIO $ do
|
||||||
Custom.purgePool $ appConnPool app
|
Custom.purgePool $ appConnPool app
|
||||||
for_ (appSmtpPool app) destroyAllResources
|
for_ (appSmtpPool app) destroyAllResources
|
||||||
for_ (appLdapPool app) . mapFailover $ views _2 destroyAllResources
|
for_ (appLdapPool app) $ views _2 destroyAllResources
|
||||||
for_ (appWidgetMemcached app) Memcached.close
|
for_ (appWidgetMemcached app) Memcached.close
|
||||||
for_ (appMemcached app) $ views _memcachedConn Memcached.close
|
for_ (appMemcached app) $ views _memcachedConn Memcached.close
|
||||||
release . fst $ appLogger app
|
release . fst $ appLogger app
|
||||||
@ -748,8 +723,8 @@ shutdownApp app = do
|
|||||||
|
|
||||||
-- | Run a handler
|
-- | Run a handler
|
||||||
handler, handler' :: Handler a -> IO a
|
handler, handler' :: Handler a -> IO a
|
||||||
handler h = runResourceT $ getAppDevSettings >>= makeFoundation >>= liftIO . flip unsafeHandler h
|
handler h = runResourceT $ getAppDevSettings >>= makeFoundation >>= liftIO . flip unsafeHandler h
|
||||||
handler' h = runResourceT $ getAppSettings >>= makeFoundation >>= liftIO . flip unsafeHandler h
|
handler' h = runResourceT $ getAppSettings >>= makeFoundation >>= liftIO . flip unsafeHandler h
|
||||||
|
|
||||||
-- | Run DB queries
|
-- | Run DB queries
|
||||||
db, db' :: DB a -> IO a
|
db, db' :: DB a -> IO a
|
||||||
@ -759,7 +734,7 @@ db' = handler' . runDB
|
|||||||
addPWEntry :: User
|
addPWEntry :: User
|
||||||
-> Text {-^ Password -}
|
-> Text {-^ Password -}
|
||||||
-> IO ()
|
-> IO ()
|
||||||
addPWEntry User{ userAuthentication = _, ..} (Text.encodeUtf8 -> pw) = db' $ do
|
addPWEntry User{ userPasswordHash = _, ..} (Text.encodeUtf8 -> pw) = db' $ do
|
||||||
PWHashConf{..} <- getsYesod $ view _appAuthPWHash
|
PWHashConf{..} <- getsYesod $ view _appAuthPWHash
|
||||||
(AuthPWHash . Text.decodeUtf8 -> userAuthentication) <- liftIO $ makePasswordWith pwHashAlgorithm pw pwHashStrength
|
(Just . Text.decodeUtf8 -> userPasswordHash) <- liftIO $ makePasswordWith pwHashAlgorithm pw pwHashStrength
|
||||||
void $ insert User{..}
|
void $ insert User{..}
|
||||||
|
|||||||
36
src/Audit.hs
36
src/Audit.hs
@ -1,9 +1,7 @@
|
|||||||
-- SPDX-FileCopyrightText: 2023-24 Gregor Kleen <gregor.kleen@ifi.lmu.de>,Steffen Jost <s.jost@fraport.de>
|
-- SPDX-FileCopyrightText: 2023 Gregor Kleen <gregor.kleen@ifi.lmu.de>,Steffen Jost <s.jost@fraport.de>
|
||||||
--
|
--
|
||||||
-- SPDX-License-Identifier: AGPL-3.0-or-later
|
-- SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
{-# LANGUAGE TypeApplications #-}
|
|
||||||
|
|
||||||
module Audit
|
module Audit
|
||||||
( module Audit.Types
|
( module Audit.Types
|
||||||
, AuditException(..)
|
, AuditException(..)
|
||||||
@ -11,7 +9,6 @@ module Audit
|
|||||||
, AuditRemoteException(..)
|
, AuditRemoteException(..)
|
||||||
, getRemote
|
, getRemote
|
||||||
, logInterface, logInterface'
|
, logInterface, logInterface'
|
||||||
, reportAdminProblem
|
|
||||||
) where
|
) where
|
||||||
|
|
||||||
|
|
||||||
@ -19,8 +16,6 @@ import Import.NoModel
|
|||||||
import Settings
|
import Settings
|
||||||
import Model
|
import Model
|
||||||
import Database.Persist.Sql
|
import Database.Persist.Sql
|
||||||
import qualified Database.Esqueleto.Experimental as E -- needs TypeApplications Lang-Pragma
|
|
||||||
import qualified Database.Esqueleto.Utils as E
|
|
||||||
import Audit.Types
|
import Audit.Types
|
||||||
|
|
||||||
import qualified Data.Text as Text
|
import qualified Data.Text as Text
|
||||||
@ -133,7 +128,7 @@ logInterface :: ( AuthId (HandlerSite m) ~ Key User
|
|||||||
-> Text -- ^ Any additional information
|
-> Text -- ^ Any additional information
|
||||||
-> ReaderT (YesodPersistBackend (HandlerSite m)) m ()
|
-> ReaderT (YesodPersistBackend (HandlerSite m)) m ()
|
||||||
-- ^ Log a transaction using information available from `HandlerT`, also calls `audit`
|
-- ^ Log a transaction using information available from `HandlerT`, also calls `audit`
|
||||||
logInterface interfaceLogInterface interfaceLogSubtype interfaceLogSuccess interfaceLogRows interfaceLogInfo = do
|
logInterface interfaceLogInterface interfaceLogSubtype interfaceLogSuccess interfaceLogRows interfaceLogInfo = do
|
||||||
interfaceLogWrite <- (methodGet /=) . Wai.requestMethod . reqWaiRequest <$> getRequest
|
interfaceLogWrite <- (methodGet /=) . Wai.requestMethod . reqWaiRequest <$> getRequest
|
||||||
logInterface' interfaceLogInterface interfaceLogSubtype interfaceLogWrite interfaceLogSuccess interfaceLogRows interfaceLogInfo
|
logInterface' interfaceLogInterface interfaceLogSubtype interfaceLogWrite interfaceLogSuccess interfaceLogRows interfaceLogInfo
|
||||||
|
|
||||||
@ -157,7 +152,7 @@ logInterface' :: ( AuthId (HandlerSite m) ~ Key User
|
|||||||
-- ^ Log a transaction using information available from `HandlerT`, also calls `audit`
|
-- ^ Log a transaction using information available from `HandlerT`, also calls `audit`
|
||||||
logInterface' (Text.strip -> interfaceLogInterface) (Text.strip -> interfaceLogSubtype) interfaceLogWrite interfaceLogSuccess interfaceLogRows (Text.strip -> interfaceLogInfo) = do
|
logInterface' (Text.strip -> interfaceLogInterface) (Text.strip -> interfaceLogSubtype) interfaceLogWrite interfaceLogSuccess interfaceLogRows (Text.strip -> interfaceLogInfo) = do
|
||||||
interfaceLogTime <- liftIO getCurrentTime
|
interfaceLogTime <- liftIO getCurrentTime
|
||||||
-- deleteBy $ UniqueInterfaceSubtypeWrite interfaceLogInterface interfaceLogSubtype interfaceLogWrite -- deleteBy & insert would be justified here, leading to a new Row-ID, since the two rows are not truly connected.
|
-- deleteBy $ UniqueInterfaceSubtypeWrite interfaceLogInterface interfaceLogSubtype interfaceLogWrite -- always replace: deleteBy & insert seems to be safest and fastest
|
||||||
-- insert_ InterfaceLog{..}
|
-- insert_ InterfaceLog{..}
|
||||||
void $ upsertBy (UniqueInterfaceSubtypeWrite interfaceLogInterface interfaceLogSubtype interfaceLogWrite)
|
void $ upsertBy (UniqueInterfaceSubtypeWrite interfaceLogInterface interfaceLogSubtype interfaceLogWrite)
|
||||||
( InterfaceLog{..} )
|
( InterfaceLog{..} )
|
||||||
@ -174,28 +169,3 @@ logInterface' (Text.strip -> interfaceLogInterface) (Text.strip -> interfaceLogS
|
|||||||
, transactionInterfaceInfo = interfaceLogInfo
|
, transactionInterfaceInfo = interfaceLogInfo
|
||||||
, transactionInterfaceSuccess = Just interfaceLogSuccess
|
, transactionInterfaceSuccess = Just interfaceLogSuccess
|
||||||
}
|
}
|
||||||
|
|
||||||
reportAdminProblem :: ( IsSqlBackend (YesodPersistBackend (HandlerSite m))
|
|
||||||
, SqlBackendCanWrite (YesodPersistBackend (HandlerSite m))
|
|
||||||
, MonadHandler m
|
|
||||||
-- , HasCallStack
|
|
||||||
)
|
|
||||||
=> AdminProblem -- ^ Problem to record
|
|
||||||
-> ReaderT (YesodPersistBackend (HandlerSite m)) m ()
|
|
||||||
-- ^ Log a problem that needs interventions by admins, provided this problem has not already been reported and is still unsolved
|
|
||||||
--
|
|
||||||
-- - `problemLogTime` is now
|
|
||||||
-- - `problemSolver` is Nothing, we do not record the person who caused it
|
|
||||||
reportAdminProblem problem = do
|
|
||||||
let problemLogSolved = Nothing
|
|
||||||
problemLogSolver = Nothing
|
|
||||||
problemLogInfo = toJSON problem
|
|
||||||
problemLogTime <- liftIO getCurrentTime
|
|
||||||
isKnown <- E.selectExists $ do
|
|
||||||
pl <- E.from $ E.table @ProblemLog
|
|
||||||
E.where_ $ E.isNothing (pl E.^. ProblemLogSolved)
|
|
||||||
E.&&. E.val problemLogInfo E.==. pl E.^. ProblemLogInfo
|
|
||||||
unless isKnown $ insert_ ProblemLog{..}
|
|
||||||
$logWarnS "Problem" $ Text.filter (/= '\n') $ tshow problem -- <> " - " <> pack (prettyCallStack callStack)
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -1,18 +1,15 @@
|
|||||||
-- SPDX-FileCopyrightText: 2022-24 Gregor Kleen <gregor.kleen@ifi.lmu.de>,Sarah Vaupel <vaupel.sarah@campus.lmu.de>,Steffen Jost <s.jost@fraport.de>
|
-- SPDX-FileCopyrightText: 2022-23 Gregor Kleen <gregor.kleen@ifi.lmu.de>,Sarah Vaupel <vaupel.sarah@campus.lmu.de>,Steffen Jost <s.jost@fraport.de>
|
||||||
--
|
--
|
||||||
-- SPDX-License-Identifier: AGPL-3.0-or-later
|
-- SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
module Audit.Types
|
module Audit.Types
|
||||||
( Transaction(..)
|
( Transaction(..)
|
||||||
, AdminProblem(..)
|
|
||||||
, decodeAdminProblem
|
|
||||||
) where
|
) where
|
||||||
|
|
||||||
import ClassyPrelude.Yesod hiding (derivePersistFieldJSON)
|
import ClassyPrelude.Yesod hiding (derivePersistFieldJSON)
|
||||||
import Model.Types.TH.JSON
|
import Model.Types.TH.JSON
|
||||||
import Model
|
import Model
|
||||||
|
|
||||||
import Data.Aeson
|
|
||||||
import Data.Aeson.TH
|
import Data.Aeson.TH
|
||||||
import Utils.PathPiece
|
import Utils.PathPiece
|
||||||
|
|
||||||
@ -185,7 +182,7 @@ data Transaction
|
|||||||
}
|
}
|
||||||
| TransactionLmsStart
|
| TransactionLmsStart
|
||||||
{ transactionQualification :: QualificationId
|
{ transactionQualification :: QualificationId
|
||||||
, transactionLmsIdent :: LmsIdent
|
, transactionLmsIdent :: LmsIdent
|
||||||
, transactionLmsUser :: UserId
|
, transactionLmsUser :: UserId
|
||||||
, transactionLmsUserKey :: LmsUserId
|
, transactionLmsUserKey :: LmsUserId
|
||||||
}
|
}
|
||||||
@ -216,7 +213,7 @@ data Transaction
|
|||||||
| TransactionQualificationUserEdit -- Note that a renewal always entails unblocking as well!
|
| TransactionQualificationUserEdit -- Note that a renewal always entails unblocking as well!
|
||||||
{ transactionUser :: UserId -- qualification holder that is updated
|
{ transactionUser :: UserId -- qualification holder that is updated
|
||||||
, transactionQualificationUser :: QualificationUserId -- not really necessary, maybe remove?
|
, transactionQualificationUser :: QualificationUserId -- not really necessary, maybe remove?
|
||||||
, transactionQualification :: QualificationId
|
, transactionQualification :: QualificationId
|
||||||
, transactionQualificationValidUntil :: Day
|
, transactionQualificationValidUntil :: Day
|
||||||
, transactionQualificationScheduleRenewal :: Maybe Bool -- Maybe, because some update may leave it unchanged (also avoids DB Migration)
|
, transactionQualificationScheduleRenewal :: Maybe Bool -- Maybe, because some update may leave it unchanged (also avoids DB Migration)
|
||||||
, transactionNote :: Maybe Text
|
, transactionNote :: Maybe Text
|
||||||
@ -254,63 +251,4 @@ deriveJSON defaultOptions
|
|||||||
, sumEncoding = TaggedObject "transaction" "data"
|
, sumEncoding = TaggedObject "transaction" "data"
|
||||||
} ''Transaction
|
} ''Transaction
|
||||||
|
|
||||||
derivePersistFieldJSON ''Transaction
|
derivePersistFieldJSON ''Transaction
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
-- Datatype for raising admin awareness to certain problems
|
|
||||||
-- Database stores generic Value in table ProblemLog, such that changes do not disturb old entries
|
|
||||||
-- Note that there is no RenderMessage instance, instead see @Handler.Admin.adminProblemCell dealing with special cases instead
|
|
||||||
-- Note: Adjust MsgAdminProblemInfoTooltip as well
|
|
||||||
data AdminProblem
|
|
||||||
= AdminProblemNewCompany -- new company was noticed, presumably without supervisors
|
|
||||||
{ adminProblemCompany :: CompanyId
|
|
||||||
}
|
|
||||||
| AdminProblemSupervisorNewCompany
|
|
||||||
{ adminProblemUser :: UserId -- a default supervisor has changed company
|
|
||||||
, adminProblemCompany :: CompanyId -- old company where the user had default supervisor rights
|
|
||||||
, adminProblemCompanyNew :: CompanyId -- new company of the user
|
|
||||||
, adminProblemSupervisorReroute :: Bool -- reroute included?
|
|
||||||
}
|
|
||||||
| AdminProblemSupervisorLeftCompany
|
|
||||||
{ adminProblemUser :: UserId -- user who had a supervisor but no longer has, due to supervisor change
|
|
||||||
, adminProblemCompany :: CompanyId -- old company
|
|
||||||
, adminProblemSupervisorReroute :: Bool -- reroute included?
|
|
||||||
}
|
|
||||||
| AdminProblemCompanySuperiorChange -- a company received a new superior user through AVS
|
|
||||||
{ adminProblemUser :: UserId -- new superior user
|
|
||||||
, adminProblemCompany :: CompanyId -- affected company
|
|
||||||
, adminProblemUserOld :: Maybe UserId -- previous superior
|
|
||||||
}
|
|
||||||
| AdminProblemCompanySuperiorNotFound -- a company received a new superior user through AVS, but user could not be created from email
|
|
||||||
{ adminProblemEmail :: Maybe Text -- new superior user's email, not found in LDAP
|
|
||||||
, adminProblemCompany :: CompanyId -- affected company
|
|
||||||
, adminProblemUserOld :: Maybe UserId -- previous superior
|
|
||||||
}
|
|
||||||
| AdminProblemNewlyUnsupervised
|
|
||||||
{ adminProblemUser :: UserId -- user who had a supervisor but no longer has, due to user company change
|
|
||||||
, adminProblemCompanyOld :: Maybe CompanyId -- old company
|
|
||||||
, adminProblemCompanyNew :: CompanyId -- new company of the user
|
|
||||||
}
|
|
||||||
| AdminProblemUnknown -- miscellanous problem, just displaying text
|
|
||||||
{ adminProblemText :: Text
|
|
||||||
}
|
|
||||||
deriving (Eq, Ord, Read, Show, Generic)
|
|
||||||
|
|
||||||
-- Columns shown in problem table: adminProblemCompany, adminProblemUser
|
|
||||||
-- For display: add clause to Handler.Admin.adminProblemCell
|
|
||||||
|
|
||||||
deriveJSON defaultOptions
|
|
||||||
{ constructorTagModifier = camelToPathPiece' 2
|
|
||||||
, fieldLabelModifier = camelToPathPiece' 2
|
|
||||||
, tagSingleConstructors = True
|
|
||||||
, sumEncoding = TaggedObject "problem" "data"
|
|
||||||
, rejectUnknownFields = False
|
|
||||||
} ''AdminProblem
|
|
||||||
|
|
||||||
derivePersistFieldJSON ''AdminProblem
|
|
||||||
|
|
||||||
decodeAdminProblem :: Value -> AdminProblem
|
|
||||||
decodeAdminProblem v = case fromJSON v of
|
|
||||||
Error msg -> AdminProblemUnknown $ pack msg
|
|
||||||
Success p -> p
|
|
||||||
@ -34,7 +34,7 @@ dummyForm = do
|
|||||||
mr <- getMessageRender
|
mr <- getMessageRender
|
||||||
wreq (ciField & addDatalist userList) (fslpI MsgDummyIdent (mr MsgDummyIdentPlaceholder) & addAttr "autocomplete" "username" & addName PostLoginDummy) Nothing
|
wreq (ciField & addDatalist userList) (fslpI MsgDummyIdent (mr MsgDummyIdentPlaceholder) & addAttr "autocomplete" "username" & addName PostLoginDummy) Nothing
|
||||||
where
|
where
|
||||||
userList = fmap mkOptionList . runDB $ withReaderT projectBackend (map toOption <$> selectList [UserId <=. UserKey 12] [Asc UserIdent] :: ReaderT SqlBackend _ [Option UserIdent])
|
userList = fmap mkOptionList . runDB $ withReaderT projectBackend (map toOption <$> selectList [] [Asc UserIdent] :: ReaderT SqlBackend _ [Option UserIdent])
|
||||||
toOption (Entity _ User{..}) = Option userDisplayName userIdent (CI.original userIdent)
|
toOption (Entity _ User{..}) = Option userDisplayName userIdent (CI.original userIdent)
|
||||||
|
|
||||||
apDummy :: Text
|
apDummy :: Text
|
||||||
|
|||||||
242
src/Auth/LDAP.hs
242
src/Auth/LDAP.hs
@ -1,4 +1,4 @@
|
|||||||
-- SPDX-FileCopyrightText: 2022 Felix Hamann <felix.hamann@campus.lmu.de>,Gregor Kleen <gregor.kleen@ifi.lmu.de>,Sarah Vaupel <sarah.vaupel@ifi.lmu.de>,Steffen Jost <jost@cip.ifi.lmu.de>,Steffen Jost <jost@tcs.ifi.lmu.de>
|
-- SPDX-FileCopyrightText: 2022-2024 Sarah Vaupel <sarah.vaupel@uniworx.de>, Felix Hamann <felix.hamann@campus.lmu.de>, Gregor Kleen <gregor.kleen@ifi.lmu.de>, Sarah Vaupel <sarah.vaupel@ifi.lmu.de>, Steffen Jost <jost@cip.ifi.lmu.de>, Steffen Jost <jost@tcs.ifi.lmu.de>
|
||||||
--
|
--
|
||||||
-- SPDX-License-Identifier: AGPL-3.0-or-later
|
-- SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
@ -7,11 +7,11 @@
|
|||||||
module Auth.LDAP
|
module Auth.LDAP
|
||||||
( apLdap
|
( apLdap
|
||||||
, ADError(..), ADInvalidCredentials(..)
|
, ADError(..), ADInvalidCredentials(..)
|
||||||
, campusLogin
|
, ldapLogin
|
||||||
, CampusUserException(..)
|
, LdapUserException(..)
|
||||||
, campusUser, campusUser', campusUser''
|
, ldapUser, ldapUser', ldapUser''
|
||||||
, campusUserReTest, campusUserReTest'
|
--, ldapUserReTest, ldapUserReTest'
|
||||||
, campusUserMatr, campusUserMatr'
|
, ldapUserMatr, ldapUserMatr'
|
||||||
, CampusMessage(..)
|
, CampusMessage(..)
|
||||||
, ldapPrimaryKey
|
, ldapPrimaryKey
|
||||||
, ldapUserPrincipalName, ldapUserEmail, ldapUserDisplayName
|
, ldapUserPrincipalName, ldapUserEmail, ldapUserDisplayName
|
||||||
@ -20,32 +20,36 @@ module Auth.LDAP
|
|||||||
, ldapUserMobile, ldapUserTelephone
|
, ldapUserMobile, ldapUserTelephone
|
||||||
, ldapUserFraportPersonalnummer, ldapUserFraportAbteilung
|
, ldapUserFraportPersonalnummer, ldapUserFraportAbteilung
|
||||||
, ldapUserTitle
|
, ldapUserTitle
|
||||||
|
, ldapSearch
|
||||||
) where
|
) where
|
||||||
|
|
||||||
import Import.NoFoundation
|
import Import.NoFoundation
|
||||||
|
|
||||||
import qualified Data.CaseInsensitive as CI
|
import Auth.LDAP.AD
|
||||||
|
|
||||||
import Utils.Metrics
|
|
||||||
import Utils.Form
|
|
||||||
|
|
||||||
import qualified Ldap.Client as Ldap
|
import qualified Ldap.Client as Ldap
|
||||||
|
|
||||||
|
import Utils.Form
|
||||||
|
import Utils.Metrics
|
||||||
|
|
||||||
|
import qualified Data.CaseInsensitive as CI
|
||||||
import qualified Data.Text.Encoding as Text
|
import qualified Data.Text.Encoding as Text
|
||||||
|
|
||||||
import qualified Yesod.Auth.Message as Msg
|
import qualified Yesod.Auth.Message as Msg
|
||||||
|
|
||||||
import Auth.LDAP.AD
|
|
||||||
|
|
||||||
-- allow Ldap.Attr usage as key for Data.Map
|
-- | Plugin name of the LDAP yesod auth plugin
|
||||||
deriving newtype instance Ord Ldap.Attr
|
apLdap :: Text
|
||||||
|
apLdap = "LDAP"
|
||||||
|
|
||||||
|
|
||||||
|
-- TODO: rename
|
||||||
data CampusLogin = CampusLogin
|
data CampusLogin = CampusLogin
|
||||||
{ campusIdent :: CI Text
|
{ campusIdent :: CI Text
|
||||||
, campusPassword :: Text
|
, campusPassword :: Text
|
||||||
} deriving (Generic)
|
} deriving (Generic)
|
||||||
|
|
||||||
|
-- TODO: rename
|
||||||
data CampusMessage = MsgCampusIdentPlaceholder
|
data CampusMessage = MsgCampusIdentPlaceholder
|
||||||
| MsgCampusIdent
|
| MsgCampusIdent
|
||||||
| MsgCampusPassword
|
| MsgCampusPassword
|
||||||
@ -53,8 +57,12 @@ data CampusMessage = MsgCampusIdentPlaceholder
|
|||||||
deriving (Eq, Ord, Enum, Bounded, Read, Show, Generic)
|
deriving (Eq, Ord, Enum, Bounded, Read, Show, Generic)
|
||||||
|
|
||||||
|
|
||||||
findUser :: LdapConf -> Ldap -> Text -> [Ldap.Attr] -> IO [Ldap.SearchEntry]
|
findUser :: LdapConf
|
||||||
findUser conf@LdapConf{..} ldap ident retAttrs = fromMaybe [] <$> findM (assertM (not . null) . lift . flip (Ldap.search ldap ldapBase $ userSearchSettings conf) retAttrs) userFilters
|
-> Ldap
|
||||||
|
-> Text -- ^ needle
|
||||||
|
-> [Ldap.Attr]
|
||||||
|
-> IO [Ldap.SearchEntry]
|
||||||
|
findUser conf@LdapConf{..} ldap ident retAttrs = fromMaybe [] <$> findM (assertM (not . null) . lift . flip (Ldap.search ldap ldapConfBase $ userSearchSettings conf) retAttrs) userFilters
|
||||||
where
|
where
|
||||||
userFilters =
|
userFilters =
|
||||||
[ ldapUserPrincipalName Ldap.:= Text.encodeUtf8 ident
|
[ ldapUserPrincipalName Ldap.:= Text.encodeUtf8 ident
|
||||||
@ -69,21 +77,37 @@ findUser conf@LdapConf{..} ldap ident retAttrs = fromMaybe [] <$> findM (assertM
|
|||||||
[ ldapUserFraportPersonalnummer Ldap.:= Text.encodeUtf8 ident
|
[ ldapUserFraportPersonalnummer Ldap.:= Text.encodeUtf8 ident
|
||||||
]
|
]
|
||||||
|
|
||||||
findUserMatr :: LdapConf -> Ldap -> Text -> [Ldap.Attr] -> IO [Ldap.SearchEntry]
|
findUserMatr :: LdapConf
|
||||||
findUserMatr conf@LdapConf{..} ldap userMatr retAttrs = fromMaybe [] <$> findM (assertM (not . null) . lift . flip (Ldap.search ldap ldapBase $ userSearchSettings conf) retAttrs) userFilters
|
-> Ldap
|
||||||
|
-> Text -- ^ matriculation needle
|
||||||
|
-> [Ldap.Attr]
|
||||||
|
-> IO [Ldap.SearchEntry]
|
||||||
|
findUserMatr conf@LdapConf{..} ldap userMatr retAttrs = fromMaybe [] <$> findM (assertM (not . null) . lift . flip (Ldap.search ldap ldapConfBase $ userSearchSettings conf) retAttrs) userFilters
|
||||||
where
|
where
|
||||||
userFilters =
|
userFilters =
|
||||||
[ ldapUserFraportPersonalnummer Ldap.:= Text.encodeUtf8 userMatr
|
[ ldapUserFraportPersonalnummer Ldap.:= Text.encodeUtf8 userMatr
|
||||||
]
|
]
|
||||||
|
|
||||||
userSearchSettings :: LdapConf -> Ldap.Mod Ldap.Search
|
userSearchSettings :: LdapConf
|
||||||
|
-> Ldap.Mod Ldap.Search
|
||||||
userSearchSettings LdapConf{..} = mconcat
|
userSearchSettings LdapConf{..} = mconcat
|
||||||
[ Ldap.scope ldapScope
|
[ Ldap.scope ldapConfScope
|
||||||
, Ldap.size 2
|
, Ldap.size 2
|
||||||
, Ldap.time ldapSearchTimeout
|
, Ldap.time ldapConfSearchTimeout
|
||||||
, Ldap.derefAliases Ldap.DerefAlways
|
, Ldap.derefAliases Ldap.DerefAlways
|
||||||
]
|
]
|
||||||
|
|
||||||
|
ldapSearch :: forall m.
|
||||||
|
( MonadUnliftIO m
|
||||||
|
, MonadCatch m
|
||||||
|
)
|
||||||
|
=> (LdapConf, LdapPool)
|
||||||
|
-> Text -- ^ needle
|
||||||
|
-> m [Ldap.SearchEntry]
|
||||||
|
ldapSearch (conf@LdapConf{..}, ldapPool) needle = either (throwM . LdapUserLdapError) return <=< withLdap ldapPool $ \ldap -> liftIO $ do
|
||||||
|
Ldap.bind ldap ldapConfDn ldapConfPassword
|
||||||
|
findUser conf ldap needle []
|
||||||
|
|
||||||
ldapPrimaryKey, ldapUserPrincipalName, ldapUserDisplayName, ldapUserFirstName, ldapUserSurname, ldapAffiliation, ldapUserTitle, ldapUserTelephone, ldapUserMobile, ldapUserFraportPersonalnummer, ldapUserFraportAbteilung :: Ldap.Attr
|
ldapPrimaryKey, ldapUserPrincipalName, ldapUserDisplayName, ldapUserFirstName, ldapUserSurname, ldapAffiliation, ldapUserTitle, ldapUserTelephone, ldapUserMobile, ldapUserFraportPersonalnummer, ldapUserFraportAbteilung :: Ldap.Attr
|
||||||
ldapPrimaryKey = Ldap.Attr "cn" -- should always be identical to "sAMAccountName"
|
ldapPrimaryKey = Ldap.Attr "cn" -- should always be identical to "sAMAccountName"
|
||||||
ldapUserPrincipalName = Ldap.Attr "userPrincipalName"
|
ldapUserPrincipalName = Ldap.Attr "userPrincipalName"
|
||||||
@ -104,30 +128,35 @@ ldapUserEmail = Ldap.Attr "mail" :|
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
data CampusUserException = CampusUserLdapError LdapPoolError
|
-- TODO: deprecate in favour of FetchUserDataException
|
||||||
| CampusUserNoResult
|
data LdapUserException = LdapUserLdapError LdapPoolError
|
||||||
| CampusUserAmbiguous
|
| LdapUserNoResult
|
||||||
|
| LdapUserAmbiguous
|
||||||
deriving (Show, Eq, Generic)
|
deriving (Show, Eq, Generic)
|
||||||
|
|
||||||
instance Exception CampusUserException
|
instance Exception LdapUserException
|
||||||
|
|
||||||
makePrisms ''CampusUserException
|
makePrisms ''LdapUserException
|
||||||
|
|
||||||
campusUserWith :: ( MonadUnliftIO m
|
|
||||||
, MonadCatch m
|
ldapUserWith :: ( MonadUnliftIO m
|
||||||
)
|
, MonadCatch m
|
||||||
=> ( Lens (LdapConf, LdapPool) (LdapConf, Ldap) LdapPool Ldap
|
--, MonadLogger m
|
||||||
-> Failover (LdapConf, LdapPool)
|
)
|
||||||
-> FailoverMode
|
-- ( Lens (LdapConf, LdapPool) (LdapConf, Ldap) LdapPool Ldap
|
||||||
-> ((LdapConf, Ldap) -> m (Either CampusUserException (Ldap.AttrList [])))
|
-- -> (LdapConf, LdapPool)
|
||||||
-> m (Either LdapPoolError (Either CampusUserException (Ldap.AttrList [])))
|
-- -> ((LdapConf, Ldap) -> m (Either CampusUserException (Ldap.AttrList [])))
|
||||||
)
|
-- -> m (Either LdapPoolError (Either CampusUserException (Ldap.AttrList [])))
|
||||||
-> Failover (LdapConf, LdapPool)
|
-- )
|
||||||
-> FailoverMode
|
=> ( LdapPool
|
||||||
-> Creds site
|
-> (Ldap -> m (Either LdapUserException (Ldap.AttrList [])))
|
||||||
-> m (Either CampusUserException (Ldap.AttrList []))
|
-> m (Either LdapPoolError (Either LdapUserException (Ldap.AttrList [])))
|
||||||
campusUserWith withLdap' pool mode Creds{..} = either (throwM . CampusUserLdapError) return <=< withLdap' _2 pool mode $ \(conf@LdapConf{..}, ldap) -> liftIO . runExceptT $ do
|
)
|
||||||
lift $ Ldap.bind ldap ldapDn ldapPassword
|
-> (LdapConf, LdapPool)
|
||||||
|
-> Creds site
|
||||||
|
-> m (Either LdapUserException (Ldap.AttrList []))
|
||||||
|
ldapUserWith withLdap' (conf@LdapConf{..}, pool) Creds{..} = either (throwM . LdapUserLdapError) return <=< withLdap' pool $ \ldap -> liftIO . runExceptT $ do
|
||||||
|
lift $ Ldap.bind ldap ldapConfDn ldapConfPassword
|
||||||
results <- case lookup "DN" credsExtra of
|
results <- case lookup "DN" credsExtra of
|
||||||
Just userDN -> do
|
Just userDN -> do
|
||||||
let userFilter = Ldap.Present ldapUserPrincipalName
|
let userFilter = Ldap.Present ldapUserPrincipalName
|
||||||
@ -135,43 +164,91 @@ campusUserWith withLdap' pool mode Creds{..} = either (throwM . CampusUserLdapEr
|
|||||||
Nothing -> do
|
Nothing -> do
|
||||||
lift $ findUser conf ldap credsIdent []
|
lift $ findUser conf ldap credsIdent []
|
||||||
case results of
|
case results of
|
||||||
[] -> throwE CampusUserNoResult
|
[] -> throwE LdapUserNoResult
|
||||||
[Ldap.SearchEntry _ attrs] -> return attrs
|
[Ldap.SearchEntry _ attrs] -> return attrs
|
||||||
_otherwise -> throwE CampusUserAmbiguous
|
_otherwise -> throwE LdapUserAmbiguous
|
||||||
|
|
||||||
campusUserReTest :: (MonadUnliftIO m, MonadMask m, MonadLogger m) => Failover (LdapConf, LdapPool) -> (Nano -> Bool) -> FailoverMode -> Creds site -> m (Ldap.AttrList [])
|
|
||||||
campusUserReTest pool doTest mode creds = throwLeft =<< campusUserWith (\l -> flip (withLdapFailoverReTest l) doTest) pool mode creds
|
|
||||||
|
|
||||||
campusUserReTest' :: (MonadMask m, MonadLogger m, MonadUnliftIO m) => Failover (LdapConf, LdapPool) -> (Nano -> Bool) -> FailoverMode -> User -> m (Maybe (Ldap.AttrList []))
|
|
||||||
campusUserReTest' pool doTest mode User{userIdent,userLdapPrimaryKey}
|
|
||||||
= runMaybeT . catchIfMaybeT (is _CampusUserNoResult) $ campusUserReTest pool doTest mode (Creds apLdap upsertIdent [])
|
|
||||||
where upsertIdent = fromMaybe (CI.original userIdent) userLdapPrimaryKey
|
|
||||||
|
|
||||||
|
|
||||||
campusUser :: (MonadMask m, MonadUnliftIO m, MonadLogger m) => Failover (LdapConf, LdapPool) -> FailoverMode -> Creds site -> m (Ldap.AttrList [])
|
-- TODO: reintroduce once failover has been reimplemented
|
||||||
campusUser pool mode creds = throwLeft =<< campusUserWith withLdapFailover pool mode creds
|
-- ldapUserReTest :: ( MonadUnliftIO m
|
||||||
|
-- , MonadMask m
|
||||||
|
-- , MonadLogger m
|
||||||
|
-- )
|
||||||
|
-- => Failover (LdapConf, LdapPool)
|
||||||
|
-- -> (Nano -> Bool)
|
||||||
|
-- -> FailoverMode
|
||||||
|
-- -> Creds site
|
||||||
|
-- -> m (Ldap.AttrList [])
|
||||||
|
-- ldapUserReTest pool doTest mode creds = throwLeft =<< ldapUserWith (\l -> flip (withLdapFailoverReTest l) doTest) pool mode creds
|
||||||
|
--
|
||||||
|
-- ldapUserReTest' :: ( MonadMask m
|
||||||
|
-- , MonadLogger m
|
||||||
|
-- , MonadUnliftIO m
|
||||||
|
-- )
|
||||||
|
-- => Failover (LdapConf, LdapPool)
|
||||||
|
-- -> (Nano -> Bool)
|
||||||
|
-- -> FailoverMode
|
||||||
|
-- -> User
|
||||||
|
-- -> m (Maybe (Ldap.AttrList []))
|
||||||
|
-- ldapUserReTest' pool doTest mode User{userIdent,userLdapPrimaryKey}
|
||||||
|
-- = runMaybeT . catchIfMaybeT (is _CampusUserNoResult) $ ldapUserReTest pool doTest mode (Creds apLdap upsertIdent [])
|
||||||
|
-- where upsertIdent = fromMaybe (CI.original userIdent) userLdapPrimaryKey
|
||||||
|
|
||||||
campusUser' :: (MonadMask m, MonadUnliftIO m, MonadLogger m) => Failover (LdapConf, LdapPool) -> FailoverMode -> User -> m (Maybe (Ldap.AttrList []))
|
|
||||||
campusUser' pool mode User{userIdent}
|
|
||||||
= campusUser'' pool mode $ CI.original userIdent
|
|
||||||
|
|
||||||
campusUser'' :: (MonadMask m, MonadUnliftIO m, MonadLogger m) => Failover (LdapConf, LdapPool) -> FailoverMode -> Text -> m (Maybe (Ldap.AttrList []))
|
-- TODO: deprecate in favour of fetchUserData
|
||||||
campusUser'' pool mode ident
|
ldapUser :: ( MonadMask m
|
||||||
= runMaybeT . catchIfMaybeT (is _CampusUserNoResult) $ campusUser pool mode (Creds apLdap ident [])
|
, MonadUnliftIO m
|
||||||
|
--, MonadLogger m
|
||||||
|
)
|
||||||
|
=> (LdapConf, LdapPool)
|
||||||
|
-> Creds site
|
||||||
|
-> m (Ldap.AttrList [])
|
||||||
|
ldapUser pool creds = throwLeft =<< ldapUserWith withLdap pool creds
|
||||||
|
|
||||||
campusUserMatr :: (MonadUnliftIO m, MonadMask m, MonadLogger m) => Failover (LdapConf, LdapPool) -> FailoverMode -> UserMatriculation -> m (Ldap.AttrList [])
|
ldapUser' :: ( MonadMask m
|
||||||
campusUserMatr pool mode userMatr = either (throwM . CampusUserLdapError) return <=< withLdapFailover _2 pool mode $ \(conf@LdapConf{..}, ldap) -> liftIO $ do
|
, MonadUnliftIO m
|
||||||
Ldap.bind ldap ldapDn ldapPassword
|
--, MonadLogger m
|
||||||
|
)
|
||||||
|
=> (LdapConf, LdapPool)
|
||||||
|
-> User
|
||||||
|
-> m (Maybe (Ldap.AttrList []))
|
||||||
|
ldapUser' pool User{userIdent}
|
||||||
|
= ldapUser'' pool $ CI.original userIdent
|
||||||
|
|
||||||
|
ldapUser'' :: ( MonadMask m
|
||||||
|
, MonadUnliftIO m
|
||||||
|
--, MonadLogger m
|
||||||
|
)
|
||||||
|
=> (LdapConf, LdapPool)
|
||||||
|
-> Text
|
||||||
|
-> m (Maybe (Ldap.AttrList []))
|
||||||
|
ldapUser'' pool ident
|
||||||
|
= runMaybeT . catchIfMaybeT (is _LdapUserNoResult) $ ldapUser pool (Creds apLdap ident [])
|
||||||
|
|
||||||
|
|
||||||
|
ldapUserMatr :: ( MonadUnliftIO m
|
||||||
|
, MonadMask m
|
||||||
|
--, MonadLogger m
|
||||||
|
)
|
||||||
|
=> (LdapConf, LdapPool)
|
||||||
|
-> UserMatriculation
|
||||||
|
-> m (Ldap.AttrList [])
|
||||||
|
ldapUserMatr (conf@LdapConf{..}, pool) userMatr = either (throwM . LdapUserLdapError) return <=< withLdap pool $ \ldap -> liftIO $ do
|
||||||
|
Ldap.bind ldap ldapConfDn ldapConfPassword
|
||||||
results <- findUserMatr conf ldap userMatr []
|
results <- findUserMatr conf ldap userMatr []
|
||||||
case results of
|
case results of
|
||||||
[] -> throwM CampusUserNoResult
|
[] -> throwM LdapUserNoResult
|
||||||
[Ldap.SearchEntry _ attrs] -> return attrs
|
[Ldap.SearchEntry _ attrs] -> return attrs
|
||||||
_otherwise -> throwM CampusUserAmbiguous
|
_otherwise -> throwM LdapUserAmbiguous
|
||||||
|
|
||||||
campusUserMatr' :: (MonadMask m, MonadUnliftIO m, MonadLogger m) => Failover (LdapConf, LdapPool) -> FailoverMode -> UserMatriculation -> m (Maybe (Ldap.AttrList []))
|
|
||||||
campusUserMatr' pool mode
|
|
||||||
= runMaybeT . catchIfMaybeT (is _CampusUserNoResult) . campusUserMatr pool mode
|
|
||||||
|
|
||||||
|
ldapUserMatr' :: ( MonadMask m
|
||||||
|
, MonadUnliftIO m
|
||||||
|
--, MonadLogger m
|
||||||
|
)
|
||||||
|
=> (LdapConf, LdapPool)
|
||||||
|
-> UserMatriculation
|
||||||
|
-> m (Maybe (Ldap.AttrList []))
|
||||||
|
ldapUserMatr' pool = runMaybeT . catchIfMaybeT (is _LdapUserNoResult) . ldapUserMatr pool
|
||||||
|
|
||||||
|
|
||||||
newtype ADInvalidCredentials = ADInvalidCredentials ADError
|
newtype ADInvalidCredentials = ADInvalidCredentials ADError
|
||||||
@ -186,25 +263,28 @@ campusForm :: ( RenderMessage (HandlerSite m) FormMessage
|
|||||||
, RenderMessage (HandlerSite m) (ValueRequired (HandlerSite m))
|
, RenderMessage (HandlerSite m) (ValueRequired (HandlerSite m))
|
||||||
, RenderMessage (HandlerSite m) CampusMessage
|
, RenderMessage (HandlerSite m) CampusMessage
|
||||||
, MonadHandler m
|
, MonadHandler m
|
||||||
) => WForm m (FormResult CampusLogin)
|
)
|
||||||
|
=> WForm m (FormResult CampusLogin)
|
||||||
campusForm = do
|
campusForm = do
|
||||||
MsgRenderer mr <- getMsgRenderer
|
MsgRenderer mr <- getMsgRenderer
|
||||||
aFormToWForm $ CampusLogin
|
aFormToWForm $ CampusLogin
|
||||||
<$> areq ciField (fslpI MsgCampusIdent (mr MsgCampusIdentPlaceholder) & addAttr "autofocus" "" & addAttr "autocomplete" "username") Nothing
|
<$> areq ciField (fslpI MsgCampusIdent (mr MsgCampusIdentPlaceholder) & addAttr "autofocus" "" & addAttr "autocomplete" "username") Nothing
|
||||||
<*> areq passwordField (fslpI MsgCampusPassword (mr MsgCampusPasswordPlaceholder) & addAttr "autocomplete" "current-password") Nothing
|
<*> areq passwordField (fslpI MsgCampusPassword (mr MsgCampusPasswordPlaceholder) & addAttr "autocomplete" "current-password") Nothing
|
||||||
|
|
||||||
apLdap :: Text
|
|
||||||
apLdap = "LDAP"
|
|
||||||
|
|
||||||
campusLogin :: forall site.
|
-- TODO: reintroduce Failover
|
||||||
( YesodAuth site
|
ldapLogin :: forall site.
|
||||||
, RenderMessage site CampusMessage
|
( YesodAuth site
|
||||||
, RenderAFormSite site
|
, RenderMessage site CampusMessage
|
||||||
, RenderMessage site (ValueRequired site)
|
, RenderAFormSite site
|
||||||
, RenderMessage site ADInvalidCredentials
|
, RenderMessage site (ValueRequired site)
|
||||||
, Button site ButtonSubmit
|
, RenderMessage site ADInvalidCredentials
|
||||||
) => Failover (LdapConf, LdapPool) -> FailoverMode -> AuthPlugin site
|
, Button site ButtonSubmit
|
||||||
campusLogin pool mode = AuthPlugin{..}
|
)
|
||||||
|
=> LdapConf
|
||||||
|
-> LdapPool
|
||||||
|
-> AuthPlugin site
|
||||||
|
ldapLogin conf@LdapConf{..} pool = AuthPlugin{..}
|
||||||
where
|
where
|
||||||
apName :: Text
|
apName :: Text
|
||||||
apName = apLdap
|
apName = apLdap
|
||||||
@ -215,8 +295,8 @@ campusLogin pool mode = AuthPlugin{..}
|
|||||||
tp <- getRouteToParent
|
tp <- getRouteToParent
|
||||||
|
|
||||||
resp <- formResultMaybe loginRes $ \CampusLogin{ campusIdent = CI.original -> campusIdent, ..} -> Just <$> do
|
resp <- formResultMaybe loginRes $ \CampusLogin{ campusIdent = CI.original -> campusIdent, ..} -> Just <$> do
|
||||||
ldapResult <- withLdapFailover _2 pool mode $ \(conf@LdapConf{..}, ldap) -> liftIO $ do
|
ldapResult <- withLdap pool $ \ldap -> liftIO $ do
|
||||||
Ldap.bind ldap ldapDn ldapPassword
|
Ldap.bind ldap ldapConfDn ldapConfPassword
|
||||||
searchResults <- findUser conf ldap campusIdent [ldapUserPrincipalName]
|
searchResults <- findUser conf ldap campusIdent [ldapUserPrincipalName]
|
||||||
case searchResults of
|
case searchResults of
|
||||||
[Ldap.SearchEntry (Ldap.Dn userDN) userAttrs]
|
[Ldap.SearchEntry (Ldap.Dn userDN) userAttrs]
|
||||||
|
|||||||
248
src/Auth/OAuth2.hs
Normal file
248
src/Auth/OAuth2.hs
Normal file
@ -0,0 +1,248 @@
|
|||||||
|
-- SPDX-FileCopyrightText: 2023-2024 Sarah Vaupel <sarah.vaupel@uniworx.de>, David Mosbach <david.mosbach@uniworx.de>
|
||||||
|
--
|
||||||
|
-- SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
|
{-# OPTIONS_GHC -fno-warn-orphans #-}
|
||||||
|
{-# OPTIONS_GHC -fno-warn-redundant-constraints #-}
|
||||||
|
|
||||||
|
module Auth.OAuth2
|
||||||
|
( apAzure
|
||||||
|
, azurePrimaryKey, azureUserPrincipalName, azureUserDisplayName, azureUserGivenName, azureUserSurname, azureUserMail, azureUserTelephone, azureUserMobile, azureUserPreferredLanguage
|
||||||
|
-- , azureUser, azureUser'
|
||||||
|
, AzureUserException(..), _AzureUserError, _AzureUserNoResult, _AzureUserAmbiguous
|
||||||
|
, apAzureMock
|
||||||
|
, azureMockServer
|
||||||
|
, queryOAuth2User
|
||||||
|
, refreshOAuth2Token
|
||||||
|
, singleSignOut
|
||||||
|
) where
|
||||||
|
|
||||||
|
-- import qualified Data.CaseInsensitive as CI
|
||||||
|
import Data.Maybe (fromJust)
|
||||||
|
import Data.Text
|
||||||
|
|
||||||
|
import Import.NoFoundation hiding (pack, unpack)
|
||||||
|
|
||||||
|
import Network.HTTP.Simple (httpJSONEither, getResponseBody, JSONException)
|
||||||
|
|
||||||
|
import System.Environment (lookupEnv)
|
||||||
|
|
||||||
|
import Yesod.Auth.OAuth2
|
||||||
|
import Yesod.Auth.OAuth2.Prelude hiding (encodeUtf8)
|
||||||
|
|
||||||
|
-- | Plugin name of the OAuth2 yesod plugin for Azure ADv2
|
||||||
|
apAzure :: Text
|
||||||
|
apAzure = "AzureADv2"
|
||||||
|
|
||||||
|
|
||||||
|
-- TODO: deprecate in favour of FetchUserDataException
|
||||||
|
data AzureUserException = AzureUserError
|
||||||
|
| AzureUserNoResult
|
||||||
|
| AzureUserAmbiguous
|
||||||
|
deriving (Show, Eq, Generic)
|
||||||
|
|
||||||
|
instance Exception AzureUserException
|
||||||
|
|
||||||
|
makePrisms ''AzureUserException
|
||||||
|
|
||||||
|
|
||||||
|
azurePrimaryKey, azureUserPrincipalName, azureUserDisplayName, azureUserGivenName, azureUserSurname, azureUserMail, azureUserTelephone, azureUserMobile, azureUserPreferredLanguage :: Text
|
||||||
|
azurePrimaryKey = "id"
|
||||||
|
azureUserPrincipalName = "userPrincipalName"
|
||||||
|
azureUserDisplayName = "displayName"
|
||||||
|
azureUserGivenName = "givenName"
|
||||||
|
azureUserSurname = "surname"
|
||||||
|
azureUserMail = "mail"
|
||||||
|
azureUserTelephone = "businessPhones"
|
||||||
|
azureUserMobile = "mobilePhone"
|
||||||
|
azureUserPreferredLanguage = "preferredLanguage"
|
||||||
|
|
||||||
|
|
||||||
|
-- | User lookup in Microsoft Graph with given credentials
|
||||||
|
-- TODO: deprecate in favour of fetchUserData
|
||||||
|
-- azureUser :: ( MonadMask m
|
||||||
|
-- , MonadHandler m
|
||||||
|
-- -- , HandlerSite m ~ site
|
||||||
|
-- -- , BackendCompatible SqlBackend (YesodPersistBackend site)
|
||||||
|
-- -- , BaseBackend (YesodPersistBackend site) ~ SqlBackend
|
||||||
|
-- -- , YesodPersist site
|
||||||
|
-- -- , PersistUniqueWrite (YesodPersistBackend site)
|
||||||
|
-- )
|
||||||
|
-- => AzureConf
|
||||||
|
-- -> Creds site
|
||||||
|
-- -> m [(Text, [ByteString])] -- (Either AzureUserException [(Text, [ByteString])])
|
||||||
|
-- azureUser AzureConf{..} Creds{..} = fmap throwLeft . runExceptT $ do
|
||||||
|
-- now <- liftIO getCurrentTime
|
||||||
|
-- results <- queryOAuth2User @[(Text, [ByteString])] credsIdent
|
||||||
|
-- case results of
|
||||||
|
-- Right [res] -> do
|
||||||
|
-- -- void . liftHandler . runDB $ upsert ExternalUser
|
||||||
|
-- -- { externalUserUser = error "no userid" -- TODO: use azureUserPrimaryKey once UserIdent is referenced instead of UserId
|
||||||
|
-- -- , externalUserSource = AuthSourceIdAzure azureConfClientId
|
||||||
|
-- -- , externalUserData = toJSON res
|
||||||
|
-- -- , externalUserLastSync = now
|
||||||
|
-- -- }
|
||||||
|
-- -- [ ExternalUserData =. toJSON res
|
||||||
|
-- -- , ExternalUserLastSync =. now
|
||||||
|
-- -- ]
|
||||||
|
-- return res
|
||||||
|
-- Right _multiple -> throwE AzureUserAmbiguous
|
||||||
|
-- Left _ -> throwE AzureUserNoResult
|
||||||
|
|
||||||
|
-- | User lookup in Microsoft Graph with given user
|
||||||
|
-- azureUser' :: ( MonadMask m
|
||||||
|
-- , MonadHandler m
|
||||||
|
-- , HandlerSite m ~ site
|
||||||
|
-- , BaseBackend (YesodPersistBackend site) ~ SqlBackend
|
||||||
|
-- , YesodPersist site
|
||||||
|
-- , PersistUniqueWrite (YesodPersistBackend site)
|
||||||
|
-- )
|
||||||
|
-- => AzureConf
|
||||||
|
-- -> User
|
||||||
|
-- -> ReaderT (YesodPersistBackend site) m (Maybe [(Text, [ByteString])]) -- (Either AzureUserException [(Text, [ByteString])])
|
||||||
|
-- azureUser' conf User{userIdent}
|
||||||
|
-- = runMaybeT . catchIfMaybeT (is _AzureUserNoResult) $ azureUser conf (Creds apAzure (CI.original userIdent) [])
|
||||||
|
|
||||||
|
|
||||||
|
-----------------------------------------------
|
||||||
|
---- OAuth2 + OIDC development auth plugin ----
|
||||||
|
-----------------------------------------------
|
||||||
|
|
||||||
|
apAzureMock :: Text
|
||||||
|
apAzureMock = "uniworx_dev"
|
||||||
|
|
||||||
|
newtype UserID = UserID Text
|
||||||
|
instance FromJSON UserID where
|
||||||
|
parseJSON = withObject "UserID" $ \o ->
|
||||||
|
UserID <$> o .: "id"
|
||||||
|
|
||||||
|
azureMockServer :: YesodAuth m => String -> AuthPlugin m
|
||||||
|
azureMockServer port =
|
||||||
|
let oa = OAuth2
|
||||||
|
{ oauthClientId = "42"
|
||||||
|
, oauthClientSecret = Just "shhh"
|
||||||
|
, oauthOAuthorizeEndpoint = fromString (mockServerURL <> "/auth")
|
||||||
|
`withQuery` [ scopeParam " " ["openid", "profile", "email", "offline_access"] -- TODO read scopes from config
|
||||||
|
, ("response_type", "code id_token")
|
||||||
|
, ("nonce", "Foo") -- TODO generate meaningful value
|
||||||
|
]
|
||||||
|
, oauthAccessTokenEndpoint = fromString $ mockServerURL <> "/token"
|
||||||
|
, oauthCallback = Nothing
|
||||||
|
}
|
||||||
|
mockServerURL = "http://localhost:" <> fromString port
|
||||||
|
profileSrc = fromString $ mockServerURL <> "/users/me"
|
||||||
|
in authOAuth2 apAzureMock oa $ \manager token -> do
|
||||||
|
(UserID userID, userResponse) <- authGetProfile apAzureMock manager token profileSrc
|
||||||
|
return Creds
|
||||||
|
{ credsPlugin = apAzureMock
|
||||||
|
, credsIdent = userID
|
||||||
|
, credsExtra = setExtra token userResponse
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
----------------------
|
||||||
|
---- User Queries ----
|
||||||
|
----------------------
|
||||||
|
|
||||||
|
data UserDataException = UserDataJSONException JSONException
|
||||||
|
| UserDataInternalException Text
|
||||||
|
deriving Show
|
||||||
|
|
||||||
|
instance Exception UserDataException
|
||||||
|
|
||||||
|
queryOAuth2User :: forall j m.
|
||||||
|
( FromJSON j
|
||||||
|
, MonadHandler m
|
||||||
|
, HasAppSettings (HandlerSite m)
|
||||||
|
, MonadThrow m
|
||||||
|
)
|
||||||
|
=> Text -- ^ User identifier (arbitrary needle)
|
||||||
|
-> m (Either UserDataException j)
|
||||||
|
queryOAuth2User userID = runExceptT $ do
|
||||||
|
(queryUrl, tokenUrl) <- mkBaseUrls
|
||||||
|
req <- parseRequest $ "GET " ++ queryUrl ++ unpack userID
|
||||||
|
mTokens <- lookupSessionJson SessionOAuth2Token
|
||||||
|
unless (isJust mTokens) . throwE $ UserDataInternalException "Tried to load session Oauth2 tokens, but there are none"
|
||||||
|
# ifdef DEVELOPMENT
|
||||||
|
let secure = False
|
||||||
|
# else
|
||||||
|
let secure = True
|
||||||
|
# endif
|
||||||
|
newTokens <- refreshOAuth2Token @m (fromJust mTokens) tokenUrl secure
|
||||||
|
setSessionJson SessionOAuth2Token (Just $ accessToken newTokens, refreshToken newTokens)
|
||||||
|
eResult <- lift $ getResponseBody <$> httpJSONEither @m @j (req
|
||||||
|
{ secure = secure
|
||||||
|
, requestHeaders = [("Authorization", encodeUtf8 . ("Bearer " <>) . atoken $ accessToken newTokens)]
|
||||||
|
})
|
||||||
|
case eResult of
|
||||||
|
Left x -> throwE $ UserDataJSONException x
|
||||||
|
Right x -> return x
|
||||||
|
|
||||||
|
|
||||||
|
mkBaseUrls :: (MonadHandler m, HasAppSettings (HandlerSite m)) => m (String, String)
|
||||||
|
mkBaseUrls = do
|
||||||
|
# ifndef DEVELOPMENT
|
||||||
|
tenantID <- fmap (maybe (throwM $ UserDataInternalException "Could not determine tenant ID from current app configuration") show) . getsYesod . preview $ _appUserAuthConf . _userAuthConfSingleSource . _AuthSourceConfAzureAdV2 . _azureConfTenantId
|
||||||
|
return ( "https://graph.microsoft.com/v1.0/users/"
|
||||||
|
, "https://login.microsoftonline.com/" ++ tenantID ++ "/oauth2/v2.0" )
|
||||||
|
# else
|
||||||
|
port :: String <- liftIO $ maybe (throwM $ UserDataInternalException "Development environment variable OAUTH2_SERVER_PORT is unset") id <$> lookupEnv "OAUTH2_SERVER_PORT"
|
||||||
|
let base = "http://localhost:" ++ port
|
||||||
|
return ( base ++ "/users/query?id="
|
||||||
|
, base ++ "/token" )
|
||||||
|
# endif
|
||||||
|
|
||||||
|
|
||||||
|
refreshOAuth2Token :: forall m.
|
||||||
|
( MonadHandler m
|
||||||
|
, MonadThrow m
|
||||||
|
)
|
||||||
|
=> (Maybe AccessToken, Maybe RefreshToken)
|
||||||
|
-> String
|
||||||
|
-> Bool
|
||||||
|
-> ExceptT UserDataException m OAuth2Token
|
||||||
|
refreshOAuth2Token (_, rToken) url secure
|
||||||
|
| isJust rToken = do
|
||||||
|
req <- parseRequest $ "POST " ++ url
|
||||||
|
let
|
||||||
|
body =
|
||||||
|
[ ("grant_type", "refresh_token")
|
||||||
|
, ("refresh_token", encodeUtf8 . rtoken $ fromJust rToken)
|
||||||
|
]
|
||||||
|
body' <- if secure then do
|
||||||
|
clientID <- liftIO $ fromJust <$> lookupEnv "CLIENT_ID"
|
||||||
|
clientSecret <- liftIO $ fromJust <$> lookupEnv "CLIENT_SECRET"
|
||||||
|
return $ body ++ [("client_id", fromString clientID), ("client_secret", fromString clientSecret), scopeParam " " ["openid","profile"," offline_access"]] -- TODO read from config
|
||||||
|
else return $ scopeParam " " ["openid","profile","offline_access"] : body -- TODO read from config
|
||||||
|
$logDebugS "\27[31mAdmin Handler\27[0m" $ tshow (requestBody $ urlEncodedBody body' req{ secure = secure })
|
||||||
|
eResult <- lift $ getResponseBody <$> httpJSONEither @m @OAuth2Token (urlEncodedBody body' req{ secure = secure })
|
||||||
|
case eResult of
|
||||||
|
Left x -> throwE $ UserDataJSONException x
|
||||||
|
Right x -> return x
|
||||||
|
| otherwise = throwE $ UserDataInternalException "Could not refresh access token. Refresh token is missing."
|
||||||
|
|
||||||
|
instance Show RequestBody where
|
||||||
|
show (RequestBodyLBS x) = show x
|
||||||
|
show _ = error ":("
|
||||||
|
|
||||||
|
|
||||||
|
-----------------------
|
||||||
|
---- Single Sign-Out ----
|
||||||
|
-----------------------
|
||||||
|
|
||||||
|
singleSignOut :: forall a m. (MonadHandler m)
|
||||||
|
=> Maybe Text -- ^ redirect uri
|
||||||
|
-> m a
|
||||||
|
singleSignOut mRedirect = do
|
||||||
|
# ifdef DEVELOPMENT
|
||||||
|
port <- liftIO $ fromJust <$> lookupEnv "OAUTH2_SERVER_PORT"
|
||||||
|
let base = "http://localhost:" <> pack port <> "/logout"
|
||||||
|
# else
|
||||||
|
let base = "" -- TODO find out fraport oidc end_session_endpoint
|
||||||
|
# endif
|
||||||
|
endpoint = case mRedirect of
|
||||||
|
Just r -> base <> "?post_logout_redirect_uri=" <> r
|
||||||
|
Nothing -> base
|
||||||
|
$logDebugS "\n\27[31mSSO\27[0m" endpoint
|
||||||
|
redirect endpoint
|
||||||
|
|
||||||
@ -1,4 +1,4 @@
|
|||||||
-- SPDX-FileCopyrightText: 2022 Felix Hamann <felix.hamann@campus.lmu.de>,Gregor Kleen <gregor.kleen@ifi.lmu.de>,Sarah Vaupel <sarah.vaupel@ifi.lmu.de>
|
-- SPDX-FileCopyrightText: 2022-2024 Sarah Vaupel <sarah.vaupel@uniworx.de>, Felix Hamann <felix.hamann@campus.lmu.de>, Gregor Kleen <gregor.kleen@ifi.lmu.de>, Sarah Vaupel <sarah.vaupel@ifi.lmu.de>
|
||||||
--
|
--
|
||||||
-- SPDX-License-Identifier: AGPL-3.0-or-later
|
-- SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
@ -68,12 +68,13 @@ hashLogin pwHashAlgo = AuthPlugin{..}
|
|||||||
tp <- getRouteToParent
|
tp <- getRouteToParent
|
||||||
|
|
||||||
resp <- formResultMaybe loginRes $ \HashLogin{..} -> Just <$> do
|
resp <- formResultMaybe loginRes $ \HashLogin{..} -> Just <$> do
|
||||||
user <- liftHandler . runDB . getBy $ UniqueAuthentication hashIdent
|
user :: Maybe (Entity User) <- liftHandler . runDB . getBy $ UniqueAuthentication hashIdent
|
||||||
case user of
|
case user of
|
||||||
Just (Entity _ User{ userAuthentication = AuthPWHash{..}, userIdent = CI.original -> userIdent })
|
Just (Entity _ User{userIdent,userPasswordHash})
|
||||||
| verifyPasswordWith pwHashAlgo (2^) (encodeUtf8 hashPassword) (encodeUtf8 authPWHash) -> do -- (2^) is magic.
|
| Just pwHash <- userPasswordHash
|
||||||
|
, verifyPasswordWith pwHashAlgo (2^) (encodeUtf8 hashPassword) (encodeUtf8 pwHash) -> do -- (2^) is magic.
|
||||||
observeLoginOutcome apName LoginSuccessful
|
observeLoginOutcome apName LoginSuccessful
|
||||||
setCredsRedirect $ Creds apName userIdent []
|
setCredsRedirect $ Creds apName (CI.original userIdent) []
|
||||||
other -> do
|
other -> do
|
||||||
$logDebugS apName $ tshow other
|
$logDebugS apName $ tshow other
|
||||||
observeLoginOutcome apName LoginInvalidCredentials
|
observeLoginOutcome apName LoginInvalidCredentials
|
||||||
|
|||||||
@ -59,7 +59,6 @@ decCryptoIDs [ ''SubmissionId
|
|||||||
, ''MaterialFileId
|
, ''MaterialFileId
|
||||||
, ''PrintJobId
|
, ''PrintJobId
|
||||||
, ''QualificationId
|
, ''QualificationId
|
||||||
, ''SentMailId
|
|
||||||
]
|
]
|
||||||
|
|
||||||
decCryptoIDKeySize
|
decCryptoIDKeySize
|
||||||
|
|||||||
@ -128,4 +128,4 @@ instance Swagger.ToSchema s => Swagger.ToSchema (CI s) where
|
|||||||
|
|
||||||
instance (CI.FoldCase s, Binary s) => Binary (CI s) where
|
instance (CI.FoldCase s, Binary s) => Binary (CI s) where
|
||||||
get = CI.mk <$> Binary.get
|
get = CI.mk <$> Binary.get
|
||||||
put = Binary.put . CI.original
|
put = Binary.put . CI.original
|
||||||
|
|||||||
@ -15,7 +15,6 @@ module Database.Esqueleto.Utils
|
|||||||
, (=?.), (?=.)
|
, (=?.), (?=.)
|
||||||
, (=~.), (~=.)
|
, (=~.), (~=.)
|
||||||
, (>~.), (<~.)
|
, (>~.), (<~.)
|
||||||
, (~.), (~*.), (!~.), (!~*.)
|
|
||||||
, or, and
|
, or, and
|
||||||
, any, all
|
, any, all
|
||||||
, not__, parens
|
, not__, parens
|
||||||
@ -27,14 +26,12 @@ module Database.Esqueleto.Utils
|
|||||||
, mkContainsFilterWithSet, mkContainsFilterWithComma, mkContainsFilterWithCommaPlus
|
, mkContainsFilterWithSet, mkContainsFilterWithComma, mkContainsFilterWithCommaPlus
|
||||||
, mkDayFilter, mkDayFilterFrom, mkDayFilterTo
|
, mkDayFilter, mkDayFilterFrom, mkDayFilterTo
|
||||||
, mkExistsFilter, mkExistsFilterWithComma
|
, mkExistsFilter, mkExistsFilterWithComma
|
||||||
-- , mkRegExFilterWith
|
|
||||||
, anyFilter, allFilter
|
, anyFilter, allFilter
|
||||||
, ascNullsFirst, descNullsLast
|
, ascNullsFirst, descNullsLast
|
||||||
, orderByList
|
, orderByList
|
||||||
, orderByOrd, orderByEnum
|
, orderByOrd, orderByEnum
|
||||||
, strip, lower, ciEq
|
, strip, lower, ciEq
|
||||||
, selectExists, selectNotExists
|
, selectExists, selectNotExists
|
||||||
, filterExists
|
|
||||||
, SqlHashable
|
, SqlHashable
|
||||||
, sha256
|
, sha256
|
||||||
, isTrue, isFalse
|
, isTrue, isFalse
|
||||||
@ -44,19 +41,16 @@ module Database.Esqueleto.Utils
|
|||||||
, greatest, least
|
, greatest, least
|
||||||
, abs
|
, abs
|
||||||
, SqlProject(..)
|
, SqlProject(..)
|
||||||
, (->.), (->>.), (->>>.), (#>>.)
|
, (->.), (->>.), (#>>.)
|
||||||
, fromSqlKey
|
, fromSqlKey
|
||||||
, unKey
|
, unKey
|
||||||
, subSelectCountDistinct
|
, subSelectCountDistinct
|
||||||
, selectCountRows, selectCountDistinct
|
, selectCountRows, selectCountDistinct
|
||||||
, selectMaybe
|
, selectMaybe
|
||||||
, str2text, str2text'
|
, num2text
|
||||||
, num2text --, text2num
|
|
||||||
, day, day', dayMaybe, interval, diffDays, diffTimes
|
, day, day', dayMaybe, interval, diffDays, diffTimes
|
||||||
, exprLift
|
, exprLift
|
||||||
, explicitUnsafeCoerceSqlExprValue
|
, explicitUnsafeCoerceSqlExprValue
|
||||||
, psqlVersion_
|
|
||||||
, truncateTable
|
|
||||||
, module Database.Esqueleto.Utils.TH
|
, module Database.Esqueleto.Utils.TH
|
||||||
) where
|
) where
|
||||||
|
|
||||||
@ -67,16 +61,12 @@ import qualified Data.Set as Set
|
|||||||
import qualified Data.List as List
|
import qualified Data.List as List
|
||||||
import qualified Data.Foldable as F
|
import qualified Data.Foldable as F
|
||||||
import Data.List.NonEmpty (NonEmpty(..))
|
import Data.List.NonEmpty (NonEmpty(..))
|
||||||
import qualified Database.Persist as P
|
|
||||||
import qualified Database.Persist.EntityDef.Internal as P (entityDB)
|
|
||||||
import qualified Database.Esqueleto.Legacy as E
|
import qualified Database.Esqueleto.Legacy as E
|
||||||
import qualified Database.Esqueleto.Experimental as Ex
|
import qualified Database.Esqueleto.Experimental as Ex
|
||||||
import qualified Database.Esqueleto.PostgreSQL as E
|
import qualified Database.Esqueleto.PostgreSQL as E
|
||||||
import qualified Database.Esqueleto.Internal.Internal as E
|
import qualified Database.Esqueleto.Internal.Internal as E
|
||||||
import Database.Esqueleto.Utils.TH
|
import Database.Esqueleto.Utils.TH
|
||||||
|
|
||||||
-- import qualified Database.Persist.Postgresql as P
|
|
||||||
|
|
||||||
import qualified Data.Text as Text
|
import qualified Data.Text as Text
|
||||||
import qualified Data.Text.Lazy as Lazy (Text)
|
import qualified Data.Text.Lazy as Lazy (Text)
|
||||||
import qualified Data.ByteString.Lazy as Lazy (ByteString)
|
import qualified Data.ByteString.Lazy as Lazy (ByteString)
|
||||||
@ -166,24 +156,6 @@ infixl 4 <~.
|
|||||||
(<~.) :: PersistField typ => E.SqlExpr (E.Value typ) -> E.SqlExpr (E.Value (Maybe typ)) -> E.SqlExpr (E.Value Bool)
|
(<~.) :: PersistField typ => E.SqlExpr (E.Value typ) -> E.SqlExpr (E.Value (Maybe typ)) -> E.SqlExpr (E.Value Bool)
|
||||||
(<~.) a b = E.isNothing b E.||. (E.just a E.<. b)
|
(<~.) a b = E.isNothing b E.||. (E.just a E.<. b)
|
||||||
|
|
||||||
infixr 2 ~., ~*., !~., !~*.
|
|
||||||
|
|
||||||
-- | PostgreSQL regular expression match, case sensitive. Works, but may throw SQL error for unblanced parenthesis, etc. Not suitable for dbTable filters
|
|
||||||
(~.) :: E.SqlString s => E.SqlExpr (E.Value s) -> E.SqlExpr (E.Value s) -> E.SqlExpr (E.Value Bool)
|
|
||||||
(~.) = E.unsafeSqlBinOp " ~ "
|
|
||||||
|
|
||||||
-- | PostgreSQL regular expression match, case insensitive. Works, but may throw SQL errors
|
|
||||||
(~*.) :: E.SqlString s => E.SqlExpr (E.Value s) -> E.SqlExpr (E.Value s) -> E.SqlExpr (E.Value Bool)
|
|
||||||
(~*.) = E.unsafeSqlBinOp " ~* "
|
|
||||||
|
|
||||||
-- | PostgreSQL regular expression does not match, case sensitive. Works, but may throw SQL error for unblanced parenthesis, etc. Not suitable for dbTable filters
|
|
||||||
(!~.) :: E.SqlString s => E.SqlExpr (E.Value s) -> E.SqlExpr (E.Value s) -> E.SqlExpr (E.Value Bool)
|
|
||||||
(!~.) = E.unsafeSqlBinOp " !~ "
|
|
||||||
|
|
||||||
-- | PostgreSQL regular expression does not match, case insensitive. Works, but may throw SQL errors
|
|
||||||
(!~*.) :: E.SqlString s => E.SqlExpr (E.Value s) -> E.SqlExpr (E.Value s) -> E.SqlExpr (E.Value Bool)
|
|
||||||
(!~*.) = E.unsafeSqlBinOp " !~* "
|
|
||||||
|
|
||||||
|
|
||||||
-- | Negation of `isNothing` which is missing
|
-- | Negation of `isNothing` which is missing
|
||||||
isJust :: PersistField typ => E.SqlExpr (E.Value (Maybe typ)) -> E.SqlExpr (E.Value Bool)
|
isJust :: PersistField typ => E.SqlExpr (E.Value (Maybe typ)) -> E.SqlExpr (E.Value Bool)
|
||||||
@ -350,7 +322,7 @@ mkExactFilterLastWith :: (PersistField b)
|
|||||||
-> Last a -- ^ needle
|
-> Last a -- ^ needle
|
||||||
-> E.SqlExpr (E.Value Bool)
|
-> E.SqlExpr (E.Value Bool)
|
||||||
mkExactFilterLastWith cast lenslike row criterias
|
mkExactFilterLastWith cast lenslike row criterias
|
||||||
| Last (Just crit) <- criterias = lenslike row E.==. E.val (cast crit)
|
| Last (Just crit) <- criterias = lenslike row E.==. E.val (cast crit)
|
||||||
| otherwise = true
|
| otherwise = true
|
||||||
|
|
||||||
-- | like `mkExactFilterLast` but deals with Nothing being a filter criterion as well
|
-- | like `mkExactFilterLast` but deals with Nothing being a filter criterion as well
|
||||||
@ -379,7 +351,7 @@ mkExactFilterMaybeLast' lensexists lenslike row criterias
|
|||||||
-- | generic filter creation for dbTable
|
-- | generic filter creation for dbTable
|
||||||
-- Given a lens-like function, make filter searching for needles in String-like elements
|
-- Given a lens-like function, make filter searching for needles in String-like elements
|
||||||
-- (Keep Set here to ensure that there are no duplicates)
|
-- (Keep Set here to ensure that there are no duplicates)
|
||||||
mkContainsFilter :: (E.SqlString a, Ord a)
|
mkContainsFilter :: E.SqlString a
|
||||||
=> (t -> E.SqlExpr (E.Value a)) -- ^ getter from query to searched element
|
=> (t -> E.SqlExpr (E.Value a)) -- ^ getter from query to searched element
|
||||||
-> t -- ^ query row
|
-> t -- ^ query row
|
||||||
-> Set.Set a -- ^ needle collection
|
-> Set.Set a -- ^ needle collection
|
||||||
@ -387,7 +359,7 @@ mkContainsFilter :: (E.SqlString a, Ord a)
|
|||||||
mkContainsFilter = mkContainsFilterWith id
|
mkContainsFilter = mkContainsFilterWith id
|
||||||
|
|
||||||
-- | like `mkContainsFilter` but allows for conversion; convenient in conjunction with `anyFilter` and `allFilter`
|
-- | like `mkContainsFilter` but allows for conversion; convenient in conjunction with `anyFilter` and `allFilter`
|
||||||
mkContainsFilterWith :: (E.SqlString b, Ord a)
|
mkContainsFilterWith :: E.SqlString b
|
||||||
=> (a -> b)
|
=> (a -> b)
|
||||||
-> (t -> E.SqlExpr (E.Value b)) -- ^ getter from query to searched element
|
-> (t -> E.SqlExpr (E.Value b)) -- ^ getter from query to searched element
|
||||||
-> t -- ^ query row
|
-> t -- ^ query row
|
||||||
@ -395,7 +367,7 @@ mkContainsFilterWith :: (E.SqlString b, Ord a)
|
|||||||
-> E.SqlExpr (E.Value Bool)
|
-> E.SqlExpr (E.Value Bool)
|
||||||
mkContainsFilterWith cast lenslike row criterias
|
mkContainsFilterWith cast lenslike row criterias
|
||||||
| Set.null criterias = true
|
| Set.null criterias = true
|
||||||
| otherwise = any (hasInfix (lenslike row) . E.val . cast) criterias
|
| otherwise = any (hasInfix $ lenslike row) (E.val . cast <$> Set.toList criterias)
|
||||||
|
|
||||||
-- | like `mkContainsFilterWith` but allows conversion to produce multiple needles
|
-- | like `mkContainsFilterWith` but allows conversion to produce multiple needles
|
||||||
mkContainsFilterWithSet :: (E.SqlString b, Ord b, Ord a)
|
mkContainsFilterWithSet :: (E.SqlString b, Ord b, Ord a)
|
||||||
@ -406,7 +378,7 @@ mkContainsFilterWithSet :: (E.SqlString b, Ord b, Ord a)
|
|||||||
-> E.SqlExpr (E.Value Bool)
|
-> E.SqlExpr (E.Value Bool)
|
||||||
mkContainsFilterWithSet cast lenslike row criterias
|
mkContainsFilterWithSet cast lenslike row criterias
|
||||||
| Set.null criterias = true
|
| Set.null criterias = true
|
||||||
| otherwise = any (hasInfix (lenslike row) . E.val) (foldMap cast criterias)
|
| otherwise = any (hasInfix $ lenslike row) (E.val <$> Set.toList (foldMap cast criterias))
|
||||||
|
|
||||||
-- | like `mkContainsFilterWithSet` but fixed to comma separated Texts
|
-- | like `mkContainsFilterWithSet` but fixed to comma separated Texts
|
||||||
mkContainsFilterWithComma :: (E.SqlString b, Ord b)
|
mkContainsFilterWithComma :: (E.SqlString b, Ord b)
|
||||||
@ -417,7 +389,7 @@ mkContainsFilterWithComma :: (E.SqlString b, Ord b)
|
|||||||
-> E.SqlExpr (E.Value Bool)
|
-> E.SqlExpr (E.Value Bool)
|
||||||
mkContainsFilterWithComma cast lenslike row (foldMap commaSeparatedText -> criterias)
|
mkContainsFilterWithComma cast lenslike row (foldMap commaSeparatedText -> criterias)
|
||||||
| Set.null criterias = true
|
| Set.null criterias = true
|
||||||
| otherwise = any (hasInfix (lenslike row) . E.val . cast) criterias
|
| otherwise = any (hasInfix $ lenslike row) (E.val . cast <$> Set.toList criterias)
|
||||||
|
|
||||||
-- | like `mkContainsFilterWithComma` but enforced the existence of all Texts prefixed with +
|
-- | like `mkContainsFilterWithComma` but enforced the existence of all Texts prefixed with +
|
||||||
mkContainsFilterWithCommaPlus :: (E.SqlString b, Ord b)
|
mkContainsFilterWithCommaPlus :: (E.SqlString b, Ord b)
|
||||||
@ -431,22 +403,10 @@ mkContainsFilterWithCommaPlus cast lenslike row (foldMap commaSeparatedText -> c
|
|||||||
| Set.null compulsories = cond_optional
|
| Set.null compulsories = cond_optional
|
||||||
| Set.null alternatives = cond_compulsory
|
| Set.null alternatives = cond_compulsory
|
||||||
| otherwise = cond_compulsory E.&&. cond_optional
|
| otherwise = cond_compulsory E.&&. cond_optional
|
||||||
where
|
where
|
||||||
(Set.mapMonotonic (Text.stripStart . Text.drop 1) -> compulsories, alternatives) = Set.partition (Text.isPrefixOf "+") criterias
|
(Set.mapMonotonic (Text.stripStart . Text.drop 1) -> compulsories, alternatives) = Set.partition (Text.isPrefixOf "+") criterias
|
||||||
cond_compulsory = all (hasInfix (lenslike row) . E.val . cast) compulsories
|
cond_compulsory = all (hasInfix $ lenslike row) (E.val . cast <$> Set.toList compulsories)
|
||||||
cond_optional = any (hasInfix (lenslike row) . E.val . cast) alternatives
|
cond_optional = any (hasInfix $ lenslike row) (E.val . cast <$> Set.toList alternatives)
|
||||||
|
|
||||||
-- like `mkContainsFilterWith` but allows regular expression criterias
|
|
||||||
-- This works, but throws SQL errors for unbalanced parenthesis and similar invalid regex expressions
|
|
||||||
-- mkRegExFilterWith :: (E.SqlString b, Ord a)
|
|
||||||
-- => (a -> b)
|
|
||||||
-- -> (t -> E.SqlExpr (E.Value b)) -- ^ getter from query to searched element
|
|
||||||
-- -> t -- ^ query row
|
|
||||||
-- -> Set.Set a -- ^ needle collection
|
|
||||||
-- -> E.SqlExpr (E.Value Bool)
|
|
||||||
-- mkRegExFilterWith cast lenslike row criterias
|
|
||||||
-- | Set.null criterias = true
|
|
||||||
-- | otherwise = any ((~.) (lenslike row) . E.val . cast) criterias
|
|
||||||
|
|
||||||
mkDayFilter :: (t -> E.SqlExpr (E.Value UTCTime)) -- ^ getter from query to searched element
|
mkDayFilter :: (t -> E.SqlExpr (E.Value UTCTime)) -- ^ getter from query to searched element
|
||||||
-> t -- ^ query row
|
-> t -- ^ query row
|
||||||
@ -491,7 +451,7 @@ mkExistsFilterWithComma :: PathPiece a
|
|||||||
-> E.SqlExpr (E.Value Bool)
|
-> E.SqlExpr (E.Value Bool)
|
||||||
mkExistsFilterWithComma cast query row (foldMap commaSeparatedText -> criterias)
|
mkExistsFilterWithComma cast query row (foldMap commaSeparatedText -> criterias)
|
||||||
| Set.null criterias = true
|
| Set.null criterias = true
|
||||||
| otherwise = any (E.exists . query row . cast) criterias
|
| otherwise = any (E.exists . query row) (cast <$> Set.toList criterias)
|
||||||
|
|
||||||
|
|
||||||
-- | Combine several filters, using logical or
|
-- | Combine several filters, using logical or
|
||||||
@ -550,13 +510,6 @@ selectExists query = do
|
|||||||
_other -> error "SELECT EXISTS ... returned zero or more than one rows"
|
_other -> error "SELECT EXISTS ... returned zero or more than one rows"
|
||||||
selectNotExists = fmap not . selectExists
|
selectNotExists = fmap not . selectExists
|
||||||
|
|
||||||
filterExists :: (MonadIO m, PersistEntity val, MonoFoldable mono, PersistField (Element mono))
|
|
||||||
=> EntityField val (Element mono) -> mono -> E.SqlReadT m [Element mono]
|
|
||||||
filterExists prj vs = fmap (fmap Ex.unValue) <$> Ex.select $ do
|
|
||||||
ent <- Ex.from Ex.table
|
|
||||||
Ex.where_ $ ent Ex.^. prj `Ex.in_` vals vs
|
|
||||||
return $ ent Ex.^. prj
|
|
||||||
|
|
||||||
|
|
||||||
class SqlHashable a
|
class SqlHashable a
|
||||||
instance SqlHashable Text
|
instance SqlHashable Text
|
||||||
@ -650,7 +603,7 @@ max, min :: PersistField a
|
|||||||
max a b = bool a b $ b E.>. a
|
max a b = bool a b $ b E.>. a
|
||||||
min a b = bool a b $ b E.<. a
|
min a b = bool a b $ b E.<. a
|
||||||
|
|
||||||
-- these alternatives for max/min ought to be more efficient; note that NULL is avoided by PostgreSQL greatest/least; for Bool: t > f
|
-- these alternatives for max/min ought to be more efficient; note that NULL is avoided by PostgreSQL greatest/least
|
||||||
greatest :: PersistField a => E.SqlExpr (E.Value a) -> E.SqlExpr (E.Value a) -> E.SqlExpr (E.Value a)
|
greatest :: PersistField a => E.SqlExpr (E.Value a) -> E.SqlExpr (E.Value a) -> E.SqlExpr (E.Value a)
|
||||||
greatest a b = E.unsafeSqlFunction "GREATEST" $ E.toArgList (a,b)
|
greatest a b = E.unsafeSqlFunction "GREATEST" $ E.toArgList (a,b)
|
||||||
|
|
||||||
@ -689,16 +642,9 @@ infixl 8 ->.
|
|||||||
|
|
||||||
infixl 8 ->>.
|
infixl 8 ->>.
|
||||||
|
|
||||||
-- Unsafe variant, see Database.Esqueleto.PostgreSQL.JSON for a safe version!
|
|
||||||
(->>.) :: E.SqlExpr (E.Value a) -> Text -> E.SqlExpr (E.Value Text)
|
(->>.) :: E.SqlExpr (E.Value a) -> Text -> E.SqlExpr (E.Value Text)
|
||||||
(->>.) expr t = E.unsafeSqlBinOp "->>" expr $ E.val t
|
(->>.) expr t = E.unsafeSqlBinOp "->>" expr $ E.val t
|
||||||
|
|
||||||
infixl 8 ->>>.
|
|
||||||
|
|
||||||
-- Unsafe variant to obtain a DB key from a JSON field. Use with caution!
|
|
||||||
(->>>.) :: (PersistField (Key entity)) => E.SqlExpr (E.Value a) -> Text -> E.SqlExpr (E.Value (Maybe (Key entity)))
|
|
||||||
(->>>.) expr t = E.unsafeSqlCastAs "int" $ E.unsafeSqlBinOp "->>" expr $ E.val t
|
|
||||||
|
|
||||||
infixl 8 #>>.
|
infixl 8 #>>.
|
||||||
|
|
||||||
(#>>.) :: E.SqlExpr (E.Value a) -> Text -> E.SqlExpr (E.Value (Maybe Text))
|
(#>>.) :: E.SqlExpr (E.Value a) -> Text -> E.SqlExpr (E.Value (Maybe Text))
|
||||||
@ -717,7 +663,7 @@ unKey = E.veryUnsafeCoerceSqlExprValue
|
|||||||
-- | distinct version of `Database.Esqueleto.subSelectCount`
|
-- | distinct version of `Database.Esqueleto.subSelectCount`
|
||||||
subSelectCountDistinct :: (Num a, PersistField a) => Ex.SqlQuery (Ex.SqlExpr (Ex.Value typ)) -> Ex.SqlExpr (Ex.Value a)
|
subSelectCountDistinct :: (Num a, PersistField a) => Ex.SqlQuery (Ex.SqlExpr (Ex.Value typ)) -> Ex.SqlExpr (Ex.Value a)
|
||||||
subSelectCountDistinct query = Ex.subSelectUnsafe (Ex.countDistinct <$> query)
|
subSelectCountDistinct query = Ex.subSelectUnsafe (Ex.countDistinct <$> query)
|
||||||
|
|
||||||
-- PersistField a => SqlQuery (SqlExpr (Value a)) -> SqlExpr (Value a)
|
-- PersistField a => SqlQuery (SqlExpr (Value a)) -> SqlExpr (Value a)
|
||||||
-- countDistinct :: Num a => SqlExpr (Value typ) -> SqlExpr (Value a)
|
-- countDistinct :: Num a => SqlExpr (Value typ) -> SqlExpr (Value a)
|
||||||
|
|
||||||
@ -742,21 +688,10 @@ selectCountDistinct q = do
|
|||||||
selectMaybe :: (E.SqlSelect a r, MonadIO m) => E.SqlQuery a -> E.SqlReadT m (Maybe r)
|
selectMaybe :: (E.SqlSelect a r, MonadIO m) => E.SqlQuery a -> E.SqlReadT m (Maybe r)
|
||||||
selectMaybe = fmap listToMaybe . E.select . (<* E.limit 1)
|
selectMaybe = fmap listToMaybe . E.select . (<* E.limit 1)
|
||||||
|
|
||||||
-- | convert something that is like a text to text
|
|
||||||
str2text :: E.SqlString a => E.SqlExpr (E.Value a) -> E.SqlExpr (E.Value Text)
|
|
||||||
str2text = E.unsafeSqlCastAs "text"
|
|
||||||
|
|
||||||
str2text' :: E.SqlString a => E.SqlExpr (E.Value (Maybe a)) -> E.SqlExpr (E.Value (Maybe Text))
|
|
||||||
str2text' = E.unsafeSqlCastAs "text"
|
|
||||||
|
|
||||||
-- | cast numeric type to text, which is safe and allows for an inefficient but safe comparison of numbers stored as text and numbers
|
-- | cast numeric type to text, which is safe and allows for an inefficient but safe comparison of numbers stored as text and numbers
|
||||||
num2text :: Num n => E.SqlExpr (E.Value n) -> E.SqlExpr (E.Value Text)
|
num2text :: Num n => E.SqlExpr (E.Value n) -> E.SqlExpr (E.Value Text)
|
||||||
num2text = E.unsafeSqlCastAs "text"
|
num2text = E.unsafeSqlCastAs "text"
|
||||||
|
|
||||||
-- unsafe, use with care!
|
|
||||||
-- text2num :: E.SqlExpr (E.Value Text) -> E.SqlExpr (E.Value n)
|
|
||||||
-- text2num = E.unsafeSqlCastAs "int"
|
|
||||||
|
|
||||||
day :: E.SqlExpr (E.Value UTCTime) -> E.SqlExpr (E.Value Day)
|
day :: E.SqlExpr (E.Value UTCTime) -> E.SqlExpr (E.Value Day)
|
||||||
day = E.unsafeSqlCastAs "date"
|
day = E.unsafeSqlCastAs "date"
|
||||||
|
|
||||||
@ -768,9 +703,9 @@ dayMaybe :: E.SqlExpr (E.Value (Maybe UTCTime)) -> E.SqlExpr (E.Value (Maybe Day
|
|||||||
dayMaybe = E.unsafeSqlCastAs "date"
|
dayMaybe = E.unsafeSqlCastAs "date"
|
||||||
|
|
||||||
interval :: CalendarDiffDays -> E.SqlExpr (E.Value Day) -- E.+=. requires both types to be the same, so we use Day
|
interval :: CalendarDiffDays -> E.SqlExpr (E.Value Day) -- E.+=. requires both types to be the same, so we use Day
|
||||||
-- interval _ = E.unsafeSqlCastAs "interval" $ E.unsafeSqlValue "'P2Y'" -- tested working example
|
-- interval _ = E.unsafeSqlCastAs "interval" $ E.unsafeSqlValue "'P2Y'" -- tested working example
|
||||||
interval = E.unsafeSqlCastAs "interval". E.unsafeSqlValue . wrapSqlString . Text.Builder.fromString . iso8601Show
|
interval = E.unsafeSqlCastAs "interval". E.unsafeSqlValue . wrapSqlString . Text.Builder.fromString . iso8601Show
|
||||||
where
|
where
|
||||||
singleQuote = Text.Builder.singleton '\''
|
singleQuote = Text.Builder.singleton '\''
|
||||||
wrapSqlString b = singleQuote <> b <> singleQuote
|
wrapSqlString b = singleQuote <> b <> singleQuote
|
||||||
|
|
||||||
@ -815,16 +750,3 @@ instance (PersistField a1, PersistField a2, PersistField b, Finite a1, Finite a2
|
|||||||
]
|
]
|
||||||
(E.else_ $ E.else_ $ E.veryUnsafeCoerceSqlExprValue (E.nothing :: E.SqlExpr (E.Value (Maybe ()))))
|
(E.else_ $ E.else_ $ E.veryUnsafeCoerceSqlExprValue (E.nothing :: E.SqlExpr (E.Value (Maybe ()))))
|
||||||
|
|
||||||
psqlVersion_ :: E.SqlExpr (E.Value Text)
|
|
||||||
psqlVersion_ = E.unsafeSqlFunction "VERSION" ()
|
|
||||||
|
|
||||||
-- Suspected to cause trouble. Needs more testing!
|
|
||||||
-- truncateTable :: (MonadIO m, BackendCompatible SqlBackend backend, PersistEntity record)
|
|
||||||
-- => record -> ReaderT backend m ()
|
|
||||||
-- truncateTable tbl = E.rawExecute ("TRUNCATE TABLE " <> P.tableName tbl <> " RESTART IDENTITY") []
|
|
||||||
|
|
||||||
truncateTable :: (MonadIO m, BackendCompatible SqlBackend backend, PersistEntity record) -- TODO: test this code
|
|
||||||
=> proxy record -> ReaderT backend m ()
|
|
||||||
truncateTable tbl =
|
|
||||||
let tblName :: Text = P.unEntityNameDB $ P.entityDB $ P.entityDef tbl
|
|
||||||
in E.rawExecute ("TRUNCATE TABLE " <> tblName <> " RESTART IDENTITY") []
|
|
||||||
@ -1,4 +1,4 @@
|
|||||||
-- SPDX-FileCopyrightText: 2022 Gregor Kleen <gregor.kleen@ifi.lmu.de>,Sarah Vaupel <sarah.vaupel@ifi.lmu.de>,Steffen Jost <jost@tcs.ifi.lmu.de>,Wolfgang Witt <Wolfgang.Witt@campus.lmu.de>
|
-- SPDX-FileCopyrightText: 2022-2024 Sarah Vaupel <sarah.vaupel@uniworx.de>, Gregor Kleen <gregor.kleen@ifi.lmu.de>, Sarah Vaupel <sarah.vaupel@ifi.lmu.de>, Steffen Jost <jost@tcs.ifi.lmu.de>, Wolfgang Witt <Wolfgang.Witt@campus.lmu.de>
|
||||||
--
|
--
|
||||||
-- SPDX-License-Identifier: AGPL-3.0-or-later
|
-- SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
@ -1521,7 +1521,7 @@ tagAccessPredicate AuthSelf = APDB $ \_ _ mAuthId route _ -> exceptT return retu
|
|||||||
| uid == referencedUser -> return Authorized
|
| uid == referencedUser -> return Authorized
|
||||||
Nothing -> return AuthenticationRequired
|
Nothing -> return AuthenticationRequired
|
||||||
_other -> unauthorizedI MsgUnauthorizedSelf
|
_other -> unauthorizedI MsgUnauthorizedSelf
|
||||||
tagAccessPredicate AuthIsLDAP = APDB $ \_ _ _ route _ -> exceptT return return $ do
|
tagAccessPredicate AuthIsExternal = APDB $ \_ _ _ route _ -> exceptT return return $ do
|
||||||
referencedUser <- case route of
|
referencedUser <- case route of
|
||||||
AdminUserR cID -> return cID
|
AdminUserR cID -> return cID
|
||||||
AdminUserDeleteR cID -> return cID
|
AdminUserDeleteR cID -> return cID
|
||||||
@ -1529,13 +1529,17 @@ tagAccessPredicate AuthIsLDAP = APDB $ \_ _ _ route _ -> exceptT return return $
|
|||||||
UserNotificationR cID -> return cID
|
UserNotificationR cID -> return cID
|
||||||
UserPasswordR cID -> return cID
|
UserPasswordR cID -> return cID
|
||||||
CourseR _ _ _ (CUserR cID) -> return cID
|
CourseR _ _ _ (CUserR cID) -> return cID
|
||||||
_other -> throwError =<< $unsupportedAuthPredicate AuthIsLDAP route
|
_other -> throwError =<< $unsupportedAuthPredicate AuthIsExternal route
|
||||||
referencedUser' <- catchIfMExceptT (const $ unauthorizedI MsgUnauthorizedSelf) (const True :: CryptoIDError -> Bool) $ decrypt referencedUser
|
referencedUser' <- catchIfMExceptT (const $ unauthorizedI MsgUnauthorizedSelf) (const True :: CryptoIDError -> Bool) $ decrypt referencedUser
|
||||||
maybeTMExceptT (unauthorizedI MsgUnauthorizedLDAP) $ do
|
availableSources <- getsYesod (view _appUserAuthConf) >>= \case
|
||||||
User{..} <- MaybeT $ get referencedUser'
|
UserAuthConfSingleSource{..} -> return . singleton $ case userAuthConfSingleSource of
|
||||||
guard $ userAuthentication == AuthLDAP
|
AuthSourceConfAzureAdV2 AzureConf{..} -> AuthSourceIdAzure azureConfTenantId
|
||||||
|
AuthSourceConfLdap LdapConf{..} -> AuthSourceIdLdap ldapConfSourceId
|
||||||
|
maybeTMExceptT (unauthorizedI MsgUnauthorizedExternal) $ do
|
||||||
|
Entity _ User{userIdent} <- MaybeT $ getEntity referencedUser'
|
||||||
|
guardM . lift $ exists [ ExternalUserUser ==. userIdent, ExternalUserSource <-. availableSources ]
|
||||||
return Authorized
|
return Authorized
|
||||||
tagAccessPredicate AuthIsPWHash = APDB $ \_ _ _ route _ -> exceptT return return $ do
|
tagAccessPredicate AuthIsInternal = APDB $ \_ _ _ route _ -> exceptT return return $ do
|
||||||
referencedUser <- case route of
|
referencedUser <- case route of
|
||||||
AdminUserR cID -> return cID
|
AdminUserR cID -> return cID
|
||||||
AdminUserDeleteR cID -> return cID
|
AdminUserDeleteR cID -> return cID
|
||||||
@ -1543,11 +1547,11 @@ tagAccessPredicate AuthIsPWHash = APDB $ \_ _ _ route _ -> exceptT return return
|
|||||||
UserNotificationR cID -> return cID
|
UserNotificationR cID -> return cID
|
||||||
UserPasswordR cID -> return cID
|
UserPasswordR cID -> return cID
|
||||||
CourseR _ _ _ (CUserR cID) -> return cID
|
CourseR _ _ _ (CUserR cID) -> return cID
|
||||||
_other -> throwError =<< $unsupportedAuthPredicate AuthIsPWHash route
|
_other -> throwError =<< $unsupportedAuthPredicate AuthIsInternal route
|
||||||
referencedUser' <- catchIfMExceptT (const $ unauthorizedI MsgUnauthorizedSelf) (const True :: CryptoIDError -> Bool) $ decrypt referencedUser
|
referencedUser' <- catchIfMExceptT (const $ unauthorizedI MsgUnauthorizedSelf) (const True :: CryptoIDError -> Bool) $ decrypt referencedUser
|
||||||
maybeTMExceptT (unauthorizedI MsgUnauthorizedPWHash) $ do
|
maybeTMExceptT (unauthorizedI MsgUnauthorizedInternal) $ do
|
||||||
User{..} <- MaybeT $ get referencedUser'
|
User{..} <- MaybeT $ get referencedUser'
|
||||||
guard $ is _AuthPWHash userAuthentication
|
guard $ is _Just userPasswordHash
|
||||||
return Authorized
|
return Authorized
|
||||||
tagAccessPredicate AuthAuthentication = APDB $ \_ _ mAuthId route _ -> case route of
|
tagAccessPredicate AuthAuthentication = APDB $ \_ _ mAuthId route _ -> case route of
|
||||||
MessageR cID -> maybeT (unauthorizedI MsgUnauthorizedSystemMessageAuth) $ do
|
MessageR cID -> maybeT (unauthorizedI MsgUnauthorizedSystemMessageAuth) $ do
|
||||||
|
|||||||
@ -8,7 +8,7 @@
|
|||||||
-- 3. add constructor to list of module exports
|
-- 3. add constructor to list of module exports
|
||||||
|
|
||||||
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
|
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
|
||||||
{-# OPTIONS_GHC -fno-warn-orphans -fno-warn-unused-top-binds #-}
|
{-# OPTIONS_GHC -fno-warn-orphans #-}
|
||||||
|
|
||||||
module Foundation.I18n
|
module Foundation.I18n
|
||||||
( appLanguages, appLanguagesOpts
|
( appLanguages, appLanguagesOpts
|
||||||
@ -39,7 +39,7 @@ module Foundation.I18n
|
|||||||
, StudyDegreeTerm(..)
|
, StudyDegreeTerm(..)
|
||||||
, ShortStudyFieldType(..)
|
, ShortStudyFieldType(..)
|
||||||
, StudyDegreeTermType(..)
|
, StudyDegreeTermType(..)
|
||||||
, ErrorResponseTitle(..)
|
, ErrorResponseTitle(..)
|
||||||
, UniWorXMessages(..)
|
, UniWorXMessages(..)
|
||||||
, uniworxMessages
|
, uniworxMessages
|
||||||
, unRenderMessage, unRenderMessage', unRenderMessageLenient
|
, unRenderMessage, unRenderMessage', unRenderMessageLenient
|
||||||
@ -87,30 +87,21 @@ pluralDE num singularForm pluralForm
|
|||||||
| num == 1 = singularForm
|
| num == 1 = singularForm
|
||||||
| otherwise = pluralForm
|
| otherwise = pluralForm
|
||||||
|
|
||||||
pluralDEx :: (Eq a, Num a) => Char -> a -> Text -> Text
|
-- pluralDEx :: (Eq a, Num a) => Char -> a -> Text -> Text
|
||||||
pluralDEx c n t = pluralDE n t $ t `snoc` c
|
-- -- ^ @pluralENs n "Monat" = pluralEN n "Monat" "Monate"@
|
||||||
|
-- pluralDEx c n t = pluralDE n t $ t `snoc` c
|
||||||
|
|
||||||
-- | like `pluralDEx` but also prefixes with the number
|
-- -- | like `pluralDEe` but also prefixes with the number
|
||||||
pluralDExN :: (Eq a, Num a, Show a) => Char -> a -> Text -> Text
|
-- pluralDExN :: (Eq a, Num a, Show a) => Char -> a -> Text -> Text
|
||||||
pluralDExN c n t = tshow n <> cons ' ' (pluralDEx c n t)
|
-- pluralDExN c n t = tshow n <> cons ' ' (pluralDEx c n t)
|
||||||
|
|
||||||
pluralDEe :: (Eq a, Num a) => a -> Text -> Text
|
pluralDEe :: (Eq a, Num a) => a -> Text -> Text
|
||||||
-- ^ @pluralDEe n "Monat" = pluralDEe n "Monat" "Monate"@
|
-- ^ @pluralENs n "Monat" = pluralEN n "Monat" "Monate"@
|
||||||
pluralDEe = pluralDEx 'e'
|
pluralDEe n t = pluralDE n t $ t `snoc` 'e'
|
||||||
|
|
||||||
-- | like `pluralDEe` but also prefixes with the number
|
-- | like `pluralDEe` but also prefixes with the number
|
||||||
pluralDEeN :: (Eq a, Num a, Show a) => a -> Text -> Text
|
pluralDEeN :: (Eq a, Num a, Show a) => a -> Text -> Text
|
||||||
pluralDEeN = pluralDExN 'e'
|
pluralDEeN n t = tshow n <> cons ' ' (pluralDEe n t)
|
||||||
|
|
||||||
-- | postfix plural with an 'n'
|
|
||||||
pluralDEn :: (Eq a, Num a) => a -> Text -> Text
|
|
||||||
-- ^ @pluralENs n "Monat" = pluralEN n "Monat" "Monate"@
|
|
||||||
pluralDEn = pluralDEx 'n'
|
|
||||||
|
|
||||||
-- | like `pluralDEn` but also prefixes with the number
|
|
||||||
pluralDEnN :: (Eq a, Num a, Show a) => a -> Text -> Text
|
|
||||||
pluralDEnN = pluralDExN 'n'
|
|
||||||
|
|
||||||
|
|
||||||
noneOneMoreDE :: (Eq a, Num a)
|
noneOneMoreDE :: (Eq a, Num a)
|
||||||
=> a -- ^ Count
|
=> a -- ^ Count
|
||||||
@ -123,14 +114,14 @@ noneOneMoreDE num noneText singularForm pluralForm
|
|||||||
| num == 1 = singularForm
|
| num == 1 = singularForm
|
||||||
| otherwise = pluralForm
|
| otherwise = pluralForm
|
||||||
|
|
||||||
noneMoreDE :: (Eq a, Num a)
|
-- noneMoreDE :: (Eq a, Num a)
|
||||||
=> a -- ^ Count
|
-- => a -- ^ Count
|
||||||
-> Text -- ^ None
|
-- -> Text -- ^ None
|
||||||
-> Text -- ^ Some
|
-- -> Text -- ^ Some
|
||||||
-> Text
|
-- -> Text
|
||||||
noneMoreDE num noneText someText
|
-- noneMoreDE num noneText someText
|
||||||
| num == 0 = noneText
|
-- | num == 0 = noneText
|
||||||
| otherwise = someText
|
-- | otherwise = someText
|
||||||
|
|
||||||
pluralEN :: (Eq a, Num a)
|
pluralEN :: (Eq a, Num a)
|
||||||
=> a -- ^ Count
|
=> a -- ^ Count
|
||||||
@ -145,7 +136,7 @@ pluralENs :: (Eq a, Num a)
|
|||||||
=> a -- ^ Count
|
=> a -- ^ Count
|
||||||
-> Text -- ^ Singular
|
-> Text -- ^ Singular
|
||||||
-> Text
|
-> Text
|
||||||
-- ^ @pluralENs n "foo" = pluralEN n "foo" "foos"@
|
-- ^ @pluralENs n "foo" = pluralEN n "foo" "foos"@
|
||||||
pluralENs n t = pluralEN n t $ t `snoc` 's'
|
pluralENs n t = pluralEN n t $ t `snoc` 's'
|
||||||
|
|
||||||
-- | like `pluralENs` but also prefixes with the number
|
-- | like `pluralENs` but also prefixes with the number
|
||||||
@ -163,14 +154,14 @@ noneOneMoreEN num noneText singularForm pluralForm
|
|||||||
| num == 1 = singularForm
|
| num == 1 = singularForm
|
||||||
| otherwise = pluralForm
|
| otherwise = pluralForm
|
||||||
|
|
||||||
noneMoreEN :: (Eq a, Num a)
|
-- noneMoreEN :: (Eq a, Num a)
|
||||||
=> a -- ^ Count
|
-- => a -- ^ Count
|
||||||
-> Text -- ^ None
|
-- -> Text -- ^ None
|
||||||
-> Text -- ^ Some
|
-- -> Text -- ^ Some
|
||||||
-> Text
|
-- -> Text
|
||||||
noneMoreEN num noneText someText
|
-- noneMoreEN num noneText someText
|
||||||
| num == 0 = noneText
|
-- | num == 0 = noneText
|
||||||
| otherwise = someText
|
-- | otherwise = someText
|
||||||
|
|
||||||
_ordinalEN :: ToMessage a
|
_ordinalEN :: ToMessage a
|
||||||
=> a
|
=> a
|
||||||
@ -190,20 +181,20 @@ notEN :: Bool -> Text
|
|||||||
notEN = bool "not" ""
|
notEN = bool "not" ""
|
||||||
|
|
||||||
{- -- TODO: use this is message eventually
|
{- -- TODO: use this is message eventually
|
||||||
-- Commonly used plurals
|
-- Commonly used plurals
|
||||||
data Thing = Person | Examinee
|
data Thing = Person | Examinee
|
||||||
deriving (Eq)
|
deriving (Eq)
|
||||||
|
|
||||||
thingDE :: Int -> Thing -> Text
|
thingDE :: Int -> Thing -> Text
|
||||||
thingDE num = (tshow num <>) . Text.cons ' ' . thing
|
thingDE num = (tshow num <>) . Text.cons ' ' . thing
|
||||||
where
|
where
|
||||||
thing :: Thing -> Text
|
thing :: Thing -> Text
|
||||||
thing Person = pluralDE num "Person" "Personen"
|
thing Person = pluralDE num "Person" "Personen"
|
||||||
thing Examinee = pluralDE num "Prüfling" "Prüflinge"
|
thing Examinee = pluralDE num "Prüfling" "Prüflinge"
|
||||||
|
|
||||||
thingEN :: Int -> Thing -> Text
|
thingEN :: Int -> Thing -> Text
|
||||||
thingEN num t = tshow num <> Text.cons ' ' (thing t)
|
thingEN num t = tshow num <> Text.cons ' ' (thing t)
|
||||||
where
|
where
|
||||||
thing :: Thing -> Text
|
thing :: Thing -> Text
|
||||||
thing Person = pluralENs num "person"
|
thing Person = pluralENs num "person"
|
||||||
thing Examinee = pluralENs num "examinee"
|
thing Examinee = pluralENs num "examinee"
|
||||||
@ -219,9 +210,6 @@ maybeBoolMessage Nothing n _ _ = n
|
|||||||
maybeBoolMessage (Just True) _ t _ = t
|
maybeBoolMessage (Just True) _ t _ = t
|
||||||
maybeBoolMessage (Just False) _ _ f = f
|
maybeBoolMessage (Just False) _ _ f = f
|
||||||
|
|
||||||
-- | Convenience function avoiding type signatures
|
|
||||||
boolText :: Text -> Text -> Bool -> Text
|
|
||||||
boolText = bool
|
|
||||||
|
|
||||||
newtype ShortTermIdentifier = ShortTermIdentifier TermIdentifier
|
newtype ShortTermIdentifier = ShortTermIdentifier TermIdentifier
|
||||||
deriving stock (Eq, Ord, Read, Show)
|
deriving stock (Eq, Ord, Read, Show)
|
||||||
@ -281,7 +269,7 @@ mkMessageAddition ''UniWorX "Avs" "messages/uniworx/categories/avs" "de-de-forma
|
|||||||
embedRenderMessage ''UniWorX ''LmsStatus (uncurry ((<>) . (<> "Status")) . Text.splitAt 3)
|
embedRenderMessage ''UniWorX ''LmsStatus (uncurry ((<>) . (<> "Status")) . Text.splitAt 3)
|
||||||
|
|
||||||
|
|
||||||
newtype SomeMessages master = SomeMessages [SomeMessage master]
|
newtype SomeMessages master = SomeMessages [SomeMessage master]
|
||||||
deriving newtype (Semigroup, Monoid)
|
deriving newtype (Semigroup, Monoid)
|
||||||
|
|
||||||
instance master ~ master' => RenderMessage master (SomeMessages master') where
|
instance master ~ master' => RenderMessage master (SomeMessages master') where
|
||||||
@ -396,8 +384,6 @@ embedRenderMessage ''UniWorX ''ExamRequiredEquipmentPreset id
|
|||||||
embedRenderMessage ''UniWorX ''ChangelogItemKind id
|
embedRenderMessage ''UniWorX ''ChangelogItemKind id
|
||||||
embedRenderMessage ''UniWorX ''RoomReference' $ dropSuffix "'"
|
embedRenderMessage ''UniWorX ''RoomReference' $ dropSuffix "'"
|
||||||
|
|
||||||
embedRenderMessage ''UniWorX ''AuthenticationMode id
|
|
||||||
|
|
||||||
embedRenderMessage ''UniWorX ''RatingValidityException id
|
embedRenderMessage ''UniWorX ''RatingValidityException id
|
||||||
|
|
||||||
embedRenderMessage ''UniWorX ''UrlFieldMessage id
|
embedRenderMessage ''UniWorX ''UrlFieldMessage id
|
||||||
@ -614,12 +600,12 @@ unRenderMessage = unRenderMessage' (==)
|
|||||||
|
|
||||||
unRenderMessageLenient :: forall a master. (Ord a, Finite a, RenderMessage master a) => master -> Text -> [a]
|
unRenderMessageLenient :: forall a master. (Ord a, Finite a, RenderMessage master a) => master -> Text -> [a]
|
||||||
unRenderMessageLenient = unRenderMessage' cmp
|
unRenderMessageLenient = unRenderMessage' cmp
|
||||||
where cmp = (==) `on` mk . under packed (concatMap $ filter Char.isAlphaNum . unidecode)
|
where cmp = (==) `on` mk . under packed (filter Char.isAlphaNum . concatMap unidecode)
|
||||||
|
|
||||||
|
|
||||||
instance Default DateTimeFormatter where
|
instance Default DateTimeFormatter where
|
||||||
def = mkDateTimeFormatter (getTimeLocale' []) def appTZ
|
def = mkDateTimeFormatter (getTimeLocale' []) def appTZ
|
||||||
|
|
||||||
instance RenderMessage UniWorX Address where
|
instance RenderMessage UniWorX Address where
|
||||||
renderMessage s l a@Address{addressName = Just aname} = aname <> cons ' ' (renderMessage s l a{addressName=Nothing})
|
renderMessage s l a@Address{addressName = Just aname} = aname <> cons ' ' (renderMessage s l a{addressName=Nothing})
|
||||||
renderMessage _ _ Address{addressEmail = mail} = "<" <> mail <> ">"
|
renderMessage _ _ Address{addressEmail = mail} = "<" <> mail <> ">"
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
-- SPDX-FileCopyrightText: 2022 Gregor Kleen <gregor.kleen@ifi.lmu.de>,Sarah Vaupel <sarah.vaupel@ifi.lmu.de>,Steffen Jost <jost@tcs.ifi.lmu.de>,Wolfgang Witt <Wolfgang.Witt@campus.lmu.de>
|
-- SPDX-FileCopyrightText: 2022-2024 Sarah Vaupel <sarah.vaupel@uniworx.de>, Gregor Kleen <gregor.kleen@ifi.lmu.de>, Sarah Vaupel <sarah.vaupel@ifi.lmu.de>, Steffen Jost <jost@tcs.ifi.lmu.de>, Wolfgang Witt <Wolfgang.Witt@campus.lmu.de>, David Mosbach <david.mosbach@uniworx.de>
|
||||||
--
|
--
|
||||||
-- SPDX-License-Identifier: AGPL-3.0-or-later
|
-- SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
@ -11,11 +11,14 @@ module Foundation.Instances
|
|||||||
, unsafeHandler
|
, unsafeHandler
|
||||||
) where
|
) where
|
||||||
|
|
||||||
|
import qualified Prelude as P
|
||||||
|
|
||||||
import Import.NoFoundation
|
import Import.NoFoundation
|
||||||
|
|
||||||
import qualified Data.Text as Text
|
import qualified Data.Text as Text
|
||||||
import Data.List (inits)
|
import Data.List (inits)
|
||||||
|
|
||||||
|
import Yesod.Auth.OAuth2
|
||||||
import qualified Yesod.Core.Unsafe as Unsafe
|
import qualified Yesod.Core.Unsafe as Unsafe
|
||||||
import qualified Yesod.Auth.Message as Auth
|
import qualified Yesod.Auth.Message as Auth
|
||||||
|
|
||||||
@ -23,6 +26,7 @@ import Utils.Form
|
|||||||
import Auth.LDAP
|
import Auth.LDAP
|
||||||
import Auth.PWHash
|
import Auth.PWHash
|
||||||
import Auth.Dummy
|
import Auth.Dummy
|
||||||
|
import Auth.OAuth2
|
||||||
|
|
||||||
import qualified Foundation.Yesod.Session as UniWorX
|
import qualified Foundation.Yesod.Session as UniWorX
|
||||||
import qualified Foundation.Yesod.Middleware as UniWorX
|
import qualified Foundation.Yesod.Middleware as UniWorX
|
||||||
@ -42,6 +46,8 @@ import Foundation.DB
|
|||||||
|
|
||||||
import Network.Wai.Parse (lbsBackEnd)
|
import Network.Wai.Parse (lbsBackEnd)
|
||||||
|
|
||||||
|
import System.Environment (lookupEnv)
|
||||||
|
|
||||||
import UnliftIO.Pool (withResource)
|
import UnliftIO.Pool (withResource)
|
||||||
|
|
||||||
import qualified Control.Monad.State.Class as State
|
import qualified Control.Monad.State.Class as State
|
||||||
@ -119,7 +125,7 @@ instance YesodPersistRunner UniWorX where
|
|||||||
getDBRunner :: HasCallStack => HandlerFor UniWorX (DBRunner UniWorX, HandlerFor UniWorX ())
|
getDBRunner :: HasCallStack => HandlerFor UniWorX (DBRunner UniWorX, HandlerFor UniWorX ())
|
||||||
getDBRunner = UniWorX.getDBRunner' callStack
|
getDBRunner = UniWorX.getDBRunner' callStack
|
||||||
|
|
||||||
|
|
||||||
instance YesodAuth UniWorX where
|
instance YesodAuth UniWorX where
|
||||||
type AuthId UniWorX = UserId
|
type AuthId UniWorX = UserId
|
||||||
|
|
||||||
@ -128,21 +134,30 @@ instance YesodAuth UniWorX where
|
|||||||
-- Where to send a user after logout
|
-- Where to send a user after logout
|
||||||
logoutDest _ = NewsR
|
logoutDest _ = NewsR
|
||||||
-- Override the above two destinations when a Referer: header is present
|
-- Override the above two destinations when a Referer: header is present
|
||||||
redirectToReferer _ = True
|
redirectToReferer _ = False
|
||||||
|
|
||||||
loginHandler = do
|
loginHandler = do
|
||||||
|
plugins <- getsYesod authPlugins
|
||||||
|
AppSettings{..} <- getsYesod appSettings'
|
||||||
|
|
||||||
|
when appSingleSignOn $ do
|
||||||
|
let plugin = P.head $ P.filter ((`elem` [apAzureMock, apAzure]) . apName) plugins
|
||||||
|
pieces = case oauth2Url (apName plugin) of
|
||||||
|
PluginR _ p -> p
|
||||||
|
_ -> error "Unexpected OAuth2 AuthRoute"
|
||||||
|
void $ apDispatch plugin "GET" pieces
|
||||||
|
|
||||||
toParent <- getRouteToParent
|
toParent <- getRouteToParent
|
||||||
liftHandler . defaultLayout $ do
|
liftHandler . defaultLayout $ do
|
||||||
plugins <- getsYesod authPlugins
|
|
||||||
$logDebugS "Auth" $ "Enabled plugins: " <> Text.intercalate ", " (map apName plugins)
|
$logDebugS "Auth" $ "Enabled plugins: " <> Text.intercalate ", " (map apName plugins)
|
||||||
|
mPort <- liftIO $ lookupEnv "OAUTH2_SERVER_PORT"
|
||||||
setTitleI MsgLoginTitle
|
setTitleI MsgLoginTitle
|
||||||
$(widgetFile "login")
|
$(widgetFile "login")
|
||||||
|
|
||||||
authenticate = UniWorX.authenticate
|
authenticate = UniWorX.authenticate
|
||||||
|
|
||||||
authPlugins UniWorX{ appSettings' = AppSettings{..}, appLdapPool } = catMaybes
|
authPlugins UniWorX{ appSettings' = AppSettings{..}, appLdapPool, appAuthPlugins } = appAuthPlugins ++ catMaybes
|
||||||
[ flip campusLogin campusUserFailoverMode <$> appLdapPool
|
[ uncurry ldapLogin <$> appLdapPool
|
||||||
, Just . hashLogin $ pwHashAlgorithm appAuthPWHash
|
, Just . hashLogin $ pwHashAlgorithm appAuthPWHash
|
||||||
, dummyLogin <$ guard appAuthDummyLogin
|
, dummyLogin <$ guard appAuthDummyLogin
|
||||||
]
|
]
|
||||||
@ -157,6 +172,11 @@ instance YesodAuth UniWorX where
|
|||||||
|
|
||||||
addMessage Success . toHtml $ mr Auth.NowLoggedIn
|
addMessage Success . toHtml $ mr Auth.NowLoggedIn
|
||||||
|
|
||||||
|
-- onLogout = do
|
||||||
|
-- AppSettings{..} <- getsYesod appSettings'
|
||||||
|
-- when appSingleSignOn $ singleSignOut @UniWorX Nothing
|
||||||
|
|
||||||
|
|
||||||
onErrorHtml dest msg = do
|
onErrorHtml dest msg = do
|
||||||
addMessage Error $ toHtml msg
|
addMessage Error $ toHtml msg
|
||||||
redirect dest
|
redirect dest
|
||||||
|
|||||||
@ -73,6 +73,8 @@ breadcrumb :: ( BearerAuthSite UniWorX
|
|||||||
=> Route UniWorX
|
=> Route UniWorX
|
||||||
-> m Breadcrumb
|
-> m Breadcrumb
|
||||||
breadcrumb (AuthR _) = i18nCrumb MsgMenuLogin $ Just NewsR
|
breadcrumb (AuthR _) = i18nCrumb MsgMenuLogin $ Just NewsR
|
||||||
|
breadcrumb SOutR = i18nCrumb MsgLogout Nothing
|
||||||
|
breadcrumb SSOutR = i18nCrumb MsgSingleSignOut Nothing
|
||||||
breadcrumb (StaticR _) = i18nCrumb MsgBreadcrumbStatic Nothing
|
breadcrumb (StaticR _) = i18nCrumb MsgBreadcrumbStatic Nothing
|
||||||
breadcrumb (WellKnownR _) = i18nCrumb MsgBreadcrumbWellKnown Nothing
|
breadcrumb (WellKnownR _) = i18nCrumb MsgBreadcrumbWellKnown Nothing
|
||||||
breadcrumb MetricsR = i18nCrumb MsgBreadcrumbMetrics Nothing
|
breadcrumb MetricsR = i18nCrumb MsgBreadcrumbMetrics Nothing
|
||||||
@ -87,9 +89,9 @@ breadcrumb (AdminUserR cID) = useRunDB . maybeT (i18nCrumb MsgBreadcrumbUser $ J
|
|||||||
uid <- decrypt cID
|
uid <- decrypt cID
|
||||||
User{..} <- MaybeT $ get uid
|
User{..} <- MaybeT $ get uid
|
||||||
return (userDisplayName, Just UsersR)
|
return (userDisplayName, Just UsersR)
|
||||||
breadcrumb (AdminUserDeleteR cID) = i18nCrumb MsgBreadcrumbUserDelete . Just $ AdminUserR cID
|
breadcrumb (AdminUserDeleteR cID) = i18nCrumb MsgBreadcrumbUserDelete . Just $ AdminUserR cID
|
||||||
breadcrumb (AdminHijackUserR cID) = i18nCrumb MsgBreadcrumbUserHijack . Just $ AdminUserR cID
|
breadcrumb (AdminHijackUserR cID) = i18nCrumb MsgBreadcrumbUserHijack . Just $ AdminUserR cID
|
||||||
breadcrumb (UserNotificationR cID) = useRunDB $ do
|
breadcrumb (UserNotificationR cID) = useRunDB $ do
|
||||||
mayList <- hasReadAccessTo UsersR
|
mayList <- hasReadAccessTo UsersR
|
||||||
if
|
if
|
||||||
| mayList
|
| mayList
|
||||||
@ -115,14 +117,13 @@ breadcrumb AdminCrontabR = i18nCrumb MsgBreadcrumbAdminCrontab $ Just
|
|||||||
breadcrumb AdminJobsR = i18nCrumb MsgBreadcrumbAdminJobs $ Just AdminCrontabR
|
breadcrumb AdminJobsR = i18nCrumb MsgBreadcrumbAdminJobs $ Just AdminCrontabR
|
||||||
breadcrumb AdminAvsR = i18nCrumb MsgMenuAvs $ Just AdminR
|
breadcrumb AdminAvsR = i18nCrumb MsgMenuAvs $ Just AdminR
|
||||||
breadcrumb AdminAvsUserR{} = i18nCrumb MsgAvsPersonInfo $ Just AdminAvsR
|
breadcrumb AdminAvsUserR{} = i18nCrumb MsgAvsPersonInfo $ Just AdminAvsR
|
||||||
breadcrumb AdminLdapR = i18nCrumb MsgMenuLdap $ Just AdminR
|
breadcrumb AdminExternalUserR = i18nCrumb MsgMenuExternalUser $ Just AdminR
|
||||||
breadcrumb AdminProblemsR = i18nCrumb MsgProblemsHeading $ Just AdminR
|
breadcrumb AdminProblemsR = i18nCrumb MsgProblemsHeading $ Just AdminR
|
||||||
breadcrumb ProblemUnreachableR = i18nCrumb MsgProblemsUnreachableHeading $ Just AdminProblemsR
|
breadcrumb ProblemUnreachableR = i18nCrumb MsgProblemsUnreachableHeading $ Just AdminProblemsR
|
||||||
breadcrumb ProblemWithoutAvsId = i18nCrumb MsgProblemsNoAvsIdHeading $ Just AdminProblemsR
|
breadcrumb ProblemWithoutAvsId = i18nCrumb MsgProblemsNoAvsIdHeading $ Just AdminProblemsR
|
||||||
breadcrumb ProblemFbutNoR = i18nCrumb MsgProblemsRWithoutFHeading $ Just AdminProblemsR
|
breadcrumb ProblemFbutNoR = i18nCrumb MsgProblemsRWithoutFHeading $ Just AdminProblemsR
|
||||||
breadcrumb ProblemAvsSynchR = i18nCrumb MsgProblemsAvsSynchHeading $ Just AdminProblemsR
|
breadcrumb ProblemAvsSynchR = i18nCrumb MsgProblemsAvsSynchHeading $ Just AdminProblemsR
|
||||||
breadcrumb ProblemAvsErrorR = i18nCrumb MsgProblemsAvsErrorHeading $ Just AdminProblemsR
|
breadcrumb ProblemAvsErrorR = i18nCrumb MsgProblemsAvsErrorHeading $ Just AdminProblemsR
|
||||||
breadcrumb ConfigInterfacesR = i18nCrumb MsgConfigInterfacesHeading $ Just AdminProblemsR
|
|
||||||
|
|
||||||
breadcrumb FirmAllR = i18nCrumb MsgMenuFirms Nothing
|
breadcrumb FirmAllR = i18nCrumb MsgMenuFirms Nothing
|
||||||
breadcrumb FirmsCommR{} = i18nCrumb MsgMenuFirmsComm $ Just FirmAllR
|
breadcrumb FirmsCommR{} = i18nCrumb MsgMenuFirmsComm $ Just FirmAllR
|
||||||
@ -130,13 +131,7 @@ breadcrumb FirmUsersR{} = i18nCrumb MsgMenuFirmUsers $ Just FirmAll
|
|||||||
breadcrumb (FirmSupersR fsh)= i18nCrumb MsgMenuFirmSupervisors $ Just $ FirmUsersR fsh
|
breadcrumb (FirmSupersR fsh)= i18nCrumb MsgMenuFirmSupervisors $ Just $ FirmUsersR fsh
|
||||||
breadcrumb (FirmCommR fsh)= i18nCrumb MsgMenuFirmsComm $ Just $ FirmUsersR fsh
|
breadcrumb (FirmCommR fsh)= i18nCrumb MsgMenuFirmsComm $ Just $ FirmUsersR fsh
|
||||||
|
|
||||||
breadcrumb CommCenterR = i18nCrumb MsgMenuCommCenter Nothing
|
breadcrumb PrintCenterR = i18nCrumb MsgMenuApc Nothing
|
||||||
breadcrumb MailCenterR = i18nCrumb MsgMenuMailCenter $ Just CommCenterR
|
|
||||||
breadcrumb MailHtmlR{} = i18nCrumb MsgMenuMailHtml $ Just MailCenterR
|
|
||||||
breadcrumb MailPlainR{} = i18nCrumb MsgMenuMailPlain $ Just MailCenterR
|
|
||||||
breadcrumb (MailAttachmentR mid _) = i18nCrumb MsgMenuMailAttachment $ Just $ MailHtmlR mid
|
|
||||||
|
|
||||||
breadcrumb PrintCenterR = i18nCrumb MsgMenuApc $ Just CommCenterR
|
|
||||||
breadcrumb PrintSendR = i18nCrumb MsgMenuPrintSend $ Just PrintCenterR
|
breadcrumb PrintSendR = i18nCrumb MsgMenuPrintSend $ Just PrintCenterR
|
||||||
breadcrumb PrintDownloadR{} = i18nCrumb MsgMenuPrintDownload $ Just PrintCenterR
|
breadcrumb PrintDownloadR{} = i18nCrumb MsgMenuPrintDownload $ Just PrintCenterR
|
||||||
breadcrumb PrintAckR{} = i18nCrumb MsgMenuPrintSend $ Just PrintCenterR -- never displayed
|
breadcrumb PrintAckR{} = i18nCrumb MsgMenuPrintSend $ Just PrintCenterR -- never displayed
|
||||||
@ -549,42 +544,37 @@ defaultLinks :: ( MonadHandler m
|
|||||||
, BearerAuthSite UniWorX
|
, BearerAuthSite UniWorX
|
||||||
) => m [Nav]
|
) => m [Nav]
|
||||||
defaultLinks = fmap catMaybes . mapM runMaybeT $ -- Define the menu items of the header.
|
defaultLinks = fmap catMaybes . mapM runMaybeT $ -- Define the menu items of the header.
|
||||||
[ return NavHeader
|
[ return NavHeaderContainer
|
||||||
{ navHeaderRole = NavHeaderSecondary
|
{ navHeaderRole = NavHeaderSecondary
|
||||||
, navIcon = IconMenuLogout
|
, navLabel = SomeMessage MsgMenuAccount
|
||||||
, navLink = NavLink
|
, navIcon = IconMenuAccount
|
||||||
{ navLabel = MsgMenuLogout
|
, navChildren =
|
||||||
, navRoute = AuthR LogoutR
|
[ NavLink
|
||||||
, navAccess' = NavAccessHandler $ is _Just <$> maybeAuthId
|
{ navLabel = MsgMenuLogout
|
||||||
, navType = NavTypeLink { navModal = False }
|
, navRoute = SSOutR -- AuthR LogoutR
|
||||||
, navQuick' = mempty
|
, navAccess' = NavAccessHandler $ is _Just <$> maybeAuthId
|
||||||
, navForceActive = False
|
, navType = NavTypeLink { navModal = False }
|
||||||
}
|
, navQuick' = mempty
|
||||||
}
|
, navForceActive = False
|
||||||
, return NavHeader
|
}
|
||||||
{ navHeaderRole = NavHeaderSecondary
|
, NavLink
|
||||||
, navIcon = IconMenuLogin
|
{ navLabel = MsgMenuLogin
|
||||||
, navLink = NavLink
|
, navRoute = AuthR LoginR
|
||||||
{ navLabel = MsgMenuLogin
|
, navAccess' = NavAccessHandler $ is _Nothing <$> maybeAuthId
|
||||||
, navRoute = AuthR LoginR
|
, navType = NavTypeLink { navModal = False }
|
||||||
, navAccess' = NavAccessHandler $ is _Nothing <$> maybeAuthId
|
, navQuick' = mempty
|
||||||
, navType = NavTypeLink { navModal = True }
|
, navForceActive = False
|
||||||
, navQuick' = mempty
|
}
|
||||||
, navForceActive = False
|
, NavLink
|
||||||
}
|
{ navLabel = MsgMenuProfile
|
||||||
}
|
, navRoute = ProfileR
|
||||||
, return NavHeader
|
, navAccess' = NavAccessHandler $ is _Just <$> maybeAuthId
|
||||||
{ navHeaderRole = NavHeaderSecondary
|
, navType = NavTypeLink { navModal = False }
|
||||||
, navIcon = IconMenuProfile
|
, navQuick' = mempty
|
||||||
, navLink = NavLink
|
, navForceActive = False
|
||||||
{ navLabel = MsgMenuProfile
|
}
|
||||||
, navRoute = ProfileR
|
]
|
||||||
, navAccess' = NavAccessHandler $ is _Just <$> maybeAuthId
|
}
|
||||||
, navType = NavTypeLink { navModal = False }
|
|
||||||
, navQuick' = mempty
|
|
||||||
, navForceActive = False
|
|
||||||
}
|
|
||||||
}
|
|
||||||
, do
|
, do
|
||||||
mCurrentRoute <- getCurrentRoute
|
mCurrentRoute <- getCurrentRoute
|
||||||
|
|
||||||
@ -863,8 +853,8 @@ defaultLinks = fmap catMaybes . mapM runMaybeT $ -- Define the menu items of the
|
|||||||
, navForceActive = False
|
, navForceActive = False
|
||||||
}
|
}
|
||||||
, NavLink
|
, NavLink
|
||||||
{ navLabel = MsgMenuLdap
|
{ navLabel = MsgMenuExternalUser
|
||||||
, navRoute = AdminLdapR
|
, navRoute = AdminExternalUserR
|
||||||
, navAccess' = NavAccessTrue
|
, navAccess' = NavAccessTrue
|
||||||
, navType = NavTypeLink { navModal = False }
|
, navType = NavTypeLink { navModal = False }
|
||||||
, navQuick' = mempty
|
, navQuick' = mempty
|
||||||
@ -1221,8 +1211,8 @@ pageActions (AdminUserR cID) = return
|
|||||||
, navRoute = UserPasswordR cID
|
, navRoute = UserPasswordR cID
|
||||||
, navAccess' = NavAccessDB $ do
|
, navAccess' = NavAccessDB $ do
|
||||||
uid <- decrypt cID
|
uid <- decrypt cID
|
||||||
User{userAuthentication} <- get404 uid
|
User{userPasswordHash} <- get404 uid
|
||||||
return $ is _AuthPWHash userAuthentication
|
return $ is _Just userPasswordHash
|
||||||
, navType = NavTypeLink { navModal = True }
|
, navType = NavTypeLink { navModal = True }
|
||||||
, navQuick' = mempty
|
, navQuick' = mempty
|
||||||
, navForceActive = False
|
, navForceActive = False
|
||||||
@ -1464,12 +1454,6 @@ pageActions (ForProfileR cID) = return
|
|||||||
, navChildren = []
|
, navChildren = []
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
pageActions (ForProfileDataR cID) = return
|
|
||||||
[ NavPageActionPrimary
|
|
||||||
{ navLink = defNavLink MsgAdminUserHeading $ AdminUserR cID
|
|
||||||
, navChildren = []
|
|
||||||
}
|
|
||||||
]
|
|
||||||
pageActions TermShowR = do
|
pageActions TermShowR = do
|
||||||
participantsSecondary <- pageQuickActions NavQuickViewPageActionSecondary ParticipantsListR
|
participantsSecondary <- pageQuickActions NavQuickViewPageActionSecondary ParticipantsListR
|
||||||
return
|
return
|
||||||
@ -2484,50 +2468,6 @@ pageActions PrintCenterR = do
|
|||||||
dayLinks <- mapM toDayAck $ Map.toAscList dayMap
|
dayLinks <- mapM toDayAck $ Map.toAscList dayMap
|
||||||
return $ manualSend : printLog : printAck : take 9 dayLinks
|
return $ manualSend : printLog : printAck : take 9 dayLinks
|
||||||
|
|
||||||
pageActions CommCenterR = return
|
|
||||||
[ NavPageActionPrimary
|
|
||||||
{ navLink = defNavLink MsgMenuMailCenter MailCenterR
|
|
||||||
, navChildren = []
|
|
||||||
}
|
|
||||||
, NavPageActionPrimary
|
|
||||||
{ navLink = defNavLink MsgMenuApc PrintCenterR
|
|
||||||
, navChildren = []
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
pageActions (MailHtmlR smid) = do
|
|
||||||
sid <- decrypt smid
|
|
||||||
usrNotiSettings <- useRunDB $ runMaybeT $ do
|
|
||||||
sm <- MaybeT $ get sid
|
|
||||||
uid <- hoistMaybe $ sentMailRecipient sm
|
|
||||||
User{userDisplayName} <- MaybeT $ get uid
|
|
||||||
uuid <- liftHandler $ encrypt uid
|
|
||||||
return NavPageActionPrimary
|
|
||||||
{ navLink = defNavLink (MsgNotificationSettingsHeading userDisplayName) $ UserNotificationR uuid
|
|
||||||
, navChildren = []
|
|
||||||
}
|
|
||||||
let linkPlain = NavPageActionPrimary
|
|
||||||
{ navLink = defNavLink MsgMenuMailPlain $ MailPlainR smid
|
|
||||||
, navChildren = []
|
|
||||||
}
|
|
||||||
return $ msnoc [linkPlain] usrNotiSettings
|
|
||||||
pageActions (MailPlainR smid) = do
|
|
||||||
sid <- decrypt smid
|
|
||||||
usrNotiSettings <- useRunDB $ runMaybeT $ do
|
|
||||||
sm <- MaybeT $ get sid
|
|
||||||
uid <- hoistMaybe $ sentMailRecipient sm
|
|
||||||
User{userDisplayName} <- MaybeT $ get uid
|
|
||||||
uuid <- liftHandler $ encrypt uid
|
|
||||||
return NavPageActionPrimary
|
|
||||||
{ navLink = defNavLink (MsgNotificationSettingsHeading userDisplayName) $ UserNotificationR uuid
|
|
||||||
, navChildren = []
|
|
||||||
}
|
|
||||||
let linkHtml = NavPageActionPrimary
|
|
||||||
{ navLink = defNavLink MsgMenuMailHtml $ MailHtmlR smid
|
|
||||||
, navChildren = []
|
|
||||||
}
|
|
||||||
return $ msnoc [linkHtml] usrNotiSettings
|
|
||||||
|
|
||||||
pageActions AdminCrontabR = return
|
pageActions AdminCrontabR = return
|
||||||
[ NavPageActionPrimary
|
[ NavPageActionPrimary
|
||||||
{ navLink = defNavLink MsgMenuAdminJobs AdminJobsR
|
{ navLink = defNavLink MsgMenuAdminJobs AdminJobsR
|
||||||
@ -2535,20 +2475,6 @@ pageActions AdminCrontabR = return
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
pageActions AdminProblemsR = return
|
|
||||||
[ NavPageActionPrimary
|
|
||||||
{ navLink = defNavLink MsgConfigInterfacesHeading ConfigInterfacesR
|
|
||||||
, navChildren = []
|
|
||||||
}
|
|
||||||
, NavPageActionPrimary
|
|
||||||
{ navLink = defNavLink MsgProblemsAvsSynchHeading ProblemAvsSynchR
|
|
||||||
, navChildren = []
|
|
||||||
}
|
|
||||||
, NavPageActionSecondary
|
|
||||||
{ navLink = defNavLink MsgProblemsAvsErrorHeading ProblemAvsErrorR
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
pageActions _ = return []
|
pageActions _ = return []
|
||||||
|
|
||||||
submissionList :: ( MonadIO m
|
submissionList :: ( MonadIO m
|
||||||
|
|||||||
@ -9,7 +9,7 @@ module Foundation.Routes
|
|||||||
( module Foundation.Routes.Definitions
|
( module Foundation.Routes.Definitions
|
||||||
, module Foundation.Routes
|
, module Foundation.Routes
|
||||||
) where
|
) where
|
||||||
|
|
||||||
import Import.NoFoundation
|
import Import.NoFoundation
|
||||||
import Foundation.Type
|
import Foundation.Type
|
||||||
|
|
||||||
|
|||||||
@ -156,6 +156,10 @@ siteLayout' overrideHeading widget = do
|
|||||||
-- isParent r = r == (fst parents)
|
-- isParent r = r == (fst parents)
|
||||||
|
|
||||||
isAuth <- isJust <$> maybeAuthId
|
isAuth <- isJust <$> maybeAuthId
|
||||||
|
|
||||||
|
when (appAutoSignOn && not isAuth) $ do
|
||||||
|
$logDebugS "AutoSignOn" "AutoSignOn is enabled in AppSettings and user is not authenticated"
|
||||||
|
redirect $ AuthR LoginR
|
||||||
|
|
||||||
now <- liftIO getCurrentTime
|
now <- liftIO getCurrentTime
|
||||||
|
|
||||||
|
|||||||
@ -15,7 +15,7 @@ module Foundation.Type
|
|||||||
, _memcachedLocalARC
|
, _memcachedLocalARC
|
||||||
, SMTPPool
|
, SMTPPool
|
||||||
, _appSettings', _appStatic, _appConnPool, _appSmtpPool, _appLdapPool, _appWidgetMemcached, _appHttpManager, _appLogger, _appLogSettings, _appCryptoIDKey, _appClusterID, _appInstanceID, _appJobState, _appSessionStore, _appSecretBoxKey, _appJSONWebKeySet, _appHealthReport, _appMemcached, _appUploadCache, _appVerpSecret, _appAuthKey, _appPersonalisedSheetFilesSeedKey, _appVolatileClusterSettingsCache, _appAvsQuery
|
, _appSettings', _appStatic, _appConnPool, _appSmtpPool, _appLdapPool, _appWidgetMemcached, _appHttpManager, _appLogger, _appLogSettings, _appCryptoIDKey, _appClusterID, _appInstanceID, _appJobState, _appSessionStore, _appSecretBoxKey, _appJSONWebKeySet, _appHealthReport, _appMemcached, _appUploadCache, _appVerpSecret, _appAuthKey, _appPersonalisedSheetFilesSeedKey, _appVolatileClusterSettingsCache, _appAvsQuery
|
||||||
, DB, DBRead, Form, MsgRenderer, MailM, DBFile
|
, DB, Form, MsgRenderer, MailM, DBFile
|
||||||
) where
|
) where
|
||||||
|
|
||||||
import Import.NoFoundation
|
import Import.NoFoundation
|
||||||
@ -43,7 +43,7 @@ import Data.Time.Clock.POSIX (POSIXTime)
|
|||||||
import GHC.Fingerprint (Fingerprint)
|
import GHC.Fingerprint (Fingerprint)
|
||||||
import Handler.Sheet.PersonalisedFiles.Types (PersonalisedSheetFilesSeedKey)
|
import Handler.Sheet.PersonalisedFiles.Types (PersonalisedSheetFilesSeedKey)
|
||||||
|
|
||||||
import Utils.Avs (AvsQuery())
|
import Utils.Avs (AvsQuery)
|
||||||
|
|
||||||
|
|
||||||
type SMTPPool = Pool SMTPConnection
|
type SMTPPool = Pool SMTPConnection
|
||||||
@ -79,7 +79,7 @@ data UniWorX = UniWorX
|
|||||||
, appStatic :: EmbeddedStatic -- ^ Settings for static file serving.
|
, appStatic :: EmbeddedStatic -- ^ Settings for static file serving.
|
||||||
, appConnPool :: forall m. MonadIO m => Custom.Pool' m DBConnLabel DBConnUseState SqlBackend -- ^ Database connection pool.
|
, appConnPool :: forall m. MonadIO m => Custom.Pool' m DBConnLabel DBConnUseState SqlBackend -- ^ Database connection pool.
|
||||||
, appSmtpPool :: Maybe SMTPPool
|
, appSmtpPool :: Maybe SMTPPool
|
||||||
, appLdapPool :: Maybe (Failover (LdapConf, LdapPool))
|
, appLdapPool :: Maybe (LdapConf, LdapPool) -- TODO: reintroduce Failover
|
||||||
, appWidgetMemcached :: Maybe Memcached.Connection -- ^ Actually a proper pool
|
, appWidgetMemcached :: Maybe Memcached.Connection -- ^ Actually a proper pool
|
||||||
, appHttpManager :: Manager
|
, appHttpManager :: Manager
|
||||||
, appLogger :: (ReleaseKey, TVar Logger)
|
, appLogger :: (ReleaseKey, TVar Logger)
|
||||||
@ -97,6 +97,7 @@ data UniWorX = UniWorX
|
|||||||
, appUploadCache :: Maybe MinioConn
|
, appUploadCache :: Maybe MinioConn
|
||||||
, appVerpSecret :: VerpSecret
|
, appVerpSecret :: VerpSecret
|
||||||
, appAuthKey :: Auth.Key
|
, appAuthKey :: Auth.Key
|
||||||
|
, appAuthPlugins :: [AuthPlugin UniWorX]
|
||||||
, appFileSourceARC :: Maybe (ARCHandle (FileContentChunkReference, (Int, Int)) Int ByteString)
|
, appFileSourceARC :: Maybe (ARCHandle (FileContentChunkReference, (Int, Int)) Int ByteString)
|
||||||
, appFileSourcePrewarm :: Maybe (LRUHandle (FileContentChunkReference, (Int, Int)) UTCTime Int ByteString)
|
, appFileSourcePrewarm :: Maybe (LRUHandle (FileContentChunkReference, (Int, Int)) UTCTime Int ByteString)
|
||||||
, appFileInjectInhibit :: TVar (IntervalMap UTCTime (Set FileContentReference))
|
, appFileInjectInhibit :: TVar (IntervalMap UTCTime (Set FileContentReference))
|
||||||
@ -123,9 +124,8 @@ instance HasCookieSettings RegisteredCookie UniWorX where
|
|||||||
instance (MonadHandler m, HandlerSite m ~ UniWorX) => ReadLogSettings m where
|
instance (MonadHandler m, HandlerSite m ~ UniWorX) => ReadLogSettings m where
|
||||||
readLogSettings = liftIO . readTVarIO =<< getsYesod (view _appLogSettings)
|
readLogSettings = liftIO . readTVarIO =<< getsYesod (view _appLogSettings)
|
||||||
|
|
||||||
|
|
||||||
type DB = YesodDB UniWorX
|
type DB = YesodDB UniWorX
|
||||||
type DBRead = ReaderT SqlReadBackend (HandlerFor UniWorX)
|
|
||||||
type Form x = Html -> MForm (HandlerFor UniWorX) (FormResult x, WidgetFor UniWorX ())
|
type Form x = Html -> MForm (HandlerFor UniWorX) (FormResult x, WidgetFor UniWorX ())
|
||||||
type MsgRenderer = MsgRendererS UniWorX -- see Utils
|
type MsgRenderer = MsgRendererS UniWorX -- see Utils
|
||||||
type MailM a = MailT (HandlerFor UniWorX) a
|
type MailM a = MailT (HandlerFor UniWorX) a
|
||||||
|
|||||||
@ -1,23 +1,44 @@
|
|||||||
-- SPDX-FileCopyrightText: 2022 Gregor Kleen <gregor.kleen@ifi.lmu.de>
|
-- SPDX-FileCopyrightText: 2023-2024 Sarah Vaupel <sarah.vaupel@uniworx.de>, Gregor Kleen <gregor.kleen@ifi.lmu.de>, David Mosbach <david.mosbach@uniworx.de>
|
||||||
--
|
--
|
||||||
-- SPDX-License-Identifier: AGPL-3.0-or-later
|
-- SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
module Foundation.Types
|
module Foundation.Types
|
||||||
( UpsertCampusUserMode(..)
|
( UpsertUserMode(..)
|
||||||
, _UpsertCampusUserLoginLdap, _UpsertCampusUserLoginDummy, _UpsertCampusUserLoginOther, _UpsertCampusUserLdapSync, _UpsertCampusUserGuessUser
|
, _UpsertUserLogin, _UpsertUserLoginDummy, _UpsertUserLoginOther, _UpsertUserSync, _UpsertUserGuessUser
|
||||||
, _upsertCampusUserIdent
|
, _upsertUserSource, _upsertUserIdent
|
||||||
|
, UpsertUserData(..)
|
||||||
|
, _UpsertUserDataAzure, _UpsertUserDataLdap
|
||||||
|
, _upsertUserAzureTenantId, _upsertUserAzureData, _upsertUserLdapHost, _upsertUserLdapData
|
||||||
) where
|
) where
|
||||||
|
|
||||||
import Import.NoFoundation
|
import Import.NoFoundation
|
||||||
|
|
||||||
|
import qualified Ldap.Client as Ldap
|
||||||
|
|
||||||
data UpsertCampusUserMode
|
|
||||||
= UpsertCampusUserLoginLdap
|
|
||||||
| UpsertCampusUserLoginDummy { upsertCampusUserIdent :: UserIdent }
|
|
||||||
| UpsertCampusUserLoginOther { upsertCampusUserIdent :: UserIdent } -- erlaubt keinen späteren Login
|
|
||||||
| UpsertCampusUserLdapSync { upsertCampusUserIdent :: UserIdent }
|
|
||||||
| UpsertCampusUserGuessUser
|
|
||||||
deriving (Eq, Ord, Read, Show, Generic)
|
|
||||||
|
|
||||||
makeLenses_ ''UpsertCampusUserMode
|
-- TODO: rename?
|
||||||
makePrisms ''UpsertCampusUserMode
|
data UpsertUserMode
|
||||||
|
= UpsertUserLogin { upsertUserSource :: Text } -- TODO: use type synonym?
|
||||||
|
| UpsertUserLoginDummy { upsertUserIdent :: UserIdent }
|
||||||
|
| UpsertUserLoginOther { upsertUserIdent :: UserIdent } -- does not allow further login
|
||||||
|
| UpsertUserSync { upsertUserIdent :: UserIdent }
|
||||||
|
| UpsertUserGuessUser
|
||||||
|
deriving (Show)
|
||||||
|
|
||||||
|
makeLenses_ ''UpsertUserMode
|
||||||
|
makePrisms ''UpsertUserMode
|
||||||
|
|
||||||
|
|
||||||
|
data UpsertUserData
|
||||||
|
= UpsertUserDataAzure
|
||||||
|
{ upsertUserAzureTenantId :: UUID
|
||||||
|
, upsertUserAzureData :: [(Text, [ByteString])] -- TODO: use type synonym?
|
||||||
|
}
|
||||||
|
| UpsertUserDataLdap
|
||||||
|
{ upsertUserLdapHost :: Text
|
||||||
|
, upsertUserLdapData :: Ldap.AttrList []
|
||||||
|
}
|
||||||
|
deriving (Show)
|
||||||
|
|
||||||
|
makeLenses_ ''UpsertUserData
|
||||||
|
makePrisms ''UpsertUserData
|
||||||
|
|||||||
@ -1,69 +1,66 @@
|
|||||||
-- SPDX-FileCopyrightText: 2022 Gregor Kleen <gregor.kleen@ifi.lmu.de>,Steffen Jost <jost@cip.ifi.lmu.de>,Steffen Jost <jost@tcs.ifi.lmu.de>
|
-- SPDX-FileCopyrightText: 2022-2024 Sarah Vaupel <sarah.vaupel@uniworx.de>, Gregor Kleen <gregor.kleen@ifi.lmu.de>, Steffen Jost <jost@cip.ifi.lmu.de>, Steffen Jost <jost@tcs.ifi.lmu.de>, David Mosbach <david.mosbach@uniworx.de>
|
||||||
--
|
--
|
||||||
-- SPDX-License-Identifier: AGPL-3.0-or-later
|
-- SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
module Foundation.Yesod.Auth
|
module Foundation.Yesod.Auth
|
||||||
( authenticate
|
( authenticate
|
||||||
, ldapLookupAndUpsert
|
, userLookupAndUpsert
|
||||||
, upsertCampusUser
|
, upsertUser, maybeUpsertUser
|
||||||
, decodeUserTest
|
, decodeUserTest
|
||||||
, CampusUserConversionException(..)
|
, DecodeUserException(..)
|
||||||
, campusUserFailoverMode, updateUserLanguage
|
, updateUserLanguage
|
||||||
) where
|
) where
|
||||||
|
|
||||||
import Import.NoFoundation hiding (authenticate)
|
import Import.NoFoundation hiding (authenticate)
|
||||||
|
|
||||||
|
import Auth.Dummy (apDummy)
|
||||||
|
import Auth.LDAP
|
||||||
|
import Auth.OAuth2
|
||||||
|
import Auth.PWHash (apHash)
|
||||||
|
|
||||||
import Foundation.Type
|
import Foundation.Type
|
||||||
import Foundation.Types
|
import Foundation.Types
|
||||||
import Foundation.I18n
|
import Foundation.I18n
|
||||||
|
|
||||||
import Handler.Utils.Profile
|
-- import Handler.Utils.Profile
|
||||||
import Handler.Utils.LdapSystemFunctions
|
import Handler.Utils.LdapSystemFunctions
|
||||||
import Handler.Utils.Memcached
|
import Handler.Utils.Memcached
|
||||||
import Foundation.Authorization (AuthorizationCacheKey(..))
|
import Foundation.Authorization (AuthorizationCacheKey(..))
|
||||||
|
|
||||||
import Yesod.Auth.Message
|
import Yesod.Auth.Message
|
||||||
import Auth.LDAP
|
import Yesod.Auth.OAuth2 (getAccessToken, getRefreshToken)
|
||||||
import Auth.PWHash (apHash)
|
|
||||||
import Auth.Dummy (apDummy)
|
|
||||||
|
|
||||||
import qualified Data.CaseInsensitive as CI
|
|
||||||
import qualified Control.Monad.Catch as C (Handler(..))
|
import qualified Control.Monad.Catch as C (Handler(..))
|
||||||
import qualified Ldap.Client as Ldap
|
|
||||||
|
import qualified Data.ByteString as ByteString
|
||||||
|
import qualified Data.CaseInsensitive as CI
|
||||||
|
import qualified Data.Map as Map
|
||||||
|
import qualified Data.List.NonEmpty as NonEmpty (toList)
|
||||||
|
import qualified Data.Set as Set
|
||||||
import qualified Data.Text as Text
|
import qualified Data.Text as Text
|
||||||
import qualified Data.Text.Encoding as Text
|
import qualified Data.Text.Encoding as Text
|
||||||
import qualified Data.ByteString as ByteString
|
|
||||||
import qualified Data.Set as Set
|
|
||||||
import qualified Data.Map as Map
|
|
||||||
-- import qualified Data.Conduit.Combinators as C
|
|
||||||
|
|
||||||
-- import qualified Data.List as List ((\\))
|
import qualified Ldap.Client as Ldap
|
||||||
|
|
||||||
-- import qualified Data.UUID as UUID
|
|
||||||
-- import Data.ByteArray (convert)
|
|
||||||
-- import Crypto.Hash (SHAKE128)
|
|
||||||
-- import qualified Data.Binary as Binary
|
|
||||||
|
|
||||||
-- import qualified Database.Esqueleto.Legacy as E
|
|
||||||
-- import qualified Database.Esqueleto.Utils as E
|
|
||||||
|
|
||||||
-- import Crypto.Hash.Conduit (sinkHash)
|
|
||||||
|
|
||||||
|
|
||||||
authenticate :: ( MonadHandler m, HandlerSite m ~ UniWorX
|
authenticate :: ( MonadHandler m, HandlerSite m ~ UniWorX
|
||||||
, YesodPersist UniWorX, BackendCompatible SqlBackend (YesodPersistBackend UniWorX)
|
, YesodPersist UniWorX, BackendCompatible SqlBackend (YesodPersistBackend UniWorX)
|
||||||
, YesodAuth UniWorX, UserId ~ AuthId UniWorX
|
, YesodAuth UniWorX, UserId ~ AuthId UniWorX
|
||||||
)
|
)
|
||||||
=> Creds UniWorX -> m (AuthenticationResult UniWorX)
|
=> Creds UniWorX
|
||||||
|
-> m (AuthenticationResult UniWorX)
|
||||||
authenticate creds@Creds{..} = liftHandler . runDB . withReaderT projectBackend $ do
|
authenticate creds@Creds{..} = liftHandler . runDB . withReaderT projectBackend $ do
|
||||||
|
$logDebugS "Auth Debug" $ "\a\27[31m" <> tshow creds <> "\27[0m"
|
||||||
|
setSessionJson SessionOAuth2Token (getAccessToken creds, getRefreshToken creds)
|
||||||
|
|
||||||
now <- liftIO getCurrentTime
|
now <- liftIO getCurrentTime
|
||||||
|
|
||||||
let
|
let
|
||||||
uAuth = UniqueAuthentication $ CI.mk credsIdent
|
uAuth = UniqueAuthentication $ CI.mk credsIdent
|
||||||
upsertMode = creds ^? _upsertCampusUserMode
|
upsertMode = creds ^? _upsertUserMode
|
||||||
|
|
||||||
isDummy = is (_Just . _UpsertCampusUserLoginDummy) upsertMode
|
isDummy = is (_Just . _UpsertUserLoginDummy) upsertMode
|
||||||
isOther = is (_Just . _UpsertCampusUserLoginOther) upsertMode
|
isOther = is (_Just . _UpsertUserLoginOther) upsertMode
|
||||||
|
|
||||||
excRecovery res
|
excRecovery res
|
||||||
| isDummy || isOther
|
| isDummy || isOther
|
||||||
@ -77,21 +74,21 @@ authenticate creds@Creds{..} = liftHandler . runDB . withReaderT projectBackend
|
|||||||
= return res
|
= return res
|
||||||
|
|
||||||
excHandlers =
|
excHandlers =
|
||||||
[ C.Handler $ \case
|
[ C.Handler $ \(fExc :: FetchUserDataException) -> case fExc of
|
||||||
CampusUserNoResult -> do
|
FetchUserDataNoResult -> do
|
||||||
$logWarnS "LDAP" $ "User lookup failed after successful login for " <> credsIdent
|
$logWarnS "FetchUserException" $ "User lookup failed after successful login for " <> credsIdent
|
||||||
excRecovery . UserError $ IdentifierNotFound credsIdent
|
excRecovery . UserError $ IdentifierNotFound credsIdent
|
||||||
CampusUserAmbiguous -> do
|
FetchUserDataAmbiguous -> do
|
||||||
$logWarnS "LDAP" $ "Multiple LDAP results for " <> credsIdent
|
$logWarnS "FetchUserException" $ "Multiple User results for " <> credsIdent
|
||||||
excRecovery . UserError $ IdentifierNotFound credsIdent
|
excRecovery . UserError $ IdentifierNotFound credsIdent
|
||||||
err -> do
|
err -> do
|
||||||
$logErrorS "LDAP" $ tshow err
|
$logErrorS "FetchUserException" $ tshow err
|
||||||
mr <- getMessageRender
|
mr <- getMessageRender
|
||||||
excRecovery . ServerError $ mr MsgInternalLdapError
|
excRecovery . ServerError $ mr MsgInternalLoginError
|
||||||
, C.Handler $ \(cExc :: CampusUserConversionException) -> do
|
, C.Handler $ \(dExc :: DecodeUserException) -> do
|
||||||
$logErrorS "LDAP" $ tshow cExc
|
$logErrorS "Auth" $ tshow dExc
|
||||||
mr <- getMessageRender
|
mr <- getMessageRender
|
||||||
excRecovery . ServerError $ mr cExc
|
excRecovery . ServerError $ mr dExc
|
||||||
]
|
]
|
||||||
|
|
||||||
acceptExisting :: SqlPersistT (HandlerFor UniWorX) (AuthenticationResult UniWorX)
|
acceptExisting :: SqlPersistT (HandlerFor UniWorX) (AuthenticationResult UniWorX)
|
||||||
@ -107,230 +104,300 @@ authenticate creds@Creds{..} = liftHandler . runDB . withReaderT projectBackend
|
|||||||
| not isDummy -> res <$ update uid [ UserLastAuthentication =. Just now ]
|
| not isDummy -> res <$ update uid [ UserLastAuthentication =. Just now ]
|
||||||
_other -> return res
|
_other -> return res
|
||||||
|
|
||||||
$logDebugS "auth" $ tshow Creds{..}
|
$logDebugS "Auth" $ tshow Creds{..}
|
||||||
ldapPool' <- getsYesod $ view _appLdapPool
|
|
||||||
|
|
||||||
flip catches excHandlers $ case ldapPool' of
|
flip catches excHandlers $ if
|
||||||
Just ldapPool
|
| not isDummy, not isOther
|
||||||
| Just upsertMode' <- upsertMode -> do
|
, Just upsertMode' <- upsertMode -> fetchUserData Creds{..} >>= \case
|
||||||
ldapData <- campusUser ldapPool campusUserFailoverMode Creds{..}
|
Just userData -> do
|
||||||
$logDebugS "LDAP" $ "Successful LDAP lookup: " <> tshow ldapData
|
$logDebugS "Auth" $ "Successful user data lookup: " <> tshow userData
|
||||||
Authenticated . entityKey <$> upsertCampusUser upsertMode' ldapData
|
Authenticated . entityKey <$> upsertUser upsertMode' userData
|
||||||
_other
|
Nothing
|
||||||
|
-> throwM FetchUserDataNoResult
|
||||||
|
| otherwise
|
||||||
-> acceptExisting
|
-> acceptExisting
|
||||||
|
|
||||||
|
|
||||||
data CampusUserConversionException
|
data DecodeUserException
|
||||||
= CampusUserInvalidIdent
|
= DecodeUserInvalidIdent
|
||||||
| CampusUserInvalidEmail
|
| DecodeUserInvalidEmail
|
||||||
| CampusUserInvalidDisplayName
|
| DecodeUserInvalidDisplayName
|
||||||
| CampusUserInvalidGivenName
|
| DecodeUserInvalidGivenName
|
||||||
| CampusUserInvalidSurname
|
| DecodeUserInvalidSurname
|
||||||
| CampusUserInvalidTitle
|
| DecodeUserInvalidTitle
|
||||||
-- | CampusUserInvalidMatriculation
|
| DecodeUserInvalidFeaturesOfStudy Text
|
||||||
| CampusUserInvalidFeaturesOfStudy Text
|
| DecodeUserInvalidAssociatedSchools Text
|
||||||
| CampusUserInvalidAssociatedSchools Text
|
|
||||||
deriving (Eq, Ord, Read, Show, Generic)
|
deriving (Eq, Ord, Read, Show, Generic)
|
||||||
deriving anyclass (Exception)
|
deriving anyclass (Exception)
|
||||||
|
|
||||||
_upsertCampusUserMode :: Traversal' (Creds UniWorX) UpsertCampusUserMode
|
|
||||||
_upsertCampusUserMode mMode cs@Creds{..}
|
_upsertUserMode :: Traversal' (Creds UniWorX) UpsertUserMode
|
||||||
| credsPlugin == apDummy = setMode <$> mMode (UpsertCampusUserLoginDummy $ CI.mk credsIdent)
|
_upsertUserMode mMode cs@Creds{..}
|
||||||
| credsPlugin == apLdap = setMode <$> mMode UpsertCampusUserLoginLdap
|
| credsPlugin == apDummy = setMode <$> mMode (UpsertUserLoginDummy $ CI.mk credsIdent)
|
||||||
| otherwise = setMode <$> mMode (UpsertCampusUserLoginOther $ CI.mk credsIdent)
|
| credsPlugin `elem` loginAPs
|
||||||
|
= setMode <$> mMode (UpsertUserLogin credsPlugin)
|
||||||
|
| otherwise = setMode <$> mMode (UpsertUserLoginOther $ CI.mk credsIdent)
|
||||||
where
|
where
|
||||||
setMode UpsertCampusUserLoginLdap
|
setMode UpsertUserLogin{..} | upsertUserSource `elem` loginAPs
|
||||||
= cs{ credsPlugin = apLdap }
|
= cs { credsPlugin = upsertUserSource }
|
||||||
setMode (UpsertCampusUserLoginDummy ident)
|
setMode UpsertUserLoginDummy{..}
|
||||||
= cs{ credsPlugin = apDummy
|
= cs { credsPlugin = apDummy
|
||||||
, credsIdent = CI.original ident
|
, credsIdent = CI.original upsertUserIdent
|
||||||
}
|
}
|
||||||
setMode (UpsertCampusUserLoginOther ident)
|
setMode UpsertUserLoginOther{..}
|
||||||
= cs{ credsPlugin = bool defaultOther credsPlugin (credsPlugin /= apDummy && credsPlugin /= apLdap)
|
= cs { credsPlugin = bool defaultOther credsPlugin (credsPlugin `notElem` [apDummy, apLdap, apAzure])
|
||||||
, credsIdent = CI.original ident
|
, credsIdent = CI.original upsertUserIdent
|
||||||
}
|
}
|
||||||
setMode _ = cs
|
setMode _ = cs
|
||||||
|
|
||||||
|
loginAPs = [ apAzure, apLdap ]
|
||||||
defaultOther = apHash
|
defaultOther = apHash
|
||||||
|
|
||||||
ldapLookupAndUpsert :: forall m. (MonadHandler m, HandlerSite m ~ UniWorX, MonadMask m, MonadUnliftIO m) => Text -> SqlPersistT m (Entity User)
|
|
||||||
ldapLookupAndUpsert ident =
|
|
||||||
getsYesod (view _appLdapPool) >>= \case
|
|
||||||
Nothing -> throwM $ CampusUserLdapError $ LdapHostNotResolved "No LDAP configuration in Foundation."
|
|
||||||
Just ldapPool ->
|
|
||||||
campusUser'' ldapPool campusUserFailoverMode ident >>= \case
|
|
||||||
Nothing -> throwM CampusUserNoResult
|
|
||||||
Just ldapResponse -> upsertCampusUser UpsertCampusUserGuessUser ldapResponse
|
|
||||||
|
|
||||||
{- THIS FUNCION JUST DECODES, BUT IT DOES NOT QUERY LDAP!
|
userLookupAndUpsert :: forall m.
|
||||||
upsertCampusUserByCn :: forall m.
|
( MonadHandler m
|
||||||
( MonadHandler m, HandlerSite m ~ UniWorX
|
, HandlerSite m ~ UniWorX
|
||||||
, MonadThrow m
|
, MonadMask m
|
||||||
)
|
, MonadUnliftIO m
|
||||||
=> Text -> SqlPersistT m (Entity User)
|
)
|
||||||
upsertCampusUserByCn persNo = upsertCampusUser UpsertCampusUserGuessUser [(ldapPrimaryKey,[Text.encodeUtf8 persNo])]
|
=> Text
|
||||||
-}
|
-> UpsertUserMode
|
||||||
|
-> SqlPersistT m (Maybe (Entity User))
|
||||||
|
userLookupAndUpsert credsIdent mode =
|
||||||
|
fetchUserData Creds{credsPlugin=mempty,credsExtra=mempty,..} >>= maybeUpsertUser mode
|
||||||
|
|
||||||
-- | Upsert User DB according to given LDAP data (does not query LDAP itself)
|
|
||||||
upsertCampusUser :: forall m.
|
data FetchUserDataException
|
||||||
( MonadHandler m, HandlerSite m ~ UniWorX
|
= FetchUserDataNoResult
|
||||||
, MonadCatch m
|
| FetchUserDataAmbiguous
|
||||||
)
|
| FetchUserDataException
|
||||||
=> UpsertCampusUserMode -> Ldap.AttrList [] -> SqlPersistT m (Entity User)
|
deriving (Eq, Ord, Read, Show, Generic)
|
||||||
upsertCampusUser upsertMode ldapData = do
|
deriving anyclass (Exception)
|
||||||
|
|
||||||
|
-- | Fetch user data with given credentials from external source(s)
|
||||||
|
fetchUserData :: forall m site.
|
||||||
|
( MonadHandler m
|
||||||
|
, HandlerSite m ~ UniWorX
|
||||||
|
, MonadCatch m
|
||||||
|
, MonadMask m
|
||||||
|
, MonadUnliftIO m
|
||||||
|
)
|
||||||
|
=> Creds site
|
||||||
|
-> SqlPersistT m (Maybe (NonEmpty UpsertUserData))
|
||||||
|
fetchUserData Creds{..} = do
|
||||||
|
userAuthConf <- getsYesod $ view _appUserAuthConf
|
||||||
|
now <- liftIO getCurrentTime
|
||||||
|
|
||||||
|
results :: Maybe (NonEmpty UpsertUserData) <- case userAuthConf of
|
||||||
|
UserAuthConfSingleSource{..} -> fmap (:| []) <$> case userAuthConfSingleSource of
|
||||||
|
AuthSourceConfAzureAdV2 AzureConf{ azureConfClientId = upsertUserAzureTenantId } -> do
|
||||||
|
queryOAuth2User @[(Text, [ByteString])] credsIdent >>= \case
|
||||||
|
Right upsertUserAzureData -> return $ Just UpsertUserDataAzure{..}
|
||||||
|
Left _ -> return Nothing
|
||||||
|
AuthSourceConfLdap LdapConf{..} -> getsYesod (view _appLdapPool) >>= \case
|
||||||
|
Just ldapPool -> fmap (UpsertUserDataLdap ldapConfSourceId) <$> ldapUser'' ldapPool credsIdent
|
||||||
|
Nothing -> throwM FetchUserDataException
|
||||||
|
|
||||||
|
-- insert ExternalUser entries for each fetched dataset
|
||||||
|
whenIsJust results $ \ress -> forM_ ress $ \res -> do
|
||||||
|
let externalUserLastSync = now
|
||||||
|
(externalUserData, externalUserSource) = case res of
|
||||||
|
UpsertUserDataAzure{..} -> (toJSON upsertUserAzureData, AuthSourceIdAzure upsertUserAzureTenantId)
|
||||||
|
UpsertUserDataLdap{..} -> (toJSON upsertUserLdapData, AuthSourceIdLdap upsertUserLdapHost)
|
||||||
|
externalUserUser <- if
|
||||||
|
| UpsertUserDataAzure{..} <- res
|
||||||
|
, azureData <- Map.fromListWith (++) $ upsertUserAzureData <&> second (filter (not . ByteString.null))
|
||||||
|
, [Text.decodeUtf8' -> Right azureUserPrincipalName'] <- azureData !!! azureUserPrincipalName
|
||||||
|
-> return $ CI.mk azureUserPrincipalName'
|
||||||
|
| UpsertUserDataLdap{..} <- res
|
||||||
|
, ldapData <- Map.fromListWith (++) $ upsertUserLdapData <&> second (filter (not . ByteString.null))
|
||||||
|
, [Text.decodeUtf8' -> Right ldapPrimaryKey'] <- ldapData !!! ldapPrimaryKey
|
||||||
|
-> return $ CI.mk ldapPrimaryKey'
|
||||||
|
| otherwise
|
||||||
|
-> throwM DecodeUserInvalidIdent
|
||||||
|
void $ upsert ExternalUser{..} [ExternalUserData =. externalUserData, ExternalUserLastSync =. externalUserLastSync]
|
||||||
|
|
||||||
|
return results
|
||||||
|
|
||||||
|
|
||||||
|
-- | Upsert User and related auth in DB according to given external source data (does not query source itself)
|
||||||
|
maybeUpsertUser :: forall m.
|
||||||
|
( MonadHandler m
|
||||||
|
, HandlerSite m ~ UniWorX
|
||||||
|
, MonadCatch m
|
||||||
|
)
|
||||||
|
=> UpsertUserMode
|
||||||
|
-> Maybe (NonEmpty UpsertUserData)
|
||||||
|
-> SqlPersistT m (Maybe (Entity User))
|
||||||
|
maybeUpsertUser _upsertMode Nothing = return Nothing
|
||||||
|
maybeUpsertUser _upsertMode (Just upsertData) = do
|
||||||
now <- liftIO getCurrentTime
|
now <- liftIO getCurrentTime
|
||||||
userDefaultConf <- getsYesod $ view _appUserDefaults
|
userDefaultConf <- getsYesod $ view _appUserDefaults
|
||||||
|
|
||||||
(newUser,userUpdate) <- decodeUser now userDefaultConf upsertMode ldapData
|
(newUser,userUpdate) <- decodeUser now userDefaultConf upsertData
|
||||||
|
--TODO: newUser should be associated with a company and company supervisor through Handler.Utils.Company.upsertUserCompany, but this is called by upsertAvsUser already - conflict?
|
||||||
|
|
||||||
oldUsers <- for (userLdapPrimaryKey newUser) $ \pKey -> selectKeysList [ UserLdapPrimaryKey ==. Just pKey ] []
|
oldUsers <- selectKeysList [ UserIdent ==. userIdent newUser ] []
|
||||||
|
|
||||||
user@(Entity userId userRec) <- case oldUsers of
|
user@(Entity userId _userRec) <- case oldUsers of
|
||||||
Just [oldUserId] -> updateGetEntity oldUserId userUpdate
|
[oldUserId] -> updateGetEntity oldUserId userUpdate
|
||||||
_other -> upsertBy (UniqueAuthentication (newUser ^. _userIdent)) newUser userUpdate
|
_other -> upsertBy (UniqueAuthentication (newUser ^. _userIdent)) newUser userUpdate
|
||||||
unless (validDisplayName (newUser ^. _userTitle)
|
|
||||||
(newUser ^. _userFirstName)
|
|
||||||
(newUser ^. _userSurname)
|
|
||||||
(userRec ^. _userDisplayName)) $
|
|
||||||
update userId [ UserDisplayName =. (newUser ^. _userDisplayName) ] -- update invalid display names only
|
|
||||||
when (validEmail' (userRec ^. _userEmail)) $ do -- RECALL: userRec already contains basic updates
|
|
||||||
let emUps = [ UserDisplayEmail =. (newUser ^. _userEmail) | not (validEmail' (userRec ^. _userDisplayEmail)) ]
|
|
||||||
++ [ UserAuthentication =. AuthLDAP | is _AuthNoLogin (userRec ^. _userAuthentication) ]
|
|
||||||
update userId emUps -- update already checks whether list is empty
|
|
||||||
-- Attempt to update ident, too:
|
|
||||||
unless (validEmail' (userRec ^. _userIdent)) $
|
|
||||||
void $ maybeCatchAll (update userId [ UserIdent =. (newUser ^. _userEmail) ] >> return (Just ()))
|
|
||||||
|
|
||||||
let
|
let
|
||||||
userSystemFunctions = determineSystemFunctions . Set.fromList $ map CI.mk userSystemFunctions'
|
userSystemFunctions = determineSystemFunctions . Set.fromList $ map CI.mk userSystemFunctions'
|
||||||
userSystemFunctions' = do
|
userSystemFunctions' = concat $ upsertData <&> \case
|
||||||
(k, v) <- ldapData
|
UpsertUserDataAzure{..} -> do
|
||||||
guard $ k == ldapAffiliation
|
(_k, v) <- upsertUserAzureData
|
||||||
v' <- v
|
v' <- v
|
||||||
Right str <- return $ Text.decodeUtf8' v'
|
Right str <- return $ Text.decodeUtf8' v'
|
||||||
assertM' (not . Text.null) $ Text.strip str
|
assertM' (not . Text.null) $ Text.strip str
|
||||||
|
UpsertUserDataLdap{..} -> do
|
||||||
|
(k, v) <- upsertUserLdapData
|
||||||
|
guard $ k == ldapAffiliation
|
||||||
|
v' <- v
|
||||||
|
Right str <- return $ Text.decodeUtf8' v'
|
||||||
|
assertM' (not . Text.null) $ Text.strip str
|
||||||
|
|
||||||
iforM_ userSystemFunctions $ \func preset -> do
|
iforM_ userSystemFunctions $ \func preset -> do
|
||||||
memcachedByInvalidate (AuthCacheSystemFunctionList func) $ Proxy @(Set UserId)
|
memcachedByInvalidate (AuthCacheSystemFunctionList func) $ Proxy @(Set UserId)
|
||||||
if | preset -> void $ upsert (UserSystemFunction userId func False False) []
|
if | preset -> void $ upsert (UserSystemFunction userId func False False) []
|
||||||
| otherwise -> deleteWhere [UserSystemFunctionUser ==. userId, UserSystemFunctionFunction ==. func, UserSystemFunctionIsOptOut ==. False, UserSystemFunctionManual ==. False]
|
| otherwise -> deleteWhere [UserSystemFunctionUser ==. userId, UserSystemFunctionFunction ==. func, UserSystemFunctionIsOptOut ==. False, UserSystemFunctionManual ==. False]
|
||||||
|
|
||||||
return user
|
return $ Just user
|
||||||
|
|
||||||
decodeUserTest :: (MonadHandler m, HandlerSite m ~ UniWorX, MonadCatch m)
|
upsertUser :: forall m.
|
||||||
=> Maybe UserIdent -> Ldap.AttrList [] -> m (Either CampusUserConversionException (User, [Update User]))
|
( MonadHandler m
|
||||||
decodeUserTest mbIdent ldapData = do
|
, HandlerSite m ~ UniWorX
|
||||||
now <- liftIO getCurrentTime
|
, MonadCatch m
|
||||||
userDefaultConf <- getsYesod $ view _appUserDefaults
|
)
|
||||||
let mode = maybe UpsertCampusUserLoginLdap UpsertCampusUserLoginDummy mbIdent
|
=> UpsertUserMode
|
||||||
try $ decodeUser now userDefaultConf mode ldapData
|
-> NonEmpty UpsertUserData
|
||||||
|
-> SqlPersistT m (Entity User)
|
||||||
|
upsertUser upsertMode upsertData = maybeUpsertUser upsertMode (Just upsertData) >>= \case
|
||||||
|
Nothing -> error "upsertUser: No user result from maybeUpsertUser!"
|
||||||
|
Just user -> return user
|
||||||
|
|
||||||
|
|
||||||
decodeUser :: (MonadThrow m) => UTCTime -> UserDefaultConf -> UpsertCampusUserMode -> Ldap.AttrList [] -> m (User,_)
|
decodeUser :: ( MonadThrow m
|
||||||
decodeUser now UserDefaultConf{..} upsertMode ldapData = do
|
)
|
||||||
let
|
=> UTCTime -- ^ Now
|
||||||
userTelephone = decodeLdap ldapUserTelephone <&> canonicalPhone
|
-> UserDefaultConf
|
||||||
userMobile = decodeLdap ldapUserMobile <&> canonicalPhone
|
-> NonEmpty UpsertUserData -- ^ Raw source data
|
||||||
userCompanyPersonalNumber = decodeLdap ldapUserFraportPersonalnummer
|
-> m (User,_) -- ^ Data for new User entry and updating existing User entries
|
||||||
userCompanyDepartment = decodeLdap ldapUserFraportAbteilung
|
decodeUser now UserDefaultConf{..} upsertData = do
|
||||||
|
|
||||||
userAuthentication
|
|
||||||
| is _UpsertCampusUserLoginOther upsertMode
|
|
||||||
= AuthNoLogin -- AuthPWHash (error "Non-LDAP logins should only work for users that are already known")
|
|
||||||
| otherwise = AuthLDAP
|
|
||||||
userLastAuthentication = guardOn isLogin now
|
|
||||||
isLogin = has (_UpsertCampusUserLoginLdap <> _UpsertCampusUserLoginOther . united) upsertMode
|
|
||||||
|
|
||||||
userTitle = decodeLdap ldapUserTitle -- CampusUserInvalidTitle
|
|
||||||
userFirstName = decodeLdap' ldapUserFirstName -- CampusUserInvalidGivenName
|
|
||||||
userSurname = decodeLdap' ldapUserSurname -- CampusUserInvalidSurname
|
|
||||||
userDisplayName <- decodeLdap1 ldapUserDisplayName CampusUserInvalidDisplayName <&> fixDisplayName -- do not check LDAP-given userDisplayName
|
|
||||||
|
|
||||||
--userDisplayName <- decodeLdap1 ldapUserDisplayName CampusUserInvalidDisplayName >>=
|
|
||||||
-- (maybeThrow CampusUserInvalidDisplayName . checkDisplayName userTitle userFirstName userSurname)
|
|
||||||
|
|
||||||
userIdent <- if
|
userIdent <- if
|
||||||
| [bs] <- ldapMap !!! ldapUserPrincipalName
|
| Just azureData <- mbAzureData
|
||||||
, Right userIdent' <- CI.mk <$> Text.decodeUtf8' bs
|
, [Text.decodeUtf8' -> Right azureUserPrincipalName'] <- azureData !!! azureUserPrincipalName
|
||||||
, hasn't _upsertCampusUserIdent upsertMode || has (_upsertCampusUserIdent . only userIdent') upsertMode
|
, Just azureUserPrincipalName'' <- assertM' (not . Text.null) $ Text.strip azureUserPrincipalName'
|
||||||
-> return userIdent'
|
-> return $ CI.mk azureUserPrincipalName''
|
||||||
| Just userIdent' <- upsertMode ^? _upsertCampusUserIdent
|
| Just ldapData <- mbLdapData
|
||||||
-> return userIdent'
|
, [Text.decodeUtf8' -> Right ldapPrimaryKey'] <- ldapData !!! ldapPrimaryKey
|
||||||
|
, Just ldapPrimaryKey'' <- assertM' (not . Text.null) $ Text.strip ldapPrimaryKey'
|
||||||
|
-> return $ CI.mk ldapPrimaryKey''
|
||||||
| otherwise
|
| otherwise
|
||||||
-> throwM CampusUserInvalidIdent
|
-> throwM DecodeUserInvalidIdent
|
||||||
|
|
||||||
userEmail <- if -- TODO: refactor! NOTE: LDAP doesnt know email for all users; we use userPrincialName instead; however validEmail refutes `E<number@fraport.de` here, too strong! Make Email-Field optional!
|
|
||||||
| userEmail : _ <- mapMaybe (assertM (elem '@') . either (const Nothing) Just . Text.decodeUtf8') (lookupSome ldapMap $ toList ldapUserEmail)
|
|
||||||
-> return $ CI.mk userEmail
|
|
||||||
-- | userEmail : _ <- mapMaybe (assertM validEmail . either (const Nothing) Just . Text.decodeUtf8') (lookupSome ldapMap $ toList ldapUserEmail) -- TOO STRONG, see above!
|
|
||||||
-- -> return $ CI.mk userEmail
|
|
||||||
| otherwise
|
|
||||||
-> throwM CampusUserInvalidEmail
|
|
||||||
|
|
||||||
userLdapPrimaryKey <- if
|
|
||||||
| [bs] <- ldapMap !!! ldapPrimaryKey
|
|
||||||
, Right userLdapPrimaryKey'' <- Text.decodeUtf8' bs
|
|
||||||
, Just userLdapPrimaryKey''' <- assertM' (not . Text.null) $ Text.strip userLdapPrimaryKey''
|
|
||||||
-> return $ Just userLdapPrimaryKey'''
|
|
||||||
| otherwise
|
|
||||||
-> return Nothing
|
|
||||||
|
|
||||||
let
|
let
|
||||||
|
(azureSurname, azureFirstName, azureDisplayName, azureEmail, azureTelephone, azureMobile, azureLanguages)
|
||||||
|
| Just azureData <- mbAzureData
|
||||||
|
= ( azureData `decodeAzure` azureUserSurname
|
||||||
|
, azureData `decodeAzure` azureUserGivenName
|
||||||
|
, azureData `decodeAzure` azureUserDisplayName
|
||||||
|
, azureData `decodeAzure` azureUserMail
|
||||||
|
, azureData `decodeAzure` azureUserTelephone
|
||||||
|
, azureData `decodeAzure` azureUserMobile
|
||||||
|
, Nothing -- azureData `decodeAzure` azureUserPreferredLanguage -- TODO: parse Languages from azureUserPreferredLanguage
|
||||||
|
)
|
||||||
|
| otherwise
|
||||||
|
= ( Nothing, Nothing, Nothing, Nothing, Nothing, Nothing, Nothing )
|
||||||
|
|
||||||
|
(ldapSurname, ldapFirstName, ldapDisplayName, ldapEmail, ldapTelephone, ldapMobile, ldapCompanyPersonalNumber, ldapCompanyDepartment)
|
||||||
|
| Just ldapData <- mbLdapData
|
||||||
|
= ( ldapData `decodeLdap` ldapUserSurname
|
||||||
|
, ldapData `decodeLdap` ldapUserFirstName
|
||||||
|
, ldapData `decodeLdap` ldapUserDisplayName
|
||||||
|
, ldapData `decodeLdap` Ldap.Attr "mail" -- TODO: use ldapUserEmail?
|
||||||
|
, ldapData `decodeLdap` ldapUserTelephone
|
||||||
|
, ldapData `decodeLdap` ldapUserMobile
|
||||||
|
, ldapData `decodeLdap` ldapUserFraportPersonalnummer
|
||||||
|
, ldapData `decodeLdap` ldapUserFraportAbteilung
|
||||||
|
)
|
||||||
|
| otherwise
|
||||||
|
= ( Nothing, Nothing, Nothing, Nothing, Nothing, Nothing, Nothing, Nothing )
|
||||||
|
|
||||||
|
-- TODO: throw on collisions?
|
||||||
|
|
||||||
|
-- TODO: use user-auth precedence from app config when implementing multi-source support
|
||||||
|
let
|
||||||
|
userSurname = fromMaybe mempty $ azureSurname <|> ldapSurname
|
||||||
|
userFirstName = fromMaybe mempty $ azureFirstName <|> ldapFirstName
|
||||||
|
userDisplayName = fromMaybe mempty $ azureDisplayName <|> ldapDisplayName
|
||||||
|
userEmail = maybe mempty CI.mk $ azureEmail <|> ldapEmail
|
||||||
|
userTelephone = azureTelephone <|> ldapTelephone
|
||||||
|
userMobile = azureMobile <|> ldapMobile
|
||||||
|
userLanguages = azureLanguages
|
||||||
|
userCompanyPersonalNumber = ldapCompanyPersonalNumber
|
||||||
|
userCompanyDepartment = ldapCompanyDepartment
|
||||||
|
|
||||||
newUser = User
|
newUser = User
|
||||||
{ userMaxFavourites = userDefaultMaxFavourites
|
{ userMaxFavourites = userDefaultMaxFavourites
|
||||||
, userMaxFavouriteTerms = userDefaultMaxFavouriteTerms
|
, userMaxFavouriteTerms = userDefaultMaxFavouriteTerms
|
||||||
, userTheme = userDefaultTheme
|
, userTheme = userDefaultTheme
|
||||||
, userDateTimeFormat = userDefaultDateTimeFormat
|
, userDateTimeFormat = userDefaultDateTimeFormat
|
||||||
, userDateFormat = userDefaultDateFormat
|
, userDateFormat = userDefaultDateFormat
|
||||||
, userTimeFormat = userDefaultTimeFormat
|
, userTimeFormat = userDefaultTimeFormat
|
||||||
, userDownloadFiles = userDefaultDownloadFiles
|
, userDownloadFiles = userDefaultDownloadFiles
|
||||||
, userWarningDays = userDefaultWarningDays
|
, userWarningDays = userDefaultWarningDays
|
||||||
, userShowSex = userDefaultShowSex
|
, userShowSex = userDefaultShowSex
|
||||||
, userSex = Nothing
|
, userSex = Nothing
|
||||||
, userBirthday = Nothing
|
, userBirthday = Nothing
|
||||||
, userExamOfficeGetSynced = userDefaultExamOfficeGetSynced
|
, userTitle = Nothing
|
||||||
, userExamOfficeGetLabels = userDefaultExamOfficeGetLabels
|
, userExamOfficeGetSynced = userDefaultExamOfficeGetSynced
|
||||||
, userNotificationSettings = def
|
, userExamOfficeGetLabels = userDefaultExamOfficeGetLabels
|
||||||
, userLanguages = Nothing
|
, userNotificationSettings = def
|
||||||
, userCsvOptions = def
|
, userCsvOptions = def
|
||||||
, userTokensIssuedAfter = Nothing
|
, userTokensIssuedAfter = Nothing
|
||||||
, userCreated = now
|
, userDisplayEmail = userEmail
|
||||||
, userLastLdapSynchronisation = Just now
|
, userMatrikelnummer = Nothing -- TODO: not known from Azure/LDAP, must be derived from REST interface to AVS
|
||||||
, userDisplayName = userDisplayName
|
, userPostAddress = Nothing -- TODO: not known from Azure/LDAP, must be derived from REST interface to AVS
|
||||||
, userDisplayEmail = userEmail
|
, userPostLastUpdate = Nothing
|
||||||
, userMatrikelnummer = Nothing -- not known from LDAP, must be derived from REST interface to AVS TODO
|
, userPinPassword = Nothing -- must be derived via AVS
|
||||||
, userPostAddress = Nothing -- not known from LDAP, must be derived from REST interface to AVS TODO
|
, userPrefersPostal = userDefaultPrefersPostal
|
||||||
, userPostLastUpdate = Nothing
|
, userPasswordHash = Nothing
|
||||||
, userPinPassword = Nothing -- must be derived via AVS
|
, userLastAuthentication = Nothing
|
||||||
, userPrefersPostal = userDefaultPrefersPostal
|
, userCreated = now
|
||||||
|
, userLastSync = Just now
|
||||||
, ..
|
, ..
|
||||||
}
|
}
|
||||||
userUpdate =
|
userUpdate =
|
||||||
[ UserLastAuthentication =. Just now | isLogin ] ++
|
[ UserSurname =. userSurname
|
||||||
[ UserEmail =. userEmail | validEmail' userEmail ] ++
|
, UserFirstName =. userFirstName
|
||||||
[
|
-- , UserDisplayName =. userDisplayName -- not updated, since users are allowed to change their DisplayName
|
||||||
-- UserDisplayName =. userDisplayName -- not updated here, since users are allowed to change their DisplayName; see line 191
|
, UserEmail =. userEmail
|
||||||
UserFirstName =. userFirstName
|
, UserTelephone =. userTelephone
|
||||||
, UserSurname =. userSurname
|
, UserMobile =. userMobile
|
||||||
, UserLastLdapSynchronisation =. Just now
|
, UserCompanyPersonalNumber =. userCompanyPersonalNumber
|
||||||
, UserLdapPrimaryKey =. userLdapPrimaryKey
|
, UserCompanyDepartment =. userCompanyDepartment
|
||||||
, UserMobile =. userMobile
|
, UserLastSync =. Just now
|
||||||
, UserTelephone =. userTelephone
|
|
||||||
, UserCompanyPersonalNumber =. userCompanyPersonalNumber
|
|
||||||
, UserCompanyDepartment =. userCompanyDepartment
|
|
||||||
]
|
]
|
||||||
return (newUser, userUpdate)
|
return (newUser, userUpdate)
|
||||||
|
|
||||||
where
|
where
|
||||||
ldapMap :: Map.Map Ldap.Attr [Ldap.AttrValue] -- Recall: Ldap.AttrValue == ByteString
|
mbAzureData :: Maybe (Map Text [ByteString])
|
||||||
ldapMap = Map.fromListWith (++) $ ldapData <&> second (filter (not . ByteString.null))
|
mbAzureData = fmap (Map.fromListWith (++) . map (second (filter (not . ByteString.null)))) . concat $ preview _upsertUserAzureData <$> NonEmpty.toList upsertData
|
||||||
|
mbLdapData :: Maybe (Map Ldap.Attr [Ldap.AttrValue]) -- Recall: Ldap.AttrValue == ByteString
|
||||||
|
mbLdapData = fmap (Map.fromListWith (++) . map (second (filter (not . ByteString.null)))) . concat $ preview _upsertUserLdapData <$> NonEmpty.toList upsertData
|
||||||
|
|
||||||
-- just returns Nothing on error, pure
|
-- just returns Nothing on error, pure
|
||||||
decodeLdap :: Ldap.Attr -> Maybe Text
|
decodeAzure :: Map Text [ByteString] -> Text -> Maybe Text
|
||||||
decodeLdap attr = listToMaybe . rights $ Text.decodeUtf8' <$> ldapMap !!! attr
|
decodeAzure azureData k = listToMaybe . rights $ Text.decodeUtf8' <$> azureData !!! k
|
||||||
|
decodeLdap :: Map Ldap.Attr [Ldap.AttrValue] -> Ldap.Attr -> Maybe Text
|
||||||
|
decodeLdap ldapData attr = listToMaybe . rights $ Text.decodeUtf8' <$> ldapData !!! attr
|
||||||
|
|
||||||
decodeLdap' :: Ldap.Attr -> Text
|
-- decodeAzure' :: Map Text [ByteString] -> Text -> Text
|
||||||
decodeLdap' = fromMaybe "" . decodeLdap
|
-- decodeAzure' azureData = fromMaybe "" . decodeAzure azureData
|
||||||
|
-- decodeLdap' :: Map Ldap.Attr [Ldap.AttrValue] -> Ldap.Attr -> Text
|
||||||
|
-- decodeLdap' ldapData = fromMaybe "" . decodeLdap ldapData
|
||||||
-- accept the first successful decoding or empty; only throw an error if all decodings fail
|
-- accept the first successful decoding or empty; only throw an error if all decodings fail
|
||||||
-- decodeLdap' :: (Exception e) => Ldap.Attr -> e -> m (Maybe Text)
|
-- decodeLdap' :: (Exception e) => Ldap.Attr -> e -> m (Maybe Text)
|
||||||
-- decodeLdap' attr err
|
-- decodeLdap' attr err
|
||||||
@ -342,11 +409,11 @@ decodeUser now UserDefaultConf{..} upsertMode ldapData = do
|
|||||||
|
|
||||||
-- only accepts the first successful decoding, ignoring all others, but failing if there is none
|
-- only accepts the first successful decoding, ignoring all others, but failing if there is none
|
||||||
-- decodeLdap1 :: (MonadThrow m, Exception e) => Ldap.Attr -> e -> m Text
|
-- decodeLdap1 :: (MonadThrow m, Exception e) => Ldap.Attr -> e -> m Text
|
||||||
decodeLdap1 attr err
|
-- decodeLdap1 ldapData attr err
|
||||||
| (h:_) <- rights vs = return h
|
-- | (h:_) <- rights vs = return h
|
||||||
| otherwise = throwM err
|
-- | otherwise = throwM err
|
||||||
where
|
-- where
|
||||||
vs = Text.decodeUtf8' <$> (ldapMap !!! attr)
|
-- vs = Text.decodeUtf8' <$> (ldapData !!! attr)
|
||||||
|
|
||||||
-- accept and merge one or more successful decodings, ignoring all others
|
-- accept and merge one or more successful decodings, ignoring all others
|
||||||
-- decodeLdapN attr err
|
-- decodeLdapN attr err
|
||||||
@ -356,6 +423,17 @@ decodeUser now UserDefaultConf{..} upsertMode ldapData = do
|
|||||||
-- where
|
-- where
|
||||||
-- vs = Text.decodeUtf8' <$> (ldapMap !!! attr)
|
-- vs = Text.decodeUtf8' <$> (ldapMap !!! attr)
|
||||||
|
|
||||||
|
decodeUserTest :: ( MonadHandler m
|
||||||
|
, HandlerSite m ~ UniWorX
|
||||||
|
, MonadCatch m
|
||||||
|
)
|
||||||
|
=> NonEmpty UpsertUserData
|
||||||
|
-> m (Either DecodeUserException (User, [Update User]))
|
||||||
|
decodeUserTest decodeData = do
|
||||||
|
now <- liftIO getCurrentTime
|
||||||
|
userDefaultConf <- getsYesod $ view _appUserDefaults
|
||||||
|
try $ decodeUser now userDefaultConf decodeData
|
||||||
|
|
||||||
|
|
||||||
associateUserSchoolsByTerms :: MonadIO m => UserId -> SqlPersistT m ()
|
associateUserSchoolsByTerms :: MonadIO m => UserId -> SqlPersistT m ()
|
||||||
associateUserSchoolsByTerms uid = do
|
associateUserSchoolsByTerms uid = do
|
||||||
@ -370,11 +448,14 @@ associateUserSchoolsByTerms uid = do
|
|||||||
, userSchoolIsOptOut = False
|
, userSchoolIsOptOut = False
|
||||||
}
|
}
|
||||||
|
|
||||||
updateUserLanguage :: ( MonadHandler m, HandlerSite m ~ UniWorX
|
|
||||||
|
updateUserLanguage :: ( MonadHandler m
|
||||||
|
, HandlerSite m ~ UniWorX
|
||||||
, YesodAuth UniWorX
|
, YesodAuth UniWorX
|
||||||
, UserId ~ AuthId UniWorX
|
, UserId ~ AuthId UniWorX
|
||||||
)
|
)
|
||||||
=> Maybe Lang -> SqlPersistT m (Maybe Lang)
|
=> Maybe Lang
|
||||||
|
-> SqlPersistT m (Maybe Lang)
|
||||||
updateUserLanguage (Just lang) = do
|
updateUserLanguage (Just lang) = do
|
||||||
unless (lang `elem` appLanguages) $
|
unless (lang `elem` appLanguages) $
|
||||||
invalidArgs ["Unsupported language"]
|
invalidArgs ["Unsupported language"]
|
||||||
@ -405,7 +486,4 @@ updateUserLanguage Nothing = runMaybeT $ do
|
|||||||
setRegisteredCookie CookieLang lang
|
setRegisteredCookie CookieLang lang
|
||||||
return lang
|
return lang
|
||||||
|
|
||||||
campusUserFailoverMode :: FailoverMode
|
embedRenderMessage ''UniWorX ''DecodeUserException id
|
||||||
campusUserFailoverMode = FailoverUnlimited
|
|
||||||
|
|
||||||
embedRenderMessage ''UniWorX ''CampusUserConversionException id
|
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
-- SPDX-FileCopyrightText: 2022-24 Gregor Kleen <gregor.kleen@ifi.lmu.de>,Steffen Jost <jost@tcs.ifi.lmu.de>,Steffen Jost <s.jost@fraport.de>
|
-- SPDX-FileCopyrightText: 2022-2024 Sarah Vaupel <sarah.vaupel@uniworx.de>, Gregor Kleen <gregor.kleen@ifi.lmu.de>, Steffen Jost <jost@tcs.ifi.lmu.de>
|
||||||
--
|
--
|
||||||
-- SPDX-License-Identifier: AGPL-3.0-or-later
|
-- SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
@ -8,65 +8,37 @@ module Handler.Admin
|
|||||||
|
|
||||||
import Import
|
import Import
|
||||||
|
|
||||||
|
import Jobs
|
||||||
-- import Data.Either
|
-- import Data.Either
|
||||||
import qualified Data.Set as Set
|
import qualified Data.Set as Set
|
||||||
import qualified Data.Map as Map
|
|
||||||
import qualified Data.Text as Text
|
|
||||||
-- import qualified Data.Text.Lazy.Encoding as LBS
|
-- import qualified Data.Text.Lazy.Encoding as LBS
|
||||||
|
|
||||||
-- import qualified Control.Monad.Catch as Catch
|
-- import qualified Control.Monad.Catch as Catch
|
||||||
-- import Servant.Client (ClientError(..), ResponseF(..))
|
-- import Servant.Client (ClientError(..), ResponseF(..))
|
||||||
-- import Text.Blaze.Html (preEscapedToHtml)
|
-- import Text.Blaze.Html (preEscapedToHtml)
|
||||||
|
|
||||||
import Database.Persist.Sql (updateWhereCount)
|
|
||||||
import Database.Esqueleto.Experimental ((:&)(..))
|
import Database.Esqueleto.Experimental ((:&)(..))
|
||||||
import qualified Database.Esqueleto.Experimental as E
|
import qualified Database.Esqueleto.Experimental as E
|
||||||
import qualified Database.Esqueleto.Legacy as EL (on) -- needed for dbTable
|
|
||||||
import qualified Database.Esqueleto.Utils as E
|
import qualified Database.Esqueleto.Utils as E
|
||||||
|
|
||||||
import Jobs
|
|
||||||
import Handler.Utils
|
import Handler.Utils
|
||||||
import Handler.Utils.Avs
|
import Handler.Utils.Avs
|
||||||
import Handler.Utils.Users
|
import Handler.Utils.Users
|
||||||
-- import Handler.Utils.Company
|
|
||||||
import Handler.Health.Interface
|
import Handler.Health.Interface
|
||||||
import Handler.Users (AllUsersAction(..))
|
|
||||||
|
|
||||||
import Handler.Admin.Test as Handler.Admin
|
import Handler.Admin.Test as Handler.Admin
|
||||||
import Handler.Admin.ErrorMessage as Handler.Admin
|
import Handler.Admin.ErrorMessage as Handler.Admin
|
||||||
import Handler.Admin.Tokens as Handler.Admin
|
import Handler.Admin.Tokens as Handler.Admin
|
||||||
import Handler.Admin.Crontab as Handler.Admin
|
import Handler.Admin.Crontab as Handler.Admin
|
||||||
import Handler.Admin.Avs as Handler.Admin
|
import Handler.Admin.Avs as Handler.Admin
|
||||||
import Handler.Admin.Ldap as Handler.Admin
|
import Handler.Admin.ExternalUser as Handler.Admin
|
||||||
|
|
||||||
-- avoids repetition of local definitions
|
|
||||||
single :: (k,a) -> Map k a
|
|
||||||
single = uncurry Map.singleton
|
|
||||||
|
|
||||||
|
|
||||||
-- Types and Template Haskell
|
|
||||||
data ProblemTableAction = ProblemTableMarkSolved
|
|
||||||
| ProblemTableMarkUnsolved
|
|
||||||
deriving (Eq, Ord, Enum, Bounded, Read, Show, Generic)
|
|
||||||
deriving anyclass (Universe, Finite)
|
|
||||||
|
|
||||||
nullaryPathPiece ''ProblemTableAction $ camelToPathPiece' 2
|
|
||||||
embedRenderMessage ''UniWorX ''ProblemTableAction id
|
|
||||||
|
|
||||||
data ProblemTableActionData = ProblemTableMarkSolvedData
|
|
||||||
| ProblemTableMarkUnsolvedData -- Placeholder, remove later
|
|
||||||
deriving (Eq, Ord, Read, Show, Generic)
|
|
||||||
|
|
||||||
|
|
||||||
-- Handlers
|
|
||||||
getAdminR :: Handler Html
|
getAdminR :: Handler Html
|
||||||
getAdminR = redirect AdminProblemsR
|
getAdminR = redirect AdminProblemsR
|
||||||
|
|
||||||
getAdminProblemsR, postAdminProblemsR :: Handler Html
|
getAdminProblemsR :: Handler Html
|
||||||
getAdminProblemsR = handleAdminProblems Nothing
|
getAdminProblemsR = do
|
||||||
|
|
||||||
handleAdminProblems :: Maybe Widget -> Handler Html
|
|
||||||
handleAdminProblems mbProblemTable = do
|
|
||||||
now <- liftIO getCurrentTime
|
now <- liftIO getCurrentTime
|
||||||
let nowaday = utctDay now
|
let nowaday = utctDay now
|
||||||
cutOffOldDays = 1
|
cutOffOldDays = 1
|
||||||
@ -78,27 +50,26 @@ handleAdminProblems mbProblemTable = do
|
|||||||
msgErrorTooltip <- messageI Error MsgMessageError
|
msgErrorTooltip <- messageI Error MsgMessageError
|
||||||
|
|
||||||
let flagError = messageTooltip . bool msgErrorTooltip msgSuccessTooltip
|
let flagError = messageTooltip . bool msgErrorTooltip msgSuccessTooltip
|
||||||
flagWarning = messageTooltip . bool msgWarningTooltip msgSuccessTooltip
|
flagWarning = messageTooltip . bool msgWarningTooltip msgSuccessTooltip
|
||||||
flagNonZero :: Int -> Widget
|
flagNonZero :: Int -> Widget
|
||||||
flagNonZero n | n <= 0 = flagError True
|
flagNonZero n | n <= 0 = flagError True
|
||||||
| otherwise = messageTooltip =<< handlerToWidget (messageI Error (MsgProblemsDriverSynch n))
|
| otherwise = messageTooltip =<< handlerToWidget (messageI Error (MsgProblemsDriverSynch n))
|
||||||
|
|
||||||
(usersAreReachable, driversHaveAvsIds, rDriversHaveFs, noStalePrintJobs, noBadAPCids, (interfaceOks, interfaceTable)) <- runDB $ (,,,,,)
|
(usersAreReachable, driversHaveAvsIds, rDriversHaveFs, noStalePrintJobs, noBadAPCids, (interfaceOks, interfaceTable)) <- runDB $ (,,,,,)
|
||||||
<$> areAllUsersReachable
|
<$> areAllUsersReachable
|
||||||
<*> allDriversHaveAvsId now
|
<*> allDriversHaveAvsId now
|
||||||
<*> allRDriversHaveFs now
|
<*> allRDriversHaveFs now
|
||||||
<*> (not <$> exists [PrintJobAcknowledged ==. Nothing, PrintJobCreated <. cutOffOldTime])
|
<*> (not <$> exists [PrintJobAcknowledged ==. Nothing, PrintJobCreated <. cutOffOldTime])
|
||||||
<*> (not <$> exists [PrintAcknowledgeProcessed ==. False])
|
<*> (not <$> exists [PrintAcknowledgeProcessed ==. False])
|
||||||
<*> mkInterfaceLogTable mempty
|
<*> mkInterfaceLogTable flagError mempty
|
||||||
let interfacesBadNr = length $ filter (not . snd) interfaceOks
|
let interfacesBadNr = length $ filter (not . snd) interfaceOks
|
||||||
-- interfacesOk = all snd interfaceOks
|
-- interfacesOk = all snd interfaceOks
|
||||||
|
|
||||||
diffLics <- try retrieveDifferingLicences >>= \case
|
diffLics <- try retrieveDifferingLicences >>= \case
|
||||||
-- (Left (UnsupportedContentType "text/html" resp)) -> Left $ text2widget "Html received"
|
-- (Left (UnsupportedContentType "text/html" resp)) -> Left $ text2widget "Html received"
|
||||||
(Left e) -> return $ Left $ text2widget $ tshow (e :: SomeException)
|
(Left e) -> return $ Left $ text2widget $ tshow (e :: SomeException)
|
||||||
(Right (AvsLicenceDifferences{..},_)) -> do
|
(Right AvsLicenceDifferences{..}) -> do
|
||||||
let problemIds = avsLicenceDiffRevokeAll <> avsLicenceDiffGrantVorfeld <> avsLicenceDiffRevokeRollfeld <> avsLicenceDiffGrantRollfeld
|
let problemIds = avsLicenceDiffRevokeAll <> avsLicenceDiffGrantVorfeld <> avsLicenceDiffRevokeRollfeld <> avsLicenceDiffGrantRollfeld
|
||||||
void $ runDB $ queueAvsUpdateByAID problemIds $ Just nowaday
|
forM_ (take 42 $ Set.toList problemIds) $ queueJob' . flip JobSynchroniseAvsId (Just nowaday)
|
||||||
return $ Right
|
return $ Right
|
||||||
( Set.size avsLicenceDiffRevokeAll
|
( Set.size avsLicenceDiffRevokeAll
|
||||||
, Set.size avsLicenceDiffGrantVorfeld
|
, Set.size avsLicenceDiffGrantVorfeld
|
||||||
@ -107,7 +78,7 @@ handleAdminProblems mbProblemTable = do
|
|||||||
)
|
)
|
||||||
-- Attempt to format results in a nicer way failed, since rendering Html within a modal destroyed the page layout itself
|
-- Attempt to format results in a nicer way failed, since rendering Html within a modal destroyed the page layout itself
|
||||||
-- let procDiffLics (to0, to1, to2) = Right (Set.size to0, Set.size to1, Set.size to2)
|
-- let procDiffLics (to0, to1, to2) = Right (Set.size to0, Set.size to1, Set.size to2)
|
||||||
-- diffLics <- (procDiffLics . fst <$> retrieveDifferingLicences) `catches`
|
-- diffLics <- (procDiffLics <$> retrieveDifferingLicences) `catches`
|
||||||
-- [ Catch.Handler (\case (UnsupportedContentType "text/html;charset=utf-8" Response{responseBody})
|
-- [ Catch.Handler (\case (UnsupportedContentType "text/html;charset=utf-8" Response{responseBody})
|
||||||
-- -> return $ Left $ toWidget $ preEscapedToHtml $ fromRight "Response UTF8-decoding error" $ LBS.decodeUtf8' responseBody
|
-- -> return $ Left $ toWidget $ preEscapedToHtml $ fromRight "Response UTF8-decoding error" $ LBS.decodeUtf8' responseBody
|
||||||
-- ex -> return $ Left $ text2widget $ tshow ex)
|
-- ex -> return $ Left $ text2widget $ tshow ex)
|
||||||
@ -115,63 +86,20 @@ handleAdminProblems mbProblemTable = do
|
|||||||
-- ]
|
-- ]
|
||||||
|
|
||||||
rerouteMail <- getsYesod $ view _appMailRerouteTo
|
rerouteMail <- getsYesod $ view _appMailRerouteTo
|
||||||
problemLogTable <- maybeM (snd <$> runDB mkProblemLogTable) return $ return mbProblemTable -- formResult only processed in POST-Handler
|
|
||||||
|
|
||||||
siteLayoutMsg MsgProblemsHeading $ do
|
siteLayoutMsg MsgProblemsHeading $ do
|
||||||
setTitleI MsgProblemsHeading
|
setTitleI MsgProblemsHeading
|
||||||
$(widgetFile "admin-problems")
|
$(widgetFile "admin-problems")
|
||||||
|
|
||||||
postAdminProblemsR = do
|
|
||||||
(problemLogRes, problemLogTable) <- runDB mkProblemLogTable
|
|
||||||
formResult problemLogRes procProblems
|
|
||||||
handleAdminProblems $ Just problemLogTable
|
|
||||||
where
|
|
||||||
procProblems :: (ProblemTableActionData, Set ProblemLogId) -> Handler ()
|
|
||||||
procProblems (ProblemTableMarkSolvedData , pids) = actUpdate True pids
|
|
||||||
procProblems (ProblemTableMarkUnsolvedData, pids) = actUpdate False pids
|
|
||||||
|
|
||||||
actUpdate markdone pids = do
|
getProblemUnreachableR :: Handler Html
|
||||||
mauid <- maybeAuthId
|
getProblemUnreachableR = do
|
||||||
now <- liftIO getCurrentTime
|
|
||||||
let (pls_fltr,newv,msg) | markdone = (ProblemLogSolved ==. Nothing, Just now, MsgAdminProblemsSolved)
|
|
||||||
| otherwise = (ProblemLogSolved !=. Nothing, Nothing , MsgAdminProblemsReopened)
|
|
||||||
(fromIntegral -> oks) <- runDB $ updateWhereCount [pls_fltr, ProblemLogId <-. toList pids]
|
|
||||||
[ProblemLogSolved =. newv, ProblemLogSolver =. mauid]
|
|
||||||
let no_req = Set.size pids
|
|
||||||
mkind = if oks < no_req || no_req <= 0 then Warning else Success
|
|
||||||
addMessageI mkind $ msg oks
|
|
||||||
when (oks > 0) $ reloadKeepGetParams AdminProblemsR -- reload to update all tables
|
|
||||||
|
|
||||||
getProblemUnreachableR, postProblemUnreachableR :: Handler Html
|
|
||||||
getProblemUnreachableR = postProblemUnreachableR
|
|
||||||
postProblemUnreachableR = do
|
|
||||||
unreachables <- runDB retrieveUnreachableUsers
|
unreachables <- runDB retrieveUnreachableUsers
|
||||||
|
|
||||||
-- the following form is a nearly identicaly copy from Handler.Users:
|
|
||||||
((noreachUsersRes, noreachUsersWgt'), noreachUsersEnctype) <- runFormPost . identifyForm FIDUnreachableUsersAction $ buttonForm
|
|
||||||
let noreachUsersWgt = wrapForm noreachUsersWgt' def
|
|
||||||
{ formSubmit = FormNoSubmit
|
|
||||||
, formAction = Just $ SomeRoute ProblemUnreachableR
|
|
||||||
, formEncoding = noreachUsersEnctype
|
|
||||||
}
|
|
||||||
formResult noreachUsersRes $ \case
|
|
||||||
AllUsersLdapSync -> do
|
|
||||||
forM_ unreachables $ \Entity{entityKey=uid} -> void . queueJob $ JobSynchroniseLdapUser uid
|
|
||||||
addMessageI Success . MsgSynchroniseLdapUserQueued $ length unreachables
|
|
||||||
redirect ProblemUnreachableR
|
|
||||||
AllUsersAvsSync -> do
|
|
||||||
n <- runDB $ queueAvsUpdateByUID (entityKey <$> unreachables) Nothing
|
|
||||||
addMessageI Success . MsgSynchroniseAvsUserQueued $ fromIntegral n
|
|
||||||
redirect ProblemUnreachableR
|
|
||||||
|
|
||||||
siteLayoutMsg MsgProblemsUnreachableHeading $ do
|
siteLayoutMsg MsgProblemsUnreachableHeading $ do
|
||||||
setTitleI MsgProblemsUnreachableHeading
|
setTitleI MsgProblemsUnreachableHeading
|
||||||
[whamlet|
|
[whamlet|
|
||||||
<section>
|
<section>
|
||||||
<h3>_{MsgProblemsUnreachableButtons}
|
#{length unreachables} _{MsgProblemsUnreachableBody}
|
||||||
^{noreachUsersWgt}
|
|
||||||
<section>
|
|
||||||
#{length unreachables} _{MsgProblemsUnreachableBody}
|
|
||||||
<ul>
|
<ul>
|
||||||
$forall usr <- unreachables
|
$forall usr <- unreachables
|
||||||
<li>
|
<li>
|
||||||
@ -179,8 +107,8 @@ postProblemUnreachableR = do
|
|||||||
|]
|
|]
|
||||||
|
|
||||||
getProblemFbutNoR :: Handler Html
|
getProblemFbutNoR :: Handler Html
|
||||||
getProblemFbutNoR = do
|
getProblemFbutNoR = do
|
||||||
now <- liftIO getCurrentTime
|
now <- liftIO getCurrentTime
|
||||||
rnofs <- runDB $ E.select $ retrieveDriversRWithoutF now
|
rnofs <- runDB $ E.select $ retrieveDriversRWithoutF now
|
||||||
siteLayoutMsg MsgProblemsRWithoutFHeading $ do
|
siteLayoutMsg MsgProblemsRWithoutFHeading $ do
|
||||||
setTitleI MsgProblemsRWithoutFHeading
|
setTitleI MsgProblemsRWithoutFHeading
|
||||||
@ -194,8 +122,8 @@ getProblemFbutNoR = do
|
|||||||
|]
|
|]
|
||||||
|
|
||||||
getProblemWithoutAvsId :: Handler Html
|
getProblemWithoutAvsId :: Handler Html
|
||||||
getProblemWithoutAvsId = do
|
getProblemWithoutAvsId = do
|
||||||
now <- liftIO getCurrentTime
|
now <- liftIO getCurrentTime
|
||||||
rnofs <- runDB $ E.select $ retrieveDriversWithoutAvsId now
|
rnofs <- runDB $ E.select $ retrieveDriversWithoutAvsId now
|
||||||
siteLayoutMsg MsgProblemsNoAvsIdHeading $ do
|
siteLayoutMsg MsgProblemsNoAvsIdHeading $ do
|
||||||
setTitleI MsgProblemsNoAvsIdHeading
|
setTitleI MsgProblemsNoAvsIdHeading
|
||||||
@ -210,47 +138,40 @@ getProblemWithoutAvsId = do
|
|||||||
|
|
||||||
{-
|
{-
|
||||||
mkUnreachableUsersTable = do
|
mkUnreachableUsersTable = do
|
||||||
let dbtSQLQuery user -> do
|
let dbtSQLQuery user -> do
|
||||||
E.where_ $ E.isNothing (user E.^. UserPostAddress)
|
E.where_ $ E.isNothing (user E.^. UserPostAddress)
|
||||||
E.&&. E.not_ ((user E.^. UserEmail) `E.like` E.val "%@%.%")
|
E.&&. E.not_ ((user E.^. UserEmail) `E.like` E.val "%@%.%")
|
||||||
pure user
|
pure user
|
||||||
dbtRowKey = (E.^. UserId)
|
dbtRowKey = (E.^. UserId)
|
||||||
dbtProj = dbtProjId
|
dbtProj = dbtProjId
|
||||||
dbtColonnade =
|
dbtColonnade =
|
||||||
-}
|
-}
|
||||||
|
|
||||||
areAllUsersReachable :: DB Bool
|
areAllUsersReachable :: DB Bool
|
||||||
-- areAllUsersReachable = E.selectNotExists retrieveUnreachableUsers' -- works and would be more efficient, but we cannot check proper email validity within DB alone
|
-- areAllUsersReachable = isNothing <$> E.selectOne retrieveUnreachableUsers'
|
||||||
|
-- areAllUsersReachable = E.selectNotExists retrieveUnreachableUsers'
|
||||||
areAllUsersReachable = null <$> retrieveUnreachableUsers
|
areAllUsersReachable = null <$> retrieveUnreachableUsers
|
||||||
|
|
||||||
-- retrieveUnreachableUsers' :: E.SqlQuery (E.SqlExpr (Entity User))
|
-- retrieveUnreachableUsers' :: E.SqlQuery (E.SqlExpr (Entity User))
|
||||||
-- retrieveUnreachableUsers' = do
|
-- retrieveUnreachableUsers' = do
|
||||||
-- user <- E.from $ E.table @User
|
-- user <- E.from $ E.table @User
|
||||||
-- E.where_ $ E.isNothing (user E.^. UserPostAddress)
|
-- E.where_ $ E.isNothing (user E.^. UserPostAddress)
|
||||||
-- E.&&. (E.isNothing (user E.^. UserCompanyDepartment) E.||. user E.^. UserCompanyPersonalNumber `E.ilike` E.justVal "E%")
|
-- E.&&. (E.isNothing (user E.^. UserCompanyDepartment) E.||. user E.^. UserCompanyPersonalNumber `E.ilike` E.justVal "E%")
|
||||||
-- E.&&. E.not_ ((user E.^. UserDisplayEmail) `E.like` E.val "%@%.%")
|
-- E.&&. E.not_ ((user E.^. UserDisplayEmail) `E.like` E.val "%@%.%")
|
||||||
-- E.&&. E.not_ ((user E.^. UserEmail) `E.like` E.val "%@%.%")
|
-- E.&&. E.not_ ((user E.^. UserEmail) `E.like` E.val "%@%.%")
|
||||||
-- return user
|
-- return user
|
||||||
|
|
||||||
retrieveUnreachableUsers :: DB [Entity User]
|
retrieveUnreachableUsers :: DB [Entity User]
|
||||||
retrieveUnreachableUsers = do
|
retrieveUnreachableUsers = do
|
||||||
emailOnlyUsers <- E.select $ do
|
emailOnlyUsers <- E.select $ do
|
||||||
user <- E.from $ E.table @User
|
user <- E.from $ E.table @User
|
||||||
E.where_ $ E.isNothing (user E.^. UserPostAddress)
|
E.where_ $ E.isNothing (user E.^. UserPostAddress)
|
||||||
E.&&. (E.isNothing (user E.^. UserCompanyDepartment) E.||. user E.^. UserCompanyPersonalNumber `E.ilike` E.justVal "E%")
|
E.&&. (E.isNothing (user E.^. UserCompanyDepartment) E.||. user E.^. UserCompanyPersonalNumber `E.ilike` E.justVal "E%")
|
||||||
E.&&. E.notExists (do
|
|
||||||
(cmp :& usrCmp) <- E.from $ E.table @Company `E.innerJoin` E.table @UserCompany
|
|
||||||
`E.on` (\(cmp :& usrCmp) -> cmp E.^. CompanyId E.==. usrCmp E.^. UserCompanyCompany)
|
|
||||||
E.where_ $ user E.^. UserId E.==. usrCmp E.^. UserCompanyUser
|
|
||||||
E.&&. usrCmp E.^. UserCompanyUseCompanyAddress
|
|
||||||
E.&&. E.isJust (cmp E.^. CompanyPostAddress)
|
|
||||||
)
|
|
||||||
return user
|
return user
|
||||||
filterM hasInvalidEmail emailOnlyUsers
|
return $ filter hasInvalidEmail emailOnlyUsers
|
||||||
-- filterM hasInvalifPostal -- probably not worth it, since Utils.Postal.validPostAddress is pretty weak anyway
|
where
|
||||||
where
|
hasInvalidEmail = isNothing . getEmailAddress . entityVal
|
||||||
hasInvalidEmail = fmap isNothing . getUserEmail
|
|
||||||
|
|
||||||
|
|
||||||
allDriversHaveAvsId :: UTCTime -> DB Bool
|
allDriversHaveAvsId :: UTCTime -> DB Bool
|
||||||
-- allDriversHaveAvsId = fmap isNothing . E.selectOne . retrieveDriversWithoutAvsId
|
-- allDriversHaveAvsId = fmap isNothing . E.selectOne . retrieveDriversWithoutAvsId
|
||||||
@ -259,17 +180,17 @@ allDriversHaveAvsId = E.selectNotExists . retrieveDriversWithoutAvsId
|
|||||||
{-
|
{-
|
||||||
-- | Returns users more than once if they own multiple avs-related valid licences, but no AvsID is known
|
-- | Returns users more than once if they own multiple avs-related valid licences, but no AvsID is known
|
||||||
retrieveDriversWithoutAvsId' :: Day -> E.SqlQuery (E.SqlExpr (Entity User))
|
retrieveDriversWithoutAvsId' :: Day -> E.SqlQuery (E.SqlExpr (Entity User))
|
||||||
retrieveDriversWithoutAvsId' nowaday = do
|
retrieveDriversWithoutAvsId' nowaday = do
|
||||||
(usr :& qualUsr :& qual) <- E.from $ E.table @User
|
(usr :& qualUsr :& qual) <- E.from $ E.table @User
|
||||||
`E.innerJoin` E.table @QualificationUser
|
`E.innerJoin` E.table @QualificationUser
|
||||||
`E.on` (\(usr :& qualUsr) -> usr E.^. UserId E.==. qualUsr E.^. QualificationUserUser)
|
`E.on` (\(usr :& qualUsr) -> usr E.^. UserId E.==. qualUsr E.^. QualificationUserUser)
|
||||||
`E.innerJoin` E.table @Qualification
|
`E.innerJoin` E.table @Qualification
|
||||||
`E.on` (\(_usr :& qualUsr :& qual) -> qual E.^. QualificationId E.==. qualUsr E.^. QualificationUserQualification)
|
`E.on` (\(_usr :& qualUsr :& qual) -> qual E.^. QualificationId E.==. qualUsr E.^. QualificationUserQualification)
|
||||||
E.where_ $ -- is avs licence
|
E.where_ $ -- is avs licence
|
||||||
E.isJust (qual E.^. QualificationAvsLicence)
|
E.isJust (qual E.^. QualificationAvsLicence)
|
||||||
E.&&. (qualUsr & validQualification nowaday)
|
E.&&. (qualUsr & validQualification nowaday)
|
||||||
E.&&. -- AvsId is unknown
|
E.&&. -- AvsId is unknown
|
||||||
E.notExists (do
|
E.notExists (do
|
||||||
avsUsr <- E.from $ E.table @UserAvs
|
avsUsr <- E.from $ E.table @UserAvs
|
||||||
E.where_ $ avsUsr E.^. UserAvsUser E.==. usr E.^. UserId
|
E.where_ $ avsUsr E.^. UserAvsUser E.==. usr E.^. UserId
|
||||||
)
|
)
|
||||||
@ -278,20 +199,20 @@ retrieveDriversWithoutAvsId' nowaday = do
|
|||||||
|
|
||||||
-- | Returns users at most once, even if they own multiple avs-related licences, but no AvsID is known
|
-- | Returns users at most once, even if they own multiple avs-related licences, but no AvsID is known
|
||||||
retrieveDriversWithoutAvsId :: UTCTime -> E.SqlQuery (E.SqlExpr (Entity User))
|
retrieveDriversWithoutAvsId :: UTCTime -> E.SqlQuery (E.SqlExpr (Entity User))
|
||||||
retrieveDriversWithoutAvsId now = do
|
retrieveDriversWithoutAvsId now = do
|
||||||
usr <- E.from $ E.table @User
|
usr <- E.from $ E.table @User
|
||||||
E.where_ $
|
E.where_ $
|
||||||
E.exists (do -- a valid avs licence
|
E.exists (do -- a valid avs licence
|
||||||
(qual :& qualUsr) <- E.from (E.table @Qualification
|
(qual :& qualUsr) <- E.from (E.table @Qualification
|
||||||
`E.innerJoin` E.table @QualificationUser
|
`E.innerJoin` E.table @QualificationUser
|
||||||
`E.on` (\(qual :& qualUsr) -> qual E.^. QualificationId E.==. qualUsr E.^. QualificationUserQualification))
|
`E.on` (\(qual :& qualUsr) -> qual E.^. QualificationId E.==. qualUsr E.^. QualificationUserQualification))
|
||||||
E.where_ $ -- is avs licence
|
E.where_ $ -- is avs licence
|
||||||
E.isJust (qual E.^. QualificationAvsLicence)
|
E.isJust (qual E.^. QualificationAvsLicence)
|
||||||
E.&&. (qualUsr & validQualification now) -- currently valid
|
E.&&. (qualUsr & validQualification now) -- currently valid
|
||||||
E.&&. -- matches user
|
E.&&. -- matches user
|
||||||
(qualUsr E.^. QualificationUserUser E.==. usr E.^. UserId)
|
(qualUsr E.^. QualificationUserUser E.==. usr E.^. UserId)
|
||||||
)
|
)
|
||||||
E.&&.
|
E.&&.
|
||||||
E.notExists (do -- a known AvsId
|
E.notExists (do -- a known AvsId
|
||||||
avsUsr <- E.from $ E.table @UserAvs
|
avsUsr <- E.from $ E.table @UserAvs
|
||||||
E.where_ $ avsUsr E.^. UserAvsUser E.==. usr E.^. UserId
|
E.where_ $ avsUsr E.^. UserAvsUser E.==. usr E.^. UserId
|
||||||
@ -300,133 +221,20 @@ retrieveDriversWithoutAvsId now = do
|
|||||||
|
|
||||||
|
|
||||||
allRDriversHaveFs :: UTCTime -> DB Bool
|
allRDriversHaveFs :: UTCTime -> DB Bool
|
||||||
-- allRDriversHaveFs = fmap isNothing . E.selectOne . retrieveDriversRWithoutF
|
-- allRDriversHaveFs = fmap isNothing . E.selectOne . retrieveDriversRWithoutF
|
||||||
allRDriversHaveFs = E.selectNotExists . retrieveDriversRWithoutF
|
allRDriversHaveFs = E.selectNotExists . retrieveDriversRWithoutF
|
||||||
|
|
||||||
-- | Returns users at most once, even if they own multiple avs-related licences, but no AvsID is known
|
-- | Returns users at most once, even if they own multiple avs-related licences, but no AvsID is known
|
||||||
retrieveDriversRWithoutF :: UTCTime -> E.SqlQuery (E.SqlExpr (Entity User))
|
retrieveDriversRWithoutF :: UTCTime -> E.SqlQuery (E.SqlExpr (Entity User))
|
||||||
retrieveDriversRWithoutF now = do
|
retrieveDriversRWithoutF now = do
|
||||||
usr <- E.from $ E.table @User
|
usr <- E.from $ E.table @User
|
||||||
let hasValidQual lic = do
|
let hasValidQual lic = do
|
||||||
(qual :& qualUsr) <- E.from (E.table @Qualification
|
(qual :& qualUsr) <- E.from (E.table @Qualification
|
||||||
`E.innerJoin` E.table @QualificationUser
|
`E.innerJoin` E.table @QualificationUser
|
||||||
`E.on` (\(qual :& qualUsr) -> qual E.^. QualificationId E.==. qualUsr E.^. QualificationUserQualification))
|
`E.on` (\(qual :& qualUsr) -> qual E.^. QualificationId E.==. qualUsr E.^. QualificationUserQualification))
|
||||||
E.where_ $ (qual E.^. QualificationAvsLicence E.==. E.justVal lic) -- matches licence
|
E.where_ $ (qual E.^. QualificationAvsLicence E.==. E.justVal lic) -- matches licence
|
||||||
E.&&. (qualUsr E.^. QualificationUserUser E.==. usr E.^. UserId) -- matches user
|
E.&&. (qualUsr E.^. QualificationUserUser E.==. usr E.^. UserId) -- matches user
|
||||||
E.&&. (qualUsr & validQualification now) -- currently valid
|
E.&&. (qualUsr & validQualification now) -- currently valid
|
||||||
E.where_ $ E.exists (hasValidQual AvsLicenceRollfeld)
|
E.where_ $ E.exists (hasValidQual AvsLicenceRollfeld)
|
||||||
E.&&. E.notExists (hasValidQual AvsLicenceVorfeld)
|
E.&&. E.notExists (hasValidQual AvsLicenceVorfeld)
|
||||||
return usr
|
return usr
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
type ProblemLogTableExpr = E.SqlExpr (Entity ProblemLog) `E.LeftOuterJoin` E.SqlExpr (Maybe (Entity User)) `E.LeftOuterJoin` E.SqlExpr (Maybe (Entity User))
|
|
||||||
queryProblem :: ProblemLogTableExpr -> E.SqlExpr (Entity ProblemLog)
|
|
||||||
queryProblem = $(E.sqlLOJproj 3 1)
|
|
||||||
|
|
||||||
querySolver :: ProblemLogTableExpr -> E.SqlExpr (Maybe (Entity User))
|
|
||||||
querySolver = $(E.sqlLOJproj 3 2)
|
|
||||||
|
|
||||||
queryUser :: ProblemLogTableExpr -> E.SqlExpr (Maybe (Entity User))
|
|
||||||
queryUser = $(E.sqlLOJproj 3 3)
|
|
||||||
|
|
||||||
type ProblemLogTableData = DBRow (Entity ProblemLog, Maybe (Entity User), Maybe (Entity User))
|
|
||||||
resultProblem :: Lens' ProblemLogTableData (Entity ProblemLog)
|
|
||||||
resultProblem = _dbrOutput . _1
|
|
||||||
|
|
||||||
resultSolver :: Traversal' ProblemLogTableData (Entity User)
|
|
||||||
resultSolver = _dbrOutput . _2 . _Just
|
|
||||||
|
|
||||||
resultUser :: Traversal' ProblemLogTableData (Entity User)
|
|
||||||
resultUser = _dbrOutput . _3 . _Just
|
|
||||||
|
|
||||||
mkProblemLogTable :: DB (FormResult (ProblemTableActionData, Set ProblemLogId), Widget)
|
|
||||||
mkProblemLogTable = do
|
|
||||||
-- problem_types <- E.select $ do
|
|
||||||
-- ap <- E.from $ E.table @ProblemLog
|
|
||||||
-- let res = ap E.^. ProblemLogInfo E.->>. "problem"
|
|
||||||
-- E.groupBy res
|
|
||||||
-- return res
|
|
||||||
over _1 postprocess <$> dbTable validator DBTable{..}
|
|
||||||
where
|
|
||||||
-- TODO: query to collect all occurring problem types to use as tooltip for the problem filter, so that these don't run out of synch
|
|
||||||
dbtIdent = "problem-log" :: Text
|
|
||||||
dbtSQLQuery = \(problem `E.LeftOuterJoin` solver `E.LeftOuterJoin` usr) -> do
|
|
||||||
-- EL.on (usr E.?. UserId E.==. E.text2num (problem E.^. ProblemLogInfo E.->>. "user")) -- works
|
|
||||||
EL.on (usr E.?. UserId E.==. problem E.^. ProblemLogInfo E.->>>. "user")
|
|
||||||
EL.on (solver E.?. UserId E.==. problem E.^. ProblemLogSolver)
|
|
||||||
return (problem, solver, usr)
|
|
||||||
dbtRowKey = queryProblem >>> (E.^. ProblemLogId)
|
|
||||||
dbtProj = dbtProjFilteredPostId
|
|
||||||
dbtColonnade = formColonnade $ mconcat
|
|
||||||
[ dbSelect (applying _2) id $ return . view (resultProblem . _entityKey)
|
|
||||||
, sortable (Just "time") (i18nCell MsgAdminProblemCreated) $ \( view $ resultProblem . _entityVal . _problemLogTime -> t) -> dateTimeCell t
|
|
||||||
, sortable (Just "info") (i18nCell MsgAdminProblemInfo) $ \( view $ resultProblem . _entityVal . _problemLogAdminProblem -> p) -> adminProblemCell p
|
|
||||||
-- , sortable (Just "firm") (i18nCell MsgTableCompany) $ \(preview $ resultProblem . _entityVal . _problemLogAdminProblem . _adminProblemCompany -> c) -> cellMaybe companyIdCell c
|
|
||||||
, sortable (Just "firm") (i18nCell MsgTableCompany) $ \( view $ resultProblem . _entityVal . _problemLogAdminProblem -> p) -> cellMaybe companyIdCell $ join (p ^? _adminProblemCompanyOld) <|> (p ^? _adminProblemCompany)
|
|
||||||
, sortable (Just "user") (i18nCell MsgAdminProblemUser) $ \(preview resultUser -> u) -> maybeCell u $ cellHasUserLink AdminUserR
|
|
||||||
, sortable (Just "solved") (i18nCell MsgAdminProblemSolved) $ \( view $ resultProblem . _entityVal . _problemLogSolved -> t) -> cellMaybe dateTimeCell t
|
|
||||||
, sortable (Just "solver") (i18nCell MsgAdminProblemSolver) $ \(preview resultSolver -> u) -> maybeCell u $ cellHasUserLink AdminUserR
|
|
||||||
]
|
|
||||||
dbtSorting = mconcat
|
|
||||||
[ single ("time" , SortColumn $ queryProblem >>> (E.^. ProblemLogTime))
|
|
||||||
, single ("info" , SortColumn $ queryProblem >>> (E.^. ProblemLogInfo))
|
|
||||||
-- , single ("firm" , SortColumn ((E.->>. "company" ).(queryProblem >>> (E.^. ProblemLogInfo))))
|
|
||||||
, single ("firm" , SortColumn $ \r -> queryProblem r E.^. ProblemLogInfo E.->>. "company")
|
|
||||||
, single ("user" , sortUserNameBareM queryUser)
|
|
||||||
, single ("solved", SortColumn $ queryProblem >>> (E.^. ProblemLogSolved))
|
|
||||||
, single ("solver", sortUserNameBareM querySolver)
|
|
||||||
]
|
|
||||||
dbtFilter = mconcat
|
|
||||||
[ single ("user" , FilterColumn . E.mkContainsFilterWithCommaPlus Just $ views (to queryUser) (E.?. UserDisplayName))
|
|
||||||
, single ("solver" , FilterColumn . E.mkContainsFilterWithCommaPlus Just $ views (to querySolver) (E.?. UserDisplayName))
|
|
||||||
, single ("company" , FilterColumn . E.mkContainsFilter $ views (to queryProblem) ((E.->>. "company").(E.^. ProblemLogInfo)))
|
|
||||||
, single ("solved" , FilterColumn . E.mkExactFilterLast $ views (to queryProblem) (E.isJust . (E.^. ProblemLogSolved)))
|
|
||||||
-- , single ("problem" , FilterColumn . E.mkContainsFilter $ views (to queryProblem) ((E.->>. "problem").(E.^. ProblemLogInfo))) -- not stored in plaintext!
|
|
||||||
, single ("problem" , mkFilterProjectedPost $ \(getLast -> criterion) dbr -> -- falls es nicht schnell genug ist: in dbtProj den Anzeigetext nur einmal berechnen
|
|
||||||
ifNothingM criterion True $ \(crit::Text) -> do
|
|
||||||
let problem = dbr ^. resultProblem . _entityVal . _problemLogAdminProblem
|
|
||||||
protxt <- adminProblem2Text problem
|
|
||||||
return $ crit `Text.isInfixOf` protxt
|
|
||||||
)
|
|
||||||
]
|
|
||||||
dbtFilterUI mPrev = mconcat
|
|
||||||
[ prismAForm (singletonFilter "user" . maybePrism _PathPiece) mPrev $ aopt (hoistField lift textField) (fslI MsgAdminProblemUser & setTooltip MsgTableFilterCommaPlus)
|
|
||||||
, prismAForm (singletonFilter "solver" . maybePrism _PathPiece) mPrev $ aopt (hoistField lift textField) (fslI MsgAdminProblemSolver & setTooltip MsgTableFilterCommaPlusShort)
|
|
||||||
, prismAForm (singletonFilter "problem" . maybePrism _PathPiece) mPrev $ aopt (hoistField lift textField) (fslI MsgAdminProblemInfo)
|
|
||||||
, prismAForm (singletonFilter "company" . maybePrism _PathPiece) mPrev $ aopt (hoistField lift textField) (fslI MsgTableCompanyShort)
|
|
||||||
, prismAForm (singletonFilter "solved" . maybePrism _PathPiece) mPrev $ aopt (boolField . Just $ SomeMessage MsgBoolIrrelevant) (fslI MsgAdminProblemSolved)
|
|
||||||
]
|
|
||||||
acts :: Map ProblemTableAction (AForm Handler ProblemTableActionData)
|
|
||||||
acts = mconcat
|
|
||||||
[ singletonMap ProblemTableMarkSolved $ pure ProblemTableMarkSolvedData
|
|
||||||
, singletonMap ProblemTableMarkUnsolved $ pure ProblemTableMarkUnsolvedData
|
|
||||||
]
|
|
||||||
dbtParams = DBParamsForm
|
|
||||||
{ dbParamsFormMethod = POST
|
|
||||||
, dbParamsFormAction = Nothing
|
|
||||||
, dbParamsFormAttrs = []
|
|
||||||
, dbParamsFormSubmit = FormSubmit
|
|
||||||
, dbParamsFormAdditional
|
|
||||||
= renderAForm FormStandard
|
|
||||||
$ (, mempty) . First . Just
|
|
||||||
<$> multiActionA acts (fslI MsgTableAction) (Just ProblemTableMarkSolved)
|
|
||||||
, dbParamsFormEvaluate = liftHandler . runFormPost
|
|
||||||
, dbParamsFormResult = id
|
|
||||||
, dbParamsFormIdent = def
|
|
||||||
}
|
|
||||||
dbtCsvEncode = noCsvEncode
|
|
||||||
dbtCsvDecode = Nothing
|
|
||||||
dbtExtraReps = []
|
|
||||||
dbtStyle = def { dbsFilterLayout = defaultDBSFilterLayout }
|
|
||||||
validator = def & defaultSorting [SortAscBy "time"]
|
|
||||||
& defaultFilter (singletonMap "solved" [toPathPiece False])
|
|
||||||
postprocess :: FormResult (First ProblemTableActionData, DBFormResult ProblemLogId Bool ProblemLogTableData)
|
|
||||||
-> FormResult ( ProblemTableActionData, Set ProblemLogId)
|
|
||||||
postprocess inp = do
|
|
||||||
(First (Just act), usrMap) <- inp
|
|
||||||
let usrSet = Map.keysSet . Map.filter id $ getDBFormResult (const False) usrMap
|
|
||||||
return (act, usrSet)
|
|
||||||
|
|
||||||
-- adminProblemCell :: IsDBTable m a => AdminProblem -> DBCell m a -- moved to Handler.Utils
|
|
||||||
-- msgAdminProblem :: AdminProblem -> DB (SomeMessages UniWorX) -- moved to Handler.Utils
|
|
||||||
|
|||||||
@ -9,7 +9,7 @@
|
|||||||
|
|
||||||
module Handler.Admin.Avs
|
module Handler.Admin.Avs
|
||||||
( getAdminAvsR, postAdminAvsR
|
( getAdminAvsR, postAdminAvsR
|
||||||
, getAdminAvsUserR, postAdminAvsUserR
|
, getAdminAvsUserR
|
||||||
, getProblemAvsSynchR, postProblemAvsSynchR
|
, getProblemAvsSynchR, postProblemAvsSynchR
|
||||||
, getProblemAvsErrorR
|
, getProblemAvsErrorR
|
||||||
) where
|
) where
|
||||||
@ -17,7 +17,7 @@ module Handler.Admin.Avs
|
|||||||
import Import
|
import Import
|
||||||
import qualified Control.Monad.State.Class as State
|
import qualified Control.Monad.State.Class as State
|
||||||
-- import Data.Aeson (encode)
|
-- import Data.Aeson (encode)
|
||||||
-- import qualified Data.Aeson.Encode.Pretty as Pretty
|
import qualified Data.Aeson.Encode.Pretty as Pretty
|
||||||
import qualified Data.Text as Text
|
import qualified Data.Text as Text
|
||||||
import qualified Data.CaseInsensitive as CI
|
import qualified Data.CaseInsensitive as CI
|
||||||
import qualified Data.Set as Set
|
import qualified Data.Set as Set
|
||||||
@ -27,8 +27,9 @@ import qualified Data.Map as Map
|
|||||||
import Handler.Utils
|
import Handler.Utils
|
||||||
import Handler.Utils.Avs
|
import Handler.Utils.Avs
|
||||||
-- import Handler.Utils.Qualification
|
-- import Handler.Utils.Qualification
|
||||||
import Handler.Utils.Users (getUserPrimaryCompany)
|
|
||||||
import Handler.Utils.Company (switchAvsUserCompany)
|
import Utils.Avs
|
||||||
|
|
||||||
|
|
||||||
import Database.Esqueleto.Experimental ((:&)(..))
|
import Database.Esqueleto.Experimental ((:&)(..))
|
||||||
import qualified Database.Esqueleto.Legacy as E
|
import qualified Database.Esqueleto.Legacy as E
|
||||||
@ -42,13 +43,6 @@ import qualified Database.Esqueleto.Utils as E
|
|||||||
single :: (k,a) -> Map k a
|
single :: (k,a) -> Map k a
|
||||||
single = uncurry Map.singleton
|
single = uncurry Map.singleton
|
||||||
|
|
||||||
exceptionWgt :: SomeException -> Widget
|
|
||||||
exceptionWgt (SomeException e) = [whamlet|<h2>Error:</h2> #{tshow e}|]
|
|
||||||
|
|
||||||
tryShow :: MonadCatch m => m Widget -> m Widget
|
|
||||||
tryShow act = try act >>= \case
|
|
||||||
Left err -> return $ exceptionWgt err
|
|
||||||
Right res -> return res
|
|
||||||
|
|
||||||
-- Button only needed in AVS TEST; further buttons see below
|
-- Button only needed in AVS TEST; further buttons see below
|
||||||
data ButtonAvsTest = BtnCheckLicences -- | BtnSynchLicences
|
data ButtonAvsTest = BtnCheckLicences -- | BtnSynchLicences
|
||||||
@ -59,7 +53,7 @@ instance Finite ButtonAvsTest
|
|||||||
nullaryPathPiece ''ButtonAvsTest camelToPathPiece
|
nullaryPathPiece ''ButtonAvsTest camelToPathPiece
|
||||||
|
|
||||||
instance Button UniWorX ButtonAvsTest where
|
instance Button UniWorX ButtonAvsTest where
|
||||||
btnLabel BtnCheckLicences = "Show all licence difference to current AVS" -- could be msg
|
btnLabel BtnCheckLicences = "Check all licences" -- could be msg
|
||||||
-- btnLabel BtnSynchLicences = "Synchronize all licences" -- could be msg
|
-- btnLabel BtnSynchLicences = "Synchronize all licences" -- could be msg
|
||||||
btnClasses BtnCheckLicences = [BCIsButton, BCPrimary]
|
btnClasses BtnCheckLicences = [BCIsButton, BCPrimary]
|
||||||
-- btnClasses BtnSynchLicences = [BCIsButton, BCDanger]
|
-- btnClasses BtnSynchLicences = [BCIsButton, BCDanger]
|
||||||
@ -93,7 +87,7 @@ validateAvsQueryPerson = do
|
|||||||
is _Just avsPersonQueryInternalPersonalNo ||
|
is _Just avsPersonQueryInternalPersonalNo ||
|
||||||
is _Just avsPersonQueryVersionNo
|
is _Just avsPersonQueryVersionNo
|
||||||
|
|
||||||
makeAvsStatusForm :: Maybe AvsPersonId -> Form AvsQueryStatus
|
makeAvsStatusForm :: Maybe AvsQueryStatus -> Form AvsQueryStatus
|
||||||
makeAvsStatusForm tmpl = identifyForm FIDAvsQueryStatus . validateForm validateAvsQueryStatus $ \html ->
|
makeAvsStatusForm tmpl = identifyForm FIDAvsQueryStatus . validateForm validateAvsQueryStatus $ \html ->
|
||||||
flip (renderAForm FormStandard) html $
|
flip (renderAForm FormStandard) html $
|
||||||
parseAvsIds <$> areq textField (fslI MsgAvsPersonId) (unparseAvsIds <$> tmpl)
|
parseAvsIds <$> areq textField (fslI MsgAvsPersonId) (unparseAvsIds <$> tmpl)
|
||||||
@ -103,15 +97,15 @@ makeAvsStatusForm tmpl = identifyForm FIDAvsQueryStatus . validateForm validateA
|
|||||||
where
|
where
|
||||||
nonemptys = filter (not . Text.null) $ Text.strip <$> Text.split (==',') txt
|
nonemptys = filter (not . Text.null) $ Text.strip <$> Text.split (==',') txt
|
||||||
ids = mapMaybe readMay nonemptys
|
ids = mapMaybe readMay nonemptys
|
||||||
unparseAvsIds :: AvsPersonId -> Text
|
unparseAvsIds :: AvsQueryStatus -> Text
|
||||||
unparseAvsIds = tshow . avsPersonId
|
unparseAvsIds (AvsQueryStatus ids) = Text.intercalate ", " $ tshow <$> Set.toAscList ids
|
||||||
|
|
||||||
validateAvsQueryStatus :: FormValidator AvsQueryStatus Handler ()
|
validateAvsQueryStatus :: FormValidator AvsQueryStatus Handler ()
|
||||||
validateAvsQueryStatus = do
|
validateAvsQueryStatus = do
|
||||||
AvsQueryStatus ids <- State.get
|
AvsQueryStatus ids <- State.get
|
||||||
guardValidation (MsgAvsQueryStatusInvalid $ tshow ids) $ not (null ids)
|
guardValidation (MsgAvsQueryStatusInvalid $ tshow ids) $ not (null ids)
|
||||||
|
|
||||||
makeAvsContactForm :: Maybe AvsPersonId -> Form AvsQueryContact
|
makeAvsContactForm :: Maybe AvsQueryContact -> Form AvsQueryContact
|
||||||
makeAvsContactForm tmpl = identifyForm FIDAvsQueryContact . validateForm validateAvsQueryContact $ \html ->
|
makeAvsContactForm tmpl = identifyForm FIDAvsQueryContact . validateForm validateAvsQueryContact $ \html ->
|
||||||
flip (renderAForm FormStandard) html $
|
flip (renderAForm FormStandard) html $
|
||||||
parseAvsIds <$> areq textField (fslI MsgAvsPersonId) (unparseAvsIds <$> tmpl) -- consider using cfAnySeparatedSet here
|
parseAvsIds <$> areq textField (fslI MsgAvsPersonId) (unparseAvsIds <$> tmpl) -- consider using cfAnySeparatedSet here
|
||||||
@ -121,9 +115,8 @@ makeAvsContactForm tmpl = identifyForm FIDAvsQueryContact . validateForm validat
|
|||||||
where
|
where
|
||||||
nonemptys = filter (not . Text.null) $ Text.strip <$> Text.split (==',') txt
|
nonemptys = filter (not . Text.null) $ Text.strip <$> Text.split (==',') txt
|
||||||
ids = mapMaybe (fmap AvsObjPersonId . readMay) nonemptys
|
ids = mapMaybe (fmap AvsObjPersonId . readMay) nonemptys
|
||||||
unparseAvsIds :: AvsPersonId -> Text
|
unparseAvsIds :: AvsQueryContact -> Text
|
||||||
unparseAvsIds = tshow . avsPersonId
|
unparseAvsIds (AvsQueryContact ids) = Text.intercalate ", " $ tshow <$> Set.toAscList ids
|
||||||
--unparseAvsIds (AvsQueryContact ids) = Text.intercalate ", " $ tshow <$> Set.toAscList ids
|
|
||||||
|
|
||||||
validateAvsQueryContact :: FormValidator AvsQueryContact Handler ()
|
validateAvsQueryContact :: FormValidator AvsQueryContact Handler ()
|
||||||
validateAvsQueryContact = do
|
validateAvsQueryContact = do
|
||||||
@ -147,270 +140,173 @@ postAdminAvsR = do
|
|||||||
mbAvsConf <- getsYesod $ view _appAvsConf
|
mbAvsConf <- getsYesod $ view _appAvsConf
|
||||||
let avsWgt = [whamlet|
|
let avsWgt = [whamlet|
|
||||||
$maybe avsConf <- mbAvsConf
|
$maybe avsConf <- mbAvsConf
|
||||||
<h2>
|
AVS Konfiguration ist #{decodeUtf8 (avsUser avsConf)}@#{avsHost avsConf}:#{avsPort avsConf}
|
||||||
AVS Konfiguration
|
|
||||||
<ul>
|
|
||||||
<li>
|
|
||||||
Host: #{decodeUtf8 (avsUser avsConf)}@#{avsHost avsConf}:#{avsPort avsConf}
|
|
||||||
<li>
|
|
||||||
Timeout sekundäre AVS Abfragen: #{avsTimeout avsConf}s
|
|
||||||
<li>
|
|
||||||
Cache Gültigkeit sekundäre AVS Abfragen: #{tshow (avsCacheExpiry avsConf)}
|
|
||||||
$nothing
|
$nothing
|
||||||
AVS nicht konfiguriert!
|
AVS nicht konfiguriert!
|
||||||
|]
|
|]
|
||||||
|
mAvsQuery <- getsYesod $ view _appAvsQuery
|
||||||
|
case mAvsQuery of
|
||||||
|
Nothing -> siteLayoutMsg MsgMenuAvs [whamlet|Error: AVS interface configuration is incomplete.|] -- should never occur after initilisation
|
||||||
|
Just AvsQuery{..} -> do
|
||||||
|
((presult, pwidget), penctype) <- runFormPost $ makeAvsPersonForm Nothing
|
||||||
|
|
||||||
((presult, pwidget), penctype) <- runFormPost $ makeAvsPersonForm Nothing
|
let procFormPerson fr = do
|
||||||
|
addMessage Info $ text2Html $ "Query: " <> tshow (toJSON fr)
|
||||||
|
res <- avsQueryPerson fr
|
||||||
|
case res of
|
||||||
|
Left err -> let msg = tshow err in return $ Just [whamlet|<h2>Error:</h2> #{msg}|]
|
||||||
|
Right (AvsResponsePerson pns) -> return $ Just [whamlet|
|
||||||
|
<ul>
|
||||||
|
$forall p <- pns
|
||||||
|
<li>#{decodeUtf8 (Pretty.encodePretty (toJSON p))}
|
||||||
|
|]
|
||||||
|
mbPerson <- formResultMaybe presult procFormPerson
|
||||||
|
|
||||||
let procFormPerson :: AvsQueryPerson -> Handler (Maybe (Maybe Widget, Maybe AvsPersonId))
|
((sresult, swidget), senctype) <- runFormPost $ makeAvsStatusForm Nothing
|
||||||
procFormPerson (fixAvsQueryPerson -> fr) = do
|
let procFormStatus fr = do
|
||||||
addMessage Info $ text2Html $ "Query: " <> tshow (toJSON fr)
|
addMessage Info $ text2Html $ "Status Query: " <> tshow (toJSON fr)
|
||||||
try (avsQuery fr) >>= \case
|
res <- avsQueryStatus fr
|
||||||
Left err -> return $ Just (Just $ exceptionWgt err, Nothing)
|
case res of
|
||||||
Right (AvsResponsePerson pns) -> do
|
Left err -> let msg = tshow err in return $ Just [whamlet|<h2>Error:</h2> #{msg}|]
|
||||||
let mapid = case Set.toList pns of
|
Right (AvsResponseStatus pns) -> return $ Just [whamlet|
|
||||||
[AvsDataPerson{avsPersonPersonID=apid}] -> Just apid
|
<ul>
|
||||||
_ -> Nothing
|
$forall p <- pns
|
||||||
wgt = [whamlet|
|
<li>#{decodeUtf8 (Pretty.encodePretty (toJSON p))}
|
||||||
|
|]
|
||||||
|
mbStatus <- formResultMaybe sresult procFormStatus
|
||||||
|
|
||||||
|
((cresult, cwidget), cenctype) <- runFormPost $ makeAvsContactForm Nothing
|
||||||
|
let procFormContact fr = do
|
||||||
|
addMessage Info $ text2Html $ "Contact Query: " <> tshow (toJSON fr)
|
||||||
|
res <- avsQueryContact fr
|
||||||
|
case res of
|
||||||
|
Left err -> let msg = tshow err in return $ Just [whamlet|<h2>Error:</h2> #{msg}|]
|
||||||
|
Right (AvsResponseContact pns) -> return $ Just [whamlet|
|
||||||
|
<ul>
|
||||||
|
$forall AvsDataContact{..} <- pns
|
||||||
|
<li>
|
||||||
<ul>
|
<ul>
|
||||||
$forall p <- pns
|
<li>AvsId: #{tshow avsContactPersonID}
|
||||||
<li>^{jsonWidget p}
|
<li>#{decodeUtf8 (Pretty.encodePretty (toJSON avsContactPersonInfo))}
|
||||||
|] -- <li>#{decodeUtf8 (Pretty.encodePretty (toJSON p))}
|
<li>#{decodeUtf8 (Pretty.encodePretty (toJSON avsContactFirmInfo))}
|
||||||
return $ Just (toMaybe (notNull pns) wgt, mapid)
|
|]
|
||||||
(mbPerson,mapid) <- fromMaybe (Nothing,Nothing) <$> formResultMaybe presult procFormPerson
|
mbContact <- formResultMaybe cresult procFormContact
|
||||||
|
|
||||||
((sresult', swidget), senctype) <- runFormPost $ makeAvsStatusForm mapid
|
|
||||||
let sresult = sresult' <|> maybe FormMissing (FormSuccess . AvsQueryStatus . Set.singleton) mapid -- use unique AvsId from PersonSearch for convenience, if form was empty
|
|
||||||
procFormStatus fr = do
|
|
||||||
addMessage Info $ text2Html $ "Status Query: " <> tshow (toJSON fr)
|
|
||||||
tryShow $ do
|
|
||||||
AvsResponseStatus pns <- avsQuery fr
|
|
||||||
return [whamlet|
|
|
||||||
<ul>
|
|
||||||
$forall p <- pns
|
|
||||||
<li>^{jsonWidget p}
|
|
||||||
|]
|
|
||||||
mbStatus <- formResultMaybe sresult (Just <<$>> procFormStatus)
|
|
||||||
|
|
||||||
((cresult', cwidget), cenctype) <- runFormPost $ makeAvsContactForm mapid
|
((crUsrRes, crUsrWgt), crUsrEnctype) <- runFormPost $ identifyForm FIDAvsCreateUser $ \html ->
|
||||||
let cresult = cresult' <|> maybe FormMissing (FormSuccess . AvsQueryContact . Set.singleton . AvsObjPersonId) mapid -- use unique AvsId from PersonSearch for convenience, if form was empty
|
flip (renderAForm FormStandard) html $ areq textField (fslI MsgAvsCardNo) Nothing
|
||||||
procFormContact fr = do
|
let procFormCrUsr fr = do
|
||||||
addMessage Info $ text2Html $ "Contact Query: " <> tshow (toJSON fr)
|
-- addMessage Info $ text2Html $ "Query: " <> tshow (toJSON fr)
|
||||||
tryShow $ do
|
res <- try $ guessAvsUser fr
|
||||||
AvsResponseContact pns <- avsQuery fr
|
case res of
|
||||||
return [whamlet|
|
(Right (Just uid)) -> do
|
||||||
<ul>
|
uuid :: CryptoUUIDUser <- encrypt uid
|
||||||
$forall AvsDataContact{..} <- pns
|
return $ Just [whamlet|<h2>Success:</h2> <a href=@{ForProfileR uuid}>User created or updated.|]
|
||||||
<li>
|
(Right Nothing) ->
|
||||||
|
return $ Just [whamlet|<h2>Warning:</h2> No user found.|]
|
||||||
|
(Left e) -> do
|
||||||
|
let msg = tshow (e :: SomeException)
|
||||||
|
return $ Just [whamlet|<h2>Error:</h2> #{msg}|]
|
||||||
|
mbCrUser <- formResultMaybe crUsrRes procFormCrUsr
|
||||||
|
|
||||||
|
((getLicRes, getLicWgt), getLicEnctype) <- runFormPost $ identifyForm FIDAvsQueryLicence $ \html ->
|
||||||
|
flip (renderAForm FormStandard) html $ (,,) <$> aopt intField (fslI $ text2message "Min AvsPersonId") Nothing
|
||||||
|
<*> aopt intField (fslI $ text2message "Max AvsPersonId") Nothing
|
||||||
|
<*> aopt (selectField $ return avsLicenceOptions) (fslI MsgAvsLicence) Nothing
|
||||||
|
let procFormGetLic fr = do
|
||||||
|
res <- avsQueryGetAllLicences
|
||||||
|
case res of
|
||||||
|
(Right (AvsResponseGetLicences lics)) -> do
|
||||||
|
let flics = Set.toList $ Set.filter lfltr lics
|
||||||
|
lfltr = case fr of -- not pretty, but it'll do
|
||||||
|
(Just idmin, Just idmax, Just lic) -> \AvsPersonLicence{..} -> (avsLicenceRampLicence == lic) && (avsLicencePersonID `inBetween` (AvsPersonId idmin, AvsPersonId idmax))
|
||||||
|
(Just idmin, Nothing, Just lic) -> \AvsPersonLicence{..} -> (avsLicenceRampLicence == lic) && (avsLicencePersonID == AvsPersonId idmin)
|
||||||
|
(Nothing , Just idmax, Just lic) -> \AvsPersonLicence{..} -> (avsLicenceRampLicence == lic) && (avsLicencePersonID == AvsPersonId idmax)
|
||||||
|
(Nothing , Nothing, Just lic) -> \AvsPersonLicence{..} -> avsLicenceRampLicence == lic
|
||||||
|
(Just idmin, Just idmax, Nothing ) -> (`inBetween` (AvsPersonId idmin, AvsPersonId idmax)) . avsLicencePersonID
|
||||||
|
(Just idmin, Nothing, Nothing ) -> (== AvsPersonId idmin) . avsLicencePersonID
|
||||||
|
(Nothing , Just idmax, Nothing ) -> (== AvsPersonId idmax) . avsLicencePersonID
|
||||||
|
(Nothing , Nothing, Nothing ) -> const True
|
||||||
|
addMessage Info $ text2Html $ "Query returned " <> tshow (length flics) <> " licences."
|
||||||
|
return $ Just [whamlet|
|
||||||
|
<h2>Success:</h2>
|
||||||
<ul>
|
<ul>
|
||||||
<li>AvsId: #{tshow avsContactPersonID}
|
$forall AvsPersonLicence{..} <- flics
|
||||||
<li>^{jsonWidget avsContactPersonInfo}
|
<li> #{tshow avsLicencePersonID}: #{licence2char avsLicenceRampLicence}
|
||||||
<li>^{jsonWidget avsContactFirmInfo}
|
|]
|
||||||
|] -- <li>#{decodeUtf8 (Pretty.encodePretty (toJSON avsContactPersonInfo))}
|
|
||||||
mbContact <- formResultMaybe cresult (Just <<$>> procFormContact)
|
(Left err) -> do
|
||||||
|
let msg = tshow err
|
||||||
|
return $ Just [whamlet|<h2>Error:</h2> #{msg}|]
|
||||||
|
mbGetLic <- formResultMaybe getLicRes procFormGetLic
|
||||||
|
|
||||||
|
((setLicRes, setLicWgt), setLicEnctype) <- runFormPost $ identifyForm FIDAvsSetLicence $ \html ->
|
||||||
|
flip (renderAForm FormStandard) html $ (,) <$> areq intField (fslI MsgAvsPersonId) Nothing
|
||||||
|
<*> areq (selectField $ return avsLicenceOptions) (fslI MsgAvsLicence) (Just AvsLicenceVorfeld)
|
||||||
|
let procFormSetLic (aid, lic) = do
|
||||||
|
res <- try $ setLicenceAvs (AvsPersonId aid) lic
|
||||||
|
case res of
|
||||||
|
(Right True) ->
|
||||||
|
return $ Just [whamlet|<h2>Success:</h2> Licence #{tshow (licence2char lic)} set for #{tshow aid}.|]
|
||||||
|
(Right False) ->
|
||||||
|
return $ Just [whamlet|<h2>Error:</h2> Licence could not be set for #{tshow aid}.|]
|
||||||
|
(Left e) -> do
|
||||||
|
let msg = tshow (e :: SomeException)
|
||||||
|
return $ Just [whamlet|<h2>Error:</h2> #{msg}|]
|
||||||
|
mbSetLic <- formResultMaybe setLicRes procFormSetLic
|
||||||
|
|
||||||
|
|
||||||
((crUsrRes, crUsrWgt), crUsrEnctype) <- runFormPost $ identifyForm FIDAvsCreateUser $ \html ->
|
(qryLicForm, qryLicRes) <- runButtonForm FIDAvsQueryLicenceDiffs
|
||||||
flip (renderAForm FormStandard) html $ areq textField (fslI MsgAvsCardNo) Nothing
|
mbQryLic <- case qryLicRes of
|
||||||
let procFormCrUsr fr = do
|
Nothing -> return Nothing
|
||||||
-- addMessage Info $ text2Html $ "Query: " <> tshow (toJSON fr)
|
(Just BtnCheckLicences) -> do
|
||||||
res <- try $ guessAvsUser fr
|
res <- try $ do
|
||||||
case res of
|
allLicences <- throwLeftM avsQueryGetAllLicences
|
||||||
(Right (Just uid)) -> do
|
computeDifferingLicences allLicences
|
||||||
uuid :: CryptoUUIDUser <- encrypt uid
|
case res of
|
||||||
return $ Just [whamlet|<h2>Success:</h2> <a href=@{ForProfileR uuid}>User created or updated.|]
|
(Right diffs) -> do
|
||||||
(Right Nothing) ->
|
let showLics l = Text.intercalate ", " $ fmap (tshow . avsLicencePersonID) $ Set.toList $ Set.filter ((l ==) . avsLicenceRampLicence) diffs
|
||||||
return $ Just [whamlet|<h2>Warning:</h2> No user found.|]
|
r_grant = showLics AvsLicenceRollfeld
|
||||||
(Left e) -> return $ Just $ exceptionWgt e
|
f_set = showLics AvsLicenceVorfeld
|
||||||
mbCrUser <- formResultMaybe crUsrRes procFormCrUsr
|
revoke = showLics AvsNoLicence
|
||||||
|
return $ Just [whamlet|
|
||||||
((getLicRes, getLicWgt), getLicEnctype) <- runFormPost $ identifyForm FIDAvsQueryLicence $ \html ->
|
<h2>Licence check differences:
|
||||||
flip (renderAForm FormStandard) html $ (,,) <$> aopt intField (fslI $ text2message "Min AvsPersonId") Nothing
|
<h3>Grant R:
|
||||||
<*> aopt intField (fslI $ text2message "Max AvsPersonId") Nothing
|
<p>
|
||||||
<*> aopt (selectField $ return avsLicenceOptions) (fslI MsgAvsLicence) Nothing
|
#{r_grant}
|
||||||
let procFormGetLic fr = tryShow $ do
|
<h3>Set to F:
|
||||||
AvsResponseGetLicences lics <- avsQuery AvsQueryGetAllLicences
|
<p>
|
||||||
let flics = Set.toList $ Set.filter lfltr lics
|
#{f_set}
|
||||||
lfltr = case fr of -- not pretty, but it'll do
|
<h3>Revoke licence:
|
||||||
(Just idmin, Just idmax, Just lic) -> \AvsPersonLicence{..} -> (avsLicenceRampLicence == lic) && (avsLicencePersonID `inBetween` (AvsPersonId idmin, AvsPersonId idmax))
|
<p>
|
||||||
(Just idmin, Nothing, Just lic) -> \AvsPersonLicence{..} -> (avsLicenceRampLicence == lic) && (avsLicencePersonID == AvsPersonId idmin)
|
#{revoke}
|
||||||
(Nothing , Just idmax, Just lic) -> \AvsPersonLicence{..} -> (avsLicenceRampLicence == lic) && (avsLicencePersonID == AvsPersonId idmax)
|
|
||||||
(Nothing , Nothing, Just lic) -> \AvsPersonLicence{..} -> avsLicenceRampLicence == lic
|
|
||||||
(Just idmin, Just idmax, Nothing ) -> (`inBetween` (AvsPersonId idmin, AvsPersonId idmax)) . avsLicencePersonID
|
|
||||||
(Just idmin, Nothing, Nothing ) -> (== AvsPersonId idmin) . avsLicencePersonID
|
|
||||||
(Nothing , Just idmax, Nothing ) -> (== AvsPersonId idmax) . avsLicencePersonID
|
|
||||||
(Nothing , Nothing, Nothing ) -> const True
|
|
||||||
addMessage Info $ text2Html $ "Query returned " <> tshow (length flics) <> " licences."
|
|
||||||
return [whamlet|
|
|
||||||
<h2>Success:</h2>
|
|
||||||
<ul>
|
|
||||||
$forall AvsPersonLicence{..} <- flics
|
|
||||||
<li> #{tshow avsLicencePersonID}: #{licence2char avsLicenceRampLicence}
|
|
||||||
|]
|
|
||||||
mbGetLic <- formResultMaybe getLicRes (Just <<$>> procFormGetLic)
|
|
||||||
|
|
||||||
((setLicRes, setLicWgt), setLicEnctype) <- runFormPost $ identifyForm FIDAvsSetLicence $ \html ->
|
|
||||||
flip (renderAForm FormStandard) html $ (,) <$> areq intField (fslI MsgAvsPersonId) Nothing
|
|
||||||
<*> areq (selectField $ return avsLicenceOptions) (fslI MsgAvsLicence) (Just AvsLicenceVorfeld)
|
|
||||||
let procFormSetLic (aid, lic) = do
|
|
||||||
res <- try $ setLicenceAvs (AvsPersonId aid) lic
|
|
||||||
case res of
|
|
||||||
(Right True) ->
|
|
||||||
return $ Just [whamlet|<h2>Success:</h2> Licence #{tshow (licence2char lic)} set for #{tshow aid}.|]
|
|
||||||
(Right False) ->
|
|
||||||
return $ Just [whamlet|<h2>Error:</h2> Licence could not be set for #{tshow aid}.|]
|
|
||||||
(Left e) -> do
|
|
||||||
let msg = tshow (e :: SomeException)
|
|
||||||
return $ Just [whamlet|<h2>Error:</h2> #{msg}|]
|
|
||||||
mbSetLic <- formResultMaybe setLicRes procFormSetLic
|
|
||||||
|
|
||||||
|
|
||||||
(qryLicForm, qryLicRes) <- runButtonForm FIDAvsQueryLicenceDiffs
|
|
||||||
(mbQryLic :: Maybe Widget, mbAutoDiffs :: Maybe Html) <- case qryLicRes of
|
|
||||||
Nothing -> return mempty
|
|
||||||
(Just BtnCheckLicences) -> do
|
|
||||||
res <- try $ do
|
|
||||||
allLicences <- avsQueryNoCache AvsQueryGetAllLicences
|
|
||||||
computeDifferingLicences allLicences
|
|
||||||
basediffs <- case res of
|
|
||||||
(Right diffs) -> do
|
|
||||||
let showLics l =
|
|
||||||
let chgs = Set.filter ((l ==) . avsLicenceRampLicence) diffs
|
|
||||||
in if Set.null chgs
|
|
||||||
then ("[ ]", 0)
|
|
||||||
else (Text.intercalate ", " (tshow . avsLicencePersonID <$> Set.toList chgs), Set.size chgs)
|
|
||||||
(r_grant, rg_size) = showLics AvsLicenceRollfeld
|
|
||||||
(f_set , fs_size) = showLics AvsLicenceVorfeld
|
|
||||||
(revoke , rv_size) = showLics AvsNoLicence
|
|
||||||
return $ Just [whamlet|
|
|
||||||
<h2>Licence check AVS-ID differences:
|
|
||||||
<dl .deflist>
|
|
||||||
<dt .deflist__dt>Grant R (#{rg_size}):
|
|
||||||
<dd .deflist__dd>#{r_grant}
|
|
||||||
|
|
||||||
<dt .deflist__dt>Set to F (#{fs_size}):
|
|
||||||
<dd .deflist__dd>#{f_set}
|
|
||||||
|
|
||||||
<dt .deflist__dt>Revoke licence (#{rv_size}):
|
|
||||||
<dd .deflist__dd>#{revoke}
|
|
||||||
|]
|
|
||||||
(Left e) -> do
|
|
||||||
let msg = tshow (e :: SomeException)
|
|
||||||
return $ Just [whamlet|<h2>Licence check error:</h2> #{msg}|]
|
|
||||||
autoDiffs <- do
|
|
||||||
-- what follows is copy of the code from Jobs.Handler.SynchroniseAvs.dispatchJobSynchroniseAvsLicences modified to not do anything actually
|
|
||||||
AvsLicenceSynchConf
|
|
||||||
{ avsLicenceSynchLevel = synchLevel -- SynchLevel corresponds to tables of ProblemAvsSynchR: 4=top grant R, 3= reduce R->F, 2= grant F, 1= revoke F
|
|
||||||
, avsLicenceSynchReasonFilter = reasonFilter
|
|
||||||
, avsLicenceSynchMaxChanges = maxChanges
|
|
||||||
} <- getsYesod $ view _appAvsLicenceSynchConf
|
|
||||||
guardMonoidM (synchLevel > 0) $ do
|
|
||||||
let showApids apids
|
|
||||||
| null apids = "[ ]"
|
|
||||||
| otherwise = Text.intercalate ", " (tshow <$> Set.toList apids)
|
|
||||||
procLic :: (Ord a, Show a) => AvsLicence -> Bool -> Set a -> Html
|
|
||||||
procLic aLic up apids
|
|
||||||
| n <- Set.size apids, n > 0 =
|
|
||||||
let subtype = Text.cons (bool '↧' '↥' up) $ Text.singleton $ licence2char aLic
|
|
||||||
in if NTop (Just n) <= NTop maxChanges
|
|
||||||
then
|
|
||||||
[shamlet|
|
|
||||||
<dt .deflist__dt>#{subtype} (#{n}):
|
|
||||||
<dd .deflist__dd>#{showApids apids}
|
|
||||||
|]
|
|
||||||
else
|
|
||||||
[shamlet|
|
|
||||||
<dt .deflist__dt>#{subtype} (#{n}):
|
|
||||||
<dd .deflist__dd>Too many changes at once. Consider increasing avs-licence-synch-max-changes #{tshow maxChanges}
|
|
||||||
|]
|
|
||||||
| otherwise = mempty
|
|
||||||
|
|
||||||
(AvsLicenceDifferences{..}, rsChanged) <- retrieveDifferingLicences
|
|
||||||
-- prevent automatic changes to users blocked with certain reasons and with currently being associated with multiple companies
|
|
||||||
reasonFltrdIds <- ifNothingM reasonFilter mempty $ \reasons -> do
|
|
||||||
now <- liftIO getCurrentTime
|
|
||||||
firmBlocks <- runDBRead $ E.select $ do
|
|
||||||
(uavs :& _qualUser :& qblock) <- X.from $ E.table @UserAvs
|
|
||||||
`E.innerJoin` E.table @QualificationUser `X.on` (\( uavs :& qualUser) -> uavs E.^. UserAvsUser E.==. qualUser E.^. QualificationUserUser)
|
|
||||||
`E.innerJoin` E.table @QualificationUserBlock `X.on` (\(_uavs :& qualUser :& qblock) ->
|
|
||||||
qualUser E.^. QualificationUserId E.==. qblock E.^. QualificationUserBlockQualificationUser
|
|
||||||
E.&&. qblock `isLatestBlockBefore'` E.val now)
|
|
||||||
E.where_ $ (qblock E.^. QualificationUserBlockReason E.~*. E.val reasons)
|
|
||||||
E.&&. uavs E.^. UserAvsPersonId `E.in_` E.vals (Set.unions [avsLicenceDiffRevokeAll, avsLicenceDiffRevokeRollfeld, avsLicenceDiffGrantVorfeld, avsLicenceDiffGrantRollfeld])
|
|
||||||
return $ uavs E.^. UserAvsPersonId
|
|
||||||
return $ Set.fromList $ map E.unValue firmBlocks
|
|
||||||
|
|
||||||
let fltrIds
|
|
||||||
| synchLevel >= 5 = id
|
|
||||||
| synchLevel >= 3 = flip Set.difference reasonFltrdIds
|
|
||||||
| otherwise = flip Set.difference $ reasonFltrdIds `Set.union` rsChanged
|
|
||||||
|
|
||||||
l1 = guardMonoid (synchLevel >= 1) $ procLic AvsNoLicence False $ fltrIds avsLicenceDiffRevokeAll --revoke Vorfeld and maybe also Rollfeld
|
|
||||||
l2 = guardMonoid (synchLevel >= 2) $ procLic AvsLicenceVorfeld True $ fltrIds avsLicenceDiffGrantVorfeld --grant Vorfeld
|
|
||||||
l3 = guardMonoid (synchLevel >= 3) $ procLic AvsLicenceVorfeld False $ fltrIds avsLicenceDiffRevokeRollfeld --downgrade Rollfeld -> Vorfeld
|
|
||||||
l4 = guardMonoid (synchLevel >= 4) $ procLic AvsLicenceRollfeld True $ fltrIds avsLicenceDiffGrantRollfeld --grant Rollfeld
|
|
||||||
avsIdChanges = [shamlet|
|
|
||||||
<h3>
|
|
||||||
Next automatic AVS-ID licence synchronisation:
|
|
||||||
<dl .deflist>
|
|
||||||
^{l4}
|
|
||||||
^{l3}
|
|
||||||
^{l2}
|
|
||||||
^{l1}
|
|
||||||
$maybe reason <- reasonFilter
|
|
||||||
<dt .deflist__dt>Filtered "#{reason}" (#{Set.size reasonFltrdIds}):
|
|
||||||
<dd .deflist__dd>#{showApids reasonFltrdIds}
|
|
||||||
|]
|
|]
|
||||||
----------------------------------------------------
|
(Left e) -> do
|
||||||
-- translate AVS-IDs to AVS-NOs for convenience only
|
let msg = tshow (e :: SomeException)
|
||||||
avsidnos <- runDBRead $ E.select $ do
|
return $ Just [whamlet|<h2>Licence check error:</h2> #{msg}|]
|
||||||
ua <- X.from $ E.table @UserAvs
|
-- (Just BtnSynchLicences) -> do
|
||||||
E.where_ $ ua E.^. UserAvsPersonId `E.in_` E.vals (Set.unions [avsLicenceDiffRevokeAll, avsLicenceDiffRevokeRollfeld, avsLicenceDiffGrantVorfeld, avsLicenceDiffGrantRollfeld]) -- , reasonFltrdIds])
|
-- res <- try synchAvsLicences
|
||||||
return (ua E.^. UserAvsPersonId, ua E.^. UserAvsNoPerson)
|
-- case res of
|
||||||
let id2no = Map.fromList $ $(E.unValueN 2) <$> avsidnos
|
-- (Right True) ->
|
||||||
translate = setMapMaybe (`Map.lookup` id2no)
|
-- return $ Just [whamlet|<h2>Success:</h2> Licences sychronized.|]
|
||||||
l1' = guardMonoid (synchLevel >= 1) $ procLic AvsNoLicence False $ translate $ fltrIds avsLicenceDiffRevokeAll --revoke Vorfeld and maybe also Rollfeld
|
-- (Right False) ->
|
||||||
l2' = guardMonoid (synchLevel >= 2) $ procLic AvsLicenceVorfeld True $ translate $ fltrIds avsLicenceDiffGrantVorfeld --grant Vorfeld
|
-- return $ Just [whamlet|<h2>Error:</h2> Licences could not be synchronized, see error log.|]
|
||||||
l3' = guardMonoid (synchLevel >= 3) $ procLic AvsLicenceVorfeld False $ translate $ fltrIds avsLicenceDiffRevokeRollfeld --downgrade Rollfeld -> Vorfeld
|
-- (Left e) -> do
|
||||||
l4' = guardMonoid (synchLevel >= 4) $ procLic AvsLicenceRollfeld True $ translate $ fltrIds avsLicenceDiffGrantRollfeld --grant Rollfeld
|
-- let msg = tshow (e :: SomeException)
|
||||||
autoNoDiffs = [shamlet|
|
-- return $ Just [whamlet|<h2>Licence synchronisation error:</h2> #{msg}|]
|
||||||
<h3>
|
|
||||||
Next automatic licence changes translated to human readable AVS-Numbers, if known:
|
|
||||||
<dl .deflist>
|
|
||||||
^{l4'}
|
|
||||||
^{l3'}
|
|
||||||
^{l2'}
|
|
||||||
^{l1'}
|
|
||||||
$maybe reason <- reasonFilter
|
|
||||||
<dt .deflist__dt>Filtered "#{reason}" (#{Set.size reasonFltrdIds}):
|
|
||||||
<dd .deflist__dd>#{showApids $ translate reasonFltrdIds}
|
|
||||||
|]
|
|
||||||
return $ Just $ avsIdChanges <> autoNoDiffs
|
|
||||||
return (basediffs, autoDiffs)
|
|
||||||
|
|
||||||
-- (Just BtnSynchLicences) -> do
|
actionUrl <- fromMaybe AdminAvsR <$> getCurrentRoute
|
||||||
-- res <- try synchAvsLicences
|
siteLayoutMsg MsgMenuAvs $ do
|
||||||
-- case res of
|
setTitleI MsgMenuAvs
|
||||||
-- (Right True) ->
|
let wrapFormHere fw fe = wrapForm fw def { formAction = Just $ SomeRoute actionUrl, formEncoding = fe }
|
||||||
-- return $ Just [whamlet|<h2>Success:</h2> Licences sychronized.|]
|
personForm = wrapFormHere pwidget penctype
|
||||||
-- (Right False) ->
|
statusForm = wrapFormHere swidget senctype
|
||||||
-- return $ Just [whamlet|<h2>Error:</h2> Licences could not be synchronized, see error log.|]
|
contactForm = wrapFormHere cwidget cenctype
|
||||||
-- (Left e) -> do
|
crUsrForm = wrapFormHere crUsrWgt crUsrEnctype
|
||||||
-- let msg = tshow (e :: SomeException)
|
getLicForm = wrapFormHere getLicWgt getLicEnctype
|
||||||
-- return $ Just [whamlet|<h2>Licence synchronisation error:</h2> #{msg}|]
|
setLicForm = wrapFormHere setLicWgt setLicEnctype
|
||||||
|
-- TODO: use i18nWidgetFile instead if this is to become permanent
|
||||||
actionUrl <- fromMaybe AdminAvsR <$> getCurrentRoute
|
$(widgetFile "avs")
|
||||||
siteLayoutMsg MsgMenuAvs $ do
|
|
||||||
setTitleI MsgMenuAvs
|
|
||||||
let wrapFormHere fw fe = wrapForm fw def { formAction = Just $ SomeRoute actionUrl, formEncoding = fe }
|
|
||||||
personForm = wrapFormHere pwidget penctype
|
|
||||||
statusForm = wrapFormHere swidget senctype
|
|
||||||
contactForm = wrapFormHere cwidget cenctype
|
|
||||||
crUsrForm = wrapFormHere crUsrWgt crUsrEnctype
|
|
||||||
getLicForm = wrapFormHere getLicWgt getLicEnctype
|
|
||||||
setLicForm = wrapFormHere setLicWgt setLicEnctype
|
|
||||||
-- TODO: use i18nWidgetFile instead if this is to become permanent
|
|
||||||
$(widgetFile "avs")
|
|
||||||
|
|
||||||
{-
|
{-
|
||||||
|
|
||||||
@ -473,8 +369,8 @@ postProblemAvsSynchR = getProblemAvsSynchR
|
|||||||
getProblemAvsSynchR = do
|
getProblemAvsSynchR = do
|
||||||
let catchAllAvs' r = flip catch (\err -> addMessageModal Error (i18n MsgAvsCommunicationError) (Right (text2widget $ tshow (err :: SomeException))) >> redirect r)
|
let catchAllAvs' r = flip catch (\err -> addMessageModal Error (i18n MsgAvsCommunicationError) (Right (text2widget $ tshow (err :: SomeException))) >> redirect r)
|
||||||
catchAllAvs = catchAllAvs' ProblemAvsSynchR -- == current route; use only in conditions that are not repeated upon reload; do not call redirect within catchAllAvs actions!
|
catchAllAvs = catchAllAvs' ProblemAvsSynchR -- == current route; use only in conditions that are not repeated upon reload; do not call redirect within catchAllAvs actions!
|
||||||
((AvsLicenceDifferences{..}, rsChanged), apidStatus) <- catchAllAvs' AdminR retrieveDifferingLicencesStatus
|
(AvsLicenceDifferences{..}, apidStatus) <- catchAllAvs' AdminR retrieveDifferingLicencesStatus
|
||||||
let mkLicTbl = mkLicenceTable apidStatus rsChanged
|
|
||||||
--
|
--
|
||||||
unknownLicenceOwners' <- whenNonEmpty avsLicenceDiffRevokeAll $ \neZeros ->
|
unknownLicenceOwners' <- whenNonEmpty avsLicenceDiffRevokeAll $ \neZeros ->
|
||||||
runDB $ E.select $ do
|
runDB $ E.select $ do
|
||||||
@ -487,7 +383,7 @@ getProblemAvsSynchR = do
|
|||||||
numUnknownLicenceOwners = length unknownLicenceOwners
|
numUnknownLicenceOwners = length unknownLicenceOwners
|
||||||
|
|
||||||
(btnImportUnknownWgt, btnImportUnknownRes) <- runButtonFormHash unknownLicenceOwners FIDBtnAvsImportUnknown
|
(btnImportUnknownWgt, btnImportUnknownRes) <- runButtonFormHash unknownLicenceOwners FIDBtnAvsImportUnknown
|
||||||
ifNothingM btnImportUnknownRes () $ \BtnAvsImportUnknown -> do
|
ifMaybeM btnImportUnknownRes () $ \BtnAvsImportUnknown -> do
|
||||||
res <- catchAllAvs $ forM (take 500 unknownLicenceOwners) $ try . upsertAvsUserById -- TODO: turn this into a background job
|
res <- catchAllAvs $ forM (take 500 unknownLicenceOwners) $ try . upsertAvsUserById -- TODO: turn this into a background job
|
||||||
let procRes (Right _) = (Sum 1, mempty :: Set.Set AvsPersonId, mempty :: Set.Set AvsPersonId, mempty)
|
let procRes (Right _) = (Sum 1, mempty :: Set.Set AvsPersonId, mempty :: Set.Set AvsPersonId, mempty)
|
||||||
--TODO: continue here!
|
--TODO: continue here!
|
||||||
@ -518,7 +414,7 @@ getProblemAvsSynchR = do
|
|||||||
^{revokeUnknownExecWgt}
|
^{revokeUnknownExecWgt}
|
||||||
|]
|
|]
|
||||||
|
|
||||||
ifNothingM btnRevokeUnknownRes () $ \BtnAvsRevokeUnknown -> do
|
ifMaybeM btnRevokeUnknownRes () $ \BtnAvsRevokeUnknown -> do
|
||||||
let revokes = Set.map (AvsPersonLicence AvsNoLicence) $ Set.fromList unknownLicenceOwners
|
let revokes = Set.map (AvsPersonLicence AvsNoLicence) $ Set.fromList unknownLicenceOwners
|
||||||
no_revokes = Set.size revokes
|
no_revokes = Set.size revokes
|
||||||
oks <- catchAllAvs $ setLicencesAvs revokes
|
oks <- catchAllAvs $ setLicencesAvs revokes
|
||||||
@ -529,10 +425,10 @@ getProblemAvsSynchR = do
|
|||||||
|
|
||||||
-- licence differences
|
-- licence differences
|
||||||
((tres0,tb0),(tres1up,tb1up),(tres1down,tb1down),(tres2,tb2)) <- runDB $ (,,,)
|
((tres0,tb0),(tres1up,tb1up),(tres1down,tb1down),(tres2,tb2)) <- runDB $ (,,,)
|
||||||
<$> mkLicTbl "avsLicDiffRevokeVorfeld" AvsLicenceVorfeld avsLicenceDiffRevokeAll
|
<$> mkLicenceTable apidStatus "avsLicDiffRevokeVorfeld" AvsLicenceVorfeld avsLicenceDiffRevokeAll
|
||||||
<*> mkLicTbl "avsLicDiffGrantVorfeld" AvsNoLicence avsLicenceDiffGrantVorfeld
|
<*> mkLicenceTable apidStatus "avsLicDiffGrantVorfeld" AvsNoLicence avsLicenceDiffGrantVorfeld
|
||||||
<*> mkLicTbl "avsLicDiffRevokeRollfeld" AvsLicenceRollfeld avsLicenceDiffRevokeRollfeld -- downgrade to Vorfeld
|
<*> mkLicenceTable apidStatus "avsLicDiffRevokeRollfeld" AvsLicenceRollfeld avsLicenceDiffRevokeRollfeld
|
||||||
<*> mkLicTbl "avsLicDiffGrantRollfeld" AvsNoLicence avsLicenceDiffGrantRollfeld
|
<*> mkLicenceTable apidStatus "avsLicDiffGrantRollfeld" AvsNoLicence avsLicenceDiffGrantRollfeld
|
||||||
|
|
||||||
now <- liftIO getCurrentTime
|
now <- liftIO getCurrentTime
|
||||||
let procRes :: AvsLicence -> (LicenceTableActionData, Set AvsPersonId) -> Handler ()
|
let procRes :: AvsLicence -> (LicenceTableActionData, Set AvsPersonId) -> Handler ()
|
||||||
@ -545,8 +441,8 @@ getProblemAvsSynchR = do
|
|||||||
|
|
||||||
procRes alic (LicenceTableRevokeFDriveData{..}, apids) = do
|
procRes alic (LicenceTableRevokeFDriveData{..}, apids) = do
|
||||||
oks <- runDB $ do
|
oks <- runDB $ do
|
||||||
qIds <- selectKeysList [QualificationAvsLicence ==. Just alic] [] -- sanity check
|
qId <- getKeyBy404 $ UniqueQualificationAvsLicence $ Just alic
|
||||||
if licenceTableChangeFDriveQId `notElem` qIds
|
if qId /= licenceTableChangeFDriveQId
|
||||||
then return (-1)
|
then return (-1)
|
||||||
else do
|
else do
|
||||||
uids <- view _userAvsUser <<$>> selectList [UserAvsPersonId <-. Set.toList apids] []
|
uids <- view _userAvsUser <<$>> selectList [UserAvsPersonId <-. Set.toList apids] []
|
||||||
@ -571,7 +467,6 @@ getProblemAvsSynchR = do
|
|||||||
formResult tres1up $ procRes AvsLicenceVorfeld
|
formResult tres1up $ procRes AvsLicenceVorfeld
|
||||||
formResult tres0 $ procRes AvsNoLicence
|
formResult tres0 $ procRes AvsNoLicence
|
||||||
|
|
||||||
AvsLicenceSynchConf{..} <- getsYesod $ view _appAvsLicenceSynchConf
|
|
||||||
siteLayoutMsg MsgAvsTitleLicenceSynch $ do
|
siteLayoutMsg MsgAvsTitleLicenceSynch $ do
|
||||||
setTitleI MsgAvsTitleLicenceSynch
|
setTitleI MsgAvsTitleLicenceSynch
|
||||||
$(i18nWidgetFile "avs-synchronisation")
|
$(i18nWidgetFile "avs-synchronisation")
|
||||||
@ -624,17 +519,14 @@ instance HasUser LicenceTableData where
|
|||||||
-- instance HasQualificationUser LicenceTableData where -- Not possible, since not all rows have a QualificationUser
|
-- instance HasQualificationUser LicenceTableData where -- Not possible, since not all rows have a QualificationUser
|
||||||
-- hasQualificationUser = resultQualUser . _entityVal
|
-- hasQualificationUser = resultQualUser . _entityVal
|
||||||
|
|
||||||
mkLicenceTable :: AvsPersonIdMapPersonCard -> Set AvsPersonId -> Text -> AvsLicence -> Set AvsPersonId -> DB (FormResult (LicenceTableActionData, Set AvsPersonId), Widget)
|
mkLicenceTable :: AvsPersonIdMapPersonCard -> Text -> AvsLicence -> Set AvsPersonId -> DB (FormResult (LicenceTableActionData, Set AvsPersonId), Widget)
|
||||||
mkLicenceTable apidStatus rsChanged dbtIdent aLic apids = do
|
mkLicenceTable apidStatus dbtIdent aLic apids = do
|
||||||
(currentRoute, usrHasAvsRerr) <- liftHandler $ (,)
|
currentRoute <- fromMaybe (error "mkLicenceTable called from 404-handler") <$> liftHandler getCurrentRoute
|
||||||
<$> (fromMaybe (error "mkLicenceTable called from 404-handler") <$> liftHandler getCurrentRoute)
|
avsQualifications <- selectList [QualificationAvsLicence !=. Nothing] []
|
||||||
<*> (messageTooltip <$> messageI Error MsgProblemAvsUsrHadR)
|
|
||||||
avsQualifications <- selectList [QualificationAvsLicence !=. Nothing] [Asc QualificationName]
|
|
||||||
now <- liftIO getCurrentTime
|
now <- liftIO getCurrentTime
|
||||||
|
|
||||||
let nowaday = utctDay now
|
let nowaday = utctDay now
|
||||||
avsQids = entityKey <$> avsQualifications
|
avsQids = entityKey <$> avsQualifications
|
||||||
qualOpts = pure $ qualificationsOptionList avsQualifications
|
|
||||||
-- fltrLic qual = if
|
-- fltrLic qual = if
|
||||||
-- | aLic == AvsNoLicence -> E.isNothing (qual E.?. QualificationId) E.||. E.isJust (E.joinV $ qual E.?. QualificationAvsLicence) -- could be R, F, both or none at all, but has licence in AVS
|
-- | aLic == AvsNoLicence -> E.isNothing (qual E.?. QualificationId) E.||. E.isJust (E.joinV $ qual E.?. QualificationAvsLicence) -- could be R, F, both or none at all, but has licence in AVS
|
||||||
-- | otherwise -> E.isNothing (qual E.?. QualificationId) E.||. (E.val aLic E.=?. E.joinV (qual E.?. QualificationAvsLicence)) -- if we suggest granting that licence, this join should deliver a value too
|
-- | otherwise -> E.isNothing (qual E.?. QualificationId) E.||. (E.val aLic E.=?. E.joinV (qual E.?. QualificationAvsLicence)) -- if we suggest granting that licence, this join should deliver a value too
|
||||||
@ -659,28 +551,17 @@ mkLicenceTable apidStatus rsChanged dbtIdent aLic apids = do
|
|||||||
, sortable (Just "avspersonno") (i18nCell MsgAvsPersonNo) $ \(view resultUserAvs -> a) -> avsPersonNoLinkedCellAdmin a
|
, sortable (Just "avspersonno") (i18nCell MsgAvsPersonNo) $ \(view resultUserAvs -> a) -> avsPersonNoLinkedCellAdmin a
|
||||||
-- , colUserCompany
|
-- , colUserCompany
|
||||||
, sortable (Just "user-company") (i18nCell MsgTableCompanies) $ \(view (resultUser . _entityKey) -> uid) -> flip (set' cellContents) mempty $ do -- why does sqlCell not work here? Mismatch "YesodDB UniWorX" and "RWST (Maybe (Env,FileEnv), UniWorX, [Lang]) Enctype Ints (HandlerFor UniWorX"
|
, sortable (Just "user-company") (i18nCell MsgTableCompanies) $ \(view (resultUser . _entityKey) -> uid) -> flip (set' cellContents) mempty $ do -- why does sqlCell not work here? Mismatch "YesodDB UniWorX" and "RWST (Maybe (Env,FileEnv), UniWorX, [Lang]) Enctype Ints (HandlerFor UniWorX"
|
||||||
companies' <- liftHandler . runDBRead . E.select $ E.from $ \(usrComp `E.InnerJoin` comp) -> do
|
companies' <- liftHandler . runDB . E.select $ E.from $ \(usrComp `E.InnerJoin` comp) -> do
|
||||||
E.on $ usrComp E.^. UserCompanyCompany E.==. comp E.^. CompanyId
|
E.on $ usrComp E.^. UserCompanyCompany E.==. comp E.^. CompanyId
|
||||||
E.where_ $ usrComp E.^. UserCompanyUser E.==. E.val uid
|
E.where_ $ usrComp E.^. UserCompanyUser E.==. E.val uid
|
||||||
E.orderBy [E.asc (comp E.^. CompanyName)]
|
E.orderBy [E.asc (comp E.^. CompanyName)]
|
||||||
return (comp E.^. CompanyShorthand, comp E.^. CompanyName, usrComp E.^. UserCompanySupervisor)
|
return (comp E.^. CompanyShorthand, comp E.^. CompanyName, usrComp E.^. UserCompanySupervisor)
|
||||||
let icnSuper = toWidget $ text2markup " " <> icon IconSupervisor
|
let icnSuper = toWidget $ text2markup " " <> icon IconSupervisor
|
||||||
companies =
|
companies =
|
||||||
(\(E.Value cmpSh, E.Value cmpName, E.Value cmpSpr) -> simpleLink (citext2widget cmpName) (FirmUsersR cmpSh) <> bool mempty icnSuper cmpSpr) <$> companies'
|
(\(E.Value cmpSh, E.Value cmpName, E.Value cmpSpr) -> simpleLink (citext2widget cmpName) (FirmUsersR cmpSh) <> bool mempty icnSuper cmpSpr) <$> companies'
|
||||||
|
|
||||||
pure $ intercalate (text2widget "; ") companies
|
pure $ intercalate (text2widget "; ") companies
|
||||||
, sortable (Just "qualification") (i18nCell MsgTableQualifications) $
|
, sortable (Just "qualification") (i18nCell MsgTableQualifications) $ \(preview resultQualification -> q) -> cellMaybe lmsShortCell q
|
||||||
if aLic /= AvsLicenceVorfeld
|
|
||||||
then
|
|
||||||
\(preview resultQualification -> q) -> cellMaybe lmsShortCell q
|
|
||||||
else
|
|
||||||
\row ->
|
|
||||||
let q = row ^? resultQualification
|
|
||||||
apid = row ^. resultUserAvs . _userAvsPersonId
|
|
||||||
warnCell c = if Set.member apid rsChanged
|
|
||||||
then c <> spacerCell <> wgtCell usrHasAvsRerr -- expected to be effectively dead code in practice, but we never know
|
|
||||||
else c
|
|
||||||
in warnCell $ cellMaybe lmsShortCell q
|
|
||||||
, sortable (Just "first-held") (i18nCell MsgTableQualificationFirstHeld) $ \(preview $ resultQualUser . _entityVal . _qualificationUserFirstHeld -> d) -> cellMaybe dayCell d
|
, sortable (Just "first-held") (i18nCell MsgTableQualificationFirstHeld) $ \(preview $ resultQualUser . _entityVal . _qualificationUserFirstHeld -> d) -> cellMaybe dayCell d
|
||||||
, sortable (Just "last-refresh") (i18nCell MsgTableQualificationLastRefresh)$ \(preview $ resultQualUser . _entityVal . _qualificationUserLastRefresh -> d) -> cellMaybe dayCell d
|
, sortable (Just "last-refresh") (i18nCell MsgTableQualificationLastRefresh)$ \(preview $ resultQualUser . _entityVal . _qualificationUserLastRefresh -> d) -> cellMaybe dayCell d
|
||||||
-- , sortable (Just "valid-until") (i18nCell MsgLmsQualificationValidUntil) $ \(preview $ resultQualUser . _entityVal . _qualificationUserValidUntil -> d) -> cellMaybe dayCell d
|
-- , sortable (Just "valid-until") (i18nCell MsgLmsQualificationValidUntil) $ \(preview $ resultQualUser . _entityVal . _qualificationUserValidUntil -> d) -> cellMaybe dayCell d
|
||||||
@ -724,6 +605,14 @@ mkLicenceTable apidStatus rsChanged dbtIdent aLic apids = do
|
|||||||
, prismAForm (singletonFilter "user-company") mPrev $ aopt textField (fslI MsgTableCompany)
|
, prismAForm (singletonFilter "user-company") mPrev $ aopt textField (fslI MsgTableCompany)
|
||||||
]
|
]
|
||||||
|
|
||||||
|
qualOpt :: Entity Qualification -> Handler (Option QualificationId)
|
||||||
|
qualOpt (Entity qualId qual) = do
|
||||||
|
cQualId :: CryptoUUIDQualification <- encrypt qualId
|
||||||
|
return $ Option
|
||||||
|
{ optionDisplay = CI.original $ qualificationName qual
|
||||||
|
, optionInternalValue = qualId
|
||||||
|
, optionExternalValue = tshow cQualId
|
||||||
|
}
|
||||||
aLicQid = fmap entityKey . headMay $ filter ((== Just aLic) . qualificationAvsLicence . entityVal) avsQualifications
|
aLicQid = fmap entityKey . headMay $ filter ((== Just aLic) . qualificationAvsLicence . entityVal) avsQualifications
|
||||||
|
|
||||||
-- Block identical to Handler/Qualifications TODO: refactor
|
-- Block identical to Handler/Qualifications TODO: refactor
|
||||||
@ -741,20 +630,20 @@ mkLicenceTable apidStatus rsChanged dbtIdent aLic apids = do
|
|||||||
mkOption :: E.Value Text -> Option Text
|
mkOption :: E.Value Text -> Option Text
|
||||||
mkOption (E.unValue -> t) = Option{ optionDisplay = t, optionInternalValue = t, optionExternalValue = toPathPiece t }
|
mkOption (E.unValue -> t) = Option{ optionDisplay = t, optionInternalValue = t, optionExternalValue = toPathPiece t }
|
||||||
suggestionsBlock :: HandlerFor UniWorX (OptionList Text)
|
suggestionsBlock :: HandlerFor UniWorX (OptionList Text)
|
||||||
suggestionsBlock = mkOptionList . fmap mkOption <$> runDBRead (getBlockReasons E.not__)
|
suggestionsBlock = mkOptionList . fmap mkOption <$> runDB (getBlockReasons E.not_)
|
||||||
suggestionsUnblock = mkOptionList . fmap mkOption <$> runDBRead (getBlockReasons id)
|
suggestionsUnblock = mkOptionList . fmap mkOption <$> runDB (getBlockReasons id)
|
||||||
|
|
||||||
acts :: Map LicenceTableAction (AForm Handler LicenceTableActionData)
|
acts :: Map LicenceTableAction (AForm Handler LicenceTableActionData)
|
||||||
acts = mconcat
|
acts = mconcat
|
||||||
[ singletonMap LicenceTableChangeAvs $ pure LicenceTableChangeAvsData
|
[ singletonMap LicenceTableChangeAvs $ pure LicenceTableChangeAvsData
|
||||||
, if aLic == AvsNoLicence
|
, if aLic == AvsNoLicence
|
||||||
then singletonMap LicenceTableRevokeFDrive $ LicenceTableRevokeFDriveData
|
then singletonMap LicenceTableRevokeFDrive $ LicenceTableRevokeFDriveData
|
||||||
<$> apreq (selectField qualOpts) (fslI MsgQualificationName) aLicQid
|
<$> apreq (selectField . fmap mkOptionList $ mapM qualOpt avsQualifications) (fslI MsgQualificationName) aLicQid
|
||||||
<*> apreq (textField & cfStrip & addDatalist suggestionsBlock) (fslI MsgQualificationBlockReason) Nothing
|
<*> apreq (textField & cfStrip & addDatalist suggestionsBlock) (fslI MsgQualificationBlockReason) Nothing
|
||||||
<*> areq (boolField . Just $ SomeMessage MsgBoolIrrelevant) (fslI MsgQualificationBlockNotify) (Just False)
|
<*> areq (boolField . Just $ SomeMessage MsgBoolIrrelevant) (fslI MsgQualificationBlockNotify) (Just False)
|
||||||
|
|
||||||
else singletonMap LicenceTableGrantFDrive $ LicenceTableGrantFDriveData
|
else singletonMap LicenceTableGrantFDrive $ LicenceTableGrantFDriveData
|
||||||
<$> apreq (selectField qualOpts) (fslI MsgQualificationName) aLicQid
|
<$> apreq (selectField . fmap mkOptionList $ mapM qualOpt avsQualifications) (fslI MsgQualificationName) aLicQid
|
||||||
<*> apreq (textField & cfStrip & addDatalist suggestionsUnblock) (fslI MsgQualificationGrantReason) Nothing
|
<*> apreq (textField & cfStrip & addDatalist suggestionsUnblock) (fslI MsgQualificationGrantReason) Nothing
|
||||||
<*> apreq dayField (fslI MsgLmsQualificationValidUntil) Nothing -- apreq?!
|
<*> apreq dayField (fslI MsgLmsQualificationValidUntil) Nothing -- apreq?!
|
||||||
<*> aopt (convertField not not (boolField . Just $ SomeMessage MsgBoolIrrelevant)) (fslI MsgQualificationUserNoRenewal) Nothing
|
<*> aopt (convertField not not (boolField . Just $ SomeMessage MsgBoolIrrelevant)) (fslI MsgQualificationUserNoRenewal) Nothing
|
||||||
@ -788,204 +677,52 @@ mkLicenceTable apidStatus rsChanged dbtIdent aLic apids = do
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
data UserAvsAction = UserAvsSwitchCompany
|
getAdminAvsUserR :: CryptoUUIDUser -> Handler Html
|
||||||
deriving (Eq, Ord, Enum, Bounded, Read, Show, Generic)
|
getAdminAvsUserR uuid = do
|
||||||
deriving anyclass (Universe, Finite)
|
uid <- decrypt uuid
|
||||||
|
Entity{entityVal=UserAvs{..}} <- runDB $ getBy404 $ UniqueUserAvsUser uid
|
||||||
nullaryPathPiece ''UserAvsAction $ camelToPathPiece' 1
|
mAvsQuery <- getsYesod $ view _appAvsQuery
|
||||||
embedRenderMessage ''UniWorX ''UserAvsAction id
|
resWgt <- case mAvsQuery of
|
||||||
instance Button UniWorX UserAvsAction where
|
Nothing -> return [whamlet|Error: AVS interface configuration is incomplete.|] -- should never occur after initilisation
|
||||||
btnClasses UserAvsSwitchCompany = [BCIsButton, BCDefault]
|
Just AvsQuery{..} -> do
|
||||||
|
mbContact <- avsQueryContact $ AvsQueryContact $ Set.singleton $ AvsObjPersonId userAvsPersonId
|
||||||
|
mbDataPerson <- lookupAvsUser userAvsPersonId
|
||||||
getAdminAvsUserR, postAdminAvsUserR :: CryptoUUIDUser -> Handler Html
|
return [whamlet|
|
||||||
getAdminAvsUserR = postAdminAvsUserR
|
|
||||||
postAdminAvsUserR uuid = do
|
|
||||||
isModal <- hasCustomHeader HeaderIsModal
|
|
||||||
|
|
||||||
uid <- decrypt uuid
|
|
||||||
Entity{entityVal=UserAvs{..}} <- runDB $ getBy404 $ UniqueUserAvsUser uid
|
|
||||||
-- let fltrById prj = over _Wrapped (Set.filter ((== userAvsPersonId) . prj)) -- not sufficiently polymorphic
|
|
||||||
let fltrIdContact = over _Wrapped (Set.filter ((== userAvsPersonId) . avsContactPersonID))
|
|
||||||
-- fltrIdStatus = over _Wrapped (Set.filter ((== userAvsPersonId) . avsStatusPersonID))
|
|
||||||
mbContact <- try $ fmap fltrIdContact $ avsQuery $ AvsQueryContact $ Set.singleton $ AvsObjPersonId userAvsPersonId
|
|
||||||
-- mbStatus <- try $ fmap fltrIdStatus $ avsQuery $ AvsQueryStatus $ Set.singleton userAvsPersonId
|
|
||||||
mbStatus <- try $ queryAvsFullStatus userAvsPersonId -- TODO: delete Handler.Utils.Avs.lookupAvsUser if no longer needed -- NOTE: currently needed to provide card firms that are missing in AVS status query responses
|
|
||||||
let compsUsed :: [CompanyName] = stripCI <$> mbStatus ^.. _Right . _Wrapped . folded . _avsStatusPersonCardStatus . folded . _avsDataFirm . _Just
|
|
||||||
compDict <- if 1 >= length compsUsed
|
|
||||||
then return mempty -- switch company only sensible if there is more than one company to choose
|
|
||||||
else do
|
|
||||||
let switchCompFormHandler :: [(CompanyName,CompanyId)] -> Maybe CompanyId -> Handler Widget
|
|
||||||
switchCompFormHandler availComps _ | 1 >= length availComps = return mempty -- don't offer a form if there is only one company
|
|
||||||
switchCompFormHandler availComps mbPrime = do
|
|
||||||
let switchCompForm :: AForm (HandlerFor UniWorX) (CryptoUUIDUser,CompanyId)
|
|
||||||
switchCompForm = (,)
|
|
||||||
<$> apopt hiddenField "" (Just uuid)
|
|
||||||
<*> areq (selectFieldList [(ciOriginal cn, cid) | (cn, cid) <- availComps]) (fslI MsgUserAvsSwitchCompanyField) mbPrime
|
|
||||||
<* aopt (buttonField UserAvsSwitchCompany) "" Nothing
|
|
||||||
switchCompValidate :: FormValidator (CryptoUUIDUser,CompanyId) Handler ()
|
|
||||||
switchCompValidate = do
|
|
||||||
(uuid_rcvd,_) <- State.get
|
|
||||||
guardValidation MsgWrongButtonValue $ uuid_rcvd == uuid
|
|
||||||
((spRes, spWgt), spEnc) <- runFormPost . validateForm switchCompValidate . identifyForm ("switch-primary-company"::Text) $ renderAForm FormStandard switchCompForm
|
|
||||||
formResultModal spRes (AdminAvsUserR uuid) (\(_,cid) -> do
|
|
||||||
lift $ $logInfoS "AVS" ("Switch company option result " <> tshow spRes)
|
|
||||||
problems <- liftHandler . runDB $ do
|
|
||||||
(usrUp, problems) <- switchAvsUserCompany True False uid cid
|
|
||||||
update uid usrUp
|
|
||||||
forM problems $ \p -> reportAdminProblem p >> msgAdminProblem p
|
|
||||||
forM_ problems (\p -> do
|
|
||||||
-- lift $ $logErrorS "AVS" $ "Switch company problem: " <> tshow p -- no instance Show for SomeMessages
|
|
||||||
tell . pure =<< messageI Warning p
|
|
||||||
)
|
|
||||||
let ok = if null problems then Success else Error
|
|
||||||
tell . pure =<< messageI ok (MsgUserAvsCompanySwitched $ unCompanyKey cid)
|
|
||||||
)
|
|
||||||
return $ wrapForm spWgt
|
|
||||||
def { formAction = Just $ SomeRoute (AdminAvsUserR uuid), formEncoding = spEnc, formSubmit = FormNoSubmit, formAttrs = [ asyncSubmitAttr | isModal ]}
|
|
||||||
(availComps, primName, primId) <- runDB $ do
|
|
||||||
mbPrimeUsrComp :: Maybe UserCompany <- getUserPrimaryCompany uid
|
|
||||||
mbPrimeComp :: Maybe Company <- traverseJoin (get . userCompanyCompany) mbPrimeUsrComp
|
|
||||||
-- let fltrCmps = (CompanyName <-. compsUsed) : maybeEmpty mbPrimeComp (\Company{companyShorthand=pShort} -> [CompanyShorthand !=. pShort])
|
|
||||||
comps :: [Entity Company] <- selectList [CompanyName <-. compsUsed] [Asc CompanyName, Asc CompanyAvsId] -- company name is already unique, but AVS sometimes contains uses whitespace
|
|
||||||
return ([(companyName v, k) | (Entity k v) <- comps], companyName <$> mbPrimeComp, CompanyKey . companyShorthand <$> mbPrimeComp)
|
|
||||||
-- formDict <- Map.traverseWithKey runSwitchFrom compDict
|
|
||||||
swForm <- switchCompFormHandler availComps primId
|
|
||||||
return (primName, swForm)
|
|
||||||
|
|
||||||
msgWarningTooltip <- messageI Warning MsgMessageWarning
|
|
||||||
let warnBolt = messageTooltip msgWarningTooltip
|
|
||||||
heading = [whamlet|_{MsgAvsPersonNo} #{userAvsNoPerson}|]
|
|
||||||
siteLayout heading $ do
|
|
||||||
setTitle $ toHtml $ show userAvsNoPerson
|
|
||||||
let contactWgt = case mbContact of
|
|
||||||
Left err -> exceptionWgt err
|
|
||||||
Right (AvsResponseContact adcs) ->
|
|
||||||
if null adcs
|
|
||||||
then [whamlet|_{MsgAvsPersonSearchEmpty}|]
|
|
||||||
else
|
|
||||||
let cs = mkContactWgt warnBolt userAvsNoPerson <$> toList adcs
|
|
||||||
in mconcat cs
|
|
||||||
cardsWgt = case mbStatus of
|
|
||||||
Left err -> exceptionWgt err
|
|
||||||
Right (AvsResponseStatus asts) ->
|
|
||||||
if null asts
|
|
||||||
then [whamlet|_{MsgAvsStatusSearchEmpty}|]
|
|
||||||
else
|
|
||||||
let cs = mkCardsWgt compDict . avsStatusPersonCardStatus <$> toList asts
|
|
||||||
in mconcat cs
|
|
||||||
[whamlet|
|
|
||||||
<p>
|
<p>
|
||||||
^{contactWgt}
|
Vorläufige Admin Ansicht AVS Daten.
|
||||||
|
Ansicht zeigt aktuelle Daten.
|
||||||
|
Es erfolgte damit aber noch kein Update der FRADrive Daten.
|
||||||
<p>
|
<p>
|
||||||
^{cardsWgt}
|
<dl .deflist>
|
||||||
<p>
|
<dt .deflist__dt>InfoPersonContact <br>
|
||||||
_{MsgAvsCurrentData}
|
<i>(bevorzugt)
|
||||||
|]
|
|
||||||
where
|
|
||||||
mkContactWgt :: Widget -> Int -> AvsDataContact -> Widget
|
|
||||||
mkContactWgt warnBolt reqAvsNo AvsDataContact
|
|
||||||
{ -- avsContactPersonID = _api
|
|
||||||
avsContactPersonInfo = AvsPersonInfo{..}
|
|
||||||
, avsContactFirmInfo = AvsFirmInfo{ avsFirmFirm = firmName }
|
|
||||||
} =
|
|
||||||
let avsNoOk = readMay avsInfoPersonNo /= Just reqAvsNo in
|
|
||||||
[whamlet|
|
|
||||||
<section .profile>
|
|
||||||
<dl .deflist.profile-dl>
|
|
||||||
$if avsNoOk
|
|
||||||
<dt .deflist__dt>
|
|
||||||
_{MsgAvsPersonNo}
|
|
||||||
<dd .deflist__dd>
|
|
||||||
#{avsInfoPersonNo}
|
|
||||||
^{warnBolt}
|
|
||||||
_{MsgAvsPersonNoMismatch}
|
|
||||||
<dt .deflist__dt>
|
|
||||||
_{MsgAvsLastName}
|
|
||||||
<dd .deflist__dd>
|
<dd .deflist__dd>
|
||||||
#{avsInfoLastName}
|
$case mbContact
|
||||||
<dt .deflist__dt>
|
$of Left err
|
||||||
_{MsgAvsFirstName}
|
Fehler: #{tshow err}
|
||||||
|
$of Right contactInfo
|
||||||
|
#{decodeUtf8 (Pretty.encodePretty (toJSON contactInfo))}
|
||||||
|
<dt .deflist__dt>PersonStatus und mehrere PersonSearch <br>
|
||||||
|
<i>(benötigt mehrere AVS Abfragen)
|
||||||
<dd .deflist__dd>
|
<dd .deflist__dd>
|
||||||
#{avsInfoFirstName}
|
$maybe dataPerson <- mbDataPerson
|
||||||
<dt .deflist__dt>
|
#{decodeUtf8 (Pretty.encodePretty (toJSON dataPerson))}
|
||||||
_{MsgAvsPrimaryCompany}
|
|
||||||
<dd .deflist__dd>
|
|
||||||
#{firmName}
|
|
||||||
$maybe bday <- avsInfoDateOfBirth
|
|
||||||
<dt .deflist__dt>
|
|
||||||
_{MsgAdminUserBirthday}
|
|
||||||
<dd .deflist__dd>
|
|
||||||
^{formatTimeW SelFormatDate bday}
|
|
||||||
<dt .deflist__dt>
|
|
||||||
_{MsgAvsLicence}
|
|
||||||
<dd .deflist__dd>
|
|
||||||
$maybe licence <- parseAvsLicence avsInfoRampLicence
|
|
||||||
_{licence}
|
|
||||||
$nothing
|
$nothing
|
||||||
_{MsgAvsNoLicenceGuest}
|
Keine Daten erhalten.
|
||||||
|]
|
<h3>
|
||||||
|
Provisorische formatierte Ansicht
|
||||||
-- mkCardsWgt :: (Maybe CompanyName, Map CompanyName Widget, Widget) -> Set AvsDataPersonCard -> Widget
|
<p>
|
||||||
mkCardsWgt :: (Maybe CompanyName, Widget) -> Set AvsDataPersonCard -> Widget
|
Generisch formatierte Ansicht, die zeigt, in welche Richtung die Endansicht gehen könnte.
|
||||||
mkCardsWgt (mbPrimName, swForm) crds
|
In der Endansicht wären nur ausgewählte Felder mit besserer Bennenung in einer manuell gewählten Reihenfolge sichtbar.
|
||||||
| null crds = [whamlet|_{MsgAvsCardsEmpty}|]
|
<p>
|
||||||
| otherwise = do
|
^{foldMap jsonWidget mbContact}
|
||||||
let hasCompany = isJust $ Set.foldr ((<|>) . avsDataFirm) Nothing crds -- some if, since a true AVS status query never delivers values for these fields, but queryAvsFullStatus-workaround does
|
<p>
|
||||||
hasIssueDate = isJust $ Set.foldr ((<|>) . avsDataIssueDate) Nothing crds
|
^{foldMap jsonWidget mbDataPerson}
|
||||||
hasValidToDate = isJust $ Set.foldr ((<|>) . avsDataValidTo) Nothing crds
|
|
||||||
[whamlet|
|
|
||||||
<div .scrolltable .scrolltable-bordered>
|
|
||||||
<table .table .table--striped>
|
|
||||||
<thead>
|
|
||||||
<tr .table__row .table__row--head>
|
|
||||||
<th .table__th>_{MsgAvsCardNo}
|
|
||||||
<th .table__th>_{MsgTableAvsCardValid}
|
|
||||||
<th .table__th>_{MsgAvsCardColor}
|
|
||||||
<th .table__th>_{MsgAvsCardAreas}
|
|
||||||
$if hasIssueDate
|
|
||||||
<th .table__th>_{MsgTableAvsCardIssueDate}
|
|
||||||
$if hasValidToDate
|
|
||||||
<th .table__th>_{MsgTableAvsCardValidTo}
|
|
||||||
$if hasCompany
|
|
||||||
<th .table__th>_{MsgTableCompany}
|
|
||||||
<th .table__th>_{MsgAvsPrimaryCompany}
|
|
||||||
<tbody>
|
|
||||||
$forall c <- Set.toDescList crds
|
|
||||||
$with AvsDataPersonCard{avsDataValid,avsDataCardColor,avsDataCardAreas,avsDataFirm,avsDataIssueDate,avsDataValidTo} <- c
|
|
||||||
<tr .table__row>
|
|
||||||
<td .table__td>
|
|
||||||
#{tshowAvsFullCardNo (getFullCardNo c)}
|
|
||||||
<td .table__td>
|
|
||||||
#{boolSymbol avsDataValid}
|
|
||||||
<td .table__td>
|
|
||||||
_{avsDataCardColor}
|
|
||||||
<td .table__td>
|
|
||||||
$forall a <- avsDataCardAreas
|
|
||||||
#{a} #
|
|
||||||
$if hasIssueDate
|
|
||||||
<td .table__td>
|
|
||||||
$maybe d <- avsDataIssueDate
|
|
||||||
^{formatTimeW SelFormatDate d}
|
|
||||||
$if hasValidToDate
|
|
||||||
<td .table__td>
|
|
||||||
$maybe d <- avsDataValidTo
|
|
||||||
^{formatTimeW SelFormatDate d}
|
|
||||||
$if hasCompany
|
|
||||||
<td .table__td>
|
|
||||||
$maybe f <- avsDataFirm
|
|
||||||
#{f}
|
|
||||||
<td .table__td>
|
|
||||||
$maybe f <- avsDataFirm
|
|
||||||
$with fci <- stripCI f
|
|
||||||
$maybe primName <- mbPrimName
|
|
||||||
$if (primName == fci)
|
|
||||||
_{MsgAvsPrimaryCompany}
|
|
||||||
<p>
|
|
||||||
^{swForm}
|
|
||||||
|]
|
|]
|
||||||
|
let heading = [whamlet|_{MsgAvsPersonNo} #{userAvsNoPerson}|]
|
||||||
|
siteLayout heading $ do
|
||||||
|
setTitle $ toHtml $ show userAvsNoPerson
|
||||||
|
resWgt
|
||||||
|
|
||||||
instance HasEntity (DBRow (Entity UserAvs, Entity User)) User where
|
instance HasEntity (DBRow (Entity UserAvs, Entity User)) User where
|
||||||
hasEntity = _dbrOutput . _2
|
hasEntity = _dbrOutput . _2
|
||||||
@ -1003,9 +740,9 @@ getProblemAvsErrorR = do
|
|||||||
dbtSQLQuery (usravs `E.InnerJoin` user) = do
|
dbtSQLQuery (usravs `E.InnerJoin` user) = do
|
||||||
E.on $ usravs E.^. UserAvsUser E.==. user E.^. UserId
|
E.on $ usravs E.^. UserAvsUser E.==. user E.^. UserId
|
||||||
E.where_ $ E.isJust $ usravs E.^. UserAvsLastSynchError
|
E.where_ $ E.isJust $ usravs E.^. UserAvsLastSynchError
|
||||||
return (usravs, user) -- , E.substring (usravs E.^. UserAvsLastSynchError) (E.val ("'#\"%#\" %'") (E.val "#")) -- needs a different type on substring
|
return (usravs, user)
|
||||||
qerryUsrAvs :: (E.SqlExpr (Entity UserAvs) `E.InnerJoin` E.SqlExpr (Entity User)) -> E.SqlExpr (Entity UserAvs)
|
qerryUsrAvs :: (E.SqlExpr (Entity UserAvs) `E.InnerJoin` E.SqlExpr (Entity User)) -> E.SqlExpr (Entity UserAvs)
|
||||||
qerryUsrAvs = $(E.sqlIJproj 2 1)
|
qerryUsrAvs = $(E.sqlIJproj 2 1)
|
||||||
qerryUser :: (E.SqlExpr (Entity UserAvs) `E.InnerJoin` E.SqlExpr (Entity User)) -> E.SqlExpr (Entity User)
|
qerryUser :: (E.SqlExpr (Entity UserAvs) `E.InnerJoin` E.SqlExpr (Entity User)) -> E.SqlExpr (Entity User)
|
||||||
qerryUser = $(E.sqlIJproj 2 2)
|
qerryUser = $(E.sqlIJproj 2 2)
|
||||||
reserrUsrAvs :: Lens' (DBRow (Entity UserAvs, Entity User)) (Entity UserAvs)
|
reserrUsrAvs :: Lens' (DBRow (Entity UserAvs, Entity User)) (Entity UserAvs)
|
||||||
@ -1051,3 +788,4 @@ getProblemAvsErrorR = do
|
|||||||
siteLayoutMsg MsgMenuAvsSynchError $ do
|
siteLayoutMsg MsgMenuAvsSynchError $ do
|
||||||
setTitleI MsgMenuAvsSynchError
|
setTitleI MsgMenuAvsSynchError
|
||||||
[whamlet|^{avsSyncErrTbl}|]
|
[whamlet|^{avsSyncErrTbl}|]
|
||||||
|
|
||||||
@ -35,9 +35,6 @@ import qualified Database.Esqueleto.Legacy as E
|
|||||||
import qualified Database.Esqueleto.Utils as E
|
import qualified Database.Esqueleto.Utils as E
|
||||||
-- import Database.Esqueleto.Utils.TH
|
-- import Database.Esqueleto.Utils.TH
|
||||||
|
|
||||||
-- Number of minutes a job must have been locked already to allow forced deletion
|
|
||||||
jobDeleteLockMinutes :: Int
|
|
||||||
jobDeleteLockMinutes = 3
|
|
||||||
|
|
||||||
deriveJSON defaultOptions
|
deriveJSON defaultOptions
|
||||||
{ constructorTagModifier = camelToPathPiece' 1
|
{ constructorTagModifier = camelToPathPiece' 1
|
||||||
@ -121,9 +118,7 @@ instance Finite JobTableAction
|
|||||||
nullaryPathPiece ''JobTableAction $ camelToPathPiece' 1
|
nullaryPathPiece ''JobTableAction $ camelToPathPiece' 1
|
||||||
embedRenderMessage ''UniWorX ''JobTableAction id
|
embedRenderMessage ''UniWorX ''JobTableAction id
|
||||||
|
|
||||||
newtype JobTableActionData = ActJobDeleteData
|
data JobTableActionData = ActJobDeleteData
|
||||||
{ jobDeleteLocked :: Bool
|
|
||||||
}
|
|
||||||
deriving (Eq, Ord, Read, Show, Generic)
|
deriving (Eq, Ord, Read, Show, Generic)
|
||||||
|
|
||||||
|
|
||||||
@ -169,8 +164,7 @@ postAdminJobsR = do
|
|||||||
]
|
]
|
||||||
dbtStyle = def { dbsFilterLayout = defaultDBSFilterLayout }
|
dbtStyle = def { dbsFilterLayout = defaultDBSFilterLayout }
|
||||||
acts :: Map JobTableAction (AForm Handler JobTableActionData)
|
acts :: Map JobTableAction (AForm Handler JobTableActionData)
|
||||||
acts = Map.singleton ActJobDelete $ ActJobDeleteData
|
acts = Map.singleton ActJobDelete $ pure ActJobDeleteData
|
||||||
<$> areq checkBoxField (fslI $ MsgActJobDeleteForce jobDeleteLockMinutes) Nothing
|
|
||||||
dbtParams = DBParamsForm
|
dbtParams = DBParamsForm
|
||||||
{ dbParamsFormAdditional =
|
{ dbParamsFormAdditional =
|
||||||
renderAForm FormStandard
|
renderAForm FormStandard
|
||||||
@ -199,22 +193,13 @@ postAdminJobsR = do
|
|||||||
(jobActRes, jobsTable) <- runDB (over _1 postprocess <$> dbTable jobsDBTableValidator jobsDBTable)
|
(jobActRes, jobsTable) <- runDB (over _1 postprocess <$> dbTable jobsDBTableValidator jobsDBTable)
|
||||||
|
|
||||||
formResult jobActRes $ \case
|
formResult jobActRes $ \case
|
||||||
(ActJobDeleteData{jobDeleteLocked}, jobIds) -> do
|
(ActJobDeleteData, jobIds) -> do
|
||||||
now <- liftIO getCurrentTime
|
let jobReq = length jobIds
|
||||||
let cutoff :: UTCTime
|
|
||||||
cutoff = addUTCTime (nominalMinute * fromIntegral (negate jobDeleteLockMinutes)) now
|
|
||||||
jobReq = length jobIds
|
|
||||||
lockCriteria
|
|
||||||
| jobDeleteLocked =
|
|
||||||
[ QueuedJobLockTime ==. Nothing ] ||.
|
|
||||||
[ QueuedJobLockTime <=. Just cutoff ]
|
|
||||||
| otherwise =
|
|
||||||
[ QueuedJobLockTime ==. Nothing
|
|
||||||
, QueuedJobLockInstance ==. Nothing
|
|
||||||
]
|
|
||||||
rmvd <- runDB $ fromIntegral <$> deleteWhereCount
|
rmvd <- runDB $ fromIntegral <$> deleteWhereCount
|
||||||
((QueuedJobId <-. Set.toList jobIds) : lockCriteria)
|
[ QueuedJobLockTime ==. Nothing
|
||||||
|
, QueuedJobLockInstance ==. Nothing
|
||||||
|
, QueuedJobId <-. Set.toList jobIds
|
||||||
|
]
|
||||||
addMessageI (bool Success Warning $ rmvd < jobReq) (MsgTableJobActDeleteFeedback rmvd jobReq)
|
addMessageI (bool Success Warning $ rmvd < jobReq) (MsgTableJobActDeleteFeedback rmvd jobReq)
|
||||||
reloadKeepGetParams AdminJobsR
|
reloadKeepGetParams AdminJobsR
|
||||||
|
|
||||||
|
|||||||
74
src/Handler/Admin/ExternalUser.hs
Normal file
74
src/Handler/Admin/ExternalUser.hs
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
-- SPDX-FileCopyrightText: 2022-2024 Sarah Vaupel <sarah.vaupel@uniworx.de>, David Mosbach <david.mosbach@uniworx.de>, Steffen Jost <jost@tcs.ifi.lmu.de>
|
||||||
|
--
|
||||||
|
-- SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
|
module Handler.Admin.ExternalUser
|
||||||
|
( getAdminExternalUserR
|
||||||
|
, postAdminExternalUserR
|
||||||
|
) where
|
||||||
|
|
||||||
|
import Import
|
||||||
|
|
||||||
|
import Foundation.Yesod.Auth (userLookupAndUpsert) -- decodeUserTest
|
||||||
|
import Auth.OAuth2 (queryOAuth2User)
|
||||||
|
import Auth.LDAP
|
||||||
|
|
||||||
|
import Handler.Utils
|
||||||
|
|
||||||
|
import Data.Aeson.Encode.Pretty (encodePretty)
|
||||||
|
import qualified Data.Text.Lazy as Lazy
|
||||||
|
import qualified Data.Text.Lazy.Encoding as Lazy
|
||||||
|
|
||||||
|
|
||||||
|
getAdminExternalUserR, postAdminExternalUserR :: Handler Html
|
||||||
|
getAdminExternalUserR = postAdminExternalUserR
|
||||||
|
postAdminExternalUserR = do
|
||||||
|
((presult, pwidget), penctype) <- runFormPost $ identifyForm ("adminExternalUserLookup"::Text) $ \html ->
|
||||||
|
flip (renderAForm FormStandard) html $ areq textField (fslI MsgAdminUserIdent) Nothing
|
||||||
|
|
||||||
|
let
|
||||||
|
-- presentUtf8 v = Text.intercalate ", " (either tshow id . Text.decodeUtf8' <$> v)
|
||||||
|
-- presentLatin1 v = Text.intercalate ", " ( Text.decodeLatin1 <$> v)
|
||||||
|
|
||||||
|
procFormPerson :: Text -> Handler (Maybe [(AuthSourceIdent,Lazy.Text)]) -- (Maybe [(AuthSourceIdent, [(Text,(Int,Text,Text))])])
|
||||||
|
procFormPerson needle = getsYesod (view _appUserAuthConf) >>= \case
|
||||||
|
UserAuthConfSingleSource{..} -> case userAuthConfSingleSource of
|
||||||
|
AuthSourceConfAzureAdV2 AzureConf{..} -> do
|
||||||
|
-- only singleton results supported right now, i.e. lookups by email, userPrincipalName (aka fraport ident), or id
|
||||||
|
queryOAuth2User @Value needle >>= \case
|
||||||
|
Left _ -> addMessage Error (text2Html "Encountered UserDataException while Azure user query!") >> return Nothing
|
||||||
|
Right azureResponse -> return . Just . singleton . (AuthSourceIdAzure azureConfTenantId,) . Lazy.decodeUtf8 $ encodePretty azureResponse
|
||||||
|
-- Right azureData -> return . Just . singleton . (AuthSourceIdAzure azureConfTenantId,) $ azureData <&> \(k,vs) -> (k, (length vs, presentUtf8 vs, presentLatin1 vs))
|
||||||
|
AuthSourceConfLdap LdapConf{..} -> do
|
||||||
|
getsYesod (view _appLdapPool) >>= \case
|
||||||
|
Nothing -> addMessage Error (text2Html "LDAP Pool configuration missing!") >> return Nothing
|
||||||
|
Just pool -> do
|
||||||
|
ldapData <- ldapSearch pool needle
|
||||||
|
-- decodedErr <- decodeUserTest $ NonEmpty.singleton UpsertUserDataLdap{ upsertUserLdapHost = ldapConfSourceId, upsertUserLdapData = concat ldapData }
|
||||||
|
-- whenIsLeft decodedErr $ addMessageI Error
|
||||||
|
return . Just . singleton . (AuthSourceIdLdap ldapConfSourceId,) . Lazy.decodeUtf8 $ encodePretty ldapData
|
||||||
|
-- return . Just $ ldapData <&> \(Ldap.SearchEntry _dn attrs) -> (AuthSourceIdLdap{..}, (\(k,v) -> (tshow k, (length v, presentUtf8 v, presentLatin1 v))) <$> attrs)
|
||||||
|
|
||||||
|
mbData <- formResultMaybe presult procFormPerson
|
||||||
|
|
||||||
|
|
||||||
|
((uresult, uwidget), uenctype) <- runFormPost $ identifyForm ("adminExternalUserUpsert"::Text) $ \html ->
|
||||||
|
flip (renderAForm FormStandard) html $ areq textField (fslI MsgAdminUserIdent) Nothing
|
||||||
|
let procFormUpsert :: Text -> Handler (Maybe (Entity User))
|
||||||
|
procFormUpsert lid = runDB (userLookupAndUpsert lid UpsertUserGuessUser)
|
||||||
|
|
||||||
|
mbUpsert <- formResultMaybe uresult procFormUpsert
|
||||||
|
|
||||||
|
|
||||||
|
actionUrl <- fromMaybe AdminExternalUserR <$> getCurrentRoute
|
||||||
|
siteLayoutMsg MsgMenuExternalUser $ do
|
||||||
|
setTitleI MsgMenuExternalUser
|
||||||
|
let personForm = wrapForm pwidget def
|
||||||
|
{ formAction = Just $ SomeRoute actionUrl
|
||||||
|
, formEncoding = penctype
|
||||||
|
}
|
||||||
|
upsertForm = wrapForm uwidget def
|
||||||
|
{ formAction = Just $ SomeRoute actionUrl
|
||||||
|
, formEncoding = uenctype
|
||||||
|
}
|
||||||
|
$(widgetFile "admin/external-user")
|
||||||
@ -1,69 +0,0 @@
|
|||||||
-- SPDX-FileCopyrightText: 2022 Steffen Jost <jost@tcs.ifi.lmu.de>
|
|
||||||
--
|
|
||||||
-- SPDX-License-Identifier: AGPL-3.0-or-later
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
module Handler.Admin.Ldap
|
|
||||||
( getAdminLdapR
|
|
||||||
, postAdminLdapR
|
|
||||||
) where
|
|
||||||
|
|
||||||
import Import
|
|
||||||
-- import qualified Control.Monad.State.Class as State
|
|
||||||
-- import Data.Aeson (encode)
|
|
||||||
import qualified Data.CaseInsensitive as CI
|
|
||||||
import qualified Data.Text as Text
|
|
||||||
import qualified Data.Text.Encoding as Text
|
|
||||||
-- import qualified Data.Set as Set
|
|
||||||
import Foundation.Yesod.Auth (decodeUserTest,ldapLookupAndUpsert,campusUserFailoverMode,CampusUserConversionException())
|
|
||||||
import Handler.Utils
|
|
||||||
|
|
||||||
import qualified Ldap.Client as Ldap
|
|
||||||
import Auth.LDAP
|
|
||||||
|
|
||||||
|
|
||||||
getAdminLdapR, postAdminLdapR :: Handler Html
|
|
||||||
getAdminLdapR = postAdminLdapR
|
|
||||||
postAdminLdapR = do
|
|
||||||
((presult, pwidget), penctype) <- runFormPost $ identifyForm ("adminLdapLookup"::Text) $ \html ->
|
|
||||||
flip (renderAForm FormStandard) html $ areq textField (fslI MsgAdminUserIdent) Nothing
|
|
||||||
|
|
||||||
let procFormPerson :: Text -> Handler (Maybe (Ldap.AttrList []))
|
|
||||||
procFormPerson lid = do
|
|
||||||
ldapPool' <- getsYesod $ view _appLdapPool
|
|
||||||
case ldapPool' of
|
|
||||||
Nothing -> addMessage Error (text2Html "LDAP Configuration missing.") >> return Nothing
|
|
||||||
Just ldapPool -> do
|
|
||||||
addMessage Info $ text2Html "Input for LDAP test received."
|
|
||||||
ldapData <- campusUser'' ldapPool campusUserFailoverMode lid
|
|
||||||
decodedErr <- decodeUserTest (pure $ CI.mk lid) $ concat ldapData
|
|
||||||
whenIsLeft decodedErr $ addMessageI Error
|
|
||||||
return ldapData
|
|
||||||
mbLdapData <- formResultMaybe presult procFormPerson
|
|
||||||
|
|
||||||
|
|
||||||
((uresult, uwidget), uenctype) <- runFormPost $ identifyForm ("adminLdapUpsert"::Text) $ \html ->
|
|
||||||
flip (renderAForm FormStandard) html $ areq textField (fslI MsgAdminUserIdent) Nothing
|
|
||||||
let procFormUpsert :: Text -> Handler (Maybe (Either CampusUserConversionException (Entity User)))
|
|
||||||
procFormUpsert lid = pure <$> runDB (try $ ldapLookupAndUpsert lid)
|
|
||||||
mbLdapUpsert <- formResultMaybe uresult procFormUpsert
|
|
||||||
|
|
||||||
|
|
||||||
actionUrl <- fromMaybe AdminLdapR <$> getCurrentRoute
|
|
||||||
siteLayoutMsg MsgMenuLdap $ do
|
|
||||||
setTitleI MsgMenuLdap
|
|
||||||
let personForm = wrapForm pwidget def
|
|
||||||
{ formAction = Just $ SomeRoute actionUrl
|
|
||||||
, formEncoding = penctype
|
|
||||||
}
|
|
||||||
upsertForm = wrapForm uwidget def
|
|
||||||
{ formAction = Just $ SomeRoute actionUrl
|
|
||||||
, formEncoding = uenctype
|
|
||||||
}
|
|
||||||
presentUtf8 lv = Text.intercalate ", " (either tshow id . Text.decodeUtf8' <$> lv)
|
|
||||||
presentLatin1 lv = Text.intercalate ", " ( Text.decodeLatin1 <$> lv)
|
|
||||||
|
|
||||||
-- TODO: use i18nWidgetFile instead if this is to become permanent
|
|
||||||
$(widgetFile "ldap")
|
|
||||||
|
|
||||||
@ -1,4 +1,4 @@
|
|||||||
-- SPDX-FileCopyrightText: 2022 Gregor Kleen <gregor.kleen@ifi.lmu.de>,Steffen Jost <jost@tcs.ifi.lmu.de>
|
-- SPDX-FileCopyrightText: 2022-2024 Sarah Vaupel <sarah.vaupel@uniworx.de>, Gregor Kleen <gregor.kleen@ifi.lmu.de>, Steffen Jost <jost@tcs.ifi.lmu.de>
|
||||||
--
|
--
|
||||||
-- SPDX-License-Identifier: AGPL-3.0-or-later
|
-- SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
@ -28,9 +28,7 @@ import Text.Hamlet
|
|||||||
-- import Handler.Utils.I18n
|
-- import Handler.Utils.I18n
|
||||||
|
|
||||||
import Handler.Admin.Test.Download (testDownload)
|
import Handler.Admin.Test.Download (testDownload)
|
||||||
import qualified Database.Esqueleto.Experimental as E (selectOne, unValue)
|
|
||||||
import qualified Database.Esqueleto.PostgreSQL as E (now_)
|
|
||||||
import qualified Database.Esqueleto.Utils as E (psqlVersion_)
|
|
||||||
|
|
||||||
-- BEGIN - Buttons needed only here
|
-- BEGIN - Buttons needed only here
|
||||||
data ButtonCreate = CreateMath | CreateInf | CrashApp -- Dummy for Example
|
data ButtonCreate = CreateMath | CreateInf | CrashApp -- Dummy for Example
|
||||||
@ -114,7 +112,7 @@ postAdminTestR = do
|
|||||||
let emailWidget' = wrapForm emailWidget def
|
let emailWidget' = wrapForm emailWidget def
|
||||||
{ formAction = Just . SomeRoute $ AdminTestR
|
{ formAction = Just . SomeRoute $ AdminTestR
|
||||||
, formEncoding = emailEnctype
|
, formEncoding = emailEnctype
|
||||||
, formAttrs = [asyncSubmitAttr] -- equivalent to [("uw-async-form", "")]
|
, formAttrs = [("uw-async-form", "")]
|
||||||
}
|
}
|
||||||
|
|
||||||
now <- liftIO getCurrentTime
|
now <- liftIO getCurrentTime
|
||||||
@ -228,13 +226,10 @@ postAdminTestR = do
|
|||||||
|
|
||||||
UniWorX{ appSettings' = AppSettings{..} } <- getYesod
|
UniWorX{ appSettings' = AppSettings{..} } <- getYesod
|
||||||
|
|
||||||
psqlVersion <- runDBRead $ E.selectOne $ return E.psqlVersion_
|
|
||||||
dbTime <- runDBRead $ E.selectOne $ return E.now_
|
|
||||||
|
|
||||||
let locallyDefinedPageHeading = [whamlet|Admin TestPage for Uni2work|]
|
let locallyDefinedPageHeading = [whamlet|Admin TestPage for Uni2work|]
|
||||||
siteLayout locallyDefinedPageHeading $ do
|
siteLayout locallyDefinedPageHeading $ do
|
||||||
-- defaultLayout $ do
|
-- defaultLayout $ do
|
||||||
setTitle "Uni2work Admin Testpage"
|
setTitle "Uni2work Admin Testpage"
|
||||||
|
|
||||||
$(i18nWidgetFile "admin-test")
|
$(i18nWidgetFile "admin-test")
|
||||||
|
|
||||||
@ -326,36 +321,25 @@ postAdminTestR = do
|
|||||||
<dl .deflist>
|
<dl .deflist>
|
||||||
<dt .deflist__dt> appJobCronInterval
|
<dt .deflist__dt> appJobCronInterval
|
||||||
<dd .deflist__dd>#{tshow appJobCronInterval}
|
<dd .deflist__dd>#{tshow appJobCronInterval}
|
||||||
<dt .deflist__dt> appSynchroniseLdapUsersWithin
|
<dt .deflist__dt> appUserSyncWithin
|
||||||
<dd .deflist__dd>#{tshow appSynchroniseLdapUsersWithin}
|
<dd .deflist__dd>#{tshow appUserSyncWithin}
|
||||||
<dt .deflist__dt> appSynchroniseAvsUsersWithin
|
<dt .deflist__dt> appSynchroniseAvsUsersWithin
|
||||||
<dd .deflist__dd>#{tshow appSynchroniseAvsUsersWithin}
|
<dd .deflist__dd>#{tshow appSynchroniseAvsUsersWithin}
|
||||||
|]
|
|]
|
||||||
|
|
||||||
[whamlet|
|
|
||||||
<section>
|
|
||||||
<h2> PostgreSQL Information
|
|
||||||
<dl .deflist>
|
|
||||||
$maybe pver <- psqlVersion
|
|
||||||
<dt .deflist__dt>DB Version
|
|
||||||
<dd .deflist__dd>#{E.unValue pver}
|
|
||||||
$maybe ptme <- dbTime
|
|
||||||
<dt .deflist__dt>DB Time
|
|
||||||
<dd .deflist__dd>#{tshow (E.unValue ptme)}
|
|
||||||
|]
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
getAdminTestPdfR :: Handler TypedContent
|
getAdminTestPdfR :: Handler TypedContent
|
||||||
getAdminTestPdfR = do
|
getAdminTestPdfR = do
|
||||||
usr <- requireAuth -- to determine language and recipient for test
|
usr <- requireAuth -- to determine language and recipient for test
|
||||||
qual <- fromMaybeM
|
qual <- fromMaybeM
|
||||||
(addMessage Error "Keine Qualifikation in der Datenbank zur Erzeugung eines Test-PDFs gefunden." >> redirect AdminTestR)
|
(addMessage Error "Keine Qualifikation in der Datenbank zur Erzeugung eines Test-PDFs gefunden." >> redirect AdminTestR)
|
||||||
(runDB $ selectFirst [] [Asc QualificationAvsLicence, Asc QualificationShorthand])
|
(runDB $ selectFirst [] [Asc QualificationAvsLicence, Asc QualificationShorthand])
|
||||||
encRecipient :: CryptoUUIDUser <- encrypt $ usr ^. _entityKey
|
encRecipient :: CryptoUUIDUser <- encrypt $ usr ^. _entityKey
|
||||||
now <- liftIO getCurrentTime
|
now <- liftIO getCurrentTime
|
||||||
let nowaday = utctDay now
|
let nowaday = utctDay now
|
||||||
letter = LetterRenewQualification
|
letter = LetterRenewQualificationF
|
||||||
{ lmsLogin = LmsIdent "abcdefgh"
|
{ lmsLogin = LmsIdent "abcdefgh"
|
||||||
, lmsPin = "12345678"
|
, lmsPin = "12345678"
|
||||||
, qualHolderID = usr ^. _entityKey
|
, qualHolderID = usr ^. _entityKey
|
||||||
@ -367,17 +351,15 @@ getAdminTestPdfR = do
|
|||||||
, qualShort = qual ^. _qualificationShorthand . _CI
|
, qualShort = qual ^. _qualificationShorthand . _CI
|
||||||
, qualSchool = qual ^. _qualificationSchool
|
, qualSchool = qual ^. _qualificationSchool
|
||||||
, qualDuration = qual ^. _qualificationValidDuration
|
, qualDuration = qual ^. _qualificationValidDuration
|
||||||
, qualRenewAuto = qual ^. _qualificationElearningRenews
|
|
||||||
, qualELimit = qual ^. _qualificationElearningLimit
|
|
||||||
, isReminder = False
|
, isReminder = False
|
||||||
}
|
}
|
||||||
apcIdent <- letterApcIdent letter encRecipient now
|
apcIdent <- letterApcIdent letter encRecipient now
|
||||||
renderLetterPDF usr letter apcIdent Nothing >>= \case
|
renderLetterPDF usr letter apcIdent >>= \case
|
||||||
Left err -> sendResponseStatus internalServerError500 $ "PDF generation failed: \n" <> err
|
Left err -> sendResponseStatus internalServerError500 $ "PDF generation failed: \n" <> err
|
||||||
Right pdf -> do
|
Right pdf -> do
|
||||||
liftIO $ LBS.writeFile "/tmp/generated.pdf" pdf
|
liftIO $ LBS.writeFile "/tmp/generated.pdf" pdf
|
||||||
encryptPDF "tomatenmarmelade" pdf >>= \case
|
encryptPDF "tomatenmarmelade" pdf >>= \case
|
||||||
Left err -> sendResponseStatus internalServerError500 $ "PDFtk error: \n" <> err
|
Left err -> sendResponseStatus internalServerError500 $ "PDFtk error: \n" <> err
|
||||||
Right encPdf -> do
|
Right encPdf -> do
|
||||||
liftIO $ LBS.writeFile "/tmp/crypted.pdf" encPdf
|
liftIO $ LBS.writeFile "/tmp/crypted.pdf" encPdf
|
||||||
sendByteStringAsFile "demoPDF.pdf" (LBS.toStrict pdf) now
|
sendByteStringAsFile "demoPDF.pdf" (LBS.toStrict pdf) now
|
||||||
|
|||||||
@ -1,152 +0,0 @@
|
|||||||
-- SPDX-FileCopyrightText: 2024 Steffen Jost <s.jost@fraport.de>
|
|
||||||
--
|
|
||||||
-- SPDX-License-Identifier: AGPL-3.0-or-later
|
|
||||||
|
|
||||||
{-# OPTIONS_GHC -fno-warn-orphans #-}
|
|
||||||
|
|
||||||
module Handler.CommCenter
|
|
||||||
( getCommCenterR
|
|
||||||
) where
|
|
||||||
|
|
||||||
import Import
|
|
||||||
import Handler.Utils
|
|
||||||
|
|
||||||
-- import qualified Data.Set as Set
|
|
||||||
import qualified Data.Map as Map
|
|
||||||
-- import qualified Data.Text as Text
|
|
||||||
import Data.Text.Lens (packed)
|
|
||||||
|
|
||||||
-- import Database.Persist.Sql (updateWhereCount)
|
|
||||||
-- import Database.Esqueleto.Experimental ((:&)(..))
|
|
||||||
import qualified Database.Esqueleto.Legacy as EL (on) -- only `on` and `from` are different, needed for dbTable using Esqueleto.Legacy
|
|
||||||
import qualified Database.Esqueleto.Experimental as E
|
|
||||||
import qualified Database.Esqueleto.Utils as E
|
|
||||||
import qualified Database.Esqueleto.PostgreSQL as E
|
|
||||||
import Database.Esqueleto.Utils.TH
|
|
||||||
|
|
||||||
|
|
||||||
-- avoids repetition of local definitions
|
|
||||||
single :: (k,a) -> Map k a
|
|
||||||
single = uncurry Map.singleton
|
|
||||||
|
|
||||||
|
|
||||||
data CCTableAction = CCActDummy -- just a dummy, since we don't now yet which actions we will be needing
|
|
||||||
deriving (Eq, Ord, Enum, Bounded, Read, Show, Generic)
|
|
||||||
|
|
||||||
instance Universe CCTableAction
|
|
||||||
instance Finite CCTableAction
|
|
||||||
nullaryPathPiece ''CCTableAction $ camelToPathPiece' 2
|
|
||||||
embedRenderMessage ''UniWorX ''CCTableAction id
|
|
||||||
|
|
||||||
data CCTableActionData = CCActDummyData
|
|
||||||
deriving (Eq, Ord, Read, Show, Generic)
|
|
||||||
|
|
||||||
|
|
||||||
-- SJ: I don't know how to use E.unionAll_ with dbTable, so we simulate it by a FullOuterJoin with constant False ON-clause instead
|
|
||||||
type CCTableExpr =
|
|
||||||
( (E.SqlExpr (Maybe (Entity User)) `E.InnerJoin` E.SqlExpr (Maybe (Entity SentMail)))
|
|
||||||
`E.FullOuterJoin` (E.SqlExpr (Maybe (Entity User)) `E.InnerJoin` E.SqlExpr (Maybe (Entity PrintJob)))
|
|
||||||
)
|
|
||||||
|
|
||||||
queryRecipientMail :: CCTableExpr -> E.SqlExpr (Maybe (Entity User))
|
|
||||||
queryRecipientMail = $(sqlIJproj 2 1) . $(sqlFOJproj 2 1)
|
|
||||||
|
|
||||||
queryMail :: CCTableExpr -> E.SqlExpr (Maybe (Entity SentMail))
|
|
||||||
queryMail = $(sqlIJproj 2 2) . $(sqlFOJproj 2 1)
|
|
||||||
|
|
||||||
queryRecipientPrint :: CCTableExpr -> E.SqlExpr (Maybe (Entity User))
|
|
||||||
queryRecipientPrint = $(sqlIJproj 2 1) . $(sqlFOJproj 2 2)
|
|
||||||
|
|
||||||
queryPrint :: CCTableExpr -> E.SqlExpr (Maybe (Entity PrintJob))
|
|
||||||
queryPrint = $(sqlIJproj 2 2) . $(sqlFOJproj 2 2)
|
|
||||||
|
|
||||||
type CCTableData = DBRow (Maybe (Entity User), Maybe (Entity SentMail), Maybe (Entity User), Maybe (Entity PrintJob))
|
|
||||||
|
|
||||||
resultRecipientMail :: Traversal' CCTableData (Entity User)
|
|
||||||
resultRecipientMail = _dbrOutput . _1 . _Just
|
|
||||||
|
|
||||||
resultMail :: Traversal' CCTableData (Entity SentMail)
|
|
||||||
resultMail = _dbrOutput . _2 . _Just
|
|
||||||
|
|
||||||
resultRecipientPrint :: Traversal' CCTableData (Entity User)
|
|
||||||
resultRecipientPrint = _dbrOutput . _3 . _Just
|
|
||||||
|
|
||||||
resultPrint :: Traversal' CCTableData (Entity PrintJob)
|
|
||||||
resultPrint = _dbrOutput . _4 . _Just
|
|
||||||
|
|
||||||
|
|
||||||
mkCCTable :: DB (Any, Widget)
|
|
||||||
mkCCTable = do
|
|
||||||
let
|
|
||||||
dbtSQLQuery :: CCTableExpr -> E.SqlQuery (E.SqlExpr (Maybe (Entity User)), E.SqlExpr (Maybe (Entity SentMail)), E.SqlExpr (Maybe (Entity User)), E.SqlExpr (Maybe (Entity PrintJob)))
|
|
||||||
dbtSQLQuery ((recipientMail `E.InnerJoin` mail) `E.FullOuterJoin` (recipientPrint `E.InnerJoin` printJob)) = do
|
|
||||||
EL.on $ recipientMail E.?. UserId E.==. E.joinV (mail E.?. SentMailRecipient)
|
|
||||||
EL.on $ recipientPrint E.?. UserId E.==. E.joinV (printJob E.?. PrintJobRecipient)
|
|
||||||
-- EL.on $ recipientMail E.?. UserId E.==. recipientPrint E.?. UserId E.&&. E.false -- simulating E.unionAll_ by a constant false full outer join, since it is unclear how dbTable could handle E.unionAll_
|
|
||||||
EL.on E.false -- simulating E.unionAll_ by a constant false full outer join, since it is unclear how dbTable could handle E.unionAll_
|
|
||||||
-- E.where_ $ E.isJust (recipientMail E.?. UserId) E.||. E.isJust (recipientPrint E.?. UserId) -- not needed for full outer join
|
|
||||||
-- return (E.coalesce[recipientMail, recipientPrint], mail, print) -- coalesce only works on values, not entities
|
|
||||||
return (recipientMail, mail, recipientPrint, printJob)
|
|
||||||
-- dbtRowKey = (,) <$> views (to queryMail) (E.?. SentMailId) <*> views (to queryPrint) (E.?. PrintJobId)
|
|
||||||
dbtRowKey ((_recipientMail `E.InnerJoin` mail) `E.FullOuterJoin` (_recipientPrint `E.InnerJoin` printJob)) = (mail E.?. SentMailId, printJob E.?. PrintJobId)
|
|
||||||
|
|
||||||
dbtProj = dbtProjId
|
|
||||||
dbtColonnade = dbColonnade $ mconcat -- prefer print over email in the impossible case that both are Just
|
|
||||||
[ sortable (Just "date") (i18nCell MsgPrintJobCreated) $ \row ->
|
|
||||||
let tprint = row ^? resultPrint . _entityVal . _printJobCreated
|
|
||||||
tmail = row ^? resultMail . _entityVal . _sentMailSentAt
|
|
||||||
in maybeCell (tprint <|> tmail) dateTimeCell
|
|
||||||
, sortable (Just "recipient") (i18nCell MsgPrintRecipient) $ \row ->
|
|
||||||
let uprint = row ^? resultRecipientPrint
|
|
||||||
umail = row ^? resultRecipientMail
|
|
||||||
in maybeCell (uprint <|> umail) $ cellHasUserLink AdminUserR
|
|
||||||
, sortable Nothing (i18nCell MsgCommBody) $ \row -> if
|
|
||||||
| (Just k) <- row ^? resultPrint . _entityKey
|
|
||||||
-> anchorCellM (PrintDownloadR <$> encrypt k) $ toWgt (iconLetterOrEmail True ) <> text2widget "-link"
|
|
||||||
| (Just k) <- row ^? resultMail . _entityKey
|
|
||||||
-> anchorCellM (MailHtmlR <$> encrypt k) $ toWgt (iconLetterOrEmail False) <> text2widget "-link"
|
|
||||||
| otherwise
|
|
||||||
-> mempty
|
|
||||||
, sortable Nothing (i18nCell MsgCommSubject) $ \row ->
|
|
||||||
let tsubject = row ^? resultPrint . _entityVal . _printJobFilename . packed
|
|
||||||
msubject = row ^? resultMail . _entityVal . _sentMailHeaders . _mailHeaders' . _mailHeader' "Subject"
|
|
||||||
in maybeCell (tsubject <|> msubject) textCell
|
|
||||||
]
|
|
||||||
dbtSorting = mconcat
|
|
||||||
[ singletonMap "date" $ SortColumn $ \row -> E.coalesce [queryPrint row E.?. PrintJobCreated, queryMail row E.?. SentMailSentAt]
|
|
||||||
, singletonMap "recipient" $ SortColumns $ \row ->
|
|
||||||
[ SomeExprValue $ E.coalesce [queryRecipientPrint row E.?. UserSurname , queryRecipientMail row E.?. UserSurname ]
|
|
||||||
, SomeExprValue $ E.coalesce [queryRecipientPrint row E.?. UserDisplayName, queryRecipientMail row E.?. UserDisplayName]
|
|
||||||
]
|
|
||||||
]
|
|
||||||
dbtFilter = mconcat
|
|
||||||
[ single ("sent" , FilterColumn . E.mkDayFilterTo
|
|
||||||
$ \row -> E.coalesceDefault [queryPrint row E.?. PrintJobCreated, queryMail row E.?. SentMailSentAt] E.now_) -- either one is guaranteed to be non-null, default never used
|
|
||||||
, single ("recipient" , FilterColumn . E.mkContainsFilterWithCommaPlus Just
|
|
||||||
$ \row -> E.coalesce [queryRecipientPrint row E.?. UserDisplayName, queryRecipientMail row E.?. UserDisplayName])
|
|
||||||
, single ("subject" , FilterColumn . E.mkContainsFilterWithCommaPlus Just
|
|
||||||
$ \row -> E.coalesce [E.str2text' $ queryPrint row E.?. PrintJobFilename
|
|
||||||
,E.str2text' $ queryMail row E.?. SentMailHeaders ])
|
|
||||||
]
|
|
||||||
dbtFilterUI mPrev = mconcat
|
|
||||||
[ prismAForm (singletonFilter "date" . maybePrism _PathPiece) mPrev $ aopt (hoistField lift dayField) (fslI MsgPrintJobCreated)
|
|
||||||
, prismAForm (singletonFilter "recipient" . maybePrism _PathPiece) mPrev $ aopt (hoistField lift textField) (fslI MsgPrintRecipient & setTooltip MsgTableFilterCommaPlus)
|
|
||||||
, prismAForm (singletonFilter "subject" . maybePrism _PathPiece) mPrev $ aopt (hoistField lift textField) (fslI MsgCommSubject & setTooltip MsgTableFilterCommaPlusShort)
|
|
||||||
]
|
|
||||||
dbtStyle = def { dbsFilterLayout = defaultDBSFilterLayout}
|
|
||||||
dbtIdent :: Text
|
|
||||||
dbtIdent = "comms"
|
|
||||||
dbtCsvEncode = noCsvEncode
|
|
||||||
dbtCsvDecode = Nothing
|
|
||||||
dbtExtraReps = []
|
|
||||||
dbtParams = def
|
|
||||||
psValidator = def & defaultSorting [SortDescBy "date"]
|
|
||||||
dbTable psValidator DBTable{..}
|
|
||||||
|
|
||||||
getCommCenterR :: Handler Html
|
|
||||||
getCommCenterR = do
|
|
||||||
(_, ccTable) <- runDB mkCCTable
|
|
||||||
siteLayoutMsg MsgMenuCommCenter $ do
|
|
||||||
setTitleI MsgMenuCommCenter
|
|
||||||
$(widgetFile "comm-center")
|
|
||||||
|
|
||||||
@ -1,4 +1,4 @@
|
|||||||
-- SPDX-FileCopyrightText: 2022-24 Gregor Kleen <gregor.kleen@ifi.lmu.de>,Sarah Vaupel <sarah.vaupel@ifi.lmu.de>,Sarah Vaupel <vaupel.sarah@campus.lmu.de>,Steffen Jost <jost@cip.ifi.lmu.de>,Steffen Jost <s.jost@fraport.de>,Winnie Ros <winnie.ros@campus.lmu.de>
|
-- SPDX-FileCopyrightText: 2022 Gregor Kleen <gregor.kleen@ifi.lmu.de>,Sarah Vaupel <sarah.vaupel@ifi.lmu.de>,Sarah Vaupel <vaupel.sarah@campus.lmu.de>,Steffen Jost <jost@cip.ifi.lmu.de>,Steffen Jost <jost@tcs.ifi.lmu.de>,Winnie Ros <winnie.ros@campus.lmu.de>
|
||||||
--
|
--
|
||||||
-- SPDX-License-Identifier: AGPL-3.0-or-later
|
-- SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
@ -46,13 +46,12 @@ data CourseForm = CourseForm
|
|||||||
, cfRegTo :: Maybe UTCTime
|
, cfRegTo :: Maybe UTCTime
|
||||||
, cfDeRegUntil :: Maybe UTCTime
|
, cfDeRegUntil :: Maybe UTCTime
|
||||||
, cfLecturers :: [Either (UserEmail, Maybe LecturerType) (UserId, LecturerType)]
|
, cfLecturers :: [Either (UserEmail, Maybe LecturerType) (UserId, LecturerType)]
|
||||||
, cfQualis :: [(QualificationId, Int)]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
makeLenses_ ''CourseForm
|
makeLenses_ ''CourseForm
|
||||||
|
|
||||||
courseToForm :: Entity Course -> [Lecturer] -> Map UserEmail (InvitationDBData Lecturer) -> [CourseQualification] -> CourseForm
|
courseToForm :: Entity Course -> [Lecturer] -> Map UserEmail (InvitationDBData Lecturer) -> CourseForm
|
||||||
courseToForm (Entity cid Course{..}) lecs lecInvites qualis = CourseForm
|
courseToForm (Entity cid Course{..}) lecs lecInvites = CourseForm
|
||||||
{ cfCourseId = Just cid
|
{ cfCourseId = Just cid
|
||||||
, cfName = courseName
|
, cfName = courseName
|
||||||
, cfDesc = courseDescription
|
, cfDesc = courseDescription
|
||||||
@ -70,9 +69,6 @@ courseToForm (Entity cid Course{..}) lecs lecInvites qualis = CourseForm
|
|||||||
, cfDeRegUntil = courseDeregisterUntil
|
, cfDeRegUntil = courseDeregisterUntil
|
||||||
, cfLecturers = [Right (lecturerUser, lecturerType) | Lecturer{..} <- lecs]
|
, cfLecturers = [Right (lecturerUser, lecturerType) | Lecturer{..} <- lecs]
|
||||||
++ [Left (email, mType) | (email, InvDBDataLecturer mType) <- Map.toList lecInvites ]
|
++ [Left (email, mType) | (email, InvDBDataLecturer mType) <- Map.toList lecInvites ]
|
||||||
-- TODO: Filterung nach aktueller Schule, da ansonsten ein Sicherheitleck droht! Siehe #150
|
|
||||||
, cfQualis = [ (courseQualificationQualification, courseQualificationSortOrder)
|
|
||||||
| CourseQualification{..} <- qualis, courseQualificationCourse == cid ]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -85,19 +81,17 @@ makeCourseForm miButtonAction template = identifyForm FIDcourse . validateFormDB
|
|||||||
MsgRenderer mr <- getMsgRenderer
|
MsgRenderer mr <- getMsgRenderer
|
||||||
|
|
||||||
uid <- liftHandler requireAuthId
|
uid <- liftHandler requireAuthId
|
||||||
(userSchools, elegibleQualifications) :: ([SchoolId], OptionList QualificationId) <- liftHandler . runDB $ do
|
(lecturerSchools, adminSchools, oldSchool) <- liftHandler . runDB $ do
|
||||||
lecturerSchools <- map (userFunctionSchool . entityVal) <$> selectList [UserFunctionUser ==. uid, UserFunctionFunction <-. [SchoolLecturer]] []
|
lecturerSchools <- map (userFunctionSchool . entityVal) <$> selectList [UserFunctionUser ==. uid, UserFunctionFunction <-. [SchoolLecturer]] []
|
||||||
protoAdminSchools <- map (userFunctionSchool . entityVal) <$> selectList [UserFunctionUser ==. uid, UserFunctionFunction <-. [SchoolAdmin]] [] -- default rights
|
protoAdminSchools <- map (userFunctionSchool . entityVal) <$> selectList [UserFunctionUser ==. uid, UserFunctionFunction <-. [SchoolAdmin]] []
|
||||||
adminSchools <- filterM (hasWriteAccessTo . flip SchoolR SchoolEditR) protoAdminSchools -- and user as admin rights active right now
|
adminSchools <- filterM (hasWriteAccessTo . flip SchoolR SchoolEditR) protoAdminSchools
|
||||||
oldSchool <- forM (cfCourseId =<< template) $ fmap courseSchool . getJust
|
oldSchool <- forM (cfCourseId =<< template) $ fmap courseSchool . getJust
|
||||||
let elegibleSchools = Set.fromList $ lecturerSchools ++ adminSchools
|
return (lecturerSchools, adminSchools, oldSchool)
|
||||||
userSchools = Set.toList $ maybe id Set.insert oldSchool elegibleSchools
|
let userSchools = nubOrd . maybe id (:) oldSchool $ lecturerSchools ++ adminSchools
|
||||||
elegibleQualifications <- selectList [QualificationSchool <-. Set.toList elegibleSchools] [Asc QualificationName, Asc QualificationSchool]
|
|
||||||
return (userSchools, qualificationsOptionList elegibleQualifications)
|
|
||||||
|
|
||||||
(termsField, userTerms) <- liftHandler $ case template of
|
(termsField, userTerms) <- liftHandler $ case template of
|
||||||
-- Change of term is only allowed if user may delete the course (i.e. no participants) or admin
|
-- Change of term is only allowed if user may delete the course (i.e. no participants) or admin
|
||||||
(Just cform) | (Just cid) <- cfCourseId cform -> do -- edit existing course& \c
|
(Just cform) | (Just cid) <- cfCourseId cform -> do -- edit existing course
|
||||||
_courseOld@Course{..} <- runDB $ get404 cid
|
_courseOld@Course{..} <- runDB $ get404 cid
|
||||||
mayEditTerm <- isAuthorized TermEditR True
|
mayEditTerm <- isAuthorized TermEditR True
|
||||||
mayDelete <- isAuthorized (CourseR courseTerm courseSchool courseShorthand CDeleteR) True
|
mayDelete <- isAuthorized (CourseR courseTerm courseSchool courseShorthand CDeleteR) True
|
||||||
@ -108,7 +102,51 @@ makeCourseForm miButtonAction template = identifyForm FIDcourse . validateFormDB
|
|||||||
-> return (termsSetField [cfTerm cform], [cfTerm cform])
|
-> return (termsSetField [cfTerm cform], [cfTerm cform])
|
||||||
_allOtherCases -> (termsAllowedField, ) . Set.toList <$> runDB getActiveTerms
|
_allOtherCases -> (termsAllowedField, ) . Set.toList <$> runDB getActiveTerms
|
||||||
|
|
||||||
let lecturerForm :: AForm Handler [Either (UserEmail, Maybe LecturerType) (UserId, LecturerType)]
|
let miAdd :: ListPosition -> Natural -> ListLength -> (Text -> Text) -> FieldView UniWorX -> Maybe (Form (Map ListPosition (Either UserEmail UserId) -> FormResult (Map ListPosition (Either UserEmail UserId))))
|
||||||
|
miAdd _ _ _ nudge btn = Just $ \csrf -> do
|
||||||
|
(addRes, addView) <- mpreq (multiUserInvitationField $ MUILookupAnyUser Nothing) (fslI MsgCourseLecturerEmail & addName (nudge "email") & addPlaceholder (mr MsgLdapIdentificationOrEmail)) Nothing
|
||||||
|
let addRes'' = addRes <&> \newDat oldDat -> if
|
||||||
|
| existing <- newDat `Set.intersection` Set.fromList (Map.elems oldDat)
|
||||||
|
, not $ Set.null existing
|
||||||
|
-> FormFailure [mr MsgCourseLecturerAlreadyAdded]
|
||||||
|
| otherwise
|
||||||
|
-> FormSuccess . Map.fromList . zip [maybe 0 (succ . fst) $ Map.lookupMax oldDat ..] $ Set.toList newDat
|
||||||
|
addView' = $(widgetFile "course/lecturerMassInput/add")
|
||||||
|
return (addRes'', addView')
|
||||||
|
|
||||||
|
miCell :: ListPosition -> Either UserEmail UserId -> Maybe (Maybe LecturerType) -> (Text -> Text) -> Form (Maybe LecturerType)
|
||||||
|
miCell _ (Right lid) defType nudge = \csrf -> do
|
||||||
|
(lrwRes,lrwView) <- mreq (selectField optionsFinite) (fslI MsgCourseLecturerType & addName (nudge "lecturer-type")) (join defType)
|
||||||
|
usr <- liftHandler . runDB $ get404 lid
|
||||||
|
let lrwView' = $(widgetFile "course/lecturerMassInput/cellKnown")
|
||||||
|
return (Just <$> lrwRes,lrwView')
|
||||||
|
miCell _ (Left lEmail) defType nudge = \csrf -> do
|
||||||
|
(lrwRes,lrwView) <- mopt (selectField optionsFinite) ("" & addName (nudge "lecturer-type")) defType
|
||||||
|
invWarnMsg <- messageIconI Info IconEmail MsgEmailInvitationWarning
|
||||||
|
let lrwView' = $(widgetFile "course/lecturerMassInput/cellInvitation")
|
||||||
|
return (lrwRes,lrwView')
|
||||||
|
|
||||||
|
miDelete :: Map ListPosition (Either UserEmail UserId) -- ^ Current shape
|
||||||
|
-> ListPosition -- ^ Coordinate to delete
|
||||||
|
-> MaybeT (MForm (HandlerFor UniWorX)) (Map ListPosition ListPosition)
|
||||||
|
miDelete = miDeleteList
|
||||||
|
|
||||||
|
miAddEmpty :: ListPosition -> Natural -> ListLength -> Set ListPosition
|
||||||
|
miAddEmpty _ _ _ = Set.empty
|
||||||
|
|
||||||
|
miLayout :: ListLength
|
||||||
|
-> Map ListPosition (Either UserEmail UserId, FormResult (Maybe LecturerType)) -- ^ massInput state
|
||||||
|
-> Map ListPosition Widget -- ^ Cell widgets
|
||||||
|
-> Map ListPosition (FieldView UniWorX) -- ^ Deletion buttons
|
||||||
|
-> Map (Natural, ListPosition) Widget -- ^ Addition widgets
|
||||||
|
-> Widget
|
||||||
|
miLayout lLength _ cellWdgts delButtons addWdgts = $(widgetFile "course/lecturerMassInput/layout")
|
||||||
|
|
||||||
|
miIdent :: Text
|
||||||
|
miIdent = "lecturers"
|
||||||
|
|
||||||
|
|
||||||
|
lecturerForm :: AForm Handler [Either (UserEmail, Maybe LecturerType) (UserId, LecturerType)]
|
||||||
lecturerForm = formToAForm . over (mapped._2) pure . over (mapped._1.mapped) (map liftEither . Map.elems) $ massInput
|
lecturerForm = formToAForm . over (mapped._2) pure . over (mapped._1.mapped) (map liftEither . Map.elems) $ massInput
|
||||||
MassInput{..}
|
MassInput{..}
|
||||||
(fslI MsgCourseLecturers & setTooltip MsgCourseLecturerRightsIdentical)
|
(fslI MsgCourseLecturers & setTooltip MsgCourseLecturerRightsIdentical)
|
||||||
@ -125,79 +163,6 @@ makeCourseForm miButtonAction template = identifyForm FIDcourse . validateFormDB
|
|||||||
unliftEither (Right (lid , lType )) = (Right lid , Just lType)
|
unliftEither (Right (lid , lType )) = (Right lid , Just lType)
|
||||||
unliftEither (Left (lEmail, mLType)) = (Left lEmail, mLType )
|
unliftEither (Left (lEmail, mLType)) = (Left lEmail, mLType )
|
||||||
|
|
||||||
miAdd :: ListPosition -> Natural -> ListLength -> (Text -> Text) -> FieldView UniWorX -> Maybe (Form (Map ListPosition (Either UserEmail UserId) -> FormResult (Map ListPosition (Either UserEmail UserId))))
|
|
||||||
miAdd _ _ _ nudge btn = Just $ \csrf -> do
|
|
||||||
(addRes, addView) <- mpreq (multiUserInvitationField $ MUILookupAnyUser Nothing) (fslI MsgCourseLecturerEmail & addName (nudge "email") & addPlaceholder (mr MsgLdapIdentificationOrEmail)) Nothing
|
|
||||||
let addRes'' = addRes <&> \newDat oldDat -> if
|
|
||||||
| existing <- newDat `Set.intersection` Set.fromList (Map.elems oldDat)
|
|
||||||
, not $ Set.null existing
|
|
||||||
-> FormFailure [mr MsgCourseLecturerAlreadyAdded]
|
|
||||||
| otherwise
|
|
||||||
-> FormSuccess . Map.fromList . zip [maybe 0 (succ . fst) $ Map.lookupMax oldDat ..] $ Set.toList newDat
|
|
||||||
addView' = $(widgetFile "course/lecturerMassInput/add")
|
|
||||||
return (addRes'', addView')
|
|
||||||
|
|
||||||
miCell :: ListPosition -> Either UserEmail UserId -> Maybe (Maybe LecturerType) -> (Text -> Text) -> Form (Maybe LecturerType)
|
|
||||||
miCell _ (Right lid) defType nudge = \csrf -> do
|
|
||||||
(lrwRes,lrwView) <- mreq (selectField optionsFinite) (fslI MsgCourseLecturerType & addName (nudge "lecturer-type")) (join defType)
|
|
||||||
usr <- liftHandler . runDB $ get404 lid
|
|
||||||
let lrwView' = $(widgetFile "course/lecturerMassInput/cellKnown")
|
|
||||||
return (Just <$> lrwRes,lrwView')
|
|
||||||
miCell _ (Left lEmail) defType nudge = \csrf -> do
|
|
||||||
(lrwRes,lrwView) <- mopt (selectField optionsFinite) ("" & addName (nudge "lecturer-type")) defType
|
|
||||||
invWarnMsg <- messageIconI Info IconEmail MsgEmailInvitationWarning
|
|
||||||
let lrwView' = $(widgetFile "course/lecturerMassInput/cellInvitation")
|
|
||||||
return (lrwRes,lrwView')
|
|
||||||
|
|
||||||
miDelete :: Map ListPosition (Either UserEmail UserId) -- ^ Current shape
|
|
||||||
-> ListPosition -- ^ Coordinate to delete
|
|
||||||
-> MaybeT (MForm (HandlerFor UniWorX)) (Map ListPosition ListPosition)
|
|
||||||
miDelete = miDeleteList
|
|
||||||
|
|
||||||
miAddEmpty :: ListPosition -> Natural -> ListLength -> Set ListPosition
|
|
||||||
miAddEmpty _ _ _ = Set.empty
|
|
||||||
|
|
||||||
miLayout :: ListLength
|
|
||||||
-> Map ListPosition (Either UserEmail UserId, FormResult (Maybe LecturerType)) -- ^ massInput state
|
|
||||||
-> Map ListPosition Widget -- ^ Cell widgets
|
|
||||||
-> Map ListPosition (FieldView UniWorX) -- ^ Deletion buttons
|
|
||||||
-> Map (Natural, ListPosition) Widget -- ^ Addition widgets
|
|
||||||
-> Widget
|
|
||||||
miLayout lLength _ cellWdgts delButtons addWdgts = $(widgetFile "course/lecturerMassInput/layout")
|
|
||||||
|
|
||||||
miIdent :: Text
|
|
||||||
miIdent = "lecturers"
|
|
||||||
|
|
||||||
qualificationsForm :: Maybe [(QualificationId, Int)] -> AForm Handler [(QualificationId, Int)] -- filter by admin school done later through upsertCourseQualifications
|
|
||||||
qualificationsForm = massInputAccumEditA miAdd miEdit miButtonAction miLayout miIdent (fslI $ MsgCourseQualifications 9) False
|
|
||||||
where
|
|
||||||
miIdent :: Text
|
|
||||||
miIdent = "qualifications"
|
|
||||||
|
|
||||||
miAdd :: (Text -> Text) -> FieldView UniWorX -> Form ([(QualificationId,Int)] -> FormResult [(QualificationId,Int)])
|
|
||||||
miAdd nudge submitView csrf = do
|
|
||||||
(formRes, formView) <- aCourseQualiForm nudge Nothing csrf
|
|
||||||
let addRes = formRes <&> \newDat@(newQid,oldOrd) (unzip -> (oldQids,oldOrds)) ->
|
|
||||||
let qidBad = guardMonoid (newQid `elem` oldQids) [mr MsgCourseEditQualificationFailExists]
|
|
||||||
ordBad = guardMonoid (oldOrd `elem` oldOrds) [mr MsgCourseEditQualificationFailOrder ]
|
|
||||||
problems = qidBad ++ ordBad
|
|
||||||
in if null problems
|
|
||||||
then FormSuccess $ pure newDat
|
|
||||||
else FormFailure problems
|
|
||||||
return (addRes, $(widgetFile "widgets/massinput/courseQualifications/add"))
|
|
||||||
|
|
||||||
miEdit :: (Text -> Text) -> (QualificationId, Int) -> Form (QualificationId, Int)
|
|
||||||
miEdit nudge = aCourseQualiForm nudge . Just
|
|
||||||
|
|
||||||
miLayout :: MassInputLayout ListLength (QualificationId,Int) (QualificationId, Int)
|
|
||||||
miLayout lLength _ cellWdgts delButtons addWdgts = $(widgetFile "widgets/massinput/courseQualifications/layout")
|
|
||||||
|
|
||||||
aCourseQualiForm :: (Text -> Text) -> Maybe (QualificationId, Int) -> Form (QualificationId, Int)
|
|
||||||
aCourseQualiForm nudge mTemplate csrf = do
|
|
||||||
(cquRes, cquView) <- mpreq (selectField $ pure elegibleQualifications) ("" & addName (nudge "cquali")) (view _1 <$> mTemplate)
|
|
||||||
(ordRes, ordView) <- mpreq intField ("" & addName (nudge "cqordr")) (view _2 <$> mTemplate)
|
|
||||||
return ((,) <$> cquRes <*> ordRes, $(widgetFile "widgets/massinput/courseQualifications/form"))
|
|
||||||
|
|
||||||
(newVisFrom,newRegFrom,newRegTo,newDeRegUntil) <- case template of
|
(newVisFrom,newRegFrom,newRegTo,newDeRegUntil) <- case template of
|
||||||
(Just cform) | (Just _cid) <- cfCourseId cform -> return (Nothing,Nothing,Nothing,Nothing)
|
(Just cform) | (Just _cid) <- cfCourseId cform -> return (Nothing,Nothing,Nothing,Nothing)
|
||||||
_allIOtherCases -> do
|
_allIOtherCases -> do
|
||||||
@ -243,7 +208,6 @@ makeCourseForm miButtonAction template = identifyForm FIDcourse . validateFormDB
|
|||||||
& setTooltip MsgCourseDeregisterUntilTip) (deepAlt (cfDeRegUntil <$> template) newDeRegUntil)
|
& setTooltip MsgCourseDeregisterUntilTip) (deepAlt (cfDeRegUntil <$> template) newDeRegUntil)
|
||||||
<* aformSection MsgCourseFormSectionAdministration
|
<* aformSection MsgCourseFormSectionAdministration
|
||||||
<*> lecturerForm
|
<*> lecturerForm
|
||||||
<*> qualificationsForm (cfQualis <$> template)
|
|
||||||
return (result, widget)
|
return (result, widget)
|
||||||
|
|
||||||
|
|
||||||
@ -263,10 +227,6 @@ validateCourse = do
|
|||||||
unless userAdmin $ do
|
unless userAdmin $ do
|
||||||
guardValidation MsgCourseUserMustBeLecturer
|
guardValidation MsgCourseUserMustBeLecturer
|
||||||
$ anyOf (traverse . _Right . _1) (== uid) cfLecturers
|
$ anyOf (traverse . _Right . _1) (== uid) cfLecturers
|
||||||
guardValidation MsgCourseEditQualificationFailExists
|
|
||||||
$ not $ hasDuplicates $ fst <$> cfQualis
|
|
||||||
guardValidation MsgCourseEditQualificationFailOrder
|
|
||||||
$ not $ hasDuplicates $ snd <$> cfQualis
|
|
||||||
|
|
||||||
warnValidation MsgCourseShorthandTooLong
|
warnValidation MsgCourseShorthandTooLong
|
||||||
$ length (CI.original cfShort) <= 10
|
$ length (CI.original cfShort) <= 10
|
||||||
@ -320,11 +280,8 @@ getCourseNewR = do
|
|||||||
E.limit 1
|
E.limit 1
|
||||||
return course
|
return course
|
||||||
template <- case oldCourses of
|
template <- case oldCourses of
|
||||||
(oldTemplate:_) -> runDB $ do
|
(oldTemplate:_) ->
|
||||||
mbLecs <- oldTemplate & \course -> map entityVal <$> selectList [LecturerCourse ==. entityKey course] [Asc LecturerType]
|
let newTemplate = courseToForm oldTemplate mempty mempty in
|
||||||
mbLecInvites <- oldTemplate & sourceInvitationsF . entityKey
|
|
||||||
mbQualis <- oldTemplate & \course -> map entityVal <$> selectList [CourseQualificationCourse ==. entityKey course] [Asc CourseQualificationSortOrder]
|
|
||||||
let newTemplate = courseToForm oldTemplate mbLecs mbLecInvites mbQualis
|
|
||||||
return $ Just $ newTemplate
|
return $ Just $ newTemplate
|
||||||
{ cfCourseId = Nothing
|
{ cfCourseId = Nothing
|
||||||
, cfTerm = TermKey $ termFromRational 0 -- invalid, will be ignored; undefined won't work due to strictness
|
, cfTerm = TermKey $ termFromRational 0 -- invalid, will be ignored; undefined won't work due to strictness
|
||||||
@ -334,9 +291,9 @@ getCourseNewR = do
|
|||||||
}
|
}
|
||||||
[] -> do
|
[] -> do
|
||||||
(tidOk,sshOk,cshOk) <- runDB $ (,,)
|
(tidOk,sshOk,cshOk) <- runDB $ (,,)
|
||||||
<$> ifNothingM mbTid True existsKey
|
<$> ifMaybeM mbTid True existsKey
|
||||||
<*> ifNothingM mbSsh True existsKey
|
<*> ifMaybeM mbSsh True existsKey
|
||||||
<*> ifNothingM mbCsh True (\csh -> not . null <$> selectKeysList [CourseShorthand ==. csh] [LimitTo 1])
|
<*> ifMaybeM mbCsh True (\csh -> not . null <$> selectKeysList [CourseShorthand ==. csh] [LimitTo 1])
|
||||||
unless tidOk $ addMessageI Warning $ MsgNoSuchTerm $ fromJust mbTid -- safe, since tidOk==True otherwise
|
unless tidOk $ addMessageI Warning $ MsgNoSuchTerm $ fromJust mbTid -- safe, since tidOk==True otherwise
|
||||||
unless sshOk $ addMessageI Warning $ MsgNoSuchSchool $ fromJust mbSsh -- safe, since sshOk==True otherwise
|
unless sshOk $ addMessageI Warning $ MsgNoSuchSchool $ fromJust mbSsh -- safe, since sshOk==True otherwise
|
||||||
unless cshOk $ addMessageI Warning $ MsgNoSuchCourseShorthand $ fromJust mbCsh
|
unless cshOk $ addMessageI Warning $ MsgNoSuchCourseShorthand $ fromJust mbCsh
|
||||||
@ -357,11 +314,10 @@ pgCEditR tid ssh csh = do
|
|||||||
mbCourse <- getBy (TermSchoolCourseShort tid ssh csh)
|
mbCourse <- getBy (TermSchoolCourseShort tid ssh csh)
|
||||||
mbLecs <- for mbCourse $ \course -> map entityVal <$> selectList [LecturerCourse ==. entityKey course] [Asc LecturerType]
|
mbLecs <- for mbCourse $ \course -> map entityVal <$> selectList [LecturerCourse ==. entityKey course] [Asc LecturerType]
|
||||||
mbLecInvites <- for mbCourse $ sourceInvitationsF . entityKey
|
mbLecInvites <- for mbCourse $ sourceInvitationsF . entityKey
|
||||||
mbQualis <- for mbCourse $ \course -> map entityVal <$> selectList [CourseQualificationCourse ==. entityKey course] [Asc CourseQualificationSortOrder]
|
return $ (,,) <$> mbCourse <*> mbLecs <*> mbLecInvites
|
||||||
return $ (,,,) <$> mbCourse <*> mbLecs <*> mbLecInvites <*> mbQualis
|
|
||||||
-- IMPORTANT: both GET and POST Handler must use the same template,
|
-- IMPORTANT: both GET and POST Handler must use the same template,
|
||||||
-- since an Edit is identified via CourseID, which is not embedded in the received form data for security reasons.
|
-- since an Edit is identified via CourseID, which is not embedded in the received form data for security reasons.
|
||||||
courseEditHandler (\p -> Just . SomeRoute $ CourseR tid ssh csh CEditR :#: p) $ $(uncurryN 4) courseToForm <$> courseData
|
courseEditHandler (\p -> Just . SomeRoute $ CourseR tid ssh csh CEditR :#: p) $ $(uncurryN 3) courseToForm <$> courseData
|
||||||
|
|
||||||
|
|
||||||
-- | Course Creation and Editing
|
-- | Course Creation and Editing
|
||||||
@ -401,7 +357,6 @@ courseEditHandler miButtonAction mbCourseForm = do
|
|||||||
let (invites, adds) = partitionEithers $ cfLecturers res
|
let (invites, adds) = partitionEithers $ cfLecturers res
|
||||||
insertMany_ $ map (\(lid, lty) -> Lecturer lid cid lty) adds
|
insertMany_ $ map (\(lid, lty) -> Lecturer lid cid lty) adds
|
||||||
sinkInvitationsF lecturerInvitationConfig $ map (\(lEmail, mLty) -> (lEmail, cid, (InvDBDataLecturer mLty, InvTokenDataLecturer))) invites
|
sinkInvitationsF lecturerInvitationConfig $ map (\(lEmail, mLty) -> (lEmail, cid, (InvDBDataLecturer mLty, InvTokenDataLecturer))) invites
|
||||||
void $ upsertCourseQualifications aid cid $ cfQualis res
|
|
||||||
insert_ $ CourseEdit aid now cid
|
insert_ $ CourseEdit aid now cid
|
||||||
memcachedByInvalidate AuthCacheLecturerList $ Proxy @(Set UserId)
|
memcachedByInvalidate AuthCacheLecturerList $ Proxy @(Set UserId)
|
||||||
return insertOkay
|
return insertOkay
|
||||||
@ -450,9 +405,11 @@ courseEditHandler miButtonAction mbCourseForm = do
|
|||||||
let (invites, adds) = partitionEithers $ cfLecturers res
|
let (invites, adds) = partitionEithers $ cfLecturers res
|
||||||
insertMany_ $ map (\(lid, lty) -> Lecturer lid cid lty) adds
|
insertMany_ $ map (\(lid, lty) -> Lecturer lid cid lty) adds
|
||||||
sinkInvitationsF lecturerInvitationConfig $ map (\(lEmail, mLty) -> (lEmail, cid, (InvDBDataLecturer mLty, InvTokenDataLecturer))) invites
|
sinkInvitationsF lecturerInvitationConfig $ map (\(lEmail, mLty) -> (lEmail, cid, (InvDBDataLecturer mLty, InvTokenDataLecturer))) invites
|
||||||
void $ upsertCourseQualifications aid cid $ cfQualis res
|
|
||||||
insert_ $ CourseEdit aid now cid
|
insert_ $ CourseEdit aid now cid
|
||||||
|
|
||||||
memcachedByInvalidate AuthCacheLecturerList $ Proxy @(Set UserId)
|
memcachedByInvalidate AuthCacheLecturerList $ Proxy @(Set UserId)
|
||||||
|
|
||||||
addMessageI Success $ MsgCourseEditOk tid ssh csh
|
addMessageI Success $ MsgCourseEditOk tid ssh csh
|
||||||
return True
|
return True
|
||||||
when success $ redirect $ CourseR tid ssh csh CShowR
|
when success $ redirect $ CourseR tid ssh csh CShowR
|
||||||
@ -463,35 +420,3 @@ courseEditHandler miButtonAction mbCourseForm = do
|
|||||||
{ formAction = Just $ SomeRoute actionUrl
|
{ formAction = Just $ SomeRoute actionUrl
|
||||||
, formEncoding = formEnctype
|
, formEncoding = formEnctype
|
||||||
}
|
}
|
||||||
|
|
||||||
-- upsertCourseQualifications :: forall m backend . (MonadIO m, PersistStoreWrite backend, PersistQueryRead backend) => UserId -> CourseId -> [(QualificationId, Int)] -> ReaderT backend m Bool
|
|
||||||
upsertCourseQualifications :: UserId -> CourseId -> [(QualificationId, Int)] -> YesodJobDB UniWorX Bool -- could be generalized
|
|
||||||
upsertCourseQualifications uid cid qualis = do
|
|
||||||
let newQualis = Map.fromList qualis
|
|
||||||
oldQualis <- Map.fromList . fmap (\Entity{entityKey=k, entityVal=CourseQualification{..}} -> (courseQualificationQualification, (k, courseQualificationSortOrder)))
|
|
||||||
<$> selectList [CourseQualificationCourse ==. cid] [Asc CourseQualificationQualification]
|
|
||||||
-- NOTE: CourseQualification allow the immediate assignment of these qualifications to any enrolled user. Hence SchoolAdmins must not be allowed to assign school-foreign qualifications, see #150
|
|
||||||
okSchools <- Set.fromList . fmap (userFunctionSchool . entityVal)
|
|
||||||
<$> selectList [UserFunctionUser ==. uid, UserFunctionFunction <-. [SchoolAdmin, SchoolLecturer]] [Asc UserFunctionSchool]
|
|
||||||
{- Some debugging due to an error caused by using fromDistinctAscList with violated precondition:
|
|
||||||
$logErrorS "CourseQuali" $ "OLD Course Qualifications:" <> tshow oldQualis
|
|
||||||
$logErrorS "CourseQuali" $ "NEW Course Qualifications:" <> tshow newQualis
|
|
||||||
$logErrorS "CourseQuali" $ "DIFF Course Qualifications:" <> tshow (newQualis Map.\\ oldQualis)
|
|
||||||
-}
|
|
||||||
foldWithKeyMapM oldQualis $ \qu (k, so_old) -> case Map.lookup qu newQualis of
|
|
||||||
Just so_new | so_new /= so_old
|
|
||||||
-> update k [CourseQualificationSortOrder =. so_new] -- existing CourseQualifications may be re-ordered, regardless of school association
|
|
||||||
Nothing -> delete k -- existing CourseQualifications may be removed, regardless of school association
|
|
||||||
_ -> return ()
|
|
||||||
res <- foldWithKeyMapM (newQualis Map.\\ oldQualis) $ \qu so -> get qu >>= \case
|
|
||||||
Just Qualification{qualificationSchool=ssh, qualificationShorthand=qsh}
|
|
||||||
| Set.member ssh okSchools ->
|
|
||||||
insert_ CourseQualification{courseQualificationQualification = qu, courseQualificationCourse = cid, courseQualificationSortOrder = so}
|
|
||||||
$> All True
|
|
||||||
| otherwise -> do
|
|
||||||
addMessageI Warning $ MsgCourseEditQualificationFailRights qsh ssh
|
|
||||||
pure $ All False
|
|
||||||
_ -> do
|
|
||||||
addMessageI Warning MsgCourseEditQualificationFail
|
|
||||||
pure $ All False
|
|
||||||
pure $ getAll res
|
|
||||||
|
|||||||
@ -68,7 +68,7 @@ courseRegisterForm (Entity cid Course{..}) = liftHandler $ do
|
|||||||
| otherwise
|
| otherwise
|
||||||
-> return $ FormSuccess ()
|
-> return $ FormSuccess ()
|
||||||
|
|
||||||
mayViewCourseAfterDeregistration <- liftHandler . runDBRead $ E.selectExists . E.from $ \course -> do
|
mayViewCourseAfterDeregistration <- liftHandler . runDB $ E.selectExists . E.from $ \course -> do
|
||||||
E.where_ $ course E.^. CourseId E.==. E.val cid
|
E.where_ $ course E.^. CourseId E.==. E.val cid
|
||||||
E.&&. ( isSchoolAdminLike muid ata (course E.^. CourseSchool)
|
E.&&. ( isSchoolAdminLike muid ata (course E.^. CourseSchool)
|
||||||
E.||. mayEditCourse muid ata course
|
E.||. mayEditCourse muid ata course
|
||||||
@ -92,7 +92,7 @@ courseMayReRegister :: Entity Course -> DB Bool
|
|||||||
courseMayReRegister (Entity cid Course{..}) = do
|
courseMayReRegister (Entity cid Course{..}) = do
|
||||||
registrations <- count [ CourseParticipantState ==. CourseParticipantActive, CourseParticipantCourse ==. cid ]
|
registrations <- count [ CourseParticipantState ==. CourseParticipantActive, CourseParticipantCourse ==. cid ]
|
||||||
let capacity = maybe True (>= registrations) courseCapacity
|
let capacity = maybe True (>= registrations) courseCapacity
|
||||||
|
|
||||||
wouldHaveWriteAccessTo [(AuthCapacity, capacity), (AuthCourseRegistered, False)] $ CourseR courseTerm courseSchool courseShorthand CRegisterR
|
wouldHaveWriteAccessTo [(AuthCapacity, capacity), (AuthCourseRegistered, False)] $ CourseR courseTerm courseSchool courseShorthand CRegisterR
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -9,11 +9,12 @@ module Handler.Course.User
|
|||||||
import Import
|
import Import
|
||||||
|
|
||||||
import Utils.Form
|
import Utils.Form
|
||||||
import Utils.Mail (pickValidUserEmail)
|
|
||||||
import Handler.Utils
|
import Handler.Utils
|
||||||
import Handler.Utils.SheetType
|
import Handler.Utils.SheetType
|
||||||
|
import Handler.Utils.Profile (pickValidEmail)
|
||||||
import Handler.Utils.StudyFeatures
|
import Handler.Utils.StudyFeatures
|
||||||
import Handler.Submission.List
|
import Handler.Submission.List
|
||||||
|
|
||||||
import Handler.Course.Register
|
import Handler.Course.Register
|
||||||
|
|
||||||
import Jobs.Queue
|
import Jobs.Queue
|
||||||
|
|||||||
@ -129,11 +129,11 @@ _userSheets = _dbrOutput . _7
|
|||||||
|
|
||||||
-- _userQualifications :: Traversal' UserTableData [Entity Qualification]
|
-- _userQualifications :: Traversal' UserTableData [Entity Qualification]
|
||||||
-- _userQualifications = _dbrOutput . _8 . (traverse _1)
|
-- _userQualifications = _dbrOutput . _8 . (traverse _1)
|
||||||
-- last part: ([Entity Qualification] -> f [Entity Qualification]) -> UserTableQualifications -> f UserTableQualifications
|
-- last part: ([Entity Qualification] -> f [Entity Qualification]) -> UserTableQualfications -> f UserTableQualifications
|
||||||
|
|
||||||
_userQualifications :: Getter UserTableData [Entity Qualification]
|
_userQualifications :: Getter UserTableData [Entity Qualification]
|
||||||
_userQualifications = _dbrOutput . _8 . to (fmap fst3)
|
_userQualifications = _dbrOutput . _8 . to (fmap fst3)
|
||||||
-- _userQualifications = _dbrOutput . _8 . each . _1 -- TODO: how to make this work
|
-- _userQualifications = _dbrOutput . _8 . each . _1 -- TODO: how to make this work
|
||||||
|
|
||||||
|
|
||||||
_userCourseQualifications :: Lens' UserTableData UserTableQualifications
|
_userCourseQualifications :: Lens' UserTableData UserTableQualifications
|
||||||
@ -194,7 +194,7 @@ colUserQualifications cutoff = sortable (Just "qualifications") (i18nCell MsgTab
|
|||||||
colUserQualificationBlocked :: forall m c. IsDBTable m c => Bool -> Day -> Colonnade Sortable UserTableData (DBCell m c)
|
colUserQualificationBlocked :: forall m c. IsDBTable m c => Bool -> Day -> Colonnade Sortable UserTableData (DBCell m c)
|
||||||
colUserQualificationBlocked isAdmin cutoff = sortable (Just "qualification-block") (i18nCell MsgQualificationValidIndicator & cellTooltip MsgTableQualificationBlockedTooltipSimple) $
|
colUserQualificationBlocked isAdmin cutoff = sortable (Just "qualification-block") (i18nCell MsgQualificationValidIndicator & cellTooltip MsgTableQualificationBlockedTooltipSimple) $
|
||||||
let qualNamedReasonCell (q,qu,qb) = textCell ((q ^. hasQualification . _qualificationShorthand . _CI) <> ": ") <> qualificationValidReasonCell' Nothing isAdmin cutoff qb qu
|
let qualNamedReasonCell (q,qu,qb) = textCell ((q ^. hasQualification . _qualificationShorthand . _CI) <> ": ") <> qualificationValidReasonCell' Nothing isAdmin cutoff qb qu
|
||||||
in \(view _userCourseQualifications -> qualis) ->
|
in \(view _userCourseQualifications -> qualis) ->
|
||||||
(cellAttrs <>~ [("class", "list--inline list--comma-separated list--iconless")]) . listCell qualis $ qualNamedReasonCell
|
(cellAttrs <>~ [("class", "list--inline list--comma-separated list--iconless")]) . listCell qualis $ qualNamedReasonCell
|
||||||
|
|
||||||
data UserTableCsv = UserTableCsv
|
data UserTableCsv = UserTableCsv
|
||||||
@ -204,7 +204,6 @@ data UserTableCsv = UserTableCsv
|
|||||||
, csvUserSex :: Maybe Sex
|
, csvUserSex :: Maybe Sex
|
||||||
, csvUserBirthday :: Maybe Day
|
, csvUserBirthday :: Maybe Day
|
||||||
, csvUserMatriculation :: Maybe UserMatriculation
|
, csvUserMatriculation :: Maybe UserMatriculation
|
||||||
, csvUserEPPN :: Maybe UserEduPersonPrincipalName
|
|
||||||
, csvUserEmail :: UserEmail
|
, csvUserEmail :: UserEmail
|
||||||
, csvUserQualifications :: [QualificationName]
|
, csvUserQualifications :: [QualificationName]
|
||||||
, csvUserSubmissionGroup :: Maybe SubmissionGroupName
|
, csvUserSubmissionGroup :: Maybe SubmissionGroupName
|
||||||
@ -224,7 +223,6 @@ instance Csv.ToNamedRecord UserTableCsv where
|
|||||||
, "sex" Csv..= csvUserSex
|
, "sex" Csv..= csvUserSex
|
||||||
, "birthday" Csv..= csvUserBirthday
|
, "birthday" Csv..= csvUserBirthday
|
||||||
, "matriculation" Csv..= csvUserMatriculation
|
, "matriculation" Csv..= csvUserMatriculation
|
||||||
, "eduPersonPrincipalName" Csv..= csvUserEPPN
|
|
||||||
, "email" Csv..= csvUserEmail
|
, "email" Csv..= csvUserEmail
|
||||||
, "qualifications" Csv..= CsvSemicolonList csvUserQualifications
|
, "qualifications" Csv..= CsvSemicolonList csvUserQualifications
|
||||||
, "submission-group" Csv..= csvUserSubmissionGroup
|
, "submission-group" Csv..= csvUserSubmissionGroup
|
||||||
@ -286,7 +284,6 @@ data UserTableJson = UserTableJson
|
|||||||
, jsonUserName :: UserDisplayName
|
, jsonUserName :: UserDisplayName
|
||||||
, jsonUserSex :: Maybe (Maybe Sex)
|
, jsonUserSex :: Maybe (Maybe Sex)
|
||||||
, jsonUserMatriculation :: Maybe UserMatriculation
|
, jsonUserMatriculation :: Maybe UserMatriculation
|
||||||
, jsonUserEPPN :: Maybe UserEduPersonPrincipalName
|
|
||||||
, jsonUserEmail :: UserEmail
|
, jsonUserEmail :: UserEmail
|
||||||
, jsonUserQualifications :: Set QualificationName
|
, jsonUserQualifications :: Set QualificationName
|
||||||
, jsonUserSubmissionGroup :: Maybe SubmissionGroupName
|
, jsonUserSubmissionGroup :: Maybe SubmissionGroupName
|
||||||
@ -323,7 +320,6 @@ instance ToJSON UserTableJson where
|
|||||||
, pure $ "name" JSON..= jsonUserName
|
, pure $ "name" JSON..= jsonUserName
|
||||||
, ("sex" JSON..=) <$> jsonUserSex
|
, ("sex" JSON..=) <$> jsonUserSex
|
||||||
, ("matriculation" JSON..=) <$> jsonUserMatriculation
|
, ("matriculation" JSON..=) <$> jsonUserMatriculation
|
||||||
, ("eduPersonPrincipalName" JSON..=) <$> jsonUserEPPN
|
|
||||||
, pure $ "email" JSON..= jsonUserEmail
|
, pure $ "email" JSON..= jsonUserEmail
|
||||||
, ("qualifications" JSON..=) <$> assertM' (not . onull) jsonUserQualifications
|
, ("qualifications" JSON..=) <$> assertM' (not . onull) jsonUserQualifications
|
||||||
, ("submission-group" JSON..=) <$> jsonUserSubmissionGroup
|
, ("submission-group" JSON..=) <$> jsonUserSubmissionGroup
|
||||||
@ -420,12 +416,12 @@ makeCourseUserTable cid acts restrict colChoices psValidator csvColumns = do
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
qualis <- E.select . E.from $ \(qualification `E.InnerJoin` qualificationUser `E.LeftOuterJoin` qualificationBlock) -> do
|
qualis <- E.select . E.from $ \(qualification `E.InnerJoin` qualificationUser `E.LeftOuterJoin` qualificationBlock) -> do
|
||||||
E.on $ qualificationUser E.^. QualificationUserId E.=?. qualificationBlock E.?. QualificationUserBlockQualificationUser
|
E.on $ qualificationUser E.^. QualificationUserId E.=?. qualificationBlock E.?. QualificationUserBlockQualificationUser
|
||||||
E.&&. qualificationBlock `isLatestBlockBefore` E.now_
|
E.&&. qualificationBlock `isLatestBlockBefore` E.now_
|
||||||
E.on $ qualificationUser E.^. QualificationUserQualification E.==. qualification E.^. QualificationId
|
E.on $ qualificationUser E.^. QualificationUserQualification E.==. qualification E.^. QualificationId
|
||||||
E.where_ $ qualificationUser E.^. QualificationUserUser E.==. E.val (entityKey user)
|
E.where_ $ qualificationUser E.^. QualificationUserUser E.==. E.val (entityKey user)
|
||||||
E.&&. qualification E.^. QualificationId `E.in_` E.valList cqids
|
E.&&. qualification E.^. QualificationId `E.in_` E.valList cqids
|
||||||
E.orderBy [E.asc $ qualification E.^. QualificationShorthand] -- we should sort by CourseQualificationSortOrder instead, but since we have not seen a course with multiple qualifications yet, we take a shortcut here
|
E.orderBy [E.asc $ qualification E.^. QualificationShorthand] -- we should sort by CourseQualificationSortOrder instead, but since we have not seen a course with multiple qualifications yet, we take a shortcut here
|
||||||
return (qualification, qualificationUser, qualificationBlock)
|
return (qualification, qualificationUser, qualificationBlock)
|
||||||
let
|
let
|
||||||
regGroups = setOf (folded . _entityVal . _tutorialRegGroup . _Just) tutorials
|
regGroups = setOf (folded . _entityVal . _tutorialRegGroup . _Just) tutorials
|
||||||
@ -566,7 +562,6 @@ makeCourseUserTable cid acts restrict colChoices psValidator csvColumns = do
|
|||||||
<*> view (hasUser . _userSex)
|
<*> view (hasUser . _userSex)
|
||||||
<*> view (hasUser . _userBirthday)
|
<*> view (hasUser . _userBirthday)
|
||||||
<*> view (hasUser . _userMatrikelnummer)
|
<*> view (hasUser . _userMatrikelnummer)
|
||||||
<*> view (hasUser . _userLdapPrimaryKey)
|
|
||||||
<*> view (hasUser . _userEmail)
|
<*> view (hasUser . _userEmail)
|
||||||
<*> (over traverse (qualificationName . entityVal) <$> view _userQualifications)
|
<*> (over traverse (qualificationName . entityVal) <$> view _userQualifications)
|
||||||
<*> preview (_userSubmissionGroup . _entityVal . _submissionGroupName)
|
<*> preview (_userSubmissionGroup . _entityVal . _submissionGroupName)
|
||||||
@ -598,7 +593,6 @@ makeCourseUserTable cid acts restrict colChoices psValidator csvColumns = do
|
|||||||
<*> view (hasUser . _userDisplayName)
|
<*> view (hasUser . _userDisplayName)
|
||||||
<*> views (hasUser . _userSex) (guardOn showSex)
|
<*> views (hasUser . _userSex) (guardOn showSex)
|
||||||
<*> view (hasUser . _userMatrikelnummer)
|
<*> view (hasUser . _userMatrikelnummer)
|
||||||
<*> view (hasUser . _userLdapPrimaryKey)
|
|
||||||
<*> view (hasUser . _userEmail)
|
<*> view (hasUser . _userEmail)
|
||||||
<*> view (_userQualifications . folded . to (Set.singleton . qualificationName . entityVal))
|
<*> view (_userQualifications . folded . to (Set.singleton . qualificationName . entityVal))
|
||||||
<*> preview (_userSubmissionGroup . _entityVal . _submissionGroupName)
|
<*> preview (_userSubmissionGroup . _entityVal . _submissionGroupName)
|
||||||
@ -739,7 +733,7 @@ postCUsersR tid ssh csh = do
|
|||||||
redirect $ CourseR tid ssh csh CUsersR
|
redirect $ CourseR tid ssh csh CUsersR
|
||||||
(CourseUserRegisterExamData{..}, selectedUsers) -> do
|
(CourseUserRegisterExamData{..}, selectedUsers) -> do
|
||||||
Sum nrReg <- fmap mconcat . runDB . forM (Set.toList selectedUsers) $ \uid -> maybeT (return mempty) $ do
|
Sum nrReg <- fmap mconcat . runDB . forM (Set.toList selectedUsers) $ \uid -> maybeT (return mempty) $ do
|
||||||
guardM . lift $ exists [ CourseParticipantUser ==. uid, CourseParticipantCourse ==. cid, CourseParticipantState ==. CourseParticipantActive ]
|
guardM . lift $ exists [ CourseParticipantUser ==. uid, CourseParticipantCourse ==. cid, CourseParticipantState ==. CourseParticipantActive ]
|
||||||
let (exam, mOccurrence) = registerExam
|
let (exam, mOccurrence) = registerExam
|
||||||
mExamReg <- lift $ insertUnique ExamRegistration
|
mExamReg <- lift $ insertUnique ExamRegistration
|
||||||
{ examRegistrationExam = exam
|
{ examRegistrationExam = exam
|
||||||
@ -763,7 +757,7 @@ postCUsersR tid ssh csh = do
|
|||||||
Just _ -> addMessageI Success $ MsgCourseUsersSubmissionGroupSetNew nrSet
|
Just _ -> addMessageI Success $ MsgCourseUsersSubmissionGroupSetNew nrSet
|
||||||
|
|
||||||
redirect $ CourseR tid ssh csh CUsersR
|
redirect $ CourseR tid ssh csh CUsersR
|
||||||
(CourseUserReRegisterData, selectedUsers) -> do
|
(CourseUserReRegisterData, selectedUsers) -> do
|
||||||
Sum nrSet <- runDB . flip foldMapM selectedUsers $ \uid -> maybeT (return mempty) $ do
|
Sum nrSet <- runDB . flip foldMapM selectedUsers $ \uid -> maybeT (return mempty) $ do
|
||||||
didUpdate <- lift $ updateWhereCount
|
didUpdate <- lift $ updateWhereCount
|
||||||
[ CourseParticipantUser ==. uid
|
[ CourseParticipantUser ==. uid
|
||||||
|
|||||||
@ -187,7 +187,6 @@ data ExamUserTableCsv = ExamUserTableCsv
|
|||||||
, csvEUserFirstName :: Maybe Text
|
, csvEUserFirstName :: Maybe Text
|
||||||
, csvEUserName :: Maybe Text
|
, csvEUserName :: Maybe Text
|
||||||
, csvEUserMatriculation :: Maybe Text
|
, csvEUserMatriculation :: Maybe Text
|
||||||
, csvEUserEPPN :: Maybe UserEduPersonPrincipalName
|
|
||||||
, csvEUserStudyFeatures :: UserTableStudyFeatures
|
, csvEUserStudyFeatures :: UserTableStudyFeatures
|
||||||
, csvEUserOccurrence :: Maybe (CI Text)
|
, csvEUserOccurrence :: Maybe (CI Text)
|
||||||
, csvEUserExercisePoints :: Maybe (Maybe Points)
|
, csvEUserExercisePoints :: Maybe (Maybe Points)
|
||||||
@ -208,7 +207,6 @@ instance ToNamedRecord ExamUserTableCsv where
|
|||||||
, "first-name" Csv..= csvEUserFirstName
|
, "first-name" Csv..= csvEUserFirstName
|
||||||
, "name" Csv..= csvEUserName
|
, "name" Csv..= csvEUserName
|
||||||
, "matriculation" Csv..= csvEUserMatriculation
|
, "matriculation" Csv..= csvEUserMatriculation
|
||||||
, "eduPersonPrincipalName" Csv..= csvEUserEPPN
|
|
||||||
, "study-features" Csv..= csvEUserStudyFeatures
|
, "study-features" Csv..= csvEUserStudyFeatures
|
||||||
, "occurrence" Csv..= csvEUserOccurrence
|
, "occurrence" Csv..= csvEUserOccurrence
|
||||||
] ++ catMaybes
|
] ++ catMaybes
|
||||||
@ -234,7 +232,6 @@ instance FromNamedRecord ExamUserTableCsv where
|
|||||||
<*> csv .:?? "first-name"
|
<*> csv .:?? "first-name"
|
||||||
<*> csv .:?? "name"
|
<*> csv .:?? "name"
|
||||||
<*> csv .:?? "matriculation"
|
<*> csv .:?? "matriculation"
|
||||||
<*> csv .:?? "eduPersonPrincipalName"
|
|
||||||
<*> pure mempty
|
<*> pure mempty
|
||||||
<*> csv .:?? "occurrence"
|
<*> csv .:?? "occurrence"
|
||||||
<*> fmap Just (csv .:?? "exercise-points")
|
<*> fmap Just (csv .:?? "exercise-points")
|
||||||
@ -277,7 +274,7 @@ examUserTableCsvHeader :: ( MonoFoldable mono
|
|||||||
=> SheetGradeSummary -> Bool -> mono -> Csv.Header
|
=> SheetGradeSummary -> Bool -> mono -> Csv.Header
|
||||||
examUserTableCsvHeader allBoni doBonus pNames = Csv.header $
|
examUserTableCsvHeader allBoni doBonus pNames = Csv.header $
|
||||||
[ "surname", "first-name", "name"
|
[ "surname", "first-name", "name"
|
||||||
, "matriculation", "eduPersonPrincipalName"
|
, "matriculation"
|
||||||
, "study-features"
|
, "study-features"
|
||||||
, "course-note"
|
, "course-note"
|
||||||
, "occurrence"
|
, "occurrence"
|
||||||
@ -615,7 +612,6 @@ postEUsersR tid ssh csh examn = do
|
|||||||
<*> view (resultUser . _entityVal . _userFirstName . to Just)
|
<*> view (resultUser . _entityVal . _userFirstName . to Just)
|
||||||
<*> view (resultUser . _entityVal . _userDisplayName . to Just)
|
<*> view (resultUser . _entityVal . _userDisplayName . to Just)
|
||||||
<*> view (resultUser . _entityVal . _userMatrikelnummer)
|
<*> view (resultUser . _entityVal . _userMatrikelnummer)
|
||||||
<*> view (resultUser . _entityVal . _userLdapPrimaryKey)
|
|
||||||
<*> view resultStudyFeatures
|
<*> view resultStudyFeatures
|
||||||
<*> preview (resultExamOccurrence . _entityVal . _examOccurrenceName)
|
<*> preview (resultExamOccurrence . _entityVal . _examOccurrenceName)
|
||||||
<*> fmap (bool (const Nothing) Just showPoints) (preview $ resultUser . _entityKey . to (examBonusAchieved ?? bonus) . _achievedPoints . _Wrapped)
|
<*> fmap (bool (const Nothing) Just showPoints) (preview $ resultUser . _entityKey . to (examBonusAchieved ?? bonus) . _achievedPoints . _Wrapped)
|
||||||
@ -939,7 +935,6 @@ postEUsersR tid ssh csh examn = do
|
|||||||
guessUser' ExamUserTableCsv{..} = do
|
guessUser' ExamUserTableCsv{..} = do
|
||||||
let criteria = PredDNF . maybe Set.empty Set.singleton . fromNullable . Set.fromList . fmap PLVariable $ catMaybes
|
let criteria = PredDNF . maybe Set.empty Set.singleton . fromNullable . Set.fromList . fmap PLVariable $ catMaybes
|
||||||
[ GuessUserMatrikelnummer <$> csvEUserMatriculation
|
[ GuessUserMatrikelnummer <$> csvEUserMatriculation
|
||||||
, GuessUserEduPersonPrincipalName <$> csvEUserEPPN
|
|
||||||
, GuessUserDisplayName <$> csvEUserName
|
, GuessUserDisplayName <$> csvEUserName
|
||||||
, GuessUserSurname <$> csvEUserSurname
|
, GuessUserSurname <$> csvEUserSurname
|
||||||
, GuessUserFirstName <$> csvEUserFirstName
|
, GuessUserFirstName <$> csvEUserFirstName
|
||||||
|
|||||||
@ -19,7 +19,6 @@ import Import
|
|||||||
|
|
||||||
-- import Jobs
|
-- import Jobs
|
||||||
import Handler.Utils
|
import Handler.Utils
|
||||||
import Handler.Utils.Company
|
|
||||||
import Handler.Utils.Communication
|
import Handler.Utils.Communication
|
||||||
import Handler.Utils.Avs (guessAvsUser)
|
import Handler.Utils.Avs (guessAvsUser)
|
||||||
|
|
||||||
@ -29,12 +28,11 @@ import qualified Data.Map as Map
|
|||||||
-- import qualified Data.Text as T
|
-- import qualified Data.Text as T
|
||||||
import qualified Data.CaseInsensitive as CI
|
import qualified Data.CaseInsensitive as CI
|
||||||
-- import qualified Data.Conduit.List as C
|
-- import qualified Data.Conduit.List as C
|
||||||
-- import Database.Persist.Sql (deleteWhereCount, updateWhereCount)
|
import Database.Persist.Sql (deleteWhereCount, updateWhereCount)
|
||||||
import Database.Persist.Postgresql
|
|
||||||
import Database.Esqueleto.Experimental ((:&)(..))
|
import Database.Esqueleto.Experimental ((:&)(..))
|
||||||
import qualified Database.Esqueleto.Experimental as E -- needs TypeApplications Lang-Pragma
|
import qualified Database.Esqueleto.Experimental as E -- needs TypeApplications Lang-Pragma
|
||||||
import qualified Database.Esqueleto.Legacy as EL (on) -- needed for legacy join expected by dbTable
|
import qualified Database.Esqueleto.Legacy as EL (on)
|
||||||
-- import qualified Database.Esqueleto.PostgreSQL as E
|
import qualified Database.Esqueleto.PostgreSQL as E
|
||||||
import qualified Database.Esqueleto.Utils as E
|
import qualified Database.Esqueleto.Utils as E
|
||||||
import Database.Esqueleto.Utils.TH
|
import Database.Esqueleto.Utils.TH
|
||||||
|
|
||||||
@ -57,7 +55,7 @@ postalEmailField = boolFieldCustom (SomeMessage MsgUtilPostal) (SomeMessage MsgU
|
|||||||
|
|
||||||
data FirmAction = FirmActNotify
|
data FirmAction = FirmActNotify
|
||||||
| FirmActResetSupervision
|
| FirmActResetSupervision
|
||||||
| FirmActAddSupervisors
|
| FirmActAddSupersvisors
|
||||||
| FirmActChangeContactFirm
|
| FirmActChangeContactFirm
|
||||||
| FirmActChangeContactUser
|
| FirmActChangeContactUser
|
||||||
deriving (Eq, Ord, Enum, Bounded, Read, Show, Generic)
|
deriving (Eq, Ord, Enum, Bounded, Read, Show, Generic)
|
||||||
@ -71,11 +69,10 @@ data FirmActionData = FirmActNotifyData
|
|||||||
{ firmActResetKeepOldSupers :: Maybe Bool
|
{ firmActResetKeepOldSupers :: Maybe Bool
|
||||||
, firmActResetMutualSupervision :: Maybe Bool
|
, firmActResetMutualSupervision :: Maybe Bool
|
||||||
}
|
}
|
||||||
| FirmActAddSupervisorsData
|
| FirmActAddSupersvisorsData
|
||||||
{ firmActAddSupervisorIds :: Set Text
|
{ firmActAddSupervisorIds :: Set Text
|
||||||
, firmActAddSupervisorReroute :: Bool
|
, firmActAddSupervisorReroute :: Bool
|
||||||
, firmActAddSupervisorPostal :: Maybe Bool
|
, firmActAddSupervisorPostal :: Maybe Bool
|
||||||
, firmActAddSupervisorReason :: Maybe Text
|
|
||||||
}
|
}
|
||||||
| FirmActChangeContactFirmData
|
| FirmActChangeContactFirmData
|
||||||
{ firmActCCFPostalAddr :: Maybe StoredMarkup
|
{ firmActCCFPostalAddr :: Maybe StoredMarkup
|
||||||
@ -84,7 +81,6 @@ data FirmActionData = FirmActNotifyData
|
|||||||
}
|
}
|
||||||
| FirmActChangeContactUserData
|
| FirmActChangeContactUserData
|
||||||
{ firmActCCUPostalAddr :: Maybe StoredMarkup
|
{ firmActCCUPostalAddr :: Maybe StoredMarkup
|
||||||
, firmActCCUUseCompanyPostal :: Maybe Bool
|
|
||||||
, firmActCCUPostalPref :: Maybe Bool
|
, firmActCCUPostalPref :: Maybe Bool
|
||||||
}
|
}
|
||||||
deriving (Eq, Ord, Read, Show, Generic)
|
deriving (Eq, Ord, Read, Show, Generic)
|
||||||
@ -94,31 +90,21 @@ firmActionMap mr isAdmin acts = mconcat (mkAct isAdmin <$> acts)
|
|||||||
where
|
where
|
||||||
mkAct True FirmActNotify = singletonMap FirmActNotify $ pure FirmActNotifyData
|
mkAct True FirmActNotify = singletonMap FirmActNotify $ pure FirmActNotifyData
|
||||||
mkAct _ FirmActResetSupervision = singletonMap FirmActResetSupervision $ FirmActResetSupervisionData
|
mkAct _ FirmActResetSupervision = singletonMap FirmActResetSupervision $ FirmActResetSupervisionData
|
||||||
<$> aopt boolField' (fslI MsgFirmActResetSuperKeep) (Just $ Just False)
|
<$> aopt (boolField . Just $ SomeMessage MsgBoolIrrelevant) (fslI MsgFirmActResetSuperKeep) (Just $ Just False)
|
||||||
<*> aopt checkBoxField (fslI MsgFirmActResetMutualSupervision) (Just $ Just True )
|
<*> aopt checkBoxField (fslI MsgFirmActResetMutualSupervision) (Just $ Just True )
|
||||||
mkAct _ FirmActAddSupervisors = singletonMap FirmActAddSupervisors $ FirmActAddSupervisorsData
|
mkAct _ FirmActAddSupersvisors = singletonMap FirmActAddSupersvisors $ FirmActAddSupersvisorsData
|
||||||
<$> areq (textField & cfAnySeparatedSet) (fslI MsgFirmSuperDefault & setTooltip MsgCourseParticipantsRegisterUsersFieldTip) Nothing
|
<$> areq (textField & cfAnySeparatedSet) (fslI MsgTableIsDefaultSupervisor & setTooltip MsgCourseParticipantsRegisterUsersFieldTip) Nothing
|
||||||
<*> areq checkBoxField (fslI MsgTableIsDefaultReroute) (Just True)
|
<*> areq checkBoxField (fslI MsgTableIsDefaultReroute) (Just True)
|
||||||
<*> aopt postalEmailField (fslI MsgFormFieldPostal & setTooltip MsgFormFieldPostalTip) Nothing
|
<*> aopt postalEmailField (fslI MsgFormFieldPostal & setTooltip MsgFormFieldPostalTip) Nothing
|
||||||
<*> aopt (textField & cfStrip & addDatalist ucdefSuperReasons)
|
|
||||||
(fslI MsgUserCompanyReason & setTooltip MsgUserCompanyReasonTooltip) Nothing
|
|
||||||
mkAct _ FirmActChangeContactFirm = singletonMap FirmActChangeContactFirm $ FirmActChangeContactFirmData
|
mkAct _ FirmActChangeContactFirm = singletonMap FirmActChangeContactFirm $ FirmActChangeContactFirmData
|
||||||
<$> aopt htmlField (fslI MsgPostAddress & setTooltip (SomeMessages [SomeMessage MsgPostAddressTip, SomeMessage MsgUtilEmptyNoChangeTip])) Nothing
|
<$> aopt htmlField (fslI MsgPostAddress & setTooltip (SomeMessages [SomeMessage MsgPostAddressTip, SomeMessage MsgUtilEmptyNoChangeTip])) Nothing
|
||||||
<*> aopt (emailField & cfStrip & cfCI) (fslI MsgUserDisplayEmail & setTooltip MsgUtilEmptyNoChangeTip) Nothing
|
<*> aopt (emailField & cfStrip & cfCI) (fslI MsgUserDisplayEmail & setTooltip MsgUtilEmptyNoChangeTip) Nothing
|
||||||
<*> aopt postalEmailField (fslI MsgFormFieldPostal & setTooltip MsgFirmDefaultPreferenceInfo) Nothing
|
<*> aopt postalEmailField (fslI MsgFormFieldPostal & setTooltip MsgFormFieldPostalTip) Nothing
|
||||||
<* aformMessage (Message Info (toHtml $ mr MsgFirmActChangeContactFirmInfo) (Just IconNotificationNonactive))
|
<* aformMessage (Message Info (toHtml $ mr MsgFirmActChangeContactFirmInfo) (Just IconNotificationNonactive))
|
||||||
mkAct _ FirmActChangeContactUser = singletonMap FirmActChangeContactUser $ FirmActChangeContactUserData
|
mkAct _ FirmActChangeContactUser = singletonMap FirmActChangeContactUser $ FirmActChangeContactUserData
|
||||||
<$> aopt htmlField (fslI MsgPostAddress & setTooltip (SomeMessages [SomeMessage MsgPostAddressTip, SomeMessage MsgUtilEmptyNoChangeTip])) Nothing
|
<$> aopt htmlField (fslI MsgPostAddress & setTooltip (SomeMessages [SomeMessage MsgPostAddressTip, SomeMessage MsgUtilEmptyNoChangeTip])) Nothing
|
||||||
<*> aopt boolField' (fslI MsgCompanyUserUseCompanyAddress & setTooltip MsgCompanyUserUseCompanyAddressTip) Nothing
|
|
||||||
<*> aopt postalEmailField (fslI MsgFormFieldPostal & setTooltip MsgFormFieldPostalTip) Nothing
|
<*> aopt postalEmailField (fslI MsgFormFieldPostal & setTooltip MsgFormFieldPostalTip) Nothing
|
||||||
mkAct _ _ = mempty
|
mkAct _ _ = mempty
|
||||||
ucdefSuperReasons :: HandlerFor UniWorX (OptionList Text)
|
|
||||||
ucdefSuperReasons = fmap (mkOptionList . map (\t -> Option t t t) . Set.toAscList) . runDB $
|
|
||||||
fmap (setOf $ folded . _Value . _Just) . E.select . E.distinct $ do
|
|
||||||
usrc <- E.from $ E.table @UserCompany
|
|
||||||
E.where_ $ E.isJust $ usrc E.^. UserCompanyReason
|
|
||||||
return $ usrc E.^. UserCompanyReason
|
|
||||||
|
|
||||||
|
|
||||||
firmActionForm :: _ -> Bool -> [FirmAction] -> AForm Handler FirmActionData
|
firmActionForm :: _ -> Bool -> [FirmAction] -> AForm Handler FirmActionData
|
||||||
firmActionForm mr isAdmin acts = multiActionA (firmActionMap mr isAdmin acts) (fslI MsgTableAction) Nothing
|
firmActionForm mr isAdmin acts = multiActionA (firmActionMap mr isAdmin acts) (fslI MsgTableAction) Nothing
|
||||||
@ -132,7 +118,7 @@ firmActionHandler route isAdmin = flip formResult faHandler
|
|||||||
faHandler (_,fids) | null fids = addMessageI Error MsgNoCompanySelected
|
faHandler (_,fids) | null fids = addMessageI Error MsgNoCompanySelected
|
||||||
|
|
||||||
faHandler (FirmActNotifyData, Set.toList -> fids) = do
|
faHandler (FirmActNotifyData, Set.toList -> fids) = do
|
||||||
usrs <- runDBRead $ E.select $ E.distinct $ do
|
usrs <- runDB $ E.select $ E.distinct $ do
|
||||||
(usr :& uc) <- E.from $ E.table @User `E.innerJoin` E.table @UserCompany `E.on` (\(emp :& uc) -> emp E.^. UserId E.==. uc E.^. UserCompanyUser)
|
(usr :& uc) <- E.from $ E.table @User `E.innerJoin` E.table @UserCompany `E.on` (\(emp :& uc) -> emp E.^. UserId E.==. uc E.^. UserCompanyUser)
|
||||||
E.where_ $ uc E.^. UserCompanyCompany `E.in_` E.valList fids
|
E.where_ $ uc E.^. UserCompanyCompany `E.in_` E.valList fids
|
||||||
return $ usr E.^. UserId
|
return $ usr E.^. UserId
|
||||||
@ -149,19 +135,17 @@ firmActionHandler route isAdmin = flip formResult faHandler
|
|||||||
delSupers <- if firmActResetKeepOldSupers == Just False
|
delSupers <- if firmActResetKeepOldSupers == Just False
|
||||||
then E.deleteCount $ do
|
then E.deleteCount $ do
|
||||||
spr <- E.from $ E.table @UserSupervisor
|
spr <- E.from $ E.table @UserSupervisor
|
||||||
E.where_ $ suprFltr spr
|
E.where_ $ suprFltr spr E.&&. E.exists (do
|
||||||
E.&&. spr E.^. UserSupervisorReason E.~=. E.val (tshow SupervisorReasonCompanyDefault)
|
usr <- E.from $ E.table @UserCompany
|
||||||
E.&&. E.exists (do
|
E.where_ $ usr E.^. UserCompanyCompany `E.in_` E.vals fids
|
||||||
usr <- E.from $ E.table @UserCompany
|
E.&&. usr E.^. UserCompanyUser E.==. spr E.^. UserSupervisorUser
|
||||||
E.where_ $ usr E.^. UserCompanyCompany `E.in_` E.vals fids
|
)
|
||||||
E.&&. usr E.^. UserCompanyUser E.==. spr E.^. UserSupervisorUser
|
|
||||||
)
|
|
||||||
else return 0
|
else return 0
|
||||||
newSupers <- addDefaultSupervisorsFor (Just $ tshow SupervisorReasonCompanyDefault) madId (firmActResetMutualSupervision /= Just False) fids
|
newSupers <- addDefaultSupervisorsFor madId (firmActResetMutualSupervision /= Just False) fids
|
||||||
addMessageI Success $ MsgFirmResetSupervision delSupers newSupers
|
addMessageI Success $ MsgFirmResetSupervision delSupers newSupers
|
||||||
reloadKeepGetParams route -- reload to reflect changes
|
reloadKeepGetParams route -- reload to reflect changes
|
||||||
|
|
||||||
faHandler (FirmActAddSupervisorsData{..}, Set.toList -> [cid]) = do
|
faHandler (FirmActAddSupersvisorsData{..}, Set.toList -> [cid]) = do
|
||||||
avsUsers :: Map Text (Maybe UserId) <- sequenceA $ Map.fromSet guessAvsUser firmActAddSupervisorIds
|
avsUsers :: Map Text (Maybe UserId) <- sequenceA $ Map.fromSet guessAvsUser firmActAddSupervisorIds
|
||||||
let (usersFound', usersNotFound) = partition (is _Just . view _2) $ Map.toList avsUsers
|
let (usersFound', usersNotFound) = partition (is _Just . view _2) $ Map.toList avsUsers
|
||||||
usersFound = mapMaybe snd usersFound'
|
usersFound = mapMaybe snd usersFound'
|
||||||
@ -177,9 +161,7 @@ firmActionHandler route isAdmin = flip formResult faHandler
|
|||||||
addMessageI Warning MsgFirmActAddSupersEmpty
|
addMessageI Warning MsgFirmActAddSupersEmpty
|
||||||
reloadKeepGetParams route
|
reloadKeepGetParams route
|
||||||
runDB $ do
|
runDB $ do
|
||||||
-- putMany [UserCompany uid cid True firmActAddSupervisorReroute 0 False | uid <- usersFound] -- putMany always overwrites existing records, which would destroy priority and useCompanyAddress here
|
putMany [UserCompany uid cid True firmActAddSupervisorReroute | uid <- usersFound]
|
||||||
-- upsertManyWhere [UserCompany uid cid True firmActAddSupervisorReroute 0 False | uid <- usersFound] [copyField UserCompanySupervisor, copyField UserCompanySupervisorReroute] [] [] -- overwrite Supervisor and SupervisorReroute, keep priority and useCompanyAddress
|
|
||||||
upsertManyWhere [UserCompany uid cid True firmActAddSupervisorReroute 0 False firmActAddSupervisorReason| uid <- usersFound] [] [UserCompanySupervisor =. True, UserCompanySupervisorReroute =. firmActAddSupervisorReroute, UserCompanyReason =. firmActAddSupervisorReason] [] -- identical to previous line, but perhaps more clear?
|
|
||||||
whenIsJust firmActAddSupervisorPostal $ \prefPostal ->
|
whenIsJust firmActAddSupervisorPostal $ \prefPostal ->
|
||||||
updateWhere [UserId <-. usersFound] [UserPrefersPostal =. prefPostal]
|
updateWhere [UserId <-. usersFound] [UserPrefersPostal =. prefPostal]
|
||||||
addMessageI Success $ MsgFirmActAddSupersSet (fromIntegral $ length usersFound) firmActAddSupervisorPostal
|
addMessageI Success $ MsgFirmActAddSupersSet (fromIntegral $ length usersFound) firmActAddSupervisorPostal
|
||||||
@ -192,34 +174,25 @@ firmActionHandler route isAdmin = flip formResult faHandler
|
|||||||
, (CompanyPrefersPostal =.) <$> firmActCCFPostalPref
|
, (CompanyPrefersPostal =.) <$> firmActCCFPostalPref
|
||||||
]
|
]
|
||||||
in unless (null changes) $ do
|
in unless (null changes) $ do
|
||||||
runDB $ update cid changes
|
runDB $ updateBy (UniqueCompanyShorthand $ unCompanyKey cid) changes
|
||||||
addMessageI Success MsgFirmActChangeContactFirmResult
|
addMessageI Success MsgFirmActChangeContactFirmResult
|
||||||
reloadKeepGetParams route
|
reloadKeepGetParams route
|
||||||
|
|
||||||
faHandler (FirmActChangeContactUserData{..}, Set.toList -> [cid])
|
faHandler (FirmActChangeContactUserData{..}, Set.toList -> [cid]) =
|
||||||
| firmActCCUUseCompanyPostal == Just True, isJust firmActCCUPostalAddr =
|
let changes = catMaybes
|
||||||
addMessageI Error MsgCompanyUserUseCompanyPostalError
|
[ (UserPostAddress E.=.) . E.justVal <$> canonical firmActCCUPostalAddr -- note that Nothing means no change and not delete address!
|
||||||
| otherwise = do
|
, (UserPrefersPostal E.=.) . E.val <$> firmActCCUPostalPref
|
||||||
let changes = catMaybes
|
]
|
||||||
[ toMaybe (firmActCCUUseCompanyPostal == Just True) (UserPostAddress E.=. E.nothing) -- precondition ensures that only one update applies for UserPostAddress
|
in unless (null changes) $ do
|
||||||
, (UserPostAddress E.=.) . E.justVal <$> canonical firmActCCUPostalAddr -- note that Nothing means no change and not delete address!
|
nrChanged <- runDB $ E.updateCount $ \usr -> do
|
||||||
, (UserPrefersPostal E.=.) . E.val <$> firmActCCUPostalPref
|
E.set usr changes
|
||||||
]
|
E.where_ $ E.exists $ do
|
||||||
(total, nrChanged) <- runDB $ do
|
usrCmpy <- E.from $ E.table @UserCompany
|
||||||
nrUsrChange <- E.updateCount $ \usr -> do
|
E.where_ $ usrCmpy E.^. UserCompanyCompany E.==. E.val cid
|
||||||
E.set usr changes
|
E.&&. usrCmpy E.^. UserCompanyUser E.==. usr E.^. UserId
|
||||||
E.where_ $ E.exists $ do
|
addMessageI Success $ MsgFirmUserChanges nrChanged
|
||||||
usrCmpy <- E.from $ E.table @UserCompany
|
reloadKeepGetParams route -- reload to reflect changes
|
||||||
E.where_ $ usrCmpy E.^. UserCompanyCompany E.==. E.val cid
|
|
||||||
E.&&. usrCmpy E.^. UserCompanyUser E.==. usr E.^. UserId
|
|
||||||
nrUseComp <- case firmActCCUUseCompanyPostal of
|
|
||||||
Just x -> updateWhereCount [UserCompanyCompany ==. cid] [UserCompanyUseCompanyAddress =. x]
|
|
||||||
Nothing -> return 0
|
|
||||||
nrCid <- count [UserCompanyCompany ==. cid]
|
|
||||||
return (fromIntegral nrCid, max nrUsrChange nrUseComp)
|
|
||||||
let allok = bool Warning Success $ nrChanged == total
|
|
||||||
addMessageI allok $ MsgFirmUserActChangeResult nrChanged total
|
|
||||||
reloadKeepGetParams route -- reload to reflect changes
|
|
||||||
faHandler _ = addMessageI Error MsgErrorUnknownFormAction
|
faHandler _ = addMessageI Error MsgErrorUnknownFormAction
|
||||||
|
|
||||||
|
|
||||||
@ -254,16 +227,80 @@ runFirmActionFormPost cid route isAdmin acts = do
|
|||||||
-- Firm specific utilities
|
-- Firm specific utilities
|
||||||
-- for filters and counts also see before FirmAllR Handlers
|
-- for filters and counts also see before FirmAllR Handlers
|
||||||
|
|
||||||
-- repeatedly useful queries
|
|
||||||
|
|
||||||
usrPrimaryCompanies :: E.SqlExpr (Entity Company) -> E.SqlExpr (Entity UserCompany) -> E.SqlQuery ()
|
|
||||||
-- usrPrimaryCompanies :: E.SqlExpr (E.Value CompanyId) -> E.SqlExpr (Entity UserCompany) -> E.SqlQuery (Entity UserCompany) -- possible alternative
|
-- remove supervisors:
|
||||||
usrPrimaryCompanies cmp usr = do
|
deleteSupervisors :: NonEmpty UserId -> DB Int64
|
||||||
othr <- E.from $ E.table @UserCompany
|
deleteSupervisors usrs = deleteWhereCount [UserSupervisorUser <-. toList usrs]
|
||||||
E.where_ $ othr E.^. UserCompanyPriority E.>. usr E.^. UserCompanyPriority
|
|
||||||
E.&&. othr E.^. UserCompanyUser E.==. usr E.^. UserCompanyUser
|
-- reset supervisors given employees of a company to default company supervision, deleting all other supervisors
|
||||||
E.&&. othr E.^. UserCompanyCompany E.!=. cmp E.^. CompanyId -- redundant due to > above, but likely performance improving
|
resetSupervisors :: CompanyId -> NonEmpty UserId -> DB Int64
|
||||||
-- return othr
|
resetSupervisors cid employees = do
|
||||||
|
nr_del <- deleteSupervisors employees
|
||||||
|
nr_add <- addDefaultSupervisors cid employees
|
||||||
|
return $ max nr_del nr_add
|
||||||
|
|
||||||
|
-- adds the default company supervisors as supervisor to a given set of users, which themselves may belong to any company
|
||||||
|
addDefaultSupervisors :: CompanyId -> NonEmpty UserId -> DB Int64
|
||||||
|
addDefaultSupervisors cid employees = do
|
||||||
|
E.insertSelectWithConflictCount UniqueUserSupervisor
|
||||||
|
(do
|
||||||
|
(spr :& usr) <- E.from $ E.table @UserCompany `E.crossJoin` E.toValues employees
|
||||||
|
E.where_ $ spr E.^. UserCompanyCompany E.==. E.val cid
|
||||||
|
E.&&. spr E.^. UserCompanySupervisor
|
||||||
|
E.distinct $ return $ UserSupervisor
|
||||||
|
E.<# (spr E.^. UserCompanyUser)
|
||||||
|
E.<&> usr
|
||||||
|
E.<&> (spr E.^. UserCompanySupervisorReroute)
|
||||||
|
)
|
||||||
|
(\_old new -> [UserSupervisorRerouteNotifications E.=. new E.^. UserSupervisorRerouteNotifications])
|
||||||
|
|
||||||
|
-- like `addDefaultSupervisors`, but selects all employees of given companies from database, optionally filtered by being under supervision of a given individual
|
||||||
|
addDefaultSupervisorsFor :: (CompanyId ~ Element mono, MonoFoldable mono) => Maybe UserId -> Bool -> mono -> DB Int64
|
||||||
|
addDefaultSupervisorsFor mbSuperId mutualSupervision cids = do
|
||||||
|
E.insertSelectWithConflictCount UniqueUserSupervisor
|
||||||
|
(do
|
||||||
|
(spr :& usr) <- E.from $ E.table @UserCompany `E.innerJoin` E.table @UserCompany `E.on` (\(spr :& usr) -> spr E.^. UserCompanyCompany E.==. usr E.^. UserCompanyCompany)
|
||||||
|
E.where_ $ E.and $ guardMonoid (not mutualSupervision)
|
||||||
|
[ E.not_ $ usr E.^. UserCompanySupervisor ]
|
||||||
|
<> maybeEmpty mbSuperId (\sprId -> [E.exists $ do
|
||||||
|
superv <- E.from $ E.table @UserSupervisor
|
||||||
|
E.where_ $ superv E.^. UserSupervisorSupervisor E.==. E.val sprId
|
||||||
|
E.&&. superv E.^. UserSupervisorUser E.==. usr E.^. UserCompanyUser
|
||||||
|
])
|
||||||
|
<> [ spr E.^. UserCompanySupervisor
|
||||||
|
, spr E.^. UserCompanyCompany `E.in_` E.vals cids
|
||||||
|
, usr E.^. UserCompanyCompany `E.in_` E.vals cids
|
||||||
|
]
|
||||||
|
E.distinct $ return $ UserSupervisor
|
||||||
|
E.<# (spr E.^. UserCompanyUser)
|
||||||
|
E.<&> (usr E.^. UserCompanyUser)
|
||||||
|
E.<&> (spr E.^. UserCompanySupervisorReroute)
|
||||||
|
)
|
||||||
|
(\_old new -> [UserSupervisorRerouteNotifications E.=. new E.^. UserSupervisorRerouteNotifications] )
|
||||||
|
|
||||||
|
-- like `addDefaultSupervisors`, but selects all employees of given companies from database
|
||||||
|
addDefaultSupervisorsAll :: (CompanyId ~ Element mono, MonoFoldable mono) => Bool -> mono -> DB Int64
|
||||||
|
addDefaultSupervisorsAll mutualSupervision cids = do
|
||||||
|
E.insertSelectWithConflictCount UniqueUserSupervisor
|
||||||
|
(do
|
||||||
|
(spr :& usr) <- E.from $ E.table @UserCompany `E.innerJoin` E.table @UserCompany `E.on` (\(spr :& usr) -> spr E.^. UserCompanyCompany E.==. usr E.^. UserCompanyCompany)
|
||||||
|
E.where_ $ E.and $ guardMonoid (not mutualSupervision)
|
||||||
|
[ E.not_ $ usr E.^. UserCompanySupervisor ]
|
||||||
|
<> [ spr E.^. UserCompanySupervisor
|
||||||
|
, spr E.^. UserCompanyCompany `E.in_` E.vals cids
|
||||||
|
, usr E.^. UserCompanyCompany `E.in_` E.vals cids
|
||||||
|
]
|
||||||
|
E.distinct $ return $ UserSupervisor
|
||||||
|
E.<# (spr E.^. UserCompanyUser)
|
||||||
|
E.<&> (usr E.^. UserCompanyUser)
|
||||||
|
E.<&> (spr E.^. UserCompanySupervisorReroute)
|
||||||
|
)
|
||||||
|
(\_old new -> [UserSupervisorRerouteNotifications E.=. new E.^. UserSupervisorRerouteNotifications] )
|
||||||
|
|
||||||
|
|
||||||
|
------------------------------
|
||||||
|
-- repeatedly useful queries
|
||||||
|
|
||||||
fromUserCompany :: Maybe (E.SqlExpr (Entity UserCompany) -> E.SqlExpr (E.Value Bool)) -> E.SqlExpr (Entity Company) -> E.SqlQuery ()
|
fromUserCompany :: Maybe (E.SqlExpr (Entity UserCompany) -> E.SqlExpr (E.Value Bool)) -> E.SqlExpr (Entity Company) -> E.SqlQuery ()
|
||||||
fromUserCompany mbFltr cmpy = do
|
fromUserCompany mbFltr cmpy = do
|
||||||
@ -271,18 +308,8 @@ fromUserCompany mbFltr cmpy = do
|
|||||||
let basecond = usrCmpy E.^. UserCompanyCompany E.==. cmpy E.^. CompanyId
|
let basecond = usrCmpy E.^. UserCompanyCompany E.==. cmpy E.^. CompanyId
|
||||||
E.where_ $ maybe basecond ((basecond E.&&.).($ usrCmpy)) mbFltr
|
E.where_ $ maybe basecond ((basecond E.&&.).($ usrCmpy)) mbFltr
|
||||||
|
|
||||||
firmCountUsers :: E.SqlExpr (Entity Company) -> E.SqlExpr (E.Value Word64)
|
firmCountUsers :: E.SqlExpr (Entity Company) -> E.SqlExpr (E.Value Word64)
|
||||||
firmCountUsers = E.subSelectCount . fromUserCompany Nothing
|
firmCountUsers = E.subSelectCount . fromUserCompany Nothing
|
||||||
|
|
||||||
firmCountUsersPrimary :: E.SqlExpr (Entity Company) -> E.SqlExpr (E.Value Word64)
|
|
||||||
firmCountUsersPrimary cmp = E.subSelectCount $ fromUserCompany (Just primFltr) cmp
|
|
||||||
where
|
|
||||||
primFltr = E.notExists . usrPrimaryCompanies cmp
|
|
||||||
|
|
||||||
firmCountUsersSecondary :: E.SqlExpr (Entity Company) -> E.SqlExpr (E.Value Word64)
|
|
||||||
firmCountUsersSecondary cmp = E.subSelectCount $ fromUserCompany (Just primFltr) cmp
|
|
||||||
where
|
|
||||||
primFltr = E.exists . usrPrimaryCompanies cmp
|
|
||||||
|
|
||||||
firmCountSupervisors :: E.SqlExpr (Entity Company) -> E.SqlExpr (E.Value Word64)
|
firmCountSupervisors :: E.SqlExpr (Entity Company) -> E.SqlExpr (E.Value Word64)
|
||||||
firmCountSupervisors = E.subSelectCount . fromUserCompany (Just (E.^. UserCompanySupervisor))
|
firmCountSupervisors = E.subSelectCount . fromUserCompany (Just (E.^. UserCompanySupervisor))
|
||||||
@ -395,7 +422,7 @@ type AllCompanyTableExpr = E.SqlExpr (Entity Company)
|
|||||||
queryAllCompany :: AllCompanyTableExpr -> E.SqlExpr (Entity Company)
|
queryAllCompany :: AllCompanyTableExpr -> E.SqlExpr (Entity Company)
|
||||||
queryAllCompany = id
|
queryAllCompany = id
|
||||||
|
|
||||||
type AllCompanyTableData = DBRow (Entity Company, E.Value Word64, E.Value Bool, E.Value Bool, E.Value Word64)
|
type AllCompanyTableData = DBRow (Entity Company, E.Value Word64, E.Value Bool, E.Value Bool)
|
||||||
resultAllCompanyEntity :: Lens' AllCompanyTableData (Entity Company)
|
resultAllCompanyEntity :: Lens' AllCompanyTableData (Entity Company)
|
||||||
resultAllCompanyEntity = _dbrOutput . _1
|
resultAllCompanyEntity = _dbrOutput . _1
|
||||||
|
|
||||||
@ -411,12 +438,10 @@ resultAllCompanySupervisors = _dbrOutput . _3 . _unValue
|
|||||||
resultAllCompanyDefaultReroutes :: Lens' AllCompanyTableData Bool
|
resultAllCompanyDefaultReroutes :: Lens' AllCompanyTableData Bool
|
||||||
resultAllCompanyDefaultReroutes = _dbrOutput . _4 . _unValue
|
resultAllCompanyDefaultReroutes = _dbrOutput . _4 . _unValue
|
||||||
|
|
||||||
resultAllCompanyUsersSecondary :: Lens' AllCompanyTableData Word64
|
|
||||||
resultAllCompanyUsersSecondary = _dbrOutput . _5 . _unValue
|
|
||||||
|
|
||||||
mkFirmAllTable :: Bool -> UserId -> DB (FormResult (FirmActionData, Set CompanyId), Widget)
|
mkFirmAllTable :: Bool -> UserId -> DB (FormResult (FirmActionData, Set CompanyId), Widget)
|
||||||
mkFirmAllTable isAdmin uid = do
|
mkFirmAllTable isAdmin uid = do
|
||||||
now <- liftIO getCurrentTime
|
-- now <- liftIO getCurrentTime
|
||||||
mr <- getMessageRender
|
mr <- getMessageRender
|
||||||
let
|
let
|
||||||
resultDBTable = DBTable{..}
|
resultDBTable = DBTable{..}
|
||||||
@ -435,13 +460,12 @@ mkFirmAllTable isAdmin uid = do
|
|||||||
, cmpy & firmCountUsers -- 2
|
, cmpy & firmCountUsers -- 2
|
||||||
, cmpy & firmHasSupervisors -- 3
|
, cmpy & firmHasSupervisors -- 3
|
||||||
, cmpy & firmHasDefaultReroutes -- 4
|
, cmpy & firmHasDefaultReroutes -- 4
|
||||||
, cmpy & firmCountUsersSecondary -- 5
|
-- , cmpy & firmCountEmployeeSupervised -- 4
|
||||||
-- , cmpy & firmCountEmployeeSupervised
|
-- , cmpy & firmCountEmployeeRerouted -- 5
|
||||||
-- , cmpy & firmCountEmployeeRerouted
|
-- , cmpy & firmCountEmployeeRerPost -- 6
|
||||||
-- , cmpy & firmCountEmployeeRerPost
|
-- , cmpy & firmCountForeignSupervisors -- 7
|
||||||
-- , cmpy & firmCountForeignSupervisors
|
-- , cmpy & firmCountActiveReroutes -- 9
|
||||||
-- , cmpy & firmCountActiveReroutes
|
-- , cmpy & firmCountActiveReroutes' -- 10
|
||||||
-- , cmpy & firmCountActiveReroutes'
|
|
||||||
)
|
)
|
||||||
dbtRowKey = (E.^. CompanyId)
|
dbtRowKey = (E.^. CompanyId)
|
||||||
dbtProj = dbtProjFilteredPostId
|
dbtProj = dbtProjFilteredPostId
|
||||||
@ -454,7 +478,6 @@ mkFirmAllTable isAdmin uid = do
|
|||||||
in anchorCell (FirmSupersR fsh) $ toWgt fsh
|
in anchorCell (FirmSupersR fsh) $ toWgt fsh
|
||||||
, sortable (Just "avsnr") (i18nCell MsgTableCompanyNo) $ \(view resultAllCompany -> firm) -> numCell $ companyAvsId firm
|
, sortable (Just "avsnr") (i18nCell MsgTableCompanyNo) $ \(view resultAllCompany -> firm) -> numCell $ companyAvsId firm
|
||||||
, sortable (Just "users") (i18nCell MsgTableCompanyNrUsers) $ \(view resultAllCompanyUsers -> nr) -> wgtCell $ word2widget nr
|
, sortable (Just "users") (i18nCell MsgTableCompanyNrUsers) $ \(view resultAllCompanyUsers -> nr) -> wgtCell $ word2widget nr
|
||||||
, sortable (Just "secondary") (i18nCell MsgTableCompanyNrSecondaryUsers) $ \(view resultAllCompanyUsersSecondary -> nr) -> wgtCell $ word2widget nr
|
|
||||||
, sortable (Just "supervisors") (i18nCell MsgTableCompanyNrSupersDefault) $ \row ->
|
, sortable (Just "supervisors") (i18nCell MsgTableCompanyNrSupersDefault) $ \row ->
|
||||||
anchorCell (FirmSupersR $ row ^. resultAllCompany . _companyShorthand) $ toWgt $ hasTickmark $ row ^. resultAllCompanySupervisors
|
anchorCell (FirmSupersR $ row ^. resultAllCompany . _companyShorthand) $ toWgt $ hasTickmark $ row ^. resultAllCompanySupervisors
|
||||||
, sortable (Just "reroute-def") (i18nCell MsgTableCompanyNrRerouteDefault) $ \(view resultAllCompanyDefaultReroutes -> ok) -> tickmarkCell ok
|
, sortable (Just "reroute-def") (i18nCell MsgTableCompanyNrRerouteDefault) $ \(view resultAllCompanyDefaultReroutes -> ok) -> tickmarkCell ok
|
||||||
@ -472,7 +495,6 @@ mkFirmAllTable isAdmin uid = do
|
|||||||
, singletonMap "avsnr" $ SortColumn (E.^. CompanyAvsId)
|
, singletonMap "avsnr" $ SortColumn (E.^. CompanyAvsId)
|
||||||
, singletonMap "postal-pref" $ SortColumn (E.^. CompanyPrefersPostal)
|
, singletonMap "postal-pref" $ SortColumn (E.^. CompanyPrefersPostal)
|
||||||
, singletonMap "users" $ SortColumn firmCountUsers
|
, singletonMap "users" $ SortColumn firmCountUsers
|
||||||
, singletonMap "secondary" $ SortColumn firmCountUsersSecondary
|
|
||||||
, singletonMap "supervisors" $ SortColumn firmHasSupervisors
|
, singletonMap "supervisors" $ SortColumn firmHasSupervisors
|
||||||
-- , singletonMap "emp-supervised" $ SortColumn firmCountEmployeeSupervised
|
-- , singletonMap "emp-supervised" $ SortColumn firmCountEmployeeSupervised
|
||||||
-- , singletonMap "emp-rerouted" $ SortColumn firmCountEmployeeRerouted
|
-- , singletonMap "emp-rerouted" $ SortColumn firmCountEmployeeRerouted
|
||||||
@ -553,7 +575,7 @@ mkFirmAllTable isAdmin uid = do
|
|||||||
-- ))
|
-- ))
|
||||||
-- )
|
-- )
|
||||||
-- )
|
-- )
|
||||||
-- , single ("is-supervisor", FilterColumn $ \row (getLast -> criterion) ->
|
-- , single ("is-supervisor", FilterColumn $ \row (getLast -> criterion) ->
|
||||||
-- case criterion of
|
-- case criterion of
|
||||||
-- Nothing -> E.true
|
-- Nothing -> E.true
|
||||||
-- (Just (crit::Text)) -> E.exists $ do
|
-- (Just (crit::Text)) -> E.exists $ do
|
||||||
@ -573,11 +595,11 @@ mkFirmAllTable isAdmin uid = do
|
|||||||
-- ))
|
-- ))
|
||||||
-- )
|
-- )
|
||||||
-- )
|
-- )
|
||||||
, single ("is-supervisor", mkFilterProjectedPost $ \(getLast -> criterion) dbr ->
|
, single ("is-supervisor", mkFilterProjectedPost $ \(getLast -> criterion) dbr ->
|
||||||
case criterion of
|
case criterion of
|
||||||
Nothing -> return True :: DB Bool
|
Nothing -> return True :: DB Bool
|
||||||
(Just (crit::Text)) -> do
|
(Just (crit::Text)) -> do
|
||||||
critFirms <- memcachedBy (Just . Right $ 3 * diffMinute) ("SVR:"<>crit) $ fmap (Set.fromList . fmap E.unValue) $ E.select $ E.distinct $ do
|
critFirms <- memcachedBy (Just . Right $ 5 * diffMinute) ("svr:"<>crit) $ fmap (Set.fromAscList . fmap E.unValue) $ E.select $ E.distinct $ do
|
||||||
(usr :& cmp) <- E.from $ E.table @User `E.innerJoin` E.table @Company
|
(usr :& cmp) <- E.from $ E.table @User `E.innerJoin` E.table @Company
|
||||||
`E.on` (\(usr :& cmp) -> E.exists (do
|
`E.on` (\(usr :& cmp) -> E.exists (do
|
||||||
usrCmp <- E.from $ E.table @UserCompany
|
usrCmp <- E.from $ E.table @UserCompany
|
||||||
@ -590,13 +612,13 @@ mkFirmAllTable isAdmin uid = do
|
|||||||
E.&&. E.exists (do
|
E.&&. E.exists (do
|
||||||
usrSub <- E.from $ E.table @UserCompany
|
usrSub <- E.from $ E.table @UserCompany
|
||||||
E.where_ $ usrSub E.^. UserCompanyUser E.==. usrSpr E.^. UserSupervisorUser
|
E.where_ $ usrSub E.^. UserCompanyUser E.==. usrSpr E.^. UserSupervisorUser
|
||||||
E.&&. usrSub E.^. UserCompanyCompany E.==. cmp E.^. CompanyId
|
E.&&. usrSub E.^. UserCompanyCompany E.==. cmp E.^. CompanyId
|
||||||
)
|
)
|
||||||
))
|
))
|
||||||
E.where_ $ (usr E.^. UserDisplayName `E.hasInfix` E.val crit )
|
E.where_ $ (usr E.^. UserDisplayName `E.hasInfix` E.val crit)
|
||||||
E.||. (usr E.^. UserDisplayEmail `E.hasInfix` E.val (CI.mk crit))
|
E.||. (usr E.^. UserDisplayEmail `E.hasInfix` E.val (CI.mk crit))
|
||||||
E.||. (usr E.^. UserSurname `E.hasInfix` E.val crit )
|
E.||. (usr E.^. UserSurname `E.hasInfix` E.val crit)
|
||||||
-- E.orderBy [E.asc $ cmp E.^. CompanyId]
|
E.orderBy [E.asc $ cmp E.^. CompanyId]
|
||||||
return $ cmp E.^. CompanyId
|
return $ cmp E.^. CompanyId
|
||||||
let cid = dbr ^. resultAllCompanyEntity . _entityKey
|
let cid = dbr ^. resultAllCompanyEntity . _entityKey
|
||||||
return $ Set.member cid critFirms
|
return $ Set.member cid critFirms
|
||||||
@ -656,18 +678,6 @@ mkFirmAllTable isAdmin uid = do
|
|||||||
Just False -> E.notExists checkSuper
|
Just False -> E.notExists checkSuper
|
||||||
)
|
)
|
||||||
, single ("company-postal", FilterColumn $ E.mkExactFilterLast $ views (to queryAllCompany) (E.isJust . (E.^. CompanyPostAddress)))
|
, single ("company-postal", FilterColumn $ E.mkExactFilterLast $ views (to queryAllCompany) (E.isJust . (E.^. CompanyPostAddress)))
|
||||||
, single ("qualification" , FilterColumn . E.mkExistsFilter $ \row (CI.mk -> criterion :: CI Text) -> do
|
|
||||||
(usrCmp :& usrQual :& qual) <- E.from $ E.table @UserCompany
|
|
||||||
`E.innerJoin` E.table @QualificationUser
|
|
||||||
`E.on` (\(usrCmp :& usrQual) -> usrCmp E.^. UserCompanyUser E.==. usrQual E.^. QualificationUserUser)
|
|
||||||
`E.innerJoin` E.table @Qualification
|
|
||||||
`E.on` (\(_ :& usrQual :& qual) -> qual E.^. QualificationId E.==. usrQual E.^. QualificationUserQualification)
|
|
||||||
E.where_ $ usrCmp E.^. UserCompanyCompany E.==. queryAllCompany row E.^. CompanyId
|
|
||||||
E.&&. qual E.^. QualificationShorthand E.==. E.val criterion
|
|
||||||
E.&&. validQualification now usrQual
|
|
||||||
)
|
|
||||||
, single ("company-address", FilterColumn $ E.mkContainsFilterWithCommaPlus id $ views (to queryAllCompany) ((E.->>. "markup-input").(E.^. CompanyPostAddress))
|
|
||||||
)
|
|
||||||
]
|
]
|
||||||
dbtFilterUI mPrev = mconcat
|
dbtFilterUI mPrev = mconcat
|
||||||
[ fltrCompanyNameUI mPrev
|
[ fltrCompanyNameUI mPrev
|
||||||
@ -677,9 +687,7 @@ mkFirmAllTable isAdmin uid = do
|
|||||||
, prismAForm (singletonFilter "is-supervisor") mPrev $ aopt textField (fslI MsgTableSupervisor)
|
, prismAForm (singletonFilter "is-supervisor") mPrev $ aopt textField (fslI MsgTableSupervisor)
|
||||||
, prismAForm (singletonFilter "is-default-supervisor") mPrev $ aopt textField (fslI MsgFirmSuperDefault)
|
, prismAForm (singletonFilter "is-default-supervisor") mPrev $ aopt textField (fslI MsgFirmSuperDefault)
|
||||||
, prismAForm (singletonFilter "foreign-supervisor" . maybePrism _PathPiece) mPrev $ aopt (boolField . Just $ SomeMessage MsgBoolIrrelevant) (fslI MsgFilterForeignSupervisor)
|
, prismAForm (singletonFilter "foreign-supervisor" . maybePrism _PathPiece) mPrev $ aopt (boolField . Just $ SomeMessage MsgBoolIrrelevant) (fslI MsgFilterForeignSupervisor)
|
||||||
, prismAForm (singletonFilter "company-postal" . maybePrism _PathPiece) mPrev $ aopt (boolField . Just $ SomeMessage MsgBoolIrrelevant) (fslI MsgFilterFirmExtern & setTooltip MsgFilterFirmExternTooltip)
|
, prismAForm (singletonFilter "company-postal" . maybePrism _PathPiece) mPrev $ aopt (boolField . Just $ SomeMessage MsgBoolIrrelevant) (fslI MsgFilterFirmExtern)
|
||||||
, prismAForm (singletonFilter "company-address") mPrev $ aopt textField (fslI MsgFirmAddress)
|
|
||||||
, fltrQualificationHdrUI MsgFilterHasQualification mPrev
|
|
||||||
]
|
]
|
||||||
dbtStyle = def { dbsFilterLayout = defaultDBSFilterLayout }
|
dbtStyle = def { dbsFilterLayout = defaultDBSFilterLayout }
|
||||||
dbtParams = DBParamsForm
|
dbtParams = DBParamsForm
|
||||||
@ -731,9 +739,7 @@ data FirmUserAction = FirmUserActNotify
|
|||||||
| FirmUserActResetSupervision
|
| FirmUserActResetSupervision
|
||||||
| FirmUserActSetSupervisor
|
| FirmUserActSetSupervisor
|
||||||
| FirmUserActMkSuper
|
| FirmUserActMkSuper
|
||||||
| FirmUserActChangeDetails
|
|
||||||
| FirmUserActChangeContact
|
| FirmUserActChangeContact
|
||||||
| FirmUserActRemove
|
|
||||||
deriving (Eq, Ord, Enum, Bounded, Read, Show, Generic)
|
deriving (Eq, Ord, Enum, Bounded, Read, Show, Generic)
|
||||||
deriving anyclass (Universe, Finite)
|
deriving anyclass (Universe, Finite)
|
||||||
|
|
||||||
@ -742,28 +748,20 @@ embedRenderMessage ''UniWorX ''FirmUserAction id
|
|||||||
|
|
||||||
data FirmUserActionData = FirmUserActNotifyData
|
data FirmUserActionData = FirmUserActNotifyData
|
||||||
| FirmUserActResetSupervisionData
|
| FirmUserActResetSupervisionData
|
||||||
{ firmUserActResetSupers :: Maybe Bool
|
{ firmUserActResetKeepOldSupers :: Maybe Bool
|
||||||
|
-- , firmUserActResetMutualSupervision :: Maybe Bool
|
||||||
}
|
}
|
||||||
| FirmUserActSetSupervisorData
|
| FirmUserActSetSupervisorData
|
||||||
{ firmUserActSetSuperNames :: Maybe (Set Text)
|
{ firmUserActSetSuperNames :: Maybe (Set Text)
|
||||||
, firmUserActSetSuperIds :: Maybe [UserId]
|
, firmUserActSetSuperIds :: Maybe [UserId]
|
||||||
, firmUserActSetSuperReason :: Maybe Text
|
, firmUserActSetSuperReroute :: Bool
|
||||||
, firmUserActSetSuperReroute :: Bool
|
, firmUserActSetSuperKeep :: Bool
|
||||||
, firmUserActResetSupers :: Maybe Bool
|
|
||||||
}
|
}
|
||||||
| FirmUserActMkSuperData
|
| FirmUserActMkSuperData
|
||||||
{ firmUserActMkSuperReroute :: Maybe Bool }
|
{ firmUserActMkSuperReroute :: Maybe Bool }
|
||||||
| FirmUserActChangeDetailsData
|
|
||||||
{ firmUserActDetailPriority :: Maybe Int
|
|
||||||
, firmUserActDetailReason :: Maybe Text
|
|
||||||
}
|
|
||||||
| FirmUserActChangeContactData
|
| FirmUserActChangeContactData
|
||||||
{ firmUserActPostalAddr :: Maybe StoredMarkup
|
{ firmUserActPostalAddr :: Maybe StoredMarkup
|
||||||
, firmUserActUseCompanyPostal :: Maybe Bool
|
, firmUserActPostalPref :: Maybe Bool
|
||||||
, firmUserActPostalPref :: Maybe Bool
|
|
||||||
}
|
|
||||||
| FirmUserActRemoveData
|
|
||||||
{ firmUserActRemoveSupers :: Bool
|
|
||||||
}
|
}
|
||||||
deriving (Eq, Ord, Show, Generic)
|
deriving (Eq, Ord, Show, Generic)
|
||||||
|
|
||||||
@ -775,7 +773,7 @@ queryUserUser = $(sqlIJproj 2 1)
|
|||||||
queryUserUserCompany :: UserCompanyTableExpr -> E.SqlExpr (Entity UserCompany)
|
queryUserUserCompany :: UserCompanyTableExpr -> E.SqlExpr (Entity UserCompany)
|
||||||
queryUserUserCompany = $(sqlIJproj 2 2)
|
queryUserUserCompany = $(sqlIJproj 2 2)
|
||||||
|
|
||||||
type UserCompanyTableData = DBRow (Entity User, Entity UserCompany, E.Value Word64, E.Value Word64, E.Value Bool)
|
type UserCompanyTableData = DBRow (Entity User, Entity UserCompany, E.Value Word64, E.Value Word64)
|
||||||
|
|
||||||
resultUserUser :: Lens' UserCompanyTableData (Entity User)
|
resultUserUser :: Lens' UserCompanyTableData (Entity User)
|
||||||
resultUserUser = _dbrOutput . _1
|
resultUserUser = _dbrOutput . _1
|
||||||
@ -789,9 +787,6 @@ resultUserCompanySupervisors = _dbrOutput . _3 . _unValue
|
|||||||
resultUserCompanyReroutes :: Lens' UserCompanyTableData Word64
|
resultUserCompanyReroutes :: Lens' UserCompanyTableData Word64
|
||||||
resultUserCompanyReroutes = _dbrOutput . _4 . _unValue
|
resultUserCompanyReroutes = _dbrOutput . _4 . _unValue
|
||||||
|
|
||||||
resultUserCompanyPrimary :: Lens' UserCompanyTableData Bool
|
|
||||||
resultUserCompanyPrimary = _dbrOutput . _5 . _unValue
|
|
||||||
|
|
||||||
instance HasEntity UserCompanyTableData User where
|
instance HasEntity UserCompanyTableData User where
|
||||||
hasEntity = resultUserUser
|
hasEntity = resultUserUser
|
||||||
|
|
||||||
@ -803,44 +798,37 @@ mkFirmUserTable :: Bool -> CompanyId -> DB (FormResult (FirmUserActionData, Set
|
|||||||
mkFirmUserTable isAdmin cid = do
|
mkFirmUserTable isAdmin cid = do
|
||||||
mr <- getMessageRender
|
mr <- getMessageRender
|
||||||
let
|
let
|
||||||
reasonSuperior = Just $ tshow SupervisorReasonAvsSuperior
|
mkSprOption (E.Value uid, E.Value udn, E.Value mbSpr) = do
|
||||||
mkSprOption (E.Value uid, E.Value udn, E.Value mbSpr, E.Value mbmbReason) = do
|
|
||||||
uuid <- toPathPiece <$> encryptUser uid
|
uuid <- toPathPiece <$> encryptUser uid
|
||||||
return (Option{ optionDisplay = udn, optionInternalValue = uid, optionExternalValue = uuid }, mbSpr, mbmbReason == Just reasonSuperior)
|
return (Option{ optionDisplay = udn, optionInternalValue = uid, optionExternalValue = uuid }, mbSpr)
|
||||||
|
|
||||||
procOptions rawSupers = do
|
procOptions rawSupers = do
|
||||||
procSupers <- traverse mkSprOption rawSupers
|
procSupers <- traverse mkSprOption rawSupers
|
||||||
return $ mkOptionListGrouped $ filter (notNull . snd)
|
return $ mkOptionListGrouped $ filter (notNull . snd)
|
||||||
[ (mr MsgTableSuperior , [opt | (opt, _ , True ) <- procSupers])
|
[ (mr MsgFirmSuperDefault , [opt | (opt, Just True ) <- procSupers])
|
||||||
, (mr MsgFirmSuperDefault , [opt | (opt, Just True , False) <- procSupers])
|
, (mr MsgFirmSuperIrregular, [opt | (opt, Just False) <- procSupers])
|
||||||
, (mr MsgFirmSuperIrregular, [opt | (opt, Just False, False) <- procSupers])
|
, (mr MsgFirmSuperForeign , [opt | (opt, Nothing ) <- procSupers])
|
||||||
, (mr MsgFirmSuperForeign , [opt | (opt, Nothing , False) <- procSupers])
|
|
||||||
]
|
]
|
||||||
|
|
||||||
rawSupers <- E.select $ do
|
rawSupers <- E.select $ do
|
||||||
(usr :& usrCmp) <- E.from $ E.table @User `E.leftJoin` E.table @UserCompany
|
(usr :& usrCmp) <- E.from $ E.table @User `E.leftJoin` E.table @UserCompany
|
||||||
`E.on` (\(usr :& usrCmp) -> usr E.^. UserId E.=?. usrCmp E.?. UserCompanyUser E.&&. usrCmp E.?. UserCompanyCompany E.==. E.justVal cid)
|
`E.on` (\(usr :& usrCmp) -> usr E.^. UserId E.=?. usrCmp E.?. UserCompanyUser E.&&. usrCmp E.?. UserCompanyCompany E.==. E.justVal cid)
|
||||||
E.where_ $ E.isTrue (usrCmp E.?. UserCompanySupervisor)
|
E.where_ $ E.isTrue (usrCmp E.?. UserCompanySupervisor)
|
||||||
E.||. (usrCmp E.?. UserCompanyReason E.?=. E.val reasonSuperior)
|
|
||||||
E.||. E.exists (firmQuerySupervisedBy cid Nothing usr)
|
E.||. E.exists (firmQuerySupervisedBy cid Nothing usr)
|
||||||
return (usr E.^. UserId, usr E.^. UserDisplayName, usrCmp E.?. UserCompanySupervisor, usrCmp E.?. UserCompanyReason)
|
return (usr E.^. UserId, usr E.^. UserDisplayName, usrCmp E.?. UserCompanySupervisor)
|
||||||
let
|
let
|
||||||
-- supervisorField :: Field Handler UserId
|
-- supervisorField :: Field Handler UserId
|
||||||
-- supervisorField = selectField' (Just $ SomeMessage MsgMultiNoSelection) $ procOptions rawSupers
|
-- supervisorField = selectField' (Just $ SomeMessage MsgMultiNoSelection) $ procOptions rawSupers
|
||||||
supervisorsField = multiSelectField' (Just $ SomeMessage MsgMultiNoSelection) $ procOptions rawSupers
|
supervisorsField = multiSelectField' (Just $ SomeMessage MsgMultiNoSelection) $ procOptions rawSupers
|
||||||
|
|
||||||
|
|
||||||
fsh = unCompanyKey cid
|
fsh = unCompanyKey cid
|
||||||
resultDBTable = DBTable{..}
|
resultDBTable = DBTable{..}
|
||||||
where
|
where
|
||||||
dbtSQLQuery = \(usr `E.InnerJoin` usrCmp) -> do
|
dbtSQLQuery = \(usr `E.InnerJoin` usrCmp) -> do
|
||||||
EL.on $ usr E.^. UserId E.==. usrCmp E.^. UserCompanyUser
|
EL.on $ usr E.^. UserId E.==. usrCmp E.^. UserCompanyUser
|
||||||
E.where_ $ usrCmp E.^. UserCompanyCompany E.==. E.val cid
|
E.where_ $ usrCmp E.^. UserCompanyCompany E.==. E.val cid
|
||||||
let isPrimary = E.notExists (do
|
return (usr, usrCmp, firmCountUserSupervisors usrCmp, firmCountUserSupervisorsReroute usrCmp)
|
||||||
other <- E.from $ E.table @UserCompany
|
|
||||||
E.where_ $ other E.^. UserCompanyUser E.==. usrCmp E.^. UserCompanyUser
|
|
||||||
E.&&. other E.^. UserCompanyPriority E.>. usrCmp E.^. UserCompanyPriority
|
|
||||||
)
|
|
||||||
return (usr, usrCmp, firmCountUserSupervisors usrCmp, firmCountUserSupervisorsReroute usrCmp, isPrimary)
|
|
||||||
dbtRowKey = queryUserUser >>> (E.^. UserId)
|
dbtRowKey = queryUserUser >>> (E.^. UserId)
|
||||||
dbtProj = dbtProjId
|
dbtProj = dbtProjId
|
||||||
dbtColonnade = formColonnade $ mconcat
|
dbtColonnade = formColonnade $ mconcat
|
||||||
@ -851,16 +839,7 @@ mkFirmUserTable isAdmin cid = do
|
|||||||
, sortable (Just "supervisors") (i18nCell MsgTableCompanyNrSupers ) $ \(view resultUserCompanySupervisors -> nr) -> wgtCell $ word2widget nr
|
, sortable (Just "supervisors") (i18nCell MsgTableCompanyNrSupers ) $ \(view resultUserCompanySupervisors -> nr) -> wgtCell $ word2widget nr
|
||||||
, sortable (Just "reroutes") (i18nCell MsgTableCompanyNrRerouteActive) $ \(view resultUserCompanyReroutes -> nr) -> wgtCell $ word2widget nr
|
, sortable (Just "reroutes") (i18nCell MsgTableCompanyNrRerouteActive) $ \(view resultUserCompanyReroutes -> nr) -> wgtCell $ word2widget nr
|
||||||
, sortable (Just "postal-pref") (i18nCell MsgPrefersPostal) $ \(view $ resultUserUser . _userPrefersPostal -> b) -> iconFixedCell $ iconLetterOrEmail b
|
, sortable (Just "postal-pref") (i18nCell MsgPrefersPostal) $ \(view $ resultUserUser . _userPrefersPostal -> b) -> iconFixedCell $ iconLetterOrEmail b
|
||||||
, sortable Nothing (i18nCell MsgCompanyUserUseCompanyAddress) $ \row ->
|
|
||||||
let noUsrAddr = isNothing $ row ^. resultUserUser . _userPostAddress
|
|
||||||
useCompA = row ^. resultUserUserCompany . _entityVal . _userCompanyUseCompanyAddress
|
|
||||||
in tickmarkCell $ noUsrAddr && useCompA
|
|
||||||
, colUserEmail
|
, colUserEmail
|
||||||
, sortable (Just "usr-reason") (i18nCell MsgTableCompanyReason) $ \(view $ resultUserUserCompany . _entityVal . _userCompanyReason -> r) -> cellMaybe textCell r
|
|
||||||
, sortable (Just "priority") (i18nCell MsgCompanyUserPriority) $ \row ->
|
|
||||||
let prio :: Int = row ^. resultUserUserCompany . _entityVal . _userCompanyPriority
|
|
||||||
isPrime = row ^. resultUserCompanyPrimary
|
|
||||||
in numCell prio <> spacerCell <> ifIconCell isPrime IconTop
|
|
||||||
, sortable Nothing (i18nCell MsgTableUserEdit) $ \(view resultUserUser -> entUsr) -> cellEditUserModal entUsr
|
, sortable Nothing (i18nCell MsgTableUserEdit) $ \(view resultUserUser -> entUsr) -> cellEditUserModal entUsr
|
||||||
]
|
]
|
||||||
dbtSorting = mconcat
|
dbtSorting = mconcat
|
||||||
@ -871,8 +850,6 @@ mkFirmUserTable isAdmin cid = do
|
|||||||
, singletonMap "personal-number" $ SortColumn $ queryUserUser >>> (E.^. UserCompanyPersonalNumber)
|
, singletonMap "personal-number" $ SortColumn $ queryUserUser >>> (E.^. UserCompanyPersonalNumber)
|
||||||
, singletonMap "supervisors" $ SortColumn $ queryUserUserCompany >>> firmCountUserSupervisors
|
, singletonMap "supervisors" $ SortColumn $ queryUserUserCompany >>> firmCountUserSupervisors
|
||||||
, singletonMap "reroutes" $ SortColumn $ queryUserUserCompany >>> firmCountUserSupervisorsReroute
|
, singletonMap "reroutes" $ SortColumn $ queryUserUserCompany >>> firmCountUserSupervisorsReroute
|
||||||
, singletonMap "usr-reason" $ SortColumn $ queryUserUserCompany >>> (E.^. UserCompanyReason)
|
|
||||||
, singletonMap "priority" $ SortColumn $ queryUserUserCompany >>> (E.^. UserCompanyPriority)
|
|
||||||
]
|
]
|
||||||
dbtFilter = mconcat
|
dbtFilter = mconcat
|
||||||
[ single $ fltrUserNameEmail queryUserUser
|
[ single $ fltrUserNameEmail queryUserUser
|
||||||
@ -928,63 +905,33 @@ mkFirmUserTable isAdmin cid = do
|
|||||||
usrSpr <- E.from $ E.table @UserSupervisor
|
usrSpr <- E.from $ E.table @UserSupervisor
|
||||||
E.where_ $ usrSpr E.^. UserSupervisorUser E.==. queryUserUser row E.^. UserId
|
E.where_ $ usrSpr E.^. UserSupervisorUser E.==. queryUserUser row E.^. UserId
|
||||||
E.&&. usrSpr E.^. UserSupervisorSupervisor `E.in_` E.vals criteria
|
E.&&. usrSpr E.^. UserSupervisorSupervisor `E.in_` E.vals criteria
|
||||||
, singletonMap "is-primary-company" $ FilterColumn $ \row (getLast -> criterion) ->
|
|
||||||
let checkPrimary = do
|
|
||||||
other <- E.from $ E.table @UserCompany
|
|
||||||
E.where_ $ other E.^. UserCompanyUser E.==. queryUserUserCompany row E.^. UserCompanyUser
|
|
||||||
E.&&. other E.^. UserCompanyPriority E.>. queryUserUserCompany row E.^. UserCompanyPriority
|
|
||||||
in case criterion of
|
|
||||||
Nothing -> E.true
|
|
||||||
Just False -> E.exists checkPrimary
|
|
||||||
Just True -> E.notExists checkPrimary
|
|
||||||
]
|
]
|
||||||
-- superField = selectField $ ????
|
-- superField = selectField $ ????
|
||||||
dbtFilterUI mPrev = mconcat
|
dbtFilterUI mPrev = mconcat
|
||||||
[ fltrUserNameEmailHdrUI MsgTableCompanyUser mPrev
|
[ fltrUserNameEmailHdrUI MsgTableCompanyUser mPrev
|
||||||
-- , prismAForm (singletonFilter "supervisor-is" . maybePrism _PathPiece) mPrev $ aopt (hoistField lift supervisorField) (fslI MsgFilterSupervisor)
|
-- , prismAForm (singletonFilter "supervisor-is" . maybePrism _PathPiece) mPrev $ aopt (hoistField lift supervisorField) (fslI MsgFilterSupervisor)
|
||||||
, prismAForm (multiFilter "supervisors-are" . maybePrism monoPathPieces) mPrev $ aopt (hoistField lift supervisorsField) (fslI MsgFilterSupervisor & setTooltip MsgMultiSelectTip)
|
, prismAForm (multiFilter "supervisors-are" . maybePrism monoPathPieces) mPrev $ aopt (hoistField lift supervisorsField) (fslI MsgFilterSupervisor & setTooltip MsgMultiSelectTip)
|
||||||
, prismAForm (singletonFilter "has-supervisor" . maybePrism _PathPiece) mPrev $ aopt (boolField . Just $ SomeMessage MsgBoolIrrelevant) (fslI MsgFilterSupervisor)
|
, prismAForm (singletonFilter "has-supervisor" . maybePrism _PathPiece) mPrev $ aopt (boolField . Just $ SomeMessage MsgBoolIrrelevant) (fslI MsgFilterSupervisor)
|
||||||
, prismAForm (singletonFilter "has-company-supervisor" . maybePrism _PathPiece) mPrev $ aopt (boolField . Just $ SomeMessage MsgBoolIrrelevant) (fslI $ MsgFilterSupervisorCompany fsh)
|
, prismAForm (singletonFilter "has-company-supervisor" . maybePrism _PathPiece) mPrev $ aopt (boolField . Just $ SomeMessage MsgBoolIrrelevant) (fslI $ MsgFilterSupervisorCompany fsh)
|
||||||
, prismAForm (singletonFilter "has-foreign-supervisor" . maybePrism _PathPiece) mPrev $ aopt (boolField . Just $ SomeMessage MsgBoolIrrelevant) (fslI $ MsgFilterSupervisorForeign fsh)
|
, prismAForm (singletonFilter "has-foreign-supervisor" . maybePrism _PathPiece) mPrev $ aopt (boolField . Just $ SomeMessage MsgBoolIrrelevant) (fslI $ MsgFilterSupervisorForeign fsh)
|
||||||
, prismAForm (singletonFilter "is-primary-company" . maybePrism _PathPiece) mPrev $ aopt (boolField . Just $ SomeMessage MsgBoolIrrelevant) (fslI MsgFilterFirmPrimary)
|
|
||||||
]
|
]
|
||||||
dbtStyle = def { dbsFilterLayout = defaultDBSFilterLayout }
|
dbtStyle = def { dbsFilterLayout = defaultDBSFilterLayout }
|
||||||
userReasons :: HandlerFor UniWorX (OptionList Text)
|
|
||||||
userReasons = fmap (mkOptionList . map (\t -> Option t t t) . Set.toAscList) . runDB $
|
|
||||||
fmap (setOf $ folded . _Value . _Just) . E.select . E.distinct $ do
|
|
||||||
usrc <- E.from $ E.table @UserCompany
|
|
||||||
E.where_ $ E.isJust (usrc E.^. UserCompanyReason)
|
|
||||||
E.&&. usrc E.^. UserCompanyCompany E.==. E.val cid
|
|
||||||
return $ usrc E.^. UserCompanyReason
|
|
||||||
superReasons :: HandlerFor UniWorX (OptionList Text)
|
|
||||||
superReasons = fmap (mkOptionList . map (\t -> Option t t t) . Set.toAscList) . runDB $
|
|
||||||
fmap (setOf $ folded . _Value . _Just) . E.select . E.distinct $ do
|
|
||||||
usrc <- E.from $ E.table @UserSupervisor
|
|
||||||
E.where_ $ E.isJust (usrc E.^. UserSupervisorReason)
|
|
||||||
E.&&. usrc E.^. UserSupervisorCompany E.~=. E.val cid
|
|
||||||
return $ usrc E.^. UserSupervisorReason
|
|
||||||
acts :: Map FirmUserAction (AForm Handler FirmUserActionData)
|
acts :: Map FirmUserAction (AForm Handler FirmUserActionData)
|
||||||
acts = mconcat
|
acts = mconcat
|
||||||
[ guardMonoid isAdmin $ singletonMap FirmUserActNotify $ pure FirmUserActNotifyData
|
[ guardMonoid isAdmin $ singletonMap FirmUserActNotify $ pure FirmUserActNotifyData
|
||||||
, singletonMap FirmUserActResetSupervision $ FirmUserActResetSupervisionData
|
, singletonMap FirmUserActResetSupervision $ FirmUserActResetSupervisionData
|
||||||
<$> aopt (boolFieldCustom (SomeMessage MsgFirmActResetSupersRemoveAll) (SomeMessage MsgFirmActResetSupersRemoveAps) $ Just $ SomeMessage MsgFirmActResetSupersKeepAll) (fslI MsgFirmActResetSuperKeep) (Just $ Just False)
|
<$> aopt (boolField . Just $ SomeMessage MsgBoolIrrelevant) (fslI MsgFirmActResetSuperKeep) (Just $ Just False)
|
||||||
|
-- <*> aopt checkBoxField (fslI MsgFirmActResetMutualSupervision) (Just $ Just True )
|
||||||
, singletonMap FirmUserActSetSupervisor $ FirmUserActSetSupervisorData
|
, singletonMap FirmUserActSetSupervisor $ FirmUserActSetSupervisorData
|
||||||
<$> aopt (textField & cfAnySeparatedSet) (fslI MsgFirmNewSupervisor & setTooltip MsgCourseParticipantsRegisterUsersFieldTip) Nothing
|
<$> aopt (textField & cfAnySeparatedSet) (fslI MsgFirmNewSupervisor & setTooltip MsgCourseParticipantsRegisterUsersFieldTip) Nothing
|
||||||
<*> aopt supervisorsField (fslI MsgFirmSetSupervisor & setTooltip MsgMultiSelectTip) Nothing
|
<*> aopt supervisorsField (fslI MsgFirmSetSupervisor & setTooltip MsgMultiSelectTip) Nothing
|
||||||
<*> aopt (textField & cfStrip & addDatalist superReasons) (fslI MsgUserSupervisorReason & setTooltip MsgUserSupervisorReasonTooltip) Nothing
|
<*> areq (boolField . Just $ SomeMessage MsgBoolIrrelevant) (fslI MsgMailSupervisorReroute & setTooltip MsgMailSupervisorRerouteTooltip) (Just False)
|
||||||
<*> areq boolField' (fslI MsgMailSupervisorReroute & setTooltip MsgMailSupervisorRerouteTooltip) (Just False)
|
<*> areq (boolField . Just $ SomeMessage MsgBoolIrrelevant) (fslI MsgFirmActResetSuperKeep) (Just False)
|
||||||
<*> aopt (boolFieldCustom (SomeMessage MsgFirmActResetSupersRemoveAll) (SomeMessage MsgFirmActResetSupersRemoveAps) $ Just $ SomeMessage MsgFirmActResetSupersKeepAll) (fslI MsgFirmActResetSuperKeep) (Just $ Just False)
|
|
||||||
, singletonMap FirmUserActMkSuper $ FirmUserActMkSuperData
|
, singletonMap FirmUserActMkSuper $ FirmUserActMkSuperData
|
||||||
<$> aopt checkBoxField (fslI MsgTableIsDefaultReroute) (Just $ Just True)
|
<$> aopt checkBoxField (fslI MsgTableIsDefaultReroute) (Just $ Just True)
|
||||||
, singletonMap FirmUserActChangeContact $ FirmUserActChangeContactData
|
, singletonMap FirmUserActChangeContact $ FirmUserActChangeContactData
|
||||||
<$> aopt htmlField (fslI MsgPostAddress & setTooltip (SomeMessages [SomeMessage MsgPostAddressTip, SomeMessage MsgUtilEmptyNoChangeTip])) Nothing
|
<$> aopt htmlField (fslI MsgPostAddress & setTooltip (SomeMessages [SomeMessage MsgPostAddressTip, SomeMessage MsgUtilEmptyNoChangeTip])) Nothing
|
||||||
<*> aopt boolField' (fslI MsgCompanyUserUseCompanyAddress & setTooltip MsgCompanyUserUseCompanyAddressTip) Nothing
|
<*> aopt postalEmailField (fslI MsgFormFieldPostal & setTooltip MsgFormFieldPostalTip) Nothing
|
||||||
<*> aopt postalEmailField (fslI MsgFormFieldPostal & setTooltip MsgFormFieldPostalTip) Nothing
|
|
||||||
, singletonMap FirmUserActChangeDetails $ FirmUserActChangeDetailsData
|
|
||||||
<$> aopt intField (fslI MsgCompanyUserPriority & setTooltip MsgCompanyUserPriorityTip) Nothing
|
|
||||||
<*> aopt (textField & cfStrip & addDatalist userReasons) (fslI MsgUserCompanyReason & setTooltip (SomeMessages [SomeMessage MsgUserCompanyReasonTooltip, SomeMessage MsgNullDeletes])) Nothing
|
|
||||||
, singletonMap FirmUserActRemove $ FirmUserActRemoveData
|
|
||||||
<$> areq boolField' (fslI MsgFirmActRemoveSupers) (Just True)
|
|
||||||
]
|
]
|
||||||
dbtParams = DBParamsForm
|
dbtParams = DBParamsForm
|
||||||
{ dbParamsFormMethod = POST
|
{ dbParamsFormMethod = POST
|
||||||
@ -1051,10 +998,6 @@ postFirmUsersR fsh = do
|
|||||||
-- return usr
|
-- return usr
|
||||||
<*> mkFirmUserTable isAdmin cid
|
<*> mkFirmUserTable isAdmin cid
|
||||||
|
|
||||||
let resetSupers :: Maybe Bool -> NonEmpty UserId -> DB Int64
|
|
||||||
resetSupers Nothing _ = return 0
|
|
||||||
resetSupers (Just False) uids = deleteDefaultSupervisorsForUsers [] [] uids
|
|
||||||
resetSupers (Just True ) uids = deleteWhereCount [UserSupervisorUser <-. toList uids]
|
|
||||||
formResult fusrRes $ \case
|
formResult fusrRes $ \case
|
||||||
(_, uids) | null uids -> addMessageI Error MsgUtilEmptyChoice
|
(_, uids) | null uids -> addMessageI Error MsgUtilEmptyChoice
|
||||||
(FirmUserActNotifyData , uids) -> do
|
(FirmUserActNotifyData , uids) -> do
|
||||||
@ -1062,8 +1005,10 @@ postFirmUsersR fsh = do
|
|||||||
redirect (FirmCommR fsh, [(toPathPiece GetRecipient, toPathPiece cID) | cID <- cuids])
|
redirect (FirmCommR fsh, [(toPathPiece GetRecipient, toPathPiece cID) | cID <- cuids])
|
||||||
(FirmUserActResetSupervisionData{..}, set2NonEmpty (error "Unexpected empty user list in getFirmUserR action handler.") -> uids) -> do -- set guaranteed to be non-empty due to first case clause
|
(FirmUserActResetSupervisionData{..}, set2NonEmpty (error "Unexpected empty user list in getFirmUserR action handler.") -> uids) -> do -- set guaranteed to be non-empty due to first case clause
|
||||||
runDB $ do
|
runDB $ do
|
||||||
delSupers <- resetSupers firmUserActResetSupers uids
|
delSupers <- if firmUserActResetKeepOldSupers == Just False
|
||||||
newSupers <- addDefaultSupervisors Nothing cid uids
|
then deleteSupervisors uids
|
||||||
|
else return 0
|
||||||
|
newSupers <- addDefaultSupervisors cid uids
|
||||||
addMessageI Info $ MsgFirmResetSupervision delSupers newSupers
|
addMessageI Info $ MsgFirmResetSupervision delSupers newSupers
|
||||||
reloadKeepGetParams $ FirmUsersR fsh -- reload to reflect changes
|
reloadKeepGetParams $ FirmUsersR fsh -- reload to reflect changes
|
||||||
(FirmUserActSetSupervisorData{..}, set2NonEmpty (error "Unexpected empty user list in getFirmUserR action handler.") -> uids) -> do
|
(FirmUserActSetSupervisorData{..}, set2NonEmpty (error "Unexpected empty user list in getFirmUserR action handler.") -> uids) -> do
|
||||||
@ -1081,55 +1026,27 @@ postFirmUsersR fsh = do
|
|||||||
<li>#{usr}
|
<li>#{usr}
|
||||||
|]
|
|]
|
||||||
in addMessageModal Error (i18n . MsgCourseParticipantsRegisterNotFoundInAvs $ length usersNotFound) (Right msgContent)
|
in addMessageModal Error (i18n . MsgCourseParticipantsRegisterNotFoundInAvs $ length usersNotFound) (Right msgContent)
|
||||||
delSupers <- runDB $ resetSupers firmUserActResetSupers uids
|
delSupers <- runDB
|
||||||
<* putMany [UserSupervisor s u firmUserActSetSuperReroute (Just cid) firmUserActSetSuperReason | u <- toList uids, s <- newSupers]
|
$ bool (deleteSupervisors uids) (return 0) firmUserActSetSuperKeep
|
||||||
|
<* putMany [UserSupervisor s u firmUserActSetSuperReroute | u <- toList uids, s <- newSupers]
|
||||||
addMessageI Success $ MsgFirmSetSupersReport nrUsers nrSupers delSupers
|
addMessageI Success $ MsgFirmSetSupersReport nrUsers nrSupers delSupers
|
||||||
reloadKeepGetParams $ FirmUsersR fsh -- reload to reflect changes
|
reloadKeepGetParams $ FirmUsersR fsh -- reload to reflect changes
|
||||||
(FirmUserActMkSuperData{..}, Set.toList -> uids) -> do
|
|
||||||
nrUpd <- runDB $ updateWhereCount [UserCompanyCompany ==. cid, UserCompanyUser <-. uids] [UserCompanySupervisor =. True, UserCompanySupervisorReroute =. (firmUserActMkSuperReroute == Just True)]
|
|
||||||
addMessageI Success $ MsgFirmActAddSupersSet nrUpd Nothing
|
|
||||||
reloadKeepGetParams $ FirmUsersR fsh -- reload to reflect changes
|
|
||||||
(FirmUserActChangeDetailsData{..}, Set.toList -> uids) -> do
|
|
||||||
let upReason = case canonical firmUserActDetailReason of
|
|
||||||
Nothing -> Nothing
|
|
||||||
Just "NULL" -> Just $ UserCompanyReason =. Nothing
|
|
||||||
other -> Just $ UserCompanyReason =. other
|
|
||||||
nrUpd <- runDB $ updateWhereCount [UserCompanyCompany ==. cid, UserCompanyUser <-. uids] $ catMaybes [upReason, (UserCompanyPriority =.) <$> firmUserActDetailPriority]
|
|
||||||
let total = fromIntegral $ length uids
|
|
||||||
allok = bool Warning Success $ nrUpd == total
|
|
||||||
addMessageI allok $ MsgFirmUserActChangeDetailsResult nrUpd total
|
|
||||||
reloadKeepGetParams $ FirmUsersR fsh -- reload to reflect changes
|
|
||||||
(FirmUserActChangeContactData{..}, Set.toList -> uids)
|
|
||||||
| firmUserActUseCompanyPostal == Just True, isJust firmUserActPostalAddr ->
|
|
||||||
addMessageI Error MsgCompanyUserUseCompanyPostalError
|
|
||||||
| otherwise -> do
|
|
||||||
let changes = catMaybes
|
|
||||||
[ toMaybe (firmUserActUseCompanyPostal == Just True) (UserPostAddress =. Nothing) -- precondition ensures that only one update applies for UserPostAddress
|
|
||||||
, (UserPostAddress =.) . Just <$> canonical firmUserActPostalAddr -- note that Nothing means no change and not delete address!
|
|
||||||
, (UserPrefersPostal =.) <$> firmUserActPostalPref
|
|
||||||
]
|
|
||||||
nrChanged <- runDB $ do
|
|
||||||
nrUsrChange <- updateWhereCount [UserId <-. uids] changes
|
|
||||||
nrUseComp <- case firmUserActUseCompanyPostal of
|
|
||||||
Just x -> updateWhereCount [UserCompanyCompany ==. cid, UserCompanyUser <-. uids] [UserCompanyUseCompanyAddress =. x]
|
|
||||||
Nothing -> return 0
|
|
||||||
return $ max nrUsrChange nrUseComp
|
|
||||||
let total = fromIntegral $ length uids
|
|
||||||
allok = bool Warning Success $ nrChanged == total
|
|
||||||
addMessageI allok $ MsgFirmUserActChangeResult nrChanged total
|
|
||||||
reloadKeepGetParams $ FirmUsersR fsh -- reload to reflect changes
|
|
||||||
(FirmUserActRemoveData{..}, Set.toList -> uids) -> do
|
|
||||||
let optRemove = if firmUserActRemoveSupers then id else const $ return 0
|
|
||||||
(nrUc, nrSuper, nrSubs) <- runDB $ (,,)
|
|
||||||
<$> deleteWhereCount [UserCompanyCompany ==. cid, UserCompanyUser <-. uids]
|
|
||||||
<*> optRemove (deleteWhereCount [UserSupervisorCompany ==. Just cid, UserSupervisorSupervisor <-. uids])
|
|
||||||
<*> optRemove (deleteWhereCount [UserSupervisorCompany ==. Just cid, UserSupervisorUser <-. uids])
|
|
||||||
let total = fromIntegral $ length uids
|
|
||||||
allok = bool Warning Success $ total == nrUc
|
|
||||||
addMessageI allok $ someMessages [MsgFirmUserActRemoveResult nrUc, MsgFirmRemoveSupervision nrSuper nrSubs]
|
|
||||||
reloadKeepGetParams $ FirmUsersR fsh -- reload to reflect changes
|
|
||||||
|
|
||||||
formFirmAction <- runFirmActionFormPost cid (FirmUsersR fsh) isAdmin [FirmActNotify, FirmActResetSupervision, FirmActAddSupervisors, FirmActChangeContactFirm, FirmActChangeContactUser]
|
(FirmUserActMkSuperData{..}, Set.toList -> uids) -> do
|
||||||
|
nrMkSuper <- runDB $ updateWhereCount [UserCompanyUser <-. uids, UserCompanyCompany ==. cid] [UserCompanySupervisor =. True, UserCompanySupervisorReroute =. (firmUserActMkSuperReroute == Just True)]
|
||||||
|
addMessageI Info $ MsgFirmActAddSupersSet nrMkSuper Nothing
|
||||||
|
reloadKeepGetParams $ FirmUsersR fsh -- reload to reflect changes
|
||||||
|
(FirmUserActChangeContactData{..}, Set.toList -> uids) ->
|
||||||
|
let changes = catMaybes
|
||||||
|
[ (UserPostAddress =.) . Just <$> canonical firmUserActPostalAddr -- note that Nothing means no change and not delete address!
|
||||||
|
, (UserPrefersPostal =.) <$> firmUserActPostalPref
|
||||||
|
]
|
||||||
|
in unless (null changes) $ do
|
||||||
|
nrChanged <- runDB $ updateWhereCount [UserId <-. uids] changes
|
||||||
|
addMessageI Success $ MsgFirmUserChanges nrChanged
|
||||||
|
reloadKeepGetParams $ FirmUsersR fsh -- reload to reflect changes
|
||||||
|
|
||||||
|
formFirmAction <- runFirmActionFormPost cid (FirmUsersR fsh) isAdmin [FirmActNotify, FirmActResetSupervision, FirmActAddSupersvisors, FirmActChangeContactFirm, FirmActChangeContactUser]
|
||||||
|
|
||||||
siteLayout (citext2widget companyName) $ do
|
siteLayout (citext2widget companyName) $ do
|
||||||
setTitle $ toHtml $ CI.original companyShorthand <> "-" <> tshow companyAvsId
|
setTitle $ toHtml $ CI.original companyShorthand <> "-" <> tshow companyAvsId
|
||||||
@ -1156,7 +1073,7 @@ data FirmSuperActionData = FirmSuperActNotifyData
|
|||||||
, firmSuperActSwitchReroute :: Maybe Bool
|
, firmSuperActSwitchReroute :: Maybe Bool
|
||||||
}
|
}
|
||||||
| FirmSuperActRMSuperDefData
|
| FirmSuperActRMSuperDefData
|
||||||
{ firmSuperActRMSuperActive :: Bool }
|
{ firmSuperActRMSuperActive :: Maybe Bool }
|
||||||
|
|
||||||
deriving (Eq, Ord, Show, Generic)
|
deriving (Eq, Ord, Show, Generic)
|
||||||
|
|
||||||
@ -1172,7 +1089,6 @@ querySuperUserCompany = $(sqlLOJproj 2 2)
|
|||||||
type SuperCompanyTableData = DBRow (Entity User, E.Value Word64, E.Value Word64
|
type SuperCompanyTableData = DBRow (Entity User, E.Value Word64, E.Value Word64
|
||||||
, [(E.Value CompanyName, E.Value CompanyShorthand, E.Value Bool)]
|
, [(E.Value CompanyName, E.Value CompanyShorthand, E.Value Bool)]
|
||||||
, E.Value (Maybe Bool), E.Value (Maybe Bool) -- Maybe (Entity UserCompany)
|
, E.Value (Maybe Bool), E.Value (Maybe Bool) -- Maybe (Entity UserCompany)
|
||||||
, E.Value Bool
|
|
||||||
)
|
)
|
||||||
|
|
||||||
resultSuperUser :: Lens' SuperCompanyTableData (Entity User)
|
resultSuperUser :: Lens' SuperCompanyTableData (Entity User)
|
||||||
@ -1193,9 +1109,6 @@ resultSuperCompanyDefaultSuper = _dbrOutput . _5 . _unValue
|
|||||||
resultSuperCompanyDefaultReroute :: Lens' SuperCompanyTableData (Maybe Bool)
|
resultSuperCompanyDefaultReroute :: Lens' SuperCompanyTableData (Maybe Bool)
|
||||||
resultSuperCompanyDefaultReroute = _dbrOutput . _6 . _unValue
|
resultSuperCompanyDefaultReroute = _dbrOutput . _6 . _unValue
|
||||||
|
|
||||||
resultSuperCompanySuperior :: Lens' SuperCompanyTableData Bool
|
|
||||||
resultSuperCompanySuperior = _dbrOutput . _7 . _unValue
|
|
||||||
|
|
||||||
instance HasEntity SuperCompanyTableData User where
|
instance HasEntity SuperCompanyTableData User where
|
||||||
hasEntity = resultSuperUser
|
hasEntity = resultSuperUser
|
||||||
|
|
||||||
@ -1207,31 +1120,27 @@ mkFirmSuperTable :: Bool -> CompanyId -> DB (FormResult (FirmSuperActionData, Se
|
|||||||
mkFirmSuperTable isAdmin cid = do
|
mkFirmSuperTable isAdmin cid = do
|
||||||
msgSupervisorUnchanged <- messageI Info MsgFirmSuperActSwitchSuperInfo
|
msgSupervisorUnchanged <- messageI Info MsgFirmSuperActSwitchSuperInfo
|
||||||
let
|
let
|
||||||
reasonSuperior = tshow SupervisorReasonAvsSuperior
|
|
||||||
-- fsh = unCompanyKey cid
|
-- fsh = unCompanyKey cid
|
||||||
resultDBTable = DBTable{..}
|
resultDBTable = DBTable{..}
|
||||||
where
|
where
|
||||||
dbtSQLQuery = \(usr `E.LeftOuterJoin` usrCmp) -> do
|
dbtSQLQuery = \(usr `E.LeftOuterJoin` usrCmp) -> do
|
||||||
EL.on $ usr E.^. UserId E.=?. usrCmp E.?. UserCompanyUser E.&&. usrCmp E.?. UserCompanyCompany E.?=. E.val cid
|
EL.on $ usr E.^. UserId E.=?. usrCmp E.?. UserCompanyUser E.&&. usrCmp E.?. UserCompanyCompany E.?=. E.val cid
|
||||||
E.where_ $ E.isTrue (usrCmp E.?. UserCompanySupervisor) E.||. E.exists (firmQuerySupervisedBy cid Nothing usr)
|
E.where_ $ E.isTrue (usrCmp E.?. UserCompanySupervisor)
|
||||||
-- let uc_reason = E.joinV (usrCmp E.?. UserCompanyReason)
|
E.||. E.exists (firmQuerySupervisedBy cid Nothing usr)
|
||||||
return ( usr
|
return ( usr
|
||||||
, usr & firmCountForSupervisor cid Nothing
|
, usr & firmCountForSupervisor cid Nothing
|
||||||
, usr & firmCountForSupervisor cid (Just (E.^. UserSupervisorRerouteNotifications))
|
, usr & firmCountForSupervisor cid (Just (E.^. UserSupervisorRerouteNotifications))
|
||||||
, usrCmp E.?. UserCompanySupervisor
|
, usrCmp E.?. UserCompanySupervisor
|
||||||
, usrCmp E.?. UserCompanySupervisorReroute
|
, usrCmp E.?. UserCompanySupervisorReroute
|
||||||
-- , (E.isJust uc_reason E.&&. uc_reason E.==. E.justVal reasonSuperior) -- NOTE: this is problematic, as obvious approaches caused errors such as: Failed to parse Haskell type bool, received PersistNull, since the SQL comparison with NULL returns NULL
|
|
||||||
, (E.coalesceDefault [E.joinV (usrCmp E.?. UserCompanyReason)] (E.val mempty) E.==. E.val reasonSuperior) -- works as well
|
|
||||||
E.||. E.exists (firmQuerySupervisedBy cid (Just (\usrSpr -> usrSpr E.^. UserSupervisorReason E.==. E.justVal reasonSuperior)) usr)
|
|
||||||
)
|
)
|
||||||
dbtRowKey = querySuperUser >>> (E.^. UserId)
|
dbtRowKey = querySuperUser >>> (E.^. UserId)
|
||||||
dbtProj = dbtProjSimple $ \(usr, supervised, rerouted, supervisor, reroute, isSuperior) -> do
|
dbtProj = dbtProjSimple $ \(usr, supervised, rerouted, supervisor, reroute) -> do
|
||||||
cmps <- E.select $ do
|
cmps <- E.select $ do
|
||||||
(cmp :& usrCmp) <- E.from $ E.table @Company `E.innerJoin` E.table @UserCompany `E.on` (\(cmp :& usrCmp) -> cmp E.^. CompanyId E.==. usrCmp E.^. UserCompanyCompany)
|
(cmp :& usrCmp) <- E.from $ E.table @Company `E.innerJoin` E.table @UserCompany `E.on` (\(cmp :& usrCmp) -> cmp E.^. CompanyId E.==. usrCmp E.^. UserCompanyCompany)
|
||||||
E.where_ $ usrCmp E.^. UserCompanyUser E.==. E.val (entityKey usr)
|
E.where_ $ usrCmp E.^. UserCompanyUser E.==. E.val (entityKey usr)
|
||||||
E.orderBy [E.asc $ cmp E.^. CompanyName]
|
E.orderBy [E.asc $ cmp E.^. CompanyName]
|
||||||
return (cmp E.^. CompanyName, cmp E.^. CompanyShorthand, usrCmp E.^. UserCompanySupervisor)
|
return (cmp E.^. CompanyName, cmp E.^. CompanyShorthand, usrCmp E.^. UserCompanySupervisor)
|
||||||
return (usr, supervised, rerouted, cmps, supervisor, reroute, isSuperior)
|
return (usr, supervised, rerouted, cmps, supervisor, reroute)
|
||||||
dbtColonnade = formColonnade $ mconcat
|
dbtColonnade = formColonnade $ mconcat
|
||||||
[ dbSelect (applying _2) id (return . view (resultSuperUser . _entityKey))
|
[ dbSelect (applying _2) id (return . view (resultSuperUser . _entityKey))
|
||||||
, colUserNameModalHdr MsgTableSupervisor ForProfileDataR
|
, colUserNameModalHdr MsgTableSupervisor ForProfileDataR
|
||||||
@ -1243,11 +1152,7 @@ mkFirmSuperTable isAdmin cid = do
|
|||||||
, colUserEmail
|
, colUserEmail
|
||||||
, sortable (Just "supervised") (i18nCell MsgTableCompanyNrEmpSupervised) $ \(view resultSuperCompanySupervised -> nr) -> wgtCell $ word2widget nr
|
, sortable (Just "supervised") (i18nCell MsgTableCompanyNrEmpSupervised) $ \(view resultSuperCompanySupervised -> nr) -> wgtCell $ word2widget nr
|
||||||
, sortable (Just "rerouted") (i18nCell MsgTableCompanyNrEmpRerouted ) $ \(view resultSuperCompanyReroutes -> nr) -> wgtCell $ word2widget nr
|
, sortable (Just "rerouted") (i18nCell MsgTableCompanyNrEmpRerouted ) $ \(view resultSuperCompanyReroutes -> nr) -> wgtCell $ word2widget nr
|
||||||
, sortable (Just "def-super") (i18nCell MsgTableIsDefaultSupervisor) $ view resultSuperCompanyDefaultSuper >>> \case
|
, sortable (Just "def-super") (i18nCell MsgTableIsDefaultSupervisor) $ \(view resultSuperCompanyDefaultSuper -> mb) -> case mb of { Nothing -> iconCell IconSupervisorForeign; Just True -> iconCell IconSupervisor; Just False -> iconSpacerCell }
|
||||||
Nothing -> iconCell IconSupervisorForeign
|
|
||||||
(Just True ) -> iconCell IconSupervisor
|
|
||||||
(Just False) -> iconSpacerCell
|
|
||||||
, sortable Nothing (i18nCell MsgTableSuperior) $ view resultSuperCompanySuperior >>> flip ifIconCell IconSuperior
|
|
||||||
, sortable (Just "def-reroute") (i18nCell MsgTableIsDefaultReroute) $ \(view resultSuperCompanyDefaultReroute -> mb) -> tickmarkCell (mb == Just True)
|
, sortable (Just "def-reroute") (i18nCell MsgTableIsDefaultReroute) $ \(view resultSuperCompanyDefaultReroute -> mb) -> tickmarkCell (mb == Just True)
|
||||||
, sortable Nothing (i18nCell MsgTableUserEdit) $ \(view resultSuperUser -> entUsr) -> cellEditUserModal entUsr
|
, sortable Nothing (i18nCell MsgTableUserEdit) $ \(view resultSuperUser -> entUsr) -> cellEditUserModal entUsr
|
||||||
]
|
]
|
||||||
@ -1270,40 +1175,20 @@ mkFirmSuperTable isAdmin cid = do
|
|||||||
]
|
]
|
||||||
dbtFilter = mconcat
|
dbtFilter = mconcat
|
||||||
[ single $ fltrUserNameEmail querySuperUser
|
[ single $ fltrUserNameEmail querySuperUser
|
||||||
, singletonMap "is-foreign-supervisor" $ FilterColumn $ \(querySuperUserCompany -> suc) (getLast -> criterion) ->
|
|
||||||
case criterion of
|
|
||||||
Nothing -> E.true
|
|
||||||
Just True -> E.isNothing $ suc E.?. UserCompanyUser
|
|
||||||
Just False -> E.isJust $ suc E.?. UserCompanyUser
|
|
||||||
, singletonMap "super-relation-foreign" $ FilterColumn $ \row (getLast -> criterion) ->
|
|
||||||
let checkSuper = do
|
|
||||||
usrSpr <- E.from $ E.table @UserSupervisor
|
|
||||||
E.where_ $ usrSpr E.^. UserSupervisorSupervisor E.==. querySuperUser row E.^. UserId
|
|
||||||
E.&&. E.exists (do
|
|
||||||
usr <- E.from $ E.table @UserCompany
|
|
||||||
E.where_ $ usr E.^. UserCompanyCompany E.!=. E.val cid
|
|
||||||
E.&&. usr E.^. UserCompanyUser E.==. usrSpr E.^. UserSupervisorUser
|
|
||||||
)
|
|
||||||
in case criterion of
|
|
||||||
Nothing -> E.true
|
|
||||||
Just True -> E.exists checkSuper
|
|
||||||
Just False -> E.notExists checkSuper
|
|
||||||
]
|
]
|
||||||
dbtFilterUI mPrev = mconcat
|
dbtFilterUI mPrev = mconcat
|
||||||
[ fltrUserNameEmailHdrUI MsgTableSupervisor mPrev
|
[ fltrUserNameEmailHdrUI MsgTableSupervisor mPrev
|
||||||
, prismAForm (singletonFilter "is-foreign-supervisor" . maybePrism _PathPiece) mPrev $ aopt (boolField . Just $ SomeMessage MsgBoolIrrelevant) (fslI MsgFirmSuperForeign)
|
|
||||||
, prismAForm (singletonFilter "super-relation-foreign" . maybePrism _PathPiece) mPrev $ aopt (boolField . Just $ SomeMessage MsgBoolIrrelevant) (fslI MsgFilterIsForeignSupervisee)
|
|
||||||
]
|
]
|
||||||
dbtStyle = def { dbsFilterLayout = defaultDBSFilterLayout }
|
dbtStyle = def { dbsFilterLayout = defaultDBSFilterLayout }
|
||||||
acts :: Map FirmSuperAction (AForm Handler FirmSuperActionData)
|
acts :: Map FirmSuperAction (AForm Handler FirmSuperActionData)
|
||||||
acts = mconcat
|
acts = mconcat
|
||||||
[ guardMonoid isAdmin $ singletonMap FirmSuperActNotify $ pure FirmSuperActNotifyData
|
[ guardMonoid isAdmin $ singletonMap FirmSuperActNotify $ pure FirmSuperActNotifyData
|
||||||
, singletonMap FirmSuperActSwitchSuper $ FirmSuperActSwitchSuperData
|
, singletonMap FirmSuperActSwitchSuper $ FirmSuperActSwitchSuperData
|
||||||
<$> aopt boolField' (fslI MsgFirmSuperDefault) (Just $ Just True)
|
<$> aopt (boolField . Just $ SomeMessage MsgBoolIrrelevant) (fslI MsgTableIsDefaultSupervisor) (Just $ Just True)
|
||||||
<*> aopt boolField' (fslI MsgTableIsDefaultReroute) Nothing
|
<*> aopt (boolField . Just $ SomeMessage MsgBoolIrrelevant) (fslI MsgTableIsDefaultReroute) Nothing
|
||||||
<* aformMessage msgSupervisorUnchanged
|
<* aformMessage msgSupervisorUnchanged
|
||||||
, singletonMap FirmSuperActRMSuperDef $ FirmSuperActRMSuperDefData
|
, singletonMap FirmSuperActRMSuperDef $ FirmSuperActRMSuperDefData
|
||||||
<$> areq boolField' (fslI MsgFirmSuperActRMSuperActive) (Just True)
|
<$> aopt checkBoxField (fslI MsgFirmSuperActRMSuperActive) (Just $ Just True)
|
||||||
]
|
]
|
||||||
dbtParams = DBParamsForm
|
dbtParams = DBParamsForm
|
||||||
{ dbParamsFormMethod = POST
|
{ dbParamsFormMethod = POST
|
||||||
@ -1347,14 +1232,19 @@ postFirmSupersR fsh = do
|
|||||||
formResult fsprRes $ \case
|
formResult fsprRes $ \case
|
||||||
(_, uids) | null uids -> addMessageI Error MsgUtilEmptyChoice
|
(_, uids) | null uids -> addMessageI Error MsgUtilEmptyChoice
|
||||||
(FirmSuperActRMSuperDefData{..}, Set.toList -> uids) -> do
|
(FirmSuperActRMSuperDefData{..}, Set.toList -> uids) -> do
|
||||||
let optRemove = if firmSuperActRMSuperActive then id else const $ return 0
|
(nrRmSuper,nrRmActual) <- runDB $ (,)
|
||||||
(nrRmSuper,nrRmSupers,nrRmSubs) <- runDB $ (,,)
|
|
||||||
<$> updateWhereCount [UserCompanyUser <-. uids, UserCompanyCompany ==. cid] [UserCompanySupervisor =. False, UserCompanySupervisorReroute =. False]
|
<$> updateWhereCount [UserCompanyUser <-. uids, UserCompanyCompany ==. cid] [UserCompanySupervisor =. False, UserCompanySupervisorReroute =. False]
|
||||||
<*> optRemove (deleteWhereCount [UserSupervisorCompany ==. Just cid, UserSupervisorSupervisor <-. uids])
|
<*> if firmSuperActRMSuperActive /= Just True
|
||||||
<*> optRemove (deleteWhereCount [UserSupervisorCompany ==. Just cid, UserSupervisorUser <-. uids])
|
then return 0
|
||||||
let total = fromIntegral $ length uids
|
else E.deleteCount $ do
|
||||||
allok = bool Warning Success $ total == nrRmSuper
|
spr <- E.from $ E.table @UserSupervisor
|
||||||
addMessageI allok $ someMessages [MsgRemoveSupervisors nrRmSuper, MsgFirmRemoveSupervision nrRmSupers nrRmSubs]
|
E.where_ $ spr E.^. UserSupervisorSupervisor `E.in_` E.vals uids
|
||||||
|
E.&&. E.exists (do
|
||||||
|
usr <- E.from $ E.table @UserCompany
|
||||||
|
E.where_ $ usr E.^. UserCompanyCompany E.==. E.val cid
|
||||||
|
E.&&. usr E.^. UserCompanyUser E.==. spr E.^. UserSupervisorUser
|
||||||
|
)
|
||||||
|
addMessageI Info $ MsgRemoveSupervisors nrRmSuper nrRmActual
|
||||||
reloadKeepGetParams $ FirmUsersR fsh -- reload to reflect changes
|
reloadKeepGetParams $ FirmUsersR fsh -- reload to reflect changes
|
||||||
(FirmSuperActSwitchSuperData{..}, Set.toList -> uids) -> do
|
(FirmSuperActSwitchSuperData{..}, Set.toList -> uids) -> do
|
||||||
let (fltrSpr, changes) = case (firmSuperActSwitchSuper, firmSuperActSwitchReroute) of
|
let (fltrSpr, changes) = case (firmSuperActSwitchSuper, firmSuperActSwitchReroute) of
|
||||||
@ -1372,9 +1262,9 @@ postFirmSupersR fsh = do
|
|||||||
cuids <- traverse encrypt $ Set.toList uids :: Handler [CryptoUUIDUser]
|
cuids <- traverse encrypt $ Set.toList uids :: Handler [CryptoUUIDUser]
|
||||||
redirect (FirmCommR fsh, [(toPathPiece GetRecipient, toPathPiece cID) | cID <- cuids])
|
redirect (FirmCommR fsh, [(toPathPiece GetRecipient, toPathPiece cID) | cID <- cuids])
|
||||||
|
|
||||||
formFirmAction <- runFirmActionFormPost cid (FirmSupersR fsh) isAdmin [FirmActAddSupervisors, FirmActResetSupervision, FirmActChangeContactFirm]
|
formFirmAction <- runFirmActionFormPost cid (FirmSupersR fsh) isAdmin [FirmActAddSupersvisors, FirmActResetSupervision, FirmActChangeContactFirm]
|
||||||
|
|
||||||
siteLayout (citext2widget companyName) $ do
|
siteLayout (citext2widget fsh) $ do
|
||||||
setTitle $ citext2Html $ fsh <> " Supers"
|
setTitle $ citext2Html $ fsh <> " Supers"
|
||||||
let firmContactInfo = $(widgetFile "firm-contact-info")
|
let firmContactInfo = $(widgetFile "firm-contact-info")
|
||||||
$(i18nWidgetFile "firm-supervisors")
|
$(i18nWidgetFile "firm-supervisors")
|
||||||
@ -1409,14 +1299,14 @@ handleFirmCommR ultDest cs = do
|
|||||||
csKeys = CompanyKey <$> cs
|
csKeys = CompanyKey <$> cs
|
||||||
mbUser <- maybeAuthId
|
mbUser <- maybeAuthId
|
||||||
-- get employees of chosen companies
|
-- get employees of chosen companies
|
||||||
empys <- mkCompanyUsrList <$> runDBRead (E.select $ do
|
empys <- mkCompanyUsrList <$> runDB (E.select $ do
|
||||||
(emp :& cmp) <- E.from $ E.table @User `E.innerJoin` E.table @UserCompany `E.on` (\(emp :& cmp) -> emp E.^. UserId E.==. cmp E.^. UserCompanyUser)
|
(emp :& cmp) <- E.from $ E.table @User `E.innerJoin` E.table @UserCompany `E.on` (\(emp :& cmp) -> emp E.^. UserId E.==. cmp E.^. UserCompanyUser)
|
||||||
E.where_ $ cmp E.^. UserCompanyCompany `E.in_` E.valList csKeys
|
E.where_ $ cmp E.^. UserCompanyCompany `E.in_` E.valList csKeys
|
||||||
E.orderBy [E.ascNullsFirst $ cmp E.^. UserCompanyCompany]
|
E.orderBy [E.ascNullsFirst $ cmp E.^. UserCompanyCompany]
|
||||||
return (E.just $ cmp E.^. UserCompanyCompany, emp E.^. UserId)
|
return (E.just $ cmp E.^. UserCompanyCompany, emp E.^. UserId)
|
||||||
)
|
)
|
||||||
-- get supervisors of employees
|
-- get supervisors of employees
|
||||||
sprs <- mkCompanyUsrList <$> runDBRead (E.select $ do
|
sprs <- mkCompanyUsrList <$> runDB (E.select $ do
|
||||||
(spr :& cmp) <- E.from $ E.table @User `E.leftJoin` E.table @UserCompany `E.on` (\(spr :& cmp) -> spr E.^. UserId E.=?. cmp E.?. UserCompanyUser)
|
(spr :& cmp) <- E.from $ E.table @User `E.leftJoin` E.table @UserCompany `E.on` (\(spr :& cmp) -> spr E.^. UserId E.=?. cmp E.?. UserCompanyUser)
|
||||||
E.where_ $ (E.isTrue (cmp E.?. UserCompanySupervisor) E.&&. cmp E.?. UserCompanyCompany `E.in_` E.justValList csKeys)
|
E.where_ $ (E.isTrue (cmp E.?. UserCompanySupervisor) E.&&. cmp E.?. UserCompanyCompany `E.in_` E.justValList csKeys)
|
||||||
E.||. (spr E.^. UserId E.=?. E.val mbUser)
|
E.||. (spr E.^. UserId E.=?. E.val mbUser)
|
||||||
|
|||||||
@ -6,7 +6,6 @@ module Handler.Health where
|
|||||||
|
|
||||||
import Import
|
import Import
|
||||||
|
|
||||||
import Data.Time.Format.ISO8601 (iso8601Show)
|
|
||||||
import Handler.Utils.DateTime (formatTimeW)
|
import Handler.Utils.DateTime (formatTimeW)
|
||||||
|
|
||||||
import qualified Data.Aeson.Encode.Pretty as Aeson
|
import qualified Data.Aeson.Encode.Pretty as Aeson
|
||||||
@ -20,9 +19,6 @@ import Control.Concurrent.STM.Delay
|
|||||||
|
|
||||||
import System.Environment (lookupEnv) -- while git version number is not working
|
import System.Environment (lookupEnv) -- while git version number is not working
|
||||||
|
|
||||||
import qualified Database.Esqueleto.Experimental as E
|
|
||||||
import qualified Database.Esqueleto.PostgreSQL as E (now_)
|
|
||||||
|
|
||||||
-- import Data.FileEmbed (embedStringFile)
|
-- import Data.FileEmbed (embedStringFile)
|
||||||
|
|
||||||
getHealthR :: Handler TypedContent
|
getHealthR :: Handler TypedContent
|
||||||
@ -81,12 +77,12 @@ getHealthR = do
|
|||||||
#{boolSymbol (healthOk hcstatus)} #
|
#{boolSymbol (healthOk hcstatus)} #
|
||||||
$case report
|
$case report
|
||||||
$of HealthLDAPAdmins (Just found)
|
$of HealthLDAPAdmins (Just found)
|
||||||
#{textPercent found 1}
|
#{textPercent found 1}
|
||||||
$of HealthActiveJobExecutors (Just active)
|
$of HealthActiveJobExecutors (Just active)
|
||||||
#{textPercent active 1}
|
#{textPercent active 1}
|
||||||
$of _
|
$of _
|
||||||
<div>
|
<div>
|
||||||
^{formatTimeW SelFormatDateTime lUp}
|
^{formatTimeW SelFormatDateTime lUp}
|
||||||
|]
|
|]
|
||||||
provideJson healthReports
|
provideJson healthReports
|
||||||
provideRep . return . Builder.toLazyText $ Aeson.encodePrettyToTextBuilder healthReports
|
provideRep . return . Builder.toLazyText $ Aeson.encodePrettyToTextBuilder healthReports
|
||||||
@ -117,44 +113,34 @@ getInstanceR = do
|
|||||||
getStatusR :: Handler Html
|
getStatusR :: Handler Html
|
||||||
getStatusR = do
|
getStatusR = do
|
||||||
starttime <- getsYesod appStartTime
|
starttime <- getsYesod appStartTime
|
||||||
dbTime <- runDBRead $ E.selectOne $ return E.now_
|
(currtime, env_version) <- liftIO $ (,) <$> getCurrentTime <*> lookupEnv "VERSION_NR"
|
||||||
(currtime,env_version) <- liftIO $ (,) <$> getCurrentTime <*> lookupEnv "VERSION_NR"
|
|
||||||
-- ft <- formatTime' "%Y-%m-%d %H:%M:%S" currtime
|
-- ft <- formatTime' "%Y-%m-%d %H:%M:%S" currtime
|
||||||
let diffTime :: UTCTime -> Text
|
withUrlRenderer
|
||||||
diffTime t =
|
|
||||||
let tdiff = diffUTCTime currtime t
|
|
||||||
in if 64 > abs tdiff
|
|
||||||
then tshow tdiff
|
|
||||||
else pack . iso8601Show . calendarTimeTime . fromIntegral $ truncate tdiff
|
|
||||||
|
|
||||||
withUrlRenderer
|
|
||||||
[hamlet|
|
[hamlet|
|
||||||
$doctype 5
|
$doctype 5
|
||||||
<html lang=en>
|
<html lang=en>
|
||||||
<head>
|
<head>
|
||||||
<title>Status
|
<title>Status
|
||||||
<body>
|
<body>
|
||||||
$maybe env_ver <- env_version
|
$maybe env_ver <- env_version
|
||||||
<p>
|
<p>
|
||||||
Environment version #{env_ver}
|
Environment version #{env_ver}
|
||||||
|
<p>
|
||||||
|
Current Time <br>
|
||||||
|
#{show currtime} <br>
|
||||||
<p>
|
<p>
|
||||||
Current Application Time <br>
|
Instance Start <br>
|
||||||
#{show currtime} <br>
|
|
||||||
$maybe dbtval <- dbTime
|
|
||||||
$with dbt <- E.unValue dbtval
|
|
||||||
Current Database Time <br>
|
|
||||||
#{show dbt} #
|
|
||||||
Difference: #{diffTime dbt} <br>
|
|
||||||
<p>
|
|
||||||
Instance Start <br>
|
|
||||||
#{show starttime} #
|
#{show starttime} #
|
||||||
Uptime: #{diffTime starttime}
|
Uptime: #{show $ ddays starttime currtime} days.
|
||||||
<p>
|
<p>
|
||||||
Compile Time <br>
|
Compile Time <br>
|
||||||
#{show cTime} #
|
#{show cTime} #
|
||||||
Build age: #{diffTime cTime}
|
Build age: #{show $ ddays cTime currtime} days.
|
||||||
|]
|
|]
|
||||||
where
|
where
|
||||||
-- vnr_full :: Text = $(embedStringFile "nix/docker/version.json") -- nix/ files not accessible during container construction
|
-- vnr_full :: Text = $(embedStringFile "nix/docker/version.json") -- nix/ files not accessible during container construction
|
||||||
cTime :: UTCTime
|
cTime :: UTCTime
|
||||||
cTime = $compileTime
|
cTime = $compileTime
|
||||||
|
|
||||||
|
ddays :: UTCTime -> UTCTime -> Double
|
||||||
|
ddays tstart tstop = (/100) $ fromIntegral $ round $ diffUTCTime tstop tstart / (36 * 24)
|
||||||
|
|||||||
@ -8,14 +8,12 @@ module Handler.Health.Interface
|
|||||||
getHealthInterfaceR
|
getHealthInterfaceR
|
||||||
, mkInterfaceLogTable
|
, mkInterfaceLogTable
|
||||||
, runInterfaceChecks
|
, runInterfaceChecks
|
||||||
, getConfigInterfacesR, postConfigInterfacesR
|
|
||||||
)
|
)
|
||||||
where
|
where
|
||||||
|
|
||||||
import Import
|
import Import
|
||||||
|
|
||||||
import qualified Data.Set as Set
|
-- import qualified Data.Set as Set
|
||||||
import qualified Data.Map as Map
|
|
||||||
import qualified Data.Text as Text
|
import qualified Data.Text as Text
|
||||||
import Handler.Utils
|
import Handler.Utils
|
||||||
import Handler.Utils.Concurrent
|
import Handler.Utils.Concurrent
|
||||||
@ -26,8 +24,6 @@ import qualified Database.Esqueleto.Utils as E
|
|||||||
import qualified Database.Esqueleto.Legacy as EL (on)
|
import qualified Database.Esqueleto.Legacy as EL (on)
|
||||||
import qualified Database.Persist.Sql as E (deleteWhereCount)
|
import qualified Database.Persist.Sql as E (deleteWhereCount)
|
||||||
|
|
||||||
defaultInterfaceWarnHours :: Int
|
|
||||||
defaultInterfaceWarnHours = 3 * 24 -- if no warn time can be found, use 3 days instead
|
|
||||||
|
|
||||||
-- | identify a wildcard argument
|
-- | identify a wildcard argument
|
||||||
wc2null :: Text -> Maybe Text
|
wc2null :: Text -> Maybe Text
|
||||||
@ -37,12 +33,6 @@ wc2null "_" = Nothing
|
|||||||
wc2null "*" = Nothing
|
wc2null "*" = Nothing
|
||||||
wc2null o = Just o
|
wc2null o = Just o
|
||||||
|
|
||||||
warnIntervalCell :: (IsDBTable m b, Integral a) => a -> DBCell m b
|
|
||||||
warnIntervalCell x
|
|
||||||
| x >= 0 = textCell $ formatDiffHours x
|
|
||||||
| x <= (-100) = i18nCell MsgInterfaceWarningDisabledEntirely
|
|
||||||
| otherwise = i18nCell MsgInterfaceWarningDisabledInterval
|
|
||||||
|
|
||||||
-- | sloppily parse a boolean, also see Model.Types.Avs.SloppyBool
|
-- | sloppily parse a boolean, also see Model.Types.Avs.SloppyBool
|
||||||
pbool :: Text -> Maybe Bool
|
pbool :: Text -> Maybe Bool
|
||||||
pbool (Text.toLower . Text.strip -> w)
|
pbool (Text.toLower . Text.strip -> w)
|
||||||
@ -98,7 +88,12 @@ getHealthInterfaceR (dropWhile (=="force") -> ris) = do -- for bac
|
|||||||
|
|
||||||
runInterfaceLogTable :: ReqBanInterfaceHealth -> Handler (Bool, Bool, [(Text,Bool)], Widget)
|
runInterfaceLogTable :: ReqBanInterfaceHealth -> Handler (Bool, Bool, [(Text,Bool)], Widget)
|
||||||
runInterfaceLogTable interfs@(reqIfs,_) = do
|
runInterfaceLogTable interfs@(reqIfs,_) = do
|
||||||
(res, twgt) <- runDB $ mkInterfaceLogTable interfs
|
-- we abuse messageTooltip for colored icons here
|
||||||
|
msgSuccessTooltip <- messageI Success MsgMessageSuccess
|
||||||
|
-- msgWarningTooltip <- messageI Warning MsgMessageWarning
|
||||||
|
msgErrorTooltip <- messageI Error MsgMessageError
|
||||||
|
let flagError = messageTooltip . bool msgErrorTooltip msgSuccessTooltip
|
||||||
|
(res, twgt) <- runDB $ mkInterfaceLogTable flagError interfs
|
||||||
let missing = notNull [ifce | (UniqueInterfaceHealth ifce _subt _writ) <- reqIfs, ifce `notElem` (fst <$> res) ]
|
let missing = notNull [ifce | (UniqueInterfaceHealth ifce _subt _writ) <- reqIfs, ifce `notElem` (fst <$> res) ]
|
||||||
allok = all snd res
|
allok = all snd res
|
||||||
return (missing, allok, res, twgt)
|
return (missing, allok, res, twgt)
|
||||||
@ -106,14 +101,12 @@ runInterfaceLogTable interfs@(reqIfs,_) = do
|
|||||||
-- ihDebugShow :: Unique InterfaceHealth -> Text
|
-- ihDebugShow :: Unique InterfaceHealth -> Text
|
||||||
-- ihDebugShow (UniqueInterfaceHealth i s w) = "(" <> tshow i <> "," <> tshow s <> "," <> tshow w <> ")"
|
-- ihDebugShow (UniqueInterfaceHealth i s w) = "(" <> tshow i <> "," <> tshow s <> "," <> tshow w <> ")"
|
||||||
|
|
||||||
mkInterfaceLogTable :: ReqBanInterfaceHealth -> DB ([(Text,Bool)], Widget)
|
mkInterfaceLogTable :: (Bool -> Widget) -> ReqBanInterfaceHealth -> DB ([(Text,Bool)], Widget)
|
||||||
mkInterfaceLogTable interfs@(reqIfs, banIfs) = do
|
mkInterfaceLogTable flagError interfs@(reqIfs, banIfs) = do
|
||||||
-- $logWarnS "DEBUG" $ tshow ([ihDebugShow x | x<- reqIfs], [ihDebugShow x | x<- banIfs])
|
-- $logWarnS "DEBUG" $ tshow ([ihDebugShow x | x<- reqIfs], [ihDebugShow x | x<- banIfs])
|
||||||
flagError <- liftHandler $ do
|
void $ liftHandler $ timeoutHandler 42000001 $ runDB $ runInterfaceChecks interfs
|
||||||
void $ timeoutHandler 42000001 $ runDB $ runInterfaceChecks interfs -- ensure interface checkc are up to date
|
|
||||||
mkErrorFlag
|
|
||||||
now <- liftIO getCurrentTime
|
now <- liftIO getCurrentTime
|
||||||
dbTableDB ilvalidator DBTable{dbtColonnade=colonnade now flagError, ..}
|
dbTableDB ilvalidator DBTable{dbtColonnade=colonnade now, ..}
|
||||||
where
|
where
|
||||||
sanitize = text2AlphaNumPlus ['+','-','_','Ä','Ö','Ü','ß','ä','ö','ü']
|
sanitize = text2AlphaNumPlus ['+','-','_','Ä','Ö','Ü','ß','ä','ö','ü']
|
||||||
dbtIdent = "interface-log" :: Text
|
dbtIdent = "interface-log" :: Text
|
||||||
@ -122,16 +115,7 @@ mkInterfaceLogTable interfs@(reqIfs, banIfs) = do
|
|||||||
EL.on ( ilog E.^. InterfaceLogInterface E.=?. ihealth E.?. InterfaceHealthInterface
|
EL.on ( ilog E.^. InterfaceLogInterface E.=?. ihealth E.?. InterfaceHealthInterface
|
||||||
E.&&. ilog E.^. InterfaceLogSubtype E.=~. E.joinV (ihealth E.?. InterfaceHealthSubtype)
|
E.&&. ilog E.^. InterfaceLogSubtype E.=~. E.joinV (ihealth E.?. InterfaceHealthSubtype)
|
||||||
E.&&. ilog E.^. InterfaceLogWrite E.=~. E.joinV (ihealth E.?. InterfaceHealthWrite )
|
E.&&. ilog E.^. InterfaceLogWrite E.=~. E.joinV (ihealth E.?. InterfaceHealthWrite )
|
||||||
E.&&. E.notExists (do -- a more specific match does not exist
|
)
|
||||||
otherh <- E.from $ E.table @InterfaceHealth
|
|
||||||
E.where_ $ ilog E.^. InterfaceLogInterface E.==. otherh E.^. InterfaceHealthInterface
|
|
||||||
E.&&. ilog E.^. InterfaceLogSubtype E.=~. otherh E.^. InterfaceHealthSubtype
|
|
||||||
E.&&. ilog E.^. InterfaceLogWrite E.=~. otherh E.^. InterfaceHealthWrite
|
|
||||||
E.&&. ihealth E.?. InterfaceHealthHours E.!=. E.just (otherh E.^. InterfaceHealthHours)
|
|
||||||
E.&&. (E.isNothing (E.joinV $ ihealth E.?. InterfaceHealthSubtype)
|
|
||||||
E.||. E.isNothing (E.joinV $ ihealth E.?. InterfaceHealthWrite ))
|
|
||||||
)
|
|
||||||
)
|
|
||||||
let matchUIH crits = E.or
|
let matchUIH crits = E.or
|
||||||
[ E.and $ catMaybes
|
[ E.and $ catMaybes
|
||||||
[ ilog E.^. InterfaceLogInterface E.==. E.val (sanitize ifce) & Just
|
[ ilog E.^. InterfaceLogInterface E.==. E.val (sanitize ifce) & Just
|
||||||
@ -155,34 +139,32 @@ mkInterfaceLogTable interfs@(reqIfs, banIfs) = do
|
|||||||
-- E.where_ $ E.not_ (ilog E.^. InterfaceLogInterface E.==. E.val "LMS" E.&&. ilog E.^. InterfaceLogSubtype E.==. E.val (sanitize "F")) -- BAD All missing, except for "Printer" "F"
|
-- E.where_ $ E.not_ (ilog E.^. InterfaceLogInterface E.==. E.val "LMS" E.&&. ilog E.^. InterfaceLogSubtype E.==. E.val (sanitize "F")) -- BAD All missing, except for "Printer" "F"
|
||||||
-- E.where_ $ E.not_ $ E.parens (ilog E.^. InterfaceLogInterface E.==. E.val "LMS" E.&&. ilog E.^. InterfaceLogSubtype E.==. E.val (sanitize "F")) -- WORKS OKAY
|
-- E.where_ $ E.not_ $ E.parens (ilog E.^. InterfaceLogInterface E.==. E.val "LMS" E.&&. ilog E.^. InterfaceLogSubtype E.==. E.val (sanitize "F")) -- WORKS OKAY
|
||||||
-- E.where_ $ ilog E.^. InterfaceLogInterface E.!=. E.val "LMS" E.||. ilog E.^. InterfaceLogSubtype E.!=. E.val (sanitize "F") -- WORKS OKAY
|
-- E.where_ $ ilog E.^. InterfaceLogInterface E.!=. E.val "LMS" E.||. ilog E.^. InterfaceLogSubtype E.!=. E.val (sanitize "F") -- WORKS OKAY
|
||||||
let ihour = E.coalesceDefault [ihealth E.?. InterfaceHealthHours] (E.val defaultInterfaceWarnHours) -- if no default time is set, use a default instead
|
let ihour = E.coalesceDefault [ihealth E.?. InterfaceHealthHours] (E.val $ 3 * 24) -- if no default time is set, use 3 days instead
|
||||||
return (ilog, ihour)
|
return (ilog, ihour)
|
||||||
|
|
||||||
queryILog :: (E.SqlExpr (Entity InterfaceLog) `E.LeftOuterJoin` E.SqlExpr (Maybe (Entity InterfaceHealth))) -> E.SqlExpr (Entity InterfaceLog)
|
queryILog :: (E.SqlExpr (Entity InterfaceLog) `E.LeftOuterJoin` E.SqlExpr (Maybe (Entity InterfaceHealth))) -> E.SqlExpr (Entity InterfaceLog)
|
||||||
queryILog = $(E.sqlLOJproj 2 1)
|
queryILog = $(E.sqlLOJproj 2 1)
|
||||||
queryHealth :: (E.SqlExpr (Entity InterfaceLog) `E.LeftOuterJoin` E.SqlExpr (Maybe (Entity InterfaceHealth))) -> E.SqlExpr (Maybe (Entity InterfaceHealth))
|
|
||||||
queryHealth = $(E.sqlLOJproj 2 2)
|
|
||||||
resultILog :: Lens' (DBRow (Entity InterfaceLog, E.Value Int)) InterfaceLog
|
resultILog :: Lens' (DBRow (Entity InterfaceLog, E.Value Int)) InterfaceLog
|
||||||
resultILog = _dbrOutput . _1 . _entityVal
|
resultILog = _dbrOutput . _1 . _entityVal
|
||||||
resultHours :: Lens' (DBRow (Entity InterfaceLog, E.Value Int)) Int
|
resultHours :: Lens' (DBRow (Entity InterfaceLog, E.Value Int)) Int
|
||||||
resultHours = _dbrOutput . _2 . E._unValue
|
resultHours = _dbrOutput . _2 . E._unValue
|
||||||
|
|
||||||
dbtRowKey = queryILog >>> (E.^.InterfaceLogId)
|
dbtRowKey = queryILog >>> (E.^.InterfaceLogId)
|
||||||
colonnade now flagError = mconcat
|
colonnade now = mconcat
|
||||||
[ sortable Nothing (i18nCell MsgInterfaceStatus) $ \row -> -- do
|
[ sortable Nothing (i18nCell MsgInterfaceStatus) $ \row -> -- do
|
||||||
let hours = row ^. resultHours
|
let hours = row ^. resultHours
|
||||||
-- defmsg = row ^? resultErrMsg
|
-- defmsg = row ^? resultErrMsg
|
||||||
logtime = row ^. resultILog . _interfaceLogTime
|
logtime = row ^. resultILog . _interfaceLogTime
|
||||||
success = row ^. resultILog . _interfaceLogSuccess
|
success = row ^. resultILog . _interfaceLogSuccess
|
||||||
iface = row ^. resultILog . _interfaceLogInterface
|
iface = row ^. resultILog . _interfaceLogInterface
|
||||||
status = (success || hours <= -100) && (hours < 0 || now <= addHours hours logtime)
|
status = success && now <= addHours hours logtime
|
||||||
in tellCell [(iface,status)] $ wgtCell $ flagError $ toMaybe (success || not status) status
|
in tellCell [(iface,status)] $
|
||||||
|
wgtCell $ flagError status
|
||||||
, sortable (Just "interface") (i18nCell MsgInterfaceName ) $ \(view (resultILog . _interfaceLogInterface) -> n) -> textCell n
|
, sortable (Just "interface") (i18nCell MsgInterfaceName ) $ \(view (resultILog . _interfaceLogInterface) -> n) -> textCell n
|
||||||
, sortable (Just "subtype") (i18nCell MsgInterfaceSubtype ) $ textCell . view (resultILog . _interfaceLogSubtype)
|
, sortable (Just "subtype") (i18nCell MsgInterfaceSubtype ) $ textCell . view (resultILog . _interfaceLogSubtype)
|
||||||
, sortable (Just "write") (i18nCell MsgInterfaceWrite ) $ (`ifIconCell` IconEdit) . view (resultILog . _interfaceLogWrite)
|
, sortable (Just "write") (i18nCell MsgInterfaceWrite ) $ (`ifIconCell` IconEdit) . view (resultILog . _interfaceLogWrite)
|
||||||
, sortable (Just "time") (i18nCell MsgInterfaceLastSynch ) $ dateTimeCell . view (resultILog . _interfaceLogTime)
|
, sortable (Just "time") (i18nCell MsgInterfaceLastSynch ) $ dateTimeCell . view (resultILog . _interfaceLogTime)
|
||||||
, sortable (Just "hours") (i18nCell MsgInterfaceFreshness & cellTooltips [SomeMessage MsgInterfaceFreshnessTooltip, SomeMessage MsgTableDiffDaysTooltip]
|
, sortable Nothing (i18nCell MsgInterfaceFreshness ) $ numCell . view resultHours
|
||||||
) $ warnIntervalCell . view resultHours
|
|
||||||
, sortable (Just "rows") (i18nCell MsgTableRows ) $ cellMaybe numCell . view (resultILog . _interfaceLogRows)
|
, sortable (Just "rows") (i18nCell MsgTableRows ) $ cellMaybe numCell . view (resultILog . _interfaceLogRows)
|
||||||
, sortable (Just "success") (i18nCell MsgInterfaceSuccess ) $ \(view (resultILog . _interfaceLogSuccess) -> s) -> iconBoolCell s
|
, sortable (Just "success") (i18nCell MsgInterfaceSuccess ) $ \(view (resultILog . _interfaceLogSuccess) -> s) -> iconBoolCell s
|
||||||
, sortable Nothing (i18nCell MsgInterfaceInfo ) $ \(view resultILog -> ilt) -> case ilt of
|
, sortable Nothing (i18nCell MsgInterfaceInfo ) $ \(view resultILog -> ilt) -> case ilt of
|
||||||
@ -198,7 +180,6 @@ mkInterfaceLogTable interfs@(reqIfs, banIfs) = do
|
|||||||
, singletonMap "time" $ SortColumn $ queryILog >>> (E.^. InterfaceLogTime)
|
, singletonMap "time" $ SortColumn $ queryILog >>> (E.^. InterfaceLogTime)
|
||||||
, singletonMap "rows" $ SortColumn $ queryILog >>> (E.^. InterfaceLogRows)
|
, singletonMap "rows" $ SortColumn $ queryILog >>> (E.^. InterfaceLogRows)
|
||||||
, singletonMap "success" $ SortColumn $ queryILog >>> (E.^. InterfaceLogSuccess)
|
, singletonMap "success" $ SortColumn $ queryILog >>> (E.^. InterfaceLogSuccess)
|
||||||
, singletonMap "hours" $ SortColumn $ \r -> E.coalesceDefault [queryHealth r E.?. InterfaceHealthHours] (E.val defaultInterfaceWarnHours)
|
|
||||||
]
|
]
|
||||||
ilvalidator = def & defaultSorting [SortAscBy "interface", SortAscBy "subtype", SortAscBy "write"]
|
ilvalidator = def & defaultSorting [SortAscBy "interface", SortAscBy "subtype", SortAscBy "write"]
|
||||||
dbtFilter = mempty
|
dbtFilter = mempty
|
||||||
@ -268,135 +249,3 @@ avsInterfaceCheck interfs = maybeRunCheck interfs (UniqueInterfaceHealth "AVS" (
|
|||||||
-- lastOk <- userAvsLastSynch . entityVal <<$>> selectFirst [UserAvsLastSynchError ==. Nothing] [Desc UserAvsLastSynch]
|
-- lastOk <- userAvsLastSynch . entityVal <<$>> selectFirst [UserAvsLastSynchError ==. Nothing] [Desc UserAvsLastSynch]
|
||||||
writeAvsSynchStats Nothing =<< mkBadInfo badRows badTime
|
writeAvsSynchStats Nothing =<< mkBadInfo badRows badTime
|
||||||
_ -> return ()
|
_ -> return ()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
data IWTableAction
|
|
||||||
= IWTActAdd
|
|
||||||
| IWTActDelete
|
|
||||||
deriving (Eq, Ord, Enum, Bounded, Read, Show, Generic)
|
|
||||||
|
|
||||||
instance Universe IWTableAction
|
|
||||||
instance Finite IWTableAction
|
|
||||||
nullaryPathPiece ''IWTableAction $ camelToPathPiece' 2
|
|
||||||
embedRenderMessage ''UniWorX ''IWTableAction id
|
|
||||||
|
|
||||||
data IWTableActionData
|
|
||||||
= IWTActAddData
|
|
||||||
{ iwtActInterface :: Text
|
|
||||||
, iwtActSubtype :: Maybe Text
|
|
||||||
, iwtActWrite :: Maybe Bool
|
|
||||||
, iwtActHours :: Int
|
|
||||||
}
|
|
||||||
| IWTActDeleteData
|
|
||||||
deriving (Eq, Ord, Read, Show, Generic)
|
|
||||||
|
|
||||||
type IWTableExpr = E.SqlExpr (Entity InterfaceHealth)
|
|
||||||
|
|
||||||
queryInterfaceHealth :: IWTableExpr -> E.SqlExpr (Entity InterfaceHealth)
|
|
||||||
queryInterfaceHealth = id
|
|
||||||
|
|
||||||
type IWTableData = DBRow (Entity InterfaceHealth)
|
|
||||||
|
|
||||||
resultInterfaceHealth :: Lens' IWTableData (Entity InterfaceHealth)
|
|
||||||
resultInterfaceHealth = _dbrOutput
|
|
||||||
|
|
||||||
wildcardCell :: IsDBTable m b => (a -> DBCell m b) -> Maybe a -> DBCell m b
|
|
||||||
wildcardCell _ Nothing = iconFixedCell $ icon IconWildcard
|
|
||||||
wildcardCell c (Just x) = c x
|
|
||||||
|
|
||||||
mkInterfaceWarnTable :: DB (FormResult (IWTableActionData, Set InterfaceHealthId), Widget)
|
|
||||||
mkInterfaceWarnTable = do
|
|
||||||
let
|
|
||||||
mkOption :: E.Value Text -> Option Text
|
|
||||||
mkOption (E.unValue -> t) = Option{ optionDisplay = t, optionInternalValue = t, optionExternalValue = toPathPiece t }
|
|
||||||
getSuggestion pj = E.select $ E.distinct $ do
|
|
||||||
il <- E.from $ E.table @InterfaceLog
|
|
||||||
let res = il E.^. pj
|
|
||||||
E.orderBy [E.asc res]
|
|
||||||
pure res
|
|
||||||
suggestionInterface :: HandlerFor UniWorX (OptionList Text)
|
|
||||||
suggestionInterface = mkOptionList . fmap mkOption <$> runDB (getSuggestion InterfaceLogInterface)
|
|
||||||
suggestionSubtype :: HandlerFor UniWorX (OptionList Text)
|
|
||||||
suggestionSubtype = mkOptionList . fmap mkOption <$> runDB (getSuggestion InterfaceLogSubtype)
|
|
||||||
dbtIdent = "interface-warnings" :: Text
|
|
||||||
dbtSQLQuery :: IWTableExpr -> E.SqlQuery IWTableExpr
|
|
||||||
dbtSQLQuery = return
|
|
||||||
dbtRowKey = queryInterfaceHealth >>> (E.^. InterfaceHealthId)
|
|
||||||
dbtProj = dbtProjId
|
|
||||||
dbtColonnade = formColonnade $ mconcat
|
|
||||||
[ dbSelect (applying _2) id (return . view (resultInterfaceHealth . _entityKey))
|
|
||||||
, sortable (Just "interface") (i18nCell MsgInterfaceName ) $ \(view (resultInterfaceHealth . _entityVal . _interfaceHealthInterface) -> n) -> textCell n
|
|
||||||
, sortable (Just "subtype") (i18nCell MsgInterfaceSubtype ) $ wildcardCell textCell . view (resultInterfaceHealth . _entityVal . _interfaceHealthSubtype )
|
|
||||||
, sortable (Just "write") (i18nCell MsgInterfaceWrite ) $ wildcardCell (iconFixedCell . iconWriteReadOnly) . view (resultInterfaceHealth . _entityVal . _interfaceHealthWrite )
|
|
||||||
-- , sortable (Just "hours") (i18nCell MsgInterfaceFreshness ) $ numCell . view (resultInterfaceHealth . _entityVal . _interfaceHealthHours )
|
|
||||||
, sortable (Just "hours") (i18nCell MsgInterfaceFreshness
|
|
||||||
& cellTooltip MsgTableDiffDaysTooltip ) $ warnIntervalCell . view (resultInterfaceHealth . _entityVal . _interfaceHealthHours )
|
|
||||||
]
|
|
||||||
dbtSorting = mconcat
|
|
||||||
[ singletonMap "interface" $ SortColumn $ queryInterfaceHealth >>> (E.^. InterfaceHealthInterface)
|
|
||||||
, singletonMap "subtype" $ SortColumn $ queryInterfaceHealth >>> (E.^. InterfaceHealthSubtype)
|
|
||||||
, singletonMap "write" $ SortColumn $ queryInterfaceHealth >>> (E.^. InterfaceHealthWrite)
|
|
||||||
, singletonMap "hours" $ SortColumn $ queryInterfaceHealth >>> (E.^. InterfaceHealthHours)
|
|
||||||
]
|
|
||||||
dbtFilter = mempty
|
|
||||||
dbtFilterUI = mempty
|
|
||||||
dbtStyle = def { dbsFilterLayout = defaultDBSFilterLayout }
|
|
||||||
dbtParams = DBParamsForm
|
|
||||||
{ dbParamsFormMethod = POST
|
|
||||||
, dbParamsFormAction = Nothing -- Just $ SomeRoute currentRoute
|
|
||||||
, dbParamsFormAttrs = []
|
|
||||||
, dbParamsFormSubmit = FormSubmit
|
|
||||||
, dbParamsFormAdditional
|
|
||||||
= let acts :: Map IWTableAction (AForm Handler IWTableActionData)
|
|
||||||
acts = mconcat
|
|
||||||
[ singletonMap IWTActAdd $ IWTActAddData
|
|
||||||
<$> apreq (textField & cfStrip & addDatalist suggestionInterface) (fslI MsgInterfaceName) Nothing
|
|
||||||
<*> aopt (textField & cfStrip & addDatalist suggestionSubtype) (fslI MsgInterfaceSubtype) Nothing
|
|
||||||
<*> aopt boolField' (fslI MsgInterfaceWrite) Nothing
|
|
||||||
<*> apreq intField (fslI MsgInterfaceFreshness & setTooltip MsgHours) Nothing
|
|
||||||
, singletonMap IWTActDelete $ pure IWTActDeleteData
|
|
||||||
]
|
|
||||||
in renderAForm FormStandard
|
|
||||||
$ (, mempty) . First . Just
|
|
||||||
<$> multiActionA acts (fslI MsgTableAction) Nothing
|
|
||||||
, dbParamsFormEvaluate = liftHandler . runFormPost
|
|
||||||
, dbParamsFormResult = id
|
|
||||||
, dbParamsFormIdent = def
|
|
||||||
}
|
|
||||||
dbtCsvEncode = noCsvEncode
|
|
||||||
dbtCsvDecode = Nothing
|
|
||||||
dbtExtraReps = []
|
|
||||||
postprocess :: FormResult (First IWTableActionData, DBFormResult InterfaceHealthId Bool IWTableData)
|
|
||||||
-> FormResult ( IWTableActionData, Set InterfaceHealthId)
|
|
||||||
postprocess inp = do
|
|
||||||
(First (Just act), jobMap) <- inp
|
|
||||||
let jobSet = Map.keysSet . Map.filter id $ getDBFormResult (const False) jobMap
|
|
||||||
return (act, jobSet)
|
|
||||||
psValidator = def & defaultSorting [SortAscBy "interface", SortAscBy "subtype", SortAscBy "write"]
|
|
||||||
over _1 postprocess <$> dbTable psValidator DBTable{..}
|
|
||||||
|
|
||||||
getConfigInterfacesR, postConfigInterfacesR :: Handler Html
|
|
||||||
getConfigInterfacesR = postConfigInterfacesR
|
|
||||||
postConfigInterfacesR = do
|
|
||||||
((interfaceOks, interfaceTable), (warnRes, configTable)) <- runDB $ (,)
|
|
||||||
<$> mkInterfaceLogTable mempty
|
|
||||||
<*> mkInterfaceWarnTable
|
|
||||||
let interfacesBadNr = length $ filter (not . snd) interfaceOks
|
|
||||||
formResult warnRes $ \case
|
|
||||||
(IWTActAddData{..}, _) -> do
|
|
||||||
void $ runDB $ upsertBy
|
|
||||||
(UniqueInterfaceHealth iwtActInterface iwtActSubtype iwtActWrite)
|
|
||||||
( InterfaceHealth iwtActInterface iwtActSubtype iwtActWrite iwtActHours)
|
|
||||||
[InterfaceHealthHours =. iwtActHours]
|
|
||||||
addMessageI Success MsgInterfaceWarningAdded
|
|
||||||
reloadKeepGetParams ConfigInterfacesR
|
|
||||||
(IWTActDeleteData, ihids) -> do
|
|
||||||
runDB $ mapM_ delete ihids
|
|
||||||
addMessageI Success $ MsgInterfaceWarningDeleted $ Set.size ihids
|
|
||||||
reloadKeepGetParams ConfigInterfacesR
|
|
||||||
|
|
||||||
siteLayoutMsg MsgConfigInterfacesHeading $ do
|
|
||||||
setTitleI MsgConfigInterfacesHeading
|
|
||||||
let defWarnTime = formatDiffHours defaultInterfaceWarnHours
|
|
||||||
$(i18nWidgetFile "config-interfaces")
|
|
||||||
@ -1,4 +1,4 @@
|
|||||||
-- SPDX-FileCopyrightText: 2022-24 Sarah Vaupel <sarah.vaupel@ifi.lmu.de>,Steffen Jost <s.jost@fraport.de>,Steffen Jost <jost@tcs.ifi.lmu.de>
|
-- SPDX-FileCopyrightText: 2022-23 Sarah Vaupel <sarah.vaupel@ifi.lmu.de>,Steffen Jost <s.jost@fraport.de>,Steffen Jost <jost@tcs.ifi.lmu.de>
|
||||||
--
|
--
|
||||||
-- SPDX-License-Identifier: AGPL-3.0-or-later
|
-- SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
@ -19,7 +19,7 @@ module Handler.LMS
|
|||||||
, getLmsFakeR , postLmsFakeR
|
, getLmsFakeR , postLmsFakeR
|
||||||
, getLmsUserR
|
, getLmsUserR
|
||||||
, getLmsUserSchoolR
|
, getLmsUserSchoolR
|
||||||
, getLmsUserAllR
|
, getLmsUserAllR
|
||||||
)
|
)
|
||||||
where
|
where
|
||||||
|
|
||||||
@ -81,11 +81,11 @@ postLmsAllR = do
|
|||||||
mbBtnForm <- if not isAdmin then return Nothing else do
|
mbBtnForm <- if not isAdmin then return Nothing else do
|
||||||
((btnResult, btnWdgt), btnEnctype) <- runFormPost $ identifyForm ("buttons" :: Text) (buttonForm :: Form ButtonManualLms)
|
((btnResult, btnWdgt), btnEnctype) <- runFormPost $ identifyForm ("buttons" :: Text) (buttonForm :: Form ButtonManualLms)
|
||||||
case btnResult of
|
case btnResult of
|
||||||
(FormSuccess BtnLmsEnqueue) ->
|
(FormSuccess BtnLmsEnqueue) ->
|
||||||
queueJob' JobLmsQualificationsEnqueue
|
queueJob' JobLmsQualificationsEnqueue
|
||||||
>> addMessage Info "Einreihung ablaufender Qualifikationen zum LMS wird nun im Hintergund durchgeführt."
|
>> addMessage Info "Einreihung ablaufender Qualifikationen zum LMS wird nun im Hintergund durchgeführt."
|
||||||
(FormSuccess BtnLmsDequeue) ->
|
(FormSuccess BtnLmsDequeue) ->
|
||||||
queueJob' JobLmsQualificationsDequeue
|
queueJob' JobLmsQualificationsDequeue
|
||||||
>> addMessage Info "Benachrichtigung abgelaufener Qualifikationen und Aufräumen beendeter LMS Nutzer wird im Hintergund ausgeführt."
|
>> addMessage Info "Benachrichtigung abgelaufener Qualifikationen und Aufräumen beendeter LMS Nutzer wird im Hintergund ausgeführt."
|
||||||
FormMissing -> return ()
|
FormMissing -> return ()
|
||||||
_other -> addMessage Warning "Kein korrekter LMS Knopf erkannt"
|
_other -> addMessage Warning "Kein korrekter LMS Knopf erkannt"
|
||||||
@ -112,20 +112,20 @@ resultAllQualificationActive = _dbrOutput . _2 . _unValue
|
|||||||
resultAllQualificationTotal :: Lens' AllQualificationTableData Word64
|
resultAllQualificationTotal :: Lens' AllQualificationTableData Word64
|
||||||
resultAllQualificationTotal = _dbrOutput . _3 . _unValue
|
resultAllQualificationTotal = _dbrOutput . _3 . _unValue
|
||||||
|
|
||||||
|
|
||||||
mkLmsAllTable :: Bool -> Int -> DB (Any, Widget)
|
mkLmsAllTable :: Bool -> Int -> DB (Any, Widget)
|
||||||
mkLmsAllTable isAdmin lmsDeletionDays = do
|
mkLmsAllTable isAdmin lmsDeletionDays = do
|
||||||
svs <- getSupervisees
|
svs <- getSupervisees
|
||||||
let
|
let
|
||||||
resultDBTable = DBTable{..}
|
resultDBTable = DBTable{..}
|
||||||
where
|
where
|
||||||
dbtSQLQuery quali = do
|
dbtSQLQuery quali = do
|
||||||
let filterSvs luser = luser Ex.^. LmsUserQualification Ex.==. quali Ex.^. QualificationId
|
let filterSvs luser = luser Ex.^. LmsUserQualification Ex.==. quali Ex.^. QualificationId
|
||||||
Ex.&&. (E.val isAdmin E.||. luser Ex.^. LmsUserUser `Ex.in_` E.vals svs)
|
Ex.&&. (E.val isAdmin E.||. luser Ex.^. LmsUserUser `Ex.in_` E.vals svs)
|
||||||
cusers = Ex.subSelectCount $ do
|
cusers = Ex.subSelectCount $ do
|
||||||
luser <- Ex.from $ Ex.table @LmsUser
|
luser <- Ex.from $ Ex.table @LmsUser
|
||||||
Ex.where_ $ filterSvs luser
|
Ex.where_ $ filterSvs luser
|
||||||
cactive = Ex.subSelectCount $ do
|
cactive = Ex.subSelectCount $ do
|
||||||
luser <- Ex.from $ Ex.table @LmsUser
|
luser <- Ex.from $ Ex.table @LmsUser
|
||||||
Ex.where_ $ filterSvs luser Ex.&&. E.isNothing (luser E.^. LmsUserStatus)
|
Ex.where_ $ filterSvs luser Ex.&&. E.isNothing (luser E.^. LmsUserStatus)
|
||||||
-- Failed attempt using Join/GroupBy instead of subselect: see branch csv-osis-demo-groupby-problem
|
-- Failed attempt using Join/GroupBy instead of subselect: see branch csv-osis-demo-groupby-problem
|
||||||
@ -149,29 +149,21 @@ mkLmsAllTable isAdmin lmsDeletionDays = do
|
|||||||
, sortable Nothing (i18nCell MsgQualificationRefreshWithin & cellTooltips [SomeMessage MsgQualificationRefreshWithinTooltip , SomeMessage MsgTableDiffDaysTooltip]) $
|
, sortable Nothing (i18nCell MsgQualificationRefreshWithin & cellTooltips [SomeMessage MsgQualificationRefreshWithinTooltip , SomeMessage MsgTableDiffDaysTooltip]) $
|
||||||
foldMap (textCell . formatCalendarDiffDays ) . view (resultAllQualification . _qualificationRefreshWithin)
|
foldMap (textCell . formatCalendarDiffDays ) . view (resultAllQualification . _qualificationRefreshWithin)
|
||||||
-- , sortable Nothing (i18nCell MsgQualificationRefreshWithin) $ foldMap textCell . view (resultAllQualification . _qualificationRefreshWithin . to formatCalendarDiffDays) -- does not work, since there is a maybe in between
|
-- , sortable Nothing (i18nCell MsgQualificationRefreshWithin) $ foldMap textCell . view (resultAllQualification . _qualificationRefreshWithin . to formatCalendarDiffDays) -- does not work, since there is a maybe in between
|
||||||
, sortable (Just "qelearning") (i18nCell MsgTableLmsElearning & cellTooltip MsgQualificationElearningStart) $ \row ->
|
|
||||||
let elearnstart = row ^. resultAllQualification . _qualificationElearningStart
|
|
||||||
reminder = row ^. resultAllQualification . _qualificationRefreshReminder
|
|
||||||
in tickmarkCell $ elearnstart && isJust reminder
|
|
||||||
, sortable Nothing (i18nCell MsgQualificationRefreshReminder & cellTooltips [SomeMessage MsgQualificationRefreshReminderTooltip, SomeMessage MsgTableDiffDaysTooltip]) $
|
, sortable Nothing (i18nCell MsgQualificationRefreshReminder & cellTooltips [SomeMessage MsgQualificationRefreshReminderTooltip, SomeMessage MsgTableDiffDaysTooltip]) $
|
||||||
foldMap (textCell . formatCalendarDiffDays ) . view (resultAllQualification . _qualificationRefreshReminder)
|
foldMap (textCell . formatCalendarDiffDays ) . view (resultAllQualification . _qualificationRefreshReminder)
|
||||||
, sortable Nothing (i18nCell MsgQualificationAuditDuration & cellTooltips [SomeMessage (MsgQualificationAuditDurationTooltip lmsDeletionDays), SomeMessage MsgTableDiffDaysTooltip]) $
|
, sortable Nothing (i18nCell MsgQualificationAuditDuration & cellTooltips [SomeMessage (MsgQualificationAuditDurationTooltip lmsDeletionDays), SomeMessage MsgTableDiffDaysTooltip]) $
|
||||||
foldMap (textCell . formatCalendarDiffDays . fromMonths) . view (resultAllQualification . _qualificationAuditDuration)
|
foldMap (textCell . formatCalendarDiffDays . fromMonths) . view (resultAllQualification . _qualificationAuditDuration)
|
||||||
, sortable (Just "qel-renew") (i18nCell MsgTableLmsElearningRenews & cellTooltip MsgQualificationElearningRenew)
|
, sortable (Just "qelearning") (i18nCell MsgTableLmsElearning & cellTooltip MsgQualificationElearningStart)
|
||||||
$ tickmarkCell . view (resultAllQualification . _qualificationElearningRenews)
|
$ tickmarkCell . view (resultAllQualification . _qualificationElearningStart)
|
||||||
, sortable (Just "qel-limit") (i18nCell MsgTableLmsElearningLimit & cellTooltip MsgQualificationElearningLimit)
|
, sortable Nothing (i18nCell MsgTableQualificationIsAvsLicence & cellTooltip MsgTableQualificationIsAvsLicenceTooltip)
|
||||||
$ cellMaybe numCell . view (resultAllQualification . _qualificationElearningLimit)
|
|
||||||
, sortable (Just "qel-reuse") (i18nCell MsgTableQualificationLmsReuses & cellTooltip MsgTableQualificationLmsReusesTooltip)
|
|
||||||
$ \(view (resultAllQualification . _qualificationLmsReuses) -> reuseQid) -> maybeCell reuseQid qualificationIdShortCell
|
|
||||||
, sortable Nothing (i18nCell MsgTableQualificationIsAvsLicence & cellTooltip MsgTableQualificationIsAvsLicenceTooltip)
|
|
||||||
$ \(view (resultAllQualification . _qualificationAvsLicence) -> licence) -> maybeCell licence $ textCell . T.singleton . licence2char
|
$ \(view (resultAllQualification . _qualificationAvsLicence) -> licence) -> maybeCell licence $ textCell . T.singleton . licence2char
|
||||||
, sortable Nothing (i18nCell MsgTableQualificationSapExport & cellTooltip MsgTableQualificationSapExportTooltip)
|
, sortable Nothing (i18nCell MsgTableQualificationSapExport & cellTooltip MsgTableQualificationSapExportTooltip)
|
||||||
$ \(view (resultAllQualification . _qualificationSapId) -> mbSapId) ->
|
$ \(view (resultAllQualification . _qualificationSapId) -> mbSapId) ->
|
||||||
let icn = IconOK -- change icon here, if desired
|
let icn = IconOK -- change icon here, if desired
|
||||||
in case mbSapId of
|
in case mbSapId of
|
||||||
Nothing -> mempty
|
Nothing -> mempty
|
||||||
Just sapId | isAdmin -> cellTooltipIcon (Just icn) (text2message sapId) mempty
|
Just sapId | isAdmin -> cellTooltipIcon (Just icn) (text2message sapId) mempty
|
||||||
Just _ -> iconCell icn
|
Just _ -> iconCell icn
|
||||||
, adminable Nothing (i18nCell MsgTableQualificationCountActive & cellTooltip MsgTableQualificationCountActiveTooltip)
|
, adminable Nothing (i18nCell MsgTableQualificationCountActive & cellTooltip MsgTableQualificationCountActiveTooltip)
|
||||||
$ \(view resultAllQualificationActive -> n) -> wgtCell $ word2widget n
|
$ \(view resultAllQualificationActive -> n) -> wgtCell $ word2widget n
|
||||||
, adminable Nothing (i18nCell MsgTableQualificationCountTotal) $ wgtCell . word2widget . view resultAllQualificationTotal
|
, adminable Nothing (i18nCell MsgTableQualificationCountTotal) $ wgtCell . word2widget . view resultAllQualificationTotal
|
||||||
@ -183,9 +175,6 @@ mkLmsAllTable isAdmin lmsDeletionDays = do
|
|||||||
, singletonMap "qshort" $ SortColumn (E.^. QualificationShorthand)
|
, singletonMap "qshort" $ SortColumn (E.^. QualificationShorthand)
|
||||||
, singletonMap "qname" $ SortColumn (E.^. QualificationName)
|
, singletonMap "qname" $ SortColumn (E.^. QualificationName)
|
||||||
, singletonMap "qelearning" $ SortColumn (E.^. QualificationElearningStart)
|
, singletonMap "qelearning" $ SortColumn (E.^. QualificationElearningStart)
|
||||||
, singletonMap "qel-renew" $ SortColumn (E.^. QualificationElearningRenews)
|
|
||||||
, singletonMap "qel-limit" $ SortColumn (E.^. QualificationElearningLimit)
|
|
||||||
, singletonMap "qel-reuse" $ SortColumn (E.^. QualificationLmsReuses)
|
|
||||||
]
|
]
|
||||||
dbtFilter = mconcat
|
dbtFilter = mconcat
|
||||||
[
|
[
|
||||||
@ -220,6 +209,7 @@ data LmsTableCsv = LmsTableCsv -- L..T..C.. -> ltc..
|
|||||||
{ ltcDisplayName :: UserDisplayName
|
{ ltcDisplayName :: UserDisplayName
|
||||||
, ltcEmail :: UserEmail
|
, ltcEmail :: UserEmail
|
||||||
, ltcCompany :: Maybe Text
|
, ltcCompany :: Maybe Text
|
||||||
|
, ltcCompanyNumbers :: CsvSemicolonList Int
|
||||||
, ltcValidUntil :: Day
|
, ltcValidUntil :: Day
|
||||||
, ltcLastRefresh :: Day
|
, ltcLastRefresh :: Day
|
||||||
, ltcFirstHeld :: Day
|
, ltcFirstHeld :: Day
|
||||||
@ -241,7 +231,8 @@ ltcExample :: LmsTableCsv
|
|||||||
ltcExample = LmsTableCsv
|
ltcExample = LmsTableCsv
|
||||||
{ ltcDisplayName = "Max Mustermann"
|
{ ltcDisplayName = "Max Mustermann"
|
||||||
, ltcEmail = "m.mustermann@example.com"
|
, ltcEmail = "m.mustermann@example.com"
|
||||||
, ltcCompany = Just "Example Brothers LLC"
|
, ltcCompany = Just "Example Brothers LLC, SecondaryJobs Inc"
|
||||||
|
, ltcCompanyNumbers = CsvSemicolonList [27,69]
|
||||||
, ltcValidUntil = succ compDay
|
, ltcValidUntil = succ compDay
|
||||||
, ltcLastRefresh = compDay
|
, ltcLastRefresh = compDay
|
||||||
, ltcFirstHeld = pred $ pred compDay
|
, ltcFirstHeld = pred $ pred compDay
|
||||||
@ -283,7 +274,8 @@ instance CsvColumnsExplained LmsTableCsv where
|
|||||||
csvColumnsExplanations = genericCsvColumnsExplanations ltcOptions $ Map.fromList
|
csvColumnsExplanations = genericCsvColumnsExplanations ltcOptions $ Map.fromList
|
||||||
[ ('ltcDisplayName , SomeMessage MsgLmsUser)
|
[ ('ltcDisplayName , SomeMessage MsgLmsUser)
|
||||||
, ('ltcEmail , SomeMessage MsgTableLmsEmail)
|
, ('ltcEmail , SomeMessage MsgTableLmsEmail)
|
||||||
, ('ltcCompany , SomeMessage MsgTablePrimeCompany)
|
, ('ltcCompany , SomeMessage MsgTableCompanies)
|
||||||
|
, ('ltcCompanyNumbers , SomeMessage MsgTableCompanyNos)
|
||||||
, ('ltcValidUntil , SomeMessage MsgLmsQualificationValidUntil)
|
, ('ltcValidUntil , SomeMessage MsgLmsQualificationValidUntil)
|
||||||
, ('ltcLastRefresh , SomeMessage MsgTableQualificationLastRefresh)
|
, ('ltcLastRefresh , SomeMessage MsgTableQualificationLastRefresh)
|
||||||
, ('ltcFirstHeld , SomeMessage MsgTableQualificationFirstHeld)
|
, ('ltcFirstHeld , SomeMessage MsgTableQualificationFirstHeld)
|
||||||
@ -317,7 +309,7 @@ queryQualBlock :: LmsTableExpr -> E.SqlExpr (Maybe (Entity QualificationUserBloc
|
|||||||
queryQualBlock = $(sqlLOJproj 2 2)
|
queryQualBlock = $(sqlLOJproj 2 2)
|
||||||
|
|
||||||
|
|
||||||
type LmsTableData = DBRow (Entity QualificationUser, Entity User, Entity LmsUser, Maybe (Entity QualificationUserBlock), E.Value (Maybe [Maybe UTCTime]), E.Value (Maybe CompanyId), E.Value Bool)
|
type LmsTableData = DBRow (Entity QualificationUser, Entity User, Entity LmsUser, Maybe (Entity QualificationUserBlock), E.Value (Maybe [Maybe UTCTime]), [Entity UserCompany], E.Value Bool)
|
||||||
|
|
||||||
resultQualUser :: Lens' LmsTableData (Entity QualificationUser)
|
resultQualUser :: Lens' LmsTableData (Entity QualificationUser)
|
||||||
resultQualUser = _dbrOutput . _1
|
resultQualUser = _dbrOutput . _1
|
||||||
@ -334,8 +326,8 @@ resultQualBlock = _dbrOutput . _4 . _Just
|
|||||||
resultPrintAck :: Traversal' LmsTableData [Maybe UTCTime]
|
resultPrintAck :: Traversal' LmsTableData [Maybe UTCTime]
|
||||||
resultPrintAck = _dbrOutput . _5 . _unValue . _Just
|
resultPrintAck = _dbrOutput . _5 . _unValue . _Just
|
||||||
|
|
||||||
resultCompanyId :: Traversal' LmsTableData CompanyId
|
resultCompanyUser :: Lens' LmsTableData [Entity UserCompany]
|
||||||
resultCompanyId = _dbrOutput . _6 . _unValue . _Just
|
resultCompanyUser = _dbrOutput . _6
|
||||||
|
|
||||||
resultValidQualification :: Lens' LmsTableData Bool
|
resultValidQualification :: Lens' LmsTableData Bool
|
||||||
resultValidQualification = _dbrOutput . _7 . _unValue
|
resultValidQualification = _dbrOutput . _7 . _unValue
|
||||||
@ -350,7 +342,7 @@ instance HasEntity LmsTableData QualificationUser where
|
|||||||
hasEntity = resultQualUser
|
hasEntity = resultQualUser
|
||||||
|
|
||||||
instance HasQualificationUser LmsTableData where
|
instance HasQualificationUser LmsTableData where
|
||||||
hasQualificationUser = resultQualUser . _entityVal
|
hasQualificationUser = resultQualUser . _entityVal
|
||||||
|
|
||||||
data LmsTableAction = LmsActNotify
|
data LmsTableAction = LmsActNotify
|
||||||
| LmsActRenewNotify
|
| LmsActRenewNotify
|
||||||
@ -359,7 +351,7 @@ data LmsTableAction = LmsActNotify
|
|||||||
| LmsActRestart
|
| LmsActRestart
|
||||||
deriving (Eq, Ord, Enum, Bounded, Read, Show, Generic)
|
deriving (Eq, Ord, Enum, Bounded, Read, Show, Generic)
|
||||||
deriving anyclass (Universe, Finite)
|
deriving anyclass (Universe, Finite)
|
||||||
|
|
||||||
nullaryPathPiece ''LmsTableAction $ camelToPathPiece' 2
|
nullaryPathPiece ''LmsTableAction $ camelToPathPiece' 2
|
||||||
embedRenderMessage ''UniWorX ''LmsTableAction id
|
embedRenderMessage ''UniWorX ''LmsTableAction id
|
||||||
|
|
||||||
@ -368,12 +360,12 @@ data LmsTableActionData = LmsActNotifyData
|
|||||||
| LmsActRenewPinData -- no longer used
|
| LmsActRenewPinData -- no longer used
|
||||||
| LmsActResetData
|
| LmsActResetData
|
||||||
{ lmsActRestartExtend :: Maybe Integer
|
{ lmsActRestartExtend :: Maybe Integer
|
||||||
, lmsActRestartUnblock :: Maybe Bool
|
, lmsActRestartUnblock :: Maybe Bool
|
||||||
, lmsActRestartNotify :: Maybe Bool
|
, lmsActRestartNotify :: Maybe Bool
|
||||||
}
|
}
|
||||||
| LmsActRestartData
|
| LmsActRestartData
|
||||||
{ lmsActRestartExtend :: Maybe Integer
|
{ lmsActRestartExtend :: Maybe Integer
|
||||||
, lmsActRestartUnblock :: Maybe Bool
|
, lmsActRestartUnblock :: Maybe Bool
|
||||||
, lmsActRestartNotify :: Maybe Bool
|
, lmsActRestartNotify :: Maybe Bool
|
||||||
}
|
}
|
||||||
deriving (Eq, Ord, Read, Show, Generic)
|
deriving (Eq, Ord, Read, Show, Generic)
|
||||||
@ -403,7 +395,6 @@ lmsTableQuery :: UTCTime -> QualificationId -> LmsTableExpr
|
|||||||
, E.SqlExpr (Entity LmsUser)
|
, E.SqlExpr (Entity LmsUser)
|
||||||
, E.SqlExpr (Maybe (Entity QualificationUserBlock))
|
, E.SqlExpr (Maybe (Entity QualificationUserBlock))
|
||||||
, E.SqlExpr (E.Value (Maybe [Maybe UTCTime])) -- outer maybe indicates, whether a printJob exists, inner maybe indicates all acknowledged printJobs
|
, E.SqlExpr (E.Value (Maybe [Maybe UTCTime])) -- outer maybe indicates, whether a printJob exists, inner maybe indicates all acknowledged printJobs
|
||||||
, E.SqlExpr (E.Value (Maybe CompanyId))
|
|
||||||
, E.SqlExpr (E.Value Bool)
|
, E.SqlExpr (E.Value Bool)
|
||||||
)
|
)
|
||||||
lmsTableQuery now qid (qualUser `E.InnerJoin` user `E.InnerJoin` lmsUser `E.LeftOuterJoin` qualBlock) = do
|
lmsTableQuery now qid (qualUser `E.InnerJoin` user `E.InnerJoin` lmsUser `E.LeftOuterJoin` qualBlock) = do
|
||||||
@ -416,19 +407,15 @@ lmsTableQuery now qid (qualUser `E.InnerJoin` user `E.InnerJoin` lmsUser `E.Left
|
|||||||
E.on $ user E.^. UserId E.==. lmsUser E.^. LmsUserUser
|
E.on $ user E.^. UserId E.==. lmsUser E.^. LmsUserUser
|
||||||
E.&&. E.val qid E.==. lmsUser E.^. LmsUserQualification -- NOTE: condition was once erroneously placed in where-clause, which does not work
|
E.&&. E.val qid E.==. lmsUser E.^. LmsUserQualification -- NOTE: condition was once erroneously placed in where-clause, which does not work
|
||||||
E.on $ user E.^. UserId E.==. qualUser E.^. QualificationUserUser
|
E.on $ user E.^. UserId E.==. qualUser E.^. QualificationUserUser
|
||||||
E.where_ $ E.val qid E.==. qualUser E.^. QualificationUserQualification
|
E.where_ $ E.val qid E.==. qualUser E.^. QualificationUserQualification
|
||||||
-- Letztes Datum anzeigen, wenn mehrere, dann diese in klickbaren Tooltip verstecken!
|
-- Letztes Datum anzeigen, wenn mehrere, dann diese in klickbaren Tooltip verstecken!
|
||||||
let printAcknowledged = E.subSelectMaybe . E.from $ \pj -> do
|
let printAcknowledged = E.subSelectMaybe . E.from $ \pj -> do
|
||||||
E.where_ $ E.isJust (pj E.^. PrintJobLmsUser)
|
E.where_ $ E.isJust (pj E.^. PrintJobLmsUser)
|
||||||
E.&&. ((lmsUser E.^. LmsUserIdent) E.=?. (pj E.^. PrintJobLmsUser))
|
E.&&. ((lmsUser E.^. LmsUserIdent) E.=?. (pj E.^. PrintJobLmsUser))
|
||||||
let pjOrder = [E.desc $ pj E.^. PrintJobCreated, E.desc $ pj E.^. PrintJobAcknowledged] -- latest created comes first! This is assumed to be the case later on!
|
let pjOrder = [E.desc $ pj E.^. PrintJobCreated, E.desc $ pj E.^. PrintJobAcknowledged] -- latest created comes first! This is assumed to be the case later on!
|
||||||
pure $ --(E.arrayAggWith E.AggModeAll (pj E.^. PrintJobCreated ) pjOrder, -- return two aggregates only works with select, the restricted type of subSelect does not seem to support this!
|
pure $ --(E.arrayAggWith E.AggModeAll (pj E.^. PrintJobCreated ) pjOrder, -- return two aggregates only works with select, the restricted type of subSelect does not seem to support this!
|
||||||
E.arrayAggWith E.AggModeAll (pj E.^. PrintJobAcknowledged) pjOrder
|
E.arrayAggWith E.AggModeAll (pj E.^. PrintJobAcknowledged) pjOrder
|
||||||
primeComp = E.subSelect . E.from $ \uc -> do
|
return (qualUser, user, lmsUser, qualBlock, printAcknowledged, validQualification now qualUser)
|
||||||
E.where_ $ user E.^. UserId E.==. uc E.^. UserCompanyUser
|
|
||||||
E.orderBy [E.desc $ uc E.^. UserCompanyPriority, E.asc $ uc E.^. UserCompanyCompany]
|
|
||||||
return (uc E.^. UserCompanyCompany)
|
|
||||||
return (qualUser, user, lmsUser, qualBlock, printAcknowledged, primeComp, validQualification now qualUser)
|
|
||||||
|
|
||||||
|
|
||||||
mkLmsTable :: ( Functor h, ToSortable h
|
mkLmsTable :: ( Functor h, ToSortable h
|
||||||
@ -436,27 +423,26 @@ mkLmsTable :: ( Functor h, ToSortable h
|
|||||||
)
|
)
|
||||||
=> Bool
|
=> Bool
|
||||||
-> Entity Qualification
|
-> Entity Qualification
|
||||||
-> Map LmsTableAction (AForm Handler LmsTableActionData)
|
-> Map LmsTableAction (AForm Handler LmsTableActionData)
|
||||||
-> ((CompanyId -> CompanyName) -> cols)
|
-> (Map CompanyId Company -> cols)
|
||||||
-> PSValidator (MForm Handler) (FormResult (First LmsTableActionData, DBFormResult UserId Bool LmsTableData))
|
-> PSValidator (MForm Handler) (FormResult (First LmsTableActionData, DBFormResult UserId Bool LmsTableData))
|
||||||
-> DB (FormResult (LmsTableActionData, Set UserId), Widget)
|
-> DB (FormResult (LmsTableActionData, Set UserId), Widget)
|
||||||
mkLmsTable isAdmin (Entity qid quali) acts cols psValidator = do
|
mkLmsTable isAdmin (Entity qid quali) acts cols psValidator = do
|
||||||
now <- liftIO getCurrentTime
|
now <- liftIO getCurrentTime
|
||||||
-- lookup all companies
|
-- lookup all companies
|
||||||
cmpMap <- memcachedBy (Just . Right $ 15 * diffMinute) ("CompanyDictionary"::Text) $ do
|
cmpMap <- memcachedBy (Just . Right $ 5 * diffMinute) ("CompanyDictionary"::Text) $ do
|
||||||
cmps <- selectList [] [] -- [Asc CompanyShorthand]
|
cmps <- selectList [] [] -- [Asc CompanyShorthand]
|
||||||
return $ Map.fromList $ fmap (\Entity{..} -> (entityKey, entityVal)) cmps
|
return $ Map.fromList $ fmap (\Entity{..} -> (entityKey, entityVal)) cmps
|
||||||
let
|
let
|
||||||
getCompanyName :: CompanyId -> CompanyName
|
|
||||||
getCompanyName cid = maybe (unCompanyKey cid) companyName $ Map.lookup cid cmpMap -- use shorthand in case of impossible failure
|
|
||||||
|
|
||||||
csvName = T.replace " " "-" $ ciOriginal (quali ^. _qualificationName)
|
csvName = T.replace " " "-" $ ciOriginal (quali ^. _qualificationName)
|
||||||
dbtIdent :: Text
|
dbtIdent :: Text
|
||||||
dbtIdent = "lms"
|
dbtIdent = "lms"
|
||||||
dbtSQLQuery = lmsTableQuery now qid
|
dbtSQLQuery = lmsTableQuery now qid
|
||||||
dbtRowKey = queryUser >>> (E.^. UserId)
|
dbtRowKey = queryUser >>> (E.^. UserId)
|
||||||
dbtProj = dbtProjId
|
dbtProj = dbtProjSimple $ \(qualUsr, usr, lmsUsr, qUsrBlock, printAcks, validQ) -> do
|
||||||
dbtColonnade = cols getCompanyName
|
cmpUsr <- selectList [UserCompanyUser ==. entityKey usr] [Asc UserCompanyCompany]
|
||||||
|
return (qualUsr, usr, lmsUsr, qUsrBlock, printAcks, cmpUsr, validQ)
|
||||||
|
dbtColonnade = cols cmpMap
|
||||||
dbtSorting = mconcat
|
dbtSorting = mconcat
|
||||||
[ single $ sortUserNameLink queryUser
|
[ single $ sortUserNameLink queryUser
|
||||||
, single $ sortUserEmail queryUser
|
, single $ sortUserEmail queryUser
|
||||||
@ -500,37 +486,43 @@ mkLmsTable isAdmin (Entity qid quali) acts cols psValidator = do
|
|||||||
-- )
|
-- )
|
||||||
, single ("notified", FilterColumn . E.mkExactFilterLast $ views (to queryLmsUser) (E.isJust . (E.^. LmsUserNotified)))
|
, single ("notified", FilterColumn . E.mkExactFilterLast $ views (to queryLmsUser) (E.isJust . (E.^. LmsUserNotified)))
|
||||||
, single ("avs-number" , FilterColumn . E.mkExistsFilter $ \row criterion ->
|
, single ("avs-number" , FilterColumn . E.mkExistsFilter $ \row criterion ->
|
||||||
E.from $ \usrAvs -> -- do
|
E.from $ \usrAvs -> -- do
|
||||||
E.where_ $ usrAvs E.^. UserAvsUser E.==. queryUser row E.^. UserId
|
E.where_ $ usrAvs E.^. UserAvsUser E.==. queryUser row E.^. UserId
|
||||||
E.&&. ((E.val criterion :: E.SqlExpr (E.Value (CI Text))) E.==.
|
E.&&. ((E.val criterion :: E.SqlExpr (E.Value (CI Text))) E.==.
|
||||||
(E.explicitUnsafeCoerceSqlExprValue "citext" (usrAvs E.^. UserAvsNoPerson) :: E.SqlExpr (E.Value (CI Text))) ))
|
(E.explicitUnsafeCoerceSqlExprValue "citext" (usrAvs E.^. UserAvsNoPerson) :: E.SqlExpr (E.Value (CI Text))) ))
|
||||||
, single ("user-company", FilterColumn . E.mkExistsFilter $ \row criterion ->
|
, single ("user-company", FilterColumn . E.mkExistsFilter $ \row criterion ->
|
||||||
E.from $ \(usrComp `E.InnerJoin` comp) -> do
|
E.from $ \(usrComp `E.InnerJoin` comp) -> do
|
||||||
let testname = (E.val criterion :: E.SqlExpr (E.Value (CI Text))) `E.isInfixOf`
|
let testname = (E.val criterion :: E.SqlExpr (E.Value (CI Text))) `E.isInfixOf`
|
||||||
(E.explicitUnsafeCoerceSqlExprValue "citext" (comp E.^. CompanyName) :: E.SqlExpr (E.Value (CI Text)))
|
(E.explicitUnsafeCoerceSqlExprValue "citext" (comp E.^. CompanyName) :: E.SqlExpr (E.Value (CI Text)))
|
||||||
testnumber nr = E.val nr E.==. comp E.^. CompanyAvsId
|
testnumber nr = E.val nr E.==. comp E.^. CompanyAvsId
|
||||||
testcrit = maybe testname testnumber $ readMay $ ciOriginal criterion
|
testcrit = maybe testname testnumber $ readMay $ ciOriginal criterion
|
||||||
E.on $ usrComp E.^. UserCompanyCompany E.==. comp E.^. CompanyId
|
E.on $ usrComp E.^. UserCompanyCompany E.==. comp E.^. CompanyId
|
||||||
E.where_ $ usrComp E.^. UserCompanyUser E.==. queryUser row E.^. UserId E.&&. testcrit
|
E.where_ $ usrComp E.^. UserCompanyUser E.==. queryUser row E.^. UserId E.&&. testcrit
|
||||||
)
|
)
|
||||||
, fltrAVSCardNos queryUser
|
, single ("avs-card" , FilterColumn $ \(queryUser -> user) (criterion :: Set.Set Text) -> case readAvsFullCardNo =<< Set.lookupMin criterion of
|
||||||
|
Nothing -> E.false
|
||||||
|
Just cardNo -> E.exists $ E.from $ \(avsCard `E.InnerJoin` usrAvs) -> do
|
||||||
|
E.on $ usrAvs E.^. UserAvsPersonId E.==. avsCard E.^. UserAvsCardPersonId
|
||||||
|
E.where_ $ usrAvs E.^. UserAvsUser E.==. user E.^. UserId
|
||||||
|
E.&&. (avsCard E.^. UserAvsCardCardNo E.==. E.val cardNo)
|
||||||
|
)
|
||||||
, single ("personal-number", FilterColumn $ \(queryUser -> user) (criteria :: Set.Set Text) -> if
|
, single ("personal-number", FilterColumn $ \(queryUser -> user) (criteria :: Set.Set Text) -> if
|
||||||
| Set.null criteria -> E.true
|
| Set.null criteria -> E.true
|
||||||
| otherwise -> E.any (\c -> user E.^. UserCompanyPersonalNumber `E.hasInfix` E.val c) criteria
|
| otherwise -> E.any (\c -> user E.^. UserCompanyPersonalNumber `E.hasInfix` E.val c) criteria
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
dbtFilterUI mPrev = mconcat
|
dbtFilterUI mPrev = mconcat
|
||||||
[ fltrUserNameEmailHdrUI MsgLmsUser mPrev
|
[ fltrUserNameEmailHdrUI MsgLmsUser mPrev
|
||||||
, prismAForm (singletonFilter "user-company") mPrev $ aopt textField (fslI MsgTableCompany)
|
, prismAForm (singletonFilter "user-company") mPrev $ aopt textField (fslI MsgTableCompany)
|
||||||
, prismAForm (singletonFilter "personal-number" ) mPrev $ aopt textField (fslI MsgCompanyPersonalNumber)
|
, prismAForm (singletonFilter "personal-number" ) mPrev $ aopt textField (fslI MsgCompanyPersonalNumber)
|
||||||
, fltrAVSCardNosUI mPrev
|
, prismAForm (singletonFilter "avs-card" ) mPrev $ aopt textField (fslI MsgAvsCardNo)
|
||||||
, prismAForm (singletonFilter "avs-number" ) mPrev $ aopt textField (fslI MsgAvsPersonNo)
|
, prismAForm (singletonFilter "avs-number" ) mPrev $ aopt textField (fslI MsgAvsPersonNo)
|
||||||
, prismAForm (singletonFilter "ident" . maybePrism _PathPiece) mPrev $ aopt (hoistField lift textField) (fslI MsgTableLmsIdent)
|
, prismAForm (singletonFilter "ident" . maybePrism _PathPiece) mPrev $ aopt (hoistField lift textField) (fslI MsgTableLmsIdent)
|
||||||
, prismAForm (singletonFilter "validity" . maybePrism _PathPiece) mPrev $ aopt (boolField . Just $ SomeMessage MsgBoolIrrelevant) (fslI MsgFilterLmsValid)
|
, prismAForm (singletonFilter "validity" . maybePrism _PathPiece) mPrev $ aopt (boolField . Just $ SomeMessage MsgBoolIrrelevant) (fslI MsgFilterLmsValid)
|
||||||
, prismAForm (singletonFilter "notified" . maybePrism _PathPiece) mPrev $ aopt (boolField . Just $ SomeMessage MsgBoolIrrelevant) (fslI MsgFilterLmsNotified)
|
, prismAForm (singletonFilter "notified" . maybePrism _PathPiece) mPrev $ aopt (boolField . Just $ SomeMessage MsgBoolIrrelevant) (fslI MsgFilterLmsNotified)
|
||||||
, prismAForm (singletonFilter "status" . maybePrism _PathPiece) mPrev $ aopt (hoistField liftHandler (selectField optionsFinite) :: (Field _ (Maybe LmsStatus))) (fslI MsgTableLmsStatus)
|
, prismAForm (singletonFilter "status" . maybePrism _PathPiece) mPrev $ aopt (hoistField liftHandler (selectField optionsFinite) :: (Field _ (Maybe LmsStatus))) (fslI MsgTableLmsStatus)
|
||||||
-- , if isNothing mbRenewal then mempty
|
-- , if isNothing mbRenewal then mempty
|
||||||
-- else prismAForm (singletonFilter "renewal-due" . maybePrism _PathPiece) mPrev $ aopt checkBoxField (fslI MsgFilterLmsRenewal)
|
-- else prismAForm (singletonFilter "renewal-due" . maybePrism _PathPiece) mPrev $ aopt checkBoxField (fslI MsgFilterLmsRenewal)
|
||||||
]
|
]
|
||||||
dbtStyle = def { dbsFilterLayout = defaultDBSFilterLayout }
|
dbtStyle = def { dbsFilterLayout = defaultDBSFilterLayout }
|
||||||
dbtCsvEncode = Just DBTCsvEncode
|
dbtCsvEncode = Just DBTCsvEncode
|
||||||
@ -547,24 +539,29 @@ mkLmsTable isAdmin (Entity qid quali) acts cols psValidator = do
|
|||||||
doEncode' = LmsTableCsv
|
doEncode' = LmsTableCsv
|
||||||
<$> view (resultUser . _entityVal . _userDisplayName)
|
<$> view (resultUser . _entityVal . _userDisplayName)
|
||||||
<*> view (resultUser . _entityVal . _userDisplayEmail)
|
<*> view (resultUser . _entityVal . _userDisplayEmail)
|
||||||
<*> preview (resultCompanyId . to getCompanyName . _CI)
|
<*> (view resultCompanyUser >>= getCompanies)
|
||||||
<*> view (resultQualUser . _entityVal . _qualificationUserValidUntil)
|
<*> (view resultCompanyUser >>= getCompanyNos)
|
||||||
<*> view (resultQualUser . _entityVal . _qualificationUserLastRefresh)
|
<*> view (resultQualUser . _entityVal . _qualificationUserValidUntil)
|
||||||
<*> view (resultQualUser . _entityVal . _qualificationUserFirstHeld)
|
<*> view (resultQualUser . _entityVal . _qualificationUserLastRefresh)
|
||||||
|
<*> view (resultQualUser . _entityVal . _qualificationUserFirstHeld)
|
||||||
<*> preview (resultQualBlock . _entityVal . _qualificationUserBlockUnblock . _not)
|
<*> preview (resultQualBlock . _entityVal . _qualificationUserBlockUnblock . _not)
|
||||||
<*> preview (resultQualBlock . _entityVal . _qualificationUserBlockFrom)
|
<*> preview (resultQualBlock . _entityVal . _qualificationUserBlockFrom)
|
||||||
<*> view (resultLmsUser . _entityVal . _lmsUserIdent)
|
<*> view (resultLmsUser . _entityVal . _lmsUserIdent)
|
||||||
<*> view (resultLmsUser . _entityVal . _lmsUserStatus)
|
<*> view (resultLmsUser . _entityVal . _lmsUserStatus)
|
||||||
<*> view (resultLmsUser . _entityVal . _lmsUserStatusDay)
|
<*> view (resultLmsUser . _entityVal . _lmsUserStatusDay)
|
||||||
<*> view (resultLmsUser . _entityVal . _lmsUserStarted)
|
<*> view (resultLmsUser . _entityVal . _lmsUserStarted)
|
||||||
<*> view (resultLmsUser . _entityVal . _lmsUserDatePin)
|
<*> view (resultLmsUser . _entityVal . _lmsUserDatePin)
|
||||||
<*> view (resultLmsUser . _entityVal . _lmsUserReceived)
|
<*> view (resultLmsUser . _entityVal . _lmsUserReceived)
|
||||||
<*> view (resultLmsUser . _entityVal . _lmsUserNotified) -- TODO: only exports last email date / print job sending date, not print acknowledge
|
<*> view (resultLmsUser . _entityVal . _lmsUserNotified) -- TODO: only exports last email date / print job sending date, not print acknowledge
|
||||||
<*> view (resultLmsUser . _entityVal . _lmsUserEnded)
|
<*> view (resultLmsUser . _entityVal . _lmsUserEnded)
|
||||||
|
getCompanies cmps = case mapMaybe (flip Map.lookup cmpMap . view (_entityVal . _userCompanyCompany)) cmps of
|
||||||
|
[] -> pure Nothing
|
||||||
|
somecmps -> pure $ Just $ intercalate ", " $ fmap (view (_companyName . _CI)) somecmps
|
||||||
|
getCompanyNos = pure . CsvSemicolonList . mapMaybe (preview (_Just . _companyAvsId) . flip Map.lookup cmpMap . view (_entityVal . _userCompanyCompany))
|
||||||
|
|
||||||
dbtCsvDecode = Nothing
|
dbtCsvDecode = Nothing
|
||||||
dbtExtraReps = []
|
dbtExtraReps = []
|
||||||
dbtParams = if not isAdmin then def {dbParamsFormAction = Nothing, dbParamsFormSubmit = FormNoSubmit} else
|
dbtParams = if not isAdmin then def {dbParamsFormAction = Nothing, dbParamsFormSubmit = FormNoSubmit} else
|
||||||
DBParamsForm
|
DBParamsForm
|
||||||
{ dbParamsFormMethod = POST
|
{ dbParamsFormMethod = POST
|
||||||
, dbParamsFormAction = Nothing -- Just $ SomeRoute currentRoute
|
, dbParamsFormAction = Nothing -- Just $ SomeRoute currentRoute
|
||||||
@ -605,34 +602,37 @@ postLmsR sid qsh = do
|
|||||||
msgResetInfo <- messageIconI Info IconNotificationNonactive MsgLmsActResetInfo
|
msgResetInfo <- messageIconI Info IconNotificationNonactive MsgLmsActResetInfo
|
||||||
msgRestartWarning <- messageIconI Warning IconWarning MsgLmsActRestartWarning
|
msgRestartWarning <- messageIconI Warning IconWarning MsgLmsActRestartWarning
|
||||||
|
|
||||||
((lmsRes, lmsTable), Entity qid quali, lmsQualiReused) <- runDB $ do
|
((lmsRes, lmsTable), Entity qid quali) <- runDB $ do
|
||||||
qent@Entity{entityVal=Qualification{qualificationLmsReuses = reuseQuali}} <- getBy404 $ SchoolQualificationShort sid qsh
|
qent <- getBy404 $ SchoolQualificationShort sid qsh
|
||||||
lmsQualiReused <- traverseJoin get reuseQuali
|
|
||||||
let acts :: Map LmsTableAction (AForm Handler LmsTableActionData)
|
let acts :: Map LmsTableAction (AForm Handler LmsTableActionData)
|
||||||
acts = mconcat
|
acts = mconcat
|
||||||
[ singletonMap LmsActNotify $ pure LmsActNotifyData
|
[ singletonMap LmsActNotify $ pure LmsActNotifyData
|
||||||
, singletonMap LmsActRenewNotify $ pure LmsActRenewNotifyData
|
, singletonMap LmsActRenewNotify $ pure LmsActRenewNotifyData
|
||||||
-- , singletonMap LmsActRenewPin $ pure LmsActRenewPinData
|
-- , singletonMap LmsActRenewPin $ pure LmsActRenewPinData
|
||||||
, singletonMap LmsActReset $ LmsActResetData
|
, singletonMap LmsActReset $ LmsActResetData
|
||||||
<$> aopt intField (fslI MsgLmsActRestartExtend) Nothing
|
<$> aopt intField (fslI MsgLmsActRestartExtend) Nothing
|
||||||
<*> aopt checkBoxField (fslI MsgLmsActRestartUnblock) Nothing
|
<*> aopt checkBoxField (fslI MsgLmsActRestartUnblock) Nothing
|
||||||
<*> aopt checkBoxField (fslI MsgLmsActNotify) Nothing
|
<*> aopt checkBoxField (fslI MsgLmsActNotify) Nothing
|
||||||
<* aformMessage msgResetInfo
|
<* aformMessage msgResetInfo
|
||||||
, singletonMap LmsActRestart $ LmsActRestartData
|
, singletonMap LmsActRestart $ LmsActRestartData
|
||||||
<$> aopt intField (fslI MsgLmsActRestartExtend) Nothing
|
<$> aopt intField (fslI MsgLmsActRestartExtend) Nothing
|
||||||
<*> aopt checkBoxField (fslI MsgLmsActRestartUnblock) Nothing
|
<*> aopt checkBoxField (fslI MsgLmsActRestartUnblock) Nothing
|
||||||
<*> aopt checkBoxField (fslI MsgLmsActNotify) Nothing
|
<*> aopt checkBoxField (fslI MsgLmsActNotify) Nothing
|
||||||
-- <*> aopt (commentField MsgQualificationActBlockSupervisor) (fslI MsgMessageWarning) Nothing
|
-- <*> aopt (commentField MsgQualificationActBlockSupervisor) (fslI MsgMessageWarning) Nothing
|
||||||
<* aformMessage msgRestartWarning
|
<* aformMessage msgRestartWarning
|
||||||
]
|
]
|
||||||
colChoices getCompanyName = mconcat
|
colChoices cmpMap = mconcat
|
||||||
[ guardMonoid isAdmin $ dbSelect (applying _2) id (return . view (resultUser . _entityKey))
|
[ guardMonoid isAdmin $ dbSelect (applying _2) id (return . view (resultUser . _entityKey))
|
||||||
, colUserNameModalHdrAdmin MsgLmsUser AdminUserR
|
, colUserNameModalHdrAdmin MsgLmsUser AdminUserR
|
||||||
, colUserEmail
|
, colUserEmail
|
||||||
, sortable (Just "user-company") (i18nCell MsgTablePrimeCompany) $ \(preview resultCompanyId -> mcid) ->
|
, sortable (Just "user-company") (i18nCell MsgTableCompanies) $ \( view resultCompanyUser -> cmps) ->
|
||||||
maybeEmpty mcid $ \cid -> companyCell (unCompanyKey cid) (getCompanyName cid) False
|
let cs = [ companyCell (unCompanyKey cmpId) cmpName cmpSpr
|
||||||
|
| Entity _ UserCompany{userCompanyCompany=cmpId, userCompanySupervisor=cmpSpr} <- cmps
|
||||||
|
, let cmpName = maybe (unCompanyKey cmpId) companyName $ Map.lookup cmpId cmpMap
|
||||||
|
]
|
||||||
|
in intercalate spacerCell cs
|
||||||
, colUserMatriclenr isAdmin
|
, colUserMatriclenr isAdmin
|
||||||
-- , sortable (Just "validity") (i18nCell MsgQualificationValidIndicator) (qualificationValidIconCell nowaday . view resultQualUser)
|
-- , sortable (Just "validity") (i18nCell MsgQualificationValidIndicator) (qualificationValidIconCell nowaday . view resultQualUser)
|
||||||
, sortable (Just "first-held") (i18nCell MsgTableQualificationFirstHeld) $ \( view $ resultQualUser . _entityVal . _qualificationUserFirstHeld -> d) -> dayCell d
|
, sortable (Just "first-held") (i18nCell MsgTableQualificationFirstHeld) $ \( view $ resultQualUser . _entityVal . _qualificationUserFirstHeld -> d) -> dayCell d
|
||||||
, sortable (Just "last-refresh") (i18nCell MsgTableQualificationLastRefresh)$ \( view $ resultQualUser . _entityVal . _qualificationUserLastRefresh -> d) -> dayCell d
|
, sortable (Just "last-refresh") (i18nCell MsgTableQualificationLastRefresh)$ \( view $ resultQualUser . _entityVal . _qualificationUserLastRefresh -> d) -> dayCell d
|
||||||
, sortable (Just "valid-until") (i18nCell MsgLmsQualificationValidUntil) $ \( view $ resultQualUser . _entityVal . _qualificationUserValidUntil -> d) -> dayCell d
|
, sortable (Just "valid-until") (i18nCell MsgLmsQualificationValidUntil) $ \( view $ resultQualUser . _entityVal . _qualificationUserValidUntil -> d) -> dayCell d
|
||||||
@ -659,8 +659,8 @@ postLmsR sid qsh = do
|
|||||||
-- - Letter printed : LmsUserNotified == Just _ && PrintJobId == Just _
|
-- - Letter printed : LmsUserNotified == Just _ && PrintJobId == Just _
|
||||||
-- - Letter sent : LmsUserNotified == Just _ && PrintJobId == Just _ && PrintJobAcknowledged == Just _
|
-- - Letter sent : LmsUserNotified == Just _ && PrintJobId == Just _ && PrintJobAcknowledged == Just _
|
||||||
let notifyDate = row ^. resultLmsUser . _entityVal . _lmsUserNotified
|
let notifyDate = row ^. resultLmsUser . _entityVal . _lmsUserNotified
|
||||||
lmsident = row ^. resultLmsUser . _entityVal . _lmsUserIdent
|
lmsident = row ^. resultLmsUser . _entityVal . _lmsUserIdent
|
||||||
recipient = row ^. hasUser
|
recipient = row ^. hasUser
|
||||||
letterDates = row ^? resultPrintAck
|
letterDates = row ^? resultPrintAck
|
||||||
lastLetterDate = headDef Nothing =<< letterDates
|
lastLetterDate = headDef Nothing =<< letterDates
|
||||||
letterSent = isJust letterDates && (isNothing lastLetterDate || lastLetterDate >= notifyDate) -- was a letter attempted to send last (not 100% safe, if an email is sent after an unacknowledged letter)
|
letterSent = isJust letterDates && (isNothing lastLetterDate || lastLetterDate >= notifyDate) -- was a letter attempted to send last (not 100% safe, if an email is sent after an unacknowledged letter)
|
||||||
@ -681,7 +681,7 @@ postLmsR sid qsh = do
|
|||||||
$maybe ackdate <- mbackdate
|
$maybe ackdate <- mbackdate
|
||||||
^{formatTimeW SelFormatDateTime ackdate}
|
^{formatTimeW SelFormatDateTime ackdate}
|
||||||
$nothing
|
$nothing
|
||||||
_{MsgPrintJobUnacknowledged}
|
_{MsgPrintJobUnacknowledged}
|
||||||
<p>
|
<p>
|
||||||
<a href=@{lprLink}>
|
<a href=@{lprLink}>
|
||||||
_{MsgPrintJobs}
|
_{MsgPrintJobs}
|
||||||
@ -700,31 +700,31 @@ postLmsR sid qsh = do
|
|||||||
i18nLms msg = cell [whamlet|LMS #|] <> i18nCell msg
|
i18nLms msg = cell [whamlet|LMS #|] <> i18nCell msg
|
||||||
psValidator = def & defaultSorting [SortDescBy "started", SortDescBy "status"]
|
psValidator = def & defaultSorting [SortDescBy "started", SortDescBy "status"]
|
||||||
tbl <- mkLmsTable isAdmin qent acts colChoices psValidator
|
tbl <- mkLmsTable isAdmin qent acts colChoices psValidator
|
||||||
return (tbl, qent, lmsQualiReused)
|
return (tbl, qent)
|
||||||
|
|
||||||
formResult lmsRes $ \case
|
formResult lmsRes $ \case
|
||||||
_ | not isAdmin -> addMessageI Error MsgUnauthorized -- only admins can use the form on this page
|
_ | not isAdmin -> addMessageI Error MsgUnauthorized -- only admins can use the form on this page
|
||||||
|
|
||||||
(action, selectedUsers) | isResetRestartAct action -> do
|
(action, selectedUsers) | isResetRestartAct action -> do
|
||||||
let usersList = Set.toList selectedUsers
|
let usersList = Set.toList selectedUsers
|
||||||
numUsers = Set.size selectedUsers
|
numUsers = Set.size selectedUsers
|
||||||
isReset = isResetAct action
|
isReset = isResetAct action
|
||||||
actRestartExtend = action & lmsActRestartExtend
|
actRestartExtend = action & lmsActRestartExtend
|
||||||
actRestartUnblock = action & lmsActRestartUnblock
|
actRestartUnblock = action & lmsActRestartUnblock
|
||||||
actRestartNotify = action & lmsActRestartNotify
|
actRestartNotify = action & lmsActRestartNotify
|
||||||
|
|
||||||
chgUsers <- runDB $ do
|
chgUsers <- runDB $ do
|
||||||
when (actRestartUnblock == Just True) $ do
|
when (actRestartUnblock == Just True) $ do
|
||||||
oks <- qualificationUserBlocking qid usersList True Nothing (Left $ bool "Manueller LMS Neustart" "Manuelle LMS Zurücksetzung" isReset) (fromMaybe True actRestartNotify)
|
oks <- qualificationUserBlocking qid usersList True Nothing (Left $ bool "Manueller LMS Neustart" "Manuelle LMS Zurücksetzung" isReset) (fromMaybe True actRestartNotify)
|
||||||
addMessageI Success $ MsgQualificationStatusUnblock qsh oks numUsers
|
addMessageI Success $ MsgQualificationStatusUnblock qsh oks numUsers
|
||||||
|
|
||||||
whenIsJust actRestartExtend $ \extDays -> do
|
whenIsJust actRestartExtend $ \extDays -> do
|
||||||
let cutoff = addDays extDays nowaday
|
let cutoff = addDays extDays nowaday
|
||||||
shortUsers <- view (_entityVal . _qualificationUserUser) <<$>> selectList
|
shortUsers <- view (_entityVal . _qualificationUserUser) <<$>> selectList
|
||||||
[ QualificationUserQualification ==. qid
|
[ QualificationUserQualification ==. qid
|
||||||
, QualificationUserUser <-. usersList
|
, QualificationUserUser <-. usersList
|
||||||
, QualificationUserValidUntil <. cutoff
|
, QualificationUserValidUntil <. cutoff
|
||||||
] []
|
] []
|
||||||
forM_ shortUsers $ upsertQualificationUser qid now cutoff Nothing "E-Learning Reset"
|
forM_ shortUsers $ upsertQualificationUser qid now cutoff Nothing "E-Learning Reset"
|
||||||
|
|
||||||
fromIntegral <$> (if isReset
|
fromIntegral <$> (if isReset
|
||||||
@ -733,25 +733,25 @@ postLmsR sid qsh = do
|
|||||||
else deleteWhereCount [LmsUserQualification ==. qid, LmsUserUser <-. usersList]
|
else deleteWhereCount [LmsUserQualification ==. qid, LmsUserUser <-. usersList]
|
||||||
)
|
)
|
||||||
|
|
||||||
unless isReset $
|
unless isReset $
|
||||||
forM_ selectedUsers $ \uid ->
|
forM_ selectedUsers $ \uid ->
|
||||||
queueJob' $ JobLmsEnqueueUser { jQualification = qid, jUser = uid }
|
queueJob' $ JobLmsEnqueueUser { jQualification = qid, jUser = uid }
|
||||||
|
|
||||||
runDB $ forM_ selectedUsers $ \uid ->
|
runDB $ forM_ selectedUsers $ \uid ->
|
||||||
audit $ TransactionLmsReset
|
audit $ TransactionLmsReset
|
||||||
{ transactionQualification = qid
|
{ transactionQualification = qid
|
||||||
, transactionLmsUser = uid
|
, transactionLmsUser = uid
|
||||||
, transactionLmsReset = isReset
|
, transactionLmsReset = isReset
|
||||||
, transactionLmsResetExtend = actRestartExtend
|
, transactionLmsResetExtend = actRestartExtend
|
||||||
, transactionLmsResetUnblock = actRestartUnblock
|
, transactionLmsResetUnblock = actRestartUnblock
|
||||||
, transactionLmsResetNotify = actRestartNotify
|
, transactionLmsResetNotify = actRestartNotify
|
||||||
}
|
}
|
||||||
|
|
||||||
let mStatus = bool Success Warning $ chgUsers < numUsers
|
let mStatus = bool Success Warning $ chgUsers < numUsers
|
||||||
addMessageI mStatus $ bool MsgLmsActRestartFeedback MsgLmsActResetFeedback isReset chgUsers numUsers
|
addMessageI mStatus $ bool MsgLmsActRestartFeedback MsgLmsActResetFeedback isReset chgUsers numUsers
|
||||||
reloadKeepGetParams $ LmsR sid qsh
|
reloadKeepGetParams $ LmsR sid qsh
|
||||||
|
|
||||||
(action, selectedUsers) | isRenewPinAct action || isNotifyAct action -> do
|
(action, selectedUsers) | isRenewPinAct action || isNotifyAct action -> do
|
||||||
numExaminees <- runDB $ do
|
numExaminees <- runDB $ do
|
||||||
okUsers <- selectList [ LmsUserQualification ==. qid -- matching qualification
|
okUsers <- selectList [ LmsUserQualification ==. qid -- matching qualification
|
||||||
, LmsUserEnded ==. Nothing -- not yet deleted
|
, LmsUserEnded ==. Nothing -- not yet deleted
|
||||||
@ -767,7 +767,7 @@ postLmsR sid qsh = do
|
|||||||
return $ length okUsers
|
return $ length okUsers
|
||||||
let numSelected = length selectedUsers
|
let numSelected = length selectedUsers
|
||||||
diffSelected = numSelected - numExaminees
|
diffSelected = numSelected - numExaminees
|
||||||
mstat = bool Success Warning $ diffSelected /= 0
|
mstat = bool Success Warning $ diffSelected /= 0
|
||||||
when (isRenewPinAct action) $ addMessageI mstat $ MsgLmsPinRenewal numExaminees
|
when (isRenewPinAct action) $ addMessageI mstat $ MsgLmsPinRenewal numExaminees
|
||||||
when (isNotifyAct action) $ addMessageI mstat $ MsgLmsNotificationSend numExaminees
|
when (isNotifyAct action) $ addMessageI mstat $ MsgLmsNotificationSend numExaminees
|
||||||
when (diffSelected /= 0) $ addMessageI Warning $ MsgLmsActionFailed diffSelected
|
when (diffSelected /= 0) $ addMessageI Warning $ MsgLmsActionFailed diffSelected
|
||||||
@ -797,22 +797,22 @@ getLmsUserR sid qsh = viewLmsUserR (Just sid) (Just qsh)
|
|||||||
viewLmsUserR :: Maybe SchoolId -> Maybe QualificationShorthand -> CryptoUUIDUser -> Handler Html
|
viewLmsUserR :: Maybe SchoolId -> Maybe QualificationShorthand -> CryptoUUIDUser -> Handler Html
|
||||||
viewLmsUserR msid mqsh uuid = do
|
viewLmsUserR msid mqsh uuid = do
|
||||||
uid <- decrypt uuid
|
uid <- decrypt uuid
|
||||||
now <- liftIO getCurrentTime
|
now <- liftIO getCurrentTime
|
||||||
(user@User{userDisplayName}, quals, qblocks) <- runDBRead $ do
|
(user@User{userDisplayName}, quals, qblocks) <- runDB $ do
|
||||||
usr <- get404 uid
|
usr <- get404 uid
|
||||||
qs <- Ex.select $ do
|
qs <- Ex.select $ do
|
||||||
(qual :& qualUsr :& lmsUsr) <-
|
(qual :& qualUsr :& lmsUsr) <-
|
||||||
Ex.from $ Ex.table @Qualification
|
Ex.from $ Ex.table @Qualification
|
||||||
`Ex.leftJoin` Ex.table @QualificationUser
|
`Ex.leftJoin` Ex.table @QualificationUser
|
||||||
`Ex.on` (\(qual :& qualUsr) -> qualUsr E.?. QualificationUserUser E.?=. Ex.val uid
|
`Ex.on` (\(qual :& qualUsr) -> qualUsr E.?. QualificationUserUser E.?=. Ex.val uid
|
||||||
E.&&. qualUsr E.?. QualificationUserQualification E.?=. qual Ex.^. QualificationId
|
E.&&. qualUsr E.?. QualificationUserQualification E.?=. qual Ex.^. QualificationId
|
||||||
)
|
)
|
||||||
`Ex.leftJoin` Ex.table @LmsUser
|
`Ex.leftJoin` Ex.table @LmsUser
|
||||||
`Ex.on` (\(qual :& _ :& lmsUsr) -> lmsUsr E.?. LmsUserUser E.?=. Ex.val uid
|
`Ex.on` (\(qual :& _ :& lmsUsr) -> lmsUsr E.?. LmsUserUser E.?=. Ex.val uid
|
||||||
E.&&. lmsUsr E.?. LmsUserQualification E.?=. qual Ex.^. QualificationId
|
E.&&. lmsUsr E.?. LmsUserQualification E.?=. qual Ex.^. QualificationId
|
||||||
)
|
)
|
||||||
Ex.where_ $ E.and $
|
Ex.where_ $ E.and $
|
||||||
(E.isJust (qualUsr E.?. QualificationUserUser) E.||. E.isJust ( lmsUsr E.?. LmsUserUser)) : catMaybes
|
(E.isJust (qualUsr E.?. QualificationUserUser) E.||. E.isJust ( lmsUsr E.?. LmsUserUser)) : catMaybes
|
||||||
[ (qual E.^. QualificationSchool E.==.) . E.val <$> msid
|
[ (qual E.^. QualificationSchool E.==.) . E.val <$> msid
|
||||||
, (qual E.^. QualificationShorthand E.==.) . E.val <$> mqsh
|
, (qual E.^. QualificationShorthand E.==.) . E.val <$> mqsh
|
||||||
]
|
]
|
||||||
@ -822,7 +822,7 @@ viewLmsUserR msid mqsh uuid = do
|
|||||||
<- foldMapM (\(_, mbqu, _, _) -> case mbqu of
|
<- foldMapM (\(_, mbqu, _, _) -> case mbqu of
|
||||||
Nothing -> pure mempty
|
Nothing -> pure mempty
|
||||||
Just (Entity quid _) -> do
|
Just (Entity quid _) -> do
|
||||||
blocks <- Ex.select $ do
|
blocks <- Ex.select $ do
|
||||||
(qBlock :& qbUsr) <- Ex.from $ Ex.table @QualificationUserBlock
|
(qBlock :& qbUsr) <- Ex.from $ Ex.table @QualificationUserBlock
|
||||||
`Ex.leftJoin` Ex.table @User
|
`Ex.leftJoin` Ex.table @User
|
||||||
`Ex.on` (\(qBlock :& qbUsr) -> qBlock Ex.^. QualificationUserBlockBlocker Ex.==. qbUsr Ex.?. UserId)
|
`Ex.on` (\(qBlock :& qbUsr) -> qBlock Ex.^. QualificationUserBlockBlocker Ex.==. qbUsr Ex.?. UserId)
|
||||||
@ -832,7 +832,7 @@ viewLmsUserR msid mqsh uuid = do
|
|||||||
return $ Map.singleton quid blocks
|
return $ Map.singleton quid blocks
|
||||||
) qs
|
) qs
|
||||||
return (usr, qs, Map.filter notNull bs)
|
return (usr, qs, Map.filter notNull bs)
|
||||||
let heading = [whamlet|_{MsgMenuLmsUser} ^{userWidget user}|]
|
let heading = [whamlet|_{MsgMenuLmsUser} ^{userWidget user}|]
|
||||||
siteLayout heading $ do
|
siteLayout heading $ do
|
||||||
setTitle $ toHtml userDisplayName
|
setTitle $ toHtml userDisplayName
|
||||||
$(widgetFile "lms-user")
|
$(widgetFile "lms-user")
|
||||||
|
|||||||
@ -67,15 +67,15 @@ fakeQualificationUsers (Entity qid Qualification{qualificationRefreshWithin}) (u
|
|||||||
let pw = "123.456"
|
let pw = "123.456"
|
||||||
PWHashConf{..} <- getsYesod $ view _appAuthPWHash
|
PWHashConf{..} <- getsYesod $ view _appAuthPWHash
|
||||||
pwHash <- liftIO $ PWStore.makePasswordWith pwHashAlgorithm pw pwHashStrength
|
pwHash <- liftIO $ PWStore.makePasswordWith pwHashAlgorithm pw pwHashStrength
|
||||||
return $ AuthPWHash $ TEnc.decodeUtf8 pwHash
|
return $ TEnc.decodeUtf8 pwHash
|
||||||
theSupervisor <- selectKeysList [UserSurname ==. "Jost", UserFirstName ==. "Steffen"] [Asc UserCreated, LimitTo 1]
|
theSupervisor <- selectKeysList [UserSurname ==. "Jost", UserFirstName ==. "Steffen"] [Asc UserCreated, LimitTo 1]
|
||||||
let addSupervisor = case theSupervisor of
|
let addSupervisor = case theSupervisor of
|
||||||
[s] -> \suid k -> case k of
|
[s] -> \suid k -> case k of
|
||||||
1 -> void $ insertBy $ UserSupervisor s suid True Nothing Nothing
|
1 -> void $ insertBy $ UserSupervisor s suid True
|
||||||
2 -> do
|
2 -> do
|
||||||
void $ insertBy $ UserSupervisor s suid True Nothing (Just "Test")
|
void $ insertBy $ UserSupervisor s suid True
|
||||||
void $ insertBy $ UserSupervisor suid suid True Nothing Nothing
|
void $ insertBy $ UserSupervisor suid suid True
|
||||||
3 -> void $ insertBy $ UserSupervisor s suid True Nothing Nothing
|
3 -> void $ insertBy $ UserSupervisor s suid True
|
||||||
_ -> return ()
|
_ -> return ()
|
||||||
_ -> \_ _ -> return ()
|
_ -> \_ _ -> return ()
|
||||||
expiryOffsets = concatMap (replicate usersPerDay) [0..(diffDays dto dfrom)]
|
expiryOffsets = concatMap (replicate usersPerDay) [0..(diffDays dto dfrom)]
|
||||||
@ -83,15 +83,14 @@ fakeQualificationUsers (Entity qid Qualification{qualificationRefreshWithin}) (u
|
|||||||
fakeUser :: ([Text], UserSurname, (Maybe Languages, DateTimeFormat, DateTimeFormat, DateTimeFormat), Bool, Int) -> User
|
fakeUser :: ([Text], UserSurname, (Maybe Languages, DateTimeFormat, DateTimeFormat, DateTimeFormat), Bool, Int) -> User
|
||||||
fakeUser (firstNames, userSurname, (userLanguages, userDateTimeFormat, userDateFormat, userTimeFormat), userPrefersPostal, _isSupervised) =
|
fakeUser (firstNames, userSurname, (userLanguages, userDateTimeFormat, userDateFormat, userTimeFormat), userPrefersPostal, _isSupervised) =
|
||||||
let userIdent = CI.mk $ Text.intercalate "." (take 1 firstNames ++ (Text.take 1 <$> drop 1 firstNames) ++ [userSurname]) <> "@example.com"
|
let userIdent = CI.mk $ Text.intercalate "." (take 1 firstNames ++ (Text.take 1 <$> drop 1 firstNames) ++ [userSurname]) <> "@example.com"
|
||||||
|
userPasswordHash = Just pwSimple
|
||||||
|
userLastAuthentication = Nothing
|
||||||
userEmail = userIdent
|
userEmail = userIdent
|
||||||
userDisplayEmail = userIdent
|
userDisplayEmail = userIdent
|
||||||
userDisplayName = Text.unwords $ firstNames <> [userSurname]
|
userDisplayName = Text.unwords $ firstNames <> [userSurname]
|
||||||
userMatrikelnummer = Just "TESTUSER"
|
userMatrikelnummer = Just "TESTUSER"
|
||||||
userAuthentication = pwSimple
|
|
||||||
userLastAuthentication = Nothing
|
|
||||||
userCreated = now
|
userCreated = now
|
||||||
userLastLdapSynchronisation = Nothing
|
userLastSync = Just now
|
||||||
userLdapPrimaryKey = Nothing
|
|
||||||
userTokensIssuedAfter = Nothing
|
userTokensIssuedAfter = Nothing
|
||||||
userFirstName = Text.unwords firstNames
|
userFirstName = Text.unwords firstNames
|
||||||
userTitle = Nothing
|
userTitle = Nothing
|
||||||
|
|||||||
@ -19,7 +19,6 @@ import Handler.Utils.LMS
|
|||||||
|
|
||||||
import qualified Data.Map as Map
|
import qualified Data.Map as Map
|
||||||
import qualified Data.Csv as Csv
|
import qualified Data.Csv as Csv
|
||||||
import qualified Data.Text as Text
|
|
||||||
import qualified Data.Conduit.List as C
|
import qualified Data.Conduit.List as C
|
||||||
-- import qualified Database.Esqueleto.Experimental as Ex -- needs TypeApplications Lang-Pragma
|
-- import qualified Database.Esqueleto.Experimental as Ex -- needs TypeApplications Lang-Pragma
|
||||||
import qualified Database.Esqueleto.Legacy as E
|
import qualified Database.Esqueleto.Legacy as E
|
||||||
@ -39,7 +38,7 @@ lmsUser2csv :: UTCTime -> LmsUser -> LmsUserTableCsv
|
|||||||
lmsUser2csv cutoff lu@LmsUser{..} = LmsUserTableCsv
|
lmsUser2csv cutoff lu@LmsUser{..} = LmsUserTableCsv
|
||||||
{ csvLUTident = lmsUserIdent
|
{ csvLUTident = lmsUserIdent
|
||||||
, csvLUTpin = lmsUserPin
|
, csvLUTpin = lmsUserPin
|
||||||
, csvLUTresetPin = LmsBool lmsUserResetPin
|
, csvLUTresetPin = LmsBool lmsUserResetPin
|
||||||
, csvLUTdelete = LmsBool (lmsUserToDelete cutoff lu)
|
, csvLUTdelete = LmsBool (lmsUserToDelete cutoff lu)
|
||||||
, csvLUTstaff = LmsBool (lmsUserStaff lu)
|
, csvLUTstaff = LmsBool (lmsUserStaff lu)
|
||||||
, csvLUTresetTries= LmsBool (lmsUserToResetTries lu) -- TODO: verify this works as intended!
|
, csvLUTresetTries= LmsBool (lmsUserToResetTries lu) -- TODO: verify this works as intended!
|
||||||
@ -93,7 +92,7 @@ instance CsvColumnsExplained LmsUserTableCsv where
|
|||||||
|
|
||||||
mkUserTable :: SchoolId -> QualificationShorthand -> QualificationId -> UTCTime -> DB (Any, Widget)
|
mkUserTable :: SchoolId -> QualificationShorthand -> QualificationId -> UTCTime -> DB (Any, Widget)
|
||||||
mkUserTable _sid qsh qid cutoff = do
|
mkUserTable _sid qsh qid cutoff = do
|
||||||
dbtCsvName <- csvFilenameLmsUser qsh
|
dbtCsvName <- csvFilenameLmsUser qsh
|
||||||
let dbtCsvSheetName = dbtCsvName
|
let dbtCsvSheetName = dbtCsvName
|
||||||
let
|
let
|
||||||
userDBTable = DBTable{..}
|
userDBTable = DBTable{..}
|
||||||
@ -167,7 +166,7 @@ getQidCutoff sid qsh = do
|
|||||||
getLmsLearnersR :: SchoolId -> QualificationShorthand -> Handler Html
|
getLmsLearnersR :: SchoolId -> QualificationShorthand -> Handler Html
|
||||||
getLmsLearnersR sid qsh = do
|
getLmsLearnersR sid qsh = do
|
||||||
lmsTable <- runDB $ do
|
lmsTable <- runDB $ do
|
||||||
(qid, cutoff) <- getQidCutoff sid qsh
|
(qid, cutoff) <- getQidCutoff sid qsh
|
||||||
view _2 <$> mkUserTable sid qsh qid cutoff
|
view _2 <$> mkUserTable sid qsh qid cutoff
|
||||||
siteLayoutMsg MsgMenuLmsLearners $ do
|
siteLayoutMsg MsgMenuLmsLearners $ do
|
||||||
setTitleI MsgMenuLmsLearners
|
setTitleI MsgMenuLmsLearners
|
||||||
@ -175,17 +174,14 @@ getLmsLearnersR sid qsh = do
|
|||||||
|
|
||||||
getLmsLearnersDirectR :: SchoolId -> QualificationShorthand -> Handler TypedContent
|
getLmsLearnersDirectR :: SchoolId -> QualificationShorthand -> Handler TypedContent
|
||||||
getLmsLearnersDirectR sid qsh = do
|
getLmsLearnersDirectR sid qsh = do
|
||||||
$logInfoS "LMS" $ "Direct Download Users for " <> tshow qsh <> " at " <> tshow sid
|
$logInfoS "LMS" $ "Direct Download Users for " <> tshow qsh <> " at " <> tshow sid
|
||||||
(lms_users,cutoff,qshs) <- runDB $ do
|
(lms_users,cutoff) <- runDB $ do
|
||||||
(qid, cutoff) <- getQidCutoff sid qsh
|
(qid, cutoff) <- getQidCutoff sid qsh
|
||||||
qidsReuse <- selectList [QualificationLmsReuses ==. Just qid] []
|
lms_users <- selectList [ LmsUserQualification ==. qid
|
||||||
let qids = qid : (entityKey <$> qidsReuse)
|
|
||||||
qshs = qsh : (qualificationShorthand . entityVal <$> qidsReuse)
|
|
||||||
lms_users <- selectList [ LmsUserQualification <-. qids
|
|
||||||
, LmsUserEnded ==. Nothing
|
, LmsUserEnded ==. Nothing
|
||||||
-- , LmsUserReceived ==. Nothing ||. LmsUserResetPin ==. True ||. LmsUserStatus !=. Nothing -- send delta only NOTE: know-how no longer expects delta
|
-- , LmsUserReceived ==. Nothing ||. LmsUserResetPin ==. True ||. LmsUserStatus !=. Nothing -- send delta only NOTE: know-how no longer expects delta
|
||||||
] [Asc LmsUserStarted, Asc LmsUserIdent]
|
] [Asc LmsUserStarted, Asc LmsUserIdent]
|
||||||
return (lms_users, cutoff, qshs)
|
return (lms_users, cutoff)
|
||||||
|
|
||||||
{- To avoid exporting unneeded columns, we would need an SqlSelect instance for LmsUserTableCsv; probably not worth it
|
{- To avoid exporting unneeded columns, we would need an SqlSelect instance for LmsUserTableCsv; probably not worth it
|
||||||
Ex.select $ do
|
Ex.select $ do
|
||||||
@ -200,7 +196,7 @@ getLmsLearnersDirectR sid qsh = do
|
|||||||
, csvLUTstaff = LmsBool False
|
, csvLUTstaff = LmsBool False
|
||||||
}
|
}
|
||||||
-}
|
-}
|
||||||
LmsConf{..} <- getsYesod $ view _appLmsConf
|
LmsConf{..} <- getsYesod $ view _appLmsConf
|
||||||
let --csvRenderedData = toNamedRecord . lmsUser2csv . entityVal <$> lms_users
|
let --csvRenderedData = toNamedRecord . lmsUser2csv . entityVal <$> lms_users
|
||||||
--csvRenderedHeader = lmsUserTableCsvHeader
|
--csvRenderedHeader = lmsUserTableCsvHeader
|
||||||
--cvsRendered = CsvRendered {..}
|
--cvsRendered = CsvRendered {..}
|
||||||
@ -213,7 +209,7 @@ getLmsLearnersDirectR sid qsh = do
|
|||||||
csvOpts = def { csvFormat = fmtOpts }
|
csvOpts = def { csvFormat = fmtOpts }
|
||||||
csvSheetName <- csvFilenameLmsUser qsh
|
csvSheetName <- csvFilenameLmsUser qsh
|
||||||
let nr = length lms_users
|
let nr = length lms_users
|
||||||
msg = "Success. LMS user learners download file " <> csvSheetName <> " containing " <> tshow nr <> " rows for Qualifications " <> Text.intercalate ", " (ciOriginal <$> qshs)
|
msg = "Success. LMS user learners download file " <> csvSheetName <> " containing " <> tshow nr <> " rows"
|
||||||
$logInfoS "LMS" msg
|
$logInfoS "LMS" msg
|
||||||
addHeader "Content-Disposition" $ "attachment; filename=\"" <> csvSheetName <> "\""
|
addHeader "Content-Disposition" $ "attachment; filename=\"" <> csvSheetName <> "\""
|
||||||
csvRenderedToTypedContentWith csvOpts csvSheetName csvRendered
|
csvRenderedToTypedContentWith csvOpts csvSheetName csvRendered
|
||||||
|
|||||||
@ -3,7 +3,6 @@
|
|||||||
-- SPDX-License-Identifier: AGPL-3.0-or-later
|
-- SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
{-# OPTIONS_GHC -fno-warn-orphans #-} -- needed for HasEntity instances
|
{-# OPTIONS_GHC -fno-warn-orphans #-} -- needed for HasEntity instances
|
||||||
{-# LANGUAGE TypeApplications #-}
|
|
||||||
|
|
||||||
module Handler.LMS.Report
|
module Handler.LMS.Report
|
||||||
( getLmsReportR, postLmsReportR
|
( getLmsReportR, postLmsReportR
|
||||||
@ -18,13 +17,10 @@ import Handler.Utils
|
|||||||
import Handler.Utils.Csv
|
import Handler.Utils.Csv
|
||||||
import Handler.Utils.LMS
|
import Handler.Utils.LMS
|
||||||
|
|
||||||
import qualified Data.Text as Text
|
|
||||||
import qualified Data.Map as Map
|
import qualified Data.Map as Map
|
||||||
import qualified Data.Csv as Csv
|
import qualified Data.Csv as Csv
|
||||||
import qualified Data.Conduit.List as C
|
import qualified Data.Conduit.List as C
|
||||||
-- import Database.Esqueleto.Experimental ((:&)(..))
|
import qualified Database.Esqueleto.Legacy as E
|
||||||
import qualified Database.Esqueleto.Experimental as E -- needs TypeApplications Lang-Pragma
|
|
||||||
import qualified Database.Esqueleto.PostgreSQL as E
|
|
||||||
import qualified Database.Esqueleto.Utils as E
|
import qualified Database.Esqueleto.Utils as E
|
||||||
|
|
||||||
import Jobs.Queue
|
import Jobs.Queue
|
||||||
@ -125,7 +121,7 @@ mkReportTable sid qsh qid = do
|
|||||||
]
|
]
|
||||||
dbtFilter = Map.fromList
|
dbtFilter = Map.fromList
|
||||||
[ (csvLmsIdent , FilterColumn $ E.mkContainsFilterWithCommaPlus LmsIdent (E.^. LmsReportIdent))
|
[ (csvLmsIdent , FilterColumn $ E.mkContainsFilterWithCommaPlus LmsIdent (E.^. LmsReportIdent))
|
||||||
, (csvLmsDate , FilterColumn $ E.mkExactFilter (E.^. LmsReportDate))
|
, (csvLmsDate , FilterColumn $ E.mkExactFilter (E.^. LmsReportDate))
|
||||||
]
|
]
|
||||||
dbtFilterUI = \mPrev -> mconcat
|
dbtFilterUI = \mPrev -> mconcat
|
||||||
[ prismAForm (singletonFilter csvLmsIdent . maybePrism _PathPiece) mPrev $ aopt (hoistField lift textField) (fslI MsgTableLmsIdent & setTooltip MsgTableFilterCommaPlus)
|
[ prismAForm (singletonFilter csvLmsIdent . maybePrism _PathPiece) mPrev $ aopt (hoistField lift textField) (fslI MsgTableLmsIdent & setTooltip MsgTableFilterCommaPlus)
|
||||||
@ -203,7 +199,7 @@ mkReportTable sid qsh qid = do
|
|||||||
, LmsReportResult =. lmsReportCsvResult actionData
|
, LmsReportResult =. lmsReportCsvResult actionData
|
||||||
, LmsReportLock =. lmsReportCsvLock actionData
|
, LmsReportLock =. lmsReportCsvLock actionData
|
||||||
, LmsReportTimestamp =. eanow
|
, LmsReportTimestamp =. eanow
|
||||||
]
|
]
|
||||||
lift . queueDBJob $ JobLmsReports qid
|
lift . queueDBJob $ JobLmsReports qid
|
||||||
return $ LmsReportR sid qsh
|
return $ LmsReportR sid qsh
|
||||||
, dbtCsvRenderKey = const $ \case
|
, dbtCsvRenderKey = const $ \case
|
||||||
@ -250,8 +246,8 @@ postLmsReportR sid qsh = do
|
|||||||
|
|
||||||
-- Direct File Upload/Download
|
-- Direct File Upload/Download
|
||||||
|
|
||||||
saveReportCsv :: UTCTime -> NonEmpty QualificationId -> Int -> LmsReportTableCsv -> JobDB Int
|
saveReportCsv :: UTCTime -> QualificationId -> Int -> LmsReportTableCsv -> JobDB Int
|
||||||
saveReportCsv now (qid :| []) i LmsReportTableCsv{..} = do
|
saveReportCsv now qid i LmsReportTableCsv{..} = do
|
||||||
void $ upsert
|
void $ upsert
|
||||||
LmsReport
|
LmsReport
|
||||||
{ lmsReportQualification = qid
|
{ lmsReportQualification = qid
|
||||||
@ -267,30 +263,6 @@ saveReportCsv now (qid :| []) i LmsReportTableCsv{..} = do
|
|||||||
, LmsReportTimestamp =. now
|
, LmsReportTimestamp =. now
|
||||||
]
|
]
|
||||||
return $ succ i
|
return $ succ i
|
||||||
saveReportCsv now qids@(qid :| _) i lrtc@LmsReportTableCsv{..} = do
|
|
||||||
ok <- E.insertSelectWithConflictCount UniqueLmsReport
|
|
||||||
(do
|
|
||||||
lusr <- E.from $ E.table @LmsUser
|
|
||||||
E.where_ $ lusr E.^. LmsUserIdent E.==. E.val csvLRident
|
|
||||||
E.&&. lusr E.^. LmsUserQualification `E.in_` E.vals qids
|
|
||||||
return $ LmsReport
|
|
||||||
E.<# (lusr E.^. LmsUserQualification)
|
|
||||||
E.<&> E.val csvLRident
|
|
||||||
E.<&> E.val (csvLRdate <&> lms2timestamp)
|
|
||||||
E.<&> E.val csvLRresult
|
|
||||||
E.<&> E.val (csvLRlock & lms2bool)
|
|
||||||
E.<&> E.val now
|
|
||||||
)
|
|
||||||
(\_old _new ->
|
|
||||||
[ LmsReportDate E.=. E.val (csvLRdate <&> lms2timestamp)
|
|
||||||
, LmsReportResult E.=. E.val csvLRresult
|
|
||||||
, LmsReportLock E.=. E.val (csvLRlock & lms2bool)
|
|
||||||
, LmsReportTimestamp E.=. E.val now
|
|
||||||
]
|
|
||||||
)
|
|
||||||
if ok > 0
|
|
||||||
then return $ succ i
|
|
||||||
else saveReportCsv now (qid :| []) i lrtc -- save unknown LmsIdent to primary qid regardless, so that the error can be tracked
|
|
||||||
|
|
||||||
makeReportUploadForm :: Form FileInfo
|
makeReportUploadForm :: Form FileInfo
|
||||||
makeReportUploadForm = renderAForm FormStandard $ fileAFormReq "Report CSV"
|
makeReportUploadForm = renderAForm FormStandard $ fileAFormReq "Report CSV"
|
||||||
@ -304,18 +276,15 @@ postLmsReportUploadR sid qsh = do
|
|||||||
FormSuccess file -> do
|
FormSuccess file -> do
|
||||||
-- content <- fileSourceByteString file
|
-- content <- fileSourceByteString file
|
||||||
-- return $ Just (fileName file, content)
|
-- return $ Just (fileName file, content)
|
||||||
(nr, qids, qshs) <- runDBJobs $ do
|
(nr, qid) <- runDBJobs $ do
|
||||||
qid <- getKeyBy404 $ SchoolQualificationShort sid qsh
|
qid <- getKeyBy404 $ SchoolQualificationShort sid qsh
|
||||||
qidsReuse <- selectList [QualificationLmsReuses ==. Just qid] []
|
|
||||||
let qids = qid :| (entityKey <$> qidsReuse)
|
|
||||||
qshs = qsh : (qualificationShorthand . entityVal <$> qidsReuse)
|
|
||||||
nr <- runConduit $ fileSource file
|
nr <- runConduit $ fileSource file
|
||||||
.| decodeCsv
|
.| decodeCsv
|
||||||
.| foldMC (saveReportCsv now qids) 0
|
.| foldMC (saveReportCsv now qid) 0
|
||||||
return (nr, qids, qshs)
|
return (nr, qid)
|
||||||
addMessage Success $ toHtml $ pack "Erfolgreicher Upload der Datei " <> fileName file <> pack (" mit " <> show nr <> " Zeilen") <> " für Qualifikationen: " <> Text.intercalate ", " (ciOriginal <$> qshs)
|
addMessage Success $ toHtml $ pack "Erfolgreicher Upload der Datei " <> fileName file <> pack (" mit " <> show nr <> " Zeilen")
|
||||||
-- redirect $ LmsReportR sid qsh
|
-- redirect $ LmsReportR sid qsh
|
||||||
getLmsReportR sid qsh <* forM_ qids (queueJob' . JobLmsReports) -- show uploaded data before processing
|
getLmsReportR sid qsh <* queueJob' (JobLmsReports qid) -- show uploaded data before processing
|
||||||
|
|
||||||
FormFailure errs -> do
|
FormFailure errs -> do
|
||||||
forM_ errs $ addMessage Error . toHtml
|
forM_ errs $ addMessage Error . toHtml
|
||||||
@ -325,7 +294,7 @@ postLmsReportUploadR sid qsh = do
|
|||||||
setTitleI MsgMenuLmsUpload
|
setTitleI MsgMenuLmsUpload
|
||||||
[whamlet|$newline never
|
[whamlet|$newline never
|
||||||
<form method=post enctype=#{enctype}>
|
<form method=post enctype=#{enctype}>
|
||||||
^{widget}
|
^{widget}
|
||||||
<input type=submit>
|
<input type=submit>
|
||||||
|]
|
|]
|
||||||
|
|
||||||
@ -339,21 +308,18 @@ postLmsReportDirectR sid qsh = do
|
|||||||
lmsDecoder <- getLmsCsvDecoder
|
lmsDecoder <- getLmsCsvDecoder
|
||||||
runDBJobs $ do
|
runDBJobs $ do
|
||||||
qid <- getKeyBy404 $ SchoolQualificationShort sid qsh
|
qid <- getKeyBy404 $ SchoolQualificationShort sid qsh
|
||||||
qidsReuse <- selectList [QualificationLmsReuses ==. Just qid] []
|
|
||||||
let qids = qid :| (entityKey <$> qidsReuse)
|
|
||||||
qshs = qsh : (qualificationShorthand . entityVal <$> qidsReuse)
|
|
||||||
enr <- try $ runConduit $ fileSource file
|
enr <- try $ runConduit $ fileSource file
|
||||||
.| lmsDecoder
|
.| lmsDecoder
|
||||||
.| foldMC (saveReportCsv now qids) 0
|
.| foldMC (saveReportCsv now qid) 0
|
||||||
case enr of
|
case enr of
|
||||||
Left (e :: SomeException) -> do -- catch all to avoid ok220 in case of any error
|
Left (e :: SomeException) -> do -- catch all to avoid ok220 in case of any error
|
||||||
$logWarnS "LMS" $ "Report upload failed parsing: " <> tshow e <> " for Qualification: " <> Text.intercalate ", " (ciOriginal <$> qshs)
|
$logWarnS "LMS" $ "Report upload failed parsing: " <> tshow e
|
||||||
logInterface "LMS" (ciOriginal qsh) False Nothing ""
|
logInterface "LMS" (ciOriginal qsh) False Nothing ""
|
||||||
return (badRequest400, "Exception: " <> tshow e)
|
return (badRequest400, "Exception: " <> tshow e)
|
||||||
Right nr -> do
|
Right nr -> do
|
||||||
let msg = "Success. LMS Report upload file " <> fileName file <> " containing " <> tshow nr <> " rows for " <> fhead <> " and Qualifications: " <> Text.intercalate ", " (ciOriginal <$> qshs)
|
let msg = "Success. LMS Report upload file " <> fileName file <> " containing " <> tshow nr <> " rows for " <> fhead <> ". "
|
||||||
$logInfoS "LMS" msg
|
$logInfoS "LMS" msg
|
||||||
when (nr > 0) $ forM_ qids (queueDBJob . JobLmsReports)
|
when (nr > 0) $ queueDBJob $ JobLmsReports qid
|
||||||
logInterface "LMS" (ciOriginal qsh) True (Just nr) ""
|
logInterface "LMS" (ciOriginal qsh) True (Just nr) ""
|
||||||
return (ok200, msg)
|
return (ok200, msg)
|
||||||
[] -> do
|
[] -> do
|
||||||
|
|||||||
@ -1,375 +0,0 @@
|
|||||||
-- SPDX-FileCopyrightText: 2024 Steffen Jost <s.jost@fraport.de>
|
|
||||||
--
|
|
||||||
-- SPDX-License-Identifier: AGPL-3.0-or-later
|
|
||||||
|
|
||||||
{-# OPTIONS_GHC -fno-warn-orphans #-}
|
|
||||||
|
|
||||||
module Handler.MailCenter
|
|
||||||
( getMailCenterR, postMailCenterR
|
|
||||||
, getMailHtmlR
|
|
||||||
, getMailPlainR
|
|
||||||
, getMailAttachmentR
|
|
||||||
) where
|
|
||||||
|
|
||||||
import Import
|
|
||||||
|
|
||||||
import qualified Data.Set as Set
|
|
||||||
import qualified Data.Map as Map
|
|
||||||
-- import qualified Data.Aeson as Aeson
|
|
||||||
-- import qualified Data.Text as Text
|
|
||||||
|
|
||||||
-- import Database.Persist.Sql (updateWhereCount)
|
|
||||||
-- import Database.Esqueleto.Experimental ((:&)(..))
|
|
||||||
import qualified Database.Esqueleto.Legacy as EL (on) -- only `on` and `from` are different, needed for dbTable using Esqueleto.Legacy
|
|
||||||
import qualified Database.Esqueleto.Experimental as E
|
|
||||||
import qualified Database.Esqueleto.Utils as E
|
|
||||||
import Database.Esqueleto.Utils.TH
|
|
||||||
|
|
||||||
import qualified Data.Aeson as Aeson
|
|
||||||
|
|
||||||
import Text.Blaze.Html (preEscapedToHtml)
|
|
||||||
-- import Text.Blaze.Html5 as H (html, body, pre, p, h1)
|
|
||||||
-- import Text.Blaze.Html.Renderer.String (renderHtml)
|
|
||||||
|
|
||||||
import Numeric (readHex)
|
|
||||||
import qualified Data.Text as T
|
|
||||||
import qualified Data.Text.Encoding as TE
|
|
||||||
import qualified Data.ByteString as S
|
|
||||||
import qualified Data.ByteString.Lazy as LB
|
|
||||||
|
|
||||||
|
|
||||||
import Handler.Utils
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
-- avoids repetition of local definitions
|
|
||||||
single :: (k,a) -> Map k a
|
|
||||||
single = uncurry Map.singleton
|
|
||||||
|
|
||||||
|
|
||||||
data MCTableAction = MCActDummy -- just a dummy, since we don't now yet which actions we will be needing
|
|
||||||
deriving (Eq, Ord, Enum, Bounded, Read, Show, Generic)
|
|
||||||
|
|
||||||
instance Universe MCTableAction
|
|
||||||
instance Finite MCTableAction
|
|
||||||
nullaryPathPiece ''MCTableAction $ camelToPathPiece' 2
|
|
||||||
embedRenderMessage ''UniWorX ''MCTableAction id
|
|
||||||
|
|
||||||
data MCTableActionData = MCActDummyData
|
|
||||||
deriving (Eq, Ord, Read, Show, Generic)
|
|
||||||
|
|
||||||
|
|
||||||
type MCTableExpr =
|
|
||||||
( E.SqlExpr (Entity SentMail)
|
|
||||||
`E.LeftOuterJoin` E.SqlExpr (Maybe (Entity User))
|
|
||||||
)
|
|
||||||
|
|
||||||
queryMail :: MCTableExpr -> E.SqlExpr (Entity SentMail)
|
|
||||||
queryMail = $(sqlLOJproj 2 1)
|
|
||||||
|
|
||||||
|
|
||||||
queryRecipient :: MCTableExpr -> E.SqlExpr (Maybe (Entity User))
|
|
||||||
queryRecipient = $(sqlLOJproj 2 2)
|
|
||||||
|
|
||||||
|
|
||||||
type MCTableData = DBRow (Entity SentMail, Maybe (Entity User))
|
|
||||||
|
|
||||||
resultMail :: Lens' MCTableData (Entity SentMail)
|
|
||||||
resultMail = _dbrOutput . _1
|
|
||||||
|
|
||||||
resultRecipient :: Traversal' MCTableData (Entity User)
|
|
||||||
resultRecipient = _dbrOutput . _2 . _Just
|
|
||||||
|
|
||||||
|
|
||||||
mkMCTable :: DB (FormResult (MCTableActionData, Set SentMailId), Widget)
|
|
||||||
mkMCTable = do
|
|
||||||
let
|
|
||||||
dbtSQLQuery :: MCTableExpr -> E.SqlQuery (E.SqlExpr (Entity SentMail), E.SqlExpr (Maybe (Entity User)))
|
|
||||||
dbtSQLQuery (mail `E.LeftOuterJoin` recipient) = do
|
|
||||||
EL.on $ mail E.^. SentMailRecipient E.==. recipient E.?. UserId
|
|
||||||
return (mail, recipient)
|
|
||||||
dbtRowKey = queryMail >>> (E.^. SentMailId)
|
|
||||||
dbtProj = dbtProjId
|
|
||||||
dbtColonnade = mconcat
|
|
||||||
[ -- dbSelect (applying _2) id (return . view (resultMail . _entityKey))
|
|
||||||
sortable (Just "sent") (i18nCell MsgPrintJobCreated) $ \( view $ resultMail . _entityVal . _sentMailSentAt -> t) -> dateTimeCell t
|
|
||||||
, sortable (Just "recipient") (i18nCell MsgPrintRecipient) $ \(preview resultRecipient -> u) -> maybeCell u $ cellHasUserLink AdminUserR
|
|
||||||
, sortable Nothing (i18nCell MsgCommSubject) $ \(view resultMail -> Entity k v) ->
|
|
||||||
let subject = v ^? _sentMailHeaders . _mailHeaders' . _mailHeader' "Subject"
|
|
||||||
linkWgt = maybe (msg2widget MsgUtilEMail) text2widget subject
|
|
||||||
in anchorCellM (MailHtmlR <$> encrypt k) linkWgt
|
|
||||||
-- , sortable Nothing (i18nCell MsgCommContent) $ \(view $ resultMail . _entityKey -> k) -> anchorCellM (MailHtmlR <$> encrypt k) (text2widget "html")
|
|
||||||
-- , sortable Nothing (i18nCell MsgCommSubject) $ \(preview $ resultMail . _entityVal . _sentMailHeaders . _mailHeaders' . _mailHeader' "Subject" -> h) -> cellMaybe textCell h
|
|
||||||
]
|
|
||||||
dbtSorting = mconcat
|
|
||||||
[ single ("sent" , SortColumn $ queryMail >>> (E.^. SentMailSentAt))
|
|
||||||
, single ("recipient" , sortUserNameBareM queryRecipient)
|
|
||||||
]
|
|
||||||
dbtFilter = mconcat
|
|
||||||
[ single ("sent" , FilterColumn . E.mkDayFilterTo $ views (to queryMail) (E.^. SentMailSentAt))
|
|
||||||
, single ("recipient" , FilterColumn . E.mkContainsFilterWithCommaPlus Just $ views (to queryRecipient) (E.?. UserDisplayName))
|
|
||||||
, single ("subject" , FilterColumn . E.mkContainsFilterWithCommaPlus id $ views (to queryMail) (E.str2text . (E.^. SentMailHeaders)))
|
|
||||||
-- , single ("regex" , FilterColumn . E.mkRegExFilterWith id $ views (to queryMail) (E.str2text . (E.^. SentMailHeaders)))
|
|
||||||
]
|
|
||||||
dbtFilterUI mPrev = mconcat
|
|
||||||
[ prismAForm (singletonFilter "sent" . maybePrism _PathPiece) mPrev $ aopt (hoistField lift dayField) (fslI MsgPrintJobCreated)
|
|
||||||
, prismAForm (singletonFilter "recipient" . maybePrism _PathPiece) mPrev $ aopt (hoistField lift textField) (fslI MsgPrintRecipient & setTooltip MsgTableFilterCommaPlus)
|
|
||||||
, prismAForm (singletonFilter "subject" . maybePrism _PathPiece) mPrev $ aopt (hoistField lift textField) (fslI MsgCommSubject & setTooltip MsgTableFilterCommaPlusShort)
|
|
||||||
-- , prismAForm (singletonFilter "regex" . maybePrism _PathPiece) mPrev $ aopt (hoistField lift textField) (fslI MsgCommSubject )
|
|
||||||
]
|
|
||||||
dbtStyle = def { dbsFilterLayout = defaultDBSFilterLayout}
|
|
||||||
dbtIdent :: Text
|
|
||||||
dbtIdent = "sent-mail"
|
|
||||||
dbtCsvEncode = noCsvEncode
|
|
||||||
dbtCsvDecode = Nothing
|
|
||||||
dbtExtraReps = []
|
|
||||||
dbtParams = DBParamsForm
|
|
||||||
{ dbParamsFormMethod = POST
|
|
||||||
, dbParamsFormAction = Nothing -- Just $ SomeRoute currentRoute
|
|
||||||
, dbParamsFormAttrs = []
|
|
||||||
, dbParamsFormSubmit = FormNoSubmit
|
|
||||||
, dbParamsFormAdditional = \_csrf -> return (FormMissing, mempty)
|
|
||||||
-- , dbParamsFormSubmit = FormSubmit
|
|
||||||
-- , dbParamsFormAdditional
|
|
||||||
-- = let acts :: Map MCTableAction (AForm Handler MCTableActionData)
|
|
||||||
-- acts = mconcat
|
|
||||||
-- [ singletonMap MCActDummy $ pure MCActDummyData
|
|
||||||
-- ]
|
|
||||||
-- in renderAForm FormStandard
|
|
||||||
-- $ (, mempty) . First . Just
|
|
||||||
-- <$> multiActionA acts (fslI MsgTableAction) Nothing
|
|
||||||
, dbParamsFormEvaluate = liftHandler . runFormPost
|
|
||||||
, dbParamsFormResult = id
|
|
||||||
, dbParamsFormIdent = def
|
|
||||||
}
|
|
||||||
postprocess :: FormResult (First MCTableActionData, DBFormResult SentMailId Bool MCTableData)
|
|
||||||
-> FormResult ( MCTableActionData, Set SentMailId)
|
|
||||||
postprocess inp = do
|
|
||||||
(First (Just act), jobMap) <- inp
|
|
||||||
let jobSet = Map.keysSet . Map.filter id $ getDBFormResult (const False) jobMap
|
|
||||||
return (act, jobSet)
|
|
||||||
psValidator = def & defaultSorting [SortDescBy "sent"]
|
|
||||||
over _1 postprocess <$> dbTable psValidator DBTable{..}
|
|
||||||
|
|
||||||
getMailCenterR, postMailCenterR :: Handler Html
|
|
||||||
getMailCenterR = postMailCenterR
|
|
||||||
postMailCenterR = do
|
|
||||||
(mcRes, mcTable) <- runDB mkMCTable
|
|
||||||
formResult mcRes $ \case
|
|
||||||
(MCActDummyData, Set.toList -> _smIds) -> do
|
|
||||||
addMessageI Success MsgBoolIrrelevant
|
|
||||||
reloadKeepGetParams MailCenterR
|
|
||||||
siteLayoutMsg MsgMenuMailCenter $ do
|
|
||||||
setTitleI MsgMenuMailCenter
|
|
||||||
$(widgetFile "mail-center")
|
|
||||||
|
|
||||||
|
|
||||||
typePDF :: ContentType
|
|
||||||
typePDF = "application/pdf"
|
|
||||||
|
|
||||||
getMailAttachmentR :: CryptoUUIDSentMail -> Text -> Handler TypedContent
|
|
||||||
getMailAttachmentR cusm attdisp = do
|
|
||||||
smid <- decrypt cusm
|
|
||||||
(sm,cn) <- runDBRead $ do
|
|
||||||
sm <- get404 smid
|
|
||||||
cn <- get404 $ sm ^. _sentMailContentRef
|
|
||||||
return (sm,cn)
|
|
||||||
let mcontent = getMailContent (sentMailContentContent cn)
|
|
||||||
getAttm alts = case selectAlternative [typePDF] alts of
|
|
||||||
(Just Part{partContent=PartContent (LB.toStrict -> pc), partDisposition=AttachmentDisposition t}) -- partType=pt,
|
|
||||||
| t == attdisp
|
|
||||||
-> Just pc
|
|
||||||
_ -> Nothing
|
|
||||||
attm = firstJust getAttm mcontent
|
|
||||||
case attm of
|
|
||||||
(Just pc) -> sendByteStringAsFile (T.unpack attdisp) pc $ sm ^. _sentMailSentAt
|
|
||||||
_ -> notFound
|
|
||||||
|
|
||||||
getMailHtmlR :: CryptoUUIDSentMail -> Handler Html
|
|
||||||
getMailHtmlR = handleMailShow (SomeMessages [SomeMessage MsgUtilEMail, SomeMessage MsgMenuMailHtml]) [typeHtml,typePlain]
|
|
||||||
|
|
||||||
getMailPlainR :: CryptoUUIDSentMail -> Handler Html
|
|
||||||
getMailPlainR = handleMailShow (SomeMessages [SomeMessage MsgUtilEMail, SomeMessage MsgMenuMailPlain]) [typePlain,typeHtml]
|
|
||||||
|
|
||||||
handleMailShow :: _ -> [ContentType] -> CryptoUUIDSentMail -> Handler Html
|
|
||||||
handleMailShow hdr prefTypes cusm = do
|
|
||||||
smid <- decrypt cusm
|
|
||||||
(sm,cn) <- runDBRead $ do
|
|
||||||
sm <- get404 smid
|
|
||||||
cn <- get404 $ sm ^. _sentMailContentRef
|
|
||||||
return (sm,cn)
|
|
||||||
siteLayout' Nothing $ do
|
|
||||||
setTitleI hdr
|
|
||||||
let mcontent = getMailContent (sentMailContentContent cn)
|
|
||||||
getHeader h = preview (_mailHeader' h) (sm ^. _sentMailHeaders . _mailHeaders')
|
|
||||||
mparts = reorderParts $ mapMaybe (selectAlternative prefTypes) mcontent
|
|
||||||
[whamlet|
|
|
||||||
<section>
|
|
||||||
<dl .deflist>
|
|
||||||
<dt .deflist__dt>
|
|
||||||
_{MsgPrintJobCreated}
|
|
||||||
<dd .deflist__dd>
|
|
||||||
^{formatTimeW SelFormatDateTime (sm ^. _sentMailSentAt)}
|
|
||||||
$maybe usr <- sm ^. _sentMailRecipient
|
|
||||||
<dt .deflist__dt>
|
|
||||||
_{MsgPrintRecipient}
|
|
||||||
<dd .deflist__dd>
|
|
||||||
^{userIdWidget usr}
|
|
||||||
$maybe r <- getHeader "To"
|
|
||||||
<dt .deflist__dt>
|
|
||||||
To
|
|
||||||
<dd .deflist__dd>
|
|
||||||
#{decodeEncodedWord r}
|
|
||||||
$maybe r <- getHeader "Cc"
|
|
||||||
<dt .deflist__dt>
|
|
||||||
Cc
|
|
||||||
<dd .deflist__dd>
|
|
||||||
#{decodeEncodedWord r}
|
|
||||||
$maybe r <- getHeader "From"
|
|
||||||
<dt .deflist__dt>
|
|
||||||
From
|
|
||||||
<dd .deflist__dd>
|
|
||||||
#{decodeEncodedWord r}
|
|
||||||
$maybe r <- getHeader "Subject"
|
|
||||||
<dt .deflist__dt>
|
|
||||||
_{MsgCommSubject}
|
|
||||||
<dd .deflist__dd>
|
|
||||||
#{decodeEncodedWord r}
|
|
||||||
|
|
||||||
<section>
|
|
||||||
$forall pt <- mparts
|
|
||||||
^{part2widget cusm pt}
|
|
||||||
|]
|
|
||||||
-- Include for Debugging:
|
|
||||||
-- <section>
|
|
||||||
-- <h2>Debugging
|
|
||||||
-- <p>
|
|
||||||
-- ^{jsonWidget (sm ^. _sentMailHeaders)}
|
|
||||||
-- <p>
|
|
||||||
-- ^{jsonWidget (sentMailContentContent cn)} -- content fields needs decoding of base64 to make sense here
|
|
||||||
|
|
||||||
selectAlternative :: [ContentType] -> Alternatives -> Maybe Part
|
|
||||||
selectAlternative (fmap decodeUtf8 -> prefTypes) allAlts = aux prefTypes allAlts
|
|
||||||
where
|
|
||||||
aux ts@(ct:_) (pt:ps)
|
|
||||||
| ct == partType pt = Just pt
|
|
||||||
| otherwise = aux ts ps
|
|
||||||
aux (_:ts) [] = aux ts allAlts
|
|
||||||
aux [] (pt:_) = Just pt
|
|
||||||
aux _ [] = Nothing
|
|
||||||
|
|
||||||
reorderParts :: [Part] -> [Part]
|
|
||||||
reorderParts = sortBy pOrder
|
|
||||||
where
|
|
||||||
pOrder Part{partDisposition=d1} Part{partDisposition=d2} = dispoOrder d1 d2
|
|
||||||
|
|
||||||
dispoOrder DefaultDisposition DefaultDisposition = EQ
|
|
||||||
dispoOrder DefaultDisposition _ = LT
|
|
||||||
dispoOrder _ DefaultDisposition = GT
|
|
||||||
dispoOrder (InlineDisposition t1) (InlineDisposition t2) = compare t1 t2
|
|
||||||
dispoOrder (InlineDisposition _) _ = LT
|
|
||||||
dispoOrder _ (InlineDisposition _) = GT
|
|
||||||
dispoOrder (AttachmentDisposition t1) (AttachmentDisposition t2) = compare t1 t2
|
|
||||||
|
|
||||||
disposition2widget :: Disposition -> Widget
|
|
||||||
disposition2widget (AttachmentDisposition _) = [whamlet|<h3>_{MsgMailFileAttachment}|]
|
|
||||||
disposition2widget (InlineDisposition n) = [whamlet|<h3>_{MsgMenuMailAttachment} #{n}|]
|
|
||||||
disposition2widget DefaultDisposition = mempty
|
|
||||||
|
|
||||||
part2widget :: CryptoUUIDSentMail -> Part -> Widget
|
|
||||||
part2widget cusm Part{partContent=NestedParts ps} =
|
|
||||||
[whamlet|
|
|
||||||
$forall p <- ps
|
|
||||||
^{part2widget cusm p}
|
|
||||||
|]
|
|
||||||
part2widget cusm Part{partContent=PartContent (LB.toStrict -> pc), partType=pt, partDisposition=dispo} =
|
|
||||||
[whamlet|
|
|
||||||
<section>
|
|
||||||
^{disposition2widget dispo}
|
|
||||||
^{showBody}
|
|
||||||
^{showPass}
|
|
||||||
|]
|
|
||||||
where
|
|
||||||
showBody
|
|
||||||
| pt == decodeUtf8 typePlain = toWidget $ preEscapedToHtml $ plainTextToHtml $ decodeUtf8 pc
|
|
||||||
| pt == decodeUtf8 typeHtml = toWidget $ preEscapedToHtml $ plainHtmlToHtml $ decodeUtf8 pc -- preEscapedToHtml :: ToMarkup a => a -> Html
|
|
||||||
| pt == decodeUtf8 typeJson =
|
|
||||||
let jw :: Aeson.Value -> Widget = jsonWidget
|
|
||||||
in either str2widget jw $ Aeson.eitherDecodeStrict' pc
|
|
||||||
| pt == decodeUtf8 typePDF
|
|
||||||
, AttachmentDisposition t <- dispo
|
|
||||||
= [whamlet|<a href=@{MailAttachmentR cusm t}>#{t}|]
|
|
||||||
| otherwise = [whamlet|FRADrive cannot decode email parts of type #{pt} yet.|]
|
|
||||||
showPass
|
|
||||||
| pt == decodeUtf8 typePlain
|
|
||||||
, let cw = T.words $ decodeUtf8 pc
|
|
||||||
, Just name <- listBracket ("Inhaber","Gültig") cw -- heursitic for dirving licence renewal letters only; improve
|
|
||||||
<|> listBracket ("Licensee","Valid") cw
|
|
||||||
= let sdn = T.filter (/= '*') (T.unwords $ dropWhile (":"==) name) in
|
|
||||||
liftHandler (runDBRead $ getByFilter [UserDisplayName ==. sdn]) >>= \case
|
|
||||||
Nothing -> mempty -- DEBUG: [whamlet|<h2>Not found: #{sdn}|]
|
|
||||||
Just Entity{entityVal = u@User{userPinPassword=mbpw}} ->
|
|
||||||
[whamlet|
|
|
||||||
<section>
|
|
||||||
$maybe pw <- mbpw
|
|
||||||
<details>
|
|
||||||
<summary>
|
|
||||||
_{MsgAdminUserPinPassword}
|
|
||||||
<p>
|
|
||||||
<dl .deflist>
|
|
||||||
<dt .deflist__dt>
|
|
||||||
^{userWidget u}
|
|
||||||
<dd .deflist__dd>
|
|
||||||
<b>
|
|
||||||
#{pw}
|
|
||||||
<p>
|
|
||||||
_{MsgAdminUserPinPassNotIncluded}
|
|
||||||
$nothing
|
|
||||||
_{MsgAdminUserNoPassword}
|
|
||||||
|]
|
|
||||||
| otherwise = mempty
|
|
||||||
|
|
||||||
------------------------------
|
|
||||||
-- Decode MIME Encoded Word
|
|
||||||
|
|
||||||
-- | decode the MIME encoded-word format, which is used in email headers to encode non-ASCII text. This format is specified in RFC 2047.
|
|
||||||
decodeEncodedWord :: Text -> Text
|
|
||||||
decodeEncodedWord tinp
|
|
||||||
| (pl, T.drop 2 -> cf) <- T.breakOn "=?" tinp
|
|
||||||
, (cw, T.drop 2 -> rm) <- T.breakOn "?=" cf
|
|
||||||
, notNull cw
|
|
||||||
= pl <> decodeEncodedWordHeader cw <> decodeEncodedWord rm
|
|
||||||
| otherwise
|
|
||||||
= tinp
|
|
||||||
|
|
||||||
decodeEncodedWordHeader :: Text -> Text
|
|
||||||
decodeEncodedWordHeader tinp
|
|
||||||
| [enc, bin, cw] <- T.splitOn "?" tinp
|
|
||||||
, "utf-8" == T.toLower enc
|
|
||||||
, "Q" == T.toUpper bin -- Quoted Printable Text
|
|
||||||
= decEncWrdUtf8Q cw
|
|
||||||
-- TODO: add more decoders for other possible encodings here, but "=?utf-8?Q?..?=" is the only one used by Network.Mail.Mime at the moment
|
|
||||||
| otherwise
|
|
||||||
= tinp
|
|
||||||
|
|
||||||
decEncWrdUtf8Q :: Text -> Text
|
|
||||||
decEncWrdUtf8Q tinp
|
|
||||||
| Right ok <- TE.decodeUtf8' $ decWds tinp
|
|
||||||
= ok
|
|
||||||
| otherwise
|
|
||||||
= tinp
|
|
||||||
where
|
|
||||||
decWds :: Text -> S.ByteString
|
|
||||||
decWds t
|
|
||||||
| (h:tl) <- T.splitOn "=" t
|
|
||||||
= mconcat $ TE.encodeUtf8 h : map deco tl
|
|
||||||
| otherwise
|
|
||||||
= TE.encodeUtf8 t
|
|
||||||
|
|
||||||
deco :: Text -> S.ByteString
|
|
||||||
deco w
|
|
||||||
| (c,r) <- T.splitAt 2 w
|
|
||||||
, [(v,"")] <- readHex $ T.unpack c
|
|
||||||
= S.cons v $ TE.encodeUtf8 r
|
|
||||||
| otherwise
|
|
||||||
= TE.encodeUtf8 w
|
|
||||||
@ -13,7 +13,7 @@ import Handler.SystemMessage
|
|||||||
|
|
||||||
import qualified Data.Map.Strict as Map
|
import qualified Data.Map.Strict as Map
|
||||||
import qualified Data.Set as Set
|
import qualified Data.Set as Set
|
||||||
|
|
||||||
import Database.Esqueleto.Utils.TH
|
import Database.Esqueleto.Utils.TH
|
||||||
import qualified Database.Esqueleto.Legacy as E
|
import qualified Database.Esqueleto.Legacy as E
|
||||||
import qualified Database.Esqueleto.Utils as E
|
import qualified Database.Esqueleto.Utils as E
|
||||||
@ -315,16 +315,16 @@ newsUpcomingExams uid = do
|
|||||||
| otherwise -> mempty
|
| otherwise -> mempty
|
||||||
]
|
]
|
||||||
dbtSorting = Map.fromList
|
dbtSorting = Map.fromList
|
||||||
[ ("demo-both", SortColumns $ queryCourse &&& queryExam >>> (\(course,exam)-> [SomeExprValue $ course E.^. CourseShorthand, SomeExprValue $ exam E.^. ExamName]))
|
[ ("demo-both", SortColumn $ queryCourse &&& queryExam >>> (\(_course,exam)-> exam E.^. ExamName))
|
||||||
, ("term", SortColumn $ queryCourse >>> (E.^. CourseTerm ))
|
, ("term", SortColumn $ queryCourse >>> (E.^. CourseTerm ))
|
||||||
, ("school", SortColumn $ queryCourse >>> (E.^. CourseSchool ))
|
, ("school", SortColumn $ queryCourse >>> (E.^. CourseSchool ))
|
||||||
, ("course", SortColumn $ queryCourse >>> (E.^. CourseShorthand ))
|
, ("course", SortColumn $ queryCourse >>> (E.^. CourseShorthand ))
|
||||||
, ("name", SortColumn $ queryExam >>> (E.^. ExamName ))
|
, ("name", SortColumn $ queryExam >>> (E.^. ExamName ))
|
||||||
, ("time", SortColumn $ queryExam >>> (E.^. ExamStart ))
|
, ("time", SortColumn $ queryExam >>> (E.^. ExamStart ))
|
||||||
, ("register-from", SortColumn $ queryExam >>> (E.^. ExamRegisterFrom ))
|
, ("register-from", SortColumn $ queryExam >>> (E.^. ExamRegisterFrom ))
|
||||||
, ("register-to", SortColumn $ queryExam >>> (E.^. ExamRegisterTo ))
|
, ("register-to", SortColumn $ queryExam >>> (E.^. ExamRegisterTo ))
|
||||||
, ("visible", SortColumn $ queryExam >>> (E.^. ExamVisibleFrom ))
|
, ("visible", SortColumn $ queryExam >>> (E.^. ExamVisibleFrom ))
|
||||||
, ("registered", SortColumn $ queryExam >>> (\exam ->
|
, ("registered", SortColumn $ queryExam >>> (\exam ->
|
||||||
E.exists $ E.from $ \registration -> do
|
E.exists $ E.from $ \registration -> do
|
||||||
E.where_ $ registration E.^. ExamRegistrationUser E.==. E.val uid
|
E.where_ $ registration E.^. ExamRegistrationUser E.==. E.val uid
|
||||||
E.where_ $ registration E.^. ExamRegistrationExam E.==. exam E.^. ExamId
|
E.where_ $ registration E.^. ExamRegistrationExam E.==. exam E.^. ExamId
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
-- SPDX-FileCopyrightText: 2022-24 Sarah Vaupel <sarah.vaupel@ifi.lmu.de>,Steffen Jost <jost@tcs.ifi.lmu.de>,Steffen Jost <s.jost@fraport.de>
|
-- SPDX-FileCopyrightText: 2022 Sarah Vaupel <sarah.vaupel@ifi.lmu.de>,Steffen Jost <jost@tcs.ifi.lmu.de>
|
||||||
--
|
--
|
||||||
-- SPDX-License-Identifier: AGPL-3.0-or-later
|
-- SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
@ -20,9 +20,9 @@ import qualified Data.Set as Set
|
|||||||
import qualified Data.Map as Map
|
import qualified Data.Map as Map
|
||||||
|
|
||||||
import Database.Persist.Sql (updateWhereCount)
|
import Database.Persist.Sql (updateWhereCount)
|
||||||
import qualified Database.Esqueleto.Experimental as Ex -- needs TypeApplications Lang-Pragma
|
import qualified Database.Esqueleto.Experimental as Ex -- needs TypeApplications Lang-Pragma
|
||||||
import qualified Database.Esqueleto.Legacy as E -- needed for dbTable using Esqueleto.Legacy
|
import qualified Database.Esqueleto.Legacy as E
|
||||||
import qualified Database.Esqueleto.Utils as E
|
import qualified Database.Esqueleto.Utils as E
|
||||||
import Database.Esqueleto.Utils.TH
|
import Database.Esqueleto.Utils.TH
|
||||||
|
|
||||||
import Utils.Print
|
import Utils.Print
|
||||||
@ -56,7 +56,7 @@ data LRQF = LRQF
|
|||||||
} deriving (Eq, Generic)
|
} deriving (Eq, Generic)
|
||||||
|
|
||||||
makeRenewalForm :: Maybe LRQF -> Form LRQF
|
makeRenewalForm :: Maybe LRQF -> Form LRQF
|
||||||
makeRenewalForm tmpl = identifyForm FIDLmsLetter . validateForm validateLetterRenewQualification $ \html -> do
|
makeRenewalForm tmpl = identifyForm FIDLmsLetter . validateForm validateLetterRenewQualificationF $ \html -> do
|
||||||
-- now_day <- utctDay <$> liftIO getCurrentTime
|
-- now_day <- utctDay <$> liftIO getCurrentTime
|
||||||
flip (renderAForm FormStandard) html $ LRQF
|
flip (renderAForm FormStandard) html $ LRQF
|
||||||
<$> areq textField (fslI MsgPrintLetterType) (lrqfLetter <$> tmpl)
|
<$> areq textField (fslI MsgPrintLetterType) (lrqfLetter <$> tmpl)
|
||||||
@ -71,8 +71,8 @@ makeRenewalForm tmpl = identifyForm FIDLmsLetter . validateForm validateLetterRe
|
|||||||
where
|
where
|
||||||
lmsField = convertField LmsIdent getLmsIdent textField
|
lmsField = convertField LmsIdent getLmsIdent textField
|
||||||
|
|
||||||
validateLetterRenewQualification :: FormValidator LRQF Handler ()
|
validateLetterRenewQualificationF :: FormValidator LRQF Handler ()
|
||||||
validateLetterRenewQualification = -- do
|
validateLetterRenewQualificationF = -- do
|
||||||
-- LRQF{..} <- State.get
|
-- LRQF{..} <- State.get
|
||||||
return ()
|
return ()
|
||||||
|
|
||||||
@ -82,7 +82,7 @@ lrqf2letter LRQF{..}
|
|||||||
usr <- getUser lrqfUser
|
usr <- getUser lrqfUser
|
||||||
rcvr <- mapM getUser lrqfSuper
|
rcvr <- mapM getUser lrqfSuper
|
||||||
now <- liftIO getCurrentTime
|
now <- liftIO getCurrentTime
|
||||||
let letter = LetterRenewQualification
|
let letter = LetterRenewQualificationF
|
||||||
{ lmsLogin = lrqfIdent
|
{ lmsLogin = lrqfIdent
|
||||||
, lmsPin = lrqfPin
|
, lmsPin = lrqfPin
|
||||||
, qualHolderID = usr ^. _entityKey
|
, qualHolderID = usr ^. _entityKey
|
||||||
@ -94,8 +94,6 @@ lrqf2letter LRQF{..}
|
|||||||
, qualShort = lrqfQuali ^. _qualificationShorthand . _CI
|
, qualShort = lrqfQuali ^. _qualificationShorthand . _CI
|
||||||
, qualSchool = lrqfQuali ^. _qualificationSchool
|
, qualSchool = lrqfQuali ^. _qualificationSchool
|
||||||
, qualDuration = lrqfQuali ^. _qualificationValidDuration
|
, qualDuration = lrqfQuali ^. _qualificationValidDuration
|
||||||
, qualRenewAuto = lrqfQuali ^. _qualificationElearningRenews
|
|
||||||
, qualELimit = lrqfQuali ^. _qualificationElearningLimit
|
|
||||||
, isReminder = lrqfReminder
|
, isReminder = lrqfReminder
|
||||||
}
|
}
|
||||||
return (fromMaybe usr rcvr, SomeLetter letter)
|
return (fromMaybe usr rcvr, SomeLetter letter)
|
||||||
@ -133,12 +131,11 @@ instance Finite PJTableAction
|
|||||||
nullaryPathPiece ''PJTableAction $ camelToPathPiece' 2
|
nullaryPathPiece ''PJTableAction $ camelToPathPiece' 2
|
||||||
embedRenderMessage ''UniWorX ''PJTableAction id
|
embedRenderMessage ''UniWorX ''PJTableAction id
|
||||||
|
|
||||||
|
-- Not yet needed, since there is no additional data for now:
|
||||||
data PJTableActionData = PJActAcknowledgeData | PJActReprintData { ignoreReroute :: Maybe Bool }
|
data PJTableActionData = PJActAcknowledgeData | PJActReprintData { ignoreReroute :: Maybe Bool }
|
||||||
deriving (Eq, Ord, Read, Show, Generic)
|
deriving (Eq, Ord, Read, Show, Generic)
|
||||||
|
|
||||||
|
|
||||||
type PJTableExpr = ( E.SqlExpr (Entity PrintJob)
|
type PJTableExpr = ( E.SqlExpr (Entity PrintJob)
|
||||||
`E.LeftOuterJoin` E.SqlExpr (Maybe (Entity User))
|
|
||||||
`E.LeftOuterJoin` E.SqlExpr (Maybe (Entity User))
|
`E.LeftOuterJoin` E.SqlExpr (Maybe (Entity User))
|
||||||
`E.LeftOuterJoin` E.SqlExpr (Maybe (Entity User))
|
`E.LeftOuterJoin` E.SqlExpr (Maybe (Entity User))
|
||||||
`E.LeftOuterJoin` E.SqlExpr (Maybe (Entity Course))
|
`E.LeftOuterJoin` E.SqlExpr (Maybe (Entity Course))
|
||||||
@ -146,24 +143,21 @@ type PJTableExpr = ( E.SqlExpr (Entity PrintJob)
|
|||||||
)
|
)
|
||||||
|
|
||||||
queryPrintJob :: PJTableExpr -> E.SqlExpr (Entity PrintJob)
|
queryPrintJob :: PJTableExpr -> E.SqlExpr (Entity PrintJob)
|
||||||
queryPrintJob = $(sqlLOJproj 6 1)
|
queryPrintJob = $(sqlLOJproj 5 1)
|
||||||
|
|
||||||
queryRecipient :: PJTableExpr -> E.SqlExpr (Maybe (Entity User))
|
queryRecipient :: PJTableExpr -> E.SqlExpr (Maybe (Entity User))
|
||||||
queryRecipient = $(sqlLOJproj 6 2)
|
queryRecipient = $(sqlLOJproj 5 2)
|
||||||
|
|
||||||
queryAffected :: PJTableExpr -> E.SqlExpr (Maybe (Entity User))
|
|
||||||
queryAffected = $(sqlLOJproj 6 3)
|
|
||||||
|
|
||||||
querySender :: PJTableExpr -> E.SqlExpr (Maybe (Entity User))
|
querySender :: PJTableExpr -> E.SqlExpr (Maybe (Entity User))
|
||||||
querySender = $(sqlLOJproj 6 4)
|
querySender = $(sqlLOJproj 5 3)
|
||||||
|
|
||||||
queryCourse :: PJTableExpr -> E.SqlExpr (Maybe (Entity Course))
|
queryCourse :: PJTableExpr -> E.SqlExpr (Maybe (Entity Course))
|
||||||
queryCourse = $(sqlLOJproj 6 5)
|
queryCourse = $(sqlLOJproj 5 4)
|
||||||
|
|
||||||
queryQualification :: PJTableExpr -> E.SqlExpr (Maybe (Entity Qualification))
|
queryQualification :: PJTableExpr -> E.SqlExpr (Maybe (Entity Qualification))
|
||||||
queryQualification = $(sqlLOJproj 6 6)
|
queryQualification = $(sqlLOJproj 5 5)
|
||||||
|
|
||||||
type PJTableData = DBRow (Entity PrintJob, Maybe (Entity User), Maybe (Entity User), Maybe (Entity User), Maybe (Entity Course), Maybe (Entity Qualification))
|
type PJTableData = DBRow (Entity PrintJob, Maybe (Entity User), Maybe (Entity User), Maybe (Entity Course), Maybe (Entity Qualification))
|
||||||
|
|
||||||
resultPrintJob :: Lens' PJTableData (Entity PrintJob)
|
resultPrintJob :: Lens' PJTableData (Entity PrintJob)
|
||||||
resultPrintJob = _dbrOutput . _1
|
resultPrintJob = _dbrOutput . _1
|
||||||
@ -171,36 +165,30 @@ resultPrintJob = _dbrOutput . _1
|
|||||||
resultRecipient :: Traversal' PJTableData (Entity User)
|
resultRecipient :: Traversal' PJTableData (Entity User)
|
||||||
resultRecipient = _dbrOutput . _2 . _Just
|
resultRecipient = _dbrOutput . _2 . _Just
|
||||||
|
|
||||||
resultAffected :: Traversal' PJTableData (Entity User)
|
|
||||||
resultAffected = _dbrOutput . _3 . _Just
|
|
||||||
|
|
||||||
resultSender :: Traversal' PJTableData (Entity User)
|
resultSender :: Traversal' PJTableData (Entity User)
|
||||||
resultSender = _dbrOutput . _4 . _Just
|
resultSender = _dbrOutput . _3 . _Just
|
||||||
|
|
||||||
resultCourse :: Traversal' PJTableData (Entity Course)
|
resultCourse :: Traversal' PJTableData (Entity Course)
|
||||||
resultCourse = _dbrOutput . _5 . _Just
|
resultCourse = _dbrOutput . _4 . _Just
|
||||||
|
|
||||||
resultQualification :: Traversal' PJTableData (Entity Qualification)
|
resultQualification :: Traversal' PJTableData (Entity Qualification)
|
||||||
resultQualification = _dbrOutput . _6 . _Just
|
resultQualification = _dbrOutput . _5 . _Just
|
||||||
|
|
||||||
pjTableQuery :: PJTableExpr -> E.SqlQuery
|
pjTableQuery :: PJTableExpr -> E.SqlQuery
|
||||||
( E.SqlExpr (Entity PrintJob)
|
( E.SqlExpr (Entity PrintJob)
|
||||||
, E.SqlExpr (Maybe (Entity User))
|
, E.SqlExpr (Maybe (Entity User))
|
||||||
, E.SqlExpr (Maybe (Entity User))
|
, E.SqlExpr (Maybe (Entity User))
|
||||||
, E.SqlExpr (Maybe (Entity User))
|
|
||||||
, E.SqlExpr (Maybe (Entity Course))
|
, E.SqlExpr (Maybe (Entity Course))
|
||||||
, E.SqlExpr (Maybe (Entity Qualification)))
|
, E.SqlExpr (Maybe (Entity Qualification)))
|
||||||
pjTableQuery (printJob `E.LeftOuterJoin` recipient
|
pjTableQuery (printJob `E.LeftOuterJoin` recipient
|
||||||
`E.LeftOuterJoin` affected
|
|
||||||
`E.LeftOuterJoin` sender
|
`E.LeftOuterJoin` sender
|
||||||
`E.LeftOuterJoin` course
|
`E.LeftOuterJoin` course
|
||||||
`E.LeftOuterJoin` quali ) = do
|
`E.LeftOuterJoin` quali ) = do
|
||||||
E.on $ printJob E.^. PrintJobRecipient E.==. recipient E.?. UserId
|
E.on $ printJob E.^. PrintJobRecipient E.==. recipient E.?. UserId
|
||||||
E.on $ printJob E.^. PrintJobAffected E.==. affected E.?. UserId
|
|
||||||
E.on $ printJob E.^. PrintJobSender E.==. sender E.?. UserId
|
E.on $ printJob E.^. PrintJobSender E.==. sender E.?. UserId
|
||||||
E.on $ printJob E.^. PrintJobCourse E.==. course E.?. CourseId
|
E.on $ printJob E.^. PrintJobCourse E.==. course E.?. CourseId
|
||||||
E.on $ printJob E.^. PrintJobQualification E.==. quali E.?. QualificationId
|
E.on $ printJob E.^. PrintJobQualification E.==. quali E.?. QualificationId
|
||||||
return (printJob, recipient, affected, sender, course, quali)
|
return (printJob, recipient, sender, course, quali)
|
||||||
|
|
||||||
mkPJTable :: DB (FormResult (PJTableActionData, Set PrintJobId), Widget)
|
mkPJTable :: DB (FormResult (PJTableActionData, Set PrintJobId), Widget)
|
||||||
mkPJTable = do
|
mkPJTable = do
|
||||||
@ -218,7 +206,6 @@ mkPJTable = do
|
|||||||
, sortable (Just "apcid") (i18nCell MsgPrintJobApcAcknowledge)$ \( view $ resultPrintJob . _entityVal . _printJobApcIdent -> t) -> textCell t
|
, sortable (Just "apcid") (i18nCell MsgPrintJobApcAcknowledge)$ \( view $ resultPrintJob . _entityVal . _printJobApcIdent -> t) -> textCell t
|
||||||
, sortable (Just "name") (i18nCell MsgPrintJobName) $ \( view $ resultPrintJob . _entityVal . _printJobName -> n) -> textCell n
|
, sortable (Just "name") (i18nCell MsgPrintJobName) $ \( view $ resultPrintJob . _entityVal . _printJobName -> n) -> textCell n
|
||||||
, sortable (Just "recipient") (i18nCell MsgPrintRecipient) $ \(preview resultRecipient -> u) -> maybeCell u $ cellHasUserLink AdminUserR
|
, sortable (Just "recipient") (i18nCell MsgPrintRecipient) $ \(preview resultRecipient -> u) -> maybeCell u $ cellHasUserLink AdminUserR
|
||||||
, sortable (Just "affected") (i18nCell MsgPrintAffected) $ \(preview resultAffected -> u) -> maybeCell u $ cellHasUserLink AdminUserR
|
|
||||||
, sortable (Just "sender") (i18nCell MsgPrintSender) $ \(preview resultSender -> u) -> maybeCell u $ cellHasUserLink AdminUserR
|
, sortable (Just "sender") (i18nCell MsgPrintSender) $ \(preview resultSender -> u) -> maybeCell u $ cellHasUserLink AdminUserR
|
||||||
, sortable (Just "course") (i18nCell MsgPrintCourse) $ \(preview $ resultCourse . _entityVal -> c) -> maybeCell c courseCell
|
, sortable (Just "course") (i18nCell MsgPrintCourse) $ \(preview $ resultCourse . _entityVal -> c) -> maybeCell c courseCell
|
||||||
, sortable (Just "qualification")(i18nCell MsgPrintQualification) $ \(preview $ resultQualification . _entityVal -> q) -> maybeCell q qualificationCell
|
, sortable (Just "qualification")(i18nCell MsgPrintQualification) $ \(preview $ resultQualification . _entityVal -> q) -> maybeCell q qualificationCell
|
||||||
@ -231,7 +218,6 @@ mkPJTable = do
|
|||||||
, single ("acknowledged" , SortColumn $ queryPrintJob >>> (E.^. PrintJobAcknowledged))
|
, single ("acknowledged" , SortColumn $ queryPrintJob >>> (E.^. PrintJobAcknowledged))
|
||||||
, single ("apcid" , SortColumn $ queryPrintJob >>> (E.^. PrintJobApcIdent))
|
, single ("apcid" , SortColumn $ queryPrintJob >>> (E.^. PrintJobApcIdent))
|
||||||
, single ("recipient" , sortUserNameBareM queryRecipient)
|
, single ("recipient" , sortUserNameBareM queryRecipient)
|
||||||
, single ("affected" , sortUserNameBareM queryAffected)
|
|
||||||
, single ("sender" , sortUserNameBareM querySender )
|
, single ("sender" , sortUserNameBareM querySender )
|
||||||
, single ("course" , SortColumn $ queryCourse >>> (E.?. CourseName))
|
, single ("course" , SortColumn $ queryCourse >>> (E.?. CourseName))
|
||||||
, single ("qualification", SortColumn $ queryQualification >>> (E.?. QualificationName))
|
, single ("qualification", SortColumn $ queryQualification >>> (E.?. QualificationName))
|
||||||
@ -244,7 +230,6 @@ mkPJTable = do
|
|||||||
, single ("created" , FilterColumn . E.mkDayFilter $ views (to queryPrintJob) (E.^. PrintJobCreated))
|
, single ("created" , FilterColumn . E.mkDayFilter $ views (to queryPrintJob) (E.^. PrintJobCreated))
|
||||||
--, single ("created" , FilterColumn . E.mkDayBetweenFilter $ views (to queryPrintJob) (E.^. PrintJobCreated))
|
--, single ("created" , FilterColumn . E.mkDayBetweenFilter $ views (to queryPrintJob) (E.^. PrintJobCreated))
|
||||||
, single ("recipient" , FilterColumn . E.mkContainsFilterWithCommaPlus Just $ views (to queryRecipient) (E.?. UserDisplayName))
|
, single ("recipient" , FilterColumn . E.mkContainsFilterWithCommaPlus Just $ views (to queryRecipient) (E.?. UserDisplayName))
|
||||||
, single ("affected" , FilterColumn . E.mkContainsFilterWithCommaPlus Just $ views (to queryAffected) (E.?. UserDisplayName))
|
|
||||||
, single ("sender" , FilterColumn . E.mkContainsFilterWithCommaPlus Just $ views (to querySender) (E.?. UserDisplayName))
|
, single ("sender" , FilterColumn . E.mkContainsFilterWithCommaPlus Just $ views (to querySender) (E.?. UserDisplayName))
|
||||||
, single ("course" , FilterColumn . E.mkContainsFilterWith (Just . CI.mk) $ views (to queryCourse) (E.?. CourseName))
|
, single ("course" , FilterColumn . E.mkContainsFilterWith (Just . CI.mk) $ views (to queryCourse) (E.?. CourseName))
|
||||||
, single ("qualification", FilterColumn . E.mkContainsFilterWith (Just . CI.mk) $ views (to queryQualification) (E.?. QualificationName))
|
, single ("qualification", FilterColumn . E.mkContainsFilterWith (Just . CI.mk) $ views (to queryQualification) (E.?. QualificationName))
|
||||||
@ -259,12 +244,11 @@ mkPJTable = do
|
|||||||
--, prismAForm (singletonFilter "created" . maybePrism _PathPiece) mPrev ((,) <$> aopt (hoistField lift dayField) (fslI MsgPrintJobCreated)
|
--, prismAForm (singletonFilter "created" . maybePrism _PathPiece) mPrev ((,) <$> aopt (hoistField lift dayField) (fslI MsgPrintJobCreated)
|
||||||
-- <*> aopt (hoistField lift dayField) (fslI MsgPrintJobCreated)
|
-- <*> aopt (hoistField lift dayField) (fslI MsgPrintJobCreated)
|
||||||
-- )
|
-- )
|
||||||
, prismAForm (singletonFilter "recipient" . maybePrism _PathPiece) mPrev $ aopt (hoistField lift textField) (fslI MsgPrintRecipient & setTooltip MsgTableFilterCommaPlusShort)
|
, prismAForm (singletonFilter "recipient" . maybePrism _PathPiece) mPrev $ aopt (hoistField lift textField) (fslI MsgPrintRecipient & setTooltip MsgTableFilterCommaPlus)
|
||||||
, prismAForm (singletonFilter "affected" . maybePrism _PathPiece) mPrev $ aopt (hoistField lift textField) (fslI MsgPrintAffected & setTooltip MsgTableFilterCommaPlusShort)
|
, prismAForm (singletonFilter "sender" . maybePrism _PathPiece) mPrev $ aopt (hoistField lift textField) (fslI MsgPrintSender & setTooltip MsgTableFilterCommaPlus)
|
||||||
, prismAForm (singletonFilter "sender" . maybePrism _PathPiece) mPrev $ aopt (hoistField lift textField) (fslI MsgPrintSender & setTooltip MsgTableFilterCommaPlusShort)
|
|
||||||
, prismAForm (singletonFilter "course" . maybePrism _PathPiece) mPrev $ aopt (hoistField lift textField) (fslI MsgPrintCourse)
|
, prismAForm (singletonFilter "course" . maybePrism _PathPiece) mPrev $ aopt (hoistField lift textField) (fslI MsgPrintCourse)
|
||||||
, prismAForm (singletonFilter "qualification". maybePrism _PathPiece) mPrev $ aopt (hoistField lift textField) (fslI MsgPrintQualification)
|
, prismAForm (singletonFilter "qualification". maybePrism _PathPiece) mPrev $ aopt (hoistField lift textField) (fslI MsgPrintQualification)
|
||||||
, prismAForm (singletonFilter "lmsid" . maybePrism _PathPiece) mPrev $ aopt (hoistField lift textField) (fslI MsgPrintLmsUser & setTooltip MsgTableFilterCommaPlusShort)
|
, prismAForm (singletonFilter "lmsid" . maybePrism _PathPiece) mPrev $ aopt (hoistField lift textField) (fslI MsgPrintLmsUser & setTooltip MsgTableFilterCommaPlus)
|
||||||
, prismAForm (singletonFilter "apcid" . maybePrism _PathPiece) mPrev $ aopt (hoistField lift textField) (fslI MsgPrintJobApcAcknowledge & setTooltip MsgTableFilterComma)
|
, prismAForm (singletonFilter "apcid" . maybePrism _PathPiece) mPrev $ aopt (hoistField lift textField) (fslI MsgPrintJobApcAcknowledge & setTooltip MsgTableFilterComma)
|
||||||
, prismAForm (singletonFilter "acknowledged" . maybePrism _PathPiece) mPrev $ aopt (boolField . Just $ SomeMessage MsgBoolIrrelevant) (fslI MsgPrintJobAcknowledged)
|
, prismAForm (singletonFilter "acknowledged" . maybePrism _PathPiece) mPrev $ aopt (boolField . Just $ SomeMessage MsgBoolIrrelevant) (fslI MsgPrintJobAcknowledged)
|
||||||
]
|
]
|
||||||
|
|||||||
@ -2,12 +2,10 @@
|
|||||||
--
|
--
|
||||||
-- SPDX-License-Identifier: AGPL-3.0-or-later
|
-- SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
{-# OPTIONS_GHC -fno-warn-orphans #-} -- needed for HasEntity and HasUser instances
|
|
||||||
|
|
||||||
module Handler.Profile
|
module Handler.Profile
|
||||||
( getProfileR, postProfileR
|
( getProfileR, postProfileR
|
||||||
, getForProfileR, postForProfileR
|
, getForProfileR, postForProfileR
|
||||||
, getProfileDataR, makeProfileData
|
, getProfileDataR, makeProfileData
|
||||||
, getForProfileDataR
|
, getForProfileDataR
|
||||||
, getAuthPredsR, postAuthPredsR
|
, getAuthPredsR, postAuthPredsR
|
||||||
, getUserNotificationR, postUserNotificationR
|
, getUserNotificationR, postUserNotificationR
|
||||||
@ -19,10 +17,7 @@ module Handler.Profile
|
|||||||
import Import
|
import Import
|
||||||
|
|
||||||
import Handler.Utils
|
import Handler.Utils
|
||||||
import Handler.Utils.AvsUpdate
|
|
||||||
import Handler.Utils.Profile
|
import Handler.Utils.Profile
|
||||||
import Handler.Utils.Users
|
|
||||||
import Handler.Utils.Company
|
|
||||||
|
|
||||||
import Utils.Print (validCmdArgument)
|
import Utils.Print (validCmdArgument)
|
||||||
|
|
||||||
@ -31,12 +26,9 @@ import Utils.Print (validCmdArgument)
|
|||||||
import Data.Map ((!))
|
import Data.Map ((!))
|
||||||
import qualified Data.Map as Map
|
import qualified Data.Map as Map
|
||||||
import qualified Data.Set as Set
|
import qualified Data.Set as Set
|
||||||
import Database.Esqueleto.Experimental ((:&)(..))
|
import qualified Database.Esqueleto.Legacy as E
|
||||||
import qualified Database.Esqueleto.Experimental as E
|
|
||||||
import qualified Database.Esqueleto.Legacy as EL (on,from)
|
|
||||||
import qualified Database.Esqueleto.Utils as E
|
import qualified Database.Esqueleto.Utils as E
|
||||||
import qualified Database.Esqueleto.PostgreSQL as E
|
-- import Database.Esqueleto ((^.))
|
||||||
|
|
||||||
import qualified Data.Text as Text
|
import qualified Data.Text as Text
|
||||||
import Data.List (inits)
|
import Data.List (inits)
|
||||||
|
|
||||||
@ -47,9 +39,6 @@ import Jobs
|
|||||||
import Foundation.Yesod.Auth (updateUserLanguage)
|
import Foundation.Yesod.Auth (updateUserLanguage)
|
||||||
|
|
||||||
|
|
||||||
{-# ANN module ("HLint: ignore Functor law" :: String) #-}
|
|
||||||
|
|
||||||
|
|
||||||
data ExamOfficeSettings
|
data ExamOfficeSettings
|
||||||
= ExamOfficeSettings
|
= ExamOfficeSettings
|
||||||
{ eosettingsGetSynced :: Bool
|
{ eosettingsGetSynced :: Bool
|
||||||
@ -76,11 +65,11 @@ data SettingsForm = SettingsForm
|
|||||||
, stgDownloadFiles :: Bool
|
, stgDownloadFiles :: Bool
|
||||||
, stgWarningDays :: NominalDiffTime
|
, stgWarningDays :: NominalDiffTime
|
||||||
, stgShowSex :: Bool
|
, stgShowSex :: Bool
|
||||||
|
|
||||||
, stgPinPassword :: Maybe Text
|
, stgPinPassword :: Maybe Text
|
||||||
, stgPrefersPostal :: Bool
|
, stgPrefersPostal :: Bool
|
||||||
, stgPostAddress :: Maybe StoredMarkup
|
, stgPostAddress :: Maybe StoredMarkup
|
||||||
|
|
||||||
, stgTelephone :: Maybe Text
|
, stgTelephone :: Maybe Text
|
||||||
, stgMobile :: Maybe Text
|
, stgMobile :: Maybe Text
|
||||||
|
|
||||||
@ -119,11 +108,10 @@ instance RenderMessage UniWorX NotificationTriggerKind where
|
|||||||
makeSettingForm :: Maybe SettingsForm -> Form SettingsForm
|
makeSettingForm :: Maybe SettingsForm -> Form SettingsForm
|
||||||
makeSettingForm template html = do
|
makeSettingForm template html = do
|
||||||
MsgRenderer mr <- getMsgRenderer
|
MsgRenderer mr <- getMsgRenderer
|
||||||
-- isAdmin <- checkAdmin
|
|
||||||
(result, widget) <- flip (renderAForm FormStandard) html $ SettingsForm
|
(result, widget) <- flip (renderAForm FormStandard) html $ SettingsForm
|
||||||
<$ aformSection MsgFormPersonalAppearance
|
<$ aformSection MsgFormPersonalAppearance
|
||||||
<*> areq (textField & cfStrip) (fslI MsgUserDisplayName & setTooltip MsgUserDisplayNameRulesBelow) (stgDisplayName <$> template)
|
<*> areq (textField & cfStrip) (fslI MsgUserDisplayName & setTooltip MsgUserDisplayNameRulesBelow) (stgDisplayName <$> template)
|
||||||
<*> areq (textField & cfStrip & cfCI) (fslI MsgUserDisplayEmail & setTooltip MsgUserDisplayEmailTip) (stgDisplayEmail <$> template)
|
<*> areq (emailField & cfStrip & cfCI) (fslI MsgUserDisplayEmail & setTooltip MsgUserDisplayEmailTip) (stgDisplayEmail <$> template)
|
||||||
<* aformSection MsgFormCosmetics
|
<* aformSection MsgFormCosmetics
|
||||||
<*> areq (natFieldI MsgFavouritesNotNatural)
|
<*> areq (natFieldI MsgFavouritesNotNatural)
|
||||||
(fslpI MsgFavourites (mr MsgFavouritesPlaceholder) & setTooltip MsgFavouritesTip) (stgMaxFavourites <$> template)
|
(fslpI MsgFavourites (mr MsgFavouritesPlaceholder) & setTooltip MsgFavouritesTip) (stgMaxFavourites <$> template)
|
||||||
@ -149,9 +137,9 @@ makeSettingForm template html = do
|
|||||||
<*> aopt htmlField (fslI MsgPostAddress & setTooltip MsgPostAddressTip) (stgPostAddress <$> template)
|
<*> aopt htmlField (fslI MsgPostAddress & setTooltip MsgPostAddressTip) (stgPostAddress <$> template)
|
||||||
|
|
||||||
<*> aopt (textField & cfStrip) (fslI MsgUserTelephone) (stgTelephone <$> template)
|
<*> aopt (textField & cfStrip) (fslI MsgUserTelephone) (stgTelephone <$> template)
|
||||||
<*> aopt (textField & cfStrip) (fslI MsgUserMobile ) (stgMobile <$> template)
|
<*> aopt (textField & cfStrip) (fslI MsgUserMobile ) (stgMobile <$> template)
|
||||||
|
|
||||||
<*> examOfficeForm (stgExamOfficeSettings <$> template)
|
<*> examOfficeForm (stgExamOfficeSettings <$> template)
|
||||||
<*> schoolsForm (stgSchools <$> template)
|
<*> schoolsForm (stgSchools <$> template)
|
||||||
<*> notificationForm (stgNotificationSettings <$> template)
|
<*> notificationForm (stgNotificationSettings <$> template)
|
||||||
return (result, widget) -- no validation here, done later by validateSettings
|
return (result, widget) -- no validation here, done later by validateSettings
|
||||||
@ -163,7 +151,7 @@ schoolsForm template = formToAForm $ schoolsFormView =<< renderWForm FormStandar
|
|||||||
where
|
where
|
||||||
schoolsForm' :: WForm Handler (FormResult (Set SchoolId))
|
schoolsForm' :: WForm Handler (FormResult (Set SchoolId))
|
||||||
schoolsForm' = do
|
schoolsForm' = do
|
||||||
allSchools <- liftHandler . runDBRead $ selectList [] [Asc SchoolName]
|
allSchools <- liftHandler . runDB $ selectList [] [Asc SchoolName]
|
||||||
|
|
||||||
let
|
let
|
||||||
schoolForm (Entity ssh School{schoolName})
|
schoolForm (Entity ssh School{schoolName})
|
||||||
@ -198,28 +186,28 @@ notificationForm template = wFormToAForm $ do
|
|||||||
-> return False
|
-> return False
|
||||||
NTKCourseParticipant
|
NTKCourseParticipant
|
||||||
| Just uid <- mbUid
|
| Just uid <- mbUid
|
||||||
-> fmap not . E.selectExists . EL.from $ \courseParticipant ->
|
-> fmap not . E.selectExists . E.from $ \courseParticipant ->
|
||||||
E.where_ $ courseParticipant E.^. CourseParticipantUser E.==. E.val uid
|
E.where_ $ courseParticipant E.^. CourseParticipantUser E.==. E.val uid
|
||||||
E.&&. courseParticipant E.^. CourseParticipantState E.==. E.val CourseParticipantActive
|
E.&&. courseParticipant E.^. CourseParticipantState E.==. E.val CourseParticipantActive
|
||||||
NTKSubmissionUser
|
NTKSubmissionUser
|
||||||
| Just uid <- mbUid
|
| Just uid <- mbUid
|
||||||
-> fmap not . E.selectExists . EL.from $ \submissionUser ->
|
-> fmap not . E.selectExists . E.from $ \submissionUser ->
|
||||||
E.where_ $ submissionUser E.^. SubmissionUserUser E.==. E.val uid
|
E.where_ $ submissionUser E.^. SubmissionUserUser E.==. E.val uid
|
||||||
NTKExamParticipant
|
NTKExamParticipant
|
||||||
| Just uid <- mbUid
|
| Just uid <- mbUid
|
||||||
-> fmap not . E.selectExists . EL.from $ \examRegistration ->
|
-> fmap not . E.selectExists . E.from $ \examRegistration ->
|
||||||
E.where_ $ examRegistration E.^. ExamRegistrationUser E.==. E.val uid
|
E.where_ $ examRegistration E.^. ExamRegistrationUser E.==. E.val uid
|
||||||
NTKCorrector
|
NTKCorrector
|
||||||
| Just uid <- mbUid
|
| Just uid <- mbUid
|
||||||
-> fmap not . E.selectExists . EL.from $ \sheetCorrector ->
|
-> fmap not . E.selectExists . E.from $ \sheetCorrector ->
|
||||||
E.where_ $ sheetCorrector E.^. SheetCorrectorUser E.==. E.val uid
|
E.where_ $ sheetCorrector E.^. SheetCorrectorUser E.==. E.val uid
|
||||||
NTKCourseLecturer
|
NTKCourseLecturer
|
||||||
| Just uid <- mbUid
|
| Just uid <- mbUid
|
||||||
-> fmap not . E.selectExists . EL.from $ \lecturer ->
|
-> fmap not . E.selectExists . E.from $ \lecturer ->
|
||||||
E.where_ $ lecturer E.^. LecturerUser E.==. E.val uid
|
E.where_ $ lecturer E.^. LecturerUser E.==. E.val uid
|
||||||
NTKFunctionary f
|
NTKFunctionary f
|
||||||
| Just uid <- mbUid
|
| Just uid <- mbUid
|
||||||
-> fmap not . E.selectExists . EL.from $ \userFunction ->
|
-> fmap not . E.selectExists . E.from $ \userFunction ->
|
||||||
E.where_ $ userFunction E.^. UserFunctionUser E.==. E.val uid
|
E.where_ $ userFunction E.^. UserFunctionUser E.==. E.val uid
|
||||||
E.&&. userFunction E.^. UserFunctionFunction E.==. E.val f
|
E.&&. userFunction E.^. UserFunctionFunction E.==. E.val f
|
||||||
_ | Nothing <- mbUid -> return False -- Show everything for not-logged-in users (e.g. if they presented a token)
|
_ | Nothing <- mbUid -> return False -- Show everything for not-logged-in users (e.g. if they presented a token)
|
||||||
@ -233,7 +221,7 @@ notificationForm template = wFormToAForm $ do
|
|||||||
let
|
let
|
||||||
ntfs nt = fslI nt & case nt of
|
ntfs nt = fslI nt & case nt of
|
||||||
_other -> id
|
_other -> id
|
||||||
|
|
||||||
nsForm nt
|
nsForm nt
|
||||||
| maybe False ntHidden $ ntSection nt
|
| maybe False ntHidden $ ntSection nt
|
||||||
= pure $ notificationAllowed def nt
|
= pure $ notificationAllowed def nt
|
||||||
@ -304,7 +292,7 @@ examOfficeForm template = wFormToAForm $ do
|
|||||||
| otherwise
|
| otherwise
|
||||||
-> FormSuccess $ Map.singleton kStart (Left nLabel)
|
-> FormSuccess $ Map.singleton kStart (Left nLabel)
|
||||||
return (addRes', $(widgetFile "profile/exam-office-labels/add"))
|
return (addRes', $(widgetFile "profile/exam-office-labels/add"))
|
||||||
|
|
||||||
miCell :: ListPosition
|
miCell :: ListPosition
|
||||||
-> Either ExamOfficeLabelName ExamOfficeLabelId
|
-> Either ExamOfficeLabelName ExamOfficeLabelId
|
||||||
-> Maybe EOLabelData
|
-> Maybe EOLabelData
|
||||||
@ -373,13 +361,11 @@ validateSettings User{..} = do
|
|||||||
userDisplayName' <- use _stgDisplayName
|
userDisplayName' <- use _stgDisplayName
|
||||||
guardValidation MsgUserDisplayNameInvalid $
|
guardValidation MsgUserDisplayNameInvalid $
|
||||||
userDisplayName == userDisplayName' || -- unchanged or valid (invalid displayNames delivered by LDAP are preserved)
|
userDisplayName == userDisplayName' || -- unchanged or valid (invalid displayNames delivered by LDAP are preserved)
|
||||||
validDisplayName userTitle userFirstName userSurname userDisplayName'
|
validDisplayName userTitle userFirstName userSurname userDisplayName'
|
||||||
|
|
||||||
userDisplayEmail' <- use _stgDisplayEmail
|
userDisplayEmail' <- use _stgDisplayEmail
|
||||||
guardValidation (MsgInvalidEmailAddressWith $ CI.original userDisplayEmail') $
|
guardValidation (MsgInvalidEmailAddressWith $ CI.original userDisplayEmail') $
|
||||||
validEmail' userDisplayEmail' || -- valid
|
validEmail' userDisplayEmail'
|
||||||
userDisplayEmail' == userDisplayEmail || -- unchanged
|
|
||||||
userDisplayEmail' == userEmail -- euqal to default, which is then ignored
|
|
||||||
|
|
||||||
userPostAddress' <- use _stgPostAddress
|
userPostAddress' <- use _stgPostAddress
|
||||||
let postalNotSet = isNothing userPostAddress' -- TODO $ canonical userPostAddress'
|
let postalNotSet = isNothing userPostAddress' -- TODO $ canonical userPostAddress'
|
||||||
@ -421,7 +407,7 @@ nullaryPathPiece ''ProfileAnchor $ camelToPathPiece' 1
|
|||||||
|
|
||||||
getForProfileR , postForProfileR :: CryptoUUIDUser -> Handler Html
|
getForProfileR , postForProfileR :: CryptoUUIDUser -> Handler Html
|
||||||
getForProfileR = postForProfileR
|
getForProfileR = postForProfileR
|
||||||
postForProfileR cID = do
|
postForProfileR cID = do
|
||||||
uid <- decrypt cID
|
uid <- decrypt cID
|
||||||
user <- runDB $ get404 uid
|
user <- runDB $ get404 uid
|
||||||
serveProfileR (uid, user)
|
serveProfileR (uid, user)
|
||||||
@ -434,8 +420,8 @@ serveProfileR :: (UserId, User) -> Handler Html
|
|||||||
serveProfileR (uid, user@User{..}) = do
|
serveProfileR (uid, user@User{..}) = do
|
||||||
currentRoute <- fromMaybe ProfileR <$> getCurrentRoute
|
currentRoute <- fromMaybe ProfileR <$> getCurrentRoute
|
||||||
(userSchools, userExamOfficeLabels) <- runDB $ do
|
(userSchools, userExamOfficeLabels) <- runDB $ do
|
||||||
userSchools <- fmap (setOf $ folded . _Value) . E.select . EL.from $ \school -> do
|
userSchools <- fmap (setOf $ folded . _Value) . E.select . E.from $ \school -> do
|
||||||
E.where_ . E.exists . EL.from $ \userSchool ->
|
E.where_ . E.exists . E.from $ \userSchool ->
|
||||||
E.where_ $ E.not_ (userSchool E.^. UserSchoolIsOptOut)
|
E.where_ $ E.not_ (userSchool E.^. UserSchoolIsOptOut)
|
||||||
E.&&. userSchool E.^. UserSchoolUser E.==. E.val uid
|
E.&&. userSchool E.^. UserSchoolUser E.==. E.val uid
|
||||||
E.&&. userSchool E.^. UserSchoolSchool E.==. school E.^. SchoolId
|
E.&&. userSchool E.^. UserSchoolSchool E.==. school E.^. SchoolId
|
||||||
@ -444,7 +430,7 @@ serveProfileR (uid, user@User{..}) = do
|
|||||||
return (userSchools, userExamOfficeLabels)
|
return (userSchools, userExamOfficeLabels)
|
||||||
let settingsTemplate = Just SettingsForm
|
let settingsTemplate = Just SettingsForm
|
||||||
{ stgDisplayName = userDisplayName
|
{ stgDisplayName = userDisplayName
|
||||||
, stgDisplayEmail = if userDisplayEmail == "" then userEmail else userDisplayEmail
|
, stgDisplayEmail = userDisplayEmail
|
||||||
, stgMaxFavourites = userMaxFavourites
|
, stgMaxFavourites = userMaxFavourites
|
||||||
, stgMaxFavouriteTerms = userMaxFavouriteTerms
|
, stgMaxFavouriteTerms = userMaxFavouriteTerms
|
||||||
, stgTheme = userTheme
|
, stgTheme = userTheme
|
||||||
@ -458,7 +444,7 @@ serveProfileR (uid, user@User{..}) = do
|
|||||||
, stgShowSex = userShowSex
|
, stgShowSex = userShowSex
|
||||||
, stgPinPassword = userPinPassword
|
, stgPinPassword = userPinPassword
|
||||||
, stgPostAddress = userPostAddress
|
, stgPostAddress = userPostAddress
|
||||||
, stgPrefersPostal = userPrefersPostal
|
, stgPrefersPostal = userPrefersPostal
|
||||||
, stgTelephone = userTelephone
|
, stgTelephone = userTelephone
|
||||||
, stgMobile = userMobile
|
, stgMobile = userMobile
|
||||||
, stgExamOfficeSettings = ExamOfficeSettings
|
, stgExamOfficeSettings = ExamOfficeSettings
|
||||||
@ -473,12 +459,11 @@ serveProfileR (uid, user@User{..}) = do
|
|||||||
now <- liftIO getCurrentTime
|
now <- liftIO getCurrentTime
|
||||||
isAdmin <- checkAdmin
|
isAdmin <- checkAdmin
|
||||||
thisUser <- fromMaybe uid <$> maybeAuthId
|
thisUser <- fromMaybe uid <$> maybeAuthId
|
||||||
let changeEmailByUser = not isAdmin || thisUser == uid
|
let changeEmailByUser = userDisplayEmail /= stgDisplayEmail && (not isAdmin || thisUser == uid)
|
||||||
changeEmailProper = userDisplayEmail /= stgDisplayEmail && userEmail /= stgDisplayEmail
|
|
||||||
runDBJobs $ do
|
runDBJobs $ do
|
||||||
update uid $
|
update uid $
|
||||||
[ UserDisplayEmail =. stgDisplayEmail | not changeEmailByUser, changeEmailProper ] ++ -- DisplayEmail changes by Users must be confirmed, see 480 below
|
[ UserDisplayEmail =. stgDisplayEmail | not changeEmailByUser ] ++ -- DisplayEmail changes by Users must be confirmed, see 480 below
|
||||||
[ UserPostLastUpdate =. Just now | userPostAddress /= stgPostAddress ] ++
|
[ UserPostLastUpdate =. Just now | userPostAddress /= stgPostAddress ] ++
|
||||||
[ UserDisplayName =. stgDisplayName
|
[ UserDisplayName =. stgDisplayName
|
||||||
, UserMaxFavourites =. stgMaxFavourites
|
, UserMaxFavourites =. stgMaxFavourites
|
||||||
, UserMaxFavouriteTerms =. stgMaxFavouriteTerms
|
, UserMaxFavouriteTerms =. stgMaxFavouriteTerms
|
||||||
@ -499,7 +484,7 @@ serveProfileR (uid, user@User{..}) = do
|
|||||||
, UserExamOfficeGetLabels =. (stgExamOfficeSettings & eosettingsGetLabels)
|
, UserExamOfficeGetLabels =. (stgExamOfficeSettings & eosettingsGetLabels)
|
||||||
]
|
]
|
||||||
updateFavourites Nothing
|
updateFavourites Nothing
|
||||||
when (changeEmailByUser && changeEmailProper) $ do
|
when changeEmailByUser $ do
|
||||||
queueDBJob $ JobChangeUserDisplayEmail uid stgDisplayEmail
|
queueDBJob $ JobChangeUserDisplayEmail uid stgDisplayEmail
|
||||||
addMessageI Info $ MsgUserDisplayEmailChangeSent stgDisplayEmail
|
addMessageI Info $ MsgUserDisplayEmailChangeSent stgDisplayEmail
|
||||||
let
|
let
|
||||||
@ -525,8 +510,8 @@ serveProfileR (uid, user@User{..}) = do
|
|||||||
oldExamLabels = userExamOfficeLabels
|
oldExamLabels = userExamOfficeLabels
|
||||||
newExamLabels = stgExamOfficeSettings & eosettingsLabels
|
newExamLabels = stgExamOfficeSettings & eosettingsLabels
|
||||||
forM_ oldExamLabels $ \(Entity eolid ExamOfficeLabel{..}) -> unless (Right eolid `Map.member` newExamLabels || Left examOfficeLabelName `Map.member` newExamLabels) $ do
|
forM_ oldExamLabels $ \(Entity eolid ExamOfficeLabel{..}) -> unless (Right eolid `Map.member` newExamLabels || Left examOfficeLabelName `Map.member` newExamLabels) $ do
|
||||||
E.delete . EL.from $ \examOfficeExternalExamLabel -> E.where_ $ examOfficeExternalExamLabel E.^. ExamOfficeExternalExamLabelLabel E.==. E.val eolid
|
E.delete . E.from $ \examOfficeExternalExamLabel -> E.where_ $ examOfficeExternalExamLabel E.^. ExamOfficeExternalExamLabelLabel E.==. E.val eolid
|
||||||
E.delete . EL.from $ \examOfficeExamLabel -> E.where_ $ examOfficeExamLabel E.^. ExamOfficeExamLabelLabel E.==. E.val eolid
|
E.delete . E.from $ \examOfficeExamLabel -> E.where_ $ examOfficeExamLabel E.^. ExamOfficeExamLabelLabel E.==. E.val eolid
|
||||||
when (csvExportLabel userCsvOptions == Just examOfficeLabelName) $
|
when (csvExportLabel userCsvOptions == Just examOfficeLabelName) $
|
||||||
update uid [ UserCsvOptions =. userCsvOptions { csvExportLabel = Nothing } ]
|
update uid [ UserCsvOptions =. userCsvOptions { csvExportLabel = Nothing } ]
|
||||||
delete eolid
|
delete eolid
|
||||||
@ -590,122 +575,72 @@ getProfileDataR = do
|
|||||||
getForProfileDataR :: CryptoUUIDUser -> Handler Html
|
getForProfileDataR :: CryptoUUIDUser -> Handler Html
|
||||||
getForProfileDataR cID = do
|
getForProfileDataR cID = do
|
||||||
uid <- decrypt cID
|
uid <- decrypt cID
|
||||||
(user,dataWidget) <- runDB $ bind2 (get404 uid) $ makeProfileData . Entity uid
|
(user,dataWidget) <- runDB $ bind2 (get404 uid) $ makeProfileData . Entity uid
|
||||||
defaultLayout $ do
|
defaultLayout $ do
|
||||||
setTitleI $ MsgHeadingForProfileData $ userDisplayName user
|
setTitleI $ MsgHeadingForProfileData $ userDisplayName user
|
||||||
dataWidget
|
dataWidget
|
||||||
|
|
||||||
-- data TableHasData = TableHasData{tableHasRows :: Bool, tableWidget :: Widget}
|
|
||||||
-- a poor man's record subsitute
|
|
||||||
|
|
||||||
{-
|
|
||||||
type TableHasData = (Bool, Widget)
|
|
||||||
tableHasRows :: TableHasData -> Bool
|
|
||||||
tableHasRows = fst
|
|
||||||
tableWidget :: TableHasData -> Widget
|
|
||||||
tableWidget = snd
|
|
||||||
-}
|
|
||||||
|
|
||||||
-- | Given a header message, a bool and widget; display widget and header only if the boolean is true
|
|
||||||
maybeTable :: (RenderMessage UniWorX a)
|
|
||||||
=> a -> (Bool, Widget) -> Widget
|
|
||||||
maybeTable m = maybeTable' m Nothing Nothing
|
|
||||||
|
|
||||||
maybeTable' :: (RenderMessage UniWorX a)
|
|
||||||
=> a -> Maybe a -> Maybe Widget -> (Bool, Widget) -> Widget
|
|
||||||
maybeTable' _ Nothing _ (False, _ ) = mempty
|
|
||||||
maybeTable' _ (Just nodata) _ (False, _ ) =
|
|
||||||
[whamlet|
|
|
||||||
<div .container>
|
|
||||||
_{nodata}
|
|
||||||
|]
|
|
||||||
maybeTable' hdr _ mbRemark (True ,tbl) =
|
|
||||||
[whamlet|
|
|
||||||
<div .container>
|
|
||||||
<h2> _{hdr}
|
|
||||||
<div .container>
|
|
||||||
^{tbl}
|
|
||||||
$maybe remark <- mbRemark
|
|
||||||
<em>_{MsgProfileRemark}
|
|
||||||
\ ^{remark}
|
|
||||||
|]
|
|
||||||
|
|
||||||
|
|
||||||
makeProfileData :: Entity User -> DB Widget
|
makeProfileData :: Entity User -> DB Widget
|
||||||
makeProfileData usrEnt@(Entity uid usrVal@User{..}) = do
|
makeProfileData (Entity uid User{..}) = do
|
||||||
now <- liftIO getCurrentTime
|
now <- liftIO getCurrentTime
|
||||||
avsId <- entityVal <<$>> getBy (UniqueUserAvsUser uid)
|
avsId <- entityVal <<$>> getBy (UniqueUserAvsUser uid)
|
||||||
let usrAutomatic :: CU_UserAvs_User -> Widget
|
externalUsers <- (\(Entity _ ExternalUser{..}) -> (externalUserUser, externalUserSource, externalUserLastSync)) <<$>> selectList [ ExternalUserUser ==. userIdent ] []
|
||||||
usrAutomatic = updateAutomatic . mayUpdate usrVal avsId . mkCheckUpdate
|
|
||||||
addressLinkdIcon <- messageTooltip <$> messageIconI Info IconLink MsgAddressIsLinkedTip
|
-- avsCards <- maybe (pure mempty) (\a -> selectList [UserAvsCardPersonId ==. userAvsPersonId a] []) avsId
|
||||||
(actualPrefersPostal, (actualPostAddress, postalAutomatic), (actualDisplayEmail, emailAutomatic)) <- getPostalPreferenceAndAddress' usrEnt
|
|
||||||
functions <- Map.fromListWith Set.union . map (\(Entity _ UserFunction{..}) -> (userFunctionFunction, Set.singleton userFunctionSchool)) <$> selectList [UserFunctionUser ==. uid] []
|
functions <- Map.fromListWith Set.union . map (\(Entity _ UserFunction{..}) -> (userFunctionFunction, Set.singleton userFunctionSchool)) <$> selectList [UserFunctionUser ==. uid] []
|
||||||
lecture_corrector <- E.select $ E.distinct $ EL.from $ \(sheet `E.InnerJoin` corrector `E.InnerJoin` course) -> do
|
lecture_corrector <- E.select $ E.distinct $ E.from $ \(sheet `E.InnerJoin` corrector `E.InnerJoin` course) -> do
|
||||||
EL.on $ sheet E.^. SheetCourse E.==. course E.^. CourseId
|
E.on $ sheet E.^. SheetCourse E.==. course E.^. CourseId
|
||||||
EL.on $ sheet E.^. SheetId E.==. corrector E.^. SheetCorrectorSheet
|
E.on $ sheet E.^. SheetId E.==. corrector E.^. SheetCorrectorSheet
|
||||||
E.where_ $ corrector E.^. SheetCorrectorUser E.==. E.val uid
|
E.where_ $ corrector E.^. SheetCorrectorUser E.==. E.val uid
|
||||||
return (course E.^. CourseTerm, course E.^. CourseSchool, course E.^. CourseShorthand)
|
return (course E.^. CourseTerm, course E.^. CourseSchool, course E.^. CourseShorthand)
|
||||||
studies <- E.select $ EL.from $ \(studydegree `E.InnerJoin` studyfeat `E.InnerJoin` studyterms) -> do
|
studies <- E.select $ E.from $ \(studydegree `E.InnerJoin` studyfeat `E.InnerJoin` studyterms) -> do
|
||||||
EL.on $ studyfeat E.^. StudyFeaturesField E.==. studyterms E.^. StudyTermsId
|
E.on $ studyfeat E.^. StudyFeaturesField E.==. studyterms E.^. StudyTermsId
|
||||||
EL.on $ studyfeat E.^. StudyFeaturesDegree E.==. studydegree E.^. StudyDegreeId
|
E.on $ studyfeat E.^. StudyFeaturesDegree E.==. studydegree E.^. StudyDegreeId
|
||||||
E.where_ $ studyfeat E.^. StudyFeaturesUser E.==. E.val uid
|
E.where_ $ studyfeat E.^. StudyFeaturesUser E.==. E.val uid
|
||||||
return (studyfeat, studydegree, studyterms)
|
return (studyfeat, studydegree, studyterms)
|
||||||
companies <- wgtCompanies uid
|
companies' <- E.select $ E.from $ \(usrComp `E.InnerJoin` comp) -> do
|
||||||
-- supervisors' <- E.select $ EL.from $ \(spvr `E.InnerJoin` usrSpvr) -> do
|
E.on $ usrComp E.^. UserCompanyCompany E.==. comp E.^. CompanyId
|
||||||
-- EL.on $ spvr E.^. UserSupervisorSupervisor E.==. usrSpvr E.^. UserId
|
E.where_ $ usrComp E.^. UserCompanyUser E.==. E.val uid
|
||||||
-- E.where_ $ spvr E.^. UserSupervisorUser E.==. E.val uid
|
E.orderBy [E.asc (comp E.^. CompanyName)] -- E.desc (usrComp E.^. UserCompanySupervisor),
|
||||||
-- E.orderBy [E.asc (usrSpvr E.^. UserDisplayName)]
|
return (comp E.^. CompanyName, usrComp E.^. UserCompanySupervisor)
|
||||||
-- return (usrSpvr, spvr E.^. UserSupervisorRerouteNotifications)
|
let companies = intersperse (text2markup ", ") $
|
||||||
-- let numSupervisors = length supervisors'
|
(\(E.Value cmpName, E.Value cmpSpr) -> text2markup (CI.original cmpName) <> bool mempty icnSuper cmpSpr) <$> companies'
|
||||||
-- supervisors = intersperse (text2widget ", ") $
|
icnSuper = text2markup " " <> icon IconSupervisor
|
||||||
-- (\(usr, E.Value reroutCom) -> linkUserWidget ForProfileDataR usr <> bool mempty icnReroute reroutCom) <$> supervisors'
|
supervisors' <- E.select $ E.from $ \(spvr `E.InnerJoin` usrSpvr) -> do
|
||||||
-- icnReroute = text2widget " " <> toWgt (icon IconReroute)
|
E.on $ spvr E.^. UserSupervisorSupervisor E.==. usrSpvr E.^. UserId
|
||||||
-- supervisees' <- E.select $ EL.from $ \(spvr `E.InnerJoin` usrSpvr) -> do
|
E.where_ $ spvr E.^. UserSupervisorUser E.==. E.val uid
|
||||||
-- EL.on $ spvr E.^. UserSupervisorUser E.==. usrSpvr E.^. UserId
|
E.orderBy [E.asc (usrSpvr E.^. UserDisplayName)]
|
||||||
-- E.where_ $ spvr E.^. UserSupervisorSupervisor E.==. E.val uid
|
return (usrSpvr, spvr E.^. UserSupervisorRerouteNotifications)
|
||||||
-- return (usrSpvr, spvr E.^. UserSupervisorRerouteNotifications)
|
let numSupervisors = length supervisors'
|
||||||
-- let numSupervisees = length supervisees'
|
supervisors = intersperse (text2widget ", ") $
|
||||||
-- supervisees = intersperse (text2widget ", ") $
|
(\(usr, E.Value reroutCom) -> linkUserWidget ForProfileDataR usr <> bool mempty icnReroute reroutCom) <$> supervisors'
|
||||||
-- (\(usr, E.Value reroutCom) -> linkUserWidget ForProfileDataR usr <> bool mempty icnReroute reroutCom) <$> supervisees'
|
icnReroute = text2widget " " <> toWgt (icon IconLetter)
|
||||||
-- -- icnReroute = text2widget " " <> toWgt (icon IconReroute)
|
supervisees' <- E.select $ E.from $ \(spvr `E.InnerJoin` usrSpvr) -> do
|
||||||
|
E.on $ spvr E.^. UserSupervisorUser E.==. usrSpvr E.^. UserId
|
||||||
|
E.where_ $ spvr E.^. UserSupervisorSupervisor E.==. E.val uid
|
||||||
|
return (usrSpvr, spvr E.^. UserSupervisorRerouteNotifications)
|
||||||
|
let numSupervisees = length supervisees'
|
||||||
|
supervisees = intersperse (text2widget ", ") $
|
||||||
|
(\(usr, E.Value reroutCom) -> linkUserWidget ForProfileDataR usr <> bool mempty icnReroute reroutCom) <$> supervisees'
|
||||||
|
-- icnReroute = text2widget " " <> toWgt (icon IconLetter)
|
||||||
--Tables
|
--Tables
|
||||||
ownedCoursesTable <- mkOwnedCoursesTable uid -- Tabelle mit eigenen Kursen
|
(hasRows, ownedCoursesTable) <- mkOwnedCoursesTable uid -- Tabelle mit eigenen Kursen
|
||||||
enrolledCoursesTable <- mkEnrolledCoursesTable uid -- Tabelle mit allen Teilnehmer: Kurs (link), Datum
|
enrolledCoursesTable <- mkEnrolledCoursesTable uid -- Tabelle mit allen Teilnehmer: Kurs (link), Datum
|
||||||
submissionTable <- mkSubmissionTable uid -- Tabelle mit allen Abgaben und Abgabe-Gruppen
|
submissionTable <- mkSubmissionTable uid -- Tabelle mit allen Abgaben und Abgabe-Gruppen
|
||||||
submissionGroupTable <- mkSubmissionGroupTable uid -- Tabelle mit allen Abgabegruppen
|
submissionGroupTable <- mkSubmissionGroupTable uid -- Tabelle mit allen Abgabegruppen
|
||||||
correctionsTable <- mkCorrectionsTable uid -- Tabelle mit allen Korrektor-Aufgaben
|
correctionsTable <- mkCorrectionsTable uid -- Tabelle mit allen Korrektor-Aufgaben
|
||||||
qualificationsTable <- mkQualificationsTable now uid -- Tabelle mit allen Qualifikationen
|
qualificationsTable <- mkQualificationsTable now uid -- Tabelle mit allen Qualifikationen
|
||||||
supervisorsTable <- mkSupervisorsTable uid -- Tabelle mit allen Supervisors
|
let examTable, ownTutorialTable, tutorialTable :: Widget
|
||||||
superviseesTable <- mkSuperviseesTable actualPrefersPostal uid -- Tabelle mit allen Supervisees
|
examTable = i18n MsgPersonalInfoExamAchievementsWip
|
||||||
countUnderlings <- E.select $ do
|
ownTutorialTable = i18n MsgPersonalInfoOwnTutorialsWip
|
||||||
spr <- E.from $ E.table @UserSupervisor
|
tutorialTable = i18n MsgPersonalInfoTutorialsWip
|
||||||
E.where_ $ spr E.^. UserSupervisorSupervisor E.==. E.val uid
|
|
||||||
return (E.countRows, E.count (spr E.^. UserSupervisorId) `E.filterWhere` (spr E.^. UserSupervisorRerouteNotifications))
|
|
||||||
countSupervisors <- E.select $ do
|
|
||||||
spr <- E.from $ E.table @UserSupervisor
|
|
||||||
E.where_ $ spr E.^. UserSupervisorUser E.==. E.val uid
|
|
||||||
return (E.countRows, E.count (spr E.^. UserSupervisorId) `E.filterWhere` (spr E.^. UserSupervisorRerouteNotifications))
|
|
||||||
let errorCount ((E.Value x, E.Value y):_) = (x,y)
|
|
||||||
errorCount _ = (-1,-1)
|
|
||||||
supervisorsWgt :: Widget =
|
|
||||||
let (nrSupers, nrSupersReroute) = errorCount countSupervisors
|
|
||||||
in maybeTable' (MsgProfileSupervisor nrSupers nrSupersReroute) (Just MsgProfileNoSupervisor)
|
|
||||||
(toMaybe (nrSupersReroute > 0) $ msg2widget $ MsgProfileSupervisorRemark nrSupers nrSupersReroute 0) (nrSupers > 0, supervisorsTable)
|
|
||||||
superviseesWgt :: Widget =
|
|
||||||
let (nrUnderlings, nrUndersReroute) = errorCount countUnderlings
|
|
||||||
in maybeTable' (MsgProfileSupervisee nrUnderlings nrUndersReroute) (Just MsgProfileNoSupervisee)
|
|
||||||
(toMaybe (nrUndersReroute > 0) $ msg2widget $ MsgProfileSuperviseeRemark nrUnderlings nrUndersReroute) (nrUnderlings > 0, superviseesTable)
|
|
||||||
-- let examTable, ownTutorialTable, tutorialTable :: Widget
|
|
||||||
-- examTable = i18n MsgPersonalInfoExamAchievementsWip
|
|
||||||
-- ownTutorialTable = i18n MsgPersonalInfoOwnTutorialsWip
|
|
||||||
-- tutorialTable = i18n MsgPersonalInfoTutorialsWip -- note that tutorials are linked in enrolledCoursesTable
|
|
||||||
|
|
||||||
cID <- encrypt uid
|
cID <- encrypt uid
|
||||||
mCRoute <- getCurrentRoute
|
mCRoute <- getCurrentRoute
|
||||||
showAdminInfo <- pure (mCRoute == Just (AdminUserR cID)) `or2M` hasReadAccessTo (AdminUserR cID)
|
showAdminInfo <- pure (mCRoute == Just (AdminUserR cID)) `or2M` hasReadAccessTo (AdminUserR cID)
|
||||||
tooltipAvsPersNo <- messageI Info MsgAvsPersonNoNotId
|
tooltipAvsPersNo <- messageI Info MsgAvsPersonNoNotId
|
||||||
tooltipAvsPersNoDiffers <- messageI Error MsgAvsPersonNoDiffers
|
tooltipInvalidEmail <- messageI Error MsgInvalidEmailAddress
|
||||||
tooltipInvalidEmail <- messageI Error MsgInvalidEmailAddress
|
|
||||||
let profileRemarks = $(i18nWidgetFile "profile-remarks")
|
let profileRemarks = $(i18nWidgetFile "profile-remarks")
|
||||||
return $(widgetFile "profileData")
|
return $(widgetFile "profileData")
|
||||||
|
|
||||||
@ -722,7 +657,7 @@ mkOwnedCoursesTable =
|
|||||||
withType = id
|
withType = id
|
||||||
|
|
||||||
dbtSQLQuery' uid (course `E.InnerJoin` lecturer) = do
|
dbtSQLQuery' uid (course `E.InnerJoin` lecturer) = do
|
||||||
EL.on $ course E.^. CourseId E.==. lecturer E.^. LecturerCourse
|
E.on $ course E.^. CourseId E.==. lecturer E.^. LecturerCourse
|
||||||
E.where_ $ lecturer E.^. LecturerUser E.==. E.val uid
|
E.where_ $ lecturer E.^. LecturerUser E.==. E.val uid
|
||||||
return ( course E.^. CourseTerm
|
return ( course E.^. CourseTerm
|
||||||
, course E.^. CourseSchool
|
, course E.^. CourseSchool
|
||||||
@ -763,36 +698,26 @@ mkOwnedCoursesTable =
|
|||||||
|
|
||||||
|
|
||||||
-- | Table listing all courses that the given user is enrolled in
|
-- | Table listing all courses that the given user is enrolled in
|
||||||
mkEnrolledCoursesTable :: UserId -> DB (Bool, Widget)
|
mkEnrolledCoursesTable :: UserId -> DB Widget
|
||||||
mkEnrolledCoursesTable uid = do
|
mkEnrolledCoursesTable =
|
||||||
usrTuts <- E.select $ do
|
let withType :: ((E.SqlExpr (Entity Course) `E.InnerJoin` E.SqlExpr (Entity CourseParticipant)) -> a)
|
||||||
(tpar :& tut) <- E.from $ E.table @TutorialParticipant `E.innerJoin` E.table @Tutorial
|
|
||||||
`E.on` (\(tpar :& tut) -> tut E.^. TutorialId E.==. tpar E.^. TutorialParticipantTutorial)
|
|
||||||
E.where_ $ tpar E.^. TutorialParticipantUser E.==. E.val uid
|
|
||||||
E.orderBy [E.asc $ tut E.^. TutorialCourse, E.desc $ tut E.^. TutorialName] -- Data.Map.fromAscListWith reverses tutorials, hence E.desc
|
|
||||||
return (tut E.^. TutorialCourse, tut E.^. TutorialName)
|
|
||||||
|
|
||||||
let usrTutMap :: Map CourseId [TutorialName]
|
|
||||||
usrTutMap = Map.fromAscListWith (++) [(tcid, [tnm]) | (E.Value tcid, E.Value tnm) <- usrTuts]
|
|
||||||
|
|
||||||
withType :: ((E.SqlExpr (Entity Course) `E.InnerJoin` E.SqlExpr (Entity CourseParticipant)) -> a)
|
|
||||||
-> ((E.SqlExpr (Entity Course) `E.InnerJoin` E.SqlExpr (Entity CourseParticipant)) -> a)
|
-> ((E.SqlExpr (Entity Course) `E.InnerJoin` E.SqlExpr (Entity CourseParticipant)) -> a)
|
||||||
withType = id
|
withType = id
|
||||||
|
|
||||||
validator = def & defaultSorting [SortDescBy "time"]
|
validator = def & defaultSorting [SortDescBy "time"]
|
||||||
|
|
||||||
(_1 %~ getAny) <$> dbTableWidget validator
|
in \uid -> dbTableWidget' validator
|
||||||
DBTable
|
DBTable
|
||||||
{ dbtIdent = "courseMembership" :: Text
|
{ dbtIdent = "courseMembership" :: Text
|
||||||
, dbtSQLQuery = \(course `E.InnerJoin` participant) -> do
|
, dbtSQLQuery = \(course `E.InnerJoin` participant) -> do
|
||||||
EL.on $ course E.^. CourseId E.==. participant E.^. CourseParticipantCourse
|
E.on $ course E.^. CourseId E.==. participant E.^. CourseParticipantCourse
|
||||||
E.where_ $ participant E.^. CourseParticipantUser E.==. E.val uid
|
E.where_ $ participant E.^. CourseParticipantUser E.==. E.val uid
|
||||||
E.&&. participant E.^. CourseParticipantState E.==. E.val CourseParticipantActive
|
E.&&. participant E.^. CourseParticipantState E.==. E.val CourseParticipantActive
|
||||||
return (course, participant E.^. CourseParticipantRegistration)
|
return (course, participant E.^. CourseParticipantRegistration)
|
||||||
, dbtRowKey = \(course `E.InnerJoin` _) -> course E.^. CourseId
|
, dbtRowKey = \(course `E.InnerJoin` _) -> course E.^. CourseId
|
||||||
, dbtProj = dbtProjId <&> _dbrOutput . _2 %~ E.unValue
|
, dbtProj = dbtProjId <&> _dbrOutput . _2 %~ E.unValue
|
||||||
, dbtColonnade = mconcat
|
, dbtColonnade = mconcat
|
||||||
[ sortable (Just "term") (i18nCell MsgTableTerm) $ fmap addIndicatorCell
|
[ sortable (Just "term") (i18nCell MsgTableTerm) $
|
||||||
termCell <$> view (_dbrOutput . _1 . _entityVal . _courseTerm)
|
termCell <$> view (_dbrOutput . _1 . _entityVal . _courseTerm)
|
||||||
, sortable (Just "school") (i18nCell MsgTableCourseSchool) . magnify (_dbrOutput . _1 . _entityVal) $
|
, sortable (Just "school") (i18nCell MsgTableCourseSchool) . magnify (_dbrOutput . _1 . _entityVal) $
|
||||||
schoolCell <$> view _courseTerm
|
schoolCell <$> view _courseTerm
|
||||||
@ -802,14 +727,7 @@ mkEnrolledCoursesTable uid = do
|
|||||||
, sortable (Just "time") (i18nCell MsgProfileRegistered) $ do
|
, sortable (Just "time") (i18nCell MsgProfileRegistered) $ do
|
||||||
regTime <- view $ _dbrOutput . _2
|
regTime <- view $ _dbrOutput . _2
|
||||||
return $ dateTimeCell regTime
|
return $ dateTimeCell regTime
|
||||||
, sortable Nothing (i18nCell MsgCourseTutorials) $ \(view $ _dbrOutput . _1 -> Entity{entityKey=cid, entityVal=Course{..}}) ->
|
]
|
||||||
cell [whamlet|
|
|
||||||
<ul .list--iconless>
|
|
||||||
$forall tutName <- maybeMonoid (Map.lookup cid usrTutMap)
|
|
||||||
<li>
|
|
||||||
^{simpleLink (citext2widget tutName) (CTutorialR courseTerm courseSchool courseShorthand tutName TUsersR)}
|
|
||||||
|]
|
|
||||||
]
|
|
||||||
, dbtSorting = Map.fromList
|
, dbtSorting = Map.fromList
|
||||||
[ ( "course", SortColumn $ withType $ \(crse `E.InnerJoin` _) -> crse E.^. CourseName )
|
[ ( "course", SortColumn $ withType $ \(crse `E.InnerJoin` _) -> crse E.^. CourseName )
|
||||||
, ( "term" , SortColumn $ withType $ \(crse `E.InnerJoin` _) -> crse E.^. CourseTerm )
|
, ( "term" , SortColumn $ withType $ \(crse `E.InnerJoin` _) -> crse E.^. CourseTerm )
|
||||||
@ -832,7 +750,7 @@ mkEnrolledCoursesTable uid = do
|
|||||||
|
|
||||||
|
|
||||||
-- | Table listing all submissions for the given user
|
-- | Table listing all submissions for the given user
|
||||||
mkSubmissionTable :: UserId -> DB (Bool, Widget)
|
mkSubmissionTable :: UserId -> DB Widget
|
||||||
mkSubmissionTable =
|
mkSubmissionTable =
|
||||||
let dbtIdent = "submissions" :: Text
|
let dbtIdent = "submissions" :: Text
|
||||||
dbtStyle = def
|
dbtStyle = def
|
||||||
@ -842,9 +760,9 @@ mkSubmissionTable =
|
|||||||
withType = id
|
withType = id
|
||||||
|
|
||||||
dbtSQLQuery' uid (course `E.InnerJoin` sheet `E.InnerJoin` submission `E.InnerJoin` subUser) = do
|
dbtSQLQuery' uid (course `E.InnerJoin` sheet `E.InnerJoin` submission `E.InnerJoin` subUser) = do
|
||||||
EL.on $ submission E.^. SubmissionId E.==. subUser E.^. SubmissionUserSubmission
|
E.on $ submission E.^. SubmissionId E.==. subUser E.^. SubmissionUserSubmission
|
||||||
EL.on $ sheet E.^. SheetId E.==. submission E.^. SubmissionSheet
|
E.on $ sheet E.^. SheetId E.==. submission E.^. SubmissionSheet
|
||||||
EL.on $ course E.^. CourseId E.==. sheet E.^. SheetCourse
|
E.on $ course E.^. CourseId E.==. sheet E.^. SheetCourse
|
||||||
E.where_ $ subUser E.^. SubmissionUserUser E.==. E.val uid
|
E.where_ $ subUser E.^. SubmissionUserUser E.==. E.val uid
|
||||||
let crse = ( course E.^. CourseTerm
|
let crse = ( course E.^. CourseTerm
|
||||||
, course E.^. CourseSchool
|
, course E.^. CourseSchool
|
||||||
@ -855,7 +773,7 @@ mkSubmissionTable =
|
|||||||
dbtRowKey (_ `E.InnerJoin` _ `E.InnerJoin` submission `E.InnerJoin` _) = submission E.^. SubmissionId
|
dbtRowKey (_ `E.InnerJoin` _ `E.InnerJoin` submission `E.InnerJoin` _) = submission E.^. SubmissionId
|
||||||
|
|
||||||
lastSubEdit uid submission = -- latest Edit-Time of this user for submission
|
lastSubEdit uid submission = -- latest Edit-Time of this user for submission
|
||||||
E.subSelectMaybe . EL.from $ \subEdit -> do
|
E.subSelectMaybe . E.from $ \subEdit -> do
|
||||||
E.where_ $ subEdit E.^. SubmissionEditSubmission E.==. submission E.^. SubmissionId
|
E.where_ $ subEdit E.^. SubmissionEditSubmission E.==. submission E.^. SubmissionId
|
||||||
E.&&. subEdit E.^. SubmissionEditUser E.==. E.val (Just uid)
|
E.&&. subEdit E.^. SubmissionEditUser E.==. E.val (Just uid)
|
||||||
return . E.max_ $ subEdit E.^. SubmissionEditTime
|
return . E.max_ $ subEdit E.^. SubmissionEditTime
|
||||||
@ -866,7 +784,7 @@ mkSubmissionTable =
|
|||||||
<&> _dbrOutput . _4 %~ E.unValue
|
<&> _dbrOutput . _4 %~ E.unValue
|
||||||
|
|
||||||
dbtColonnade = mconcat
|
dbtColonnade = mconcat
|
||||||
[ sortable (Just "term") (i18nCell MsgTableTerm) $ fmap addIndicatorCell
|
[ sortable (Just "term") (i18nCell MsgTableTerm) $
|
||||||
termCell <$> view (_dbrOutput . _1 . _1)
|
termCell <$> view (_dbrOutput . _1 . _1)
|
||||||
, sortable (Just "school") (i18nCell MsgTableCourseSchool) . magnify (_dbrOutput . _1 ) $
|
, sortable (Just "school") (i18nCell MsgTableCourseSchool) . magnify (_dbrOutput . _1 ) $
|
||||||
schoolCell <$> view _1
|
schoolCell <$> view _1
|
||||||
@ -910,10 +828,14 @@ mkSubmissionTable =
|
|||||||
dbtExtraReps = []
|
dbtExtraReps = []
|
||||||
in \uid -> let dbtSQLQuery = dbtSQLQuery' uid
|
in \uid -> let dbtSQLQuery = dbtSQLQuery' uid
|
||||||
dbtSorting = dbtSorting' uid
|
dbtSorting = dbtSorting' uid
|
||||||
in (_1 %~ getAny) <$> dbTableWidget validator DBTable{..}
|
in dbTableWidget' validator DBTable{..}
|
||||||
|
-- in do dbtSQLQuery <- dbtSQLQuery'
|
||||||
|
-- dbtSorting <- dbtSorting'
|
||||||
|
-- return $ dbTableWidget' validator $ DBTable {..}
|
||||||
|
|
||||||
|
|
||||||
-- | Table listing all submissions for the given user
|
-- | Table listing all submissions for the given user
|
||||||
mkSubmissionGroupTable :: UserId -> DB (Bool, Widget)
|
mkSubmissionGroupTable :: UserId -> DB Widget
|
||||||
mkSubmissionGroupTable =
|
mkSubmissionGroupTable =
|
||||||
let dbtIdent = "subGroups" :: Text
|
let dbtIdent = "subGroups" :: Text
|
||||||
dbtStyle = def
|
dbtStyle = def
|
||||||
@ -922,8 +844,8 @@ mkSubmissionGroupTable =
|
|||||||
withType = id
|
withType = id
|
||||||
|
|
||||||
dbtSQLQuery' uid (course `E.InnerJoin` sgroup `E.InnerJoin` sguser) = do
|
dbtSQLQuery' uid (course `E.InnerJoin` sgroup `E.InnerJoin` sguser) = do
|
||||||
EL.on $ sguser E.^. SubmissionGroupUserSubmissionGroup E.==. sgroup E.^. SubmissionGroupId
|
E.on $ sguser E.^. SubmissionGroupUserSubmissionGroup E.==. sgroup E.^. SubmissionGroupId
|
||||||
EL.on $ sgroup E.^. SubmissionGroupCourse E.==. course E.^. CourseId
|
E.on $ sgroup E.^. SubmissionGroupCourse E.==. course E.^. CourseId
|
||||||
E.where_ $ sguser E.^. SubmissionGroupUserUser E.==. E.val uid
|
E.where_ $ sguser E.^. SubmissionGroupUserUser E.==. E.val uid
|
||||||
let crse = ( course E.^. CourseTerm
|
let crse = ( course E.^. CourseTerm
|
||||||
, course E.^. CourseSchool
|
, course E.^. CourseSchool
|
||||||
@ -936,7 +858,7 @@ mkSubmissionGroupTable =
|
|||||||
<&> _dbrOutput . _1 %~ $(E.unValueN 3)
|
<&> _dbrOutput . _1 %~ $(E.unValueN 3)
|
||||||
|
|
||||||
dbtColonnade = mconcat
|
dbtColonnade = mconcat
|
||||||
[ sortable (Just "term") (i18nCell MsgTableTerm) $ fmap addIndicatorCell
|
[ sortable (Just "term") (i18nCell MsgTableTerm) $
|
||||||
termCell <$> view (_dbrOutput . _1 . _1)
|
termCell <$> view (_dbrOutput . _1 . _1)
|
||||||
, sortable (Just "school") (i18nCell MsgTableCourseSchool) . magnify (_dbrOutput . _1 ) $
|
, sortable (Just "school") (i18nCell MsgTableCourseSchool) . magnify (_dbrOutput . _1 ) $
|
||||||
schoolCell <$> view _1
|
schoolCell <$> view _1
|
||||||
@ -965,10 +887,10 @@ mkSubmissionGroupTable =
|
|||||||
dbtCsvDecode = Nothing
|
dbtCsvDecode = Nothing
|
||||||
dbtExtraReps = []
|
dbtExtraReps = []
|
||||||
in \uid -> let dbtSQLQuery = dbtSQLQuery' uid
|
in \uid -> let dbtSQLQuery = dbtSQLQuery' uid
|
||||||
in (_1 %~ getAny) <$> dbTableWidget validator DBTable{..}
|
in dbTableWidget' validator DBTable{..}
|
||||||
|
|
||||||
|
|
||||||
mkCorrectionsTable :: UserId -> DB (Bool, Widget)
|
mkCorrectionsTable :: UserId -> DB Widget
|
||||||
mkCorrectionsTable =
|
mkCorrectionsTable =
|
||||||
let dbtIdent = "corrections" :: Text
|
let dbtIdent = "corrections" :: Text
|
||||||
dbtStyle = def
|
dbtStyle = def
|
||||||
@ -976,18 +898,18 @@ mkCorrectionsTable =
|
|||||||
-> ((E.SqlExpr (Entity Course) `E.InnerJoin` E.SqlExpr (Entity Sheet) `E.InnerJoin` E.SqlExpr (Entity SheetCorrector))->a)
|
-> ((E.SqlExpr (Entity Course) `E.InnerJoin` E.SqlExpr (Entity Sheet) `E.InnerJoin` E.SqlExpr (Entity SheetCorrector))->a)
|
||||||
withType = id
|
withType = id
|
||||||
|
|
||||||
corrsAssigned uid sheet = E.subSelectCount . EL.from $ \submission ->
|
corrsAssigned uid sheet = E.subSelectCount . E.from $ \submission ->
|
||||||
E.where_ $ submission E.^. SubmissionSheet E.==. sheet E.^. SheetId
|
E.where_ $ submission E.^. SubmissionSheet E.==. sheet E.^. SheetId
|
||||||
E.&&. submission E.^. SubmissionRatingBy E.==. E.just (E.val uid)
|
E.&&. submission E.^. SubmissionRatingBy E.==. E.just (E.val uid)
|
||||||
|
|
||||||
corrsCorrected uid sheet = E.subSelectCount . EL.from $ \submission ->
|
corrsCorrected uid sheet = E.subSelectCount . E.from $ \submission ->
|
||||||
E.where_ $ submission E.^. SubmissionSheet E.==. sheet E.^. SheetId
|
E.where_ $ submission E.^. SubmissionSheet E.==. sheet E.^. SheetId
|
||||||
E.&&. submission E.^. SubmissionRatingBy E.==. E.just (E.val uid)
|
E.&&. submission E.^. SubmissionRatingBy E.==. E.just (E.val uid)
|
||||||
E.&&. E.not_ (E.isNothing $ submission E.^. SubmissionRatingTime)
|
E.&&. E.not_ (E.isNothing $ submission E.^. SubmissionRatingTime)
|
||||||
|
|
||||||
dbtSQLQuery' uid (course `E.InnerJoin` sheet `E.InnerJoin` corrector) = do
|
dbtSQLQuery' uid (course `E.InnerJoin` sheet `E.InnerJoin` corrector) = do
|
||||||
EL.on $ sheet E.^. SheetId E.==. corrector E.^. SheetCorrectorSheet
|
E.on $ sheet E.^. SheetId E.==. corrector E.^. SheetCorrectorSheet
|
||||||
EL.on $ sheet E.^. SheetCourse E.==. course E.^. CourseId
|
E.on $ sheet E.^. SheetCourse E.==. course E.^. CourseId
|
||||||
E.where_ $ corrector E.^. SheetCorrectorUser E.==. E.val uid
|
E.where_ $ corrector E.^. SheetCorrectorUser E.==. E.val uid
|
||||||
let crse = ( course E.^. CourseTerm
|
let crse = ( course E.^. CourseTerm
|
||||||
, course E.^. CourseSchool
|
, course E.^. CourseSchool
|
||||||
@ -1001,7 +923,7 @@ mkCorrectionsTable =
|
|||||||
<&> _dbrOutput . _2 %~ E.unValue
|
<&> _dbrOutput . _2 %~ E.unValue
|
||||||
|
|
||||||
dbtColonnade = mconcat
|
dbtColonnade = mconcat
|
||||||
[ sortable (Just "term") (i18nCell MsgTableTerm) $ fmap addIndicatorCell
|
[ sortable (Just "term") (i18nCell MsgTableTerm) $
|
||||||
termCellCL <$> view (_dbrOutput . _1)
|
termCellCL <$> view (_dbrOutput . _1)
|
||||||
, sortable (Just "school") (i18nCell MsgTableCourseSchool) $
|
, sortable (Just "school") (i18nCell MsgTableCourseSchool) $
|
||||||
schoolCellCL <$> view (_dbrOutput . _1)
|
schoolCellCL <$> view (_dbrOutput . _1)
|
||||||
@ -1038,7 +960,7 @@ mkCorrectionsTable =
|
|||||||
dbtCsvDecode = Nothing
|
dbtCsvDecode = Nothing
|
||||||
dbtExtraReps = []
|
dbtExtraReps = []
|
||||||
in \uid -> let dbtSQLQuery = dbtSQLQuery' uid
|
in \uid -> let dbtSQLQuery = dbtSQLQuery' uid
|
||||||
in (_1 %~ getAny) <$> dbTableWidget validator DBTable{..}
|
in dbTableWidget' validator DBTable{..}
|
||||||
|
|
||||||
|
|
||||||
-- | Table listing all qualifications that the given user is enrolled in
|
-- | Table listing all qualifications that the given user is enrolled in
|
||||||
@ -1052,29 +974,29 @@ mkQualificationsTable =
|
|||||||
DBTable
|
DBTable
|
||||||
{ dbtIdent = "userQualifications" :: Text
|
{ dbtIdent = "userQualifications" :: Text
|
||||||
, dbtSQLQuery = \(quali `E.InnerJoin` quser `E.LeftOuterJoin` qblock) -> do
|
, dbtSQLQuery = \(quali `E.InnerJoin` quser `E.LeftOuterJoin` qblock) -> do
|
||||||
EL.on $ quser E.^. QualificationUserId E.=?. qblock E.?. QualificationUserBlockQualificationUser
|
E.on $ quser E.^. QualificationUserId E.=?. qblock E.?. QualificationUserBlockQualificationUser
|
||||||
E.&&. qblock `isLatestBlockBefore` E.val now
|
E.&&. qblock `isLatestBlockBefore` E.val now
|
||||||
EL.on $ quser E.^. QualificationUserQualification E.==. quali E.^. QualificationId
|
E.on $ quser E.^. QualificationUserQualification E.==. quali E.^. QualificationId
|
||||||
E.where_ $ quser E.^. QualificationUserUser E.==. E.val uid
|
E.where_ $ quser E.^. QualificationUserUser E.==. E.val uid
|
||||||
return (quali, quser, qblock)
|
return (quali, quser, qblock)
|
||||||
, dbtRowKey = \(_quali `E.InnerJoin` quser `E.LeftOuterJoin` _qblock) -> quser E.^. QualificationUserId
|
, dbtRowKey = \(_quali `E.InnerJoin` quser `E.LeftOuterJoin` _qblock) -> quser E.^. QualificationUserId
|
||||||
, dbtProj = dbtProjId
|
, dbtProj = dbtProjId
|
||||||
, dbtColonnade = mconcat
|
, dbtColonnade = mconcat
|
||||||
[ colSchool (_dbrOutput . _1 . _entityVal . _qualificationSchool)
|
[ colSchool (_dbrOutput . _1 . _entityVal . _qualificationSchool)
|
||||||
, sortable (Just "quali") (i18nCell MsgQualificationName) $ qualificationDescrCell <$> view (_dbrOutput . _1 . _entityVal)
|
, sortable (Just "quali") (i18nCell MsgQualificationName) $ qualificationDescrCell <$> view (_dbrOutput . _1 . _entityVal)
|
||||||
, sortable (Just "first-held") (i18nCell MsgTableQualificationFirstHeld) $ dayCell <$> view (_dbrOutput . _2 . _entityVal . _qualificationUserFirstHeld )
|
, sortable (Just "first-held") (i18nCell MsgTableQualificationFirstHeld) $ dayCell <$> view (_dbrOutput . _2 . _entityVal . _qualificationUserFirstHeld )
|
||||||
, sortable (Just "last-refresh") (i18nCell MsgTableQualificationLastRefresh) $ dayCell <$> view (_dbrOutput . _2 . _entityVal . _qualificationUserLastRefresh)
|
, sortable (Just "last-refresh") (i18nCell MsgTableQualificationLastRefresh) $ dayCell <$> view (_dbrOutput . _2 . _entityVal . _qualificationUserLastRefresh)
|
||||||
, sortable (Just "valid-until") (i18nCell MsgLmsQualificationValidUntil) $ dayCell <$> view (_dbrOutput . _2 . _entityVal . _qualificationUserValidUntil )
|
, sortable (Just "valid-until") (i18nCell MsgLmsQualificationValidUntil) $ dayCell <$> view (_dbrOutput . _2 . _entityVal . _qualificationUserValidUntil )
|
||||||
, sortable (Just "blocked") (i18nCell MsgQualificationValidIndicator & cellTooltip MsgTableQualificationBlockedTooltipSimple) $ \row ->
|
, sortable (Just "blocked") (i18nCell MsgQualificationValidIndicator & cellTooltip MsgTableQualificationBlockedTooltipSimple) $ \row ->
|
||||||
qualificationValidReasonCell False (utctDay now) (row ^? _dbrOutput . _3 . _Just . _entityVal) (row ^. _dbrOutput . _2 . _entityVal)
|
qualificationValidReasonCell False (utctDay now) (row ^? _dbrOutput . _3 . _Just . _entityVal) (row ^. _dbrOutput . _2 . _entityVal)
|
||||||
]
|
]
|
||||||
, dbtSorting = mconcat
|
, dbtSorting = mconcat
|
||||||
[ sortSchool $ to (\(quali `E.InnerJoin` _ `E.LeftOuterJoin` _) -> quali E.^. QualificationSchool)
|
[ sortSchool $ to (\(quali `E.InnerJoin` _ `E.LeftOuterJoin` _) -> quali E.^. QualificationSchool)
|
||||||
, singletonMap "quali" $ SortColumn $ withType $ \(quali `E.InnerJoin` _ `E.LeftOuterJoin` _) -> quali E.^. QualificationName
|
, singletonMap "quali" $ SortColumn $ withType $ \(quali `E.InnerJoin` _ `E.LeftOuterJoin` _) -> quali E.^. QualificationName
|
||||||
, singletonMap "blocked" $ SortColumn $ withType $ \(_ `E.InnerJoin` _ `E.LeftOuterJoin` qblock) -> qblock E.?. QualificationUserBlockFrom
|
, singletonMap "blocked" $ SortColumn $ withType $ \(_ `E.InnerJoin` _ `E.LeftOuterJoin` qblock) -> qblock E.?. QualificationUserBlockFrom
|
||||||
, singletonMap "valid-until" $ SortColumn $ withType $ \(_ `E.InnerJoin` quser `E.LeftOuterJoin` _) -> quser E.^. QualificationUserValidUntil
|
, singletonMap "valid-until" $ SortColumn $ withType $ \(_ `E.InnerJoin` quser `E.LeftOuterJoin` _) -> quser E.^. QualificationUserValidUntil
|
||||||
, singletonMap "last-refresh" $ SortColumn $ withType $ \(_ `E.InnerJoin` quser `E.LeftOuterJoin` _) -> quser E.^. QualificationUserLastRefresh
|
, singletonMap "last-refresh" $ SortColumn $ withType $ \(_ `E.InnerJoin` quser `E.LeftOuterJoin` _) -> quser E.^. QualificationUserLastRefresh
|
||||||
, singletonMap "first-held" $ SortColumn $ withType $ \(_ `E.InnerJoin` quser `E.LeftOuterJoin` _) -> quser E.^. QualificationUserFirstHeld
|
, singletonMap "first-held" $ SortColumn $ withType $ \(_ `E.InnerJoin` quser `E.LeftOuterJoin` _) -> quser E.^. QualificationUserFirstHeld
|
||||||
]
|
]
|
||||||
, dbtFilter = mempty
|
, dbtFilter = mempty
|
||||||
, dbtFilterUI = mempty
|
, dbtFilterUI = mempty
|
||||||
@ -1086,125 +1008,6 @@ mkQualificationsTable =
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
-- Types & Definitions used for both mkSupervisorsTable and mkSuperviseeTable
|
|
||||||
type TblSupervisorExpr = E.SqlExpr (Entity User) `E.InnerJoin` E.SqlExpr (Entity UserSupervisor) -- `E.LeftOuterJoin` E.SqlExpr (Entity Company)
|
|
||||||
type TblSupervisorData = DBRow (Entity User, Entity UserSupervisor)
|
|
||||||
|
|
||||||
queryUser :: TblSupervisorExpr -> E.SqlExpr (Entity User)
|
|
||||||
queryUser = $(E.sqlIJproj 2 1)
|
|
||||||
queryUserSupervisor :: TblSupervisorExpr -> E.SqlExpr (Entity UserSupervisor)
|
|
||||||
queryUserSupervisor = $(E.sqlIJproj 2 2)
|
|
||||||
resultUser :: Lens' TblSupervisorData (Entity User)
|
|
||||||
resultUser = _dbrOutput . _1
|
|
||||||
resultUserSupervisor :: Lens' TblSupervisorData (Entity UserSupervisor)
|
|
||||||
resultUserSupervisor = _dbrOutput . _2
|
|
||||||
|
|
||||||
instance HasEntity TblSupervisorData User where
|
|
||||||
hasEntity = _dbrOutput . _1
|
|
||||||
instance HasUser TblSupervisorData where
|
|
||||||
hasUser = _dbrOutput . _1 . _entityVal
|
|
||||||
|
|
||||||
-- | Table listing all supervisor of the given user
|
|
||||||
mkSupervisorsTable :: UserId -> DB Widget
|
|
||||||
mkSupervisorsTable uid = dbTableWidget' validator DBTable{..}
|
|
||||||
where
|
|
||||||
dbtIdent = "supervisors" :: Text
|
|
||||||
dbtStyle = def
|
|
||||||
|
|
||||||
dbtSQLQuery (usr `E.InnerJoin` spr) = do
|
|
||||||
EL.on $ spr E.^. UserSupervisorSupervisor E.==. usr E.^. UserId
|
|
||||||
E.where_ $ spr E.^. UserSupervisorUser E.==. E.val uid
|
|
||||||
return (usr, spr)
|
|
||||||
dbtRowKey (_ `E.InnerJoin` spr) = spr E.^. UserSupervisorId
|
|
||||||
dbtProj = dbtProjId
|
|
||||||
|
|
||||||
dbtColonnade = mconcat
|
|
||||||
[ colUserNameModalHdr MsgTableSupervisor ForProfileDataR
|
|
||||||
, colUserEmail
|
|
||||||
-- , sortable (Just "rerouted") (i18nCell MsgTableRerouteActive) $ \(view $ resultUserSupervisor . _entityVal . _userSupervisorRerouteNotifications -> b) -> indicatorCell <> ifIconCell b IconReroute
|
|
||||||
-- , sortable (Just "postal-pref") (i18nCell MsgPrefersPostal) $ \(view $ resultUser . _userPrefersPostal -> b) -> iconFixedCell $ iconLetterOrEmail b
|
|
||||||
, sortable (Just "reroute") (i18nCell MsgTableRerouteActive) $ \row ->
|
|
||||||
let isReroute = row ^. resultUserSupervisor . _entityVal ._userSupervisorRerouteNotifications
|
|
||||||
isLetter = row ^. resultUser . _userPrefersPostal
|
|
||||||
in if isReroute
|
|
||||||
then iconCell IconReroute <> spacerCell <> iconFixedCell (iconLetterOrEmail isLetter)
|
|
||||||
else mempty
|
|
||||||
, sortable (Just "cshort") (i18nCell MsgTableCompany) $ \(view $ resultUserSupervisor . _entityVal . _userSupervisorCompany -> mc) -> maybeCell mc (\(unCompanyKey -> c) -> anchorCell (FirmUsersR c) $ citext2widget c)
|
|
||||||
, sortable (Just "reason") (i18nCell MsgTableReason) $ \(view $ resultUserSupervisor . _entityVal . _userSupervisorReason -> mr) -> maybeCell mr textCell
|
|
||||||
]
|
|
||||||
validator = def & defaultSorting [ SortAscBy "cshort", SortAscBy "user-name" ]
|
|
||||||
dbtSorting = mconcat
|
|
||||||
[ singletonMap & uncurry $ sortUserNameLink queryUser
|
|
||||||
, singletonMap & uncurry $ sortUserEmail queryUser
|
|
||||||
, singletonMap "postal-pref" $ SortColumn $ queryUser >>> (E.^. UserPrefersPostal)
|
|
||||||
, singletonMap "rerouted" $ SortColumn $ queryUserSupervisor >>> (E.^. UserSupervisorRerouteNotifications)
|
|
||||||
-- , singletonMap "reroute" $ SortColumn $ queryUserSupervisor &&& queryUser >>> (\(spr,usr) -> mTuple (spr E.^. UserSupervisorRerouteNotifications) (usr E.^. UserPrefersPostal))
|
|
||||||
, singletonMap "reroute" $ SortColumns $ \row ->
|
|
||||||
[ SomeExprValue $ queryUserSupervisor row E.^. UserSupervisorRerouteNotifications
|
|
||||||
, SomeExprValue $ queryUser row E.^. UserPrefersPostal
|
|
||||||
]
|
|
||||||
, singletonMap "cshort" $ SortColumn $ queryUserSupervisor >>> (E.^. UserSupervisorCompany)
|
|
||||||
, singletonMap "reason" $ SortColumn $ queryUserSupervisor >>> (E.^. UserSupervisorReason)
|
|
||||||
]
|
|
||||||
dbtFilter = mconcat
|
|
||||||
[ singletonMap & uncurry $ fltrUserNameEmail queryUser
|
|
||||||
]
|
|
||||||
dbtFilterUI = mempty
|
|
||||||
dbtParams = def
|
|
||||||
dbtCsvEncode = noCsvEncode
|
|
||||||
dbtCsvDecode = Nothing
|
|
||||||
dbtExtraReps = []
|
|
||||||
|
|
||||||
|
|
||||||
-- | Table listing all persons supervised by the given user
|
|
||||||
mkSuperviseesTable ::Bool -> UserId -> DB Widget
|
|
||||||
mkSuperviseesTable userPrefersPostal uid = dbTableWidget' validator DBTable{..}
|
|
||||||
where
|
|
||||||
dbtIdent = "supervisees" :: Text
|
|
||||||
dbtStyle = def
|
|
||||||
|
|
||||||
dbtSQLQuery (usr `E.InnerJoin` spr) = do
|
|
||||||
EL.on $ spr E.^. UserSupervisorUser E.==. usr E.^. UserId
|
|
||||||
E.where_ $ spr E.^. UserSupervisorSupervisor E.==. E.val uid
|
|
||||||
return (usr, spr)
|
|
||||||
dbtRowKey (_ `E.InnerJoin` spr) = spr E.^. UserSupervisorId
|
|
||||||
dbtProj = dbtProjId
|
|
||||||
|
|
||||||
iconCellLetterOrEmail = spacerCell <> iconFixedCell (iconLetterOrEmail userPrefersPostal) -- only notification type of supervisor matters here
|
|
||||||
dbtColonnade = mconcat
|
|
||||||
[ colUserNameModalHdr MsgTableSupervisee ForProfileDataR
|
|
||||||
, colUserEmail
|
|
||||||
-- , sortable (Just "rerouted") (i18nCell MsgTableRerouteActive) $ \(view $ resultUserSupervisor . _entityVal . _userSupervisorRerouteNotifications -> b) -> ifIconCell b IconReroute
|
|
||||||
-- , sortable (Just "postal-pref") (i18nCell MsgPrefersPostal) $ \(view $ resultUser . _userPrefersPostal -> b) -> iconFixedCell $ iconLetterOrEmail b
|
|
||||||
, sortable (Just "reroute") (i18nCell MsgTableRerouteActive) $ \row ->
|
|
||||||
let isReroute = row ^. resultUserSupervisor . _entityVal ._userSupervisorRerouteNotifications
|
|
||||||
in boolCell isReroute $ iconCell IconReroute <> iconCellLetterOrEmail
|
|
||||||
, sortable (Just "cshort") (i18nCell MsgTableCompany) $ \(view $ resultUserSupervisor . _entityVal . _userSupervisorCompany -> mc) -> maybeCell mc (\(unCompanyKey -> c) -> anchorCell (FirmUsersR c) $ citext2widget c)
|
|
||||||
, sortable (Just "reason") (i18nCell MsgTableReason) $ \(view $ resultUserSupervisor . _entityVal . _userSupervisorReason -> mr) -> maybeCell mr textCell
|
|
||||||
]
|
|
||||||
validator = def & defaultSorting [ SortAscBy "cshort", SortAscBy "user-name" ]
|
|
||||||
dbtSorting = mconcat
|
|
||||||
[ singletonMap & uncurry $ sortUserNameLink queryUser
|
|
||||||
, singletonMap & uncurry $ sortUserEmail queryUser
|
|
||||||
-- , singletonMap "postal-pref" $ SortColumn $ queryUser >>> (E.^. UserPrefersPostal)
|
|
||||||
-- , singletonMap "rerouted" $ SortColumn $ queryUserSupervisor >>> (E.^. UserSupervisorRerouteNotifications)
|
|
||||||
, singletonMap "reroute" $ SortColumns $ \row ->
|
|
||||||
[ SomeExprValue $ queryUserSupervisor row E.^. UserSupervisorRerouteNotifications
|
|
||||||
, SomeExprValue $ queryUser row E.^. UserPrefersPostal
|
|
||||||
]
|
|
||||||
, singletonMap "cshort" $ SortColumn $ queryUserSupervisor >>> (E.^. UserSupervisorCompany)
|
|
||||||
, singletonMap "reason" $ SortColumn $ queryUserSupervisor >>> (E.^. UserSupervisorReason)
|
|
||||||
]
|
|
||||||
dbtFilter = mconcat
|
|
||||||
[ singletonMap & uncurry $ fltrUserNameEmail queryUser
|
|
||||||
]
|
|
||||||
dbtFilterUI = mempty
|
|
||||||
dbtParams = def
|
|
||||||
dbtCsvEncode = noCsvEncode
|
|
||||||
dbtCsvDecode = Nothing
|
|
||||||
dbtExtraReps = []
|
|
||||||
|
|
||||||
|
|
||||||
getAuthPredsR, postAuthPredsR :: Handler Html
|
getAuthPredsR, postAuthPredsR :: Handler Html
|
||||||
getAuthPredsR = postAuthPredsR
|
getAuthPredsR = postAuthPredsR
|
||||||
postAuthPredsR = do
|
postAuthPredsR = do
|
||||||
@ -1323,7 +1126,7 @@ postCsvOptionsR = do
|
|||||||
Entity uid User{userCsvOptions} <- requireAuth
|
Entity uid User{userCsvOptions} <- requireAuth
|
||||||
|
|
||||||
userIsExamOffice <- hasReadAccessTo $ ExamOfficeR EOExamsR
|
userIsExamOffice <- hasReadAccessTo $ ExamOfficeR EOExamsR
|
||||||
examOfficeLabels <- if not userIsExamOffice then return mempty else runDB . E.select . EL.from $ \examOfficeLabel -> do
|
examOfficeLabels <- if not userIsExamOffice then return mempty else runDB . E.select . E.from $ \examOfficeLabel -> do
|
||||||
E.where_ $ examOfficeLabel E.^. ExamOfficeLabelUser E.==. E.val uid
|
E.where_ $ examOfficeLabel E.^. ExamOfficeLabelUser E.==. E.val uid
|
||||||
E.orderBy [ E.asc (examOfficeLabel E.^. ExamOfficeLabelName) ]
|
E.orderBy [ E.asc (examOfficeLabel E.^. ExamOfficeLabelName) ]
|
||||||
return $ examOfficeLabel E.^. ExamOfficeLabelName
|
return $ examOfficeLabel E.^. ExamOfficeLabelName
|
||||||
|
|||||||
@ -14,11 +14,12 @@ module Handler.Qualification
|
|||||||
|
|
||||||
import Import
|
import Import
|
||||||
|
|
||||||
import Jobs
|
-- import Jobs
|
||||||
import Handler.Utils
|
import Handler.Utils
|
||||||
import Handler.Utils.Users
|
import Handler.Utils.Users
|
||||||
import Handler.Utils.LMS
|
import Handler.Utils.LMS
|
||||||
|
|
||||||
|
|
||||||
import qualified Data.Set as Set
|
import qualified Data.Set as Set
|
||||||
import qualified Data.Map as Map
|
import qualified Data.Map as Map
|
||||||
import qualified Data.Csv as Csv
|
import qualified Data.Csv as Csv
|
||||||
@ -55,7 +56,7 @@ getQualificationAllR = do
|
|||||||
|
|
||||||
type AllQualificationTableData = DBRow (Entity Qualification, Ex.Value Word64, Ex.Value Word64)
|
type AllQualificationTableData = DBRow (Entity Qualification, Ex.Value Word64, Ex.Value Word64)
|
||||||
resultAllQualification :: Lens' AllQualificationTableData Qualification
|
resultAllQualification :: Lens' AllQualificationTableData Qualification
|
||||||
resultAllQualification = _dbrOutput . _1 . _entityVal
|
resultAllQualification = _dbrOutput . _1 . _entityVal
|
||||||
|
|
||||||
resultAllQualificationActive :: Lens' AllQualificationTableData Word64
|
resultAllQualificationActive :: Lens' AllQualificationTableData Word64
|
||||||
resultAllQualificationActive = _dbrOutput . _2 . _unValue
|
resultAllQualificationActive = _dbrOutput . _2 . _unValue
|
||||||
@ -65,59 +66,53 @@ resultAllQualificationTotal = _dbrOutput . _3 . _unValue
|
|||||||
|
|
||||||
|
|
||||||
mkQualificationAllTable :: Bool -> DB (Any, Widget)
|
mkQualificationAllTable :: Bool -> DB (Any, Widget)
|
||||||
mkQualificationAllTable isAdmin = do
|
mkQualificationAllTable isAdmin = do
|
||||||
svs <- getSupervisees
|
svs <- getSupervisees
|
||||||
now <- liftIO getCurrentTime
|
now <- liftIO getCurrentTime
|
||||||
let
|
let
|
||||||
resultDBTable = DBTable{..}
|
resultDBTable = DBTable{..}
|
||||||
where
|
where
|
||||||
dbtSQLQuery quali = do
|
dbtSQLQuery quali = do
|
||||||
let filterSvs quser = quser Ex.^. QualificationUserQualification Ex.==. quali Ex.^. QualificationId
|
let filterSvs quser = quser Ex.^. QualificationUserQualification Ex.==. quali Ex.^. QualificationId
|
||||||
Ex.&&. (E.val isAdmin E.||. quser Ex.^. QualificationUserUser `Ex.in_` E.vals svs)
|
Ex.&&. (E.val isAdmin E.||. quser Ex.^. QualificationUserUser `Ex.in_` E.vals svs)
|
||||||
cusers = Ex.subSelectCount $ do
|
cusers = Ex.subSelectCount $ do
|
||||||
quser <- Ex.from $ Ex.table @QualificationUser
|
quser <- Ex.from $ Ex.table @QualificationUser
|
||||||
Ex.where_ $ filterSvs quser
|
Ex.where_ $ filterSvs quser
|
||||||
cactive = Ex.subSelectCount $ do
|
cactive = Ex.subSelectCount $ do
|
||||||
quser <- Ex.from $ Ex.table @QualificationUser
|
quser <- Ex.from $ Ex.table @QualificationUser
|
||||||
Ex.where_ $ filterSvs quser Ex.&&. validQualification now quser
|
Ex.where_ $ filterSvs quser Ex.&&. validQualification now quser
|
||||||
return (quali, cactive, cusers)
|
return (quali, cactive, cusers)
|
||||||
dbtRowKey = (Ex.^. QualificationId)
|
dbtRowKey = (Ex.^. QualificationId)
|
||||||
dbtProj = dbtProjId
|
dbtProj = dbtProjId
|
||||||
dbtColonnade = dbColonnade $ mconcat
|
dbtColonnade = dbColonnade $ mconcat
|
||||||
[ colSchool $ resultAllQualification . _qualificationSchool
|
[ colSchool $ resultAllQualification . _qualificationSchool
|
||||||
, sortable (Just "qshort") (i18nCell MsgQualificationShort) $ \(view resultAllQualification -> quali) ->
|
, sortable (Just "qshort") (i18nCell MsgQualificationShort) $ \(view resultAllQualification -> quali) ->
|
||||||
let qsh = qualificationShorthand quali in
|
let qsh = qualificationShorthand quali in
|
||||||
anchorCell (QualificationR (qualificationSchool quali) qsh) $ toWgt qsh
|
anchorCell (QualificationR (qualificationSchool quali) qsh) $ toWgt qsh
|
||||||
, sortable (Just "qname") (i18nCell MsgQualificationName) $ \(view resultAllQualification -> quali) ->
|
, sortable (Just "qname") (i18nCell MsgQualificationName) $ \(view resultAllQualification -> quali) ->
|
||||||
let qsh = qualificationShorthand quali
|
let qsh = qualificationShorthand quali
|
||||||
qnm = qualificationName quali
|
qnm = qualificationName quali
|
||||||
in anchorCell (QualificationR (qualificationSchool quali) qsh) $ toWgt qnm
|
in anchorCell (QualificationR (qualificationSchool quali) qsh) $ toWgt qnm
|
||||||
, sortable Nothing (i18nCell MsgQualificationDescription) $ \(view resultAllQualification -> quali) ->
|
, sortable Nothing (i18nCell MsgQualificationDescription) $ \(view resultAllQualification -> quali) ->
|
||||||
maybeCell (qualificationDescription quali) markupCellLargeModal
|
maybeCell (qualificationDescription quali) markupCellLargeModal
|
||||||
, sortable Nothing (i18nCell MsgQualificationValidDuration & cellTooltip MsgTableDiffDaysTooltip) $
|
, sortable Nothing (i18nCell MsgQualificationValidDuration & cellTooltip MsgTableDiffDaysTooltip) $
|
||||||
foldMap (textCell . formatCalendarDiffDays . fromMonths) . view (resultAllQualification . _qualificationValidDuration)
|
foldMap (textCell . formatCalendarDiffDays . fromMonths) . view (resultAllQualification . _qualificationValidDuration)
|
||||||
, sortable Nothing (i18nCell MsgQualificationRefreshWithin & cellTooltips [SomeMessage MsgQualificationRefreshWithinTooltip , SomeMessage MsgTableDiffDaysTooltip]) $
|
, sortable Nothing (i18nCell MsgQualificationRefreshWithin & cellTooltips [SomeMessage MsgQualificationRefreshWithinTooltip , SomeMessage MsgTableDiffDaysTooltip]) $
|
||||||
foldMap (textCell . formatCalendarDiffDays ) . view (resultAllQualification . _qualificationRefreshWithin)
|
foldMap (textCell . formatCalendarDiffDays ) . view (resultAllQualification . _qualificationRefreshWithin)
|
||||||
, sortable (Just "qelearning") (i18nCell MsgTableLmsElearning & cellTooltip MsgQualificationElearningStart) $ \row ->
|
|
||||||
let elearnstart = row ^. resultAllQualification . _qualificationElearningStart
|
|
||||||
reminder = row ^. resultAllQualification . _qualificationRefreshReminder
|
|
||||||
in tickmarkCell $ elearnstart && isJust reminder
|
|
||||||
, sortable Nothing (i18nCell MsgQualificationRefreshReminder & cellTooltips [SomeMessage MsgQualificationRefreshReminderTooltip, SomeMessage MsgTableDiffDaysTooltip]) $
|
, sortable Nothing (i18nCell MsgQualificationRefreshReminder & cellTooltips [SomeMessage MsgQualificationRefreshReminderTooltip, SomeMessage MsgTableDiffDaysTooltip]) $
|
||||||
foldMap (textCell . formatCalendarDiffDays ) . view (resultAllQualification . _qualificationRefreshReminder)
|
foldMap (textCell . formatCalendarDiffDays ) . view (resultAllQualification . _qualificationRefreshReminder)
|
||||||
, sortable (Just "noteexpiry") (i18nCell MsgQualificationExpiryNotification & cellTooltip MsgQualificationExpiryNotificationTooltip)
|
, sortable (Just "qelearning") (i18nCell MsgTableLmsElearning & cellTooltip MsgQualificationElearningStart)
|
||||||
|
$ tickmarkCell . view (resultAllQualification . _qualificationElearningStart)
|
||||||
|
, sortable (Just "noteexpiry") (i18nCell MsgQualificationExpiryNotification & cellTooltip MsgQualificationExpiryNotificationTooltip)
|
||||||
$ tickmarkCell . view (resultAllQualification . _qualificationExpiryNotification)
|
$ tickmarkCell . view (resultAllQualification . _qualificationExpiryNotification)
|
||||||
-- , sortable (Just "qelearrenew") (i18nCell MsgTableLmsElearningRenews & cellTooltip MsgQualificationElearningRenew)
|
, sortable Nothing (i18nCell MsgTableQualificationIsAvsLicence & cellTooltip MsgTableQualificationIsAvsLicenceTooltip)
|
||||||
-- $ tickmarkCell . view (resultAllQualification . _qualificationElearningRenews)
|
|
||||||
-- , sortable Nothing (i18nCell MsgTableQualificationLmsReuses & cellTooltip MsgTableQualificationLmsReusesTooltip)
|
|
||||||
-- $ \(view (resultAllQualification . _qualificationLmsReuses) -> reuseQid) -> maybeCell reuseQid qualificationIdShortCell
|
|
||||||
, sortable Nothing (i18nCell MsgTableQualificationIsAvsLicence & cellTooltip MsgTableQualificationIsAvsLicenceTooltip)
|
|
||||||
$ \(view (resultAllQualification . _qualificationAvsLicence) -> licence) -> maybeCell licence $ textCell . T.singleton . licence2char
|
$ \(view (resultAllQualification . _qualificationAvsLicence) -> licence) -> maybeCell licence $ textCell . T.singleton . licence2char
|
||||||
, sortable Nothing (i18nCell MsgTableQualificationSapExport & cellTooltip MsgTableQualificationSapExportTooltip)
|
, sortable Nothing (i18nCell MsgTableQualificationSapExport & cellTooltip MsgTableQualificationSapExportTooltip)
|
||||||
$ \(view (resultAllQualification . _qualificationSapId) -> mbSapId) -> tickmarkCell $ isJust mbSapId
|
$ \(view (resultAllQualification . _qualificationSapId) -> mbSapId) -> tickmarkCell $ isJust mbSapId
|
||||||
, sortable Nothing (i18nCell MsgTableQualificationCountActive & cellTooltip MsgTableQualificationCountActiveTooltip)
|
, sortable Nothing (i18nCell MsgTableQualificationCountActive & cellTooltip MsgTableQualificationCountActiveTooltip)
|
||||||
$ \(view resultAllQualificationActive -> n) -> wgtCell $ word2widget n
|
$ \(view resultAllQualificationActive -> n) -> wgtCell $ word2widget n
|
||||||
, sortable Nothing (i18nCell MsgTableQualificationCountTotal) $ wgtCell . word2widget . view resultAllQualificationTotal
|
, sortable Nothing (i18nCell MsgTableQualificationCountTotal) $ wgtCell . word2widget . view resultAllQualificationTotal
|
||||||
]
|
]
|
||||||
dbtSorting = mconcat
|
dbtSorting = mconcat
|
||||||
[
|
[
|
||||||
sortSchool $ to (E.^. QualificationSchool)
|
sortSchool $ to (E.^. QualificationSchool)
|
||||||
@ -139,7 +134,7 @@ mkQualificationAllTable isAdmin = do
|
|||||||
dbtStyle = def { dbsFilterLayout = defaultDBSFilterLayout }
|
dbtStyle = def { dbsFilterLayout = defaultDBSFilterLayout }
|
||||||
dbtParams = def
|
dbtParams = def
|
||||||
dbtIdent :: Text
|
dbtIdent :: Text
|
||||||
dbtIdent = "qualification-overview"
|
dbtIdent = "qualification-overview"
|
||||||
dbtCsvEncode = noCsvEncode
|
dbtCsvEncode = noCsvEncode
|
||||||
dbtCsvDecode = Nothing
|
dbtCsvDecode = Nothing
|
||||||
dbtExtraReps = []
|
dbtExtraReps = []
|
||||||
@ -151,17 +146,18 @@ mkQualificationAllTable isAdmin = do
|
|||||||
|
|
||||||
|
|
||||||
-- getQualificationEditR, postQualificationEditR :: SchoolId -> QualificationShorthand -> Handler Html
|
-- getQualificationEditR, postQualificationEditR :: SchoolId -> QualificationShorthand -> Handler Html
|
||||||
-- getQualificationEditR = postQualificationEditR
|
-- getQualificationEditR = postQualificationEditR
|
||||||
-- postQualificationEditR = error "TODO"
|
-- postQualificationEditR = error "TODO"
|
||||||
|
|
||||||
data QualificationTableCsv = QualificationTableCsv -- Q..T..C.. -> qtc..
|
data QualificationTableCsv = QualificationTableCsv -- Q..T..C.. -> qtc..
|
||||||
{ qtcDisplayName :: UserDisplayName
|
{ qtcDisplayName :: UserDisplayName
|
||||||
, qtcEmail :: UserEmail
|
, qtcEmail :: UserEmail
|
||||||
, qtcCompany :: Maybe Text
|
, qtcCompany :: Maybe Text
|
||||||
|
, qtcCompanyNumbers :: CsvSemicolonList Int
|
||||||
, qtcValidUntil :: Day
|
, qtcValidUntil :: Day
|
||||||
, qtcLastRefresh :: Day
|
, qtcLastRefresh :: Day
|
||||||
, qtcBlockStatus :: Maybe Bool
|
, qtcBlockStatus :: Maybe Bool
|
||||||
, qtcBlockFrom :: Maybe UTCTime
|
, qtcBlockFrom :: Maybe UTCTime
|
||||||
, qtcScheduleRenewal:: Bool
|
, qtcScheduleRenewal:: Bool
|
||||||
, qtcLmsStatusTxt :: Maybe Text
|
, qtcLmsStatusTxt :: Maybe Text
|
||||||
, qtcLmsStatusDay :: Maybe UTCTime
|
, qtcLmsStatusDay :: Maybe UTCTime
|
||||||
@ -173,11 +169,12 @@ qtcExample :: QualificationTableCsv
|
|||||||
qtcExample = QualificationTableCsv
|
qtcExample = QualificationTableCsv
|
||||||
{ qtcDisplayName = "Max Mustermann"
|
{ qtcDisplayName = "Max Mustermann"
|
||||||
, qtcEmail = "m.mustermann@example.com"
|
, qtcEmail = "m.mustermann@example.com"
|
||||||
, qtcCompany = Just "Example Brothers LLC"
|
, qtcCompany = Just "Example Brothers LLC, SecondaryJobs Inc"
|
||||||
|
, qtcCompanyNumbers = CsvSemicolonList [27,69]
|
||||||
, qtcValidUntil = compDay
|
, qtcValidUntil = compDay
|
||||||
, qtcLastRefresh = compDay
|
, qtcLastRefresh = compDay
|
||||||
, qtcBlockStatus = Nothing
|
, qtcBlockStatus = Nothing
|
||||||
, qtcBlockFrom = Nothing
|
, qtcBlockFrom = Nothing
|
||||||
, qtcScheduleRenewal= True
|
, qtcScheduleRenewal= True
|
||||||
, qtcLmsStatusTxt = Just "Success"
|
, qtcLmsStatusTxt = Just "Success"
|
||||||
, qtcLmsStatusDay = Just compTime
|
, qtcLmsStatusDay = Just compTime
|
||||||
@ -207,14 +204,15 @@ instance CsvColumnsExplained QualificationTableCsv where
|
|||||||
csvColumnsExplanations = genericCsvColumnsExplanations qtcOptions $ Map.fromList
|
csvColumnsExplanations = genericCsvColumnsExplanations qtcOptions $ Map.fromList
|
||||||
[ ('qtcDisplayName , SomeMessage MsgLmsUser)
|
[ ('qtcDisplayName , SomeMessage MsgLmsUser)
|
||||||
, ('qtcEmail , SomeMessage MsgTableLmsEmail)
|
, ('qtcEmail , SomeMessage MsgTableLmsEmail)
|
||||||
, ('qtcCompany , SomeMessage MsgTablePrimeCompany)
|
, ('qtcCompany , SomeMessage MsgTableCompanies)
|
||||||
|
, ('qtcCompanyNumbers , SomeMessage MsgTableCompanyNos)
|
||||||
, ('qtcValidUntil , SomeMessage MsgLmsQualificationValidUntil)
|
, ('qtcValidUntil , SomeMessage MsgLmsQualificationValidUntil)
|
||||||
, ('qtcLastRefresh , SomeMessage MsgTableQualificationLastRefresh)
|
, ('qtcLastRefresh , SomeMessage MsgTableQualificationLastRefresh)
|
||||||
, ('qtcBlockStatus , SomeMessage MsgInfoQualificationBlockStatus)
|
, ('qtcBlockStatus , SomeMessage MsgInfoQualificationBlockStatus)
|
||||||
, ('qtcBlockFrom , SomeMessage MsgInfoQualificationBlockFrom)
|
, ('qtcBlockFrom , SomeMessage MsgInfoQualificationBlockFrom)
|
||||||
, ('qtcScheduleRenewal, SomeMessage MsgQualificationScheduleRenewalTooltip)
|
, ('qtcScheduleRenewal, SomeMessage MsgQualificationScheduleRenewalTooltip)
|
||||||
, ('qtcLmsStatusTxt , SomeMessage MsgTableLmsStatus)
|
, ('qtcLmsStatusTxt , SomeMessage MsgTableLmsStatus)
|
||||||
, ('qtcLmsStatusDay , SomeMessage MsgTableLmsStatusDay)
|
, ('qtcLmsStatusDay , SomeMessage MsgTableLmsStatusDay)
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@ -235,7 +233,7 @@ queryLmsUser = $(sqlLOJproj 3 2)
|
|||||||
queryQualBlock :: QualificationTableExpr -> E.SqlExpr (Maybe (Entity QualificationUserBlock))
|
queryQualBlock :: QualificationTableExpr -> E.SqlExpr (Maybe (Entity QualificationUserBlock))
|
||||||
queryQualBlock = $(sqlLOJproj 3 3)
|
queryQualBlock = $(sqlLOJproj 3 3)
|
||||||
|
|
||||||
type QualificationTableData = DBRow (Entity QualificationUser, Entity User, Maybe (Entity LmsUser), Maybe (Entity QualificationUserBlock), E.Value (Maybe CompanyId))
|
type QualificationTableData = DBRow (Entity QualificationUser, Entity User, Maybe (Entity LmsUser), Maybe (Entity QualificationUserBlock), [Entity UserCompany])
|
||||||
|
|
||||||
resultQualUser :: Lens' QualificationTableData (Entity QualificationUser)
|
resultQualUser :: Lens' QualificationTableData (Entity QualificationUser)
|
||||||
resultQualUser = _dbrOutput . _1
|
resultQualUser = _dbrOutput . _1
|
||||||
@ -249,8 +247,8 @@ resultLmsUser = _dbrOutput . _3 . _Just
|
|||||||
resultQualBlock :: Traversal' QualificationTableData (Entity QualificationUserBlock)
|
resultQualBlock :: Traversal' QualificationTableData (Entity QualificationUserBlock)
|
||||||
resultQualBlock = _dbrOutput . _4 . _Just
|
resultQualBlock = _dbrOutput . _4 . _Just
|
||||||
|
|
||||||
resultCompanyId :: Traversal' QualificationTableData CompanyId
|
resultCompanyUser :: Lens' QualificationTableData [Entity UserCompany]
|
||||||
resultCompanyId = _dbrOutput . _5 . _unValue . _Just
|
resultCompanyUser = _dbrOutput . _5
|
||||||
|
|
||||||
|
|
||||||
instance HasEntity QualificationTableData User where
|
instance HasEntity QualificationTableData User where
|
||||||
@ -269,16 +267,15 @@ instance HasQualificationUser QualificationTableData where
|
|||||||
-- hasQualificationUserBlock = resultQualBlock
|
-- hasQualificationUserBlock = resultQualBlock
|
||||||
|
|
||||||
|
|
||||||
data QualificationTableAction
|
data QualificationTableAction
|
||||||
= QualificationActExpire
|
= QualificationActExpire
|
||||||
| QualificationActUnexpire
|
| QualificationActUnexpire
|
||||||
| QualificationActBlockSupervisor
|
| QualificationActBlockSupervisor
|
||||||
| QualificationActBlock
|
| QualificationActBlock
|
||||||
| QualificationActUnblock
|
| QualificationActUnblock
|
||||||
| QualificationActRenew
|
| QualificationActRenew
|
||||||
| QualificationActGrant
|
| QualificationActGrant
|
||||||
| QualificationActStartELearning
|
deriving (Eq, Ord, Enum, Bounded, Read, Show, Generic)
|
||||||
deriving (Eq, Ord, Enum, Bounded, Read, Show, Generic)
|
|
||||||
|
|
||||||
instance Universe QualificationTableAction
|
instance Universe QualificationTableAction
|
||||||
instance Finite QualificationTableAction
|
instance Finite QualificationTableAction
|
||||||
@ -293,16 +290,15 @@ isAdminAct QualificationActBlockSupervisor = False
|
|||||||
isAdminAct _ = True
|
isAdminAct _ = True
|
||||||
-}
|
-}
|
||||||
|
|
||||||
data QualificationTableActionData
|
data QualificationTableActionData
|
||||||
= QualificationActExpireData
|
= QualificationActExpireData
|
||||||
| QualificationActUnexpireData
|
| QualificationActUnexpireData
|
||||||
| QualificationActBlockSupervisorData
|
| QualificationActBlockSupervisorData
|
||||||
| QualificationActBlockData { qualTableActBlockReason :: Text, qualTableActNotify :: Bool, qualTableActRemoveSupervisors :: Bool }
|
| QualificationActBlockData { qualTableActBlockReason :: Text, qualTableActNotify :: Bool, qualTableActRemoveSupervisors :: Bool }
|
||||||
| QualificationActUnblockData { qualTableActBlockReason :: Text, qualTableActNotify :: Bool }
|
| QualificationActUnblockData { qualTableActBlockReason :: Text, qualTableActNotify :: Bool}
|
||||||
| QualificationActRenewData { qualTableActChangeReason :: Text }
|
| QualificationActRenewData { qualTableActChangeReason :: Text}
|
||||||
| QualificationActGrantData { qualTableActGrantUntil :: Day }
|
| QualificationActGrantData { qualTableActGrantUntil :: Day }
|
||||||
| QualificationActStartELearningData -- { qualTableActELearnUntil :: Maybe Day }
|
deriving (Eq, Ord, Show, Generic)
|
||||||
deriving (Eq, Ord, Show, Generic)
|
|
||||||
|
|
||||||
isExpiryAct :: QualificationTableActionData -> Bool
|
isExpiryAct :: QualificationTableActionData -> Bool
|
||||||
isExpiryAct QualificationActExpireData = True
|
isExpiryAct QualificationActExpireData = True
|
||||||
@ -337,23 +333,18 @@ qualificationTableQuery :: UTCTime -> QualificationId -> (_ -> E.SqlExpr (E.Valu
|
|||||||
, E.SqlExpr (Entity User)
|
, E.SqlExpr (Entity User)
|
||||||
, E.SqlExpr (Maybe (Entity LmsUser))
|
, E.SqlExpr (Maybe (Entity LmsUser))
|
||||||
, E.SqlExpr (Maybe (Entity QualificationUserBlock))
|
, E.SqlExpr (Maybe (Entity QualificationUserBlock))
|
||||||
, E.SqlExpr (E.Value (Maybe CompanyId))
|
|
||||||
)
|
)
|
||||||
qualificationTableQuery now qid fltr (qualUser `E.InnerJoin` user `E.LeftOuterJoin` lmsUser `E.LeftOuterJoin` qualBlock) = do
|
qualificationTableQuery now qid fltr (qualUser `E.InnerJoin` user `E.LeftOuterJoin` lmsUser `E.LeftOuterJoin` qualBlock) = do
|
||||||
-- E.distinctOnOrderBy will not work: sorting with dbTable should work, except that columns contained in distinctOnOrderBy cannot be sorted inversely by user; but PostgreSQL leftJoin with distinct filters too many results, see SQL Example lead/lag under jost/misc DevOps
|
-- E.distinctOnOrderBy will not work: sorting with dbTable should work, except that columns contained in distinctOnOrderBy cannot be sorted inversely by user; but PostgreSQL leftJoin with distinct filters too many results, see SQL Example lead/lag under jost/misc DevOps
|
||||||
--
|
--
|
||||||
E.on $ qualBlock E.?. QualificationUserBlockQualificationUser E.?=. qualUser E.^. QualificationUserId
|
E.on $ qualBlock E.?. QualificationUserBlockQualificationUser E.?=. qualUser E.^. QualificationUserId
|
||||||
E.&&. qualBlock `isLatestBlockBefore` E.val now
|
E.&&. qualBlock `isLatestBlockBefore` E.val now
|
||||||
E.on $ user E.^. UserId E.=?. lmsUser E.?. LmsUserUser
|
E.on $ user E.^. UserId E.=?. lmsUser E.?. LmsUserUser
|
||||||
E.&&. E.val qid E.=?. lmsUser E.?. LmsUserQualification -- NOTE: condition was once erroneously placed in where-clause, which does not work
|
E.&&. E.val qid E.=?. lmsUser E.?. LmsUserQualification -- NOTE: condition was once erroneously placed in where-clause, which does not work
|
||||||
E.on $ user E.^. UserId E.==. qualUser E.^. QualificationUserUser
|
E.on $ user E.^. UserId E.==. qualUser E.^. QualificationUserUser
|
||||||
E.where_ $ fltr qualUser
|
E.where_ $ fltr qualUser
|
||||||
E.&&. (E.val qid E.==. qualUser E.^. QualificationUserQualification)
|
E.&&. (E.val qid E.==. qualUser E.^. QualificationUserQualification)
|
||||||
let primeComp = E.subSelect . E.from $ \uc -> do
|
return (qualUser, user, lmsUser, qualBlock)
|
||||||
E.where_ $ user E.^. UserId E.==. uc E.^. UserCompanyUser
|
|
||||||
E.orderBy [E.desc $ uc E.^. UserCompanyPriority, E.asc $ uc E.^. UserCompanyCompany]
|
|
||||||
return (uc E.^. UserCompanyCompany)
|
|
||||||
return (qualUser, user, lmsUser, qualBlock, primeComp)
|
|
||||||
|
|
||||||
|
|
||||||
mkQualificationTable ::
|
mkQualificationTable ::
|
||||||
@ -362,20 +353,18 @@ mkQualificationTable ::
|
|||||||
)
|
)
|
||||||
=> Bool
|
=> Bool
|
||||||
-> Entity Qualification
|
-> Entity Qualification
|
||||||
-> Map QualificationTableAction (AForm Handler QualificationTableActionData)
|
-> Map QualificationTableAction (AForm Handler QualificationTableActionData)
|
||||||
-> ((CompanyId -> CompanyName) -> cols)
|
-> (Map CompanyId Company -> cols)
|
||||||
-> PSValidator (MForm Handler) (FormResult (First QualificationTableActionData, DBFormResult UserId Bool QualificationTableData))
|
-> PSValidator (MForm Handler) (FormResult (First QualificationTableActionData, DBFormResult UserId Bool QualificationTableData))
|
||||||
-> DB (FormResult (QualificationTableActionData, Set UserId), Widget)
|
-> DB (FormResult (QualificationTableActionData, Set UserId), Widget)
|
||||||
mkQualificationTable isAdmin (Entity qid quali) acts cols psValidator = do
|
mkQualificationTable isAdmin (Entity qid quali) acts cols psValidator = do
|
||||||
svs <- getSupervisees
|
svs <- getSupervisees
|
||||||
now <- liftIO getCurrentTime
|
now <- liftIO getCurrentTime
|
||||||
-- lookup all companies
|
-- lookup all companies
|
||||||
cmpMap <- memcachedBy (Just . Right $ 15 * diffMinute) ("CompanyDictionary"::Text) $ do
|
cmpMap <- memcachedBy (Just . Right $ 5 * diffMinute) ("CompanyDictionary"::Text) $ do
|
||||||
cmps <- selectList [] [] -- [Asc CompanyShorthand]
|
cmps <- selectList [] [] -- [Asc CompanyShorthand]
|
||||||
return $ Map.fromList $ fmap (\Entity{..} -> (entityKey, entityVal)) cmps
|
return $ Map.fromList $ fmap (\Entity{..} -> (entityKey, entityVal)) cmps
|
||||||
let
|
let
|
||||||
getCompanyName :: CompanyId -> CompanyName
|
|
||||||
getCompanyName cid = maybe (unCompanyKey cid) companyName $ Map.lookup cid cmpMap -- use shorthand in case of impossible failure
|
|
||||||
nowaday = utctDay now
|
nowaday = utctDay now
|
||||||
mbRenewal = addGregorianDurationClip <$> qualificationRefreshWithin quali <*> Just nowaday
|
mbRenewal = addGregorianDurationClip <$> qualificationRefreshWithin quali <*> Just nowaday
|
||||||
csvName = T.replace " " "-" $ CI.original (quali ^. _qualificationName)
|
csvName = T.replace " " "-" $ CI.original (quali ^. _qualificationName)
|
||||||
@ -384,8 +373,15 @@ mkQualificationTable isAdmin (Entity qid quali) acts cols psValidator = do
|
|||||||
fltrSvs = if isAdmin then const E.true else \quser -> quser E.^. QualificationUserUser `Ex.in_` E.vals svs
|
fltrSvs = if isAdmin then const E.true else \quser -> quser E.^. QualificationUserUser `Ex.in_` E.vals svs
|
||||||
dbtSQLQuery = qualificationTableQuery now qid fltrSvs
|
dbtSQLQuery = qualificationTableQuery now qid fltrSvs
|
||||||
dbtRowKey = queryUser >>> (E.^. UserId)
|
dbtRowKey = queryUser >>> (E.^. UserId)
|
||||||
dbtProj = dbtProjId
|
dbtProj = dbtProjSimple $ \(qualUsr, usr, lmsUsr, qUsrBlock) -> do
|
||||||
dbtColonnade = cols getCompanyName
|
-- cmps <- E.select . E.from $ \(usrComp `E.InnerJoin` comp) -> do
|
||||||
|
-- E.on $ usrComp E.^. UserCompanyCompany E.==. comp E.^. CompanyId
|
||||||
|
-- E.where_ $ usrComp E.^. UserCompanyUser E.==. E.val (entityKey usr)
|
||||||
|
-- E.orderBy [E.asc (comp E.^. CompanyName)]
|
||||||
|
-- return (comp E.^. CompanyName, comp E.^. CompanyAvsId, usrComp E.^. UserCompanySupervisor)
|
||||||
|
cmpUsr <- selectList [UserCompanyUser ==. entityKey usr] [Asc UserCompanyCompany]
|
||||||
|
return (qualUsr, usr, lmsUsr, qUsrBlock, cmpUsr)
|
||||||
|
dbtColonnade = cols cmpMap
|
||||||
dbtSorting = mconcat
|
dbtSorting = mconcat
|
||||||
[ single $ sortUserNameLink queryUser
|
[ single $ sortUserNameLink queryUser
|
||||||
, single $ sortUserEmail queryUser
|
, single $ sortUserEmail queryUser
|
||||||
@ -395,7 +391,7 @@ mkQualificationTable isAdmin (Entity qid quali) acts cols psValidator = do
|
|||||||
, single ("last-notified" , SortColumn $ queryQualUser >>> (E.^. QualificationUserLastNotified))
|
, single ("last-notified" , SortColumn $ queryQualUser >>> (E.^. QualificationUserLastNotified))
|
||||||
, single ("valid-until" , SortColumn $ queryQualUser >>> (E.^. QualificationUserValidUntil))
|
, single ("valid-until" , SortColumn $ queryQualUser >>> (E.^. QualificationUserValidUntil))
|
||||||
, single ("blocked" , SortColumnNeverNull $ queryQualBlock >>> (E.?. QualificationUserBlockFrom))
|
, single ("blocked" , SortColumnNeverNull $ queryQualBlock >>> (E.?. QualificationUserBlockFrom))
|
||||||
, single ("lms-status-plus",SortColumnNullsInv $ \row -> E.coalesce [ E.joinV (queryLmsUser row E.?. LmsUserStatusDay)
|
, single ("lms-status-plus",SortColumnNeverNull $ \row -> E.coalesce [ E.joinV (queryLmsUser row E.?. LmsUserStatusDay)
|
||||||
, E.joinV (queryLmsUser row E.?. LmsUserNotified)
|
, E.joinV (queryLmsUser row E.?. LmsUserNotified)
|
||||||
, queryLmsUser row E.?. LmsUserStarted])
|
, queryLmsUser row E.?. LmsUserStarted])
|
||||||
, single ("schedule-renew", SortColumnNullsInv $ queryQualUser >>> (E.^. QualificationUserScheduleRenewal))
|
, single ("schedule-renew", SortColumnNullsInv $ queryQualUser >>> (E.^. QualificationUserScheduleRenewal))
|
||||||
@ -408,26 +404,32 @@ mkQualificationTable isAdmin (Entity qid quali) acts cols psValidator = do
|
|||||||
-- , single ("validity", SortColumn $ queryQualUser >>> validQualification now)
|
-- , single ("validity", SortColumn $ queryQualUser >>> validQualification now)
|
||||||
]
|
]
|
||||||
dbtFilter = mconcat
|
dbtFilter = mconcat
|
||||||
[ single $ fltrUserNameEmail queryUser
|
[ single $ fltrUserNameEmail queryUser
|
||||||
, single ("avs-number" , FilterColumn . E.mkExistsFilter $ \row criterion ->
|
, single ("avs-number" , FilterColumn . E.mkExistsFilter $ \row criterion ->
|
||||||
E.from $ \usrAvs -> -- do
|
E.from $ \usrAvs -> -- do
|
||||||
E.where_ $ usrAvs E.^. UserAvsUser E.==. queryUser row E.^. UserId
|
E.where_ $ usrAvs E.^. UserAvsUser E.==. queryUser row E.^. UserId
|
||||||
E.&&. ((E.val criterion :: E.SqlExpr (E.Value (CI Text))) E.==.
|
E.&&. ((E.val criterion :: E.SqlExpr (E.Value (CI Text))) E.==.
|
||||||
(E.explicitUnsafeCoerceSqlExprValue "citext" (usrAvs E.^. UserAvsNoPerson) :: E.SqlExpr (E.Value (CI Text))) ))
|
(E.explicitUnsafeCoerceSqlExprValue "citext" (usrAvs E.^. UserAvsNoPerson) :: E.SqlExpr (E.Value (CI Text))) ))
|
||||||
, fltrAVSCardNos queryUser
|
, single ("avs-card" , FilterColumn $ \(queryUser -> user) (criterion :: Set.Set Text) -> case readAvsFullCardNo =<< Set.lookupMin criterion of
|
||||||
|
Nothing -> E.false
|
||||||
|
Just cardNo -> E.exists $ E.from $ \(avsCard `E.InnerJoin` usrAvs) -> do
|
||||||
|
E.on $ usrAvs E.^. UserAvsPersonId E.==. avsCard E.^. UserAvsCardPersonId
|
||||||
|
E.where_ $ usrAvs E.^. UserAvsUser E.==. user E.^. UserId
|
||||||
|
E.&&. (avsCard E.^. UserAvsCardCardNo E.==. E.val cardNo)
|
||||||
|
)
|
||||||
, single ("personal-number", FilterColumn $ \(queryUser -> user) (criteria :: Set.Set Text) -> if
|
, single ("personal-number", FilterColumn $ \(queryUser -> user) (criteria :: Set.Set Text) -> if
|
||||||
| Set.null criteria -> E.true
|
| Set.null criteria -> E.true
|
||||||
| otherwise -> E.any (\c -> user E.^. UserCompanyPersonalNumber `E.hasInfix` E.val c) criteria
|
| otherwise -> E.any (\c -> user E.^. UserCompanyPersonalNumber `E.hasInfix` E.val c) criteria
|
||||||
)
|
)
|
||||||
, single ("user-company", FilterColumn . E.mkExistsFilter $ \row criterion ->
|
, single ("user-company", FilterColumn . E.mkExistsFilter $ \row criterion ->
|
||||||
E.from $ \(usrComp `E.InnerJoin` comp) -> do
|
E.from $ \(usrComp `E.InnerJoin` comp) -> do
|
||||||
let testname = (E.val criterion :: E.SqlExpr (E.Value (CI Text))) `E.isInfixOf`
|
let testname = (E.val criterion :: E.SqlExpr (E.Value (CI Text))) `E.isInfixOf`
|
||||||
(E.explicitUnsafeCoerceSqlExprValue "citext" (comp E.^. CompanyName) :: E.SqlExpr (E.Value (CI Text)))
|
(E.explicitUnsafeCoerceSqlExprValue "citext" (comp E.^. CompanyName) :: E.SqlExpr (E.Value (CI Text)))
|
||||||
testnumber nr = E.val nr E.==. comp E.^. CompanyAvsId
|
testnumber nr = E.val nr E.==. comp E.^. CompanyAvsId
|
||||||
testcrit = maybe testname testnumber $ readMay $ CI.original criterion
|
testcrit = maybe testname testnumber $ readMay $ CI.original criterion
|
||||||
E.on $ usrComp E.^. UserCompanyCompany E.==. comp E.^. CompanyId
|
E.on $ usrComp E.^. UserCompanyCompany E.==. comp E.^. CompanyId
|
||||||
E.where_ $ usrComp E.^. UserCompanyUser E.==. queryUser row E.^. UserId E.&&. testcrit
|
E.where_ $ usrComp E.^. UserCompanyUser E.==. queryUser row E.^. UserId E.&&. testcrit
|
||||||
)
|
)
|
||||||
, single ("validity" , FilterColumn . E.mkExactFilterLast $ views (to queryQualUser) (validQualification now))
|
, single ("validity" , FilterColumn . E.mkExactFilterLast $ views (to queryQualUser) (validQualification now))
|
||||||
, single ("renewal-due" , FilterColumn $ \(queryQualUser -> quser) criterion ->
|
, single ("renewal-due" , FilterColumn $ \(queryQualUser -> quser) criterion ->
|
||||||
if | Just renewal <- mbRenewal
|
if | Just renewal <- mbRenewal
|
||||||
@ -445,8 +447,8 @@ mkQualificationTable isAdmin (Entity qid quali) acts cols psValidator = do
|
|||||||
[ fltrUserNameEmailHdrUI MsgLmsUser mPrev
|
[ fltrUserNameEmailHdrUI MsgLmsUser mPrev
|
||||||
, prismAForm (singletonFilter "user-company") mPrev $ aopt textField (fslI MsgTableCompany)
|
, prismAForm (singletonFilter "user-company") mPrev $ aopt textField (fslI MsgTableCompany)
|
||||||
, prismAForm (singletonFilter "personal-number" ) mPrev $ aopt textField (fslI MsgCompanyPersonalNumber)
|
, prismAForm (singletonFilter "personal-number" ) mPrev $ aopt textField (fslI MsgCompanyPersonalNumber)
|
||||||
, fltrAVSCardNosUI mPrev
|
, prismAForm (singletonFilter "avs-card" ) mPrev $ aopt textField (fslI MsgAvsCardNo)
|
||||||
, prismAForm (singletonFilter "avs-number" ) mPrev $ aopt textField (fslI MsgAvsPersonNo)
|
, prismAForm (singletonFilter "avs-number" ) mPrev $ aopt textField (fslI MsgAvsPersonNo)
|
||||||
, prismAForm (singletonFilter "validity" . maybePrism _PathPiece) mPrev $ aopt (boolField . Just $ SomeMessage MsgBoolIrrelevant) (fslI MsgFilterLmsValid)
|
, prismAForm (singletonFilter "validity" . maybePrism _PathPiece) mPrev $ aopt (boolField . Just $ SomeMessage MsgBoolIrrelevant) (fslI MsgFilterLmsValid)
|
||||||
, if isNothing mbRenewal then mempty
|
, if isNothing mbRenewal then mempty
|
||||||
else prismAForm (singletonFilter "renewal-due" . maybePrism _PathPiece) mPrev $ aopt checkBoxField (fslI MsgFilterLmsRenewal)
|
else prismAForm (singletonFilter "renewal-due" . maybePrism _PathPiece) mPrev $ aopt checkBoxField (fslI MsgFilterLmsRenewal)
|
||||||
@ -468,29 +470,34 @@ mkQualificationTable isAdmin (Entity qid quali) acts cols psValidator = do
|
|||||||
doEncode' = QualificationTableCsv
|
doEncode' = QualificationTableCsv
|
||||||
<$> view (resultUser . _entityVal . _userDisplayName)
|
<$> view (resultUser . _entityVal . _userDisplayName)
|
||||||
<*> view (resultUser . _entityVal . _userDisplayEmail)
|
<*> view (resultUser . _entityVal . _userDisplayEmail)
|
||||||
<*> preview (resultCompanyId . to getCompanyName . _CI)
|
<*> (view resultCompanyUser >>= getCompanies)
|
||||||
|
<*> (view resultCompanyUser >>= getCompanyNos)
|
||||||
<*> view (resultQualUser . _entityVal . _qualificationUserValidUntil)
|
<*> view (resultQualUser . _entityVal . _qualificationUserValidUntil)
|
||||||
<*> view (resultQualUser . _entityVal . _qualificationUserLastRefresh)
|
<*> view (resultQualUser . _entityVal . _qualificationUserLastRefresh)
|
||||||
<*> preview (resultQualBlock. _entityVal . _qualificationUserBlockUnblock . _not)
|
<*> preview (resultQualBlock. _entityVal . _qualificationUserBlockUnblock . _not)
|
||||||
<*> preview (resultQualBlock. _entityVal . _qualificationUserBlockFrom)
|
<*> preview (resultQualBlock. _entityVal . _qualificationUserBlockFrom)
|
||||||
<*> view (resultQualUser . _entityVal . _qualificationUserScheduleRenewal)
|
<*> view (resultQualUser . _entityVal . _qualificationUserScheduleRenewal)
|
||||||
<*> getStatusPlusTxt
|
<*> getStatusPlusTxt
|
||||||
<*> getStatusPlusDay
|
<*> getStatusPlusDay
|
||||||
|
getCompanies cmps = case mapMaybe (flip Map.lookup cmpMap . view (_entityVal . _userCompanyCompany)) cmps of
|
||||||
|
[] -> pure Nothing
|
||||||
|
somecmps -> pure $ Just $ intercalate ", " $ fmap (view (_companyName . _CI)) somecmps
|
||||||
|
getCompanyNos = pure . CsvSemicolonList . mapMaybe (preview (_Just . _companyAvsId) . flip Map.lookup cmpMap . view (_entityVal . _userCompanyCompany))
|
||||||
|
|
||||||
getStatusPlusTxt =
|
getStatusPlusTxt =
|
||||||
(join . preview (resultLmsUser . _entityVal . _lmsUserStatus)) >>= \case
|
(join . preview (resultLmsUser . _entityVal . _lmsUserStatus)) >>= \case
|
||||||
Just LmsBlocked{} -> return $ Just "Failed"
|
Just LmsBlocked{} -> return $ Just "Failed"
|
||||||
Just LmsExpired{} -> return $ Just "Expired"
|
Just LmsExpired{} -> return $ Just "Expired"
|
||||||
Just LmsSuccess{} -> return $ Just "Success"
|
Just LmsSuccess{} -> return $ Just "Success"
|
||||||
Nothing -> maybeM (return Nothing) (const $ return $ Just "Open") $
|
Nothing -> maybeM (return Nothing) (const $ return $ Just "Open") $
|
||||||
preview (resultLmsUser . _entityVal . _lmsUserStarted)
|
preview (resultLmsUser . _entityVal . _lmsUserStarted)
|
||||||
getStatusPlusDay =
|
getStatusPlusDay =
|
||||||
(join . preview (resultLmsUser . _entityVal . _lmsUserStatusDay)) >>= \case
|
(join . preview (resultLmsUser . _entityVal . _lmsUserStatusDay)) >>= \case
|
||||||
lsd@(Just _) -> return lsd
|
lsd@(Just _) -> return lsd
|
||||||
Nothing -> preview (resultLmsUser . _entityVal . _lmsUserStarted)
|
Nothing -> preview (resultLmsUser . _entityVal . _lmsUserStarted)
|
||||||
|
|
||||||
dbtCsvDecode = Nothing
|
dbtCsvDecode = Nothing
|
||||||
dbtExtraReps = []
|
dbtExtraReps = []
|
||||||
dbtParams = DBParamsForm
|
dbtParams = DBParamsForm
|
||||||
{ dbParamsFormMethod = POST
|
{ dbParamsFormMethod = POST
|
||||||
, dbParamsFormAction = Nothing
|
, dbParamsFormAction = Nothing
|
||||||
@ -518,32 +525,31 @@ mkQualificationTable isAdmin (Entity qid quali) acts cols psValidator = do
|
|||||||
|
|
||||||
getQualificationR, postQualificationR :: SchoolId -> QualificationShorthand -> Handler Html
|
getQualificationR, postQualificationR :: SchoolId -> QualificationShorthand -> Handler Html
|
||||||
getQualificationR = postQualificationR
|
getQualificationR = postQualificationR
|
||||||
postQualificationR sid qsh = do
|
postQualificationR sid qsh = do
|
||||||
isAdmin <- hasReadAccessTo AdminR
|
isAdmin <- hasReadAccessTo AdminR
|
||||||
msgGrantWarning <- messageIconI Warning IconWarning MsgQualificationActGrantWarning
|
msgGrantWarning <- messageIconI Warning IconWarning MsgQualificationActGrantWarning
|
||||||
msgUnexpire <- messageIconI Info IconWarning MsgQualificationActUnexpireWarning
|
msgUnexpire <- messageIconI Info IconWarning MsgQualificationActUnexpireWarning
|
||||||
now <- liftIO getCurrentTime
|
now <- liftIO getCurrentTime
|
||||||
let nowaday = utctDay now
|
let nowaday = utctDay now
|
||||||
((lmsRes, qualificationTable), Entity qid quali, lmsQualiReused) <- runDB $ do
|
((lmsRes, qualificationTable), Entity qid quali) <- runDB $ do
|
||||||
qent@Entity{
|
qent@Entity{
|
||||||
entityKey=qid
|
entityKey=qid
|
||||||
, entityVal=Qualification{
|
, entityVal=Qualification{
|
||||||
qualificationAuditDuration=auditMonths
|
qualificationAuditDuration=auditMonths
|
||||||
, qualificationValidDuration=validMonths
|
, qualificationValidDuration=validMonths
|
||||||
, qualificationLmsReuses =reuseQuali
|
|
||||||
}} <- getBy404 $ SchoolQualificationShort sid qsh
|
}} <- getBy404 $ SchoolQualificationShort sid qsh
|
||||||
lmsQualiReused <- traverseJoin get reuseQuali
|
|
||||||
-- Block copied to Handler/Qualifications TODO: refactor
|
-- Block copied to Handler/Qualifications TODO: refactor
|
||||||
let getBlockReasons unblk = Ex.select $ do
|
let getBlockReasons unblk = Ex.select $ do
|
||||||
(quser :& qblock) <- Ex.from $ Ex.table @QualificationUser
|
(quser :& qblock) <- Ex.from $ Ex.table @QualificationUser
|
||||||
`Ex.innerJoin` Ex.table @QualificationUserBlock
|
`Ex.innerJoin` Ex.table @QualificationUserBlock
|
||||||
`Ex.on` (\(quser :& qblock) -> quser Ex.^. QualificationUserId Ex.==. qblock Ex.^. QualificationUserBlockQualificationUser)
|
`Ex.on` (\(quser :& qblock) -> quser Ex.^. QualificationUserId Ex.==. qblock Ex.^. QualificationUserBlockQualificationUser)
|
||||||
Ex.where_ $ quser Ex.^. QualificationUserQualification Ex.==. Ex.val qid
|
Ex.where_ $ quser Ex.^. QualificationUserQualification Ex.==. Ex.val qid
|
||||||
Ex.&&. unblk (qblock Ex.^. QualificationUserBlockUnblock)
|
Ex.&&. unblk (qblock Ex.^. QualificationUserBlockUnblock)
|
||||||
Ex.groupBy (qblock Ex.^. QualificationUserBlockReason)
|
Ex.groupBy (qblock Ex.^. QualificationUserBlockReason)
|
||||||
let countRows' :: Ex.SqlExpr (Ex.Value Int64) = Ex.countRows
|
let countRows' :: Ex.SqlExpr (Ex.Value Int64) = Ex.countRows
|
||||||
Ex.orderBy [Ex.desc countRows']
|
Ex.orderBy [Ex.desc countRows']
|
||||||
Ex.limit 9
|
Ex.limit 7
|
||||||
pure (qblock Ex.^. QualificationUserBlockReason)
|
pure (qblock Ex.^. QualificationUserBlockReason)
|
||||||
mkOption :: Ex.Value Text -> Option Text
|
mkOption :: Ex.Value Text -> Option Text
|
||||||
mkOption (Ex.unValue -> t) = Option{ optionDisplay = t, optionInternalValue = t, optionExternalValue = toPathPiece t }
|
mkOption (Ex.unValue -> t) = Option{ optionDisplay = t, optionInternalValue = t, optionExternalValue = toPathPiece t }
|
||||||
@ -554,78 +560,64 @@ postQualificationR sid qsh = do
|
|||||||
acts :: Map QualificationTableAction (AForm Handler QualificationTableActionData)
|
acts :: Map QualificationTableAction (AForm Handler QualificationTableActionData)
|
||||||
acts = mconcat $
|
acts = mconcat $
|
||||||
[ singletonMap QualificationActExpire $ pure QualificationActExpireData
|
[ singletonMap QualificationActExpire $ pure QualificationActExpireData
|
||||||
, singletonMap QualificationActUnexpire $ QualificationActUnexpireData
|
, singletonMap QualificationActUnexpire $ QualificationActUnexpireData
|
||||||
<$ aformMessage msgUnexpire
|
<$ aformMessage msgUnexpire
|
||||||
] ++ bool
|
] ++ bool
|
||||||
-- nonAdmin actions, ie. Supervisor
|
-- nonAdmin actions, ie. Supervisor
|
||||||
[ singletonMap QualificationActBlockSupervisor $ pure QualificationActBlockSupervisorData ]
|
[ singletonMap QualificationActBlockSupervisor $ pure QualificationActBlockSupervisorData ]
|
||||||
-- Admin-only actions
|
-- Admin-only actions
|
||||||
[ singletonMap QualificationActUnblock $ QualificationActUnblockData
|
[ singletonMap QualificationActUnblock $ QualificationActUnblockData
|
||||||
<$> apreq (textField & cfStrip & addDatalist suggestionsUnblock) (fslI MsgQualificationGrantReason) Nothing
|
<$> apreq (textField & cfStrip & addDatalist suggestionsUnblock) (fslI MsgQualificationGrantReason) Nothing
|
||||||
<*> areq (boolField . Just $ SomeMessage MsgBoolIrrelevant) (fslI MsgQualificationBlockNotify) (Just False)
|
<*> areq (boolField . Just $ SomeMessage MsgBoolIrrelevant) (fslI MsgQualificationBlockNotify) (Just False)
|
||||||
, singletonMap QualificationActBlock $ QualificationActBlockData
|
, singletonMap QualificationActBlock $ QualificationActBlockData
|
||||||
<$> apreq (textField & cfStrip & addDatalist suggestionsBlock) (fslI MsgQualificationBlockReason) Nothing
|
<$> apreq (textField & cfStrip & addDatalist suggestionsBlock) (fslI MsgQualificationBlockReason) Nothing
|
||||||
<*> areq (boolField . Just $ SomeMessage MsgBoolIrrelevant) (fslI MsgQualificationBlockNotify) (Just False)
|
<*> areq (boolField . Just $ SomeMessage MsgBoolIrrelevant) (fslI MsgQualificationBlockNotify) (Just False)
|
||||||
<*> areq (boolField . Just $ SomeMessage MsgBoolIrrelevant) (fslI MsgQualificationBlockRemoveSupervisor) (Just False)
|
<*> areq (boolField . Just $ SomeMessage MsgBoolIrrelevant) (fslI MsgQualificationBlockRemoveSupervisor) (Just False)
|
||||||
, singletonMap QualificationActRenew $ QualificationActRenewData
|
, singletonMap QualificationActRenew $ QualificationActRenewData
|
||||||
<$> apreq (textField & cfStrip & addDatalist suggestionsBlock) (fslI MsgQualificationRenewReason) Nothing
|
<$> apreq (textField & cfStrip & addDatalist suggestionsBlock) (fslI MsgQualificationRenewReason) Nothing
|
||||||
, singletonMap QualificationActGrant $ QualificationActGrantData
|
, singletonMap QualificationActGrant $ QualificationActGrantData
|
||||||
<$> apopt dayField (fslI MsgLmsQualificationValidUntil) dayExpiry
|
<$> apopt dayField (fslI MsgLmsQualificationValidUntil) dayExpiry
|
||||||
<* aformMessage msgGrantWarning
|
<* aformMessage msgGrantWarning
|
||||||
, singletonMap QualificationActStartELearning $ pure QualificationActStartELearningData
|
|
||||||
-- <$> aopt dayField (fslI MsgQualificationReduceValidUntil) Nothing
|
|
||||||
] isAdmin
|
] isAdmin
|
||||||
linkLmsUser = toMaybe isAdmin (LmsUserR sid qsh)
|
linkLmsUser = toMaybe isAdmin (LmsUserR sid qsh)
|
||||||
linkUserName = bool ForProfileR ForProfileDataR isAdmin
|
linkUserName = bool ForProfileR ForProfileDataR isAdmin
|
||||||
colChoices getCompanyName = mconcat
|
colChoices cmpMap = mconcat
|
||||||
[ dbSelect (applying _2) id (return . view (hasEntity . _entityKey))
|
[ dbSelect (applying _2) id (return . view (hasEntity . _entityKey))
|
||||||
, colUserNameModalHdr MsgLmsUser linkUserName
|
, colUserNameModalHdr MsgLmsUser linkUserName
|
||||||
, colUserEmail
|
, colUserEmail
|
||||||
, sortable (Just "user-company") (i18nCell MsgTablePrimeCompany) $ \(preview resultCompanyId -> mcid) ->
|
, sortable (Just "user-company") (i18nCell MsgTableCompanies) $ \( view resultCompanyUser -> cmps) ->
|
||||||
maybeEmpty mcid $ \cid -> companyCell (unCompanyKey cid) (getCompanyName cid) False
|
let cs = [ companyCell (unCompanyKey cmpId) cmpName cmpSpr
|
||||||
|
| Entity _ UserCompany{userCompanyCompany=cmpId, userCompanySupervisor=cmpSpr} <- cmps
|
||||||
|
, let cmpName = maybe (unCompanyKey cmpId) companyName $ Map.lookup cmpId cmpMap
|
||||||
|
]
|
||||||
|
in intercalate spacerCell cs
|
||||||
, guardMonoid isAdmin $ colUserMatriclenr isAdmin
|
, guardMonoid isAdmin $ colUserMatriclenr isAdmin
|
||||||
-- , sortable (Just "validity") (i18nCell MsgQualificationValidIndicator) (qualificationValidIconCell nowaday . view resultQualUser)
|
-- , sortable (Just "validity") (i18nCell MsgQualificationValidIndicator) (qualificationValidIconCell nowaday . view resultQualUser)
|
||||||
, sortable (Just "first-held") (i18nCell MsgTableQualificationFirstHeld) $ \( view $ resultQualUser . _entityVal . _qualificationUserFirstHeld -> d) -> dayCell d
|
, sortable (Just "first-held") (i18nCell MsgTableQualificationFirstHeld) $ \( view $ resultQualUser . _entityVal . _qualificationUserFirstHeld -> d) -> dayCell d
|
||||||
, sortable (Just "last-refresh") (i18nCell MsgTableQualificationLastRefresh)$ \( view $ resultQualUser . _entityVal . _qualificationUserLastRefresh -> d) -> dayCell d
|
, sortable (Just "last-refresh") (i18nCell MsgTableQualificationLastRefresh)$ \( view $ resultQualUser . _entityVal . _qualificationUserLastRefresh -> d) -> dayCell d
|
||||||
, sortable (Just "valid-until") (i18nCell MsgLmsQualificationValidUntil) (dayCell . view ( resultQualUser . _entityVal . _qualificationUserValidUntil))
|
, sortable (Just "valid-until") (i18nCell MsgLmsQualificationValidUntil) (dayCell . view ( resultQualUser . _entityVal . _qualificationUserValidUntil))
|
||||||
, sortable (Just "blocked") (i18nCell MsgQualificationValidIndicator & cellTooltip MsgTableQualificationBlockedTooltipSimple) $ \row ->
|
, sortable (Just "blocked") (i18nCell MsgQualificationValidIndicator & cellTooltip MsgTableQualificationBlockedTooltipSimple) $ \row ->
|
||||||
qualificationValidReasonCell' (Just $ LmsUserR sid qsh) isAdmin nowaday (row ^? resultQualBlock) row
|
qualificationValidReasonCell' (Just $ LmsUserR sid qsh) isAdmin nowaday (row ^? resultQualBlock) row
|
||||||
, sortable (Just "schedule-renew")(i18nCell MsgTableQualificationNoRenewal & cellTooltip MsgTableQualificationNoRenewalTooltip
|
, sortable (Just "schedule-renew")(i18nCell MsgTableQualificationNoRenewal & cellTooltip MsgTableQualificationNoRenewalTooltip
|
||||||
) $ \( view $ resultQualUser . _entityVal . _qualificationUserScheduleRenewal -> b) -> ifIconCell (not b) IconNoNotification
|
) $ \( view $ resultQualUser . _entityVal . _qualificationUserScheduleRenewal -> b) -> ifIconCell (not b) IconNoNotification
|
||||||
, sortable (Just "lms-status-plus")(i18nCell MsgTableLmsStatus & cellTooltipWgt Nothing (lmsStatusInfoCell isAdmin auditMonths))
|
, sortable (Just "lms-status-plus")(i18nCell MsgTableLmsStatus & cellTooltipWgt Nothing (lmsStatusInfoCell isAdmin auditMonths))
|
||||||
$ \(preview $ resultLmsUser . _entityVal -> lu) -> foldMap (lmsStatusCell isAdmin linkLmsUser) lu
|
$ \(preview $ resultLmsUser . _entityVal -> lu) -> foldMap (lmsStatusCell isAdmin linkLmsUser) lu
|
||||||
-- QualificationUserLastNotified is about notification on actual validity changes. If a user's licence is about to expire and renewed before expiry via e-learning, this value does not change.
|
, sortable (Just "last-notified") (i18nCell MsgTableQualificationLastNotified) $ \( view $ resultQualUser . _entityVal . _qualificationUserLastNotified -> d) -> dateTimeCell d
|
||||||
-- NOTE: If this column is reinstatiated, header and tooltip were already updated to avoid any confusion!
|
|
||||||
-- , sortable (Just "last-notified") (i18nCell MsgTableQualificationLastNotified & cellTooltip MsgTableQualificationLastNotifiedTooltip)
|
|
||||||
-- $ \( view $ resultQualUser . _entityVal . _qualificationUserLastNotified -> d) -> dateTimeCell d
|
|
||||||
]
|
]
|
||||||
psValidator = def & defaultSorting [SortDescBy "last-refresh"]
|
psValidator = def & defaultSorting [SortDescBy "last-refresh"]
|
||||||
tbl <- mkQualificationTable isAdmin qent acts colChoices psValidator
|
tbl <- mkQualificationTable isAdmin qent acts colChoices psValidator
|
||||||
return (tbl, qent, lmsQualiReused)
|
return (tbl, qent)
|
||||||
|
|
||||||
formResult lmsRes $ \case
|
formResult lmsRes $ \case
|
||||||
(QualificationActRenewData renewReason, selectedUsers) | isAdmin -> do
|
(QualificationActRenewData renewReason, selectedUsers) | isAdmin -> do
|
||||||
noks <- runDB $ renewValidQualificationUsers qid (canonical $ Just $ Left renewReason) Nothing $ Set.toList selectedUsers
|
noks <- runDB $ renewValidQualificationUsers qid (canonical $ Just $ Left renewReason) Nothing $ Set.toList selectedUsers
|
||||||
addMessageI (if noks > 0 && noks == Set.size selectedUsers then Success else Warning) $ MsgTutorialUserRenewedQualification noks
|
addMessageI (if noks > 0 && noks == Set.size selectedUsers then Success else Warning) $ MsgTutorialUserRenewedQualification noks
|
||||||
reloadKeepGetParams $ QualificationR sid qsh
|
reloadKeepGetParams $ QualificationR sid qsh
|
||||||
(QualificationActGrantData grantValidday, selectedUsers) | isAdmin -> do
|
(QualificationActGrantData grantValidday, selectedUsers) | isAdmin -> do
|
||||||
runDB . forM_ selectedUsers $ upsertQualificationUser qid now grantValidday Nothing "Admin"
|
runDB . forM_ selectedUsers $ upsertQualificationUser qid now grantValidday Nothing "Admin"
|
||||||
addMessageI (if 0 < Set.size selectedUsers then Success else Warning) . MsgTutorialUserGrantedQualification $ Set.size selectedUsers
|
addMessageI (if 0 < Set.size selectedUsers then Success else Warning) . MsgTutorialUserGrantedQualification $ Set.size selectedUsers
|
||||||
reloadKeepGetParams $ QualificationR sid qsh
|
reloadKeepGetParams $ QualificationR sid qsh
|
||||||
(QualificationActStartELearningData, Set.toList -> selectedUsers) | isAdmin -> do
|
(action, selectedUsers) | isExpiryAct action -> do
|
||||||
-- whenIsJust mbExpDay $ \expDay ->
|
|
||||||
-- when expDay > nowaday $
|
|
||||||
-- -- updateWhere [QualificationUserQualification ==. qid, QualificationUserUser <-. selectedUsers, QualificationUserValidUntil >. expDay] [QualificationUserValidUntil =. expDay] -- DO NOT USE: no audit
|
|
||||||
-- NOTE: if needed, create function Handler.Utils.Qualification.updateQualificationUser qid QualificationChangeReason -> Day -> [UserId] -> DB Int
|
|
||||||
validQualHolderEnts <- runDB $ selectValidQualifications qid selectedUsers now
|
|
||||||
let validQualHolders = view (_entityVal . _qualificationUserUser) <$> validQualHolderEnts
|
|
||||||
jobs <- forM validQualHolders $ queueJob . JobLmsEnqueueUser qid
|
|
||||||
let nrTodo = length selectedUsers
|
|
||||||
nrEnqueued = length $ catMaybes jobs
|
|
||||||
addMessageI (bool Warning Success $ nrEnqueued > 0 && nrEnqueued == nrTodo) $ MsgQualificationActStartELearningStatus qsh nrEnqueued nrTodo
|
|
||||||
-- transaction audit identical to automatic start, performed by JobLmsEnqueueUser
|
|
||||||
reloadKeepGetParams $ QualificationR sid qsh
|
|
||||||
(action, selectedUsers) | isExpiryAct action -> do
|
|
||||||
let isUnexpire = action == QualificationActUnexpireData
|
let isUnexpire = action == QualificationActUnexpireData
|
||||||
upd <- runDB $ do
|
upd <- runDB $ do
|
||||||
forM_ selectedUsers $ \uid -> audit TransactionQualificationUserScheduleRenewal
|
forM_ selectedUsers $ \uid -> audit TransactionQualificationUserScheduleRenewal
|
||||||
@ -640,18 +632,18 @@ postQualificationR sid qsh = do
|
|||||||
msgVal = upd & if isUnexpire then MsgQualificationSetUnexpire else MsgQualificationSetExpire
|
msgVal = upd & if isUnexpire then MsgQualificationSetUnexpire else MsgQualificationSetExpire
|
||||||
addMessageI msgKind msgVal
|
addMessageI msgKind msgVal
|
||||||
reloadKeepGetParams $ QualificationR sid qsh
|
reloadKeepGetParams $ QualificationR sid qsh
|
||||||
(action, selectedUsers) | isBlockAct action && (isAdmin || action == QualificationActBlockSupervisorData) -> do
|
(action, selectedUsers) | isBlockAct action && (isAdmin || action == QualificationActBlockSupervisorData) -> do
|
||||||
let selUserIds = Set.toList selectedUsers
|
let selUserIds = Set.toList selectedUsers
|
||||||
(unblock, reason) = case action of
|
(unblock, reason) = case action of
|
||||||
QualificationActBlockSupervisorData -> (False, Right QualificationBlockReturnedByCompany)
|
QualificationActBlockSupervisorData -> (False, Right QualificationBlockReturnedByCompany)
|
||||||
QualificationActBlockData{..} -> (False, Left qualTableActBlockReason)
|
QualificationActBlockData{..} -> (False, Left qualTableActBlockReason)
|
||||||
QualificationActUnblockData{..} -> (True , Left qualTableActBlockReason)
|
QualificationActUnblockData{..} -> (True , Left qualTableActBlockReason)
|
||||||
_ -> error "Handle.Qualification.isBlockAct returned non-block action" -- cannot occur due to earlier checks
|
_ -> error "Handle.Qualification.isBlockAct returned non-block action" -- cannot occur due to earlier checks
|
||||||
notify = case action of
|
notify = case action of
|
||||||
QualificationActBlockData{qualTableActNotify} -> qualTableActNotify
|
QualificationActBlockData{qualTableActNotify} -> qualTableActNotify
|
||||||
_ -> False
|
_ -> False
|
||||||
|
|
||||||
oks <- runDB $ do
|
oks <- runDB $ do
|
||||||
when (blockActRemoveSupervisors action) $ deleteWhere [UserSupervisorUser <-. selUserIds]
|
when (blockActRemoveSupervisors action) $ deleteWhere [UserSupervisorUser <-. selUserIds]
|
||||||
qualificationUserBlocking qid selUserIds unblock Nothing reason notify
|
qualificationUserBlocking qid selUserIds unblock Nothing reason notify
|
||||||
let nrq = length selectedUsers
|
let nrq = length selectedUsers
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
-- SPDX-FileCopyrightText: 2022 Steffen Jost <jost@tcs.ifi.lmu.de>
|
-- SPDX-FileCopyrightText: 2022-2024 Sarah Vaupel <sarah.vaupel@uniworx.de>, Steffen Jost <jost@tcs.ifi.lmu.de>
|
||||||
--
|
--
|
||||||
-- SPDX-License-Identifier: AGPL-3.0-or-later
|
-- SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
@ -17,20 +17,18 @@ import Handler.Utils.Csv
|
|||||||
import Handler.Utils.Profile
|
import Handler.Utils.Profile
|
||||||
|
|
||||||
import qualified Data.Text as Text (intercalate)
|
import qualified Data.Text as Text (intercalate)
|
||||||
-- import qualified Data.CaseInsensitive as CI
|
|
||||||
import qualified Data.Csv as Csv
|
import qualified Data.Csv as Csv
|
||||||
import Database.Esqueleto.Experimental ((:&)(..))
|
import Database.Esqueleto.Experimental ((:&)(..))
|
||||||
import qualified Database.Esqueleto.Experimental as E -- needs TypeApplications Lang-Pragma
|
import qualified Database.Esqueleto.Experimental as E -- needs TypeApplications Lang-Pragma
|
||||||
-- import qualified Database.Esqueleto.Legacy as E
|
|
||||||
import qualified Database.Esqueleto.PostgreSQL as E
|
import qualified Database.Esqueleto.PostgreSQL as E
|
||||||
import qualified Database.Esqueleto.Utils as E
|
import qualified Database.Esqueleto.Utils as E
|
||||||
|
|
||||||
|
|
||||||
data SapUserTableCsv = SapUserTableCsv -- for csv export only
|
data SapUserTableCsv = SapUserTableCsv -- for csv export only
|
||||||
{ csvSUTpersonalNummer :: Text
|
{ csvSUTpersonalNummer :: Text
|
||||||
, csvSUTqualifikation :: Text
|
, csvSUTqualifikation :: Text
|
||||||
, csvSUTgültigVon :: Day
|
, csvSUTgültigVon :: Day
|
||||||
, csvSUTgültigBis :: Day
|
, csvSUTgültigBis :: Day
|
||||||
-- , csvSUTsupendiertBis :: Maybe Day
|
-- , csvSUTsupendiertBis :: Maybe Day
|
||||||
, csvSUTausprägung :: Text
|
, csvSUTausprägung :: Text
|
||||||
}
|
}
|
||||||
@ -38,7 +36,7 @@ data SapUserTableCsv = SapUserTableCsv -- for csv export only
|
|||||||
makeLenses_ ''SapUserTableCsv
|
makeLenses_ ''SapUserTableCsv
|
||||||
|
|
||||||
sapUserTableCsvHeader :: Csv.Header
|
sapUserTableCsvHeader :: Csv.Header
|
||||||
sapUserTableCsvHeader = Csv.header
|
sapUserTableCsvHeader = Csv.header
|
||||||
[ "PersonalNummer"
|
[ "PersonalNummer"
|
||||||
, "Qualifikation"
|
, "Qualifikation"
|
||||||
, "GültigVon"
|
, "GültigVon"
|
||||||
@ -51,40 +49,40 @@ instance ToNamedRecord SapUserTableCsv where
|
|||||||
toNamedRecord SapUserTableCsv{..} = Csv.namedRecord
|
toNamedRecord SapUserTableCsv{..} = Csv.namedRecord
|
||||||
[ "PersonalNummer" Csv..= csvSUTpersonalNummer
|
[ "PersonalNummer" Csv..= csvSUTpersonalNummer
|
||||||
, "Qualifikation" Csv..= csvSUTqualifikation
|
, "Qualifikation" Csv..= csvSUTqualifikation
|
||||||
, "GültigVon" Csv..= csvSUTgültigVon
|
, "GültigVon" Csv..= csvSUTgültigVon
|
||||||
, "GültigBis" Csv..= csvSUTgültigBis
|
, "GültigBis" Csv..= csvSUTgültigBis
|
||||||
-- , "SupendiertBis" Csv..= csvSUTsupendiertBis
|
-- , "SupendiertBis" Csv..= csvSUTsupendiertBis
|
||||||
, "Ausprägung" Csv..= csvSUTausprägung
|
, "Ausprägung" Csv..= csvSUTausprägung
|
||||||
]
|
]
|
||||||
|
|
||||||
-- | Removes all personalNummer which are not numbers between 10000 and 99999 (also excludes E-Accounts), which should not be returned by the query anyway (only qualifications with sap id and users with internal personnel number must be transmitted)
|
-- | Removes all personalNummer which are not numbers between 10000 and 99999 (also excludes E-Accounts), which should not be returned by the query anyway (only qualfications with sap id and users with internal personnel number must be transmitted)
|
||||||
-- temporary suspensions are transmitted to SAP in multiple rows: firstheld->suspension1, reinstate1->suspension2, reinstate2->validTo
|
-- temporary suspensions are transmitted to SAP in multiple rows: firstheld->suspension1, reinstate1->suspension2, reinstate2->validTo
|
||||||
sapRes2csv :: [(E.Value (Maybe Text), E.Value (Maybe Text), E.Value Day, E.Value Day, E.Value (Maybe [Maybe Day]), E.Value (Maybe [Maybe Bool]))] -> [SapUserTableCsv]
|
sapRes2csv :: [(E.Value (Maybe Text), E.Value (Maybe Text), E.Value Day, E.Value Day, E.Value (Maybe [Maybe Day]), E.Value (Maybe [Maybe Bool]))] -> [SapUserTableCsv]
|
||||||
sapRes2csv = concatMap procRes
|
sapRes2csv = concatMap procRes
|
||||||
where
|
where
|
||||||
procRes (E.Value pn@(Just persNo), E.Value (Just sapId), E.Value firstHeld, E.Value validUntil, E.Value (fromMaybe [] -> qubFroms), E.Value (fromMaybe [] -> qubUnblocks))
|
procRes (E.Value pn@(Just persNo), E.Value (Just sapId), E.Value firstHeld, E.Value validUntil, E.Value (fromMaybe [] -> qubFroms), E.Value (fromMaybe [] -> qubUnblocks))
|
||||||
| validFraportPersonalNumber pn -- between 10000 and 99999 also see Handler.Utils.Profile.validFraportPersonalNumber
|
| validFraportPersonalNumber pn -- between 10000 and 99999 also see Handler.Utils.Profile.validFraportPersonalNumber
|
||||||
= let mkSap (dfrom,duntil) = SapUserTableCsv
|
= let mkSap (dfrom,duntil) = SapUserTableCsv
|
||||||
{ csvSUTpersonalNummer = persNo
|
{ csvSUTpersonalNummer = persNo
|
||||||
, csvSUTqualifikation = sapId
|
, csvSUTqualifikation = sapId
|
||||||
, csvSUTgültigVon = dfrom
|
, csvSUTgültigVon = dfrom
|
||||||
, csvSUTgültigBis = duntil
|
, csvSUTgültigBis = duntil
|
||||||
, csvSUTausprägung = "J"
|
, csvSUTausprägung = "J"
|
||||||
}
|
}
|
||||||
in fmap mkSap $ compileBlocks firstHeld validUntil $ zipMaybes qubFroms qubUnblocks
|
in fmap mkSap $ compileBlocks firstHeld validUntil $ zipMaybes qubFroms qubUnblocks
|
||||||
procRes _ = []
|
procRes _ = []
|
||||||
|
|
||||||
-- | compute a series of valid periods, assume that lists is already sorted by Day
|
-- | compute a series of valid periods, assume that lists is already sorted by Day
|
||||||
-- the lists encodes qualification_user_blocks with block=False/unblock=True
|
-- the lists encodes qualification_user_blocks with block=False/unblock=True
|
||||||
compileBlocks :: Day -> Day -> [(Day,Bool)] -> [(Day, Day)]
|
compileBlocks :: Day -> Day -> [(Day,Bool)] -> [(Day, Day)]
|
||||||
compileBlocks dStart dEnd = go (dStart, True)
|
compileBlocks dStart dEnd = go (dStart, True)
|
||||||
where
|
where
|
||||||
go :: (Day,Bool) -> [(Day,Bool)] -> [(Day, Day)]
|
go :: (Day,Bool) -> [(Day,Bool)] -> [(Day, Day)]
|
||||||
go (d,s) (p1@(d1,s1):r1@((d2,s2):r2))
|
go (d,s) (p1@(d1,s1):r1@((d2,s2):r2))
|
||||||
| s1 == s2 && d <= d1 = go (d,s) (p1:r2) -- ignore unnecessary 2nd change
|
| s1 == s2 && d <= d1 = go (d,s) (p1:r2) -- ignore unnecessary 2nd change
|
||||||
| d1 == d2 || succ d1 == d2 || s == s1 || d > d1 = go (d,s) r1 -- ignore unnecessary 1st change
|
| d1 == d2 || succ d1 == d2 || s == s1 || d > d1 = go (d,s) r1 -- ignore unnecessary 1st change
|
||||||
go (d,s) ((d1,s1):r1)
|
go (d,s) ((d1,s1):r1)
|
||||||
| dEnd <= d1 = go (d ,s ) [] -- remaining dates extend validity
|
| dEnd <= d1 = go (d ,s ) [] -- remaining dates extend validity
|
||||||
| s, not s1, d < d1 = (d,d1) : go (d1,s1) r1 -- valid interval found
|
| s, not s1, d < d1 = (d,d1) : go (d1,s1) r1 -- valid interval found
|
||||||
| s == s1 = go (d ,s ) r1 -- no change
|
| s == s1 = go (d ,s ) r1 -- no change
|
||||||
| otherwise = go (d1,s1) r1 -- ignore invalid interval
|
| otherwise = go (d1,s1) r1 -- ignore invalid interval
|
||||||
@ -95,40 +93,51 @@ compileBlocks dStart dEnd = go (dStart, True)
|
|||||||
-- | Deliver all employess with a successful LDAP synch within the last 3 months
|
-- | Deliver all employess with a successful LDAP synch within the last 3 months
|
||||||
getQualificationSAPDirectR :: Handler TypedContent
|
getQualificationSAPDirectR :: Handler TypedContent
|
||||||
getQualificationSAPDirectR = do
|
getQualificationSAPDirectR = do
|
||||||
now <- liftIO getCurrentTime
|
now <- liftIO getCurrentTime
|
||||||
fdate <- formatTime' "%Y%m%d_%H-%M" now
|
fdate <- formatTime' "%Y%m%d_%H-%M" now
|
||||||
let ldap_cutoff = addDiffDaysRollOver (fromMonths $ -3) now
|
userAuthConf <- getsYesod $ view _appUserAuthConf
|
||||||
qualUsers <- runDBRead $ E.select $ do
|
|
||||||
(qual :& qualUser :& user :& qualBlock) <-
|
let
|
||||||
E.from $ E.table @Qualification
|
ldapSources = case userAuthConf of
|
||||||
|
UserAuthConfSingleSource (AuthSourceConfLdap LdapConf{..})
|
||||||
|
-> singleton $ AuthSourceIdLdap ldapConfSourceId
|
||||||
|
_other -> mempty
|
||||||
|
ldapCutoff = addDiffDaysRollOver (fromMonths $ -3) now
|
||||||
|
|
||||||
|
qualUsers <- runDB $ E.select $ do
|
||||||
|
(qual :& qualUser :& user :& qualBlock) <-
|
||||||
|
E.from $ E.table @Qualification
|
||||||
`E.innerJoin` E.table @QualificationUser
|
`E.innerJoin` E.table @QualificationUser
|
||||||
`E.on` (\(qual :& qualUser) -> qual E.^. QualificationId E.==. qualUser E.^. QualificationUserQualification)
|
`E.on` (\(qual :& qualUser) -> qual E.^. QualificationId E.==. qualUser E.^. QualificationUserQualification)
|
||||||
`E.innerJoin` E.table @User
|
`E.innerJoin` E.table @User
|
||||||
`E.on` (\(_ :& qualUser :& user) -> qualUser E.^. QualificationUserUser E.==. user E.^. UserId)
|
`E.on` (\(_ :& qualUser :& user) -> qualUser E.^. QualificationUserUser E.==. user E.^. UserId)
|
||||||
`E.leftJoin` E.table @QualificationUserBlock
|
`E.leftJoin` E.table @QualificationUserBlock
|
||||||
`E.on` (\(_ :& qualUser :& _ :& qualBlock) ->
|
`E.on` (\(_ :& qualUser :& _ :& qualBlock) ->
|
||||||
qualUser E.^. QualificationUserId E.=?. qualBlock E.?. QualificationUserBlockQualificationUser
|
qualUser E.^. QualificationUserId E.=?. qualBlock E.?. QualificationUserBlockQualificationUser
|
||||||
E.&&. E.val now E.>~. qualBlock E.?. QualificationUserBlockFrom
|
E.&&. E.val now E.>~. qualBlock E.?. QualificationUserBlockFrom
|
||||||
)
|
)
|
||||||
E.where_ $ E.isJust (qual E.^. QualificationSapId)
|
E.where_ $ E.isJust (qual E.^. QualificationSapId)
|
||||||
E.&&. E.isJust (user E.^. UserCompanyPersonalNumber)
|
E.&&. E.isJust (user E.^. UserCompanyPersonalNumber)
|
||||||
E.&&. E.isJust (user E.^. UserLastLdapSynchronisation)
|
E.where_ . E.exists $ do
|
||||||
E.&&. (E.justVal ldap_cutoff E.<=. user E.^. UserLastLdapSynchronisation)
|
externalUser <- E.from $ E.table @ExternalUser
|
||||||
|
E.where_ $ externalUser E.^. ExternalUserUser E.==. user E.^. UserIdent
|
||||||
|
E.&&. externalUser E.^. ExternalUserSource `E.in_` E.valList ldapSources
|
||||||
|
E.&&. externalUser E.^. ExternalUserLastSync E.>=. E.val ldapCutoff
|
||||||
E.groupBy ( user E.^. UserCompanyPersonalNumber
|
E.groupBy ( user E.^. UserCompanyPersonalNumber
|
||||||
, qualUser E.^. QualificationUserFirstHeld
|
, qualUser E.^. QualificationUserFirstHeld
|
||||||
, qualUser E.^. QualificationUserValidUntil
|
, qualUser E.^. QualificationUserValidUntil
|
||||||
, qual E.^. QualificationSapId
|
, qual E.^. QualificationSapId
|
||||||
)
|
)
|
||||||
let blockOrder = [E.asc $ qualBlock E.?. QualificationUserBlockFrom, E.asc $ qualBlock E.?. QualificationUserBlockId]
|
let blockOrder = [E.asc $ qualBlock E.?. QualificationUserBlockFrom, E.asc $ qualBlock E.?. QualificationUserBlockId]
|
||||||
-- blockAgg f = E.arrayAggWith E.AggModeAll (qualBlock E.^. f) blockOrder
|
-- blockAgg f = E.arrayAggWith E.AggModeAll (qualBlock E.^. f) blockOrder
|
||||||
return
|
return
|
||||||
( user E.^. UserCompanyPersonalNumber
|
( user E.^. UserCompanyPersonalNumber
|
||||||
, qual E.^. QualificationSapId
|
, qual E.^. QualificationSapId
|
||||||
, qualUser E.^. QualificationUserFirstHeld
|
, qualUser E.^. QualificationUserFirstHeld
|
||||||
, qualUser E.^. QualificationUserValidUntil
|
, qualUser E.^. QualificationUserValidUntil
|
||||||
, E.arrayAggWith E.AggModeAll (E.dayMaybe $ qualBlock E.?. QualificationUserBlockFrom ) blockOrder
|
, E.arrayAggWith E.AggModeAll (E.dayMaybe $ qualBlock E.?. QualificationUserBlockFrom ) blockOrder
|
||||||
, E.arrayAggWith E.AggModeAll ( qualBlock E.?. QualificationUserBlockUnblock) blockOrder
|
, E.arrayAggWith E.AggModeAll ( qualBlock E.?. QualificationUserBlockUnblock) blockOrder
|
||||||
)
|
)
|
||||||
let csvRendered = toCsvRendered sapUserTableCsvHeader $ sapRes2csv qualUsers
|
let csvRendered = toCsvRendered sapUserTableCsvHeader $ sapRes2csv qualUsers
|
||||||
fmtOpts = (review csvPreset CsvPresetRFC)
|
fmtOpts = (review csvPreset CsvPresetRFC)
|
||||||
{ csvIncludeHeader = True
|
{ csvIncludeHeader = True
|
||||||
@ -144,7 +153,7 @@ getQualificationSAPDirectR = do
|
|||||||
let logInt = runDB $ logInterface "SAP" quals True (Just nr) ""
|
let logInt = runDB $ logInterface "SAP" quals True (Just nr) ""
|
||||||
addHeader "Content-Disposition" $ "attachment; filename=\"" <> csvSheetName <> "\""
|
addHeader "Content-Disposition" $ "attachment; filename=\"" <> csvSheetName <> "\""
|
||||||
csvRenderedToTypedContentWith csvOpts csvSheetName csvRendered <* logInt
|
csvRenderedToTypedContentWith csvOpts csvSheetName csvRendered <* logInt
|
||||||
|
|
||||||
|
|
||||||
-- direct Download see:
|
-- direct Download see:
|
||||||
-- https://ersocon.net/blog/2017/2/22/creating-csv-files-in-yesod
|
-- https://ersocon.net/blog/2017/2/22/creating-csv-files-in-yesod
|
||||||
|
|||||||
31
src/Handler/SingleSignOut.hs
Normal file
31
src/Handler/SingleSignOut.hs
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
-- SPDX-FileCopyrightText: 2024 David Mosbach <david.mosbach@uniworx.de>
|
||||||
|
--
|
||||||
|
-- SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
|
module Handler.SingleSignOut
|
||||||
|
( getSOutR
|
||||||
|
, getSSOutR
|
||||||
|
) where
|
||||||
|
|
||||||
|
import Import
|
||||||
|
import Auth.OAuth2 (singleSignOut)
|
||||||
|
import qualified Network.Wai as W
|
||||||
|
|
||||||
|
|
||||||
|
getSOutR :: Handler Html
|
||||||
|
getSOutR = do
|
||||||
|
$logDebugS "\27[31mSOut\27[0m" "Redirect to LogoutR"
|
||||||
|
redirect $ AuthR LogoutR
|
||||||
|
|
||||||
|
getSSOutR :: Handler Html
|
||||||
|
getSSOutR = do
|
||||||
|
app <- getYesod
|
||||||
|
let redir = intercalate "/" . fst . renderRoute $ SOutR
|
||||||
|
root = case approot of
|
||||||
|
ApprootRequest f -> f app W.defaultRequest
|
||||||
|
_ -> error "approt implementation changed"
|
||||||
|
url = decodeUtf8 . urlEncode True . encodeUtf8 $ root <> "/" <> redir
|
||||||
|
AppSettings{..} <- getsYesod appSettings'
|
||||||
|
$logDebugS "\27[31mSSOut\27[0m" "Redirect to auth server"
|
||||||
|
if appSingleSignOn then singleSignOut (Just url) else redirect (AuthR LogoutR)
|
||||||
|
|
||||||
@ -48,14 +48,14 @@ import Data.List (genericLength)
|
|||||||
|
|
||||||
import qualified Data.Csv as Csv
|
import qualified Data.Csv as Csv
|
||||||
|
|
||||||
{-# ANN module ("HLint: ignore Functor law" :: String) #-}
|
|
||||||
|
|
||||||
data CorrectionTableFilterProj = CorrectionTableFilterProj
|
data CorrectionTableFilterProj = CorrectionTableFilterProj
|
||||||
{ corrProjFilterSubmission :: Maybe (Set [CI Char])
|
{ corrProjFilterSubmission :: Maybe (Set [CI Char])
|
||||||
, corrProjFilterPseudonym :: Maybe (Set [CI Char])
|
, corrProjFilterPseudonym :: Maybe (Set [CI Char])
|
||||||
, corrProjFilterAuthorshipStatementState :: Last AuthorshipStatementSubmissionState
|
, corrProjFilterAuthorshipStatementState :: Last AuthorshipStatementSubmissionState
|
||||||
}
|
}
|
||||||
|
|
||||||
instance Default CorrectionTableFilterProj where
|
instance Default CorrectionTableFilterProj where
|
||||||
def = CorrectionTableFilterProj
|
def = CorrectionTableFilterProj
|
||||||
{ corrProjFilterSubmission = Nothing
|
{ corrProjFilterSubmission = Nothing
|
||||||
@ -64,7 +64,7 @@ instance Default CorrectionTableFilterProj where
|
|||||||
}
|
}
|
||||||
|
|
||||||
makeLenses_ ''CorrectionTableFilterProj
|
makeLenses_ ''CorrectionTableFilterProj
|
||||||
|
|
||||||
|
|
||||||
type CorrectionTableExpr = ( E.SqlExpr (Entity Course)
|
type CorrectionTableExpr = ( E.SqlExpr (Entity Course)
|
||||||
`E.InnerJoin` E.SqlExpr (Entity Sheet)
|
`E.InnerJoin` E.SqlExpr (Entity Sheet)
|
||||||
@ -135,7 +135,7 @@ resultSubmittors = _dbrOutput . _6 . itraversed
|
|||||||
|
|
||||||
resultUserUser :: Lens' CorrectionTableUserData User
|
resultUserUser :: Lens' CorrectionTableUserData User
|
||||||
resultUserUser = _1
|
resultUserUser = _1
|
||||||
|
|
||||||
resultUserPseudonym :: Traversal' CorrectionTableUserData Pseudonym
|
resultUserPseudonym :: Traversal' CorrectionTableUserData Pseudonym
|
||||||
resultUserPseudonym = _2 . _Just
|
resultUserPseudonym = _2 . _Just
|
||||||
|
|
||||||
@ -207,7 +207,7 @@ instance Csv.ToNamedRecord CorrectionTableCsv where
|
|||||||
, "rating-points" Csv..= csvCorrectionRatingPoints
|
, "rating-points" Csv..= csvCorrectionRatingPoints
|
||||||
, "rating-comment" Csv..= csvCorrectionRatingComment
|
, "rating-comment" Csv..= csvCorrectionRatingComment
|
||||||
]
|
]
|
||||||
where
|
where
|
||||||
mkEmpty = \case
|
mkEmpty = \case
|
||||||
[Nothing] -> []
|
[Nothing] -> []
|
||||||
x -> x
|
x -> x
|
||||||
@ -269,7 +269,7 @@ data CorrectionTableCsvQualification
|
|||||||
= CorrectionTableCsvNoQualification
|
= CorrectionTableCsvNoQualification
|
||||||
| CorrectionTableCsvQualifySheet
|
| CorrectionTableCsvQualifySheet
|
||||||
| CorrectionTableCsvQualifyCourse
|
| CorrectionTableCsvQualifyCourse
|
||||||
deriving (Eq, Ord, Read, Show, Enum, Bounded, Generic)
|
deriving (Eq, Ord, Read, Show, Enum, Bounded, Generic)
|
||||||
deriving anyclass (Universe, Finite)
|
deriving anyclass (Universe, Finite)
|
||||||
|
|
||||||
correctionTableCsvHeader :: Bool -- ^ @showCorrector@
|
correctionTableCsvHeader :: Bool -- ^ @showCorrector@
|
||||||
@ -402,7 +402,7 @@ colSubmissionLink = sortable (Just "submission") (i18nCell MsgTableSubmission) $
|
|||||||
colSelect :: forall act h epId. (Semigroup act, Monoid act, Headedness h, Ord epId) => Colonnade h CorrectionTableData (DBCell _ (FormResult (act, DBFormResult CryptoFileNameSubmission Bool CorrectionTableData), SheetTypeSummary epId))
|
colSelect :: forall act h epId. (Semigroup act, Monoid act, Headedness h, Ord epId) => Colonnade h CorrectionTableData (DBCell _ (FormResult (act, DBFormResult CryptoFileNameSubmission Bool CorrectionTableData), SheetTypeSummary epId))
|
||||||
colSelect = dbSelect (_1 . applying _2) id $ views resultCryptoID return
|
colSelect = dbSelect (_1 . applying _2) id $ views resultCryptoID return
|
||||||
|
|
||||||
|
|
||||||
colSubmittors :: IsDBTable m a => Colonnade Sortable CorrectionTableData (DBCell m a)
|
colSubmittors :: IsDBTable m a => Colonnade Sortable CorrectionTableData (DBCell m a)
|
||||||
colSubmittors = sortable (Just "submittors") (i18nCell MsgSubmissionUsers) $ \x ->
|
colSubmittors = sortable (Just "submittors") (i18nCell MsgSubmissionUsers) $ \x ->
|
||||||
let tid = x ^. resultCourseTerm
|
let tid = x ^. resultCourseTerm
|
||||||
@ -457,7 +457,7 @@ colRating' l = sortable (Just "rating") (i18nCell MsgTableRating) $ \x ->
|
|||||||
]
|
]
|
||||||
|
|
||||||
colAssigned :: IsDBTable m a => Colonnade Sortable CorrectionTableData (DBCell m a)
|
colAssigned :: IsDBTable m a => Colonnade Sortable CorrectionTableData (DBCell m a)
|
||||||
colAssigned = sortable (Just "assignedtime") (i18nCell MsgAssignedTime) $ \x -> maybeCell (x ^? resultSubmission . _entityVal . _submissionRatingAssigned . _Just) dateTimeCell
|
colAssigned = sortable (Just "assignedtime") (i18nCell MsgAssignedTime) $ \x -> maybeCell (x ^? resultSubmission . _entityVal . _submissionRatingAssigned . _Just) dateTimeCell
|
||||||
|
|
||||||
colRated :: IsDBTable m a => Colonnade Sortable CorrectionTableData (DBCell m a)
|
colRated :: IsDBTable m a => Colonnade Sortable CorrectionTableData (DBCell m a)
|
||||||
colRated = sortable (Just "ratingtime") (i18nCell MsgTableRatingTime) $ \x -> maybeCell (x ^? resultSubmission . _entityVal . _submissionRatingTime . _Just) dateTimeCell
|
colRated = sortable (Just "ratingtime") (i18nCell MsgTableRatingTime) $ \x -> maybeCell (x ^? resultSubmission . _entityVal . _submissionRatingTime . _Just) dateTimeCell
|
||||||
@ -515,7 +515,7 @@ colAuthorshipStatementState = sortable (Just "as-state") (i18nCell MsgSubmission
|
|||||||
csh = x ^. resultCourseShorthand
|
csh = x ^. resultCourseShorthand
|
||||||
shn = x ^. resultSheet . _entityVal . _sheetName
|
shn = x ^. resultSheet . _entityVal . _sheetName
|
||||||
cID = x ^. resultCryptoID
|
cID = x ^. resultCryptoID
|
||||||
|
|
||||||
asRoute = CSubmissionR tid ssh csh shn cID SubAuthorshipStatementsR
|
asRoute = CSubmissionR tid ssh csh shn cID SubAuthorshipStatementsR
|
||||||
in maybeCell (guardOnM (x ^. resultNonAnonymousAccess) $ x ^. resultASState) (\s -> heatC s $ anchorCell asRoute (i18n s :: Widget))
|
in maybeCell (guardOnM (x ^. resultNonAnonymousAccess) $ x ^. resultASState) (\s -> heatC s $ anchorCell asRoute (i18n s :: Widget))
|
||||||
|
|
||||||
@ -537,7 +537,7 @@ filterUIIsRated = flip (prismAForm $ singletonFilter "israted" . maybePrism _Pat
|
|||||||
|
|
||||||
filterUISubmission :: DBFilterUI
|
filterUISubmission :: DBFilterUI
|
||||||
filterUISubmission = flip (prismAForm $ singletonFilter "submission") $ aopt (lift `hoistField` textField) (fslI MsgTableSubmission)
|
filterUISubmission = flip (prismAForm $ singletonFilter "submission") $ aopt (lift `hoistField` textField) (fslI MsgTableSubmission)
|
||||||
|
|
||||||
filterUIPseudonym :: DBFilterUI
|
filterUIPseudonym :: DBFilterUI
|
||||||
filterUIPseudonym = flip (prismAForm $ singletonFilter "pseudonym") $ aopt (lift `hoistField` textField) (fslI MsgSubmissionPseudonym)
|
filterUIPseudonym = flip (prismAForm $ singletonFilter "pseudonym") $ aopt (lift `hoistField` textField) (fslI MsgSubmissionPseudonym)
|
||||||
|
|
||||||
@ -809,7 +809,7 @@ correctionsR whereClause displayColumns dbtFilterUI csvSettings psValidator acti
|
|||||||
fmap toTypedContent . defaultLayout $ do
|
fmap toTypedContent . defaultLayout $ do
|
||||||
setTitleI MsgCourseCorrectionsTitle
|
setTitleI MsgCourseCorrectionsTitle
|
||||||
$(widgetFile "corrections")
|
$(widgetFile "corrections")
|
||||||
|
|
||||||
correctionsR' :: CorrectionTableWhere -> _ -> _ -> Maybe CorrectionTableCsvSettings -> _ -> Map ActionCorrections (AForm (HandlerFor UniWorX) ActionCorrectionsData) -> Handler (Widget, SheetTypeSummary SqlBackendKey)
|
correctionsR' :: CorrectionTableWhere -> _ -> _ -> Maybe CorrectionTableCsvSettings -> _ -> Map ActionCorrections (AForm (HandlerFor UniWorX) ActionCorrectionsData) -> Handler (Widget, SheetTypeSummary SqlBackendKey)
|
||||||
correctionsR' whereClause displayColumns dbtFilterUI csvSettings psValidator actions = do
|
correctionsR' whereClause displayColumns dbtFilterUI csvSettings psValidator actions = do
|
||||||
currentRoute <- fromMaybe (error "correctionsR called from 404-handler") <$> getCurrentRoute -- This should never be called from a 404 handler
|
currentRoute <- fromMaybe (error "correctionsR called from 404-handler") <$> getCurrentRoute -- This should never be called from a 404 handler
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user