diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index d1d47e48e..48adc6a88 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -14,6 +14,12 @@ variables: POSTGRES_DB: uniworx_test POSTGRES_USER: uniworx POSTGRES_PASSWORD: uniworx + MINIO_ACCESS_KEY: gOel7KvadwNKgjjy + MINIO_SECRET_KEY: ugO5pkEla7F0JW9MdPwLi4MWLT5ZbqAL + UPLOAD_S3_HOST: localhost + UPLOAD_S3_PORT: 9000 + UPLOAD_S3_KEY_ID: gOel7KvadwNKgjjy + UPLOAD_S3_KEY: ugO5pkEla7F0JW9MdPwLi4MWLT5ZbqAL N_PREFIX: "${HOME}/.n" stages: @@ -82,9 +88,12 @@ frontend:lint: interruptible: true yesod:build:dev: - services: + services: &build-services - name: postgres:10.10 alias: postgres + - name: minio/minio:RELEASE.2020-08-27T05-16-20Z + alias: minio + command: ["minio", "server", "/data"] stage: yesod:build script: @@ -114,9 +123,7 @@ yesod:build:dev: interruptible: true yesod:build: - services: - - name: postgres:10.10 - alias: postgres + services: *build-services stage: yesod:build script: diff --git a/CHANGELOG.md b/CHANGELOG.md index 452d377d4..67e64602c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,86 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +## [20.1.0](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/v20.0.0...v20.1.0) (2020-09-17) + + +### Features + +* **sheet:** warn about no submission without not graded ([9373266](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/937326639a02c576f278b79b8ebb441a2652bece)), closes [#342](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/issues/342) + + +### Bug Fixes + +* **eexamlistr:** allow access for users with exam results ([885de44](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/885de4403c0172b3e9c3b59c277628106a7e925b)) +* **files:** fix download of non-injected files ([ce54adc](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/ce54adce6b67f3de95d65d74ff62b36cccdba47e)) + +## [20.0.0](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/v19.3.1...v20.0.0) (2020-09-11) + + +### ⚠ BREAKING CHANGES + +* **files:** files now chunked + +### Features + +* **files:** avoid initial unnecessary rechunking ([e80f7d7](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/e80f7d7a89e205ce53a70178e0b44d9b0ddf5b97)) +* **files:** chunk prune-unreferenced-files finer ([58c2420](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/58c242045887673f69c368668803574d829cc823)) +* **files:** chunking ([8f608c1](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/8f608c19552ef7bd6ce61af92496b3d5f5bf61b1)) +* **files:** content dependent chunking ([d624a95](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/d624a951c54bda86e04d440eba9901d2a65153b9)) + + +### Bug Fixes + +* zip handling & tests ([350ee79](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/350ee79af3c8fcc480970166a559596873beab2a)) + +### [19.3.1](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/v19.3.0...v19.3.1) (2020-09-10) + + +### Bug Fixes + +* **dbtable:** calculate height of header correctly ([5659f2d](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/5659f2df1e6ea473794075d85f2a43fc1037fce9)), closes [#634](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/issues/634) + +## [19.3.0](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/v19.2.2...v19.3.0) (2020-08-28) + + +### Features + +* add user-system-function ([abc37ac](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/abc37aca9c2aa5eafe7eea9333886b43189d5591)) +* automatically sync system functions from ldap ([297ff4f](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/297ff4f02591339dda7f3270cc9cd332e18febb7)) +* course applications study features ([44eeffc](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/44eeffcc70a8b4c119e1a88a9ef01c687fe2e10a)) +* generated columns tooltip ([2c4080d](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/2c4080d0e0d7f59829238830a5200116a9d884ec)) +* implement system-exam-office ([42aee66](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/42aee66d1f9c189a6a6b13b1970c61e0299630ae)) +* log ldap error messages on invalid-credentials ([0b4fade](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/0b4fadedd2d7ffbb58598d9844e1c7d97cabc447)) +* reduce number of study features for courses ([51a98f0](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/51a98f067086bcef3daff601b53d5eb45f4a27f0)) +* restore study features in all tables ([363f7ab](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/363f7abc192872ebd2a609b8bd89b58032bc9131)) +* study feature filtering ([96d0ba8](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/96d0ba8f7a1c8d8d4e895541b66e36d35392fb25)) +* support for ldap primary keys ([bbfd182](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/bbfd182ed93d1e602229a2fd1ac1e0fa4c4439ef)) +* **study-features:** add study-features-first-observed ([dcb83d9](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/dcb83d96fc0e52c0c322e50d9467d9a2bed90359)) +* **study-features:** further restriction by course ([f7a9bc8](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/f7a9bc831a3b0ef58fcbf7918be9f5e3b262641e)) + + +### Bug Fixes + +* don't set user-last-authentication during ldap sync ([fdaad16](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/fdaad16e713e69a7b47f80a690a97d2ff5eb9986)) +* missing translations ([dcfdb51](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/dcfdb5130d19e737147bfe9065a6ccb5edf49a77)) +* order of on in exam office auth ([f44f150](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/f44f1507471a9310a9c88738ca5b3d8268afc136)) +* tests ([018d26f](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/018d26f4a1a1cf411324aeac56ce4d4203670942)) +* tests ([5541619](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/5541619372f4a4e46ccc403004e869afdfaed7b0)) + +### [19.2.2](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/v19.2.1...v19.2.2) (2020-08-26) + + +### Bug Fixes + +* have exam deregistration always delete stored grades ([24f428b](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/24f428b13bb181bec99417b4e69fc538e35acbcf)) + +### [19.2.1](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/v19.2.0...v19.2.1) (2020-08-26) + + +### Bug Fixes + +* improve hidecolumns behaviour ([9a4f30b](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/9a4f30b811fdf8c58ec5c50c185628eb3158931a)) + ## [19.2.0](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/v19.1.5...v19.2.0) (2020-08-24) diff --git a/config/settings.yml b/config/settings.yml index 3824a9f2b..d8b8b0330 100644 --- a/config/settings.yml +++ b/config/settings.yml @@ -35,7 +35,8 @@ bearer-expiration: 604800 bearer-encoding: HS256 maximum-content-length: "_env:MAX_UPLOAD_SIZE:134217728" session-files-expire: 3600 -prune-unreferenced-files: 28800 +prune-unreferenced-files-within: 57600 +prune-unreferenced-files-interval: 3600 keep-unreferenced-files: 86400 health-check-interval: matching-cluster-config: "_env:HEALTHCHECK_INTERVAL_MATCHING_CLUSTER_CONFIG:600" @@ -158,7 +159,13 @@ upload-cache: auto-discover-region: "_env:UPLOAD_S3_AUTO_DISCOVER_REGION:true" disable-cert-validation: "_env:UPLOAD_S3_DISABLE_CERT_VALIDATION:false" upload-cache-bucket: "uni2work-uploads" -inject-files: 10 + +inject-files: 307 +rechunk-files: 601 + +file-upload-db-chunksize: 4194304 # 4MiB +file-chunking-target-exponent: 21 # 2MiB +file-chunking-hash-window: 4096 server-sessions: idle-timeout: 28807 @@ -229,6 +236,9 @@ token-buckets: depth: 1572864000 # 1500MiB inv-rate: 1.9e-6 # 2MiB/s initial-value: 0 - + rechunk-files: + depth: 20971520 # 20MiB + inv-rate: 9.5e-7 # 1MiB/s + initial-value: 0 fallback-personalised-sheet-files-keys-expire: 2419200 diff --git a/config/test-settings.yml b/config/test-settings.yml index 7ba4552eb..905639ac1 100644 --- a/config/test-settings.yml +++ b/config/test-settings.yml @@ -1,5 +1,6 @@ database: database: "_env:PGDATABASE_TEST:uniworx_test" +upload-cache-bucket: "uni2work-test-uploads" log-settings: detailed: true @@ -10,4 +11,5 @@ log-settings: auth-dummy-login: true server-session-acid-fallback: true +job-cron-interval: null job-workers: 1 diff --git a/frontend/src/_common.sass b/frontend/src/_common.sass index cf4fab2cf..00bf18dfe 100644 --- a/frontend/src/_common.sass +++ b/frontend/src/_common.sass @@ -5,5 +5,5 @@ @use "~@fortawesome/fontawesome-pro/scss/solid" @use "~typeface-roboto" as roboto - @use "~typeface-source-sans-pro" as source-sans-pro +@use "~typeface-source-code-pro" as source-code-pro diff --git a/frontend/src/app.sass b/frontend/src/app.sass index 2c4e1a45d..d8c5f6e5d 100644 --- a/frontend/src/app.sass +++ b/frontend/src/app.sass @@ -22,6 +22,7 @@ // FONTS --font-base: "Source Sans Pro", "Trebuchet MS", sans-serif --font-logo: "Roboto", var(--font-base) + --font-monospace: "Source Code Pro", monospace // DIMENSIONS --header-height: 100px @@ -594,9 +595,6 @@ section border-bottom: none padding-bottom: 0px -.pseudonym - font-family: monospace - .headline-one margin-bottom: 10px @@ -726,8 +724,13 @@ section background-color: hsla($hue, 75%, 50%, $opacity) !important -.uuid - font-family: monospace +.uuid, .pseudonym, .ldap-primary-key, .email, .file-path, .metric-value, .metric-label + font-family: var(--font-monospace) + +.token + font-family: var(--font-monospace) + white-space: pre-wrap + word-break: break-all .form--inline display: inline-block @@ -841,7 +844,7 @@ section .comment, .literal-error white-space: pre-wrap - font-family: monospace + font-family: var(--font-monospace) th vertical-align: top @@ -1108,12 +1111,12 @@ th, td #changelog font-size: 14px white-space: pre-wrap - font-family: monospace + font-family: var(--font-monospace) #gitrev font-size: 12px white-space: pre-wrap - font-family: monospace + font-family: var(--font-monospace) .breadcrumbs__container position: relative @@ -1234,12 +1237,12 @@ a.breadcrumbs__home top: 5px .table__td--csv, .table__th--csv - font-family: monospace + font-family: var(--font-monospace) .confirmationText white-space: pre-wrap font-size: 14px - font-family: monospace + font-family: var(--font-monospace) .func-field__wrapper, .allocation-missing-prios, .allocation-users__accept, .corrections-overview__section max-height: 75vh @@ -1304,7 +1307,7 @@ a.breadcrumbs__home .csv-parse-error white-space: pre-wrap - font-family: monospace + font-family: var(--font-monospace) overflow: auto max-height: 75vh diff --git a/frontend/src/utils/form/datepicker.css b/frontend/src/utils/form/datepicker.css index 7c1172da1..b052a1e7c 100644 --- a/frontend/src/utils/form/datepicker.css +++ b/frontend/src/utils/form/datepicker.css @@ -29,7 +29,7 @@ visibility: hidden; direction: ltr; border-collapse: separate; - font-family: "Open Sans", Calibri, Arial, sans-serif; + /* font-family: "Open Sans", Calibri, Arial, sans-serif; */ background-color: white; border-width: 0; border-style: solid; @@ -724,4 +724,4 @@ } /* @end RTL */ -/*# sourceMappingURL=tail.datetime-default-green.map */ \ No newline at end of file +/*# sourceMappingURL=tail.datetime-default-green.map */ diff --git a/frontend/src/utils/inputs/inputs.sass b/frontend/src/utils/inputs/inputs.sass index 374d37189..022efa71d 100644 --- a/frontend/src/utils/inputs/inputs.sass +++ b/frontend/src/utils/inputs/inputs.sass @@ -235,7 +235,7 @@ option padding-bottom: 0 .file-input__list-item - font-family: monospace + font-family: var(--font-monospace) font-size: 15px // PREVIOUSLY UPLOADED FILES diff --git a/messages/uniworx/de-de-formal.msg b/messages/uniworx/de-de-formal.msg index abf0768af..d02d9acd1 100644 --- a/messages/uniworx/de-de-formal.msg +++ b/messages/uniworx/de-de-formal.msg @@ -389,6 +389,7 @@ SheetWarnNoActiveTo: "Aktiv bis/Ende Abgabezeitraum" sollte stets angegeben werd SheetNoCurrent: Es gibt momentan kein aktives Übungsblatt. SheetNoOldUnassigned: Alle Abgaben inaktiver Blätter sind bereits einen Korrektor zugeteilt. SheetsUnassignable name@Text: Momentan keine Abgaben zuteilbar für #{name} +SheetSubmissionModeNoneWithoutNotGraded: Es wurde "Keine Abgabe" eingestellt, jedoch nicht "Keine Bewertung". Kursteilnehmer werden nicht abgeben können. Deadline: Abgabe Done: Eingereicht @@ -478,6 +479,7 @@ UnauthorizedSchoolAdmin: Sie sind nicht als Administrator für dieses Institut e UnauthorizedAdminEscalation: Sie sind nicht Administrator für alle Institute, für die dieser Nutzer Administrator oder Veranstalter ist. UnauthorizedExamOffice: Sie sind nicht mit Prüfungsverwaltung beauftragt. UnauthorizedExamExamOffice: Es existieren keine Prüfungsergebnisse für Nutzer, für die Sie mit der Prüfungsverwaltung beauftragt sind. +UnauthorizedSystemExamOffice: Sie sind nicht mit systemweiter Prüfungsverwaltung beauftragt. UnauthorizedExternalExamExamOffice: Es existieren keine Prüfungsergebnisse für Nutzer, für die Sie mit der Prüfungsverwaltung beauftragt sind. UnauthorizedEvaluation: Sie sind nicht mit der Kursumfragenverwaltung beauftragt. UnauthorizedAllocationAdmin: Sie sind nicht mit der Administration von Zentralanmeldungen beauftragt. @@ -786,6 +788,9 @@ CorrectorsFor n@Int: #{pluralDE n "Korrektor" "Korrektoren"} UserListTitle: Komprehensive Benutzerliste AccessRightsSaved: Berechtigungen erfolgreich verändert AccessRightsNotChanged: Berechtigungen wurden nicht verändert +UserSystemFunctions: Systemweite Rollen +UserSystemFunctionsSaved: Systemweite Rollen gespeichert +UserSystemFunctionsNotChanged: Es wurden keine systemweiten Rollen angepasst LecturersForN n@Int: #{pluralDE n "Dozent" "Dozenten"} @@ -900,6 +905,8 @@ SubmissionReplace: Abgabe ersetzen SubmissionCreated: Abgabe erfolgreich angelegt SubmissionUpdated: Abgabe erfolgreich ersetzt +ColumnStudyFeatures: Studiendaten + AdminFeaturesHeading: Studiengänge StudyTerms: Studiengänge StudyTerm: Studiengang @@ -1033,6 +1040,10 @@ MailUserRightsIntro name@Text email@UserEmail: #{name} <#{email}> hat folgende U MailNoLecturerRights: Sie haben derzeit keine Dozenten-Rechte. MailLecturerRights n@Int: Als Dozent dürfen Sie Veranstaltungen innerhalb #{pluralDE n "Ihres Instituts" "Ihrer Institute"} anlegen. +MailSubjectUserSystemFunctionsUpdate name@Text: Berechtigungen für #{name} aktualisiert +MailUserSystemFunctionsIntro name@Text email@UserEmail: #{name} <#{email}> hat folgende Uni2work nicht-institutsbezogene Berechtigungen: +MailUserSystemFunctionsNoFunctions: Keine + MailSubjectUserAuthModeUpdate: Ihr Uni2work-Login UserAuthModePWHashChangedToLDAP: Sie können sich nun mit Ihrer Campus-Kennung in Uni2work einloggen UserAuthModeLDAPChangedToPWHash: Sie können sich nun mit einer Uni2work-internen Kennung in Uni2work einloggen @@ -1468,6 +1479,7 @@ AuthPredsActiveChanged: Authorisierungseinstellungen für aktuelle Sitzung gespe AuthTagFree: Seite ist universell zugänglich AuthTagAdmin: Nutzer ist Administrator AuthTagExamOffice: Nutzer ist mit Prüfungsverwaltung beauftragt +AuthTagSystemExamOffice: Nutzer ist mit systemweiter Prüfungsverwaltung beauftragt AuthTagEvaluation: Nutzer ist mit Kursumfragenverwaltung beauftragt AuthTagAllocationAdmin: Nutzer ist mit der Administration von Zentralanmeldungen beauftragt AuthTagToken: Nutzer präsentiert Authorisierungs-Token @@ -2020,7 +2032,7 @@ CsvColumnUserName: Voller Name des Teilnehmers CsvColumnUserMatriculation: Matrikelnummer des Teilnehmers CsvColumnUserSex: Geschlecht CsvColumnUserEmail: E-Mail-Adresse des Teilnehmers -CsvColumnUserStudyFeatures: Alle aktiven Studiendaten des Teilnehmers als Semikolon (;) separierte Liste +CsvColumnUserStudyFeatures: Alle relevanten Studiendaten des Teilnehmers als Semikolon (;) separierte Liste CsvColumnUserField: Studienfach, mit dem der Teilnehmer seine Kursanmeldung assoziiert hat CsvColumnUserDegree: Abschluss, den der Teilnehmer im assoziierten Studienfach anstrebt CsvColumnUserSemester: Fachsemester des Teilnehmers im assoziierten Studienfach @@ -2046,6 +2058,11 @@ CsvColumnApplicationsVeto: Bewerber mit Veto werden garantiert nicht dem Kurs zu CsvColumnApplicationsRating: Bewertung der Bewerbung; "1.0", "1.3", "1.7", ..., "4.0", "5.0" (Leer wird behandelt wie eine Note zwischen 2.3 und 2.7) CsvColumnApplicationsComment: Kommentar zur Bewerbung; je nach Kurs-Einstellungen entweder nur als Notiz für die Kursverwalter oder Feedback für den Bewerber +ApplicationGeneratedColumns: Stammdaten +ApplicationGeneratedColumnsTip: Stammdaten eines Bewerbers sind Daten, welche dem System zu diesem Benutzer bekannt sind und welche der Benutzer im Zuge der Bewerbung nicht beeinflussen kann. +ApplicationUserColumns: Bewerbung +ApplicationRatingColumns: Bewertung + Action: Aktion ActionNoUsersSelected: Keine Benutzer ausgewählt @@ -2501,6 +2518,9 @@ StudyTermsDefaultFieldType: Default Typ MenuLanguage: Sprache LanguageChanged: Sprache erfolgreich geändert +ProfileLastLdapSynchronisation: Letzte LDAP-Synchronisation +ProfileLdapPrimaryKey: LDAP-Primärschlüssel + ProfileCorrector: Korrektor ProfileCourses: Eigene Kurse ProfileCourseParticipations: Kursanmeldungen @@ -2766,3 +2786,6 @@ SheetPersonalisedFilesUsersList: Liste von Teilnehmern mit personalisierten Übu AdminCrontabNotGenerated: (Noch) keine Crontab generiert CronMatchAsap: ASAP CronMatchNone: Nie + +SystemExamOffice: Prüfungsverwaltung +SystemFaculty: Fakultätsmitglied \ No newline at end of file diff --git a/messages/uniworx/en-eu.msg b/messages/uniworx/en-eu.msg index 159d750c1..01e686a3e 100644 --- a/messages/uniworx/en-eu.msg +++ b/messages/uniworx/en-eu.msg @@ -389,6 +389,7 @@ SheetWarnNoActiveTo: “Active to/Submission period end” should always be spec 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. +SheetSubmissionModeNoneWithoutNotGraded: The sheet was configured to be "No submission" but not "Not marked". Course participants will not be able to submit. Deadline: Deadline Done: Submitted @@ -479,6 +480,7 @@ UnauthorizedExamOffice: You are not part of an exam office. UnauthorizedEvaluation: You are not charged with course evaluation. UnauthorizedAllocationAdmin: You are not charged with the administration of central allocations. UnauthorizedExamExamOffice: You are not part of the appropriate exam office for any of the participants of this exam. +UnauthorizedSystemExamOffice: You are not charged with system wide exam administration UnauthorizedExternalExamExamOffice: 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. @@ -784,6 +786,9 @@ CorrectorsFor n: #{pluralEN n "Corrector" "Correctors"} UserListTitle: Comprehensive list of users AccessRightsSaved: Successfully updated permissions AccessRightsNotChanged: Permissions left unchanged +UserSystemFunctions: System wide roles +UserSystemFunctionsSaved: Successfully saved system wide roles +UserSystemFunctionsNotChanged: No system wide roles were changed LecturersForN n: #{pluralEN n "Lecturer" "Lecturers"} @@ -898,6 +903,8 @@ SubmissionReplace: Replace submission SubmissionCreated: Successfully created submission SubmissionUpdated: Successfully replaced submission +ColumnStudyFeatures: Features of study + AdminFeaturesHeading: Features of study StudyTerms: Fields of study StudyTerm: Field of study @@ -1034,6 +1041,10 @@ MailUserRightsIntro name email: #{name} <#{email}> now has the following permiss 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"}. +MailSubjectUserSystemFunctionsUpdate name: Permissions for #{name} changed +MailUserSystemFunctionsIntro name email: #{name} <#{email}> now has the following, not school restricted, permissions: +MailUserSystemFunctionsNoFunctions: None + 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 @@ -1469,6 +1480,7 @@ 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 +AuthTagSystemExamOffice: User is charged with system wide exam administration AuthTagEvaluation: User is charged with course evaluation AuthTagAllocationAdmin: User is charged with administration of central allocations AuthTagToken: User is presenting an authorisation-token @@ -2019,7 +2031,7 @@ 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 (;) +CsvColumnUserStudyFeatures: All relevant features 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 @@ -2045,6 +2057,11 @@ CsvColumnApplicationsVeto: Vetoed applicants are never assigned to the course; " 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 +ApplicationGeneratedColumns: Master data +ApplicationGeneratedColumnsTip: An applicant's master data is data which is known to the system about this user and which the user cannot modify when applying for the course. +ApplicationUserColumns: Application +ApplicationRatingColumns: Rating + Action: Action ActionNoUsersSelected: No users selected @@ -2501,6 +2518,9 @@ StudyTermsDefaultFieldType: Default type MenuLanguage: Language LanguageChanged: Language changed successfully +ProfileLastLdapSynchronisation: Last LDAP synchronisation +ProfileLdapPrimaryKey: LDAP primary key + ProfileCorrector: Corrector ProfileCourses: Own courses ProfileCourseParticipations: Course registrations @@ -2767,3 +2787,6 @@ SheetPersonalisedFilesUsersList: List of course participants who have personalis AdminCrontabNotGenerated: Crontab not (yet) generated CronMatchAsap: ASAP CronMatchNone: Never + +SystemExamOffice: Exam office +SystemFaculty: Faculty member diff --git a/models/courses.model b/models/courses.model index 509a49d5f..137c0cdf1 100644 --- a/models/courses.model +++ b/models/courses.model @@ -56,7 +56,7 @@ CourseParticipant -- course enrolement course CourseId user UserId registration UTCTime -- time of last enrolement for this course - field StudyFeaturesId Maybe -- associated degree course, user-defined; required for communicating grades + field StudyFeaturesId Maybe MigrationOnly allocated AllocationId Maybe -- participant was centrally allocated state CourseParticipantState UniqueParticipant user course diff --git a/models/courses/applications.model b/models/courses/applications.model index b4648a60e..8e7d6c8d5 100644 --- a/models/courses/applications.model +++ b/models/courses/applications.model @@ -1,7 +1,7 @@ CourseApplication course CourseId user UserId - field StudyFeaturesId Maybe -- associated degree course, user-defined; required for communicating grades + field StudyFeaturesId Maybe MigrationOnly text Text Maybe -- free text entered by user ratingVeto Bool default=false ratingPoints ExamGrade Maybe diff --git a/models/files.model b/models/files.model index 428331b36..2a8656a3e 100644 --- a/models/files.model +++ b/models/files.model @@ -1,9 +1,20 @@ -FileContent +FileContentEntry hash FileContentReference + ix Natural + chunkHash FileContentChunkId + UniqueFileContentEntry hash ix + +FileContentChunk + hash FileContentChunkReference content ByteString - unreferencedSince UTCTime Maybe + contentBased Bool default=false -- For Migration Primary hash +FileContentChunkUnreferenced + hash FileContentChunkId + since UTCTime + UniqueFileContentChunkUnreferenced hash + SessionFile content FileContentReference Maybe touched UTCTime @@ -12,3 +23,8 @@ FileLock content FileContentReference instance InstanceId time UTCTime + +FileChunkLock + hash FileContentChunkReference + instance InstanceId + time UTCTime \ No newline at end of file diff --git a/models/users.model b/models/users.model index 657669910..b3b92e2d3 100644 --- a/models/users.model +++ b/models/users.model @@ -17,6 +17,7 @@ User json -- Each Uni2work user has a corresponding row in this table; create lastAuthentication UTCTime Maybe -- last login date created UTCTime default=now() lastLdapSynchronisation UTCTime Maybe + ldapPrimaryKey Text Maybe tokensIssuedAfter UTCTime Maybe -- do not accept bearer tokens issued before this time (accept all tokens if null) matrikelnummer UserMatriculation Maybe -- optional immatriculation-string; usually a number, but not always (e.g. lecturers, pupils, guests,...) firstName Text -- For export in tables, pre-split firstName from displayName @@ -42,6 +43,12 @@ UserFunction -- Administratively assigned functions (lecturer, admin, evaluation school SchoolId function SchoolFunction UniqueUserFunction user school function +UserSystemFunction + user UserId + function SystemFunction + manual Bool + isOptOut Bool + UniqueUserSystemFunction user function UserExamOffice user UserId field StudyTermsId @@ -58,8 +65,9 @@ StudyFeatures -- multiple entries possible for students pursuing several degree superField StudyTermsId Maybe type StudyFieldType -- Major or minor, i.e. Haupt-/Nebenfach semester Int - updated UTCTime default=now() -- last update from LDAP - valid Bool default=true -- marked as active in LDAP (students may switch, but LDAP never forgets) + firstObserved UTCTime Maybe + lastObserved UTCTime default=now() -- last update from LDAP + valid Bool default=true UniqueStudyFeatures user degree field type semester deriving Eq Show -- UniqueUserSubject ubuser degree field -- There exists a counterexample diff --git a/package-lock.json b/package-lock.json index 756462fda..5a78f8bad 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "uni2work", - "version": "19.2.0", + "version": "20.1.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -20773,6 +20773,11 @@ "integrity": "sha512-VrR/IiH00Z1tFP4vDGfwZ1esNqTiDMchBEXYY9kilT6wRGgFoCAlgkEUMHb1E3mB0FsfZhv756IF0+R+SFPfdg==", "dev": true }, + "typeface-source-code-pro": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/typeface-source-code-pro/-/typeface-source-code-pro-1.1.3.tgz", + "integrity": "sha512-BAQ8I7Xy5zS5+KuG+gjRPNYCdfwL8vSF9jT8q9wzCRiiOG4h7id5zt8wcQx59riGRbRsgycRfs/ubyAm2z/FJQ==" + }, "typeface-source-sans-pro": { "version": "0.0.75", "resolved": "https://registry.npmjs.org/typeface-source-sans-pro/-/typeface-source-sans-pro-0.0.75.tgz", diff --git a/package.json b/package.json index 2b75424df..92a1f56d5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "uni2work", - "version": "19.2.0", + "version": "20.1.0", "description": "", "keywords": [], "author": "", @@ -109,6 +109,7 @@ "tmp": "^0.1.0", "typeface-roboto": "0.0.75", "typeface-source-sans-pro": "0.0.75", + "typeface-source-code-pro": "^1.1.3", "webpack": "^4.44.1", "webpack-cli": "^3.3.12", "webpack-manifest-plugin": "^2.2.0", diff --git a/package.yaml b/package.yaml index 513b569a9..fe4f0c6ce 100644 --- a/package.yaml +++ b/package.yaml @@ -1,5 +1,5 @@ name: uniworx -version: 19.2.0 +version: 20.1.0 dependencies: - base @@ -151,6 +151,7 @@ dependencies: - minio-hs - network-ip - data-textual + - fastcdc other-extensions: - GeneralizedNewtypeDeriving @@ -310,6 +311,7 @@ tests: - generic-arbitrary - http-types - yesod-persistent + - quickcheck-io ghc-options: - -fno-warn-orphans - -threaded -rtsopts "-with-rtsopts=-N -T" diff --git a/routes b/routes index e6420957e..d0c137c64 100644 --- a/routes +++ b/routes @@ -79,10 +79,10 @@ /user/storage-key StorageKeyR POST !free /exam-office ExamOfficeR !exam-office: - / EOExamsR GET + / EOExamsR GET !system-exam-office /fields EOFieldsR GET POST - /users EOUsersR GET POST - /users/invite EOUsersInviteR GET POST + /users EOUsersR GET POST !system-exam-office + /users/invite EOUsersInviteR GET POST !system-exam-office /external-exam EExamListR GET !lecturer !¬empty /external-exam/new EExamNewR GET POST !lecturer diff --git a/src/Application.hs b/src/Application.hs index 0325d9ef8..2eb0b9d46 100644 --- a/src/Application.hs +++ b/src/Application.hs @@ -228,13 +228,15 @@ makeFoundation appSettings'@AppSettings{..} = do forM_ ldapPool $ registerFailoverMetrics "ldap" -- Perform database migration using our application's logging settings. - if - | appAutoDbMigrate -> do - $logDebugS "setup" "Migration" - migrateAll `runSqlPool` sqlPool - | otherwise -> whenM (requiresMigration `runSqlPool` sqlPool) $ do - $logErrorS "setup" "Migration required" - liftIO . exitWith $ ExitFailure 2 + flip runReaderT tempFoundation $ + if + | appAutoDbMigrate -> do + $logDebugS "setup" "Migration" + migrateAll `runSqlPool` sqlPool + | otherwise -> whenM (requiresMigration `runSqlPool` sqlPool) $ do + $logErrorS "setup" "Migration required" + liftIO . exitWith $ ExitFailure 2 + $logDebugS "setup" "Cluster-Config" appCryptoIDKey <- clusterSetting (Proxy :: Proxy 'ClusterCryptoIDKey) `runSqlPool` sqlPool appSecretBoxKey <- clusterSetting (Proxy :: Proxy 'ClusterSecretBoxKey) `runSqlPool` sqlPool diff --git a/src/Auth/Dummy.hs b/src/Auth/Dummy.hs index 859b04554..351893fc9 100644 --- a/src/Auth/Dummy.hs +++ b/src/Auth/Dummy.hs @@ -1,5 +1,6 @@ module Auth.Dummy - ( dummyLogin + ( apDummy + , dummyLogin , DummyMessage(..) ) where @@ -32,6 +33,9 @@ dummyForm = do userList = fmap mkOptionList . runDB $ withReaderT projectBackend (map toOption <$> selectList [] [Asc UserIdent] :: ReaderT SqlBackend _ [Option UserIdent]) toOption (Entity _ User{..}) = Option userDisplayName userIdent (CI.original userIdent) +apDummy :: Text +apDummy = "dummy" + dummyLogin :: forall site. ( YesodAuth site , YesodPersist site @@ -44,7 +48,7 @@ dummyLogin :: forall site. dummyLogin = AuthPlugin{..} where apName :: Text - apName = "dummy" + apName = apDummy apDispatch :: forall m. MonadAuthHandler site m => Text -> [Text] -> m TypedContent apDispatch method [] | encodeUtf8 method == methodPost = liftSubHandler $ do diff --git a/src/Auth/LDAP.hs b/src/Auth/LDAP.hs index 9b57c8904..007178793 100644 --- a/src/Auth/LDAP.hs +++ b/src/Auth/LDAP.hs @@ -10,6 +10,7 @@ module Auth.LDAP , ldapUserMatriculation, ldapUserFirstName, ldapUserSurname , ldapUserTitle, ldapUserStudyFeatures, ldapUserFieldName , ldapUserSchoolAssociation, ldapUserSubTermsSemester, ldapSex + , ldapAffiliation, ldapPrimaryKey ) where import Import.NoFoundation @@ -68,7 +69,7 @@ userSearchSettings LdapConf{..} = mconcat , Ldap.derefAliases Ldap.DerefAlways ] -ldapUserPrincipalName, ldapUserDisplayName, ldapUserMatriculation, ldapUserFirstName, ldapUserSurname, ldapUserTitle, ldapUserStudyFeatures, ldapUserFieldName, ldapUserSchoolAssociation, ldapSex, ldapUserSubTermsSemester :: Ldap.Attr +ldapUserPrincipalName, ldapUserDisplayName, ldapUserMatriculation, ldapUserFirstName, ldapUserSurname, ldapUserTitle, ldapUserStudyFeatures, ldapUserFieldName, ldapUserSchoolAssociation, ldapSex, ldapUserSubTermsSemester, ldapAffiliation, ldapPrimaryKey :: Ldap.Attr ldapUserPrincipalName = Ldap.Attr "userPrincipalName" ldapUserDisplayName = Ldap.Attr "displayName" ldapUserMatriculation = Ldap.Attr "LMU-Stud-Matrikelnummer" @@ -80,6 +81,8 @@ ldapUserFieldName = Ldap.Attr "LMU-Stg-Fach" ldapUserSchoolAssociation = Ldap.Attr "LMU-IFI-eduPersonOrgUnitDNString" ldapSex = Ldap.Attr "schacGender" ldapUserSubTermsSemester = Ldap.Attr "LMU-Stg-FachundFS" +ldapAffiliation = Ldap.Attr "eduPersonAffiliation" +ldapPrimaryKey = Ldap.Attr "eduPersonPrincipalName" ldapUserEmail :: NonEmpty Ldap.Attr ldapUserEmail = Ldap.Attr "mail" :| @@ -200,7 +203,11 @@ campusLogin pool mode = AuthPlugin{..} $logErrorS apName $ "Error during login: " <> tshow err observeLoginOutcome apName LoginError loginErrorMessageI LoginR Msg.AuthError - Right (Left _bindErr) -> do + Right (Left bindErr) -> do + case bindErr of + Ldap.ResponseErrorCode _ _ _ errTxt -> + $logInfoS apName [st|#{campusIdent}: #{errTxt}|] + _other -> return () $logDebugS apName "Invalid credentials" observeLoginOutcome apName LoginInvalidCredentials loginErrorMessageI LoginR Msg.InvalidLogin diff --git a/src/Auth/PWHash.hs b/src/Auth/PWHash.hs index c5c7d53a8..9cca6d440 100644 --- a/src/Auth/PWHash.hs +++ b/src/Auth/PWHash.hs @@ -1,5 +1,6 @@ module Auth.PWHash - ( hashLogin + ( apHash + , hashLogin , PWHashMessage(..) ) where @@ -39,6 +40,8 @@ hashForm = do <$> areq ciField (fslpI MsgPWHashIdent (mr MsgPWHashIdentPlaceholder)) Nothing <*> areq passwordField (fslpI MsgPWHashPassword (mr MsgPWHashPasswordPlaceholder)) Nothing +apHash :: Text +apHash = "PWHash" hashLogin :: forall site. ( YesodAuth site @@ -53,7 +56,7 @@ hashLogin :: forall site. hashLogin pwHashAlgo = AuthPlugin{..} where apName :: Text - apName = "PWHash" + apName = apHash apDispatch :: forall m. MonadAuthHandler site m => Text -> [Text] -> m TypedContent apDispatch method [] | encodeUtf8 method == methodPost = liftSubHandler $ do diff --git a/src/Crypto/Hash/Instances.hs b/src/Crypto/Hash/Instances.hs index 93bf63516..0be90af18 100644 --- a/src/Crypto/Hash/Instances.hs +++ b/src/Crypto/Hash/Instances.hs @@ -18,6 +18,8 @@ import Data.Aeson as Aeson import Control.Monad.Fail +import Language.Haskell.TH.Syntax (Lift(liftTyped)) +import Instances.TH.Lift () instance HashAlgorithm hash => PersistField (Digest hash) where toPersistValue = PersistByteString . convert @@ -46,3 +48,6 @@ instance HashAlgorithm hash => FromJSON (Digest hash) where instance Hashable (Digest hash) where hashWithSalt s = (hashWithSalt s :: ByteString -> Int) . convert + +instance HashAlgorithm hash => Lift (Digest hash) where + liftTyped dgst = [||fromMaybe (error "Lifted digest has wrong length") $ digestFromByteString $$(liftTyped (convert dgst :: ByteString))||] diff --git a/src/Database/Esqueleto/Utils.hs b/src/Database/Esqueleto/Utils.hs index b31708c48..7db0e3c39 100644 --- a/src/Database/Esqueleto/Utils.hs +++ b/src/Database/Esqueleto/Utils.hs @@ -6,8 +6,10 @@ module Database.Esqueleto.Utils , justVal, justValList , isJust , isInfixOf, hasInfix + , strConcat, substring , or, and , any, all + , subSelectAnd, subSelectOr , mkExactFilter, mkExactFilterWith , mkContainsFilter, mkContainsFilterWith , mkExistsFilter @@ -21,22 +23,25 @@ module Database.Esqueleto.Utils , maybe, maybeEq, unsafeCoalesce , bool , max, min + , abs , SqlProject(..) , (->.) , fromSqlKey , selectCountRows , selectMaybe + , day, diffDays , module Database.Esqueleto.Utils.TH ) where -import ClassyPrelude.Yesod hiding (isInfixOf, any, all, or, and, isJust, maybe, bool, max, min) +import ClassyPrelude.Yesod hiding (isInfixOf, any, all, or, and, isJust, maybe, bool, max, min, abs) import Data.Universe import qualified Data.Set as Set import qualified Data.List as List import qualified Data.Foldable as F import qualified Database.Esqueleto as E -import qualified Database.Esqueleto.Internal.Sql as E +import qualified Database.Esqueleto.PostgreSQL as E +import qualified Database.Esqueleto.Internal.Internal as E import Database.Esqueleto.Utils.TH import qualified Data.Text.Lazy as Lazy (Text) @@ -93,6 +98,42 @@ hasInfix :: ( E.SqlString s1 => E.SqlExpr (E.Value s2) -> E.SqlExpr (E.Value s1) -> E.SqlExpr (E.Value Bool) hasInfix = flip isInfixOf +infixl 6 `strConcat` + +strConcat :: E.SqlString s + => E.SqlExpr (E.Value s) -> E.SqlExpr (E.Value s) -> E.SqlExpr (E.Value s) +strConcat = E.unsafeSqlBinOp " || " + +substring :: ( E.SqlString str + , Num from, Num for + ) + => E.SqlExpr (E.Value str) + -> E.SqlExpr (E.Value from) + -> E.SqlExpr (E.Value for) + -> E.SqlExpr (E.Value str) +substring (E.ERaw p1 f1) (E.ERaw p2 f2) (E.ERaw p3 f3) + = E.ERaw E.Never $ \info -> + let (strTLB, strVals) = f1 info + (fromiTLB, fromiVals) = f2 info + (foriTLB, foriVals) = f3 info + in ( "SUBSTRING" <> E.parens (E.parensM p1 strTLB <> " FROM " <> E.parensM p2 fromiTLB <> " FOR " <> E.parensM p3 foriTLB) + , strVals <> fromiVals <> foriVals + ) +substring a b c = substring (construct a) (construct b) (construct c) + where construct :: E.SqlExpr (E.Value a) -> E.SqlExpr (E.Value a) + construct (E.ERaw p f) = E.ERaw E.Parens $ \info -> + let (b1, vals) = f info + build ("?", [E.PersistList vals']) = + (E.uncommas $ replicate (length vals') "?", vals') + build expr = expr + in build (E.parensM p b1, vals) + construct (E.ECompositeKey f) = + E.ERaw E.Parens $ \info -> (E.uncommas $ f info, mempty) + construct (E.EAliasedValue i _) = + E.ERaw E.Never $ E.aliasedValueIdentToRawSql i + construct (E.EValueReference i i') = + E.ERaw E.Never $ E.valueReferenceToRawSql i i' + and, or :: Foldable f => f (E.SqlExpr (E.Value Bool)) -> E.SqlExpr (E.Value Bool) and = F.foldr (E.&&.) true or = F.foldr (E.||.) false @@ -107,6 +148,13 @@ any test = or . map test . otoList all :: MonoFoldable f => (Element f -> E.SqlExpr (E.Value Bool)) -> f -> E.SqlExpr (E.Value Bool) all test = and . map test . otoList +subSelectAnd, subSelectOr :: E.SqlQuery (E.SqlExpr (E.Value Bool)) -> E.SqlExpr (E.Value Bool) +subSelectAnd q = parens . E.subSelectUnsafe $ flip (E.unsafeSqlAggregateFunction "bool_and" E.AggModeAll) [] <$> q +subSelectOr q = parens . E.subSelectUnsafe $ flip (E.unsafeSqlAggregateFunction "bool_or" E.AggModeAll) [] <$> q + +parens :: E.SqlExpr (E.Value a) -> E.SqlExpr (E.Value a) +parens = E.unsafeSqlFunction "" + -- Allow usage of Tuples as DbtRowKey, i.e. SqlIn instances for tuples $(sqlInTuples [2..16]) @@ -289,6 +337,11 @@ max, min :: PersistField a max a b = bool a b $ b E.>. a min a b = bool a b $ b E.<. a +abs :: (PersistField a, Num a) + => E.SqlExpr (E.Value a) + -> E.SqlExpr (E.Value a) +abs x = bool (E.val 0 E.-. x) x $ x E.>. E.val 0 + unsafeCoalesce :: E.PersistField a => [E.SqlExpr (E.Value (Maybe a))] -> E.SqlExpr (E.Value a) unsafeCoalesce = E.veryUnsafeCoerceSqlExprValue . E.coalesce @@ -325,3 +378,13 @@ selectCountRows q = do selectMaybe :: (E.SqlSelect a r, MonadIO m) => E.SqlQuery a -> E.SqlReadT m (Maybe r) selectMaybe = fmap listToMaybe . E.select . (<* E.limit 1) + + +day :: E.SqlExpr (E.Value UTCTime) -> E.SqlExpr (E.Value Day) +day = E.unsafeSqlCastAs "date" + +infixl 6 `diffDays` + +diffDays :: E.SqlExpr (E.Value Day) -> E.SqlExpr (E.Value Day) -> E.SqlExpr (E.Value Int) +-- ^ PostgreSQL is weird. +diffDays a b = E.veryUnsafeCoerceSqlExprValue $ a E.-. b diff --git a/src/Foundation/Authorization.hs b/src/Foundation/Authorization.hs index 991224b2f..9b7b211bd 100644 --- a/src/Foundation/Authorization.hs +++ b/src/Foundation/Authorization.hs @@ -324,6 +324,11 @@ tagAccessPredicate AuthAdmin = APDB $ \mAuthId route _ -> case route of adrights <- lift $ selectFirst [UserFunctionUser ==. authId, UserFunctionFunction ==. SchoolAdmin] [] guardMExceptT (isJust adrights) (unauthorizedI MsgUnauthorizedSiteAdmin) return Authorized +tagAccessPredicate AuthSystemExamOffice = APDB $ \mAuthId _ _ -> $cachedHereBinary mAuthId . exceptT return return $ do + authId <- maybeExceptT AuthenticationRequired $ return mAuthId + isExamOffice <- lift $ exists [UserSystemFunctionUser ==. authId, UserSystemFunctionFunction ==. SystemExamOffice, UserSystemFunctionIsOptOut ==. False] + guardMExceptT isExamOffice $ unauthorizedI MsgUnauthorizedSystemExamOffice + return Authorized tagAccessPredicate AuthExamOffice = APDB $ \mAuthId route _ -> case route of CExamR tid ssh csh examn _ -> $cachedHereBinary (mAuthId, tid, ssh, csh, examn) . exceptT return return $ do authId <- maybeExceptT AuthenticationRequired $ return mAuthId @@ -1191,6 +1196,10 @@ tagAccessPredicate AuthEmpty = APDB $ \mAuthId route _ -> case route of hasExternalExams <- $cachedHereBinary authId . lift . E.selectExists . E.from $ \(eexam `E.InnerJoin` eexamStaff) -> do E.on $ eexam E.^. ExternalExamId E.==. eexamStaff E.^. ExternalExamStaffExam E.where_ $ eexamStaff E.^. ExternalExamStaffUser E.==. E.val authId + E.||. E.exists (E.from $ \externalExamResult -> + E.where_ $ externalExamResult E.^. ExternalExamResultExam E.==. eexam E.^. ExternalExamId + E.&&. externalExamResult E.^. ExternalExamResultUser E.==. E.val authId + ) guardMExceptT (not hasExternalExams) $ unauthorizedI MsgUnauthorizedExternalExamListNotEmpty return Authorized CourseR tid ssh csh _ -> maybeT (unauthorizedI MsgCourseNotEmpty) $ do diff --git a/src/Foundation/I18n.hs b/src/Foundation/I18n.hs index aa514a72d..71543f2d9 100644 --- a/src/Foundation/I18n.hs +++ b/src/Foundation/I18n.hs @@ -219,6 +219,7 @@ embedRenderMessage ''UniWorX ''UploadModeDescr id embedRenderMessage ''UniWorX ''SecretJSONFieldException id embedRenderMessage ''UniWorX ''AFormMessage $ concat . drop 2 . splitCamel embedRenderMessage ''UniWorX ''SchoolFunction id +embedRenderMessage ''UniWorX ''SystemFunction id embedRenderMessage ''UniWorX ''CsvPreset id embedRenderMessage ''UniWorX ''Quoting ("Csv" <>) embedRenderMessage ''UniWorX ''FavouriteReason id diff --git a/src/Foundation/Type.hs b/src/Foundation/Type.hs index 5595127e8..5257f1c35 100644 --- a/src/Foundation/Type.hs +++ b/src/Foundation/Type.hs @@ -7,7 +7,7 @@ module Foundation.Type , _SessionStorageMemcachedSql, _SessionStorageAcid , SMTPPool , _appSettings', _appStatic, _appConnPool, _appSmtpPool, _appLdapPool, _appWidgetMemcached, _appHttpManager, _appLogger, _appLogSettings, _appCryptoIDKey, _appClusterID, _appInstanceID, _appJobState, _appSessionStore, _appSecretBoxKey, _appJSONWebKeySet, _appHealthReport - , DB, Form, MsgRenderer, MailM + , DB, Form, MsgRenderer, MailM, DBFile ) where import Import.NoFoundation @@ -81,3 +81,4 @@ type DB = YesodDB UniWorX type Form x = Html -> MForm (HandlerFor UniWorX) (FormResult x, WidgetFor UniWorX ()) type MsgRenderer = MsgRendererS UniWorX -- see Utils type MailM a = MailT (HandlerFor UniWorX) a +type DBFile = File (YesodDB UniWorX) diff --git a/src/Foundation/Types.hs b/src/Foundation/Types.hs index 4e21dce2f..7cfa5dc0a 100644 --- a/src/Foundation/Types.hs +++ b/src/Foundation/Types.hs @@ -1,6 +1,6 @@ module Foundation.Types ( UpsertCampusUserMode(..) - , _UpsertCampusUser, _UpsertCampusUserDummy, _UpsertCampusUserOther + , _UpsertCampusUserLoginLdap, _UpsertCampusUserLoginDummy, _UpsertCampusUserLoginOther, _UpsertCampusUserLdapSync, _UpsertCampusUserGuessUser , _upsertCampusUserIdent ) where @@ -8,9 +8,11 @@ import Import.NoFoundation data UpsertCampusUserMode - = UpsertCampusUser - | UpsertCampusUserDummy { upsertCampusUserIdent :: UserIdent } - | UpsertCampusUserOther { uspertCampusUserIdent :: UserIdent } + = UpsertCampusUserLoginLdap + | UpsertCampusUserLoginDummy { upsertCampusUserIdent :: UserIdent } + | UpsertCampusUserLoginOther { upsertCampusUserIdent :: UserIdent } + | UpsertCampusUserLdapSync { upsertCampusUserIdent :: UserIdent } + | UpsertCampusUserGuessUser deriving (Eq, Ord, Read, Show, Generic, Typeable) makeLenses_ ''UpsertCampusUserMode diff --git a/src/Foundation/Yesod/Auth.hs b/src/Foundation/Yesod/Auth.hs index 66941c9f6..1c8bd1e61 100644 --- a/src/Foundation/Yesod/Auth.hs +++ b/src/Foundation/Yesod/Auth.hs @@ -14,14 +14,17 @@ import Foundation.I18n import Handler.Utils.Profile import Handler.Utils.StudyFeatures import Handler.Utils.SchoolLdap +import Handler.Utils.LdapSystemFunctions import Yesod.Auth.Message import Auth.LDAP +import Auth.PWHash (apHash) +import Auth.Dummy (apDummy) import qualified Data.CaseInsensitive as CI import qualified Control.Monad.Catch as C (Handler(..)) -import qualified Data.List.NonEmpty as NonEmpty import qualified Ldap.Client as Ldap +import qualified Data.Text as Text import qualified Data.Text.Encoding as Text import qualified Data.ByteString as ByteString import qualified Data.Set as Set @@ -53,8 +56,8 @@ authenticate creds@Creds{..} = liftHandler . runDB . withReaderT projectBackend uAuth = UniqueAuthentication $ CI.mk credsIdent upsertMode = creds ^? _upsertCampusUserMode - isDummy = is (_Just . _UpsertCampusUserDummy) upsertMode - isOther = is (_Just . _UpsertCampusUserOther) upsertMode + isDummy = is (_Just . _UpsertCampusUserLoginDummy) upsertMode + isOther = is (_Just . _UpsertCampusUserLoginOther) upsertMode excRecovery res | isDummy || isOther @@ -127,31 +130,37 @@ data CampusUserConversionException _upsertCampusUserMode :: Traversal' (Creds UniWorX) UpsertCampusUserMode _upsertCampusUserMode mMode cs@Creds{..} - | credsPlugin == "dummy" = setMode <$> mMode (UpsertCampusUserDummy $ CI.mk credsIdent) - | credsPlugin `elem` others = setMode <$> mMode (UpsertCampusUserOther $ CI.mk credsIdent) - | otherwise = setMode <$> mMode UpsertCampusUser + | credsPlugin == apDummy = setMode <$> mMode (UpsertCampusUserLoginDummy $ CI.mk credsIdent) + | credsPlugin == apLdap = setMode <$> mMode UpsertCampusUserLoginLdap + | otherwise = setMode <$> mMode (UpsertCampusUserLoginOther $ CI.mk credsIdent) where - setMode UpsertCampusUser - = cs{ credsPlugin = "LDAP" } - setMode (UpsertCampusUserDummy ident) - = cs{ credsPlugin = "dummy", credsIdent = CI.original ident } - setMode (UpsertCampusUserOther ident) - = cs{ credsPlugin = bool (NonEmpty.head others) credsPlugin (credsPlugin `elem` others), credsIdent = CI.original ident } + setMode UpsertCampusUserLoginLdap + = cs{ credsPlugin = apLdap } + setMode (UpsertCampusUserLoginDummy ident) + = cs{ credsPlugin = apDummy + , credsIdent = CI.original ident + } + setMode (UpsertCampusUserLoginOther ident) + = cs{ credsPlugin = bool defaultOther credsPlugin (credsPlugin /= apDummy && credsPlugin /= apLdap) + , credsIdent = CI.original ident + } + setMode _ = cs - others = "PWHash" :| [] + defaultOther = apHash upsertCampusUser :: forall m. ( MonadHandler m, HandlerSite m ~ UniWorX , MonadThrow m ) => UpsertCampusUserMode -> Ldap.AttrList [] -> SqlPersistT m (Entity User) -upsertCampusUser plugin ldapData = do +upsertCampusUser upsertMode ldapData = do now <- liftIO getCurrentTime UserDefaultConf{..} <- getsYesod $ view _appUserDefaults let userIdent'' = fold [ v | (k, v) <- ldapData, k == ldapUserPrincipalName ] userMatrikelnummer' = fold [ v | (k, v) <- ldapData, k == ldapUserMatriculation ] + userLdapPrimaryKey' = fold [ v | (k, v) <- ldapData, k == ldapPrimaryKey ] userEmail' = fold $ do k' <- toList ldapUserEmail (k, v) <- ldapData @@ -164,17 +173,18 @@ upsertCampusUser plugin ldapData = do userSex' = fold [ v | (k, v) <- ldapData, k == ldapSex ] userAuthentication - | is _UpsertCampusUserOther plugin - = error "PWHash should only work for users that are already known" + | is _UpsertCampusUserLoginOther upsertMode + = error "Non-LDAP logins should only work for users that are already known" | otherwise = AuthLDAP - userLastAuthentication = now <$ guard (isn't _UpsertCampusUserDummy plugin) + userLastAuthentication = guardOn isLogin now + isLogin = has (_UpsertCampusUserLoginLdap <> _UpsertCampusUserLoginOther . united) upsertMode userIdent <- if | [bs] <- userIdent'' , Right userIdent' <- CI.mk <$> Text.decodeUtf8' bs - , hasn't _upsertCampusUserIdent plugin || has (_upsertCampusUserIdent . only userIdent') plugin + , hasn't _upsertCampusUserIdent upsertMode || has (_upsertCampusUserIdent . only userIdent') upsertMode -> return userIdent' - | Just userIdent' <- plugin ^? _upsertCampusUserIdent + | Just userIdent' <- upsertMode ^? _upsertCampusUserIdent -> return userIdent' | otherwise -> throwM CampusUserInvalidIdent @@ -227,6 +237,13 @@ upsertCampusUser plugin ldapData = do -> return Nothing | otherwise -> throwM CampusUserInvalidSex + userLdapPrimaryKey <- if + | [bs] <- userLdapPrimaryKey' + , Right userLdapPrimaryKey'' <- Text.decodeUtf8' bs + , Just userLdapPrimaryKey''' <- assertM' (not . Text.null) $ Text.strip userLdapPrimaryKey'' + -> return $ Just userLdapPrimaryKey''' + | otherwise + -> return Nothing let newUser = User @@ -257,10 +274,15 @@ upsertCampusUser plugin ldapData = do , UserEmail =. userEmail , UserSex =. userSex , UserLastLdapSynchronisation =. Just now + , UserLdapPrimaryKey =. userLdapPrimaryKey ] ++ - [ UserLastAuthentication =. Just now | isn't _UpsertCampusUserDummy plugin ] + [ UserLastAuthentication =. Just now | isLogin ] - user@(Entity userId userRec) <- upsertBy (UniqueAuthentication userIdent) newUser userUpdate + oldUsers <- for userLdapPrimaryKey $ \pKey -> selectKeysList [ UserLdapPrimaryKey ==. Just pKey ] [] + + user@(Entity userId userRec) <- case oldUsers of + Just [oldUserId] -> updateGetEntity oldUserId userUpdate + _other -> upsertBy (UniqueAuthentication userIdent) newUser userUpdate unless (validDisplayName userTitle userFirstName userSurname $ userDisplayName userRec) $ update userId [ UserDisplayName =. userDisplayName' ] @@ -321,7 +343,7 @@ upsertCampusUser plugin ldapData = do , Just defType <- studyTermsDefaultType -> do $logDebugS "Campus" [st|Applying default for standalone study term “#{tshow subterm}”|] - (:) (StudyFeatures userId defDegree subterm Nothing defType subSemester now True) <$> assimilateSubTerms subterms unusedFeats + (:) (StudyFeatures userId defDegree subterm Nothing defType subSemester (Just now) now True) <$> assimilateSubTerms subterms unusedFeats Nothing | [] <- unusedFeats -> do $logDebugS "Campus" [st|Saw subterm “#{tshow subterm}” when no fos-terms remain|] @@ -389,26 +411,11 @@ upsertCampusUser plugin ldapData = do forM_ fs $ \f@StudyFeatures{..} -> do insertMaybe studyFeaturesDegree $ StudyDegree (unStudyDegreeKey studyFeaturesDegree) Nothing Nothing insertMaybe studyFeaturesField $ StudyTerms (unStudyTermsKey studyFeaturesField) Nothing Nothing Nothing Nothing - oldFs <- selectKeysList - [ StudyFeaturesUser ==. studyFeaturesUser - , StudyFeaturesDegree ==. studyFeaturesDegree - , StudyFeaturesField ==. studyFeaturesField - , StudyFeaturesType ==. studyFeaturesType - , StudyFeaturesSemester ==. studyFeaturesSemester - ] - [] - case oldFs of - [oldF] -> update oldF - [ StudyFeaturesUpdated =. now - , StudyFeaturesValid =. True - , StudyFeaturesField =. studyFeaturesField - , StudyFeaturesSuperField =. studyFeaturesSuperField - ] - _other -> void $ upsert f - [ StudyFeaturesUpdated =. now - , StudyFeaturesValid =. True - , StudyFeaturesSuperField =. studyFeaturesSuperField - ] + void $ upsert f + [ StudyFeaturesLastObserved =. now + , StudyFeaturesValid =. True + , StudyFeaturesSuperField =. studyFeaturesSuperField + ] associateUserSchoolsByTerms userId let @@ -440,6 +447,19 @@ upsertCampusUser plugin ldapData = do forM_ ss $ void . insertUnique . SchoolLdap Nothing + let + userSystemFunctions = determineSystemFunctions . Set.fromList $ map CI.mk userSystemFunctions' + userSystemFunctions' = do + (k, v) <- ldapData + guard $ k == ldapAffiliation + v' <- v + Right str <- return $ Text.decodeUtf8' v' + assertM' (not . Text.null) $ Text.strip str + + iforM_ userSystemFunctions $ \func preset -> if + | preset -> void $ upsert (UserSystemFunction userId func False False) [] + | otherwise -> deleteWhere [UserSystemFunctionUser ==. userId, UserSystemFunctionFunction ==. func, UserSystemFunctionIsOptOut ==. False, UserSystemFunctionManual ==. False] + return user where insertMaybe key val = get key >>= maybe (insert_ val) (\_ -> return ()) diff --git a/src/Foundation/Yesod/ErrorHandler.hs b/src/Foundation/Yesod/ErrorHandler.hs index 025b4098d..f24a7ea85 100644 --- a/src/Foundation/Yesod/ErrorHandler.hs +++ b/src/Foundation/Yesod/ErrorHandler.hs @@ -44,29 +44,24 @@ errorHandler err = do [whamlet|
_{MsgErrorResponseEncrypted} -
+
#{ciphertext}
|]
| otherwise -> plaintext
errPage = case err of
NotFound -> [whamlet|_{MsgErrorResponseNotFound}|]
- InternalError err' -> encrypted err' [whamlet|
#{err'}|]
+ InternalError err' -> encrypted err' [whamlet|
#{err'}|]
InvalidArgs errs -> [whamlet|
_{MsgErrorResponseNotAuthenticated}|] PermissionDenied err' -> [whamlet|
#{err'}|] BadMethod method -> [whamlet|
_{MsgErrorResponseBadMethod (decodeUtf8 method)}|] siteLayout (toWgt . mr $ ErrorResponseTitle err) $ do - toWidget - [cassius| - .errMsg - white-space: pre-wrap - font-family: monospace - |] errPage provideRep . fmap PrettyValue $ case err of PermissionDenied err' -> return $ object [ "message" JSON..= err' ] diff --git a/src/Handler/Admin/ErrorMessage.hs b/src/Handler/Admin/ErrorMessage.hs index 64d0d538c..681388175 100644 --- a/src/Handler/Admin/ErrorMessage.hs +++ b/src/Handler/Admin/ErrorMessage.hs @@ -18,8 +18,9 @@ postAdminErrMsgR = do let ctView' = wrapForm ctView def{ formAction = Just . SomeRoute $ AdminErrMsgR, formEncoding = ctEncoding } defaultLayout [whamlet| + $newline never $maybe t <- plaintext -
+
$case t
$of String t'
#{t'}
diff --git a/src/Handler/Admin/Test.hs b/src/Handler/Admin/Test.hs
index 645152b0e..fdfe301ec 100644
--- a/src/Handler/Admin/Test.hs
+++ b/src/Handler/Admin/Test.hs
@@ -224,7 +224,7 @@ postAdminTestR = do
$forall err <- errs
+
#{tshow res} |] diff --git a/src/Handler/Admin/Test/Download.hs b/src/Handler/Admin/Test/Download.hs index dc02ae8e0..9bf85419d 100644 --- a/src/Handler/Admin/Test/Download.hs +++ b/src/Handler/Admin/Test/Download.hs @@ -80,12 +80,12 @@ testDownload = do sourceDBChunks :: ConduitT () Int DB () sourceDBChunks = forever sourceDBFiles .| C.mapM (\x -> x <$ $logDebugS "testDownload.sourceDBChunks" (tshow $ entityKey x)) - .| C.map ((length $!!) . fileContentContent . entityVal) + .| C.map ((length $!!) . fileContentChunkContent . entityVal) .| takeLimit dlMaxSize where - sourceDBFiles = E.selectSource . E.from $ \fileContent -> do + sourceDBFiles = E.selectSource . E.from $ \fileContentChunk -> do E.orderBy [E.asc $ E.random_ @Int64] - return fileContent + return fileContentChunk takeLimit n | n <= 0 = return () takeLimit n = do diff --git a/src/Handler/Allocation/Application.hs b/src/Handler/Allocation/Application.hs index 7f0a6154e..f48db411e 100644 --- a/src/Handler/Allocation/Application.hs +++ b/src/Handler/Allocation/Application.hs @@ -44,7 +44,6 @@ data ApplicationFormView = ApplicationFormView data ApplicationForm = ApplicationForm { afPriority :: Maybe Natural - , afField :: Maybe StudyFeaturesId , afText :: Maybe Text , afFiles :: Maybe FileUploads , afRatingVeto :: Bool @@ -118,12 +117,6 @@ applicationForm maId@(is _Just -> isAlloc) cid uid ApplicationFormMode{..} csrf (False, _ , _ , _ ) -> pure (FormSuccess Nothing, Nothing) - (fieldRes, fieldView') <- if - | afmApplicantEdit || afmLecturer - -> mreq (studyFeaturesFieldFor Nothing False [] $ Just uid) (fslI MsgCourseStudyFeature & setTooltip MsgCourseStudyFeatureTip) (courseApplicationField . entityVal <$> mApp) - | otherwise - -> mforced (studyFeaturesFieldFor Nothing True (maybeToList $ mApp >>= courseApplicationField . entityVal) $ Just uid) (fslI MsgCourseStudyFeature & setTooltip MsgCourseStudyFeatureTip) (mApp >>= courseApplicationField . entityVal) - let textField' = convertField (Text.strip . unTextarea) Textarea textareaField textFs | is _Just courseApplicationsInstructions @@ -216,7 +209,6 @@ applicationForm maId@(is _Just -> isAlloc) cid uid ApplicationFormMode{..} csrf return ( ApplicationForm <$> prioRes - <*> fieldRes <*> textRes <*> filesRes <*> vetoRes @@ -226,8 +218,7 @@ applicationForm maId@(is _Just -> isAlloc) cid uid ApplicationFormMode{..} csrf , ApplicationFormView { afvPriority = prioView , afvForm = catMaybes $ - [ Just fieldView' - , textView + [ textView , filesLinkView , filesWarningView ] ++ maybe [] (map Just) filesView ++ @@ -274,7 +265,6 @@ editApplicationR maId uid cid mAppId afMode allowAction postAction = do appId <- insert CourseApplication { courseApplicationCourse = cid , courseApplicationUser = uid - , courseApplicationField = afField , courseApplicationText = afText , courseApplicationRatingVeto = afRatingVeto , courseApplicationRatingPoints = afRatingPoints @@ -303,8 +293,7 @@ editApplicationR maId uid cid mAppId afMode allowAction postAction = do oldApp <- get404 appId let newApp = oldApp - { courseApplicationField = afField - , courseApplicationText = afText + { courseApplicationText = afText , courseApplicationRatingVeto = afRatingVeto , courseApplicationRatingPoints = afRatingPoints , courseApplicationRatingComment = afRatingComment @@ -323,8 +312,7 @@ editApplicationR maId uid cid mAppId afMode allowAction postAction = do ] appChanged = any (\f -> f oldApp newApp) - [ (/=) `on` courseApplicationField - , (/=) `on` courseApplicationText + [ (/=) `on` courseApplicationText , \_ _ -> not $ Set.null changes ] diff --git a/src/Handler/Course/Application/List.hs b/src/Handler/Course/Application/List.hs index b2b7200b4..57ed2f2db 100644 --- a/src/Handler/Course/Application/List.hs +++ b/src/Handler/Course/Application/List.hs @@ -25,6 +25,7 @@ import qualified Data.Map as Map import qualified Data.Conduit.List as C import Handler.Course.ParticipantInvite +import Handler.Utils.StudyFeatures import Jobs.Queue @@ -33,53 +34,38 @@ type CourseApplicationsTableExpr = ( E.SqlExpr (Entity CourseApplic `E.InnerJoin` E.SqlExpr (Entity User) ) `E.LeftOuterJoin` E.SqlExpr (Maybe (Entity Allocation)) - `E.LeftOuterJoin` ( E.SqlExpr (Maybe (Entity StudyFeatures)) - `E.InnerJoin` E.SqlExpr (Maybe (Entity StudyTerms)) - `E.InnerJoin` E.SqlExpr (Maybe (Entity StudyDegree)) - ) `E.LeftOuterJoin` E.SqlExpr (Maybe (Entity CourseParticipant)) type CourseApplicationsTableData = DBRow ( Entity CourseApplication , Entity User , Bool -- hasFiles , Maybe (Entity Allocation) - , Maybe (Entity StudyFeatures) - , Maybe (Entity StudyTerms) - , Maybe (Entity StudyDegree) , Bool -- isParticipant + , UserTableStudyFeatures ) courseApplicationsIdent :: Text courseApplicationsIdent = "applications" queryCourseApplication :: Getter CourseApplicationsTableExpr (E.SqlExpr (Entity CourseApplication)) -queryCourseApplication = to $ $(sqlIJproj 2 1) . $(sqlLOJproj 4 1) +queryCourseApplication = to $ $(sqlIJproj 2 1) . $(sqlLOJproj 3 1) queryUser :: Getter CourseApplicationsTableExpr (E.SqlExpr (Entity User)) -queryUser = to $ $(sqlIJproj 2 2) . $(sqlLOJproj 4 1) +queryUser = to $ $(sqlIJproj 2 2) . $(sqlLOJproj 3 1) queryHasFiles :: Getter CourseApplicationsTableExpr (E.SqlExpr (E.Value Bool)) -queryHasFiles = to $ hasFiles . $(sqlIJproj 2 1) . $(sqlLOJproj 4 1) +queryHasFiles = to $ hasFiles . $(sqlIJproj 2 1) . $(sqlLOJproj 3 1) where hasFiles appl = E.exists . E.from $ \courseApplicationFile -> E.where_ $ courseApplicationFile E.^. CourseApplicationFileApplication E.==. appl E.^. CourseApplicationId queryAllocation :: Getter CourseApplicationsTableExpr (E.SqlExpr (Maybe (Entity Allocation))) -queryAllocation = to $(sqlLOJproj 4 2) - -queryStudyFeatures :: Getter CourseApplicationsTableExpr (E.SqlExpr (Maybe (Entity StudyFeatures))) -queryStudyFeatures = to $ $(sqlIJproj 3 1) . $(sqlLOJproj 4 3) - -queryStudyTerms :: Getter CourseApplicationsTableExpr (E.SqlExpr (Maybe (Entity StudyTerms))) -queryStudyTerms = to $ $(sqlIJproj 3 2) . $(sqlLOJproj 4 3) - -queryStudyDegree :: Getter CourseApplicationsTableExpr (E.SqlExpr (Maybe (Entity StudyDegree))) -queryStudyDegree = to $ $(sqlIJproj 3 3) . $(sqlLOJproj 4 3) +queryAllocation = to $(sqlLOJproj 3 2) queryCourseParticipant :: Getter CourseApplicationsTableExpr (E.SqlExpr (Maybe (Entity CourseParticipant))) -queryCourseParticipant = to $(sqlLOJproj 4 4) +queryCourseParticipant = to $(sqlLOJproj 3 3) queryIsParticipant :: Getter CourseApplicationsTableExpr (E.SqlExpr (E.Value Bool)) -queryIsParticipant = to $ E.not_ . E.isNothing . (E.?. CourseParticipantId) . $(sqlLOJproj 4 4) +queryIsParticipant = to $ E.not_ . E.isNothing . (E.?. CourseParticipantId) . $(sqlLOJproj 3 3) resultCourseApplication :: Lens' CourseApplicationsTableData (Entity CourseApplication) resultCourseApplication = _dbrOutput . _1 @@ -93,17 +79,11 @@ resultHasFiles = _dbrOutput . _3 resultAllocation :: Traversal' CourseApplicationsTableData (Entity Allocation) resultAllocation = _dbrOutput . _4 . _Just -resultStudyFeatures :: Traversal' CourseApplicationsTableData (Entity StudyFeatures) -resultStudyFeatures = _dbrOutput . _5 . _Just - -resultStudyTerms :: Traversal' CourseApplicationsTableData (Entity StudyTerms) -resultStudyTerms = _dbrOutput . _6 . _Just - -resultStudyDegree :: Traversal' CourseApplicationsTableData (Entity StudyDegree) -resultStudyDegree = _dbrOutput . _7 . _Just - resultIsParticipant :: Lens' CourseApplicationsTableData Bool -resultIsParticipant = _dbrOutput . _8 +resultIsParticipant = _dbrOutput . _5 + +resultStudyFeatures :: Lens' CourseApplicationsTableData UserTableStudyFeatures +resultStudyFeatures = _dbrOutput . _6 newtype CourseApplicationsTableVeto = CourseApplicationsTableVeto Bool @@ -127,9 +107,7 @@ data CourseApplicationsTableCsv = CourseApplicationsTableCsv , csvCAName :: Maybe Text , csvCAEmail :: Maybe UserEmail , csvCAMatriculation :: Maybe Text - , csvCAField :: Maybe Text - , csvCADegree :: Maybe Text - , csvCASemester :: Maybe Int + , csvCAStudyFeatures :: UserTableStudyFeatures , csvCAText :: Maybe Text , csvCAHasFiles :: Maybe Bool , csvCAVeto :: Maybe CourseApplicationsTableVeto @@ -152,9 +130,7 @@ instance Csv.FromNamedRecord CourseApplicationsTableCsv where <*> csv .:?? "name" <*> csv .:?? "email" <*> csv .:?? "matriculation" - <*> csv .:?? "field" - <*> csv .:?? "degree" - <*> csv .:?? "semester" + <*> pure mempty <*> csv .:?? "text" <*> csv .:?? "has-files" <*> csv .:?? "veto" @@ -171,9 +147,7 @@ instance CsvColumnsExplained CourseApplicationsTableCsv where , ('csvCAName , MsgCsvColumnApplicationsName ) , ('csvCAEmail , MsgCsvColumnApplicationsEmail ) , ('csvCAMatriculation, MsgCsvColumnApplicationsMatriculation) - , ('csvCAField , MsgCsvColumnApplicationsField ) - , ('csvCADegree , MsgCsvColumnApplicationsDegree ) - , ('csvCASemester , MsgCsvColumnApplicationsSemester ) + , ('csvCAStudyFeatures, MsgCsvColumnUserStudyFeatures ) , ('csvCAText , MsgCsvColumnApplicationsText ) , ('csvCAHasFiles , MsgCsvColumnApplicationsHasFiles ) , ('csvCAVeto , MsgCsvColumnApplicationsVeto ) @@ -182,19 +156,14 @@ instance CsvColumnsExplained CourseApplicationsTableCsv where ] data CourseApplicationsTableCsvActionClass - = CourseApplicationsTableCsvSetField - | CourseApplicationsTableCsvSetVeto + = CourseApplicationsTableCsvSetVeto | CourseApplicationsTableCsvSetRating | CourseApplicationsTableCsvSetComment deriving (Eq, Ord, Read, Show, Enum, Bounded, Generic, Typeable) embedRenderMessage ''UniWorX ''CourseApplicationsTableCsvActionClass id data CourseApplicationsTableCsvAction - = CourseApplicationsTableCsvSetFieldData - { caCsvActApplication :: CourseApplicationId - , caCsvActField :: Maybe StudyFeaturesId - } - | CourseApplicationsTableCsvSetVetoData + = CourseApplicationsTableCsvSetVetoData { caCsvActApplication :: CourseApplicationId , caCsvActVeto :: Bool } @@ -284,18 +253,12 @@ postCApplicationsR tid ssh csh = do hasFiles <- view queryHasFiles user <- view queryUser allocation <- view queryAllocation - studyFeatures <- view queryStudyFeatures - studyTerms <- view queryStudyTerms - studyDegree <- view queryStudyDegree courseParticipant <- view queryCourseParticipant lift $ do E.on $ E.just (user E.^. UserId) E.==. courseParticipant E.?. CourseParticipantUser E.&&. courseParticipant E.?. CourseParticipantCourse E.==. E.just (E.val cid) E.&&. courseParticipant E.?. CourseParticipantState E.==. E.just (E.val CourseParticipantActive) - E.on $ studyDegree E.?. StudyDegreeId E.==. studyFeatures E.?. StudyFeaturesDegree - E.on $ studyTerms E.?. StudyTermsId E.==. studyFeatures E.?. StudyFeaturesField - E.on $ studyFeatures E.?. StudyFeaturesId E.==. courseApplication E.^. CourseApplicationField E.on $ courseApplication E.^. CourseApplicationAllocation E.==. allocation E.?. AllocationId E.on $ user E.^. UserId E.==. courseApplication E.^. CourseApplicationUser E.&&. courseApplication E.^. CourseApplicationCourse E.==. E.val cid @@ -306,34 +269,38 @@ postCApplicationsR tid ssh csh = do , user , hasFiles , allocation - , studyFeatures - , studyTerms - , studyDegree , E.not_ . E.isNothing $ courseParticipant E.?. CourseParticipantId ) dbtProj :: DBRow _ -> DB CourseApplicationsTableData - dbtProj = traverse $ return . over _3 E.unValue . over _8 E.unValue + dbtProj = traverse $ \(application, user, E.Value hasFiles, allocation, E.Value isParticipant) -> do + feats <- courseUserStudyFeatures (application ^. _entityVal . _courseApplicationCourse) (user ^. _entityKey) + return (application, user, hasFiles, allocation, isParticipant, feats) dbtRowKey = view $ queryCourseApplication . to (E.^. CourseApplicationId) - dbtColonnade :: Colonnade Sortable _ _ + dbtColonnade :: Cornice Sortable ('Cap 'Base) _ _ dbtColonnade = mconcat - [ sortable (Just "participant") (i18nCell MsgCourseApplicationIsParticipant) $ bool mempty (cell $ toWidget iconOK) . view resultIsParticipant - , emptyOpticColonnade (resultAllocation . _entityVal) $ \l -> anchorColonnade (views l allocationLink) $ colAllocationShorthand (l . _allocationShorthand) - , anchorColonnadeM (views (resultCourseApplication . _entityKey) applicationLink) $ colApplicationId (resultCourseApplication . _entityKey) - , anchorColonnadeM (views (resultUser . _entityKey) participantLink) $ colUserDisplayName (resultUser . _entityVal . $(multifocusL 2) _userDisplayName _userSurname) - , lmap (view $ resultUser . _entityVal) colUserEmail - , colUserMatriculation (resultUser . _entityVal . _userMatrikelnummer) - , emptyOpticColonnade (resultStudyTerms . _entityVal) colStudyTerms - , emptyOpticColonnade (resultStudyDegree . _entityVal) colStudyDegree - , emptyOpticColonnade (resultStudyFeatures . _entityVal . _studyFeaturesSemester) colStudyFeaturesSemester - , colApplicationText (resultCourseApplication . _entityVal . _courseApplicationText) - , lmap ((tid, ssh, csh), ) $ colApplicationFiles ($(multifocusG 5) (_1 . _1) (_1 . _2) (_1 . _3) (_2 . resultCourseApplication . _entityKey) (_2 . resultHasFiles)) - , colApplicationVeto (resultCourseApplication . _entityVal . _courseApplicationRatingVeto) - , colApplicationRatingPoints (resultCourseApplication . _entityVal . _courseApplicationRatingPoints) - , colApplicationRatingComment (resultCourseApplication . _entityVal . _courseApplicationRatingComment) + [ cap (Sortable Nothing generatedColumnsHeader) $ mconcat + [ sortable (Just "participant") (i18nCell MsgCourseApplicationIsParticipant) $ bool mempty (cell $ toWidget iconOK) . view resultIsParticipant + , emptyOpticColonnade (resultAllocation . _entityVal) $ \l -> anchorColonnade (views l allocationLink) $ colAllocationShorthand (l . _allocationShorthand) + , anchorColonnadeM (views (resultCourseApplication . _entityKey) applicationLink) $ colApplicationId (resultCourseApplication . _entityKey) + , anchorColonnadeM (views (resultUser . _entityKey) participantLink) $ colUserDisplayName (resultUser . _entityVal . $(multifocusL 2) _userDisplayName _userSurname) + , lmap (view $ resultUser . _entityVal) colUserEmail + , colUserMatriculation (resultUser . _entityVal . _userMatrikelnummer) + , colStudyFeatures resultStudyFeatures + ] + , cap (Sortable Nothing $ i18nCell MsgApplicationUserColumns) $ mconcat + [ colApplicationText (resultCourseApplication . _entityVal . _courseApplicationText) + , lmap ((tid, ssh, csh), ) $ colApplicationFiles ($(multifocusG 5) (_1 . _1) (_1 . _2) (_1 . _3) (_2 . resultCourseApplication . _entityKey) (_2 . resultHasFiles)) + ] + , cap (Sortable Nothing $ i18nCell MsgApplicationRatingColumns) $ mconcat + [ colApplicationVeto (resultCourseApplication . _entityVal . _courseApplicationRatingVeto) + , colApplicationRatingPoints (resultCourseApplication . _entityVal . _courseApplicationRatingPoints) + , colApplicationRatingComment (resultCourseApplication . _entityVal . _courseApplicationRatingComment) + ] ] + where generatedColumnsHeader = cell $ i18n MsgApplicationGeneratedColumns <> (messageTooltip =<< messageI Info MsgApplicationGeneratedColumnsTip) dbtSorting = mconcat [ singletonMap "participant" . SortColumn $ view queryIsParticipant @@ -341,9 +308,6 @@ postCApplicationsR tid ssh csh = do , sortUserName' $ $(multifocusG 2) (queryUser . to (E.^. UserDisplayName)) (queryUser . to (E.^. UserSurname)) , uncurry singletonMap . sortUserEmail $ view queryUser , sortUserMatriculation $ queryUser . to (E.^. UserMatrikelnummer) - , sortStudyTerms queryStudyTerms - , sortStudyDegree queryStudyDegree - , sortStudyFeaturesSemester $ queryStudyFeatures . to (E.?. StudyFeaturesSemester) , sortApplicationText $ queryCourseApplication . to (E.^. CourseApplicationText) , sortApplicationFiles queryHasFiles , sortApplicationVeto $ queryCourseApplication . to (E.^. CourseApplicationRatingVeto) @@ -356,28 +320,37 @@ postCApplicationsR tid ssh csh = do , fltrUserName' $ queryUser . to (E.^. UserDisplayName) , uncurry singletonMap . fltrUserEmail $ view queryUser , fltrUserMatriculation $ queryUser . to (E.^. UserMatrikelnummer) - , fltrStudyTerms queryStudyTerms - , fltrStudyDegree queryStudyDegree - , fltrStudyFeaturesSemester $ queryStudyFeatures . to (E.?. StudyFeaturesSemester) , fltrApplicationText $ queryCourseApplication . to (E.^. CourseApplicationText) , fltrApplicationFiles queryHasFiles , fltrApplicationVeto $ queryCourseApplication . to (E.^. CourseApplicationRatingVeto) , fltrApplicationRatingPoints $ queryCourseApplication . to (E.^. CourseApplicationRatingPoints) , fltrApplicationRatingComment $ queryCourseApplication . to (E.^. CourseApplicationRatingComment) + , fltrRelevantStudyFeaturesTerms (to $ + \t -> ( E.val courseTerm + , views queryUser (E.^. UserId) t + )) + , fltrRelevantStudyFeaturesDegree (to $ + \t -> ( E.val courseTerm + , views queryUser (E.^. UserId) t + )) + , fltrRelevantStudyFeaturesSemester (to $ + \t -> ( E.val courseTerm + , views queryUser (E.^. UserId) t + )) ] dbtFilterUI = mconcat [ fltrAllocationUI , fltrUserNameUI' , fltrUserMatriculationUI , fltrUserEmailUI - , fltrStudyTermsUI - , fltrStudyDegreeUI - , fltrStudyFeaturesSemesterUI , fltrApplicationTextUI , fltrApplicationFilesUI , fltrApplicationVetoUI , fltrApplicationRatingPointsUI , fltrApplicationRatingCommentUI + , fltrRelevantStudyFeaturesTermsUI + , fltrRelevantStudyFeaturesDegreeUI + , fltrRelevantStudyFeaturesSemesterUI ] dbtStyle = def @@ -391,9 +364,7 @@ postCApplicationsR tid ssh csh = do <*> preview (resultUser . _entityVal . _userDisplayName) <*> preview (resultUser . _entityVal . _userEmail) <*> preview (resultUser . _entityVal . _userMatrikelnummer . _Just) - <*> preview (resultStudyTerms . _entityVal . (_studyTermsName . _Just <> _studyTermsShorthand . _Just <> to (tshow . studyTermsKey))) - <*> preview (resultStudyDegree . _entityVal . (_studyDegreeName . _Just <> _studyDegreeShorthand . _Just <> to (tshow . studyDegreeKey))) - <*> preview (resultStudyFeatures . _entityVal . _studyFeaturesSemester) + <*> view resultStudyFeatures <*> preview (resultCourseApplication . _entityVal . _courseApplicationText . _Just) <*> preview resultHasFiles <*> preview (resultCourseApplication . _entityVal . _courseApplicationRatingVeto . re _CourseApplicationsTableVeto) @@ -416,10 +387,6 @@ postCApplicationsR tid ssh csh = do DBCsvDiffExisting{..} -> do let appId = dbCsvOld ^. resultCourseApplication . _entityKey - newFeatures <- lift $ lookupStudyFeatures dbCsvNew - when (newFeatures /= dbCsvOld ^? resultStudyFeatures . _entityKey) $ - yield $ CourseApplicationsTableCsvSetFieldData appId newFeatures - let mVeto = dbCsvNew ^? _csvCAVeto . _Just . _CourseApplicationsTableVeto whenIsJust mVeto $ \veto -> when (veto /= dbCsvOld ^. resultCourseApplication . _entityVal . _courseApplicationRatingVeto) $ @@ -431,18 +398,12 @@ postCApplicationsR tid ssh csh = do when (dbCsvNew ^. _csvCAComment /= dbCsvOld ^. resultCourseApplication . _entityVal . _courseApplicationRatingComment) $ yield $ CourseApplicationsTableCsvSetCommentData appId (dbCsvNew ^. _csvCAComment) , dbtCsvClassifyAction = \case - CourseApplicationsTableCsvSetFieldData{} -> CourseApplicationsTableCsvSetField CourseApplicationsTableCsvSetVetoData{} -> CourseApplicationsTableCsvSetVeto CourseApplicationsTableCsvSetRatingData{} -> CourseApplicationsTableCsvSetRating CourseApplicationsTableCsvSetCommentData{} -> CourseApplicationsTableCsvSetComment , dbtCsvCoarsenActionClass = const DBCsvActionExisting , dbtCsvExecuteActions = do C.mapM_ $ \case - CourseApplicationsTableCsvSetFieldData{..} -> do - CourseApplication{..} <- updateGet caCsvActApplication [ CourseApplicationField =. caCsvActField - , CourseApplicationTime =. now - ] - audit $ TransactionCourseApplicationEdit cid courseApplicationUser caCsvActApplication CourseApplicationsTableCsvSetVetoData{..} -> do CourseApplication{..} <- updateGet caCsvActApplication [ CourseApplicationRatingVeto =. caCsvActVeto , CourseApplicationRatingTime =. Just now @@ -460,15 +421,6 @@ postCApplicationsR tid ssh csh = do audit $ TransactionCourseApplicationEdit cid courseApplicationUser caCsvActApplication return $ CourseR tid ssh csh CApplicationsR , dbtCsvRenderKey = \(existingApplicantName -> existingApplicantName') -> \case - CourseApplicationsTableCsvSetFieldData{..} -> - [whamlet| - $newline never - ^{existingApplicantName' caCsvActApplication} - $maybe features <- caCsvActField - , ^{studyFeaturesWidget features} - $nothing - , _{MsgCourseStudyFeatureNone} - |] CourseApplicationsTableCsvSetVetoData{..} -> [whamlet| $newline never @@ -538,59 +490,6 @@ postCApplicationsR tid ssh csh = do where Entity _ User{..} = existing ^. singular (ix appId . resultUser) - lookupStudyFeatures :: CourseApplicationsTableCsv -> DB (Maybe StudyFeaturesId) - lookupStudyFeatures csv@CourseApplicationsTableCsv{..} = do - appRes <- guessUser csv - (uid, oldFeatures) <- case appRes of - Left uid -> (uid, ) <$> selectList [ CourseApplicationUser ==. uid, CourseApplicationCourse ==. cid ] [] - Right appId -> (courseApplicationUser . entityVal &&& pure) <$> getJustEntity appId - studyFeatures <- E.select . E.from $ \(studyFeatures `E.InnerJoin` studyDegree `E.InnerJoin` studyTerms) -> - E.distinctOnOrderBy [ E.asc (studyFeatures E.^. StudyFeaturesField) - , E.asc (studyFeatures E.^. StudyFeaturesDegree) - , E.asc (studyFeatures E.^. StudyFeaturesSemester)] $ do - E.on $ studyTerms E.^. StudyTermsId E.==. studyFeatures E.^. StudyFeaturesField - E.on $ studyDegree E.^. StudyDegreeId E.==. studyFeatures E.^. StudyFeaturesDegree - E.where_ . E.and $ catMaybes - [ do - field <- csvCAField - return . E.or $ catMaybes - [ Just $ studyTerms E.^. StudyTermsName `E.ciEq` E.just (E.val field) - , Just $ studyTerms E.^. StudyTermsShorthand `E.ciEq` E.just (E.val field) - , (studyTerms E.^. StudyTermsKey E.==.) . E.val <$> readMay field - ] - , do - degree <- csvCADegree - return . E.or $ catMaybes - [ Just $ studyDegree E.^. StudyDegreeName `E.ciEq` E.just (E.val degree) - , Just $ studyDegree E.^. StudyDegreeShorthand `E.ciEq` E.just (E.val degree) - , (studyDegree E.^. StudyDegreeKey E.==.) . E.val <$> readMay degree - ] - , (studyFeatures E.^. StudyFeaturesSemester E.==.) . E.val <$> csvCASemester - ] - E.where_ $ studyFeatures E.^. StudyFeaturesUser E.==. E.val uid - let isActiveOrPrevious = E.or - $ (studyFeatures E.^. StudyFeaturesValid) - : [ E.val sfid E.==. studyFeatures E.^. StudyFeaturesId - | Entity _ CourseApplication{ courseApplicationField = Just sfid } <- oldFeatures - ] - E.where_ isActiveOrPrevious -- either active studyFeature or the one previously associated with this course - E.orderBy [E.desc isActiveOrPrevious, E.asc (E.orderByOrd $ studyFeatures E.^. StudyFeaturesType)] - return $ studyFeatures E.^. StudyFeaturesId - case studyFeatures of - [E.Value fid] -> return $ Just fid - _other - | is _Nothing csvCAField - , is _Nothing csvCADegree - , is _Nothing csvCASemester - -> return Nothing - _other - | [Entity _ CourseApplication{..}] <- oldFeatures - , Just sfid <- courseApplicationField - , E.Value sfid `elem` studyFeatures - -> return $ Just sfid - _other -> throwM CourseApplicationsTableCsvExceptionNoMatchingStudyFeatures - - dbtIdent = courseApplicationsIdent psValidator :: PSValidator _ _ diff --git a/src/Handler/Course/ParticipantInvite.hs b/src/Handler/Course/ParticipantInvite.hs index dab5b62e2..f7c9ea350 100644 --- a/src/Handler/Course/ParticipantInvite.hs +++ b/src/Handler/Course/ParticipantInvite.hs @@ -40,7 +40,6 @@ instance IsInvitableJunction CourseParticipant where type InvitationFor CourseParticipant = Course data InvitableJunction CourseParticipant = JunctionParticipant { jParticipantRegistration :: UTCTime - , jParticipantField :: Maybe StudyFeaturesId , jParticipantAllocated :: Maybe AllocationId , jParticipantState :: CourseParticipantState } deriving (Eq, Ord, Read, Show, Generic, Typeable) @@ -53,8 +52,8 @@ instance IsInvitableJunction CourseParticipant where deriving (Eq, Ord, Read, Show, Generic, Typeable) _InvitableJunction = iso - (\CourseParticipant{..} -> (courseParticipantUser, courseParticipantCourse, JunctionParticipant courseParticipantRegistration courseParticipantField courseParticipantAllocated courseParticipantState)) - (\(courseParticipantUser, courseParticipantCourse, JunctionParticipant courseParticipantRegistration courseParticipantField courseParticipantAllocated courseParticipantState) -> CourseParticipant{..}) + (\CourseParticipant{..} -> (courseParticipantUser, courseParticipantCourse, JunctionParticipant courseParticipantRegistration courseParticipantAllocated courseParticipantState)) + (\(courseParticipantUser, courseParticipantCourse, JunctionParticipant courseParticipantRegistration courseParticipantAllocated courseParticipantState) -> CourseParticipant{..}) instance ToJSON (InvitableJunction CourseParticipant) where toJSON = genericToJSON defaultOptions { fieldLabelModifier = camelToPathPiece' 1 } @@ -92,11 +91,9 @@ participantInvitationConfig = InvitationConfig{..} itAuthority <- HashSet.singleton . Right <$> liftHandler requireAuthId return $ InvitationTokenConfig itAuthority Nothing Nothing Nothing invitationRestriction _ _ = return Authorized - invitationForm _ _ uid = hoistAForm lift . wFormToAForm $ do + invitationForm _ _ _ = hoistAForm lift . wFormToAForm $ do now <- liftIO getCurrentTime - studyFeatures <- wreq (studyFeaturesFieldFor Nothing False [] $ Just uid) - (fslI MsgCourseStudyFeature & setTooltip MsgCourseStudyFeatureTip) Nothing - return . fmap (, ()) $ JunctionParticipant now <$> studyFeatures <*> pure Nothing <*> pure CourseParticipantActive + return . pure . (, ()) $ JunctionParticipant now Nothing CourseParticipantActive invitationInsertHook _ _ (_, InvTokenDataParticipant{..}) CourseParticipant{..} _ act = do deleteBy $ UniqueParticipant courseParticipantUser courseParticipantCourse -- there are no foreign key references to @{CourseParticipant}; therefor we can delete and recreate to simulate upsert res <- act -- insertUnique @@ -109,7 +106,6 @@ participantInvitationConfig = InvitationConfig{..} data AddParticipantsResult = AddParticipantsResult { aurAlreadyRegistered - , aurNoUniquePrimaryField , aurSuccess :: Set UserId } deriving (Read, Show, Generic, Typeable) @@ -169,20 +165,14 @@ addParticipantsResultMessages :: (MonadHandler m, HandlerSite m ~ UniWorX) => AddParticipantsResult -> ReaderT (YesodPersistBackend UniWorX) m [Message] addParticipantsResultMessages AddParticipantsResult{..} = execWriterT $ do - (aurAlreadyRegistered', aurNoUniquePrimaryField') <- - (,) <$> fmap sort (lift . mapM (fmap userEmail . getJust) $ Set.toList aurAlreadyRegistered) - <*> fmap sort (lift . mapM (fmap userEmail . getJust) $ Set.toList aurNoUniquePrimaryField) + aurAlreadyRegistered' <- + fmap sort (lift . mapM (fmap userEmail . getJust) $ Set.toList aurAlreadyRegistered) unless (null aurAlreadyRegistered) $ do let modalTrigger = [whamlet|_{MsgCourseParticipantsAlreadyRegistered (length aurAlreadyRegistered)}|] modalContent = $(widgetFile "messages/courseInvitationAlreadyRegistered") tell . pure <=< messageWidget Info $ msgModal modalTrigger (Right modalContent) - unless (null aurNoUniquePrimaryField) $ do - let modalTrigger = [whamlet|_{MsgCourseParticipantsRegisteredWithoutField (length aurNoUniquePrimaryField)}|] - modalContent = $(widgetFile "messages/courseInvitationRegisteredWithoutField") - tell . pure <=< messageWidget Warning $ msgModal modalTrigger (Right modalContent) - unless (null aurSuccess) $ tell . pure <=< messageI Success . MsgCourseParticipantsRegistered $ length aurSuccess @@ -200,18 +190,6 @@ registerUser' cid uid mbGrp = exceptT tell tell $ do whenM (lift . lift $ exists [CourseParticipantCourse ==. cid, CourseParticipantUser ==. uid, CourseParticipantState ==. CourseParticipantActive]) $ throwError $ mempty { aurAlreadyRegistered = Set.singleton uid } - features <- lift . lift $ selectKeysList [ StudyFeaturesUser ==. uid, StudyFeaturesValid ==. True, StudyFeaturesType ==. FieldPrimary ] [] - applications <- lift . lift $ selectList [ CourseApplicationCourse ==. cid, CourseApplicationUser ==. uid ] [] - - let courseParticipantField - | [f] <- features - = Just f - | [f'] <- nub $ mapMaybe (courseApplicationField . entityVal) applications - , f' `elem` features - = Just f' - | otherwise - = Nothing - courseParticipantRegistration <- liftIO getCurrentTime void . lift . lift $ upsert CourseParticipant @@ -222,7 +200,6 @@ registerUser' cid uid mbGrp = exceptT tell tell $ do , .. } [ CourseParticipantRegistration =. courseParticipantRegistration - , CourseParticipantField =. courseParticipantField , CourseParticipantAllocated =. Nothing , CourseParticipantState =. CourseParticipantActive ] @@ -231,9 +208,7 @@ registerUser' cid uid mbGrp = exceptT tell tell $ do void . lift . lift $ setUserSubmissionGroup cid uid mbGrp - return $ case courseParticipantField of - Nothing -> mempty { aurNoUniquePrimaryField = Set.singleton uid } - Just _ -> mempty { aurSuccess = Set.singleton uid } + return $ mempty { aurSuccess = Set.singleton uid } getCInviteR, postCInviteR :: TermId -> SchoolId -> CourseShorthand -> Handler Html diff --git a/src/Handler/Course/Register.hs b/src/Handler/Course/Register.hs index e0cd7f593..92297d3d9 100644 --- a/src/Handler/Course/Register.hs +++ b/src/Handler/Course/Register.hs @@ -9,6 +9,7 @@ module Handler.Course.Register import Import import Handler.Utils +import Handler.Utils.Exam import Utils.Course @@ -41,8 +42,7 @@ instance Button UniWorX ButtonCourseRegister where data CourseRegisterForm = CourseRegisterForm - { crfStudyFeatures :: Maybe StudyFeaturesId - , crfApplicationText :: Maybe Text + { crfApplicationText :: Maybe Text , crfApplicationFiles :: Maybe FileUploads } @@ -82,17 +82,6 @@ courseRegisterForm (Entity cid Course{..}) = liftHandler $ do | otherwise -> return $ FormSuccess () - fieldRes <- if - | is _Nothing muid - -> return $ FormSuccess Nothing - | is _Just muid - , isRegistered - , Just mFeature <- courseApplicationField . entityVal <$> application - <|> courseParticipantField . entityVal <$> registration - -> wforced (studyFeaturesFieldFor Nothing True (maybeToList mFeature) muid) (fslI MsgCourseStudyFeature & setTooltip MsgCourseStudyFeatureTip) mFeature - | otherwise - -> wreq (studyFeaturesFieldFor Nothing False [] muid) (fslI MsgCourseStudyFeature & setTooltip MsgCourseStudyFeatureTip) Nothing - appTextRes <- let fs | courseApplicationsRequired , is _Just courseApplicationsInstructions = fslI MsgCourseApplicationText & setTooltip MsgCourseApplicationFollowInstructions @@ -167,7 +156,6 @@ courseRegisterForm (Entity cid Course{..}) = liftHandler $ do return $ CourseRegisterForm <$ secretRes - <*> fieldRes <*> appTextRes <*> appFilesRes @@ -200,7 +188,7 @@ postCRegisterR tid ssh csh = do = void <$> do appIds <- selectKeysList [ CourseApplicationAllocation ==. Nothing, CourseApplicationCourse ==. cid, CourseApplicationUser ==. uid ] [] appRes <- case appIds of - [] -> insertUnique $ CourseApplication cid uid crfStudyFeatures crfApplicationText False Nothing Nothing Nothing Nothing cTime Nothing + [] -> insertUnique $ CourseApplication cid uid crfApplicationText False Nothing Nothing Nothing Nothing cTime Nothing (prevId:ps) -> do forM_ ps $ \appId -> do deleteApplicationFiles appId @@ -208,7 +196,7 @@ postCRegisterR tid ssh csh = do audit $ TransactionCourseApplicationDeleted cid uid appId deleteApplicationFiles prevId - update prevId [ CourseApplicationField =. crfStudyFeatures, CourseApplicationText =. crfApplicationText, CourseApplicationTime =. cTime ] + update prevId [ CourseApplicationText =. crfApplicationText, CourseApplicationTime =. cTime ] return $ Just prevId @@ -222,9 +210,8 @@ postCRegisterR tid ssh csh = do mkRegistration = do audit $ TransactionCourseParticipantEdit cid uid entityKey <$> upsert - (CourseParticipant cid uid cTime crfStudyFeatures Nothing CourseParticipantActive) + (CourseParticipant cid uid cTime Nothing CourseParticipantActive) [ CourseParticipantRegistration =. cTime - , CourseParticipantField =. crfStudyFeatures , CourseParticipantAllocated =. Nothing , CourseParticipantState =. CourseParticipantActive ] @@ -297,9 +284,8 @@ deregisterParticipant uid cid = do E.where_ $ exam E.^. ExamCourse E.==. E.val cid E.&&. examRegistration E.^. ExamRegistrationUser E.==. E.val uid return examRegistration - forM_ examRegistrations $ \(Entity erId ExamRegistration{..}) -> do - delete erId - audit $ TransactionExamDeregister examRegistrationExam uid + forM_ examRegistrations $ \(Entity _ ExamRegistration{..}) -> do + deregisterExamUsers examRegistrationExam $ pure examRegistrationUser E.delete . E.from $ \tutorialParticipant -> do let tutorialCourse = E.subSelectForeign tutorialParticipant TutorialParticipantTutorial (E.^. TutorialCourse) diff --git a/src/Handler/Course/User.hs b/src/Handler/Course/User.hs index e7ad89d12..de8747fc4 100644 --- a/src/Handler/Course/User.hs +++ b/src/Handler/Course/User.hs @@ -22,6 +22,8 @@ import Jobs.Queue import Handler.Submission.List +import Handler.Utils.StudyFeatures + import qualified Data.Map as Map import qualified Data.Text as Text import qualified Data.CaseInsensitive as CI @@ -93,36 +95,15 @@ courseUserProfileSection (Entity cid Course{..}) (Entity uid User{ userShowSex = (mRegistration, studies) <- lift . runDB $ do registration <- fmap (assertM . has $ _entityVal . _courseParticipantState . _CourseParticipantActive) . getBy $ UniqueParticipant uid cid - studies <- E.select $ E.from $ \(studydegree `E.InnerJoin` studyfeat `E.InnerJoin` studyterms) -> do - E.where_ $ studyfeat E.^. StudyFeaturesUser E.==. E.val uid + studies <- E.select $ E.from $ \(course `E.InnerJoin` studyfeat `E.InnerJoin` studydegree `E.InnerJoin` studyterms) -> do E.on $ studyfeat E.^. StudyFeaturesField E.==. studyterms E.^. StudyTermsId E.on $ studyfeat E.^. StudyFeaturesDegree E.==. studydegree E.^. StudyDegreeId + E.on $ isCourseStudyFeature course studyfeat + E.where_ $ studyfeat E.^. StudyFeaturesUser E.==. E.val uid + E.where_ $ course E.^. CourseId E.==. E.val cid return (studyfeat, studydegree, studyterms) return (registration, studies) - ((regFieldRes, regFieldView), regFieldEnctype) <- lift . runFormPost . identifyForm FIDcRegField $ \csrf -> - let currentField :: Maybe (Maybe StudyFeaturesId) - currentField = courseParticipantField . entityVal <$> mRegistration - in over _2 ((toWidget csrf <>) . fvWidget) <$> mreq (studyFeaturesFieldFor Nothing True (maybeToList $ join currentField) $ Just uid) ("" & addAutosubmit) currentField - - let registrationFieldFrag :: Text - registrationFieldFrag = "registration-field" - regFieldWidget = wrapForm regFieldView FormSettings - { formMethod = POST - , formAction = Just . SomeRoute $ currentRoute :#: registrationFieldFrag - , formEncoding = regFieldEnctype - , formAttrs = [] - , formSubmit = FormAutoSubmit - , formAnchor = Just registrationFieldFrag - } - for_ mRegistration $ \(Entity pId CourseParticipant{}) -> - formResult regFieldRes $ \courseParticipantField' -> do - lift . runDB $ do - update pId [ CourseParticipantField =. courseParticipantField' ] - audit $ TransactionCourseParticipantEdit cid uid - addMessageI Success MsgCourseStudyFeatureUpdated - redirect $ currentRoute :#: registrationFieldFrag - mayRegister <- hasWriteAccessTo $ CourseR courseTerm courseSchool courseShorthand CAddUserR let regButton | is _Just mRegistration = BtnCourseDeregister @@ -179,16 +160,10 @@ courseUserProfileSection (Entity cid Course{..}) (Entity uid User{ userShowSex = -> invalidArgs ["User not registered"] (BtnCourseRegister, _) -> do now <- liftIO getCurrentTime - let field - | [(Entity featId _, _, _)] <- filter (\(Entity _ StudyFeatures{..}, _, _) -> studyFeaturesValid) studies - = Just featId - | otherwise - = Nothing lift . runDBJobs $ do void $ upsert - (CourseParticipant cid uid now field Nothing CourseParticipantActive) + (CourseParticipant cid uid now Nothing CourseParticipantActive) [ CourseParticipantRegistration =. now - , CourseParticipantField =. field , CourseParticipantAllocated =. Nothing , CourseParticipantState =. CourseParticipantActive ] diff --git a/src/Handler/Course/Users.hs b/src/Handler/Course/Users.hs index f12e7993d..f6c31ef4e 100644 --- a/src/Handler/Course/Users.hs +++ b/src/Handler/Course/Users.hs @@ -4,7 +4,7 @@ module Handler.Course.Users ( queryUser , makeCourseUserTable , postCUsersR, getCUsersR - , colUserDegreeShort, colUserField, colUserSemester, colUserSex' + , colUserSex', _userStudyFeatures ) where import Import @@ -16,6 +16,7 @@ import qualified Database.Esqueleto.Utils as E import Database.Esqueleto.Utils.TH import Handler.Course.Register (deregisterParticipant) +import Handler.Utils.StudyFeatures import qualified Data.Set as Set import qualified Data.Map as Map @@ -39,10 +40,6 @@ type UserTableExpr = ( E.SqlExpr (Entity User) `E.InnerJoin` E.SqlExpr (Entity CourseParticipant) ) `E.LeftOuterJoin` E.SqlExpr (Maybe (Entity CourseUserNote)) - `E.LeftOuterJoin` ( E.SqlExpr (Maybe (Entity StudyFeatures)) - `E.InnerJoin` E.SqlExpr (Maybe (Entity StudyDegree)) - `E.InnerJoin` E.SqlExpr (Maybe (Entity StudyTerms)) - ) `E.LeftOuterJoin` ( E.SqlExpr (Maybe (Entity SubmissionGroup)) `E.InnerJoin` E.SqlExpr (Maybe (Entity SubmissionGroupUser)) ) @@ -53,54 +50,43 @@ type UserTableExpr = ( E.SqlExpr (Entity User) -- Sql-Getters for this query, used for sorting and filtering (cannot be lenses due to being Esqueleto expressions) -- This ought to ease refactoring the query queryUser :: UserTableExpr -> E.SqlExpr (Entity User) -queryUser = $(sqlIJproj 2 1) . $(sqlLOJproj 4 1) +queryUser = $(sqlIJproj 2 1) . $(sqlLOJproj 3 1) queryParticipant :: UserTableExpr -> E.SqlExpr (Entity CourseParticipant) -queryParticipant = $(sqlIJproj 2 2) . $(sqlLOJproj 4 1) +queryParticipant = $(sqlIJproj 2 2) . $(sqlLOJproj 3 1) queryUserNote :: UserTableExpr -> E.SqlExpr (Maybe (Entity CourseUserNote)) -queryUserNote = $(sqlLOJproj 4 2) - -queryFeaturesStudy :: UserTableExpr -> E.SqlExpr (Maybe (Entity StudyFeatures)) -queryFeaturesStudy = $(sqlIJproj 3 1) . $(sqlLOJproj 4 3) - -queryFeaturesDegree :: UserTableExpr -> E.SqlExpr (Maybe (Entity StudyDegree)) -queryFeaturesDegree = $(sqlIJproj 3 2) . $(sqlLOJproj 4 3) - -queryFeaturesField :: UserTableExpr -> E.SqlExpr (Maybe (Entity StudyTerms)) -queryFeaturesField = $(sqlIJproj 3 3) . $(sqlLOJproj 4 3) +queryUserNote = $(sqlLOJproj 3 2) querySubmissionGroup :: UserTableExpr -> E.SqlExpr (Maybe (Entity SubmissionGroup)) -querySubmissionGroup = $(sqlIJproj 2 1) . $(sqlLOJproj 4 4) +querySubmissionGroup = $(sqlIJproj 2 1) . $(sqlLOJproj 3 3) userTableQuery :: CourseId -> UserTableExpr -> E.SqlQuery ( E.SqlExpr (Entity User) , E.SqlExpr (Entity CourseParticipant) , E.SqlExpr (E.Value (Maybe (Key CourseUserNote))) - , StudyFeaturesDescription' , E.SqlExpr (Maybe (Entity SubmissionGroup)) ) -userTableQuery cid ((user `E.InnerJoin` participant) `E.LeftOuterJoin` note `E.LeftOuterJoin` studyFeatures `E.LeftOuterJoin` (subGroup `E.InnerJoin` subGroupUser)) = do +userTableQuery cid ((user `E.InnerJoin` participant) `E.LeftOuterJoin` note `E.LeftOuterJoin` (subGroup `E.InnerJoin` subGroupUser)) = do -- Note that order of E.on for nested joins is seemingly right-to-left, ignoring nesting paranthesis E.on $ subGroup E.?. SubmissionGroupId E.==. subGroupUser E.?. SubmissionGroupUserSubmissionGroup E.on $ subGroupUser E.?. SubmissionGroupUserUser E.==. E.just (user E.^. UserId) E.&&. subGroup E.?. SubmissionGroupCourse E.==. E.just (E.val cid) - features <- studyFeaturesQuery' (participant E.^. CourseParticipantField) studyFeatures E.on $ (note E.?. CourseUserNoteUser E.==. E.just (participant E.^. CourseParticipantUser)) E.&&. (note E.?. CourseUserNoteCourse E.==. E.just (E.val cid)) E.on $ participant E.^. CourseParticipantUser E.==. user E.^. UserId E.where_ $ participant E.^. CourseParticipantCourse E.==. E.val cid - return (user, participant, note E.?. CourseUserNoteId, features, subGroup) + return (user, participant, note E.?. CourseUserNoteId, subGroup) type UserTableData = DBRow ( Entity User , Entity CourseParticipant , Maybe CourseUserNoteId - , (Maybe StudyFeatures, Maybe StudyDegree, Maybe StudyTerms) , ([Entity Tutorial], Map (CI Text) (Maybe (Entity Tutorial))) , [Entity Exam] , Maybe (Entity SubmissionGroup) , Map SheetName (SheetType, Maybe Points) + , UserTableStudyFeatures ) instance HasEntity UserTableData User where @@ -118,23 +104,20 @@ _userTableRegistration = _userTableParticipant . _entityVal . _courseParticipant _userTableNote :: Lens' UserTableData (Maybe CourseUserNoteId) _userTableNote = _dbrOutput . _3 -_userTableFeatures :: Lens' UserTableData (Maybe StudyFeatures, Maybe StudyDegree, Maybe StudyTerms) -_userTableFeatures = _dbrOutput . _4 - -_rowUserSemester :: Traversal' UserTableData Int -_rowUserSemester = _userTableFeatures . _1 . _Just . _studyFeaturesSemester - _userTutorials :: Lens' UserTableData ([Entity Tutorial], Map (CI Text) (Maybe (Entity Tutorial))) -_userTutorials = _dbrOutput . _5 +_userTutorials = _dbrOutput . _4 _userExams :: Lens' UserTableData [Entity Exam] -_userExams = _dbrOutput . _6 +_userExams = _dbrOutput . _5 _userSubmissionGroup :: Traversal' UserTableData (Entity SubmissionGroup) -_userSubmissionGroup = _dbrOutput . _7 . _Just +_userSubmissionGroup = _dbrOutput . _6 . _Just _userSheets :: Lens' UserTableData (Map SheetName (SheetType, Maybe Points)) -_userSheets = _dbrOutput . _8 +_userSheets = _dbrOutput . _7 + +_userStudyFeatures :: Lens' UserTableData UserTableStudyFeatures +_userStudyFeatures = _dbrOutput . _8 colUserComment :: IsDBTable m c => TermId -> SchoolId -> CourseShorthand -> Colonnade Sortable UserTableData (DBCell m c) @@ -161,26 +144,6 @@ colUserExams tid ssh csh = sortable (Just "exams") (i18nCell MsgCourseUserExams) (\(Entity _ Exam{..}) -> CExamR tid ssh csh examName EUsersR) (examName . entityVal) -colUserSemester :: IsDBTable m c => Colonnade Sortable UserTableData (DBCell m c) -colUserSemester = sortable (Just "semesternr") (i18nCell MsgStudyFeatureAge) $ - foldMap numCell . preview _rowUserSemester - -colUserField :: IsDBTable m c => Colonnade Sortable UserTableData (DBCell m c) -colUserField = sortable (Just "field") (i18nCell MsgCourseStudyFeature) $ - foldMap i18nCell . view (_userTableFeatures . _3) - --- colUserFieldShort :: IsDBTable m c => Colonnade Sortable UserTableData (DBCell m c) --- colUserFieldShort = sortable (Just "field-short") (i18nCell MsgCourseStudyFeature) $ --- foldMap (i18nCell . ShortStudyTerms) . view (_userTableFeatures . _3) - --- colUserDegree :: IsDBTable m c => Colonnade Sortable UserTableData (DBCell m c) --- colUserDegree = sortable (Just "degree") (i18nCell MsgStudyFeatureDegree) $ --- foldMap i18nCell . preview (_userTableFeatures . _2 . _Just) - -colUserDegreeShort :: IsDBTable m c => Colonnade Sortable UserTableData (DBCell m c) -colUserDegreeShort = sortable (Just "degree-short") (i18nCell MsgStudyFeatureDegree) $ - foldMap (i18nCell . ShortStudyDegree) . preview (_userTableFeatures . _2 . _Just) - colUserSex' :: IsDBTable m c => Colonnade Sortable UserTableData (DBCell m c) colUserSex' = colUserSex $ hasUser . _userSex @@ -203,20 +166,12 @@ colUserSheets shns = cap (Sortable Nothing caption) $ foldMap userSheetCol shns _other -> mempty -data UserTableCsvStudyFeature = UserTableCsvStudyFeature - { csvUserField :: Text - , csvUserDegree :: Text - , csvUserSemester :: Int - , csvUserType :: StudyFieldType - } deriving (Eq, Ord, Read, Show, Generic, Typeable) -makeLenses_ ''UserTableCsvStudyFeature - data UserTableCsv = UserTableCsv { csvUserName :: Text , csvUserSex :: Maybe Sex , csvUserMatriculation :: Maybe Text , csvUserEmail :: CI Email - , csvUserStudyFeatures :: Either (Maybe UserTableCsvStudyFeature) (Set UserTableCsvStudyFeature) + , csvUserStudyFeatures :: UserTableStudyFeatures , csvUserSubmissionGroup :: Maybe SubmissionGroupName , csvUserRegistration :: UTCTime , csvUserNote :: Maybe Html @@ -232,20 +187,8 @@ instance Csv.ToNamedRecord UserTableCsv where , "sex" Csv..= csvUserSex , "matriculation" Csv..= csvUserMatriculation , "email" Csv..= csvUserEmail - ] ++ case csvUserStudyFeatures of - Left feats - -> [ "field" Csv..= (csvUserField <$> feats) - , "degree" Csv..= (csvUserDegree <$> feats) - , "semester" Csv..= (csvUserSemester <$> feats) - ] - Right feats - -> let featsStr = Text.intercalate "; " . flip map (Set.toList feats) $ \UserTableCsvStudyFeature{..} - -> let csvUserType' = renderMessage (error "no foundation needed" :: UniWorX) [] $ ShortStudyFieldType csvUserType - in [st|#{csvUserField} #{csvUserDegree} (#{csvUserType'} #{tshow csvUserSemester})|] - in [ "study-features" Csv..= featsStr - ] - ++ - [ "submission-group" Csv..= csvUserSubmissionGroup + , "study-features" Csv..= csvUserStudyFeatures + , "submission-group" Csv..= csvUserSubmissionGroup ] ++ [ let tutsStr = Text.intercalate "; " . map CI.original $ csvUserTutorials ^. _1 in "tutorial" Csv..= tutsStr @@ -270,9 +213,6 @@ instance CsvColumnsExplained UserTableCsv where , single "matriculation" MsgCsvColumnUserMatriculation , single "email" MsgCsvColumnUserEmail , single "study-features" MsgCsvColumnUserStudyFeatures - , single "field" MsgCsvColumnUserField - , single "degree" MsgCsvColumnUserDegree - , single "semester" MsgCsvColumnUserSemester , single "submission-group" MsgCsvColumnUserSubmissionGroup , single "tutorial" MsgCsvColumnUserTutorial , single "exams" MsgCsvColumnUserExam @@ -283,19 +223,17 @@ instance CsvColumnsExplained UserTableCsv where single :: RenderMessage UniWorX msg => Csv.Name -> msg -> Map Csv.Name Widget single k v = singletonMap k [whamlet|_{v}|] -data UserCsvExportData = UserCsvExportData - { csvUserSimplifiedFeaturesOfStudy :: Bool - , csvUserIncludeSheets :: Bool +newtype UserCsvExportData = UserCsvExportData + { csvUserIncludeSheets :: Bool } deriving (Eq, Ord, Read, Show, Generic, Typeable) instance Default UserCsvExportData where - def = UserCsvExportData True False + def = UserCsvExportData False userTableCsvHeader :: Bool -> [Entity Tutorial] -> [Entity Sheet] -> UserCsvExportData -> Csv.Header userTableCsvHeader showSex tuts sheets UserCsvExportData{..} = Csv.header $ [ "name" ] ++ [ "sex" | showSex ] ++ - [ "matriculation", "email" - ] ++ bool (pure "study-features") ["field", "degree", "semester"] csvUserSimplifiedFeaturesOfStudy ++ + [ "matriculation", "email", "study-features"] ++ [ "tutorial" | hasEmptyRegGroup ] ++ map (encodeUtf8 . CI.foldedCase) regGroups ++ [ "exams", "registration" ] ++ @@ -376,7 +314,7 @@ makeCourseUserTable cid acts restrict colChoices psValidator csvColumns = do dbtStyle = def { dbsFilterLayout = defaultDBSFilterLayout } dbtSQLQuery q = userTableQuery cid q <* E.where_ (restrict q) dbtRowKey = queryUser >>> (E.^. UserId) - dbtProj = traverse $ \(user, participant, E.Value userNoteId, (feature,degree,terms), subGroup) -> do + dbtProj = traverse $ \(user, participant, E.Value userNoteId, subGroup) -> do tuts'' <- selectList [ TutorialParticipantUser ==. entityKey user, TutorialParticipantTutorial <-. map entityKey tutorials ] [] exams' <- selectList [ ExamRegistrationUser ==. entityKey user ] [] subs' <- E.select . E.from $ \(sheet `E.LeftOuterJoin` (submission `E.InnerJoin` submissionUser)) -> do @@ -389,13 +327,14 @@ makeCourseUserTable cid acts restrict colChoices psValidator csvColumns = do , submission ) ) + feats <- courseUserStudyFeatures (participant ^. _entityVal . _courseParticipantCourse) (participant ^. _entityVal . _courseParticipantUser) let regGroups = setOf (folded . _entityVal . _tutorialRegGroup . _Just) tutorials tuts' = filter (\(Entity tutId _) -> any ((== tutId) . tutorialParticipantTutorial . entityVal) tuts'') tutorials tuts = foldr (\tut@(Entity _ Tutorial{..}) -> maybe (over _1 $ cons tut) (over _2 . flip (Map.insertWith (<|>)) (Just tut)) tutorialRegGroup) ([], Map.fromSet (const Nothing) regGroups) tuts' exs = filter (\(Entity eId _) -> any ((== eId) . examRegistrationExam . entityVal) exams') exams subs = Map.fromList $ map (over (_2 . _2) (views _entityVal submissionRatingPoints <=< assertM (views _entityVal submissionRatingDone)) . over _1 E.unValue . over (_2 . _1) E.unValue) subs' - return (user, participant, userNoteId, (entityVal <$> feature, entityVal <$> degree, entityVal <$> terms), tuts, exs, subGroup, subs) + return (user, participant, userNoteId, tuts, exs, subGroup, subs, feats) dbtColonnade = colChoices dbtSorting = mconcat [ single $ sortUserNameLink queryUser -- slower sorting through clicking name column header @@ -404,11 +343,6 @@ makeCourseUserTable cid acts restrict colChoices psValidator csvColumns = do , single $ sortUserEmail queryUser , single $ sortUserMatriclenr queryUser , sortUserSex (to queryUser . to (E.^. UserSex)) - , single ("degree" , SortColumn $ queryFeaturesDegree >>> (E.?. StudyDegreeName)) - , single ("degree-short", SortColumn $ queryFeaturesDegree >>> (E.?. StudyDegreeShorthand)) - , single ("field" , SortColumn $ queryFeaturesField >>> (E.?. StudyTermsName)) - , single ("field-short" , SortColumn $ queryFeaturesField >>> (E.?. StudyTermsShorthand)) - , single ("semesternr" , SortColumn $ queryFeaturesStudy >>> (E.?. StudyFeaturesSemester)) , single ("registration", SortColumn $ queryParticipant >>> (E.^. CourseParticipantRegistration)) , single ("note" , SortColumn $ queryUserNote >>> \note -> -- sort by last edit date E.subSelectMaybe . E.from $ \edit -> do @@ -450,20 +384,6 @@ makeCourseUserTable cid acts restrict colChoices psValidator csvColumns = do , single $ fltrUserMatriclenr queryUser , single $ fltrUserNameEmail queryUser , fltrUserSex (to queryUser . to (E.^. UserSex)) - , single ("field-name" , FilterColumn $ E.mkContainsFilter $ queryFeaturesField >>> (E.?. StudyTermsName)) - , single ("field-short" , FilterColumn $ E.mkContainsFilter $ queryFeaturesField >>> (E.?. StudyTermsShorthand)) - , single ("field-key" , FilterColumn $ E.mkExactFilter $ queryFeaturesField >>> (E.?. StudyTermsKey)) - , single ("field" , FilterColumn $ E.anyFilter - [ E.mkContainsFilterWith Just $ queryFeaturesField >>> E.joinV . (E.?. StudyTermsName) - , E.mkContainsFilterWith Just $ queryFeaturesField >>> E.joinV . (E.?. StudyTermsShorthand) - , E.mkExactFilterWith readMay $ queryFeaturesField >>> (E.?. StudyTermsKey) - ] ) - , single ("degree" , FilterColumn $ E.anyFilter - [ E.mkContainsFilterWith Just $ queryFeaturesDegree >>> E.joinV . (E.?. StudyDegreeName) - , E.mkContainsFilterWith Just $ queryFeaturesDegree >>> E.joinV . (E.?. StudyDegreeShorthand) - , E.mkExactFilterWith readMay $ queryFeaturesDegree >>> (E.?. StudyDegreeKey) - ] ) - , single ("semesternr" , FilterColumn $ E.mkExactFilter $ queryFeaturesStudy >>> (E.?. StudyFeaturesSemester)) , single ("tutorial" , FilterColumn $ E.mkExistsFilter $ \row criterion -> E.from $ \(tutorial `E.InnerJoin` tutorialParticipant) -> do E.on $ tutorial E.^. TutorialId E.==. tutorialParticipant E.^. TutorialParticipantTutorial @@ -489,6 +409,18 @@ makeCourseUserTable cid acts restrict colChoices psValidator csvColumns = do E.where_ $ sheet E.^. SheetCourse E.==. E.val cid E.&&. sheet E.^. SheetName E.==. E.val shn ) + , fltrRelevantStudyFeaturesTerms (to $ + \t -> ( E.subSelectForeign (queryParticipant t) CourseParticipantCourse (E.^. CourseTerm) + , queryUser t E.^. UserId + )) + , fltrRelevantStudyFeaturesDegree (to $ + \t -> ( E.subSelectForeign (queryParticipant t) CourseParticipantCourse (E.^. CourseTerm) + , queryUser t E.^. UserId + )) + , fltrRelevantStudyFeaturesSemester (to $ + \t -> ( E.subSelectForeign (queryParticipant t) CourseParticipantCourse (E.^. CourseTerm) + , queryUser t E.^. UserId + )) ] where single = uncurry Map.singleton dbtFilterUI mPrev = mconcat $ @@ -497,11 +429,12 @@ makeCourseUserTable cid acts restrict colChoices psValidator csvColumns = do , fltrUserMatriclenrUI mPrev ] ++ [ fltrUserSexUI mPrev | showSex ] ++ - [ prismAForm (singletonFilter "degree") mPrev $ aopt textField (fslI MsgStudyFeatureDegree) - , prismAForm (singletonFilter "field") mPrev $ aopt textField (fslI MsgCourseStudyFeature) - , prismAForm (singletonFilter "submission-group") mPrev $ aopt textField (fslI MsgSubmissionGroup) + [ prismAForm (singletonFilter "submission-group") mPrev $ aopt textField (fslI MsgSubmissionGroup) , prismAForm (singletonFilter "tutorial") mPrev $ aopt textField (fslI MsgCourseUserTutorial) , prismAForm (singletonFilter "exam") mPrev $ aopt textField (fslI MsgCourseUserExam) + , fltrRelevantStudyFeaturesDegreeUI mPrev + , fltrRelevantStudyFeaturesTermsUI mPrev + , fltrRelevantStudyFeaturesSemesterUI mPrev ] ++ [ prismAForm (singletonFilter "has-personalised-sheet-files". maybePrism _PathPiece) mPrev $ aopt (selectField' (Just $ SomeMessage MsgNoFilter) . optionsF $ map E.unValue personalisedSheets) (fslI MsgCourseUserHasPersonalisedSheetFilesFilter) | not $ null personalisedSheets @@ -523,44 +456,14 @@ makeCourseUserTable cid acts restrict colChoices psValidator csvColumns = do csvColumns' <- csvColumns return $ DBTCsvEncode { dbtCsvExportForm = UserCsvExportData - <$> apopt checkBoxField (fslI MsgUserSimplifiedFeaturesOfStudyCsv & setTooltip MsgUserSimplifiedFeaturesOfStudyCsvTip) (Just $ csvUserSimplifiedFeaturesOfStudy def) - <*> apopt checkBoxField (fslI MsgCourseUserCsvIncludeSheets & setTooltip MsgCourseUserCsvIncludeSheetsTip) (Just $ csvUserIncludeSheets def) - , dbtCsvDoEncode = \UserCsvExportData{..} -> C.mapM $ \(E.Value uid, row) -> flip runReaderT row $ + <$> apopt checkBoxField (fslI MsgCourseUserCsvIncludeSheets & setTooltip MsgCourseUserCsvIncludeSheetsTip) (Just $ csvUserIncludeSheets def) + , dbtCsvDoEncode = \UserCsvExportData{} -> C.mapM $ \(_, row) -> flip runReaderT row $ UserTableCsv <$> view (hasUser . _userDisplayName) <*> view (hasUser . _userSex) <*> view (hasUser . _userMatrikelnummer) <*> view (hasUser . _userEmail) - <*> if - | csvUserSimplifiedFeaturesOfStudy -> fmap Left . runMaybeT $ - UserTableCsvStudyFeature - <$> MaybeT (preview $ _userTableFeatures . _3 . _Just . _studyTermsName . _Just - <> _userTableFeatures . _3 . _Just . _studyTermsKey . to tshow - ) - <*> MaybeT (preview $ _userTableFeatures . _2 . _Just . _studyDegreeName . _Just - <> _userTableFeatures . _2 . _Just . _studyDegreeKey . to tshow - ) - <*> MaybeT (preview $ _userTableFeatures . _1 . _Just . _studyFeaturesSemester) - <*> MaybeT (preview $ _userTableFeatures . _1 . _Just . _studyFeaturesType) - | otherwise -> Right <$> do - feats <- lift . E.select . E.from $ \(feat `E.InnerJoin` terms `E.InnerJoin` degree) -> do - E.on $ degree E.^. StudyDegreeId E.==. feat E.^. StudyFeaturesDegree - E.on $ terms E.^. StudyTermsId E.==. feat E.^. StudyFeaturesField - let registered = E.exists . E.from $ \participant -> - E.where_ $ participant E.^. CourseParticipantCourse E.==. E.val cid - E.&&. participant E.^. CourseParticipantUser E.==. E.val uid - E.&&. participant E.^. CourseParticipantField E.==. E.just (feat E.^. StudyFeaturesId) - E.where_ $ registered - E.||. feat E.^. StudyFeaturesValid - E.where_ $ feat E.^. StudyFeaturesUser E.==. E.val uid - return (terms, degree, feat) - return . Set.fromList . flip map feats $ \(Entity _ StudyTerms{..}, Entity _ StudyDegree{..}, Entity _ StudyFeatures{..}) -> - UserTableCsvStudyFeature - { csvUserField = fromMaybe (tshow studyTermsKey) studyTermsName - , csvUserDegree = fromMaybe (tshow studyDegreeKey) studyDegreeName - , csvUserSemester = studyFeaturesSemester - , csvUserType = studyFeaturesType - } + <*> view _userStudyFeatures <*> preview (_userSubmissionGroup . _entityVal . _submissionGroupName) <*> view _userTableRegistration <*> userNote @@ -636,9 +539,7 @@ postCUsersR tid ssh csh = do , guardOn showSex . cap' $ colUserSex' , pure . cap' $ colUserEmail , pure . cap' $ colUserMatriclenr - , pure . cap' $ colUserDegreeShort - , pure . cap' $ colUserField - , pure . cap' $ colUserSemester + , pure . cap' $ colStudyFeatures _userStudyFeatures , guardOn hasSubmissionGroups $ cap' colUserSubmissionGroup , guardOn hasTutorials . cap' $ colUserTutorials tid ssh csh , guardOn hasExams . cap' $ colUserExams tid ssh csh diff --git a/src/Handler/Exam/AddUser.hs b/src/Handler/Exam/AddUser.hs index 912e52054..a833073f6 100644 --- a/src/Handler/Exam/AddUser.hs +++ b/src/Handler/Exam/AddUser.hs @@ -22,7 +22,6 @@ import Generics.Deriving.Monoid data AddRecipientsResult = AddRecipientsResult { aurAlreadyRegistered - , aurNoUniquePrimaryField , aurNoCourseRegistration , aurSuccess , aurSuccessCourse :: [UserEmail] @@ -101,11 +100,6 @@ postEAddUserR tid ssh csh examn = do unless (null aurSuccess) $ tell . pure <=< messageI Success . MsgExamRegistrationParticipantsRegistered $ length aurSuccess - unless (null aurNoUniquePrimaryField) $ do - let modalTrigger = [whamlet|_{MsgExamRegistrationRegisteredWithoutField (length aurNoUniquePrimaryField)}|] - modalContent = $(widgetFile "messages/examRegistrationInvitationRegisteredWithoutField") - tell . pure <=< messageWidget Warning $ msgModal modalTrigger (Right modalContent) - unless (null aurNoCourseRegistration) $ do let modalTrigger = [whamlet|_{MsgExamRegistrationNotRegisteredWithoutCourse (length aurNoCourseRegistration)}|] modalContent = $(widgetFile "messages/examRegistrationInvitationNotRegisteredWithoutCourse") @@ -137,11 +131,6 @@ postEAddUserR tid ssh csh examn = do guardAuthResult =<< lift (lift $ evalAccessDB (CourseR tid ssh csh CAddUserR) True) - features <- lift . lift $ selectKeysList [ StudyFeaturesUser ==. uid, StudyFeaturesValid ==. True, StudyFeaturesType ==. FieldPrimary ] [] - - let courseParticipantField - | [f] <- features = Just f - | otherwise = Nothing lift . lift . void $ upsert CourseParticipant @@ -154,15 +143,12 @@ postEAddUserR tid ssh csh examn = do } [ CourseParticipantRegistration =. now , CourseParticipantAllocated =. Nothing - , CourseParticipantField =. courseParticipantField , CourseParticipantState =. CourseParticipantActive ] lift . lift . audit $ TransactionCourseParticipantEdit cid uid lift . lift . queueDBJob . JobQueueNotification $ NotificationCourseRegistered uid cid lift $ lift examRegister - return $ case courseParticipantField of - Nothing -> mempty { aurNoUniquePrimaryField = pure userEmail } - Just _ -> mempty { aurSuccessCourse = pure userEmail } + return $ mempty { aurSuccessCourse = pure userEmail } diff --git a/src/Handler/Exam/RegistrationInvite.hs b/src/Handler/Exam/RegistrationInvite.hs index 05703e42a..3b721ec26 100644 --- a/src/Handler/Exam/RegistrationInvite.hs +++ b/src/Handler/Exam/RegistrationInvite.hs @@ -95,16 +95,13 @@ examRegistrationInvitationConfig = InvitationConfig{..} case (isRegistered, invDBExamRegistrationCourseRegister) of (False, False) -> permissionDeniedI MsgUnauthorizedParticipant - (False, True ) -> do - fieldRes <- wreq (studyFeaturesFieldFor Nothing False [] $ Just uid) (fslI MsgCourseStudyFeature) Nothing - return $ (JunctionExamRegistration invDBExamRegistrationOccurrence now, ) . Just <$> fieldRes - (True , _ ) -> return $ pure (JunctionExamRegistration invDBExamRegistrationOccurrence now, Nothing) - invitationInsertHook _ (Entity eid Exam{..}) _ ExamRegistration{..} mField act = do - whenIsJust mField $ \cpField -> do + (False, True ) -> return $ pure (JunctionExamRegistration invDBExamRegistrationOccurrence now, True) + (True , _ ) -> return $ pure (JunctionExamRegistration invDBExamRegistrationOccurrence now, False) + invitationInsertHook _ (Entity eid Exam{..}) _ ExamRegistration{..} doReg act = do + when doReg $ do void $ upsert - (CourseParticipant examCourse examRegistrationUser examRegistrationTime cpField Nothing CourseParticipantActive) + (CourseParticipant examCourse examRegistrationUser examRegistrationTime Nothing CourseParticipantActive) [ CourseParticipantRegistration =. examRegistrationTime - , CourseParticipantField =. cpField , CourseParticipantAllocated =. Nothing , CourseParticipantState =. CourseParticipantActive ] diff --git a/src/Handler/Exam/Show.hs b/src/Handler/Exam/Show.hs index e8b306d85..e206bc17b 100644 --- a/src/Handler/Exam/Show.hs +++ b/src/Handler/Exam/Show.hs @@ -124,6 +124,7 @@ getEShowR tid ssh csh examn = do , mayRegister' (entityKey <$> mOcc) = Just $ do (examRegisterForm, examRegisterEnctype) <- liftHandler . generateFormPost . buttonForm' $ bool [BtnExamRegister] [BtnExamDeregister] isRegistered [whamlet| + $newline never
$if isRegistered _{MsgExamRegistered} @@ -147,11 +148,12 @@ getEShowR tid ssh csh examn = do } | is _Nothing mOcc , is _Nothing registered - = Just [whamlet|_{MsgExamLoginToRegister}|] + = Just $ i18n MsgExamLoginToRegister | is _Nothing mOcc , isRegistered <- is _Just $ join registered = Just [whamlet| + $newline never
$if isRegistered _{MsgExamRegistered} diff --git a/src/Handler/Exam/Users.hs b/src/Handler/Exam/Users.hs index 9fcf76c04..30b351113 100644 --- a/src/Handler/Exam/Users.hs +++ b/src/Handler/Exam/Users.hs @@ -10,6 +10,7 @@ import Handler.Utils import Handler.Utils.Exam import Handler.Utils.Users import Handler.Utils.Csv +import Handler.Utils.StudyFeatures import Handler.Exam.AutoOccurrence (examAutoOccurrenceCalculateWidget) @@ -36,7 +37,7 @@ import qualified Data.CaseInsensitive as CI import Numeric.Lens (integral) -import Database.Persist.Sql (deleteWhereCount, updateWhereCount) +import Database.Persist.Sql (updateWhereCount) import Control.Lens.Indexed ((<.), (.>)) @@ -47,25 +48,18 @@ type ExamUserTableExpr = ( E.SqlExpr (Entity ExamRegistration) `E.InnerJoin` E.SqlExpr (Entity User) ) `E.LeftOuterJoin` E.SqlExpr (Maybe (Entity ExamOccurrence)) - `E.LeftOuterJoin` ( E.SqlExpr (Maybe (Entity CourseParticipant)) - `E.LeftOuterJoin` ( E.SqlExpr (Maybe (Entity StudyFeatures)) - `E.InnerJoin` E.SqlExpr (Maybe (Entity StudyDegree)) - `E.InnerJoin` E.SqlExpr (Maybe (Entity StudyTerms)) - ) - ) + `E.LeftOuterJoin` E.SqlExpr (Maybe (Entity CourseParticipant)) `E.LeftOuterJoin` E.SqlExpr (Maybe (Entity ExamBonus)) `E.LeftOuterJoin` E.SqlExpr (Maybe (Entity ExamResult)) `E.LeftOuterJoin` E.SqlExpr (Maybe (Entity CourseUserNote)) type ExamUserTableData = DBRow ( Entity ExamRegistration , Entity User , Maybe (Entity ExamOccurrence) - , Maybe (Entity StudyFeatures) - , Maybe (Entity StudyDegree) - , Maybe (Entity StudyTerms) , Maybe (Entity ExamBonus) , Maybe (Entity ExamResult) , Map ExamPartId (ExamPart, Maybe (Entity ExamPartResult)) , Maybe (Entity CourseUserNote) + , UserTableStudyFeatures ) instance HasEntity ExamUserTableData User where @@ -87,16 +81,7 @@ queryExamOccurrence :: ExamUserTableExpr -> E.SqlExpr (Maybe (Entity ExamOccurre queryExamOccurrence = $(sqlLOJproj 6 2) queryCourseParticipant :: ExamUserTableExpr -> E.SqlExpr (Maybe (Entity CourseParticipant)) -queryCourseParticipant = $(sqlLOJproj 2 1) . $(sqlLOJproj 6 3) - -queryStudyFeatures :: ExamUserTableExpr -> E.SqlExpr (Maybe (Entity StudyFeatures)) -queryStudyFeatures = $(sqlIJproj 3 1) . $(sqlLOJproj 2 2) . $(sqlLOJproj 6 3) - -queryStudyDegree :: ExamUserTableExpr -> E.SqlExpr (Maybe (Entity StudyDegree)) -queryStudyDegree = $(sqlIJproj 3 2) . $(sqlLOJproj 2 2) . $(sqlLOJproj 6 3) - -queryStudyField :: ExamUserTableExpr -> E.SqlExpr (Maybe (Entity StudyTerms)) -queryStudyField = $(sqlIJproj 3 3) . $(sqlLOJproj 2 2) . $(sqlLOJproj 6 3) +queryCourseParticipant = $(sqlLOJproj 6 3) queryExamBonus :: ExamUserTableExpr -> E.SqlExpr (Maybe (Entity ExamBonus)) queryExamBonus = $(sqlLOJproj 6 4) @@ -130,38 +115,32 @@ resultExamRegistration = _dbrOutput . _1 resultUser :: Lens' ExamUserTableData (Entity User) resultUser = _dbrOutput . _2 -resultStudyFeatures :: Traversal' ExamUserTableData (Entity StudyFeatures) -resultStudyFeatures = _dbrOutput . _4 . _Just - -resultStudyDegree :: Traversal' ExamUserTableData (Entity StudyDegree) -resultStudyDegree = _dbrOutput . _5 . _Just - -resultStudyField :: Traversal' ExamUserTableData (Entity StudyTerms) -resultStudyField = _dbrOutput . _6 . _Just - resultExamOccurrence :: Traversal' ExamUserTableData (Entity ExamOccurrence) resultExamOccurrence = _dbrOutput . _3 . _Just resultExamBonus :: Traversal' ExamUserTableData (Entity ExamBonus) -resultExamBonus = _dbrOutput . _7 . _Just +resultExamBonus = _dbrOutput . _4 . _Just resultExamResult :: Traversal' ExamUserTableData (Entity ExamResult) -resultExamResult = _dbrOutput . _8 . _Just +resultExamResult = _dbrOutput . _5 . _Just resultExamParts :: IndexedTraversal' ExamPartId ExamUserTableData (ExamPart, Maybe (Entity ExamPartResult)) -resultExamParts = _dbrOutput . _9 . itraversed +resultExamParts = _dbrOutput . _6 . itraversed -- resultExamParts' :: Traversal' ExamUserTableData (Entity ExamPart) -- resultExamParts' = (resultExamParts <. _1) . withIndex . from _Entity resultExamPartResult :: ExamPartId -> Lens' ExamUserTableData (Maybe (Entity ExamPartResult)) -resultExamPartResult epId = _dbrOutput . _9 . unsafeSingular (ix epId) . _2 +resultExamPartResult epId = _dbrOutput . _6 . unsafeSingular (ix epId) . _2 resultExamPartResults :: IndexedTraversal' ExamPartId ExamUserTableData (Maybe (Entity ExamPartResult)) resultExamPartResults = resultExamParts <. _2 resultCourseNote :: Traversal' ExamUserTableData (Entity CourseUserNote) -resultCourseNote = _dbrOutput . _10 . _Just +resultCourseNote = _dbrOutput . _7 . _Just + +resultStudyFeatures :: Lens' ExamUserTableData UserTableStudyFeatures +resultStudyFeatures = _dbrOutput . _8 resultAutomaticExamBonus :: Exam -> Map UserId SheetTypeSummary -> Fold ExamUserTableData Points @@ -191,9 +170,7 @@ data ExamUserTableCsv = ExamUserTableCsv , csvEUserFirstName :: Maybe Text , csvEUserName :: Maybe Text , csvEUserMatriculation :: Maybe Text - , csvEUserField :: Maybe Text - , csvEUserDegree :: Maybe Text - , csvEUserSemester :: Maybe Int + , csvEUserStudyFeatures :: UserTableStudyFeatures , csvEUserOccurrence :: Maybe (CI Text) , csvEUserExercisePoints :: Maybe (Maybe Points) , csvEUserExerciseNumPasses :: Maybe (Maybe Int) @@ -213,9 +190,7 @@ instance ToNamedRecord ExamUserTableCsv where , "first-name" Csv..= csvEUserFirstName , "name" Csv..= csvEUserName , "matriculation" Csv..= csvEUserMatriculation - , "field" Csv..= csvEUserField - , "degree" Csv..= csvEUserDegree - , "semester" Csv..= csvEUserSemester + , "study-features" Csv..= csvEUserStudyFeatures , "occurrence" Csv..= csvEUserOccurrence ] ++ catMaybes [ fmap ("exercise-points" Csv..=) csvEUserExercisePoints @@ -240,9 +215,7 @@ instance FromNamedRecord ExamUserTableCsv where <*> csv .:?? "first-name" <*> csv .:?? "name" <*> csv .:?? "matriculation" - <*> csv .:?? "field" - <*> csv .:?? "degree" - <*> csv .:?? "semester" + <*> pure mempty <*> csv .:?? "occurrence" <*> fmap Just (csv .:?? "exercise-points") <*> fmap Just (csv .:?? "exercise-num-passes") @@ -263,9 +236,7 @@ instance CsvColumnsExplained ExamUserTableCsv where , single "first-name" MsgCsvColumnExamUserFirstName , single "name" MsgCsvColumnExamUserName , single "matriculation" MsgCsvColumnExamUserMatriculation - , single "field" MsgCsvColumnExamUserField - , single "degree" MsgCsvColumnExamUserDegree - , single "semester" MsgCsvColumnExamUserSemester + , single "study-features" MsgCsvColumnUserStudyFeatures , single "occurrence" MsgCsvColumnExamUserOccurrence , single "exercise-points" MsgCsvColumnExamUserExercisePoints , single "exercise-num-passes" MsgCsvColumnExamUserExercisePasses @@ -287,7 +258,7 @@ examUserTableCsvHeader :: ( MonoFoldable mono examUserTableCsvHeader allBoni doBonus pNames = Csv.header $ [ "surname", "first-name", "name" , "matriculation" - , "field", "degree", "semester" + , "study-features" , "course-note" , "occurrence" ] ++ bool mempty ["exercise-points", "exercise-points-max"] (doBonus && showPoints) @@ -329,7 +300,6 @@ data ExamUserCsvActionClass = ExamUserCsvCourseRegister | ExamUserCsvRegister | ExamUserCsvAssignOccurrence - | ExamUserCsvSetCourseField | ExamUserCsvSetPartResult | ExamUserCsvSetBonus | ExamUserCsvOverrideBonus @@ -343,7 +313,6 @@ embedRenderMessage ''UniWorX ''ExamUserCsvActionClass id data ExamUserCsvAction = ExamUserCsvCourseRegisterData { examUserCsvActUser :: UserId - , examUserCsvActCourseField :: Maybe StudyFeaturesId , examUserCsvActOccurrence :: Maybe ExamOccurrenceId } | ExamUserCsvRegisterData @@ -354,10 +323,6 @@ data ExamUserCsvAction { examUserCsvActRegistration :: ExamRegistrationId , examUserCsvActOccurrence :: Maybe ExamOccurrenceId } - | ExamUserCsvSetCourseFieldData - { examUserCsvActCourseParticipant :: CourseParticipantId - , examUserCsvActCourseField :: Maybe StudyFeaturesId - } | ExamUserCsvDeregisterData { examUserCsvActRegistration :: ExamRegistrationId } @@ -404,6 +369,7 @@ getEUsersR = postEUsersR postEUsersR tid ssh csh examn = do (((Any computedValues, registrationResult), examUsersTable), Entity eId examVal@Exam{..}, bonus) <- runDB $ do exam@(Entity eid examVal@Exam{..}) <- fetchExam tid ssh csh examn + Course{..} <- getJust examCourse occurrences <- selectList [ExamOccurrenceExam ==. eid] [Asc ExamOccurrenceName] examParts <- selectList [ExamPartExam ==. eid] [Asc ExamPartName] bonus <- examBonus exam @@ -453,9 +419,6 @@ postEUsersR tid ssh csh examn = do user <- asks queryUser occurrence <- asks queryExamOccurrence courseParticipant <- asks queryCourseParticipant - studyFeatures <- asks queryStudyFeatures - studyDegree <- asks queryStudyDegree - studyField <- asks queryStudyField examBonus' <- asks queryExamBonus examResult <- asks queryExamResult courseUserNote <- asks queryCourseNote @@ -467,9 +430,6 @@ postEUsersR tid ssh csh examn = do E.&&. examResult E.?. ExamResultExam E.==. E.just (E.val eid) E.on $ examBonus' E.?. ExamBonusUser E.==. E.just (user E.^. UserId) E.&&. examBonus' E.?. ExamBonusExam E.==. E.just (E.val eid) - E.on $ studyField E.?. StudyTermsId E.==. studyFeatures E.?. StudyFeaturesField - E.on $ studyDegree E.?. StudyDegreeId E.==. studyFeatures E.?. StudyFeaturesDegree - E.on $ studyFeatures E.?. StudyFeaturesId E.==. E.joinV (courseParticipant E.?. CourseParticipantField) E.on $ courseParticipant E.?. CourseParticipantCourse E.==. E.just (E.val examCourse) E.&&. courseParticipant E.?. CourseParticipantUser E.==. E.just (user E.^. UserId) E.&&. courseParticipant E.?. CourseParticipantState E.==. E.just (E.val CourseParticipantActive) @@ -479,13 +439,14 @@ postEUsersR tid ssh csh examn = do E.where_ $ examRegistration E.^. ExamRegistrationExam E.==. E.val eid - return (examRegistration, user, occurrence, studyFeatures, studyDegree, studyField, examBonus', examResult, courseUserNote) + return (examRegistration, user, occurrence, examBonus', examResult, courseUserNote) dbtRowKey = queryExamRegistration >>> (E.^. ExamRegistrationId) dbtProj = runReaderT $ (asks . set _dbrOutput) <=< magnify _dbrOutput $ - (,,,,,,,,,) - <$> view _1 <*> view _2 <*> view _3 <*> view _4 <*> view _5 <*> view _6 <*> view _7 <*> view _8 + (,,,,,,,) + <$> view _1 <*> view _2 <*> view _3 <*> view _4 <*> view _5 <*> getExamParts - <*> view _9 + <*> view _6 + <*> (lift . courseUserStudyFeatures examCourse =<< view (_2 . _entityKey)) where getExamParts :: ReaderT _ DB (Map ExamPartId (ExamPart, Maybe (Entity ExamPartResult))) getExamParts = do @@ -504,9 +465,7 @@ postEUsersR tid ssh csh examn = do [ pure $ dbSelect (_2 . applying _2) _1 $ return . view (resultExamRegistration . _entityKey) , pure $ colUserNameLink (CourseR tid ssh csh . CUserR) , pure colUserMatriclenr - , pure $ colField resultStudyField - , pure $ colDegreeShort resultStudyDegree - , pure $ colFeaturesSemester resultStudyFeatures + , pure $ colStudyFeatures resultStudyFeatures , pure $ sortable (Just "occurrence") (i18nCell MsgExamOccurrence) $ maybe mempty (anchorCell' (\n -> CExamR tid ssh csh examn EShowR :#: [st|exam-occurrence__#{n}|]) id . examOccurrenceName . entityVal) . view _userTableOccurrence , guardOn showPasses $ sortable Nothing (i18nCell MsgAchievedPasses) $ \(view $ resultUser . _entityKey -> uid) -> let SheetGradeSummary{achievedPasses} = examBonusAchieved uid bonus @@ -528,9 +487,6 @@ postEUsersR tid ssh csh examn = do dbtSorting = mconcat [ uncurry singletonMap $ sortUserNameLink queryUser , uncurry singletonMap $ sortUserMatriclenr queryUser - , uncurry singletonMap $ sortField queryStudyField - , uncurry singletonMap $ sortDegreeShort queryStudyDegree - , uncurry singletonMap $ sortFeaturesSemester queryStudyFeatures , mconcat [ singletonMap (fromText [st|part-#{toPathPiece examPartNumber}|]) . SortColumn . queryExamPart epId $ \_ examPartResult -> return $ examPartResult E.?. ExamPartResultResult | Entity epId ExamPart{..} <- examParts @@ -546,20 +502,29 @@ postEUsersR tid ssh csh examn = do dbtFilter = mconcat [ uncurry singletonMap $ fltrUserNameEmail queryUser , uncurry singletonMap $ fltrUserMatriclenr queryUser - , uncurry singletonMap $ fltrField queryStudyField - , uncurry singletonMap $ fltrDegree queryStudyDegree - , uncurry singletonMap $ fltrFeaturesSemester queryStudyFeatures , uncurry singletonMap ("occurrence", FilterColumn . E.mkContainsFilterWith Just $ queryExamOccurrence >>> (E.?. ExamOccurrenceName)) , fltrExamResultPoints (to $ queryExamResult >>> (E.?. ExamResultResult)) + , fltrRelevantStudyFeaturesTerms (to $ + \t -> ( E.val courseTerm + , queryUser t E.^. UserId + )) + , fltrRelevantStudyFeaturesDegree (to $ + \t -> ( E.val courseTerm + , queryUser t E.^. UserId + )) + , fltrRelevantStudyFeaturesSemester (to $ + \t -> ( E.val courseTerm + , queryUser t E.^. UserId + )) ] dbtFilterUI mPrev = mconcat $ catMaybes [ Just $ fltrUserNameEmailUI mPrev , Just $ fltrUserMatriclenrUI mPrev - , Just $ fltrFieldUI mPrev - , Just $ fltrDegreeUI mPrev - , Just $ fltrFeaturesSemesterUI mPrev , Just $ prismAForm (singletonFilter "occurrence") mPrev $ aopt (selectField' (Just $ SomeMessage MsgNoFilter) $ optionsF [CI.original examOccurrenceName | Entity _ ExamOccurrence{..} <- occurrences]) (fslI MsgExamOccurrence) , Just $ fltrExamResultPointsUI mPrev + , Just $ fltrRelevantStudyFeaturesTermsUI mPrev + , Just $ fltrRelevantStudyFeaturesDegreeUI mPrev + , Just $ fltrRelevantStudyFeaturesSemesterUI mPrev ] dbtStyle = def { dbsFilterLayout = defaultDBSFilterLayout } dbtParams = DBParamsForm @@ -627,9 +592,7 @@ postEUsersR tid ssh csh examn = do <*> view (resultUser . _entityVal . _userFirstName . to Just) <*> view (resultUser . _entityVal . _userDisplayName . to Just) <*> view (resultUser . _entityVal . _userMatrikelnummer) - <*> preview (resultStudyField . _entityVal . to (\StudyTerms{..} -> studyTermsName <|> studyTermsShorthand <|> Just (tshow studyTermsKey)) . _Just) - <*> preview (resultStudyDegree . _entityVal . to (\StudyDegree{..} -> studyDegreeName <|> studyDegreeShorthand <|> Just (tshow studyDegreeKey)) . _Just) - <*> preview (resultStudyFeatures . _entityVal . _studyFeaturesSemester) + <*> view resultStudyFeatures <*> preview (resultExamOccurrence . _entityVal . _examOccurrenceName) <*> fmap (bool (const Nothing) Just showPoints) (preview $ resultUser . _entityKey . to (examBonusAchieved ?? bonus) . _achievedPoints . _Wrapped) <*> fmap (bool (const Nothing) Just showPasses) (preview $ resultUser . _entityKey . to (examBonusAchieved ?? bonus) . _achievedPasses . _Wrapped . integral) @@ -650,15 +613,8 @@ postEUsersR tid ssh csh examn = do -> error "An UniqueExamRegistration could be found, but the ExamRegistrationKey is not among the existing keys" DBCsvDiffNew{dbCsvNewKey = Nothing, dbCsvNew} -> do (isPart, uid) <- lift $ guessUser' dbCsvNew - if - | isPart -> do - yieldM $ ExamUserCsvRegisterData uid <$> lookupOccurrence dbCsvNew - newFeatures <- lift $ lookupStudyFeatures dbCsvNew - Entity cpId CourseParticipant{ courseParticipantField = oldFeatures } <- lift . getJustBy $ UniqueParticipant uid examCourse - when (newFeatures /= oldFeatures) $ - yield $ ExamUserCsvSetCourseFieldData cpId newFeatures - | otherwise -> - yieldM $ ExamUserCsvCourseRegisterData uid <$> lookupStudyFeatures dbCsvNew <*> lookupOccurrence dbCsvNew + unless isPart $ + yieldM $ ExamUserCsvCourseRegisterData uid <$> lookupOccurrence dbCsvNew iforMOf_ (ifolded <. _Just) (csvEUserExamPartResults dbCsvNew) $ \epNumber epRes -> when (epNumber `elem` examPartNumbers) $ @@ -679,11 +635,6 @@ postEUsersR tid ssh csh examn = do when (newOccurrence /= dbCsvOld ^? resultExamOccurrence . _entityKey) $ yield $ ExamUserCsvAssignOccurrenceData (E.unValue dbCsvOldKey) newOccurrence - newFeatures <- lift $ lookupStudyFeatures dbCsvNew - when (newFeatures /= dbCsvOld ^? resultStudyFeatures . _entityKey) $ do - Entity cpId _ <- lift . getJustBy . flip UniqueParticipant examCourse $ dbCsvOld ^. resultUser . _entityKey - yield $ ExamUserCsvSetCourseFieldData cpId newFeatures - let uid = dbCsvOld ^. resultUser . _entityKey forM_ examPartNumbers $ \epNumber -> @@ -742,7 +693,6 @@ postEUsersR tid ssh csh examn = do ExamUserCsvRegisterData{} -> ExamUserCsvRegister ExamUserCsvDeregisterData{} -> ExamUserCsvDeregister ExamUserCsvAssignOccurrenceData{} -> ExamUserCsvAssignOccurrence - ExamUserCsvSetCourseFieldData{} -> ExamUserCsvSetCourseField ExamUserCsvSetPartResultData{} -> ExamUserCsvSetPartResult ExamUserCsvSetBonusData{..} | examUserCsvIsBonusOverride -> ExamUserCsvOverrideBonus @@ -765,12 +715,10 @@ postEUsersR tid ssh csh examn = do { courseParticipantCourse = examCourse , courseParticipantUser = examUserCsvActUser , courseParticipantRegistration = now - , courseParticipantField = examUserCsvActCourseField , courseParticipantAllocated = Nothing , courseParticipantState = CourseParticipantActive } [ CourseParticipantRegistration =. now - , CourseParticipantField =. examUserCsvActCourseField , CourseParticipantAllocated =. Nothing , CourseParticipantState =. CourseParticipantActive ] @@ -794,10 +742,6 @@ postEUsersR tid ssh csh examn = do audit $ TransactionExamRegister eid examUserCsvActUser ExamUserCsvAssignOccurrenceData{..} -> update examUserCsvActRegistration [ ExamRegistrationOccurrence =. examUserCsvActOccurrence ] - ExamUserCsvSetCourseFieldData{..} -> do - update examUserCsvActCourseParticipant [ CourseParticipantField =. examUserCsvActCourseField ] - CourseParticipant{..} <- getJust examUserCsvActCourseParticipant - audit $ TransactionCourseParticipantEdit examCourse courseParticipantUser ExamUserCsvSetPartResultData{..} -> do epid <- getKeyJustBy $ UniqueExamPartNumber eid examUserCsvActExamPart case examUserCsvActExamPartResult of @@ -840,13 +784,8 @@ postEUsersR tid ssh csh examn = do ] audit $ TransactionExamResultEdit eid examUserCsvActUser ExamUserCsvDeregisterData{..} -> do - ExamRegistration{examRegistrationUser} <- getJust examUserCsvActRegistration - audit $ TransactionExamDeregister eid examRegistrationUser - delete examUserCsvActRegistration - result <- getBy $ UniqueExamResult eid examRegistrationUser - forM_ result $ \(Entity erId _) -> do - delete erId - audit $ TransactionExamResultDeleted eid examRegistrationUser + ExamRegistration{..} <- getJust examUserCsvActRegistration + deregisterExamUsers examRegistrationExam $ pure examRegistrationUser ExamUserCsvSetCourseNoteData{ examUserCsvActCourseNote = Nothing, .. } -> do noteId <- getKeyBy $ UniqueCourseUserNote examUserCsvActUser examCourse whenIsJust noteId $ \nid -> do @@ -864,10 +803,6 @@ postEUsersR tid ssh csh examn = do [whamlet| $newline never ^{nameWidget userDisplayName userSurname} - $maybe features <- examUserCsvActCourseField - , ^{studyFeaturesWidget features} - $nothing - , _{MsgCourseStudyFeatureNone} $maybe ExamOccurrence{examOccurrenceName} <- occ \ (#{examOccurrenceName}) $nothing @@ -893,16 +828,6 @@ postEUsersR tid ssh csh examn = do $nothing \ (_{MsgExamNoOccurrence}) |] - ExamUserCsvSetCourseFieldData{..} -> do - User{..} <- liftHandler . runDB $ getJust . courseParticipantUser =<< getJust examUserCsvActCourseParticipant - [whamlet| - $newline never - ^{nameWidget userDisplayName userSurname} - $maybe features <- examUserCsvActCourseField - , ^{studyFeaturesWidget features} - $nothing - , _{MsgCourseStudyFeatureNone} - |] ExamUserCsvSetPartResultData{..} -> do (User{..}, Entity _ ExamPart{..}) <- liftHandler . runDB $ (,) <$> getJust examUserCsvActUser @@ -990,56 +915,6 @@ postEUsersR tid ssh csh examn = do [occId] -> return occId _other -> throwM ExamUserCsvExceptionNoMatchingOccurrence - lookupStudyFeatures :: ExamUserTableCsv -> DB (Maybe StudyFeaturesId) - lookupStudyFeatures csv@ExamUserTableCsv{..} = do - uid <- view _2 <$> guessUser' csv - oldFeatures <- getBy $ UniqueParticipant uid examCourse - studyFeatures <- E.select . E.from $ \(studyFeatures `E.InnerJoin` studyDegree `E.InnerJoin` studyTerms) -> - E.distinctOnOrderBy [ E.asc (studyFeatures E.^. StudyFeaturesField) - , E.asc (studyFeatures E.^. StudyFeaturesDegree) - , E.asc (studyFeatures E.^. StudyFeaturesSemester)] $ do - E.on $ studyTerms E.^. StudyTermsId E.==. studyFeatures E.^. StudyFeaturesField - E.on $ studyDegree E.^. StudyDegreeId E.==. studyFeatures E.^. StudyFeaturesDegree - E.where_ . E.and $ catMaybes - [ do - field <- csvEUserField - return . E.or $ catMaybes - [ Just $ studyTerms E.^. StudyTermsName `E.ciEq` E.just (E.val field) - , Just $ studyTerms E.^. StudyTermsShorthand `E.ciEq` E.just (E.val field) - , (studyTerms E.^. StudyTermsKey E.==.) . E.val <$> readMay field - ] - , do - degree <- csvEUserDegree - return . E.or $ catMaybes - [ Just $ studyDegree E.^. StudyDegreeName `E.ciEq` E.just (E.val degree) - , Just $ studyDegree E.^. StudyDegreeShorthand `E.ciEq` E.just (E.val degree) - , (studyDegree E.^. StudyDegreeKey E.==.) . E.val <$> readMay degree - ] - , (studyFeatures E.^. StudyFeaturesSemester E.==.) . E.val <$> csvEUserSemester - ] - E.where_ $ studyFeatures E.^. StudyFeaturesUser E.==. E.val uid - let isActive = studyFeatures E.^. StudyFeaturesValid E.==. E.val True - isActiveOrPrevious = case oldFeatures of - Just (Entity _ CourseParticipant{courseParticipantField = Just sfid}) - -> isActive E.||. (E.val sfid E.==. studyFeatures E.^. StudyFeaturesId) - _ -> isActive - E.where_ isActiveOrPrevious -- either active studyFeature or the one previously associated with this course - E.orderBy [E.desc isActiveOrPrevious, E.asc (E.orderByOrd $ studyFeatures E.^. StudyFeaturesType)] - return $ studyFeatures E.^. StudyFeaturesId - case studyFeatures of - [E.Value fid] -> return $ Just fid - _other - | is _Nothing csvEUserField - , is _Nothing csvEUserDegree - , is _Nothing csvEUserSemester - -> return Nothing - _other - | Just (Entity _ CourseParticipant{..}) <- oldFeatures - , Just sfid <- courseParticipantField - , E.Value sfid `elem` studyFeatures - -> return $ Just sfid - _other -> throwM ExamUserCsvExceptionNoMatchingStudyFeatures - examUsersDBTableValidator = def & defaultSorting [SortAscBy "user-name"] & defaultPagesize PagesizeAll @@ -1051,10 +926,9 @@ postEUsersR tid ssh csh examn = do (, exam, bonus) . over (_1 . _2) postprocess <$> dbTable examUsersDBTableValidator examUsersDBTable formResult registrationResult $ \case - (ExamUserDeregisterData, Map.keysSet -> selectedRegistrations) -> do - nrDel <- runDB $ deleteWhereCount - [ ExamRegistrationId <-. Set.toList selectedRegistrations - ] + (ExamUserDeregisterData, Map.elems -> selectedRegistrations) -> do + nrDel <- runDB . setSerializable . deregisterExamUsersCount eId $ map (view $ resultUser . _entityKey) selectedRegistrations + addMessageI Success $ MsgExamUsersDeregistered nrDel redirect $ CExamR tid ssh csh examn EUsersR (ExamUserAssignOccurrenceData occId, Map.keysSet -> selectedRegistrations) -> do diff --git a/src/Handler/ExamOffice/Exam.hs b/src/Handler/ExamOffice/Exam.hs index e5be277ea..4364079c7 100644 --- a/src/Handler/ExamOffice/Exam.hs +++ b/src/Handler/ExamOffice/Exam.hs @@ -22,6 +22,8 @@ import qualified Data.Map as Map import qualified Data.Conduit.List as C import qualified Colonnade +import Handler.Utils.StudyFeatures + data ButtonCloseExam = BtnCloseExam deriving (Enum, Eq, Ord, Bounded, Read, Show, Generic, Typeable) @@ -68,21 +70,14 @@ type ExamUserTableExpr = ( E.SqlExpr (Entity ExamResult) ) `E.LeftOuterJoin` E.SqlExpr (Maybe (Entity ExamRegistration)) `E.LeftOuterJoin` E.SqlExpr (Maybe (Entity ExamOccurrence)) - `E.LeftOuterJoin` ( E.SqlExpr (Maybe (Entity CourseParticipant)) - `E.LeftOuterJoin` ( E.SqlExpr (Maybe (Entity StudyFeatures)) - `E.InnerJoin` E.SqlExpr (Maybe (Entity StudyDegree)) - `E.InnerJoin` E.SqlExpr (Maybe (Entity StudyTerms)) - ) - ) + `E.LeftOuterJoin` E.SqlExpr (Maybe (Entity CourseParticipant)) type ExamUserTableData = DBRow ( Entity ExamResult , Entity User , Maybe (Entity ExamOccurrence) - , Maybe (Entity StudyFeatures) - , Maybe (Entity StudyDegree) - , Maybe (Entity StudyTerms) , Maybe (Entity ExamRegistration) , Bool , [(UserDisplayName, UserSurname, UTCTime, Set SchoolShorthand)] + , UserTableStudyFeatures ) queryExamRegistration :: Getter ExamUserTableExpr (E.SqlExpr (Maybe (Entity ExamRegistration))) @@ -95,16 +90,7 @@ queryExamOccurrence :: Getter ExamUserTableExpr (E.SqlExpr (Maybe (Entity ExamOc queryExamOccurrence = to $(E.sqlLOJproj 4 3) queryCourseParticipant :: Getter ExamUserTableExpr (E.SqlExpr (Maybe (Entity CourseParticipant))) -queryCourseParticipant = to $ $(E.sqlLOJproj 2 1) . $(E.sqlLOJproj 4 4) - -queryStudyFeatures :: Getter ExamUserTableExpr (E.SqlExpr (Maybe (Entity StudyFeatures))) -queryStudyFeatures = to $ $(E.sqlIJproj 3 1) . $(E.sqlLOJproj 2 2) . $(E.sqlLOJproj 4 4) - -queryStudyDegree :: Getter ExamUserTableExpr (E.SqlExpr (Maybe (Entity StudyDegree))) -queryStudyDegree = to $ $(E.sqlIJproj 3 2) . $(E.sqlLOJproj 2 2) . $(E.sqlLOJproj 4 4) - -queryStudyField :: Getter ExamUserTableExpr (E.SqlExpr (Maybe (Entity StudyTerms))) -queryStudyField = to $ $(E.sqlIJproj 3 3) . $(E.sqlLOJproj 2 2) . $(E.sqlLOJproj 4 4) +queryCourseParticipant = to $(E.sqlLOJproj 4 4) queryExamResult :: Getter ExamUserTableExpr (E.SqlExpr (Entity ExamResult)) queryExamResult = to $ $(E.sqlIJproj 2 1) . $(E.sqlLOJproj 4 1) @@ -118,15 +104,6 @@ queryIsSynced authId = to $ Exam.resultIsSynced authId <$> view queryExamResult resultUser :: Lens' ExamUserTableData (Entity User) resultUser = _dbrOutput . _2 -resultStudyFeatures :: Traversal' ExamUserTableData (Entity StudyFeatures) -resultStudyFeatures = _dbrOutput . _4 . _Just - -resultStudyDegree :: Traversal' ExamUserTableData (Entity StudyDegree) -resultStudyDegree = _dbrOutput . _5 . _Just - -resultStudyField :: Traversal' ExamUserTableData (Entity StudyTerms) -resultStudyField = _dbrOutput . _6 . _Just - resultExamOccurrence :: Traversal' ExamUserTableData (Entity ExamOccurrence) resultExamOccurrence = _dbrOutput . _3 . _Just @@ -134,19 +111,20 @@ resultExamResult :: Lens' ExamUserTableData (Entity ExamResult) resultExamResult = _dbrOutput . _1 resultIsSynced :: Lens' ExamUserTableData Bool -resultIsSynced = _dbrOutput . _8 +resultIsSynced = _dbrOutput . _5 resultSynchronised :: Traversal' ExamUserTableData (UserDisplayName, UserSurname, UTCTime, Set SchoolShorthand) -resultSynchronised = _dbrOutput . _9 . traverse +resultSynchronised = _dbrOutput . _6 . traverse + +resultStudyFeatures :: Lens' ExamUserTableData UserTableStudyFeatures +resultStudyFeatures = _dbrOutput . _7 data ExamUserTableCsv = ExamUserTableCsv { csvEUserSurname :: Text , csvEUserFirstName :: Text , csvEUserName :: Text , csvEUserMatriculation :: Maybe Text - , csvEUserField :: Maybe Text - , csvEUserDegree :: Maybe Text - , csvEUserSemester :: Maybe Int + , csvEUserStudyFeatures :: UserTableStudyFeatures , csvEUserOccurrenceStart :: Maybe ZonedTime , csvEUserExamResult :: ExamResultPassedGrade } @@ -168,9 +146,7 @@ instance CsvColumnsExplained ExamUserTableCsv where , ('csvEUserFirstName , MsgCsvColumnExamUserFirstName ) , ('csvEUserName , MsgCsvColumnExamUserName ) , ('csvEUserMatriculation , MsgCsvColumnExamUserMatriculation ) - , ('csvEUserField , MsgCsvColumnExamUserField ) - , ('csvEUserDegree , MsgCsvColumnExamUserDegree ) - , ('csvEUserSemester , MsgCsvColumnExamUserSemester ) + , ('csvEUserStudyFeatures , MsgCsvColumnUserStudyFeatures ) , ('csvEUserOccurrenceStart , MsgCsvColumnExamOfficeExamUserOccurrenceStart ) , ('csvEUserExamResult , MsgCsvColumnExamUserResult ) ] @@ -198,6 +174,7 @@ postEGradesR tid ssh csh examn = do now <- liftIO getCurrentTime ((usersResult, examUsersTable), Entity eId _) <- runDB $ do exam@(Entity eid Exam{..}) <- fetchExam tid ssh csh examn + Course{..} <- getJust examCourse csvName <- getMessageRender <*> pure (MsgExamUserCsvName tid ssh csh examn) isLecturer <- hasReadAccessTo $ CExamR tid ssh csh examn EUsersR @@ -249,16 +226,10 @@ postEGradesR tid ssh csh examn = do examRegistration <- view queryExamRegistration occurrence <- view queryExamOccurrence courseParticipant <- view queryCourseParticipant - studyFeatures <- view queryStudyFeatures - studyDegree <- view queryStudyDegree - studyField <- view queryStudyField isSynced <- view . queryIsSynced $ E.val uid lift $ do - E.on $ studyField E.?. StudyTermsId E.==. studyFeatures E.?. StudyFeaturesField - E.on $ studyDegree E.?. StudyDegreeId E.==. studyFeatures E.?. StudyFeaturesDegree - E.on $ studyFeatures E.?. StudyFeaturesId E.==. E.joinV (courseParticipant E.?. CourseParticipantField) E.on $ courseParticipant E.?. CourseParticipantCourse E.==. E.just (E.val examCourse) E.&&. courseParticipant E.?. CourseParticipantUser E.==. E.just (user E.^. UserId) E.&&. courseParticipant E.?. CourseParticipantState E.==. E.just (E.val CourseParticipantActive) @@ -274,14 +245,15 @@ postEGradesR tid ssh csh examn = do unless isLecturer $ E.where_ $ Exam.examOfficeExamResultAuth (E.val uid) examResult - return (examResult, user, occurrence, studyFeatures, studyDegree, studyField, examRegistration, isSynced) + return (examResult, user, occurrence, examRegistration, isSynced) dbtRowKey = views queryExamResult (E.^. ExamResultId) dbtProj :: DBRow _ -> DB ExamUserTableData dbtProj = runReaderT $ (asks . set _dbrOutput) <=< magnify _dbrOutput $ - (,,,,,,,,) - <$> view _1 <*> view _2 <*> view _3 <*> view _4 <*> view _5 <*> view _6 <*> view _7 <*> view (_8 . _Value) + (,,,,,,) + <$> view _1 <*> view _2 <*> view _3 <*> view _4 <*> view (_5 . _Value) <*> getSynchronised + <*> (lift . courseUserStudyFeatures examCourse =<< view (_2 . _entityKey)) where getSynchronised :: ReaderT _ DB [(UserDisplayName, UserSurname, UTCTime, Set SchoolShorthand)] getSynchronised = do @@ -335,9 +307,7 @@ postEGradesR tid ssh csh examn = do , colSynced , imapColonnade participantAnchor . anchorColonnadeM (views (resultUser . _entityKey) participantLink) $ colUserDisplayName (resultUser . _entityVal . $(multifocusL 2) _userDisplayName _userSurname) , colUserMatriculation (resultUser . _entityVal . _userMatrikelnummer) - , emptyOpticColonnade (resultStudyField . _entityVal) colStudyTerms - , emptyOpticColonnade (resultStudyDegree . _entityVal) colStudyDegree - , emptyOpticColonnade (resultStudyFeatures . _entityVal . _studyFeaturesSemester) colStudyFeaturesSemester + , colStudyFeatures resultStudyFeatures , Colonnade.singleton (fromSortable . Sortable (Just "occurrence-start") $ i18nCell MsgExamTime) $ \x -> cell . flip runReaderT x $ do start <- preview $ resultExamOccurrence . _entityVal . _examOccurrenceStart <> like examStart . _Just end <- preview $ resultExamOccurrence . _entityVal . _examOccurrenceEnd . _Just <> like examEnd . _Just @@ -347,9 +317,6 @@ postEGradesR tid ssh csh examn = do dbtSorting = mconcat [ sortUserName' (queryUser . to ((,) <$> (E.^. UserDisplayName) <*> (E.^. UserSurname))) , sortUserMatriculation (queryUser . to (E.^. UserMatrikelnummer)) - , sortStudyTerms queryStudyField - , sortStudyDegree queryStudyDegree - , sortStudyFeaturesSemester (queryStudyFeatures . to (E.?. StudyFeaturesSemester)) , sortOccurrenceStart (queryExamOccurrence . to (E.maybe (E.val examStart) E.just . (E.?. ExamOccurrenceStart))) , maybeOpticSortColumn sortExamResult (queryExamResult . to (E.^. ExamResultResult)) , singletonMap "is-synced" . SortColumn $ view (queryIsSynced $ E.val uid) @@ -357,20 +324,30 @@ postEGradesR tid ssh csh examn = do dbtFilter = mconcat [ fltrUserName' (queryUser . to (E.^. UserDisplayName)) , fltrUserMatriculation (queryUser . to (E.^. UserMatrikelnummer)) - , fltrStudyTerms queryStudyField - , fltrStudyDegree queryStudyDegree - , fltrStudyFeaturesSemester (queryStudyFeatures . to (E.?. StudyFeaturesSemester)) , fltrExamResultPoints (queryExamResult . to (E.^. ExamResultResult) . to E.just) , singletonMap "is-synced" . FilterColumn $ E.mkExactFilter (view . queryIsSynced $ E.val uid) + , fltrRelevantStudyFeaturesTerms (to $ + \t -> ( E.val courseTerm + , views queryUser (E.^. UserId) t + )) + , fltrRelevantStudyFeaturesDegree (to $ + \t -> ( E.val courseTerm + , views queryUser (E.^. UserId) t + )) + , fltrRelevantStudyFeaturesSemester (to $ + \t -> ( E.val courseTerm + , views queryUser (E.^. UserId) t + )) + ] dbtFilterUI = mconcat [ fltrUserNameUI' , fltrUserMatriculationUI - , fltrStudyTermsUI - , fltrStudyDegreeUI - , fltrStudyFeaturesSemesterUI , fltrExamResultPointsUI , \mPrev -> prismAForm (singletonFilter "is-synced" . maybePrism _PathPiece) mPrev $ aopt (boolField . Just $ SomeMessage MsgBoolIrrelevant) (fslI MsgExamUserSynchronised) + , fltrRelevantStudyFeaturesTermsUI + , fltrRelevantStudyFeaturesDegreeUI + , fltrRelevantStudyFeaturesSemesterUI ] dbtStyle = def { dbsFilterLayout = defaultDBSFilterLayout } dbtParams = DBParamsForm @@ -405,9 +382,7 @@ postEGradesR tid ssh csh examn = do (row ^. resultUser . _entityVal . _userFirstName) (row ^. resultUser . _entityVal . _userDisplayName) (row ^. resultUser . _entityVal . _userMatrikelnummer) - (row ^? resultStudyField . _entityVal . to (\StudyTerms{..} -> fromMaybe (tshow studyTermsKey) $ studyTermsName <|> studyTermsShorthand)) - (row ^? resultStudyDegree . _entityVal . to (\StudyDegree{..} -> fromMaybe (tshow studyDegreeKey) $ studyDegreeName <|> studyDegreeShorthand)) - (row ^? resultStudyFeatures . _entityVal . _studyFeaturesSemester) + (row ^. resultStudyFeatures) (row ^? (resultExamOccurrence . _entityVal . _examOccurrenceStart <> like examStart . _Just) . to utcToZonedTime) (row ^. resultExamResult . _entityVal . _examResultResult) , dbtCsvName = unpack csvName diff --git a/src/Handler/Health.hs b/src/Handler/Health.hs index 72a04eee4..050644330 100644 --- a/src/Handler/Health.hs +++ b/src/Handler/Health.hs @@ -88,9 +88,9 @@ getInstanceR = do $newline never