1 # BEGIN BPS TAGGED BLOCK {{{
5 # This software is Copyright (c) 1996-2013 Best Practical Solutions, LLC
6 # <sales@bestpractical.com>
8 # (Except where explicitly superseded by other copyright notices)
13 # This work is made available to you under the terms of Version 2 of
14 # the GNU General Public License. A copy of that license should have
15 # been provided with this software, but in any event can be snarfed
18 # This work is distributed in the hope that it will be useful, but
19 # WITHOUT ANY WARRANTY; without even the implied warranty of
20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
21 # General Public License for more details.
23 # You should have received a copy of the GNU General Public License
24 # along with this program; if not, write to the Free Software
25 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
26 # 02110-1301 or visit their web page on the internet at
27 # http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
30 # CONTRIBUTION SUBMISSION POLICY:
32 # (The following paragraph is not intended to limit the rights granted
33 # to you to modify and distribute this software under the terms of
34 # the GNU General Public License and is only of importance to you if
35 # you choose to contribute your changes and enhancements to the
36 # community by submitting them to Best Practical Solutions, LLC.)
38 # By intentionally submitting any modifications, corrections or
39 # derivatives to this work, or any other work intended for use with
40 # Request Tracker, to Best Practical Solutions, LLC, you confirm that
41 # you are the copyright holder for those contributions and you grant
42 # Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable,
43 # royalty-free, perpetual, license to use, copy, create derivative
44 # works based on those contributions, and sublicense and distribute
45 # those contributions and any derivatives thereof.
47 # END BPS TAGGED BLOCK }}}
52 my $ticket = RT::Ticket->new($CurrentUser);
53 $ticket->Load($ticket_id);
57 This module lets you manipulate RT's ticket object.
81 use RT::URI::fsck_com_rt;
84 use Devel::GlobalDestruction;
87 # A helper table for links mapping to make it easier
88 # to build and parse links between tickets
91 MemberOf => { Type => 'MemberOf',
93 Parents => { Type => 'MemberOf',
95 Members => { Type => 'MemberOf',
97 Children => { Type => 'MemberOf',
99 HasMember => { Type => 'MemberOf',
101 RefersTo => { Type => 'RefersTo',
103 ReferredToBy => { Type => 'RefersTo',
105 DependsOn => { Type => 'DependsOn',
107 DependedOnBy => { Type => 'DependsOn',
109 MergedInto => { Type => 'MergedInto',
115 # A helper table for links mapping to make it easier
116 # to build and parse links between tickets
119 MemberOf => { Base => 'MemberOf',
120 Target => 'HasMember', },
121 RefersTo => { Base => 'RefersTo',
122 Target => 'ReferredToBy', },
123 DependsOn => { Base => 'DependsOn',
124 Target => 'DependedOnBy', },
125 MergedInto => { Base => 'MergedInto',
126 Target => 'MergedInto', },
131 sub LINKTYPEMAP { return \%LINKTYPEMAP }
132 sub LINKDIRMAP { return \%LINKDIRMAP }
142 Takes a single argument. This can be a ticket id, ticket alias or
143 local ticket uri. If the ticket can't be loaded, returns undef.
144 Otherwise, returns the ticket id.
151 $id = '' unless defined $id;
153 # TODO: modify this routine to look at EffectiveId and
154 # do the recursive load thing. be careful to cache all
155 # the interim tickets we try so we don't loop forever.
157 unless ( $id =~ /^\d+$/ ) {
158 $RT::Logger->debug("Tried to load a bogus ticket id: '$id'");
162 $id = $MERGE_CACHE{'effective'}{ $id }
163 if $MERGE_CACHE{'effective'}{ $id };
165 my ($ticketid, $msg) = $self->LoadById( $id );
166 unless ( $self->Id ) {
167 $RT::Logger->debug("$self tried to load a bogus ticket: $id");
171 #If we're merged, resolve the merge.
172 if ( $self->EffectiveId && $self->EffectiveId != $self->Id ) {
174 "We found a merged ticket. "
175 . $self->id ."/". $self->EffectiveId
177 my $real_id = $self->Load( $self->EffectiveId );
178 $MERGE_CACHE{'effective'}{ $id } = $real_id;
182 #Ok. we're loaded. lets get outa here.
190 Arguments: ARGS is a hash of named parameters. Valid parameters are:
193 Queue - Either a Queue object or a Queue Name
194 Requestor - A reference to a list of email addresses or RT user Names
195 Cc - A reference to a list of email addresses or Names
196 AdminCc - A reference to a list of email addresses or Names
197 SquelchMailTo - A reference to a list of email addresses -
198 who should this ticket not mail
199 Type -- The ticket's type. ignore this for now
200 Owner -- This ticket's owner. either an RT::User object or this user's id
201 Subject -- A string describing the subject of the ticket
202 Priority -- an integer from 0 to 99
203 InitialPriority -- an integer from 0 to 99
204 FinalPriority -- an integer from 0 to 99
205 Status -- any valid status (Defined in RT::Queue)
206 TimeEstimated -- an integer. estimated time for this task in minutes
207 TimeWorked -- an integer. time worked so far in minutes
208 TimeLeft -- an integer. time remaining in minutes
209 Starts -- an ISO date describing the ticket's start date and time in GMT
210 Due -- an ISO date describing the ticket's due date and time in GMT
211 MIMEObj -- a MIME::Entity object with the content of the initial ticket request.
212 CustomField-<n> -- a scalar or array of values for the customfield with the id <n>
214 Ticket links can be set up during create by passing the link type as a hask key and
215 the ticket id to be linked to as a value (or a URI when linking to other objects).
216 Multiple links of the same type can be created by passing an array ref. For example:
219 DependsOn => [ 15, 22 ],
220 RefersTo => 'http://www.bestpractical.com',
222 Supported link types are C<MemberOf>, C<HasMember>, C<RefersTo>, C<ReferredToBy>,
223 C<DependsOn> and C<DependedOnBy>. Also, C<Parents> is alias for C<MemberOf> and
224 C<Members> and C<Children> are aliases for C<HasMember>.
226 Returns: TICKETID, Transaction Object, Error Message
236 EffectiveId => undef,
241 SquelchMailTo => undef,
242 TransSquelchMailTo => undef,
246 InitialPriority => undef,
247 FinalPriority => undef,
258 _RecordTransaction => 1,
263 my ($ErrStr, @non_fatal_errors);
265 my $QueueObj = RT::Queue->new( RT->SystemUser );
266 if ( ref $args{'Queue'} eq 'RT::Queue' ) {
267 $QueueObj->Load( $args{'Queue'}->Id );
269 elsif ( $args{'Queue'} ) {
270 $QueueObj->Load( $args{'Queue'} );
273 $RT::Logger->debug("'". ( $args{'Queue'} ||''). "' not a recognised queue object." );
276 #Can't create a ticket without a queue.
277 unless ( $QueueObj->Id ) {
278 $RT::Logger->debug("$self No queue given for ticket creation.");
279 return ( 0, 0, $self->loc('Could not create ticket. Queue not set') );
283 #Now that we have a queue, Check the ACLS
285 $self->CurrentUser->HasRight(
286 Right => 'CreateTicket',
293 $self->loc( "No permission to create tickets in the queue '[_1]'", $QueueObj->Name));
296 my $cycle = $QueueObj->Lifecycle;
297 unless ( defined $args{'Status'} && length $args{'Status'} ) {
298 $args{'Status'} = $cycle->DefaultOnCreate;
301 unless ( $cycle->IsValid( $args{'Status'} ) ) {
303 $self->loc("Status '[_1]' isn't a valid status for tickets in this queue.",
304 $self->loc($args{'Status'}))
308 unless ( $cycle->IsTransition( '' => $args{'Status'} ) ) {
310 $self->loc("New tickets can not have status '[_1]' in this queue.",
311 $self->loc($args{'Status'}))
317 #Since we have a queue, we can set queue defaults
320 # If there's no queue default initial priority and it's not set, set it to 0
321 $args{'InitialPriority'} = $QueueObj->InitialPriority || 0
322 unless defined $args{'InitialPriority'};
325 # If there's no queue default final priority and it's not set, set it to 0
326 $args{'FinalPriority'} = $QueueObj->FinalPriority || 0
327 unless defined $args{'FinalPriority'};
329 # Priority may have changed from InitialPriority, for the case
330 # where we're importing tickets (eg, from an older RT version.)
331 $args{'Priority'} = $args{'InitialPriority'}
332 unless defined $args{'Priority'};
335 #TODO we should see what sort of due date we're getting, rather +
336 # than assuming it's in ISO format.
338 #Set the due date. if we didn't get fed one, use the queue default due in
339 my $Due = RT::Date->new( $self->CurrentUser );
340 if ( defined $args{'Due'} ) {
341 $Due->Set( Format => 'ISO', Value => $args{'Due'} );
343 elsif ( my $due_in = $QueueObj->DefaultDueIn ) {
345 $Due->AddDays( $due_in );
348 my $Starts = RT::Date->new( $self->CurrentUser );
349 if ( defined $args{'Starts'} ) {
350 $Starts->Set( Format => 'ISO', Value => $args{'Starts'} );
353 my $Started = RT::Date->new( $self->CurrentUser );
354 if ( defined $args{'Started'} ) {
355 $Started->Set( Format => 'ISO', Value => $args{'Started'} );
358 # If the status is not an initial status, set the started date
359 elsif ( !$cycle->IsInitial($args{'Status'}) ) {
363 my $Resolved = RT::Date->new( $self->CurrentUser );
364 if ( defined $args{'Resolved'} ) {
365 $Resolved->Set( Format => 'ISO', Value => $args{'Resolved'} );
368 #If the status is an inactive status, set the resolved date
369 elsif ( $cycle->IsInactive( $args{'Status'} ) )
371 $RT::Logger->debug( "Got a ". $args{'Status'}
372 ."(inactive) ticket with undefined resolved date. Setting to now."
379 # Dealing with time fields
381 $args{'TimeEstimated'} = 0 unless defined $args{'TimeEstimated'};
382 $args{'TimeWorked'} = 0 unless defined $args{'TimeWorked'};
383 $args{'TimeLeft'} = 0 unless defined $args{'TimeLeft'};
387 # Deal with setting the owner
390 if ( ref( $args{'Owner'} ) eq 'RT::User' ) {
391 if ( $args{'Owner'}->id ) {
392 $Owner = $args{'Owner'};
394 $RT::Logger->error('Passed an empty RT::User for owner');
395 push @non_fatal_errors,
396 $self->loc("Owner could not be set.") . " ".
397 $self->loc("Invalid value for [_1]",loc('owner'));
402 #If we've been handed something else, try to load the user.
403 elsif ( $args{'Owner'} ) {
404 $Owner = RT::User->new( $self->CurrentUser );
405 $Owner->Load( $args{'Owner'} );
407 $Owner->LoadByEmail( $args{'Owner'} )
409 unless ( $Owner->Id ) {
410 push @non_fatal_errors,
411 $self->loc("Owner could not be set.") . " "
412 . $self->loc( "User '[_1]' could not be found.", $args{'Owner'} );
417 #If we have a proposed owner and they don't have the right
418 #to own a ticket, scream about it and make them not the owner
421 if ( $Owner && $Owner->Id != RT->Nobody->Id
422 && !$Owner->HasRight( Object => $QueueObj, Right => 'OwnTicket' ) )
424 $DeferOwner = $Owner;
426 $RT::Logger->debug('going to deffer setting owner');
430 #If we haven't been handed a valid owner, make it nobody.
431 unless ( defined($Owner) && $Owner->Id ) {
432 $Owner = RT::User->new( $self->CurrentUser );
433 $Owner->Load( RT->Nobody->Id );
438 # We attempt to load or create each of the people who might have a role for this ticket
439 # _outside_ the transaction, so we don't get into ticket creation races
440 foreach my $type ( "Cc", "AdminCc", "Requestor" ) {
441 $args{ $type } = [ $args{ $type } ] unless ref $args{ $type };
442 foreach my $watcher ( splice @{ $args{$type} } ) {
443 next unless $watcher;
444 if ( $watcher =~ /^\d+$/ ) {
445 push @{ $args{$type} }, $watcher;
447 my @addresses = RT::EmailParser->ParseEmailAddress( $watcher );
448 foreach my $address( @addresses ) {
449 my $user = RT::User->new( RT->SystemUser );
450 my ($uid, $msg) = $user->LoadOrCreateByEmail( $address );
452 push @non_fatal_errors,
453 $self->loc("Couldn't load or create user: [_1]", $msg);
455 push @{ $args{$type} }, $user->id;
462 $RT::Handle->BeginTransaction();
465 Queue => $QueueObj->Id,
467 Subject => $args{'Subject'},
468 InitialPriority => $args{'InitialPriority'},
469 FinalPriority => $args{'FinalPriority'},
470 Priority => $args{'Priority'},
471 Status => $args{'Status'},
472 TimeWorked => $args{'TimeWorked'},
473 TimeEstimated => $args{'TimeEstimated'},
474 TimeLeft => $args{'TimeLeft'},
475 Type => $args{'Type'},
476 Starts => $Starts->ISO,
477 Started => $Started->ISO,
478 Resolved => $Resolved->ISO,
482 # Parameters passed in during an import that we probably don't want to touch, otherwise
483 foreach my $attr (qw(id Creator Created LastUpdated LastUpdatedBy)) {
484 $params{$attr} = $args{$attr} if $args{$attr};
487 # Delete null integer parameters
489 (qw(TimeWorked TimeLeft TimeEstimated InitialPriority FinalPriority))
491 delete $params{$attr}
492 unless ( exists $params{$attr} && $params{$attr} );
495 # Delete the time worked if we're counting it in the transaction
496 delete $params{'TimeWorked'} if $args{'_RecordTransaction'};
498 my ($id,$ticket_message) = $self->SUPER::Create( %params );
500 $RT::Logger->crit( "Couldn't create a ticket: " . $ticket_message );
501 $RT::Handle->Rollback();
503 $self->loc("Ticket could not be created due to an internal error")
507 #Set the ticket's effective ID now that we've created it.
508 my ( $val, $msg ) = $self->__Set(
509 Field => 'EffectiveId',
510 Value => ( $args{'EffectiveId'} || $id )
513 $RT::Logger->crit("Couldn't set EffectiveId: $msg");
514 $RT::Handle->Rollback;
516 $self->loc("Ticket could not be created due to an internal error")
520 my $create_groups_ret = $self->_CreateTicketGroups();
521 unless ($create_groups_ret) {
522 $RT::Logger->crit( "Couldn't create ticket groups for ticket "
524 . ". aborting Ticket creation." );
525 $RT::Handle->Rollback();
527 $self->loc("Ticket could not be created due to an internal error")
531 # Set the owner in the Groups table
532 # We denormalize it into the Ticket table too because doing otherwise would
533 # kill performance, bigtime. It gets kept in lockstep thanks to the magic of transactionalization
534 $self->OwnerGroup->_AddMember(
535 PrincipalId => $Owner->PrincipalId,
536 InsideTransaction => 1
537 ) unless $DeferOwner;
541 # Deal with setting up watchers
543 foreach my $type ( "Cc", "AdminCc", "Requestor" ) {
544 # we know it's an array ref
545 foreach my $watcher ( @{ $args{$type} } ) {
547 # Note that we're using AddWatcher, rather than _AddWatcher, as we
548 # actually _want_ that ACL check. Otherwise, random ticket creators
549 # could make themselves adminccs and maybe get ticket rights. that would
551 my $method = $type eq 'AdminCc'? 'AddWatcher': '_AddWatcher';
553 my ($val, $msg) = $self->$method(
555 PrincipalId => $watcher,
558 push @non_fatal_errors, $self->loc("Couldn't set [_1] watcher: [_2]", $type, $msg)
563 if ($args{'SquelchMailTo'}) {
564 my @squelch = ref( $args{'SquelchMailTo'} ) ? @{ $args{'SquelchMailTo'} }
565 : $args{'SquelchMailTo'};
566 $self->_SquelchMailTo( @squelch );
572 # Add all the custom fields
574 foreach my $arg ( keys %args ) {
575 next unless $arg =~ /^CustomField-(\d+)$/i;
579 UNIVERSAL::isa( $args{$arg} => 'ARRAY' ) ? @{ $args{$arg} } : ( $args{$arg} ) )
581 next unless defined $value && length $value;
583 # Allow passing in uploaded LargeContent etc by hash reference
584 my ($status, $msg) = $self->_AddCustomFieldValue(
585 (UNIVERSAL::isa( $value => 'HASH' )
590 RecordTransaction => 0,
592 push @non_fatal_errors, $msg unless $status;
598 # Deal with setting up links
600 # TODO: Adding link may fire scrips on other end and those scrips
601 # could create transactions on this ticket before 'Create' transaction.
603 # We should implement different lifecycle: record 'Create' transaction,
604 # create links and only then fire create transaction's scrips.
606 # Ideal variant: add all links without firing scrips, record create
607 # transaction and only then fire scrips on the other ends of links.
611 foreach my $type ( keys %LINKTYPEMAP ) {
612 next unless ( defined $args{$type} );
614 ref( $args{$type} ) ? @{ $args{$type} } : ( $args{$type} ) )
616 my ( $val, $msg, $obj ) = $self->__GetTicketFromURI( URI => $link );
618 push @non_fatal_errors, $msg;
622 # Check rights on the other end of the link if we must
623 # then run _AddLink that doesn't check for ACLs
624 if ( RT->Config->Get( 'StrictLinkACL' ) ) {
625 if ( $obj && !$obj->CurrentUserHasRight('ModifyTicket') ) {
626 push @non_fatal_errors, $self->loc('Linking. Permission denied');
631 if ( $obj && $obj->Status eq 'deleted' ) {
632 push @non_fatal_errors,
633 $self->loc("Linking. Can't link to a deleted ticket");
637 my ( $wval, $wmsg ) = $self->_AddLink(
638 Type => $LINKTYPEMAP{$type}->{'Type'},
639 $LINKTYPEMAP{$type}->{'Mode'} => $link,
640 Silent => !$args{'_RecordTransaction'} || $self->Type eq 'reminder',
641 'Silent'. ( $LINKTYPEMAP{$type}->{'Mode'} eq 'Base'? 'Target': 'Base' )
645 push @non_fatal_errors, $wmsg unless ($wval);
650 # Now that we've created the ticket and set up its metadata, we can actually go and check OwnTicket on the ticket itself.
651 # This might be different than before in cases where extensions like RTIR are doing clever things with RT's ACL system
653 if (!$DeferOwner->HasRight( Object => $self, Right => 'OwnTicket')) {
655 $RT::Logger->warning( "User " . $DeferOwner->Name . "(" . $DeferOwner->id
656 . ") was proposed as a ticket owner but has no rights to own "
657 . "tickets in " . $QueueObj->Name );
658 push @non_fatal_errors, $self->loc(
659 "Owner '[_1]' does not have rights to own this ticket.",
663 $Owner = $DeferOwner;
664 $self->__Set(Field => 'Owner', Value => $Owner->id);
667 $self->OwnerGroup->_AddMember(
668 PrincipalId => $Owner->PrincipalId,
669 InsideTransaction => 1
673 if ( $args{'_RecordTransaction'} ) {
675 # Add a transaction for the create
676 my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction(
678 TimeTaken => $args{'TimeWorked'},
679 MIMEObj => $args{'MIMEObj'},
680 CommitScrips => !$args{'DryRun'},
681 SquelchMailTo => $args{'TransSquelchMailTo'},
684 if ( $self->Id && $Trans ) {
686 $TransObj->UpdateCustomFields(ARGSRef => \%args);
688 $RT::Logger->info( "Ticket " . $self->Id . " created in queue '" . $QueueObj->Name . "' by " . $self->CurrentUser->Name );
689 $ErrStr = $self->loc( "Ticket [_1] created in queue '[_2]'", $self->Id, $QueueObj->Name );
690 $ErrStr = join( "\n", $ErrStr, @non_fatal_errors );
693 $RT::Handle->Rollback();
695 $ErrStr = join( "\n", $ErrStr, @non_fatal_errors );
696 $RT::Logger->error("Ticket couldn't be created: $ErrStr");
697 return ( 0, 0, $self->loc( "Ticket could not be created due to an internal error"));
700 if ( $args{'DryRun'} ) {
701 $RT::Handle->Rollback();
702 return ($self->id, $TransObj, $ErrStr);
704 $RT::Handle->Commit();
705 return ( $self->Id, $TransObj->Id, $ErrStr );
711 # Not going to record a transaction
712 $RT::Handle->Commit();
713 $ErrStr = $self->loc( "Ticket [_1] created in queue '[_2]'", $self->Id, $QueueObj->Name );
714 $ErrStr = join( "\n", $ErrStr, @non_fatal_errors );
715 return ( $self->Id, 0, $ErrStr );
723 =head2 _Parse822HeadersForAttributes Content
725 Takes an RFC822 style message and parses its attributes into a hash.
729 sub _Parse822HeadersForAttributes {
734 my @lines = ( split ( /\n/, $content ) );
735 while ( defined( my $line = shift @lines ) ) {
736 if ( $line =~ /^(.*?):(?:\s+(.*))?$/ ) {
741 if ( defined( $args{$tag} ) )
742 { #if we're about to get a second value, make it an array
743 $args{$tag} = [ $args{$tag} ];
745 if ( ref( $args{$tag} ) )
746 { #If it's an array, we want to push the value
747 push @{ $args{$tag} }, $value;
749 else { #if there's nothing there, just set the value
750 $args{$tag} = $value;
752 } elsif ($line =~ /^$/) {
754 #TODO: this won't work, since "" isn't of the form "foo:value"
756 while ( defined( my $l = shift @lines ) ) {
757 push @{ $args{'content'} }, $l;
763 foreach my $date (qw(due starts started resolved)) {
764 my $dateobj = RT::Date->new(RT->SystemUser);
765 if ( defined ($args{$date}) and $args{$date} =~ /^\d+$/ ) {
766 $dateobj->Set( Format => 'unix', Value => $args{$date} );
769 $dateobj->Set( Format => 'unknown', Value => $args{$date} );
771 $args{$date} = $dateobj->ISO;
773 $args{'mimeobj'} = MIME::Entity->new();
774 $args{'mimeobj'}->build(
775 Type => ( $args{'contenttype'} || 'text/plain' ),
776 Data => ($args{'content'} || '')
784 =head2 Import PARAMHASH
787 Doesn't create a transaction.
788 Doesn't supply queue defaults, etc.
796 my ( $ErrStr, $QueueObj, $Owner );
800 EffectiveId => undef,
804 Owner => RT->Nobody->Id,
805 Subject => '[no subject]',
806 InitialPriority => undef,
807 FinalPriority => undef,
818 if ( ( defined( $args{'Queue'} ) ) && ( !ref( $args{'Queue'} ) ) ) {
819 $QueueObj = RT::Queue->new(RT->SystemUser);
820 $QueueObj->Load( $args{'Queue'} );
822 #TODO error check this and return 0 if it's not loading properly +++
824 elsif ( ref( $args{'Queue'} ) eq 'RT::Queue' ) {
825 $QueueObj = RT::Queue->new(RT->SystemUser);
826 $QueueObj->Load( $args{'Queue'}->Id );
830 "$self " . $args{'Queue'} . " not a recognised queue object." );
833 #Can't create a ticket without a queue.
834 unless ( defined($QueueObj) and $QueueObj->Id ) {
835 $RT::Logger->debug("$self No queue given for ticket creation.");
836 return ( 0, $self->loc('Could not create ticket. Queue not set') );
839 #Now that we have a queue, Check the ACLS
841 $self->CurrentUser->HasRight(
842 Right => 'CreateTicket',
848 $self->loc("No permission to create tickets in the queue '[_1]'"
852 # Deal with setting the owner
854 # Attempt to take user object, user name or user id.
855 # Assign to nobody if lookup fails.
856 if ( defined( $args{'Owner'} ) ) {
857 if ( ref( $args{'Owner'} ) ) {
858 $Owner = $args{'Owner'};
861 $Owner = RT::User->new( $self->CurrentUser );
862 $Owner->Load( $args{'Owner'} );
863 if ( !defined( $Owner->id ) ) {
864 $Owner->Load( RT->Nobody->id );
869 #If we have a proposed owner and they don't have the right
870 #to own a ticket, scream about it and make them not the owner
873 and ( $Owner->Id != RT->Nobody->Id )
883 $RT::Logger->warning( "$self user "
887 . "as a ticket owner but has no rights to own "
889 . $QueueObj->Name . "'" );
894 #If we haven't been handed a valid owner, make it nobody.
895 unless ( defined($Owner) ) {
896 $Owner = RT::User->new( $self->CurrentUser );
897 $Owner->Load( RT->Nobody->UserObj->Id );
902 unless ( $self->ValidateStatus( $args{'Status'} ) ) {
903 return ( 0, $self->loc("'[_1]' is an invalid value for status", $args{'Status'}) );
906 $self->{'_AccessibleCache'}{Created} = { 'read' => 1, 'write' => 1 };
907 $self->{'_AccessibleCache'}{Creator} = { 'read' => 1, 'auto' => 1 };
908 $self->{'_AccessibleCache'}{LastUpdated} = { 'read' => 1, 'write' => 1 };
909 $self->{'_AccessibleCache'}{LastUpdatedBy} = { 'read' => 1, 'auto' => 1 };
911 # If we're coming in with an id, set that now.
912 my $EffectiveId = undef;
914 $EffectiveId = $args{'id'};
918 my $id = $self->SUPER::Create(
920 EffectiveId => $EffectiveId,
921 Queue => $QueueObj->Id,
923 Subject => $args{'Subject'}, # loc
924 InitialPriority => $args{'InitialPriority'}, # loc
925 FinalPriority => $args{'FinalPriority'}, # loc
926 Priority => $args{'InitialPriority'}, # loc
927 Status => $args{'Status'}, # loc
928 TimeWorked => $args{'TimeWorked'}, # loc
929 Type => $args{'Type'}, # loc
930 Created => $args{'Created'}, # loc
931 Told => $args{'Told'}, # loc
932 LastUpdated => $args{'Updated'}, # loc
933 Resolved => $args{'Resolved'}, # loc
934 Due => $args{'Due'}, # loc
937 # If the ticket didn't have an id
938 # Set the ticket's effective ID now that we've created it.
940 $self->Load( $args{'id'} );
944 $self->__Set( Field => 'EffectiveId', Value => $id );
948 $self . "->Import couldn't set EffectiveId: $msg" );
952 my $create_groups_ret = $self->_CreateTicketGroups();
953 unless ($create_groups_ret) {
955 "Couldn't create ticket groups for ticket " . $self->Id );
958 $self->OwnerGroup->_AddMember( PrincipalId => $Owner->PrincipalId );
960 foreach my $watcher ( @{ $args{'Cc'} } ) {
961 $self->_AddWatcher( Type => 'Cc', Email => $watcher, Silent => 1 );
963 foreach my $watcher ( @{ $args{'AdminCc'} } ) {
964 $self->_AddWatcher( Type => 'AdminCc', Email => $watcher,
967 foreach my $watcher ( @{ $args{'Requestor'} } ) {
968 $self->_AddWatcher( Type => 'Requestor', Email => $watcher,
972 return ( $self->Id, $ErrStr );
978 =head2 _CreateTicketGroups
980 Create the ticket groups and links for this ticket.
981 This routine expects to be called from Ticket->Create _inside of a transaction_
983 It will create four groups for this ticket: Requestor, Cc, AdminCc and Owner.
985 It will return true on success and undef on failure.
991 sub _CreateTicketGroups {
994 my @types = (qw(Requestor Owner Cc AdminCc));
996 foreach my $type (@types) {
997 my $type_obj = RT::Group->new($self->CurrentUser);
998 my ($id, $msg) = $type_obj->CreateRoleGroup(Domain => 'RT::Ticket-Role',
999 Instance => $self->Id,
1002 $RT::Logger->error("Couldn't create a ticket group of type '$type' for ticket ".
1003 $self->Id.": ".$msg);
1015 A constructor which returns an RT::Group object containing the owner of this ticket.
1021 my $owner_obj = RT::Group->new($self->CurrentUser);
1022 $owner_obj->LoadTicketRoleGroup( Ticket => $self->Id, Type => 'Owner');
1023 return ($owner_obj);
1031 AddWatcher takes a parameter hash. The keys are as follows:
1033 Type One of Requestor, Cc, AdminCc
1035 PrincipalId The RT::Principal id of the user or group that's being added as a watcher
1037 Email The email address of the new watcher. If a user with this
1038 email address can't be found, a new nonprivileged user will be created.
1040 If the watcher you're trying to set has an RT account, set the PrincipalId paremeter to their User Id. Otherwise, set the Email parameter to their Email address.
1048 PrincipalId => undef,
1053 # ModifyTicket works in any case
1054 return $self->_AddWatcher( %args )
1055 if $self->CurrentUserHasRight('ModifyTicket');
1056 if ( $args{'Email'} ) {
1057 my ($addr) = RT::EmailParser->ParseEmailAddress( $args{'Email'} );
1058 return (0, $self->loc("Couldn't parse address from '[_1]' string", $args{'Email'} ))
1061 if ( lc $self->CurrentUser->EmailAddress
1062 eq lc RT::User->CanonicalizeEmailAddress( $addr->address ) )
1064 $args{'PrincipalId'} = $self->CurrentUser->id;
1065 delete $args{'Email'};
1069 # If the watcher isn't the current user then the current user has no right
1071 unless ( $args{'PrincipalId'} && $self->CurrentUser->id == $args{'PrincipalId'} ) {
1072 return ( 0, $self->loc("Permission Denied") );
1075 # If it's an AdminCc and they don't have 'WatchAsAdminCc', bail
1076 if ( $args{'Type'} eq 'AdminCc' ) {
1077 unless ( $self->CurrentUserHasRight('WatchAsAdminCc') ) {
1078 return ( 0, $self->loc('Permission Denied') );
1082 # If it's a Requestor or Cc and they don't have 'Watch', bail
1083 elsif ( $args{'Type'} eq 'Cc' || $args{'Type'} eq 'Requestor' ) {
1084 unless ( $self->CurrentUserHasRight('Watch') ) {
1085 return ( 0, $self->loc('Permission Denied') );
1089 $RT::Logger->warning( "AddWatcher got passed a bogus type");
1090 return ( 0, $self->loc('Error in parameters to Ticket->AddWatcher') );
1093 return $self->_AddWatcher( %args );
1096 #This contains the meat of AddWatcher. but can be called from a routine like
1097 # Create, which doesn't need the additional acl check
1103 PrincipalId => undef,
1109 my $principal = RT::Principal->new($self->CurrentUser);
1110 if ($args{'Email'}) {
1111 if ( RT::EmailParser->IsRTAddress( $args{'Email'} ) ) {
1112 return (0, $self->loc("[_1] is an address RT receives mail at. Adding it as a '[_2]' would create a mail loop", $args{'Email'}, $self->loc($args{'Type'})));
1114 my $user = RT::User->new(RT->SystemUser);
1115 my ($pid, $msg) = $user->LoadOrCreateByEmail( $args{'Email'} );
1116 $args{'PrincipalId'} = $pid if $pid;
1118 if ($args{'PrincipalId'}) {
1119 $principal->Load($args{'PrincipalId'});
1120 if ( $principal->id and $principal->IsUser and my $email = $principal->Object->EmailAddress ) {
1121 return (0, $self->loc("[_1] is an address RT receives mail at. Adding it as a '[_2]' would create a mail loop", $email, $self->loc($args{'Type'})))
1122 if RT::EmailParser->IsRTAddress( $email );
1128 # If we can't find this watcher, we need to bail.
1129 unless ($principal->Id) {
1130 $RT::Logger->error("Could not load create a user with the email address '".$args{'Email'}. "' to add as a watcher for ticket ".$self->Id);
1131 return(0, $self->loc("Could not find or create that user"));
1135 my $group = RT::Group->new($self->CurrentUser);
1136 $group->LoadTicketRoleGroup(Type => $args{'Type'}, Ticket => $self->Id);
1137 unless ($group->id) {
1138 return(0,$self->loc("Group not found"));
1141 if ( $group->HasMember( $principal)) {
1143 return ( 0, $self->loc('[_1] is already a [_2] for this ticket',
1144 $principal->Object->Name, $self->loc($args{'Type'})) );
1148 my ( $m_id, $m_msg ) = $group->_AddMember( PrincipalId => $principal->Id,
1149 InsideTransaction => 1 );
1151 $RT::Logger->error("Failed to add ".$principal->Id." as a member of group ".$group->Id.": ".$m_msg);
1153 return ( 0, $self->loc('Could not make [_1] a [_2] for this ticket',
1154 $principal->Object->Name, $self->loc($args{'Type'})) );
1157 unless ( $args{'Silent'} ) {
1158 $self->_NewTransaction(
1159 Type => 'AddWatcher',
1160 NewValue => $principal->Id,
1161 Field => $args{'Type'}
1165 return ( 1, $self->loc('Added [_1] as a [_2] for this ticket',
1166 $principal->Object->Name, $self->loc($args{'Type'})) );
1172 =head2 DeleteWatcher { Type => TYPE, PrincipalId => PRINCIPAL_ID, Email => EMAIL_ADDRESS }
1175 Deletes a Ticket watcher. Takes two arguments:
1177 Type (one of Requestor,Cc,AdminCc)
1181 PrincipalId (an RT::Principal Id of the watcher you want to remove)
1183 Email (the email address of an existing wathcer)
1192 my %args = ( Type => undef,
1193 PrincipalId => undef,
1197 unless ( $args{'PrincipalId'} || $args{'Email'} ) {
1198 return ( 0, $self->loc("No principal specified") );
1200 my $principal = RT::Principal->new( $self->CurrentUser );
1201 if ( $args{'PrincipalId'} ) {
1203 $principal->Load( $args{'PrincipalId'} );
1206 my $user = RT::User->new( $self->CurrentUser );
1207 $user->LoadByEmail( $args{'Email'} );
1208 $principal->Load( $user->Id );
1211 # If we can't find this watcher, we need to bail.
1212 unless ( $principal->Id ) {
1213 return ( 0, $self->loc("Could not find that principal") );
1216 my $group = RT::Group->new( $self->CurrentUser );
1217 $group->LoadTicketRoleGroup( Type => $args{'Type'}, Ticket => $self->Id );
1218 unless ( $group->id ) {
1219 return ( 0, $self->loc("Group not found") );
1223 #If the watcher we're trying to add is for the current user
1224 if ( $self->CurrentUser->PrincipalId == $principal->id ) {
1226 # If it's an AdminCc and they don't have
1227 # 'WatchAsAdminCc' or 'ModifyTicket', bail
1228 if ( $args{'Type'} eq 'AdminCc' ) {
1229 unless ( $self->CurrentUserHasRight('ModifyTicket')
1230 or $self->CurrentUserHasRight('WatchAsAdminCc') ) {
1231 return ( 0, $self->loc('Permission Denied') );
1235 # If it's a Requestor or Cc and they don't have
1236 # 'Watch' or 'ModifyTicket', bail
1237 elsif ( ( $args{'Type'} eq 'Cc' ) or ( $args{'Type'} eq 'Requestor' ) )
1239 unless ( $self->CurrentUserHasRight('ModifyTicket')
1240 or $self->CurrentUserHasRight('Watch') ) {
1241 return ( 0, $self->loc('Permission Denied') );
1245 $RT::Logger->warning("$self -> DeleteWatcher got passed a bogus type");
1247 $self->loc('Error in parameters to Ticket->DeleteWatcher') );
1251 # If the watcher isn't the current user
1252 # and the current user doesn't have 'ModifyTicket' bail
1254 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
1255 return ( 0, $self->loc("Permission Denied") );
1261 # see if this user is already a watcher.
1263 unless ( $group->HasMember($principal) ) {
1265 $self->loc( '[_1] is not a [_2] for this ticket',
1266 $principal->Object->Name, $args{'Type'} ) );
1269 my ( $m_id, $m_msg ) = $group->_DeleteMember( $principal->Id );
1271 $RT::Logger->error( "Failed to delete "
1273 . " as a member of group "
1279 'Could not remove [_1] as a [_2] for this ticket',
1280 $principal->Object->Name, $args{'Type'} ) );
1283 unless ( $args{'Silent'} ) {
1284 $self->_NewTransaction( Type => 'DelWatcher',
1285 OldValue => $principal->Id,
1286 Field => $args{'Type'} );
1290 $self->loc( "[_1] is no longer a [_2] for this ticket.",
1291 $principal->Object->Name,
1299 =head2 SquelchMailTo [EMAIL]
1301 Takes an optional email address to never email about updates to this ticket.
1304 Returns an array of the RT::Attribute objects for this ticket's 'SquelchMailTo' attributes.
1312 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
1316 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
1321 return $self->_SquelchMailTo(@_);
1324 sub _SquelchMailTo {
1328 $self->AddAttribute( Name => 'SquelchMailTo', Content => $attr )
1329 unless grep { $_->Content eq $attr }
1330 $self->Attributes->Named('SquelchMailTo');
1332 my @attributes = $self->Attributes->Named('SquelchMailTo');
1333 return (@attributes);
1337 =head2 UnsquelchMailTo ADDRESS
1339 Takes an address and removes it from this ticket's "SquelchMailTo" list. If an address appears multiple times, each instance is removed.
1341 Returns a tuple of (status, message)
1345 sub UnsquelchMailTo {
1348 my $address = shift;
1349 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
1350 return ( 0, $self->loc("Permission Denied") );
1353 my ($val, $msg) = $self->Attributes->DeleteEntry ( Name => 'SquelchMailTo', Content => $address);
1354 return ($val, $msg);
1359 =head2 RequestorAddresses
1361 B<Returns> String: All Ticket Requestor email addresses as a string.
1365 sub RequestorAddresses {
1368 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
1372 return ( $self->Requestors->MemberEmailAddressesAsString );
1376 =head2 AdminCcAddresses
1378 returns String: All Ticket AdminCc email addresses as a string
1382 sub AdminCcAddresses {
1385 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
1389 return ( $self->AdminCc->MemberEmailAddressesAsString )
1395 returns String: All Ticket Ccs as a string of email addresses
1402 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
1405 return ( $self->Cc->MemberEmailAddressesAsString);
1415 Returns this ticket's Requestors as an RT::Group object
1422 my $group = RT::Group->new($self->CurrentUser);
1423 if ( $self->CurrentUserHasRight('ShowTicket') ) {
1424 $group->LoadTicketRoleGroup(Type => 'Requestor', Ticket => $self->Id);
1435 Returns an RT::Group object which contains this ticket's Ccs.
1436 If the user doesn't have "ShowTicket" permission, returns an empty group
1443 my $group = RT::Group->new($self->CurrentUser);
1444 if ( $self->CurrentUserHasRight('ShowTicket') ) {
1445 $group->LoadTicketRoleGroup(Type => 'Cc', Ticket => $self->Id);
1456 Returns an RT::Group object which contains this ticket's AdminCcs.
1457 If the user doesn't have "ShowTicket" permission, returns an empty group
1464 my $group = RT::Group->new($self->CurrentUser);
1465 if ( $self->CurrentUserHasRight('ShowTicket') ) {
1466 $group->LoadTicketRoleGroup(Type => 'AdminCc', Ticket => $self->Id);
1475 # a generic routine to be called by IsRequestor, IsCc and IsAdminCc
1477 =head2 IsWatcher { Type => TYPE, PrincipalId => PRINCIPAL_ID, Email => EMAIL }
1479 Takes a param hash with the attributes Type and either PrincipalId or Email
1481 Type is one of Requestor, Cc, AdminCc and Owner
1483 PrincipalId is an RT::Principal id, and Email is an email address.
1485 Returns true if the specified principal (or the one corresponding to the
1486 specified address) is a member of the group Type for this ticket.
1488 XX TODO: This should be Memoized.
1495 my %args = ( Type => 'Requestor',
1496 PrincipalId => undef,
1501 # Load the relevant group.
1502 my $group = RT::Group->new($self->CurrentUser);
1503 $group->LoadTicketRoleGroup(Type => $args{'Type'}, Ticket => $self->id);
1505 # Find the relevant principal.
1506 if (!$args{PrincipalId} && $args{Email}) {
1507 # Look up the specified user.
1508 my $user = RT::User->new($self->CurrentUser);
1509 $user->LoadByEmail($args{Email});
1511 $args{PrincipalId} = $user->PrincipalId;
1514 # A non-existent user can't be a group member.
1519 # Ask if it has the member in question
1520 return $group->HasMember( $args{'PrincipalId'} );
1525 =head2 IsRequestor PRINCIPAL_ID
1527 Takes an L<RT::Principal> id.
1529 Returns true if the principal is a requestor of the current ticket.
1537 return ( $self->IsWatcher( Type => 'Requestor', PrincipalId => $person ) );
1543 =head2 IsCc PRINCIPAL_ID
1545 Takes an RT::Principal id.
1546 Returns true if the principal is a Cc of the current ticket.
1555 return ( $self->IsWatcher( Type => 'Cc', PrincipalId => $cc ) );
1561 =head2 IsAdminCc PRINCIPAL_ID
1563 Takes an RT::Principal id.
1564 Returns true if the principal is an AdminCc of the current ticket.
1572 return ( $self->IsWatcher( Type => 'AdminCc', PrincipalId => $person ) );
1580 Takes an RT::User object. Returns true if that user is this ticket's owner.
1581 returns undef otherwise
1589 # no ACL check since this is used in acl decisions
1590 # unless ($self->CurrentUserHasRight('ShowTicket')) {
1594 #Tickets won't yet have owners when they're being created.
1595 unless ( $self->OwnerObj->id ) {
1599 if ( $person->id == $self->OwnerObj->id ) {
1611 =head2 TransactionAddresses
1613 Returns a composite hashref of the results of L<RT::Transaction/Addresses> for
1614 all this ticket's Create, Comment or Correspond transactions. The keys are
1615 stringified email addresses. Each value is an L<Email::Address> object.
1617 NOTE: For performance reasons, this method might want to skip transactions and go straight for attachments. But to make that work right, we're going to need to go and walk around the access control in Attachment.pm's sub _Value.
1622 sub TransactionAddresses {
1624 my $txns = $self->Transactions;
1628 my $attachments = RT::Attachments->new( $self->CurrentUser );
1629 $attachments->LimitByTicket( $self->id );
1630 $attachments->Columns( qw( id Headers TransactionId));
1633 foreach my $type (qw(Create Comment Correspond)) {
1634 $attachments->Limit( ALIAS => $attachments->TransactionAlias,
1638 ENTRYAGGREGATOR => 'OR',
1643 while ( my $att = $attachments->Next ) {
1644 foreach my $addrlist ( values %{$att->Addresses } ) {
1645 foreach my $addr (@$addrlist) {
1647 # Skip addresses without a phrase (things that are just raw addresses) if we have a phrase
1649 if ( $addresses{ $addr->address }
1650 && $addresses{ $addr->address }->phrase
1651 && not $addr->phrase );
1653 # skips "comment-only" addresses
1654 next unless ( $addr->address );
1655 $addresses{ $addr->address } = $addr;
1674 $RT::Logger->warning( " RT:::Queue::ValidateQueue called with a null value. this isn't ok.");
1678 my $QueueObj = RT::Queue->new( $self->CurrentUser );
1679 my $id = $QueueObj->Load($Value);
1693 my $NewQueue = shift;
1695 #Redundant. ACL gets checked in _Set;
1696 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
1697 return ( 0, $self->loc("Permission Denied") );
1700 my $NewQueueObj = RT::Queue->new( $self->CurrentUser );
1701 $NewQueueObj->Load($NewQueue);
1703 unless ( $NewQueueObj->Id() ) {
1704 return ( 0, $self->loc("That queue does not exist") );
1707 if ( $NewQueueObj->Id == $self->QueueObj->Id ) {
1708 return ( 0, $self->loc('That is the same value') );
1710 unless ( $self->CurrentUser->HasRight( Right => 'CreateTicket', Object => $NewQueueObj)) {
1711 return ( 0, $self->loc("You may not create requests in that queue.") );
1715 my $old_lifecycle = $self->QueueObj->Lifecycle;
1716 my $new_lifecycle = $NewQueueObj->Lifecycle;
1717 if ( $old_lifecycle->Name ne $new_lifecycle->Name ) {
1718 unless ( $old_lifecycle->HasMoveMap( $new_lifecycle ) ) {
1719 return ( 0, $self->loc("There is no mapping for statuses between these queues. Contact your system administrator.") );
1721 $new_status = $old_lifecycle->MoveMap( $new_lifecycle )->{ $self->Status };
1722 return ( 0, $self->loc("Mapping between queues' lifecycles is incomplete. Contact your system administrator.") )
1726 if ( $new_status ) {
1727 my $clone = RT::Ticket->new( RT->SystemUser );
1728 $clone->Load( $self->Id );
1729 unless ( $clone->Id ) {
1730 return ( 0, $self->loc("Couldn't load copy of ticket #[_1].", $self->Id) );
1733 my $now = RT::Date->new( $self->CurrentUser );
1736 my $old_status = $clone->Status;
1738 #If we're changing the status from initial in old to not intial in new,
1739 # record that we've started
1740 if ( $old_lifecycle->IsInitial($old_status) && !$new_lifecycle->IsInitial($new_status) && $clone->StartedObj->Unix == 0 ) {
1741 #Set the Started time to "now"
1745 RecordTransaction => 0
1749 #When we close a ticket, set the 'Resolved' attribute to now.
1750 # It's misnamed, but that's just historical.
1751 if ( $new_lifecycle->IsInactive($new_status) ) {
1753 Field => 'Resolved',
1755 RecordTransaction => 0,
1759 #Actually update the status
1760 my ($val, $msg)= $clone->_Set(
1762 Value => $new_status,
1763 RecordTransaction => 0,
1765 $RT::Logger->error( 'Status change failed on queue change: '. $msg )
1769 my ($status, $msg) = $self->_Set( Field => 'Queue', Value => $NewQueueObj->Id() );
1772 # Clear the queue object cache;
1773 $self->{_queue_obj} = undef;
1775 # Untake the ticket if we have no permissions in the new queue
1776 unless ( $self->OwnerObj->HasRight( Right => 'OwnTicket', Object => $NewQueueObj ) ) {
1777 my $clone = RT::Ticket->new( RT->SystemUser );
1778 $clone->Load( $self->Id );
1779 unless ( $clone->Id ) {
1780 return ( 0, $self->loc("Couldn't load copy of ticket #[_1].", $self->Id) );
1782 my ($status, $msg) = $clone->SetOwner( RT->Nobody->Id, 'Force' );
1783 $RT::Logger->error("Couldn't set owner on queue change: $msg") unless $status;
1786 # On queue change, change queue for reminders too
1787 my $reminder_collection = $self->Reminders->Collection;
1788 while ( my $reminder = $reminder_collection->Next ) {
1789 my ($status, $msg) = $reminder->SetQueue($NewQueue);
1790 $RT::Logger->error('Queue change failed for reminder #' . $reminder->Id . ': ' . $msg) unless $status;
1794 return ($status, $msg);
1801 Takes nothing. returns this ticket's queue object
1808 if(!$self->{_queue_obj} || ! $self->{_queue_obj}->id) {
1810 $self->{_queue_obj} = RT::Queue->new( $self->CurrentUser );
1812 #We call __Value so that we can avoid the ACL decision and some deep recursion
1813 my ($result) = $self->{_queue_obj}->Load( $self->__Value('Queue') );
1815 return ($self->{_queue_obj});
1820 Takes nothing. Returns SubjectTag for this ticket. Includes
1821 queue's subject tag or rtname if that is not set, ticket
1822 id and braces, for example:
1824 [support.example.com #123456]
1832 . ($self->QueueObj->SubjectTag || RT->Config->Get('rtname'))
1841 Returns an RT::Date object containing this ticket's due date
1848 my $time = RT::Date->new( $self->CurrentUser );
1850 # -1 is RT::Date slang for never
1851 if ( my $due = $self->Due ) {
1852 $time->Set( Format => 'sql', Value => $due );
1855 $time->Set( Format => 'unix', Value => -1 );
1865 Returns this ticket's due date as a human readable string
1871 return $self->DueObj->AsString();
1878 Returns an RT::Date object of this ticket's 'resolved' time.
1885 my $time = RT::Date->new( $self->CurrentUser );
1886 $time->Set( Format => 'sql', Value => $self->Resolved );
1891 =head2 FirstActiveStatus
1893 Returns the first active status that the ticket could transition to,
1894 according to its current Queue's lifecycle. May return undef if there
1895 is no such possible status to transition to, or we are already in it.
1896 This is used in L<RT::Action::AutoOpen>, for instance.
1900 sub FirstActiveStatus {
1903 my $lifecycle = $self->QueueObj->Lifecycle;
1904 my $status = $self->Status;
1905 my @active = $lifecycle->Active;
1906 # no change if no active statuses in the lifecycle
1907 return undef unless @active;
1909 # no change if the ticket is already has first status from the list of active
1910 return undef if lc $status eq lc $active[0];
1912 my ($next) = grep $lifecycle->IsActive($_), $lifecycle->Transitions($status);
1916 =head2 FirstInactiveStatus
1918 Returns the first inactive status that the ticket could transition to,
1919 according to its current Queue's lifecycle. May return undef if there
1920 is no such possible status to transition to, or we are already in it.
1921 This is used in resolve action in UnsafeEmailCommands, for instance.
1925 sub FirstInactiveStatus {
1928 my $lifecycle = $self->QueueObj->Lifecycle;
1929 my $status = $self->Status;
1930 my @inactive = $lifecycle->Inactive;
1931 # no change if no inactive statuses in the lifecycle
1932 return undef unless @inactive;
1934 # no change if the ticket is already has first status from the list of inactive
1935 return undef if lc $status eq lc $inactive[0];
1937 my ($next) = grep $lifecycle->IsInactive($_), $lifecycle->Transitions($status);
1943 Takes a date in ISO format or undef
1944 Returns a transaction id and a message
1945 The client calls "Start" to note that the project was started on the date in $date.
1946 A null date means "now"
1952 my $time = shift || 0;
1954 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
1955 return ( 0, $self->loc("Permission Denied") );
1958 #We create a date object to catch date weirdness
1959 my $time_obj = RT::Date->new( $self->CurrentUser() );
1961 $time_obj->Set( Format => 'ISO', Value => $time );
1964 $time_obj->SetToNow();
1967 # We need $TicketAsSystem, in case the current user doesn't have
1969 my $TicketAsSystem = RT::Ticket->new(RT->SystemUser);
1970 $TicketAsSystem->Load( $self->Id );
1971 # Now that we're starting, open this ticket
1972 # TODO: do we really want to force this as policy? it should be a scrip
1973 my $next = $TicketAsSystem->FirstActiveStatus;
1975 $self->SetStatus( $next ) if defined $next;
1977 return ( $self->_Set( Field => 'Started', Value => $time_obj->ISO ) );
1985 Returns an RT::Date object which contains this ticket's
1993 my $time = RT::Date->new( $self->CurrentUser );
1994 $time->Set( Format => 'sql', Value => $self->Started );
2002 Returns an RT::Date object which contains this ticket's
2010 my $time = RT::Date->new( $self->CurrentUser );
2011 $time->Set( Format => 'sql', Value => $self->Starts );
2019 Returns an RT::Date object which contains this ticket's
2027 my $time = RT::Date->new( $self->CurrentUser );
2028 $time->Set( Format => 'sql', Value => $self->Told );
2036 A convenience method that returns ToldObj->AsString
2038 TODO: This should be deprecated
2044 if ( $self->Told ) {
2045 return $self->ToldObj->AsString();
2054 =head2 TimeWorkedAsString
2056 Returns the amount of time worked on this ticket as a Text String
2060 sub TimeWorkedAsString {
2062 my $value = $self->TimeWorked;
2064 # return the # of minutes worked turned into seconds and written as
2065 # a simple text string, this is not really a date object, but if we
2066 # diff a number of seconds vs the epoch, we'll get a nice description
2068 return "" unless $value;
2069 return RT::Date->new( $self->CurrentUser )
2070 ->DurationAsString( $value * 60 );
2075 =head2 TimeLeftAsString
2077 Returns the amount of time left on this ticket as a Text String
2081 sub TimeLeftAsString {
2083 my $value = $self->TimeLeft;
2084 return "" unless $value;
2085 return RT::Date->new( $self->CurrentUser )
2086 ->DurationAsString( $value * 60 );
2094 Comment on this ticket.
2095 Takes a hash with the following attributes:
2096 If MIMEObj is undefined, Content will be used to build a MIME::Entity for this
2099 MIMEObj, TimeTaken, CcMessageTo, BccMessageTo, Content, DryRun
2101 If DryRun is defined, this update WILL NOT BE RECORDED. Scrips will not be committed.
2102 They will, however, be prepared and you'll be able to access them through the TransactionObj
2104 Returns: Transaction id, Error Message, Transaction Object
2105 (note the different order from Create()!)
2112 my %args = ( CcMessageTo => undef,
2113 BccMessageTo => undef,
2120 unless ( ( $self->CurrentUserHasRight('CommentOnTicket') )
2121 or ( $self->CurrentUserHasRight('ModifyTicket') ) ) {
2122 return ( 0, $self->loc("Permission Denied"), undef );
2124 $args{'NoteType'} = 'Comment';
2126 $RT::Handle->BeginTransaction();
2127 if ($args{'DryRun'}) {
2128 $args{'CommitScrips'} = 0;
2131 my @results = $self->_RecordNote(%args);
2132 if ($args{'DryRun'}) {
2133 $RT::Handle->Rollback();
2135 $RT::Handle->Commit();
2144 Correspond on this ticket.
2145 Takes a hashref with the following attributes:
2148 MIMEObj, TimeTaken, CcMessageTo, BccMessageTo, Content, DryRun
2150 if there's no MIMEObj, Content is used to build a MIME::Entity object
2152 If DryRun is defined, this update WILL NOT BE RECORDED. Scrips will not be committed.
2153 They will, however, be prepared and you'll be able to access them through the TransactionObj
2155 Returns: Transaction id, Error Message, Transaction Object
2156 (note the different order from Create()!)
2163 my %args = ( CcMessageTo => undef,
2164 BccMessageTo => undef,
2170 unless ( ( $self->CurrentUserHasRight('ReplyToTicket') )
2171 or ( $self->CurrentUserHasRight('ModifyTicket') ) ) {
2172 return ( 0, $self->loc("Permission Denied"), undef );
2174 $args{'NoteType'} = 'Correspond';
2176 $RT::Handle->BeginTransaction();
2177 if ($args{'DryRun'}) {
2178 $args{'CommitScrips'} = 0;
2181 my @results = $self->_RecordNote(%args);
2183 unless ( $results[0] ) {
2184 $RT::Handle->Rollback();
2188 #Set the last told date to now if this isn't mail from the requestor.
2189 #TODO: Note that this will wrongly ack mail from any non-requestor as a "told"
2190 unless ( $self->IsRequestor($self->CurrentUser->id) ) {
2192 $squelch{$_}++ for map {$_->Content} $self->SquelchMailTo, $results[2]->SquelchMailTo;
2194 if grep {not $squelch{$_}} $self->Requestors->MemberEmailAddresses;
2197 if ($args{'DryRun'}) {
2198 $RT::Handle->Rollback();
2200 $RT::Handle->Commit();
2211 the meat of both comment and correspond.
2213 Performs no access control checks. hence, dangerous.
2220 CcMessageTo => undef,
2221 BccMessageTo => undef,
2226 NoteType => 'Correspond',
2229 SquelchMailTo => undef,
2233 unless ( $args{'MIMEObj'} || $args{'Content'} ) {
2234 return ( 0, $self->loc("No message attached"), undef );
2237 unless ( $args{'MIMEObj'} ) {
2238 $args{'MIMEObj'} = MIME::Entity->build(
2239 Data => ( ref $args{'Content'}? $args{'Content'}: [ $args{'Content'} ] )
2243 $args{'MIMEObj'}->head->replace('X-RT-Interface' => 'API')
2244 unless $args{'MIMEObj'}->head->get('X-RT-Interface');
2246 # convert text parts into utf-8
2247 RT::I18N::SetMIMEEntityToUTF8( $args{'MIMEObj'} );
2249 # If we've been passed in CcMessageTo and BccMessageTo fields,
2250 # add them to the mime object for passing on to the transaction handler
2251 # The "NotifyOtherRecipients" scripAction will look for RT-Send-Cc: and
2252 # RT-Send-Bcc: headers
2255 foreach my $type (qw/Cc Bcc/) {
2256 if ( defined $args{ $type . 'MessageTo' } ) {
2258 my $addresses = join ', ', (
2259 map { RT::User->CanonicalizeEmailAddress( $_->address ) }
2260 Email::Address->parse( $args{ $type . 'MessageTo' } ) );
2261 $args{'MIMEObj'}->head->replace( 'RT-Send-' . $type, Encode::encode_utf8( $addresses ) );
2265 foreach my $argument (qw(Encrypt Sign)) {
2266 $args{'MIMEObj'}->head->replace(
2267 "X-RT-$argument" => Encode::encode_utf8( $args{ $argument } )
2268 ) if defined $args{ $argument };
2271 # If this is from an external source, we need to come up with its
2272 # internal Message-ID now, so all emails sent because of this
2273 # message have a common Message-ID
2274 my $org = RT->Config->Get('Organization');
2275 my $msgid = $args{'MIMEObj'}->head->get('Message-ID');
2276 unless (defined $msgid && $msgid =~ /<(rt-.*?-\d+-\d+)\.(\d+-0-0)\@\Q$org\E>/) {
2277 $args{'MIMEObj'}->head->set(
2278 'RT-Message-ID' => Encode::encode_utf8(
2279 RT::Interface::Email::GenMessageId( Ticket => $self )
2284 #Record the correspondence (write the transaction)
2285 my ( $Trans, $msg, $TransObj ) = $self->_NewTransaction(
2286 Type => $args{'NoteType'},
2287 Data => ( $args{'MIMEObj'}->head->get('subject') || 'No Subject' ),
2288 TimeTaken => $args{'TimeTaken'},
2289 MIMEObj => $args{'MIMEObj'},
2290 CommitScrips => $args{'CommitScrips'},
2291 SquelchMailTo => $args{'SquelchMailTo'},
2295 $RT::Logger->err("$self couldn't init a transaction $msg");
2296 return ( $Trans, $self->loc("Message could not be recorded"), undef );
2299 return ( $Trans, $self->loc("Message recorded"), $TransObj );
2305 Builds a MIME object from the given C<UpdateSubject> and
2306 C<UpdateContent>, then calls L</Comment> or L</Correspond> with
2307 C<< DryRun => 1 >>, and returns the transaction so produced.
2315 if (($args{'UpdateType'} || $args{Action}) =~ /^respon(d|se)$/i ) {
2316 $action = 'Correspond';
2318 $action = 'Comment';
2321 my $Message = MIME::Entity->build(
2322 Type => 'text/plain',
2323 Subject => defined $args{UpdateSubject} ? Encode::encode_utf8( $args{UpdateSubject} ) : "",
2325 Data => $args{'UpdateContent'} || "",
2328 my ( $Transaction, $Description, $Object ) = $self->$action(
2329 CcMessageTo => $args{'UpdateCc'},
2330 BccMessageTo => $args{'UpdateBcc'},
2331 MIMEObj => $Message,
2332 TimeTaken => $args{'UpdateTimeWorked'},
2335 unless ( $Transaction ) {
2336 $RT::Logger->error("Couldn't fire '$action' action: $Description");
2344 Prepares a MIME mesage with the given C<Subject>, C<Cc>, and
2345 C<Content>, then calls L</Create> with C<< DryRun => 1 >> and returns
2346 the resulting L<RT::Transaction>.
2353 my $Message = MIME::Entity->build(
2354 Type => 'text/plain',
2355 Subject => defined $args{Subject} ? Encode::encode_utf8( $args{'Subject'} ) : "",
2356 (defined $args{'Cc'} ?
2357 ( Cc => Encode::encode_utf8( $args{'Cc'} ) ) : ()),
2359 Data => $args{'Content'} || "",
2362 my ( $Transaction, $Object, $Description ) = $self->Create(
2363 Type => $args{'Type'} || 'ticket',
2364 Queue => $args{'Queue'},
2365 Owner => $args{'Owner'},
2366 Requestor => $args{'Requestors'},
2368 AdminCc => $args{'AdminCc'},
2369 InitialPriority => $args{'InitialPriority'},
2370 FinalPriority => $args{'FinalPriority'},
2371 TimeLeft => $args{'TimeLeft'},
2372 TimeEstimated => $args{'TimeEstimated'},
2373 TimeWorked => $args{'TimeWorked'},
2374 Subject => $args{'Subject'},
2375 Status => $args{'Status'},
2376 MIMEObj => $Message,
2379 unless ( $Transaction ) {
2380 $RT::Logger->error("Couldn't fire Create action: $Description");
2391 #TODO: Field isn't the right thing here. but I ahave no idea what mnemonic ---
2394 my $type = shift || "";
2396 my $cache_key = "$field$type";
2397 return $self->{ $cache_key } if $self->{ $cache_key };
2399 my $links = $self->{ $cache_key }
2400 = RT::Links->new( $self->CurrentUser );
2401 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
2402 $links->Limit( FIELD => 'id', VALUE => 0, SUBCLAUSE => 'acl' );
2406 # Maybe this ticket is a merge ticket
2407 my $limit_on = 'Local'. $field;
2408 # at least to myself
2412 ENTRYAGGREGATOR => 'OR',
2417 ENTRYAGGREGATOR => 'OR',
2418 ) foreach $self->Merged;
2431 Delete a link. takes a paramhash of Base, Target, Type, Silent,
2432 SilentBase and SilentTarget. Either Base or Target must be null.
2433 The null value will be replaced with this ticket's id.
2435 If Silent is true then no transaction would be recorded, in other
2436 case you can control creation of transactions on both base and
2437 target with SilentBase and SilentTarget respectively. By default
2438 both transactions are created.
2449 SilentBase => undef,
2450 SilentTarget => undef,
2454 unless ( $args{'Target'} || $args{'Base'} ) {
2455 $RT::Logger->error("Base or Target must be specified");
2456 return ( 0, $self->loc('Either base or target must be specified') );
2461 $right++ if $self->CurrentUserHasRight('ModifyTicket');
2462 if ( !$right && RT->Config->Get( 'StrictLinkACL' ) ) {
2463 return ( 0, $self->loc("Permission Denied") );
2466 # If the other URI is an RT::Ticket, we want to make sure the user
2467 # can modify it too...
2468 my ($status, $msg, $other_ticket) = $self->__GetTicketFromURI( URI => $args{'Target'} || $args{'Base'} );
2469 return (0, $msg) unless $status;
2470 if ( !$other_ticket || $other_ticket->CurrentUserHasRight('ModifyTicket') ) {
2473 if ( ( !RT->Config->Get( 'StrictLinkACL' ) && $right == 0 ) ||
2474 ( RT->Config->Get( 'StrictLinkACL' ) && $right < 2 ) )
2476 return ( 0, $self->loc("Permission Denied") );
2479 my ($val, $Msg) = $self->SUPER::_DeleteLink(%args);
2480 return ( 0, $Msg ) unless $val;
2482 return ( $val, $Msg ) if $args{'Silent'};
2484 my ($direction, $remote_link);
2486 if ( $args{'Base'} ) {
2487 $remote_link = $args{'Base'};
2488 $direction = 'Target';
2490 elsif ( $args{'Target'} ) {
2491 $remote_link = $args{'Target'};
2492 $direction = 'Base';
2495 my $remote_uri = RT::URI->new( $self->CurrentUser );
2496 $remote_uri->FromURI( $remote_link );
2498 unless ( $args{ 'Silent'. $direction } ) {
2499 my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction(
2500 Type => 'DeleteLink',
2501 Field => $LINKDIRMAP{$args{'Type'}}->{$direction},
2502 OldValue => $remote_uri->URI || $remote_link,
2505 $RT::Logger->error("Couldn't create transaction: $Msg") unless $Trans;
2508 if ( !$args{ 'Silent'. ( $direction eq 'Target'? 'Base': 'Target' ) } && $remote_uri->IsLocal ) {
2509 my $OtherObj = $remote_uri->Object;
2510 my ( $val, $Msg ) = $OtherObj->_NewTransaction(
2511 Type => 'DeleteLink',
2512 Field => $direction eq 'Target' ? $LINKDIRMAP{$args{'Type'}}->{Base}
2513 : $LINKDIRMAP{$args{'Type'}}->{Target},
2514 OldValue => $self->URI,
2515 ActivateScrips => !RT->Config->Get('LinkTransactionsRun1Scrip'),
2518 $RT::Logger->error("Couldn't create transaction: $Msg") unless $val;
2521 return ( $val, $Msg );
2528 Takes a paramhash of Type and one of Base or Target. Adds that link to this ticket.
2530 If Silent is true then no transaction would be recorded, in other
2531 case you can control creation of transactions on both base and
2532 target with SilentBase and SilentTarget respectively. By default
2533 both transactions are created.
2539 my %args = ( Target => '',
2543 SilentBase => undef,
2544 SilentTarget => undef,
2547 unless ( $args{'Target'} || $args{'Base'} ) {
2548 $RT::Logger->error("Base or Target must be specified");
2549 return ( 0, $self->loc('Either base or target must be specified') );
2553 $right++ if $self->CurrentUserHasRight('ModifyTicket');
2554 if ( !$right && RT->Config->Get( 'StrictLinkACL' ) ) {
2555 return ( 0, $self->loc("Permission Denied") );
2558 # If the other URI is an RT::Ticket, we want to make sure the user
2559 # can modify it too...
2560 my ($status, $msg, $other_ticket) = $self->__GetTicketFromURI( URI => $args{'Target'} || $args{'Base'} );
2561 return (0, $msg) unless $status;
2562 if ( !$other_ticket || $other_ticket->CurrentUserHasRight('ModifyTicket') ) {
2565 if ( ( !RT->Config->Get( 'StrictLinkACL' ) && $right == 0 ) ||
2566 ( RT->Config->Get( 'StrictLinkACL' ) && $right < 2 ) )
2568 return ( 0, $self->loc("Permission Denied") );
2571 return ( 0, "Can't link to a deleted ticket" )
2572 if $other_ticket && $other_ticket->Status eq 'deleted';
2574 return $self->_AddLink(%args);
2577 sub __GetTicketFromURI {
2579 my %args = ( URI => '', @_ );
2581 # If the other URI is an RT::Ticket, we want to make sure the user
2582 # can modify it too...
2583 my $uri_obj = RT::URI->new( $self->CurrentUser );
2584 unless ($uri_obj->FromURI( $args{'URI'} )) {
2585 my $msg = $self->loc( "Couldn't resolve '[_1]' into a URI.", $args{'URI'} );
2586 $RT::Logger->warning( $msg );
2589 my $obj = $uri_obj->Resolver->Object;
2590 unless ( UNIVERSAL::isa($obj, 'RT::Ticket') && $obj->id ) {
2591 return (1, 'Found not a ticket', undef);
2593 return (1, 'Found ticket', $obj);
2598 Private non-acled variant of AddLink so that links can be added during create.
2604 my %args = ( Target => '',
2608 SilentBase => undef,
2609 SilentTarget => undef,
2612 my ($val, $msg, $exist) = $self->SUPER::_AddLink(%args);
2613 return ($val, $msg) if !$val || $exist;
2614 return ($val, $msg) if $args{'Silent'};
2616 my ($direction, $remote_link);
2617 if ( $args{'Target'} ) {
2618 $remote_link = $args{'Target'};
2619 $direction = 'Base';
2620 } elsif ( $args{'Base'} ) {
2621 $remote_link = $args{'Base'};
2622 $direction = 'Target';
2625 my $remote_uri = RT::URI->new( $self->CurrentUser );
2626 $remote_uri->FromURI( $remote_link );
2628 unless ( $args{ 'Silent'. $direction } ) {
2629 my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction(
2631 Field => $LINKDIRMAP{$args{'Type'}}->{$direction},
2632 NewValue => $remote_uri->URI || $remote_link,
2635 $RT::Logger->error("Couldn't create transaction: $Msg") unless $Trans;
2638 if ( !$args{ 'Silent'. ( $direction eq 'Target'? 'Base': 'Target' ) } && $remote_uri->IsLocal ) {
2639 my $OtherObj = $remote_uri->Object;
2640 my ( $val, $msg ) = $OtherObj->_NewTransaction(
2642 Field => $direction eq 'Target' ? $LINKDIRMAP{$args{'Type'}}->{Base}
2643 : $LINKDIRMAP{$args{'Type'}}->{Target},
2644 NewValue => $self->URI,
2645 ActivateScrips => !RT->Config->Get('LinkTransactionsRun1Scrip'),
2648 $RT::Logger->error("Couldn't create transaction: $msg") unless $val;
2651 return ( $val, $msg );
2659 MergeInto take the id of the ticket to merge this ticket into.
2665 my $ticket_id = shift;
2667 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
2668 return ( 0, $self->loc("Permission Denied") );
2671 # Load up the new ticket.
2672 my $MergeInto = RT::Ticket->new($self->CurrentUser);
2673 $MergeInto->Load($ticket_id);
2675 # make sure it exists.
2676 unless ( $MergeInto->Id ) {
2677 return ( 0, $self->loc("New ticket doesn't exist") );
2680 # Make sure the current user can modify the new ticket.
2681 unless ( $MergeInto->CurrentUserHasRight('ModifyTicket') ) {
2682 return ( 0, $self->loc("Permission Denied") );
2685 delete $MERGE_CACHE{'effective'}{ $self->id };
2686 delete @{ $MERGE_CACHE{'merged'} }{
2687 $ticket_id, $MergeInto->id, $self->id
2690 $RT::Handle->BeginTransaction();
2692 $self->_MergeInto( $MergeInto );
2694 $RT::Handle->Commit();
2696 return ( 1, $self->loc("Merge Successful") );
2701 my $MergeInto = shift;
2704 # We use EffectiveId here even though it duplicates information from
2705 # the links table becasue of the massive performance hit we'd take
2706 # by trying to do a separate database query for merge info everytime
2709 #update this ticket's effective id to the new ticket's id.
2710 my ( $id_val, $id_msg ) = $self->__Set(
2711 Field => 'EffectiveId',
2712 Value => $MergeInto->Id()
2716 $RT::Handle->Rollback();
2717 return ( 0, $self->loc("Merge failed. Couldn't set EffectiveId") );
2721 my $force_status = $self->QueueObj->Lifecycle->DefaultOnMerge;
2722 if ( $force_status && $force_status ne $self->__Value('Status') ) {
2723 my ( $status_val, $status_msg )
2724 = $self->__Set( Field => 'Status', Value => $force_status );
2726 unless ($status_val) {
2727 $RT::Handle->Rollback();
2729 "Couldn't set status to $force_status. RT's Database may be inconsistent."
2731 return ( 0, $self->loc("Merge failed. Couldn't set Status") );
2735 # update all the links that point to that old ticket
2736 my $old_links_to = RT::Links->new($self->CurrentUser);
2737 $old_links_to->Limit(FIELD => 'Target', VALUE => $self->URI);
2740 while (my $link = $old_links_to->Next) {
2741 if (exists $old_seen{$link->Base."-".$link->Type}) {
2744 elsif ($link->Base eq $MergeInto->URI) {
2747 # First, make sure the link doesn't already exist. then move it over.
2748 my $tmp = RT::Link->new(RT->SystemUser);
2749 $tmp->LoadByCols(Base => $link->Base, Type => $link->Type, LocalTarget => $MergeInto->id);
2753 $link->SetTarget($MergeInto->URI);
2754 $link->SetLocalTarget($MergeInto->id);
2756 $old_seen{$link->Base."-".$link->Type} =1;
2761 my $old_links_from = RT::Links->new($self->CurrentUser);
2762 $old_links_from->Limit(FIELD => 'Base', VALUE => $self->URI);
2764 while (my $link = $old_links_from->Next) {
2765 if (exists $old_seen{$link->Type."-".$link->Target}) {
2768 if ($link->Target eq $MergeInto->URI) {
2771 # First, make sure the link doesn't already exist. then move it over.
2772 my $tmp = RT::Link->new(RT->SystemUser);
2773 $tmp->LoadByCols(Target => $link->Target, Type => $link->Type, LocalBase => $MergeInto->id);
2777 $link->SetBase($MergeInto->URI);
2778 $link->SetLocalBase($MergeInto->id);
2779 $old_seen{$link->Type."-".$link->Target} =1;
2785 # Update time fields
2786 foreach my $type (qw(TimeEstimated TimeWorked TimeLeft)) {
2788 my $mutator = "Set$type";
2789 $MergeInto->$mutator(
2790 ( $MergeInto->$type() || 0 ) + ( $self->$type() || 0 ) );
2793 #add all of this ticket's watchers to that ticket.
2794 foreach my $watcher_type (qw(Requestors Cc AdminCc)) {
2796 my $people = $self->$watcher_type->MembersObj;
2797 my $addwatcher_type = $watcher_type;
2798 $addwatcher_type =~ s/s$//;
2800 while ( my $watcher = $people->Next ) {
2802 my ($val, $msg) = $MergeInto->_AddWatcher(
2803 Type => $addwatcher_type,
2805 PrincipalId => $watcher->MemberId
2808 $RT::Logger->debug($msg);
2814 #find all of the tickets that were merged into this ticket.
2815 my $old_mergees = RT::Tickets->new( $self->CurrentUser );
2816 $old_mergees->Limit(
2817 FIELD => 'EffectiveId',
2822 # update their EffectiveId fields to the new ticket's id
2823 while ( my $ticket = $old_mergees->Next() ) {
2824 my ( $val, $msg ) = $ticket->__Set(
2825 Field => 'EffectiveId',
2826 Value => $MergeInto->Id()
2830 #make a new link: this ticket is merged into that other ticket.
2831 $self->AddLink( Type => 'MergedInto', Target => $MergeInto->Id());
2833 $MergeInto->_SetLastUpdated;
2838 Returns list of tickets' ids that's been merged into this ticket.
2846 return @{ $MERGE_CACHE{'merged'}{ $id } }
2847 if $MERGE_CACHE{'merged'}{ $id };
2849 my $mergees = RT::Tickets->new( $self->CurrentUser );
2851 FIELD => 'EffectiveId',
2859 return @{ $MERGE_CACHE{'merged'}{ $id } ||= [] }
2860 = map $_->id, @{ $mergees->ItemsArrayRef || [] };
2869 Takes nothing and returns an RT::User object of
2877 #If this gets ACLed, we lose on a rights check in User.pm and
2878 #get deep recursion. if we need ACLs here, we need
2879 #an equiv without ACLs
2881 my $owner = RT::User->new( $self->CurrentUser );
2882 $owner->Load( $self->__Value('Owner') );
2884 #Return the owner object
2890 =head2 OwnerAsString
2892 Returns the owner's email address
2898 return ( $self->OwnerObj->EmailAddress );
2906 Takes two arguments:
2907 the Id or Name of the owner
2908 and (optionally) the type of the SetOwner Transaction. It defaults
2909 to 'Set'. 'Steal' is also a valid option.
2916 my $NewOwner = shift;
2917 my $Type = shift || "Set";
2919 $RT::Handle->BeginTransaction();
2921 $self->_SetLastUpdated(); # lock the ticket
2922 $self->Load( $self->id ); # in case $self changed while waiting for lock
2924 my $OldOwnerObj = $self->OwnerObj;
2926 my $NewOwnerObj = RT::User->new( $self->CurrentUser );
2927 $NewOwnerObj->Load( $NewOwner );
2928 unless ( $NewOwnerObj->Id ) {
2929 $RT::Handle->Rollback();
2930 return ( 0, $self->loc("That user does not exist") );
2934 # must have ModifyTicket rights
2935 # or TakeTicket/StealTicket and $NewOwner is self
2936 # see if it's a take
2937 if ( $OldOwnerObj->Id == RT->Nobody->Id ) {
2938 unless ( $self->CurrentUserHasRight('ModifyTicket')
2939 || $self->CurrentUserHasRight('TakeTicket') ) {
2940 $RT::Handle->Rollback();
2941 return ( 0, $self->loc("Permission Denied") );
2945 # see if it's a steal
2946 elsif ( $OldOwnerObj->Id != RT->Nobody->Id
2947 && $OldOwnerObj->Id != $self->CurrentUser->id ) {
2949 unless ( $self->CurrentUserHasRight('ModifyTicket')
2950 || $self->CurrentUserHasRight('StealTicket') ) {
2951 $RT::Handle->Rollback();
2952 return ( 0, $self->loc("Permission Denied") );
2956 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
2957 $RT::Handle->Rollback();
2958 return ( 0, $self->loc("Permission Denied") );
2962 # If we're not stealing and the ticket has an owner and it's not
2964 if ( $Type ne 'Steal' and $Type ne 'Force'
2965 and $OldOwnerObj->Id != RT->Nobody->Id
2966 and $OldOwnerObj->Id != $self->CurrentUser->Id )
2968 $RT::Handle->Rollback();
2969 return ( 0, $self->loc("You can only take tickets that are unowned") )
2970 if $NewOwnerObj->id == $self->CurrentUser->id;
2973 $self->loc("You can only reassign tickets that you own or that are unowned" )
2977 #If we've specified a new owner and that user can't modify the ticket
2978 elsif ( !$NewOwnerObj->HasRight( Right => 'OwnTicket', Object => $self ) ) {
2979 $RT::Handle->Rollback();
2980 return ( 0, $self->loc("That user may not own tickets in that queue") );
2983 # If the ticket has an owner and it's the new owner, we don't need
2985 elsif ( $NewOwnerObj->Id == $OldOwnerObj->Id ) {
2986 $RT::Handle->Rollback();
2987 return ( 0, $self->loc("That user already owns that ticket") );
2990 # Delete the owner in the owner group, then add a new one
2991 # TODO: is this safe? it's not how we really want the API to work
2992 # for most things, but it's fast.
2993 my ( $del_id, $del_msg );
2994 for my $owner (@{$self->OwnerGroup->MembersObj->ItemsArrayRef}) {
2995 ($del_id, $del_msg) = $owner->Delete();
2996 last unless ($del_id);
3000 $RT::Handle->Rollback();
3001 return ( 0, $self->loc("Could not change owner: [_1]", $del_msg) );
3004 my ( $add_id, $add_msg ) = $self->OwnerGroup->_AddMember(
3005 PrincipalId => $NewOwnerObj->PrincipalId,
3006 InsideTransaction => 1 );
3008 $RT::Handle->Rollback();
3009 return ( 0, $self->loc("Could not change owner: [_1]", $add_msg ) );
3012 # We call set twice with slightly different arguments, so
3013 # as to not have an SQL transaction span two RT transactions
3015 my ( $val, $msg ) = $self->_Set(
3017 RecordTransaction => 0,
3018 Value => $NewOwnerObj->Id,
3020 TransactionType => 'Set',
3021 CheckACL => 0, # don't check acl
3025 $RT::Handle->Rollback;
3026 return ( 0, $self->loc("Could not change owner: [_1]", $msg) );
3029 ($val, $msg) = $self->_NewTransaction(
3032 NewValue => $NewOwnerObj->Id,
3033 OldValue => $OldOwnerObj->Id,
3038 $msg = $self->loc( "Owner changed from [_1] to [_2]",
3039 $OldOwnerObj->Name, $NewOwnerObj->Name );
3042 $RT::Handle->Rollback();
3046 $RT::Handle->Commit();
3048 return ( $val, $msg );
3055 A convenince method to set the ticket's owner to the current user
3061 return ( $self->SetOwner( $self->CurrentUser->Id, 'Take' ) );
3068 Convenience method to set the owner to 'nobody' if the current user is the owner.
3074 return ( $self->SetOwner( RT->Nobody->UserObj->Id, 'Untake' ) );
3081 A convenience method to change the owner of the current ticket to the
3082 current user. Even if it's owned by another user.
3089 if ( $self->IsOwner( $self->CurrentUser ) ) {
3090 return ( 0, $self->loc("You already own this ticket") );
3093 return ( $self->SetOwner( $self->CurrentUser->Id, 'Steal' ) );
3103 =head2 ValidateStatus STATUS
3105 Takes a string. Returns true if that status is a valid status for this ticket.
3106 Returns false otherwise.
3110 sub ValidateStatus {
3114 #Make sure the status passed in is valid
3115 return 1 if $self->QueueObj->IsValidStatus($status);
3118 while ( my $caller = (caller($i++))[3] ) {
3119 return 1 if $caller eq 'RT::Ticket::SetQueue';
3127 =head2 SetStatus STATUS
3129 Set this ticket's status. STATUS can be one of: new, open, stalled, resolved, rejected or deleted.
3131 Alternatively, you can pass in a list of named parameters (Status => STATUS, Force => FORCE, SetStarted => SETSTARTED ).
3132 If FORCE is true, ignore unresolved dependencies and force a status change.
3133 if SETSTARTED is true( it's the default value), set Started to current datetime if Started
3134 is not set and the status is changed from initial to not initial.
3142 $args{Status} = shift;
3148 # this only allows us to SetStarted, not we must SetStarted.
3149 # this option was added for rtir initially
3150 $args{SetStarted} = 1 unless exists $args{SetStarted};
3153 my $lifecycle = $self->QueueObj->Lifecycle;
3155 my $new = $args{'Status'};
3156 unless ( $lifecycle->IsValid( $new ) ) {
3157 return (0, $self->loc("Status '[_1]' isn't a valid status for tickets in this queue.", $self->loc($new)));
3160 my $old = $self->__Value('Status');
3161 unless ( $lifecycle->IsTransition( $old => $new ) ) {
3162 return (0, $self->loc("You can't change status from '[_1]' to '[_2]'.", $self->loc($old), $self->loc($new)));
3165 my $check_right = $lifecycle->CheckRight( $old => $new );
3166 unless ( $self->CurrentUserHasRight( $check_right ) ) {
3167 return ( 0, $self->loc('Permission Denied') );
3170 if ( !$args{Force} && $lifecycle->IsInactive( $new ) && $self->HasUnresolvedDependencies) {
3171 return (0, $self->loc('That ticket has unresolved dependencies'));
3174 my $now = RT::Date->new( $self->CurrentUser );
3177 my $raw_started = RT::Date->new(RT->SystemUser);
3178 $raw_started->Set(Format => 'ISO', Value => $self->__Value('Started'));
3180 #If we're changing the status from new, record that we've started
3181 if ( $args{SetStarted} && $lifecycle->IsInitial($old) && !$lifecycle->IsInitial($new) && !$raw_started->Unix) {
3182 #Set the Started time to "now"
3186 RecordTransaction => 0
3190 #When we close a ticket, set the 'Resolved' attribute to now.
3191 # It's misnamed, but that's just historical.
3192 if ( $lifecycle->IsInactive($new) ) {
3194 Field => 'Resolved',
3196 RecordTransaction => 0,
3200 #Actually update the status
3201 my ($val, $msg)= $self->_Set(
3203 Value => $args{Status},
3206 TransactionType => 'Status',
3208 return ($val, $msg);
3215 Takes no arguments. Marks this ticket for garbage collection
3221 unless ( $self->QueueObj->Lifecycle->IsValid('deleted') ) {
3222 return (0, $self->loc('Delete operation is disabled by lifecycle configuration') ); #loc
3224 return ( $self->SetStatus('deleted') );
3228 =head2 SetTold ISO [TIMETAKEN]
3230 Updates the told and records a transaction
3237 $told = shift if (@_);
3238 my $timetaken = shift || 0;
3240 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
3241 return ( 0, $self->loc("Permission Denied") );
3244 my $datetold = RT::Date->new( $self->CurrentUser );
3246 $datetold->Set( Format => 'iso',
3250 $datetold->SetToNow();
3253 return ( $self->_Set( Field => 'Told',
3254 Value => $datetold->ISO,
3255 TimeTaken => $timetaken,
3256 TransactionType => 'Told' ) );
3261 Updates the told without a transaction or acl check. Useful when we're sending replies.
3268 my $now = RT::Date->new( $self->CurrentUser );
3271 #use __Set to get no ACLs ;)
3272 return ( $self->__Set( Field => 'Told',
3273 Value => $now->ISO ) );
3283 my $uid = $self->CurrentUser->id;
3284 my $attr = $self->FirstAttribute( "User-". $uid ."-SeenUpTo" );
3285 return if $attr && $attr->Content gt $self->LastUpdated;
3287 my $txns = $self->Transactions;
3288 $txns->Limit( FIELD => 'Type', VALUE => 'Comment' );
3289 $txns->Limit( FIELD => 'Type', VALUE => 'Correspond' );
3290 $txns->Limit( FIELD => 'Creator', OPERATOR => '!=', VALUE => $uid );
3294 VALUE => $attr->Content
3296 $txns->RowsPerPage(1);
3297 return $txns->First;
3300 =head2 RanTransactionBatch
3302 Acts as a guard around running TransactionBatch scrips.
3304 Should be false until you enter the code that runs TransactionBatch scrips
3306 Accepts an optional argument to indicate that TransactionBatch Scrips should no longer be run on this object.
3310 sub RanTransactionBatch {
3314 if ( defined $val ) {
3315 return $self->{_RanTransactionBatch} = $val;
3317 return $self->{_RanTransactionBatch};
3323 =head2 TransactionBatch
3325 Returns an array reference of all transactions created on this ticket during
3326 this ticket object's lifetime or since last application of a batch, or undef
3329 Only works when the C<UseTransactionBatch> config option is set to true.
3333 sub TransactionBatch {
3335 return $self->{_TransactionBatch};
3338 =head2 ApplyTransactionBatch
3340 Applies scrips on the current batch of transactions and shinks it. Usually
3341 batch is applied when object is destroyed, but in some cases it's too late.
3345 sub ApplyTransactionBatch {
3348 my $batch = $self->TransactionBatch;
3349 return unless $batch && @$batch;
3351 $self->_ApplyTransactionBatch;
3353 $self->{_TransactionBatch} = [];
3356 sub _ApplyTransactionBatch {
3359 return if $self->RanTransactionBatch;
3360 $self->RanTransactionBatch(1);
3362 my $still_exists = RT::Ticket->new( RT->SystemUser );
3363 $still_exists->Load( $self->Id );
3364 if (not $still_exists->Id) {
3365 # The ticket has been removed from the database, but we still
3366 # have pending TransactionBatch txns for it. Unfortunately,
3367 # because it isn't in the DB anymore, attempting to run scrips
3368 # on it may produce unpredictable results; simply drop the
3369 # batched transactions.
3370 $RT::Logger->warning("TransactionBatch was fired on a ticket that no longer exists; unable to run scrips! Call ->ApplyTransactionBatch before shredding the ticket, for consistent results.");
3374 my $batch = $self->TransactionBatch;
3377 my $types = join ',', grep !$seen{$_}++, grep defined, map $_->__Value('Type'), grep defined, @{$batch};
3380 RT::Scrips->new(RT->SystemUser)->Apply(
3381 Stage => 'TransactionBatch',
3383 TransactionObj => $batch->[0],
3387 # Entry point of the rule system
3388 my $rules = RT::Ruleset->FindAllRules(
3389 Stage => 'TransactionBatch',
3391 TransactionObj => $batch->[0],
3394 RT::Ruleset->CommitRules($rules);
3400 # DESTROY methods need to localize $@, or it may unset it. This
3401 # causes $m->abort to not bubble all of the way up. See perlbug
3402 # http://rt.perl.org/rt3/Ticket/Display.html?id=17650
3405 # The following line eliminates reentrancy.
3406 # It protects against the fact that perl doesn't deal gracefully
3407 # when an object's refcount is changed in its destructor.
3408 return if $self->{_Destroyed}++;
3410 if (in_global_destruction()) {
3411 unless ($ENV{'HARNESS_ACTIVE'}) {
3412 warn "Too late to safely run transaction-batch scrips!"
3413 ." This is typically caused by using ticket objects"
3414 ." at the top-level of a script which uses the RT API."
3415 ." Be sure to explicitly undef such ticket objects,"
3416 ." or put them inside of a lexical scope.";
3421 return $self->ApplyTransactionBatch;
3427 sub _OverlayAccessible {
3429 EffectiveId => { 'read' => 1, 'write' => 1, 'public' => 1 },
3430 Queue => { 'read' => 1, 'write' => 1 },
3431 Requestors => { 'read' => 1, 'write' => 1 },
3432 Owner => { 'read' => 1, 'write' => 1 },
3433 Subject => { 'read' => 1, 'write' => 1 },
3434 InitialPriority => { 'read' => 1, 'write' => 1 },
3435 FinalPriority => { 'read' => 1, 'write' => 1 },
3436 Priority => { 'read' => 1, 'write' => 1 },
3437 Status => { 'read' => 1, 'write' => 1 },
3438 TimeEstimated => { 'read' => 1, 'write' => 1 },
3439 TimeWorked => { 'read' => 1, 'write' => 1 },
3440 TimeLeft => { 'read' => 1, 'write' => 1 },
3441 Told => { 'read' => 1, 'write' => 1 },
3442 Resolved => { 'read' => 1 },
3443 Type => { 'read' => 1 },
3444 Starts => { 'read' => 1, 'write' => 1 },
3445 Started => { 'read' => 1, 'write' => 1 },
3446 Due => { 'read' => 1, 'write' => 1 },
3447 Creator => { 'read' => 1, 'auto' => 1 },
3448 Created => { 'read' => 1, 'auto' => 1 },
3449 LastUpdatedBy => { 'read' => 1, 'auto' => 1 },
3450 LastUpdated => { 'read' => 1, 'auto' => 1 }
3460 my %args = ( Field => undef,
3463 RecordTransaction => 1,
3466 TransactionType => 'Set',
3469 if ($args{'CheckACL'}) {
3470 unless ( $self->CurrentUserHasRight('ModifyTicket')) {
3471 return ( 0, $self->loc("Permission Denied"));
3475 unless ($args{'UpdateTicket'} || $args{'RecordTransaction'}) {
3476 $RT::Logger->error("Ticket->_Set called without a mandate to record an update or update the ticket");
3477 return(0, $self->loc("Internal Error"));
3480 #if the user is trying to modify the record
3482 #Take care of the old value we really don't want to get in an ACL loop.
3483 # so ask the super::_Value
3484 my $Old = $self->SUPER::_Value("$args{'Field'}");
3487 if ( $args{'UpdateTicket'} ) {
3490 ( $ret, $msg ) = $self->SUPER::_Set( Field => $args{'Field'},
3491 Value => $args{'Value'} );
3493 #If we can't actually set the field to the value, don't record
3494 # a transaction. instead, get out of here.
3495 return ( 0, $msg ) unless $ret;
3498 if ( $args{'RecordTransaction'} == 1 ) {
3500 my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction(
3501 Type => $args{'TransactionType'},
3502 Field => $args{'Field'},
3503 NewValue => $args{'Value'},
3505 TimeTaken => $args{'TimeTaken'},
3507 # Ensure that we can read the transaction, even if the change
3508 # just made the ticket unreadable to us
3509 $TransObj->{ _object_is_readable } = 1;
3510 return ( $Trans, scalar $TransObj->BriefDescription );
3513 return ( $ret, $msg );
3521 Takes the name of a table column.
3522 Returns its value as a string, if the user passes an ACL check
3531 #if the field is public, return it.
3532 if ( $self->_Accessible( $field, 'public' ) ) {
3534 #$RT::Logger->debug("Skipping ACL check for $field");
3535 return ( $self->SUPER::_Value($field) );
3539 #If the current user doesn't have ACLs, don't let em at it.
3541 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
3544 return ( $self->SUPER::_Value($field) );
3550 =head2 _UpdateTimeTaken
3552 This routine will increment the timeworked counter. it should
3553 only be called from _NewTransaction
3557 sub _UpdateTimeTaken {
3559 my $Minutes = shift;
3562 $Total = $self->SUPER::_Value("TimeWorked");
3563 $Total = ( $Total || 0 ) + ( $Minutes || 0 );
3565 Field => "TimeWorked",
3576 =head2 CurrentUserHasRight
3578 Takes the textual name of a Ticket scoped right (from RT::ACE) and returns
3579 1 if the user has that right. It returns 0 if the user doesn't have that right.
3583 sub CurrentUserHasRight {
3587 return $self->CurrentUser->PrincipalObj->HasRight(
3594 =head2 CurrentUserCanSee
3596 Returns true if the current user can see the ticket, using ShowTicket
3600 sub CurrentUserCanSee {
3602 return $self->CurrentUserHasRight('ShowTicket');
3607 Takes a paramhash with the attributes 'Right' and 'Principal'
3608 'Right' is a ticket-scoped textual right from RT::ACE
3609 'Principal' is an RT::User object
3611 Returns 1 if the principal has the right. Returns undef if not.
3623 unless ( ( defined $args{'Principal'} ) and ( ref( $args{'Principal'} ) ) )
3625 Carp::cluck("Principal attrib undefined for Ticket::HasRight");
3626 $RT::Logger->crit("Principal attrib undefined for Ticket::HasRight");
3631 $args{'Principal'}->HasRight(
3633 Right => $args{'Right'}
3642 Return the Reminders object for this ticket. (It's an RT::Reminders object.)
3643 It isn't acutally a searchbuilder collection itself.
3650 unless ($self->{'__reminders'}) {
3651 $self->{'__reminders'} = RT::Reminders->new($self->CurrentUser);
3652 $self->{'__reminders'}->Ticket($self->id);
3654 return $self->{'__reminders'};
3663 Returns an RT::Transactions object of all transactions on this ticket
3670 my $transactions = RT::Transactions->new( $self->CurrentUser );
3672 #If the user has no rights, return an empty object
3673 if ( $self->CurrentUserHasRight('ShowTicket') ) {
3674 $transactions->LimitToTicket($self->id);
3676 # if the user may not see comments do not return them
3677 unless ( $self->CurrentUserHasRight('ShowTicketComments') ) {
3678 $transactions->Limit(
3684 $transactions->Limit(
3688 VALUE => "CommentEmailRecord",
3689 ENTRYAGGREGATOR => 'AND'
3694 $transactions->Limit(
3698 ENTRYAGGREGATOR => 'AND'
3702 return ($transactions);
3708 =head2 TransactionCustomFields
3710 Returns the custom fields that transactions on tickets will have.
3714 sub TransactionCustomFields {
3716 my $cfs = $self->QueueObj->TicketTransactionCustomFields;
3717 $cfs->SetContextObject( $self );
3722 =head2 LoadCustomFieldByIdentifier
3724 Finds and returns the custom field of the given name for the ticket,
3725 overriding L<RT::Record/LoadCustomFieldByIdentifier> to look for
3726 queue-specific CFs before global ones.
3730 sub LoadCustomFieldByIdentifier {
3734 return $self->SUPER::LoadCustomFieldByIdentifier($field)
3735 if ref $field or $field =~ /^\d+$/;
3737 my $cf = RT::CustomField->new( $self->CurrentUser );
3738 $cf->SetContextObject( $self );
3739 $cf->LoadByNameAndQueue( Name => $field, Queue => $self->Queue );
3740 $cf->LoadByNameAndQueue( Name => $field, Queue => 0 ) unless $cf->id;
3745 =head2 CustomFieldLookupType
3747 Returns the RT::Ticket lookup type, which can be passed to
3748 RT::CustomField->Create() via the 'LookupType' hash key.
3753 sub CustomFieldLookupType {
3754 "RT::Queue-RT::Ticket";
3757 =head2 ACLEquivalenceObjects
3759 This method returns a list of objects for which a user's rights also apply
3760 to this ticket. Generally, this is only the ticket's queue, but some RT
3761 extensions may make other objects available too.
3763 This method is called from L<RT::Principal/HasRight>.
3767 sub ACLEquivalenceObjects {
3769 return $self->QueueObj;
3778 Jesse Vincent, jesse@bestpractical.com
3788 use base 'RT::Record';
3790 sub Table {'Tickets'}
3799 Returns the current value of id.
3800 (In the database, id is stored as int(11).)
3808 Returns the current value of EffectiveId.
3809 (In the database, EffectiveId is stored as int(11).)
3813 =head2 SetEffectiveId VALUE
3816 Set EffectiveId to VALUE.
3817 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
3818 (In the database, EffectiveId will be stored as a int(11).)
3826 Returns the current value of Queue.
3827 (In the database, Queue is stored as int(11).)
3831 =head2 SetQueue VALUE
3835 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
3836 (In the database, Queue will be stored as a int(11).)
3844 Returns the current value of Type.
3845 (In the database, Type is stored as varchar(16).)
3849 =head2 SetType VALUE
3853 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
3854 (In the database, Type will be stored as a varchar(16).)
3860 =head2 IssueStatement
3862 Returns the current value of IssueStatement.
3863 (In the database, IssueStatement is stored as int(11).)
3867 =head2 SetIssueStatement VALUE
3870 Set IssueStatement to VALUE.
3871 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
3872 (In the database, IssueStatement will be stored as a int(11).)
3880 Returns the current value of Resolution.
3881 (In the database, Resolution is stored as int(11).)
3885 =head2 SetResolution VALUE
3888 Set Resolution to VALUE.
3889 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
3890 (In the database, Resolution will be stored as a int(11).)
3898 Returns the current value of Owner.
3899 (In the database, Owner is stored as int(11).)
3903 =head2 SetOwner VALUE
3907 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
3908 (In the database, Owner will be stored as a int(11).)
3916 Returns the current value of Subject.
3917 (In the database, Subject is stored as varchar(200).)
3921 =head2 SetSubject VALUE
3924 Set Subject to VALUE.
3925 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
3926 (In the database, Subject will be stored as a varchar(200).)
3932 =head2 InitialPriority
3934 Returns the current value of InitialPriority.
3935 (In the database, InitialPriority is stored as int(11).)
3939 =head2 SetInitialPriority VALUE
3942 Set InitialPriority to VALUE.
3943 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
3944 (In the database, InitialPriority will be stored as a int(11).)
3950 =head2 FinalPriority
3952 Returns the current value of FinalPriority.
3953 (In the database, FinalPriority is stored as int(11).)
3957 =head2 SetFinalPriority VALUE
3960 Set FinalPriority to VALUE.
3961 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
3962 (In the database, FinalPriority will be stored as a int(11).)
3970 Returns the current value of Priority.
3971 (In the database, Priority is stored as int(11).)
3975 =head2 SetPriority VALUE
3978 Set Priority to VALUE.
3979 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
3980 (In the database, Priority will be stored as a int(11).)
3986 =head2 TimeEstimated
3988 Returns the current value of TimeEstimated.
3989 (In the database, TimeEstimated is stored as int(11).)
3993 =head2 SetTimeEstimated VALUE
3996 Set TimeEstimated to VALUE.
3997 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
3998 (In the database, TimeEstimated will be stored as a int(11).)
4006 Returns the current value of TimeWorked.
4007 (In the database, TimeWorked is stored as int(11).)
4011 =head2 SetTimeWorked VALUE
4014 Set TimeWorked to VALUE.
4015 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
4016 (In the database, TimeWorked will be stored as a int(11).)
4024 Returns the current value of Status.
4025 (In the database, Status is stored as varchar(64).)
4029 =head2 SetStatus VALUE
4032 Set Status to VALUE.
4033 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
4034 (In the database, Status will be stored as a varchar(64).)
4042 Returns the current value of TimeLeft.
4043 (In the database, TimeLeft is stored as int(11).)
4047 =head2 SetTimeLeft VALUE
4050 Set TimeLeft to VALUE.
4051 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
4052 (In the database, TimeLeft will be stored as a int(11).)
4060 Returns the current value of Told.
4061 (In the database, Told is stored as datetime.)
4065 =head2 SetTold VALUE
4069 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
4070 (In the database, Told will be stored as a datetime.)
4078 Returns the current value of Starts.
4079 (In the database, Starts is stored as datetime.)
4083 =head2 SetStarts VALUE
4086 Set Starts to VALUE.
4087 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
4088 (In the database, Starts will be stored as a datetime.)
4096 Returns the current value of Started.
4097 (In the database, Started is stored as datetime.)
4101 =head2 SetStarted VALUE
4104 Set Started to VALUE.
4105 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
4106 (In the database, Started will be stored as a datetime.)
4114 Returns the current value of Due.
4115 (In the database, Due is stored as datetime.)
4123 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
4124 (In the database, Due will be stored as a datetime.)
4132 Returns the current value of Resolved.
4133 (In the database, Resolved is stored as datetime.)
4137 =head2 SetResolved VALUE
4140 Set Resolved to VALUE.
4141 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
4142 (In the database, Resolved will be stored as a datetime.)
4148 =head2 LastUpdatedBy
4150 Returns the current value of LastUpdatedBy.
4151 (In the database, LastUpdatedBy is stored as int(11).)
4159 Returns the current value of LastUpdated.
4160 (In the database, LastUpdated is stored as datetime.)
4168 Returns the current value of Creator.
4169 (In the database, Creator is stored as int(11).)
4177 Returns the current value of Created.
4178 (In the database, Created is stored as datetime.)
4186 Returns the current value of Disabled.
4187 (In the database, Disabled is stored as smallint(6).)
4191 =head2 SetDisabled VALUE
4194 Set Disabled to VALUE.
4195 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
4196 (In the database, Disabled will be stored as a smallint(6).)
4203 sub _CoreAccessible {
4207 {read => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => ''},
4209 {read => 1, write => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'},
4211 {read => 1, write => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'},
4213 {read => 1, write => 1, sql_type => 12, length => 16, is_blob => 0, is_numeric => 0, type => 'varchar(16)', default => ''},
4215 {read => 1, write => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'},
4217 {read => 1, write => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'},
4219 {read => 1, write => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'},
4221 {read => 1, write => 1, sql_type => 12, length => 200, is_blob => 0, is_numeric => 0, type => 'varchar(200)', default => '[no subject]'},
4223 {read => 1, write => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'},
4225 {read => 1, write => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'},
4227 {read => 1, write => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'},
4229 {read => 1, write => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'},
4231 {read => 1, write => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'},
4233 {read => 1, write => 1, sql_type => 12, length => 64, is_blob => 0, is_numeric => 0, type => 'varchar(64)', default => ''},
4235 {read => 1, write => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'},
4237 {read => 1, write => 1, sql_type => 11, length => 0, is_blob => 0, is_numeric => 0, type => 'datetime', default => ''},
4239 {read => 1, write => 1, sql_type => 11, length => 0, is_blob => 0, is_numeric => 0, type => 'datetime', default => ''},
4241 {read => 1, write => 1, sql_type => 11, length => 0, is_blob => 0, is_numeric => 0, type => 'datetime', default => ''},
4243 {read => 1, write => 1, sql_type => 11, length => 0, is_blob => 0, is_numeric => 0, type => 'datetime', default => ''},
4245 {read => 1, write => 1, sql_type => 11, length => 0, is_blob => 0, is_numeric => 0, type => 'datetime', default => ''},
4247 {read => 1, auto => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'},
4249 {read => 1, auto => 1, sql_type => 11, length => 0, is_blob => 0, is_numeric => 0, type => 'datetime', default => ''},
4251 {read => 1, auto => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'},
4253 {read => 1, auto => 1, sql_type => 11, length => 0, is_blob => 0, is_numeric => 0, type => 'datetime', default => ''},
4255 {read => 1, write => 1, sql_type => 5, length => 6, is_blob => 0, is_numeric => 1, type => 'smallint(6)', default => '0'},
4260 RT::Base->_ImportOverlays();