feat(course-registration): allow independent course application

This commit is contained in:
Gregor Kleen 2019-08-09 16:44:26 +02:00
parent e54d6e4acf
commit a00698e99e
20 changed files with 341 additions and 177 deletions

View File

@ -86,8 +86,9 @@ CourseCapacityTip: Anzahl erlaubter Kursanmeldungen, leer lassen für unbeschrä
CourseNoCapacity: In diesem Kurs sind keine Plätze mehr frei. CourseNoCapacity: In diesem Kurs sind keine Plätze mehr frei.
TutorialNoCapacity: In dieser Übung sind keine Plätze mehr frei. TutorialNoCapacity: In dieser Übung sind keine Plätze mehr frei.
CourseNotEmpty: In diesem Kurs sind momentan Teilnehmer angemeldet. CourseNotEmpty: In diesem Kurs sind momentan Teilnehmer angemeldet.
CourseRegisterOk: Anmeldung erfolgreich CourseRegistration: Kursanmeldung
CourseDeregisterOk: Erfolgreich abgemeldet CourseRegisterOk: Erfolgreich zum Kurs angemeldet
CourseDeregisterOk: Erfolgreich vom Kurs abgemeldet
CourseDeregisterLecturerTip: Wenn Sie den Teilnehmer vom Kurs abmelden kann es sein, dass sie Zugriff auf diese Daten verlieren CourseDeregisterLecturerTip: Wenn Sie den Teilnehmer vom Kurs abmelden kann es sein, dass sie Zugriff auf diese Daten verlieren
CourseStudyFeature: Assoziiertes Hauptfach CourseStudyFeature: Assoziiertes Hauptfach
CourseStudyFeatureUpdated: Assoziiertes Hauptfach geändert CourseStudyFeatureUpdated: Assoziiertes Hauptfach geändert
@ -112,7 +113,7 @@ CourseMembers: Teilnehmer
CourseMemberOf: Teilnehmer CourseMemberOf: Teilnehmer
CourseMembersCount n@Int: #{n} CourseMembersCount n@Int: #{n}
CourseMembersCountLimited n@Int max@Int: #{n}/#{max} CourseMembersCountLimited n@Int max@Int: #{n}/#{max}
CourseMembersCountOf n@Int mbNum@IntMaybe: #{n} Anmeldungen #{maybeToMessage " von " mbNum " möglichen"} CourseMembersCountOf n@Int mbNum@IntMaybe: #{n} Kursanmeldungen #{maybeToMessage " von " mbNum " möglichen"}
CourseName: Name CourseName: Name
CourseDescription: Beschreibung CourseDescription: Beschreibung
CourseDescriptionTip: Beliebiges HTML-Markup ist gestattet CourseDescriptionTip: Beliebiges HTML-Markup ist gestattet
@ -136,8 +137,8 @@ CourseUserNote: Notiz
CourseUserNoteTooltip: Nur für Dozenten dieses Kurses einsehbar CourseUserNoteTooltip: Nur für Dozenten dieses Kurses einsehbar
CourseUserNoteSaved: Notizänderungen gespeichert CourseUserNoteSaved: Notizänderungen gespeichert
CourseUserNoteDeleted: Teilnehmernotiz gelöscht CourseUserNoteDeleted: Teilnehmernotiz gelöscht
CourseUserDeregister: Abmelden CourseUserDeregister: Vom Kurs abmelden
CourseUsersDeregistered count@Int64: #{show count} Teilnehmer abgemeldet CourseUsersDeregistered count@Int64: #{show count} Teilnehmer vom Kurs abgemeldet
CourseUserSendMail: Mitteilung verschicken CourseUserSendMail: Mitteilung verschicken
TutorialUserDeregister: Vom Tutorium Abmelden TutorialUserDeregister: Vom Tutorium Abmelden
TutorialUserSendMail: Mitteilung verschicken TutorialUserSendMail: Mitteilung verschicken
@ -149,20 +150,28 @@ CourseAllocationOption term@Text name@Text: #{name} (#{term})
CourseAllocationMinCapacity: Minimale Teilnehmeranzahl CourseAllocationMinCapacity: Minimale Teilnehmeranzahl
CourseAllocationMinCapacityTip: Wenn der Veranstaltung bei der Zentralanmeldung weniger als diese Anzahl von Teilnehmern zugeteilt würden, werden diese stattdessen auf andere Kurse umverteilt CourseAllocationMinCapacityTip: Wenn der Veranstaltung bei der Zentralanmeldung weniger als diese Anzahl von Teilnehmern zugeteilt würden, werden diese stattdessen auf andere Kurse umverteilt
CourseAllocationMinCapacityMustBeNonNegative: Minimale Teilnehmeranzahl darf nicht negativ sein CourseAllocationMinCapacityMustBeNonNegative: Minimale Teilnehmeranzahl darf nicht negativ sein
CourseAllocationInstructions: Anweisungen zur Bewerbung CourseApplicationInstructions: Anweisungen zur Bewerbung/Anmeldung
CourseAllocationInstructionsTip: Wird den Studierenden angezeigt, wenn diese sich für Ihre Veranstaltung bewerben CourseApplicationInstructionsTip: Wird den Studierenden angezeigt, wenn diese sich für Ihre Veranstaltung bewerben bzw. bei dieser anmelden
CourseAllocationApplicationTemplate: Bewerbungsvorlagen CourseApplicationTemplate: Bewerbungsvorlagen
CourseAllocationApplicationText: Text-Bewerbungen CourseApplicationTemplateTip: Werden den Studierenden zum download angeboten, wenn diese sich für Ihre Veranstaltung bewerben bzw. bei dieser anmelden
CourseAllocationApplicationTextTip: Sollen die Studierenden Bewerbungen (ggf. zusätzlich zu abgegebenen Dateien) als unformatierten Text einreichen? CourseApplicationText: Text-Bewerbungen
CourseAllocationApplicationRatingsVisible: Feedback für Bewerbungen CourseApplicationTextTip: Sollen die Studierenden Bewerbungen (ggf. zusätzlich zu abgegebenen Dateien) als unformatierten Text einreichen?
CourseAllocationApplicationRatingsVisibleTip: Sollen Bewertung und Kommentar der Bewerbungen den Studierenden nach Ende der Bewertungs-Phase angezeigt werden? CourseApplicationRatingsVisible: Feedback für Bewerbungen
CourseApplicationRatingsVisibleTip: Sollen Bewertung und Kommentar der Bewerbungen den Studierenden nach Ende der Bewertungs-Phase angezeigt werden?
CourseApplicationRequired: Bewerbungsverfahren
CourseApplicationRequiredTip: Sollen Anmeldungen zu diesem Kurs zunächst provisorisch (ohne Kapazitätsbeschränkung) sein, bis sie durch einen Kursverwalter (nach Bewertung der Bewerbungen) akzeptiert werden?
CourseApplicationInstructionsApplication: Anweisungen zur Bewerbung
CourseApplicationInstructionsRegistration: Anweisungen zur Anmeldung
CourseApplicationTemplateApplication: Bewerbungsvorlage(n)
CourseApplicationTemplateRegistration: Anmeldungsvorlage(n)
CourseApplicationTemplateArchiveName tid@TermId ssh@SchoolId csh@CourseShorthand: #{foldCase (termToText (unTermKey tid))}-#{foldedCase (unSchoolKey ssh)}-#{foldedCase csh}-bewerbungsvorlagen
CourseNoAllocationsAvailable: Es sind aktuell keine Zentralanmeldungen verfügbar CourseNoAllocationsAvailable: Es sind aktuell keine Zentralanmeldungen verfügbar
AllocationStaffRegisterToExpired: Es dürfen keine Änderungen an der Eintragung des Kurses zur Zentralanmeldung mehr vorgenommen werden AllocationStaffRegisterToExpired: Es dürfen keine Änderungen an der Eintragung des Kurses zur Zentralanmeldung mehr vorgenommen werden
CourseFormSectionRegistration: Anmeldung CourseFormSectionRegistration: Anmeldung zum Kurs
CourseFormSectionAdministration: Verwaltung CourseFormSectionAdministration: Verwaltung
CourseLecturers: Kursverwalter CourseLecturers: Kursverwalter
@ -290,6 +299,7 @@ MaterialDeleteCaption: Wollen Sie das unten aufgeführte Material wirklich lösc
MaterialDelHasFiles count@Int64: inklusive #{count} #{pluralDE count "Datei" "Dateien"} MaterialDelHasFiles count@Int64: inklusive #{count} #{pluralDE count "Datei" "Dateien"}
MaterialIsVisible: Achtung, dieses Material wurde bereits veröffentlicht. MaterialIsVisible: Achtung, dieses Material wurde bereits veröffentlicht.
MaterialDeleted materialName@MaterialName: Material "#{materialName}" gelöscht 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}
Unauthorized: Sie haben hierfür keine explizite Berechtigung. Unauthorized: Sie haben hierfür keine explizite Berechtigung.
@ -445,6 +455,10 @@ UpdatedSheetCorrectorsAutoFailed n@Int: #{n} #{pluralDE n "Abgabe konnte" "Abgab
CouldNotAssignCorrectorsAuto num@Int64: #{num} Abgaben konnten nicht automatisch zugewiesen werden: CouldNotAssignCorrectorsAuto num@Int64: #{num} Abgaben konnten nicht automatisch zugewiesen werden:
SelfCorrectors num@Int64: #{num} Abgaben wurden Abgebenden als eigenem Korrektor zugeteilt! SelfCorrectors num@Int64: #{num} Abgaben wurden Abgebenden als eigenem Korrektor zugeteilt!
SubmissionOriginal: Original
SubmissionCorrected: Korrigiert
SubmissionArchiveName: abgaben
SubmissionTypeArchiveName tid@TermId ssh@SchoolId csh@CourseShorthand shn@SheetName subId@CryptoFileNameSubmission renderedSfType@Text: #{foldCase (termToText (unTermKey tid))}-#{foldedCase (unSchoolKey ssh)}-#{foldedCase csh}-#{foldedCase shn}-#{foldCase (toPathPiece subId)}-#{foldCase renderedSfType}
CorrectionSheets: Übersicht Korrekturen nach Blättern CorrectionSheets: Übersicht Korrekturen nach Blättern
CorrectionCorrectors: Übersicht Korrekturen nach Korrektoren CorrectionCorrectors: Übersicht Korrekturen nach Korrektoren
@ -770,6 +784,9 @@ SheetGroupMaxGroupsize: Maximale Gruppengröße
SheetFiles: Übungsblatt-Dateien SheetFiles: Übungsblatt-Dateien
SheetFileTypeHeader: Zugehörigkeit SheetFileTypeHeader: Zugehörigkeit
SheetArchiveName tid@TermId ssh@SchoolId csh@CourseShorthand shn@SheetName: #{foldCase (termToText (unTermKey tid))}-#{foldedCase (unSchoolKey ssh)}-#{foldedCase csh}-#{foldedCase shn}
SheetTypeArchiveName tid@TermId ssh@SchoolId csh@CourseShorthand shn@SheetName renderedSft@Text: #{foldCase (termToText (unTermKey tid))}-#{foldedCase (unSchoolKey ssh)}-#{foldedCase csh}-#{foldedCase shn}-#{foldCase renderedSft}
NotificationTriggerSubmissionRatedGraded: Meine Abgabe in einem gewerteten Übungsblatt wurde korrigiert NotificationTriggerSubmissionRatedGraded: Meine Abgabe in einem gewerteten Übungsblatt wurde korrigiert
NotificationTriggerSubmissionRated: Meine Abgabe wurde korrigiert NotificationTriggerSubmissionRated: Meine Abgabe wurde korrigiert
NotificationTriggerSheetActive: Ich kann ein neues Übungsblatt herunterladen NotificationTriggerSheetActive: Ich kann ein neues Übungsblatt herunterladen
@ -1262,11 +1279,11 @@ ExamPassed: Bestanden
ExamNotPassed: Nicht bestanden ExamNotPassed: Nicht bestanden
ExamResult: Prüfungsergebnis ExamResult: Prüfungsergebnis
ExamRegisteredSuccess exam@ExamName: Erfolgreich zur #{exam} angemeldet ExamRegisteredSuccess exam@ExamName: Erfolgreich zur Prüfung #{exam} angemeldet
ExamDeregisteredSuccess exam@ExamName: Erfolgreich von der #{exam} abgemeldet ExamDeregisteredSuccess exam@ExamName: Erfolgreich von der Prüfung #{exam} abgemeldet
ExamRegistered: Angemeldet ExamRegistered: Zur Prüfung angemeldet
ExamNotRegistered: Nicht angemeldet ExamNotRegistered: Nicht zur Prüfung angemeldet
ExamRegistration: Anmeldung ExamRegistration: Prüfungsanmeldung
ExamRegisterToMustBeAfterRegisterFrom: "Anmeldung ab" muss vor "Anmeldung bis" liegen ExamRegisterToMustBeAfterRegisterFrom: "Anmeldung ab" muss vor "Anmeldung bis" liegen
ExamDeregisterUntilMustBeAfterRegisterFrom: "Abmeldung bis" muss nach "Anmeldung bis" liegen ExamDeregisterUntilMustBeAfterRegisterFrom: "Abmeldung bis" muss nach "Anmeldung bis" liegen
@ -1291,7 +1308,7 @@ ImplementationDetails: Implementierung
ExamUsersHeading: Prüfungsteilnehmer ExamUsersHeading: Prüfungsteilnehmer
ExamUserDeregister: Teilnehmer von Prüfung abmelden ExamUserDeregister: Teilnehmer von Prüfung abmelden
ExamUserAssignOccurrence: Termin/Raum zuweisen ExamUserAssignOccurrence: Termin/Raum zuweisen
ExamUsersDeregistered count@Int64: #{show count} Teilnehmer abgemeldet ExamUsersDeregistered count@Int64: #{show count} Teilnehmer von der Prüfung abgemeldet
ExamUsersOccurrenceUpdated count@Int64: Termin/Raum für #{show count} Teilnehmer gesetzt ExamUsersOccurrenceUpdated count@Int64: Termin/Raum für #{show count} Teilnehmer gesetzt
CsvFile: CSV-Datei CsvFile: CSV-Datei
@ -1364,8 +1381,8 @@ BtnPasswordReset: Passwort zurücksetzen
AuthLDAPLookupFailed: Nutzer konnte aufgrund eines LDAP-Fehlers nicht nachgeschlagen werden AuthLDAPLookupFailed: Nutzer konnte aufgrund eines LDAP-Fehlers nicht nachgeschlagen werden
AuthLDAPInvalidLookup: Bestehender Nutzer konnte nicht eindeutig einem LDAP-Eintrag zugeordnet werden AuthLDAPInvalidLookup: Bestehender Nutzer konnte nicht eindeutig einem LDAP-Eintrag zugeordnet werden
AuthLDAPAlreadyConfigured: Nutzer meldet sich bereits per Campus-Kennung an AuthLDAPAlreadyConfigured: Nutzer meldet sich bereits per Campus-Kennung in Uni2work an
AuthLDAPConfigured: Nutzer meldet sich nun per Campus-Kennung an AuthLDAPConfigured: Nutzer meldet sich nun per Campus-Kennung in Uni2work an
AuthPWHashAlreadyConfigured: Nutzer meldet sich bereits per Uni2work-Kennung an AuthPWHashAlreadyConfigured: Nutzer meldet sich bereits per Uni2work-Kennung an
AuthPWHashConfigured: Nutzer meldet sich nun per Uni2work-Kennung an AuthPWHashConfigured: Nutzer meldet sich nun per Uni2work-Kennung an

