diff --git a/frontend/src/app.sass b/frontend/src/app.sass index 6df8a8afa..c82d1bb8e 100644 --- a/frontend/src/app.sass +++ b/frontend/src/app.sass @@ -776,6 +776,9 @@ section .allocation__courses margin: 20px 0 0 40px + .form-group__input > & + margin: 0 + .allocation-course display: grid grid-template-columns: minmax(105px, 1fr) 9fr diff --git a/messages/uniworx/de-de-formal.msg b/messages/uniworx/de-de-formal.msg index 0aa58cc7c..14e103f49 100644 --- a/messages/uniworx/de-de-formal.msg +++ b/messages/uniworx/de-de-formal.msg @@ -1375,6 +1375,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 @@ -1449,6 +1450,7 @@ BreadcrumbAllocationUsers: Bewerber BreadcrumbAllocationPriorities: Zentrale Dringlichkeiten BreadcrumbAllocationCompute: Platzvergabe berechnen BreadcrumbAllocationAccept: Platzvergabe akzeptieren +BreadcrumbAllocationAddUser: Bewerber hinzufügen BreadcrumbMessageHide: Verstecken BreadcrumbFaq: FAQ BreadcrumbSheetPersonalisedFiles: Personalisierte Dateien herunterladen @@ -2707,12 +2709,34 @@ AllocationUsersCsvName tid@TermId ssh@SchoolId ash@AllocationShorthand: #{foldCa 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 diff --git a/messages/uniworx/en-eu.msg b/messages/uniworx/en-eu.msg index b493635d6..fa8496197 100644 --- a/messages/uniworx/en-eu.msg +++ b/messages/uniworx/en-eu.msg @@ -1375,6 +1375,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 @@ -1449,6 +1450,7 @@ BreadcrumbAllocationUsers: Applicants BreadcrumbAllocationPriorities: Central priorities BreadcrumbAllocationCompute: Compute allocation BreadcrumbAllocationAccept: Accept allocation +BreadcrumbAllocationAddUser: Add applicant BreadcrumbMessageHide: Hide BreadcrumbFaq: FAQ BreadcrumbSheetPersonalisedFiles: Download personalised sheet files @@ -2707,12 +2709,34 @@ AllocationUsersCsvName tid ssh ash: #{foldCase (termToText (unTermKey tid))}-#{f 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 diff --git a/routes b/routes index 6a60ab694..7658aa6ce 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/Foundation/Navigation.hs b/src/Foundation/Navigation.hs index f1e8281c1..99fc523e8 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 @@ -1332,6 +1333,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/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/AddUser.hs b/src/Handler/Allocation/AddUser.hs new file mode 100644 index 000000000..1eb83e59d --- /dev/null +++ b/src/Handler/Allocation/AddUser.hs @@ -0,0 +1,162 @@ +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 + ) + + 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, hasTemplate)) | (Entity cId course, E.Value hasTemplate) <- 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, Bool) + -> FieldSettings UniWorX + -> Bool + -> AForm Handler (Map CourseId ApplicationForm) +allocationApplicationsForm aId courses FieldSettings{..} fvRequired = formToAForm $ do + let afmApplicant = True + afmApplicantEdit = True + afmLecturer = True + + appsRes' <- iforM courses $ \cId (course, hasApplicationTemplate) -> over _2 (course, hasApplicationTemplate, ) <$> applicationForm (Just aId) cId Nothing ApplicationFormMode{..} Nothing + let appsRes = sequenceA $ view _1 <$> appsRes' + appsViews = view _2 <$> appsRes' + + let fvInput = + [whamlet| + $newline never +