Location: PHPKode > projects > Community Learning Network > cln/modules/Tutorial/Tutorial.php
<?php
/*
 * Tutorial Module for the CLN Class
 *
 * Copyright (c) 2003-4 St. Christopher House
 *
 * Developed by The Working Group Inc.
 *
 * This program 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 2
 * of the License, or (at your option) any later version.
 *
 * This program 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 this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
 *
 * @version $Id: Tutorial.php,v 1.38 2005/01/25 17:47:46 darcy Exp $
 *
 */

require_once('Cln_Module.php');

if(!defined('TABLE_KB_tutorial')) define('TABLE_KB_tutorial', 'modtutorial');
if(!defined('TABLE_KB_tutorial_step')) define('TABLE_KB_tutorial_step', 'modtutorial_step');
if(!defined('TABLE_KB_tutorial_layout')) define('TABLE_KB_tutorial_layout', 'modtutorial_layout');

if(!defined('MOD_TUTORIAL_EXCLUDE_MODULES')) define('MOD_TUTORIAL_EXCLUDE_MODULES', '1,2,8,9,10,11,12,14,15,16,17,18'); // A comma separated list of modIds that can't be in the tutorial

class Cln_Module_Tutorial extends Cln_Module
{

	/*
	 *
	 * Class Attributes:  Cln_Module_Tutorial
	 *
	 * 		The attributes for this class are:
	 *
	 *		TBD
	 *
	 */
	var $tutorialId;
	var $title;
	var $topic;
	var $objectives;
	var $steps; // Multidimensional array of steps.


	/***********************************************************************************
	 *
	 * Externally accessed methods
	 *
	 **********************************************************************************/

	/*
	 *
	 * Function:  Cln_Module_Tutorial()
	 *
	 * 		Class Constructor
	 *
	 * @access public
	 * @return TBD
	 *
	 */
	function Cln_Module_Tutorial($tutorialId, $passedData = FALSE)
	{
		$this->tutorialId = $tutorialId;

		if ($passedData && isset($passedData['title'])) {
			$this->title = $passedData['title'];
			$this->topic = $passedData['topic'];
			$this->objectives = $passedData['objectives'];
			$this->steps = $passedData['steps'];
		}
		else {
			$this->title = '';
			$this->topic = '';
			$this->objectives = '';
			$this->steps = Array();
		}
	}


	/*
	 *
	 * Function:  getContent()
	 *
	 * 		Returns the text content from the DB
	 *
	 * @access public
	 * @return TBD
	 *
	 */
	function getContent()
	{
		$this->registerStylesheet(CLN_CLEAN_URL_BASE . MOD_CLN_MODULE_TUTORIAL_PATH . 'style/Tutorial.css');
		
		// Display the intro, or a specific step
		if (isset($_GET['viewKoId'])) {
			if (isset($_GET['tutorialStep'])) {
				$content = $this->getStep($_GET['tutorialStep']);
			}
			else {
				$content = $this->getTOC();
			}
		}
		// Else just display the teaser
		else {
			$content = $this->getTeaser();
		}
		return $content;
	}


	/*
	 *
	 * Function:  getEditPanelGeneral()
	 *
	 * 		Returns the general edit panel components
	 *
	 * @access public
	 * @return String 		$message
	 *
	 */
	function getEditPanelGeneral()
	{
		$generalParts = Array();
		return $generalParts;
	}


	/*
	 *
	 * Function:  getEditPanelLanguages()
	 *
	 * 		Returns the language specific edit panel components
	 *
	 * @access public
	 * @return String 		$message
	 *
	 */
	function getEditPanelLanguages()
	{
		$specificParts[0]['title'] = 'Edit The General Details';
		$specificParts[0]['description'] = 'Edit the title, topic, and objectives of your tutorial.';
		$specificParts[0]['subprocess'] = 'EditDetails';

		if ($this->tutorialId != 'NEW') {
			$specificParts[1]['title'] = 'Edit The Tutorial Steps';
			$specificParts[1]['description'] = 'Edit the content of the steps in your tutorial, and add new steps, etc.';
			$specificParts[1]['subprocess'] = 'EditSteps';
		}

		return $specificParts;
	}


