Merge branch 'master' into workflows

This commit is contained in:
Gregor Kleen 2020-10-27 11:02:03 +01:00
commit 3e6935490b
27 changed files with 497 additions and 122 deletions

View File

@ -2,6 +2,18 @@
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
## [20.13.0](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/v20.12.1...v20.13.0) (2020-10-20)
### Features
* **allocations:** display participant counts to admins ([b79bac7](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/b79bac777c6d349a626ea4efa6c43141b7f669d0))
### Bug Fixes
* **allocations:** fix allocation-course-accept-substitutes ([b4df980](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/b4df98069982752e36e69571f5557a6179b44cff))
### [20.12.1](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/v20.12.0...v20.12.1) (2020-10-14)

View File

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

View File

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

View File

@ -206,6 +206,7 @@ CourseAllocationMinCapacityTip: Wenn der Veranstaltung bei der Zentralanmeldung
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
@ -262,7 +263,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
@ -1995,6 +1998,8 @@ ExamOccurrenceStartMustBeAfterExamStart eoName@ExamOccurrenceName: Beginn des Te
ExamOccurrenceEndMustBeBeforeExamEnd eoName@ExamOccurrenceName: Ende des Termins #{eoName} liegt nach dem Ende der Prüfung
ExamOccurrenceDuplicate eoRoom@Text eoRange@Text: Raum #{eoRoom}, Termin #{eoRange} kommt mehrfach mit der selben Beschreibung vor
ExamOccurrenceDuplicateName eoName@ExamOccurrenceName: Interne Terminbezeichnung #{eoName} kommt mehrfach vor
ExamOccurrenceCannotBeDeletedDueToRegistrations eoName@ExamOccurrenceName: Termin #{eoName} kann nicht gelöscht werden, da noch Teilnehmer diesem Termin zugewiesen sind. Über die Liste von Prüfungsteilnehmern können Sie zunächst die entsprechenden Terminzuweisungen entfernen.
ExamPartCannotBeDeletedDueToResults exampartnum@ExamPartNumber: Teil #{exampartnum} kann nicht gelöscht werden, da bereits Prüfungsergebnisse für diesen Teil eingetragen wurden.
VersionHistory: Versionsgeschichte
KnownBugs: Bekannte Bugs
@ -2317,6 +2322,7 @@ AllocationNotificationNewCourseCurrentlyOn: Aktuell würden Sie benachrichtigt w
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

View File

@ -206,6 +206,7 @@ CourseAllocationMinCapacityTip: If fewer students than this number were to be as
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
@ -262,7 +263,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
@ -1955,6 +1957,8 @@ ExamOccurrenceStartMustBeAfterExamStart eoName: Start of the occurrence #{eoName
ExamOccurrenceEndMustBeBeforeExamEnd eoName: End of the occurrence #{eoName} must be before the exam end
ExamOccurrenceDuplicate eoRoom eoRange: Combination of room #{eoRoom} and occurrence #{eoRange} occurs multiple times
ExamOccurrenceDuplicateName eoName: Internal name #{eoName} occurs multiple times
ExamOccurrenceCannotBeDeletedDueToRegistrations eoName: Occurrence #{eoName} cannot be deleted because participants are registered for it. You can remove the offending registrations via the list of exam participants.
ExamPartCannotBeDeletedDueToResults exampartnum: Part #{exampartnum} cannot be deleted because some exam part results were already entered for it.
VersionHistory: Version history
KnownBugs: Known bugs
@ -2277,6 +2281,7 @@ 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

View File

@ -4,7 +4,7 @@
import ((nixpkgs {}).fetchFromGitHub {
owner = "NixOS";
repo = "nixpkgs";
rev = "bc00ecedfa709f4fa91d445dd76ecd792cb2c728";
sha256 = "0plhwb04srr4b0h7w8qlqi207a19szz2wqz6r4gmic856jlkchaa";
rev = "a7a1447e5d40a9ad90983d33e151f5474eddeed9";
sha256 = "1zb8wgsq9grrsdcz81y08h45rj8i5r8ckjhg2cv1cqmam4dczcrf";
fetchSubmodules = true;
})

2
package-lock.json generated
View File

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

View File

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

View File

@ -1,5 +1,5 @@
name: uniworx
version: 20.12.1
version: 20.13.0
dependencies:
- base

View File