View File

@ -31,38 +31,14 @@ AllocationCourse
allocation AllocationId allocation AllocationId
course CourseId course CourseId
minCapacity Int -- if the course would get assigned fewer than this many applicants, restart the assignment process without the course minCapacity Int -- if the course would get assigned fewer than this many applicants, restart the assignment process without the course
instructions Html Maybe -- instructions from the lecturer to applicants
applicationText Bool -- lecturer will read application texts supplied by users
applicationFiles UploadMode -- lecturer wants to receive course specific application files
ratingsVisible Bool -- lecturer wants applicants to receive feedback on their application (Grade & comment)
UniqueAllocationCourse course UniqueAllocationCourse course
AllocationCourseFile
allocationCourse AllocationCourseId
file FileId
UniqueAllocationCourseFile allocationCourse file
AllocationUser AllocationUser
allocation AllocationId allocation AllocationId
user UserId user UserId
totalCourses Natural -- number of total allocated courses for this user must be <= than this number totalCourses Natural -- number of total allocated courses for this user must be <= than this number
UniqueAllocationUser allocation user UniqueAllocationUser allocation user
AllocationApplication
allocationCourse AllocationCourseId
allocationUser AllocationUserId
text Text Maybe -- free text entered by user
priority Natural -- priority, higher number means higher priority
ratingVeto Bool
ratingPoints ExamGrade Maybe
ratingComment Text Maybe
UniqueAllocationApplication allocationCourse allocationUser
AllocationApplicationFile -- supplemental file for application by a user for a certain course
application AllocationApplicationId
file FileId
UniqueAllocationUserFile application file
AllocationDeregister -- self-inflicted user-deregistrations from an allocated course AllocationDeregister -- self-inflicted user-deregistrations from an allocated course
user UserId user UserId
allocation AllocationId Maybe allocation AllocationId Maybe