	/*
	 *
	 * Function:  getEditContent()
	 *
	 * 		Returns the edit form for this module
	 *
	 * @access public
	 * @return TBD
	 *
	 */
	function getEditContent($override = FALSE)
	{
		d("Cln_Tutorial: {$this->tutorialId} getEditContent()", 3);
		
		$this->registerStylesheet(CLN_CLEAN_URL_BASE . MOD_CLN_MODULE_TUTORIAL_PATH . 'style/Tutorial.css');
		
		// If a subprocess is being passed, get it
		if (isset($_GET['subprocess'])) {
			$this->currentSubprocess = $_GET['subprocess'];
		}

		// If the subprocess isn't set, default it
		if (!isset($this->currentSubprocess)) {
			$this->currentSubprocess = 'EditDetails';
		}

		// Switch, based on the subprocess
		switch ($this->currentSubprocess) {

		case 'ReturnToPanel':
			return FALSE;
		break;

		case 'EditDetails':

			// If they're submitting the details page
			if (isset($_POST['submitTutorialDetails'])) {

				$this->title = $_POST['koMetadataTitle'];
				$this->topic = $_POST['tutorialTopic'];
				$this->objectives = $_POST['tutorialObjectives'];

				if ($this->validateEditData()) {
					// Only save now for existing objects, not new
					if ($this->tutorialId != 'NEW') {
						$this->saveModuleData();
					}
					if (!$override) {
						if ($this->tutorialId == 'NEW') {
							PEAR::raiseError('Before you can add steps so your tutorial, you must save it.', E_USER_NOTICE);
						}
						return FALSE;
					}
					else {
						return $this->getInterface('details');
					}
				}
				else {
					return $this->getInterface('details');
				}
			}
			// Else they're just getting here
			else {
				return $this->getInterface('details');
			}
		break;

		case 'EditSteps':
			if (isset($_POST['returnToPanel'])) {
				return FALSE;
			}
			else if (isset($_POST['submitTutorialAddStep'])) {
				$stepNumber = $this->addStep();
				if ($this->tutorialId != 'NEW') {
					$this->saveStep($stepNumber);
				}
				PEAR::raiseError('You have added a step to your tutorial. You can edit it below.', E_USER_NOTICE);
				return $this->getInterface('steps');
			}
			else {
				return $this->getInterface('steps');
			}
		break;

		case 'EditStepDetails':
			if (isset($_POST['editTutorialStep'])) {
				$stepNumber = $_GET['tutorialStep'];
				$this->steps[$stepNumber]['title'] = $_POST['tutorialStepTitle'];
				if ($this->tutorialId != 'NEW') {
					$this->saveStep($stepNumber);
				}
				PEAR::raiseError('The title of that step has been updated', E_USER_NOTICE);
				return $this->getInterface('steps');
			}
			else {
				return $this->getInterface('step');
			}
		break;

		// 2. Add Block
		case 'AddBlock':
			d('Cln_Tutorial: getEditContent (currentSubprocess: AddBlock)',3);
			// Adding a new Block
			if (isset($_POST['AddNewBlock'])) {
				d('------> AddNewBlock');
				$this->currentSubprocess = 'ChooseModType';
				return $this->getInterface('ChooseModType');
			}

			// Else if they want to copy a block
			else if (isset($_POST['copyBlock'])) {
				$this->currentSubprocess = 'CopyBlockFromKB';

				// Create the search, and set it's default values
				include_once('Cln_KB_Search.php');
				$search = new Cln_KB_Search();
				$search->excludedModules = explode(',', MOD_TUTORIAL_EXCLUDE_MODULES);
				$search->nameValuePairs['editKoId'] = $this->_super->koId;
				$search->nameValuePairs['editProcess'] = 'Content';
				$search->nameValuePairs['subprocess'] = $this->currentSubprocess;
				$search->nameValuePairs['tutorialStep'] = $_GET['tutorialStep'];

				// Run the search. We don't need to check anythign here, because that happens below
				return $search->getContent();
			}

			// Else if they want to link to an existing block
			else if (isset($_POST['linkToBlock'])) {
				$this->currentSubprocess = 'LinkToBlockInKB';

				// Create the search, and set it's default values
				include_once('Cln_KB_Search.php');
				$search = new Cln_KB_Search();
				$search->excludedModules = explode(',', MOD_TUTORIAL_EXCLUDE_MODULES);;
				$search->nameValuePairs['editKoId'] = $this->_super->koId;
				$search->nameValuePairs['editProcess'] = 'Content';
				$search->nameValuePairs['subprocess='] = $this->currentSubprocess;
				$search->nameValuePairs['tutorialStep'] = $_GET['tutorialStep'];

				// Run the search. We don't need to check anythign here, because that happens below
				return $search->getContent();
			}

			// Else, just getting here
			else {
				$this->currentSubprocess = 'AddBlock';
				return $this->getInterface('AddBlock');
			}
		break;

		case 'CopyBlockFromKB':
			d('Cln_Tutorial: getEditContent (currentSubprocess: CopyBlockFromKB)',3);
			// Create the search, and set it's default values
			include_once('Cln_KB_Search.php');
			$search = new Cln_KB_Search();
			$search->excludedModules = Array(1, 2, 8, 9, 10);
			$search->nameValuePairs['editKoId'] = $this->_super->koId;
			$search->nameValuePairs['editProcess'] = 'Content';
			$search->nameValuePairs['subprocess'] = $this->currentSubprocess; 
			$search->nameValuePairs['tutorialStep'] = $_GET['tutorialStep'];
			$search->run();

			if ($search->isComplete()) {
				// Get the blockId
				$blockKoId = $search->userResult;

				$originalBlock = & new $GLOBALS['classes']['ko']['classname']($blockKoId);

				$newBlock = & new $GLOBALS['classes']['ko']['classname']('NEW',$originalBlock->modId);
				$newBlock = $newBlock->copy($blockKoId);
				$newBlock->save();

				$this->addBlockToStepLayout($newBlock->koId, $_GET['tutorialStep']);
				// Only save now for existing objects, not new
				if ($this->tutorialId != 'NEW') {
					$this->saveStep($_GET['tutorialStep']);
				}

				// Go to the block
				PEAR::raiseError('The block was copied and the copied version was added to your tutorial step. Please change the title of this block to something different, and then make any additional changes you want.', E_USER_NOTICE);
				$_SESSION['ProcessManager']->appendData('queryAppend', 'editProcess=Content&subprocess=EditSteps');

				$_SESSION['ProcessManager']->push('editBlock', Array('koId' => $newBlock->koId,
																		'editProcess' => 'Content'));
				$_SESSION['ProcessManager']->goToCurrentProcess();
			}
			else {
				// Return the interface
				$this->currentSubprocess = 'CopyBlockFromKB';
				return $search->getInterface();
			}
		break;

		case 'LinkToBlockInKB':
			d('Cln_Tutorial: getEditContent (currentSubprocess: LinkToBlockInKB)',3);
			// Create the search, and set it's default values
			include_once('Cln_KB_Search.php');
			$search = new Cln_KB_Search();
			$search->excludedModules = Array(1, 2, 8, 9, 10);
			$search->nameValuePairs['editKoId'] = $this->_super->koId;
			$search->nameValuePairs['editProcess'] = 'Content';
			$search->nameValuePairs['subprocess='] = $this->currentSubprocess;
			$search->nameValuePairs['tutorialStep'] = $_GET['tutorialStep'];
			
			$search->run();

			if ($search->isComplete()) {
				// Get the blockId
				$blockKoId = $search->userResult;

				$this->addBlockToStepLayout($blockKoId, $_GET['tutorialStep']);
				PEAR::raiseError('The block you chose was added to this tutorial step you were working on.', E_USER_NOTICE);
				// Only save now for existing objects, not new
				if ($this->tutorialId != 'NEW') {
					$this->saveStep($_GET['tutorialStep']);
				}
				return $this->getInterface('steps');

			}
			else {
				// Return the interface
				$this->currentSubprocess = 'LinkToBlockInKB';
				return $search->getInterface();
			}
		break;

		// 2A. Choosing a Module Type
		case 'ChooseModType':
			d('Cln_Tutorial: getEditContent (currentSubprocess: ChooseModType)',3);
			if (isset($_POST['ChooseModType'])) {
				// They didn't choose a module
				if (!isset($_POST['selectedModule']) || empty($_POST['selectedModule'])) {
					PEAR::raiseError(ADMIN_EM_NO_MODULE, E_USER_WARNING);
					$this->currentSubprocess = 'ChooseModType';
					return $this->getInterface('ChooseModType');
				}
				// They did choose a module, figure it out, and push them to the KO process
				else {
					if (is_numeric($_POST['selectedModule'])) {
						$newModId = $_POST['selectedModule'];
						$newModVariables = FALSE;
					}
					else {
						$splitValue = explode(' ', $_POST['selectedModule']);
						$newModId = $splitValue[0];
						$newModVariables = $splitValue[1];
					}

					// Make the block
					$newKO = & new $GLOBALS['classes']['ko']['classname']('NEW', $newModId);

					// Give it roles
					$newKO->roles = $this->_super->roles;

					// Load the object
					$newKO->loadPartObject(0);
					$newKO->loadMetadata();
					$newKO->currentPart['title'] = 'New Block';

					// Deal with the newModVariables if needed
					if ($newModVariables) {
						$nameValuePairs = explode('|', $newModVariables);
						foreach ($nameValuePairs as $nameValuePair) {
							$nameValue = explode('=', $nameValuePair);
							$newKO->parts['NEW']['object']->$nameValue[0] = $nameValue[1];
						}
					}

					$newKO->save();
					$this->addBlockToStepLayout($newKO->koId, $_GET['tutorialStep']);
					// Only save now for existing objects, not new
					if ($this->tutorialId != 'NEW') {
						$this->saveModuleData();
					}
					$_SESSION['ProcessManager']->appendData('queryAppend', 'editProcess=Content&subprocess=EditSteps');

					$_SESSION['ProcessManager']->push('editBlock', Array('koId' => $newKO->koId,
																		'editProcess' => 'Content'));
					$_SESSION['ProcessManager']->goToCurrentProcess();
				}
			}
			else {
				$this->currentSubprocess = 'ChooseModType';
				return $this->getInterface('ChooseModType');
			}
		break;

		case 'EditBlock':
			d('Cln_Tutorial: getEditContent (currentSubprocess: EditBlock)',3);
			// Have they chosen one?
			if (isset($_POST['selectBlock']) && isset($_POST['addKoId'])) {
				$_SESSION['ProcessManager']->appendData('queryAppend',
													'editProcess=Content&subprocess=EditSteps');
				$_SESSION['ProcessManager']->push('editBlock', Array('koId' => $_POST['addKoId'],
													'editProcess' => 'Content'));
				$_SESSION['ProcessManager']->goToCurrentProcess();
			}
			// Or didn't they choose
			else if (isset($_POST['selectBlock']) && !isset($_POST['addKoId'])) {
				PEAR::raiseError('You need to select a block to edit', E_USER_WARNING);
				$this->currentSubprocess = 'EditBlock';
				return $this->getInterface('EditBlock');
			}
			// Or are the just getting here
			else {
				$this->currentSubprocess = 'EditBlock';
				return $this->getInterface('EditBlock');
			}
		break;

		case 'DeleteBlock':
			d('Cln_Tutorial: getEditContent (currentSubprocess: DeleteBlock)',3);
			// Have they chosen one?
			if (isset($_POST['selectBlock']) && isset($_POST['addKoId'])) {
				$this->currentSubprocess = 'DeleteBlockConfirmation';
				return $this->getInterface('DeleteBlockConfirmation');
			}
			// Or didn't they choose
			else if (isset($_POST['selectBlock']) && !isset($_POST['addKoId'])) {
				PEAR::raiseError('You need to select a block to delete', E_USER_WARNING);
				$this->currentSubprocess = 'DeleteBlock';
				return $this->getInterface('DeleteBlock');
			}
			// Or are the just getting here
			else {
				$this->currentSubprocess = 'DeleteBlock';
				return $this->getInterface('DeleteBlock');
			}
		break;

		case 'DeleteBlockConfirmation':
			d('Cln_Tutorial: getEditContent (currentSubprocess: DeleteBlockConfirmation)',3);
			// If they've submitted the form
			if (isset($_POST['DeleteBlockConfirmation'])) {
				// Are we deleting it completely from the KB?
				if (isset($_POST['deleteFromKB']) && $_POST['deleteFromKB']) {
					$this->deleteBlockFromStep($_POST['deleteBlockKoId'], $_GET['tutorialStep']);
					if (isset($_SESSION['Page'])) {
						$_SESSION['Page']->currentPart['object']->deleteBlockFromKB($_POST['deleteBlockKoId']);
					}

					// Only save now for existing objects, not new
					if ($this->tutorialId != 'NEW') {
						$this->saveModuleData();
					}
					PEAR::raiseError('The block was removed from this step in your Tutorial, and from the knowledge base. You should: <ul><li>Check your tutorial step layout to make sure it still makes sense without the block you removed</li><li>Be sure to publish your tutorial when you\'re finished making your changes</li></ul>', E_USER_NOTICE);
					return $this->getInterface('steps');
				}
				// Or just from this page
				else {
					$this->deleteBlockFromStep($_POST['deleteBlockKoId'], $_GET['tutorialStep']);
					// Only save now for existing objects, not new
					if ($this->tutorialId != 'NEW') {
						$this->saveModuleData();
					}
					PEAR::raiseError('The block was removed from this tutorial step. You should: <ul><li>Check your step layout to make sure it still makes sense without the block you removed</li><li>Be sure to publish your tutorial when you\'re finished making your changes</li></ul>', E_USER_NOTICE);
					return $this->getInterface('steps');
				}
			}
			// Else if they're cancelling the delete
			else if (isset($_POST['cancel'])) {
				return FALSE;
			}
			// Or they're just getting here
			else {
				$this->currentSubprocess = 'DeleteBlock';
				return $this->getInterface('DeleteBlock');
			}
		break;

		case 'EditLayout':
			if (!isset($_GET['tutorialStep'])) {
				PEAR::raiseError('There was an error editing a step', E_USER_WARNING);
				return $this->getInterface('steps');
			}

			// capture new layout
			else if(isset($_POST['editLayout'])) {
				$stepNumber = $_REQUEST['tutorialStep'];
				$this->steps[$stepNumber]['layout'] = strtolower($_POST['editLayout']);

				if ($this->tutorialId != 'NEW') {
					$this->saveStep($stepNumber);
				}

				return $this->getInterface('steps');
			}

			// Do all the stuff related to editing layout
			else {
				return $this->getInterface('layout');
			}
		break;

		case 'DeleteStep':
			// Have they confirmed?
			if (isset($_POST['deleteStepConfirm']) && isset($_POST['deleteStepId'])) {
				// Do the delete
				$this->deleteStep($_POST['deleteStepId']);
				PEAR::raiseError('The step you chose was deleted from the tutorial', E_USER_WARNING);
				return $this->getInterface('steps');
			}
			// Or didn't they're cancelling
			else if (isset($_POST['deleteStepCancel'])) {
				return $this->getInterface('steps');
			}
			// Or are the just getting here
			else {
				$this->currentSubprocess = 'DeleteStep';
				return $this->getInterface('DeleteStep');
			}
		break;

		case 'MoveStepUp':
			if (isset($_GET['tutorialStep']) && $_GET['tutorialStep'] > 0 && $_GET['tutorialStep'] < count($this->steps)) {
				echo 'Moving Up';
				$fromStep = $_GET['tutorialStep'];
				$toStep = $_GET['tutorialStep'] - 1;
				$this->swapSteps($fromStep, $toStep);
			}
			return $this->getInterface('steps');
		break;

		case 'MoveStepDown':
			if (isset($_GET['tutorialStep']) && $_GET['tutorialStep'] >= 0 && $_GET['tutorialStep'] < (count($this->steps) - 1)) {
				echo 'Moving Down';
				$fromStep = $_GET['tutorialStep'];
				$toStep = $_GET['tutorialStep'] + 1;
				$this->swapSteps($fromStep, $toStep);
			}
			return $this->getInterface('steps');
		break;

		default:
			return FALSE;
		break;
		}
	}


