Otrs Developer Book
Otrs Developer Book
Otrs Developer Book
Build Date:
2016-03-14
OTRS 6 - Developer Manual
Copyright © 2003-2016 OTRS AG
This work is copyrighted by OTRS AG.
You may copy it in whole or in part as long as the copies retain this copyright statement.
All trade names are used without the guarantee for their free use and are possibly registered trade marks. All
products mentioned in this manual may be trade marks of the respective manufacturer.
The source code of this document can be found at github, in the repository doc-developer. Contributions are more
than welcome. You can also help translating it to your language at Transifex.
Table of Contents
1. Getting Started .......................................................................................................... 1
1. Development Environment ................................................................................. 1
1.1. Framework checkout ................................................................................ 1
1.2. Useful Tools .............................................................................................. 1
1.3. Linking Expansion Modules ...................................................................... 1
2. Architecture Overview ........................................................................................ 2
2.1. Directories ................................................................................................ 3
2.2. Files .......................................................................................................... 4
2.3. Core Modules ........................................................................................... 4
2.4. Frontend Handle ....................................................................................... 4
2.5. Frontend Modules ..................................................................................... 5
2.6. CMD Frontend .......................................................................................... 5
2.7. Generic Interface Modules ....................................................................... 5
2.8. Scheduler Task Handler Modules ............................................................. 6
2.9. Database .................................................................................................. 6
2. OTRS Internals - How it Works .................................................................................. 7
1. Config Mechanism .............................................................................................. 7
1.1. Defaults.pm: OTRS Default Configuration .............................................. 7
1.2. Automatically Generated Configuration Files ........................................... 7
1.3. XML Configuration Files ........................................................................... 7
1.4. Accessing Config Options at Runtime .................................................... 15
2. Database Mechanism ....................................................................................... 15
2.1. How it works .......................................................................................... 15
2.2. Database Drivers ................................................................................... 19
2.3. Supported Databases ............................................................................. 19
3. Log Mechanism ................................................................................................. 19
3.1. System Log ............................................................................................ 19
3.2. Communication Log ............................................................................... 20
4. Date and Time ................................................................................................. 22
4.1. Introduction ............................................................................................ 22
4.2. Creation of a DateTime object ............................................................... 22
4.3. Time zones ............................................................................................. 23
4.4. Method summary ................................................................................... 23
4.5. Deprecated package Kernel::System::Time ........................................... 24
5. Skins ................................................................................................................. 24
5.1. Skin Basics ............................................................................................. 24
5.2. How skins are loaded ............................................................................ 25
5.3. Creating a New Skin .............................................................................. 26
6. The CSS and JavaScript "Loader" ..................................................................... 27
6.1. How it works .......................................................................................... 27
6.2. Basic Operation ...................................................................................... 28
6.3. Configuring the Loader: JavaScript ........................................................ 29
6.4. Configuring the Loader: CSS .................................................................. 31
7. Templating Mechanism ..................................................................................... 32
7.1. Template Commands ............................................................................. 33
7.2. Using a template file ............................................................................. 39
8. Creating Your Own Themes .............................................................................. 40
9. Localization / Translation Mechanism ............................................................... 40
9.1. Marking translatable strings in the source files ..................................... 40
9.2. Collecting translatable strings into the translation database ................. 41
9.3. The translation process itself ................................................................. 42
9.4. Using the translated data from the code ............................................... 43
3. How to Extend OTRS ............................................................................................... 44
1. Writing a new OTRS frontend module .............................................................. 44
1.1. What we want to write .......................................................................... 44
iii
1.2. Default Config File ................................................................................. 44
1.3. Frontend Module .................................................................................... 45
1.4. Core Module ........................................................................................... 45
1.5. Template File .......................................................................................... 47
1.6. Language File ......................................................................................... 47
1.7. Summary ................................................................................................ 47
2. Using the power of the OTRS module layers ................................................... 48
2.1. Authentication and user management .................................................. 48
2.2. Preferences ............................................................................................ 55
2.3. Other core functions .............................................................................. 64
2.4. Frontend Modules ................................................................................... 86
2.5. Generic Interface Modules ..................................................................... 94
2.6. Daemon And Scheduler ....................................................................... 115
2.7. Dynamic Fields ..................................................................................... 120
2.8. Email Handling ..................................................................................... 153
4. How to Publish Your OTRS Extensions ................................................................... 156
1. Package Management .................................................................................... 156
1.1. Package Distribution ............................................................................ 156
1.2. Package Commands ............................................................................. 156
2. Package Building ............................................................................................ 157
2.1. Package Spec File ................................................................................ 157
2.2. Example .sopm ..................................................................................... 163
2.3. Package Build ....................................................................................... 164
2.4. Package Life Cycle - Install/Upgrade/Uninstall ..................................... 164
3. Package Porting .............................................................................................. 164
3.1. From OTRS 5 to 6 ................................................................................ 164
3.2. From OTRS 4 to 5 ................................................................................ 179
3.3. From OTRS 3.3 to 4 ............................................................................. 181
5. Contributing to OTRS ............................................................................................. 189
1. Sending Contributions .................................................................................... 189
2. Translating OTRS ............................................................................................ 189
2.1. Updating an existing translation .......................................................... 189
2.2. Adding a new frontend translation ...................................................... 190
3. Translating the Documentation ...................................................................... 190
4. Code Style Guide ........................................................................................... 190
4.1. Perl ....................................................................................................... 190
4.2. JavaScript ............................................................................................. 201
4.3. HTML .................................................................................................... 202
4.4. CSS ....................................................................................................... 202
5. User Interface Design ..................................................................................... 203
5.1. Capitalization ....................................................................................... 203
6. Accessibility Guide ......................................................................................... 203
6.1. Accessibility Basics .............................................................................. 204
6.2. Accessibility Standards ........................................................................ 204
6.3. Implementation guidelines ................................................................... 205
7. Unit Tests ........................................................................................................ 207
7.1. Creating a test file ............................................................................... 207
7.2. Prerequisites for testing ....................................................................... 209
7.3. Testing .................................................................................................. 209
7.4. Unit Test API ......................................................................................... 210
A. Additional Resources .............................................................................................. 214
iv
List of Figures
1.1. OTRS Architecture ................................................................................................... 2
1.2. Generic Interface Architecture ................................................................................ 3
3.1. Dashboard Widget ................................................................................................ 87
3.2. Dynamic Fields Architecture ............................................................................... 121
3.3. Dynamic Field Interaction ................................................................................... 125
3.4. Email Processing Flow ......................................................................................... 155
4.1. Package Life Cycle .............................................................................................. 164
v
List of Tables
4.1. Template Changes from OTRS 3.3 to 4 ............................................................... 186
vi
Chapter 1. Getting Started
OTRS is a multi-platform web application framework which was originally developed for a
trouble ticket system. It supports different web servers and databases.
This manual shows how to develop your own OTRS modules and applications based on the
OTRS styleguides.
1. Development Environment
To facilitate the writing of OTRS expansion modules, the creation of a development en-
vironment is necessary. The source code of OTRS and additional public modules can be
found on github.
1.1. Framework checkout
First of all a directory must be created in which the modules can be stored. Then switch
to the new directory using the command line and check them out by using the following
command:
Check out the module-tools module (from github) too, for your development environ-
ment. It contains a number of useful tools:
Please configure the OTRS system according to the installation instructions in the admin
manual.
1.2. Useful Tools
There are two modules that are highly recommended for OTRS development: OTRSCode-
Policy and Fred.
OTRSCodePolicy is a code quality checker that enforces the use of common coding stan-
dards also for the OTRS development team. It is highly recommended to use it if you plan
to make contributions. You can use it as a standalone test script or even register it as a
git commit hook that runs every time that you create a commit. Please see the module
documentation for details.
Fred is a little development helper module that you can actually install or link (as described
below) into your development system. It features several helpful modules that you can
activate, such as an SQL logger or an STDERR console. You can find some more details
in its module documentation.
By the way, these tools are also open source, and we will be happy about any improve-
ments that you can contribute.
1
OTRS access the files, links must be created. This is done by a script in the directory
module tools repository. Example: Linking the Calendar Module:
Whenever new files are added, they must be linked as described above.
As soon as the linking is completed, the SysConfig must be rebuilt to register the module
in OTRS. Additional SQL or Perl code from the module must also be executed. Example:
2. Architecture Overview
The OTRS framework is modular. The following picture shows the basic layer architecture
of OTRS.
Figure 1.1. OTRS Architecture
2
Introduced in OTRS 3.1, the OTRS Generic Interface continues OTRS modularity. The next
picture shows the basic layer architecture of the Generic Interface.
2.1. Directories
Directory Description
bin/ commandline tools
bin/cgi-bin/ web handle
bin/fcgi-bin/ fast cgi web handle
Kernel application codebase
Kernel/Config/ configuration files
Kernel/Config/Files configuration files
Kernel/GenericInterface/ the Generic Interface API
Kernel/GenericInterface/Invoker/ invoker modules for Generic Interface
Kernel/GenericInterface/Mapping/ mapping modules for Generic Interface, e.g.
Simple
Kernel/GenericInterface/Operation/ operation modules for Generic Interface
Kernel/GenericInterface/Transport/ transport modules for Generic Interface,
e.g. "HTTP SOAP"
Kernel/Language language translation files
Kernel/Scheduler/ Scheduler files
3
Directory Description
Kernel/Scheduler/TaskHandler handler modules for scheduler tasks, e.g.
GenericInterface
Kernel/System/ core modules, e.g. Log, Ticket...
Kernel/Modules/ frontend modules, e.g. QueueView...
Kernel/Output/HTML/ html templates
var/ variable data
var/log logfiles
var/cron/ cron files
var/httpd/htdocs/ htdocs directory with index.html
var/httpd/htdocs/skins/Agent/ available skins for the Agent interface
var/httpd/htdocs/skins/Customer/ available skins for the Customer interface
var/httpd/htdocs/js/ JavaScript files
scripts/ misc files
scripts/test/ unit test files
scripts/test/sample/ unit test sample data files
2.2. Files
.pl = Perl
2.3. Core Modules
Core modules are located under $OTRS_HOME/Kernel/System/*. This layer is for the log-
ical work. Core modules are used to handle system routines like "lock ticket" and "create
ticket". A few main core modules are:
2.4. Frontend Handle
The interface between the browser, web server and the frontend modules. A frontend
module can be used via the HTTP-link.
4
http://localhost/otrs/index.pl?Action=Module
2.5. Frontend Modules
Frontend modules are located under $OTRS_HOME/Kernel/Modules/*.pm. There are two
public functions in there - new() and run() - which are accessed from the Frontend Handle
(e.g. index.pl).
new() is used to create a frontend module object. The Frontend Handle provides the used
frontend module with the basic framework objects. These are, for example: ParamObject
(to get web form params), DBObject (to use existing database connections), LayoutOb-
ject (to use templates and other html layout functions), ConfigObject (to access config
settings), LogObject (to use the framework log system), UserObject (to get the user
functions from the current user), GroupObject (to get the group functions).
2.6. CMD Frontend
The CMD (Command) Frontend is like the Web Frontend Handle and the Web Frontend
Module in one (just without the LayoutObject) and uses the core modules for some ac-
tions in the system.
Generic Interface Invoker modules are used as a backend to create requests for Remote
Systems to execute actions.
5
2.7.2. Generic Interface Mapping Modules
Generic Interface Mapping modules are located under $OTRS_HOME/Kernel/GenericIn-
terface/Mapping/*. These modules are used to transform data (keys and values) from
one format to another.
Generic Interface operation modules are used as a backend to perform actions requested
by a remote system.
Generic Interface transport modules are used send data to, and receive data from a Re-
mote System.
2.9. Database
The database interface supports different databases.
For the OTRS data model please refer to the files in your /doc directory. Alternatively you
can look at the data model on github .
6
Chapter 2. OTRS Internals -
How it Works
1. Config Mechanism
OTRS comes with a dedicated mechanism to manage configuration options via a graphical
interface (System Configuration). This section describes how it works internally and how
you can provide new configuration options or change existing default values.
ZZZAAuto.pm
Perl cache of the XML configuration's current values (default or modified by user)
ZZZACL.pm
ZZZACL.pm
These files are a Perl representation of the current system configuration. They should
never be manually changed as they are overwritten by OTRS.
7
</otrs_config>
The global init attribute describes where the config options should be loaded. There
are different levels available and will be loaded/overloaded in the following order:
Framework (for framework settings e. g. session option), Application (for application
settings e. g. ticket options), Config (for extensions to existing applications e. g. ITSM
options) and Changes (for custom development e. g. to overwrite framework or ticket
options).
The configuration items are written as Setting elements with a Description, a Naviga-
tion group (for the tree-based navigation in the GUI) and the Value that it represents.
Here's an example:
Valid
ConfigLevel
If the optional attribute ConfigLevel is set, the config variable might not be edited by
the administrator, depending on his own config level. The config variable ConfigLevel
sets the level of technical experience of the administrator. It can be 100 (Expert), 200
(Advanced) or 300 (Beginner). As a guideline which config level should be given to
an option, it is recommended that all options having to do with the configuration of
external interaction, like Sendmail, LDAP, SOAP, and others should get a config level
of at least 200 (Advanced).
Invisible
Readonly
UserModificationPossible
8
UserModificationActive
UserPreferencesGroup
• Don't place new settings in Core directly. This is reserved for a few important global
settings.
• Core::* can take new groups that contain settings that cover the same topic (like
Core::Email) or relate to the same entity (like Core::Queue).
• Don't add new direct child nodes within Frontend. Global front end settings go to Fron-
tend::Base, settings only affecting a part of the system go to the respective Admin,
Agent, Customer or Public sub nodes.
• Front end settings that only affect one screen should go to the relevant screen (View)
node (create one if needed), for example AgentTicketZoom related settings go to
Frontend::Agent::View::TicketZoom. If there are module layers within one screen
with groups of related settings, they would also go to a sub group here (e. g. Fron-
tend::Agent::View::TicketZoom::MenuModule for all ticket zoom menu module reg-
istrations).
1.3.1.1. Item
An Item element holds one piece of data. The optional ValueType attribute determines
which kind of data and how it needs to be presented to the user in the GUI. If no ValueType
is specified, it defaults to String.
9
1.3.1.2. Array
With this config element arrays can be displayed.
<Setting Name="SettingName">
...
<Value>
<Array>
<Item Translatable="1">Value 1</Item>
<Item Translatable="1">Value 2</Item>
...
</Array>
</Value>
</Setting>
1.3.1.3. Hash
With this config element hashes can be displayed.
<Setting Name="SettingName">
...
<Value>
<Hash>
<Item Key="One" Translatable="1">First</Item>
<Item Key="Two" Translatable="1">Second</Item>
...
</Hash>
</Value>
</Setting>
It's possible to have nested array/hash elements (like hash of arrays, array of hashes,
array of hashes of arrays, ...). Below is an example array of hashes.
<Setting Name="ExampleAoH">
...
<Value>
<Array>
<DefaultItem>
<Hash>
<Item></Item>
</Hash>
</DefaultItem>
<Item>
<Hash>
<Item Key="One">1</Item>
<Item Key="Two">2</Item>
</Hash>
</Item>
<Item>
<Hash>
<Item Key="Three">3</Item>
<Item Key="Four">4</Item>
</Hash>
</Item>
</Array>
</Value>
</Setting>
1.3.2. Value Types
The XML config settings support various types of configuration variables.
10
1.3.2.1. String
<Setting Name="SettingName">
...
<Value>
<Item ValueType="String" ValueRegex=""></Item>
</Value>
</Setting>
A config element for numbers and single-line strings. Checking the validity with a regular
expression is possible (optional). This is the default ValueType.
<Setting Name="SettingName">
...
<Value>
<Item ValueType="String" ValueRegex="" Translatable="1">Value</Item>
</Value>
</Setting>
The optional Translatable attribute marks this setting as translatable, which will cause
it to be included in the OTRS translation files. This attribute can be placed on any tag
(see also below).
1.3.2.2. Password
A config element for passwords. It's still stored as plain text in the XML, but it's masked
in the GUI.
<Setting Name="SettingName">
...
<Value>
<Item ValueType="Password">Secret</Item>
</Value>
</Setting>
1.3.2.3. PerlModule
A config element for Perl module. It has a ValueFilter attribute, which filters possible
values for selection. In the example below, user can select Perl module Kernel::Sys-
tem::Log::SysLog or Kernel::System::Log::File.
<Setting Name="SettingName">
...
<Value>
<Item ValueType="PerlModule" ValueFilter="Kernel/System/Log/
*.pm">Kernel::System::Log::SysLog</Item>
</Value>
</Setting>
1.3.2.4. Textarea
A config element for multiline text.
<Setting Name="SettingName">
...
<Value>
<Item ValueType="Textarea"></Item>
</Value>
11
</Setting>
1.3.2.5. Select
This config element offers preset values as a pull-down menu. The SelectedID or Select-
edValue attributes can pre-select a default value.
<Setting Name="SettingName">
...
<Value>
<Item ValueType="Select" SelectedID="Queue">
<Item ValueType="Option" Value="Queue" Translatable="1">Queue</Item>
<Item ValueType="Option" Value="SystemAddress" Translatable="1">System address</
Item>
</Item>
</Value>
</Setting>
1.3.2.6. Checkbox
This config element checkbox has two states: 0 or 1.
<Setting Name="SettingName">
...
<Value>
<Item ValueType="Checkbox">0</Item>
</Value>
</Setting>
1.3.2.7. Date
This config element contains a date value.
<Setting Name="SettingName">
...
<Value>
<Item ValueType="Date">2016-02-02</Item>
</Value>
</Setting>
1.3.2.8. DateTime
This config element contains a date and time value.
<Setting Name="SettingName">
...
<Value>
<Item ValueType="DateTime">2016-12-08 01:02:03</Item>
</Value>
</Setting>
1.3.2.9. Directory
This config element contains a directory.
<Setting Name="SettingName">
...
<Value>
12
<Item ValueType="Directory">/etc</Item>
</Value>
</Setting>
1.3.2.10. File
This config element contains a file path.
<Setting Name="SettingName">
...
<Value>
<Item ValueType="File">/etc/hosts</Item>
</Value>
</Setting>
1.3.2.11. Entity
This config element contains a value of a particular entity. ValueEntityType attribute
defines the entity type. Supported entities: DynamicField, Queue, Priority, State and Type.
Consistency checks will ensure that only valid entities can be configured and that entities
used in the configuration cannot be set to invalid. Also, when an entity is renamed, all
referencing config settings will be updated.
<Setting Name="SettingName">
...
<Value>
<Item ValueType="Entity" ValueEntityType="Queue">Junk</Item>
</Value>
</Setting>
1.3.2.12. TimeZone
This config element contains a time zone value.
<Setting Name="SettingName">
...
<Value>
<Item ValueType="TimeZone">UTC</Item>
</Value>
</Setting>
1.3.2.13. VacationDays
This config element contains definitions for vacation days which are repeating each year.
Following attributes are mandatory: ValueMonth, ValueDay.
<Setting Name="SettingName">
...
<Value>
<Item ValueType="VacationDays">
<DefaultItem ValueType="VacationDays"></DefaultItem>
<Item ValueMonth="1" ValueDay="1" Translatable="1">New Year's Day</Item>
<Item ValueMonth="5" ValueDay="1" Translatable="1">International Workers' Day</
Item>
<Item ValueMonth="12" ValueDay="24" Translatable="1">Christmas Eve</Item>
</Item>
</Value>
</Setting>
13
1.3.2.14. VacationDaysOneTime
This config element contains definitions for vacation days which occur only once. Follow-
ing attributes are mandatory: ValueMonth, ValueDay and ValueYear.
<Setting Name="SettingName">
...
<Value>
<Item ValueType="VacationDaysOneTime">
<Item ValueYear="2004" ValueMonth="1" ValueDay="1">test</Item>
</Item>
</Value>
</Setting>
1.3.2.15. WorkingHours
This config element contains definitions for working hours.
<Setting Name="SettingName">
...
<Value>
<Item ValueType="WorkingHours">
<Item ValueType="Day" ValueName="Mon">
<Item ValueType="Hour">8</Item>
<Item ValueType="Hour">9</Item>
</Item>
<Item ValueType="Day" ValueName="Tue">
<Item ValueType="Hour">8</Item>
<Item ValueType="Hour">9</Item>
</Item>
</Item>
</Value>
</Setting>
1.3.2.16. Frontend Registration
Module registration for Agent Interface:
<Setting Name="SettiFrontend::Module###AgentModuleName">
...
<Value>
<Item ValueType="FrontendRegistration">
<Hash>
<Item Key="Group">
<Array>
</Array>
</Item>
<Item Key="GroupRo">
<Array>
</Array>
</Item>
<Item Key="Description" Translatable="1">Phone Call.</Item>
<Item Key="Title" Translatable="1">Phone-Ticket</Item>
<Item Key="NavBarName">Ticket</Item>
</Hash>
</Item>
</Value>
</Setting>
14
considers that you want simple Array/Hash with scalar values. DefaultItem is used as a
template when adding new elements, so it can contain additional attributes, like Value-
Type, and it can define default values.
<Array>
<DefaultItem>
<Array>
<DefaultItem ValueType="Select" SelectedID='option-2'>
<Item ValueType="Option" Value="option-1">Option 1</Item>
<Item ValueType="Option" Value="option-2">Option 2</Item>
</DefaultItem>
</Array>
</DefaultItem>
...
</Array>
<Hash>
<DefaultItem>
<Hash>
<DefaultItem ValueType="Date">2017-01-01</DefaultItem>
</Hash>
</DefaultItem>
...
</Hash>
my $ConfigOption = $Kernel::OM->Get('Kernel::Config')->Get('Prefix::Option');
If you want to change a config option at runtime and just for this one request/process:
$Kernel::OM->Get('Kernel::Config')->Set(
Key => 'Prefix::Option'
Value => 'SomeNewValue',
);
2. Database Mechanism
OTRS comes with a database layer that supports different databases.
2.1. How it works
The database layer (Kernel::System::DB) has two input options: SQL and XML.
15
2.1.1. SQL
The SQL interface should be used for normal database actions (SELECT, INSERT, UP-
DATE, ...). It can be used like a normal Perl DBI interface.
2.1.1.1. INSERT/UPDATE/DELETE
$Kernel::OM->Get('Kernel::System::DB')->Do(
SQL=> "INSERT INTO table (name, id) VALUES ('SomeName', 123)",
);
$Kernel::OM->Get('Kernel::System::DB')->Do(
SQL=> "UPDATE table SET name = 'SomeName', id = 123",
);
$Kernel::OM->Get('Kernel::System::DB')->Do(
SQL=> "DELETE FROM table WHERE id = 123",
);
2.1.1.2. SELECT
Note
Take care to use Limit as param and not in the SQL string because not all data-
bases support LIMIT in SQL strings.
$Kernel::OM->Get('Kernel::System::DB')->Prepare(
SQL => $SQL,
Limit => 15,
Bind => [ $Tn, $Group ],
);
Note
Use the Bind attribute where ever you can, especially for long statements. If you
use Bind you do not need the function Quote().
2.1.1.3. QUOTE
String:
16
Integer:
Number:
Note
Please use the Bind attribute instead of Quote() where ever you can.
2.1.2. XML
The XML interface should be used for INSERT, CREATE TABLE, DROP TABLE and ALTER
TABLE. As this syntax is different from database to database, using it makes sure that you
write applications that can be used in all of them.
Note
The <Insert> syntax has changed in >=2.2. Values are now used in the tag con-
tent (not longer in an attribute).
2.1.2.1. INSERT
<Insert Table="some_table">
<Data Key="id">1</Data>
<Data Key="description" Type="Quote">exploit</Data>
</Insert>
2.1.2.2. CREATE TABLE
Possible data types are: BIGINT, SMALLINT, INTEGER, VARCHAR (Size=1-1000000), DATE
(Format: yyyy-mm-dd hh:mm:ss) and LONGBLOB.
<TableCreate Name="calendar_event">
<Column Name="id" Required="true" PrimaryKey="true" AutoIncrement="true" Type="BIGINT"/>
<Column Name="title" Required="true" Size="250" Type="VARCHAR"/>
<Column Name="content" Required="false" Size="250" Type="VARCHAR"/>
<Column Name="start_time" Required="true" Type="DATE"/>
<Column Name="end_time" Required="true" Type="DATE"/>
<Column Name="owner_id" Required="true" Type="INTEGER"/>
<Column Name="event_status" Required="true" Size="50" Type="VARCHAR"/>
<Index Name="calendar_event_title">
<IndexColumn Name="title"/>
</Index>
<Unique Name="calendar_event_title">
<UniqueColumn Name="title"/>
</Unique>
<ForeignKey ForeignTable="users">
<Reference Local="owner_id" Foreign="id"/>
</ForeignKey>
</TableCreate>
LONGBLOB columns need special treatment. Their content needs to be Base64 transcoded
if the database driver does not support the feature DirectBlob. Please see the following
example:
17
my $Content = $StorableContent;
if ( !$DBObject->GetDatabaseFunction('DirectBlob') ) {
$Content = MIME::Base64::encode_base64($StorableContent);
}
Similarly, when reading from such a column, the content must not automatically be de-
coded as UTF-8 by passing the Encode => 0 flag to Prepare():
return if !$DBObject->Prepare(
SQL => '
SELECT content_type, content, content_id, content_alternative, disposition, filename
FROM article_data_mime_attachment
WHERE id = ?',
Bind => [ \$AttachmentID ],
Encode => [ 1, 0, 0, 0, 1, 1 ],
);
$Data{ContentType} = $Row[0];
2.1.2.3. DROP TABLE
<TableDrop Name="calendar_event"/>
2.1.2.4. ALTER TABLE
The following shows an example of add, change and drop columns.
<TableAlter Name="calendar_event">
<ColumnAdd Name="test_name" Type="varchar" Size="20" Required="true"/>
<ColumnDrop Name="test_title"/>
<IndexCreate Name="index_test3">
<IndexColumn Name="test3"/>
</IndexCreate>
<IndexDrop Name="index_test3"/>
<UniqueCreate Name="uniq_test3">
<UniqueColumn Name="test3"/>
</UniqueCreate>
<UniqueDrop Name="uniq_test3"/>
</TableAlter>
18
The next shows an example how to rename a table.
my @SQL = $Kernel::OM->Get('Kernel::System::DB')->SQLProcessor(
Database => \@XMLARRAY,
);
push(@SQL, $Kernel::OM->Get('Kernel::System::DB')->SQLProcessorPost());
for (@SQL) {
$Kernel::OM->Get('Kernel::System::DB')->Do(SQL => $_);
}
2.2. Database Drivers
The database drivers are located under $OTRS_HOME/Kernel/System/DB/*.pm.
2.3. Supported Databases
• MySQL
• PostgreSQL
• Oracle
• Microsoft SQL Server (only for external database connections, not as OTRS database)
3. Log Mechanism
3.1. System Log
OTRS comes with a system log backend that can be used for application logging and
debugging.
The log object can be accessed and used via the ObjectManager like this:
$Kernel::OM->Get('Kernel::System::Log')->Log(
Priority => 'error',
Message => 'Need something!',
);
Depending on the configured log level via MinimumLogLevel option in SysConfig, logged
message will either be saved or not, based on their Priority flag.
If error is set, just errors are logged. With debug, you get all logging messages. The order
of log levels is:
• debug
• info
• notice
19
• error
The output of the system log can be directed to either a syslog daemon or log file, de-
pending on the configured LogModule option in SysConfig.
3.2. Communication Log
In addition to System Log, OTRS provides specialized logging backend for any communi-
cation related logging. Since OTRS 6, system comes with dedicated tables and frontends
to track and display communication logs for easier debugging and operational overview.
To take advantage of the new system, first create a non-singleton instance of communi-
cation log object:
my $CommunicationLogObject = $Kernel::OM->Create(
'Kernel::System::CommunicationLog',
ObjectParams => {
Transport => 'Email', # Transport log module
Direction => 'Incoming', # Incoming|Outgoing
AccountType => 'POP3', # Mail account type
AccountID => 1, # Mail account ID
},
);
When you have a communication log object instance, you can start an object log for log-
ging individual messages. There are two object logs currently implemented: Connection
and Message.
Connection object log should be used for logging any connection related messages (for
example: authenticating on server or retrieving incoming messages).
Simply, start the object log by declaring its type, and you can use it immediately:
$CommunicationLogObject->ObjectLogStart(
ObjectLogType => 'Connection',
);
$CommunicationLogObject->ObjectLog(
ObjectLogType => 'Connection',
Priority => 'Debug', # Trace, Debug, Info, Notice,
Warning or Error
Key => 'Kernel::System::MailAccount::POP3',
Value => "Open connection to 'host.example.com' (user-1).",
);
The communication log object instance handles the current started object logs, so you
don't need to remember and bring them around everywhere, but it also means that you
can only start one object per type.
If you encounter an unrecoverable error, you can choose to close the object log and mark
it as failed:
$CommunicationLogObject->ObjectLog(
ObjectLogType => 'Connection',
Priority => 'Error',
Key => 'Kernel::System::MailAccount::POP3',
Value => 'Something went wrong!',
);
$CommunicationLogObject->ObjectLogStop(
ObjectLogType => 'Connection',
20
Status => 'Failed',
);
$CommunicationLogObject->CommunicationStop(
Status => 'Failed',
);
Otherwise, stop the object log and in turn communication log as success:
$CommunicationLogObject->ObjectLog(
ObjectLogType => 'Connection',
Priority => 'Debug',
Key => 'Kernel::System::MailAccount::POP3',
Value => "Connection to 'host.example.com' closed.",
);
$CommunicationLogObject->ObjectLogStop(
ObjectLogType => 'Connection',
Status => 'Successful',
);
$CommunicationLogObject->CommunicationStop(
Status => 'Successful',
);
Message object log should be used for any log entries regarding specific messages and
their processing. It is used in a similar way, just make sure to start it before using it:
$CommunicationLogObject->ObjectLogStart(
ObjectLogType => 'Message',
);
$CommunicationLogObject->ObjectLog(
ObjectLogType => 'Message',
Priority => 'Error',
Key => 'Kernel::System::MailAccount::POP3',
Value => "Could not process message. Raw mail saved (report it on http://
bugs.otrs.org/)!",
);
$CommunicationLogObject->ObjectLogStop(
ObjectLogType => 'Message',
Status => 'Failed',
);
$CommunicationLogObject->CommunicationStop(
Status => 'Failed',
);
You also have the possibility to link the log object and later lookup the communications
for a certain object type and ID:
$CommunicationLogObject->ObjectLookupSet(
ObjectLogType => 'Message',
TargetObjectType => 'Article',
TargetObjectID => 2,
);
my $LookupInfo = $CommunicationLogObject->ObjectLookupGet(
TargetObjectType => 'Article',
TargetObjectID => 2,
21
);
You should make sure to always stop communication and flag it as failed, if any log object
failed as well. This will allow administrators to see failed communications in the overview,
and take any action if needed.
It's important to preserve the communication log for duration of a single process. If your
work is spanning over multiple modules and any of them can benefit from logging, make
sure to pass the existing communication log instance around so all methods can use the
same one. With this approach, you will make sure any log entries spawned for the same
process are contained in a single communication.
If passing the communication log instance is not an option (async tasks!), you can also
choose to recreate the communication log object in the same state as in previous step.
Just get the communication ID and pass it to the new code, and then create the instance
with this parameter supplied:
You can then continue to use this instance as previously stated, start any object logs if
needed, adding entries and setting status in the end.
If you need to retrieve the communication log data or do something else with it, please
also take a look at Kernel::System::CommunicationLog::DB.pm
4.1. Introduction
Date and time are represented by an object of Kernel::System::DateTime. Every Date-
Time object holds its own date, time and time zone information. In contrast to the now
deprecated Kernel::System::Time package, this means that you can and should create
a DateTime object for every date/time you want to use.
my $DateTimeObject = $Kernel::OM->Create(
'Kernel::System::DateTime',
ObjectParams => {
TimeZone => 'Europe/Berlin'
},
22
);
The example above will create a DateTime object for the current date and time in time
zone Europe/Berlin. There are more options to create a DateTime object (time compo-
nents, string, time stamp, cloning), see POD of Kernel::System::DateTime.
Note
You will get an error if you try to retrieve a DateTime object via $Ker-
nel::OM->Get('Kernel::System::DateTime').
4.3. Time zones
Time offsets in hours (+2, -10, etc.) have been replaced by time zones (Europe/Berlin,
America/New_York, etc.). The conversion between time zones is completely encapsulated
within a DateTime object. If you want to convert to another time zone, simply use the
following code:
There is a new SysConfig option OTRSTimeZone. This setting defines the time zone that
OTRS uses internally to store date and time within the database.
Note
You have to ensure to convert a DateTime object to the OTRS time zone before
it gets stored in the database (there's a convenient method for this: ToOTRSTime-
Zone()). An exception could be that you explicitly want a database column to hold
a date/time in a specific time zone. But be aware that the database itself won't
provide time zone information by itself when retrieving it.
Note
TimeZoneList() of Kernel::System::DateTime provides a list of available time
zones.
4.4. Method summary
The Kernel::System::DateTime package provides the following methods (this is only a
selection, see source code for details).
4.4.2. Get method
With Get() all data of a DateTime object will be returned as a hash (date and time com-
ponents including day name, etc. as well as time zone).
4.4.3. Set method
With Set() you can either change certain components of the DateTime object (year,
month, day, hour, minute, second) or you can set a date and time based on a given string
('2016-05-24 23:04:12'). Note that you cannot change the time zone with this method.
23
4.4.4. Time zone methods
To change the time zone of a DateTime object use method ToTimeZone() or as a shortcut
for converting to OTRS time zone ToOTRSTimeZone().
To retrieve the configured OTRS time zone or user default time zone, always use method
OTRSTimeZoneGet() or UserDefaultTimeZoneGet(). Never retrieve these manually via
Kernel::Config.
5. Skins
Since OTRS 3.0, the visual appearance of OTRS is controlled by "skins".
A skin is a set of CSS and image files, which together control how the GUI is presented to
the user. Skins do not change the HTML content that is generated by OTRS (this is what
"Themes" do), but they control how it is displayed. With the help of modern CSS standards
it is possible to change the display thoroughly (e.g. repositioning parts of dialogs, hiding
elements, ...).
5.1. Skin Basics
All skins are in $OTRS_HOME/var/httpd/htdocs/skins/$SKIN_TYPE/$SKIN_NAME. There
are two types of skins: agent and customer skins.
24
Each of the agents can select individually, which of the installed agent skins they want
to "wear".
For the customer interface, a skin has to be selected globally with the config setting
Loader::Customer::SelectedSkin. All customers will see this skin.
Here it can clearly be seen that the default skin is loaded first, and then the custom
skin specified by the agent. In this example, we see the result of the activated loader
(Loader::Enabled set to 1), which gathers all CSS files, concatenates and minifies them
and serves them as one chunk to the browser. This saves bandwidth and also reduces the
number of HTTP requests. Let's see the same example with the Loader turned off:
Here we can better see the individual files that come from the skins.
There are different types of CSS files: common files which must always be loaded, and
"module-specific" files which are only loaded for special modules within the OTRS frame-
work.
In addition, it is possible to specify CSS files which only must be loaded on IE7 or IE8 (in
the case of the customer interface, also IE6). This is unfortunate, but it was not possible
to develop a modern GUI on these browsers without having special CSS for them.
For details regarding the CSS file types, please see the section on the Loader.
For each HTML page generation, the loader will first take all configured CSS files from the
default skin, and then for each file look if it is also available in a custom skin (if a custom
skin is selected) and load them after the default files.
25
That means a) that CSS files in custom skins need to have the same names as in the
default skins, and b) that a custom skin does not need to have all files of the default skin.
That is the big advantage of loading the default skin first: a custom skin has all default CSS
rules present and only needs to change those which should result in a different display.
That can often be done in a single file, like in the example above.
Another effect of this is that you need to be careful to overwrite all default CSS rules in
your custom skins that you want to change. Let's see an example:
.Header h1 {
font-weight: bold;
color: #000;
}
This defines special headings inside of the .Header element as bold, black text. Now if
you want to change that in your skin to another color and normal text, it is not enough
to write this:
.Header h1 {
color: #F00;
}
Because the original rule for font-weight still applies. You need to override it explicitly:
.Header h1 {
font-weight: normal;
color: #F00;
}
There are only three simple steps we need to take to achieve this goal:
Let's start by creating the files needed for our new skin. First of all, we need to create a
new folder for this skin (we'll call it custom). This folder will be $OTRS_HOME/var/httpd/
htdocs/skins/Agent/custom.
In there, we need to place the new CSS file in a new directory css which defines the new
skin's appearance. We'll call it Core.Default.css (remember that it must have the same
name as one of the files in the "default" skin). This is the code needed for the CSS file:
body {
background-color: #c0c0c0; /* not very beautiful but it meets our purpose */
}
Now follows the second step, adding a new logo and making the new skin known to
the OTRS system. For this, we first need to place our custom logo (e.g. logo.png) in
26
a new directory (e.g. img) in our skin directory. Then we need to create a new config
file $OTRS_HOME/Kernel/Config/Files/CustomSkin.xml, which will contain the needed
settings as follows:
To make this configuration active, we need to navigate to the SysConfig module in the
admin area of OTRS (alternatively, you can run the script $OTRS_HOME/bin/otrs.Con-
sole.pl Maint::Config::Rebuild). This will regenerate the Perl cache of the XML con-
figuration files, so that our new skin is now known and can be selected in the system.
To make it the default skin that new agents see before they made their own skin se-
lection, edit the SysConfig setting Loader::Agent::DefaultSelectedSkin and set it to
"custom".
In conclusion: to create a new skin in OTRS, we had to place the new logo file, and create
one CSS and one XML file, resulting in three new files:
$OTRS_HOME/Kernel/Config/Files/CustomSkin.xml
$OTRS_HOME/var/httpd/htdocs/skins/Agent/custom/img/custom-logo.png
$OTRS_HOME/var/httpd/htdocs/skins/Agent/custom/css/Core.Header.css
6.1. How it works
To put it simple, the Loader
27
• determines for each request precisely which CSS and JavaScript files are needed at the
client side by the current application module
• serves it to the client in only a few HTTP requests instead of many individual ones,
allowing the client to cache these snippets in the browser cache
• performs these tasks in a highly performing way, utilizing the caching mechanisms of
OTRS.
Of course, there is a little bit more detailed involved, but this should suffice as a first
overview.
6.2. Basic Operation
With the configuration settings Loader::Enabled::CSS and Loader::En-
abled::JavaScript, the loader can be turned on and off for CSS and JavaScript, respec-
tively (it is on by default).
Warning
Because of rendering problems in Internet Explorer, the Loader cannot be turned
off for CSS files for this client browser (config setting will be overridden). Up to
version 8, Internet Explorer cannot handle more than 32 CSS files on a page.
To learn about how the Loader works, please turn it off in your OTRS installation with the
aforementioned configuration settings. Now look at the source code of the application
module that you are currently using in this OTRS system (after a reload, of course). You
will see that there are many CSS files loaded in the <head> section of the page, and many
JavaScript files at the bottom of the page, just before the closing </body> element.
Having the content like this in many individual files with a readable formatting makes the
development much easier, and even possible at all. However, this has the disadvantage
of a large number of HTTP requests (network latency has a big effect) and unnecessary
content (whitespace and documentation) which needs to be transferred to the client.
The Loader solves this problem by performing the steps outlined in the short description
above. Please turn on the Loader again and reload your page now. Now you can see that
there are only very few CSS and JavaScript tags in the HTML code, like this:
What just happened? During the original request generating the HTML code for this page,
the Loader generated these two files (or took them from the cache) and put the shown
<script> tags on the page which link to these files, instead of linking to all relevant
JavaScript files separately (as you saw it without the loader being active).
<!--[if IE 7]>
28
<link rel="stylesheet" type="text/css" href="/otrs30-dev-web/skins/Agent/default/css-
cache/CommonCSS_IE7_59394a0516ce2e7359c255a06835d31f.css" />
<![endif]-->
<!--[if IE 8]>
<link rel="stylesheet" type="text/css" href="/otrs30-dev-web/skins/Agent/default/css-
cache/CommonCSS_IE8_ff58bd010ef0169703062b6001b13ca9.css" />
<![endif]-->
The reason is that Internet Explorer 7 and 8 need special treatment in addition to the
default CSS because of their lacking support of web standard technologies. So we have
some normal CSS that is loaded in all browsers, and some special CSS that is inside of
so-called "conditional comments" which cause it to be loaded only by Internet Explorer
7/8. All other browsers will ignore it.
Now we have outlined how the loader works. Let's look at how you can utilize that in
your own OTRS extensions by adding configuration data to the loader, telling it to load
additional or alternative CSS or JavaScript content.
6.3.1. Common JavaScript
The list of JavaScript files to be loaded is configured in the configuration settings
Loader::Agent::CommonJS (for the agent interface) and Loader::Customer::CommonJS
(for the customer interface).
These settings are designed as hashes, so that OTRS extensions can add their own hash
keys for additional content to be loaded. Let's look at an example:
...
<Item>Core.App.js</Item>
<Item>Core.Agent.js</Item>
<Item>Core.Agent.Search.js</Item>
</Array>
</Setting>
</ConfigItem>
This is the list of JavaScript files which always need to be loaded for the agent interface
of OTRS.
To add new content which is supposed to be loaded always in the agent interface, just
add an XML configuration file with another hash entry:
29
<Description Translatable="1">List of JS files to always be loaded for the agent
interface for package "CustomPackage".</Description>
<Group>Framework</Group>
<SubGroup>Core::Web</SubGroup>
<Setting>
<Array>
<Item>CustomPackage.App.js</Item>
</Array>
</Setting>
</ConfigItem>
6.3.2. Module-Specific JavaScript
Not all JavaScript is usable for all application modules of OTRS. Therefore it is possible
to specify module-specific JavaScript files. Whenever a certain module is used (such as
AgentDashboard), the module-specific JavaScript for this module will also be loaded. The
configuration is done in the frontend module registration in the XML configurations. Again,
an example:
It is possible to put a <Loader> tag in the frontend module registrations which may contain
<JavaScript> tags, one for each file that is supposed to be loaded for this application
module.
Now you have all information you need to configure the way the Loader handles JavaScript
code.
There is one special case: for ToolbarModules, you can also add custom JavaScript files.
Just add a JavaScript attribute to the configuration like this:
30
<Description Translatable="1">Toolbar Item for a shortcut.</Description>
<Group>Ticket</Group>
<SubGroup>Frontend::Agent::ToolBarModule</SubGroup>
<Setting>
<Hash>
<Item Key="Module">Kernel::Output::HTML::ToolBarLink</Item>
<Item Key="Name">New email ticket</Item>
<Item Key="Priority">1009999</Item>
<Item Key="Link">Action=AgentTicketEmail</Item>
<Item Key="Action">AgentTicketEmail</Item>
<Item Key="AccessKey">l</Item>
<Item Key="CssClass">EmailTicket</Item>
<Item Key="JavaScript">OTRS.Agent.CustomToolbarModule.js</Item>
</Hash>
</Setting>
</ConfigItem>
6.4.1. Common CSS
The way common CSS is handled is very similar to the way common JavaScript is
loaded. Here, the configuration settings are called Loader::Agent::CommonCSS and
Loader::Customer::CommonCSS, respectively.
However, as we already noted above, Internet Explorer 7 and 8 (and for the
customer interface also 6) need special treatment. That's why there are spe-
cial configuration settings for them, to specify common CSS which should only
be loaded in these browsers. The respective settings are Loader::Agent::Common-
CSS::IE7, Loader::Agent::CommonCSS::IE8, Loader::Customer::CommonCSS::IE6,
Loader::Customer::CommonCSS::IE7 and Loader::Customer::CommonCSS::IE8.
An example:
This is the list of common CSS files for the agent interface which should only be loaded
in Internet Explorer 8.
6.4.2. Module-Specific CSS
Module-specific CSS is handled very similar to the way module-specific JavaScript is han-
dled. It is also configured in the frontend module registrations. Example:
31
<SubGroup>Frontend::Admin::ModuleRegistration</SubGroup>
<Setting>
<FrontendModuleReg>
<Group>admin</Group>
<Description>Admin-Area</Description>
<Title></Title>
<NavBarName>Admin</NavBarName>
<NavBar>
<Type>Menu</Type>
<Description Translatable="1"></Description>
<Block>ItemArea</Block>
<Name Translatable="1">Admin</Name>
<Link>Action=Admin</Link>
<NavBar>Admin</NavBar>
<AccessKey>a</AccessKey>
<Prio>10000</Prio>
</NavBar>
<NavBarModule>
<Module>Kernel::Output::HTML::NavBarModuleAdmin</Module>
</NavBarModule>
<Loader>
<CSS>Core.Agent.Admin.css</CSS>
<CSS_IE7>Core.Agent.AdminIE7.css</CSS_IE7>
<JavaScript>Core.Agent.Admin.SysConfig.js</JavaScript>
</Loader>
</FrontendModuleReg>
</Setting>
</ConfigItem>
Here we have a module (the admin overview page of the agent interface) which has
special JavaScript, normal CSS (tagname <CSS>) and special CSS for Internet Explorer 7
(tagname <CSS_IE7>). All of these need to be loaded in addition to the common JavaScript
and CSS defined for the agent interface.
There is one special case: for ToolbarModules, you can also add custom CSS files. Just
add a CSS, CSS_IE7 or CSS_IE8 attribute to the configuration like this:
7. Templating Mechanism
Internally, OTRS uses a templating mechanism to dynamically generate its HTML pages
(and other content), while keeping the program logic (Perl) and the presentation (HTML)
32
separate. Typically, a frontend module will use an own template file, pass some data to
it and return the rendered result to the user.
OTRS relies on the Template::Toolkit rendering engine. The full Template::Toolkit syntax
can be used in OTRS templates. This section describes some example use cases and OTRS
extensions to the Template::Toolkit syntax.
7.1. Template Commands
7.1.1. Inserting dynamic data
In templates, dynamic data must be inserted, quoted etc. This section lists the relevant
commands to do that.
7.1.1.1. [% Data.Name %]
If data parameters are given to the templates by the application module, these data can
be output to the template. [% Data.Name %] is the most simple, but also most dangerous
one. It will insert the data parameter whose name is Name into the template as it is, without
further processing.
Warning
Because of the missing HTML quoting, this can result in security problems. Never
output data that was input by a user without quoting in HTML context. The user
could - for example - just insert a <script> tag, and it would be output on the
HTML page generated by OTRS.
If you have data entries with complex names containing special characters, you cannot
use the dot (.) notation to access this data. Use item() instead: [% Data.item('Com-
plex-name') %].
It's also possible specify a maximum length for the value. If, for example, you just want
to show 8 characters of a variable (result will be "SomeName[...]"), use the following:
The first 20 characters of the author's name: [% Data.Name | truncate(20) | html %].
33
7.1.1.3. [% Data.Name | uri %]
This command performs URL encoding on the data as it is inserted to the template. This
should be used to output single parameter names or values of URLs, to prevent security
problems. It cannot be used for complete URLs because it will also mask =, for example.
Please note that the filter notation will only work for simple strings. To output complex
data as JSON, please use it as a function:
7.1.1.5. [% Env() %]
Inserts environment variables provided by the LayoutObject. Some examples:
Warning
Because of the missing HTML quoting, this can result in security problems. Never
output data that was input by a user without quoting in HTML context. The user
could - for example - just insert a <script> tag, and it would be output on the
HTML page generated by OTRS.
7.1.1.6. [% Config() %]
Inserts config variables into the template. Let's see an example Kernel/Config.pm:
[Kernel/Config.pm]
# FQDN
# (Full qualified domain name of your system.)
34
$Self->{FQDN} = 'otrs.example.com';
# AdminEmail
# (Email of the system admin.)
$Self->{AdminEmail} = 'admin@example.com';
[...]
Warning
Because of the missing HTML quoting, this can result in security problems.
7.1.2. Localization Commands
7.1.2.1. [% Translate() %]
Translates a string into the current user's selected language. If no translation is found,
the original string will be used.
You can also specify one or more parameters (%s) inside of the string which should be
replaced with dynamic data:
Translate this text and insert the given data: [% Translate("Change %s settings", Data.Type)
| html %]
Strings in JavaScript can be translated and processed with the JSON filter.
7.1.2.2. [% Localize() %]
Outputs data according to the current language/locale.
In different cultural areas, different convention for date and time formatting are used.
For example, what is the 01.02.2010 in Germany, would be 02/01/2010 in the USA. [%
Localize() %] abstracts this away from the templates. Let's see an example:
[% Data.CreateTime ǀ Localize("TimeLong") %]
# Result for US English locale:
06/09/2010 15:45:41
35
First, the data is inserted from the application module with Data. Here always an ISO UTC
timestamp (2010-06-09 15:45:41) must be passed as data to [% Localize() %]. Then
it will be output it according to the date/time definition of the current locale.
The data passed to [% Localize() %] must be UTC. If a time zone offset is specified for
the current agent, it will be applied to the UTC timestamp before the output is generated.
There are different possible date/time output formats: TimeLong (full date/time),
TimeShort (no seconds) and Date (no time).
[% Data.CreateTime ǀ Localize("TimeLong") %]
# Result for US English locale:
06/09/2010 15:45:41
[% Data.CreateTime ǀ Localize("TimeShort") %]
# Result for US English locale:
06/09/2010 15:45
[% Data.CreateTime ǀ Localize("Date") %]
# Result for US English locale:
06/09/2010
Also the output of human readable file sizes is available as an option Localize('File-
size') (just pass the raw file size in bytes).
[% Data.Filesize ǀ Localize("Filesize") %]
# Result for US English locale:
23 MB
7.1.2.3. [% ReplacePlaceholders() %]
Replaces placeholders (%s) in strings with passed parameters.
In certain cases, you might want to insert HTML code in translations, instead of placehold-
ers. On the other hand, you also need to take care of sanitization, since translated strings
should not be trusted as-is. For this, you can first translate the string, pass it through the
HTML filter and finally replace placeholders with static (safe) HTML code.
You can also use [% ReplacePlaceholders() %] in function format, in case you are
not translating anything. In this case, first parameter is the target string, and any found
placeholders in it are substituted with subsequent parameters.
36
# this section is temporarily disabled
# <div class="AsBlock">
# <a href="...">link</a>
# </div>
7.1.3.2. [% InsertTemplate("Copyright.tt") %]
Warning
Please note that the InsertTemplate command was added to provide better back-
wards compatibility to the old DTL system. It might be deprecated in a future ver-
sion of OTRS and removed later. If you don't use block commands in your included
template, you don't need InsertTemplate and can use standard Template::Toolkit
syntax like INCLUDE/PROCESS instead.
Includes another template file into the current one. The included file may also contain
template commands.
# include Copyright.tt
[% InsertTemplate("Copyright") %]
Please note this is not the same as Template::Toolkit's [% INCLUDE %] command, which
just processes the referenced template. [% InsertTemplate() %] actually adds the con-
tent of the referenced template into the current template, so that it can be processed
together. That makes it possible for the embedded template to access the same environ-
ment/data as the main template.
Warning
Please note that the blocks commands were added to provide better backwards
compatibility to the old DTL system. They might be deprecated in a future version
of OTRS and removed later. We advise you to develop any new code without using
the blocks commands. You can use standard Template::Toolkit syntax like IF/ELSE,
LOOPs and other helpful things for conditional template output.
With this command, one can specify parts of a template file as a block. This block needs to
be explicitly filled with a function call from the application, to be present in the generated
output. The application can call the block 0 (it will not be present in the output), 1 or more
times (each with possibly a different set of data parameters passed to the template).
One common use case is the filling of a table with dynamic data:
<table class="DataTable">
<thead>
<tr>
<th>[% Translate("Name") | html %]</th>
<th>[% Translate("Type") | html %]</th>
<th>[% Translate("Comment") | html %]</th>
<th>[% Translate("Validity") | html %]</th>
<th>[% Translate("Changed") | html %]</th>
<th>[% Translate("Created") | html %]</th>
</tr>
</thead>
<tbody>
[% RenderBlockStart("NoDataFoundMsg") %]
<tr>
<td colspan="6">
[% Translate("No data found.") | html %]
37
</td>
</tr>
[% RenderBlockEnd("NoDataFoundMsg") %]
[% RenderBlockStart("OverviewResultRow") %]
<tr>
<td><a class="AsBlock" href="[% Env("Baselink") %]Action=[% Env("Action")
%];Subaction=Change;ID=[% Data.ID | uri %]">[% Data.Name | html %]</a></td>
<td>[% Translate(Data.TypeName) | html %]</td>
<td title="[% Data.Comment | html %]">[% Data.Comment | truncate(20) | html %]</
td>
<td>[% Translate(Data.Valid) | html %]</td>
<td>[% Data.ChangeTime | Localize("TimeShort") %]</td>
<td>[% Data.CreateTime | Localize("TimeShort") %]</td>
</tr>
[% RenderBlockEnd("OverviewResultRow") %]
</tbody>
</table>
The surrounding table with the header is always generated. If no data was found, the block
NoDataFoundMsg is called once, resulting in a table with one data row with the message
"No data found."
If data was found, for each row there is one function call made for the block OverViewRe-
sultRow (each time passing in the data for this particular row), resulting in a table with
as many data rows as results were found.
Let's look at how the blocks are called from the application module:
my %List = $Kernel::OM->Get('Kernel::System::State)->StateList(
UserID => 1,
Valid => 0,
);
Note how the blocks have both their name and an optional set of data passed in as sepa-
rate parameters to the block function call. Data inserting commands inside a block always
need the data provided to the block function call of this block, not the general template
rendering call.
38
7.1.4. [% WRAPPER JSOnDocumentComplete %]...[% END %]
Marks JavaScript code which should be executed after all CSS, JavaScript and other ex-
ternal content has been loaded and the basic JavaScript initialization was finished. Again,
let's look at an example:
...
<div class="Content">
<fieldset class="TableLike FixedLabel">
<label class="Mandatory" for="DestQueueID"><span class="Marker">*</span> [%
Translate("New Queue") | html %]:</label>
<div class="Field">
[% Data.MoveQueuesStrg %]
<div id="DestQueueIDError" class="TooltipErrorMessage" ><p>[%
Translate("This field is required.") | html %]</p></div>
<div id="DestQueueIDServerError" class="TooltipErrorMessage"><p>[%
Translate("This field is required.") | html %]</p></div>
[% WRAPPER JSOnDocumentComplete %]
<script type="text/javascript">
$('#DestQueueID').bind('change', function (Event) {
$('#NoSubmit').val('1');
Core.AJAX.FormUpdate($('#MoveTicketToQueue'), 'AJAXUpdate', 'DestQueueID',
['NewUserID', 'OldUserID', 'NewStateID', 'NewPriorityID' [% Data.DynamicFieldNamesStrg
%]]);
});
</script>
[% END %]
</div>
<div class="Clear"></div>
This snippet creates a small form and puts an onchange handler on the <select> element
which triggers an AJAX based form update.
Inside the [% WRAPPER JSOnDocumentComplete %]...[% END %] block, you can use
<script> tags to enclose your JavaScript code, but you do not have to do so. It may be
beneficial because it will enable correct syntax highlighting in IDEs which support it.
# render AdminState.tt
$Output .= $Kernel::OM->Get('Kernel::Output::HTML::Layout')->Output(
TemplateFile => 'AdminState',
Data => \%Param,
);
39
In the frontend modules, the Output() function of Kernel::Output::HTML::Layout is
called (after all the needed blocks have been called in this template) to generate the final
output. An optional set of data parameters is passed to the template, for all data inserting
commands which are not inside of a block.
As an example, perform the following steps to create a new theme called "Company":
Important
Only copy over the files you're planning to change. OTRS will automatically get
the missing files from the Standard theme. This will make upgrading at a later
stage much easier.
Now the new theme should be usable. You can select it via your personal preferences.
Warning
Do not change the theme files shipped with OTRS, since these changes will be lost
after an update. Create your own themes only by performing the steps described
above.
package MyPackage;
use strict;
use warnings;
40
use Kernel::Language (qw(Translatable));
...
In Template files, all literal strings enclosed in the Translate()-Tag are automatically
marked for extraction: [% Translate('My string %s', Data.Data )%].
In SysConfig and Database XML files you can mark strings for extraction with the Trans-
latable="1" attribute.
# Database XML
<Insert Table="groups">
<Data Key="id" Type="AutoIncrement">1</Data>
...
<Data Key="comments" Type="Quote" Translatable="1">Group for default access.</Data>
...
</Insert>
# SysConfig XML
<Setting>
<Option SelectedID="0">
<Item Key="0" Translatable="1">No</Item>
<Item Key="1" Translatable="1">Yes</Item>
</Option>
</Setting>
For the OTRS framework and all extension modules that also use Transifex for managing
the translations, .pot and .po files are written. These files are used to push the translatable
strings to Transifex and pull the translations from there.
But OTRS requires the translations to be in Perl files for speed reasons. These files will
also be generated by otrs.Console.pl Dev::Tools::TranslationsUpdate. There are
two different translation cache file types which are used in the following order. If a word/
sentence is redefined in a translation file, the last definition will be used.
Kernel/Language/$Language.pm
Kernel/Language/$Language_Custom.pm
Format:
package Kernel::Language::de;
41
use strict;
use warnings;
sub Data {
my $Self = shift;
# $$START$$
# possible charsets
$Self->{Charset} = ['iso-8859-1', 'iso-8859-15', ];
# date formats (%A=WeekDay;%B=LongMonth;%T=Time;%D=Day;%M=Month;%Y=Jear;)
$Self->{DateFormat} = '%D.%M.%Y %T';
$Self->{DateFormatLong} = '%A %D %B %T %Y';
$Self->{DateFormatShort} = '%D.%M.%Y';
$Self->{DateInputFormat} = '%D.%M.%Y';
$Self->{DateInputFormatLong} = '%D.%M.%Y - %T';
$Self->{Translation} = {
# Template: AAABase
'Yes' => 'Ja',
'No' => 'Nein',
'yes' => 'ja',
'no' => 'kein',
'Off' => 'Aus',
'off' => 'aus',
};
# $$STOP$$
return 1;
}
1;
Format:
package Kernel::Language::xx_Custom;
use strict;
use warnings;
sub Data {
my $Self = shift;
# $$START$$
# own translations
$Self->{Translation}->{'Lock'} = 'Lala';
$Self->{Translation}->{'Unlock'} = 'Lulu';
# $$STOP$$
return 1;
}
1;
42
9.4. Using the translated data from the code
You can use the method $LanguageObject->Translate() to translate strings at runtime
from Perl code, and the Translate()-Tag in templates.
43
Chapter 3. How to Extend OTRS
1. Writing a new OTRS frontend mod-
ule
In this chapter, the writing of a new OTRS module is illustrated on the basis of a simple
small program. Necessary prerequisite is an OTRS development environment as specified
in the chapter of the same name.
Kernel
Kernel/System
Kernel/Modules
Kernel/Output/HTML/Templates/Standard
Kernel/Config
Kernel/Config/Files
Kernel/Language
44
1.3. Frontend Module
After creating the links and executing the Sysconfig, a new module with the name 'Hel-
loWorld' is displayed. When calling it up, an error message is displayed as OTRS cannot
find the matching frontend module yet. This is the next thing to be created. To do so, we
create the following file:
# --
# Kernel/Modules/AgentHelloWorld.pm - frontend module
# Copyright (C) (year) (name of author) (email of author)
# --
# This software comes with ABSOLUTELY NO WARRANTY. For details, see
# the enclosed file COPYING for license information (AGPL). If you
# did not receive this file, see http://www.gnu.org/licenses/agpl.txt.
# --
package Kernel::Modules::AgentHelloWorld;
use strict;
use warnings;
sub new {
my ( $Type, %Param ) = @_;
return $Self;
}
sub Run {
my ( $Self, %Param ) = @_;
my %Data = ();
my $HelloWorldObject = $Kernel::OM->Get('Kernel::System::HelloWorld');
my $LayoutObject = $Kernel::OM->Get('Kernel::Output::HTML::Layout');
$Data{HelloWorldText} = $HelloWorldObject->GetHelloWorldText();
# build output
my $Output = $LayoutObject->Header(Title => "HelloWorld");
$Output .= $LayoutObject->NavigationBar();
$Output .= $LayoutObject->Output(
Data => \%Data,
TemplateFile => 'AgentHelloWorld',
);
$Output .= $LayoutObject->Footer();
return $Output;
}
1;
1.4. Core Module
Next, we create the file for the core module /HelloWorld/Kernel/System/Hel-
loWorld.pm with the following content:
# --
# Kernel/System/HelloWorld.pm - core module
# Copyright (C) (year) (name of author) (email of author)
# --
45
# This software comes with ABSOLUTELY NO WARRANTY. For details, see
# the enclosed file COPYING for license information (AGPL). If you
# did not receive this file, see http://www.gnu.org/licenses/agpl.txt.
# --
package Kernel::System::HelloWorld;
use strict;
use warnings;
=head1 NAME
=head1 DESCRIPTION
Little OTRS module that displays the text 'Hello World' when called up.
=head2 new()
my $HelloWorldObject = $Kernel::OM->Get('Kernel::System::HelloWorld');
=cut
sub new {
my ( $Type, %Param ) = @_;
return $Self;
}
=head2 GetHelloWorldText()
my $HelloWorldText = $HelloWorldObject->GetHelloWorldText();
=cut
sub GetHelloWorldText {
my ( $Self, %Param ) = @_;
my $HelloWorld = $Self->_FormatHelloWorldText(
String => 'Hello World',
);
return $HelloWorld;
}
=begin Internal:
=cut
=head2 _FormatHelloWorldText()
my $HelloWorld = $Self->_FormatHelloWorldText(
String => 'Hello World',
46
);
sub _FormatHelloWorldText{
my ( $Self, %Param ) = @_;
my $HelloWorld = uc $Param{String};
return $HelloWorld;
=end Internal:
1;
1.5. Template File
The last thing missing before the new module can run is the relevant HTML template.
Thus, we create the following file:
# --
# Kernel/Output/HTML/Templates/Standard/AgentHelloWorld.tt - overview
# Copyright (C) (year) (name of author) (email of author)
# --
# This software comes with ABSOLUTELY NO WARRANTY. For details, see
# the enclosed file COPYING for license information (AGPL). If you
# did not receive this file, see http://www.gnu.org/licenses/agpl.txt.
# --
<h1>[% Translate("Overview") | html %]: [% Translate("HelloWorld") %]</h1>
<p>
[% Data.HelloWorldText | Translate() | html %]
</p>
The module is working now and displays the text 'Hello World' when called.
1.6. Language File
If the text 'Hello World!' is to be translated into for instance German, you can cre-
ate a translation file for this language in HelloWorld/Kernel/Language/de_AgentHel-
loWorld.pm. Example:
package Kernel::Language::de_AgentHelloWorld;
use strict;
use warnings;
sub Data {
my $Self = shift;
return 1;
}
1;
1.7. Summary
The example given above shows that it is not too difficult to write a new module for OTRS.
It is important, though, to make sure that the module and file name are unique and thus do
not interfere with the framework or other expansion modules. When a module is finished,
an OPM package must be generated from it (see chapter Package Building).
47
2. Using the power of the OTRS mod-
ule layers
OTRS has a large number of so-called "module layers" which make it very easy to extend
the system without patching existing code. One example is the number generation mech-
anism for tickets. It is a "module layer" with pluggable modules, and you can add your
own custom number generator modules if you wish to do so. Let's look at the different
layers in detail!
2.1.1.1. Code Example
The interface class is called Kernel::System::Auth. The example agent authentication
may be called Kernel::System::Auth::CustomAuth. You can find an example below.
# --
# Kernel/System/Auth/CustomAuth.pm - provides the CustomAuth authentication
# based on Martin Edenhofer's Kernel::System::Auth::DB
# Copyright (C) 2001-2017 OTRS AG, http://otrs.com/
# --
# ID: CustomAuth.pm,v 1.1 2010/05/10 15:30:34 fk Exp $
# --
# This software comes with ABSOLUTELY NO WARRANTY. For details, see
# the enclosed file COPYING for license information (AGPL). If you
# did not receive this file, see http://www.gnu.org/licenses/agpl.txt.
# --
package Kernel::System::Auth::CustomAuth;
use strict;
use warnings;
use Authen::CustomAuth;
sub new {
my ( $Type, %Param ) = @_;
# get config
$Self->{Die} = $Self->{ConfigObject}->Get( 'AuthModule::CustomAuth::Die' .
$Param{Count} );
48
# get user table
$Self->{CustomAuthHost} = $Self->{ConfigObject}->Get( 'AuthModule::CustomAuth::Host' .
$Param{Count} )
|| die "Need AuthModule::CustomAuth::Host$Param{Count}.";
$Self->{CustomAuthSecret}
= $Self->{ConfigObject}->Get( 'AuthModule::CustomAuth::Password' . $Param{Count} )
|| die "Need AuthModule::CustomAuth::Password$Param{Count}.";
return $Self;
}
sub GetOption {
my ( $Self, %Param ) = @_;
# module options
my %Option = ( PreAuth => 0, );
# return option
return $Option{ $Param{What} };
}
sub Auth {
my ( $Self, %Param ) = @_;
# get params
my $User = $Param{User} || '';
my $Pw = $Param{Pw} || '';
my $RemoteAddr = $ENV{REMOTE_ADDR} || 'Got no REMOTE_ADDR env!';
my $UserID = '';
my $GetPw = '';
# just a note
if ( !$User ) {
$Self->{LogObject}->Log(
Priority => 'notice',
Message => "No User given!!! (REMOTE_ADDR: $RemoteAddr)",
);
return;
}
# just a note
if ( !$Pw ) {
$Self->{LogObject}->Log(
Priority => 'notice',
Message => "User: $User authentication without Pw!!! (REMOTE_ADDR:
$RemoteAddr)",
);
return;
}
49
my $CustomAuth = Authen::CustomAuth->new(
Host => $Self->{CustomAuthHost},
Secret => $Self->{CustomAuthecret},
);
if ( !$CustomAuth ) {
if ( $Self->{Die} ) {
die "Can't connect to $Self->{CustomAuthHost}: $@";
}
else {
$Self->{LogObject}->Log(
Priority => 'error',
Message => "Can't connect to $Self->{CustomAuthHost}: $@",
);
return;
}
}
my $AuthResult = $CustomAuth->check_pwd( $User, $Pw );
# login note
if ( defined($AuthResult) && $AuthResult == 1 ) {
$Self->{LogObject}->Log(
Priority => 'notice',
Message => "User: $User authentication ok (REMOTE_ADDR: $RemoteAddr).",
);
return $User;
}
# just a note
else {
$Self->{LogObject}->Log(
Priority => 'notice',
Message => "User: $User authentication with wrong Pw!!! (REMOTE_ADDR:
$RemoteAddr)"
);
return;
}
}
1;
2.1.1.2. Configuration Example
There is the need to activate your custom agent authenticate module. This can be done
using the Perl configuration below. It is not recommended to use the XML configuration
because you can lock you out via the sysconfig.
$Self->{'AuthModule'} = 'Kernel::System::Auth::CustomAuth';
2.1.1.4. Release Availability
Name Release
DB 1.0
HTTPBasicAuth 1.2
LDAP 1.0
RADIUS 1.3
50
2.1.2. Authentication Synchronization Module
There is an LDAP authentication synchronization module which come with the OTRS
framework. It is also possible to develop your own authentication modules. The authenti-
cation synchronization modules are located under Kernel/System/Auth/Sync/*.pm. For
more information about their configuration see the admin manual. Following, there is
an example of an authentication synchronization module. Save it under Kernel/Sys-
tem/Auth/Sync/CustomAuthSync.pm. You just need 2 functions: new() and Sync(). Re-
turn 1, then the synchronization is ok.
2.1.2.1. Code Example
The interface class is called Kernel::System::Auth. The example agent authentication
may be called Kernel::System::Auth::Sync::CustomAuthSync. You can find an exam-
ple below.
# --
# Kernel/System/Auth/Sync/CustomAuthSync.pm - provides the CustomAuthSync
# Copyright (C) 2001-2017 OTRS AG, http://otrs.com/
# --
# Id: CustomAuthSync.pm,v 1.9 2010/03/25 14:42:45 martin Exp $
# --
# This software comes with ABSOLUTELY NO WARRANTY. For details, see
# the enclosed file COPYING for license information (AGPL). If you
# did not receive this file, see http://www.gnu.org/licenses/agpl.txt.
# --
package Kernel::System::Auth::Sync::CustomAuthSync;
use strict;
use warnings;
use Net::LDAP;
sub new {
my ( $Type, %Param ) = @_;
...
return $Self;
}
sub Sync {
my ( $Self, %Param ) = @_;
51
2.1.2.2. Configuration Example
You should activate your custom synchronization module. This can be done using the Perl
configuration below. It is not recommended to use the XML configuration because this
would allow you to lock yourself out via SysConfig.
$Self->{'AuthSyncModule'} = 'Kernel::System::Auth::Sync::LDAP';
2.1.2.4. Release Availability
Name Release
LDAP 2.4
2.1.3.1. Code Example
The interface class is called Kernel::System::CustomerAuth. The example customer
authentication may be called Kernel::System::CustomerAuth::CustomAuth. You can
find an example below.
# --
# Kernel/System/CustomerAuth/CustomAuth.pm - provides the custom Authentication
# based on Martin Edenhofer's Kernel::System::Auth::DB
# Copyright (C) 2001-2017 OTRS AG, http://otrs.com/
# --
# Id: CustomAuth.pm,v 1.11 2009/09/22 15:16:05 mb Exp $
# --
# This software comes with ABSOLUTELY NO WARRANTY. For details, see
# the enclosed file COPYING for license information (AGPL). If you
# did not receive this file, see http://www.gnu.org/licenses/agpl.txt.
# --
package Kernel::System::CustomerAuth::CustomAuth;
use strict;
use warnings;
use Authen::CustomAuth;
sub new {
my ( $Type, %Param ) = @_;
52
bless( $Self, $Type );
# get config
$Self->{Die}
= $Self->{ConfigObject}->Get( 'Customer::AuthModule::CustomAuth::Die' .
$Param{Count} );
return $Self;
}
sub GetOption {
my ( $Self, %Param ) = @_;
# module options
my %Option = ( PreAuth => 0, );
# return option
return $Option{ $Param{What} };
}
sub Auth {
my ( $Self, %Param ) = @_;
# get params
my $User = $Param{User} || '';
my $Pw = $Param{Pw} || '';
my $RemoteAddr = $ENV{REMOTE_ADDR} || 'Got no REMOTE_ADDR env!';
my $UserID = '';
my $GetPw = '';
# just a note
53
if ( !$User ) {
$Self->{LogObject}->Log(
Priority => 'notice',
Message => "No User given!!! (REMOTE_ADDR: $RemoteAddr)",
);
return;
}
# just a note
if ( !$Pw ) {
$Self->{LogObject}->Log(
Priority => 'notice',
Message => "User: $User Authentication without Pw!!! (REMOTE_ADDR:
$RemoteAddr)",
);
return;
}
# login note
if ( defined($AuthResult) && $AuthResult == 1 ) {
$Self->{LogObject}->Log(
Priority => 'notice',
Message => "User: $User Authentication ok (REMOTE_ADDR: $RemoteAddr).",
);
return $User;
}
# just a note
else {
$Self->{LogObject}->Log(
Priority => 'notice',
Message => "User: $User Authentication with wrong Pw!!! (REMOTE_ADDR:
$RemoteAddr)"
);
return;
}
}
1;
2.1.3.2. Configuration Example
There is the need to activate your custom customer authenticate module. This can be
done using the XML configuration below.
54
<Setting>
<Option Location="Kernel/System/CustomerAuth/*.pm"
SelectedID="Kernel::System::CustomerAuth::CustomAuth"></Option>
</Setting>
</ConfigItem>
2.1.3.4. Release Availability
Name Release
DB 1.0
HTTPBasicAuth 1.2
LDAP 1.0
RADIUS 1.3
2.2. Preferences
2.2.1. Customer User Preferences Module
There is a DB customer-user preferences module which come with the OTRS frame-
work. It is also possible to develop your own customer-user preferences mod-
ules. The customer-user preferences modules are located under Kernel/System/Cus-
tomerUser/Preferences/*.pm. For more information about their configuration see the
admin manual. Following, there is an example of a customer-user preferences module.
Save it under Kernel/System/CustomerUser/Preferences/Custom.pm. You just need 4
functions: new(), SearchPreferences(), SetPreferences() and GetPreferences().
2.2.1.1. Code Example
The interface class is called Kernel::System::CustomerUser. The example cus-
tomer-user preferences may be called Kernel::System::CustomerUser::Prefer-
ences::Custom. You can find an example below.
# --
# Kernel/System/CustomerUser/Preferences/Custom.pm - some customer user functions
# Copyright (C) 2001-2017 OTRS AG, http://otrs.com/
# --
# Id: Custom.pm,v 1.20 2009/10/07 20:41:50 martin Exp $
# --
# This software comes with ABSOLUTELY NO WARRANTY. For details, see
# the enclosed file COPYING for license information (AGPL). If you
# did not receive this file, see http://www.gnu.org/licenses/agpl.txt.
# --
package Kernel::System::CustomerUser::Preferences::Custom;
use strict;
use warnings;
sub new {
my ( $Type, %Param ) = @_;
55
for my $Object (qw(DBObject ConfigObject LogObject)) {
$Self->{$Object} = $Param{$Object} || die "Got no $Object!";
}
return $Self;
}
sub SetPreferences {
my ( $Self, %Param ) = @_;
$Value .= 'Custom';
return 1;
}
sub GetPreferences {
my ( $Self, %Param ) = @_;
# get preferences
return if !$Self->{DBObject}->Prepare(
SQL => "SELECT $Self->{PreferencesTableKey}, $Self->{PreferencesTableValue} "
. " FROM $Self->{PreferencesTable} WHERE $Self->{PreferencesTableUserID} = ?",
Bind => [ \$UserID ],
);
while ( my @Row = $Self->{DBObject}->FetchrowArray() ) {
$Data{ $Row[0] } = $Row[1];
}
# return data
return %Data;
}
sub SearchPreferences {
my ( $Self, %Param ) = @_;
56
my %UserID;
my $Key = $Param{Key} || '';
my $Value = $Param{Value} || '';
# get preferences
my $SQL = "SELECT $Self->{PreferencesTableUserID}, $Self->{PreferencesTableValue} "
. " FROM "
. " $Self->{PreferencesTable} "
. " WHERE "
. " $Self->{PreferencesTableKey} = '"
. $Self->{DBObject}->Quote($Key) . "'" . " AND "
. " LOWER($Self->{PreferencesTableValue}) LIKE LOWER('"
. $Self->{DBObject}->Quote( $Value, 'Like' ) . "')";
# return data
return %UserID;
}
1;
2.2.1.2. Configuration Example
There is the need to activate your custom customer-user preferences module. This can
be done using the XML configuration below.
2.2.1.4. Release Availability
Name Release
DB 2.3
57
ules are located under Kernel/System/Queue/*.pm. For more information about their
configuration see the admin manual. Following, there is an example of a queue pref-
erences module. Save it under Kernel/System/Queue/PreferencesCustom.pm. You just
need 3 functions: new(), QueuePreferencesSet() and QueuePreferencesGet(). Return
1, then the synchronization is ok.
2.2.2.1. Code Example
The interface class is called Kernel::System::Queue. The example queue preferences
may be called Kernel::System::Queue::PreferencesCustom. You can find an example
below.
# --
# Kernel/System/Queue/PreferencesCustom.pm - some user functions
# Copyright (C) 2001-2017 OTRS AG, http://otrs.com/
# --
# Id: PreferencesCustom.pm,v 1.5 2009/02/16 11:47:34 tr Exp $
# --
# This software comes with ABSOLUTELY NO WARRANTY. For details, see
# the enclosed file COPYING for license information (AGPL). If you
# did not receive this file, see http://www.gnu.org/licenses/agpl.txt.
# --
package Kernel::System::Queue::PreferencesCustom;
use strict;
use warnings;
sub new {
my ( $Type, %Param ) = @_;
return $Self;
}
sub QueuePreferencesSet {
my ( $Self, %Param ) = @_;
58
$Self->{PreferencesTableValue} .= 'PreferencesCustom';
sub QueuePreferencesGet {
my ( $Self, %Param ) = @_;
# get preferences
return if !$Self->{DBObject}->Prepare(
SQL => "SELECT $Self->{PreferencesTableKey}, $Self->{PreferencesTableValue} "
. " FROM $Self->{PreferencesTable} WHERE $Self->{PreferencesTableQueueID} = ?",
Bind => [ \$Param{QueueID} ],
);
my %Data;
while ( my @Row = $Self->{DBObject}->FetchrowArray() ) {
$Data{ $Row[0] } = $Row[1];
}
# return data
return %Data;
}
1;
2.2.2.2. Configuration Example
There is the need to activate your custom queue preferences module. This can be done
using the XML configuration below.
59
2.2.2.4. Release Availability
Name Release
PreferencesDB 2.3
2.2.3.1. Code Example
The interface class is called Kernel::System::Service. The example service preferences
may be called Kernel::System::Service::PreferencesCustom. You can find an exam-
ple below.
# --
# Kernel/System/Service/PreferencesCustom - some user functions
# Copyright (C) 2001-2017 OTRS AG, http://otrs.com/
# --
# Id: PreferencesCustom.pm,v 1.2 2009/02/16 11:47:34 tr Exp $
# --
# This software comes with ABSOLUTELY NO WARRANTY. For details, see
# the enclosed file COPYING for license information (AGPL). If you
# did not receive this file, see http://www.gnu.org/licenses/agpl.txt.
# --
package Kernel::System::Service::PreferencesCustom;
use strict;
use warnings;
sub new {
my ( $Type, %Param ) = @_;
return $Self;
}
sub ServicePreferencesSet {
my ( $Self, %Param ) = @_;
60
$Self->{LogObject}->Log( Priority => 'error', Message => "Need $_!" );
return;
}
}
$Self->{PreferencesTableValue} .= 'PreferencesCustom';
sub ServicePreferencesGet {
my ( $Self, %Param ) = @_;
# get preferences
return if !$Self->{DBObject}->Prepare(
SQL => "SELECT $Self->{PreferencesTableKey}, $Self->{PreferencesTableValue} "
. " FROM $Self->{PreferencesTable} WHERE $Self->{PreferencesTableServiceID}
= ?",
Bind => [ \$Param{ServiceID} ],
);
my %Data;
while ( my @Row = $Self->{DBObject}->FetchrowArray() ) {
$Data{ $Row[0] } = $Row[1];
}
# return data
return %Data;
}
1;
2.2.3.2. Configuration Example
There is the need to activate your custom service preferences module. This can be done
using the XML configuration below.
61
</Setting>
</ConfigItem>
2.2.3.4. Release Availability
Name Release
PreferencesDB 2.4
2.2.4.1. Code Example
The interface class is called Kernel::System::SLA. The example SLA preferences may
be called Kernel::System::SLA::PreferencesCustom. You can find an example below.
# --
# Kernel/System/SLA/PreferencesCustom.pm - some user functions
# Copyright (C) 2001-2017 OTRS AG, http://otrs.com/
# --
# This software comes with ABSOLUTELY NO WARRANTY. For details, see
# the enclosed file COPYING for license information (AGPL). If you
# did not receive this file, see http://www.gnu.org/licenses/agpl.txt.
# --
package Kernel::System::SLA::PreferencesCustom;
use strict;
use warnings;
sub new {
my ( $Type, %Param ) = @_;
return $Self;
}
sub SLAPreferencesSet {
my ( $Self, %Param ) = @_;
62
# check needed stuff
for (qw(SLAID Key Value)) {
if ( !defined( $Param{$_} ) ) {
$Self->{LogObject}->Log( Priority => 'error', Message => "Need $_!" );
return;
}
}
$Self->{PreferencesTableValue} .= 'PreferencesCustom';
sub SLAPreferencesGet {
my ( $Self, %Param ) = @_;
# get preferences
return if !$Self->{DBObject}->Prepare(
SQL => "SELECT $Self->{PreferencesTableKey}, $Self->{PreferencesTableValue} "
. " FROM $Self->{PreferencesTable} WHERE $Self->{PreferencesTableSLAID} = ?",
Bind => [ \$Param{SLAID} ],
);
my %Data;
while ( my @Row = $Self->{DBObject}->FetchrowArray() ) {
$Data{ $Row[0] } = $Row[1];
}
# return data
return %Data;
}
1;
2.2.4.2. Configuration Example
There is the need to activate your custom SLA preferences module. This can be done
using the XML configuration below.
63
<Setting>
<String Regex="">Kernel::System::SLA::PreferencesCustom</String>
</Setting>
</ConfigItem>
2.2.4.4. Release Availability
Name Release
PreferencesDB 2.4
# --
# Kernel/System/Log/CustomFile.pm - file log backend
# Copyright (C) 2001-2017 OTRS AG, http://otrs.com/
# --
# This software comes with ABSOLUTELY NO WARRANTY. For details, see
# the enclosed file COPYING for license information (AGPL). If you
# did not receive this file, see http://www.gnu.org/licenses/agpl.txt.
# --
package Kernel::System::Log::CustomFile;
use strict;
use warnings;
umask "002";
sub new {
my ( $Type, %Param ) = @_;
64
# set custom prefix
$Self->{CustomPrefix} = 'CustomFileExample';
# Fixed bug# 2265 - For IIS we need to create a own error log file.
# Bind stderr to log file, because IIS do print stderr to web page.
if ( $ENV{SERVER_SOFTWARE} && $ENV{SERVER_SOFTWARE} =~ /^microsoft\-iis/i ) {
if ( !open STDERR, '>>', $Self->{LogFile} . '.error' ) {
print STDERR "ERROR: Can't write $Self->{LogFile}.error: $!";
}
}
return $Self;
}
sub Log {
my ( $Self, %Param ) = @_;
my $FH;
# open logfile
if ( !open $FH, '>>', $Self->{LogFile} ) {
1;
2.3.1.2. Configuration example
To activate our custom logging module, the administrator can either set the existing con-
figuration item LogModule manually to Kernel::System::Log::CustomFile. To realize
65
this automatically, you can provide an XML configuration file which overrides the default
setting.
2.3.2. Output Filter
Output filters allow to modify HTML on the fly. It is best practice to use output filters
instead of modifying .tt files directly. There are three good reasons for that. When the
same adaptation has to be applied to several frontend modules then the adaption only
has to be implemented once. The second advantage is that when OTRS is upgraded there
is a chance that the filter doesn't have to be updated, when the relevant pattern has
not changed. When two extensions modify the same file there is a conflict during the
installation of the second package. This conflict can be resolved by using two output filters
that modify the same frontend module.
There are three different kinds of output filters. They are active at different stages of the
generation of HTML content.
2.3.2.1. FilterElementPost
These filters allow to modify the output of a template after it was rendered.
To translate content, you can run $LayoutObject->Translate() directly. If you need oth-
er template features, just define a small template file for your output filter and use it to
render your content before injecting it into the main data. It can also be helpful to use
jQuery DOM operations to reorder/replace content on the screen in some cases instead
of using regular expressions. In this case you would inject the new code somewhere in
the page as invisible content (e. g. with the class Hidden), and then move it with jQuery
to the correct location in the DOM and show it.
To make using post output filters easier, there is also a mechanism to request HTML com-
ment hooks for certain templates/blocks. You can add in your module config XML like:
<ConfigItem
Name="Frontend::Template::GenerateBlockHooks###100-OTRSBusiness-ContactWithData"
Required="1" Valid="1">
<Description Translatable="1">Generate HTML comment hooks for
the specified blocks so that filters can use them.</Description>
<Group>OTRSBusiness</Group>
<SubGroup>Core</SubGroup>
66
<Setting>
<Hash>
<Item Key="AgentTicketZoom">
<Array>
<Item>CustomerTable</Item>
</Array>
</Item>
</Hash>
</Setting>
</ConfigItem>
<!--HookStartCustomerTable-->
... block output ...
<!--HookEndCustomerTable-->
With this mechanism every package can request just the block hooks it needs, and they
are consistently rendered. These HTML comments can then be used in your output filter
for easy regular expression matching.
2.3.2.2. FilterContent
This kind of filter allows to process the complete HTML output for the request right before
it is sent to the browser. This can be used for global transformations.
2.3.2.3. FilterText
This kind of output filter is a plugin for the method Kernel::Output::HTML::Lay-
out::Ascii2HTML() and is only active when the parameter LinkFeature is set to 1. Thus
the FilterText output filters are currently only active for the display of the body of plain
text articles. Plain text articles are generated by incoming non-HTML mails and when
OTRS is configured to not use the Rich Text feature in the frontend.
2.3.2.4. Code example
See package TemplateModule.
2.3.2.5. Configuration example
See package TemplateModule.
2.3.2.6. Use Cases
2.3.2.6.1. Show additional ticket attributes in AgentTicketZoom
Use a FilterElementPost for this feature. The list of selectable services can be parsed
from the processed template output. The multi level selection can be constructed from
the service list and inserted into the template content. A FilterElementPost output filter
must be used for that.
A biotech company uses gene names like IPI00217472 in plain text articles. A Filter-
Text output filter can be used to create links to a sequence database, e.g. http://srs.e-
bi.ac.uk/srsbin/cgi-bin/wgetz?-e+[IPI-acc:IPI00217472]+-vn+2, for the gene names.
67
2.3.2.6.4. Prohibit active content
There is firewall rule that disallows all active content. In order to avoid rejection by the
firewall, the HTML tag <applet> can be filtered with a FilterContent output filter.
2.3.2.8. Best Practices
In order to increase flexibility the list of affected templates should be configured in
SysConfig.
2.3.2.9. Release Availability
The output filters are available since OTRS 2.4. The type FilterElementPre was dropped
with OTRS 5.
2.3.3. Stats Module
There are two different types of internal stats modules - dynamic and static. This section
describes how such stats modules can be developed.
2.3.3.1. Dynamic Stats
In contrast to static stats modules, dynamic statistics can be configured via the OTRS
web interface. In this section a simple statistic module is developed. Each dynamic stats
module has to implement these subroutines:
• new
• GetObjectName
• GetObjectAttributes
• ExportWrapper
• ImportWrapper
2.3.3.1.1. Code example
In this section a sample stats module is shown and each subroutine is explained.
# --
# Kernel/System/Stats/Dynamic/DynamicStatsTemplate.pm - all advice functions
# Copyright (C) 2001-2017 OTRS AG, http://otrs.com/
# --
# This software comes with ABSOLUTELY NO WARRANTY. For details, see
# the enclosed file COPYING for license information (AGPL). If you
# did not receive this file, see http://www.gnu.org/licenses/agpl.txt.
# --
package Kernel::System::Stats::Dynamic::DynamicStatsTemplate;
use strict;
use warnings;
use Kernel::System::Queue;
68
use Kernel::System::State;
use Kernel::System::Ticket;
This is a common boilerplate that can be found in common OTRS modules. The class/
package name is declared via the package keyword. Then the needed modules are used
via the use keyword.
sub new {
my ( $Type, %Param ) = @_;
return $Self;
}
The new is the constructor for this statistic module. It creates a new instance of the class.
According to the coding guidelines objects of other classes that are needed in this module
have to be created in new. In lines 27 to 29 the object of the stats module is created. Lines
31 to 37 check if objects that are needed in this code - either for creating other objects
or in this module - are passed. After that the other objects are created.
sub GetObjectName {
my ( $Self, %Param ) = @_;
GetObjectName returns a name for the statistics module. This is the label that is shown
in the drop down in the configuration as well as in the list of existing statistics (column
"object").
sub GetObjectAttributes {
my ( $Self, %Param ) = @_;
my @ObjectAttributes = (
{
69
Name => 'State',
UseAsXvalue => 1,
UseAsValueSeries => 1,
UseAsRestriction => 1,
Element => 'StateIDs',
Block => 'MultiSelectField',
Values => \%StateList,
},
{
Name => 'Created in Queue',
UseAsXvalue => 1,
UseAsValueSeries => 1,
UseAsRestriction => 1,
Element => 'CreatedQueueIDs',
Block => 'MultiSelectField',
Translation => 0,
Values => \%QueueList,
},
{
Name => 'Create Time',
UseAsXvalue => 1,
UseAsValueSeries => 1,
UseAsRestriction => 1,
Element => 'CreateTime',
TimePeriodFormat => 'DateInputFormat', # 'DateInputFormatLong',
Block => 'Time',
TimeStop => $Today,
Values => {
TimeStart => 'TicketCreateTimeNewerDate',
TimeStop => 'TicketCreateTimeOlderDate',
},
},
);
return @ObjectAttributes;
}
In this sample stats module, we want to provide three attributes the user can chose from:
a list of queues, a list of states and a time drop down. To get the values shown in the drop
down, some operations are needed. In this case StateList and GetAllQueues are called.
Then the list of attributes is created. Each attribute is defined via a hash reference. You
can use these keys:
• Name
• UseAsXvalue
• UseAsValueSeries
• UseAsRestriction
• Element
• Block
70
• Values
Hint: If you install this sample and you configure a statistic with some queues - lets say
'queue A' and 'queue B' - then these queues are the only ones that are shown to the
user when he starts the statistic. Sometimes a dynamic drop down or multiselect field is
needed. In this case, you can set SelectedValues in the definition of the attribute:
{
Name => 'Created in Queue',
UseAsXvalue => 1,
UseAsValueSeries => 1,
UseAsRestriction => 1,
Element => 'CreatedQueueIDs',
Block => 'MultiSelectField',
Translation => 0,
Values => \%QueueList,
SelectedValues => [ @SelectedQueues ],
},
sub GetStatElement {
my ( $Self, %Param ) = @_;
# search tickets
return $Self->{TicketObject}->TicketSearch(
UserID => 1,
Result => 'COUNT',
Permission => 'ro',
Limit => 100_000_000,
%Param,
);
}
GetStatElement gets called for each cell in the result table. So it should be a numeric
value. In this sample it does a simple ticket search. The hash %Param contains information
about the "current" x-value and the y-value as well as any restrictions. So, for a cell that
should count the created tickets for queue 'Misc' with state 'open' the passed parameter
hash looks something like this:
'CreatedQueueIDs' => [
'4'
],
'StateIDs' => [
'2'
]
sub GetStatTable {
my ( $Self, %Param ) = @_;
my @StatData;
71
Result => 'COUNT',
Permission => 'ro',
Limit => 100_000_000,
%{$Params},
);
return @StatData;
}
GetStatTable gets all information about the stats query that is needed. The passed para-
meters contain information about the attributes (Restrictions, attributes that are used
for x/y-axis) and the table structure. The table structure is a hash reference where the
keys are the values of the y-axis and their values are hash references with the parameters
used for GetStatElement subroutines.
72
}
],
'XValue' => {
'Block' => 'MultiSelectField',
'Element' => 'CreatedQueueIDs',
'Name' => 'Created in Queue',
'SelectedValues' => [
'3',
'4',
'1',
'2'
],
'Translation' => 0,
'Values' => {
'1' => 'Postmaster',
'2' => 'Raw',
'3' => 'Junk',
'4' => 'Misc'
}
}
Sometimes the headers of the table have to be changed. In that case, a subroutine called
GetHeaderLine has to be implemented. That subroutine has to return an array reference
with the column headers as elements. It gets information about the x-values passed.
sub GetHeaderLine {
my ( $Self, %Param ) = @_;
my @HeaderLine = ('');
for my $SelectedXValue ( @{ $Param{XValue}->{SelectedValues} } ) {
push @HeaderLine, $Param{XValue}->{Values}->{$SelectedXValue};
}
return \@HeaderLine;
}
sub ExportWrapper {
my ( $Self, %Param ) = @_;
73
Configured statistics can be exported into XML format. But as queues with the same queue
names can have different IDs on different OTRS instances it would be quite painful to
export the IDs (the statistics would calculate the wrong numbers then). So an export
wrapper should be written to use the names instead of ids. This should be done for each
"dimension" of the stats module (x-axis, y-axis and restrictions).
ImportWrapper works the other way around - it converts the name to the ID in the instance
the configuration is imported to.
<otrs_stats>
<Cache>0</Cache>
<Description>Sample stats module</Description>
<File></File>
<Format>CSV</Format>
<Format>Print</Format>
<Object>DeveloperManualSample</Object>
<ObjectModule>Kernel::System::Stats::Dynamic::DynamicStatsTemplate</ObjectModule>
<ObjectName>Sample Statistics</ObjectName>
<Permission>stats</Permission>
<StatType>dynamic</StatType>
<SumCol>0</SumCol>
<SumRow>0</SumRow>
<Title>Sample 1</Title>
<UseAsValueSeries Element="StateIDs" Fixed="1">
<SelectedValues>removed</SelectedValues>
<SelectedValues>closed unsuccessful</SelectedValues>
<SelectedValues>closed successful</SelectedValues>
<SelectedValues>new</SelectedValues>
<SelectedValues>open</SelectedValues>
</UseAsValueSeries>
<UseAsXvalue Element="CreatedQueueIDs" Fixed="1">
<SelectedValues>Junk</SelectedValues>
<SelectedValues>Misc</SelectedValues>
<SelectedValues>Postmaster</SelectedValues>
<SelectedValues>Raw</SelectedValues>
</UseAsXvalue>
<Valid>1</Valid>
</otrs_stats>
Now, that all subroutines are explained, this is the complete sample stats module.
# --
# Kernel/System/Stats/Dynamic/DynamicStatsTemplate.pm - all advice functions
# Copyright (C) 2001-2017 OTRS AG, http://otrs.com/
# --
# This software comes with ABSOLUTELY NO WARRANTY. For details, see
# the enclosed file COPYING for license information (AGPL). If you
# did not receive this file, see http://www.gnu.org/licenses/agpl.txt.
# --
package Kernel::System::Stats::Dynamic::DynamicStatsTemplate;
use strict;
use warnings;
use Kernel::System::Queue;
use Kernel::System::State;
use Kernel::System::Ticket;
sub new {
my ( $Type, %Param ) = @_;
74
bless( $Self, $Type );
return $Self;
}
sub GetObjectName {
my ( $Self, %Param ) = @_;
sub GetObjectAttributes {
my ( $Self, %Param ) = @_;
my @ObjectAttributes = (
{
Name => 'State',
UseAsXvalue => 1,
UseAsValueSeries => 1,
UseAsRestriction => 1,
Element => 'StateIDs',
Block => 'MultiSelectField',
Values => \%StateList,
},
{
Name => 'Created in Queue',
UseAsXvalue => 1,
UseAsValueSeries => 1,
UseAsRestriction => 1,
Element => 'CreatedQueueIDs',
Block => 'MultiSelectField',
Translation => 0,
Values => \%QueueList,
},
{
Name => 'Create Time',
UseAsXvalue => 1,
UseAsValueSeries => 1,
UseAsRestriction => 1,
Element => 'CreateTime',
TimePeriodFormat => 'DateInputFormat', # 'DateInputFormatLong',
Block => 'Time',
TimeStop => $Today,
Values => {
TimeStart => 'TicketCreateTimeNewerDate',
TimeStop => 'TicketCreateTimeOlderDate',
75
},
},
);
return @ObjectAttributes;
}
sub GetStatElement {
my ( $Self, %Param ) = @_;
# search tickets
return $Self->{TicketObject}->TicketSearch(
UserID => 1,
Result => 'COUNT',
Permission => 'ro',
Limit => 100_000_000,
%Param,
);
}
sub ExportWrapper {
my ( $Self, %Param ) = @_;
sub ImportWrapper {
my ( $Self, %Param ) = @_;
76
$Self->{LogObject}->Log(
Priority => 'error',
Message => "Import: Can' find the queue $ID->{Content}!"
);
$ID = undef;
}
}
}
elsif ( $ElementName eq 'StateIDs' || $ElementName eq 'CreatedStateIDs' ) {
ID:
for my $ID ( @{$Values} ) {
next ID if !$ID;
my %State = $Self->{StateObject}->StateGet(
Name => $ID->{Content},
Cache => 1,
);
if ( $State{ID} ) {
$ID->{Content} = $State{ID};
}
else {
$Self->{LogObject}->Log(
Priority => 'error',
Message => "Import: Can' find state $ID->{Content}!"
);
$ID = undef;
}
}
}
}
}
return \%Param;
}
1;
2.3.3.1.2. Configuration example
Use cases.
If you have a lot of cells in the result table and the GetStatElement is quite complex, the
request can take a long time.
2.3.3.1.5. Release Availability
77
2.3.3.2. Static Stats
The subsequent paragraphs describe the static stats. Static stats are very easy to create
as these modules have to implement only three subroutines.
• new
• Param
• Run
2.3.3.2.1. Code example
sub new {
my ( $Type, %Param ) = @_;
return $Self;
}
The new creates a new instance of the static stats class. First it creates a new object and
then it checks for the needed objects.
sub Param {
my $Self = shift;
my %Queues = $Self->{QueueObject}->GetAllQueues();
my %Types = $Self->{TypeObject}->TypeList(
Valid => 1,
);
my @Params = (
{
Frontend => 'Type',
Name => 'TypeIDs',
Multiple => 1,
Size => 3,
Data => \%Types,
},
{
Frontend => 'Queue',
Name => 'QueueIDs',
Multiple => 1,
Size => 3,
Data => \%Queues,
},
);
78
return @Params;
}
The Param method provides the list of all parameters/attributes that can be selected to
create a static stat. It gets some parameters passed: The values for the stats attributes
provided in a request, the format of the stats and the name of the object (name of the
module).
• Frontend
• Name
• Data
Other parameter for the BuildSelection method of the LayoutObject can be used, as
it is done with Size and Multiple in this sample module.
sub Run {
my ( $Self, %Param ) = @_;
# table headlines
my @HeadData = (
'Ticket Number',
'Queue',
'Type',
);
my @Data;
my @TicketIDs = $Self->{TicketObject}->TicketSearch(
UserID => 1,
Result => 'ARRAY',
Permission => 'ro',
%Param,
);
79
The Run method actually generates the table data for the stats. It gets the attributes
for this stats passed. In this sample in %Param a key TypeIDs and a key QueueIDs exist
(see attributes in Param method) and their values are array references. The returned data
consists of three parts: Two array references and an array. In the first array reference the
title for the statistic is stored, the second array reference contains the headlines for the
columns in the table. And then the data for the table body follow.
# --
# Kernel/System/Stats/Static/StaticStatsTemplate.pm
# Copyright (C) 2001-2017 OTRS AG, http://otrs.com/
# --
# This software comes with ABSOLUTELY NO WARRANTY. For details, see
# the enclosed file COPYING for license information (AGPL). If you
# did not receive this file, see http://www.gnu.org/licenses/agpl.txt.
# --
package Kernel::System::Stats::Static::StaticStatsTemplate;
use strict;
use warnings;
use Kernel::System::Type;
use Kernel::System::Ticket;
use Kernel::System::Queue;
=head1 NAME
StaticStatsTemplate.pm - the module that creates the stats about tickets in a queue
=head1 SYNOPSIS
All functions
=over 4
=cut
=item new()
create an object
use Kernel::Config;
use Kernel::System::Encode;
use Kernel::System::Log;
use Kernel::System::Main;
use Kernel::System::Time;
use Kernel::System::DB;
use Kernel::System::Stats::Static::StaticStatsTemplate;
my $ConfigObject = Kernel::Config->new();
my $EncodeObject = Kernel::System::Encode->new(
ConfigObject => $ConfigObject,
);
my $LogObject = Kernel::System::Log->new(
ConfigObject => $ConfigObject,
);
my $MainObject = Kernel::System::Main->new(
ConfigObject => $ConfigObject,
LogObject => $LogObject,
);
my $TimeObject = Kernel::System::Time->new(
ConfigObject => $ConfigObject,
LogObject => $LogObject,
);
my $DBObject = Kernel::System::DB->new(
ConfigObject => $ConfigObject,
LogObject => $LogObject,
MainObject => $MainObject,
80
);
my $StatsObject = Kernel::System::Stats::Static::StaticStatsTemplate->new(
ConfigObject => $ConfigObject,
LogObject => $LogObject,
MainObject => $MainObject,
TimeObject => $TimeObject,
DBObject => $DBObject,
EncodeObject => $EncodeObject,
);
=cut
sub new {
my ( $Type, %Param ) = @_;
return $Self;
}
=item Param()
my @Params = $StatsObject->Param();
=cut
sub Param {
my $Self = shift;
my %Queues = $Self->{QueueObject}->GetAllQueues();
my %Types = $Self->{TypeObject}->TypeList(
Valid => 1,
);
my @Params = (
{
Frontend => 'Type',
Name => 'TypeIDs',
Multiple => 1,
Size => 3,
Data => \%Types,
},
{
Frontend => 'Queue',
Name => 'QueueIDs',
Multiple => 1,
Size => 3,
Data => \%Queues,
},
);
return @Params;
}
81
=item Run()
my $StatsInfo = $StatsObject->Run(
TypeIDs => [
1, 2, 4
],
QueueIDs => [
3, 4, 6
],
);
=cut
sub Run {
my ( $Self, %Param ) = @_;
# table headlines
my @HeadData = (
'Ticket Number',
'Queue',
'Type',
);
my @Data;
my @TicketIDs = $Self->{TicketObject}->TicketSearch(
UserID => 1,
Result => 'ARRAY',
Permission => 'ro',
%Param,
);
1;
=back
=cut
82
2.3.3.2.2. Configuration example
There is no configuration needed. Right after installation, the module is available to create
a statistic for this module.
Use cases.
2.3.3.2.5. Release Availability
Standard OTRS versions 1.3 and 2.0 already facilitated the generation of stats. Various
stats for OTRS versions 1.3 and 2.0 which have been specially developed to meet cus-
tomers' requirements can be used in more recent versions too.
The files must merely be moved from the Kernel/System/Stats/ path to Kernel/Sys-
tem/Stats/Static/. Additionally the package name of the respective script must be
amended by ::Static.
package Kernel::System::Stats::AccountedTime;
package Kernel::System::Stats::Static::AccountedTime;
When creating a ticket number, make sure the result is prefixed by the SysConfig variable
SystemID in order to enable the detection of ticket numbers on inbound email responses.
A ticket number generator module needs the two functions TicketCreateNumber() and
GetTNByString().
The method TicketCreateNumber() is called without parameters and returns the new
ticket number.
The method GetTNByString() is called with the param String which contains the string
to be parsed for a ticket number and returns the ticket number if found.
2.3.4.1. Code example
See Kernel/System/Ticket/Number/UserRandom.pm in the package TemplateModule.
2.3.4.2. Configuration example
See Kernel/Config/Files/TicketNumberGenerator.xml in the package TemplateMod-
ule.
83
2.3.4.3. Use Cases
You will need to create a new ticket number generator if the default modules don't provide
the ticket number scheme you'd like to use.
You should stick to the code of GetTNByString() as used in existing ticket number gen-
erators to prevent problems with ticket number parsing. Also the routine to detect a loop
in TicketCreateNumber() should be kept intact to prevent duplicate ticket numbers.
2.3.4.5. Release Availability
Ticket number generators have been available in OTRS since OTRS 1.1.
2.3.5.1. Code example
2.3.5.2. Configuration example
2.3.5.3. Use Cases
This standard feature has been implemented with the ticket event module Kernel::Sys-
tem::Ticket::Event::ForceUnlock. When this feature is not wanted, then it can be
turned off by unsetting the SysConfig entry Ticket::EventModulePost###910-Force-
UnlockOnMove.
A customized OTRS might hold non-standard data in additional database tables. When a
ticket is deleted then this additional data needs to be deleted. This functionality can be
achieved with a ticket event module listening to TicketDelete events.
84
2.3.5.5. Release Availability
Ticket events have been available in OTRS since OTRS 2.0.
• TicketCreate
• TicketDelete
• TicketTitleUpdate
• TicketUnlockTimeoutUpdate
• TicketQueueUpdate
• TicketTypeUpdate
• TicketServiceUpdate
• TicketSLAUpdate
• TicketCustomerUpdate
• TicketPendingTimeUpdate
• TicketLockUpdate
• TicketArchiveFlagUpdate
• TicketStateUpdate
• TicketOwnerUpdate
• TicketResponsibleUpdate
• TicketPriorityUpdate
• HistoryAdd
• HistoryDelete
• TicketAccountTime
• TicketMerge
• TicketSubscribe
• TicketUnsubscribe
• TicketFlagSet
• TicketFlagDelete
• EscalationResponseTimeNotifyBefore
• EscalationUpdateTimeNotifyBefore
• EscalationSolutionTimeNotifyBefore
• EscalationResponseTimeStart
85
• EscalationUpdateTimeStart
• EscalationSolutionTimeStart
• EscalationResponseTimeStop
• EscalationUpdateTimeStop
• EscalationSolutionTimeStop
• NotificationNewTicket
• NotificationFollowUp
• NotificationLockTimeout
• NotificationOwnerUpdate
• NotificationResponsibleUpdate
• NotificationAddNote
• NotificationMove
• NotificationPendingReminder
• NotificationEscalation
• NotificationEscalationNotifyBefore
• NotificationServiceUpdate
• ArticleCreate
• ArticleUpdate
• ArticleSend
• ArticleBounce
• ArticleAgentNotification
• ArticleCustomerNotification
• ArticleAutoResponse
• ArticleFlagSet
• ArticleFlagDelete
• ArticleAgentNotification
• ArticleCustomerNotification
2.4. Frontend Modules
2.4.1. Dashboard Module
Dashboard module to display statistics in the form of a line graph.
86
Figure 3.1. Dashboard Widget
# --
# Kernel/Output/HTML/DashboardTicketStatsGeneric.pm - message of the day
# Copyright (C) 2001-2017 OTRS AG, http://otrs.com/
# --
# This software comes with ABSOLUTELY NO WARRANTY. For details, see
# the enclosed file COPYING for license information (AGPL). If you
# did not receive this file, see http://www.gnu.org/licenses/agpl.txt.
# --
package Kernel::Output::HTML::DashboardTicketStatsGeneric;
use strict;
use warnings;
sub new {
my ( $Type, %Param ) = @_;
return $Self;
}
sub Preferences {
my ( $Self, %Param ) = @_;
return;
}
sub Config {
87
my ( $Self, %Param ) = @_;
sub Run {
my ( $Self, %Param ) = @_;
my %Axis = (
'7Day' => {
0 => { Day => 'Sun', Created => 0, Closed => 0, },
1 => { Day => 'Mon', Created => 0, Closed => 0, },
2 => { Day => 'Tue', Created => 0, Closed => 0, },
3 => { Day => 'Wed', Created => 0, Closed => 0, },
4 => { Day => 'Thu', Created => 0, Closed => 0, },
5 => { Day => 'Fri', Created => 0, Closed => 0, },
6 => { Day => 'Sat', Created => 0, Closed => 0, },
},
);
my @Data;
my $Max = 1;
for my $Key ( 0 .. 6 ) {
my $TimeNow = $Self->{TimeObject}->SystemTime();
if ($Key) {
$TimeNow = $TimeNow - ( 60 * 60 * 24 * $Key );
}
my ( $Sec, $Min, $Hour, $Day, $Month, $Year, $WeekDay )
= $Self->{TimeObject}->SystemTime2Date(
SystemTime => $TimeNow,
);
$Data[$Key]->{Day} = $Self->{LayoutObject}->{LanguageObject}->Get(
$Axis{'7Day'}->{$WeekDay}->{Day}
);
my $CountCreated = $Self->{TicketObject}->TicketSearch(
# tickets with create time after ... (ticket newer than this date) (optional)
TicketCreateTimeNewerDate => "$Year-$Month-$Day 00:00:00",
# tickets with created time before ... (ticket older than this date) (optional)
TicketCreateTimeOlderDate => "$Year-$Month-$Day 23:59:59",
my $CountClosed = $Self->{TicketObject}->TicketSearch(
# tickets with create time after ... (ticket newer than this date) (optional)
TicketCloseTimeNewerDate => "$Year-$Month-$Day 00:00:00",
88
# tickets with created time before ... (ticket older than this date) (optional)
TicketCloseTimeOlderDate => "$Year-$Month-$Day 23:59:59",
my $Content = $Self->{LayoutObject}->Output(
TemplateFile => 'AgentDashboardTicketStats',
Data => {
%{ $Self->{Config} },
Key => int rand 99999,
Max => $Max,
Source => $Source,
},
);
return $Content;
}
1;
To use this module add the following to the Kernel/Config.pm and restart your web
server (if you use mod_perl).
89
2.4.1.1. Caveats and Warnings
2.4.1.2. Release Availability
2.4.2. Notification Module
Notification modules are used to display a notification below the main navigation. You
can write and register your own notification module. There are currently 5 ticket menus
in the OTRS framework.
• AgentOnline
• AgentTicketEscalation
• CharsetCheck
• CustomerOnline
• UIDCheck
2.4.2.1. Code Example
# --
# Kernel/Output/HTML/NotificationCustom.pm
# Copyright (C) 2001-2017 OTRS AG, http://otrs.com/
# --
# This software comes with ABSOLUTELY NO WARRANTY. For details, see
# the enclosed file COPYING for license information (AGPL). If you
# did not receive this file, see http://www.gnu.org/licenses/agpl.txt.
# --
package Kernel::Output::HTML::NotificationCustom;
use strict;
use warnings;
use Kernel::System::Custom;
sub new {
my ( $Type, %Param ) = @_;
sub Run {
my ( $Self, %Param ) = @_;
90
# get session info
my %CustomParam = ();
my @Customs = $Self->{CustomObject}->GetAllCustomIDs();
my $IdleMinutes = $Param{Config}->{IdleMinutes} || 60 * 2;
for (@Customs) {
my %Data = $Self->{CustomObject}->GetCustomIDData( CustomID => $_, );
if (
$Self->{UserID} ne $Data{UserID}
&& $Data{UserType} eq 'User'
&& $Data{UserLastRequest}
&& $Data{UserLastRequest} + ( $IdleMinutes * 60 ) > $Self->{TimeObject}-
>SystemTime()
&& $Data{UserFirstname}
&& $Data{UserLastname}
)
{
$CustomParam{ $Data{UserID} } = "$Data{UserFirstname} $Data{UserLastname}";
if ( $Param{Config}->{ShowEmail} ) {
$CustomParam{ $Data{UserID} } .= " ($Data{UserEmail})";
}
}
}
for ( sort { $CustomParam{$a} cmp $CustomParam{$b} } keys %CustomParam ) {
if ( $Param{Message} ) {
$Param{Message} .= ', ';
}
$Param{Message} .= "$CustomParam{$_}";
}
if ( $Param{Message} ) {
return $Self->{LayoutObject}->Notify( Info => 'Custom Message: %s", "' .
$Param{Message} );
}
else {
return '';
}
}
1;
2.4.2.2. Configuration Example
There is the need to activate your custom notification module. This can be done using
the XML configuration below. There may be additional parameters in the config hash for
your notification module.
91
2.4.2.4. Release Availability
Name Release
NotificationAgentOnline 2.0
NotificationAgentTicketEscalation 2.0
NotificationCharsetCheck 1.2
NotificationCustomerOnline 2.0
NotificationUIDCheck 1.2
2.4.3.1. Code Example
The ticket menu modules are located under Kernel/Output/HTML/TicketMenu*.pm. Fol-
lowing, there is an example of a ticket menu module. Save it under Kernel/Output/HTML/
TicketMenuCustom.pm. You just need 2 functions: new() and Run().
# --
# Kernel/Output/HTML/TicketMenuCustom.pm
# Copyright (C) 2001-2017 OTRS AG, http://otrs.com/
# --
# Id: TicketMenuCustom.pm,v 1.17 2010/04/12 21:34:06 martin Exp $
# --
# This software comes with ABSOLUTELY NO WARRANTY. For details, see
# the enclosed file COPYING for license information (AGPL). If you
# did not receive this file, see http://www.gnu.org/licenses/agpl.txt.
# --
package Kernel::Output::HTML::TicketMenuCustom;
use strict;
use warnings;
sub new {
my ( $Type, %Param ) = @_;
return $Self;
}
sub Run {
my ( $Self, %Param ) = @_;
92
# check if frontend module registered, if not, do not show action
if ( $Param{Config}->{Action} ) {
my $Module = $Self->{ConfigObject}->Get('Frontend::Module')->{ $Param{Config}-
>{Action} };
return if !$Module;
}
# check permission
my $AccessOk = $Self->{TicketObject}->Permission(
Type => 'rw',
TicketID => $Param{Ticket}->{TicketID},
UserID => $Self->{UserID},
LogNo => 1,
);
return if !$AccessOk;
# check permission
if ( $Self->{TicketObject}->CustomIsTicketCustom( TicketID => $Param{Ticket}-
>{TicketID} ) ) {
my $AccessOk = $Self->{TicketObject}->OwnerCheck(
TicketID => $Param{Ticket}->{TicketID},
OwnerID => $Self->{UserID},
);
return if !$AccessOk;
}
# check acl
return
if defined $Param{ACL}->{ $Param{Config}->{Action} }
&& !$Param{ACL}->{ $Param{Config}->{Action} };
# if ticket is customized
if ( $Param{Ticket}->{Custom} eq 'lock' ) {
# if ticket is customized
return {
%{ $Param{Config} },
%{ $Param{Ticket} },
%Param,
Name => 'Custom',
Description => 'Custom it to work on it!',
Link => 'Action=AgentTicketCustom;Subaction=Custom;TicketID=
$QData{"TicketID"}',
};
}
1;
2.4.3.2. Configuration Example
There is the need to activate your custom ticket menu module. This can be done using
the XML configuration below. There may be additional parameters in the config hash for
your ticket menu module.
93
<ConfigItem Name="Ticket::Frontend::MenuModule###110-Custom" Required="0" Valid="1">
<Description Lang="en">Module to show custom link in menu.</Description>
<Description Lang="de">Mit diesem Modul wird der Custom-Link in der Linkleiste der
Ticketansicht angezeigt.</Description>
<Group>Ticket</Group>
<SubGroup>Frontend::Agent::Ticket::MenuModule</SubGroup>
<Setting>
<Hash>
<Item Key="Module">Kernel::Output::HTML::TicketMenuCustom</Item>
<Item Key="Name">Custom</Item>
<Item Key="Action">AgentTicketCustom</Item>
</Hash>
</Setting>
</ConfigItem>
2.4.3.5. Release Availability
Name Release
TicketMenuGeneric 2.0
TicketMenuLock 2.0
TicketMenuResponsible 2.1
TicketMenuTicketWatcher 2.4
OTRS as provider:
OTRS uses the network transport modules to get the data from the Remote System and
the operation to be executed. After the operation is performed OTRS uses them again to
send the response back to the Remote System.
OTRS as requester:
OTRS uses the network transport modules to send petitions to the Remote System to
perform a remote action along with the required data. OTRS waits for the Remote System
response and send it back to the Requester module.
In both ways network transport modules deal with the data in the Remote System format.
It is not recommended to do any data transformation in this modules, as the Mapping layer
is the responsible to perform any data transformation needed during the communication.
An exception of this is the data conversion that is required specifically by for the transport
e.g. XML or JSON from / to Perl conversions.
94
2.5.1.1. Transport backend
Next we will show how to develop a new transport backend. Each transport backend has
to implement these subroutines:
• new
• ProviderProcessRequest
• ProviderGenerateResponse
• RequesterPerformRequest
We should implement each one of this methods in order to be able to communicate cor-
rectly with a Remote System in both ways. All network transport backends are handled
by the transport module (Kernel/GenericInterface/Transport.pm).
Currently Generic Interface implements the HTTP SOAP and HTTP REST transports. If the
planned web service can use HTTP SOAP or HTTP SOAP there is no need to create a new
network transport module, instead we recommend to take a look into HTTP SOAP or HTTP
REST configurations to check their settings and how it can be tuned according to the
remote system.
2.5.1.1.1. Code example
In case that the provided network transports does not match the web service needs,
then in this section a sample network transport module is shown and each subroutine is
explained. Normally transport modules uses CPAN modules as backends. For example the
HTTP SOAP transport modules uses SOAP::Lite module as backend.
For this example a custom package is used to return the data without doing a real network
request to a Remote System, instead this custom module acts as a loop-back interface.
# --
# Kernel/GenericInterface/Transport/HTTP/Test.pm - GenericInterface network transport
interface for testing
# Copyright (C) 2001-2017 OTRS AG, http://otrs.com/
# --
# This software comes with ABSOLUTELY NO WARRANTY. For details, see
# the enclosed file COPYING for license information (AGPL). If you
# did not receive this file, see http://www.gnu.org/licenses/agpl.txt.
# --
package Kernel::GenericInterface::Transport::HTTP::Test;
use strict;
use warnings;
use HTTP::Request::Common;
use LWP::UserAgent;
use LWP::Protocol;
our $ObjectManagerDisabled = 1;
This is common header that can be found in common OTRS modules. The class/package
name is declared via the package keyword. Transports can not be instantiated by the
Object Manager.
sub new {
my ( $Type, %Param ) = @_;
95
my $Self = {};
bless( $Self, $Type );
return $Self;
}
The constructor new creates a new instance of the class. According to the coding guide-
lines only objects of other classes not handled by the object manager that are needed in
this module have to be created in new.
sub ProviderProcessRequest {
my ( $Self, %Param ) = @_;
if ( $Self->{TransportConfig}->{Config}->{Fail} ) {
return {
Success => 0,
ErrorMessage => "HTTP status code: 500",
Data => {},
};
}
my $ParamObject = $Kernel::OM->Get('Kernel::System::Web::Request');
my %Result;
for my $ParamName ( $ParamObject->GetParamNames() ) {
$Result{$ParamName} = $ParamObject->GetParam( Param => $ParamName );
}
if ( !%Result ) {
return $Self->{DebuggerObject}->Error(
Summary => 'No request data found.',
);
}
return {
Success => 1,
Data => \%Result,
Operation => 'test_operation',
};
}
The ProviderProcessRequest function gets the request from the Remote System (in this
case the same OTRS) and extracts the data and the operation to perform from the request.
For this example the operation is always test_operation.
The way this function parses the request to get the data and the operation name, depends
completely on the protocol to be implemented and the external modules that are used for.
sub ProviderGenerateResponse {
my ( $Self, %Param ) = @_;
96
if ( $Self->{TransportConfig}->{Config}->{Fail} ) {
return {
Success => 0,
ErrorMessage => 'Test response generation failed',
};
}
my $Response;
if ( !$Param{Success} ) {
$Response
= HTTP::Response->new( 500 => ( $Param{ErrorMessage} || 'Internal Server
Error' ) );
$Response->protocol('HTTP/1.0');
$Response->content_type("text/plain; charset=UTF-8");
$Response->date(time);
}
else {
$Self->{DebuggerObject}->Debug(
Summary => 'Sending HTTP response',
Data => $Response->as_string(),
);
return {
Success => 1,
};
}
This function sends the response back to the Remote System for the requested operation.
For this particular example we return a standard HTTP response success (200) or not
(500), along with the required data on each case.
sub RequesterPerformRequest {
my ( $Self, %Param ) = @_;
if ( $Self->{TransportConfig}->{Config}->{Fail} ) {
return {
Success => 0,
ErrorMessage => "HTTP status code: 500",
Data => {},
};
}
# use custom protocol handler to avoid sending out real network requests
LWP::Protocol::implementor(
testhttp => 'Kernel::GenericInterface::Transport::HTTP::Test::CustomHTTPProtocol'
);
my $UserAgent = LWP::UserAgent->new();
my $Response = $UserAgent->post( 'testhttp://localhost.local/', Content =>
$Param{Data} );
97
return {
Success => 1,
Data => {
ResponseContent => $Response->content(),
},
};
}
This is the only function that is used by OTRS as requester. It sends the request to the
Remote System and waits for its response.
For this example we use a custom protocol handler to avoid send the request to the real
network. This custom protocol is specified below.
package Kernel::GenericInterface::Transport::HTTP::Test::CustomHTTPProtocol;
sub new {
my $Class = shift;
return $Class->SUPER::new(@_);
}
#print $Request->as_string();
#print $Response->as_string();
return $Response;
}
This is the code for the custom protocol that we use. This approach is only useful for
training or for testing environments where the Remote Systems are not available.
For a new module development we do not recommend to use this approach, a real protocol
needs to be implemented.
1;
=cut
2.5.1.1.2. Configuration Example
There is the need to register this network transport module to be accessible in the OTRS
GUI. This can be done using the XML configuration below.
98
<ConfigItem Name="GenericInterface::Transport::Module###HTTP::Test" Required="0" Valid="1">
<Description Translatable="1">GenericInterface module registration for the transport
layer.</Description>
<Group>GenericInterface</Group>
<SubGroup>GenericInterface::Transport::ModuleRegistration</SubGroup>
<Setting>
<Hash>
<Item Key="Name">Test</Item>
<Item Key="Protocol">HTTP</Item>
<Item Key="ConfigDialog">AdminGenericInterfaceTransportHTTPTest</Item>
</Hash>
</Setting>
</ConfigItem>
2.5.2. Mapping
The mapping is used to convert data from OTRS to the external systems, and vice versa.
This data can be represented as key => value pairs. Mapping modules can be developed
to transform not just values but also the keys.
For example:
From To
Prio => Warning PriorityID => 3
The mapping layer is not absolutely necessary, a web service can skip it completely de-
pending on the web service configuration and how invokers and operation are implement-
ed. But if some data transformations are needed, is highly recommended to use an exist-
ing mapping module or create a new one.
Mapping modules can be called more than one time during a normal communication, take
a look to the following examples.
1. The remote system sends the request with the data in the remote system format
2. The data is mapped from the remote system format to the OTRS format
3. OTRS performs the operation and return the response in OTRS format
4. The data is mapped from the OTRS format to the remote system format
5. The response with the data in the remote system format is sent to the remote system
1. OTRS prepares the request to the remote system using the data in the OTRS format
2. The data is mapped from the OTRS format to the remote system format
3. The request is sent to the remote system which performs the action and sends the
response back to OTRS with the data in remote system format
4. The data is mapped form remote system format (again) to the OTRS format
2.5.2.1. Mapping backend
Generic Interface provides a mapping module called Simple. With this module most of the
data transformations including key and value mapping can be done, and also it defines
rules for to handling the default mappings for both keys and values.
99
So it is highly probable that you don't need to develop a custom mapping module. Please
check Simple mapping module (Kernel/GenericInterface/Mapping/Simple.pm) and its
on-line documentation before continue.
If Simple mapping module does not match your needs then we will show how to develop
a new mapping backend. Each mapping backend has to implement these subroutines:
• new
• Map
We should implement each one of this methods in order to be able to map the data in the
communication, handled either by the requester or provider. All mapping backends are
handled by the mapping module (Kernel/GenericInterface/Mapping.pm).
2.5.2.1.1. Code example
In this section a sample mapping module is shown and each subroutine is explained.
# --
# Kernel/GenericInterface/Mapping/Test.pm - GenericInterface test data mapping backend
# Copyright (C) 2001-2017 OTRS AG, http://otrs.com/
# --
# This software comes with ABSOLUTELY NO WARRANTY. For details, see
# the enclosed file COPYING for license information (AGPL). If you
# did not receive this file, see http://www.gnu.org/licenses/agpl.txt.
# --
package Kernel::GenericInterface::Mapping::Test;
use strict;
use warnings;
our $ObjectManagerDisabled = 1;
This is common header that can be found in common OTRS modules. The class/package
name is declared via the package keyword.
We also include VariableCheck module to perform certain validation over some variables.
Mappings can not be instantiated by the Object Manager.
sub new {
my ( $Type, %Param ) = @_;
return {
Success => 0,
ErrorMessage => "Got no $Needed!"
};
}
$Self->{$Needed} = $Param{$Needed};
}
return $Self->{DebuggerObject}->Error(
100
Summary => 'Got no MappingConfig as hash ref with content!',
);
}
return $Self->{DebuggerObject}->Error(
Summary => 'Got MappingConfig with Data, but Data is no hash ref with content!',
);
}
return $Self;
}
The constructor new creates a new instance of the class. According to the coding guide-
lines only objects of other classes not handled by the object manager that are needed in
this module have to be created in new.
sub Map {
my ( $Self, %Param ) = @_;
return $Self->{DebuggerObject}->Error(
Summary => 'Got Data but it is not a hash ref in Mapping Test backend!'
);
}
return {
Success => 1,
Data => {},
};
}
return {
Success => 1,
Data => $Param{Data},
};
}
return $Self->{DebuggerObject}->Error(
Summary => 'Got no TestOption as string with value!',
);
}
101
$ReturnData = $Self->_ToLower( Data => $Param{Data} );
}
elsif ( $Self->{MappingConfig}->{Config}->{TestOption} eq 'Empty' ) {
$ReturnData = $Self->_Empty( Data => $Param{Data} );
}
else {
$ReturnData = $Param{Data};
}
# return result
return {
Success => 1,
Data => $ReturnData,
};
}
The Map function is the main part of each mapping module. It receives the mapping con-
figuration (rules) and the data in the original format (either OTRS or remote system for-
mat) and converts it to a new format, even if the structure of the data can be changed
during the mapping process.
In this particular example there are three rules to map the values. This rules are set in
the mapping configuration key TestOption and they are ToUpper, ToLower and Empty.
sub _ToUpper {
my ( $Self, %Param ) = @_;
my $ReturnData = {};
for my $Key ( sort keys %{ $Param{Data} } ) {
$ReturnData->{$Key} = uc $Param{Data}->{$Key};
}
return $ReturnData;
}
sub _ToLower {
my ( $Self, %Param ) = @_;
my $ReturnData = {};
for my $Key ( sort keys %{ $Param{Data} } ) {
$ReturnData->{$Key} = lc $Param{Data}->{$Key};
}
return $ReturnData;
}
sub _Empty {
my ( $Self, %Param ) = @_;
my $ReturnData = {};
for my $Key ( sort keys %{ $Param{Data} } ) {
$ReturnData->{$Key} = '';
}
return $ReturnData;
}
This are the helper functions that actually performs the string conversions.
102
1;
=cut
2.5.2.1.2. Configuration Example
There is the need to register this mapping module to be accessible in the OTRS GUI. This
can be done using the XML configuration below.
2.5.3. Invoker
The invoker is used to create a request from OTRS to a Remote System. This part of the
GI is in charge of perform necessary tasks in OTRS side, to gather the necessary data in
order to construct the request.
2.5.3.1. Invoker backend
Next we will show how to develop a new Invoker. Each invoker has to implement these
subroutines:
• new
• PrepareRequest
• HandleResponse
We should implement each one of this methods in order to be able to execute a request
using the request handler (Kernel/GenericInterface/Requester.pm).
2.5.3.1.1. Code example
In this section a sample invoker module is shown and each subroutine is explained.
# --
# Kernel/GenericInterface/Invoker/Test.pm - GenericInterface test data Invoker backend
# Copyright (C) 2001-2017 OTRS AG, http://otrs.com/
# --
# This software comes with ABSOLUTELY NO WARRANTY. For details, see
# the enclosed file COPYING for license information (AGPL). If you
# did not receive this file, see http://www.gnu.org/licenses/agpl.txt.
# --
package Kernel::GenericInterface::Invoker::Test::Test;
103
use strict;
use warnings;
our $ObjectManagerDisabled = 1;
This is common header that can be found in common OTRS modules. The class/package
name is declared via the package keyword. Invokers can not be instantiated by the Object
Manager.
sub new {
my ( $Type, %Param ) = @_;
$Self->{DebuggerObject} = $Param{DebuggerObject};
return $Self;
}
The constructor new creates a new instance of the class. According to the coding guide-
lines only objects of other classes not handled by the object manager that are needed in
this module have to be created in new.
sub PrepareRequest {
my ( $Self, %Param ) = @_;
# we need a TicketNumber
if ( !IsStringWithData( $Param{Data}->{TicketNumber} ) ) {
return $Self->{DebuggerObject}->Error( Summary => 'Got no TicketNumber' );
}
my %ReturnData;
$ReturnData{TicketNumber} = $Param{Data}->{TicketNumber};
# check Action
if ( IsStringWithData( $Param{Data}->{Action} ) ) {
$ReturnData{Action} = $Param{Data}->{Action} . 'Test';
}
return {
Success => 1,
Data => \%ReturnData,
};
}
104
The PrepareRequest function is used to handle and collect all needed data to be sent
into the request. Here we can receive data from the request handler, use it, extend it,
generate new data, and after that, we can transfer the results to the mapping layer.
For this example we are expecting to receive a ticket number. If there isn't then we use
the debugger method Error() that creates an entry in the debug log and also returns a
structure with the parameter Success as 0 and an error message as the passed Summary.
Also this example appends the word "Test" to the parameter Action and if GetSystemTime
is requested, it will fill the SystemTime parameter with the current system time. This part
of the code is to prepare the data to be sent. On a real invoker some calls to core modules
(Kernel/System/*.pm) should be made here.
If during any part of the PrepareRequest function the request need to be stop without
generating and error an entry in the debug log the following code can be used:
Using this, the Requester will understand that the request should not continue (it will
not be sent to Mapping layer and will also not be sent to the Network Transport). The
Requester will not send an error on the debug log, it will only silently stop.
sub HandleResponse {
my ( $Self, %Param ) = @_;
return $Self->{DebuggerObject}->Error(
Summary => 'Got response error, but no response error message!',
);
}
return {
Success => 0,
ErrorMessage => $Param{ResponseErrorMessage},
};
}
# we need a TicketNumber
if ( !IsStringWithData( $Param{Data}->{TicketNumber} ) ) {
# prepare TicketNumber
my %ReturnData = (
TicketNumber => $Param{Data}->{TicketNumber},
);
# check Action
if ( IsStringWithData( $Param{Data}->{Action} ) ) {
if ( $Param{Data}->{Action} !~ m{ \A ( .*? ) Test \z }xms ) {
return $Self->{DebuggerObject}->Error(
Summary => 'Got Action but it is not in required format!',
);
}
$ReturnData{Action} = $1;
}
105
return {
Success => 1,
Data => \%ReturnData,
};
}
The HandleResponse function is used to receive and process the data from the previous
request, that was made to the Remote System. This data already passed by Mapping
layer, to transform it from Remote System format to OTRS format (if needed).
For this particular example it checks the ticket number again and check if the action ends
with the word 'Test' (as was done in the PrepareRequest function).
Note
This invoker is only used for tests, a real invoker will check if the response was on
the format described by the Remote System and can perform some actions like:
call another invoker, perform a call to a Core Module, update the database, send
an error, etc.
=back
=cut
2.5.3.1.2. Configuration Example
There is the need to register this invoker module to be accessible in the OTRS GUI. This
can be done using the XML configuration below.
2.5.4. Operation
The operation is used to perform an action within OTRS. This action is requested by the
external system and can include special parameters in order to correctly execute the
action. After the action is performed, OTRS sends a defined confirmation to the external
system.
2.5.4.1. Operation backend
Next we will show how to develop a new Operation, each operation has to implement
these subroutines:
106
• new
• Run
We should implement each one of this methods in order to be able to execute the action
handled by the provider (Kernel/GenericInterface/Provider.pm).
2.5.4.1.1. Code example
In this section a sample operation module is shown and each subroutine is explained.
# --
# Kernel/GenericInterface/Operation/Test/Test.pm - GenericInterface test operation backend
# Copyright (C) 2001-2017 OTRS AG, http://otrs.com/
# --
# This software comes with ABSOLUTELY NO WARRANTY. For details, see
# the enclosed file COPYING for license information (AGPL). If you
# did not receive this file, see http://www.gnu.org/licenses/agpl.txt.
# --
package Kernel::GenericInterface::Operation::Test::Test;
use strict;
use warnings;
our $ObjectManagerDisabled = 1;
This is common header that can be found in common OTRS modules. The class/package
name is declared via the package keyword.
We also include VariableCheck module to perform certain validation over some variables.
Operations can not be instantiated by the Object Manager.
sub new {
my ( $Type, %Param ) = @_;
my $Self = {};
bless( $Self, $Type );
$Self->{$Needed} = $Param{$Needed};
}
return $Self;
}
The constructor new creates a new instance of the class. According to the coding guide-
lines only objects of other classes not handled by the object manager that are needed in
this module have to be created in new.
sub Run {
my ( $Self, %Param ) = @_;
107
return $Self->{DebuggerObject}->Error(
Summary => 'Got Data but it is not a hash ref in Operation Test backend)!'
);
}
return {
Success => 0,
ErrorMessage => "Error message for error code: $Param{Data}->{TestError}",
Data => {
ErrorData => $Param{Data}->{ErrorData},
},
};
}
# copy data
my $ReturnData;
# return result
return {
Success => 1,
Data => $ReturnData,
};
}
The Run function is the main part of each operation. It receives all internal mapped data
from remote system needed by the provider to execute the action, it performs the action
and returns the result to the provider to be external mapped and deliver back to the
remote system.
This particular example returns the same data as came from the remote system, unless
TestError parameter is passed. In this case it returns an error.
1;
=cut
2.5.4.1.2. Configuration Example
There is the need to register this operation module to be accessible in the OTRS GUI. This
can be done using the XML configuration below.
108
<Item Key="Controller">Test</Item>
<Item Key="ConfigDialog">AdminGenericInterfaceOperationDefault</Item>
</Hash>
</Setting>
</ConfigItem>
Unit Test for Generic Interface operations does not differs from other unit tests but it is
needed to consider testing locally, but also simulating a remote connection. It is a good
practice to test both separately since results could be slightly different.
To learn more about unit tests, please take a look to the Unit Test Chapter.
# --
# Copyright (C) 2001-2017 OTRS AG, http://otrs.com/
# --
# This software comes with ABSOLUTELY NO WARRANTY. For details, see
# the enclosed file COPYING for license information (AGPL). If you
# did not receive this file, see http://www.gnu.org/licenses/agpl.txt.
# --
## no critic (Modules::RequireExplicitPackage)
use strict;
use warnings;
use utf8;
use Kernel::GenericInterface::Debugger;
use Kernel::GenericInterface::Operation::Test::Test;
# Skip SSL certificate verification (RestoreDatabase must not be used in this test).
$Kernel::OM->ObjectParamAdd(
'Kernel::System::UnitTest::Helper' => {
SkipSSLVerify => 1,
},
);
my $Helper = $Kernel::OM->Get('Kernel::System::UnitTest::Helper');
my $UserID = $Kernel::OM->Get('Kernel::System::User')->UserLookup(
UserLogin => $UserLogin,
);
my $WebserviceID = $WebserviceObject->WebserviceAdd(
109
Name => $WebserviceName,
Config => {
Debugger => {
DebugThreshold => 'debug',
},
Provider => {
Transport => {
Type => '',
},
},
},
ValidID => 1,
UserID => 1,
);
$Self->True(
$WebserviceID,
"Added Web Service",
);
# get remote host with some precautions for certain unit test systems
my $Host = $Helper->GetTestHTTPHostname();
my $ConfigObject = $Kernel::OM->Get('Kernel::Config');
my $WebserviceConfig = {
Description =>
'Test for Ticket Connector using SOAP transport backend.',
Debugger => {
DebugThreshold => 'debug',
TestMode => 1,
},
Provider => {
Transport => {
Type => 'HTTP::SOAP',
Config => {
MaxLength => 10000000,
NameSpace => 'http://otrs.org/SoapTestInterface/',
Endpoint => $RemoteSystem,
},
},
Operation => {
Test => {
Type => 'Test::Test',
},
},
},
Requester => {
Transport => {
Type => 'HTTP::SOAP',
Config => {
NameSpace => 'http://otrs.org/SoapTestInterface/',
Encoding => 'UTF-8',
Endpoint => $RemoteSystem,
},
},
Invoker => {
Test => {
Type => 'Test::TestSimple'
, # requester needs to be Test::TestSimple in order to simulate a request
to a remote system
},
},
110
},
};
# debugger object
my $DebuggerObject = Kernel::GenericInterface::Debugger->new(
DebuggerConfig => {
DebugThreshold => 'debug',
TestMode => 1,
},
WebserviceID => $WebserviceID,
CommunicationType => 'Provider',
);
$Self->Is(
ref $DebuggerObject,
'Kernel::GenericInterface::Debugger',
'DebuggerObject instantiate correctly',
);
TEST:
for my $Test (@Tests) {
$Self->Is(
111
"Kernel::GenericInterface::Operation::Test::$Test->{Operation}",
ref $LocalObject,
"$Test->{Name} - Create local object",
);
my %Auth = (
UserLogin => $UserLogin,
Password => $Password,
);
if ( IsHashRefWithData( $Test->{Auth} ) ) {
%Auth = %{ $Test->{Auth} };
}
# check result
$Self->Is(
'HASH',
ref $LocalResult,
"$Test->{Name} - Local result structure is valid",
);
# check result
$Self->Is(
'HASH',
ref $RequesterResult,
"$Test->{Name} - Requester result structure is valid",
);
$Self->Is(
$RequesterResult->{Success},
$Test->{SuccessRequest},
"$Test->{Name} - Requester successful result",
);
112
);
# also delete any other added data during the this test, since RestoreDatabase must not be
used.
1;
WSDL files contains the definitions of the web services and its operations for SOAP
messages, in case we will extend development/webservices/GenericTickeConnec-
torSOAP.wsdl in some places:
Port Type:
<wsdl:portType name="GenericTicketConnector_PortType">
<!-- ... -->
<wsdl:operation name="Test">
<wsdl:input message="tns:TestRequest"/>
<wsdl:output message="tns:TestResponse"/>
</wsdl:operation>
<!-- ... -->
Binding:
<wsdl:binding name="GenericTicketConnector_Binding"
type="tns:GenericTicketConnector_PortType">
<soap:binding style="document" transport="http://schemas.xmlsoap.org/soap/http"/>
<!-- ... -->
<wsdl:operation name="Test">
<soap:operation soapAction="http://www.otrs.org/TicketConnector/Test"/>
<wsdl:input>
<soap:body use="literal"/>
</wsdl:input>
<wsdl:output>
<soap:body use="literal"/>
</wsdl:output>
</wsdl:operation>
<!-- ... -->
</wsdl:binding>
Type:
<wsdl:types>
<xsd:schema targetNamespace="http://www.otrs.org/TicketConnector/"
xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<!-- ... -->
<xsd:element name="Test">
<xsd:complexType>
<xsd:sequence>
<xsd:element minOccurs="0" name="Param1" type="xsd:string"/>
<xsd:element minOccurs="0" name="Param2"
type="xsd:positiveInteger"/>
</xsd:sequence>
</xsd:complexType>
</xsd:element>
<xsd:element name="TestResponse">
<xsd:complexType>
<xsd:sequence>
<xsd:element maxOccurs="unbounded" minOccurs="1" name="Attribute1"
type="xsd:string"/>
</xsd:sequence>
</xsd:complexType>
</xsd:element>
<!-- ... -->
113
</xsd:schema>
</wsdl:types>
Message:
WADL files contains the definitions of the web services and its operations for REST in-
terface, add a new resource to development/webservices/GenericTickeConnector-
REST.wadl.
<resources base="http://localhost/otrs/nph-genericinterface.pl/Webservice/
GenericTicketConnectorREST">
<!-- ... -->
<resource path="Test" id="Test">
<doc xml:lang="en" title="Test"/>
<param name="Param1" type="xs:string" required="false" default="" style="query"
xmlns:xs="http://www.w3.org/2001/XMLSchema"/>
<param name="Param2" type="xs:string" required="false" default="" style="query"
xmlns:xs="http://www.w3.org/2001/XMLSchema"/>
<method name="GET" id="GET_Test">
<doc xml:lang="en" title="GET_Test"/>
<request/>
<response status="200">
<representation mediaType="application/json; charset=UTF-8"/>
</response>
</method>
</resource>
</resource>
<!-- ... -->
</resources>
Web services can be imported into OTRS by a YAML with with a predefined structure in
this case we will extend development/webservices/GenericTickeConnectorSOAP.yml
for a SOAP web service.
Provider:
Operation:
# ...
Test:
Description: This is only a test
MappingInbound: {}
MappingOutbound: {}
Type: Test::Test
Web services can be imported into OTRS by a YAML with with a predefined structure in
this case we will extend development/webservices/GenericTickeConnectorREST.yml
for a REST web service.
114
Provider:
Operation:
# ...
Test:
Description: This is only a test
MappingInbound: {}
MappingOutbound: {}
Type: Test::Test
# ...
Transport:
Config:
# ...
RouteOperationMapping:
# ..
Test:
RequestMethod:
- GET
Route: /Test
Each daemon module must implement a common API in order to be correctly called by
the OTRS Daemon and be a semi persistent process in the system. Persistent process
could grow in size and memory usage over the time and normally they do not respond
to changes in the configuration. That is why the daemon modules should implement a
discard mechanism to be stopped and re-spawned again from time to time, freeing system
resources and re-reading the configuration.
A daemon module could be an all-in-one solution to perform a certain job, but there could
be the case that a solution requires different daemon modules due to its complexity.
That is exactly the case of the OTRS Scheduler Daemon that is split into several daemon
modules including some daemon modules for task management and task execution.
It is not always necessary to create a new daemon module to perform certain task, usually
the OTRS Scheduler Daemon can deal with the majority of them, either if it is an OTRS
function that needs to be executed on a regular basis (CRON like) or if it's triggered by
an OTRS event, the OTRS Scheduler should be capable to deal with it out of the box or
by adding a new scheduler task worker module.
115
</Hash>
</Value>
</Setting>
The following code implements a daemon module that displays the system time every
2 seconds.
# --
# Copyright (C) 2001-2017 OTRS AG, http://otrs.com/
# --
# This software comes with ABSOLUTELY NO WARRANTY. For details, see
# the enclosed file COPYING for license information (AGPL). If you
# did not receive this file, see http://www.gnu.org/licenses/agpl.txt.
# --
package Kernel::System::Daemon::DaemonModules::TestDaemon;
use strict;
use warnings;
use utf8;
our @ObjectDependencies = (
'Kernel::Config',
'Kernel::System::Cache',
'Kernel::System::DB',
);
This is common header that can be found in most OTRS modules. The class/package name
is declared via the package keyword.
In this case we are inheriting from BaseDaemon class, and the object manager dependen-
cies are set.
sub new {
my ( $Type, %Param ) = @_;
$Self->{Debug} = $Param{Debug};
$Self->{DaemonName} = 'Daemon: TestDaemon';
return $Self;
}
116
The constructor new creates a new instance of the class. Some used objects are also
created here. It is highly recommended to disable in-memory cache in daemon modules
especially if OTRS runs in a cluster environment.
In order to make this daemon module to be executed every two seconds it is necessary
to define a sleep time accordingly, otherwise it will be executed as soon as possible.
Refreshing the daemon module from time to time is necessary in order to define when
it should be discarded.
For the following functions (PreRun, Run and PostRun) if they return false, the main OTRS
Daemon will discard the object and create a new one as soon as possible.
sub PreRun {
my ( $Self, %Param ) = @_;
sleep 10;
return;
}
The PreRun method is executed before the main daemon module method, and the its
purpose is to perform some test before the real operation. In this case a check to the data-
base is done (always recommended), otherwise it sleeps for 10 seconds. This is needed
in order to wait for DB connection to be reestablished.
sub Run {
my ( $Self, %Param ) = @_;
return 1;
}
The Run method is where the main daemon module code resides, in this case it only prints
the current time.
sub PostRun {
my ( $Self, %Param ) = @_;
sleep $Self->{SleepPost};
$Self->{DiscardCount}--;
if ( $Self->{Debug} ) {
print " $Self->{DaemonName} Discard Count: $Self->{DiscardCount}\n";
}
return 1;
}
The PostRun method is used to perform the sleeps (preventing the daemon module to be
executed too often) and also to manage the safe discarding of the object. Other operations
like verification or cleanup can be done here.
sub Summary {
117
my ( $Self, %Param ) = @_;
my %Summary = (
Header => 'Test Daemon Summary:',
Column => [
{
Name => 'SomeColumn',
DisplayName => 'Some Column',
Size => 15,
},
{
Name => 'AnotherColumn',
DisplayName => 'Another Column',
Size => 15,
},
# ...
],
Data => [
{
SomeColumn => 'Some Data 1',
AnotherColumn => 'Another Data 1',
},
{
SomeColumn => 'Some Data 2',
AnotherColumn => 'Another Data 2',
},
# ...
],
NoDataMesssage => '',
);
return \%Summary;
}
1;
End of file.
2.6.2. OTRS Scheduler
The OTRS Scheduler is a conjunction of daemon modules and task workers that runs
together in order to perform all needed OTRS tasks asynchronously from the web server
process.
SchedulerFutureTaskManager checks the tasks that are set to be executed just one time
in the future and sets this task to be executed in time. For example, when a Generic
Interface Invoker can not reach the remote server, it can self schedule to be run again
5 minutes later.
Whenever these tasks managers are not enough, a new daemon module can be created.
At a certain point of its Run() method it needs to call TaskAdd() from the chedulerDB
118
object to register a task, and as soon as it is registered, it will be executed in the next
free slot by the SchedulerTaskWorker.
In order to execute each task, the SchedulerTaskWorker calls a backend module (Task
Worker) to perform the specific task. The worker module is determined by the task type.
If a new task type is added, it will require a new task worker.
# --
# Copyright (C) 2001-2017 OTRS AG, http://otrs.com/
# --
# This software comes with ABSOLUTELY NO WARRANTY. For details, see
# the enclosed file COPYING for license information (AGPL). If you
# did not receive this file, see http://www.gnu.org/licenses/agpl.txt.
# --
package Kernel::System::Daemon::DaemonModules::SchedulerTaskWorker::TestWorker;
use strict;
use warnings;
our @ObjectDependencies = (
'Kernel::System::Log',
);
This is common header that can be found in most OTRS modules. The class/package name
is declared via the package keyword.
In this case we are inheriting from BaseTaskWorker class, and the object manager de-
pendencies are set.
sub new {
my ( $Type, %Param ) = @_;
my $Self = {};
bless( $Self, $Type );
$Self->{Debug} = $Param{Debug};
$Self->{WorkerName} = 'Worker: Test';
return $Self;
}
sub Run {
my ( $Self, %Param ) = @_;
119
# Check task params.
my $CheckResult = $Self->_CheckTaskParams(
%Param,
NeededDataAttributes => [ 'NeededAtrribute1', 'NeededAtrribute2' ],
DataParamsRef => 'HASH', # or 'ARRAT'
);
my $Success;
my $ErrorMessage;
if ( $Self->{Debug} ) {
print " $Self->{WorkerName} executes task: $Param{TaskName}\n";
}
do {
$Success = $Kernel::OM->Get('Kernel::System::MyPackage')->Run(
Param1 => 'someparam',
);
};
if ( !$Success ) {
$Self->_HandleError(
TaskName => $Param{TaskName},
TaskType => 'Test',
LogMessage => "There was an error executing $Param{TaskName}: $ErrorMessage",
ErrorMessage => "$ErrorMessage",
);
}
return $Success;
}
The Run is the main method. A call to _CheckTaskParams() from the base class will save
some lines of code. Executing the task while capturing the STDERR is a very good practice,
since the OTRS Scheduler runs normally unattended, and saving all errors to a variable will
make it available for further processing. _HandleError() provides a common interface to
send the error messages as email to the recipient specified in the System Configuration.
1;
End of file.
2.7. Dynamic Fields
2.7.1. Overview
Dynamic Fields are custom fields that can be added to a screen to enhance and add
information to an object (e.g. a ticket or an article).
The Dynamic Fields are the evolution of the ticket and article Free Fields (TikcetFree-
Text, TicketFreeKey, TicketFreeTime, ArticleFreeText, ArticleFreeKey and Arti-
cleFreeTime) from older versions of OTRS.
120
From OTRS version 3.1 the old Free Fields has been replaced with the new Dynamic Fields.
For a better backward compatibility and data preservation when updating from previous
versions, a migration script has been developed to convert the existing Free Fields to
Dynamic Fields and to move their values from the ticket and article tables in the database
to new dynamic fields tables.
Note
Any custom development that uses Free Fields needs to be ported to the new
Dynamic Fields code structure, otherwise it will not work anymore. For this reason
is very important to know that only updated installations of OTRS 3.0 has the old
Free Fields converted to Dynamic Fields, new or clean installations of OTRS has
no Dynamic Fields defined "out of the box" and any Dynamic Field needed by the
custom development needs to be added.
The restriction on the number of the fields per ticket or article has been removed. This
means that a ticket or article could have as many fields as needed. And now it is also
possible to use the Dynamic Fields framework for other objects rather than just ticket or
article.
The new Dynamic Fields can store the same data types as the Free Fields (Text and Date/
Time), and they can be also defined as them (Single line input, drop-down and date-time),
but Dynamic Fields go beyond that, a new integer data type has been added and also
new options to define the fields like Multiple-line inputs, check-boxes, Multiple-select and
date (without time) fields. Each field type defines its own data type.
Due to its modular design each Dynamic Field type can be seen as a plug-in to a frame-
work, and this plug-in can be an OTRS standard package to extend the available types of
the Dynamic Fields or even to extend current Dynamic Field with more functions.
The following picture shows the architecture of the Dynamic Fields framework.
121
2.7.2.1. Dynamic Field Backend Modules
2.7.2.1.1. Dynamic Field (Backend)
Normally called as BackendObject in the frontend modules is the mediator between the
frontend modules and each specific Dynamic Field implementation or Driver. It defines a
Generic middle API for all Dynamic Field Drivers, and each Driver has the responsibility
to implement the middle API for the specific needs for the field.
The Dynamic Field Backend is the master controller of all the Drivers. Each function in
this module is responsible to check the required parameters and call the same function
in the specific Driver according to the Dynamic Field Configuration parameter received.
This module is also responsible to call specific functions on each Object Type Delegate
(like Ticket or Article) e.g. to add a history entry or fire an event.
A Dynamic Field Driver is the implementation of the Dynamic Field. Each Driver must
implement all the mandatory functions specified in the Backend (there are some functions
that depends on a behavior and it is not needed to implement those if the Dynamic Field
does not have that particular behavior).
A Driver is responsible to know how to get its own value or values from a web request,
or from a profile (like a search profile). It also needs to know the HTML code to render
the field in edit or display screens, or how to interact with the stats module, among other
functions.
It exists some base drivers like Base.pm, BaseText.pm, BaseSelect.pm and BaseDate-
Time.pm, that implements common functions for certain drivers (e.g. Driver TextArea.pm
uses BaseText.pm that also uses Base.pm then TextArea only needs to implement the
functions that are missing in Base.pm and BateText.pm or the ones that are special cas-
es).
• Base.pm
• BaseText.pm
• Text.pm
• TextArea.pm
• BaseSelect.pm
• Dropdown.pm
• Multiselect.pm
• BaseDateTime.pm
• DateTime.pm
• Date.pm
• Checkbox.pm
122
2.7.2.1.3. Object Type Delegate
An Object Type Delegate is responsible to perform specific functions on the object linked
to the dynamic field. These functions are triggered by the Backend object as they are
needed.
Normally a Dynamic Field Driver needs its own Admin Module (Admin Dialog) to define its
properties. This dialog might differ from other Drivers. But this is not mandatory, Drivers
can share Admin Dialogs, if they can provide needed information for all the Drivers that
are linked to them, no matter if they are from different type. What is mandatory is that
each Driver must be linked to an Admin Dialog (e.g. Text and TextArea Drivers share Ad-
minDynamicFieldText.pm Admin Dialog, and Date and Date/Time Drivers share Admin-
DynamicFieldDateTime.pm Admin Dialog).
Admin Dialogs follow the normal OTRS Admin Module rules and architecture. But for stan-
dardization all configuration common parts to all Dynamic Fields should have the same
look and feel among all Admin Dialogs.
Note
Each Admin Dialog needs its corresponding HTML template file (.tt).
This module is responsible to manage the Dynamic Field definitions. It provides the ba-
sic API for add, change, delete, list and get Dynamic Fields. This module is located in
$OTRS_HOME/Kernel/System/DynamicField.pm.
This module is responsible to read and write Dynamic Field values into the form and into
the database. This module is highly used by the Drivers and is located in $OTRS_HOME/
Kernel/System/DynamicFieldValue.pm.
dynamic_field: Used by the Core Module DynamicField.pm, it stores the Dynamic Field
definitions.
123
2.7.2.5. Dynamic Fields Configuration Files
The Backend module needs a way to know which Drivers exists and since the amount of
Drivers can be easily extended. The easiest way to manage them is to use the system
configuration, where the information of Dynamic Field Drivers and Object Type Drivers
can be stored and extended.
The master Admin Module also needs to know this information about the available Dy-
namic Field Drivers to use the Admin Dialog linked with, to create or modify the Dynamic
Fields.
Frontend modules need to read the system configuration to know which Dynamic Fields
are active for each screen and which ones are also mandatory. For example: Tick-
et::Frontend::AgentTicketPhone###DynamicField stores the active, mandatory and
inactive Dynamic Fields for New Phone Ticket Screen.
The following picture shows a simple example of how the Dynamic Fields interact with
other OTRS framework parts.
124
Figure 3.3. Dynamic Field Interaction
The first step is that the Frontend module reads the configured Dynamic Fields. For ex-
ample AgentTicketNote should read Ticket::Frontend::AgentTicketNote###Dynam-
icField setting. This setting can be used as the filter parameter for DynamicField Core
Module function DynamicFieldListGet(). The screen can store the results of this func-
tion to have the list of the Dynamic Fields activated for this particular screen.
Next, the screen should try to get the values from the web request. It can use the Back-
end Object function EditFieldValueGet() for this purpose, and can use this values to
trigger ACLs. The Backend Object will use each Driver to perform the specific actions for
all functions.
To continue, the screen should get the HTML for each field to display it. The Backend Object
function EditFieldRender() can be used to perform this action and the ACLs restriction
as well as the Values from the web request can be passed to this function in order to get
better results. In case of a submit the screen could also use the BackendObject function
EditFieldValueValidate() to check the mandatory fields.
Note
Other screens could use DisplayFieldRender() instead of EditFieldRender()
if the screen only shows the field value, and in such case no value validation is
needed.
125
To store the value of the Dynamic Field is necessary to get the Object ID. For this example if
the Dynamic Field is linked to a ticket object, the screen should already have the TicketID,
otherwise if the field is linked to an article object in order to set the value of the field is
necessary to create the article first. ValueSet() from the Backend Object can be used
to set the Dynamic Field value.
In summary the Frontend modules does not need to know how each Dynamic Field works
internally to get or set their values or to display them. It just needs to call the Backend
Object module and use the fields in a generic way.
To register the new field in the Backend (or new Admin Dialogs in the framework if
needed) and be able to create instances or it.
This is necessary, even if the "other object" does not require any specific data handling
in its functions (e.g. after a value is set). All Object Type Delegates must implement the
functions that the Backend requires.
Take a look in the current Object Type Delegates to implement the same functions, even
if they just return a successful value for the "other object".
To register the new field in the Backend (or new Admin Dialogs in the framework if
needed) and be able to create instances or it. And make the needed settings to show,
hide or show the Dynamic Fields as Mandatory in the new screens.
126
2.7.4.3. Create a New package to use Dynamic Fields
To create a package to use existing dynamic fields is necessary to:
To give the end user the possibility to show, hide or show the Dynamic Fields as Manda-
tory in the new screens.
The easiest way to do this, is to extend the current field files. For this it is necessary
to create a new Backend extension file that defines the new functions and create also
Drivers extensions that implement these new functions for each field. These new drivers
will only need to implement the new functions since the original drivers takes care of the
standard functions. All these new files do not need a constructor as they will be loaded
as a base for the Backend object and the drivers.
The only restrictions are that the functions should be named different than the ones on
the Backend and Drivers, otherwise they will be overwritten with current objects.
Put the new Backend extension into the DynamicField directory (e.g. /$OTRS_HOME/
Kernel/System/DynamicField/NewPackageBackend.pm and its Drivers in /$OTRS_HOME/
Kernel/System/DynamicField/Driver/NewPackage*.pm).
New behaviors only need a small setting in the extensions configuration file.
2.7.4.5. Other Extensions
Other extensions could be a combination of the above examples.
127
Note
This new password field implementation is just for educational purposes, it does
not provide any level of security and is not recommended for production systems.
To create this new Dynamic Field we will create 4 files: a Configuration File (XML), to
register the modules, an Admin Dialog Module (Perl), to setup the field options, a template
module, for the Admin Dialog and a Dynamic Field Driver (Perl).
File Structure:
The configuration files are used to register the Dynamic Field Types (Driver) and the Object
Type Drivers for the BackendObject. They also store standard registrations for Admin
Modules in the framework.
2.7.5.1.1.1. Code Example:
In this section a configuration file for password Dynamic Field is shown and explained.
128
This setting registers the Password Dynamic Field Driver for the Backend module so it can
be included in the list of available Dynamic Fields Types. It also specify its own Admin
Dialog in the key ConfigDialog. This key is used by the Master Dynamic Field Admin
Module to manage this new Dynamic Field Type.
This is a standard module registration for the Password Admin Dialog in the Admin Inter-
face.
</otrs_config>
The Admin Dialogs are standard Admin modules to manage (add or edit) the Dynamic
Fields.
2.7.5.1.2.1. Code Example:
In this section an Admin Dialog for password dynamic field is shown and explained.
# --
# Kernel/Modules/AdminDynamicFieldPassword.pm - provides a dynamic fields password config
view for admins
# Copyright (C) 2001-2017 OTRS AG, http://otrs.com/
# --
# This software comes with ABSOLUTELY NO WARRANTY. For details, see
# the enclosed file COPYING for license information (AGPL). If you
# did not receive this file, see http://www.gnu.org/licenses/agpl.txt.
# --
package Kernel::Modules::AdminDynamicFieldPassword;
use strict;
use warnings;
This is common header that can be found in common OTRS modules. The class/package
name is declared via the package keyword.
sub new {
my ( $Type, %Param ) = @_;
129
my $Self = {%Param};
bless( $Self, $Type );
$Self->{DefaultValueMask} = '****';
return $Self;
}
The constructor new creates a new instance of the class. According to the coding guide-
lines objects of other classes that are needed in this module have to be created in new.
sub Run {
my ( $Self, %Param ) = @_;
if ( $Self->{Subaction} eq 'Add' ) {
return $Self->_Add(
%Param,
);
}
elsif ( $Self->{Subaction} eq 'AddAction' ) {
return $Self->_AddAction(
%Param,
);
}
if ( $Self->{Subaction} eq 'Change' ) {
return $Self->_Change(
%Param,
);
}
elsif ( $Self->{Subaction} eq 'ChangeAction' ) {
return $Self->_ChangeAction(
%Param,
);
}
return $Self->{LayoutObject}->ErrorScreen(
Message => "Undefined subaction.",
);
}
Run is the default function to be called by the web request. We try to make this function
as simple as possible and let the helper functions to do the "hard" work.
130
sub _Add {
my ( $Self, %Param ) = @_;
my %GetParam;
for my $Needed (qw(ObjectType FieldType FieldOrder)) {
$GetParam{$Needed} = $Self->{ParamObject}->GetParam( Param => $Needed );
if ( !$Needed ) {
return $Self->{LayoutObject}->ErrorScreen(
Message => "Need $Needed",
);
}
}
return $Self->_ShowScreen(
%Param,
%GetParam,
Mode => 'Add',
ObjectTypeName => $ObjectTypeName,
FieldTypeName => $FieldTypeName,
);
}
_Add function is also pretty simple, it just get some parameters from the web request and
call the _ShowScreen() function. Normally this function is not needed to be modified.
sub _AddAction {
my ( $Self, %Param ) = @_;
my %Errors;
my %GetParam;
if ( $GetParam{Name} ) {
if ( $DynamicFieldsList{ $GetParam{Name} } ) {
131
# add server error error class
$Errors{NameServerError} = 'ServerError';
$Errors{NameServerErrorMessage} = 'There is another field with the same name.';
}
}
if ( $GetParam{FieldOrder} ) {
for my $ConfigParam (
qw(
ObjectType ObjectTypeName FieldType FieldTypeName DefaultValue ValidID ShowValue
ValueMask
)
)
{
$GetParam{$ConfigParam} = $Self->{ParamObject}->GetParam( Param => $ConfigParam );
}
# uncorrectable errors
if ( !$GetParam{ValidID} ) {
return $Self->{LayoutObject}->ErrorScreen(
Message => "Need ValidID",
);
}
return $Self->_ShowScreen(
%Param,
%Errors,
%GetParam,
Mode => 'Add',
);
}
if ( !$FieldID ) {
return $Self->{LayoutObject}->ErrorScreen(
Message => "Could not create the new field",
);
}
132
return $Self->{LayoutObject}->Redirect(
OP => "Action=AdminDynamicField",
);
}
The _AddAction function gets the configuration parameters from a new dynamic field,
and it validates that the Dynamic Field name only contains letters and numbers. This
function could validate any other parameter.
Name, Label, FieldOrder, Validity are common parameters for all Dynamic Fields and
they are required. Each Dynamic Field has its specific configuration that must contain at
least the DefaultValue parameter. In this case it also have ShowValue and ValueMask
parameters for Password field.
If the field has the ability to store a fixed list of values they should be stored in the Pos-
sibleValues parameter inside the specific configuration hash.
As in other Admin Modules, if a parameter is not valid this function returns to the Add
screen highlighting the erroneous form fields.
sub _Change {
my ( $Self, %Param ) = @_;
my %GetParam;
for my $Needed (qw(ObjectType FieldType)) {
$GetParam{$Needed} = $Self->{ParamObject}->GetParam( Param => $Needed );
if ( !$Needed ) {
return $Self->{LayoutObject}->ErrorScreen(
Message => "Need $Needed",
);
}
}
if ( !$FieldID ) {
return $Self->{LayoutObject}->ErrorScreen(
Message => "Need ID",
);
}
return $Self->{LayoutObject}->ErrorScreen(
Message => "Could not get data for dynamic field $FieldID",
);
}
my %Config = ();
# extract configuration
if ( IsHashRefWithData( $DynamicFieldData->{Config} ) ) {
133
%Config = %{ $DynamicFieldData->{Config} };
}
return $Self->_ShowScreen(
%Param,
%GetParam,
%${DynamicFieldData},
%Config,
ID => $FieldID,
Mode => 'Change',
ObjectTypeName => $ObjectTypeName,
FieldTypeName => $FieldTypeName,
);
}
The _Change function is very similar to the _Add function but since this function is used to
edit an existing field it needs to validated the FieldID parameter and gather the current
Dynamic Field data.
sub _ChangeAction {
my ( $Self, %Param ) = @_;
my %Errors;
my %GetParam;
return $Self->{LayoutObject}->ErrorScreen(
Message => "Need ID",
);
}
if ( $GetParam{Name} ) {
if (
$DynamicFieldsList{ $GetParam{Name} } &&
$DynamicFieldsList{ $GetParam{Name} } ne $FieldID
)
{
134
$Errors{NameServerErrorMessage} = 'There is another field with the same name.';
}
}
if ( $GetParam{FieldOrder} ) {
for my $ConfigParam (
qw(
ObjectType ObjectTypeName FieldType FieldTypeName DefaultValue ValidID ShowValue
ValueMask
)
)
{
$GetParam{$ConfigParam} = $Self->{ParamObject}->GetParam( Param => $ConfigParam );
}
# uncorrectable errors
if ( !$GetParam{ValidID} ) {
return $Self->{LayoutObject}->ErrorScreen(
Message => "Need ValidID",
);
}
return $Self->{LayoutObject}->ErrorScreen(
Message => "Could not get data for dynamic field $FieldID",
);
}
return $Self->_ShowScreen(
%Param,
%Errors,
%GetParam,
ID => $FieldID,
Mode => 'Change',
);
}
# update dynamic field (FieldType and ObjectType cannot be changed; use old values)
my $UpdateSuccess = $Self->{DynamicFieldObject}->DynamicFieldUpdate(
ID => $FieldID,
Name => $GetParam{Name},
Label => $GetParam{Label},
FieldOrder => $GetParam{FieldOrder},
FieldType => $DynamicFieldData->{FieldType},
135
ObjectType => $DynamicFieldData->{ObjectType},
Config => $FieldConfig,
ValidID => $GetParam{ValidID},
UserID => $Self->{UserID},
);
if ( !$UpdateSuccess ) {
return $Self->{LayoutObject}->ErrorScreen(
Message => "Could not update the field $GetParam{Name}",
);
}
return $Self->{LayoutObject}->Redirect(
OP => "Action=AdminDynamicField",
);
}
_ChangeAction() is very similar to _AddAction(), but adapted for the update of an ex-
isting field instead of creating a new one.
sub _ShowScreen {
my ( $Self, %Param ) = @_;
$Param{DisplayFieldName} = 'New';
if ( $Param{Mode} eq 'Change' ) {
$Param{ShowWarning} = 'ShowWarning';
$Param{DisplayFieldName} = $Param{Name};
}
# header
my $Output = $Self->{LayoutObject}->Header();
$Output .= $Self->{LayoutObject}->NavigationBar();
# when adding we need to create an extra order number for the new field
if ( $Param{Mode} eq 'Add' ) {
# get the last element from the order list and add 1
my $LastOrderNumber = $DynamicfieldOrderList[-1];
$LastOrderNumber++;
my $DynamicFieldOrderSrtg = $Self->{LayoutObject}->BuildSelection(
Data => \@DynamicfieldOrderList,
Name => 'FieldOrder',
SelectedValue => $Param{FieldOrder} || 1,
PossibleNone => 0,
Class => 'W50pc Validate_Number',
);
my %ValidList = $Self->{ValidObject}->ValidList();
136
Name => 'ValidID',
SelectedID => $Param{ValidID} || 1,
PossibleNone => 0,
Translation => 1,
Class => 'W50pc',
);
# generate output
$Output .= $Self->{LayoutObject}->Output(
TemplateFile => 'AdminDynamicFieldPassword',
Data => {
%Param,
ValidityStrg => $ValidityStrg,
DynamicFieldOrderSrtg => $DynamicFieldOrderSrtg,
DefaultValue => $DefaultValue,
ShowValueStrg => $ShowValueStrg,
ValueMask => $Param{ValueMask} || $Self->{DefaultValueMask},
},
);
$Output .= $Self->{LayoutObject}->Footer();
return $Output;
}
1;
The _ShowScreen function is used to set and define the HTML elements and blocks from
a template to generate the Admin Dialog HTML code.
The template is the place where the HTML code of the dialog is stored.
2.7.5.1.3.1. Code Example:
In this section an Admin Dialog template for the password Dynamic Field is shown and
explained.
# --
# AdminDynamicFieldPassword.tt - provides HTML form for AdminDynamicFieldPassword
# Copyright (C) 2001-2017 OTRS AG, http://otrs.com/
# --
# This software comes with ABSOLUTELY NO WARRANTY. For details, see
# the enclosed file COPYING for license information (AGPL). If you
# did not receive this file, see http://www.gnu.org/licenses/agpl.txt.
# --
137
<div class="Clear"></div>
<div class="SidebarColumn">
<div class="WidgetSimple">
<div class="Header">
<h2>[% Translate("Actions") | html %]</h2>
</div>
<div class="Content">
<ul class="ActionList">
<li>
<a href="[% Env("Baselink") %]Action=AdminDynamicField"
class="CallForAction"><span>[% Translate("Go back to overview") | html %]</span></a>
</li>
</ul>
</div>
</div>
</div>
This part of the code has the main box and also the actions side bar. No modifications
are needed in this section.
<div class="ContentColumn">
<form action="[% Env("CGIHandle") %]" method="post" class="Validate
PreventMultipleSubmits">
<input type="hidden" name="Action" value="AdminDynamicFieldPassword" />
<input type="hidden" name="Subaction" value="[% Data.Mode | html %]Action" />
<input type="hidden" name="ObjectType" value="[% Data.ObjectType | html %]" />
<input type="hidden" name="FieldType" value="[% Data.FieldType | html %]" />
<input type="hidden" name="ID" value="[% Data.ID | html %]" />
In this section of the code is defined the right part of the dialog. Notice that the value of
the Action hidden input must match with the name of the Admin Dialog.
<div class="WidgetSimple">
<div class="Header">
<h2>[% Translate("General") | html %]</h2>
</div>
<div class="Content">
<div class="LayoutGrid ColumnsWithSpacing">
<div class="Size1of2">
<fieldset class="TableLike">
<label class="Mandatory" for="Name"><span class="Marker">*</
span> [% Translate("Name") | html %]:</label>
<div class="Field">
<input id="Name" class="W50pc [% Data.NameServerError |
html %] [% Data.ShowWarning | html %] Validate_Alphanumeric" type="text" maxlength="200"
value="[% Data.Name | html %]" name="Name"/>
<div id="NameError" class="TooltipErrorMessage"><p>[%
Translate("This field is required, and the value should be alphabetic and numeric
characters only.") | html %]</p></div>
<div id="NameServerError"
class="TooltipErrorMessage"><p>[% Translate(Data.NameServerErrorMessage) | html %]</p></
div>
<p class="FieldExplanation">[% Translate("Must be unique
and only accept alphabetic and numeric characters.") | html %]</p>
<p class="Warning Hidden">[% Translate("Changing this
value will require manual changes in the system.") | html %]</p>
</div>
<div class="Clear"></div>
138
<div id="LabelError" class="TooltipErrorMessage"><p>[%
Translate("This field is required.") | html %]</p></div>
<div id="LabelServerError"
class="TooltipErrorMessage"><p>[% Translate(Data.LabelServerErrorMessage) | html %]</p></
div>
<p class="FieldExplanation">[% Translate("This is the
name to be shown on the screens where the field is active.") | html %]</p>
</div>
<div class="Clear"></div>
<div class="SpacingTop"></div>
<label for="FieldTypeName">[% Translate("Field type") | html
%]:</label>
<div class="Field">
<input id="FieldTypeName" readonly="readonly"
class="W50pc" type="text" maxlength="200" value="[% Data.FieldTypeName | html %]"
name="FieldTypeName"/>
<div class="Clear"></div>
</div>
<div class="SpacingTop"></div>
<label for="ObjectTypeName">[% Translate("Object type") |
html %]:</label>
<div class="Field">
<input id="ObjectTypeName" readonly="readonly"
class="W50pc" type="text" maxlength="200" value="[% Data.ObjectTypeName | html %]"
name="ObjectTypeName"/>
<div class="Clear"></div>
</div>
</fieldset>
</div>
</div>
</div>
</div>
This first widget contains the common form attributes for the Dynamic Fields. For consis-
tency with other Dynamic Fields is recommended to leave this part of the code unchanged.
<div class="WidgetSimple">
<div class="Header">
<h2>[% Translate(Data.FieldTypeName) | html %] [% Translate("Field
Settings") | html %]</h2>
</div>
<div class="Content">
<fieldset class="TableLike">
139
<label for="DefaultValue">[% Translate("Default value") | html %]:</
label>
<div class="Field">
<input id="DefaultValue" class="W50pc" type="text"
maxlength="200" value="[% Data.DefaultValue | html %]" name="DefaultValue"/>
<p class="FieldExplanation">[% Translate("This is the default
value for this field.") | html %]</p>
</div>
<div class="Clear"></div>
</fieldset>
</div>
</div>
The second widget has the Dynamic Field specific form attributes. This is the place where
new attributes can be set and it could use JavaScript and AJAX technologies to make it
more easy or friendly for the end user.
<fieldset class="TableLike">
<div class="Field SpacingTop">
<button type="submit" class="Primary" value="[% Translate("Save") | html
%]">[% Translate("Save") | html %]</button>
[% Translate("or") | html %]
<a href="[% Env("Baselink") %]Action=AdminDynamicField">[%
Translate("Cancel") | html %]</a>
</div>
<div class="Clear"></div>
</fieldset>
</form>
</div>
</div>
[% WRAPPER JSOnDocumentComplete %]
<script type="text/javascript">//<![CDATA[
$('.ShowWarning').bind('change keyup', function (Event) {
$('p.Warning').removeClass('Hidden');
});
Core.Agent.Admin.DynamicField.ValidationInit();
//]]></script>
[% END %]
The final part of the file contains the "Submit" button and the "Cancel" link, as well as
other needed JavaScript code.
140
2.7.5.1.4. Dynamic Field Driver Example
The driver is the Dynamic Field. It contains several functions that are used wide in the
OTRS framework. A driver can inherit some functions form base classes, for example
TextArea driver inherits most of the functions from Base.pm and BaseText.pm and it only
implements the functions that requires different logic or results. Checkbox field driver only
inherits from Base.pm as all other functions are very different from any other Base driver.
Note
Please refer to the Perl On-line Documentation (POD) of the module /Kernel/Sys-
tem/DynmicField/Backend.pm to have the list of all attributes and possible return
data for each function.
2.7.5.1.4.1. Code Example:
In this section the Password Dynamic Field driver is shown and explained. This driver in-
herits some functions from Base.pm and BaseText.pm and only implements the functions
that needs different results.
# --
# Kernel/System/DynamicField/Driver/Password.pm - Driver for DynamicField Password backend
# Copyright (C) 2001-2017 OTRS AG, http://otrs.com/
# --
# This software comes with ABSOLUTELY NO WARRANTY. For details, see
# the enclosed file COPYING for license information (AGPL). If you
# did not receive this file, see http://www.gnu.org/licenses/agpl.txt.
# --
package Kernel::System::DynamicField::Driver::Password;
use strict;
use warnings;
our @ObjectDependencies = (
'Kernel::Config',
'Kernel::System::DynamicFieldValue',
'Kernel::System::Main',
);
This is the common header that can be found in common OTRS modules. The class/pack-
age name is declared via the package keyword. Notice that BaseText is used as base
class.
sub new {
my ( $Type, %Param ) = @_;
141
# get the Dynamic Field Backend custom extensions
my $DynamicFieldDriverExtensions
= $Kernel::OM->Get('Kernel::Config')-
>Get('DynamicFields::Extension::Driver::Password');
EXTENSION:
for my $ExtensionKey ( sort keys %{$DynamicFieldDriverExtensions} ) {
%{ $Self->{Behaviors} } = (
%{ $Self->{Behaviors} },
%{ $Extension->{Behaviors} }
);
}
}
return $Self;
}
The constructor new creates a new instance of the class. According to the coding guide-
lines objects of other classes that are needed in this module have to be created in new.
It is important to define the behaviors correctly as the field might or might not be used in
certain screens, functions that depends on behaviors that are not active for this particular
field might not be needed to be implemented.
Note
Drivers are created only by the BackendObject and not directly from any other
module.
sub EditFieldRender {
my ( $Self, %Param ) = @_;
my $Value = '';
142
}
$Value = $Param{Value} if defined $Param{Value};
my $HTMLString = <<"EOF";
<input type="password" class="$FieldClass" id="$FieldName" name="$FieldName"
title="$FieldLabel" value="$Value" />
EOF
if ( $Param{Mandatory} ) {
my $DivID = $FieldName . 'Error';
if ( $Param{ServerError} ) {
my $Data = {
Field => $HTMLString,
Label => $LabelString,
};
return $Data;
143
}
This function is the responsible to create the HTML representation of the field and its label,
and is used in the edit screens like AgentTicketPhone, AgentTicketNote, etc.
sub DisplayValueRender {
my ( $Self, %Param ) = @_;
my $Value;
my $Title;
# HTMLOutput transformations
if ( $Param{HTMLOutput} ) {
$Value = $Param{LayoutObject}->Ascii2Html(
Text => $Value,
Max => $Param{ValueMaxChars} || '',
);
$Title = $Param{LayoutObject}->Ascii2Html(
Text => $Title,
Max => $Param{TitleMaxChars} || '',
);
}
else {
if ( $Param{ValueMaxChars} && length($Value) > $Param{ValueMaxChars} ) {
$Value = substr( $Value, 0, $Param{ValueMaxChars} ) . '...';
}
if ( $Param{TitleMaxChars} && length($Title) > $Param{TitleMaxChars} ) {
$Title = substr( $Title, 0, $Param{TitleMaxChars} ) . '...';
}
}
return $Data;
}
DisplayValueRender() function returns the field value as a plain text as well as its title
(both can be translated). For this particular example we are checking if the password
should be revealed or display a predefined mask by a configuration parameter in the
Dynamic Field.
144
sub ReadableValueRender {
my ( $Self, %Param ) = @_;
my $Value;
my $Title;
return $Data;
}
1;
=back
=cut
2.7.5.1.4.2. Other Functions:
The following are other functions that are might needed if the new Dynamic Field does not
inherit from other classes. To see the complete code of this functions please take a look
directly into the files Kernel/System/DynamicField/Driver/Base.pm and Kernel/Sys-
tem/DynamicField/Driver/BaseText.pm
145
sub ValueGet { ... }
This function retrieves the value from the field on a specified Object. In this case we are
returning the first text value, since the field only stores one text value at time.
ValueSet() is used to store a Dynamic Field value. In this case this field only stores
one text type value. Other fields could store more than one value on either ValueText,
ValueDateTime or ValueInt format.
This function is used to delete one field value attached to a particular object ID. For ex-
ample if the instance of an object is to be deleted, then there is no reason to have the
field value stored in the database for that particular object instance.
AllValuesDelete() function is used to delete all values from a certain Dynamic Field.
This function is very useful when a Dynamic Field is going to be deleted.
The ValueValidate() function is used to check if the value is consistent to its type.
This function is used by TicketSearch core module to build the internal query to search
for a ticket based on this field as a search parameter.
EditFieldValueGet() is a function used in the edit screens of OTRS and its purpose is
to get the value of the field, either from a template like generic agent profile or from a
web request. This function gets the web request in the $Param{ParamObject}, that is a
copy of the ParamObject of the Frontend Module or screen.
There are two return formats for this function, the normal: that is just the raw value or a
structure: that is the pair field name => field value. For example a Date Dynamic Field
returns normally the date as string, and if it should return a structure it returns a pair for
each part of the date in the hash.
146
If the result should be a structure then, normally this is used to store its values in a tem-
plate, like a generic agent profile. For example a date field uses several HTML components
to build the field, like the "Used" check-box and selects for year, month, day etc.
This function should provide at least a method to validate if the field is empty, and return
an error if the field is empty and mandatory, but it can also do more validations for other
kind of fields, like if the option selected is valid, or if a date should be only in the past etc.
It can provide a custom error message also.
This function is used by ticket search dialog and it is similar to EditFieldRander(), but
normally on a search screen small changes has to be done for all fields. For this example
we use a HTML text input instead of a password input. In other fields like Dropdown field
is displayed as a Multiple select in order to let the user search for more than one value
at a time.
Very similar to EditFieldValueGet(), but uses a different name prefix, adapted for the
search dialog screen.
SearchFieldParameterBuild() is used also by the ticket search dialog to set the correct
operator and value to do the search on this field. It also returns how the value should be
displayed in the used search attributes in the results page.
This function is used by the stats modules. It includes the field definition in the stats
format. For fields with fixed values it also includes all this possible values and if they can
be translated, take a look to the BaseSelect driver code for an example how to implement
those.
The TemplateValueTypeGet() function is used to know how the Dynamic Field values
stored on a profile should be retrieved, as a SCALAR or as an ARRAY, and it also defines
the correct name of the field in the profile.
147
sub RandomValueSet { ... }
This function is used by otrs.FillDB.pl script to populate the database with some test
and random data. The value inserted by this function is not really relevant. The only
restriction is that the value must be compatible with the field value type.
Used by the notification modules. This function returns 1 if the field is present in the
$Param{ObjectAttributes} parameter and if it matches the given value.
To create this extension we will create 3 files: a Configuration File (XML) to register the
modules, a Backend extension (Perl) to define the new function, and a Text field Driver
extension (Perl) that implements the new function for Text fields.
File Structure:
The configuration files are used to register the extensions for the Backend and Drivers as
well as new behaviors for each drivers.
Note
If a driver is extended with a new function, the backend will need also an extension
for that function.
2.7.6.1.1.1. Code Example:
In this section a configuration file for Foo extension is shown and explained.
148
This is the normal header for a configuration file.
This setting registers the extension in the Backend object. The module will be loaded from
Backend as a base class.
This is the registration for an extension in the Text Dynamic Field Driver. The module will
be loaded as a base class in the Driver. Notice also that new behaviors can be specified.
These extended behaviors will be added to the behaviors that the Driver has out of the
box, therefore a call to HasBehavior() to check for these new behaviors will be totally
transparent.
</otrs_config>
Backend extensions will be loaded transparently into the Backend itself as a base class.
All defined object and properties from the Backend will be accessible in the extension.
Note
All new functions defined in the Backend extension should be implemented in a
Driver extension.
2.7.6.1.2.1. Code Example:
In this section the Foo extension for Backend is shown and explained. The extension only
defines the function Foo().
# --
149
# Kernel/System/DynamicField/FooExtensionBackend.pm - Extension for DynamicField backend
# Copyright (C) 2001-2017 OTRS AG, http://otrs.com/
# --
# This software comes with ABSOLUTELY NO WARRANTY. For details, see
# the enclosed file COPYING for license information (AGPL). If you
# did not receive this file, see http://www.gnu.org/licenses/agpl.txt.
# --
package Kernel::System::DynamicField::FooExtensionBackend;
use strict;
use warnings;
=head1 NAME
Kernel::System::DynamicField::FooExtensionBackend
=head1 SYNOPSIS
=over 4
=cut
This is common header that can be found in common OTRS modules. The class/package
name is declared via the package keyword.
=item Foo()
my $Success = $BackendObject->Foo(
DynamicFieldConfig => $DynamicFieldConfig, # complete config of the
DynamicField
);
Returns:
$Success = 1; # or undef
=cut
sub Foo {
my ( $Self, %Param ) = @_;
return;
}
}
return;
}
150
# check DynamicFieldConfig (internally)
for my $Needed (qw(ID FieldType ObjectType)) {
if ( !$Param{DynamicFieldConfig}->{$Needed} ) {
$Kernel::OM->Get('Kernel::System::Log')->Log(
Priority => 'error',
Message => "Need $Needed in DynamicFieldConfig!",
);
return;
}
}
if ( !$Self->{$DynamicFieldBackend} ) {
$Kernel::OM->Get('Kernel::System::Log')->Log(
Priority => 'error',
Message => "Backend $Param{DynamicFieldConfig}->{FieldType} is invalid!",
);
return;
}
The function Foo() is only used for test purposes. First it checks the Dynamic Field con-
figuration, then it checks if the Dynamic Field Driver (type) exists and was already loaded.
To prevent the function call on a driver where is not defined it first check if the driver can
execute the function, then executes the function in the driver passing all parameters.
Note
It is also possible to skip the step that tests if the Driver can execute the function.
To do that it is necessary to implement a mechanism in the Frontend module to
require a special behavior on the Dynamic Field, and only after call the function
in the Backend object.
1;
=back
=cut
Driver extensions will be loaded transparently into the Driver itself as a base class. All
defined object and properties from the Driver will be accessible in the extension.
151
Note
All new functions implemented in the Driver extension should be defined in a Back-
end extension, as every function is called from the Backend Object.
2.7.6.1.3.1. Code Example:
In this section the Foo extension for Text field driver is shown and explained. The extension
only implements the function Foo().
# --
# Kernel/System/DynamicField/Driver/FooExtensionText.pm - Extension for DynamicField Text
Driver
# Copyright (C) 2001-2017 OTRS AG, http://otrs.com/
# --
# This software comes with ABSOLUTELY NO WARRANTY. For details, see
# the enclosed file COPYING for license information (AGPL). If you
# did not receive this file, see http://www.gnu.org/licenses/agpl.txt.
# --
package Kernel::System::DynamicField::Driver::FooExtensionText;
use strict;
use warnings;
=head1 NAME
Kernel::System::DynamicField::Driver::FooExtensionText
=head1 SYNOPSIS
=over 4
=cut
This is common header that can be found in common OTRS modules. The class/package
name is declared via the package keyword.
sub Foo {
my ( $Self, %Param ) = @_;
return 1;
}
The function Foo() has no special logic. It is only for testing and it always returns 1.
1;
=back
152
=cut
2.8. Email Handling
2.8.1. Ticket PostMaster Module
PostMaster modules are used during the PostMaster process. There are two kinds of Post-
Master modules: PostMasterPre (used after parsing an email) and PostMasterPost (used
after an email is processed and is in the database).
If you want to create your own postmaster filter, just create your own module. These
modules are located under Kernel/System/PostMaster/Filter/*.pm. For default mod-
ules see the admin manual. You just need two functions: new() and Run().
The following is an exemplary module to match emails and set X-OTRS-Headers (see doc/
X-OTRS-Headers.txt for more info).
Kernel/Config/Files/MyModule.xml:
# --
# Copyright (C) 2001-2017 OTRS AG, http://otrs.com/
# --
# This software comes with ABSOLUTELY NO WARRANTY. For details, see
# the enclosed file COPYING for license information (AGPL). If you
# did not receive this file, see http://www.gnu.org/licenses/agpl.txt.
# --
package Kernel::System::PostMaster::Filter::Example;
use strict;
use warnings;
our @ObjectDependencies = (
'Kernel::System::Log',
);
153
sub new {
my ( $Type, %Param ) = @_;
$Self->{Debug} = $Param{Debug} || 0;
return $Self;
}
sub Run {
my ( $Self, %Param ) = @_;
# get config options
my %Config = ();
my %Match = ();
my %Set = ();
if ($Param{JobConfig} && ref($Param{JobConfig}) eq 'HASH') {
%Config = %{$Param{JobConfig}};
if ($Config{Match}) {
%Match = %{$Config{Match}};
}
if ($Config{Set}) {
%Set = %{$Config{Set}};
}
}
# match 'Match => ???' stuff
my $Matched = '';
my $MatchedNot = 0;
for (sort keys %Match) {
if ($Param{GetParam}->{$_} && $Param{GetParam}->{$_} =~ /$Match{$_}/i) {
$Matched = $1 || '1';
if ($Self->{Debug} > 1) {
$Kernel::OM->Get('Kernel::System::Log')->Log(
Priority => 'debug',
Message => "'$Param{GetParam}->{$_}' =~ /$Match{$_}/i matched!",
);
}
}
else {
$MatchedNot = 1;
if ($Self->{Debug} > 1) {
$Kernel::OM->Get('Kernel::System::Log')->Log(
Priority => 'debug',
Message => "'$Param{GetParam}->{$_}' =~ /$Match{$_}/i matched NOT!",
);
}
}
}
# should I ignore the incoming mail?
if ($Matched && !$MatchedNot) {
for (keys %Set) {
if ($Set{$_} =~ /\[\*\*\*\]/i) {
$Set{$_} = $Matched;
}
$Param{GetParam}->{$_} = $Set{$_};
$Kernel::OM->Get('Kernel::System::Log')->Log(
Priority => 'notice',
Message => "Set param '$_' to '$Set{$_}' (Message-ID: $Param{GetParam}-
>{'Message-ID'}) ",
);
}
}
return 1;
}
1;
154
Figure 3.4. Email Processing Flow
155
Chapter 4. How to Publish Your
OTRS Extensions
1. Package Management
The OPM (OTRS Package Manager) is a mechanism to distribute software packages for
the OTRS framework via HTTP, FTP or file upload.
For example, the OTRS project offers OTRS modules like a calendar, a file manager or
web mail in OTRS packages via online repositories on our ftp servers. The packages can
be managed (install/upgrade/uninstall) via the admin interface.
1.1. Package Distribution
If you want to create an OPM online repository, just tell the OTRS framework where the
location is by activating the SysConfig setting Package::RepositoryList and adding the
new location there. Then you will have a new select option in the package manager.
In your repository, create an index file for your OPM packages. OTRS just reads this index
file and knows what packages are available.
1.2. Package Commands
You can use the following OPM commands over the admin interface or over bin/
otrs.PackageManager.pl to manage admin jobs for OPM packages.
1.2.1. Install
Install OPM packages.
• Web: http://localhost/otrs/index.pl?Action=AdminPackageManager
• CMD:
1.2.2. Uninstall
Uninstall OPM packages.
• Web: http://localhost/otrs/index.pl?Action=AdminPackageManager
• CMD:
1.2.3. Upgrade
Upgrade OPM packages.
156
• Web: http://localhost/otrs/index.pl?Action=AdminPackageManager
• CMD:
1.2.4. List
List all OPM packages.
• Web: http://localhost/otrs/index.pl?Action=AdminPackageManager
• CMD:
2. Package Building
If you want to create an OPM package (.opm) you need to create a spec file (.sopm) which
includes the properties of the package.
2.1.1. <Name>
The package name (required).
<Name>Calendar</Name>
2.1.2. <Version>
The package version (required).
<Version>1.2.3</Version>
2.1.3. <Framework>
The targeted framework version (3.2.x means e.g. 3.2.1 or 3.2.2) (required).
<Framework>3.2.x</Framework>
<Framework>3.0.x</Framework>
<Framework>3.1.x</Framework>
<Framework>3.2.x</Framework>
2.1.4. <Vendor>
The package vendor (required).
157
<Vendor>OTRS AG</Vendor>
2.1.5. <URL>
The vendor URL (required).
<URL>http://otrs.org/</URL>
2.1.6. <License>
The license of the package (required).
2.1.7. <ChangeLog>
The package change log (optional).
2.1.8. <Description>
The package description in different languages (required).
2.1.9. Package Actions
The possible actions for the package after installation. If one of these actions is not defined
on the package, it will be considered as possible.
<PackageIsVisible>1</PackageIsVisible>
<PackageIsDownloadable>0</PackageIsDownloadable>
<PackageIsRemovable>1</PackageIsRemovable>
2.1.10. <BuildHost>
This will be filled in automatically by OPM.
<BuildHost>?</BuildHost>
2.1.11. <BuildDate>
This will be filled in automatically by OPM.
<BuildDate>?</BuildDate>
158
2.1.12. <PackageRequired>
Packages that must be installed beforehand (optional). If PackageRequired is used, a
version of the required package must be specified.
<PackageRequired Version="1.0.3">SomeOtherPackage</PackageRequired>
<PackageRequired Version="5.3.2">SomeotherPackage2</PackageRequired>
2.1.13. <ModuleRequired>
Perl modules that must be installed beforehand (optional).
<ModuleRequired Version="1.03">Encode</ModuleRequired>
<ModuleRequired Version="5.32">MIME::Tools</ModuleRequired>
2.1.14. <OS>
Required OS (optional).
<OS>linux</OS>
<OS>darwin</OS>
<OS>mswin32</OS>
2.1.15. <Filelist>
This is a list of files included in the package (optional).
<Filelist>
<File Permission="644" Location="Kernel/Config/Files/Calendar.pm"/>
<File Permission="644" Location="Kernel/System/CalendarEvent.pm"/>
<File Permission="644" Location="Kernel/Modules/AgentCalendar.pm"/>
<File Permission="644" Location="Kernel/Language/de_AgentCalendar.pm"/>
</Filelist>
2.1.16. <DatabaseInstall>
Database entries that have to be created when a package is installed (optional).
<DatabaseInstall>
<TableCreate Name="calendar_event">
<Column Name="id" Required="true" PrimaryKey="true" AutoIncrement="true" Type="BIGINT"/>
<Column Name="title" Required="true" Size="250" Type="VARCHAR"/>
<Column Name="content" Required="false" Size="250" Type="VARCHAR"/>
<Column Name="start_time" Required="true" Type="DATE"/>
<Column Name="end_time" Required="true" Type="DATE"/>
<Column Name="owner_id" Required="true" Type="INTEGER"/>
<Column Name="event_status" Required="true" Size="50" Type="VARCHAR"/>
</TableCreate>
</DatabaseInstall>
159
2.1.17. <DatabaseUpgrade>
Information on which actions have to be performed in case of an upgrade (optional).
Example if already installed package version is below 1.3.4 (e. g. 1.2.6), the defined action
will be performed:
<DatabaseUpgrade>
<TableCreate Name="calendar_event_involved" Version="1.3.4">
<Column Name="event_id" Required="true" Type="BIGINT"/>
<Column Name="user_id" Required="true" Type="INTEGER"/>
</TableCreate>
</DatabaseUpgrade>
2.1.18. <DatabaseReinstall>
Information on which actions have to be performed if the package is reinstalled (optional).
<DatabaseReinstall></DatabaseReinstall>
2.1.19. <DatabaseUninstall>
Actions to be performed on package uninstall (optional).
<DatabaseUninstall>
<TableDrop Name="calendar_event" />
</DatabaseUninstall>
2.1.20. <IntroInstall>
To show a "pre" or "post" install introduction in installation dialog.
You can also use the Format attribute to define if you want to use "html" (which is default)
or "plain" to use automatically a <pre></pre> tag when intro is shown (to keep the new-
lines and whitespace of the content).
2.1.21. <IntroUninstall>
To show a "pre" or "post" uninstall introduction in uninstallation dialog.
160
Some Info formatted in html....
]]></IntroUninstall>
You can also use the Format attribute to define if you want to use "html" (which is default)
or "plain" to use automatically a <pre></pre> tag when intro is shown (to keep the new-
lines and whitespace of the content).
2.1.22. <IntroReinstall>
To show a "pre" or "post" reinstall introduction in re-installation dialog.
You can also use the Format attribute to define if you want to use "html" (which is default)
or "plain" to use automatically a <pre></pre> tag when intro is shown (to keep the new-
lines and whitespace of the content).
2.1.23. <IntroUpgrade>
To show a "pre" or "post" upgrade introduction in upgrading dialog.
You can also use the Format attribute to define if you want to use "html" (which is default)
or "plain" to use automatically a <pre></pre> tag when intro is shown (to keep the new-
lines and whitespace of the content).
2.1.24. <CodeInstall>
Perl code to be executed when the package is installed (optional).
<CodeInstall><![CDATA[
# log example
$Kernel::OM->Get('Kernel::System::Log')->Log(
Priority => 'notice',
Message => "Some Message!",
);
# database example
$Kernel::OM->Get('Kernel::System::DB')->Do(SQL => "SOME SQL");
]]></CodeInstall>
2.1.25. <CodeUninstall>
Perl code to be executed when the package is uninstalled (optional). On "pre" or "post"
time of package uninstallation.
<CodeUninstall><![CDATA[
...
]]></CodeUninstall>
161
You also can choose <CodeUninstall Type="post"> or <CodeUninstall Type="pre">
to define the time of execution separately (post is default). For more info see Package
Life Cycle.
2.1.26. <CodeReinstall>
Perl code to be executed when the package is reinstalled (optional).
<CodeReinstall><![CDATA[
...
]]></CodeReinstall>
2.1.27. <CodeUpgrade>
Perl code to be executed when the package is upgraded (subject to version tag), (op-
tional). Example if already installed package version is below 1.3.4 (e. g. 1.2.6), defined
action will be performed:
<CodeUpgrade Version="1.3.4"><![CDATA[
...
]]></CodeUpgrade>
2.1.28. <PackageMerge>
This tag singals that a package has been merged into another package. In this case the
original package needs to be removed from the file system and the packages database,
but all data must be kept. Let's assume that PackageOne was merged into PackageTwo.
Then PackageTwo.sopm should contain this:
If PackageOne also contained database structures, we need to be sure that it was at the
latest available version of the package to have a consistent state in the database after
merging the package. The attribute TargetVersion does just this: it signifies the last
known version of PackageOne at the time PackageTwo was created. This is mainly to stop
the upgrade process if in the user's system a version of PackageOne was found that is
newer than the one specified in TargetVersion as this could lead to problems.
Additionally it is possible to add required database and code upgrade tags for PackageOne
to make sure that it gets properly upgraded to the TargetVersionbefore merging it - to
avoid inconsistency problems. Here's how this could look like:
162
</DatabaseUpgrade>
</PackageMerge>
As you can see the attribute Type="merge" needs to be set in this case. These sections
will only be executed if a package merge is possible.
<DatabaseInstall IfPackage="AnyPackage">
...
</DatabaseInstall>
or
<CodeUpgrade IfNotPackage="OtherPackage">
...
</CodeUpgrade>
These attributes can be also set in the Database* and Code* sections inside the Pack-
ageMerge tags.
2.2. Example .sopm
This is an example spec file looks with some of the above tags.
163
<Column Name="content" Required="false" Size="250" Type="VARCHAR"/>
<Column Name="start_time" Required="true" Type="DATE"/>
<Column Name="end_time" Required="true" Type="DATE"/>
<Column Name="owner_id" Required="true" Type="INTEGER"/>
<Column Name="event_status" Required="true" Size="50" Type="VARCHAR"/>
</TableCreate>
</DatabaseInstall>
<DatabaseUninstall>
<TableDrop Name="calendar_event"/>
</DatabaseUninstall>
</otrs_package>
2.3. Package Build
To build an .opm package from the spec opm.
3. Package Porting
With every new minor or major version of OTRS, you need to port your package(s) and
make sure they still work with the OTRS API.
3.1. From OTRS 5 to 6
This section lists changes that you need to examine when porting your package from
OTRS 5 to 6.
The main advantage of the new Kernel::System::DateTime module is the support for
real time zones like Europe/Berlin instead of time offsets in hours like +2. Note that
also the old Kernel::System::Time module has been improved to support time zones.
Time offsets have been completely dropped. This means that any code that uses time
164
offsets for calculations has to be ported to use the new DateTime module instead. Code
that doesn't fiddle around with time offsets itself can be left untouched in most cases.
You just have to make sure that upon creation of a Kernel::System::Time object a valid
time zone will be given.
Please note that the returned time values with the new Get() function in the Ker-
nel::System::DateTime module are without leading zero instead of the old System-
Time2Date() function in the Kernel::System::Time module. In the new Kernel::Sys-
tem::DateTime module the function Format() returns the date/time as string formatted
according to the given format.
Note
Please note that this is currently only applicable for places where it actually
makes sense to have the possibility to upload multiple files (like AgentTicketPhone,
AgentTicketCompose, etc.). This is not usable out of the box for admin screens.
To include the new multi attachment upload in the template, replace the existing input
type="file" with the following code in your .tt template file:
It is also necessary to remove the IsUpload variable and all other IsUpload parts from
the perl module. Code parts like following are not needed anymore:
165
Additional to that, the Attachment Layout Block needs to be replaced:
$LayoutObject->Block(
Name => 'Attachment',
Data => $Attachment,
);
If the module where you want to integrate multiupload supports standard templates,
make sure to add a section to have a human readable file size format right after the
attachments of the selected template have been loaded (see e.g. AgentTicketPhone for
reference):
When adding selenium unit tests for the modules you ported, please take a look at Se-
lenium/Agent/MultiAttachmentUpload.t for reference.
Please make sure to add the correct breadcrumb for all levels of your admin module (e.g.
Subactions):
[% BreadcrumbPath = [
{
Name => Translate('Module Home Screen'),
Link => Env("Action"),
},
{
Name => Translate("Some Subaction"),
},
]
%]
166
[% INCLUDE "Breadcrumb.tt" Path = BreadcrumbPath %]
Done.
In the past, Perl configuration files were sometimes misused as an autoload mechanism
to override code in existing packages. This is not neccessary any more as OTRS 6 features
a dedicated Autoload mechanism. Please see Kernel/Autoload/Test.pm for a demon-
stration on how to use this mechanism to add a method in an existing file.
167
3.1.5. Perldoc structure changed
The structure of POD in Perl files was slightly improved and should be adapted in all files.
POD is now also enforced to be syntactically correct.
=head1 NAME
=head1 SYNOPSIS
=head1 DESCRIPTION
The ObjectManager is the central place to create and access singleton OTRS objects (via
C<L</Get()>>)
as well as create regular (unmanaged) object instances (via C<L</Create()>>).
In case the DESCRIPTION does not add any value to the line in the NAME section, it should
be rewritten or removed altogether.
The second important change is that functions are now documented as =head2 instead
of the previously used =item.
=head2 Get()
Retrieves a singleton object, and if it not yet exists, implicitly creates one for you.
my $ConfigObject = $Kernel::OM->Get('Kernel::Config');
=cut
These changes lead to an improved online API documentation as can be seen in the Ob-
jectManager documentation for OTRS 5 and OTRS 6.
168
conditions JavaScript needs to be placed within template files. For all other occurences,
place the JS code in module-specific JavaScript files. An Init() method within such a
JavaScript file is executed automatically on file load (for the initialization of event bind-
ings etc.) if you register the JavaScript file at the OTRS application. This is done by ex-
ecuting Core.Init.RegisterNamespace(TargetNS, 'APP_MODULE'); at the end of the
namespace declaration within the JavaScript file.
Typically, these template files were included in the module-specific template files within
a block:
[% RenderBlockStart("RichText") %]
[% InsertTemplate("RichTextEditor.tt") %]
[% RenderBlockEnd("RichText") %]
This is no longer needed and can be removed. Instead of calling this block from the perl
module, it is now necessary to set the needed rich text parameters there. Instead of:
$LayoutObject->Block(
Name => 'RichText',
Data => \%Param,
);
$LayoutObject->SetRichTextParameters(
Data => \%Param,
);
Same rule applies for Customer interface. Remove RichText blocks with CustomerRich-
TextEditor.tt and apply following code instead:
$LayoutObject->CustomerSetRichTextParameters(
Data => \%Param,
);
169
Core.Language.Translate('The %s to %s', 'string', 'translate');
Every %s is replaced by the variable given as extra parameter. The number of parameters
is not limited.
To achieve template files without JavaScript code, some other workarounds had to be re-
placed with an appropriate solution. Besides translations, also the handover of data from
Perl to JavaScript has been a problem in OTRS. The workaround was to add a JavaScript
block in the template in which JavaScript variables were declared and filled with template
tags based on data handed over from Perl to the template.
The handover process of data from Perl to JavaScript is now much easier in OTRS 6. To
send specific data as variable from Perl to JavaScript, one only has to call a function on
Perl-side. The data is than automatically available in JavaScript.
$Self->{LayoutObject}->AddJSData(
Key => 'KeyToBeAvailableInJS',
Value => $YourData,
);
The Value parameter is automatically converted to a JSON object and can also contain
complex data.
Core.Config.Get('KeyToBeAvailableInJS');
This replaces all workarounds which need to be removed when porting a module to OTRS
6, because JavaScript in template files is now only allowed in very rare conditions (see
above).
OTRS 6 exposes new JavaScript template API via Core.Template class. You can use it in
your JavaScript code in a similar way as you use TemplateToolkit from Perl code.
Here's an example for porting existing jQuery based code to new template API:
$('<div />').addClass('CSSClass')
.attr('id', DivID)
.text(DivText)
.appendTo('body');
170
• You may reuse any existing subfolder structure but only if it makes sense for your com-
ponent (e.g. Agent/MyModule/ or Agent/Admin/MyModule/).
• Name templates succinctly and clearly in order to avoid confusion (i.e. good: Agent/
MyModule/SettingsDialog.html.tmpl, bad: Agent/SettingsDialogTemplate.htm-
l.tmpl).
Then, add your HTML to the template file, making sure to use placeholders for any vari-
ables you might need:
Then, just get rendered HTML by calling Core.Template.Render method with template
path (without extension) and object containing variables for replacement:
$(DivHTML).appendTo('body');
Internally, Core.Template uses Nunjucks engine for parsing templates. Essentially, any
valid Nunjucks syntax is supported, please see their documentation for more information.
• You can use | Translate filter for string translation to current language.
• All {{ VarName }} variable outputs are HTML escaped by default. If you need to output
some existing HTML, please use | safe filter to bypass escaping.
• Complex structures in replacement object are supported, so feel free to pass arrays or
hashes and iterate over them right from template. For example, look at {% for %}
syntax in Nunjucks documentation.
With OTRS 6, permissions are no longer stored in the session and also not passed to the
LayoutObject. Please switch your code to calling PermissionCheck() on Kernel::Sys-
tem::Group (for agents) or Kernel::System::CustomerGroup (for customers). Here's an
example:
my $HasPermission = $Kernel::OM->Get('Kernel::System::Group')->PermissionCheck(
UserID => $UserID,
GroupName => $GroupName,
Type => 'move_into',
);
171
3.1.8. Ticket API changes
3.1.8.1. TicketGet()
For OTRS 6, all extensions need to be checked and ported from $Ticket{Solution-
Time} to $Ticket{Closed} if TicketGet() is called with the Extended parameter (see
bug#11872).
3.1.8.2. LinkObject Events
In OTRS 6, old ticket-specific LinkObject events have been dropped:
• TicketSlaveLinkAdd
• TicketSlaveLinkDelete
• TicketMasterLinkDelete
Any event handlers listening on these events should be ported to two new events instead:
• LinkObjectLinkAdd
• LinkObjectLinkDelete
These new events will be triggered any time a link is added or deleted by LinkObject,
regardless of the object type. Data parameter will contain all information your event han-
dlers might need for further processing, e.g.:
SourceObject
SourceKey
TargetObject
TargetKey
Type
State
With these new events in place, any events specific for custom LinkObject module im-
plementations can be dropped, and all event handlers ported to use them instead. Since
source and target object names are provided in the event itself, it would be trivial to make
them run only in specific situations.
To register your event handler for these new events, make sure to add a registration in
the configuration, for example:
172
<!-- OLD STYLE -->
<ConfigItem Name="LinkObject::EventModulePost###1000-SampleModule" Required="0" Valid="1">
<Description Translatable="1">Event handler for sample link object module.</Description>
<Group>Framework</Group>
<SubGroup>Core::Event::Package</SubGroup>
<Setting>
<Hash>
<Item Key="Module">Kernel::System::LinkObject::Event::SampleModule</Item>
<Item Key="Event">(LinkObjectLinkAdd|LinkObjectLinkDelete)</Item>
<Item Key="Transaction">1</Item>
</Hash>
</Setting>
</ConfigItem>
• ArticleFlagSet()
• ArticleFlagDelete()
• ArticleFlagGet()
• ArticleFlagsOfTicketGet()
• ArticleAccountedTimeGet()
• ArticleAccountedTimeDelete()
• ArticleSenderTypeList()
• ArticleSenderTypeLookup()
• SearchStringStopWordsFind()
• SearchStringStopWordsUsageWarningActive()
If you are referencing any of these methods via Kernel::System::Ticket object in your
code, please switch to Article object and use it instead. For example:
my $ArticleObject = $Kernel::OM->Get('Kernel::System::Ticket::Article');
173
my %ArticleSenderTypeList = $ArticleObject->ArticleSenderTypeList();
New ArticleList() method is now provided by the article object, and can be used for
article listing and locating. This method implements filters and article numbering and
returns article meta data only as an ordered list. For example:
my @Articles = $ArticleObject->ArticleList(
TicketID => 123,
CommunicationChannel => 'Email', # optional, to limit to a certain
CommunicationChannel
SenderType => 'customer', # optional, to limit to a certain article
SenderType
IsVisibleForCustomer => 1, # optional, to limit to a certain visibility
OnlyFirst => 1, # optional, only return first match, or
OnlyLast => 1, # optional, only return last match
);
Following methods related to articles have been dropped all-together. If you are using any
of them in your code, please evaluate possibility of alternatives.
• ArticleTypeList()
• ArticleTypeLookup()
• ArticleContentIndex()
To work with article data please use new article backend API. To get correct backend object
for an article, please use:
• BackendForArticle(%Article)
BackendForArticle() returns the correct back end for a given article, or the invalid back
end, so that you can always expect a back end object instance that can be used for chain-
calling.
BackendForChannel() returns the correct back end for a given communication channel.
174
OTRS 6 Free ships with some default channels and corresponding backends:
Note
While chat article backend is available in OTRS 6 Free, it is only utilized when
system has a valid OTRS Business Solution™ installed.
• ArticleCreate()
• ArticleUpdate()
• ArticleGet()
• ArticleDelete()
All of these methods have dropped article type parameter, which must be substituted for
SenderType and IsVisibleForCustomer parameter combination. In addition, all these
methods now also require TicketID and UserID parameters.
Note
Since changes in article API are system-wide, any code using the old API must
be ported for OTRS 6. This includes any web service definitions which leverage
these methods directly via GenericInterface for example. They will need to be re-
assessed and adapted to provide all required parameters to the new API during
requests and manage subsequent responses in new format.
Please note that returning hash of ArticleGet() has changed, and some things (like
ticket data) might be missing. Utilize parameters like DynamicFields => 1 and RealNames
=> 1 to get more information.
In addition, attachment data is not returned any more, please use combination of following
methods from the article backends:
• ArticleAttachmentIndex()
• ArticleAttachment()
As an example, here is how to get all article and attachment data in the same hash:
my @Articles = $ArticleObject->ArticleList(
TicketID => $TicketID,
);
ARTICLE:
for my $Article (@Articles) {
175
# Make sure to retrieve backend object for this specific article.
my $ArticleBackendObject = $ArticleObject->BackendForArticle( %{$Article} );
my %ArticleData = $ArticleBackendObject->ArticleGet(
%{$Article},
DynamicFields => 1,
UserID => $UserID,
);
$Article = \%ArticleData;
my @Attachments;
ATTACHMENT:
for my $FileID ( sort keys %AtmIndex ) {
my %Attachment = $ArticleBackendObject->ArticleAttachment(
ArticleID => $Article->{ArticleID},
FileID => $FileID,
UserID => $UserID,
);
next ATTACHMENT if !%Attachment;
$Attachment{FileID} = $FileID;
$Attachment{Content} = encode_base64( $Attachment{Content} );
Since now every article backend can provide search on arbitrary number of article fields,
use BackendSearchableFieldsGet() method to get information about them. This data
can also be used for forming requests to TicketSearch() method. Coincidentally, some
TicketSearch() parameters have changed their name to also include article backend
information, for example:
Additionally, article search indexing will be done in an async call now, in order to off-load
index calculation to a separate task. While this is fine for production systems, it might
create new problems in certain situations, i.e. unit tests. If you are manually creating
articles in your unit test, but expect it to be searchable immediately after created, make
sure to manually call the new ArticleSearchIndexBuild() method on article object.
176
3.1.10. SysConfig API changes
Note that in OTRS 6 SysConfig API was changed, so you should check if the methods are
still existing. For example, ConfigItemUpdate() is removed. To replace it you should use
combination of the following methods:
• SettingLock()
• SettingUpdate()
• ConfigurationDeploy()
In case that you want to update a configuration setting during a CodeInstall section of
a package, you could use SettingsSet(). It does all previouly mentioned steps and it
can be used for multiple settings at once.
Note
Do not use SettingSet() in the SysConfig GUI itself.
my $Success = $SysConfigObject->SettingsSet(
UserID => 1, # (required) UserID
Comments => 'Deployment comment', # (optional) Comment
Settings => [ # (required) List of settings to
update.
{
Name => 'Setting::Name', # (required)
EffectiveValue => 'Value', # (optional)
IsValid => 1, # (optional)
UserModificationActive => 1, # (optional)
},
...
],
);
Old code:
my $LinkList = $LinkObject->LinkList(
Object => 'Ticket',
Key => '321',
Object2 => 'FAQ',
State => 'Valid',
Type => 'ParentChild',
Direction => 'Target',
UserID => 1,
);
New code:
my $LinkList = $LinkObject->LinkList(
Object => 'Ticket',
Key => '321',
Object2 => 'FAQ',
State => 'Valid',
177
Type => 'ParentChild',
Direction => 'Source',
UserID => 1,
);
If your package implements additional PostMaster filters, make sure to get acquainted
with API usage instructions. Also, you can get an example of how to implement this logging
mechanism by looking the code in the Kernel::System::PostMaster::NewTicket.
To the unit tests that depend on emails continue to work properly is necessary to force
the processing of the email queue.
my $MailQueueObject = $Kernel::OM->Get('Kernel::System::MailQueue');
$MailQueueObject->Delete();
If for some reason you can't clean completely the queue, e.g. selenium unit tests, just
delete the items created during the tests:
my $MailQueueObject = $Kernel::OM->Get('Kernel::System::MailQueue');
my %MailQueueCurrentItems = map { $_->{ID} => $_ } @{ $MailQueueObject-
>List() || [] };
my $Items = $MailQueueObject->List();
MAIL_QUEUE_ITEM:
for my $Item ( @{$Items} ) {
next MAIL_QUEUE_ITEM if $MailQueueCurrentItems{ $Item->{ID} };
$MailQueueObject->Delete(
ID => $Item->{ID},
);
}
Process the queue after the code that you expect to send emails:
my $MailQueueObject = $Kernel::OM->Get('Kernel::System::MailQueue');
my $QueueItems = $MailQueueObject->List();
for my $Item ( @{$QueueItems} ) {
$MailQueueObject->Send( %{$Item} );
}
my $MailQueueObject = $Kernel::OM->Get('Kernel::System::MailQueue');
my $QueueItems = $MailQueueObject->List();
178
MAIL_QUEUE_ITEM:
for my $Item ( @{$QueueItems} ) {
next MAIL_QUEUE_ITEM if $MailQueueCurrentItems{ $Item->{ID} };
$MailQueueObject->Send( %{$Item} );
}
Depending on your case, you may need to clean the queue after or before processing it.
<Setting Name="Ticket::Frontend::AgentTicketZoom###Widgets###0100-TicketInformation"
Required="0" Valid="1">
<Description Translatable="1">AgentTicketZoom widget that displays ticket data in the
side bar.</Description>
<Navigation>Frontend::Agent::View::TicketZoom</Navigation>
<Value>
<Hash>
<Item Key="Module">Kernel::Output::HTML::TicketZoom::TicketInformation</Item>
<Item Key="Location">Sidebar</Item>
</Hash>
</Value>
</Setting>
<Setting Name="Ticket::Frontend::AgentTicketZoom###Widgets###0200-CustomerInformation"
Required="0" Valid="1">
<Description Translatable="1">AgentTicketZoom widget that displays customer information
for the ticket in the side bar.</Description>
<Navigation>Frontend::Agent::View::TicketZoom</Navigation>
<Value>
<Hash>
<Item Key="Module">Kernel::Output::HTML::TicketZoom::CustomerInformation</Item>
<Item Key="Location">Sidebar</Item>
<Item Key="Async">1</Item>
</Hash>
</Value>
</Setting>
Note
With this change, the template blocks in the widget code have been removed,
so you should check if you use the old widget blocks in some output filters via
Frontend::Template::GenerateBlockHooks functionality, and implement it in
the new fashion.
3.2. From OTRS 4 to 5
This section lists changes that you need to examine when porting your package from
OTRS 4 to 5.
3.2.1. Kernel/Output/HTML restructured
In OTRS 5, Kernel/Output/HTML was restructured. All Perl modules (except Layout.pm)
were moved to subdirectories (one for every module layer). Template (theme) files
were also moved from Kernel/Output/HTML/Standard to Kernel/Output/HTML/Tem-
plates/Standard. Please perform this migration also in your code.
179
3.2.2. Pre-Output-Filters
With OTRS 5 there is no support for pre output filters any more. These filters changed the
template content before it was parsed, and that could potentially lead to bad performance
issues because the templates could not be cached any more and had to be parsed and
compiled every time.
Just switch from pre to post output filters. To translate content, you can run $LayoutO-
bject->Translate() directly. If you need other template features, just define a small
template file for your output filter and use it to render your content before injecting it
into the main data. It can also be helpful to use jQuery DOM operations to reorder/replace
content on the screen in some cases instead of using regular expressions. In this case you
would inject the new code somewhere in the page as invisible content (e. g. with the class
Hidden), and then move it with jQuery to the correct location in the DOM and show it.
To make using post output filters easier, there is also a new mechanism to request HTML
comment hooks for certain templates/blocks. You can add in your module config XML like:
<ConfigItem
Name="Frontend::Template::GenerateBlockHooks###100-OTRSBusiness-ContactWithData"
Required="1" Valid="1">
<Description Translatable="1">Generate HTML comment hooks for
the specified blocks so that filters can use them.</Description>
<Group>OTRSBusiness</Group>
<SubGroup>Core</SubGroup>
<Setting>
<Hash>
<Item Key="AgentTicketZoom">
<Array>
<Item>CustomerTable</Item>
</Array>
</Item>
</Hash>
</Setting>
</ConfigItem>
<!--HookStartCustomerTable-->
... block output ...
<!--HookEndCustomerTable-->
With this mechanism every package can request just the block hooks it needs, and they
are consistently rendered. These HTML comments can then be used in your output filter
for easy regular expression matching.
3.2.3. IE 8 and IE 9
Support for IE 8 and 9 was dropped. You can remove any workarounds in your code for
these platforms, as well as any old <CSS_IE7> or <CSS_IE8> loader tags that might still
lurk in your XML config files.
180
# changed from:
Ticket => [
{
TicketNumber => '20101027000001',
Title => 'some title',
...
DynamicField_X => 'value_x',
},
]
# to:
Ticket => [
{
TicketNumber => '20101027000001',
Title => 'some title',
...
DynamicField => [
{
Name => 'some name',
Value => 'some value',
},
],
},
]
181
This will require you first of all to change all top level Perl scripts (.pl files only!) to load
and provide the ObjectManager to all OTRS objects. Let's look at otrs.CheckDB.pl from
OTRS 3.3 as an example:
use strict;
use warnings;
use File::Basename;
use FindBin qw($RealBin);
use lib dirname($RealBin);
use lib dirname($RealBin) . '/Kernel/cpan-lib';
use lib dirname($RealBin) . '/Custom';
use Kernel::Config;
use Kernel::System::Encode;
use Kernel::System::Log;
use Kernel::System::Main;
use Kernel::System::DB;
We can see that a lot of code is used to load the packages and create the common objects
that must be passed to OTRS objects to be used in the script. With OTRS 4, this looks
quite different:
use strict;
use warnings;
use File::Basename;
use FindBin qw($RealBin);
use lib dirname($RealBin);
use lib dirname($RealBin) . '/Kernel/cpan-lib';
use lib dirname($RealBin) . '/Custom';
use Kernel::System::ObjectManager;
The new code is a bit shorter than the old. It is no longer necessary to load all
the packages, just the ObjectManager. Subsequently $Kernel::OM->Get('My::Per-
l::Package') can be used to get instances of objects which only have to be created once.
The LogPrefix setting controls the log messages that Kernel::System::Log writes, it
could also be omitted.
From this example you can also deduce the general porting guide when it comes to ac-
cessing objects: don't store them in $Self any more (unless needed for specific rea-
sons). Just fetch and use the objects on demand like $Kernel::OM->Get('Kernel::Sys-
tem::Log')->Log(...). This also has the benefit that the Log object will only be created
182
if something must be logged. Sometimes it could also be useful to create local variables
if an object is used many times in a function, like $DBObject in the example above.
There's not much more to know when porting packages that should be loadable by the
ObjectManager. They should declare the modules they use (via $Kernel::OM->Get())
like this:
our @ObjectDependencies = (
'Kernel::Config',
'Kernel::System::Log',
'Kernel::System::Main',
);
The @ObjectDependencies declaration is needed for the ObjectManager to keep the cor-
rect order when destroying the objects.
Let's look at Valid.pm from OTRS 3.3 and 4 to see the difference. Old:
package Kernel::System::Valid;
use strict;
use warnings;
use Kernel::System::CacheInternal;
...
sub new {
my ( $Type, %Param ) = @_;
$Self->{CacheInternalObject} = Kernel::System::CacheInternal->new(
%{$Self},
Type => 'Valid',
TTL => 60 * 60 * 24 * 20,
);
return $Self;
}
...
sub ValidList {
my ( $Self, %Param ) = @_;
# read cache
my $CacheKey = 'ValidList';
my $Cache = $Self->{CacheInternalObject}->Get( Key => $CacheKey );
return %{$Cache} if $Cache;
183
# set cache
$Self->{CacheInternalObject}->Set( Key => $CacheKey, Value => \%Data );
return %Data;
}
New:
package Kernel::System::Valid;
use strict;
use warnings;
our @ObjectDependencies = (
'Kernel::System::Cache',
'Kernel::System::DB',
'Kernel::System::Log',
);
...
sub new {
my ( $Type, %Param ) = @_;
$Self->{CacheType} = 'Valid';
$Self->{CacheTTL} = 60 * 60 * 24 * 20;
return $Self;
}
...
sub ValidList {
my ( $Self, %Param ) = @_;
# read cache
my $CacheKey = 'ValidList';
my $Cache = $Kernel::OM->Get('Kernel::System::Cache')->Get(
Type => $Self->{CacheType},
Key => $CacheKey,
);
return %{$Cache} if $Cache;
# set cache
$Kernel::OM->Get('Kernel::System::Cache')->Set(
Type => $Self->{CacheType},
TTL => $Self->{CacheTTL},
Key => $CacheKey,
Value => \%Data
);
return %Data;
}
184
You can see that the dependencies are declared and the objects are only fetched on
demand. We'll talk about the CacheInternalObject in the next section.
3.3.2. CacheInternalObject removed
Since Kernel::System::Cache is now also able to cache in-memory, Kernel::Sys-
tem::CacheInternal was dropped. Please see the previous example for how to migrate
your code: you need to use the global Cache object and pass the Type settings with every
call to Get(), Set(), Delete() and CleanUp(). The TTL parameter is now optional and
defaults to 20 days, so you only have to specify it in Get() if you require a different TTL
value.
Warning
It is especially important to add the Type to CleanUp() as otherwise not just the
current cache type but the entire cache would be deleted.
<CodeInstall Type="post">
if ($CodeObject) {
# start method
if ( !$CodeObject->$FunctionName(%{$Self}) ) {
$Self->{LogObject}->Log(
Priority => 'error',
Message => "Could not call method $FunctionName() on $CodeModule.pm."
);
}
}
# error handling
else {
$Self->{LogObject}->Log(
Priority => 'error',
Message => "Could not call method new() on $CodeModule.pm."
);
}
}
</CodeInstall>
185
Now this should be replaced by:
<CodeInstall Type="post"><![CDATA[
$Kernel::OM->Get('var::packagesetup::MyPackage')->CodeInstall();
]]></CodeInstall>
These are the changes that you need to apply when converting existing DTL templates
to the new Template::Toolkit syntax:
186
DTL Tag Template::Toolkit tag
<-- dtl:block:Name -->...<-- [% RenderBlockStart("Name") %]...[%
dtl:block:Name --> RenderBlockEnd("Name") %]
<-- dtl:js_on_document_complete -- [% WRAPPER JSOnDocumentComplete %]...
>...<-- dtl:js_on_document_complete [% END %]
-->
<-- dtl:js_on_document_com- [% PROCESS JSOnDocumentCom-
plete_placeholder --> pleteInsert %]
$Include{"Copyright"} [% InsertTemplate("Copyright") %]
There are a few more things to note when porting your code to the new template engine:
• All language files must now have the use utf8; pragma.
• All occurrences of $Text{""} in Perl code must now be replaced by calls to Lay-
out::Translate().
This is because in DTL there was no separation between template and data. If DTL-Tags
were inserted as part of some data, the engine would still parse them. This is no longer
the case in Template::Toolkit, there is a strict separation of template and data.
Hint: should you ever need to interpolate tags in data, you can use the Interpolate
filter for this ([% Data.Name | Interpolate %]). This is not recommended for security
and performance reasons!
• For the same reason, dynamically injected JavaScript that was enclosed by
dtl:js_on_document_complete will not work any more. Please use Layout::AddJSOn-
DocumentComplete() instead of injecting this as template data.
• Please be careful with pre output filters (the ones configured in Frontend::Out-
put::FilterElementPre). They still work, but they will prevent the template from be-
ing cached. This could lead to serious performance issues. You should definitely not
have any pre output filters that operate on all templates, but limit them to certain tem-
plates via configuration setting.
Due to this change, you need to make sure to update all custom frontend module registra-
tions which make use of icons (e.g. for the top navigation bar) to use the new schema. This
is also true for templates where you're using icon elements like <i class="icon-{icon-
name}"></i>.
187
3.3.7. Unit Tests
With OTRS 4, in Unit Tests $Self no longer provides common objects like the MainObject,
for example. Please always use $Kernel::OM->Get('...') to fetch these objects.
Firstly, you have to register your custom ticket history types via SysConfig. This could
look like:
The second step is to translate the English text that you provided for the custom ticket
history type in your translation files, if needed. That's it!
If you are interested in the details, please refer to this commit for additional information
about the changes that happened in OTRS.
188
Chapter 5. Contributing to
OTRS
This chapter will show how you can contribute to the OTRS framework, so that other users
will be able to benefit from your work.
1. Sending Contributions
The source code of OTRS and additional public modules can be found on github. From
there you can get to the listing of all available repositories. It also describes the currently
active branches and where contributions should go to (stable vs. development branches).
It is highly recommended that you use the OTRS code quality checker OTRSCodePolicy
as described in the development environment chapter even before sending in your con-
tributions. If your code does not validate against this tool, it will likely not be accepted.
The easiest way to send your contributions to the OTRS developer's team is by creating a
"pull request" in github. Please take a look at the instructions on github, specifically about
forking a repository and sending pull requests.
• Fork the repository you want to contribute to, and checkout the branch that the changes
should go in.
• Create a new development branch for your fix/feature/contribution, based on the cur-
rent branch.
• After you finished your changes and committed them, push your branch to github.
• Create a pull request. The OTRS dev team will be notified about this, check your pull
request and either merge it or give you some feedback about possible improvements.
It might sound complicated, but once you have this workflow set up you'll see that making
contributions is extremely easy.
2. Translating OTRS
The OTRS framework allows for different languages to be used in the frontend. The trans-
lations are contributed and maintained mainly by OTRS users, so your help is needed.
189
2.2. Adding a new frontend translation
If you want to translate the OTRS framework into a new language, you can propose a
new language translation on the Transifex OTRS project page. After it is approved, you
can just start translating.
It is important that the structure of the generated XML stays intact. So if the original string
is Edit <filename>Kernel/Config.pm</filename>, then the German translation has
to be <filename>Kernel/Config.pm</filename> bearbeiten, keeping the XML tags
intact. Regular < and > signs that are escaped in the source text must also be escaped
in the translations (like <someone@example.com>). Scripts and examples usually
do not have to be translated (so you can just copy the source text to the translation text
field in this case).
4.1. Perl
4.1.1. Formatting
4.1.1.1. Whitespace
TAB: We use 4 spaces. Examples for braces:
if ($Condition) {
Foo();
}
else {
Bar();
}
while ($Condition == 1) {
Foo();
}
4.1.1.2. Length of lines
Lines should generally not be longer than 120 characters, unless it is necessary for special
reasons.
if ()...
for ()...
190
If there is just one single variable, the parenthesis enclose the variable with no spaces
inside.
if ($Condition) { ... }
# instead of
if ( $Condition ) { ... }
If the condition is not just one single variable, we use spaces between the parenthesis
and the condition. And there is still the space between the keyword (e.g. if) and the
opening parenthesis.
chomp $Variable;
# --
# (file name) - a short description what it does
# Copyright (C) 2001-2017 OTRS AG, http://otrs.com/
# --
# This software comes with ABSOLUTELY NO WARRANTY. For details, see
# the enclosed file COPYING for license information (AGPL). If you
# did not receive this file, see http://www.gnu.org/licenses/agpl.txt.
# --
#!/usr/bin/perl
# --
# (file name) - a short description what it does
# Copyright (C) 2001-2017 OTRS AG, http://otrs.com/
# --
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU AFFERO General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
# or see http://www.gnu.org/licenses/agpl.txt.
191
# --
Conditions can be quite complex and there can be "chained" conditions (linked with logical
'or' or 'and' operations). When coding for OTRS, you have to be aware of several situations.
Perl Best Practices says, that high precedence operators (&& and ||) shouldn't mixed up
with low precedence operators (and and or). To avoid confusion, we always use the high
precedence operators.
# instead of
This means that you have to be aware of traps. Sometimes you need to use parenthesis
to make clear what you want.
If you have long conditions (line is longer than 120 characters over all), you have to break
it in several lines. And the start of the conditions is in a new line (not in the line of the if).
if (
$Condition1
&& $Condition2
)
{ ... }
# instead of
if ( $Condition1
&& $Condition2
)
{ ... }
Also note, that the right parenthesis is in a line on its own and the left curly bracket is also
in a new line and with the same indentation as the if. The operators are at the beginning
of a new line! The subsequent examples show how to do it...
if (
$XMLHash[0]->{otrs_stats}[1]{StatType}[1]{Content}
&& $XMLHash[0]->{otrs_stats}[1]{StatType}[1]{Content} eq 'static'
)
{ ... }
if ( $TemplateName eq 'AgentTicketCustomer' ) {
...
}
if (
( $Param{Section} eq 'Xaxis' || $Param{Section} eq 'All' )
&& $StatData{StatType} eq 'dynamic'
)
192
{ ... }
if (
$Self->{TimeObject}->TimeStamp2SystemTime( String => $Cell->{TimeStop} )
> $Self->{TimeObject}->TimeStamp2SystemTime(
String => $ValueSeries{$Row}{$TimeStop}
)
|| $Self->{TimeObject}->TimeStamp2SystemTime( String => $Cell->{TimeStart} )
< $Self->{TimeObject}->TimeStamp2SystemTime(
String => $ValueSeries{$Row}{$TimeStart}
)
)
{ ... }
4.1.2.1.2. Postfix if
Generally we use "postfix if" statements to reduce the number of levels. But we don't
use it for multiline statements and is only allowed when involves return statements in
functions or to end a loop or to go next iteration.
This is correct:
This is wrong:
return $Self->{LogObject}->Log(
Priority => 'error',
Message => 'ItemID needed!',
) if !$ItemId;
if( !$ItemId ) {
$Self->{LogObject}->Log( ... );
return;
}
This is correct:
This is wrong:
193
• Don't use die and exit in .pm files.
• Use the functions of the TimeObject instead of the builtin functions like time(), lo-
caltime(), etc.
4.1.2.3. Regular Expressions
For regular expressions in the source code, we always use the m// operator with curly
braces as delimiters. We also use the modifiers x, m and s. The x modifiers allows you to
comment your regex and use spaces to "group" logical groups.
As the space no longer has a special meaning, you have to use a single character class
to match a single space ([ ]). If you want to match any whitespace you can use \s.
In the regex, the dot (.) includes the newline (whereas in regex without s modifier the dot
means 'everything but newline'). If you want to match anything but newline, you have to
use the negated single character class ([^\n]).
$Text =~ m{
Test
[ ] # there must be a space between 'Test' and 'Regex'
Regex
}xms;
An exception to the convention above applies to all cases where regular expressions are
not written statically in the code but instead are supplied by users in one form or an-
other (for example via SysConfig or in a PostMaster filter configuration). Any evaluation
of such a regular expression has to be done without any modifiers (e.g. $Variable =~
m{$Regex}) in order to match the expectation of (mostly inexperienced) users and also
to be backwards compatible.
If modifiers are strictly necessary for user supplied regular expressions, it is always pos-
sible to use embedded modifiers (e.g. (?:(?i)SmAlL oR lArGe)). For details, please see
perlretut.
4.1.2.4. Naming
Names and comments are written in English. Variables, objects and methods must be
descriptive nouns or noun phrases with the first letter set upper case (CamelCase).
Names should be as descriptive as possible. A reader should be able to say what is meant
by a name without digging too deep into the code. E.g. use $ConfigItemID instead of
$ID. Examples: @TicktIDs, $Output, StateSet(), etc.
194
4.1.2.5. Variables
4.1.2.5.1. Declaration
If you have several variables, you can declare them in one line if they "belong together":
my $Minute;
my $ID;
Do not set to undef or '' in the declaration as this might hide mistakes in code.
my $Variable = undef;
# is the same as
my $Variable;
my $SqlStatement = '';
for my $Part ( @Parts ) {
$SqlStatement .= $Part;
}
4.1.2.6. Subroutines
4.1.2.6.1. Handling of parameters
To fetch the parameters passed to subroutines, OTRS normally uses the hash %Param (not
%Params). This leads to more readable code as every time we use %Param in the subroutine
code we know it is the parameter hash passed to the subroutine.
Just in some exceptions a regular list of parameters should be used. So we want to avoid
something like this:
sub TestSub {
my ( $Self, $Param1, $Param2 ) = @_;
}
sub TestSub {
my ( $Self, %Param ) = @_;
}
195
This has several advantages: We do not have to change the code in the subroutine when a
new parameter should be passed, and calling a function with named parameters is much
more readable.
If a function call requires more than one named parameter, split them into multiple lines:
$Self->{LogObject}->Log(
Priority => 'error',
Message => "Need $Needed!",
);
Instead of:
4.1.2.6.3. return statements
Subroutines have to have a return statement. The explicit return statement is preferred
over the implicit way (result of last statement in subroutine) as this clarifies what the
subroutine returns.
sub TestSub {
...
return; # return undef, but not the result of the last statement
}
Explicit return values means that you should not have a return statement followed by
a subroutine call.
The following example is better as this says explicitly what is returned. With the example
above the reader doesn't know what the return value is as he might not know what Do()
returns.
If you assign the result of a subroutine to a variable, a "good" variable name indicates
what was returned:
196
4.1.2.7. Packages
4.1.2.7.1. use statements
use strict and use warnings have to be the first two "use"s in a module. This is correct:
package Kernel::System::ITSMConfigItem::History;
use strict;
use warnings;
use Kernel::System::User;
use Kernel::System::Time;
This is wrong:
package Kernel::System::ITSMConfigItem::History;
use Kernel::System::User;
use Kernel::System::Time;
use strict;
use warnings;
In OTRS many objects are available. But you should not use every object in every file to
keep the frontend/backend separation.
'NAME' section
This section should include the module name, ' - ' as separator and a brief description
of the module purpose.
=head1 NAME
'SYNOPSIS' section
This section should give a short usage example of commonly used module functions.
=head1 SYNOPSIS
197
my $Object = $Kernel::OM->Get('Kernel::System::MyModule');
Read data
my $FileContent = $Object->Read(
File => '/tmp/testfile',
);
Write data
$Object->Write(
Content => 'my file content',
File => '/tmp/testfile',
);
'DESCRIPTION' section
This section should give more in-depth information about the module if deemed nec-
essary (instead of having a long 'NAME' section).
=head1 DESCRIPTION
This section marks the begin of all functions that are part of the API and therefore
meant to be used by other modules.
Functions below are not part of the API, to be used only within the module and there-
fore not considered stable.
It is advisable to use this section whenever one or more private functions exist.
4.1.3.1.2. Documenting subroutines
=head2 LastTimeObjectChanged()
calculates the last time the object was changed. It returns a hash reference with
information about the object and the time.
198
my $Info = $Object->LastTimeObjectChanged(
Param => 'Value',
);
my $Info = {
ConfigItemID => 1234,
HistoryType => 'foo',
LastTimeChanged => '08.10.2009',
};
=cut
You can copy and paste a Data::Dumper output for the return values.
4.1.3.2. Code Comments
In general, you should try to write your code as readable and self-explaining as possible.
Don't write a comment to explain what obvious code does, this is unnecessary duplication.
Good comments should explain why there is some code, possible side effects and anything
that might be special or unusually complicated about the code.
Make the code so readable that comments are not needed, if possible.
It's always preferable to write code so that it is very readable and self-explaining, for
example with precise variable and function names.
# WRONG:
Usually, code comments should explain the purpose of code, not how it works in de-
tail. There might be exceptions for specially complicated code, but in this case also a
refactoring to make it more readable could be commendable.
Document pitfalls.
Everything that is unclear, tricky or that puzzled you during development should be
documented.
Always use full sentences (uppercase first letter and final period). Subsequent lines
of a sentence should be indented.
# Record the object we are about to retrieve to potentially build better error messages.
199
# Needs to be a statement-modifying 'if', otherwise 'local' is local
# to the scope of the 'if'-block.
local $CurrentObject = $_[1] if !$CurrentObject;
These can either be a complete sentence (capital first letter and period) or just a
phrase (lowercase first letter and no period).
# or
4.1.4. Database interaction
4.1.4.1. Declaration of SQL statements
If there is no chance for changing the SQL statement, it should be used in the Prepare
function. The reason for this is, that the SQL statement and the bind parameters are closer
to each other.
The SQL statement should be written as one nicely indented string without concatenation
like this:
return if !$Self->{DBObject}->Prepare(
SQL => '
SELECT art.id
FROM article art, article_sender_type ast
WHERE art.ticket_id = ?
AND art.article_sender_type_id = ast.id
AND ast.name = ?
ORDER BY art.id',
Bind => [ \$Param{TicketID}, \$Param{SenderType} ],
);
This is easy to read and modify, and the whitespace can be handled well by our support-
ed DBMSs. For auto-generated SQL code (like in TicketSearch), this indentation is not
necessary.
4.1.4.2. Returning on errors
Whenever you use database functions you should handle errors. If anything goes wrong,
return from subroutine:
4.1.4.3. Using Limit
Use Limit => 1 if you expect just one row to be returned.
$Self->{DBObject}->Prepare(
SQL => 'SELECT id FROM users WHERE username = ?',
Bind => [ \$Username ],
Limit => 1,
);
200
4.1.4.4. Using the while loop
Always use the while loop, even when you expect one row to be returned, as some data-
bases do not release the statement handle and this can lead to weird bugs.
4.2. JavaScript
4.2.1. Browser Handling
All JavaScript is loaded in all browsers (no browser hacks in the template files). The code
is responsible to decide if it has to skip or execute certain parts of itself only in certain
browsers.
4.2.2. Directory Structure
Directory structure inside the js/ folder:
* js
* thirdparty # thirdparty libs always have the version number inside the
directory
* ckeditor-3.0.1
* jquery-1.3.2
* Core.Agent.* # stuff specific to the agent interface
* Core.Customer.* # customer interface
* Core.* # common API
4.2.2.1. Thirdparty Code
Every thirdparty module gets its own subdirectory: "module name"-"version number" (e.g.
ckeditor-3.0.1, jquery-1.3.2). Inside of that, file names should not have a version num-
ber or postfix included (wrong: jquery/jquery-1.4.3.min.js, right: jquery-1.4.3/
jquery.js).
4.2.3. Variables
• Variable names should be CamelCase, just like in Perl.
• Variables that hold a jQuery object should start with $, for example: $Tooltip.
4.2.4. Functions
• Function names should be CamelCase, just like in Perl.
4.2.5. Namespaces
• TODO...
4.2.6. Code Comments
The commenting guidelines for Perl code also apply to JavaScript.
• If you comment out parts of your JavaScript code, only use // because /* ... */ can
cause problems with Regular Expressions in the code.
201
4.2.7. Event Handling
• Always use $.on() instead of the event-shorthand methods of jQuery for better read-
ability (wrong: $SomeObject.click(...), right: $SomeObject.on('click', ...).
• If you $.on() events, make sure to $.off() them beforehand, to make sure that events
will not be bound twice, should the code be executed another time.
4.3. HTML
• Use HTML 5 notation. Don't use self-closing tags for non-void elements (such as div,
span, etc.).
• Use proper intendation. Elements which contain other non-void child elements should
not be on the same level as their children.
• Don't use HTML elements for layout reasons (e.g. using br elements for adding space
to the top or bottom of other elements). Use the proper CSS classes instead.
• Don't use inline CSS. All CSS should either be added by using predefined classes or (if
necessary) using JavaScript (e.g. for showing/hiding elements).
• Don't use JavaScript in TT templates. All needed JavaScript should be part of the proper
library for a certain frontend module or of a proper global library. If you need to pass
JavaScript data to the frontend, use $LayoutObject->AddJSData().
4.4. CSS
• Minimum resolution is 1024x768px.
• The layout is liquid, which means that if the screen is wider, the space will be used.
• Documentation is made with CSSDOC (see CSS files for examples). All logical blocks
should have a CSSDOC comment.
4.4.1. Architecture
• We follow the Object Oriented CSS approach. In essence, this means that the layout is
achieved by combining different generic building blocks to realize a particular design.
• Wherever possible, module specific design should not be used. Therefore we also do
not work with IDs on the body element, for example, if it can be avoided.
4.4.2. Style
• All definitions have a { in the same line as the selector, all rules are defined in one row
per rule, the definition ends with a row with a single } in it. See the following example:
#Selector {
width: 10px;
height: 20px;
padding: 4px;
}
202
• Between : and the rule value, there is a space.
• If multiple selectors are specified, separate them with comma and put each one on an
own line:
#Selector1,
#Selector2,
#Selector3 {
width: 10px;
}
• Rules should be in a logical order within a definition (all color specific rule together, all
positioning rules together, ...).
• Headings (h1-h6) and Titles (Names, such as Queue View) are set in "title style" capi-
talization, that means all first letters will be capitalized (with a few exceptions such as
"this", "and", "or" etc.).
• Other structural elements such as buttons, labels, tabs, menu items are set in "sentence
style" capitalization (only the first letter of a phrase is capitalized), but no final dot is
added to complete the phrase as a sentence.
Examples: First name, Select queue refresh time, Print this ticket.
• For translations, it has to be checked if the title style capitalization is also appropriate
in the target language. It might have to be changed to sentence style capitalization or
something else.
6. Accessibility Guide
This document is supposed to explain basics about accessibility issues and give guidelines
for contributions to OTRS.
203
6.1. Accessibility Basics
6.1.1. What is Accessibility?
Accessibility is a general term used to describe the degree to which a product, device,
service or environment is accessible by as many people as possible. Accessibility can be
viewed as the "ability to access" and possible benefit of some system or entity. Accessi-
bility is often used to focus on people with disabilities and their right of access to entities,
often through use of assistive technology.
In the context of web development, accessibility has a focus on enabling people with
impairments full access to web interfaces. For example, this group of people can include
partially visually impaired or completely blind people. While the former can still partially
use the GUI, the latter have to completely rely on assistive technologies such as software
which reads the screen to them (screen readers).
Don't
Then try to use OTRS with the help of a screen reader and your keyboard only. This should
give you an idea of how it will feel for a blind person.
204
• WCAG 2.0
WCAG has different levels of accessibility support. We currently plan to support level A,
as AA and AAA deal with matters that seem not relevant for OTRS.
• WAI-ARIA 1.0
6.3. Implementation guidelines
6.3.1. Provide alternatives for non-text content
Goal: All non-text content that is presented to the user has a text alternative that serves
the equivalent purpose. (WCAG 1.1.1)
It is very important to understand that screen readers can only present textual information
and available metadata to the user. To give you an example, whenever a screen reader
sees <a href="#" class="CloseLink"></a>, it can only read "link" to the user, but not
the target of this link. With a slight improvement, it would be accessible: <a href="#"
class="CloseLink" title="Close this widget"></a>. In this case the user would
hear "link close this widget", voila!
It is important to always formulate the text in a most "speaking" way. Just imagine it is
the only information that you have. Will it help you? Can you understand its purpose just
by hearing it?
• Rule: Wherever possible, use speaking texts and formulate in real, understandable and
precise sentences. "Close this widget" is much better than "Close", because the latter
is redundant.
• Rule: Links always must have either text content that is spoken by the screen reader (<a
href="#" >Delete this entry</a>), or a title attribute (<a href="#" title="Close
this widget"></a>).
• Rule: Images must always have an alternative text that can be read to the user (<img
src="house.png" alt="Image of a house" />).
The title tag is the first thing a user hears from the screen reader when opening a web
page. For OTRS, there is also always just one h1 element on the page, indicating the cur-
rent page (it contains part of the information from title). This navigational information
helps the user to understand where they are, and what the purpose of the current page is.
205
• Rule: Always give a precise title to the page that allows the user to understand where
they currently are.
Screen readers can use the built-in document structure of HTML (headings h1 to h6) to
determine the structure of a document and to allow the user to jump around from section
to section. However, this is not enough to reflect the structure of a dynamic web applica-
tion. That's why ARIA defines several "landmark" roles that can be given to elements to
indicate their navigational significance.
To keep the validity of the HTML documents, the role attributes (ARIA landmark roles)
are not inserted into the source code directly, but instead by classes which will later be
used by the JavaScript functions in OTRS.UI.Accessibility to set the corresponding
role attributes on the node.
• Rule: Use WAI-ARIA Landmark Roles to structure the content for screen readers.
For navigation inside of <form< elements, it is necessary for the impaired user to know
what each input elements purpose is. This can be achieved by using standard HTML <la-
bel> elements which create a link between the label and the form element.
When an input element gets focus, the screen reader will usually read the connected
label, so that the user can hear its exact purpose. An additional benefit for seeing users
is that they can click on the label, and the input element will get focus (especially helpful
for checkboxes, for example).
• Rule: Provide <label> elements for all form element (input, select, textarea) fields.
• Rule: For interactions, always use elements that can receive focus, such as a, input,
select and button.
• Rule: Make sure that the user can always identify the nature of the interaction (see rules
about non-textual content and labelling of form elements).
206
Goal: Make dynamic changes known to the user.
A special area of accessibility problems are dynamic changes in the user interface, ei-
ther by JavaScript or also by AJAX calls. The screen reader will not tell the user about
changes without special precautions. This is a difficult topic and cannot yet be completely
explained here.
• Rule: Always use the validation framework OTRS.Validate for form validation.
This will make sure that the error tooltips are being read by the screen reader. That way
the blind user a) knows the item which has an error and b) get a text describing the error.
• Rule: Use the OTRS.UI.Dialog framework to create modal dialogs. These are already
optimized for accessibility.
• Rule: Each page must identify its own main language so that the screen reader can
choose the right speech synthesis engine.
7. Unit Tests
OTRS provides a test suite which can be used to develop and run unit tests for all system
related code.
Every test file should ideally instantiate unit test helper object at the start, so it can benefit
from some built-in methods provided by it:
# --
# Copyright (C) 2001-2017 OTRS AG, http://otrs.com/
# --
# This software comes with ABSOLUTELY NO WARRANTY. For details, see
# the enclosed file COPYING for license information (AGPL). If you
# did not receive this file, see http://www.gnu.org/licenses/agpl.txt.
# --
use strict;
use warnings;
use utf8;
$Kernel::OM->ObjectParamAdd(
'Kernel::System::UnitTest::Helper' => {
RestoreDatabase => 1,
},
);
my $Helper = $Kernel::OM->Get('Kernel::System::UnitTest::Helper');
207
By providing RestoreDatabase parameter to helper constructor, any database statement
executed during the unit test will be rolled back at the end, making sure no permanent
change has been done.
Like any other test suite, OTRS provides assertion methods which can be used to test
conditions. For example, this is how we create a test user and test that it has been indeed
created:
my $UserLogin = $Helper->TestUserCreate();
my $UserID = $UserObject->UserLookup( UserLogin => $UserLogin );
$Self->True(
$UserID,
"Test user $UserID created"
);
Please consult API section below for complete list of assertion methods.
It's always good practice to create random data in unit tests, which can help distinguish it
from previously added data. Use random methods from API to get the strings and include
them in your parameters:
my $RandomID = $Helper->GetRandomID();
$Self->True(
$GroupID,
"Test group $GroupID created"
);
Good developers make their unit test easy to maintain. Consider putting all test cases
in an array and then iterate over them with some code. This will provide an easy way to
extend the test later:
#
# Tests for CalendarCreate()
#
my @Tests = (
{
Name => 'CalendarCreate - No params',
Config => {},
Success => 0,
},
{
Name => 'CalendarCreate - All required parameters',
Config => {
CalendarName => "Calendar-$RandomID",
Color => '#3A87AD',
GroupID => $GroupID,
UserID => $UserID,
},
Success => 1,
},
{
Name => 'CalendarCreate - Same name',
Config => {
CalendarName => "Calendar-$RandomID",
208
Color => '#3A87AD',
GroupID => $GroupID,
UserID => $UserID,
},
Success => 0,
},
);
# check data
if ( $Test->{Success} ) {
for my $Key (qw(CalendarID GroupID CalendarName Color CreateTime CreateBy ChangeTime
ChangeBy ValidID)) {
$Self->True(
$Calendar{$Key},
"$Test->{Name} - $Key exists",
);
}
KEY:
for my $Key ( sort keys %{ $Test->{Config} } ) {
next KEY if $Key eq 'UserID';
$Self->IsDeeply(
$Test->{Config}->{$Key},
$Calendar{$Key},
"$Test->{Name} - Data for $Key",
);
}
}
else {
$Self->False(
$Calendar{CalendarID},
"$Test->{Name} - No success",
);
}
}
You also need to have an instance of the OTRS web frontend running on the FQDN that
is configured in your local OTRS's Config.pm file. This OTRS instance must use the same
database that is configured for the unit tests.
7.3. Testing
To run your tests, just use bin/otrs.Console.pl Dev::UnitTest::Run --test Calen-
dar to use scripts/test/Calendar.t.
209
All 97 tests passed.
shell:/opt/otrs>
You can even run several tests at once, just supply additional test arguments to the com-
mand:
Provide --verbose argument in order to see messages about successful tests too. Any
errors encountered during testing will be displayed regardless of this switch, provided
they are actually raised in the test.
True()
This function tests whether given scalar value is a true value in Perl.
$Self->True(
1,
'Scalar 1 is always evaluated as true'
);
False()
This function tests whether given scalar value is a false value in Perl.
$Self->False(
0,
'Scalar 0 is always evaluated as false'
);
Is()
This function tests whether the given scalar variables are equal.
$Self->Is(
$A,
$B,
210
'Test Name',
);
IsNot()
This function tests whether the given scalar variables are unequal.
$Self->IsNot(
$A,
$B,
'Test Name'
);
IsDeeply()
This function compares complex data structures for equality. $A and $B have to be
references.
$Self->IsDeeply(
$A,
$B,
'Test Name'
);
IsNotDeeply()
This function compares complex data structures for inequality. $A and $B have to be
references.
$Self->IsNotDeeply(
$A,
$B,
'Test Name'
);
Besides this, unit test helper object also provides some helpful methods for common test
conditions. For full reference, please see the online API reference of Kernel::System::U-
nitTest::Helper.
GetRandomID()
This function creates a random ID that can be used in tests as a unique identifier. It is
guaranteed that within a test this function will never return a duplicate.
Note
Please note that these numbers are not really random and should only be used
to create test data.
my $RandomID = $Helper->GetRandomID();
# $RandomID = 'test6326004144100003';
TestUserCreate()
This function creates a test user that can be used in tests. It will be set to invalid
automatically during the destructor. It returns the login name of the new user, the
password is the same.
211
my $TestUserLogin = $Helper->TestUserCreate(
Groups => ['admin', 'users'], # optional, list of groups to add this user
to (rw rights)
Language => 'de', # optional, defaults to 'en' if not set
);
FixedTimeSet()
This functions makes it possible to override the system time as long as this object
lives. You can pass an optional time parameter that should be used, if not, the current
system time will be used.
Note
All calls to methods of Kernel::System::Time and Kernel::System::Date-
Time will use the given time afterwards.
FixedTimeUnset()
FixedTimeAddSeconds()
This functions adds a number of seconds to the fixed system time which was previously
set by FixedTimeSet(). You can pass a negative value to go back in time.
ConfigSettingChange()
Note
Please note that this will not work correctly in clustered environments.
$Helper->ConfigSettingChange(
Valid => 1, # (optional) enable or disable setting
Key => 'MySetting', # setting name
Value => { ... } , # setting value
);
CustomCodeActivate()
This function will temporarily include custom code in the system. For example, you
may use this to redefine a subroutine from another class. This change will persist for
remainder of the test. All code will be removed when the Helper object is destroyed.
Note
Please note that this will not work correctly in clustered environments.
212
$Helper->CustomCodeActivate(
Code => q^
use Kernel::System::WebUserAgent;
package Kernel::System::WebUserAgent;
use strict;
use warnings;
{
no warnings 'redefine';
sub Request {
my $JSONString = '{"Results":{},"ErrorMessage":"","Success":1}';
return (
Content => \$JSONString,
Status => '200 OK',
);
}
}
1;^,
Identifier => 'News', # (optional) Code identifier to include in file name
);
ProvideTestDatabase()
This function will provide a temporary database for the test. Please first define test
database settings in Kernel/Config.pm, i.e:
$Self->{TestDatabase} = {
DatabaseDSN => 'DBI:mysql:database=otrs_test;host=127.0.0.1;',
DatabaseUser => 'otrs_test',
DatabasePw => 'otrs_test',
};
The method call will override global database configuration for duration of the test,
i.e. temporary database will receive all calls sent over system DBObject.
All database contents will be automatically dropped when the Helper object is de-
stroyed.
This method returns 'undef' in case the test database is not configured. If it is con-
figured, but the supplied XML cannot be read or executed, this method will die() to
interrupt the test with an error.
$Helper->ProvideTestDatabase(
DatabaseXMLString => $XML, # (optional) OTRS database XML schema to execute
# or
DatabaseXMLFiles => [ # (optional) List of XML files to load and execute
'/opt/otrs/scripts/database/otrs-schema.xml',
'/opt/otrs/scripts/database/otrs-initial_insert.xml',
],
);
213
Appendix A. Additional
Resources
otrs.com
The OTRS website with source code, documentation and news is available at
www.otrs.com. Here you can also find information about professional services and
OTRS Administrator training seminars from OTRS Group, the creator of OTRS.
The OTRS developer API documentation is available for Perl and JavaScript.
214