From 38a4e6cdb7499b10843eec04e6aab47f5a7b04d9 Mon Sep 17 00:00:00 2001 From: Gregor Kleen Date: Mon, 25 Nov 2019 10:25:52 +0100 Subject: [PATCH] chore: merge master --- .gitlab-ci.yml | 234 ++ CHANGELOG.md | 1478 +++++++----- clean.sh | 64 +- config/settings.yml | 2 + frontend/src/services/i18n/i18n.js | 33 +- frontend/src/services/i18n/i18n.spec.js | 4 +- frontend/src/utils/form/datepicker.js | 31 +- frontend/src/utils/inputs/checkbox.js | 2 +- frontend/src/utils/inputs/checkbox.scss | 19 +- frontend/src/utils/inputs/inputs.scss | 5 +- frontend/src/utils/navbar/navbar.js | 48 + .../src/utils/navbar/navbar.scss | 56 +- frontend/src/utils/utils.js | 2 + messages/button/en.msg | 3 + messages/campus/de.msg | 1 + messages/campus/en.msg | 6 + messages/dummy/de.msg | 3 +- messages/dummy/en.msg | 3 + .../frontend/{de.msg => de-de-formal.msg} | 2 +- messages/frontend/en.msg | 4 + messages/pw-hash/de.msg | 4 +- messages/pw-hash/en.msg | 4 + messages/uniworx/{de.msg => de-de-formal.msg} | 247 +- messages/uniworx/en-eu.msg | 2139 +++++++++++++++++ models/files.model | 6 + models/jobs.model | 7 + models/sheets.model | 4 +- models/users.model | 13 +- package-lock.json | 2 +- package.json | 10 +- package.yaml | 11 +- routes | 6 +- src/Application.hs | 120 +- src/Auth/Dummy.hs | 5 +- src/Auth/LDAP.hs | 3 +- src/Auth/PWHash.hs | 10 +- src/Cron.hs | 12 +- .../Universe/Instances/Reverse/WithIndex.hs | 20 + src/Database/Esqueleto/Utils.hs | 4 + src/Foundation.hs | 847 +++---- src/Foundation/I18n.hs | 313 +++ src/Foundation/Routes.hs | 10 + src/Foundation/Type.hs | 58 + src/Handler/Admin.hs | 8 +- src/Handler/Admin/Test.hs | 6 +- src/Handler/Allocation/Application.hs | 16 +- src/Handler/Common.hs | 5 +- src/Handler/Corrections.hs | 82 +- src/Handler/Course/Edit.hs | 5 +- src/Handler/Course/LecturerInvite.hs | 2 +- src/Handler/Course/List.hs | 2 +- src/Handler/Course/ParticipantInvite.hs | 2 +- src/Handler/Course/Register.hs | 4 +- src/Handler/CryptoIDDispatch.hs | 4 +- src/Handler/Exam/CorrectorInvite.hs | 2 +- src/Handler/Exam/RegistrationInvite.hs | 2 +- src/Handler/ExamOffice/Exam.hs | 2 +- src/Handler/ExamOffice/Users.hs | 2 +- src/Handler/Home.hs | 10 +- src/Handler/Info.hs | 19 + src/Handler/Info/TH.hs | 23 + src/Handler/Metrics.hs | 45 + src/Handler/Profile.hs | 36 +- src/Handler/Sheet.hs | 331 ++- src/Handler/Submission.hs | 10 +- src/Handler/SystemMessage.hs | 26 +- src/Handler/Term.hs | 76 +- src/Handler/Tutorial/TutorInvite.hs | 2 +- src/Handler/Users.hs | 4 +- src/Handler/Users/Add.hs | 2 +- src/Handler/Utils/DateTime.hs | 2 +- src/Handler/Utils/Exam.hs | 2 +- src/Handler/Utils/Form.hs | 85 +- src/Handler/Utils/I18n.hs | 39 +- src/Handler/Utils/Invitations.hs | 2 +- src/Handler/Utils/Mail.hs | 4 +- src/Handler/Utils/Rating.hs | 6 +- src/Handler/Utils/Routes.hs | 11 + src/Handler/Utils/Sheet.hs | 2 +- src/Handler/Utils/Submission.hs | 4 +- src/Handler/Utils/Table/Columns.hs | 6 +- src/Import/NoModel.hs | 4 + src/Jobs.hs | 62 +- src/Jobs/Crontab.hs | 81 +- src/Jobs/Handler/ChangeUserDisplayEmail.hs | 2 +- src/Jobs/Handler/PruneFiles.hs | 38 + src/Jobs/Handler/SendNotification.hs | 8 +- src/Jobs/Handler/SendNotification/Utils.hs | 2 +- src/Jobs/Handler/SendPasswordReset.hs | 2 +- src/Jobs/Types.hs | 2 + src/Mail.hs | 46 +- src/Model/Migration.hs | 8 + src/Model/Tokens.hs | 10 +- src/Model/Types.hs | 1 + src/Model/Types/Common.hs | 2 + src/Model/Types/Languages.hs | 25 + src/Model/Types/Security.hs | 23 + src/Model/Types/TH/PathPiece.hs | 15 +- src/Prometheus/Instances.hs | 29 + src/Settings.hs | 6 + src/Utils/DateTime.hs | 13 +- src/Utils/Form.hs | 1 + src/Utils/Frontend/I18n.hs | 8 + src/Utils/Lang.hs | 58 +- src/Utils/Lens.hs | 2 + src/Utils/Parameters.hs | 1 + src/Utils/Sheet.hs | 7 +- src/Utils/Sql.hs | 2 +- src/Utils/TH/Routes.hs | 25 + src/Utils/Tokens.hs | 2 +- stack.yaml | 3 + start.sh | 2 +- templates/adminFeatures.hamlet | 11 +- templates/adminUser.hamlet | 6 +- templates/corrections-overview.hamlet | 2 +- templates/default-layout.lucius | 2 +- templates/deletedUser.hamlet | 10 +- templates/glossary.cassius | 14 + templates/glossary.hamlet | 5 + templates/i18n.julius | 2 +- .../admin-test/de-de-formal.hamlet} | 0 templates/i18n/admin-test/en-eu.hamlet | 65 + .../{de.hamlet => de-de-formal.hamlet} | 0 templates/i18n/allocation-info/en-eu.hamlet | 88 + .../{de.hamlet => de-de-formal.hamlet} | 7 + templates/i18n/changelog/en-eu.hamlet | 260 ++ .../{de.hamlet => de-de-formal.hamlet} | 0 .../en-eu.hamlet | 27 + .../{de.hamlet => de-de-formal.hamlet} | 0 .../en-eu.hamlet | 25 + .../data-delete/de-de-formal.hamlet} | 0 templates/i18n/data-delete/en-eu.hamlet | 25 + .../{de.hamlet => de-de-formal.hamlet} | 2 +- templates/i18n/data-protection/en.hamlet | 196 ++ .../{de.hamlet => de-de-formal.hamlet} | 0 .../computed-values-tip/en-eu.hamlet | 23 + .../{de.hamlet => de-de-formal.hamlet} | 1 - templates/i18n/featureList/en-eu.hamlet | 7 + .../administrator.de-de-formal.hamlet | 7 + .../i18n/glossary/administrator.en-eu.hamlet | 7 + .../glossary/allocation.de-de-formal.hamlet | 7 + .../i18n/glossary/allocation.en-eu.hamlet | 6 + .../glossary/applicant.de-de-formal.hamlet | 8 + .../i18n/glossary/applicant.en-eu.hamlet | 9 + .../i18n/glossary/clone.de-de-formal.hamlet | 6 + templates/i18n/glossary/clone.en-eu.hamlet | 5 + .../glossary/comm-course.de-de-formal.hamlet | 6 + .../i18n/glossary/comm-course.en-eu.hamlet | 6 + .../glossary/corrector.de-de-formal.hamlet | 16 + .../i18n/glossary/corrector.en-eu.hamlet | 15 + ...e-application-required.de-de-formal.hamlet | 14 + .../course-application-required.en-eu.hamlet | 13 + ...e-application-template.de-de-formal.hamlet | 8 + .../course-application-template.en-eu.hamlet | 8 + .../glossary/course-exams.de-de-formal.hamlet | 4 + .../i18n/glossary/course-exams.en-eu.hamlet | 4 + .../course-lecturers.de-de-formal.hamlet | 5 + .../glossary/course-lecturers.en-eu.hamlet | 4 + .../course-material.de-de-formal.hamlet | 5 + .../glossary/course-material.en-eu.hamlet | 4 + .../course-participant.de-de-formal.hamlet | 8 + .../glossary/course-participant.en-eu.hamlet | 8 + .../glossary/csv-format.de-de-formal.hamlet | 12 + .../i18n/glossary/csv-format.en-eu.hamlet | 10 + .../i18n/glossary/deficit.de-de-formal.hamlet | 9 + templates/i18n/glossary/deficit.en-eu.hamlet | 9 + ...finition-course-events.de-de-formal.hamlet | 5 + .../definition-course-events.en-eu.hamlet | 5 + ...definition-course-news.de-de-formal.hamlet | 4 + .../definition-course-news.en-eu.hamlet | 4 + .../glossary/exam-result.de-de-formal.hamlet | 8 + .../i18n/glossary/exam-result.en-eu.hamlet | 7 + .../exercise-sheet.de-de-formal.hamlet | 8 + .../i18n/glossary/exercise-sheet.en-eu.hamlet | 7 + .../glossary/invitations.de-de-formal.hamlet | 13 + .../i18n/glossary/invitations.en-eu.hamlet | 10 + .../navigation-favourites.de-de-formal.hamlet | 6 + .../navigation-favourites.en-eu.hamlet | 4 + .../school-evaluation.de-de-formal.hamlet | 6 + .../glossary/school-evaluation.en-eu.hamlet | 5 + .../school-exam-office.de-de-formal.hamlet | 11 + .../glossary/school-exam-office.en-eu.hamlet | 10 + .../school-lecturer.de-de-formal.hamlet | 4 + .../glossary/school-lecturer.en-eu.hamlet | 4 + .../i18n/glossary/school.de-de-formal.hamlet | 5 + templates/i18n/glossary/school.en-eu.hamlet | 5 + .../glossary/sheet-group.de-de-formal.hamlet | 11 + .../i18n/glossary/sheet-group.en-eu.hamlet | 10 + .../sheet-pseudonym.de-de-formal.hamlet | 10 + .../glossary/sheet-pseudonym.en-eu.hamlet | 9 + .../{de.hamlet => de-de-formal.hamlet} | 5 +- templates/i18n/html-input/en-eu.hamlet | 10 + .../{de.hamlet => de-de-formal.hamlet} | 4 +- templates/i18n/implementation/en-eu.hamlet | 30 + .../{de.hamlet => de-de-formal.hamlet} | 0 templates/i18n/imprint/en.hamlet | 86 + .../{de.hamlet => de-de-formal.hamlet} | 44 +- templates/i18n/info-lecturer/en-eu.hamlet | 380 +++ templates/i18n/info-lecturer/en.hamlet | 380 +++ templates/i18n/knownBugs/de-de-formal.hamlet | 10 + templates/i18n/knownBugs/de.hamlet | 6 - templates/i18n/knownBugs/en-eu.hamlet | 10 + .../i18n/profile-remarks/de-de-formal.hamlet | 22 + templates/i18n/profile-remarks/en-eu.hamlet | 24 + .../{de.hamlet => de-de-formal.hamlet} | 0 .../profile/displayNameRules/en-eu.hamlet | 9 + .../{de.hamlet => de-de-formal.hamlet} | 8 +- .../profile/tokenExplanation/en-eu.hamlet | 17 + .../{de.hamlet => de-de-formal.hamlet} | 3 +- templates/i18n/set-display-email/en-eu.hamlet | 5 + .../{de.hamlet => de-de-formal.hamlet} | 9 +- templates/i18n/sheet-edit/en-eu.hamlet | 24 + .../{de.hamlet => de-de-formal.hamlet} | 0 .../table/csv-import-explanation/en-eu.hamlet | 54 + .../{de.hamlet => de-de-formal.hamlet} | 5 +- templates/i18n/unauth-home/en-eu.hamlet | 6 + templates/mail/support.hamlet | 11 +- .../messages/submissionFilesIgnored.hamlet | 2 +- templates/metrics.hamlet | 46 + templates/profileData.hamlet | 59 +- templates/sheetShow.hamlet | 19 +- templates/submission.hamlet | 9 +- templates/widgets/asidenav/asidenav.hamlet | 3 +- templates/widgets/fields/bool.hamlet | 7 +- templates/widgets/lipsum/lipsum.hamlet | 10 - templates/widgets/navbar/navbar.hamlet | 12 +- test/Database.hs | 170 +- test/Handler/Utils/RatingSpec.hs | 30 + test/MailSpec.hs | 7 +- test/Model/RatingSpec.hs | 16 + test/Model/Types/LanguagesSpec.hs | 14 + test/Model/TypesSpec.hs | 2 + test/ModelSpec.hs | 16 +- test/TestImport.hs | 7 +- 234 files changed, 8012 insertions(+), 1987 deletions(-) create mode 100644 .gitlab-ci.yml create mode 100644 frontend/src/utils/navbar/navbar.js rename templates/widgets/navbar/navbar.lucius => frontend/src/utils/navbar/navbar.scss (83%) create mode 100644 messages/button/en.msg create mode 100644 messages/campus/en.msg create mode 100644 messages/dummy/en.msg rename messages/frontend/{de.msg => de-de-formal.msg} (50%) create mode 100644 messages/frontend/en.msg create mode 100644 messages/pw-hash/en.msg rename messages/uniworx/{de.msg => de-de-formal.msg} (91%) create mode 100644 messages/uniworx/en-eu.msg create mode 100644 src/Data/Universe/Instances/Reverse/WithIndex.hs create mode 100644 src/Foundation/I18n.hs create mode 100644 src/Foundation/Routes.hs create mode 100644 src/Foundation/Type.hs create mode 100644 src/Handler/Info/TH.hs create mode 100644 src/Handler/Metrics.hs create mode 100644 src/Handler/Utils/Routes.hs create mode 100644 src/Jobs/Handler/PruneFiles.hs create mode 100644 src/Model/Types/Languages.hs create mode 100644 src/Prometheus/Instances.hs create mode 100644 src/Utils/TH/Routes.hs create mode 100644 templates/glossary.cassius create mode 100644 templates/glossary.hamlet rename templates/{adminTest.hamlet => i18n/admin-test/de-de-formal.hamlet} (100%) create mode 100644 templates/i18n/admin-test/en-eu.hamlet rename templates/i18n/allocation-info/{de.hamlet => de-de-formal.hamlet} (100%) create mode 100644 templates/i18n/allocation-info/en-eu.hamlet rename templates/i18n/changelog/{de.hamlet => de-de-formal.hamlet} (97%) create mode 100644 templates/i18n/changelog/en-eu.hamlet rename templates/i18n/corrections-upload-instructions/{de.hamlet => de-de-formal.hamlet} (100%) create mode 100644 templates/i18n/corrections-upload-instructions/en-eu.hamlet rename templates/i18n/course-exam-office-explanation/{de.hamlet => de-de-formal.hamlet} (100%) create mode 100644 templates/i18n/course-exam-office-explanation/en-eu.hamlet rename templates/{widgets/data-delete/data-delete.hamlet => i18n/data-delete/de-de-formal.hamlet} (100%) create mode 100644 templates/i18n/data-delete/en-eu.hamlet rename templates/i18n/data-protection/{de.hamlet => de-de-formal.hamlet} (99%) create mode 100644 templates/i18n/data-protection/en.hamlet rename templates/i18n/exam-users/computed-values-tip/{de.hamlet => de-de-formal.hamlet} (100%) create mode 100644 templates/i18n/exam-users/computed-values-tip/en-eu.hamlet rename templates/i18n/featureList/{de.hamlet => de-de-formal.hamlet} (76%) create mode 100644 templates/i18n/featureList/en-eu.hamlet create mode 100644 templates/i18n/glossary/administrator.de-de-formal.hamlet create mode 100644 templates/i18n/glossary/administrator.en-eu.hamlet create mode 100644 templates/i18n/glossary/allocation.de-de-formal.hamlet create mode 100644 templates/i18n/glossary/allocation.en-eu.hamlet create mode 100644 templates/i18n/glossary/applicant.de-de-formal.hamlet create mode 100644 templates/i18n/glossary/applicant.en-eu.hamlet create mode 100644 templates/i18n/glossary/clone.de-de-formal.hamlet create mode 100644 templates/i18n/glossary/clone.en-eu.hamlet create mode 100644 templates/i18n/glossary/comm-course.de-de-formal.hamlet create mode 100644 templates/i18n/glossary/comm-course.en-eu.hamlet create mode 100644 templates/i18n/glossary/corrector.de-de-formal.hamlet create mode 100644 templates/i18n/glossary/corrector.en-eu.hamlet create mode 100644 templates/i18n/glossary/course-application-required.de-de-formal.hamlet create mode 100644 templates/i18n/glossary/course-application-required.en-eu.hamlet create mode 100644 templates/i18n/glossary/course-application-template.de-de-formal.hamlet create mode 100644 templates/i18n/glossary/course-application-template.en-eu.hamlet create mode 100644 templates/i18n/glossary/course-exams.de-de-formal.hamlet create mode 100644 templates/i18n/glossary/course-exams.en-eu.hamlet create mode 100644 templates/i18n/glossary/course-lecturers.de-de-formal.hamlet create mode 100644 templates/i18n/glossary/course-lecturers.en-eu.hamlet create mode 100644 templates/i18n/glossary/course-material.de-de-formal.hamlet create mode 100644 templates/i18n/glossary/course-material.en-eu.hamlet create mode 100644 templates/i18n/glossary/course-participant.de-de-formal.hamlet create mode 100644 templates/i18n/glossary/course-participant.en-eu.hamlet create mode 100644 templates/i18n/glossary/csv-format.de-de-formal.hamlet create mode 100644 templates/i18n/glossary/csv-format.en-eu.hamlet create mode 100644 templates/i18n/glossary/deficit.de-de-formal.hamlet create mode 100644 templates/i18n/glossary/deficit.en-eu.hamlet create mode 100644 templates/i18n/glossary/definition-course-events.de-de-formal.hamlet create mode 100644 templates/i18n/glossary/definition-course-events.en-eu.hamlet create mode 100644 templates/i18n/glossary/definition-course-news.de-de-formal.hamlet create mode 100644 templates/i18n/glossary/definition-course-news.en-eu.hamlet create mode 100644 templates/i18n/glossary/exam-result.de-de-formal.hamlet create mode 100644 templates/i18n/glossary/exam-result.en-eu.hamlet create mode 100644 templates/i18n/glossary/exercise-sheet.de-de-formal.hamlet create mode 100644 templates/i18n/glossary/exercise-sheet.en-eu.hamlet create mode 100644 templates/i18n/glossary/invitations.de-de-formal.hamlet create mode 100644 templates/i18n/glossary/invitations.en-eu.hamlet create mode 100644 templates/i18n/glossary/navigation-favourites.de-de-formal.hamlet create mode 100644 templates/i18n/glossary/navigation-favourites.en-eu.hamlet create mode 100644 templates/i18n/glossary/school-evaluation.de-de-formal.hamlet create mode 100644 templates/i18n/glossary/school-evaluation.en-eu.hamlet create mode 100644 templates/i18n/glossary/school-exam-office.de-de-formal.hamlet create mode 100644 templates/i18n/glossary/school-exam-office.en-eu.hamlet create mode 100644 templates/i18n/glossary/school-lecturer.de-de-formal.hamlet create mode 100644 templates/i18n/glossary/school-lecturer.en-eu.hamlet create mode 100644 templates/i18n/glossary/school.de-de-formal.hamlet create mode 100644 templates/i18n/glossary/school.en-eu.hamlet create mode 100644 templates/i18n/glossary/sheet-group.de-de-formal.hamlet create mode 100644 templates/i18n/glossary/sheet-group.en-eu.hamlet create mode 100644 templates/i18n/glossary/sheet-pseudonym.de-de-formal.hamlet create mode 100644 templates/i18n/glossary/sheet-pseudonym.en-eu.hamlet rename templates/i18n/html-input/{de.hamlet => de-de-formal.hamlet} (72%) create mode 100644 templates/i18n/html-input/en-eu.hamlet rename templates/i18n/implementation/{de.hamlet => de-de-formal.hamlet} (89%) create mode 100644 templates/i18n/implementation/en-eu.hamlet rename templates/i18n/imprint/{de.hamlet => de-de-formal.hamlet} (100%) create mode 100644 templates/i18n/imprint/en.hamlet rename templates/i18n/info-lecturer/{de.hamlet => de-de-formal.hamlet} (90%) create mode 100644 templates/i18n/info-lecturer/en-eu.hamlet create mode 100644 templates/i18n/info-lecturer/en.hamlet create mode 100644 templates/i18n/knownBugs/de-de-formal.hamlet delete mode 100644 templates/i18n/knownBugs/de.hamlet create mode 100644 templates/i18n/knownBugs/en-eu.hamlet create mode 100644 templates/i18n/profile-remarks/de-de-formal.hamlet create mode 100644 templates/i18n/profile-remarks/en-eu.hamlet rename templates/i18n/profile/displayNameRules/{de.hamlet => de-de-formal.hamlet} (100%) create mode 100644 templates/i18n/profile/displayNameRules/en-eu.hamlet rename templates/i18n/profile/tokenExplanation/{de.hamlet => de-de-formal.hamlet} (93%) create mode 100644 templates/i18n/profile/tokenExplanation/en-eu.hamlet rename templates/i18n/set-display-email/{de.hamlet => de-de-formal.hamlet} (56%) create mode 100644 templates/i18n/set-display-email/en-eu.hamlet rename templates/i18n/sheet-edit/{de.hamlet => de-de-formal.hamlet} (91%) create mode 100644 templates/i18n/sheet-edit/en-eu.hamlet rename templates/i18n/table/csv-import-explanation/{de.hamlet => de-de-formal.hamlet} (100%) create mode 100644 templates/i18n/table/csv-import-explanation/en-eu.hamlet rename templates/i18n/unauth-home/{de.hamlet => de-de-formal.hamlet} (65%) create mode 100644 templates/i18n/unauth-home/en-eu.hamlet create mode 100644 templates/metrics.hamlet delete mode 100644 templates/widgets/lipsum/lipsum.hamlet create mode 100644 test/Handler/Utils/RatingSpec.hs create mode 100644 test/Model/RatingSpec.hs create mode 100644 test/Model/Types/LanguagesSpec.hs diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 000000000..9057a6839 --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,234 @@ +default: + image: + name: fpco/stack-build:lts-13.21 + cache: + paths: + - node_modules + - .stack + - .stack-work + +variables: + STACK_ROOT: "${CI_PROJECT_DIR}/.stack" + CHROME_BIN: "/usr/bin/chromium-browser" + POSTGRES_DB: uniworx_test + POSTGRES_USER: uniworx + POSTGRES_PASSWORD: uniworx + +stages: + - setup + - frontend:build + - yesod:build + - lint + - test + - deploy + +npm install: + stage: setup + script: + - npm install + before_script: &npm + - apt-get update -y + - npm install -g n + - n stable + - npm install -g npm + - hash -r + artifacts: + paths: + - node_modules/ + name: "${CI_JOB_NAME}" + expire_in: "1 day" + retry: 2 + +frontend:build: + stage: frontend:build + script: + - npm run frontend:build + before_script: *npm + needs: + - npm install + artifacts: + paths: + - static/bundles/ + name: "${CI_JOB_NAME}" + expire_in: "1 day" + dependencies: + - npm install + retry: 2 + +frontend:lint: + stage: lint + script: + - npm run frontend:lint + before_script: *npm + needs: + - npm install + dependencies: + - npm install + retry: 2 + +yesod:build:dev: + stage: yesod:build + script: + - stack build --copy-bins --local-bin-path $(pwd)/bin --fast --flag uniworx:-library-only --flag uniworx:dev --flag uniworx:pedantic + needs: + - frontend:build + before_script: + - apt-get update -y + - apt-get install -y --no-install-recommends locales-all + - ln -s $(which g++-7) $(dirname $(which g++-7))/g++ + artifacts: + paths: + - bin/ + name: "${CI_JOB_NAME}" + expire_in: "1 week" + dependencies: + - frontend:build + + only: + variables: + - $CI_COMMIT_REF_NAME !~ /^v[0-9].*/ + retry: 2 + +yesod:build: + stage: yesod:build + script: + - stack build --copy-bins --local-bin-path $(pwd)/bin --flag uniworx:-library-only --flag uniworx:-dev --flag uniworx:pedantic + needs: + - frontend:build + before_script: + - apt-get update -y + - apt-get install -y --no-install-recommends locales-all + - ln -s $(which g++-7) $(dirname $(which g++-7))/g++ + artifacts: + paths: + - bin/ + name: "${CI_JOB_NAME}" + dependencies: + - frontend:build + + only: + variables: + - $CI_COMMIT_REF_NAME =~ /^v[0-9].*/ + retry: 2 + +frontend:test: + stage: test + script: + - npm run frontend:test + needs: + - npm install + before_script: + - apt-get update -y + - npm install -g n + - n stable + - npm install -g npm + - hash -r + - apt-get install -y --no-install-recommends chromium-browser + dependencies: + - npm install + retry: 2 + +hlint:dev: + stage: lint + script: + - stack test --fast --flag uniworx:-library-only --flag uniworx:dev --flag uniworx:pedantic uniworx:test:hlint + needs: + - frontend:build + - yesod:build:dev # For caching + before_script: + - apt-get update -y + - apt-get install -y --no-install-recommends locales-all + - ln -s $(which g++-7) $(dirname $(which g++-7))/g++ + dependencies: + - frontend:build + + only: + variables: + - $CI_COMMIT_REF_NAME !~ /^v[0-9].*/ + retry: 2 + +yesod:test:dev: + services: + - name: postgres:10.10 + alias: postgres + + stage: test + script: + - stack test --coverage --fast --flag uniworx:-library-only --flag uniworx:dev --flag uniworx:pedantic --skip hlint + needs: + - frontend:build + - yesod:build:dev # For caching + before_script: + - apt-get update -y + - apt-get install -y --no-install-recommends locales-all + - ln -s $(which g++-7) $(dirname $(which g++-7))/g++ + dependencies: + - frontend:build + + only: + variables: + - $CI_COMMIT_REF_NAME !~ /^v[0-9].*/ + retry: 2 + +hlint: + stage: lint + script: + - stack test --flag uniworx:-library-only --flag uniworx:-dev --flag uniworx:pedantic uniworx:test:hlint + needs: + - frontend:build + - yesod:build # For caching + before_script: + - apt-get update -y + - apt-get install -y --no-install-recommends locales-all + - ln -s $(which g++-7) $(dirname $(which g++-7))/g++ + dependencies: + - frontend:build + + only: + variables: + - $CI_COMMIT_REF_NAME =~ /^v[0-9].*/ + retry: 2 + +yesod:test: + services: + - name: postgres:10.10 + alias: postgres + + stage: test + script: + - stack test --coverage --flag uniworx:-library-only --flag uniworx:-dev --flag uniworx:pedantic --skip hlint + needs: + - frontend:build + - yesod:build # For caching + before_script: + - apt-get update -y + - apt-get install -y --no-install-recommends locales-all + - ln -s $(which g++-7) $(dirname $(which g++-7))/g++ + dependencies: + - frontend:build + + only: + variables: + - $CI_COMMIT_REF_NAME =~ /^v[0-9].*/ + retry: 2 + +deploy:uniworx3: + stage: deploy + script: + - ssh -i ~/.ssh/id root@uniworx3.ifi.lmu.de occurrence everywhere ([96387cb](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/96387cb)) -* filter submission by not having corrector ([3bded50](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/3bded50)) -* minor heat correction for correction overview ([5546849](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/5546849)) -* **ratings:** disallow ratings for graded sheets without point value ([463b2b7](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/463b2b7)) -* **standard-version:** properly reset staging area before release ([5aa906e](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/5aa906e)) +* **sheet corrector assigment:** minor bugfix ([749cd2f](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/749cd2f)) +* async table js util now knows current random css prefix ([cc90faf](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/cc90faf)) +* **correction assignment:** correcting lecturer's names are shown now ([16c556b](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/16c556b)) +* **corrector assignment:** sheet tabel mixed up columns sorted ([d07f53e](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/d07f53e)) +* **datepicker:** hide number input spinners in datepicker ([2073130](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/2073130)) +* **exam grading keys:** Fix spacing ([24aacef](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/24aacef)) +* **exams:** Fix registration ([1684da0](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/1684da0)) +* **fe:** style notifications acceptably for now ([fc80f08](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/fc80f08)) +* **fe-async-table:** Emulate no-js behaviour when handling pagesize ([28dcc8d](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/28dcc8d)) +* **fe-check-all:** use arrow fn to keep scope in event listeners ([09e681e](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/09e681e)) +* **fe-deflist:** avoid horizontal scroll on pages with deflist ([16d422d](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/16d422d)) +* **Help Widget, Corrector Assignment:** Modal Form closes in place; assign alerts ([89d5364](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/89d5364)), closes [#195](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/issues/195) +* **info-lecturer:** Touch ups ([e1e26ab](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/e1e26ab)) +* **many occurrences throughout the project:** Fix typo: occurence -> occurrence everywhere ([96387cb](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/96387cb)) +* filter submission by not having corrector ([3bded50](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/3bded50)) +* minor heat correction for correction overview ([5546849](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/5546849)) +* **ratings:** disallow ratings for graded sheets without point value ([463b2b7](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/463b2b7)) +* **standard-version:** properly reset staging area before release ([5aa906e](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/5aa906e)) ### Features -* **corrector-assignment:** show load/submission percentages ([228cd50](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/228cd50)) -* make pagesize changes load async ([6486120](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/6486120)) -* **development:** add commitlint to ensure proper commit msgs ([dd528c1](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/dd528c1)) -* **development:** add standard-version for automatic changelog generation ([c495ef5](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/c495ef5)) -* **exams:** CRU (no D) for exams ([67a50c9](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/67a50c9)) -* **exams:** exam registration ([99184ff](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/99184ff)) -* **exams:** Form validation ([6fb1399](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/6fb1399)) -* **fe-heatmap:** add css class heated for heatmap elements ([b09b876](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/b09b876)), closes [#405](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/issues/405) -* **forms:** Introduce more convenient form validation ([f8d0b02](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/f8d0b02)) -* **standard-version:** allow adding additional changes to release ([7ed6fe4](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/7ed6fe4)) -* **standard-version:** complete release workflow ([605e62f](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/605e62f)) +* **corrector-assignment:** show load/submission percentages ([228cd50](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/228cd50)) +* make pagesize changes load async ([6486120](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/6486120)) +* **development:** add commitlint to ensure proper commit msgs ([dd528c1](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/dd528c1)) +* **development:** add standard-version for automatic changelog generation ([c495ef5](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/c495ef5)) +* **exams:** CRU (no D) for exams ([67a50c9](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/67a50c9)) +* **exams:** exam registration ([99184ff](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/99184ff)) +* **exams:** Form validation ([6fb1399](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/6fb1399)) +* **fe-heatmap:** add css class heated for heatmap elements ([b09b876](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/b09b876)), closes [#405](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/issues/405) +* **forms:** Introduce more convenient form validation ([f8d0b02](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/f8d0b02)) +* **standard-version:** allow adding additional changes to release ([7ed6fe4](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/7ed6fe4)) +* **standard-version:** complete release workflow ([605e62f](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/605e62f)) ### Tests -* Does ist build with everything except for `makeClassy ''Entity`? Probably the functional dependency is to blame?! ([bb552c4](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/bb552c4)) -* removing makeCLassyFor maybe build works then? ([2550f74](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/2550f74)) +* Does ist build with everything except for `makeClassy ''Entity`? Probably the functional dependency is to blame?! ([bb552c4](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/bb552c4)) +* removing makeCLassyFor maybe build works then? ([2550f74](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/2550f74)) ### BREAKING CHANGES diff --git a/clean.sh b/clean.sh index 02487e8b2..d63a4deab 100755 --- a/clean.sh +++ b/clean.sh @@ -4,39 +4,39 @@ set -e [ "${FLOCKER}" != "$0" ] && exec env FLOCKER="$0" flock -en .stack-work.lock "$0" "$@" || : -case $1 in - "") - exec -- stack clean - ;; - *) - target=".stack-work-${1}" - shift +if [[ -n "${1}" ]]; then + target=".stack-work-${1}" +else + target=".stack-work" +fi +shift - if [[ ! -d "${target}" ]]; then - printf "%s does not exist or is no directory\n" "${target}" >&2 - exit 1 - fi - if [[ -e .stack-work-clean ]]; then - printf ".stack-work-clean exists\n" >&2 - exit 1 - fi +if [[ ! -d "${target}" ]]; then + printf "%s does not exist or is no directory\n" "${target}" >&2 + exit 1 +fi - move-back() { - if [[ -d .stack-work ]]; then - mv -v .stack-work "${target}" - else - mkdir -v "${target}" - fi - [[ -d .stack-work-clean ]] && mv -v .stack-work-clean .stack-work - } +if [[ "${target}" != ".stack-work" ]]; then + if [[ -e .stack-work-clean ]]; then + printf ".stack-work-clean exists\n" >&2 + exit 1 + fi - mv -v .stack-work .stack-work-clean - mv -v "${target}" .stack-work - trap move-back EXIT + move-back() { + if [[ -d .stack-work ]]; then + mv -v .stack-work "${target}" + else + mkdir -v "${target}" + fi + [[ -d .stack-work-clean ]] && mv -v .stack-work-clean .stack-work + } - ( - set -ex - stack clean $@ - ) - ;; -esac + mv -v .stack-work .stack-work-clean + mv -v "${target}" .stack-work + trap move-back EXIT +fi + +( + set -ex + stack clean $@ +) diff --git a/config/settings.yml b/config/settings.yml index 63d2fcd88..df21d993c 100644 --- a/config/settings.yml +++ b/config/settings.yml @@ -30,6 +30,8 @@ session-timeout: 7200 jwt-expiration: 604800 jwt-encoding: HS256 maximum-content-length: "_env:MAX_UPLOAD_SIZE:134217728" +session-files-expire: 3600 +prune-unreferenced-files: 86400 health-check-interval: matching-cluster-config: "_env:HEALTHCHECK_INTERVAL_MATCHING_CLUSTER_CONFIG:600" http-reachable: "_env:HEALTHCHECK_INTERVAL_HTTP_REACHABLE:600" diff --git a/frontend/src/services/i18n/i18n.js b/frontend/src/services/i18n/i18n.js index 7061f6ba6..fd383b1b6 100644 --- a/frontend/src/services/i18n/i18n.js +++ b/frontend/src/services/i18n/i18n.js @@ -1,3 +1,5 @@ +import moment from 'moment'; + /** * I18n * @@ -13,10 +15,15 @@ export class I18n { - translations = {}; + _translations = {}; + _datetimeLocale = undefined; add(id, translation) { - this.translations[id] = translation; + if (!this._translations[id]) { + this._translations[id] = translation; + } else { + throw new Error('I18N Error: Attempting to set translation multiple times for »' + id + '«!'); + } } addMany(manyTranslations) { @@ -24,9 +31,27 @@ export class I18n { } get(id) { - if (!this.translations[id]) { + if (!this._translations[id]) { throw new Error('I18N Error: Translation missing for »' + id + '«!'); } - return this.translations[id]; + return this._translations[id]; + } + + + setDatetimeLocale(locale) { + if (!this._datetimeLocale) { + moment.locale(locale); + this._datetimeLocale = locale; + } else { + throw new Error('I18N Error: Attempting to set datetime locale multiple times!'); + } + } + + getDatetimeLocale() { + if (!this._datetimeLocale) { + throw new Error('I18N Error: Attempting to access datetime locale when it has not been set!'); + } + + return this._datetimeLocale; } } diff --git a/frontend/src/services/i18n/i18n.spec.js b/frontend/src/services/i18n/i18n.spec.js index 1b4edf3c4..b809895e5 100644 --- a/frontend/src/services/i18n/i18n.spec.js +++ b/frontend/src/services/i18n/i18n.spec.js @@ -9,7 +9,7 @@ describe('I18n', () => { // helper function function expectTranslation(id, value) { - expect(i18n.translations[id]).toMatch(value); + expect(i18n.get(id)).toMatch(value); } it('should create', () => { @@ -38,7 +38,7 @@ describe('I18n', () => { describe('get()', () => { it('should return stored translations', () => { - i18n.translations.id1 = 'something'; + i18n.add('id1', 'something'); expect(i18n.get('id1')).toMatch('something'); }); diff --git a/frontend/src/utils/form/datepicker.js b/frontend/src/utils/form/datepicker.js index 2cc4972c8..2c3ad9f61 100644 --- a/frontend/src/utils/form/datepicker.js +++ b/frontend/src/utils/form/datepicker.js @@ -48,9 +48,6 @@ const DATEPICKER_CONFIG = { timeMinutes: 0, timeSeconds: 0, - // german settings - // TODO: hardcoded, get from current language / settings - locale: 'de', weekStart: 1, dateFormat: FORM_DATE_FORMAT_DATE_DT, timeFormat: FORM_DATE_FORMAT_TIME_DT, @@ -86,6 +83,7 @@ export class Datepicker { datepickerInstance; _element; elementType; + _locale; constructor(element) { if (!element) { @@ -96,6 +94,8 @@ export class Datepicker { return false; } + this._locale = window.App.i18n.getDatetimeLocale(); + // initialize datepickerCollections singleton if not already done if (!Datepicker.datepickerCollections) { Datepicker.datepickerCollections = new Map(); @@ -134,7 +134,7 @@ export class Datepicker { } // initialize tail.datetime (datepicker) instance and let it do weird stuff with the element value - this.datepickerInstance = datetime(this._element, { ...datepickerGlobalConfig, ...datepickerConfig }); + this.datepickerInstance = datetime(this._element, { ...datepickerGlobalConfig, ...datepickerConfig, locale: this._locale }); // reset date to something sane if (parsedMomentDate) @@ -180,14 +180,27 @@ export class Datepicker { // change the selected date in the tail.datetime instance if the value of the input element is changed this._element.addEventListener('change', setDatepickerDate, { once: true }); - // close the instance if something other than the instance was clicked (i.e. if the target is not within the datepicker instance and if any previously clicked calendar view was replaced (is not in the window anymore) because it was clicked). YES, I KNOW - window.addEventListener('click', event => { - if (!this.datepickerInstance.dt.contains(event.target) && window.document.contains(event.target)) { + // close the instance on focusout of any element if another input is focussed that is neither the timepicker nor _element + window.addEventListener('focusout', event => { + const hasFocus = event.relatedTarget !== null; + const focussedIsNotTimepicker = !this.datepickerInstance.dt.contains(event.relatedTarget); + const focussedIsNotElement = event.relatedTarget !== this._element; + const focussedIsInDocument = window.document.contains(event.relatedTarget); + if (hasFocus && focussedIsNotTimepicker && focussedIsNotElement && focussedIsInDocument) + this.datepickerInstance.close(); + }); + + // close the instance on click on any element outside of the datepicker (except the input element itself) + window.addEventListener('click', event => { + const targetIsOutside = !this.datepickerInstance.dt.contains(event.target) + && event.target !== this.datepickerInstance.dt; + const targetIsInDocument = window.document.contains(event.target); + const targetIsNotElement = event.target !== this._element; + if (targetIsOutside && targetIsInDocument && targetIsNotElement) this.datepickerInstance.close(); - } }); - // close the datepicker on escape keydown events + // close the instance on escape keydown events this._element.addEventListener('keydown', event => { if (event.keyCode === KEYCODE_ESCAPE) { this.datepickerInstance.close(); diff --git a/frontend/src/utils/inputs/checkbox.js b/frontend/src/utils/inputs/checkbox.js index e93c88856..6f52ea3ae 100644 --- a/frontend/src/utils/inputs/checkbox.js +++ b/frontend/src/utils/inputs/checkbox.js @@ -5,7 +5,7 @@ var CHECKBOX_CLASS = 'checkbox'; var CHECKBOX_INITIALIZED_CLASS = 'checkbox--initialized'; @Utility({ - selector: 'input[type="checkbox"]', + selector: 'input[type="checkbox"]:not([uw-no-checkbox])', }) export class Checkbox { diff --git a/frontend/src/utils/inputs/checkbox.scss b/frontend/src/utils/inputs/checkbox.scss index 5dc0c9995..817aa5f3a 100644 --- a/frontend/src/utils/inputs/checkbox.scss +++ b/frontend/src/utils/inputs/checkbox.scss @@ -1,19 +1,20 @@ /* CUSTOM CHECKBOXES */ /* Completely replaces legacy checkbox */ +.checkbox [type='checkbox'], #lang-checkbox { + position: fixed; + top: -1px; + left: -1px; + width: 1px; + height: 1px; + overflow: hidden; + display: none; +} + .checkbox { position: relative; display: inline-block; - [type='checkbox'] { - position: fixed; - top: -1px; - left: -1px; - width: 1px; - height: 1px; - overflow: hidden; - } - label { display: block; height: 20px; diff --git a/frontend/src/utils/inputs/inputs.scss b/frontend/src/utils/inputs/inputs.scss index 6f12d81b8..f19a17bda 100644 --- a/frontend/src/utils/inputs/inputs.scss +++ b/frontend/src/utils/inputs/inputs.scss @@ -150,7 +150,6 @@ textarea { padding: 4px 13px; font-size: 1rem; font-family: var(--font-base); - -webkit-appearance: none; appearance: none; border: 1px solid #dbdbdb; border-radius: 2px; @@ -184,8 +183,8 @@ textarea { /* OPTIONS */ -select { - -webkit-appearance: menulist; +select[size = "1"], select:not([size]) { + appearance: menulist; } select, diff --git a/frontend/src/utils/navbar/navbar.js b/frontend/src/utils/navbar/navbar.js new file mode 100644 index 000000000..95af958f4 --- /dev/null +++ b/frontend/src/utils/navbar/navbar.js @@ -0,0 +1,48 @@ +import { Utility } from '../../core/utility'; +import './navbar.scss'; + + +export const LANGUAGE_SELECT_UTIL_SELECTOR = '[uw-language-select]'; +const LANGUAGE_SELECT_INITIALIZED_CLASS = 'language-select--initialized'; + + +@Utility({ + selector: LANGUAGE_SELECT_UTIL_SELECTOR, +}) +export class LanguageSelectUtil { + _element; + checkbox; + + constructor(element) { + if (!element) { + throw new Error('Language Select utility needs to be passed an element!'); + } + + if (element.classList.contains(LANGUAGE_SELECT_INITIALIZED_CLASS)) { + return false; + } + + this._element = element; + this.checkbox = element.querySelector('#lang-checkbox'); + + window.addEventListener('click', event => this.close(event)); + + element.classList.add(LANGUAGE_SELECT_INITIALIZED_CLASS); + } + + close(event) { + if (!this._element.contains(event.target) && window.document.contains(event.target)) { + this.checkbox.checked = false; + } + } + + destroy() { + // TODO + } + +} + + +export const NavbarUtils = [ + LanguageSelectUtil, +]; diff --git a/templates/widgets/navbar/navbar.lucius b/frontend/src/utils/navbar/navbar.scss similarity index 83% rename from templates/widgets/navbar/navbar.lucius rename to frontend/src/utils/navbar/navbar.scss index c3885f975..888d8d5cb 100644 --- a/templates/widgets/navbar/navbar.lucius +++ b/frontend/src/utils/navbar/navbar.scss @@ -68,14 +68,7 @@ color: var(--color-lightwhite); transition: height .2s cubic-bezier(0.03, 0.43, 0.58, 1); overflow: hidden; - - &:hover { - color: var(--color-lightwhite); - - .navbar__link-icon { - opacity: 1; - } - } + cursor: pointer; } .navbar__link-icon { @@ -88,6 +81,7 @@ transition: opacity .2s ease; padding: 2px 4px; text-transform: uppercase; + font-weight: 600; } @media (min-width: 769px) { @@ -146,7 +140,9 @@ .navbar__list-item { position: relative; transition: background-color .1s ease; - + &:not(.navbar__list-item--favorite) + .navbar__list-item--lang-wrapper { + margin-left: 12px; + } &:not(.navbar__list-item--favorite) + .navbar__list-item { margin-left: 12px; } @@ -160,6 +156,9 @@ &:not(.navbar__list-item--favorite) + .navbar__list-item { margin-left: 0; } + &:not(.navbar__list-item--favorite) + .navbar__list-item--lang-wrapper { + margin-left: 0; + } } } @@ -219,9 +218,13 @@ color: var(--color-dark); } -.navbar .navbar__list-item:not(.navbar__list-item--active):not(.navbar__list-item--favorite):hover .navbar__link-wrapper { +.navbar .navbar__list-item:not(.navbar__list-item--active):not(.navbar__list-item--favorite):hover .navbar__link-wrapper, #lang-checkbox:checked ~ * .navbar__link-wrapper { background-color: var(--color-dark); color: var(--color-lightwhite); + + .navbar__link-icon { + opacity: 1; + } } /* sticky state */ @@ -267,3 +270,36 @@ height: var(--header-height-collapsed); } } + + +#lang-dropdown { + display: none; + + position: fixed; + top: var(--header-height); + right: 0; + min-width: 200px; + z-index: 10; + background-color: white; + border-radius: 2px; + box-shadow: 0 0 10px rgba(0,0,0,0.3); + + select { + display: block; + } + + button { + display: block; + width: 100%; + } +} + +#lang-checkbox:checked ~ #lang-dropdown { + display: block; +} + +@media (max-width: 768px) { + #lang-dropdown { + top: var(--header-height-collapsed); + } +} diff --git a/frontend/src/utils/utils.js b/frontend/src/utils/utils.js index 06e095af6..b539edb44 100644 --- a/frontend/src/utils/utils.js +++ b/frontend/src/utils/utils.js @@ -10,6 +10,7 @@ import { MassInput } from './mass-input/mass-input'; import { Modal } from './modal/modal'; import { Tooltip } from './tooltips/tooltips'; import { CourseTeaser } from './course-teaser/course-teaser'; +import { NavbarUtils } from './navbar/navbar'; export const Utils = [ Alerts, @@ -25,4 +26,5 @@ export const Utils = [ ShowHide, Tooltip, CourseTeaser, + ...NavbarUtils, ]; diff --git a/messages/button/en.msg b/messages/button/en.msg new file mode 100644 index 000000000..b468d80ff --- /dev/null +++ b/messages/button/en.msg @@ -0,0 +1,3 @@ +AmbiguousButtons: Multiple active submit buttons +WrongButtonValue: Submit button has wrong value +MultipleButtonValues: Submit button has multiple values diff --git a/messages/campus/de.msg b/messages/campus/de.msg index 9a4b384fc..43d544af9 100644 --- a/messages/campus/de.msg +++ b/messages/campus/de.msg @@ -1,5 +1,6 @@ CampusIdentPlaceholder: Vorname.Nachname@campus.lmu.de CampusIdent: Campus-Kennung CampusPassword: Passwort +CampusPasswordPlaceholder: Passwort CampusSubmit: Abschicken CampusInvalidCredentials: Ungültige Logindaten \ No newline at end of file diff --git a/messages/campus/en.msg b/messages/campus/en.msg new file mode 100644 index 000000000..6264db29e --- /dev/null +++ b/messages/campus/en.msg @@ -0,0 +1,6 @@ +CampusIdentPlaceholder: First.Last@campus.lmu.de +CampusIdent: Campus account +CampusPassword: Password +CampusPasswordPlaceholder: Password +CampusSubmit: Send +CampusInvalidCredentials: Invalid login diff --git a/messages/dummy/de.msg b/messages/dummy/de.msg index 5a24922aa..16bd26af5 100644 --- a/messages/dummy/de.msg +++ b/messages/dummy/de.msg @@ -1,2 +1,3 @@ -DummyIdent: Nutzer-Kennung +DummyIdent: Identifikation +DummyIdentPlaceholder: Identifikation DummyNoFormData: Keine Formulardaten empfangen \ No newline at end of file diff --git a/messages/dummy/en.msg b/messages/dummy/en.msg new file mode 100644 index 000000000..da7b50f0f --- /dev/null +++ b/messages/dummy/en.msg @@ -0,0 +1,3 @@ +DummyIdent: Identification +DummyIdentPlaceholder: Identification +DummyNoFormData: No form data received diff --git a/messages/frontend/de.msg b/messages/frontend/de-de-formal.msg similarity index 50% rename from messages/frontend/de.msg rename to messages/frontend/de-de-formal.msg index f01c31640..a17c4540c 100644 --- a/messages/frontend/de.msg +++ b/messages/frontend/de-de-formal.msg @@ -1,4 +1,4 @@ FilesSelected: Dateien ausgewählt SelectFile: Datei auswählen SelectFiles: Datei(en) auswählen -AsyncFormFailure: Da ist etwas schief gelaufen, das tut uns Leid. Falls das erneut passiert schicke uns gerne eine kurze Beschreibung dieses Ereignisses über das Hilfe-Widget rechts oben. Vielen Dank für deine Hilfe! \ No newline at end of file +AsyncFormFailure: Da ist etwas schief gelaufen, das tut uns Leid. Falls das erneut passiert schicken Sie uns bitte eine kurze Beschreibung dieses Ereignisses über das Hilfe-Widget rechts oben. Vielen Dank für Ihre Hilfe! \ No newline at end of file diff --git a/messages/frontend/en.msg b/messages/frontend/en.msg new file mode 100644 index 000000000..743eb91be --- /dev/null +++ b/messages/frontend/en.msg @@ -0,0 +1,4 @@ +FilesSelected: Files selected +SelectFile: Select file +SelectFiles: Select file(s) +AsyncFormFailure: Something went wrong, we are sorry. If this error occurs again, please let us know by clicking the Support button in the upper right corner. Thank you very much! diff --git a/messages/pw-hash/de.msg b/messages/pw-hash/de.msg index 9fb1eb5e4..6a172120b 100644 --- a/messages/pw-hash/de.msg +++ b/messages/pw-hash/de.msg @@ -1,2 +1,4 @@ PWHashIdent: Identifikation -PWHashPassword: Passwort \ No newline at end of file +PWHashIdentPlaceholder: Identifikation +PWHashPassword: Passwort +PWHashPasswordPlaceholder: Passwort \ No newline at end of file diff --git a/messages/pw-hash/en.msg b/messages/pw-hash/en.msg new file mode 100644 index 000000000..52fb04bdf --- /dev/null +++ b/messages/pw-hash/en.msg @@ -0,0 +1,4 @@ +PWHashIdent: Identification +PWHashIdentPlaceholder: Identification +PWHashPassword: Password +PWHashPasswordPlaceholder: Password diff --git a/messages/uniworx/de.msg b/messages/uniworx/de-de-formal.msg similarity index 91% rename from messages/uniworx/de.msg rename to messages/uniworx/de-de-formal.msg index ac10303ba..c133390fe 100644 --- a/messages/uniworx/de.msg +++ b/messages/uniworx/de-de-formal.msg @@ -1,5 +1,7 @@ PrintDebugForStupid name@Text: Debug message "#{name}" +Logo: Uni2work + BtnSubmit: Senden BtnAbort: Abbrechen BtnDelete: Löschen @@ -71,9 +73,9 @@ Term: Semester TermPlaceholder: W/S + vierstellige Jahreszahl TermStartDay: Erster Tag -TermStartDayTooltip: Üblicherweise immer 1.April oder 1.Oktober +TermStartDayTooltip: Üblicherweise immer 1. April oder 1. Oktober TermEndDay: Letzter Tag -TermEndDayTooltip: Üblicherweise immer 30.September oder 31.März +TermEndDayTooltip: Üblicherweise immer 30. September oder 31. März TermHolidays: Feiertage TermHolidayPlaceholder: Feiertag TermLectureStart: Beginn Vorlesungen @@ -98,6 +100,7 @@ CourseRegistration: Kursanmeldung CourseRegisterOpen: Anmeldung möglich CourseRegisterOk: Erfolgreich zum Kurs angemeldet CourseDeregisterOk: Erfolgreich vom Kurs abgemeldet +CourseApply: Zum Kurs bewerben CourseApplyOk: Erfolgreich zum Kurs beworben CourseRetractApplyOk: Bewerbung zum Kurs erfolgreich zurückgezogen CourseDeregisterLecturerTip: Wenn Sie den Teilnehmer vom Kurs abmelden kann es sein, dass sie Zugriff auf diese Daten verlieren @@ -109,8 +112,8 @@ CourseTutorial: Tutorium CourseSecretWrong: Falsches Passwort CourseSecret: Zugangspasswort CourseEditOk tid@TermId ssh@SchoolId csh@CourseShorthand: Kurs #{tid}-#{ssh}-#{csh} wurde erfolgreich geändert. -CourseNewDupShort tid@TermId ssh@SchoolId csh@CourseShorthand: Kurs #{tid}-#{ssh}-#{csh} konnte nicht erstellt werden: Es gibt bereits einen anderen Kurs mit dem Kürzel #{csh} in diesem Semester. -CourseEditDupShort tid@TermId ssh@SchoolId csh@CourseShorthand: Kurs #{tid}-#{ssh}-#{csh} konnte nicht geändert werden: Es gibt bereits einen anderen Kurs mit dem Kürzel #{csh} in diesem Semester. +CourseNewDupShort tid@TermId ssh@SchoolId csh@CourseShorthand: Kurs #{tid}-#{ssh}-#{csh} konnte nicht erstellt werden: Es gibt bereits einen anderen Kurs mit dem Kürzel #{csh} in diesem Semester und Institut. +CourseEditDupShort tid@TermId ssh@SchoolId csh@CourseShorthand: Kurs #{tid}-#{ssh}-#{csh} konnte nicht geändert werden: Es gibt bereits einen anderen Kurs mit dem Kürzel #{csh} in diesem Semester und Institut. FFSheetName: Name TermCourseListHeading tid@TermId: Kursübersicht #{tid} TermSchoolCourseListHeading tid@TermId school@SchoolName: Kursübersicht #{tid} für #{school} @@ -128,7 +131,7 @@ CourseMembersCountLimited n@Int max@Int: #{n}/#{max} CourseMembersCountOf n@Int mbNum@IntMaybe: #{n} Kursanmeldungen #{maybeToMessage " von " mbNum " möglichen"} CourseName: Name CourseDescription: Beschreibung -CourseDescriptionTip: Beliebiges HTML-Markup ist gestattet +CourseDescriptionTip: Beliebiges Html-Markup ist gestattet CourseHomepageExternal: Externe Homepage CourseShorthand: Kürzel CourseShorthandUnique: Muss nur innerhalb Institut und Semester eindeutig sein. Wird verbatim in die Url der Kursseite übernommen. @@ -142,14 +145,16 @@ CourseRegisterToTip: Darf auch unbegrenzt offen bleiben CourseDeregisterUntilTip: Abmeldung ist ab "Anmeldungen von" bis zu diesem Zeitpunkt erlaubt. Die Abmeldung darf auch unbegrenzt erlaubt bleiben. CourseFilterSearch: Volltext-Suche CourseFilterRegistered: Registriert -CourseFilterNone: Egal +CourseFilterNone: — +BoolIrrelevant: — CourseDeleteQuestion: Wollen Sie den unten aufgeführten Kurs wirklich löschen? CourseDeleted: Kurs gelöscht CourseUserTutorials: Angemeldete Tutorien CourseUserNote: Notiz -CourseUserNoteTooltip: Nur für Dozenten dieses Kurses einsehbar +CourseUserNoteTooltip: Nur für Verwalter dieses Kurses einsehbar CourseUserNoteSaved: Notizänderungen gespeichert CourseUserNoteDeleted: Teilnehmernotiz gelöscht +CourseUserRegister: Zum Kurs anmelden CourseUserDeregister: Vom Kurs abmelden CourseUsersDeregistered count@Int64: #{show count} Teilnehmer vom Kurs abgemeldet CourseUserRegisterTutorial: Zu einem Tutorium anmelden @@ -184,7 +189,7 @@ CourseApplication: Bewerbung CourseApplicationIsParticipant: Kursteilnehmer CourseApplicationExists: Sie haben sich bereits für diesen Kurs beworben -CourseApplicationInvalidAction: Angegeben Aktion kann nicht durchgeführt werden +CourseApplicationInvalidAction: Angegebene Aktion kann nicht durchgeführt werden CourseApplicationCreated csh@CourseShorthand: Erfolgreich zu #{csh} beworben CourseApplicationEdited csh@CourseShorthand: Bewerbung zu #{csh} erfolgreich angepasst CourseApplicationNotEdited csh@CourseShorthand: Bewerbung zu #{csh} hat sich nicht verändert @@ -271,6 +276,8 @@ SheetSubmissionMode: Abgabe-Modus SheetExercise: Aufgabenstellung SheetHint: Hinweis SheetHintFrom: Hinweis ab +SheetHintFromPlaceholder: Datum, sonst nur für Korrektoren +SheetSolutionFromPlaceholder: Datum, sonst nur für Korrektoren SheetSolution: Lösung SheetSolutionFrom: Lösung ab SheetMarking: Hinweise für Korrektoren @@ -282,9 +289,15 @@ SheetDescription: Hinweise für Teilnehmer SheetGroup: Gruppenabgabe SheetVisibleFrom: Sichtbar für Teilnehmer ab SheetVisibleFromTip: Ohne Datum nie sichtbar und keine Abgabe möglich; nur für unfertige Blätter leer lassen, deren Bewertung/Fristen sich noch ändern können -SheetActiveFrom: Beginn Abgabezeitraum -SheetActiveFromTip: Download der Aufgabenstellung erst ab diesem Datum möglich -SheetActiveTo: Ende Abgabezeitraum +SheetActiveFrom: Aktiv ab/Beginn Abgabezeitraum +SheetActiveFromParticipant: Beginn Abgabezeitraum +SheetActiveFromParticipantNoSubmit: Herausgabe der Aufgabestellung +SheetActiveFromTip: Download der Aufgabenstellung und Abgabe erst ab diesem Datum möglich. Ohne Datum keine Abgabe und keine Herausgabe der Aufgabenstellung +SheetActiveFromUnset: Nie +SheetActiveTo: Aktiv bis/Ende Abgabezeitraum +SheetActiveToParticipant: Ende Abgabezeitraum +SheetActiveToTip: Abgabe nur bis zu diesem Datum möglich. Ohne Datum unbeschränkte Abgabe möglich (soweit gefordert). +SheetActiveToUnset: Nie SheetHintFromTip: Ohne Datum nie für Teilnehmer sichtbar, Korrektoren können diese Dateien immer herunterladen SheetSolutionFromTip: Ohne Datum nie für Teilnehmer sichtbar, Korrektoren können diese Dateien immer herunterladen SheetMarkingTip: Hinweise zur Korrektur, sichtbar nur für Korrektoren @@ -315,6 +328,7 @@ SubmissionEditHead tid@TermId ssh@SchoolId csh@CourseShorthand sheetName@SheetNa CorrectionHead tid@TermId ssh@SchoolId csh@CourseShorthand sheetName@SheetName cid@CryptoFileNameSubmission: #{tid}-#{ssh}-#{csh} #{sheetName}: Korrektur SubmissionMembers: Abgebende SubmissionMember: Abgebende(r) +CosubmittorTip: Einladungen per E-Mail erhalten genau jene Adressen, für die nicht gesichert werden kann, dass sie mit der dahinter stehenden Person schon einmal für diesen Kurs abgegeben haben. Wenn eine angegebene Adresse einer Person zugeordnet werden kann, mit der Sie in diesem Kurs schon einmal zusammen abgegeben haben, wird der Name der Person angezeigt und die Abgabe erfolgt sofort auch im Namen jener Person. SubmissionArchive: Zip-Archiv der Abgabedatei(en) SubmissionFile: Datei zur Abgabe SubmissionFiles: Abgegebene Dateien @@ -333,6 +347,8 @@ CourseCorrectionsTitle: Korrekturen für diesen Kurs CorrectorsHead sheetName@SheetName: Korrektoren für #{sheetName} CorrectorAssignTitle: Korrektor zuweisen +CorrectionsGrade: Abgaben online korrigieren + MaterialName: Name MaterialType: Art MaterialTypePlaceholder: Folien, Code, Beispiel, ... @@ -370,6 +386,8 @@ UnauthorizedTokenNotStarted: Ihr Authorisierungs-Token ist noch nicht gültig. UnauthorizedTokenInvalid: Ihr Authorisierungs-Token konnte nicht verarbeitet werden. UnauthorizedTokenInvalidRoute: Ihr Authorisierungs-Token ist auf dieser Unterseite nicht gültig. UnauthorizedTokenInvalidAuthority: Ihr Authorisierungs-Token basiert auf den Rechten eines Nutzers, der nicht mehr existiert. +UnauthorizedTokenInvalidAuthorityGroup: Ihr Authorisierungs-Token basiert auf den Rechten einer Gruppe von Nutzern, die nicht mehr existiert. +UnauthorizedTokenInvalidAuthorityValue: Ihr Authorisierungs-Token basiert auf Rechten, deren Spezifikation nicht interpretiert werden konnte. UnauthorizedToken404: Authorisierungs-Tokens können nicht auf Fehlerseiten ausgewertet werden. UnauthorizedSiteAdmin: Sie sind kein System-weiter Administrator. UnauthorizedSchoolAdmin: Sie sind nicht als Administrator für dieses Institut eingetragen. @@ -478,7 +496,7 @@ HomeOpenAllocations: Offene Zentralanmeldungen HomeUpcomingSheets: Anstehende Übungsblätter HomeUpcomingExams: Bevorstehende Prüfungen -NumCourses num@Int64: #{num} Kurse +NumCourses num@Int64: #{num} #{pluralDE num "Kurs" "Kurse"} CloseAlert: Schliessen Name: Name @@ -519,15 +537,15 @@ NatField name@Text: #{name} muss eine natürliche Zahl sein! JSONFieldDecodeFailure aesonFailure@String: Konnte JSON nicht parsen: #{aesonFailure} SecretJSONFieldDecryptFailure: Konnte versteckte vertrauliche Daten nicht entschlüsseln -SubmissionsAlreadyAssigned num@Int64: #{num} Abgaben waren bereits einem Korrektor zugeteilt und wurden nicht verändert: -SubmissionsAssignUnauthorized num@Int64: #{num} Abgaben können momentan nicht einem Korrektor zugeteilt werden (z.B. weil die Abgabe noch offen ist): -UpdatedAssignedCorrectorSingle num@Int64: #{num} Abgaben wurden dem neuen Korrektor zugeteilt. +SubmissionsAlreadyAssigned num@Int64: #{num} #{pluralDE num "Abgabe" "Abgaben"} waren bereits einem Korrektor zugeteilt und wurden nicht verändert: +SubmissionsAssignUnauthorized num@Int64: #{num} #{pluralDE num "Abgabe" "Abgaben"} können momentan nicht einem Korrektor zugeteilt werden (z.B. weil die Abgabe noch offen ist): +UpdatedAssignedCorrectorSingle num@Int64: #{num} #{pluralDE num "Abgabe" "Abgaben"} wurden dem neuen Korrektor zugeteilt. NoCorrector: Kein Korrektor -RemovedCorrections num@Int64: Korrektur-Daten wurden von #{num} Abgaben entfernt. -UpdatedAssignedCorrectorsAuto num@Int64: #{num} Abgaben wurden unter den Korrektoren aufgeteilt. +RemovedCorrections num@Int64: Korrektur-Daten wurden von #{num} #{pluralDE num "Abgabe" "Abgaben"} entfernt. +UpdatedAssignedCorrectorsAuto num@Int64: #{num} #{pluralDE num "Abgabe" "Abgaben"} wurden unter den Korrektoren aufgeteilt. UpdatedSheetCorrectorsAutoAssigned n@Int: #{n} #{pluralDE n "Abgabe wurde einem Korrektor" "Abgaben wurden Korrektoren"} zugteilt. UpdatedSheetCorrectorsAutoFailed n@Int: #{n} #{pluralDE n "Abgabe konnte" "Abgaben konnten"} nicht automatisch zugewiesen werden. -CouldNotAssignCorrectorsAuto num@Int64: #{num} Abgaben konnten nicht automatisch zugewiesen werden: +CouldNotAssignCorrectorsAuto num@Int64: #{num} #{pluralDE num "Abgabe konnte" "Abgaben konnten"} nicht automatisch zugewiesen werden: SelfCorrectors num@Int64: #{num} Abgaben wurden Abgebenden als eigenem Korrektor zugeteilt! SubmissionOriginal: Original @@ -604,20 +622,21 @@ RatingNotUnicode uexc@UnicodeException: Bewertungsdatei nicht in UTF-8 kodiert: RatingMissingSeparator: Präambel der Bewertungsdatei konnte nicht identifziert werden RatingMultiple: Bewertungen enthält mehrere Punktzahlen für die gleiche Abgabe RatingInvalid parseErr@Text: Bewertungspunktzahl konnte nicht als Zahl verstanden werden: #{parseErr} -RatingFileIsDirectory: Unerwarteter Fehler: Datei ist unerlaubterweise ein Verzeichnis +RatingFileIsDirectory: Bewertungsdatei ist unerlaubterweise ein Verzeichnis RatingNegative: Bewertungspunkte dürfen nicht negativ sein RatingExceedsMax: Bewertung übersteigt die erlaubte Maximalpunktzahl RatingNotExpected: Keine Bewertungen erlaubt RatingBinaryExpected: Bewertung muss 0 (=durchgefallen) oder 1 (=bestanden) sein RatingPointsRequired: Bewertung erfordert für dieses Blatt eine Punktzahl +RatingFile: Bewertungsdatei SubmissionSinkExceptionDuplicateFileTitle file@FilePath: Dateiname #{show file} kommt mehrfach im Zip-Archiv vor SubmissionSinkExceptionDuplicateRating: Mehr als eine Bewertung gefunden. SubmissionSinkExceptionRatingWithoutUpdate: Bewertung gefunden, es ist hier aber keine Bewertung der Abgabe möglich. SubmissionSinkExceptionForeignRating smid@CryptoFileNameSubmission: Fremde Bewertung für Abgabe #{toPathPiece smid} enthalten. Bewertungen müssen sich immer auf die gleiche Abgabe beziehen! -SubmissionSinkExceptionInvalidFileTitleExtension file@FilePath: Dateiname #{show file} hat keine der für dieses Übungsblatt zulässigen Dateiendungen. +SubmissionSinkExceptionInvalidFileTitleExtension file@FilePath: Dateiname „#{show file}“ hat keine der für dieses Übungsblatt zulässigen Dateiendungen. -MultiSinkException name@Text error@Text: In Abgabe #{name} ist ein Fehler aufgetreten: #{error} +MultiSinkException name@Text error@Text: In Abgabe „#{name}“ ist ein Fehler aufgetreten: #{error} NoTableContent: Kein Tabelleninhalt NoUpcomingSheetDeadlines: Keine anstehenden Übungsblätter @@ -627,7 +646,7 @@ AdminHeading: Administration AdminUserHeading: Benutzeradministration AdminUserRightsHeading: Benutzerrechte AdminUserAuthHeading: Benutzer-Authentifizierung -AdminUserHeadingFor: Benuterprofil für +AdminUserHeadingFor: Benutzerprofil für AdminFor: Administrator LecturerFor: Dozent LecturersFor: Dozenten @@ -635,7 +654,6 @@ AssistantFor: Assistent AssistantsFor: Assistenten TutorsFor n@Int: #{pluralDE n "Tutor" "Tutoren"} CorrectorsFor n@Int: #{pluralDE n "Korrektor" "Korrektoren"} -ForSchools n@Int: für #{pluralDE n "Institut" "Institute"} UserListTitle: Komprehensive Benutzerliste AccessRightsSaved: Berechtigungen erfolgreich verändert AccessRightsNotChanged: Berechtigungen wurden nicht verändert @@ -647,7 +665,7 @@ DateTimeFormat: Datums- und Uhrzeitformat DateFormat: Datumsformat TimeFormat: Uhrzeitformat DownloadFiles: Dateien automatisch herunterladen -DownloadFilesTip: Wenn gesetzt werden Dateien von Abgaben und Übungsblättern automatisch als Download behandelt, ansonsten ist das Verhalten browserabhängig (es können z.B. PDFs im Browser geöffnet werden). +DownloadFilesTip: Wenn gesetzt werden Dateien automatisch als Download behandelt, ansonsten ist das Verhalten browserabhängig (es können z.B. PDFs im Browser geöffnet werden). WarningDays: Fristen-Vorschau WarningDaysTip: Wie viele Tage im Voraus sollen Fristen von Klausuren etc. auf Ihrer Startseite angezeigt werden? NotificationSettings: Erwünschte Benachrichtigungen @@ -659,6 +677,10 @@ FormCosmetics: Oberfläche FormPersonalAppearance: Öffentliche Daten FormFieldRequiredTip: Gekennzeichnete Pflichtfelder sind immer auszufüllen +PersonalInfoExamAchievementsWip: Die Anzeige von Prüfungsergebnissen wird momentan an dieser Stelle leider noch nicht unterstützt. +PersonalInfoOwnTutorialsWip: Die Anzeige von Tutorien, zu denen Sie als Tutor eingetragen sind wird momentan an dieser Stelle leider noch nicht unterstützt. +PersonalInfoTutorialsWip: Die Anzeige von Tutorien, zu denen Sie angemeldet sind wird momentan an dieser Stelle leider noch nicht unterstützt. + ActiveAuthTags: Aktivierte Authorisierungsprädikate InvalidDateTimeFormat: Ungültiges Datums- und Zeitformat, JJJJ-MM-TTTHH:MM[:SS] Format erwartet @@ -725,8 +747,8 @@ UploadSpecificFileRequired: Zur Abgabe erforderlich NoSubmissions: Keine Abgabe CorrectorSubmissions: Abgabe extern mit Pseudonym -UserSubmissions: Direkte Abgabe -BothSubmissions: Abgabe direkt & extern mit Pseudonym +UserSubmissions: Direkte Abgabe in Uni2work +BothSubmissions: Abgabe direkt in Uni2work & extern mit Pseudonym SheetCorrectorSubmissionsTip: Abgabe erfolgt über ein Uni2work-externes Verfahren (zumeist in Papierform durch Einwurf) unter Angabe eines persönlichen Pseudonyms. Korrektoren können mithilfe des Pseudonyms später Korrekturergebnisse in Uni2work eintragen, damit Sie sie einsehen können. @@ -738,8 +760,10 @@ SubmissionUpdated: Abgabe erfolgreich ersetzt AdminFeaturesHeading: Studiengänge StudyTerms: Studiengänge StudyTerm: Studiengang -NoStudyTermsKnown: Nicht eingeschrieben +NoStudyTermsKnown: Keine Studiengänge bekannt StudyFeatureInference: Studiengangschlüssel-Inferenz +StudyFeatureInferenceNoConflicts: Keine Konflikte beobachtet +StudyFeatureInferenceConflictsHeading: Studiengangseinträge mit beobachteten Konflikten StudyFeatureAge: Fachsemester StudyFeatureDegree: Abschluss FieldPrimary: Hauptfach @@ -757,9 +781,9 @@ DegreeShort: Abschlusskürzel StudyTermsKey: Studiengangschlüssel StudyTermsName: Studiengang StudyTermsShort: Studiengangkürzel -StudyTermsChangeSuccess: Zuordnung Abschlüsse aktualisiert -StudyDegreeChangeSuccess: Zuordnung Studiengänge aktualisiert -StudyCandidateIncidence: Anmeldevorgang +StudyTermsChangeSuccess: Zuordnung Studiengänge aktualisiert +StudyDegreeChangeSuccess: Zuordnung Abschlüsse aktualisiert +StudyCandidateIncidence: Synchronisation AmbiguousCandidatesRemoved n@Int: #{show n} #{pluralDE n "uneindeutiger Kandidat" "uneindeutige Kandiaten"} entfernt RedundantCandidatesRemoved n@Int: #{show n} bereits #{pluralDE n "bekannter Kandidat" "bekannte Kandiaten"} entfernt CandidatesInferred n@Int: #{show n} neue #{pluralDE n "Studiengangszuordnung" "Studiengangszuordnungen"} inferiert @@ -778,6 +802,8 @@ MailTestDateTime: Test der Datumsformattierung: German: Deutsch GermanGermany: Deutsch (Deutschland) +English: Englisch +EnglishEurope: Englisch (Europa) MailSubjectSubmissionRated csh@CourseShorthand: Ihre #{csh}-Abgabe wurde korrigiert MailSubmissionRatedIntro courseName@Text termDesc@Text: Ihre Abgabe im Kurs #{courseName} (#{termDesc}) wurde korrigiert. @@ -859,9 +885,9 @@ MailSubjectExamOfficeUserInvitation displayName@Text: Berücksichtigung von Prü MailSubjectPasswordReset: Uni2work-Passwort ändern bzw. setzen SheetGrading: Bewertung -SheetGradingPoints maxPoints@Points: #{maxPoints} Punkte -SheetGradingPassPoints maxPoints@Points passingPoints@Points: Bestanden ab #{passingPoints} von #{maxPoints} Punkten -SheetGradingPassBinary: Bestanden/Nicht Bestanden +SheetGradingPoints maxPoints@Points: #{maxPoints} #{pluralDE maxPoints "Punkt" "Punkte"} +SheetGradingPassPoints maxPoints@Points passingPoints@Points: Bestanden ab #{passingPoints} von #{maxPoints} #{pluralDE maxPoints "Punkt" "Punkten"} +SheetGradingPassBinary: Bestanden/Nicht Bestanden SheetGradingInfo: "Bestanden nach Punkten" zählt sowohl zur maximal erreichbaren Gesamtpunktzahl also auch zur Anzahl der zu bestehenden Blätter. SheetGradingCount': Anzahl @@ -915,10 +941,10 @@ NotificationTriggerExamRegistrationSoonInactive: Ich kann mich bald nicht mehr f NotificationTriggerExamDeregistrationSoonInactive: Ich kann mich bald nicht mehr von einer Prüfung abmelden NotificationTriggerExamResult: Ich kann ein neues Prüfungsergebnis einsehen NotificationTriggerAllocationStaffRegister: Ich kann Kurse bei einer neuen Zentralanmeldung eintragen -NotificationTriggerAllocationAllocation: Ich kann Zentralanmeldung-Bewerbungen für einen meiner Kurse bewerten +NotificationTriggerAllocationAllocation: Ich kann Zentralanmeldungs-Bewerbungen für einen meiner Kurse bewerten NotificationTriggerAllocationRegister: Ich kann mich bei einer neuen Zentralanmeldung bewerben -NotificationTriggerAllocationOutdatedRatings: Zentralanmeldung-Bewerbungen für einen meiner Kurse wurden verändert, nachdem sie bewertet wurden -NotificationTriggerAllocationUnratedApplications: Bewertungen zu Zentralanmeldung-Bewerbungen für einen meiner Kurse stehen aus +NotificationTriggerAllocationOutdatedRatings: Zentralanmeldungs-Bewerbungen für einen meiner Kurse wurden verändert, nachdem sie bewertet wurden +NotificationTriggerAllocationUnratedApplications: Bewertungen zu Zentralanmeldungs-Bewerbungen für einen meiner Kurse stehen aus NotificationTriggerAllocationResults: Plätze wurden für eine meiner Zentralanmeldungen verteilt NotificationTriggerExamOfficeExamResults: Ich kann neue Prüfungsergebnisse einsehen NotificationTriggerExamOfficeExamResultsChanged: Prüfungsergebnisse wurden verändert @@ -936,7 +962,7 @@ NotificationTriggerKindEvaluation: Für Vorlesungsumfragen NotificationTriggerKindAllocationStaff: Für Zentralanmeldungen (Dozenten) NotificationTriggerKindAllocationParticipant: Für Zentralanmeldungen -CorrCreate: Abgaben erstellen +CorrCreate: Abgaben registrieren UnknownPseudonymWord pseudonymWord@Text: Unbekanntes Pseudonym-Wort "#{pseudonymWord}" InvalidPseudonym pseudonym@Text: Invalides Pseudonym "#{pseudonym}" InvalidPseudonymSubmissionIgnored oPseudonyms@Text iPseudonym@Text: Abgabe mit Pseudonymen „#{oPseudonyms}“ wurde ignoriert, da „#{iPseudonym}“ nicht automatisiert zu einem validen Pseudonym korrigiert werden konnte. @@ -956,6 +982,10 @@ SheetCreateExisting: Folgende Pseudonyme haben bereits abgegeben: CorrGrade: Korrekturen eintragen UserAccountDeleted name@Text: Konto für #{name} wurde gelöscht! +UserSubmissionsDeleted n@Int: #{tshow n} Abgaben wurden unwiderruflich gelöscht. +UserGroupSubmissionsKept n@Int: #{tshow n} Gruppenabgaben verbleiben in der Datenbank, aber die Zuordnung zum Benutzer wurde gelöscht. Gruppenabgaben können dadurch zu Einzelabgaben werden, die dann mit dem letzten Benutzer gelöscht werden. +UserSubmissionGroupsDeleted count@Int64: #{tshow count} benannte Abgabengruppen wurden gelöscht, da sie ohne den Nutzer leer wären. +UserAccountDeleteWarning: Achtung, dies löscht den kompletten Benutzer unwiderruflich und mit allen assoziierten Daten aus der Datenbank. Prüfungsdaten müssen jedoch langfristig gespeichert bleiben! HelpTitle : Hilfe HelpAnswer: Antworten an @@ -1028,7 +1058,6 @@ EncodedSecretBoxCouldNotDecodeNonce: Konnte secretbox-nonce nicht dekodieren EncodedSecretBoxCouldNotOpenSecretBox: Konnte libsodium-secretbox nicht öffnen (Verschlüsselte Daten sind nicht authentisch) EncodedSecretBoxCouldNotDecodePlaintext aesonErr@String: Konnte Klartext nicht JSON-dekodieren: #{aesonErr} ErrMsgHeading: Fehlermeldung entschlüsseln -ErrorCryptoIdMismatch: Verschlüsselte Id der Abgabe passte nicht zu anderen Daten InvalidRoute: Konnte URL nicht interpretieren @@ -1065,6 +1094,7 @@ MenuProfileData: Persönliche Daten MenuTermCreate: Neues Semester anlegen MenuCourseNew: Neuen Kurs anlegen MenuTermEdit: Semester editieren +MenuTermCurrent: Aktuelles Semester MenuCorrection: Korrektur MenuCorrections: Korrekturen MenuCorrectionsOwn: Meine Korrekturen @@ -1093,8 +1123,8 @@ MenuCorrectionsUpload: Korrekturen hochladen MenuCorrectionsDownload: Offene Abgaben herunterladen MenuCorrectionsCreate: Abgaben registrieren MenuCorrectionsGrade: Abgaben online korrigieren -MenuCorrectionsAssign: Zuteilung Korrekturen -MenuCorrectionsAssignSheet name@Text: Zuteilung Korrekturen von #{name} +MenuCorrectionsAssign: Zuteilung der Korrekturen +MenuCorrectionsAssignSheet name@Text: Zuteilung der Korrekturen von #{name} MenuAuthPreds: Authorisierungseinstellungen MenuTutorialDelete: Tutorium löschen MenuTutorialEdit: Tutorium editieren @@ -1108,7 +1138,7 @@ MenuExamAddMembers: Prüfungsteilnehmer hinzufügen MenuExamOfficeExams: Prüfungen MenuExamOfficeFields: Fächer MenuExamOfficeUsers: Benutzer -MenuLecturerInvite: Dozenten hinzufügen +MenuLecturerInvite: Funktionäre hinzufügen MenuAllocationInfo: Hinweise zum Ablauf einer Zentralanmeldung MenuCourseApplicationsFiles: Dateien aller Bewerbungen MenuSchoolList: Institute @@ -1118,6 +1148,61 @@ MenuCourseNewsEdit: Kursnachricht bearbeiten MenuCourseEventNew: Neuer Kurstermin MenuCourseEventEdit: Kurstermin bearbeiten +BreadcrumbSubmissionFile: Datei +BreadcrumbSubmissionUserInvite: Einladung zur Abgabe +BreadcrumbCryptoIDDispatch: CryptoID-Weiterleitung +BreadcrumbCourseAppsFiles: Bewerbungsdateien +BreadcrumbCourseNotes: Kursnotizen +BreadcrumbHiWis: Korrektoren +BreadcrumbMaterial: Material +BreadcrumbSheet: Übungsblatt +BreadcrumbTutorial: Tutorium +BreadcrumbExam: Prüfung +BreadcrumbApplicant: Bewerber +BreadcrumbCourseRegister: Anmelden +BreadcrumbCourseRegisterTemplate: Bewerbungsvorlagen +BreadcrumbCourseFavourite: Favorisieren +BreadcrumbCourse: Kurs +BreadcrumbAllocationRegister: Teilnahme registrieren +BreadcrumbAllocation: Zentralanmeldung +BreadcrumbTerm: Semester +BreadcrumbSchool: Institut +BreadcrumbUser: Benutzer +BreadcrumbStatic: Statische Resource +BreadcrumbFavicon: Favicon +BreadcrumbRobots: robots.txt +BreadcrumbMetrics: Metriken +BreadcrumbLecturerInvite: Einladung zum Kursverwalter +BreadcrumbExamOfficeUserInvite: Einladung bzgl. Prüfungsleistungen +BreadcrumbFunctionaryInvite: Einladung zum Instituts-Funktionär +BreadcrumbUserDelete: Nutzer-Account löschen +BreadcrumbUserHijack: Nutzer-Sitzung übernehmen +BreadcrumbSystemMessage: Statusmeldung +BreadcrumbSubmission: Abgabe +BreadcrumbCourseNews: Kursnachricht +BreadcrumbCourseNewsDelete: Kursnachricht löschen +BreadcrumbCourseEventDelete: Kurstermin löschen +BreadcrumbProfile: Einstellungen +BreadcrumbAllocationInfo: Ablauf einer Zentralanmeldung +BreadcrumbCourseParticipantInvitation: Einladung zum Kursteilnehmer +BreadcrumbMaterialArchive: Archiv +BreadcrumbMaterialFile: Datei +BreadcrumbSheetArchive: Dateien +BreadcrumbSheetIsCorrector: Korrektor-Überprüfung +BreadcrumbSheetPseudonym: Pseudonym +BreadcrumbSheetCorrectorInvite: Einladung zum Korrektor +BreadcrumbSheetFile: Datei +BreadcrumbTutorialRegister: Anmelden +BreadcrumbTutorInvite: Einladung zum Tutor +BreadcrumbExamCorrectorInvite: Einladung zum Prüfungskorrektor +BreadcrumbExamParticipantInvite: Einladung zum Prüfungsteilnehmer +BreadcrumbExamRegister: Anmelden +BreadcrumbApplicationFiles: Bewerbungsdateien +BreadcrumbCourseNewsArchive: Archiv +BreadcrumbCourseNewsFile: Datei + +TitleMetrics: Metriken + AuthPredsInfo: Um eigene Veranstaltungen aus Sicht der Teilnehmer anzusehen, können Veranstalter und Korrektoren hier die Prüfung ihrer erweiterten Berechtigungen temporär deaktivieren. Abgewählte Prädikate schlagen immer fehl. Abgewählte Prädikate werden also nicht geprüft um Zugriffe zu gewähren, welche andernfalls nicht erlaubt wären. Diese Einstellungen gelten nur temporär bis Ihre Sitzung abgelaufen ist, d.h. bis ihr Browser-Cookie abgelaufen ist. Durch Abwahl von Prädikaten kann man sich höchstens temporär aussperren. AuthPredsActive: Aktive Authorisierungsprädikate AuthPredsActiveChanged: Authorisierungseinstellungen für aktuelle Sitzung gespeichert @@ -1196,7 +1281,7 @@ RGTutorialParticipants: Tutorium-Teilnehmer MultiSelectFieldTip: Mehrfach-Auswahl ist möglich (Umschalt bzw. Strg) MultiEmailFieldTip: Es sind mehrere, Komma-separierte, E-Mail-Addressen möglich -EmailInvitationWarning: Dem System ist kein Nutzer mit dieser Addresse bekannt. Es wird eine Einladung per E-Mail versandt. +EmailInvitationWarning: Diese Adresse konnte mit Ihren aktuellen Rechten keinem Uni2work-Benutzer zugeordnet werden (ggf. unter gewissen Einschränkungen). Es wird eine Einladung per E-Mail versandt. LecturerInvitationAccepted lType@Text csh@CourseShorthand: Sie wurden als #{lType} für #{csh} eingetragen LecturerInvitationDeclined csh@CourseShorthand: Sie haben die Einladung, Kursverwalter für #{csh} zu werden, abgelehnt @@ -1289,7 +1374,7 @@ TutorialTutorControlled: Tutoren dürfen Tutorium editieren TutorialTutorControlledTip: Sollen Tutoren beliebige Aspekte dieses Tutoriums (Name, Registrierungs-Gruppe, Raum, Zeit, andere Tutoren, ...) beliebig editieren dürfen? CourseExams: Prüfungen -CourseTutorials: Übungen +CourseTutorials: Tutorien ParticipantsN n@Int: #{n} Teilnehmer TutorialDeleteQuestion: Wollen Sie das unten aufgeführte Tutorium wirklich löschen? @@ -1412,7 +1497,7 @@ ExamBonusRoundNonPositive: Vielfaches, auf das gerundet werden soll, muss positi ExamBonusRoundTip: Bonuspunkte werden kaufmännisch auf ein Vielfaches der angegeben Zahl gerundet. ExamAutomaticOccurrenceAssignment: Automatische Termin- bzw. Raumzuteilung -ExamAutomaticOccurrenceAssignmentTip: Sollen Prüfungsteilnehmer zum Zeitpunkt der Bekanntgabe der Raum- bzw. Terminzuteilung automatisch auf die zur Verfügung stehenden Räume bzw. Termine verteilt werden? Manuelle Umverteilung bzw. vorheriges Festlegen von Zuteilungen einzelner Teilnehmer ist trotzdem möglich. +ExamAutomaticOccurrenceAssignmentTip: Sollen Prüfungsteilnehmer automatisch auf die zur Verfügung stehenden Räume bzw. Termine verteilt werden? Manuelle Umverteilung bzw. vorheriges Festlegen von Zuteilungen einzelner Teilnehmer ist trotzdem möglich. ExamOccurrenceRule: Verfahren ExamOccurrenceRuleParticipant: Termin- bzw. Raumzuteilungsverfahren ExamRoomManual': Keine automatische Zuteilung @@ -1457,7 +1542,7 @@ ExamPartName: Titel ExamPartNameTip: Wird den Studierenden angezeigt ExamPartMaxPoints: Maximalpunktzahl ExamPartWeight: Gewichtung -ExamPartWeightTip: Wird vor Anzeige oder Notenberechnung mit der erreichten Punktzahl und der Maximalpunktzahl multipliziert; Änderungen hier passen auch bestehende Korrekturergebnisse an +ExamPartWeightTip: Wird vor Anzeige oder automatischen Notenberechnung mit der erreichten Punktzahl und der Maximalpunktzahl multipliziert; Änderungen hier passen also auch bestehende Korrekturergebnisse an (derart geänderte Noten müssen erneut manuell übernommen werden) ExamPartResultPoints: Erreichte Punkte ExamNameTaken exam@ExamName: Es existiert bereits eine Prüfung mit Namen #{exam} @@ -1527,6 +1612,8 @@ ExamUserMarkedSynchronised n@Int: #{n} #{pluralDE n "Prüfungsleistung" "Prüfun ExamOfficeExamUsersHeading: Prüfungsleistungen CsvFile: CSV-Datei +CsvImport: CSV-Import +CsvExport: CSV-Export CsvModifyExisting: Existierende Einträge angleichen CsvAddNew: Neue Einträge einfügen CsvDeleteMissing: Fehlende Einträge entfernen @@ -1681,6 +1768,7 @@ SchoolFunctionInvitationAccepted school@SchoolName renderedFunction@Text: #{rend AllocationActive: Aktiv AllocationName: Name AllocationAvailableCourses: Kurse +AllocationApplication: Bewerbung AllocationAppliedCourses: Bewerbungen AllocationNumCoursesAvailableApplied available@Int applied@Int: Sie haben sich bisher für #{applied}/#{available} #{pluralDE applied "Kurs" "Kursen"} beworben AllocationTitle termText@Text ssh'@SchoolShorthand allocation@AllocationName: #{termText} - #{ssh'}: #{allocation} @@ -1879,7 +1967,7 @@ AcceptApplicationsSecondaryRandom: Zufällig AcceptApplicationsSecondaryTime: Nach Zeitpunkt der Bewerbung CsvOptions: CSV-Optionen -CsvOptionsTip: Diese Einstellungen betreffen nur den CSV-Export; beim Import werden die verwendeten Einstellungen automatisch ermittelt. Als Zeichenkodierung wird beim Import stets Unicode erwartet. +CsvOptionsTip: Diese Einstellungen betreffen primär den CSV-Export; beim Import werden die meisten Einstellungen automatisch ermittelt. Als Zeichenkodierung wird beim Import die selbe Kodierung wie beim Export erwartet. CsvFormatOptions: Dateiformat CsvTimestamp: Zeitstempel CsvTimestampTip: Soll an den Namen jeder exportierten CSV-Datei ein Zeitstempel vorne angehängt werden? @@ -1898,6 +1986,7 @@ CsvDelimiterNull: Null-Byte CsvDelimiterTab: Tabulator CsvDelimiterComma: Komma CsvDelimiterColon: Doppelpunkt +CsvDelimiterSemicolon: Strichpunkt CsvDelimiterBar: Senkrechter Strich CsvDelimiterSpace: Leerzeichen CsvDelimiterUnitSep: Teilgruppentrennzeichen @@ -1989,5 +2078,67 @@ ShowSex: Geschlechter anderer Nutzer anzeigen ShowSexTip: Sollen in Kursteilnehmer-Tabellen u.Ä. die Geschlechter der Nutzer angezeigt werden? StudySubTermsParentKey: Elter -StudyTermsDefaultDegree: Abschluss -StudyTermsDefaultFieldType: Typ \ No newline at end of file +StudyTermsDefaultDegree: Default Abschluss +StudyTermsDefaultFieldType: Default Typ + +MenuLanguage: Sprache +LanguageChanged: Sprache erfolgreich geändert + +ProfileCorrector: Korrektor +ProfileCourses: Eigene Kurse +ProfileCourseParticipations: Kursanmeldungen +ProfileCourseExamResults: Prüfungsleistungen +ProfileTutorials: Eigene Tutorien +ProfileTutorialParticipations: Tutorien +ProfileSubmissionGroups: Abgabegruppen +ProfileSubmissions: Abgaben +ProfileRemark: Hinweis +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. +ProfileCorrections: Auflistung aller zugewiesenen Korrekturen + +GroupSizeNotNatural: „Gruppengröße“ muss eine natürliche Zahl sein +AmbiguousEmail: E-Mail Adresse nicht eindeutig +CourseDescriptionPlaceholder: Bitte mindestens die Modulbeschreibung angeben +CourseHomepageExternalPlaceholder: Optionale externe URL +PointsPlaceholder: Punktezahl +RFC1766: RFC1766-Sprachcode + +TermShort: Kürzel +TermCourseCount: Kurse +TermStart: Semesteranfang +TermEnd: Semesterende +TermStartMustMatchName: Jahreszahl im Namenskürzel stimmt nicht mit Semesterbeginn überein. +TermEndMustBeAfterStart: Semester darf nicht enden, bevor es beginnt. +TermLectureEndMustBeAfterStart: Vorlesungszeit muss vor ihrem Ende anfgangen. +TermStartMustBeBeforeLectureStart: Semester muss vor der Vorlesungszeit beginnen. +TermEndMustBeAfterLectureEnd: Vorlesungszeit muss vor dem Semester enden. +AdminPageEmpty: Diese Seite soll eine Übersichtsseite für Administratoren werden. Aktuell finden sich hier nur Links zu wichtigen Administrator-Funktionalitäten. +HaveCorrectorAccess sheetName@SheetName: Sie haben Korrektor-Zugang zu #{original sheetName}. +FavouritesPlaceholder: Anzahl Favoriten +FavouritesNotNatural: Anzahl der Favoriten muss eine natürliche Zahl sein! +FavouritesSemestersPlaceholder: Anzahl Semester +FavouritesSemestersNotNatural: Anzahl der Favoriten-Semester muss eine natürliche Zahl sein! + +ProfileTitle: Benutzereinstellungen + +GlossaryTitle: Begriffsverzeichnis +MenuGlossary: Begriffsverzeichnis + +Applicant: Bewerber +CourseParticipant: Kursteilnehmer +Administrator: Administrator +CsvFormat: CSV-Format +ExerciseSheet: Übungsblatt +DefinitionCourseEvents: Kurstermine +DefinitionCourseNews: Kurs-Aktuelles +Invitations: Einladungen +SheetSubmission: Abgabe +CommCourse: Kursmitteilung +CommTutorial: Tutorium-Mitteilung +Clone: Klonen +Deficit: Defizit + +MetricNoSamples: Keine Messwerte +MetricName: Name +MetricValue: Wert diff --git a/messages/uniworx/en-eu.msg b/messages/uniworx/en-eu.msg new file mode 100644 index 000000000..5307eb03e --- /dev/null +++ b/messages/uniworx/en-eu.msg @@ -0,0 +1,2139 @@ +PrintDebugForStupid name@Text: Debug message "#{name}" + +Logo: Uni2work + +BtnSubmit: Submit +BtnAbort: Abort +BtnDelete: Delete +BtnRegister: Register +BtnDeregister: Deregister +BtnCourseRegister: Enrol for course +BtnCourseDeregister: Leave course +BtnCourseApply: Apply for course +BtnCourseRetractApplication: Retract application +BtnExamRegister: Enrol for exam +BtnExamDeregister: Leave exam +BtnHijack: Hijack session +BtnSave: Save +PressSaveToSave: Changes will only be saved after clicking "Save". +BtnHandIn: Hand in submission +BtnCandidatesInfer: Infer mapping +BtnCandidatesDeleteConflicts: Delete conflicts +BtnCandidatesDeleteAll: Delete all observations +BtnResetTokens: Invalidate tokens +BtnLecInvAccept: Accept +BtnLecInvDecline: Decline +BtnCorrInvAccept: Accept +BtnCorrInvDecline: Decline +BtnSubmissionsAssign: Assign submissions automatically + + +Aborted: Aborted +Remarks: Remarks +Registered: Enrolled +RegisteredSince: Enrolled since +NotRegistered: Note enrolled for this course +Registration: Enrolment +RegisterFrom: Enrolment starts +RegisterTo: Enrolment ends +DeRegUntil: Deregistration until +RegisterRetry: You haven't been enrolled. Press "Enrol for course" to enrol + +CourseRegistrationInterval: Enrolment +CourseDirectRegistrationInterval: Direct enrolment +CourseDeregisterUntil time: Deregistration only until #{time} + +GenericKey: Key +GenericShort: Shorthand +GenericIsNew: New +GenericHasConflict: Conflict +GenericBack: Back +GenericChange: Change +GenericNumChange: +/- +GenericMin: Min +GenericAvg: Avg +GenericMax: Max +GenericAll: All + +SummerTerm year: Summer semester #{year} +WinterTerm year: Winter semester #{year}/#{succ year} +SummerTermShort year: Summer #{year} +WinterTermShort year: Winter #{year}/#{mod (succ year) 100} +PSLimitNonPositive: “pagesize” must be greater than zero +Page num: #{num} + +TermsHeading: Semesters +TermCurrent: Current semester +TermEditHeading: Edit semester +TermEditTid tid: Edit semester #{tid} +TermEdited tid: Successfully edited semester #{tid} +TermNewTitle: Edit/create semester +InvalidInput: Invalid input +Term: Semester +TermPlaceholder: (W|S) + +TermStartDay: Starting day +TermStartDayTooltip: Usually 1st of April or 1st of October +TermEndDay: Last day +TermEndDayTooltip: Usually 30th of September or 31st of March +TermHolidays: Legal holidays +TermHolidayPlaceholder: Legal holiday +TermLectureStart: Lectures start +TermLectureEnd: Lectures end +TermLectureEndTooltip: Summer semesters are usually 14 weeks; winter semesters 15 +TermActive: Active + + +SchoolListHeading: Department +SchoolHeading school: #{school} + +LectureStart: Lectures start + +Course: Course +CourseShort: Shorthand +CourseCapacity: Capacity +CourseCapacityTip: Maximum permissable number of enrolments for this course; leave empty for unlimited capacity +CourseNoCapacity: Course has reached maximum capacity +TutorialNoCapacity: Tutorial has reached maximum capacity +CourseNotEmpty: There are currently no participants enrolled for this course. +CourseRegistration: Enrolment +CourseRegisterOpen: Enrolment is allowed +CourseRegisterOk: Successfully enrolled for course +CourseDeregisterOk: Successfully left course +CourseApply: Apply for course +CourseApplyOk: Successfully applied for course +CourseRetractApplyOk: Successfully retracted application for course +CourseDeregisterLecturerTip: If you deregister the participant you might loose access to this data +CourseStudyFeature: Associated subject +CourseStudyFeatureTip: For information purposes only (visible to course administrators) +CourseStudyFeatureUpdated: Successfully updated associated subject +CourseStudyFeatureNone: No associated subject +CourseTutorial: Tutorial +CourseSecretWrong: Wrong password +CourseSecret: Access password +CourseEditOk tid ssh csh: Successfully edited course #{tid}-#{ssh}-#{csh} +CourseNewDupShort tid ssh csh: Could not create course #{tid}-#{ssh}-#{csh}. Another course with shorthand #{csh} already exists for the given semester and school. +CourseEditDupShort tid ssh csh: Could not edit course #{tid}-#{ssh}-#{csh}. Another course with shorthand #{csh} already exists for the given semester and school. +FFSheetName: Name +TermCourseListHeading tid: Courses #{tid} +TermSchoolCourseListHeading tid school: Courses #{tid}, #{school} +CourseListTitle: All courses +TermCourseListTitle tid: Courses #{tid} +TermSchoolCourseListTitle tid school: Courses #{tid}, #{school} +CourseNewHeading: Create new course +CourseEditHeading tid ssh csh: Edit course #{tid}-#{ssh}-#{csh} +CourseEditTitle: Edit/Create course +CourseMembers: Participants +CourseMemberOf: Participant of +CourseAssociatedWith: associated with +CourseMembersCount n: #{n} +CourseMembersCountLimited n max: #{n}/#{max} +CourseMembersCountOf n mbNum: #{n} #{maybeToMessage "of " mbNum " "}participants +CourseName: Title +CourseDescription: Description +CourseDescriptionTip: You may use arbitrary Html-Markup +CourseHomepageExternal: External homepage +CourseShorthand: Shorthand +CourseShorthandUnique: Needs to be unique within school and semester. Will be used verbatim within the url of the course page. +CourseSemester: Semester +CourseSchool: Department +CourseSchoolShort: Department +CourseSecretTip: Enrollment for this course will require the password, if set +CourseSecretFormat: Arbitrary string +CourseRegisterFromTip: When left empty students will not be able to enrol themselves +CourseRegisterToTip: May be left empty to allow enrolment indefinitely +CourseDeregisterUntilTip: Participants may deregister from immediately after registration starts up to this time. May be left empty to allow deregistration indefinitely. +CourseFilterSearch: Text search +CourseFilterRegistered: Registered +CourseFilterNone: — +BoolIrrelevant: — +CourseDeleteQuestion: Are you sure you want to delete the below-mentioned course? +CourseDeleted: Course deleted +CourseUserRegister: Enrol for course +CourseUserTutorials: Registered tutorials +CourseUserNote: Note +CourseUserNoteTooltip: Only visible to administrators of this course +CourseUserNoteSaved: Successfully saved note changes +CourseUserNoteDeleted: Successfully deleted user note deleted +CourseUserDeregister: Deregister from course +CourseUsersDeregistered count@Int64: Successfully deregistered #{show count} users from course +CourseUserRegisterTutorial: Register for a tutorial +CourseUsersTutorialRegistered count@Int64: Successfully registered #{show count} users for tutorial +CourseUserSendMail: Send mail +TutorialUserDeregister: Deregister from tutorial +TutorialUserSendMail: Send mail +TutorialUsersDeregistered count@Int64: Successfully deregistered #{show count} participants from tutorial +CourseAllocationParticipate: Participate in central allocation +CourseAllocationParticipateTip: If a course participates in a central allocation, you might lose some permissions that you would normally have (e.g. registering students for the course directly, deregistering students, ...) +CourseAllocation: Central allocation +CourseAllocationOption term@Text name@Text: #{name} (#{term}) +CourseAllocationMinCapacity: Minimum number of participants +CourseAllocationMinCapacityTip: If fewer students than this number were to be assigned to this course, then these students would instead be assigned to other courses +CourseAllocationMinCapacityMustBeNonNegative: Minimum number of participants must not be negative +CourseApplicationInstructions: Instructions for application +CourseApplicationInstructionsTip: Will be shown to students if they decide to apply for this course +CourseApplicationTemplate: Application template +CourseApplicationTemplateTip: Students can download this template if they decide to apply for this course +CourseApplicationsText: Text application +CourseApplicationsTextTip: Should students submit a plaintext application (in addition to submitted files if applicable)? +CourseApplicationRatingsVisible: Feedback to applications +CourseApplicationRatingsVisibleTip: Should students be allowed to view rating and comments on their application after the rating period? +CourseApplicationRequired: Applications required +CourseApplicationRequiredTip: Should registrations for this course be provisional at first (without capacity constraint), until they are approved by a course administrator? +CourseApplicationInstructionsApplication: Instructions for application +CourseApplicationInstructionsRegistration: Instructions for registration +CourseApplicationTemplateApplication: Application template(s) +CourseApplicationTemplateRegistration: Registration template(s) +CourseApplicationTemplateArchiveName tid ssh csh: #{foldCase (termToText (unTermKey tid))}-#{foldedCase (unSchoolKey ssh)}-#{foldedCase csh}-application-templates +CourseApplication: Application +CourseApplicationIsParticipant: Course participant + +CourseApplicationExists: You already applied for this course +CourseApplicationInvalidAction: Invalid action +CourseApplicationCreated csh: Successfully applied for #{csh} +CourseApplicationEdited csh: Successfully changed application for #{csh} +CourseApplicationNotEdited csh: Application for #{csh} not changed +CourseApplicationRated: Successfully edited rating +CourseApplicationRatingDeleted: Successfully deleted rating +CourseApplicationDeleted csh: Successfully withdrew application for #{csh} + +CourseApplicationTitle displayName csh: Application for #{csh}: #{displayName} + +CourseApplicationText: Application text +CourseApplicationFollowInstructions: Please follow the instructions for applications! +CourseRegistrationText: Registration text +CourseRegistrationFollowInstructions: Please follow the instructions for registrations! + +CourseApplicationFile: Application +CourseApplicationFiles: Application file(s) +CourseApplicationArchive: Zip archive of application files +CourseRegistrationFile: Registration file +CourseRegistrationFiles: Registration file(s) +CourseRegistrationArchive: Zip archive of registration files +CourseApplicationNoFiles: No file(s) +CourseApplicationFilesNeedReupload: Application files need to be reuploaded every time the application is changed +CourseRegistrationFilesNeedReupload: Registration files need to be reuploaded every time the registration is changed + +CourseApplicationDeleteToEdit: You need to withdraw your application and reapply to edit your application. +CourseRegistrationDeleteToEdit: You need to deregister and reregister to edit your registration. + +CourseLoginToApply: You need to login to Uni2work before you can apply for this course. +CourseLoginToRegister: Your need to login to Uni2work before you can register for this course. + +CourseApplicationArchiveName tid ssh csh appId displayName: #{foldCase (termToText (unTermKey tid))}-#{foldedCase (unSchoolKey ssh)}-#{foldedCase csh}-#{foldCase (toPathPiece appId)}-#{foldCase displayName} +CourseAllApplicationsArchiveName tid ssh csh: #{foldCase (termToText (unTermKey tid))}-#{foldedCase (unSchoolKey ssh)}-#{foldedCase csh}-applications +CourseApplicationsAllocatedDirectory: central +CourseApplicationsNotAllocatedDirectory: direct + +CourseNoAllocationsAvailable: There are no ongoing central allocations +AllocationStaffRegisterToExpired: You cannot change course properties concerning the central allocation after the course registration period. Your changes may have been discarded. + +CourseFormSectionRegistration: Registration +CourseFormSectionAdministration: Administration + +CourseLecturers: Course administrators +CourseLecturer: Lecturer +CourseAssistant: Assistant +CourseLecturerAlreadyAdded email: There already is a course administrator with email #{email} +CourseRegistrationEndMustBeAfterStart: The end of the registration period must be before its start +CourseDeregistrationEndMustBeAfterStart: The end of the deregistration period must be after the start of the registration period +CourseUserMustBeLecturer: The current user needs to be a course administrator +CourseAllocationRequiresCapacity: Course capacity needs to be specified if the course participates in a central allocation +CourseAllocationTermMustMatch: Course semester needs to match the semester of the central allocation +CourseAllocationCapacityMayNotBeChanged: The capacity of a course that participates in a central allocation must not be altered +CourseShorthandTooLong: Long course shorthands may lead to display issues and might complicate communication with students. Please choose a more concise shorthand if possible. + +CourseLecturerRightsIdentical: All sorts of course administrators have the same permissions. + +School: Department + +NoSuchTerm tid: Semester #{tid} does not exist. +NoSuchSchool ssh: Department #{ssh} does not exist. +NoSuchCourseShorthand csh: There is no course with shorthand #{csh}. +NoSuchCourse: No such course found. + +NoCourseDescription: This course does not provide a description. + +Sheet: Sheet +SheetList tid ssh csh : #{tid}-#{ssh}-#{csh} Sheet Overview +SheetNewHeading tid ssh csh : #{tid}-#{ssh}-#{csh} New Exercise Sheet +SheetNewOk tid ssh csh sheetName: Successfully created sheet #{sheetName} in course #{tid}-#{ssh}-#{csh}. +SheetTitle tid ssh csh sheetName: #{tid}-#{ssh}-#{csh} #{sheetName} +SheetTitleNew tid ssh csh : #{tid}-#{ssh}-#{csh}: New Exercise Sheet +SheetEditHead tid ssh csh sheetName: #{tid}-#{ssh}-#{csh} Edit #{sheetName} +SheetEditOk tid ssh csh sheetName: Successfully saved exercise sheet #{sheetName} in course #{tid}-#{ssh}-#{csh} +SheetNameDup tid ssh csh sheetName: There already is an exercise sheet #{sheetName} in course #{tid}-#{ssh}-#{csh} +SheetDelHead tid ssh csh sheetName: Do you really want to delete sheet #{sheetName} from course #{tid}-#{ssh}-#{csh}? Any associated submissions and corrections will be lost! +SheetDelOk tid ssh csh sheetName: #{tid}-#{ssh}-#{csh}: #{sheetName} has been deleted. +SheetDelHasSubmissions objs: Incl. #{objs} #{pluralEN objs "submission" "submissions"} + +SheetDeleteQuestion: Do you really want to delete the below-mentioned exercise sheet and all associated submissions? +SheetDeleted: Successfully deleted exercise sheet + +SheetUploadMode: Submission of files +SheetSubmissionMode: Submission mode +SheetExercise: Assignment +SheetHint: Hint +SheetHintFrom: Hint from +SheetHintFromPlaceholder: Date, correctors only otherwise +SheetSolutionFromPlaceholder: Date, correctors only otherwise +SheetSolution: Solution +SheetSolutionFrom: Solution from +SheetMarking: Marking hints for correctors +SheetMarkingFiles: Correction +SheetType: Marking +SheetInvisible: This exercise sheet is currently invisible for participants! +SheetName: Name +SheetDescription: Description +SheetGroup: Group submission +SheetVisibleFrom: Visible from (for participants) +SheetVisibleFromTip: Always invisible for participants and no submission possible if left empty; only leave this field empty for temporary/unfinished sheets +SheetActiveFrom: Active from/Submission period start +SheetActiveFromParticipant: Submission period start +SheetActiveFromParticipantNoSubmit: Assignment published +SheetActiveFromTip: The exercise sheet's assignment will only be available for download and submission starting at this time. If left empty no submission or download of assignment is ever allowed +SheetActiveFromUnset: Never +SheetActiveTo: Active to/Submission period end +SheetActiveToParticipant: Submission period end +SheetActiveToTip: Submission will only be possible until this time. If left empty submissions are allowed forever (if at all possible) +SheetActiveToUnset: Never +SheetHintFromTip: Always invisible for participants if left empty; correctors can always download hints +SheetSolutionFromTip: Always invisible for participants if left empty; correctors can always download solutions +SheetMarkingTip: Instructions for correction, visible only to correctors +SheetPseudonym: Personal pseudonym +SheetGeneratePseudonym: Generate + +SheetFormType: Valuation & submission +SheetFormTimes: Times +SheetFormFiles: Files + +SheetErrVisibility: "Submission period start" must be after "Visible from" +SheetErrDeadlineEarly: "Submission period end" must be after "Submission period start" +SheetErrHintEarly: "Hint from" must be after "Submission period start" +SheetErrSolutionEarly: "Solution from" must be after "Submission period end" +SheetNoCurrent: There is no currently active exercise sheet +SheetNoOldUnassigned: All submissions for inactive sheets are already assigned to correctors. +SheetsUnassignable name: Submission for #{name} may not currently be assigned to correctors. + +Deadline: Deadline +Done: Submitted + +Submission: Submission-number +SubmissionsCourse tid ssh csh: All submissions for Course #{tid}-#{ssh}-#{csh} +SubmissionsSheet sheetName: Submissions for #{sheetName} +SubmissionWrongSheet: Submission does not belong to the given sheet. +SubmissionAlreadyExists: You already have a submission for this sheet. +SubmissionEditHead tid ssh csh sheetName: #{tid}-#{ssh}-#{csh} #{sheetName}: Edit/Create submission +CorrectionHead tid ssh csh sheetName cid: #{tid}-#{ssh}-#{csh} #{sheetName}: Marking +SubmissionMembers: Submittors +SubmissionMember: Submittor +CosubmittorTip: Invitations are sent via email to exactly those addresses for which it cannot be determined, that you have already submitted for this course with the associated person, at least once. If one of the specified addresses can be matched to a person with whom you have submitted at least once for this course already, the name of that person will be shown and the submission will immediately be made in their name as well. +SubmissionArchive: Zip-archive of submission files +SubmissionFile: Submission file +SubmissionFiles: Submitted files +SubmissionAlreadyExistsFor email: #{email} already has a submission for this sheet. +SubmissionUsersEmpty: Submissions may not be created without submittors. +SubmissionUserAlreadyAdded: This user is already configured as a submittor +NoOpenSubmissions: No open submissions exist + +SubmissionsDeleteQuestion n: Do you really want to delete the #{pluralEN n "submission" "submissions"} mentioned below? +SubmissionsDeleted n: #{pluralEN n "Submission" "Submissions"} deleted + +SubmissionGroupName: Group name + +CorrectionsTitle: Assigned corrections +CourseCorrectionsTitle: Corrections for this course +CorrectorsHead sheetName: Correctors for #{sheetName} +CorrectorAssignTitle: Assign corrector + +CorrectionsGrade: Grade submissions online + +MaterialName: Name +MaterialType: Type +MaterialTypePlaceholder: Slides, Code, Example, ... +MaterialTypeSlides: Slides +MaterialTypeCode: Code +MaterialTypeExample: Example +MaterialDescription: Description +MaterialVisibleFrom: Visible to participants from +MaterialVisibleFromTip: Never visible to participants if left empty; leaving the date empty is only sensible for unfinished course material or when course material should be provided only to sheet correctors +MaterialVisibleFromEditWarning: This course material has already been published and should not be edited. Doing so might confuse the participants. +MaterialInvisible: This course material is currently invisible to participants! +MaterialFiles: Files +MaterialHeading materialName: Course material “#{materialName}” +MaterialListHeading: Course materials +MaterialNewHeading: Publish new course material +MaterialNewTitle: New course material +MaterialEditHeading materialName: Edit course material “#{materialName}” +MaterialEditTitle materialName: Edit course material “#{materialName}” +MaterialSaveOk tid ssh csh materialName: Successfully saved “#{materialName}” for course #{tid}-#{ssh}-#{csh} +MaterialNameDup tid ssh csh materialName: Course material with the name “#{materialName}” already exists for course #{tid}-#{ssh}-#{csh} +MaterialDeleteCaption: Do you really want to delete the course material mentioned below? +MaterialDelHasFiles count: including #{count} #{pluralEN count "file" "files"} +MaterialIsVisible: Caution, this course material has already been published. +MaterialDeleted materialName: Successfully deleted course material “#{materialName}” +MaterialArchiveName tid ssh csh materialName: #{foldCase (termToText (unTermKey tid))}-#{foldedCase (unSchoolKey ssh)}-#{foldedCase csh}-#{foldedCase materialName} + +Unauthorized: You do not have explicit authorisation. +UnauthorizedAnd l r: (#{l} AND #{r}) +UnauthorizedOr l r: (#{l} OR #{r}) +UnauthorizedNot r: (NOT #{r}) +UnauthorizedNoToken: No authorisation-token was provided with your request. +UnauthorizedTokenExpired: Your authorisation-token is expired. +UnauthorizedTokenNotStarted: Your authorisation-token is not yet valid. +UnauthorizedTokenInvalid: Your authorisation-token could not be processed. +UnauthorizedTokenInvalidRoute: Your authorisation-token is not valid for this page. +UnauthorizedTokenInvalidAuthority: Your authorisation-token is based in an user's rights who does not exist anymore. +UnauthorizedTokenInvalidAuthorityGroup: Your authorisation-token is based in an user groups rights which does not exist anymore. +UnauthorizedTokenInvalidAuthorityValue: The specification of the rights in which your authorisation-token is based, could not be interpreted. +UnauthorizedToken404: Authorisation-tokens cannot be processed on error pages. +UnauthorizedSiteAdmin: You are no system-wide administrator. +UnauthorizedSchoolAdmin: You are no administrator for this department. +UnauthorizedAdminEscalation: You aren't an administrator for all departments for which this user is an administrator. +UnauthorizedExamOffice: You are not part of an exam office. +UnauthorizedExamExamOffice: You are not part of the appropriate exam office for any of the participants of this exam. +UnauthorizedSchoolLecturer: You are no lecturer for this department. +UnauthorizedLecturer: You are no administrator for this course. +UnauthorizedAllocationLecturer: You are no administrator for any of the courses of this central allocation. +UnauthorizedCorrector: You are no sheet corrector for this course. +UnauthorizedSheetCorrector: You are no corrector for this sheet. +UnauthorizedCorrectorAny: You are no corrector for any course. +UnauthorizedRegistered: You are no participant in this course. +UnauthorizedAllocationRegistered: You are no participant in this central allocation. +UnauthorizedExamResult: You have no results in this exam. +UnauthorizedParticipant: The specified user is no participant of this course. +UnauthorizedParticipantSelf: You are no participant of this course. +UnauthorizedApplicant: The specified user is no applicant for this course. +UnauthorizedApplicantSelf: You are no applicant for this course. +UnauthorizedCourseTime: This course does not currently allow enrollment. +UnauthorizedAllocationRegisterTime: This central allocation does not currently allow applications. +UnauthorizedSheetTime: This sheet is not currently available. +UnauthorizedApplicationTime: This allocation is not currently available. +UnauthorizedMaterialTime: This course material is not currently available. +UnauthorizedTutorialTime: This tutorial does not currently allow registration. +UnauthorizedCourseNewsTime: This news item is not currently available. +UnauthorizedExamTime: This exam is not currently available. +UnauthorizedSubmissionOwner: You are no submittor for this submission. +UnauthorizedSubmissionRated: This submission is not yet marked. +UnauthorizedSubmissionCorrector: You are no corrector for this submission. +UnauthorizedUserSubmission: Users may not directly submit for this exercise sheet. +UnauthorizedCorrectorSubmission: Correctors may not create submissions for this exercise sheet. +OnlyUploadOneFile: Please only upload one file +DeprecatedRoute: This view is deprecated and will be removed. +UnfreeMaterials: Course material are not publicly accessable. +MaterialFree: Course material is publicly available. +UnauthorizedWrite: You do not have the write permission necessary to perform this action +UnauthorizedSystemMessageTime: This system-message is not currently available. +UnauthorizedSystemMessageAuth: This system-message is only available to logged in users. +UnsupportedAuthPredicate authTagT shownRoute: “#{authTagT}” was applied to a route which does not support it: “#{shownRoute}” +UnauthorizedDisabledTag authTag: Auth predicate “#{toPathPiece authTag}” is disabled for your session +UnknownAuthPredicate tag: Auth predicate “#{tag}” is unknown +UnauthorizedRedirect: The requested view does not exist or you haven't the required permissions to access it. +UnauthorizedSelf: You are not the specified user. +UnauthorizedTutorialTutor: You are no tutor for this tutorial. +UnauthorizedTutorialTutorControl: Tutors may not edit this tutorial. +UnauthorizedCourseTutor: You are no tutor for this course. +UnauthorizedTutor: You are no tutor. +UnauthorizedTutorialRegisterGroup: You are already registered for a tutorial with the same registration group. +UnauthorizedLDAP: Specified user does not log in with their campus account. +UnauthorizedPWHash: Specified user does not log in with an Uni2work-account. + +UnauthorizedPasswordResetToken: This authorisation-token may no longer be used to change passwords + +UnauthorizedAllocatedCourseRegister: Direct enrollment to this course is currently not allowed due to participation in a central allocation +UnauthorizedAllocatedCourseDeregister: Deregistration from this course is currently not allowed due to participation in a central allocation +UnauthorizedAllocatedCourseDelete: Courses that participate in a central allocation may not be deleted + +EMail: Email +EMailUnknown email: Email #{email} does not belong to any known user. +NotAParticipant email tid csh: #{email} is not a participant of #{tid}-#{csh}. +TooManyParticipants: You have specified more than the allowed number of submittors. + +AddCorrector: Additional corrector +CorrectorExists: User already is a corrector +SheetCorrectorsTitle tid ssh csh sheetName: Correctors for #{tid}-#{ssh}-#{csh} #{sheetName} +CountTutProp: Tutorials count against proportion +CountTutPropTip: If submissions are assigned by tutorial, do those assignments count with regard to the set proportion? +AutoAssignCorrs: Automatically assign corrections after expiration of the submission period +Corrector: Corrector +Correctors: Correctors +CorState: State +CorByTut: Assign by tutorial +CorProportion: Proportion +CorDeficitProportion: Deficit (proportion) +CorByProportionOnly proportion: #{rationalToFixed3 proportion} parts +CorByProportionIncludingTutorial proportion: #{rationalToFixed3 proportion} parts - tutorials +CorByProportionExcludingTutorial proportion: #{rationalToFixed3 proportion} parts + tutorials + +RowCount count: #{count} matching #{pluralEN count "entry" "entries"} +DeleteRow: Delete +ProportionNegative: Proportions may not be negative +CorrectorUpdated: Successfully updated corrector +CorrectorsUpdated: Successfully updated correctors +CorrectorsPlaceholder: Correctors... +CorrectorsDefaulted: List of correctors was automatically generated based on those of preceding sheets for this course. No data has been saved, yet. + +Users: Users +HomeHeading: Home +LoginHeading: Authentication +LoginTitle: Authentication +ProfileHeading: Settings +ProfileFor: Settings for +ProfileDataHeading: Personal information +InfoHeading: Information +VersionHeading: Version history +ImpressumHeading: Imprint +DataProtHeading: Data protection +SystemMessageHeading: Uni2work system message +SystemMessageListHeading: Uni2work system message +NotificationSettingsHeading displayName: Notification settings for #{displayName} +TokensLastReset: Tokens last reset +TokensResetSuccess: Successfully invalidated all authorisation tokens + +HomeOpenAllocations: Active central allocations +HomeUpcomingSheets: Upcoming exercise sheets +HomeUpcomingExams: Upcoming exams + +NumCourses num: #{num} #{pluralEN num "course" "courses"} +CloseAlert: Close + +Name: Name +MatrikelNr: Matriculation +LdapSynced: LDAP-synchronised +LdapSyncedBefore: Last LDAP-synchronisation before +NoMatrikelKnown: No matriculation +Theme: Theme +Favourites: Number of saved favourites +FavouritesTip: Only relevant for automatically generated favourites (“visited recently”) +FavouriteSemesters: Maximum number of semesters in favourites bar +Plugin: Plugin +Ident: Identification +LastLogin: Last login +Settings: Settings +SettingsUpdate: Successfully updated settings +NotificationSettingsUpdate: Successfully updated notification settings +Never: Never + +PreviouslyUploadedInfo: Files already uploaded: +PreviouslyUploadedDeletionInfo: (Files not checked will be deleted) +MultiFileUploadInfo: (Choose multiple files using Shift or Ctrl) +AddMoreFiles: Additional files: + +NrColumn: # +SelectColumn: Selection +DBTablePagesize: Entries per page +DBTablePagesizeAll: All + +CorrDownload: Download +CorrUploadField: Corrections +CorrUpload: Upload corrections +CorrSetCorrector: Assign corrector +CorrSetCorrectorTooltip: Submissions already assigned to a corrector must first be assigned to “” before they can be assigned again. +CorrAutoSetCorrector: Distribute corrections +CorrDelete: Delete submissions +NatField name: #{name} must be a natural number! +JSONFieldDecodeFailure aesonFailure: Could not parse JSON: #{aesonFailure} +SecretJSONFieldDecryptFailure: Could not decrypt hidden data + +SubmissionsAlreadyAssigned num: #{num} #{pluralEN num "correction" "corrections"} were already assigned to a corrector and were left unchanged: +SubmissionsAssignUnauthorized num: #{num} #{pluralEN num "correction" "corrections"} cannot currently be assigned to correctors (e.g. because changes to submissions are still allowed) +UpdatedAssignedCorrectorSingle num: Successfully assigned #{num} #{pluralEN num "correction" "corrections"} to the corrector. +NoCorrector: No corrector +RemovedCorrections num: Successfully deleted #{num} #{pluralEN num "correction" "corrections"} +UpdatedAssignedCorrectorsAuto num: Successfully distributed #{num} #{pluralEN num "correction" "corrections"} among correctors. +UpdatedSheetCorrectorsAutoAssigned n: Successfully distributed #{n} #{pluralEN n "correction" "corrections"} among correctors. +UpdatedSheetCorrectorsAutoFailed n: #{n} #{pluralEN n "correction" "corrections"} could not be distributed. +CouldNotAssignCorrectorsAuto num: #{num} #{pluralEN num "correction" "corrections"} could not be distributed: +SelfCorrectors num: #{num} #{pluralEN num "correction was" "corrections were"} assigned to correctors that are also submittors for their correction! + +SubmissionOriginal: Original +SubmissionCorrected: Marked +SubmissionArchiveName: submissions +SubmissionTypeArchiveName tid ssh csh shn subId renderedSfType: #{foldCase (termToText (unTermKey tid))}-#{foldedCase (unSchoolKey ssh)}-#{foldedCase csh}-#{foldedCase shn}-#{foldCase (toPathPiece subId)}-#{foldCase renderedSfType} + +CorrectionSheets: Corrections by sheet +CorrectionCorrectors: Corrections by corrector +AssignSubmissionExceptionNoCorrectors: No correctors configured +AssignSubmissionExceptionNoCorrectorsByProportion: No correctors have any non-zero proportion +AssignSubmissionExceptionSubmissionsNotFound n: #{n} #{pluralEN n "submission" "submissions"} could not be identified +NrSubmittorsTotal: Submittors +NrSubmissionsTotal: Submissions +NrSubmissionsTotalShort: Subm +NrSubmissionsUnassigned: No corrector +NoCorrectorAssigned: No corrector +NrCorrectors: Correctors +NrSubmissionsNewlyAssigned: Newly assigned +NrSubmissionsNotAssigned: Not assigned +NrSubmissionsNotCorrected: Not corrected +NrSubmissionsNotCorrectedShort: N.corr. +CorrectionTime: Correction time +AssignSubmissionsRandomWarning: The assignment preview might be different from the actual assignment if multiple sheets are being distributed. This is due to the fact that only assigned submissions are considered when handling corrector-deficits. Due to this being a randomised process small differences are also possible for a single sheet. + +CorrectionsUploaded num: Successfully saved #{num} #{pluralEN num "correction" "corrections"}: +NoCorrectionsUploaded: No corrections could be found within the uploaded file. + +RatingBy: Marked by +HasCorrector: Corrector assigned +AssignedTime: Assigned +AchievedBonusPoints: Bonus points achieved +AchievedNormalPoints: Points achieved +AchievedPoints: Points achieved +AchievedPassPoints: Points achieved to pass +AchievedPasses: Passed sheets +AchievedOf achieved possible: #{achieved} of #{possible} +PassAchievedOf points passingPoints maxPoints: #{points} of #{maxPoints} (pass at #{passingPoints}) +PassedResult: Result +Passed: Passed +NotPassed: Failed +RatingTime: Marked +RatingComment: Comment +SubmissionUsers: Submittors +Rating: Marking +RatingPoints: Points +RatingDone: Rating visible +RatingPercent: Achieved +RatingFiles: Marked files +PointsNotPositive: Points may not be negative +PointsTooHigh maxPoints: Points may not be more than #{maxPoints} +PointsTooLow minPoints: Points may not be less than #{minPoints} +RatingPointsDone: Correction counts as marked iff “Points” is set +ColumnRatingPoints: Points +Pseudonyms: Pseudonyms + +Files: Files +FileTitle: Filename +FileModified: Last changed +VisibleFrom: Published +AccessibleSince: Accessible since + +Corrected: Marked +CorrectionAchievedPoints: Achieved points +CorrectionAchievedPass: Passed +FileCorrected: Marked (files) +FileCorrectedDeleted: Marked (deleted) +RatingUpdated: Successfully updated correction +RatingDeleted: Successfully reset correction +RatingFilesUpdated: Corrected files successfully overwritten + +RatingNotUnicode uexc: Marking file is not UTF-8 encoded: #{tshow uexc} +RatingMissingSeparator: Preamble of the marking file could not be identified +RatingMultiple: Correction contains multiple markings +RatingInvalid parseErr: Marking points could not be parsed as a number: #{parseErr} +RatingFileIsDirectory: Marking file must not be a directory +RatingNegative: Marking points may not be negative +RatingExceedsMax: Marking points exceed maximum +RatingNotExpected: No marking points expected for this sheet +RatingBinaryExpected: Marking must be 0 (=failed) or 1(=passed) +RatingPointsRequired: Marking points required for this sheet +RatingFile: Marking file + +SubmissionSinkExceptionDuplicateFileTitle file: File #{show file} occurs multiple files within zip-archive. +SubmissionSinkExceptionDuplicateRating: Found more than one marking file +SubmissionSinkExceptionRatingWithoutUpdate: Marking file found without permission +SubmissionSinkExceptionForeignRating smid: Foreign marking file for submission #{toPathPiece smid} found. +SubmissionSinkExceptionInvalidFileTitleExtension file: Filename “#{show file}” does not have any of the file extensions allowed for this sheet. + +MultiSinkException name error: An error occurred in submission “#{name}”: #{error} + +NoTableContent: No entries +NoUpcomingSheetDeadlines: No upcoming sheets +NoUpcomingExams difftime: No exams for your courses occur or allow registration in the next #{difftime} + +AdminHeading: Administration +AdminUserHeading: User administration +AdminUserRightsHeading: User permissions +AdminUserAuthHeading: User authentication +AdminUserHeadingFor: Profile of +AdminFor: Administrator +LecturerFor: Lecturer +LecturersFor: Lecturers +AssistantFor: Assistant +AssistantsFor: Assistants +TutorsFor n: #{pluralEN n "Tutor" "Tutors"} +CorrectorsFor n: #{pluralEN n "Corrector" "Correctors"} +UserListTitle: Comprehensive list of users +AccessRightsSaved: Successfully updated permissions +AccessRightsNotChanged: Permissions left unchanged + +LecturersForN n: #{pluralEN n "Lecturer" "Lecturers"} + +Date: Date +DateTimeFormat: Date and time format +DateFormat: Date format +TimeFormat: Time format +DownloadFiles: Automatically download files +DownloadFilesTip: When set, files are automatically treated as downloads. Otherwise behaviour is browser dependent (PDFs might, for example, be opened within the browser) +WarningDays: Deadline-preview +WarningDaysTip: How many days ahead should deadlines regarding exams etc. be displayed on the homepage? +NotificationSettings: Desired notifications +UserSchools: Relevant departments +UserSchoolsTip: You will only receive department-wide notifications for the selected departments. +FormNotifications: Notifications +FormBehaviour: Behaviour +FormCosmetics: Interface +FormPersonalAppearance: Public data +FormFieldRequiredTip: Marked fields need to be filled + +PersonalInfoExamAchievementsWip: The feature to display your exam achievements has not yet been implemented. +PersonalInfoOwnTutorialsWip: The feature to display tutorials you have been assigned to as tutor has not yet been implemented. +PersonalInfoTutorialsWip: The feature to display tutorials you have registered for has not yet been implemented. + +ActiveAuthTags: Active authorisation predicates + +InvalidDateTimeFormat: Invalid date and time format. YYYY-MM-DDTHH:MM[:SS] expected +AmbiguousUTCTime: The given timestamp cannot be converted to UTC unambiguously +IllDefinedUTCTime: The given timestamp cannot be converted to UTC + +LastEdits: Latest edits +EditedBy name time: #{time} by #{name} +LastEdit: Latest edit +LastEditByUser: Your last edit +NoEditByUser: Not edited by you + +SubmissionFilesIgnored n: Ignored #{n} #{pluralEN n "file" "files"} +SubmissionDoesNotExist smid: There is no submission “#{toPathPiece smid}”. + +LDAPLoginTitle: Campus login +PWHashLoginTitle: Uni2work login +PWHashLoginNote: Use this form if you have received special credentials from the Uni2work-team. Most users need to use campus login! +DummyLoginTitle: Development login +LoginNecessary: Please log in first! + +InternalLdapError: Internal error during campus login + +CampusUserInvalidEmail: Could not determine email address during campus login +CampusUserInvalidDisplayName: Could not determine display name during campus login +CampusUserInvalidGivenName: Could not determine given name during campus login +CampusUserInvalidSurname: Could not determine surname during campus login +CampusUserInvalidTitle: Could not determine title during campus login +CampusUserInvalidMatriculation: Could not determine matriculation during campus login +CampusUserInvalidFeaturesOfStudy parseErr: Could not determine features of study during campus login +CampusUserInvalidAssociatedSchools parseErr: Could not determine associated departments during campus login +CampusUserInvalidSex: Could not determine sex during campus login + +CorrectorNormal: Normal +CorrectorMissing: Missing +CorrectorExcused: Excused +CorrectorStateTip: Missing correctors are assigned additional corrections during later sheets. Excused correctors are not assigned any additional deficit. + +DayIsAHoliday tid name date: “#{name}” (#{date}) is a legal holiday +DayIsOutOfLecture tid name date: “#{name}” (#{date}) is not within lecture period of #{tid} +DayIsOutOfTerm tid name date: “#{name}” (#{date}) is not within #{tid} + +UploadModeNone: No Upload +UploadModeAny: Upload arbitrary files +UploadModeSpecific: Upload pre-defined files + +UploadModeUnpackZips: Upload multiple files +UploadModeUnpackZipsTip: If upload of multiple files is permitted, supported archive formats are also accepted. Archives are automatically unpacked during upload. + +AutoUnzip: Automatically unpack ZIPs +AutoUnzipInfo: Automatically unpacks ZIP-files (*.zip) and adds their content to the root directory. + +UploadModeExtensionRestriction: Allowed file extensions +UploadModeExtensionRestrictionTip: Comma-separated. If no file extensions are specified, uploads are not restricted. +UploadModeExtensionRestrictionEmpty: List of permitted file extensions may not be emptyy + +UploadSpecificFiles: Pre-defined files +NoUploadSpecificFilesConfigured: If pre-defined files are selected, at least one file needs to be configured. +UploadSpecificFilesDuplicateNames: Names of pre-defined files must be unique +UploadSpecificFilesDuplicateLabels: Labels of pre-defined files must be unique +UploadSpecificFileLabel: Label +UploadSpecificFileName: Filename +UploadSpecificFileRequired: Required for submission + +NoSubmissions: No submission +CorrectorSubmissions: External submission via pseudonym +UserSubmissions: Direct submission in Uni2work +BothSubmissions: Submission either directly in Uni2work or externally via pseudonym + +SheetCorrectorSubmissionsTip: Submissions are expected to be handed in through some Uni2work-external procedure (usually on paper) marked with your personal pseudonym. Correctors can, using the pseudonym, register the marking in Uni2work for you to review. + +SubmissionNoUploadExpected: No upload of files expected. +SubmissionReplace: Replace submission +SubmissionCreated: Successfully created submission +SubmissionUpdated: Successfully replaced submission + +AdminFeaturesHeading: Features of study +StudyTerms: Fields of study +StudyTerm: Field of study +NoStudyTermsKnown: No known features of study +StudyFeatureInference: Infer field of study mapping +StudyFeatureInferenceNoConflicts: No observed conflicts +StudyFeatureInferenceConflictsHeading: Fields of study with observed conflicts +StudyFeatureAge: Semester +StudyFeatureDegree: Degree +FieldPrimary: Major +FieldSecondary: Minor +ShortFieldPrimary: Mj +ShortFieldSecondary: Mn +NoStudyField: No field of study +StudyFeatureType: +StudyFeatureValid: Valid +StudyFeatureUpdate: Updated + +DegreeKey: Degree key +DegreeName: Degree +DegreeShort: Degree shorthand +StudyTermsKey: Field key +StudyTermsName: Field of study +StudyTermsShort: Field shorthand +StudyTermsChangeSuccess: Successfully updated fields of study +StudyDegreeChangeSuccess: Successfully updated degrees +StudyCandidateIncidence: Synchronisation +AmbiguousCandidatesRemoved n: Successfully removed #{n} ambiguous #{pluralEN n "candidate" "candidates"} +RedundantCandidatesRemoved n: Successfully removed #{n} rendundant #{pluralEN n "candidate" "candidates"} +CandidatesInferred n: Successfully inferred #{n} field #{pluralEN n "mapping" "mappings"} +NoCandidatesInferred: No new mappings inferred +AllIncidencesDeleted: Successfully deleted all observations +IncidencesDeleted n: Successfully deleted #{show n} #{pluralEN n "observation" "observations"} +StudyTermIsNew: New +StudyFeatureConflict: Observed conflicts in field mapping + +MailTestFormEmail: Email address +MailTestFormLanguages: Language settings + +MailTestSubject: Uni2work test email +MailTestContent: This is a test email sent by Uni2work. No action on your part is required. +MailTestDateTime: Test of datetime formatting: + +German: German +GermanGermany: German (Germany) +English: English +EnglishEurope: English (Europe) + +MailSubjectSubmissionRated csh: Your #{csh}-submission was marked +MailSubmissionRatedIntro courseName termDesc: Your submission for #{courseName} (#{termDesc}) was marked. + +MailSubjectSheetActive csh sheetName: #{sheetName} in #{csh} was released +MailSheetActiveIntro courseName termDesc sheetName: You may now download #{sheetName} for #{courseName} (#{termDesc}). + +MailSubjectCourseRegistered csh: You were enrolled for #{csh} +MailSubjectCourseRegisteredOther displayName csh: #{displayName} was enrolled for #{csh} +MailCourseRegisteredIntro courseName termDesc: You were enrolled for the course “#{courseName}” (#{termDesc}) +MailCourseRegisteredIntroOther displayName courseName termDesc: #{displayName} was enrolled for the course “#{courseName}” (#{termDesc}). + +MailSubjectExamResult csh examn: Results for #{examn} in #{csh} are now available +MailExamResultIntro courseName termDesc examn: You may now view your result for #{examn} of the course #{courseName} (#{termDesc}). + +MailSubjectExamOfficeExamResults csh examn: Results for #{examn} of #{csh} are now available +MailExamOfficeExamResultsIntro courseName termDesc examn: A course administrator has made the results for #{examn} of the course #{courseName} (#{termDesc}) available. + +MailSubjectExamOfficeExamResultsChanged csh examn: Results for #{examn} of #{csh} were changed +MailExamOfficeExamResultsChangedIntro courseName termDesc examn: A course administrator has changed exam results for #{examn} of the course #{courseName} (#{termDesc}). + +MailSubjectExamRegistrationActive csh examn: Registration is now allowed for #{examn} of #{csh} +MailExamRegistrationActiveIntro courseName termDesc examn: You may now register for #{examn} of the course #{courseName} (#{termDesc}). + +MailSubjectExamRegistrationSoonInactive csh examn: The registration period for #{examn} of #{csh} ends shortly +MailExamRegistrationSoonInactiveIntro courseName termDesc examn: Soon you will no longer be allowed to register for #{examn} of #{courseName} (#{termDesc}). + +MailSubjectExamDeregistrationSoonInactive csh examn: Deregistration for #{examn} in #{csh} ends shortly +MailExamDeregistrationSoonInactiveIntro courseName termDesc examn: Soon you will no longer be allowed to deregister from #{examn} of #{courseName} (#{termDesc}). + +MailSubjectSubmissionsUnassigned csh sheetName: Corrections for #{sheetName} of #{csh} could not be distributed +MailSubmissionsUnassignedIntro n courseName termDesc sheetName: #{n} corrections for #{sheetName} of the course #{courseName} (#{termDesc}) could not be automatically distributed. + +MailSubjectSheetSoonInactive csh sheetName: The submission period for #{sheetName} of #{csh} ends shortly +MailSheetSoonInactiveIntro courseName termDesc sheetName: Soon you will no longer be allowed to submit for #{sheetName} of the course #{courseName} (#{termDesc}). +MailSubjectSheetInactive csh sheetName: The submission period for #{sheetName} of #{csh} has ended +MailSheetInactiveIntro courseName termDesc sheetName n num: The submission period for #{sheetName} of the course #{courseName} (#{termDesc}) has ended. #{noneOneMoreEN num "" "One participant" (toMessage num <> " participants")}#{noneOneMoreEN n "" "" (" made " <> toMessage num)}#{noneOneMoreEN n "There were no submissions" " made one submission" " submissions"}. + +MailSubjectCorrectionsAssigned csh sheetName: You were assigned corrections for #{sheetName} of #{csh} +MailCorrectionsAssignedIntro courseName termDesc sheetName n: You were assigned #{n} #{pluralEN n "correction" "corrections"} for #{sheetName} of #{courseName} (#{termDesc}). + +MailSubjectUserRightsUpdate name: Permissions for #{name} changed +MailUserRightsIntro name email: #{name} <#{email}> now has the following permissions: +MailNoLecturerRights: You don't currently have lecturer permissions for any department. +MailLecturerRights n: As a lecturer you may create new courses within your #{pluralEN n "department" "departments"}. + +MailSubjectUserAuthModeUpdate: Your Uni2work login +UserAuthModePWHashChangedToLDAP: You can now log in to Uni2work using your Campus-account +UserAuthModeLDAPChangedToPWHash: You can now log in to Uni2work using your Uni2work-internal account +NewPasswordLinkTip: You can set the password for your Uni2work-internal account on the following page: +NewPasswordLink: Set password +AuthPWHashTip: You now need to use the login form labeled "Uni2work login". Please ensure that you have already set a password when you try to log in. +PasswordResetEmailIncoming: For security reasons you will receive a link to the page on which you can set and later change your password in a separate email. + +MailEditNotifications: Enable/Disable notifications +MailSubjectSupport: Support request +MailSubjectSupportCustom customSubject: [Support] #{customSubject} + +CommCourseSubject: Course message +MailSubjectLecturerInvitation tid ssh csh: [#{tid}-#{ssh}-#{csh}] Invitation to be a course administrator +InvitationAcceptDecline: Accept/Decline invitation +InvitationFromTip displayName: You are receiving this invitation because #{displayName} has caused it to be sent from within Uni2work. +InvitationUniWorXTip: Uni2work is a web based teaching management system at LMU Munich. + +MailSubjectParticipantInvitation tid ssh csh: [#{tid}-#{ssh}-#{csh}] Invitation to be a course participant + +MailSubjectCorrectorInvitation tid ssh csh shn: [#{tid}-#{ssh}-#{csh}] Invitation to be a corrector for #{shn} + +MailSubjectTutorInvitation tid ssh csh tutn: [#{tid}-#{ssh}-#{csh}] Invitation to be a tutor for #{tutn} + +MailSubjectExamCorrectorInvitation tid ssh csh examn: [#{tid}-#{ssh}-#{csh}] Invitation to be a corrector for #{examn} + +MailSubjectExamRegistrationInvitation tid ssh csh examn: [#{tid}-#{ssh}-#{csh}] Invitation to be a participant of #{examn} + +MailSubjectSubmissionUserInvitation tid ssh csh shn: [#{tid}-#{ssh}-#{csh}] Invitation to participate in a submission for #{shn} + +MailSubjectExamOfficeUserInvitation displayName: Consideration of your exam achievements within Uni2work + +MailSubjectPasswordReset: Set/Change Uni2work password + +SheetGrading: Marking +SheetGradingPoints maxPoints: #{maxPoints} #{pluralEN maxPoints "point" "points"} +SheetGradingPassPoints maxPoints passingPoints: Pass with #{passingPoints} of #{maxPoints} #{pluralEN maxPoints "point" "points"} +SheetGradingPassBinary: Pass/Fail +SheetGradingInfo: "Passing by points" counts both towards the maximum achievable number of points and towards the number of sheets that can be passed. + +SheetGradingCount': Number +SheetGradingPoints': Points +SheetGradingPassing': Passing +SheetGradingPassPoints': Passing by points +SheetGradingPassBinary': Pass/Fail + +SheetTypeBonus grading: Bonus +SheetTypeNormal grading: Normal +SheetTypeInformational grading: Informational +SheetTypeNotGraded: Not marked +SheetTypeInfoNotGraded: "Not marked" means that there will be no feedback at all. +SheetTypeInfoBonus: Sheets marked "bonus" count normally but do not increase either the maximum number of points or the count of sheets that can be passed. +SheetTypeInfoInformational: Sheets marked "informational" do not counted anywhere. They are marked only as feedback for participants. +SheetGradingBonusIncluded: Achieved bonus points are already counted among the achieved normal Their marking points. +SummaryTitle: Summary of +SheetGradingSummaryTitle intgr: #{intgr} #{pluralEN intgr "sheet" "sheets"} +SubmissionGradingSummaryTitle intgr: #{intgr} #{pluralEN intgr "submission" "submissions"} + +SheetTypeBonus': Bonus +SheetTypeNormal': Normal +SheetTypeInformational': Informational +SheetTypeNotGraded': Not marked + +SheetGradingMaxPoints: Maximum number of points +SheetGradingPassingPoints: Points necessary to pass + +SheetGroupArbitrary: Arbitrary groups +SheetGroupRegisteredGroups: Registered groups +SheetGroupNoGroups: No group submission +SheetGroupMaxGroupsize: Maximum group size + +SheetFiles: Exercise sheet files +SheetFileTypeHeader: Belongs to + +SheetArchiveName tid ssh csh shn: #{foldCase (termToText (unTermKey tid))}-#{foldedCase (unSchoolKey ssh)}-#{foldedCase csh}-#{foldedCase shn} +SheetTypeArchiveName tid ssh csh shn renderedSft: #{foldCase (termToText (unTermKey tid))}-#{foldedCase (unSchoolKey ssh)}-#{foldedCase csh}-#{foldedCase shn}-#{foldCase renderedSft} + +NotificationTriggerSubmissionRatedGraded: My submission for an exercise sheet was marked (not purely informational) +NotificationTriggerSubmissionRated: My submission for an exercise sheet was marked +NotificationTriggerSheetActive: I can now download a new exercise sheet +NotificationTriggerSheetSoonInactive: I will soon no longer be able to submit for an exercise sheet +NotificationTriggerSheetInactive: The submission period for one of my exercise sheets is over +NotificationTriggerCorrectionsAssigned: I was assigned corrections +NotificationTriggerCorrectionsNotDistributed: Not all submissions for one of my exercise sheets could be assigned a corrector +NotificationTriggerUserRightsUpdate: My permissions changed +NotificationTriggerUserAuthModeUpdate: My login mode changed +NotificationTriggerExamRegistrationActive: I can now register for an exam +NotificationTriggerExamRegistrationSoonInactive: I will soon no longer be able to register for an exam +NotificationTriggerExamDeregistrationSoonInactive: I will soon no longer be able to deregister from an exam +NotificationTriggerExamResult: An exam result is available +NotificationTriggerAllocationStaffRegister: I can now register a course for central allocation +NotificationTriggerAllocationAllocation: I can now grade applications to a central alloction for one of my courses +NotificationTriggerAllocationRegister: I can now apply to a new central allocation +NotificationTriggerAllocationOutdatedRatings: Applications to a central allocation for one of my courses have changed since they were graded +NotificationTriggerAllocationUnratedApplications: Grades are pending for applications to a central allocation for one of my courses +NotificationTriggerAllocationResults: Participants have been placed by one of my central allocations +NotificationTriggerExamOfficeExamResults: New exam results are available +NotificationTriggerExamOfficeExamResultsChanged: Exam results have changed +NotificationTriggerCourseRegistered: A course administrator has enrolled me in a course + +NotificationTriggerKindAll: For all users +NotificationTriggerKindCourseParticipant: For course participants +NotificationTriggerKindExamParticipant: For exam participants +NotificationTriggerKindCorrector: For correctors +NotificationTriggerKindLecturer: For lecturers +NotificationTriggerKindCourseLecturer: For course administrators +NotificationTriggerKindAdmin: For administrators +NotificationTriggerKindExamOffice: For the exam office +NotificationTriggerKindEvaluation: For course evaluations +NotificationTriggerKindAllocationStaff: For central allocations (lecturers) +NotificationTriggerKindAllocationParticipant: For central allocations + +CorrCreate: Register submissions +UnknownPseudonymWord pseudonymWord: Invalid pseudonym-word “#{pseudonymWord}” +InvalidPseudonym pseudonym: Invalid pseudonym “#{pseudonym}” +InvalidPseudonymSubmissionIgnored oPseudonyms iPseudonym: The submission with pseudonyms “#{oPseudonyms}” has been ignored since “#{iPseudonym}” could not be automatically corrected to be a valid pseudonym. +PseudonymAutocorrections: Suggestions: +UnknownPseudonym pseudonym: Unknown pseudonym “#{pseudonym}” +CorrectionPseudonyms: Pseudonyms +CorrectionPseudonymsTip: One submission per line. For group submissions include multiple pseudonyms (comma-separated) per line. Typos may be automatically corrected. +PseudonymSheet: Exercise sheet +CorrectionPseudonymSheet termDesc csh shn: #{termDesc} » #{csh} » #{shn} +SheetGroupTooLarge sheetGroupDesc: Submission group to large: #{sheetGroupDesc} +SheetNoRegisteredGroup sheetGroupDesc: “#{sheetGroupDesc}” are not registered as a submission group +SheetAmbiguousRegisteredGroup sheetGroupDesc: “#{sheetGroupDesc}” contains members of multiple submission groups +SheetNoGroupSubmission sheetGroupDesc: Group submission is not allowed for this exercise sheet (#{sheetGroupDesc}) +SheetDuplicatePseudonym: The following pseudonyms occurred multiple times. All occurrances except the first have been ignored: +SheetCreateExisting: The following pseudonyms have already submitted: + +CorrGrade: Mark submissions + +UserAccountDeleted name: User account for #{name} was deleted! +UserSubmissionsDeleted n: #{tshow n} #{pluralEN n "submission was" "submissions were"} permanently deleted. +UserGroupSubmissionsKept n: #{tshow n} #{pluralEN n "group submission was" "group submissions were"} kept. They are no longer associated with the deleted user. Group submissions can thus become as if made by a single user. Such submissions are deleted together with their last user. +UserSubmissionGroupsDeleted count: #{tshow count} #{pluralEN count "submission group was" "submission groups were"} deleted since #{pluralEN count "it" "they"} would have become empty. +UserAccountDeleteWarning: Caution, this permanently deletes users and all of their associated data. Exam results must be stored long term! + +HelpTitle: Support +HelpAnswer: Send answers to +HelpUser: My user account +HelpAnonymous: Send no answers (anonymous) +HelpEmail: Email +HelpSubject: Subject +HelpRequest: Support request / Suggestion +HelpProblemPage: Problematic page +HelpIntroduction: If you have trouble using this website or if you find something that could be improved, please contact us even if you were already able to solve your problem by yourself! We are continually making changes and try to keep the site as intuitive as possible even for new users. +HelpSent: Your support request has been sent. + +InfoLecturerTitle: Information for lecturers + +SystemMessageFrom: Visible from +SystemMessageTo: Visible to +SystemMessageAuthenticatedOnly: Only logged in users +SystemMessageSeverity: Severity +SystemMessageId: Id +SystemMessageSummaryContent: Summary / Content +SystemMessageSummary: Summary +SystemMessageContent: Content +SystemMessageLanguage: Language + +SystemMessageDelete: Delete +SystemMessageActivate: Set to be visible +SystemMessageDeactivate: Set to be invisible +SystemMessageTimestamp: Timestamp + +SystemMessagesDeleted: System messages deleted: +SystemMessagesActivated: System messages set to become visible at: +SystemMessagesDeactivated: System messages set to become invisable at: +SystemMessageEmptySelection: No system messages selected +SystemMessageAdded sysMsgId: System message added: #{toPathPiece sysMsgId} +SystemMessageEdit: Edit system message +SystemMessageEditTranslations: Edit translations +SystemMessageAddTranslation: Add translation + +SystemMessageEditSuccess: Successfully edited system message. +SystemMessageAddTranslationSuccess: Successfully added translation. +SystemMessageEditTranslationSuccess: Successfully edited translation. +SystemMessageDeleteTranslationSuccess: Successfully deleted translation. + +MessageError: Error +MessageWarning: Warning +MessageInfo: Information +MessageSuccess: Success + +InvalidLangFormat: Invalid language code (RFC1766) + +ErrorResponseTitleNotFound: Resource not found +ErrorResponseTitleInternalError internalError@Text: An internal error occurred +ErrorResponseTitleInvalidArgs invalidArgs@Texts: Request contained invalid arguments +ErrorResponseTitleNotAuthenticated: Request requires authentication +ErrorResponseTitlePermissionDenied permissionDenied@Text: Permission denied +ErrorResponseTitleBadMethod requestMethod@Method: HTTP-method not supported + +UnknownErrorResponse: An error has occurred that could not be further classified: +ErrorResponseNotFound: No page could be found under the url requested by your browser. +ErrorResponseNotAuthenticated: To be granted access to most parts of Uni2work you need to login first. +ErrorResponseBadMethod requestMethodText@Text: Your browser can interact in multiple ways with the resources offered by Uni2work. The requested method (#{requestMethodText}) is not supported here. + +ErrorResponseEncrypted: In order not to reveal sensitive information further details have been encrypted. If you send a support request, please include the encrypted data listed below. +ErrMsgCiphertext: Encrypted error message +EncodedSecretBoxCiphertextTooShort: Encrypted data are too short to be valid +EncodedSecretBoxInvalidBase64 base64Err@String: Encrypted data ar not correctly base64url-encoded: #{base64Err} +EncodedSecretBoxInvalidPadding: Encrypted data are not padded correctly +EncodedSecretBoxCouldNotDecodeNonce: Could not decode secretbox-nonce +EncodedSecretBoxCouldNotOpenSecretBox: Could not open libsodium-secretbox (Encrypted data are not authentic) +EncodedSecretBoxCouldNotDecodePlaintext aesonErr@String: Could not decode json cleartext: #{aesonErr} +ErrMsgHeading: Decrypt error message + +InvalidRoute: Could not interpret url + +MenuOpenCourses: Courses with open registration +MenuOpenAllocations: Active central allocations +MenuHome: Home +MenuInformation: Information +MenuImpressum: Imprint +MenuDataProt: Data protection +MenuVersion: Version history +MenuInstance: Instance identification +MenuHealth: Instance health +MenuHelp: Support +MenuProfile: Settings +MenuLogin: Login +MenuLogout: Logout +MenuAllocationList: Central allocations +MenuCourseList: Courses +MenuCourseMembers: Participants +MenuCourseAddMembers: Add participants +MenuCourseCommunication: Course message (email) +MenuCourseApplications: Applications +MenuCourseExamOffice: Exam offices +MenuTermShow: Semesters +MenuSubmissionDelete: Delete submission +MenuUsers: User +MenuUserAdd: Add user +MenuUserNotifications: Notification settings +MenuUserPassword: Password +MenuAdminTest: Admin-demo +MenuMessageList: System messages +MenuAdminErrMsg: Decrypt error message +MenuProfileData: Personal information +MenuTermCreate: Create new semester +MenuCourseNew: Create new course +MenuTermEdit: Edit semester +MenuTermCurrent: Current semester +MenuCorrection: Marking +MenuCorrections: Corrections +MenuCorrectionsOwn: My corrections +MenuSubmissions: Submissions +MenuSheetList: Exercise sheets +MenuMaterialList: Material +MenuMaterialNew: Publish new material +MenuMaterialEdit: Edit material +MenuMaterialDelete: Delete material +MenuTutorialList: Tutorials +MenuTutorialNew: Create new tutorial +MenuSheetNew: Create new exercise sheet +MenuSheetCurrent: Current exercise sheet +MenuSheetOldUnassigned: Submissions without corrector +MenuCourseEdit: Edit course +MenuCourseClone: Clone course +MenuCourseDelete: Delete course +MenuSubmissionNew: Create submission +MenuSubmissionOwn: Submission +MenuCorrectors: Correctors +MenuCorrectorsChange: Adjust correctors +MenuSheetEdit: Edit exercise sheet +MenuSheetDelete: Delete exercise sheet +MenuSheetClone: Clone exercise sheet +MenuCorrectionsUpload: Upload corrections +MenuCorrectionsDownload: Download corrections +MenuCorrectionsCreate: Register submissions +MenuCorrectionsGrade: Grade submissions online +MenuCorrectionsAssign: Assign corrections +MenuCorrectionsAssignSheet name@Text: Assign corrections for #{name} +MenuAuthPreds: Authorisation settings +MenuTutorialDelete: Delete tutorial +MenuTutorialEdit: Edit tutorial +MenuTutorialComm: Send course message +MenuExamList: Exams +MenuExamNew: Create new exam +MenuExamEdit: Edit +MenuExamUsers: Participants +MenuExamGrades: Exam results +MenuExamAddMembers: Add exam participants +MenuExamOfficeExams: Exams +MenuExamOfficeFields: Fields of study +MenuExamOfficeUsers: Users +MenuLecturerInvite: Add functionaries +MenuAllocationInfo: Information regarding central allocations +MenuCourseApplicationsFiles: Files of all applications +MenuSchoolList: Departments +MenuSchoolNew: Create new department +MenuCourseNewsNew: Add course news +MenuCourseNewsEdit: Edit course news +MenuCourseEventNew: New course occurrence +MenuCourseEventEdit: Edit course occurrence + +BreadcrumbSubmissionFile: File +BreadcrumbSubmissionUserInvite: Invitation to participate in a submission +BreadcrumbCryptoIDDispatch: CryptoID-redirect +BreadcrumbCourseAppsFiles: Application files +BreadcrumbCourseNotes: Course notes +BreadcrumbHiWis: Correctors +BreadcrumbMaterial: Material +BreadcrumbSheet: Exercise sheet +BreadcrumbTutorial: Tutorial +BreadcrumbExam: Exam +BreadcrumbApplicant: Applicant +BreadcrumbCourseRegister: Register +BreadcrumbCourseRegisterTemplate: Application template +BreadcrumbCourseFavourite: Favourite +BreadcrumbCourse: Course +BreadcrumbAllocationRegister: Register participation +BreadcrumbAllocation: Central allocation +BreadcrumbTerm: Semester +BreadcrumbSchool: Department +BreadcrumbUser: User +BreadcrumbStatic: Static resource +BreadcrumbFavicon: Favicon +BreadcrumbRobots: robots.txt +BreadcrumbMetrics: Metrics +BreadcrumbLecturerInvite: Invitation to be a course administrator +BreadcrumbExamOfficeUserInvite: Invitation regarding exam achievements +BreadcrumbFunctionaryInvite: Invitation to be a department functionary +BreadcrumbUserDelete: Delete user account +BreadcrumbUserHijack: Hijack user session +BreadcrumbSystemMessage: System message +BreadcrumbSubmission: Submission +BreadcrumbCourseNews: Course news +BreadcrumbCourseNewsDelete: Delete course news +BreadcrumbCourseEventDelete: Delete course occurrence +BreadcrumbProfile: Settings +BreadcrumbAllocationInfo: On central allocations +BreadcrumbCourseParticipantInvitation: Invitation to be a course participant +BreadcrumbMaterialArchive: Archive +BreadcrumbMaterialFile: File +BreadcrumbSheetArchive: Files +BreadcrumbSheetIsCorrector: Corrector-check +BreadcrumbSheetPseudonym: Pseudonym +BreadcrumbSheetCorrectorInvite: Invitation to be a corrector +BreadcrumbSheetFile: File +BreadcrumbTutorialRegister: Register +BreadcrumbTutorInvite: Invitation to be a tutor +BreadcrumbExamCorrectorInvite: Invitation to be an exam corrector +BreadcrumbExamParticipantInvite: Invitation to be an exam participant +BreadcrumbExamRegister: Register +BreadcrumbApplicationFiles: Application files +BreadcrumbCourseNewsArchive: Archive +BreadcrumbCourseNewsFile: File + +TitleMetrics: Metrics + +AuthPredsInfo: To view their own courses like a participant would, administrators and correctors can deactivate the checking of their credentials temporarily. Disabled authorisation predicates always fail. This means that deactivated predicates are not checked to grant access where it would otherwise not be permitted. These settings are only temporary, until your session expires i.e. your browser-cookie does. By deactivating predicates you can lock yourself out temporarily, at most. +AuthPredsActive: Active authorisation predicates +AuthPredsActiveChanged: Authorisation settings saved for the current session +AuthTagFree: Page is freely accessable +AuthTagAdmin: User is administrator +AuthTagExamOffice: User is part of an exam office +AuthTagToken: User is presenting an authorisation-token +AuthTagNoEscalation: User permissions are not being expanded to other departments +AuthTagDeprecated: Page is not deprecated +AuthTagDevelopment: Page is not in development +AuthTagLecturer: User is lecturer +AuthTagCorrector: User is corrector +AuthTagTutor: User is tutor +AuthTagTutorControl: Tutors have control over their tutorial +AuthTagTime: Time restrictions are fulfilled +AuthTagStaffTime: Time restrictions wrt. staff are fulfilled +AuthTagAllocationTime: Time restrictions due to a central allocation are fulfilled +AuthTagCourseRegistered: User is enrolled in course +AuthTagAllocationRegistered: User participates in central allocation +AuthTagTutorialRegistered: User is tutorial participant +AuthTagExamRegistered: User is exam participant +AuthTagExamResult: User has an exam result +AuthTagParticipant: User participates in course +AuthTagApplicant: User is applicant for course +AuthTagRegisterGroup: User is not participant in any tutorial of the same registration group +AuthTagCapacity: Capacity is sufficient +AuthTagEmpty: Course is empty +AuthTagMaterials: Course material is publicly accessable +AuthTagOwner: User is owner +AuthTagRated: Submission is marked +AuthTagUserSubmissions: Submissions are made by course participants +AuthTagCorrectorSubmissions: Submissions are registered by correctors +AuthTagSelf: User is only accessing their only data +AuthTagIsLDAP: User logs in using their campus account +AuthTagIsPWHash: User logs in using their Uni2work-internal account +AuthTagAuthentication: User is authenticated +AuthTagRead: Access is read only +AuthTagWrite: Access might write + +DeleteCopyStringIfSure n: If you are sure that you want to permanently delete the #{pluralEN n "object" "objects"} listed below, please copy the shown text. +DeletePressButtonIfSure n: If you are sure that you want to permanently delete the #{pluralEN n "object" "objects"} listed below, please confirm the action by pressing the button. +DeleteConfirmation: Confirmation text +DeleteConfirmationWrong: Confirmation text must match the shown text exactly. + +DBTIRowsMissing n: #{pluralDE n "A line" "A number of lines"} vanished from the database since the form you submitted was generated for you + +MassInputAddDimension: + +MassInputDeleteCell: - + +NavigationFavourites: Favourites + +CommSubject: Subject +CommBody: Message +CommBodyTip: This input field currently accepts only Html. Line breaks are thus ignored and must be designated manually by inserting
in the appropriate places. +CommRecipients: Recipients +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. +CommDuplicateRecipients n: #{n} duplicate #{pluralEN n "recipient" "recipients"} ignored +CommSuccess n: Message was sent to #{n} #{pluralEN n "recipient" "recipients"} +CommUndisclosedRecipients: Undisclosed recipients +CommAllRecipients: all-recipients + +CommCourseHeading: Course message +CommTutorialHeading: Tutorial message + +RecipientCustom: Custom recipients +RecipientToggleAll: All/None + +DBCsvImportActionToggleAll: All/None + +RGCourseParticipants: Course participants +RGCourseLecturers: Course administrators +RGCourseCorrectors: Course correctors +RGCourseTutors: Course tutors +RGTutorialParticipants: Tutorial participants + +MultiSelectFieldTip: Multiple selections are possible (Shift or Ctrl) +MultiEmailFieldTip: Multiple emails addresses may be specified (comma-separated) +EmailInvitationWarning: This address could not be matched to any Uni2work-user under your current permissions (may be subject to some restrictions). An invitation will be sent via email. + +LecturerInvitationAccepted lType csh: You were registered as #{lType} for #{csh} +LecturerInvitationDeclined csh: You have declined the invitation to become course administrator for #{csh} +CourseLecInviteHeading courseName: Invitation to be a course administrator for #{courseName} +CourseLecInviteExplanation: You were invited to be a course administrator. + +CourseParticipantInviteHeading courseName: Invitation to enrol for #{courseName} +CourseParticipantInviteExplanation: You were invited to be a participant of a course. +CourseParticipantEnlistDirectly: Enrol known users directly +CourseParticipantInviteField: Email addresses to invite + +CourseParticipantInvitationAccepted courseName: You were enrolled in #{courseName} + +CorrectorInvitationAccepted shn: You are now a corrector for #{shn} +CorrectorInvitationDeclined shn: You have declined the invitation to be a corrector for #{shn} +SheetCorrInviteHeading shn: Invitation to be a corrector for #{shn} +SheetCorrInviteExplanation: You were invited to be a sheet corrector. + +TutorInvitationAccepted tutn: You are now tutor for #{tutn} +TutorInvitationDeclined tutn: You have declined the invitation to be a tutor for #{tutn} +TutorInviteHeading tutn: Invitation to be tutor for #{tutn} +TutorInviteExplanation: You were invited to be a tutor. + +ExamCorrectorInvitationAccepted examn: You are now corrector for #{examn} +ExamCorrectorInvitationDeclined examn: You have declined the invitation to be a corrector for #{examn} +ExamCorrectorInviteHeading examn: Invitation to be a corrector for #{examn} +ExamCorrectorInviteExplanation: You were invited to be a corrector. + +ExamRegistrationInvitationAccepted examn: You are now registered for #{examn} +ExamRegistrationInvitationDeclined examn: You have declined the invitation to participate in #{examn} +ExamRegistrationInviteHeading examn: Invitation to participate in #{examn} +ExamRegistrationInviteExplanation: You were invited to register for an exam. + +SubmissionUserInvitationAccepted shn: You now participate in a submission for #{shn} +SubmissionUserInvitationDeclined shn: You have declined the invitation to participate in a submission for #{shn} +SubmissionUserInviteHeading shn: Invitation to participate in a submission for #{shn} +SubmissionUserInviteExplanation: You were invited to participate in a submission for an exercise sheet. + +ExamOfficeUserInviteHeading displayName: Access of your exam achievements by #{displayName} +ExamOfficeUserInviteExplanation: To properly consider your exam achievements (e.g. in the final transcript of records for Erasmus-students) you are invited to grant access to the appropriate parties. +ExamOfficeUserInvitationAccepted: Access to exam achievements granted + +InvitationAction: Action +InvitationActionTip: Declined invitations cannot be accepted later +InvitationMissingRestrictions: Your authorisation-token is missing required data +InvitationCollision: Invitation could not be accepted since an entry of this type already exists +InvitationDeclined: Invitation declined +BtnInviteAccept: Accept invitation +BtnInviteDecline: Decline invitation + +LecturerType: Role +ScheduleKindWeekly: Weekly + +ScheduleRegular: Regular occurrence +ScheduleRegularKind: Schedule +WeekDay: Day of the week +Day: Day +OccurrenceStart: Start +OccurrenceEnd: End +OccurrenceNever: Never +ScheduleExists: This schedule already exists + +ScheduleExceptions: Exceptions +ScheduleExceptionsTip: “Does not occur” overrides the regular schedule. “Does occur” overides “does not occur”. +ExceptionKind: Event ... +ExceptionKindOccur: Does occur +ExceptionKindNoOccur: Does not occur +ExceptionExists: This exception already exists +ExceptionNoOccurAt: Event + +TutorialType: Type +TutorialTypePlaceholder: Tutorial, Exercise discussion, ... +TutorialTypeTip: Only for informational purposes +TutorialName: Name +TutorialParticipants: Participants +TutorialCapacity: Capacity +TutorialFreeCapacity: Free capacity +TutorialRoom: Regular room +TutorialTime: Time +TutorialRegistered: Registered +TutorialRegGroup: Registration group +TutorialRegisterFrom: Register from +TutorialRegisterTo: Register to +TutorialDeregisterUntil: Deregister until +TutorialsHeading: Tutorials +TutorialEdit: Edit +TutorialDelete: Delete +TutorialTutorControlled: Tutors may edit tutorial +TutorialTutorControlledTip: Should tutors be allowed to edit arbitrary aspects of this tutorial (name, registration group, room, time, other tutors, ...) at will? + +CourseExams: Exams +CourseTutorials: Tutorials + +ParticipantsN n: #{n} #{pluralEN n "participant" "participants"} +TutorialDeleteQuestion: Do you really want to delete the tutorial listed below? +TutorialDeleted: Tutorial deleted + +TutorialRegisteredSuccess tutn: Successfully registered for the tutorial #{tutn} +TutorialDeregisteredSuccess tutn: Successfully de-registered for the tutorial #{tutn} + +TutorialNameTip: Needs to be unique within the course +TutorialCapacityNonPositive: Capacity may not be negative +TutorialCapacityTip: Limits how many course participants may register for this tutorial +TutorialRegGroupTip: Course participants may only register for a maximum of one tutorial per registration group. Tutorials that do not have a registration group are treated as being in different registration groups +TutorialRoomPlaceholder: Room +TutorialTutors: Tutors +TutorialTutorAlreadyAdded: An user with this email address is already registered as tutor + +OccurrenceNoneScheduled: No regular occurrences (yet) +OccurrenceNoneExceptions: No exceptions (yet) + +TutorialNew: New tutorial + +TutorialNameTaken tutn: A tutorial named #{tutn} already exists +TutorialCreated tutn: Successfully created tutorial #{tutn} +TutorialEdited tutn: Successfully edited tutorial #{tutn} + +TutorialEditHeading tutn: Edit #{tutn} + +MassInputTip: You may specify multiple values. Values must be added to the list by clicking + and can be removed again by clicking -. All changes must be confirmed by clicking the form submit button. + +HealthReport: Health report +InstanceIdentification: Instance identification + +InstanceId: Instance id +ClusterId: Cluster id + +HealthMatchingClusterConfig: Cluster config matches +HealthHTTPReachable: Cluster can be reached under the expected URL via HTTP +HealthLDAPAdmins: Proportion of administrators that were found in the LDAP directory +HealthSMTPConnect: SMTP server is reachable +HealthWidgetMemcached: Memcached server is serving widgets correctly +HealthActiveJobExecutors: Proportion of job workers accepting new jobs + +CourseParticipantsHeading: Course participants +CourseParticipantsCount n: #{n} +CourseParticipantsCountOf n m: #{n} of #{m} +CourseParticipants n: Currently #{n} course #{pluralEN n "participant" "participants"} +CourseParticipantsInvited n: #{n} #{pluralEN n "invitation" "invitations"} sent via email +CourseParticipantsAlreadyRegistered n: #{n} #{pluralEN n "participant is" "participants are"} already enrolled +CourseParticipantsRegisteredWithoutField n: #{n} #{pluralEN n "participant was" "participants were"} registered without #{pluralEN n "an associated field of study" "associated fields of study"}, because #{pluralEN n "it" "they"} could not be determined uniquely. +CourseParticipantsRegistered n: Successfully registered #{n} #{pluralEN n "participant" "participants"} +CourseParticipantsRegisterHeading: Add course participants + +ExamRegistrationAndCourseParticipantsRegistered n: Registered #{n} #{pluralEN n "participant" "participants"} for the exam as well as for the course +ExamRegistrationNotRegisteredWithoutCourse n: #{n} #{pluralEN n "user" "users"} were not registered for the exam since they are not enrolled in the course +ExamRegistrationRegisteredWithoutField n: Registered #{n} #{pluralEN n "participant" "participants"} for the exam as well as for the course. The #{pluralEN n "participant was" "participants were"} enrolled without #{pluralEN n "an associated field of study" "associated fields of study"} since #{pluralEN n "it" "they"} could not be determined uniquely. +ExamRegistrationParticipantsRegistered n: #{n} #{pluralEN n "participant was" "participants were"} registered for the exam +ExamRegistrationInviteDeadline: Invitation valid until +ExamRegistrationEnlistDirectly: Register known users directly +ExamRegistrationEnlistDirectlyTip: Should users whose email addresses are known to the system be registered for the exam directly? Otherwise invitations will be sent to alle users, which they will have to accept first in order to be registered. Unknown users always receive an invitation. +ExamRegistrationRegisterCourse: Also enroll users in course +ExamRegistrationRegisterCourseTip: Users that aren't enrolled already won't be registered for the exam otherwise. +ExamRegistrationInviteField: Email addresses +ExamParticipantsRegisterHeading: Add exam participants +ExamParticipantsInvited n: #{n} #{pluralEN n "invitation" "invitations"} sent via email + +ExamName: Name +ExamTime: Time +ExamsHeading: Exams +ExamNameTip: Needs to be unique within the course +ExamStart: Start +ExamEnd: End +ExamDescription: Description +ExamVisibleFrom: Visible from +ExamVisibleFromTip: If left empty the exam is never visible and course participants may not register. +ExamRegisterFrom: Register from +ExamRegisterFromTip: Start of the period in which course participants may register themselves for the exam. If left empty participants are never allowed to register. +ExamRegisterTo: Register to +ExamDeregisterUntil: Deregister until +ExamPublishOccurrenceAssignments: Publish occurrence/room-assignments +ExamPublishOccurrenceAssignmentsTip: At this time participants are informed to which occurrence/room they are assigned +ExamPublishOccurrenceAssignmentsParticipant: Occurrence/room-assignments published +ExamFinished: Marking finished +ExamFinishedOffice: Exam achievements published +ExamFinishedParticipant: Marking expected to be finished +ExamFinishedTip: At this participants are informed of their exam achievements +ExamClosed: Exam achievements registered +ExamClosedTip: At this time exam offices, which pull exam achievements from Uni2work, are informed. Changes to exam achievements trigger further notifications +ExamShowGrades: Exam is graded +ExamShowGradesTip: Should participants and relevant exam offices be show exact grades or only whether the exam was passed or failed? +ExamPublicStatistics: Publish statistics +ExamPublicStatisticsTip: Should automatically computed statistics also be shown to participants as soon as they are informed of their achievements? +ExamAutomaticGrading: Automatically compute grades +ExamAutomaticGradingTip: Should the exam achievement be automatically computed from the results of the exam parts? Bonus points are considered if configured. Manually overriding the computed exam achievements remains possible. +ExamGradingRule: Grade computation +ExamGradingManual': No automatic computation +ExamGradingKey': By grading scale +ExamGradingKey: Grading scale +ExamGradingKeyTip: Values in the grading scale refer to the effective maximum number of points. Bonus points from exercises are added and results for exam parts are multiplied with the part's weight. +Points: Points +PointsMustBeNonNegative: Point boundaries may not be negative +PointsMustBeMonotonic: Point boundaries must increase monotonically +GradingFrom: From +ExamNew: New Exam +ExamBonus: Bonus point system +ExamBonusRule: Bonus points from exercises +ExamNoBonus': No automatic exam bonus +ExamBonusPoints': Compute from exercise achievements +ExamBonusManual': Manual computation + +ExamBonusAchieved: Bonus points + +ExamEditHeading examn: Edit #{examn} + +ExamBonusMaxPoints: Maximum exam bonus points +ExamBonusMaxPointsTip: Bonus points are linearly interpolated from the number of exercise points achieved (or exercise sheets passed as applicable) between zero and the given bound. +ExamBonusMaxPointsNonPositive: Maximum exam bonus points must be positive and greater than zero +ExamBonusOnlyPassed: Apply bonus only if already passed +ExamBonusRound: Round bonus to +ExamBonusRoundNonPositive: Rounding multiple must be positive and greater than zero +ExamBonusRoundTip: Bonus points are rounded commercially to a multiple of the given number + +ExamAutomaticOccurrenceAssignment: Automatically assign occurrence/room +ExamAutomaticOccurrenceAssignmentTip: Should exam participants be distributed automatically among the configured occurrences/rooms? Manipulation of the distribution and manually assigning participants remains possible. +ExamOccurrenceRule: Procedure +ExamOccurrenceRuleParticipant: Occurrence/room assignment procedure +ExamRoomManual': No automatic assignment +ExamRoomSurname': By surname +ExamRoomMatriculation': By matriculation +ExamRoomRandom': Randomly + +ExamOccurrence: Occurrence/room +ExamNoOccurrence: No occurrence/room +ExamOccurrences: Exams +ExamRooms: Rooms +ExamRoomAlreadyExists: Occurrence already configured +ExamRoomName: Internal name +ExamRoom: Room +ExamRoomCapacity: Capacity +ExamRoomCapacityNegative: Capacity may not be negative +ExamRoomTime: Time +ExamRoomStart: Start +ExamRoomEnd: End +ExamRoomDescription: Description +ExamTimeTip: Only for informational purposes. The actual times are set for each occurrence/room +ExamRoomRegistered: Assigned + +ExamOccurrenceStart: Exam starts + +ExamFormTimes: Times +ExamFormOccurrences: Occurrences/rooms +ExamFormAutomaticFunctions: Automatic functions +ExamFormCorrection: Correction +ExamFormParts: Exam parts + +ExamCorrectors: Correctors +ExamCorrectorAlreadyAdded: A corrector with this email address already exists + +ExamParts: Exam parts/questions +ExamPartWeightNegative: Weight of all exam parts must be greater than or equal to zero +ExamPartAlreadyExists: An exam part of this name already exists +ExamPartNumber: Number +ExamPartNumbered examPartNumber: Part #{view _ExamPartNumber examPartNumber} +ExamPartNumberTip: Will be used as an internal name e.g. during CSV-export +ExamPartName: Title +ExamPartNameTip: Will be shown to participants +ExamPartMaxPoints: Maximum points +ExamPartWeight: Weight +ExamPartWeightTip: Will be multiplied with the achieved number of points before they are shown to the participant or used in automatic grade computation. Thus this also affects existing exam results (changed exam achievements have to be accepted manually again) +ExamPartResultPoints: Achieved points + +ExamNameTaken exam: There already is an exam named #{exam} +ExamCreated exam: Successfully created #{exam} +ExamEdited exam: Successfully edited #{exam} + +ExamNoShow: Not present +ExamVoided: Voided + +ExamBonusManualParticipants: Manually computed by course administrators +ExamBonusPoints possible: Up to #{showFixed True possible} exam points +ExamBonusPointsPassed possible: Up to #{showFixed True possible} exam points applied only if the exam would also be passed without + +ExamPassed: Passed +ExamNotPassed: Failed +ExamResult: Exam result + +ExamRegisteredSuccess exam: Successfully registered for the exam #{exam} +ExamDeregisteredSuccess exam: Successufly deregistered from the exam #{exam} +ExamRegistered: Registered for the exam +ExamNotRegistered: Not registered for the exam +ExamRegistration: Exam registration + +ExamRegisterToMustBeAfterRegisterFrom: "Register to" must be after "register from" +ExamDeregisterUntilMustBeAfterRegisterFrom: "Deregister until" must be after "register from" +ExamStartMustBeAfterPublishOccurrenceAssignments: "Start" must be after "publish occurrence/room-assignments" +ExamEndMustBeAfterStart: "End" must be after "start" +ExamFinishedMustBeAfterEnd: "Marking finished" must be after "end" +ExamFinishedMustBeAfterStart: "Marking finished" must be after "start" +ExamClosedMustBeAfterFinished: "Exam achievements registered" must be after "marking finished" +ExamClosedMustBeAfterStart: "Exam achievements registered" must be after "start" +ExamClosedMustBeAfterEnd: "Exam achievements registered" must be after "end" + +ExamOccurrenceEndMustBeAfterStart eoName: End of the occurrence #{eoName} must be after it's start +ExamOccurrenceStartMustBeAfterExamStart eoName: Start of the occurrence #{eoName} must be after the exam start +ExamOccurrenceEndMustBeBeforeExamEnd eoName: End of the occurrence #{eoName} must be before the exam end +ExamOccurrenceDuplicate eoRoom eoRange: Combination of room #{eoRoom} and occurrence #{eoRange} occurs multiple times +ExamOccurrenceDuplicateName eoName: Internal name #{eoName} occurs multiple times + +VersionHistory: Version history +KnownBugs: Known bugs +ImplementationDetails: Implementation + +ExamSynchronised: Synchronised + +ExamUsersHeading: Exam participants +ExamUserDeregister: Deregister participants +ExamUserAssignOccurrence: Assign occurrence/room +ExamUserAcceptComputedResult: Accept computed result +ExamUserResetToComputedResult: Reset result +ExamUserResetBonus: Also reset exam bonus +ExamUsersDeregistered count: Successfully deregistered #{show count} #{pluralEN count "participant" "participants"} +ExamUsersOccurrenceUpdated count: Successfully assigned occurrence/room for #{show count} #{pluralEN count "participant" "participants"} +ExamUsersResultsAccepted count: Successfully accepted computed result for #{show count} #{pluralEN count "participant" "participants"} +ExamUsersResultsReset count: Successfully reset result for #{show count} #{pluralEN count "participant" "participants"} + +ExamUserSynchronised: Synchronised +ExamUserSyncOfficeName: Name +ExamUserSyncTime: Timestamp +ExamUserSyncSchools: Department +ExamUserSyncLastChange: Last changed +ExamUserMarkSynchronised: Mark exam achievements as synchronised + +ExamUserMarkSynchronisedCsv: Mark exam achievements as synchronised while exporting +ExamUserMarkedSynchronised n: Successfully marked #{n} #{pluralEN n "exam achievement" "exam achievements"} as synchronised + +ExamOfficeExamUsersHeading: Exam achievements + +CsvFile: CSV file +CsvImport: CSV import +CsvExport: CSV export +CsvModifyExisting: Modify existing entries +CsvAddNew: Add new entries +CsvDeleteMissing: Delete missing entries +BtnCsvExport: Export CSV file +BtnCsvImport: Import CSV file +BtnCsvImportConfirm: Finalise CSV import + +CsvImportNotConfigured: CSV import not configured +CsvImportConfirmationHeading: CSV import preview (no changes have been made yet) +CsvImportConfirmationTip: No changes have been made yet! Importing this CSV file corresponds to performing the edits listed below. Please choose the edits that should be performed before finalising the import. +CsvImportUnnecessary: Importing the given CSV file does not correspond to performing any edits +CsvImportSuccessful n: Successfully imported CSV file. #{n} #{pluralEN n "edit" "edits"} have been performed. +CsvImportAborted: CSV import aborted +CsvImportExplanationLabel: Informating regarding CSV import + +Proportion c of prop: #{c}/#{of} (#{rationalToFixed2 (100 * prop)}%) + +CourseUserCsvName tid ssh csh: #{foldCase (termToText (unTermKey tid))}-#{foldedCase (unSchoolKey ssh)}-#{foldedCase csh}-participants +ExamUserCsvName tid ssh csh examn: #{foldCase (termToText (unTermKey tid))}-#{foldedCase (unSchoolKey ssh)}-#{foldedCase csh}-#{foldedCase examn}-participants +CourseApplicationsTableCsvName tid ssh csh: #{foldCase (termToText (unTermKey tid))}-#{foldedCase (unSchoolKey ssh)}-#{foldedCase csh}-applications + +CsvColumnsExplanationsLabel: Column & cell format +CsvColumnsExplanationsTip: Meaning and format of the columns contained in imported and exported CSV files +CsvColumnExamUserSurname: Participant's surname +CsvColumnExamUserFirstName: Participant's given name +CsvColumnExamUserName: Participant's full name (usually includes surname and given name) +CsvColumnExamUserMatriculation: Participant's matriculation +CsvColumnExamUserField: Field of study the participant specified when enrolling for the course +CsvColumnExamUserDegree: Degree the participant pursues in their associated field of study +CsvColumnExamUserSemester: Semester the participant is in wrt. to their associated field of study +CsvColumnExamUserOccurrence: Occurrence/room the participant has been assigned +CsvColumnExamUserExercisePoints: Number of points the participant has achieved in the excercise +CsvColumnExamUserExercisePointsMax: Maximum number of points the participant could have achieved up to their assigned exam occurrence +CsvColumnExamUserExercisePasses: Number of exercise sheets the participant has passed +CsvColumnExamUserExercisePassesMax: Maximum number of exercise sheets the participant could have passed up to their assigned exam occurrence +CsvColumnExamUserBonus: Exam bonus points +CsvColumnExamUserParts: Number of points the participant achieved per exam part. One column per exam part if applicable. +CsvColumnExamUserResult: Exam achievement; "passed", "failed", "no-show", "voided", or any number grade ("1.0", "1.3", "1.7", ..., "4.0", "5.0") +CsvColumnExamUserCourseNote: Course notes for the participant + +CsvColumnUserName: Participant's full name +CsvColumnUserMatriculation: Participant's matriculation +CsvColumnUserSex: Participant's sex +CsvColumnUserEmail: Participant's email address +CsvColumnUserStudyFeatures: All active fields of study for the participant, separated by semicolon (;) +CsvColumnUserField: Field of study the participant specified when enrolling for the course +CsvColumnUserDegree: Degree the participant pursues in their associated field of study +CsvColumnUserSemester: Semester the participant is in wrt. to their associated field of study +CsvColumnUserRegistration: Time of participant's enrollment (ISO 8601) +CsvColumnUserNote: Course notes for the participant +CsvColumnUserTutorial: Tutorials which the user is registered for, separated by semicolon (;). For each registration group among the tutorials there is a separate column. The registration group columns contain at most one tutorial per participant. If every tutorial has a registration group there is no column "tutorial". + +CsvColumnExamOfficeExamUserOccurrenceStart: Exam occurrence (ISO 8601) + +CsvColumnApplicationsAllocation: Central allocation for which this application was made +CsvColumnApplicationsApplication: Globally unique number of application (for matching with the ZIP archive of all application files) +CsvColumnApplicationsName: Participant's full name +CsvColumnApplicationsMatriculation: Participant's matriculation +CsvColumnApplicationsField: Field of study the participant specified when applying +CsvColumnApplicationsDegree: Degree the participant pursues in their associated field of study +CsvColumnApplicationsSemester: Semester the participant is in wrt. to their associated field of study +CsvColumnApplicationsText: Application text +CsvColumnApplicationsHasFiles: Did the applicant provide any additional files with their application (see ZIP archive of all application files)? +CsvColumnApplicationsVeto: Vetoed applicants are never assigned to the course; "veto" or empty +CsvColumnApplicationsRating: Application grading; Any number grade ("1.0", "1.3", "1.7", ..., "4.0", "5.0"); Empty cells will be treated as if they contained a grade between 2.3 and 2.7 +CsvColumnApplicationsComment: Application comment; depending on course settings this might purely be a note for course administrators or be feedback for the applicant + +Action: Action +ActionNoUsersSelected: No users selected + +DBCsvDuplicateKey: Two rows in the CSV file reference the same database entry and are thus invalid +DBCsvDuplicateKeyTip: Please remove one of the lines listed below and try again. +DBCsvKeyException: For a row in the CSV file it could not be determined whether it references any database entry. +DBCsvException: An error occurred hile computing the set of edits this CSV import corresponds to. + +ExamUserCsvCourseRegister: Register users for the exam and enroll them in the course +ExamUserCsvRegister: Register users for the exam +ExamUserCsvAssignOccurrence: Assign occurrences/rooms to participants +ExamUserCsvDeregister: Deregister participants from the exam +ExamUserCsvSetCourseField: Modify field of study associated with the course +ExamUserCsvOverrideBonus: Override bonus points in contradiction of computed value +ExamUserCsvOverrideResult: Override exam result in contradiction of computed value +ExamUserCsvSetBonus: Set bonus points +ExamUserCsvSetResult: Set exam result +ExamUserCsvSetPartResult: Set result for exam part +ExamUserCsvSetCourseNote: Modify course participant notes +ExamBonusNone: No bonus points + +ExamUserCsvCourseNoteDeleted: Course note will be deleted + +ExamUserCsvExceptionNoMatchingUser: Course participant could not be identified uniquely +ExamUserCsvExceptionNoMatchingStudyFeatures: The specified field did not match with any of the participant's fields of study +ExamUserCsvExceptionNoMatchingOccurrence: Occurrence/room could not be identified uniquely + +CourseApplicationsTableCsvSetField: Modify field of study associated with the applicatio +CourseApplicationsTableCsvSetVeto: Set/remove veto +CourseApplicationsTableCsvSetRating: Set grading +CourseApplicationsTableCsvSetComment: Set comment + +CourseApplicationsTableCsvExceptionNoMatchingUser: Applicant could not be identified uniquely +CourseApplicationsTableCsvExceptionNoMatchingAllocation: Central allocation could not be identified uniquely +CourseApplicationsTableCsvExceptionNoMatchingStudyFeatures: The specified field did not match with any of the participant's fields of study + +TableHeadingFilter: Filter +TableHeadingCsvImport: CSV import +TableHeadingCsvExport: CSV export + +ExamResultAttended: Attended +ExamResultNoShow: Not present +ExamResultVoided: Voided +ExamResultNone: No exam result + +BtnAuthLDAP: Change to campus account +BtnAuthPWHash: Change to Uni2work accont +BtnPasswordReset: Reset password + +AuthLDAPLookupFailed: User could not be looked up due to a LDAP error +AuthLDAPInvalidLookup: Existing user could not be uniquely matched with a LDAP entry +AuthLDAPAlreadyConfigured: User already logs in using their campus account +AuthLDAPConfigured: User now logs in using their campus account + +AuthPWHashAlreadyConfigured: User already logs in using their Uni2work account +AuthPWHashConfigured: User now logs in using their Uni2work account + +PasswordResetQueued: Sent link to reset password +ResetPassword: Reselt Uni2work password + +AuthMode: Authentication +AuthLDAP: Campus +AuthPWHash pwHash: Uni2work +CurrentPassword: Current password +NewPassword: New password +NewPasswordRepeat: New password (again) +CurrentPasswordInvalid: Current password is incorrect +PasswordRepeatInvalid: New passwords do not match +UserPasswordHeadingFor: Change password for +PasswordChangedSuccess: Successfully changed password + +FunctionaryInviteFunction: Function +FunctionaryInviteSchool: Department +FunctionaryInviteField: Email addresses to invite +FunctionaryInviteHeading: Add department functionaries + +FunctionariesInvited n: Invited #{n} #{pluralEN n "functionary" "functionaries"} via email +FunctionariesAdded n: Added #{n} #{pluralEN n "functionary" "functionaries"} + +MailSubjectSchoolFunctionInvitation school renderedFunction: Invitation to be #{renderedFunction} for “#{school}” +MailSchoolFunctionInviteHeading school renderedFunction: Invitation to be #{renderedFunction} for “#{school}” +SchoolFunctionInviteExplanation renderedFunction: You were invited to act as #{renderedFunction} for a department. By accepting the invitation you are granted elevated rights within the department. +SchoolFunctionInvitationAccepted school renderedFunction: Successfully accepted invitation to be #{renderedFunction} for “#{school}” + +AllocationActive: Active +AllocationName: Name +AllocationAvailableCourses: Courses +AllocationApplication: Application +AllocationAppliedCourses: Applications +AllocationNumCoursesAvailableApplied available applied: You have applied for #{applied}/#{available} #{pluralEN applied "course" "courses"} +AllocationTitle termText ssh' allocation: #{termText} - #{ssh'}: #{allocation} +AllocationShortTitle termText ssh' ash: #{termText} - #{ssh'} - #{ash} +AllocationSchool: Department +AllocationSemester: Semester +AllocationDescription: Description +AllocationStaffDescription: Staff description +AllocationStaffRegisterFrom: Registration of courses starts +AllocationStaffRegister: Registration of courses +AllocationRegisterFrom: Application period start +AllocationRegister: Application period +AllocationRegisterClosed: This central allocation is currently closed. +AllocationRegisterOpensIn difftime: This central allocation is expected to open in #{difftime} +AllocationStaffAllocationFrom: Grading of applications starts +AllocationStaffAllocation: Grading of applications +AllocationRegisterByStaff: Enrollment by course administrators +AllocationRegisterByStaffFrom: Enrollment by course administrators starts +AllocationRegisterByStaffTip: In this periods course administrators may enroll participants in their courses. +AllocationRegisterByStaffFromTip: Starting at this time course administrators may enroll participants in their courses. +AllocationRegisterByCourseFrom: Direct enrollment starts +AllocationRegisterByCourseFromTip: Starting at this time course administrators participating in this central allocation may open their courses for participants to manage their participation themselves. +AllocationOverrideDeregister: Leaving courses only until +AllocationProcess: Allocation process +AllocationNoApplication: No application +AllocationPriority: Priority +AllocationPriorityTip: Courses to which you assign a higher priority are preferred during the allocation process. +AllocationPriorityRelative: The absolute priority values are meaningless. The only consideration is whether one course has a higher priority than another. +AllocationTotalCoursesNegative: Requested number of placements must be greater than zero +AllocationTotalCourses: Requested number of placements +AllocationTotalCoursesTip: During the allocation process you will be placed in at most as many courses as specified +AllocationRegistered: Successfully registered participation in this central allocation +AllocationRegistrationEdited: Successfully edited registration for this central allocation +BtnAllocationRegister: Register participation +BtnAllocationRegistrationEdit: Edit registration +AllocationParticipation: Your participation in this central allocation +AllocationParticipationLoginFirst: To participate in this central allocation, please log in first +AllocationCourses: Centrally allocated courses +AllocationData: Organisation +AllocationCoursePriority i: #{ordinalEN i} +AllocationCourseNoApplication: No application +BtnAllocationApply: Apply +BtnAllocationApplicationEdit: Edit application +BtnAllocationApplicationRetract: Retract application +BtnAllocationApplicationRate: Grade application +ApplicationPriority: Priority +ApplicationVeto: Veto +ApplicationVetoTip: Vetoed applicants will not be assigned to the course +ApplicationRatingPoints: Grading +ApplicationRatingPointsTip: Applicants graded 5.0 will not be assigned to the course +ApplicationRatingComment: Comment +ApplicationRatingCommentVisibleTip: Feedback for the applicant +ApplicationRatingCommentInvisibleTip: Currently only a note for course administrators +ApplicationRatingSection: Grading +ApplicationRatingSectionSelfTip: You are authorised to edit the application as well as it's grading. + +AllocationSchoolShort: Department +Allocation: Central allocation +AllocationRegisterTo: Registration until + +AllocationListTitle: Central allocations + +CourseApplicationsListTitle: Applications +CourseApplicationId: Application number +CourseApplicationRatingPoints: Grading +CourseApplicationVeto: Veto +CourseApplicationNoVeto: No veto +CourseApplicationNoRatingPoints: No grading +CourseApplicationNoRatingComment: No comment + +UserDisplayName: Display name +UserDisplayNameInvalid: Display name does not comply with specification +UserDisplayNameRules: Specification for display names +UserDisplayNameRulesBelow: Specifications of what can be a display name can be found below +UserMatriculation: Matriculation + +UserDisplayEmail: Display email +UserDisplayEmailTip: This email address may be displayed publicly alongside your display name. Notifications and other communication from Uni2work or users with elevated permissions are always sent to your primary email address as specified under "personal information". +UserDisplayEmailChangeSent displayEmail: Instructions to change your display email have been sent to “#{displayEmail}”. + +SchoolShort: Shorthand +SchoolName: Name +SchoolLdapOrganisations: Associated LDAP fragments +SchoolLdapOrganisationsTip: When logging in users are associated with any departments whose associated LDAP fragments are found in the users LDAP entry + +SchoolUpdated ssh: Successfully edited #{ssh} +SchoolTitle ssh: Department „#{ssh}“ +TitleSchoolNew: Neues Institut anlegen +SchoolCreated ssh: Successfully created #{ssh} +SchoolExists ssh: A department named „#{ssh}“ already exists + +SchoolAdmin: Admin +SchoolLecturer: Lecturer +SchoolEvaluation: Course evaluation +SchoolExamOffice: Exam office + +ApplicationEditTip: During the application period you may edit and retract your applications at will. + +UserLdapSync: Synchronise with LDAP +AllUsersLdapSync: Synchronise all with LDAP +SynchroniseLdapUserQueued n: Triggered LDAP synchronisation of #{n} #{pluralEN n "user" "users"}. +SynchroniseLdapAllUsersQueued: Triggered LDAP synchronisation of all users +UserHijack: Hijack session + +MailSubjectAllocationStaffRegister allocation: You may now register courses for the central allocation “#{allocation}” +MailAllocationStaffRegisterNewCourse: You can create new courses on the page linked below. While doing so you now have to option to specifiy that the course should participate in the central allocation. +MailAllocationStaffRegisterDeadline deadline: Please consider that alle courses must be registered until #{deadline} in order to participate in the central allocation. + +MailSubjectAllocationRegister allocation: You may now apply for the central allocation “#{allocation}” +MailAllocationRegister: You may now apply for each individual course particpating in the central allocation on the page linked below. +MailAllocationRegisterDeadline deadline: Please consider that all applications have to be made until #{deadline}. + +MailSubjectAllocationAllocation allocation: You may now grade applications made to your courses for the central allocation “#{allocation}” +MailAllocationAllocation: On the page linked below you may now grade applications made to your corses for the central allocation. The grades you specify will be considered during the allocation process. +MailAllocationApplicationsMayChange deadline: Please consider that students may continue to apply, retract, and edit their applications until #{deadline}. Applications that change after being graded will need to be graded again. +MailAllocationAllocationDeadline deadline: Please consider that grading of applications is only possible until #{deadline}. + +MailSubjectAllocationUnratedApplications allocation: Some applications made to your courses for the central allocation “#{allocation}” are not yet graded +MailAllocationUnratedApplications: Applications have been made to the courses listed below for the central allocation, which have not yet been graded. + +MailSubjectAllocationOutdatedRatings allocation: Applications made to your courses for the central allocation “#{allocation}” have changed since being graded +MailAllocationOutdatedRatings: Applications have been made to the courses list below for the central allocation, which have changed since they were last graded. +MailAllocationOutdatedRatingsWarning: Applications whose grading is deprecated (i.e. that have been changed since they were graded) are considered not to have been graded during the allocation process. + +ExamOfficeSubscribedUsers: Users +ExamOfficeSubscribedUsersTip: You may specify multiple matriculations; comma-separated + +ExamOfficeSubscribedUsersExplanation: You will be able to view all exam achievements (with no regard for the students fields of study) for all users specified here. +ExamOfficeSubscribedFieldsExplanation: You will be able to view all exam achievements for any user that has at least one of the specified fields of study. You may additionally configure whether users should be allowed to opt out on a course by course basis. + +UserMatriculationNotFound matriculation@Text: Es existiert kein Uni2work-Benutzer mit Matrikelnummer „#{matriculation}“ +UserMatriculationAmbiguous matriculation@Text: Matrikelnummer „#{matriculation}“ ist nicht eindeutig + +TransactionExamOfficeUsersUpdated nDeleted@Int nAdded@Int: #{nAdded} Benutzer hinzugefügt, #{nDeleted} Benutzer gelöscht + +TransactionExamOfficeFieldsUpdated nUpdates@Int: #{nUpdates} #{pluralDE nUpdates "Studienfach" "Studienfächer"} angepasst +ExamOfficeFieldNotSubscribed: — +ExamOfficeFieldSubscribed: Einsicht +ExamOfficeFieldForced: Forcierte Einsicht +InvalidExamOfficeFieldMode parseErr@Text: Konnte „#{parseErr}“ nicht interpretieren + +LdapIdentification: Campus account +LdapIdentificationOrEmail: Campus account/email address +AdminUserTitle: Title +AdminUserFirstName: Given name +AdminUserSurname: Surname +AdminUserDisplayName: Display name +AdminUserEmail: Email address +AdminUserDisplayEmail: Display email +AdminUserIdent: Identification +AdminUserAuth: Authentication +AdminUserMatriculation: Matriculation +AdminUserSex: Sex +AuthKindLDAP: Campus account +AuthKindPWHash: Uni2work account +UserAdded: Successfully added user +UserCollision: Could not create user due to uniqueness constraint + +CourseAllocationsBounds n: Expected number of alloctions due to #{pluralEN n "central allocation" "central allocations"} +CourseAllocationsBoundCoincide numFirstChoice: Est. #{numFirstChoice} #{pluralEN numFirstChoice "participant" "participants"} +CourseAllocationsBound numApps numFirstChoice: Est. between #{numFirstChoice} and #{numApps} #{pluralEN numApps "participant" "participants"} +CourseAllocationsBoundCapped: The numbers listed above were modified based on the currently configured course capacity. +CourseAllocationsBoundWarningOpen: The information listed above represents only the current state of applications and is subject to change. + +BtnSetDisplayEmail: Set email address +UserDisplayEmailChanged: Successfully set display email +TitleChangeUserDisplayEmail: Set display email + +MailSubjectChangeUserDisplayEmail: Publishing this email address in Uni2work +MailIntroChangeUserDisplayEmail displayEmail: The user mentioned above wants to publish “#{displayEmail}” as their own email address. If you have not caused this email to be sent, please ignore it! +MailTitleChangeUserDisplayEmail displayName: #{displayName} wants to publish this email address as their own in Uni2work + +ExamOfficeOptOutsChanged: Successfully adjusted relevant exam offices + +BtnCloseExam: Close exam +ExamCloseTip: When an exam is closed all relevant exam offices, which pull exam achievements from Uni2work, are informed and kept up to date with changes. +ExamCloseReminder: Please close the exam as soon as possible, when exam achievements are no longer expected to change e.g. after inspection of the exam has concluced. +ExamDidClose: Successfully closed exam + +ExamClosedSince time: Exam closed since #{time} + +LecturerInfoTooltipNew: New feature +LecturerInfoTooltipProblem: Feature with known issues +LecturerInfoTooltipPlanned: Planned feature +LecturerInfoTooltipNewU2W: Unlike UniWorX + +BtnAcceptApplications: Accept applications +BtnAcceptApplicationsTip: By clicking the button below you may fill the course with applicants (only up to the maximum capacity if configured). Grading of applications will be considered (no grading is treated as if graded between 2.3 and 2.7). Vetoed applicants and applications graded 5.0 will not be enrolled. +AcceptApplicationsMode: Accept applications +AcceptApplicationsModeTip: Should accepted applications be enrolled in the course directly or should invitations be sent via email? +AcceptApplicationsDirect: Enroll directly +AcceptApplicationsInvite: Send invitations +AcceptApplicationsSecondary: Breaking ties +AcceptApplicationsSecondaryTip: If a tie occurs during the acceptance process, how should it be broken? +AcceptApplicationsSecondaryRandom: Randomly +AcceptApplicationsSecondaryTime: By time of application + +CsvOptions: CSV options +CsvOptionsTip: These settings primarily affect CSV export. During import most settings can be determined automatically. CSV import expects the same character encoding as used for export. +CsvFormatOptions: File format +CsvTimestamp: Timestamp +CsvTimestampTip: Should the name of every exported csv file contain a timestamp? +CsvPresetRFC: Standards-compliant (RFC 4180) +CsvPresetExcel: Excel compatible +CsvCustom: User defined +CsvDelimiter: Separator character +CsvUseCrLf: Linebreaks +CsvQuoting: Quoting +CsvQuotingTip: When should quotation characters (") be placed around fields so characters contained within will not be interpreted as field separators? +CsvEncoding: Encoding +CsvEncodingTip: CSV files can be exported in a different character encoding than the UTF-8 used by default. Please consider that non-UTF-8 character encodings might lead to encoding problems with special characters. +CsvUTF8: UTF-8 (Unicode) +CsvCP1252: Windows CP-1252 ("ANSI") +CsvDelimiterNull: Null byte +CsvDelimiterTab: Tab +CsvDelimiterComma: Comma +CsvDelimiterColon: Colon +CsvDelimiterSemicolon: Semicolon +CsvDelimiterBar: Vertical bar +CsvDelimiterSpace: Space +CsvDelimiterUnitSep: Unit separator character +CsvCrLf: DOS (CRLF) +CsvLf: Unix (LF) +CsvQuoteNone: Never +CsvQuoteMinimal: Only when necessary +CsvQuoteAll: Always +CsvOptionsUpdated: Successfully changed CSV options +CsvChangeOptionsLabel: Export options + +CourseNews: News +CourseNewsArchiveName tid ssh csh newsTitle: #{foldCase (termToText (unTermKey tid))}-#{foldedCase (unSchoolKey ssh)}-#{foldedCase csh}-#{foldCase newsTitle} +CourseNewsFiles: Files +CourseNewsLastEdited time: Last changed: #{time} +CourseNewsActionEdit: Edit +CourseNewsActionDelete: Delete +CourseNewsActionCreate: Create new item +CourseMaterial: Material +CourseMaterialFree: Course material is publicly accessable +CourseMaterialNotFree: Only course participants may access course material + +CourseNewsVisibleFromEditWarning: This item of course news has already been published and should no longer be changed sind this might confuse participants. +CourseNewsVisibleFromTip: If left empty this item is never visible. Leave empty for unfinished items +CourseNewsTitle: Title +CourseNewsSummary: Summary +CourseNewsSummaryTip: If specified this only the summary will be shown on the course page, saving space. The content will be shown in a popup +CourseNewsContent: Content +CourseNewsParticipantsOnly: Only for course participants +CourseNewsVisibleFrom: Visible from +CourseNewsCreated: Successfully created item of course news +CourseNewsEdited: Successfully edited item of course news +CourseNewsDeleteQuestion: Are you sure you want to delete the item of course news listed below? +CourseNewsDeleted: Successfully deleted item of course news + +CourseDeregistrationAllocationLog: Your enrollment in this course is due to a central allocation. If you leave the course, this will be permanently recorded and might affect you negatively in future central allocations. If you have good reasons why you should not be held accountable for leaving the course, please contact a course administrator. Course administrators can deregister you without incurring a permanent record. +CourseDeregistrationAllocationReason: Reason +CourseDeregistrationAllocationReasonTip: The specified reason will be permanently stored and might be the only information available during conflict resolution +CourseDeregistrationAllocationShouldLog: Self imposed +CourseDeregistrationAllocationShouldLogTip: If the participant was enrolled in this course due to a central allocation, it is intended that a permanent record be made that might affect the student negatively in future central allocations. As a course administrator you have the right to prevent this if the participant can present good reasons why them leaving the course is not self imposed. + +MailSubjectAllocationResults allocation: Placements have been made for the central allocation “#{allocation}” +AllocationResultsLecturer: In the course of the central allocations placements have been made as follows: +AllocationResultLecturer csh count count2: #{count} #{pluralEN count "participant" "participants"} (of #{count2}) for #{csh} +AllocationResultLecturerAll csh count: #{count} #{pluralEN count "participant" "participants"} for #{csh} +AllocationResultLecturerNone csh: No participants for #{csh} +AllocationResultsStudent: You have been placed in: +AllocationNoResultsStudent: Unfortunately you were not placed in any courses. +AllocationResultStudent csh: You were placed in #{csh}. +AllocationResultsTip: The following information reflect the current state of the allocation and are subject to change (e.g. when handling succession). You will be informed separately if any future changes concern you. +AllocationResultsStudentTip: Listed below are placements in courses which you have received due to the mentioned central allocation and for which you have not left the respective course or have been deregistered. Thus placements you have been informed of already may be listed again. +AllocationResultStudentRegistrationTip: You were enrolled in the course mentioned above in Uni2work. +AllocationResultsStudentRegistrationTip: You were enrolled in the courses mentioned above in Uni2work. + +FavouriteVisited: Visited +FavouriteParticipant: Your courses +FavouriteManual: Favourites +FavouriteCurrent: Current course + +CourseEvents: Occurrences +CourseEventType: Type +CourseEventTypePlaceholder: Lecture, Exercise discussion, ... +CourseEventTime: Time +CourseEventRoom: Regular room +CourseEventActions: Actions +CourseEventsActionEdit: Edit +CourseEventsActionDelete: Delete +CourseEventsActionCreate: New occurrence +CourseEventCreated: Successfully created course occurrence +CourseEventEdited: Successfully edited course occurrence +CourseEventDeleteQuestion: Are you sure you want to delete the course occurrence mentioned below? +CourseEventDeleted: Successfully deleted course occurrence + +UserSimplifiedFeaturesOfStudyCsv: Simplified features of study +UserSimplifiedFeaturesOfStudyCsvTip: Should field of study, degree, and semester be exported in separate columns for ease of processing? If so only the field of study associated by the user with their course registration will be exported. + +Sex: Sex +SexNotKnown: Unknown +SexMale: Male +SexFemale: Female +SexNotApplicable: Not applicable + +ShortSexNotKnown: unk. +ShortSexMale: m +ShortSexFemale: f +ShortSexNotApplicable: N/A + +ShowSex: Show sex of other users +ShowSexTip: Should users' sex be displayed in (among others) lists of course participants? + +StudySubTermsParentKey: Parent +StudyTermsDefaultDegree: Default degree +StudyTermsDefaultFieldType: Default type + +MenuLanguage: Language +LanguageChanged: Language changed successfully + +ProfileCorrector: Corrector +ProfileCourses: Own courses +ProfileCourseParticipations: Course registrations +ProfileCourseExamResults: Exam achievements +ProfileTutorials: Own tutorials +ProfileTutorialParticipations: Tutorials +ProfileSubmissionGroups: Submission groups +ProfileSubmissions: Submissions +ProfileRemark: Remarks +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. +ProfileCorrections: List of all assigned corrections + +GroupSizeNotNatural: “Maximum group size” needs to be a natural number +AmbiguousEmail: Email address is ambiguous +CourseDescriptionPlaceholder: Please include the module description +CourseHomepageExternalPlaceholder: Optional external URL +PointsPlaceholder: Points +RFC1766: RFC1766 language code + +TermShort: Shorthand +TermCourseCount: Courses +TermStart: Semester start +TermEnd: Semester end +TermStartMustMatchName: Shorthand number does not match semester start. +TermEndMustBeAfterStart: Semester end may not be before semester start. +TermLectureEndMustBeAfterStart: Lecture start may not be after lecture end. +TermStartMustBeBeforeLectureStart: Semester start must be before lecture start. +TermEndMustBeAfterLectureEnd: Lecture end must be before semester end. +AdminPageEmpty: This page shall provide an overview for administrators in the future. For now there are only links to important administrator-functions. +HaveCorrectorAccess sheetName: You have corrector access to #{original sheetName}. +FavouritesPlaceholder: Number of favourites +FavouritesNotNatural: Number of favourites must be a natural number! +FavouritesSemestersPlaceholder: Number of semesters +FavouritesSemestersNotNatural: Maximum number of semesters in favourites bar must be a natural number! + +ProfileTitle: Settings + +GlossaryTitle: Glossary +MenuGlossary: Glossary + +Applicant: Applicant +CourseParticipant: Course participant +Administrator: Administrator +CsvFormat: CSV format +ExerciseSheet: Exercise sheet +DefinitionCourseEvents: Course occurrences +DefinitionCourseNews: Course news +Invitations: Invitations +SheetSubmission: Sheet submission +CommCourse: Course message +CommTutorial: Tutorial message +Clone: Cloning +Deficit: Deficit + +MetricNoSamples: No samples +MetricName: Name +MetricValue: Value \ No newline at end of file diff --git a/models/files.model b/models/files.model index f96745687..2ea0569ef 100644 --- a/models/files.model +++ b/models/files.model @@ -6,3 +6,9 @@ File content ByteString Maybe -- Nothing iff this is a directory modified UTCTime deriving Show Eq Generic + +SessionFile + user UserId + reference SessionFileReference + file FileId + touched UTCTime \ No newline at end of file diff --git a/models/jobs.model b/models/jobs.model index 06be9fbeb..49a21a6e1 100644 --- a/models/jobs.model +++ b/models/jobs.model @@ -16,3 +16,10 @@ CronLastExec time UTCTime -- When was the job executed instance InstanceId -- Which uni2work-instance did the work UniqueCronLastExec job + + +SentNotification + content Value + user UserId + time UTCTime + instance InstanceId \ No newline at end of file diff --git a/models/sheets.model b/models/sheets.model index 138b50bf1..fcd2cadc4 100644 --- a/models/sheets.model +++ b/models/sheets.model @@ -6,8 +6,8 @@ Sheet -- exercise sheet for a given course grouping SheetGroup -- May participants submit in groups of certain sizes? markingText Html Maybe -- Instructons for correctors, included in marking templates visibleFrom UTCTime Maybe -- Invisible to enrolled participants before - activeFrom UTCTime -- Download of questions and submission is permitted afterwards - activeTo UTCTime -- Submission is only permitted before + activeFrom UTCTime Maybe -- Download of questions and submission is permitted afterwards + activeTo UTCTime Maybe -- Submission is only permitted before hintFrom UTCTime Maybe -- Additional files are made available solutionFrom UTCTime Maybe -- Solution is made available submissionMode SubmissionMode -- Submission upload by students and/or through tutors? diff --git a/models/users.model b/models/users.model index 93453ac1b..4210cd5ac 100644 --- a/models/users.model +++ b/models/users.model @@ -28,7 +28,7 @@ User json -- Each Uni2work user has a corresponding row in this table; create dateFormat DateTimeFormat "default='%d.%m.%Y'" -- preferred Date-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) - mailLanguages MailLanguages "default='[]'::jsonb" -- Preferred language for eMail; i18n not yet implemented; user-defined + languages Languages Maybe -- Preferred language; user-defined notificationSettings NotificationSettings -- Bit-array for which events email notifications are requested by user; user-defined warningDays NominalDiffTime default=1209600 -- timedistance to pending deadlines for homepage infos csvOptions CsvOptions "default='{}'::jsonb" @@ -102,4 +102,13 @@ StudySubTermParentCandidate StudyTermStandaloneCandidate incidence TermCandidateIncidence key Int - deriving Show Eq Ord \ No newline at end of file + deriving Show Eq Ord + +UserGroupMember + group UserGroupName + user UserId + primary Checkmark nullable + + UniquePrimaryUserGroupMember group primary !force + UniqueUserGroupMember group user + diff --git a/package-lock.json b/package-lock.json index 05990c127..a5d734d8f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "uni2work", - "version": "7.19.2", + "version": "7.25.1", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 2803e4270..603a648a8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "uni2work", - "version": "7.19.2", + "version": "7.25.1", "description": "", "keywords": [], "author": "", @@ -42,10 +42,10 @@ "scripts": { "postbump": "./sync-versions.hs && git add -- package.yaml" }, - "commitUrlFormat": "https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/{{hash}}", - "compareUrlFormat": "https://gitlab.cip.ifi.lmu.de/jost/UniWorX/compare/{{previousTag}}...{{currentTag}}", - "issueUrlFormat": "https://gitlab.cip.ifi.lmu.de/jost/UniWorX/issues/{{id}}", - "userUrlFormat": "https://gitlab.cip.ifi.lmu.de/{{user}}" + "commitUrlFormat": "https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/{{hash}}", + "compareUrlFormat": "https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/{{previousTag}}...{{currentTag}}", + "issueUrlFormat": "https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/issues/{{id}}", + "userUrlFormat": "https://gitlab2.rz.ifi.lmu.de/{{user}}" }, "browserslist": [ "defaults" diff --git a/package.yaml b/package.yaml index 8a382e645..c5339dc30 100644 --- a/package.yaml +++ b/package.yaml @@ -1,5 +1,5 @@ name: uniworx -version: 7.19.2 +version: 7.25.1 dependencies: - base >=4.9.1.0 && <5 @@ -140,6 +140,10 @@ dependencies: - retry - generic-lens - array + - cookie + - prometheus-client + - prometheus-metrics-ghc + - wai-middleware-prometheus other-extensions: - GeneralizedNewtypeDeriving @@ -233,10 +237,7 @@ executables: uniworx: main: main.hs source-dirs: app - ghc-options: - - -threaded - - -rtsopts - - -with-rtsopts=-N + ghc-options: -threaded -rtsopts "-with-rtsopts=-N -T" dependencies: - uniworx when: diff --git a/routes b/routes index 582834521..469ec0297 100644 --- a/routes +++ b/routes @@ -40,7 +40,8 @@ /auth AuthR Auth getAuth !free /favicon.ico FaviconR GET !free -/robots.txt RobotsR GET !free +/robots.txt RobotsR GET !free +/metrics MetricsR GET / HomeR GET !free /users UsersR GET POST -- no tags, i.e. admins only @@ -63,6 +64,7 @@ /info/lecturer InfoLecturerR GET !lecturer /info/data DataProtR GET !free /info/allocation InfoAllocationR GET !free +/info/glossary GlossaryR GET !free /impressum ImpressumR GET !free /version VersionR GET !free @@ -73,6 +75,7 @@ /user/authpreds AuthPredsR GET POST !free /user/set-display-email SetDisplayEmailR GET POST !free /user/csv-options CsvOptionsR GET POST !free +/user/lang LangR POST !free /exam-office ExamOfficeR !exam-office: / EOExamsR GET @@ -141,7 +144,6 @@ /invite SInviteR GET POST !ownerANDtimeANDuser-submissions !/#SubmissionFileType SubArchiveR GET !owner !corrector !/#SubmissionFileType/*FilePath SubDownloadR GET !owner !corrector - /correctors SCorrR GET POST /iscorrector SIsCorrR GET !corrector -- Route is used to check for corrector access to this sheet /pseudonym SPseudonymR GET POST !course-registeredANDcorrector-submissions /corrector-invite/ SCorrInviteR GET POST diff --git a/src/Application.hs b/src/Application.hs index 41ca6fed4..2da92e313 100644 --- a/src/Application.hs +++ b/src/Application.hs @@ -22,6 +22,8 @@ import Database.Persist.Postgresql (createPostgresqlPool, pgConnStr, import Import hiding (cancel) import Language.Haskell.TH.Syntax (qLocation) import Network.Wai (Middleware) +import qualified Network.Wai as Wai +import qualified Network.HTTP.Types as HTTP import Network.Wai.Handler.Warp (Settings, defaultSettings, defaultShouldDisplayException, runSettings, runSettingsSocket, setHost, @@ -48,6 +50,7 @@ import System.Directory import Jobs import qualified Data.Text.Encoding as Text + import Yesod.Auth.Util.PasswordStore import qualified Data.ByteString.Lazy as LBS @@ -81,12 +84,20 @@ import Network.Socket (socketPort, Socket, PortNumber) import qualified Network.Socket as Socket (close) import Control.Concurrent.STM.Delay -import Control.Monad.STM (retry) import Control.Monad.Trans.Cont (runContT, callCC) import qualified Data.Set as Set -import Data.Semigroup (Max(..), Min(..)) +import Data.Semigroup (Min(..)) + +import qualified Prometheus.Metric.GHC as Prometheus +import qualified Prometheus + +import Data.Time.Clock.POSIX + +import Handler.Utils.Routes (classifyHandler) + +import Data.List (cycle) -- Import all relevant handler modules here. -- (HPack takes care to add new modules to our cabal file nowadays.) @@ -111,6 +122,7 @@ import Handler.Health import Handler.Exam import Handler.Allocation import Handler.ExamOffice +import Handler.Metrics -- This line actually creates our YesodDispatch instance. It is the second half @@ -124,6 +136,8 @@ mkYesodDispatch "UniWorX" resourcesUniWorX -- migrations handled by Yesod. makeFoundation :: (MonadResource m, MonadUnliftIO m) => AppSettings -> m UniWorX makeFoundation appSettings'@AppSettings{..} = do + void $ Prometheus.register Prometheus.ghcMetrics + -- Some basic initializations: HTTP connection manager, logger, and static -- subsite. appHttpManager <- newManager @@ -290,7 +304,39 @@ makeApplication foundation = liftIO $ do logWare <- makeLogWare foundation -- Create the WAI application and apply middlewares appPlain <- toWaiAppPlain foundation - return $ logWare $ defaultMiddlewaresNoLogging appPlain + return . prometheusMiddleware . logWare $ defaultMiddlewaresNoLogging appPlain + where + prometheusMiddleware :: Middleware + prometheusMiddleware app req respond' = do + start <- getPOSIXTime + app req $ \res -> do + end <- getPOSIXTime + let method = decodeUtf8 $ Wai.requestMethod req + status = tshow . HTTP.statusCode $ Wai.responseStatus res + route :: Maybe (Route UniWorX) + route = parseRoute ( Wai.pathInfo req + , over (mapped . _2) (fromMaybe "") . HTTP.queryToQueryText $ Wai.queryString req + ) + handler' = pack . classifyHandler <$> route + + labels :: Prometheus.Label3 + labels = (fromMaybe "n/a" handler', method, status) + Prometheus.withLabel requestLatency labels . flip Prometheus.observe . realToFrac $ end - start + + respond' res + +{-# NOINLINE requestLatency #-} +requestLatency :: Prometheus.Vector Prometheus.Label3 Prometheus.Histogram +requestLatency = Prometheus.unsafeRegister + $ Prometheus.vector ("handler", "method", "status") + $ Prometheus.histogram info buckets + where info = Prometheus.Info "http_request_duration_seconds" + "HTTP request latency" + buckets = map fromRational . takeWhile (<= 500) . go 50e-6 $ cycle [2, 2, 2.5] + where + go n [] = [n] + go n (f:fs) = n : go (f * n) fs + makeLogWare :: MonadIO m => UniWorX -> m Middleware makeLogWare app = do @@ -320,13 +366,22 @@ makeLogWare app = do logWare <- either mkLogWare return lookupRes logWare wai req fin +data ReadySince = MkReadySince + -- | Warp settings for the given foundation value. warpSettings :: UniWorX -> Settings warpSettings foundation = defaultSettings & setBeforeMainLoop (runAppLoggingT foundation $ do let notifyReady = do $logInfoS "setup" "Ready" - void $ liftIO Systemd.notifyReady + void . liftIO $ do + void . Prometheus.register . readyMetric =<< getCurrentTime + Systemd.notifyReady + readyMetric ts = Prometheus.Metric $ return (MkReadySince, collectReadySince) + where + collectReadySince = return [Prometheus.SampleGroup info Prometheus.GaugeType [Prometheus.Sample "ready_time" [] sample]] + info = Prometheus.Info "ready_time" "POSIXTime this Uni2work-instance became ready" + sample = encodeUtf8 . tshow . (realToFrac :: POSIXTime -> Nano) $ utcTimeToPOSIXSeconds ts if | foundation ^. _appHealthCheckDelayNotify -> void . forkIO $ do @@ -425,39 +480,46 @@ appMain = runResourceT $ do case watchdogMicroSec of Just wInterval | maybe True (== myProcessID) watchdogProcess - -> let notifyWatchdog :: IO () + -> let notifyWatchdog :: forall a. IO a notifyWatchdog = runAppLoggingT foundation $ go Nothing where - go pStatus = do - d <- liftIO . newDelay . floor $ wInterval % 2 + go :: Maybe (Set (UTCTime, HealthReport)) -> LoggingT IO a + go pResults = do + let delay = floor $ wInterval % 2 + d <- liftIO $ newDelay delay - status <- atomically $ asum - [ Nothing <$ waitDelay d - , Just <$> do + $logDebugS "Notify" $ "Waiting up to " <> tshow delay <> "µs..." + mResults <- atomically $ asum + [ pResults <$ waitDelay d + , do results <- readTVar $ foundation ^. _appHealthReport - case fromNullable results of - Nothing -> retry - Just rs -> do - let status = ofoldMap1 (Max *** Min . healthReportStatus) rs - guard $ pStatus /= Just status - return status + guardOn (pResults /= Just results) $ Just results ] - case status of - Just (_, Min status') -> do - $logInfoS "NotifyStatus" $ toPathPiece status' - liftIO . void . Systemd.notifyStatus . unpack $ toPathPiece status' - Nothing -> return () + $logDebugS "Notify" "Checking for status/watchdog..." + (*> go mResults) . void . runMaybeT $ do + results <- hoistMaybe mResults - case status of - Just (_, Min HealthSuccess) -> do - $logInfoS "NotifyWatchdog" "Notify" - liftIO $ void Systemd.notifyWatchdog - _other -> return () + Min status <- hoistMaybe $ ofoldMap1 (Min . healthReportStatus . view _2) <$> fromNullable results + $logInfoS "NotifyStatus" $ toPathPiece status + liftIO . void . Systemd.notifyStatus . unpack $ toPathPiece status - go status - in void $ allocateLinkedAsync notifyWatchdog - _other -> return () + now <- liftIO getCurrentTime + iforM_ (foundation ^. _appHealthCheckInterval) . curry $ \case + (_, Nothing) -> return () + (hc, Just interval) -> do + lastSuccess <- hoistMaybe $ results + & Set.filter (\(_, rep) -> classifyHealthReport rep == hc) + & Set.filter (\(_, rep) -> healthReportStatus rep >= HealthSuccess) + & Set.mapMonotonic (view _1) + & Set.lookupMax + guard $ lastSuccess > addUTCTime (negate interval) now + $logInfoS "NotifyWatchdog" "Notify" + liftIO $ void Systemd.notifyWatchdog + in do + $logDebugS "Notify" "Spawning notify thread..." + void $ allocateLinkedAsync notifyWatchdog + _other -> $logWarnS "Notify" "Not sending notifications of status/poking watchdog" let runWarp socket = runSettingsSocket (warpSettings foundation) socket app case sockets of diff --git a/src/Auth/Dummy.hs b/src/Auth/Dummy.hs index 4bfc09d01..90f3e3cee 100644 --- a/src/Auth/Dummy.hs +++ b/src/Auth/Dummy.hs @@ -13,6 +13,7 @@ import qualified Data.CaseInsensitive as CI data DummyMessage = MsgDummyIdent + | MsgDummyIdentPlaceholder | MsgDummyNoFormData deriving (Eq, Ord, Enum, Bounded, Read, Show, Generic, Typeable) @@ -24,7 +25,9 @@ dummyForm :: ( RenderMessage (HandlerSite m) FormMessage , Button (HandlerSite m) ButtonSubmit , MonadHandler m ) => AForm m (CI Text) -dummyForm = areq (ciField & addDatalist userList) (fslI MsgDummyIdent & noAutocomplete) Nothing +dummyForm = wFormToAForm $ do + mr <- getMessageRender + aFormToWForm $ areq (ciField & addDatalist userList) (fslpI MsgDummyIdent (mr MsgDummyIdentPlaceholder) & noAutocomplete & addName PostLoginDummy) Nothing where userList = fmap mkOptionList . runDB $ withReaderT projectBackend (map toOption <$> selectList [] [Asc UserIdent] :: ReaderT SqlBackend _ [Option UserIdent]) toOption (Entity _ User{..}) = Option userDisplayName userIdent (CI.original userIdent) diff --git a/src/Auth/LDAP.hs b/src/Auth/LDAP.hs index bc6a3b686..5acb12e95 100644 --- a/src/Auth/LDAP.hs +++ b/src/Auth/LDAP.hs @@ -36,6 +36,7 @@ data CampusLogin = CampusLogin data CampusMessage = MsgCampusIdentPlaceholder | MsgCampusIdent | MsgCampusPassword + | MsgCampusPasswordPlaceholder | MsgCampusSubmit | MsgCampusInvalidCredentials deriving (Eq, Ord, Enum, Bounded, Read, Show, Generic, Typeable) @@ -130,7 +131,7 @@ campusForm = do MsgRenderer mr <- getMsgRenderer ident <- wreq ciField (fslpI MsgCampusIdent (mr MsgCampusIdentPlaceholder) & addAttr "autofocus" "") Nothing - password <- wreq passwordField (fslI MsgCampusPassword) Nothing + password <- wreq passwordField (fslpI MsgCampusPassword (mr MsgCampusPasswordPlaceholder)) Nothing return $ CampusLogin <$> ident diff --git a/src/Auth/PWHash.hs b/src/Auth/PWHash.hs index b2194bf90..3fd716694 100644 --- a/src/Auth/PWHash.hs +++ b/src/Auth/PWHash.hs @@ -22,7 +22,9 @@ data HashLogin = HashLogin } deriving (Generic, Typeable) data PWHashMessage = MsgPWHashIdent + | MsgPWHashIdentPlaceholder | MsgPWHashPassword + | MsgPWHashPasswordPlaceholder deriving (Eq, Ord, Enum, Bounded, Read, Show, Generic, Typeable) @@ -30,9 +32,11 @@ hashForm :: ( RenderMessage (HandlerSite m) FormMessage , RenderMessage (HandlerSite m) PWHashMessage , MonadHandler m ) => AForm m HashLogin -hashForm = HashLogin - <$> areq ciField (fslpI MsgPWHashIdent "Identifikation") Nothing - <*> areq passwordField (fslpI MsgPWHashPassword "Passwort") Nothing +hashForm = wFormToAForm $ do + mr <- getMessageRender + aFormToWForm $ HashLogin + <$> areq ciField (fslpI MsgPWHashIdent (mr MsgPWHashIdentPlaceholder)) Nothing + <*> areq passwordField (fslpI MsgPWHashPassword (mr MsgPWHashPasswordPlaceholder)) Nothing hashLogin :: forall site. diff --git a/src/Cron.hs b/src/Cron.hs index 5017e71d1..36a063321 100644 --- a/src/Cron.hs +++ b/src/Cron.hs @@ -155,7 +155,7 @@ nextCronMatch :: TZ -- ^ Timezone of the `Cron`-Entry -> UTCTime -- ^ Current time, used only for `CronCalendar` -> Cron -> CronNextMatch UTCTime -nextCronMatch tz mPrev prec now c@Cron{..} = case notAfter of +nextCronMatch tz mPrev prec now c@Cron{..} = onlyOnceWithinPrec $ case notAfter of MatchAsap -> MatchNone MatchAt ts | MatchAt ts' <- nextMatch @@ -165,6 +165,16 @@ nextCronMatch tz mPrev prec now c@Cron{..} = case notAfter of | otherwise -> MatchNone MatchNone -> nextMatch where + onlyOnceWithinPrec sched = case mPrev of + Nothing -> sched + Just prevT -> case sched of + MatchAsap + | now >= addUTCTime prec prevT -> MatchAsap + | otherwise -> MatchAt $ addUTCTime prec prevT + MatchAt ts -> let ts' = max ts $ addUTCTime prec prevT + in if | ts' <= addUTCTime prec now -> MatchAsap + | otherwise -> MatchAt ts' + MatchNone -> MatchNone notAfter | Right c' <- cronNotAfter , Just ref <- notAfterRef diff --git a/src/Data/Universe/Instances/Reverse/WithIndex.hs b/src/Data/Universe/Instances/Reverse/WithIndex.hs new file mode 100644 index 000000000..ff6550058 --- /dev/null +++ b/src/Data/Universe/Instances/Reverse/WithIndex.hs @@ -0,0 +1,20 @@ +{-# OPTIONS_GHC -fno-warn-orphans #-} + +module Data.Universe.Instances.Reverse.WithIndex + ( + ) where + +import ClassyPrelude + +import Data.Universe +import Control.Lens.Indexed + +import Data.Universe.Instances.Reverse () + +import qualified Data.Map as Map + + +instance Finite a => FoldableWithIndex a ((->) a) where + ifoldMap f g = fold [ f x (g x) | x <- universeF ] +instance (Ord a, Finite a) => TraversableWithIndex a ((->) a) where + itraverse f g = (Map.!) . Map.fromList <$> sequenceA [ (x, ) <$> f x (g x) | x <- universeF ] diff --git a/src/Database/Esqueleto/Utils.hs b/src/Database/Esqueleto/Utils.hs index 2cdfc69d2..b9cc734bb 100644 --- a/src/Database/Esqueleto/Utils.hs +++ b/src/Database/Esqueleto/Utils.hs @@ -19,6 +19,7 @@ module Database.Esqueleto.Utils , sha256 , maybe , SqlProject(..) + , (->.) , module Database.Esqueleto.Utils.TH ) where @@ -246,3 +247,6 @@ instance (PersistEntity val, PersistField typ) => SqlProject val typ (E.Entity v instance (PersistEntity val, PersistField typ) => SqlProject val typ (Maybe (E.Entity val)) (Maybe typ) where sqlProject = (E.?.) unSqlProject _ _ = Just + +(->.) :: E.SqlExpr (E.Value a) -> Text -> E.SqlExpr (E.Value b) +(->.) expr t = E.unsafeSqlBinOp "->" expr $ E.val t diff --git a/src/Foundation.hs b/src/Foundation.hs index 8fce558c2..12b181aa1 100644 --- a/src/Foundation.hs +++ b/src/Foundation.hs @@ -2,23 +2,26 @@ {-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# OPTIONS_GHC -fno-warn-orphans #-} -- MonadCrypto -module Foundation where +module Foundation + ( module Foundation + ) where + +import Foundation.Type as Foundation +import Foundation.I18n as Foundation +import Foundation.Routes as Foundation + import Import.NoFoundation hiding (embedFile) -import Database.Persist.Sql (ConnectionPool, runSqlPool) +import Database.Persist.Sql (runSqlPool) import Text.Hamlet (hamletFile) -import qualified Web.ClientSession as ClientSession - import Yesod.Auth.Message import Auth.LDAP import Auth.PWHash import Auth.Dummy -import Jobs.Types import qualified Network.Wai as W (pathInfo) -import Yesod.Core.Types (Logger) import qualified Yesod.Core.Unsafe as Unsafe import Data.CaseInsensitive (original, mk) @@ -43,9 +46,11 @@ import Data.Map (Map, (!?)) import qualified Data.Map as Map import qualified Data.HashSet as HashSet -import Data.List (nubBy, (!!), findIndex) +import Data.List (nubBy, (!!), findIndex, inits) import qualified Data.List as List +import Web.Cookie + import Data.Monoid (Any(..)) import Data.Conduit.List (sourceList) @@ -73,13 +78,11 @@ import Utils.SystemMessage import Text.Shakespeare.Text (st) import Yesod.Form.I18n.German +import Yesod.Form.I18n.English import qualified Yesod.Auth.Message as Auth import qualified Data.Conduit.List as C -import qualified Crypto.Saltine.Core.SecretBox as SecretBox -import qualified Jose.Jwk as Jose - import qualified Database.Memcached.Binary.IO as Memcached import Data.Bits (Bits(zeroBits)) @@ -94,45 +97,6 @@ import qualified Ldap.Client as Ldap import UnliftIO.Pool -type SMTPPool = Pool SMTPConnection - --- infixl 9 :$: --- pattern a :$: b = a b - --- | The foundation datatype for your application. This can be a good place to --- keep settings and values requiring initialization before your application --- starts running, such as database connections. Every handler will have --- access to the data present here. -data UniWorX = UniWorX - { appSettings' :: AppSettings - , appStatic :: EmbeddedStatic -- ^ Settings for static file serving. - , appConnPool :: ConnectionPool -- ^ Database connection pool. - , appSmtpPool :: Maybe SMTPPool - , appLdapPool :: Maybe LdapPool - , appWidgetMemcached :: Maybe Memcached.Connection -- ^ Actually a proper pool - , appHttpManager :: Manager - , appLogger :: (ReleaseKey, TVar Logger) - , appLogSettings :: TVar LogSettings - , appCryptoIDKey :: CryptoIDKey - , appClusterID :: ClusterId - , appInstanceID :: InstanceId - , appJobState :: TMVar JobState - , appSessionKey :: ClientSession.Key - , appSecretBoxKey :: SecretBox.Key - , appJSONWebKeySet :: Jose.JwkSet - , appHealthReport :: TVar (Set (UTCTime, HealthReport)) - } - -makeLenses_ ''UniWorX -instance HasInstanceID UniWorX InstanceId where - instanceID = _appInstanceID -instance HasJSONWebKeySet UniWorX Jose.JwkSet where - jsonWebKeySet = _appJSONWebKeySet -instance HasHttpManager UniWorX Manager where - httpManager = _appHttpManager -instance HasAppSettings UniWorX where - appSettings = _appSettings' - -- This is where we define all of the routes in our application. For a full -- explanation of the syntax, please see: -- http://www.yesodweb.com/book/routing-and-handlers @@ -145,7 +109,7 @@ instance HasAppSettings UniWorX where -- This function also generates the following type synonyms: -- type Handler x = HandlerT UniWorX IO x -- type Widget = WidgetT UniWorX IO () -mkYesodData "UniWorX" $(parseRoutesFile "routes") +mkYesodData "UniWorX" uniworxRoutes deriving instance Generic CourseR deriving instance Generic SheetR @@ -212,103 +176,7 @@ pattern CEventR tid ssh csh nId ptn = CourseR tid ssh csh (CourseEventR nId ptn) -pluralDE :: (Eq a, Num a) - => a -- ^ Count - -> Text -- ^ Singular - -> Text -- ^ Plural - -> Text -pluralDE num singularForm pluralForm - | num == 1 = singularForm - | otherwise = pluralForm - -noneOneMoreDE :: (Eq a, Num a) - => a -- ^ Count - -> Text -- ^ None - -> Text -- ^ Singular - -> Text -- ^ Plural - -> Text -noneOneMoreDE num noneText singularForm pluralForm - | num == 0 = noneText - | num == 1 = singularForm - | otherwise = pluralForm - -noneMoreDE :: (Eq a, Num a) - => a -- ^ Count - -> Text -- ^ None - -> Text -- ^ Some - -> Text -noneMoreDE num noneText someText - | num == 0 = noneText - | otherwise = someText - --- Convenience Type for Messages, since Yesod messages cannot deal with compound type identifiers -type IntMaybe = Maybe Int -type TextList = [Text] - --- | Convenience function for i18n messages definitions -maybeToMessage :: ToMessage m => Text -> Maybe m -> Text -> Text -maybeToMessage _ Nothing _ = mempty -maybeToMessage before (Just x) after = before <> (toMessage x) <> after - --- Messages creates type UniWorXMessage and RenderMessage UniWorX instance -mkMessage "UniWorX" "messages/uniworx" "de" -mkMessageVariant "UniWorX" "Campus" "messages/campus" "de" -mkMessageVariant "UniWorX" "Dummy" "messages/dummy" "de" -mkMessageVariant "UniWorX" "PWHash" "messages/pw-hash" "de" -mkMessageVariant "UniWorX" "Button" "messages/button" "de" -mkMessageVariant "UniWorX" "Frontend" "messages/frontend" "de" - --- This instance is required to use forms. You can modify renderMessage to --- achieve customized and internationalized form validation messages. -instance RenderMessage UniWorX FormMessage where - renderMessage _ _ = germanFormMessage -- TODO - -instance RenderMessage UniWorX TermIdentifier where - renderMessage foundation ls TermIdentifier{..} = case season of - Summer -> renderMessage' $ MsgSummerTerm year - Winter -> renderMessage' $ MsgWinterTerm year - where renderMessage' = renderMessage foundation ls - -newtype ShortTermIdentifier = ShortTermIdentifier TermIdentifier - deriving (Eq, Ord, Read, Show) -instance RenderMessage UniWorX ShortTermIdentifier where - renderMessage foundation ls (ShortTermIdentifier TermIdentifier{..}) = case season of - Summer -> renderMessage' $ MsgSummerTermShort year - Winter -> renderMessage' $ MsgWinterTermShort year - where renderMessage' = renderMessage foundation ls - -instance RenderMessage UniWorX String where - renderMessage f ls str = renderMessage f ls $ Text.pack str - --- TODO: raw number representation; instead, display e.g. 1000 as 1.000 or 1,000 or ... (language-dependent!) -instance RenderMessage UniWorX Int where - renderMessage f ls = renderMessage f ls . tshow -instance RenderMessage UniWorX Int64 where - renderMessage f ls = renderMessage f ls . tshow -instance RenderMessage UniWorX Integer where - renderMessage f ls = renderMessage f ls . tshow -instance RenderMessage UniWorX Natural where - renderMessage f ls = renderMessage f ls . tshow - -instance HasResolution a => RenderMessage UniWorX (Fixed a) where - renderMessage f ls = renderMessage f ls . showFixed True - -instance RenderMessage UniWorX Load where - renderMessage foundation ls = renderMessage foundation ls . \case - (Load {byTutorial=Nothing , byProportion=p}) -> MsgCorByProportionOnly p - (Load {byTutorial=Just True , byProportion=p}) -> MsgCorByProportionIncludingTutorial p - (Load {byTutorial=Just False, byProportion=p}) -> MsgCorByProportionExcludingTutorial p - -newtype MsgLanguage = MsgLanguage Lang - deriving (Eq, Ord, Show, Read) -instance RenderMessage UniWorX MsgLanguage where - renderMessage foundation ls (MsgLanguage lang@(Text.splitOn "-" -> lang')) - | ["de", "DE"] <- lang' = mr MsgGermanGermany - | ("de" : _) <- lang' = mr MsgGerman - | otherwise = lang - where - mr = renderMessage foundation ls - +-- Requires `rendeRoute`, thus cannot currently be moved to Foundation.I18n instance RenderMessage UniWorX (UnsupportedAuthPredicate AuthTag (Route UniWorX)) where renderMessage f ls (UnsupportedAuthPredicate tag route) = mr . MsgUnsupportedAuthPredicate (mr tag) $ Text.intercalate "/" pieces where @@ -316,142 +184,6 @@ instance RenderMessage UniWorX (UnsupportedAuthPredicate AuthTag (Route UniWorX) mr = renderMessage f ls (pieces, _) = renderRoute route -embedRenderMessage ''UniWorX ''MessageStatus ("Message" <>) -embedRenderMessage ''UniWorX ''NotificationTrigger $ ("NotificationTrigger" <>) . concat . drop 1 . splitCamel -embedRenderMessage ''UniWorX ''StudyFieldType id -embedRenderMessage ''UniWorX ''SheetFileType id -embedRenderMessage ''UniWorX ''SubmissionFileType id -embedRenderMessage ''UniWorX ''CorrectorState id -embedRenderMessage ''UniWorX ''RatingException id -embedRenderMessage ''UniWorX ''SubmissionSinkException ("SubmissionSinkException" <>) -embedRenderMessage ''UniWorX ''SheetGrading ("SheetGrading" <>) -embedRenderMessage ''UniWorX ''AuthTag $ ("AuthTag" <>) . concat . drop 1 . splitCamel -embedRenderMessage ''UniWorX ''EncodedSecretBoxException id -embedRenderMessage ''UniWorX ''LecturerType id -embedRenderMessage ''UniWorX ''SubmissionModeDescr - $ let verbMap [_, _, "None"] = "NoSubmissions" - verbMap [_, _, v] = v <> "Submissions" - verbMap _ = error "Invalid number of verbs" - in verbMap . splitCamel -embedRenderMessage ''UniWorX ''UploadModeDescr id -embedRenderMessage ''UniWorX ''SecretJSONFieldException id -embedRenderMessage ''UniWorX ''AFormMessage $ concat . drop 2 . splitCamel -embedRenderMessage ''UniWorX ''SchoolFunction id -embedRenderMessage ''UniWorX ''CsvPreset id -embedRenderMessage ''UniWorX ''Quoting ("Csv" <>) -embedRenderMessage ''UniWorX ''FavouriteReason id -embedRenderMessage ''UniWorX ''Sex id - -embedRenderMessage ''UniWorX ''AuthenticationMode id - -newtype ShortSex = ShortSex Sex -embedRenderMessageVariant ''UniWorX ''ShortSex ("Short" <>) - -newtype SheetTypeHeader = SheetTypeHeader SheetType -embedRenderMessageVariant ''UniWorX ''SheetTypeHeader ("SheetType" <>) - -instance RenderMessage UniWorX SheetType where - renderMessage foundation ls sheetType = case sheetType of - NotGraded -> mr $ SheetTypeHeader NotGraded - other -> mr (grading other) <> ", " <> mr (SheetTypeHeader other) - where - mr :: RenderMessage UniWorX msg => msg -> Text - mr = renderMessage foundation ls - -instance RenderMessage UniWorX StudyDegree where - renderMessage _found _ls StudyDegree{..} = fromMaybe (tshow studyDegreeKey) (studyDegreeName <|> studyDegreeShorthand) - -newtype ShortStudyDegree = ShortStudyDegree StudyDegree - -instance RenderMessage UniWorX ShortStudyDegree where - renderMessage _found _ls (ShortStudyDegree StudyDegree{..}) = fromMaybe (tshow studyDegreeKey) studyDegreeShorthand - -instance RenderMessage UniWorX StudyTerms where - renderMessage _found _ls StudyTerms{..} = fromMaybe (tshow studyTermsKey) (studyTermsName <|> studyTermsShorthand) - -newtype ShortStudyTerms = ShortStudyTerms StudyTerms - -instance RenderMessage UniWorX ShortStudyTerms where - renderMessage _found _ls (ShortStudyTerms StudyTerms{..}) = fromMaybe (tshow studyTermsKey) studyTermsShorthand - -data StudyDegreeTerm = StudyDegreeTerm StudyDegree StudyTerms - -instance RenderMessage UniWorX StudyDegreeTerm where - renderMessage foundation ls (StudyDegreeTerm deg trm) = (mr trm) <> " (" <> (mr $ ShortStudyDegree deg) <> ")" - where - mr :: RenderMessage UniWorX msg => msg -> Text - mr = renderMessage foundation ls - -newtype ShortStudyFieldType = ShortStudyFieldType StudyFieldType -embedRenderMessageVariant ''UniWorX ''ShortStudyFieldType ("Short" <>) - -data StudyDegreeTermType = StudyDegreeTermType StudyDegree StudyTerms StudyFieldType - -instance RenderMessage UniWorX StudyDegreeTermType where - renderMessage foundation ls (StudyDegreeTermType deg trm typ) = (mr trm) <> " (" <> (mr $ ShortStudyDegree deg) <> ", " <> (mr $ ShortStudyFieldType typ) <> ")" - where - mr :: RenderMessage UniWorX msg => msg -> Text - mr = renderMessage foundation ls - -instance RenderMessage UniWorX ExamGrade where - renderMessage _ _ = pack . (showFixed False :: Deci -> String) . fromRational . review numberGrade - -instance RenderMessage UniWorX ExamPassed where - renderMessage foundation ls = \case - ExamPassed True -> mr MsgExamPassed - ExamPassed False -> mr MsgExamNotPassed - where - mr :: RenderMessage UniWorX msg => msg -> Text - mr = renderMessage foundation ls - -instance RenderMessage UniWorX a => RenderMessage UniWorX (ExamResult' a) where - renderMessage foundation ls = \case - ExamAttended{..} -> mr examResult - ExamNoShow -> mr MsgExamResultNoShow - ExamVoided -> mr MsgExamResultVoided - where - mr :: RenderMessage UniWorX msg => msg -> Text - mr = renderMessage foundation ls - -instance RenderMessage UniWorX (Either ExamPassed ExamGrade) where - renderMessage foundation ls = either mr mr - where - mr :: RenderMessage UniWorX msg => msg -> Text - mr = renderMessage foundation ls - --- ToMessage instances for converting raw numbers to Text (no internationalization) - -instance ToMessage Int where - toMessage = tshow -instance ToMessage Int64 where - toMessage = tshow -instance ToMessage Integer where - toMessage = tshow -instance ToMessage Natural where - toMessage = tshow - -instance HasResolution a => ToMessage (Fixed a) where - toMessage = toMessage . showFixed True - --- Do not use toMessage on Rationals and round them automatically. Instead, use rationalToFixed3 (declared in src/Utils.hs) to convert a Rational to Fixed E3! --- instance ToMessage Rational where --- toMessage = toMessage . fromRational' --- where fromRational' = fromRational :: Rational -> Fixed E3 - - -newtype ErrorResponseTitle = ErrorResponseTitle ErrorResponse -embedRenderMessageVariant ''UniWorX ''ErrorResponseTitle ("ErrorResponseTitle" <>) - -newtype UniWorXMessages = UniWorXMessages [SomeMessage UniWorX] - deriving (Generic, Typeable) - deriving newtype (Semigroup, Monoid, IsList) - -instance RenderMessage UniWorX UniWorXMessages where - renderMessage foundation ls (UniWorXMessages msgs) = - intercalate " " $ map (renderMessage foundation ls) msgs - -uniworxMessages :: [UniWorXMessage] -> UniWorXMessages -uniworxMessages = UniWorXMessages . map SomeMessage -- Menus and Favourites data MenuType = NavbarAside | NavbarRight | NavbarSecondary | PageActionPrime | PageActionSecondary | Footer @@ -512,13 +244,13 @@ instance Button UniWorX ButtonSubmit where getTimeLocale' :: [Lang] -> TimeLocale -getTimeLocale' = $(timeLocaleMap [("de", "de_DE.utf8")]) +getTimeLocale' = $(timeLocaleMap [("de-de", "de_DE.utf8"), ("en-GB", "en_GB.utf8")]) appTZ :: TZ appTZ = $(includeSystemTZ "Europe/Berlin") appLanguages :: NonEmpty Lang -appLanguages = "de-DE" :| [] +appLanguages = "de-de-formal" :| ["en-eu"] appLanguagesOpts :: ( MonadHandler m , HandlerSite m ~ UniWorX @@ -534,6 +266,13 @@ appLanguagesOpts = do langOptions = map mkOption $ toList appLanguages return $ mkOptionList langOptions +-- This instance is required to use forms. You can modify renderMessage to +-- achieve customized and internationalized form validation messages. +instance RenderMessage UniWorX FormMessage where + renderMessage _ ls = case lang of + ("en" : _) -> englishFormMessage + _other -> germanFormMessage + where lang = Text.splitOn "-" $ selectLanguage' appLanguages ls instance RenderMessage UniWorX WeekDay where renderMessage _ ls wDay = pack $ map fst (wDays $ getTimeLocale' ls) !! fromEnum wDay @@ -622,7 +361,15 @@ validateToken mAuthId' route' isWrite' token' = $runCachedMemoT $ for4 memo vali validateToken' mAuthId route isWrite BearerToken{..} = lift . exceptT return return $ do guardMExceptT (maybe True (HashSet.member route) tokenRoutes) (unauthorizedI MsgUnauthorizedTokenInvalidRoute) - User{userTokensIssuedAfter} <- maybeMExceptT (unauthorizedI MsgUnauthorizedTokenInvalidAuthority) $ get tokenAuthority + tokenAuthority' <- case tokenAuthority of + Left tVal + | JSON.Success groupName <- JSON.fromJSON tVal -> maybeT (throwError =<< unauthorizedI MsgUnauthorizedTokenInvalidAuthorityGroup) . hoist lift $ do + Entity _ UserGroupMember{..} <- MaybeT . getBy $ UniquePrimaryUserGroupMember groupName Active + return userGroupMemberUser + | otherwise -> throwError =<< unauthorizedI MsgUnauthorizedTokenInvalidAuthorityValue + Right uid -> return uid + + User{userTokensIssuedAfter} <- maybeMExceptT (unauthorizedI MsgUnauthorizedTokenInvalidAuthority) $ get tokenAuthority' guardMExceptT (Just tokenIssuedAt >= userTokensIssuedAfter) (unauthorizedI MsgUnauthorizedTokenExpired) let @@ -632,7 +379,7 @@ validateToken mAuthId' route' isWrite' token' = $runCachedMemoT $ for4 memo vali authorityVal <- do dnf <- either throwM return $ routeAuthTags route - fmap fst . runWriterT $ evalAuthTags (AuthTagActive $ const True) (noTokenAuth dnf) (Just tokenAuthority) route isWrite + fmap fst . runWriterT $ evalAuthTags (AuthTagActive $ const True) (noTokenAuth dnf) (Just tokenAuthority') route isWrite guardExceptT (is _Authorized authorityVal) authorityVal whenIsJust tokenAddAuth $ \addDNF -> do @@ -875,19 +622,19 @@ tagAccessPredicate AuthTime = APDB $ \mAuthId route _ -> case route of cTime <- liftIO getCurrentTime let visible = NTop sheetVisibleFrom <= NTop (Just cTime) - active = sheetActiveFrom <= cTime && cTime <= sheetActiveTo - marking = cTime > sheetActiveTo + active = NTop sheetActiveFrom <= NTop (Just cTime) && NTop (Just cTime) <= NTop sheetActiveTo + marking = NTop (Just cTime) > NTop sheetActiveTo guard visible case subRoute of -- Single Files - SFileR SheetExercise _ -> guard $ sheetActiveFrom <= cTime + SFileR SheetExercise _ -> guard $ NTop sheetActiveFrom <= NTop (Just cTime) SFileR SheetHint _ -> guard $ maybe False (<= cTime) sheetHintFrom SFileR SheetSolution _ -> guard $ maybe False (<= cTime) sheetSolutionFrom SFileR _ _ -> mzero -- Archives of SheetFileType - SZipR SheetExercise -> guard $ sheetActiveFrom <= cTime + SZipR SheetExercise -> guard $ NTop sheetActiveFrom <= NTop (Just cTime) SZipR SheetHint -> guard $ maybe False (<= cTime) sheetHintFrom SZipR SheetSolution -> guard $ maybe False (<= cTime) sheetSolutionFrom SZipR _ -> mzero @@ -1505,6 +1252,15 @@ redirectAccess url = do Authorized -> redirect url _ -> permissionDeniedI MsgUnauthorizedRedirect +redirectAccessWith :: (MonadThrow m, MonadHandler m, HandlerSite m ~ UniWorX) => Status -> Route UniWorX -> m a +redirectAccessWith status url = do + -- must hide URL if not authorized + access <- evalAccess url False + case access of + Authorized -> redirectWith status url + _ -> permissionDeniedI MsgUnauthorizedRedirect + + -- | Verify that the currently logged in user is lecturer or corrector for at least one sheet for the given course evalAccessCorrector :: (MonadThrow m, MonadHandler m, HandlerSite m ~ UniWorX) => TermId -> SchoolId -> CourseShorthand -> m AuthResult @@ -1565,7 +1321,7 @@ instance Yesod UniWorX where -- b) Validates that incoming write requests include that token in either a header or POST parameter. -- To add it, chain it together with the defaultMiddleware: yesodMiddleware = defaultYesodMiddleware . defaultCsrfMiddleware -- For details, see the CSRF documentation in the Yesod.Core.Handler module of the yesod-core package. - yesodMiddleware = headerMessagesMiddleware . defaultYesodMiddleware . normalizeRouteMiddleware . defaultCsrfMiddleware . updateFavouritesMiddleware + yesodMiddleware = languagesMiddleware appLanguages . headerMessagesMiddleware . defaultYesodMiddleware . normalizeRouteMiddleware . defaultCsrfMiddleware . updateFavouritesMiddleware where updateFavouritesMiddleware :: Handler a -> Handler a updateFavouritesMiddleware handler = (*> handler) . runMaybeT $ do @@ -1684,6 +1440,17 @@ instance Yesod UniWorX where makeLogger = readTVarIO . snd . appLogger +langForm :: Form (Lang, Route UniWorX) +langForm csrf = do + lang <- selectLanguage appLanguages + route <- getCurrentRoute + (urlRes, urlView) <- mreq hiddenField ("" & addName ("referer" :: Text)) route + (langBoxRes, langBoxView) <- mreq + (selectField appLanguagesOpts) + ("" & addAttr "multiple" "multiple" & addAttr "size" (tshow . min 10 $ length appLanguages) & addAutosubmit & addName ("lang" :: Text)) + (Just lang) + return ((,) <$> langBoxRes <*> urlRes, toWidget csrf <> fvInput urlView <> fvInput langBoxView) + siteLayoutMsg :: (RenderMessage site msg, site ~ UniWorX) => msg -> Widget -> Handler Html siteLayoutMsg msg widget = do mr <- getMessageRender @@ -1708,7 +1475,21 @@ siteLayout' headingOverride widget = do mcurrentRoute <- getCurrentRoute -- Get the breadcrumbs, as defined in the YesodBreadcrumbs instance. - (title, parents) <- breadcrumbs + let + breadcrumbs' mcRoute = do + mr <- getMessageRender + case mcRoute of + Nothing -> return (mr MsgErrorResponseTitleNotFound, []) + Just cRoute -> do + (title, next) <- breadcrumb cRoute + crumbs <- go [] next + return (title, crumbs) + where + go crumbs Nothing = return crumbs + go crumbs (Just cRoute) = do + (title, next) <- breadcrumb cRoute + go ((cRoute, title) : crumbs) next + (title, parents) <- breadcrumbs' mcurrentRoute -- let isParent :: Route UniWorX -> Bool -- isParent r = r == (fst parents) @@ -1786,6 +1567,13 @@ siteLayout' headingOverride widget = do \authTag -> addMessageWidget Info $ msgModal [whamlet|_{MsgUnauthorizedDisabledTag authTag}|] (Left $ SomeRoute (AuthPredsR, catMaybes [(toPathPiece GetReferer, ) . toPathPiece <$> mcurrentRoute])) getMessages + (langFormView, langFormEnctype) <- generateFormPost $ identifyForm FIDLanguage langForm + let langFormView' = wrapForm langFormView def + { formAction = Just $ SomeRoute LangR + , formSubmit = FormAutoSubmit + , formEncoding = langFormEnctype + } + let highlight :: Route UniWorX -> Bool -- highlight last route in breadcrumbs, favorites taking priority highlight = let crumbs = mcons mcurrentRoute $ fst <$> reverse parents navItems = map (view _2) favourites ++ map (urlRoute . menuItemRoute . view _1) menuTypes @@ -1842,6 +1630,7 @@ siteLayout' headingOverride widget = do let -- See Utils.Frontend.I18n and files in messages/frontend for message definitions frontendI18n = toJSON (mr :: FrontendMessage -> Text) + frontendDatetimeLocale <- toJSON <$> selectLanguage frontendDatetimeLocales pc <- widgetToPageContent $ do -- fonts @@ -1884,136 +1673,261 @@ applySystemMessages = liftHandler . runDB . runConduit $ selectSource [] [] .| C Nothing -> addMessage systemMessageSeverity content -- Define breadcrumbs. +i18nCrumb :: ( RenderMessage (HandlerSite m) msg, MonadHandler m ) + => msg + -> Maybe (Route (HandlerSite m)) + -> m (Text, Maybe (Route (HandlerSite m))) +i18nCrumb msg mbR = do + mr <- getMessageRender + return (mr msg, mbR) + +-- `breadcrumb` _really_ needs to be total for _all_ routes +-- +-- Even if routes are POST only or don't usually use `siteLayout` they will if +-- an error occurs. +-- +-- Keep in mind that Breadcrumbs are also shown by the 403-Handler, +-- i.e. information might be leaked by not performing permission checks if the +-- breadcrumb value depends on sensitive content (like an user's name). instance YesodBreadcrumbs UniWorX where - breadcrumb (AuthR _) = return ("Login" , Just HomeR) - breadcrumb HomeR = return ("Uni2work" , Nothing) - breadcrumb UsersR = return ("Benutzer" , Just AdminR) - breadcrumb AdminUserAddR = return ("Benutzer anlegen", Just UsersR) - breadcrumb (AdminUserR _) = return ("Users" , Just UsersR) - breadcrumb AdminR = return ("Administration", Nothing) - breadcrumb AdminFeaturesR = return ("Test" , Just AdminR) - breadcrumb AdminTestR = return ("Test" , Just AdminR) - breadcrumb AdminErrMsgR = return ("Test" , Just AdminR) - - breadcrumb SchoolListR = return ("Institute" , Just AdminR) - breadcrumb (SchoolR ssh SchoolEditR) = return (original (unSchoolKey ssh), Just SchoolListR) - breadcrumb SchoolNewR = return ("Neu" , Just SchoolListR) + breadcrumb (AuthR _) = i18nCrumb MsgMenuLogin $ Just HomeR + breadcrumb (StaticR _) = i18nCrumb MsgBreadcrumbStatic Nothing + breadcrumb FaviconR = i18nCrumb MsgBreadcrumbFavicon Nothing + breadcrumb RobotsR = i18nCrumb MsgBreadcrumbRobots Nothing + breadcrumb MetricsR = i18nCrumb MsgBreadcrumbMetrics Nothing - breadcrumb (ExamOfficeR EOExamsR) = return ("Prüfungen", Nothing) - breadcrumb (ExamOfficeR EOFieldsR) = return ("Fächer" , Just $ ExamOfficeR EOExamsR) - breadcrumb (ExamOfficeR EOUsersR) = return ("Benutzer" , Just $ ExamOfficeR EOExamsR) - - breadcrumb InfoR = return ("Information" , Nothing) - breadcrumb InfoLecturerR = return ("Veranstalter" , Just InfoR) - breadcrumb DataProtR = return ("Datenschutz" , Just InfoR) - breadcrumb InfoAllocationR = return ("Zentralanmeldungen", Just InfoR) - breadcrumb ImpressumR = return ("Impressum" , Just InfoR) - breadcrumb VersionR = return ("Versionsgeschichte", Just InfoR) - - - breadcrumb HelpR = return ("Hilfe" , Just HomeR) - - - breadcrumb HealthR = return ("Status" , Nothing) - breadcrumb InstanceR = return ("Identifikation", Nothing) - - - breadcrumb ProfileR = return ("Einstellungen" , Just HomeR) - breadcrumb SetDisplayEmailR = return ("Öffentliche E-Mail Adresse", Just ProfileR) - breadcrumb ProfileDataR = return ("Persönliche Daten", Just ProfileR) - breadcrumb AuthPredsR = return ("Authorisierung" , Just ProfileR) - - breadcrumb TermShowR = return ("Semester" , Just HomeR) - breadcrumb TermCurrentR = return ("Aktuell" , Just TermShowR) - breadcrumb TermEditR = return ("Neu" , Just TermCurrentR) - breadcrumb (TermEditExistR tid) = return ("Editieren" , Just $ TermCourseListR tid) - breadcrumb (TermCourseListR (unTermKey -> tid)) = getMessageRender <&> \mr -> (mr $ ShortTermIdentifier tid, Just CourseListR) - - breadcrumb (TermSchoolCourseListR tid ssh) = return (original $ unSchoolKey ssh, Just $ TermCourseListR tid) - - breadcrumb AllocationListR = return ("Zentralanmeldungen", Just HomeR) - breadcrumb (AllocationR tid ssh ash AShowR) = do - mr <- getMessageRender - Entity _ Allocation{allocationName} <- runDB . getBy404 $ TermSchoolAllocationShort tid ssh ash - return ([st|#{allocationName} (#{mr (ShortTermIdentifier (unTermKey tid))}, #{original (unSchoolKey ssh)})|], Just $ AllocationListR) - - breadcrumb CourseListR = return ("Kurse" , Nothing) - breadcrumb CourseNewR = return ("Neu" , Just CourseListR) - breadcrumb (CourseR tid ssh csh CShowR) = return (original csh, Just $ TermSchoolCourseListR tid ssh) - -- (CourseR tid ssh csh CRegisterR) -- is POST only - breadcrumb (CourseR tid ssh csh CEditR) = return ("Editieren" , Just $ CourseR tid ssh csh CShowR) - breadcrumb (CourseR tid ssh csh CUsersR) = return ("Anmeldungen", Just $ CourseR tid ssh csh CShowR) - breadcrumb (CourseR tid ssh csh CAddUserR) = return ("Kursteilnehmer hinzufügen", Just $ CourseR tid ssh csh CUsersR) - breadcrumb (CourseR tid ssh csh CInviteR) = return ("Einladung", Just $ CourseR tid ssh csh CShowR) - breadcrumb (CourseR tid ssh csh CExamOfficeR) = return ("Prüfungsamter", Just $ CourseR tid ssh csh CShowR) - breadcrumb (CourseR tid ssh csh (CUserR cID)) = do + breadcrumb HomeR = i18nCrumb MsgMenuHome Nothing + breadcrumb UsersR = i18nCrumb MsgMenuUsers $ Just AdminR + breadcrumb AdminUserAddR = i18nCrumb MsgMenuUserAdd $ Just UsersR + breadcrumb (AdminUserR cID) = maybeT (i18nCrumb MsgBreadcrumbUser $ Just UsersR) $ do + guardM . hasReadAccessTo $ AdminUserR cID uid <- decrypt cID - User{userDisplayName} <- runDB $ get404 uid - return (userDisplayName, Just $ CourseR tid ssh csh CShowR) - breadcrumb (CourseR tid ssh csh CCorrectionsR) = return ("Abgaben" , Just $ CourseR tid ssh csh CShowR) - breadcrumb (CourseR tid ssh csh CAssignR) = return ("Zuteilung Korrekturen" , Just $ CourseR tid ssh csh CCorrectionsR) - breadcrumb (CourseR tid ssh csh SheetListR) = return ("Übungen" , Just $ CourseR tid ssh csh CShowR) - breadcrumb (CourseR tid ssh csh SheetNewR ) = return ("Neu", Just $ CourseR tid ssh csh SheetListR) - breadcrumb (CourseR tid ssh csh SheetCurrentR) = return ("Aktuelles Blatt", Just $ CourseR tid ssh csh SheetListR) - breadcrumb (CourseR tid ssh csh SheetOldUnassignedR) = return ("Offene Abgaben", Just $ CourseR tid ssh csh SheetListR) - breadcrumb (CourseR tid ssh csh CCommR ) = return ("Kursmitteilung", Just $ CourseR tid ssh csh CShowR) - breadcrumb (CourseR tid ssh csh CTutorialListR) = return ("Tutorien", Just $ CourseR tid ssh csh CShowR) - breadcrumb (CourseR tid ssh csh CTutorialNewR) = return ("Anlegen", Just $ CourseR tid ssh csh CTutorialListR) + User{..} <- MaybeT . runDB $ get uid + return (userDisplayName, Just UsersR) + breadcrumb (AdminUserDeleteR cID) = i18nCrumb MsgBreadcrumbUserDelete . Just $ AdminUserR cID + breadcrumb (AdminHijackUserR cID) = i18nCrumb MsgBreadcrumbUserHijack . Just $ AdminUserR cID + breadcrumb (UserNotificationR cID) = do + mayList <- hasReadAccessTo UsersR + if + | mayList + -> i18nCrumb MsgMenuUserNotifications . Just $ AdminUserR cID + | otherwise + -> i18nCrumb MsgMenuUserNotifications $ Just ProfileR + breadcrumb (UserPasswordR cID) = do + mayList <- hasReadAccessTo UsersR + if + | mayList + -> i18nCrumb MsgMenuUserPassword . Just $ AdminUserR cID + | otherwise + -> i18nCrumb MsgMenuUserPassword $ Just ProfileR + breadcrumb AdminNewFunctionaryInviteR = i18nCrumb MsgMenuLecturerInvite $ Just UsersR + breadcrumb AdminFunctionaryInviteR = i18nCrumb MsgBreadcrumbFunctionaryInvite Nothing - breadcrumb (CourseR tid ssh csh CNewsNewR) = return ("Neue Nachricht", Just $ CourseR tid ssh csh CShowR) - breadcrumb (CNewsR tid ssh csh _ CNShowR) = return ("Kursnachricht" , Just $ CourseR tid ssh csh CShowR) - breadcrumb (CNewsR tid ssh csh cID CNEditR) = return ("Bearbeiten" , Just $ CNewsR tid ssh csh cID CNShowR) - breadcrumb (CNewsR tid ssh csh cID CNDeleteR) = return ("Löschen" , Just $ CNewsR tid ssh csh cID CNShowR) + breadcrumb AdminR = i18nCrumb MsgAdminHeading Nothing + breadcrumb AdminFeaturesR = i18nCrumb MsgAdminFeaturesHeading $ Just AdminR + breadcrumb AdminTestR = i18nCrumb MsgMenuAdminTest $ Just AdminR + breadcrumb AdminErrMsgR = i18nCrumb MsgMenuAdminErrMsg $ Just AdminR + + breadcrumb SchoolListR = i18nCrumb MsgMenuSchoolList $ Just AdminR + breadcrumb (SchoolR ssh SchoolEditR) = maybeT (i18nCrumb MsgBreadcrumbSchool $ Just SchoolListR) $ do + School{..} <- MaybeT . runDB $ get ssh + return (original schoolName, Just SchoolListR) + breadcrumb SchoolNewR = i18nCrumb MsgMenuSchoolNew $ Just SchoolListR - breadcrumb (CourseR tid ssh csh CExamListR) = return ("Prüfungen", Just $ CourseR tid ssh csh CShowR) - breadcrumb (CourseR tid ssh csh CExamNewR) = return ("Anlegen", Just $ CourseR tid ssh csh CExamListR) + breadcrumb (ExamOfficeR EOExamsR) = i18nCrumb MsgMenuExamOfficeExams Nothing + breadcrumb (ExamOfficeR EOFieldsR) = i18nCrumb MsgMenuExamOfficeFields . Just $ ExamOfficeR EOExamsR + breadcrumb (ExamOfficeR EOUsersR) = i18nCrumb MsgMenuExamOfficeUsers . Just $ ExamOfficeR EOExamsR + breadcrumb (ExamOfficeR EOUsersInviteR) = i18nCrumb MsgBreadcrumbExamOfficeUserInvite Nothing - breadcrumb (CourseR tid ssh csh CApplicationsR) = return ("Bewerbungen", Just $ CourseR tid ssh csh CShowR) + breadcrumb InfoR = i18nCrumb MsgMenuInformation Nothing + breadcrumb InfoLecturerR = i18nCrumb MsgInfoLecturerTitle $ Just InfoR + breadcrumb DataProtR = i18nCrumb MsgMenuDataProt $ Just InfoR + breadcrumb InfoAllocationR = i18nCrumb MsgBreadcrumbAllocationInfo $ Just InfoR + breadcrumb ImpressumR = i18nCrumb MsgMenuImpressum $ Just InfoR + breadcrumb VersionR = i18nCrumb MsgMenuVersion $ Just InfoR - breadcrumb (CApplicationR tid ssh csh _ CAEditR) = return ("Bewerbung", Just $ CourseR tid ssh csh CApplicationsR) - breadcrumb (CExamR tid ssh csh examn EShowR) = return (original examn, Just $ CourseR tid ssh csh CExamListR) - breadcrumb (CExamR tid ssh csh examn EEditR) = return ("Bearbeiten", Just $ CExamR tid ssh csh examn EShowR) - breadcrumb (CExamR tid ssh csh examn EUsersR) = return ("Teilnehmer", Just $ CExamR tid ssh csh examn EShowR) - breadcrumb (CExamR tid ssh csh examn EAddUserR) = return ("Prüfungsteilnehmer hinzufügen", Just $ CExamR tid ssh csh examn EUsersR) - breadcrumb (CExamR tid ssh csh examn EGradesR) = return ("Prüfungsleistungen", Just $ CExamR tid ssh csh examn EShowR) + breadcrumb HelpR = i18nCrumb MsgMenuHelp Nothing - breadcrumb (CTutorialR tid ssh csh tutn TUsersR) = return (original tutn, Just $ CourseR tid ssh csh CTutorialListR) - breadcrumb (CTutorialR tid ssh csh tutn TEditR) = return ("Bearbeiten", Just $ CTutorialR tid ssh csh tutn TUsersR) - breadcrumb (CTutorialR tid ssh csh tutn TDeleteR) = return ("Löschen", Just $ CTutorialR tid ssh csh tutn TUsersR) - breadcrumb (CTutorialR tid ssh csh tutn TCommR) = return ("Mitteilung", Just $ CTutorialR tid ssh csh tutn TUsersR) - breadcrumb (CSheetR tid ssh csh shn SShowR) = return (original shn, Just $ CourseR tid ssh csh SheetListR) - breadcrumb (CSheetR tid ssh csh shn SEditR) = return ("Bearbeiten" , Just $ CSheetR tid ssh csh shn SShowR) - breadcrumb (CSheetR tid ssh csh shn SDelR ) = return ("Löschen" , Just $ CSheetR tid ssh csh shn SShowR) - breadcrumb (CSheetR tid ssh csh shn SSubsR) = return ("Abgaben" , Just $ CSheetR tid ssh csh shn SShowR) - breadcrumb (CSheetR tid ssh csh shn SAssignR) = return ("Zuteilung Korrekturen" , Just $ CSheetR tid ssh csh shn SSubsR) - breadcrumb (CSheetR tid ssh csh shn SubmissionNewR) = return ("Abgabe", Just $ CSheetR tid ssh csh shn SShowR) - breadcrumb (CSheetR tid ssh csh shn SubmissionOwnR) = return ("Abgabe", Just $ CSheetR tid ssh csh shn SShowR) - breadcrumb (CSubmissionR tid ssh csh shn _ SubShowR) = return ("Abgabe", Just $ CSheetR tid ssh csh shn SShowR) --- (CSubmissionR tid ssh csh shn _ SubArchiveR) -- just for Download - breadcrumb (CSubmissionR tid ssh csh shn cid CorrectionR) = return ("Korrektur", Just $ CSubmissionR tid ssh csh shn cid SubShowR) --- (CSubmissionR tid ssh csh shn _ SubDownloadR) -- just for Download - breadcrumb (CSheetR tid ssh csh shn SCorrR) = return ("Korrektoren", Just $ CSheetR tid ssh csh shn SShowR) - -- (CSheetR tid ssh csh shn SFileR) -- just for Downloads + breadcrumb HealthR = i18nCrumb MsgMenuHealth Nothing + breadcrumb InstanceR = i18nCrumb MsgMenuInstance Nothing - breadcrumb (CourseR tid ssh csh MaterialListR) = return ("Material" , Just $ CourseR tid ssh csh CShowR) - breadcrumb (CourseR tid ssh csh MaterialNewR ) = return ("Neu" , Just $ CourseR tid ssh csh MaterialListR) - breadcrumb (CMaterialR tid ssh csh mnm MShowR) = return (original mnm, Just $ CourseR tid ssh csh MaterialListR) - breadcrumb (CMaterialR tid ssh csh mnm MEditR) = return ("Bearbeiten" , Just $ CMaterialR tid ssh csh mnm MShowR) - breadcrumb (CMaterialR tid ssh csh mnm MDelR) = return ("Löschen" , Just $ CMaterialR tid ssh csh mnm MShowR) - -- (CMaterialR tid ssh csh mnm MFileR) -- just for Downloads + breadcrumb ProfileR = i18nCrumb MsgBreadcrumbProfile Nothing + breadcrumb SetDisplayEmailR = i18nCrumb MsgUserDisplayEmail $ Just ProfileR + breadcrumb ProfileDataR = i18nCrumb MsgMenuProfileData $ Just ProfileR + breadcrumb AuthPredsR = i18nCrumb MsgMenuAuthPreds $ Just ProfileR + breadcrumb CsvOptionsR = i18nCrumb MsgCsvOptions $ Just ProfileR + breadcrumb LangR = i18nCrumb MsgMenuLanguage $ Just ProfileR - -- Others - breadcrumb (CorrectionsR) = return ("Korrekturen", Just HomeR) - breadcrumb (CorrectionsUploadR) = return ("Hochladen", Just CorrectionsR) + breadcrumb TermShowR = i18nCrumb MsgMenuTermShow $ Just HomeR + breadcrumb TermCurrentR = i18nCrumb MsgMenuTermCurrent $ Just TermShowR + breadcrumb TermEditR = i18nCrumb MsgMenuTermCreate $ Just TermShowR + breadcrumb (TermEditExistR tid) = i18nCrumb MsgMenuTermEdit . Just $ TermCourseListR tid + breadcrumb (TermCourseListR tid) = maybeT (i18nCrumb MsgBreadcrumbTerm $ Just CourseListR) $ do -- redirect only, used in other breadcrumbs + guardM . lift . runDB $ isJust <$> get tid + i18nCrumb (ShortTermIdentifier $ unTermKey tid) $ Just CourseListR + + breadcrumb (TermSchoolCourseListR tid ssh) = maybeT (i18nCrumb MsgBreadcrumbSchool . Just $ TermCourseListR tid) $ do -- redirect only, used in other breadcrumbs + guardM . lift . runDB $ + (&&) <$> fmap isJust (get ssh) + <*> fmap isJust (get tid) + return (original $ unSchoolKey ssh, Just $ TermCourseListR tid) + + breadcrumb AllocationListR = i18nCrumb MsgAllocationListTitle $ Just HomeR + breadcrumb (AllocationR tid ssh ash AShowR) = maybeT (i18nCrumb MsgBreadcrumbAllocation $ Just AllocationListR) $ do + mr <- getMessageRender + Entity _ Allocation{allocationName} <- MaybeT . runDB . getBy $ TermSchoolAllocationShort tid ssh ash + return ([st|#{allocationName} (#{mr (ShortTermIdentifier (unTermKey tid))}, #{original (unSchoolKey ssh)})|], Just $ AllocationListR) + breadcrumb (AllocationR tid ssh ash ARegisterR) = i18nCrumb MsgBreadcrumbAllocationRegister . Just $ AllocationR tid ssh ash AShowR + breadcrumb (AllocationR tid ssh ash (AApplyR cID)) = maybeT (i18nCrumb MsgBreadcrumbCourse . Just $ AllocationR tid ssh ash AShowR) $ do + cid <- decrypt cID + Course{..} <- hoist runDB $ do + aid <- MaybeT . getKeyBy $ TermSchoolAllocationShort tid ssh ash + guardM . lift $ exists [ AllocationCourseAllocation ==. aid, AllocationCourseCourse ==. cid ] + MaybeT $ get cid + return (original courseName, Just $ AllocationR tid ssh ash AShowR) + + breadcrumb CourseListR = i18nCrumb MsgMenuCourseList Nothing + breadcrumb CourseNewR = i18nCrumb MsgMenuCourseNew $ Just CourseListR + breadcrumb (CourseR tid ssh csh CShowR) = maybeT (i18nCrumb MsgBreadcrumbCourse . Just $ TermSchoolCourseListR tid ssh) $ do + guardM . lift . runDB . existsBy $ TermSchoolCourseShort tid ssh csh + return (original csh, Just $ TermSchoolCourseListR tid ssh) + breadcrumb (CourseR tid ssh csh CEditR) = i18nCrumb MsgMenuCourseEdit . Just $ CourseR tid ssh csh CShowR + breadcrumb (CourseR tid ssh csh CUsersR) = i18nCrumb MsgMenuCourseMembers . Just $ CourseR tid ssh csh CShowR + breadcrumb (CourseR tid ssh csh CAddUserR) = i18nCrumb MsgMenuCourseAddMembers . Just $ CourseR tid ssh csh CUsersR + breadcrumb (CourseR tid ssh csh CInviteR) = i18nCrumb MsgBreadcrumbCourseParticipantInvitation . Just $ CourseR tid ssh csh CShowR + breadcrumb (CourseR tid ssh csh CExamOfficeR) = i18nCrumb MsgMenuCourseExamOffice . Just $ CourseR tid ssh csh CShowR + breadcrumb (CourseR tid ssh csh (CUserR cID)) = maybeT (i18nCrumb MsgBreadcrumbUser . Just $ CourseR tid ssh csh CUsersR) $ do + guardM . hasReadAccessTo . CourseR tid ssh csh $ CUserR cID + uid <- decrypt cID + User{userDisplayName} <- MaybeT . runDB $ get uid + return (userDisplayName, Just $ CourseR tid ssh csh CUsersR) + breadcrumb (CourseR tid ssh csh CCorrectionsR) = i18nCrumb MsgMenuSubmissions . Just $ CourseR tid ssh csh CShowR + breadcrumb (CourseR tid ssh csh CAssignR) = i18nCrumb MsgMenuCorrectionsAssign . Just $ CourseR tid ssh csh CCorrectionsR + breadcrumb (CourseR tid ssh csh SheetListR) = i18nCrumb MsgMenuSheetList . Just $ CourseR tid ssh csh CShowR + breadcrumb (CourseR tid ssh csh SheetNewR ) = i18nCrumb MsgMenuSheetNew . Just $ CourseR tid ssh csh SheetListR + breadcrumb (CourseR tid ssh csh SheetCurrentR) = i18nCrumb MsgMenuSheetCurrent . Just $ CourseR tid ssh csh SheetListR + breadcrumb (CourseR tid ssh csh SheetOldUnassignedR) = i18nCrumb MsgMenuSheetOldUnassigned . Just $ CourseR tid ssh csh SheetListR + breadcrumb (CourseR tid ssh csh CCommR ) = i18nCrumb MsgMenuCourseCommunication . Just $ CourseR tid ssh csh CShowR + breadcrumb (CourseR tid ssh csh CTutorialListR) = i18nCrumb MsgMenuTutorialList . Just $ CourseR tid ssh csh CShowR + breadcrumb (CourseR tid ssh csh CTutorialNewR) = i18nCrumb MsgMenuTutorialNew . Just $ CourseR tid ssh csh CTutorialListR + breadcrumb (CourseR tid ssh csh CFavouriteR) = i18nCrumb MsgBreadcrumbCourseFavourite . Just $ CourseR tid ssh csh CShowR + breadcrumb (CourseR tid ssh csh CRegisterR) = i18nCrumb MsgBreadcrumbCourseRegister . Just $ CourseR tid ssh csh CShowR + breadcrumb (CourseR tid ssh csh CRegisterTemplateR) = i18nCrumb MsgBreadcrumbCourseRegisterTemplate . Just $ CourseR tid ssh csh CShowR + breadcrumb (CourseR tid ssh csh CLecInviteR) = i18nCrumb MsgBreadcrumbLecturerInvite . Just $ CourseR tid ssh csh CShowR + breadcrumb (CourseR tid ssh csh CDeleteR) = i18nCrumb MsgMenuCourseDelete . Just $ CourseR tid ssh csh CShowR + breadcrumb (CourseR tid ssh csh CHiWisR) = i18nCrumb MsgBreadcrumbHiWis . Just $ CourseR tid ssh csh CShowR + breadcrumb (CourseR tid ssh csh CNotesR) = i18nCrumb MsgBreadcrumbCourseNotes . Just $ CourseR tid ssh csh CShowR + + breadcrumb (CourseR tid ssh csh CNewsNewR) = i18nCrumb MsgMenuCourseNewsNew . Just $ CourseR tid ssh csh CShowR + breadcrumb (CourseR tid ssh csh (CourseNewsR cID sRoute)) = case sRoute of + CNShowR -> i18nCrumb MsgBreadcrumbCourseNews . Just $ CourseR tid ssh csh CShowR + CNEditR -> i18nCrumb MsgMenuCourseNewsEdit . Just $ CNewsR tid ssh csh cID CNShowR + CNDeleteR -> i18nCrumb MsgBreadcrumbCourseNewsDelete . Just $ CNewsR tid ssh csh cID CNShowR + CNArchiveR -> i18nCrumb MsgBreadcrumbCourseNewsArchive . Just $ CNewsR tid ssh csh cID CNShowR + CNFileR _ -> i18nCrumb MsgBreadcrumbCourseNewsFile . Just $ CNewsR tid ssh csh cID CNShowR + + breadcrumb (CourseR tid ssh csh CEventsNewR) = i18nCrumb MsgMenuCourseEventNew . Just $ CourseR tid ssh csh CShowR + breadcrumb (CourseR tid ssh csh (CourseEventR _cID sRoute)) = case sRoute of + CEvEditR -> i18nCrumb MsgMenuCourseEventEdit . Just $ CourseR tid ssh csh CShowR + CEvDeleteR -> i18nCrumb MsgBreadcrumbCourseEventDelete . Just $ CourseR tid ssh csh CShowR + + breadcrumb (CourseR tid ssh csh CExamListR) = i18nCrumb MsgMenuExamList . Just $ CourseR tid ssh csh CShowR + breadcrumb (CourseR tid ssh csh CExamNewR) = i18nCrumb MsgMenuExamNew . Just $ CourseR tid ssh csh CExamListR + + breadcrumb (CourseR tid ssh csh CApplicationsR) = i18nCrumb MsgMenuCourseApplications . Just $ CourseR tid ssh csh CShowR + breadcrumb (CourseR tid ssh csh CAppsFilesR) = i18nCrumb MsgBreadcrumbCourseAppsFiles . Just $ CourseR tid ssh csh CApplicationsR + + breadcrumb (CourseR tid ssh csh (CourseApplicationR cID sRoute)) = case sRoute of + CAEditR -> maybeT (i18nCrumb MsgBreadcrumbApplicant . Just $ CourseR tid ssh csh CApplicationsR) $ do + guardM . hasReadAccessTo $ CApplicationR tid ssh csh cID CAEditR + appId <- decrypt cID + User{..} <- hoist runDB $ MaybeT (get appId) >>= MaybeT . get . courseApplicationUser + return (userDisplayName, Just $ CourseR tid ssh csh CApplicationsR) + CAFilesR -> i18nCrumb MsgBreadcrumbApplicationFiles . Just $ CApplicationR tid ssh csh cID CAEditR + + breadcrumb (CourseR tid ssh csh (ExamR examn sRoute)) = case sRoute of + EShowR -> maybeT (i18nCrumb MsgBreadcrumbExam . Just $ CourseR tid ssh csh CExamListR) $ do + guardM . hasReadAccessTo $ CExamR tid ssh csh examn EShowR + return (original examn, Just $ CourseR tid ssh csh CExamListR) + EEditR -> i18nCrumb MsgMenuExamEdit . Just $ CExamR tid ssh csh examn EShowR + EUsersR -> i18nCrumb MsgMenuExamUsers . Just $ CExamR tid ssh csh examn EShowR + EAddUserR -> i18nCrumb MsgMenuExamAddMembers . Just $ CExamR tid ssh csh examn EUsersR + EGradesR -> i18nCrumb MsgMenuExamGrades . Just $ CExamR tid ssh csh examn EShowR + ECInviteR -> i18nCrumb MsgBreadcrumbExamCorrectorInvite . Just $ CExamR tid ssh csh examn EShowR + EInviteR -> i18nCrumb MsgBreadcrumbExamParticipantInvite . Just $ CExamR tid ssh csh examn EShowR + ERegisterR -> i18nCrumb MsgBreadcrumbExamRegister . Just $ CExamR tid ssh csh examn EShowR + + breadcrumb (CourseR tid ssh csh (TutorialR tutn sRoute)) = case sRoute of + TUsersR -> maybeT (i18nCrumb MsgBreadcrumbTutorial . Just $ CourseR tid ssh csh CTutorialListR) $ do + guardM . hasReadAccessTo $ CTutorialR tid ssh csh tutn TUsersR + return (original tutn, Just $ CourseR tid ssh csh CTutorialListR) + TEditR -> i18nCrumb MsgMenuTutorialEdit . 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 + TRegisterR -> i18nCrumb MsgBreadcrumbTutorialRegister . Just $ CourseR tid ssh csh CShowR + TInviteR -> i18nCrumb MsgBreadcrumbTutorInvite . Just $ CTutorialR tid ssh csh tutn TUsersR + + breadcrumb (CourseR tid ssh csh (SheetR shn sRoute)) = case sRoute of + SShowR -> maybeT (i18nCrumb MsgBreadcrumbSheet . Just $ CourseR tid ssh csh SheetListR) $ do + guardM . hasReadAccessTo $ CSheetR tid ssh csh shn SShowR + return (original shn, Just $ CourseR tid ssh csh SheetListR) + SEditR -> i18nCrumb MsgMenuSheetEdit . Just $ CSheetR tid ssh csh shn SShowR + SDelR -> i18nCrumb MsgMenuSheetDelete . Just $ CSheetR tid ssh csh shn SShowR + SSubsR -> i18nCrumb MsgMenuSubmissions . Just $ CSheetR tid ssh csh shn SShowR + SAssignR -> i18nCrumb MsgMenuCorrectionsAssign . Just $ CSheetR tid ssh csh shn SSubsR + SubmissionNewR -> i18nCrumb MsgMenuSubmissionNew . Just $ CSheetR tid ssh csh shn SShowR + SubmissionOwnR -> i18nCrumb MsgMenuSubmissionOwn . Just $ CSheetR tid ssh csh shn SShowR + SubmissionR cid sRoute' -> case sRoute' of + SubShowR -> do + mayList <- hasReadAccessTo $ CSheetR tid ssh csh shn SSubsR + if + | mayList + -> i18nCrumb MsgBreadcrumbSubmission . Just $ CSheetR tid ssh csh shn SSubsR + | otherwise + -> i18nCrumb MsgBreadcrumbSubmission . Just $ CSheetR tid ssh csh shn SShowR + CorrectionR -> i18nCrumb MsgMenuCorrection . Just $ CSubmissionR tid ssh csh shn cid SubShowR + SubDelR -> i18nCrumb MsgMenuSubmissionDelete . Just $ CSubmissionR tid ssh csh shn cid SubShowR + SubAssignR -> i18nCrumb MsgCorrectorAssignTitle . Just $ CSubmissionR tid ssh csh shn cid SubShowR + SInviteR -> i18nCrumb MsgBreadcrumbSubmissionUserInvite . Just $ CSubmissionR tid ssh csh shn cid SubShowR + SubArchiveR sft -> i18nCrumb sft . Just $ CSubmissionR tid ssh csh shn cid SubShowR + SubDownloadR _ _ -> i18nCrumb MsgBreadcrumbSubmissionFile . Just $ CSubmissionR tid ssh csh shn cid SubShowR + SArchiveR -> i18nCrumb MsgBreadcrumbSheetArchive . Just $ CSheetR tid ssh csh shn SShowR + SIsCorrR -> i18nCrumb MsgBreadcrumbSheetIsCorrector . Just $ CSheetR tid ssh csh shn SShowR + SPseudonymR -> i18nCrumb MsgBreadcrumbSheetPseudonym . Just $ CSheetR tid ssh csh shn SShowR + SCorrInviteR -> i18nCrumb MsgBreadcrumbSheetCorrectorInvite . Just $ CSheetR tid ssh csh shn SShowR + SZipR sft -> i18nCrumb sft . Just $ CSheetR tid ssh csh shn SShowR + SFileR _ _ -> i18nCrumb MsgBreadcrumbSheetFile . Just $ CSheetR tid ssh csh shn SShowR + + breadcrumb (CourseR tid ssh csh MaterialListR) = i18nCrumb MsgMenuMaterialList . Just $ CourseR tid ssh csh CShowR + breadcrumb (CourseR tid ssh csh MaterialNewR ) = i18nCrumb MsgMenuMaterialNew . Just $ CourseR tid ssh csh MaterialListR + breadcrumb (CourseR tid ssh csh (MaterialR mnm sRoute)) = case sRoute of + MShowR -> maybeT (i18nCrumb MsgBreadcrumbMaterial . Just $ CourseR tid ssh csh MaterialListR) $ do + guardM . hasReadAccessTo $ CMaterialR tid ssh csh mnm MShowR + return (original mnm, Just $ CourseR tid ssh csh MaterialListR) + MEditR -> i18nCrumb MsgMenuMaterialEdit . Just $ CMaterialR tid ssh csh mnm MShowR + MDelR -> i18nCrumb MsgMenuMaterialDelete . Just $ CMaterialR tid ssh csh mnm MShowR + MArchiveR -> i18nCrumb MsgBreadcrumbMaterialArchive . Just $ CMaterialR tid ssh csh mnm MShowR + MFileR _ -> i18nCrumb MsgBreadcrumbMaterialFile . Just $ CMaterialR tid ssh csh mnm MShowR + + breadcrumb CorrectionsR = i18nCrumb MsgMenuCorrections Nothing + breadcrumb CorrectionsUploadR = i18nCrumb MsgMenuCorrectionsUpload $ Just CorrectionsR + breadcrumb CorrectionsCreateR = i18nCrumb MsgMenuCorrectionsCreate $ Just CorrectionsR + breadcrumb CorrectionsGradeR = i18nCrumb MsgMenuCorrectionsGrade $ Just CorrectionsR + breadcrumb CorrectionsDownloadR = i18nCrumb MsgMenuCorrectionsDownload $ Just CorrectionsR + + breadcrumb (CryptoUUIDDispatchR _) = i18nCrumb MsgBreadcrumbCryptoIDDispatch Nothing + breadcrumb (MessageR _) = do mayList <- (== Authorized) <$> evalAccess MessageListR False - return $ if - | mayList -> ("Statusmeldung", Just MessageListR) - | otherwise -> ("Statusmeldung", Just HomeR) - breadcrumb (MessageListR) = return ("Statusmeldungen", Just AdminR) - breadcrumb _ = return ("Uni2work", Nothing) -- Default is no breadcrumb at all + if + | mayList -> i18nCrumb MsgBreadcrumbSystemMessage $ Just MessageListR + | otherwise -> i18nCrumb MsgBreadcrumbSystemMessage $ Just HomeR + breadcrumb MessageListR = i18nCrumb MsgMenuMessageList $ Just AdminR + + breadcrumb GlossaryR = i18nCrumb MsgMenuGlossary $ Just InfoR + -- breadcrumb _ = return ("Uni2work", Nothing) -- Default is no breadcrumb at all submissionList :: TermId -> CourseShorthand -> SheetName -> UserId -> DB [E.Value SubmissionId] submissionList tid csh shn uid = E.select . E.from $ \(course `E.InnerJoin` sheet `E.InnerJoin` submission `E.InnerJoin` submissionUser) -> do @@ -2064,6 +1978,14 @@ defaultLinks = fmap catMaybes . mapM runMaybeT $ -- Define the menu items of the , menuItemModal = False , menuItemAccessCallback' = return True } + , return MenuItem + { menuItemType = Footer + , menuItemLabel = MsgMenuGlossary + , menuItemIcon = Nothing + , menuItemRoute = SomeRoute GlossaryR + , menuItemModal = False + , menuItemAccessCallback' = return True + } , do mCurrentRoute <- getCurrentRoute @@ -2310,6 +2232,14 @@ pageActions (InfoR) = [ , menuItemModal = False , menuItemAccessCallback' = return True } + , MenuItem + { menuItemType = PageActionPrime + , menuItemLabel = MsgMenuGlossary + , menuItemIcon = Nothing + , menuItemRoute = SomeRoute GlossaryR + , menuItemModal = False + , menuItemAccessCallback' = return True + } ] pageActions (VersionR) = [ MenuItem @@ -2890,14 +2820,6 @@ pageActions (CSheetR tid ssh csh shn SShowR) = , menuItemModal = False , menuItemAccessCallback' = (== Authorized) <$> evalAccessCorrector tid ssh csh } - , MenuItem - { menuItemType = PageActionPrime - , menuItemLabel = MsgMenuCorrectors - , menuItemIcon = Nothing - , menuItemRoute = SomeRoute $ CSheetR tid ssh csh shn SCorrR - , menuItemModal = False - , menuItemAccessCallback' = return True - } , MenuItem { menuItemType = PageActionPrime , menuItemLabel = MsgMenuSubmissions @@ -2948,14 +2870,6 @@ pageActions (CSheetR tid ssh csh shn SSubsR) = , menuItemModal = True , menuItemAccessCallback' = return True } - , MenuItem - { menuItemType = PageActionPrime - , menuItemLabel = MsgMenuCorrectors - , menuItemIcon = Nothing - , menuItemRoute = SomeRoute $ CSheetR tid ssh csh shn SCorrR - , menuItemModal = False - , menuItemAccessCallback' = return True - } , MenuItem { menuItemType = PageActionPrime , menuItemLabel = MsgMenuCorrectionsAssign @@ -3001,32 +2915,6 @@ pageActions (CSubmissionR tid ssh csh shn cid CorrectionR) = , menuItemAccessCallback' = return True } ] -pageActions (CSheetR tid ssh csh shn SCorrR) = - [ MenuItem - { menuItemType = PageActionPrime - , menuItemLabel = MsgMenuSubmissions - , menuItemIcon = Nothing - , menuItemRoute = SomeRoute $ CSheetR tid ssh csh shn SSubsR - , menuItemModal = False - , menuItemAccessCallback' = return True - } - , MenuItem - { menuItemType = PageActionPrime - , menuItemLabel = MsgMenuCorrectionsAssign - , menuItemIcon = Nothing - , menuItemRoute = SomeRoute $ CSheetR tid ssh csh shn SAssignR - , menuItemModal = False - , menuItemAccessCallback' = return True - } - , MenuItem - { menuItemType = PageActionSecondary - , menuItemLabel = MsgMenuSheetEdit - , menuItemIcon = Nothing - , menuItemRoute = SomeRoute $ CSheetR tid ssh csh shn SEditR - , menuItemModal = False - , menuItemAccessCallback' = return True - } - ] pageActions (CourseR tid ssh csh CApplicationsR) = [ MenuItem { menuItemType = PageActionPrime @@ -3226,8 +3114,6 @@ pageHeading (CSubmissionR tid ssh csh shn _ SubShowR) -- TODO: Rethink this one! pageHeading (CSubmissionR tid ssh csh shn cid CorrectionR) = Just $ i18nHeading $ MsgCorrectionHead tid ssh csh shn cid -- (CSubmissionR tid csh shn cid SubDownloadR) -- just a download -pageHeading (CSheetR _tid _ssh _csh shn SCorrR) - = Just $ i18nHeading $ MsgCorrectorsHead shn -- (CSheetR tid ssh csh shn SFileR) -- just for Downloads pageHeading CorrectionsR @@ -3465,7 +3351,7 @@ upsertCampusUser ldapData Creds{..} = do , userWarningDays = userDefaultWarningDays , userShowSex = userDefaultShowSex , userNotificationSettings = def - , userMailLanguages = def + , userLanguages = Nothing , userCsvOptions = def , userTokensIssuedAfter = Nothing , userCreated = now @@ -3647,6 +3533,47 @@ associateUserSchoolsByTerms uid = do , userSchoolIsOptOut = False } +setLangCookie :: MonadHandler m => Lang -> m () +setLangCookie lang = do + now <- liftIO getCurrentTime + setCookie $ def + { setCookieName = "_LANG" + , setCookieValue = encodeUtf8 lang + , setCookieExpires = Just $ addUTCTime (400 * avgNominalYear) now + , setCookiePath = Just "/" + } + +updateUserLanguage :: Maybe Lang -> DB (Maybe Lang) +updateUserLanguage (Just lang) = do + unless (lang `elem` appLanguages) $ + invalidArgs ["Unsupported language"] + + muid <- maybeAuthId + for_ muid $ \uid -> do + langs <- languages + update uid [ UserLanguages =. Just (Languages $ lang : nub (filter ((&&) <$> (`elem` appLanguages) <*> (/= lang)) langs)) ] + setLangCookie lang + return $ Just lang +updateUserLanguage Nothing = runMaybeT $ do + uid <- MaybeT maybeAuthId + User{..} <- MaybeT $ get uid + setLangs <- toList . selectLanguages appLanguages <$> languages + highPrioSetLangs <- toList . selectLanguages appLanguages <$> highPrioRequestedLangs + let userLanguages' = toList . selectLanguages appLanguages <$> userLanguages ^? _Just . _Wrapped + lang <- case (userLanguages', setLangs, highPrioSetLangs) of + (_, _, hpl : _) + -> lift $ hpl <$ update uid [ UserLanguages =. Just (Languages highPrioSetLangs) ] + (Just (l : _), _, _) + -> return l + (Nothing, l : _, _) + -> lift $ l <$ update uid [ UserLanguages =. Just (Languages setLangs) ] + (Just [], l : _, _) + -> return l + (_, [], _) + -> mzero + setLangCookie lang + return lang + instance YesodAuth UniWorX where type AuthId UniWorX = UserId @@ -3738,13 +3665,21 @@ instance YesodAuth UniWorX where authHttpManager = getsYesod appHttpManager - onLogin = addMessageI Success Auth.NowLoggedIn + onLogin = liftHandler $ do + mlang <- runDB $ updateUserLanguage Nothing + app <- getYesod + let mr | Just lang <- mlang = renderMessage app . map (Text.intercalate "-") . reverse . inits $ Text.splitOn "-" lang + | otherwise = renderMessage app [] + addMessage Success . toHtml $ mr Auth.NowLoggedIn onErrorHtml dest msg = do addMessage Error $ toHtml msg redirect dest - renderAuthMessage _ _ = Auth.germanMessage -- TODO + renderAuthMessage _ ls = case lang of + ("en" : _) -> Auth.englishMessage + _other -> Auth.germanMessage + where lang = Text.splitOn "-" $ selectLanguage' appLanguages ls instance YesodAuthPersist UniWorX diff --git a/src/Foundation/I18n.hs b/src/Foundation/I18n.hs new file mode 100644 index 000000000..99406decd --- /dev/null +++ b/src/Foundation/I18n.hs @@ -0,0 +1,313 @@ +{-# LANGUAGE GeneralizedNewtypeDeriving #-} +{-# OPTIONS_GHC -fno-warn-orphans #-} +module Foundation.I18n + ( UniWorXMessage(..) + , ShortTermIdentifier(..) + , MsgLanguage(..) + , ShortSex(..) + , SheetTypeHeader(..) + , ShortStudyDegree(..) + , ShortStudyTerms(..) + , StudyDegreeTerm(..) + , ShortStudyFieldType(..) + , StudyDegreeTermType(..) + , ErrorResponseTitle(..) + , UniWorXMessages(..) + , uniworxMessages + ) where + +import Foundation.Type + + +import Import.NoFoundation + +import Auth.LDAP +import Auth.PWHash +import Auth.Dummy + +import Data.CaseInsensitive (original, mk) + +import qualified Data.Text as Text + +import Utils.Form + +import Text.Shakespeare.Text (st) + +import GHC.Exts (IsList(..)) + + +pluralDE :: (Eq a, Num a) + => a -- ^ Count + -> Text -- ^ Singular + -> Text -- ^ Plural + -> Text +pluralDE num singularForm pluralForm + | num == 1 = singularForm + | otherwise = pluralForm + +noneOneMoreDE :: (Eq a, Num a) + => a -- ^ Count + -> Text -- ^ None + -> Text -- ^ Singular + -> Text -- ^ Plural + -> Text +noneOneMoreDE num noneText singularForm pluralForm + | num == 0 = noneText + | num == 1 = singularForm + | otherwise = pluralForm + +-- noneMoreDE :: (Eq a, Num a) +-- => a -- ^ Count +-- -> Text -- ^ None +-- -> Text -- ^ Some +-- -> Text +-- noneMoreDE num noneText someText +-- | num == 0 = noneText +-- | otherwise = someText + +pluralEN :: (Eq a, Num a) + => a -- ^ Count + -> Text -- ^ Singular + -> Text -- ^ Plural + -> Text +pluralEN num singularForm pluralForm + | num == 1 = singularForm + | otherwise = pluralForm + +noneOneMoreEN :: (Eq a, Num a) + => a -- ^ Count + -> Text -- ^ None + -> Text -- ^ Singular + -> Text -- ^ Plural + -> Text +noneOneMoreEN num noneText singularForm pluralForm + | num == 0 = noneText + | num == 1 = singularForm + | otherwise = pluralForm + +-- noneMoreEN :: (Eq a, Num a) +-- => a -- ^ Count +-- -> Text -- ^ None +-- -> Text -- ^ Some +-- -> Text +-- noneMoreEN num noneText someText +-- | num == 0 = noneText +-- | otherwise = someText + +ordinalEN :: ToMessage a + => a + -> Text +ordinalEN (toMessage -> numStr) = case lastChar of + Just '1' -> [st|#{numStr}st|] + Just '2' -> [st|#{numStr}nd|] + Just '3' -> [st|#{numStr}rd|] + _other -> [st|#{numStr}th|] + where + lastChar = last <$> fromNullable numStr + + +-- Convenience Type for Messages, since Yesod messages cannot deal with compound type identifiers +type IntMaybe = Maybe Int + +-- | Convenience function for i18n messages definitions +maybeToMessage :: ToMessage m => Text -> Maybe m -> Text -> Text +maybeToMessage _ Nothing _ = mempty +maybeToMessage before (Just x) after = before <> toMessage x <> after + +-- Messages creates type UniWorXMessage and RenderMessage UniWorX instance +mkMessage "UniWorX" "messages/uniworx" "de-de-formal" +mkMessageVariant "UniWorX" "Campus" "messages/campus" "de" +mkMessageVariant "UniWorX" "Dummy" "messages/dummy" "de" +mkMessageVariant "UniWorX" "PWHash" "messages/pw-hash" "de" +mkMessageVariant "UniWorX" "Button" "messages/button" "de" +mkMessageVariant "UniWorX" "Frontend" "messages/frontend" "de-de-formal" + +instance RenderMessage UniWorX TermIdentifier where + renderMessage foundation ls TermIdentifier{..} = case season of + Summer -> renderMessage' $ MsgSummerTerm year + Winter -> renderMessage' $ MsgWinterTerm year + where renderMessage' = renderMessage foundation ls + +newtype ShortTermIdentifier = ShortTermIdentifier TermIdentifier + deriving stock (Eq, Ord, Read, Show) +instance RenderMessage UniWorX ShortTermIdentifier where + renderMessage foundation ls (ShortTermIdentifier TermIdentifier{..}) = case season of + Summer -> renderMessage' $ MsgSummerTermShort year + Winter -> renderMessage' $ MsgWinterTermShort year + where renderMessage' = renderMessage foundation ls + +instance RenderMessage UniWorX String where + renderMessage f ls str = renderMessage f ls $ Text.pack str + +-- TODO: raw number representation; instead, display e.g. 1000 as 1.000 or 1,000 or ... (language-dependent!) +instance RenderMessage UniWorX Int where + renderMessage f ls = renderMessage f ls . tshow +instance RenderMessage UniWorX Int64 where + renderMessage f ls = renderMessage f ls . tshow +instance RenderMessage UniWorX Integer where + renderMessage f ls = renderMessage f ls . tshow +instance RenderMessage UniWorX Natural where + renderMessage f ls = renderMessage f ls . tshow + +instance HasResolution a => RenderMessage UniWorX (Fixed a) where + renderMessage f ls = renderMessage f ls . showFixed True + +instance RenderMessage UniWorX Load where + renderMessage foundation ls = renderMessage foundation ls . \case + Load { byTutorial = Nothing , byProportion = p } -> MsgCorByProportionOnly p + Load { byTutorial = Just True , byProportion = p } -> MsgCorByProportionIncludingTutorial p + Load { byTutorial = Just False, byProportion = p } -> MsgCorByProportionExcludingTutorial p + +newtype MsgLanguage = MsgLanguage Lang + deriving stock (Eq, Ord, Show, Read) +instance RenderMessage UniWorX MsgLanguage where + renderMessage foundation ls (MsgLanguage lang@(map mk . Text.splitOn "-" -> lang')) + | ("de" : "DE" : _) <- lang' = mr MsgGermanGermany + | ("de" : _) <- lang' = mr MsgGerman + | ("en" : "EU" : _) <- lang' = mr MsgEnglishEurope + | ("en" : _) <- lang' = mr MsgEnglish + | otherwise = lang + where + mr = renderMessage foundation ls + +embedRenderMessage ''UniWorX ''MessageStatus ("Message" <>) +embedRenderMessage ''UniWorX ''NotificationTrigger $ ("NotificationTrigger" <>) . concat . drop 1 . splitCamel +embedRenderMessage ''UniWorX ''StudyFieldType id +embedRenderMessage ''UniWorX ''SheetFileType id +embedRenderMessage ''UniWorX ''SubmissionFileType id +embedRenderMessage ''UniWorX ''CorrectorState id +embedRenderMessage ''UniWorX ''RatingException id +embedRenderMessage ''UniWorX ''SubmissionSinkException ("SubmissionSinkException" <>) +embedRenderMessage ''UniWorX ''SheetGrading ("SheetGrading" <>) +embedRenderMessage ''UniWorX ''AuthTag $ ("AuthTag" <>) . concat . drop 1 . splitCamel +embedRenderMessage ''UniWorX ''EncodedSecretBoxException id +embedRenderMessage ''UniWorX ''LecturerType id +embedRenderMessage ''UniWorX ''SubmissionModeDescr + $ let verbMap [_, _, "None"] = "NoSubmissions" + verbMap [_, _, v] = v <> "Submissions" + verbMap _ = error "Invalid number of verbs" + in verbMap . splitCamel +embedRenderMessage ''UniWorX ''UploadModeDescr id +embedRenderMessage ''UniWorX ''SecretJSONFieldException id +embedRenderMessage ''UniWorX ''AFormMessage $ concat . drop 2 . splitCamel +embedRenderMessage ''UniWorX ''SchoolFunction id +embedRenderMessage ''UniWorX ''CsvPreset id +embedRenderMessage ''UniWorX ''Quoting ("Csv" <>) +embedRenderMessage ''UniWorX ''FavouriteReason id +embedRenderMessage ''UniWorX ''Sex id + +embedRenderMessage ''UniWorX ''AuthenticationMode id + +newtype ShortSex = ShortSex Sex +embedRenderMessageVariant ''UniWorX ''ShortSex ("Short" <>) + +newtype SheetTypeHeader = SheetTypeHeader SheetType +embedRenderMessageVariant ''UniWorX ''SheetTypeHeader ("SheetType" <>) + +instance RenderMessage UniWorX SheetType where + renderMessage foundation ls sheetType = case sheetType of + NotGraded -> mr $ SheetTypeHeader NotGraded + other -> mr (grading other) <> ", " <> mr (SheetTypeHeader other) + where + mr :: RenderMessage UniWorX msg => msg -> Text + mr = renderMessage foundation ls + +instance RenderMessage UniWorX StudyDegree where + renderMessage _found _ls StudyDegree{..} = fromMaybe (tshow studyDegreeKey) (studyDegreeName <|> studyDegreeShorthand) + +newtype ShortStudyDegree = ShortStudyDegree StudyDegree + +instance RenderMessage UniWorX ShortStudyDegree where + renderMessage _found _ls (ShortStudyDegree StudyDegree{..}) = fromMaybe (tshow studyDegreeKey) studyDegreeShorthand + +instance RenderMessage UniWorX StudyTerms where + renderMessage _found _ls StudyTerms{..} = fromMaybe (tshow studyTermsKey) (studyTermsName <|> studyTermsShorthand) + +newtype ShortStudyTerms = ShortStudyTerms StudyTerms + +instance RenderMessage UniWorX ShortStudyTerms where + renderMessage _found _ls (ShortStudyTerms StudyTerms{..}) = fromMaybe (tshow studyTermsKey) studyTermsShorthand + +data StudyDegreeTerm = StudyDegreeTerm StudyDegree StudyTerms + +instance RenderMessage UniWorX StudyDegreeTerm where + renderMessage foundation ls (StudyDegreeTerm deg trm) = mr trm <> " (" <> mr (ShortStudyDegree deg) <> ")" + where + mr :: RenderMessage UniWorX msg => msg -> Text + mr = renderMessage foundation ls + +newtype ShortStudyFieldType = ShortStudyFieldType StudyFieldType +embedRenderMessageVariant ''UniWorX ''ShortStudyFieldType ("Short" <>) + +data StudyDegreeTermType = StudyDegreeTermType StudyDegree StudyTerms StudyFieldType + +instance RenderMessage UniWorX StudyDegreeTermType where + renderMessage foundation ls (StudyDegreeTermType deg trm typ) = mr trm <> " (" <> mr (ShortStudyDegree deg) <> ", " <> mr (ShortStudyFieldType typ) <> ")" + where + mr :: RenderMessage UniWorX msg => msg -> Text + mr = renderMessage foundation ls + +instance RenderMessage UniWorX ExamGrade where + renderMessage _ _ = pack . (showFixed False :: Deci -> String) . fromRational . review numberGrade + +instance RenderMessage UniWorX ExamPassed where + renderMessage foundation ls = \case + ExamPassed True -> mr MsgExamPassed + ExamPassed False -> mr MsgExamNotPassed + where + mr :: RenderMessage UniWorX msg => msg -> Text + mr = renderMessage foundation ls + +instance RenderMessage UniWorX a => RenderMessage UniWorX (ExamResult' a) where + renderMessage foundation ls = \case + ExamAttended{..} -> mr examResult + ExamNoShow -> mr MsgExamResultNoShow + ExamVoided -> mr MsgExamResultVoided + where + mr :: RenderMessage UniWorX msg => msg -> Text + mr = renderMessage foundation ls + +instance RenderMessage UniWorX (Either ExamPassed ExamGrade) where + renderMessage foundation ls = either mr mr + where + mr :: RenderMessage UniWorX msg => msg -> Text + mr = renderMessage foundation ls + +-- ToMessage instances for converting raw numbers to Text (no internationalization) + +instance ToMessage Int where + toMessage = tshow +instance ToMessage Int64 where + toMessage = tshow +instance ToMessage Integer where + toMessage = tshow +instance ToMessage Natural where + toMessage = tshow + +instance HasResolution a => ToMessage (Fixed a) where + toMessage = toMessage . showFixed True + +-- Do not use toMessage on Rationals and round them automatically. Instead, use rationalToFixed3 (declared in src/Utils.hs) to convert a Rational to Fixed E3! +-- instance ToMessage Rational where +-- toMessage = toMessage . fromRational' +-- where fromRational' = fromRational :: Rational -> Fixed E3 + + +newtype ErrorResponseTitle = ErrorResponseTitle ErrorResponse +embedRenderMessageVariant ''UniWorX ''ErrorResponseTitle ("ErrorResponseTitle" <>) + +newtype UniWorXMessages = UniWorXMessages [SomeMessage UniWorX] + deriving stock (Generic, Typeable) + deriving newtype (Semigroup, Monoid) + +instance IsList UniWorXMessages where + type Item UniWorXMessages = SomeMessage UniWorX + fromList = UniWorXMessages + toList (UniWorXMessages msgs) = msgs + +instance RenderMessage UniWorX UniWorXMessages where + renderMessage foundation ls (UniWorXMessages msgs) = + Text.unwords $ map (renderMessage foundation ls) msgs + +uniworxMessages :: [UniWorXMessage] -> UniWorXMessages +uniworxMessages = UniWorXMessages . map SomeMessage diff --git a/src/Foundation/Routes.hs b/src/Foundation/Routes.hs new file mode 100644 index 000000000..614bdea6d --- /dev/null +++ b/src/Foundation/Routes.hs @@ -0,0 +1,10 @@ +module Foundation.Routes + ( uniworxRoutes + ) where + +import ClassyPrelude.Yesod +import Yesod.Routes.TH.Types (ResourceTree) + + +uniworxRoutes :: [ResourceTree String] +uniworxRoutes = $(parseRoutesFile "routes") diff --git a/src/Foundation/Type.hs b/src/Foundation/Type.hs new file mode 100644 index 000000000..5b63b9080 --- /dev/null +++ b/src/Foundation/Type.hs @@ -0,0 +1,58 @@ +module Foundation.Type + ( UniWorX(..) + , SMTPPool + , _appSettings', _appStatic, _appConnPool, _appSmtpPool, _appLdapPool, _appWidgetMemcached, _appHttpManager, _appLogger, _appLogSettings, _appCryptoIDKey, _appClusterID, _appInstanceID, _appJobState, _appSessionKey, _appSecretBoxKey, _appJSONWebKeySet, _appHealthReport + ) where + +import Import.NoFoundation +import Database.Persist.Sql (ConnectionPool) + +import qualified Web.ClientSession as ClientSession + +import Jobs.Types + +import Yesod.Core.Types (Logger) + +import Data.Set (Set) + +import qualified Crypto.Saltine.Core.SecretBox as SecretBox +import qualified Jose.Jwk as Jose + +import qualified Database.Memcached.Binary.IO as Memcached + + +type SMTPPool = Pool SMTPConnection + +-- | The foundation datatype for your application. This can be a good place to +-- keep settings and values requiring initialization before your application +-- starts running, such as database connections. Every handler will have +-- access to the data present here. +data UniWorX = UniWorX + { appSettings' :: AppSettings + , appStatic :: EmbeddedStatic -- ^ Settings for static file serving. + , appConnPool :: ConnectionPool -- ^ Database connection pool. + , appSmtpPool :: Maybe SMTPPool + , appLdapPool :: Maybe LdapPool + , appWidgetMemcached :: Maybe Memcached.Connection -- ^ Actually a proper pool + , appHttpManager :: Manager + , appLogger :: (ReleaseKey, TVar Logger) + , appLogSettings :: TVar LogSettings + , appCryptoIDKey :: CryptoIDKey + , appClusterID :: ClusterId + , appInstanceID :: InstanceId + , appJobState :: TMVar JobState + , appSessionKey :: ClientSession.Key + , appSecretBoxKey :: SecretBox.Key + , appJSONWebKeySet :: Jose.JwkSet + , appHealthReport :: TVar (Set (UTCTime, HealthReport)) + } + +makeLenses_ ''UniWorX +instance HasInstanceID UniWorX InstanceId where + instanceID = _appInstanceID +instance HasJSONWebKeySet UniWorX Jose.JwkSet where + jsonWebKeySet = _appJSONWebKeySet +instance HasHttpManager UniWorX Manager where + httpManager = _appHttpManager +instance HasAppSettings UniWorX where + appSettings = _appSettings' diff --git a/src/Handler/Admin.hs b/src/Handler/Admin.hs index ae605e860..0002200e8 100644 --- a/src/Handler/Admin.hs +++ b/src/Handler/Admin.hs @@ -4,6 +4,8 @@ module Handler.Admin import Import +import Handler.Utils + import Handler.Admin.Test as Handler.Admin import Handler.Admin.ErrorMessage as Handler.Admin import Handler.Admin.StudyFeatures as Handler.Admin @@ -13,8 +15,4 @@ getAdminR :: Handler Html getAdminR = siteLayoutMsg MsgAdminHeading $ do setTitleI MsgAdminHeading - [whamlet| - This shall become the Administrators' overview page. - Its current purpose is to provide links to some important admin functions - |] - + i18n MsgAdminPageEmpty diff --git a/src/Handler/Admin/Test.hs b/src/Handler/Admin/Test.hs index 7acf3a7ea..b08f07dff 100644 --- a/src/Handler/Admin/Test.hs +++ b/src/Handler/Admin/Test.hs @@ -38,7 +38,7 @@ emailTestForm :: AForm (HandlerFor UniWorX) (Email, MailContext) emailTestForm = (,) <$> areq emailField (fslI MsgMailTestFormEmail) Nothing <*> ( MailContext - <$> (MailLanguages <$> areq (reorderField appLanguagesOpts) (fslI MsgMailTestFormLanguages) Nothing) + <$> (Languages <$> areq (reorderField appLanguagesOpts) (fslI MsgMailTestFormLanguages) Nothing) <*> (toMailDateTimeFormat <$> areq (selectField $ dateTimeFormatOptions SelFormatDateTime) (fslI MsgDateTimeFormat) Nothing <*> areq (selectField $ dateTimeFormatOptions SelFormatDate) (fslI MsgDateFormat) Nothing @@ -54,7 +54,7 @@ emailTestForm = (,) makeDemoForm :: Int -> Form (Int,Bool,Double) makeDemoForm n = identifyForm ("adminTestForm" :: Text) $ \html -> do (result, widget) <- flip (renderAForm FormStandard) html $ (,,) - <$> areq (minIntField n "Zahl") (fromString $ "Ganzzahl > " ++ show n) Nothing + <$> areq (minIntFieldI n ("Zahl" :: Text)) (fromString $ "Ganzzahl > " ++ show n) Nothing <* aformSection MsgFormBehaviour <*> areq checkBoxField "Muss nächste Zahl größer sein?" (Just True) <*> areq doubleField "Fliesskommazahl" Nothing @@ -194,7 +194,7 @@ postAdminTestR = do siteLayout locallyDefinedPageHeading $ do -- defaultLayout $ do setTitle "Uni2work Admin Testpage" - $(widgetFile "adminTest") + $(i18nWidgetFile "admin-test") [whamlet|

Formular Demonstration|] wrapForm formWidget FormSettings diff --git a/src/Handler/Allocation/Application.hs b/src/Handler/Allocation/Application.hs index 912fd8450..9c1cba0e1 100644 --- a/src/Handler/Allocation/Application.hs +++ b/src/Handler/Allocation/Application.hs @@ -52,7 +52,7 @@ data ApplicationForm = ApplicationForm { afPriority :: Maybe Natural , afField :: Maybe StudyFeaturesId , afText :: Maybe Text - , afFiles :: Maybe (ConduitT () File Handler ()) + , afFiles :: Maybe FileUploads , afRatingVeto :: Bool , afRatingPoints :: Maybe ExamGrade , afRatingComment :: Maybe Text @@ -291,8 +291,9 @@ editApplicationR maId uid cid mAppId afMode allowAction postAction = do , courseApplicationRatingTime = guardOn rated now } let - sinkFile' file = do - fId <- insert file + sinkFile' (Right file) = + insert file >>= sinkFile' . Left + sinkFile' (Left fId) = insert_ $ CourseApplicationFile appId fId forM_ afFiles $ \afFiles' -> runConduit $ transPipe liftHandler afFiles' .| C.mapM_ sinkFile' @@ -308,7 +309,7 @@ editApplicationR maId uid cid mAppId afMode allowAction postAction = do | afmApplicantEdit afMode -> do oldFiles <- Set.fromList . map (courseApplicationFileFile . entityVal) <$> selectList [CourseApplicationFileApplication ==. appId] [] changes <- flip execStateT oldFiles . forM_ afFiles $ \afFiles' -> - let sinkFile' file = do + let sinkFile' (Right file) = do oldFiles' <- lift . E.select . E.from $ \(courseApplicationFile `E.InnerJoin` file') -> do E.on $ courseApplicationFile E.^. CourseApplicationFileFile E.==. file' E.^. FileId E.where_ $ file' E.^. FileTitle E.==. E.val (fileTitle file) @@ -326,7 +327,12 @@ editApplicationR maId uid cid mAppId afMode allowAction postAction = do fId <- lift $ insert file lift . insert_ $ CourseApplicationFile appId fId modify $ Set.insert fId - in runConduit $ transPipe liftHandler afFiles' .| C.mapM_ sinkFile' + sinkFile' (Left fId) + | fId `Set.member` oldFiles = modify $ Set.delete fId + | otherwise = do + lift . insert_ $ CourseApplicationFile appId fId + modify $ Set.insert fId + in runConduit $ transPipe liftHandler afFiles' .| C.mapM_ sinkFile' deleteCascadeWhere [ FileId <-. Set.toList (oldFiles `Set.intersection` changes) ] return changes | otherwise diff --git a/src/Handler/Common.hs b/src/Handler/Common.hs index f11a76cfb..da1330be9 100644 --- a/src/Handler/Common.hs +++ b/src/Handler/Common.hs @@ -1,5 +1,8 @@ -- | Common handler functions. -module Handler.Common where +module Handler.Common + ( getFaviconR + , getRobotsR + ) where import Data.FileEmbed (embedFile) import Import hiding (embedFile) diff --git a/src/Handler/Corrections.hs b/src/Handler/Corrections.hs index 4d2c37aba..8a39796fc 100644 --- a/src/Handler/Corrections.hs +++ b/src/Handler/Corrections.hs @@ -57,6 +57,8 @@ import qualified Control.Monad.State.Class as State import Data.Foldable (foldrM) +import qualified Data.Conduit.List as C + type CorrectionTableExpr = (E.SqlExpr (Entity Course) `E.InnerJoin` E.SqlExpr (Entity Sheet) `E.InnerJoin` E.SqlExpr (Entity Submission)) `E.LeftOuterJoin` E.SqlExpr (Maybe (Entity User)) @@ -215,6 +217,9 @@ colPointsField = sortable (Just "rating") (i18nCell MsgColumnRatingPoints) $ for _other -> over (_1.mapped) (_2 .~) . over _2 fvInput <$> mopt (pointsFieldMax $ preview (_grading . _maxPoints) sheetType) (fsUniq mkUnique "points") (Just submissionRatingPoints) ) +colMaxPointsField :: Colonnade Sortable CorrectionTableData (DBCell _ (FormResult (DBFormResult SubmissionId (a, Maybe Points, b) CorrectionTableData))) +colMaxPointsField = sortable (Just "sheet-type") (i18nCell MsgSheetType) $ i18nCell . (\DBRow{ dbrOutput=(_, Entity _ Sheet{sheetType}, _, _, _, _) } -> sheetType) + colCommentField :: Colonnade Sortable CorrectionTableData (DBCell _ (FormResult (DBFormResult SubmissionId (a, b, Maybe Text) CorrectionTableData))) colCommentField = sortable (Just "comment") (i18nCell MsgRatingComment) $ fmap (cellAttrs <>~ [("style","width:60%")]) $ formCell id (\DBRow{ dbrOutput=(Entity subId _, _, _, _, _, _) } -> return subId) @@ -274,6 +279,13 @@ makeCorrectionsTable whereClause dbtColonnade dbtFilterUI psValidator dbtProj' d , ( "rating" , SortColumn $ \((_ `E.InnerJoin` _ `E.InnerJoin` submission) `E.LeftOuterJoin` _) -> submission E.^. SubmissionRatingPoints ) + , ( "sheet-type" + , SortColumns $ \((_ `E.InnerJoin` sheet `E.InnerJoin` _) `E.LeftOuterJoin` _) -> + [ SomeExprValue ((sheet E.^. SheetType) E.->. "type" :: E.SqlExpr (E.Value Value)) + , SomeExprValue (((sheet E.^. SheetType) E.->. "grading" :: E.SqlExpr (E.Value Value)) E.->. "max" :: E.SqlExpr (E.Value Value)) + , SomeExprValue (((sheet E.^. SheetType) E.->. "grading" :: E.SqlExpr (E.Value Value)) E.->. "passing" :: E.SqlExpr (E.Value Value)) + ] + ) , ( "israted" , SortColumn $ \((_ `E.InnerJoin` _ `E.InnerJoin` submission) `E.LeftOuterJoin` _) -> E.not_ . E.isNothing $ submission E.^. SubmissionRatingTime ) @@ -367,11 +379,22 @@ makeCorrectionsTable whereClause dbtColonnade dbtFilterUI psValidator dbtProj' d E.where_ $ (\f -> f user $ Set.singleton needle) $ E.mkContainsFilter (E.^. UserMatrikelnummer) ) - -- , ( "pseudonym" - -- , FilterColumn $ E.mkExistsFilter $ \table needle -> E.from $ \(pseudonym) -> do - -- E.where_ $ querySheet table E.^. SheetId E.==. pseudonym E.^. SheetPseudonymSheet - -- E.where_ $ E.mkContainsFilter -- DB only stores Pseudonym == Word24. Conversion not possible in DB. - -- ) + , ( "rating-visible" + , FilterColumn $ \((_ `E.InnerJoin` _ `E.InnerJoin` submission) `E.LeftOuterJoin` _ :: CorrectionTableExpr) criterion -> case getLast (criterion :: Last Bool) of + Nothing -> E.val True :: E.SqlExpr (E.Value Bool) + Just True -> E.isJust $ submission E.^. SubmissionRatingTime + Just False-> E.isNothing $ submission E.^. SubmissionRatingTime + ) + , ( "rating" + , FilterColumn $ \((_ `E.InnerJoin` _ `E.InnerJoin` submission) `E.LeftOuterJoin` _ :: CorrectionTableExpr) pts -> if + | Set.null pts -> E.val True :: E.SqlExpr (E.Value Bool) + | otherwise -> E.maybe (E.val False :: E.SqlExpr (E.Value Bool)) (\p -> p `E.in_` E.valList (Set.toList pts)) (submission E.^. SubmissionRatingPoints) + ) + , ( "comment" + , FilterColumn $ \((_ `E.InnerJoin` _ `E.InnerJoin` submission) `E.LeftOuterJoin` _ :: CorrectionTableExpr) comm -> case getLast (comm :: Last Text) of + Nothing -> E.val True :: E.SqlExpr (E.Value Bool) + Just needle -> E.maybe (E.val False :: E.SqlExpr (E.Value Bool)) (E.isInfixOf $ E.val needle) (submission E.^. SubmissionRatingComment) + ) ] , dbtFilterUI = fromMaybe mempty dbtFilterUI , dbtStyle = def { dbsFilterLayout = maybe (\_ _ _ -> id) (\_ -> defaultDBSFilterLayout) dbtFilterUI } @@ -605,7 +628,7 @@ postCorrectionsR = do , prismAForm (singletonFilter "term" ) mPrev $ aopt (lift `hoistField` selectField termOptions) (fslI MsgTerm) , prismAForm (singletonFilter "school" ) mPrev $ aopt (lift `hoistField` selectField schoolOptions) (fslI MsgCourseSchool) , Map.singleton "sheet-search" . maybeToList <$> aopt (lift `hoistField` textField) (fslI MsgSheet) (Just <$> listToMaybe =<< ((Map.lookup "sheet-search" =<< mPrev) <|> (Map.lookup "sheet" =<< mPrev))) - , prismAForm (singletonFilter "israted" . maybePrism _PathPiece) mPrev $ aopt boolField (fslI MsgRatingTime) + , prismAForm (singletonFilter "israted" . maybePrism _PathPiece) mPrev $ aopt (boolField . Just $ SomeMessage MsgBoolIrrelevant) (fslI MsgRatingTime) ] courseOptions = runDB $ do courses <- selectList [] [Asc CourseShorthand] >>= filterM (\(Entity _ Course{..}) -> (== Authorized) <$> evalAccessCorrector courseTerm courseSchool courseShorthand) @@ -650,8 +673,8 @@ postCCorrectionsR tid ssh csh = do -- "pseudonym" TODO DB only stores Word24 , Map.singleton "sheet-search" . maybeToList <$> aopt textField (fslI MsgSheet) (Just <$> listToMaybe =<< ((Map.lookup "sheet-search" =<< mPrev) <|> (Map.lookup "sheet" =<< mPrev))) , prismAForm (singletonFilter "corrector-name-email") mPrev $ aopt textField (fslI MsgCorrector) - , prismAForm (singletonFilter "isassigned" . maybePrism _PathPiece) mPrev $ aopt boolField (fslI MsgHasCorrector) - , prismAForm (singletonFilter "israted" . maybePrism _PathPiece) mPrev $ aopt boolField (fslI MsgRatingTime) + , prismAForm (singletonFilter "isassigned" . maybePrism _PathPiece) mPrev $ aopt (boolField . Just $ SomeMessage MsgBoolIrrelevant) (fslI MsgHasCorrector) + , prismAForm (singletonFilter "israted" . maybePrism _PathPiece) mPrev $ aopt (boolField . Just $ SomeMessage MsgBoolIrrelevant) (fslI MsgRatingTime) ] psValidator = def & defaultPagesize PagesizeAll -- Assisstant always want to see them all at once anyway correctionsR whereClause colonnade filterUI psValidator $ Map.fromList @@ -681,8 +704,8 @@ postSSubsR tid ssh csh shn = do [ prismAForm (singletonFilter "user-name-email") mPrev $ aopt textField (fslI MsgCourseMembers) , prismAForm (singletonFilter "user-matriclenumber") mPrev $ aopt textField (fslI MsgMatrikelNr) , prismAForm (singletonFilter "corrector-name-email") mPrev $ aopt textField (fslI MsgCorrector) - , prismAForm (singletonFilter "isassigned" . maybePrism _PathPiece) mPrev $ aopt boolField (fslI MsgHasCorrector) - , prismAForm (singletonFilter "israted" . maybePrism _PathPiece) mPrev $ aopt boolField (fslI MsgRatingTime) + , prismAForm (singletonFilter "isassigned" . maybePrism _PathPiece) mPrev $ aopt (boolField . Just $ SomeMessage MsgBoolIrrelevant) (fslI MsgHasCorrector) + , prismAForm (singletonFilter "israted" . maybePrism _PathPiece) mPrev $ aopt (boolField . Just $ SomeMessage MsgBoolIrrelevant) (fslI MsgRatingTime) -- "pseudonym" TODO DB only stores Word24 ] psValidator = def & defaultPagesize PagesizeAll -- Assisstant always want to see them all at once anyway @@ -714,13 +737,14 @@ postCorrectionR tid ssh csh shn cid = do results <- runDB $ correctionData tid ssh csh shn sub + MsgRenderer mr <- getMsgRenderer case results of [(Entity _ Course{..}, Entity _ Sheet{..}, Entity _ subm@Submission{..}, corrector)] -> do let ratingComment = fmap Text.strip submissionRatingComment >>= (\c -> c <$ guard (not $ null c)) pointsForm = case sheetType of NotGraded -> pure Nothing _otherwise -> aopt (pointsFieldMax $ preview (_grading . _maxPoints) sheetType) - (fslpI MsgRatingPoints "Punktezahl" & setTooltip sheetType) + (fslpI MsgRatingPoints (mr MsgPointsPlaceholder) & setTooltip sheetType) (Just submissionRatingPoints) ((corrResult, corrForm'), corrEncoding) <- runFormPost . identifyForm FIDcorrection . renderAForm FormStandard $ (,,) @@ -769,14 +793,13 @@ postCorrectionR tid ssh csh shn cid = do formResult uploadResult $ \fileUploads -> do uid <- requireAuthId - res <- msgSubmissionErrors . runDBJobs . runConduit $ transPipe (lift . lift) fileUploads .| extractRatingsMsg .| sinkSubmission uid (Right sub) True + res <- msgSubmissionErrors . runDBJobs . runConduit $ transPipe (lift . lift) fileUploads .| C.mapM (either get404 return) .| extractRatingsMsg .| sinkSubmission uid (Right sub) True case res of Nothing -> return () -- ErrorMessages are already added by msgSubmissionErrors (Just _) -> do addMessageI Success MsgRatingFilesUpdated redirect $ CSubmissionR tid ssh csh shn cid CorrectionR - mr <- getMessageRender let sheetTypeDesc = mr sheetType heading = MsgCorrectionHead tid ssh csh shn cid headingWgt = [whamlet| @@ -818,7 +841,7 @@ postCorrectionsUploadR = do FormFailure errs -> mapM_ (addMessage Error . toHtml) errs FormSuccess files -> do uid <- requireAuthId - mbSubs <- msgSubmissionErrors . runDBJobs . runConduit $ transPipe (lift . lift) files .| extractRatingsMsg .| sinkMultiSubmission uid True + mbSubs <- msgSubmissionErrors . runDBJobs . runConduit $ transPipe (lift . lift) files .| C.mapM (either get404 return) .| extractRatingsMsg .| sinkMultiSubmission uid True case mbSubs of Nothing -> return () (Just subs) @@ -868,9 +891,10 @@ postCorrectionsCreateR = do , optionInternalValue = sid , optionExternalValue = toPathPiece (cID :: CryptoUUIDSheet) } + MsgRenderer mr <- getMsgRenderer ((pseudonymRes, pseudonymWidget), pseudonymEncoding) <- runFormPost . renderAForm FormStandard $ (,) <$> areq (selectField sheetOptions) (fslI MsgPseudonymSheet) Nothing - <*> (textToList <$> areq textareaField (fslpI MsgCorrectionPseudonyms "Pseudonyme" & setTooltip MsgCorrectionPseudonymsTip) Nothing) + <*> (textToList <$> areq textareaField (fslpI MsgCorrectionPseudonyms (mr MsgPseudonyms) & setTooltip MsgCorrectionPseudonymsTip) Nothing) case pseudonymRes of FormMissing -> return () @@ -980,7 +1004,8 @@ postCorrectionsCreateR = do , formEncoding = pseudonymEncoding } - defaultLayout + siteLayoutMsg MsgCorrCreate $ do + setTitleI MsgCorrCreate $(widgetFile "corrections-create") where partitionEithers' :: [[Either a b]] -> ([[b]], [a]) @@ -1010,8 +1035,28 @@ postCorrectionsGradeR = do , colRated , colRatedField , colPointsField + , colMaxPointsField , colCommentField ] -- Continue here + filterUI = Just $ \mPrev -> mconcat + [ prismAForm (singletonFilter "course" ) mPrev $ aopt (lift `hoistField` selectField courseOptions) (fslI MsgCourse) + , prismAForm (singletonFilter "term" ) mPrev $ aopt (lift `hoistField` selectField termOptions) (fslI MsgTerm) + , prismAForm (singletonFilter "school" ) mPrev $ aopt (lift `hoistField` selectField schoolOptions) (fslI MsgCourseSchool) + , Map.singleton "sheet-search" . maybeToList <$> aopt (lift `hoistField` textField) (fslI MsgSheet) (Just <$> listToMaybe =<< ((Map.lookup "sheet-search" =<< mPrev) <|> (Map.lookup "sheet" =<< mPrev))) + , prismAForm (singletonFilter "israted" . maybePrism _PathPiece) mPrev $ aopt (boolField . Just $ SomeMessage MsgBoolIrrelevant) (fslI MsgRatingTime) + , prismAForm (singletonFilter "rating-visible" . maybePrism _PathPiece) mPrev $ aopt (boolField . Just $ SomeMessage MsgBoolIrrelevant) (fslI MsgRatingDone) + , prismAForm (singletonFilter "rating" . maybePrism _PathPiece) mPrev $ aopt (lift `hoistField` pointsField) (fslI MsgColumnRatingPoints) + , Map.singleton "comment" . maybeToList <$> aopt (lift `hoistField` textField) (fslI MsgRatingComment) (Just <$> listToMaybe =<< (Map.lookup "comment" =<< mPrev)) + ] + courseOptions = runDB $ do + courses <- selectList [] [Asc CourseShorthand] >>= filterM (\(Entity _ Course{..}) -> (== Authorized) <$> evalAccessCorrector courseTerm courseSchool courseShorthand) + optionsPairs $ map (id &&& id) $ nub $ map (CI.original . courseShorthand . entityVal) courses + termOptions = runDB $ do + courses <- selectList [] [Asc CourseTerm] >>= filterM (\(Entity _ Course{..}) -> (== Authorized) <$> evalAccessCorrector courseTerm courseSchool courseShorthand) + optionsPairs $ map (id &&& id) $ nub $ map (termToText . unTermKey . courseTerm . entityVal) courses + schoolOptions = runDB $ do + courses <- selectList [] [Asc CourseSchool] >>= filterM (\(Entity _ Course{..}) -> (== Authorized) <$> evalAccessCorrector courseTerm courseSchool courseShorthand) + optionsPairs $ map (id &&& id) $ nub $ map (CI.original . unSchoolKey . courseSchool . entityVal) courses psValidator = def & defaultSorting [SortDescBy "ratingtime"] :: PSValidator (MForm (HandlerFor UniWorX)) (FormResult (DBFormResult SubmissionId (Bool, Maybe Points, Maybe Text) CorrectionTableData)) unFormResult = getDBFormResult $ \DBRow{ dbrOutput = (Entity _ sub@Submission{..}, _, _, _, _, _) } -> (submissionRatingDone sub, submissionRatingPoints, submissionRatingComment) @@ -1020,7 +1065,7 @@ postCorrectionsGradeR = do void . assertM (== Authorized) . lift $ evalAccessDB (CSubmissionR tid ssh csh shn cID CorrectionR) True return i - (fmap unFormResult -> tableRes, table) <- runDB $ makeCorrectionsTable whereClause displayColumns mempty psValidator dbtProj' $ def + (fmap unFormResult -> tableRes, table) <- runDB $ makeCorrectionsTable whereClause displayColumns filterUI psValidator dbtProj' $ def { dbParamsFormAction = Just $ SomeRoute CorrectionsGradeR } @@ -1045,7 +1090,8 @@ postCorrectionsGradeR = do content = Right $(widgetFile "messages/correctionsUploaded") unless (null subs') $ addMessageModal Success trigger content - defaultLayout $ + siteLayoutMsg MsgCorrectionsGrade $ do + setTitleI MsgCorrectionsGrade $(widgetFile "corrections-grade") diff --git a/src/Handler/Course/Edit.hs b/src/Handler/Course/Edit.hs index 81dbe8573..b8fcf5748 100644 --- a/src/Handler/Course/Edit.hs +++ b/src/Handler/Course/Edit.hs @@ -256,7 +256,6 @@ makeCourseForm miButtonAction template = identifyForm FIDcourse . validateFormDB optionalActionW' (bool mforcedJust mpopt mayChange) allocationForm' (fslI MsgCourseAllocationParticipate & setTooltip MsgCourseAllocationParticipateTip) (is _Just . cfAllocation <$> template) - -- TODO: internationalization -- let autoUnzipInfo = [|Entpackt hochgeladene Zip-Dateien (*.zip) automatisch und fügt den Inhalt dem Stamm-Verzeichnis der Abgabe hinzu. TODO|] (result, widget) <- flip (renderAForm FormStandard) html $ CourseForm @@ -267,9 +266,9 @@ makeCourseForm miButtonAction template = identifyForm FIDcourse . validateFormDB & setTooltip MsgCourseShorthandUnique) (cfShort <$> template) <*> areq (schoolFieldFor userSchools) (fslI MsgCourseSchool) (cfSchool <$> template) <*> areq termsField (fslI MsgCourseSemester) (cfTerm <$> template) - <*> aopt htmlField (fslpI MsgCourseDescription "Bitte mindestens die Modulbeschreibung angeben" + <*> aopt htmlField (fslpI MsgCourseDescription (mr MsgCourseDescriptionPlaceholder) & setTooltip MsgCourseDescriptionTip) (cfDesc <$> template) - <*> aopt (urlField & cfStrip) (fslpI MsgCourseHomepageExternal "Optionale externe URL") + <*> aopt (urlField & cfStrip) (fslpI MsgCourseHomepageExternal (mr MsgCourseHomepageExternalPlaceholder)) (cfLink <$> template) <*> apopt checkBoxField (fslI MsgMaterialFree) (cfMatFree <$> template) <* aformSection MsgCourseFormSectionRegistration diff --git a/src/Handler/Course/LecturerInvite.hs b/src/Handler/Course/LecturerInvite.hs index 753bd7c10..44b27ce64 100644 --- a/src/Handler/Course/LecturerInvite.hs +++ b/src/Handler/Course/LecturerInvite.hs @@ -66,7 +66,7 @@ lecturerInvitationConfig = InvitationConfig{..} invitationHeading (Entity _ Course{..}) _ = return . SomeMessage $ MsgCourseLecInviteHeading $ CI.original courseName invitationExplanation _ _ = return [ihamlet|_{SomeMessage MsgCourseLecInviteExplanation}|] invitationTokenConfig _ _ = do - itAuthority <- liftHandler requireAuthId + itAuthority <- Right <$> liftHandler requireAuthId return $ InvitationTokenConfig itAuthority Nothing Nothing Nothing invitationRestriction _ _ = return Authorized invitationForm _ (InvDBDataLecturer mlType, _) _ = hoistAForm liftHandler $ toJunction <$> case mlType of diff --git a/src/Handler/Course/List.hs b/src/Handler/Course/List.hs index 7e815fba2..a9075cee3 100644 --- a/src/Handler/Course/List.hs +++ b/src/Handler/Course/List.hs @@ -176,7 +176,7 @@ makeCourseTable whereClause colChoices psValidator = do , Just $ prismAForm (singletonFilter "lecturer") mPrev $ aopt textField (fslI MsgCourseLecturer) , Just $ prismAForm (singletonFilter "search") mPrev $ aopt textField (fslI MsgCourseFilterSearch) , Just $ prismAForm (singletonFilter "openregistration" . maybePrism _PathPiece) mPrev $ fmap (\x -> if isJust x && not (fromJust x) then Nothing else x) . aopt checkBoxField (fslI MsgCourseRegisterOpen) - , muid $> prismAForm (singletonFilter "registered" . maybePrism _PathPiece) mPrev (aopt boolField (fslI MsgCourseFilterRegistered)) + , muid $> prismAForm (singletonFilter "registered" . maybePrism _PathPiece) mPrev (aopt (boolField . Just $ SomeMessage MsgBoolIrrelevant) (fslI MsgCourseFilterRegistered)) ] , dbtStyle = def { dbsFilterLayout = defaultDBSFilterLayout diff --git a/src/Handler/Course/ParticipantInvite.hs b/src/Handler/Course/ParticipantInvite.hs index 6e2baca9d..280a69d6f 100644 --- a/src/Handler/Course/ParticipantInvite.hs +++ b/src/Handler/Course/ParticipantInvite.hs @@ -83,7 +83,7 @@ participantInvitationConfig = InvitationConfig{..} invitationHeading (Entity _ Course{..}) _ = return . SomeMessage $ MsgCourseParticipantInviteHeading $ CI.original courseName invitationExplanation _ _ = return [ihamlet|_{SomeMessage MsgCourseParticipantInviteExplanation}|] invitationTokenConfig _ _ = do - itAuthority <- liftHandler requireAuthId + itAuthority <- Right <$> liftHandler requireAuthId return $ InvitationTokenConfig itAuthority Nothing Nothing Nothing invitationRestriction _ _ = return Authorized invitationForm (Entity _ Course{..}) _ uid = hoistAForm lift . wFormToAForm $ do diff --git a/src/Handler/Course/Register.hs b/src/Handler/Course/Register.hs index 5da5f6735..fc687713a 100644 --- a/src/Handler/Course/Register.hs +++ b/src/Handler/Course/Register.hs @@ -42,7 +42,7 @@ instance Button UniWorX ButtonCourseRegister where data CourseRegisterForm = CourseRegisterForm { crfStudyFeatures :: Maybe StudyFeaturesId , crfApplicationText :: Maybe Text - , crfApplicationFiles :: Maybe (ConduitT () File Handler ()) + , crfApplicationFiles :: Maybe FileUploads } courseRegisterForm :: (MonadHandler m, HandlerSite m ~ UniWorX) => Entity Course -> m (AForm Handler CourseRegisterForm, ButtonCourseRegister) @@ -195,7 +195,7 @@ postCRegisterR tid ssh csh = do whenIsJust appRes $ audit . TransactionCourseApplicationEdit cid uid whenIsJust ((,) <$> appRes <*> crfApplicationFiles) $ \(appId, fSource) -> do - runConduit $ transPipe liftHandler fSource .| C.mapM_ (\f -> insert f >>= insert_ . CourseApplicationFile appId) + runConduit $ transPipe liftHandler fSource .| C.mapM_ (insert_ . CourseApplicationFile appId <=< either return insert) return appRes | otherwise = return $ Just () diff --git a/src/Handler/CryptoIDDispatch.hs b/src/Handler/CryptoIDDispatch.hs index 8a34cde8d..9270fffe2 100644 --- a/src/Handler/CryptoIDDispatch.hs +++ b/src/Handler/CryptoIDDispatch.hs @@ -66,7 +66,7 @@ instance (CryptoRoute ciphertext plaintext, Dispatch ciphertext ps) => Dispatch getCryptoUUIDDispatchR :: UUID -> Handler () -getCryptoUUIDDispatchR uuid = dispatchID p uuid >>= maybe notFound (redirectWith found302) +getCryptoUUIDDispatchR uuid = dispatchID p uuid >>= maybe notFound (redirectAccessWith movedPermanently301) where p :: Proxy '[ SubmissionId , UserId @@ -74,7 +74,7 @@ getCryptoUUIDDispatchR uuid = dispatchID p uuid >>= maybe notFound (redirectWith p = Proxy getCryptoFileNameDispatchR :: CI FilePath -> Handler () -getCryptoFileNameDispatchR path = dispatchID p path >>= maybe notFound (redirectWith found302) +getCryptoFileNameDispatchR path = dispatchID p path >>= maybe notFound (redirectAccessWith movedPermanently301) where p :: Proxy '[ SubmissionId ] p = Proxy diff --git a/src/Handler/Exam/CorrectorInvite.hs b/src/Handler/Exam/CorrectorInvite.hs index 8314a8ca1..d207ff9ef 100644 --- a/src/Handler/Exam/CorrectorInvite.hs +++ b/src/Handler/Exam/CorrectorInvite.hs @@ -67,7 +67,7 @@ examCorrectorInvitationConfig = InvitationConfig{..} invitationHeading (Entity _ Exam{..}) _ = return . SomeMessage $ MsgExamCorrectorInviteHeading examName invitationExplanation _ _ = return [ihamlet|_{SomeMessage MsgExamCorrectorInviteExplanation}|] invitationTokenConfig _ _ = do - itAuthority <- liftHandler requireAuthId + itAuthority <- Right <$> liftHandler requireAuthId return $ InvitationTokenConfig itAuthority Nothing Nothing Nothing invitationRestriction _ _ = return Authorized invitationForm _ _ _ = pure (JunctionExamCorrector, ()) diff --git a/src/Handler/Exam/RegistrationInvite.hs b/src/Handler/Exam/RegistrationInvite.hs index 8a157f72d..cfd109f94 100644 --- a/src/Handler/Exam/RegistrationInvite.hs +++ b/src/Handler/Exam/RegistrationInvite.hs @@ -77,7 +77,7 @@ examRegistrationInvitationConfig = InvitationConfig{..} invitationHeading (Entity _ Exam{..}) _ = return . SomeMessage $ MsgExamRegistrationInviteHeading examName invitationExplanation _ _ = return [ihamlet|_{SomeMessage MsgExamRegistrationInviteExplanation}|] invitationTokenConfig _ (InvDBDataExamRegistration{..}, _) = do - itAuthority <- liftHandler requireAuthId + itAuthority <- Right <$> liftHandler requireAuthId let itExpiresAt = Just $ Just invDBExamRegistrationDeadline itAddAuth | not invDBExamRegistrationCourseRegister diff --git a/src/Handler/ExamOffice/Exam.hs b/src/Handler/ExamOffice/Exam.hs index 87f5bbf10..320822663 100644 --- a/src/Handler/ExamOffice/Exam.hs +++ b/src/Handler/ExamOffice/Exam.hs @@ -369,7 +369,7 @@ postEGradesR tid ssh csh examn = do , fltrStudyDegreeUI , fltrStudyFeaturesSemesterUI , fltrExamResultPointsUI examShowGrades - , \mPrev -> prismAForm (singletonFilter "is-synced" . maybePrism _PathPiece) mPrev $ aopt boolField (fslI MsgExamUserSynchronised) + , \mPrev -> prismAForm (singletonFilter "is-synced" . maybePrism _PathPiece) mPrev $ aopt (boolField . Just $ SomeMessage MsgBoolIrrelevant) (fslI MsgExamUserSynchronised) ] dbtStyle = def { dbsFilterLayout = defaultDBSFilterLayout } dbtParams = DBParamsForm diff --git a/src/Handler/ExamOffice/Users.hs b/src/Handler/ExamOffice/Users.hs index 0a5d3b9bd..3e688c936 100644 --- a/src/Handler/ExamOffice/Users.hs +++ b/src/Handler/ExamOffice/Users.hs @@ -67,7 +67,7 @@ examOfficeUserInvitationConfig = InvitationConfig{..} return . SomeMessage $ MsgExamOfficeUserInviteHeading userDisplayName invitationExplanation _ _ = return [ihamlet|_{SomeMessage MsgExamOfficeUserInviteExplanation}|] invitationTokenConfig _ _ = do - itAuthority <- liftHandler requireAuthId + itAuthority <- Right <$> liftHandler requireAuthId let itExpiresAt = Nothing itStartsAt = Nothing itAddAuth = Nothing diff --git a/src/Handler/Home.hs b/src/Handler/Home.hs index 0382fab4a..7608b1195 100644 --- a/src/Handler/Home.hs +++ b/src/Handler/Home.hs @@ -32,7 +32,7 @@ homeUpcomingSheets uid = do , E.SqlExpr (E.Value SchoolId) , E.SqlExpr (E.Value CourseShorthand) , E.SqlExpr (E.Value SheetName) - , E.SqlExpr (E.Value UTCTime) + , E.SqlExpr (E.Value (Maybe UTCTime)) , E.SqlExpr (E.Value (Maybe SubmissionId))) tableData ((participant `E.InnerJoin` course `E.InnerJoin` sheet) `E.LeftOuterJoin` (submission `E.InnerJoin` subuser)) = do E.on $ submission E.?. SubmissionId E.==. subuser E.?. SubmissionUserSubmission @@ -41,7 +41,7 @@ homeUpcomingSheets uid = do E.on $ course E.^. CourseId E.==. sheet E.^. SheetCourse E.on $ course E.^. CourseId E.==. participant E.^. CourseParticipantCourse E.where_ $ participant E.^. CourseParticipantUser E.==. E.val uid - E.&&. sheet E.^. SheetActiveTo E.>=. E.val cTime + E.&&. E.maybe E.true (E.>=. E.val cTime) (sheet E.^. SheetActiveTo) return ( course E.^. CourseTerm , course E.^. CourseSchool @@ -55,7 +55,7 @@ homeUpcomingSheets uid = do , E.Value SchoolId , E.Value CourseShorthand , E.Value SheetName - , E.Value UTCTime + , E.Value (Maybe UTCTime) , E.Value (Maybe SubmissionId) )) (DBCell Handler ()) @@ -70,8 +70,8 @@ homeUpcomingSheets uid = do anchorCell (CourseR tid ssh csh CShowR) csh , sortable (Just "sheet") (i18nCell MsgSheet) $ \DBRow{ dbrOutput=(E.Value tid, E.Value ssh, E.Value csh, E.Value shn, _, _) } -> anchorCell (CSheetR tid ssh csh shn SShowR) shn - , sortable (Just "deadline") (i18nCell MsgDeadline) $ \DBRow{ dbrOutput=(_, _, _, _, E.Value deadline, _) } -> - cell $ formatTime SelFormatDateTime deadline >>= toWidget + , sortable (Just "deadline") (i18nCell MsgDeadline) $ \DBRow{ dbrOutput=(_, _, _, _, E.Value mDeadline, _) } -> + maybe mempty (cell . formatTimeW SelFormatDateTime) mDeadline , sortable (Just "done") (i18nCell MsgDone) $ \DBRow{ dbrOutput=(E.Value tid, E.Value ssh, E.Value csh, E.Value shn, _, E.Value mbsid) } -> case mbsid of Nothing -> cell $ do diff --git a/src/Handler/Info.hs b/src/Handler/Info.hs index 751bbe171..69aa8437b 100644 --- a/src/Handler/Info.hs +++ b/src/Handler/Info.hs @@ -2,6 +2,10 @@ module Handler.Info where import Import import Handler.Utils +import Handler.Info.TH + +import qualified Data.Map as Map +import qualified Data.CaseInsensitive as CI import Development.GitRev @@ -67,3 +71,18 @@ getInfoLecturerR = if currentTime > expiryTime then mempty else toWidget [whamlet| ^{iconTooltip tooltipNew (Just IconNew) False} |] + +getGlossaryR :: Handler Html +getGlossaryR = + siteLayoutMsg' MsgGlossaryTitle $ do + setTitleI MsgGlossaryTitle + MsgRenderer mr <- getMsgRenderer + let + entries' = sortOn (CI.mk . view _2) $ do + (k, v) <- Map.toList entries + msg <- maybeToList $ Map.lookup k msgMap + return (k, mr msg, v) + $(widgetFile "glossary") + where + entries = $(i18nWidgetFiles "glossary") + msgMap = $(glossaryTerms "glossary") diff --git a/src/Handler/Info/TH.hs b/src/Handler/Info/TH.hs new file mode 100644 index 000000000..25c55bdb6 --- /dev/null +++ b/src/Handler/Info/TH.hs @@ -0,0 +1,23 @@ +module Handler.Info.TH + ( glossaryTerms + ) where + +import Import +import Handler.Utils.I18n + +import Language.Haskell.TH + +import qualified Data.Char as Char + +import qualified Data.Map.Strict as Map +import qualified Data.Text as Text + + +glossaryTerms :: FilePath -> Q Exp +glossaryTerms basename = do + translationsAvailable <- i18nWidgetFilesAvailable' basename + let terms = Map.mapWithKey (\k _ -> "Msg" <> unPathPiece k) translationsAvailable + [e|Map.fromList $(listE . map (\(int, msg) -> tupE [litE . stringL $ repack int, conE $ mkName msg]) $ Map.toList terms)|] + where + unPathPiece :: Text -> String + unPathPiece = repack . mconcat . map (over _head Char.toUpper) . Text.splitOn "-" diff --git a/src/Handler/Metrics.hs b/src/Handler/Metrics.hs new file mode 100644 index 000000000..b51d8ebe9 --- /dev/null +++ b/src/Handler/Metrics.hs @@ -0,0 +1,45 @@ +module Handler.Metrics + ( getMetricsR + ) where + +import Import hiding (Info) + +import Prometheus +import qualified Network.Wai.Middleware.Prometheus as Prometheus + +import qualified Data.Text as Text +import qualified Data.HashSet as HashSet + + +getMetricsR :: Handler TypedContent +getMetricsR = selectRep $ do + provideRep (sendWaiApplication Prometheus.metricsApp :: Handler Text) + provideRep metricsHtml + provideRep $ collectMetrics >>= returnJson + where + metricsHtml :: Handler Html + metricsHtml = do + samples <- collectMetrics + + metricsToken <- runMaybeT . hoist runDB $ do + uid <- MaybeT maybeAuthId + guardM . lift . existsBy $ UniqueUserGroupMember UserGroupMetrics uid + + encodeToken =<< bearerToken (Left $ toJSON UserGroupMetrics) (Just $ HashSet.singleton MetricsR) Nothing (Just Nothing) Nothing + + defaultLayout $ do + setTitleI MsgTitleMetrics + $(widgetFile "metrics") + + metricBasename base sName + | Just suffix <- Text.stripPrefix base sName + = if | Just suffix' <- Text.stripPrefix "_" suffix + -> suffix' + | otherwise + -> suffix + | otherwise + = sName + getLabels = nub . concatMap (\(Sample _ lPairs _) -> lPairs ^.. folded . _1) + singleSample base [Sample sName lPairs sValue] + | sName == base = Just (lPairs, sValue) + singleSample _ _ = Nothing diff --git a/src/Handler/Profile.hs b/src/Handler/Profile.hs index 164d575c2..d6de8d24e 100644 --- a/src/Handler/Profile.hs +++ b/src/Handler/Profile.hs @@ -5,6 +5,7 @@ module Handler.Profile , getUserNotificationR, postUserNotificationR , getSetDisplayEmailR, postSetDisplayEmailR , getCsvOptionsR, postCsvOptionsR + , postLangR ) where import Import @@ -22,6 +23,8 @@ import qualified Data.Set as Set import qualified Database.Esqueleto as E import qualified Database.Esqueleto.Utils as E -- import Database.Esqueleto ((^.)) +import qualified Data.Text as Text +import Data.List (inits) import qualified Data.CaseInsensitive as CI @@ -76,15 +79,16 @@ instance RenderMessage UniWorX NotificationTriggerKind where makeSettingForm :: Maybe SettingsForm -> Form SettingsForm makeSettingForm template html = do + MsgRenderer mr <- getMsgRenderer (result, widget) <- flip (renderAForm FormStandard) html $ SettingsForm <$ aformSection MsgFormPersonalAppearance <*> areq (textField & cfStrip) (fslI MsgUserDisplayName & setTooltip MsgUserDisplayNameRulesBelow) (stgDisplayName <$> template) <*> areq (emailField & cfStrip & cfCI) (fslI MsgUserDisplayEmail & setTooltip MsgUserDisplayEmailTip) (stgDisplayEmail <$> template) <* aformSection MsgFormCosmetics - <*> areq (natFieldI $ MsgNatField "Favoriten") - (fslpI MsgFavourites "Anzahl Favoriten" & setTooltip MsgFavouritesTip) (stgMaxFavourites <$> template) - <*> areq (natFieldI $ MsgNatField "Favoriten-Semester") - (fslpI MsgFavouriteSemesters "Anzahl Semester") (stgMaxFavouriteTerms <$> template) + <*> areq (natFieldI MsgFavouritesNotNatural) + (fslpI MsgFavourites (mr MsgFavouritesPlaceholder) & setTooltip MsgFavouritesTip) (stgMaxFavourites <$> template) + <*> areq (natFieldI MsgFavouritesSemestersNotNatural) + (fslpI MsgFavouriteSemesters (mr MsgFavouritesSemestersPlaceholder)) (stgMaxFavouriteTerms <$> template) <*> areq (selectField . return $ mkOptionList themeList) (fslI MsgTheme) { fsId = Just "theme-select" } (stgTheme <$> template) <*> areq (selectField $ dateTimeFormatOptions SelFormatDateTime) (fslI MsgDateTimeFormat) (stgDateTime <$> template) @@ -315,7 +319,7 @@ postProfileR = do tResetTime <- traverse (formatTime SelFormatDateTime) userTokensIssuedAfter siteLayout [whamlet|_{MsgProfileFor} ^{nameWidget userDisplayName userSurname}|] $ do - setTitle . toHtml $ "Profil " <> userIdent + setTitleI MsgProfileTitle let settingsForm = wrapForm formWidget FormSettings { formMethod = POST @@ -366,10 +370,11 @@ makeProfileData (Entity uid User{..}) = do submissionTable <- mkSubmissionTable uid -- Tabelle mit allen Abgaben und Abgabe-Gruppen submissionGroupTable <- mkSubmissionGroupTable uid -- Tabelle mit allen Abgabegruppen correctionsTable <- mkCorrectionsTable uid -- Tabelle mit allen Korrektor-Aufgaben - let examTable = [whamlet|Prüfungen werden hier momentan leider noch nicht unterstützt.|] - let ownTutorialTable = [whamlet|Übungsgruppen werden momentan leider noch nicht unterstützt.|] - let tutorialTable = [whamlet|Übungsgruppen werden momentan leider noch nicht unterstützt.|] + let examTable = [whamlet|_{MsgPersonalInfoExamAchievementsWip}|] + let ownTutorialTable = [whamlet|_{MsgPersonalInfoOwnTutorialsWip}|] + let tutorialTable = [whamlet|_{MsgPersonalInfoTutorialsWip}|] lastLogin <- traverse (formatTime SelFormatDateTime) userLastAuthentication + let profileRemarks = $(i18nWidgetFile "profile-remarks") return $(widgetFile "profileData") @@ -837,3 +842,18 @@ postCsvOptionsR = do , formEncoding = optionsEnctype , formAttrs = [ asyncSubmitAttr | isModal ] } + +postLangR :: Handler () +postLangR = do + ((langRes, _), _) <- runFormPost $ identifyForm FIDLanguage langForm + + formResult langRes $ \(lang, route) -> do + lang' <- runDB . updateUserLanguage $ Just lang + + app <- getYesod + let mr | Just lang'' <- lang' = renderMessage app . map (Text.intercalate "-") . reverse . inits $ Text.splitOn "-" lang'' + | otherwise = renderMessage app [] + addMessage Success . toHtml $ mr MsgLanguageChanged + redirect route + + invalidArgs ["Language form required"] diff --git a/src/Handler/Sheet.hs b/src/Handler/Sheet.hs index 543865af2..bc5c308d4 100644 --- a/src/Handler/Sheet.hs +++ b/src/Handler/Sheet.hs @@ -55,6 +55,8 @@ import Text.Hamlet (ihamlet) import System.FilePath (addExtension) +import Data.Time.Clock.System (systemEpochDay) + {- * Implement Handlers @@ -62,22 +64,38 @@ import System.FilePath (addExtension) * Implement Access in Foundation -} +type Loads = Map (Either UserEmail UserId) (InvitationData SheetCorrector) + data SheetForm = SheetForm { sfName :: SheetName + , sfDescription :: Maybe Html + , sfSheetF, sfHintF, sfSolutionF, sfMarkingF :: Maybe (ConduitT () (Either FileId File) Handler ()) , sfVisibleFrom :: Maybe UTCTime - , sfActiveFrom :: UTCTime - , sfActiveTo :: UTCTime + , sfActiveFrom :: Maybe UTCTime + , sfActiveTo :: Maybe UTCTime , sfHintFrom :: Maybe UTCTime , sfSolutionFrom :: Maybe UTCTime - , sfSheetF, sfHintF, sfSolutionF, sfMarkingF :: Maybe (ConduitT () (Either FileId File) Handler ()) , sfType :: SheetType , sfGrouping :: SheetGroup , sfSubmissionMode :: SubmissionMode - , sfDescription :: Maybe Html + , sfAutoDistribute :: Bool , sfMarkingText :: Maybe Html + , sfCorrectors :: Loads -- Keine SheetId im Formular! } +data ButtonGeneratePseudonym = BtnGenerate + deriving (Enum, Eq, Ord, Bounded, Read, Show, Generic, Typeable) +instance Universe ButtonGeneratePseudonym +instance Finite ButtonGeneratePseudonym + +nullaryPathPiece ''ButtonGeneratePseudonym (camelToPathPiece' 1) + +instance Button UniWorX ButtonGeneratePseudonym where + btnLabel BtnGenerate = [whamlet|_{MsgSheetGeneratePseudonym}|] + btnClasses BtnGenerate = [BCIsButton, BCDefault] + + getFtIdMap :: Key Sheet -> DB (SheetFileType -> Set FileId) getFtIdMap sId = do allfIds <- E.select . E.from $ \(sheetFile `E.InnerJoin` file) -> do @@ -91,40 +109,41 @@ makeSheetForm msId template = identifyForm FIDsheet $ \html -> do oldFileIds <- (return.) <$> case msId of Nothing -> return $ partitionFileType mempty (Just sId) -> liftHandler $ runDB $ getFtIdMap sId - mr <- getMsgRenderer + mr'@(MsgRenderer mr) <- getMsgRenderer ctime <- ceilingQuarterHour <$> liftIO getCurrentTime (result, widget) <- flip (renderAForm FormStandard) html $ SheetForm <$> areq (textField & cfStrip & cfCI) (fslI MsgSheetName) (sfName <$> template) - <* aformSection MsgSheetFormTimes - <*> aopt utcTimeField (fslI MsgSheetVisibleFrom - & setTooltip MsgSheetVisibleFromTip) - ((sfVisibleFrom <$> template) <|> pure (Just ctime)) - <*> areq utcTimeField (fslI MsgSheetActiveFrom - & setTooltip MsgSheetActiveFromTip) - (sfActiveFrom <$> template) - <*> areq utcTimeField (fslI MsgSheetActiveTo) (sfActiveTo <$> template) - <*> aopt utcTimeField (fslpI MsgSheetHintFrom "Datum, sonst nur für Korrektoren" - & setTooltip MsgSheetHintFromTip) (sfHintFrom <$> template) - <*> aopt utcTimeField (fslpI MsgSheetSolutionFrom "Datum, sonst nur für Korrektoren" - & setTooltip MsgSheetSolutionFromTip) (sfSolutionFrom <$> template) + <*> aopt htmlField (fslpI MsgSheetDescription "Html") (sfDescription <$> template) <* aformSection MsgSheetFormFiles <*> aopt (multiFileField $ oldFileIds SheetExercise) (fslI MsgSheetExercise) (sfSheetF <$> template) <*> aopt (multiFileField $ oldFileIds SheetHint) (fslI MsgSheetHint) (sfHintF <$> template) <*> aopt (multiFileField $ oldFileIds SheetSolution) (fslI MsgSheetSolution) (sfSolutionF <$> template) <*> aopt (multiFileField $ oldFileIds SheetMarking) (fslI MsgSheetMarkingFiles & setTooltip MsgSheetMarkingTip) (sfMarkingF <$> template) + <* aformSection MsgSheetFormTimes + <*> aopt utcTimeField (fslI MsgSheetVisibleFrom + & setTooltip MsgSheetVisibleFromTip) + ((sfVisibleFrom <$> template) <|> pure (Just ctime)) + <*> aopt utcTimeField (fslI MsgSheetActiveFrom + & setTooltip MsgSheetActiveFromTip) + (sfActiveFrom <$> template) + <*> aopt utcTimeField (fslI MsgSheetActiveTo & setTooltip MsgSheetActiveToTip) (sfActiveTo <$> template) + <*> aopt utcTimeField (fslpI MsgSheetHintFrom (mr MsgSheetHintFromPlaceholder) + & setTooltip MsgSheetHintFromTip) (sfHintFrom <$> template) + <*> aopt utcTimeField (fslpI MsgSheetSolutionFrom (mr MsgSheetSolutionFromPlaceholder) + & setTooltip MsgSheetSolutionFromTip) (sfSolutionFrom <$> template) <* aformSection MsgSheetFormType <*> sheetTypeAFormReq (fslI MsgSheetType & setTooltip (uniworxMessages [MsgSheetTypeInfoBonus, MsgSheetTypeInfoInformational, MsgSheetTypeInfoNotGraded])) (sfType <$> template) <*> sheetGroupAFormReq (fslI MsgSheetGroup) (sfGrouping <$> template) <*> submissionModeForm ((sfSubmissionMode <$> template) <|> pure (SubmissionMode False . Just $ UploadAny True defaultExtensionRestriction)) - <*> aopt htmlField (fslpI MsgSheetDescription "Html") - (sfDescription <$> template) + <*> apopt checkBoxField (fslI MsgAutoAssignCorrs) (sfAutoDistribute <$> template) <*> aopt htmlField (fslpI MsgSheetMarking "Html") (sfMarkingText <$> template) + <*> correctorForm (fromMaybe mempty $ sfCorrectors <$> template) return $ case result of FormSuccess sheetResult - | errorMsgs <- validateSheet mr sheetResult + | errorMsgs <- validateSheet mr' sheetResult , not $ null errorMsgs -> (FormFailure errorMsgs, widget) _ -> (result, widget) @@ -132,10 +151,10 @@ makeSheetForm msId template = identifyForm FIDsheet $ \html -> do validateSheet :: MsgRenderer -> SheetForm -> [Text] validateSheet (MsgRenderer {..}) (SheetForm{..}) = [ msg | (False, msg) <- - [ ( sfVisibleFrom <= Just sfActiveFrom , render MsgSheetErrVisibility) - , ( sfActiveFrom <= sfActiveTo , render MsgSheetErrDeadlineEarly) - , ( NTop sfHintFrom >= NTop (Just sfActiveFrom) , render MsgSheetErrHintEarly) - , ( NTop sfSolutionFrom >= NTop (Just sfActiveTo) , render MsgSheetErrSolutionEarly) + [ ( NTop sfVisibleFrom <= NTop sfActiveFrom , render MsgSheetErrVisibility) + , ( NTop sfActiveFrom <= NTop sfActiveTo , render MsgSheetErrDeadlineEarly) + , ( NTop sfHintFrom >= NTop sfActiveFrom , render MsgSheetErrHintEarly) + , ( NTop sfSolutionFrom >= NTop sfActiveTo , render MsgSheetErrSolutionEarly) ] ] @@ -216,9 +235,9 @@ getSheetListR tid ssh csh = do else spacerCell ] id & cellAttrs <>~ [("class","list--inline list--space-separated")] , sortable (Just "submission-since") (i18nCell MsgSheetActiveFrom) - $ \DBRow{dbrOutput=(Entity _ Sheet{..}, _, _, _)} -> dateTimeCell sheetActiveFrom + $ \DBRow{dbrOutput=(Entity _ Sheet{..}, _, _, _)} -> maybe mempty dateTimeCell sheetActiveFrom , sortable (Just "submission-until") (i18nCell MsgSheetActiveTo) - $ \DBRow{dbrOutput=(Entity _ Sheet{..}, _, _, _)} -> dateTimeCell sheetActiveTo + $ \DBRow{dbrOutput=(Entity _ Sheet{..}, _, _, _)} -> maybe mempty dateTimeCell sheetActiveTo , sortable Nothing (i18nCell MsgSheetType) $ \DBRow{dbrOutput=(Entity _ Sheet{..}, _, _, _)} -> i18nCell sheetType , sortable Nothing (i18nCell MsgSubmission) @@ -231,26 +250,34 @@ getSheetListR tid ssh csh = do return $ CSubmissionR tid ssh csh sheetName cid' SubShowR in anchorCellM mkRoute (mkCid >>= \cid2 -> [whamlet|#{cid2}|]) , sortable (Just "rating") (i18nCell MsgRating) - $ \DBRow{dbrOutput=(Entity _ Sheet{..}, _, mbSub,_)} -> + $ \DBRow{dbrOutput=(Entity _ Sheet{..}, _, mbSub,_)} -> let stats = sheetTypeSum sheetType in -- for statistics over all shown rows case mbSub of Nothing -> cellTell mempty $ stats Nothing (Just (Entity sid sub@Submission{..})) -> - let mkCid = encrypt sid - mkRoute = do - cid' <- mkCid + let + mkRoute :: (MonadHandler m, HandlerSite m ~ UniWorX) => m (Route UniWorX) + mkRoute = liftHandler $ do + cid' <- encrypt sid return $ CSubmissionR tid ssh csh sheetName cid' CorrectionR mTuple mA mB = (,) <$> mA <*> mB -- Hamlet does not support enough haskell-syntax for this acell = anchorCellM mkRoute $(widgetFile "widgets/rating/rating") - in cellTell acell $ stats submissionRatingPoints + tellStats = do + r <- mkRoute + showRating <- hasReadAccessTo r + tell . stats $ bool Nothing submissionRatingPoints showRating + in acell & cellContents %~ (<* tellStats) , sortable Nothing -- (Just "percent") (i18nCell MsgRatingPercent) - $ \DBRow{dbrOutput=(Entity _ Sheet{sheetType=sType}, _, mbSub,_)} -> case mbSub of - (Just (Entity _ Submission{submissionRatingPoints=Just sPoints})) -> + $ \DBRow{dbrOutput=(Entity _ Sheet{sheetType=sType, sheetName}, _, mbSub,_)} -> case mbSub of + (Just (Entity sid Submission{submissionRatingPoints=Just sPoints})) -> case preview (_grading . _maxPoints) sType of Just maxPoints - | maxPoints /= 0 -> textCell $ textPercent sPoints maxPoints + | maxPoints /= 0 -> cell $ do + cID <- encrypt sid + showRating <- hasReadAccessTo $ CSubmissionR tid ssh csh sheetName cID CorrectionR + bool (return ()) (toWidget . toMessage $ textPercent sPoints maxPoints) showRating _other -> mempty _other -> mempty ] @@ -319,17 +346,6 @@ getSheetListR tid ssh csh = do defaultLayout $ do $(widgetFile "sheetList") -data ButtonGeneratePseudonym = BtnGenerate - deriving (Enum, Eq, Ord, Bounded, Read, Show, Generic, Typeable) -instance Universe ButtonGeneratePseudonym -instance Finite ButtonGeneratePseudonym - -nullaryPathPiece ''ButtonGeneratePseudonym (camelToPathPiece' 1) - -instance Button UniWorX ButtonGeneratePseudonym where - btnLabel BtnGenerate = [whamlet|_{MsgSheetGeneratePseudonym}|] - btnClasses BtnGenerate = [BCIsButton, BCDefault] - -- Show single sheet getSShowR :: TermId -> SchoolId -> CourseShorthand -> SheetName -> Handler Html getSShowR tid ssh csh shn = do @@ -422,8 +438,9 @@ getSShowR tid ssh csh shn = do setTitleI $ prependCourseTitle tid ssh csh $ SomeMessage shn let zipLink = CSheetR tid ssh csh shn SArchiveR visibleFrom = visibleUTCTime SelFormatDateTime <$> sheetVisibleFrom sheet - sheetFrom <- formatTime SelFormatDateTime $ sheetActiveFrom sheet - sheetTo <- formatTime SelFormatDateTime $ sheetActiveTo sheet + hasSubmission = classifySubmissionMode (sheetSubmissionMode sheet) /= SubmissionModeNone + sheetFrom <- traverse (formatTime SelFormatDateTime) $ sheetActiveFrom sheet + sheetTo <- traverse (formatTime SelFormatDateTime) $ sheetActiveTo sheet hintsFrom <- traverse (formatTime SelFormatDateTime) $ sheetHintFrom sheet solutionFrom <- traverse (formatTime SelFormatDateTime) $ sheetSolutionFrom sheet markingText <- runMaybeT $ assertM_ (Authorized ==) (evalAccessCorrector tid ssh csh) >> hoistMaybe (sheetMarkingText sheet) @@ -480,7 +497,8 @@ getSheetNewR tid ssh csh = do (FormSuccess (Just shn)) -> E.where_ $ sheet E.^. SheetName E.==. E.val shn -- (FormFailure msgs) -> -- not in MonadHandler anymore -- forM_ msgs (addMessage Error . toHtml) _other -> return () - lastSheets <- runDB $ E.select . E.from $ \(course `E.InnerJoin` sheet) -> do + (lastSheets, loads) <- runDB $ do + lSheets <- E.select . E.from $ \(course `E.InnerJoin` sheet) -> do E.on $ course E.^. CourseId E.==. sheet E.^. SheetCourse E.where_ $ course E.^. CourseTerm E.==. E.val tid E.&&. course E.^. CourseSchool E.==. E.val ssh @@ -493,27 +511,35 @@ getSheetNewR tid ssh csh = do -- E.orderBy [E.desc lastSheetEdit, E.desc (sheet E.^. SheetActiveFrom)] E.orderBy [E.desc (sheet E.^. SheetActiveFrom)] E.limit 1 - return sheet + let firstEdit = E.sub_select . E.from $ \sheetEdit -> do + E.where_ $ sheetEdit E.^. SheetEditSheet E.==. sheet E.^. SheetId + return . E.min_ $ sheetEdit E.^. SheetEditTime + return (sheet, firstEdit) + cid <- getKeyBy404 $ TermSchoolCourseShort tid ssh csh + loads <- defaultLoads cid + return (lSheets, loads) now <- liftIO getCurrentTime let template = case lastSheets of - ((Entity {entityVal=Sheet{..}}):_) -> - let addTime = addWeeks $ max 1 $ weeksToAdd sheetActiveTo now + ((Entity {entityVal=Sheet{..}}, E.Value fEdit):_) -> + let addTime = addWeeks $ max 1 $ weeksToAdd (fromMaybe (UTCTime systemEpochDay 0) $ sheetActiveTo <|> fEdit) now in Just $ SheetForm - { sfName = stepTextCounterCI sheetName - , sfDescription = sheetDescription - , sfType = sheetType - , sfGrouping = sheetGrouping - , sfVisibleFrom = addTime <$> sheetVisibleFrom - , sfActiveFrom = addTime sheetActiveFrom - , sfActiveTo = addTime sheetActiveTo + { sfName = stepTextCounterCI sheetName + , sfDescription = sheetDescription + , sfType = sheetType + , sfGrouping = sheetGrouping + , sfVisibleFrom = addTime <$> sheetVisibleFrom + , sfActiveFrom = addTime <$> sheetActiveFrom + , sfActiveTo = addTime <$> sheetActiveTo , sfSubmissionMode = sheetSubmissionMode - , sfSheetF = Nothing - , sfHintFrom = addTime <$> sheetHintFrom - , sfHintF = Nothing - , sfSolutionFrom = addTime <$> sheetSolutionFrom - , sfSolutionF = Nothing - , sfMarkingF = Nothing - , sfMarkingText = sheetMarkingText + , sfSheetF = Nothing + , sfHintFrom = addTime <$> sheetHintFrom + , sfHintF = Nothing + , sfSolutionFrom = addTime <$> sheetSolutionFrom + , sfSolutionF = Nothing + , sfMarkingF = Nothing + , sfMarkingText = sheetMarkingText + , sfAutoDistribute = sheetAutoDistribute + , sfCorrectors = loads } _other -> Nothing let action newSheet = -- More specific error message for new sheet could go here, if insertUnique returns Nothing @@ -526,44 +552,49 @@ postSheetNewR = getSheetNewR getSEditR :: TermId -> SchoolId -> CourseShorthand -> SheetName -> Handler Html getSEditR tid ssh csh shn = do - (Entity sid Sheet{..}, sheetFileIds) <- runDB $ do - ent <- fetchSheet tid ssh csh shn + (Entity sid Sheet{..}, sheetFileIds, currentLoads) <- runDB $ do + ent@(Entity sid _) <- fetchSheet tid ssh csh shn fti <- getFtIdMap $ entityKey ent - return (ent, fti) + cLoads <- Map.union + <$> fmap (foldMap $ \(Entity _ SheetCorrector{..}) -> Map.singleton (Right sheetCorrectorUser) (InvDBDataSheetCorrector sheetCorrectorLoad sheetCorrectorState, InvTokenDataSheetCorrector)) (selectList [ SheetCorrectorSheet ==. sid ] []) + <*> fmap (fmap (, InvTokenDataSheetCorrector) . Map.mapKeysMonotonic Left) (sourceInvitationsF sid) + return (ent, fti, cLoads) let template = Just $ SheetForm - { sfName = sheetName - , sfDescription = sheetDescription - , sfType = sheetType - , sfGrouping = sheetGrouping - , sfVisibleFrom = sheetVisibleFrom - , sfActiveFrom = sheetActiveFrom - , sfActiveTo = sheetActiveTo + { sfName = sheetName + , sfDescription = sheetDescription + , sfType = sheetType + , sfGrouping = sheetGrouping + , sfVisibleFrom = sheetVisibleFrom + , sfActiveFrom = sheetActiveFrom + , sfActiveTo = sheetActiveTo , sfSubmissionMode = sheetSubmissionMode - , sfSheetF = Just . yieldMany . map Left . Set.elems $ sheetFileIds SheetExercise - , sfHintFrom = sheetHintFrom - , sfHintF = Just . yieldMany . map Left . Set.elems $ sheetFileIds SheetHint - , sfSolutionFrom = sheetSolutionFrom - , sfSolutionF = Just . yieldMany . map Left . Set.elems $ sheetFileIds SheetSolution - , sfMarkingF = Just . yieldMany . map Left . Set.elems $ sheetFileIds SheetMarking - , sfMarkingText = sheetMarkingText + , sfSheetF = Just . yieldMany . map Left . Set.elems $ sheetFileIds SheetExercise + , sfHintFrom = sheetHintFrom + , sfHintF = Just . yieldMany . map Left . Set.elems $ sheetFileIds SheetHint + , sfSolutionFrom = sheetSolutionFrom + , sfSolutionF = Just . yieldMany . map Left . Set.elems $ sheetFileIds SheetSolution + , sfMarkingF = Just . yieldMany . map Left . Set.elems $ sheetFileIds SheetMarking + , sfMarkingText = sheetMarkingText + , sfAutoDistribute = sheetAutoDistribute + , sfCorrectors = currentLoads } + let action = uniqueReplace sid -- More specific error message for edit old sheet could go here by using myReplaceUnique instead handleSheetEdit tid ssh csh (Just sid) template action postSEditR :: TermId -> SchoolId -> CourseShorthand -> SheetName -> Handler Html postSEditR = getSEditR -handleSheetEdit :: TermId -> SchoolId -> CourseShorthand -> Maybe SheetId -> Maybe SheetForm -> (Sheet -> YesodDB UniWorX (Maybe SheetId)) -> Handler Html +handleSheetEdit :: TermId -> SchoolId -> CourseShorthand -> Maybe SheetId -> Maybe SheetForm -> (Sheet -> YesodJobDB UniWorX (Maybe SheetId)) -> Handler Html handleSheetEdit tid ssh csh msId template dbAction = do let mbshn = sfName <$> template aid <- requireAuthId + cid <- runDB $ getKeyBy404 $ TermSchoolCourseShort tid ssh csh ((res,formWidget), formEnctype) <- runFormPost $ makeSheetForm msId template case res of (FormSuccess SheetForm{..}) -> do - saveOkay <- runDB $ do + saveOkay <- runDBJobs $ do actTime <- liftIO getCurrentTime - cid <- getKeyBy404 $ TermSchoolCourseShort tid ssh csh - oldAutoDistribute <- fmap sheetAutoDistribute . join <$> traverse get msId let newSheet = Sheet { sheetCourse = cid , sheetName = sfName @@ -577,7 +608,7 @@ handleSheetEdit tid ssh csh msId template dbAction = do , sheetHintFrom = sfHintFrom , sheetSolutionFrom = sfSolutionFrom , sheetSubmissionMode = sfSubmissionMode - , sheetAutoDistribute = fromMaybe False oldAutoDistribute + , sheetAutoDistribute = sfAutoDistribute } mbsid <- dbAction newSheet case mbsid of @@ -590,22 +621,36 @@ handleSheetEdit tid ssh csh msId template dbAction = do insert_ $ SheetEdit aid actTime sid addMessageI Success $ MsgSheetEditOk tid ssh csh sfName -- Sanity checks generating warnings only, but not errors! - warnTermDays tid $ Map.fromList [ (date,name) | (Just date, name) <- + hoist lift . warnTermDays tid $ Map.fromList [ (date,name) | (Just date, name) <- [ (sfVisibleFrom, MsgSheetVisibleFrom) - , (Just sfActiveFrom, MsgSheetActiveFrom) - , (Just sfActiveTo, MsgSheetActiveTo) + , (sfActiveFrom, MsgSheetActiveFrom) + , (sfActiveTo, MsgSheetActiveTo) , (sfHintFrom, MsgSheetSolutionFromTip) , (sfSolutionFrom, MsgSheetSolutionFrom) ] ] + + let + sheetCorrectors :: Set (Either (Invitation' SheetCorrector) SheetCorrector) + sheetCorrectors = Set.fromList . map f $ Map.toList sfCorrectors + where + f (Left email, invData) = Left (email, sid, invData) + f (Right uid, (InvDBDataSheetCorrector load cState, InvTokenDataSheetCorrector)) = Right $ SheetCorrector uid sid load cState + (invites, adds) = partitionEithers $ Set.toList sheetCorrectors + + deleteWhere [ SheetCorrectorSheet ==. sid ] + insertMany_ adds + + deleteWhere [InvitationFor ==. invRef @SheetCorrector sid, InvitationEmail /<-. toListOf (folded . _1) invites] + sinkInvitationsF correctorInvitationConfig invites + return True - when saveOkay $ redirect $ case msId of - Just _ -> CSheetR tid ssh csh sfName SShowR -- redirect must happen outside of runDB - Nothing -> CSheetR tid ssh csh sfName SCorrR + when saveOkay $ + redirect $ CSheetR tid ssh csh sfName SShowR -- redirect must happen outside of runDB (FormFailure msgs) -> forM_ msgs $ (addMessage Error) . toHtml _ -> runDB $ warnTermDays tid $ Map.fromList [ (date,name) | (Just date, name) <- [(sfVisibleFrom =<< template, MsgSheetVisibleFrom) - ,(sfActiveFrom <$> template, MsgSheetActiveFrom) - ,(sfActiveTo <$> template, MsgSheetActiveTo) + ,(sfActiveFrom =<< template, MsgSheetActiveFrom) + ,(sfActiveTo =<< template, MsgSheetActiveTo) ,(sfHintFrom =<< template, MsgSheetSolutionFromTip) ,(sfSolutionFrom =<< template, MsgSheetSolutionFrom) ] ] @@ -641,14 +686,14 @@ insertSheetFile sid ftype finfo = do fid <- insert file void . insert $ SheetFile sid fid ftype -- cannot fail due to uniqueness, since we generated a fresh FileId in the previous step -insertSheetFile' :: SheetId -> SheetFileType -> ConduitT () (Either FileId File) Handler () -> YesodDB UniWorX () +insertSheetFile' :: SheetId -> SheetFileType -> ConduitT () (Either FileId File) Handler () -> YesodJobDB UniWorX () insertSheetFile' sid ftype fs = do oldFileIds <- fmap setFromList . fmap (map E.unValue) . E.select . E.from $ \(file `E.InnerJoin` sheetFile) -> do E.on $ file E.^. FileId E.==. sheetFile E.^. SheetFileFile E.where_ $ sheetFile E.^. SheetFileSheet E.==. E.val sid E.&&. sheetFile E.^. SheetFileType E.==. E.val ftype return (file E.^. FileId) - keep <- execWriterT . runConduit $ transPipe (lift . lift) fs .| C.mapM_ finsert + keep <- execWriterT . runConduit $ transPipe liftHandler fs .| C.mapM_ finsert mapM_ deleteCascade $ (oldFileIds \\ keep :: Set FileId) where finsert (Left fileId) = tell $ singleton fileId @@ -657,22 +702,12 @@ insertSheetFile' sid ftype fs = do void . insert $ SheetFile sid fid ftype -- cannot fail due to uniqueness, since we generated a fresh FileId in the previous step -data CorrectorForm = CorrectorForm - { cfUserId :: UserId - , cfUserName :: Text - , cfResult :: FormResult (CorrectorState, Load) - , cfViewByTut, cfViewProp, cfViewDel, cfViewState :: FieldView UniWorX - } - -type Loads = Map (Either UserEmail UserId) (CorrectorState, Load) - -defaultLoads :: SheetId -> DB Loads +defaultLoads :: CourseId -> DB Loads -- ^ Generate `Loads` in such a way that minimal editing is required -- -- For every user, that ever was a corrector for this course, return their last `Load`. -- "Last `Load`" is taken to mean their `Load` on the `Sheet` with the most recent creation time (first edit). -defaultLoads shid = do - cId <- sheetCourse <$> getJust shid +defaultLoads cId = do fmap toMap . E.select . E.from $ \(sheet `E.InnerJoin` sheetCorrector) -> E.distinctOnOrderBy [E.asc (sheetCorrector E.^. SheetCorrectorUser)] $ do E.on $ sheet E.^. SheetId E.==. sheetCorrector E.^. SheetCorrectorSheet @@ -687,37 +722,20 @@ defaultLoads shid = do return (sheetCorrector E.^. SheetCorrectorUser, sheetCorrector E.^. SheetCorrectorLoad, sheetCorrector E.^. SheetCorrectorState) where toMap :: [(E.Value UserId, E.Value Load, E.Value CorrectorState)] -> Loads - toMap = foldMap $ \(E.Value uid, E.Value cLoad, E.Value cState) -> Map.singleton (Right uid) (cState, cLoad) + toMap = foldMap $ \(E.Value uid, E.Value cLoad, E.Value cState) -> Map.singleton (Right uid) (InvDBDataSheetCorrector cLoad cState, InvTokenDataSheetCorrector) -correctorForm :: SheetId -> AForm Handler (Set (Either (Invitation' SheetCorrector) SheetCorrector)) -correctorForm shid = wFormToAForm $ do +correctorForm :: Loads -> AForm Handler Loads +correctorForm loads' = wFormToAForm $ do currentRoute <- fromMaybe (error "correctorForm called from 404-handler") <$> liftHandler getCurrentRoute userId <- liftHandler requireAuthId MsgRenderer mr <- getMsgRenderer let - currentLoads :: DB Loads - currentLoads = Map.union - <$> fmap (foldMap $ \(Entity _ SheetCorrector{..}) -> Map.singleton (Right sheetCorrectorUser) (sheetCorrectorState, sheetCorrectorLoad)) (selectList [ SheetCorrectorSheet ==. shid ] []) - <*> fmap (fmap ((,) <$> invDBSheetCorrectorState <*> invDBSheetCorrectorLoad) . Map.mapKeysMonotonic Left) (sourceInvitationsF shid) - (defaultLoads', currentLoads') <- liftHandler . runDB $ (,) <$> defaultLoads shid <*> currentLoads - - isWrite <- liftHandler $ isWriteRequest currentRoute - - let - applyDefaultLoads = Map.null currentLoads' && not isWrite loads :: Map (Either UserEmail UserId) (CorrectorState, Load) - loads - | applyDefaultLoads = defaultLoads' - | otherwise = currentLoads' + loads = loads' <&> \(InvDBDataSheetCorrector load cState, InvTokenDataSheetCorrector) -> (cState, load) - countTutRes <- wreq checkBoxField (fslI MsgCountTutProp & setTooltip MsgCountTutPropTip) . Just . any (\(_, Load{..}) -> fromMaybe False byTutorial) $ Map.elems loads - - -- when (not (Map.null loads) && applyDefaultLoads) $ -- Alert Message - -- addMessageI Warning MsgCorrectorsDefaulted - when (not (Map.null loads) && applyDefaultLoads) $ -- Alert Notification - wformMessage =<< messageIconI Warning IconNoCorrectors MsgCorrectorsDefaulted + countTutRes <- wpopt checkBoxField (fslI MsgCountTutProp & setTooltip MsgCountTutPropTip) . Just . any (\(_, Load{..}) -> fromMaybe False byTutorial) $ Map.elems loads let @@ -804,51 +822,16 @@ correctorForm shid = wFormToAForm $ do miIdent :: Text miIdent = "correctors" - postProcess :: Map ListPosition (Either UserEmail UserId, (CorrectorState, Load)) -> Set (Either (Invitation' SheetCorrector) SheetCorrector) - postProcess = Set.fromList . map postProcess' . Map.elems - where - sheetCorrectorSheet = shid - - postProcess' :: (Either UserEmail UserId, (CorrectorState, Load)) -> Either (Invitation' SheetCorrector) SheetCorrector - postProcess' (Right sheetCorrectorUser, (sheetCorrectorState, sheetCorrectorLoad)) = Right SheetCorrector{..} - postProcess' (Left email, (cState, load)) = Left (email, shid, (InvDBDataSheetCorrector load cState, InvTokenDataSheetCorrector)) + postProcess :: Map ListPosition (Either UserEmail UserId, (CorrectorState, Load)) -> Loads + postProcess = Map.fromList . map postProcess' . Map.elems + where + postProcess' :: (Either UserEmail UserId, (CorrectorState, Load)) -> (Either UserEmail UserId, (InvitationDBData SheetCorrector, InvitationTokenData SheetCorrector)) + postProcess' = over _2 $ \(cState, load) -> (InvDBDataSheetCorrector load cState, InvTokenDataSheetCorrector) filledData :: Maybe (Map ListPosition (Either UserEmail UserId, (CorrectorState, Load))) filledData = Just . Map.fromList . zip [0..] $ Map.toList loads -- TODO orderBy Name?! - fmap postProcess <$> massInputW MassInput{..} (fslI MsgCorrectors & setTooltip MsgMassInputTip) True filledData - -getSCorrR, postSCorrR :: TermId -> SchoolId -> CourseShorthand -> SheetName -> Handler Html -postSCorrR = getSCorrR -getSCorrR tid ssh csh shn = do - Entity shid Sheet{..} <- runDB $ fetchSheet tid ssh csh shn - - ((res,formWidget), formEnctype) <- runFormPost . identifyForm FIDcorrectors . renderAForm FormStandard $ - (,) <$> areq checkBoxField (fslI MsgAutoAssignCorrs) (Just sheetAutoDistribute) - <*> correctorForm shid - - case res of - FormFailure errs -> mapM_ (addMessage Error . toHtml) errs - FormSuccess (autoDistribute, sheetCorrectors) -> runDBJobs $ do - update shid [ SheetAutoDistribute =. autoDistribute ] - - let (invites, adds) = partitionEithers $ Set.toList sheetCorrectors - - deleteWhere [ SheetCorrectorSheet ==. shid ] - insertMany_ adds - - deleteWhere [InvitationFor ==. invRef @SheetCorrector shid, InvitationEmail /<-. toListOf (folded . _1) invites] - sinkInvitationsF correctorInvitationConfig invites - - addMessageI Success MsgCorrectorsUpdated - FormMissing -> return () - - defaultLayout $ do - setTitleI $ MsgSheetCorrectorsTitle tid ssh csh shn - wrapForm formWidget def - { formAction = Just . SomeRoute $ CSheetR tid ssh csh shn SCorrR - , formEncoding = formEnctype - } + fmap postProcess <$> massInputW MassInput{..} (fslI MsgCorrectors & setTooltip MsgMassInputTip) False filledData instance IsInvitableJunction SheetCorrector where @@ -905,7 +888,7 @@ correctorInvitationConfig = InvitationConfig{..} invitationHeading (Entity _ Sheet{..}) _ = return . SomeMessage $ MsgSheetCorrInviteHeading sheetName invitationExplanation _ _ = return [ihamlet|_{SomeMessage MsgSheetCorrInviteExplanation}|] invitationTokenConfig _ _ = do - itAuthority <- liftHandler requireAuthId + itAuthority <- Right <$> liftHandler requireAuthId return $ InvitationTokenConfig itAuthority Nothing Nothing Nothing invitationRestriction _ _ = return Authorized invitationForm _ (InvDBDataSheetCorrector cLoad cState, _) _ = pure $ (JunctionSheetCorrector cLoad cState, ()) @@ -923,5 +906,5 @@ postSCorrInviteR = invitationR correctorInvitationConfig getSIsCorrR :: TermId -> SchoolId -> CourseShorthand -> SheetName -> Handler Html -- NOTE: The route SIsCorrR is only used to verfify corrector access rights to given sheet! getSIsCorrR _ _ _ shn = do - defaultLayout $ [whamlet|You have corrector access to #{shn}.|] + defaultLayout . i18n $ MsgHaveCorrectorAccess shn diff --git a/src/Handler/Submission.hs b/src/Handler/Submission.hs index 8e5ca85c1..9f3fe2788 100644 --- a/src/Handler/Submission.hs +++ b/src/Handler/Submission.hs @@ -107,7 +107,7 @@ submissionUserInvitationConfig = InvitationConfig{..} invitationTokenConfig (Entity _ Submission{..}) _ = do Sheet{..} <- getJust submissionSheet Course{..} <- getJust sheetCourse - itAuthority <- liftHandler requireAuthId + itAuthority <- Right <$> liftHandler requireAuthId itAddAuth <- either throwM (return . Just) $ routeAuthTags (CSheetR courseTerm courseSchool courseShorthand sheetName SubmissionNewR) let itExpiresAt = Nothing itStartsAt = Nothing @@ -125,7 +125,7 @@ submissionUserInvitationConfig = InvitationConfig{..} return . SomeRoute $ CSubmissionR courseTerm courseSchool courseShorthand sheetName cID SubShowR -makeSubmissionForm :: CourseId -> Maybe SubmissionId -> UploadMode -> SheetGroup -> Bool -> Set (Either UserEmail UserId) -> Form (Maybe (ConduitT () File Handler ()), Set (Either UserEmail UserId)) +makeSubmissionForm :: CourseId -> Maybe SubmissionId -> UploadMode -> SheetGroup -> Bool -> Set (Either UserEmail UserId) -> Form (Maybe FileUploads, Set (Either UserEmail UserId)) makeSubmissionForm cid msmid uploadMode grouping isLecturer prefillUsers = identifyForm FIDsubmission . renderAForm FormStandard $ (,) <$> fileUploadForm (not isLecturer && is _Nothing msmid) (fslI . bool MsgSubmissionFile MsgSubmissionArchive) uploadMode <*> wFormToAForm submittorsForm @@ -265,6 +265,8 @@ makeSubmissionForm cid msmid uploadMode grouping isLecturer prefillUsers = ident postProcess :: Map ListPosition (Either UserEmail UserId, ()) -> Set (Either UserEmail UserId) postProcess = setOf $ folded . _1 + when (maxSize > Just 1) $ + wformMessage =<< messageI Info MsgCosubmittorTip fmap postProcess <$> massInputW MassInput{..} submittorSettings' True (Just . Map.fromList . zip [0..] . map (, ()) $ Set.toList prefillUsers) @@ -428,7 +430,7 @@ submissionHelper tid ssh csh shn mcid = do (Nothing, Just smid) -- no new files, existing submission partners updated -> return smid (Just files, _) -> -- new files - runConduit $ transPipe (lift . lift) files .| extractRatingsMsg .| sinkSubmission uid (maybe (Left shid) Right msmid) False + runConduit $ transPipe (lift . lift) files .| Conduit.mapM (either get404 return) .| extractRatingsMsg .| sinkSubmission uid (maybe (Left shid) Right msmid) False (Nothing, Nothing) -- new submission, no file upload requested -> do sid <- insert Submission @@ -484,7 +486,7 @@ submissionHelper tid ssh csh shn mcid = do | otherwise -> case change of Left subEmail -> deleteInvitation @SubmissionUser smid subEmail Right subUid -> do - deleteWhere [SubmissionUserUser ==. subUid] + deleteBy $ UniqueSubmissionUser subUid smid audit $ TransactionSubmissionUserDelete smid subUid addMessageI Success $ if | Nothing <- msmid -> MsgSubmissionCreated diff --git a/src/Handler/SystemMessage.hs b/src/Handler/SystemMessage.hs index c6a3c2214..3ae62b70f 100644 --- a/src/Handler/SystemMessage.hs +++ b/src/Handler/SystemMessage.hs @@ -23,6 +23,7 @@ postMessageR cID = do Nothing -> (systemMessageSummary, systemMessageContent) Just SystemMessageTranslation{..} -> (systemMessageTranslationSummary, systemMessageTranslationContent) + MsgRenderer mr <- getMsgRenderer let mkForm = do ((modifyRes, modifyView), modifyEnctype) <- runFormPost . identifyForm FIDSystemMessageModify . renderAForm FormStandard @@ -31,9 +32,9 @@ postMessageR cID = do <*> aopt utcTimeField (fslI MsgSystemMessageTo) (Just systemMessageTo) <*> areq checkBoxField (fslI MsgSystemMessageAuthenticatedOnly) (Just systemMessageAuthenticatedOnly) <*> areq (selectField optionsFinite) (fslI MsgSystemMessageSeverity) (Just systemMessageSeverity) - <*> areq (langField False) (fslpI MsgSystemMessageLanguage "RFC1766-Sprachcode") (Just systemMessageDefaultLanguage) - <*> areq htmlField' (fslpI MsgSystemMessageContent "HTML") (Just systemMessageContent) - <*> aopt htmlField' (fslpI MsgSystemMessageSummary "HTML") (Just systemMessageSummary) + <*> areq (langField False) (fslpI MsgSystemMessageLanguage (mr MsgRFC1766)) (Just systemMessageDefaultLanguage) + <*> areq htmlField' (fslpI MsgSystemMessageContent "Html") (Just systemMessageContent) + <*> aopt htmlField' (fslpI MsgSystemMessageSummary "Html") (Just systemMessageSummary) ts <- runDB $ selectList [ SystemMessageTranslationMessage ==. smId ] [Asc SystemMessageTranslationLanguage] let ts' = Map.fromList $ (systemMessageTranslationLanguage . entityVal &&& id) <$> ts @@ -45,9 +46,9 @@ postMessageR cID = do <$> fmap (Entity tId) ( SystemMessageTranslation <$> pure systemMessageTranslationMessage - <*> areq (langField False) (fslpI MsgSystemMessageLanguage "RFC1766-Sprachcode") (Just systemMessageTranslationLanguage) - <*> areq htmlField' (fslpI MsgSystemMessageContent "HTML") (Just systemMessageTranslationContent) - <*> aopt htmlField' (fslpI MsgSystemMessageSummary "HTML") (Just systemMessageTranslationSummary) + <*> areq (langField False) (fslpI MsgSystemMessageLanguage (mr MsgRFC1766)) (Just systemMessageTranslationLanguage) + <*> areq htmlField' (fslpI MsgSystemMessageContent "Html") (Just systemMessageTranslationContent) + <*> aopt htmlField' (fslpI MsgSystemMessageSummary "Html") (Just systemMessageTranslationSummary) ) <*> combinedButtonFieldF "" @@ -56,9 +57,9 @@ postMessageR cID = do ((addTransRes, addTransView), addTransEnctype) <- runFormPost . identifyForm FIDSystemMessageAddTranslation . renderAForm FormStandard $ SystemMessageTranslation <$> pure smId - <*> areq (langField False) (fslpI MsgSystemMessageLanguage "RFC1766-Sprachcode") Nothing - <*> areq htmlField' (fslpI MsgSystemMessageContent "HTML") Nothing - <*> aopt htmlField' (fslpI MsgSystemMessageSummary "HTML") Nothing + <*> areq (langField False) (fslpI MsgSystemMessageLanguage (mr MsgRFC1766)) Nothing + <*> areq htmlField' (fslpI MsgSystemMessageContent "Html") Nothing + <*> aopt htmlField' (fslpI MsgSystemMessageSummary "Html") Nothing formResult modifyRes $ modifySystemMessage smId @@ -252,14 +253,15 @@ postMessageListR = do FormSuccess (_, _selection) -- prop> null _selection -> addMessageI Error MsgSystemMessageEmptySelection + MsgRenderer mr <- getMsgRenderer ((addRes, addView), addEncoding) <- runFormPost . identifyForm FIDSystemMessageAdd . renderAForm FormStandard $ SystemMessage <$> aopt utcTimeField (fslI MsgSystemMessageFrom) Nothing <*> aopt utcTimeField (fslI MsgSystemMessageTo) Nothing <*> areq checkBoxField (fslI MsgSystemMessageAuthenticatedOnly) Nothing <*> areq (selectField optionsFinite) (fslI MsgSystemMessageSeverity) Nothing - <*> areq (langField False) (fslpI MsgSystemMessageLanguage "RFC1766-Sprachcode") (Just $ NonEmpty.head appLanguages) - <*> areq htmlField' (fslpI MsgSystemMessageContent "HTML") Nothing - <*> aopt htmlField' (fslpI MsgSystemMessageSummary "HTML") Nothing + <*> areq (langField False) (fslpI MsgSystemMessageLanguage (mr MsgRFC1766)) (Just $ NonEmpty.head appLanguages) + <*> areq htmlField' (fslpI MsgSystemMessageContent "Html") Nothing + <*> aopt htmlField' (fslpI MsgSystemMessageSummary "Html") Nothing case addRes of FormMissing -> return () diff --git a/src/Handler/Term.hs b/src/Handler/Term.hs index 26c349c83..abce61c63 100644 --- a/src/Handler/Term.hs +++ b/src/Handler/Term.hs @@ -7,6 +7,8 @@ import qualified Data.Map as Map import qualified Database.Esqueleto as E import qualified Data.Set as Set + +import qualified Control.Monad.State.Class as State -- | Default start day of term for season, @@ -18,32 +20,15 @@ defaultDay True Summer = fromGregorian 2020 4 1 defaultDay False Summer = fromGregorian 2020 9 30 -validateTerm :: Term -> [Text] -validateTerm Term{..} = - [ msg | (False, msg) <- - [ --startOk - ( termStart `withinTerm` termName - , "Jahreszahl im Namenskürzel stimmt nicht mit Semesterbeginn überein." - ) - , -- endOk - ( termStart < termEnd - , "Semester darf nicht enden, bevor es begann." - ) - , -- startOk - ( termLectureStart < termLectureEnd - , "Vorlesungszeit muss vor ihrem Ende anfgangen." - ) - , -- lecStartOk - ( termStart <= termLectureStart - , "Semester muss vor der Vorlesungszeit beginnen." - ) - , -- lecEndOk - ( termEnd >= termLectureEnd - , "Vorlesungszeit muss vor dem Semester enden." - ) - ] ] - - +validateTerm :: (MonadHandler m, HandlerSite m ~ UniWorX) + => FormValidator Term m () +validateTerm = do + Term{..} <- State.get + guardValidation MsgTermStartMustMatchName $ termStart `withinTerm` termName + guardValidation MsgTermEndMustBeAfterStart $ termStart < termEnd + guardValidation MsgTermLectureEndMustBeAfterStart $ termLectureStart < termLectureEnd + guardValidation MsgTermStartMustBeBeforeLectureStart $ termStart <= termLectureStart + guardValidation MsgTermEndMustBeAfterLectureEnd $ termEnd >= termLectureEnd getTermShowR :: Handler TypedContent @@ -66,22 +51,22 @@ getTermShowR = do provideRep $ toJSON . map fst <$> runDB (E.select $ E.from termData) provideRep $ do let colonnadeTerms = widgetColonnade $ mconcat - [ sortable (Just "term-id") "Kürzel" $ \(Entity tid _, _) -> anchorCell + [ sortable (Just "term-id") (i18nCell MsgTermShort) $ \(Entity tid _, _) -> anchorCell (TermCourseListR tid) [whamlet|#{toPathPiece tid}|] , sortable (Just "lecture-start") (i18nCell MsgLectureStart) $ \(Entity _ Term{..},_) -> cell $ formatTime SelFormatDate termLectureStart >>= toWidget - , sortable (Just "lecture-end") "Ende Vorlesungen" $ \(Entity _ Term{..},_) -> + , sortable (Just "lecture-end") (i18nCell MsgTermLectureEnd) $ \(Entity _ Term{..},_) -> cell $ formatTime SelFormatDate termLectureEnd >>= toWidget - , sortable Nothing "Aktiv" $ \(Entity _ Term{..},_) -> + , sortable Nothing (i18nCell MsgTermActive) $ \(Entity _ Term{..},_) -> tickmarkCell termActive - , sortable Nothing "Kurse" $ \(_, E.Value numCourses) -> + , sortable Nothing (i18nCell MsgTermCourseCount) $ \(_, E.Value numCourses) -> cell [whamlet|_{MsgNumCourses numCourses}|] - , sortable (Just "start") "Semesteranfang" $ \(Entity _ Term{..},_) -> + , sortable (Just "start") (i18nCell MsgTermStart) $ \(Entity _ Term{..},_) -> cell $ formatTime SelFormatDate termStart >>= toWidget - , sortable (Just "end") "Semesterende" $ \(Entity _ Term{..},_) -> + , sortable (Just "end") (i18nCell MsgTermEnd) $ \(Entity _ Term{..},_) -> cell $ formatTime SelFormatDate termEnd >>= toWidget - , sortable Nothing "Feiertage im Semester" $ \(Entity _ Term{..},_) -> + , sortable Nothing (i18nCell MsgTermHolidays) $ \(Entity _ Term{..},_) -> cell $ do termHolidays' <- mapM (formatTime SelFormatDate) termHolidays [whamlet| @@ -248,7 +233,7 @@ termToTemplate (Just Term{..}) = TermFormTemplate } newTermForm :: TermFormTemplate -> Form Term -newTermForm template html = do +newTermForm template = validateForm validateTerm $ \html -> do mr <- getMessageRender let tidForm @@ -264,7 +249,7 @@ newTermForm template html = do (fslI MsgTermHolidays & setTooltip MsgMassInputTip) True (tftHolidays template) - (result, widget) <- flip (renderAForm FormStandard) html $ Term + flip (renderAForm FormStandard) html $ Term <$> tidForm <*> areq dayField (fslI MsgTermStartDay & setTooltip MsgTermStartDayTooltip) (tftStart template) <*> areq dayField (fslI MsgTermEndDay & setTooltip MsgTermEndDayTooltip) (tftEnd template) @@ -272,24 +257,3 @@ newTermForm template html = do <*> areq dayField (fslI MsgTermLectureStart) (tftLectureStart template) <*> areq dayField (fslI MsgTermLectureEnd & setTooltip MsgTermLectureEndTooltip) (tftLectureEnd template) <*> areq checkBoxField (fslI MsgTermActive) (tftActive template) - return $ case result of - FormSuccess termResult - | errorMsgs <- validateTerm termResult - , not $ null errorMsgs -> - (FormFailure errorMsgs, - [whamlet| -
-
-

Fehler: -