	/*
	 *
	 * Function:  getPublishData()
	 *
	 * Should return an array of all the data from an object that is needed by another
	 * object to publish it.
	 *
	 *
	 * @access public
	 * @return TBD
	 *
	 */
	function getPublishData()
	{
		$returnVal['title'] = $this->title;
		$returnVal['topic'] = $this->topic;
		$returnVal['objectives'] = $this->objectives;
		$returnVal['steps'] = $this->steps;
		foreach ($returnVal['steps'] as $stepNumber => $stepInfo) {
			$returnVal['steps'][$stepNumber]['tutorialStepId'] = 'NEW';
			$returnVal['steps'][$stepNumber]['tutorialLayoutId'] = 'NEW';
		}

		return $returnVal;
	}


	/*
	 *
	 * Function:  publish()
	 *
	 * Should take an array of all the data from an object that is needed by another
	 * object to publish it.
	 *
	 * @access public
	 * @return TBD
	 *
	 */
	function publish($publishData = FALSE)
	{
		if ($publishData) {
			$this->title = $publishData['title'];
			$this->topic = $publishData['topic'];
			$this->objectives = $publishData['objectives'];


			if (isset($publishData['steps'])) {
				$this->deleteSteps();
				$this->steps = $publishData['steps'];
			}
		}
		return $this->saveModuleData();
	}



