WriteCustomAction

From Request Tracker Wiki
Jump to navigation Jump to search
The printable version is no longer supported and may have rendering errors. Please update your browser bookmarks and please use the default browser print function instead.

This text in Portuguese in WriteCustomActionBR

How to write custom action code

Introduction

Included in RT3 is the ability to create your own custom ticket actions via the Web UI. The RT3 Custom Scrip capability lets you access the RT API and is a very powerful tool for customising RT.

Basics

File:Example1ScripUI.png
Modify a scrip UI

Let's start with Web UI. Goto Configuration -> Globals -> Scrips -> New Scrip.

The Scrip UI will display the following selectors: Description, Condition, Action, Template and Stage. In the action selector you can pick actions based on files (modules). An outstanding action from this list is "User Defined". The rest of this document describes this action. So in the Action field, select "User Defined" from the menu.

Once you have selected "User Defined" from the Action Field, the following two Action code areas are enabled and used by RT:

  • Custom action preparation code
  • Custom action cleanup code


When a Transaction is created, RT does the following steps to enable all this magic and give you a chance to be a small God:

  • Select Scrips
  • Check applicability of scrips by executing Conditions
  • Execute preparation code scrip by scrip
  • Scrips that fail will be thrown away
  • Execute commit code of survived scrips proceeding scrip by scrip

When RT executes your perl Action code within this scrip, your code can become an actor upon the ticket. RT has already defined the variable $self. This variable represents an instance of class [=RT::Action::UserDefined], a subclass of RT::Action::Generic. You can get more info from perldoc and from the source code.

Instance $self provides your Action code with very useful methods:

$self->TransactionObj

Returns RT::Transaction instance, the transaction that has been created and caused all this.

$self->TicketObj

Returns RT::Ticket instance which represents the ticket. Our transaction was applied to it.

$self->TemplateObj

Returns the template (RT::Template) which was selected for this scrip.

Getting more info about these objects

You can get complete information about these objects from their POD (embedded documentation).

perldoc /opt/rt3/lib/RT/Ticket.pm
perldoc /opt/rt3/lib/RT/Ticket_Overlay.pm
perldoc /opt/rt3/lib/RT/Tickets.pm
perldoc /opt/rt3/lib/RT/Tickets_Overlay.pm
perldoc /opt/rt3/lib/RT/Transaction.pm
perldoc /opt/rt3/lib/RT/Transaction_Overlay.pm
perldoc /opt/rt3/lib/RT/Transactions.pm
perldoc /opt/rt3/lib/RT/Transactions_Overlay.pm
...

Simple example

Ok, let's try to change something.

Requirement: There is a support queue for special customers where each request must have high priority on ticket creation.

Preparation code:

# we don't need any preparation yet.
return 1;

Commit code:

$self->TicketObj->SetPriority( 100 );
return 1;

I hope that this example is understandable enough, but it has at least one weakness. I've hardcoded the priority value. Since RT lets the administrator define default final priority per queue, our code should reflect that.

my $qfp = $self->TicketObj->QueueObj->FinalPriority || 100;
$self->TicketObj->SetPriority( int( $qfp * 0.9 ) );
return 1;

This change to the logic first retrieves the FinalPriority of the current Queue or 100 if no FinalPriority is set for the Queue. Then it sets the Priority of this ticket to 90% of the retrieved Priority. The final 10% of the priority is reserved for very exclusive, super high priority requests.

Errors handling

It's important to check errors to protect you from headache. Most methods in RT that change something return a tuple ($status, $msg). If $status is not true value then something failed and $msg is error text describing the reason. Let's extend our code:

my $qfp = $self->TicketObj->QueueObj->FinalPriority || 100;
my ($status, $msg) = $self->TicketObj->SetPriority( int( $qfp * 0.9 ) );
unless ( $status ) {
    $RT::Logger->error("Couldn't change priority: $msg");
    return 0;
}
return 1;

What you can (not) do with scrips

You can manipulate almost any object in RT within a scrip.

  • update properties of tickets, for example set properties of tickets with commands in email
  • change linked tickets, for example OpenTicketOnAllMemberResolve and OpenDependantsOnResolve
  • extract info from messages, implement own workflow, create approvals and many-many more actions

Let's talk about impossible things you don't even want to try to do with a scrip:

  • you couldn't deny action scrip's triggered, for example you don't want to allow users to open ticket unless it has owner, you may think that you can create scrip "on ticket open block action if ticket has no owner", but it's impossible. I said impossible? no... sure you can create such scrip, but instead of preventing action you can revert it by setting status back to old value. yeah, this works but don't forget that it would be two transactions 'set open status' and 'set previouse status'. Each action could be applied to other scrips, so really it's not preventing action.
  • you couldn't run scrip at some time, remember RT runs scrips after creating transactions, but of course we have solution for this situation - rt-crontool.

