feat(course-communication): one recipient group per tutorial

Fixes #428
This commit is contained in:
Gregor Kleen 2020-05-25 14:04:31 +02:00
parent 4fb6762b89
commit 99f23f2558
8 changed files with 139 additions and 71 deletions

View File

@ -1445,7 +1445,7 @@ RGCourseParticipants: Kursteilnehmer
RGCourseLecturers: Kursverwalter RGCourseLecturers: Kursverwalter
RGCourseCorrectors: Korrektoren RGCourseCorrectors: Korrektoren
RGCourseTutors: Tutoren RGCourseTutors: Tutoren
RGTutorialParticipants: Tutorium-Teilnehmer RGTutorialParticipants tutn@TutorialName: Tutorium-Teilnehmer (#{tutn})
MultiSelectFieldTip: Mehrfach-Auswahl ist möglich (Umschalt bzw. Strg) MultiSelectFieldTip: Mehrfach-Auswahl ist möglich (Umschalt bzw. Strg)
MultiEmailFieldTip: Es sind mehrere, Komma-separierte, E-Mail-Addressen möglich MultiEmailFieldTip: Es sind mehrere, Komma-separierte, E-Mail-Addressen möglich

View File

@ -1445,7 +1445,7 @@ RGCourseParticipants: Course participants
RGCourseLecturers: Course administrators RGCourseLecturers: Course administrators
RGCourseCorrectors: Course correctors RGCourseCorrectors: Course correctors
RGCourseTutors: Course tutors RGCourseTutors: Course tutors
RGTutorialParticipants: Tutorial participants RGTutorialParticipants tutn: Tutorial participants (#{tutn})
MultiSelectFieldTip: Multiple selections are possible (Shift or Ctrl) MultiSelectFieldTip: Multiple selections are possible (Shift or Ctrl)
MultiEmailFieldTip: Multiple emails addresses may be specified (comma-separated) MultiEmailFieldTip: Multiple emails addresses may be specified (comma-separated)

View File

@ -16,21 +16,28 @@ import qualified Data.Map as Map
getTCommR, postTCommR :: TermId -> SchoolId -> CourseShorthand -> TutorialName -> Handler Html getTCommR, postTCommR :: TermId -> SchoolId -> CourseShorthand -> TutorialName -> Handler Html
getTCommR = postTCommR getTCommR = postTCommR
postTCommR tid ssh csh tutn = do postTCommR tid ssh csh tutn = do
(cid, tutid) <- runDB $ fetchCourseIdTutorialId tid ssh csh tutn ((cid, tutid), usertuts) <- runDB $ do
tutData@(cid, _) <- fetchCourseIdTutorialId tid ssh csh tutn
tuts <- selectList [TutorialCourse ==. cid] []
usertuts <- forMaybeM tuts $ \(Entity tutid Tutorial{..}) -> do
cID <- encrypt tutid
guardM . hasReadAccessTo $ CTutorialR tid ssh csh tutorialName TUsersR
return ( RGTutorialParticipants cID
, E.from $ \(user `E.InnerJoin` participant) -> do
E.on $ user E.^. UserId E.==. participant E.^. TutorialParticipantUser
E.where_ $ participant E.^. TutorialParticipantTutorial E.==. E.val tutid
return user
)
return (tutData, usertuts)
commR CommunicationRoute commR CommunicationRoute
{ crHeading = SomeMessage . prependCourseTitle tid ssh csh $ SomeMessage MsgCommTutorialHeading { crHeading = SomeMessage . prependCourseTitle tid ssh csh $ SomeMessage MsgCommTutorialHeading
, crUltDest = SomeRoute $ CTutorialR tid ssh csh tutn TCommR , crUltDest = SomeRoute $ CTutorialR tid ssh csh tutn TCommR
, crJobs = crJobsCourseCommunication cid , crJobs = crJobsCourseCommunication cid
, crTestJobs = crTestJobsCourseCommunication cid , crTestJobs = crTestJobsCourseCommunication cid
, crRecipients = Map.fromList , crRecipients = Map.fromList $
[ ( RGTutorialParticipants [ ( RGCourseLecturers
, E.from $ \(user `E.InnerJoin` participant) -> do
E.on $ user E.^. UserId E.==. participant E.^. TutorialParticipantUser
E.where_ $ participant E.^. TutorialParticipantTutorial E.==. E.val tutid
return user
)
, ( RGCourseLecturers
, E.from $ \(user `E.InnerJoin` lecturer) -> do , E.from $ \(user `E.InnerJoin` lecturer) -> do
E.on $ user E.^. UserId E.==. lecturer E.^. LecturerUser E.on $ user E.^. UserId E.==. lecturer E.^. LecturerUser
E.where_ $ lecturer E.^. LecturerCourse E.==. E.val cid E.where_ $ lecturer E.^. LecturerCourse E.==. E.val cid
@ -52,7 +59,7 @@ postTCommR tid ssh csh tutn = do
E.&&. tutor E.^. TutorUser E.==. user E.^. UserId E.&&. tutor E.^. TutorUser E.==. user E.^. UserId
return user return user
) )
] ] ++ usertuts
, crRecipientAuth = Just $ \uid -> do , crRecipientAuth = Just $ \uid -> do
isTutorialUser <- E.selectExists . E.from $ \tutorialUser -> isTutorialUser <- E.selectExists . E.from $ \tutorialUser ->
E.where_ $ tutorialUser E.^. TutorialParticipantUser E.==. E.val uid E.where_ $ tutorialUser E.^. TutorialParticipantUser E.==. E.val uid

View File

@ -23,17 +23,14 @@ import qualified Data.Conduit.Combinators as C
data RecipientGroup = RGCourseParticipants | RGCourseLecturers | RGCourseCorrectors | RGCourseTutors data RecipientGroup = RGCourseParticipants | RGCourseLecturers | RGCourseCorrectors | RGCourseTutors
| RGTutorialParticipants | RGTutorialParticipants CryptoUUIDTutorial
deriving (Eq, Ord, Enum, Bounded, Read, Show, Generic, Typeable) deriving (Eq, Ord, Read, Show, Generic, Typeable)
instance Universe RecipientGroup instance LowerBounded RecipientGroup where
instance Finite RecipientGroup minBound' = RGCourseParticipants
nullaryPathPiece ''RecipientGroup $ camelToPathPiece' 1
embedRenderMessage ''UniWorX ''RecipientGroup id
deriveJSON defaultOptions derivePathPiece ''RecipientGroup (camelToPathPiece' 1) "--"
{ constructorTagModifier = camelToPathPiece' 1 pathPieceJSON ''RecipientGroup
} ''RecipientGroup
data RecipientCategory data RecipientCategory
@ -41,19 +38,8 @@ data RecipientCategory
| RecipientCustom | RecipientCustom
deriving (Eq, Ord, Read, Show, Generic, Typeable) deriving (Eq, Ord, Read, Show, Generic, Typeable)
deriveFinite ''RecipientCategory instance LowerBounded RecipientCategory where
finiteEnum ''RecipientCategory minBound' = RecipientGroup minBound'
deriveJSON defaultOptions
{ constructorTagModifier = camelToPathPiece' 1
, unwrapUnaryRecords = True
, sumEncoding = UntaggedValue
} ''RecipientCategory
instance ToJSONKey RecipientCategory where
toJSONKey = toJSONKeyText toPathPiece
instance FromJSONKey RecipientCategory where
fromJSONKey = FromJSONKeyTextParser $ maybe (fail "Could not pars RecipientCategory") return . fromPathPiece
instance PathPiece RecipientCategory where instance PathPiece RecipientCategory where
toPathPiece RecipientCustom = "custom" toPathPiece RecipientCustom = "custom"
@ -62,13 +48,8 @@ instance PathPiece RecipientCategory where
fromPathPiece "custom" = Just RecipientCustom fromPathPiece "custom" = Just RecipientCustom
fromPathPiece t = RecipientGroup <$> fromPathPiece t fromPathPiece t = RecipientGroup <$> fromPathPiece t
instance RenderMessage UniWorX RecipientCategory where pathPieceJSON ''RecipientCategory
renderMessage foundation ls = \case pathPieceJSONKey ''RecipientCategory
RecipientCustom -> renderMessage' MsgRecipientCustom
RecipientGroup g -> renderMessage' g
where
renderMessage' :: forall msg. RenderMessage UniWorX msg => msg -> Text
renderMessage' = renderMessage foundation ls
data CommunicationButton data CommunicationButton
= BtnCommunicationSend = BtnCommunicationSend
@ -147,13 +128,13 @@ commR CommunicationRoute{..} = do
= entityVal . unsafeHead . filter ((== lId) . entityKey) $ concat (Map.elems suggestedRecipients) ++ chosenRecipients = entityVal . unsafeHead . filter ((== lId) . entityKey) $ concat (Map.elems suggestedRecipients) ++ chosenRecipients
let chosenRecipients' = Map.fromList $ let chosenRecipients' = Map.fromList $
[ ( (EnumPosition $ RecipientGroup g, pos) [ ( (BoundedPosition $ RecipientGroup g, pos)
, (Right recp, recp `elem` map entityKey chosenRecipients) , (Right recp, recp `elem` map entityKey chosenRecipients)
) )
| (g, recps) <- Map.toList suggestedRecipients | (g, recps) <- Map.toList suggestedRecipients
, (pos, recp) <- zip [0..] $ map entityKey recps , (pos, recp) <- zip [0..] $ map entityKey recps
] ++ ] ++
[ ( (EnumPosition RecipientCustom, pos) [ ( (BoundedPosition RecipientCustom, pos)
, (Right recp, True) , (Right recp, True)
) )
| (pos, recp) <- zip [0..] . Set.toList $ Set.fromList (map entityKey chosenRecipients) \\ Set.fromList (concatMap (map entityKey) $ Map.elems suggestedRecipients) | (pos, recp) <- zip [0..] . Set.toList $ Set.fromList (map entityKey chosenRecipients) \\ Set.fromList (concatMap (map entityKey) $ Map.elems suggestedRecipients)
@ -163,10 +144,10 @@ commR CommunicationRoute{..} = do
let recipientAForm :: AForm Handler (Set (Either UserEmail UserId)) let recipientAForm :: AForm Handler (Set (Either UserEmail UserId))
recipientAForm = postProcess <$> massInputA MassInput{..} (fslI MsgCommRecipients & setTooltip MsgCommRecipientsTip) True (Just chosenRecipients') recipientAForm = postProcess <$> massInputA MassInput{..} (fslI MsgCommRecipients & setTooltip MsgCommRecipientsTip) True (Just chosenRecipients')
where where
miAdd (EnumPosition RecipientCustom, 0) 1 nudge submitView = Just $ \csrf -> do miAdd (BoundedPosition RecipientCustom, 0) 1 nudge submitView = Just $ \csrf -> do
(addRes, addView) <- mpreq (multiUserField True Nothing) (fslpI MsgEMail (mr MsgEMail) & setTooltip MsgMultiEmailFieldTip & addName (nudge "email")) Nothing (addRes, addView) <- mpreq (multiUserField True Nothing) (fslpI MsgEMail (mr MsgEMail) & setTooltip MsgMultiEmailFieldTip & addName (nudge "email")) Nothing
let let
addRes' = addRes <&> \nEmails ((Map.elems &&& maybe 0 (succ . snd . fst) . Map.lookupMax) . Map.filterWithKey (\(EnumPosition c, _) _ -> c == RecipientCustom) -> (oEmails, kStart)) -> FormSuccess . Map.fromList . zip (map (EnumPosition RecipientCustom, ) [kStart..]) . Set.toList $ nEmails `Set.difference` Set.fromList oEmails addRes' = addRes <&> \nEmails ((Map.elems &&& maybe 0 (succ . snd . fst) . Map.lookupMax) . Map.filterWithKey (\(BoundedPosition c, _) _ -> c == RecipientCustom) -> (oEmails, kStart)) -> FormSuccess . Map.fromList . zip (map (BoundedPosition RecipientCustom, ) [kStart..]) . Set.toList $ nEmails `Set.difference` Set.fromList oEmails
return (addRes', $(widgetFile "widgets/communication/recipientAdd")) return (addRes', $(widgetFile "widgets/communication/recipientAdd"))
miAdd _ _ _ _ = Nothing miAdd _ _ _ _ = Nothing
miCell _ (Left (CI.original -> email)) initRes nudge csrf = do miCell _ (Left (CI.original -> email)) initRes nudge csrf = do
@ -179,31 +160,36 @@ commR CommunicationRoute{..} = do
| otherwise | otherwise
-> mpreq checkBoxField ("" & addName (nudge "tick")) $ initRes <|> Just True -> mpreq checkBoxField ("" & addName (nudge "tick")) $ initRes <|> Just True
return (tickRes, $(widgetFile "widgets/communication/recipientName")) return (tickRes, $(widgetFile "widgets/communication/recipientName"))
miAllowAdd (EnumPosition RecipientCustom, 0) 1 _ = True miAllowAdd (BoundedPosition RecipientCustom, 0) 1 _ = True
miAllowAdd _ _ _ = False miAllowAdd _ _ _ = False
miAddEmpty _ 0 _ = Set.singleton (EnumPosition RecipientCustom, 0) miAddEmpty _ 0 _ = Set.singleton (BoundedPosition RecipientCustom, 0)
miAddEmpty _ _ _ = Set.empty miAddEmpty _ _ _ = Set.empty
miButtonAction :: forall p . PathPiece p => p -> Maybe (SomeRoute UniWorX) miButtonAction :: forall p . PathPiece p => p -> Maybe (SomeRoute UniWorX)
miButtonAction anchor = SomeRoute . (:#: anchor) <$> mbCurrentRoute miButtonAction anchor = SomeRoute . (:#: anchor) <$> mbCurrentRoute
miLayout :: MapLiveliness (EnumLiveliness RecipientCategory) ListLength miLayout :: MapLiveliness (BoundedLiveliness RecipientCategory) ListLength
-> Map (EnumPosition RecipientCategory, ListPosition) (_, FormResult Bool) -> Map (BoundedPosition RecipientCategory, ListPosition) (_, FormResult Bool)
-> Map (EnumPosition RecipientCategory, ListPosition) Widget -> Map (BoundedPosition RecipientCategory, ListPosition) Widget
-> Map (EnumPosition RecipientCategory, ListPosition) (FieldView UniWorX) -> Map (BoundedPosition RecipientCategory, ListPosition) (FieldView UniWorX)
-> Map (Natural, (EnumPosition RecipientCategory, ListPosition)) Widget -> Map (Natural, (BoundedPosition RecipientCategory, ListPosition)) Widget
-> Widget -> Widget
miLayout liveliness cState cellWdgts _delButtons addWdgts = do miLayout liveliness cState cellWdgts _delButtons addWdgts = do
checkedIdentBase <- newIdent checkedIdentBase <- newIdent
let checkedCategories = Set.mapMonotonic (unEnumPosition . fst) . Set.filter (\k' -> Map.foldrWithKey (\k (_, checkState) -> (||) $ k == k' && checkState /= FormSuccess False && (checkState /= FormMissing || maybe True snd (chosenRecipients' !? k))) False cState) $ Map.keysSet cState let checkedCategories = Set.mapMonotonic (unBoundedPosition . fst) . Set.filter (\k' -> Map.foldrWithKey (\k (_, checkState) -> (||) $ k == k' && checkState /= FormSuccess False && (checkState /= FormMissing || maybe True snd (chosenRecipients' !? k))) False cState) $ Map.keysSet cState
checkedIdent c = checkedIdentBase <> "-" <> toPathPiece c checkedIdent c = checkedIdentBase <> "-" <> toPathPiece c
hasContent c = not (null $ categoryIndices c) || Map.member (1, (EnumPosition c, 0)) addWdgts hasContent c = not (null $ categoryIndices c) || Map.member (1, (BoundedPosition c, 0)) addWdgts
categoryIndices c = Set.filter ((== c) . unEnumPosition . fst) $ review liveCoords liveliness categoryIndices c = Set.filter ((== c) . unBoundedPosition . fst) $ review liveCoords liveliness
rgTutorialParticipantsCaption :: CryptoUUIDTutorial -> Widget
rgTutorialParticipantsCaption cID = do
tutId <- decrypt cID
Tutorial{..} <- liftHandler . runDBRead $ get404 tutId
i18n $ MsgRGTutorialParticipants tutorialName
$(widgetFile "widgets/communication/recipientLayout") $(widgetFile "widgets/communication/recipientLayout")
miDelete :: Map (EnumPosition RecipientCategory, ListPosition) (Either UserEmail UserId) -> (EnumPosition RecipientCategory, ListPosition) -> MaybeT (MForm Handler) (Map (EnumPosition RecipientCategory, ListPosition) (EnumPosition RecipientCategory, ListPosition)) miDelete :: Map (BoundedPosition RecipientCategory, ListPosition) (Either UserEmail UserId) -> (BoundedPosition RecipientCategory, ListPosition) -> MaybeT (MForm Handler) (Map (BoundedPosition RecipientCategory, ListPosition) (BoundedPosition RecipientCategory, ListPosition))
-- miDelete liveliness@(MapLiveliness lMap) (EnumPosition RecipientCustom, delPos) = mappend (Map.fromSet id . Set.filter (\(EnumPosition c, _) -> c /= RecipientCustom) $ review liveCoords liveliness) . fmap (EnumPosition RecipientCustom, ) . Map.mapKeysMonotonic (EnumPosition RecipientCustom, ) <$> miDeleteList (lMap ! EnumPosition RecipientCustom) delPos -- miDelete liveliness@(MapLiveliness lMap) (BoundedPosition RecipientCustom, delPos) = mappend (Map.fromSet id . Set.filter (\(BoundedPosition c, _) -> c /= RecipientCustom) $ review liveCoords liveliness) . fmap (BoundedPosition RecipientCustom, ) . Map.mapKeysMonotonic (BoundedPosition RecipientCustom, ) <$> miDeleteList (lMap ! BoundedPosition RecipientCustom) delPos
miDelete _ _ = mzero miDelete _ _ = mzero
miIdent :: Text miIdent :: Text
miIdent = "recipients" miIdent = "recipients"
postProcess :: Map (EnumPosition RecipientCategory, ListPosition) (Either UserEmail UserId, Bool) -> Set (Either UserEmail UserId) postProcess :: Map (BoundedPosition RecipientCategory, ListPosition) (Either UserEmail UserId, Bool) -> Set (Either UserEmail UserId)
postProcess = Set.fromList . map fst . filter snd . Map.elems postProcess = Set.fromList . map fst . filter snd . Map.elems
recipientsListMsg <- messageI Info MsgCommRecipientsList recipientsListMsg <- messageI Info MsgCommRecipientsList

View File

@ -13,6 +13,7 @@ module Handler.Utils.Form.MassInput
, ListLength(..), ListPosition(..), miDeleteList , ListLength(..), ListPosition(..), miDeleteList
, EnumLiveliness(..), EnumPosition(..) , EnumLiveliness(..), EnumPosition(..)
, MapLiveliness(..) , MapLiveliness(..)
, LowerBounded(..), BoundedLiveliness(..), BoundedPosition(..)
) where ) where
import Import import Import
@ -81,17 +82,10 @@ instance Liveliness ListLength where
newtype EnumLiveliness enum = EnumLiveliness { unEnumLiveliness :: IntSet } newtype EnumLiveliness enum = EnumLiveliness { unEnumLiveliness :: IntSet }
deriving (Eq, Ord, Generic, Typeable, Read, Show) deriving (Eq, Ord, Generic, Typeable, Read, Show)
deriving newtype (Lattice, BoundedJoinSemiLattice)
makeWrapped ''EnumLiveliness makeWrapped ''EnumLiveliness
instance Lattice (EnumLiveliness enum) where
(EnumLiveliness a) \/ (EnumLiveliness b) = EnumLiveliness $ a `IntSet.union` b
(EnumLiveliness a) /\ (EnumLiveliness b) = EnumLiveliness $ a `IntSet.intersection` b
instance BoundedJoinSemiLattice (EnumLiveliness enum) where
bottom = EnumLiveliness IntSet.empty
instance (Enum enum, Bounded enum) => BoundedMeetSemiLattice (EnumLiveliness enum) where
top = EnumLiveliness . IntSet.fromList $ map (fromEnum :: enum -> Int) [minBound..maxBound]
newtype EnumPosition enum = EnumPosition { unEnumPosition :: enum } newtype EnumPosition enum = EnumPosition { unEnumPosition :: enum }
deriving newtype (Enum, Bounded, PathPiece, ToJSON, FromJSON, ToJSONKey, FromJSONKey) deriving newtype (Enum, Bounded, PathPiece, ToJSON, FromJSON, ToJSONKey, FromJSONKey)
@ -99,11 +93,11 @@ newtype EnumPosition enum = EnumPosition { unEnumPosition :: enum }
makeWrapped ''EnumPosition makeWrapped ''EnumPosition
instance (Enum enum, Bounded enum, PathPiece enum, ToJSON enum, FromJSON enum, ToJSONKey enum, FromJSONKey enum, Eq enum, Ord enum) => IsBoxCoord (EnumPosition enum) where instance (Bounded enum, PathPiece enum, ToJSON enum, FromJSON enum, ToJSONKey enum, FromJSONKey enum, Ord enum) => IsBoxCoord (EnumPosition enum) where
boxDimensions = [BoxDimension _Wrapped] boxDimensions = [BoxDimension _Wrapped]
boxOrigin = minBound boxOrigin = minBound
instance (Enum enum, Bounded enum, PathPiece enum, ToJSON enum, FromJSON enum, ToJSONKey enum, FromJSONKey enum, Eq enum, Ord enum) => Liveliness (EnumLiveliness enum) where instance (Enum enum, Bounded enum, PathPiece enum, ToJSON enum, FromJSON enum, ToJSONKey enum, FromJSONKey enum, Ord enum) => Liveliness (EnumLiveliness enum) where
type BoxCoord (EnumLiveliness enum) = EnumPosition enum type BoxCoord (EnumLiveliness enum) = EnumPosition enum
liveCoords = iso fromSet toSet liveCoords = iso fromSet toSet
where where
@ -113,6 +107,28 @@ instance (Enum enum, Bounded enum, PathPiece enum, ToJSON enum, FromJSON enum, T
fromSet = EnumLiveliness . IntSet.fromList . map fromEnum . Set.toList fromSet = EnumLiveliness . IntSet.fromList . map fromEnum . Set.toList
class Ord coord => LowerBounded coord where
minBound' :: coord
newtype BoundedLiveliness coord = BoundedLiveliness { unBoundedLiveliness :: Set coord }
deriving (Eq, Ord, Generic, Typeable, Read, Show)
deriving newtype (Lattice, BoundedJoinSemiLattice, BoundedMeetSemiLattice)
makeWrapped ''BoundedLiveliness
newtype BoundedPosition coord = BoundedPosition { unBoundedPosition :: coord }
deriving newtype (Enum, Bounded, PathPiece, ToJSON, FromJSON, ToJSONKey, FromJSONKey, LowerBounded)
deriving (Eq, Ord, Generic, Typeable, Read, Show)
makeWrapped ''BoundedPosition
instance (LowerBounded coord, PathPiece coord, ToJSON coord, FromJSON coord, ToJSONKey coord, FromJSONKey coord, Ord coord) => IsBoxCoord (BoundedPosition coord) where
boxDimensions = [BoxDimension _Wrapped]
boxOrigin = minBound'
instance (LowerBounded coord, PathPiece coord, ToJSON coord, FromJSON coord, ToJSONKey coord, FromJSONKey coord, Ord coord) => Liveliness (BoundedLiveliness coord) where
type BoxCoord (BoundedLiveliness coord) = BoundedPosition coord
liveCoords = iso (Set.mapMonotonic $ view _Wrapped) (Set.mapMonotonic $ view _Unwrapped) . _Unwrapped
newtype MapLiveliness l1 l2 = MapLiveliness { unMapLiveliness :: Map (BoxCoord l1) l2 } newtype MapLiveliness l1 l2 = MapLiveliness { unMapLiveliness :: Map (BoxCoord l1) l2 }
deriving (Generic, Typeable) deriving (Generic, Typeable)

View File

@ -20,7 +20,7 @@ import qualified Data.Set as Set
import Data.List (genericLength, genericIndex) import Data.List (genericLength, genericIndex)
data BoxDimension x = forall n. (Enum n, Eq n) => BoxDimension (Lens' x n) data BoxDimension x = forall n. Eq n => BoxDimension (Lens' x n)
class (ToJSON x, FromJSON x, ToJSONKey x, FromJSONKey x, PathPiece x, Eq x, Ord x) => IsBoxCoord x where class (ToJSON x, FromJSON x, ToJSONKey x, FromJSONKey x, PathPiece x, Eq x, Ord x) => IsBoxCoord x where
boxDimensions :: [BoxDimension x] boxDimensions :: [BoxDimension x]

View File

@ -1,6 +1,7 @@
module Utils.PathPiece module Utils.PathPiece
( nullaryToPathPiece', nullaryToPathPiece ( nullaryToPathPiece', nullaryToPathPiece
, nullaryPathPiece', nullaryPathPiece, finitePathPiece , nullaryPathPiece', nullaryPathPiece, finitePathPiece
, derivePathPiece
, splitCamel , splitCamel
, camelToPathPiece, camelToPathPiece' , camelToPathPiece, camelToPathPiece'
, nameToPathPiece, nameToPathPiece' , nameToPathPiece, nameToPathPiece'
@ -13,6 +14,7 @@ import ClassyPrelude.Yesod
import Language.Haskell.TH import Language.Haskell.TH
import qualified Language.Haskell.TH.Syntax as TH (Lift(..)) import qualified Language.Haskell.TH.Syntax as TH (Lift(..))
import Language.Haskell.TH.Datatype
import Data.Universe import Data.Universe
import qualified Data.Text as Text import qualified Data.Text as Text
@ -85,6 +87,51 @@ finitePathPiece finiteType verbs = do
, funD 'fromPathPiece , funD 'fromPathPiece
[ clause [] (normalB $ return finExp) [] ] [ clause [] (normalB $ return finExp) [] ]
] ]
derivePathPiece :: Name -> (Text -> Text) -> Text -> DecsQ
derivePathPiece adt mangle joinPP = do
let mangle' = TH.lift . mangle . pack . nameBase
DatatypeInfo{..} <- reifyDatatype adt
mapName <- newName $ "pathPieceConstructorMap" <> nameBase adt
let
toClause ConstructorInfo{..} = do
vars <- mapM (const $ newName "x") constructorFields
clause [conP constructorName $ map varP vars] (normalB [e|Text.intercalate joinPP $ $(mangle' constructorName) : $(listE $ map (\v -> [e|toPathPiece $(varE v)|]) vars)|]) []
fromClause = do
constrName <- newName "c"
argsName <- newName "args"
clause [viewP [e|Text.splitOn joinPP|] $ infixP (varP constrName) '(:) (varP argsName)]
(normalB [e|HashMap.lookup $(varE constrName) $(varE mapName) >>= ($ $(varE argsName))|])
[]
finDecs =
[ pragInlD mapName NoInline FunLike AllPhases
, sigD mapName [t|HashMap Text ([Text] -> Maybe $(conT adt))|]
, funD mapName
[ clause [] (normalB finClause) [] ]
]
where finClause = ([e|HashMap.fromList|] `appE`) . listE $ map listItem datatypeCons
listItem ConstructorInfo{..} = do
vars <- mapM (const $ newName "x") constructorFields
tupE [ mangle' constructorName
, lamCaseE
[ match (listP $ map varP vars)
(normalB $ case vars of
[] -> [e|Just $(conE constructorName)|]
v : vs -> foldl' (\acc v' -> [e|$(acc) <*> fromPathPiece $(varE v')|]) [e|$(conE constructorName) <$> fromPathPiece $(varE v)|] vs
)
[]
, match wildP (normalB [e|Nothing|]) []
]
]
sequence . (finDecs ++ ) . pure $
instanceD (cxt []) [t|PathPiece $(conT adt)|]
[ funD 'toPathPiece
(map toClause datatypeCons)
, funD 'fromPathPiece
[ fromClause
, clause [wildP] (normalB [e|Nothing|]) []
]
]
splitCamel :: Textual t => t -> [t] splitCamel :: Textual t => t -> [t]

View File

@ -5,7 +5,19 @@ $if not (null activeCategories)
<div .recipient-category> <div .recipient-category>
<input type=checkbox id=#{checkedIdent category} .recipient-category__checkbox :elem category checkedCategories:checked> <input type=checkbox id=#{checkedIdent category} .recipient-category__checkbox :elem category checkedCategories:checked>
<label .recipient-category__label for=#{checkedIdent category}> <label .recipient-category__label for=#{checkedIdent category}>
_{category} $case category
$of RecipientCustom
_{MsgRecipientCustom}
$of RecipientGroup RGCourseParticipants
_{MsgRGCourseParticipants}
$of RecipientGroup RGCourseLecturers
_{MsgRGCourseLecturers}
$of RecipientGroup RGCourseCorrectors
_{MsgRGCourseCorrectors}
$of RecipientGroup RGCourseTutors
_{MsgRGCourseTutors}
$of RecipientGroup (RGTutorialParticipants tutid)
^{rgTutorialParticipantsCaption tutid}
$if hasContent category $if hasContent category
<fieldset .recipient-category__fieldset uw-interactive-fieldset .interactive-fieldset__target data-conditional-input=#{checkedIdent category}> <fieldset .recipient-category__fieldset uw-interactive-fieldset .interactive-fieldset__target data-conditional-input=#{checkedIdent category}>
@ -18,5 +30,5 @@ $if not (null activeCategories)
$forall optIx <- categoryIndices category $forall optIx <- categoryIndices category
^{cellWdgts ! optIx} ^{cellWdgts ! optIx}
$maybe addWdgt <- addWdgts !? (1, (EnumPosition category, 0)) $maybe addWdgt <- addWdgts !? (1, (BoundedPosition category, 0))
^{addWdgt} ^{addWdgt}