Compare commits

..

26 Commits

Author SHA1 Message Date
378f38eece chore(jobsystem): rename priority to JobPrio to avoid name clashes 2023-12-06 01:57:05 +00:00
5fe9665150 chore: add SemVer instances 2023-12-06 01:51:47 +00:00
96622d3a2b chore(jobsystem): switch to UTCTime instead of LocalTime 2023-12-06 01:22:32 +00:00
d6662e3781 chore: bump jobsystem dependencies 2023-12-06 01:19:28 +00:00
8d87c50a4a chore(jobsystem): add TimeRelative and JobStatus instances 2023-12-03 02:00:12 +00:00
bb76399531 chore(stack): update jobsys 2023-12-03 01:53:18 +00:00
1b8458c83b chore(stack): update jobsys 2023-12-02 16:03:40 +00:00
9b3b5c1a99 chore(jobsystem): add more model fields 2023-12-02 16:03:29 +00:00
ab8bbb5495 chore(stack): update jobsys 2023-12-01 01:04:29 +00:00
0154239133 chore(stack): update jobsys 2023-11-30 02:35:31 +00:00
f50ff8ddca Merge branch 'fradrive/new-jobsystem' of gitlab.uniworx.de:fradrive/fradrive into fradrive/new-jobsystem 2023-11-30 00:23:13 +00:00
79d2c87405 Revert "chore(lms): split qualification-check-hour setting into two"
This reverts commit 8d5fb3c2b0
2023-11-30 00:23:05 +00:00
18c41afe0f Merge branch 'fradrive/new-jobsystem' of gitlab.uniworx.de:fradrive/fradrive into fradrive/new-jobsystem 2023-11-30 00:21:32 +00:00
ac1e272fb5 chore(jobsystem): add ressource model representation 2023-11-30 00:19:47 +00:00
c81bc149a9 chore(model): add model table for jobs 2023-11-30 00:19:09 +00:00
8d5fb3c2b0 chore(lms): split qualification-check-hour setting into two 2023-11-23 11:43:21 +01:00
1f7808ddf1 reimplement JobOccurrence as newtype; add JSON and PersistField instances 2023-11-21 21:37:11 +00:00
cacf2d291d bump jobsys dependencies 2023-11-21 21:31:30 +00:00
36a53d3dae chore(jobsystem): add dependencies, work on first model defs 2023-11-02 20:49:05 +00:00
44c1d98cc0 chore(stack): update jobsys 2023-10-30 21:52:49 +00:00
61a74804f4 chore: add jobsys as dependency in package.yaml 2023-10-30 01:34:10 +00:00
77e53812b4 chore(stack): update jobsys 2023-10-30 01:33:43 +00:00
cbd134b330 chore(jobsystem): import Jobsys 2023-10-30 01:32:59 +00:00
d6f936b270 chore: add jobsys dependency 2023-10-23 23:58:38 +00:00
31239454f5 fix: fix build 2023-10-23 23:36:12 +00:00
fc00b56b04 chore(jobsystem): add module stub for new jobsystem main entry point 2023-10-23 23:32:00 +00:00
231 changed files with 5964 additions and 12312 deletions

View File

@ -2,12 +2,12 @@
# #
# SPDX-License-Identifier: AGPL-3.0-or-later # SPDX-License-Identifier: AGPL-3.0-or-later
# workflow: workflow:
# rules: rules:
# - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
# - if: $CI_MERGE_REQUEST_ID - if: $CI_MERGE_REQUEST_ID
# - if: $CI_COMMIT_TAG =~ /^v/ - if: $CI_COMMIT_TAG =~ /^v/
# - if: $CI_COMMIT_TAG =~ /^t/ - if: $CI_COMMIT_TAG =~ /^t/
default: default:
image: image:

View File

