weekdaze-0.0.0.1: src-lib/WeekDaze/TeacherView/Timetable.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 the /timetable/ for any week, for all /teacher/s.
* It contains identical information to 'StudentView.Timetable.Timetable',
but the data has been re-indexed to present it in a form relevant to a /teacher/ rather than a /student/.
-}
module WeekDaze.TeacherView.Timetable(
-- * Types
-- ** Type-synonyms
Timetable,
-- Booking,
InterCampusMigrationsByTeacherId,
TimesByTeacherId,
-- * Functions
findStudentClassesByLocationId,
calculateMeanStudentClassSize,
countLocationChangesByTeacherId,
-- ** Mutators
bookStudentViewLesson,
-- bookTeacherViewLesson,
-- ** Translation
fromStudentViewTimetable,
toStudentViewTimetable,
toXHtml
) where
import Control.Arrow((&&&))
import Data.Map((!))
import qualified Control.Arrow
import qualified Data.Array.IArray
import qualified Data.Foldable
import qualified Data.Map
import qualified Data.Maybe
import qualified Data.Set
import qualified Data.Tuple
import qualified Factory.Math.Statistics
import qualified Text.XHtml.Strict
import qualified WeekDaze.Aggregate.StudentClass as Aggregate.StudentClass
import qualified WeekDaze.Aggregate.TeacherRegister as Aggregate.TeacherRegister
import qualified WeekDaze.Data.Course as Data.Course
import qualified WeekDaze.Data.HumanResource as Data.HumanResource
import qualified WeekDaze.Model.Lesson as Model.Lesson
import qualified WeekDaze.Model.Meeting as Model.Meeting
import qualified WeekDaze.Model.Timetable as Model.Timetable
import qualified WeekDaze.Model.TimetableForWeek as Model.TimetableForWeek
import qualified WeekDaze.Size as Size
import qualified WeekDaze.StudentView.LessonResourceIds as StudentView.LessonResourceIds
import qualified WeekDaze.StudentView.Timetable as StudentView.Timetable
import qualified WeekDaze.TeacherView.Lesson as TeacherView.Lesson
import qualified WeekDaze.TeacherView.LessonResourceIds as TeacherView.LessonResourceIds
import qualified WeekDaze.TeacherView.TimetableForWeek as TeacherView.TimetableForWeek
import qualified WeekDaze.Temporal.Time as Temporal.Time
import qualified WeekDaze.Text.CSS as Text.CSS
import Text.XHtml.Strict((+++), (<<))
import WeekDaze.Text.XHTML()
-- | An association-list of the timetables for all /teachers/.
type Timetable teacherId timeslotId locationId level = Model.Timetable.Timetable teacherId timeslotId (TeacherView.LessonResourceIds.LessonResourceIds locationId) level
{- |
* Builds a 'Timetable' by inverting a 'StudentView.Timetable.Timetable'.
* CAVEAT: in the original 'StudentView.Timetable.Timetable', different /student-bodies/ may simultaneously reference a single /teacher/.
This is normal behaviour, provided the /teacher/ can cope with the class-size,
& is implemented seemlessly, as a /map/-structure with one or more /key/s referencing a /value/, containing the same /teacher/.
* When inverting such a /timetable/, this many-to-one relationship becomes a one-to-many, & a single /teacher-id/ must now reference many /student-bodies/.
The simple /map/-structure, unlike a /multi-map/, can't directly accommodate multiple identical keys, with different values,
so this is achieved by merging /student-bodies/ into /student-class/es.
-}
fromStudentViewTimetable :: (
Data.Array.IArray.Ix timeslotId,
Enum timeslotId,
Eq level,
Eq locationId,
Ord teacherId,
Show level,
Show locationId,
Show teacherId,
Show timeslotId
)
=> Bool -- ^ Permit a temporary /student-body/ merger.
-> Timetable teacherId timeslotId locationId level -- ^ An unallocated /timetable/, supplied for efficiency.
-> StudentView.Timetable.Timetable timeslotId locationId teacherId level
-> Timetable teacherId timeslotId locationId level
fromStudentViewTimetable permitTemporaryStudentBodyMerger = Data.Map.foldrWithKey (
\studentBody studentViewTimetableForWeek teacherViewTimetable -> snd {-timetable-} $ Data.Foldable.foldr (
\studentViewTimetableForDay (day, teacherViewTimetable') -> (
pred day, -- Because of the use of 'foldr'.
snd {-timetable-} $ Data.Foldable.foldr (
\maybeStudentViewLesson (timeslotId, teacherViewTimetable'') -> (
pred timeslotId, -- Because of the use of 'foldr'.
Data.Maybe.maybe teacherViewTimetable'' (
(
$ teacherViewTimetable''
) . bookStudentViewLesson permitTemporaryStudentBodyMerger {-partially apply-} . (,) (
studentBody,
Temporal.Time.mkTime day timeslotId
) {-construct a booking-}
) maybeStudentViewLesson
)
) (
snd {-maximum timeslotId-} $ Data.Array.IArray.bounds studentViewTimetableForDay,
teacherViewTimetable'
) studentViewTimetableForDay
) {-CAVEAT: performance-hotspot-}
) (
snd {-maximum day-} $ Data.Array.IArray.bounds studentViewTimetableForWeek,
teacherViewTimetable
) studentViewTimetableForWeek
)
{- |
* Build a 'StudentView.Timetable.Timetable', by inverting a 'Timetable'.
* Each /lesson/ in the /timetable/ seen from the /teacher/'s perspective, contains a /student-class/.
This /student-class/ must be broken into /student-bodies/, before inserting repeatedly into the required /timetable/ as seen from the /student-body/'s perpective.
-}
toStudentViewTimetable
:: (Data.Array.IArray.Ix timeslotId, Enum timeslotId)
=> StudentView.Timetable.Timetable timeslotId locationId teacherId level -- ^ An unallocated /timetable/, supplied for efficiency.
-> Timetable teacherId timeslotId locationId level
-> StudentView.Timetable.Timetable timeslotId locationId teacherId level
toStudentViewTimetable = Data.Map.foldrWithKey (
\teacherId teacherViewTimetableForWeek studentViewTimetable -> snd {-timetable-} $ Data.Foldable.foldr (
\teacherViewTimetableForDay (day, studentViewTimetable') -> (
pred day, -- Because of the use of 'foldr'.
snd {-timetable-} $ Data.Foldable.foldr (
\maybeTeacherViewLesson (timeslotId, studentViewTimetable'') -> (
pred timeslotId, -- Because of the use of 'foldr'.
Data.Maybe.maybe studentViewTimetable'' (
(
$ studentViewTimetable''
) . bookTeacherViewLesson {-partially apply-} . (,) (
teacherId,
Temporal.Time.mkTime day timeslotId
) {-construct a booking-}
) maybeTeacherViewLesson
)
) (
snd {-maximum timeslotId-} $ Data.Array.IArray.bounds teacherViewTimetableForDay,
studentViewTimetable'
) teacherViewTimetableForDay
)
) (
snd {-maximum day-} $ Data.Array.IArray.bounds teacherViewTimetableForWeek,
studentViewTimetable
) teacherViewTimetableForWeek
)
-- | Apply a 'StudentView.Timetable.Booking' to a /TeacherView-timetable/.
bookStudentViewLesson :: (
Data.Array.IArray.Ix timeslotId,
Eq locationId,
Eq level,
Ord teacherId,
Show level,
Show locationId,
Show teacherId,
Show timeslotId
)
=> Bool -- ^ Permit a temporary /student-body/ merger.
-> StudentView.Timetable.Booking timeslotId locationId teacherId level -- ^ The /coordinates/ & /lesson/ from the perspective of the /student/-view of the /timetable/.
-> Timetable teacherId timeslotId locationId level -- ^ The /timetable/ as seen from the /teacher/'s perspective.
-> Timetable teacherId timeslotId locationId level
bookStudentViewLesson permitTemporaryStudentBodyMerger ((studentBody, time), studentViewLesson) teacherViewTimetable = Model.Timetable.defineTimeslot (
teacherViewTimetableCoordinates,
Just studentViewLesson {
Model.Lesson.getResourceIds = TeacherView.LessonResourceIds.mkLessonResourceIds locationId . Data.Set.insert studentBody $ case Model.Timetable.getMaybeLesson teacherViewTimetableCoordinates teacherViewTimetable of
Nothing -> Data.Set.empty -- Start a new student-class.
Just teacherViewLesson
| and [
permitTemporaryStudentBodyMerger,
TeacherView.LessonResourceIds.getLocationId teacherViewResourceIds == locationId,
Model.Lesson.getSubject teacherViewLesson == Model.Lesson.getSubject studentViewLesson
] -> TeacherView.LessonResourceIds.getStudentClass teacherViewResourceIds
| otherwise -> error $ "WeekDaze.TeacherView.Timetable.bookStudentViewLesson:\tlesson at coordinates=" ++ show teacherViewTimetableCoordinates ++ " has already been booked in either a different subject or location; " ++ show (studentViewLesson, teacherViewLesson) ++ "."
where
teacherViewResourceIds = Model.Lesson.getResourceIds teacherViewLesson
} -- Mutate.
) teacherViewTimetable where
studentViewResourceIds = Model.Lesson.getResourceIds studentViewLesson
locationId = StudentView.LessonResourceIds.getLocationId studentViewResourceIds
teacherViewTimetableCoordinates = (StudentView.LessonResourceIds.getTeacherId studentViewResourceIds, time)
-- | Catalogue the /student-class/es booked in the specified /timetable/, indexed by /location-Id/.
findStudentClassesByLocationId
:: (Data.Array.IArray.Ix timeslotId, Ord locationId)
=> Timetable teacherId timeslotId locationId level
-> Data.Map.Map locationId [Aggregate.StudentClass.StudentClass]
findStudentClassesByLocationId = Data.Map.fromListWith (++) . map (
(TeacherView.LessonResourceIds.getLocationId &&& return {-to List-monad-} . TeacherView.LessonResourceIds.getStudentClass) . Model.Lesson.getResourceIds
) . Model.Timetable.extractLessons
-- | Calculates the mean over all /booking/s, of the size of the /student-class/.
calculateMeanStudentClassSize :: (
Data.Array.IArray.Ix timeslotId,
Fractional meanValue
) => Timetable teacherId timeslotId locationId level -> meanValue
calculateMeanStudentClassSize teacherViewTimetable
| null lessons = 0 -- The timetable is completely unpopulated.
| otherwise = Factory.Math.Statistics.getMean $ map (
Aggregate.StudentClass.getSize . TeacherView.LessonResourceIds.getStudentClass . Model.Lesson.getResourceIds
) lessons
where
lessons = Model.Timetable.extractLessons teacherViewTimetable
{- |
* Counts the /location/-changes made by each /teacher/.
* The minimum value of zero can be achieved either by a completely unbooked /timetable/,
or one completely booked by the /lesson/s in the same /location/.
* CAVEAT: doesn't account for the /location/ of any /meeting/s.
-}
countLocationChangesByTeacherId :: (
Data.Array.IArray.Ix timeslotId,
Eq locationId
) => Timetable teacherId timeslotId locationId level -> Data.Map.Map teacherId Int
countLocationChangesByTeacherId = Data.Map.map TeacherView.TimetableForWeek.countLocationChanges
-- | A /lesson/ qualified by the /coordinates/ at which it is booked.
type Booking teacherId timeslotId locationId level = Model.Timetable.Booking teacherId timeslotId (TeacherView.LessonResourceIds.LessonResourceIds locationId) level
-- | Books a /TeacherView-booking/ in a /StudentView-timetable/.
bookTeacherViewLesson
:: Data.Array.IArray.Ix timeslotId
=> Booking teacherId timeslotId locationId level -- ^ The /coordinates/ & /lesson/ from the perspective of the /teacher/-view of the /timetable/.
-> StudentView.Timetable.Timetable timeslotId locationId teacherId level -- ^ The /timetable/ as seen from the /student/'s perspective.
-> StudentView.Timetable.Timetable timeslotId locationId teacherId level
bookTeacherViewLesson ((teacherId, time), teacherViewLesson) studentViewTimetable = Data.Set.foldr (
Model.Timetable.defineTimeslot . flip (,) (
Just $ TeacherView.Lesson.toStudentView teacherId teacherViewLesson
) . flip (,) time
) studentViewTimetable $ TeacherView.LessonResourceIds.getStudentClass teacherViewResourceIds where
teacherViewResourceIds = Model.Lesson.getResourceIds teacherViewLesson
-- | The type of a map passed to 'toXHtml'.
type InterCampusMigrationsByTeacherId teacherId = Data.Map.Map teacherId Size.NTimeslots
-- | A map indexed by /teacherId/, of sets of /times/.
type TimesByTeacherId teacherId timeslotId = Data.Map.Map teacherId (Temporal.Time.TimeSet timeslotId)
-- | Render in /XHTML/, as a /definition-list/.
toXHtml :: (
Data.Array.IArray.Ix timeslotId,
Fractional minimumContrastRatio,
Ord level,
Ord locationId,
Ord minimumContrastRatio,
Ord teacherId,
RealFrac teachingRatio,
Show level,
Show locationId,
Text.XHtml.Strict.HTML level,
Text.XHtml.Strict.HTML locationId,
Text.XHtml.Strict.HTML synchronisationId,
Text.XHtml.Strict.HTML teacherId,
Text.XHtml.Strict.HTML timeslotId
)
=> InterCampusMigrationsByTeacherId teacherId -- ^ The number of inter-/campus/ /teacher/-migrations, indexed by /teacherId/.
-> TimesByTeacherId teacherId timeslotId -- ^ Unbooked but specified times, by /teacherId/.
-> Size.NTimeslots -- ^ The number of /time-slot/s per /day/.
-> (teacherId -> TeacherView.Lesson.Lesson locationId level -> Data.Course.Course synchronisationId level timeslotId) -- ^ Find the /course/ to which the specified /lesson/ belongs.
-> Aggregate.TeacherRegister.TeacherRegister teacherId synchronisationId level timeslotId locationId teachingRatio
-> Model.Timetable.GenericTimetableToMarkup locationId minimumContrastRatio teacherId timeslotId (Timetable teacherId timeslotId locationId level)
toXHtml interCampusMigrationsByTeacherId unbookedSpecifiedTimesByTeacherId nTimeslotsPerDay findCourseFor teacherRegister displaySupplementaryInformation meetingsByTime htmlMergeDuplicateTimeslots displayAxisLabels weekend maybeGenerateLessonColourFrom = Text.XHtml.Strict.defList . map (
\(teacherId, timetableForWeek) -> let
teacherProfile = teacherRegister ! teacherId
in (
Text.XHtml.Strict.unordList (
Text.XHtml.Strict.toHtml teacherId : if displaySupplementaryInformation
then Data.Maybe.catMaybes [
let
nInterCampusMigrations = interCampusMigrationsByTeacherId ! teacherId
in if nInterCampusMigrations == 0
then Nothing
else Just $ Text.XHtml.Strict.thediv Text.XHtml.Strict.! [
Text.XHtml.Strict.theclass Text.CSS.warningCSSIdentifier,
Text.XHtml.Strict.title "The number of inter-campus migrations required of this teacher."
] << (
"Inter-campus migrations" +++ Text.XHtml.Strict.spaceHtml +++ '=' +++ Text.XHtml.Strict.spaceHtml +++ Text.XHtml.Strict.thespan Text.XHtml.Strict.! [
Text.XHtml.Strict.theclass Text.CSS.numericDataCSSIdentifier
] << nInterCampusMigrations
),
if Data.HumanResource.getNTimeslotsPerWeekOfTeaching nTimeslotsPerDay teacherProfile == 0
then Nothing
else Just $ Text.XHtml.Strict.thespan Text.XHtml.Strict.! [
Text.XHtml.Strict.theclass Text.CSS.infoCSSIdentifier,
Text.XHtml.Strict.title "Time allocated to teaching / time available for teaching."
] << (
"Utilisation-ratio" +++ Text.XHtml.Strict.spaceHtml +++ '=' +++ Text.XHtml.Strict.spaceHtml +++ Text.XHtml.Strict.thespan Text.XHtml.Strict.! [
Text.XHtml.Strict.theclass Text.CSS.numericDataCSSIdentifier
] << (
round $ 100 * (
Model.TimetableForWeek.calculateUtilisationRatio nTimeslotsPerDay teacherProfile timetableForWeek ::Rational
) :: Int
) +++ '%'
),
let
nFreePeriods = Model.TimetableForWeek.countUnallocatedAvailableTimeslots teacherProfile timetableForWeek - Data.HumanResource.getNTimeslotsPerWeekOfNonTeaching nTimeslotsPerDay teacherProfile {-which includes the time for meetings-}
in if nFreePeriods == 0
then Nothing
else Just $ Text.XHtml.Strict.thespan Text.XHtml.Strict.! [
Text.XHtml.Strict.theclass Text.CSS.warningCSSIdentifier,
Text.XHtml.Strict.title "The number of available but unallocated time-slots, excluding those allocated to administration & meetings."
] << (
"Free-periods" +++ Text.XHtml.Strict.spaceHtml +++ '=' +++ Text.XHtml.Strict.spaceHtml +++ Text.XHtml.Strict.thespan Text.XHtml.Strict.! [
Text.XHtml.Strict.theclass Text.CSS.numericDataCSSIdentifier
] << nFreePeriods
),
let
separatedEqualLessons = Model.TimetableForWeek.findSeparatedEqualLessonsWithinAnyDay timetableForWeek
in if null separatedEqualLessons
then Nothing
else Just $ Text.XHtml.Strict.thediv Text.XHtml.Strict.! [
Text.XHtml.Strict.theclass Text.CSS.warningCSSIdentifier,
Text.XHtml.Strict.title "The number of instances of separated sessions of identical lessons, in any single day."
] << (
"Split sessions:" +++ Text.XHtml.Strict.defList (
map (
Data.Tuple.swap . Control.Arrow.first (
(
Text.XHtml.Strict.thespan Text.XHtml.Strict.! [
Text.XHtml.Strict.theclass Text.CSS.numericDataCSSIdentifier
] <<
) . pred {-count those exceeding one-}
)
) separatedEqualLessons
)
),
let
unbookedSpecifiedTimes = unbookedSpecifiedTimesByTeacherId ! teacherId
in if Data.Set.null unbookedSpecifiedTimes
then Nothing
else Just $ Text.XHtml.Strict.thediv Text.XHtml.Strict.! [
Text.XHtml.Strict.theclass Text.CSS.warningCSSIdentifier,
Text.XHtml.Strict.title "Times specified by courses, which haven't been booked, for courses which have."
] << (
"Unbooked times:" +++ (
Text.XHtml.Strict.thediv Text.XHtml.Strict.! [
Text.XHtml.Strict.theclass Text.CSS.dataCSSIdentifier
] << Text.XHtml.Strict.unordList (
Data.Set.toAscList unbookedSpecifiedTimes
)
)
)
]
else []
) Text.XHtml.Strict.! [Text.XHtml.Strict.theclass Text.CSS.observerSummaryCSSIdentifier], -- CAVEAT: xhtml only permits inline elements to be nested inside a 'dt' element, & 'ul' is a block element.
Model.TimetableForWeek.toXHtml (
findCourseFor teacherId -- Partially apply.
) teacherProfile (
Model.Meeting.deleteTeacherId teacherId meetingsByTime
) htmlMergeDuplicateTimeslots displayAxisLabels weekend maybeGenerateLessonColourFrom timetableForWeek
) -- Pair.
) . Data.Map.toList