diff --git a/messages/uniworx/de-de-formal.msg b/messages/uniworx/de-de-formal.msg
index da7f03b5e..48ac52d8f 100644
--- a/messages/uniworx/de-de-formal.msg
+++ b/messages/uniworx/de-de-formal.msg
@@ -401,9 +401,9 @@ UnauthorizedToken404: Authorisierungs-Tokens können nicht auf Fehlerseiten ausg
UnauthorizedSiteAdmin: Sie sind kein System-weiter Administrator.
UnauthorizedSchoolAdmin: Sie sind nicht als Administrator für dieses Institut eingetragen.
UnauthorizedAdminEscalation: Sie sind nicht Administrator für alle Institute, für die dieser Nutzer Administrator oder Veranstalter ist.
-UnauthorizedExamOffice: Sie sind nicht Teil eines Prüfungsamts.
-UnauthorizedExamExamOffice: Es existieren keine Prüfungsergebnisse für Nutzer, für die Sie Teil eines assoziierten Prüfungsamts sind.
-UnauthorizedExternalExamExamOffice: Es existieren keine Prüfungsergebnisse für Nutzer, für die Sie Teil eines assoziierten Prüfungsamts sind.
+UnauthorizedExamOffice: Sie sind nicht mit Prüfungsverwaltung beauftragt.
+UnauthorizedExamExamOffice: Es existieren keine Prüfungsergebnisse für Nutzer, für die Sie mit der Prüfungsverwaltung beauftragt sind.
+UnauthorizedExternalExamExamOffice: Es existieren keine Prüfungsergebnisse für Nutzer, für die Sie mit der Prüfungsverwaltung beauftragt sind.
UnauthorizedSchoolLecturer: Sie sind nicht als Veranstalter für dieses Institut eingetragen.
UnauthorizedLecturer: Sie sind nicht als Veranstalter für diese Veranstaltung eingetragen.
UnauthorizedAllocationLecturer: Sie sind nicht als Veranstalter für eine Veranstaltung dieser Zentralanmeldung eingetragen.
@@ -1000,7 +1000,7 @@ NotificationTriggerKindCorrector: Für Korrektoren
NotificationTriggerKindLecturer: Für Dozenten
NotificationTriggerKindCourseLecturer: Für Kursverwalter
NotificationTriggerKindAdmin: Für Administratoren
-NotificationTriggerKindExamOffice: Für das Prüfungsamt
+NotificationTriggerKindExamOffice: Für Prüfungsverwalter
NotificationTriggerKindEvaluation: Für Vorlesungsumfragen
NotificationTriggerKindAllocationStaff: Für Zentralanmeldungen (Dozenten)
NotificationTriggerKindAllocationParticipant: Für Zentralanmeldungen
@@ -1127,7 +1127,7 @@ MenuCourseMembers: Kursteilnehmer
MenuCourseAddMembers: Kursteilnehmer hinzufügen
MenuCourseCommunication: Kursmitteilung (E-Mail)
MenuCourseApplications: Bewerbungen
-MenuCourseExamOffice: Prüfungsämter
+MenuCourseExamOffice: Prüfungsbeauftragte
MenuTermShow: Semester
MenuSubmissionDelete: Abgabe löschen
MenuUsers: Benutzer
@@ -1267,7 +1267,7 @@ AuthPredsActive: Aktive Authorisierungsprädikate
AuthPredsActiveChanged: Authorisierungseinstellungen für aktuelle Sitzung gespeichert
AuthTagFree: Seite ist universell zugänglich
AuthTagAdmin: Nutzer ist Administrator
-AuthTagExamOffice: Nutzer ist Teil eines Prüfungsamts
+AuthTagExamOffice: Nutzer ist mit Prüfungsverwaltung beauftragt
AuthTagToken: Nutzer präsentiert Authorisierungs-Token
AuthTagNoEscalation: Nutzer-Rechte werden nicht auf fremde Institute ausgeweitet
AuthTagDeprecated: Seite ist nicht überholt
@@ -1522,9 +1522,9 @@ ExamFinishedOffice: Noten bekannt gegeben
ExamFinishedParticipant: Bewertung vorrausichtlich abgeschlossen
ExamFinishedTip: Zeitpunkt zu dem Prüfungergebnisse den Teilnehmern gemeldet werden
ExamClosed: Noten gemeldet
-ExamClosedTip: Prüfungsämter, die im System Noten einsehen, werden zu diesem Zeitpunkt benachrichtigt und danach bei Änderungen informiert
+ExamClosedTip: Prüfungsbeauftraget, die im System Noten einsehen, werden zu diesem Zeitpunkt benachrichtigt und danach bei Änderungen informiert
ExamShowGrades: Klausur ist benotet
-ExamShowGradesTip: Sollen genaue Noten angezeigt werden, oder sollen Teilnehmer und Prüfungsämter nur informiert werden, ob die Klausur bestanden wurde?
+ExamShowGradesTip: Sollen genaue Noten angezeigt werden, oder sollen Teilnehmer und Prüfungsbeauftragte nur informiert werden, ob die Klausur bestanden wurde?
ExamPublicStatistics: Statistik veröffentlichen
ExamPublicStatisticsTip: Soll die automatisch berechnete statistische Auswertung auch den Teilnehmern angezeigt werden, sobald diese ihre Noten einsehen können?
ExamAutomaticGrading: Automatische Notenberechnung
@@ -1938,7 +1938,7 @@ SchoolExists ssh@SchoolId: Institut „#{ssh}“ existiert bereits
SchoolAdmin: Admin
SchoolLecturer: Dozent
SchoolEvaluation: Kursumfragenverwaltung
-SchoolExamOffice: Prüfungsamt
+SchoolExamOffice: Prüfungsverwaltung
ApplicationEditTip: Während des Bewerbungszeitraums können eigene Bewerbungen beliebig angepasst und auch wieder zurückgezogen werden.
@@ -2016,10 +2016,10 @@ MailSubjectChangeUserDisplayEmail: Diese E-Mail Adresse in Uni2work veröffentli
MailIntroChangeUserDisplayEmail displayEmail@UserEmail: Der oben genannte Benutzer möchte „#{displayEmail}“ als öffentliche Adresse, assoziiert mit sich selbst, angeben. Wenn Sie diese Aktion nicht selbst ausgelöst haben, ignorieren Sie diese Mitteilung bitte!
MailTitleChangeUserDisplayEmail displayName@Text: #{displayName} möchte diese E-Mail Adresse in Uni2work veröffentlichen
-ExamOfficeOptOutsChanged: Zuständige Prüfungsämter erfolgreich angepasst
+ExamOfficeOptOutsChanged: Zuständige Prüfungsbeauftragte erfolgreich angepasst
BtnCloseExam: Klausur abschließen
-ExamCloseTip: Wenn eine Klausur abgeschlossen wird, werden Prüfungsämter, die im System Noten einsehen, benachrichtigt und danach bei Änderungen informiert.
+ExamCloseTip: Wenn eine Klausur abgeschlossen wird, werden Prüfungsbeauftragte, die im System Noten einsehen, benachrichtigt und danach bei Änderungen informiert.
ExamCloseReminder: Bitte schließen Sie die Klausur frühstmöglich, sobald die Prüfungsleistungen sich voraussichtlich nicht mehr ändern werden. Z.B. direkt nach der Klausureinsicht.
ExamDidClose: Klausur erfolgreich abgeschlossen
@@ -2220,3 +2220,26 @@ Deficit: Defizit
MetricNoSamples: Keine Messwerte
MetricName: Name
MetricValue: Wert
+
+ExternalExamSemester: Semester
+ExternalExamSchool: Institut
+ExternalExamCourseName: Veranstaltung
+ExternalExamCourseNameTip: Muss nur innerhalb von Semester und Institut eindeutig sein.
+ExternalExamCourseNamePlaceholder: Analysis I, Programmierung und Modellierung, ...
+ExternalExamExamName: Prüfung
+ExternalExamExamNameTip: Muss innerhalb der Veranstaltung eindeutig sein.
+ExternalExamExamNamePlaceholder: Klausur, Nachklausur, Projektabnahme, ...
+ExternalExamDefaultTime: Voreingestellter Zeitpunkt
+ExternalExamDefaultTimePlaceholder: Zeitpunkt
+ExternalExamDefaultTimeTip: Der Zeitpunkt zu dem die Prüfung abgelegt wurde, muss pro Teilnehmer festgelegt werden. Der hier angegebene Zeitpunkt wird als Standardwert für Teilnehmer verwendet, bei denen später nicht ein abweichender Zeitpunkt angegeben wird.
+ExternalExamShowGrades: Klausur ist benotet
+ExternalExamShowGradesTip: Sollen genaue Noten angezeigt werden, oder sollen Teilnehmer und Prüfungsbeauftragte nur informiert werden, ob die Klausur bestanden wurde?
+ExternalExamExamOfficeSchools: Zusätzliche Institute
+ExternalExamExamOfficeSchoolsTip: Prüfungsbeauftragte von Instituten, die Sie hier angeben, erhalten im System (zusätzlich zum angegebenen primären Institut) volle Einsicht in sämtliche für diese Prüfung hinterlegten Leistungen, unabhängig von den Studiendaten der Teilnehmer.
+ExternalExamStaff: Assoziierte Personen
+ExternalExamStaffTip: Assoziierte Personen werden den Prüfungsbeauftragten und Teilnehmern angezeigt und dürfen Leistungen für die Prüfung hinterlegen.
+ExternalExamStaffAlreadyAdded: Person wurde bereits der Prüfung hinzugefügt
+ExternalExamUserMustBeStaff: Sie selbst müssen stets assoziierte Person sein, für die externen Prüfungen, die Sie anlegen
+ExternalExamCourseExists: Der angegebene Kurs existiert im System. Prüfungen sollten daher direkt beim Kurs (statt extern) hinterlegt werden.
+ExternalExamExists coursen@CourseName examn@ExamName: Prüfung „#{examn}“ für Kurs „#{coursen}“ existiert bereits.
+ExternalExamCreated coursen@CourseName examn@ExamName: Prüfung „#{examn}“ für Kurs „#{coursen}“ erfolgreich angelegt.
\ No newline at end of file
diff --git a/messages/uniworx/en-eu.msg b/messages/uniworx/en-eu.msg
index bbc7d1fa4..7dc50cac4 100644
--- a/messages/uniworx/en-eu.msg
+++ b/messages/uniworx/en-eu.msg
@@ -401,6 +401,7 @@ UnauthorizedSchoolAdmin: You are no administrator for this department.
UnauthorizedAdminEscalation: You aren't an administrator for all departments for which this user is an administrator.
UnauthorizedExamOffice: You are not part of an exam office.
UnauthorizedExamExamOffice: You are not part of the appropriate exam office for any of the participants of this exam.
+UnauthorizedExternalExamExamOffice: You are not part of the appropriate exam office for any of the participants of this exam.
UnauthorizedSchoolLecturer: You are no lecturer for this department.
UnauthorizedLecturer: You are no administrator for this course.
UnauthorizedAllocationLecturer: You are no administrator for any of the courses of this central allocation.
@@ -449,6 +450,7 @@ UnauthorizedTutorialRegisterGroup: You are already registered for a tutorial wit
UnauthorizedLDAP: Specified user does not log in with their campus account.
UnauthorizedPWHash: Specified user does not log in with an Uni2work-account.
UnauthorizedExternalExamListNotEmpty: List of external exams is not empty
+UnauthorizedExternalExamLecturer: You are not an associated person for this external exam
UnauthorizedPasswordResetToken: This authorisation-token may no longer be used to change passwords
@@ -2217,3 +2219,26 @@ Deficit: Deficit
MetricNoSamples: No samples
MetricName: Name
MetricValue: Value
+
+ExternalExamSemester: Semester
+ExternalExamSchool: Department
+ExternalExamCourseName: Course
+ExternalExamCourseNameTip: Needs only be unique among within semester and department.
+ExternalExamCourseNamePlaceholder: Analysis I, Programming and Modelling, ...
+ExternalExamExamName: Exam title
+ExternalExamExamNameTip: Needs only be unique within the course.
+ExternalExamExamNamePlaceholder: Exam, Exam resit, Project discussion, ...
+ExternalExamDefaultTime: Default time
+ExternalExamDefaultTimePlaceholder: Time
+ExternalExamDefaultTimeTip: The time of the exam needs to be specified for each participant. The time entered here is used as a default value for participants for whom no different time is later specified.
+ExternalExamShowGrades: Exam is graded
+ExternalExamShowGradesTip: Should participants and relevant exam offices be show exact grades or only whether the exam was passed or failed?
+ExternalExamExamOfficeSchools: Additional departments
+ExternalExamExamOfficeSchoolsTip: Exam offices of departments you specify here will also have full access to all results for this exam disregarding the individual participants' features of study.
+ExternalExamStaff: Associated persons
+ExternalExamStaffTip: The list of ssociated persons is shown to exam offices and participants. Additionally associated persons may upload results for the exam.
+ExternalExamStaffAlreadyAdded: Person is already associated with the exam.
+ExternalExamUserMustBeStaff: You yourself must always be an associated person for exams you create.
+ExternalExamCourseExists: This course already exists with uni2work. Exams for courses that exist within uni2work should be associated with the course directly instead of being created as an external exam.
+ExternalExamExists coursen@CourseName examn@ExamName: Exam “#{examn}” already exists for course “#{coursen}”.
+ExternalExamCreated coursen@CourseName examn@ExamName: Succesfully created exam “#{examn}” for course “#{coursen}”.
diff --git a/src/Handler/ExternalExam/Edit.hs b/src/Handler/ExternalExam/Edit.hs
index 66961d477..d89987ab9 100644
--- a/src/Handler/ExternalExam/Edit.hs
+++ b/src/Handler/ExternalExam/Edit.hs
@@ -4,6 +4,8 @@ module Handler.ExternalExam.Edit
import Import
+import Handler.ExternalExam.Form ()
+
getEEEditR, postEEEditR :: TermId -> SchoolId -> CourseName -> ExamName -> Handler Html
getEEEditR = postEEEditR
diff --git a/src/Handler/ExternalExam/Form.hs b/src/Handler/ExternalExam/Form.hs
new file mode 100644
index 000000000..a8c5d5c18
--- /dev/null
+++ b/src/Handler/ExternalExam/Form.hs
@@ -0,0 +1,115 @@
+module Handler.ExternalExam.Form
+ ( ExternalExamForm(..)
+ , externalExamForm
+ ) where
+
+import Import
+import Handler.Utils
+
+import Handler.ExternalExam.StaffInvite ()
+
+import qualified Data.Set as Set
+import Data.Map ((!))
+
+import qualified Control.Monad.State.Class as State
+
+
+data ExternalExamForm = ExternalExamForm
+ { eefTerm :: TermId
+ , eefSchool :: SchoolId
+ , eefCourseName :: CI Text
+ , eefExamName :: CI Text
+ , eefDefaultTime :: Maybe UTCTime
+ , eefShowGrades :: Bool
+ , eefOfficeSchools :: Set SchoolId
+ , eefStaff :: Set (Either UserEmail UserId)
+ }
+
+makeLenses_ ''ExternalExamForm
+
+externalExamForm :: Maybe ExternalExamForm -> Form ExternalExamForm
+externalExamForm template = validateForm validateExternalExam $ \html -> do
+ uid <- requireAuthId
+ cRoute <- fromMaybe (error "tutorialForm called from 404-Handler") <$> getCurrentRoute
+ MsgRenderer mr <- getMsgRenderer
+
+ let termsField = case template of
+ Just template' -> termsSetField [eefTerm template']
+ _other -> termsAllowedField
+
+ (lecturerSchools, adminSchools, oldSchool) <- liftHandler . runDB $ do
+ lecturerSchools <- map (userFunctionSchool . entityVal) <$> selectList [UserFunctionUser ==. uid, UserFunctionFunction <-. [SchoolLecturer]] []
+ protoAdminSchools <- map (userFunctionSchool . entityVal) <$> selectList [UserFunctionUser ==. uid, UserFunctionFunction <-. [SchoolAdmin]] []
+ adminSchools <- filterM (hasWriteAccessTo . flip SchoolR SchoolEditR) protoAdminSchools
+ let oldSchool = eefSchool <$> template
+ return (lecturerSchools, adminSchools, oldSchool)
+ let userSchools = nub . maybe id (:) oldSchool $ lecturerSchools ++ adminSchools
+
+ flip (renderAForm FormStandard) html $ ExternalExamForm
+ <$> areq termsField (fslI MsgExternalExamSemester) (eefTerm <$> template)
+ <*> areq (schoolFieldFor userSchools) (fslI MsgExternalExamSchool) (eefSchool <$> template)
+ <*> areq (textField & cfStrip & cfCI) (fslI MsgExternalExamCourseName & setTooltip MsgExternalExamCourseNameTip & addPlaceholder (mr MsgExternalExamCourseNamePlaceholder)) (eefCourseName <$> template)
+ <*> 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 checkBoxField (fslI MsgExternalExamShowGrades & setTooltip MsgExternalExamShowGradesTip) (eefShowGrades <$> template)
+ <*> (Set.fromList <$> officeSchoolForm cRoute (Set.toList . eefOfficeSchools <$> template))
+ <*> (Set.fromList <$> staffForm cRoute ((Set.toList . eefStaff <$> template) <|> pure (pure $ Right uid)))
+ where
+ officeSchoolForm cRoute = massInputAccumA miAdd miCell miButtonAction miLayout miIdent fSettings fRequired
+ where
+ miAdd mkUnique submitView csrf = do
+ (schoolRes, addView) <- mpopt schoolField ("" & addName (mkUnique "school")) Nothing
+ let schoolRes' = schoolRes <&> \newDat oldDat -> FormSuccess (guardOn (newDat `notElem` oldDat) newDat)
+ return (schoolRes', $(widgetFile "external-exam/schoolMassInput/add"))
+ miCell ssh = do
+ School{..} <- liftHandler . runDB $ getJust ssh
+ $(widgetFile "external-exam/schoolMassInput/cell")
+ miButtonAction :: forall p. PathPiece p => p -> Maybe (SomeRoute UniWorX)
+ miButtonAction = Just . SomeRoute . (cRoute :#:)
+ miLayout :: MassInputLayout ListLength SchoolId ()
+ miLayout lLength _ cellWdgts delButtons addWdgts = $(widgetFile "external-exam/schoolMassInput/layout")
+ miIdent :: Text
+ miIdent = "external-exams-school"
+ fSettings = fslI MsgExternalExamExamOfficeSchools & setTooltip (UniWorXMessages [SomeMessage MsgExternalExamExamOfficeSchoolsTip, SomeMessage MsgMassInputTip])
+ fRequired = False
+ staffForm cRoute = massInputAccumA miAdd miCell miButtonAction miLayout miIdent fSettings fRequired
+ where
+ miAdd mkUnique submitView csrf = do
+ MsgRenderer mr <- getMsgRenderer
+ (usersRes, addView) <- mpreq (multiUserField False Nothing) ("" & addName (mkUnique "email")) Nothing
+ let
+ usersRes' = usersRes <&> \newDat oldDat -> if
+ | existing <- newDat `Set.intersection` Set.fromList oldDat
+ , not $ Set.null existing
+ -> FormFailure [mr MsgExternalExamStaffAlreadyAdded]
+ | otherwise
+ -> FormSuccess $ Set.toList newDat
+ return (usersRes', $(widgetFile "external-exam/staffMassInput/add"))
+ miCell (Left email) = do
+ invWarnMsg <- messageI Warning MsgEmailInvitationWarning
+ $(widgetFile "external-exam/staffMassInput/cellInvitation")
+ miCell (Right userId) = do
+ User{..} <- liftHandler . runDB $ getJust userId
+ $(widgetFile "external-exam/staffMassInput/cellKnown")
+ miButtonAction :: forall p. PathPiece p => p -> Maybe (SomeRoute UniWorX)
+ miButtonAction = Just . SomeRoute . (cRoute :#:)
+ miLayout :: MassInputLayout ListLength (Either UserEmail UserId) ()
+ miLayout lLength _ cellWdgts delButtons addWdgts = $(widgetFile "external-exam/staffMassInput/layout")
+ miIdent :: Text
+ miIdent = "external-exams-staff"
+ fSettings = fslI MsgExternalExamStaff & setTooltip (UniWorXMessages [SomeMessage MsgExternalExamStaffTip, SomeMessage MsgMassInputTip])
+ fRequired = True
+
+validateExternalExam :: (MonadThrow m, MonadHandler m, HandlerSite m ~ UniWorX) => FormValidator ExternalExamForm m ()
+validateExternalExam = do
+ State.modify $ \eeForm -> eeForm & over _eefOfficeSchools (Set.delete $ eeForm ^. _eefSchool)
+
+ ExternalExamForm{..} <- State.get
+
+ isAdmin <- hasWriteAccessTo $ SchoolR eefSchool SchoolEditR
+ unless isAdmin $ do
+ uid <- requireAuthId
+ guardValidation MsgExternalExamUserMustBeStaff $ Right uid `Set.member` eefStaff
+
+ courseExists <- liftHandler . runDB . existsBy $ TermSchoolCourseName eefTerm eefSchool eefCourseName
+ guardValidation MsgExternalExamCourseExists $ not courseExists
diff --git a/src/Handler/ExternalExam/New.hs b/src/Handler/ExternalExam/New.hs
index 1ccd89731..b8403b674 100644
--- a/src/Handler/ExternalExam/New.hs
+++ b/src/Handler/ExternalExam/New.hs
@@ -4,7 +4,59 @@ module Handler.ExternalExam.New
import Import
+import Jobs.Queue
+
+import Handler.Utils
+import Handler.Utils.Invitations
+
+import Handler.ExternalExam.StaffInvite
+import Handler.ExternalExam.Form
+
+import qualified Data.Set as Set
+
getEExamNewR, postEExamNewR :: Handler Html
getEExamNewR = postEExamNewR
-postEExamNewR = error "Not implemented"
+postEExamNewR = do
+ ((newExamResult, newExamWidget'), newExamEnctype) <- runFormPost $ externalExamForm Nothing
+
+ formResult newExamResult $ \ExternalExamForm{..} -> do
+ insertRes <- runDBJobs $ do
+ insertRes <- insertUnique ExternalExam
+ { externalExamTerm = eefTerm
+ , externalExamSchool = eefSchool
+ , externalExamCourseName = eefCourseName
+ , externalExamExamName = eefExamName
+ , externalExamDefaultTime = eefDefaultTime
+ , externalExamShowGrades = eefShowGrades
+ }
+ whenIsJust insertRes $ \eeId -> do
+ insertMany_
+ [ ExternalExamOfficeSchool{..}
+ | externalExamOfficeSchoolSchool <- Set.toList eefOfficeSchools
+ , externalExamOfficeSchoolSchool /= eefSchool
+ , let externalExamOfficeSchoolExam = eeId
+ ]
+
+ let (invites, adds) = partitionEithers $ Set.toList eefStaff
+ insertMany_ [ ExternalExamStaff{..}
+ | externalExamStaffUser <- adds
+ , let externalExamStaffExam = eeId
+ ]
+ sinkInvitationsF externalExamStaffInvitationConfig $ map (, eeId, (InvDBDataExternalExamStaff, InvTokenDataExternalExamStaff)) invites
+ return insertRes
+
+ case insertRes of
+ Nothing -> addMessageI Error $ MsgExternalExamExists eefCourseName eefExamName
+ Just _ -> do
+ addMessageI Success $ MsgExternalExamCreated eefCourseName eefExamName
+ redirect $ EExamR eefTerm eefSchool eefCourseName eefExamName EEShowR
+
+ let heading = MsgMenuExternalExamNew
+
+ siteLayoutMsg heading $ do
+ setTitleI heading
+ wrapForm newExamWidget' def
+ { formAction = Just $ SomeRoute EExamNewR
+ , formEncoding = newExamEnctype
+ }
diff --git a/templates/external-exam/schoolMassInput/add.hamlet b/templates/external-exam/schoolMassInput/add.hamlet
new file mode 100644
index 000000000..cf4cc6e72
--- /dev/null
+++ b/templates/external-exam/schoolMassInput/add.hamlet
@@ -0,0 +1,6 @@
+$newline never
+
+ #{csrf}
+ ^{fvInput addView}
+ |
+ ^{fvInput submitView}
diff --git a/templates/external-exam/schoolMassInput/cell.hamlet b/templates/external-exam/schoolMassInput/cell.hamlet
new file mode 100644
index 000000000..13905b209
--- /dev/null
+++ b/templates/external-exam/schoolMassInput/cell.hamlet
@@ -0,0 +1,3 @@
+$newline never
+ |
+ #{schoolName}
diff --git a/templates/external-exam/schoolMassInput/layout.hamlet b/templates/external-exam/schoolMassInput/layout.hamlet
new file mode 100644
index 000000000..65352dd95
--- /dev/null
+++ b/templates/external-exam/schoolMassInput/layout.hamlet
@@ -0,0 +1,11 @@
+$newline never
+
+
+ $forall coord <- review liveCoords lLength
+
+ ^{cellWdgts ! coord}
+ |
+ ^{fvInput (delButtons ! coord)}
+ |
+
+ ^{addWdgts ! (0, 0)}
diff --git a/templates/external-exam/staffMassInput/add.hamlet b/templates/external-exam/staffMassInput/add.hamlet
new file mode 100644
index 000000000..bdf6da247
--- /dev/null
+++ b/templates/external-exam/staffMassInput/add.hamlet
@@ -0,0 +1,6 @@
+$newline never
+|
+ #{csrf}
+ ^{fvInput addView}
+ |
+ ^{fvInput submitView}
diff --git a/templates/external-exam/staffMassInput/cellInvitation.hamlet b/templates/external-exam/staffMassInput/cellInvitation.hamlet
new file mode 100644
index 000000000..df7df418a
--- /dev/null
+++ b/templates/external-exam/staffMassInput/cellInvitation.hamlet
@@ -0,0 +1,6 @@
+$newline never
+ |
+
+ #{email}
+ |
+ ^{messageTooltip invWarnMsg}
diff --git a/templates/external-exam/staffMassInput/cellKnown.hamlet b/templates/external-exam/staffMassInput/cellKnown.hamlet
new file mode 100644
index 000000000..5ea4cca6f
--- /dev/null
+++ b/templates/external-exam/staffMassInput/cellKnown.hamlet
@@ -0,0 +1,3 @@
+$newline never
+ |
+ ^{nameEmailWidget userEmail userDisplayName userSurname}
diff --git a/templates/external-exam/staffMassInput/layout.hamlet b/templates/external-exam/staffMassInput/layout.hamlet
new file mode 100644
index 000000000..65352dd95
--- /dev/null
+++ b/templates/external-exam/staffMassInput/layout.hamlet
@@ -0,0 +1,11 @@
+$newline never
+
+
+ $forall coord <- review liveCoords lLength
+
+ ^{cellWdgts ! coord}
+ |
+ ^{fvInput (delButtons ! coord)}
+ |
+
+ ^{addWdgts ! (0, 0)}
| |