@ -2,342 +2,6 @@
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
## [27.4.79](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/v27.4.78...v27.4.79) (2024-09-10)
### Bug Fixes
* **notifications:** fix [#180](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/issues/180) qualification expiry notification are sent only once ([74f7633](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/74f7633837870448f7cab1013719f42ab49941fe))
* **supervision:** fix [#181](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/issues/181) by unifying deletion of supervision ([6a070a6](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/6a070a67756bd4ef4b9b5efc176f34c7ed183f1a))
## [27.4.78](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/v27.4.77...v27.4.78) (2024-09-05)
### Bug Fixes
* **avs:** acs auto synch had inverted success/failure ([4f7855b](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/4f7855b9ee7133c5ee7e2ca63d63e5d9f060d62f))
* **avs:** fix [#124](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/issues/124) avs auto synch filter working ([2a27a1e](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/2a27a1efa673a4245a7e8667bd30c79ac1891b9c))
* **avs:** fix [#178](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/issues/178) by deleting old superiors for individual users ([ade27e6](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/ade27e647913ffe4432b41d585b3e00d1c68d4a0))
* **avs:** typo in superior remark, towards [#178](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/issues/178) ([3c5edb1](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/3c5edb1b970c8c154d9957837007815b29e23964))
* **mail:** fix [#179](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/issues/179) by adding download links for PDF attachments ([620e3e4](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/620e3e470080831826ccc960dd876e7bb4fcea03))
## [27.4.77](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/v27.4.76...v27.4.77) (2024-09-02)
### Bug Fixes
* **avs:** attempt LDAP upsert before creating avs users ([cfe2318](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/cfe2318f81c951a7f7310e8bcd9ec25d79417587))
* **avs:** company superiors are now irregular supervisors and old ones are deleted ([7e5c256](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/7e5c256b4c15a15f7218dd7c1490d5e7add4b1c1))
* **avs:** fix [#124](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/issues/124) implement automatic avs driving licence synchronisation ([cc5da9a](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/cc5da9a2a9bfc8a29f6fe19260bd6dc5412ad4a1))
* **avs:** switch company did not always increase priority ([8ec2875](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/8ec2875590718f28c3bab8c10141065e11f1405c))
* **build:** minor linter fix ([be5e609](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/be5e609b1fe879428784d78fa62a559d0764a85a))
* **firm:** fix [#174](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/issues/174) by adding address search filter to all company view ([40dadd5](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/40dadd58762156005b5889b93a56ffdc044b4460))
* **firm:** fix [#175](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/issues/175) by separating superiors in firm tables and selections ([8397c46](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/8397c468a04af42ba3baee2f84a0051adbc74374))
* **ldap:** no more timeout for ldap synch all button ([f946e99](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/f946e99da3bc37514a4e3621438ac133cdc16732))
* **linter:** minor bug in exam-correct.hs ([8bc3663](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/8bc3663ee2e4ded19091ebe350de82cd693093fc))
* **mail:** display html emails no longer distorts page ([b0972bb](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/b0972bb154f453edd545fb4f658d9f5ff79966eb)), closes [#2](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/issues/2)
* **model:** flip erroneous boolean SQL default for CompanyPostalAddress ([b7e5b8f](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/b7e5b8f111b5115d816d984c6ef2f12edfcef5bb))
* **user:** fix pagination and count for supervision tables ([9c82558](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/9c82558d71a032dad27e892c489c7004d091e088))
## [27.4.76](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/v27.4.75...v27.4.76) (2024-08-08)
### Bug Fixes
* **ap:** disambiguate action message ([8b0466e](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/8b0466e74e36e1d0d07518fd317d46b00ab53eff))
* **avs:** fix [#173](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/issues/173) by not using firm superior email as display email ([43f5c5f](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/43f5c5f4854d1ab2af27b479e72a58e2818a5696))
* **avs:** towards [#117](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/issues/117) update if current value is Nothing even if oldval == newval ([d1fa01f](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/d1fa01fcc5125c4adee8849f9c944884926f78ad))
* **avs:** using firm superior as UserEmail is a no-go due to uniqueness constraints ([507a7e0](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/507a7e02fc68476d01031dc9f9ee1a669a453ed1))
* **build:** linter likes it ([f929e03](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/f929e03129378e08c8a08ed4bd6f8e8716401813))
* **course:** fix [#150](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/issues/150) course edit for associated qualifications requires school admin or lecturer rights ([5b6e4e6](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/5b6e4e60e7d2957fbce93ee2e2d6d3464b4e3db7))
* **course:** fix [#148](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/issues/148) course qualification ordering ([cfd2534](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/cfd25348ad3b63ac6bc5031467a3c4ead2e07eed)), closes [#150](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/issues/150)
* **course:** fix [#149](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/issues/149) course cloning proposes associated qualifications ([e141976](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/e1419766f3a06f702abad0ea42f6552305504ba0))
* **course:** fix [#150](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/issues/150) no longer allow duplicated associated qualifications and orders due to editing existing ([ec02767](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/ec027675525b30198378745ed281f60a42471807))
* **course:** WIP course cloning should propose same associated qualifications, towards [#149](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/issues/149) ([bc47387](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/bc47387c91dda60a2f12e52dba28ea7b079316f0))
* **lms:** max e-learning tries default removed and info added to lms overview ([11fdcf0](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/11fdcf0d445b8cfe97c3a3c26513a9229937c536))
* **user:** format userDisplayNames having umlaut substitutes with respect to userSurname correctly ([e35a5e9](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/e35a5e99a6cea0976fd1c28f919e7d0ac0338503))
## [27.4.75](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/v27.4.74...v27.4.75) (2024-07-12)
### Bug Fixes
* **build:** make linter happy again ([c17c18f](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/c17c18f9247ef322bc051602a3cb4a52cd50affa))
* **build:** minor ([ab28c8c](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/ab28c8c2437680023d80e6ab43113d4328b3a151))
* **firm:** fix [#157](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/issues/157) by removing redundant duplicated code in firm user and supervision handling ([28e2739](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/28e2739e515700d15c75647c0efe2fe9a9cf15b1))
* **job:** change some queueJob' to queueJob instead ([fa0541a](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/fa0541aa4eaf10f98535a0959593b148b8346109))
* **lms:** allow 2nd reminders to be independent of renewal period ([d853e85](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/d853e8559b753865ee818bf24764f5c8d2e2303f))
* **lms:** move lms reuse info from QualificationR to LmsR ([468af9d](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/468af9de9da44a8ad685ca4bb6890a3e630b58be))
* **lms:** send second reminder indepentently from renewal period ([a97c3a5](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/a97c3a5c9d3cb9dddf90f561712f0845400893bd))
* **nix:** workaround parsing port numbers failed in nix-shell ([b5215cc](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/b5215cc7e8df3a7ad636271c8e6950979b2b8e42))
* **users:** nameHtml no longer complains about differing case for surname and displayname ([a1668f8](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/a1668f891a36b887439afb098f016ef22535af42))
* **users:** remove users with company post address from list of unreachable users ([c813c66](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/c813c665ed306135b7813d91d23310341c689f41))
## [27.4.74](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/v27.4.73...v27.4.74) (2024-07-04)
### Bug Fixes
* **lms:** fix [#161](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/issues/161) lms for multiple joint qualifications ([f869a82](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/f869a829d2c1a726930864b3af62d1f0fbebe955))
## [27.4.73](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/v27.4.72...v27.4.73) (2024-07-03)
### Bug Fixes
* **letter:** rephrase some minor letter parts ([0ac75e0](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/0ac75e0d5948cb90855d0e36ca8e99c22a0f6fcb))
## [27.4.72](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/v27.4.71...v27.4.72) (2024-07-02)
### Bug Fixes
* **avs:** do not associate users by AvsInfoPersonEmail ([9e2f221](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/9e2f2214ce5c7ee1e8d80e6fa75298b7a70d9043))
* **avs:** fix superfluous quotes for matriculation numbers on newly created users ([ff9014c](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/ff9014ce05d197c1dc0fce0774a640789cb38b26))
* **avs:** towards [#169](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/issues/169) - superiors are elevated to max priority for that company ([5bf8539](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/5bf85394d4db6de8f10b4e318d667130d37601ac))
* **firm:** supervisor secondary did not work as intended ([d4f3ce7](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/d4f3ce7bf3d208b16f95ab81971b47dfa752939a))
## [27.4.71](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/v27.4.70...v27.4.71) (2024-06-27)
### Bug Fixes
* **avs:** company superior emails become company wide supervisors ([37efc89](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/37efc89e0723452e6d271ba5b43d6bd026642190))
* **avs:** match mobile number better between LDAP and AVS ([f108c6c](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/f108c6cfec2d94d866e7c1605b0abe5471fd0f2b))
* **avs:** new AVS from existing LDAP user no longer misses fields ([2559346](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/2559346d963ede802321dfc8cbd2088d9a5de685))
* **avs:** priority for picking primary email demote superior ([e4fa1dd](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/e4fa1ddd6873910bef82d569fe16aca936efc567))
* **build:** add missing license file ([8721bdb](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/8721bdb3f349658baab144d64c19942bfd7fa49a))
* **build:** hlint wants a newtype instead ([18cdc52](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/18cdc52df094b9dbccd4f015561367cea59e33fe))
* **doc:** fix erroneous unintentional haddock annotations ([3dfc7f8](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/3dfc7f8c8b12dd6ef87848a75f1669d700fffe4c))
* **i18n:** add missing translation for new primary company ([c212f2e](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/c212f2e8d735616e59c9b8111a34118e3a48fd47))
* **i18n:** add missing translation for new primary company ([2cc529b](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/2cc529be39655c317ca028f8f09fa80826ec668d))
* **ldap:** match mobile number better between LDAP and AVS ([47e5628](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/47e56280fce4ad37e6bc3b9f1c61cb7867069cc5))
* **letter:** adjust spacing, pin location and interpolation ([d4a0e1f](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/d4a0e1f201151f76e8e9afd67b456cc878d2afde))
* **letter:** convenience links working again ([5f1af13](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/5f1af130edae7ada2f0c7f7829890bbe0d4f395a))
* **letter:** expiry and valid dates were wrong ([f8c3663](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/f8c36636ff1f2591507e993af32ed01af94cf1fc))
* **letter:** switch markdown for renewal letter too ([c38e87e](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/c38e87e1e0e9285a10c00521b7440cd8246af88a))
* **print:** fix [#167](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/issues/167) by sotring affected user in PrintJob ([73aecc2](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/73aecc2df833bdeed93a113b6c756e36b50491b7))
## [27.4.70](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/v27.4.69...v27.4.70) (2024-06-21)
### Bug Fixes
* **build:** hlint wants a newtype instead ([0766351](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/07663516e520814e26740d671325b7cd10855dd4))
## [27.4.69](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/v27.4.68...v27.4.69) (2024-06-21)
### Bug Fixes
* **avs:** fix type causing avs surname upate not working ([822c43c](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/822c43c8a7db2086954ad187502ec2c4f1811d17))
* **avs:** keep company on unchange address/email only if either is non-empty ([766b858](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/766b8589d6945df21fc6ce90d35a004655ffa471))
* **avs:** synch job deletes used row instead of truncation ([d7acc7a](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/d7acc7a2d0fe5fc18929a8cb2d9c9f8a259c9944))
## [27.4.68](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/v27.4.67...v27.4.68) (2024-06-19)
### Bug Fixes
* **letter:** minor ([2ae11dc](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/2ae11dc25c000486af9acc26439a0580f5c687f2))
## [27.4.67](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/v27.4.66...v27.4.67) (2024-06-17)
### Bug Fixes
* **avs:** fix rare avs update bug involving values optional in avs but compulsory in user entity ([a6d0105](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/a6d0105903caba0eb47715eeb217ea2c53d99e23))
## [27.4.66](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/v27.4.65...v27.4.66) (2024-06-12)
### Bug Fixes
* **avs:** fix [#164](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/issues/164) by removing companyPersonalNumber and companyDepartment upon ldap sync expiry ([da74b95](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/da74b957295caefb010c90297af557f997b18e7c))
* **avs:** fix [#165](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/issues/165) by updating userCompanyDepartmen and userCompanyPersonalNumer ([76e0710](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/76e0710c7b54a40d2c236299ea4fabd009d3f35a))
* **avs:** repeated avs sync enqueue no longe violates duplicate db uniqueness constraints ([996e6a0](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/996e6a0ce563bda96638863efd40ce38fce8ac2b))
* **avs:** update email on manual company switch ([9fd80f2](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/9fd80f25526eefce217c659f6ea2991771c11ece)), closes [#164](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/issues/164)
## [27.4.65](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/v27.4.64...v27.4.65) (2024-06-10)
### Bug Fixes
* **avs:** company update no longer fails on duplicate key ([bb101de](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/bb101dee7b40cd3d8ba10a559af642396d5b87b5))
* **avs:** profile page correctly indicates automatic email and postal addresses ([e553ad4](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/e553ad4358a71fc96fa946533f0441d4af5202c9))
* **avs:** steps towards [#164](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/issues/164) ([aa1d230](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/aa1d230e497f0e59dbea9f4fd5c7da773f5a4280))
* **lette:** adjust window for new pin letters ([6acfd84](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/6acfd849aeb473a018f7a9c34e69f61b3c22b6f8))
## [27.4.64](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/v27.4.63...v27.4.64) (2024-05-27)
## [27.4.63](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/v27.4.62...v27.4.63) (2024-05-23)
### Bug Fixes
* **avs:** company update checks uniques and ignores those updates if necessary ([9451d90](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/9451d90a9e00d08a2a7d169c4674d99ff1018ee9))
## [27.4.62](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/v27.4.61...v27.4.62) (2024-05-19)
### Bug Fixes
* **avs:** avs update on company shorthands working now ([ff2347b](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/ff2347b1c950c7a2bb281cdcd07a52925e23b9f0))
* **avs:** deal gracefully with empty card status results ([ccf9340](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/ccf934044938277d821eb4b9ea08a8a134e84189))
## [27.4.61](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/v27.4.60...v27.4.61) (2024-05-06)
### Bug Fixes
* **avs:** fix [#76](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/issues/76) allowing company changes and fix [#69](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/issues/69) ([3c4a0b8](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/3c4a0b86c1e3d8a28405ab73b964ba1b988d2822))
* **build:** add missing tex packages ([6750798](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/6750798920dc76882f4e8ef39b47018fb7b77e44))
* **build:** workaround non modal form result handler ([2fbd281](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/2fbd28154cd7aea282eaa2604a42263ac90e3b1e))
## [27.4.60](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/v27.4.59...v27.4.60) (2024-04-26)
### Bug Fixes
* **avs:** disable caching by 0s no longer causes an exception ([d578e80](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/d578e80282c8bf6872fa6040514a9d2c85582707))
* **avs:** fix [#152](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/issues/152) by providing new online avs card filter throughout ([ad2375b](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/ad2375b338866f37c8b7825a9eab12fa6c9abccb))
* **avs:** fix [#36](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/issues/36) and remove dead code ([4f8850b](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/4f8850b3b4f710f9cf59163175b27599c97ac5c0))
* **avs:** fix [#69](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/issues/69) by redesigning live avs status page ([697979c](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/697979c277ce7198f4573d6cea30373a1fcc17da))
* **avs:** invalidate contact cache after licence writes ([c382be9](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/c382be9325fcc92e13cb5dc2ad7c20b198db26fc))
* **avs:** several minor bugfixes ([a52c8a6](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/a52c8a6ad709029a8822d383370b0d2bdd25e7d7)), closes [#158](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/issues/158)
* **build:** add import needed for production only ([724e4a0](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/724e4a0bec343ab9c6d172d8e93b8040bbe3fe7d))
* **build:** migration needs to check for table existens first ([f439ea4](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/f439ea45af9b1c4a029fc1b9b6383f3c97194ed0))
* **build:** minor error non-development code ([66eaa4f](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/66eaa4f7dcc124b631414d4a1adbe555a4029100))
* **build:** missing parameters added ([83afdf7](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/83afdf760f93fc1a553de3a122b444412ed84ba4))
* **build:** simple type error ([d56a1cd](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/d56a1cdd46259418faa737b9bb0a9d9ffba442e0))
* **build:** type error in test db fill data ([f465cc9](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/f465cc972367233a4944dd0aeb81b223a187bb85))
* **doc:** minor haddock problems ([d4f8a6c](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/d4f8a6c77b2a4a4540935f7f0beca0d0605508c8))
* **firm:** supervisor filter acts weird in test environment ([b566e59](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/b566e59eb1325485fe26dc4f0b5cb63165c58f74))
* **i18n:** fix some bad plurals ([890f8ad](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/890f8ad8b60115533faa6b99f4c4504243cbfb1d))
* **lint:** remove minor superfluous dollar ([64a1233](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/64a123387f3539b73649d02a6ecd97de577097e6))
* **qualification:** fix [#159](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/issues/159) by removing an misleadingly named column for user qualification table ([fd6a538](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/fd6a5384d3517958a3c7726e32eed3bad197a591))
## [27.4.59](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/v27.4.58...v27.4.59) (2024-02-13)
### Bug Fixes
* **sql:** remove potential bug in relation to missing parenthesis after not_ ([42695cf](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/42695cf5ef9f21691dc027f1ec97d57eec72f03e))
## [27.4.58](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/v27.4.57...v27.4.58) (2024-02-08)
### Bug Fixes
* **health:** negative interface routes working as intended now ([3303c4e](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/3303c4eebf928e527d2f9c1eb6e2495c10b94b13))
* **lms:** previouly failed notifications will be sent again ([263894b](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/263894b05899ce55635d790f5334729fbc655ecc))
## [27.4.57](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/v27.4.56...v27.4.57) (2024-02-06)
### Bug Fixes
* **course:** fix [#147](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/issues/147) abort addd participant aborts now ([d332c0c](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/d332c0c11afd8b1dfe1343659f0b1626c968bbde))
* **health:** fix [#151](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/issues/151) by offering route /health/interface/* ([c71814d](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/c71814d1ef1efc16c278136dfd6ebd86bd1d20db))
* **health:** fix [#153](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/issues/153) and offer interface health route matching ([ce3852e](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/ce3852e3d365e62b32d181d58b7cbcc749e49373))
## [27.4.56](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/v27.4.55...v27.4.56) (2023-12-20)
### Bug Fixes
* **firm:** improve supervisor filter by caching ([88f24fe](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/88f24fe6f199290a83af2d204ba9aa2a838d11b8))
* **firm:** improve supervisor filter yet once more ([c7b5a3c](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/c7b5a3c6cb70c314ecbfbe25969b4b6be1d43161))
* **users:** fix [#121](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/issues/121) by providing last login column, which was the last part missing ([decc5af](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/decc5af6829998e2d0db79382bbd9a7bad7b5b09))
## [27.4.55](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/v27.4.54...v27.4.55) (2023-12-14)
### Bug Fixes
* **build:** while the blank is necessary to prevent unnecessary migrations, it is not allowed either, see [#133](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/issues/133) ([a4b2af7](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/a4b2af7f157444ead8c9df989741b266f7c2b4f2))
* **firm:** supervisor filter performance ([db77850](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/db77850c4f4cd1d68bfd38e02e0ae24584e1e556))
* **migration:** fix [#133](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/issues/133) by removing old outdated migrations irrelevant to FRADrive ([d4f0d69](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/d4f0d69428a4f7fc887cb6854cb59e3dea83b9bc))
* **migration:** ignore superfluous migration entries gracefully ([1d48b62](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/1d48b627f6b8cf1b03e2ef63850c36c429c9d3d6))
* **school:** fix [#133](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/issues/133) by adjusting default value ([2509358](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/25093588784381a19f34e5b091677b908420ddea))
## [27.4.54](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/v27.4.53...v27.4.54) (2023-12-11)
### Bug Fixes
* **db:** prevent superfluous migrations ([b73557a](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/b73557a1eee4315911c6369032447f8d1836d964))
## [27.4.53](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/v27.4.52...v27.4.53) (2023-12-09)
### Bug Fixes
* **admin:** minor fixes and translations for admin problem page ([30fae33](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/30fae33dedb1501e570e9edca288fea3c84ac84a))
* **avs:** background synch was only triggerd by manual synchs ([48ef25a](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/48ef25aa8ffbbd96c1578ae85b76f090d9042595))
* **firm:** group multi select field supervisor ([fc0ca7b](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/fc0ca7b854a686cf395dadf81b7423e530fd26b8))
* **firm:** set supervisor field not all fields required ([9878956](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/9878956716b04c7ae88989cb9b059d3edcb923dc))
* **firm:** supervisor filter ([3acb847](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/3acb847915010d10358ea02000c231dbba7cba26))
* **form:** multiSelectField working with grouped options ([3aa8901](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/3aa89019a8b4393da0eca715871a3793c1e3abb2))
* **print:** keep print jobs on user merge and lms id deletion ([a15862e](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/a15862ea72bc374af870ef3a23f86ae32c2c67a9))
## [27.4.52](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/v27.4.51...v27.4.52) (2023-12-01)
### Bug Fixes
* **build:** redundant parenthesis ([50eda5f](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/50eda5f65f7394fe519546609fe748490cb4dd72))
* **firm:** restrict firm access to company supervisors only ([0a06efd](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/0a06efd76c63180c996657c2c7d78efc5bddd83d))
* **firm:** supervisor changes led to inconsistent DB ([1d3345c](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/1d3345cbba1cb65ee49c6f62e145750545439642))
## [27.4.51](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/v27.4.50...v27.4.51) (2023-11-24)
### Bug Fixes
* **build:** minor errors firm handler ([06bb44c](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/06bb44cf715375b5dd0141a46f8e10924ad6cd9c))
* **cache:** remove risky caching for submissions ([4ae59fc](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/4ae59fc1fa658e1462139ddddd6dc80308d85872))
* **firm:** show default supervisors with no employees too ([0f9a7a8](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/0f9a7a8c53d216ca7a6d0a25462b19ab1fa00bb4))
## [27.4.50](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/v27.4.49...v27.4.50) (2023-11-17)
### Bug Fixes
* **avs:** preserve unset pin passwords in update ([8c4f848](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/8c4f848675e1125547d1fdfa05560affe4794118))
* **build:** fix whitespace in routes ([a24e44e](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/a24e44efc9a20d3934d96640bb9e21b3b6d55b96))
* **build:** minor ([954a239](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/954a23936a35ea6c32247d7e191312e63888c12d))
* **firm:** add sql indices for frequent filters to greatly enhance performance ([63e6d94](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/63e6d94df2fd1ce879cb59d14bc854f3c2556586))
* **firm:** firm messaging now works fine ([65cdc8d](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/65cdc8ddfef19eb3a5578c536575f91ba9717a13))
* **firm:** foreign supervisor counts correct and sortable ([601ce7a](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/601ce7abdf2a392d30f1ff799a2338968be795f1))
* **firm:** sending messages works, but not test messages ([42ff02d](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/42ff02d27e431a8855db7bf3046a1b74d297e6da))
* **lms:** improve sorting for firm all ([3865bda](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/3865bda64d488c161b55e1f6eb48ca1b742dff98))
* **lms:** LMS restart failing due to old LmsUser entry ([6761767](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/6761767c6ca8cab62a22aa6f755e6231e07ab411))
## [27.4.49](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/v27.4.48...v27.4.49) (2023-11-09)
### Bug Fixes
* **lms:** report log did not match qualification ([390ff31](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/390ff317ea3bb4ef8918c9cda858f5f228e4a882))
## [27.4.48](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/v27.4.47...v27.4.48) (2023-11-07)
### Bug Fixes
* **lms:** mark as ended only if not seen for at least one day ([8165892](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/8165892b2e4f945780bb8420cfc4eed50fdd294d))
## [27.4.47](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/v27.4.46...v27.4.47) (2023-11-03)
## [27.4.46](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/v27.4.45...v27.4.46) (2023-11-03)
### Bug Fixes
* **course:** grant qualifications now issues and unblocks ([5d8d8cf](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/5d8d8cf17e634ecb950a1c329c859fb93f94ef77))
* **users:** allow prefer postal setting for users with fraport department ([a9d56c5](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/a9d56c51dcc727f8637b09a0e849372e75032f5e))
## [27.4.45](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/v27.4.44...v27.4.45) (2023-10-18) ## [27.4.45](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/v27.4.44...v27.4.45) (2023-10-18)

View File

@ -83,7 +83,6 @@ health-check-matching-cluster-config-timeout: "_env:HEALTHCHECK_MATCHING_CLUSTER
synchronise-ldap-users-within: "_env:SYNCHRONISE_LDAP_WITHIN:1209600" # 14 Tage in Sekunden 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-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
@ -91,9 +90,8 @@ synchronise-avs-users-interval: "_env:SYNCHRONISE_AVS_INTERVAL:21600" # alle 6
study-features-recache-relevance-within: 172800 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, dequeue 30min later
job-lms-qualifications-enqueue-hour: 16 # qualification-check-hour: 3
job-lms-qualifications-dequeue-hour: 4
log-settings: log-settings:
detailed: "_env:DETAILED_LOGGING:false" detailed: "_env:DETAILED_LOGGING:false"
@ -158,12 +156,10 @@ 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"
@ -279,8 +275,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

View File

@ -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

View File

@ -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);

View File

@ -1,9 +1,11 @@
# 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 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
FAQLoginExpired: Mein Passwort ist abgelaufen und muss erneuert werden
FAQNoCampusAccount: Ich habe keine Fraport AG Kennung (Büko-Login); kann ich trotzdem Zugang zum System erhalten? FAQNoCampusAccount: Ich habe keine Fraport AG Kennung (Büko-Login); kann ich trotzdem Zugang zum System erhalten?
FAQForgottenPassword: Ich habe mein Passwort vergessen FAQForgottenPassword: Ich habe mein Passwort vergessen
FAQCampusCantLogin: Ich kann mich mit meiner Fraport AG Kennung (Büko-Login) nicht anmelden FAQCampusCantLogin: Ich kann mich mit meiner Fraport AG Kennung (Büko-Login) nicht anmelden
FAQNotLecturerHowToCreateCourses: Wie kann ich eine neue Kursart anlegen? FAQCourseCorrectorsTutors: Wie kann ich Ausbilder oder Korrektoren für meine Kursart konfigurieren?
FAQNotLecturerHowToCreateCourses: Wie kann ich eine neue Kursart anlegen?
FAQExamPoints: Warum kann ich bei meiner Klausur keine Punkte eintragen?
FAQInvalidCredentialsAdAccountDisabled: Ich kann mich nicht anmelden und bekomme die Meldung „Benutzereintrag gesperrt“

View File

@ -1,9 +1,11 @@
# 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 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
FAQLoginExpired: My password expired
FAQNoCampusAccount: I don't have Fraport AG credentials (Büko login); can I still get access? FAQNoCampusAccount: I don't have Fraport AG credentials (Büko login); can I still get access?
FAQForgottenPassword: I have forgotten my password FAQForgottenPassword: I have forgotten my password
FAQCampusCantLogin: I can't log in using my Fraport AG credentials (Büko login) FAQCampusCantLogin: I can't log in using my Fraport AG credentials (Büko login)
FAQNotLecturerHowToCreateCourses: How can I create new courses? FAQCourseCorrectorsTutors: How can I add instructors or correctors to my course?
FAQNotLecturerHowToCreateCourses: How can I create new courses?
FAQExamPoints: Why can't I enter achievements for my exam as points?
FAQInvalidCredentialsAdAccountDisabled: I can't log in and am instead given the message “Account disabled”

View File

@ -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 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,53 +109,14 @@ 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
ProblemsNoAvsSynchProblems: Synchronisation mit Ausweisverwaltungssystem (AVS) meldete keine Probleme
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
ProblemsNoAvsIdBody: Fahrer mit gültiger Fahrberechtigung in FRADrive, welche trotzdem nicht fahren dürfen, da die Fahrberechtigung aufgrund einer unbekannten AVS Id nicht an die Ausweisstelle übermittelt werden konnte: ProblemsNoAvsIdBody: Fahrer mit gültiger Fahrberechtigung in FRADrive, welche trotzdem nicht fahren dürfen, da die Fahrberechtigung aufgrund einer unbekannten AVS Id nicht an die Ausweisstelle übermittelt werden konnte:
ProblemsAvsSynchHeading: Synchronisation AVS Fahrberechtigungen ProblemsAvsSynchHeading: Synchronisation AVS Fahrberechtigungen
ProblemsAvsErrorHeading: Fehlermeldungen ProblemsAvsErrorHeading: Fehlermeldungen
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.
InterfacesFail n@Int: #{pluralDEeN n "Schnittstellenproblem"}!
InterfaceStatus !ident-ok: Status
InterfaceName: Schnittstelle
InterfaceLastSynch: Zuletzt
InterfaceSubtype: Betreffend
InterfaceWrite: Schreibend
InterfaceSuccess: Rückmeldung
InterfaceInfo: Nachricht
InterfaceFreshness: Maximale Zugriffsfrist
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

View File

@ -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 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,53 +109,14 @@ 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
ProblemsNoAvsSynchProblems: AVS synchronisation had not problems
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
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.
InterfacesFail n: #{pluralENsN n "interface problem"}!
InterfaceStatus: Status
InterfaceName: Interface
InterfaceLastSynch: Last
InterfaceSubtype: Affecting
InterfaceWrite: Write
InterfaceSuccess: Returned
InterfaceInfo: Message
InterfaceFreshness: Maximum usage period
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

View File

@ -20,8 +20,6 @@ UnauthorizedTokenInvalidAuthorityValue: Ihr Authorisierungs-Token basiert auf Re
UnauthorizedTokenInvalidImpersonation: Ihr Authorisierungs-Token enthält die Anweisung sich als ein Nutzer:in auszugeben, dies ist jedoch nicht allen Benutzer:innen, auf deren Rechten ihr Authorisierungs-Token basiert, erlaubt. UnauthorizedTokenInvalidImpersonation: Ihr Authorisierungs-Token enthält die Anweisung sich als ein Nutzer:in auszugeben, dies ist jedoch nicht allen Benutzer:innen, auf deren Rechten ihr Authorisierungs-Token basiert, erlaubt.
UnauthorizedToken404: Authorisierungs-Tokens können nicht auf Fehlerseiten ausgewertet werden. UnauthorizedToken404: Authorisierungs-Tokens können nicht auf Fehlerseiten ausgewertet werden.
UnauthorizedSupervisor: Sie sind kein Ansprechpartner:in für diesen Benutzer:in. UnauthorizedSupervisor: Sie sind kein Ansprechpartner:in für diesen Benutzer:in.
UnauthorizedAnySupervisor: Sie sind kein Ansprechpartner:in.
UnauthorizedCompanySupervisor fsh@CompanyShorthand: Sie sind kein Standard Ansprechpartner:in für Firma #{fsh}.
UnauthorizedSiteAdmin: Sie sind nicht System-weiter Administrator:in. UnauthorizedSiteAdmin: Sie sind nicht System-weiter Administrator:in.
UnauthorizedSchoolAdmin: Sie sind nicht als Administrator:in für diesen Bereich eingetragen. UnauthorizedSchoolAdmin: Sie sind nicht als Administrator:in für diesen Bereich eingetragen.
UnauthorizedAdminEscalation: Sie sind nicht Administrator:in für alle Bereiche, für die dieser Nutzer/diese Nutzerin Administrator:in oder Veranstalter:in ist. UnauthorizedAdminEscalation: Sie sind nicht Administrator:in für alle Bereiche, für die dieser Nutzer/diese Nutzerin Administrator:in oder Veranstalter:in ist.

View File

@ -20,8 +20,6 @@ UnauthorizedTokenInvalidAuthorityValue: The specification of the rights in which
UnauthorizedTokenInvalidImpersonation: Your authorisation-token contains an instruction to impersonate an user. Not all users on whose rights your token is based however are permitted to do so. UnauthorizedTokenInvalidImpersonation: Your authorisation-token contains an instruction to impersonate an user. Not all users on whose rights your token is based however are permitted to do so.
UnauthorizedToken404: Authorisation-tokens cannot be processed on error pages. UnauthorizedToken404: Authorisation-tokens cannot be processed on error pages.
UnauthorizedSupervisor: You are not a supervisor for the requested user. UnauthorizedSupervisor: You are not a supervisor for the requested user.
UnauthorizedAnySupervisor: You are not a supervisor.
UnauthorizedCompanySupervisor fsh: You are not a default supervisor for company #{fsh}.
UnauthorizedSiteAdmin: You are no system-wide administrator. UnauthorizedSiteAdmin: You are no system-wide administrator.
UnauthorizedSchoolAdmin: You are no administrator for this department. UnauthorizedSchoolAdmin: You are no administrator for this department.
UnauthorizedAdminEscalation: You aren't an administrator for all departments for which this user is an administrator. UnauthorizedAdminEscalation: You aren't an administrator for all departments for which this user is an administrator.

View File

@ -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.

View File

@ -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.

View File

@ -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}
@ -99,7 +95,7 @@ CourseParticipantsInvited n@Int: #{n} #{pluralDE n "Einladung" "Einladungen"} pe
CourseParticipantsAlreadyRegistered n@Int: #{n} #{pluralDE n "Teinehmer:in" "Teilnehmer:innen"} #{pluralDE n "ist" "sind"} bereits zur Kursart angemeldet CourseParticipantsAlreadyRegistered n@Int: #{n} #{pluralDE n "Teinehmer:in" "Teilnehmer:innen"} #{pluralDE n "ist" "sind"} bereits zur Kursart angemeldet
CourseParticipantsAlreadyTutorialMember n@Int: #{n} #{pluralDE n "Teinehmer:in" "Teilnehmer:innen"} #{pluralDE n "ist" "sind"} bereits in dieser Kurs angemeldet CourseParticipantsAlreadyTutorialMember n@Int: #{n} #{pluralDE n "Teinehmer:in" "Teilnehmer:innen"} #{pluralDE n "ist" "sind"} bereits in dieser Kurs angemeldet
CourseParticipantsRegistered n@Int: #{n} #{pluralDE n "Teinehmer:in" "Teilnehmer:innen"} erfolgreich zur Kursart angemeldet CourseParticipantsRegistered n@Int: #{n} #{pluralDE n "Teinehmer:in" "Teilnehmer:innen"} erfolgreich zur Kursart angemeldet
CourseParticipantsRegisteredTutorial n@Int: #{n} #{pluralDE n "Teinehmer:in" "Teilnehmer:innen"} erfolgreich zum Kurs angemeldet CourseParticipantsRegisteredTutorial n@Int: #{n} #{pluralDE n "Teinehmer:in" "Teilnehmer:innen"} erfolgreich zur Kurs angemeldet
CourseParticipantsRegisterConfirmationHeading: Teilnehmer:innen hinzufügen CourseParticipantsRegisterConfirmationHeading: Teilnehmer:innen hinzufügen
CourseParticipantsRegisterUnnecessary: Alle angeforderten Anmeldungen sind bereits vorhanden. Es wurden keine Änderungen vorgenommen. CourseParticipantsRegisterUnnecessary: Alle angeforderten Anmeldungen sind bereits vorhanden. Es wurden keine Änderungen vorgenommen.
CourseParticipantsRegisterConfirmInvalid: Ungültiges Bestätigungsformular! CourseParticipantsRegisterConfirmInvalid: Ungültiges Bestätigungsformular!

View File

@ -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

View File

@ -1,78 +0,0 @@
# SPDX-FileCopyrightText: 2023-24 Steffen Jost <s.jost@fraport.de>
#
# SPDX-License-Identifier: AGPL-3.0-or-later
FirmSuperDefault: Standardansprechpartner
FirmSuperForeign: Firmenfremde Ansprechpartner
FirmSuperIrregular: Irreguläre Ansprechpartner
FirmAssociates: Firmenangehörige
FirmContact: Firmenkontakt
FirmEmail: Allgemeine Email
FirmAddress: Postanschrift
FirmDefaultPreferenceInfo: Diese Voreinstellungen gelten nur für neue Firmenangehörige
FirmAction: Firmenweite Aktion
FirmActionInfo: Betrifft alle Firmenangehörigen unter Ihrer Aufsicht.
FirmActNotify: Mitteilung versenden
FirmActResetSupervision: Ansprechpartner für alle Firmenangehörigen zurücksetzen
FirmActResetSuperKeep: Bisherige Ansprechpartner der Firmenangehörigen zusätzlich beibehalten?
FirmActRemoveSupers: Alle rein firmenbezogenen Ansprechpartnerbeziehungen für diese Personen entfernen?
FirmActResetMutualSupervision: Ansprechpartner beaufsichtigen sich gegenseitig
FirmActResetSupersKeepAll: Alle behalten
FirmActResetSupersRemoveAps: Nur Standardansprechpartner entfernen
FirmActResetSupersRemoveAll: Alle entfernen
FirmActAddSupervisors: Ansprechpartner hinzufügen
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.
RemoveSupervisors ndef@Int64: #{ndef} Standardansprechpartner entfernt.
FirmActChangeContactUser: Kontaktinformationen von allen Firmenangehörigen ändern
FirmActChangeContactFirm: Kontaktinformationen der Firma ändern
FirmActChangeContactFirmInfo: Firmenkontaktinformationen werden nur für neue Firmenangehörige verwendet, für die sonst keine Kontaktinformationen vorliegen.
FirmActChangeContactFirmResult: Firmenkontaktinformationen geändert. Betrifft nur neue Firmenangehörige ohne eigene Kontaktinformationen
FirmUserActNotify: Mitteilung versenden
FirmUserActResetSupervision: Ansprechpartner auf Firmenstandard zurücksetzen
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
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)}
FirmResetSupervision rem@Int64 set@Int64: #{tshow set} Ansprechpartner gesetzt#{bool mempty (", " <> tshow rem <> " zuvor gelöscht") (rem > 0)}
FirmSuperActNotify: Mitteilung versenden
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.
FirmSuperActRMSuperDef: Firmenansprechpartner entfernen
FirmSuperActRMSuperActive: Aktive Ansprechpartnerbeziehungen innerhalb dieser Firma beenden?
FirmsNotification: Firmen E-Mail versenden
FirmNotification fsh@CompanyShorthand: E-Mail an #{fsh} senden
FirmsNotificationTitle: Firmen benachrichtigen
FirmNotificationTitle fsh@CompanyShorthand: #{fsh} benachrichtigen
FilterSupervisor: Hat aktiven Ansprechpartner
FilterSupervisorCompany fsh@CompanyShorthand: Hat aktiven Ansprechpartner, #{fsh} der angehört
FilterSupervisorForeign fsh@CompanyShorthand: Hat aktiven Ansprechpartner, der selbst nicht #{fsh} angehört
FilterForeignSupervisor: Hat firmenfremde Ansprechpartner
FilterIsForeignSupervisee: Ist Ansprechpartner für Firmenfremde
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
FirmSupervisorIndependent: Ansprechpartner ohne jegliche Firmenzugehörigkeit
FirmEmployeeOf fsh@CompanyShorthand: Firmenangehörige #{fsh}
NoCompanySelected: Bitte wählen Sie mindestens eine Firma aus.
TableIsDefaultSupervisor: Standardansprechpartner
TableSuperior: Vorgesetzter
TableIsDefaultReroute: Standardumleitung
FormFieldPostal: Benachrichtigungseinstellung
FormFieldPostalTip: Gilt für alle Benachrichtigungen an diese Person, nicht nur für Umleitungen an diesen Ansprechpartner
FirmSupervisionKeyData: Kennzahlen Ansprechpartner
CompanyUserPriority: Firmenpriorität
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!

View File

@ -1,78 +0,0 @@
# SPDX-FileCopyrightText: 2023-24 Steffen Jost <s.jost@fraport.de>
#
# SPDX-License-Identifier: AGPL-3.0-or-later
FirmSuperDefault: Default supervisor
FirmSuperForeign: External supervisor
FirmSuperIrregular: Irregular supervisor
FirmAssociates: Company associated users
FirmContact: Company Contact
FirmEmail: General company email
FirmAddress: Postal address
FirmDefaultPreferenceInfo: Default setting for new company associates only
FirmAction: Companywide action
FirmActionInfo: Affects alle company associates under your supervision.
FirmActNotify: Send message
FirmActResetSupervision: Reset supervisors for all company associates
FirmActResetSuperKeep: Additionally keep existing supervisors of company associates?
FirmActRemoveSupers: Terminate all company related supervisonships?
FirmActResetMutualSupervision: Supervisors supervise each other
FirmActResetSupersKeepAll: Keep all
FirmActResetSupersRemoveAps: Remove default supervisors only
FirmActResetSupersRemoveAll: Remove all
FirmActAddSupervisors: Add supervisors
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.
RemoveSupervisors ndef: #{ndef} default supervisors removed.
FirmActChangeContactUser: Change contact data for all company associates
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.
FirmActChangeContactFirmResult: Company contact data changed, affecting future company associates without contact information only
FirmUserActNotify: Send message
FirmUserActResetSupervision: Reset supervisors to company default
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
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)}
FirmResetSupervision rem set: #{tshow set} supervisors set#{bool mempty (", " <> tshow rem <> " deleted before") (rem > 0)}
FirmSuperActNotify: Send message
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.
FirmSuperActRMSuperDef: Remove default supervisor
FirmSuperActRMSuperActive: Terminate active supervisions within this company?
FirmsNotification: Send company notification e-mail
FirmNotification fsh: Send e-mail to #{fsh}
FirmsNotificationTitle: Company notification
FirmNotificationTitle fsh@CompanyShorthand: #{fsh} notification
FilterSupervisor: Has active supervisor
FilterSupervisorCompany fsh: Has active company supervisor belonging to #{fsh}
FilterSupervisorForeign fsh: Has active supervisor not belonging to #{fsh}
FilterForeignSupervisor: Has company-external supervisors
FilterIsForeignSupervisee: Supervisor for company external users
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}
FirmSupervisorIndependent: Independent supervisors
FirmEmployeeOf fsh@CompanyShorthand: #{fsh} associated users
NoCompanySelected: Select at least one company, please.
TableIsDefaultSupervisor: Default supervisor
TableSuperior: Superior
TableIsDefaultReroute: Default reroute
FormFieldPostal: Notification type
FormFieldPostalTip: Affects all notifications to this person, not just reroutes to this supervisor
FirmSupervisionKeyData: Supervision key data
CompanyUserPriority: Company priority
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!

View File

@ -15,6 +15,7 @@ ResetPassword: FRADrive-Passwort ändern bzw. setzen
MailSubjectChangeUserDisplayEmail: E-Mail-Adresse in FRADrive verwenden MailSubjectChangeUserDisplayEmail: E-Mail-Adresse in FRADrive verwenden
MailIntroChangeUserDisplayEmail displayEmail@UserEmail: Der oben genannte Benutzer/Die oben genannte Benutzerin möchte „#{displayEmail}“ als E-Mail-Adresse in FRADrive verwenden. Wenn Sie diese Aktion nicht selbst ausgelöst haben, ignorieren Sie diese Mitteilung bitte! MailIntroChangeUserDisplayEmail displayEmail@UserEmail: Der oben genannte Benutzer/Die oben genannte Benutzerin möchte „#{displayEmail}“ als E-Mail-Adresse in FRADrive verwenden. Wenn Sie diese Aktion nicht selbst ausgelöst haben, ignorieren Sie diese Mitteilung bitte!
MailTitleChangeUserDisplayEmail displayName@Text: #{displayName} möchte diese E-Mail-Adresse in FRADrive verwenden MailTitleChangeUserDisplayEmail displayName@Text: #{displayName} möchte diese E-Mail-Adresse in FRADrive verwenden
CommCourseSubject: Kursartmitteilung
InvitationAcceptDecline: Einladung annehmen/ablehnen InvitationAcceptDecline: Einladung annehmen/ablehnen
InvitationFromTip displayName@Text: Sie erhalten diese Einladung, weil #{displayName} ihren Versand in FRADrive ausgelöst hat. InvitationFromTip displayName@Text: Sie erhalten diese Einladung, weil #{displayName} ihren Versand in FRADrive ausgelöst hat.
InvitationFromTipAnonymous: Sie erhalten diese Einladung, weil ein nicht eingeloggter Benutzer/eine nichteingeloggte Benutzerin ihren Versand in FRADrive ausgelöst hat. InvitationFromTipAnonymous: Sie erhalten diese Einladung, weil ein nicht eingeloggter Benutzer/eine nichteingeloggte Benutzerin ihren Versand in FRADrive ausgelöst hat.

View File

@ -15,6 +15,7 @@ ResetPassword: Reselt FRADrive password
MailSubjectChangeUserDisplayEmail: Set email address in FRADrive MailSubjectChangeUserDisplayEmail: Set email address in FRADrive
MailIntroChangeUserDisplayEmail displayEmail: The user mentioned above wants to set “#{displayEmail}” as their own email address. If you have not caused this email to be sent, please ignore it! MailIntroChangeUserDisplayEmail displayEmail: The user mentioned above wants to set “#{displayEmail}” as their own email address. If you have not caused this email to be sent, please ignore it!
MailTitleChangeUserDisplayEmail displayName: #{displayName} wants to set this email address as their own in FRADrive MailTitleChangeUserDisplayEmail displayName: #{displayName} wants to set this email address as their own in FRADrive
CommCourseSubject: Course type message
InvitationAcceptDecline: Accept/Decline invitation InvitationAcceptDecline: Accept/Decline invitation
InvitationFromTip displayName: You are receiving this invitation because #{displayName} has caused it to be sent from within FRADrive. InvitationFromTip displayName: You are receiving this invitation because #{displayName} has caused it to be sent from within FRADrive.
InvitationFromTipAnonymous: You are receiving this invitiation because an user who didn't log in has caused it to be send from within FRADrive. InvitationFromTipAnonymous: You are receiving this invitiation because an user who didn't log in has caused it to be send from within FRADrive.

View File

@ -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: ELearning Id PrintLmsUser: ELearning Id
PrintJobs: Druckaufräge PrintJobs: Druckaufräge
PrintLetterType: Brieftypkürzel PrintLetterType: Brieftypkürzel
MCActDummy: Platzhalter
CCActDummy: Platzhalter

View File

@ -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: Elearning id PrintLmsUser: Elearning id
PrintJobs: Print jobs PrintJobs: Print jobs
PrintLetterType: Letter type shorthand PrintLetterType: Letter type shorthand
MCActDummy: Placeholder
CCActDummy: Placeholder

View File

@ -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 ELearning Daten. Hinweis: Der ELearning 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 ELearning Daten. Hinweis: Der ELearning Server kann seine anonymisierten Daten schon früher löschen, aber spätestens #{n} Tage nach Abschluss.
QualificationAuditDurationReuseError: Diese Qualifikation nutzt das ELearning 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 ELearning wird dieses gestartet und die Benachrichtigung erfolgt per Brief oder Email. QualificationRefreshWithinTooltip: Optionaler Zeitraum vor Ablauf für automatischen Start des ELearnings 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 ELearning Zugangsdaten, sofern die Qualifikation noch gültig und das ELearning 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 ELearning automatisch gestartet? QualificationElearningStart: Wird das ELearning automatisch gestartet?
QualificationElearningRenew: Verlängert ein erfolgreiches ELearning die Qualifikation automatisch um die reguläre Gültigkeitsdauer?
QualificationElearningLimit: Ist die Anzahl der ELearning 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 ELearning, sondern wird über das ELearning 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?
@ -46,7 +38,6 @@ QualificationScheduleRenewalTooltip: Wird eine Benachrichtigung versendet, falls
QualificationUserNoRenewal: Läuft ohne Benachrichtigung aus QualificationUserNoRenewal: Läuft ohne Benachrichtigung aus
QualificationUserNone: Für diese Person sind keine Qualifikationen registriert. QualificationUserNone: Für diese Person sind keine Qualifikationen registriert.
QualificationGrantReason: Erteilungsbegründung QualificationGrantReason: Erteilungsbegründung
QualificationRenewReason: Verlängerungsbegründung
QualificationBlockReason: Entzugsbegründung QualificationBlockReason: Entzugsbegründung
QualificationBlockNotify: Benachrichtigung verschicken QualificationBlockNotify: Benachrichtigung verschicken
QualificationBlockRemoveSupervisor: Alle Ansprechpartner löschen QualificationBlockRemoveSupervisor: Alle Ansprechpartner löschen
@ -54,13 +45,11 @@ QualificationExpired: Ungültig seit
LmsUser: Inhaber LmsUser: Inhaber
LmsURL: Link ELearning LmsURL: Link ELearning
TableLmsEmail: EMail TableLmsEmail: EMail
TableLmsIdent: ELearning Benutzer TableLmsIdent: E-Learning Benutzer
TableLmsElearning: ELearning TableLmsElearning: ELearning
TableLmsElearningRenews: Automatische Verlängerung
TableLmsElearningLimit: Maximale Versuche
TableLmsPin: ELearning Passwort TableLmsPin: ELearning Passwort
TableLmsResetPin: ELearning Passwort zurücksetzen? TableLmsResetPin: E-Learning Passwort zurücksetzen?
TableLmsDatePin: ELearning Passwort erstellt TableLmsDatePin: E-Learning Passwort erstellt
TableLmsDate: Datum TableLmsDate: Datum
TableLmsDelete: Löschen? TableLmsDelete: Löschen?
TableLmsStaff: Interner Mitarbeiter? TableLmsStaff: Interner Mitarbeiter?
@ -94,12 +83,17 @@ CsvColumnLmsDate: Datum des ELearning Ereignisses
CsvColumnLmsResetTries: Anzahl der bisher verbrauchten ELearning Prüfungsversuche zurücksetzen CsvColumnLmsResetTries: Anzahl der bisher verbrauchten ELearning Prüfungsversuche zurücksetzen
CsvColumnLmsLock: ELearning Login gesperrt CsvColumnLmsLock: ELearning Login gesperrt
CsvColumnLmsResult !ident-ok: LMS Status CsvColumnLmsResult !ident-ok: LMS Status
LmsUserlistInsert: Neuer LMS User
LmsUserlistUpdate: LMS User Aktualisierung
LmsResultInsert: Neues LMS Ergebnis
LmsResultUpdate: LMS Ergebnis Aktualisierung
LmsReportInsert: Neues LMS Ereignis LmsReportInsert: Neues LMS Ereignis
LmsReportUpdate: LMS Ereignis Aktualisierung LmsReportUpdate: LMS Ereignis Aktualisierung
LmsResultCsvExceptionDuplicatedKey: CSV-Import LmsResult fand uneindeutigen Schlüssel
LmsUserlistCsvExceptionDuplicatedKey: CSV-Import LmsUserlist fand uneindeutigen Schlüssel
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: ELearning wird nicht automatisch gestartet, da die Zeitspanne für den Erneurerungszeitraum nicht festgelegt wurde! LmsErrorNoRefreshElearning: Fehler: ELearning wird nicht automatisch gestartet, da die Zeitspanne für den Erneurerungszeitraum nicht festgelegt wurde.
LmsErrorNoRenewElearning: Fehler: Erfoglreiches ELearning 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 +111,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: ELearning für gültige Inhaber (neu) starten
QualificationActStartELearningStatus l@QualificationShorthand n@Int m@Int: ELearning #{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 ELearning aktiv LmsInactive: Aktuell kein ELearning 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 ELearning 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 ELearning verlängert werden.
LmsRenewalReminder: Erinnerung LmsRenewalReminder: Erinnerung
LmsActNotify: Benachrichtigung ELearning erneut per Post oder E-Mail versenden LmsActNotify: Benachrichtigung ELearning erneut per Post oder E-Mail versenden
LmsActRenewPin: Neues zufällige ELearning Passwort zuweisen LmsActRenewPin: Neues zufällige ELearning Passwort zuweisen
@ -132,7 +124,7 @@ LmsActReset: ELearning Fehlversuche zurücksetzen und entsperren
LmsActResetInfo: ELearning 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: ELearning 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} ELearning Nutzer wurden alle Fehlversuche zurückgesetzt. LmsActResetFeedback n@Int m@Int: Für #{n}/#{m} ELearning Nutzer wurden alle Fehlversuche zurückgesetzt.
LmsActRestart: ELearning komplett neu starten LmsActRestart: ELearning komplett neu starten
LmsActRestartWarning: Das vorhandene ELearning 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 ELearning 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} ELearning Nutzer wurden komplett neu gestartet mit neuem Login und Passwort. LmsActRestartFeedback n@Int m@Int: #{n}/#{m} ELearning 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
@ -145,5 +137,7 @@ LmsNotificationSend n@Int: ELearning Benachrichtigungen an #{n} #{pluralDE n
LmsPinRenewal n@Int: ELearning Passwort ausgetauscht für #{n} #{pluralDE n "Prüfling" "Prüflinge"}. LmsPinRenewal n@Int: ELearning Passwort ausgetauscht für #{n} #{pluralDE n "Prüfling" "Prüflinge"}.
LmsActionFailed n@Int: Aktion nicht durchgeführt für #{n} #{pluralDE n "Person" "Personen"}, da diese derzeit nicht an einer Prüfung teilnehmen. LmsActionFailed n@Int: Aktion nicht durchgeführt für #{n} #{pluralDE n "Person" "Personen"}, da diese derzeit nicht an einer Prüfung teilnehmen.
LmsStarted: ELearning eröffnet LmsStarted: ELearning eröffnet
LmsAutomaticQueuing n@Natural: Die folgenden Funktionen werden normalerweise einmal pro Tag um #{show n} Uhr ausgeführt.
LmsManualQueuing: Die folgenden Funktionen sollten einmal pro Tag ausgeführt werden.
BtnLmsEnqueue: Nutzer mit ablaufenden Qualifikationen zum ELearning anmelden und benachrichtigen BtnLmsEnqueue: Nutzer mit ablaufenden Qualifikationen zum ELearning anmelden und benachrichtigen
BtnLmsDequeue: Nutzer mit beendetem ELearning aufräumen und ggf. benachrichtigen BtnLmsDequeue: Nutzer mit beendetem ELearning ggf. benachrichtigen und aufräumen

View File

@ -7,33 +7,25 @@ QualificationName: Qualification
QualificationDescription: Description QualificationDescription: Description
QualificationValidIndicator: Validity QualificationValidIndicator: Validity
QualificationValidDuration: Validity period QualificationValidDuration: Validity period
QualificationAuditDuration: Audit log retention period QualificationAuditDuration: Audit log keept
QualificationAuditDurationTooltip n@Int: Optional period for deletion of elearning data. Note that the elearning server may delete its anonymised data earlier, at most #{n} days after closing. QualificationAuditDurationTooltip n@Int: Optional period for deletion of elearning data. Note that the elearning server may delete its anonymised data earlier, at most #{n} days after closing.
QualificationAuditDurationReuseError: This qualification reuses the elearning 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 elearning is set to start automatically, it will be started and elearning credentials are send with this notification by post or email. QualificationRefreshWithinTooltip: Optional period before expiry to start elearning 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 elearning 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 elearning automatically started? QualificationElearningStart: Is elearning automatically started?
QualificationElearningRenew: Does successful elearning automatically extend a qualification by the default validity period?
QualificationElearningLimit: Is the number of elearning 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 TableQualificationIsAvsLicence: AVS Driving License
TableQualificationLmsReusesTooltip: This qualification reuses the elearning of the given qualification, instead of having a separate elearning of its own.
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?
@ -46,7 +38,6 @@ QualificationScheduleRenewalTooltip: Will there be a notification, if this quali
QualificationUserNoRenewal: Expires without further notification QualificationUserNoRenewal: Expires without further notification
QualificationUserNone: No registered qualifications for this person. QualificationUserNone: No registered qualifications for this person.
QualificationGrantReason: Reason for granting QualificationGrantReason: Reason for granting
QualificationRenewReason: Reason for renewal
QualificationBlockReason: Reason for revoking QualificationBlockReason: Reason for revoking
QualificationBlockNotify: Send notification QualificationBlockNotify: Send notification
QualificationBlockRemoveSupervisor: Remove all supervisors QualificationBlockRemoveSupervisor: Remove all supervisors
@ -57,8 +48,6 @@ TableLmsEmail: Email
TableLmsIdent: Elearning user TableLmsIdent: Elearning user
TableLmsPin: Elearning password TableLmsPin: Elearning password
TableLmsElearning: Elearning TableLmsElearning: Elearning
TableLmsElearningRenews: Automatic renewal
TableLmsElearningLimit: Max attempts
TableLmsResetPin: Reset Elearning password? TableLmsResetPin: Reset Elearning password?
TableLmsDatePin: Elearning password created TableLmsDatePin: Elearning password created
TableLmsDate: Date TableLmsDate: Date
@ -94,12 +83,17 @@ CsvColumnLmsResetTries: Reset number of used up elearning exam attempts
CsvColumnLmsDate: Date of elearning event CsvColumnLmsDate: Date of elearning event
CsvColumnLmsResult: LMS Status CsvColumnLmsResult: LMS Status
CsvColumnLmsLock: Elearning login is not permitted CsvColumnLmsLock: Elearning login is not permitted
LmsUserlistInsert: New LMS user
LmsUserlistUpdate: Update of LMS user
LmsResultInsert: New LMS result
LmsResultUpdate: Update of LMS result
LmsReportInsert: New LMS event LmsReportInsert: New LMS event
LmsReportUpdate: Update of LMS event LmsReportUpdate: Update of LMS event
LmsResultCsvExceptionDuplicatedKey: CSV import LmsResult with ambiguous key
LmsUserlistCsvExceptionDuplicatedKey: CSV import LmsUserlist with ambiguous key
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: Elearning will not be started automatically due to refresh-within time period not being set! LmsErrorNoRefreshElearning: Error: Elearning will not be started automatically due to refresh-within time period not being set.
LmsErrorNoRenewElearning: Error: Elearning 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 +111,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 elearning for valid qualification holders
QualificationActStartELearningStatus l n m: Elearning #{l} (re)started for #{n}/#{m} users. Note: It may take a while, until the elearning 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 elearning LmsInactive: Currently no active elearning
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 elearning 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 elearning only.
LmsRenewalReminder: Reminder LmsRenewalReminder: Reminder
LmsActNotify: Resend elearning notification by post or email LmsActNotify: Resend elearning notification by post or email
LmsActRenewPin: Randomly replace elearning password LmsActRenewPin: Randomly replace elearning password
@ -145,5 +137,7 @@ LmsNotificationSend n: Elearning notifications will be sent to #{n} #{pluralE
LmsPinRenewal n: Elearning password replaced randomly for #{n} #{pluralENs n "examinee"}. LmsPinRenewal n: Elearning password replaced randomly for #{n} #{pluralENs n "examinee"}.
LmsActionFailed n: No action for #{n} #{pluralENs n "person"}, since there was no ongoing examination. LmsActionFailed n: No action for #{n} #{pluralENs n "person"}, since there was no ongoing examination.
LmsStarted: Elearning open since LmsStarted: Elearning open since
BtnLmsEnqueue: Enqueue users with expiring qualifications for elearning and notify them LmsAutomaticQueuing n@Natural: The following functions are executed daily at #{show n} o'clock.
BtnLmsDequeue: Dequeue users with finished elearning and notify failed users LmsManualQueuing: The following functions should be executed daily.
BtnLmsEnqueue: Enqueue users with expiring qualifications for elearning and notify them.
BtnLmsDequeue: Dequeue users with finished elearning and notify, if appropriate.

View File

@ -103,4 +103,4 @@ MailSupervisorNoCopy: Warnung: Diese Nachricht wurde nicht an den eigentlichen E
MailSupervisedNote: Hinweis MailSupervisedNote: Hinweis
MailSupervisedBody: Eine Kopie dieser Nachricht wurde auch an folgende in FRADrive eingetragene Ansprechpartner gesendet: MailSupervisedBody: Eine Kopie dieser Nachricht wurde auch an folgende in FRADrive eingetragene Ansprechpartner gesendet:
MailSupervisorReroute: Benachrichtigungsumleitung MailSupervisorReroute: Benachrichtigungsumleitung
MailSupervisorRerouteTooltip: Alle Benachrichtigungen werden stattdessen an diese Ansprechpartner mit Benachrichtigungsumleitung gesandt MailSupervisorRerouteTooltip: Alle Benachrichtigungen werden stattdessen an alle Ansprechpartner mit Benachrichtigungsumleitung gesandt

View File

@ -103,4 +103,4 @@ MailSupervisorNoCopy: Warning: This message was not sent to the original recipie
MailSupervisedNote: Please note MailSupervisedNote: Please note
MailSupervisedBody: A copy of this message has been sent to all supervisors registered for you in FRADrive, namely: MailSupervisedBody: A copy of this message has been sent to all supervisors registered for you in FRADrive, namely:
MailSupervisorReroute: Reroute notifications MailSupervisorReroute: Reroute notifications
MailSupervisorRerouteTooltip: All notification will be rerouted to these supervisors instead MailSupervisorRerouteTooltip: All notification will be sent to all supervisors with notification rerouting instead

View File

@ -37,8 +37,7 @@ PDFPassword: Passwort zur Verschlüsselung von PDF Anhängen an Email Benachrich
PDFPasswordTip: Achtung, dieses Passwort ist für FRADrive Administratoren einsehbar und wird unverschlüsselt gespeichert! PDFPasswordTip: Achtung, dieses Passwort ist für FRADrive Administratoren einsehbar und wird unverschlüsselt gespeichert!
PDFPasswordInvalid c@Char: Bitte ein nicht-triviales Passwort für PDF Email Anhänge eintragen! Ungültiges Zeichen: #{char2Text c} PDFPasswordInvalid c@Char: Bitte ein nicht-triviales Passwort für PDF Email Anhänge eintragen! Ungültiges Zeichen: #{char2Text c}
PDFPasswordTooShort n@Int: Bitte ein PDF Passwort mit mindestens #{show n} Zeichen wählen oder Post-Versand aktivieren PDFPasswordTooShort n@Int: Bitte ein PDF Passwort mit mindestens #{show n} Zeichen wählen oder Post-Versand aktivieren
PrefersPostal: Bevorzugte Benachrichtigung PrefersPostal: Sollen Benachrichtigung möglichst per Post versendet werden anstatt per Email?
PrefersPostalExp: Sollen Benachrichtigung möglichst per Post versendet werden anstatt per Email?
PostalTip: Postversand kann in Rechnung gestellt werden und ist derzeit nur für Benachrichtigungen über Erneuerung und Ablauf von Qualifikation, wie z.B. Führerscheine, verfügbar. PostalTip: Postversand kann in Rechnung gestellt werden und ist derzeit nur für Benachrichtigungen über Erneuerung und Ablauf von Qualifikation, wie z.B. Führerscheine, verfügbar.
PostAddress: Postalische Adresse PostAddress: Postalische Adresse
PostAddressTip: Mindestens eine Zeile mit Straße und Hausnummer und eine Zeile mit Postleitzahl und Ort. Kein Empfängername, denn dieser wird später automatisch hinzugefügt. PostAddressTip: Mindestens eine Zeile mit Straße und Hausnummer und eine Zeile mit Postleitzahl und Ort. Kein Empfängername, denn dieser wird später automatisch hinzugefügt.

View File

@ -37,8 +37,7 @@ PDFPassword: Password to lock PDF email attachments
PDFPasswordTip: Please note that this password is displayed to FRADrive admins and is saved unencrypted PDFPasswordTip: Please note that this password is displayed to FRADrive admins and is saved unencrypted
PDFPasswordInvalid c: Please supply a sensible password for encrypting PDF email attachments! Invalid character #{char2Text c} PDFPasswordInvalid c: Please supply a sensible password for encrypting PDF email attachments! Invalid character #{char2Text c}
PDFPasswordTooShort n: Please provide a password with at least #{show n} characters or choose postal mail PDFPasswordTooShort n: Please provide a password with at least #{show n} characters or choose postal mail
PrefersPostal: Notification preference PrefersPostal: Should notifications preferably send by post instead of email?
PrefersPostalExp: Should notifications preferably send by post instead of email?
PostalTip: Mailing may incur a fee and is currently only avaulable for qualification expiry notifications, such as driving lincence renewal. PostalTip: Mailing may incur a fee and is currently only avaulable for qualification expiry notifications, such as driving lincence renewal.
PostAddress: Postal address PostAddress: Postal address
PostAddressTip: Should contain at least one line with street and house number and another line featuring zip code and town. Omit a recipient name, since it will be added later. PostAddressTip: Should contain at least one line with street and house number and another line featuring zip code and town. Omit a recipient name, since it will be added later.

View File

@ -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

View File

@ -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

View File

@ -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! SynchroniseLdapUserQueued n@Int: LDAP-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! SynchroniseLdapAllUsersQueued: LDAP-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
@ -91,19 +89,12 @@ 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 AuthKindLDAP: Fraport AG Kennung
AuthKindPWHash: FRADrive Kennung AuthKindPWHash: FRADrive Kennung
AuthKindNoLogin: Kein Login möglich AuthKindNoLogin: Kein Login möglich
@ -111,9 +102,3 @@ 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
UserCompanyReasonTooltip: Optionale Notiz für besondere Fälle. Kann ggf. autmatische Entfernung bei AVS Firmenwechsel verhindern.
UserSupervisorReason: Begründung Ansprechpartner
UserSupervisorReasonTooltip: Optionale Notiz für besondere Fälle. Kann ggf. autmatische Entfernung bei AVS Firmenwechsel verhindern.
AdminUserAllNotifications: Alle Benachrichtigungen and diesen Benutzer

View File

@ -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. SynchroniseLdapUserQueued n: Triggered LDAP 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. SynchroniseLdapAllUsersQueued: Triggered LDAP 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
@ -91,29 +89,16 @@ 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 AuthKindLDAP: Fraport AG account
AuthKindPWHash: FRADrive account AuthKindPWHash: FRADrive account
AuthKindNoLogin: No login 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
UserCompanyReasonTooltip: Optional note for special cases. In some case this may prevent automatic removel upon AVS user company changes.
UserSupervisorReason: Reason for supervision
UserSupervisorReasonTooltip: Optional note for special cases. In some case this may prevent automatic removel upon AVS user company changes.
AdminUserAllNotifications: All notification sent to this user

View File

@ -4,31 +4,23 @@
#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
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

View File

@ -4,31 +4,23 @@
#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
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

View File

@ -23,7 +23,6 @@ MenuPayments: Zahlungsbedingungen
MenuInstance: Instanz-Identifikation MenuInstance: Instanz-Identifikation
MenuHealth: Instanz-Zustand MenuHealth: Instanz-Zustand
MenuHealthInterface: Schnittstellen Zustand
MenuHelp: Hilfe MenuHelp: Hilfe
MenuProfile: Anpassen MenuProfile: Anpassen
MenuLogin !ident-ok: Login MenuLogin !ident-ok: Login
@ -125,6 +124,8 @@ MenuLmsUser: Benutzerqualifikationen
MenuLmsUserSchool: Bereichs Benutzerqualifikationen MenuLmsUserSchool: Bereichs Benutzerqualifikationen
MenuLmsUserAll: Alle Benutzerqualifikationen MenuLmsUserAll: Alle Benutzerqualifikationen
MenuLmsUsers: Veralteter Export ELearning Benutzer MenuLmsUsers: Veralteter Export ELearning Benutzer
MenuLmsUserlist: Veraltetes Melden ELearning Benutzer
MenuLmsResult: Veralteter Melden Ergebnisse ELearning
MenuLmsUpload: Hochladen MenuLmsUpload: Hochladen
MenuLmsDirectUpload: Direkter Upload MenuLmsDirectUpload: Direkter Upload
MenuLmsDirectDownload: Direkter Download MenuLmsDirectDownload: Direkter Download
@ -132,28 +133,14 @@ MenuLmsFake: Testnutzer generieren
MenuLmsLearners: Export Benutzer ELearning MenuLmsLearners: Export Benutzer ELearning
MenuLmsReport: Ergebnisse ELearning MenuLmsReport: Ergebnisse ELearning
MenuFirms: Firmen
MenuFirmUsers: Angehörige
MenuFirmSupervisors: Ansprechpartner
MenuFirmsComm: Mitteilung
MenuInterfaces: Schnittstellen
MenuSap: SAP Schnittstelle MenuSap: SAP Schnittstelle
MenuAvs: AVS Schnittstelle MenuAvs: AVS Schnittstelle
MenuAvsSynchError: AVS Problemübersicht MenuAvsSynchError: AVS Problemübersicht
MenuLdap: LDAP Schnittstelle MenuLdap: LDAP Schnittstelle
MenuApc: Druck MenuApc: Druckerei
MenuPrintSend: Manueller Briefversand MenuPrintSend: Manueller Briefversand
MenuPrintDownload: Brief herunterladen MenuPrintDownload: Brief herunterladen
MenuPrintLog: LPR Schnittstelle
MenuPrintAck: Druckbestätigung
MenuCommCenter: Benachrichtigungen
MenuMailCenter: EMails
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)

View File

@ -23,7 +23,6 @@ MenuPayments: Payment Terms
MenuInstance: Instance identification MenuInstance: Instance identification
MenuHealth: Instance health MenuHealth: Instance health
MenuHealthInterface: Interface health
MenuHelp: Support MenuHelp: Support
MenuProfile: Settings MenuProfile: Settings
MenuLogin: Login MenuLogin: Login
@ -71,6 +70,7 @@ MenuCourseDelete: Delete course
MenuSubmissionNew: Create submission MenuSubmissionNew: Create submission
MenuSubmissionOwn: Submission MenuSubmissionOwn: Submission
MenuCorrectors: Correctors MenuCorrectors: Correctors
MenuSheetEdit: Edit exercise sheet MenuSheetEdit: Edit exercise sheet
MenuSheetDelete: Delete exercise sheet MenuSheetDelete: Delete exercise sheet
MenuSheetClone: Clone exercise sheet MenuSheetClone: Clone exercise sheet
@ -125,6 +125,8 @@ MenuLmsUser: User Qualifications
MenuLmsUserSchool: Institute User Qualifications MenuLmsUserSchool: Institute User Qualifications
MenuLmsUserAll: All User Qualifications MenuLmsUserAll: All User Qualifications
MenuLmsUsers: Legacy download elearning users MenuLmsUsers: Legacy download elearning users
MenuLmsUserlist: Legacy upload elearning users
MenuLmsResult: Legacy upload rlearning results
MenuLmsUpload: Upload MenuLmsUpload: Upload
MenuLmsDirectUpload: Direct Upload MenuLmsDirectUpload: Direct Upload
MenuLmsDirectDownload: Direct Download MenuLmsDirectDownload: Direct Download
@ -132,28 +134,14 @@ MenuLmsFake: Generate Test Users
MenuLmsLearners: Elearning Users MenuLmsLearners: Elearning Users
MenuLmsReport: Elearning Results MenuLmsReport: Elearning Results
MenuFirms: Companies
MenuFirmUsers: Associates
MenuFirmSupervisors: Supervisors
MenuFirmsComm: Messaging
MenuInterfaces: Interfaces
MenuSap: SAP Interface MenuSap: SAP Interface
MenuAvs: AVS Interface MenuAvs: AVS Interface
MenuAvsSynchError: AVS Problem Overview MenuAvsSynchError: AVS Problem Overview
MenuLdap: LDAP Interface MenuLdap: LDAP Interface
MenuApc: Print MenuApc: Printing
MenuPrintSend: Send Letter MenuPrintSend: Send Letter
MenuPrintDownload: Download Letter MenuPrintDownload: Download Letter
MenuPrintLog: LPR Interface
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)

View File

@ -16,7 +16,7 @@ TableTerm !ident-ok: Jahr
TableCourseSchool: Bereich TableCourseSchool: Bereich
TableSubmissionGroup: Feste Abgabegruppe TableSubmissionGroup: Feste Abgabegruppe
TableNoSubmissionGroup: Keine feste Abgabegruppe TableNoSubmissionGroup: Keine feste Abgabegruppe
TableMatrikelNr: AVS Personennummer TableMatrikelNr: AVS Nr
TableSex: Geschlecht TableSex: Geschlecht
TableBirthday: Geburtsdatum TableBirthday: Geburtsdatum
TableSchool: Bereich TableSchool: Bereich
@ -73,33 +73,11 @@ 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
TableCompanyShort: Firmenkürzel
TableCompanies: Firmen TableCompanies: Firmen
TablePrimeCompany: Primäre Firma
TableCompanyNo: Firmennummer
TableCompanyNos: Firmennummern TableCompanyNos: Firmennummern
TableCompanyUser: Firmenangehöriger
TableCompanyNrUsers: Firmenangehörige
TableCompanyNrSecondaryUsers: Sekundäre Firmenangehörige
TableCompanyReason: Notiz
TableCompanyNrSupers: Ansprechpartner
TableCompanyNrEmpSupervised: Firmenangehörige mit Ansprechpartner
TableCompanyNrEmpRerouted: Firmenangehörige mit Umleitung
TableCompanyNrEmpRerPost: Firmenangehörige mit postalischer Umleitung
TableCompanyNrSupersActive: Mitarbeiter mit Ansprechpartner
TableCompanyNrSupersDefault: Standard Ansprechpartner
TableCompanyNrForeignSupers: Firmenfremde Ansprechpartner
TableCompanyNrRerouteDefault: Standard Umleitungen
TableCompanyNrRerouteActive: Aktive Umleitungen
TableRerouteActive: Umleitung
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,12 +85,6 @@ 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.
TableFilterCommaNameNr: Mehrere Namen oder Nummern mit Komma trennen. Nummern werden nur exakt gesucht.
TableUserEdit: Benutzer bearbeiten
TableRows: Zeilen

View File

@ -16,7 +16,7 @@ TableTerm: Year
TableCourseSchool: Department TableCourseSchool: Department
TableSubmissionGroup: Registered submission group TableSubmissionGroup: Registered submission group
TableNoSubmissionGroup: No registered submission group TableNoSubmissionGroup: No registered submission group
TableMatrikelNr: AVS person no TableMatrikelNr: AVS No
TableSex: Sex TableSex: Sex
TableBirthday: Birthday TableBirthday: Birthday
TableSchool: Department TableSchool: Department
@ -73,33 +73,11 @@ 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
TableCompanyShort: Company shorthand
TableCompanies: Companies TableCompanies: Companies
TablePrimeCompany: Primary company
TableCompanyNo: Company number
TableCompanyNos: Company numbers TableCompanyNos: Company numbers
TableCompanyUser: Associate
TableCompanyNrUsers: Associates
TableCompanyNrSecondaryUsers: Secondary Associates
TableCompanyReason: Note
TableCompanyNrSupers: Supervisors
TableCompanyNrEmpSupervised: Supervised employees
TableCompanyNrEmpRerouted: Employees having reroute
TableCompanyNrEmpRerPost: Employees having postal reroute
TableCompanyNrSupersActive: Associates having supervisors
TableCompanyNrSupersDefault: Default supervisors
TableCompanyNrForeignSupers: External Supervisors
TableCompanyNrRerouteDefault: Default reroutes
TableCompanyNrRerouteActive: Active reroutes
TableRerouteActive: Reroute
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,12 +85,6 @@ 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.
TableFilterCommaNameNr: Separate names and numbers by comma. Numbers have to match exact.
TableUserEdit: Edit user
TableRows: Rows

View File

@ -13,19 +13,15 @@ RGCourseUnacceptedApplicants: Nicht akzeptierte Bewerber:innen
RecipientToggleAll: Alle/Keine RecipientToggleAll: Alle/Keine
CommCourseTestSubject customSubject@Text !ident-ok: [TEST] #{customSubject} CommCourseTestSubject customSubject@Text !ident-ok: [TEST] #{customSubject}
UtilCommCourseSubject: Kursartmitteilung UtilCommCourseSubject: Kursartmitteilung
UtilCommFirmSubject: Firmenmitteilung
CommRecipients: Empfänger:innen CommRecipients: Empfänger:innen
CommRecipientsTip: Sie selbst erhalten immer eine Kopie der Nachricht CommRecipientsTip: Sie selbst erhalten immer eine Kopie der Nachricht
CommRecipientsList: Die an Sie selbst verschickte Kopie der Nachricht wird, zu Archivierungszwecken, eine vollständige Liste aller Empfänger:innen enthalten. Die Empfängerliste wird im CSV-Format an die E-Mail angehängt. Andere Empfänger:innen erhalten die Liste nicht. Bitte entfernen Sie dementsprechend den Anhang bevor Sie die E-Mail weiterleiten oder anderweitig mit Dritten teilen. CommRecipientsList: Die an Sie selbst verschickte Kopie der Nachricht wird, zu Archivierungszwecken, eine vollständige Liste aller Empfänger:innen enthalten. Die Empfängerliste wird im CSV-Format an die E-Mail angehängt. Andere Empfänger:innen erhalten die Liste nicht. Bitte entfernen Sie dementsprechend den Anhang bevor Sie die E-Mail weiterleiten oder anderweitig mit Dritten teilen.
UtilEMail: E-Mail UtilEMail: E-Mail
UtilPostal: Brief
UtilUnchanged: Nicht verändern
UtilMultiEmailFieldTip: Es sind mehrere, Komma-separierte, E-Mail-Adressen möglich UtilMultiEmailFieldTip: Es sind mehrere, Komma-separierte, E-Mail-Adressen möglich
RGTutorialParticipants tutn@TutorialName: Kursteilnehmer:innen (#{tutn}) 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 +79,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,10 +93,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
UtilEmptyNoChangeTip: Eine leere Eingabe belässt den vorherigen Wert unverändert.
MultiNoSelection: Keine Auswahl
#invitation.hs #invitation.hs
InvitationAction: Aktion InvitationAction: Aktion

View File

@ -13,19 +13,15 @@ RGCourseUnacceptedApplicants: Applicants not accepted
RecipientToggleAll: All/None RecipientToggleAll: All/None
CommCourseTestSubject customSubject: [TEST] #{customSubject} CommCourseTestSubject customSubject: [TEST] #{customSubject}
UtilCommCourseSubject: Course type message UtilCommCourseSubject: Course type message
UtilCommFirmSubject: Company message
CommRecipients: Recipients CommRecipients: Recipients
CommRecipientsTip: You always receive a copy of the message CommRecipientsTip: You always receive a copy of the message
CommRecipientsList: For archival purposes the copy of the message sent to you will contain a complete list of all recipients. The list of recipients will be attached to the email in CSV-format. Other recipients do not receive the list. Thus, please remove the attachment before you forward the email or otherwise share it with third parties. CommRecipientsList: For archival purposes the copy of the message sent to you will contain a complete list of all recipients. The list of recipients will be attached to the email in CSV-format. Other recipients do not receive the list. Thus, please remove the attachment before you forward the email or otherwise share it with third parties.
UtilEMail: Email UtilEMail: Email
UtilPostal: Postal
UtilUnchanged: No change
UtilMultiEmailFieldTip: Multiple emails addresses may be specified (comma-separated) UtilMultiEmailFieldTip: Multiple emails addresses may be specified (comma-separated)
RGTutorialParticipants tutn: Course participants (#{tutn}) 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 +79,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,10 +93,6 @@ RoomReferenceLinkLink: Link
RoomReferenceLinkLinkPlaceholder: URL RoomReferenceLinkLinkPlaceholder: URL
RoomReferenceLinkInstructions: Instructions RoomReferenceLinkInstructions: Instructions
RoomReferenceLinkInstructionsPlaceholder: Instructions RoomReferenceLinkInstructionsPlaceholder: Instructions
UtilNoneSet: None set
UtilEmptyChoice: Empty selection
UtilEmptyNoChangeTip: Existing values remain unchanged if this field is left empty.
MultiNoSelection: No selection
#invitation.hs #invitation.hs
InvitationAction: Action InvitationAction: Action

View File

@ -1,4 +1,4 @@
-- SPDX-FileCopyrightText: 2022-24 Gregor Kleen <gregor.kleen@ifi.lmu.de>,Steffen Jost <s.jost@fraport.de> -- SPDX-FileCopyrightText: 2022 Gregor Kleen <gregor.kleen@ifi.lmu.de>
-- --
-- SPDX-License-Identifier: AGPL-3.0-or-later -- SPDX-License-Identifier: AGPL-3.0-or-later
@ -8,31 +8,5 @@ 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
InterfaceLog
interface Text
subtype Text
write Bool -- requestMethod /= GET, i.e. True implies a write to FRADrive
time UTCTime
rows Int Maybe -- number of datasets transmitted
info Text -- addtional status information
success Bool default=true -- false logs a failure; but it will be overwritten by next transaction, but logged in TransactionLog
UniqueInterfaceSubtypeWrite interface subtype write
deriving Eq Read Show Generic
InterfaceHealth
interface Text
subtype Text Maybe
write Bool Maybe
hours Int -- negative number: never expires, i.e. if the last entry is a success, this remains indefinitely
UniqueInterfaceHealth interface subtype write !force -- Note that nullable fields must be either empty or unique
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 deriving Eq Read Show Generic

View File

@ -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

View File

@ -1,18 +1,24 @@
-- 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 UniqueCompanyName name
-- UniqueCompanyName name -- Should be Unique in AVS, but we do not yet need to enforce it UniqueCompanyShorthand shorthand
-- UniqueCompanyShorthand shorthand -- unnecessary, since it is the primary key already -- UniqueCompanyAvsId avsId -- should be the case, unclear if enforcing works here, since we cannot query avs by company id
UniqueCompanyAvsId avsId -- Should be the key, is not for historical reasons and for convenience in URLs and columns 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

View File

@ -3,6 +3,7 @@
-- SPDX-License-Identifier: AGPL-3.0-or-later -- SPDX-License-Identifier: AGPL-3.0-or-later
-- Jobs to be executed as soon as possible in the background (so not to delay HTTP-responses, or triggered by cron-system without associated HTTP-Request) -- Jobs to be executed as soon as possible in the background (so not to delay HTTP-responses, or triggered by cron-system without associated HTTP-Request)
-- part of the *old* job system
QueuedJob QueuedJob
content Value -- JSON-encoded description of the work to be done (send an email to "test@example.org", find all recipients for a certain notifications and queue one new job each, distribute all submissions for a sheet to correctors, ...) content Value -- JSON-encoded description of the work to be done (send an email to "test@example.org", find all recipients for a certain notifications and queue one new job each, distribute all submissions for a sheet to correctors, ...)
creationInstance InstanceId -- multiple uni2work-instances access the same database, record which instance created this job for debugging purposes creationInstance InstanceId -- multiple uni2work-instances access the same database, record which instance created this job for debugging purposes
@ -15,16 +16,18 @@ QueuedJob
-- Jobs are deleted from @QueuedJob@ after they are executed successfully and recorded in @CronLastExec@ -- Jobs are deleted from @QueuedJob@ after they are executed successfully and recorded in @CronLastExec@
-- There is a Cron-system that, at set intervals, queries the database for work to be done in the background (i.e. if a lecturer has set a sheet's submissions to be automatically distributed and the submission deadline passed since the last check, then queue a new job to actually do the distribution) -- There is a Cron-system that, at set intervals, queries the database for work to be done in the background (i.e. if a lecturer has set a sheet's submissions to be automatically distributed and the submission deadline passed since the last check, then queue a new job to actually do the distribution)
-- For the cron-system to determine whether a job needs to be done it needs to know if and when it was last (or ever) executed (i.e. a sheet's submissions should not be distributed twice) -- For the cron-system to determine whether a job needs to be done it needs to know if and when it was last (or ever) executed (i.e. a sheet's submissions should not be distributed twice)
-- part of the *old* job system
CronLastExec CronLastExec
job Value -- JSON-encoded description of work done job Value -- JSON-encoded description of work done
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
-- part of the *old* job system
TokenBucket TokenBucket
ident TokenBucketIdent ident TokenBucketIdent
lastValue Int64 lastValue Int64
lastAccess UTCTime lastAccess UTCTime
Primary ident Primary ident
deriving Generic Show deriving Generic

48
models/jobsystem.model Normal file
View File

@ -0,0 +1,48 @@
-- Metadata of jobs to be fetched by the scheduler and to be
-- assigned to the workers
-- part of the *new* job system
JobMeta
occurrence JobOccurrence -- When should the job be executed?
-- (Possibly recurring)
relativeInTime TimeRelative Maybe -- TODO find better name
planExecutionAhead NominalDiffTime -- TODO find better name
executionTimeEpsilon NominalDiffTime Maybe
priority JobPrio
ressources JobRessources
status JobStatus
lastStartSystem UTCTime Maybe -- when did a job scheduler last order
-- a job worker to execute this job?
-- when a job is to be executed now,
-- lastStartWorker and lastFinished
-- are unset
lastStartWorker UTCTime Maybe -- when did a job worker last start
-- executing this job?
-- when no time is set, the job either
-- was never executed yet or execution
-- was just ordered (or something went
-- very wrong within the jobsystem)
lastFinished UTCTime Maybe -- when did a job worker last report
-- successful execution?
-- all "last..." fields should be set by the scheduler atomically!
retryAfter NominalDiffTime Maybe -- if set, the scheduler checks on
-- the job after this time and retries
-- the job iff lastStartWorker is set,
-- but lastFinished is unset
-- Ensure that retryAfter is sufficient
-- for this job and application, so better
-- set to higher values until we know this
-- job is definitely dead after execution
-- (pun intended)
isSystemJob Bool -- new system jobs are to be scheduled, old system jobs to be deleted on migration
appVersion AppVersion
deriving Eq Ord Show Generic
-- Actual job to be fetched and executed by the workers
-- part of the *new* job system
JobData
meta JobMetaId
content Value -- JSON representation of the job content
-- (parsing as generic Aeson Value
-- instead of Job for compatibility with
-- legacy jobs)
deriving Eq Ord Show Generic

View File

@ -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
-- 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?
@ -42,20 +40,19 @@ Qualification
-- - PinReset==1 mit bestehendem Passwort kann problemlos erneut gesendet werden -- - PinReset==1 mit bestehendem Passwort kann problemlos erneut gesendet werden
-- - 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 Show
-- TODO: connect Qualification with Exams! -- TODO: connect Qualification with Exams!
@ -63,7 +60,7 @@ 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 +69,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
@ -98,20 +95,25 @@ QualificationUserBlock
-- - delete-flag: isJust LmsUserStatus -- - delete-flag: isJust LmsUserStatus
-- Note: REST means that LmsUserResetPin and LmsUserDelete remain unchanged by this GET request! -- Note: REST means that LmsUserResetPin and LmsUserDelete remain unchanged by this GET request!
-- --
-- 3. REST POST Report.csv: just save as is to LmsReport for later processing -- 3. REST POST Userlist.csv: just save as is to LmsUserlist
-- --
-- 4. When received: Job LmsReport: -- Note: containment needs at-once processing -- 4. REST POST Ergebnisse.csv: just save as is to LmsResult
--
-- 5. When received: Job LmsUserlist: -- Note: containment needs at-once processing
-- - For all LmsUser: -- - For all LmsUser:
-- + if contained: -- + if contained:
-- set LmsUserReceived to Just now() -- set LmsUserReceived to Just now()
-- if Failed: set LmsUserStatus to Just LmsBlocked now -- if LmsUserlistFailed: set LmsUserStatus to Just LmsBlocked now
-- if Success: set LmsUserStatus to Just LmsSuccess now
-- and renew QualificationValidTo
-- + not contained, by LmsUserReceived is set: set LmsUserEnded to Just now() -- + not contained, by LmsUserReceived is set: set LmsUserEnded to Just now()
-- - move row to LmsAudit -- - move row to LmsAudit
-- --
-- 5. Daily Job: dequeue LMS Users -- 6. When received: Daily Job LmsResult:
-- - fail and mark expired LmsUser -- - set LmsUserReceived to Just now() -- always
-- - set LmsUserStatus to Just LmsSuccess now -- conditional
-- - and renew QualificationValidTo
-- - move row to LmsAudit
--
-- 7. Daily Job: dequeue LMS Users
-- - remove from LmsUser after audit Period has passed -- - remove from LmsUser after audit Period has passed
LmsUser LmsUser
@ -133,7 +135,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
@ -142,7 +144,24 @@ LmsUser
-- UniqueLmsUserStatus lmsUser -- enforcing uniqueness prohibits history -- UniqueLmsUserStatus lmsUser -- enforcing uniqueness prohibits history
-- deriving Generic -- deriving Generic
-- V2 Stores LMS upload for processing in Background Job -- LmsUserlist stores LMS upload for later processing only
LmsUserlist
qualification QualificationId OnDeleteCascade OnUpdateCascade
ident LmsIdent
failed Bool
timestamp UTCTime default=now()
UniqueLmsUserlist qualification ident
deriving Generic Show
-- LmsResult stores LMS upload for later processing only
LmsResult
qualification QualificationId OnDeleteCascade OnUpdateCascade
ident LmsIdent
success Day -- BEWARE: timezone is local as submitted by LMS
timestamp UTCTime default=now()
UniqueLmsResult qualification ident -- required by DBTable
deriving Generic
LmsReport LmsReport
qualification QualificationId OnDeleteCascade OnUpdateCascade qualification QualificationId OnDeleteCascade OnUpdateCascade
ident LmsIdent ident LmsIdent
@ -151,16 +170,4 @@ 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
-- due to frequent transmit errors, a separate lms tranmission log is necessary again
LmsReportLog
qualification QualificationId OnDeleteCascade OnUpdateCascade
ident LmsIdent
date UTCTime Maybe -- BEWARE: timezone is local as submitted by LMS
result LmsState -- (0|1|2) 0=LmsFailed[too many tries], 1=LmsOpen, 2=LmsPassed[success]
lock Bool -- (0|1)
timestamp UTCTime default=now()
missing Bool default=false
deriving Generic Show

View File

@ -9,24 +9,23 @@ PrintJob
file ByteString -- stores plain pdf; otherwise use FileContentReference Maybe file ByteString -- stores plain pdf; otherwise use FileContentReference Maybe
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 OnDeleteCascade 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 OnDeleteCascade 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

View File

@ -10,8 +10,8 @@ School json
examMinimumRegisterBeforeStart NominalDiffTime Maybe examMinimumRegisterBeforeStart NominalDiffTime Maybe
examMinimumRegisterDuration NominalDiffTime Maybe examMinimumRegisterDuration NominalDiffTime Maybe
examRequireModeForRegistration Bool default=false examRequireModeForRegistration Bool default=false
examDiscouragedModes ExamModeDNF examDiscouragedModes ExamModeDNF default='{"dnf-terms":[]}' -- This comment fixes syntax highlighting error only "
examCloseMode ExamCloseMode default='separate' examCloseMode ExamCloseMode default='separate'
sheetAuthorshipStatementMode SchoolAuthorshipStatementMode default='optional' sheetAuthorshipStatementMode SchoolAuthorshipStatementMode default='optional'
sheetAuthorshipStatementDefinition AuthorshipStatementDefinitionId Maybe sheetAuthorshipStatementDefinition AuthorshipStatementDefinitionId Maybe
sheetAuthorshipStatementAllowOther Bool default=true sheetAuthorshipStatementAllowOther Bool default=true

View File

@ -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 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
@ -14,14 +14,14 @@
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.
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 ident UserIdent -- Case-insensitive user-identifier
authentication AuthenticationMode -- 'AuthLDAP' or ('AuthPWHash'+password-hash) authentication AuthenticationMode -- 'AuthLDAP' or ('AuthPWHash'+password-hash)
lastAuthentication UTCTime Maybe -- last login date lastAuthentication UTCTime Maybe -- last login date
created UTCTime default=now() created UTCTime default=now()
lastLdapSynchronisation UTCTime Maybe lastLdapSynchronisation UTCTime Maybe
ldapPrimaryKey UserEduPersonPrincipalName Maybe -- Fraport Personnel Number or Email-Prefix for @fraport.de work here ldapPrimaryKey UserEduPersonPrincipalName Maybe
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
@ -34,7 +34,7 @@ User json -- Each Uni2work user has a corresponding row in this table; create
timeFormat DateTimeFormat "default='%R'" -- preferred Time-only display format for user; user-defined timeFormat DateTimeFormat "default='%R'" -- preferred Time-only display format for user; user-defined
downloadFiles Bool default=false -- Should files be opened in browser or downloaded? (users often oblivious that their browser has a setting for this) downloadFiles Bool default=false -- Should files be opened in browser or downloaded? (users often oblivious that their browser has a setting for this)
languages Languages Maybe -- Preferred language; user-defined languages Languages Maybe -- Preferred language; user-defined
notificationSettings NotificationSettings "default='{}'::jsonb" -- Bit-array for which events email notifications are requested by user; user-defined; missing fields in json object will be parsed to default trigger notificationSettings NotificationSettings "default='{}'::jsonb" -- Bit-array for which events email notifications are requested by user; user-defined
warningDays NominalDiffTime default=1209600 -- timedistance to pending deadlines for homepage infos warningDays NominalDiffTime default=1209600 -- timedistance to pending deadlines for homepage infos
csvOptions CsvOptions "default='{}'::jsonb" csvOptions CsvOptions "default='{}'::jsonb"
sex Sex Maybe -- currently ignored sex Sex Maybe -- currently ignored
@ -44,9 +44,9 @@ 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
@ -61,47 +61,42 @@ UserFunction -- Administratively assigned functions (lecturer, admin, evaluation
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

View File

@ -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
; ;

View File

@ -1,3 +1,3 @@
{ {
"version": "27.4.79" "version": "27.4.45"
} }

2
package-lock.json generated
View File

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

View File

@ -1,6 +1,6 @@
{ {
"name": "uni2work", "name": "uni2work",
"version": "27.4.79", "version": "27.4.45",
"description": "", "description": "",
"keywords": [], "keywords": [],
"author": "", "author": "",

View File

@ -1,5 +1,5 @@
name: uniworx name: uniworx
version: 27.4.79 version: 27.4.45
dependencies: dependencies:
- base - base
- yesod - yesod
@ -186,6 +186,10 @@ dependencies:
- haskell-src-meta - haskell-src-meta
- either - either
- xlsx - xlsx
- jobsys
- occurrence
- segmented
- haskell-nf
other-extensions: other-extensions:
- GeneralizedNewtypeDeriving - GeneralizedNewtypeDeriving
- IncoherentInstances - IncoherentInstances
@ -259,7 +263,6 @@ ghc-options:
- -j - -j
- -freduction-depth=0 - -freduction-depth=0
- -fprof-auto-calls - -fprof-auto-calls
- -g
when: when:
- condition: flag(pedantic) - condition: flag(pedantic)
ghc-options: ghc-options:

73
routes
View File

@ -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 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
@ -52,9 +52,9 @@
/ 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
@ -68,44 +68,35 @@
/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/ldap AdminLdapR 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
/print/acknowledge/direct PrintAckDirectR GET POST !system-printer /print/acknowledge/direct PrintAckDirectR POST !system-printer
/print/send PrintSendR GET POST /print/send PrintSendR GET POST
/print/download/#CryptoUUIDPrintJob PrintDownloadR GET !system-printer /print/download/#CryptoUUIDPrintJob PrintDownloadR GET !system-printer
/print/log PrintLogR GET !system-printer
/health HealthR GET !free /health HealthR GET !free
/health/interface/+Texts HealthInterfaceR GET !free /instance InstanceR GET !free
/instance InstanceR GET !free /info InfoR GET !free
/info InfoR GET !free /info/lecturer InfoLecturerR GET !free
/info/lecturer InfoLecturerR GET !free /info/supervisor InfoSupervisorR GET !free
/info/supervisor InfoSupervisorR GET !free /info/legal LegalR GET !free
/info/legal LegalR GET !free /info/glossary GlossaryR GET !free
/info/glossary GlossaryR GET !free /info/faq FaqR GET !free
/info/faq FaqR GET !free /info/terms-of-use TermsOfUseR GET !free
/info/terms-of-use TermsOfUseR GET !free /info/payments PaymentsR GET !free
/info/payments PaymentsR GET !free /imprint ImprintR GET !free
/imprint ImprintR GET !free /data-protection DataProtectionR GET !free
/data-protection DataProtectionR GET !free /version VersionR GET !free
/version VersionR GET !free /status StatusR GET !free
/status StatusR GET !free
/help HelpR GET POST !free /help HelpR GET POST !free
@ -122,11 +113,6 @@
/for/#CryptoUUIDUser/user ForProfileR GET POST !supervisor !self /for/#CryptoUUIDUser/user ForProfileR GET POST !supervisor !self
/for/#CryptoUUIDUser/user/profile ForProfileDataR GET !supervisor !self /for/#CryptoUUIDUser/user/profile ForProfileDataR GET !supervisor !self
/firms FirmAllR GET POST -- not yet !supervisor
/firms/comm/+Companies FirmsCommR GET POST
/firm/#CompanyShorthand/comm FirmCommR GET POST
/firm/#CompanyShorthand FirmUsersR GET POST -- not yet !supervisor
/firm/#CompanyShorthand/supers FirmSupersR GET POST -- not yet !supervisor
/exam-office ExamOfficeR !exam-office: /exam-office ExamOfficeR !exam-office:
/ EOExamsR GET POST !system-exam-office / EOExamsR GET POST !system-exam-office
@ -288,13 +274,22 @@
/lms/#SchoolId LmsSchoolR GET /lms/#SchoolId LmsSchoolR GET
/lms/#SchoolId/#QualificationShorthand LmsR GET POST /lms/#SchoolId/#QualificationShorthand LmsR GET POST
/lms/#SchoolId/#QualificationShorthand/edit LmsEditR GET POST /lms/#SchoolId/#QualificationShorthand/edit LmsEditR GET POST
-- old V1 LMS Interface
/lms/#SchoolId/#QualificationShorthand/users LmsUsersR GET
/lms/#SchoolId/#QualificationShorthand/users/direct LmsUsersDirectR GET !token -- LMS
/lms/#SchoolId/#QualificationShorthand/userlist LmsUserlistR GET POST
/lms/#SchoolId/#QualificationShorthand/userlist/upload LmsUserlistUploadR GET POST !development
/lms/#SchoolId/#QualificationShorthand/userlist/direct LmsUserlistDirectR POST !token -- LMS, also remove JobLmsUserlist constructor
/lms/#SchoolId/#QualificationShorthand/result LmsResultR GET POST
/lms/#SchoolId/#QualificationShorthand/result/upload LmsResultUploadR GET POST !development
/lms/#SchoolId/#QualificationShorthand/result/direct LmsResultDirectR POST !token -- LMS, also remove JobLmsResults constructor
-- new V2 LMS Interface -- new V2 LMS Interface
/lms/#SchoolId/#QualificationShorthand/learners LmsLearnersR GET /lms/#SchoolId/#QualificationShorthand/learners LmsLearnersR GET
/lms/#SchoolId/#QualificationShorthand/learners/direct LmsLearnersDirectR GET !token -- LMS /lms/#SchoolId/#QualificationShorthand/learners/direct LmsLearnersDirectR GET !token -- LMS
/lms/#SchoolId/#QualificationShorthand/report LmsReportR GET POST /lms/#SchoolId/#QualificationShorthand/report LmsReportR GET POST
/lms/#SchoolId/#QualificationShorthand/report/upload LmsReportUploadR GET POST /lms/#SchoolId/#QualificationShorthand/report/upload LmsReportUploadR GET POST !development
/lms/#SchoolId/#QualificationShorthand/report/direct LmsReportDirectR POST !token -- LMS /lms/#SchoolId/#QualificationShorthand/report/direct LmsReportDirectR POST !token -- LMS
-- other lms routes -- other lms routes
/lms/#SchoolId/#QualificationShorthand/ident/#LmsIdent LmsIdentR GET -- redirect to LmsR with filter-parameter /lms/#SchoolId/#QualificationShorthand/ident/#LmsIdent LmsIdentR GET -- redirect to LmsR with filter-parameter
/lms/#SchoolId/#QualificationShorthand/user/#CryptoUUIDUser LmsUserR GET /lms/#SchoolId/#QualificationShorthand/user/#CryptoUUIDUser LmsUserR GET
/lmsuser/#CryptoUUIDUser LmsUserAllR GET /lmsuser/#CryptoUUIDUser LmsUserAllR GET

View File

@ -197,9 +197,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
@ -223,7 +223,7 @@ let
fi fi
''; '';
killallUni2work = pkgs.writeScriptBin "killuni2work" '' killallUni2work = pkgs.writeScriptBin "killall-uni2work" ''
#!${pkgs.zsh}/bin/zsh #!${pkgs.zsh}/bin/zsh
set -o pipefail set -o pipefail
@ -279,14 +279,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
; ;
}) })

View File

@ -57,8 +57,12 @@ import qualified Data.UUID.V4 as UUID
import System.Directory import System.Directory
-- old job system
import Jobs import Jobs
-- new job system
import JobSystem () -- TODO work in progress
import qualified Data.Text.Encoding as Text import qualified Data.Text.Encoding as Text
import Yesod.Auth.Util.PasswordStore import Yesod.Auth.Util.PasswordStore
@ -124,7 +128,7 @@ import Handler.Utils.Memcached (manageMemcachedLocalInvalidations)
import qualified System.Clock as Clock import qualified System.Clock as Clock
import Utils.Avs (mkAvsQuery) import Utils.Avs
-- 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.)
@ -145,7 +149,6 @@ import Handler.Material
import Handler.CryptoIDDispatch import Handler.CryptoIDDispatch
import Handler.SystemMessage import Handler.SystemMessage
import Handler.Health import Handler.Health
import Handler.Health.Interface
import Handler.Exam import Handler.Exam
import Handler.ExamOffice import Handler.ExamOffice
import Handler.Metrics import Handler.Metrics
@ -157,12 +160,9 @@ 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 ServantApi () -- YesodSubDispatch instances import ServantApi () -- YesodSubDispatch instances
import Servant.API import Servant.API
@ -354,15 +354,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
@ -659,7 +659,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
@ -748,8 +748,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

View File

@ -1,17 +1,13 @@
-- SPDX-FileCopyrightText: 2023-24 Gregor Kleen <gregor.kleen@ifi.lmu.de>,Steffen Jost <s.jost@fraport.de> -- SPDX-FileCopyrightText: 2022 Gregor Kleen <gregor.kleen@ifi.lmu.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(..)
, audit , audit
, AuditRemoteException(..) , AuditRemoteException(..)
, getRemote , getRemote
, logInterface, logInterface'
, reportAdminProblem
) where ) where
@ -19,8 +15,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
@ -109,93 +103,12 @@ audit :: ( AuthId (HandlerSite m) ~ Key User
-- - `transactionLogInitiator` is currently logged in user (or none) -- - `transactionLogInitiator` is currently logged in user (or none)
-- - `transactionLogRemote` is determined from current HTTP-Request -- - `transactionLogRemote` is determined from current HTTP-Request
audit transaction@(toJSON -> transactionLogInfo) = do audit transaction@(toJSON -> transactionLogInfo) = do
transactionLogTime <- liftIO getCurrentTime transactionLogTime <- liftIO getCurrentTime
transactionLogInstance <- getsYesod $ view instanceID transactionLogInstance <- getsYesod $ view instanceID
transactionLogInitiator <- liftHandler maybeAuthId transactionLogInitiator <- liftHandler maybeAuthId
transactionLogRemote <- handle (throwM . AuditRemoteException) $ Just <$> getRemote transactionLogRemote <- handle (throwM . AuditRemoteException) $ Just <$> getRemote
insert_ TransactionLog{..} insert_ TransactionLog{..}
$logInfoS "Audit" $ Text.filter (/= '\n') $ tshow (transaction, transactionLogInitiator, transactionLogRemote) <> " - " <> pack (prettyCallStack callStack) $logInfoS "Audit" $ Text.filter (/= '\n') $ tshow (transaction, transactionLogInitiator, transactionLogRemote) <> " - " <> pack (prettyCallStack callStack)
logInterface :: ( AuthId (HandlerSite m) ~ Key User
, IsSqlBackend (YesodPersistBackend (HandlerSite m))
, SqlBackendCanWrite (YesodPersistBackend (HandlerSite m))
, HasInstanceID (HandlerSite m) InstanceId
, YesodAuthPersist (HandlerSite m)
, MonadHandler m
, MonadCatch m
, HasAppSettings (HandlerSite m)
, HasCallStack
)
=> Text -- ^ Interface that is used
-> Text -- ^ Subtype of the interface, if any
-> Bool -- ^ Success=True, Failure=False
-> Maybe Int -- ^ Number of transmitted datasets
-> Text -- ^ Any additional information
-> ReaderT (YesodPersistBackend (HandlerSite m)) m ()
-- ^ Log a transaction using information available from `HandlerT`, also calls `audit`
logInterface interfaceLogInterface interfaceLogSubtype interfaceLogSuccess interfaceLogRows interfaceLogInfo = do
interfaceLogWrite <- (methodGet /=) . Wai.requestMethod . reqWaiRequest <$> getRequest
logInterface' interfaceLogInterface interfaceLogSubtype interfaceLogWrite interfaceLogSuccess interfaceLogRows interfaceLogInfo
logInterface' :: ( AuthId (HandlerSite m) ~ Key User
, IsSqlBackend (YesodPersistBackend (HandlerSite m))
, SqlBackendCanWrite (YesodPersistBackend (HandlerSite m))
, HasInstanceID (HandlerSite m) InstanceId
, YesodAuthPersist (HandlerSite m)
, MonadHandler m
, MonadCatch m
, HasAppSettings (HandlerSite m)
, HasCallStack
)
=> Text -- ^ Interface that is used
-> Text -- ^ Subtype of the interface, if any
-> Bool -- ^ True indicates Write Access to FRADrive
-> Bool -- ^ Success=True, Failure=False
-> Maybe Int -- ^ Number of transmitted datasets
-> Text -- ^ Any additional information
-> ReaderT (YesodPersistBackend (HandlerSite m)) m ()
-- ^ 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
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.
-- insert_ InterfaceLog{..}
void $ upsertBy (UniqueInterfaceSubtypeWrite interfaceLogInterface interfaceLogSubtype interfaceLogWrite)
( InterfaceLog{..} )
[ InterfaceLogTime =. interfaceLogTime
, InterfaceLogRows =. interfaceLogRows
, InterfaceLogInfo =. interfaceLogInfo
, InterfaceLogSuccess =. interfaceLogSuccess
]
audit TransactionInterface
{ transactionInterfaceName = interfaceLogInterface
, transactionInterfaceSubtype = interfaceLogSubtype
, transactionInterfaceWrite = interfaceLogWrite
, transactionInterfaceRows = interfaceLogRows
, transactionInterfaceInfo = interfaceLogInfo
, 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)

View File

@ -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,10 +213,9 @@ 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
} }
| TransactionQualificationUserDelete | TransactionQualificationUserDelete
{ transactionUser :: UserId { transactionUser :: UserId
@ -237,14 +233,6 @@ data Transaction
, transactionQualification :: QualificationId , transactionQualification :: QualificationId
, transactionQualificationScheduleRenewal :: Maybe Bool -- TRUE=will be notified upon expiry, FALSE=won't be notified; always JUST, for compatibility with TransactionQualificationUserEdit , transactionQualificationScheduleRenewal :: Maybe Bool -- TRUE=will be notified upon expiry, FALSE=won't be notified; always JUST, for compatibility with TransactionQualificationUserEdit
} }
| TransactionInterface
{ transactionInterfaceName :: Text
, transactionInterfaceSubtype :: Text
, transactionInterfaceWrite :: Bool -- True implies a write to FRADrive
, transactionInterfaceRows :: Maybe Int
, transactionInterfaceInfo :: Text
, transactionInterfaceSuccess :: Maybe Bool -- Just False implies a failure; Maybe used to achieve backwards compatibility
}
deriving (Eq, Ord, Read, Show, Generic) deriving (Eq, Ord, Read, Show, Generic)
deriveJSON defaultOptions deriveJSON defaultOptions
@ -255,62 +243,3 @@ deriveJSON defaultOptions
} ''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

View File

@ -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

View File

@ -59,7 +59,6 @@ decCryptoIDs [ ''SubmissionId
, ''MaterialFileId , ''MaterialFileId
, ''PrintJobId , ''PrintJobId
, ''QualificationId , ''QualificationId
, ''SentMailId
] ]
decCryptoIDKeySize decCryptoIDKeySize

View File

@ -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

View File

@ -0,0 +1,42 @@
-- SPDX-FileCopyrightText: 2023 Sarah Vaupel <sarah.vaupel@uniworx.de>
--
-- SPDX-License-Identifier: AGPL-3.0-or-later
{-# OPTIONS_GHC -fno-warn-orphans #-}
module Data.Jobsys.Instances
(
) where
import ClassyPrelude
import Jobsys.Basics (TimeRelative(..), JobStatus(..))
import Model.Types.TH.PathPiece
import Utils.PathPiece (camelToPathPiece', nullaryPathPiece, pathPieceJSON, pathPieceJSONKey)
import Data.Universe.Class (Finite, Universe)
-- TODO Refactor: Move to separate modules
deriving instance Generic TimeRelative
deriving anyclass instance Finite TimeRelative
deriving anyclass instance Universe TimeRelative
deriving anyclass instance NFData TimeRelative
nullaryPathPiece ''TimeRelative $ camelToPathPiece' 2
pathPieceJSON ''TimeRelative
pathPieceJSONKey ''TimeRelative
derivePersistFieldPathPiece ''TimeRelative
deriving instance Generic JobStatus
deriving anyclass instance Finite JobStatus
deriving anyclass instance Universe JobStatus
deriving anyclass instance NFData JobStatus
nullaryPathPiece ''JobStatus $ camelToPathPiece' 2
pathPieceJSON ''JobStatus
pathPieceJSONKey ''JobStatus
derivePersistFieldPathPiece ''JobStatus

View File

@ -1,4 +1,4 @@
-- SPDX-FileCopyrightText: 2022 Sarah Vaupel <sarah.vaupel@ifi.lmu.de> -- SPDX-FileCopyrightText: 2022-2023 Sarah Vaupel <sarah.vaupel@uniworx.de>, Sarah Vaupel <sarah.vaupel@ifi.lmu.de>
-- --
-- SPDX-License-Identifier: AGPL-3.0-or-later -- SPDX-License-Identifier: AGPL-3.0-or-later
@ -7,14 +7,45 @@
module Data.SemVer.Instances module Data.SemVer.Instances
() where () where
import ClassyPrelude import ClassyPrelude.Yesod
import qualified Data.SemVer as SemVer import qualified Data.SemVer as SemVer
import qualified Data.SemVer.Constraint as SemVer (Constraint(..)) import qualified Data.SemVer.Constraint as SemVer (Constraint(..))
import qualified Data.SemVer.Constraint as SemVer.Constraint import qualified Data.SemVer.Constraint as SemVer.Constraint
import Data.Either.Combinators (rightToMaybe)
-- import qualified Data.Text as Text
import Model.Types.TH.PathPiece
import Web.HttpApiData import Web.HttpApiData
instance PathPiece SemVer.Version where
fromPathPiece = rightToMaybe . SemVer.fromText
toPathPiece = SemVer.toText
-- instance PathPiece SemVer.Constraint where
-- fromPathPiece t = case Text.unpack t of
-- "*" -> Just SemVer.CAny
-- '<' : t' -> SemVer.CLt <$> fromPathPiece t'
-- '<':'=' : t' -> SemVer.CLtEq <$> fromPathPiece t'
-- '>' : t' -> SemVer.CGt <$> fromPathPiece t'
-- '>':'=' : t' -> SemVer.CGtEq <$> fromPathPiece t'
-- _other
-- | (t',t'') <- splitAt " || " t -> SemVer.COr <$> fromPathPiece t' <*> fromPathPiece t''
-- | (t',t'') <- splitAt " " t -> SemVer.CAnd <$> fromPathPiece t' <*> fromPathPiece t''
-- | otherwise -> SemVer.CEq <$> fromPathPiece t
-- toPathPiece SemVer.CAny = "*"
-- toPathPiece (SemVer.CLt v) = "<" <> toPathPiece v
-- toPathPiece (SemVer.CLtEq v) = "<=" <> toPathPiece v
-- toPathPiece (SemVer.CGt v) = ">" <> toPathPiece v
-- toPathPiece (SemVer.CGtEq v) = ">=" <> toPathPiece v
-- toPathPiece (SemVer.CEq v) = toPathPiece v
-- toPathPiece (SemVer.CAnd a b) = toPathPiece a <> " " <> toPathPiece b
-- toPathPiece (SemVer.COr a b) = toPathPiece a <> " || " <> toPathPiece b
derivePersistFieldPathPiece ''SemVer.Version
instance ToHttpApiData SemVer.Version where instance ToHttpApiData SemVer.Version where
toUrlPiece = SemVer.toText toUrlPiece = SemVer.toText

View File

@ -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-23 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
@ -15,26 +15,22 @@ module Database.Esqueleto.Utils
, (=?.), (?=.) , (=?.), (?=.)
, (=~.), (~=.) , (=~.), (~=.)
, (>~.), (<~.) , (>~.), (<~.)
, (~.), (~*.), (!~.), (!~*.)
, or, and , or, and
, any, all , any, all
, not__, parens
, subSelectAnd, subSelectOr , subSelectAnd, subSelectOr
, mkExactFilter, mkExactFilterWith, mkExactFilterWithComma , mkExactFilter, mkExactFilterWith
, mkExactFilterLast, mkExactFilterLastWith , mkExactFilterLast, mkExactFilterLastWith
, mkExactFilterMaybeLast, mkExactFilterMaybeLast' , mkExactFilterMaybeLast, mkExactFilterMaybeLast'
, mkContainsFilter, mkContainsFilterWith , mkContainsFilter, mkContainsFilterWith
, mkContainsFilterWithSet, mkContainsFilterWithComma, mkContainsFilterWithCommaPlus , mkContainsFilterWithSet, mkContainsFilterWithComma, mkContainsFilterWithCommaPlus
, mkDayFilter, mkDayFilterFrom, mkDayFilterTo , mkDayFilter, mkDayFilterFrom, mkDayFilterTo
, mkExistsFilter, mkExistsFilterWithComma , mkExistsFilter
-- , 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 +40,14 @@ module Database.Esqueleto.Utils
, greatest, least , greatest, least
, abs , abs
, SqlProject(..) , SqlProject(..)
, (->.), (->>.), (->>>.), (#>>.) , (->.), (->>.), (#>>.)
, fromSqlKey , fromSqlKey
, unKey , unKey
, subSelectCountDistinct
, selectCountRows, selectCountDistinct , selectCountRows, selectCountDistinct
, selectMaybe , selectMaybe
, str2text, str2text'
, 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 +58,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 +153,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)
@ -256,13 +225,8 @@ explicitUnsafeCoerceSqlExprValue typ (E.ERaw _m1 f1) = E.ERaw E.noMeta $ \_nPare
) )
and, or :: Foldable f => f (E.SqlExpr (E.Value Bool)) -> E.SqlExpr (E.Value Bool) and, or :: Foldable f => f (E.SqlExpr (E.Value Bool)) -> E.SqlExpr (E.Value Bool)
-- and = F.foldl' (E.&&.) true -- we can use foldl' since PostgreSQL reorders conditions anyway and = F.foldr (E.&&.) true
-- or = F.foldl' (E.||.) false or = F.foldr (E.||.) false
-- Maybe this help the PostgreSQL query optimizer, though I doubt it?
and f | F.null f = true
| otherwise = F.foldl1 (E.&&.) f
or f | F.null f = false
| otherwise = F.foldl1 (E.||.) f
-- | Given a test and a set of values, check whether anyone succeeds the test -- | Given a test and a set of values, check whether anyone succeeds the test
-- WARNING: SQL leaves it explicitely unspecified whether `||` is short curcuited (i.e. lazily evaluated) -- WARNING: SQL leaves it explicitely unspecified whether `||` is short curcuited (i.e. lazily evaluated)
@ -281,9 +245,6 @@ subSelectOr q = parens . E.subSelectUnsafe $ flip (E.unsafeSqlAggregateFunction
parens :: E.SqlExpr (E.Value a) -> E.SqlExpr (E.Value a) parens :: E.SqlExpr (E.Value a) -> E.SqlExpr (E.Value a)
parens = E.unsafeSqlFunction "" parens = E.unsafeSqlFunction ""
-- | Workaround for Esqueleto-Bug not placing parenthesis after NOT, see #155
not__ :: E.SqlExpr (E.Value Bool) -> E.SqlExpr (E.Value Bool)
not__ = E.not_ . parens
-- Allow usage of Tuples as DbtRowKey, i.e. SqlIn instances for tuples -- Allow usage of Tuples as DbtRowKey, i.e. SqlIn instances for tuples
$(sqlInTuples [2..16]) $(sqlInTuples [2..16])
@ -322,17 +283,6 @@ mkExactFilterWith cast lenslike row criterias
| Set.null criterias = true | Set.null criterias = true
| otherwise = lenslike row `E.in_` E.valList (cast <$> Set.toList criterias) | otherwise = lenslike row `E.in_` E.valList (cast <$> Set.toList criterias)
-- | like `mkExactFilterWith` but splits comma separared Texts into multiple criteria
mkExactFilterWithComma :: (PersistField b)
=> (Text -> Maybe b) -- ^ type conversion
-> (t -> E.SqlExpr (E.Value b)) -- ^ getter from query to searched element
-> t -- ^ query row
-> Set.Set Text -- ^ needle collection
-> E.SqlExpr (E.Value Bool)
mkExactFilterWithComma cast lenslike row (foldMap commaSeparatedText -> criterias)
| Set.null criterias = true
| otherwise = lenslike row `E.in_` E.valList (mapMaybe cast $ Set.toList criterias)
-- | generic filter creation for dbTable -- | generic filter creation for dbTable
-- Given a lens-like function, make filter for exact matches against last element of a collection -- Given a lens-like function, make filter for exact matches against last element of a collection
mkExactFilterLast :: (PersistField a) mkExactFilterLast :: (PersistField a)
@ -350,7 +300,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 +329,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 +337,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 +345,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 +356,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 +367,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 +381,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
@ -483,17 +421,6 @@ mkExistsFilter query row criterias
| Set.null criterias = true | Set.null criterias = true
| otherwise = any (E.exists . query row) $ Set.toList criterias | otherwise = any (E.exists . query row) $ Set.toList criterias
mkExistsFilterWithComma :: PathPiece a
=> (Text -> a)
-> (t -> a -> E.SqlQuery ())
-> t
-> Set.Set Text
-> E.SqlExpr (E.Value Bool)
mkExistsFilterWithComma cast query row (foldMap commaSeparatedText -> criterias)
| Set.null criterias = true
| otherwise = any (E.exists . query row . cast) criterias
-- | Combine several filters, using logical or -- | Combine several filters, using logical or
anyFilter :: Foldable f anyFilter :: Foldable f
=> f (t -> cs -> E.SqlExpr (E.Value Bool)) => f (t -> cs -> E.SqlExpr (E.Value Bool))
@ -550,13 +477,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 +570,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 +609,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))
@ -714,12 +627,6 @@ unKey :: ( Coercible (Key entity) a
=> E.SqlExpr (E.Value (Key entity)) -> E.SqlExpr (E.Value a) => E.SqlExpr (E.Value (Key entity)) -> E.SqlExpr (E.Value a)
unKey = E.veryUnsafeCoerceSqlExprValue unKey = E.veryUnsafeCoerceSqlExprValue
-- | distinct version of `Database.Esqueleto.subSelectCount`
subSelectCountDistinct :: (Num a, PersistField a) => Ex.SqlQuery (Ex.SqlExpr (Ex.Value typ)) -> Ex.SqlExpr (Ex.Value a)
subSelectCountDistinct query = Ex.subSelectUnsafe (Ex.countDistinct <$> query)
-- PersistField a => SqlQuery (SqlExpr (Value a)) -> SqlExpr (Value a)
-- countDistinct :: Num a => SqlExpr (Value typ) -> SqlExpr (Value a)
selectCountRows :: (Num a, PersistField a, MonadIO m) => E.SqlQuery ignored -> E.SqlReadT m a selectCountRows :: (Num a, PersistField a, MonadIO m) => E.SqlQuery ignored -> E.SqlReadT m a
selectCountRows q = do selectCountRows q = do
@ -742,25 +649,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
num2text :: Num n => E.SqlExpr (E.Value n) -> E.SqlExpr (E.Value 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"
-- | cast text to day, truly unsafe
day' :: E.SqlExpr (E.Value Text) -> E.SqlExpr (E.Value Day) day' :: E.SqlExpr (E.Value Text) -> E.SqlExpr (E.Value Day)
day' = E.unsafeSqlCastAs "date" day' = E.unsafeSqlCastAs "date"
@ -768,12 +660,13 @@ 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
infixl 6 `diffDays`, `diffTimes` infixl 6 `diffDays`, `diffTimes`
diffDays :: E.SqlExpr (E.Value Day) -> E.SqlExpr (E.Value Day) -> E.SqlExpr (E.Value Int) diffDays :: E.SqlExpr (E.Value Day) -> E.SqlExpr (E.Value Day) -> E.SqlExpr (E.Value Int)
@ -815,16 +708,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") []

View File

@ -539,11 +539,8 @@ tagAccessPredicate AuthAdmin = cacheAPSchoolFunction SchoolAdmin (Just $ Right d
return Authorized return Authorized
tagAccessPredicate AuthSupervisor = APDB $ \_ _ mAuthId route _ -> case route of tagAccessPredicate AuthSupervisor = APDB $ \_ _ mAuthId route _ -> case route of
ForProfileR cID -> checkSupervisor (mAuthId, cID) ForProfileR cID -> checkSupervisor (mAuthId, cID)
ForProfileDataR cID -> checkSupervisor (mAuthId, cID) ForProfileDataR cID -> checkSupervisor (mAuthId, cID)
FirmAllR -> checkAnySupervisor mAuthId
FirmUsersR fsh -> checkCompanySupervisor (mAuthId, fsh)
FirmSupersR fsh -> checkCompanySupervisor (mAuthId, fsh)
r -> $unsupportedAuthPredicate AuthSupervisor r r -> $unsupportedAuthPredicate AuthSupervisor r
where where
checkSupervisor sup@(mAuthId, cID) = $cachedHereBinary sup . exceptT return return $ do checkSupervisor sup@(mAuthId, cID) = $cachedHereBinary sup . exceptT return return $ do
@ -552,17 +549,6 @@ tagAccessPredicate AuthSupervisor = APDB $ \_ _ mAuthId route _ -> case route of
isSupervisor <- lift . existsBy $ UniqueUserSupervisor authId uid isSupervisor <- lift . existsBy $ UniqueUserSupervisor authId uid
guardMExceptT isSupervisor (unauthorizedI MsgUnauthorizedSupervisor) guardMExceptT isSupervisor (unauthorizedI MsgUnauthorizedSupervisor)
return Authorized return Authorized
checkCompanySupervisor sup@(mAuthId, fsh) = $cachedHereBinary sup . exceptT return return $ do
authId <- maybeExceptT AuthenticationRequired $ return mAuthId
-- isSupervisor <- lift . existsBy $ UniqueUserCompany authId $ CompanyKey fsh
isSupervisor <- lift $ exists [UserCompanyUser ==. authId, UserCompanyCompany ==. CompanyKey fsh, UserCompanySupervisor ==. True]
guardMExceptT isSupervisor (unauthorizedI $ MsgUnauthorizedCompanySupervisor fsh)
return Authorized
checkAnySupervisor mAuthId = $cachedHereBinary mAuthId . exceptT return return $ do
authId <- maybeExceptT AuthenticationRequired $ return mAuthId
isSupervisor <- lift $ exists [UserSupervisorSupervisor ==. authId]
guardMExceptT isSupervisor (unauthorizedI MsgUnauthorizedAnySupervisor)
return Authorized
tagAccessPredicate AuthSystemExamOffice = cacheAPSystemFunction SystemExamOffice (Just $ Right diffHour) $ \mAuthId' _ _ examOfficeList -> if tagAccessPredicate AuthSystemExamOffice = cacheAPSystemFunction SystemExamOffice (Just $ Right diffHour) $ \mAuthId' _ _ examOfficeList -> if
| maybe True (`Set.notMember` examOfficeList) mAuthId' -> Right $ if | maybe True (`Set.notMember` examOfficeList) mAuthId' -> Right $ if

View File

@ -1,14 +1,9 @@
-- SPDX-FileCopyrightText: 2022-23 Gregor Kleen <gregor.kleen@ifi.lmu.de>,Sarah Vaupel <sarah.vaupel@ifi.lmu.de>,Sarah Vaupel <vaupel.sarah@campus.lmu.de>,Steffen Jost <s.jost@fraport.de>,Steffen Jost <jost@tcs.ifi.lmu.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
-- To add new language files:
-- 1. include new statement, e.g. mkMessageAddition ''UniWorX "Print" "messages/uniworx/categories/print" "de-de-formal"
-- 2. create appropriate translation files in the specified folder
-- 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
@ -25,7 +20,6 @@ module Foundation.I18n
, UniWorXI18nMessage(..),UniWorXJobsHandlerMessage(..), UniWorXModelTypesMessage(..), UniWorXYesodMiddlewareMessage(..) , UniWorXI18nMessage(..),UniWorXJobsHandlerMessage(..), UniWorXModelTypesMessage(..), UniWorXYesodMiddlewareMessage(..)
, UniWorXQualificationMessage(..) , UniWorXQualificationMessage(..)
, UniWorXPrintMessage(..) , UniWorXPrintMessage(..)
, UniWorXFirmMessage(..)
, UniWorXAvsMessage(..) , UniWorXAvsMessage(..)
, UniWorXAuthorshipStatementMessage(..) , UniWorXAuthorshipStatementMessage(..)
, ShortTermIdentifier(..) , ShortTermIdentifier(..)
@ -39,12 +33,10 @@ module Foundation.I18n
, StudyDegreeTerm(..) , StudyDegreeTerm(..)
, ShortStudyFieldType(..) , ShortStudyFieldType(..)
, StudyDegreeTermType(..) , StudyDegreeTermType(..)
, ErrorResponseTitle(..) , ErrorResponseTitle(..)
, UniWorXMessages(..) , UniWorXMessages(..)
, uniworxMessages , uniworxMessages
, unRenderMessage, unRenderMessage', unRenderMessageLenient , unRenderMessage, unRenderMessage', unRenderMessageLenient
, SomeMessages(..)
, someMessages
, module Foundation.I18n.TH , module Foundation.I18n.TH
) where ) where
@ -87,30 +79,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 +106,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 +128,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 +146,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 +173,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"
@ -214,14 +197,6 @@ maybeToMessage :: ToMessage m => Text -> Maybe m -> Text -> Text
maybeToMessage _ Nothing _ = mempty maybeToMessage _ Nothing _ = mempty
maybeToMessage before (Just x) after = before <> toMessage x <> after maybeToMessage before (Just x) after = before <> toMessage x <> after
maybeBoolMessage :: Maybe Bool -> Text -> Text -> Text -> Text
maybeBoolMessage Nothing n _ _ = n
maybeBoolMessage (Just True) _ t _ = t
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)
@ -258,7 +233,6 @@ mkMessageAddition ''UniWorX "Send" "messages/uniworx/categories/send" "de-de-for
mkMessageAddition ''UniWorX "YesodMiddleware" "messages/uniworx/categories/yesod_middleware" "de-de-formal" mkMessageAddition ''UniWorX "YesodMiddleware" "messages/uniworx/categories/yesod_middleware" "de-de-formal"
mkMessageAddition ''UniWorX "User" "messages/uniworx/categories/user" "de-de-formal" mkMessageAddition ''UniWorX "User" "messages/uniworx/categories/user" "de-de-formal"
mkMessageAddition ''UniWorX "Print" "messages/uniworx/categories/print" "de-de-formal" mkMessageAddition ''UniWorX "Print" "messages/uniworx/categories/print" "de-de-formal"
mkMessageAddition ''UniWorX "Firm" "messages/uniworx/categories/firm" "de-de-formal"
mkMessageAddition ''UniWorX "Button" "messages/uniworx/utils/buttons" "de-de-formal" mkMessageAddition ''UniWorX "Button" "messages/uniworx/utils/buttons" "de-de-formal"
mkMessageAddition ''UniWorX "Form" "messages/uniworx/utils/handler_form" "de-de-formal" mkMessageAddition ''UniWorX "Form" "messages/uniworx/utils/handler_form" "de-de-formal"
mkMessageAddition ''UniWorX "TableColumn" "messages/uniworx/utils/table_column" "de-de-formal" mkMessageAddition ''UniWorX "TableColumn" "messages/uniworx/utils/table_column" "de-de-formal"
@ -280,18 +254,6 @@ 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]
deriving newtype (Semigroup, Monoid)
instance master ~ master' => RenderMessage master (SomeMessages master') where
renderMessage a b (SomeMessages msgs) = Text.intercalate "\n " $ renderMessage a b <$> msgs
-- | convenienience function if all messages happen to belong to the exact same type
someMessages :: RenderMessage master msg => [msg] -> SomeMessages master
someMessages msgs = SomeMessages $ SomeMessage <$> msgs
instance RenderMessage UniWorX (Maybe LmsStatus) where -- useful for Filter with optionsFinite instance RenderMessage UniWorX (Maybe LmsStatus) where -- useful for Filter with optionsFinite
renderMessage f ls (Just s) = renderMessage f ls s renderMessage f ls (Just s) = renderMessage f ls s
renderMessage f ls Nothing = renderMessage f ls MsgLmsStateOpen renderMessage f ls Nothing = renderMessage f ls MsgLmsStateOpen
@ -614,12 +576,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 <> ">"

View File

@ -87,9 +87,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
@ -121,27 +121,13 @@ breadcrumb ProblemUnreachableR = i18nCrumb MsgProblemsUnreachableHeading $ Just
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 ProblemAvsSynchR
breadcrumb ConfigInterfacesR = i18nCrumb MsgConfigInterfacesHeading $ Just AdminProblemsR
breadcrumb FirmAllR = i18nCrumb MsgMenuFirms Nothing breadcrumb PrintCenterR = i18nCrumb MsgMenuApc Nothing
breadcrumb FirmsCommR{} = i18nCrumb MsgMenuFirmsComm $ Just FirmAllR
breadcrumb FirmUsersR{} = i18nCrumb MsgMenuFirmUsers $ Just FirmAllR
breadcrumb (FirmSupersR fsh)= i18nCrumb MsgMenuFirmSupervisors $ Just $ FirmUsersR fsh
breadcrumb (FirmCommR fsh)= i18nCrumb MsgMenuFirmsComm $ Just $ FirmUsersR fsh
breadcrumb CommCenterR = i18nCrumb MsgMenuCommCenter 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
breadcrumb PrintAckDirectR{}= i18nCrumb MsgMenuPrintAck $ Just PrintCenterR breadcrumb PrintAckDirectR{}= i18nCrumb MsgMenuPrintSend $ Just PrintCenterR -- never displayed
breadcrumb PrintLogR = i18nCrumb MsgMenuPrintLog $ Just PrintCenterR
breadcrumb SchoolListR = i18nCrumb MsgMenuSchoolList $ Just AdminR breadcrumb SchoolListR = i18nCrumb MsgMenuSchoolList $ Just AdminR
breadcrumb (SchoolR ssh sRoute) = case sRoute of breadcrumb (SchoolR ssh sRoute) = case sRoute of
@ -172,10 +158,9 @@ breadcrumb FaqR = i18nCrumb MsgBreadcrumbFaq $ Just InfoR
breadcrumb HelpR = i18nCrumb MsgMenuHelp Nothing breadcrumb HelpR = i18nCrumb MsgMenuHelp Nothing
breadcrumb HealthR = i18nCrumb MsgMenuHealth Nothing breadcrumb HealthR = i18nCrumb MsgMenuHealth Nothing
breadcrumb (HealthInterfaceR _) = i18nCrumb MsgMenuHealthInterface (Just HealthR) breadcrumb InstanceR = i18nCrumb MsgMenuInstance Nothing
breadcrumb InstanceR = i18nCrumb MsgMenuInstance Nothing breadcrumb StatusR = i18nCrumb MsgMenuHealth Nothing -- never displayed
breadcrumb StatusR = i18nCrumb MsgMenuHealth Nothing -- never displayed
breadcrumb QualificationAllR = i18nCrumb MsgMenuQualifications Nothing breadcrumb QualificationAllR = i18nCrumb MsgMenuQualifications Nothing
breadcrumb (QualificationSchoolR ssh ) = useRunDB . maybeT (i18nCrumb MsgBreadcrumbSchool . Just $ SchoolListR) $ do -- redirect only, used in other breadcrumbs breadcrumb (QualificationSchoolR ssh ) = useRunDB . maybeT (i18nCrumb MsgBreadcrumbSchool . Just $ SchoolListR) $ do -- redirect only, used in other breadcrumbs
@ -194,13 +179,21 @@ breadcrumb (LmsR ssh qsh) = useRunDB . maybeT (i18nCrumb MsgBrea
guardM . lift . existsBy $ SchoolQualificationShort ssh qsh guardM . lift . existsBy $ SchoolQualificationShort ssh qsh
return (CI.original qsh, Just $ LmsSchoolR ssh) return (CI.original qsh, Just $ LmsSchoolR ssh)
breadcrumb (LmsEditR ssh qsh) = i18nCrumb MsgMenuLmsEdit $ Just $ LmsR ssh qsh breadcrumb (LmsEditR ssh qsh) = i18nCrumb MsgMenuLmsEdit $ Just $ LmsR ssh qsh
breadcrumb (LmsUsersR ssh qsh) = i18nCrumb MsgMenuLmsUsers $ Just $ LmsR ssh qsh
breadcrumb (LmsUsersDirectR ssh qsh) = i18nCrumb MsgMenuLmsUsers $ Just $ LmsUsersR ssh qsh -- never displayed, TypedContent
breadcrumb (LmsUserlistR ssh qsh) = i18nCrumb MsgMenuLmsUserlist $ Just $ LmsR ssh qsh
breadcrumb (LmsUserlistUploadR ssh qsh) = i18nCrumb MsgMenuLmsUpload $ Just $ LmsUserlistR ssh qsh
breadcrumb (LmsUserlistDirectR ssh qsh) = i18nCrumb MsgMenuLmsUpload $ Just $ LmsUserlistR ssh qsh -- never displayed
breadcrumb (LmsResultR ssh qsh) = i18nCrumb MsgMenuLmsResult $ Just $ LmsR ssh qsh
breadcrumb (LmsResultUploadR ssh qsh) = i18nCrumb MsgMenuLmsUpload $ Just $ LmsResultR ssh qsh
breadcrumb (LmsResultDirectR ssh qsh) = i18nCrumb MsgMenuLmsUpload $ Just $ LmsResultR ssh qsh -- never displayed
-- v2 -- v2
breadcrumb (LmsLearnersR ssh qsh) = i18nCrumb MsgMenuLmsLearners $ Just $ LmsR ssh qsh breadcrumb (LmsLearnersR ssh qsh) = i18nCrumb MsgMenuLmsLearners $ Just $ LmsR ssh qsh
breadcrumb (LmsLearnersDirectR ssh qsh) = i18nCrumb MsgMenuLmsLearners $ Just $ LmsLearnersR ssh qsh -- never displayed, TypedContent breadcrumb (LmsLearnersDirectR ssh qsh) = i18nCrumb MsgMenuLmsLearners $ Just $ LmsLearnersR ssh qsh -- never displayed, TypedContent
breadcrumb (LmsReportR ssh qsh) = i18nCrumb MsgMenuLmsReport $ Just $ LmsR ssh qsh breadcrumb (LmsReportR ssh qsh) = i18nCrumb MsgMenuLmsReport $ Just $ LmsR ssh qsh
breadcrumb (LmsReportUploadR ssh qsh) = i18nCrumb MsgMenuLmsUpload $ Just $ LmsReportR ssh qsh breadcrumb (LmsReportUploadR ssh qsh) = i18nCrumb MsgMenuLmsUpload $ Just $ LmsReportR ssh qsh
breadcrumb (LmsReportDirectR ssh qsh) = i18nCrumb MsgMenuLmsUpload $ Just $ LmsReportR ssh qsh -- never displayed breadcrumb (LmsReportDirectR ssh qsh) = i18nCrumb MsgMenuLmsUpload $ Just $ LmsReportR ssh qsh -- never displayed
-- --
breadcrumb (LmsIdentR ssh qsh _ ) = breadcrumb $ LmsR ssh qsh -- just a redirect breadcrumb (LmsIdentR ssh qsh _ ) = breadcrumb $ LmsR ssh qsh -- just a redirect
breadcrumb (LmsUserR ssh _qsh u ) = i18nCrumb MsgMenuLmsUser $ Just $ LmsUserSchoolR u ssh breadcrumb (LmsUserR ssh _qsh u ) = i18nCrumb MsgMenuLmsUser $ Just $ LmsUserSchoolR u ssh
breadcrumb (LmsUserSchoolR u _ ) = i18nCrumb MsgMenuLmsUserSchool $ Just $ LmsUserAllR u breadcrumb (LmsUserSchoolR u _ ) = i18nCrumb MsgMenuLmsUserSchool $ Just $ LmsUserAllR u
@ -301,7 +294,7 @@ breadcrumb (CourseR tid ssh csh (TutorialR tutn sRoute)) = case sRoute of
TUsersR -> useRunDB . maybeT (i18nCrumb MsgBreadcrumbTutorial . Just $ CourseR tid ssh csh CTutorialListR) $ do TUsersR -> useRunDB . maybeT (i18nCrumb MsgBreadcrumbTutorial . Just $ CourseR tid ssh csh CTutorialListR) $ do
guardM . lift . hasReadAccessTo $ CTutorialR tid ssh csh tutn TUsersR guardM . lift . hasReadAccessTo $ CTutorialR tid ssh csh tutn TUsersR
return (CI.original tutn, Just $ CourseR tid ssh csh CTutorialListR) return (CI.original tutn, Just $ CourseR tid ssh csh CTutorialListR)
TAddUserR -> i18nCrumb MsgMenuTutorialAddMembers . Just $ CTutorialR tid ssh csh tutn TUsersR TAddUserR -> i18nCrumb MsgMenuTutorialAddMembers . Just $ CTutorialR tid ssh csh tutn TUsersR
TEditR -> i18nCrumb MsgMenuTutorialEdit . Just $ CTutorialR tid ssh csh tutn TUsersR TEditR -> i18nCrumb MsgMenuTutorialEdit . Just $ CTutorialR tid ssh csh tutn TUsersR
TDeleteR -> i18nCrumb MsgMenuTutorialDelete . Just $ CTutorialR tid ssh csh tutn TUsersR TDeleteR -> i18nCrumb MsgMenuTutorialDelete . Just $ CTutorialR tid ssh csh tutn TUsersR
TCommR -> i18nCrumb MsgMenuTutorialComm . Just $ CTutorialR tid ssh csh tutn TUsersR TCommR -> i18nCrumb MsgMenuTutorialComm . Just $ CTutorialR tid ssh csh tutn TUsersR
@ -761,18 +754,6 @@ defaultLinks = fmap catMaybes . mapM runMaybeT $ -- Define the menu items of the
, navForceActive = False , navForceActive = False
} }
} }
, return NavHeader
{ navHeaderRole = NavHeaderPrimary
, navIcon = IconCompany
, navLink = NavLink
{ navLabel = MsgMenuFirms
, navRoute = FirmAllR
, navAccess' = NavAccessTrue
, navType = NavTypeLink { navModal = False }
, navQuick' = mempty
, navForceActive = False
}
}
, return NavHeader , return NavHeader
{ navHeaderRole = NavHeaderPrimary { navHeaderRole = NavHeaderPrimary
, navIcon = IconPrintCenter , navIcon = IconPrintCenter
@ -1342,17 +1323,6 @@ pageActions HealthR = return
} }
, navChildren = [] , navChildren = []
} }
, NavPageActionPrimary
{ navLink = NavLink
{ navLabel = MsgMenuHealthInterface
, navRoute = HealthInterfaceR []
, navAccess' = NavAccessTrue
, navType = NavTypeLink { navModal = False }
, navQuick' = mempty
, navForceActive = False
}
, navChildren = []
}
] ]
pageActions InstanceR = return pageActions InstanceR = return
[ NavPageActionPrimary [ NavPageActionPrimary
@ -1464,12 +1434,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
@ -2394,6 +2358,26 @@ pageActions (LmsR sid qsh) = return
, defNavLink MsgMenuLmsDirectUpload $ LmsReportDirectR sid qsh , defNavLink MsgMenuLmsDirectUpload $ LmsReportDirectR sid qsh
] ]
} }
, NavPageActionSecondary
{ navLink = defNavLink MsgMenuLmsUsers $ LmsUsersR sid qsh
-- , navChildren =
-- [ defNavLink MsgMenuLmsDirectDownload $ LmsUsersDirectR sid qsh
-- ]
}
, NavPageActionSecondary
{ navLink = defNavLink MsgMenuLmsUserlist $ LmsUserlistR sid qsh
-- , navChildren =
-- [ defNavLink MsgMenuLmsUpload $ LmsUserlistUploadR sid qsh
-- , defNavLink MsgMenuLmsDirectUpload $ LmsUserlistDirectR sid qsh
-- ]
}
, NavPageActionSecondary
{ navLink = defNavLink MsgMenuLmsResult $ LmsResultR sid qsh
-- , navChildren =
-- [ defNavLink MsgMenuLmsUpload $ LmsResultUploadR sid qsh
-- , defNavLink MsgMenuLmsDirectUpload $ LmsResultDirectR sid qsh
-- ]
}
, NavPageActionSecondary { , NavPageActionSecondary {
navLink = defNavLink MsgMenuLmsEdit $ LmsEditR sid qsh navLink = defNavLink MsgMenuLmsEdit $ LmsEditR sid qsh
} }
@ -2414,18 +2398,6 @@ pageActions ApiDocsR = return
, navChildren = [] , navChildren = []
} }
] ]
pageActions (FirmUsersR fsh) = return
[ NavPageActionPrimary
{ navLink = defNavLink MsgTableCompanyNrSupers $ FirmSupersR fsh
, navChildren = []
}
]
pageActions (FirmSupersR fsh) = return
[ NavPageActionPrimary
{ navLink = defNavLink MsgTableCompanyNrUsers $ FirmUsersR fsh
, navChildren = []
}
]
pageActions PrintCenterR = do pageActions PrintCenterR = do
openDays <- useRunDB $ Ex.select $ do openDays <- useRunDB $ Ex.select $ do
pj <- Ex.from $ Ex.table @PrintJob pj <- Ex.from $ Ex.table @PrintJob
@ -2461,94 +2433,16 @@ pageActions PrintCenterR = do
, navForceActive = False , navForceActive = False
} }
} }
printLog = NavPageActionSecondary
{ navLink = NavLink
{ navLabel = MsgMenuPrintLog
, navRoute = PrintLogR
, navAccess' = NavAccessTrue
, navType = NavTypeLink { navModal = False }
, navQuick' = mempty
, navForceActive = False
}
}
printAck = NavPageActionSecondary
{ navLink = NavLink
{ navLabel = MsgMenuPrintAck
, navRoute = PrintAckDirectR
, navAccess' = NavAccessTrue
, navType = NavTypeLink { navModal = False }
, navQuick' = mempty
, navForceActive = False
}
}
dayLinks <- mapM toDayAck $ Map.toAscList dayMap dayLinks <- mapM toDayAck $ Map.toAscList dayMap
return $ manualSend : printLog : printAck : take 9 dayLinks return $ manualSend : take 9 dayLinks
pageActions CommCenterR = return pageActions AdminCrontabR = 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
[ NavPageActionPrimary [ NavPageActionPrimary
{ navLink = defNavLink MsgMenuAdminJobs AdminJobsR { navLink = defNavLink MsgMenuAdminJobs AdminJobsR
, navChildren = [] , navChildren = []
} }
] ]
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

View File

@ -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
@ -123,9 +123,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

View File

@ -107,7 +107,7 @@ 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 ldapPool' <- getsYesod $ view _appLdapPool
flip catches excHandlers $ case ldapPool' of flip catches excHandlers $ case ldapPool' of
@ -153,9 +153,9 @@ _upsertCampusUserMode mMode cs@Creds{..}
defaultOther = apHash defaultOther = apHash
ldapLookupAndUpsert :: forall m. (MonadHandler m, HandlerSite m ~ UniWorX, MonadMask m, MonadUnliftIO m) => Text -> SqlPersistT m (Entity User) ldapLookupAndUpsert :: forall m. (MonadHandler m, HandlerSite m ~ UniWorX, MonadMask m, MonadUnliftIO m) => Text -> SqlPersistT m (Entity User)
ldapLookupAndUpsert ident = ldapLookupAndUpsert ident =
getsYesod (view _appLdapPool) >>= \case getsYesod (view _appLdapPool) >>= \case
Nothing -> throwM $ CampusUserLdapError $ LdapHostNotResolved "No LDAP configuration in Foundation." Nothing -> throwM $ CampusUserLdapError $ LdapHostNotResolved "No LDAP configuration in Foundation."
Just ldapPool -> Just ldapPool ->
campusUser'' ldapPool campusUserFailoverMode ident >>= \case campusUser'' ldapPool campusUserFailoverMode ident >>= \case
@ -182,21 +182,22 @@ upsertCampusUser upsertMode ldapData = do
userDefaultConf <- getsYesod $ view _appUserDefaults userDefaultConf <- getsYesod $ view _appUserDefaults
(newUser,userUpdate) <- decodeUser now userDefaultConf upsertMode ldapData (newUser,userUpdate) <- decodeUser now userDefaultConf upsertMode ldapData
--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 <- for (userLdapPrimaryKey newUser) $ \pKey -> selectKeysList [ UserLdapPrimaryKey ==. Just pKey ] []
user@(Entity userId userRec) <- case oldUsers of user@(Entity userId userRec) <- case oldUsers of
Just [oldUserId] -> updateGetEntity oldUserId userUpdate Just [oldUserId] -> updateGetEntity oldUserId userUpdate
_other -> upsertBy (UniqueAuthentication (newUser ^. _userIdent)) newUser userUpdate _other -> upsertBy (UniqueAuthentication (newUser ^. _userIdent)) newUser userUpdate
unless (validDisplayName (newUser ^. _userTitle) unless (validDisplayName (newUser ^. _userTitle)
(newUser ^. _userFirstName) (newUser ^. _userFirstName)
(newUser ^. _userSurname) (newUser ^. _userSurname)
(userRec ^. _userDisplayName)) $ (userRec ^. _userDisplayName)) $
update userId [ UserDisplayName =. (newUser ^. _userDisplayName) ] -- update invalid display names only update userId [ UserDisplayName =. (newUser ^. _userDisplayName) ]
when (validEmail' (userRec ^. _userEmail)) $ do -- RECALL: userRec already contains basic updates when (validEmail' (userRec ^. _userEmail)) $ do
let emUps = [ UserDisplayEmail =. (newUser ^. _userEmail) | not (validEmail' (userRec ^. _userDisplayEmail)) ] let emUps = [ UserDisplayEmail =. (newUser ^. _userEmail) | not (validEmail' (userRec ^. _userDisplayEmail)) ]
++ [ UserAuthentication =. AuthLDAP | is _AuthNoLogin (userRec ^. _userAuthentication) ] ++ [ UserAuthentication =. AuthLDAP | is _AuthNoLogin (userRec ^. _userAuthentication) ]
update userId emUps -- update already checks whether list is empty unless (null emUps) $ update userId emUps
-- Attempt to update ident, too: -- Attempt to update ident, too:
unless (validEmail' (userRec ^. _userIdent)) $ unless (validEmail' (userRec ^. _userIdent)) $
void $ maybeCatchAll (update userId [ UserIdent =. (newUser ^. _userEmail) ] >> return (Just ())) void $ maybeCatchAll (update userId [ UserIdent =. (newUser ^. _userEmail) ] >> return (Just ()))
@ -227,10 +228,10 @@ decodeUserTest mbIdent ldapData = do
decodeUser :: (MonadThrow m) => UTCTime -> UserDefaultConf -> UpsertCampusUserMode -> Ldap.AttrList [] -> m (User,_) decodeUser :: (MonadThrow m) => UTCTime -> UserDefaultConf -> UpsertCampusUserMode -> Ldap.AttrList [] -> m (User,_)
decodeUser now UserDefaultConf{..} upsertMode ldapData = do decodeUser now UserDefaultConf{..} upsertMode ldapData = do
let let
userTelephone = decodeLdap ldapUserTelephone <&> canonicalPhone userTelephone = decodeLdap ldapUserTelephone
userMobile = decodeLdap ldapUserMobile <&> canonicalPhone userMobile = decodeLdap ldapUserMobile
userCompanyPersonalNumber = decodeLdap ldapUserFraportPersonalnummer userCompanyPersonalNumber = decodeLdap ldapUserFraportPersonalnummer
userCompanyDepartment = decodeLdap ldapUserFraportAbteilung userCompanyDepartment = decodeLdap ldapUserFraportAbteilung
@ -266,7 +267,7 @@ decodeUser now UserDefaultConf{..} upsertMode ldapData = do
-- -> return $ CI.mk userEmail -- -> return $ CI.mk userEmail
| otherwise | otherwise
-> throwM CampusUserInvalidEmail -> throwM CampusUserInvalidEmail
userLdapPrimaryKey <- if userLdapPrimaryKey <- if
| [bs] <- ldapMap !!! ldapPrimaryKey | [bs] <- ldapMap !!! ldapPrimaryKey
, Right userLdapPrimaryKey'' <- Text.decodeUtf8' bs , Right userLdapPrimaryKey'' <- Text.decodeUtf8' bs
@ -305,13 +306,13 @@ decodeUser now UserDefaultConf{..} upsertMode ldapData = do
, userPrefersPostal = userDefaultPrefersPostal , userPrefersPostal = userDefaultPrefersPostal
, .. , ..
} }
userUpdate = userUpdate =
[ UserLastAuthentication =. Just now | isLogin ] ++ [ UserLastAuthentication =. Just now | isLogin ] ++
[ UserEmail =. userEmail | validEmail' userEmail ] ++ [ UserEmail =. userEmail | validEmail' userEmail ] ++
[ [
-- UserDisplayName =. userDisplayName -- not updated here, since users are allowed to change their DisplayName; see line 191 -- UserDisplayName =. userDisplayName -- not updated here, since users are allowed to change their DisplayName; see line 272
UserFirstName =. userFirstName UserFirstName =. userFirstName
, UserSurname =. userSurname , UserSurname =. userSurname
, UserLastLdapSynchronisation =. Just now , UserLastLdapSynchronisation =. Just now
, UserLdapPrimaryKey =. userLdapPrimaryKey , UserLdapPrimaryKey =. userLdapPrimaryKey
, UserMobile =. userMobile , UserMobile =. userMobile

View File

@ -9,9 +9,9 @@ module Foundation.Yesod.ErrorHandler
import Import.NoFoundation hiding (errorHandler) import Import.NoFoundation hiding (errorHandler)
import Foundation.Type import Foundation.Type
import Foundation.I18n -- import Foundation.I18n
import Foundation.Authorization import Foundation.Authorization
import Foundation.SiteLayout -- import Foundation.SiteLayout
import Foundation.Routes import Foundation.Routes
import Foundation.DB import Foundation.DB
@ -20,15 +20,15 @@ import qualified Data.Text as Text
import qualified Network.Wai as W import qualified Network.Wai as W
import System.Exit -- DEBUG: just for testing -- import System.Exit -- DEBUG: just for testing
import System.Posix.Process -- DEBUG: just for testing -- import System.Posix.Process -- DEBUG: just for testing
errorHandler :: ( MonadSecretBox (HandlerFor UniWorX) errorHandler :: ( MonadSecretBox (HandlerFor UniWorX)
, MonadSecretBox (WidgetFor UniWorX) -- , MonadSecretBox (WidgetFor UniWorX)
, MonadSecretBox (ExceptT EncodedSecretBoxException (HandlerFor UniWorX)) , MonadSecretBox (ExceptT EncodedSecretBoxException (HandlerFor UniWorX))
, MonadAuth (HandlerFor UniWorX) , MonadAuth (HandlerFor UniWorX)
, BearerAuthSite UniWorX , BearerAuthSite UniWorX
, YesodPersistBackend UniWorX ~ SqlBackend -- , YesodPersistBackend UniWorX ~ SqlBackend
) )
=> ErrorResponse -> HandlerFor UniWorX TypedContent => ErrorResponse -> HandlerFor UniWorX TypedContent
errorHandler err = do errorHandler err = do
@ -72,39 +72,39 @@ errorHandler err = do
setSessionJson SessionError sessErr setSessionJson SessionError sessErr
selectRep $ do selectRep $ do
provideRep $ do -- provideRep $ do
mr <- getMessageRender -- mr <- getMessageRender
let -- let
encrypted :: Text -> WidgetFor UniWorX () -> WidgetFor UniWorX () -- encrypted :: Text -> WidgetFor UniWorX () -> WidgetFor UniWorX ()
encrypted plaintextJson plaintext = do -- encrypted plaintextJson plaintext = do
let displayEncrypted ciphertext = -- let displayEncrypted ciphertext =
[whamlet| -- [whamlet|
$newline never -- $newline never
<p>_{MsgErrorResponseEncrypted} -- <p>_{MsgErrorResponseEncrypted}
<pre .literal-error> -- <pre .literal-error>
#{ciphertext} -- #{ciphertext}
|] -- |]
if -- if
| isEncrypted && shouldEncrypt -> displayEncrypted plaintextJson -- | isEncrypted && shouldEncrypt -> displayEncrypted plaintextJson
| shouldEncrypt -> displayEncrypted =<< encodedSecretBox SecretBoxPretty plaintextJson -- | shouldEncrypt -> displayEncrypted =<< encodedSecretBox SecretBoxPretty plaintextJson
| otherwise -> plaintext -- | otherwise -> plaintext
errPage = case err of -- errPage = case err of
NotFound -> [whamlet|<p>_{MsgErrorResponseNotFound}|] -- NotFound -> [whamlet|<p>_{MsgErrorResponseNotFound}|]
InternalError err' -- InternalError err'
| "Crash Button" `isPrefixOf` err' -> liftIO $ exitImmediately ExitSuccess -- DEBUG: just for Testing -- | "Crash Button" `isPrefixOf` err' -> liftIO $ exitImmediately ExitSuccess -- DEBUG: just for Testing
| otherwise -> encrypted err' [whamlet|<p .literal-error>#{fromMaybe err' decrypted}|] -- | otherwise -> encrypted err' [whamlet|<p .literal-error>#{fromMaybe err' decrypted}|]
InvalidArgs errs -> [whamlet| -- InvalidArgs errs -> [whamlet|
<ul> -- <ul>
$forall err' <- errs -- $forall err' <- errs
<li .literal-error> -- <li .literal-error>
#{err'} -- #{err'}
|] -- |]
NotAuthenticated -> [whamlet|<p>_{MsgErrorResponseNotAuthenticated}|] -- NotAuthenticated -> [whamlet|<p>_{MsgErrorResponseNotAuthenticated}|]
PermissionDenied err' -> [whamlet|<p .errMsg>#{err'}|] -- PermissionDenied err' -> [whamlet|<p .errMsg>#{err'}|]
BadMethod method -> [whamlet|<p>_{MsgErrorResponseBadMethod (decodeUtf8 method)}|] -- BadMethod method -> [whamlet|<p>_{MsgErrorResponseBadMethod (decodeUtf8 method)}|]
siteLayout (toWgt . mr $ ErrorResponseTitle err) $ do -- siteLayout (toWgt . mr $ ErrorResponseTitle err) $ do
errPage -- errPage
provideRep $ case err of provideRep $ case err of
PermissionDenied err' -> return err' PermissionDenied err' -> return err'
InternalError err' InternalError err'

