packages feed

weekdaze-0.0.0.1: src-lib/WeekDaze/Aggregate/LocationCatalogue.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@]	Defines the names & attributes of all /location/s available to be booked in the /timetable/.

-}

module WeekDaze.Aggregate.LocationCatalogue(
-- * Types
-- ** Type-synonyms
	LocationCatalogue,
-- * Constants
	tag,
-- * Functions
	countDaysByFacilityName,
--	countDistinctCampuses,
	extractDistinctFacilityNames,
	findSuitableLocations,
-- ** Accessors
	getLocationIds,
#ifdef USE_HDBC
-- ** Constructor
	fromDatabase,
#endif
-- ** Predicates
	hasAnyFacilities,
	isSingleCampus
) where

import qualified	Data.Foldable
import qualified	Data.Map
import qualified	Data.Set
import qualified	WeekDaze.Data.Location		as Data.Location
import qualified	WeekDaze.Data.Resource		as Data.Resource
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	WeekDaze.Database.Selector	as Database.Selector
import qualified	WeekDaze.Temporal.Availability	as Temporal.Availability

{- |
	* Construct from the specified database-connection.

	* CAVEAT: though the database may not permit a null value for many fields (applying its own default value when the value is unspecified),
	default values are applied here should the SQL-query return one.
-}
fromDatabase :: (
	Database.HDBC.IConnection		connection,
	Data.Convertible.Convertible		Database.HDBC.SqlValue campus,		-- Flexible context.
	Data.Convertible.Convertible		Database.HDBC.SqlValue locationId,	-- Flexible context.
	Data.Default.Default			campus,
	Ord					locationId,
	Show					campus
 )
	=> connection			-- ^ An abstract database-connection.
	-> Database.HDBC.SqlValue	-- ^ The project-id.
	-> IO (LocationCatalogue locationId campus)
fromDatabase connection	projectIdSql	= let
	locationCatalogueIdColumnName :: Database.Selector.ColumnName
	locationCatalogueIdColumnName	= showString tag "Id";

	facilityTableName, locationCatalogueTableName :: Database.Selector.TableName
	facilityTableName		= showString Database.Selector.tablePrefix "facility"
	locationCatalogueTableName	= showString Database.Selector.tablePrefix tag
 in do
	facilityNameByFacilityTypeId			<- Data.Location.findFacilityNameByFacilityTypeId connection projectIdSql

#ifdef USE_HDBC_ODBC
	selectFacilityTypeIdsForLocationCatalogueId	<- Database.Selector.prepare connection [Data.Location.facilityTypeIdTag] [facilityTableName] [locationCatalogueIdColumnName]	-- Select the facilityTypeIds at a locationId to be defined. CAVEAT: prepared statements don't seem to work reliably with HDBC-mysql.
#endif
	Database.Selector.select connection [
		locationCatalogueIdColumnName,
		Database.Selector.locationIdColumnName,
		Temporal.Availability.tag,
		Data.Location.capacityTag,
		Database.Selector.campusColumnName
	 ] [locationCatalogueTableName] [(Database.Selector.projectIdColumnName, projectIdSql)] >>= fmap Data.Map.fromList . mapM (
		\locationRow -> case locationRow of
			[locationCatalogueIdSql, locationIdSql, availabilitySql, capacitySql, campusSql]	-> do
				facilityNames	<- map (
					\locationCatalogueRow -> case locationCatalogueRow of
						[facilityTypeIdSql]	-> let
							facilityTypeId	= Data.Maybe.fromMaybe (
								error . showString "WeekDaze.Aggregate.LocationCatalogue.fromDatabase:\tnull " $ shows Data.Location.facilityTypeIdTag "."
							 ) . either (
								error . showString "WeekDaze.Aggregate.LocationCatalogue.fromDatabase:\tfailed to parse the value for " . shows Data.Location.facilityTypeIdTag . showString " read from the database; " . show
							 ) id $ Database.HDBC.safeFromSql facilityTypeIdSql
						 in Data.Maybe.fromMaybe (
							error . showString "WeekDaze.Aggregate.LocationCatalogue.fromDatabase:\tunknown " . showString Data.Location.facilityTypeIdTag . showChar '=' $ shows facilityTypeId "."
						 ) $ Data.IntMap.lookup facilityTypeId facilityNameByFacilityTypeId
						_			-> error . showString "WeekDaze.Aggregate.LocationCatalogue.fromDatabase:\tunexpected number of columns=" . shows (length locationCatalogueRow) . showString" in row of table " $ shows facilityTableName "."
#ifdef USE_HDBC_ODBC
				 ) `fmap` (
					Database.HDBC.execute selectFacilityTypeIdsForLocationCatalogueId [locationCatalogueIdSql] >> Database.HDBC.fetchAllRows' selectFacilityTypeIdsForLocationCatalogueId
				 )
#else
				 ) `fmap` Database.Selector.select connection [Data.Location.facilityTypeIdTag] [facilityTableName] [(locationCatalogueIdColumnName, locationCatalogueIdSql)]	-- Select the facilityTypeIds at this locationId.