	/*
	 *
	 * Function:  saveModuleData()
	 *
	 * 		Saves the KO
	 *
	 * @access public
	 * @return TBD
	 *
	 */
	function saveModuleData()
	{
		d('Tutorial: saveModuleData()', 3);
		$db = &Cln_Db::singleton(MAIN_CLN_DSN);

		// New module, get an ID and save the main stuff
		if ($this->tutorialId == 'NEW') {
			$this->tutorialId = $db->nextId(TABLE_KB_tutorial);

			$sql = sprintf("INSERT INTO %s SET tutorialId = '%s', title = '%s', topic = '%s', objectives = '%s', created = NOW();",
							TABLE_KB_tutorial, $this->tutorialId, addslashes($this->title), addslashes($this->topic), addslashes($this->objectives));
		}
		// Existing module, save the main stuff
		else {
			$sql = sprintf("UPDATE %s SET title = '%s', topic = '%s', objectives = '%s' WHERE tutorialId = %s",
								TABLE_KB_tutorial, addslashes($this->title), addslashes($this->topic), addslashes($this->objectives), $this->tutorialId);
		}
		$result = $db->query($sql);
		if (PEAR::isError($result))	 {
			PEAR::raiseError('ADMIN_EM_DB_ERROR_INSERT', E_USER_ERROR);
			PEAR::raiseError("Error on tutorial ko insert/update: $sql", E_ERROR);
			return FALSE;
		}
		else {
			if ($this->saveSteps()) {
				d("returning {$this->tutorialId} from tutorial saveModuleData", 5);
				$this->updateModified();
				return $this->tutorialId;
			}
			else {
				d('returning FALSE from tutorial save module data', 5);
				return FALSE;
			}
		}
	}


