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->UserObj->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->warn("$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);
1915 Takes a date in ISO format or undef
1916 Returns a transaction id and a message
1917 The client calls "Start" to note that the project was started on the date in $date.
1918 A null date means "now"
1924 my $time = shift || 0;
1926 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
1927 return ( 0, $self->loc("Permission Denied") );
1930 #We create a date object to catch date weirdness
1931 my $time_obj = RT::Date->new( $self->CurrentUser() );
1933 $time_obj->Set( Format => 'ISO', Value => $time );
1936 $time_obj->SetToNow();
1939 # We need $TicketAsSystem, in case the current user doesn't have
1941 my $TicketAsSystem = RT::Ticket->new(RT->SystemUser);
1942 $TicketAsSystem->Load( $self->Id );
1943 # Now that we're starting, open this ticket
1944 # TODO: do we really want to force this as policy? it should be a scrip
1945 my $next = $TicketAsSystem->FirstActiveStatus;
1947 $self->SetStatus( $next ) if defined $next;
1949 return ( $self->_Set( Field => 'Started', Value => $time_obj->ISO ) );
1957 Returns an RT::Date object which contains this ticket's
1965 my $time = RT::Date->new( $self->CurrentUser );
1966 $time->Set( Format => 'sql', Value => $self->Started );
1974 Returns an RT::Date object which contains this ticket's
1982 my $time = RT::Date->new( $self->CurrentUser );
1983 $time->Set( Format => 'sql', Value => $self->Starts );
1991 Returns an RT::Date object which contains this ticket's
1999 my $time = RT::Date->new( $self->CurrentUser );
2000 $time->Set( Format => 'sql', Value => $self->Told );
2008 A convenience method that returns ToldObj->AsString
2010 TODO: This should be deprecated
2016 if ( $self->Told ) {
2017 return $self->ToldObj->AsString();
2026 =head2 TimeWorkedAsString
2028 Returns the amount of time worked on this ticket as a Text String
2032 sub TimeWorkedAsString {
2034 my $value = $self->TimeWorked;
2036 # return the # of minutes worked turned into seconds and written as
2037 # a simple text string, this is not really a date object, but if we
2038 # diff a number of seconds vs the epoch, we'll get a nice description
2040 return "" unless $value;
2041 return RT::Date->new( $self->CurrentUser )
2042 ->DurationAsString( $value * 60 );
2047 =head2 TimeLeftAsString
2049 Returns the amount of time left on this ticket as a Text String
2053 sub TimeLeftAsString {
2055 my $value = $self->TimeLeft;
2056 return "" unless $value;
2057 return RT::Date->new( $self->CurrentUser )
2058 ->DurationAsString( $value * 60 );
2066 Comment on this ticket.
2067 Takes a hash with the following attributes:
2068 If MIMEObj is undefined, Content will be used to build a MIME::Entity for this
2071 MIMEObj, TimeTaken, CcMessageTo, BccMessageTo, Content, DryRun
2073 If DryRun is defined, this update WILL NOT BE RECORDED. Scrips will not be committed.
2074 They will, however, be prepared and you'll be able to access them through the TransactionObj
2076 Returns: Transaction id, Error Message, Transaction Object
2077 (note the different order from Create()!)
2084 my %args = ( CcMessageTo => undef,
2085 BccMessageTo => undef,
2092 unless ( ( $self->CurrentUserHasRight('CommentOnTicket') )
2093 or ( $self->CurrentUserHasRight('ModifyTicket') ) ) {
2094 return ( 0, $self->loc("Permission Denied"), undef );
2096 $args{'NoteType'} = 'Comment';
2098 if ($args{'DryRun'}) {
2099 $RT::Handle->BeginTransaction();
2100 $args{'CommitScrips'} = 0;
2103 my @results = $self->_RecordNote(%args);
2104 if ($args{'DryRun'}) {
2105 $RT::Handle->Rollback();
2114 Correspond on this ticket.
2115 Takes a hashref with the following attributes:
2118 MIMEObj, TimeTaken, CcMessageTo, BccMessageTo, Content, DryRun
2120 if there's no MIMEObj, Content is used to build a MIME::Entity object
2122 If DryRun is defined, this update WILL NOT BE RECORDED. Scrips will not be committed.
2123 They will, however, be prepared and you'll be able to access them through the TransactionObj
2125 Returns: Transaction id, Error Message, Transaction Object
2126 (note the different order from Create()!)
2133 my %args = ( CcMessageTo => undef,
2134 BccMessageTo => undef,
2140 unless ( ( $self->CurrentUserHasRight('ReplyToTicket') )
2141 or ( $self->CurrentUserHasRight('ModifyTicket') ) ) {
2142 return ( 0, $self->loc("Permission Denied"), undef );
2145 $args{'NoteType'} = 'Correspond';
2146 if ($args{'DryRun'}) {
2147 $RT::Handle->BeginTransaction();
2148 $args{'CommitScrips'} = 0;
2151 my @results = $self->_RecordNote(%args);
2153 #Set the last told date to now if this isn't mail from the requestor.
2154 #TODO: Note that this will wrongly ack mail from any non-requestor as a "told"
2155 unless ( $self->IsRequestor($self->CurrentUser->id) ) {
2157 $squelch{$_}++ for map {$_->Content} $self->SquelchMailTo, $results[2]->SquelchMailTo;
2159 if grep {not $squelch{$_}} $self->Requestors->MemberEmailAddresses;
2162 if ($args{'DryRun'}) {
2163 $RT::Handle->Rollback();
2174 the meat of both comment and correspond.
2176 Performs no access control checks. hence, dangerous.
2183 CcMessageTo => undef,
2184 BccMessageTo => undef,
2189 NoteType => 'Correspond',
2192 SquelchMailTo => undef,
2196 unless ( $args{'MIMEObj'} || $args{'Content'} ) {
2197 return ( 0, $self->loc("No message attached"), undef );
2200 unless ( $args{'MIMEObj'} ) {
2201 $args{'MIMEObj'} = MIME::Entity->build(
2202 Data => ( ref $args{'Content'}? $args{'Content'}: [ $args{'Content'} ] )
2206 # convert text parts into utf-8
2207 RT::I18N::SetMIMEEntityToUTF8( $args{'MIMEObj'} );
2209 # If we've been passed in CcMessageTo and BccMessageTo fields,
2210 # add them to the mime object for passing on to the transaction handler
2211 # The "NotifyOtherRecipients" scripAction will look for RT-Send-Cc: and
2212 # RT-Send-Bcc: headers
2215 foreach my $type (qw/Cc Bcc/) {
2216 if ( defined $args{ $type . 'MessageTo' } ) {
2218 my $addresses = join ', ', (
2219 map { RT::User->CanonicalizeEmailAddress( $_->address ) }
2220 Email::Address->parse( $args{ $type . 'MessageTo' } ) );
2221 $args{'MIMEObj'}->head->replace( 'RT-Send-' . $type, Encode::encode_utf8( $addresses ) );
2225 foreach my $argument (qw(Encrypt Sign)) {
2226 $args{'MIMEObj'}->head->replace(
2227 "X-RT-$argument" => Encode::encode_utf8( $args{ $argument } )
2228 ) if defined $args{ $argument };
2231 # If this is from an external source, we need to come up with its
2232 # internal Message-ID now, so all emails sent because of this
2233 # message have a common Message-ID
2234 my $org = RT->Config->Get('Organization');
2235 my $msgid = $args{'MIMEObj'}->head->get('Message-ID');
2236 unless (defined $msgid && $msgid =~ /<(rt-.*?-\d+-\d+)\.(\d+-0-0)\@\Q$org\E>/) {
2237 $args{'MIMEObj'}->head->set(
2238 'RT-Message-ID' => RT::Interface::Email::GenMessageId( Ticket => $self )
2242 #Record the correspondence (write the transaction)
2243 my ( $Trans, $msg, $TransObj ) = $self->_NewTransaction(
2244 Type => $args{'NoteType'},
2245 Data => ( $args{'MIMEObj'}->head->get('subject') || 'No Subject' ),
2246 TimeTaken => $args{'TimeTaken'},
2247 MIMEObj => $args{'MIMEObj'},
2248 CommitScrips => $args{'CommitScrips'},
2249 SquelchMailTo => $args{'SquelchMailTo'},
2253 $RT::Logger->err("$self couldn't init a transaction $msg");
2254 return ( $Trans, $self->loc("Message could not be recorded"), undef );
2257 return ( $Trans, $self->loc("Message recorded"), $TransObj );
2263 Builds a MIME object from the given C<UpdateSubject> and
2264 C<UpdateContent>, then calls L</Comment> or L</Correspond> with
2265 C<< DryRun => 1 >>, and returns the transaction so produced.
2273 if (($args{'UpdateType'} || $args{Action}) =~ /^respon(d|se)$/i ) {
2274 $action = 'Correspond';
2276 $action = 'Comment';
2279 my $Message = MIME::Entity->build(
2280 Type => 'text/plain',
2281 Subject => defined $args{UpdateSubject} ? Encode::encode_utf8( $args{UpdateSubject} ) : "",
2283 Data => $args{'UpdateContent'} || "",
2286 my ( $Transaction, $Description, $Object ) = $self->$action(
2287 CcMessageTo => $args{'UpdateCc'},
2288 BccMessageTo => $args{'UpdateBcc'},
2289 MIMEObj => $Message,
2290 TimeTaken => $args{'UpdateTimeWorked'},
2293 unless ( $Transaction ) {
2294 $RT::Logger->error("Couldn't fire '$action' action: $Description");
2302 Prepares a MIME mesage with the given C<Subject>, C<Cc>, and
2303 C<Content>, then calls L</Create> with C<< DryRun => 1 >> and returns
2304 the resulting L<RT::Transaction>.
2311 my $Message = MIME::Entity->build(
2312 Type => 'text/plain',
2313 Subject => defined $args{Subject} ? Encode::encode_utf8( $args{'Subject'} ) : "",
2314 (defined $args{'Cc'} ?
2315 ( Cc => Encode::encode_utf8( $args{'Cc'} ) ) : ()),
2317 Data => $args{'Content'} || "",
2320 my ( $Transaction, $Object, $Description ) = $self->Create(
2321 Type => $args{'Type'} || 'ticket',
2322 Queue => $args{'Queue'},
2323 Owner => $args{'Owner'},
2324 Requestor => $args{'Requestors'},
2326 AdminCc => $args{'AdminCc'},
2327 InitialPriority => $args{'InitialPriority'},
2328 FinalPriority => $args{'FinalPriority'},
2329 TimeLeft => $args{'TimeLeft'},
2330 TimeEstimated => $args{'TimeEstimated'},
2331 TimeWorked => $args{'TimeWorked'},
2332 Subject => $args{'Subject'},
2333 Status => $args{'Status'},
2334 MIMEObj => $Message,
2337 unless ( $Transaction ) {
2338 $RT::Logger->error("Couldn't fire Create action: $Description");
2349 #TODO: Field isn't the right thing here. but I ahave no idea what mnemonic ---
2352 my $type = shift || "";
2354 my $cache_key = "$field$type";
2355 return $self->{ $cache_key } if $self->{ $cache_key };
2357 my $links = $self->{ $cache_key }
2358 = RT::Links->new( $self->CurrentUser );
2359 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
2360 $links->Limit( FIELD => 'id', VALUE => 0, SUBCLAUSE => 'acl' );
2364 # Maybe this ticket is a merge ticket
2365 my $limit_on = 'Local'. $field;
2366 # at least to myself
2370 ENTRYAGGREGATOR => 'OR',
2375 ENTRYAGGREGATOR => 'OR',
2376 ) foreach $self->Merged;
2389 Delete a link. takes a paramhash of Base, Target, Type, Silent,
2390 SilentBase and SilentTarget. Either Base or Target must be null.
2391 The null value will be replaced with this ticket\'s id.
2393 If Silent is true then no transaction would be recorded, in other
2394 case you can control creation of transactions on both base and
2395 target with SilentBase and SilentTarget respectively. By default
2396 both transactions are created.
2407 SilentBase => undef,
2408 SilentTarget => undef,
2412 unless ( $args{'Target'} || $args{'Base'} ) {
2413 $RT::Logger->error("Base or Target must be specified");
2414 return ( 0, $self->loc('Either base or target must be specified') );
2419 $right++ if $self->CurrentUserHasRight('ModifyTicket');
2420 if ( !$right && RT->Config->Get( 'StrictLinkACL' ) ) {
2421 return ( 0, $self->loc("Permission Denied") );
2424 # If the other URI is an RT::Ticket, we want to make sure the user
2425 # can modify it too...
2426 my ($status, $msg, $other_ticket) = $self->__GetTicketFromURI( URI => $args{'Target'} || $args{'Base'} );
2427 return (0, $msg) unless $status;
2428 if ( !$other_ticket || $other_ticket->CurrentUserHasRight('ModifyTicket') ) {
2431 if ( ( !RT->Config->Get( 'StrictLinkACL' ) && $right == 0 ) ||
2432 ( RT->Config->Get( 'StrictLinkACL' ) && $right < 2 ) )
2434 return ( 0, $self->loc("Permission Denied") );
2437 my ($val, $Msg) = $self->SUPER::_DeleteLink(%args);
2438 return ( 0, $Msg ) unless $val;
2440 return ( $val, $Msg ) if $args{'Silent'};
2442 my ($direction, $remote_link);
2444 if ( $args{'Base'} ) {
2445 $remote_link = $args{'Base'};
2446 $direction = 'Target';
2448 elsif ( $args{'Target'} ) {
2449 $remote_link = $args{'Target'};
2450 $direction = 'Base';
2453 my $remote_uri = RT::URI->new( $self->CurrentUser );
2454 $remote_uri->FromURI( $remote_link );
2456 unless ( $args{ 'Silent'. $direction } ) {
2457 my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction(
2458 Type => 'DeleteLink',
2459 Field => $LINKDIRMAP{$args{'Type'}}->{$direction},
2460 OldValue => $remote_uri->URI || $remote_link,
2463 $RT::Logger->error("Couldn't create transaction: $Msg") unless $Trans;
2466 if ( !$args{ 'Silent'. ( $direction eq 'Target'? 'Base': 'Target' ) } && $remote_uri->IsLocal ) {
2467 my $OtherObj = $remote_uri->Object;
2468 my ( $val, $Msg ) = $OtherObj->_NewTransaction(
2469 Type => 'DeleteLink',
2470 Field => $direction eq 'Target' ? $LINKDIRMAP{$args{'Type'}}->{Base}
2471 : $LINKDIRMAP{$args{'Type'}}->{Target},
2472 OldValue => $self->URI,
2473 ActivateScrips => !RT->Config->Get('LinkTransactionsRun1Scrip'),
2476 $RT::Logger->error("Couldn't create transaction: $Msg") unless $val;
2479 return ( $val, $Msg );
2486 Takes a paramhash of Type and one of Base or Target. Adds that link to this ticket.
2488 If Silent is true then no transaction would be recorded, in other
2489 case you can control creation of transactions on both base and
2490 target with SilentBase and SilentTarget respectively. By default
2491 both transactions are created.
2497 my %args = ( Target => '',
2501 SilentBase => undef,
2502 SilentTarget => undef,
2505 unless ( $args{'Target'} || $args{'Base'} ) {
2506 $RT::Logger->error("Base or Target must be specified");
2507 return ( 0, $self->loc('Either base or target must be specified') );
2511 $right++ if $self->CurrentUserHasRight('ModifyTicket');
2512 if ( !$right && RT->Config->Get( 'StrictLinkACL' ) ) {
2513 return ( 0, $self->loc("Permission Denied") );
2516 # If the other URI is an RT::Ticket, we want to make sure the user
2517 # can modify it too...
2518 my ($status, $msg, $other_ticket) = $self->__GetTicketFromURI( URI => $args{'Target'} || $args{'Base'} );
2519 return (0, $msg) unless $status;
2520 if ( !$other_ticket || $other_ticket->CurrentUserHasRight('ModifyTicket') ) {
2523 if ( ( !RT->Config->Get( 'StrictLinkACL' ) && $right == 0 ) ||
2524 ( RT->Config->Get( 'StrictLinkACL' ) && $right < 2 ) )
2526 return ( 0, $self->loc("Permission Denied") );
2529 return ( 0, "Can't link to a deleted ticket" )
2530 if $other_ticket && $other_ticket->Status eq 'deleted';
2532 return $self->_AddLink(%args);
2535 sub __GetTicketFromURI {
2537 my %args = ( URI => '', @_ );
2539 # If the other URI is an RT::Ticket, we want to make sure the user
2540 # can modify it too...
2541 my $uri_obj = RT::URI->new( $self->CurrentUser );
2542 $uri_obj->FromURI( $args{'URI'} );
2544 unless ( $uri_obj->Resolver && $uri_obj->Scheme ) {
2545 my $msg = $self->loc( "Couldn't resolve '[_1]' into a URI.", $args{'URI'} );
2546 $RT::Logger->warning( $msg );
2549 my $obj = $uri_obj->Resolver->Object;
2550 unless ( UNIVERSAL::isa($obj, 'RT::Ticket') && $obj->id ) {
2551 return (1, 'Found not a ticket', undef);
2553 return (1, 'Found ticket', $obj);
2558 Private non-acled variant of AddLink so that links can be added during create.
2564 my %args = ( Target => '',
2568 SilentBase => undef,
2569 SilentTarget => undef,
2572 my ($val, $msg, $exist) = $self->SUPER::_AddLink(%args);
2573 return ($val, $msg) if !$val || $exist;
2574 return ($val, $msg) if $args{'Silent'};
2576 my ($direction, $remote_link);
2577 if ( $args{'Target'} ) {
2578 $remote_link = $args{'Target'};
2579 $direction = 'Base';
2580 } elsif ( $args{'Base'} ) {
2581 $remote_link = $args{'Base'};
2582 $direction = 'Target';
2585 my $remote_uri = RT::URI->new( $self->CurrentUser );
2586 $remote_uri->FromURI( $remote_link );
2588 unless ( $args{ 'Silent'. $direction } ) {
2589 my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction(
2591 Field => $LINKDIRMAP{$args{'Type'}}->{$direction},
2592 NewValue => $remote_uri->URI || $remote_link,
2595 $RT::Logger->error("Couldn't create transaction: $Msg") unless $Trans;
2598 if ( !$args{ 'Silent'. ( $direction eq 'Target'? 'Base': 'Target' ) } && $remote_uri->IsLocal ) {
2599 my $OtherObj = $remote_uri->Object;
2600 my ( $val, $msg ) = $OtherObj->_NewTransaction(
2602 Field => $direction eq 'Target' ? $LINKDIRMAP{$args{'Type'}}->{Base}
2603 : $LINKDIRMAP{$args{'Type'}}->{Target},
2604 NewValue => $self->URI,
2605 ActivateScrips => !RT->Config->Get('LinkTransactionsRun1Scrip'),
2608 $RT::Logger->error("Couldn't create transaction: $msg") unless $val;
2611 return ( $val, $msg );
2619 MergeInto take the id of the ticket to merge this ticket into.
2625 my $ticket_id = shift;
2627 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
2628 return ( 0, $self->loc("Permission Denied") );
2631 # Load up the new ticket.
2632 my $MergeInto = RT::Ticket->new($self->CurrentUser);
2633 $MergeInto->Load($ticket_id);
2635 # make sure it exists.
2636 unless ( $MergeInto->Id ) {
2637 return ( 0, $self->loc("New ticket doesn't exist") );
2640 # Make sure the current user can modify the new ticket.
2641 unless ( $MergeInto->CurrentUserHasRight('ModifyTicket') ) {
2642 return ( 0, $self->loc("Permission Denied") );
2645 delete $MERGE_CACHE{'effective'}{ $self->id };
2646 delete @{ $MERGE_CACHE{'merged'} }{
2647 $ticket_id, $MergeInto->id, $self->id
2650 $RT::Handle->BeginTransaction();
2652 $self->_MergeInto( $MergeInto );
2654 $RT::Handle->Commit();
2656 return ( 1, $self->loc("Merge Successful") );
2661 my $MergeInto = shift;
2664 # We use EffectiveId here even though it duplicates information from
2665 # the links table becasue of the massive performance hit we'd take
2666 # by trying to do a separate database query for merge info everytime
2669 #update this ticket's effective id to the new ticket's id.
2670 my ( $id_val, $id_msg ) = $self->__Set(
2671 Field => 'EffectiveId',
2672 Value => $MergeInto->Id()
2676 $RT::Handle->Rollback();
2677 return ( 0, $self->loc("Merge failed. Couldn't set EffectiveId") );
2681 my $force_status = $self->QueueObj->Lifecycle->DefaultOnMerge;
2682 if ( $force_status && $force_status ne $self->__Value('Status') ) {
2683 my ( $status_val, $status_msg )
2684 = $self->__Set( Field => 'Status', Value => $force_status );
2686 unless ($status_val) {
2687 $RT::Handle->Rollback();
2689 "Couldn't set status to $force_status. RT's Database may be inconsistent."
2691 return ( 0, $self->loc("Merge failed. Couldn't set Status") );
2695 # update all the links that point to that old ticket
2696 my $old_links_to = RT::Links->new($self->CurrentUser);
2697 $old_links_to->Limit(FIELD => 'Target', VALUE => $self->URI);
2700 while (my $link = $old_links_to->Next) {
2701 if (exists $old_seen{$link->Base."-".$link->Type}) {
2704 elsif ($link->Base eq $MergeInto->URI) {
2707 # First, make sure the link doesn't already exist. then move it over.
2708 my $tmp = RT::Link->new(RT->SystemUser);
2709 $tmp->LoadByCols(Base => $link->Base, Type => $link->Type, LocalTarget => $MergeInto->id);
2713 $link->SetTarget($MergeInto->URI);
2714 $link->SetLocalTarget($MergeInto->id);
2716 $old_seen{$link->Base."-".$link->Type} =1;
2721 my $old_links_from = RT::Links->new($self->CurrentUser);
2722 $old_links_from->Limit(FIELD => 'Base', VALUE => $self->URI);
2724 while (my $link = $old_links_from->Next) {
2725 if (exists $old_seen{$link->Type."-".$link->Target}) {
2728 if ($link->Target eq $MergeInto->URI) {
2731 # First, make sure the link doesn't already exist. then move it over.
2732 my $tmp = RT::Link->new(RT->SystemUser);
2733 $tmp->LoadByCols(Target => $link->Target, Type => $link->Type, LocalBase => $MergeInto->id);
2737 $link->SetBase($MergeInto->URI);
2738 $link->SetLocalBase($MergeInto->id);
2739 $old_seen{$link->Type."-".$link->Target} =1;
2745 # Update time fields
2746 foreach my $type (qw(TimeEstimated TimeWorked TimeLeft)) {
2748 my $mutator = "Set$type";
2749 $MergeInto->$mutator(
2750 ( $MergeInto->$type() || 0 ) + ( $self->$type() || 0 ) );
2753 #add all of this ticket's watchers to that ticket.
2754 foreach my $watcher_type (qw(Requestors Cc AdminCc)) {
2756 my $people = $self->$watcher_type->MembersObj;
2757 my $addwatcher_type = $watcher_type;
2758 $addwatcher_type =~ s/s$//;
2760 while ( my $watcher = $people->Next ) {
2762 my ($val, $msg) = $MergeInto->_AddWatcher(
2763 Type => $addwatcher_type,
2765 PrincipalId => $watcher->MemberId
2768 $RT::Logger->debug($msg);
2774 #find all of the tickets that were merged into this ticket.
2775 my $old_mergees = RT::Tickets->new( $self->CurrentUser );
2776 $old_mergees->Limit(
2777 FIELD => 'EffectiveId',
2782 # update their EffectiveId fields to the new ticket's id
2783 while ( my $ticket = $old_mergees->Next() ) {
2784 my ( $val, $msg ) = $ticket->__Set(
2785 Field => 'EffectiveId',
2786 Value => $MergeInto->Id()
2790 #make a new link: this ticket is merged into that other ticket.
2791 $self->AddLink( Type => 'MergedInto', Target => $MergeInto->Id());
2793 $MergeInto->_SetLastUpdated;
2798 Returns list of tickets' ids that's been merged into this ticket.
2806 return @{ $MERGE_CACHE{'merged'}{ $id } }
2807 if $MERGE_CACHE{'merged'}{ $id };
2809 my $mergees = RT::Tickets->new( $self->CurrentUser );
2811 FIELD => 'EffectiveId',
2819 return @{ $MERGE_CACHE{'merged'}{ $id } ||= [] }
2820 = map $_->id, @{ $mergees->ItemsArrayRef || [] };
2829 Takes nothing and returns an RT::User object of
2837 #If this gets ACLed, we lose on a rights check in User.pm and
2838 #get deep recursion. if we need ACLs here, we need
2839 #an equiv without ACLs
2841 my $owner = RT::User->new( $self->CurrentUser );
2842 $owner->Load( $self->__Value('Owner') );
2844 #Return the owner object
2850 =head2 OwnerAsString
2852 Returns the owner's email address
2858 return ( $self->OwnerObj->EmailAddress );
2866 Takes two arguments:
2867 the Id or Name of the owner
2868 and (optionally) the type of the SetOwner Transaction. It defaults
2869 to 'Set'. 'Steal' is also a valid option.
2876 my $NewOwner = shift;
2877 my $Type = shift || "Set";
2879 $RT::Handle->BeginTransaction();
2881 $self->_SetLastUpdated(); # lock the ticket
2882 $self->Load( $self->id ); # in case $self changed while waiting for lock
2884 my $OldOwnerObj = $self->OwnerObj;
2886 my $NewOwnerObj = RT::User->new( $self->CurrentUser );
2887 $NewOwnerObj->Load( $NewOwner );
2888 unless ( $NewOwnerObj->Id ) {
2889 $RT::Handle->Rollback();
2890 return ( 0, $self->loc("That user does not exist") );
2894 # must have ModifyTicket rights
2895 # or TakeTicket/StealTicket and $NewOwner is self
2896 # see if it's a take
2897 if ( $OldOwnerObj->Id == RT->Nobody->Id ) {
2898 unless ( $self->CurrentUserHasRight('ModifyTicket')
2899 || $self->CurrentUserHasRight('TakeTicket') ) {
2900 $RT::Handle->Rollback();
2901 return ( 0, $self->loc("Permission Denied") );
2905 # see if it's a steal
2906 elsif ( $OldOwnerObj->Id != RT->Nobody->Id
2907 && $OldOwnerObj->Id != $self->CurrentUser->id ) {
2909 unless ( $self->CurrentUserHasRight('ModifyTicket')
2910 || $self->CurrentUserHasRight('StealTicket') ) {
2911 $RT::Handle->Rollback();
2912 return ( 0, $self->loc("Permission Denied") );
2916 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
2917 $RT::Handle->Rollback();
2918 return ( 0, $self->loc("Permission Denied") );
2922 # If we're not stealing and the ticket has an owner and it's not
2924 if ( $Type ne 'Steal' and $Type ne 'Force'
2925 and $OldOwnerObj->Id != RT->Nobody->Id
2926 and $OldOwnerObj->Id != $self->CurrentUser->Id )
2928 $RT::Handle->Rollback();
2929 return ( 0, $self->loc("You can only take tickets that are unowned") )
2930 if $NewOwnerObj->id == $self->CurrentUser->id;
2933 $self->loc("You can only reassign tickets that you own or that are unowned" )
2937 #If we've specified a new owner and that user can't modify the ticket
2938 elsif ( !$NewOwnerObj->HasRight( Right => 'OwnTicket', Object => $self ) ) {
2939 $RT::Handle->Rollback();
2940 return ( 0, $self->loc("That user may not own tickets in that queue") );
2943 # If the ticket has an owner and it's the new owner, we don't need
2945 elsif ( $NewOwnerObj->Id == $OldOwnerObj->Id ) {
2946 $RT::Handle->Rollback();
2947 return ( 0, $self->loc("That user already owns that ticket") );
2950 # Delete the owner in the owner group, then add a new one
2951 # TODO: is this safe? it's not how we really want the API to work
2952 # for most things, but it's fast.
2953 my ( $del_id, $del_msg );
2954 for my $owner (@{$self->OwnerGroup->MembersObj->ItemsArrayRef}) {
2955 ($del_id, $del_msg) = $owner->Delete();
2956 last unless ($del_id);
2960 $RT::Handle->Rollback();
2961 return ( 0, $self->loc("Could not change owner: [_1]", $del_msg) );
2964 my ( $add_id, $add_msg ) = $self->OwnerGroup->_AddMember(
2965 PrincipalId => $NewOwnerObj->PrincipalId,
2966 InsideTransaction => 1 );
2968 $RT::Handle->Rollback();
2969 return ( 0, $self->loc("Could not change owner: [_1]", $add_msg ) );
2972 # We call set twice with slightly different arguments, so
2973 # as to not have an SQL transaction span two RT transactions
2975 my ( $val, $msg ) = $self->_Set(
2977 RecordTransaction => 0,
2978 Value => $NewOwnerObj->Id,
2980 TransactionType => 'Set',
2981 CheckACL => 0, # don't check acl
2985 $RT::Handle->Rollback;
2986 return ( 0, $self->loc("Could not change owner: [_1]", $msg) );
2989 ($val, $msg) = $self->_NewTransaction(
2992 NewValue => $NewOwnerObj->Id,
2993 OldValue => $OldOwnerObj->Id,
2998 $msg = $self->loc( "Owner changed from [_1] to [_2]",
2999 $OldOwnerObj->Name, $NewOwnerObj->Name );
3002 $RT::Handle->Rollback();
3006 $RT::Handle->Commit();
3008 return ( $val, $msg );
3015 A convenince method to set the ticket's owner to the current user
3021 return ( $self->SetOwner( $self->CurrentUser->Id, 'Take' ) );
3028 Convenience method to set the owner to 'nobody' if the current user is the owner.
3034 return ( $self->SetOwner( RT->Nobody->UserObj->Id, 'Untake' ) );
3041 A convenience method to change the owner of the current ticket to the
3042 current user. Even if it's owned by another user.
3049 if ( $self->IsOwner( $self->CurrentUser ) ) {
3050 return ( 0, $self->loc("You already own this ticket") );
3053 return ( $self->SetOwner( $self->CurrentUser->Id, 'Steal' ) );
3063 =head2 ValidateStatus STATUS
3065 Takes a string. Returns true if that status is a valid status for this ticket.
3066 Returns false otherwise.
3070 sub ValidateStatus {
3074 #Make sure the status passed in is valid
3075 return 1 if $self->QueueObj->IsValidStatus($status);
3078 while ( my $caller = (caller($i++))[3] ) {
3079 return 1 if $caller eq 'RT::Ticket::SetQueue';
3087 =head2 SetStatus STATUS
3089 Set this ticket\'s status. STATUS can be one of: new, open, stalled, resolved, rejected or deleted.
3091 Alternatively, you can pass in a list of named parameters (Status => STATUS, Force => FORCE, SetStarted => SETSTARTED ).
3092 If FORCE is true, ignore unresolved dependencies and force a status change.
3093 if SETSTARTED is true( it's the default value), set Started to current datetime if Started
3094 is not set and the status is changed from initial to not initial.
3102 $args{Status} = shift;
3108 # this only allows us to SetStarted, not we must SetStarted.
3109 # this option was added for rtir initially
3110 $args{SetStarted} = 1 unless exists $args{SetStarted};
3113 my $lifecycle = $self->QueueObj->Lifecycle;
3115 my $new = $args{'Status'};
3116 unless ( $lifecycle->IsValid( $new ) ) {
3117 return (0, $self->loc("Status '[_1]' isn't a valid status for tickets in this queue.", $self->loc($new)));
3120 my $old = $self->__Value('Status');
3121 unless ( $lifecycle->IsTransition( $old => $new ) ) {
3122 return (0, $self->loc("You can't change status from '[_1]' to '[_2]'.", $self->loc($old), $self->loc($new)));
3125 my $check_right = $lifecycle->CheckRight( $old => $new );
3126 unless ( $self->CurrentUserHasRight( $check_right ) ) {
3127 return ( 0, $self->loc('Permission Denied') );
3130 if ( !$args{Force} && $lifecycle->IsInactive( $new ) && $self->HasUnresolvedDependencies) {
3131 return (0, $self->loc('That ticket has unresolved dependencies'));
3134 my $now = RT::Date->new( $self->CurrentUser );
3137 my $raw_started = RT::Date->new(RT->SystemUser);
3138 $raw_started->Set(Format => 'ISO', Value => $self->__Value('Started'));
3140 #If we're changing the status from new, record that we've started
3141 if ( $args{SetStarted} && $lifecycle->IsInitial($old) && !$lifecycle->IsInitial($new) && !$raw_started->Unix) {
3142 #Set the Started time to "now"
3146 RecordTransaction => 0
3150 #When we close a ticket, set the 'Resolved' attribute to now.
3151 # It's misnamed, but that's just historical.
3152 if ( $lifecycle->IsInactive($new) ) {
3154 Field => 'Resolved',
3156 RecordTransaction => 0,
3160 #Actually update the status
3161 my ($val, $msg)= $self->_Set(
3163 Value => $args{Status},
3166 TransactionType => 'Status',
3168 return ($val, $msg);
3175 Takes no arguments. Marks this ticket for garbage collection
3181 unless ( $self->QueueObj->Lifecycle->IsValid('deleted') ) {
3182 return (0, $self->loc('Delete operation is disabled by lifecycle configuration') ); #loc
3184 return ( $self->SetStatus('deleted') );
3188 =head2 SetTold ISO [TIMETAKEN]
3190 Updates the told and records a transaction
3197 $told = shift if (@_);
3198 my $timetaken = shift || 0;
3200 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
3201 return ( 0, $self->loc("Permission Denied") );
3204 my $datetold = RT::Date->new( $self->CurrentUser );
3206 $datetold->Set( Format => 'iso',
3210 $datetold->SetToNow();
3213 return ( $self->_Set( Field => 'Told',
3214 Value => $datetold->ISO,
3215 TimeTaken => $timetaken,
3216 TransactionType => 'Told' ) );
3221 Updates the told without a transaction or acl check. Useful when we're sending replies.
3228 my $now = RT::Date->new( $self->CurrentUser );
3231 #use __Set to get no ACLs ;)
3232 return ( $self->__Set( Field => 'Told',
3233 Value => $now->ISO ) );
3243 my $uid = $self->CurrentUser->id;
3244 my $attr = $self->FirstAttribute( "User-". $uid ."-SeenUpTo" );
3245 return if $attr && $attr->Content gt $self->LastUpdated;
3247 my $txns = $self->Transactions;
3248 $txns->Limit( FIELD => 'Type', VALUE => 'Comment' );
3249 $txns->Limit( FIELD => 'Type', VALUE => 'Correspond' );
3250 $txns->Limit( FIELD => 'Creator', OPERATOR => '!=', VALUE => $uid );
3254 VALUE => $attr->Content
3256 $txns->RowsPerPage(1);
3257 return $txns->First;
3261 =head2 TransactionBatch
3263 Returns an array reference of all transactions created on this ticket during
3264 this ticket object's lifetime or since last application of a batch, or undef
3267 Only works when the C<UseTransactionBatch> config option is set to true.
3271 sub TransactionBatch {
3273 return $self->{_TransactionBatch};
3276 =head2 ApplyTransactionBatch
3278 Applies scrips on the current batch of transactions and shinks it. Usually
3279 batch is applied when object is destroyed, but in some cases it's too late.
3283 sub ApplyTransactionBatch {
3286 my $batch = $self->TransactionBatch;
3287 return unless $batch && @$batch;
3289 $self->_ApplyTransactionBatch;
3291 $self->{_TransactionBatch} = [];
3294 sub _ApplyTransactionBatch {
3296 my $batch = $self->TransactionBatch;
3299 my $types = join ',', grep !$seen{$_}++, grep defined, map $_->__Value('Type'), grep defined, @{$batch};
3302 RT::Scrips->new(RT->SystemUser)->Apply(
3303 Stage => 'TransactionBatch',
3305 TransactionObj => $batch->[0],
3309 # Entry point of the rule system
3310 my $rules = RT::Ruleset->FindAllRules(
3311 Stage => 'TransactionBatch',
3313 TransactionObj => $batch->[0],
3316 RT::Ruleset->CommitRules($rules);
3322 # DESTROY methods need to localize $@, or it may unset it. This
3323 # causes $m->abort to not bubble all of the way up. See perlbug
3324 # http://rt.perl.org/rt3/Ticket/Display.html?id=17650
3327 # The following line eliminates reentrancy.
3328 # It protects against the fact that perl doesn't deal gracefully
3329 # when an object's refcount is changed in its destructor.
3330 return if $self->{_Destroyed}++;
3332 if (in_global_destruction()) {
3333 unless ($ENV{'HARNESS_ACTIVE'}) {
3334 warn "Too late to safely run transaction-batch scrips!"
3335 ." This is typically caused by using ticket objects"
3336 ." at the top-level of a script which uses the RT API."
3337 ." Be sure to explicitly undef such ticket objects,"
3338 ." or put them inside of a lexical scope.";
3343 my $batch = $self->TransactionBatch;
3344 return unless $batch && @$batch;
3346 return $self->_ApplyTransactionBatch;
3352 sub _OverlayAccessible {
3354 EffectiveId => { 'read' => 1, 'write' => 1, 'public' => 1 },
3355 Queue => { 'read' => 1, 'write' => 1 },
3356 Requestors => { 'read' => 1, 'write' => 1 },
3357 Owner => { 'read' => 1, 'write' => 1 },
3358 Subject => { 'read' => 1, 'write' => 1 },
3359 InitialPriority => { 'read' => 1, 'write' => 1 },
3360 FinalPriority => { 'read' => 1, 'write' => 1 },
3361 Priority => { 'read' => 1, 'write' => 1 },
3362 Status => { 'read' => 1, 'write' => 1 },
3363 TimeEstimated => { 'read' => 1, 'write' => 1 },
3364 TimeWorked => { 'read' => 1, 'write' => 1 },
3365 TimeLeft => { 'read' => 1, 'write' => 1 },
3366 Told => { 'read' => 1, 'write' => 1 },
3367 Resolved => { 'read' => 1 },
3368 Type => { 'read' => 1 },
3369 Starts => { 'read' => 1, 'write' => 1 },
3370 Started => { 'read' => 1, 'write' => 1 },
3371 Due => { 'read' => 1, 'write' => 1 },
3372 Creator => { 'read' => 1, 'auto' => 1 },
3373 Created => { 'read' => 1, 'auto' => 1 },
3374 LastUpdatedBy => { 'read' => 1, 'auto' => 1 },
3375 LastUpdated => { 'read' => 1, 'auto' => 1 }
3385 my %args = ( Field => undef,
3388 RecordTransaction => 1,
3391 TransactionType => 'Set',
3394 if ($args{'CheckACL'}) {
3395 unless ( $self->CurrentUserHasRight('ModifyTicket')) {
3396 return ( 0, $self->loc("Permission Denied"));
3400 unless ($args{'UpdateTicket'} || $args{'RecordTransaction'}) {
3401 $RT::Logger->error("Ticket->_Set called without a mandate to record an update or update the ticket");
3402 return(0, $self->loc("Internal Error"));
3405 #if the user is trying to modify the record
3407 #Take care of the old value we really don't want to get in an ACL loop.
3408 # so ask the super::_Value
3409 my $Old = $self->SUPER::_Value("$args{'Field'}");
3412 if ( $args{'UpdateTicket'} ) {
3415 ( $ret, $msg ) = $self->SUPER::_Set( Field => $args{'Field'},
3416 Value => $args{'Value'} );
3418 #If we can't actually set the field to the value, don't record
3419 # a transaction. instead, get out of here.
3420 return ( 0, $msg ) unless $ret;
3423 if ( $args{'RecordTransaction'} == 1 ) {
3425 my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction(
3426 Type => $args{'TransactionType'},
3427 Field => $args{'Field'},
3428 NewValue => $args{'Value'},
3430 TimeTaken => $args{'TimeTaken'},
3432 return ( $Trans, scalar $TransObj->BriefDescription );
3435 return ( $ret, $msg );
3443 Takes the name of a table column.
3444 Returns its value as a string, if the user passes an ACL check
3453 #if the field is public, return it.
3454 if ( $self->_Accessible( $field, 'public' ) ) {
3456 #$RT::Logger->debug("Skipping ACL check for $field");
3457 return ( $self->SUPER::_Value($field) );
3461 #If the current user doesn't have ACLs, don't let em at it.
3463 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
3466 return ( $self->SUPER::_Value($field) );
3472 =head2 _UpdateTimeTaken
3474 This routine will increment the timeworked counter. it should
3475 only be called from _NewTransaction
3479 sub _UpdateTimeTaken {
3481 my $Minutes = shift;
3484 $Total = $self->SUPER::_Value("TimeWorked");
3485 $Total = ( $Total || 0 ) + ( $Minutes || 0 );
3487 Field => "TimeWorked",
3498 =head2 CurrentUserHasRight
3500 Takes the textual name of a Ticket scoped right (from RT::ACE) and returns
3501 1 if the user has that right. It returns 0 if the user doesn't have that right.
3505 sub CurrentUserHasRight {
3509 return $self->CurrentUser->PrincipalObj->HasRight(
3516 =head2 CurrentUserCanSee
3518 Returns true if the current user can see the ticket, using ShowTicket
3522 sub CurrentUserCanSee {
3524 return $self->CurrentUserHasRight('ShowTicket');
3529 Takes a paramhash with the attributes 'Right' and 'Principal'
3530 'Right' is a ticket-scoped textual right from RT::ACE
3531 'Principal' is an RT::User object
3533 Returns 1 if the principal has the right. Returns undef if not.
3545 unless ( ( defined $args{'Principal'} ) and ( ref( $args{'Principal'} ) ) )
3547 Carp::cluck("Principal attrib undefined for Ticket::HasRight");
3548 $RT::Logger->crit("Principal attrib undefined for Ticket::HasRight");
3553 $args{'Principal'}->HasRight(
3555 Right => $args{'Right'}
3564 Return the Reminders object for this ticket. (It's an RT::Reminders object.)
3565 It isn't acutally a searchbuilder collection itself.
3572 unless ($self->{'__reminders'}) {
3573 $self->{'__reminders'} = RT::Reminders->new($self->CurrentUser);
3574 $self->{'__reminders'}->Ticket($self->id);
3576 return $self->{'__reminders'};
3585 Returns an RT::Transactions object of all transactions on this ticket
3592 my $transactions = RT::Transactions->new( $self->CurrentUser );
3594 #If the user has no rights, return an empty object
3595 if ( $self->CurrentUserHasRight('ShowTicket') ) {
3596 $transactions->LimitToTicket($self->id);
3598 # if the user may not see comments do not return them
3599 unless ( $self->CurrentUserHasRight('ShowTicketComments') ) {
3600 $transactions->Limit(
3606 $transactions->Limit(
3610 VALUE => "CommentEmailRecord",
3611 ENTRYAGGREGATOR => 'AND'
3616 $transactions->Limit(
3620 ENTRYAGGREGATOR => 'AND'
3624 return ($transactions);
3630 =head2 TransactionCustomFields
3632 Returns the custom fields that transactions on tickets will have.
3636 sub TransactionCustomFields {
3638 my $cfs = $self->QueueObj->TicketTransactionCustomFields;
3639 $cfs->SetContextObject( $self );
3645 =head2 CustomFieldValues
3647 # Do name => id mapping (if needed) before falling back to
3648 # RT::Record's CustomFieldValues
3654 sub CustomFieldValues {
3658 return $self->SUPER::CustomFieldValues( $field ) if !$field || $field =~ /^\d+$/;
3660 my $cf = RT::CustomField->new( $self->CurrentUser );
3661 $cf->SetContextObject( $self );
3662 $cf->LoadByNameAndQueue( Name => $field, Queue => $self->Queue );
3663 unless ( $cf->id ) {
3664 $cf->LoadByNameAndQueue( Name => $field, Queue => 0 );
3667 # If we didn't find a valid cfid, give up.
3668 return RT::ObjectCustomFieldValues->new( $self->CurrentUser ) unless $cf->id;
3670 return $self->SUPER::CustomFieldValues( $cf->id );
3675 =head2 CustomFieldLookupType
3677 Returns the RT::Ticket lookup type, which can be passed to
3678 RT::CustomField->Create() via the 'LookupType' hash key.
3683 sub CustomFieldLookupType {
3684 "RT::Queue-RT::Ticket";
3687 =head2 ACLEquivalenceObjects
3689 This method returns a list of objects for which a user's rights also apply
3690 to this ticket. Generally, this is only the ticket's queue, but some RT
3691 extensions may make other objects available too.
3693 This method is called from L<RT::Principal/HasRight>.
3697 sub ACLEquivalenceObjects {
3699 return $self->QueueObj;
3708 Jesse Vincent, jesse@bestpractical.com
3718 use base 'RT::Record';
3720 sub Table {'Tickets'}
3729 Returns the current value of id.
3730 (In the database, id is stored as int(11).)
3738 Returns the current value of EffectiveId.
3739 (In the database, EffectiveId is stored as int(11).)
3743 =head2 SetEffectiveId VALUE
3746 Set EffectiveId to VALUE.
3747 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
3748 (In the database, EffectiveId will be stored as a int(11).)
3756 Returns the current value of Queue.
3757 (In the database, Queue is stored as int(11).)
3761 =head2 SetQueue VALUE
3765 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
3766 (In the database, Queue will be stored as a int(11).)
3774 Returns the current value of Type.
3775 (In the database, Type is stored as varchar(16).)
3779 =head2 SetType VALUE
3783 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
3784 (In the database, Type will be stored as a varchar(16).)
3790 =head2 IssueStatement
3792 Returns the current value of IssueStatement.
3793 (In the database, IssueStatement is stored as int(11).)
3797 =head2 SetIssueStatement VALUE
3800 Set IssueStatement to VALUE.
3801 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
3802 (In the database, IssueStatement will be stored as a int(11).)
3810 Returns the current value of Resolution.
3811 (In the database, Resolution is stored as int(11).)
3815 =head2 SetResolution VALUE
3818 Set Resolution to VALUE.
3819 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
3820 (In the database, Resolution will be stored as a int(11).)
3828 Returns the current value of Owner.
3829 (In the database, Owner is stored as int(11).)
3833 =head2 SetOwner VALUE
3837 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
3838 (In the database, Owner will be stored as a int(11).)
3846 Returns the current value of Subject.
3847 (In the database, Subject is stored as varchar(200).)
3851 =head2 SetSubject VALUE
3854 Set Subject to VALUE.
3855 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
3856 (In the database, Subject will be stored as a varchar(200).)
3862 =head2 InitialPriority
3864 Returns the current value of InitialPriority.
3865 (In the database, InitialPriority is stored as int(11).)
3869 =head2 SetInitialPriority VALUE
3872 Set InitialPriority to VALUE.
3873 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
3874 (In the database, InitialPriority will be stored as a int(11).)
3880 =head2 FinalPriority
3882 Returns the current value of FinalPriority.
3883 (In the database, FinalPriority is stored as int(11).)
3887 =head2 SetFinalPriority VALUE
3890 Set FinalPriority to VALUE.
3891 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
3892 (In the database, FinalPriority will be stored as a int(11).)
3900 Returns the current value of Priority.
3901 (In the database, Priority is stored as int(11).)
3905 =head2 SetPriority VALUE
3908 Set Priority to VALUE.
3909 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
3910 (In the database, Priority will be stored as a int(11).)
3916 =head2 TimeEstimated
3918 Returns the current value of TimeEstimated.
3919 (In the database, TimeEstimated is stored as int(11).)
3923 =head2 SetTimeEstimated VALUE
3926 Set TimeEstimated to VALUE.
3927 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
3928 (In the database, TimeEstimated will be stored as a int(11).)
3936 Returns the current value of TimeWorked.
3937 (In the database, TimeWorked is stored as int(11).)
3941 =head2 SetTimeWorked VALUE
3944 Set TimeWorked to VALUE.
3945 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
3946 (In the database, TimeWorked will be stored as a int(11).)
3954 Returns the current value of Status.
3955 (In the database, Status is stored as varchar(64).)
3959 =head2 SetStatus VALUE
3962 Set Status to VALUE.
3963 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
3964 (In the database, Status will be stored as a varchar(64).)
3972 Returns the current value of TimeLeft.
3973 (In the database, TimeLeft is stored as int(11).)
3977 =head2 SetTimeLeft VALUE
3980 Set TimeLeft to VALUE.
3981 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
3982 (In the database, TimeLeft will be stored as a int(11).)
3990 Returns the current value of Told.
3991 (In the database, Told is stored as datetime.)
3995 =head2 SetTold VALUE
3999 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
4000 (In the database, Told will be stored as a datetime.)
4008 Returns the current value of Starts.
4009 (In the database, Starts is stored as datetime.)
4013 =head2 SetStarts VALUE
4016 Set Starts to VALUE.
4017 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
4018 (In the database, Starts will be stored as a datetime.)
4026 Returns the current value of Started.
4027 (In the database, Started is stored as datetime.)
4031 =head2 SetStarted VALUE
4034 Set Started to VALUE.
4035 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
4036 (In the database, Started will be stored as a datetime.)
4044 Returns the current value of Due.
4045 (In the database, Due is stored as datetime.)
4053 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
4054 (In the database, Due will be stored as a datetime.)
4062 Returns the current value of Resolved.
4063 (In the database, Resolved is stored as datetime.)
4067 =head2 SetResolved VALUE
4070 Set Resolved to VALUE.
4071 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
4072 (In the database, Resolved will be stored as a datetime.)
4078 =head2 LastUpdatedBy
4080 Returns the current value of LastUpdatedBy.
4081 (In the database, LastUpdatedBy is stored as int(11).)
4089 Returns the current value of LastUpdated.
4090 (In the database, LastUpdated is stored as datetime.)
4098 Returns the current value of Creator.
4099 (In the database, Creator is stored as int(11).)
4107 Returns the current value of Created.
4108 (In the database, Created is stored as datetime.)
4116 Returns the current value of Disabled.
4117 (In the database, Disabled is stored as smallint(6).)
4121 =head2 SetDisabled VALUE
4124 Set Disabled to VALUE.
4125 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
4126 (In the database, Disabled will be stored as a smallint(6).)
4133 sub _CoreAccessible {
4137 {read => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => ''},
4139 {read => 1, write => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'},
4141 {read => 1, write => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'},
4143 {read => 1, write => 1, sql_type => 12, length => 16, is_blob => 0, is_numeric => 0, type => 'varchar(16)', default => ''},
4145 {read => 1, write => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'},
4147 {read => 1, write => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'},
4149 {read => 1, write => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'},
4151 {read => 1, write => 1, sql_type => 12, length => 200, is_blob => 0, is_numeric => 0, type => 'varchar(200)', default => '[no subject]'},
4153 {read => 1, write => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'},
4155 {read => 1, write => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'},
4157 {read => 1, write => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'},
4159 {read => 1, write => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'},
4161 {read => 1, write => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'},
4163 {read => 1, write => 1, sql_type => 12, length => 64, is_blob => 0, is_numeric => 0, type => 'varchar(64)', default => ''},
4165 {read => 1, write => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'},
4167 {read => 1, write => 1, sql_type => 11, length => 0, is_blob => 0, is_numeric => 0, type => 'datetime', default => ''},
4169 {read => 1, write => 1, sql_type => 11, length => 0, is_blob => 0, is_numeric => 0, type => 'datetime', default => ''},
4171 {read => 1, write => 1, sql_type => 11, length => 0, is_blob => 0, is_numeric => 0, type => 'datetime', default => ''},
4173 {read => 1, write => 1, sql_type => 11, length => 0, is_blob => 0, is_numeric => 0, type => 'datetime', default => ''},
4175 {read => 1, write => 1, sql_type => 11, length => 0, is_blob => 0, is_numeric => 0, type => 'datetime', default => ''},
4177 {read => 1, auto => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'},
4179 {read => 1, auto => 1, sql_type => 11, length => 0, is_blob => 0, is_numeric => 0, type => 'datetime', default => ''},
4181 {read => 1, auto => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'},
4183 {read => 1, auto => 1, sql_type => 11, length => 0, is_blob => 0, is_numeric => 0, type => 'datetime', default => ''},
4185 {read => 1, write => 1, sql_type => 5, length => 6, is_blob => 0, is_numeric => 1, type => 'smallint(6)', default => '0'},
4190 RT::Base->_ImportOverlays();