module Handler.Term
( getTermShowR
, getTermEditR, postTermEditR
, getTermEditExistR, postTermEditExistR
) where
import Import
import Utils.Course (mayViewCourse)
import Handler.Utils
import qualified Data.Map.Strict as Map
import Data.Map.Strict ((!))
import qualified Database.Esqueleto.Legacy as E
import qualified Data.Set as Set
import qualified Control.Monad.State.Class as State
import Data.Time.Calendar.WeekDate
data TermDay
= TermDayStart | TermDayEnd
| TermDayLectureStart | TermDayLectureEnd
deriving (Eq, Ord, Read, Show, Enum, Bounded, Generic, Typeable)
deriving anyclass (Universe, Finite)
guessDay :: TermIdentifier
-> TermDay
-> Day
guessDay TermIdentifier{ year, season = Winter } TermDayStart
= fromGregorian year 10 1
guessDay TermIdentifier{ year, season = Winter } TermDayEnd
= fromGregorian (succ year) 3 31
guessDay TermIdentifier{ year, season = Summer } TermDayStart
= fromGregorian year 4 1
guessDay TermIdentifier{ year, season = Summer } TermDayEnd
= fromGregorian year 9 30
guessDay tid@TermIdentifier{ year, season = Winter } TermDayLectureStart
= fromWeekDate year (wWeekStart + 2) 1
where (_, wWeekStart, _) = toWeekDate $ guessDay tid TermDayStart
guessDay tid@TermIdentifier{ year, season = Winter } TermDayLectureEnd
= fromWeekDate (succ year) ((wWeekStart + 21) `div` bool 53 54 longYear) 5
where longYear = is _Just $ fromWeekDateValid year 53 1
(_, wWeekStart, _) = toWeekDate $ guessDay tid TermDayStart
guessDay tid@TermIdentifier{ year, season = Summer } TermDayLectureStart
= fromWeekDate year (wWeekStart + 2) 1
where (_, wWeekStart, _) = toWeekDate $ guessDay tid TermDayStart
guessDay tid@TermIdentifier{ year, season = Summer } TermDayLectureEnd
= fromWeekDate year (wWeekStart + 17) 5
where (_, wWeekStart, _) = toWeekDate $ guessDay tid TermDayStart
validateTerm :: (MonadHandler m, HandlerSite m ~ UniWorX)
=> FormValidator TermForm m ()
validateTerm = do
TermForm{..} <- State.get
guardValidation MsgTermStartMustMatchName $ tfStart `withinTerm` tfName
guardValidation MsgTermEndMustBeAfterStart $ tfStart < tfEnd
guardValidation MsgTermLectureEndMustBeAfterStart $ tfLectureStart < tfLectureEnd
guardValidation MsgTermStartMustBeBeforeLectureStart $ tfStart <= tfLectureStart
guardValidation MsgTermEndMustBeAfterLectureEnd $ tfEnd >= tfLectureEnd
getTermShowR :: Handler Html
getTermShowR = do
muid <- maybeAuthId
now <- liftIO getCurrentTime
ata <- getSessionActiveAuthTags
table <- runDB $
let termDBTable = DBTable{..}
where dbtSQLQuery term = return (term, courseCount, isActive)
where courseCount = E.subSelectCount . E.from $ \(course `E.LeftOuterJoin` allocation) -> do
E.on . E.exists . E.from $ \allocationCourse ->
E.where_ $ allocationCourse E.^. AllocationCourseCourse E.==. course E.^. CourseId
E.&&. E.just (allocationCourse E.^. AllocationCourseAllocation) E.==. allocation E.?. AllocationId
E.where_ $ term E.^. TermId E.==. course E.^. CourseTerm
E.&&. mayViewCourse muid ata now course (allocation E.?. AllocationId)
isActive = termIsActiveE (E.val now) (E.val muid) (term E.^. TermId)
dbtRowKey = (E.^. TermId)
dbtProj = dbrOutput <$> dbtProjId
dbtColonnade = widgetColonnade $ mconcat
[ sortable (Just "term-id") (i18nCell MsgTermShort) $ \(Entity tid _, _, _)
-> cell $ do
mayEdit <- hasWriteAccessTo $ TermEditExistR tid
[whamlet|
$newline never
#{toPathPiece tid}
$if mayEdit
#{iconMenuAdmin}
|]
, sortable (Just "lecture-start") (i18nCell MsgLectureStart) $ \(Entity _ Term{..},_,_)
-> cell $ formatTime SelFormatDate termLectureStart >>= toWidget
, sortable (Just "lecture-end") (i18nCell MsgTermLectureEnd) $ \(Entity _ Term{..},_,_)
-> cell $ formatTime SelFormatDate termLectureEnd >>= toWidget
, sortable Nothing (i18nCell MsgTermActive) $ \(_, _, E.Value isActive)
-> tickmarkCell isActive
, sortable Nothing (i18nCell MsgTermCourseCount) $ \(_, E.Value numCourses, _)
-> cell [whamlet|_{MsgNumCourses numCourses}|]
, sortable (Just "start") (i18nCell MsgTermStart) $ \(Entity _ Term{..},_, _)
-> cell $ formatTime SelFormatDate termStart >>= toWidget
, sortable (Just "end") (i18nCell MsgTermEnd) $ \(Entity _ Term{..},_, _)
-> cell $ formatTime SelFormatDate termEnd >>= toWidget
, sortable Nothing (i18nCell MsgTermHolidays) $ \(Entity _ Term{..},_, _)
-> cell $ do
let termHolidays' = groupHolidays termHolidays
[whamlet|
$newline never
$forall holiday <- termHolidays'
$case holiday
$of Left singleHoliday
- ^{formatTimeW SelFormatDate singleHoliday}
$of Right (startD, endD)
-
^{formatTimeW SelFormatDate startD}
–
^{formatTimeW SelFormatDate endD}
|]
]
dbtSorting = Map.fromList
[ ( "start"
, SortColumn $ \term -> term E.^. TermStart
)
, ( "end"
, SortColumn $ \term -> term E.^. TermEnd
)
, ( "lecture-start"
, SortColumn $ \term -> term E.^. TermLectureStart
)
, ( "lecture-end"
, SortColumn $ \term -> term E.^. TermLectureEnd
)
, ( "term-id"
, SortColumn $ \term -> term E.^. TermId
)
]
dbtFilter = Map.fromList
[ ( "active"
, FilterColumn $ \term -> termIsActiveE (E.val now) (E.val muid) (term E.^. TermId) :: E.SqlExpr (E.Value Bool)
)
, ( "course"
, FilterColumn $ \term csh -> case csh of -- FilterColumn-Lambdas are
[] -> E.val True :: E.SqlExpr (E.Value Bool)
cshs -> E.exists . E.from $ \course ->
E.where_ $ course E.^. CourseTerm E.==. term E.^. TermId
E.&&. course E.^. CourseShorthand `E.in_` E.valList cshs
)
]
dbtFilterUI = mempty
dbtStyle = def
dbtParams = def
dbtIdent = "terms" :: Text
dbtCsvEncode = noCsvEncode
dbtCsvDecode = Nothing
dbtExtraReps = []
termDBTableValidator = def & defaultSorting [SortDescBy "term-id"]
in dbTableWidget' termDBTableValidator termDBTable
defaultLayout $ do
setTitleI MsgTermsHeading
$(widgetFile "terms")
getTermEditR, postTermEditR :: Handler Html
getTermEditR = postTermEditR
postTermEditR = do
mbLastTerm <- runDB $ selectFirst [] [Desc TermName]
let template = case mbLastTerm of
Nothing -> mempty
(Just Entity{ entityVal=Term{..}})
-> let ntid = succ termName
in mempty
{ tftName = Just ntid
, tftStart = Just $ guessDay ntid TermDayStart
, tftEnd = Just $ guessDay ntid TermDayEnd
, tftLectureStart = Just $ guessDay ntid TermDayLectureStart
, tftLectureEnd = Just $ guessDay ntid TermDayLectureEnd
}
termEditHandler Nothing template
getTermEditExistR, postTermEditExistR :: TermId -> Handler Html
getTermEditExistR = postTermEditExistR
postTermEditExistR tid = do
(term, active) <- runDB $ do
term <- get tid
active <- selectList [ TermActiveTerm ==. tid ] []
return (term, map entityVal active)
termEditHandler (Just tid) $ termToTemplate term (map termActiveToForm active)
termEditHandler :: Maybe TermId -> TermFormTemplate -> Handler Html
termEditHandler mtid template = do
eHandler <- fromMaybe (error "termEditHandler called from 404-handler") <$> getCurrentRoute
((result, formWidget), formEnctype) <- runFormPost $ newTermForm mtid template
case result of
(FormSuccess TermForm{..}) -> exceptT (addMessageI Error) return $ do
let tid = TermKey tfName
hoist runDB $ do
let term = Term
{ termName = tfName
, termStart = tfStart
, termEnd = tfEnd
, termHolidays = tfHolidays
, termLectureStart = tfLectureStart
, termLectureEnd = tfLectureEnd
}
case mtid of
Just oTId | tid == oTId ->
lift $ replace tid term
_other -> do
whenM (lift $ exists [TermName ==. tfName]) $
throwE MsgTermExists
lift $ insertKey tid term
lift $ deleteWhere [TermActiveTerm ==. tid]
forM_ tfActive $ \TermActiveForm{..} ->
lift $ insert_ TermActive
{ termActiveTerm = tid
, termActiveFrom = tafFrom
, termActiveTo = tafTo
, termActiveFor = tafFor
}
lift . audit $ TransactionTermEdit tid
addMessageI Success $ MsgTermEdited tid
redirect TermShowR
FormMissing -> return ()
(FormFailure _) -> addMessageI Warning MsgInvalidInput
defaultLayout $ do
setTitleI MsgTermEditHeading
wrapForm formWidget def
{ formAction = Just $ SomeRoute eHandler
, formEncoding = formEnctype
}
data TermFormTemplate = TermFormTemplate
{ tftName :: Maybe TermIdentifier
, tftStart :: Maybe Day
, tftEnd :: Maybe Day
, tftHolidays :: Maybe [Day]
, tftLectureStart :: Maybe Day
, tftLectureEnd :: Maybe Day
, tftActive :: Maybe [TermActiveForm]
}
-- | TermFormTemplates form a pointwise-left biased Semigroup
instance Semigroup TermFormTemplate where
l <> r = TermFormTemplate
{ tftName = tftName l <|> tftName r
, tftStart = tftStart l <|> tftStart r
, tftEnd = tftEnd l <|> tftEnd r
, tftHolidays = tftHolidays l <|> tftHolidays r
, tftLectureStart = tftLectureStart l <|> tftLectureStart r
, tftLectureEnd = tftLectureEnd l <|> tftLectureEnd r
, tftActive = tftActive l <|> tftActive r
}
instance Monoid TermFormTemplate where
mappend = (<>)
mempty = TermFormTemplate
{ tftName = Nothing
, tftStart = Nothing
, tftEnd = Nothing
, tftHolidays = Nothing
, tftLectureStart = Nothing
, tftLectureEnd = Nothing
, tftActive = Nothing
}
termToTemplate :: Maybe Term -> [TermActiveForm] -> TermFormTemplate
termToTemplate Nothing active = mempty { tftActive = Just active }
termToTemplate (Just Term{..}) active = TermFormTemplate
{ tftName = Just termName
, tftStart = Just termStart
, tftEnd = Just termEnd
, tftHolidays = Just termHolidays
, tftLectureStart = Just termLectureStart
, tftLectureEnd = Just termLectureEnd
, tftActive = Just active
}
data TermActiveForm = TermActiveForm
{ tafFrom :: UTCTime
, tafTo :: Maybe UTCTime
, tafFor :: Maybe UserId
} deriving (Eq)
termActiveToForm :: TermActive -> TermActiveForm
termActiveToForm TermActive{..} = TermActiveForm
{ tafFrom = termActiveFrom
, tafTo = termActiveTo
, tafFor = termActiveFor
}
data TermForm = TermForm
{ tfName :: TermIdentifier
, tfStart :: Day
, tfEnd :: Day
, tfHolidays :: [Day]
, tfLectureStart :: Day
, tfLectureEnd :: Day
, tfActive :: [TermActiveForm]
}
newTermForm :: Maybe TermId -> TermFormTemplate -> Form TermForm
newTermForm mtid template = validateForm validateTerm $ \html -> do
cRoute <- fromMaybe (error "newTermForm called from 404-Handler") <$> getCurrentRoute
mr <- getMessageRender
let
tidForm
| Just tid <- unTermKey <$> mtid
= aforced termNewField (fslpI MsgTerm (mr MsgTermPlaceholder)) tid
| otherwise
= areq termNewField (fslpI MsgTerm (mr MsgTermPlaceholder)) (tftName template)
holidayForm = massInputAccumA miAdd miCell miButtonAction miLayout miIdent fSettings fRequired
where
miAdd mkUnique submitView csrf = do
(fromRes, fromView) <- mpreq dayField ("" & addName (mkUnique "from") & addPlaceholder (mr MsgTermHolidayPlaceholder)) Nothing
(toRes, toView) <- mopt dayField ("" & addName (mkUnique "to") & addPlaceholder (mr MsgTermHolidayPlaceholder)) Nothing
let
holidaysRes = case (fromRes, toRes) of
(FormSuccess f, FormSuccess (Just t)) -> FormSuccess $ Right (f, t)
(FormSuccess f, FormSuccess Nothing) -> FormSuccess $ Left f
(FormSuccess f, FormMissing) -> FormSuccess $ Left f
(f, tRes) -> fmap Left f <* tRes
holidaysRes' = holidaysRes <&> \newDat oldDat -> if
| let newUngrouped = Set.fromList . ungroupHolidays $ pure newDat
oldUngrouped = Set.fromList $ ungroupHolidays oldDat
, newUngrouped `Set.isSubsetOf` oldUngrouped
-> FormFailure [mr MsgTermFormHolidaysAlreadyAdded]
| otherwise
-> FormSuccess $ pure newDat
return (holidaysRes', $(widgetFile "term/holiday-mass-input/add"))
miCell x = $(widgetFile "term/holiday-mass-input/cell")
where (f, t) = case x of
Left d -> (d, Nothing)
Right (f', t') -> (f', Just t')
miButtonAction :: forall p. PathPiece p => p -> Maybe (SomeRoute UniWorX)
miButtonAction = Just . SomeRoute . (cRoute :#:)
miLayout :: MassInputLayout ListLength (Either Day (Day, Day)) ()
miLayout lLength _ cellWdgts delButtons addWdgts = $(widgetFile "term/holiday-mass-input/layout")
miIdent :: Text
miIdent = "holidays"
fSettings = fslI MsgTermHolidays
fRequired = False
ungroupHolidays = foldMap $ \case
Left d -> pure d
Right (f, t) -> [f..t]
activeForm = massInputAccumA miAdd miCell miButtonAction miLayout miIdent fSettings fRequired
where
miAdd mkUnique submitView csrf = do
(fromRes, fromView) <- mpreq utcTimeField ("" & addName (mkUnique "from")) Nothing
(toRes, toView) <- mopt utcTimeField ("" & addName (mkUnique "to")) Nothing
(forRes, forView) <- mopt (checkMap (first $ const MsgTermFormActiveUserNotFound) Right $ userField False Nothing) ("" & addName (mkUnique "for")) Nothing
let res = TermActiveForm <$> fromRes <*> toRes <*> forRes
res' = res <&> \newDat oldDat -> if
| newDat `elem` oldDat
-> FormFailure [mr MsgTermFormActiveAlreadyAdded]
| otherwise
-> FormSuccess $ pure newDat
return (res', $(widgetFile "term/active-mass-input/add"))
miCell TermActiveForm{..} = do
user <- for tafFor $ liftHandler . runDB . get404
$(widgetFile "term/active-mass-input/cell")
miButtonAction :: forall p. PathPiece p => p -> Maybe (SomeRoute UniWorX)
miButtonAction = Just . SomeRoute . (cRoute :#:)
miLayout :: MassInputLayout ListLength TermActiveForm ()
miLayout lLength _ cellWdgts delButtons addWdgts = $(widgetFile "term/active-mass-input/layout")
miIdent :: Text
miIdent = "active-periods"
fSettings = fslI MsgTermActive
fRequired = False
flip (renderAForm FormStandard) html $ TermForm
<$> tidForm
<*> areq dayField (fslI MsgTermStartDay & setTooltip MsgTermStartDayTooltip) (tftStart template)
<*> areq dayField (fslI MsgTermEndDay & setTooltip MsgTermEndDayTooltip) (tftEnd template)
<*> (ungroupHolidays <$> holidayForm (groupHolidays <$> tftHolidays template))
<*> areq dayField (fslI MsgTermLectureStart) (tftLectureStart template)
<*> areq dayField (fslI MsgTermLectureEnd & setTooltip MsgTermLectureEndTooltip) (tftLectureEnd template)
<*> activeForm (tftActive template)
deriveJSON defaultOptions
{ fieldLabelModifier = camelToPathPiece' 1
} ''TermActiveForm