ATutor 1.5.2 introduced the concept of modules, providing developers with a framework to implement additional functionality in a coherent and loosely coupled way.
The framework defines methods for assigning privileges, backing-up and restoring content, deleting course specific content, and adding side menu blocks, student tools, course management and administrative tools, as well as public tools and other types of added functionality .
The intent is to allow for the development and distribution of modules independent of the ongoing development and release of ATutor. The module structure does allow for the creation of modules that run software that is not distributed under the GNU General Public License, but distributed separately under their own, perhaps commercial licenses.
The Hello World example module is included with each ATutor distribution for developers who want to investigate how modules work. The module is found in the mods/hello_world directory. A copy of the Hello World module works well as a starting point for creating a new module, since it implements (in a simple way) just about all the features found in modules. Also see the files from other modules that operate like you expect your module to operate.
Modules are stored under ATutor's mods directory. Core modules are stored in the mods/_core subdirectory and are made available with every release of ATutor. These modules cannot be disabled by the administrator as they are vital to ATutor's functionality. Standard modules are stored in the mods/_standard subdirectory and are also made available with every release of ATutor. Standard modules can be disabled by the administrator. Extra modules are stored in the mods directory and are installed and distributed independently of ATutor. Although the process of developing modules is the same for each type of module, only extra modules can be distributed separately, while core and standard modules are added to the ATutor code repository (i.e. SVN trunk).
Whenever a module identifier is needed within code, it should appear in lowercase with spaces converted to underscores.
The module name, and hence the directory and function names (see below for additional details), must be unique across all possible modules. A module should not be made available if an existing module is already being distributed under that same name. It is up to the module developer to ensure that their module name is unique.
The name given to the directory must be chosen carefully. The name is used to namespace the module's function by prefixing required functions with that directory name. For example, a module named Example Maker should be placed in a directory named example_maker and the delete function would be named example_maker_delete().
The following files should exist under the module's top level directory: mods/module_name.
The module.xml file is used for displaying information about the module before it is installed and is useful when distributing the module.
<?xml version="1.0" encoding="ISO-8859-1"?> <module version="0.1"> <name lang="en">Example Maker</name> <description lang="en">This is an example module that makes examples.</description> <maintainers> <maintainer> <name>ATutor Team</name> <email>info@atutor.ca</email> </maintainer> <maintainer> <name>John Doe</name> <email>jd@example.com</email> </maintainer> </maintainers> <url>http://www.example.com</url> <license>BSD</license> <release> <version>0.2</version> <date>2005-08-22</date> <state>stable</state> <notes>Fixes several bugs in previous version.</notes> </release> </module>
The module.php file is typically used to set permissions, and to link module components into the ATutor navigation elements, as tool icons, navigation tabs, sub navigation menus, or side menu blocks, as administrator, course management, and student tools. It is also used to link tools to My Start Page, or to various public pages that appear where a user is browsing the system without being logged in. The module.php file can also be used to run module specific functionality. It runs everytime a module screen is viewed, loading whatever settings it may contain, or running any scripts that may be required by the module.
<?php /******* * doesn't allow this file to be loaded with a browser. */ if (!defined('AT_INCLUDE_PATH')) { exit; } /****** * this file must only be included within a Module obj */ if (!isset($this) || (isset($this) && (strtolower(get_class($this)) != 'module'))) { exit(__FILE__ . ' is not a Module'); } /******* * assign the instructor and admin privileges to the constants. */ define('AT_PRIV_EXAMPLE_MAKER', $this->getPrivilege()); define('AT_ADMIN_PRIV_EXAMPLE_MAKER', $this->getAdminPrivilege()); /******* * create a side menu box/stack. */ $this->_stacks['example_maker'] = array('title_var'=>'example_maker', 'file'=>'mods/example_maker/side_menu.inc.php'); // ** possible alternative: ** // $this->addStack('example_maker', array('title_var' => 'example_maker', 'file' => './side_menu.inc.php'); /******* * if this module is to be made available to students on the Home or Main Navigation. */ $_student_tool = 'mods/example_maker/index.php'; // ** possible alternative: ** // $this->addTool('./index.php'); /******* * add the admin pages when needed. */ if (admin_authenticate(AT_ADMIN_PRIV_EXAMPLE_MAKER, TRUE) || admin_authenticate(AT_ADMIN_PRIV_ADMIN, TRUE)) { $this->_pages[AT_NAV_ADMIN] = array('mods/example_maker/index_admin.php'); $this->_pages['mods/example_maker/index_admin.php']['title_var'] = 'example_maker'; $this->_pages['mods/example_maker/index_admin.php']['parent'] = AT_NAV_ADMIN; } /******* * instructor Manage section: */ $this->_pages['mods/example_maker/index_instructor.php']['title_var'] = 'example_maker'; $this->_pages['mods/example_maker/index_instructor.php']['parent'] = 'tools/index.php'; // ** possible alternative: ** // $this->pages['./index_instructor.php']['title_var'] = 'example_maker'; // $this->pages['./index_instructor.php']['parent'] = 'tools/index.php'; /******* * student page. */ $this->_pages['mods/example_maker/index.php']['title_var'] = 'example_maker'; $this->_pages['mods/example_maker/index.php']['img'] = 'mods/example_maker/example_maker.jpg'; /* public pages */ $this->_pages[AT_NAV_PUBLIC] = array('mods/example_maker/index_public.php'); $this->_pages['mods/example_maker/index_public.php']['title_var'] = 'example_maker'; $this->_pages['mods/example_maker/index_public.php']['parent'] = 'login.php'; $this->_pages['login.php']['children'] = array('mods/example_maker/index_public.php'); /* my start page pages */ $this->_pages[AT_NAV_START] = array('mods/example_maker/index_mystart.php'); $this->_pages['mods/example_maker/index_mystart.php']['title_var'] = 'example_maker'; $this->_pages['mods/example_maker/index_mystart.php']['parent'] = 'users/index.php'; $this->_pages['users/index.php']['children'] = array('mods/example_maker/index_mystart.php'); ?>
A very simple module.sql file such as the following, creates a table for the module, and inserts it's language into the ATutor language_text table. Module language can then be managed from the ATutor Language Manager. The module.sql file can contain any number of SQL statements used to add tables or insert data into the ATutor database.
# sql file for example maker module CREATE TABLE example_maker ( `course_id` mediumint(8) unsigned NOT NULL, `value` VARCHAR( 30 ) NOT NULL , PRIMARY KEY ( `course_id` ) ); INSERT INTO `language_text` VALUES ('en', '_module','example_maker','Example Maker',NOW(),''); INSERT INTO `language_text` VALUES ('en', '_module','AT_ERROR_GOES_HERE','Example Maker Error Message',NOW(),'');
The module_install.php script gets executed during the installation process, using the administrator's Install Module feature. If the script's execution results in $msg->containsErrors() evaluating to TRUE, then the errors are displayed and the user is prompted to correct them. The process is then repeated until errors are no longer being generated and the module is installed successfully. Ultimately, it is up to the module to determine the logical steps involved in its installation. For example, it might be better to create the data directories before trying to create any database tables since creating the directory may require several attempts. Typically the flow we describe here should be suitable in most cases.
Theoretically, the install script's execution is wide-open and does not have to adhere to the process outlined below or make use of any special privileges, provided it generates errors as appropriate.
# pseudo-code for installing a module: while (there are errors) print the error message # inside module_install.php: define the privileges used if (create database tables is unsuccessful) then generate an error message if (there are no errors AND there is an SQL file) then execute the SQL file end while add the module to the system using the defined privileges
The module_install.php file is typically used to run any installation related files, such as an sql file that sets up a database table, or installs the language for the module. The following is an example module_install.php file
<?php /******* * the line below safe-guards this file from being accessed directly from * a web browser. It will only execute if required from within an ATutor script, * in our case the Module::install() method. */ if (!defined('AT_INCLUDE_PATH')) { exit; } /******* * Note: the many options for these variables are used to decrease confusion. * TRUE | FALSE | 1 will be the convention. * * $_course_privilege * specifies the type of instructor privilege this module uses. * set to empty | FALSE | 0 to disable any privileges. * set to 1 | AT_PRIV_ADMIN to use the instructor only privilege. * set to TRUE | 'new' to create a privilege specifically for this module: * will make this module available as a student privilege. * * $_admin_privilege * specifies the type of ATutor administrator privilege this module uses. * set to FALSE | AT_ADMIN_PRIV_ADMIN to use the super administrator only privilege. * set to TRUE | 'new' to create a privilege specifically for this module: * will make this module available as an administrator privilege. * * * $_cron_interval * if non-zero specifies in minutes how often the module's cron job should be run. * set to 0 or not set to disable. */ $_course_privilege = TRUE; // possible values: FALSE | AT_PRIV_ADMIN | TRUE $_admin_privilege = TRUE; // possible values: FALSE | TRUE $_cron_interval = 35; // run every 30 minutes /******** * the following code is used for creating a module-specific directory. * it generates appropriate error messages to aid in its creation. */ $directory = AT_CONTENT_DIR .'example_maker'; // check if the directory is writeable if (!is_dir($directory) && !@mkdir($directory)) { $msg->addError(array('MODULE_INSTALL', '<li>'.$directory.' does not exist. Please create it.</li>')); } else if (!is_writable($directory) && @chmod($directory, 0666)) { $msg->addError(array('MODULE_INSTALL', '<li>'.$directory.' is not writeable. On Unix issue the command <kbd>chmod a+rw</kbd>.</li>')); } /****** * the following code checks if there are any errors (generated previously) * then uses the SqlUtility to run any database queries it needs, ie. to create * its own tables. */ if (!$msg->containsErrors() && file_exists(dirname(__FILE__) . '/module.sql')) { // deal with the SQL file: require(AT_INCLUDE_PATH . 'classes/sqlutility.class.php'); $sqlUtility = new SqlUtility(); /* * the SQL file could be stored anywhere, and named anything, "module.sql" is simply * a convention we're using. */ $sqlUtility->queryFromFile(dirname(__FILE__) . '/module.sql', TABLE_PREFIX); } ?>
Privileges control who has access to the course management and administrative sections.
See the Authentication & Privileges section for additional details using the privileges.
This variable controls access to a course's management section and can take one of the following values:
This variable can take one of the following values:
Note that creating a privilege is not in itself enough to make the module appear in the Manage section! The hierarchy and navigation path to the management page must be set correctly. See the Navigation & Hierarchy section for additional details.
It is best to keep the directory within the AT_CONTENT_DIR directory as it should already allow the creation of files and directories by the web server. It is then up to the module to create individual course directories as needed.
$directory = AT_CONTENT_DIR .'example_maker'; // check if the directory is writable if (!is_dir($directory) && !@mkdir($directory)) { $msg->addError(array('MODULE_INSTALL', '<li>' .$directory .' does not exist. Please create it.</li>')); } else if (!is_writable($directory) && @chmod($directory, 0666)) { $msg->addError(array('MODULE_INSTALL', '<li>' . $directory .' is not writable.</li>')); } // else: it was created successfully.
If the module requires its own database tables or custom language, then it will have to create them itself. The SQL can either be executed inline using PHP database execution directly, or using the SQLUtility class to execute an external SQL file. Also see module.sql for more about creating an SQL file.
if (!$msg->containsErrors() && file_exists(dirname(__FILE__) . '/module.sql')) { // deal with the SQL file: require(AT_INCLUDE_PATH . 'classes/sqlutility.class.php'); $sqlUtility = new SqlUtility(); $sqlUtility->queryFromFile(dirname(__FILE__) . '/module.sql', TABLE_PREFIX); }
It is up to the module to generate and check for any errors that occur during the installation. An error message can be generated using $msg->addError(array('MODULE_INSTALL', '<li>your error msg goes here</li>'));. Note that the text supplied to the error message is not translated in this case. If the language should be localised, then the appropriate language vairable should replace the text. Something like $msg->addError(array('MODULE_INSTALL', 'AT_ERROR_GOES_HERE')); . The corresponding SQL INSERT statement should then be found in the module.sql file, so the language gets added to the language_text table in ATutor.
To check if any errors have been generated, use $msg->containsErrors() which evaluates to TRUE if a previous error has been generated.
See the Error and Feedback section of the Developer Documentation for more details about displaying messages.
See the Installation: Specifying Privileges section on creating privileges during the installation process.
Authentication uses constants for the privilege levels. The privileges should be declared in the module.php file using the $this->getPrivilege() and $this->getAdminPrivilege() methods, respectively.
define('AT_PRIV_FORUMS', $this->getPrivilege() ); define('AT_ADMIN_PRIV_FORUMS', $this->getAdminPrivilege() );
Once declared, a page can then authenticate against those privileges using either the authentication() or the admin_authenticate() functions.
define('AT_INCLUDE_PATH', '../include/'); require(AT_INCLUDE_PATH.'vitals.inc.php'); // authenticate the administrator forums section: admin_authenticate(AT_ADMIN_PRIV_FORUMS);
Although a module can be created with all hard-coded language, we recommended you use ATutor's localisation functions. All of ATutor's language is stored in the database, which is then retrieved using the _AT() function for simple terms and the $msg object for feedback and error messages.
Additional details on localising ATutor can be found on the Thing You Should Know Before Translating and in the ATutor Developer Documentation.
Module-specific language should be inserted into the language_text table during the installation process. The fields in the table are as follows:
Each language item should have a corresponding SQL INSERT line in the module.sql file, that gets inserted into the ATutor language_text table during installation. Do not include a prefix (e.g. "AT_") on the language_text table name. The installer will detect the right prefix, and automatically prepend it to tables names. Also see the section module.sql for information about creating a file to install the module's language.
# Insert module specific language: INSERT INTO `language_text` VALUES ('en', '_module', 'example_maker', 'Example Maker', NOW(), 'the module title');
Module configuration options can be stored in the ATutor config table, and retrieved using a $_config[] array variable. All module configuration key/value pairs are automatically loaded from the table into global memory while ATutor is running, and can be accessed from any of the module scripts (or from anywhere is ATutor for that matter). The value for a particular configuration option is retrieved by entering its key into the array. To retrieve a URL for the Example Maker module for example, you might use $_config['example_maker'].
In the following example of an index_admin.php file, the form below accepts a URL to an external application used by the Example Maker module (though it could be any value). In this case imagine a third party application has been installed, and the URL to the application is being stored as a configuration option for the example_maker module. When the form is submitted, $_POST['uri'] is inserted into the config table as the value for the example_maker key. The following is an example module administrator script used to add/edit a config option for the example_maker module.
<?php // make sure user is allowed to see this page (admins only) admin_authenticate(AT_ADMIN_PRIV_EXAMPLE_MAKER); if (isset($_POST['submit'])) { // trim whitespace from the value submitted $_POST['uri'] = trim($_POST['uri']); // display an error message if the value is empty if (!$_POST['uri']){ $msg->addError('EXAMPLE_MAKER_ADD_EMPTY'); } // if no errors, insert the key "example_maker" and value "$_POST['uri']" into the config table if (!$msg->containsErrors()) { $_POST['uri'] = $addslashes($_POST['uri']); $sql = "REPLACE INTO ".TABLE_PREFIX."config VALUES ('example_maker', '$_POST[uri]')"; mysql_query($sql, $db); $msg->addFeedback('EXAMPLE_MAKER_URL_SAVED'); header('Location: '.$_SERVER['PHP_SELF']); exit; } } require (AT_INCLUDE_PATH.'header.inc.php'); /******* * First check to see if there is a value for the example_maker key $_config['example_maker'] * If there isn't a value then a missing value message is displayed * The form below that has a single field for submitting a value, in this case a URL * If the value exists in the config table, then display it in the text field using $_config['example_maker'] */ ?> <?php if ($_config['example_maker']): ?> <div class="input-form"> <div class="row"> <p><?php echo _AT('example_maker_text'); ?></p> </div> </div> <?php else: ?> <div class="input-form"> <div class="row"> <p><?php echo _AT('example_maker_missing_url'); ?></p> </div> </div> <?php endif; ?> <form action="<?php $_SERVER['PHP_SELF']; ?>" method="post"> <div class="input-form"> <div class="row"> <p><label for="uri"><?php echo _AT('example_maker_url'); ?></label></p> <input type="text" name="uri" value="<?php echo $_config['example_maker']; ?>" id="uri" size="60" style="min-width: 65%;" /> </div> <div class="row buttons"> <input type="submit" name="submit" value="<?php echo _AT('save'); ?>" /> </div> </div> </form>
A custom style sheet can be linked into pages by setting $_custom_css to be the absolute path to the style sheet. This variable must be set on every page that requires that style sheet.
define('AT_INCLUDE_PATH', '../../include/'); require(AT_INCLUDE_PATH.'vitals.inc.php'); // using a custom style sheet: $_custom_css = $_base_path . 'mods/example_maker/module.css';
Side menu boxes generally appear in a column at the side of a course (though this layout can be altered by a theme). A module may implement one or more side menu boxes.
Side menus are specified using the $_module_stacks array in module.php. $_module_stacks have the attributes title_var (or title) and file. The title_var value is the language key used for that box; the title will be generated by executing _AT($title_var). If title is set instead, a hard-coded title will be used. The file attribute specifies the absolute path to the side menu's include file.
The key to the $_module_stacks should be the name of the module.
$_module_stacks['example_maker'] = array('title_var' => 'example_maker', 'file' => dirname(__FILE__).'\side_menu.inc.php');
Creating a side menu box involves using the $savant template object and assigning the output of the box to the dropdown_contents variable.
<?php global $savant; $box_content = 'This is my side menu box'; $savant->assign('dropdown_contents', $box_content); $savant->assign('title', _AT('example_maker')); $savant->display('include/box.tmpl.php'); ?>
Student tools are pages linked from the home page or the main navigation of courses. A module can only implement one student tool. An instructor controls which student tools are available to a course using the Student Tools section found under Manage.
The tool main page must be specified using the $_student_tool variable in the module.php file. The value of that variable must be the relative path to the file from the ATutor base directory (not the module directory). Example: $_student_tool = 'mods/example_maker/index.php';.
For the tool to correctly appear its Navigation & Hierarchy must be defined correctly. If the tool is to have an instructor management section then the parent must be specified as being tools/index.php and the module must have a non-zero privilege level.
Every page in ATutor must have an entry in the global $_pages array where the key to the array is the relative path to the file from ATutor's base directory. Module pages are specified using the $_module_pages array, which are then merged into the $_pages array when the module.php file is loaded. The array supports the following attributes:
For pages to appear in the instructor Manage section, their parent field must be set to tools/index.php.
$path = 'mods/example_maker/'; // the student tool: $_module_pages[$path.'index.php']['title_var'] = 'example_maker'; $_module_pages[$path.'index.php']['img'] = $path.'icon.gif'; $_module_pages[$path.'index.php']['children'] = array($path.'sub.php', $path.'more.php'); $_module_pages[$path.'sub.php']['title_var'] = 'sub_page'; $_module_pages[$path.'sub.php']['parent'] = $path.'index.php'; $_module_pages[$path.'more.php']['title_var'] = 'more_page'; $_module_pages[$path.'more.php']['parent'] = $path.'index.php'; // the instructor page: $_module_pages[$path . 'inst_index.php']['title_var'] = 'example_maker'; $_module_pages[$path . 'inst_index.php']['parent'] = 'tools/index.php';
When a course is being deleted, or when a back-up is being restored by overriding (i.e. deleting) existing content, a module has to ensure that the content for that course is also deleted. If the module maintains course data directories, then those directories have to either be emptied or deleted. If the module uses database tables for course content, then it has to delete the appropriate entries for that course.
The function used to delete the course content for that module must be stored in the module_delete.php file and named module_name_delete(). The delete function takes a single argument which is the ID of the course to delete.
<?php function example_maker_delete($course) { global $db; // delete directory $path = AT_CONTENT_DIR . 'example_maker/' . $course . '/'; clr_dir($path); // delete from database $sql = "DELETE FROM ".TABLE_PREFIX."example_content WHERE course_id=$course"; mysql_query($sql, $db); } ?>
It is possible for a module to include its content when a course backup is being created or restored. Backups support database tables with foreign-key constraints as well as course specific directories.
A module can backup as many directories as it requires, all specified using the $dirs array variable.
The example below uses the special ? token as the place holder for the course ID. When the course is backed-up, the question mark will be replaced with the correct course ID. The key to the array is the unique name of the directory to be used inside the backup archive file. The same information to create the backup is also used to restore it, so no additional details are required.
$dirs = array(); $dirs['example_maker/'] = AT_CONTENT_DIR . 'example_maker/?/';
There are two parts to backing-up and restoring a module's database tables. First, the SQL queries must be specified using the $sql array variable and then the restore functions must convert the rows so that they can be inserted into the database tables.
The example below uses the special ? token as the place holder for the course ID. When the course is backed-up the question-mark will be replaced with the correct course ID. The key to the array is the unique name of the CSV file to save in the backup archive, without the extension. The SQL query itself must only select the fields that will be backed up. If there are foreign key constraints to preserve then the key will have to be retrieved as well so that it can be used when restoring the tables.
$sql = array(); $sql['example'] = 'SELECT title FROM '.TABLE_PREFIX.'example WHERE course_id=?';
For each key in the $sql array there must be a function with the same name, but suffixed with _convert. The tbl_name_convert() function must return the newly transformed row with respect to the version of ATutor that was used to generate the CSV file. The function accepts the following arguments:
function example_convert($row, $course_id, $table_id_map, $version) { $new_row = array(); $new_row[0] = 0; // auto-increment field $new_row[1] = $course_id; $new_row[2] = $row[1]; // the title if (version_compare($version, '1.5.2', '<')) { // this field did not exist prior to 1.5.2 $new_row[3] = ''; } else { $new_row[3] = $row[2]; } return $new_row; }
The module_cron.php file can be used to run module related functions at specified intervals. The following example doesn't do much of anything..., but you get the idea. If cron has been enabled in ATutor, all the module_cron.php files from each module are run at the interval specified when the Cron is setup. See the Cron Setup sections in the Administrator Handbook, and in ATutor in the Administrator's System Preferences.
<?php /******* * this function named [module_name]_cron is run by the global cron script at the module's specified * interval. */ function hello_world_cron() { global $db; debug('yay i am running!'); } ?>