How to be silent

Now you put SetXxxx calls all over the places in RT and suddenly note that strange transactions appear in tickets. They have creator RT_System and describe what you've done with your scrips. Sometimes it's better to be silent and not mislead users. These transactions also go through the steps described earlier and could trigger some conditions too. Just use the long form of SetXxx functions:

$TicketObj->_Set(Field => 'Priority', Value => 90, RecordTransaction => 0);

The zero in the RecordTransaction argument informs RT not to record the change as a new transaction.

How to change ticket custom field values

Step 1, get the custom field (CF) object or ID. Don't use the hardcoded CF ID from the database. [why?] Step 2, get the CF object by ticket object by ticket object (I hope you remember how to get a ticket object) and CF name.

...
my $CFName = 'MyCustomField';
my $CF = RT::CustomField->new( $RT::SystemUser );
$CF->LoadByNameAndQueue( Name => $CFName, Queue => $Ticket->Queue );
# RT has bug/feature until 3.0.10, you should load global CF yourself
unless( $CF->id ) {
  # queue 0 is special case and is a synonym for global queue
  $CF->LoadByNameAndQueue( Name => $CFName, Queue => '0' );
}

unless( $CF->id ) {
  $RT::Logger->error( "No field $CFName in queue ". $Ticket->QueueObj->Name );
  return undef;
}
...

Now we could add value to ticket:

my $Value = 'MyValue';
$Ticket->AddCustomFieldValue( Field => $CF, Value => $Value );

or

$Ticket->AddCustomFieldValue( Field => $CF, Value => $Value, RecordTransaction => 0 );

you also could use custom field id instead of object.

$Ticket->AddCustomFieldValue( Field => NN , Value => $Value );

Step by step or "Tickets, transactions and attachments"

Lets look what happens with RT from the beginning when a user clicks on create button in their browser. Web server gets request with queue id, ticket subject and body, owner and etc. RT fetches info from the request that is needed for Ticket record: its Queue id, Status, Subject, CurrentUser(Creator), and then runs the next code:

# init empty instance of RT::Ticket class
my $TicketObj = RT::Ticket->new( $session{'CurrentUser'} );

# create new record
# if you more familiar with SQL then it's INSERT
# this call is inherited from SearchBuilder API
my $id = $TicketObj->Create( %ARGS );

Ticket is created and it's time to record transaction into table. RT has all info for this: ticket's id that was recently created and transaction type - 'Create'.

# this call creates new transaction record
# this is very similar to situation with new ticket record
my $TransactionObj = $Ticket->_RecordTransaction( %ARGS );

Transactions in RT has an many-to-one mapping with ticket. One ticket => one or more transactions. You can get collection of transactions by calling Transactions method on a ticket object:

my $transactions = $TicketObj->Transactions;

Each transaction belongs to only one ticket and you get ticket object with the following code:

my $ticket = $TransactionObj->TicketObj;

As you can see RT still didn't use content that you wrote in the body. Content is an RT::Attachment object. You know how RT creates new record and know about Ticket-Transaction relation, the same relationship applies to Attachments and Transactions.

From "User Defined" to a module

User defined actions you write in the web UI are handy and quick way to write an action. However, at some point you want to re-use your action, make it configurable, edit it in more suitable editor rather than text area, make it more complex and consist of additional methods. So it's time to move from "User Defined" action to your first module file.

use strict;
use warnings;

package RT::Action::MyAction;
use base qw(RT::Action::Generic);

sub Prepare {
  my $self = shift;

  ... here goes preparation code ...

  return 1;
}

sub Commit {
  my $self = shift;

  ... here goes commit code ...

  return 1;
}

1;

Note: for RT4, replace RT::Action::Generic by RT::Action

That's it. Save it as lib/RT/Action/MyAction.pm (it is important that the file have the same name as the package ("MyAction" in the example)), fill in preparation and commit code then you can register your action in the DB. Use the following data file and AddDatabaseRecords instructions.

@ScripActions = (
  {
    Name        => 'My super duper action',
    Description => 'Super-puper action that does hell of a job' ,
    ExecModule => 'MyAction',
    Argument   => 'some argument if needed'
  },
);

Now your action in the DB and you can pick it for a scrip through the Web UI instead of picking "User Defined".

See also

Other documentation on this wiki that may help

GlobalObjects, ObjectModel

Special thanks

  • TimWilson, who was the main editor and reviewer