diff --git a/CHANGELOG.md b/CHANGELOG.md index c5836333f..ad81239bf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,39 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +### [22.1.1](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/v22.1.0...v22.1.1) (2020-11-14) + +## [22.1.0](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/v22.0.0...v22.1.0) (2020-11-10) + + +### Features + +* partial/conditional downloads & video streaming ([5b28303](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/5b28303539e28024b43addb413aedc4e5ee0e470)) + + +### Bug Fixes + +* translation ([80960f4](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/80960f42c578c201f78e226653431e9dd965cfce)) +* **personalised-sheet-files:** don't delete files when "keep" ([6008cb0](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/6008cb040dea268e0a096f6c2fafa87f321d115f)) + +## [22.0.0](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/v21.1.1...v22.0.0) (2020-11-06) + + +### ⚠ BREAKING CHANGES + +* **html-field:** StoredMarkup + +### Bug Fixes + +* **html-field:** introduce stored-markup ([e25e8a2](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/e25e8a2f4ca65afc29acc8a3884df9acf77d4398)) + +### [21.1.1](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/v21.1.0...v21.1.1) (2020-11-06) + + +### Bug Fixes + +* **course:** better explanation for material access ([78c5bc5](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/78c5bc5258c9305deafac18b010dc6a41e5ea864)) + ## [21.1.0](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/v21.0.3...v21.1.0) (2020-11-05) diff --git a/config/settings.yml b/config/settings.yml index 4ded0132e..9c4060e61 100644 --- a/config/settings.yml +++ b/config/settings.yml @@ -35,7 +35,7 @@ notification-expiration: 259200 session-timeout: 7200 bearer-expiration: 604800 bearer-encoding: HS256 -maximum-content-length: "_env:MAX_UPLOAD_SIZE:134217728" +maximum-content-length: "_env:MAX_UPLOAD_SIZE:805306368" session-files-expire: 3600 prune-unreferenced-files-within: 57600 prune-unreferenced-files-interval: 3600 diff --git a/config/video-types b/config/video-types new file mode 100644 index 000000000..361fd28ef --- /dev/null +++ b/config/video-types @@ -0,0 +1,31 @@ +# Simple list of mime-types corresponding to video-formats +# +# Comments are empty lines and any line for which the first non-whitespace symbol is ‘#’ +# +# Format is a single mime-type per line (may not contain whitespace) +# +# Largely copied from https://en.wikipedia.org/wiki/Video_file_format + +video/webm +video/x-matroska +video/x-flv +video/x-f4v +video/ogg +video/x-mng +video/x-msvideo +model/vnd.mts +video/quicktime +video/x-ms-wmv +application/vnd.rn-realmedia +application/vnd.rn-realmedia-vbr +video/vnd.vivo +video/x-ms-asf +video/mp4 +video/mpeg +video/x-m4v +video/3gpp +video/3gpp2 +application/mxf +video/h261 +video/h263 +video/h264 \ No newline at end of file diff --git a/frontend/src/app.sass b/frontend/src/app.sass index bbef8618a..7e1414622 100644 --- a/frontend/src/app.sass +++ b/frontend/src/app.sass @@ -1496,3 +1496,17 @@ pre, tt, code .workflow-payload--label font-size: 20px font-weight: 600 + +video + max-width: 100% + max-height: calc(90vh - var(--current-header-height)) + background: black + +.video-container + display: flex + justify-content: center + width: 100% + + & > video + object-fit: contain + flex-grow: 1 diff --git a/frontend/src/utils/inputs/inputs.sass b/frontend/src/utils/inputs/inputs.sass index 022efa71d..a42071863 100644 --- a/frontend/src/utils/inputs/inputs.sass +++ b/frontend/src/utils/inputs/inputs.sass @@ -225,11 +225,15 @@ option margin: 10px 0 color: var(--color-fontsec) +.file-input__list-wrapper + overflow: auto + max-height: 75vh + max-width: 30vw + .file-input__list margin-left: 40px margin-top: 10px font-weight: 600 - max-width: 25vw tr:last-child td padding-bottom: 0 @@ -237,6 +241,7 @@ option .file-input__list-item font-family: var(--font-monospace) font-size: 15px + word-break: break-all // PREVIOUSLY UPLOADED FILES @@ -250,3 +255,13 @@ option .checkbox margin-left: 12px + +.form--vertical .form-group__input + grid-column: unset + grid-row: 2 + +.form-group.form--vertical + grid-template: auto auto / auto + +.form--vertical__cell + vertical-align: top diff --git a/messages/uniworx/de-de-formal.msg b/messages/uniworx/de-de-formal.msg index 93d2e2322..9fb87c738 100644 --- a/messages/uniworx/de-de-formal.msg +++ b/messages/uniworx/de-de-formal.msg @@ -435,7 +435,7 @@ MaterialVisibleFromTip: Ohne Datum nie sichtbar für Teilnehmer; leer lassen ist MaterialVisibleFromEditWarning: Das Datum der Veröffentlichung liegt in der Vergangenheit und sollte nicht mehr verändert werden, da dies die Benutzer verwirren könnte. MaterialInvisible: Dieses Material ist für Teilnehmer momentan unsichtbar! MaterialFiles: Dateien -MaterialHeading materialName@MaterialName: Material "#{materialName}" +MaterialHeading materialName@MaterialName: #{materialName} MaterialListHeading: Materialien MaterialNewHeading: Neues Material veröffentlichen MaterialNewTitle: Neues Material @@ -448,6 +448,9 @@ MaterialDelHasFiles count@Int64: inklusive #{count} #{pluralDE count "Datei" "Da MaterialIsVisible: Achtung, dieses Material wurde bereits veröffentlicht. MaterialDeleted materialName@MaterialName: Material "#{materialName}" gelöscht MaterialArchiveName tid@TermId ssh@SchoolId csh@CourseShorthand materialName@MaterialName: #{foldCase (termToText (unTermKey tid))}-#{foldedCase (unSchoolKey ssh)}-#{foldedCase csh}-#{foldedCase materialName} +MaterialVideo materialName@MaterialName: #{materialName} - Video +MaterialVideoUnsupported: Ihr Browser scheint keine eingebetten Videos zu unterstützen +MaterialVideoDownload: Herunterladen Unauthorized: Sie haben hierfür keine explizite Berechtigung. @@ -1026,10 +1029,10 @@ MailCourseRegisteredIntroOther displayName@Text courseName@Text termDesc@Text: # MailSubjectExamResult csh@CourseShorthand examn@ExamName: Ergebnisse für #{examn} in #{csh} wurden herausgegeben MailExamResultIntro courseName@Text termDesc@Text examn@ExamName: Sie können nun Ihr Ergebnis für #{examn} im Kurs #{courseName} (#{termDesc}) einsehen. -MailSubjectExamOfficeExamResults csh@CourseShorthand examn@ExamName: Ergebnisse für #{examn} in #{csh} sind fertiggestellt +MailSubjectExamOfficeExamResults coursen@CourseName examn@ExamName: Ergebnisse für #{examn} in #{coursen} sind fertiggestellt MailExamOfficeExamResultsIntro courseName@Text termDesc@Text examn@ExamName: Ein Kursverwalter hat die Prüfungsleistungen für #{examn} im Kurs #{courseName} (#{termDesc}) freigegeben. -MailSubjectExamOfficeExamResultsChanged csh@CourseShorthand examn@ExamName: Ergebnisse für #{examn} in #{csh} wurden verändert +MailSubjectExamOfficeExamResultsChanged coursen@CourseName examn@ExamName: Ergebnisse für #{examn} in #{coursen} wurden verändert MailExamOfficeExamResultsChangedIntro courseName@Text termDesc@Text examn@ExamName: Ein Kursverwalter hat Prüfungsleistungen für #{examn} im Kurs #{courseName} (#{termDesc}) verändert. MailSubjectExamOfficeExternalExamResults coursen@CourseName examn@ExamName: Ergebnisse für #{examn} in #{coursen} @@ -1468,6 +1471,7 @@ BreadcrumbAllocationInfo: Ablauf einer Zentralanmeldung BreadcrumbCourseParticipantInvitation: Einladung zum Kursteilnehmer BreadcrumbMaterialArchive: Archiv BreadcrumbMaterialFile: Datei +BreadcrumbMaterialVideo: Video BreadcrumbSheetArchive: Dateien BreadcrumbSheetIsCorrector: Korrektor-Überprüfung BreadcrumbSheetPseudonym: Pseudonym @@ -1738,6 +1742,10 @@ TutorialParticipants: Teilnehmer TutorialCapacity: Kapazität TutorialFreeCapacity: Freie Plätze TutorialRoom: Regulärer Raum +TutorialRoomHidden: Raum nur für Teilnehmer +TutorialRoomHiddenTip: Soll der Raum nur den Teilnehmern des Tutoriums angezeigt werden? +TutorialRoomIsUnset: — +TutorialRoomIsHidden: Raum wird nur Teilnehmern angezeigt TutorialTime: Zeit TutorialRegistered: Angemeldet TutorialRegGroup: Registrierungs-Gruppe @@ -1839,6 +1847,7 @@ ExamFinished: Ergebnisse sichtbar ab ExamFinishedOffice: Noten bekannt gegeben ExamFinishedParticipant: Bewertung voraussichtlich abgeschlossen ExamFinishedTip: Zeitpunkt zu dem Prüfungergebnisse den Teilnehmern gemeldet werden; ohne Datum werden die Prüfungsergebnisse zunächst nie gemeldet +ExamFinishedTipCloseOnFinished: Zeitpunkt zu dem Prüfungergebnisse den Teilnehmern und den Prüfungsverwaltungen gemeldet werden; ohne Datum werden die Prüfungsergebnisse zunächst nie gemeldet ExamClosed: Noten gemeldet ExamClosedTip: Prüfungsbeauftraget, die im System Noten einsehen, werden zu diesem Zeitpunkt benachrichtigt und danach bei Änderungen informiert ExamGradingMode: Bewertungsmodus @@ -1886,6 +1895,8 @@ ExamRoomSurname': Nach Nachname ExamRoomMatriculation': Nach Matrikelnummer ExamRoomRandom': Zufällig pro Teilnehmer ExamRoomFifo': Auswahl durch Teilnehmer bei Anmeldung +ExamOccurrenceRoomIsUnset: — +ExamOccurrenceRoomIsHidden: Raum wird nur Teilnehmern angezeigt ExamOccurrence: Termin/Raum ExamNoOccurrence: Kein Termin/Raum @@ -1894,6 +1905,8 @@ ExamOccurrences: Termine ExamRooms: Räume ExamTimes: Termine ExamRoomRoom: Raum +ExamRoomRoomHidden: Raum nur für Angemeldete +ExamRoomRoomHiddenTip: Soll der Raum nur zu diesem Termin/Raum angemeldeten Prüfungsteilnehmern angezeigt werden? ExamRoomAlreadyExists: Prüfung ist bereits eingetragen ExamRoomName: Interne Bezeichnung ExamRoomCapacity: Kapazität @@ -2501,6 +2514,7 @@ BtnCloseExam: Prüfung abschließen ExamCloseTip: Wenn eine Prüfung abgeschlossen wird, werden Prüfungsbeauftragte, die im System Noten einsehen, benachrichtigt und danach bei Änderungen informiert. ExamCloseReminder: Bitte schließen Sie die Prüfung frühstmöglich, sobald die Prüfungsleistungen sich voraussichtlich nicht mehr ändern werden. Z.B. direkt nach der Klausureinsicht. ExamDidClose: Prüfung erfolgreich abgeschlossen +ExamCloseTipOnFinished: Die Prüfung wird automatisch abgeschlossen, also Prüfungsbeauftragte, die im System Note einsehen, benachrichtigt und danach bei Änderungen informiert, sobald die Noten für die Prüfungsteilnehmer veröffentlicht werden. ExamClosedSince time@Text: Prüfung abgeschlossen seit #{time} @@ -2561,7 +2575,12 @@ CourseNewsActionDelete: Löschen CourseNewsActionCreate: Neue Nachricht CourseMaterial: Material CourseMaterialFree: Das Kursmaterial ist ohne Anmeldung frei zugänglich -CourseMaterialNotFree: Eine Anmeldung zum Kurs ist Voraussetzung zum Zugang zu Kursmaterial +CourseMaterialNotFree: Das Kursmaterial ist nur für Mitglieder des Kurses einsehbar, also z.B. für Teilnehmer, Tutoren, Korrektoren und Verwalter. + +CourseSheetsFoundHere: Die Übungsblatter zum Kurs finden Sie hier +CourseSheetsNoneVisible: Aktuell gibt es zu diesem Kurs keine Übungsblätter, oder nur Übungsblätter auf die Sie keinen Zugriff haben (z.B. aufgrund von Fristen bzgl. der Sichtbarkeit). +CourseMaterialsFoundHere: Material zum Kurs finden Sie hier +CourseMaterialsNoneVisible: Aktuell gibt es zu diesem Kurs kein Material, oder nur Material auf das Sie keinen Zugriff haben (z.B. aufgrund von Fristen bzgl. der Sichtbarkeit). CourseNewsVisibleFromEditWarning: Das Datum der Veröffentlichung liegt in der Vergangenheit und sollte nicht mehr verändert werden, da dies die Teilnehmer verwirren könnte. CourseNewsVisibleFromTip: Ohne Datum nie sichtbar für Teilnehmer; leer lassen ist nur sinnvoll für noch unfertige Nachrichten @@ -2616,6 +2635,10 @@ CourseEventType: Art CourseEventTypePlaceholder: Vorlesung, Zentralübung, ... CourseEventTime: Zeit CourseEventRoom: Regulärer Raum +CourseEventRoomHidden: Raum nur für Teilnehmer +CourseEventRoomHiddenTip: Soll der Raum nur angemeldeten Kursteilnehmern angezeigt werden? +CourseEventRoomIsUnset: — +CourseEventRoomIsHidden: Raum wird nur Kurs-assoziierten Personen (Teilnehmer, Tutoren, Korrektoren, etc.) angezeigt CourseEventNote: Notiz CourseEventActions: Aktionen CourseEventsActionEdit: Bearbeiten @@ -3069,3 +3092,20 @@ InvalidCredentialsADTooManyContextIds: Benutzereintrag trägt zu viele Sicherhei InvalidCredentialsADAccountExpired: Benutzereintrag abgelaufen InvalidCredentialsADPasswordMustChange: Passwort muss geändert werden InvalidCredentialsADAccountLockedOut: Benutzereintrag wurde durch Eindringlingserkennung gesperrt + +ExamCloseModeSeparate: Separat +ExamCloseModeOnFinished: Mit Veröffentlichung +ExamCloseModeOnFinishedHidden: Mit Veröffentlichung (versteckt) +ExamCloseMode: Prüfungs-Abschluss + +RoomReferenceSimple: Text +RoomReferenceLink: Link & Anweisungen +RoomReferenceSimpleText: Raum +RoomReferenceSimpleTextPlaceholder: Raum +RoomReferenceLinkLink: Link +RoomReferenceLinkLinkPlaceholder: URL +RoomReferenceLinkInstructions: Anweisungen +RoomReferenceLinkInstructionsPlaceholder: Anweisungen +RoomReferenceNone: — + +UrlFieldCouldNotParseAbsolute: Konnte nicht als absolute URL interpretiert werden diff --git a/messages/uniworx/en-eu.msg b/messages/uniworx/en-eu.msg index c9332920a..8814dabf3 100644 --- a/messages/uniworx/en-eu.msg +++ b/messages/uniworx/en-eu.msg @@ -433,7 +433,7 @@ MaterialVisibleFromTip: Never visible to participants if left empty; leaving the MaterialVisibleFromEditWarning: This course material has already been published and should not be edited. Doing so might confuse the participants. MaterialInvisible: This course material is currently invisible to participants! MaterialFiles: Files -MaterialHeading materialName: Course material “#{materialName}” +MaterialHeading materialName: #{materialName} MaterialListHeading: Course materials MaterialNewHeading: Publish new course material MaterialNewTitle: New course material @@ -446,6 +446,9 @@ MaterialDelHasFiles count: including #{count} #{pluralEN count "file" "files"} MaterialIsVisible: Caution, this course material has already been published. MaterialDeleted materialName: Successfully deleted course material “#{materialName}” MaterialArchiveName tid ssh csh materialName: #{foldCase (termToText (unTermKey tid))}-#{foldedCase (unSchoolKey ssh)}-#{foldedCase csh}-#{foldedCase materialName} +MaterialVideo materialName: #{materialName} - Video +MaterialVideoUnsupported: Your browser does not seem to support embedded video +MaterialVideoDownload: Download Unauthorized: You do not have explicit authorisation. UnauthorizedAnd l r: (#{l} AND #{r}) @@ -1015,10 +1018,10 @@ MailCourseRegisteredIntroOther displayName courseName termDesc: #{displayName} w MailSubjectExamResult csh examn: Results for #{examn} in #{csh} are now available MailExamResultIntro courseName termDesc examn: You may now view your result for #{examn} of the course #{courseName} (#{termDesc}). -MailSubjectExamOfficeExamResults csh examn: Results for #{examn} of #{csh} are now available +MailSubjectExamOfficeExamResults coursen examn: Results for #{examn} of #{coursen} are now available MailExamOfficeExamResultsIntro courseName termDesc examn: A course administrator has made the results for #{examn} of the course #{courseName} (#{termDesc}) available. -MailSubjectExamOfficeExamResultsChanged csh examn: Results for #{examn} of #{csh} were changed +MailSubjectExamOfficeExamResultsChanged coursen examn: Results for #{examn} of #{coursen} were changed MailExamOfficeExamResultsChangedIntro courseName termDesc examn: A course administrator has changed exam results for #{examn} of the course #{courseName} (#{termDesc}). MailSubjectExamOfficeExternalExamResults coursen@CourseName examn@ExamName: Results for #{examn} in #{coursen} @@ -1446,6 +1449,7 @@ BreadcrumbAllocationInfo: On central allocations BreadcrumbCourseParticipantInvitation: Invitation to be a course participant BreadcrumbMaterialArchive: Archive BreadcrumbMaterialFile: File +BreadcrumbMaterialVideo: Video BreadcrumbSheetArchive: Files BreadcrumbSheetIsCorrector: Corrector-check BreadcrumbSheetPseudonym: Pseudonym @@ -1697,6 +1701,10 @@ TutorialParticipants: Participants TutorialCapacity: Capacity TutorialFreeCapacity: Free capacity TutorialRoom: Regular room +TutorialRoomHidden: Room only for participants +TutorialRoomHiddenTip: Should the room only be displayed to tutorial participants? +TutorialRoomIsUnset: — +TutorialRoomIsHidden: Room is only displayed to participants TutorialTime: Time TutorialRegistered: Registered TutorialRegGroup: Registration group @@ -1798,6 +1806,7 @@ ExamFinished: Results visible from ExamFinishedOffice: Exam achievements published ExamFinishedParticipant: Marking expected to be finished ExamFinishedTip: At this participants are informed of their exam achievements. If left empty participants are never informed of their exam achievements. +ExamFinishedTipCloseOnFinished: At this time participants and exam offices are informed of the exam achievements. If left empty participants and exam offices are never informed of the exam achievements. ExamClosed: Exam achievements registered ExamClosedTip: At this time exam offices, which pull exam achievements from Uni2work, are informed. Changes to exam achievements trigger further notifications ExamGradingMode: Grading mode @@ -1845,6 +1854,8 @@ ExamRoomSurname': By surname ExamRoomMatriculation': By matriculation ExamRoomRandom': Randomly ExamRoomFifo': Selected by the participants when registering +ExamOccurrenceRoomIsUnset: — +ExamOccurrenceRoomIsHidden: Room is only displayed to participants registered for this occurrence/room ExamOccurrence: Occurrence/room ExamNoOccurrence: No occurrence/room @@ -1853,6 +1864,8 @@ ExamOccurrences: Exams ExamRooms: Rooms ExamTimes: Times ExamRoomRoom: Room +ExamRoomRoomHidden: Room only for participants +ExamRoomRoomHiddenTip: Should the room only be displayed to participants registered for this occurrence/room? ExamRoomAlreadyExists: Occurrence already configured ExamRoomName: Internal name ExamRoomCapacity: Capacity @@ -2461,6 +2474,7 @@ BtnCloseExam: Close exam ExamCloseTip: When an exam is closed all relevant exam offices, which pull exam achievements from Uni2work, are informed and kept up to date with changes. ExamCloseReminder: Please close the exam as soon as possible, when exam achievements are no longer expected to change e.g. after inspection of the exam has concluced. ExamDidClose: Successfully closed exam +ExamCloseTipOnFinished: The exam will be closed automatically as soon as exam participants are informed of their exam achievements. That means exam offices will be able notified once and after that each time a grade changes. ExamClosedSince time: Exam closed since #{time} @@ -2520,8 +2534,13 @@ CourseNewsActionEdit: Edit CourseNewsActionDelete: Delete CourseNewsActionCreate: Create new item CourseMaterial: Material -CourseMaterialFree: Course material is publicly accessable -CourseMaterialNotFree: Only course participants may access course material +CourseMaterialFree: Course material is publicly accessible +CourseMaterialNotFree: Course material is only accessible to members of the course, e.g. for participants, tutors, correctors or administratiors. + +CourseSheetsFoundHere: Exercise sheets for this course are available here +CourseSheetsNoneVisible: Currently there are no exercise sheets for this course or only exercise sheets to which you don't have access (e.g. because of visibility settings) +CourseMaterialsFoundHere: Material for this course is available here +CourseMaterialsNoneVisible: Currently there is no material for this course or only material to which you don't have access (e.g. because of visibility settings) CourseNewsVisibleFromEditWarning: This item of course news has already been published and should no longer be changed sind this might confuse participants. CourseNewsVisibleFromTip: If left empty this item is never visible. Leave empty for unfinished items @@ -2576,6 +2595,10 @@ CourseEventType: Type CourseEventTypePlaceholder: Lecture, Exercise discussion, ... CourseEventTime: Time CourseEventRoom: Regular room +CourseEventRoomHidden: Room only for participants +CourseEventRoomHiddenTip: Should the room only be displayde to course participants? +CourseEventRoomIsUnset: — +CourseEventRoomIsHidden: Room is only displayed to course associated persons (participants, tutor, correctors, etc.) CourseEventNote: Note CourseEventActions: Actions CourseEventsActionEdit: Edit @@ -2959,3 +2982,20 @@ InvalidCredentialsADTooManyContextIds: Account carries to many security identifi InvalidCredentialsADAccountExpired: Account expired InvalidCredentialsADPasswordMustChange: Password needs to be changed InvalidCredentialsADAccountLockedOut: Account disabled by intruder detection + +ExamCloseModeSeparate: Seperately +ExamCloseModeOnFinished: With publication of achievements +ExamCloseModeOnFinishedHidden: With publication of achievements (hidden) +ExamCloseMode: Exam closure + +RoomReferenceSimple: Text +RoomReferenceLink: Link & Instructions +RoomReferenceSimpleText: Room +RoomReferenceSimpleTextPlaceholder: Room +RoomReferenceLinkLink: Link +RoomReferenceLinkLinkPlaceholder: URL +RoomReferenceLinkInstructions: Instructions +RoomReferenceLinkInstructionsPlaceholder: Instructions +RoomReferenceNone: — + +UrlFieldCouldNotParseAbsolute: Could not parse as an absolute URL diff --git a/models/allocations.model b/models/allocations.model index 0bbebbea5..e4e96f6b2 100644 --- a/models/allocations.model +++ b/models/allocations.model @@ -3,8 +3,8 @@ Allocation -- attributes with prefix staff- affect lecturers only, but are invis school SchoolId -- school that manages this central allocation, not necessarily school of courses shorthand AllocationShorthand -- practical shorthand name AllocationName - description Html Maybe -- description for prospective students - staffDescription Html Maybe -- description seen by prospective lecturers only + description StoredMarkup Maybe -- description for prospective students + staffDescription StoredMarkup Maybe -- description seen by prospective lecturers only staffRegisterFrom UTCTime Maybe -- lectureres may register courses staffRegisterTo UTCTime Maybe -- course registration stops -- staffDeregisterUntil not needed: staff may make arbitrary changes until staffRegisterTo, always frozen afterwards diff --git a/models/courses.model b/models/courses.model index 6033ff0a9..d64ec14ac 100644 --- a/models/courses.model +++ b/models/courses.model @@ -5,8 +5,8 @@ DegreeCourse json -- for which degree programmes this course is appropriate fo UniqueDegreeCourse course degree terms Course -- Information about a single course; contained info is always visible to all users name (CI Text) - description Html Maybe -- user-defined large Html, ought to contain module description - linkExternal Text Maybe -- arbitrary user-defined url for external course page + description StoredMarkup Maybe -- user-defined large Html, ought to contain module description + linkExternal URI Maybe -- arbitrary user-defined url for external course page shorthand (CI Text) -- practical shorthand of course name, used for identification term TermId -- semester this course is taught school SchoolId @@ -21,7 +21,7 @@ Course -- Information about a single course; contained info is always visible registerSecret Text Maybe -- enrolement maybe protected by a simple common passphrase materialFree Bool -- False: only enrolled users may see course materials not stored in this table applicationsRequired Bool default=false - applicationsInstructions Html Maybe + applicationsInstructions StoredMarkup Maybe applicationsText Bool default=false applicationsFiles UploadMode "default='{\"mode\": \"no-upload\"}'::jsonb" applicationsRatingsVisible Bool default=false @@ -31,9 +31,10 @@ Course -- Information about a single course; contained info is always visible CourseEvent type (CI Text) course CourseId - room Text + room RoomReference Maybe + roomHidden Bool default=false time Occurrences - note Html Maybe + note StoredMarkup Maybe lastChanged UTCTime default=now() CourseAppInstructionFile @@ -72,7 +73,7 @@ CourseParticipant -- course enrolement CourseUserNote -- lecturers of a specific course may share a text note on each enrolled student course CourseId user UserId - note Html -- arbitrary user-defined text; visible only to lecturer of this course + note StoredMarkup -- arbitrary user-defined text; visible only to lecturer of this course UniqueCourseUserNote user course CourseUserNoteEdit -- who edited a participants course note when user UserId diff --git a/models/courses/materials.model b/models/courses/materials.model index fd1d19fb7..3a4767ec5 100644 --- a/models/courses/materials.model +++ b/models/courses/materials.model @@ -2,7 +2,7 @@ Material -- course material for disemination to course participants course CourseId name (CI Text) type (CI Text) Maybe - description Html Maybe + description StoredMarkup Maybe visibleFrom UTCTime Maybe -- Invisible to enrolled participants before lastEdit UTCTime UniqueMaterial course name diff --git a/models/courses/news.model b/models/courses/news.model index fdd2c0254..c31312d2e 100644 --- a/models/courses/news.model +++ b/models/courses/news.model @@ -3,8 +3,8 @@ CourseNews visibleFrom UTCTime Maybe participantsOnly Bool title Text Maybe - content Html - summary Html Maybe + content StoredMarkup + summary StoredMarkup Maybe lastEdit UTCTime CourseNewsFile news CourseNewsId diff --git a/models/exams.model b/models/exams.model index 4963e4075..1f496f43a 100644 --- a/models/exams.model +++ b/models/exams.model @@ -16,7 +16,7 @@ Exam closed UTCTime Maybe -- Prüfungsamt hat Einsicht (notification) publicStatistics Bool gradingMode ExamGradingMode - description Html Maybe + description StoredMarkup Maybe examMode ExamMode staff Text Maybe UniqueExam course name @@ -31,11 +31,12 @@ ExamPart ExamOccurrence exam ExamId name ExamOccurrenceName - room Text + room RoomReference Maybe + roomHidden Bool default=false capacity Natural start UTCTime end UTCTime Maybe - description Html Maybe + description StoredMarkup Maybe UniqueExamOccurrence exam name ExamRegistration exam ExamId diff --git a/models/schools.model b/models/schools.model index 950b1c624..af9e54889 100644 --- a/models/schools.model +++ b/models/schools.model @@ -7,6 +7,7 @@ School json examMinimumRegisterDuration NominalDiffTime Maybe examRequireModeForRegistration Bool default=false examDiscouragedModes ExamModeDNF + examCloseMode ExamCloseMode default='separate' UniqueSchool name UniqueSchoolShorthand shorthand -- required for Normalisation of CI Text Primary shorthand -- newtype Key School = SchoolKey { unSchoolKey :: SchoolShorthand } diff --git a/models/sheets.model b/models/sheets.model index f54426040..6f0bb6176 100644 --- a/models/sheets.model +++ b/models/sheets.model @@ -1,10 +1,10 @@ Sheet -- exercise sheet for a given course course CourseId name (CI Text) - description Html Maybe + description StoredMarkup Maybe type SheetType -- Does it count towards overall course grade? grouping SheetGroup -- May participants submit in groups of certain sizes? - markingText Html Maybe -- Instructons for correctors, included in marking templates + markingText StoredMarkup Maybe -- Instructons for correctors, included in marking templates visibleFrom UTCTime Maybe -- Invisible to enrolled participants before activeFrom UTCTime Maybe -- Download of questions and submission is permitted afterwards activeTo UTCTime Maybe -- Submission is only permitted before diff --git a/models/system-messages.model b/models/system-messages.model index 0d5cd5611..e8dfbd9ad 100644 --- a/models/system-messages.model +++ b/models/system-messages.model @@ -11,14 +11,14 @@ SystemMessage lastChanged UTCTime default=now() lastUnhide UTCTime default=now() defaultLanguage Lang -- Language of @content@ and @summary@ - content Html -- Detailed message shown when clicking on the @summary@-popup or when no @summary@ is specified - summary Html Maybe + content StoredMarkup -- Detailed message shown when clicking on the @summary@-popup or when no @summary@ is specified + summary StoredMarkup Maybe SystemMessageTranslation -- Translation of a @SystemMessage@ into another language; which language to choose is determined by user-sent HTTP-headers message SystemMessageId language Lang - content Html - summary Html Maybe + content StoredMarkup + summary StoredMarkup Maybe UniqueSystemMessageTranslation message language SystemMessageHidden diff --git a/models/tutorials.model b/models/tutorials.model index 90066fcb1..d193ff5d5 100644 --- a/models/tutorials.model +++ b/models/tutorials.model @@ -3,7 +3,8 @@ Tutorial json course CourseId type (CI Text) -- "Tutorium", "Zentralübung", ... capacity Int Maybe -- limit for enrolment in this tutorial - room Text Maybe + room RoomReference Maybe + roomHidden Bool default=false time Occurrences regGroup (CI Text) Maybe -- each participant may register for one tutorial per regGroup registerFrom UTCTime Maybe diff --git a/models/workflows.model b/models/workflows.model index e3f258ecd..590b79744 100644 --- a/models/workflows.model +++ b/models/workflows.model @@ -9,14 +9,14 @@ WorkflowDefinitionDescription definition WorkflowDefinitionId language Lang title Text - description Html Maybe + description StoredMarkup Maybe UniqueWorkflowDefinitionDescription definition language WorkflowDefinitionInstanceDescription definition WorkflowDefinitionId language Lang title Text - description Html Maybe + description StoredMarkup Maybe UniqueWorkflowDefinitionInstanceDescription definition language WorkflowInstance @@ -31,7 +31,7 @@ WorkflowInstanceDescription instance WorkflowInstanceId language Lang title Text - description Html Maybe + description StoredMarkup Maybe UniqueWorkflowInstanceDescription instance language WorkflowWorkflow diff --git a/package-lock.json b/package-lock.json index acad56503..992dc44c6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "uni2work", - "version": "21.1.0", + "version": "22.1.1", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index e573d0ac6..e7f198ec4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "uni2work", - "version": "21.1.0", + "version": "22.1.1", "description": "", "keywords": [], "author": "", diff --git a/package.yaml b/package.yaml index ef2c4c728..6328dde4f 100644 --- a/package.yaml +++ b/package.yaml @@ -1,5 +1,5 @@ name: uniworx -version: 21.1.0 +version: 22.1.1 dependencies: - base @@ -8,6 +8,7 @@ dependencies: - yesod-auth - yesod-static - yesod-form + - yesod-persistent - classy-prelude - classy-prelude-yesod - bytestring @@ -157,6 +158,7 @@ dependencies: - list-t - insert-ordered-containers - topograph + - network-uri other-extensions: - GeneralizedNewtypeDeriving @@ -318,6 +320,7 @@ tests: - http-types - yesod-persistent - quickcheck-io + - network-arbitrary ghc-options: - -fno-warn-orphans - -threaded -rtsopts "-with-rtsopts=-N -T" diff --git a/routes b/routes index f3bc11243..e76b6eb4b 100644 --- a/routes +++ b/routes @@ -204,6 +204,7 @@ /show MShowR GET !timeANDcourse-registered !timeANDmaterialsANDcourse-time !corrector !tutor !/download MArchiveR GET !timeANDcourse-registered !timeANDmaterialsANDcourse-time !corrector !tutor !/download/*FilePath MFileR GET !timeANDcourse-registered !timeANDmaterialsANDcourse-time !corrector !tutor + /video/#CryptoUUIDMaterialFile MVideoR GET !timeANDcourse-registered !timeANDmaterialsANDcourse-time !corrector !tutor /tuts CTutorialListR GET !tutor -- THIS route is used to check for overall course tutor access! /tuts/new CTutorialNewR GET POST /tuts/#TutorialName TutorialR: diff --git a/src/CryptoID.hs b/src/CryptoID.hs index 4183bf75d..96e022d2a 100644 --- a/src/CryptoID.hs +++ b/src/CryptoID.hs @@ -75,6 +75,7 @@ decCryptoIDs [ ''SubmissionId , ''ExternalExamId , ''WorkflowInstanceId , ''WorkflowWorkflowId + , ''MaterialFileId ] type instance CryptoIDNamespace a WorkflowStateIndex = "WorkflowStateIndex" diff --git a/src/Database/Esqueleto/Utils.hs b/src/Database/Esqueleto/Utils.hs index 66061ec3e..9a5412cfa 100644 --- a/src/Database/Esqueleto/Utils.hs +++ b/src/Database/Esqueleto/Utils.hs @@ -20,12 +20,12 @@ module Database.Esqueleto.Utils , selectExists, selectNotExists , SqlHashable , sha256 - , maybe, maybeEq, unsafeCoalesce + , maybe, maybe2, maybeEq, unsafeCoalesce , bool , max, min , abs , SqlProject(..) - , (->.) + , (->.), (#>>.) , fromSqlKey , selectCountRows , selectMaybe @@ -302,6 +302,20 @@ maybe onNothing onJust val = E.case_ ] (E.else_ onNothing) +maybe2 :: (PersistField a, PersistField b, PersistField c) + => E.SqlExpr (E.Value c) + -> (E.SqlExpr (E.Value a) -> E.SqlExpr (E.Value b) -> E.SqlExpr (E.Value c)) + -> E.SqlExpr (E.Value (Maybe a)) + -> E.SqlExpr (E.Value (Maybe b)) + -> E.SqlExpr (E.Value c) +maybe2 onNothing onJust val1 val2 = E.case_ + [ E.when_ + (isJust val1 E.&&. isJust val2) + E.then_ + (onJust (E.veryUnsafeCoerceSqlExprValue val1) (E.veryUnsafeCoerceSqlExprValue val2)) + ] + (E.else_ onNothing) + infix 4 `maybeEq` maybeEq :: PersistField a @@ -350,23 +364,32 @@ unsafeCoalesce :: E.PersistField a => [E.SqlExpr (E.Value (Maybe a))] -> E.SqlEx unsafeCoalesce = E.veryUnsafeCoerceSqlExprValue . E.coalesce -class (PersistEntity entity, PersistField value) => SqlProject entity value entity' value' | entity value entity' -> value', entity value value' -> entity' where +class (PersistEntity entity, PersistField value, PersistField value') => SqlProject entity value entity' value' | entity value entity' -> value', entity value value' -> entity' where sqlProject :: E.SqlExpr entity' -> EntityField entity value -> E.SqlExpr (E.Value value') unSqlProject :: forall p1 p2. p1 entity -> p2 entity' -> value -> value' + unSqlProjectExpr :: forall p1 p2. p1 entity -> p2 entity' -> E.SqlExpr (E.Value value) -> E.SqlExpr (E.Value value') instance (PersistEntity val, PersistField typ) => SqlProject val typ (E.Entity val) typ where sqlProject = (E.^.) unSqlProject _ _ = id + unSqlProjectExpr _ _ = id instance (PersistEntity val, PersistField typ) => SqlProject val typ (Maybe (E.Entity val)) (Maybe typ) where sqlProject = (E.?.) unSqlProject _ _ = Just + unSqlProjectExpr _ _ = E.just infixl 8 ->. (->.) :: E.SqlExpr (E.Value a) -> Text -> E.SqlExpr (E.Value b) (->.) expr t = E.unsafeSqlBinOp "->" expr $ E.val t +infixl 8 #>>. + +(#>>.) :: E.SqlExpr (E.Value a) -> Text -> E.SqlExpr (E.Value (Maybe Text)) +(#>>.) expr t = E.unsafeSqlBinOp "#>>" expr $ E.val t + + fromSqlKey :: (ToBackendKey SqlBackend entity, PersistField (Key entity)) => E.SqlExpr (E.Value (Key entity)) -> E.SqlExpr (E.Value Int64) fromSqlKey = E.veryUnsafeCoerceSqlExprValue diff --git a/src/Foundation/I18n.hs b/src/Foundation/I18n.hs index 42a820930..2ebf6e554 100644 --- a/src/Foundation/I18n.hs +++ b/src/Foundation/I18n.hs @@ -230,11 +230,14 @@ embedRenderMessage ''UniWorX ''ExamSynchronicityPreset id embedRenderMessage ''UniWorX ''ExamRequiredEquipmentPreset id embedRenderMessage ''UniWorX ''ChangelogItemKind id embedRenderMessage ''UniWorX ''WorkflowScope' $ ("WorkflowScopeKind" <>) . concat . drop 1 . splitCamel . fromMaybe (error "Expected WorkflowScope' to have '") . stripSuffix "'" +embedRenderMessage ''UniWorX ''RoomReference' $ dropSuffix "'" embedRenderMessage ''UniWorX ''AuthenticationMode id embedRenderMessage ''UniWorX ''RatingValidityException id +embedRenderMessage ''UniWorX ''UrlFieldMessage id + embedRenderMessageVariant ''UniWorX ''ADInvalidCredentials ("InvalidCredentials" <>) newtype ShortSex = ShortSex Sex @@ -326,6 +329,15 @@ instance RenderMessage UniWorX CourseParticipantState where mr :: RenderMessage UniWorX msg => msg -> Text mr = renderMessage foundation ls +instance RenderMessage UniWorX ExamCloseMode where + renderMessage foundation ls = \case + ExamCloseSeparate -> mr MsgExamCloseModeSeparate + ExamCloseOnFinished False -> mr MsgExamCloseModeOnFinished + ExamCloseOnFinished True -> mr MsgExamCloseModeOnFinishedHidden + where + mr :: RenderMessage UniWorX msg => msg -> Text + mr = renderMessage foundation ls + -- ToMessage instances for converting raw numbers to Text (no internationalization) instance ToMessage Int where diff --git a/src/Foundation/Navigation.hs b/src/Foundation/Navigation.hs index 0a9c24d57..60e384ad5 100644 --- a/src/Foundation/Navigation.hs +++ b/src/Foundation/Navigation.hs @@ -291,6 +291,7 @@ instance BearerAuthSite UniWorX => YesodBreadcrumbs UniWorX where MDelR -> i18nCrumb MsgMenuMaterialDelete . Just $ CMaterialR tid ssh csh mnm MShowR MArchiveR -> i18nCrumb MsgBreadcrumbMaterialArchive . Just $ CMaterialR tid ssh csh mnm MShowR MFileR _ -> i18nCrumb MsgBreadcrumbMaterialFile . Just $ CMaterialR tid ssh csh mnm MShowR + MVideoR _ -> i18nCrumb MsgBreadcrumbMaterialVideo . Just $ CMaterialR tid ssh csh mnm MShowR breadcrumb (CourseR tid ssh csh CPersonalFilesR) = i18nCrumb MsgBreadcrumbCourseSheetPersonalisedFiles . Just $ CourseR tid ssh csh CShowR diff --git a/src/Foundation/SiteLayout.hs b/src/Foundation/SiteLayout.hs index 8ca6c3b29..9f151bca7 100644 --- a/src/Foundation/SiteLayout.hs +++ b/src/Foundation/SiteLayout.hs @@ -454,7 +454,7 @@ applySystemMessages = liftHandler . maybeT_ . catchMPlus (Proxy @CryptoIDError) case summary of Just s -> addMessageWidget systemMessageSeverity $ msgModal (toWidget s) (Left . SomeRoute $ MessageR cID) - Nothing -> addMessage systemMessageSeverity content + Nothing -> addMessage systemMessageSeverity $ toHtml content tellRegisteredCookieJson CookieSystemMessageState . MergeHashMap $ HashMap.singleton cID mempty{ userSystemMessageShown = Just now } diff --git a/src/Foundation/Yesod/Middleware.hs b/src/Foundation/Yesod/Middleware.hs index 311321ae8..c392a9245 100644 --- a/src/Foundation/Yesod/Middleware.hs +++ b/src/Foundation/Yesod/Middleware.hs @@ -164,6 +164,7 @@ routeNormalizers = map (hoist (hoist liftHandler . withReaderT projectBackend) . , verifyCourseApplication , verifyCourseNews , verifyWorkflowWorkflow + , verifyMaterialVideo ] where normalizeRender :: Route UniWorX -> WriterT Any (ReaderT SqlReadBackend (HandlerFor UniWorX)) (Route UniWorX) @@ -291,3 +292,12 @@ routeNormalizers = map (hoist (hoist liftHandler . withReaderT projectBackend) . let newRoute = _WorkflowScopeRoute # (rScope, WorkflowWorkflowR cID wwR) tell . Any $ route /= newRoute return newRoute + verifyMaterialVideo = maybeOrig $ \route -> do + CMaterialR _tid _ssh _csh _mnm (MVideoR cID) <- return route + mfId <- decrypt cID + MaterialFile{materialFileMaterial} <- lift . lift $ get404 mfId + Material{materialName, materialCourse} <- lift . lift $ get404 materialFileMaterial + Course{courseTerm, courseSchool, courseShorthand} <- lift . lift $ get404 materialCourse + let newRoute = CMaterialR courseTerm courseSchool courseShorthand materialName (MVideoR cID) + tell . Any $ route /= newRoute + return newRoute diff --git a/src/Handler/Course/Edit.hs b/src/Handler/Course/Edit.hs index d1a340740..5c58a0385 100644 --- a/src/Handler/Course/Edit.hs +++ b/src/Handler/Course/Edit.hs @@ -34,14 +34,14 @@ data CourseForm = CourseForm , cfShort :: CourseShorthand , cfSchool :: SchoolId , cfTerm :: TermId - , cfDesc :: Maybe Html - , cfLink :: Maybe Text + , cfDesc :: Maybe StoredMarkup + , cfLink :: Maybe URI , cfVisFrom :: Maybe UTCTime , cfVisTo :: Maybe UTCTime , cfMatFree :: Bool , cfAllocation :: Maybe AllocationCourseForm , cfAppRequired :: Bool - , cfAppInstructions :: Maybe Html + , cfAppInstructions :: Maybe StoredMarkup , cfAppInstructionFiles :: Maybe FileUploads , cfAppText :: Bool , cfAppFiles :: UploadMode @@ -292,7 +292,7 @@ makeCourseForm miButtonAction template = identifyForm FIDcourse . validateFormDB <*> areq termsField (fslI MsgCourseSemester) (cfTerm <$> template) <*> aopt htmlField (fslpI MsgCourseDescription (mr MsgCourseDescriptionPlaceholder)) (cfDesc <$> template) - <*> aopt (urlField & cfStrip) (fslpI MsgCourseHomepageExternal (mr MsgCourseHomepageExternalPlaceholder)) + <*> aopt urlField (fslpI MsgCourseHomepageExternal (mr MsgCourseHomepageExternalPlaceholder)) (cfLink <$> template) <*> aopt utcTimeField (fslpI MsgCourseVisibleFrom (mr MsgDate) & setTooltip MsgCourseVisibleFromTip) (deepAlt (cfVisFrom <$> template) newVisFrom) diff --git a/src/Handler/Course/Events/Delete.hs b/src/Handler/Course/Events/Delete.hs index 3dd5d06fb..65c742bbf 100644 --- a/src/Handler/Course/Events/Delete.hs +++ b/src/Handler/Course/Events/Delete.hs @@ -27,7 +27,8 @@ postCEvDeleteR tid ssh csh cID = do [whamlet| $newline never #{courseEventType} - , #{courseEventRoom} + $maybe room <- courseEventRoom + , #{roomReferenceText room} : ^{occurrencesWidget courseEventTime} |] diff --git a/src/Handler/Course/Events/Edit.hs b/src/Handler/Course/Events/Edit.hs index 0dcfaa30a..19390889d 100644 --- a/src/Handler/Course/Events/Edit.hs +++ b/src/Handler/Course/Events/Edit.hs @@ -23,6 +23,7 @@ postCEvEditR tid ssh csh cID = do { courseEventCourse , courseEventType = cefType , courseEventRoom = cefRoom + , courseEventRoomHidden = cefRoomHidden , courseEventTime = cefTime , courseEventNote = cefNote , courseEventLastChanged = now diff --git a/src/Handler/Course/Events/Form.hs b/src/Handler/Course/Events/Form.hs index ecc01b8e9..1f3eb88bc 100644 --- a/src/Handler/Course/Events/Form.hs +++ b/src/Handler/Course/Events/Form.hs @@ -13,9 +13,10 @@ import qualified Database.Esqueleto as E data CourseEventForm = CourseEventForm { cefType :: CI Text - , cefRoom :: Text + , cefRoom :: Maybe RoomReference + , cefRoomHidden :: Bool , cefTime :: Occurrences - , cefNote :: Maybe Html + , cefNote :: Maybe StoredMarkup } courseEventForm :: Maybe CourseEventForm -> Form CourseEventForm @@ -30,16 +31,17 @@ courseEventForm template = identifyForm FIDCourseEvent . renderWForm FormStandar return event ) let courseEventTypes = optionsPairs [ (courseEventType, courseEventType) | Entity _ CourseEvent{..} <- existingEvents ] - courseEventRooms = optionsPairs [ (courseEventRoom, courseEventRoom) | Entity _ CourseEvent{..} <- existingEvents ] cefType' <- wreq (textField & cfStrip & cfCI & addDatalist courseEventTypes) (fslI MsgCourseEventType & addPlaceholder (mr MsgCourseEventTypePlaceholder)) (cefType <$> template) - cefRoom' <- wreq (textField & cfStrip & addDatalist courseEventRooms) (fslI MsgCourseEventRoom) (cefRoom <$> template) + cefRoom' <- aFormToWForm $ roomReferenceFormOpt (fslI MsgCourseEventRoom) (cefRoom <$> template) + cefRoomHidden' <- wpopt checkBoxField (fslI MsgCourseEventRoomHidden & setTooltip MsgCourseEventRoomHiddenTip) (cefRoomHidden <$> template) cefTime' <- aFormToWForm $ occurrencesAForm ("time" :: Text) (cefTime <$> template) cefNote' <- wopt htmlField (fslI MsgCourseEventNote) (cefNote <$> template) return $ CourseEventForm <$> cefType' <*> cefRoom' + <*> cefRoomHidden' <*> cefTime' <*> cefNote' @@ -47,6 +49,7 @@ courseEventToForm :: CourseEvent -> CourseEventForm courseEventToForm CourseEvent{..} = CourseEventForm { cefType = courseEventType , cefRoom = courseEventRoom + , cefRoomHidden = courseEventRoomHidden , cefTime = courseEventTime , cefNote = courseEventNote } diff --git a/src/Handler/Course/Events/New.hs b/src/Handler/Course/Events/New.hs index b389de9de..d2800db1e 100644 --- a/src/Handler/Course/Events/New.hs +++ b/src/Handler/Course/Events/New.hs @@ -21,6 +21,7 @@ postCEventsNewR tid ssh csh = do { courseEventCourse = cid , courseEventType = cefType , courseEventRoom = cefRoom + , courseEventRoomHidden = cefRoomHidden , courseEventTime = cefTime , courseEventNote = cefNote , courseEventLastChanged = now diff --git a/src/Handler/Course/List.hs b/src/Handler/Course/List.hs index a996fc3e5..2ab517fce 100644 --- a/src/Handler/Course/List.hs +++ b/src/Handler/Course/List.hs @@ -81,7 +81,12 @@ makeCourseTable whereClause colChoices psValidator = do return (course, participants, registered, school) lecturerQuery cid (user `E.InnerJoin` lecturer) = do E.on $ user E.^. UserId E.==. lecturer E.^. LecturerUser - E.where_ $ cid E.==. lecturer E.^. LecturerCourse E.&&. lecturer E.^. LecturerType E.==. E.val CourseLecturer + E.where_ $ cid E.==. lecturer E.^. LecturerCourse + E.&&. lecturer E.^. LecturerType E.==. E.val CourseLecturer + return user + isCourseAdminQuery cid (user `E.InnerJoin` lecturer) = do + E.on $ user E.^. UserId E.==. lecturer E.^. LecturerUser + E.where_ $ cid E.==. lecturer E.^. LecturerCourse return user isEditorQuery course user = E.where_ $ mayEditCourse' muid ata course E.&&. E.just (user E.^. UserId) E.==. E.val muid @@ -135,8 +140,8 @@ makeCourseTable whereClause colChoices psValidator = do , ( "lecturer", FilterColumn $ \(course `E.InnerJoin` _school :: CourseTableExpr) criterias -> if | Set.null criterias -> E.val True :: E.SqlExpr (E.Value Bool) | otherwise -> E.exists $ E.from $ \t -> do - user <- lecturerQuery (course E.^. CourseId) t - E.where_ $ E.any (E.hasInfix (user E.^. UserSurname) . E.val) (criterias :: Set.Set Text) + user <- isCourseAdminQuery (course E.^. CourseId) t + E.where_ $ E.any (E.hasInfix (user E.^. UserDisplayName) . E.val) (criterias :: Set.Set Text) ) , ( "openregistration", FilterColumn $ \(course `E.InnerJoin` _school :: CourseTableExpr) criterion -> case getLast (criterion :: Last Bool) of Nothing -> E.val True @@ -167,7 +172,7 @@ makeCourseTable whereClause colChoices psValidator = do Nothing -> E.val True :: E.SqlExpr (E.Value Bool) Just needle -> (E.castString (course E.^. CourseName) `E.ilike` (E.%) E.++. E.val needle E.++. (E.%)) E.||. (E.castString (course E.^. CourseShorthand) `E.ilike` (E.%) E.++. E.val needle E.++. (E.%)) - E.||. (E.castString (course E.^. CourseDescription) `E.ilike` (E.%) E.++. E.val needle E.++. (E.%)) + E.||. (E.maybe (E.val mempty) (E.castString . esqueletoMarkupOutput) (course E.^. CourseDescription) `E.ilike` (E.%) E.++. E.val needle E.++. (E.%)) ) ] , dbtFilterUI = \mPrev -> mconcat $ catMaybes diff --git a/src/Handler/Course/News/Form.hs b/src/Handler/Course/News/Form.hs index 5d5aeb599..33e9a1938 100644 --- a/src/Handler/Course/News/Form.hs +++ b/src/Handler/Course/News/Form.hs @@ -12,8 +12,8 @@ import qualified Data.Conduit.List as C data CourseNewsForm = CourseNewsForm { cnfTitle :: Maybe Text - , cnfSummary :: Maybe Html - , cnfContent :: Html + , cnfSummary :: Maybe StoredMarkup + , cnfContent :: StoredMarkup , cnfParticipantsOnly :: Bool , cnfVisibleFrom :: Maybe UTCTime , cnfFiles :: Maybe FileUploads diff --git a/src/Handler/Course/Show.hs b/src/Handler/Course/Show.hs index f34d5048f..022a35559 100644 --- a/src/Handler/Course/Show.hs +++ b/src/Handler/Course/Show.hs @@ -8,14 +8,16 @@ import Import import Utils.Course import Utils.Form import Handler.Utils -import qualified Database.Esqueleto.Utils as E -import Database.Esqueleto.Utils.TH +import Handler.Utils.Course +import Handler.Utils.Tutorial import qualified Data.CaseInsensitive as CI import qualified Data.Map as Map import qualified Database.Esqueleto as E +import qualified Database.Esqueleto.Utils as E +import Database.Esqueleto.Utils.TH import Handler.Course.Register @@ -28,7 +30,7 @@ getCShowR :: TermId -> SchoolId -> CourseShorthand -> Handler Html getCShowR tid ssh csh = do mbAid <- maybeAuthId now <- liftIO getCurrentTime - (cid,course,courseVisible,schoolName,participants,registration,lecturers,assistants,correctors,tutors,mAllocation,hasApplicationTemplate,mApplication,news,events,submissionGroup,hasAllocationRegistrationOpen,mayReRegister) <- runDB . maybeT notFound $ do + (cid,course,courseVisible,schoolName,participants,registration,lecturers,assistants,correctors,tutors,mAllocation,hasApplicationTemplate,mApplication,news,events,submissionGroup,hasAllocationRegistrationOpen,mayReRegister,(mayViewSheets, mayViewAnySheet), (mayViewMaterials, mayViewAnyMaterial)) <- runDB . maybeT notFound $ do [(E.Entity cid course, E.Value courseVisible, E.Value schoolName, E.Value participants, fmap entityVal -> registration, E.Value hasAllocationRegistrationOpen)] <- lift . E.select . E.from $ \((school `E.InnerJoin` course) `E.LeftOuterJoin` participant) -> do @@ -90,10 +92,13 @@ getCShowR tid ssh csh = do lastEditText <- formatTime SelFormatDateTime $ maybe id max (guardOn visible =<< courseNewsVisibleFrom) courseNewsLastEdit mayEditNews <- hasWriteAccessTo $ CNewsR tid ssh csh cID CNEditR mayDelete <- hasWriteAccessTo $ CNewsR tid ssh csh cID CNDeleteR + return (cID, n, visible, files, lastEditText, mayEditNews, mayDelete) - events' <- fmap (sortOn $ courseEventTime . entityVal) . lift $ selectList [ CourseEventCourse ==. cid ] [] - events <- mapM (\(Entity evId ev) -> (, ev) <$> encrypt evId) events' + events' <- fmap (sortOn $ courseEventTime . entityVal . view _1) . lift . E.select . E.from $ \courseEvent -> do + E.where_ $ courseEvent E.^. CourseEventCourse E.==. E.val cid + return (courseEvent, maybe E.false (flip showCourseEventRoom courseEvent . E.val) mbAid) + events <- mapM (\(Entity evId ev, E.Value showRoom) -> (, ev, showRoom) <$> encrypt evId) events' hasSubmissionGroups <- lift . E.selectExists . E.from $ \(submissionGroupUser `E.InnerJoin` submissionGroup) -> do E.on $ submissionGroupUser E.^. SubmissionGroupUserSubmissionGroup E.==. submissionGroup E.^. SubmissionGroupId @@ -108,7 +113,19 @@ getCShowR tid ssh csh = do mayReRegister <- lift . courseMayReRegister $ Entity cid course - return (cid,course,courseVisible,schoolName,participants,registration,lecturers,assistants,correctors,tutors,mAllocation,hasApplicationTemplate,mApplication,news,events,submissionGroup,hasAllocationRegistrationOpen,mayReRegister) + mayViewSheets <- hasReadAccessTo $ CourseR tid ssh csh SheetListR + sheets <- lift . E.select . E.from $ \sheet -> do + E.where_ $ sheet E.^. SheetCourse E.==. E.val cid + return $ sheet E.^. SheetName + mayViewAnySheet <- anyM sheets $ \(E.Value shn) -> hasReadAccessTo $ CSheetR tid ssh csh shn SShowR + + mayViewMaterials <- hasReadAccessTo $ CourseR tid ssh csh MaterialListR + materials <- lift . E.select . E.from $ \material -> do + E.where_ $ material E.^. MaterialCourse E.==. E.val cid + return $ material E.^. MaterialName + mayViewAnyMaterial <- anyM materials $ \(E.Value mnm) -> hasReadAccessTo $ CMaterialR tid ssh csh mnm MShowR + + return (cid,course,courseVisible,schoolName,participants,registration,lecturers,assistants,correctors,tutors,mAllocation,hasApplicationTemplate,mApplication,news,events,submissionGroup,hasAllocationRegistrationOpen,mayReRegister, (mayViewSheets, mayViewAnySheet), (mayViewMaterials, mayViewAnyMaterial)) let mDereg' = maybe id min (allocationOverrideDeregister =<< mAllocation) <$> courseDeregisterUntil course mDereg <- traverse (formatTime SelFormatDateTime) mDereg' @@ -134,15 +151,19 @@ getCShowR tid ssh csh = do let tutorialDBTable = DBTable{..} where + resultTutorial :: Lens' (DBRow (Entity Tutorial, Bool)) (Entity Tutorial) + resultTutorial = _dbrOutput . _1 + resultShowRoom = _dbrOutput . _2 + dbtSQLQuery tutorial = do E.where_ $ tutorial E.^. TutorialCourse E.==. E.val cid - return tutorial + return (tutorial, maybe E.false (flip showTutorialRoom tutorial . E.val) mbAid) dbtRowKey = (E.^. TutorialId) - dbtProj = return + dbtProj = traverse $ return . over _2 E.unValue dbtColonnade = dbColonnade $ mconcat - [ sortable (Just "type") (i18nCell MsgTutorialType) $ \DBRow{ dbrOutput = Entity _ Tutorial{..} } -> textCell $ CI.original tutorialType - , sortable (Just "name") (i18nCell MsgTutorialName) $ \DBRow{ dbrOutput = Entity _ Tutorial{..} } -> indicatorCell <> anchorCell (CTutorialR tid ssh csh tutorialName TUsersR) [whamlet|#{tutorialName}|] - , sortable (Just "tutors") (i18nCell MsgTutorialTutors) $ \DBRow{ dbrOutput = Entity tutid _ } -> sqlCell $ do + [ sortable (Just "type") (i18nCell MsgTutorialType) $ \(view $ resultTutorial . _entityVal -> Tutorial{..}) -> textCell $ CI.original tutorialType + , sortable (Just "name") (i18nCell MsgTutorialName) $ \(view $ resultTutorial . _entityVal -> Tutorial{..}) -> indicatorCell <> anchorCell (CTutorialR tid ssh csh tutorialName TUsersR) [whamlet|#{tutorialName}|] + , sortable (Just "tutors") (i18nCell MsgTutorialTutors) $ \(view $ resultTutorial . _entityKey -> tutid) -> sqlCell $ do tutTutors <- fmap (map $(unValueN 3)) . E.select . E.from $ \(tutor `E.InnerJoin` user) -> do E.on $ tutor E.^. TutorUser E.==. user E.^. UserId E.where_ $ tutor E.^. TutorTutorial E.==. E.val tutid @@ -154,12 +175,14 @@ getCShowR tid ssh csh = do
  • ^{nameEmailWidget' tutor} |] - , sortable (Just "room") (i18nCell MsgTutorialRoom) $ \DBRow{ dbrOutput = Entity _ Tutorial{..} } -> maybe mempty textCell tutorialRoom - , sortable Nothing (i18nCell MsgTutorialTime) $ \DBRow{ dbrOutput = Entity _ Tutorial{..} } -> occurrencesCell tutorialTime - , sortable (Just "register-from") (i18nCell MsgTutorialRegisterFrom) $ \DBRow{ dbrOutput = Entity _ Tutorial{..} } -> maybeDateTimeCell tutorialRegisterFrom - , sortable (Just "register-to") (i18nCell MsgTutorialRegisterTo) $ \DBRow{ dbrOutput = Entity _ Tutorial{..} } -> maybeDateTimeCell tutorialRegisterTo - , sortable (Just "deregister-until") (i18nCell MsgTutorialDeregisterUntil) $ \DBRow{ dbrOutput = Entity _ Tutorial{..} } -> maybeDateTimeCell tutorialDeregisterUntil - , sortable Nothing (i18nCell MsgTutorialFreeCapacity) $ \DBRow{ dbrOutput = Entity tutid Tutorial{..} } -> case tutorialCapacity of + , sortable (Just "room") (i18nCell MsgTutorialRoom) $ \res -> if + | res ^. resultShowRoom -> maybe (i18nCell MsgTutorialRoomIsUnset) roomReferenceCell $ views (resultTutorial . _entityVal) tutorialRoom res + | otherwise -> i18nCell MsgTutorialRoomIsHidden & addCellClass ("explanation" :: Text) + , sortable Nothing (i18nCell MsgTutorialTime) $ \(view $ resultTutorial . _entityVal -> Tutorial{..}) -> occurrencesCell tutorialTime + , sortable (Just "register-from") (i18nCell MsgTutorialRegisterFrom) $ \(view $ resultTutorial . _entityVal -> Tutorial{..}) -> maybeDateTimeCell tutorialRegisterFrom + , sortable (Just "register-to") (i18nCell MsgTutorialRegisterTo) $ \(view $ resultTutorial . _entityVal -> Tutorial{..}) -> maybeDateTimeCell tutorialRegisterTo + , sortable (Just "deregister-until") (i18nCell MsgTutorialDeregisterUntil) $ \(view $ resultTutorial . _entityVal -> Tutorial{..}) -> maybeDateTimeCell tutorialDeregisterUntil + , sortable Nothing (i18nCell MsgTutorialFreeCapacity) $ \(view resultTutorial -> Entity tutid Tutorial{..}) -> case tutorialCapacity of Nothing -> mempty Just tutorialCapacity' -> sqlCell $ do freeCapacity <- fmap (maybe 0 (max 0 . E.unValue) . listToMaybe) @@ -168,7 +191,7 @@ getCShowR tid ssh csh = do E.where_ $ participant E.^. TutorialParticipantTutorial E.==. E.val tutid in return $ E.val tutorialCapacity' E.-. numParticipants return . toWidget $ tshow freeCapacity - , sortable Nothing (mempty & cellAttrs <>~ pure ("uw-hide-columns--hider-label", mr MsgActionsHead)) $ \DBRow{ dbrOutput = Entity tutId Tutorial{..} } -> sqlCell $ do + , sortable Nothing (mempty & cellAttrs <>~ pure ("uw-hide-columns--hider-label", mr MsgActionsHead)) $ \(view resultTutorial -> Entity tutId Tutorial{..}) -> sqlCell $ do mayRegister <- (== Authorized) <$> evalAccessDB (CTutorialR tid ssh csh tutorialName TRegisterR) True isRegistered <- case mbAid of Nothing -> return False @@ -219,7 +242,7 @@ getCShowR tid ssh csh = do , length fs <= 3 , all (notElem pathSeparator . view _2) fs ] - hiddenEventNotes = all (\(_,CourseEvent{..}) -> is _Nothing courseEventNote) events + hiddenEventNotes = all (\(_,CourseEvent{..},_) -> is _Nothing courseEventNote) events Course{courseVisibleFrom,courseVisibleTo} = course mayCreateNews <- hasWriteAccessTo $ CourseR tid ssh csh CNewsNewR mayCreateEvents <- hasWriteAccessTo $ CourseR tid ssh csh CEventsNewR diff --git a/src/Handler/Course/User.hs b/src/Handler/Course/User.hs index de8747fc4..c9a5e2217 100644 --- a/src/Handler/Course/User.hs +++ b/src/Handler/Course/User.hs @@ -29,6 +29,7 @@ import qualified Data.Text as Text import qualified Data.CaseInsensitive as CI import qualified Data.Conduit.Combinators as C +import qualified Data.Text.Lazy as LT data ExamAction = ExamDeregister @@ -226,7 +227,7 @@ courseUserNoteSection (Entity cid Course{..}) (Entity uid _) = do maybeM (return ()) (\nk -> deleteWhere [CourseUserNoteEditNote ==. nk]) (getKeyBy thisUniqueNote) deleteBy thisUniqueNote addMessageI Info MsgCourseUserNoteDeleted - _ | (renderHtml <$> mbNote) == (renderHtml <$> noteText) -> return () -- no changes + _ | ((==) `on` fmap (LT.strip . renderHtml . markupOutput)) mbNote noteText -> return () -- no changes (Just note) -> do dozentId <- requireAuthId (Entity noteKey _) <- upsertBy thisUniqueNote (CourseUserNote cid uid note) [CourseUserNoteNote =. note] @@ -459,7 +460,7 @@ courseUserTutorialsSection (Entity cid Course{..}) (Entity uid _) = do
  • ^{nameEmailWidget userEmail userDisplayName userSurname} |] - , sortable (Just "room") (i18nCell MsgTutorialRoom) $ maybe mempty textCell . view (_dbrOutput . _1 . _entityVal . _tutorialRoom) + , sortable (Just "room") (i18nCell MsgTutorialRoom) $ maybe (i18nCell MsgTutorialRoomIsUnset) roomReferenceCell . view (_dbrOutput . _1 . _entityVal . _tutorialRoom) , sortable Nothing (i18nCell MsgTutorialTime) $ occurrencesCell . view (_dbrOutput . _1 . _entityVal . _tutorialTime) ] dbtSorting = mconcat diff --git a/src/Handler/Course/Users.hs b/src/Handler/Course/Users.hs index f6c31ef4e..9775952b4 100644 --- a/src/Handler/Course/Users.hs +++ b/src/Handler/Course/Users.hs @@ -174,7 +174,7 @@ data UserTableCsv = UserTableCsv , csvUserStudyFeatures :: UserTableStudyFeatures , csvUserSubmissionGroup :: Maybe SubmissionGroupName , csvUserRegistration :: UTCTime - , csvUserNote :: Maybe Html + , csvUserNote :: Maybe StoredMarkup , csvUserTutorials :: ([TutorialName], Map (CI Text) (Maybe TutorialName)) , csvUserExams :: [ExamName] , csvUserSheets :: Map SheetName (SheetType, Maybe Points) diff --git a/src/Handler/Exam/Edit.hs b/src/Handler/Exam/Edit.hs index 7cc3ef518..7f46d7a58 100644 --- a/src/Handler/Exam/Edit.hs +++ b/src/Handler/Exam/Edit.hs @@ -20,10 +20,11 @@ getEEditR = postEEditR postEEditR tid ssh csh examn = do (template, (editExamAct, (editExamWidget, editExamEnctype))) <- runDBJobs $ do (cid, exam@(Entity eId oldExam)) <- fetchCourseIdExam tid ssh csh examn + course <- getEntity404 cid template <- examFormTemplate exam - ((editExamResult, editExamWidget), editExamEnctype) <- runFormPost . validateForm (validateExam cid $ Just exam) . examForm $ Just template + ((editExamResult, editExamWidget), editExamEnctype) <- runFormPost . validateForm (validateExam cid $ Just exam) . examForm course $ Just template editExamAct <- formResultMaybe editExamResult $ \ExamForm{..} -> do insertRes <- myReplaceUnique eId Exam @@ -58,6 +59,7 @@ postEEditR tid ssh csh examn = do { examOccurrenceExam = eId , examOccurrenceName = eofName , examOccurrenceRoom = eofRoom + , examOccurrenceRoomHidden = eofRoomHidden , examOccurrenceCapacity = eofCapacity , examOccurrenceStart = eofStart , examOccurrenceEnd = eofEnd @@ -72,6 +74,7 @@ postEEditR tid ssh csh examn = do { examOccurrenceExam = eId , examOccurrenceName = eofName , examOccurrenceRoom = eofRoom + , examOccurrenceRoomHidden = eofRoomHidden , examOccurrenceCapacity = eofCapacity , examOccurrenceStart = eofStart , examOccurrenceEnd = eofEnd diff --git a/src/Handler/Exam/Form.hs b/src/Handler/Exam/Form.hs index 2a19dfa4f..b179a9a43 100644 --- a/src/Handler/Exam/Form.hs +++ b/src/Handler/Exam/Form.hs @@ -22,12 +22,14 @@ import qualified Database.Esqueleto as E import qualified Database.Esqueleto.Utils as E import qualified Control.Monad.State.Class as State -import Text.Blaze.Html.Renderer.String (renderHtml) +import Text.Blaze.Html.Renderer.Text (renderHtml) + +import qualified Data.Text.Lazy as LT data ExamForm = ExamForm { efName :: ExamName - , efDescription :: Maybe Html + , efDescription :: Maybe StoredMarkup , efStart :: Maybe UTCTime , efEnd :: Maybe UTCTime , efVisibleFrom :: Maybe UTCTime @@ -52,12 +54,13 @@ data ExamForm = ExamForm data ExamOccurrenceForm = ExamOccurrenceForm { eofId :: Maybe CryptoUUIDExamOccurrence , eofName :: ExamOccurrenceName - , eofRoom :: Text + , eofRoom :: Maybe RoomReference + , eofRoomHidden :: Bool , eofCapacity :: Natural , eofStart :: UTCTime , eofEnd :: Maybe UTCTime - , eofDescription :: Maybe Html - } deriving (Read, Show, Eq, Generic, Typeable) + , eofDescription :: Maybe StoredMarkup + } deriving (Show, Eq, Generic, Typeable) instance Ord ExamOccurrenceForm where compare = mconcat @@ -67,6 +70,7 @@ instance Ord ExamOccurrenceForm where , comparing eofEnd , comparing eofCapacity , comparing eofDescription + , comparing eofRoomHidden , comparing eofId ] @@ -101,9 +105,10 @@ deriveJSON defaultOptions examForm :: ( MonadHandler m , HandlerSite m ~ UniWorX ) - => Maybe ExamForm -> (Html -> MForm m (FormResult ExamForm, Widget)) -examForm template csrf = hoist liftHandler $ do + => Entity Course -> Maybe ExamForm -> (Html -> MForm m (FormResult ExamForm, Widget)) +examForm (Entity _ Course{..}) template csrf = hoist liftHandler $ do MsgRenderer mr <- getMsgRenderer + School{..} <- liftHandler . runDBRead $ getJust courseSchool flip (renderAForm FormStandard) csrf $ ExamForm <$> areq ciField (fslpI MsgExamName (mr MsgExamName) & setTooltip MsgExamNameTip) (efName <$> template) @@ -116,7 +121,7 @@ examForm template csrf = hoist liftHandler $ do <*> aopt utcTimeField (fslpI MsgExamRegisterTo (mr MsgDate)) (efRegisterTo <$> template) <*> aopt utcTimeField (fslpI MsgExamDeregisterUntil (mr MsgDate)) (efDeregisterUntil <$> template) <*> aopt utcTimeField (fslpI MsgExamPublishOccurrenceAssignments (mr MsgDate) & setTooltip MsgExamPublishOccurrenceAssignmentsTip) (efPublishOccurrenceAssignments <$> template) - <*> aopt utcTimeField (fslpI MsgExamFinished (mr MsgDate) & setTooltip MsgExamFinishedTip) (efFinished <$> template) + <*> aopt utcTimeField (fslpI MsgExamFinished (mr MsgDate) & setTooltip (bool MsgExamFinishedTip MsgExamFinishedTipCloseOnFinished $ is _ExamCloseOnFinished' schoolExamCloseMode)) (efFinished <$> template) <* aformSection MsgExamFormOccurrences <*> examOccurrenceForm (efOccurrences <$> template) <* aformSection MsgExamFormAutomaticFunctions @@ -218,20 +223,25 @@ examOccurrenceForm prev = wFormToAForm $ do examOccurrenceForm' nudge mPrev csrf = do (eofIdRes, eofIdView) <- mopt hiddenField ("" & addName (nudge "id")) (Just $ eofId =<< mPrev) (eofNameRes, eofNameView) <- mpreq (textField & cfStrip & cfCI) (fslI MsgExamRoomName & addName (nudge "name")) (eofName <$> mPrev) - (eofRoomRes, eofRoomView) <- mpreq (textField & cfStrip) (fslI MsgExamRoomRoom & addName (nudge "room")) (eofRoom <$> mPrev) + (eofRoomRes', eofRoomView) <- ($ mempty) . renderAForm FormVertical $ (,) + <$> roomReferenceFormOpt (fslI MsgExamRoomRoom & addName (nudge "room")) (eofRoom <$> mPrev) + <*> apopt checkBoxField (fslI MsgExamRoomRoomHidden & setTooltip MsgExamRoomRoomHiddenTip & addName (nudge "room-hidden")) (eofRoomHidden <$> mPrev) + let eofRoomRes = view _1 <$> eofRoomRes' + eofRoomHiddenRes = view _2 <$> eofRoomRes' (eofCapacityRes, eofCapacityView) <- mpreq (natFieldI MsgExamRoomCapacityNegative) (fslI MsgExamRoomCapacity & addName (nudge "capacity")) (eofCapacity <$> mPrev) (eofStartRes, eofStartView) <- mpreq utcTimeField (fslI MsgExamRoomStart & addName (nudge "start")) (eofStart <$> mPrev) (eofEndRes, eofEndView) <- mopt utcTimeField (fslI MsgExamRoomEnd & addName (nudge "end")) (eofEnd <$> mPrev) - (eofDescRes, eofDescView) <- mopt htmlFieldSmall (fslI MsgExamRoomDescription & addName (nudge "description")) (eofDescription <$> mPrev) + (eofDescRes, eofDescView) <- mopt htmlField (fslI MsgExamRoomDescription & addName (nudge "description")) (eofDescription <$> mPrev) return ( ExamOccurrenceForm <$> eofIdRes <*> eofNameRes <*> eofRoomRes + <*> eofRoomHiddenRes <*> eofCapacityRes <*> eofStartRes <*> eofEndRes - <*> (assertM (not . null . renderHtml) <$> eofDescRes) + <*> eofDescRes , $(widgetFile "widgets/massinput/examRooms/form") ) @@ -324,6 +334,7 @@ examFormTemplate (Entity eId Exam{..}) = do { eofId , eofName = examOccurrenceName , eofRoom = examOccurrenceRoom + , eofRoomHidden = examOccurrenceRoomHidden , eofCapacity = examOccurrenceCapacity , eofStart = examOccurrenceStart , eofEnd = examOccurrenceEnd @@ -426,11 +437,12 @@ validateExam cId oldExam = do forM_ [ (a, b) | a <- Set.toAscList efOccurrences, b <- Set.toAscList efOccurrences, b > a ] $ \(a, b) -> do eofRange' <- formatTimeRange SelFormatDateTime (eofStart a) (eofEnd a) - guardValidation (MsgExamOccurrenceDuplicate (eofRoom a) eofRange') $ any (\f -> f a b) + MsgRenderer mr <- getMsgRenderer + guardValidation (MsgExamOccurrenceDuplicate (maybe (mr MsgExamOccurrenceRoomIsUnset) roomReferenceText $ eofRoom a) eofRange') $ any (\f -> f a b) [ (/=) `on` eofRoom , (/=) `on` eofStart , (/=) `on` eofEnd - , (/=) `on` fmap renderHtml . eofDescription + , (/=) `on` fmap (LT.strip . renderHtml . markupOutput) . eofDescription ] guardValidation (MsgExamOccurrenceDuplicateName $ eofName a) $ ((/=) `on` eofName) a b diff --git a/src/Handler/Exam/New.hs b/src/Handler/Exam/New.hs index 43b7b287e..5961f0187 100644 --- a/src/Handler/Exam/New.hs +++ b/src/Handler/Exam/New.hs @@ -21,9 +21,10 @@ getCExamNewR = postCExamNewR postCExamNewR tid ssh csh = do (newExamAct, (newExamWidget, newExamEnctype)) <- runDBJobs $ do cid <- getKeyBy404 $ TermSchoolCourseShort tid ssh csh + course <- getEntity404 cid template <- examTemplate cid - ((newExamResult, newExamWidget), newExamEnctype) <- runFormPost . validateForm (validateExam cid Nothing) $ examForm template + ((newExamResult, newExamWidget), newExamEnctype) <- runFormPost . validateForm (validateExam cid Nothing) $ examForm course template newExamAct <- formResultMaybe newExamResult $ \ExamForm{..} -> do now <- liftIO getCurrentTime @@ -67,6 +68,7 @@ postCExamNewR tid ssh csh = do , let examOccurrenceExam = examid examOccurrenceName = eofName examOccurrenceRoom = eofRoom + examOccurrenceRoomHidden = eofRoomHidden examOccurrenceCapacity = eofCapacity examOccurrenceStart = eofStart examOccurrenceEnd = eofEnd diff --git a/src/Handler/Exam/Show.hs b/src/Handler/Exam/Show.hs index 00584ff83..0540e10ce 100644 --- a/src/Handler/Exam/Show.hs +++ b/src/Handler/Exam/Show.hs @@ -66,20 +66,20 @@ getEShowR tid ssh csh examn = do E.where_ $ examRegistration E.^. ExamRegistrationExam E.==. E.val eId E.&&. examRegistration E.^. ExamRegistrationOccurrence E.==. E.just (examOccurrence E.^. ExamOccurrenceId) E.orderBy [E.desc registered, E.asc $ examOccurrence E.^. ExamOccurrenceStart, E.asc $ examOccurrence E.^. ExamOccurrenceRoom] - return (examOccurrence, registered, registeredCount) + return (examOccurrence, registered, registeredCount, maybe E.false (flip showExamOccurrenceRoom examOccurrence . E.val) mUid) registeredCount <- fromIntegral <$> count [ ExamRegistrationExam ==. eId ] registered <- for mUid $ getBy . UniqueExamRegistration eId mayRegister <- if - | examOccurrenceRule == ExamRoomFifo -> anyM occurrencesRaw $ \(Entity _ ExamOccurrence{..}, _, _) -> + | examOccurrenceRule == ExamRoomFifo -> anyM occurrencesRaw $ \(Entity _ ExamOccurrence{..}, _, _, _) -> hasWriteAccessTo . CExamR tid ssh csh examName $ ERegisterOccR examOccurrenceName | otherwise -> hasWriteAccessTo $ CExamR tid ssh csh examName ERegisterR - let occurrences = sortOn sortPred $ map (over _3 E.unValue . over _2 E.unValue) occurrencesRaw + let occurrences = sortOn sortPred $ map (over _4 E.unValue . over _3 E.unValue . over _2 E.unValue) occurrencesRaw where - sortPred (Entity _ ExamOccurrence{..}, registered', _) - = (Down $ registered' && not mayRegister, examOccurrenceStart, examOccurrenceRoom) + sortPred (Entity _ ExamOccurrence{..}, registered', _, showRoom) + = (Down $ registered' && not mayRegister, examOccurrenceStart, guardOn @Maybe showRoom examOccurrenceRoom) lecturerInfoShown <- hasReadAccessTo $ CExamR tid ssh csh examn EEditR @@ -94,7 +94,7 @@ getEShowR tid ssh csh examn = do let occurrenceNamesShown = lecturerInfoShown partNumbersShown = lecturerInfoShown - examClosedShown = lecturerInfoShown + examClosedShown = lecturerInfoShown && isn't _ExamCloseOnFinished' schoolExamCloseMode showCloseWidget = lecturerInfoShown showAutoOccurrenceCalculateWidget = lecturerInfoShown showRegisteredCount = lecturerInfoShown @@ -118,14 +118,15 @@ getEShowR tid ssh csh examn = do mayRegister' <- fmap ((Map.!) . Map.fromList) . for (Nothing : map Just occurrences) $ \case Nothing -> fmap (Nothing, ) . hasWriteAccessTo $ CExamR tid ssh csh examName ERegisterR - Just (Entity occId ExamOccurrence{..}, _, _) -> + Just (Entity occId ExamOccurrence{..}, _, _, _) -> fmap (Just occId, ) . hasWriteAccessTo . CExamR tid ssh csh examName $ ERegisterOccR examOccurrenceName - let examTimes = all (\(Entity _ ExamOccurrence{..}, _, _) -> Just examOccurrenceStart == examStart && examOccurrenceEnd == examEnd) occurrences + let examTimes = all (\(Entity _ ExamOccurrence{..}, _, _, _) -> Just examOccurrenceStart == examStart && examOccurrenceEnd == examEnd) occurrences examRoom = do - Entity _ primeOcc <- occurrences ^? _head . _1 - guard $ all (\(Entity _ occ, _, _) -> examOccurrenceRoom occ == examOccurrenceRoom primeOcc) occurrences - return $ examOccurrenceRoom primeOcc + (Entity _ primeOcc, _, _, _) <- occurrences ^? _head + guard $ all (\(Entity _ occ, _, _, _) -> examOccurrenceRoom occ == examOccurrenceRoom primeOcc) occurrences + guard $ andOf (folded . _4) occurrences + examOccurrenceRoom primeOcc registerWidget mOcc | isRegistered <- is _Just $ join registered , examOccurrenceRule /= ExamRoomFifo || (isRegistered && not (orOf (folded . _2) occurrences)) diff --git a/src/Handler/Exam/Users.hs b/src/Handler/Exam/Users.hs index 6e76ed20b..eb3f4aba1 100644 --- a/src/Handler/Exam/Users.hs +++ b/src/Handler/Exam/Users.hs @@ -181,7 +181,7 @@ data ExamUserTableCsv = ExamUserTableCsv , csvEUserBonus :: Maybe (Maybe Points) , csvEUserExamPartResults :: Map ExamPartNumber (Maybe ExamResultPoints) , csvEUserExamResult :: Maybe ExamResultPassedGrade - , csvEUserCourseNote :: Maybe Html + , csvEUserCourseNote :: Maybe StoredMarkup } deriving (Generic) makeLenses_ ''ExamUserTableCsv @@ -345,7 +345,7 @@ data ExamUserCsvAction } | ExamUserCsvSetCourseNoteData { examUserCsvActUser :: UserId - , examUserCsvActCourseNote :: Maybe Html + , examUserCsvActCourseNote :: Maybe StoredMarkup } deriving (Eq, Ord, Read, Show, Generic, Typeable) deriveJSON defaultOptions diff --git a/src/Handler/ExamOffice/Exam.hs b/src/Handler/ExamOffice/Exam.hs index 4364079c7..c41684727 100644 --- a/src/Handler/ExamOffice/Exam.hs +++ b/src/Handler/ExamOffice/Exam.hs @@ -39,30 +39,44 @@ instance Button UniWorX ButtonCloseExam where examCloseWidget :: SomeRoute UniWorX -> ExamId -> Handler Widget examCloseWidget dest eId = do - Exam{..} <- runDB $ get404 eId + (Exam{..}, School{..}) <- runDB $ do + exam@Exam{..} <- get404 eId + Course{..} <- get404 examCourse + school <- get404 courseSchool + return (exam, school) - ((closeRes, closeView), closeEnc) <- runFormPost $ identifyForm BtnCloseExam buttonForm + let closeTime = case (examClosed, examFinished) of + (mClose, Just finish) + | isn't _ExamCloseSeparate schoolExamCloseMode -> Just $ maybe id min mClose finish + (Just close, _) + | is _ExamCloseSeparate schoolExamCloseMode -> Just close + _other -> Nothing - formResult closeRes $ \case - BtnCloseExam -> do - now <- liftIO getCurrentTime + examClosedStr <- for closeTime $ formatTime SelFormatDateTime - unless (is _Nothing examClosed) $ - invalidArgs ["Exam is already closed"] + if | is _ExamCloseOnFinished' schoolExamCloseMode + -> return $(widgetFile "widgets/exam-close-on-finished") + | otherwise -> do + ((closeRes, closeView'), closeEnc) <- runFormPost $ identifyForm BtnCloseExam buttonForm - runDB $ update eId [ ExamClosed =. Just now ] - addMessageI Success MsgExamDidClose - redirect dest + formResult closeRes $ \case + BtnCloseExam -> do + now <- liftIO getCurrentTime - let closeView' = wrapForm closeView def - { formSubmit = FormNoSubmit - , formAction = Just dest - , formEncoding = closeEnc - } + unless (is _Nothing examClosed) $ + invalidArgs ["Exam is already closed"] - examClosed' <- for examClosed $ formatTime SelFormatDateTime + runDB $ update eId [ ExamClosed =. Just now ] + addMessageI Success MsgExamDidClose + redirect dest - return $(widgetFile "widgets/exam-close") + let closeView = wrapForm closeView' def + { formSubmit = FormNoSubmit + , formAction = Just dest + , formEncoding = closeEnc + } + + return $(widgetFile "widgets/exam-close") type ExamUserTableExpr = ( E.SqlExpr (Entity ExamResult) diff --git a/src/Handler/ExamOffice/Exams.hs b/src/Handler/ExamOffice/Exams.hs index c2f4f1c75..7aa94a7f8 100644 --- a/src/Handler/ExamOffice/Exams.hs +++ b/src/Handler/ExamOffice/Exams.hs @@ -18,18 +18,22 @@ import qualified Colonnade type ExamsTableExpr = ( E.SqlExpr (Maybe (Entity Exam)) `E.InnerJoin` E.SqlExpr (Maybe (Entity Course)) + `E.InnerJoin` E.SqlExpr (Maybe (Entity School)) ) `E.FullOuterJoin` E.SqlExpr (Maybe (Entity ExternalExam)) -type ExamsTableData = DBRow ( Either (Entity ExternalExam) (Entity Exam, Entity Course) +type ExamsTableData = DBRow ( Either (Entity ExternalExam) (Entity Exam, Entity Course, Entity School) , Natural, Natural ) queryExam :: Getter ExamsTableExpr (E.SqlExpr (Maybe (Entity Exam))) -queryExam = to $ $(E.sqlIJproj 2 1) . $(E.sqlFOJproj 2 1) +queryExam = to $ $(E.sqlIJproj 3 1) . $(E.sqlFOJproj 2 1) queryCourse :: Getter ExamsTableExpr (E.SqlExpr (Maybe (Entity Course))) -queryCourse = to $ $(E.sqlIJproj 2 2) . $(E.sqlFOJproj 2 1) +queryCourse = to $ $(E.sqlIJproj 3 2) . $(E.sqlFOJproj 2 1) + +querySchool :: Getter ExamsTableExpr (E.SqlExpr (Maybe (Entity School))) +querySchool = to $ $(E.sqlIJproj 3 3) . $(E.sqlFOJproj 2 1) queryExternalExam :: Getter ExamsTableExpr (E.SqlExpr (Maybe (Entity ExternalExam))) queryExternalExam = to $(E.sqlFOJproj 2 2) @@ -66,6 +70,7 @@ queryIsSynced :: UTCTime -> E.SqlExpr (E.Value UserId) -> Getter ExamsTableExpr queryIsSynced now office = to . runReader $ do exam' <- view queryExam externalExam' <- view queryExternalExam + school' <- view querySchool let examSynchronised examId = E.not_ . E.exists . E.from $ \examResult -> do E.where_ $ examResult E.^. ExamResultExam E.==. examId @@ -75,8 +80,11 @@ queryIsSynced now office = to . runReader $ do E.where_ $ externalExamResult E.^. ExternalExamResultExam E.==. externalExamId E.where_ $ ExternalExam.examOfficeExternalExamResultAuth office externalExamResult E.where_ . E.not_ $ ExternalExam.resultIsSynced office externalExamResult - open examClosed' = E.maybe E.true (E.>. E.val now) examClosed' - return $ E.maybe E.false examSynchronised (exam' E.?. ExamId) E.||. E.maybe E.false open (exam' E.?. ExamClosed) E.||. E.maybe E.false externalExamSynchronised (externalExam' E.?. ExternalExamId) + open examClosed' examFinished' + = E.bool (E.maybe E.true (E.>. E.val now) $ E.min examClosed' examFinished') + (E.maybe E.true (E.>. E.val now) examClosed') + (E.maybe E.false (E.==. E.val ExamCloseSeparate) (school' E.?. SchoolExamCloseMode)) + return $ E.maybe E.false examSynchronised (exam' E.?. ExamId) E.||. E.maybe2 E.false open (exam' E.?. ExamClosed) (exam' E.?. ExamFinished) E.||. E.maybe E.false externalExamSynchronised (externalExam' E.?. ExternalExamId) resultExam :: Traversal' ExamsTableData (Entity Exam) @@ -85,6 +93,9 @@ resultExam = _dbrOutput . _1 . _Right . _1 resultCourse :: Traversal' ExamsTableData (Entity Course) resultCourse = _dbrOutput . _1 . _Right . _2 +resultSchool :: Traversal' ExamsTableData (Entity School) +resultSchool = _dbrOutput . _1 . _Right . _3 + resultExternalExam :: Traversal' ExamsTableData (Entity ExternalExam) resultExternalExam = _dbrOutput . _1 . _Left @@ -126,6 +137,7 @@ getEOExamsR = do dbtSQLQuery = runReaderT $ do exam <- view queryExam course <- view queryCourse + school <- view querySchool externalExam <- view queryExternalExam synchronised <- view querySynchronised' @@ -133,35 +145,41 @@ getEOExamsR = do lift $ do E.on E.false + E.on $ school E.?. SchoolId E.==. course E.?. CourseSchool E.on $ exam E.?. ExamCourse E.==. course E.?. CourseId E.where_ $ results E.>. E.val 0 E.where_ $ (E.not_ (E.isNothing $ exam E.?. ExamId) E.&&. E.not_ (E.isNothing $ course E.?. CourseId) E.&&. E.isNothing (externalExam E.?. ExternalExamId)) E.||. ( E.isNothing (exam E.?. ExamId) E.&&. E.isNothing (course E.?. CourseId) E.&&. E.not_ (E.isNothing $ externalExam E.?. ExternalExamId)) - return (exam, course, externalExam, synchronised, results) + return (exam, course, school, externalExam, synchronised, results) dbtRowKey = views ($(multifocusG 2) queryExam queryExternalExam) (bimap (E.?. ExamId) (E.?. ExternalExamId)) dbtProj :: DBRow _ -> DB ExamsTableData dbtProj = runReaderT $ (asks . set _dbrOutput) <=< magnify _dbrOutput $ do exam <- view _1 course <- view _2 - externalExam <- view _3 + school <- view _3 + externalExam <- view _4 - case (exam, course, externalExam) of - (Just exam', Just course', Nothing) -> - (Right (exam', course'),,) <$> view (_4 . _Value) <*> view (_5 . _Value) - (Nothing, Nothing, Just externalExam') -> - (Left externalExam',,) <$> view (_4 . _Value) <*> view (_5 . _Value) + case (exam, course, school, externalExam) of + (Just exam', Just course', Just school', Nothing) -> + (Right (exam', course', school'),,) <$> view (_5 . _Value) <*> view (_6 . _Value) + (Nothing, Nothing, Nothing, Just externalExam') -> + (Left externalExam',,) <$> view (_5 . _Value) <*> view (_6 . _Value) _other -> return $ error "Got exam & externalExam in same result" colSynced = Colonnade.singleton (fromSortable . Sortable (Just "synced") $ i18nCell MsgExamSynchronised) $ \x -> flip runReader x $ do mExam <- preview resultExam + mSchool <- preview resultSchool if - | Just (Entity _ Exam{examClosed}) <- mExam - , NTop examClosed > NTop (Just now) + | Just (Entity _ Exam{examClosed, examFinished}) <- mExam + , Just (Entity _ School{schoolExamCloseMode}) <- mSchool + , bool ((min `on` NTop) examClosed examFinished > NTop (Just now)) + (NTop examClosed > NTop (Just now)) + $ is _ExamCloseSeparate schoolExamCloseMode -> return . cell $ toWidget iconNew | otherwise -> do diff --git a/src/Handler/ExternalExam/Form.hs b/src/Handler/ExternalExam/Form.hs index 9d54f3c04..02944853c 100644 --- a/src/Handler/ExternalExam/Form.hs +++ b/src/Handler/ExternalExam/Form.hs @@ -13,6 +13,8 @@ import Data.Map ((!)) import qualified Control.Monad.State.Class as State +import qualified Database.Esqueleto as E + data ExternalExamForm = ExternalExamForm { eefTerm :: TermId @@ -32,6 +34,7 @@ externalExamForm template = validateForm validateExternalExam $ \html -> do uid <- requireAuthId cRoute <- fromMaybe (error "tutorialForm called from 404-Handler") <$> getCurrentRoute MsgRenderer mr <- getMsgRenderer + allSchoolIds <- fmap (map E.unValue) . liftHandler . runDBRead . E.select . E.from $ return . (E.^. SchoolId) let termsField = case template of Just template' -> termsSetField [eefTerm template'] @@ -44,6 +47,9 @@ externalExamForm template = validateForm validateExternalExam $ \html -> do let oldSchool = eefSchool <$> template return (lecturerSchools, adminSchools, oldSchool) let userSchools = nub . maybe id (:) oldSchool $ lecturerSchools ++ adminSchools + templateSchool = eefSchool <$> template <|> case userSchools of + [ssh] -> pure ssh + _ -> mzero flip (renderAForm FormStandard) html $ ExternalExamForm <$> areq termsField (fslI MsgExternalExamSemester) (eefTerm <$> template) @@ -52,7 +58,7 @@ externalExamForm template = validateForm validateExternalExam $ \html -> do <*> areq (textField & cfStrip & cfCI) (fslI MsgExternalExamExamName & setTooltip MsgExternalExamExamNameTip & addPlaceholder (mr MsgExternalExamExamNamePlaceholder)) (eefExamName <$> template) <*> aopt utcTimeField (fslI MsgExternalExamDefaultTime & setTooltip MsgExternalExamDefaultTimeTip & addPlaceholder (mr MsgExternalExamDefaultTimePlaceholder)) (eefDefaultTime <$> template) <*> apopt (selectField optionsFinite) (fslI MsgExamGradingMode & setTooltip MsgExamGradingModeTip) (eefGradingMode <$> template <|> Just ExamGradingMixed) - <*> (Set.fromList <$> officeSchoolForm cRoute (Set.toList . eefOfficeSchools <$> template)) + <*> (Set.fromList <$> officeSchoolForm cRoute (Set.toList . eefOfficeSchools <$> template <|> Just (filter ((/= templateSchool) . Just) allSchoolIds))) <*> (Set.fromList <$> staffForm cRoute ((Set.toList . eefStaff <$> template) <|> pure (pure $ Right uid))) where officeSchoolForm cRoute = massInputAccumA miAdd miCell miButtonAction miLayout miIdent fSettings fRequired diff --git a/src/Handler/Help.hs b/src/Handler/Help.hs index 4c2bd4f0b..f6070369e 100644 --- a/src/Handler/Help.hs +++ b/src/Handler/Help.hs @@ -67,7 +67,7 @@ helpForm mReferer mUid = renderWForm FormStandard $ do <$> hfReferer' <*> hfUserId' <*> hfSubject' - <*> hfRequest' + <*> (fmap markupOutput <$> hfRequest') <*> hfError' validateHelpForm :: FormValidator HelpForm Handler () diff --git a/src/Handler/Material.hs b/src/Handler/Material.hs index c0679cd31..fa6a8db39 100644 --- a/src/Handler/Material.hs +++ b/src/Handler/Material.hs @@ -19,7 +19,7 @@ import Handler.Utils.Delete data MaterialForm = MaterialForm { mfName :: MaterialName , mfType :: Maybe (CI Text) - , mfDescription :: Maybe Html + , mfDescription :: Maybe StoredMarkup , mfVisibleFrom :: Maybe UTCTime , mfFiles :: Maybe FileUploads } @@ -170,12 +170,33 @@ getMFileR tid ssh csh mnm title = serveOneFile $ fileQuery .| C.map entityVal -- return file entity return matFile +getMVideoR :: TermId -> SchoolId -> CourseShorthand -> MaterialName -> CryptoUUIDMaterialFile -> Handler Html +getMVideoR tid ssh csh mnm cID = do + mfId <- decrypt cID + MaterialFile{..} <- runDB $ get404 mfId + let mimeType = mimeLookup $ pack materialFileTitle + mfile = CMaterialR tid ssh csh mnm $ MFileR materialFileTitle + unless (mimeType `Set.member` videoTypes) $ + redirectWith movedPermanently301 mfile + siteLayout' Nothing $ do + setTitleI . prependCourseTitle tid ssh csh $ MsgMaterialVideo mnm + [whamlet| + $newline never +
    +
    +