#endif
				return {-to IO-monad-} (
					Data.Maybe.fromMaybe (
						error . showString "WeekDaze.Aggregate.LocationCatalogue.fromDatabase:\tnull " $ shows Database.Selector.locationIdColumnName "."
					) $ Database.HDBC.fromSql locationIdSql,
					Data.Location.mkProfile (
						Data.Maybe.fromMaybe (
							error . showString "WeekDaze.Aggregate.LocationCatalogue.fromDatabase:\tnull " $ shows Data.Location.capacityTag "."
						) . either (
							error . showString "WeekDaze.Aggregate.LocationCatalogue.fromDatabase:\tfailed to parse the value for " . shows Data.Location.capacityTag . showString " read from the database; " . show
						) id $ Database.HDBC.safeFromSql capacitySql
					) (
						Data.Set.fromList facilityNames
					) (
						Data.Maybe.fromMaybe Data.Default.def $ Database.HDBC.fromSql availabilitySql
					) . Data.Maybe.fromMaybe Data.Default.def $ Database.HDBC.fromSql campusSql
				 ) -- Pair.
			_											-> error . showString "WeekDaze.Aggregate.LocationCatalogue.fromDatabase:\tunexpected number of columns=" . shows (length locationRow) . showString " in row of table " $ shows locationCatalogueTableName "."
	 )
#endif /* USE_HDBC */

-- | Used to qualify XML.
tag :: String
tag	= "locationCatalogue"

-- | The complete set of /location/s, indexed by their 'locationId'.
type LocationCatalogue locationId campus	= Data.Resource.ResourceMap locationId (Data.Location.Profile campus)

-- | Accessor.
getLocationIds :: LocationCatalogue locationId campus -> [locationId]
getLocationIds	= Data.Map.keys

-- | Count the total available /day/s, of those /location/s offering each type of /facility/.
countDaysByFacilityName :: LocationCatalogue locationId campus -> Data.Map.Map Data.Location.FacilityName Size.NDays
countDaysByFacilityName	= Data.Map.foldr (
	\profile m	-> Data.Set.foldr (
		\facilityName -> Data.Map.insertWith (+) facilityName (Data.Resource.countDaysPerWeekAvailable profile)
	) m $ Data.Location.getFacilityNames profile
 ) Data.Map.empty

-- | Count the number of distinct /campus/es which have been configured.
countDistinctCampuses :: Ord campus => LocationCatalogue locationId campus -> Int
countDistinctCampuses	= Data.Set.size . Data.Foldable.foldr (Data.Set.insert . Data.Location.getCampus) Data.Set.empty

-- | Whether all /location/s exist on the same /campus/.
isSingleCampus :: Ord campus => LocationCatalogue locationId campus -> Bool
isSingleCampus	= (== 1) . countDistinctCampuses

-- | Extracts the set of distinct /facilities/, from the catalogue.
extractDistinctFacilityNames :: LocationCatalogue locationId campus -> Data.Location.FacilityNames
extractDistinctFacilityNames	= Data.Map.foldr (Data.Set.union . Data.Location.getFacilityNames) Data.Set.empty

-- | Find those /locations/ which meet or exceed, the specified criteria.
findSuitableLocations
	:: Size.NStudents
	-> Data.Location.FacilityNames
	-> LocationCatalogue locationId campus
	-> LocationCatalogue locationId campus
findSuitableLocations requiredCapacity requiredFacilityNames	= Data.Map.filter (Data.Location.isSuitable requiredCapacity requiredFacilityNames)

-- | True if any /location/ offers any /facilities/.
hasAnyFacilities :: LocationCatalogue locationId campus -> Bool
hasAnyFacilities	= Data.Foldable.any Data.Location.hasFacilities