View File

@ -17,9 +17,20 @@ Course -- Information about a single course; contained info is always visible
deregisterUntil UTCTime Maybe -- unenrolement may be prohibited from a given date onwards deregisterUntil UTCTime Maybe -- unenrolement may be prohibited from a given date onwards
registerSecret Text Maybe -- enrolement maybe protected by a simple common passphrase 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 materialFree Bool -- False: only enrolled users may see course materials not stored in this table
applicationsRequired Bool
applicationsInstructions Html Maybe
applicationsText Bool
applicationsFiles UploadMode
applicationsRatingsVisible Bool
TermSchoolCourseShort term school shorthand -- shorthand must be unique within school and semester TermSchoolCourseShort term school shorthand -- shorthand must be unique within school and semester
TermSchoolCourseName term school name -- name must be unique within school and semester TermSchoolCourseName term school name -- name must be unique within school and semester
deriving Generic deriving Generic
CourseAppInstructionFile
course CourseId
file FileId
UniqueCourseAppInstructionFile course file
CourseEdit -- who edited when a row in table "Course", kept indefinitely (might be replaced by generic Audit Table; like all ...-Edit tables) CourseEdit -- who edited when a row in table "Course", kept indefinitely (might be replaced by generic Audit Table; like all ...-Edit tables)
user UserId user UserId
time UTCTime time UTCTime
@ -59,3 +70,16 @@ CourseUserNoteEdit -- who edited a participants course note when
user UserId user UserId
time UTCTime time UTCTime
note CourseUserNoteId -- PROBLEM: deleted notes have no modification date any more note CourseUserNoteId -- PROBLEM: deleted notes have no modification date any more
CourseApplication
course CourseId
user UserId
text Text Maybe -- free text entered by user
ratingPoints ExamGrade Maybe
ratingComment Text Maybe
allocation AllocationId Maybe
allocationPriority Natural Maybe
CourseApplicationFile
application CourseApplication
file FileId
UniqueApplicationFile application file

1
routes
View File

@ -87,6 +87,7 @@
/course/#TermId/#SchoolId/#CourseShorthand CourseR !lecturer: /course/#TermId/#SchoolId/#CourseShorthand CourseR !lecturer:
/ CShowR GET !free / CShowR GET !free
/register CRegisterR GET POST !timeANDcapacityANDallocation-time !lecturerANDallocation-time /register CRegisterR GET POST !timeANDcapacityANDallocation-time !lecturerANDallocation-time
/register-template CRegisterTemplateR GET
/edit CEditR GET POST /edit CEditR GET POST
/lecturer-invite CLecInviteR GET POST /lecturer-invite CLecInviteR GET POST
/delete CDeleteR GET POST !lecturerANDemptyANDallocation-time /delete CDeleteR GET POST !lecturerANDemptyANDallocation-time

View File

@ -282,6 +282,7 @@ embedRenderMessage ''UniWorX ''MessageStatus ("Message" <>)
embedRenderMessage ''UniWorX ''NotificationTrigger $ ("NotificationTrigger" <>) . concat . drop 1 . splitCamel embedRenderMessage ''UniWorX ''NotificationTrigger $ ("NotificationTrigger" <>) . concat . drop 1 . splitCamel
embedRenderMessage ''UniWorX ''StudyFieldType id embedRenderMessage ''UniWorX ''StudyFieldType id
embedRenderMessage ''UniWorX ''SheetFileType id embedRenderMessage ''UniWorX ''SheetFileType id
embedRenderMessage ''UniWorX ''SubmissionFileType id
embedRenderMessage ''UniWorX ''CorrectorState id embedRenderMessage ''UniWorX ''CorrectorState id
embedRenderMessage ''UniWorX ''RatingException id embedRenderMessage ''UniWorX ''RatingException id
embedRenderMessage ''UniWorX ''SubmissionSinkException ("SubmissionSinkException" <>) embedRenderMessage ''UniWorX ''SubmissionSinkException ("SubmissionSinkException" <>)

View File

