From 57f1ce9265e2a122aa7a318b923df51ee9ead0a3 Mon Sep 17 00:00:00 2001 From: Gregor Kleen Date: Tue, 28 Apr 2020 14:31:27 +0200 Subject: [PATCH] feat(sheets): submission groups & rework sheet form --- .../src/utils/form/interactive-fieldset.js | 7 + frontend/src/utils/inputs/inputs.sass | 10 +- messages/uniworx/de-de-formal.msg | 24 ++- messages/uniworx/en-eu.msg | 18 ++ missing-translations.sh | 5 +- routes | 6 +- src/Foundation.hs | 23 ++ src/Handler/Course/Show.hs | 15 +- src/Handler/Course/Users.hs | 83 ++++++-- src/Handler/Sheet.hs | 43 ++-- src/Handler/Submission.hs | 41 +++- src/Handler/Utils/Form.hs | 199 ++++++++++++------ src/Model/Types/Security.hs | 1 + src/Model/Types/Sheet.hs | 8 +- templates/course.hamlet | 9 + .../arbitrary/de-de-formal.hamlet | 20 ++ .../arbitrary/en-eu.hamlet | 18 ++ .../registered/de-de-formal.hamlet | 18 ++ .../registered/en-eu.hamlet | 23 ++ .../corrector/de-de-formal.hamlet | 25 +++ .../corrector/en-eu.hamlet | 25 +++ .../user/de-de-formal.hamlet | 20 ++ .../user/en-eu.hamlet | 20 ++ 23 files changed, 547 insertions(+), 114 deletions(-) create mode 100644 templates/i18n/sheet-grouping-explanation/arbitrary/de-de-formal.hamlet create mode 100644 templates/i18n/sheet-grouping-explanation/arbitrary/en-eu.hamlet create mode 100644 templates/i18n/sheet-grouping-explanation/registered/de-de-formal.hamlet create mode 100644 templates/i18n/sheet-grouping-explanation/registered/en-eu.hamlet create mode 100644 templates/i18n/submission-mode-explanation/corrector/de-de-formal.hamlet create mode 100644 templates/i18n/submission-mode-explanation/corrector/en-eu.hamlet create mode 100644 templates/i18n/submission-mode-explanation/user/de-de-formal.hamlet create mode 100644 templates/i18n/submission-mode-explanation/user/en-eu.hamlet diff --git a/frontend/src/utils/form/interactive-fieldset.js b/frontend/src/utils/form/interactive-fieldset.js index 30e2bead9..9ba364470 100644 --- a/frontend/src/utils/form/interactive-fieldset.js +++ b/frontend/src/utils/form/interactive-fieldset.js @@ -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]'); + } } diff --git a/frontend/src/utils/inputs/inputs.sass b/frontend/src/utils/inputs/inputs.sass index ec9581ff7..7288f47e7 100644 --- a/frontend/src/utils/inputs/inputs.sass +++ b/frontend/src/utils/inputs/inputs.sass @@ -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 diff --git a/messages/uniworx/de-de-formal.msg b/messages/uniworx/de-de-formal.msg index e18996e77..a66d69881 100644 --- a/messages/uniworx/de-de-formal.msg +++ b/messages/uniworx/de-de-formal.msg @@ -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 \ No newline at end of file +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 \ No newline at end of file diff --git a/messages/uniworx/en-eu.msg b/messages/uniworx/en-eu.msg index 744cd9cb7..c2e266aae 100644 --- a/messages/uniworx/en-eu.msg +++ b/messages/uniworx/en-eu.msg @@ -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 diff --git a/missing-translations.sh b/missing-translations.sh index 558d027bb..b335ec902 100755 --- a/missing-translations.sh +++ b/missing-translations.sh @@ -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 diff --git a/routes b/routes index 14a99926e..374d94282 100644 --- a/routes +++ b/routes @@ -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 diff --git a/src/Foundation.hs b/src/Foundation.hs index 73a223407..52fbda5c0 100644 --- a/src/Foundation.hs +++ b/src/Foundation.hs @@ -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 diff --git a/src/Handler/Course/Show.hs b/src/Handler/Course/Show.hs index 48666e9f6..dc14e2095 100644 --- a/src/Handler/Course/Show.hs +++ b/src/Handler/Course/Show.hs @@ -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' diff --git a/src/Handler/Course/Users.hs b/src/Handler/Course/Users.hs index 6709350c7..e6e4779b3 100644 --- a/src/Handler/Course/Users.hs +++ b/src/Handler/Course/Users.hs @@ -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 diff --git a/src/Handler/Sheet.hs b/src/Handler/Sheet.hs index a73a54403..07369029f 100644 --- a/src/Handler/Sheet.hs +++ b/src/Handler/Sheet.hs @@ -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 diff --git a/src/Handler/Submission.hs b/src/Handler/Submission.hs index 4e67cd86a..bb652625c 100644 --- a/src/Handler/Submission.hs +++ b/src/Handler/Submission.hs @@ -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 diff --git a/src/Handler/Utils/Form.hs b/src/Handler/Utils/Form.hs index 3b1325f7f..3d7ac307f 100644 --- a/src/Handler/Utils/Form.hs +++ b/src/Handler/Utils/Form.hs @@ -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 diff --git a/src/Model/Types/Security.hs b/src/Model/Types/Security.hs index ffb14f0da..0cd47cc69 100644 --- a/src/Model/Types/Security.hs +++ b/src/Model/Types/Security.hs @@ -71,6 +71,7 @@ data AuthTag -- sortiert nach gewünschter Reihenfolge auf /authpreds, d.h. Prä | AuthRated | AuthUserSubmissions | AuthCorrectorSubmissions + | AuthSubmissionGroup | AuthCapacity | AuthRegisterGroup | AuthEmpty diff --git a/src/Model/Types/Sheet.hs b/src/Model/Types/Sheet.hs index 10ec7ceef..0ef6f6ae8 100644 --- a/src/Model/Types/Sheet.hs +++ b/src/Model/Types/Sheet.hs @@ -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 diff --git a/templates/course.hamlet b/templates/course.hamlet index 6aee55dc5..b7a269c3c 100644 --- a/templates/course.hamlet +++ b/templates/course.hamlet @@ -165,6 +165,15 @@ $# $if NTop (Just 0) < NTop (courseCapacity course) _{MsgCourseApplicationTemplateApplication} $else _{MsgCourseApplicationTemplateRegistration} + $maybe mGroup <- submissionGroup +
+ _{MsgSubmissionGroup} +
+ $maybe groupName <- mGroup + #{groupName} + $nothing + + _{MsgNoSubmissionGroup} $if registrationOpen || isJust registration
_{MsgCourseRegistration} diff --git a/templates/i18n/sheet-grouping-explanation/arbitrary/de-de-formal.hamlet b/templates/i18n/sheet-grouping-explanation/arbitrary/de-de-formal.hamlet new file mode 100644 index 000000000..d5fa4c379 --- /dev/null +++ b/templates/i18n/sheet-grouping-explanation/arbitrary/de-de-formal.hamlet @@ -0,0 +1,20 @@ +$newline never + +Teilnehmer dürfen bei Anlegen der Abgabe selbstständig ihre # +Gruppenmitglieder angeben. + +
+ +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. + +
+ +Es kann eine maximale Gruppengröße festgelegt werden. + +
+ +Teilnehmer müssen nicht notwendigerweise in den gleichen arbiträren # +Gruppen abgeben wie für ein vorhergehendes Übungsblatt. diff --git a/templates/i18n/sheet-grouping-explanation/arbitrary/en-eu.hamlet b/templates/i18n/sheet-grouping-explanation/arbitrary/en-eu.hamlet new file mode 100644 index 000000000..ba75ac11d --- /dev/null +++ b/templates/i18n/sheet-grouping-explanation/arbitrary/en-eu.hamlet @@ -0,0 +1,18 @@ +$newline never + +Participants may freely specify their group members when submitting. + +
+ +Participants can have Uni2work send invitations via email or specify # +other participants directly if they have submitted together before in # +this course. + +
+ +A maximum group size may be specified. + +
+ +Participants need not submit in the same groups as for any previous # +exercise sheet. diff --git a/templates/i18n/sheet-grouping-explanation/registered/de-de-formal.hamlet b/templates/i18n/sheet-grouping-explanation/registered/de-de-formal.hamlet new file mode 100644 index 000000000..546b4c4a8 --- /dev/null +++ b/templates/i18n/sheet-grouping-explanation/registered/de-de-formal.hamlet @@ -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. + +
+ +Es kann stets nur für alle aktuellen Mitglieder der eigenen # +Abgabegruppe abgegeben werden. + +
+ +Wird die Zuordnung von Teilnehmern auf feste Abgabegruppen nach einer # +erfolgten Abgabe noch angepasst, hat dies keinen Einfluss auf die # +Abgebenden dieser konkreten Abgabe. diff --git a/templates/i18n/sheet-grouping-explanation/registered/en-eu.hamlet b/templates/i18n/sheet-grouping-explanation/registered/en-eu.hamlet new file mode 100644 index 000000000..75be1efd2 --- /dev/null +++ b/templates/i18n/sheet-grouping-explanation/registered/en-eu.hamlet @@ -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. + +
+ +Participants can only submit for their entire submission group at # +once. + +
+ +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. diff --git a/templates/i18n/submission-mode-explanation/corrector/de-de-formal.hamlet b/templates/i18n/submission-mode-explanation/corrector/de-de-formal.hamlet new file mode 100644 index 000000000..9080cd0bd --- /dev/null +++ b/templates/i18n/submission-mode-explanation/corrector/de-de-formal.hamlet @@ -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). + +
+ +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. + +
+ +Korrektoren können dann anhand der ihnen auf den Abgaben übermittelten # +Pseudonyme die Abgaben in Uni2work registrieren. + +
+ +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. diff --git a/templates/i18n/submission-mode-explanation/corrector/en-eu.hamlet b/templates/i18n/submission-mode-explanation/corrector/en-eu.hamlet new file mode 100644 index 000000000..29d3bf71d --- /dev/null +++ b/templates/i18n/submission-mode-explanation/corrector/en-eu.hamlet @@ -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. + +
+ +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. + +
+ +Correctors can, given the pseudonyms on their submissions, register # +the submissions in Uni2work. + +
+ +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). diff --git a/templates/i18n/submission-mode-explanation/user/de-de-formal.hamlet b/templates/i18n/submission-mode-explanation/user/de-de-formal.hamlet new file mode 100644 index 000000000..0569ba98a --- /dev/null +++ b/templates/i18n/submission-mode-explanation/user/de-de-formal.hamlet @@ -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. + +
+ +Die Abgaben werden dann von einem Kursverwalter oder automatisiert auf # +die eingestellten Korrektoren verteilt. + +
+ +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). diff --git a/templates/i18n/submission-mode-explanation/user/en-eu.hamlet b/templates/i18n/submission-mode-explanation/user/en-eu.hamlet new file mode 100644 index 000000000..cfac1740f --- /dev/null +++ b/templates/i18n/submission-mode-explanation/user/en-eu.hamlet @@ -0,0 +1,20 @@ +$newline never + +Participants may create submissions directly in Uni2work and may be # +permitted to upload files (“Submission of files”). + +
+ +Submissions are distributed to correctors by course administrators # +either manually or automatically. + +
+ +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.