Course user notes working, but needs model refactor

This commit is contained in:
Steffen Jost 2019-03-22 18:01:26 +01:00
parent 21ba1f2afa
commit c9ab64e518
14 changed files with 147 additions and 27 deletions

View File

@ -95,6 +95,10 @@ CourseFilterNone: Egal
CourseDeleteQuestion: Wollen Sie den unten aufgeführten Kurs wirklich löschen? CourseDeleteQuestion: Wollen Sie den unten aufgeführten Kurs wirklich löschen?
CourseDeleted: Kurs gelöscht CourseDeleted: Kurs gelöscht
CourseUserNote: Notiz 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. NoSuchTerm tid@TermId: Semester #{display tid} gibt es nicht.
NoSuchSchool ssh@SchoolId: Institut #{display ssh} gibt es nicht. NoSuchSchool ssh@SchoolId: Institut #{display ssh} gibt es nicht.

View File

@ -40,12 +40,20 @@ CourseParticipant -- course enrolement
registration UTCTime -- time of last enrolement for this course registration UTCTime -- time of last enrolement for this course
field StudyFeaturesId Maybe -- associated degree course, user-defined; required for communicating grades field StudyFeaturesId Maybe -- associated degree course, user-defined; required for communicating grades
UniqueParticipant user course 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 CourseUserNote -- lecturers of a specific course may share a text note on each enrolled student
course CourseId course CourseId
user UserId user UserId
note Text -- arbitrary user-defined text; visible only to lecturer of this course note Text -- arbitrary user-defined text; visible only to lecturer of this course
UniqueCourseUserNotes user course UniqueCourseUserNote user course
CourseUserNoteEdit -- who edited a participants course note whenl CourseUserNoteEdit -- who edited a participants course note when
user UserId user UserId
time UTCTime time UTCTime
note CourseUserNoteId note CourseUserNoteId -- PROBLEM: deleted notes have no modification date any more

2
routes
View File

@ -76,7 +76,7 @@
/edit CEditR GET POST /edit CEditR GET POST
/delete CDeleteR GET POST !lecturerANDempty /delete CDeleteR GET POST !lecturerANDempty
/users CUsersR GET /users CUsersR GET
/users/#CryptoUUIDUser CUserR GET !lecturerANDparticipant /users/#CryptoUUIDUser CUserR GET POST !lecturerANDparticipant
/correctors CHiWisR GET /correctors CHiWisR GET
/notes CNotesR GET POST !corrector /notes CNotesR GET POST !corrector
/subs CCorrectionsR GET POST /subs CCorrectionsR GET POST

View File

@ -54,13 +54,20 @@ all :: Foldable f =>
all test = F.foldr (\needle acc -> acc E.&&. test needle) true all test = F.foldr (\needle acc -> acc E.&&. test needle) true
-- Allow usage of Tuples as DbtRowKey, i.e. SqlIn instances for tuples -- Allow usage of Tuples as DbtRowKey, i.e. SqlIn instances for tuples
$(sqlInTuples [2..16]) $(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 -- | Example for usage of sqlIJproj
-- queryFeaturesDegree :: (a `E.InnerJoin` b `E.InnerJoin` c) -> b _queryFeaturesDegree :: (a `E.InnerJoin` b `E.InnerJoin` c) -> b
-- queryFeaturesDegree = $(sqlIJproj 3 2) _queryFeaturesDegree = $(sqlIJproj 3 2)
-- | generic filter creation for dbTable -- | generic filter creation for dbTable

View File

@ -1,6 +1,7 @@
module Database.Esqueleto.Utils.TH module Database.Esqueleto.Utils.TH
( SqlIn(..) ( SqlIn(..)
, sqlInTuple, sqlInTuples , sqlInTuple, sqlInTuples
, unValueN, unValueNIs
, sqlIJproj, sqlLOJproj , sqlIJproj, sqlLOJproj
) where ) 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 -- | Generic projections for InnerJoin-tuples
-- gives I-th element of N-tuple of left-associative InnerJoin-pairs, -- 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) -- i.e. @$(projN n m) :: (t1 `E.InnerJoin` .. `E.InnerJoin` tn) -> tm@ (for m<=n)

