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()) { if (this._isCheckbox()) {
matches = this.conditionalInput.checked === true; 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 { } else {
matches = this.conditionalInput.value === this.conditionalValue; matches = this.conditionalInput.value === this.conditionalValue;
} }
@ -112,4 +115,8 @@ export class InteractiveFieldset {
_isCheckbox() { _isCheckbox() {
return this.conditionalInput.getAttribute('type') === 'checkbox'; return this.conditionalInput.getAttribute('type') === 'checkbox';
} }
_isRadio() {
return !!this.conditionalInput.querySelector('input[type=radio]');
}
} }

View File

@ -60,13 +60,21 @@
grid-column: 1 grid-column: 1
.form-group--has-error .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 input, textarea
border-color: var(--color-error) !important border-color: var(--color-error) !important
.form-error .form-error
display: block display: block
font-weight: 600
color: var(--color-error)
margin: 7px 0
.form-error .form-error
display: none display: none

View File

@ -331,10 +331,12 @@ SheetFormType: Wertung & Abgabe
SheetFormTimes: Zeiten SheetFormTimes: Zeiten
SheetFormFiles: Dateien SheetFormFiles: Dateien
SheetErrVisibility: "Beginn Abgabezeitraum" muss nach "Sichbar für Teilnehmer ab" liegen SheetErrVisibility: "Aktiv ab/Beginn Abgabezeitraum" muss nach "Sichbar für Teilnehmer ab" liegen
SheetErrDeadlineEarly: "Ende Abgabezeitraum" muss nach "Beginn Abzeitraum" liegen SheetErrDeadlineEarly: "Aktiv bis/Ende Abgabezeitraum" muss nach "Aktiv ab/Beginn Abzeitraum" liegen
SheetErrHintEarly: Hinweise dürfen erst nach Beginn des Abgabezeitraums herausgegeben werden SheetErrHintEarly: Hinweise dürfen erst nach Beginn des Abgabezeitraums herausgegeben werden
SheetErrSolutionEarly: Lösungen dürfen erst nach Ende der 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. SheetNoCurrent: Es gibt momentan kein aktives Übungsblatt.
SheetNoOldUnassigned: Alle Abgaben inaktiver Blätter sind bereits einen Korrektor zugeteilt. SheetNoOldUnassigned: Alle Abgaben inaktiver Blätter sind bereits einen Korrektor zugeteilt.
SheetsUnassignable name@Text: Momentan keine Abgaben zuteilbar für #{name} 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. UnauthorizedPWHash: Angegebener Nutzer meldet sich nicht mit Uni2work-Kennung an.
UnauthorizedExternalExamListNotEmpty: Liste von externen Prüfungen ist nicht leer UnauthorizedExternalExamListNotEmpty: Liste von externen Prüfungen ist nicht leer
UnauthorizedExternalExamLecturer: Sie sind nicht als Prüfer für diese externe Prüfung eingetragen 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 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 UserSubmissions: Direkte Abgabe in Uni2work
BothSubmissions: Abgabe direkt in Uni2work & extern mit Pseudonym 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. 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. SubmissionNoUploadExpected: Es ist keine Abgabe von Dateien vorgesehen.
@ -973,6 +979,7 @@ SheetTypeBonus grading@SheetGrading: Bonus
SheetTypeNormal grading@SheetGrading: Normal SheetTypeNormal grading@SheetGrading: Normal
SheetTypeInformational grading@SheetGrading: Ohne Anrechnung SheetTypeInformational grading@SheetGrading: Ohne Anrechnung
SheetTypeNotGraded: Keine Korrektur 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. 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. 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. 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 AuthTagAuthentication: Nutzer ist angemeldet, falls erforderlich
AuthTagRead: Zugriff ist nur lesend AuthTagRead: Zugriff ist nur lesend
AuthTagWrite: Zugriff ist i.A. schreibend 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. 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. 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 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 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}“. 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 ExamUserSynchronised: Synchronisiert
ExamUserSyncOfficeName: Name ExamUserSyncOfficeName: Name
@ -1854,6 +1869,7 @@ CsvColumnUserRegistration: Zeitpunkt der Anmeldung zum Kurs (ISO 8601)
CsvColumnUserNote: Notizen zum Teilnehmer 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". 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. CsvColumnUserExam: Prüfungen zu denen der Teilnehmer angemeldet ist, als Semikolon (;) separierte Liste.
CsvColumnUserSubmissionGroup: Registrierte Abgabegruppe
CsvColumnExamOfficeExamUserOccurrenceStart: Prüfungstermin (ISO 8601) 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. BearerTokenOverrideStartTip: Wird kein Startzeitpunkt angegeben, wird bei Verwendung des Tokens nur der Ablaufzeitpunkt überprüft.
FaqTitle: Häufig gestellte Fragen 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" SheetErrDeadlineEarly: "Submission period end" must be after "Submission period start"
SheetErrHintEarly: "Hint from" must be after "Submission period start" SheetErrHintEarly: "Hint from" must be after "Submission period start"
SheetErrSolutionEarly: "Solution from" must be after "Submission period end" 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 SheetNoCurrent: There is no currently active exercise sheet
SheetNoOldUnassigned: All submissions for inactive sheets are already assigned to correctors. SheetNoOldUnassigned: All submissions for inactive sheets are already assigned to correctors.
SheetsUnassignable name: Submission for #{name} may not currently be 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. UnauthorizedPWHash: Specified user does not log in with an Uni2work-account.
UnauthorizedExternalExamListNotEmpty: List of external exams is not empty UnauthorizedExternalExamListNotEmpty: List of external exams is not empty
UnauthorizedExternalExamLecturer: You are not an associated person for this external exam 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 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 UserSubmissions: Direct submission in Uni2work
BothSubmissions: Submission either directly in Uni2work or externally via pseudonym 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. 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. SubmissionNoUploadExpected: No upload of files expected.
@ -973,6 +979,7 @@ SheetTypeBonus grading: Bonus
SheetTypeNormal grading: Normal SheetTypeNormal grading: Normal
SheetTypeInformational grading: Informational SheetTypeInformational grading: Informational
SheetTypeNotGraded: Not marked 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. 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. 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. 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 AuthTagAuthentication: User is authenticated
AuthTagRead: Access is read only AuthTagRead: Access is read only
AuthTagWrite: Access might write 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. 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. 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 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"} 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}” 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 ExamUserSynchronised: Synchronised
ExamUserSyncOfficeName: Name ExamUserSyncOfficeName: Name
@ -1853,6 +1868,7 @@ CsvColumnUserRegistration: Time of participant's enrollment (ISO 8601)
CsvColumnUserNote: Course notes for the participant 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". 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 (;). CsvColumnUserExam: Exams which the user is registered for, separated by semicolon (;).
CsvColumnUserSubmissionGroup: Registered submission group
CsvColumnExamOfficeExamUserOccurrenceStart: Exam occurrence (ISO 8601) CsvColumnExamOfficeExamUserOccurrenceStart: Exam occurrence (ISO 8601)
@ -2488,3 +2504,5 @@ BearerTokenOverrideStartTip: If no start time is given, only the expiration time
FaqTitle: Frequently asked questions FaqTitle: Frequently asked questions
AdditionalFaqs: More 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 ext (${templateExtensions}); do
for lang (${requiredLangs}); 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} # printf ">> %s %s %s\n" ${prefix} ${lang} ${ext}
foundLang=1 foundLang=1
@ -156,7 +157,7 @@ for templateDirectory (templates/i18n/**/*(FN)); do
if [[ $foundLang -ne 0 ]]; then if [[ $foundLang -ne 0 ]]; then
templateDifference=1 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 if [[ $fix == 0 ]]; then
./translate.hs dir $templateDirectory && templateDifference=0 ./translate.hs dir $templateDirectory && templateDifference=0