	/*
	 *
	 * Function:  deleteStep()
	 *
	 * 		Saves the Steps
	 *
	 * @access public
	 * @return TBD
	 *
	 */
	function deleteStep($stepNumber)
	{
		$db = &Cln_Db::singleton(MAIN_CLN_DSN);

		$sql = sprintf('DELETE FROM `%s` WHERE tutorialLayoutId = %d',
						TABLE_KB_tutorial_layout, $this->steps[$stepNumber]['tutorialLayoutId']);
		$db->query($sql);

		$sql = sprintf('DELETE FROM `%s` WHERE tutorialLayoutId = %d',
						TABLE_KB_tutorial_step, $this->steps[$stepNumber]['tutorialLayoutId']);
		$db->query($sql);

		$numSteps = count($this->steps);
		unset($this->steps[$stepNumber]);

		for($x = $stepNumber; $x < ($numSteps - 1); $x++) {
			$this->steps[$x] = $this->steps[$x+1];
			$this->steps[$x]['order'] = $x;
		}
		unset($this->steps[($numSteps - 1)]);

		$this->saveSteps();
	}


	/*
	 *
	 * Function:  saveSteps()
	 *
	 * 		Saves the Steps
	 *
	 * @access public
	 * @return TBD
	 *
	 */
	function saveSteps()
	{
		// Loop through the steps saving each one
		$result = TRUE;
		foreach ($this->steps as $stepNumber => $stepInfo) {
			$result = $this->saveStep($stepNumber);
		}
		return $result;
	}


	/*
	 *
	 * Function:  saveStep()
	 *
	 * 		Saves a passed step
	 *
	 * @access public
	 * @return TBD
	 *
	 */
	function saveStep($stepNumber)
	{
		$db = &Cln_Db::singleton(MAIN_CLN_DSN);

		// Make a reference to the step
		$step = & $this->steps[$stepNumber];

		// First save the layout
		// If it's new
		if ($step['tutorialLayoutId'] == 'NEW') {
			$step['tutorialLayoutId'] = $db->nextId(TABLE_KB_tutorial_layout);
			$sql = sprintf("INSERT INTO `%s` SET tutorialLayoutId = %d, layout = '%s', stepOrder = %d, created = NOW()",
							TABLE_KB_tutorial_layout, $step['tutorialLayoutId'], addslashes($step['layout']), $step['order']);
		}
		// Else it's existing
		else {
			$sql = sprintf("UPDATE `%s` SET layout = '%s', stepOrder = %d WHERE tutorialLayoutId = %d",
							TABLE_KB_tutorial_layout, addslashes($step['layout']), $step['order'], $step['tutorialLayoutId']);
		}

		$result = $db->query($sql);
		if (PEAR::isError($result)) {
			PEAR::raiseError("Error on tutorial step layout saving: $sql", E_ERROR);
			return FALSE;
		}

		// Then save the step
		// If it's new
		if ($step['tutorialStepId'] == 'NEW') {
			$step['tutorialStepId'] = $db->nextId(TABLE_KB_tutorial_step);
			$sql = sprintf("INSERT INTO `%s` SET tutorialStepId = %d, tutorialId = %d, tutorialLayoutId = %d, title = '%s', created = NOW()",
							TABLE_KB_tutorial_step, $step['tutorialStepId'], $this->tutorialId, $step['tutorialLayoutId'], addslashes($step['title']));
		}
		// Else it's existing
		else {
			$sql = sprintf("UPDATE `%s` SET tutorialId = %d, tutorialLayoutId = %d, title = '%s' WHERE tutorialStepId = %d",
							TABLE_KB_tutorial_step, $this->tutorialId, $step['tutorialLayoutId'], addslashes($step['title']), $step['tutorialStepId']);
		}

		$result = $db->query($sql);
		if (PEAR::isError($result)) {
			PEAR::raiseError("Error on tutorial step saving: $sql", E_ERROR);
			return FALSE;
		}

		$this->updateModified();
		return TRUE;

	}