View File

@ -1147,7 +1147,7 @@ instance YesodBreadcrumbs UniWorX where
-- (CourseR tid ssh csh CRegisterR) -- is POST only -- (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 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 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 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 SheetListR) = return ("Übungen" , Just $ CourseR tid ssh csh CShowR)
breadcrumb (CourseR tid ssh csh SheetNewR ) = return ("Neu", Just $ CourseR tid ssh csh SheetListR) breadcrumb (CourseR tid ssh csh SheetNewR ) = return ("Neu", Just $ CourseR tid ssh csh SheetListR)

View File

@ -807,7 +807,7 @@ getCUsersR tid ssh csh = do
, sortable (Just "registration") (i18nCell MsgRegisteredHeader) (dateCell . view _userTableRegistration) , sortable (Just "registration") (i18nCell MsgRegisteredHeader) (dateCell . view _userTableRegistration)
, colUserComment tid ssh csh , colUserComment tid ssh csh
] ]
psValidator = def psValidator = def & defaultSortingByName
Entity cid course <- getBy404 $ TermSchoolCourseShort tid ssh csh Entity cid course <- getBy404 $ TermSchoolCourseShort tid ssh csh
numParticipants <- count [CourseParticipantCourse ==. cid] numParticipants <- count [CourseParticipantCourse ==. cid]
participantTable <- makeCourseUserTable cid colChoices psValidator participantTable <- makeCourseUserTable cid colChoices psValidator
@ -819,9 +819,9 @@ getCUsersR tid ssh csh = do
$(widgetFile "course-participants") $(widgetFile "course-participants")
getCUserR, postCUserR :: TermId -> SchoolId -> CourseShorthand -> CryptoUUIDUser -> Handler Html
getCUserR :: TermId -> SchoolId -> CourseShorthand -> CryptoUUIDUser -> Handler Html getCUserR = postCUserR
getCUserR _tid _ssh _csh uCId = do postCUserR tid ssh csh uCId = do
-- Has authorization checks (OR): -- Has authorization checks (OR):
-- --
-- - User is current member of course -- - User is current member of course
@ -831,20 +831,72 @@ getCUserR _tid _ssh _csh uCId = do
-- - User is corrector for course -- - User is corrector for course
-- - User is a tutor for course -- - User is a tutor for course
-- - User is a lecturer for course -- - User is a lecturer for course
let currentRoute = CourseR tid ssh csh (CUserR uCId)
dozentId <- requireAuthId
uid <- decrypt uCId 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
<br>
_{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 -- USE src/utils/Form.formResult
defaultLayout -- TODO defaultLayout -- TODO
[whamlet| [whamlet|
<p>^{nameWidget userDisplayName userSurname} <h2>^{nameWidget userDisplayName userSurname}
#{mailtoHtml userEmail}
<section>
<a id="note-form">
<form method=post action=@{currentRoute}#note-form enctype=#{noteEnctype}>
^{noteView}
|] |]
getCHiWisR :: TermId -> SchoolId -> CourseShorthand -> Handler Html getCHiWisR :: TermId -> SchoolId -> CourseShorthand -> Handler Html
getCHiWisR = error "CHiWisR: Not implemented" getCHiWisR = error "CHiWisR: Not implemented"
getCNotesR, postCNotesR :: TermId -> SchoolId -> CourseShorthand -> Handler Html getCNotesR, postCNotesR :: TermId -> SchoolId -> CourseShorthand -> Handler Html
-- NOTE: The route getNotesR is abused for correctorORlecturer access rights! -- NOTE: The route getNotesR is abused for correctorORlecturer access rights!
-- PROBLEM: Correctors usually don't know Participants by name (anonymous), maybe notes are not shared? -- PROBLEM: Correctors usually don't know Participants by name (anonymous), maybe notes are not shared?
-- If they are shared, adjust MsgCourseUserNoteTooltip
getCNotesR = error "CNotesR: Not implemented" getCNotesR = error "CNotesR: Not implemented"
postCNotesR = error "CNotesR: Not implemented" postCNotesR = error "CNotesR: Not implemented"

View File

@ -130,7 +130,7 @@ getProfileDataR = do
dataWidget dataWidget
$(widgetFile "dsgvDisclaimer") $(widgetFile "dsgvDisclaimer")
makeProfileData :: (Entity User) -> DB Widget makeProfileData :: Entity User -> DB Widget
makeProfileData (Entity uid User{..}) = do makeProfileData (Entity uid User{..}) = do
-- MsgRenderer mr <- getMsgRenderer -- MsgRenderer mr <- getMsgRenderer
admin_rights <- E.select $ E.from $ \(adright `E.InnerJoin` school) -> do admin_rights <- E.select $ E.from $ \(adright `E.InnerJoin` school) -> do

View File

@ -3,7 +3,6 @@ module Handler.SystemMessage where
import Import import Import
import qualified Data.Map.Lazy as Map import qualified Data.Map.Lazy as Map
import qualified Data.Text as Text
import qualified Data.Set as Set import qualified Data.Set as Set
@ -16,13 +15,7 @@ import Utils.Lens
import qualified Database.Esqueleto as E import qualified Database.Esqueleto as E
-- htmlField' moved to Handler.Utils.Form/Fields
htmlField' :: Field (HandlerT UniWorX IO) Html
htmlField' = htmlField
{ fieldParse = \vs fis -> fieldParse htmlField (map Text.strip vs) fis
}
getMessageR, postMessageR :: CryptoUUIDSystemMessage -> Handler Html getMessageR, postMessageR :: CryptoUUIDSystemMessage -> Handler Html
getMessageR = postMessageR getMessageR = postMessageR

View File

@ -91,6 +91,12 @@ wrapMailto (original -> email) linkText
mailtoHtml :: CI Text -> Html mailtoHtml :: CI Text -> Html
mailtoHtml email = wrapMailto email $ toHtml email mailtoHtml email = wrapMailto email $ toHtml email
-- | Generic i18n text for "edited at sometime by someone"
editedByW :: SelDateTimeFormat -> UTCTime -> Text -> Widget
editedByW fmt tm usr = do
ft <- handlerToWidget $ formatTime fmt tm
[whamlet|_{MsgEditedBy usr ft}|]
-- | Prefix a message with a short course id, -- | Prefix a message with a short course id,
-- eg. for window title bars, etc. -- eg. for window title bars, etc.
-- This function should help to make this consistent everywhere -- This function should help to make this consistent everywhere

View File

@ -12,7 +12,7 @@ import Handler.Utils.DateTime
import Import hiding (cons) import Import hiding (cons)
import qualified Data.Char as Char import qualified Data.Char as Char
import qualified Data.Text as Text
import qualified Data.CaseInsensitive as CI import qualified Data.CaseInsensitive as CI
-- import Yesod.Core -- import Yesod.Core
@ -141,6 +141,16 @@ buttonForm = identifyForm FIDbuttonForm buttonFormAux -- TODO: distinguish diffe
-- Fields -- -- Fields --
------------ ------------
-- | add some additional text immediately after the field widget; probably not a good idea to use
annotateField :: ToWidget (HandlerSite m) wgt => wgt -> Field m a -> Field m a
annotateField ann field@Field{fieldView=fvf} =
let fvf' idt nmt atts ei bl =
[whamlet|
^{fvf idt nmt atts ei bl}
^{ann}
|]
in field { fieldView=fvf'}
-- ciField moved to Utils.Form -- ciField moved to Utils.Form
routeField :: ( Monad m routeField :: ( Monad m
@ -148,6 +158,12 @@ routeField :: ( Monad m
) => Field m (Route UniWorX) ) => Field m (Route UniWorX)
routeField = checkMMap (return . maybe (Left MsgInvalidRoute) Right . fromPathPiece) toPathPiece textField routeField = checkMMap (return . maybe (Left MsgInvalidRoute) Right . fromPathPiece) toPathPiece textField
-- | Variant that simply removes leading and trailing white space
htmlField' :: Field (HandlerT UniWorX IO) Html
htmlField' = htmlField
{ fieldParse = \vs fis -> fieldParse htmlField (map Text.strip vs) fis
}
natFieldI :: (Monad m, Integral i, RenderMessage (HandlerSite m) msg, RenderMessage (HandlerSite m) FormMessage) => msg -> Field m i natFieldI :: (Monad m, Integral i, RenderMessage (HandlerSite m) msg, RenderMessage (HandlerSite m) FormMessage) => msg -> Field m i
natFieldI msg = checkBool (>= 0) msg intField natFieldI msg = checkBool (>= 0) msg intField

View File

@ -66,8 +66,8 @@ sortUserDisplayName queryUser = ("user-display-name", SortColumn $ queryUser >>>
defaultSortingByName :: PSValidator m x -> PSValidator m x defaultSortingByName :: PSValidator m x -> PSValidator m x
defaultSortingByName = defaultSortingByName =
defaultSorting [SortAscBy "user-surname", SortAscBy "user-display-name"] -- old way, requiring two exta sorters -- defaultSorting [SortAscBy "user-surname", SortAscBy "user-display-name"] -- old way, requiring two exta sorters
-- defaultSorting [SortAscBy "user-name"] -- new way, untested, working with single sorter defaultSorting [SortAscBy "user-name"] -- new way, untested, working with single sorter
-- | Alias for sortUserName for consistency -- | Alias for sortUserName for consistency
fltrUserNameLink :: (IsFilterColumn t (a -> Set Text -> E.SqlExpr (E.Value Bool)), IsString d) => (a -> E.SqlExpr (Entity User)) -> (d, FilterColumn t) fltrUserNameLink :: (IsFilterColumn t (a -> Set Text -> E.SqlExpr (E.Value Bool)), IsString d) => (a -> E.SqlExpr (Entity User)) -> (d, FilterColumn t)

View File

@ -218,6 +218,7 @@ data FormIdentifier
| FIDDelete | FIDDelete
| FIDCourseRegister | FIDCourseRegister
| FIDuserRights | FIDuserRights
| FIDcUserNote
| FIDbuttonForm | FIDbuttonForm
deriving (Eq, Ord, Read, Show) deriving (Eq, Ord, Read, Show)

View File

@ -24,7 +24,7 @@ projNI n i = do
x <- newName "x" x <- newName "x"
let rhs = varE x let rhs = varE x
let pat = tupP $ replicate (pred i) wildP ++ varP x : replicate (n-i) wildP let pat = tupP $ replicate (pred i) wildP ++ varP x : replicate (n-i) wildP
lamE [pat] rhs lam1E pat rhs
-- | Generic projections N-tuples that are actually left-associative pairs -- | Generic projections N-tuples that are actually left-associative pairs
@ -83,6 +83,14 @@ uncurryN n = do
return $ LamE pat rhs return $ LamE pat rhs
afterN :: Int -> ExpQ -- apply a function after another of arity N, i.e. $(afterN 1) = (.)
afterN n = do
f <- newName "f"
g <- newName "g"
--let rhs = [|$(curryN n) (g . ($(uncurryN n) f))|]
lamE [(varP g),(varP f)] [|$(curryN n) ($(varE g) . ($(uncurryN n) $(varE f)))|]
-- Special Show-Instances for Themes -- Special Show-Instances for Themes
deriveShowWith :: (String -> String) -> Name -> Q [Dec] deriveShowWith :: (String -> String) -> Name -> Q [Dec]
deriveShowWith = deriveSimpleWith ''Show 'show deriveShowWith = deriveSimpleWith ''Show 'show