diff --git a/messages/uniworx/de.msg b/messages/uniworx/de.msg
index 2f13a93f9..2a429a9ae 100644
--- a/messages/uniworx/de.msg
+++ b/messages/uniworx/de.msg
@@ -95,6 +95,10 @@ CourseFilterNone: Egal
CourseDeleteQuestion: Wollen Sie den unten aufgeführten Kurs wirklich löschen?
CourseDeleted: Kurs gelöscht
CourseUserNote: Notiz
+CourseUserNoteTooltip: Nur für Dozenten dieses Kurses einsehbar
+CourseUserNoteSaved: Notizänderungen gespeichert
+CourseUserNoteDeleted: Teilnehmernotiz gelöscht
+
NoSuchTerm tid@TermId: Semester #{display tid} gibt es nicht.
NoSuchSchool ssh@SchoolId: Institut #{display ssh} gibt es nicht.
diff --git a/models/courses b/models/courses
index fb9b06462..a731e778d 100644
--- a/models/courses
+++ b/models/courses
@@ -40,12 +40,20 @@ CourseParticipant -- course enrolement
registration UTCTime -- time of last enrolement for this course
field StudyFeaturesId Maybe -- associated degree course, user-defined; required for communicating grades
UniqueParticipant user course
+-- Replace the last two by the following, once an audit log is available
+-- 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
+-- time UTCTime -- PROBLEM: deleted note has no modification date
+-- editor UserId -- who edited this note last
+-- UniqueCourseUserNote user course
CourseUserNote -- lecturers of a specific course may share a text note on each enrolled student
course CourseId
user UserId
note Text -- arbitrary user-defined text; visible only to lecturer of this course
- UniqueCourseUserNotes user course
-CourseUserNoteEdit -- who edited a participants course note whenl
+ UniqueCourseUserNote user course
+CourseUserNoteEdit -- who edited a participants course note when
user UserId
time UTCTime
- note CourseUserNoteId
+ note CourseUserNoteId -- PROBLEM: deleted notes have no modification date any more
diff --git a/routes b/routes
index 381a9486a..4f1ce19bd 100644
--- a/routes
+++ b/routes
@@ -76,7 +76,7 @@
/edit CEditR GET POST
/delete CDeleteR GET POST !lecturerANDempty
/users CUsersR GET
- /users/#CryptoUUIDUser CUserR GET !lecturerANDparticipant
+ /users/#CryptoUUIDUser CUserR GET POST !lecturerANDparticipant
/correctors CHiWisR GET
/notes CNotesR GET POST !corrector
/subs CCorrectionsR GET POST
diff --git a/src/Database/Esqueleto/Utils.hs b/src/Database/Esqueleto/Utils.hs
index 2dab7cf8d..ae1628b45 100644
--- a/src/Database/Esqueleto/Utils.hs
+++ b/src/Database/Esqueleto/Utils.hs
@@ -54,13 +54,20 @@ all :: Foldable f =>
all test = F.foldr (\needle acc -> acc E.&&. test needle) true
-
-- Allow usage of Tuples as DbtRowKey, i.e. SqlIn instances for tuples
$(sqlInTuples [2..16])
+-- | Example for usage of unValueN
+_example_unValueN :: (E.Value a, E.Value b, E.Value c) -> (a,b,c)
+_example_unValueN = $(unValueN 3)
+
+-- | Example for usage of unValueNIs
+_example_unValueNIs :: (E.Value a, b, E.Value c) -> (a,b,c)
+_example_unValueNIs = $(unValueNIs 3 [1,3])
+
-- | Example for usage of sqlIJproj
--- queryFeaturesDegree :: (a `E.InnerJoin` b `E.InnerJoin` c) -> b
--- queryFeaturesDegree = $(sqlIJproj 3 2)
+_queryFeaturesDegree :: (a `E.InnerJoin` b `E.InnerJoin` c) -> b
+_queryFeaturesDegree = $(sqlIJproj 3 2)
-- | generic filter creation for dbTable
diff --git a/src/Database/Esqueleto/Utils/TH.hs b/src/Database/Esqueleto/Utils/TH.hs
index 5596f31ee..cc1f5d7b9 100644
--- a/src/Database/Esqueleto/Utils/TH.hs
+++ b/src/Database/Esqueleto/Utils/TH.hs
@@ -1,6 +1,7 @@
module Database.Esqueleto.Utils.TH
( SqlIn(..)
, sqlInTuple, sqlInTuples
+ , unValueN, unValueNIs
, sqlIJproj, sqlLOJproj
) where
@@ -48,6 +49,30 @@ sqlInTuple arity = do
]
]
+-- | Generic unValuing of Tuples of Values, i.e.
+-- $(unValueN 3) :: (E.Value a, E.Value b, E.Value c) -> (a,b,c)
+unValueN :: Int -> ExpQ
+unValueN arity = do
+ vs <- replicateM arity $ newName "v"
+ let pat = tupP $ map varP vs
+ let uvE v = [e|E.unValue $(varE v)|]
+ let rhs = tupE $ map uvE vs
+ lam1E pat rhs
+
+-- | Generic unValuing of certain indices of a Tuple, i.e.
+-- $(unValueNIs 3 [1,3]) :: (E.Value a, b, E.Value c) -> (a,b,c)
+unValueNIs :: Int -> [Int] -> ExpQ
+unValueNIs arity uvIdx = do
+ vs <- replicateM arity $ newName "v"
+ let pat = tupP $ map varP vs
+ let rhs = tupE $ map uvEi $ zip vs [1..]
+ lam1E pat rhs
+ where
+ uvEi (v,i) | i `elem` uvIdx = [e|E.unValue $(varE v)|]
+ | otherwise = varE v
+
+
+
-- | Generic projections for InnerJoin-tuples
-- gives I-th element of N-tuple of left-associative InnerJoin-pairs,
-- i.e. @$(projN n m) :: (t1 `E.InnerJoin` .. `E.InnerJoin` tn) -> tm@ (for m<=n)
diff --git a/src/Foundation.hs b/src/Foundation.hs
index c776faea6..60fb249f1 100644
--- a/src/Foundation.hs
+++ b/src/Foundation.hs
@@ -1147,7 +1147,7 @@ instance YesodBreadcrumbs UniWorX where
-- (CourseR tid ssh csh CRegisterR) -- is POST only
breadcrumb (CourseR tid ssh csh CEditR) = return ("Editieren" , Just $ CourseR tid ssh csh CShowR)
breadcrumb (CourseR tid ssh csh CUsersR) = return ("Anmeldungen", Just $ CourseR tid ssh csh CShowR)
- breadcrumb (CourseR tid ssh csh (CUserR _)) = return ("Teilnehmer" , Just $ CourseR tid ssh csh CShowR)
+ breadcrumb (CourseR tid ssh csh (CUserR _)) = return ("Teilnehmer" , Just $ CourseR tid ssh csh CUsersR)
breadcrumb (CourseR tid ssh csh CCorrectionsR) = return ("Abgaben" , Just $ CourseR tid ssh csh CShowR)
breadcrumb (CourseR tid ssh csh SheetListR) = return ("Übungen" , Just $ CourseR tid ssh csh CShowR)
breadcrumb (CourseR tid ssh csh SheetNewR ) = return ("Neu", Just $ CourseR tid ssh csh SheetListR)
diff --git a/src/Handler/Course.hs b/src/Handler/Course.hs
index ed3b194ac..6145f379f 100644
--- a/src/Handler/Course.hs
+++ b/src/Handler/Course.hs
@@ -807,7 +807,7 @@ getCUsersR tid ssh csh = do
, sortable (Just "registration") (i18nCell MsgRegisteredHeader) (dateCell . view _userTableRegistration)
, colUserComment tid ssh csh
]
- psValidator = def
+ psValidator = def & defaultSortingByName
Entity cid course <- getBy404 $ TermSchoolCourseShort tid ssh csh
numParticipants <- count [CourseParticipantCourse ==. cid]
participantTable <- makeCourseUserTable cid colChoices psValidator
@@ -819,9 +819,9 @@ getCUsersR tid ssh csh = do
$(widgetFile "course-participants")
-
-getCUserR :: TermId -> SchoolId -> CourseShorthand -> CryptoUUIDUser -> Handler Html
-getCUserR _tid _ssh _csh uCId = do
+getCUserR, postCUserR :: TermId -> SchoolId -> CourseShorthand -> CryptoUUIDUser -> Handler Html
+getCUserR = postCUserR
+postCUserR tid ssh csh uCId = do
-- Has authorization checks (OR):
--
-- - User is current member of course
@@ -831,20 +831,72 @@ getCUserR _tid _ssh _csh uCId = do
-- - User is corrector for course
-- - User is a tutor for course
-- - User is a lecturer for course
+ let currentRoute = CourseR tid ssh csh (CUserR uCId)
+ dozentId <- requireAuthId
uid <- decrypt uCId
- User{..} <- runDB $ get404 uid
+ -- DB reads
+ (cid, User{..}, thisUniqueNote, noteText, noteEdits ) <- runDB $ do
+ cid <- getKeyBy404 $ TermSchoolCourseShort tid ssh csh
+ user <- get404 uid
+ let thisUniqueNote = UniqueCourseUserNote uid cid
+ mbNoteEnt <- getBy thisUniqueNote
+ (noteText,noteEdits) <- case mbNoteEnt of
+ Nothing -> return (Nothing,[])
+ (Just (Entity noteKey CourseUserNote{courseUserNoteNote})) -> do
+ noteEdits <- E.select $ E.from $ \(edit `E.InnerJoin` usr) -> do
+ E.on $ edit E.^. CourseUserNoteEditUser E.==. usr E.^. UserId
+ E.where_ $ edit E.^. CourseUserNoteEditNote E.==. E.val noteKey
+ E.orderBy [E.desc $ edit E.^. CourseUserNoteEditTime]
+ E.limit 1 -- more will be shown, if changed here
+ return (edit E.^. CourseUserNoteEditTime, usr E.^. UserEmail, usr E.^. UserDisplayName, usr E.^. UserSurname)
+ return (Just courseUserNoteNote, $(unValueN 4) <$> noteEdits)
+ return (cid,user,thisUniqueNote,noteText,noteEdits)
+ let editByWgt = [whamlet|
+ $forall (etime,_eemail,ename,_esurname) <- noteEdits
+
+ _{MsgLastEdit} ^{editedByW SelFormatDateTime etime ename}
+ |] -- _{MsgLastEdit} ^{formatTimeW SelFormatDateTime etime} ^{nameWidget ename esurname}
+
+ ((noteRes, noteView), noteEnctype) <- runFormPost . identifyForm FIDcUserNote . renderAForm FormStandard $
+ (aopt (annotateField editByWgt textField) (fslpI MsgCourseUserNote "Text" & setTooltip MsgCourseUserNoteTooltip) $ Just noteText)
+ <* submitButton
+ formResult noteRes $ \mbNote -> (do
+ let note = foldMap id mbNote -- Maybe Text to maybe empty Text
+ now <- liftIO getCurrentTime
+ if null note
+ then do
+ runDB $ do
+ -- must delete all edits due to foreign key constraints, which does not make sense -> refactor!
+ maybeM (return ()) (\nk -> deleteWhere [CourseUserNoteEditNote ==. nk]) (getKeyBy thisUniqueNote)
+ deleteBy thisUniqueNote
+ addMessageI Info MsgCourseUserNoteDeleted
+ else do
+ runDB $ do
+ (Entity noteKey _) <- upsertBy thisUniqueNote (CourseUserNote cid uid note) [CourseUserNoteNote =. note]
+ void . insert $ CourseUserNoteEdit dozentId now noteKey
+ addMessageI Success MsgCourseUserNoteSaved
+ )
+
+
-- USE src/utils/Form.formResult
defaultLayout -- TODO
[whamlet|
-
^{nameWidget userDisplayName userSurname} +