diff --git a/frontend/src/app.sass b/frontend/src/app.sass index c82d1bb8e..a5195c783 100644 --- a/frontend/src/app.sass +++ b/frontend/src/app.sass @@ -782,7 +782,7 @@ section .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 @@ -833,10 +833,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/uniworx/de-de-formal.msg b/messages/uniworx/de-de-formal.msg index 14e103f49..37e635897 100644 --- a/messages/uniworx/de-de-formal.msg +++ b/messages/uniworx/de-de-formal.msg @@ -204,6 +204,8 @@ 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 CourseApplicationInstructions: Anweisungen zur Bewerbung/Anmeldung CourseApplicationInstructionsTip: Wird den Studierenden angezeigt, wenn diese sich für Ihre Veranstaltung bewerben bzw. bei dieser anmelden CourseApplicationTemplate: Bewerbungsvorlagen @@ -2263,6 +2265,8 @@ 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 AllocationSchoolShort: Institut Allocation: Zentralanmeldung @@ -2502,6 +2506,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 @@ -2513,6 +2519,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. @@ -2752,6 +2759,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. +AllocationCourseRestrictionNone: 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! @@ -2769,6 +2779,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} diff --git a/messages/uniworx/en-eu.msg b/messages/uniworx/en-eu.msg index fa8496197..d581d1730 100644 --- a/messages/uniworx/en-eu.msg +++ b/messages/uniworx/en-eu.msg @@ -204,6 +204,8 @@ 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 CourseApplicationInstructions: Instructions for application CourseApplicationInstructionsTip: Will be shown to students if they decide to apply for this course CourseApplicationTemplate: Application template @@ -2262,6 +2264,8 @@ 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 AllocationSchoolShort: Department Allocation: Central allocation @@ -2502,6 +2506,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 @@ -2513,6 +2519,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}. @@ -2752,6 +2759,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. +AllocationCourseRestrictionNone: 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! @@ -2769,6 +2779,7 @@ AllocationOfferedPlaces: Offered places AllocationUserNewMatches: New allocations AllocationUsersCount: Participants AllocationCoursesCount: Courses +AllocationCourseEligible: Considered CourseOption tid ssh coursen: #{tid} - #{ssh} - #{coursen} 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/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 index 1eb83e59d..689919c45 100644 --- a/src/Handler/Allocation/AddUser.hs +++ b/src/Handler/Allocation/AddUser.hs @@ -33,6 +33,7 @@ postAAddUserR tid ssh ash = do return ( course , E.exists . E.from $ \courseAppInstructionFile -> E.where_ $ courseAppInstructionFile E.^. CourseAppInstructionFileCourse E.==. course E.^. CourseId + , allocationCourse ) MsgRenderer mr <- getMsgRenderer @@ -40,7 +41,7 @@ postAAddUserR tid ssh ash = do <$> 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, hasTemplate)) | (Entity cId course, E.Value hasTemplate) <- allocCourses ]) (fslI MsgAllocationAddUserApplications) False + <*> 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 @@ -103,16 +104,18 @@ postAAddUserR tid ssh ash = do } allocationApplicationsForm :: AllocationId - -> Map CourseId (Course, Bool) + -> 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, hasApplicationTemplate) -> over _2 (course, hasApplicationTemplate, ) <$> applicationForm (Just aId) cId Nothing ApplicationFormMode{..} Nothing + 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' @@ -120,7 +123,7 @@ allocationApplicationsForm aId courses FieldSettings{..} fvRequired = formToAFor [whamlet| $newline never
+ $maybe deadline <- allocationCourseAcceptSubstitutes + _{MsgCourseAllocationCourseAcceptsSubstitutesUntil}: # + ^{formatTimeW SelFormatDateTime deadline} + $nothing + _{MsgCourseAllocationCourseAcceptsSubstitutesNever} + $if allocationCourseAcceptSubstitutes >= Just now + \ ^{iconOK} $if hasApplicationTemplate || is _Just courseApplicationsInstructions
+ $maybe deadline <- allocationCourseAcceptSubstitutes + _{MsgCourseAllocationCourseAcceptsSubstitutesUntil}: # + ^{formatTimeW SelFormatDateTime deadline} + $nothing + _{MsgCourseAllocationCourseAcceptsSubstitutesNever} + $if allocationCourseAcceptSubstitutes >= Just now + \ ^{iconOK} $if hasApplicationTemplate || is _Just courseApplicationsInstructions
$maybe visFrom <- courseVisibleFrom
^{formatTimeRangeW SelFormatDateTime visFrom courseVisibleTo}
+
$if NTop (Just now) < NTop courseVisibleFrom
$if hasAllocationRegistrationOpen
_{MsgCourseInvisibleOverridenByAllocation}
diff --git a/templates/i18n/changelog/allocation-course-accept-substitutes.de-de-formal.hamlet b/templates/i18n/changelog/allocation-course-accept-substitutes.de-de-formal.hamlet
new file mode 100644
index 000000000..dc1db6461
--- /dev/null
+++ b/templates/i18n/changelog/allocation-course-accept-substitutes.de-de-formal.hamlet
@@ -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
diff --git a/templates/i18n/changelog/allocation-course-accept-substitutes.en-eu.hamlet b/templates/i18n/changelog/allocation-course-accept-substitutes.en-eu.hamlet
new file mode 100644
index 000000000..d7e5ad889
--- /dev/null
+++ b/templates/i18n/changelog/allocation-course-accept-substitutes.en-eu.hamlet
@@ -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
diff --git a/templates/mail/allocationResults.hamlet b/templates/mail/allocationResults.hamlet
index bf2bddf89..8b9039215 100644
--- a/templates/mail/allocationResults.hamlet
+++ b/templates/mail/allocationResults.hamlet
@@ -16,6 +16,15 @@ $newline never
_{SomeMessage MsgAllocationResultsTip} + $if not (null warnSubstituteCourses) +
+ _{SomeMessage MsgAllocationResultsLecturerSubstituteCoursesWarning} +