Merge branch 'master' into stundenplan

This commit is contained in:
Sarah Vaupel 2020-10-22 13:19:03 +02:00
commit acb663c480
80 changed files with 1514 additions and 227 deletions

View File

@ -1,7 +1,7 @@
default:
image:
name: fpco/stack-build:lts-16.11
cache:
cache: &global_cache
paths:
- node_modules
- .stack
@ -57,6 +57,9 @@ npm install:
interruptible: true
frontend:build:
cache:
<<: *global_cache
policy: pull
stage: frontend:build
script:
- npm run frontend:build
@ -146,6 +149,9 @@ yesod:build:
resource_group: ram
frontend:test:
cache:
<<: *global_cache
policy: pull
stage: test
script:
- npm run frontend:test
@ -167,9 +173,10 @@ frontend:test:
interruptible: true
deploy:uniworx3:
cache: {}
stage: deploy
script:
- ssh -i ~/.ssh/id root@uniworx3.ifi.lmu.de <bin/uniworx
- zip -qj - bin/uniworx bin/uniworxdb | ssh root@uniworx3.ifi.lmu.de /root/bin/accept_uni2work
needs:
- yesod:build
- frontend:test # For sanity

View File

@ -2,6 +2,89 @@
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.13.0](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/v20.12.1...v20.13.0) (2020-10-20)
### Features
* **allocations:** display participant counts to admins ([b79bac7](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/b79bac777c6d349a626ea4efa6c43141b7f669d0))
### Bug Fixes
* **allocations:** fix allocation-course-accept-substitutes ([b4df980](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/b4df98069982752e36e69571f5557a6179b44cff))
### [20.12.1](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/v20.12.0...v20.12.1) (2020-10-14)
### Bug Fixes
* **auth:** prettier active directory errors in help messages ([b631ed7](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/b631ed7d0620748fd833c4cda4b421dc147d0906))
* **migration:** don't consider changelog in requiresMigration ([ea95d74](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/ea95d74cb5572688531ba0fdeed3983fb70ab236))
## [20.12.0](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/v20.11.1...v20.12.0) (2020-10-14)
### Features
* **ldap:** expose active directory errors ([51ed7e0](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/51ed7e0a26a94d2178a4ca10ad7ea36b99076b54))
### [20.11.1](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/v20.11.0...v20.11.1) (2020-10-14)
### Bug Fixes
* **allocations:** work around yesod weirdness wrt "none" ([4a731ec](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/4a731eca4e69b5ee080f229a602e76f5ae165c64))
## [20.11.0](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/v20.10.0...v20.11.0) (2020-10-13)
### Features
* **allocations:** allocation-course-accept-substitutes ([8abcd65](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/8abcd65edf2a1bf5b6de62103af7427fa7ed7db3))
* **authorisation:** cookie-active-auth-tags ([0d372c6](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/0d372c636a735b4003448ab2518f6354b08ca042))
### Bug Fixes
* **changelog:** try not to crash on unknown changelog items ([850c8d4](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/850c8d4dae47489e0dbf0eb46276eaf0002bf123))
## [20.10.0](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/v20.9.0...v20.10.0) (2020-10-12)
### Features
* **allocations:** ui for adding applicants ([7b7f11e](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/7b7f11e72853e11717c671d434397c707eff3b7f))
## [20.9.0](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/v20.8.1...v20.9.0) (2020-10-12)
### Features
* **exams:** auth ExamResults by ExamExamOfficeSchools ([29a3e24](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/29a3e24bcf01cd9c893857eda00dcd249e6cbbe2))
* **exams:** exam staff & additional schools ([94436ee](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/94436ee0e1ce2cbf13a66f9ad81883d7286acb9b))
### [20.8.1](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/v20.8.0...v20.8.1) (2020-10-12)
### Bug Fixes
* **authorization:** have AllocationTime consider ParticipantState ([b69481e](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/b69481e88fb20890b4ece7a0023dcfdad21604d6))
## [20.8.0](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/v20.7.0...v20.8.0) (2020-10-10)
### Features
* **allocations:** csv-export new-assigned ([a4114a7](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/a4114a79f1bfd968bb9d300f0c39400a8904ee7c))
## [20.7.0](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/v20.6.0...v20.7.0) (2020-10-10)
### Features
* **allocations:** include study features in users table ([7f7d2c7](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/7f7d2c795767fd6fac1fa4a10a304e3e3d2280c3))
## [20.6.0](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/v20.5.1...v20.6.0) (2020-10-06)

View File

@ -196,6 +196,11 @@ cookies:
same-site: lax
http-only: false
secure: "_env:COOKIES_SECURE:true"
ACTIVE-AUTH-TAGS:
expires: 12622780800
same-site: lax
http-only: true
secure: "_env:COOKIES_SECURE:true"
user-defaults:
max-favourites: 12

View File

@ -785,10 +785,13 @@ section
.allocation__courses
margin: 20px 0 0 40px
.form-group__input > &
margin: 0
.allocation-course
display: grid
grid-template-columns: minmax(105px, 1fr) 9fr
grid-template-areas: 'name name ' '. registered ' 'prio-label prio ' 'instr-label instr ' 'form-label form '
grid-template-areas: 'name name' '. admin-info' '. registered' 'prio-label prio' 'instr-label instr' 'form-label form'
grid-gap: 5px 7px
margin: 12px 0
padding: 0 10px 12px 7px
@ -839,10 +842,14 @@ section
text-align: right
padding-top: 6px
.allocation-course__admin-info
@extend .explanation
grid-area: admin-info
@media (max-width: 426px)
.allocation-course
grid-template-columns: 1fr
grid-template-areas: 'name ' 'registered ' 'prio-label ' 'prio ' 'instr-label' 'instr ' 'form-label ' 'form '
grid-template-areas: 'name' 'admin-info' 'registered' 'prio-label' 'prio' 'instr-label' 'instr' 'form-label' 'form'
.allocation-course__application-label
padding-top: 0

View File

@ -1,6 +1,8 @@
FAQNoCampusAccount: Ich habe keine LMU-Benutzerkennung (ehem. Campus-Kennung); kann ich trotzdem Zugang zum System erhalten?
FAQForgottenPassword: Ich habe mein Passwort vergessen
FAQCampusCantLogin: Ich kann mich mit meiner LMU-Benutzerkennung (ehem. Campus-Kennung) nicht anmelden
FAQCourseCorrectorsTutors: Wie kann ich Tutoren oder Korrektoren für meinen Kurs einstellen?
FAQCourseCorrectorsTutors: Wie kann ich Tutoren oder Korrektoren für meinen Kurs konfigurieren?
FAQNotLecturerHowToCreateCourses: Wie kann ich einen neuen Kurs anlegen?
FAQExamPoints: Warum kann ich bei meiner Klausur keine Punkte eintragen?
FAQExamPoints: Warum kann ich bei meiner Klausur keine Punkte eintragen?
FAQInvalidCredentialsAdAccountDisabled: Ich kann mich nicht anmelden und bekomme die Meldung „Benutzereintrag gesperrt“
FAQAllocationNoPlaces: Ich habe über eine Zentralanmeldung keine Plätze/nicht die Plätze, die ich möchte, erhalten

View File

@ -4,3 +4,5 @@ FAQCampusCantLogin: I can't log in using my LMU user ID (formerly Campus-ID)
FAQCourseCorrectorsTutors: How can I add tutors or correctors to my course?
FAQNotLecturerHowToCreateCourses: How can I create new courses?
FAQExamPoints: Why can't I enter achievements for my exam as points?
FAQInvalidCredentialsAdAccountDisabled: I can't log in and am instead given the message “Account disabled”
FAQAllocationNoPlaces: I did not receive any places/the places I wanted from a central allocation

View File

@ -225,6 +225,9 @@ CourseAllocationOption term@Text name@Text: #{name} (#{term})
CourseAllocationMinCapacity: Minimale Teilnehmeranzahl
CourseAllocationMinCapacityTip: Wenn der Veranstaltung bei der Zentralanmeldung weniger als diese Anzahl von Teilnehmern zugeteilt würden, werden diese stattdessen auf andere Kurse umverteilt
CourseAllocationMinCapacityMustBeNonNegative: Minimale Teilnehmeranzahl darf nicht negativ sein
CourseAllocationCourseAcceptsSubstitutesUntil: Akzeptiert Nachrücker bis
CourseAllocationCourseAcceptsSubstitutesNever: Akzeptiert keine Nachrücker
CourseAllocationCourseParticipants: Teilnehmer
CourseApplicationInstructions: Anweisungen zur Bewerbung/Anmeldung
CourseApplicationInstructionsTip: Wird den Studierenden angezeigt, wenn diese sich für Ihre Veranstaltung bewerben bzw. bei dieser anmelden
CourseApplicationTemplate: Bewerbungsvorlagen
@ -281,7 +284,9 @@ CourseApplicationsAllocatedDirectory: zentral
CourseApplicationsNotAllocatedDirectory: direkt
CourseNoAllocationsAvailable: Es sind aktuell keine Zentralanmeldungen verfügbar
AllocationStaffRegisterToExpired: Es dürfen keine Änderungen an der Eintragung des Kurses zur Zentralanmeldung mehr vorgenommen werden. Ihre Änderungen wurden ignoriert.
AllocationStaffRegisterToExpiredAllocation: Die Frist zur Eintrageng von Kursen in die Zentralanmeldung ist verstrichen. Die Teilnahme darf nicht mehr verändert werden.
AllocationStaffRegisterToExpiredMinCapacity: Die Frist zur Eintrageng von Kursen in die Zentralanmeldung ist verstrichen. Die minimale Kapazität darf nicht mehr verändert werden.
CourseFormSectionRegistration: Anmeldung zum Kurs
@ -824,6 +829,8 @@ PersonalInfoExamAchievementsWip: Die Anzeige von Prüfungsergebnissen wird momen
PersonalInfoOwnTutorialsWip: Die Anzeige von Tutorien, zu denen Sie als Tutor eingetragen sind wird momentan an dieser Stelle leider noch nicht unterstützt.
PersonalInfoTutorialsWip: Die Anzeige von Tutorien, zu denen Sie angemeldet sind wird momentan an dieser Stelle leider noch nicht unterstützt.
ActiveAuthTagsSaveCookie: In Cookie speichern?
ActiveAuthTagsSaveCookieTip: Falls gesetzt werden die aktivierten Authorisierungsprädikate zusätzlich zur aktiven Session auch in einem persistenten Cookie gespeichert. Dies kann vor Allem in Kombination mit Tab-Containern nützlich sein.
ActiveAuthTags: Aktivierte Authorisierungsprädikate
InvalidDateTimeFormat: Ungültiges Datums- und Zeitformat, JJJJ-MM-TTTHH:MM[:SS] Format erwartet
@ -1397,6 +1404,7 @@ MenuAllocationUsers: Bewerber
MenuAllocationPriorities: Zentrale Dringlichkeiten
MenuAllocationCompute: Platzvergabe berechnen
MenuAllocationAccept: Platzvergabe akzeptieren
MenuAllocationAddUser: Bewerber hinzufügen
MenuFaq: FAQ
MenuSheetPersonalisedFiles: Personalisierte Dateien herunterladen
MenuCourseSheetPersonalisedFiles: Vorlage für personalisierte Übungsblatt-Dateien herunterladen
@ -1471,6 +1479,7 @@ BreadcrumbAllocationUsers: Bewerber
BreadcrumbAllocationPriorities: Zentrale Dringlichkeiten
BreadcrumbAllocationCompute: Platzvergabe berechnen
BreadcrumbAllocationAccept: Platzvergabe akzeptieren
BreadcrumbAllocationAddUser: Bewerber hinzufügen
BreadcrumbMessageHide: Verstecken
BreadcrumbFaq: FAQ
BreadcrumbSheetPersonalisedFiles: Personalisierte Dateien herunterladen
@ -1484,9 +1493,9 @@ ExternalExamUsers coursen@CourseName examn@ExamName: Teilnehmer: #{coursen}, #{e
TitleMetrics: Metriken
AuthPredsInfo: Um eigene Veranstaltungen aus Sicht der Teilnehmer anzusehen, können Veranstalter und Korrektoren hier die Prüfung ihrer erweiterten Berechtigungen temporär deaktivieren. Abgewählte Prädikate schlagen immer fehl. Abgewählte Prädikate werden also nicht geprüft um Zugriffe zu gewähren, welche andernfalls nicht erlaubt wären. Diese Einstellungen gelten nur temporär bis Ihre Sitzung abgelaufen ist, d.h. bis ihr Browser-Cookie abgelaufen ist. Durch Abwahl von Prädikaten kann man sich höchstens temporär aussperren.
AuthPredsInfo: Um eigene Veranstaltungen aus Sicht der Teilnehmer anzusehen, können Veranstalter und Korrektoren hier die Prüfung ihrer erweiterten Berechtigungen temporär deaktivieren. Abgewählte Prädikate schlagen immer fehl. Abgewählte Prädikate werden also nicht geprüft um Zugriffe zu gewähren, welche andernfalls nicht erlaubt wären.
AuthPredsActive: Aktive Authorisierungsprädikate
AuthPredsActiveChanged: Authorisierungseinstellungen für aktuelle Sitzung gespeichert
AuthPredsActiveChanged: Authorisierungseinstellungen gespeichert
AuthTagFree: Seite ist universell zugänglich
AuthTagAdmin: Nutzer ist Administrator
AuthTagExamOffice: Nutzer ist mit Prüfungsverwaltung beauftragt
@ -1855,6 +1864,11 @@ ExamRoomDescription: Beschreibung
ExamTimeTip: Nur zur Information der Studierenden, die tatsächliche Zeitangabe erfolgt pro Prüfungstermin/Raum
ExamRoomAssigned: Zugeteilt
ExamRoomRegistered: Anmeldung
ExamStaff: Prüfer/Verantwortliche Hochschullehrer
ExamStaffTip: Geben Sie bitte in jedem Fall einen Namen an, der den Prüfer/Veranstalter/Verantwortlichen Hochschullehrer eindeutig identifiziert! Sollte der Name des Prüfers allein womöglich nicht eindeutig sein, so geben Sie bitte eindeutig identifizierende Zusatzinfos, wie beispielsweise den Lehrstuhl bzw. die LFE o.Ä., an.
ExamStaffRequired: „Prüfer/Verantwortilche Hochschullehrer” muss angegeben werden
ExamExamOfficeSchools: Zusätzliche Institute
ExamExamOfficeSchoolsTip: Prüfungsbeauftragte von Instituten, die Sie hier angeben, erhalten im System (zusätzlich zum primären Institut des zugehörigen Kurses) volle Einsicht in sämtliche für diese Prüfung hinterlegten Leistungen, unabhängig von den Studiendaten der Teilnehmer.
ExamOccurrenceStart: Prüfungsbeginn
@ -1864,6 +1878,7 @@ ExamFormAutomaticFunctions: Automatische Funktionen
ExamFormCorrection: Korrektur
ExamFormParts: Teile
ExamFormMode: Ausgestaltung der Prüfung
ExamFormGrades: Prüfungsleistungen
ExamModeFormNone: Keine Angabe
ExamModeFormCustom: Benutzerdefiniert
@ -2278,6 +2293,9 @@ AllocationNotificationNewCourseSuccessForceOff: Sie werden nicht benachrichtigt,
AllocationNotificationNewCourseCurrentlyOff: Aktuell würden Sie keine Benachrichtigung erhalten.
AllocationNotificationNewCourseCurrentlyOn: Aktuell würden Sie benachrichtigt werden.
AllocationNotificationLoginFirst: Um Ihre Benachrichtigungseinstellungen zu ändern, loggen Sie sich bitte zunächst ein.
AllocationNextSubstitutesDeadline: Nächster Kurs akzeptiert Nachrücker bis
AllocationNextSubstitutesDeadlineNever: Keine Kurse akzeptieren mehr Nachrücker
AllocationFreeCapacity: Freie Plätze
AllocationSchoolShort: Institut
Allocation: Zentralanmeldung
@ -2517,6 +2535,8 @@ CourseDeregistrationAllocationReason: Grund
CourseDeregistrationAllocationReasonTip: Der angegebene Grund wird permanent im System hinterlegt und ist i.A. einziger Anhaltspunkt zur Schlichtung etwaiger Konflikte
CourseDeregistrationAllocationNoShow: „Nicht erschienen“ eintragen
CourseDeregistrationAllocationNoShowTip: Soll für alle Prüfungen dieses Kurses „nicht erschienen“ als Prüfungsleistung eingetragen werden? Dies geschieht einmalig bei der Abmeldung (sofern nicht bereits eine Prüfungsleistung existiert) und automatisch beim Anlegen von neuen Prüfungen.
CourseAcceptSubstitutesUntil: Nachrücker akzeptieren bis
CourseAcceptSubstitutesUntilTip: Bis zu welchem Zeitpunkt sollen durch die Zentralanmeldung Nachrücker diesem Kurs zugewiesen werden? Wird kein Datum angegeben werden nach der Initialen Verteilung nie Nachrücker zugewiesen. Diese Frist sollte nicht willkürlich früh bzw. nicht gesetzt werden, um für die Studierenden keine unnötige Beschränkung darzustellen. Geeignet ist z.B. bei einem Seminar wenige Stunden vor dem ersten Treffen zum Verteilen der Themen.
CourseDeregisterNoShow: „Nicht erschienen“ bei Abmeldung
CourseDeregisterNoShowTip: Soll, wenn sich Teilnehmer selbstständig abmelden, für alle Prüfungen dieses Kurses „nicht erschienen“ als Prüfungsleistung eingetragen werden? Dies geschieht einmalig bei der Abmeldung (sofern nicht bereits eine Prüfungsleistung existiert) und automatisch beim Anlegen von neuen Prüfungen.
CourseDeregistrationAllocationShouldLog: Selbstverschuldet
@ -2528,6 +2548,7 @@ AllocationResultsLecturer: Im Rahmen der oben genannten Zentralanmeldung wurden
AllocationResultLecturer csh@CourseShorthand count@Int64 count2@Int64: #{count} Teilnehmer (von insgesamt #{count2}) für #{csh}
AllocationResultLecturerAll csh@CourseShorthand count@Int64: #{count} Teilnehmer für #{csh}
AllocationResultLecturerNone csh@CourseShorthand: Keine Teilnehmer für #{csh}
AllocationResultsLecturerSubstituteCoursesWarning: Bitte konfigurieren Sie so bald wie möglich einen Zeitrahmen in dem Sie bereit sind etwaige Nachrücker in den folgenden Kursen zu akzeptieren:
AllocationResultsStudent: Sie haben Plätze erhalten in:
AllocationNoResultsStudent: Sie haben leider keine Plätze erhalten.
AllocationResultStudent csh@CourseShorthand: Sie haben einen Platz in #{csh} erhalten.
@ -2712,22 +2733,46 @@ CsvColumnAllocationUserSurname: Nachname(n) des Bewerbers
CsvColumnAllocationUserFirstName: Vorname(n) des Bewerbers
CsvColumnAllocationUserName: Voller Name des Bewerbers
CsvColumnAllocationUserMatriculation: Matrikelnummer des Bewerber
CsvColumnAllocationUserStudyFeatures: Studiendaten
CsvColumnAllocationUserRequested: Maximale Anzahl von Plätzen, die der Bewerber bereit ist, zu akzeptieren
CsvColumnAllocationUserApplied: Anzahl von Bewerbungen, die der Bewerber eingereicht hat
CsvColumnAllocationUserVetos: Anzahl von Bewerbungen, die von Kursverwaltern ein Veto oder eine Note erhalten haben, die äquivalent ist zu "Nicht Bestanden" (5.0)
CsvColumnAllocationUserAssigned: Anzahl von Plätzen, die der Bewerber durch diese Zentralanmeldung bereits erhalten hat
CsvColumnAllocationUserNewAssigned: Anzahl von Plätzen, die der Bewerber, nach Akzeptieren der berechneten Verteilung, zusätzlich erhalten würde
CsvColumnAllocationUserPriority: Zentrale Dringlichkeit des Bewerbers; entweder einzelne Zahl für Sortierungsbasierte Dringlichkeiten (höhere Dringlichkeit entspricht größerer Zahl) oder Komma-separierte Liste von numerischen Dringlichkeiten in eckigen Klammern (z.B. [1, 2, 3])
AllocationUsersCsvName tid@TermId ssh@SchoolId ash@AllocationShorthand: #{foldCase (termToText (unTermKey tid))}-#{foldedCase (unSchoolKey ssh)}-#{foldedCase ash}-bewerber
AllocationPrioritiesMode: Modus
AllocationPrioritiesNumeric: Numerische Dringlichkeiten
AllocationPrioritiesOrdinal: Dringlichkeiten durch Sortierung
AllocationPriorityNumeric': Numerisch
AllocationPriorityOrdinal': Nach Sortierung
AllocationPriorityNumericValues: Numerische Werte
AllocationPriorityNumericValuesTip: Komma-separierte ganze Zahlen
AllocationPriorityNumericNoValues: Es wurden keine numerischen Werte angegeben
AllocationPriorityNumericNoParse val@Text: Ganze Zahl konnte nicht geparst werden: „#{val}“
AllocationPriorityOrdinalValueNegative: Sortier-Index darf nicht negativ sein
AllocationPriorityOrdinalValue: Sortier-Index
AllocationPriorityOrdinalValueTip: Null entspricht dem ersten Eintrag der Liste, höhere Indizes entsprechen später in der sortierten Liste vorkommenden Bewerbern und damit einer höheren Dringlichkeit
AllocationPrioritiesTitle tid@TermId ssh@SchoolId ash@AllocationShorthand: #{tid}-#{ssh}-#{ash}: Zentrale Dringlichkeiten
AllocationPrioritiesFile: CSV-Datei
AllocationPrioritiesSunk num@Int64: Zentrale Prioritäten für #{num} Bewerber erfolgreich hinterlegt
AllocationPrioritiesMissing num@Int64: Für #{num} Bewerber ist keine zentrale Priorität hinterlegt, da in der hochgeladenen CSV-Datei die #{pluralDE num "entsprechende Matrikelnummer" "entsprechenden Matrikelnummern"} nicht gefunden #{pluralDE num "wurde" "wurden"}
AllocationMissingPrioritiesIgnored: Bewerber, für die keine zentrale Priorität angegeben wird, werden bei der Vergabe ignoriert!
AllocationAddUserUserNotFound: E-Mail Adresse konnte keinem Benutzer zugeordnet werden
AllocationAddUserUser: Benutzer
AllocationAddUserUserPlaceholder: E-Mail
AllocationAddUserTotalCoursesLessThanOne: Anzahl angefragter Plätze muss größer null sein
AllocationAddUserTotalCourses: Angefragte Plätze
AllocationAddUserSetPriority: Zentrale Dringlichkeit eintragen?
AllocationAddUserPriority: Zentrale Dringlichkeit
AllocationAddUserApplications: Bewerbungen/Bewertungen
AllocationAddUserTitle termText@Text ssh'@SchoolShorthand allocation@AllocationName: #{termText} - #{ssh'} - #{allocation}: Bewerber hinzufügen
AllocationAddUserShortTitle tid@TermId ssh@SchoolId ash@AllocationShorthand: #{tid}-#{ssh}-#{ash}: Bewerber hinzufügen
AllocationAddUserUserAdded: Bewerber erfolgreich zur Zentralanmeldung hinzugefügt
AllocationAddUserUserExists: Der angegebene Benutzer ist bereits ein Bewerber zur Zentralanmeldung
ExampleUser1FirstName: Max ZweiterName
ExampleUser1Surname: Mustermann
ExampleUser1DisplayName: Max Mustermann
@ -2743,6 +2788,9 @@ AllocationUsersMissingPrioritiesTip: Es muss sichergestellt sein, dass keine Tei
AllocationUsersMissingPrioritiesOk: Es wurde sichergestellt, dass es für jeden der genannten Benutzer einen zulässigen Grund gibt, warum dieser nicht an der Zentralanmeldung teilnehmen sollte.
AllocationRestrictCourses: Kurse einschränken
AllocationRestrictCoursesTip: Sollen nur Plätze für eine Teilmenge von Kursen zugewiesen werden? So können u.A. Nachrücker verteilt werden. Diese Funktionalität sollte nur verwendet werden, wenn manche Kurse aus zulässigen Gründen ausgeschlossen werden müssen; z.B. weil ein Seminar bereits ein Treffen zur Organisation hatte und nun keine weiteren Teilnehmer mehr akzeptieren kann.
AllocationCourseRestrictionDontRestrict: Nicht einschränken
AllocationCourseRestrictionSubstitutes: Kurse, die aktuell Nachrücker azkeptieren
AllocationCourseRestrictionCustom: Benutzerdefiniert
AllocationRestrictCoursesSelection: Kurse
AllocationRestrictCoursesSelectionTip: Teilnehmer werden nur auf die Kurse verteilt, die hier angegeben werden.
AllocationUsersMissingPrioritiesNotOk: Zentralvergabe kann nicht erfolgen, solange nicht allen Teilnehmern, die nicht explizit von der Vergabe ausgeschlossen wurden („Teilnehmer ohne zentrale Dringlichkeit”), eine zentrale Dringlichkeit zugewiesen wurde!
@ -2760,6 +2808,7 @@ AllocationOfferedPlaces: Angebotene Plätze
AllocationUserNewMatches: Neue Zuteilungen
AllocationUsersCount: Teilnehmer
AllocationCoursesCount: Kurse
AllocationCourseEligible: Berücksichtigt
CourseOption tid@TermId ssh@SchoolId coursen@CourseName: #{tid} - #{ssh} - #{coursen}
@ -2858,4 +2907,16 @@ SystemExamOffice: Prüfungsverwaltung
SystemFaculty: Fakultätsmitglied
ChangelogItemFeature: Feature
ChangelogItemBugfix: Bugfix
ChangelogItemBugfix: Bugfix
InvalidCredentialsADNoSuchObject: Benutzereintrag existiert nicht
InvalidCredentialsADLogonFailure: Ungültiges Passwort
InvalidCredentialsADAccountRestriction: Kontobeschränkungen verhindern Login
InvalidCredentialsADInvalidLogonHours: Benutzer darf sich zur aktuellen Tageszeit nicht anmelden
InvalidCredentialsADInvalidWorkstation: Benutzer darf sich von diesem System aus nicht anmelden
InvalidCredentialsADPasswordExpired: Passwort abgelaufen
InvalidCredentialsADAccountDisabled: Benutzereintrag gesperrt
InvalidCredentialsADTooManyContextIds: Benutzereintrag trägt zu viele Sicherheitskennzeichen
InvalidCredentialsADAccountExpired: Benutzereintrag abgelaufen
InvalidCredentialsADPasswordMustChange: Passwort muss geändert werden
InvalidCredentialsADAccountLockedOut: Benutzereintrag wurde durch Eindringlingserkennung gesperrt

