feat(sheets): submission groups & rework sheet form

This commit is contained in:
Gregor Kleen 2020-04-28 14:31:27 +02:00
parent 6d00410682
commit 57f1ce9265
23 changed files with 547 additions and 114 deletions

View File

@ -98,6 +98,9 @@ export class InteractiveFieldset {
if (this._isCheckbox()) {
matches = this.conditionalInput.checked === true;
} else if (this._isRadio()) {
const radios = Array.from(this.conditionalInput.querySelectorAll('input[type=radio]'));
matches = radios.some(radio => radio.checked && radio.value === this.conditionalValue);
} else {
matches = this.conditionalInput.value === this.conditionalValue;
}
@ -112,4 +115,8 @@ export class InteractiveFieldset {
_isCheckbox() {
return this.conditionalInput.getAttribute('type') === 'checkbox';
}
_isRadio() {
return !!this.conditionalInput.querySelector('input[type=radio]');
}
}

View File

@ -60,13 +60,21 @@
grid-column: 1
.form-group--has-error
background-color: rgba(255, 0, 0, 0.1)
background-color: rgba(140, 7, 7, 0.05)
.form-group-label
border-left: 2px solid var(--color-error)
align-self: stretch
padding-left: 7px
input, textarea
border-color: var(--color-error) !important
.form-error
display: block
font-weight: 600
color: var(--color-error)
margin: 7px 0
.form-error
display: none

View File

@ -331,10 +331,12 @@ SheetFormType: Wertung & Abgabe
SheetFormTimes: Zeiten
SheetFormFiles: Dateien
SheetErrVisibility: "Beginn Abgabezeitraum" muss nach "Sichbar für Teilnehmer ab" liegen
SheetErrDeadlineEarly: "Ende Abgabezeitraum" muss nach "Beginn Abzeitraum" liegen
SheetErrVisibility: "Aktiv ab/Beginn Abgabezeitraum" muss nach "Sichbar für Teilnehmer ab" liegen
SheetErrDeadlineEarly: "Aktiv bis/Ende Abgabezeitraum" muss nach "Aktiv ab/Beginn Abzeitraum" liegen
SheetErrHintEarly: Hinweise dürfen erst nach Beginn des Abgabezeitraums herausgegeben werden
SheetErrSolutionEarly: Lösungen dürfen erst nach Ende der Abgabezeitraums herausgegeben werden
SheetErrVisibleWithoutActive: Wird "Sichtbar für Teilnehmer ab" angegeben, muss auch "Aktiv ab/Beginn Abgabezeitraum" angegeben werden
SheetWarnNoActiveTo: "Aktiv bis/Ende Abgabezeitraum" sollte stets angegeben werden
SheetNoCurrent: Es gibt momentan kein aktives Übungsblatt.
SheetNoOldUnassigned: Alle Abgaben inaktiver Blätter sind bereits einen Korrektor zugeteilt.
SheetsUnassignable name@Text: Momentan keine Abgaben zuteilbar für #{name}
@ -474,6 +476,8 @@ UnauthorizedLDAP: Angegebener Nutzer meldet sich nicht mit Campus-Kennung an.
UnauthorizedPWHash: Angegebener Nutzer meldet sich nicht mit Uni2work-Kennung an.
UnauthorizedExternalExamListNotEmpty: Liste von externen Prüfungen ist nicht leer
UnauthorizedExternalExamLecturer: Sie sind nicht als Prüfer für diese externe Prüfung eingetragen
UnauthorizedSubmissionSubmissionGroup: Sie sind nicht Mitglied in einer der registrierten Abgabegruppen, die an dieser Abgabe beteiligt sind
UnauthorizedSheetSubmissionGroup: Sie sind nicht Mitglied in einer registrierten Abgabegruppe
UnauthorizedPasswordResetToken: Dieses Authorisierungs-Token kann nicht mehr zum Passwort ändern benutzt werden
@ -793,6 +797,8 @@ CorrectorSubmissions: Abgabe extern mit Pseudonym
UserSubmissions: Direkte Abgabe in Uni2work
BothSubmissions: Abgabe direkt in Uni2work & extern mit Pseudonym
BothSubmissionsTip: Abgabe kann, nach Wahl des Teilnehmers, entweder direkt in Uni2work oder extern mit Pseudonym erfolgen
SheetCorrectorSubmissionsTip: Abgabe erfolgt über ein Uni2work-externes Verfahren (zumeist in Papierform durch Einwurf) unter Angabe eines persönlichen Pseudonyms. Korrektoren können mithilfe des Pseudonyms später Korrekturergebnisse in Uni2work eintragen, damit Sie sie einsehen können.
SubmissionNoUploadExpected: Es ist keine Abgabe von Dateien vorgesehen.
@ -973,6 +979,7 @@ SheetTypeBonus grading@SheetGrading: Bonus
SheetTypeNormal grading@SheetGrading: Normal
SheetTypeInformational grading@SheetGrading: Ohne Anrechnung
SheetTypeNotGraded: Keine Korrektur
SheetTypeInfoNormalLecturer: Normale Blätter werden zur Berechnung eines etwaigen Klausurbonus herangezogen. Der Bonus kann sowohl anhand der zu bestehenden Blätter als auch der erreichbaren Maximalpunktzahl automatisch oder manuell berechnet werden.
SheetTypeInfoNotGraded: Keine Korrektur bedeutet, dass es gar kein Feedback gibt.
SheetTypeInfoBonus: Bonus Blätter zählen normal, erhöhen aber nicht die maximal erreichbare Punktzahl bzw. Anzahl zu bestehender Blätter.
SheetTypeInfoInformational: Blätter ohne Anrechnung werden nirgends angerechnet, die Bewertung durch den Korrektor dient lediglich zur Information der Teilnehmer.
@ -1376,6 +1383,7 @@ AuthTagIsPWHash: Nutzer meldet sich mit Uni2work-Kennung an
AuthTagAuthentication: Nutzer ist angemeldet, falls erforderlich
AuthTagRead: Zugriff ist nur lesend
AuthTagWrite: Zugriff ist i.A. schreibend
AuthTagSubmissionGroup: Nutzer ist Mitglied in registrierter Abgabegruppe
DeleteCopyStringIfSure n@Int: Wenn Sie sich sicher sind, dass Sie #{pluralDE n "das obige Objekt" "obige Objekte"} unwiderbringlich löschen möchten, schreiben Sie bitte zunächst den angezeigten Text ab.
DeletePressButtonIfSure n@Int: Wenn Sie sich sicher sind, dass Sie #{pluralDE n "das obige Objekt" "obige Objekte"} unwiderbringlich löschen möchten, bestätigen Sie dies bitte durch Drücken des untigen Knopfes.
@ -1772,6 +1780,13 @@ CourseUserExamsDeregistered count@Int64: Teilnehmer von #{show count} #{pluralDE
CourseUserNoExamsDeregistered: Teilnehmer ist zu keiner der gewählten Prüfungen angemeldet
CourseUserExamsResultSet count@Int64: Ergebnis zu #{show count} #{pluralDE count "Prüfung" "Prüfungen"} erfolgreich angepasst
CourseUserExamResultDoesNotMatchMode examn@ExamName: Gewähtes Ergebnis passt nicht zu Bewertungsmodus von Prüfung „#{examn}“.
CourseUserSetSubmissionGroup: Feste Abgabegruppe setzen/entfernen
CourseUsersSubmissionGroupSetNew count@Int64: #{show count} Benutzer der festen Abgabegruppe zugeordnet
CourseUsersSubmissionGroupUnset count@Int64: #{show count} Benutzer aus ihren jeweiligen festen Abgabegruppen entfernt
SubmissionGroup: Feste Abgabegruppe
NoSubmissionGroup: Keine feste Abgabegruppe
SubmissionGroupEmptyIsUnsetTip: Leer lassen um Benutzer aus den jeweiligen Abgabegruppen ersatzlos zu entfernen
ExamUserSynchronised: Synchronisiert
ExamUserSyncOfficeName: Name
@ -1854,6 +1869,7 @@ CsvColumnUserRegistration: Zeitpunkt der Anmeldung zum Kurs (ISO 8601)
CsvColumnUserNote: Notizen zum Teilnehmer
CsvColumnUserTutorial: Tutorien zu denen der Teilnehmer angemeldet ist, als Semikolon (;) separierte Liste. Für Registrierungs-Gruppen unter den Tutorien gibt es jeweils eine weitere Spalte. Die Registrierungs-Gruppen-Spalten enthalten jeweils maximal ein Tutorium pro Teilnehmer. Sind alle Tutorien in Registrierungs-Gruppen, so gibt es keine Spalte "tutorial".
CsvColumnUserExam: Prüfungen zu denen der Teilnehmer angemeldet ist, als Semikolon (;) separierte Liste.
CsvColumnUserSubmissionGroup: Registrierte Abgabegruppe
CsvColumnExamOfficeExamUserOccurrenceStart: Prüfungstermin (ISO 8601)
@ -2487,4 +2503,6 @@ BearerTokenOverrideStart: Startzeitpunkt
BearerTokenOverrideStartTip: Wird kein Startzeitpunkt angegeben, wird bei Verwendung des Tokens nur der Ablaufzeitpunkt überprüft.
FaqTitle: Häufig gestellte Fragen
AdditionalFaqs: Weitere häufig gestellte Fragen
AdditionalFaqs: Weitere häufig gestellte Fragen
MultiActionUnknownAction: In einem von einem Eingabefeld abhängigen Formular wurde ein Wert gewählt, für den kein Formular verfügbar ist

View File

@ -334,6 +334,8 @@ SheetErrVisibility: "Submission period start" must be after "Visible from"
SheetErrDeadlineEarly: "Submission period end" must be after "Submission period start"
SheetErrHintEarly: "Hint from" must be after "Submission period start"
SheetErrSolutionEarly: "Solution from" must be after "Submission period end"
SheetErrVisibleWithoutActive: If “Visible from (for participants)” is specified “Active from/Submission period start” must also be specified
SheetWarnNoActiveTo: “Active to/Submission period end” should always be specified
SheetNoCurrent: There is no currently active exercise sheet
SheetNoOldUnassigned: All submissions for inactive sheets are already assigned to correctors.
SheetsUnassignable name: Submission for #{name} may not currently be assigned to correctors.
@ -472,6 +474,8 @@ UnauthorizedLDAP: Specified user does not log in with their campus account.
UnauthorizedPWHash: Specified user does not log in with an Uni2work-account.
UnauthorizedExternalExamListNotEmpty: List of external exams is not empty
UnauthorizedExternalExamLecturer: You are not an associated person for this external exam
UnauthorizedSubmissionSubmissionGroup: You are not member in any of the submission groups for this submission
UnauthorizedSheetSubmissionGroup: You are not member in any submission group
UnauthorizedPasswordResetToken: This authorisation-token may no longer be used to change passwords
@ -790,6 +794,8 @@ CorrectorSubmissions: External submission via pseudonym
UserSubmissions: Direct submission in Uni2work
BothSubmissions: Submission either directly in Uni2work or externally via pseudonym
BothSubmissionsTip: Participants may choose to submit either directly in Uni2work or externally via a pseudonym
SheetCorrectorSubmissionsTip: Submissions are expected to be handed in through some Uni2work-external procedure (usually on paper) marked with your personal pseudonym. Correctors can, using the pseudonym, register the marking in Uni2work for you to review.
SubmissionNoUploadExpected: No upload of files expected.
@ -973,6 +979,7 @@ SheetTypeBonus grading: Bonus
SheetTypeNormal grading: Normal
SheetTypeInformational grading: Informational
SheetTypeNotGraded: Not marked
SheetTypeInfoNormalLecturer: Normal sheets are used to calculate exam bonuses. Bonuses may be calculated from the number of sheets that can be passed or the maximum number of points achievable either manually or automatically.
SheetTypeInfoNotGraded: "Not marked" means that there will be no feedback at all.
SheetTypeInfoBonus: Sheets marked "bonus" count normally but do not increase either the maximum number of points or the count of sheets that can be passed.
SheetTypeInfoInformational: Sheets marked "informational" do not counted anywhere. They are marked only as feedback for participants.
@ -1375,6 +1382,7 @@ AuthTagIsPWHash: User logs in using their Uni2work-internal account
AuthTagAuthentication: User is authenticated
AuthTagRead: Access is read only
AuthTagWrite: Access might write
AuthTagSubmissionGroup: User is part of a submission group
DeleteCopyStringIfSure n: If you are sure that you want to permanently delete the #{pluralEN n "object" "objects"} listed below, please copy the shown text.
DeletePressButtonIfSure n: If you are sure that you want to permanently delete the #{pluralEN n "object" "objects"} listed below, please confirm the action by pressing the button.
@ -1771,6 +1779,13 @@ CourseUserExamsDeregistered count: Successfully deregistered participant from #{
CourseUserNoExamsDeregistered: Participant is not registered for any of the selected exams
CourseUserExamsResultSet count: Successfully adjusted the participant's result for #{show count} #{pluralEN count "exam" "exams"}
CourseUserExamResultDoesNotMatchMode examn: The chosen result does not match the grading mode for exam “#{examn}”
CourseUserSetSubmissionGroup: Set/Unset registered submission group
CourseUsersSubmissionGroupSetNew count: Successfully added #{show count} #{pluralEN count "user" "users"} to submission group
CourseUsersSubmissionGroupUnset count: Successfully removed #{show count} #{pluralEN count "user" "users"} from their #{pluralEN count "submission group" "respective submission groups"}
SubmissionGroup: Registered submission group
NoSubmissionGroup: No registered submission group
SubmissionGroupEmptyIsUnsetTip: Leave empty to remove users from their respective submission groups
ExamUserSynchronised: Synchronised
ExamUserSyncOfficeName: Name
@ -1853,6 +1868,7 @@ CsvColumnUserRegistration: Time of participant's enrollment (ISO 8601)
CsvColumnUserNote: Course notes for the participant
CsvColumnUserTutorial: Tutorials which the user is registered for, separated by semicolon (;). For each registration group among the tutorials there is a separate column. The registration group columns contain at most one tutorial per participant. If every tutorial has a registration group there is no column "tutorial".
CsvColumnUserExam: Exams which the user is registered for, separated by semicolon (;).
CsvColumnUserSubmissionGroup: Registered submission group
CsvColumnExamOfficeExamUserOccurrenceStart: Exam occurrence (ISO 8601)
@ -2488,3 +2504,5 @@ BearerTokenOverrideStartTip: If no start time is given, only the expiration time
FaqTitle: Frequently asked questions
AdditionalFaqs: More frequently asked questions
MultiActionUnknownAction: In a form dependent on the value of a field a value was given for which no form is available

View File

@ -140,7 +140,8 @@ for templateDirectory (templates/i18n/**/*(FN)); do
for ext (${templateExtensions}); do
for lang (${requiredLangs}); do
for prefix (${templatePrefixes}); do
for prefixQ (${(q)templatePrefixes}); do
prefix=${(Q)prefixQ}
# printf ">> %s %s %s\n" ${prefix} ${lang} ${ext}
foundLang=1
@ -156,7 +157,7 @@ for templateDirectory (templates/i18n/**/*(FN)); do
if [[ $foundLang -ne 0 ]]; then
templateDifference=1
[[ $fix != 0 ]] && printf "%s: %s*.%s (%s)\n" $templateDirectory $prefix $ext $lang
[[ $fix != 0 ]] && printf "%s: %s*.%s (%s)\n" "$templateDirectory" "$prefix" "$ext" "$lang"
if [[ $fix == 0 ]]; then
./translate.hs dir $templateDirectory && templateDifference=0

6
routes
View File

@ -151,15 +151,15 @@
/edit SEditR GET POST
/delete SDelR GET POST
/subs SSubsR GET POST -- for lecturer only
!/subs/new SubmissionNewR GET POST !timeANDcourse-registeredANDuser-submissions
!/subs/new SubmissionNewR GET POST !timeANDcourse-registeredANDuser-submissionsANDsubmission-group
!/subs/own SubmissionOwnR GET !free -- just redirect
!/subs/assign SAssignR GET POST !lecturerANDtime
/subs/#CryptoFileNameSubmission SubmissionR:
/ SubShowR GET POST !ownerANDtimeANDuser-submissions !ownerANDread !correctorANDread
/ SubShowR GET POST !ownerANDtimeANDuser-submissionsANDsubmission-group !ownerANDread !correctorANDread
/delete SubDelR GET POST !ownerANDtimeANDuser-submissions
/assign SubAssignR GET POST !lecturerANDtime
/correction CorrectionR GET POST !corrector !ownerANDreadANDrated
/invite SInviteR GET POST !ownerANDtimeANDuser-submissions
/invite SInviteR GET POST !ownerANDtimeANDuser-submissionsANDsubmission-group
!/#SubmissionFileType SubArchiveR GET !owner !corrector
!/#SubmissionFileType/*FilePath SubDownloadR GET !owner !corrector
/iscorrector SIsCorrR GET !corrector -- Route is used to check for corrector access to this sheet

View File

@ -619,6 +619,29 @@ tagAccessPredicate AuthTutorControl = APDB $ \_ route _ -> case route of
guard tutorialTutorControlled
return Authorized
r -> $unsupportedAuthPredicate AuthTutorControl r
tagAccessPredicate AuthSubmissionGroup = APDB $ \mAuthId route _ -> case route of
CSubmissionR _ _ _ _ cID _ -> maybeT (unauthorizedI MsgUnauthorizedSubmissionSubmissionGroup) $ do
smId <- catchIfMaybeT (const True :: CryptoIDError -> Bool) $ decrypt cID
groups <- $cachedHereBinary cID . lift . fmap (Set.fromList . fmap E.unValue) . E.select . E.from $ \(submissionGroupUser `E.InnerJoin` submissionUser) -> do
E.on $ submissionGroupUser E.^. SubmissionGroupUserUser E.==. submissionUser E.^. SubmissionUserUser
E.where_ $ submissionUser E.^. SubmissionUserSubmission E.==. E.val smId
return $ submissionGroupUser E.^. SubmissionGroupUserSubmissionGroup
unless (Set.null groups) $ do
uid <- hoistMaybe mAuthId
guardM . lift $ exists [SubmissionGroupUserUser ==. uid, SubmissionGroupUserSubmissionGroup <-. Set.toList groups]
return Authorized
CSheetR tid ssh csh sheetn _ -> maybeT (unauthorizedI MsgUnauthorizedSheetSubmissionGroup) $ do
course <- $cachedHereBinary (tid, ssh, csh) . MaybeT . getKeyBy $ TermSchoolCourseShort tid ssh csh
Entity _ Sheet{..} <- $cachedHereBinary (course, sheetn) . MaybeT . getBy $ CourseSheet course sheetn
when (is _RegisteredGroups sheetGrouping) $ do
uid <- hoistMaybe mAuthId
guardM . lift . E.selectExists . E.from $ \(submissionGroup `E.InnerJoin` submissionGroupUser) -> do
E.on $ submissionGroupUser E.^. SubmissionGroupUserSubmissionGroup E.==. submissionGroup E.^. SubmissionGroupId
E.where_ $ submissionGroup E.^. SubmissionGroupCourse E.==. E.val course
E.&&. submissionGroupUser E.^. SubmissionGroupUserUser E.==. E.val uid
return Authorized
r -> $unsupportedAuthPredicate AuthSubmissionGroup r
tagAccessPredicate AuthTime = APDB $ \mAuthId route _ -> case route of
CApplicationR tid ssh csh _ _ -> maybeT (unauthorizedI MsgUnauthorizedApplicationTime) $ do
course <- $cachedHereBinary (tid, ssh, csh) . MaybeT . getKeyBy $ TermSchoolCourseShort tid ssh csh

View File

@ -26,7 +26,7 @@ import Handler.Exam.List (mkExamTable)
getCShowR :: TermId -> SchoolId -> CourseShorthand -> Handler Html
getCShowR tid ssh csh = do
mbAid <- maybeAuthId
(cid,course,schoolName,participants,registration,lecturers,assistants,correctors,tutors,mAllocation,hasApplicationTemplate,mApplication,news,events) <- runDB . maybeT notFound $ do
(cid,course,schoolName,participants,registration,lecturers,assistants,correctors,tutors,mAllocation,hasApplicationTemplate,mApplication,news,events,submissionGroup) <- runDB . maybeT notFound $ do
[(E.Entity cid course, E.Value schoolName, E.Value participants, fmap entityVal -> registration)]
<- lift . E.select . E.from $
\((school `E.InnerJoin` course) `E.LeftOuterJoin` participant) -> do
@ -91,8 +91,19 @@ getCShowR tid ssh csh = do
events' <- fmap (sortOn $ courseEventTime . entityVal) . lift $ selectList [ CourseEventCourse ==. cid ] []
events <- mapM (\(Entity evId ev) -> (, ev) <$> encrypt evId) events'
hasSubmissionGroups <- lift . E.selectExists . E.from $ \(submissionGroupUser `E.InnerJoin` submissionGroup) -> do
E.on $ submissionGroupUser E.^. SubmissionGroupUserSubmissionGroup E.==. submissionGroup E.^. SubmissionGroupId
E.where_ $ submissionGroup E.^. SubmissionGroupCourse E.==. E.val cid
submissionGroup' <- lift . for mbAid $ \uid ->
fmap (listToMaybe . fmap E.unValue) . E.select . E.from $ \(submissionGroupUser `E.InnerJoin` submissionGroup) -> do
E.on $ submissionGroupUser E.^. SubmissionGroupUserSubmissionGroup E.==. submissionGroup E.^. SubmissionGroupId
E.where_ $ submissionGroup E.^. SubmissionGroupCourse E.==. E.val cid
E.where_ $ submissionGroupUser E.^. SubmissionGroupUserUser E.==. E.val uid
return $ submissionGroup E.^. SubmissionGroupName
let submissionGroup = guardOnM (hasSubmissionGroups && is _Just registration) submissionGroup'
return (cid,course,schoolName,participants,registration,lecturers,assistants,correctors,tutors,mAllocation,hasApplicationTemplate,mApplication,news,events)
return (cid,course,schoolName,participants,registration,lecturers,assistants,correctors,tutors,mAllocation,hasApplicationTemplate,mApplication,news,events,submissionGroup)
let mDereg' = maybe id min (allocationOverrideDeregister =<< mAllocation) <$> courseDeregisterUntil course
mDereg <- traverse (formatTime SelFormatDateTime) mDereg'

View File

@ -38,6 +38,9 @@ type UserTableExpr = ( E.SqlExpr (Entity User)
`E.InnerJoin` E.SqlExpr (Maybe (Entity StudyDegree))
`E.InnerJoin` E.SqlExpr (Maybe (Entity StudyTerms))
)
`E.LeftOuterJoin` ( E.SqlExpr (Maybe (Entity SubmissionGroup))
`E.InnerJoin` E.SqlExpr (Maybe (Entity SubmissionGroupUser))
)
-- forceUserTableType :: (UserTableExpr -> a) -> (UserTableExpr -> a)
-- forceUserTableType = id
@ -45,36 +48,44 @@ type UserTableExpr = ( E.SqlExpr (Entity User)
-- Sql-Getters for this query, used for sorting and filtering (cannot be lenses due to being Esqueleto expressions)
-- This ought to ease refactoring the query
queryUser :: UserTableExpr -> E.SqlExpr (Entity User)
queryUser = $(sqlIJproj 2 1) . $(sqlLOJproj 3 1)
queryUser = $(sqlIJproj 2 1) . $(sqlLOJproj 4 1)
queryParticipant :: UserTableExpr -> E.SqlExpr (Entity CourseParticipant)
queryParticipant = $(sqlIJproj 2 2) . $(sqlLOJproj 3 1)
queryParticipant = $(sqlIJproj 2 2) . $(sqlLOJproj 4 1)
queryUserNote :: UserTableExpr -> E.SqlExpr (Maybe (Entity CourseUserNote))
queryUserNote = $(sqlLOJproj 3 2)
queryUserNote = $(sqlLOJproj 4 2)
queryFeaturesStudy :: UserTableExpr -> E.SqlExpr (Maybe (Entity StudyFeatures))
queryFeaturesStudy = $(sqlIJproj 3 1) . $(sqlLOJproj 3 3)
queryFeaturesStudy = $(sqlIJproj 3 1) . $(sqlLOJproj 4 3)
queryFeaturesDegree :: UserTableExpr -> E.SqlExpr (Maybe (Entity StudyDegree))
queryFeaturesDegree = $(sqlIJproj 3 2) . $(sqlLOJproj 3 3)
queryFeaturesDegree = $(sqlIJproj 3 2) . $(sqlLOJproj 4 3)
queryFeaturesField :: UserTableExpr -> E.SqlExpr (Maybe (Entity StudyTerms))
queryFeaturesField = $(sqlIJproj 3 3) . $(sqlLOJproj 3 3)
queryFeaturesField = $(sqlIJproj 3 3) . $(sqlLOJproj 4 3)
querySubmissionGroup :: UserTableExpr -> E.SqlExpr (Maybe (Entity SubmissionGroup))
querySubmissionGroup = $(sqlIJproj 2 1) . $(sqlLOJproj 4 4)
userTableQuery :: CourseId -> UserTableExpr -> E.SqlQuery ( E.SqlExpr (Entity User)
, E.SqlExpr (E.Value UTCTime)
, E.SqlExpr (E.Value (Maybe (Key CourseUserNote)))
, StudyFeaturesDescription')
userTableQuery cid ((user `E.InnerJoin` participant) `E.LeftOuterJoin` note `E.LeftOuterJoin` studyFeatures) = do
, StudyFeaturesDescription'
, E.SqlExpr (Maybe (Entity SubmissionGroup))
)
userTableQuery cid ((user `E.InnerJoin` participant) `E.LeftOuterJoin` note `E.LeftOuterJoin` studyFeatures `E.LeftOuterJoin` (subGroup `E.InnerJoin` subGroupUser)) = do
-- Note that order of E.on for nested joins is seemingly right-to-left, ignoring nesting paranthesis
E.on $ subGroup E.?. SubmissionGroupId E.==. subGroupUser E.?. SubmissionGroupUserSubmissionGroup
E.on $ subGroupUser E.?. SubmissionGroupUserUser E.==. E.just (user E.^. UserId)
E.&&. subGroup E.?. SubmissionGroupCourse E.==. E.just (E.val cid)
features <- studyFeaturesQuery' (participant E.^. CourseParticipantField) studyFeatures
E.on $ (note E.?. CourseUserNoteUser E.==. E.just (participant E.^. CourseParticipantUser))
E.&&. (note E.?. CourseUserNoteCourse E.==. E.just (E.val cid))
E.on $ participant E.^. CourseParticipantUser E.==. user E.^. UserId
E.where_ $ participant E.^. CourseParticipantCourse E.==. E.val cid
return (user, participant E.^. CourseParticipantRegistration, note E.?. CourseUserNoteId, features)
return (user, participant E.^. CourseParticipantRegistration, note E.?. CourseUserNoteId, features, subGroup)
type UserTableData = DBRow ( Entity User
@ -83,6 +94,7 @@ type UserTableData = DBRow ( Entity User
, (Maybe StudyFeatures, Maybe StudyDegree, Maybe StudyTerms)
, ([Entity Tutorial], Map (CI Text) (Maybe (Entity Tutorial)))
, [Entity Exam]
, Maybe (Entity SubmissionGroup)
)
instance HasEntity UserTableData User where
@ -109,11 +121,14 @@ _userTutorials = _dbrOutput . _5
_userExams :: Lens' UserTableData [Entity Exam]
_userExams = _dbrOutput . _6
_userSubmissionGroup :: Traversal' UserTableData (Entity SubmissionGroup)
_userSubmissionGroup = _dbrOutput . _7 . _Just
colUserComment :: IsDBTable m c => TermId -> SchoolId -> CourseShorthand -> Colonnade Sortable UserTableData (DBCell m c)
colUserComment tid ssh csh =
sortable (Just "note") (i18nCell MsgCourseUserNote)
$ \DBRow{ dbrOutput=(Entity uid _, _, mbNoteKey, _, _, _) } ->
$ \DBRow{ dbrOutput=(Entity uid _, _, mbNoteKey, _, _, _, _) } ->
maybeEmpty mbNoteKey $ const $
anchorCellM (courseLink <$> encrypt uid) (hasComment True)
where
@ -158,6 +173,10 @@ colUserDegreeShort = sortable (Just "degree-short") (i18nCell MsgStudyFeatureDeg
colUserSex' :: IsDBTable m c => Colonnade Sortable UserTableData (DBCell m c)
colUserSex' = colUserSex $ hasUser . _userSex
colUserSubmissionGroup :: IsDBTable m c => Colonnade Sortable UserTableData (DBCell m c)
colUserSubmissionGroup = sortable (Just "submission-group") (i18nCell MsgSubmissionGroup) $
foldMap (cell . toWidget) . preview (_userSubmissionGroup . _entityVal . _submissionGroupName)
data UserTableCsvStudyFeature = UserTableCsvStudyFeature
{ csvUserField :: Text
@ -173,6 +192,7 @@ data UserTableCsv = UserTableCsv
, csvUserMatriculation :: Maybe Text
, csvUserEmail :: CI Email
, csvUserStudyFeatures :: Either (Maybe UserTableCsvStudyFeature) (Set UserTableCsvStudyFeature)
, csvUserSubmissionGroup :: Maybe SubmissionGroupName
, csvUserRegistration :: UTCTime
, csvUserNote :: Maybe Html
, csvUserTutorials :: ([TutorialName], Map (CI Text) (Maybe TutorialName))
@ -199,6 +219,8 @@ instance Csv.ToNamedRecord UserTableCsv where
in [ "study-features" Csv..= featsStr
]
++
[ "submission-group" Csv..= csvUserSubmissionGroup
] ++
[ let tutsStr = Text.intercalate "; " . map CI.original $ csvUserTutorials ^. _1
in "tutorial" Csv..= tutsStr
] ++
@ -221,6 +243,7 @@ instance CsvColumnsExplained UserTableCsv where
, single "field" MsgCsvColumnUserField
, single "degree" MsgCsvColumnUserDegree
, single "semester" MsgCsvColumnUserSemester
, single "submission-group" MsgCsvColumnUserSubmissionGroup
, single "tutorial" MsgCsvColumnUserTutorial
, single "exams" MsgCsvColumnUserExam
, single "registration" MsgCsvColumnUserRegistration
@ -255,6 +278,7 @@ data CourseUserAction = CourseUserSendMail
| CourseUserDeregister
| CourseUserRegisterTutorial
| CourseUserRegisterExam
| CourseUserSetSubmissionGroup
deriving (Eq, Ord, Enum, Bounded, Read, Show, Generic, Typeable)
instance Universe CourseUserAction
@ -272,6 +296,9 @@ data CourseUserActionData = CourseUserSendMailData
| CourseUserRegisterExamData
{ registerExam :: (ExamId, Maybe ExamOccurrenceId)
}
| CourseUserSetSubmissionGroupData
{ setSubmissionGroup :: Maybe SubmissionGroupName
}
deriving (Eq, Ord, Read, Show, Generic, Typeable)
@ -298,7 +325,7 @@ makeCourseUserTable cid acts restrict colChoices psValidator csvColumns = do
dbtStyle = def { dbsFilterLayout = defaultDBSFilterLayout }
dbtSQLQuery q = userTableQuery cid q <* E.where_ (restrict q)
dbtRowKey = queryUser >>> (E.^. UserId)
dbtProj = traverse $ \(user, E.Value registrationTime , E.Value userNoteId, (feature,degree,terms)) -> do
dbtProj = traverse $ \(user, E.Value registrationTime , E.Value userNoteId, (feature,degree,terms), subGroup) -> do
tuts'' <- selectList [ TutorialParticipantUser ==. entityKey user, TutorialParticipantTutorial <-. map entityKey tutorials ] []
exams' <- selectList [ ExamRegistrationUser ==. entityKey user ] []
let
@ -306,7 +333,7 @@ makeCourseUserTable cid acts restrict colChoices psValidator csvColumns = do
tuts' = filter (\(Entity tutId _) -> any ((== tutId) . tutorialParticipantTutorial . entityVal) tuts'') tutorials
tuts = foldr (\tut@(Entity _ Tutorial{..}) -> maybe (over _1 $ cons tut) (over _2 . flip (Map.insertWith (<|>)) (Just tut)) tutorialRegGroup) ([], Map.fromSet (const Nothing) regGroups) tuts'
exs = filter (\(Entity eId _) -> any ((== eId) . examRegistrationExam . entityVal) exams') exams
return (user, registrationTime, userNoteId, (entityVal <$> feature, entityVal <$> degree, entityVal <$> terms), tuts, exs)
return (user, registrationTime, userNoteId, (entityVal <$> feature, entityVal <$> degree, entityVal <$> terms), tuts, exs, subGroup)
dbtColonnade = colChoices
dbtSorting = mconcat
[ single $ sortUserNameLink queryUser -- slower sorting through clicking name column header
@ -340,6 +367,7 @@ makeCourseUserTable cid acts restrict colChoices psValidator csvColumns = do
E.where_ $ examRegistration E.^. ExamRegistrationUser E.==. user E.^. UserId
return . E.min_ $ exam E.^. ExamName
)
, single $ ("submission-group", SortColumn $ querySubmissionGroup >>> (E.?. SubmissionGroupName))
]
where single = uncurry Map.singleton
dbtFilter = mconcat
@ -378,6 +406,7 @@ makeCourseUserTable cid acts restrict colChoices psValidator csvColumns = do
)
-- , ("course-registration", error "TODO") -- TODO
-- , ("course-user-note", error "TODO") -- TODO
, single $ ("submission-group", FilterColumn $ E.mkContainsFilter $ querySubmissionGroup >>> (E.?. SubmissionGroupName))
]
where single = uncurry Map.singleton
dbtFilterUI mPrev = mconcat $
@ -387,6 +416,7 @@ makeCourseUserTable cid acts restrict colChoices psValidator csvColumns = do
[ fltrUserSexUI mPrev | showSex ] ++
[ prismAForm (singletonFilter "degree") mPrev $ aopt textField (fslI MsgStudyFeatureDegree)
, prismAForm (singletonFilter "field") mPrev $ aopt textField (fslI MsgCourseStudyFeature)
, prismAForm (singletonFilter "submission-group") mPrev $ aopt textField (fslI MsgSubmissionGroup)
, prismAForm (singletonFilter "tutorial") mPrev $ aopt textField (fslI MsgCourseUserTutorial)
, prismAForm (singletonFilter "exam") mPrev $ aopt textField (fslI MsgCourseUserExam)
]
@ -444,6 +474,7 @@ makeCourseUserTable cid acts restrict colChoices psValidator csvColumns = do
, csvUserSemester = studyFeaturesSemester
, csvUserType = studyFeaturesType
}
<*> preview (_userSubmissionGroup . _entityVal . _submissionGroupName)
<*> view _userTableRegistration
<*> userNote
<*> (over (_2.traverse._Just) (tutorialName . entityVal) . over (_1.traverse) (tutorialName . entityVal) <$> view _userTutorials)
@ -490,6 +521,11 @@ postCUsersR tid ssh csh = do
E.on $ E.just (exam E.^. ExamId) E.==. examOccurrence E.?. ExamOccurrenceExam
E.where_ $ exam E.^. ExamCourse E.==. E.val cid
return (exam, examOccurrence)
hasSubmissionGroups <- E.selectExists . E.from $ \(submissionGroup `E.InnerJoin` submissionGroupUser `E.InnerJoin` courseParticipant) -> do
E.on $ courseParticipant E.^. CourseParticipantUser E.==. submissionGroupUser E.^. SubmissionGroupUserUser
E.&&. courseParticipant E.^. CourseParticipantCourse E.==. submissionGroup E.^. SubmissionGroupCourse
E.on $ submissionGroupUser E.^. SubmissionGroupUserSubmissionGroup E.==. submissionGroup E.^. SubmissionGroupId
E.where_ $ submissionGroup E.^. SubmissionGroupCourse E.==. E.val cid
let exams = nubOn entityKey $ examOccurrencesPerExam ^.. folded . _1
let colChoices = mconcat $ catMaybes
[ pure $ dbSelect (applying _2) id (return . view (hasEntity . _entityKey))
@ -500,6 +536,7 @@ postCUsersR tid ssh csh = do
, pure $ colUserDegreeShort
, pure $ colUserField
, pure $ colUserSemester
, guardOn hasSubmissionGroups colUserSubmissionGroup
, guardOn hasTutorials $ colUserTutorials tid ssh csh
, guardOn hasExams $ colUserExams tid ssh csh
, pure $ sortable (Just "registration") (i18nCell MsgRegisteredSince) (dateCell . view _userTableRegistration)
@ -532,6 +569,7 @@ postCUsersR tid ssh csh = do
optionInternalValue = entityKey
optionDisplay = CI.original $ examName entityVal
return Option{..}
submissionGroupOpts = optionsPersist [SubmissionGroupCourse ==. cid] [Asc SubmissionGroupName] submissionGroupName <&> fmap (submissionGroupName . entityVal)
acts = mconcat
[ singletonMap CourseUserSendMail $ pure CourseUserSendMailData
, singletonMap CourseUserRegisterTutorial $ CourseUserRegisterTutorialData <$>
@ -540,6 +578,8 @@ postCUsersR tid ssh csh = do
Nothing
, singletonMap CourseUserRegisterExam $ CourseUserRegisterExamData <$>
multiActionAOpts examOccActs examActs (fslI MsgCourseExam) Nothing
, singletonMap CourseUserSetSubmissionGroup $ CourseUserSetSubmissionGroupData . assertM (not . Text.null . CI.original) <$>
aopt (textField & cfStrip & cfCI & addDatalist submissionGroupOpts) (fslI MsgSubmissionGroup & setTooltip MsgSubmissionGroupEmptyIsUnsetTip) Nothing
, if
| mayRegister
-> singletonMap CourseUserDeregister $ courseUserDeregisterForm cid
@ -589,6 +629,25 @@ postCUsersR tid ssh csh = do
return mempty
addMessageI Success $ MsgCourseUsersExamRegistered nrReg
redirect $ CourseR tid ssh csh CUsersR
(CourseUserSetSubmissionGroupData{ setSubmissionGroup = Just setSubmissionGroup }, selectedUsers) -> do
Sum nrSet <- runDB $ do
Entity gId _ <- upsert (SubmissionGroup cid setSubmissionGroup) [ SubmissionGroupName =. setSubmissionGroup ]
flip foldMapM selectedUsers $ \uid ->
fmap (maybe mempty . const $ Sum 1) . insertUnique $ SubmissionGroupUser gId uid
addMessageI Success $ MsgCourseUsersSubmissionGroupSetNew nrSet
redirect $ CourseR tid ssh csh CUsersR
(CourseUserSetSubmissionGroupData{ setSubmissionGroup = Nothing }, selectedUsers) -> do
nrUnset <- runDB $ do
nrUnset <- E.deleteCount . E.from $ \submissionGroupUser ->
E.where_ $ submissionGroupUser E.^. SubmissionGroupUserUser `E.in_` E.valList (Set.toList selectedUsers)
E.&&. E.subSelectForeign submissionGroupUser SubmissionGroupUserSubmissionGroup (E.^. SubmissionGroupCourse) E.==. E.val cid
E.delete . E.from $ \submissionGroup ->
E.where_ $ submissionGroup E.^. SubmissionGroupCourse E.==. E.val cid
E.&&. E.not_ (E.exists . E.from $ \submissionGroupUser -> E.where_ $ submissionGroupUser E.^. SubmissionGroupUserSubmissionGroup E.==. submissionGroup E.^. SubmissionGroupId)
return nrUnset
addMessageI Success $ MsgCourseUsersSubmissionGroupUnset nrUnset
redirect $ CourseR tid ssh csh CUsersR
let headingLong = [whamlet|_{MsgMenuCourseMembers} #{courseName course} #{tid}|]
headingShort = prependCourseTitle tid ssh csh MsgCourseMembers
siteLayout headingLong $ do

View File

@ -43,6 +43,8 @@ import Text.Hamlet (ihamlet)
import Data.Time.Clock.System (systemEpochDay)
import qualified Control.Monad.State.Class as State
{-
* Implement Handlers
@ -61,9 +63,9 @@ data SheetForm = SheetForm
, sfActiveTo :: Maybe UTCTime
, sfHintFrom :: Maybe UTCTime
, sfSolutionFrom :: Maybe UTCTime
, sfType :: SheetType
, sfGrouping :: SheetGroup
, sfSubmissionMode :: SubmissionMode
, sfGrouping :: SheetGroup
, sfType :: SheetType
, sfAutoDistribute :: Bool
, sfMarkingText :: Maybe Html
, sfAnonymousCorrection :: Bool
@ -92,13 +94,13 @@ getFtIdMap sId = do
return $ partitionFileType [(t,i)|(E.Value t, E.Value i) <- allfIds]
makeSheetForm :: Maybe SheetId -> Maybe SheetForm -> Form SheetForm
makeSheetForm msId template = identifyForm FIDsheet $ \html -> do
makeSheetForm msId template = identifyForm FIDsheet . validateForm validateSheet $ \html -> do
oldFileIds <- (return.) <$> case msId of
Nothing -> return $ partitionFileType mempty
(Just sId) -> liftHandler $ runDB $ getFtIdMap sId
mr'@(MsgRenderer mr) <- getMsgRenderer
MsgRenderer mr <- getMsgRenderer
ctime <- ceilingQuarterHour <$> liftIO getCurrentTime
(result, widget) <- flip (renderAForm FormStandard) html $ SheetForm
flip (renderAForm FormStandard) html $ SheetForm
<$> areq (textField & cfStrip & cfCI) (fslI MsgSheetName) (sfName <$> template)
<*> aopt htmlField (fslI MsgSheetDescription) (sfDescription <$> template)
<* aformSection MsgSheetFormFiles
@ -120,31 +122,26 @@ makeSheetForm msId template = identifyForm FIDsheet $ \html -> do
<*> aopt utcTimeField (fslpI MsgSheetSolutionFrom (mr MsgSheetSolutionFromPlaceholder)
& setTooltip MsgSheetSolutionFromTip) (sfSolutionFrom <$> template)
<* aformSection MsgSheetFormType
<*> sheetTypeAFormReq (fslI MsgSheetType
& setTooltip (uniworxMessages [MsgSheetTypeInfoBonus, MsgSheetTypeInfoInformational, MsgSheetTypeInfoNotGraded]))
(sfType <$> template)
<*> sheetGroupAFormReq (fslI MsgSheetGroup) (sfGrouping <$> template)
<*> submissionModeForm ((sfSubmissionMode <$> template) <|> pure (SubmissionMode False . Just $ UploadAny True defaultExtensionRestriction))
<*> sheetGroupAFormReq (fslI MsgSheetGroup) ((sfGrouping <$> template) <|> pure NoGroups)
<*> sheetTypeAFormReq (fslI MsgSheetType) (sfType <$> template)
<*> apopt checkBoxField (fslI MsgAutoAssignCorrs) (sfAutoDistribute <$> template)
<*> aopt htmlField (fslI MsgSheetMarking) (sfMarkingText <$> template)
<*> apopt checkBoxField (fslI MsgSheetAnonymousCorrection & setTooltip MsgSheetAnonymousCorrectionTip) (sfAnonymousCorrection <$> template)
<*> correctorForm (fromMaybe mempty $ sfCorrectors <$> template)
return $ case result of
FormSuccess sheetResult
| errorMsgs <- validateSheet mr' sheetResult
, not $ null errorMsgs ->
(FormFailure errorMsgs, widget)
_ -> (result, widget)
where
validateSheet :: MsgRenderer -> SheetForm -> [Text]
validateSheet (MsgRenderer {..}) (SheetForm{..}) =
[ msg | (False, msg) <-
[ ( NTop sfVisibleFrom <= NTop sfActiveFrom , render MsgSheetErrVisibility)
, ( NTop sfActiveFrom <= NTop sfActiveTo , render MsgSheetErrDeadlineEarly)
, ( NTop sfHintFrom >= NTop sfActiveFrom , render MsgSheetErrHintEarly)
, ( NTop sfSolutionFrom >= NTop sfActiveTo , render MsgSheetErrSolutionEarly)
] ]
validateSheet :: FormValidator SheetForm Handler ()
validateSheet = do
SheetForm{..} <- State.get
guardValidation MsgSheetErrVisibility $ NTop sfVisibleFrom <= NTop sfActiveFrom
guardValidation MsgSheetErrDeadlineEarly $ NTop sfActiveFrom <= NTop sfActiveTo
guardValidation MsgSheetErrHintEarly $ NTop sfHintFrom >= NTop sfActiveFrom
guardValidation MsgSheetErrSolutionEarly $ NTop sfSolutionFrom >= NTop sfActiveTo
guardValidation MsgSheetErrVisibleWithoutActive $ is _Just sfActiveFrom || is _Nothing sfVisibleFrom
warnValidation MsgSheetWarnNoActiveTo $ is _Just sfActiveTo || is _Nothing sfActiveFrom
getSheetCurrentR :: TermId -> SchoolId -> CourseShorthand -> Handler Html
getSheetCurrentR tid ssh csh = do

View File

@ -358,7 +358,34 @@ submissionHelper tid ssh csh shn mcid = do
, isLecturer
, not isLecturer
)
(Nothing, _) -> return (csheet, Set.empty, [], maySubmit, isLecturer, not isLecturer) -- TODO: Return registered group members
(Nothing, RegisteredGroups) -> do
buddies <- E.select . E.from $ \(submissionGroup `E.InnerJoin` user) -> do
E.on . E.exists . E.from $ \submissionGroupUser ->
E.where_ $ submissionGroupUser E.^. SubmissionGroupUserUser E.==. user E.^. UserId
E.where_ $ submissionGroup E.^. SubmissionGroupCourse E.==. E.val sheetCourse
E.where_ . E.exists . E.from $ \submissionGroupUser ->
E.where_ $ submissionGroupUser E.^. SubmissionGroupUserUser E.==. E.val uid
E.&&. submissionGroupUser E.^. SubmissionGroupUserSubmissionGroup E.==. submissionGroup E.^. SubmissionGroupId
E.where_ $ user E.^. UserId E.!=. E.val uid
E.where_ . E.not_ . E.exists . E.from $ \(submissionUser `E.InnerJoin` submission) -> do
E.on $ submissionUser E.^. SubmissionUserSubmission E.==. submission E.^. SubmissionId
E.where_ $ submission E.^. SubmissionSheet E.==. E.val shid
E.&&. submissionUser E.^. SubmissionUserUser E.==. user E.^. UserId
E.orderBy [E.asc $ user E.^. UserEmail]
return $ user E.^. UserId
return ( csheet
, buddies
& map (Right . E.unValue)
& Set.fromList
, []
, maySubmit
, isLecturer
, not isLecturer
)
(Nothing, _) -> return (csheet, Set.empty, [], maySubmit, isLecturer, not isLecturer)
(Just smid, _) -> do
void $ submissionMatchesSheet tid ssh csh shn (fromJust mcid)
@ -478,16 +505,24 @@ submissionHelper tid ssh csh shn mcid = do
-- Determine new submission users
subUsers <- if
| isLecturer -> return adhocMembers
| otherwise -> do
| RegisteredGroups <- sheetGrouping -> do
-- Determine members of pre-registered group
groupUids <- fmap (setFromList . map (Right . E.unValue)) . E.select . E.from $ \(submissionGroupUser `E.InnerJoin` submissionGroup `E.InnerJoin` submissionGroupUser') -> do
E.on $ submissionGroup E.^. SubmissionGroupId E.==. submissionGroupUser' E.^. SubmissionGroupUserSubmissionGroup
E.on $ submissionGroupUser E.^. SubmissionGroupUserSubmissionGroup E.==. submissionGroup E.^. SubmissionGroupId
E.where_ $ submissionGroupUser E.^. SubmissionGroupUserUser E.==. E.val uid
E.&&. submissionGroup E.^. SubmissionGroupCourse E.==. E.val sheetCourse
E.where_ . E.not_ . E.exists . E.from $ \(submissionUser `E.InnerJoin` submission) -> do
E.on $ submissionUser E.^. SubmissionUserSubmission E.==. submission E.^. SubmissionId
E.where_ $ submission E.^. SubmissionSheet E.==. E.val shid
E.&&. submissionUser E.^. SubmissionUserUser E.==. submissionGroupUser' E.^. SubmissionGroupUserUser
E.where_ $ submission E.^. SubmissionId E.!=. E.val smid
return $ submissionGroupUser' E.^. SubmissionGroupUserUser
-- SubmissionUser for all group members (pre-registered & ad-hoc)
return $ groupUids `Set.union` adhocMembers
return $ Set.insert (Right uid) groupUids
| otherwise -> return adhocMembers
-- Since invitations carry no data we only need to consider changes to
-- the set of users/invited emails

View File

@ -16,6 +16,8 @@ import Handler.Utils.DateTime
import Handler.Utils.Widgets
import Handler.Utils.I18n
import Import
import Data.Char (chr, ord)
import qualified Data.Char as Char
@ -239,6 +241,34 @@ multiAction' :: forall action a.
-> (Html -> MForm Handler (FormResult a, [FieldView UniWorX]))
multiAction' minp acts = multiActionOpts' minp acts (optionsF $ Map.keysSet acts)
multiActionField :: forall action a.
Ord action
=> (Field Handler action -> FieldSettings UniWorX -> Maybe action -> MForm Handler (FormResult action, FieldView UniWorX))
-> Map action (AForm Handler a)
-> (Field Handler action, action -> Maybe Text, action -> Maybe (SomeMessage UniWorX))
-> FieldSettings UniWorX
-> Maybe action
-> (Html -> MForm Handler (FormResult a, [FieldView UniWorX]))
multiActionField minp acts (actField, actExternal, actMessage) fs@FieldSettings{..} defAction csrf = do
(actionRes, actionView) <- minp (checkBool (`Map.member` acts) MsgMultiActionUnknownAction actField) fs defAction
results <- mapM (fmap (over _2 ($ [])) . aFormToForm) acts
MsgRenderer mr <- getMsgRenderer
let actionResults = view _1 <$> results
actionViews = Map.foldrWithKey accViews [] results
accViews :: forall b. action -> (b, [FieldView UniWorX]) -> [FieldView UniWorX] -> [FieldView UniWorX]
accViews act
| Just optionExternalValue <- actExternal act
, Just (mr -> optionDisplay) <- actMessage act
= flip mappend . over (mapped . _fvInput) (\w -> $(widgetFile "widgets/multi-action/multi-action")) . snd
| otherwise
= flip const
return ((actionResults Map.!) =<< actionRes, over _fvInput (mappend $ toWidget csrf) actionView : actionViews)
multiActionOpts' :: forall action a.
Ord action
=> (Field Handler action -> FieldSettings UniWorX -> Maybe action -> MForm Handler (FormResult action, FieldView UniWorX))
@ -254,22 +284,12 @@ multiActionOpts' minp acts mActsOpts fs@FieldSettings{..} defAction csrf = do
, olReadExternal = assertM (flip Map.member acts) . olReadExternal actsOpts
}
acts' = Map.filterWithKey (\a _ -> any ((== a) . optionInternalValue) $ olOptions actsOpts') acts
(actionRes, actionView) <- minp (selectField $ return actsOpts') fs defAction
results <- mapM (fmap (over _2 ($ [])) . aFormToForm) acts'
let actionResults = view _1 <$> results
actionViews = Map.foldrWithKey accViews [] results
actOption act = listToMaybe . filter (\Option{..} -> optionInternalValue == act) $ olOptions actsOpts'
actExternal = fmap optionExternalValue . actOption
actMessage = fmap (SomeMessage . optionDisplay) . actOption
accViews :: forall b. action -> (b, [FieldView UniWorX]) -> [FieldView UniWorX] -> [FieldView UniWorX]
accViews act
| Option{..} : _ <- filter ((== act) . optionInternalValue) $ olOptions actsOpts'
= flip mappend . over (mapped . _fvInput) (\w -> $(widgetFile "widgets/multi-action/multi-action")) . snd
| otherwise
= flip const
return ((actionResults Map.!) =<< actionRes, over _fvInput (mappend $ toWidget csrf) actionView : actionViews)
multiActionField minp acts' (selectField $ return actsOpts', actExternal, actMessage) fs defAction csrf
multiActionA :: (RenderMessage UniWorX action, PathPiece action, Ord action)
=> Map action (AForm Handler a)
@ -316,6 +336,88 @@ multiActionMOpts :: Ord action
-> (Html -> MForm Handler (FormResult a, Widget))
multiActionMOpts acts opts fSettings defAction = renderAForm FormStandard $ multiActionAOpts acts opts fSettings defAction
-------------------------
-- Explained selection --
-------------------------
explainedSelectionField :: forall m a.
( MonadHandler m
, HandlerSite m ~ UniWorX
, Eq a
)
=> Maybe (SomeMessage UniWorX, Maybe Widget) -- ^ Label for none option
-> Handler ([(Option a, Maybe Widget)], Text -> Maybe a)
-> Field m a
explainedSelectionField optMsg' mkOpts = Field{..}
where
fieldEnctype = UrlEncoded
fieldParse ts _ = do
(_, parser) <- liftHandler mkOpts
if
| t : _ <- ts
, Just t' <- parser t
-> return . Right $ Just t'
| t : _ <- ts
, null t
-> return $ Right Nothing
| t : _ <- ts
-> return . Left . SomeMessage $ MsgInvalidEntry t
| otherwise
-> return $ Right Nothing
fieldView theId name attrs val isReq = do
(opts, _) <- liftHandler mkOpts
let optMsg = guardOnM (not isReq) optMsg'
inputId optExternal = [st|#{theId}__input--#{optExternal}|]
matchesVal Nothing = is _Left val
matchesVal (Just x) = val == Right x
$(widgetFile "widgets/explained-selection-field")
explainOptionList :: forall a.
Handler (OptionList a)
-> (a -> MaybeT Handler Widget)
-> Handler ([(Option a, Maybe Widget)], Text -> Maybe a)
explainOptionList ol mkExplanation = do
OptionList{..} <- ol
olOptions' <- forM olOptions $ \opt@Option{..} -> (opt, ) <$> runMaybeT (mkExplanation optionInternalValue)
return (olOptions', olReadExternal)
explainedMultiAction' :: forall action a.
Ord action
=> (Field Handler action -> FieldSettings UniWorX -> Maybe action -> MForm Handler (FormResult action, FieldView UniWorX))
-> Map action (AForm Handler a)
-> Handler ([(Option action, Maybe Widget)], Text -> Maybe action)
-> FieldSettings UniWorX
-> Maybe action
-> (Html -> MForm Handler (FormResult a, [FieldView UniWorX]))
explainedMultiAction' minp acts mActsOpts fs@FieldSettings{..} defAction csrf = do
(actsOpts, actsReadExternal) <- liftHandler mActsOpts
let actsOpts' = filter (flip Map.member acts . optionInternalValue . view _1) actsOpts
actsReadExternal' = assertM (flip Map.member acts) . actsReadExternal
acts' = Map.filterWithKey (\a _ -> any ((== a) . optionInternalValue . view _1) actsOpts') acts
actOption act = listToMaybe . filter (\Option{..} -> optionInternalValue == act) $ view _1 <$> actsOpts'
actExternal = fmap optionExternalValue . actOption
actMessage = fmap (SomeMessage . optionDisplay) . actOption
multiActionField minp acts' (explainedSelectionField Nothing $ return (actsOpts', actsReadExternal'), actExternal, actMessage) fs defAction csrf
explainedMultiAction :: forall action a.
Ord action
=> Map action (AForm Handler a)
-> Handler ([(Option action, Maybe Widget)], Text -> Maybe action)
-> FieldSettings UniWorX
-> Maybe action
-> (Html -> MForm Handler (FormResult a, [FieldView UniWorX]))
explainedMultiAction = explainedMultiAction' mpopt
explainedMultiActionA :: forall action a.
Ord action
=> Map action (AForm Handler a)
-> Handler ([(Option action, Maybe Widget)], Text -> Maybe action)
-> FieldSettings UniWorX
-> Maybe action
-> AForm Handler a
explainedMultiActionA acts mActsOpts fSettings defAction = formToAForm $ explainedMultiAction acts mActsOpts fSettings defAction mempty
------------
-- Fields --
@ -555,7 +657,7 @@ uploadModeForm prev = multiActionA actions (fslI MsgSheetUploadMode) (classifyUp
submissionModeForm :: Maybe SubmissionMode -> AForm Handler SubmissionMode
submissionModeForm prev = multiActionA actions (fslI MsgSheetSubmissionMode) $ classifySubmissionMode <$> prev
submissionModeForm prev = explainedMultiActionA actions opts (fslI MsgSheetSubmissionMode) $ classifySubmissionMode <$> prev
where
actions :: Map SubmissionModeDescr (AForm Handler SubmissionMode)
actions = Map.fromList
@ -573,6 +675,12 @@ submissionModeForm prev = multiActionA actions (fslI MsgSheetSubmissionMode) $ c
)
]
opts = explainOptionList optionsFinite $ \case
SubmissionModeNone -> mzero
SubmissionModeCorrector -> return $(i18nWidgetFile "submission-mode-explanation/corrector")
SubmissionModeUser -> return $(i18nWidgetFile "submission-mode-explanation/user")
SubmissionModeBoth -> return $ i18n MsgBothSubmissionsTip
data ExamBonusRule' = ExamBonusManual'
| ExamBonusPoints'
deriving (Eq, Ord, Read, Show, Enum, Bounded, Generic, Typeable)
@ -847,7 +955,7 @@ nullaryPathPiece ''SheetType' (camelToPathPiece . dropSuffix "'")
embedRenderMessage ''UniWorX ''SheetType' ("SheetType" <>)
data SheetGroup' = NoGroups' | Arbitrary' | RegisteredGroups'
data SheetGroup' = Arbitrary' | RegisteredGroups' | NoGroups'
deriving (Eq, Ord, Read, Show, Enum, Bounded)
instance Universe SheetGroup'
@ -875,9 +983,9 @@ sheetGradingAFormReq fs template = multiActionA selOptions fs (classify' <$> tem
sheetTypeAFormReq :: FieldSettings UniWorX -> Maybe SheetType -> AForm Handler SheetType
sheetTypeAFormReq fs template = multiActionA selOptions fs (classify' <$> template)
sheetTypeAFormReq fs template = explainedMultiActionA acts opts fs (classify' <$> template)
where
selOptions = Map.fromList
acts = Map.fromList
[ ( Normal', Normal <$> gradingReq )
, ( Bonus' , Bonus <$> gradingReq )
, ( Informational', Informational <$> gradingReq )
@ -886,6 +994,12 @@ sheetTypeAFormReq fs template = multiActionA selOptions fs (classify' <$> templa
gradingReq = sheetGradingAFormReq (fslI MsgSheetGrading
& setTooltip MsgSheetGradingInfo) (template >>= preview _grading)
opts = explainOptionList optionsFinite $ \case
Normal' -> return $ i18n MsgSheetTypeInfoNormalLecturer
Bonus' -> return $ i18n MsgSheetTypeInfoBonus
Informational' -> return $ i18n MsgSheetTypeInfoInformational
NotGraded' -> return $ i18n MsgSheetTypeInfoNotGraded
classify' :: SheetType -> SheetType'
classify' = \case
Bonus {} -> Bonus'
@ -894,15 +1008,19 @@ sheetTypeAFormReq fs template = multiActionA selOptions fs (classify' <$> templa
NotGraded -> NotGraded'
sheetGroupAFormReq :: FieldSettings UniWorX -> Maybe SheetGroup -> AForm Handler SheetGroup
sheetGroupAFormReq fs template = multiActionA selOptions fs (classify' <$> template)
sheetGroupAFormReq fs template = explainedMultiActionA acts opts fs (classify' <$> template)
where
selOptions = Map.fromList
acts = Map.fromList
[ ( Arbitrary', Arbitrary
<$> apreq (natFieldI MsgGroupSizeNotNatural) (fslI MsgSheetGroupMaxGroupsize & noValidate) (preview _maxParticipants =<< template)
)
, ( RegisteredGroups', pure RegisteredGroups )
, ( NoGroups', pure NoGroups )
]
opts = explainOptionList optionsFinite $ \case
Arbitrary' -> return $(i18nWidgetFile "sheet-grouping-explanation/arbitrary")
RegisteredGroups' -> return $(i18nWidgetFile "sheet-grouping-explanation/registered")
NoGroups' -> mzero
classify' :: SheetGroup -> SheetGroup'
classify' = \case
Arbitrary _ -> Arbitrary'
@ -1516,47 +1634,6 @@ csvOptionsForm mPrev = hoistAForm liftHandler $ CsvOptions
<$> csvFormatOptionsForm (fslI MsgCsvFormatOptions & setTooltip MsgCsvOptionsTip) (csvFormat <$> mPrev)
<*> apopt checkBoxField (fslI MsgCsvTimestamp & setTooltip MsgCsvTimestampTip) (csvTimestamp <$> mPrev)
explainedSelectionField :: forall m a.
( MonadHandler m
, HandlerSite m ~ UniWorX
, Eq a
)
=> Maybe (SomeMessage UniWorX, Maybe Widget) -- ^ Label for none option
-> Handler ([(Option a, Maybe Widget)], Text -> Maybe a)
-> Field m a
explainedSelectionField optMsg' mkOpts = Field{..}
where
fieldEnctype = UrlEncoded
fieldParse ts _ = do
(_, parser) <- liftHandler mkOpts
if
| t : _ <- ts
, Just t' <- parser t
-> return . Right $ Just t'
| t : _ <- ts
, null t
-> return $ Right Nothing
| t : _ <- ts
-> return . Left . SomeMessage $ MsgInvalidEntry t
| otherwise
-> return $ Right Nothing
fieldView theId name attrs val isReq = do
(opts, _) <- liftHandler mkOpts
let optMsg = guardOnM (not isReq) optMsg'
inputId optExternal = [st|#{theId}__input--#{optExternal}|]
matchesVal Nothing = is _Left val
matchesVal (Just x) = val == Right x
$(widgetFile "widgets/explained-selection-field")
explainOptionList :: forall a.
Handler (OptionList a)
-> (a -> MaybeT Handler Widget)
-> Handler ([(Option a, Maybe Widget)], Text -> Maybe a)
explainOptionList ol mkExplanation = do
OptionList{..} <- ol
olOptions' <- forM olOptions $ \opt@Option{..} -> (opt, ) <$> runMaybeT (mkExplanation optionInternalValue)
return (olOptions', olReadExternal)
courseSelectForm :: forall ident handler.
( PathPiece ident

View File

@ -71,6 +71,7 @@ data AuthTag -- sortiert nach gewünschter Reihenfolge auf /authpreds, d.h. Prä
| AuthRated
| AuthUserSubmissions
| AuthCorrectorSubmissions
| AuthSubmissionGroup
| AuthCapacity
| AuthRegisterGroup
| AuthEmpty

View File

@ -208,9 +208,9 @@ deriveJSON defaultOptions
}''UploadMode
derivePersistFieldJSON ''UploadMode
data UploadModeDescr = UploadModeNone
| UploadModeAny
data UploadModeDescr = UploadModeAny
| UploadModeSpecific
| UploadModeNone
deriving (Eq, Ord, Read, Show, Enum, Bounded, Generic, Typeable)
instance Universe UploadModeDescr
instance Finite UploadModeDescr
@ -237,10 +237,10 @@ deriveJSON defaultOptions
} ''SubmissionMode
derivePersistFieldJSON ''SubmissionMode
data SubmissionModeDescr = SubmissionModeNone
| SubmissionModeCorrector
data SubmissionModeDescr = SubmissionModeCorrector
| SubmissionModeUser
| SubmissionModeBoth
| SubmissionModeNone
deriving (Eq, Ord, Read, Show, Enum, Bounded, Generic, Typeable)
instance Universe SubmissionModeDescr
instance Finite SubmissionModeDescr

View File

@ -165,6 +165,15 @@ $# $if NTop (Just 0) < NTop (courseCapacity course)
_{MsgCourseApplicationTemplateApplication}
$else
_{MsgCourseApplicationTemplateRegistration}
$maybe mGroup <- submissionGroup
<dt .deflist__dt>
_{MsgSubmissionGroup}
<dd .deflist__dd>
$maybe groupName <- mGroup
#{groupName}
$nothing
<i>
_{MsgNoSubmissionGroup}
$if registrationOpen || isJust registration
<dt .deflist__dt>
_{MsgCourseRegistration}

View File

@ -0,0 +1,20 @@
$newline never
Teilnehmer dürfen bei Anlegen der Abgabe selbstständig ihre #
Gruppenmitglieder angeben.
<br>
Teilnehmer haben hierzu die Möglichkeit Einladungen per E-Mail #
verschicken zu lassen und können andere Teilnehmer, mit denen sie #
bereits einmal zuvor in diesem Kurs abgegeben haben, auch direkt #
angeben.
<br>
Es kann eine maximale Gruppengröße festgelegt werden.
<br>
Teilnehmer müssen nicht notwendigerweise in den gleichen arbiträren #
Gruppen abgeben wie für ein vorhergehendes Übungsblatt.

View File

@ -0,0 +1,18 @@
$newline never
Participants may freely specify their group members when submitting.
<br>
Participants can have Uni2work send invitations via email or specify #
other participants directly if they have submitted together before in #
this course.
<br>
A maximum group size may be specified.
<br>
Participants need not submit in the same groups as for any previous #
exercise sheet.

View File

@ -0,0 +1,18 @@
$newline never
Nur Teilnehmer, die, auf der Liste der Kursteilnehmer, einer festen #
Abgabegruppe zugewiesen wurden, können abgeben. #
Teilnehmer können stets nur in maximal einer festen Abgabegruppe #
gleichzeitig sein.
<br>
Es kann stets nur für alle aktuellen Mitglieder der eigenen #
Abgabegruppe abgegeben werden.
<br>
Wird die Zuordnung von Teilnehmern auf feste Abgabegruppen nach einer #
erfolgten Abgabe noch angepasst, hat dies keinen Einfluss auf die #
Abgebenden dieser konkreten Abgabe.

View File

@ -0,0 +1,23 @@
$newline never
Only participants that are members of a registered submission group #
may submit. #
Membership in registered submission groups can be configured from the #
list of course participants. #
Participants can only be a member in at most one submission group at a #
time.
<br>
Participants can only submit for their entire submission group at #
once.
<br>
If submission group memberships are edited after a submission has been #
made existing associations between participants and their submissions #
remain unchanged. #
New submission group members are not added to existing submissions.

View File

@ -0,0 +1,25 @@
$newline never
Teilnehmer können sich von Uni2work ein Pseudonym zufällig wählen #
lassen (das Pseudonym ist spezifisch für dieses Übungsblatt).
<br>
Es ist dann vorgesehen, dass die Teilnehmer ihre Abgabe in einem #
externen System (z.B. auf Papier) nur mit ihrem Pseudonym #
kennzeichnen. #
So kann die Anonymität der Korrektor bewahrt werden.
<br>
Korrektoren können dann anhand der ihnen auf den Abgaben übermittelten #
Pseudonyme die Abgaben in Uni2work registrieren.
<br>
Für die so registrierten Abgaben kann der jeweilige Korrektor dann #
ganz regulär Punkte und Feedback geben. #
Die Anrechnung kann genauso wie bei Abgaben direkt in Uni2work (also, #
wenn gewünscht, mit automatischer Berechnung) erfolgen.

View File

@ -0,0 +1,25 @@
$newline never
Participants can have Uni2work choose a random pseudonym for them. #
Pseudonyms are only valid in the context of a single exercise sheet.
<br>
It is then expected that participants submit via some external system #
(e.g. on paper) identifying themselves only by their pseudonym. #
Thus submission correction can remain anonymous.
<br>
Correctors can, given the pseudonyms on their submissions, register #
the submissions in Uni2work.
<br>
Correctors can give feedback for and rate these submissions like for #
submissions created by any other method. #
Ratings can thus be included exam bonuses as usual (either manually or #
automatically).

View File

@ -0,0 +1,20 @@
$newline never
Teilnehmer können Abgaben direkt in Uni2work anlegen und dabei #
(je nach Einstellung unter „Abgabe von Dateien“) auch Dateien #
hochladen.
<br>
Die Abgaben werden dann von einem Kursverwalter oder automatisiert auf #
die eingestellten Korrektoren verteilt.
<br>
Diese haben dann die Möglichkeit Punkte (je nach Einstellung unter #
„Bewertung“) und Text-Feedback für die Abgaben zu vergeben. #
Für Feedback-Zwecke können etwaige hochgeladene Dateien beliebig #
angepasst oder neue Dateien hinterlegt werden (es werden dabei sowohl #
die originale als auch die durch die Korrektur veränderte Version #
permanent gespeichert).

View File

@ -0,0 +1,20 @@
$newline never
Participants may create submissions directly in Uni2work and may be #
permitted to upload files (“Submission of files”).
<br>
Submissions are distributed to correctors by course administrators #
either manually or automatically.
<br>
Correctors may then grade their corrections (“Marking”) and give #
text feedback. #
For feedback purposes uploaded files (if any) may be freely edited and #
new files may be uploaded. #
Both the original and any changed versions of files are saved #
permanently.