@ -41,6 +41,12 @@ data CourseForm = CourseForm
, cfLink :: Maybe Text , cfLink :: Maybe Text
, cfMatFree :: Bool , cfMatFree :: Bool
, cfAllocation :: Maybe AllocationCourseForm , cfAllocation :: Maybe AllocationCourseForm
, cfAppRequired :: Bool
, cfAppInstructions :: Maybe Html
, cfAppInstructionFiles :: Maybe (Source Handler (Either FileId File))
, cfAppText :: Bool
, cfAppFiles :: UploadMode
, cfAppRatingsVisible :: Bool
, cfCapacity :: Maybe Int , cfCapacity :: Maybe Int
, cfSecret :: Maybe Text , cfSecret :: Maybe Text
, cfRegFrom :: Maybe UTCTime , cfRegFrom :: Maybe UTCTime
@ -51,43 +57,45 @@ data CourseForm = CourseForm
data AllocationCourseForm = AllocationCourseForm data AllocationCourseForm = AllocationCourseForm
{ acfAllocation :: AllocationId { acfAllocation :: AllocationId
, acfInstructions :: Maybe Html
, acfFiles :: Maybe (Source Handler (Either FileId File))
, acfApplicationText :: Bool
, acfApplicationFiles :: UploadMode
, acfApplicationRatingsVisible :: Bool
, acfMinCapacity :: Int , acfMinCapacity :: Int
} }
courseToForm :: Entity Course -> [Lecturer] -> [(UserEmail, InvitationDBData Lecturer)] -> Maybe (Entity AllocationCourse) -> CourseForm courseToForm :: Entity Course -> [Lecturer] -> [(UserEmail, InvitationDBData Lecturer)] -> Maybe (Entity AllocationCourse) -> CourseForm
courseToForm (Entity cid Course{..}) lecs lecInvites alloc = CourseForm courseToForm (Entity cid Course{..}) lecs lecInvites alloc = CourseForm
{ cfCourseId = Just cid { cfCourseId = Just cid
, cfName = courseName , cfName = courseName
, cfDesc = courseDescription , cfDesc = courseDescription
, cfLink = courseLinkExternal , cfLink = courseLinkExternal
, cfShort = courseShorthand , cfShort = courseShorthand
, cfTerm = courseTerm , cfTerm = courseTerm
, cfSchool = courseSchool , cfSchool = courseSchool
, cfCapacity = courseCapacity , cfCapacity = courseCapacity
, cfSecret = courseRegisterSecret , cfSecret = courseRegisterSecret
, cfMatFree = courseMaterialFree , cfMatFree = courseMaterialFree
, cfRegFrom = courseRegisterFrom , cfAllocation = allocationCourseToForm <$> alloc
, cfRegTo = courseRegisterTo , cfAppRequired = courseApplicationsRequired
, cfDeRegUntil = courseDeregisterUntil , cfAppInstructions = courseApplicationsInstructions
, cfAllocation = allocationCourseToForm <$> alloc , cfAppInstructionFiles
, cfLecturers = [Right (lecturerUser, lecturerType) | Lecturer{..} <- lecs] , cfAppText = courseApplicationsText
++ [Left (email, mType) | (email, InvDBDataLecturer mType) <- lecInvites ] , cfAppFiles = courseApplicationsFiles
, cfAppRatingsVisible = courseApplicationsRatingsVisible
, cfRegFrom = courseRegisterFrom
, cfRegTo = courseRegisterTo
, cfDeRegUntil = courseDeregisterUntil
, cfLecturers = [Right (lecturerUser, lecturerType) | Lecturer{..} <- lecs]
++ [Left (email, mType) | (email, InvDBDataLecturer mType) <- lecInvites ]
} }
where
cfAppInstructionFiles = Just . transPipe runDB $ selectAppFiles .| C.map (Left . E.unValue)
where selectAppFiles = E.selectSource . E.from $ \courseAppInstructionFile -> do
E.where_ $ courseAppInstructionFile E.^. CourseAppInstructionFileCourse E.==. E.val cid
return $ courseAppInstructionFile E.^. CourseAppInstructionFileFile
allocationCourseToForm :: Entity AllocationCourse -> AllocationCourseForm allocationCourseToForm :: Entity AllocationCourse -> AllocationCourseForm
allocationCourseToForm (Entity _ AllocationCourse{..}) = AllocationCourseForm allocationCourseToForm (Entity _ AllocationCourse{..}) = AllocationCourseForm
{ acfAllocation = allocationCourseAllocation { acfAllocation = allocationCourseAllocation
, acfMinCapacity = allocationCourseMinCapacity , acfMinCapacity = allocationCourseMinCapacity
, acfInstructions = allocationCourseInstructions
, acfFiles = Nothing
, acfApplicationText = allocationCourseApplicationText
, acfApplicationFiles = allocationCourseApplicationFiles
, acfApplicationRatingsVisible = allocationCourseRatingsVisible
} }
makeCourseForm :: (forall p. PathPiece p => p -> Maybe (SomeRoute UniWorX)) -> Maybe CourseForm -> Form CourseForm makeCourseForm :: (forall p. PathPiece p => p -> Maybe (SomeRoute UniWorX)) -> Maybe CourseForm -> Form CourseForm
@ -213,21 +221,9 @@ makeCourseForm miButtonAction template = identifyForm FIDcourse $ \html -> do
_ -> do _ -> do
allocationOptions <- mkOptionList <$> mapM mkAllocationOption availableAllocations allocationOptions <- mkOptionList <$> mapM mkAllocationOption availableAllocations
oldFileIds <- for ((,) <$> (fmap acfAllocation $ template >>= cfAllocation) <*> (template >>= cfCourseId)) $ \(allId, cId) -> fmap (Set.fromList . map E.unValue) . liftHandlerT . runDB . E.select . E.from $ \(allocationCourseFile `E.InnerJoin` allocationCourse) -> do
E.on $ allocationCourseFile E.^. AllocationCourseFileAllocationCourse E.==. allocationCourse E.^. AllocationCourseId
E.where_ $ allocationCourse E.^. AllocationCourseCourse E.==. E.val cId
E.&&. allocationCourse E.^. AllocationCourseAllocation E.==. E.val allId
return $ allocationCourseFile E.^. AllocationCourseFileFile
let let
allocationForm' = AllocationCourseForm allocationForm' = AllocationCourseForm
<$> apreq (selectField' Nothing $ return allocationOptions) (fslI MsgCourseAllocation) (fmap acfAllocation $ template >>= cfAllocation) <$> apreq (selectField' Nothing $ return allocationOptions) (fslI MsgCourseAllocation) (fmap acfAllocation $ template >>= cfAllocation)
<*> (assertM (not . null . renderHtml) <$> aopt htmlField (fslI MsgCourseAllocationInstructions & setTooltip MsgCourseAllocationInstructionsTip) (fmap acfInstructions $ template >>= cfAllocation))
<*> aopt (multiFileField . return $ fromMaybe Set.empty oldFileIds) (fslI MsgCourseAllocationApplicationTemplate) (fmap acfFiles $ template >>= cfAllocation)
<*> apopt checkBoxField (fslI MsgCourseAllocationApplicationText & setTooltip MsgCourseAllocationApplicationTextTip) (fmap acfApplicationText $ template >>= cfAllocation)
<*> uploadModeForm (fmap acfApplicationFiles $ template >>= cfAllocation)
<*> apopt checkBoxField (fslI MsgCourseAllocationApplicationRatingsVisible & setTooltip MsgCourseAllocationApplicationRatingsVisibleTip) (fmap acfApplicationRatingsVisible $ template >>= cfAllocation)
<*> apreq (natFieldI MsgCourseAllocationMinCapacityMustBeNonNegative) (fslI MsgCourseAllocationMinCapacity & setTooltip MsgCourseAllocationMinCapacityTip) (fmap acfMinCapacity $ template >>= cfAllocation) <*> apreq (natFieldI MsgCourseAllocationMinCapacityMustBeNonNegative) (fslI MsgCourseAllocationMinCapacity & setTooltip MsgCourseAllocationMinCapacityTip) (fmap acfMinCapacity $ template >>= cfAllocation)
optionalActionW allocationForm' (fslI MsgCourseAllocationParticipate & setTooltip MsgCourseAllocationParticipateTip) (is _Just . cfAllocation <$> template) optionalActionW allocationForm' (fslI MsgCourseAllocationParticipate & setTooltip MsgCourseAllocationParticipateTip) (is _Just . cfAllocation <$> template)
@ -247,6 +243,12 @@ makeCourseForm miButtonAction template = identifyForm FIDcourse $ \html -> do
<*> apopt checkBoxField (fslI MsgMaterialFree) (cfMatFree <$> template) <*> apopt checkBoxField (fslI MsgMaterialFree) (cfMatFree <$> template)
<* aformSection MsgCourseFormSectionRegistration <* aformSection MsgCourseFormSectionRegistration
<*> allocationForm <*> allocationForm
<*> apopt checkBoxField (fslI MsgCourseApplicationRequired & setTooltip MsgCourseApplicationRequiredTip) (cfAppRequired <$> template)
<*> (assertM (not . null . renderHtml) <$> aopt htmlField (fslI MsgCourseApplicationInstructions & setTooltip MsgCourseApplicationInstructionsTip) (cfAppInstructions <$> template))
<*> aopt (multiFileField' . fromMaybe (return ()) $ cfAppInstructionFiles =<< template) (fslI MsgCourseApplicationTemplate & setTooltip MsgCourseApplicationTemplateTip) (cfAppInstructionFiles <$> template)
<*> apopt checkBoxField (fslI MsgCourseApplicationText & setTooltip MsgCourseApplicationTextTip) (cfAppText <$> template)
<*> uploadModeForm (cfAppFiles <$> template)
<*> apopt checkBoxField (fslI MsgCourseApplicationRatingsVisible & setTooltip MsgCourseApplicationRatingsVisibleTip) (cfAppRatingsVisible <$> template)
<*> aopt (natFieldI MsgCourseCapacity) (fslI MsgCourseCapacity <*> aopt (natFieldI MsgCourseCapacity) (fslI MsgCourseCapacity
& setTooltip MsgCourseCapacityTip) (cfCapacity <$> template) & setTooltip MsgCourseCapacityTip) (cfCapacity <$> template)
<*> aopt textField (fslpI MsgCourseSecret (mr MsgCourseSecretFormat) <*> aopt textField (fslpI MsgCourseSecret (mr MsgCourseSecretFormat)
@ -425,20 +427,26 @@ courseEditHandler miButtonAction mbCourseForm = do
} -> do -- create new course } -> do -- create new course
now <- liftIO getCurrentTime now <- liftIO getCurrentTime
insertOkay <- runDBJobs $ do insertOkay <- runDBJobs $ do
insertOkay <- insertUnique Course insertOkay <- let CourseForm{..} = res
{ courseName = cfName res in insertUnique Course
, courseDescription = cfDesc res { courseName = cfName
, courseLinkExternal = cfLink res , courseDescription = cfDesc
, courseShorthand = cfShort res , courseLinkExternal = cfLink
, courseTerm = cfTerm res , courseShorthand = cfShort
, courseSchool = cfSchool res , courseTerm = cfTerm
, courseCapacity = cfCapacity res , courseSchool = cfSchool
, courseRegisterSecret = cfSecret res , courseCapacity = cfCapacity
, courseMaterialFree = cfMatFree res , courseRegisterSecret = cfSecret
, courseRegisterFrom = cfRegFrom res , courseMaterialFree = cfMatFree
, courseRegisterTo = cfRegTo res , courseApplicationsRequired = cfAppRequired
, courseDeregisterUntil = cfDeRegUntil res , courseApplicationsInstructions = cfAppInstructions
} , courseApplicationsText = cfAppText
, courseApplicationsFiles = cfAppFiles
, courseApplicationsRatingsVisible = cfAppRatingsVisible
, courseRegisterFrom = cfRegFrom
, courseRegisterTo = cfRegTo
, courseDeregisterUntil = cfDeRegUntil
}
whenIsJust insertOkay $ \cid -> do whenIsJust insertOkay $ \cid -> do
let (invites, adds) = partitionEithers $ cfLecturers res let (invites, adds) = partitionEithers $ cfLecturers res
insertMany_ $ map (\(lid, lty) -> Lecturer lid cid lty) adds insertMany_ $ map (\(lid, lty) -> Lecturer lid cid lty) adds
@ -466,20 +474,26 @@ courseEditHandler miButtonAction mbCourseForm = do
case old of case old of
Nothing -> addMessageI Error MsgInvalidInput $> False Nothing -> addMessageI Error MsgInvalidInput $> False
(Just _) -> do (Just _) -> do
updOkay <- myReplaceUnique cid Course updOkay <- let CourseForm{..} = res
{ courseName = cfName res in myReplaceUnique cid Course
, courseDescription = cfDesc res { courseName = cfName
, courseLinkExternal = cfLink res , courseDescription = cfDesc
, courseShorthand = cfShort res , courseLinkExternal = cfLink
, courseTerm = cfTerm res -- dangerous , courseShorthand = cfShort
, courseSchool = cfSchool res , courseTerm = cfTerm -- dangerous
, courseCapacity = cfCapacity res , courseSchool = cfSchool
, courseRegisterSecret = cfSecret res , courseCapacity = cfCapacity
, courseMaterialFree = cfMatFree res , courseRegisterSecret = cfSecret
, courseRegisterFrom = cfRegFrom res , courseMaterialFree = cfMatFree
, courseRegisterTo = cfRegTo res , courseApplicationsRequired = cfAppRequired
, courseDeregisterUntil = cfDeRegUntil res , courseApplicationsInstructions = cfAppInstructions
} , courseApplicationsText = cfAppText
, courseApplicationsFiles = cfAppFiles
, courseApplicationsRatingsVisible = cfAppRatingsVisible
, courseRegisterFrom = cfRegFrom
, courseRegisterTo = cfRegTo
, courseDeregisterUntil = cfDeRegUntil
}
case updOkay of case updOkay of
(Just _) -> addMessageI Warning (MsgCourseEditDupShort tid ssh csh) $> False (Just _) -> addMessageI Warning (MsgCourseEditDupShort tid ssh csh) $> False
Nothing -> do Nothing -> do
@ -490,7 +504,19 @@ courseEditHandler miButtonAction mbCourseForm = do
sinkInvitationsF lecturerInvitationConfig $ map (\(lEmail, mLty) -> (lEmail, cid, (InvDBDataLecturer mLty, InvTokenDataLecturer))) invites sinkInvitationsF lecturerInvitationConfig $ map (\(lEmail, mLty) -> (lEmail, cid, (InvDBDataLecturer mLty, InvTokenDataLecturer))) invites
insert_ $ CourseEdit aid now cid insert_ $ CourseEdit aid now cid
let
finsert val = do
fId <- lift $ either return insert val
tell $ Set.singleton fId
lift $
void . insertUnique $ CourseAppInstructionFile cid fId
keep <- execWriterT . runConduit $ transPipe liftHandlerT (traverse_ id $ cfAppInstructionFiles res) .| C.mapM_ finsert
acfs <- selectList [ CourseAppInstructionFileCourse ==. cid, CourseAppInstructionFileFile /<-. Set.toList keep ] []
mapM_ deleteCascade $ map (courseAppInstructionFileFile . entityVal) acfs
upsertAllocationCourse cid $ cfAllocation res upsertAllocationCourse cid $ cfAllocation res
addMessageI Success $ MsgCourseEditOk tid ssh csh addMessageI Success $ MsgCourseEditOk tid ssh csh
return True return True
when success $ redirect $ CourseR tid ssh csh CShowR when success $ redirect $ CourseR tid ssh csh CShowR
@ -522,38 +548,17 @@ upsertAllocationCourse cid cfAllocation = do
when doEdit $ when doEdit $
case cfAllocation of case cfAllocation of
Just AllocationCourseForm{..} -> do Just AllocationCourseForm{..} ->
Entity acId _ <- upsert AllocationCourse void $ upsert AllocationCourse
{ allocationCourseAllocation = acfAllocation { allocationCourseAllocation = acfAllocation
, allocationCourseCourse = cid , allocationCourseCourse = cid
, allocationCourseMinCapacity = acfMinCapacity , allocationCourseMinCapacity = acfMinCapacity
, allocationCourseInstructions = acfInstructions
, allocationCourseApplicationText = acfApplicationText
, allocationCourseApplicationFiles = acfApplicationFiles
, allocationCourseRatingsVisible = acfApplicationRatingsVisible
} }
[ AllocationCourseAllocation =. acfAllocation [ AllocationCourseAllocation =. acfAllocation
, AllocationCourseCourse =. cid , AllocationCourseCourse =. cid
, AllocationCourseMinCapacity =. acfMinCapacity , AllocationCourseMinCapacity =. acfMinCapacity
, AllocationCourseInstructions =. acfInstructions
, AllocationCourseApplicationText =. acfApplicationText
, AllocationCourseApplicationFiles =. acfApplicationFiles
, AllocationCourseRatingsVisible =. acfApplicationRatingsVisible
] ]
let
finsert val = do
fId <- lift $ either return insert val
tell $ Set.singleton fId
lift $
void . insertUnique $ AllocationCourseFile acId fId
keep <- execWriterT . runConduit $ transPipe liftHandlerT (traverse_ id acfFiles) .| C.mapM_ finsert
acfs <- selectList [ AllocationCourseFileAllocationCourse ==. acId, AllocationCourseFileFile /<-. Set.toList keep ] []
mapM_ deleteCascade $ map (allocationCourseFileFile . entityVal) acfs
Nothing Nothing
| Just (Entity prevId _) <- prevAllocationCourse | Just (Entity prevId _) <- prevAllocationCourse
-> do -> delete prevId
acfs <- selectList [ AllocationCourseFileAllocationCourse ==. prevId ] []
mapM_ deleteCascade $ map (allocationCourseFileFile . entityVal) acfs
delete prevId
_other -> return () _other -> return ()

View File

@ -1,5 +1,6 @@
module Handler.Course.Show module Handler.Course.Show
( getCShowR ( getCShowR
, getCRegisterTemplateR
) where ) where
import Import import Import
@ -19,11 +20,15 @@ import qualified Database.Esqueleto as E
import Handler.Course.Register import Handler.Course.Register
import System.FilePath (addExtension)
import qualified Data.Conduit.List as C
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,defSFid,lecturers,assistants,correctors,tutors,mAllocation) <- runDB . maybeT notFound $ do (cid,course,schoolName,participants,registration,defSFid,lecturers,assistants,correctors,tutors,mAllocation,hasApplicationTemplate) <- 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
@ -66,7 +71,9 @@ getCShowR tid ssh csh = do
E.where_ $ allocationCourse E.^. AllocationCourseCourse E.==. E.val cid E.where_ $ allocationCourse E.^. AllocationCourseCourse E.==. E.val cid
E.limit 1 E.limit 1
return allocation return allocation
return (cid,course,schoolName,participants,registration,entityKey <$> defSFid,lecturers,assistants,correctors,tutors,mAllocation) hasApplicationTemplate <- lift . E.selectExists . E.from $ \courseAppInstructionFile ->
E.where_ $ courseAppInstructionFile E.^. CourseAppInstructionFileCourse E.==. E.val cid
return (cid,course,schoolName,participants,registration,entityKey <$> defSFid,lecturers,assistants,correctors,tutors,mAllocation,hasApplicationTemplate)
mRegFrom <- traverse (formatTime SelFormatDateTime) $ courseRegisterFrom course mRegFrom <- traverse (formatTime SelFormatDateTime) $ courseRegisterFrom course
mRegTo <- traverse (formatTime SelFormatDateTime) $ courseRegisterTo course mRegTo <- traverse (formatTime SelFormatDateTime) $ courseRegisterTo course
@ -224,3 +231,15 @@ getCShowR tid ssh csh = do
siteLayout (toWgt $ courseName course) $ do siteLayout (toWgt $ courseName course) $ do
setTitleI $ prependCourseTitle tid ssh csh (""::Text) setTitleI $ prependCourseTitle tid ssh csh (""::Text)
$(widgetFile "course") $(widgetFile "course")
getCRegisterTemplateR :: TermId -> SchoolId -> CourseShorthand -> Handler TypedContent
getCRegisterTemplateR tid ssh csh = do
archiveName <- fmap (flip addExtension (unpack extensionZip) . unpack). ap getMessageRender . pure $ MsgCourseApplicationTemplateArchiveName tid ssh csh
let source = (.| C.map entityVal) . E.selectSource . E.from $ \(file `E.InnerJoin` courseAppInstructionFile `E.InnerJoin` course) -> do
E.on $ course E.^. CourseId E.==. courseAppInstructionFile E.^. CourseAppInstructionFileCourse
E.on $ courseAppInstructionFile E.^. CourseAppInstructionFileFile E.==. file E.^. FileId
E.where_ $ course E.^. CourseTerm E.==. E.val tid
E.&&. course E.^. CourseSchool E.==. E.val ssh
E.&&. course E.^. CourseShorthand E.==. E.val csh
return file
serveSomeFiles archiveName source