View File

@ -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 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,29 +8,24 @@ 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.DateTime
import Handler.Utils
import Handler.Utils.Avs import Handler.Utils.Avs
import Handler.Utils.Widgets
import Handler.Utils.Users import Handler.Utils.Users
-- import Handler.Utils.Company import Handler.Utils.Qualification
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
@ -39,39 +34,45 @@ 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.Ldap 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 cutOffPrintDays = 7
cutOffOldTime = toMidnight $ addDays (-cutOffOldDays) nowaday cutOffPrintJob = addLocalDays (-cutOffPrintDays) now
(usersAreReachable, driversHaveAvsIds, rDriversHaveFs, noStalePrintJobs, noBadAPCids, noAvsSynchProblems) <- runDB $ (,,,,,)
<$> areAllUsersReachable
<*> allDriversHaveAvsId now
<*> allRDriversHaveFs now
<*> (not <$> exists [PrintJobAcknowledged ==. Nothing, PrintJobCreated <=. cutOffPrintJob])
<*> (not <$> exists [PrintAcknowledgeProcessed ==. False])
<*> (not <$> exists [UserAvsLastSynchError !=. Nothing])
diffLics <- try retrieveDifferingLicences >>= \case
-- (Left (UnsupportedContentType "text/html" resp)) -> Left $ text2widget "Html received"
(Left e) -> return $ Left $ text2widget $ tshow (e :: SomeException)
(Right AvsLicenceDifferences{..}) -> do
let problemIds = avsLicenceDiffRevokeAll <> avsLicenceDiffGrantVorfeld <> avsLicenceDiffRevokeRollfeld <> avsLicenceDiffGrantRollfeld
forM_ (take 42 $ Set.toList problemIds) $ queueJob' . flip JobSynchroniseAvsId (Just nowaday)
return $ Right
( Set.size avsLicenceDiffRevokeAll
, Set.size avsLicenceDiffGrantVorfeld
, Set.size avsLicenceDiffRevokeRollfeld
, Set.size avsLicenceDiffGrantRollfeld
)
-- 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)
-- diffLics <- (procDiffLics <$> retrieveDifferingLicences) `catches`
-- [ Catch.Handler (\case (UnsupportedContentType "text/html;charset=utf-8" Response{responseBody})
-- -> return $ Left $ toWidget $ preEscapedToHtml $ fromRight "Response UTF8-decoding error" $ LBS.decodeUtf8' responseBody
-- ex -> return $ Left $ text2widget $ tshow ex)
-- , Catch.Handler (\(ex::SomeException) -> return $ Left $ text2widget $ tshow ex)
-- ]
-- we abuse messageTooltip for colored icons here -- we abuse messageTooltip for colored icons here
msgSuccessTooltip <- messageI Success MsgMessageSuccess msgSuccessTooltip <- messageI Success MsgMessageSuccess
msgWarningTooltip <- messageI Warning MsgMessageWarning msgWarningTooltip <- messageI Warning MsgMessageWarning
@ -81,97 +82,23 @@ handleAdminProblems mbProblemTable = do
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 $ (,,,,,)
<$> areAllUsersReachable
<*> allDriversHaveAvsId now
<*> allRDriversHaveFs now
<*> (not <$> exists [PrintJobAcknowledged ==. Nothing, PrintJobCreated <. cutOffOldTime])
<*> (not <$> exists [PrintAcknowledgeProcessed ==. False])
<*> mkInterfaceLogTable mempty
let interfacesBadNr = length $ filter (not . snd) interfaceOks
-- interfacesOk = all snd interfaceOks
diffLics <- try retrieveDifferingLicences >>= \case
-- (Left (UnsupportedContentType "text/html" resp)) -> Left $ text2widget "Html received"
(Left e) -> return $ Left $ text2widget $ tshow (e :: SomeException)
(Right (AvsLicenceDifferences{..},_)) -> do
let problemIds = avsLicenceDiffRevokeAll <> avsLicenceDiffGrantVorfeld <> avsLicenceDiffRevokeRollfeld <> avsLicenceDiffGrantRollfeld
void $ runDB $ queueAvsUpdateByAID problemIds $ Just nowaday
return $ Right
( Set.size avsLicenceDiffRevokeAll
, Set.size avsLicenceDiffGrantVorfeld
, Set.size avsLicenceDiffRevokeRollfeld
, Set.size avsLicenceDiffGrantRollfeld
)
-- 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)
-- diffLics <- (procDiffLics . fst <$> retrieveDifferingLicences) `catches`
-- [ Catch.Handler (\case (UnsupportedContentType "text/html;charset=utf-8" Response{responseBody})
-- -> return $ Left $ toWidget $ preEscapedToHtml $ fromRight "Response UTF8-decoding error" $ LBS.decodeUtf8' responseBody
-- ex -> return $ Left $ text2widget $ tshow ex)
-- , Catch.Handler (\(ex::SomeException) -> return $ Left $ text2widget $ tshow ex)
-- ]
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 +106,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 +121,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 +137,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 +179,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 +198,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 +220,21 @@ 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

