weekdaze-0.0.0.1: src-lib/WeekDaze/Aggregate/StudentBodyRegister.hs
{-# LANGUAGE CPP, FlexibleContexts #-}
{-
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@] Provides a catalogue of /student-profile/s, indexed by the /student-body/ by whom it's shared.
-}
module WeekDaze.Aggregate.StudentBodyRegister(
-- * Types
-- ** Type-synonyms
StudentBodyRegister,
KnowledgeByStudentBody,
-- * Constants
tag,
-- * Functions
countAvailableStudentDays,
countSubjectsRequired,
extractDistinctSubjects,
reduce,
reduce',
-- ** Accessors
getStudentBodies,
#ifdef USE_HDBC
-- ** Constructor
fromDatabase,
#endif
-- ** Predicates
hasAnyFreePeriodPreference,
hasAnyCoreKnowledgeRequirements,
hasAnyOptionalKnowledgeRequirements
) where
import Control.Arrow((&&&))
import qualified Control.Monad.Writer
import qualified Data.Foldable
import qualified Data.Map
import qualified Data.Set
import qualified WeekDaze.Aggregate.StudentBody as Aggregate.StudentBody
import qualified WeekDaze.Aggregate.StudentClass as Aggregate.StudentClass
import qualified WeekDaze.Data.HumanResource as Data.HumanResource
import qualified WeekDaze.Data.Resource as Data.Resource
import qualified WeekDaze.Data.Student as Data.Student
import qualified WeekDaze.Data.Subject as Data.Subject
import qualified WeekDaze.Size as Size
#ifdef USE_HDBC
import qualified Database.HDBC
import qualified Data.Convertible
import qualified Data.Default
import qualified Data.IntMap
import qualified Data.Maybe
import qualified Data.Typeable
import qualified WeekDaze.Database.Selector as Database.Selector
import qualified WeekDaze.Data.Group as Data.Group
import qualified WeekDaze.Data.Requirements as Data.Requirements
import qualified WeekDaze.Temporal.Availability as Temporal.Availability
import qualified WeekDaze.Temporal.FreePeriodPreference as Temporal.FreePeriodPreference
-- | Construct from the specified database-connection.
fromDatabase :: (
Database.HDBC.IConnection connection,
Data.Convertible.Convertible Database.HDBC.SqlValue level, -- Flexible context.
Data.Convertible.Convertible Database.HDBC.SqlValue stream, -- Flexible context.
Data.Convertible.Convertible Database.HDBC.SqlValue teachingRatio, -- Flexible context.
Data.Default.Default stream,
Data.Typeable.Typeable teachingRatio,
Ord level,
RealFrac teachingRatio,
Show level
)
=> connection -- ^ An abstract database-connection.
-> Database.HDBC.SqlValue -- ^ The project-id.
-> IO (StudentBodyRegister level stream teachingRatio)
fromDatabase connection projectIdSql = let
priorityColumnName, streamIdColumnName, streamNameColumnName, studentBodyRegisterIdColumnName :: Database.Selector.ColumnName
priorityColumnName = "priority"
streamIdColumnName = "streamId"
streamNameColumnName = "streamName" -- CAVEAT: known merely as 'stream' in Data.Student & the DTD.
studentBodyRegisterIdColumnName = showString tag "Id";
knowledgeRequirementsTableName, streamTableName, studentBodyRegisterTableName, studentBodyMembershipTableName, studentGroupMembershipTableName :: Database.Selector.TableName
[knowledgeRequirementsTableName, streamTableName, studentBodyRegisterTableName, studentBodyMembershipTableName, studentGroupMembershipTableName] = map (showString Database.Selector.tablePrefix) ["knowledgeRequirement", "stream", tag, "studentBodyMembership", "studentGroupMembership"]
in do
streamNameByStreamId <- (
Data.IntMap.fromList . map (
\row -> case row of
[streamIdSql, streamNameSql] -> (
either (
error . showString "WeekDaze.Aggregate.StudentBodyRegister.fromDatabase:\tfailed to parse the value for " . shows streamIdColumnName . showString " read from the database; " . show
) id $ Database.HDBC.safeFromSql streamIdSql,
Database.HDBC.fromSql streamNameSql
) -- Pair.
_ -> error . showString "WeekDaze.Aggregate.StudentBodyRegister.fromDatabase:\tunexpected number of columns=" . shows (length row) . showString " in row of table " $ shows streamTableName "."
)
) `fmap` Database.Selector.select connection [streamIdColumnName, streamNameColumnName] [streamTableName] [(Database.Selector.projectIdColumnName, projectIdSql)]
#ifdef USE_HDBC_ODBC
[
selectStudentIdsForStudentBodyRegisterId,
selectKnowledgeRequirementsForStudentBodyRegisterId,
selectGroupIdsForStudentBodyRegisterId
] <- mapM (
\(columnNames, tableName) -> Database.Selector.prepare connection columnNames [tableName] [studentBodyRegisterIdColumnName]
) [
(
[Aggregate.StudentBody.studentIdTag],
studentBodyMembershipTableName
), (
[
priorityColumnName,
Data.Subject.topicTag,
Data.Subject.levelTag
],
knowledgeRequirementsTableName
), (
[Data.Group.groupIdTag],
studentGroupMembershipTableName
)
] -- Prepare statements for execution with each studentBodyRegisterId.
#endif /* USE_HDBC_ODBC */
Database.Selector.select connection [
studentBodyRegisterIdColumnName,
Aggregate.StudentBody.mnemonicTag,
Temporal.Availability.tag,
streamIdColumnName,
Data.Student.teachingRatioTag,
Temporal.FreePeriodPreference.tag
] [studentBodyRegisterTableName] [(Database.Selector.projectIdColumnName, projectIdSql)] >>= fmap Data.Map.fromList . mapM (
\studentRow -> case studentRow of
[studentBodyRegisterIdSql, mnemonicSql, availabilitySql, streamIdSql, teachingRatioSql, freePeriodPreferenceSql] -> do
#ifndef USE_HDBC_ODBC
let primaryKey = [(studentBodyRegisterIdColumnName, studentBodyRegisterIdSql)]
#endif
studentIds <- map (
Database.HDBC.fromSql . head {-select the only column-}
#ifdef USE_HDBC_ODBC
) `fmap` (
Database.HDBC.execute selectStudentIdsForStudentBodyRegisterId [studentBodyRegisterIdSql] >> Database.HDBC.fetchAllRows' selectStudentIdsForStudentBodyRegisterId
)
#else
) `fmap` Database.Selector.select connection [Aggregate.StudentBody.studentIdTag] [studentBodyMembershipTableName] primaryKey
#endif
knowledgeRequirements <- (
(
Data.Maybe.fromMaybe Data.Set.empty . Data.Map.lookup Data.Requirements.coreTag &&& Data.Maybe.fromMaybe Data.Set.empty . Data.Map.lookup Data.Requirements.optionalTag
) . Data.Map.fromListWith Data.Set.union . map (
\knowledgeRequirementRow -> case knowledgeRequirementRow of
[prioritySql, topicSql, levelSql] -> (
Data.Maybe.fromMaybe (
error . showString "WeekDaze.Aggregate.StudentBodyRegister.fromDatabase:\tnull " $ shows priorityColumnName "."
) $ Database.HDBC.fromSql prioritySql,
Data.Set.singleton $ Data.Subject.mkSubjectFromSql topicSql levelSql
) -- Pair
_ -> error . showString "WeekDaze.Aggregate.StudentBodyRegister.fromDatabase:\tunexpected number of columns=" . shows (length knowledgeRequirementRow) . showString " in row of table " $ shows knowledgeRequirementsTableName "."
)
#ifdef USE_HDBC_ODBC
) `fmap` (
Database.HDBC.execute selectKnowledgeRequirementsForStudentBodyRegisterId [studentBodyRegisterIdSql] >> Database.HDBC.fetchAllRows' selectKnowledgeRequirementsForStudentBodyRegisterId
)
#else
) `fmap` Database.Selector.select connection [
priorityColumnName,
Data.Subject.topicTag,
Data.Subject.levelTag
] [knowledgeRequirementsTableName] primaryKey
#endif
groupMembership <- (
Data.Set.fromList . map (
either (
error . showString "WeekDaze.Aggregate.StudentBodyRegister.fromDatabase:\tfailed to parse the value for " . shows Data.Group.groupIdTag . showString " read from the database; " . show
) id . Database.HDBC.safeFromSql . head {-select the only column-}
)
#ifdef USE_HDBC_ODBC
) `fmap` (
Database.HDBC.execute selectGroupIdsForStudentBodyRegisterId [studentBodyRegisterIdSql] >> Database.HDBC.fetchAllRows' selectGroupIdsForStudentBodyRegisterId
)
#else
) `fmap` Database.Selector.select connection [Data.Group.groupIdTag] [studentGroupMembershipTableName] primaryKey
#endif
return {-to IO-monad-} (
Aggregate.StudentBody.mkStudentBody (
either (
error . showString "WeekDaze.Aggregate.StudentBodyRegister.fromDatabase:\tfailed to parse the value for " . shows Aggregate.StudentBody.mnemonicTag . showString " read from the database; " . show
) id $ Database.HDBC.safeFromSql mnemonicSql
) . Data.Set.fromList $ map (
Data.Maybe.fromMaybe (
error . showString "WeekDaze.Aggregate.StudentBodyRegister.fromDatabase:\tnull " $ shows Aggregate.StudentBody.studentIdTag "."
)
) studentIds,
Data.Student.mkProfile (
Data.Maybe.maybe Data.Default.def (
Data.Maybe.fromMaybe (
error . showString "WeekDaze.Aggregate.StudentBodyRegister.fromDatabase:\tfailed to find a " . shows streamNameColumnName . showString " corresponding to the value for " $ shows streamIdColumnName " read from the database."
) . (
`Data.IntMap.lookup` streamNameByStreamId
)
) . either (
error . showString "WeekDaze.Aggregate.StudentBodyRegister.fromDatabase:\tfailed to parse the value for " . shows streamIdColumnName . showString " read from the database; " . show
) id $ Database.HDBC.safeFromSql streamIdSql
) knowledgeRequirements (
Data.Maybe.fromMaybe (
error . showString "WeekDaze.Aggregate.StudentBodyRegister.fromDatabase:\tnull " $ shows Temporal.Availability.tag "."
) $ Database.HDBC.fromSql availabilitySql
) (
Database.Selector.fromSqlFractional Data.Student.defaultTeachingRatio teachingRatioSql
) groupMembership $ Database.HDBC.fromSql freePeriodPreferenceSql -- Returns Nothing for SqlNull.
) -- Pair.
_ -> error . showString "WeekDaze.Aggregate.StudentBodyRegister.fromDatabase:\tunexpected number of columns=" . shows (length studentRow) . showString" in row of table " $ shows studentBodyRegisterTableName "."
)
#endif /* USE_HDBC */
-- | Used to qualify XML.
tag :: String
tag = "studentBodyRegister"
-- | A map indexed by /student-body/, of /profile/s shared amongst /student/s in the /body/.
type StudentBodyRegister level stream teachingRatio = Data.Resource.ResourceMap Aggregate.StudentBody.StudentBody (Data.Student.Profile level stream teachingRatio)
-- | Accessor.
getStudentBodies :: StudentBodyRegister level stream teachingRatio -> [Aggregate.StudentBody.StudentBody]
getStudentBodies = Data.Map.keys
-- | Extracts the set of distinct /subjects/ required, gathered from all /students/.
extractDistinctSubjects :: Ord level => StudentBodyRegister level stream teachingRatio -> Data.Subject.Knowledge level
extractDistinctSubjects = Data.Map.foldr (Data.Set.union . Data.Student.deriveAmalgamatedKnowledgeRequirement) Data.Set.empty
{- |
* Merges those /student-bodies/ whose /profile/s are identical, into a single larger /student-body/.
* Writes the lists of merged /student-bodies/.
* Checks that any new /mnemonic/s, composed from those of /student-bodies/ with identical profiles, differ from existing ones.
This shouldn't matter, because the key of 'StudentBodyRegister' is the whole /student-body/ not just the /mnemonic/,
regrettably, for efficiency, /StudentBody/ implements 'Eq' & 'Ord' using only the /mnemonic/.
-}
reduce :: (
Ord level,
Ord stream,
Ord teachingRatio
)
=> Aggregate.StudentClass.MnemonicSeparator
-> StudentBodyRegister level stream teachingRatio
-> Control.Monad.Writer.Writer [[Aggregate.StudentBody.StudentBody]] (StudentBodyRegister level stream teachingRatio)
reduce mnemonicSeparator studentBodyRegister
| null ambiguousMnemonics = Control.Monad.Writer.writer (studentBodyRegister', mergeableStudentBodies)
| otherwise = error . showString "WeekDaze.Aggregate.StudentBodyRegister.reduce:\tsome of the mnemonics composed by merging those of student-bodies with identical profiles, are ambiguous; " $ shows ambiguousMnemonics "."
where
(studentBodyRegister', (ambiguousMnemonics, mergeableStudentBodies)) = (
Data.Map.fromList . map (
\(sharedStudentProfile, studentClass) -> (Aggregate.StudentClass.merge mnemonicSeparator studentClass, sharedStudentProfile) -- Reverse the mapping back to the original.
) . Data.Map.toList &&& (
Data.Set.toList . Data.Set.intersection (
Data.Set.map Aggregate.StudentBody.getMnemonic $ Data.Map.keysSet studentBodyRegister
) . Data.Set.fromList . map (
Aggregate.StudentBody.getMnemonic . Aggregate.StudentClass.merge mnemonicSeparator
) &&& map Data.Set.toList
) . filter (
(/= 1) . Data.Set.size -- Select student-bodies that can be merged.
) . Data.Map.elems
) . Data.Map.fromListWith Data.Set.union . map (
\(studentBody, studentProfile) -> (studentProfile, Data.Set.singleton studentBody) -- Reverse the mapping.
) $ Data.Map.toList studentBodyRegister
-- | Merges those /student-bodies/ whose /profile/s are identical, into a single larger /student-body/.
reduce' :: (
Ord level,
Ord stream,
Ord teachingRatio
)
=> Aggregate.StudentClass.MnemonicSeparator
-> StudentBodyRegister level stream teachingRatio
-> StudentBodyRegister level stream teachingRatio
reduce' s = fst {-StudentBodyRegister-} . Control.Monad.Writer.runWriter . reduce s
-- | The sum of the number of /day/s worked by each /student/.
countAvailableStudentDays :: StudentBodyRegister level stream teachingRatio -> Size.NDays
countAvailableStudentDays = Data.Map.foldrWithKey (\studentBody -> (+) . (* Aggregate.StudentBody.getSize studentBody) . Data.Resource.countDaysPerWeekAvailable) 0
-- | /Knowlege-requirement/, indexed by /student-body/.
type KnowledgeByStudentBody level = Data.Map.Map Aggregate.StudentBody.StudentBody (Data.Subject.Knowledge level)
-- | Count the total number of /subject/s required by all /student/s.
countSubjectsRequired :: KnowledgeByStudentBody level -> Int
countSubjectsRequired = Data.Map.foldrWithKey (\studentBody -> (+) . (* Aggregate.StudentBody.getSize studentBody) . Data.Set.size) 0
-- | True if any /student-body/ has specified a free-period preference.
hasAnyFreePeriodPreference :: RealFrac teachingRatio => StudentBodyRegister level stream teachingRatio -> Bool
hasAnyFreePeriodPreference = Data.Foldable.any Data.HumanResource.hasFreePeriodPreference
-- | True if any /student-body/ has specified a core /subject/ in their knowledge-requirements.
hasAnyCoreKnowledgeRequirements :: StudentBodyRegister level stream teachingRatio -> Bool
hasAnyCoreKnowledgeRequirements = Data.Foldable.any Data.Student.hasAnyCoreKnowledgeRequirements
-- | True if any /student-body/ has specified an optional /subject/ in their knowledge-requirements.
hasAnyOptionalKnowledgeRequirements :: StudentBodyRegister level stream teachingRatio -> Bool
hasAnyOptionalKnowledgeRequirements = Data.Foldable.any Data.Student.hasAnyOptionalKnowledgeRequirements