6
routes
View File

@ -151,15 +151,15 @@
/edit SEditR GET POST /edit SEditR GET POST
/delete SDelR GET POST /delete SDelR GET POST
/subs SSubsR GET POST -- for lecturer only /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/own SubmissionOwnR GET !free -- just redirect
!/subs/assign SAssignR GET POST !lecturerANDtime !/subs/assign SAssignR GET POST !lecturerANDtime
/subs/#CryptoFileNameSubmission SubmissionR: /subs/#CryptoFileNameSubmission SubmissionR:
/ SubShowR GET POST !ownerANDtimeANDuser-submissions !ownerANDread !correctorANDread / SubShowR GET POST !ownerANDtimeANDuser-submissionsANDsubmission-group !ownerANDread !correctorANDread
/delete SubDelR GET POST !ownerANDtimeANDuser-submissions /delete SubDelR GET POST !ownerANDtimeANDuser-submissions
/assign SubAssignR GET POST !lecturerANDtime /assign SubAssignR GET POST !lecturerANDtime
/correction CorrectionR GET POST !corrector !ownerANDreadANDrated /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 SubArchiveR GET !owner !corrector
!/#SubmissionFileType/*FilePath SubDownloadR GET !owner !corrector !/#SubmissionFileType/*FilePath SubDownloadR GET !owner !corrector
/iscorrector SIsCorrR GET !corrector -- Route is used to check for corrector access to this sheet /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 guard tutorialTutorControlled
return Authorized return Authorized
r -> $unsupportedAuthPredicate AuthTutorControl r 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 tagAccessPredicate AuthTime = APDB $ \mAuthId route _ -> case route of
CApplicationR tid ssh csh _ _ -> maybeT (unauthorizedI MsgUnauthorizedApplicationTime) $ do CApplicationR tid ssh csh _ _ -> maybeT (unauthorizedI MsgUnauthorizedApplicationTime) $ do
course <- $cachedHereBinary (tid, ssh, csh) . MaybeT . getKeyBy $ TermSchoolCourseShort tid ssh csh 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 :: TermId -> SchoolId -> CourseShorthand -> Handler Html
getCShowR tid ssh csh = do getCShowR tid ssh csh = do
mbAid <- maybeAuthId 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)] [(E.Entity cid course, E.Value schoolName, E.Value participants, fmap entityVal -> registration)]
<- lift . E.select . E.from $ <- lift . E.select . E.from $
\((school `E.InnerJoin` course) `E.LeftOuterJoin` participant) -> do \((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' <- fmap (sortOn $ courseEventTime . entityVal) . lift $ selectList [ CourseEventCourse ==. cid ] []
events <- mapM (\(Entity evId ev) -> (, ev) <$> encrypt evId) events' 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 let mDereg' = maybe id min (allocationOverrideDeregister =<< mAllocation) <$> courseDeregisterUntil course
mDereg <- traverse (formatTime SelFormatDateTime) mDereg' 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 StudyDegree))
`E.InnerJoin` E.SqlExpr (Maybe (Entity StudyTerms)) `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 :: (UserTableExpr -> a) -> (UserTableExpr -> a)
-- forceUserTableType = id -- 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) -- 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 -- This ought to ease refactoring the query
queryUser :: UserTableExpr -> E.SqlExpr (Entity User) 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 :: 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 :: UserTableExpr -> E.SqlExpr (Maybe (Entity CourseUserNote))
queryUserNote = $(sqlLOJproj 3 2) queryUserNote = $(sqlLOJproj 4 2)
queryFeaturesStudy :: UserTableExpr -> E.SqlExpr (Maybe (Entity StudyFeatures)) 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 :: 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 :: 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) userTableQuery :: CourseId -> UserTableExpr -> E.SqlQuery ( E.SqlExpr (Entity User)
, E.SqlExpr (E.Value UTCTime) , E.SqlExpr (E.Value UTCTime)
, E.SqlExpr (E.Value (Maybe (Key CourseUserNote))) , E.SqlExpr (E.Value (Maybe (Key CourseUserNote)))
, StudyFeaturesDescription') , StudyFeaturesDescription'
userTableQuery cid ((user `E.InnerJoin` participant) `E.LeftOuterJoin` note `E.LeftOuterJoin` studyFeatures) = do , 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 -- 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 features <- studyFeaturesQuery' (participant E.^. CourseParticipantField) studyFeatures
E.on $ (note E.?. CourseUserNoteUser E.==. E.just (participant E.^. CourseParticipantUser)) E.on $ (note E.?. CourseUserNoteUser E.==. E.just (participant E.^. CourseParticipantUser))
E.&&. (note E.?. CourseUserNoteCourse E.==. E.just (E.val cid)) E.&&. (note E.?. CourseUserNoteCourse E.==. E.just (E.val cid))
E.on $ participant E.^. CourseParticipantUser E.==. user E.^. UserId E.on $ participant E.^. CourseParticipantUser E.==. user E.^. UserId
E.where_ $ participant E.^. CourseParticipantCourse E.==. E.val cid 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 type UserTableData = DBRow ( Entity User
@ -83,6 +94,7 @@ type UserTableData = DBRow ( Entity User
, (Maybe StudyFeatures, Maybe StudyDegree, Maybe StudyTerms) , (Maybe StudyFeatures, Maybe StudyDegree, Maybe StudyTerms)
, ([Entity Tutorial], Map (CI Text) (Maybe (Entity Tutorial))) , ([Entity Tutorial], Map (CI Text) (Maybe (Entity Tutorial)))
, [Entity Exam] , [Entity Exam]
, Maybe (Entity SubmissionGroup)
) )
instance HasEntity UserTableData User where instance HasEntity UserTableData User where
@ -109,11 +121,14 @@ _userTutorials = _dbrOutput . _5
_userExams :: Lens' UserTableData [Entity Exam] _userExams :: Lens' UserTableData [Entity Exam]
_userExams = _dbrOutput . _6 _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 :: IsDBTable m c => TermId -> SchoolId -> CourseShorthand -> Colonnade Sortable UserTableData (DBCell m c)
colUserComment tid ssh csh = colUserComment tid ssh csh =
sortable (Just "note") (i18nCell MsgCourseUserNote) sortable (Just "note") (i18nCell MsgCourseUserNote)
$ \DBRow{ dbrOutput=(Entity uid _, _, mbNoteKey, _, _, _) } -> $ \DBRow{ dbrOutput=(Entity uid _, _, mbNoteKey, _, _, _, _) } ->
maybeEmpty mbNoteKey $ const $ maybeEmpty mbNoteKey $ const $
anchorCellM (courseLink <$> encrypt uid) (hasComment True) anchorCellM (courseLink <$> encrypt uid) (hasComment True)
where where
@ -158,6 +173,10 @@ colUserDegreeShort = sortable (Just "degree-short") (i18nCell MsgStudyFeatureDeg
colUserSex' :: IsDBTable m c => Colonnade Sortable UserTableData (DBCell m c) colUserSex' :: IsDBTable m c => Colonnade Sortable UserTableData (DBCell m c)
colUserSex' = colUserSex $ hasUser . _userSex 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 data UserTableCsvStudyFeature = UserTableCsvStudyFeature
{ csvUserField :: Text { csvUserField :: Text
@ -173,6 +192,7 @@ data UserTableCsv = UserTableCsv
, csvUserMatriculation :: Maybe Text , csvUserMatriculation :: Maybe Text
, csvUserEmail :: CI Email , csvUserEmail :: CI Email
, csvUserStudyFeatures :: Either (Maybe UserTableCsvStudyFeature) (Set UserTableCsvStudyFeature) , csvUserStudyFeatures :: Either (Maybe UserTableCsvStudyFeature) (Set UserTableCsvStudyFeature)
, csvUserSubmissionGroup :: Maybe SubmissionGroupName
, csvUserRegistration :: UTCTime , csvUserRegistration :: UTCTime
, csvUserNote :: Maybe Html , csvUserNote :: Maybe Html
, csvUserTutorials :: ([TutorialName], Map (CI Text) (Maybe TutorialName)) , csvUserTutorials :: ([TutorialName], Map (CI Text) (Maybe TutorialName))
@ -199,6 +219,8 @@ instance Csv.ToNamedRecord UserTableCsv where
in [ "study-features" Csv..= featsStr in [ "study-features" Csv..= featsStr
] ]
++ ++
[ "submission-group" Csv..= csvUserSubmissionGroup
] ++
[ let tutsStr = Text.intercalate "; " . map CI.original $ csvUserTutorials ^. _1 [ let tutsStr = Text.intercalate "; " . map CI.original $ csvUserTutorials ^. _1
in "tutorial" Csv..= tutsStr in "tutorial" Csv..= tutsStr
] ++ ] ++
@ -221,6 +243,7 @@ instance CsvColumnsExplained UserTableCsv where
, single "field" MsgCsvColumnUserField , single "field" MsgCsvColumnUserField
, single "degree" MsgCsvColumnUserDegree , single "degree" MsgCsvColumnUserDegree
, single "semester" MsgCsvColumnUserSemester , single "semester" MsgCsvColumnUserSemester
, single "submission-group" MsgCsvColumnUserSubmissionGroup
, single "tutorial" MsgCsvColumnUserTutorial , single "tutorial" MsgCsvColumnUserTutorial
, single "exams" MsgCsvColumnUserExam , single "exams" MsgCsvColumnUserExam
, single "registration" MsgCsvColumnUserRegistration , single "registration" MsgCsvColumnUserRegistration
@ -255,6 +278,7 @@ data CourseUserAction = CourseUserSendMail
| CourseUserDeregister | CourseUserDeregister
| CourseUserRegisterTutorial | CourseUserRegisterTutorial
| CourseUserRegisterExam | CourseUserRegisterExam
| CourseUserSetSubmissionGroup
deriving (Eq, Ord, Enum, Bounded, Read, Show, Generic, Typeable) deriving (Eq, Ord, Enum, Bounded, Read, Show, Generic, Typeable)
instance Universe CourseUserAction instance Universe CourseUserAction
@ -272,6 +296,9 @@ data CourseUserActionData = CourseUserSendMailData
| CourseUserRegisterExamData | CourseUserRegisterExamData
{ registerExam :: (ExamId, Maybe ExamOccurrenceId) { registerExam :: (ExamId, Maybe ExamOccurrenceId)
} }
| CourseUserSetSubmissionGroupData
{ setSubmissionGroup :: Maybe SubmissionGroupName
}
deriving (Eq, Ord, Read, Show, Generic, Typeable) deriving (Eq, Ord, Read, Show, Generic, Typeable)
@ -298,7 +325,7 @@ makeCourseUserTable cid acts restrict colChoices psValidator csvColumns = do
dbtStyle = def { dbsFilterLayout = defaultDBSFilterLayout } dbtStyle = def { dbsFilterLayout = defaultDBSFilterLayout }
dbtSQLQuery q = userTableQuery cid q <* E.where_ (restrict q) dbtSQLQuery q = userTableQuery cid q <* E.where_ (restrict q)
dbtRowKey = queryUser >>> (E.^. UserId) 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 ] [] tuts'' <- selectList [ TutorialParticipantUser ==. entityKey user, TutorialParticipantTutorial <-. map entityKey tutorials ] []
exams' <- selectList [ ExamRegistrationUser ==. entityKey user ] [] exams' <- selectList [ ExamRegistrationUser ==. entityKey user ] []
let let
@ -306,7 +333,7 @@ makeCourseUserTable cid acts restrict colChoices psValidator csvColumns = do
tuts' = filter (\(Entity tutId _) -> any ((== tutId) . tutorialParticipantTutorial . entityVal) tuts'') tutorials 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' 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 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 dbtColonnade = colChoices
dbtSorting = mconcat dbtSorting = mconcat
[ single $ sortUserNameLink queryUser -- slower sorting through clicking name column header [ 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 E.where_ $ examRegistration E.^. ExamRegistrationUser E.==. user E.^. UserId
return . E.min_ $ exam E.^. ExamName return . E.min_ $ exam E.^. ExamName
) )
, single $ ("submission-group", SortColumn $ querySubmissionGroup >>> (E.?. SubmissionGroupName))
] ]
where single = uncurry Map.singleton where single = uncurry Map.singleton
dbtFilter = mconcat dbtFilter = mconcat
@ -378,6 +406,7 @@ makeCourseUserTable cid acts restrict colChoices psValidator csvColumns = do
) )
-- , ("course-registration", error "TODO") -- TODO -- , ("course-registration", error "TODO") -- TODO
-- , ("course-user-note", error "TODO") -- TODO -- , ("course-user-note", error "TODO") -- TODO
, single $ ("submission-group", FilterColumn $ E.mkContainsFilter $ querySubmissionGroup >>> (E.?. SubmissionGroupName))
] ]
where single = uncurry Map.singleton where single = uncurry Map.singleton
dbtFilterUI mPrev = mconcat $ dbtFilterUI mPrev = mconcat $
@ -387,6 +416,7 @@ makeCourseUserTable cid acts restrict colChoices psValidator csvColumns = do
[ fltrUserSexUI mPrev | showSex ] ++ [ fltrUserSexUI mPrev | showSex ] ++
[ prismAForm (singletonFilter "degree") mPrev $ aopt textField (fslI MsgStudyFeatureDegree) [ prismAForm (singletonFilter "degree") mPrev $ aopt textField (fslI MsgStudyFeatureDegree)
, prismAForm (singletonFilter "field") mPrev $ aopt textField (fslI MsgCourseStudyFeature) , 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 "tutorial") mPrev $ aopt textField (fslI MsgCourseUserTutorial)
, prismAForm (singletonFilter "exam") mPrev $ aopt textField (fslI MsgCourseUserExam) , prismAForm (singletonFilter "exam") mPrev $ aopt textField (fslI MsgCourseUserExam)
] ]
@ -444,6 +474,7 @@ makeCourseUserTable cid acts restrict colChoices psValidator csvColumns = do
, csvUserSemester = studyFeaturesSemester , csvUserSemester = studyFeaturesSemester
, csvUserType = studyFeaturesType , csvUserType = studyFeaturesType
} }
<*> preview (_userSubmissionGroup . _entityVal . _submissionGroupName)
<*> view _userTableRegistration <*> view _userTableRegistration
<*> userNote <*> userNote
<*> (over (_2.traverse._Just) (tutorialName . entityVal) . over (_1.traverse) (tutorialName . entityVal) <$> view _userTutorials) <*> (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.on $ E.just (exam E.^. ExamId) E.==. examOccurrence E.?. ExamOccurrenceExam
E.where_ $ exam E.^. ExamCourse E.==. E.val cid E.where_ $ exam E.^. ExamCourse E.==. E.val cid
return (exam, examOccurrence) 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 exams = nubOn entityKey $ examOccurrencesPerExam ^.. folded . _1
let colChoices = mconcat $ catMaybes let colChoices = mconcat $ catMaybes
[ pure $ dbSelect (applying _2) id (return . view (hasEntity . _entityKey)) [ pure $ dbSelect (applying _2) id (return . view (hasEntity . _entityKey))
@ -500,6 +536,7 @@ postCUsersR tid ssh csh = do
, pure $ colUserDegreeShort , pure $ colUserDegreeShort
, pure $ colUserField , pure $ colUserField
, pure $ colUserSemester , pure $ colUserSemester
, guardOn hasSubmissionGroups colUserSubmissionGroup
, guardOn hasTutorials $ colUserTutorials tid ssh csh , guardOn hasTutorials $ colUserTutorials tid ssh csh
, guardOn hasExams $ colUserExams tid ssh csh , guardOn hasExams $ colUserExams tid ssh csh
, pure $ sortable (Just "registration") (i18nCell MsgRegisteredSince) (dateCell . view _userTableRegistration) , pure $ sortable (Just "registration") (i18nCell MsgRegisteredSince) (dateCell . view _userTableRegistration)
@ -532,6 +569,7 @@ postCUsersR tid ssh csh = do
optionInternalValue = entityKey optionInternalValue = entityKey
optionDisplay = CI.original $ examName entityVal optionDisplay = CI.original $ examName entityVal
return Option{..} return Option{..}
submissionGroupOpts = optionsPersist [SubmissionGroupCourse ==. cid] [Asc SubmissionGroupName] submissionGroupName <&> fmap (submissionGroupName . entityVal)
acts = mconcat acts = mconcat
[ singletonMap CourseUserSendMail $ pure CourseUserSendMailData [ singletonMap CourseUserSendMail $ pure CourseUserSendMailData
, singletonMap CourseUserRegisterTutorial $ CourseUserRegisterTutorialData <$> , singletonMap CourseUserRegisterTutorial $ CourseUserRegisterTutorialData <$>
@ -540,6 +578,8 @@ postCUsersR tid ssh csh = do
Nothing Nothing
, singletonMap CourseUserRegisterExam $ CourseUserRegisterExamData <$> , singletonMap CourseUserRegisterExam $ CourseUserRegisterExamData <$>
multiActionAOpts examOccActs examActs (fslI MsgCourseExam) Nothing 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 , if
| mayRegister | mayRegister
-> singletonMap CourseUserDeregister $ courseUserDeregisterForm cid -> singletonMap CourseUserDeregister $ courseUserDeregisterForm cid
@ -589,6 +629,25 @@ postCUsersR tid ssh csh = do
return mempty return mempty
addMessageI Success $ MsgCourseUsersExamRegistered nrReg addMessageI Success $ MsgCourseUsersExamRegistered nrReg
redirect $ CourseR tid ssh csh CUsersR 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}|] let headingLong = [whamlet|_{MsgMenuCourseMembers} #{courseName course} #{tid}|]
headingShort = prependCourseTitle tid ssh csh MsgCourseMembers headingShort = prependCourseTitle tid ssh csh MsgCourseMembers
siteLayout headingLong $ do siteLayout headingLong $ do

View File

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

View File

@ -358,7 +358,34 @@ submissionHelper tid ssh csh shn mcid = do
, isLecturer , isLecturer
, not 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 (Just smid, _) -> do
void $ submissionMatchesSheet tid ssh csh shn (fromJust mcid) void $ submissionMatchesSheet tid ssh csh shn (fromJust mcid)
@ -478,16 +505,24 @@ submissionHelper tid ssh csh shn mcid = do
-- Determine new submission users -- Determine new submission users
subUsers <- if subUsers <- if
| isLecturer -> return adhocMembers | isLecturer -> return adhocMembers
| otherwise -> do | RegisteredGroups <- sheetGrouping -> do
-- Determine members of pre-registered group -- 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 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 $ submissionGroup E.^. SubmissionGroupId E.==. submissionGroupUser' E.^. SubmissionGroupUserSubmissionGroup
E.on $ submissionGroupUser E.^. SubmissionGroupUserSubmissionGroup E.==. submissionGroup E.^. SubmissionGroupId E.on $ submissionGroupUser E.^. SubmissionGroupUserSubmissionGroup E.==. submissionGroup E.^. SubmissionGroupId
E.where_ $ submissionGroupUser E.^. SubmissionGroupUserUser E.==. E.val uid E.where_ $ submissionGroupUser E.^. SubmissionGroupUserUser E.==. E.val uid
E.&&. submissionGroup E.^. SubmissionGroupCourse E.==. E.val sheetCourse 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 return $ submissionGroupUser' E.^. SubmissionGroupUserUser
-- SubmissionUser for all group members (pre-registered & ad-hoc) -- 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 -- Since invitations carry no data we only need to consider changes to
-- the set of users/invited emails -- the set of users/invited emails

View File

@ -16,6 +16,8 @@ import Handler.Utils.DateTime
import Handler.Utils.Widgets import Handler.Utils.Widgets
import Handler.Utils.I18n
import Import import Import
import Data.Char (chr, ord) import Data.Char (chr, ord)
import qualified Data.Char as Char import qualified Data.Char as Char
@ -239,6 +241,34 @@ multiAction' :: forall action a.
-> (Html -> MForm Handler (FormResult a, [FieldView UniWorX])) -> (Html -> MForm Handler (FormResult a, [FieldView UniWorX]))
multiAction' minp acts = multiActionOpts' minp acts (optionsF $ Map.keysSet acts) 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. multiActionOpts' :: forall action a.
Ord action Ord action
=> (Field Handler action -> FieldSettings UniWorX -> Maybe action -> MForm Handler (FormResult action, FieldView UniWorX)) => (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 , olReadExternal = assertM (flip Map.member acts) . olReadExternal actsOpts
} }
acts' = Map.filterWithKey (\a _ -> any ((== a) . optionInternalValue) $ olOptions actsOpts') acts 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 actOption act = listToMaybe . filter (\Option{..} -> optionInternalValue == act) $ olOptions actsOpts'
actExternal = fmap optionExternalValue . actOption
actionViews = Map.foldrWithKey accViews [] results actMessage = fmap (SomeMessage . optionDisplay) . actOption
accViews :: forall b. action -> (b, [FieldView UniWorX]) -> [FieldView UniWorX] -> [FieldView UniWorX] multiActionField minp acts' (selectField $ return actsOpts', actExternal, actMessage) fs defAction csrf
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)
multiActionA :: (RenderMessage UniWorX action, PathPiece action, Ord action) multiActionA :: (RenderMessage UniWorX action, PathPiece action, Ord action)
=> Map action (AForm Handler a) => Map action (AForm Handler a)
@ -316,6 +336,88 @@ multiActionMOpts :: Ord action
-> (Html -> MForm Handler (FormResult a, Widget)) -> (Html -> MForm Handler (FormResult a, Widget))
multiActionMOpts acts opts fSettings defAction = renderAForm FormStandard $ multiActionAOpts acts opts fSettings defAction 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 -- -- Fields --
@ -555,7 +657,7 @@ uploadModeForm prev = multiActionA actions (fslI MsgSheetUploadMode) (classifyUp
submissionModeForm :: Maybe SubmissionMode -> AForm Handler SubmissionMode submissionModeForm :: Maybe SubmissionMode -> AForm Handler SubmissionMode
submissionModeForm prev = multiActionA actions (fslI MsgSheetSubmissionMode) $ classifySubmissionMode <$> prev submissionModeForm prev = explainedMultiActionA actions opts (fslI MsgSheetSubmissionMode) $ classifySubmissionMode <$> prev
where where
actions :: Map SubmissionModeDescr (AForm Handler SubmissionMode) actions :: Map SubmissionModeDescr (AForm Handler SubmissionMode)
actions = Map.fromList 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' data ExamBonusRule' = ExamBonusManual'
| ExamBonusPoints' | ExamBonusPoints'
deriving (Eq, Ord, Read, Show, Enum, Bounded, Generic, Typeable) deriving (Eq, Ord, Read, Show, Enum, Bounded, Generic, Typeable)
@ -847,7 +955,7 @@ nullaryPathPiece ''SheetType' (camelToPathPiece . dropSuffix "'")
embedRenderMessage ''UniWorX ''SheetType' ("SheetType" <>) embedRenderMessage ''UniWorX ''SheetType' ("SheetType" <>)
data SheetGroup' = NoGroups' | Arbitrary' | RegisteredGroups' data SheetGroup' = Arbitrary' | RegisteredGroups' | NoGroups'
deriving (Eq, Ord, Read, Show, Enum, Bounded) deriving (Eq, Ord, Read, Show, Enum, Bounded)
instance Universe SheetGroup' instance Universe SheetGroup'
@ -875,9 +983,9 @@ sheetGradingAFormReq fs template = multiActionA selOptions fs (classify' <$> tem
sheetTypeAFormReq :: FieldSettings UniWorX -> Maybe SheetType -> AForm Handler SheetType 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 where
selOptions = Map.fromList acts = Map.fromList
[ ( Normal', Normal <$> gradingReq ) [ ( Normal', Normal <$> gradingReq )
, ( Bonus' , Bonus <$> gradingReq ) , ( Bonus' , Bonus <$> gradingReq )
, ( Informational', Informational <$> gradingReq ) , ( Informational', Informational <$> gradingReq )
@ -886,6 +994,12 @@ sheetTypeAFormReq fs template = multiActionA selOptions fs (classify' <$> templa
gradingReq = sheetGradingAFormReq (fslI MsgSheetGrading gradingReq = sheetGradingAFormReq (fslI MsgSheetGrading
& setTooltip MsgSheetGradingInfo) (template >>= preview _grading) & 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' :: SheetType -> SheetType'
classify' = \case classify' = \case
Bonus {} -> Bonus' Bonus {} -> Bonus'
@ -894,15 +1008,19 @@ sheetTypeAFormReq fs template = multiActionA selOptions fs (classify' <$> templa
NotGraded -> NotGraded' NotGraded -> NotGraded'
sheetGroupAFormReq :: FieldSettings UniWorX -> Maybe SheetGroup -> AForm Handler SheetGroup 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 where
selOptions = Map.fromList acts = Map.fromList
[ ( Arbitrary', Arbitrary [ ( Arbitrary', Arbitrary
<$> apreq (natFieldI MsgGroupSizeNotNatural) (fslI MsgSheetGroupMaxGroupsize & noValidate) (preview _maxParticipants =<< template) <$> apreq (natFieldI MsgGroupSizeNotNatural) (fslI MsgSheetGroupMaxGroupsize & noValidate) (preview _maxParticipants =<< template)
) )
, ( RegisteredGroups', pure RegisteredGroups ) , ( RegisteredGroups', pure RegisteredGroups )
, ( NoGroups', pure NoGroups ) , ( 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' :: SheetGroup -> SheetGroup'
classify' = \case classify' = \case
Arbitrary _ -> Arbitrary' Arbitrary _ -> Arbitrary'
@ -1516,47 +1634,6 @@ csvOptionsForm mPrev = hoistAForm liftHandler $ CsvOptions
<$> csvFormatOptionsForm (fslI MsgCsvFormatOptions & setTooltip MsgCsvOptionsTip) (csvFormat <$> mPrev) <$> csvFormatOptionsForm (fslI MsgCsvFormatOptions & setTooltip MsgCsvOptionsTip) (csvFormat <$> mPrev)
<*> apopt checkBoxField (fslI MsgCsvTimestamp & setTooltip MsgCsvTimestampTip) (csvTimestamp <$> 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. courseSelectForm :: forall ident handler.
( PathPiece ident ( PathPiece ident

View File

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

View File

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

View File

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