View File

@ -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
@ -656,31 +548,19 @@ mkLicenceTable apidStatus rsChanged dbtIdent aLic apids = do
[ dbSelect (applying _2) id $ return . view (resultUserAvs . _userAvsPersonId) [ dbSelect (applying _2) id $ return . view (resultUserAvs . _userAvsPersonId)
-- (\DBRow{dbrOutput=(_,_,apid,_)} -> return apid -- return . view resultAvsPID) -- does not type due to traversal -- (\DBRow{dbrOutput=(_,_,apid,_)} -> return apid -- return . view resultAvsPID) -- does not type due to traversal
, colUserNameLink AdminUserR , colUserNameLink AdminUserR
, sortable (Just "avspersonno") (i18nCell MsgAvsPersonNo) $ \(view resultUserAvs -> a) -> avsPersonNoLinkedCellAdmin a , sortable (Just "avspersonno") (i18nCell MsgAvsPersonNo) $ \(view resultUserAvs -> a) -> avsPersonNoLinkedCell 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.^. CompanyName, usrComp E.^. UserCompanySupervisor)
let icnSuper = toWidget $ text2markup " " <> icon IconSupervisor let companies = intersperse (text2markup ", ") $
companies = (\(E.Value cmpName, E.Value cmpSpr) -> text2markup (CI.original cmpName) <> bool mempty icnSuper cmpSpr) <$> companies'
(\(E.Value cmpSh, E.Value cmpName, E.Value cmpSpr) -> simpleLink (citext2widget cmpName) (FirmUsersR cmpSh) <> bool mempty icnSuper cmpSpr) <$> companies' icnSuper = text2markup " " <> icon IconSupervisor
pure $ toWgt $ mconcat companies
pure $ intercalate (text2widget "; ") companies , sortable (Just "qualification") (i18nCell MsgTableQualifications) $ \(preview resultQualification -> q) -> cellMaybe lmsShortCell q
, sortable (Just "qualification") (i18nCell MsgTableQualifications) $
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 +604,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 +629,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 +676,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 +739,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)
@ -1015,7 +751,7 @@ getProblemAvsErrorR = do
dbtRowKey = qerryUsrAvs >>> (E.^. UserAvsId) dbtRowKey = qerryUsrAvs >>> (E.^. UserAvsId)
dbtProj = dbtProjId dbtProj = dbtProjId
dbtColonnade = dbColonnade $ mconcat dbtColonnade = dbColonnade $ mconcat
[ colUserNameModalHdrAdmin MsgLmsUser AdminUserR [ colUserNameModalHdr MsgLmsUser AdminUserR
, sortable (Just "avs-nr") (i18nCell MsgAvsPersonNo) , sortable (Just "avs-nr") (i18nCell MsgAvsPersonNo)
$ avsPersonNoLinkedCell . view reserrUsrAvs $ avsPersonNoLinkedCell . view reserrUsrAvs
, sortable Nothing (i18nCell MsgAvsPersonId) , sortable Nothing (i18nCell MsgAvsPersonId)
@ -1051,3 +787,4 @@ getProblemAvsErrorR = do
siteLayoutMsg MsgMenuAvsSynchError $ do siteLayoutMsg MsgMenuAvsSynchError $ do
setTitleI MsgMenuAvsSynchError setTitleI MsgMenuAvsSynchError
[whamlet|^{avsSyncErrTbl}|] [whamlet|^{avsSyncErrTbl}|]

View File

@ -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

View File

@ -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")
@ -332,30 +327,19 @@ postAdminTestR = do
<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

View File

@ -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")

View File

@ -64,10 +64,8 @@ postCCommR tid ssh csh = do
return (cid, tuts, exams, sheets) return (cid, tuts, exams, sheets)
let heading = SomeMessage . prependCourseTitle tid ssh csh $ SomeMessage MsgCommCourseHeading
commR CommunicationRoute commR CommunicationRoute
{ crHeading = heading { crHeading = SomeMessage . prependCourseTitle tid ssh csh $ SomeMessage MsgCommCourseHeading
, crTitle = heading
, crUltDest = SomeRoute $ CourseR tid ssh csh CCommR , crUltDest = SomeRoute $ CourseR tid ssh csh CCommR
, crJobs = crJobsCourseCommunication cid , crJobs = crJobsCourseCommunication cid
, crTestJobs = crTestJobsCourseCommunication cid , crTestJobs = crTestJobsCourseCommunication cid

View File

@ -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
@ -319,12 +279,9 @@ getCourseNewR = do
, E.desc $ courseCreated course] -- most recent created course , E.desc $ courseCreated course] -- most recent created course
E.limit 1 E.limit 1
return course return course
template <- case oldCourses of template <- case listToMaybe oldCourses of
(oldTemplate:_) -> runDB $ do (Just 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
@ -332,11 +289,11 @@ getCourseNewR = do
, cfRegTo = Nothing , cfRegTo = Nothing
, cfDeRegUntil = Nothing , cfDeRegUntil = Nothing
} }
[] -> do Nothing -> 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

View File

@ -226,16 +226,7 @@ getCourseListR = do
] ]
validator = def validator = def
& defaultSorting [SortDescBy "term",SortAscBy "course"] & defaultSorting [SortDescBy "term",SortAscBy "course"]
now <- liftIO getCurrentTime coursesTable <- runDB $ makeCourseTable colonnade validator
coursesTable <- runDB $ do
activeTs <- selectList [TermActiveFrom <=. now
, FilterOr [TermActiveTo >. Just now, TermActiveTo ==. Nothing]
, FilterOr [TermActiveFor ==. muid, TermActiveFor ==. Nothing] -- TermActiveFor <-. [Nothing, muid] did not work as intended
] [Desc TermActiveTerm]
let addTermFilter = if null activeTs
then id
else defaultFilter $ singletonMap "term" [toPathPiece termActiveTerm | Entity _ TermActive{termActiveTerm} <- activeTs]
makeCourseTable colonnade (validator & addTermFilter)
defaultLayout $ do defaultLayout $ do
setTitleI MsgCourseListTitle setTitleI MsgCourseListTitle
$(widgetFile "courses") $(widgetFile "courses")