	/*
	 *
	 * Function:  swapSteps()
	 *
	 * 		Swaps the order of 2 steps
	 *
	 * @access public
	 * @return TBD
	 *
	 */
	function swapSteps($fromStep, $toStep)
	{
		$middleStep = $this->steps[$fromStep];
		$this->steps[$fromStep] = $this->steps[$toStep];
		$this->steps[$toStep] = $middleStep;
		$this->steps[$fromStep]['order'] = $fromStep;
		$this->steps[$toStep]['order'] = $toStep;
		$this->saveSteps();
	}
	/***********************************************************************************
	 *
	 * Internally accessed methods
	 *
	 **********************************************************************************/

	/*
	 *
	 * Function:  loadModuleData()
	 *
	 * 		Loads the Database data
	 *
	 * @access public
	 * @return TBD
	 *
	 */
	function loadModuleData()
	{
		// Load for existing modules
		if ($this->tutorialId != 'NEW' && is_numeric($this->tutorialId)) {
			$sql = 'SELECT title, topic, objectives FROM ' . TABLE_KB_tutorial . ' WHERE tutorialId = \'' . $this->tutorialId . '\'';

			$result = &Cln_Db::singleton(MAIN_CLN_DSN, $sql);
			$row = $result->fetchRow(DB_FETCHMODE_OBJECT);
			$this->title = stripslashes($row->title);
			$this->topic = stripslashes($row->topic);
			$this->objectives = stripslashes($row->objectives);
			$this->loadSteps();
			return TRUE;
		}
		else {
			return TRUE;
		}
	}


	/*
	 *
	 * Function:  loadSteps()
	 *
	 * 		Loads the steps from the database
	 *
	 * @access public
	 * @return TBD
	 *
	 */
	function loadSteps()
	{
		$db = &Cln_Db::singleton(MAIN_CLN_DSN);
		$sql = sprintf("SELECT s.tutorialStepId, l.tutorialLayoutId, s.title, l.layout, l.stepOrder
						FROM `%s` AS s, `%s` as l
						WHERE s.tutorialLayoutId = l.tutorialLayoutId
						AND s.tutorialId = %d
						ORDER BY l.stepOrder",
						TABLE_KB_tutorial_step, TABLE_KB_tutorial_layout, $this->tutorialId);

		$result = $db->query($sql);
		if (PEAR::isError($result)) {
			PEAR::raiseError("Error loading steps for Tutorial Block: $sql", E_ERROR);
			return FALSE;
		}
		else {
			$x = 0;
			while ($row = $result->fetchRow(DB_FETCHMODE_ASSOC)) {
				$this->steps[$x]['tutorialStepId'] = $row['tutorialStepId'];
				$this->steps[$x]['tutorialLayoutId'] = $row['tutorialLayoutId'];
				$this->steps[$x]['title'] = $row['title'];
				$this->steps[$x]['layout'] = $row['layout'];
				$this->steps[$x]['order'] = $row['stepOrder'];
				$x++;
			}
		}
	}


	/*
	 *
	 * Function:  getTeaser()
	 *
	 * 		returns the 'teaser', which is just the entry point to the tutorial, a small block
	 *		that just has the title, and topic, with a link to start the tutorial
	 *
	 * @access public
	 * @return TBD
	 *
	 */
	function getTeaser()
	{
		$content  = "<h2>Tutorial: {$this->title}</h2>\n";
		$content .= "<p>Topic: {$this->topic}</p>\n";
		$content .= '<p><b><a href="' . appendToURL(cleanURL($GLOBALS['path']), 'viewKoId=' . $this->_super->koId) . '">Start the tutorial! &gt;&gt;</a></b></p>';
		return $content;
	}


	/*
	 *
	 * Function:  getTOC()
	 *
	 * 		returns the Table of Contents for the tutorial, acts as the 'Home Page' for the tutorial, displays
	 *		all the steps, and the learning objectives, etc
	 *
	 * @access public
	 * @return TBD
	 *
	 */
	function getTOC()
	{
		$content  = "<h2>{$this->title} - Table of Contents</h2>\n";
		$content .= "<p>Topic: {$this->topic}</p>\n";
		if (!empty($this->objectives)) {
			$content .= "<p><b>In this tutorial you will learn:</b>\n";
			$content .= "{$this->objectives}</p>\n";
		}

		$x = 1;
		$content .= "<ul>\n";
		foreach ($this->steps as $stepNumber => $stepInfo) {
			$content .= '<li>Step ' . $x . ': <a href="' . appendToURL(cleanURL($GLOBALS['path']), 'viewKoId=' . $this->_super->koId . '&tutorialStep=' . $stepInfo['order']) . '">' . $stepInfo['title'] . "</a></li>\n";
			$x++;
		}
		$content .= "</ul>\n";

		$content .= $this->getForwardBack();

		return $content;
	}