@ -160,6 +160,7 @@ 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]
@ -220,7 +221,7 @@ campusLogin pool mode = AuthPlugin{..}
$logInfoS apName [st|#{campusIdent}: #{toPathPiece adError}|]
observeLoginOutcome apName LoginADInvalidCredentials
MsgRenderer mr <- liftHandler getMsgRenderer
setSessionJson SessionError . PermissionDenied . mr $ ADInvalidCredentials adError
setSessionJson SessionError . PermissionDenied . toPathPiece $ ADInvalidCredentials adError
loginErrorMessage (tp LoginR) . mr $ ADInvalidCredentials adError
Right (Left bindErr) -> do
case bindErr of

View File

@ -61,6 +61,8 @@ postAShowR tid ssh ash = do
resultCourseVisible = _5 . _Value
resultAllocationCourse :: _ => Lens' a AllocationCourse
resultAllocationCourse = _6 . _entityVal
resultParticipantCount :: _ => Lens' a Int
resultParticipantCount = _7 . _Value
(Entity aId Allocation{..}, School{..}, isAnyLecturer, isAdmin, courses, registration, wouldNotifyNewCourse) <- runDB $ do
alloc@(Entity aId Allocation{allocationSchool}) <- getBy404 $ TermSchoolAllocationShort tid ssh ash
@ -81,12 +83,16 @@ postAShowR tid ssh ash = do
E.orderBy [E.asc $ course E.^. CourseName]
let hasTemplate = E.exists . E.from $ \courseAppInstructionFile ->
E.where_ $ courseAppInstructionFile E.^. CourseAppInstructionFileCourse E.==. course E.^. CourseId
participantCount = E.subSelectCount . E.from $ \courseParticipant ->
E.where_ $ courseParticipant E.^. CourseParticipantCourse E.==. course E.^. CourseId
E.&&. courseParticipant E.^. CourseParticipantState E.==. E.val CourseParticipantActive
return ( course
, courseApplication
, hasTemplate
, E.not_ . E.isNothing $ registration E.?. CourseParticipantId
, courseIsVisible now course . Just $ E.val aId
, allocationCourse
, participantCount
)
registration <- fmap join . for muid $ getBy . UniqueAllocationUser aId
@ -99,6 +105,7 @@ postAShowR tid ssh ash = do
return (alloc, school, isAnyLecturer, isAdmin, nubOn (view $ resultCourse . _entityKey) courses, registration, wouldNotifyNewCourse)
let nextSubstitutesDeadline = minimumOf (folded . resultAllocationCourse . _allocationCourseAcceptSubstitutes . _Just . filtered (>= now)) courses
freeCapacity = fmap getSum . getAp . flip foldMap courses $ \cEntry -> Ap . fmap (Sum . max 0) $ subtract (cEntry ^. resultParticipantCount) <$> preview (resultCourse . _entityVal . _courseCapacity . _Just) cEntry
MsgRenderer mr <- getMsgRenderer
let title = MsgAllocationTitle (mr . ShortTermIdentifier $ unTermKey allocationTerm) (unSchoolKey allocationSchool) allocationName
@ -158,6 +165,7 @@ postAShowR tid ssh ash = do
isRegistered = cEntry ^. resultIsRegistered
courseVisible = cEntry ^. resultCourseVisible
AllocationCourse{..} = cEntry ^. resultAllocationCourse
partCount = cEntry ^. resultParticipantCount
cID <- encrypt cid :: WidgetFor UniWorX CryptoUUIDCourse
mayApply <- hasWriteAccessTo . AllocationR tid ssh ash $ AApplyR cID
mayEdit <- hasWriteAccessTo $ CourseR tid ssh courseShorthand CEditR

View File

@ -61,6 +61,9 @@ data AllocationCourseForm = AllocationCourseForm
, acfDeregisterNoShow :: Bool
}
makeLenses_ ''CourseForm
makeLenses_ ''AllocationCourseForm
courseToForm :: Entity Course -> [Lecturer] -> Map UserEmail (InvitationDBData Lecturer) -> Maybe (Entity AllocationCourse) -> CourseForm
courseToForm cEnt@(Entity cid Course{..}) lecs lecInvites alloc = CourseForm
{ cfCourseId = Just cid
@ -326,20 +329,28 @@ validateCourse = do
now <- liftIO getCurrentTime
uid <- liftHandler requireAuthId
userAdmin <- hasWriteAccessTo $ SchoolR cfSchool SchoolEditR
allocationTerm <- for (acfAllocation <$> cfAllocation) $ lift . fmap allocationTerm . getJust
newAllocationTerm <- for (acfAllocation <$> cfAllocation) $ lift . fmap allocationTerm . getJust
oldAllocatedCapacity <- fmap join . for cfCourseId $ \cid -> lift $ do
prevAllocationCourse <- getBy $ UniqueAllocationCourse cid
prevAllocation <- fmap join . traverse get $ allocationCourseAllocation . entityVal <$> prevAllocationCourse
prevAllocationCourse <- join <$> traverse (lift . getBy . UniqueAllocationCourse) cfCourseId
prevAllocation <- fmap join . traverse (lift . getEntity) $ allocationCourseAllocation . entityVal <$> prevAllocationCourse
fmap join . for prevAllocation $ \Allocation{allocationStaffAllocationTo, allocationRegisterByCourse} -> if
| userAdmin
-> return Nothing
| NTop allocationStaffAllocationTo <= NTop (Just now)
, NTop allocationRegisterByCourse > NTop (Just now)
-> Just . courseCapacity <$> getJust cid
| otherwise
-> return Nothing
oldAllocatedCapacity <- if
| Just (Entity _ Allocation{..}) <- prevAllocation
, Just (Entity _ AllocationCourse{..}) <- prevAllocationCourse
, NTop allocationStaffAllocationTo <= NTop (Just now)
, NTop allocationRegisterByCourse > NTop (Just now)
-> lift $ Just . courseCapacity <$> getJust allocationCourseCourse
| otherwise
-> return Nothing
let oldAllocation = do
Entity allocId Allocation{..} <- prevAllocation
guard $ NTop (Just now) > NTop allocationStaffRegisterTo
pure $ Just allocId
oldAllocatedMinCapacity = do
Entity _ Allocation{..} <- prevAllocation
Entity _ AllocationCourse{..} <- prevAllocationCourse
guard $ NTop (Just now) > NTop allocationStaffRegisterTo
pure $ Just allocationCourseMinCapacity
guardValidation MsgCourseVisibilityEndMustBeAfterStart
$ NTop cfVisFrom <= NTop cfVisTo
@ -347,15 +358,19 @@ validateCourse = do
$ NTop cfRegFrom <= NTop cfRegTo
guardValidation MsgCourseDeregistrationEndMustBeAfterStart
$ Just False /= ((<=) <$> cfRegFrom <*> cfDeRegUntil)
unless userAdmin $
guardValidation MsgCourseUserMustBeLecturer
$ anyOf (traverse . _Right . _1) (== uid) cfLecturers
guardValidation MsgCourseAllocationRequiresCapacity
$ is _Nothing cfAllocation || is _Just cfCapacity
guardValidation MsgCourseAllocationTermMustMatch
$ maybe True (== cfTerm) allocationTerm
guardValidation MsgCourseAllocationCapacityMayNotBeChanged
$ maybe True (== cfCapacity) oldAllocatedCapacity
$ maybe True (== cfTerm) newAllocationTerm
unless userAdmin $ do
guardValidation MsgCourseUserMustBeLecturer
$ anyOf (traverse . _Right . _1) (== uid) cfLecturers
guardValidation MsgCourseAllocationCapacityMayNotBeChanged
$ maybe True (== cfCapacity) oldAllocatedCapacity
guardValidation MsgAllocationStaffRegisterToExpiredAllocation
$ maybe True (== fmap acfAllocation cfAllocation) oldAllocation
guardValidation MsgAllocationStaffRegisterToExpiredMinCapacity
$ maybe True (== fmap acfMinCapacity cfAllocation) oldAllocatedMinCapacity
warnValidation MsgCourseShorthandTooLong
$ length (CI.original cfShort) <= 10
@ -567,48 +582,23 @@ courseEditHandler miButtonAction mbCourseForm = do
}
upsertAllocationCourse :: CourseId -> Maybe AllocationCourseForm -> YesodJobDB UniWorX ()
upsertAllocationCourse cid cfAllocation = do
now <- liftIO getCurrentTime
Course{} <- getJust cid
prevAllocationCourse <- getBy $ UniqueAllocationCourse cid
prevAllocation <- fmap join . traverse getEntity $ allocationCourseAllocation . entityVal <$> prevAllocationCourse
userAdmin <- fromMaybe False <$> for prevAllocation (\(Entity _ Allocation{..}) -> hasWriteAccessTo $ SchoolR allocationSchool SchoolEditR)
upsertAllocationCourse cid = \case
Just AllocationCourseForm{..} -> do
prevAllocationCourse <- getBy $ UniqueAllocationCourse cid
doEdit <- if
| userAdmin
-> return True
| Just (Entity _ Allocation{allocationStaffRegisterTo}) <- prevAllocation
, NTop allocationStaffRegisterTo <= NTop (Just now)
-> let anyChanges
| Just AllocationCourseForm{..} <- cfAllocation
, Just (Entity _ AllocationCourse{..}) <- prevAllocationCourse
= or [ acfAllocation /= allocationCourseAllocation
, acfMinCapacity /= allocationCourseMinCapacity
]
| otherwise
= True
in False <$ when anyChanges (addMessageI Error MsgAllocationStaffRegisterToExpired)
| otherwise
-> return True
void $ upsert AllocationCourse
{ allocationCourseAllocation = acfAllocation
, allocationCourseCourse = cid
, allocationCourseMinCapacity = acfMinCapacity
, allocationCourseAcceptSubstitutes = acfAcceptSubstitutes
}
[ AllocationCourseAllocation =. acfAllocation
, AllocationCourseCourse =. cid
, AllocationCourseMinCapacity =. acfMinCapacity
, AllocationCourseAcceptSubstitutes =. acfAcceptSubstitutes
]
when doEdit $
case cfAllocation of
Just AllocationCourseForm{..} -> do
void $ upsert AllocationCourse
{ allocationCourseAllocation = acfAllocation
, allocationCourseCourse = cid
, allocationCourseMinCapacity = acfMinCapacity
, allocationCourseAcceptSubstitutes = acfAcceptSubstitutes
}
[ AllocationCourseAllocation =. acfAllocation
, AllocationCourseCourse =. cid
, AllocationCourseMinCapacity =. acfMinCapacity
, AllocationCourseAcceptSubstitutes =. acfAcceptSubstitutes
]
when (Just acfAllocation /= fmap entityKey prevAllocation) $
queueDBJob . JobQueueNotification $ NotificationAllocationNewCourse acfAllocation cid
Nothing
| Just (Entity prevId _) <- prevAllocationCourse
-> delete prevId
_other -> return ()
when (Just acfAllocation /= fmap (allocationCourseAllocation . entityVal) prevAllocationCourse) $
queueDBJob . JobQueueNotification $ NotificationAllocationNewCourse acfAllocation cid
Nothing ->
deleteWhere [ AllocationCourseCourse ==. cid ]