View File

@ -192,37 +192,26 @@ handleAddUserR tid ssh csh tdesc ttyp = do
currentRoute <- fromMaybe (error "postCAddUserR called from 404-handler") <$> getCurrentRoute currentRoute <- fromMaybe (error "postCAddUserR called from 404-handler") <$> getCurrentRoute
(_ , registerConfirmResult) <- runButtonForm FIDCourseRegisterConfirm confirmedActs :: Set CourseRegisterActionData <- fmap Set.fromList . throwExceptT . mapMM encodedSecretBoxOpen . lookupPostParams $ toPathPiece PostCourseUserAddConfirmAction
-- $logDebugS "***AbortProblem***" $ tshow registerConfirmResult -- $logDebugS "CAddUserR confirmedActs" . tshow $ Set.map Aeson.encode confirmedActs
prefillUsers <- case registerConfirmResult of unless (Set.null confirmedActs) $ do -- TODO: check that all acts are member of availableActs
Nothing -> return mempty let
(Just BtnCourseRegisterAbort) -> do users = Map.fromList . fmap (\act -> (crActIdent act, Just . view _1 $ crActUser act)) $ Set.toList confirmedActs
addMessageI Warning MsgAborted tutActs = Set.filter (is _CourseRegisterActionAddTutorialMemberData) confirmedActs
-- prefill confirmed users for convenience. Note that Browser-Back may also return to the filled form, but history.back() does not in Chrome actTutorial = crActTutorial <$> Set.lookupMin tutActs -- tutorial ident must be the same for every added member!
confirmedActs :: [CourseRegisterActionData] <- exceptT (const $ return mempty) return . mapMM encodedSecretBoxOpen . lookupPostParams $ toPathPiece PostCourseUserAddConfirmAction -- ignore any exception, since it is only used to prefill a form field for convenience registeredUsers <- registerUsers cid users
return $ Just $ Set.fromList $ fmap crActIdent confirmedActs whenIsJust actTutorial $ \(tutName,tutType,tutDay) -> do
(Just BtnCourseRegisterConfirm) -> do whenIsJust (tutName <|> fmap (tutorialDefaultName tutType) tutDay) $ \tName -> do
confirmedActs :: Set CourseRegisterActionData <- fmap Set.fromList . throwExceptT . mapMM encodedSecretBoxOpen . lookupPostParams $ toPathPiece PostCourseUserAddConfirmAction tutId <- upsertNewTutorial cid tName tutType tutDay
-- $logDebugS "CAddUserR confirmedActs" . tshow $ Set.map Aeson.encode confirmedActs registerTutorialMembers tutId registeredUsers
unless (Set.null confirmedActs) $ do -- TODO: check that all acts are member of availableActs -- when (Set.size tutActs == Set.size confirmedActs) $ -- not sure how this condition might be false at this point
let redirect $ CTutorialR tid ssh csh tName TUsersR
users = Map.fromList . fmap (\act -> (crActIdent act, Just . view _1 $ crActUser act)) $ Set.toList confirmedActs redirect $ CourseR tid ssh csh CUsersR
tutActs = Set.filter (is _CourseRegisterActionAddTutorialMemberData) confirmedActs
actTutorial = crActTutorial <$> Set.lookupMin tutActs -- tutorial ident must be the same for every added member!
registeredUsers <- registerUsers cid users
whenIsJust actTutorial $ \(tutName,tutType,tutDay) -> do
whenIsJust (tutName <|> fmap (tutorialDefaultName tutType) tutDay) $ \tName -> do
tutId <- upsertNewTutorial cid tName tutType tutDay
registerTutorialMembers tutId registeredUsers
-- when (Set.size tutActs == Set.size confirmedActs) $ -- not sure how this condition might be false at this point
redirect $ CTutorialR tid ssh csh tName TUsersR
redirect $ CourseR tid ssh csh CUsersR
return mempty
((usersToAdd :: FormResult AddUserRequest, formWgt), formEncoding) <- runFormPost . identifyForm FIDCourseRegister . renderWForm FormStandard $ do ((usersToAdd :: FormResult AddUserRequest, formWgt), formEncoding) <- runFormPost . renderWForm FormStandard $ do
let tutTypesMsg = [(SomeMessage tt,tt) | tt <- tutTypes] let tutTypesMsg = [(SomeMessage tt,tt) | tt <- tutTypes]
tutDefType = ttyp >>= (\ty -> if ty `elem` tutTypes then Just ty else Nothing) tutDefType = ttyp >>= (\ty -> if ty `elem` tutTypes then Just ty else Nothing)
auReqUsers <- wreq (textField & cfAnySeparatedSet) (fslI MsgCourseParticipantsRegisterUsersField & setTooltip MsgCourseParticipantsRegisterUsersFieldTip) prefillUsers auReqUsers <- wreq (textField & cfAnySeparatedSet) (fslI MsgCourseParticipantsRegisterUsersField & setTooltip MsgCourseParticipantsRegisterUsersFieldTip) mempty
auReqTutorial <- optionalActionW auReqTutorial <- optionalActionW
( (,,) ( (,,)
<$> aopt (textField & cfStrip & cfCI & addDatalist tutNameSuggestions) <$> aopt (textField & cfStrip & cfCI & addDatalist tutNameSuggestions)

View File

@ -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

View File

@ -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

View File

@ -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
@ -420,12 +420,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
@ -660,7 +660,7 @@ postCUsersR tid ssh csh = do
, pure . cap' $ colUserNameLink (CourseR tid ssh csh . CUserR) , pure . cap' $ colUserNameLink (CourseR tid ssh csh . CUserR)
, guardOn showSex . cap' $ colUserSex' , guardOn showSex . cap' $ colUserSex'
, pure . cap' $ colUserEmail , pure . cap' $ colUserEmail
, pure . cap' $ colUserMatriclenr False , pure . cap' $ colUserMatriclenr
, pure . cap' $ colUserQualifications nowaday , pure . cap' $ colUserQualifications nowaday
, guardOn hasSubmissionGroups $ cap' colUserSubmissionGroup , guardOn hasSubmissionGroups $ cap' colUserSubmissionGroup
, guardOn hasTutorials . cap' $ colUserTutorials tid ssh csh , guardOn hasTutorials . cap' $ colUserTutorials tid ssh csh
@ -739,7 +739,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 +763,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

View File

@ -484,7 +484,7 @@ postEUsersR tid ssh csh examn = do
dbtColonnade = mconcat $ catMaybes dbtColonnade = mconcat $ catMaybes
[ pure $ dbSelect (_2 . applying _2) _1 $ return . view (resultExamRegistration . _entityKey) [ pure $ dbSelect (_2 . applying _2) _1 $ return . view (resultExamRegistration . _entityKey)
, pure $ colUserNameLink (CourseR tid ssh csh . CUserR) , pure $ colUserNameLink (CourseR tid ssh csh . CUserR)
, pure $ colUserMatriclenr False , pure colUserMatriclenr
, pure $ colStudyFeatures resultStudyFeatures , pure $ colStudyFeatures resultStudyFeatures
, pure $ sortable (Just "occurrence") (i18nCell MsgTableExamOccurrence) $ maybe mempty (anchorCell' (\n -> CExamR tid ssh csh examn EShowR :#: [st|exam-occurrence__#{n}|]) id . examOccurrenceName . entityVal) . view _userTableOccurrence , pure $ sortable (Just "occurrence") (i18nCell MsgTableExamOccurrence) $ maybe mempty (anchorCell' (\n -> CExamR tid ssh csh examn EShowR :#: [st|exam-occurrence__#{n}|]) id . examOccurrenceName . entityVal) . view _userTableOccurrence
, guardOn showPasses $ sortable Nothing (i18nCell MsgAchievedPasses) $ \(view $ resultUser . _entityKey -> uid) -> , guardOn showPasses $ sortable Nothing (i18nCell MsgAchievedPasses) $ \(view $ resultUser . _entityKey -> uid) ->

File diff suppressed because it is too large Load Diff

View File

@ -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)

View File

@ -1,402 +0,0 @@
-- SPDX-FileCopyrightText: 2024 Steffen Jost <s.jost@fraport.de>
--
-- SPDX-License-Identifier: AGPL-3.0-or-later
module Handler.Health.Interface
(
getHealthInterfaceR
, mkInterfaceLogTable
, runInterfaceChecks
, getConfigInterfacesR, postConfigInterfacesR
)
where
import Import
import qualified Data.Set as Set
import qualified Data.Map as Map
import qualified Data.Text as Text
import Handler.Utils
import Handler.Utils.Concurrent
-- import Database.Esqueleto.Experimental ((:&)(..))
import qualified Database.Esqueleto.Experimental as E
import qualified Database.Esqueleto.Utils as E
import qualified Database.Esqueleto.Legacy as EL (on)
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
wc2null :: Text -> Maybe Text
-- wc2null "." = Nothing -- does not work, since dots are eliminated in URLs
-- wc2null "-" = Nothing -- used as wildcard subtype in lpr interface
wc2null "_" = Nothing
wc2null "*" = Nothing
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
pbool :: Text -> Maybe Bool
pbool (Text.toLower . Text.strip -> w)
| w `elem` ["1", "t", "true" ,"wahr", "w"] = Just True
| w `elem` ["0", "f", "false","falsch"] = Just False
| otherwise = Nothing
-- | parse UniqueInterfaceHealth with subtype and write arguments being optional for the last interface. Wildcards '_' or '.' are also allowed in all places.
identifyInterfaces :: [Text] -> [Unique InterfaceHealth]
identifyInterfaces [] = []
identifyInterfaces [i] = [UniqueInterfaceHealth i Nothing Nothing]
identifyInterfaces [i,s] = [UniqueInterfaceHealth i (wc2null s) Nothing]
identifyInterfaces (i:s:w:r) = UniqueInterfaceHealth i (wc2null s) (pbool w) : identifyInterfaces r
type ReqBanInterfaceHealth = ([Unique InterfaceHealth],[Unique InterfaceHealth])
-- | Interface names prefixed with '-' are to be excluded from the query
splitInterfaces :: [Unique InterfaceHealth] -> ReqBanInterfaceHealth
splitInterfaces = foldl' aux mempty
where
aux (reqs,bans) uih@(UniqueInterfaceHealth i s w)
| Just ('-', b) <- Text.uncons i = (reqs, UniqueInterfaceHealth b s w : bans)
| otherwise = (uih : reqs, bans)
-- | check whether the first argument is equal or more specialzed (i.e. more Just) than the second
matchesUniqueInterfaceHealth :: Unique InterfaceHealth -> Unique InterfaceHealth -> Bool
matchesUniqueInterfaceHealth (UniqueInterfaceHealth ai as aw) (UniqueInterfaceHealth bi bs bw) = ai == bi && eqOrNothing as bs && eqOrNothing aw bw
where
eqOrNothing _ Nothing = True
eqOrNothing a b = a == b
getHealthInterfaceR :: [Text] -> Handler TypedContent
getHealthInterfaceR (dropWhile (=="force") -> ris) = do -- for backwards compatibility we ignore leading "force"
let interfs = splitInterfaces $ identifyInterfaces ris
(missing, allok, res, iltable) <- runInterfaceLogTable interfs
when missing notFound -- send 404 if any requested interface was not found
let ihstatus = if allok then status200
else internalServerError500
plainMsg = if allok then "Interfaces are healthy."
else "Unhealthy interfaces: " <> Text.intercalate ", " [iface | (iface, False) <- res]
sendResponseStatus ihstatus <=< selectRep $ do -- most browsers send accept:text/html, thus text/plain can be default here
provideRep . return . RepPlain $ toContent plainMsg -- /?_accept=text/plain
provideRep . siteLayoutMsg MsgMenuHealthInterface $ do -- /?_accept=text/html
setTitleI MsgMenuHealthInterface
[whamlet|
<div>
#{plainMsg}
<div>
^{iltable}
|]
runInterfaceLogTable :: ReqBanInterfaceHealth -> Handler (Bool, Bool, [(Text,Bool)], Widget)
runInterfaceLogTable interfs@(reqIfs,_) = do
(res, twgt) <- runDB $ mkInterfaceLogTable interfs
let missing = notNull [ifce | (UniqueInterfaceHealth ifce _subt _writ) <- reqIfs, ifce `notElem` (fst <$> res) ]
allok = all snd res
return (missing, allok, res, twgt)
-- ihDebugShow :: Unique InterfaceHealth -> Text
-- ihDebugShow (UniqueInterfaceHealth i s w) = "(" <> tshow i <> "," <> tshow s <> "," <> tshow w <> ")"
mkInterfaceLogTable :: ReqBanInterfaceHealth -> DB ([(Text,Bool)], Widget)
mkInterfaceLogTable interfs@(reqIfs, banIfs) = do
-- $logWarnS "DEBUG" $ tshow ([ihDebugShow x | x<- reqIfs], [ihDebugShow x | x<- banIfs])
flagError <- liftHandler $ do
void $ timeoutHandler 42000001 $ runDB $ runInterfaceChecks interfs -- ensure interface checkc are up to date
mkErrorFlag
now <- liftIO getCurrentTime
dbTableDB ilvalidator DBTable{dbtColonnade=colonnade now flagError, ..}
where
sanitize = text2AlphaNumPlus ['+','-','_','Ä','Ö','Ü','ß','ä','ö','ü']
dbtIdent = "interface-log" :: Text
dbtProj = dbtProjId
dbtSQLQuery (ilog `E.LeftOuterJoin` ihealth) = do
EL.on ( ilog E.^. InterfaceLogInterface E.=?. ihealth E.?. InterfaceHealthInterface
E.&&. ilog E.^. InterfaceLogSubtype E.=~. E.joinV (ihealth E.?. InterfaceHealthSubtype)
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
[ E.and $ catMaybes
[ ilog E.^. InterfaceLogInterface E.==. E.val (sanitize ifce) & Just
, (ilog E.^. InterfaceLogSubtype E.==.) . E.val . sanitize <$> subt
, (ilog E.^. InterfaceLogWrite E.==.) . E.val <$> writ
]
| (UniqueInterfaceHealth ifce subt writ) <- crits
]
matchUIHnot crits = E.and
[ E.or $ catMaybes
[ ilog E.^. InterfaceLogInterface E.!=. E.val (sanitize ifce) & Just
, (ilog E.^. InterfaceLogSubtype E.!=.) . E.val . sanitize <$> subt
, (ilog E.^. InterfaceLogWrite E.!=.) . E.val <$> writ
]
| (UniqueInterfaceHealth ifce subt writ) <- crits
]
unless (null reqIfs) $ E.where_ $ matchUIH reqIfs
unless (null banIfs) $ E.where_ $ matchUIHnot banIfs
-- unless (null banIfs) $ E.where_ $ E.not_ $ matchUIH banIfs -- !!! DOES NOT WORK !!! Yields strange results, see #155
-- unless (null banIfs) $ E.where_ $ E.not_ $ E.parens $ matchUIH banIfs -- WORKS OKAY
-- 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_ $ 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
return (ilog, ihour)
queryILog :: (E.SqlExpr (Entity InterfaceLog) `E.LeftOuterJoin` E.SqlExpr (Maybe (Entity InterfaceHealth))) -> E.SqlExpr (Entity InterfaceLog)
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 = _dbrOutput . _1 . _entityVal
resultHours :: Lens' (DBRow (Entity InterfaceLog, E.Value Int)) Int
resultHours = _dbrOutput . _2 . E._unValue
dbtRowKey = queryILog >>> (E.^.InterfaceLogId)
colonnade now flagError = mconcat
[ sortable Nothing (i18nCell MsgInterfaceStatus) $ \row -> -- do
let hours = row ^. resultHours
-- defmsg = row ^? resultErrMsg
logtime = row ^. resultILog . _interfaceLogTime
success = row ^. resultILog . _interfaceLogSuccess
iface = row ^. resultILog . _interfaceLogInterface
status = (success || hours <= -100) && (hours < 0 || now <= addHours hours logtime)
in tellCell [(iface,status)] $ wgtCell $ flagError $ toMaybe (success || not status) status
, sortable (Just "interface") (i18nCell MsgInterfaceName ) $ \(view (resultILog . _interfaceLogInterface) -> n) -> textCell n
, sortable (Just "subtype") (i18nCell MsgInterfaceSubtype ) $ textCell . view (resultILog . _interfaceLogSubtype)
, sortable (Just "write") (i18nCell MsgInterfaceWrite ) $ (`ifIconCell` IconEdit) . view (resultILog . _interfaceLogWrite)
, sortable (Just "time") (i18nCell MsgInterfaceLastSynch ) $ dateTimeCell . view (resultILog . _interfaceLogTime)
, sortable (Just "hours") (i18nCell MsgInterfaceFreshness & cellTooltips [SomeMessage MsgInterfaceFreshnessTooltip, SomeMessage MsgTableDiffDaysTooltip]
) $ warnIntervalCell . view resultHours
, sortable (Just "rows") (i18nCell MsgTableRows ) $ cellMaybe numCell . view (resultILog . _interfaceLogRows)
, sortable (Just "success") (i18nCell MsgInterfaceSuccess ) $ \(view (resultILog . _interfaceLogSuccess) -> s) -> iconBoolCell s
, sortable Nothing (i18nCell MsgInterfaceInfo ) $ \(view resultILog -> ilt) -> case ilt of
InterfaceLog "AVS" "Synch" True _ _ i _ -> anchorCell ProblemAvsErrorR $ toWgt $ bool i "AVS-Log" $ null i
InterfaceLog "LPR" _ _ _ _ i _ -> anchorCell PrintLogR $ toWgt $ bool i "LPR-Log" $ null i
InterfaceLog _ _ _ _ _ i _ -> textCell i
]
dbtSorting = mconcat
[ singletonMap "interface" $ SortColumn $ queryILog >>> (E.^. InterfaceLogInterface)
, singletonMap "subtype" $ SortColumn $ queryILog >>> (E.^. InterfaceLogSubtype)
, singletonMap "write" $ SortColumn $ queryILog >>> (E.^. InterfaceLogWrite)
, singletonMap "time" $ SortColumn $ queryILog >>> (E.^. InterfaceLogTime)
, singletonMap "rows" $ SortColumn $ queryILog >>> (E.^. InterfaceLogRows)
, 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"]
dbtFilter = mempty
dbtFilterUI = mempty
dbtStyle = def
dbtParams = def
dbtCsvEncode = noCsvEncode
dbtCsvDecode = Nothing
dbtExtraReps = []
-- | runs additional checks and logs results within InterfaceLogTable; assumed to executable within a handler call
runInterfaceChecks :: ReqBanInterfaceHealth -> DB ()
runInterfaceChecks interfs = do
avsInterfaceCheck interfs
lprAckCheck interfs
maybeRunCheck :: ReqBanInterfaceHealth -> Unique InterfaceHealth -> (UTCTime -> DB ()) -> DB ()
maybeRunCheck (reqIfs,banIfs) uih act
| null reqIfs || any (matchesUniqueInterfaceHealth uih) reqIfs
, null banIfs || not (any (matchesUniqueInterfaceHealth uih) banIfs) = do
mih <- getBy uih
whenIsJust mih $ \eih -> do
now <- liftIO getCurrentTime
act $ addHours (negate $ interfaceHealthHours $ entityVal eih) now
| otherwise = return ()
lprAckCheck :: ReqBanInterfaceHealth -> DB ()
lprAckCheck interfs = maybeRunCheck interfs (UniqueInterfaceHealth "Printer" (Just "Acknowledge") (Just True)) $ \cutOffOldTime -> do
unproc <- selectList [PrintAcknowledgeTimestamp <. cutOffOldTime, PrintAcknowledgeProcessed ==. False] []
if notNull unproc
then mkLog False (Just $ length unproc) "Long unprocessed APC-Idents exist"
else do
oks <- E.deleteWhereCount [PrintAcknowledgeTimestamp <. cutOffOldTime, PrintAcknowledgeProcessed ==. True]
if oks > 0
then mkLog True (Just $ fromIntegral oks) "Long processed APC-Idents removed"
else mkLog True Nothing mempty
where
mkLog = logInterface' "Printer" "Acknowledge" True
avsInterfaceCheck :: ReqBanInterfaceHealth -> DB ()
avsInterfaceCheck interfs = maybeRunCheck interfs (UniqueInterfaceHealth "AVS" (Just "Synch") (Just True)) $ \cutOffOldTime -> do
avsSynchStats <- E.select $ do
uavs <- E.from $ E.table @UserAvs
E.where_ $ uavs E.^. UserAvsLastSynch E.>. E.val cutOffOldTime
let isOk = E.isNothing (uavs E.^. UserAvsLastSynchError)
E.groupBy isOk
E.orderBy [E.descNullsLast isOk]
return (isOk, E.countRows, E.max_ $ uavs E.^. UserAvsLastSynch)
let
mkBadInfo badRows (fromMaybe cutOffOldTime -> badTime) | badRows > 0 = do
fmtCut <- formatTime SelFormatDate cutOffOldTime
fmtBad <- formatTime SelFormatDateTime badTime
return $ tshow badRows <> " Fehler seit " <> fmtCut <> ", zuletzt um " <> fmtBad
mkBadInfo _ _ = return mempty
writeAvsSynchStats okRows badInfo =
logInterface' "AVS" "Synch" True (null badInfo) okRows badInfo
--case $(unValueN 3) <$> avsSynchStats of
case avsSynchStats of
((E.Value True , E.Value okRows, E.Value _okTime):(E.Value False, E.Value badRows, E.Value badTime):_) ->
writeAvsSynchStats (Just okRows) =<< mkBadInfo badRows badTime
((E.Value True , E.Value okRows, E.Value _okTime):_) ->
writeAvsSynchStats (Just okRows) mempty
((E.Value False, E.Value badRows, E.Value badTime):_) ->
-- lastOk <- userAvsLastSynch . entityVal <<$>> selectFirst [UserAvsLastSynchError ==. Nothing] [Desc UserAvsLastSynch]
writeAvsSynchStats Nothing =<< mkBadInfo badRows badTime
_ -> 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")

View File

@ -1,4 +1,4 @@
-- SPDX-FileCopyrightText: 2022-2024 Felix Hamann <felix.hamann@campus.lmu.de>, Gregor Kleen <gregor@kleen.consulting>, Sarah Vaupel <sarah.vaupel@ifi.lmu.de>, Steffen Jost <jost@tcs.ifi.lmu.de>, Winnie Ros <winnie.ros@campus.lmu.de>, Steffen Jost <s.jost@fraport.de> -- SPDX-FileCopyrightText: 2022-2023 Felix Hamann <felix.hamann@campus.lmu.de>, Gregor Kleen <gregor@kleen.consulting>, 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
@ -13,12 +13,12 @@ import Data.Map ((!))
import qualified Data.CaseInsensitive as CI import qualified Data.CaseInsensitive as CI
import qualified Data.Set as Set import qualified Data.Set as Set
-- 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
import Development.GitRev import Development.GitRev
-- import Auth.LDAP (ADError(..), ADInvalidCredentials(..), CampusMessage(..)) import Auth.LDAP (ADError(..), ADInvalidCredentials(..), CampusMessage(..))
import Yesod.Auth.Message(AuthMessage(..)) import Yesod.Auth.Message(AuthMessage(..))
@ -175,7 +175,6 @@ showFAQ :: ( MonadAP m
, MonadThrow m , MonadThrow m
) )
=> Route UniWorX -> FAQItem -> m Bool => Route UniWorX -> FAQItem -> m Bool
showFAQ _ FAQLoginExpired = return True
showFAQ _ FAQNoCampusAccount = is _Nothing <$> maybeAuthId showFAQ _ FAQNoCampusAccount = is _Nothing <$> maybeAuthId
showFAQ (AuthR _) FAQCampusCantLogin = return True showFAQ (AuthR _) FAQCampusCantLogin = return True
showFAQ _ FAQCampusCantLogin = is _Nothing <$> maybeAuthId showFAQ _ FAQCampusCantLogin = is _Nothing <$> maybeAuthId
@ -184,20 +183,38 @@ showFAQ _ FAQForgottenPassword = is _Nothing <$> maybeAuthId
showFAQ _ FAQNotLecturerHowToCreateCourses showFAQ _ FAQNotLecturerHowToCreateCourses
= and2M (is _Just <$> maybeAuthId) = and2M (is _Just <$> maybeAuthId)
(not <$> hasWriteAccessTo CourseNewR) (not <$> hasWriteAccessTo CourseNewR)
-- showFAQ (CourseR tid ssh csh _) FAQCourseCorrectorsTutors showFAQ (CourseR tid ssh csh _) FAQCourseCorrectorsTutors
-- = and2M (is _Just <$> maybeAuthId) = and2M (is _Just <$> maybeAuthId)
-- (or2M (hasWriteAccessTo $ CourseR tid ssh csh SheetNewR) (or2M (hasWriteAccessTo $ CourseR tid ssh csh SheetNewR)
-- (hasWriteAccessTo $ CourseR tid ssh csh CTutorialNewR) (hasWriteAccessTo $ CourseR tid ssh csh CTutorialNewR)
-- ) )
-- showFAQ _ _ = return False showFAQ (CExamR tid ssh csh examn _) FAQExamPoints
= and2M (hasWriteAccessTo $ CExamR tid ssh csh examn EEditR)
noExamParts
where
noExamParts = liftHandler . runDB . E.selectNotExists . E.from $ \(examPart `E.InnerJoin` exam `E.InnerJoin` course) -> do
E.on $ course E.^. CourseId E.==. exam E.^. ExamCourse
E.on $ exam E.^. ExamId E.==. examPart E.^. ExamPartExam
E.where_ $ course E.^. CourseTerm E.==. E.val tid
E.&&. course E.^. CourseSchool E.==. E.val ssh
E.&&. course E.^. CourseShorthand E.==. E.val csh
E.&&. exam E.^. ExamName E.==. E.val examn
showFAQ _ FAQInvalidCredentialsAdAccountDisabled = maybeT (return False) $ do
guardM $ is _Nothing <$> maybeAuthId
sessionError <- MaybeT $ lookupSessionJson SessionError
guard $ sessionError == PermissionDenied (toPathPiece $ ADInvalidCredentials ADAccountDisabled)
return True
showFAQ _ _ = return False
prioFAQ :: Monad m prioFAQ :: Monad m
=> Route UniWorX -> FAQItem -> m Rational => Route UniWorX -> FAQItem -> m Rational
prioFAQ _ FAQLoginExpired = return 2
prioFAQ _ FAQNoCampusAccount = return 1 prioFAQ _ FAQNoCampusAccount = return 1
prioFAQ _ FAQCampusCantLogin = return 1 prioFAQ _ FAQCampusCantLogin = return 1
prioFAQ _ FAQForgottenPassword = return 1 prioFAQ _ FAQForgottenPassword = return 1
prioFAQ _ FAQNotLecturerHowToCreateCourses = return 1 prioFAQ _ FAQNotLecturerHowToCreateCourses = return 1
prioFAQ _ FAQCourseCorrectorsTutors = return 1
prioFAQ _ FAQExamPoints = return 2
prioFAQ _ FAQInvalidCredentialsAdAccountDisabled = return 3
getInfoLecturerR :: Handler Html getInfoLecturerR :: Handler Html

View File

@ -1,8 +1,9 @@
-- 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
{-# OPTIONS_GHC -fno-warn-orphans #-} -- needed for HasEntity instances {-# OPTIONS_GHC -fno-warn-orphans #-} -- needed for HasEntity instances
{-# OPTIONS -Wno-unused-top-binds #-} -- TODO: remove me, for debugging only
{-# LANGUAGE TypeApplications #-} {-# LANGUAGE TypeApplications #-}
module Handler.LMS module Handler.LMS
@ -11,7 +12,13 @@ module Handler.LMS
, getLmsR , postLmsR , getLmsR , postLmsR
, getLmsIdentR , getLmsIdentR
, getLmsEditR , postLmsEditR , getLmsEditR , postLmsEditR
-- V2 -- V1
, getLmsUsersR , getLmsUsersDirectR
, getLmsUserlistR , postLmsUserlistR
, getLmsUserlistUploadR , postLmsUserlistUploadR, postLmsUserlistDirectR
, getLmsResultR , postLmsResultR
, getLmsResultUploadR , postLmsResultUploadR , postLmsResultDirectR
-- V1
, getLmsLearnersR , getLmsLearnersDirectR , getLmsLearnersR , getLmsLearnersDirectR
, getLmsReportR , postLmsReportR , getLmsReportR , postLmsReportR
, getLmsReportUploadR , postLmsReportUploadR , postLmsReportDirectR , getLmsReportUploadR , postLmsReportUploadR , postLmsReportDirectR
@ -19,7 +26,7 @@ module Handler.LMS
, getLmsFakeR , postLmsFakeR , getLmsFakeR , postLmsFakeR
, getLmsUserR , getLmsUserR
, getLmsUserSchoolR , getLmsUserSchoolR
, getLmsUserAllR , getLmsUserAllR
) )
where where
@ -35,7 +42,7 @@ 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
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.Esqueleto.Experimental ((:&)(..)) import Database.Esqueleto.Experimental ((:&)(..))
import qualified Database.Esqueleto.Experimental as Ex -- needs TypeApplications Lang-Pragma import qualified Database.Esqueleto.Experimental as Ex -- needs TypeApplications Lang-Pragma
@ -44,6 +51,10 @@ 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
import Database.Persist.Sql (deleteWhereCount, updateWhereCount) import Database.Persist.Sql (deleteWhereCount, updateWhereCount)
-- V1
import Handler.LMS.Users as Handler.LMS
import Handler.LMS.Userlist as Handler.LMS
import Handler.LMS.Result as Handler.LMS
-- V2 -- V2
import Handler.LMS.Learners as Handler.LMS import Handler.LMS.Learners as Handler.LMS
import Handler.LMS.Report as Handler.LMS import Handler.LMS.Report as Handler.LMS
@ -65,7 +76,7 @@ embedRenderMessage ''UniWorX ''ButtonManualLms id
instance Button UniWorX ButtonManualLms where instance Button UniWorX ButtonManualLms where
btnClasses BtnLmsEnqueue = [BCIsButton, BCPrimary] btnClasses BtnLmsEnqueue = [BCIsButton, BCPrimary]
btnClasses BtnLmsDequeue = [BCIsButton, BCPrimary] btnClasses BtnLmsDequeue = [BCIsButton, BCDefault]
getLmsSchoolR :: SchoolId -> Handler Html getLmsSchoolR :: SchoolId -> Handler Html
@ -75,17 +86,16 @@ getLmsAllR, postLmsAllR :: Handler Html
getLmsAllR = postLmsAllR getLmsAllR = postLmsAllR
postLmsAllR = do postLmsAllR = do
isAdmin <- hasReadAccessTo AdminR isAdmin <- hasReadAccessTo AdminR
mbJLQenqueue <- getsYesod $ view _appJobLmsQualificationsEnqueueHour mbQcheck <- getsYesod $ view _appQualificationCheckHour
mbJLQdequeue <- getsYesod $ view _appJobLmsQualificationsDequeueHour
-- TODO: Move this functionality elsewhere without the need for `isAdmin` -- TODO: Move this functionality elsewhere without the need for `isAdmin`
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"
@ -100,7 +110,7 @@ postLmsAllR = do
view _2 <$> mkLmsAllTable isAdmin lmsDeletionDays view _2 <$> mkLmsAllTable isAdmin lmsDeletionDays
siteLayoutMsg MsgMenuLms $ do siteLayoutMsg MsgMenuLms $ do
setTitleI MsgMenuLms setTitleI MsgMenuLms
$(i18nWidgetFile "lms-all") $(widgetFile "lms-all")
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
@ -112,20 +122,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 +159,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 +185,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 +219,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 +241,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 +284,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 +319,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 +336,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 +352,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
@ -358,8 +360,9 @@ data LmsTableAction = LmsActNotify
| LmsActReset | LmsActReset
| LmsActRestart | LmsActRestart
deriving (Eq, Ord, Enum, Bounded, Read, Show, Generic) deriving (Eq, Ord, Enum, Bounded, Read, Show, Generic)
deriving anyclass (Universe, Finite)
instance Universe LmsTableAction
instance Finite LmsTableAction
nullaryPathPiece ''LmsTableAction $ camelToPathPiece' 2 nullaryPathPiece ''LmsTableAction $ camelToPathPiece' 2
embedRenderMessage ''UniWorX ''LmsTableAction id embedRenderMessage ''UniWorX ''LmsTableAction id
@ -368,12 +371,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 +406,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 +418,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 +434,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 csvName = T.replace " " "-" $ CI.original (quali ^. _qualificationName)
getCompanyName cid = maybe (unCompanyKey cid) companyName $ Map.lookup cid cmpMap -- use shorthand in case of impossible failure
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 +497,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 $ 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
) )
, 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 +550,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 +613,40 @@ 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)) [ if not isAdmin then mempty else dbSelect (applying _2) id (return . view (resultUser . _entityKey))
, colUserNameModalHdrAdmin MsgLmsUser AdminUserR , colUserNameModalHdr 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 icnSuper = text2markup " " <> icon IconSupervisor
, colUserMatriclenr isAdmin cs = [ (cmpName, cmpSpr)
-- , sortable (Just "validity") (i18nCell MsgQualificationValidIndicator) (qualificationValidIconCell nowaday . view resultQualUser) | Entity _ UserCompany{userCompanyCompany=cmpId, userCompanySupervisor=cmpSpr} <- cmps
, let cmpName = maybe (unCompanyKey cmpId) companyName $ Map.lookup cmpId cmpMap
]
companies = intercalate (text2markup ", ") $
(\(cmpName, cmpSpr) -> text2markup (CI.original cmpName) <> bool mempty icnSuper cmpSpr) <$> cs
in wgtCell companies
, colUserMatriclenr
-- , 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 +673,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 +695,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 +714,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 +747,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 +781,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 +811,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 +836,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 +846,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")

View File

@ -71,11 +71,11 @@ fakeQualificationUsers (Entity qid Qualification{qualificationRefreshWithin}) (u
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)]

View File

@ -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,10 +209,10 @@ 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
<* runDB (logInterface "LMS" (ciOriginal qsh) True (Just nr) "")
-- 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

View File

@ -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)
@ -204,6 +200,7 @@ mkReportTable sid qsh qid = do
, LmsReportLock =. lmsReportCsvLock actionData , LmsReportLock =. lmsReportCsvLock actionData
, LmsReportTimestamp =. eanow , LmsReportTimestamp =. eanow
] ]
-- audit $ Transaction.. (add to Audit.Types)
lift . queueDBJob $ JobLmsReports qid lift . queueDBJob $ JobLmsReports qid
return $ LmsReportR sid qsh return $ LmsReportR sid qsh
, dbtCsvRenderKey = const $ \case , dbtCsvRenderKey = const $ \case
@ -250,8 +247,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 +264,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 +277,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
@ -326,6 +296,7 @@ postLmsReportUploadR sid qsh = do
[whamlet|$newline never [whamlet|$newline never
<form method=post enctype=#{enctype}> <form method=post enctype=#{enctype}>
^{widget} ^{widget}
<p>
<input type=submit> <input type=submit>
|] |]
@ -339,22 +310,17 @@ 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 ""
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) ""
return (ok200, msg) return (ok200, msg)
[] -> do [] -> do
let msg = "Report upload file missing." let msg = "Report upload file missing."

