{-# LANGUAGE CPP #-} {- Copyright (C) 2013-2016 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 . -} {- | [@AUTHOR@] Dr. Alistair Ward [@DESCRIPTION@] Describes the attributes of a /teacher/. [@TODO@] Some teachers work half days, or even just specific /time-slot/s; so 'getWorkingWeek' is an inadequate description. -} module WeekDaze.Data.Teacher( -- * Types -- ** Type-synonyms Service, -- ** Data-types Profile( -- MkProfile, getService, getMaybeOwnLocationId, -- getWorkingWeek, -- getMaximumTeachingRatio, getMaybeSpecialtyTopic, -- getGroupMembership, getMaybeFreePeriodPreference ), -- * Constants -- tag, maximumTeachingRatioTag, serviceTag, specialtyTopicTag, -- defaultService, -- * Functions lookupCourseIn, lookupSuitableCourse, findSpecifiedTimes, unsubscribe, -- ** Constructor mkProfile, -- ** Predicates offersSuitableCourse, hasAnyIdealTimeslotRequest, hasAnySpecificTimeRequest, hasSpecialtyTopic, inhabits, isSpecialistIn, offersAnySynchronisedCourse, offersService ) where import Control.Arrow((&&&)) import qualified Control.DeepSeq import qualified Data.Foldable import qualified Data.List.Extra import qualified Data.Maybe import qualified Data.Set import qualified Text.XML.HXT.Arrow.Pickle as HXT import qualified ToolShed.SelfValidate import qualified WeekDaze.Data.Course as Data.Course import qualified WeekDaze.Data.Group as Data.Group import qualified WeekDaze.Data.HumanResource as Data.HumanResource import qualified WeekDaze.Data.Resource as Data.Resource import qualified WeekDaze.Data.Subject as Data.Subject import qualified WeekDaze.Size as Size import qualified WeekDaze.Temporal.Availability as Temporal.Availability import qualified WeekDaze.Temporal.FreePeriodPreference as Temporal.FreePeriodPreference import qualified WeekDaze.Temporal.Time as Temporal.Time import qualified WeekDaze.Temporal.TimeslotRequest as Temporal.TimeslotRequest -- | Used to qualify XML. tag :: String tag = "teacherProfile" -- | Used to qualify SQL & XML. maximumTeachingRatioTag :: String maximumTeachingRatioTag = "maximumTeachingRatio" -- | Used to qualify XML. serviceTag :: String serviceTag = "service" -- | Used to qualify XML. specialtyTopicTag :: String specialtyTopicTag = "specialtyTopic" {- | * A /teacher/ offers a set of /course/s. * Whilst they may perform a reserve-role in some additional /subject/, this isn't going to affect the /timetable/. -} type Service synchronisationId level timeslotId = Data.Set.Set (Data.Course.Course synchronisationId level timeslotId) -- | The default value for 'getService'. defaultService :: Service synchronisationId level timeslotId defaultService = Data.Set.empty -- | The service offered by a /teacher/. data Profile synchronisationId level timeslotId locationId teachingRatio = MkProfile { getService :: Service synchronisationId level timeslotId, -- ^ The set of /course/s offered. {- | * A /teacher/ may have their own personal /location/ (i.e. a /form-room/). * Absence of this field implies that they're nomadic, & may use any free 'locationId', appropriate for the /course/; however inconveniently located. * A personal /location/ can't be assumed to have all the /facilities/ required for the /service/ offered. * CAVEAT: it's conceivable that some /teacher/s share a /location/, but each one still only uses that /location/. -} getMaybeOwnLocationId :: Maybe locationId, -- ^ A /teacher/ may have a fixed abode. getWorkingWeek :: Temporal.Availability.Availability, -- ^ A /teacher/ may be part-time. {- | * The maximum ratio, in the closed unit-interval [0,1], of a /teacher/'s working week (rather than the whole week), which is devoted to teaching. The remainder may be administration or attendance at /meetings/. This concept may also be required, to allow ad-hoc re-scheduling, to compensate for absentees. * This concept differs from a /part-time teacher/, who teaches for a static /contractually specified/ portion of the week. * When the type-parameter is a /Rational/ & the denominator is the number of /lesson/s per week, specification is in the basic time-units of the timetable. * The value can't be exceeded, but may not be reached, thus allowing at least the required time for the administration. -} getMaximumTeachingRatio :: teachingRatio, getGroupMembership :: Data.Group.Membership, -- ^ A /teacher/ may meet regularly with various /group/s. getMaybeSpecialtyTopic :: Maybe Data.Subject.Topic, -- ^ A /teacher/ may specialise in a specific /topic/, rather than being a generalist. getMaybeFreePeriodPreference :: Maybe Temporal.FreePeriodPreference.FreePeriodPreference -- ^ A /teacher/ may have a preference for the position within each /day/, of unallocated /time-slot/s. } deriving (Eq, Ord) instance (Show synchronisationId, Show level, Show timeslotId, Show locationId, Show teachingRatio) => Show (Profile synchronisationId level timeslotId locationId teachingRatio) where showsPrec _ MkProfile { getService = service, getMaybeOwnLocationId = maybeOwnLocationId, getWorkingWeek = workingWeek, getMaximumTeachingRatio = maximumTeachingRatio, getGroupMembership = groupMembership, getMaybeSpecialtyTopic = maybeSpecialtyTopic, getMaybeFreePeriodPreference = maybeFreePeriodPreference } = showString tag . showString "={" . showString serviceTag . showChar '=' . shows ( Data.Set.toList service ) . showString ",locationId=" . showString ( Data.Maybe.maybe "" show maybeOwnLocationId ) . showChar ',' . showString Temporal.Availability.tag . showChar '=' . shows workingWeek . showChar ',' . showString maximumTeachingRatioTag . showChar '=' . shows maximumTeachingRatio . showChar ',' . showString Data.HumanResource.groupMembershipTag . showChar '=' . shows ( Data.Set.toList groupMembership ) . showChar ',' . showString specialtyTopicTag . showChar '=' . showString ( Data.Maybe.maybe "" show maybeSpecialtyTopic ) . showChar ',' . showString Temporal.FreePeriodPreference.tag . showChar '=' . showString ( Data.Maybe.maybe "" show maybeFreePeriodPreference ) . showChar '}' instance ( Ord level, #if !MIN_VERSION_containers(0,5,2) Ord synchronisationId, #endif Ord timeslotId, Real teachingRatio, Show level, Show synchronisationId, Show timeslotId ) => ToolShed.SelfValidate.SelfValidator (Profile synchronisationId level timeslotId locationId teachingRatio) where getErrors profile@MkProfile { getService = service, getWorkingWeek = workingWeek, getMaximumTeachingRatio = maximumTeachingRatio, getMaybeSpecialtyTopic = maybeSpecialtyTopic } | not $ ToolShed.SelfValidate.isValid service = ToolShed.SelfValidate.getErrors service | not $ ToolShed.SelfValidate.isValid workingWeek = ToolShed.SelfValidate.getErrors workingWeek | otherwise = ToolShed.SelfValidate.extractErrors [ ( Data.List.Extra.anySame . map Data.Course.getSubject $ Data.Set.toList service, "multiple courses in the same " ++ show Data.Subject.tag ++ " have been offered in this teacher's " ++ show serviceTag ++ "; " ++ show (Data.Set.toList service) ), ( any ($ maximumTeachingRatio) [(< 0), (> 1)], maximumTeachingRatioTag ++ "=" ++ show (realToFrac maximumTeachingRatio :: Double {-hide the actual type-}) ++ " must be within the closed unit-interval '[0,1]'" -- A staff-member can be purely administrative, since they could be a member of a group. ), ( maybeSpecialtyTopic == Just "", "explicitly null " ++ show specialtyTopicTag ), ( Data.Maybe.maybe False (`Data.Set.notMember` Data.Set.map (Data.Subject.getTopic . Data.Course.getSubject) service) maybeSpecialtyTopic, "a teacher's " ++ show specialtyTopicTag ++ " " ++ show (Data.Maybe.fromJust maybeSpecialtyTopic) ++ " must be offered as a " ++ show serviceTag ++ "; " ++ show (Data.Set.toList service) ), ( Data.List.Extra.anySame $ Data.Foldable.concatMap (Data.Set.toList . Temporal.TimeslotRequest.getSpecifiedTimes . Data.Course.getTimeslotRequest) service, "there's an overlap in the " ++ show Temporal.TimeslotRequest.specificallyTag ++ " requested booking-times, of the courses offered in the " ++ show serviceTag ++ "; " ++ show (Data.Set.toList service) ), ( uncurry (&&) $ (offersService &&& (== 0) . getMaximumTeachingRatio) profile, "a portion of the working week must be allocated to teaching " ++ show (Data.Set.toList service) ), ( uncurry (&&) $ (not . offersService &&& (> 0) . getMaximumTeachingRatio) profile, maximumTeachingRatioTag ++ "=" ++ show (realToFrac maximumTeachingRatio :: Double {-hide the actual type-}) ++ " of " ++ Temporal.Availability.tag ++ "=" ++ show workingWeek ++ " can't be filled without offering a service" ) ] instance Data.Resource.Resource (Profile synchronisationId level timeslotId locationId teachingRatio) where getAvailability = getWorkingWeek instance RealFrac teachingRatio => Data.HumanResource.HumanResource (Profile synchronisationId level timeslotId locationId teachingRatio) where getNTimeslotsPerWeekOfTeaching nTimeslotsPerDay = round . uncurry (*) . (getMaximumTeachingRatio &&& fromIntegral . Data.HumanResource.calculateNTimeslotsPerWeekAvailable nTimeslotsPerDay) getNTimeslotsPerWeekOfNonTeaching nTimeslotsPerDay = uncurry (-) . ( Data.HumanResource.calculateNTimeslotsPerWeekAvailable nTimeslotsPerDay &&& Data.HumanResource.getNTimeslotsPerWeekOfTeaching nTimeslotsPerDay ) -- Derivation from 'getNTimeslotsPerWeekOfTeaching' ensures they are compatible in the face of rounding. getGroupMembership = getGroupMembership getMaybeFreePeriodPreference = getMaybeFreePeriodPreference instance ( HXT.XmlPickler level, HXT.XmlPickler locationId, HXT.XmlPickler synchronisationId, HXT.XmlPickler teachingRatio, HXT.XmlPickler timeslotId, Ord level, Ord synchronisationId, Ord timeslotId, Real teachingRatio, Show level, Show synchronisationId, Show timeslotId ) => HXT.XmlPickler (Profile synchronisationId level timeslotId locationId teachingRatio) where xpickle = HXT.xpElem tag . HXT.xpWrap ( \(a, b, c, d, e, f, g) -> mkProfile a b c d e f g, -- Construct from a tuple. \MkProfile { getService = service, getMaybeOwnLocationId = maybeOwnLocationId, getWorkingWeek = workingWeek, getMaximumTeachingRatio = maximumTeachingRatio, getGroupMembership = groupMembership, getMaybeSpecialtyTopic = maybeSpecialtyTopic, getMaybeFreePeriodPreference = maybeFreePeriodPreference } -> ( service, maybeOwnLocationId, workingWeek, maximumTeachingRatio, groupMembership, maybeSpecialtyTopic, maybeFreePeriodPreference ) -- Deconstruct to a tuple. ) $ HXT.xp7Tuple ( HXT.xpDefault defaultService . HXT.xpElem serviceTag . HXT.xpWrap ( Data.Set.fromList, -- Construct from a List. Data.Set.toList -- Deconstruct to a List. ) $ HXT.xpList1 {-the default is null-} HXT.xpickle ) ( HXT.xpOption HXT.xpickle -- maybeOwnLocationId. ) HXT.xpickle {-workingWeek-} ( HXT.xpAttr maximumTeachingRatioTag HXT.xpickle ) ( HXT.xpDefault Data.HumanResource.defaultGroupMembership . HXT.xpElem Data.HumanResource.groupMembershipTag . HXT.xpWrap ( Data.Set.fromList, -- Construct from a List. Data.Set.toList -- Deconstruct to a List. ) . HXT.xpList1 {-the default is null-} . HXT.xpElem Data.Group.memberTag $ HXT.xpTextAttr Data.Group.groupIdTag ) ( HXT.xpOption $ HXT.xpTextAttr specialtyTopicTag {-can't be null-} ) ( HXT.xpOption HXT.xpickle -- maybeFreePeriodPreference. ) instance ( Control.DeepSeq.NFData level, Control.DeepSeq.NFData locationId, Control.DeepSeq.NFData synchronisationId, Control.DeepSeq.NFData teachingRatio, Control.DeepSeq.NFData timeslotId ) => Control.DeepSeq.NFData (Profile synchronisationId level timeslotId locationId teachingRatio) where rnf (MkProfile x0 x1 x2 x3 x4 x5 x6) = Control.DeepSeq.rnf (x0, x1, x2, x3, x4, x5, x6) -- | Smart constructor. mkProfile :: ( Ord level, Ord timeslotId, Real teachingRatio, Show level, Show synchronisationId, Show timeslotId ) => Service synchronisationId level timeslotId -- ^ The set of /course/s offered. -> Maybe locationId -- ^ Personal classroom. -> Temporal.Availability.Availability -- ^ The /day/s on which the /teacher/ is contracted to work. -> teachingRatio -- ^ The maximum ratio of a working-week actually devoted to teaching. -> Data.Group.Membership -- ^ The /group/s of which this /teacher/ is a member. -> Maybe Data.Subject.Topic -- ^ Specialty topic. -> Maybe Temporal.FreePeriodPreference.FreePeriodPreference -- ^ Any preference for the position within each /day/, of free /time-slot/s. -> Profile synchronisationId level timeslotId locationId teachingRatio mkProfile service maybeOwnLocationId workingWeek maximumTeachingRatio groupMembership maybeSpecialtyTopic maybeFreePeriodPreference | ToolShed.SelfValidate.isValid profile = profile | otherwise = error $ "WeekDaze.Data.Teacher.mkProfile:\t" ++ ToolShed.SelfValidate.getFirstError profile ++ "." where profile = MkProfile { getService = service, getMaybeOwnLocationId = maybeOwnLocationId, getWorkingWeek = workingWeek, getMaximumTeachingRatio = maximumTeachingRatio, getGroupMembership = groupMembership, getMaybeSpecialtyTopic = maybeSpecialtyTopic, getMaybeFreePeriodPreference = maybeFreePeriodPreference } -- | Extract any /course/ from the specified /profile/, which matches the specified /subject/. lookupCourseIn :: Eq level => Data.Subject.Subject level -> Profile synchronisationId level timeslotId locationId teachingRatio -> Maybe (Data.Course.Course synchronisationId level timeslotId) lookupCourseIn subject = Data.Foldable.find ((== subject) . Data.Course.getSubject) . getService -- | Extract any /course/ from the specified /profile/, which matches the specified /subject/ & caters for the required number of /student/s. lookupSuitableCourse :: Eq level => Size.NStudents -> Data.Subject.Subject level -> Profile synchronisationId level timeslotId locationId teachingRatio -> Maybe (Data.Course.Course synchronisationId level timeslotId) lookupSuitableCourse nStudents subject = Data.Foldable.find (Data.Course.isSuitable nStudents subject) . getService -- | Find the set of all specified /time/s, for any of the /course/s offered. findSpecifiedTimes :: Ord timeslotId => Profile synchronisationId level timeslotId locationId teachingRatio -> Temporal.Time.TimeSet timeslotId findSpecifiedTimes = Data.Set.foldr (Data.Set.union . Temporal.TimeslotRequest.getSpecifiedTimes . Data.Course.getTimeslotRequest) Data.Set.empty . getService -- | True if the /teacher/ offers a /course/ in the required /subject/ & can cater for the required number of /student/s. offersSuitableCourse :: Eq level => Size.NStudents -> Data.Subject.Subject level -> Profile synchronisationId level timeslotId locationId teachingRatio -> Bool offersSuitableCourse nStudents subject = Data.Foldable.any (Data.Course.isSuitable nStudents subject) . getService -- | True if the specified /teacher/ makes any /ideal timeslot-request/ amongst the /course/s offered in their /service/. hasAnyIdealTimeslotRequest :: Profile synchronisationId level timeslotId locationId teachingRatio -> Bool hasAnyIdealTimeslotRequest = Data.Foldable.any (Temporal.TimeslotRequest.isIdeally . Data.Course.getTimeslotRequest) . getService -- | True if the specified /teacher/ makes any /specific time-request/ amongst the /course/s offered in their /service/. hasAnySpecificTimeRequest :: Profile synchronisationId level timeslotId locationId teachingRatio -> Bool hasAnySpecificTimeRequest = Data.Foldable.any Data.Course.specifiesTimes . getService -- | True if the specified /teacher/ claims any /topic/ as their specialty. hasSpecialtyTopic :: Profile synchronisationId level timeslotId locationId teachingRatio -> Bool hasSpecialtyTopic = Data.Maybe.isJust . getMaybeSpecialtyTopic -- | True if the specified /teacher/ claims the specified /location/ as their own. inhabits :: Eq locationId => locationId -> Profile synchronisationId level timeslotId locationId teachingRatio -> Bool inhabits locationId = (== Just locationId) . getMaybeOwnLocationId -- | True if the /teacher/ specializes in the specified /topic/. isSpecialistIn :: Data.Subject.Topic -> Profile synchronisationId level timeslotId locationId teachingRatio -> Bool isSpecialistIn topic = (== Just topic) . getMaybeSpecialtyTopic -- | True if the specified /teacher/ offers any synchronised /course/. offersAnySynchronisedCourse :: Profile synchronisationId level timeslotId locationId teachingRatio -> Bool offersAnySynchronisedCourse = Data.Foldable.any Data.Course.isSynchronised . getService -- | True if the /teacher/ offers one or more /course/s in their /service/. offersService :: Profile synchronisationId level timeslotId locationId teachingRatio -> Bool offersService = not . Data.Set.null . getService -- | Unsubscribe from the specified set of /groups/. unsubscribe :: Data.Group.Membership -> Profile synchronisationId level timeslotId locationId teachingRatio -> Profile synchronisationId level timeslotId locationId teachingRatio unsubscribe groupMembership profile = profile { getGroupMembership = Data.Set.filter (`Data.Set.notMember` groupMembership) $ getGroupMembership profile }