View File

@ -226,6 +226,9 @@ CourseAllocationOption term name: #{name} (#{term})
CourseAllocationMinCapacity: Minimum number of participants
CourseAllocationMinCapacityTip: If fewer students than this number were to be assigned to this course, then these students would instead be assigned to other courses
CourseAllocationMinCapacityMustBeNonNegative: Minimum number of participants must not be negative
CourseAllocationCourseAcceptsSubstitutesUntil: Accepts substitutes until
CourseAllocationCourseAcceptsSubstitutesNever: Does not accept substitutes
CourseAllocationCourseParticipants: Participants
CourseApplicationInstructions: Instructions for application
CourseApplicationInstructionsTip: Will be shown to students if they decide to apply for this course
CourseApplicationTemplate: Application template
@ -282,7 +285,8 @@ CourseApplicationsAllocatedDirectory: central
CourseApplicationsNotAllocatedDirectory: direct
CourseNoAllocationsAvailable: There are no ongoing central allocations
AllocationStaffRegisterToExpired: You cannot change course properties concerning the central allocation after the course registration period. Your changes may have been discarded.
AllocationStaffRegisterToExpiredAllocation: The course registration period for the central allocation is over. Participation may not be changed.
AllocationStaffRegisterToExpiredMinCapacity: The course registration period for the central allocation is over. Minimum capacity may not be changed.
CourseFormSectionRegistration: Registration
CourseFormSectionAdministration: Administration
@ -822,6 +826,9 @@ PersonalInfoExamAchievementsWip: The feature to display your exam achievements h
PersonalInfoOwnTutorialsWip: The feature to display tutorials you have been assigned to as tutor has not yet been implemented.
PersonalInfoTutorialsWip: The feature to display tutorials you have registered for has not yet been implemented.
ActiveAuthTagsSaveCookie: Save in cookie?
ActiveAuthTagsSaveCookieTip: Should the configuration of active authorisation predicates be additionally saved in a persistent cookie? This may be especially useful if using container tabs.
ActiveAuthTags: Active authorisation predicates
InvalidDateTimeFormat: Invalid date and time format. YYYY-MM-DDTHH:MM[:SS] expected
@ -1398,6 +1405,7 @@ MenuAllocationUsers: Applicants
MenuAllocationPriorities: Central priorities
MenuAllocationCompute: Compute allocation
MenuAllocationAccept: Accept allocation
MenuAllocationAddUser: Add applicant
MenuFaq: FAQ
MenuSheetPersonalisedFiles: Download personalised sheet files
MenuCourseSheetPersonalisedFiles: Download template for personalised sheet files
@ -1472,6 +1480,7 @@ BreadcrumbAllocationUsers: Applicants
BreadcrumbAllocationPriorities: Central priorities
BreadcrumbAllocationCompute: Compute allocation
BreadcrumbAllocationAccept: Accept allocation
BreadcrumbAllocationAddUser: Add applicant
BreadcrumbMessageHide: Hide
BreadcrumbFaq: FAQ
BreadcrumbSheetPersonalisedFiles: Download personalised sheet files
@ -1485,9 +1494,9 @@ ExternalExamUsers coursen examn: Exam participants: #{coursen}, #{examn}
TitleMetrics: Metrics
AuthPredsInfo: To view their own courses like a participant would, administrators and correctors can deactivate the checking of their credentials temporarily. Disabled authorisation predicates always fail. This means that deactivated predicates are not checked to grant access where it would otherwise not be permitted. These settings are only temporary, until your session expires i.e. your browser-cookie does. By deactivating predicates you can lock yourself out temporarily, at most.
AuthPredsInfo: To view their own courses like a participant would, administrators and correctors can deactivate the checking of their credentials temporarily. Disabled authorisation predicates always fail. This means that deactivated predicates are not checked to grant access where it would otherwise not be permitted.
AuthPredsActive: Active authorisation predicates
AuthPredsActiveChanged: Authorisation settings saved for the current session
AuthPredsActiveChanged: Successfully saved authorisation settings
AuthTagFree: Page is freely accessable
AuthTagAdmin: User is administrator
AuthTagExamOffice: User is part of an exam office
@ -1854,6 +1863,11 @@ ExamRoomDescription: Description
ExamTimeTip: Only for informational purposes. The actual times are set for each occurrence/room
ExamRoomAssigned: Assigned
ExamRoomRegistered: Registration
ExamStaff: Examiner/Responsible university teacher
ExamStaffTip: Please always specify a name that uniquely identifies the examiner/organiser/repsonsible university teacher! If there is a possibility that the name alone is ambiguous please also specify some additional information e.g. the professorial chair or the educational and research unit.
ExamStaffRequired: “Examiner/Responsible university teacher” must be specified
ExamExamOfficeSchools: Additional departments
ExamExamOfficeSchoolsTip: Exam offices of departments you specify here will also have full access to all results for this exam disregarding the individual participants' features of study.
ExamOccurrenceStart: Exam starts
@ -1863,6 +1877,7 @@ ExamFormAutomaticFunctions: Automatic functions
ExamFormCorrection: Correction
ExamFormParts: Exam parts
ExamFormMode: Exam design
ExamFormGrades: Exam achievements
ExamModeFormNone: Not specified
ExamModeFormCustom: Custom
@ -2277,6 +2292,9 @@ AllocationNotificationNewCourseSuccessForceOff: You will not be notified if a ne
AllocationNotificationNewCourseCurrentlyOff: Currently you would not receive a notification.
AllocationNotificationNewCourseCurrentlyOn: Currently you would be notified.
AllocationNotificationLoginFirst: To change your notification settings, please log in first.
AllocationNextSubstitutesDeadline: Next course accepts substitutes until
AllocationNextSubstitutesDeadlineNever: No course currently accepts substitutes
AllocationFreeCapacity: Free capacity
AllocationSchoolShort: Department
Allocation: Central allocation
@ -2517,6 +2535,8 @@ CourseDeregistrationAllocationReason: Reason
CourseDeregistrationAllocationReasonTip: The specified reason will be permanently stored and might be the only information available during conflict resolution
CourseDeregistrationAllocationNoShow: Record as “no show”
CourseDeregistrationAllocationNoShowTip: Should, for all exams associated with this course, “no show” be recorded as the exam achievement automatically? This would be done once immediately (if no other achievement exists for the given exam) and automatically whenever a new exam is created.
CourseAcceptSubstitutesUntil: Accept substitute registrations until
CourseAcceptSubstitutesUntilTip: Until which time should substitute registrations through the central allocation be accepted to fill free places in the course? If left empty no substitute registrations will be made. This deadline should not arbitrarily be set early or ommitted so as to not be an unneccesarily restrictive for students. For a seminar a valid choice might be a few hours before the first meeting in which topics will be assigned.
CourseDeregisterNoShow: Record “no show” when deregistering
CourseDeregisterNoShowTip: Should “no show” be recorded as the exam achievement for all exams associated with this course automatically whenever a course participant deregisters themselves? This would be done once upon deregistration (if no other achievement exists for the given exam) and automatically whenever a new exam is created.
CourseDeregistrationAllocationShouldLog: Self imposed
@ -2528,6 +2548,7 @@ AllocationResultsLecturer: In the course of the central allocations placements h
AllocationResultLecturer csh count count2: #{count} #{pluralEN count "participant" "participants"} (of #{count2}) for #{csh}
AllocationResultLecturerAll csh count: #{count} #{pluralEN count "participant" "participants"} for #{csh}
AllocationResultLecturerNone csh: No participants for #{csh}
AllocationResultsLecturerSubstituteCoursesWarning: Please configure a deadline up to which you are able to accept substitute registrations for the following courses as soon as possible:
AllocationResultsStudent: You have been placed in:
AllocationNoResultsStudent: Unfortunately you were not placed in any courses.
AllocationResultStudent csh: You were placed in #{csh}.
@ -2712,22 +2733,46 @@ CsvColumnAllocationUserSurname: Applicant's surname(s)
CsvColumnAllocationUserFirstName: Applicants's first name(s)
CsvColumnAllocationUserName: Applicant's full name
CsvColumnAllocationUserMatriculation: Applicant's matriculation
CsvColumnAllocationUserStudyFeatures: Features of study
CsvColumnAllocationUserRequested: Maximum number of placements the applicant is prepared to accept
CsvColumnAllocationUserApplied: Number of applications the applicant has provided
CsvColumnAllocationUserVetos: Number of applications that have received a veto from a course administrator or have been rated with a grade that is equivalent to "failed" (5.0)
CsvColumnAllocationUserAssigned: Number of assignments the applicant has already received
CsvColumnAllocationUserNewAssigned: Number of assignments the applicant would receive, if the calculated matching is accepted
CsvColumnAllocationUserPriority: Central priority of this applicant; either a number based on the applicants position in the list sorted by priority (higher numbers mean a higher priority) or a comma-separated list of numerical priorities in square brackets (e.g. [1, 2, 3])
AllocationUsersCsvName tid ssh ash: #{foldCase (termToText (unTermKey tid))}-#{foldedCase (unSchoolKey ssh)}-#{foldedCase ash}-applicants
AllocationPrioritiesMode: Mode
AllocationPrioritiesNumeric: Numeric priorities
AllocationPrioritiesOrdinal: Priorities based on sorted list
AllocationPriorityNumeric': Numerical
AllocationPriorityOrdinal': Based on sorted list
AllocationPriorityNumericValues: Numerical values
AllocationPriorityNumericValuesTip: Comma separated whole numbers
AllocationPriorityNumericNoValues: No numerical values were provided
AllocationPriorityNumericNoParse val: Whole number could not be parsed: “#{val}”
AllocationPriorityOrdinalValueNegative: Sorting index may not be negative
AllocationPriorityOrdinalValue: Sorting index
AllocationPriorityOrdinalValueTip: Zero corresponds to the first entry in the list; higher indices correspond to applicants occurring later in the sorted list and thus to higher central priorities
AllocationPrioritiesTitle tid ssh ash: #{tid}-#{ssh}-#{ash}: Central priorities
AllocationPrioritiesFile: CSV file
AllocationPrioritiesSunk num: Successfully registered central priorities for #{num} #{pluralEN num "applicant" "applicants"}
AllocationPrioritiesMissing num: Could not register central priorities for #{num} #{pluralEN num "applicant" "applicants"} because their matriculation was not found in the uploaded CSV file
AllocationMissingPrioritiesIgnored: Applicants for whom no central priority has been registered will be ignored during assignment!
AllocationAddUserUserNotFound: Email could not be resolved to an user
AllocationAddUserUser: User
AllocationAddUserUserPlaceholder: Email
AllocationAddUserTotalCoursesLessThanOne: Number of requested courses needs to be greater than zero
AllocationAddUserTotalCourses: Requested courses
AllocationAddUserSetPriority: Set central priority?
AllocationAddUserPriority: Central priority
AllocationAddUserApplications: Applications/Ratings
AllocationAddUserTitle termText ssh' allocation: #{termText} - #{ssh'} - #{allocation}: Add applicant
AllocationAddUserShortTitle tid@TermId ssh@SchoolId ash@AllocationShorthand: #{tid}-#{ssh}-#{ash}: Add applicant
AllocationAddUserUserAdded: Successfully added applicant to central allocation
AllocationAddUserUserExists: The specified user is already an applicant for the central allocation
ExampleUser1FirstName: Max SecondName
ExampleUser1Surname: Mustermann
ExampleUser1DisplayName: Max Mustermann
@ -2743,6 +2788,9 @@ AllocationUsersMissingPrioritiesTip: Care must be taken, that no participant is
AllocationUsersMissingPrioritiesOk: It was ensured, that all participants mentioned above, are excluded from the allocation on valid grounds.
AllocationRestrictCourses: Restrict courses
AllocationRestrictCoursesTip: Should places be assigned only in a subset of courses? This functionality can be used to make alternate placements in the case that some participants withdraw from their assigned courses. This functionality should only be used to exclude courses on valid grounds. E.g. if a seminar already had a planning meeting and is thus unable to accept new participants.
AllocationCourseRestrictionDontRestrict: Don't restrict
AllocationCourseRestrictionSubstitutes: Courses which currently allow substitute registrations
AllocationCourseRestrictionCustom: Custom
AllocationRestrictCoursesSelection: Courses
AllocationRestrictCoursesSelectionTip: Participants will only be assigned to courses listed here.
AllocationUsersMissingPrioritiesNotOk: Central allocation cannot occur until all participants, that were not excluded explicitly (“Participants without central priority”), have been assigned a central priority!
@ -2760,6 +2808,7 @@ AllocationOfferedPlaces: Offered places
AllocationUserNewMatches: New allocations
AllocationUsersCount: Participants
AllocationCoursesCount: Courses
AllocationCourseEligible: Considered
CourseOption tid ssh coursen: #{tid} - #{ssh} - #{coursen}
@ -2859,4 +2908,16 @@ SystemExamOffice: Exam office
SystemFaculty: Faculty member
ChangelogItemFeature: Feature
ChangelogItemBugfix: Bugfix
ChangelogItemBugfix: Bugfix
InvalidCredentialsADNoSuchObject: User entry does not exist
InvalidCredentialsADLogonFailure: Invalid passwod
InvalidCredentialsADAccountRestriction: Account restrictions are preventing login
InvalidCredentialsADInvalidLogonHours: User may not login at the current time of day
InvalidCredentialsADInvalidWorkstation: User may not login from this system
InvalidCredentialsADPasswordExpired: Password expired
InvalidCredentialsADAccountDisabled: Account disabled
InvalidCredentialsADTooManyContextIds: Account carries to many security identifiers
InvalidCredentialsADAccountExpired: Account expired
InvalidCredentialsADPasswordMustChange: Password needs to be changed
InvalidCredentialsADAccountLockedOut: Account disabled by intruder detection

View File

@ -36,6 +36,7 @@ AllocationCourse
allocation AllocationId
course CourseId
minCapacity Int -- if the course would get assigned fewer than this many applicants, restart the assignment process without the course
acceptSubstitutes UTCTime Maybe
UniqueAllocationCourse course
AllocationUser

View File

@ -18,6 +18,7 @@ Exam
gradingMode ExamGradingMode
description Html Maybe
examMode ExamMode
staff Text Maybe
UniqueExam course name
ExamPart
exam ExamId
@ -68,3 +69,7 @@ ExamPartCorrector
part ExamPartId
corrector ExamCorrectorId
UniqueExamPartCorrector part corrector
ExamOfficeSchool
school SchoolId
exam ExamId
UniqueExamOfficeSchool exam school

2
package-lock.json generated
View File

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

View File

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

View File

@ -1,5 +1,5 @@
name: uniworx
version: 20.6.0
version: 20.13.0
dependencies:
- base
@ -70,6 +70,7 @@ dependencies:
- blaze-html
- conduit-resumablesink >=0.2
- parsec
- parsec-numbers
- attoparsec
- uuid
- exceptions