293
src/Handler/LMS/Result.hs Normal file
View File

@ -0,0 +1,293 @@
-- SPDX-FileCopyrightText: 2022 Steffen Jost <jost@cip.ifi.lmu.de>,Steffen Jost <jost@tcs.ifi.lmu.de>
--
-- SPDX-License-Identifier: AGPL-3.0-or-later
{-# OPTIONS_GHC -fno-warn-orphans #-} -- needed for HasEntity instances
module Handler.LMS.Result
( getLmsResultR, postLmsResultR
, getLmsResultUploadR, postLmsResultUploadR
, postLmsResultDirectR
)
where
import Import
import Handler.Utils
import Handler.Utils.Csv
import Handler.Utils.LMS
import qualified Data.Map as Map
import qualified Data.Csv as Csv
import qualified Data.Conduit.List as C
import qualified Database.Esqueleto.Legacy as E
import qualified Database.Esqueleto.Utils as E
import Jobs.Queue
data LmsResultTableCsv = LmsResultTableCsv
{ csvLRTident :: LmsIdent
, csvLRTsuccess :: LmsDay
}
deriving Generic
makeLenses_ ''LmsResultTableCsv
-- csv without headers
instance Csv.ToRecord LmsResultTableCsv -- default suffices
instance Csv.FromRecord LmsResultTableCsv -- default suffices
-- csv with headers
lmsResultTableCsvHeader :: Csv.Header
lmsResultTableCsvHeader = Csv.header [ csvLmsIdent, csvLmsSuccess ]
instance ToNamedRecord LmsResultTableCsv where
toNamedRecord LmsResultTableCsv{..} = Csv.namedRecord
[ csvLmsIdent Csv..= csvLRTident
, csvLmsSuccess Csv..= csvLRTsuccess
]
instance FromNamedRecord LmsResultTableCsv where
parseNamedRecord (lsfHeaderTranslate -> csv)
= LmsResultTableCsv
<$> csv Csv..: csvLmsIdent
<*> csv Csv..: csvLmsSuccess
instance CsvColumnsExplained LmsResultTableCsv where
csvColumnsExplanations _ = mconcat
[ single csvLmsIdent MsgCsvColumnLmsIdent
, single csvLmsSuccess MsgCsvColumnLmsSuccess
]
where
single :: RenderMessage UniWorX msg => Csv.Name -> msg -> Map Csv.Name Widget
single k v = singletonMap k [whamlet|_{v}|]
data LmsResultCsvActionClass = LmsResultInsert | LmsResultUpdate
deriving (Eq, Ord, Read, Show, Generic, Enum, Bounded)
embedRenderMessage ''UniWorX ''LmsResultCsvActionClass id
-- By coincidence the action type is identical to LmsResultTableCsv
data LmsResultCsvAction = LmsResultInsertData { lmsResultInsertIdent :: LmsIdent, lmsResultInsertSuccess :: Day }
| LmsResultUpdateData { lmsResultInsertIdent :: LmsIdent, lmsResultInsertSuccess :: Day }
deriving (Eq, Ord, Read, Show, Generic)
deriveJSON defaultOptions
{ constructorTagModifier = camelToPathPiece'' 2 1 -- LmsResultInsertData -> insert
, fieldLabelModifier = camelToPathPiece' 2 -- lmsResultInsertIdent -> insert-ident | lmsResultInsertSuccess -> insert-success
, sumEncoding = TaggedObject "action" "data"
} ''LmsResultCsvAction
data LmsResultCsvException
= LmsResultCsvExceptionDuplicatedKey -- TODO: this is not used anywhere?!
deriving (Show, Generic)
instance Exception LmsResultCsvException
embedRenderMessage ''UniWorX ''LmsResultCsvException id
mkResultTable :: SchoolId -> QualificationShorthand -> QualificationId -> DB (Any, Widget)
mkResultTable sid qsh qid = do
now_day <- utctDay <$> liftIO getCurrentTime
dbtCsvName <- csvFilenameLmsResult qsh
let dbtCsvSheetName = dbtCsvName
let
resultDBTable = DBTable{..}
where
dbtSQLQuery lmsresult = do
E.where_ $ lmsresult E.^. LmsResultQualification E.==. E.val qid
return lmsresult
dbtRowKey = (E.^. LmsResultId)
dbtProj = dbtProjId
dbtColonnade = dbColonnade $ mconcat
[ sortable (Just csvLmsIdent) (i18nCell MsgTableLmsIdent) $ \(view $ _dbrOutput . _entityVal . _lmsResultIdent . _getLmsIdent -> ident) -> textCell ident
, sortable (Just csvLmsSuccess) (i18nCell MsgTableLmsSuccess) $ \(view $ _dbrOutput . _entityVal . _lmsResultSuccess -> success) -> dayCell success
, sortable (Just csvLmsTimestamp) (i18nCell MsgTableLmsReceived) $ \(view $ _dbrOutput . _entityVal . _lmsResultTimestamp -> timestamp) -> dateTimeCell timestamp
]
dbtSorting = Map.fromList
[ (csvLmsIdent , SortColumn (E.^. LmsResultIdent))
, (csvLmsSuccess , SortColumn (E.^. LmsResultSuccess))
, (csvLmsTimestamp, SortColumn (E.^. LmsResultTimestamp))
]
dbtFilter = Map.fromList
[ (csvLmsIdent , FilterColumn $ E.mkContainsFilterWith LmsIdent (E.^. LmsResultIdent))
, (csvLmsSuccess, FilterColumn $ E.mkExactFilter (E.^. LmsResultSuccess))
]
dbtFilterUI = \mPrev -> mconcat
[ prismAForm (singletonFilter csvLmsIdent . maybePrism _PathPiece) mPrev $ aopt (hoistField lift textField) (fslI MsgTableLmsIdent)
, prismAForm (singletonFilter csvLmsSuccess . maybePrism _PathPiece) mPrev $ aopt (hoistField lift dayField) (fslI MsgTableLmsSuccess)
]
dbtStyle = def { dbsFilterLayout = defaultDBSFilterLayout }
dbtParams = def
dbtIdent :: Text
dbtIdent = "lms-result"
dbtCsvEncode = Just DBTCsvEncode
{ dbtCsvExportForm = pure ()
, dbtCsvDoEncode = \() -> C.map (doEncode' . view _2)
, dbtCsvName
, dbtCsvSheetName
, dbtCsvNoExportData = Just id
, dbtCsvHeader = const $ return lmsResultTableCsvHeader
, dbtCsvExampleData = Just
[ LmsResultTableCsv{csvLRTident = LmsIdent lid, csvLRTsuccess = LmsDay $ addDays (-dos) now_day }
| (lid,dos) <- zip ["abcdefgh", "12345678", "ident8ch"] [1..]
]
}
where
doEncode' = LmsResultTableCsv
<$> view (_dbrOutput . _entityVal . _lmsResultIdent)
<*> view (_dbrOutput . _entityVal . _lmsResultSuccess . _lmsDay)
dbtCsvDecode = Just DBTCsvDecode -- Just save to DB; Job will process data later
{ dbtCsvRowKey = \LmsResultTableCsv{..} ->
fmap E.Value . MaybeT . getKeyBy $ UniqueLmsResult qid csvLRTident
, dbtCsvComputeActions = \case -- purpose is to show a diff to the user first
DBCsvDiffNew{dbCsvNewKey = Nothing, dbCsvNew} -> do
yield $ LmsResultInsertData
{ lmsResultInsertIdent = csvLRTident dbCsvNew
, lmsResultInsertSuccess = csvLRTsuccess dbCsvNew & lms2day
}
DBCsvDiffNew{dbCsvNewKey = Just _, dbCsvNew = _} -> error "UniqueLmsResult was found, but the key no longer exists." -- TODO: how can this ever happen? Check Pagination-Code
DBCsvDiffExisting{dbCsvNew = LmsResultTableCsv{..}, dbCsvOld} -> do
let successDay = lms2day csvLRTsuccess
when (successDay /= dbCsvOld ^. _dbrOutput . _entityVal . _lmsResultSuccess) $
yield $ LmsResultUpdateData
{ lmsResultInsertIdent = csvLRTident
, lmsResultInsertSuccess = successDay
}
DBCsvDiffMissing{} -> return () -- no deletion
, dbtCsvClassifyAction = \case
LmsResultInsertData{} -> LmsResultInsert
LmsResultUpdateData{} -> LmsResultUpdate
, dbtCsvCoarsenActionClass = \case
LmsResultInsert -> DBCsvActionNew
LmsResultUpdate -> DBCsvActionExisting
, dbtCsvValidateActions = return () -- no validation, since this is an automatic upload, i.e. no user to review error
, dbtCsvExecuteActions = do
C.mapM_ $ \actionData -> do
now <- liftIO getCurrentTime
void $ upsert
LmsResult
{ lmsResultQualification = qid
, lmsResultIdent = lmsResultInsertIdent actionData
, lmsResultSuccess = lmsResultInsertSuccess actionData
, lmsResultTimestamp = now -- lmsResultInsertTimestamp -- does it matter which one to choose?
}
[ LmsResultSuccess =. lmsResultInsertSuccess actionData
, LmsResultTimestamp =. now
]
-- audit $ Transaction.. (add to Audit.Types)
lift . queueDBJob $ JobLmsResults qid
return $ LmsResultR sid qsh
, dbtCsvRenderKey = const $ \case
LmsResultInsertData{..} -> do -- TODO: i18n
[whamlet|
$newline never
Insert: Ident #{getLmsIdent lmsResultInsertIdent} #
had success on ^{formatTimeW SelFormatDate lmsResultInsertSuccess}
|]
LmsResultUpdateData{..} -> do -- TODO: i18n
[whamlet|
$newline never
Update: Ident #{getLmsIdent lmsResultInsertIdent} #
had success on ^{formatTimeW SelFormatDate lmsResultInsertSuccess}
|]
, dbtCsvRenderActionClass = toWidget <=< ap getMessageRender . pure
, dbtCsvRenderException = ap getMessageRender . pure :: LmsResultCsvException -> DB Text
}
dbtExtraReps = []
resultDBTableValidator = def
& defaultSorting [SortAscBy csvLmsIdent]
dbTable resultDBTableValidator resultDBTable
getLmsResultR, postLmsResultR :: SchoolId -> QualificationShorthand -> Handler Html
getLmsResultR = postLmsResultR
postLmsResultR sid qsh = do
let directUploadLink = LmsResultUploadR sid qsh
lmsTable <- runDB $ do
qid <- getKeyBy404 $ SchoolQualificationShort sid qsh
view _2 <$> mkResultTable sid qsh qid
siteLayoutMsg MsgMenuLmsResult $ do
setTitleI MsgMenuLmsResult
$(widgetFile "lms-result")
-- Direct File Upload/Download
saveResultCsv :: QualificationId -> Int -> LmsResultTableCsv -> DB Int
saveResultCsv qid i LmsResultTableCsv{..} = do
now <- liftIO getCurrentTime
void $ upsert
LmsResult
{ lmsResultQualification = qid
, lmsResultIdent = csvLRTident
, lmsResultSuccess = csvLRTsuccess & lms2day
, lmsResultTimestamp = now
}
[ LmsResultSuccess =. (csvLRTsuccess & lms2day)
, LmsResultTimestamp =. now
]
return $ succ i
makeResultUploadForm :: Form FileInfo
makeResultUploadForm = renderAForm FormStandard $ fileAFormReq "Result CSV"
getLmsResultUploadR, postLmsResultUploadR :: SchoolId -> QualificationShorthand -> Handler Html
getLmsResultUploadR = postLmsResultUploadR
postLmsResultUploadR sid qsh = do
((result,widget), enctype) <- runFormPost makeResultUploadForm
case result of
FormSuccess file -> do
-- content <- fileSourceByteString file
-- return $ Just (fileName file, content)
nr <- runDB $ do
qid <- getKeyBy404 $ SchoolQualificationShort sid qsh
nr <- runConduit $ fileSource file
.| decodeCsv
.| foldMC (saveResultCsv qid) 0
queueJob' $ JobLmsResults qid
return nr
addMessage Success $ toHtml $ pack "Erfolgreicher Upload der Datei " <> fileName file <> pack (" mit " <> show nr <> " Zeilen")
redirect $ LmsResultR sid qsh
FormFailure errs -> do
forM_ errs $ addMessage Error . toHtml
redirect $ LmsResultUploadR sid qsh
FormMissing ->
siteLayoutMsg MsgMenuLmsResult $ do
setTitleI MsgMenuLmsUpload
[whamlet|$newline never
<form method=post enctype=#{enctype}>
^{widget}
<p>
<input type=submit>
|]
postLmsResultDirectR :: SchoolId -> QualificationShorthand -> Handler Html
postLmsResultDirectR sid qsh = do
(_params, files) <- runRequestBody
(status, msg) <- case files of
[(fhead,file)] -> do
lmsDecoder <- getLmsCsvDecoder
runDB $ do
qid <- getKeyBy404 $ SchoolQualificationShort sid qsh
enr <- try $ runConduit $ fileSource file
.| lmsDecoder
.| foldMC (saveResultCsv qid) 0
case enr of
Left (e :: SomeException) -> do -- catch all to avoid ok220 in case of any error
$logWarnS "LMS" $ "Result upload failed parsing: " <> tshow e
return (badRequest400, "Exception: " <> tshow e)
Right nr -> do
let msg = "Success. LMS Result upload file " <> fileName file <> " containing " <> tshow nr <> " rows for " <> fhead <> ". "
$logInfoS "LMS" msg
when (nr > 0) $ queueJob' $ JobLmsResults qid
return (ok200, msg)
[] -> do
let msg = "Result upload file missing."
$logWarnS "LMS" msg
return (badRequest400, msg)
_other -> do
let msg = "Result upload received multiple files; all ignored."
$logWarnS "LMS" msg
return (badRequest400, msg)
sendResponseStatus status msg

288
src/Handler/LMS/Userlist.hs Normal file
View File

@ -0,0 +1,288 @@
-- SPDX-FileCopyrightText: 2022 Steffen Jost <jost@tcs.ifi.lmu.de>
--
-- SPDX-License-Identifier: AGPL-3.0-or-later
{-# OPTIONS_GHC -fno-warn-orphans #-} -- needed for HasEntity instances
module Handler.LMS.Userlist
( getLmsUserlistR, postLmsUserlistR
, getLmsUserlistUploadR, postLmsUserlistUploadR
, postLmsUserlistDirectR
)
where
import Import
import Handler.Utils
import Handler.Utils.Csv
import Handler.Utils.LMS
import qualified Data.Map as Map
import qualified Data.Csv as Csv
import qualified Data.Conduit.List as C
import qualified Database.Esqueleto.Legacy as E
import qualified Database.Esqueleto.Utils as E
import Jobs.Queue
data LmsUserlistTableCsv = LmsUserlistTableCsv
{ csvLULident :: LmsIdent
, csvLULfailed :: LmsBool
}
deriving Generic
makeLenses_ ''LmsUserlistTableCsv
-- csv without headers
instance Csv.ToRecord LmsUserlistTableCsv
instance Csv.FromRecord LmsUserlistTableCsv
-- csv with headers
instance DefaultOrdered LmsUserlistTableCsv where
headerOrder = const $ Csv.header [ csvLmsIdent, csvLmsBlocked ]
instance ToNamedRecord LmsUserlistTableCsv where
toNamedRecord LmsUserlistTableCsv{..} = Csv.namedRecord
[ csvLmsIdent Csv..= csvLULident
, csvLmsBlocked Csv..= csvLULfailed
]
instance FromNamedRecord LmsUserlistTableCsv where
parseNamedRecord (lsfHeaderTranslate -> csv)
= LmsUserlistTableCsv
<$> csv Csv..: csvLmsIdent
<*> csv Csv..: csvLmsBlocked
instance CsvColumnsExplained LmsUserlistTableCsv where
csvColumnsExplanations _ = mconcat
[ single csvLmsIdent MsgCsvColumnLmsIdent
, single csvLmsBlocked MsgCsvColumnLmsLock
]
where
single :: RenderMessage UniWorX msg => Csv.Name -> msg -> Map Csv.Name Widget
single k v = singletonMap k [whamlet|_{v}|]
data LmsUserlistCsvActionClass = LmsUserlistInsert | LmsUserlistUpdate
deriving (Eq, Ord, Read, Show, Generic, Enum, Bounded)
embedRenderMessage ''UniWorX ''LmsUserlistCsvActionClass id
data LmsUserlistCsvAction = LmsUserlistInsertData { lmsUserlistInsertIdent :: LmsIdent, lmsUserlistInsertFailed :: Bool }
| LmsUserlistUpdateData { lmsUserlistInsertIdent :: LmsIdent, lmsUserlistInsertFailed :: Bool }
deriving (Eq, Ord, Read, Show, Generic)
deriveJSON defaultOptions
{ constructorTagModifier = camelToPathPiece'' 2 1 -- LmsUserlistInsertData -> insert
, fieldLabelModifier = camelToPathPiece' 2 -- lmsUserlistInsertIdent -> insert-ident | lmsUserlistInsertFailed -> insert-failed
, sumEncoding = TaggedObject "action" "data"
} ''LmsUserlistCsvAction
data LmsUserlistCsvException
= LmsUserlistCsvExceptionDuplicatedKey -- TODO: this is not used anywhere?!
deriving (Show, Generic)
instance Exception LmsUserlistCsvException
embedRenderMessage ''UniWorX ''LmsUserlistCsvException id
mkUserlistTable :: SchoolId -> QualificationShorthand -> QualificationId -> DB (Any, Widget)
mkUserlistTable sid qsh qid = do
dbtCsvName <- csvFilenameLmsUserlist qsh
let dbtCsvSheetName = dbtCsvName
let
userlistTable = DBTable{..}
where
dbtSQLQuery lmslist = do
E.where_ $ lmslist E.^. LmsUserlistQualification E.==. E.val qid
return lmslist
dbtRowKey = (E.^. LmsUserlistId)
dbtProj = dbtProjId
dbtColonnade = dbColonnade $ mconcat
[ sortable (Just csvLmsIdent) (i18nCell MsgTableLmsIdent) $ \DBRow{ dbrOutput = Entity _ LmsUserlist{..} } -> textCell $ lmsUserlistIdent & getLmsIdent
, sortable (Just csvLmsBlocked) (i18nCell MsgTableLmsLock) $ \DBRow{ dbrOutput = Entity _ LmsUserlist{..} } -> ifIconCell lmsUserlistFailed IconBlocked
, sortable (Just csvLmsTimestamp) (i18nCell MsgTableLmsReceived) $ \DBRow{ dbrOutput = Entity _ LmsUserlist{..} } -> dateTimeCell lmsUserlistTimestamp
]
dbtSorting = Map.fromList
[ (csvLmsIdent , SortColumn $ \lmslist -> lmslist E.^. LmsUserlistIdent)
, (csvLmsBlocked , SortColumn $ \lmslist -> lmslist E.^. LmsUserlistFailed)
, (csvLmsTimestamp, SortColumn $ \lmslist -> lmslist E.^. LmsUserlistTimestamp)
]
dbtFilter = Map.fromList
[ (csvLmsIdent , FilterColumn $ E.mkContainsFilterWith LmsIdent (E.^. LmsUserlistIdent ))
, (csvLmsBlocked, FilterColumn $ E.mkExactFilter (E.^. LmsUserlistFailed))
]
dbtFilterUI = \mPrev -> mconcat
[ prismAForm (singletonFilter csvLmsIdent . maybePrism _PathPiece) mPrev $ aopt (hoistField lift textField) (fslI MsgTableLmsIdent)
, prismAForm (singletonFilter csvLmsBlocked . maybePrism _PathPiece) mPrev $ aopt (hoistField lift checkBoxField) (fslI MsgTableLmsLock)
]
dbtStyle = def { dbsFilterLayout = defaultDBSFilterLayout }
dbtParams = def
dbtIdent :: Text
dbtIdent = "lms-userlist"
dbtCsvEncode = simpleCsvEncode dbtCsvName dbtCsvSheetName doEncode' <&> addExample
where
addExample dce = dce{ dbtCsvExampleData = csvExample }
csvExample = Just
[ LmsUserlistTableCsv{csvLULident = LmsIdent lid, csvLULfailed = LmsBool ufl}
| (lid,ufl) <- zip ["abcdefgh", "12345678", "ident8ch"] [False,True,False]
]
doEncode' = LmsUserlistTableCsv
<$> view (_dbrOutput . _entityVal . _lmsUserlistIdent)
<*> view (_dbrOutput . _entityVal . _lmsUserlistFailed . _lmsBool)
dbtCsvDecode = Just DBTCsvDecode {..}
where
dbtCsvRowKey = \LmsUserlistTableCsv{csvLULident} ->
fmap E.Value . MaybeT . getKeyBy $ UniqueLmsUserlist qid csvLULident
dbtCsvComputeActions = \case -- shows a diff first
DBCsvDiffNew{dbCsvNew} -> do
yield $ LmsUserlistInsertData
{ lmsUserlistInsertIdent = csvLULident dbCsvNew
, lmsUserlistInsertFailed = lms2bool $ csvLULfailed dbCsvNew
}
DBCsvDiffExisting{dbCsvNew = LmsUserlistTableCsv{..}, dbCsvOld} -> do
let failedBool = lms2bool csvLULfailed
when (failedBool /= dbCsvOld ^. _dbrOutput . _entityVal . _lmsUserlistFailed) $
yield $ LmsUserlistUpdateData
{ lmsUserlistInsertIdent = csvLULident
, lmsUserlistInsertFailed = csvLULfailed & lms2bool
}
DBCsvDiffMissing{} -> return () -- no deletion
dbtCsvClassifyAction = \case
LmsUserlistInsertData{} -> LmsUserlistInsert
LmsUserlistUpdateData{} -> LmsUserlistUpdate
dbtCsvCoarsenActionClass = \case
LmsUserlistInsert -> DBCsvActionNew
LmsUserlistUpdate -> DBCsvActionExisting
dbtCsvValidateActions = return () -- no validation, since this is an automatic upload, i.e. no user to review error
dbtCsvExecuteActions = do
C.mapM_ $ \actionData -> do
now <- liftIO getCurrentTime
void $ upsert LmsUserlist
{
lmsUserlistQualification = qid
, lmsUserlistIdent = lmsUserlistInsertIdent actionData
, lmsUserlistFailed = lmsUserlistInsertFailed actionData
, lmsUserlistTimestamp = now
}
[
LmsUserlistFailed =. lmsUserlistInsertFailed actionData -- TODO: should we allow a reset from failed: True to False?
, LmsUserlistTimestamp =. now
]
-- audit
lift . queueDBJob $ JobLmsUserlist qid
return $ LmsUserlistR sid qsh
dbtCsvRenderKey = const $ \case
LmsUserlistInsertData{..} -> do -- TODO: i18n
[whamlet|
$newline never
Insert: Course for Ident #{getLmsIdent lmsUserlistInsertIdent} #
$if lmsUserlistInsertFailed
is closed due to failure.
$else
is open.
|]
LmsUserlistUpdateData{..} -> do -- TODO: i18n
[whamlet|
$newline never
Update: Course for Ident #{getLmsIdent lmsUserlistInsertIdent} #
$if lmsUserlistInsertFailed
is now closed due to failure.
$else
is still open.
|]
dbtCsvRenderActionClass = toWidget <=< ap getMessageRender . pure
dbtCsvRenderException = ap getMessageRender . pure :: LmsUserlistCsvException -> DB Text
dbtExtraReps = []
userlistDBTableValidator = def
& defaultSorting [SortAscBy csvLmsIdent]
dbTable userlistDBTableValidator userlistTable
getLmsUserlistR, postLmsUserlistR :: SchoolId -> QualificationShorthand -> Handler Html
getLmsUserlistR = postLmsUserlistR
postLmsUserlistR sid qsh = do
lmsTable <- runDB $ do
qid <- getKeyBy404 $ SchoolQualificationShort sid qsh
view _2 <$> mkUserlistTable sid qsh qid
siteLayoutMsg MsgMenuLmsUserlist $ do
setTitleI MsgMenuLmsUserlist
lmsTable
-- Direct File Upload/Download
-- saveUserlistCsv :: (PersistUniqueWrite backend, MonadIO m, BaseBackend backend ~ SqlBackend, Enum b) =>
-- Key Qualification -> b -> LmsUserlistTableCsv -> ReaderT backend m b
saveUserlistCsv :: QualificationId -> Int -> LmsUserlistTableCsv -> DB Int
saveUserlistCsv qid i LmsUserlistTableCsv{..} = do
now <- liftIO getCurrentTime
void $ upsert
LmsUserlist
{ lmsUserlistQualification = qid
, lmsUserlistIdent = csvLULident
, lmsUserlistFailed = csvLULfailed & lms2bool
, lmsUserlistTimestamp = now
}
[ LmsUserlistFailed =. (csvLULfailed & lms2bool)
, LmsUserlistTimestamp =. now
]
return $ succ i
makeUserlistUploadForm :: Form FileInfo
makeUserlistUploadForm = renderAForm FormStandard $ fileAFormReq "Userlist CSV"
getLmsUserlistUploadR, postLmsUserlistUploadR :: SchoolId -> QualificationShorthand -> Handler Html
getLmsUserlistUploadR = postLmsUserlistUploadR
postLmsUserlistUploadR sid qsh = do
((result,widget), enctype) <- runFormPost makeUserlistUploadForm
case result of
FormSuccess file -> do
nr <- runDB $ do
qid <- getKeyBy404 $ SchoolQualificationShort sid qsh
nr <- runConduit $ fileSource file .| decodeCsv .| foldMC (saveUserlistCsv qid) 0
queueJob' $ JobLmsUserlist qid
return nr
addMessage Success $ toHtml $ pack "Erfolgreicher Upload der Datei " <> fileName file <> pack (" mit " <> show nr <> " Zeilen")
redirect $ LmsUserlistR sid qsh
FormFailure errs -> do
forM_ errs $ addMessage Error . toHtml
redirect $ LmsUserlistUploadR sid qsh
FormMissing ->
siteLayoutMsg MsgMenuLmsUserlist $ do
setTitleI MsgMenuLmsUpload
[whamlet|$newline never
<form method=post enctype=#{enctype}>
^{widget}
<p>
<input type=submit>
|]
postLmsUserlistDirectR :: SchoolId -> QualificationShorthand -> Handler Html
postLmsUserlistDirectR sid qsh = do
(_params, files) <- runRequestBody
(status, msg) <- case files of
[(fhead,file)] -> do
lmsDecoder <- getLmsCsvDecoder
runDB $ do
qid <- getKeyBy404 $ SchoolQualificationShort sid qsh
enr <- try $ runConduit $ fileSource file
.| lmsDecoder
.| foldMC (saveUserlistCsv qid) 0
case enr of
Left (e :: SomeException) -> do
$logWarnS "LMS" $ "Userlist upload failed parsing: " <> tshow e
return (badRequest400, "Exception: " <> tshow e)
Right nr -> do
let msg = "Success. LMS Userlist upload file " <> fileName file <> " containing " <> tshow nr <> " rows for " <> fhead <> ". "
$logInfoS "LMS" msg
when (nr > 0) $ queueJob' $ JobLmsUserlist qid
return (ok200, msg)
[] -> do
let msg = "Userlist upload file missing."
$logWarnS "LMS" msg
return (badRequest400, msg)
_other -> do
let msg = "Userlist upload received multiple files; all ignored."
$logWarnS "LMS" msg
return (badRequest400, msg)
sendResponseStatus status msg

View File

@ -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

View File

@ -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

View File

@ -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
@ -7,11 +7,10 @@
module Handler.PrintCenter module Handler.PrintCenter
( getPrintDownloadR ( getPrintDownloadR
, getPrintCenterR, postPrintCenterR , getPrintCenterR, postPrintCenterR
, getPrintSendR , postPrintSendR , getPrintSendR , postPrintSendR
, getPrintAckR , postPrintAckR , getPrintAckR , postPrintAckR
, getPrintAckDirectR, postPrintAckDirectR , postPrintAckDirectR
, getPrintLogR
) where ) where
import Import import Import
@ -20,14 +19,14 @@ 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
import qualified Data.Aeson as Aeson -- import Data.Aeson (encode)
-- import qualified Data.Text as Text -- import qualified Data.Text as Text
-- import qualified Data.Set as Set -- import qualified Data.Set as Set
@ -44,11 +43,11 @@ single :: (k,a) -> Map k a
single = uncurry Map.singleton single = uncurry Map.singleton
data LRQF = LRQF data LRQF = LRQF
{ lrqfLetter :: Text { lrqfLetter :: Text
, lrqfUser :: Either UserEmail UserId , lrqfUser :: Either UserEmail UserId
, lrqfSuper :: Maybe (Either UserEmail UserId) , lrqfSuper :: Maybe (Either UserEmail UserId)
, lrqfQuali :: Entity Qualification , lrqfQuali :: Entity Qualification
, lrqfIdent :: LmsIdent , lrqfIdent :: LmsIdent
, lrqfPin :: Text , lrqfPin :: Text
, lrqfExpiry :: Maybe Day , lrqfExpiry :: Maybe Day
@ -56,33 +55,33 @@ 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)
<*> areq (userField False Nothing) (fslI MsgLmsUser) (lrqfUser <$> tmpl) <*> areq (userField False Nothing) (fslI MsgLmsUser) (lrqfUser <$> tmpl)
<*> aopt (userField False Nothing) (fslI MsgTableSupervisor) (lrqfSuper <$> tmpl) <*> aopt (userField False Nothing) (fslI MsgTableSupervisor) (lrqfSuper <$> tmpl)
<*> areq qualificationFieldEnt (fslI MsgQualificationName) (lrqfQuali <$> tmpl) <*> areq qualificationFieldEnt (fslI MsgQualificationName) (lrqfQuali <$> tmpl)
<*> areq lmsField (fslI MsgTableLmsIdent) (lrqfIdent <$> tmpl) <*> areq lmsField (fslI MsgTableLmsIdent) (lrqfIdent <$> tmpl)
<*> areq textField (fslI MsgTableLmsPin) (lrqfPin <$> tmpl) <*> areq textField (fslI MsgTableLmsPin) (lrqfPin <$> tmpl)
<*> aopt dayField (fslI MsgLmsQualificationValidUntil) (lrqfExpiry <$> tmpl) <*> aopt dayField (fslI MsgLmsQualificationValidUntil) (lrqfExpiry <$> tmpl)
<*> areq (boolField . Just $ SomeMessage MsgBoolIrrelevant) <*> areq (boolField . Just $ SomeMessage MsgBoolIrrelevant)
(fslI MsgLmsRenewalReminder) (lrqfReminder <$> tmpl) (fslI MsgLmsRenewalReminder) (lrqfReminder <$> tmpl)
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 ()
lrqf2letter :: LRQF -> DB (Entity User, SomeLetter) lrqf2letter :: LRQF -> DB (Entity User, SomeLetter)
lrqf2letter LRQF{..} lrqf2letter LRQF{..}
| lrqfLetter == "r" = do | lrqfLetter == "r" = do
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,18 +93,16 @@ 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)
| lrqfLetter == "e" || lrqfLetter == "E" = do | lrqfLetter == "e" || lrqfLetter == "E" = do
rcvr <- mapM getUser lrqfSuper rcvr <- mapM getUser lrqfSuper
usr <- getUser lrqfUser usr <- getUser lrqfUser
usrShrt <- encrypt $ entityKey usr usrShrt <- encrypt $ entityKey usr
usrUuid <- encrypt $ entityKey usr usrUuid <- encrypt $ entityKey usr
urender <- liftHandler getUrlRender urender <- liftHandler getUrlRender
let letter = LetterExpireQualification let letter = LetterExpireQualification
{ leqHolderCFN = usrShrt { leqHolderCFN = usrShrt
, leqHolderID = usr ^. _entityKey , leqHolderID = usr ^. _entityKey
, leqHolderDN = usr ^. _userDisplayName , leqHolderDN = usr ^. _userDisplayName
@ -114,15 +111,15 @@ lrqf2letter LRQF{..}
, leqId = lrqfQuali ^. _entityKey , leqId = lrqfQuali ^. _entityKey
, leqName = lrqfQuali ^. _qualificationName . _CI , leqName = lrqfQuali ^. _qualificationName . _CI
, leqShort = lrqfQuali ^. _qualificationShorthand . _CI , leqShort = lrqfQuali ^. _qualificationShorthand . _CI
, leqSchool = lrqfQuali ^. _qualificationSchool , leqSchool = lrqfQuali ^. _qualificationSchool
, leqUrl = pure . urender $ ForProfileDataR usrUuid , leqUrl = pure . urender $ ForProfileDataR usrUuid
} }
return (fromMaybe usr rcvr, SomeLetter letter) return (fromMaybe usr rcvr, SomeLetter letter)
| otherwise = error "Unknown Letter Type encountered. Use 'e' or 'r' only." | otherwise = error "Unknown Letter Type encountered. Use 'e' or 'r' only."
where where
getUser :: Either UserEmail UserId -> DB (Entity User) getUser :: Either UserEmail UserId -> DB (Entity User)
getUser (Right uid) = getEntity404 uid getUser (Right uid) = getEntity404 uid
getUser (Left mail) = getBy404 $ UniqueEmail mail getUser (Left mail) = getBy404 $ UniqueEmail mail
data PJTableAction = PJActAcknowledge | PJActReprint data PJTableAction = PJActAcknowledge | PJActReprint
@ -133,12 +130,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 +142,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,39 +164,33 @@ 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
let let
dbtSQLQuery = pjTableQuery dbtSQLQuery = pjTableQuery
dbtRowKey = queryPrintJob >>> (E.^. PrintJobId) dbtRowKey = queryPrintJob >>> (E.^. PrintJobId)
@ -218,7 +205,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 +217,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))
@ -240,16 +225,15 @@ mkPJTable = do
dbtFilter = mconcat dbtFilter = mconcat
[ single ("name" , FilterColumn . E.mkContainsFilterWithCommaPlus id $ views (to queryPrintJob) (E.^. PrintJobName)) [ single ("name" , FilterColumn . E.mkContainsFilterWithCommaPlus id $ views (to queryPrintJob) (E.^. PrintJobName))
, single ("apcid" , FilterColumn . E.mkContainsFilterWithComma id $ views (to queryPrintJob) (E.^. PrintJobApcIdent)) , single ("apcid" , FilterColumn . E.mkContainsFilterWithComma id $ views (to queryPrintJob) (E.^. PrintJobApcIdent))
, single ("filename" , FilterColumn . E.mkContainsFilter $ views (to queryPrintJob) (E.^. PrintJobFilename)) , single ("filename" , FilterColumn . E.mkContainsFilter $ views (to queryPrintJob) (E.^. PrintJobFilename))
, 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))
, single ("lmsid" , FilterColumn . E.mkContainsFilterWithCommaPlus (Just . LmsIdent) $ views (to queryPrintJob) (E.^. PrintJobLmsUser)) , single ("lmsid" , FilterColumn . E.mkContainsFilterWithCommaPlus (Just . LmsIdent) $ views (to queryPrintJob) (E.^. PrintJobLmsUser))
, single ("acknowledged" , FilterColumn . E.mkExactFilterLast $ views (to queryPrintJob) (E.isJust . (E.^. PrintJobAcknowledged))) , single ("acknowledged" , FilterColumn . E.mkExactFilterLast $ views (to queryPrintJob) (E.isJust . (E.^. PrintJobAcknowledged)))
] ]
dbtFilterUI mPrev = mconcat dbtFilterUI mPrev = mconcat
@ -259,12 +243,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)
] ]
@ -305,7 +288,7 @@ mkPJTable = do
getPrintCenterR, postPrintCenterR :: Handler Html getPrintCenterR, postPrintCenterR :: Handler Html
getPrintCenterR = postPrintCenterR getPrintCenterR = postPrintCenterR
postPrintCenterR = do postPrintCenterR = do
(pjRes, pjTable) <- runDB mkPJTable (pjRes, pjTable) <- runDB mkPJTable
formResult pjRes $ \case formResult pjRes $ \case
@ -315,21 +298,21 @@ postPrintCenterR = do
addMessageI Success $ MsgPrintJobAcknowledge num addMessageI Success $ MsgPrintJobAcknowledge num
reloadKeepGetParams PrintCenterR reloadKeepGetParams PrintCenterR
(PJActReprintData{ignoreReroute}, Set.toList -> pjIds) -> do (PJActReprintData{ignoreReroute}, Set.toList -> pjIds) -> do
let countOk = either (const $ Sum 0) (const $ Sum 1) let countOk = either (const $ Sum 0) (const $ Sum 1)
oks <- runDB $ forM pjIds $ fmap countOk . reprintPDF (fromMaybe False ignoreReroute) oks <- runDB $ forM pjIds $ fmap countOk . reprintPDF (fromMaybe False ignoreReroute)
let nr_oks = getSum $ mconcat oks let nr_oks = getSum $ mconcat oks
nr_tot = length pjIds nr_tot = length pjIds
mstat = bool Warning Success $ nr_oks == nr_tot mstat = bool Warning Success $ nr_oks == nr_tot
addMessageI mstat $ MsgPrintJobReprint nr_oks nr_tot addMessageI mstat $ MsgPrintJobReprint nr_oks nr_tot
reloadKeepGetParams PrintCenterR reloadKeepGetParams PrintCenterR
siteConf <- getYesod siteConf <- getYesod
let lprConf = siteConf ^. _appLprConf let lprConf = siteConf ^. _appLprConf
reroute = siteConf ^. _appMailRerouteTo reroute = siteConf ^. _appMailRerouteTo
lprWgt = [whamlet| lprWgt = [whamlet|
LPR Konfiguration ist #{lprQueue lprConf}@#{lprHost lprConf}:#{lprPort lprConf} LPR Konfiguration ist #{lprQueue lprConf}@#{lprHost lprConf}:#{lprPort lprConf}
<div> <div>
$maybe _ <- reroute $maybe _ <- reroute
Mail-reroute-to ist gesetzt, somit werden alle lpr Kommandos unterdrückt! Mail-reroute-to ist gesetzt, somit werden alle lpr Kommandos unterdrückt!
|] |]
siteLayoutMsg MsgMenuApc $ do siteLayoutMsg MsgMenuApc $ do
setTitleI MsgMenuApc setTitleI MsgMenuApc
@ -339,7 +322,7 @@ postPrintCenterR = do
getPrintSendR, postPrintSendR :: Handler Html getPrintSendR, postPrintSendR :: Handler Html
getPrintSendR = postPrintSendR getPrintSendR = postPrintSendR
postPrintSendR = do postPrintSendR = do
usr <- requireAuth -- to determine language and recipient for test usr <- requireAuth -- to determine language and recipient for test
mbQual <- runDB $ selectFirst [] [Asc QualificationAvsLicence, Asc QualificationShorthand] mbQual <- runDB $ selectFirst [] [Asc QualificationAvsLicence, Asc QualificationShorthand]
now <- liftIO getCurrentTime now <- liftIO getCurrentTime
let nowaday = utctDay now let nowaday = utctDay now
@ -357,7 +340,7 @@ postPrintSendR = do
def_lrqf = mkLetter <$> mbQual def_lrqf = mkLetter <$> mbQual
((sendResult, sendWidget), sendEnctype) <- runFormPost $ makeRenewalForm def_lrqf ((sendResult, sendWidget), sendEnctype) <- runFormPost $ makeRenewalForm def_lrqf
let procFormSend lrqf = case lrqfLetter lrqf of let procFormSend lrqf = case lrqfLetter lrqf of
"E" -> (runDB (lrqf2letter lrqf) >>= printHtml (Just uid)) >>= \case "E" -> (runDB (lrqf2letter lrqf) >>= printHtml (Just uid)) >>= \case
Right html -> sendResponse $ toTypedContent html Right html -> sendResponse $ toTypedContent html
Left err -> do Left err -> do
@ -365,7 +348,7 @@ postPrintSendR = do
$logErrorS "LPR" msg $logErrorS "LPR" msg
addMessage Error $ toHtml msg addMessage Error $ toHtml msg
pure () pure ()
_ -> do _ -> do
ok <- (runDB (lrqf2letter lrqf) >>= printLetter (Just uid)) >>= \case ok <- (runDB (lrqf2letter lrqf) >>= printLetter (Just uid)) >>= \case
Left err -> do Left err -> do
let msg = "PDF printing failed with error: " <> err let msg = "PDF printing failed with error: " <> err
@ -416,26 +399,26 @@ postPrintAckR ackDay numAck chksm = do
, formSubmit = FormNoSubmit , formSubmit = FormNoSubmit
} }
formResult ackRes $ \BtnConfirm -> do formResult ackRes $ \BtnConfirm -> do
numNew <- runDB $ do numNew <- runDB $ do
pjs <- Ex.select $ do pjs <- Ex.select $ do
pj <- Ex.from $ Ex.table @PrintJob pj <- Ex.from $ Ex.table @PrintJob
let pjDay = E.day $ pj Ex.^. PrintJobCreated let pjDay = E.day $ pj Ex.^. PrintJobCreated
Ex.where_ $ Ex.isNothing (pj Ex.^. PrintJobAcknowledged) Ex.where_ $ Ex.isNothing (pj Ex.^. PrintJobAcknowledged)
Ex.&&. (pjDay Ex.==. Ex.val ackDay) Ex.&&. (pjDay Ex.==. Ex.val ackDay)
return $ pj Ex.^. PrintJobId return $ pj Ex.^. PrintJobId
let changed = numAck /= length pjs || chksm /= hash (Set.fromList (Ex.unValue <$> pjs)) let changed = numAck /= length pjs || chksm /= hash (Set.fromList (Ex.unValue <$> pjs))
if changed if changed
then return (-1) then return (-1)
else do else do
now <- liftIO getCurrentTime now <- liftIO getCurrentTime
E.updateCount $ \pj -> do E.updateCount $ \pj -> do
let pjDay = E.day $ pj E.^. PrintJobCreated let pjDay = E.day $ pj E.^. PrintJobCreated
E.set pj [ PrintJobAcknowledged E.=. E.justVal now ] E.set pj [ PrintJobAcknowledged E.=. E.justVal now ]
E.where_ $ E.isNothing (pj E.^. PrintJobAcknowledged) E.where_ $ E.isNothing (pj E.^. PrintJobAcknowledged)
E.&&. (pjDay E.==. E.val ackDay) E.&&. (pjDay E.==. E.val ackDay)
-- Ex.updateCount $ do -- Ex.updateCount $ do
-- pj <- Ex.from $ Ex.table @PrintJob -- pj <- Ex.from $ Ex.table @PrintJob
-- let pjDay = E.day $ pj Ex.^. PrintJobCreated -- let pjDay = E.day $ pj Ex.^. PrintJobCreated
-- Ex.set pj [ PrintJobAcknowledged Ex.=. Ex.just (Ex.val now) ] -- Ex.set pj [ PrintJobAcknowledged Ex.=. Ex.just (Ex.val now) ]
-- Ex.where_ $ Ex.isNothing (pj Ex.^. PrintJobAcknowledged) -- Ex.where_ $ Ex.isNothing (pj Ex.^. PrintJobAcknowledged)
-- Ex.&&. (pjDay Ex.==. Ex.val ackDay) -- Ex.&&. (pjDay Ex.==. Ex.val ackDay)
@ -444,44 +427,29 @@ postPrintAckR ackDay numAck chksm = do
else addMessageI Error MsgPrintJobAcknowledgeFailed else addMessageI Error MsgPrintJobAcknowledgeFailed
redirect PrintCenterR redirect PrintCenterR
ackDayText <- formatTime SelFormatDate ackDay ackDayText <- formatTime SelFormatDate ackDay
siteLayoutMsg siteLayoutMsg
(MsgPrintJobAcknowledgeQuestion numAck ackDayText) (MsgPrintJobAcknowledgeQuestion numAck ackDayText)
ackForm ackForm
-- no header csv, containing a single column of lms identifiers (logins) -- no header csv, containing a single column of lms identifiers (logins)
-- instance Csv.FromRecord LmsIdent -- default suffices -- instance Csv.FromRecord LmsIdent -- default suffices
-- instance Csv.FromRecord Text where -- instance Csv.FromRecord Text where
-- parseRecord v -- parseRecord v
-- | length v >= 1 = v Csv..! 0 -- | length v >= 1 = v Csv..! 0
-- | otherwise = pure "ERROR" -- | otherwise = pure "ERROR"
saveApcident :: UTCTime -> Natural -> Text -> JobDB Natural saveApcident :: UTCTime -> Natural -> Text -> JobDB Natural
saveApcident t i apci = insert_ (PrintAcknowledge apci t False) >> return (succ i) saveApcident t i apci = insert_ (PrintAcknowledge apci t False) >> return (succ i)
makeAckUploadForm :: Form FileInfo
makeAckUploadForm = renderAForm FormStandard $ fileAFormReq "Acknowledge APC-Ident CSV"
getPrintAckDirectR :: Handler Html
getPrintAckDirectR = do
(widget, enctype) <- generateFormPost makeAckUploadForm
siteLayoutMsg MsgMenuPrintAck $ do
setTitleI MsgMenuPrintAck
[whamlet|$newline never
<form method=post enctype=#{enctype}>
^{widget}
<input type=submit>
|]
postPrintAckDirectR :: Handler Html postPrintAckDirectR :: Handler Html
postPrintAckDirectR = do postPrintAckDirectR = do
now <- liftIO getCurrentTime now <- liftIO getCurrentTime
(_params, files) <- runRequestBody (_params, files) <- runRequestBody
(status, msg) <- case files of (status, msg) <- case files of
[(_fhead,file)] -> do [(_fhead,file)] -> do
runDBJobs $ do runDBJobs $ do
enr <- try $ runConduit $ fileSource file enr <- try $ runConduit $ fileSource file
-- .| decodeCsvPositional Csv.NoHeader -- decode by separator position -- .| decodeCsvPositional Csv.NoHeader -- decode by separator position
.| decodeUtf8C -- no CSV, just convert each line to a single text .| decodeUtf8C -- no CSV, just convert each line to a single text
.| linesUnboundedC .| linesUnboundedC
.| foldMC (saveApcident now) 0 .| foldMC (saveApcident now) 0
@ -493,7 +461,7 @@ postPrintAckDirectR = do
let msg = "Success: received " <> tshow nr <> " APC identifiers to be processed later." let msg = "Success: received " <> tshow nr <> " APC identifiers to be processed later."
$logInfoS "LMS" msg $logInfoS "LMS" msg
when (nr > 0) $ queueDBJob JobPrintAck when (nr > 0) $ queueDBJob JobPrintAck
return (ok200, msg) return (ok200, msg)
[] -> do [] -> do
let msg = "Error: No file received. A file of lms identifiers must be supplied for print job acknowledging." let msg = "Error: No file received. A file of lms identifiers must be supplied for print job acknowledging."
$logWarnS "APC" msg $logWarnS "APC" msg
@ -503,55 +471,3 @@ postPrintAckDirectR = do
$logErrorS "APC" msg $logErrorS "APC" msg
return (badRequest400, msg) return (badRequest400, msg)
sendResponseStatus status msg -- must be outside of runDB; otherwise transaction is rolled back sendResponseStatus status msg -- must be outside of runDB; otherwise transaction is rolled back
getPrintLogR :: Handler Html
getPrintLogR = do
let
logDBTable = DBTable{..}
where
resultLog :: Lens' (DBRow (TransactionLog, Aeson.Result Transaction)) TransactionLog
resultLog = _dbrOutput . _1
resultTrans :: Lens' (DBRow (TransactionLog, Aeson.Result Transaction)) (Aeson.Result Transaction)
resultTrans = _dbrOutput . _2
tCell' err c dbr = case view resultTrans dbr of
(Aeson.Error msg) -> err msg -- should not happen, due to query filter
(Aeson.Success t) -> c t
tCellErr = tCell' stringCell
tCell = tCell' $ const mempty
dbtIdent = "lpr-log" :: Text
dbtSQLQuery l = do
E.where_ $ E.val "LPR" E.==. l E.^. TransactionLogInfo E.->>. "interface-name"
-- E.&&. E.val "interface" E.==. l E.^. TransactionLogInfo E.->>. "transaction" -- not necessary
return l
dbtRowKey = (E.^. TransactionLogId)
dbtProj = dbtProjSimple $ \(Entity _ l) -> do
return (l, Aeson.fromJSON $ transactionLogInfo l)
dbtColonnade = dbColonnade $ mconcat
[ sortable (Just "time") (i18nCell MsgSystemMessageTimestamp) $ \(view $ resultLog . to transactionLogTime -> t) -> dateTimeCell t
, sortable (Just "status") (textCell "Status") $ tCell (cellMaybe iconBoolCell . transactionInterfaceSuccess)
, sortable (Just "subtype") (i18nCell MsgInterfaceSubtype) $ tCell ( textCell . transactionInterfaceSubtype)
, sortable (Just "info") (i18nCell MsgSystemMessageContent) $ tCellErr ( textCell . transactionInterfaceInfo)
]
dbtSorting = mconcat
[ singletonMap "time" $ SortColumn (E.^. TransactionLogTime)
, singletonMap "status" $ SortColumn (\r -> r E.^. TransactionLogTime E.->>. "interface-success")
, singletonMap "subtype" $ SortColumn (\r -> r E.^. TransactionLogTime E.->>. "interface-subtype")
, singletonMap "info" $ SortColumn (\r -> r E.^. TransactionLogTime E.->>. "interface-info" )
]
dbtFilter = mempty
dbtFilterUI = mempty
dbtStyle = def
dbtParams = def
dbtCsvEncode = noCsvEncode
dbtCsvDecode = Nothing
dbtExtraReps = []
validator = def & defaultSorting [ SortDescBy "time" ]
tbl <- runDB $ dbTableDB' validator logDBTable
siteLayoutMsg MsgMenuPrintLog $ do
setTitleI MsgMenuPrintLog
[whamlet|^{tbl}|]