	/*
	 *
	 * Function:  getStep()
	 *
	 * 		returns a step, by parsing the layout, etc
	 *
	 * @access public
	 * @return TBD
	 *
	 */
	function getStep($stepNumber)
	{
		$step = & $this->steps[$stepNumber];

		$humanNumber = $stepNumber + 1;
		$content  = "<h2>{$this->title} - Step {$humanNumber} of " . count($this->steps) . "</h2>\n";
		$content .= "<h3>{$step['title']}</h3>";
		$content .= $this->getForwardBack();

		// Parse the layout
		require_once('LayoutParser.php');

		$xml_parser = new LayoutParser();
		$layoutToParse = $step['layout'];
		if($layoutContent = $xml_parser->parse($layoutToParse)) {
			$content .= $layoutContent;
		}
		else {
			PEAR::raiseError('LayoutParser could not process tutorial step layout', E_WARNING);
			return FALSE;
		}

		// Add the forward/back
		$content .= $this->getForwardBack();

		return $content;
	}


	/*
	 *
	 * Function:  getForwardBack()
	 *
	 * 		returns links forward and back in the tutorial steps, depending on where we are
	 *
	 * @access public
	 * @return TBD
	 *
	 */
	function getForwardBack()
	{

	//Write the start of the Nav
	$content = '<br/><p><div class="tutorialStepNav"><b>Tutorial Navigation:</b> <a href="' . appendToURL(cleanURL($GLOBALS['path']), 'viewKoId=' . $this->_super->koId) . '">Table of Contents</a><br/>';

		// If they're currently in a step
		if (isset($_GET['tutorialStep'])) {
			if ($_GET['tutorialStep'] > 0) {
				$previousStep = $_GET['tutorialStep'];
				$content .= '<a href="' . appendToURL(cleanURL($GLOBALS['path']), 'viewKoId=' . $this->_super->koId . '&tutorialStep=' . ($previousStep - 1)) . '">&lt;&lt; Back to Step ' . $previousStep . '</a>';
			}
			$content .= ' | You\'re on Step ' . ($previousStep + 1) . ' | ';
			if ($_GET['tutorialStep'] < (count($this->steps) - 1)) {
				$nextStep = $_GET['tutorialStep'] + 2;
				$content .= '<a href="' . appendToURL(cleanURL($GLOBALS['path']), 'viewKoId=' . $this->_super->koId . '&tutorialStep=' . ($nextStep - 1)) . '">Go to Step ' . $nextStep . ' &gt;&gt;</a>';
			}

			$content .= '</div></p>';
		}
		// Else they're just starting
		else {
			$content .= '<a href="' . appendToURL(cleanURL($GLOBALS['path']), 'viewKoId=' . $this->_super->koId . '&tutorialStep=0') . '">Go to Step 1 &gt;&gt;</a></div></p>';
		}
		return $content;
	}

	/*
	 *
	 * Function:  addStep()
	 *
	 * 		adds a new step to the steps list
	 *
	 * @access public
	 * @return TBD
	 *
	 */
	function addStep()
	{
		$x = count($this->steps);

		$this->steps[$x]['tutorialStepId'] = 'NEW';
		$this->steps[$x]['tutorialLayoutId'] = 'NEW';
		$this->steps[$x]['title'] = 'Tutorial Step';
		$this->steps[$x]['layout'] = '<tutorial><layout><row><column></column></row></layout></tutorial>';
		$this->steps[$x]['order'] = $x;
		return $x;
	}


	/*
	 *
	 * Function:  captureLinkData()
	 *
	 * 		Captures the submitted links
	 *
	 * @access public
	 * @return TBD
	 *
	 */
	function captureLinkData()
	{
		foreach ($_POST['linkDescription'] as $linkNumber => $description) {
			if (empty($_POST['linkUri'][$linkNumber]) && empty($_POST['linkTitle'][$linkNumber]) && empty($description)) {
				unset($this->links[$linkNumber]);
			}
			else {
				$this->links[$linkNumber]['uri'] = $_POST['linkUri'][$linkNumber];
				$this->links[$linkNumber]['title'] = $_POST['linkTitle'][$linkNumber];
				$this->links[$linkNumber]['description'] = $description;
			}
		}
		return TRUE;
	}


	/*
	 *
	 * Function:  validateLinkData()
	 *
	 * 		validates the link data
	 *
	 * @access public
	 * @return TBD
	 *
	 */
	function validateLinkData()
	{
		foreach ($this->links as $linkNumber => $linkInfo) {
			if (!validateString($linkInfo['uri'], 'URL')) {
				$invalidUris = TRUE;
				$this->links[$linkNumber]['invalidUri'] = TRUE;
			}
			if (empty($linkInfo['title'])) {
				$invalidTitles = TRUE;
				$this->links[$linkNumber]['invalidTitle'] = TRUE;
			}
		}

		if (isset($invalidUris) || isset($invalidTitles)) {
			PEAR::raiseError('Some of the information below is incorrect. It has been highlighted in red. Remember, the web address must start with http://', E_USER_WARNING);
			return FALSE;
		}
		else {
			return TRUE;
		}
	}

