LinkObject
API changesWith 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.
This section lists changes that you need to examine when porting your package from OTRS 5 to 6.
In OTRS 6, a new module for date and time calculation was added:
Kernel::System::DateTime
. The module Kernel::System::Time
is now
deprecated and should not be used for new code anymore.
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 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.
Here's an example for porting time offset code to time zones:
my $TimeObject = $Kernel::OM->Get('Kernel::System::Time'); # Assume a time offset of 0 for this time object my $SystemTime = $TimeObject->TimeStamp2SystemTime( String => '2004-08-14 22:45:00' ); my $UserTimeZone = '+2'; # normally retrieved via config or param my $UserSystemTime = $SystemTime + $UserTimeZone * 3600; my $UserTimeStamp = $TimeObject->SystemTime2TimeStamp( SystemTime => $UserSystemTime );
Code using the new Kernel::System::DateTime
module:
my $DateTimeObject = $Kernel::OM->Create('Kernel::System::DateTime'); # This implicitly sets the configured OTRS time zone my $UserTimeZone = 'Europe/Berlin'; # normally retrieved via config or param $DateTimeObject->ToTimeZone( TimeZone => $UserTimeZone ); my $SystemTime = $DateTimeObject->ToEpoch(); # note that the epoch is independent from the time zone, it's always calculated for UTC my $UserTimeStamp = $DateTimeObject->ToString();
Please note that the returned time values with the new Get()
function in the Kernel::System::DateTime
module
are without leading zero instead of the old SystemTime2Date()
function in the Kernel::System::Time
module.
In the new Kernel::System::DateTime
module the function Format()
returns the date/time as string formatted
according to the given format.
For OTRS 6, a multi attachment upload functionality was added. To implement the multi attachment upload in other extensions it is necessary to remove the attachment part from the template file, also the JSOnDocumentComplete
parts (AttachmentDelete
and AttachmentUpload
). Please keep in mind, in some cases the JavaScript parts are already outsourced in Core.Agent.XXX
files.
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:
<label>[% Translate("Attachments") | html %]:</label> <div class="Field"> [% INCLUDE "FormElements/AttachmentList.tt" %] </div> <div class="Clear"></div>
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:
my $IsUpload = ( $ParamObject->GetParam( Param => 'AttachmentUpload' ) ? 1 : 0 );
Additional to that, the Attachment Layout Block needs to be replaced:
$LayoutObject->Block( Name => 'Attachment', Data => $Attachment, );
Replace it with this code:
push @{ $Param{AttachmentList} }, $Attachment;
If the module where you want to integrate multi upload 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):
for my $Attachment (@TicketAttachments) { $Attachment->{Filesize} = $LayoutObject->HumanReadableDataSize( Size => $Attachment->{Filesize}, ); }
When adding selenium unit tests for the modules you ported, please take a look at Selenium/Agent/MultiAttachmentUpload.t
for reference.
In OTRS 6, all admin modules should have a breadcrumb. The breadcrumb only needs to be added on the .tt
template file and should be placed right after the h1 headline on top of the file. Additionally, the headline should receive the class InvisibleText
to make it only visible for screen readers.
<div class="MainBox ARIARoleMain LayoutFixedSidebar SidebarFirst"> <h1 class="InvisibleText">[% Translate("Name of your module") | html %]</h1> [% BreadcrumbPath = [ { Name => Translate('Name of your module'), }, ] %] [% INCLUDE "Breadcrumb.tt" Path = BreadcrumbPath %] ...
Please make sure to add the correct breadcrumb for all levels of your admin module (e.g. Subaction
s):
[% BreadcrumbPath = [ { Name => Translate('Module Home Screen'), Link => Env("Action"), }, { Name => Translate("Some Subaction"), }, ] %] [% INCLUDE "Breadcrumb.tt" Path = BreadcrumbPath %]
Admin modules in OTRS 6 should not only have a Save button, but also a Save and finish button. Save should leave the user on the same edit page after saving, Save and finish should lead back to the overview of the entity the user is currently working on. Please see existing OTRS admin screens for reference.
<div class="Field SpacingTop SaveButtons"> <button class="Primary CallForAction" id="SubmitAndContinue" type="submit" value="[% Translate("Save") | html %]"><span>[% Translate("Save") | html %]</span></button> [% Translate("or") | html %] <button class="Primary CallForAction" id="Submit" type="submit" value="[% Translate("Save") | html %]"><span>[% Translate("Save and finish") | html %]</span></button> [% Translate("or") | html %] <a href="[% Env("Baselink") %]Action=[% Env("Action") %]"><span>[% Translate("Cancel") | html %]</span></a> </div>
OTRS 6 uses a new XML configuration file format
and the location of configuration files moved
from Kernel/Config/Files
to Kernel/Config/Files/XML
.
To convert existing XML configuration files to the new format and location, you can use the following
tool that is part of the OTRS framework:
bin/otrs.Console.pl Dev::Tools::Migrate::ConfigXMLStructure --source-directory Kernel/Config/Files Migrating configuration XML files... Kernel/Config/Files/Calendar.xml -> Kernel/Config/Files/XML/Calendar.xml... Done. Kernel/Config/Files/CloudServices.xml -> Kernel/Config/Files/XML/CloudServices.xml... Done. Kernel/Config/Files/Daemon.xml -> Kernel/Config/Files/XML/Daemon.xml... Done. Kernel/Config/Files/Framework.xml -> Kernel/Config/Files/XML/Framework.xml... Done. Kernel/Config/Files/GenericInterface.xml -> Kernel/Config/Files/XML/GenericInterface.xml... Done. Kernel/Config/Files/ProcessManagement.xml -> Kernel/Config/Files/XML/ProcessManagement.xml... Done. Kernel/Config/Files/Ticket.xml -> Kernel/Config/Files/XML/Ticket.xml... Done. Done.
OTRS 6 speeds up configuration file loading by dropping support for the old configuration format (1)
that just used sequential Perl code and had to be run by eval
and instead enforcing the new package-based format (1.1) for Perl configuration files.
OTRS 6+ can only load files with this format, please make sure to convert any custom developments to it
(see Kernel/Config/Files/ZZZ*.pm
for examples). Every Perl configuration file
needs to contain a package with a Load()
method.
In the past, Perl configuration files were sometimes misused as an autoload mechanism
to override code in existing packages. This is not necessary any more as OTRS 6 features a dedicated
Autoload
mechanism. Please see Kernel/Autoload/Test.pm
for a demonstration
on how to use this mechanism to add a method in an existing file.
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.
What was previously called SYNOPSIS
is now changed to DESCRIPTION
, as
a synopsis typically provides a few popular code usage examples and not a description of the module itself.
An additional synopsis can be provided, of course. Here's how an example:
=head1 NAME Kernel::System::ObjectManager - Central singleton manager and object instance generator =head1 SYNOPSIS # In toplevel scripts only! local $Kernel::OM = Kernel::System::ObjectManager->new(); # Everywhere: get a singleton instance (and create it, if needed). my $ConfigObject = $Kernel::OM->Get('Kernel::Config'); # Remove singleton objects and all their dependencies. $Kernel::OM->ObjectsDiscard( Objects => ['Kernel::System::Ticket', 'Kernel::System::Queue'], ); =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'); # On the second call, this returns the same ConfigObject as above. my $ConfigObject2 = $Kernel::OM->Get('Kernel::Config'); =cut sub Get { ... }
These changes lead to an improved online API documentation as can be seen in the ObjectManager documentation for OTRS 5 and OTRS 6.
With OTRS 6, all JavaScript - especially located in JSOnDocumentComplete
blocks - is
removed from template files and moved to JavaScript files instead. Only in very rare conditions JavaScript
needs to be placed within template files. For all other occurrences, 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 bindings etc.) if you register the JavaScript
file at the OTRS application. This is done by executing Core.Init.RegisterNamespace(TargetNS,
'APP_MODULE');
at the end of the namespace declaration within the JavaScript file.
Along with the refactoring of the JavaScript within template files (see above), the template files for the
rich text editor (RichTextEditor.tt
and CustomerRichTextEditor.tt
)
were removed as they are no longer necessary.
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, );
you now have to call:
$LayoutObject->SetRichTextParameters( Data => \%Param, );
Same rule applies for customer interface. Remove RichText blocks from CustomerRichTextEditor.tt
and
apply following code instead:
$LayoutObject->CustomerSetRichTextParameters( Data => \%Param, );
Adding translatable strings in JavaScript was quite difficult in OTRS. The string had to be translated in Perl or in the template and then sent to the JavaScript function. With OTRS 6, translation of strings is possible directly in the JavaScript file. All other workarounds, especially blocks in the templates only for translating strings, should be removed.
Instead, the new JavaScript translation namespace Core.Language
should be used to
translate strings directly in the JS file:
Core.Language.Translate('The string to translate');
It is also possible to handover JS variables to be replaced in the string directly:
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 replaced 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.
In Perl you only have to call:
$Self->{LayoutObject}->AddJSData( Key => 'KeyToBeAvailableInJS', Value => $YourData, );
The Value
parameter is automatically converted to a JSON object and can also contain
complex data.
In JavaScript you can get the data with:
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:
var DivID = 'MyDiv', DivText = 'Hello, world!'; $('<div />').addClass('CSSClass') .attr('id', DivID) .text(DivText) .appendTo('body');
First, make sure to create a new template file under
Kernel/Output/JavaScript/Templates/Standard
folder. In doing this, you should keep
following in mind:
Create a subfolder with name of your Module
.
You may reuse any existing subfolder structure but only if it makes sense for your component
(e.g. Agent/MyModule/
or Agent/Admin/MyModule/
).
Use .html.tmpl
as extension for template file.
Name templates succinctly and clearly in order to avoid confusion (i.e. good:
Agent/MyModule/SettingsDialog.html.tmpl
, bad:
Agent/SettingsDialogTemplate.html.tmpl
).
Then, add your HTML to the template file, making sure to use placeholders for any variables you might need:
<div id="{{ DivID }}" class="CSSClass"> {{ DivText | Translate }} </div>
Then, just get rendered HTML by calling Core.Template.Render
method with template path
(without extension) and object containing variables for replacement:
var DivHTML = Core.Template.Render('Agent/MyModule/SettingsDialog', { DivID: 'MyDiv', DivText: 'Hello, world!' }); $(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.
Here are some tips:
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.
Use | urlencode
for encoding URL parameters.
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.
Before OTRS 6, user permissions were stored in the session and passed to the LayoutObject
as
attributes, which were then in turn accessed to determine user permissions like if
($LayoutObject->{'UserIsGroup[admin]'}) { ... }
.
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::System::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', );
For OTRS 6, all extensions need to be checked and ported from $Ticket{SolutionTime}
to
$Ticket{Closed}
if TicketGet()
is called with the
Extended
parameter (see
bug#11872).
Additionally, the database column ticket.create_time_unix
was removed, and likewise the
value CreateTimeUnix
from the TicketGet()
result data.
Please use the value Created
(database column ticket.create_time
) instead.
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
handlers might need for further processing, e.g.:
SourceObject
Name of the link source object (e.g. Ticket
).
SourceKey
Key of the link source object (e.g. TicketID
).
TargetObject
Name of the link target object (e.g. FAQItem
).
TargetKey
Key of the link target object (e.g. FAQItemID
).
Type
Type of the link (e.g. ParentChild
).
State
State of the link (Valid
or Temporary
).
With these new events in place, any events specific for custom LinkObject
module
implementations 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:
<!-- 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> <!-- NEW STYLE --> <Setting Name="LinkObject::EventModulePost###1000-SampleModule" Required="0" Valid="1"> <Description Translatable="1">Event handler for sample link object module.</Description> <Navigation>Core::Event::Package</Navigation> <Value> <Hash> <Item Key="Module">Kernel::System::LinkObject::Event::SampleModule</Item> <Item Key="Event">(LinkObjectLinkAdd|LinkObjectLinkDelete)</Item> <Item Key="Transaction">1</Item> </Hash> </Value> </Setting>
In OTRS 6, changes to Article API have been made, in preparations for new Omni Channel infrastructure.
Article object now provides top-level article functions that do not involve back-end related data.
Following methods related to articles have been moved to
Kernel::System::Ticket::Article
object:
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'); 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.
ArticleFirstArticle()
(use
ArticleList( OnlyFirst => 1)
instead)
ArticleLastCustomerArticle()
(use
ArticleList( SenderType => 'customer', OnlyLast => 1)
or similar)
ArticleCount()
(use ArticleList()
instead)
ArticlePage()
(reimplemented in AgentTicketZoom
)
ArticleTypeList()
ArticleTypeLookup()
ArticleIndex()
(use ArticleList()
instead)
ArticleContentIndex()
To work with article data please use new article backend API. To get correct backend object for an article, please use:
BackendForArticle(%Article)
BackendForChannel( ChannelName => $ChannelName )
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.
my $ArticleBackendObject = $ArticleObject->BackendForArticle( TicketID => 42, ArticleID => 123 );
BackendForChannel()
returns the correct back end for a given communication channel.
my $ArticleBackendObject = $ArticleObject->BackendForChannel( ChannelName => 'Email' );
All other article data and related methods have been moved to separate backends. Every communication channel now has a dedicated backend API that handles article data and can be used to manipulate it.
OTRS 6 Free ships with some default channels and corresponding backends:
Email (equivalent to old email
article types)
Phone (equivalent to old phone
article types)
Internal (equivalent to old note
article types)
Chat (equivalent to old chat
article types)
While chat article backend is available in OTRS 6 Free, it is only utilized when system has a valid OTRS Business Solution™ installed.
Article data manipulation can be managed via following backend methods:
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.
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()
Note that ArticleAttachmentIndex()
parameters and behavior has changed. Instead of
old strip parameter use combination of new ExcludePlainText
,
ExcludeHTMLBody
and ExcludeInline
.
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) { # 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; # Get attachment index (without attachments). my %AtmIndex = $ArticleBackendObject->ArticleAttachmentIndex( ArticleID => $Article->{ArticleID}, UserID => $UserID, ); next ARTICLE if !%AtmIndex; 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} ); push @Attachments, \%Attachment; } # Include attachment data in article hash. $Article->{Atms} = \@Attachments; }
To make article indexing more generic, article backends now provide information necessary for properly
indexing article data. Index will be created similar to old StaticDB
mechanism and
stored in a dedicated article search table.
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:
Old parameter | New parameter |
---|---|
From | MIMEBase_From |
To | MIMEBase_To |
Cc | MIMEBase_Cc |
Subject | MIMEBase_Subject |
Body | MIMEBase_Body |
AttachmentName | MIMEBase_AttachmentName |
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, e.g. 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.
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 previously mentioned steps and it can be used for multiple settings at once.
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) }, ... ], );
Note that LinkObject
was slightly modified in the OTRS 6 and methods
LinkList()
and LinkKeyList()
might return different
result if Direction
parameter is used. Consider changing Direction
.
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', Type => 'ParentChild', Direction => 'Source', UserID => 1, );
As part of email handling improvements for OTRS 6, a new logging mechanism was added to OTRS 6, exclusively used for incoming and outgoing communications. All PostMaster filters were enriched with this new Communication Log API, which means any additional filters coming with packages should also leverage the new log feature.
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
.
As part of email handling improvements for OTRS 6, all emails are now sent asynchronously, that means they are saved in a queue for future processing.
To the unit tests that depend on emails continue to work properly is necessary to force the processing of the email queue.
Make sure to start with a clean 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} ); }
Or process only the ones created during the tests:
my $MailQueueObject = $Kernel::OM->Get('Kernel::System::MailQueue'); my $QueueItems = $MailQueueObject->List(); 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.
The widgets in the ticket zoom screen have been improved to work in a more generic way. With OTRS 6, it is now possible to add new widgets for the ticket zoom screen via the SysConfig. It is possible to configure the used module, the location of the widget (e.g. Sidebar) and if the content should be loaded synchronously (default) or via AJAX.
Here is an example configuration for the default widgets:
<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>
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.
This section lists changes that you need to examine when porting your package from OTRS 4 to 5.
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/Templates/Standard
. Please perform this migration also in your code.
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 $LayoutObject->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>
This will cause the block CustomerTable
in AgentTicketZoom.tt
to be
wrapped in HTML comments each time it is rendered:
<!--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.
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.
The operation TicketGet()
returns dynamic field data from ticket and articles differently than
in OTRS 4. Now they are cleanly separated from the rest of the static ticket and article fields - they are
now grouped in a list called DynamicField
. Please adapt any applications using this
operation accordingly.
# 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', }, ], }, ]
The new statistics GUI provides a preview for the current configuration. This must be implemented in the
statistic modules and usually returns fake / random data for speed reasons. So for any dynamic (matrix)
statistic that provides the method GetStatElement()
you should also add a method
GetStatElementPreview()
, and for every dynamic (table) statistic that provides
GetStatTable()
you should accordingly add GetStatTablePreview()
. Otherwise the
preview in the new statistics GUI will not work for your statistics. You can find example implementations in
the default OTRS statistics.
Until OTRS 5, the Perl module PDF::API2
was not present on all systems. Therefore a
fallback HTML print mode existed. With OTRS 5, the module is now bundled and HTML print was dropped.
$LayoutObject->PrintHeader()
and PrintFooter()
are not available any more. Please
remove the HTML print fallback from your code and change it to generate PDF if necessary.
Until OTRS 5, translatable strings could not be extracted from Perl code and Database XML definitions. This
is now possible and makes dummy templates like AAA*.tt
obsolete. Please see this section for details.
This section lists changes that you need to examine when porting your package from OTRS 3.3 to 4.
Up to OTRS 4, objects used to be created both centrally and also locally and then handed down to all objects
by passing them to the constructors. With OTRS 4 and later versions, there is now an
ObjectManager
that centralizes singleton object creation and access.
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; # create common objects my %CommonObject = (); $CommonObject{ConfigObject} = Kernel::Config->new(); $CommonObject{EncodeObject} = Kernel::System::Encode->new(%CommonObject); $CommonObject{LogObject} = Kernel::System::Log->new( LogPrefix => 'OTRS-otrs.CheckDB.pl', ConfigObject => $CommonObject{ConfigObject}, ); $CommonObject{MainObject} = Kernel::System::Main->new(%CommonObject); $CommonObject{DBObject} = Kernel::System::DB->new(%CommonObject);
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; # create common objects local $Kernel::OM = Kernel::System::ObjectManager->new( 'Kernel::System::Log' => { LogPrefix => 'OTRS-otrs.CheckDB.pl', }, ); # get database object my $DBObject = $Kernel::OM->Get('Kernel::System::DB');
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::Perl::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 accessing objects: don't
store them in $Self
any more (unless needed for specific reasons). Just fetch and use the
objects on demand like $Kernel::OM->Get('Kernel::System::Log')->Log(...)
. This also has the
benefit that the Log
object will only be created 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 correct 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 ) = @_; # allocate new hash for object my $Self = {}; bless( $Self, $Type ); # check needed objects for my $Object (qw(DBObject ConfigObject LogObject EncodeObject MainObject)) { $Self->{$Object} = $Param{$Object} || die "Got no $Object!"; } $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; # get list from database return if !$Self->{DBObject}->Prepare( SQL => 'SELECT id, name FROM valid' ); # fetch the result my %Data; while ( my @Row = $Self->{DBObject}->FetchrowArray() ) { $Data{ $Row[0] } = $Row[1]; } # 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 ) = @_; # allocate new hash for object my $Self = {}; bless( $Self, $Type ); $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; # get database object my $DBObject = $Kernel::OM->Get('Kernel::System::DB'); # get list from database return if !$DBObject->Prepare( SQL => 'SELECT id, name FROM valid' ); # fetch the result my %Data; while ( my @Row = $DBObject->FetchrowArray() ) { $Data{ $Row[0] } = $Row[1]; } # set cache $Kernel::OM->Get('Kernel::System::Cache')->Set( Type => $Self->{CacheType}, TTL => $Self->{CacheTTL}, Key => $CacheKey, Value => \%Data ); return %Data; }
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.
Since Kernel::System::Cache
is now also able to cache in-memory,
Kernel::System::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.
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.
The backend files of the scheduler moved from Kernel/Scheduler
to
Kernel/System/Scheduler
. If you have any custom Task Handler modules, you need to move
them also.
Code tags in SOPM files have to be updated. Please do not use $Self
any more. In the past
this was used to get access to OTRS objects like the MainObject
. Please use the
ObjectManager
now. Here is an example for the old style:
<CodeInstall Type="post"> # define function name my $FunctionName = 'CodeInstall'; # create the package name my $CodeModule = 'var::packagesetup::' . $Param{Structure}->{Name}->{Content}; # load the module if ( $Self->{MainObject}->Require($CodeModule) ) { # create new instance my $CodeObject = $CodeModule->new( %{$Self} ); 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>
Now this should be replaced by:
<CodeInstall Type="post"><![CDATA[ $Kernel::OM->Get('var::packagesetup::MyPackage')->CodeInstall(); ]]></CodeInstall>
With OTRS 4, the DTL template engine was replaced by Template::Toolkit. Please refer to the Templating section for details on how the new template syntax looks like.
These are the changes that you need to apply when converting existing DTL templates to the new Template::Toolkit syntax:
Table 4.1. Template Changes from OTRS 3.3 to 4
DTL Tag | Template::Toolkit tag |
$Data{"Name"} |
[% Data.Name %] |
$Data{"Complex-Name"} |
[% Data.item("Complex-Name") %] |
$QData{"Name"} |
[% Data.Name | html %] |
$QData{"Name", "$Length"} |
[% Data.Name | truncate($Length) | html %] |
$LQData{"Name"} |
[% Data.Name | uri %] |
$Quote{"Text", "$Length"} |
cannot be replaced directly, see examples below |
$Quote{"$Config{"Name"}"} |
[% Config("Name") | html %] |
$Quote{"$Data{"Name"}", "$Length"} |
[% Data.Name | truncate($Length) | html %] |
$Quote{"$Data{"Content"}","$QData{"MaxLength"}"} |
[% Data.Name | truncate(Data.MaxLength) | html %] |
$Quote{"$Text{"$Data{"Content"}"}","$QData{"MaxLength"}"} |
[% Data.Content | Translate | truncate(Data.MaxLength) | html %] |
$Config{"Name"} |
[% Config("Name") %] |
$Env{"Name"} |
[% Env("Name") %] |
$QEnv{"Name"} |
[% Env("Name") | html %] |
$Text{"Text with %s placeholders", "String"} |
[% Translate("Text with %s placeholders", "String") | html %] |
$Text{"Text with dynamic %s placeholders", "$QData{Name}"} |
[% Translate("Text with dynamic %s placeholders", Data.Name) | html %] |
'$JSText{"Text with dynamic %s placeholders", "$QData{Name}"}' |
[% Translate("Text with dynamic %s placeholders", Data.Name) | JSON %] |
"$JSText{"Text with dynamic %s placeholders", "$QData{Name}"}" |
[% Translate("Text with dynamic %s placeholders", Data.Name) | JSON %] |
$TimeLong{"$Data{"CreateTime"}"} |
[% Data.CreateTime | Localize("TimeLong") %] |
$TimeShort{"$Data{"CreateTime"}"} |
[% Data.CreateTime | Localize("TimeShort") %] |
$Date{"$Data{"CreateTime"}"} |
[% Data.CreateTime | Localize("Date") %] |
<-- dtl:block:Name -->...<-- dtl:block:Name --> |
[% RenderBlockStart("Name") %]...[% RenderBlockEnd("Name") %] |
<-- dtl:js_on_document_complete -->...<-- dtl:js_on_document_complete --> |
[% WRAPPER JSOnDocumentComplete %]...[% END %] |
<-- dtl:js_on_document_complete_placeholder --> |
[% PROCESS JSOnDocumentCompleteInsert %] |
$Include{"Copyright"} |
[% InsertTemplate("Copyright") %] |
There is also a helper script bin/otrs.MigrateDTLtoTT.pl
that will automatically port
the DTL files to Template::Toolkit syntax for you. It might fail if you have errors in your DTL, please
correct these first and re-run the script afterwards.
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.
Layout::Get()
is now deprecated. Please use Layout::Translate()
instead.
All occurrences of $Text{""}
in Perl code must now be replaced by calls to
Layout::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::AddJSOnDocumentComplete()
instead of injecting this as template data.
You can find an example for this in
Kernel/System/DynamicField/Driver/BaseSelect.pm
.
Please be careful with pre
output filters (the ones configured in
Frontend::Output::FilterElementPre
). They still work, but they will prevent
the template from being 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 templates via configuration setting.
The post
output filters
(Frontend::Output::FilterElementPost
) don't have such strong negative
performance effects. However, they should also be used carefully, and not for all templates.
With OTRS 4, we've also updated FontAwesome to a new version. As a consequence, the icons CSS classes have
changed. While previously icons were defined by using a schema like icon-{iconname}
, it
is now fa fa-{iconname}
.
Due to this change, you need to make sure to update all custom frontend module registrations 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-{iconname}"></i>
.
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.
If you use any custom ticket history types, you have to take two steps for them to be displayed correctly
in AgentTicketHistory
of OTRS 4+.
Firstly, you have to register your custom ticket history types via SysConfig. This could look like:
<ConfigItem Name="Ticket::Frontend::HistoryTypes###100-MyCustomModule" Required="1" Valid="1"> <Description Translatable="1">Controls how to display the ticket history entries as readable values.</Description> <Group>Ticket</Group> <SubGroup>Frontend::Agent::Ticket::ViewHistory</SubGroup> <Setting> <Hash> <Item Key="MyCustomType" Translatable="1">Added information (%s)</Item> </Hash> </Setting> </ConfigItem>
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.