Development How-Tos
Tabs
Development How-Tos
Welcome to our ILIAS Development How-Tos! This collection provides in-depth guidance on various aspects of ILIAS development. Covering essential topics such as the development process, as well as guidance on how to customize system styles, write unit tests or handle permissions, this page aims to equip you with practical knowledge and best practices including ready-to-use code snippets.
General Development
The description of the development process of ILIAS has become part of our new devguide tutorial, see here.
Want to Contribute? Great!
Table of Contents
Who is a contributor?
In general we consider everyone who takes part in the development of ILIAS a contributor, where the contribution could take various forms, e.g. testing, creating feature request, writing documentation, reporting security issues. We aim to include everyone performing these or similar activities in our processes.
For practical reasons we need to define a contributor to be everyone who wants to contribute commits to our repository for now. We trying to figure out ways to also include Testers, Translators, Authors and other people into the processes described here. If you want to contribute to said activities please have a look here.
As a contributor you will be named in the release notes of our major releases with your name and your organisation as we find them in our commit history and in your profile on GitHub. If your don't want to be listed, please write a short mail to the Technical Board of the ILIAS society.
How to contribute?
Pull Request to the Repositories
Pull requests (PRs) without assignee will be assigned by the Technical Board
(TB) to the
appropriate community member. The TB will also help to resolve problems with PRs and
associated processes, if you require mediation, please write a comment mentioning
via the Technical Board (@ILIAS-eLearning/technical-board
) in the discussion
of the PR.
Please make yourself acquainted with the ILIAS Society's process for functional feature requests before starting to create your PR. Your PR should thus only contain bug fixes or non-functional changes to our code base.
Rules for Contributors
We are happy that you want to contribute. To enable us to merge your PRs in our code please make sure:
- that your PR has a description that tells what is changed and why - with a size relative to the changes
- that your PRs is minimal - prefer to make two small PRs instead of one big PR
- that you discuss huge PRs with the responsible authorities in advance - this will save your time if the authorities do not agree with your proposed change
- that you create commits of self-contained logical units with concise commit messages and no unnecessary whitespace - this will help reviewers to understand what you did
- that your code is understandable and is documented - this will help reviewers as well
- that your commit follows the ILIAS coding guidelines - this is a bare minimum when it comes to style that we require for new code
- you don't introduce new code violations which could have been easily found by importing and running our PhpStorm PHP Inspection Profile
- that your are approachable for questions of reviewers
If your PR contains a bugfix please reference the number of the mantis ticket
in the title 12345 - To many spaces
, link the ticket in the description and
label the ticket with bugfix
. You may make one PR per affected branch.
Please label non-bugfix PRs as improvement
.
Please be prudent with PRs that are work in progress or are request for comments. Only open these if you strictly want some feedback from the greater developer community for a concrete proposal of some code or some guideline. Do not use PRs to the ILIAS-repository as a general working space for incomplete features or ideas. Prefer other measures like workshops or VCs for discussion about ideas or approaches. If you are positive that you definitely need to open a PR as a draft, prefix the summary with "WIP -"" for these kind of PRs to prevent them from being merged accidentally.
List of Labels
Currently, the following labels are used for Pull-Requests. These labels will be assigned by the Technical Board or Authorities:
Label | Description |
---|---|
authorities | The label authorities has to be assigned to PRs that contain updates to the authorities of a component. |
bugfix | PRs with the label bugfix propose a solution for a reported bug in the official Bugtracker https://mantis.ilias.de |
dependencies | The label dependecies is used for PRs which propose new or updated dependencies. Please don't forget to also add the label jour fixe , when proposing new dependencies. |
documentation | The label documentation has to be assigned to PRs adding or updating documentation. |
improvement | The label improvement is used for PRs which propose a general improvement of code or documentation which is not related to a bug. |
javascript | The label javascript has to be set for PRs changing Javascript code. |
jour fixe | PRs which should be discussed during the next Jour Fixe are labeled with this jour fixe . Please set this label at least 2 days before the envisaged date of Jour Fixe. |
kitchen sink | All contributions to the Kitchen Sink Project are labeled accordingly. |
Looking for Shepherd | The label Looking for Shepherd has to be set for PRs which changes made for unmaintained components. |
php | The label php has to be set for PRs changing PHP code. |
roadmap | The label roadmap is assigned to PRs that contain strategical or tactical discussions of technical topics regarding the future of a component. |
technical board | This label is given for PRs which will be discussed in a meeting of the Technical Board. The label will be removed after the discussion. |
Looking for Shepherd
Looking for Shepherd
is a label in GitHub to mark PRs made for unmaintained components. As there is no developer that gets assigned such bugs due to her/his authority, pull requests with this tag can be reviewed by every ILIAS developer who can commit and decide if it is accepted and merged. We kindly ask every ILIAS developer to look into these PRs regularly and take responsibility for our shared code base.
Rules for Community Members assigned to PRs
As an FOSS community, we should be glad that people want to contribute code to our project as this reflects usage of our project. To show this when handling PRs, please make sure
- that you react to every PR assigned to you within 21 days - at least with a thank you and a target date if your schedule is tight
- that you give at least a brief statement why you close a PR if you reject one
- that you merge the changes in the PR in other branches if required
Want to Contribute Something else than Commits?
We are happy to get contributions that are no commits as well. There are many other things you could contribute to ILIAS:
- Ideas for new Features: The development of ILIAS is driven by requirements from the community. Contribute your ideas via feature requests.
- Bug Reports: We do our best, but ILIAS might contain bugs we do not know about yet. Check out how the ILIAS Community handles bug reports.
- Information about Security Issues: Check out how the ILIAS community handles security issues. The Reporter of security issues will also be named in the release notes.
- Time for Testing or Testcases: We always need people who contribute testcases and carry them them before new releases. Please have a look here (German only). If you have questions, do not hesitate to contact our test case manager Fabian Kruse (fabian@ilias.de).
Developer Mode
Sometimes features that are not implemented completely need to be committed to CVS because several programmers work on the same source code or new features or objects should be tested on dependencies to other parts of ILIAS. For these cases we have created the possibility to set parts of the system or whole object types into developer mode.
Object types or functions in developer mode are not shown in ILIAS by default. So users are not affected with features that are not working properly yet.
Activating Objects
To display functions in developer mode please add the following line in your client.ini
in the [system]
section:
DEVMODE = "1"
Now you will see everything as usual.
If you delete the line or set the value to 0 you will disable developer mode again.
How to Use DEVMODE for Programming
To hide entire object types by setting them to DEVMODE just add a new attribute to the desired object in objects.xml
, e.g.:
<object name="grp" class_name="Group" checkbox="1" inherit="1" translate="0" devmode="1">
In this case group objects will now not longer appear in the system.
To hide particular code parts or functions use the new constant DEVMODE:
if (DEVMODE)
{
// do something only if devmode is activated
}
Active Records
How to use Active Record in ILIAS
Active Records
General
The ActiveRecord-Implementation in ILIAS should help developers to get rid of multiple developments of CRUD functionality in their model classes. A lot of redundant code is to be found in ILIAS due to the implementations of read- and write processes to the persistent layer. ILIAS-ActiveRecord provides a lot of useful helpers such as a QueryBuilder, dynamic CRUD, ObjectCaching und data source maintenance.
Differences against other implementations
ActiveRecord are well known in other frameworks and languages such as Ruby, .NET, CakePHP … Most ActiveRecords directly represent the persistent layer, mostly database-tables. Changes in the database are automatically represented by the model. Modifications on the class-members are only possible by modifying the database-field. This is the the only big difference between ILIAS-ActiveRecord and other ActiveRecord-Implementations. ILIAS-ActiveRecord describes the whole class-member in PHP-Code with the information for the persistent layer (such as data-type, length, …). Advantages of this implementation:
- The class-member is represented in your PHP-class and not just dynamically loaded, you ‘see’ your members and let IDEs like PHPStorm automatically implement your setters and getters.
- You ‘see’ directly the field-attributes of your member in the persistent layer. Information about your members can be accessed by field-classes.
Implement your ActiveRecord-Class
Structure of your model
An ActiveRecord-Class normally extends from the abstract ActiveRecord. Let us use the following example: “We need a Message-Model. A Message has a title, a body, a sender and a receiver. Additionally the Message can be of the priority ‘low’, ‘normal’ or ‘high’ and can have a status like ‘new’ and ‘read’.” Our Model could look like this:
<?php
require_once('./Customizing/global/plugins/Libraries/ActiveRecord/class.ActiveRecord.php');
require_once(dirname(__FILE__) . '/../../Connector/class.arConnectorSession.php');
/**
* Class arMessage
*
* @author Fabian Schmid <fs@studer-raimann.ch>
* @version 1.0.0
*/
class arMessage extends ActiveRecord {
const TYPE_NEW = 1;
const TYPE_READ = 2;
const PRIO_LOW = 1;
const PRIO_NORMAL = 5;
const PRIO_HIGH = 9;
const TABLE_NAME = 'ar_message';
/**
* @return string
*/
static function returnDbTableName() {
return self::TABLE_NAME;
}
/**
* @var int
*
* @con_is_primary true
* @con_sequence true
* @con_has_field true
* @con_fieldtype integer
* @con_length 8
*/
protected $id;
/**
* @var string
*
* @con_has_field true
* @con_fieldtype text
* @con_length 256
*/
protected $title = '';
/**
* @var string
*
* @con_has_field true
* @con_fieldtype clob
* @con_length 4000
*/
protected $body = '';
/**
* @var int
*
* @con_has_field true
* @con_fieldtype integer
* @con_length 1
*/
protected $sender_id = 0;
/**
* @var int
*
* @con_has_field true
* @con_fieldtype integer
* @con_is_notnull true
* @con_length 1
*/
protected $receiver_id = 0;
/**
* @var int
*
* @con_has_field true
* @con_fieldtype integer
* @con_length 1
* @con_is_notnull true
*/
protected $priority = self::PRIO_NORMAL;
/**
* @var int
*
* @con_has_field true
* @con_fieldtype integer
* @con_length 1
* @con_is_notnull true
*/
protected $type = self::TYPE_NEW;
/**
* @param mixed $body
*/
public function setBody($body) {
$this->body = $body;
}
/**
* @return mixed
*/
public function getBody() {
return $this->body;
}
/**
* @param int $priority
*/
public function setPriority($priority) {
$this->priority = $priority;
}
/**
* @return int
*/
public function getPriority() {
return $this->priority;
}
/**
* @param int $receiver_id
*/
public function setReceiverId($receiver_id) {
$this->receiver_id = $receiver_id;
}
/**
* @return int
*/
public function getReceiverId() {
return $this->receiver_id;
}
/**
* @param int $sender_id
*/
public function setSenderId($sender_id) {
$this->sender_id = $sender_id;
}
/**
* @return int
*/
public function getSenderId() {
return $this->sender_id;
}
/**
* @param string $title
*/
public function setTitle($title) {
$this->title = $title;
}
/**
* @return string
*/
public function getTitle() {
return $this->title;
}
/**
* @param int $type
*/
public function setType($type) {
$this->type = $type;
}
/**
* @return int
*/
public function getType() {
return $this->type;
}
}
?>
The class implements the public static Method ‘returnDbTableName’, which returns the identifier of the container in the persistent layer. The rest of the Class are Members, Setters and Getters. This Class is fully functional, no other methods have to be implemented to have full CRUD, Caching, Collections, Factory, … All Class-Members, which should be represented in the persistent layer, are additionally documented with PHPDoc.
Using CRUD
After implementing your modelclass you can use the ActiveRecord CRUD commands to build and modify objects of the class:
$arMessage = new arMessage();
$arMessage->setTitle('Hello World');
$arMessage->setBody('Development using ActiveRecord saves a lot of time');
$arMessage->create();
// OR
$arMessage = new arMessage(3);
echo $arMessage->getBody();
// OR
$arMessage = new arMessage(6);
$arMessage->setType(arMessage::TYPE_READ);
$arMessage->update();
// OR
$arMessage = arMessage::find(58); // find() Uses the ObjectCache
$arMessage->delete();
Fields and FieldList
An ActiveRecord-Class-Member is described with the following attributes in PHPDoc. This information is used to provide a proper persistent layer access.
Attribute-Name | Description | Possible Values |
---|---|---|
con_hasfield | Defines whether the field is represented in the persistent layer or not (false doesn’t has to be written) | true/false |
con_is_primary | Member is primary key. Only one primary for one class possible. | true/false |
con_sequence | The (primary-)field has an auto-increment. This is needed in most of the cases | true/false |
con_is_notnull | Is member not_null (as in MySQL) | true/false |
con_fieldtype | All ilDB-Field-Types are currently supported | text, integer, float, date, time, timestamp, clob |
con_length | Length of the field in the persistent layer | determines from the fieldtype. See 'Databse Access and Database Schema' for further information. |
All this information is parsed from the PHPDoc once per ActiveRecord-Class and request and are cached for all other instances of this type. So there should not be a remarkable performance-drop. This is an Example for a primary key $id:
/**
* @var int
*
* @con_is_primary true
* @con_has_field true
* @con_sequence true
* @con_fieldtype integer
* @con_length 8
*/
protected $id;
All the meta information can be access in the ActiveRecord:
public function dummy() {
echo $this->arFieldList->getPrimaryField();
echo $this->arFieldList->getFieldByName('title')->getFieldType();
echo $this->getPrimaryFieldValue();
}
ActiveRecordList
Basics
The ActiveRecordList-Class represents the Collection, Repository, ... The List is accessible through the ActiveRecord or in an own instance:
/**
* @return arMessage[]
* @description a way to get all objects is to call get() directly on your class
*/
public static function getAllObjects() {
$array_of_arMessages = arMessage::get();
// OR
$arMessageList = new arMessageList();
$array_of_arMessages = $arMessageList->get();
return $array_of_arMessages;
}
Both examples return an Array of arMessage-Objects. But The List provide more functionality, such as a QueryBuilder:
public function getSome() {
$array_of_arMessages = arMessage::where(array('type' => arMessage::TYPE_READ))->orderBy('title')->get();
// OR
$arMessageList = new arMessageList();
$arMessageList->where(array('type'=> arMessage::TYPE_READ));
$arMessageList->orderBy('title');
$array_of_arMessages = $arMessageList->get();
}
Build a query
Where
Method-Call | Query |
---|---|
arMessage::where(array('type' => arMessage::TYPE_READ)); |
SELECT * FROM ar_message WHERE ar_message.type = 1 |
arMessage::where(array('type'=>arMessage::TYPE_NEW), '!='); |
SELECT * FROM ar_message WHERE ar_message.type != 1 |
arMessage::where(array( 'type' => arMessage::TYPE_NEW, 'title' => '%test%' ), '='); |
SELECT * FROM ar_message WHERE ar_message.type = 1 AND ar_message.title = '%test%' |
arMessage::where(array( 'type' => arMessage::TYPE_NEW, 'title' => '%test%' ), array( 'type' => '=', 'title' => 'LIKE' )); |
SELECT * FROM ar_message WHERE ar_message.type = 1 AND ar_message.title LIKE '%test%' |
arMessage::where(array( 'type' => arMessage::TYPE_NEW ))->where(array( 'title' => '%test%' ), 'LIKE') |
SELECT * FROM ar_message WHERE ar_message.type = 1 AND ar_message.title LIKE '%test%' |
Oder By
Method-Call | Query |
---|---|
arMessage::orderBy('title'); |
SELECT * FROM ar_message ORDER BY title ASC |
arMessage::orderBy('title', 'DESC'); |
SELECT * FROM ar_message ORDER BY title DESC |
arMessage::orderBy('title', 'DESC')->orderBy('type'); |
SELECT * FROM ar_message ORDER BY title DESC, type ASC' |
Limit
Method-Call | Query |
---|---|
arMessage::limit(0, 100); |
SELECT * FROM ar_message LIMIT 0, 100 |
Join
Method-Call | Query |
---|---|
arMessage::innerjoin('usr_data', 'receiver_id', 'usr_id'); |
SELECT ar_message.*, usr_data.* FROM ar_message INNER JOIN usr_data ON ar_message.receiver_id = usr_data.usr_id |
arMessage::leftjoin('usr_data', 'receiver_id', 'usr_id', array('email')); |
SELECT ar_message.*, usr_data.email FROM ar_message LEFT JOIN usr_data ON ar_message.receiver_id = usr_data.usr_id |
Combining statements
Method-Call | Query |
---|---|
arMessage::innerjoin('usr_data', 'receiver_id', 'usr_id'); |
SELECT ar_message.*, usr_data.* FROM ar_message INNER JOIN usr_data ON ar_message.receiver_id = usr_data.usr_id |
arMessage::leftjoin('usr_data', 'receiver_id', 'usr_id', array('email')); |
SELECT ar_message.*, usr_data.email FROM ar_message LEFT JOIN usr_data ON ar_message.receiver_id = usr_data.usr_id |
Get information
get(); arMessage::orderBy('title')->get(); will return an array of arMessage-Object.
getArray(); arMessage::orderBy('title')->getArray(); will return a 2D record-value array. getArray() can be filtered or the index oft he array can be set: arMessage::getArray(NULL, array('title')); will return an array with only the titles of the records.
getCollection(); If you build a query using the statements explaines above, you can store this Collection for further use. arMessage:: getCollection(); return the ActiveRecordList-Object with all statements saves.
first(); Returns the first object from your query.
last(); Returns the last object from your query.
Use ActiveRecord for Sorting and Filters in Tables
When using an Activerecord for presentation in ilTableGUI, this is an Example to use external sorting and external segmentation, which will increase performance on larger tables:
protected function parseData() {
$this->determineOffsetAndOrder();
$this->determineLimit();
$arMessageList = arMessage::orderBy($this->getOrderField(), $this->getOrderDirection());
foreach ($this->filter as $field => $value) {
if ($value) {
$arMessageList->where(array( $field => $value ));
}
}
$this->setMaxCount($arMessageList->count());
$arMessageList->limit($this->getOffset(), $this->getLimit());
$arMessageList->orderBy('title'); // Secord order field
$arMessageList->dateFormat('d.m.Y - H:i:s'); // All date-fields come in three ways: formatted, unix, unformatted (as in db)
$this->setData($arMessageList->getArray());
}
Connector
ILIAS ActiveRecord uses the ilDB connection as default persistent layer. A connector is responsible for all connections to the persistent layer. It’s possible to write your own Connector, an example is delivered with the ActiveRecord. It uses the User-Session to store the objects. This connector is not fully functional (there is no Querybuilder). Use the abstract arConnector Class to implement your own connector.
Maintenance of data source
ActiveRecord allows to maintain your persistent layer like the ILIAS Database for your class. There is no need to install the database on your own, ActiveRecord can install und update the table: Please do not use installDB; and updateDB; for core-development. Using them will be reported as a bug.
Generate DB-Update-Step to install your Class
Use the already known DB-Update-Steps to generate your AR-Databases. There is a Helper-Script to auto-generate a Installation-Updatestep. Implement these two lines with your ActiveRecord somewhere in ILIAS-Code and run the site. It generates and Downloads a php-file with the installation-Step:
$arBuilder = new arBuilder(new arMessage());
$arBuilder->generateDBUpdateForInstallation();
The downloaded file can be used to install the appropriate db-table.
You can use these methods to delete or truncate your table even in dbupdate-Scripts:
arMessage::resetDB(); // Truncates the Database
$ilDB->dropTable(arMessage::TABLE_NAME, false); // Deletes the Database
It's not yet possible to generate e database-modification step with this feature. Please write those as usual and don't forget to represent your changes in your AR-based Class.
Generate Class-File from existing MySQL-Table (Beta)
It’s possible to generate a PHP-Classfile for an existing MySQL-Datatable, e.g. with the Table usr_data:
$arConverter = new arConverter('usr_data', 'arUser');
$arConverter->downloadClassFile();
Object-Cache
Every ActiveRecord is being cached, developers don’t have to mind this task. The cache is updated on every object modification and is deleted after deleting the object. The object cache storage can be accessed if necessary:
// e.g.
$arMessageFour = new arMessage(4);
arObjectCache::purge($arMessageFour);
if (! arObjectCache::isCached('arMessage', 4)) {
arObjectCache::store(new arMessage(4));
return arObjectCache::get('arMessage', 4);
}
Exceptions
Using Exceptions
If code identifies any problems that prevent the normal processing from being executed, e.g. a directory is not writable even if it should be, exceptions should be thrown. This usually happens within the application layer. Upper contexts that have called the application classes (e.g. GUI or SOAP) can act appropriate. E.g. the user interface layour can present the error using ilUtil::sendFailure
.
Defining new Exceptions
- New exceptions should be implemented as classes derived from
ilException
(found in component ILIAS/Exceptions). - These class file should be put into a subdirectory
exceptions
within the component directory, e.g.components/ILIAS/AdvancedEditing/exceptions
. - If a component uses exceptions a top exception named after the component should be used. Other exceptions classes should be derived from this class, e.g.
ILIAS/AdvancedEditing/exceptions/class.ilAdvancedEditingException.php
.
<?php
/* Copyright (c) 1998-2011 ILIAS open source, Extended GPL, see docs/LICENSE */
require_once 'components/ILIAS/Exceptions/classes/class.ilException.php';
/**
* Class for advanced editing exception handling in ILIAS.
*
* @author Michael Jansen <mjansen@databay.de>
* @version $Id$
*
*/
class ilAdvancedEditingException extends ilException
{
/**
* Constructor
*
* A message is not optional as in build in class Exception
*
* @param string $a_message message
*/
public function __construct($a_message)
{
parent::__construct($a_message);
}
}
?>
Throwing and Catching Exceptions
Throwing an exception:
function update()
{
if (!$this->getId())
{
throw new ilObjectException('No id given');
}
}
Catching an exception (in this case in the GUI layer):
try
{
$this->object->setTitle($title);
$this->object->update();
ilUtil::sendSuccess($this->lng->txt('saved_settings'));
return true;
}
catch (ilObjectException $e)
{
ilUtil::sendFailure($e->getMessage());
return false;
}
Exception Wrapping
Advantages of exception wrapping are:
- Exception wrapping avoids the breaking of layer abstractions
- Exception wrapping gives layers the possibility to add context informations to the exception
Exception Wrapping I (application class):
...
try
{
$parser = new ilSaxParser($this->xml);
$parser->parse();
...
{
catch (ilSaxParserException $e)
{
throw new ilObjectException(?Cannot parse object XML: ?.e->getMessage());
}
...
Exception Wrapping II (here: GUI class):
...
try
{
$this->object->import();
lUtil::sendSuccess($this->$lng('obj_created'));
}
catch(ilObjectException $e)
System Styles
System Styles
The templates folder of ILIAS contains the ILIAS System Styles. System Styles are defined by the set of icons, fonts, html templates and CSS/SCSS files that define the visual appearance of ILIAS. They differ from Content Styles, which enable to manipulate the classes defining the appearance of user generated content.
Custom Styles
System Styles may be customized by creating custom System Styles. Custom styles have
to be placed in the ./public/Customizing/skin
directory to be active. One may have
multiple substyles which may be active for different branches of the repository.
A GitHub Repo for a custom System Style based on the default Delos will be available soon.
Tools
You may directly use the Sass version shipped with ILIAS.
If you want to use your own version, first install the necessary tools to your server. These tools include nodejs and the node packet manager. After that you can install the sass compiler that is used to turn SCSS into CSS using:
npm install -g sass
or
Download Dart SASS from Github and add it to the machine's PATH.
Access available Styles through Frontend
- Navigate to "Administration -> Layout and Styles" of you ILIAS Installation.
- In a table you see all available System Styles.
- You may assigne users to styles via Actions Dropdown
- You may set Sub Styles for certain sections of the repository via Actions Dropdown
How-To
Step 1: Create skin directory
To create a new skin, first add a new subdirectory to directory
./public/Customizing/skin
, e.g. ./public/Customizing/skin
.
In the future, we will provide a base System Style based on the default Delos that you can download from Github and place here.
Step 2: Create template.xml File
One file that must exist in every skin is the file template.xml. E.g.
./public/Customizing/skin/myskin/template.xml
:
<?xml version = "1.0" encoding = "UTF-8"?>
<template xmlns = "http://www.w3.org" version = "1" name = "MySkin">
<style name = "MyStyle" id = "mystyle" image_directory = "images"/>
</template>
Every skin can contain multiple styles. This example defines one style called MyStyle. This skin/style combination will be listed as MySkin/MyStyle in the ILIAS Style and Layout administration. The ILIAS administration is the place where you can activate/deactivate styles, and where you can assign users from one skin to another.
Step 3: Create main CSS File
The id
attribute of the style tag defines the name of the corresponding style
sheet (CSS) file. This CSS file must also be added to the skin directory (here:
./public/Customizing/skin/myskin/mystyle/mystyle.css
). You should start with a copy of
the default CSS file located at templates/default/delos.css
. The best way to
see which styles are used on a given ILIAS screen is to open the HTML source of
the screen. Probably import delos in the top line of your css like so: @import url("../../../../assets/css/delos.css");
If your CSS file contains references to (background) images, these images must be present at their defined locations. If you copied the default CSS file, the image paths will not be correct anymore. You can either copy them to your skin directory, change the CSS definitions or provide your own image files.
Step 3: Better Alternative
To have a working directory for your skin, you can also copy the complete folder
templates/default of your ilias installation to a new folder below
./public/Customizing/skin/skin
within that directory, edit the file template.xml
to
have an unique Style Name and id. This is needed to identify the new skin in
ILIAS' administration. Then copy the standard delos.css
file to "your-id.css".
Take care: the main CSS-File must reflect the id in its name (see above).
However, best use the stand alone skin delos
git repo, which is always an up-to-date copy of the delos skin from the main repo.
Clone it into your ./public/Customizing/skin
folder, make your changes and keep it always
up-to-date for fixes from the main repo.
Step 3: Sass
Do not froget to re-compile the scss-file after each change. Switch to the delos scss folder and execute:
./node_modules/sass/sass.js delos.scss mystyle.css
or
./node_modules/sass/sass.js --style=compressed delos.scss mystyle.css
for a minified CSS version.
Step 4: Add Icons (Optional)
If you want to replace the default icons coming with ILIAS, you can add new
representations of them to your skin. They must be stored in a subdirectory
named like the image_directory
attribute of the style tag in the
template.xml
file.
E.g. if you want to replace the default icon for categories
public/assets/images/standard/icon_cat.sfg
, and your template file defines
image_directory = "images"
as in the example above, the new version must be
stored as ./public/Customizing/skin/myskin/mystyle/images/icon_cat.svg
.
Step 5: Change Layout (Optional)
The layout is specified in HTML template files. Some standard default template
files can be found in directory templates/default
. Other template files are
stored within subdirectories of the Modules or Services directories. Most ILIAS
screens use more than one template file. Some template files are reused in many
ILIAS screens (e.g. the template file that defines the layout of the main menu).
To replace a template file for your skin, you have to create a new one in your skin directory. Please note, that your skin should only contain template files that are modified. You do not need to copy all default template files to your new skin.
Since ILIAS 5.3 we move aim to move most of the UI towards the UI Components. They are located in src/UI. To overwrite those you need to add the respective tpl files in your skins folder.
Examples:
* components
related template files must be stored in a similar
subdirectory structure (omit the templates
subdirectory). E.g. to replace the
template file components/ILIAS/XYZ/templates/tpl.xyz.html
create a new
version at ./public/Customizing/skin/myskin/components/ILIAS/XYZ/tpl.xyz.html
. A template of a UI Component located in
src/UI/templates/default/XYZ/tpl.xyz.html
can be customized by creating a
./public/Customizing/skin/myskin/UI/XYZ/tpl.xyz.html
file.
The following list contains some standard template files, that are often changed in skins:
- Standard Layout: components/ILIAS/UI/templates/default/Layout/tpl.standardpage.html, the frame of the DOM for the complete ILIAS page. Also checkout the according scss variable under section Layout (UI Layout Page).
- Meta Bar: components/ILIAS/UI/templates/default/MainControls/tpl.metabar.html, the Bar on the top holding Notification, Search User Avatar, etc. Also checkout the according metabar scss variables.
- Main Bar: components/ILIAS/UI/templates/default/MainControls/tpl.mainbar.html, the Bar on the left holding triggers for opening the slates for accessing Repository, Dasbhoard etc. Content. Also checkout the according mainbar scss variables.
- Slate: components/ILIAS/UI/templates/default/MainControls/Slate/tpl.slate.html, the Slates triggered by opening items of the Main Bar. Also checkout the according slate scss variables.
Breadcrumbs: components/ILIAS/UI/templates/default/Breadcrumbs/tpl.breadcrumbs.html, Breadcrumbs working as locator on the top of the page. Also checkout the breadcrumb scss variables.
Startup Screens (Login, Registration, ...):
components/ILIAS/Init/templates/default/tpl .startup_screen.html
Step 6: Change the ILIAS Icon
The main ILIAS icon is stored in the images Directory as logo/HeaderIcon.svg
. You
can replace this easyly by your own Icon in svg format. As long as your Icon is
close to a square, this may be all that is needed. Probably you want to change
the file favicon .ico
in ILIAS' root directory too. For non-square Icons you
may refer to:
Installation and Maintenance » Change the ILIAS icon
Migration
There might be changes you need to consider if updating to a new ILIAS version.
Note that this changelog was introduced for ILIAS 5.3. If migrating to a lower version you might find helpful information by consulting:
Installation and Maintenance » Prepare for a new skin
ILIAS 10
- Important: The location of the skin was moved to
./public/Customizing/skin
- System style Management through GUI has been abandoned, see: https://docu.ilias.de/go/wiki/wpage_1_1357
- Sass is no shipped with NPM, see: https://github.com/ILIAS-eLearning/ILIAS/pull/8115
ILIAS 9
A proposal for better structuring the System Styles has been provided and accepted by the JF in 2021, see: https://github.com/ILIAS-eLearning/ILIAS/blob/trunk/src/UI/docu/sass-guidelines.md
With ILIAS 9 the SCSS as been restructered according to the ITCSS structure suggested by this proposal, and the depencency to less from Bootstrap has mostly been removed. However, the change from less to SCSS and the abandonment from Bootstrap means, that System Styles from 8 and lower are NOT compatible with ILIAS 9. They can not be imported, be used, or compiled.
However, note, that most of the css should still work. Also less and scss are not that far appart. Best read through our SCSS Coding Guidelines to get started.
ILIAS 7
The icon-font-path
for glyphs has been renamend to il-icon-font-path
and the location has changed due to a move from the bootstrap
library to the new location for external libraries. The new location is:
"../../../../node_modules/bootstrap/fonts/"
. If a 5.2 style is
imported, the variable icon-font-path
must be adapted accordingly.
In March 2022, we moved the general Test & Assessment CSS (ta.css and
ta_split.css) to less/Modules/Test/delos.less
(like other CSS for modules)
to start refactoring this module's style code. As part of this change,
the override mechanism that fetches a custom style for just the T&A has been
removed. Please use the standard skin setup described in this document to style
the Test & Assessment like the rest of your custom skin.
ILIAS 6
Major parts of the UI of ILIAS 6 have changed. It is therefore recommended, to create a new skin for ILIAS think an manually move changes that are still needed from oder versions to the new skin.
Also, most importantly the following components have been introduced:
- Standard Layout, template directory: src/UI/templates/default/Layout, the frame of the DOM for the complete ILIAS page. Also checkout the according scss variable under section Layout (UI Layout Page).
- Meta Bar template directory: src/UI/templates/default/MainControls, the Bar on the top holding Notification, Search User Avatar, etc. Also checkout the according metabar scss variables.
- Main Bar template directory: src/UI/templates/default/MainControls, the Bar on the left holding triggers for opening the slates for accessing Repository, Dasbhoard etc. Content. Also checkout the according mainbar scss variables.
- Slate template directory: src/UI/templates/default/MainControls/Slate, the Slates triggered by opening items of the Main Bar. Also checkout the according slate scss variables.
- Breadcrumbs template directory: src/UI/templates/default/Breadcrumbs, Breadcrumbs working as locator on the top of the page. Also checkout the breadcrumb scss variables.
See above section on information on how to customize those components.
ILIAS 5.3
The icon-font-path
for glyphs has changed due to a move from the bootstrap
library to the new location for external libraries. The new location is:
"../../../../libs/bower/bower_components/bootstrap/fonts/"
. If a 5.2 style is
imported, the variable icon-font-path
must be adapted accordingly.
Coding Guidelines
If you want to change and contribute ILIAS style code, please refer to the most recent SCSS Coding Guidelines
Template Engine
Template Engine
As a component developer you should rarely need to use the Template Engine directly since you should use the components of the UI framework whenever possible.
But if you e.g. need to implement UI components yourself you need to understand the basics of the template engine.
The main idea of using templates is the separation of style (css), layout (views) and PHP code (controllers). All layout information is stored in .html template files. These files contain HTML markup and placeholders that are dynamically replaced by controller code.
Placeholders and Blocks
[...]
<!-- BEGIN address -->
<div class="ilProfileSection">
<h3 class="ilProfileSectionHead"></h3>
<!-- BEGIN address_line -->
<div></div>
<!-- END address_line -->
</div>
<!-- END address -->
[...]
This example contains placeholders and
and the block definition for
address_line
and address
.
$tpl = new ilTemplate("tpl.address.html", true, true, components/ILIAS/User");
$tpl->setCurrentBlock("address_line");
foreach($address as $line)
{
if(trim($line))
{
$tpl->setVariable("TXT_ADDRESS_LINE", trim($line));
$tpl->parseCurrentBlock();
}
}
$tpl->setCurrentBlock("address");
$tpl->setVariable("TXT_ADDRESS", $lng->txt("address"));
$tpl->parseCurrentBlock();
$html = $tpl->get()
To fill a template you need an instance of ilTemplate
. Start iterating inner blocks (setCurrentBlock()
, parseCurrentBlock()
) and fill their placeholders using setVariable()
. Work subsequently through all blocks from inner to outer blocks.
To render the content of your template you need to call $tpl->get()
.
Now you want to include your HTML in the main ILIAS layout.
Main Template
The UI framework supports you with a global main template which supports methods to include output on an ILIAS screen.
$tpl = new ilTemplate("tpl.address.html", true, true, components/ILIAS/User");
[...]
// get main template
$main_tpl = $DIC->ui()->mainTemplate();
// set content (center column)
$main_tpl->setContent($tpl->get());
// set title
$main_tpl->setTitle($title);
// set title icon
$main_tpl->setTitleIcon(ilUtil::getImagePath("standard/icon_cat.gif"));
// set description section
$main_tpl->setDescription($description);
// set content of right column
$main_tpl->->setRightContent($right_content_html);
Unit Tests
Writing Unit Tests
ILIAS supports unit testing with the PHPUnit testing framework. We highly recommend their excellent documentation that explains the basic ideas of unit testing, how PHPUnit works and how it is installed.
Configuration
After installing PHPUnit on your machine you need to configure ILIAS to work with it. The test cases are performed with an authenticated user in your ILIAS installation. The configuration determines which user is used to perform the test cases. You will find a configuration file template at:
Services/PHPUnit/config/cfg.phpunit.template.php
Make a copy at:
Services/PHPUnit/config/cfg.phpunit.php
Now change the file and provide a valid client ID, account ID, and username.
Please activate the developer mode in your
client.ini.php
when running PHPUnit tests.
Test Cases
Test classes should be located in a subdirectory test of your components directory.
[components/ILIAS]/[ComponentName]/test
E.g. the test classes for the Administration service are located at ILIAS/Administration/test
. The names for test class files should usually be derived from the application class they are written for.
[ApplicationClassName]Test.php
E.g. if you write test cases for a class ilSetting the test class file should be named ilSettingTest.php
.
The class should contain a method starting with "test" for each test case.
<?php
/*
+-----------------------------------------------------------------------------+
| ILIAS open source |
+-----------------------------------------------------------------------------+
| Copyright (c) 1998-2006 ILIAS open source, University of Cologne |
[...]
+-----------------------------------------------------------------------------+
*/
class ilSettingTest extends PHPUnit_Framework_TestCase
{
protected $backupGlobals = FALSE;
protected function setUp()
{
include_once("./Services/PHPUnit/classes/class.ilUnitUtil.php");
ilUnitUtil::performInitialisation();
}
public function testSetGetSettings()
{
$set = new ilSetting("test_module");
$set->set("foo", "bar");
$value = $set->get("foo");
$this->assertEquals("bar", $value);
}
[...]
}
?>
- Your test class must be derived from class PHPUnit_Framework_TestCase.
- Your test must be executable with PHPUnit 5.7.
- If your test needs an ILIAS Installation, it must be annotated with @group needsInstalledILIAS, and...
- you must set $backupGlobals to false, otherwise, your test cases will not work.
- the setUp() method should contain the ilUnitUtil::performInitialisation(); call. ilUnitUtil can be found in Services/PHPUnit/classes.
- If your test does not need an ILIAS Installation, it must not be annotated with @group needsInstalledILIAS and also should not use ilUnitUtil::performInitialisation().
To run your test class you simply call phpunit in your ILIAS root directory with the local path of your test class (omit the .php suffix):
> phpunit components/ILIAS/Administration/test/ilSettingTest
- Please write your test cases in a way that the requirements to run them are minimal. The test cases should run on a usual system. They should not require that certain conditions are given on the test system, e.g. an empty repository.
- Clean up the data that is written during the test case. If possible, a test run should not leave any data created during the test in the system.
Test Suites
Test suites allow to perform aggregated tests. They should be provided on the component level, one test suite for each service and module. The file name must follow the format:
il[components/ILIAS][ComponentName]Suite.php
E.g. the test suite class for the Administration service is located at:
ILIAS/Administration/test/ilServicesAdministrationSuite.php
The class is named:
ilServicesAdministrationSuite
<?php
/*
+-----------------------------------------------------------------------------+
| ILIAS open source |
+-----------------------------------------------------------------------------+
| Copyright (c) 1998-2009 ILIAS open source, University of Cologne |
[...]
+-----------------------------------------------------------------------------+
*/
class ilServicesAdministrationSuite extends PHPUnit_Framework_TestSuite
{
public static function suite()
{
$suite = new ilServicesAdministrationSuite();
// add each test class of the component
include_once("./components/ILIAS/Administration/test/ilSettingTest.php");
$suite->addTestSuite("ilSettingTest");
[...]
return $suite;
}
}
?>
The example outlines the basic structure of a test suite class. To run the test suite, simply pass the class name to phpunit in the ILIAS main directory:
> phpunit components/ILIAS/Administration/test/ilServicesAdministrationSuite
The Global Test Suite
The global test suite scans all Services and Modules directory automatically for component level test suites and aggregates them to one big test suite. The suite is located in the PHPUnit Service. You can run the suite by typing:
> phpunit Services/PHPUnit/test/ilGlobalSuite
The global suite will first list all component suites that are included in execution. If your suite is not listed, you should check whether your suite file and class is following the naming conventions as written in the previous section.
UI-Framework
The ILIAS UI-Framework
The ILIAS UI-Framework helps you to implement GUIs consistent with the guidelines of the Kitchen Sink.
Tutorial
You find a tutorial on what UI Components are and on their purpose, how they are used and what you should consider while using them here.
Correctness by Construction and Testability
The design of the ILIAS UI-Framework makes it possible to identify lots of guideline violations during the construction of a GUI and turn them into errors or exceptions in PHP. This gives you the freedom to care about your GUI instead of the guidelines it should conform to. You also can check your final GUI for Kitchen Sink compliance using the procedures the framework provides for Unit Testing.
Condsider the overarching UX
When building views with the UI framework or adding new components to it, please be mindful of the larger context. Every UI and their elements should follow an overarching strategy to intuitively guide the user as much as possible to the functions and information they are currently interested in. Coherent and comprehensive UI concepts make ILIAS easier to use and avoid overcrowded and confusing screens.
Here are some points to consider when creating a layered, context-sensitive UX strategy: * Anticipate the user intent to group, highlight, show, and hide information and actions depending on context. * Make use of the experience a user brings from other apps to match a mental model they have already learned. * Consider giving operations their own specialized interface, step, or mode rather than building a one-size-fits-all screen but utilize common UX-concepts and already existing UI components when you do.
Documents with recommendations on how to approach UX challenges of specific UI components and use cases will be added below: * Best practices for properties and actions displayed on repository objects
Implementing Elements in the Framework
To get a brief overview on how to proceed before starting to implement elements in the UI framework, we recommend to read our DevGuide about this topic.
How to Implement a Component?
If you would like to implement a new component for the framework, you should perform the following tasks:
- Add your new component into the respective factory interface. E.g. if you introduce a component of a completely new type, you MUST add the description to the main factory (components/ILIAS/UI/src/Factory.php). If you add a new type of button, you MUST add the description to the existing factory for buttons, located at src/UI/Component/Button/Factory.
The description MUST use the following template:
/** * --- * description: * purpose: What is to be done by this control * composition: What is this control composed of * effect: What happens if the control is operated * rivals: * Rival 1: What other controls are similar, what is their distinction * * background: Relevant academic information * context: * - The context states: where this control is used specifically with examples (this list might not be complete) and how common is this control used * * rules: * usage: * 1: Where and when an element is to be used or not. * composition: * 1: How this component is to be assembled. * interaction: * 1: How the interaction with this object takes place. * wording: * 1: How the wording of labels or captions must be. * style: * 1: How this element should look like. * ordering: * 1: How different elements of this instance are to be ordered. * responsiveness: * 1: How this element behaves on changing screen sizes. * accessibility: * 1: How this element is made accessible. * * --- * @param string $content * @return \ILIAS\UI\Component\Demo\Demo **/ public function demo($content);
This freshly added function in the factory leads to an error as soon as ILIAS is opened, since the implementation of the factory (located at src/UI/Implementation/Factory.php) does not implement that function yet. For the moment, implement it as follows:
/** * @inheritdoc */ public function demo($content) { throw new \ILIAS\UI\NotImplementedException(); }
Next, you should think about the interface you would like to propose for this component. You need to model the component you want to introduce by defining its interface and the factory method that constructs the component. To make your component easy to use, it should be creatable with a minimum of parameters and use sensible defaults for the most of its properties. Also think about the use cases for your component. Make typical use cases easy to implement and more special use cases harder to implement. Put getters for all properties on your interface. Make sure you understand, that all UI components should be immutable, i.e. instead of defining setters
setXYZ
you must define mutatorswithXYZ
that return copies of your component with changed properties. Try to use as little mutators as possible and try to make it easy to maintain the invariants defined in your rules when mutators will be used. Take care to keep it as minimal as possible. Add a description for each function. For the demo component, this interface could look as follows (located at (src/UI/Component/Demo/Demo.php):<?php declare(strict_types=1) namespace ILIAS\UI\Component\Demo; /** * Interface Demo * @package ILIAS\UI\Component\Demo */ interface Demo extends \ILIAS\UI\Component\Component { /** * Gets the content of this demo component * @return Demo */ public function getContent(); }
Make sure all tests are passing by executing '''phpunit tests/UI'''. For the demo component this means we have to add the following line to NoUIFactory in UI/test/Base.php:
public function demo($demo){}
Congratulations, at this point you are ready to present your work to the JF. Create a PR named "UI NameOfTheComponent". To make it easy for non-developers to follow the discussion, you MUST link to the changed/added factory classes and mock in the description you provide for your PR. Further, it would be wise to enhance your work with a little mockup. This makes it much easier to discuss the new component at the JF. So best create such an example and also link it in your comment, e.g. at src/UI/examples/Demo/mockup.php:
<?php declare(strict_types=1) function mockup() { return "<h1>Hello Demo!</h1>"; }
If needed, you can also add JS-logic (e.g. src/UI/examples/Demo/mockup.php):
<?php declare(strict_types=1) function script() { return "<script>console.log('Hello Demo');</script>Open your JS console!"; }
However best might be to just provide a screenshot showing what the component will look like:
function mockup() { global $DIC; $f = $DIC->ui()->factory(); $renderer = $DIC->ui()->renderer(); $mockup = $f->image()->responsive("src/UI/examples/Demo/mockup.png"); return $renderer->render($mockup); }
Next you should create the necessary tests for the new component. Since this is a very important step, it deserved its own chapter below. Make sure you write at at least tests for all interface methods and one full rendering test.
Currently you will only get the NotImplementedException you threw previously. That needs to be changed. First, add an implementation for the new interface (add it at src/UI/Implementation/Component/Demo/Demo.php):
<?php declare(strict_types=1) namespace ILIAS\UI\Implementation\Component\Demo; use ILIAS\UI\Component\Demo as D; use ILIAS\UI\Implementation\Component\ComponentHelper; class Demo implements D\Demo { use ComponentHelper; /** * @var string */ protected $content; /** * @param $content */ public function __construct($content){ $this->checkStringArg("title", $content); $this->content = $content; } /** * @inheritdoc */ public function getContent(){ return $this->content; } }
Next, make the factory return the new component (change demo() of src/UI/Implementation/Factory.php):
return new Component\Demo\Demo($content);
Then, implement the renderer at src/UI/Implementation/Component/Demo/Demo.php:
<?php declare(strict_types=1) /* Copyright (c) 2016 Timon Amstutz <timon.amstutz@ilub.unibe.ch> Extended GPL, see docs/LICENSE */ namespace ILIAS\UI\Implementation\Component\Demo; use ILIAS\UI\Implementation\Render\AbstractComponentRenderer; use ILIAS\UI\Renderer as RendererInterface; use ILIAS\UI\Component; class Renderer extends AbstractComponentRenderer { /** * @inheritdocs */ public function render(Component\Component $component, RendererInterface $default_renderer): string { // if this is not our component, call cannotHandleComponent($component) // to throw a unified exception. if (!$component instanceof Component\Demo\Demo) { $this->cannotHandleComponent($component); } $tpl = $this->getTemplate("tpl.demo.html", true, true); $tpl->setVariable("CONTENT",$component->getContent()); return $tpl->get(); } }
- Finally you need the template used to render your component. Create it at src/UI/templates/default/Demo/tpl.demo.html:
html <h1 class="il-demo"></h1>
- Execute the UI tests again. At this point, everything should pass. Thanks, you just made ILIAS more powerful!
Remember to add examples demonstrating the usage of your new component. Those examples should showcase the key features of the new component. Note that they will be used as basis for the test cases in testrail (see next point). The example for the demo looks as follows (located at src/UI/examples/Demo/render.php):
<?php declare(strict_types=1) function render() { //Init Factory and Renderer global $DIC; $f = $DIC->ui()->factory(); $renderer = $DIC->ui()->renderer(); $demo = $f->demo("Demo rendered by template!"); return $renderer->render($demo); }
Remember to adapt/add the Test Cases in Testrail section UI Components so that a tester with no technical expertise can confirm that all examples work as intended. They must be available and linked to the PR before the PR will be merged.
Optional: You might need to add some scss, to make your new component look nice. However, only do that if this is really required. (see: SCSS Guidelines ).
- Formulate at least one test-case for your new component on testrail.ilias.de](https://testrail.ilias.de). Best, try to formulate a testcase for each relevant client-side interaction. E.g. if your component contains a button that triggers a modal on-click, write a test-case for this interaction. Post the the link to this test-case in a comment/the description of your PR.
- Optional: If your component introduces a new factory, do not forget to wire it up in the according
location of the initialisation. Have a look into
ilInitialisation::initUIFramework
incomponents/ILIAS/Init/class/class.ilInitialisation.php
.
How to write unit tests for a Component?
When creating a new component, please make sure you provide at least tests for all interface methods and one full
rendering test. For the demo component we have created above, this looks as follows (located at
UI/tests/Component/Demo/DemoTest.php). Please make sure your unit test extends from the ILIAS_UI_TestBase
, so you can
use functionalities like getting the test renderer for rendering tests.
<?php declare(strict_types=1)
require_once(__DIR__."/../../../../vendor/composer/vendor/autoload.php");
require_once(__DIR__."/../../Base.php");
use \ILIAS\UI\Component as C;
/**
* Test on demo implementation.
*/
class DemoTest extends ILIAS_UI_TestBase {
public function testImplementsFactoryInterface() {
$f = new \ILIAS\UI\Implementation\Factory();
$this->assertInstanceOf("ILIAS\\UI\\Factory", $f);
$demo = $f->demo("Demo Implementation!");
$this->assertInstanceOf( "ILIAS\\UI\\Component\\Demo\\Demo", $demo);
}
public function testGetContent() {
$f = new \ILIAS\UI\Implementation\Factory();
$demo = $f->demo("Demo Implementation!");
$this->assertEquals("Demo Implementation!", $demo->getContent());
}
public function testRenderContent() {
$r = $this->getDefaultRenderer();
$f = new \ILIAS\UI\Implementation\Factory();
$demo = $f->demo("Demo Implementation!");
$html = $r->render($demo);
$expected_html = '<h1 class="il-demo">Demo Implementation!</h1>';
$this->assertHTMLEquals($expected_html, $html);
}
}
If you are implementing or adjusting the unit tests for a more complex component, you need to be careful when writing
rendering tests. If your component features further components from the framework, either by composable aspects like
e.g. providing a roundtrip modals action buttons (ILIAS\UI\Component\Modal\Roundtrip::withActionButtons()
), or
by rendering further components during the rendering process, it is implicitly coupled to the HTML of other components.
If thats the case, you MUST implement your rendering tests using "component stubs", to fully decouple the unit tests from other components. This means, instead of rendering actual components in your unit test, you provide mocked instances of the component interfaces used within your component or unit test, so we have full control over the HTML being rendered for each component. An implementation for the first scenario (externally provided components) would look like this for our demo component:
// ...
class DemoTest extends ILIAS_UI_TestBase
{
// ...
/**
* Tests if the action button which is provided is catually rendered.
*/
public function testWithActionButtonRendering(): void
{
$f = new \ILIAS\UI\Implementation\Factory();
$demo = $f->demo('');
// create the component mock for a standard button.
$button_stub = $this->createMock(\ILIAS\UI\Component\Button\Standard::class);
// configure the mock to return our desired HTML, it is advised to make this
// value unique, so we can check only the existense using a str_contains()
// approach.
$button_html = sha1(\ILIAS\UI\Component\Button\Standard::class);
$button_stub->method('getCanonicalName')->willReturn($button_html);
// make the mock known to the renderer, so it can be rendered.
$renderer = $this->getDefaultRenderer(null, [$button_stub]);
// provide the mock to the method we are testing.
$demo = $demo->withActionButton($button_stub);
$actual_html = $renderer->render($demo);
// checks only the existence of the component, using str_contains() to
// search our unique stub HTML.
$this->assertTrue(str_contains($actual_html, $stub_html));
$expected_html = <<<EOT
<div class="c-demo">
<h1 class="il-demo"></h1>
$button_html
</div>
EOT;
// checks the exact position of the component stub, using the unique HTML
// value embeded in the expected HTML.
$this->assertEquals(
$this->brutallyTrimHTML($expected_html),
$this->brutallyTrimHTML($actual_html)
);
}
}
Note it is also important to think about what you are actually testing with your rendering test. If testing only for
the existence of something is important, you can use the str_contains()
approach to reduce maintenance in the future.
However, it might be necessary to also test the location of a ceratin component, e.g. if an action button should be
inside a certain container, so the styling works properly. In this case, you may use the assertEquals()
approach with
the embedded stub-HTML.
For the latter scenario (internally rendered components), you need to make the component stubs available in the UI factory which is injected into the actual renderer. This way, we still have full control over the HTML of other components which may be rendered and checked in your rendering tests. For our demo component this would work like this:
// ...
class DemoTest extends ILIAS_UI_TestBase
{
// ...
protected \ILIAS\UI\Component\Button\Factory $button_factory;
protected \ILIAS\UI\Component\Button\Standard $button_stub;
protected string $button_html;
/** Sets up the button stub which will be rendered internally */
public function setUp() : void
{
// setup the button stub similar to the previous example.
$this->button_stub = $this->createMock(\ILIAS\UI\Component\Button\Standard::class);
$this->button_html = sha1(\ILIAS\UI\Component\Button\Standard::class);
$this->button_stub->method('getCanonicalName')->willReturn($this->button_html);
// setup the factory so it will return the button stub.
$this->button_factory = $this->createMock(\ILIAS\UI\Component\Button\Factory::class);
$this->button_factory->method('standard')->willReturn($this->button_stub);
// don't forget to call our parent!
parent::setUp();
}
/** Overrides the factory retrieval so it uses our instance of the button factory. */
public function getUIFactory(): NoUIFactory
{
return new class ($this->button_factory) extends NoUIFactory {
public function __construct(
protected \ILIAS\UI\Component\Button\Factory $button_factory,
) {
}
public function button(): \ILIAS\UI\Component\Button\Factory
{
return $this->button_factory;
}
};
}
/** Tests if the action button is rendered properly during the internal rendering process. */
public function testInternalActionButtonRendering(): void
{
$f = new \ILIAS\UI\Implementation\Factory();
$demo = $f->demo('');
// render the component making the stub available.
$renderer = $this->getDefaultRenderer(null, [$this->button_stub]);
$actual_html = $renderer->render($demo);
// check existence ...
$this->assertTrue(str_contains($actual_html, $this->stub_html));
$expected_html = <<<EOT
<div class="c-demo">
<h1 class="il-demo"></h1>
$this->button_html
</div>
EOT;
// check location ...
$this->assertEquals(
$this->brutallyTrimHTML($expected_html),
$this->brutallyTrimHTML($actual_html)
);
}
}
How can I make my component look different in some context?
For some use cases you might get to the point where you want to know where your component is rendered to emit different HTML in your renderer. A general idea of the UI framework is that components have their unique look that is recognisable throughout the system, which is the exact reason you could not find a simple way to get to know where your component is being rendered.
There still might be circumstances where a context dependent rendering is indeed
required. A context can be understood as a collection or stack of all other surrounding
UI components. This means, if e.g. a Page component is rendered which needs to render
a Dropdown component somewhere that features some further Shy button compoennt, the
rendering stack or context when the button is rendered would be "Page -> Dropdown -> Shy".
The DefaultRenderer orchestrates this process
and is responsible to remember this context at any time during the entire rendering
process. Component renderers are able to react to this context using a RendererFactory
,
which receives the current context as an argument when loading the renderer of some
component. The FSLoader contains directions
on how to introduce new renderers for different contexts in your component.
Before using this mechanism, please consider if you really require a different look in a different context and, and if thats the case, whether you could achieve the same effect using CSS or not.
How to Change an Existing Component?
- Create a new branch based on the current trunk.
- Implement your changes and create a PR on the current trunk.
- Clearly state in the description of the PR, why you believe the change to be necessary. If your change fixes a bug, link to the according bugfix. If you need the change for implementing a feature, link the Feature Request. If you are changing the interface of a component, your proposal will be discussed in the next JF (see: rules ).
Abstraction of Javascript in the Framework
Note: The concept described in this section is not yet fully finalized.
The functionality of some components relies on Javascript. As an example, a modal is shown and closed by clicking on some triggerer component. As a user of the framework, you should not worry about writing the Javascript logic for this kind of interaction.
About Triggerables, Triggerer and Signals
Before describing the concept in more detail, you should be aware of the following definitions:
* Signal A signal describes a Javascript action of a component which can be triggered by another component of the
framework.
* Triggerable A component offering some signals that can be triggered by other components.
* Triggerer A component triggering a signal of another component.
* Event The Javascript event on which a signal is being triggered, e.g. click
, hover
etc.
Again, consider the example if a user opens a modal by clicking on a button:
- Signal: Show Modal
- Triggerable: Modal
- Triggerer: Button
- Event: Click
How to trigger Signals of Components
This code snippet shows how to open a modal by clicking on a button:
global $DIC;
$factory = $DIC->ui()->factory();
$modal = $factory->modal()->roundtrip('Title', $factory->legacy('Hello World'));
$button = $factory->button()->standard('Open Modal', '#')
->withOnClick($modal->getShowSignal());
The button is a triggerer component. As such, it offers the method withOnClick
which takes any Signal
offered by a triggerable. This is how the framework connects triggerer components with signals of triggerable components.
Similar to the click event, there exist methods withOnHover
and withOnLoad
to abstract the Javascript events on
which a signal is being triggered.
Attention: Immutable Objects and Signals
Each triggerer component stores the signals it triggers. By cloning a component, these signals are cloned as well.
This means that a cloned component may trigger the same signals as the original. Consider the following example:
php
global $DIC;
$factory = $DIC->ui()->factory();
$modal = $factory->modal()->roundtrip('Title', $factory->legacy('Hello World'));
$button1 = $factory->button()->standard('Open Modal', '#')
->withOnClick($modal->getShowSignal());
$button2 = $button1->withLabel('Open the same Modal');
In the example above, $button2
will open the same modal as $button1
. In order to reset any triggered signals, use
the method $button2->withResetTriggeredSignals()
.
Implementing a Triggerer Component
Any component acting as triggerer must implement the Triggerer
interface. This interface is further extended by
interfaces describing the Javascript event on which a signal is being triggered. Currently, there exist the Clickable
,
Hoverable
and Onloadable
interfaces. Please check out the button component for an example implementation.
Implementing a Triggerable Component
Any component acting as triggerable must implement the Triggerable
interface. In addition, it must offer at least
one signal that can be triggered by other components. The renderer of the triggerable component is also responsible
for executing the Javascript logic if any signal is getting triggered. Please check out the modal component for an example
implementation. The next section explains how the concept of signals/triggerer/triggerable is abstracted in Javascript.
Technical Details
The magic how everything is glued together on the Javascript side happens in the renderers of the triggerer and triggerable components: * Triggerer: The renderer of the triggerer component knows which signals are triggered on which events. It registers a new event handler on the component (e.g. on click/hover) which will trigger the signal as a custom Javascript event. * Triggerable: The renderer of the triggerable component knows the signals and the Javascript logic which must be executed if any of the signals is getting triggered.
Each signal has a unique alphanumeric ID. The triggerer uses this ID to trigger a custom Javascript event which will be handled by some event handler from the triggerable. In order to understand this concept, take a look at the Javascript code that is getting generated by the renderers if a button opens a modal on click:
Renderer of button
The renderer of the button generates the HTML for the button AND registers the event handler for the button click.
This event handler triggers a custom Javascript event with the same name as the ID of show signal of the modal:
html
<button id="button1">Open Modal</button>
<script>
$('#button1').on('click', function() {
$(this).trigger('id_of_signal_to_open_the_modal',
{
'id' : 'id_of_signal_to_open_the_modal',
'triggerer' : $(this),
'event' : 'click',
'options' : {}
}
return false;
});
</script>
Note that some event data is passed with the event, such as the triggerer, event and event options. This allows the
event handler of the modal to identify the triggerer.
Renderer of modal
The renderer of the modal generates the HTML for the modal AND registers an event handler on the ID of the show signal.
The event handler calls some Javascript logic to show the modal.
<div class="modal" id="modal1"> ... </div>
<script>
$(document).on('id_of_signal_to_open_the_modal', function(event, signalData) {
il.UI.modal.showModal('modal1', signalData);
});
</script>
Note: signalData
contains the event data passed by the triggerer, e.g. signalData.triggerer
holds the JQuery
object of the button.
For more information on events in Javascript in the context of JQuery: http://api.jquery.com/category/events/
Code Style
We are currently not enforcing code style, but eventually will.
PHP
Use PHPStan to check your files:
./scripts/PHPStan/run_check.sh src/UI/...
There are different levels of checks, you can e.g. run
./vendor/composer/vendor/bin/phpstan analyse --level 8 src/UI/...
to override ./scripts/PHPStan/phpstan.neon
, however, level 9/max is the desired goal.
Java Script
In order to validate your JS-files, run
./node_modules/.bin/eslint --parser-options ecmaVersion:13 src/UI/templates/js/...
or change/add .eslintrc.json
in ILIAS' root directory:
{
"parserOptions": {
"ecmaVersion": 13
},
"extends": "airbnb-base"
}
To install the linter (and its config), run
npm i -D "eslint" "eslint-config-airbnb-base" "eslint-plugin-import"
FAQ
There are so many rules, is that really necessary?
The current state of the art in ILIAS GUI creation was dubbed "The GUI Anarchy" by some smart person. The introduction of the ILIAS UI framework aims at bringing more structure in the GUIs of ILIAS. As one (or two) maintainers for all things GUI of ILIAS is no option for several reasons and the current state (without rules) is anarchy, rules seem to be the only sensible option to get some structure. All existing rules have a purpose, but there might be a more terse way to explain them. If you have found it, we'll be glad to accept your PR.
I don't understand that stuff, is there anyone who can explain it to me?
Yes. Ask Richard Klees richard.klees@concepts-and-training.de, Timon Amstutz timon.amstutz@ilub.unibe.ch or Thibeau Fuhrer thibeau@sr.solutions.
Services
AccessControl
Permission Handling
Reference IDs and Object IDs
All objects within the ILIAS repository are protected by the role-based access control system (RBAC). An object within the repository is identified by the so-called Reference-ID (ref_id). The Reference-ID determines the object and the position of the object within the repository tree. Objects are identified by their Object-ID. Every object has only one Object-ID but may be associated with multiple Reference-IDs if it is referenced in multiple locations within the repository tree.
Related Classes:
ilObject
(classes/class.ilObject.php): Handles objects and their IDsilTree
(classes/class.ilTree.php): Handles the repository tree (and other trees).
Related Tables:
object_data
: Stores basic object dataobject_reference
: Stores Reference-IDs of objectstree
: Stores the repository tree
How to check the access permission of a user
The access checking is provided by the class ilAccessHandler
. An instance of this class is globally available through the DI-Container. The most important method of this class is:
checkAccess($a_permission, $a_cmd, $a_ref_id, $a_type = "", $a_obj_id = "")
global $DIC;
$access = $DIC->access();
if ($access->checkAccess("write", "", $this->object->getRefId())
This method checks whether the current user may perform the action $a_cmd
associated with the permission $a_permission
on the repository object identified by $a_ref_id
. The method checks the following things:
RBAC Check: Check whether the current user has the permission
$a_permission
for the object identified by$a_ref_id
.$a_permission
may be, for example, "read" or "write".Repository Path Check: Checks whether the current user has read access to all parent nodes of the object identified by
$a_ref_id
. For example, if a learning module is located within a course A in category B, ILIAS checks read access of course A and category B.Condition Check: Checks whether the user fulfills all preconditions for the object. Preconditions could be defined by authors, administrators, or tutors of repository objects. They consist of a trigger, a target, and a condition expression. For example, Learning module A (target) can be accessed only if the user has passed (condition) Test B (trigger).
Object Status Check: Checks whether the status of the object allows a command to be performed. For example, if a learning module is set to "offline," no read access-related command may be performed, even if the read permission is granted by RBAC.
The check of step 4 makes use of type-specific access classes. Every object type (learning modules, glossaries, chats, etc.) must provide an access class derived from ilObjectAccess
, named ilObj_Type_Access
, e.g., ilObjGlossaryAccess
, ilObjLearningModuleAccess
, ilObjChatAccess
, etc. Those classes must contain a static method:
_checkAccess($a_cmd, $a_permission, $a_ref_id, $a_obj_id, $a_user_id = "")
This method should check the object status-related conditions and return true if everything is OK or false if not.
Related classes:
ilAccessHandler
(components/ILIAS/AccessControl/classes/class.ilAccessHandler.php)ilObjectAccess
(classes/class.ilObjectAccess)
Calendar
How to handle dates in different time zones
With release 3.10.0 ILIAS introduces support for individual user time zones. On the presentation, side dates should always be displayed in the time zone of the currently logged in user. With the revision of the calendar in 3.10.0, new classes for the manipulation and presentation of dates have been introduced.
PHP Basics
There are three interesting levels of time:
- The Server Time Zone
- The 'Coordinated Universal Time' / UTC (which is more or less GMT)
- The User Time Zone
Working with PHP usually brings you in contact with times in the server time zone and, if you work with unix timestamps, implicitely with UTC.
Example 1: Getting current unix timestamp (which are seconds from 1.1.1970 in UTC)
// will display something like 1216474062
echo time();
Example 2: Getting the current server time zone (you will rarely need this).
// show current timezone, e.g. 'Europe/Berlin'
echo date_default_timezone_get();
Example 3: Using date()
to format a date/time value. The PHP date function uses a (UTC) unix timestamp as input and delivers the date/time in the current server time zone.
// displays something like 2008-07-19 15:27:42 (current server time zone)
echo date('Y-m-d H:i:s', time());
Example 4: The opposite function of date()
is mktime()
, which delivers a (UTC) unix timestamp, based on date/time values provided in the current server time zone.
// get current hour from date (server time zone) and using the hour in mktime
// -> mktime input parameters must be in server time zone
$server_hour = date('H', time());
echo mktime($server_hour);
Up to ILIAS 3.9.x all string representations ("YYYY-MM-DD HH:MM:SS") are in server time zone, also when working with MySQL timestamps. Only integer representated unix timestamps (like delivered by
time()
) are seconds in UTC since 1.1.1970.
With version 3.10.0 ILIAS displays dates in a user defined time zone. Every user can choose the time zone in his or her personal settings.
Dates in the format of the user defined time zone are only used in the user interface. They must not be saved to the database.
Using ilDateTime objects in ILIAS
- Creating date objects:
// Creating a DateTime object (from unix timestamp)
$now = new ilDateTime(time(), IL_CAL_UNIX);
// Creating a DateTime object (from string in server time zone)
$date = new ilDateTime('2008-07-17 10:00:00', IL_CAL_DATETIME);
// Creating a Date object (no time, no time zone conversion)
$birthday = new ilDate('1972-04-04', IL_CAL_DATE);
- Manipulating dates:
$days = new ilDateTime(time(), IL_CAL_UNIX);
// plus one hour
$days->increment(IL_CAL_HOUR, 1);
// minus two days
$days->increment(IL_CAL_DAY, -2);
// plus three months
$days->increment(IL_CAL_MONTH, 3);
...
- Comparing dates:
$early = new ilDateTime('2008-07-17 06:00:00', IL_CAL_DATETIME);
$late = new ilDateTime('2008-07-17 23:00:00', IL_CAL_DATETIME);
// Check equality => returns false
ilDateTime::_equals($early,$late);
// Check same day => returns true
ilDateTime::_equals($early, $late, IL_CAL_DAY);
// Check $early < $late
ilDateTime::_before($early, $late);
// Check $early > $late
ilDateTime::_after($early, $late);
...
- Converting date formats:
// Creating object from date/time string representation in server time zone
$today = new ilDateTime('2008-07-17 12:00:00', IL_CAL_DATETIME);
// Convert to unix timestamp
$unix = $today->get(IL_CAL_UNIX);
// Convert to custom format in server time zone (PHP date syntax)
$date_str = $today->get(IL_CAL_FKT_DATE, 'YmdHis');
// Convert to datetime in UTC time zone (usually you will not need this)
$utc = $today->get(IL_CAL_DATETIME, '', 'UTC');
Storing dates in the database
MySQL offers mainly three possibilities for storing dates in the database: Timestamp, DateTime or Integer Fields. Timestamp and DateTime are used similarly, but Timestamp internally converts to UTC and DateTime does not.
It is recommended to not rely on the database for timezone conversions, and instead convert all datetimes to UTC manually before persisting them. That leaves two options for storing datetimes: as a fromatted string in UTC in a datetime field, or as a unix timestamp in an integer field.
- Using field type 'datetime':
You provide a normal string representation in UTC (YYYY-MM-DD HH:MM:SS). When reading you will also get a string representation in UTC.
// Update/Insert
$registration_start = new ilDateTime(time(), IL_CAL_UNIX);
$db->manipulate(
"UPDATE grp_settings SET" .
" registration_start = " . $db->quote($start->get(IL_CAL_DATETIME), ilDBConstants::T_DATETIME) .
" WHERE obj_id = " . $db->quote(123, ilDBConstants::T_INTEGER)
);
// Select
$set = $db->query("SELECT * FROM grp_settings ");
while ($record = $db->fetchAssoc($set)) {
$registration_start = new ilDateTime($record['registration_start'], IL_CAL_DATETIME);
}
- Using field type 'integer' (for unix timestamps):
The (UTC) unix timestamps are just put into an integer field.
// Update/Insert
$registration_start = new ilDateTime('2008-11-11 11:11:11', IL_CAL_DATETIME);
$db->manipulate(
"UPDATE grp_settings SET" .
" registration_start = " . $db->quote($start->get(IL_CAL_UNIX), ilDBConstants::T_INTEGER) .
" WHERE obj_id = " . $db->quote(123, ilDBConstants::T_INTEGER)
);
// Select
$set = $db->query("SELECT * FROM grp_settings");
while ($record = $db->fetchAssoc($set)) {
$registration_start = new ilDateTime($record['registration_start'], IL_CAL_UNIX);
}
Date Presentation
For the presentation of dates in the user interface, the individually selected user time zone must be respected. This is done by the ilDatePresentation
class.
- Presentation of dates (without time):
ilDatePresentation::formatDate(
new ilDate(time(), IL_CAL_UNIX)
);
// Returns:
// 12. Jul 2008
- Presentation of dates (with time):
ilDatePresentation::formatDate(
new ilDateTime(time(), IL_CAL_UNIX)
);
// Returns:
// 12. Jul 2008 1:01pm or
// 12. Jul 2008 13:01
// depending on the user time zone and hour format setting.
- Presentation of date periods:
ilDatePresentation::formatPeriod(
new ilDateTime(time(), IL_CAL_UNIX),
new ilDateTime(time() + 7200, IL_CAL_UNIX)
);
// Returns:
// 17. Jul 2008 1:00 pm - 3:00 pm or
// 17. Jul 2008 23:00 - 18. Jul 2008 01:00
// Depending on the user time zone and hour format settings.
Date-Inputs in Forms
For date/time form fields, use the Kitchen Sink Date Time
input. Make sure
to manually configure the component such that it respects the user settings
concerning time zone and date-time format.
Cron
Cron
The keywords “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “MAY”, and “OPTIONAL” in this document are to be interpreted as described in RFC 2119.
Table of Contents * Implementing and Configuring a Cron-Job * Providing a Cron-Job * ilCronJob * ilCronJobResult * Schedule * Settings * Customizing * Misc * Cron Job Execution * Permission Context
Implementing and Configuring a Cron-Job
To give more control of if and when cron-jobs are executed to administrators a 2nd implementation of cron-jobs has been added to ILIAS 4.4+. All existing cron-jobs have been migrated and thus moved to their respective modules and services. The top-level directory "cron/" will probably be kept because of cron.php but should otherwise be empty at some point.
Providing a Cron-Job
A module or service has to "announce" its cron-jobs to the system by adding them to their respective module.xml or service.xml.
- The job id has to be unique.
- An optional path can be added if the module/service directory layout differs from the ILIAS standard.
<?xml version = "1.0" encoding = "UTF-8"?>
<service xmlns="http://www.w3.org" version="$Id$"
id="trac">
...
<crons>
<cron id="lp_object_statistics" class="ilLPCronObjectStatistics" />
</crons>
</service>
There are 3 basic concepts: cron-job, schedule and cron-result. Using them as intended should make testing and/or debugging of cron-jobs far easier than before.
ilCronJob
This base class must be extended by every cron-job. Besides the methods mentioned below isDue()
is noteworthy as it is the central point by which the system decides if a cron-job is to be run or not.
The "condition" of the existing CronCheck would have to be implemented here.
Several abstract methods have to be implemented to make a new cron-job usable:
getId()
: returns the Id as defined in the module.xml or service.xmlhasAutoActivation()
: is the cron-job active after "installation" or should it be activated manually?hasFlexibleSchedule()
: can the schedule be edited by an adminstrator or is it static?getDefaultScheduleType()
: see SchedulegetDefaultScheduleValue()
: see Schedulerun()
: process the cron-job
ilCronJobResult
The class ilCronJobResult
determines the status of a current cron job.
The status are:
* STATUS_INVALID_CONFIGURATION
This status will indicate the current configuration
for this cron job is not correct and MUST be adjusted.
* STATUS_NO_ACTION
This status indicates that the cron job did not perform any action.
Possible reasons to set this action:
* A cron job responsible to sent emails didn't sent emails at all.
* A cron job responsible for deleting orphaned objects did not find any object to delete.
* A lucene cron job decided that the index does not require an update.
* STATUS_OK
This status indicates that the cron job has been successfully finished.
* STATUS_CRASHED
A critical failure has occurred during
the execution of the cron job, which led
to an critical error.
The cron job needs to be restarted by
the administrator.
* STATUS_RESET
This status indicates that cron job has been rested.
* STATUS_FAIL
This status indicates that an non-critical
error appeared in the execution of the cron
job.
Every run()
-Method of a cron job MUST return
an instance of ilCronJobResult
and MUST set status before returned by a method.
public function run(): ilCronJobResult
{
$result = new ilCronJobResult();
try {
$procedure->execute();
$result->setStatus(ilCronJobResult::STATUS_OK);
} catch (Exception $exception) {
$result->setStatus(ilCronJobResult::STATUS_FAIL);
$result->setMessage($exception->getMessage());
}
return $result;
}
A message SHOULD be added additionally. If given, this message will be displayed in the cron job overview table.
Schedule
As the cron-tab (for 4.4+) should be configured in a way that it runs every few minutes the schedule is crucial to minimize system load by only running cron-jobs when really needed.
The are several schedule types available. The most basic are daily, weekly, monthly, quarterly and yearly. Furthermore a schedule can be given in minutes, hours and days. If the cron-job is set to a flexible schedule it can be configured by administrators. In any case a default schedule type and value (see above) has to be given to give users a clue when a job is due.
Use the following enum cases to implement your desired schedule:
\ILIAS\Cron\Schedule\CronJobScheduleType::SCHEDULE_TYPE_DAILY;
\ILIAS\Cron\Schedule\CronJobScheduleType::SCHEDULE_TYPE_IN_MINUTES;
\ILIAS\Cron\Schedule\CronJobScheduleType::SCHEDULE_TYPE_IN_HOURS;
\ILIAS\Cron\Schedule\CronJobScheduleType::SCHEDULE_TYPE_IN_DAYS;
\ILIAS\Cron\Schedule\CronJobScheduleType::SCHEDULE_TYPE_WEEKLY;
\ILIAS\Cron\Schedule\CronJobScheduleType::SCHEDULE_TYPE_MONTHLY;
\ILIAS\Cron\Schedule\CronJobScheduleType::SCHEDULE_TYPE_QUARTERLY;
\ILIAS\Cron\Schedule\CronJobScheduleType::SCHEDULE_TYPE_YEARLY;
Settings
A cron-job may define its own custom settings. The following methods can be used:
hasCustomSettings()
addCustomSettingsToForm()
saveCustomSettings()
addExternalSettingsToForm()
- This method can be used to integrate cron-job specific settings into 1 or more administration settings forms.
- The concept of "External Settings" is currently undocumented.
Customizing
getTitle()
getDescription()
activationWasToggled()
- This hook will be called each time the activation status of the cron-job is changed. This way the cron-job can notify other classes about this.
Misc
The isDue()
method decides if a cron-job is run or not. As a default it checks the schedule or runs anyways
if triggered manually by an administrator. The specific cron-job is free to decide by any means necessary
if it is currently active or not. A flexible schedule should of course mind the current settings.
A cron-job is set to crashed if it has been running for 3 hours straight and has not "pinged" once.
The $DIC->cron()->manager()->ping()
method will reset this counter thus enabling the cron-job to run as
long as needed. So if you know that your cron-job might take ages add a ping()
call at some sensible point
in your run()
method.
A cron-job is locked when running. The cron manager will take care of this and prevent concurrent runs. So as mentioned above the cron-tab can safely be set to every few minutes.
Cron Job Execution
In order to execute the cron job manager, the following command MUST be used:
/usr/bin/php [PATH_TO_ILIAS]/cron/cron.php run-jobs <user> <client_id> run-jobs
The <user>
MUST be a valid (but arbitrary) user account of the ILIAS installation.
The <client_id>
MUST be the client id of the ILIAS installation.
Permission Context
Implementations of ilCronJob
MUST NOT rely on specific permissions (e.g. RBAC).
Generally said, there MUST NOT be any expectations regarding given permissions
at all in the context of a cron job. Please keep this in mind when you structure
your code layers.
Export
Export/Import of Components, esp. Repository Resources
Export
Export of a repository resource in this context means: content export without user related data. There are other exports like exports of members data in courses or groups. This how-to focuses on the export of a repository resource as a whole.
ILIAS provides support for a standard way of representing ILIAS export screens and for a standardized modularized export process.
Export User Interface: Class ilExportGUI
The export tab should be placed accoring to the tabs guidelines. The export screen should be implemented by using class ilExportGUI
which is located in components/ILIAS/Export/classes
. The basic lines of code needed are:
// the code is placed in the ilObj...GUI class of the repository resource
...
* @ilCtrl_Calls ilObj...GUI: ilExportGUI
...
// in execute command:
...
$exp_gui = new ilExportGUI($this); // $this is the ilObj...GUI class of the resource
$ret = $this->ctrl->forwardCommand($exp_gui);
...
// in set/get tabs:
if ($ilAccess->checkAccess("write", "", $this->object->getRefId()))
{
$tabs_gui->addTab("export",
$lng->txt("export"),
$this->ctrl->getLinkTargetByClass("ilexportgui", ""));
}
...
The simple example above can be found in ILIAS/Exercise/classes/class.ilObjExerciseGUI.php
. A more complex usage can be found in learning modules, class ILIAS/LearningModule/classes/class.ilObjContentObjectGUI.php
.
Class ilExporter
For the XML export the export user interface class calls a generic export method ilExport->exportObject (ILIAS/Export). To use this generic export, you need to implement an il\<Module>Exporter class that is derived from ilXmlExporter
.
The method exportObject of ilExport triggers a modularized export process that is not limited to repository resources. It can be used by any component of ILIAS for different sets of entities. E.g. the service MetaData implements a class ilMetaDataExporter
and the module MediaPool implements a class ilMediaPoolExporter
.
An il\<Component>Exporter class must provide:
- a
getXmlRepresentation()
method - a
getValidSchemaVersions()
method - a
getXmlExportHeadDependencies()
method, if there are any dependencies to other components, that should be exported before the data of the current component - a
getXmlExportTailDependencies()
method, if there are any dependencies to other components, that should be exported after the data of the current component
Additionally the class may implement an init()
method that is called at the beginning of the procedure.
getXmlRepresentation()
The method getXmlRepresentation() must return the XML for a given entity, target release and id.
- entity: This is a string that represents the entity that should be exported, e.g. the object type like "lm", "file", "frm" or another identifier that is recognised by the component. E.g. the metadata component uses only one entity named "md".
- target_release: A string like "4.1.0" that identifies the target release for the export. This allows to implement export routines for older versions than the current one.
- id: The ID is the ID of the concrete entity. If an entity ID consists of multiple parts, they should be concatenated in one string using the ":" separator between each part.
The following example shows the implementation of the getXmlRepresentation method for meta data:
public function getXmlRepresentation(string $a_entity, string $a_target_release, string $a_id) : string
{
$id = explode(":", $a_id);
$mdxml = new ilMD2XML($id[0], $id[1], $id[2]);
$mdxml->setExportMode();
$mdxml->startExport();
return $mdxml->getXml();
}
getXmlExportHeadDependencies() and getXmlExportTailDependencies()
These method must return the dependencies for a given entity and target release and multiple ids.
An example for a dependency is "every media object has metadata". In this case the media object component is responsible for providing the XML for the media object, but not for the metadata. The latter is defined as a "tail dependency": The metadata should be put into the export package after the media object has been exported.
To decide whether something is a head or a tail dependency (should be imported before or after the current component), it is important to consider how the package will be imported and how relations between the data are resolved.
The import processes the data in the same sequence as the export did. The import process provides an "ID mapping" feature to get new IDs (the ones created when imported) for old ones (the ones in the package).
If data of component (a) needs the new IDs of component (b) when being imported, (b) should be exported before (a) and if (b) depends on (a), (a) should be a tail dependency in the ilExporter
clas of component (b).
Example: The metadata IDs of a media object are derived from the ID of a media object. If a media object has ID 5, the corresponding metadata ID will be "0:5:mob". This means, the media object should be imported before the metadata and the metadata should be a tail dependency in the media object export.
public function getXmlExportTailDependencies(string $a_entity, string $a_target_release, string $a_ids) : string
{
$md_ids = array();
foreach ($a_ids as $mob_id)
{
$md_ids[] = "0:".$mob_id.":mob";
}
return array (
array(
"component" => "components/ILIAS/MetaData",
"entity" => "md",
"ids" => $md_ids)
);
}
getValidSchemaVersions()
The export process relies on schema definitions (xsd files) that define the XML returned by the ilExporter
classes. The getValidSchemaVersions()
method must return all schema versions that the component can currently export to. ILIAS chooses the first one, that has min/max constraints which fit to the target release. Please put the newest schemas on top.
function getValidSchemaVersions(string $a_entity) : array
{
return array (
"4.1.0" => array(
"namespace" => "http://www.ilias.de/components/ILIAS/MediaObjects/mob/4_1",
"xsd_file" => "ilias_mob_4_1.xsd",
"uses_dataset" => true,
"min" => "4.1.0",
"max" => "")
);
}
The xsd files are located in the xml folder of ILIAS. The namespace should always have the format https://www.ilias.de/<Services|Modules>/<Component>/<entity>/<release number in the format x_x>
.
Output of the Export Process
The standard export process creates a file named <timestamp>__<installation id>__<entity>_<id>.zip
. This zip file contains a manifest.xml
file on the top level, that includes basic information and references to export.xml
files that have been created by the components involved during the export process.
<Manifest MainEntity="mep" Title="My Pool" TargetRelease="4.1.0"
InstallationId="4411" InstallationUrl="http://scott.local/ilias">
<ExportFile Component="components/ILIAS/MediaObjects" Path="components/ILIAS/MediaObjects/set_1/export.xml"/>
<ExportFile Component="components/ILIAS/MetaData" Path="components/ILIAS/MetaData/set_1/export.xml"/>
<ExportFile Component="components/ILIAS/MediaPool" Path="components/ILIAS/MediaPool/set_1/export.xml"/>
<ExportFile Component="components/ILIAS/MediaObjects" Path="components/ILIAS/MediaObjects/set_2/export.xml"/>
<ExportFile Component="components/ILIAS/MetaData" Path="components/ILIAS/MetaData/set_2/export.xml"/>
<ExportFile Component="components/ILIAS/File" Path="components/ILIAS/File/set_1/export.xml"/>
<ExportFile Component="components/ILIAS/MetaData" Path="components/ILIAS/MetaData/set_3/export.xml"/>
<ExportFile Component="components/ILIAS/COPage" Path="components/ILIAS/COPage/set_1/export.xml"/>
</Manifest>
The example above shows the manifest file for a media pool which lists all components that are involved in the export of the media pool and their export files.
Import
ILIAS also provides a standardized import process for packages that have been created with the standard export process. The process for importing a repository resource is started with a call of ilImport->importObject
(ILIAS/Export). All involved components must provide a class il<Component>Importer
that handles the import of the corresponding data.
Class ilImporter
This class must be derived from ilXmlImporter
(ILIAS/Export). The class must provide:
- a method
importXmlRepresentation()
that imports the XML of the export.
Additionally the class may implement an init()
method that is called at the beginning of the procedure.
function importXmlRepresentation(string $a_entity, string $a_id, string $a_xml, ilImportMapping $a_mapping) : void
{
$new_id = $a_mapping->getMapping("components/ILIAS/MetaData", "md", $a_id);
if ($new_id != "")
{
$id = explode(":", $new_id);
$xml_copier = new ilMDXMLCopier($a_xml, $id[0], $id[1], $id[2]);
$xml_copier->startParsing();
}
}
The example above imports the XML data that has been created by the corresponding il<Component>Exporter
class, in this case ilMetaDataExporter
. The ID provided is the ID of the export file. In this case the new id depends on the object that depends on the meta data, e.g. a media object. The media object has been imported before in this case and provided mapping information for the metadata via the mapping object that is passed to importXmlRepresentation. The mapping object allows to add (set) old-to-new-ID mappings and to lookup them up (get).
// code in ILIAS/MediaObjects that creates new media objects from
// import xml and adds mapping for metadata
$newObj = new ilObjMediaObject();
...
$newObj->create();
...
$a_mapping->addMapping("components/ILIAS/MetaData", "md",
"0:".$old_id.":mob", "0:".$newObj->getId().":mob");
Validation
The import validation is only enabled if a schema file is available. It is important that the schema file is located in the directory 'xml/SchemaValidation' and that the naming convention is followed.
Schema File Naming Convention
Schema files have to follow the naming convention:
ilias_{type_string}_{version_string}.xsd
'type_string' can either be or _. With 'type' beeing the component id found in the components corresponding module.xml or service.xml. The type of export xml files is set in the functions getXmlExportTailDependencies() and getXmlRepresentation(). 'type' corresponds to the attribute entity in the xml file. For example, the component id/type/entity value of Course is 'crs'.
'version_string' follows the pattern: _. The 'version_string' is defined in getValidSchemaVersions().
To determine the matching schema file for a given xml-export file, the value of 'type_string' is compared with the value of the attribute 'entity' of the 'exp:Export'-node and the value of 'version_string' is compared with the value of the attribute 'SchemaVersion' of the 'exp:Export'-Node.
If the xml-export file contains a dataset, the 'entity' attribute of the 'ds:Rec'-nodes is used instead of the 'entity' attribtue of the 'exp:Export'-node.
If the Version numbers do not match, the schema file with the highest version number is used.
For example take a look at 'ilias_crs_objectives_9_0.xsd'. Here 'type_string' is 'crs_objectives' with type 'crs' and subtype 'objectives'. 'version_string' is '9_0' with 'major_version_number' 9 and a 'minor_version_number' 0.
Updating Schema File Versions
During development xml file specifications may change, wich in consequence requires a new xsd. The first step is to create a new xsd and add it to the 'xml/SchemaValidation'-folder. After that an entry with the correct version string needs to be added to the components ilExporter getValidSchemaVersions()-function. If the import of older xml files should no longer be possible, the old xsd-file needs to be removed from the 'xml/SchemaValdiation'-folder and the components ilExporter getValidSchemaVersions()-function should be adjusted accoirdingly.
Enable Import Validation
Add the schema file to the 'xml/SchemaValidation'-folder.
Disable Import Validation
Remove the schema from in the 'xml/SchemaValidation'-folder.
Validation Code Examples
Validate Xml File:
// Get the xml SplFileInfo
$xml_file_spl = new SplFileInfo('path to my xml file')
// Get the xsd SplFileInfo
$xsd_file_spl = new SplFileInfo('path to my xsd file')
// Initialize a xml/xsd file handler
$import = new \ILIAS\Export\ImportHandler\Factory();
$xml_file_handler = $import->file()->xml()->handler()->withFileInfo($xml_file_spl);
$xsd_file_handler = $import->file()->xsd()->hanlder()->withFileInfo($xsd_file_spl);
/** @var \ILIAS\Export\ImportStatus\ilCollection $validation_results */
// Validate
$validation_results = $import->file()->validation()->handler()->validateXMLFile(
$xml_file_handler,
$xsd_file_handler
);
// Check if an import failure occured
if ($validation_results->hasStatusType(\ILIAS\Export\ImportStatus\StatusType::FAILED)) {
// Do something on failure
}
Validate Xml at Xml Node:
// Get the xml SplFileInfo
$xml_file_spl = new SplFileInfo('path to my xml file')
// Get the xsd SplFileInfo
$xsd_file_spl = new SplFileInfo('path to my xsd file')
// Initialize a xml/xsd file handler
$import = new \ILIAS\Export\ImportHandler\Factory();
$xml_file_handler = $import->file()->xml()->handler()->withFileInfo($xml_file_spl);
$xsd_file_handler = $import->file()->xsd()->handler()->withFileInfo($xsd_file_spl);
// Build xPath to xml node
// $path->toString() = '/RootElement/namespace:TargetElement'
/** @var \ILIAS\Export\ImportHandler\Path\Handler $path */
$path = $import->path()->handler()
->withStartAtRoot(true)
->withNode($import->path()->node()->simple()->withName('RootElement'))
->withNode($import->path()->node()->simple()->withName('namespace:TargetElement'));
// Because the path contains the namespace 'namespace' we have to add the namespace
// info to the xml file handler
$xml_file_handler = $xml_file_handler->withAdditionalNamespace(
$import->file()->namespace()->handler()
->withNamespace('http://www.example.com/Dummy1/Dummy2/namespace/4_2')
->withPrefix('namespace')
)
/** @var \ILIAS\Export\ImportStatus\ilCollection $validation_results */
// Validate
$validation_results = $import->file()->validation()->handler()->validateXMLAtPath(
$xml_file_handler,
$xsd_file_handler,
$path
);
// Check if an import failure occured
if ($validation_results->hasStatusType(\ILIAS\Export\ImportStatus\StatusType::FAILED)) {
// Do something on failure
}
Using Dataset Classes for Import/Export
It is up to the component, how the XML for the export is created and imported. The component can, but does not need to, make use of dataset classes that create XML in a generic way without the need to use the ilXmlWriter class for export or individual sax import parsers classes.
Help
Online Help Support
The online help is currently supported for the German language only.
Within the online help learning modules, help texts are assigned to screens of the ILIAS application. Screens are identified by screen IDs consisting of three parts Component/Screen/SubScreen. To set the screen ID for your component, use the global $DIC->['ilHelp']
object. It provides the following methods:
setScreenIdComponent($a_comp);
setScreenId($a_screen);
setSubScreenId($a_subscreen_id);
These methods should be called in GUI classes of your component. Best practice is to do this only if the corresponding class implements the current screen (handles the current command). A good place is the method that is also responsible for setting the tabs/subtabs.
At minimum the setScreenIdComponent
method must be called. If no screen and subcreen IDs are provided, but tabs and or subtags are displayed on the screen. ILIAS will use the IDs of the tabs/subtabs automatically to extend the overall screen ID.
Language
Logging
Logging Service
Starting with release 5.1 a new logging service based on Monolog is available.
The service provides support for different log levels per component.
Activate Logging for Components
To use different log levels for your component, you have to enable "logging" in your module.xml or service.xml.
<?xml version = "1.0" encoding = "UTF-8"?>
<module xmlns="http://www.w3.org" version="$Id$" id="grp">
...
<!-- add a line "logging" the component definition file -->
<logging />
</service>
Call composer du
to trigger the reading of the XML files.
Different log levels for components can be defined in "ILIAS -> Administration -> Logging -> Components". If no component specific log level is given, the the global log level is used.
Definition of Log Levels
ILIAS (monolog) support the following log levels defines in RFC 5424:
- DEBUG: Detailed debug information
- INFO: Interesting event. E.g user logs in
- NOTICE: Normal but significant events
- WARNING: Exceptional occurences that are no errors. E.g calls of deprecated methods
- ERROR: Runtime errors that do not require immediate action
- CRITICAL: Critical conditions - e.g. a module service is unasable due to missing librarys.
- ALERT: Immediate action is required. E.g. no database connection
- EMERGENCY: The system is unusable.
Using the Logging Service
An instance of the logging service is available via $DIC->logger()
. You should reveice the logger for your component by calling a method with your component id on this object.
// Get component logger
$grp_logger = $DIC->logger->grp();
// write a message with info log level
$grp_logger->info('info message');
Using Placeholders
The ilLogger exposes the placeholder feature given by the monolog bundle, which implements a PSR-3 compliant logger interface.
Placeholders should be used to allow escaping of user input just as $database->quote(...)
is used to escape user input in SQL queries.
Example usage
$logger->debug('Lorem ipsum dolor .', [
'foo' => 'Lorem',
'bar' => 'ipsum',
]);
Further reading
Please read the PSR-3 Specification for further information.
Manual Mail Templates
The concept of 'Manual Mail Templates' is best described as a feature which enables components to provide text templates for a 'User-to-User Email' in a specific context.
Often tutors / admins send the emails with the same purpose and texts to course members, e.g. to ask them if they have problems with the course because they have not used the course yet.
Context Registration
A module or service MAY announce its email template
contexts to the system by adding them to their
respective module.xml or service.xml.
The template context id has to be globally unique.
An optional path can be added if the module/service
directory layout differs from the ILIAS standard,
where most files are located in a ./classes
directory.
<?xml version = "1.0" encoding = "UTF-8"?>
<module xmlns="http://www.w3.org" version="$Id$" id="crs">
...
<mailtemplates>
<context id="crs_context_manual" class="ilCourseMailTemplateContext" />
</mailtemplates>
</module>
If registered once, every email template context
class defined in a module.xml or service.xml
has to extend the base class \ilMailTemplateContext
.
All abstract methods MUST be implemented to make
a template context usable.
- getId(): string
- getTitle(): string
- getDescription(): string
- getSpecificPlaceholders(): array
- resolveSpecificPlaceholder(string $placeholderId, array $contextParameters, \ilObjUser $recipient = null, $htmlMarkup = false): string
A collection of context specific placeholders can be returned by a simple array definition. The key of each element should be a unique placeholder id. Each placeholder contains (beside its id) a placeholder string and a label which is used in the user interfaced.
return [
'crs_title' => [
'placeholder' => 'CRS_TITLE',
'label' => $lng->txt('crs_title')
],
'crs_link' => [
'placeholder' => 'CRS_LINK',
'label' => $lng->txt('crs_mail_permanent_link')
]
];
Supposing the context registration succeeded and you properly derived a context PHP class providing all necessary data and placeholders, you are now able to use your registered context in your component and build hyperlinks to the mail system, transporting your context information.
Context Usage Example
Given you created a context named crs_context_tutor_manual
...
global $DIC;
class ilCourseMailTemplateTutorContext extends \ilMailTemplateContext
{
// [...]
const ID = 'crs_context_tutor_manual';
// [...]
}
... you can provide a hyperlink to the mail system (or
more precisely to \ilMailFormGUI
) as follows:
$DIC->ctrl()->redirectToUrl(
\ilMailFormCall::getRedirectTarget(
$this, // The referring ILIAS controller aka. GUI when redirecting back to the referrer
'participants', // The desired command aka. the method to be called when redirecting back to the referrer
[], // Key/Value array for parameters important for the ilCtrl/GUI context when when redirecting back to the referrer, e.g. a ref_id
[
'type' => 'new', // Could also be 'reply' with an additional 'mail_id' paremter provided here
],
[
\ilMailFormCall::CONTEXT_KEY => \ilCourseMailTemplateTutorContext::ID, // don't forget this!
'ref_id' => $courseRefId,
'ts' => time(),
// further parameters which will be later automatically passed to your context class
]
)
);
Parameters required by your mail context class
MUST be provided as a key/value pair array in the
fifth parameter of \ilMailFormCall::getRedirectTarget
.
These parameters will be passed back to
your context class when the mail system uses
\ilMailTemplateContext::resolveSpecificPlaceholder(...)
as callback when an email is actually sent and included
placeholders should be replaced.
You also MUST add a key \ilMailFormCall::CONTEXT_KEY
with your context id as value to this array.
Skill
Skill Service
This how-to describes how components can and should use the Skill Service. The Skill Service implements what is called in ILIAS the Competence Management. The different terms have historical reasons, as the feature has been called Skill Management in the beginning.
The Competence Tree
Competences are organized in a hierarchical structure. Nodes that contain subnodes are called Competence Category, leaf nodes are Competences. These competences define an ordered list of competence levels.
This simple structure becomes complex with the introduction of competence templates. Competence templates are reusable subtrees that can be referenced within the main competence structure by Competence Template References.
The competence templates can either only consist of one basic competence template (without and subnodes) or of a competence template category including subnodes.
Node | Internal Type | Purpose | Part of the Hierarchy |
---|---|---|---|
Root Node of Competence Tree | skrt | Root of the Competence Hierarchy | |
Competence Node | skll | Defines Competence Levels | Main Part |
Competence Category Node | scat | Can contain competence and competence category nodes | Main Part |
Competence Template Reference Node | sktr | References a competence template or a competence template category | Main Part |
Competence Template Node | sktp | Defines Competence Levels | Template Part |
Competence Template Category Node | sctp | Can contain competence templates or competence template categories | Template Part |
Identifying a Competence
We now focus on the leaf nodes in the competence tree that define the competence levels (either type "skll" or type "sktp").
If no competence templates would be used, a competence could simply be identified by its node ID. But since competence templates can be reused multiple times we need a second node ID, the ID of the competence reference to identify a competence.
Structure of an ID for a competence: <skill_id>:<tref_id>
If the skill_id
is the ID of a simple competence node (type "skll"), the tref_id
must be 0. If the skill_id
is the ID of a competence template node (type "sktp") the tref_id
must be the ID of a reference node (type "sktr"). A reference node refers to a template identified by the root node of the template (either type "sktp" or "sctp"). The node with the ID skill_id
must be within the subtree of this template.
The most important to keep in mind is that a competence is identified by the two parts <skill_id>:<tref_id>
.
Tabs
Using Tabs
Since 3.10 a simpler handling for tabbed menues has been introduced. One methode usually defines the main tabs in a GUI class, many times called setTabs()
. The methods that execute a certain command activate the corresponding tab. Tabs are identified by ids. A global $ilTabs
instance is used to add/activate tabs and subtabs.
function __construct(ilTabsGUI $tabs)
{
$this->tabs = $tabs; // alternatively throug $DIC->tabs();
}
// view command
function view()
{
[...]
$this->tabs->activateTab("id_view");
[...]
}
// edit command
function edit()
{
[...]
$this->tabs->activateTab("id_edit");
[...]
}
// setting main tabs
function setTabs()
{
global $ilAccess, $lng, $ilTabs, $ilCtrl;
if ($this->access->checkAccess("read", "", $this->object->getRefId()))
{
// add main tab, id
$this->tabs->addTab("id_view",
$this->lng->txt("view"),
$this->ctrl->getLinkTarget($this, "view"));
}
if ($this->access->checkAccess("write", "", $this->object->getRefId()))
{
$this->tabs->addTab("id_edit",
$this->lng->txt("edit"),
$this->ctrl->getLinkTarget($this, "edit"));
}
}
\ Common methods that should be used:
Add a tab:
$tabs->addTab($a_id, $a_text, $a_link, $a_frame = "");
Activate a tab:
$tabs->activateTab($a_id);
Add a subtab:
$tabs->addSubTab($a_id, $a_text, $a_link, $a_frame = "");
Activate a subtab:
$tabs->activateSubTab($a_id);
Clear all tabs:
$tabs->clearTargets();
Clear subtabs:
$tabs->clearSubTabs();
\ Other methods that are rarely used:
Remove a tab:
$tabs->removeTab($a_id);
Replace a tab:
$tabs->replaceTab($a_old_id,$a_new_id,$a_text,$a_link,$a_frame = '');
\ For help with the correct ordering of tabs please read the tabs guideline.
Taxonomy
Using Taxonomies
Taxonomy Settings in Repository Objects (ILIAS 9)
The taxonomy service is available via the DIC.
$taxonomy_service = $DIC->taxonomy();
Activation Setting
The general taxonomy service should be activated as "Additional Feature" in the main settings screen by using ilObjectServiceSettingsGUI
with the ilObjectServiceSettingsGUI::TAXONOMIES
flag.
Taxonomy Settings Subtab
You should add the taxonomy settings as a subtab under your main settings tab. This can be done by passing the object ID of your repository object to addSettingsSubTab
. Note that the subtab will only appear if the general setting is activated for your object.
$taxonomy_service->gui()->addSettingsSubTab($obj_id);
Forwarding to the Taxonomy Settings
You need to forward to the ilTaxonomySettingsGUI
class.
ilCtrl Declaration:
/**
* @ilCtrl_Calls ...: ilTaxonomySettingsGUI
*/
Forwarding in executeCommand:
...
case strtolower(ilTaxonomySettingsGUI::class):
$tax_gui = $taxonomy_service->gui()->getSettingsGUI(
$obj_id
);
$this->ctrl->forwardCommand($tax_gui);
break;
...
The getSettingsGUI
method provides four parameters, only the first one is mandatory:
- (int) Object ID of your repository object
- (string) An information text that is displayed on top of the settings screen
- (bool) If true, multiple taxonomies can be created for your repository object
- (\ILIAS\Taxonomy\Settings\ModifierGUIInterface) An object that implements the
ModifierGUIInterface
interface.
Settings Modifier
The ModifierGUIInterface
interface allows to add additional properties to the taxonomy items in the taxonomy list and to add additional actions for each taxonomy in the list. This way your context may introduce separate settings for each taxonomy, e.g. the Category module allows to activate presentations of taxonomies as side blocks in the presentation.
Taxonomies as Table Filter
The input class is called ilTaxSelectInputGUI
. Since this class makes subsequent requests that must be routed to an instance of the class, your table must be included in the ilCtrl control flow and forward commands to the form of the filter.
Your TableGUI class must...
- include a
@ilCtrl_Calls
comment forilFormPropertyDispatchGUI
and - add a
ilTaXSelectInputGUI
filter item in initFilter.
/**
* ...
* @ilCtrl_Calls ilPresentationListTableGUI: ilFormPropertyDispatchGUI
*/
class ilPresentationListTableGUI extends ilTable2GUI
{
...
/**
* Init filter
*/
function initFilter()
{
...
include_once("./components/ILIAS/Taxonomy/classes/class.ilTaxSelectInputGUI.php");
$tax = new ilTaxSelectInputGUI($this->tax_id, "tax_node", true);
$this->addFilterItem($tax);
$tax->readFromSession();
$this->filter["tax_node"] = $tax->getValue();
...
}
}
The GUI class that outputs the table must...
- include a
@ilCtrl_Calls
comment for your TableGUI class, - forward to your TableGUI class via
executeCommand
and - get the HTML of your TableGUI class by using
$ilCtrl->getHTML($table);
, not$table->getHTML();
.
/**
* ...
* @ilCtrl_Calls ilGlossaryPresentationGUI: ilPresentationListTableGUI
*/
class ilGlossaryPresentationGUI
{
...
/**
* execute command
*/
function executeCommand()
{
...
$next_class = $this->ctrl->getNextClass($this);
...
switch($next_class)
{
case "ilpresentationlisttablegui":
$prtab = $this->getPresentationTable();
$ilCtrl->forwardCommand($prtab);
break;
default:
...
break;
}
$this->tpl->show();
}
function getPresentationTable()
{
include_once("./components/ILIAS/Glossary/classes/class.ilPresentationListTableGUI.php");
$table = new ilPresentationListTableGUI($this, "listTerms", $this->glossary,
$this->offlineMode(), $this->tax_node, $this->glossary->getTaxonomyId());
return $table;
}
function listTerms()
{
...
$table = $this->getPresentationTable();
$tpl->setContent($ilCtrl->getHTML($table));
}
}
Tracking
Learning Progress
Note: This documentation may not be complete, but the points documented should (still) be correct. Reports of missing or wrong information using the ILIAS issue tracker or contributions via Pull Request are greatly appreciated.
Core concepts
The learning progress service in ILIAS consists of 3 core concepts:
- Change Event
- (Learning Progress) Status
- (Learning Progress) Marks
The other components like (object) statistics and session statistics which can be found in Administration > Statistics and Learning Progress are not part of this how-to.
Change Event
Not to be confused with ILIAS event handling, the change event service keeps track of user activity in ILIAS repository objects. There are read and write events. The learning progress only utilizes read events which are recorded / updated for each "click" inside a repository object. Write events are mostly restricted to changes in object settings or new repository objects and are the basis for the "changed inside" messages in the repository object lists.
Read events mostly consist of 2 figures which are tracked for each repository object and user:
- read count (or "requests")
- spent seconds (or "time spent")
For every "click" or request ILIAS calculates the time which has passed since the last request and depending on the administration setting "Max. Time Between Requests" will - add the time to the "spent seconds" figure of the current object if the time passed is below the threshold - increment the "read count" figure of the current object and ignore the time passed if it is above the threshold Both figures will also be added to all (current) parent objects.
DB: "change_event"
LP status
The learning progress status is designed to make the different repository object types comparable regarding user activity and its result. There are several different ways a LP status is calculated - called LP modes - and those can be configured for most object types which support the learning progress feature.
A learning progress status has 4 possible values:
- "not attempted": the user has no recorded activity
- "in progress": the user has recorded activity but no result yet
- "completed": the user has completed the object
- "failed": the user has failed the object
Please keep in mind that "object" is not limited to repository object here but can also mean SCO, learning module chapter, learning objective and so on.
Each repository object type supports different modes of learning progress, e.g.
- Manual by Tutor
- Automatic by Collection of Objects
- Manual by Learner
- Test finished and so on.
Currently the learning progress does not support any notion of a final status. At anytime a user LP status may change for an object. On changing the learning progress mode for a repository object - which is always possible - all LP status for existing users are re-calculated.
The LP status calculation (including the optional percentage) has to be triggered by a repository object each time "something" changed that might result in a LP status change. That "something" depends on the current LP mode for that object. The most simple example would be the 1st read event or 1st click of a user inside an object which (most of the time) results in a status change from "not attempted" to "in progress".
See:
ilLearningProgress::_tracProgress()
ilLPStatusWrapper::_updateStatus()
Do NOT use ilLPStatusWrapper::_refreshStatus()
, which will re-calculate the LP status for the complete object. Please refer to the ilObjectLP::resetLPDataFor*
-methods on how to remove LP data properly. Each repository object type that supports learning progress has its own "connector" class which extends ilObjectLP
.
Hint: there is a LPStatus*
-class for each LP mode, do not get confused by this.
DB: "ut_lp_marks" (status, status_changed, status_dirty, percentage)
Collections
A collection consists of 1-n sub-objects which can be repository objects, SCOs, learning modules or learning objectives. There is no way to discern this in the DB, it depends on the LP mode of the parent object. The LP status of a collection is determined by the status of its sub-objects (and their optional groupings). Every time a LP status changes for an object every parent collection is updated accordingly. This can lead to chains of updates for nested collections.
DB: "ut_lp_collections"
LP marks
This directly corresponds to the "edit"-form in the LP for single users. It is mostly used for "Manual by Tutor"-mode where the "completed"-flag translates to LP status "completed".
DB: "ut_lp_marks" (completed, mark, u_comment)
Misc
The complex LP DB queries can currently all be found in ilTrQuery
. This might change.
Do not call any ilLPStatus*
-class directly, use ilLPStatusWrapper
.
The LP status is a mixture of progress and success information. Those 2 concepts are kept separate in SCORM. We are aware of this, but in ILIAS the focus for the learning progress design is to keep the LP status consistent and comparable for all object types.
Figures in LP statistics
- Access/Time Spent
- For each "click" ILIAS measures the time which passed since the last "click". if it is below the threshold (administration > lp > "max. time between requests") it will be counted as the same request and the time difference will be added to "Time spent" (for the current object). If is bigger than the threshold the Access Number is incremented and the time difference is ignored.
- Things are completely different for SCORM modules though, as the player is more or less an external black box (which does the access/time spent handling by itself).
- Please keep in mind that access number and time spent of sub-items, e.g. objects in courses, will be added to their parent (in the LP statistics). Furthermore if you move sub-items to a different parent, the numbers might not add up at all.
- Percentage
- This is decided by the specific test LP mode. "Test passed" seems to use points as basis (not number of questions).
- Last Status Change
- "last access" is the last "click"/action in an object, "last status change" means the point in time when the LP status changed for the last time, e.g. from "in progress" to "completed".
- Total Time Online
- This is the time spent logged into ILIAS regardless of object or context. The access/time spent logic also applies here (see above). We might discuss the presentation of this in the near future, currently we would favour to include it in the user management and remove it from the LP statistics.
- Last Login
- This is the datetime of the last login whereas "last access" is updated on each "click" (and for each object).
- Working Time
- This is test-specific and is not supplied by the learning progress.
Permissions
- read_learning_progress: read learning progress of other users
- This gives access to the LP data of others users in the LP statistics in the repository If a user can see his/her own LP status is determined by the administration setting "Accesible Personal Learning Progress".
- edit_learning_progress
- This allows to edit the LP settings of a repository object and edit the LP data of object "members": comment, mark, completed.
- "See learning progress overview of other users" (Administration > LP)
- This gives access to the tab "Users" in Personal Desktop > LP. Due to performance reasons we cannot use object permissions to determine the access.
User
User Data
Access Data of the User Currently Logged in
An instance of class ilObjUser
for the user currently logged in ILIAS is available through the DI-Container. You can use the public methods of this class to access the data of the current user (see components/ILIAS/User/classes/class.ilObjUser.php
):
function foo()
{
global $ilUser;
[...]
$user_id = $ilUser->getId();
$user_firstname = $ilUser->getFirstname();
$user_lastname = $ilUser->getLastname();
[...]
}
Standard User Name Presentation
The class ilUserUtil
provides a static method called getNamePresentation(...)
that should be used to display user first and last name whenever possible.
- The login is always displayed as [login]
- First and last name are only displayed if there is a public profile (this can be overridden by parameter
$a_force_first_lastname
) - Optionally the user image can be included
- Optionally a link to the public profile of the user can be included
$this->tpl->setVariable("TXT_USER",
ilUserUtil::getNamePresentation($user_id, true, true, $back_link));
Web Access Checker
Web Access Checker
What's new in ILIAS 5.1
- Web Access Checker is enabled by default
- Now all Files in the ./data directory will be checked
- New Token-Based File delivery (much faster than before)
The new WebAccessChecker allows fast and secure delivery of files in the /data directory (see Feature-Wiki for performance comparison). As definied in the Data Directory Guideline, directories within the /sec-Folder are supported. Since there are folders outside the /sec-Folder which have to be secured, the new WebAccessChecker also secures those directories (e.g. lm_data, usr_images, mobs).
All requests to /data are now redirected to the WAC-Script per default, the .htaccess-File has a new entry:
RewriteRule ^data/.*/.*/.*$ wac.php [L]
The WAC delivers the file after the following decisions:
The most performant ways to deliver files are the file- or the folder-based tokens. These requests are already signed and and access-checked during the rendering of a page (e.g. display of poll-image, the user accesses a course and ILIAS renders a poll-image to the page. At this moment the access is already checked).
If there is no token or the token has become invalid, e previosly registred checking-instance has to decide whether the file can be delivered or not. if theres no checking instance, only files outside the /secore-Folder will bedelivered. if there's a checking instance and the instance declines delivery, access to the file is also denied.
Implementation
Signing Files
Developers can sign files and folders using the ilWACSignedPath-Class:
// Example in Poll:
$img = $a_poll->getImageFullPath();
$this->tpl->setVariable("URL_IMAGE", ilWACSignedPath::signFile($img));
// Example in SCORM-Module
ilWACSignedPath::signFolderOfStartFile($this->slm->getDataDirectory().'/manifest.xml');
Register Checking instance
When registering a checking instance, developers have to add a secured path to their service.xml or module.xml:
<web_access_checker>
<secure_path path="ilPoll" checking-class="ilObjPollAccess" in-sec-folder='1'/>
</web_access_checker>
This entry will be registred with the next structure reload (add one if you want to register a new secured path). It's allowed to have multiple checking-instances per module/service but they must have unique paths. After the reload all requests in ./data/my_client/sec/ilPoll/* will be checked by the class ilObjPollAccess. The method canBeDelivered() which is defined by the ilWACCheckingClass-interface receives the ilWACPath Object which proviedes several information about the requested file. Most Modules will look for the obj_id and check using ilAccess.
class ilObjPollAccess extends ilObjectAccess implements ilWACCheckingClass
{
// Other methods and checking functions of ilObjPollAccess
/**
* @param ilWACPath $ilWACPath
*
* @return bool
*/
public function canBeDelivered(ilWACPath $ilWACPath) {
global $ilAccess;
preg_match("/\\/poll_([\\d]*)\\//uism", $ilWACPath->getPath(), $results);
foreach (ilObject2::_getAllReferences($results[1]) as $ref_id) {
if ($ilAccess->checkAccess('read', '', $ref_id)) {
return true;
}
}
return false;
}
}
?>
Error Handling
Per default the Web Access Checker delivers on images and videos a error-placeholder when the user has no permission to access the file.
File Delivery
Delivering files is implemented using the ilFileDelivery-Service introduced in ILIAS 5. ilFileDelivery supports X-SendFile and introduces a updated ilMimeTypeUtil. Straming videos is now done chunked and allows (as previously in ilUtil) ranges. Methods in ilUtil are marked as deprecated.
// File-Delivery example
$ilFileDelivery = new ilFileDelivery('./components/ILIAS/WebAccessChecker/templates/images/access_denied.png', 'file_name.png');
$ilFileDelivery->setDisposition(ilFileDelivery::DISP_INLINE);
$ilFileDelivery->deliver();
If you want to use ilFileDeliver with X-SendFile please install and activate
sudo apt-get install libapache2-mod-xsendfile
sudo a2enmod xsendfile
In your Apache-Config or VHOST the "iliasdata"- and the "data"- directories must be unlocked, e.g.:
XSendFilePath /var/www
XSendFilePath /var/iliasdata
Additionally in the .htaccess the following rule activated the X-SendFile Module:
XSendFile On