1 # BEGIN BPS TAGGED BLOCK {{{
5 # This software is Copyright (c) 1996-2012 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('That principal is already a [_1] for this ticket', $self->loc($args{'Type'})) );
1147 my ( $m_id, $m_msg ) = $group->_AddMember( PrincipalId => $principal->Id,
1148 InsideTransaction => 1 );
1150 $RT::Logger->error("Failed to add ".$principal->Id." as a member of group ".$group->Id.": ".$m_msg);
1152 return ( 0, $self->loc('Could not make that principal a [_1] for this ticket', $self->loc($args{'Type'})) );
1155 unless ( $args{'Silent'} ) {
1156 $self->_NewTransaction(
1157 Type => 'AddWatcher',
1158 NewValue => $principal->Id,
1159 Field => $args{'Type'}
1163 return ( 1, $self->loc('Added principal as a [_1] for this ticket', $self->loc($args{'Type'})) );
1169 =head2 DeleteWatcher { Type => TYPE, PrincipalId => PRINCIPAL_ID, Email => EMAIL_ADDRESS }
1172 Deletes a Ticket watcher. Takes two arguments:
1174 Type (one of Requestor,Cc,AdminCc)
1178 PrincipalId (an RT::Principal Id of the watcher you want to remove)
1180 Email (the email address of an existing wathcer)
1189 my %args = ( Type => undef,
1190 PrincipalId => undef,
1194 unless ( $args{'PrincipalId'} || $args{'Email'} ) {
1195 return ( 0, $self->loc("No principal specified") );
1197 my $principal = RT::Principal->new( $self->CurrentUser );
1198 if ( $args{'PrincipalId'} ) {
1200 $principal->Load( $args{'PrincipalId'} );
1203 my $user = RT::User->new( $self->CurrentUser );
1204 $user->LoadByEmail( $args{'Email'} );
1205 $principal->Load( $user->Id );
1208 # If we can't find this watcher, we need to bail.
1209 unless ( $principal->Id ) {
1210 return ( 0, $self->loc("Could not find that principal") );
1213 my $group = RT::Group->new( $self->CurrentUser );
1214 $group->LoadTicketRoleGroup( Type => $args{'Type'}, Ticket => $self->Id );
1215 unless ( $group->id ) {
1216 return ( 0, $self->loc("Group not found") );
1220 #If the watcher we're trying to add is for the current user
1221 if ( $self->CurrentUser->PrincipalId == $principal->id ) {
1223 # If it's an AdminCc and they don't have
1224 # 'WatchAsAdminCc' or 'ModifyTicket', bail
1225 if ( $args{'Type'} eq 'AdminCc' ) {
1226 unless ( $self->CurrentUserHasRight('ModifyTicket')
1227 or $self->CurrentUserHasRight('WatchAsAdminCc') ) {
1228 return ( 0, $self->loc('Permission Denied') );
1232 # If it's a Requestor or Cc and they don't have
1233 # 'Watch' or 'ModifyTicket', bail
1234 elsif ( ( $args{'Type'} eq 'Cc' ) or ( $args{'Type'} eq 'Requestor' ) )
1236 unless ( $self->CurrentUserHasRight('ModifyTicket')
1237 or $self->CurrentUserHasRight('Watch') ) {
1238 return ( 0, $self->loc('Permission Denied') );
1242 $RT::Logger->warning("$self -> DeleteWatcher got passed a bogus type");
1244 $self->loc('Error in parameters to Ticket->DeleteWatcher') );
1248 # If the watcher isn't the current user
1249 # and the current user doesn't have 'ModifyTicket' bail
1251 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
1252 return ( 0, $self->loc("Permission Denied") );
1258 # see if this user is already a watcher.
1260 unless ( $group->HasMember($principal) ) {
1262 $self->loc( 'That principal is not a [_1] for this ticket',
1266 my ( $m_id, $m_msg ) = $group->_DeleteMember( $principal->Id );
1268 $RT::Logger->error( "Failed to delete "
1270 . " as a member of group "
1276 'Could not remove that principal as a [_1] for this ticket',
1280 unless ( $args{'Silent'} ) {
1281 $self->_NewTransaction( Type => 'DelWatcher',
1282 OldValue => $principal->Id,
1283 Field => $args{'Type'} );
1287 $self->loc( "[_1] is no longer a [_2] for this ticket.",
1288 $principal->Object->Name,
1296 =head2 SquelchMailTo [EMAIL]
1298 Takes an optional email address to never email about updates to this ticket.
1301 Returns an array of the RT::Attribute objects for this ticket's 'SquelchMailTo' attributes.
1309 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
1313 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
1318 return $self->_SquelchMailTo(@_);
1321 sub _SquelchMailTo {
1325 $self->AddAttribute( Name => 'SquelchMailTo', Content => $attr )
1326 unless grep { $_->Content eq $attr }
1327 $self->Attributes->Named('SquelchMailTo');
1329 my @attributes = $self->Attributes->Named('SquelchMailTo');
1330 return (@attributes);
1334 =head2 UnsquelchMailTo ADDRESS
1336 Takes an address and removes it from this ticket's "SquelchMailTo" list. If an address appears multiple times, each instance is removed.
1338 Returns a tuple of (status, message)
1342 sub UnsquelchMailTo {
1345 my $address = shift;
1346 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
1347 return ( 0, $self->loc("Permission Denied") );
1350 my ($val, $msg) = $self->Attributes->DeleteEntry ( Name => 'SquelchMailTo', Content => $address);
1351 return ($val, $msg);
1356 =head2 RequestorAddresses
1358 B<Returns> String: All Ticket Requestor email addresses as a string.
1362 sub RequestorAddresses {
1365 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
1369 return ( $self->Requestors->MemberEmailAddressesAsString );
1373 =head2 AdminCcAddresses
1375 returns String: All Ticket AdminCc email addresses as a string
1379 sub AdminCcAddresses {
1382 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
1386 return ( $self->AdminCc->MemberEmailAddressesAsString )
1392 returns String: All Ticket Ccs as a string of email addresses
1399 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
1402 return ( $self->Cc->MemberEmailAddressesAsString);
1412 Returns this ticket's Requestors as an RT::Group object
1419 my $group = RT::Group->new($self->CurrentUser);
1420 if ( $self->CurrentUserHasRight('ShowTicket') ) {
1421 $group->LoadTicketRoleGroup(Type => 'Requestor', Ticket => $self->Id);
1432 Returns an RT::Group object which contains this ticket's Ccs.
1433 If the user doesn't have "ShowTicket" permission, returns an empty group
1440 my $group = RT::Group->new($self->CurrentUser);
1441 if ( $self->CurrentUserHasRight('ShowTicket') ) {
1442 $group->LoadTicketRoleGroup(Type => 'Cc', Ticket => $self->Id);
1453 Returns an RT::Group object which contains this ticket's AdminCcs.
1454 If the user doesn't have "ShowTicket" permission, returns an empty group
1461 my $group = RT::Group->new($self->CurrentUser);
1462 if ( $self->CurrentUserHasRight('ShowTicket') ) {
1463 $group->LoadTicketRoleGroup(Type => 'AdminCc', Ticket => $self->Id);
1472 # a generic routine to be called by IsRequestor, IsCc and IsAdminCc
1474 =head2 IsWatcher { Type => TYPE, PrincipalId => PRINCIPAL_ID, Email => EMAIL }
1476 Takes a param hash with the attributes Type and either PrincipalId or Email
1478 Type is one of Requestor, Cc, AdminCc and Owner
1480 PrincipalId is an RT::Principal id, and Email is an email address.
1482 Returns true if the specified principal (or the one corresponding to the
1483 specified address) is a member of the group Type for this ticket.
1485 XX TODO: This should be Memoized.
1492 my %args = ( Type => 'Requestor',
1493 PrincipalId => undef,
1498 # Load the relevant group.
1499 my $group = RT::Group->new($self->CurrentUser);
1500 $group->LoadTicketRoleGroup(Type => $args{'Type'}, Ticket => $self->id);
1502 # Find the relevant principal.
1503 if (!$args{PrincipalId} && $args{Email}) {
1504 # Look up the specified user.
1505 my $user = RT::User->new($self->CurrentUser);
1506 $user->LoadByEmail($args{Email});
1508 $args{PrincipalId} = $user->PrincipalId;
1511 # A non-existent user can't be a group member.
1516 # Ask if it has the member in question
1517 return $group->HasMember( $args{'PrincipalId'} );
1522 =head2 IsRequestor PRINCIPAL_ID
1524 Takes an L<RT::Principal> id.
1526 Returns true if the principal is a requestor of the current ticket.
1534 return ( $self->IsWatcher( Type => 'Requestor', PrincipalId => $person ) );
1540 =head2 IsCc PRINCIPAL_ID
1542 Takes an RT::Principal id.
1543 Returns true if the principal is a Cc of the current ticket.
1552 return ( $self->IsWatcher( Type => 'Cc', PrincipalId => $cc ) );
1558 =head2 IsAdminCc PRINCIPAL_ID
1560 Takes an RT::Principal id.
1561 Returns true if the principal is an AdminCc of the current ticket.
1569 return ( $self->IsWatcher( Type => 'AdminCc', PrincipalId => $person ) );
1577 Takes an RT::User object. Returns true if that user is this ticket's owner.
1578 returns undef otherwise
1586 # no ACL check since this is used in acl decisions
1587 # unless ($self->CurrentUserHasRight('ShowTicket')) {
1591 #Tickets won't yet have owners when they're being created.
1592 unless ( $self->OwnerObj->id ) {
1596 if ( $person->id == $self->OwnerObj->id ) {
1608 =head2 TransactionAddresses
1610 Returns a composite hashref of the results of L<RT::Transaction/Addresses> for
1611 all this ticket's Create, Comment or Correspond transactions. The keys are
1612 stringified email addresses. Each value is an L<Email::Address> object.
1614 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.
1619 sub TransactionAddresses {
1621 my $txns = $self->Transactions;
1625 my $attachments = RT::Attachments->new( $self->CurrentUser );
1626 $attachments->LimitByTicket( $self->id );
1627 $attachments->Columns( qw( id Headers TransactionId));
1630 foreach my $type (qw(Create Comment Correspond)) {
1631 $attachments->Limit( ALIAS => $attachments->TransactionAlias,
1635 ENTRYAGGREGATOR => 'OR',
1640 while ( my $att = $attachments->Next ) {
1641 foreach my $addrlist ( values %{$att->Addresses } ) {
1642 foreach my $addr (@$addrlist) {
1644 # Skip addresses without a phrase (things that are just raw addresses) if we have a phrase
1646 if ( $addresses{ $addr->address }
1647 && $addresses{ $addr->address }->phrase
1648 && not $addr->phrase );
1650 # skips "comment-only" addresses
1651 next unless ( $addr->address );
1652 $addresses{ $addr->address } = $addr;
1671 $RT::Logger->warning( " RT:::Queue::ValidateQueue called with a null value. this isn't ok.");
1675 my $QueueObj = RT::Queue->new( $self->CurrentUser );
1676 my $id = $QueueObj->Load($Value);
1690 my $NewQueue = shift;
1692 #Redundant. ACL gets checked in _Set;
1693 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
1694 return ( 0, $self->loc("Permission Denied") );
1697 my $NewQueueObj = RT::Queue->new( $self->CurrentUser );
1698 $NewQueueObj->Load($NewQueue);
1700 unless ( $NewQueueObj->Id() ) {
1701 return ( 0, $self->loc("That queue does not exist") );
1704 if ( $NewQueueObj->Id == $self->QueueObj->Id ) {
1705 return ( 0, $self->loc('That is the same value') );
1707 unless ( $self->CurrentUser->HasRight( Right => 'CreateTicket', Object => $NewQueueObj)) {
1708 return ( 0, $self->loc("You may not create requests in that queue.") );
1712 my $old_lifecycle = $self->QueueObj->Lifecycle;
1713 my $new_lifecycle = $NewQueueObj->Lifecycle;
1714 if ( $old_lifecycle->Name ne $new_lifecycle->Name ) {
1715 unless ( $old_lifecycle->HasMoveMap( $new_lifecycle ) ) {
1716 return ( 0, $self->loc("There is no mapping for statuses between these queues. Contact your system administrator.") );
1718 $new_status = $old_lifecycle->MoveMap( $new_lifecycle )->{ $self->Status };
1719 return ( 0, $self->loc("Mapping between queues' lifecycles is incomplete. Contact your system administrator.") )
1723 if ( $new_status ) {
1724 my $clone = RT::Ticket->new( RT->SystemUser );
1725 $clone->Load( $self->Id );
1726 unless ( $clone->Id ) {
1727 return ( 0, $self->loc("Couldn't load copy of ticket #[_1].", $self->Id) );
1730 my $now = RT::Date->new( $self->CurrentUser );
1733 my $old_status = $clone->Status;
1735 #If we're changing the status from initial in old to not intial in new,
1736 # record that we've started
1737 if ( $old_lifecycle->IsInitial($old_status) && !$new_lifecycle->IsInitial($new_status) && $clone->StartedObj->Unix == 0 ) {
1738 #Set the Started time to "now"
1742 RecordTransaction => 0
1746 #When we close a ticket, set the 'Resolved' attribute to now.
1747 # It's misnamed, but that's just historical.
1748 if ( $new_lifecycle->IsInactive($new_status) ) {
1750 Field => 'Resolved',
1752 RecordTransaction => 0,
1756 #Actually update the status
1757 my ($val, $msg)= $clone->_Set(
1759 Value => $new_status,
1760 RecordTransaction => 0,
1762 $RT::Logger->error( 'Status change failed on queue change: '. $msg )
1766 my ($status, $msg) = $self->_Set( Field => 'Queue', Value => $NewQueueObj->Id() );
1769 # Clear the queue object cache;
1770 $self->{_queue_obj} = undef;
1772 # Untake the ticket if we have no permissions in the new queue
1773 unless ( $self->OwnerObj->HasRight( Right => 'OwnTicket', Object => $NewQueueObj ) ) {
1774 my $clone = RT::Ticket->new( RT->SystemUser );
1775 $clone->Load( $self->Id );
1776 unless ( $clone->Id ) {
1777 return ( 0, $self->loc("Couldn't load copy of ticket #[_1].", $self->Id) );
1779 my ($status, $msg) = $clone->SetOwner( RT->Nobody->Id, 'Force' );
1780 $RT::Logger->error("Couldn't set owner on queue change: $msg") unless $status;
1783 # On queue change, change queue for reminders too
1784 my $reminder_collection = $self->Reminders->Collection;
1785 while ( my $reminder = $reminder_collection->Next ) {
1786 my ($status, $msg) = $reminder->SetQueue($NewQueue);
1787 $RT::Logger->error('Queue change failed for reminder #' . $reminder->Id . ': ' . $msg) unless $status;
1791 return ($status, $msg);
1798 Takes nothing. returns this ticket's queue object
1805 if(!$self->{_queue_obj} || ! $self->{_queue_obj}->id) {
1807 $self->{_queue_obj} = RT::Queue->new( $self->CurrentUser );
1809 #We call __Value so that we can avoid the ACL decision and some deep recursion
1810 my ($result) = $self->{_queue_obj}->Load( $self->__Value('Queue') );
1812 return ($self->{_queue_obj});
1817 Takes nothing. Returns SubjectTag for this ticket. Includes
1818 queue's subject tag or rtname if that is not set, ticket
1819 id and braces, for example:
1821 [support.example.com #123456]
1829 . ($self->QueueObj->SubjectTag || RT->Config->Get('rtname'))
1838 Returns an RT::Date object containing this ticket's due date
1845 my $time = RT::Date->new( $self->CurrentUser );
1847 # -1 is RT::Date slang for never
1848 if ( my $due = $self->Due ) {
1849 $time->Set( Format => 'sql', Value => $due );
1852 $time->Set( Format => 'unix', Value => -1 );
1862 Returns this ticket's due date as a human readable string
1868 return $self->DueObj->AsString();
1875 Returns an RT::Date object of this ticket's 'resolved' time.
1882 my $time = RT::Date->new( $self->CurrentUser );
1883 $time->Set( Format => 'sql', Value => $self->Resolved );
1888 =head2 FirstActiveStatus
1890 Returns the first active status that the ticket could transition to,
1891 according to its current Queue's lifecycle. May return undef if there
1892 is no such possible status to transition to, or we are already in it.
1893 This is used in L<RT::Action::AutoOpen>, for instance.
1897 sub FirstActiveStatus {
1900 my $lifecycle = $self->QueueObj->Lifecycle;
1901 my $status = $self->Status;
1902 my @active = $lifecycle->Active;
1903 # no change if no active statuses in the lifecycle
1904 return undef unless @active;
1906 # no change if the ticket is already has first status from the list of active
1907 return undef if lc $status eq lc $active[0];
1909 my ($next) = grep $lifecycle->IsActive($_), $lifecycle->Transitions($status);
1913 =head2 FirstInactiveStatus
1915 Returns the first inactive status that the ticket could transition to,
1916 according to its current Queue's lifecycle. May return undef if there
1917 is no such possible status to transition to, or we are already in it.
1918 This is used in resolve action in UnsafeEmailCommands, for instance.
1922 sub FirstInactiveStatus {
1925 my $lifecycle = $self->QueueObj->Lifecycle;
1926 my $status = $self->Status;
1927 my @inactive = $lifecycle->Inactive;
1928 # no change if no inactive statuses in the lifecycle
1929 return undef unless @inactive;
1931 # no change if the ticket is already has first status from the list of inactive
1932 return undef if lc $status eq lc $inactive[0];
1934 my ($next) = grep $lifecycle->IsInactive($_), $lifecycle->Transitions($status);
1940 Takes a date in ISO format or undef
1941 Returns a transaction id and a message
1942 The client calls "Start" to note that the project was started on the date in $date.
1943 A null date means "now"
1949 my $time = shift || 0;
1951 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
1952 return ( 0, $self->loc("Permission Denied") );
1955 #We create a date object to catch date weirdness
1956 my $time_obj = RT::Date->new( $self->CurrentUser() );
1958 $time_obj->Set( Format => 'ISO', Value => $time );
1961 $time_obj->SetToNow();
1964 # We need $TicketAsSystem, in case the current user doesn't have
1966 my $TicketAsSystem = RT::Ticket->new(RT->SystemUser);
1967 $TicketAsSystem->Load( $self->Id );
1968 # Now that we're starting, open this ticket
1969 # TODO: do we really want to force this as policy? it should be a scrip
1970 my $next = $TicketAsSystem->FirstActiveStatus;
1972 $self->SetStatus( $next ) if defined $next;
1974 return ( $self->_Set( Field => 'Started', Value => $time_obj->ISO ) );
1982 Returns an RT::Date object which contains this ticket's
1990 my $time = RT::Date->new( $self->CurrentUser );
1991 $time->Set( Format => 'sql', Value => $self->Started );
1999 Returns an RT::Date object which contains this ticket's
2007 my $time = RT::Date->new( $self->CurrentUser );
2008 $time->Set( Format => 'sql', Value => $self->Starts );
2016 Returns an RT::Date object which contains this ticket's
2024 my $time = RT::Date->new( $self->CurrentUser );
2025 $time->Set( Format => 'sql', Value => $self->Told );
2033 A convenience method that returns ToldObj->AsString
2035 TODO: This should be deprecated
2041 if ( $self->Told ) {
2042 return $self->ToldObj->AsString();
2051 =head2 TimeWorkedAsString
2053 Returns the amount of time worked on this ticket as a Text String
2057 sub TimeWorkedAsString {
2059 my $value = $self->TimeWorked;
2061 # return the # of minutes worked turned into seconds and written as
2062 # a simple text string, this is not really a date object, but if we
2063 # diff a number of seconds vs the epoch, we'll get a nice description
2065 return "" unless $value;
2066 return RT::Date->new( $self->CurrentUser )
2067 ->DurationAsString( $value * 60 );
2072 =head2 TimeLeftAsString
2074 Returns the amount of time left on this ticket as a Text String
2078 sub TimeLeftAsString {
2080 my $value = $self->TimeLeft;
2081 return "" unless $value;
2082 return RT::Date->new( $self->CurrentUser )
2083 ->DurationAsString( $value * 60 );
2091 Comment on this ticket.
2092 Takes a hash with the following attributes:
2093 If MIMEObj is undefined, Content will be used to build a MIME::Entity for this
2096 MIMEObj, TimeTaken, CcMessageTo, BccMessageTo, Content, DryRun
2098 If DryRun is defined, this update WILL NOT BE RECORDED. Scrips will not be committed.
2099 They will, however, be prepared and you'll be able to access them through the TransactionObj
2101 Returns: Transaction id, Error Message, Transaction Object
2102 (note the different order from Create()!)
2109 my %args = ( CcMessageTo => undef,
2110 BccMessageTo => undef,
2117 unless ( ( $self->CurrentUserHasRight('CommentOnTicket') )
2118 or ( $self->CurrentUserHasRight('ModifyTicket') ) ) {
2119 return ( 0, $self->loc("Permission Denied"), undef );
2121 $args{'NoteType'} = 'Comment';
2123 $RT::Handle->BeginTransaction();
2124 if ($args{'DryRun'}) {
2125 $args{'CommitScrips'} = 0;
2128 my @results = $self->_RecordNote(%args);
2129 if ($args{'DryRun'}) {
2130 $RT::Handle->Rollback();
2132 $RT::Handle->Commit();
2141 Correspond on this ticket.
2142 Takes a hashref with the following attributes:
2145 MIMEObj, TimeTaken, CcMessageTo, BccMessageTo, Content, DryRun
2147 if there's no MIMEObj, Content is used to build a MIME::Entity object
2149 If DryRun is defined, this update WILL NOT BE RECORDED. Scrips will not be committed.
2150 They will, however, be prepared and you'll be able to access them through the TransactionObj
2152 Returns: Transaction id, Error Message, Transaction Object
2153 (note the different order from Create()!)
2160 my %args = ( CcMessageTo => undef,
2161 BccMessageTo => undef,
2167 unless ( ( $self->CurrentUserHasRight('ReplyToTicket') )
2168 or ( $self->CurrentUserHasRight('ModifyTicket') ) ) {
2169 return ( 0, $self->loc("Permission Denied"), undef );
2171 $args{'NoteType'} = 'Correspond';
2173 $RT::Handle->BeginTransaction();
2174 if ($args{'DryRun'}) {
2175 $args{'CommitScrips'} = 0;
2178 my @results = $self->_RecordNote(%args);
2180 #Set the last told date to now if this isn't mail from the requestor.
2181 #TODO: Note that this will wrongly ack mail from any non-requestor as a "told"
2182 unless ( $self->IsRequestor($self->CurrentUser->id) ) {
2184 $squelch{$_}++ for map {$_->Content} $self->SquelchMailTo, $results[2]->SquelchMailTo;
2186 if grep {not $squelch{$_}} $self->Requestors->MemberEmailAddresses;
2189 if ($args{'DryRun'}) {
2190 $RT::Handle->Rollback();
2192 $RT::Handle->Commit();
2203 the meat of both comment and correspond.
2205 Performs no access control checks. hence, dangerous.
2212 CcMessageTo => undef,
2213 BccMessageTo => undef,
2218 NoteType => 'Correspond',
2221 SquelchMailTo => undef,
2225 unless ( $args{'MIMEObj'} || $args{'Content'} ) {
2226 return ( 0, $self->loc("No message attached"), undef );
2229 unless ( $args{'MIMEObj'} ) {
2230 $args{'MIMEObj'} = MIME::Entity->build(
2231 Data => ( ref $args{'Content'}? $args{'Content'}: [ $args{'Content'} ] )
2235 # convert text parts into utf-8
2236 RT::I18N::SetMIMEEntityToUTF8( $args{'MIMEObj'} );
2238 # If we've been passed in CcMessageTo and BccMessageTo fields,
2239 # add them to the mime object for passing on to the transaction handler
2240 # The "NotifyOtherRecipients" scripAction will look for RT-Send-Cc: and
2241 # RT-Send-Bcc: headers
2244 foreach my $type (qw/Cc Bcc/) {
2245 if ( defined $args{ $type . 'MessageTo' } ) {
2247 my $addresses = join ', ', (
2248 map { RT::User->CanonicalizeEmailAddress( $_->address ) }
2249 Email::Address->parse( $args{ $type . 'MessageTo' } ) );
2250 $args{'MIMEObj'}->head->replace( 'RT-Send-' . $type, Encode::encode_utf8( $addresses ) );
2254 foreach my $argument (qw(Encrypt Sign)) {
2255 $args{'MIMEObj'}->head->replace(
2256 "X-RT-$argument" => Encode::encode_utf8( $args{ $argument } )
2257 ) if defined $args{ $argument };
2260 # If this is from an external source, we need to come up with its
2261 # internal Message-ID now, so all emails sent because of this
2262 # message have a common Message-ID
2263 my $org = RT->Config->Get('Organization');
2264 my $msgid = $args{'MIMEObj'}->head->get('Message-ID');
2265 unless (defined $msgid && $msgid =~ /<(rt-.*?-\d+-\d+)\.(\d+-0-0)\@\Q$org\E>/) {
2266 $args{'MIMEObj'}->head->set(
2267 'RT-Message-ID' => Encode::encode_utf8(
2268 RT::Interface::Email::GenMessageId( Ticket => $self )
2273 #Record the correspondence (write the transaction)
2274 my ( $Trans, $msg, $TransObj ) = $self->_NewTransaction(
2275 Type => $args{'NoteType'},
2276 Data => ( $args{'MIMEObj'}->head->get('subject') || 'No Subject' ),
2277 TimeTaken => $args{'TimeTaken'},
2278 MIMEObj => $args{'MIMEObj'},
2279 CommitScrips => $args{'CommitScrips'},
2280 SquelchMailTo => $args{'SquelchMailTo'},
2284 $RT::Logger->err("$self couldn't init a transaction $msg");
2285 return ( $Trans, $self->loc("Message could not be recorded"), undef );
2288 return ( $Trans, $self->loc("Message recorded"), $TransObj );
2294 Builds a MIME object from the given C<UpdateSubject> and
2295 C<UpdateContent>, then calls L</Comment> or L</Correspond> with
2296 C<< DryRun => 1 >>, and returns the transaction so produced.
2304 if (($args{'UpdateType'} || $args{Action}) =~ /^respon(d|se)$/i ) {
2305 $action = 'Correspond';
2307 $action = 'Comment';
2310 my $Message = MIME::Entity->build(
2311 Type => 'text/plain',
2312 Subject => defined $args{UpdateSubject} ? Encode::encode_utf8( $args{UpdateSubject} ) : "",
2314 Data => $args{'UpdateContent'} || "",
2317 my ( $Transaction, $Description, $Object ) = $self->$action(
2318 CcMessageTo => $args{'UpdateCc'},
2319 BccMessageTo => $args{'UpdateBcc'},
2320 MIMEObj => $Message,
2321 TimeTaken => $args{'UpdateTimeWorked'},
2324 unless ( $Transaction ) {
2325 $RT::Logger->error("Couldn't fire '$action' action: $Description");
2333 Prepares a MIME mesage with the given C<Subject>, C<Cc>, and
2334 C<Content>, then calls L</Create> with C<< DryRun => 1 >> and returns
2335 the resulting L<RT::Transaction>.
2342 my $Message = MIME::Entity->build(
2343 Type => 'text/plain',
2344 Subject => defined $args{Subject} ? Encode::encode_utf8( $args{'Subject'} ) : "",
2345 (defined $args{'Cc'} ?
2346 ( Cc => Encode::encode_utf8( $args{'Cc'} ) ) : ()),
2348 Data => $args{'Content'} || "",
2351 my ( $Transaction, $Object, $Description ) = $self->Create(
2352 Type => $args{'Type'} || 'ticket',
2353 Queue => $args{'Queue'},
2354 Owner => $args{'Owner'},
2355 Requestor => $args{'Requestors'},
2357 AdminCc => $args{'AdminCc'},
2358 InitialPriority => $args{'InitialPriority'},
2359 FinalPriority => $args{'FinalPriority'},
2360 TimeLeft => $args{'TimeLeft'},
2361 TimeEstimated => $args{'TimeEstimated'},
2362 TimeWorked => $args{'TimeWorked'},
2363 Subject => $args{'Subject'},
2364 Status => $args{'Status'},
2365 MIMEObj => $Message,
2368 unless ( $Transaction ) {
2369 $RT::Logger->error("Couldn't fire Create action: $Description");
2380 #TODO: Field isn't the right thing here. but I ahave no idea what mnemonic ---
2383 my $type = shift || "";
2385 my $cache_key = "$field$type";
2386 return $self->{ $cache_key } if $self->{ $cache_key };
2388 my $links = $self->{ $cache_key }
2389 = RT::Links->new( $self->CurrentUser );
2390 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
2391 $links->Limit( FIELD => 'id', VALUE => 0, SUBCLAUSE => 'acl' );
2395 # Maybe this ticket is a merge ticket
2396 my $limit_on = 'Local'. $field;
2397 # at least to myself
2401 ENTRYAGGREGATOR => 'OR',
2406 ENTRYAGGREGATOR => 'OR',
2407 ) foreach $self->Merged;
2420 Delete a link. takes a paramhash of Base, Target, Type, Silent,
2421 SilentBase and SilentTarget. Either Base or Target must be null.
2422 The null value will be replaced with this ticket\'s id.
2424 If Silent is true then no transaction would be recorded, in other
2425 case you can control creation of transactions on both base and
2426 target with SilentBase and SilentTarget respectively. By default
2427 both transactions are created.
2438 SilentBase => undef,
2439 SilentTarget => undef,
2443 unless ( $args{'Target'} || $args{'Base'} ) {
2444 $RT::Logger->error("Base or Target must be specified");
2445 return ( 0, $self->loc('Either base or target must be specified') );
2450 $right++ if $self->CurrentUserHasRight('ModifyTicket');
2451 if ( !$right && RT->Config->Get( 'StrictLinkACL' ) ) {
2452 return ( 0, $self->loc("Permission Denied") );
2455 # If the other URI is an RT::Ticket, we want to make sure the user
2456 # can modify it too...
2457 my ($status, $msg, $other_ticket) = $self->__GetTicketFromURI( URI => $args{'Target'} || $args{'Base'} );
2458 return (0, $msg) unless $status;
2459 if ( !$other_ticket || $other_ticket->CurrentUserHasRight('ModifyTicket') ) {
2462 if ( ( !RT->Config->Get( 'StrictLinkACL' ) && $right == 0 ) ||
2463 ( RT->Config->Get( 'StrictLinkACL' ) && $right < 2 ) )
2465 return ( 0, $self->loc("Permission Denied") );
2468 my ($val, $Msg) = $self->SUPER::_DeleteLink(%args);
2469 return ( 0, $Msg ) unless $val;
2471 return ( $val, $Msg ) if $args{'Silent'};
2473 my ($direction, $remote_link);
2475 if ( $args{'Base'} ) {
2476 $remote_link = $args{'Base'};
2477 $direction = 'Target';
2479 elsif ( $args{'Target'} ) {
2480 $remote_link = $args{'Target'};
2481 $direction = 'Base';
2484 my $remote_uri = RT::URI->new( $self->CurrentUser );
2485 $remote_uri->FromURI( $remote_link );
2487 unless ( $args{ 'Silent'. $direction } ) {
2488 my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction(
2489 Type => 'DeleteLink',
2490 Field => $LINKDIRMAP{$args{'Type'}}->{$direction},
2491 OldValue => $remote_uri->URI || $remote_link,
2494 $RT::Logger->error("Couldn't create transaction: $Msg") unless $Trans;
2497 if ( !$args{ 'Silent'. ( $direction eq 'Target'? 'Base': 'Target' ) } && $remote_uri->IsLocal ) {
2498 my $OtherObj = $remote_uri->Object;
2499 my ( $val, $Msg ) = $OtherObj->_NewTransaction(
2500 Type => 'DeleteLink',
2501 Field => $direction eq 'Target' ? $LINKDIRMAP{$args{'Type'}}->{Base}
2502 : $LINKDIRMAP{$args{'Type'}}->{Target},
2503 OldValue => $self->URI,
2504 ActivateScrips => !RT->Config->Get('LinkTransactionsRun1Scrip'),
2507 $RT::Logger->error("Couldn't create transaction: $Msg") unless $val;
2510 return ( $val, $Msg );
2517 Takes a paramhash of Type and one of Base or Target. Adds that link to this ticket.
2519 If Silent is true then no transaction would be recorded, in other
2520 case you can control creation of transactions on both base and
2521 target with SilentBase and SilentTarget respectively. By default
2522 both transactions are created.
2528 my %args = ( Target => '',
2532 SilentBase => undef,
2533 SilentTarget => undef,
2536 unless ( $args{'Target'} || $args{'Base'} ) {
2537 $RT::Logger->error("Base or Target must be specified");
2538 return ( 0, $self->loc('Either base or target must be specified') );
2542 $right++ if $self->CurrentUserHasRight('ModifyTicket');
2543 if ( !$right && RT->Config->Get( 'StrictLinkACL' ) ) {
2544 return ( 0, $self->loc("Permission Denied") );
2547 # If the other URI is an RT::Ticket, we want to make sure the user
2548 # can modify it too...
2549 my ($status, $msg, $other_ticket) = $self->__GetTicketFromURI( URI => $args{'Target'} || $args{'Base'} );
2550 return (0, $msg) unless $status;
2551 if ( !$other_ticket || $other_ticket->CurrentUserHasRight('ModifyTicket') ) {
2554 if ( ( !RT->Config->Get( 'StrictLinkACL' ) && $right == 0 ) ||
2555 ( RT->Config->Get( 'StrictLinkACL' ) && $right < 2 ) )
2557 return ( 0, $self->loc("Permission Denied") );
2560 return ( 0, "Can't link to a deleted ticket" )
2561 if $other_ticket && $other_ticket->Status eq 'deleted';
2563 return $self->_AddLink(%args);
2566 sub __GetTicketFromURI {
2568 my %args = ( URI => '', @_ );
2570 # If the other URI is an RT::Ticket, we want to make sure the user
2571 # can modify it too...
2572 my $uri_obj = RT::URI->new( $self->CurrentUser );
2573 $uri_obj->FromURI( $args{'URI'} );
2575 unless ( $uri_obj->Resolver && $uri_obj->Scheme ) {
2576 my $msg = $self->loc( "Couldn't resolve '[_1]' into a URI.", $args{'URI'} );
2577 $RT::Logger->warning( $msg );
2580 my $obj = $uri_obj->Resolver->Object;
2581 unless ( UNIVERSAL::isa($obj, 'RT::Ticket') && $obj->id ) {
2582 return (1, 'Found not a ticket', undef);
2584 return (1, 'Found ticket', $obj);
2589 Private non-acled variant of AddLink so that links can be added during create.
2595 my %args = ( Target => '',
2599 SilentBase => undef,
2600 SilentTarget => undef,
2603 my ($val, $msg, $exist) = $self->SUPER::_AddLink(%args);
2604 return ($val, $msg) if !$val || $exist;
2605 return ($val, $msg) if $args{'Silent'};
2607 my ($direction, $remote_link);
2608 if ( $args{'Target'} ) {
2609 $remote_link = $args{'Target'};
2610 $direction = 'Base';
2611 } elsif ( $args{'Base'} ) {
2612 $remote_link = $args{'Base'};
2613 $direction = 'Target';
2616 my $remote_uri = RT::URI->new( $self->CurrentUser );
2617 $remote_uri->FromURI( $remote_link );
2619 unless ( $args{ 'Silent'. $direction } ) {
2620 my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction(
2622 Field => $LINKDIRMAP{$args{'Type'}}->{$direction},
2623 NewValue => $remote_uri->URI || $remote_link,
2626 $RT::Logger->error("Couldn't create transaction: $Msg") unless $Trans;
2629 if ( !$args{ 'Silent'. ( $direction eq 'Target'? 'Base': 'Target' ) } && $remote_uri->IsLocal ) {
2630 my $OtherObj = $remote_uri->Object;
2631 my ( $val, $msg ) = $OtherObj->_NewTransaction(
2633 Field => $direction eq 'Target' ? $LINKDIRMAP{$args{'Type'}}->{Base}
2634 : $LINKDIRMAP{$args{'Type'}}->{Target},
2635 NewValue => $self->URI,
2636 ActivateScrips => !RT->Config->Get('LinkTransactionsRun1Scrip'),
2639 $RT::Logger->error("Couldn't create transaction: $msg") unless $val;
2642 return ( $val, $msg );
2650 MergeInto take the id of the ticket to merge this ticket into.
2656 my $ticket_id = shift;
2658 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
2659 return ( 0, $self->loc("Permission Denied") );
2662 # Load up the new ticket.
2663 my $MergeInto = RT::Ticket->new($self->CurrentUser);
2664 $MergeInto->Load($ticket_id);
2666 # make sure it exists.
2667 unless ( $MergeInto->Id ) {
2668 return ( 0, $self->loc("New ticket doesn't exist") );
2671 # Make sure the current user can modify the new ticket.
2672 unless ( $MergeInto->CurrentUserHasRight('ModifyTicket') ) {
2673 return ( 0, $self->loc("Permission Denied") );
2676 delete $MERGE_CACHE{'effective'}{ $self->id };
2677 delete @{ $MERGE_CACHE{'merged'} }{
2678 $ticket_id, $MergeInto->id, $self->id
2681 $RT::Handle->BeginTransaction();
2683 $self->_MergeInto( $MergeInto );
2685 $RT::Handle->Commit();
2687 return ( 1, $self->loc("Merge Successful") );
2692 my $MergeInto = shift;
2695 # We use EffectiveId here even though it duplicates information from
2696 # the links table becasue of the massive performance hit we'd take
2697 # by trying to do a separate database query for merge info everytime
2700 #update this ticket's effective id to the new ticket's id.
2701 my ( $id_val, $id_msg ) = $self->__Set(
2702 Field => 'EffectiveId',
2703 Value => $MergeInto->Id()
2707 $RT::Handle->Rollback();
2708 return ( 0, $self->loc("Merge failed. Couldn't set EffectiveId") );
2712 my $force_status = $self->QueueObj->Lifecycle->DefaultOnMerge;
2713 if ( $force_status && $force_status ne $self->__Value('Status') ) {
2714 my ( $status_val, $status_msg )
2715 = $self->__Set( Field => 'Status', Value => $force_status );
2717 unless ($status_val) {
2718 $RT::Handle->Rollback();
2720 "Couldn't set status to $force_status. RT's Database may be inconsistent."
2722 return ( 0, $self->loc("Merge failed. Couldn't set Status") );
2726 # update all the links that point to that old ticket
2727 my $old_links_to = RT::Links->new($self->CurrentUser);
2728 $old_links_to->Limit(FIELD => 'Target', VALUE => $self->URI);
2731 while (my $link = $old_links_to->Next) {
2732 if (exists $old_seen{$link->Base."-".$link->Type}) {
2735 elsif ($link->Base eq $MergeInto->URI) {
2738 # First, make sure the link doesn't already exist. then move it over.
2739 my $tmp = RT::Link->new(RT->SystemUser);
2740 $tmp->LoadByCols(Base => $link->Base, Type => $link->Type, LocalTarget => $MergeInto->id);
2744 $link->SetTarget($MergeInto->URI);
2745 $link->SetLocalTarget($MergeInto->id);
2747 $old_seen{$link->Base."-".$link->Type} =1;
2752 my $old_links_from = RT::Links->new($self->CurrentUser);
2753 $old_links_from->Limit(FIELD => 'Base', VALUE => $self->URI);
2755 while (my $link = $old_links_from->Next) {
2756 if (exists $old_seen{$link->Type."-".$link->Target}) {
2759 if ($link->Target eq $MergeInto->URI) {
2762 # First, make sure the link doesn't already exist. then move it over.
2763 my $tmp = RT::Link->new(RT->SystemUser);
2764 $tmp->LoadByCols(Target => $link->Target, Type => $link->Type, LocalBase => $MergeInto->id);
2768 $link->SetBase($MergeInto->URI);
2769 $link->SetLocalBase($MergeInto->id);
2770 $old_seen{$link->Type."-".$link->Target} =1;
2776 # Update time fields
2777 foreach my $type (qw(TimeEstimated TimeWorked TimeLeft)) {
2779 my $mutator = "Set$type";
2780 $MergeInto->$mutator(
2781 ( $MergeInto->$type() || 0 ) + ( $self->$type() || 0 ) );
2784 #add all of this ticket's watchers to that ticket.
2785 foreach my $watcher_type (qw(Requestors Cc AdminCc)) {
2787 my $people = $self->$watcher_type->MembersObj;
2788 my $addwatcher_type = $watcher_type;
2789 $addwatcher_type =~ s/s$//;
2791 while ( my $watcher = $people->Next ) {
2793 my ($val, $msg) = $MergeInto->_AddWatcher(
2794 Type => $addwatcher_type,
2796 PrincipalId => $watcher->MemberId
2799 $RT::Logger->debug($msg);
2805 #find all of the tickets that were merged into this ticket.
2806 my $old_mergees = RT::Tickets->new( $self->CurrentUser );
2807 $old_mergees->Limit(
2808 FIELD => 'EffectiveId',
2813 # update their EffectiveId fields to the new ticket's id
2814 while ( my $ticket = $old_mergees->Next() ) {
2815 my ( $val, $msg ) = $ticket->__Set(
2816 Field => 'EffectiveId',
2817 Value => $MergeInto->Id()
2821 #make a new link: this ticket is merged into that other ticket.
2822 $self->AddLink( Type => 'MergedInto', Target => $MergeInto->Id());
2824 $MergeInto->_SetLastUpdated;
2829 Returns list of tickets' ids that's been merged into this ticket.
2837 return @{ $MERGE_CACHE{'merged'}{ $id } }
2838 if $MERGE_CACHE{'merged'}{ $id };
2840 my $mergees = RT::Tickets->new( $self->CurrentUser );
2842 FIELD => 'EffectiveId',
2850 return @{ $MERGE_CACHE{'merged'}{ $id } ||= [] }
2851 = map $_->id, @{ $mergees->ItemsArrayRef || [] };
2860 Takes nothing and returns an RT::User object of
2868 #If this gets ACLed, we lose on a rights check in User.pm and
2869 #get deep recursion. if we need ACLs here, we need
2870 #an equiv without ACLs
2872 my $owner = RT::User->new( $self->CurrentUser );
2873 $owner->Load( $self->__Value('Owner') );
2875 #Return the owner object
2881 =head2 OwnerAsString
2883 Returns the owner's email address
2889 return ( $self->OwnerObj->EmailAddress );
2897 Takes two arguments:
2898 the Id or Name of the owner
2899 and (optionally) the type of the SetOwner Transaction. It defaults
2900 to 'Set'. 'Steal' is also a valid option.
2907 my $NewOwner = shift;
2908 my $Type = shift || "Set";
2910 $RT::Handle->BeginTransaction();
2912 $self->_SetLastUpdated(); # lock the ticket
2913 $self->Load( $self->id ); # in case $self changed while waiting for lock
2915 my $OldOwnerObj = $self->OwnerObj;
2917 my $NewOwnerObj = RT::User->new( $self->CurrentUser );
2918 $NewOwnerObj->Load( $NewOwner );
2919 unless ( $NewOwnerObj->Id ) {
2920 $RT::Handle->Rollback();
2921 return ( 0, $self->loc("That user does not exist") );
2925 # must have ModifyTicket rights
2926 # or TakeTicket/StealTicket and $NewOwner is self
2927 # see if it's a take
2928 if ( $OldOwnerObj->Id == RT->Nobody->Id ) {
2929 unless ( $self->CurrentUserHasRight('ModifyTicket')
2930 || $self->CurrentUserHasRight('TakeTicket') ) {
2931 $RT::Handle->Rollback();
2932 return ( 0, $self->loc("Permission Denied") );
2936 # see if it's a steal
2937 elsif ( $OldOwnerObj->Id != RT->Nobody->Id
2938 && $OldOwnerObj->Id != $self->CurrentUser->id ) {
2940 unless ( $self->CurrentUserHasRight('ModifyTicket')
2941 || $self->CurrentUserHasRight('StealTicket') ) {
2942 $RT::Handle->Rollback();
2943 return ( 0, $self->loc("Permission Denied") );
2947 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
2948 $RT::Handle->Rollback();
2949 return ( 0, $self->loc("Permission Denied") );
2953 # If we're not stealing and the ticket has an owner and it's not
2955 if ( $Type ne 'Steal' and $Type ne 'Force'
2956 and $OldOwnerObj->Id != RT->Nobody->Id
2957 and $OldOwnerObj->Id != $self->CurrentUser->Id )
2959 $RT::Handle->Rollback();
2960 return ( 0, $self->loc("You can only take tickets that are unowned") )
2961 if $NewOwnerObj->id == $self->CurrentUser->id;
2964 $self->loc("You can only reassign tickets that you own or that are unowned" )
2968 #If we've specified a new owner and that user can't modify the ticket
2969 elsif ( !$NewOwnerObj->HasRight( Right => 'OwnTicket', Object => $self ) ) {
2970 $RT::Handle->Rollback();
2971 return ( 0, $self->loc("That user may not own tickets in that queue") );
2974 # If the ticket has an owner and it's the new owner, we don't need
2976 elsif ( $NewOwnerObj->Id == $OldOwnerObj->Id ) {
2977 $RT::Handle->Rollback();
2978 return ( 0, $self->loc("That user already owns that ticket") );
2981 # Delete the owner in the owner group, then add a new one
2982 # TODO: is this safe? it's not how we really want the API to work
2983 # for most things, but it's fast.
2984 my ( $del_id, $del_msg );
2985 for my $owner (@{$self->OwnerGroup->MembersObj->ItemsArrayRef}) {
2986 ($del_id, $del_msg) = $owner->Delete();
2987 last unless ($del_id);
2991 $RT::Handle->Rollback();
2992 return ( 0, $self->loc("Could not change owner: [_1]", $del_msg) );
2995 my ( $add_id, $add_msg ) = $self->OwnerGroup->_AddMember(
2996 PrincipalId => $NewOwnerObj->PrincipalId,
2997 InsideTransaction => 1 );
2999 $RT::Handle->Rollback();
3000 return ( 0, $self->loc("Could not change owner: [_1]", $add_msg ) );
3003 # We call set twice with slightly different arguments, so
3004 # as to not have an SQL transaction span two RT transactions
3006 my ( $val, $msg ) = $self->_Set(
3008 RecordTransaction => 0,
3009 Value => $NewOwnerObj->Id,
3011 TransactionType => 'Set',
3012 CheckACL => 0, # don't check acl
3016 $RT::Handle->Rollback;
3017 return ( 0, $self->loc("Could not change owner: [_1]", $msg) );
3020 ($val, $msg) = $self->_NewTransaction(
3023 NewValue => $NewOwnerObj->Id,
3024 OldValue => $OldOwnerObj->Id,
3029 $msg = $self->loc( "Owner changed from [_1] to [_2]",
3030 $OldOwnerObj->Name, $NewOwnerObj->Name );
3033 $RT::Handle->Rollback();
3037 $RT::Handle->Commit();
3039 return ( $val, $msg );
3046 A convenince method to set the ticket's owner to the current user
3052 return ( $self->SetOwner( $self->CurrentUser->Id, 'Take' ) );
3059 Convenience method to set the owner to 'nobody' if the current user is the owner.
3065 return ( $self->SetOwner( RT->Nobody->UserObj->Id, 'Untake' ) );
3072 A convenience method to change the owner of the current ticket to the
3073 current user. Even if it's owned by another user.
3080 if ( $self->IsOwner( $self->CurrentUser ) ) {
3081 return ( 0, $self->loc("You already own this ticket") );
3084 return ( $self->SetOwner( $self->CurrentUser->Id, 'Steal' ) );
3094 =head2 ValidateStatus STATUS
3096 Takes a string. Returns true if that status is a valid status for this ticket.
3097 Returns false otherwise.
3101 sub ValidateStatus {
3105 #Make sure the status passed in is valid
3106 return 1 if $self->QueueObj->IsValidStatus($status);
3109 while ( my $caller = (caller($i++))[3] ) {
3110 return 1 if $caller eq 'RT::Ticket::SetQueue';
3118 =head2 SetStatus STATUS
3120 Set this ticket\'s status. STATUS can be one of: new, open, stalled, resolved, rejected or deleted.
3122 Alternatively, you can pass in a list of named parameters (Status => STATUS, Force => FORCE, SetStarted => SETSTARTED ).
3123 If FORCE is true, ignore unresolved dependencies and force a status change.
3124 if SETSTARTED is true( it's the default value), set Started to current datetime if Started
3125 is not set and the status is changed from initial to not initial.
3133 $args{Status} = shift;
3139 # this only allows us to SetStarted, not we must SetStarted.
3140 # this option was added for rtir initially
3141 $args{SetStarted} = 1 unless exists $args{SetStarted};
3144 my $lifecycle = $self->QueueObj->Lifecycle;
3146 my $new = $args{'Status'};
3147 unless ( $lifecycle->IsValid( $new ) ) {
3148 return (0, $self->loc("Status '[_1]' isn't a valid status for tickets in this queue.", $self->loc($new)));
3151 my $old = $self->__Value('Status');
3152 unless ( $lifecycle->IsTransition( $old => $new ) ) {
3153 return (0, $self->loc("You can't change status from '[_1]' to '[_2]'.", $self->loc($old), $self->loc($new)));
3156 my $check_right = $lifecycle->CheckRight( $old => $new );
3157 unless ( $self->CurrentUserHasRight( $check_right ) ) {
3158 return ( 0, $self->loc('Permission Denied') );
3161 if ( !$args{Force} && $lifecycle->IsInactive( $new ) && $self->HasUnresolvedDependencies) {
3162 return (0, $self->loc('That ticket has unresolved dependencies'));
3165 my $now = RT::Date->new( $self->CurrentUser );
3168 my $raw_started = RT::Date->new(RT->SystemUser);
3169 $raw_started->Set(Format => 'ISO', Value => $self->__Value('Started'));
3171 #If we're changing the status from new, record that we've started
3172 if ( $args{SetStarted} && $lifecycle->IsInitial($old) && !$lifecycle->IsInitial($new) && !$raw_started->Unix) {
3173 #Set the Started time to "now"
3177 RecordTransaction => 0
3181 #When we close a ticket, set the 'Resolved' attribute to now.
3182 # It's misnamed, but that's just historical.
3183 if ( $lifecycle->IsInactive($new) ) {
3185 Field => 'Resolved',
3187 RecordTransaction => 0,
3191 #Actually update the status
3192 my ($val, $msg)= $self->_Set(
3194 Value => $args{Status},
3197 TransactionType => 'Status',
3199 return ($val, $msg);
3206 Takes no arguments. Marks this ticket for garbage collection
3212 unless ( $self->QueueObj->Lifecycle->IsValid('deleted') ) {
3213 return (0, $self->loc('Delete operation is disabled by lifecycle configuration') ); #loc
3215 return ( $self->SetStatus('deleted') );
3219 =head2 SetTold ISO [TIMETAKEN]
3221 Updates the told and records a transaction
3228 $told = shift if (@_);
3229 my $timetaken = shift || 0;
3231 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
3232 return ( 0, $self->loc("Permission Denied") );
3235 my $datetold = RT::Date->new( $self->CurrentUser );
3237 $datetold->Set( Format => 'iso',
3241 $datetold->SetToNow();
3244 return ( $self->_Set( Field => 'Told',
3245 Value => $datetold->ISO,
3246 TimeTaken => $timetaken,
3247 TransactionType => 'Told' ) );
3252 Updates the told without a transaction or acl check. Useful when we're sending replies.
3259 my $now = RT::Date->new( $self->CurrentUser );
3262 #use __Set to get no ACLs ;)
3263 return ( $self->__Set( Field => 'Told',
3264 Value => $now->ISO ) );
3274 my $uid = $self->CurrentUser->id;
3275 my $attr = $self->FirstAttribute( "User-". $uid ."-SeenUpTo" );
3276 return if $attr && $attr->Content gt $self->LastUpdated;
3278 my $txns = $self->Transactions;
3279 $txns->Limit( FIELD => 'Type', VALUE => 'Comment' );
3280 $txns->Limit( FIELD => 'Type', VALUE => 'Correspond' );
3281 $txns->Limit( FIELD => 'Creator', OPERATOR => '!=', VALUE => $uid );
3285 VALUE => $attr->Content
3287 $txns->RowsPerPage(1);
3288 return $txns->First;
3291 =head2 RanTransactionBatch
3293 Acts as a guard around running TransactionBatch scrips.
3295 Should be false until you enter the code that runs TransactionBatch scrips
3297 Accepts an optional argument to indicate that TransactionBatch Scrips should no longer be run on this object.
3301 sub RanTransactionBatch {
3305 if ( defined $val ) {
3306 return $self->{_RanTransactionBatch} = $val;
3308 return $self->{_RanTransactionBatch};
3314 =head2 TransactionBatch
3316 Returns an array reference of all transactions created on this ticket during
3317 this ticket object's lifetime or since last application of a batch, or undef
3320 Only works when the C<UseTransactionBatch> config option is set to true.
3324 sub TransactionBatch {
3326 return $self->{_TransactionBatch};
3329 =head2 ApplyTransactionBatch
3331 Applies scrips on the current batch of transactions and shinks it. Usually
3332 batch is applied when object is destroyed, but in some cases it's too late.
3336 sub ApplyTransactionBatch {
3339 my $batch = $self->TransactionBatch;
3340 return unless $batch && @$batch;
3342 $self->_ApplyTransactionBatch;
3344 $self->{_TransactionBatch} = [];
3347 sub _ApplyTransactionBatch {
3350 return if $self->RanTransactionBatch;
3351 $self->RanTransactionBatch(1);
3353 my $still_exists = RT::Ticket->new( RT->SystemUser );
3354 $still_exists->Load( $self->Id );
3355 if (not $still_exists->Id) {
3356 # The ticket has been removed from the database, but we still
3357 # have pending TransactionBatch txns for it. Unfortunately,
3358 # because it isn't in the DB anymore, attempting to run scrips
3359 # on it may produce unpredictable results; simply drop the
3360 # batched transactions.
3361 $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.");
3365 my $batch = $self->TransactionBatch;
3368 my $types = join ',', grep !$seen{$_}++, grep defined, map $_->__Value('Type'), grep defined, @{$batch};
3371 RT::Scrips->new(RT->SystemUser)->Apply(
3372 Stage => 'TransactionBatch',
3374 TransactionObj => $batch->[0],
3378 # Entry point of the rule system
3379 my $rules = RT::Ruleset->FindAllRules(
3380 Stage => 'TransactionBatch',
3382 TransactionObj => $batch->[0],
3385 RT::Ruleset->CommitRules($rules);
3391 # DESTROY methods need to localize $@, or it may unset it. This
3392 # causes $m->abort to not bubble all of the way up. See perlbug
3393 # http://rt.perl.org/rt3/Ticket/Display.html?id=17650
3396 # The following line eliminates reentrancy.
3397 # It protects against the fact that perl doesn't deal gracefully
3398 # when an object's refcount is changed in its destructor.
3399 return if $self->{_Destroyed}++;
3401 if (in_global_destruction()) {
3402 unless ($ENV{'HARNESS_ACTIVE'}) {
3403 warn "Too late to safely run transaction-batch scrips!"
3404 ." This is typically caused by using ticket objects"
3405 ." at the top-level of a script which uses the RT API."
3406 ." Be sure to explicitly undef such ticket objects,"
3407 ." or put them inside of a lexical scope.";
3412 return $self->ApplyTransactionBatch;
3418 sub _OverlayAccessible {
3420 EffectiveId => { 'read' => 1, 'write' => 1, 'public' => 1 },
3421 Queue => { 'read' => 1, 'write' => 1 },
3422 Requestors => { 'read' => 1, 'write' => 1 },
3423 Owner => { 'read' => 1, 'write' => 1 },
3424 Subject => { 'read' => 1, 'write' => 1 },
3425 InitialPriority => { 'read' => 1, 'write' => 1 },
3426 FinalPriority => { 'read' => 1, 'write' => 1 },
3427 Priority => { 'read' => 1, 'write' => 1 },
3428 Status => { 'read' => 1, 'write' => 1 },
3429 TimeEstimated => { 'read' => 1, 'write' => 1 },
3430 TimeWorked => { 'read' => 1, 'write' => 1 },
3431 TimeLeft => { 'read' => 1, 'write' => 1 },
3432 Told => { 'read' => 1, 'write' => 1 },
3433 Resolved => { 'read' => 1 },
3434 Type => { 'read' => 1 },
3435 Starts => { 'read' => 1, 'write' => 1 },
3436 Started => { 'read' => 1, 'write' => 1 },
3437 Due => { 'read' => 1, 'write' => 1 },
3438 Creator => { 'read' => 1, 'auto' => 1 },
3439 Created => { 'read' => 1, 'auto' => 1 },
3440 LastUpdatedBy => { 'read' => 1, 'auto' => 1 },
3441 LastUpdated => { 'read' => 1, 'auto' => 1 }
3451 my %args = ( Field => undef,
3454 RecordTransaction => 1,
3457 TransactionType => 'Set',
3460 if ($args{'CheckACL'}) {
3461 unless ( $self->CurrentUserHasRight('ModifyTicket')) {
3462 return ( 0, $self->loc("Permission Denied"));
3466 unless ($args{'UpdateTicket'} || $args{'RecordTransaction'}) {
3467 $RT::Logger->error("Ticket->_Set called without a mandate to record an update or update the ticket");
3468 return(0, $self->loc("Internal Error"));
3471 #if the user is trying to modify the record
3473 #Take care of the old value we really don't want to get in an ACL loop.
3474 # so ask the super::_Value
3475 my $Old = $self->SUPER::_Value("$args{'Field'}");
3478 if ( $args{'UpdateTicket'} ) {
3481 ( $ret, $msg ) = $self->SUPER::_Set( Field => $args{'Field'},
3482 Value => $args{'Value'} );
3484 #If we can't actually set the field to the value, don't record
3485 # a transaction. instead, get out of here.
3486 return ( 0, $msg ) unless $ret;
3489 if ( $args{'RecordTransaction'} == 1 ) {
3491 my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction(
3492 Type => $args{'TransactionType'},
3493 Field => $args{'Field'},
3494 NewValue => $args{'Value'},
3496 TimeTaken => $args{'TimeTaken'},
3498 return ( $Trans, scalar $TransObj->BriefDescription );
3501 return ( $ret, $msg );
3509 Takes the name of a table column.
3510 Returns its value as a string, if the user passes an ACL check
3519 #if the field is public, return it.
3520 if ( $self->_Accessible( $field, 'public' ) ) {
3522 #$RT::Logger->debug("Skipping ACL check for $field");
3523 return ( $self->SUPER::_Value($field) );
3527 #If the current user doesn't have ACLs, don't let em at it.
3529 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
3532 return ( $self->SUPER::_Value($field) );
3538 =head2 _UpdateTimeTaken
3540 This routine will increment the timeworked counter. it should
3541 only be called from _NewTransaction
3545 sub _UpdateTimeTaken {
3547 my $Minutes = shift;
3550 $Total = $self->SUPER::_Value("TimeWorked");
3551 $Total = ( $Total || 0 ) + ( $Minutes || 0 );
3553 Field => "TimeWorked",
3564 =head2 CurrentUserHasRight
3566 Takes the textual name of a Ticket scoped right (from RT::ACE) and returns
3567 1 if the user has that right. It returns 0 if the user doesn't have that right.
3571 sub CurrentUserHasRight {
3575 return $self->CurrentUser->PrincipalObj->HasRight(
3582 =head2 CurrentUserCanSee
3584 Returns true if the current user can see the ticket, using ShowTicket
3588 sub CurrentUserCanSee {
3590 return $self->CurrentUserHasRight('ShowTicket');
3595 Takes a paramhash with the attributes 'Right' and 'Principal'
3596 'Right' is a ticket-scoped textual right from RT::ACE
3597 'Principal' is an RT::User object
3599 Returns 1 if the principal has the right. Returns undef if not.
3611 unless ( ( defined $args{'Principal'} ) and ( ref( $args{'Principal'} ) ) )
3613 Carp::cluck("Principal attrib undefined for Ticket::HasRight");
3614 $RT::Logger->crit("Principal attrib undefined for Ticket::HasRight");
3619 $args{'Principal'}->HasRight(
3621 Right => $args{'Right'}
3630 Return the Reminders object for this ticket. (It's an RT::Reminders object.)
3631 It isn't acutally a searchbuilder collection itself.
3638 unless ($self->{'__reminders'}) {
3639 $self->{'__reminders'} = RT::Reminders->new($self->CurrentUser);
3640 $self->{'__reminders'}->Ticket($self->id);
3642 return $self->{'__reminders'};
3651 Returns an RT::Transactions object of all transactions on this ticket
3658 my $transactions = RT::Transactions->new( $self->CurrentUser );
3660 #If the user has no rights, return an empty object
3661 if ( $self->CurrentUserHasRight('ShowTicket') ) {
3662 $transactions->LimitToTicket($self->id);
3664 # if the user may not see comments do not return them
3665 unless ( $self->CurrentUserHasRight('ShowTicketComments') ) {
3666 $transactions->Limit(
3672 $transactions->Limit(
3676 VALUE => "CommentEmailRecord",
3677 ENTRYAGGREGATOR => 'AND'
3682 $transactions->Limit(
3686 ENTRYAGGREGATOR => 'AND'
3690 return ($transactions);
3696 =head2 TransactionCustomFields
3698 Returns the custom fields that transactions on tickets will have.
3702 sub TransactionCustomFields {
3704 my $cfs = $self->QueueObj->TicketTransactionCustomFields;
3705 $cfs->SetContextObject( $self );
3711 =head2 CustomFieldValues
3713 # Do name => id mapping (if needed) before falling back to
3714 # RT::Record's CustomFieldValues
3720 sub CustomFieldValues {
3724 return $self->SUPER::CustomFieldValues( $field ) if !$field || $field =~ /^\d+$/;
3726 my $cf = RT::CustomField->new( $self->CurrentUser );
3727 $cf->SetContextObject( $self );
3728 $cf->LoadByNameAndQueue( Name => $field, Queue => $self->Queue );
3729 unless ( $cf->id ) {
3730 $cf->LoadByNameAndQueue( Name => $field, Queue => 0 );
3733 # If we didn't find a valid cfid, give up.
3734 return RT::ObjectCustomFieldValues->new( $self->CurrentUser ) unless $cf->id;
3736 return $self->SUPER::CustomFieldValues( $cf->id );
3741 =head2 CustomFieldLookupType
3743 Returns the RT::Ticket lookup type, which can be passed to
3744 RT::CustomField->Create() via the 'LookupType' hash key.
3749 sub CustomFieldLookupType {
3750 "RT::Queue-RT::Ticket";
3753 =head2 ACLEquivalenceObjects
3755 This method returns a list of objects for which a user's rights also apply
3756 to this ticket. Generally, this is only the ticket's queue, but some RT
3757 extensions may make other objects available too.
3759 This method is called from L<RT::Principal/HasRight>.
3763 sub ACLEquivalenceObjects {
3765 return $self->QueueObj;
3774 Jesse Vincent, jesse@bestpractical.com
3784 use base 'RT::Record';
3786 sub Table {'Tickets'}
3795 Returns the current value of id.
3796 (In the database, id is stored as int(11).)
3804 Returns the current value of EffectiveId.
3805 (In the database, EffectiveId is stored as int(11).)
3809 =head2 SetEffectiveId VALUE
3812 Set EffectiveId to VALUE.
3813 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
3814 (In the database, EffectiveId will be stored as a int(11).)
3822 Returns the current value of Queue.
3823 (In the database, Queue is stored as int(11).)
3827 =head2 SetQueue VALUE
3831 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
3832 (In the database, Queue will be stored as a int(11).)
3840 Returns the current value of Type.
3841 (In the database, Type is stored as varchar(16).)
3845 =head2 SetType VALUE
3849 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
3850 (In the database, Type will be stored as a varchar(16).)
3856 =head2 IssueStatement
3858 Returns the current value of IssueStatement.
3859 (In the database, IssueStatement is stored as int(11).)
3863 =head2 SetIssueStatement VALUE
3866 Set IssueStatement to VALUE.
3867 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
3868 (In the database, IssueStatement will be stored as a int(11).)
3876 Returns the current value of Resolution.
3877 (In the database, Resolution is stored as int(11).)
3881 =head2 SetResolution VALUE
3884 Set Resolution to VALUE.
3885 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
3886 (In the database, Resolution will be stored as a int(11).)
3894 Returns the current value of Owner.
3895 (In the database, Owner is stored as int(11).)
3899 =head2 SetOwner VALUE
3903 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
3904 (In the database, Owner will be stored as a int(11).)
3912 Returns the current value of Subject.
3913 (In the database, Subject is stored as varchar(200).)
3917 =head2 SetSubject VALUE
3920 Set Subject to VALUE.
3921 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
3922 (In the database, Subject will be stored as a varchar(200).)
3928 =head2 InitialPriority
3930 Returns the current value of InitialPriority.
3931 (In the database, InitialPriority is stored as int(11).)
3935 =head2 SetInitialPriority VALUE
3938 Set InitialPriority to VALUE.
3939 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
3940 (In the database, InitialPriority will be stored as a int(11).)
3946 =head2 FinalPriority
3948 Returns the current value of FinalPriority.
3949 (In the database, FinalPriority is stored as int(11).)
3953 =head2 SetFinalPriority VALUE
3956 Set FinalPriority to VALUE.
3957 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
3958 (In the database, FinalPriority will be stored as a int(11).)
3966 Returns the current value of Priority.
3967 (In the database, Priority is stored as int(11).)
3971 =head2 SetPriority VALUE
3974 Set Priority to VALUE.
3975 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
3976 (In the database, Priority will be stored as a int(11).)
3982 =head2 TimeEstimated
3984 Returns the current value of TimeEstimated.
3985 (In the database, TimeEstimated is stored as int(11).)
3989 =head2 SetTimeEstimated VALUE
3992 Set TimeEstimated to VALUE.
3993 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
3994 (In the database, TimeEstimated will be stored as a int(11).)
4002 Returns the current value of TimeWorked.
4003 (In the database, TimeWorked is stored as int(11).)
4007 =head2 SetTimeWorked VALUE
4010 Set TimeWorked to VALUE.
4011 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
4012 (In the database, TimeWorked will be stored as a int(11).)
4020 Returns the current value of Status.
4021 (In the database, Status is stored as varchar(64).)
4025 =head2 SetStatus VALUE
4028 Set Status to VALUE.
4029 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
4030 (In the database, Status will be stored as a varchar(64).)
4038 Returns the current value of TimeLeft.
4039 (In the database, TimeLeft is stored as int(11).)
4043 =head2 SetTimeLeft VALUE
4046 Set TimeLeft to VALUE.
4047 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
4048 (In the database, TimeLeft will be stored as a int(11).)
4056 Returns the current value of Told.
4057 (In the database, Told is stored as datetime.)
4061 =head2 SetTold VALUE
4065 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
4066 (In the database, Told will be stored as a datetime.)
4074 Returns the current value of Starts.
4075 (In the database, Starts is stored as datetime.)
4079 =head2 SetStarts VALUE
4082 Set Starts to VALUE.
4083 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
4084 (In the database, Starts will be stored as a datetime.)
4092 Returns the current value of Started.
4093 (In the database, Started is stored as datetime.)
4097 =head2 SetStarted VALUE
4100 Set Started to VALUE.
4101 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
4102 (In the database, Started will be stored as a datetime.)
4110 Returns the current value of Due.
4111 (In the database, Due is stored as datetime.)
4119 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
4120 (In the database, Due will be stored as a datetime.)
4128 Returns the current value of Resolved.
4129 (In the database, Resolved is stored as datetime.)
4133 =head2 SetResolved VALUE
4136 Set Resolved to VALUE.
4137 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
4138 (In the database, Resolved will be stored as a datetime.)
4144 =head2 LastUpdatedBy
4146 Returns the current value of LastUpdatedBy.
4147 (In the database, LastUpdatedBy is stored as int(11).)
4155 Returns the current value of LastUpdated.
4156 (In the database, LastUpdated is stored as datetime.)
4164 Returns the current value of Creator.
4165 (In the database, Creator is stored as int(11).)
4173 Returns the current value of Created.
4174 (In the database, Created is stored as datetime.)
4182 Returns the current value of Disabled.
4183 (In the database, Disabled is stored as smallint(6).)
4187 =head2 SetDisabled VALUE
4190 Set Disabled to VALUE.
4191 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
4192 (In the database, Disabled will be stored as a smallint(6).)
4199 sub _CoreAccessible {
4203 {read => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => ''},
4205 {read => 1, write => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'},
4207 {read => 1, write => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'},
4209 {read => 1, write => 1, sql_type => 12, length => 16, is_blob => 0, is_numeric => 0, type => 'varchar(16)', default => ''},
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 => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'},
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 => 12, length => 200, is_blob => 0, is_numeric => 0, type => 'varchar(200)', default => '[no subject]'},
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 => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'},
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 => 12, length => 64, is_blob => 0, is_numeric => 0, type => 'varchar(64)', default => ''},
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 => 11, length => 0, is_blob => 0, is_numeric => 0, type => 'datetime', default => ''},
4235 {read => 1, write => 1, sql_type => 11, length => 0, is_blob => 0, is_numeric => 0, type => 'datetime', default => ''},
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, auto => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'},
4245 {read => 1, auto => 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, write => 1, sql_type => 5, length => 6, is_blob => 0, is_numeric => 1, type => 'smallint(6)', default => '0'},
4256 RT::Base->_ImportOverlays();