We’ve already seen some examples of how you can modify RT’s behavior through custom scrips, templates, and actions, but RT can be customized even further. You also may need to work on RT’s core code, either to develop features to submit back to the core developers or to fix bugs that you find.
Enabling RT’s development allows several features that make developing RT and RT extensions much simpler. There are several ways you can do this. You can enable RT’s “developer mode” feature when running the configure script by adding the —with-devel-mode
switch.
You also can edit the etc/RT_SiteConfig.pm file to include this line:
Set($DevelMode => 1);
The —with-devel-mode
configuration option simply adds this line to the etc/RT_Config.pm file.
Turning on development mode disables the static_source
and use_object_files
Mason features, which means that components are re-parsed for every request. This guarantees that changes to components will be seen immediately.
It also enables the use of Module::Refresh
, which checks for changes to modules on each new request. Without this, it would be necessary to restart the HTTP server every time a module was changed.
Most of RT’s core classes are designed to allow you to customize their behavior through a mechanism called overlays. You also can use subclasses in some cases, or modules such as Damian Conway’s Hook::LexWrap
.
Overlays are the easiest way to make major changes to RT’s core classes. If you look at the RT::User
class itself, you will see that it contains surprisingly little code.
Near the end, you’ll see these lines:
eval "require RT::User_Overlay"; if ($@ && $@ !~ qr{^Can't locate RT/User_Overlay.pm}) { die $@; } eval "require RT::User_Vendor"; if ($@ && $@ !~ qr{^Can't locate RT/User_Vendor.pm}) { die $@; } eval "require RT::User_Local"; if ($@ && $@ !~ qr{^Can't locate RT/User_Local.pm}) { die $@; }
This is the core of RT’s overlay mechanism. The RT::User_Overlay
file ships with RT itself and contains most of the real core logic for the user class. It is important to note that this module’s package declaration is RT::User
.
The RT::User_Vendor
module would be created by an operating system distribution like Debian or FreeBSD, and the RT::User_Local
module is where per-installation custom code should be placed. Neither of these modules exist in a default RT installation.
This mechanism is somewhat unusual, and you may wonder why RT doesn’t just use subclasses. The first reason is that many of RT’s core database-based classes are auto-generated from the database schema. Additionally, using subclasses would require all classes that use RT::User
(for example) to use the appropriate subclass instead, which requires a potentially complex configuration mechanism and could be a bit of a headache.
The downside is that you cannot use SUPER
to call the code you’ve overridden. If you need to do that, you will need to use Hook::LexWrap
, or assign another name to the original subroutine:
BEGIN { *SUPER_SomeMethod = \&SomeMethod }; sub SomeMethod { my $self = shift; # ...some preprocessing... $self->SUPER_SomeMethod(@_); # ...some postprocessing... }
Let’s assume that you’ve created a module RT::User_Local
, and you’ve overridden the CurrentUserHasRight()
method. When you go to restart RT, you’ll get a warning like "Subroutine CurrentUserHasRight
redefined at ... RT/User_Local.pm line 42.”
This is not a helpful warning, since you already know you redefined that subroutine. You can silence it by adding this line to the top of your module:
no warnings 'redefine';
In a default RT setup, all of the user data is stored in RT’s database. But what if you want to get some of the user data from another source, like an LDAP directory or a different database? The answer is to create a User_Local.pm
overlay module.
Here is a simple example that overrides the CanonicalizeUserInfo()
method to lookup the user’s real name using an in-house module:
package RT::User; use strict; no warnings 'redefine'; use Yoyodyne::User; sub CanonicalizeUserInfo { my $self = shift; my $args = shift; my $user = Yoyodyne::User->new( $args->{EmailAddress} ); $args->{RealName} = $user->name if $user; return 1; } 1;
This method, CanonicalizeUserInfo()
, does nothing in the core RT classes and is explicitly provided to make it easy to customize user creation.
You can use the Hook::LexWrap
module to achieve something similar to overriding a method in a subclass. Hook::LexWrap
lets you insert pre- and post-call wrappers for a method or function call. Code run before the method can change the arguments being passed in, and code run after can change the return value.
Hook::LexWrap
calls the wrapper code with an extra argument at the end of @_
. If you set this value in your pre-call code, then the main method and post-call code will not be called. You can use this to short-circuit a method call and force a different return value.
Here’s an example that takes advantage of that to prevent the resolution of a ticket which has never had a reply:
package RT::Ticket; use strict; use Hook::LexWrap; wrap 'SetStatus' => pre => \&check_for_replies; sub check_for_replies { my $self = shift; my $transactions = RT::Transactions->new( $self->CurrentUser ); $transactions->LimitToTicket( $self->Id ); $transactions->Limit( FIELD => 'Type', VALUE => 'Correspond', ); unless ( $transactions->Count ) { $_[-1] = [0, $self->loc('Ticket cannot be resolved without a reply')]; } } 1;
This code would go in local/lib/RT/Ticket_Local.pm.
The wrapper code short-circuits the call to SetStatus()
if the ticket has no replies. This is done simply by setting the value of the last argument to the wrapper. If this argument is an array reference, then Hook::LexWrap
will turn it into an array if the original method was called in array context.
If the ticket has replies, then the wrapper does not set this value, so the original method is called in the normal fashion.
When adding new functionality to RT, you may need to implement access controls. RT has an access control system that you can use in your extensions. You also can add new rights to the system if necessary.
To check that a user has a specific right, simply call HasRight()
on that user object. This method is usually called with two arguments, Right
and Object
. Right
is simply the name of the right to be checked, such as SuperUser or ModifyTicket. Object
is the object the user is trying to access. The method will return a boolean indicating whether the user has the given right.
For example, if we have a queue and want to see if the user can see that queue, we call:
$user->HasRight( Right => 'SeeQueue', Object => $queue );
Many classes define a convenience method called CurrentUserHasRight()
, which simply takes a single unnamed argument, the name of the right. So if we want to know if the current user has the “SeeQueue” right, we call:
$queue->CurrentUserHasRight('SeeQueue');
Some classes define other rights-related convenience methods. See the POD documentation for individual classes for more details.
Currently, rights are defined simply as a global hash reference in RT::Group_Overlay
, RT::Queue_Overlay
, and RT::CustomField_Overlay
. To add a new right, you can use this boilerplate in the appropriate *_Local.pm module:
$RIGHTS = { SeeDeadPeople => "members of this group are allowed to see dead people", %$RIGHTS, };
Since the local overlay is always loaded last, it is safe to assume that $RIGHTS
was populated already by the RT core code.
Profiling RT code is simple if you use the standalone HTTP server that comes with RT. [18] Profiling requires the following steps:
Start the standlone HTTP server with the Perl profiling flag (-d:DProf
).
Run many requests that exercise the same code. This is necessary in order to ensure that the cost of simply starting up the server does not outweigh the cost of the code you want to profile.
Exit cleanly, do not just abort the HTTP server process. This makes sure that the state of the Perl interpreter does not become corrupted.
The last step requires that we add a simple extra top-level Mason component, which we will call exit.html. Place this in local/html under your RT installation directory. The component should look like this:
% exit;
Of course, this is not something you want in a production installation, since it would let anyone abort a process with a simple HTTP request.
To start the standalone server with profiling turned on, run it as, /path/to/perl -d:DProf /path/to/standlone_httpd.
Then run the same request many times. To exit, simply call the exit.html component, which should be available via a URL of /exit.html.
When the process exits, the Perl profiler leaves behind a file tmon.out. Use the dprofpp
script to see the results of the profiling in various formats.
If you have RT’s development mode turned on, you may want to turn it off when profiling, as development mode adds non-trivial overhead to each request that is not present in a production environment.
While it’s always nice when your code runs perfectly the first time you use it, you probably will have to spend some time debugging your RT extensions.
Debugging web applications can be tricky. If you can write a standalone script to exercise a piece of code, you can use Perl’s debugger. Run perldoc perldebug
for details.
If your code is dependent on being run in the web environment, then you cannot use the debugger.
You can use RT’s own logging system in your extensions, which is useful for debugging. RT uses Log::Dispatch
from CPAN to implement debugging. This module has a very simple API for logging messages. You simply call a method name matching the log level you want to use, and pass it a message:
$RT::Logger->debug("entering bake_cake method"); $RT::Logger->critical("cannot contact the mothership!");
RT configures Log::Dispatch
to add additional information to your message, including a timestamp and the file and line number where the message was generated. Logging can be configured in the etc/RT_SiteConfig.pm file to go to the screen, a file, or to use syslog.
For development, you should configure at least one of the log outputs to run at the “debug” level, which is the lowest posssible level, ensuring that you see all the messages that are generated.
RT has a test infrastructure that you should use if you are doing core RT development. This helps ensure that any changes you make do not break existing functionality and adding tests for bug fixes or new features makes it easier for RT’s core developers to integrate patches.
There are two make targets that can run regression tests. Running the tests installs RT to whatever location was specified when configure was run. You may want to run configure with —enable-layout=inplace
, which will make RT simply use the files in the current location.
The regression
target will use apachectl to start and stop the Apache server for some tests. It assumes that whatever apachectl it is configured to use controls a server that is configured to run RT. The regression-noapache
target will start the standalone HTTP server on port 80 to run tests against.
The tests use the standard Perl test harness. Tests are written in standard Perl and grouped into test files. As each test file runs, it prints a message showing the status of that file:
lib/t/autogen/autogen-RT-GroupMembers_Overlay............ok
This means that all the tests in this file ran correctly. If there are errors, these will be printed separately.
When all the tests have finished running, there will be a summary:
Failed Test Stat Wstat Total Fail Failed List of Failed ------------------------------------------------------------------------------- lib/t/regression/02basic_web.t 255 65280 1 0 0.00% ?? lib/t/regression/03web_compiliati 255 65280 1 0 0.00% ?? lib/t/regression/06mailgateway.t 15 3840 40 15 37.50% 4 7 9 16 18-19 21 25-26 28 32-33 35 38 40 lib/t/regression/08web_cf_access. 3 768 4 3 75.00% 2-4 lib/t/regression/19-rtname.t 255 65280 1 0 0.00% ??
This shows which test files had problems and which tests failed. If the list of failed tests is simply ??
, that means that the test file probably died before generating any output.
If you want to run a specific file, you can use the prove utility that comes with recent versions of the Test::Harness
module. Before running tests, you should clean out the test database by running make regression-reset-db
. Then you can use prove to run a single file:
prove -Ilib -v lib/t/setup_regression.t lib/t/regression/19-rtname.t
The -Ilib
switch adds lib to Perl’s library path. You should change this to the appropriate location. The -v
switch turns on verbose output. This is helpful for fixing individual test failures. The first test, lib/t/setup_regression.t, ensures that the environment is ready for further regression tests. This should always be run before any other individual tests. After that, simply put the path(s) of the test file(s) you want to run.
If you look inside an RT core module like lib/RT/User_Overlay.pm, you will notice sections like this:
=begin testing ok(require RT::User); =end testing
Code inside this block will be extracted and turned into a test file when you run make regression
. This is done with the testify-pods
make action, which can be run separately. If you are working on the RT core code, you are encouraged to add tests to these blocks. Ideally, there should be one test block per method, though not all methods currently have tests.
Tests extracted from modules all share a single lexical scope, so a test that occurs later in the file can refer to variables created previously.
The tests are extracted into lib/t/autogen, one file per module.
When writing tests
, you can use any of the testing functions provided by Test::More
, as this is automatically loaded in each auto-generated test file. If you want to use other testing modules, you will need to load them explicitly.
This is a big topic, and there are a lot of good books on testing that cover the topic in great depth. Here are some quick guidelines:
Test one thing at a time, which means test one method with one set of arguments at once.
Remember to test both success and failure conditions. If code should fail on certain inputs, make sure that it does so.
It’s probably better to have too many tests rather than too few. Some redundancy in testing is not a bad thing, and it may even turn up odd bugs, for example a method that fails after being called several times because of changes in an object’s internal state.
RT can be configured to run in a variety of languages and has a lot of support internally for internationalization.
Most of the text generated by RT is localized, except for log messages. Depending on what type of code you are writing, there are a few different ways to access RT’s localization functions.
Inside a module—such as a script condition or overlay—RT offers the methods loc()
and loc_fuzzy()
.
Inside Mason components, there is a globally available function loc()
which does the same thing. If you want to localize a piece of inline text inside a component, you can wrap it in a filtering component call, like this:
<&|/l>My text</&>
This calls a Mason component that runs your text through an I18N filter and outputs the result.
Whenever you generate a piece of text to be presented to the end user, you should use RT’s I18N framework. Under the hood, RT uses Locale::Maketext
and Locale::Maketext::Lexicon
to implement I18N.
To specify the string to localize, Locale::Maketext
uses bracket notation
, which is a mini-templating system. When localizing a string, you often need to interpolate variables. For example, if you want to localize “Found 6 tickets,” you want to make the number of tickets found a variable. In bracket notation that would be Found [*,_1,ticket]
. This tells the I18N system that the first parameter is some quantity of tickets. This allows it to properly pluralize “ticket.” The first part of the notation, *
, is shorthand for the quant
method in Locale::Maketext
.
Complete documentation of this notation can be found in the documentation for Locale::Maketext
, and there are plenty of examples of its usage in the RT core code.
Each localization of RT is contained in a single .po file installed under lib/RT/I18N. These files simply contain pairs of strings as found in the source, and their translated versions. For example, in the French translation, fr.po, we can see:
#: lib/RT/Group_Overlay.pm:1160 msgid "Group has no such member" msgstr "Un tel membre n'appartient pas au groupe"
The msgid
is what is passed in the call to the localization method, and the msgstr
is the translated version.
RT comes with a utility to generate an empty message catalog for a new translation. Simply run the script extract-message-catalog from the root of your RT installation and give it the filename for the new catalog you would like created.
The new catalog will create a new empty catalog, along with comments indicating where each msgid
comes from, as in the above example.
This script is not installed in a normal RT installation, but it is available from the source tarball.
RT has an active community, including folks working on extending RT. If you want to participate, the best places to start are the RT wiki at http://wiki.bestpractical.com/ and the RT email lists. See http://www.bestpractical.com/rt/lists.html for more information on the RT email lists.
If you have a question about developing an RT extension, the rt-devel list is the best place to ask.
Before reporting a bug, please check the current bug list at http://rt3.fsck.com/NoAuth/Buglist.html first. Bug reports should be sent to rt-bugs@bestpractical.com.
If you want to submit a patch to RT that makes major changes or adds a new feature, it is best to discuss it on the rt-devel list first. Patches for small bug fixes can be sent directly to rt-bugs@fsck.com along with the bug report.
If you’ve created an extension that you think would be generally useful for the RT community, you can package it and distribute it as a Perl module on CPAN, the Comprehensive Perl Archive Network.
All RT extensions placed on the CPAN should have a name starting with RTx
to distinguish them from core RT code.
You can use Module::Install::RTx
to help create an installer for your extension. It automates a number of RT-specific installation tasks, such as installing .po files for I18N, installing Mason components, and updating the database.
Here is an example Makefile.PL for a fictional RTx::MechaRepair
extension:
use inc::Module::Install; RTx('MechaRepair'); author('Dave Rolsky <dave@example.com>'); license('perl'); WriteAll();
Module::Install::RTx
looks for certain directories in the distribution like html or po and makes sure that files in there end up in the appropriate place when installed. See the Module::Install::RTx
documentation for details.
You can include SQL files to update the database under etc/. To change the database schema, use a file named schema.XXX, where XXX is the name of a DBI driver like “Pg” or “mysql.” SQL to update ACLs should be named acl.XXX. If you have a file named initialdata, this can be used to run a set of INSERT
or UPDATE
statements to update the database.
If any of these files are present, the end user installing your extension will be able to run make initdb
, which takes care of running rt-setup-database appropriately.
RT is open source software, which is made freely available under the terms of version 2 of the GNU GPL. When you release an extension, we encourage you to use an OSI-approved license that is compatible with RT’s. If you have any questions about appropriate licensing for your RT extensions, we encourage you to consult legal counsel.
[18] Here we cover only profiling
module code in lib/. To profile Mason components, use the MasonX::Profiler
module, which is available on CPAN.