View File

@ -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,14 +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
, stgMobile :: Maybe Text
, stgExamOfficeSettings :: ExamOfficeSettings , stgExamOfficeSettings :: ExamOfficeSettings
, stgSchools :: Set SchoolId , stgSchools :: Set SchoolId
, stgNotificationSettings :: NotificationSettings , stgNotificationSettings :: NotificationSettings
@ -116,14 +102,14 @@ instance RenderMessage UniWorX NotificationTriggerKind where
where where
mr = renderMessage f ls mr = renderMessage f ls
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)
@ -144,14 +130,11 @@ makeSettingForm template html = do
<*> apopt checkBoxField (fslI MsgShowSex & setTooltip MsgShowSexTip) (stgShowSex <$> template) <*> apopt checkBoxField (fslI MsgShowSex & setTooltip MsgShowSexTip) (stgShowSex <$> template)
<* aformSection MsgFormNotifications <* aformSection MsgFormNotifications
<*> aopt (textField & cfStrip) (fslI MsgPDFPassword & setTooltip MsgPDFPasswordTip) (stgPinPassword <$> template) <*> aopt (textField & cfStrip) (fslI MsgPDFPassword & setTooltip MsgPDFPasswordTip) (stgPinPassword <$> template)
<*> apopt checkBoxField (fslI MsgPrefersPostalExp & setTooltip MsgPostalTip) (stgPrefersPostal <$> template) <*> apopt checkBoxField (fslI MsgPrefersPostal & setTooltip MsgPostalTip) (stgPrefersPostal <$> template)
<*> aopt htmlField (fslI MsgPostAddress & setTooltip MsgPostAddressTip) (stgPostAddress <$> template) <*> aopt htmlField (fslI MsgPostAddress & setTooltip MsgPostAddressTip) (stgPostAddress <$> template)
<*> aopt (textField & cfStrip) (fslI MsgUserTelephone) (stgTelephone <$> template) <*> examOfficeForm (stgExamOfficeSettings <$> template)
<*> aopt (textField & cfStrip) (fslI MsgUserMobile ) (stgMobile <$> 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 +146,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})
@ -186,7 +169,7 @@ schoolsForm template = formToAForm $ schoolsFormView =<< renderWForm FormStandar
notificationForm :: Maybe NotificationSettings -> AForm Handler NotificationSettings notificationForm :: Maybe NotificationSettings -> AForm Handler NotificationSettings
notificationForm template = wFormToAForm $ do notificationForm template = wFormToAForm $ do
mbUid <- liftHandler maybeAuthId mbUid <- liftHandler maybeAuthId
isAdmin <- checkAdmin isAdmin <- lift . lift $ hasReadAccessTo AdminR
let let
sectionIsHidden :: NotificationTriggerKind -> DB Bool sectionIsHidden :: NotificationTriggerKind -> DB Bool
@ -198,28 +181,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 +216,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 +287,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,29 +356,27 @@ 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'
postalIsValid = validPostAddress userPostAddress' postalIsValid = validPostAddress userPostAddress'
guardValidation MsgUserPostalInvalid $ guardValidation MsgUserPostalInvalid $
postalNotSet || postalIsValid postalNotSet || postalIsValid
userPrefersPostal' <- use _stgPrefersPostal userPrefersPostal' <- use _stgPrefersPostal
guardValidation MsgUserPrefersPostalInvalid $ guardValidation MsgUserPrefersPostalInvalid $
not $ userPrefersPostal' && postalNotSet && isNothing userCompanyDepartment not $ userPrefersPostal' && (postalNotSet || isJust userCompanyDepartment)
userPinPassword' <- use _stgPinPassword userPinPassword' <- use _stgPinPassword
let pinBad = validCmdArgument =<< userPinPassword' let pinBad = validCmdArgument =<< userPinPassword'
pinMinChar = 5 pinMinChar = 5
pinLength = maybe 0 length userPinPassword' pinLength = maybe 0 length userPinPassword'
pinOk <- if userPrefersPostal' || pinMinChar <= pinLength then pure True else checkAdmin -- admins are allowed to ignore pin requirements pinOk <- if userPrefersPostal' || pinMinChar <= pinLength then pure True else liftHandler $ hasReadAccessTo AdminR -- admins are allowed to ignore pin requirements
whenIsJust pinBad (tellValidationError . MsgPDFPasswordInvalid) -- used as CMD argument for pdftk whenIsJust pinBad (tellValidationError . MsgPDFPasswordInvalid) -- used as CMD argument for pdftk
guardValidation (MsgPDFPasswordTooShort pinMinChar) pinOk guardValidation (MsgPDFPasswordTooShort pinMinChar) pinOk
@ -421,7 +402,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 +415,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 +425,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,9 +439,7 @@ serveProfileR (uid, user@User{..}) = do
, stgShowSex = userShowSex , stgShowSex = userShowSex
, stgPinPassword = userPinPassword , stgPinPassword = userPinPassword
, stgPostAddress = userPostAddress , stgPostAddress = userPostAddress
, stgPrefersPostal = userPrefersPostal , stgPrefersPostal = userPrefersPostal
, stgTelephone = userTelephone
, stgMobile = userMobile
, stgExamOfficeSettings = ExamOfficeSettings , stgExamOfficeSettings = ExamOfficeSettings
{ eosettingsGetSynced = userExamOfficeGetSynced { eosettingsGetSynced = userExamOfficeGetSynced
, eosettingsGetLabels = userExamOfficeGetLabels , eosettingsGetLabels = userExamOfficeGetLabels
@ -471,14 +450,10 @@ serveProfileR (uid, user@User{..}) = do
formResult res $ \SettingsForm{..} -> do formResult res $ \SettingsForm{..} -> do
now <- liftIO getCurrentTime now <- liftIO getCurrentTime
isAdmin <- checkAdmin
thisUser <- fromMaybe uid <$> maybeAuthId
let changeEmailByUser = 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 | userDisplayEmail == stgDisplayEmail ] ++ -- Note that DisplayEmail changes must be confirmed, see 472
[ UserPostLastUpdate =. Just now | userPostAddress /= stgPostAddress ] ++ [ UserPostLastUpdate =. Just now | userPostAddress /= stgPostAddress ] ++
[ UserDisplayName =. stgDisplayName [ UserDisplayName =. stgDisplayName
, UserMaxFavourites =. stgMaxFavourites , UserMaxFavourites =. stgMaxFavourites
, UserMaxFavouriteTerms =. stgMaxFavouriteTerms , UserMaxFavouriteTerms =. stgMaxFavouriteTerms
@ -490,16 +465,14 @@ serveProfileR (uid, user@User{..}) = do
, UserWarningDays =. stgWarningDays , UserWarningDays =. stgWarningDays
, UserNotificationSettings =. stgNotificationSettings , UserNotificationSettings =. stgNotificationSettings
, UserShowSex =. stgShowSex , UserShowSex =. stgShowSex
, UserPinPassword =. (stgPinPassword & canonical) , UserPinPassword =. stgPinPassword
, UserPostAddress =. (stgPostAddress & canonical) , UserPostAddress =. stgPostAddress
, UserPrefersPostal =. stgPrefersPostal , UserPrefersPostal =. stgPrefersPostal
, UserTelephone =. (stgTelephone & canonical)
, UserMobile =. (stgMobile & canonical)
, UserExamOfficeGetSynced =. (stgExamOfficeSettings & eosettingsGetSynced) , UserExamOfficeGetSynced =. (stgExamOfficeSettings & eosettingsGetSynced)
, UserExamOfficeGetLabels =. (stgExamOfficeSettings & eosettingsGetLabels) , UserExamOfficeGetLabels =. (stgExamOfficeSettings & eosettingsGetLabels)
] ]
updateFavourites Nothing updateFavourites Nothing
when (changeEmailByUser && changeEmailProper) $ do when (stgDisplayEmail /= userDisplayEmail) $ do
queueDBJob $ JobChangeUserDisplayEmail uid stgDisplayEmail queueDBJob $ JobChangeUserDisplayEmail uid stgDisplayEmail
addMessageI Info $ MsgUserDisplayEmailChangeSent stgDisplayEmail addMessageI Info $ MsgUserDisplayEmailChangeSent stgDisplayEmail
let let
@ -525,8 +498,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 +563,70 @@ 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 -- avsCards <- maybe (pure mempty) (\a -> selectList [UserAvsCardPersonId ==. userAvsPersonId a] []) avsId
usrAutomatic = updateAutomatic . mayUpdate usrVal avsId . mkCheckUpdate
addressLinkdIcon <- messageTooltip <$> messageIconI Info IconLink MsgAddressIsLinkedTip
(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 +643,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 +684,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 +713,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 +736,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 +746,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 +759,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 +770,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 +814,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 +830,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 +844,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 +873,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 +884,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 +909,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 +946,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 +960,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 +994,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 +1112,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

View File

@ -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
| 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,36 +470,42 @@ 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
, dbParamsFormAttrs = [] , dbParamsFormAttrs = []
, dbParamsFormSubmit = FormSubmit , dbParamsFormSubmit = FormSubmit
, dbParamsFormAdditional , dbParamsFormAdditional
= renderAForm FormStandard $ (, mempty) . First . Just = renderAForm FormStandard
$ (, mempty) . First . Just
<$> multiActionA acts (fslI MsgTableAction) Nothing <$> multiActionA acts (fslI MsgTableAction) Nothing
, dbParamsFormEvaluate = liftHandler . runFormPost , dbParamsFormEvaluate = liftHandler . runFormPost
, dbParamsFormResult = id , dbParamsFormResult = id
@ -518,32 +526,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 +561,66 @@ 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 $ pure QualificationActRenewData
<$> 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 icnSuper = text2markup " " <> icon IconSupervisor
, guardMonoid isAdmin $ colUserMatriclenr isAdmin cs = [ (cmpName, cmpSpr)
| Entity _ UserCompany{userCompanyCompany=cmpId, userCompanySupervisor=cmpSpr} <- cmps
, let cmpName = maybe (unCompanyKey cmpId) companyName $ Map.lookup cmpId cmpMap
]
companies = intercalate (text2markup ", ") $
(\(cmpName, cmpSpr) -> text2markup (CI.original cmpName) <> bool mempty icnSuper cmpSpr) <$> cs
in wgtCell companies
, guardMonoid isAdmin colUserMatriclenr
-- , 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, selectedUsers) | isAdmin -> do
noks <- runDB $ renewValidQualificationUsers qid (canonical $ Just $ Left renewReason) Nothing $ Set.toList selectedUsers noks <- runDB $ renewValidQualificationUsers qid 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 +635,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

View File

@ -16,7 +16,6 @@ import Handler.Utils
import Handler.Utils.Csv import Handler.Utils.Csv
import Handler.Utils.Profile import Handler.Utils.Profile
import qualified Data.Text as Text (intercalate)
-- import qualified Data.CaseInsensitive as CI -- 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 ((:&)(..))
@ -27,10 +26,10 @@ 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 +37,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 +50,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,18 +94,18 @@ 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 let ldap_cutoff = addDiffDaysRollOver (fromMonths $ -3) now
qualUsers <- runDBRead $ E.select $ do qualUsers <- runDB $ E.select $ do
(qual :& qualUser :& user :& qualBlock) <- (qual :& qualUser :& user :& qualBlock) <-
E.from $ E.table @Qualification 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
) )
@ -116,19 +115,19 @@ getQualificationSAPDirectR = do
E.&&. (E.justVal ldap_cutoff E.<=. user E.^. UserLastLdapSynchronisation) E.&&. (E.justVal ldap_cutoff E.<=. user E.^. UserLastLdapSynchronisation)
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
@ -138,13 +137,10 @@ getQualificationSAPDirectR = do
csvOpts = def { csvFormat = fmtOpts } csvOpts = def { csvFormat = fmtOpts }
csvSheetName = "fradrive_sap_" <> fdate <> ".csv" csvSheetName = "fradrive_sap_" <> fdate <> ".csv"
nr = length qualUsers nr = length qualUsers
msg = "Qualification download file " <> csvSheetName <> " containing " <> tshow nr <> " rows" msg = "Qualification download file " <> csvSheetName <> " containing " <> tshow nr <> " rows"
quals = Text.intercalate ", " $ nubOrd $ mapMaybe (view (_2 . E._unValue)) qualUsers
$logInfoS "SAP" msg $logInfoS "SAP" msg
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
-- 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