	/*
	 *
	 * Function:  getInterface()
	 *
	 * 		Returns the edit form for this module
	 *
	 * @access public
	 * @return TBD
	 *
	 */
	function getInterface($interfaceName)
	{
		if ($interfaceName == 'details') {
			$interfacePath = MOD_CLN_MODULE_TUTORIAL_PATH . 'TutorialEditForm.html';
		}
		else if ($interfaceName == 'steps') {
			$interfacePath = MOD_CLN_MODULE_TUTORIAL_PATH . 'TutorialStepsEditForm.html';
		}
		else if ($interfaceName == 'step') {
			$interfacePath = MOD_CLN_MODULE_TUTORIAL_PATH . 'TutorialStepEditForm.html';
		}
		else if ($interfaceName == 'layout') {
			$interfacePath = MOD_CLN_MODULE_TUTORIAL_PATH . 'TutorialEditLayout.html';
		}
		else if ($interfaceName == 'AddBlock') {
			$interfacePath = MOD_CLN_MODULE_TUTORIAL_PATH . 'TutorialAddBlock.html';
		}
		else if ($interfaceName == 'ChooseModType') {
			$interfacePath = MOD_CLN_MODULE_TUTORIAL_PATH . 'TutorialChooseModType.html';
		}
		else if ($interfaceName == 'EditBlock') {
			$interfacePath = MOD_CLN_MODULE_TUTORIAL_PATH . 'TutorialEditBlock.html';
		}
		else if ($interfaceName == 'DeleteBlock') {
			$interfacePath = MOD_CLN_MODULE_TUTORIAL_PATH . 'TutorialDeleteBlock.html';
		}
		else if ($interfaceName == 'DeleteBlockConfirmation') {
			$interfacePath = MOD_CLN_MODULE_TUTORIAL_PATH . 'TutorialDeleteBlockConfirmation.html';
		}
		else if ($interfaceName == 'DeleteStep') {
			$interfacePath = MOD_CLN_MODULE_TUTORIAL_PATH . 'TutorialDeleteStep.html';
		}
		includeLangFile('lib/CLN/lang/Page-Process');
		ob_start();
		include($interfacePath);
		$form = ob_get_contents();
		ob_end_clean();
		return $form;
	}



	/*
	 *
	 * Function:  validateEditData()
	 *
	 * 		Validates the data for validity
	 *
	 * @access public
	 * @return TBD
	 *
	 */
	function validateEditData()
	{
		return TRUE;
	}


	/***********************************************************************************
	 *
	 * Private methods
	 *
	 **********************************************************************************/

	/*
	 *
	 * Function:  delete()
	 *
	 * 		Deletes the object
	 *
	 * @access public
	 * @return TBD
	 *
	 */
	function delete()
	{
		$sql = sprintf("DELETE FROM `%s` WHERE tutorialId = '%d'", TABLE_KB_tutorial, $this->tutorialId);
		$db = &Cln_Db::singleton(MAIN_CLN_DSN);
		$result = $db->query($sql);

		if (PEAR::isError($result)) {
			PEAR::raiseError("There was an error deleting the Bookmark mod part: $sql", E_ERROR);
			return FALSE;
		}
		else {
			$this->deleteSteps();
			return TRUE;
		}
	}


	/*
	 *
	 * Function:  deleteSteps()
	 *
	 * 		Deletes the steps
	 *
	 * @access public
	 * @return TBD
	 *
	 */
	function deleteSteps()
	{
		$db = &Cln_Db::singleton(MAIN_CLN_DSN);

		foreach ($this->steps as $stepNumber => $stepInfo) {
			if ($stepInfo['tutorialStepId'] != 'NEW') {
				$sql = sprintf('DELETE FROM `%s` WHERE tutorialStepId = %d', TABLE_KB_tutorial_step, $stepInfo['tutorialStepId']);
				$result = $db->query($sql);
				if (PEAR::isError($result)) {
					PEAR::raiseError("Error deleting tutorial step: $sql", E_ERROR);
				}
			}
			if ($stepInfo['tutorialLayoutId'] != 'NEW') {
				$sql = sprintf('DELETE FROM `%s` WHERE tutorialLayoutId = %d', TABLE_KB_tutorial_layout, $stepInfo['tutorialLayoutId']);
				$result = $db->query($sql);
				if (PEAR::isError($result)) {
					PEAR::raiseError("Error deleting tutorial step: $sql", E_ERROR);
				}
			}
			unset($this->steps[$stepNumber]);
		}
	}

	/*
	 *
	 * Function:  getBlockIds()
	 *
	 * 		returns an array of blockIds for the blocks that are on the passed step
	 *
	 * @access public
	 * @return TBD
	 *
	 */
	function getBlockIds($stepNumber)
	{
		$search = '/block koid="([0-9]+)/i';
		preg_match_all($search, $this->steps[$stepNumber]['layout'], $blockStrings);

		return $blockStrings[1];
	}


	/*
	 *
	 * Function:  deleteBlockFromStep()
	 *
	 * 		Deletes a block from the step passed
	 *
	 * @access public
	 * @return TBD
	 *
	 */
	function deleteBlockFromStep($koId, $stepNumber)
	{
		// Look for the block
		$search = '<block koId="' . $koId . '"[a-z="0-9 ]* />';
		$replace = '';

		$this->steps[$stepNumber]['layout'] = eregi_replace($search, $replace, $this->steps[$stepNumber]['layout']);
	}

	/*
	 *
	 * Function:  addBlockToStepLayout()
	 *
	 * 		Adds a block to the passed step's layout
	 *
	 * @access public
	 * @return TBD
	 *
	 */
	function addBlockToStepLayout($koId, $stepNumber)
	{
		d('TutorialId: ' . $this->tutorialId . ': Cln_Tutorial addBlockToStepLayout');
		$search = '</layout>';
		$replace = '<row><column><block koId="' . $koId . '" /></column></row></layout>';
		$newLayout = str_replace($search, $replace, $this->steps[$stepNumber]['layout']);

		$this->steps[$stepNumber]['layout'] = $newLayout;
	}
}



//********************************************************************

?>
Return current item: Community Learning Network