View File

@ -18,17 +18,14 @@ import Jobs.Queue
getEEditR, postEEditR :: TermId -> SchoolId -> CourseShorthand -> ExamName -> Handler Html
getEEditR = postEEditR
postEEditR tid ssh csh examn = do
(cid, Entity eId oldExam, template) <- runDB $ do
(cid, exam) <- fetchCourseIdExam tid ssh csh examn
(template, (editExamAct, (editExamWidget, editExamEnctype))) <- runDBJobs $ do
(cid, exam@(Entity eId oldExam)) <- fetchCourseIdExam tid ssh csh examn
template <- examFormTemplate exam
return (cid, exam, template)
((editExamResult, editExamWidget), editExamEnctype) <- runFormPost . validateForm (validateExam cid $ Just exam) . examForm $ Just template
((editExamResult, editExamWidget), editExamEnctype) <- runFormPost . validateForm (validateExam cid $ Just oldExam) . examForm $ Just template
formResult editExamResult $ \ExamForm{..} -> do
insertRes <- runDBJobs $ do
editExamAct <- formResultMaybe editExamResult $ \ExamForm{..} -> do
insertRes <- myReplaceUnique eId Exam
{ examCourse = cid
, examName = efName
@ -116,13 +113,15 @@ postEEditR tid ssh csh examn = do
deleteWhere [ InvitationFor ==. invRef @ExamCorrector eId, InvitationEmail /<-. invites ]
sinkInvitationsF examCorrectorInvitationConfig $ map (, eId, (InvDBDataExamCorrector, InvTokenDataExamCorrector)) invites
return insertRes
return . Just $ case insertRes of
Just _ -> addMessageI Error $ MsgExamNameTaken efName
Nothing -> do
addMessageI Success $ MsgExamEdited efName
redirect $ CExamR tid ssh csh efName EShowR
case insertRes of
Just _ -> addMessageI Error $ MsgExamNameTaken efName
Nothing -> do
addMessageI Success $ MsgExamEdited efName
redirect $ CExamR tid ssh csh efName EShowR
return (template, (editExamAct, (editExamWidget, editExamEnctype)))
sequence_ editExamAct
let heading = prependCourseTitle tid ssh csh . MsgExamEditHeading $ efName template

View File

@ -98,11 +98,14 @@ deriveJSON defaultOptions
} ''ExamOccurrenceForm
examForm :: Maybe ExamForm -> Form ExamForm
examForm template html = do
examForm :: ( MonadHandler m
, HandlerSite m ~ UniWorX
)
=> Maybe ExamForm -> (Html -> MForm m (FormResult ExamForm, Widget))
examForm template csrf = hoist liftHandler $ do
MsgRenderer mr <- getMsgRenderer
flip (renderAForm FormStandard) html $ ExamForm
flip (renderAForm FormStandard) csrf $ ExamForm
<$> areq ciField (fslpI MsgExamName (mr MsgExamName) & setTooltip MsgExamNameTip) (efName <$> template)
<*> aopt htmlField (fslI MsgExamDescription) (efDescription <$> template)
<* aformSection MsgExamFormTimes
@ -284,7 +287,11 @@ examPartsForm prev = wFormToAForm $ do
miIdent' :: Text
miIdent' = "exam-parts"
examFormTemplate :: Entity Exam -> DB ExamForm
examFormTemplate :: ( MonadHandler m
, HandlerSite m ~ UniWorX
, MonadThrow m
)
=> Entity Exam -> SqlPersistT m ExamForm
examFormTemplate (Entity eId Exam{..}) = do
examParts <- selectList [ ExamPartExam ==. eId ] []
occurrences <- selectList [ ExamOccurrenceExam ==. eId ] []
@ -342,7 +349,8 @@ examFormTemplate (Entity eId Exam{..}) = do
, efStaff = examStaff
}
examTemplate :: CourseId -> DB (Maybe ExamForm)
examTemplate :: MonadHandler m
=> CourseId -> SqlPersistT m (Maybe ExamForm)
examTemplate cid = runMaybeT $ do
newCourse <- MaybeT $ get cid
@ -393,7 +401,12 @@ examTemplate cid = runMaybeT $ do
}
validateExam :: (MonadHandler m, HandlerSite m ~ UniWorX) => CourseId -> Maybe Exam -> FormValidator ExamForm m ()
validateExam :: forall m.
( MonadHandler m
, HandlerSite m ~ UniWorX
, MonadThrow m
)
=> CourseId -> Maybe (Entity Exam) -> FormValidator ExamForm (SqlPersistT m) ()
validateExam cId oldExam = do
ExamForm{..} <- State.get
@ -404,6 +417,7 @@ validateExam cId oldExam = do
guardValidation MsgExamFinishedMustBeAfterEnd $ Just False /= ((>=) <$> efFinished <*> efEnd)
guardValidation MsgExamFinishedMustBeAfterStart $ Just False /= ((>=) <$> efFinished <*> efStart)
forM_ efOccurrences $ \ExamOccurrenceForm{..} -> do
guardValidation (MsgExamOccurrenceEndMustBeAfterStart eofName) $ NTop eofEnd >= NTop (Just eofStart)
guardValidation (MsgExamOccurrenceStartMustBeAfterExamStart eofName) $ NTop (Just eofStart) >= NTop efStart
@ -421,6 +435,28 @@ validateExam cId oldExam = do
guardValidation (MsgExamOccurrenceDuplicateName $ eofName a) $ ((/=) `on` eofName) a b
oldOccurrencesWithRegistrations <- for oldExam $ \(Entity eId _) -> lift . E.select . E.from $ \examOccurrence -> do
E.where_ $ examOccurrence E.^. ExamOccurrenceExam E.==. E.val eId
E.where_ . E.exists . E.from $ \examRegistration ->
E.where_ $ examRegistration E.^. ExamRegistrationOccurrence E.==. E.just (examOccurrence E.^. ExamOccurrenceId)
return ( examOccurrence E.^. ExamOccurrenceId
, examOccurrence E.^. ExamOccurrenceName
)
forM_ (join $ hoistMaybe oldOccurrencesWithRegistrations) $ \(E.Value eoId, E.Value eoName) ->
guardValidationM (MsgExamOccurrenceCannotBeDeletedDueToRegistrations eoName) . anyM (otoList efOccurrences) $ \ExamOccurrenceForm{..} -> (== Just eoId) <$> traverse decrypt eofId
oldPartsWithResults <- for oldExam $ \(Entity eId _) -> lift . E.select . E.from $ \examPart -> do
E.where_ $ examPart E.^. ExamPartExam E.==. E.val eId
E.where_ . E.exists . E.from $ \examPartResult ->
E.where_ $ examPartResult E.^. ExamPartResultExamPart E.==. examPart E.^. ExamPartId
return ( examPart E.^. ExamPartId
, examPart E.^. ExamPartNumber
)
forM_ (join $ hoistMaybe oldPartsWithResults) $ \(E.Value epId, E.Value epNumber) ->
guardValidationM (MsgExamPartCannotBeDeletedDueToResults epNumber) . anyM (otoList efExamParts) $ \ExamPartForm{..} -> (== Just epId) <$> traverse decrypt epfId
mSchool <- liftHandler . runDB . E.selectMaybe . E.from $ \(course `E.InnerJoin` school) -> do
E.on $ course E.^. CourseSchool E.==. school E.^. SchoolId
E.where_ $ course E.^. CourseId E.==. E.val cId
@ -429,7 +465,7 @@ validateExam cId oldExam = do
whenIsJust mSchool $ \(Entity _ School{..}) -> do
whenIsJust schoolExamMinimumRegisterBeforeStart $ \minSep -> do
let doValidation
| Just Exam{..} <- oldExam
| Just (Entity _ Exam{..}) <- oldExam
, not . fromMaybe True $ (>=) <$> examStart <*> (addUTCTime minSep <$> examRegisterFrom)
= warnValidation
| otherwise
@ -438,7 +474,7 @@ validateExam cId oldExam = do
. fromMaybe True $ (>=) <$> efStart <*> (addUTCTime minSep <$> efRegisterFrom)
whenIsJust schoolExamMinimumRegisterDuration $ \minDur -> do
let doValidation
| Just Exam{..} <- oldExam
| Just (Entity _ Exam{..}) <- oldExam
, not . fromMaybe True $ (>=) <$> examRegisterTo <*> (addUTCTime minDur <$> examRegisterFrom)
= warnValidation
| otherwise
@ -447,7 +483,7 @@ validateExam cId oldExam = do
. fromMaybe True $ (>=) <$> efRegisterTo <*> (addUTCTime minDur <$> efRegisterFrom)
when schoolExamRequireModeForRegistration $ do
let doValidation
| Just Exam{ examExamMode = ExamMode{..}, .. } <- oldExam
| Just (Entity _ Exam{ examExamMode = ExamMode{..}, .. }) <- oldExam
, or [ is _Nothing examAids
, is _Nothing examOnline
, is _Nothing examSynchronicity
@ -468,5 +504,5 @@ validateExam cId oldExam = do
warnValidation MsgExamModeSchoolDiscouraged . not $ evalExamModeDNF schoolExamDiscouragedModes efExamMode
unless (has (_Just . _examStaff . _Nothing) oldExam) $
unless (has (_Just . _entityVal . _examStaff . _Nothing) oldExam) $
guardValidation MsgExamStaffRequired $ isn't _Nothing efStaff

View File

@ -19,15 +19,13 @@ import qualified Data.Conduit.Combinators as C
getCExamNewR, postCExamNewR :: TermId -> SchoolId -> CourseShorthand -> Handler Html
getCExamNewR = postCExamNewR
postCExamNewR tid ssh csh = do
(cid, template) <- runDB $ do
(newExamAct, (newExamWidget, newExamEnctype)) <- runDBJobs $ do
cid <- getKeyBy404 $ TermSchoolCourseShort tid ssh csh
template <- examTemplate cid
return (cid, template)
((newExamResult, newExamWidget), newExamEnctype) <- runFormPost . validateForm (validateExam cid Nothing) $ examForm template
((newExamResult, newExamWidget), newExamEnctype) <- runFormPost . validateForm (validateExam cid Nothing) $ examForm template
formResult newExamResult $ \ExamForm{..} -> do
insertRes <- runDBJobs $ do
newExamAct <- formResultMaybe newExamResult $ \ExamForm{..} -> do
now <- liftIO getCurrentTime
insertRes <- insertUnique Exam
@ -95,12 +93,15 @@ postCExamNewR tid ssh csh = do
audit $ TransactionExamResultEdit examid courseParticipantUser
runConduit $ selectSource [ CourseParticipantCourse ==. cid, CourseParticipantState ==. CourseParticipantInactive True ] [] .| C.mapM_ recordNoShow
return insertRes
case insertRes of
Nothing -> addMessageI Error $ MsgExamNameTaken efName
Just _ -> do
addMessageI Success $ MsgExamCreated efName
redirect $ CourseR tid ssh csh CExamListR
return . Just $ case insertRes of
Nothing -> addMessageI Error $ MsgExamNameTaken efName
Just _ -> do
addMessageI Success $ MsgExamCreated efName
redirect $ CourseR tid ssh csh CExamListR
return (newExamAct, (newExamWidget, newExamEnctype))
sequence_ newExamAct
let heading = prependCourseTitle tid ssh csh MsgExamNew

View File

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

View File

@ -29,15 +29,16 @@ makePrisms ''ChangelogItemKind
classifyChangelogItem :: ChangelogItem -> ChangelogItemKind
classifyChangelogItem = \case
ChangelogHaskellCampusLogin -> ChangelogItemBugfix
ChangelogTooltipsWithoutJavascript -> ChangelogItemBugfix
ChangelogButtonsWorkWithoutJavascript -> ChangelogItemBugfix
ChangelogTableFormsWorkAfterAjax -> ChangelogItemBugfix
ChangelogPassingByPointsWorks -> ChangelogItemBugfix
ChangelogErrorMessagesForTableItemVanish -> ChangelogItemBugfix
ChangelogExamAchievementParticipantDuplication -> ChangelogItemBugfix
ChangelogFormsTimesReset -> ChangelogItemBugfix
_other -> ChangelogItemFeature
ChangelogHaskellCampusLogin -> ChangelogItemBugfix
ChangelogTooltipsWithoutJavascript -> ChangelogItemBugfix
ChangelogButtonsWorkWithoutJavascript -> ChangelogItemBugfix
ChangelogTableFormsWorkAfterAjax -> ChangelogItemBugfix
ChangelogPassingByPointsWorks -> ChangelogItemBugfix
ChangelogErrorMessagesForTableItemVanish -> ChangelogItemBugfix
ChangelogExamAchievementParticipantDuplication -> ChangelogItemBugfix
ChangelogFormsTimesReset -> ChangelogItemBugfix
ChangelogAllocationCourseAcceptSubstitutesFixed -> ChangelogItemBugfix
_other -> ChangelogItemFeature
changelogItemDays :: Map ChangelogItem Day
changelogItemDays = Map.fromListWithKey (\k d1 d2 -> bool (error $ "Duplicate changelog days for " <> show k) d1 $ d1 /= d2)

View File

@ -822,14 +822,14 @@ and2M, or2M :: Monad m => m Bool -> m Bool -> m Bool
and2M ma mb = ifM ma mb (return False)
or2M ma = ifM ma (return True)
andM, orM :: (Foldable f, Monad m) => f (m Bool) -> m Bool
andM = Fold.foldr and2M (return True)
orM = Fold.foldr or2M (return False)
andM, orM :: (MonoFoldable mono, Element mono ~ (m Bool), Monad m) => mono -> m Bool
andM = ofoldl' and2M (return True)
orM = ofoldl' or2M (return False)
-- | Short-circuiting monady any
allM, anyM :: (Functor f, Foldable f, Monad m) => f a -> (a -> m Bool) -> m Bool
allM xs f = andM $ fmap f xs
anyM xs f = orM $ fmap f xs
allM, anyM :: (MonoFoldable mono, Monad m) => mono -> (Element mono -> m Bool) -> m Bool
allM xs f = andM . fmap f $ otoList xs
anyM xs f = orM . fmap f $ otoList xs
ofoldr1M, ofoldl1M :: (MonoFoldable mono, Monad m) => (Element mono -> Element mono -> m (Element mono)) -> NonNull mono -> m (Element mono)
ofoldr1M f (otoList -> x:xs) = foldrM f x xs

View File

@ -1,3 +1,5 @@
{-# LANGUAGE BangPatterns #-}
module Utils.Files
( sinkFile, sinkFiles
, sinkFile', sinkFiles'
@ -35,6 +37,8 @@ import qualified Database.Esqueleto.Utils as E
import Data.Conduit.Algorithms.FastCDC (fastCDC)
import Control.Monad.Trans.Cont
sinkFileDB :: (MonadCatch m, MonadHandler m, HandlerSite m ~ UniWorX, MonadUnliftIO m, BackendCompatible SqlBackend (YesodPersistBackend UniWorX), YesodPersist UniWorX)
=> Bool -- ^ Replace? Use only in serializable transaction
@ -43,14 +47,16 @@ sinkFileDB :: (MonadCatch m, MonadHandler m, HandlerSite m ~ UniWorX, MonadUnlif
sinkFileDB doReplace fileContentContent = do
chunkingParams <- getsYesod $ view _appFileChunkingParams
let sinkChunk fileContentChunkContent = do
let sinkChunk !fileContentChunkContent = do
fileChunkLockTime <- liftIO getCurrentTime
fileChunkLockInstance <- getsYesod appInstanceID
observeSunkChunk StorageDB $ olength fileContentChunkContent
tellM $ Set.singleton <$> insert FileChunkLock{ fileChunkLockHash = fileContentChunkHash, .. }
existsChunk <- lift $ exists [FileContentChunkHash ==. fileContentChunkHash]
let setContentBased = updateWhere [FileContentChunkHash ==. fileContentChunkHash] [FileContentChunkContentBased =. fileContentChunkContentBased]
if | existsChunk -> lift setContentBased
| otherwise -> lift . handleIfSql isUniqueConstraintViolation (const setContentBased) $
@ -144,7 +150,22 @@ sinkFile File{ fileContent = Nothing, .. } = return FileReference
, fileReferenceModified = fileModified
}
sinkFile File{ fileContent = Just fileContentContent, .. } = do
(unsealConduitT -> fileContentContent', isEmpty) <- fileContentContent $$+ is _Nothing <$> C.peekE
chunk <- liftIO newEmptyTMVarIO
sourceAsync <- allocateLinkedAsync . runConduit $ fileContentContent .| C.mapM_ (atomically . putTMVar chunk)
isEmpty <- atomically $
False <$ readTMVar chunk
<|> True <$ waitSTM sourceAsync
let fileContentContent' = evalContT . callCC $ \finishConsume -> forever $ do
inpChunk <- atomically $
Right <$> takeTMVar chunk
<|> Left <$> waitCatchSTM sourceAsync
case inpChunk of
Right inpChunk' -> lift $ yield inpChunk'
Left (Left exc) -> throwM exc
Left (Right res) -> finishConsume res
fileContentHash <- if
| not isEmpty -> maybeT (sinkFileDB False fileContentContent') $ sinkFileMinio fileContentContent'

View File

@ -62,6 +62,16 @@ $newline never
^{formatTimeW SelFormatDateTime deadline}
$nothing
_{MsgAllocationNextSubstitutesDeadlineNever}
<dt .deflist__dt>
_{MsgAllocationFreeCapacity} #
^{iconInvisible}
<dd .deflist__dd>
$maybe freeCap <- freeCapacity
#{freeCap}
$if freeCap <= 0
\ ^{iconOK}
$nothing
$maybe fromT <- allocationRegisterByCourse
<dt .deflist__dt>
_{MsgAllocationRegisterByCourseFrom}

View File

@ -24,6 +24,15 @@ $if isAdmin
_{MsgCourseAllocationCourseAcceptsSubstitutesNever}
$if allocationCourseAcceptSubstitutes >= Just now
\ ^{iconOK}
<p>
_{MsgCourseAllocationCourseParticipants}:
$maybe capacity <- courseCapacity
\ _{MsgCourseMembersCountLimited partCount capacity}
$if partCount < capacity
\ ^{iconProblem}
$nothing
\ _{MsgCourseMembersCount partCount}
\ ^{iconProblem}
$if hasApplicationTemplate || is _Just courseApplicationsInstructions
<div .allocation-course__instructions-label .allocation__label>
_{MsgCourseApplicationInstructionsApplication}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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