View File

@ -23,6 +23,8 @@ import Handler.Utils.Table.Columns
import Control.Monad.Writer (MonadWriter(..), execWriterT) import Control.Monad.Writer (MonadWriter(..), execWriterT)
import System.FilePath (addExtension)
data MaterialForm = MaterialForm data MaterialForm = MaterialForm
{ mfName :: MaterialName { mfName :: MaterialName
@ -358,16 +360,19 @@ postMDelR tid ssh csh mnm = do
-- | Serve all material-files -- | Serve all material-files
getMArchiveR :: TermId -> SchoolId -> CourseShorthand -> MaterialName -> Handler TypedContent getMArchiveR :: TermId -> SchoolId -> CourseShorthand -> MaterialName -> Handler TypedContent
getMArchiveR tid ssh csh mnm = serveSomeFiles archivename getMatQuery getMArchiveR tid ssh csh mnm = do
where archiveName <- fmap (flip addExtension (unpack extensionZip) . unpack). ap getMessageRender . pure $ MsgMaterialArchiveName tid ssh csh mnm
archivename = unpack (termToText (unTermKey tid) <> "-" <> toPathPiece (unSchoolKey ssh <> "-" <> csh <> "-" <> mnm)) <.> "zip"
getMatQuery = (.| C.map entityVal) . E.selectSource . E.from $ let getMatQuery = (.| C.map entityVal) . E.selectSource . E.from $
\(course `E.InnerJoin` material `E.InnerJoin` materialFile `E.InnerJoin` file) -> do \(course `E.InnerJoin` material `E.InnerJoin` materialFile `E.InnerJoin` file) -> do
E.on $ file E.^. FileId E.==. materialFile E.^. MaterialFileFile E.on $ file E.^. FileId E.==. materialFile E.^. MaterialFileFile
E.on $ material E.^. MaterialId E.==. materialFile E.^. MaterialFileMaterial E.on $ material E.^. MaterialId E.==. materialFile E.^. MaterialFileMaterial
E.on $ material E.^. MaterialCourse E.==. course E.^. CourseId E.on $ material E.^. MaterialCourse E.==. course E.^. CourseId
E.where_ $ course E.^. CourseTerm E.==. E.val tid E.where_ $ course E.^. CourseTerm E.==. E.val tid
E.&&. course E.^. CourseSchool E.==. E.val ssh E.&&. course E.^. CourseSchool E.==. E.val ssh
E.&&. course E.^. CourseShorthand E.==. E.val csh E.&&. course E.^. CourseShorthand E.==. E.val csh
E.&&. material E.^. MaterialName E.==. E.val mnm E.&&. material E.^. MaterialName E.==. E.val mnm
return file return file
serveSomeFiles archiveName getMatQuery

View File

@ -60,6 +60,8 @@ import Utils.Sql
import Data.Aeson hiding (Result(..)) import Data.Aeson hiding (Result(..))
import Text.Hamlet (ihamlet) import Text.Hamlet (ihamlet)
import System.FilePath (addExtension)
{- {-
* Implement Handlers * Implement Handlers
@ -439,9 +441,8 @@ getSShowR tid ssh csh shn = do
getSArchiveR :: TermId -> SchoolId -> CourseShorthand -> SheetName -> Handler TypedContent getSArchiveR :: TermId -> SchoolId -> CourseShorthand -> SheetName -> Handler TypedContent
getSArchiveR tid ssh csh shn = do getSArchiveR tid ssh csh shn = do
MsgRenderer mr <- getMsgRenderer archiveName <- fmap (flip addExtension (unpack extensionZip) . unpack). ap getMessageRender . pure $ MsgSheetArchiveName tid ssh csh shn
let archiveName = (unpack . stripAll $ mr (prependCourseTitle tid ssh csh $ SomeMessage shn)) <.> "zip" let sftArchive = CSheetR tid ssh csh shn . SZipR -- used to check access to SheetFileTypes
sftArchive = CSheetR tid ssh csh shn . SZipR -- used to check access to SheetFileTypes
allowedSFTs <- filterM (hasReadAccessTo . sftArchive) [minBound..maxBound] allowedSFTs <- filterM (hasReadAccessTo . sftArchive) [minBound..maxBound]
serveZipArchive archiveName $ sheetFilesSFTsQuery tid ssh csh shn allowedSFTs .| C.map entityVal serveZipArchive archiveName $ sheetFilesSFTsQuery tid ssh csh shn allowedSFTs .| C.map entityVal
@ -476,8 +477,8 @@ getSFileR tid ssh csh shn sft file = serveOneFile $ sheetFileQuery tid ssh csh s
getSZipR :: TermId -> SchoolId -> CourseShorthand -> SheetName -> SheetFileType -> Handler TypedContent getSZipR :: TermId -> SchoolId -> CourseShorthand -> SheetName -> SheetFileType -> Handler TypedContent
getSZipR tid ssh csh shn sft = do getSZipR tid ssh csh shn sft = do
MsgRenderer mr <- getMsgRenderer sft' <- ap getMessageRender $ pure sft
let archiveName = (unpack . stripAll $ mr (prependCourseTitle tid ssh csh $ SomeMessage shn)) <> "_" <> (unpack $ toPathPiece sft) <.> "zip" archiveName <- fmap (flip addExtension (unpack extensionZip) . unpack). ap getMessageRender . pure $ MsgSheetTypeArchiveName tid ssh csh shn sft'
serveSomeFiles archiveName $ sheetFilesAllQuery tid ssh csh shn sft .| C.map entityVal serveSomeFiles archiveName $ sheetFilesAllQuery tid ssh csh shn sft .| C.map entityVal

View File

@ -45,6 +45,8 @@ import Text.Hamlet (ihamlet)
-- import qualified Yesod.Colonnade as Yesod -- import qualified Yesod.Colonnade as Yesod
-- import qualified Text.Blaze.Html5.Attributes as HA -- import qualified Text.Blaze.Html5.Attributes as HA
import System.FilePath (addExtension)
-- DEPRECATED: We always show all edits! -- DEPRECATED: We always show all edits!
-- numberOfSubmissionEditDates :: Int64 -- numberOfSubmissionEditDates :: Int64
-- numberOfSubmissionEditDates = 3 -- for debugging only, should be 1 in production. -- numberOfSubmissionEditDates = 3 -- for debugging only, should be 1 in production.
@ -574,11 +576,10 @@ getSubArchiveR tid ssh csh shn cID sfType = do
when (sfType == SubmissionCorrected) $ when (sfType == SubmissionCorrected) $
guardAuthResult =<< evalAccess (CSubmissionR tid ssh csh shn cID CorrectionR) False guardAuthResult =<< evalAccess (CSubmissionR tid ssh csh shn cID CorrectionR) False
let filename sfType' <- ap getMessageRender $ pure sfType
| SubmissionOriginal <- sfType = toPathPiece cID <> "-" <> toPathPiece sfType archiveName <- fmap (flip addExtension (unpack extensionZip) . unpack) . ap getMessageRender . pure $ MsgSubmissionTypeArchiveName tid ssh csh shn cID sfType'
| otherwise = toPathPiece cID
source = do let source = do
submissionID <- lift $ submissionMatchesSheet tid ssh csh shn cID submissionID <- lift $ submissionMatchesSheet tid ssh csh shn cID
rating <- lift $ getRating submissionID rating <- lift $ getRating submissionID
@ -593,7 +594,7 @@ getSubArchiveR tid ssh csh shn cID sfType = do
when (sfType == SubmissionCorrected) $ when (sfType == SubmissionCorrected) $
maybe (return ()) (yieldM . ratingFile cID) rating maybe (return ()) (yieldM . ratingFile cID) rating
serveSomeFiles (unpack filename <.> "zip") source serveSomeFiles archiveName source
getSubDelR, postSubDelR :: TermId -> SchoolId -> CourseShorthand -> SheetName -> CryptoFileNameSubmission -> Handler Html getSubDelR, postSubDelR :: TermId -> SchoolId -> CourseShorthand -> SheetName -> CryptoFileNameSubmission -> Handler Html
getSubDelR = postSubDelR getSubDelR = postSubDelR

View File

@ -75,7 +75,7 @@ serveSomeFiles archiveName source = do
[file] -> sendThisFile file [file] -> sendThisFile file
_moreFiles -> do _moreFiles -> do
setContentDisposition' $ Just archiveName setContentDisposition' $ Just archiveName
respondSourceDB "application/zip" $ do respondSourceDB typeZip $ do
let zipComment = T.encodeUtf8 $ pack archiveName let zipComment = T.encodeUtf8 $ pack archiveName
source .| produceZip ZipInfo{..} .| Conduit.map toFlushBuilder source .| produceZip ZipInfo{..} .| Conduit.map toFlushBuilder
@ -92,7 +92,7 @@ serveZipArchive archiveName source = do
[] -> notFound [] -> notFound
_moreFiles -> do _moreFiles -> do
setContentDisposition' $ Just archiveName setContentDisposition' $ Just archiveName
respondSourceDB "application/zip" $ do respondSourceDB typeZip $ do
let zipComment = T.encodeUtf8 $ pack archiveName let zipComment = T.encodeUtf8 $ pack archiveName
source .| produceZip ZipInfo{..} .| Conduit.map toFlushBuilder source .| produceZip ZipInfo{..} .| Conduit.map toFlushBuilder

View File

@ -32,7 +32,6 @@ import qualified Data.Map as Map
import qualified Data.Vector as Vector import qualified Data.Vector as Vector
import qualified Data.HashMap.Strict as HashMap import qualified Data.HashMap.Strict as HashMap
import qualified Data.ByteString as BS
import qualified Data.ByteString.Lazy as LBS import qualified Data.ByteString.Lazy as LBS
import qualified Data.Attoparsec.ByteString.Lazy as A import qualified Data.Attoparsec.ByteString.Lazy as A
@ -43,8 +42,8 @@ instance Exception CsvParseError
typeCsv, typeCsv' :: ContentType typeCsv, typeCsv' :: ContentType
typeCsv = "text/csv" typeCsv = simpleContentType typeCsv'
typeCsv' = BS.intercalate "; " [typeCsv, "charset=UTF-8", "header=present"] typeCsv' = "text/csv; charset=UTF-8; header=present"
extensionCsv :: Extension extensionCsv :: Extension
extensionCsv = fromMaybe "csv" $ listToMaybe [ ext | (ext, mime) <- Map.toList mimeMap, mime == typeCsv ] extensionCsv = fromMaybe "csv" $ listToMaybe [ ext | (ext, mime) <- Map.toList mimeMap, mime == typeCsv ]

View File

@ -645,12 +645,17 @@ zipFileField doUnpack permittedExtensions = Field{..}
| otherwise = return . Left $ SomeMessage MsgOnlyUploadOneFile | otherwise = return . Left $ SomeMessage MsgOnlyUploadOneFile
fieldView fieldId fieldName attrs _ req = $(widgetFile "widgets/zipFileField") fieldView fieldId fieldName attrs _ req = $(widgetFile "widgets/zipFileField")
zipExtensions = mimeExtensions "application/zip" zipExtensions = mimeExtensions typeZip
acceptRestricted = isJust permittedExtensions acceptRestricted = isJust permittedExtensions
accept = Text.intercalate "," . map ("." <>) $ bool [] (Set.toList zipExtensions) doUnpack ++ toListOf (_Just . re _nullable . folded) permittedExtensions accept = Text.intercalate "," . map ("." <>) $ bool [] (Set.toList zipExtensions) doUnpack ++ toListOf (_Just . re _nullable . folded) permittedExtensions
multiFileField :: Handler (Set FileId) -> Field Handler (Source Handler (Either FileId File)) multiFileField' :: Source Handler (Either FileId File) -- ^ Permitted files in same format as produced by `multiFileField`
-> Field Handler (Source Handler (Either FileId File))
multiFileField' permittedFiles = multiFileField . runConduit $ permittedFiles .| C.mapMaybe (preview _Left) .| C.foldMap Set.singleton
multiFileField :: Handler (Set FileId) -- ^ Set of files that may be submitted by id-reference
-> Field Handler (Source Handler (Either FileId File))
multiFileField permittedFiles' = Field{..} multiFileField permittedFiles' = Field{..}
where where
fieldEnctype = Multipart fieldEnctype = Multipart

View File

@ -279,8 +279,9 @@ submissionMultiArchive (Set.toList -> ids) = do
execWriter . forM ratedSubmissions $ \(_rating,_submission,(shn,csh,ssh,tid)) -> execWriter . forM ratedSubmissions $ \(_rating,_submission,(shn,csh,ssh,tid)) ->
tell (Set.singleton shn, Set.singleton csh, Set.singleton ssh, Set.singleton tid) tell (Set.singleton shn, Set.singleton csh, Set.singleton ssh, Set.singleton tid)
setContentDisposition' $ Just "submissions.zip" archiveName <- ap getMessageRender $ pure MsgSubmissionArchiveName
(<* cleanup) . respondSource "application/zip" . transPipe (runDBRunner dbrunner) $ do setContentDisposition' $ Just ((addExtension `on` unpack) archiveName extensionZip)
(<* cleanup) . respondSource typeZip . transPipe (runDBRunner dbrunner) $ do
let let
fileEntitySource' :: (Rating, Entity Submission, (SheetName,CourseShorthand,SchoolId,TermId)) -> Source (YesodDB UniWorX) File fileEntitySource' :: (Rating, Entity Submission, (SheetName,CourseShorthand,SchoolId,TermId)) -> Source (YesodDB UniWorX) File
fileEntitySource' (rating, Entity submissionID Submission{..},(shn,csh,ssh,tid)) = do fileEntitySource' (rating, Entity submissionID Submission{..},(shn,csh,ssh,tid)) = do

View File

@ -2,7 +2,8 @@
{-# OPTIONS_GHC -fno-warn-orphans #-} {-# OPTIONS_GHC -fno-warn-orphans #-}
module Handler.Utils.Zip module Handler.Utils.Zip
( ZipError(..) ( typeZip, extensionZip
, ZipError(..)
, ZipInfo(..) , ZipInfo(..)
, produceZip , produceZip
, consumeZip , consumeZip
@ -27,6 +28,16 @@ import Data.Time.LocalTime (localTimeToUTC, utcToLocalTime)
import Data.List (dropWhileEnd) import Data.List (dropWhileEnd)
import qualified Data.Map as Map
typeZip :: ContentType
typeZip = "application/zip"
extensionZip :: Extension
extensionZip = fromMaybe "zip" $ listToMaybe [ ext | (ext, mime) <- Map.toList mimeMap, mime == typeZip ]
instance Default ZipInfo where instance Default ZipInfo where
def = ZipInfo def = ZipInfo
@ -95,7 +106,7 @@ modifyFileTitle f = mapC $ \x@File{..} -> x{ fileTitle = f fileTitle }
-- Takes FileInfo and if it is a ZIP-Archive, extract files, otherwiese yield fileinfo -- Takes FileInfo and if it is a ZIP-Archive, extract files, otherwiese yield fileinfo
sourceFiles :: (MonadLogger m, MonadResource m, MonadThrow m, MonadIO m) => FileInfo -> Source m File sourceFiles :: (MonadLogger m, MonadResource m, MonadThrow m, MonadIO m) => FileInfo -> Source m File
sourceFiles fInfo sourceFiles fInfo
| mimeType == "application/zip" = do | ((==) `on` simpleContentType) mimeType typeZip = do
$logInfoS "sourceFiles" "Unpacking ZIP" $logInfoS "sourceFiles" "Unpacking ZIP"
fileSource fInfo =$= void consumeZip fileSource fInfo =$= void consumeZip
| otherwise = do | otherwise = do

View File

@ -34,8 +34,11 @@ import Text.Shakespeare.Text (st)
import Control.Monad.Trans.Reader (mapReaderT) import Control.Monad.Trans.Reader (mapReaderT)
import Control.Monad.Except (MonadError(..)) import Control.Monad.Except (MonadError(..))
import Utils (exceptT, allM, whenIsJust, guardM) import Utils (exceptT, allM, whenIsJust, guardM)
import Utils.Lens (_NoUpload)
import Utils.DB (getKeyBy) import Utils.DB (getKeyBy)
import Control.Lens
import Numeric.Natural import Numeric.Natural
import qualified Net.IP as IP import qualified Net.IP as IP
@ -398,6 +401,50 @@ customMigrations = Map.fromListWith (>>)
updateTransactionInfo _ = return () updateTransactionInfo _ = return ()
runConduit $ getLogEntries .| C.mapM_ updateTransactionInfo runConduit $ getLogEntries .| C.mapM_ updateTransactionInfo
) )
, ( AppliedMigrationKey [migrationVersion|16.0.0|] [version|17.0.0|]
, do
whenM (tableExists "allocation_course") $ do
vals <- [sqlQQ| SELECT "course", "instructions", "application_text", "application_files", "ratings_visible" FROM "allocation_course"; |]
whenM (tableExists "course") $ do
[executeQQ|
ALTER TABLE "course" ADD COLUMN "applications_required" boolean not null default #{False}, ADD COLUMN "applications_instructions" varchar null, ADD COLUMN "applications_text" boolean not null default #{False}, ADD COLUMN "applications_files" jsonb not null default #{NoUpload}, ADD COLUMN "applications_ratings_visible" boolean not null default #{False};
ALTER TABLE "course" ALTER COLUMN "applications_required" DROP DEFAULT, ALTER COLUMN "applications_text" DROP DEFAULT, ALTER COLUMN "applications_files" DROP DEFAULT, ALTER COLUMN "applications_ratings_visible" DROP DEFAULT;
|]
forM_ vals $ \(cid :: CourseId, Single applicationsInstructions :: Single (Maybe Html), Single applicationsText :: Single Bool, Single applicationsFiles :: Single UploadMode, Single applicationsRatingsVisible :: Single Bool) -> do
let appRequired = applicationsText || isn't _NoUpload applicationsFiles
[executeQQ|
UPDATE "course" SET ("applications_required", "applications_instructions", "applications_text", "applications_files", "applications_ratings_visible") = (#{appRequired}, #{applicationsInstructions}, #{applicationsText}, #{applicationsFiles}, #{applicationsRatingsVisible}) WHERE "id" = #{cid};
|]
[executeQQ|
ALTER TABLE "allocation_course" DROP COLUMN "instructions", DROP COLUMN "application_text", DROP COLUMN "application_files", DROP COLUMN "ratings_visible";
|]
whenM ((&&) <$> tableExists "allocation_course_file" <*> (not <$> tableExists "course_app_instruction_file")) $ do
[executeQQ|
CREATe TABLE "course_app_instruction_file"("id" SERIAL8 PRIMARY KEY UNIQUE,"course" INT8 NOT NULL,"file" INT8 NOT NULL);
ALTER TABLE "course_app_instruction_file" ADD CONSTRAINT "unique_course_app_instruction_file" UNIQUE("course","file");
ALTER TABLE "course_app_instruction_file" ADD CONSTRAINT "course_app_instruction_file_course_fkey" FOREIGN KEY("course") REFERENCES "course"("id");
ALTER TABLE "course_app_instruction_file" ADD CONSTRAINT "course_app_instruction_file_file_fkey" FOREIGN KEY("file") REFERENCES "file"("id");
|]
let getFileEntries = rawQuery [st|SELECT "allocation_course_file"."id", "allocation_course"."course", "allocation_course_file"."file" FROM "allocation_course_file" INNER JOIN "allocation_course" ON "allocation_course"."id" = "allocation_course_file"."allocation_course"|] []
moveFileEntry [fromPersistValue -> Right (acfId :: Int64), fromPersistValue -> Right (cid :: CourseId), fromPersistValue -> Right (fid :: FileId)] =
[executeQQ|
INSERT INTO "course_app_instruction_file" ("course", "file") VALUES (#{cid}, #{fid});
DELETE FROM "allocation_course_file" WHERE "id" = #{acfId};
|]
moveFileEntry _ = return ()
runConduit $ getFileEntries .| C.mapM_ moveFileEntry
tableDropEmpty "allocation_course_file"
whenM (tableExists "allocation_application") $
tableDropEmpty "allocation_application"
whenM (tableExists "allocation_application_file") $
tableDropEmpty "allocation_application_file"
)
] ]

View File

@ -54,6 +54,7 @@ data Icon
| IconSFTSolution -- for SheetFileType only | IconSFTSolution -- for SheetFileType only
| IconSFTMarking -- for SheetFileType only | IconSFTMarking -- for SheetFileType only
| IconEmail | IconEmail
| IconRegisterTemplate
deriving (Eq, Ord, Enum, Bounded, Show, Read) deriving (Eq, Ord, Enum, Bounded, Show, Read)
iconText :: Icon -> Text iconText :: Icon -> Text
@ -82,6 +83,7 @@ iconText = \case
IconSFTSolution -> "exclamation-circle" -- for SheetFileType only IconSFTSolution -> "exclamation-circle" -- for SheetFileType only
IconSFTMarking -> "check-circle" -- for SheetFileType only IconSFTMarking -> "check-circle" -- for SheetFileType only
IconEmail -> "envelope" IconEmail -> "envelope"
IconRegisterTemplate -> "file-alt"
instance Universe Icon instance Universe Icon
instance Finite Icon instance Finite Icon

View File

@ -120,6 +120,7 @@ makePrisms ''HandlerContents
makePrisms ''ErrorResponse makePrisms ''ErrorResponse
makePrisms ''UploadMode
makeLenses_ ''UploadMode makeLenses_ ''UploadMode
makeLenses_ ''SubmissionMode makeLenses_ ''SubmissionMode

View File

@ -79,8 +79,26 @@ $# $if NTop (Just 0) < NTop (courseCapacity course)
<div> <div>
\ <em>Achtung:</em> \ <em>Achtung:</em>
\ Abmeldung nur bis #{dereg} erlaubt. \ Abmeldung nur bis #{dereg} erlaubt.
$maybe aInst <- courseApplicationsInstructions course
<dt .deflist__dt>
$if courseApplicationsRequired course
_{MsgCourseApplicationInstructionsApplication}
$else
_{MsgCourseApplicationInstructionsRegistration}
<dd .deflist__dd>
<div>
#{aInst}
$if hasApplicationTemplate
<p>
<a href=@{CourseR tid ssh csh CRegisterTemplateR}>
#{iconRegisterTemplate} #
$if courseApplicationsRequired course
_{MsgCourseApplicationTemplateApplication}
$else
_{MsgCourseApplicationTemplateRegistration}
$if registrationOpen || isJust mRegAt $if registrationOpen || isJust mRegAt
<dt .deflist__dt> <dt .deflist__dt>
_{MsgCourseRegistration}
<dd .deflist__dd> <dd .deflist__dd>
<div .course__registration> <div .course__registration>
$if registrationOpen $if registrationOpen

View File

@ -420,6 +420,11 @@ fillDb = do
, courseDeregisterUntil = Nothing , courseDeregisterUntil = Nothing
, courseRegisterSecret = Nothing , courseRegisterSecret = Nothing
, courseMaterialFree = True , courseMaterialFree = True
, courseApplicationsRequired = False
, courseApplicationsInstructions = Nothing
, courseApplicationsText = False
, courseApplicationsFiles = NoUpload
, courseApplicationsRatingsVisible = False
} }
insert_ $ CourseEdit jost now ffp insert_ $ CourseEdit jost now ffp
void . insert $ DegreeCourse ffp sdBsc sdInf void . insert $ DegreeCourse ffp sdBsc sdInf
@ -452,6 +457,11 @@ fillDb = do
, courseDeregisterUntil = Nothing , courseDeregisterUntil = Nothing
, courseRegisterSecret = Nothing , courseRegisterSecret = Nothing
, courseMaterialFree = True , courseMaterialFree = True
, courseApplicationsRequired = False
, courseApplicationsInstructions = Nothing
, courseApplicationsText = False
, courseApplicationsFiles = NoUpload
, courseApplicationsRatingsVisible = False
} }
insert_ $ CourseEdit fhamann now eip insert_ $ CourseEdit fhamann now eip
void . insert' $ DegreeCourse eip sdBsc sdInf void . insert' $ DegreeCourse eip sdBsc sdInf
@ -470,6 +480,11 @@ fillDb = do
, courseDeregisterUntil = Nothing , courseDeregisterUntil = Nothing
, courseRegisterSecret = Nothing , courseRegisterSecret = Nothing
, courseMaterialFree = True , courseMaterialFree = True
, courseApplicationsRequired = False
, courseApplicationsInstructions = Nothing
, courseApplicationsText = False
, courseApplicationsFiles = NoUpload
, courseApplicationsRatingsVisible = False
} }
insert_ $ CourseEdit fhamann now ixd insert_ $ CourseEdit fhamann now ixd
void . insert' $ DegreeCourse ixd sdBsc sdInf void . insert' $ DegreeCourse ixd sdBsc sdInf
@ -488,6 +503,11 @@ fillDb = do
, courseDeregisterUntil = Nothing , courseDeregisterUntil = Nothing
, courseRegisterSecret = Nothing , courseRegisterSecret = Nothing
, courseMaterialFree = True , courseMaterialFree = True
, courseApplicationsRequired = False
, courseApplicationsInstructions = Nothing
, courseApplicationsText = False
, courseApplicationsFiles = NoUpload
, courseApplicationsRatingsVisible = False
} }
insert_ $ CourseEdit fhamann now ux3 insert_ $ CourseEdit fhamann now ux3
void . insert' $ DegreeCourse ux3 sdBsc sdInf void . insert' $ DegreeCourse ux3 sdBsc sdInf
@ -506,6 +526,11 @@ fillDb = do
, courseDeregisterUntil = Nothing , courseDeregisterUntil = Nothing
, courseRegisterSecret = Nothing , courseRegisterSecret = Nothing
, courseMaterialFree = True , courseMaterialFree = True
, courseApplicationsRequired = False
, courseApplicationsInstructions = Nothing
, courseApplicationsText = False
, courseApplicationsFiles = NoUpload
, courseApplicationsRatingsVisible = False
} }
insert_ $ CourseEdit jost now pmo insert_ $ CourseEdit jost now pmo
void . insert $ DegreeCourse pmo sdBsc sdInf void . insert $ DegreeCourse pmo sdBsc sdInf
@ -662,6 +687,11 @@ fillDb = do
, courseDeregisterUntil = Nothing , courseDeregisterUntil = Nothing
, courseRegisterSecret = Just "dbs" , courseRegisterSecret = Just "dbs"
, courseMaterialFree = False , courseMaterialFree = False
, courseApplicationsRequired = False
, courseApplicationsInstructions = Nothing
, courseApplicationsText = False
, courseApplicationsFiles = NoUpload
, courseApplicationsRatingsVisible = False
} }
insert_ $ CourseEdit gkleen now dbs insert_ $ CourseEdit gkleen now dbs
void . insert' $ DegreeCourse dbs sdBsc sdInf void . insert' $ DegreeCourse dbs sdBsc sdInf