weekdaze-0.0.0.1: src-lib/WeekDaze/Model/Meeting.hs
{-
Copyright (C) 2013-2015 Dr. Alistair Ward
This file is part of WeekDaze.
WeekDaze is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
WeekDaze is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with WeekDaze. If not, see <http://www.gnu.org/licenses/>.
-}
{- |
[@AUTHOR@] Dr. Alistair Ward
[@DESCRIPTION@] Defines a meeting of a /group/, suitable for any /time-slot/ in the generic /timetable/.
-}
module WeekDaze.Model.Meeting(
-- * Types
-- ** Type-synonyms
MeetingsByTime,
-- MeetingsByTimeMutator,
-- ** Data-types
Meeting(
-- MkMeeting,
getMaybeLocationId,
getStudentClass,
getTeacherIds,
getGroupId
),
-- * Constants
-- cssIdentifier,
-- resourceIdsCSSIdentifier,
-- * Functions
deleteLocationId,
deleteStudentBody,
deleteTeacherId,
-- ** Constructor
mkMeeting
) where
import Control.Arrow((&&&))
import qualified Data.List
import qualified Data.Map
import qualified Data.Maybe
import qualified Data.Set
import qualified Text.XHtml.Strict
import qualified ToolShed.SelfValidate
import qualified WeekDaze.Aggregate.StudentBody as Aggregate.StudentBody
import qualified WeekDaze.Aggregate.StudentClass as Aggregate.StudentClass
import qualified WeekDaze.Data.Group as Data.Group
import qualified WeekDaze.Data.HumanResource as Data.HumanResource
import qualified WeekDaze.Temporal.Time as Temporal.Time
import qualified WeekDaze.Text.CSS as Text.CSS
import Text.XHtml.Strict((+++), (<<), (!))
-- | Used to qualify output.
cssIdentifier :: Text.CSS.CSSIdentifier
cssIdentifier = "meeting"
-- | Used to qualify output.
resourceIdsCSSIdentifier :: Text.CSS.CSSIdentifier
resourceIdsCSSIdentifier = "resourceIds" -- CAVEAT: coordinate with 'Model.Lesson.resourceIdsCSSIdentifier'.
{- |
* Defines a potential /booking/ of the /resource/s required for a /group/ to meet.
* It doesn't directly specify the /time-slot/ at which it's to be booked, because that's defined by it's position within a /timetable/.
-}
data Meeting locationId teacherId = MkMeeting {
getGroupId :: Data.Group.Id, -- ^ The name of the /group/ which is meeting.
getMaybeLocationId :: Maybe locationId, -- ^ The optional /location/ at which the meeting will be held. Whilst this could be found by looking-up the /group/, containing it permits implementation of 'Text.XHtml.Strict.HTML'.
getStudentClass :: Aggregate.StudentClass.StudentClass, -- ^ The /student-bodies/ that will be attending.
getTeacherIds :: Data.Set.Set teacherId -- ^ The /teacher/s who will be attending.
} deriving (Eq, Ord, Show)
instance (
Text.XHtml.Strict.HTML locationId,
Text.XHtml.Strict.HTML teacherId
) => Text.XHtml.Strict.HTML (Meeting locationId teacherId) where
toHtml MkMeeting {
getGroupId = groupId,
getMaybeLocationId = maybeLocationId,
getStudentClass = studentClass,
getTeacherIds = teacherIds
} = Text.XHtml.Strict.thediv ! [
Text.XHtml.Strict.theclass cssIdentifier,
Text.XHtml.Strict.title cssIdentifier
] << (
Text.XHtml.Strict.thediv ! [
Text.XHtml.Strict.theclass Data.Group.groupIdTag,
Text.XHtml.Strict.title Data.Group.groupIdTag
] << (groupId +++ '.') +++ Text.XHtml.Strict.unordList (
Data.Maybe.catMaybes [
fmap terminate maybeLocationId,
if Data.Set.null studentClass
then Nothing
else Just . terminate . Data.List.intersperse separator . map Text.XHtml.Strict.toHtml $ Data.Set.toList studentClass,
if Data.Set.null teacherIds
then Nothing
else Just . terminate . Data.List.intersperse separator . map Text.XHtml.Strict.toHtml $ Data.Set.toList teacherIds
]
) ! [
Text.XHtml.Strict.theclass resourceIdsCSSIdentifier,
Text.XHtml.Strict.title resourceIdsCSSIdentifier
]
) where
separator = Text.XHtml.Strict.toHtml ", " -- There may be many list-elements, so don't use a non-breaking space.
terminate :: Text.XHtml.Strict.HTML html => html -> Text.XHtml.Strict.Html
terminate = (+++ '.')
instance (Show locationId, Show teacherId) => ToolShed.SelfValidate.SelfValidator (Meeting locationId teacherId) where
getErrors meeting = ToolShed.SelfValidate.extractErrors [
(
uncurry (&&) $ (Data.Set.null . getStudentClass &&& Data.Set.null . getTeacherIds) meeting, -- CAVEAT: arguably two is the minimum.
"a meeting can't have an empty " ++ show Data.HumanResource.groupMembershipTag ++ "; " ++ show meeting
) -- Pair.
]
-- | Smart constructor.
mkMeeting
:: (Show locationId, Show teacherId)
=> Data.Group.Id -- ^ The identifier of the /group/ which it meeting.
-> Maybe locationId -- ^ The optional /location/ of the rendezvous.
-> Aggregate.StudentClass.StudentClass -- ^ The potentially empty set of /student/-members of the /group/.
-> Data.Set.Set teacherId -- ^ The potentially empty set of /teacher/-members of the /group/.
-> Meeting locationId teacherId
mkMeeting groupId maybeLocationId studentClass teacherIds
| ToolShed.SelfValidate.isValid meeting = meeting
| otherwise = error $ "WeekDaze.Model.Meeting.mkMeeting:\t" ++ ToolShed.SelfValidate.getFirstError meeting ++ "."
where
meeting = MkMeeting groupId maybeLocationId studentClass teacherIds
-- | A map indexed by /time/, of sets of /meeting/s.
type MeetingsByTime timeslotId locationId teacherId = Data.Map.Map (Temporal.Time.Time timeslotId) (Data.Set.Set (Meeting locationId teacherId))
-- | A function used to mutate a 'MeetingsByTime'.
type MeetingsByTimeMutator timeslotId locationId teacherId = MeetingsByTime timeslotId locationId teacherId -> MeetingsByTime timeslotId locationId teacherId
-- | Delete the specified /location-Id/ from all specified meetings.
deleteLocationId :: (
Ord locationId,
Ord teacherId
) => locationId -> MeetingsByTimeMutator timeslotId locationId teacherId
deleteLocationId locationId = Data.Map.map (
Data.Set.map (
\meeting -> meeting {
getMaybeLocationId = Nothing -- Remove the locationId.
}
)
) . Data.Map.filter (
not . Data.Set.null -- Leave only times at which there's a relevant meeting.
) . Data.Map.map (
Data.Set.filter (
(== Just locationId) . getMaybeLocationId -- Leave only meetings relevant to this location.
)
)
-- | Delete the specified /student-body/ from the members of all specified meetings.
deleteStudentBody :: (
Ord locationId,
Ord teacherId
) => Aggregate.StudentBody.StudentBody -> MeetingsByTimeMutator timeslotId locationId teacherId
deleteStudentBody studentBody = Data.Map.map (
Data.Set.map (
\meeting -> meeting {
getStudentClass = Data.Set.delete studentBody $ getStudentClass meeting
}
)
) . Data.Map.filter (
not . Data.Set.null -- Leave only times at which there's a relevant meeting.
) . Data.Map.map (
Data.Set.filter (
Data.Set.member studentBody . getStudentClass -- Leave only meetings relevant to this student.
)
)
-- | Delete the specified /teacher-Id/ from the members of all specified meetings.
deleteTeacherId :: (
Ord locationId,
Ord teacherId
) => teacherId -> MeetingsByTimeMutator timeslotId locationId teacherId
deleteTeacherId teacherId = Data.Map.map (
Data.Set.map (
\meeting -> meeting {
getTeacherIds = Data.Set.delete teacherId $ getTeacherIds meeting
}
)
) . Data.Map.filter (
not . Data.Set.null -- Leave only times at which there's a relevant meeting.
) . Data.Map.map (
Data.Set.filter (
Data.Set.member teacherId . getTeacherIds -- Leave only meetings relevant to this teacher.
)
)