diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 48adc6a88..db4ef096a 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -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 & + 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 diff --git a/messages/faq/de-de-formal.msg b/messages/faq/de-de-formal.msg index 092366c09..bc424609f 100644 --- a/messages/faq/de-de-formal.msg +++ b/messages/faq/de-de-formal.msg @@ -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? \ No newline at end of file +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 \ No newline at end of file diff --git a/messages/faq/en-eu.msg b/messages/faq/en-eu.msg index 1cce5d04b..51c56628f 100644 --- a/messages/faq/en-eu.msg +++ b/messages/faq/en-eu.msg @@ -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 \ No newline at end of file diff --git a/messages/uniworx/de-de-formal.msg b/messages/uniworx/de-de-formal.msg index f3ae33d80..a321103e3 100644 --- a/messages/uniworx/de-de-formal.msg +++ b/messages/uniworx/de-de-formal.msg @@ -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 \ No newline at end of file +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 \ No newline at end of file diff --git a/messages/uniworx/en-eu.msg b/messages/uniworx/en-eu.msg index be4d6958c..6946ac632 100644 --- a/messages/uniworx/en-eu.msg +++ b/messages/uniworx/en-eu.msg @@ -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 \ No newline at end of file +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 diff --git a/models/allocations.model b/models/allocations.model index f063a50ea..a8263979b 100644 --- a/models/allocations.model +++ b/models/allocations.model @@ -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 diff --git a/models/exams.model b/models/exams.model index 497ea575c..68ed2a782 100644 --- a/models/exams.model +++ b/models/exams.model @@ -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 diff --git a/package-lock.json b/package-lock.json index c2a023d6e..d52c1250a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "uni2work", - "version": "20.6.0", + "version": "20.13.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 57c4e377e..c9ed90296 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "uni2work", - "version": "20.6.0", + "version": "20.13.0", "description": "", "keywords": [], "author": "", diff --git a/package.yaml b/package.yaml index 6c59a87a6..fa6071e8e 100644 --- a/package.yaml +++ b/package.yaml @@ -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 diff --git a/routes b/routes index 8819dc282..2434d0b08 100644 --- a/routes +++ b/routes @@ -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 diff --git a/src/Application.hs b/src/Application.hs index 2eb0b9d46..6ca3ffff6 100644 --- a/src/Application.hs +++ b/src/Application.hs @@ -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{..} diff --git a/src/Auth/LDAP.hs b/src/Auth/LDAP.hs index 007178793..597163cd4 100644 --- a/src/Auth/LDAP.hs +++ b/src/Auth/LDAP.hs @@ -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 -> diff --git a/src/Auth/LDAP/AD.hs b/src/Auth/LDAP/AD.hs new file mode 100644 index 000000000..58d0ca4f8 --- /dev/null +++ b/src/Auth/LDAP/AD.hs @@ -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 +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 ", ") diff --git a/src/Foundation/Authorization.hs b/src/Foundation/Authorization.hs index 9b7b211bd..3f479f5df 100644 --- a/src/Foundation/Authorization.hs +++ b/src/Foundation/Authorization.hs @@ -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 diff --git a/src/Foundation/I18n.hs b/src/Foundation/I18n.hs index 1d5ac1248..ac5e31cb0 100644 --- a/src/Foundation/I18n.hs +++ b/src/Foundation/I18n.hs @@ -231,6 +231,8 @@ embedRenderMessage ''UniWorX ''AuthenticationMode id embedRenderMessage ''UniWorX ''RatingValidityException id +embedRenderMessageVariant ''UniWorX ''ADInvalidCredentials ("InvalidCredentials" <>) + newtype ShortSex = ShortSex Sex embedRenderMessageVariant ''UniWorX ''ShortSex ("Short" <>) diff --git a/src/Foundation/Instances.hs b/src/Foundation/Instances.hs index 7527dd524..4827b3583 100644 --- a/src/Foundation/Instances.hs +++ b/src/Foundation/Instances.hs @@ -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 diff --git a/src/Foundation/Navigation.hs b/src/Foundation/Navigation.hs index c1fbf1e95..79613fbea 100644 --- a/src/Foundation/Navigation.hs +++ b/src/Foundation/Navigation.hs @@ -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 diff --git a/src/Foundation/Yesod/Middleware.hs b/src/Foundation/Yesod/Middleware.hs index 1a7183602..3c9fb713c 100644 --- a/src/Foundation/Yesod/Middleware.hs +++ b/src/Foundation/Yesod/Middleware.hs @@ -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 diff --git a/src/Handler/Allocation.hs b/src/Handler/Allocation.hs index 5162e86a5..088af6f42 100644 --- a/src/Handler/Allocation.hs +++ b/src/Handler/Allocation.hs @@ -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 diff --git a/src/Handler/Allocation/Accept.hs b/src/Handler/Allocation/Accept.hs index d6b1c47d3..865f356f0 100644 --- a/src/Handler/Allocation/Accept.hs +++ b/src/Handler/Allocation/Accept.hs @@ -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 diff --git a/src/Handler/Allocation/AddUser.hs b/src/Handler/Allocation/AddUser.hs new file mode 100644 index 000000000..689919c45 --- /dev/null +++ b/src/Handler/Allocation/AddUser.hs @@ -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 +
+ $forall (Course{courseTerm, courseSchool, courseShorthand, courseName, courseApplicationsInstructions}, AllocationCourse{allocationCourseAcceptSubstitutes}, hasApplicationTemplate, ApplicationFormView{afvPriority, afvForm}) <- Map.elems appsViews +
+
+ _{MsgAllocationPriority} +
+ $maybe prioView <- afvPriority + ^{fvWidget prioView} + + #{courseName} +
+

+ $maybe deadline <- allocationCourseAcceptSubstitutes + _{MsgCourseAllocationCourseAcceptsSubstitutesUntil}: # + ^{formatTimeW SelFormatDateTime deadline} + $nothing + _{MsgCourseAllocationCourseAcceptsSubstitutesNever} + $if allocationCourseAcceptSubstitutes >= Just now + \ ^{iconOK} + $if hasApplicationTemplate || is _Just courseApplicationsInstructions +

+ _{MsgCourseApplicationInstructionsApplication} +
+ $maybe aInst <- courseApplicationsInstructions +

+ #{aInst} + $if hasApplicationTemplate +

+ + #{iconRegisterTemplate} _{MsgCourseApplicationTemplateApplication} +

+ _{MsgCourseApplication} +
+ ^{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 +