1
routes
View File

@ -113,6 +113,7 @@
/register ARegisterR POST !time
/course/#CryptoUUIDCourse/apply AApplyR POST !timeANDallocation-registered
/users AUsersR GET POST !allocation-admin
/users/add AAddUserR GET POST !allocation-admin
/priorities APriosR GET POST !allocation-admin
/compute AComputeR GET POST !allocation-admin
/accept AAcceptR GET POST !allocation-admin

View File

@ -1,7 +1,7 @@
{-# OPTIONS_GHC -fno-warn-orphans #-}
module Application
( getAppDevSettings
( getAppSettings, getAppDevSettings
, appMain
, develMain
, makeFoundation
@ -11,8 +11,8 @@ module Application
, getApplicationRepl
, shutdownApp
-- * for GHCI
, handler
, db
, handler, handler'
, db, db'
, addPWEntry
) where
@ -235,7 +235,7 @@ makeFoundation appSettings'@AppSettings{..} = do
migrateAll `runSqlPool` sqlPool
| otherwise -> whenM (requiresMigration `runSqlPool` sqlPool) $ do
$logErrorS "setup" "Migration required"
liftIO . exitWith $ ExitFailure 2
liftIO . exitWith $ ExitFailure 130
$logDebugS "setup" "Cluster-Config"
appCryptoIDKey <- clusterSetting (Proxy :: Proxy 'ClusterCryptoIDKey) `runSqlPool` sqlPool
@ -620,17 +620,19 @@ shutdownApp app = do
---------------------------------------------
-- | Run a handler
handler :: Handler a -> IO a
handler, handler' :: Handler a -> IO a
handler h = runResourceT $ getAppDevSettings >>= makeFoundation >>= liftIO . flip unsafeHandler h
handler' h = runResourceT $ getAppSettings >>= makeFoundation >>= liftIO . flip unsafeHandler h
-- | Run DB queries
db :: DB a -> IO a
db, db' :: DB a -> IO a
db = handler . runDB
db' = handler' . runDB
addPWEntry :: User
-> Text {-^ Password -}
-> IO ()
addPWEntry User{ userAuthentication = _, ..} (Text.encodeUtf8 -> pw) = db $ do
addPWEntry User{ userAuthentication = _, ..} (Text.encodeUtf8 -> pw) = db' $ do
PWHashConf{..} <- getsYesod $ view _appAuthPWHash
(AuthPWHash . Text.decodeUtf8 -> userAuthentication) <- liftIO $ makePasswordWith pwHashAlgorithm pw pwHashStrength
void $ insert User{..}

View File

@ -1,5 +1,6 @@
module Auth.LDAP
( apLdap
, ADError(..), ADInvalidCredentials(..)
, campusLogin
, CampusUserException(..)
, campusUser, campusUser'
@ -26,6 +27,8 @@ import qualified Data.Text.Encoding as Text
import qualified Yesod.Auth.Message as Msg
import Auth.LDAP.AD
data CampusLogin = CampusLogin
{ campusIdent :: CI Text
@ -155,6 +158,14 @@ campusUserMatr' pool mode
newtype ADInvalidCredentials = ADInvalidCredentials ADError
deriving (Eq, Ord, Read, Show, Generic, Typeable)
deriving newtype (Universe, Finite, Enum, Bounded, PathPiece, ToJSON, FromJSON, ToJSONKey, FromJSONKey)
isUnusualADError :: ADError -> Bool
isUnusualADError = flip notElem [ADNoSuchObject, ADLogonFailure]
campusForm :: ( RenderMessage (HandlerSite m) FormMessage
, RenderMessage (HandlerSite m) (ValueRequired (HandlerSite m))
, RenderMessage (HandlerSite m) CampusMessage
@ -174,6 +185,7 @@ campusLogin :: forall site.
, RenderMessage site CampusMessage
, RenderMessage site AFormMessage
, RenderMessage site (ValueRequired site)
, RenderMessage site ADInvalidCredentials
, Button site ButtonSubmit
) => Failover (LdapConf, LdapPool) -> FailoverMode -> AuthPlugin site
campusLogin pool mode = AuthPlugin{..}
@ -203,6 +215,14 @@ campusLogin pool mode = AuthPlugin{..}
$logErrorS apName $ "Error during login: " <> tshow err
observeLoginOutcome apName LoginError
loginErrorMessageI LoginR Msg.AuthError
Right (Left (Ldap.ResponseErrorCode _ errCode _ errTxt))
| Right adError <- parseADError errCode errTxt
, isUnusualADError adError -> do
$logInfoS apName [st|#{campusIdent}: #{toPathPiece adError}|]
observeLoginOutcome apName LoginADInvalidCredentials
MsgRenderer mr <- liftHandler getMsgRenderer
setSessionJson SessionError . PermissionDenied . toPathPiece $ ADInvalidCredentials adError
loginErrorMessage (tp LoginR) . mr $ ADInvalidCredentials adError
Right (Left bindErr) -> do
case bindErr of
Ldap.ResponseErrorCode _ _ _ errTxt ->

76
src/Auth/LDAP/AD.hs Normal file
View File

@ -0,0 +1,76 @@
module Auth.LDAP.AD
( ADError(..)
, parseADError
) where
import Import.NoFoundation hiding (try)
import Model.Types.TH.PathPiece
import qualified Data.IntMap.Strict as IntMap
import qualified Data.Map.Strict as Map
import Text.Parsec hiding ((<|>))
import Text.Parsec.String
import Text.ParserCombinators.Parsec.Number (hexnum)
import Ldap.Client (ResultCode(..))
-- | Copied from <https://ldapwiki.com/wiki/Common%20Active%20Directory%20Bind%20Errors>
data ADError
= ADNoSuchObject
| ADLogonFailure
| ADAccountRestriction
| ADInvalidLogonHours
| ADInvalidWorkstation
| ADPasswordExpired
| ADAccountDisabled
| ADTooManyContextIds
| ADAccountExpired
| ADPasswordMustChange
| ADAccountLockedOut
deriving (Eq, Ord, Read, Show, Enum, Bounded, Generic, Typeable)
deriving anyclass (Universe, Finite)
nullaryPathPiece ''ADError $ camelToPathPiece' 1
pathPieceJSON ''ADError
pathPieceJSONKey ''ADError
derivePersistFieldPathPiece ''ADError
fromADErrorCode :: ResultCode -> Word32 -> Maybe ADError
fromADErrorCode resCode subResCode = IntMap.lookup (fromIntegral subResCode) =<< Map.lookup resCode errorCodes
where
errorCodes = Map.fromList
[ ( InvalidCredentials
, IntMap.fromList
[ ( 0x525, ADNoSuchObject )
, ( 0x52e, ADLogonFailure )
, ( 0x52f, ADAccountRestriction )
, ( 0x530, ADInvalidLogonHours )
, ( 0x531, ADInvalidWorkstation )
, ( 0x532, ADPasswordExpired )
, ( 0x533, ADAccountDisabled )
, ( 0x568, ADTooManyContextIds )
, ( 0x701, ADAccountExpired )
, ( 0x773, ADPasswordMustChange )
, ( 0x775, ADAccountLockedOut )
, ( 0x80090346, ADAccountLockedOut )
]
)
]
parseADError :: ResultCode -> Text -> Either ParseError ADError
parseADError resCode = parse (pADError resCode <* eof) "LDAP" . unpack
pADError :: ResultCode -> Parser ADError
pADError resCode = do
void . manyTill anyChar . try $ string ": "
let pItem = asum
[ do
void $ string "data "
fmap Just $ hexnum >>= hoistMaybe . fromADErrorCode resCode
, Nothing <$ manyTill anyChar (lookAhead . try $ void (string ", ") <|> eof)
]
(hoistMaybe =<<) $ ala First foldMap <$> pItem `sepBy1` try (string ", ")

View File

@ -784,7 +784,7 @@ tagAccessPredicate AuthAllocationTime = APDB $ \mAuthId route _ -> case route of
Nothing -> return Authorized
Just (cid, Allocation{..}) -> do
registered <- case mAuthId of
Just uid -> $cachedHereBinary (uid, cid) . existsBy $ UniqueParticipant uid cid
Just uid -> $cachedHereBinary (uid, cid) $ exists [ CourseParticipantUser ==. uid, CourseParticipantCourse ==. cid, CourseParticipantState ==. CourseParticipantActive ]
_ -> return False
if
| not registered

View File

@ -231,6 +231,8 @@ embedRenderMessage ''UniWorX ''AuthenticationMode id
embedRenderMessage ''UniWorX ''RatingValidityException id
embedRenderMessageVariant ''UniWorX ''ADInvalidCredentials ("InvalidCredentials" <>)
newtype ShortSex = ShortSex Sex
embedRenderMessageVariant ''UniWorX ''ShortSex ("Short" <>)

View File

@ -161,6 +161,7 @@ instance YesodAuth UniWorX where
app <- getYesod
let mr | Just lang <- mlang = renderMessage app . map (Text.intercalate "-") . reverse . inits $ Text.splitOn "-" lang
| otherwise = renderMessage app []
addMessage Success . toHtml $ mr Auth.NowLoggedIn
onErrorHtml dest msg = do

View File

@ -159,6 +159,7 @@ instance BearerAuthSite UniWorX => YesodBreadcrumbs UniWorX where
APriosR -> i18nCrumb MsgBreadcrumbAllocationPriorities . Just $ AllocationR tid ssh ash AUsersR
AComputeR -> i18nCrumb MsgBreadcrumbAllocationCompute . Just $ AllocationR tid ssh ash AUsersR
AAcceptR -> i18nCrumb MsgBreadcrumbAllocationAccept . Just $ AllocationR tid ssh ash AUsersR
AAddUserR -> i18nCrumb MsgBreadcrumbAllocationAddUser . Just $ AllocationR tid ssh ash AUsersR
breadcrumb ParticipantsListR = i18nCrumb MsgBreadcrumbParticipantsList $ Just CourseListR
breadcrumb (ParticipantsR _ _) = i18nCrumb MsgBreadcrumbParticipants $ Just ParticipantsListR
@ -1334,6 +1335,17 @@ pageActions (AllocationR tid ssh ash AUsersR) = return
}
, navChildren = []
}
, NavPageActionPrimary
{ navLink = NavLink
{ navLabel = MsgMenuAllocationAddUser
, navRoute = AllocationR tid ssh ash AAddUserR
, navAccess' = return True
, navType = NavTypeLink { navModal = False }
, navQuick' = mempty
, navForceActive = False
}
, navChildren = []
}
]
pageActions CourseListR = do
participantsSecondary <- pageQuickActions NavQuickViewPageActionSecondary ParticipantsListR

View File

@ -26,7 +26,7 @@ yesodMiddleware :: ( BearerAuthSite UniWorX
, BackendCompatible SqlBackend (YesodPersistBackend UniWorX)
)
=> HandlerFor UniWorX res -> HandlerFor UniWorX res
yesodMiddleware = storeBearerMiddleware . csrfMiddleware . dryRunMiddleware . observeYesodCacheSizeMiddleware . languagesMiddleware appLanguages . headerMessagesMiddleware . defaultYesodMiddleware . normalizeRouteMiddleware . updateFavouritesMiddleware
yesodMiddleware = storeBearerMiddleware . csrfMiddleware . dryRunMiddleware . observeYesodCacheSizeMiddleware . languagesMiddleware appLanguages . headerMessagesMiddleware . defaultYesodMiddleware . normalizeRouteMiddleware . updateFavouritesMiddleware . setActiveAuthTagsMiddleware
where
dryRunMiddleware :: HandlerFor UniWorX a -> HandlerFor UniWorX a
dryRunMiddleware handler = do
@ -98,6 +98,14 @@ yesodMiddleware = storeBearerMiddleware . csrfMiddleware . dryRunMiddleware . ob
Nothing -> return ()
handler
setActiveAuthTagsMiddleware :: HandlerFor UniWorX a -> HandlerFor UniWorX a
setActiveAuthTagsMiddleware handler = do
mtagActive <- lookupSessionJson SessionActiveAuthTags :: HandlerFor UniWorX (Maybe AuthTagActive)
when (is _Nothing mtagActive) $ do
mAuthTagActive <- lookupRegisteredCookieJson CookieActiveAuthTags
for_ mAuthTagActive $ setSessionJson SessionActiveAuthTags . review _ReducedActiveAuthTags
handler
updateFavourites :: forall m backend.
( MonadHandler m, HandlerSite m ~ UniWorX

View File

@ -8,6 +8,7 @@ import Handler.Allocation.Application as Handler.Allocation
import Handler.Allocation.Register as Handler.Allocation
import Handler.Allocation.List as Handler.Allocation
import Handler.Allocation.Users as Handler.Allocation
import Handler.Allocation.AddUser as Handler.Allocation
import Handler.Allocation.Prios as Handler.Allocation
import Handler.Allocation.Compute as Handler.Allocation
import Handler.Allocation.Accept as Handler.Allocation

View File

@ -11,6 +11,7 @@ import Handler.Utils.Allocation
import Data.Map ((!?))
import qualified Data.Map as Map
import qualified Data.Set as Set
import qualified Database.Esqueleto as E
import qualified Control.Monad.State.Class as State
@ -25,12 +26,13 @@ newtype SessionDataAllocationResults = SessionDataAllocationResults
)
( UTCTime
, AllocationFingerprint
, Set CourseId
, Set (UserId, CourseId)
, Seq MatchingLogRun
)
} deriving (Eq, Ord, Read, Show, Generic, Typeable)
deriving newtype (ToJSON, FromJSON)
deriving (Monoid, Semigroup) via Dual (Map (TermId, SchoolId, AllocationShorthand) (UTCTime, AllocationFingerprint, Set (UserId, CourseId), Seq MatchingLogRun))
deriving (Monoid, Semigroup) via Dual (Map (TermId, SchoolId, AllocationShorthand) (UTCTime, AllocationFingerprint, Set CourseId, Set (UserId, CourseId), Seq MatchingLogRun))
makeWrapped ''SessionDataAllocationResults
@ -47,11 +49,11 @@ instance Button UniWorX AllocationAcceptButton where
btnClasses BtnAllocationAccept = [BCIsButton, BCPrimary]
allocationAcceptForm :: AllocationId -> DB (Maybe (Form (UTCTime, AllocationFingerprint, Set (UserId, CourseId), Seq MatchingLogRun)))
allocationAcceptForm :: AllocationId -> DB (Maybe (Form (UTCTime, AllocationFingerprint, Set CourseId, Set (UserId, CourseId), Seq MatchingLogRun)))
allocationAcceptForm aId = runMaybeT $ do
Allocation{..} <- MaybeT $ get aId
SessionDataAllocationResults allocMap <- MaybeT $ lookupSessionJson SessionAllocationResults
allocRes@(allocTime, allocFp, allocMatching, _ :|> MatchingLogRun{..}) <- hoistMaybe $ allocMap !? (allocationTerm, allocationSchool, allocationShorthand)
allocRes@(allocTime, allocFp, eligibleCourses, allocMatching, _ :|> MatchingLogRun{..}) <- hoistMaybe $ allocMap !? (allocationTerm, allocationSchool, allocationShorthand)
allocationUsers <- fmap (map $ bimap E.unValue E.unValue) . lift . E.select . E.from $ \allocationUser -> do
E.where_ $ allocationUser E.^. AllocationUserAllocation E.==. E.val aId
@ -85,6 +87,7 @@ allocationAcceptForm aId = runMaybeT $ do
E.&&. courseParticipant E.^. CourseParticipantState E.==. E.val CourseParticipantActive
return (allocationCourse, course, participants)
let allocationCapacity = sumOf (folded . _2 . _entityVal . _courseCapacity . _Just) allocationCourses
allocCourses = setOf (folded . _1 . _entityVal . _allocationCourseCourse) allocationCourses
let courseAllocations = ofoldr (\(_uid, cid) -> Map.insertWith (+) cid 1) Map.empty allocMatching
allocationCourses' <- hoistMaybe $
@ -137,9 +140,9 @@ postAAcceptR tid ssh ash = do
formRes@((acceptRes, _), _) <- liftHandler $ runFormPost acceptForm
didStore <- formResultMaybe acceptRes $ \(now, allocFp, allocMatchings, allocLog) -> do
didStore <- formResultMaybe acceptRes $ \(now, allocFp, _, allocMatchings, allocLog) -> do
modifySessionJson SessionAllocationResults . fmap (assertM $ not . views _Wrapped onull) . over (mapped . _Wrapped :: Setter' (Maybe SessionDataAllocationResults) _) $
Map.filterWithKey (\(tid', ssh', ash') (_, allocFp', _, _) ->
Map.filterWithKey (\(tid', ssh', ash') (_, allocFp', _, _, _) ->
or [ tid' /= tid
, ssh' /= ssh
, ash' /= ash

View File

@ -0,0 +1,174 @@
module Handler.Allocation.AddUser
( getAAddUserR, postAAddUserR
) where
import Import
import Handler.Allocation.Application
import Handler.Utils
import qualified Data.Map as Map
import qualified Data.Conduit.Combinators as C
import qualified Database.Esqueleto as E
data AllocationAddUserForm = AllocationAddUserForm
{ aauUser :: UserId
, aauTotalCourses :: Natural
, aauPriority :: Maybe AllocationPriority
, aauApplications :: Map CourseId ApplicationForm
}
getAAddUserR, postAAddUserR :: TermId -> SchoolId -> AllocationShorthand -> Handler Html
getAAddUserR = postAAddUserR
postAAddUserR tid ssh ash = do
(Entity _ Allocation{..}, (addUserAct, addUserForm, addUserEnctype)) <- runDB $ do
alloc@(Entity aId _) <- getBy404 $ TermSchoolAllocationShort tid ssh ash
allocCourses <- E.select . E.from $ \(course `E.InnerJoin` allocationCourse) -> do
E.on $ allocationCourse E.^. AllocationCourseCourse E.==. course E.^. CourseId
E.where_ $ allocationCourse E.^. AllocationCourseAllocation E.==. E.val aId
return ( course
, E.exists . E.from $ \courseAppInstructionFile ->
E.where_ $ courseAppInstructionFile E.^. CourseAppInstructionFileCourse E.==. course E.^. CourseId
, allocationCourse
)
MsgRenderer mr <- getMsgRenderer
((addUserRes, addUserForm), addUserEnctype) <- liftHandler . runFormPost . renderAForm FormStandard $ AllocationAddUserForm
<$> areq (checkMap (first $ const MsgAllocationAddUserUserNotFound) Right $ userField False Nothing) (fslpI MsgAllocationAddUserUser (mr MsgAllocationAddUserUserPlaceholder)) Nothing
<*> areq (posIntFieldI MsgAllocationAddUserTotalCoursesLessThanOne) (fslI MsgAllocationAddUserTotalCourses) (Just 1)
<*> optionalActionA (allocationPriorityForm (fslI MsgAllocationAddUserPriority) Nothing) (fslI MsgAllocationAddUserSetPriority) (Just True)
<*> allocationApplicationsForm aId (Map.fromList [ (cId, (course, allocationCourse, hasTemplate)) | (Entity cId course, E.Value hasTemplate, Entity _ allocationCourse) <- allocCourses ]) (fslI MsgAllocationAddUserApplications) False
addUserAct <- formResultMaybe addUserRes $ \AllocationAddUserForm{..} -> Just <$> do
now <- liftIO getCurrentTime
didInsert <- is _Just <$> insertUnique AllocationUser
{ allocationUserAllocation = aId
, allocationUserUser = aauUser
, allocationUserTotalCourses = aauTotalCourses
, allocationUserPriority = aauPriority
}
if
| didInsert -> do
oldApps <- selectList [CourseApplicationUser ==. aauUser, CourseApplicationAllocation ==. Just aId] []
forM_ oldApps $ \(Entity appId CourseApplication{..}) -> do
delete appId
unless (courseApplicationCourse `Map.member` aauApplications) $
audit $ TransactionCourseApplicationDeleted courseApplicationCourse courseApplicationUser appId
iforM_ aauApplications $ \cId ApplicationForm{..} -> maybeT (return ()) $ do
prio <- hoistMaybe afPriority
let rated = afRatingVeto || is _Just afRatingPoints
appId <- lift $ insert CourseApplication
{ courseApplicationCourse = cId
, courseApplicationUser = aauUser
, courseApplicationText = afText
, courseApplicationRatingVeto = afRatingVeto
, courseApplicationRatingPoints = afRatingPoints
, courseApplicationRatingComment = afRatingComment
, courseApplicationAllocation = Just aId
, courseApplicationAllocationPriority = Just prio
, courseApplicationTime = now
, courseApplicationRatingTime = guardOn rated now
}
lift . runConduit $ transPipe liftHandler (sequence_ afFiles) .| C.mapM_ (insert_ . review _FileReference . (, CourseApplicationFileResidual appId))
lift . audit $ TransactionCourseApplicationEdit cId aauUser appId
return $ do
addMessageI Success MsgAllocationAddUserUserAdded
redirect $ AllocationR tid ssh ash AAddUserR
| otherwise -> return $ addMessageI Error MsgAllocationAddUserUserExists
return (alloc, (addUserAct, addUserForm, addUserEnctype))
sequence_ addUserAct
MsgRenderer mr <- getMsgRenderer
let title = MsgAllocationAddUserTitle (mr . ShortTermIdentifier $ unTermKey allocationTerm) (unSchoolKey allocationSchool) allocationName
shortTitle = MsgAllocationAddUserShortTitle allocationTerm allocationSchool allocationShorthand
siteLayoutMsg title $ do
setTitleI shortTitle
wrapForm addUserForm FormSettings
{ formMethod = POST
, formAction = Just . SomeRoute $ AllocationR tid ssh ash AAddUserR
, formEncoding = addUserEnctype
, formAttrs = []
, formSubmit = FormSubmit
, formAnchor = Nothing :: Maybe Text
}
allocationApplicationsForm :: AllocationId
-> Map CourseId (Course, AllocationCourse, Bool)
-> FieldSettings UniWorX
-> Bool
-> AForm Handler (Map CourseId ApplicationForm)
allocationApplicationsForm aId courses FieldSettings{..} fvRequired = formToAForm $ do
now <- liftIO getCurrentTime
let afmApplicant = True
afmApplicantEdit = True
afmLecturer = True
appsRes' <- iforM courses $ \cId (course, allocCourse, hasApplicationTemplate) -> over _2 (course, allocCourse, hasApplicationTemplate, ) <$> applicationForm (Just aId) cId Nothing ApplicationFormMode{..} Nothing
let appsRes = sequenceA $ view _1 <$> appsRes'
appsViews = view _2 <$> appsRes'
let fvInput =
[whamlet|
$newline never
<div .allocation__courses>
$forall (Course{courseTerm, courseSchool, courseShorthand, courseName, courseApplicationsInstructions}, AllocationCourse{allocationCourseAcceptSubstitutes}, hasApplicationTemplate, ApplicationFormView{afvPriority, afvForm}) <- Map.elems appsViews
<div .allocation-course>
<div .allocation-course__priority-label .allocation__label>
_{MsgAllocationPriority}
<div .allocation-course__priority>
$maybe prioView <- afvPriority
^{fvWidget prioView}
<a .allocation-course__name href=@{CourseR courseTerm courseSchool courseShorthand CShowR} target="_blank">
#{courseName}
<div .allocation-course__admin-info>
<p>
$maybe deadline <- allocationCourseAcceptSubstitutes
_{MsgCourseAllocationCourseAcceptsSubstitutesUntil}: #
^{formatTimeW SelFormatDateTime deadline}
$nothing
_{MsgCourseAllocationCourseAcceptsSubstitutesNever}
$if allocationCourseAcceptSubstitutes >= Just now
\ ^{iconOK}
$if hasApplicationTemplate || is _Just courseApplicationsInstructions
<div .allocation-course__instructions-label .allocation__label>
_{MsgCourseApplicationInstructionsApplication}
<div .allocation-course__instructions>
$maybe aInst <- courseApplicationsInstructions
<p>
#{aInst}
$if hasApplicationTemplate
<p>
<a href=@{CourseR courseTerm courseSchool courseShorthand CRegisterTemplateR}>
#{iconRegisterTemplate} _{MsgCourseApplicationTemplateApplication}
<div .allocation-course__application-label .interactive-fieldset__target .allocation__label uw-interactive-fieldset data-conditional-input=#{maybe "" fvId afvPriority} data-conditional-value="" data-conditional-negated>
_{MsgCourseApplication}
<div .allocation-course__application .interactive-fieldset__target uw-interactive-fieldset data-conditional-input=#{maybe "" fvId afvPriority} data-conditional-value="" data-conditional-negated>
^{renderFieldViews FormStandard afvForm}
|]
MsgRenderer mr <- getMsgRenderer
let fvLabel = toHtml $ mr fsLabel
fvTooltip = toHtml . mr <$> fsTooltip
fvErrors = case appsRes of
FormFailure errs -> Just
[shamlet|
$newline never
<ul>
$forall err <- errs
<li>#{err}
|]
_other -> Nothing
fvId <- maybe newIdent return fsId
return (appsRes, pure FieldView{..})

View File

@ -71,16 +71,17 @@ instance Exception ApplicationFormException
applicationForm :: Maybe AllocationId
-> CourseId
-> UserId
-> Maybe UserId
-> ApplicationFormMode -- ^ Which parts of the shared form to display
-> Html -> MForm Handler (FormResult ApplicationForm, ApplicationFormView)
applicationForm maId@(is _Just -> isAlloc) cid uid ApplicationFormMode{..} csrf = do
-> Maybe Html -- ^ If @Just@ also include action buttons for usage as standalone form
-> MForm Handler (FormResult ApplicationForm, ApplicationFormView)
applicationForm maId@(is _Just -> isAlloc) cid muid ApplicationFormMode{..} mcsrf = do
(mApp, coursesNum, Course{..}, maxPrio) <- liftHandler . runDB $ do
mApplication <- listToMaybe <$> selectList [CourseApplicationAllocation ==. maId, CourseApplicationUser ==. uid, CourseApplicationCourse ==. cid] [LimitTo 1]
mApplication <- fmap join . for muid $ \uid -> listToMaybe <$> selectList [CourseApplicationAllocation ==. maId, CourseApplicationUser ==. uid, CourseApplicationCourse ==. cid] [LimitTo 1]
coursesNum <- fromIntegral . fromMaybe 1 <$> for maId (\aId -> count [AllocationCourseAllocation ==. aId])
course <- getJust cid
(fromMaybe 0 -> maxPrio) <- fmap (E.unValue <=< listToMaybe) . E.select . E.from $ \courseApplication -> do
(fromMaybe 0 -> maxPrio) <- fmap join . for muid $ \uid -> fmap (E.unValue <=< listToMaybe) . E.select . E.from $ \courseApplication -> do
E.where_ $ courseApplication E.^. CourseApplicationUser E.==. E.val uid
E.&&. courseApplication E.^. CourseApplicationAllocation E.==. E.val maId
E.&&. E.not_ (E.isNothing $ courseApplication E.^. CourseApplicationAllocationPriority)
@ -202,7 +203,9 @@ applicationForm maId@(is _Just -> isAlloc) cid uid ApplicationFormMode{..} csrf
, guardOn ( afmApplicantEdit && is _Nothing mApp ) BtnAllocationApply
, guardOn ( afmApplicantEdit && is _Just mApp ) BtnAllocationApplicationRetract
]
(actionRes, buttonsView) <- buttonForm' buttons csrf
(actionRes, buttonsView) <- case mcsrf of
Just csrf -> buttonForm' buttons csrf
Nothing -> return (pure BtnAllocationApplicationEdit, mempty)
ratingSection <- if
| afmLecturer
@ -251,7 +254,7 @@ editApplicationR :: Maybe AllocationId
editApplicationR maId uid cid mAppId afMode allowAction postAction = do
Course{..} <- runDB $ get404 cid
((appRes, appView), appEnc) <- runFormPost $ applicationForm maId cid uid afMode
((appRes, appView), appEnc) <- runFormPost $ applicationForm maId cid (Just uid) afMode . Just
formResult appRes $ \ApplicationForm{..} -> do
if

View File

@ -13,6 +13,7 @@ import qualified Data.Set as Set
import qualified Data.Map as Map
import qualified Database.Esqueleto as E
import qualified Database.Esqueleto.Utils as E
import qualified Control.Monad.State.Class as State
@ -70,10 +71,28 @@ missingPriorities aId = wFormToAForm $ do
-> fmap (bool Set.empty $ Map.keysSet usersWithoutPrio) <$> wpreq missingPriosField (fslI MsgAllocationUsersMissingPriorities & setTooltip MsgAllocationUsersMissingPrioritiesTip) (Just False)
data AllocationCourseRestrictionMode
= AllocationCourseRestrictionDontRestrict
| AllocationCourseRestrictionSubstitutes
| AllocationCourseRestrictionCustom
deriving (Eq, Ord, Read, Show, Enum, Bounded, Generic, Typeable)
deriving anyclass (Universe, Finite)
nullaryPathPiece ''AllocationCourseRestrictionMode $ camelToPathPiece' 3
embedRenderMessage ''UniWorX ''AllocationCourseRestrictionMode id
restrictCourses :: (MonadHandler m, HandlerSite m ~ UniWorX) => AllocationId -> AForm m (Maybe (Set CourseId))
restrictCourses aId = hoistAForm liftHandler $
optionalActionA selectCourses (fslI MsgAllocationRestrictCourses & setTooltip MsgAllocationRestrictCoursesTip) (Just False)
restrictCourses aId = hoistAForm liftHandler $ multiActionA restrictOpts (fslI MsgAllocationRestrictCourses & setTooltip MsgAllocationRestrictCoursesTip) (Just AllocationCourseRestrictionDontRestrict)
where
restrictOpts = mapF $ \case
AllocationCourseRestrictionDontRestrict -> pure Nothing
AllocationCourseRestrictionSubstitutes -> wFormToAForm $ do
now <- liftIO getCurrentTime
allocCourses <- fmap (setOf $ folded . _Value) . liftHandler . runDB . E.select . E.from $ \allocationCourse -> do
E.where_ $ allocationCourse E.^. AllocationCourseAllocation E.==. E.val aId
E.where_ . E.maybe E.false (E.>=. E.val now) $ allocationCourse E.^. AllocationCourseAcceptSubstitutes
return $ allocationCourse E.^. AllocationCourseCourse
return . pure $ Just allocCourses
AllocationCourseRestrictionCustom -> Just <$> selectCourses
selectCourses = courseSelectForm query coursePred miButtonAction' miIdent' fSettings fRequired mPrev
where
query = E.from $ \(course `E.InnerJoin` allocationCourse) -> do
@ -115,9 +134,9 @@ postAComputeR tid ssh ash = do
formResult computeFormRes $ \AllocationComputeForm{..} -> do
now <- liftIO getCurrentTime
(allocFp, allocMatching, allocLog) <- computeAllocation aEnt acfRestrictCourses
(allocFp, eligibleCourses, allocMatching, allocLog) <- computeAllocation aEnt acfRestrictCourses
tellSessionJson SessionAllocationResults . SessionDataAllocationResults $
Map.singleton (tid, ssh, ash) (now, allocFp, allocMatching, allocLog)
Map.singleton (tid, ssh, ash) (now, allocFp, eligibleCourses, allocMatching, allocLog)
addMessageI Success MsgAllocationComputed
redirect $ AllocationR tid ssh ash AUsersR -- Redirect aborts transaction for safety

View File

@ -49,18 +49,22 @@ postAShowR tid ssh ash = do
ata <- getSessionActiveAuthTags
let
resultCourse :: Simple Field1 a (Entity Course) => Lens' a (Entity Course)
resultCourse :: _ => Lens' a (Entity Course)
resultCourse = _1
resultCourseApplication :: Simple Field2 a (Maybe (Entity CourseApplication)) => Traversal' a (Entity CourseApplication)
resultCourseApplication :: _ => Traversal' a (Entity CourseApplication)
resultCourseApplication = _2 . _Just
resultHasTemplate :: Simple Field3 a (E.Value Bool) => Lens' a Bool
resultHasTemplate :: _ => Lens' a Bool
resultHasTemplate = _3 . _Value
resultIsRegistered :: Simple Field4 a (E.Value Bool) => Lens' a Bool
resultIsRegistered :: _ => Lens' a Bool
resultIsRegistered = _4 . _Value
resultCourseVisible :: Simple Field5 a (E.Value Bool) => Lens' a Bool
resultCourseVisible :: _ => Lens' a Bool
resultCourseVisible = _5 . _Value
resultAllocationCourse :: _ => Lens' a AllocationCourse
resultAllocationCourse = _6 . _entityVal
resultParticipantCount :: _ => Lens' a Int
resultParticipantCount = _7 . _Value
(Entity aId Allocation{..}, School{..}, isAnyLecturer, courses, registration, wouldNotifyNewCourse) <- runDB $ do
(Entity aId Allocation{..}, School{..}, isAnyLecturer, isAdmin, courses, registration, wouldNotifyNewCourse) <- runDB $ do
alloc@(Entity aId Allocation{allocationSchool}) <- getBy404 $ TermSchoolAllocationShort tid ssh ash
school <- getJust allocationSchool
@ -79,15 +83,29 @@ postAShowR tid ssh ash = do
E.orderBy [E.asc $ course E.^. CourseName]
let hasTemplate = E.exists . E.from $ \courseAppInstructionFile ->
E.where_ $ courseAppInstructionFile E.^. CourseAppInstructionFileCourse E.==. course E.^. CourseId
return (course, courseApplication, hasTemplate, E.not_ . E.isNothing $ registration E.?. CourseParticipantId, courseIsVisible now course (Just (E.val aId)))
participantCount = E.subSelectCount . E.from $ \courseParticipant ->
E.where_ $ courseParticipant E.^. CourseParticipantCourse E.==. course E.^. CourseId
E.&&. courseParticipant E.^. CourseParticipantState E.==. E.val CourseParticipantActive
return ( course
, courseApplication
, hasTemplate
, E.not_ . E.isNothing $ registration E.?. CourseParticipantId
, courseIsVisible now course . Just $ E.val aId
, allocationCourse
, participantCount
)
registration <- fmap join . for muid $ getBy . UniqueAllocationUser aId
isAnyLecturer <- hasWriteAccessTo CourseNewR
isAdmin <- hasReadAccessTo $ AllocationR tid ssh ash AUsersR
wouldNotifyNewCourse <- fmap (maybe False E.unValue . join) . for muid $ E.selectMaybe . pure . allocationNotifyNewCourses (E.val aId) . E.val
return (alloc, school, isAnyLecturer, nubOn (view $ resultCourse . _entityKey) courses, registration, wouldNotifyNewCourse)
return (alloc, school, isAnyLecturer, isAdmin, nubOn (view $ resultCourse . _entityKey) courses, registration, wouldNotifyNewCourse)
let nextSubstitutesDeadline = minimumOf (folded . resultAllocationCourse . _allocationCourseAcceptSubstitutes . _Just . filtered (>= now)) courses
freeCapacity = fmap getSum . getAp . flip foldMap courses $ \cEntry -> Ap . fmap (Sum . max 0) $ subtract (cEntry ^. resultParticipantCount) <$> preview (resultCourse . _entityVal . _courseCapacity . _Just) cEntry
MsgRenderer mr <- getMsgRenderer
let title = MsgAllocationTitle (mr . ShortTermIdentifier $ unTermKey allocationTerm) (unSchoolKey allocationSchool) allocationName
@ -146,11 +164,13 @@ postAShowR tid ssh ash = do
mApp = cEntry ^? resultCourseApplication
isRegistered = cEntry ^. resultIsRegistered
courseVisible = cEntry ^. resultCourseVisible
AllocationCourse{..} = cEntry ^. resultAllocationCourse
partCount = cEntry ^. resultParticipantCount
cID <- encrypt cid :: WidgetFor UniWorX CryptoUUIDCourse
mayApply <- hasWriteAccessTo . AllocationR tid ssh ash $ AApplyR cID
mayEdit <- hasWriteAccessTo $ CourseR tid ssh courseShorthand CEditR
isLecturer <- hasWriteAccessTo $ CourseR courseTerm courseSchool courseShorthand CEditR
mApplyFormView <- liftHandler . for muid $ \uid -> generateFormPost . applicationForm (Just aId) cid uid $ ApplicationFormMode True mayApply isLecturer
mApplyFormView <- liftHandler . for muid $ \uid -> generateFormPost $ applicationForm (Just aId) cid (Just uid) (ApplicationFormMode True mayApply isLecturer) . Just
tRoute <- case mApp of
Nothing -> return . AllocationR tid ssh ash $ AApplyR cID
Just (Entity appId _) -> CApplicationR courseTerm courseSchool courseShorthand <$> encrypt appId <*> pure CAEditR

View File

@ -10,6 +10,7 @@ import Handler.Allocation.Accept
import Handler.Utils
import Handler.Utils.Allocation
import Handler.Utils.StudyFeatures
import qualified Database.Esqueleto as E
import qualified Database.Esqueleto.Utils as E
@ -22,6 +23,8 @@ import qualified Data.Set as Set
import Text.Blaze (toMarkup)
import qualified Data.Conduit.Combinators as C
type UserTableExpr = E.SqlExpr (Entity User)
`E.InnerJoin` E.SqlExpr (Entity AllocationUser)
@ -59,6 +62,7 @@ queryVetoedCourses = queryAllocationUser . to queryVetoedCourses'
type UserTableData = DBRow ( Entity User
, UserTableStudyFeatures
, Entity AllocationUser
, Int -- ^ Applied
, Int -- ^ Assigned
@ -68,13 +72,16 @@ type UserTableData = DBRow ( Entity User
resultUser :: Lens' UserTableData (Entity User)
resultUser = _dbrOutput . _1
resultStudyFeatures :: Lens' UserTableData UserTableStudyFeatures
resultStudyFeatures = _dbrOutput . _2
resultAllocationUser :: Lens' UserTableData (Entity AllocationUser)
resultAllocationUser = _dbrOutput . _2
resultAllocationUser = _dbrOutput . _3
resultAppliedCourses, resultAssignedCourses, resultVetoedCourses :: Lens' UserTableData Int
resultAppliedCourses = _dbrOutput . _3
resultAssignedCourses = _dbrOutput . _4
resultVetoedCourses = _dbrOutput . _5
resultAppliedCourses = _dbrOutput . _4
resultAssignedCourses = _dbrOutput . _5
resultVetoedCourses = _dbrOutput . _6
data AllocationUserTableCsv = AllocationUserTableCsv
@ -82,10 +89,12 @@ data AllocationUserTableCsv = AllocationUserTableCsv
, csvAUserFirstName :: Text
, csvAUserName :: Text
, csvAUserMatriculation :: Maybe Text
, csvAUserStudyFeatures :: UserTableStudyFeatures
, csvAUserRequested
, csvAUserApplied
, csvAUserVetos
, csvAUserAssigned :: Natural
, csvAUserNewAssigned :: Maybe Natural
, csvAUserPriority :: Maybe AllocationPriority
} deriving (Generic)
makeLenses_ ''AllocationUserTableCsv
@ -94,10 +103,22 @@ allocationUserTableCsvOptions :: Csv.Options
allocationUserTableCsvOptions = Csv.defaultOptions { Csv.fieldLabelModifier = camelToPathPiece' 3}
instance Csv.ToNamedRecord AllocationUserTableCsv where
toNamedRecord = Csv.genericToNamedRecord allocationUserTableCsvOptions
instance Csv.DefaultOrdered AllocationUserTableCsv where
headerOrder = Csv.genericHeaderOrder allocationUserTableCsvOptions
toNamedRecord AllocationUserTableCsv{..} = Csv.namedRecord $
[ "surname" Csv..= csvAUserSurname
, "first-name" Csv..= csvAUserFirstName
, "name" Csv..= csvAUserName
, "matriculation" Csv..= csvAUserMatriculation
, "study-features" Csv..= csvAUserStudyFeatures
, "requested" Csv..= csvAUserRequested
, "applied" Csv..= csvAUserApplied
, "vetos" Csv..= csvAUserVetos
, "assigned" Csv..= csvAUserAssigned
] ++
[ "new-assigned" Csv..= newAssigned
| newAssigned <- hoistMaybe csvAUserNewAssigned
] ++
[ "priority" Csv..= csvAUserPriority
]
instance CsvColumnsExplained AllocationUserTableCsv where
csvColumnsExplanations = genericCsvColumnsExplanations allocationUserTableCsvOptions $ mconcat
@ -105,13 +126,33 @@ instance CsvColumnsExplained AllocationUserTableCsv where
, singletonMap 'csvAUserFirstName MsgCsvColumnAllocationUserFirstName
, singletonMap 'csvAUserName MsgCsvColumnAllocationUserName
, singletonMap 'csvAUserMatriculation MsgCsvColumnAllocationUserMatriculation
, singletonMap 'csvAUserStudyFeatures MsgCsvColumnAllocationUserStudyFeatures
, singletonMap 'csvAUserRequested MsgCsvColumnAllocationUserRequested
, singletonMap 'csvAUserApplied MsgCsvColumnAllocationUserApplied
, singletonMap 'csvAUserVetos MsgCsvColumnAllocationUserVetos
, singletonMap 'csvAUserAssigned MsgCsvColumnAllocationUserAssigned
, singletonMap 'csvAUserNewAssigned MsgCsvColumnAllocationUserNewAssigned
, singletonMap 'csvAUserPriority MsgCsvColumnAllocationUserPriority
]
userTableCsvHeader :: Bool -> Csv.Header
userTableCsvHeader hasNewAssigned = Csv.header $
[ "surname"
, "first-name"
, "name"
, "matriculation"
, "study-features"
, "requested"
, "applied"
, "vetos"
, "assigned"
] ++
[ "new-assigned"
| hasNewAssigned
] ++
[ "priority"
]
getAUsersR, postAUsersR :: TermId -> SchoolId -> AllocationShorthand -> Handler Html
getAUsersR = postAUsersR
@ -121,7 +162,7 @@ postAUsersR tid ssh ash = do
resultsDone <- is _Just <$> allocationStarted aId
allocMatching <- runMaybeT $ do
SessionDataAllocationResults allocMap <- MaybeT $ lookupSessionJson SessionAllocationResults
allocMatching <- fmap (view _3) . hoistMaybe $ allocMap !? (tid, ssh, ash)
allocMatching <- fmap (view _4) . hoistMaybe $ allocMap !? (tid, ssh, ash)
return $ Map.fromListWith (<>) [ (uid, opoint cid) | (uid, cid) <- Set.toList allocMatching ] :: _ (Map UserId (NonNull (Set CourseId)))
csvName <- getMessageRender <*> pure (MsgAllocationUsersCsvName tid ssh ash)
@ -148,13 +189,15 @@ postAUsersR tid ssh ash = do
, assigned
, vetoed)
dbtRowKey = views queryAllocationUser (E.^. AllocationUserId)
dbtProj = runReaderT $ (asks . set _dbrOutput) <=< magnify _dbrOutput $
(,,,,)
<$> view _1 <*> view _2 <*> view (_3 . _Value) <*> view (_4 . _Value) <*> view (_5 . _Value)
dbtProj = runReaderT $ (asks . set _dbrOutput) <=< magnify _dbrOutput $ do
feats <- lift . allocationUserStudyFeatures aId =<< views _1 entityKey
(,,,,,)
<$> view _1 <*> pure feats <*> view _2 <*> view (_3 . _Value) <*> view (_4 . _Value) <*> view (_5 . _Value)
dbtColonnade :: Colonnade Sortable _ _
dbtColonnade = mconcat . catMaybes $
[ pure $ colUserDisplayName (resultUser . _entityVal . $(multifocusL 2) _userDisplayName _userSurname)
, pure $ colUserMatriculation (resultUser . _entityVal . _userMatrikelnummer)
, pure $ colStudyFeatures resultStudyFeatures
, pure $ colAllocationRequested (resultAllocationUser . _entityVal . _allocationUserTotalCourses)
, pure . coursesModalApplied $ colAllocationApplied resultAppliedCourses
, pure . coursesModalVetoed $ colAllocationVetoed resultVetoedCourses
@ -253,16 +296,26 @@ postAUsersR tid ssh ash = do
dbtParams = def
dbtIdent :: Text
dbtIdent = "allocation-users"
dbtCsvEncode = simpleCsvEncode csvName $ AllocationUserTableCsv
<$> view (resultUser . _entityVal . _userSurname)
<*> view (resultUser . _entityVal . _userFirstName)
<*> view (resultUser . _entityVal . _userDisplayName)
<*> view (resultUser . _entityVal . _userMatrikelnummer)
<*> view (resultAllocationUser . _entityVal . _allocationUserTotalCourses)
<*> view (resultAppliedCourses . to fromIntegral)
<*> view (resultVetoedCourses . to fromIntegral)
<*> view (resultAssignedCourses . to fromIntegral)
<*> view (resultAllocationUser . _entityVal . _allocationUserPriority)
dbtCsvEncode = return DBTCsvEncode
{ dbtCsvExportForm = pure ()
, dbtCsvDoEncode = \() -> C.mapM $ \(_, row) -> flip runReaderT row $
AllocationUserTableCsv
<$> view (resultUser . _entityVal . _userSurname)
<*> view (resultUser . _entityVal . _userFirstName)
<*> view (resultUser . _entityVal . _userDisplayName)
<*> view (resultUser . _entityVal . _userMatrikelnummer)
<*> view resultStudyFeatures
<*> view (resultAllocationUser . _entityVal . _allocationUserTotalCourses)
<*> view (resultAppliedCourses . to fromIntegral)
<*> view (resultVetoedCourses . to fromIntegral)
<*> view (resultAssignedCourses . to fromIntegral)
<*> views (resultUser . _entityKey) (\uid -> maybe 0 (fromIntegral . olength) . Map.lookup uid <$> allocMatching)
<*> view (resultAllocationUser . _entityVal . _allocationUserPriority)
, dbtCsvName = unpack csvName
, dbtCsvNoExportData = Just id
, dbtCsvHeader = \_ -> return . userTableCsvHeader $ is _Just allocMatching
, dbtCsvExampleData = Nothing
}
dbtCsvDecode = Nothing
allocationUsersDBTableValidator = def
& defaultSorting [SortAscBy "priority", SortAscBy "user-matriculation"]

View File

@ -57,9 +57,13 @@ data CourseForm = CourseForm
data AllocationCourseForm = AllocationCourseForm
{ acfAllocation :: AllocationId
, acfMinCapacity :: Int
, acfAcceptSubstitutes :: Maybe UTCTime
, acfDeregisterNoShow :: Bool
}
makeLenses_ ''CourseForm
makeLenses_ ''AllocationCourseForm
courseToForm :: Entity Course -> [Lecturer] -> Map UserEmail (InvitationDBData Lecturer) -> Maybe (Entity AllocationCourse) -> CourseForm
courseToForm cEnt@(Entity cid Course{..}) lecs lecInvites alloc = CourseForm
{ cfCourseId = Just cid
@ -98,6 +102,7 @@ allocationCourseToForm :: Entity Course -> Entity AllocationCourse -> Allocation
allocationCourseToForm (Entity _ Course{..}) (Entity _ AllocationCourse{..}) = AllocationCourseForm
{ acfAllocation = allocationCourseAllocation
, acfMinCapacity = allocationCourseMinCapacity
, acfAcceptSubstitutes = allocationCourseAcceptSubstitutes
, acfDeregisterNoShow = courseDeregisterNoShow
}
@ -265,6 +270,7 @@ makeCourseForm miButtonAction template = identifyForm FIDcourse . validateFormDB
in AllocationCourseForm
<$> ainp allocField (fslI MsgCourseAllocation) (fmap acfAllocation $ template >>= cfAllocation)
<*> ainp (natFieldI MsgCourseAllocationMinCapacityMustBeNonNegative) (fslI MsgCourseAllocationMinCapacity & setTooltip MsgCourseAllocationMinCapacityTip) (fmap acfMinCapacity $ template >>= cfAllocation)
<*> aopt utcTimeField (fslI MsgCourseAcceptSubstitutesUntil & setTooltip MsgCourseAcceptSubstitutesUntilTip) (fmap acfAcceptSubstitutes $ template >>= cfAllocation)
<*> apopt checkBoxField (fslI MsgCourseDeregisterNoShow & setTooltip MsgCourseDeregisterNoShowTip) ((<|> Just True) . fmap acfDeregisterNoShow $ template >>= cfAllocation)
optionalActionW' (bool mforcedJust mpopt mayChange) allocationForm' (fslI MsgCourseAllocationParticipate & setTooltip MsgCourseAllocationParticipateTip) (is _Just . cfAllocation <$> template)
@ -323,20 +329,28 @@ validateCourse = do
now <- liftIO getCurrentTime
uid <- liftHandler requireAuthId
userAdmin <- hasWriteAccessTo $ SchoolR cfSchool SchoolEditR
allocationTerm <- for (acfAllocation <$> cfAllocation) $ lift . fmap allocationTerm . getJust
newAllocationTerm <- for (acfAllocation <$> cfAllocation) $ lift . fmap allocationTerm . getJust
oldAllocatedCapacity <- fmap join . for cfCourseId $ \cid -> lift $ do
prevAllocationCourse <- getBy $ UniqueAllocationCourse cid
prevAllocation <- fmap join . traverse get $ allocationCourseAllocation . entityVal <$> prevAllocationCourse
prevAllocationCourse <- join <$> traverse (lift . getBy . UniqueAllocationCourse) cfCourseId
prevAllocation <- fmap join . traverse (lift . getEntity) $ allocationCourseAllocation . entityVal <$> prevAllocationCourse
fmap join . for prevAllocation $ \Allocation{allocationStaffAllocationTo, allocationRegisterByCourse} -> if
| userAdmin
-> return Nothing
| NTop allocationStaffAllocationTo <= NTop (Just now)
, NTop allocationRegisterByCourse > NTop (Just now)
-> Just . courseCapacity <$> getJust cid
| otherwise
-> return Nothing
oldAllocatedCapacity <- if
| Just (Entity _ Allocation{..}) <- prevAllocation
, Just (Entity _ AllocationCourse{..}) <- prevAllocationCourse
, NTop allocationStaffAllocationTo <= NTop (Just now)
, NTop allocationRegisterByCourse > NTop (Just now)
-> lift $ Just . courseCapacity <$> getJust allocationCourseCourse
| otherwise
-> return Nothing
let oldAllocation = do
Entity allocId Allocation{..} <- prevAllocation
guard $ NTop (Just now) > NTop allocationStaffRegisterTo
pure $ Just allocId
oldAllocatedMinCapacity = do
Entity _ Allocation{..} <- prevAllocation
Entity _ AllocationCourse{..} <- prevAllocationCourse
guard $ NTop (Just now) > NTop allocationStaffRegisterTo
pure $ Just allocationCourseMinCapacity
guardValidation MsgCourseVisibilityEndMustBeAfterStart
$ NTop cfVisFrom <= NTop cfVisTo
@ -344,15 +358,19 @@ validateCourse = do
$ NTop cfRegFrom <= NTop cfRegTo
guardValidation MsgCourseDeregistrationEndMustBeAfterStart
$ Just False /= ((<=) <$> cfRegFrom <*> cfDeRegUntil)
unless userAdmin $
guardValidation MsgCourseUserMustBeLecturer
$ anyOf (traverse . _Right . _1) (== uid) cfLecturers
guardValidation MsgCourseAllocationRequiresCapacity
$ is _Nothing cfAllocation || is _Just cfCapacity
guardValidation MsgCourseAllocationTermMustMatch
$ maybe True (== cfTerm) allocationTerm
guardValidation MsgCourseAllocationCapacityMayNotBeChanged
$ maybe True (== cfCapacity) oldAllocatedCapacity
$ maybe True (== cfTerm) newAllocationTerm
unless userAdmin $ do
guardValidation MsgCourseUserMustBeLecturer
$ anyOf (traverse . _Right . _1) (== uid) cfLecturers
guardValidation MsgCourseAllocationCapacityMayNotBeChanged
$ maybe True (== cfCapacity) oldAllocatedCapacity
guardValidation MsgAllocationStaffRegisterToExpiredAllocation
$ maybe True (== fmap acfAllocation cfAllocation) oldAllocation
guardValidation MsgAllocationStaffRegisterToExpiredMinCapacity
$ maybe True (== fmap acfMinCapacity cfAllocation) oldAllocatedMinCapacity
warnValidation MsgCourseShorthandTooLong
$ length (CI.original cfShort) <= 10
@ -564,46 +582,23 @@ courseEditHandler miButtonAction mbCourseForm = do
}
upsertAllocationCourse :: CourseId -> Maybe AllocationCourseForm -> YesodJobDB UniWorX ()
upsertAllocationCourse cid cfAllocation = do
now <- liftIO getCurrentTime
Course{} <- getJust cid
prevAllocationCourse <- getBy $ UniqueAllocationCourse cid
prevAllocation <- fmap join . traverse getEntity $ allocationCourseAllocation . entityVal <$> prevAllocationCourse
userAdmin <- fromMaybe False <$> for prevAllocation (\(Entity _ Allocation{..}) -> hasWriteAccessTo $ SchoolR allocationSchool SchoolEditR)
upsertAllocationCourse cid = \case
Just AllocationCourseForm{..} -> do
prevAllocationCourse <- getBy $ UniqueAllocationCourse cid
doEdit <- if
| userAdmin
-> return True
| Just (Entity _ Allocation{allocationStaffRegisterTo}) <- prevAllocation
, NTop allocationStaffRegisterTo <= NTop (Just now)
-> let anyChanges
| Just AllocationCourseForm{..} <- cfAllocation
, Just (Entity _ AllocationCourse{..}) <- prevAllocationCourse
= or [ acfAllocation /= allocationCourseAllocation
, acfMinCapacity /= allocationCourseMinCapacity
]
| otherwise
= True
in False <$ when anyChanges (addMessageI Error MsgAllocationStaffRegisterToExpired)
| otherwise
-> return True
void $ upsert AllocationCourse
{ allocationCourseAllocation = acfAllocation
, allocationCourseCourse = cid
, allocationCourseMinCapacity = acfMinCapacity
, allocationCourseAcceptSubstitutes = acfAcceptSubstitutes
}
[ AllocationCourseAllocation =. acfAllocation
, AllocationCourseCourse =. cid
, AllocationCourseMinCapacity =. acfMinCapacity
, AllocationCourseAcceptSubstitutes =. acfAcceptSubstitutes
]
when doEdit $
case cfAllocation of
Just AllocationCourseForm{..} -> do
void $ upsert AllocationCourse
{ allocationCourseAllocation = acfAllocation
, allocationCourseCourse = cid
, allocationCourseMinCapacity = acfMinCapacity
}
[ AllocationCourseAllocation =. acfAllocation
, AllocationCourseCourse =. cid
, AllocationCourseMinCapacity =. acfMinCapacity
]
when (Just acfAllocation /= fmap entityKey prevAllocation) $
queueDBJob . JobQueueNotification $ NotificationAllocationNewCourse acfAllocation cid
Nothing
| Just (Entity prevId _) <- prevAllocationCourse
-> delete prevId
_other -> return ()
when (Just acfAllocation /= fmap (allocationCourseAllocation . entityVal) prevAllocationCourse) $
queueDBJob . JobQueueNotification $ NotificationAllocationNewCourse acfAllocation cid
Nothing ->
deleteWhere [ AllocationCourseCourse ==. cid ]

View File

@ -49,6 +49,7 @@ postEEditR tid ssh csh examn = do
, examGradingMode = efGradingMode
, examDescription = efDescription
, examExamMode = efExamMode
, examStaff = efStaff
}
when (is _Nothing insertRes) $ do
@ -80,7 +81,6 @@ postEEditR tid ssh csh examn = do
, examOccurrenceDescription = eofDescription
}
pIds <- fmap catMaybes . forM (Set.toList efExamParts) $ traverse decrypt . epfId
deleteWhere [ ExamPartExam ==. eId, ExamPartId /<-. pIds ]
forM_ (Set.toList efExamParts) $ \case
@ -105,6 +105,8 @@ postEEditR tid ssh csh examn = do
, examPartWeight = epfWeight
}
deleteWhere [ ExamOfficeSchoolExam ==. eId ]
insertMany_ [ ExamOfficeSchool ssh' eId | ssh' <- Set.toList efOfficeSchools ]
let (invites, adds) = partitionEithers $ Set.toList efCorrectors

View File

@ -28,7 +28,6 @@ import Text.Blaze.Html.Renderer.String (renderHtml)
data ExamForm = ExamForm
{ efName :: ExamName
, efDescription :: Maybe Html
, efGradingMode :: ExamGradingMode
, efStart :: Maybe UTCTime
, efEnd :: Maybe UTCTime
, efVisibleFrom :: Maybe UTCTime
@ -43,6 +42,9 @@ data ExamForm = ExamForm
, efBonusRule :: Maybe ExamBonusRule
, efOccurrenceRule :: ExamOccurrenceRule
, efExamMode :: ExamMode
, efGradingMode :: ExamGradingMode
, efOfficeSchools :: Set SchoolId
, efStaff :: Maybe Text
, efCorrectors :: Set (Either UserEmail UserId)
, efExamParts :: Set ExamPartForm
}
@ -103,7 +105,6 @@ examForm template html = do
flip (renderAForm FormStandard) html $ ExamForm
<$> areq ciField (fslpI MsgExamName (mr MsgExamName) & setTooltip MsgExamNameTip) (efName <$> template)
<*> aopt htmlField (fslI MsgExamDescription) (efDescription <$> template)
<*> apopt (selectField optionsFinite) (fslI MsgExamGradingMode & setTooltip MsgExamGradingModeTip) (efGradingMode <$> template <|> Just ExamGradingMixed)
<* aformSection MsgExamFormTimes
<*> aopt utcTimeField (fslpI MsgExamStart (mr MsgDate) & setTooltip MsgExamTimeTip) (efStart <$> template)
<*> aopt utcTimeField (fslpI MsgExamEnd (mr MsgDate) & setTooltip MsgExamTimeTip) (efEnd <$> template)
@ -122,11 +123,39 @@ examForm template html = do
<*> examOccurrenceRuleForm (efOccurrenceRule <$> template)
<* aformSection MsgExamFormMode
<*> examModeForm (efExamMode <$> template)
<* aformSection MsgExamFormGrades
<*> apopt (selectField optionsFinite) (fslI MsgExamGradingMode & setTooltip MsgExamGradingModeTip) (efGradingMode <$> template <|> Just ExamGradingMixed)
<*> officeSchoolsForm (efOfficeSchools <$> template)
<*> apreq' (textField & cfStrip) (fslpI MsgExamStaff (mr MsgExamStaff) & setTooltip MsgExamStaffTip) (efStaff <$> template)
<* aformSection MsgExamFormCorrection
<*> examCorrectorsForm (efCorrectors <$> template)
<* aformSection MsgExamFormParts
<*> examPartsForm (efExamParts <$> template)
officeSchoolsForm :: Maybe (Set SchoolId) -> AForm Handler (Set SchoolId)
officeSchoolsForm mPrev = wFormToAForm $ do
currentRoute <- fromMaybe (error "officeSchoolsForm called from 404-handler") <$> getCurrentRoute
let
miButtonAction' :: forall p. PathPiece p => p -> Maybe (SomeRoute UniWorX)
miButtonAction' frag = Just . SomeRoute $ currentRoute :#: frag
miAdd' :: (Text -> Text) -> FieldView UniWorX -> Form ([SchoolId] -> FormResult [SchoolId])
miAdd' nudge submitView csrf = do
(schoolRes, addView) <- mpopt schoolField ("" & addName (nudge "school")) Nothing
let schoolRes' = schoolRes <&> \newDat oldDat -> FormSuccess (guardOn (newDat `notElem` oldDat) newDat)
return (schoolRes', $(widgetFile "exam/schoolMassInput/add"))
miCell' :: SchoolId -> Widget
miCell' ssh = do
School{..} <- liftHandler . runDB $ getJust ssh
$(widgetFile "exam/schoolMassInput/cell")
miLayout' :: MassInputLayout ListLength SchoolId ()
miLayout' lLength _ cellWdgts delButtons addWdgts = $(widgetFile "exam/schoolMassInput/layout")
fmap Set.fromList <$> massInputAccumW miAdd' miCell' miButtonAction' miLayout' ("exam-schools" :: Text) (fslI MsgExamExamOfficeSchools & setTooltip MsgExamExamOfficeSchoolsTip) False (Set.toList <$> mPrev)
examCorrectorsForm :: Maybe (Set (Either UserEmail UserId)) -> AForm Handler (Set (Either UserEmail UserId))
examCorrectorsForm mPrev = wFormToAForm $ do
MsgRenderer mr <- getMsgRenderer
@ -261,6 +290,7 @@ examFormTemplate (Entity eId Exam{..}) = do
occurrences <- selectList [ ExamOccurrenceExam ==. eId ] []
correctors <- selectList [ ExamCorrectorExam ==. eId ] []
invitations <- Map.keysSet <$> sourceInvitationsF @ExamCorrector eId
extraSchools <- selectList [ ExamOfficeSchoolExam ==. eId ] []
examParts' <- forM examParts $ \(Entity pid part) -> (,) <$> encrypt pid <*> pure part
occurrences' <- forM occurrences $ \(Entity oid occ) -> (,) <$> encrypt oid <*> pure occ
@ -308,13 +338,15 @@ examFormTemplate (Entity eId Exam{..}) = do
return examCorrectorUser
]
, efExamMode = examExamMode
, efOfficeSchools = Set.fromList $ examOfficeSchoolSchool . entityVal <$> extraSchools
, efStaff = examStaff
}
examTemplate :: CourseId -> DB (Maybe ExamForm)
examTemplate cid = runMaybeT $ do
newCourse <- MaybeT $ get cid
[(Entity _ oldCourse, Entity _ oldExam)] <- lift . E.select . E.from $ \(course `E.InnerJoin` exam) -> do
[(Entity _ oldCourse, Entity oldExamId oldExam)] <- lift . E.select . E.from $ \(course `E.InnerJoin` exam) -> do
E.on $ course E.^. CourseId E.==. exam E.^. ExamCourse
E.where_ $ ( course E.^. CourseShorthand E.==. E.val (courseShorthand newCourse)
E.||. course E.^. CourseName E.==. E.val (courseName newCourse)
@ -327,6 +359,8 @@ examTemplate cid = runMaybeT $ do
E.limit 1
E.orderBy [ E.desc $ course E.^. CourseTerm, E.asc $ exam E.^. ExamVisibleFrom ]
return (course, exam)
extraSchools <- lift $ selectList [ ExamOfficeSchoolExam ==. oldExamId ] []
oldTerm <- MaybeT . get $ courseTerm oldCourse
newTerm <- MaybeT . get $ courseTerm newCourse
@ -354,6 +388,8 @@ examTemplate cid = runMaybeT $ do
, efExamParts = Set.empty
, efCorrectors = Set.empty
, efExamMode = examExamMode oldExam
, efStaff = examStaff oldExam
, efOfficeSchools = Set.fromList $ examOfficeSchoolSchool . entityVal <$> extraSchools
}
@ -431,3 +467,6 @@ validateExam cId oldExam = do
]
warnValidation MsgExamModeSchoolDiscouraged . not $ evalExamModeDNF schoolExamDiscouragedModes efExamMode
unless (has (_Just . _examStaff . _Nothing) oldExam) $
guardValidation MsgExamStaffRequired $ isn't _Nothing efStaff

View File

@ -50,6 +50,7 @@ postCExamNewR tid ssh csh = do
, examPublicStatistics = efPublicStatistics
, examDescription = efDescription
, examExamMode = efExamMode
, examStaff = efStaff
}
whenIsJust insertRes $ \examid -> do
insertMany_
@ -74,6 +75,8 @@ postCExamNewR tid ssh csh = do
examOccurrenceDescription = eofDescription
]
insertMany_ [ ExamOfficeSchool ssh' examid | ssh' <- Set.toList efOfficeSchools ]
let (invites, adds) = partitionEithers $ Set.toList efCorrectors
insertMany_ [ ExamCorrector{..}
| let examCorrectorExam = examid

View File

@ -26,7 +26,7 @@ getEShowR tid ssh csh examn = do
cTime <- liftIO getCurrentTime
mUid <- maybeAuthId
(Entity eId Exam{..}, School{..}, examParts, examVisible, (gradingVisible, gradingShown), (occurrenceAssignmentsVisible, occurrenceAssignmentsShown), results, result, bonus, occurrences, (registered, mayRegister), registeredCount, lecturerInfoShown) <- runDB $ do
(Entity eId Exam{..}, School{..}, examParts, examVisible, (gradingVisible, gradingShown), (occurrenceAssignmentsVisible, occurrenceAssignmentsShown), results, result, bonus, occurrences, (registered, mayRegister), registeredCount, lecturerInfoShown, staffInfoShown, extraSchools) <- runDB $ do
exam@(Entity eId Exam{..}) <- fetchExam tid ssh csh examn
school <- getJust examCourse >>= belongsToJust courseSchool
@ -83,7 +83,14 @@ getEShowR tid ssh csh examn = do
lecturerInfoShown <- hasReadAccessTo $ CExamR tid ssh csh examn EEditR
return (exam, school, examParts, examVisible, (gradingVisible, gradingShown), (occurrenceAssignmentsVisible, occurrenceAssignmentsShown), results, result, bonus, occurrences, (registered, mayRegister), registeredCount, lecturerInfoShown)
staffInfoShown <- hasReadAccessTo $ CExamR tid ssh csh examn EGradesR
extraSchools <- E.select . E.from $ \(school' `E.InnerJoin` examOfficeSchool) -> do
E.on $ school' E.^. SchoolId E.==. examOfficeSchool E.^. ExamOfficeSchoolSchool
E.where_ $ examOfficeSchool E.^. ExamOfficeSchoolExam E.==. E.val eId
return school'
return (exam, school, examParts, examVisible, (gradingVisible, gradingShown), (occurrenceAssignmentsVisible, occurrenceAssignmentsShown), results, result, bonus, occurrences, (registered, mayRegister), registeredCount, lecturerInfoShown, staffInfoShown, extraSchools)
let occurrenceNamesShown = lecturerInfoShown
partNumbersShown = lecturerInfoShown

View File

@ -14,6 +14,8 @@ import qualified Database.Esqueleto.Utils as E
import Development.GitRev
import Auth.LDAP (ADError(..), ADInvalidCredentials(..))
-- | Versionsgeschichte
getVersionR :: Handler TypedContent
getVersionR = selectRep $ do
@ -37,7 +39,7 @@ getLegalR =
-- | Allgemeine Informationen
getInfoR :: Handler Html
getInfoR = do
changelogEntries' <- runDB $ selectList [] []
changelogEntries' <- runDB $ selectList [ ChangelogItemFirstSeenItem <-. universeF ] []
let changelogEntries = Map.fromListWith Set.union
[ (Down changelogItemFirstSeenFirstSeen, Set.singleton changelogItemFirstSeenItem)
| Entity _ ChangelogItemFirstSeen{..} <- changelogEntries'
@ -181,6 +183,26 @@ showFAQ (CExamR tid ssh csh examn _) FAQExamPoints
E.&&. course E.^. CourseSchool E.==. E.val ssh
E.&&. course E.^. CourseShorthand E.==. E.val csh
E.&&. exam E.^. ExamName E.==. E.val examn
showFAQ _ FAQInvalidCredentialsAdAccountDisabled = maybeT (return False) $ do
guardM $ is _Nothing <$> maybeAuthId
sessionError <- MaybeT $ lookupSessionJson SessionError
guard $ sessionError == PermissionDenied (toPathPiece $ ADInvalidCredentials ADAccountDisabled)
return True
showFAQ _ FAQAllocationNoPlaces = maybeT (return False) $ do
uid <- MaybeT maybeAuthId
now <- liftIO getCurrentTime
liftHandler . runDB . E.selectExists . E.from $ \allocation -> do
let doneSince = E.subSelectMaybe . E.from $ \participant -> do
E.where_ $ participant E.^. CourseParticipantAllocated E.==. E.just (allocation E.^. AllocationId)
return . E.max_ $ participant E.^. CourseParticipantRegistration
isAllocationUser = E.exists . E.from $ \allocationUser ->
E.where_ $ allocationUser E.^. AllocationUserAllocation E.==. allocation E.^. AllocationId
E.&&. allocationUser E.^. AllocationUserUser E.==. E.val uid
isApplicant = E.exists . E.from $ \courseApplication ->
E.where_ $ courseApplication E.^. CourseApplicationAllocation E.==. E.just (allocation E.^. AllocationId)
E.&&. courseApplication E.^. CourseApplicationUser E.==. E.val uid
E.where_ $ isAllocationUser E.||. isApplicant
E.where_ $ E.maybe E.false (\done -> done E.>=. E.val (addUTCTime (-7 * nominalDay) now)) doneSince
showFAQ _ _ = return False
prioFAQ :: Monad m
@ -191,3 +213,5 @@ prioFAQ _ FAQForgottenPassword = return 1
prioFAQ _ FAQNotLecturerHowToCreateCourses = return 1
prioFAQ _ FAQCourseCorrectorsTutors = return 1
prioFAQ _ FAQExamPoints = return 2
prioFAQ _ FAQAllocationNoPlaces = return 2
prioFAQ _ FAQInvalidCredentialsAdAccountDisabled = return 3

View File

@ -817,8 +817,9 @@ postAuthPredsR = do
| authTag `Set.member` blacklist = aforced checkBoxField (fslI authTag) (authTagIsActive def authTag)
| otherwise = fromMaybe False <$> aopt checkBoxField (fslI authTag) (Just . Just $ authTagCurrentActive authTag)
((authActiveRes, authActiveWidget), authActiveEnctype) <- runFormPost . renderAForm FormStandard
$ AuthTagActive <$> funcForm taForm (fslI MsgActiveAuthTags) True
((authActiveRes, authActiveWidget), authActiveEnctype) <- runFormPost . renderAForm FormStandard $ (,)
<$> apopt checkBoxField (fslI MsgActiveAuthTagsSaveCookie & setTooltip MsgActiveAuthTagsSaveCookieTip) (Just False)
<*> fmap AuthTagActive (funcForm taForm (fslI MsgActiveAuthTags) True)
mReferer <- runMaybeT $ do
param <- MaybeT (lookupGetParam $ toPathPiece GetReferer) <|> MaybeT (lookupPostParam $ toPathPiece GetReferer)
@ -837,7 +838,10 @@ postAuthPredsR = do
^{authActiveWidget}
|]
formResult authActiveRes $ \authTagActive -> do
formResult authActiveRes $ \(saveCookie, authTagActive) -> do
when saveCookie $ if
| authTagActive == def -> deleteRegisteredCookie CookieActiveAuthTags
| otherwise -> setRegisteredCookieJson CookieActiveAuthTags $ authTagActive ^. _ReducedActiveAuthTags
setSessionJson SessionActiveAuthTags authTagActive
modifySessionJson SessionInactiveAuthTags . fmap $ Set.filter (not . authTagIsActive authTagActive)
addMessageI Success MsgAuthPredsActiveChanged

View File

@ -107,6 +107,7 @@ sinkAllocationPriorities allocId = fmap getSum . C.foldMapM . ifoldMapM $ \matr
computeAllocation :: Entity Allocation
-> Maybe (Set CourseId) -- ^ Optionally restrict allocation to only consider the given courses
-> DB ( AllocationFingerprint
, Set CourseId
, Set (UserId, CourseId)
, Seq MatchingLogRun
)
@ -162,6 +163,7 @@ computeAllocation (Entity allocId Allocation{allocationMatchingSeed}) cRestr = d
, allocationCourse E.^. AllocationCourseMinCapacity E.-. participants
)
let capacities = Map.filter (maybe True (> 0)) . Map.fromList $ (view (_1 . _entityVal . _allocationCourseCourse) &&& view (_2 . _Value)) <$> courses'
eligibleCourses = setOf (folded . _1 . _entityVal . _allocationCourseCourse) courses'
applications' <- selectList [ CourseApplicationAllocation ==. Just allocId ] []
excludedMatchings <- flip execStateT mempty . forM_ applications' $ \(Entity _ CourseApplication{..}) -> do
@ -254,7 +256,7 @@ computeAllocation (Entity allocId Allocation{allocationMatchingSeed}) cRestr = d
| not $ null belowMin -> allocationLoop $ cs <> Set.fromList belowMin
| otherwise -> return allocs
return . (\(ms, mLog) -> (fingerprint, ms, mLog)) $!! runWriter (allocationLoop Set.empty)
return . (\(ms, mLog) -> (fingerprint, eligibleCourses, ms, mLog)) $!! runWriter (allocationLoop Set.empty)
doAllocation :: AllocationId

View File

@ -34,7 +34,7 @@ resultIsSynced authId examResult = (hasSchool E.&&. allSchools) E.||. (E.not_ ha
examOfficeExamResultAuth :: E.SqlExpr (E.Value UserId) -- ^ office
-> E.SqlExpr (Entity ExamResult)
-> E.SqlExpr (E.Value Bool)
examOfficeExamResultAuth authId examResult = authByUser E.||. authByField E.||. authBySchool
examOfficeExamResultAuth authId examResult = authByUser E.||. authByField E.||. authBySchool E.||. authByExtraSchool
where
cId = E.subSelectForeign examResult ExamResultExam (\exam -> E.subSelectForeign exam ExamCourse (E.^. CourseId))
@ -67,3 +67,9 @@ examOfficeExamResultAuth authId examResult = authByUser E.||. authByField E.||.
E.on $ course E.^. CourseSchool E.==. userFunction E.^. UserFunctionSchool
E.&&. userFunction E.^. UserFunctionFunction E.==. E.val SchoolExamOffice
E.where_ $ userFunction E.^. UserFunctionUser E.==. authId
authByExtraSchool = E.exists . E.from $ \(userFunction `E.InnerJoin` examSchool) -> do
E.on $ userFunction E.^. UserFunctionFunction E.==. E.val SchoolExamOffice
E.&&. userFunction E.^. UserFunctionSchool E.==. examSchool E.^. ExamOfficeSchoolSchool
E.where_ $ examSchool E.^. ExamOfficeSchoolExam E.==. examResult E.^. ExamResultExam
E.where_ $ userFunction E.^. UserFunctionUser E.==. authId

View File

@ -44,6 +44,8 @@ import qualified Data.Set as Set
import Data.Map ((!), (!?))
import qualified Data.Map as Map
import qualified Data.Vector as Vector
import qualified Data.HashMap.Lazy as HashMap
import Control.Monad.Writer.Class
@ -488,7 +490,7 @@ termsAllowedField = selectField $ do
optionsPersistKey termFilter [Desc TermStart] termName
termField :: Field Handler TermId
termField = selectField $ optionsPersistKey [] [Asc TermName] termName
termField = selectField $ optionsPersistKey [] [Desc TermStart] termName
termsSetField :: [TermId] -> Field Handler TermId
termsSetField tids = selectField $ optionsPersistKey [TermName <-. (unTermKey <$> tids)] [Desc TermStart] termName
@ -1608,6 +1610,96 @@ multiUserField onlySuggested suggestions = Field{..}
)
Nothing -> E.true
userField :: forall m.
( MonadHandler m
, HandlerSite m ~ UniWorX
)
=> Bool -- ^ Only resolve suggested users?
-> Maybe (E.SqlQuery (E.SqlExpr (Entity User))) -- ^ Suggested users
-> Field m (Either UserEmail UserId)
userField onlySuggested suggestions = Field{..}
where
lookupExpr
| onlySuggested = suggestions
| otherwise = Just $ E.from return
fieldEnctype = UrlEncoded
fieldView theId name attrs val isReq = do
val' <- case val of
Left t -> return t
Right v -> case v of
Right uid -> case lookupExpr of
Nothing -> return mempty
Just lookupExpr' -> do
dbRes <- liftHandler . runDB . E.select $ do
user <- lookupExpr'
E.where_ $ user E.^. UserId E.==. E.val uid
return $ user E.^. UserEmail
case dbRes of
[E.Value email] -> return $ CI.original email
_other -> return mempty
Left email -> return $ CI.original email
datalistId <- maybe (return $ error "Not to be used") (const newIdent) suggestions
[whamlet|
$newline never
<input id="#{theId}" name="#{name}" *{attrs} type="email" :isReq:required="" value="#{val'}" :isJust suggestions:list=#{datalistId}>
|]
whenIsJust suggestions $ \suggestions' -> do
suggestedEmails <- fmap (Map.assocs . Map.fromListWith min . map (over _2 E.unValue . over _1 E.unValue)) . liftHandler . runDB . E.select $ do
user <- suggestions'
return ( E.case_
[ E.when_ (unique UserDisplayEmail user)
E.then_ (user E.^. UserDisplayEmail)
, E.when_ (unique UserEmail user)
E.then_ (user E.^. UserEmail)
]
( E.else_ $ user E.^. UserIdent)
, user E.^. UserDisplayName
)
[whamlet|
$newline never
<datalist id=#{datalistId}>
$forall (email, dName) <- suggestedEmails
<option value=#{email}>
#{email} (#{dName})
|]
fieldParse (filter (not . Text.null) -> t : _) _ = runExceptT . fmap Just $ do
email <- either (\errStr -> throwE . SomeMessage $ MsgInvalidEmail [st|#{t} (#{errStr})|]) (return . CI.mk . decodeUtf8 . Email.toByteString) $ Email.validate (encodeUtf8 t)
case lookupExpr of
Nothing -> return $ Left email
Just lookupExpr' -> do
dbRes <- fmap (setOf $ folded . _Value). liftHandler . runDB . E.select $ do
user <- lookupExpr'
E.where_ $ user E.^. UserIdent `E.ciEq` E.val email
E.||. ( user E.^. UserDisplayEmail `E.ciEq` E.val email
E.&&. unique UserDisplayEmail user
)
E.||. ( user E.^. UserEmail `E.ciEq` E.val email
E.&&. unique UserEmail user
)
return $ user E.^. UserId
if | Set.null dbRes
-> return $ Left email
| [uid] <- Set.toList dbRes
-> return $ Right uid
| otherwise
-> throwE $ SomeMessage MsgAmbiguousEmail
fieldParse _ _ = return $ Right Nothing
unique field user = case lookupExpr of
Just lookupExpr' -> E.not_ . E.exists $ do
user' <- lookupExpr'
E.where_ $ user' E.^. UserId E.!=. user E.^. UserId
E.&&. ( user' E.^. UserIdent `E.ciEq` user E.^. field
E.||. user' E.^. UserEmail `E.ciEq` user E.^. field
E.||. user' E.^. UserDisplayEmail `E.ciEq` user E.^. field
)
Nothing -> E.true
examResultField :: forall m res.
( MonadHandler m
, HandlerSite m ~ UniWorX
@ -2032,3 +2124,30 @@ examModeForm mPrev = examMode
examRequiredEquipmentToEither (ExamRequiredEquipmentCustom c) = Left c
examRequiredEquipmentFromEither (Right p) = ExamRequiredEquipmentPreset p
examRequiredEquipmentFromEither (Left c) = ExamRequiredEquipmentCustom c
data AllocationPriority' = AllocationPriorityNumeric' | AllocationPriorityOrdinal'
deriving (Eq, Ord, Read, Show, Enum, Bounded, Generic, Typeable)
deriving anyclass (Universe, Finite)
nullaryPathPiece ''AllocationPriority' $ camelToPathPiece' 2 . dropSuffix "'"
embedRenderMessage ''UniWorX ''AllocationPriority' id
classifyAllocationPriority :: AllocationPriority -> AllocationPriority'
classifyAllocationPriority = \case
AllocationPriorityNumeric{} -> AllocationPriorityNumeric'
AllocationPriorityOrdinal{} -> AllocationPriorityOrdinal'
allocationPriorityForm :: FieldSettings UniWorX
-> Maybe AllocationPriority
-> AForm Handler AllocationPriority
allocationPriorityForm fs mPrev = multiActionA opts fs $ classifyAllocationPriority <$> mPrev
where
opts = flip Map.fromSet (Set.fromList universeF) $ \case
AllocationPriorityNumeric' -> AllocationPriorityNumeric <$> apreq (checkMap toInts fromInts textField) (fslI MsgAllocationPriorityNumericValues & setTooltip MsgAllocationPriorityNumericValuesTip) (mPrev ^? _Just . _AllocationPriorityNumeric)
AllocationPriorityOrdinal' -> AllocationPriorityOrdinal <$> apreq (natFieldI MsgAllocationPriorityOrdinalValueNegative) (fslI MsgAllocationPriorityOrdinalValue & setTooltip MsgAllocationPriorityOrdinalValueTip) (mPrev ^? _Just . _AllocationPriorityOrdinal)
toInts t = fmap Vector.fromList . runExcept $ do
let ts = filter (not . Text.null) . map Text.strip $ Text.splitOn "," t
whenExceptT (null ts) MsgAllocationPriorityNumericNoValues
forM ts $ \t' -> maybeExceptT (MsgAllocationPriorityNumericNoParse t') . return $ readMay t'
fromInts = Text.intercalate ", " . map tshow . Vector.toList

View File

@ -9,6 +9,7 @@ module Handler.Utils.StudyFeatures
, isCourseStudyFeature, courseUserStudyFeatures
, isExternalExamStudyFeature, externalExamUserStudyFeatures
, isTermStudyFeature
, isAllocationStudyFeature, allocationUserStudyFeatures
) where
import Import.NoFoundation
@ -184,3 +185,24 @@ externalExamUserStudyFeatures eeId uid = do
isTermStudyFeature :: E.SqlExpr (Entity Term) -> E.SqlExpr (Entity StudyFeatures) -> E.SqlExpr (E.Value Bool)
isTermStudyFeature = isRelevantStudyFeatureCached TermId
isAllocationStudyFeature :: E.SqlExpr (Entity Allocation) -> E.SqlExpr (Entity StudyFeatures) -> E.SqlExpr (E.Value Bool)
isAllocationStudyFeature = isRelevantStudyFeatureCached AllocationTerm
allocationUserStudyFeatures :: MonadIO m => AllocationId -> UserId -> SqlPersistT m UserTableStudyFeatures
allocationUserStudyFeatures aId uid = do
feats <- E.select . E.from $ \(allocation `E.InnerJoin` studyFeatures `E.InnerJoin` terms `E.InnerJoin` degree) -> do
E.on $ degree E.^. StudyDegreeId E.==. studyFeatures E.^. StudyFeaturesDegree
E.on $ terms E.^. StudyTermsId E.==. studyFeatures E.^. StudyFeaturesField
E.on $ isAllocationStudyFeature allocation studyFeatures
E.where_ $ allocation E.^. AllocationId E.==. E.val aId
E.where_ $ studyFeatures E.^. StudyFeaturesUser E.==. E.val uid
return (terms, degree, studyFeatures)
return . UserTableStudyFeatures . Set.fromList . flip map feats $
\(Entity _ StudyTerms{..}, Entity _ StudyDegree{..}, Entity _ StudyFeatures{..}) -> UserTableStudyFeature
{ userTableField = fromMaybe (tshow studyTermsKey) studyTermsName
, userTableDegree = fromMaybe (tshow studyDegreeKey) studyDegreeName
, userTableSemester = studyFeaturesSemester
, userTableFieldType = studyFeaturesType
}

View File

@ -175,6 +175,7 @@ import Data.Word.Word24.Instances as Import ()
import Control.Monad.Trans.Memo.StateCache.Instances as Import (hoistStateCache)
import Database.Persist.Sql.Types.Instances as Import ()
import Control.Monad.Catch.Instances as Import ()
import Ldap.Client.Instances as Import ()
import Crypto.Hash as Import (Digest, SHA3_256, SHA3_512)
import Crypto.Random as Import (ChaChaDRG, Seed)

View File

@ -138,16 +138,14 @@ dispatchNotificationAllocationUnratedApplications (otoList -> nAllocations) jRec
dispatchNotificationAllocationResults :: AllocationId -> UserId -> Handler ()
dispatchNotificationAllocationResults nAllocation jRecipient = userMailT jRecipient $ do
(Allocation{..}, lecturerResults, participantResults) <- liftHandler . runDB $ do
(Allocation{..}, lecturerResults, warnSubstituteCourses, participantResults) <- liftHandler . runDB $ do
allocation <- getJust nAllocation
lecturerResults' <- E.select . E.from $ \(lecturer `E.InnerJoin` course) -> do
lecturerResults' <- E.select . E.from $ \(lecturer `E.InnerJoin` course `E.InnerJoin` allocationCourse) -> do
E.on $ allocationCourse E.^. AllocationCourseCourse E.==. course E.^. CourseId
E.on $ lecturer E.^. LecturerCourse E.==. course E.^. CourseId
E.where_ $ lecturer E.^. LecturerUser E.==. E.val jRecipient
E.&&. E.exists (E.from $ \allocationCourse ->
E.where_ $ allocationCourse E.^. AllocationCourseCourse E.==. course E.^. CourseId
E.&&. allocationCourse E.^. AllocationCourseAllocation E.==. E.val nAllocation
)
E.&&. allocationCourse E.^. AllocationCourseAllocation E.==. E.val nAllocation
let allocatedCount :: E.SqlExpr (E.Value Int64)
allocatedCount = E.subSelectCount . E.from $ \participant ->
E.where_ $ participant E.^. CourseParticipantCourse E.==. lecturer E.^. LecturerCourse
@ -157,11 +155,12 @@ dispatchNotificationAllocationResults nAllocation jRecipient = userMailT jRecipi
participantCount = E.subSelectCount . E.from $ \participant ->
E.where_ $ participant E.^. CourseParticipantCourse E.==. lecturer E.^. LecturerCourse
E.&&. participant E.^. CourseParticipantState E.==. E.val CourseParticipantActive
return (course, allocatedCount, participantCount)
let lecturerResults = flip map lecturerResults' $ \(Entity _ Course{..}, E.Value allocCount, E.Value partCount) -> SomeMessage $ if
return (course, allocationCourse, allocatedCount, participantCount)
let lecturerResults = flip map lecturerResults' $ \(Entity _ Course{..}, _, E.Value allocCount, E.Value partCount) -> SomeMessage $ if
| allocCount == partCount -> MsgAllocationResultLecturerAll courseShorthand allocCount
| allocCount == 0 -> MsgAllocationResultLecturerNone courseShorthand
| otherwise -> MsgAllocationResultLecturer courseShorthand allocCount partCount
warnSubstituteCourses = flip mapMaybe lecturerResults' $ \(Entity _ course, Entity _ AllocationCourse{..}, _, _) -> guardOn (isn't _Just allocationCourseAcceptSubstitutes) course
doParticipantResults <- E.selectExists . E.from $ \application ->
E.where_ $ application E.^. CourseApplicationAllocation E.==. E.just (E.val nAllocation)
@ -177,7 +176,7 @@ dispatchNotificationAllocationResults nAllocation jRecipient = userMailT jRecipi
| otherwise -> Nothing
cs -> Just $ map (courseShorthand . entityVal) cs
return (allocation, lecturerResults, participantResults)
return (allocation, lecturerResults, warnSubstituteCourses, participantResults)
replaceMailHeader "Auto-Submitted" $ Just "auto-generated"
setSubjectI $ MsgMailSubjectAllocationResults allocationName

View File

@ -0,0 +1,11 @@
{-# OPTIONS_GHC -fno-warn-orphans #-}
module Ldap.Client.Instances
(
) where
import ClassyPrelude
import Ldap.Client
deriving instance Ord ResultCode

View File

@ -110,6 +110,9 @@ migrateAll = do
$logDebugS "Migration" "Persistent automatic migration"
mapM_ ($logInfoS "Migration") =<< runMigrationSilent migrateAll'
$logDebugS "Migration" "Migrations marked as always safe"
mapM_ ($logInfoS "Migration") =<< runMigrationSilent migrateAlwaysSafe
requiresMigration :: forall m.
( MonadLogger m
, MonadResource m
@ -131,6 +134,8 @@ requiresMigration = mapReaderT (exceptT return return) $ do
$logInfoS "Migration" $ intercalate "; " automatic
throwError True
-- Does not consider `migrateAlwaysSafe`
return False
initialMigration :: Migration
@ -172,19 +177,6 @@ migrateManual = do
, ("user_ldap_primary_key", "CREATE INDEX user_ldap_primary_key ON \"user\" (ldap_primary_key)" )
, ("file_content_entry_chunk_hash", "CREATE INDEX file_content_entry_chunk_hash ON \"file_content_entry\" (chunk_hash)" )
]
recordedChangelogItems <- lift . lift $ selectList [] []
let missingChangelogItems = Set.toList $ Set.fromList universeF `Set.difference` recordedChangelogItems'
where recordedChangelogItems' = Set.fromList [ changelogItemFirstSeenItem | Entity _ ChangelogItemFirstSeen{..} <- recordedChangelogItems ]
unless (null missingChangelogItems) $ do
now <- iso8601Show . localDay . TZ.utcToLocalTimeTZ appTZ <$> liftIO getCurrentTime
addMigration False $
let sql = [st|INSERT INTO changelog_item_first_seen (item, first_seen) VALUES #{vals}|]
vals = Text.intercalate ", " $ do
item <- missingChangelogItems
return [st|('#{toPathPiece item}', '#{now}')|]
in sql
where
addIndex :: Text -> Sql -> Migration
addIndex ixName ixDef = do
@ -194,6 +186,21 @@ migrateManual = do
_other -> return True
unless alreadyDefined $ addMigration False ixDef
migrateAlwaysSafe :: Migration
-- | Part of `migrateAll` but not checked in `requiresMigration`
migrateAlwaysSafe = do
recordedChangelogItems <- lift . lift $ selectList [ ChangelogItemFirstSeenItem <-. universeF ] []
let missingChangelogItems = Set.toList $ Set.fromList universeF `Set.difference` recordedChangelogItems'
where recordedChangelogItems' = Set.fromList [ changelogItemFirstSeenItem | Entity _ ChangelogItemFirstSeen{..} <- recordedChangelogItems ]
unless (null missingChangelogItems) $ do
today <- localDay . TZ.utcToLocalTimeTZ appTZ <$> liftIO getCurrentTime
addMigration False $ do
let sql = [st|INSERT INTO changelog_item_first_seen (item, first_seen) VALUES #{vals}|]
vals = Text.intercalate ", " $ do
item <- missingChangelogItems
let itemDay = Map.findWithDefault today item changelogItemDays
return [st|('#{toPathPiece item}', '#{iso8601Show itemDay}')|]
in sql
{-
Confusion about quotes, from the PostgreSQL Manual:
@ -979,13 +986,7 @@ customMigrations = Map.fromListWith (>>)
|]
)
, ( AppliedMigrationKey [migrationVersion|42.0.0|] [version|43.0.0|]
, unlessM (tableExists "changelog_item_first_seen") $ do
[executeQQ|
CREATE TABLE "changelog_item_first_seen" (PRIMARY KEY ("item"), "item" VARCHAR NOT NULL, "first_seen" DATE NOT NULL);
|]
insertMany_ [ ChangelogItemFirstSeen{..}
| (changelogItemFirstSeenItem, changelogItemFirstSeenFirstSeen) <- Map.toList changelogItemDays
]
, return () -- Unused; used to create and fill `ChangelogItemFirstSeen`
)
]

View File

@ -29,15 +29,16 @@ makePrisms ''ChangelogItemKind
classifyChangelogItem :: ChangelogItem -> ChangelogItemKind
classifyChangelogItem = \case
ChangelogHaskellCampusLogin -> ChangelogItemBugfix
ChangelogTooltipsWithoutJavascript -> ChangelogItemBugfix
ChangelogButtonsWorkWithoutJavascript -> ChangelogItemBugfix
ChangelogTableFormsWorkAfterAjax -> ChangelogItemBugfix
ChangelogPassingByPointsWorks -> ChangelogItemBugfix
ChangelogErrorMessagesForTableItemVanish -> ChangelogItemBugfix
ChangelogExamAchievementParticipantDuplication -> ChangelogItemBugfix
ChangelogFormsTimesReset -> ChangelogItemBugfix
_other -> ChangelogItemFeature
ChangelogHaskellCampusLogin -> ChangelogItemBugfix
ChangelogTooltipsWithoutJavascript -> ChangelogItemBugfix
ChangelogButtonsWorkWithoutJavascript -> ChangelogItemBugfix
ChangelogTableFormsWorkAfterAjax -> ChangelogItemBugfix
ChangelogPassingByPointsWorks -> ChangelogItemBugfix
ChangelogErrorMessagesForTableItemVanish -> ChangelogItemBugfix
ChangelogExamAchievementParticipantDuplication -> ChangelogItemBugfix
ChangelogFormsTimesReset -> ChangelogItemBugfix
ChangelogAllocationCourseAcceptSubstitutesFixed -> ChangelogItemBugfix
_other -> ChangelogItemFeature
changelogItemDays :: Map ChangelogItem Day
changelogItemDays = Map.fromListWithKey (\k d1 d2 -> bool (error $ "Duplicate changelog days for " <> show k) d1 $ d1 /= d2)
@ -142,4 +143,6 @@ changelogItemDays = Map.fromListWithKey (\k d1 d2 -> bool (error $ "Duplicate ch
, (ChangelogCourseVisibility, [day|2020-08-10|])
, (ChangelogPersonalisedSheetFiles, [day|2020-08-10|])
, (ChangelogAbolishCourseAssociatedStudyFeatures, [day|2020-08-28|])
, (ChangelogExamStaff, [day|2020-10-12|])
, (ChangelogExamAdditionalSchools, [day|2020-10-12|])
]

View File

@ -108,12 +108,15 @@ instance Default AuthTagActive where
_ -> True
instance ToJSON AuthTagActive where
toJSON v = toJSON . HashMap.fromList $ map (id &&& authTagIsActive v) universeF
toJSON v = toJSON . HashMap.fromList $ map (toPathPiece &&& authTagIsActive v) universeF
instance FromJSON AuthTagActive where
parseJSON = Aeson.withObject "AuthTagActive" $ \o -> do
o' <- parseJSON $ Aeson.Object o :: Aeson.Parser (HashMap AuthTag Bool)
return . AuthTagActive $ \n -> fromMaybe (authTagIsActive def n) $ HashMap.lookup n o'
o' <- parseJSON $ Aeson.Object o :: Aeson.Parser (HashMap Text Bool)
fmap toAuthTagActive . flip ifoldMapM o' $ \k v -> maybeT mempty $ do
k' <- hoistMaybe $ fromPathPiece k
return $ HashMap.singleton k' v
where toAuthTagActive o = AuthTagActive $ \n -> fromMaybe (authTagIsActive def n) $ HashMap.lookup n o
instance Hashable AuthTagActive where
hashWithSalt s = foldl' hashWithSalt s . authTagIsActive
@ -127,6 +130,27 @@ derivePersistFieldJSON ''AuthTagActive
getSessionActiveAuthTags :: MonadHandler m => m AuthTagActive
getSessionActiveAuthTags = fromMaybe def <$> lookupSessionJson SessionActiveAuthTags
newtype ReducedActiveAuthTags = ReducedActiveAuthTags (HashMap AuthTag Bool)
deriving newtype (Monoid, Semigroup)
instance ToJSON ReducedActiveAuthTags where
toJSON (ReducedActiveAuthTags a) = toJSON $ HashMap.fromList [ (toPathPiece k, v) | (k, v) <- HashMap.toList a ]
instance FromJSON ReducedActiveAuthTags where
parseJSON = Aeson.withObject "ReducedActiveAuthTags" $ \o -> do
o' <- parseJSON $ Aeson.Object o :: Aeson.Parser (HashMap Text Bool)
fmap ReducedActiveAuthTags . flip ifoldMap o' $ \k v -> maybeT mempty $ do
k' <- hoistMaybe $ fromPathPiece k
return $ HashMap.singleton k' v
_ReducedActiveAuthTags :: Iso' AuthTagActive ReducedActiveAuthTags
_ReducedActiveAuthTags = iso toReducedActiveAuthTags fromReducedActiveAuthTags
where
toReducedActiveAuthTags a = ReducedActiveAuthTags . flip foldMap universeF $ \n -> if
| authTagIsActive a n /= authTagIsActive def n -> HashMap.singleton n $ authTagIsActive a n
| otherwise -> mempty
fromReducedActiveAuthTags (ReducedActiveAuthTags hm) = AuthTagActive $ \n -> fromMaybe (authTagIsActive def n) $ HashMap.lookup n hm
data PredLiteral a = PLVariable { plVar :: a } | PLNegated { plVar :: a }
deriving (Eq, Ord, Read, Show, Generic, Typeable)

View File

@ -31,7 +31,7 @@ import Data.Char (isAscii)
import Data.Monoid (Last(..))
data RegisteredCookie = CookieSession | CookieXSRFToken | CookieLang | CookieSystemMessageState
data RegisteredCookie = CookieSession | CookieXSRFToken | CookieLang | CookieSystemMessageState | CookieActiveAuthTags
deriving (Eq, Ord, Enum, Bounded, Read, Show, Generic, Typeable)
deriving anyclass (Universe, Finite, Hashable)

View File

@ -1261,8 +1261,7 @@ mpreq :: (RenderMessage site (ValueRequired site), HandlerSite m ~ site, MonadHa
-- Otherwise acts exactly like `mopt`.
mpreq f fs@FieldSettings{..} mx = do
mr <- getMessageRender
(res, fv) <- mopt f fs (Just <$> mx)
let fv' = fv { fvRequired = True }
(res, fv') <- mpreq' f fs $ Just <$> mx
return $ case res of
FormSuccess (Just res')
-> (FormSuccess res', fv')
@ -1293,6 +1292,25 @@ wpreq :: (RenderMessage site (ValueRequired site), HandlerSite m ~ site, MonadHa
wpreq f fs mx = mFormToWForm $ mpreq f fs mx
mpreq' :: (HandlerSite m ~ site, MonadHandler m)
=> Field m a -> FieldSettings site -> Maybe (Maybe a) -> MForm m (FormResult (Maybe a), FieldView site)
-- ^ Pseudo required
--
-- `FieldView` has `fvRequired` set to `True`.
-- Otherwise acts exactly like `mopt`.
mpreq' f fs mx = do
(res, fv) <- mopt f fs mx
return (res, fv { fvRequired = True })
apreq' :: (HandlerSite m ~ site, MonadHandler m)
=> Field m a -> FieldSettings site -> Maybe (Maybe a) -> AForm m (Maybe a)
apreq' f fs mx = formToAForm $ over _2 pure <$> mpreq' f fs mx
wpreq' :: (HandlerSite m ~ site, MonadHandler m)
=> Field m a -> FieldSettings site -> Maybe (Maybe a) -> WForm m (FormResult (Maybe a))
wpreq' f fs mx = mFormToWForm $ mpreq' f fs mx
mpopt :: (RenderMessage site (ValueRequired site), HandlerSite m ~ site, MonadHandler m)
=> Field m a -> FieldSettings site -> Maybe a -> MForm m (FormResult a, FieldView site)
-- ^ Pseudo optional

View File

@ -233,6 +233,8 @@ makeLenses_ ''Rating'
makeLenses_ ''FallbackPersonalisedSheetFilesKey
makePrisms ''AllocationPriority
-- makeClassy_ ''Load
--------------------------

View File

@ -305,6 +305,7 @@ observeFavouritesQuickActionsDuration act = do
data LoginOutcome
= LoginSuccessful
| LoginInvalidCredentials
| LoginADInvalidCredentials
| LoginError
deriving (Eq, Ord, Read, Show, Enum, Bounded, Generic, Typeable)
deriving anyclass (Universe, Finite)

View File

@ -30,7 +30,7 @@ extra-deps:
- serversession
- serversession-backend-acid-state
- git: git@gitlab2.rz.ifi.lmu.de:uni2work/xss-sanitize.git
commit: 074ed7c8810aca81f60f2c535f9e7bad67e9d95a
commit: dc928c3a456074b8777603bea20e81937321777f
- git: git@gitlab2.rz.ifi.lmu.de:uni2work/colonnade.git
commit: f8170266ab25b533576e96715bedffc5aa4f19fa
subdirs:
@ -39,7 +39,7 @@ extra-deps:
commit: 42103ab247057c04c8ce7a83d9d4c160713a3df1
- git: git@gitlab2.rz.ifi.lmu.de:uni2work/cryptoids.git
commit: 4d91394475b144ea5bf7ba111f93756cc0de8a3f
commit: 130b0dcbf2b09ccdf387b50262f1efbbbf1819e3
subdirs:
- cryptoids-class
- cryptoids-types

View File

@ -101,12 +101,12 @@ packages:
version: 0.3.6
git: git@gitlab2.rz.ifi.lmu.de:uni2work/xss-sanitize.git
pantry-tree:
size: 691
sha256: 7cada516aa3cad4adc214f5eb90dd07c3a8ecabdc5551f761366fc270ae2e086
commit: 074ed7c8810aca81f60f2c535f9e7bad67e9d95a
size: 750
sha256: f567a1c834448daaa164f2029fad164e6c8df2d4c92b51f811bae19cc0c95975
commit: dc928c3a456074b8777603bea20e81937321777f
original:
git: git@gitlab2.rz.ifi.lmu.de:uni2work/xss-sanitize.git
commit: 074ed7c8810aca81f60f2c535f9e7bad67e9d95a
commit: dc928c3a456074b8777603bea20e81937321777f
- completed:
subdir: colonnade
name: colonnade
@ -137,65 +137,65 @@ packages:
version: 0.0.0
git: git@gitlab2.rz.ifi.lmu.de:uni2work/cryptoids.git
pantry-tree:
size: 350
sha256: f014c9ff9666a4d4bab82dd2b3092fd2004b40ebf2bcd32cf7d90035e08ce75b
commit: 4d91394475b144ea5bf7ba111f93756cc0de8a3f
size: 412
sha256: 30466648d273ffb1d580b7961188d67a0bedb3703d6d5f8cca3c15a45295f203
commit: 130b0dcbf2b09ccdf387b50262f1efbbbf1819e3
original:
subdir: cryptoids-class
git: git@gitlab2.rz.ifi.lmu.de:uni2work/cryptoids.git
commit: 4d91394475b144ea5bf7ba111f93756cc0de8a3f
commit: 130b0dcbf2b09ccdf387b50262f1efbbbf1819e3
- completed:
subdir: cryptoids-types
name: cryptoids-types
version: 1.0.0
git: git@gitlab2.rz.ifi.lmu.de:uni2work/cryptoids.git
pantry-tree:
size: 258
sha256: d1465d25a1a1807d5a88d9a09085fd4a2f49f2e57b8398496691ffad30e8f88c
commit: 4d91394475b144ea5bf7ba111f93756cc0de8a3f
size: 320
sha256: 824ac5c55c2ad553bd401bb5a99731bbdccc828ecc5d71f174e9375c4e03c46e
commit: 130b0dcbf2b09ccdf387b50262f1efbbbf1819e3
original:
subdir: cryptoids-types
git: git@gitlab2.rz.ifi.lmu.de:uni2work/cryptoids.git
commit: 4d91394475b144ea5bf7ba111f93756cc0de8a3f
commit: 130b0dcbf2b09ccdf387b50262f1efbbbf1819e3
- completed:
subdir: cryptoids
name: cryptoids
version: 0.5.1.0
git: git@gitlab2.rz.ifi.lmu.de:uni2work/cryptoids.git
pantry-tree:
size: 510
sha256: 7c16ce6b5de6988ba628027a055fe7faa8b3a2e2bc77d7088e8dad23e9bac7a1
commit: 4d91394475b144ea5bf7ba111f93756cc0de8a3f
size: 566
sha256: b1f49dde76ff7e78b76e7f2f3b3f76c55e5e61555d1df5415ad3b6eb80dda2cb
commit: 130b0dcbf2b09ccdf387b50262f1efbbbf1819e3
original:
subdir: cryptoids
git: git@gitlab2.rz.ifi.lmu.de:uni2work/cryptoids.git
commit: 4d91394475b144ea5bf7ba111f93756cc0de8a3f
commit: 130b0dcbf2b09ccdf387b50262f1efbbbf1819e3
- completed:
subdir: filepath-crypto
name: filepath-crypto
version: 0.1.0.0
git: git@gitlab2.rz.ifi.lmu.de:uni2work/cryptoids.git
pantry-tree:
size: 614
sha256: 2f5d7053ba61d8727b2a0b4443017e9af013196d2d53064c98f21bbd196ccd52
commit: 4d91394475b144ea5bf7ba111f93756cc0de8a3f
size: 676
sha256: 9c31a2ffb2b1c86f9ba34eb83529c7a5a7dc68a49f89813c9b553427474654d9
commit: 130b0dcbf2b09ccdf387b50262f1efbbbf1819e3
original:
subdir: filepath-crypto
git: git@gitlab2.rz.ifi.lmu.de:uni2work/cryptoids.git
commit: 4d91394475b144ea5bf7ba111f93756cc0de8a3f
commit: 130b0dcbf2b09ccdf387b50262f1efbbbf1819e3
- completed:
subdir: uuid-crypto
name: uuid-crypto
version: 1.4.0.0
git: git@gitlab2.rz.ifi.lmu.de:uni2work/cryptoids.git
pantry-tree:
size: 359
sha256: 1861593e0b304b8a09db3e7b435ae6763f57d2051a1c8770a051adc5aa0f0edd
commit: 4d91394475b144ea5bf7ba111f93756cc0de8a3f
size: 417
sha256: 852e59807df1f2cf4b5a3748c46fa149d15a78651c93addfe5fc31d2d94c47f4
commit: 130b0dcbf2b09ccdf387b50262f1efbbbf1819e3
original:
subdir: uuid-crypto
git: git@gitlab2.rz.ifi.lmu.de:uni2work/cryptoids.git
commit: 4d91394475b144ea5bf7ba111f93756cc0de8a3f
commit: 130b0dcbf2b09ccdf387b50262f1efbbbf1819e3
- completed:
subdir: gearhash
name: gearhash

View File

@ -47,6 +47,9 @@ $newline never
_{MsgSchool}
<th .table__th>
_{MsgCourse}
$if eligibleCourses /= allocCourses
<th .table__th>
_{MsgAllocationCourseEligible}
<th .table__th>
_{MsgCourseCapacity}
<th .table__th>
@ -72,6 +75,9 @@ $newline never
<div .table__td-content>
<a href=@{CourseR courseTerm courseSchool courseShorthand CShowR}>
#{courseName}
$if eligibleCourses /= allocCourses
<td .table__td>
#{hasTickmark $ Set.member cid eligibleCourses}
<td .table__td>
<div .table__td-content>
$maybe capN <- courseCapacity

View File

@ -53,6 +53,25 @@ $newline never
^{iconTooltip (i18n MsgAllocationRegisterByStaffFromTip) Nothing True}
<dd .deflist__dd>
^{formatTimeRangeW SelFormatDateTime fromT allocationRegisterByStaffTo}
$if isAdmin
<dt .deflist__dt>
_{MsgAllocationNextSubstitutesDeadline} #
^{iconInvisible}
<dd .deflist__dd>
$maybe deadline <- nextSubstitutesDeadline
^{formatTimeW SelFormatDateTime deadline}
$nothing
_{MsgAllocationNextSubstitutesDeadlineNever}
<dt .deflist__dt>
_{MsgAllocationFreeCapacity} #
^{iconInvisible}
<dd .deflist__dd>
$maybe freeCap <- freeCapacity
#{freeCap}
$if freeCap <= 0
\ ^{iconOK}
$nothing
$maybe fromT <- allocationRegisterByCourse
<dt .deflist__dt>
_{MsgAllocationRegisterByCourseFrom}

View File

@ -14,6 +14,25 @@ $if is _Just muid
#{courseName}
$if not courseVisible && mayEdit
\ #{iconInvisible}
$if isAdmin
<div .allocation-course__admin-info>
<p>
$maybe deadline <- allocationCourseAcceptSubstitutes
_{MsgCourseAllocationCourseAcceptsSubstitutesUntil}: #
^{formatTimeW SelFormatDateTime deadline}
$nothing
_{MsgCourseAllocationCourseAcceptsSubstitutesNever}
$if allocationCourseAcceptSubstitutes >= Just now
\ ^{iconOK}
<p>
_{MsgCourseAllocationCourseParticipants}:
$maybe capacity <- courseCapacity
\ _{MsgCourseMembersCountLimited partCount capacity}
$if partCount < capacity
\ ^{iconProblem}
$nothing
\ _{MsgCourseMembersCount partCount}
\ ^{iconProblem}
$if hasApplicationTemplate || is _Just courseApplicationsInstructions
<div .allocation-course__instructions-label .allocation__label>
_{MsgCourseApplicationInstructionsApplication}

View File

@ -136,6 +136,7 @@ $# $if NTop (Just 0) < NTop (courseCapacity course)
<p>
$maybe visFrom <- courseVisibleFrom
^{formatTimeRangeW SelFormatDateTime visFrom courseVisibleTo}
<br />
$if NTop (Just now) < NTop courseVisibleFrom
$if hasAllocationRegistrationOpen
_{MsgCourseInvisibleOverridenByAllocation}

View File

@ -94,6 +94,16 @@ $maybe desc <- examDescription
$maybe closed <- examClosed
<dt .deflist__dt>_{MsgExamClosed} ^{isVisible False}
<dd .deflist__dd>^{formatTimeW SelFormatDateTime closed}
$maybe staff <- examStaff
$if staffInfoShown
<dd .deflist__dt>_{MsgExamStaff} ^{isVisible False}
<dt .deflist__dd>#{staff}
$if staffInfoShown && not (onull extraSchools)
<dd .deflist__dt>_{MsgExamExamOfficeSchools} ^{isVisible False}
<dt .deflist__dd>
<ul>
$forall Entity _ School{schoolName} <- extraSchools
<li>#{schoolName}
$if gradingShown
$maybe gradingRule <- examGradingRule
<dt .deflist__dt>

View File

@ -0,0 +1,6 @@
$newline never
<td>
#{csrf}
^{fvWidget addView}
<td>
^{fvWidget submitView}

View File

@ -0,0 +1,3 @@
$newline never
<td>
#{schoolName}

View File

@ -0,0 +1,11 @@
$newline never
<table>
<tbody>
$forall coord <- review liveCoords lLength
<tr .massinput__cell>
^{cellWdgts ! coord}
<td>
^{fvWidget (delButtons ! coord)}
<tfoot>
<tr .massinput__cell.massinput__cell--add>
^{addWdgts ! (0, 0)}

View File

@ -0,0 +1,2 @@
$newline never
Das Eintragen von Fristen bis zu denen Nachrücker aus Zentralanmeldungen akzeptiert werden ist nun möglich

View File

@ -0,0 +1,2 @@
$newline never
It is now possible to specify deadlines up to which substitute registrations from central allocations are accepted

View File

@ -0,0 +1,2 @@
$newline never
Kurse, die an Zentralanmeldungen teilnehmen, können nun angeben bis zu welcher Frist sie Nachrücker akzeptieren können

View File

@ -0,0 +1,2 @@
$newline never
Courses which participate in a central allocation may now specify a deadline up to which they are able to accept substitute registrations

View File

@ -0,0 +1,2 @@
$newline never
Aktive Authorisierungsprädikate können nun in einem persistenten Cookie gespeichert werden

View File

@ -0,0 +1,2 @@
$newline never
Active authorisation predicates can now be saved as a persistent cookie

View File

@ -0,0 +1,2 @@
$newline never
Mit Kursen assoziierte Prüfungen können nun auch weitere Institute angeben, die vollen Zugriff auf die Prüfungsleistungen erhalten

View File

@ -0,0 +1,2 @@
$newline never
Exams associated with courses may now also specify additional departments that then have full access to exam achievements

View File

@ -0,0 +1,2 @@
$newline never
Mit Kursen assoziierte Prüfungen müssen nun auch verantwortliche Hochschullehrer bzw. Prüfer angeben

View File

@ -0,0 +1,2 @@
$newline never
Exams associated with courses now also need to specify responsible university teachers/examiners

View File

@ -0,0 +1,2 @@
$newline never
Bessere Fehlermeldungen bei fehlgeschlagenem Login

View File

@ -0,0 +1,2 @@
$newline never
Better error messages on failed login

View File

@ -0,0 +1,108 @@
$newline never
<p>
Die Plätze in den Zentralanmeldungen werden nach den folgenden #
Kriterien verteilt (in grober Reihenfolge des Einfluss, den sie auf #
die Verteilung haben):
<ul>
<li>
Die eigene Priorisierung der Bewerbung (1. Wahl, etc.)
<br />
Die Priorisierung hat jedoch nur eine ordnende Funktion und #
diese auch nur innerhalb der Bewerbungen eines einzelnen #
Bewerbers. #
Die genauen Zahlen sind also bedeutungslos und werden auch nicht #
unter den Bewerbern verglichen.
<li>
Studienfortschritt (gemessen am Prozentsatz der für den Abschluss #
erforderlichen Veranstaltungen, die bereits bestanden wurden), #
nicht jedoch das Fach- oder Hochschulsemester
<br />
Den aus dem Studienfortschritt errechnet Parameter nennt Uni2work #
die „zentrale Dringlichkeit“.
<li>
Etwaige Bewertungen der Bewerbungen durch die Kursverwalter
<p>
Wenn Sie also keine Plätze in der Zentralanmeldung erhalten haben, #
liegt dies für gewöhnlich daran, dass Ihre zentrale Dringlichkeit in #
dieser Vergabe zu gering war und stattdessen andere Bewerber, mit #
weiter fortgeschrittenem Studium, Plätze erhalten haben.
<br />
Ebenso kann es sein, dass Sie nicht Ihre erste Wahl erhalten, wenn #
diese unter Studierenden mit höherer Dringlichkeit beliebt ist.
<br />
So wird sichergestellt, dass der Studienabschluss nicht durch #
fehlende Credits verzögert wird, die nur in Kursen erreicht werden #
können, die an einer Zentralanmeldung teilnehmen.
<p>
Für gewöhnlich gibt es zu jeder Zentralanmeldung auch ein #
Nachrückerverfahren. #
Es werden hierfür auf Basis der Bewerbungen für die #
Zentralanmeldungen Plätze, die wieder frei werden, erneut verteilt.
<br />
Die Kriterien für diese Verteilungen sind die selben, wie auch bei #
der ursprünglichen Verteilung. #
<br />
Wenn Sie sich bereits in der Zentralanmeldung beworben haben, ist #
eine gesonderte Anmeldung oder Bewerbung als Nachrücker nicht #
erforderlich. #
Sie werden automatisch benachrichtigt, falls Sie über das #
Nachrückerverfahren doch noch einen Platz bzw. zusätzliche Plätze #
erhalten (außer Sie haben diese Benachrichtigung aktiv unter #
„Anpassen“ ausgeschaltet).
<p>
Um in der nächsten Zentralanmeldung eine bessere Chance auf einen #
Platz zu haben können Sie folgende Schritte ergreifen:
<ul>
<li>
Für möglichst viele der angebotenen Kurse bewerben
<br />
Bei gleicher zentraler Dringlichkeit haben Bewerber, die mehr #
Bewerbungen einreichen, eine signifikant bessere Chance einen #
Platz zu erhalten.
<li>
Normal weiter studieren
<br />
Durch zusätzliche bestandene Leistungen wird sich Ihr #
Studienfortschritt und somit Ihre zentrale Dringlichkeit erhöhen.
<li>
Bessere Bewerbungen einreichen
<br />
Eine gute Bewertung der Bewerbung kann einen beträchtlichen #
Unterschied in zentraler Dringlichkeit ausgleichen. #
Wenn Ihre Bewerbungen von den Kursverwaltern gut bewertet werden, #
haben Sie eine bessere Chance auf einen Platz.

View File

@ -0,0 +1,105 @@
$newline never
<p>
Placements in central allocations are allocated according to the #
following criteria (ordered roughly by their impact on the #
allocation):
<ul>
<li>
The priority of the application (1st Choice, etc.)
<br />
The priority is only used to order the applications in the context #
of a single applicant. #
Therefore the exact numerical values are inconsequential and are #
not compared between applicants.
<li>
Study progress (measured by the number ECTS credits achieved as a #
percentage of those required for graduation) but not (university) #
semesters
<br />
The parameter calculated from study progress is referred to within #
Uni2work as “central priority”.
<li>
Ratings of applications by course administrators
<p>
If you were not allocated any placements this is usually because #
your central priority was too low. #
Instead other applicants with higher central priority, and thus a #
higher degree of study progress, have received placements.
<br />
Accordingly you may not have received the placements you wanted #
because the respective courses were popular among applicants with #
higher central priority.
<br />
This method of allocation ensures that graduation is not impeded by #
missing credits which can only be gained through courses which #
participate in a central allocation.
<p>
There usually is a process for substitute registrations. #
Places that become free after the initial allocation are assigned #
again on the basis of the existing applications.
<br />
The criteria for the allocation of placements are the same as for #
the initial allocation.
<br />
If you have already applied for the central allocation no further #
registration or application is necessary to be assigned a substitute #
registration. #
You will be notified automatically if you are assigned additional #
placements (unless you have actively disabled the notification under #
“Settings”).
<p>
To improve your chances of being allocated a placement during the #
next central allocation, you may try the following:
<ul>
<li>
Apply for as many courses as possible
<br />
Of two applicants with the same central priority, the one who #
applied for more courses has a significantly better chance of #
being allocated a placement.
<li>
Continue your studies normally
<br />
Through achieving additional credits your degree of study progress #
will improve and thus your central priority will, too.
<li>
Write better applications
<br />
Having an application rated well can ameliorate a considerable #
difference in central priority. #
If your applications are rated well by course administrators your #
chances to be allocated a placement improve.

View File

@ -0,0 +1,17 @@
$newline never
<p>
Gewöhnlicherweise wird Ihr Benutzereintrag gesperrt, wenn sie #
exmatrikuliert werden bzw. Ihr Beschäftigungsverhältnis endet. #
Es kommt gelegentlich vor, dass Ihr Benutzereintrag nicht korrekt #
entsperrt wird, wenn Sie wieder immatrikuliert bzw. eingestellt #
werden.
<p>
Falls Sie aktuell immatrikuliert bzw. eingestellt sind, oder Sie #
einen anderen triftigen Grund vorweisen können, warum Sie Zugang zu #
Uni2work brauchen, wenden Sie sich bitte über #
das <a href=@{HelpR}>Hilfe-Formular</a>, oben rechts auf jeder #
Seite, an die Uni2work-Administration und schildern Sie Ihre #
Situation.

View File

@ -0,0 +1,14 @@
$newline never
<p>
Usually your account is disabled once you are no longer matriculated #
(i.e. registered as a student) or employed. #
Occasionally accounts are not correctly re-enabled once you are #
matriculated or employed, again.
<p>
If you are currently matriculated, employed, or have another good #
reason why you should have access to Uni2work, please contact a #
Uni2work-Administrator using the <a href=@{HelpR}>Support form</a> #
(at the top right of every page) and describe your situation.

View File

@ -16,6 +16,15 @@ $newline never
<p>
_{SomeMessage MsgAllocationResultsTip}
$if not (null warnSubstituteCourses)
<p>
_{SomeMessage MsgAllocationResultsLecturerSubstituteCoursesWarning}
<ul>
$forall Course{courseTerm, courseSchool, courseShorthand, courseName} <- warnSubstituteCourses
<li>
<a href=@{CourseR courseTerm courseSchool courseShorthand CEditR}>
#{courseName}
$if not (null lecturerResults)
<p>
_{SomeMessage MsgAllocationResultsLecturer}

18
test/Auth/LDAP/ADSpec.hs Normal file
View File

@ -0,0 +1,18 @@
module Auth.LDAP.ADSpec where
import TestImport
import Auth.LDAP.AD
import Ldap.Client
spec :: Spec
spec = do
describe "parseADError" $ do
it "parses some examples" . mapM_ exampleEntry $
[ ( InvalidCredentials, ADAccountDisabled, "80090308: LdapErr: DSID-0C090446, comment: AcceptSecurityContext error, data 533, v2580")
, ( InvalidCredentials, ADLogonFailure , "80090308: LdapErr: DSID-0C090446, comment: AcceptSecurityContext error, data 52e, v2580")
]
exampleEntry :: ( ResultCode, ADError, Text ) -> Expectation
exampleEntry ( resCode, adError, errMsg ) = example $ parseADError resCode errMsg `shouldBe` Right adError

View File

@ -4,8 +4,8 @@ module Database
, module Database.Fill
) where
import "uniworx" Import hiding (Option(..))
import "uniworx" Application (db, getAppDevSettings)
import "uniworx" Import hiding (Option(..), getArgs)
import "uniworx" Application (db', getAppSettings)
import UnliftIO.Pool (destroyAllResources)
@ -15,6 +15,7 @@ import Control.Monad.Logger
import System.Console.GetOpt
import System.Exit (exitWith, ExitCode(..))
import System.IO (hPutStrLn)
import System.Environment (getArgs, withArgs)
import Database.Persist.Sql.Raw.QQ
@ -39,19 +40,19 @@ argsDescr =
main :: IO ()
main = do
args <- map unpack <$> getArgs
case getOpt Permute argsDescr args of
(acts@(_:_), [], []) -> forM_ acts $ \case
case getOpt' Permute argsDescr args of
(acts@(_:_), nonOpts, unrecOpts, []) -> withArgs (unrecOpts ++ nonOpts) . forM_ acts $ \case
DBClear -> runStderrLoggingT $ do -- We don't use `db` here, since we do /not/ want any migrations to run, yet
settings <- liftIO getAppDevSettings
settings <- liftIO getAppSettings
withPostgresqlConn (pgConnStr $ appDatabaseConf settings) . runSqlConn $ do
[executeQQ|drop owned by current_user|] :: ReaderT SqlBackend _ ()
DBTruncate -> db $ do
DBTruncate -> db' $ do
foundation <- getYesod
liftIO . destroyAllResources $ appConnPool foundation
truncateDb
DBMigrate -> db $ return ()
DBFill -> db $ fillDb
(_, _, errs) -> do
DBMigrate -> db' $ return ()
DBFill -> db' $ fillDb
(_, _, _, errs) -> do
forM_ errs $ hPutStrLn stderr
hPutStrLn stderr $ usageInfo "uniworxdb" argsDescr
exitWith $ ExitFailure 2

View File

@ -677,6 +677,7 @@ fillDb = do
, examSynchronicity = Just $ ExamSynchronicityPreset ExamSynchronous
, examRequiredEquipment = Just $ ExamRequiredEquipmentPreset ExamRequiredEquipmentNone
}
, examStaff = Just "Hofmann"
}
void . insertMany $ map (\u -> ExamRegistration examFFP u Nothing now)
[ fhamann
@ -1055,8 +1056,8 @@ fillDb = do
, allocationOverrideDeregister = Just $ termTime True Summer 1 False Monday toMidnight
, allocationMatchingSeed = aSeedFunc
}
insert_ $ AllocationCourse funAlloc pmo 100
insert_ $ AllocationCourse funAlloc ffp 2
insert_ $ AllocationCourse funAlloc pmo 100 Nothing
insert_ . AllocationCourse funAlloc ffp 2 . Just $ 2300 `addUTCTime` now
void . insertMany $ map (\(u, pState) -> CourseParticipant ffp u now (Just funAlloc) pState)
[ (svaupel, CourseParticipantInactive False)
@ -1195,6 +1196,8 @@ fillDb = do
cap <- getRandomR (10,50)
minCap <- round . (* fromIntegral cap) <$> getRandomR (0, 0.5 :: Double)
substitutesUntil <- (`addUTCTime` now) . fromInteger <$> getRandomR (900,2300)
cid <- insert' Course
{ courseName = CI.mk [st|Zentralanmeldungskurs #{n} (#{csh})|]
@ -1219,7 +1222,7 @@ fillDb = do
, courseDeregisterNoShow = False
}
insert_ $ CourseEdit gkleen now cid
insert_ $ AllocationCourse bigAlloc cid minCap
insert_ . AllocationCourse bigAlloc cid minCap $ Just substitutesUntil
-- void . insert' $ Lecturer gkleen cid CourseLecturer
return cid