View File

@ -74,7 +74,7 @@ mkSubmissionArchiveTable tid ssh csh shn showCorrection smid = do
isFile' = origIsFile <|> corrIsFile isFile' = origIsFile <|> corrIsFile
in maybeCell ((,) <$> fileTitle'' <*> isFile') $ \(fileTitle', isFile) -> if in maybeCell ((,) <$> fileTitle'' <*> isFile') $ \(fileTitle', isFile) -> if
| Just True <- origIsFile -> anchorCell (subDownloadLink SubmissionOriginal fileTitle') [whamlet|#{fileTitle'}|] | Just True <- origIsFile -> anchorCell (subDownloadLink SubmissionOriginal fileTitle') [whamlet|#{fileTitle'}|]
| otherwise -> stringCell $ bool (<> "/") id isFile fileTitle' | otherwise -> textCell $ bool (<> "/") id isFile fileTitle'
, guardOn showCorrection . sortable (toNothing "state") (i18nCell MsgTableCorState) $ \t -> case t ^? resultCorrected of , guardOn showCorrection . sortable (toNothing "state") (i18nCell MsgTableCorState) $ \t -> case t ^? resultCorrected of
Nothing -> cell mempty Nothing -> cell mempty
Just (Entity _ SubmissionFile{..}) -> tellCell (Any True) $ if Just (Entity _ SubmissionFile{..}) -> tellCell (Any True) $ if

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