1 # BEGIN BPS TAGGED BLOCK {{{
5 # This software is Copyright (c) 1996-2015 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.
70 use base 'RT::Record';
72 use Role::Basic 'with';
74 # SetStatus and _SetStatus are reimplemented below (using other pieces of the
75 # role) to deal with ACLs, moving tickets between queues, and automatically
77 with "RT::Record::Role::Status" => { -excludes => [qw(SetStatus _SetStatus)] },
78 "RT::Record::Role::Links",
79 "RT::Record::Role::Roles";
91 use RT::URI::fsck_com_rt;
93 use RT::URI::freeside;
95 use Devel::GlobalDestruction;
97 sub LifecycleColumn { "Queue" }
100 # name => description
101 Owner => 'The owner of a ticket', # loc_pair
102 Requestor => 'The requestor of a ticket', # loc_pair
103 Cc => 'The CC of a ticket', # loc_pair
104 AdminCc => 'The administrative CC of a ticket', # loc_pair
107 for my $role (sort keys %ROLES) {
108 RT::Ticket->RegisterRole(
110 EquivClasses => ['RT::Queue'],
111 ( $role eq "Owner" ? ( Column => "Owner") : () ),
112 ( $role !~ /Cc/ ? ( ACLOnlyInEquiv => 1) : () ),
124 Takes a single argument. This can be a ticket id, ticket alias or
125 local ticket uri. If the ticket can't be loaded, returns undef.
126 Otherwise, returns the ticket id.
133 $id = '' unless defined $id;
135 # TODO: modify this routine to look at EffectiveId and
136 # do the recursive load thing. be careful to cache all
137 # the interim tickets we try so we don't loop forever.
139 unless ( $id =~ /^\d+$/ ) {
140 $RT::Logger->debug("Tried to load a bogus ticket id: '$id'");
144 $id = $MERGE_CACHE{'effective'}{ $id }
145 if $MERGE_CACHE{'effective'}{ $id };
147 my ($ticketid, $msg) = $self->LoadById( $id );
148 unless ( $self->Id ) {
149 $RT::Logger->debug("$self tried to load a bogus ticket: $id");
153 #If we're merged, resolve the merge.
154 if ( $self->EffectiveId && $self->EffectiveId != $self->Id ) {
156 "We found a merged ticket. "
157 . $self->id ."/". $self->EffectiveId
159 my $real_id = $self->Load( $self->EffectiveId );
160 $MERGE_CACHE{'effective'}{ $id } = $real_id;
164 #Ok. we're loaded. lets get outa here.
172 Arguments: ARGS is a hash of named parameters. Valid parameters are:
175 Queue - Either a Queue object or a Queue Name
176 Requestor - A reference to a list of email addresses or RT user Names
177 Cc - A reference to a list of email addresses or Names
178 AdminCc - A reference to a list of email addresses or Names
179 SquelchMailTo - A reference to a list of email addresses -
180 who should this ticket not mail
181 Type -- The ticket's type. ignore this for now
182 Owner -- This ticket's owner. either an RT::User object or this user's id
183 Subject -- A string describing the subject of the ticket
184 Priority -- an integer from 0 to 99
185 InitialPriority -- an integer from 0 to 99
186 FinalPriority -- an integer from 0 to 99
187 Status -- any valid status for Queue's Lifecycle, otherwises uses on_create from Lifecycle default
188 TimeEstimated -- an integer. estimated time for this task in minutes
189 TimeWorked -- an integer. time worked so far in minutes
190 TimeLeft -- an integer. time remaining in minutes
191 Starts -- an ISO date describing the ticket's start date and time in GMT
192 Due -- an ISO date describing the ticket's due date and time in GMT
193 MIMEObj -- a MIME::Entity object with the content of the initial ticket request.
194 CustomField-<n> -- a scalar or array of values for the customfield with the id <n>
196 Ticket links can be set up during create by passing the link type as a hask key and
197 the ticket id to be linked to as a value (or a URI when linking to other objects).
198 Multiple links of the same type can be created by passing an array ref. For example:
201 DependsOn => [ 15, 22 ],
202 RefersTo => 'http://www.bestpractical.com',
204 Supported link types are C<MemberOf>, C<HasMember>, C<RefersTo>, C<ReferredToBy>,
205 C<DependsOn> and C<DependedOnBy>. Also, C<Parents> is alias for C<MemberOf> and
206 C<Members> and C<Children> are aliases for C<HasMember>.
208 Returns: TICKETID, Transaction Object, Error Message
218 EffectiveId => undef,
223 SquelchMailTo => undef,
224 TransSquelchMailTo => undef,
228 InitialPriority => undef,
229 FinalPriority => undef,
239 WillResolve => undef,
241 _RecordTransaction => 1,
246 my ($ErrStr, @non_fatal_errors);
248 my $QueueObj = RT::Queue->new( RT->SystemUser );
249 if ( ref $args{'Queue'} eq 'RT::Queue' ) {
250 $QueueObj->Load( $args{'Queue'}->Id );
252 elsif ( $args{'Queue'} ) {
253 $QueueObj->Load( $args{'Queue'} );
256 $RT::Logger->debug("'". ( $args{'Queue'} ||''). "' not a recognised queue object." );
259 #Can't create a ticket without a queue.
260 unless ( $QueueObj->Id ) {
261 $RT::Logger->debug("$self No queue given for ticket creation.");
262 return ( 0, 0, $self->loc('Could not create ticket. Queue not set') );
266 #Now that we have a queue, Check the ACLS
268 $self->CurrentUser->HasRight(
269 Right => 'CreateTicket',
271 ) and $QueueObj->Disabled != 1
276 $self->loc( "No permission to create tickets in the queue '[_1]'", $QueueObj->Name));
279 my $cycle = $QueueObj->LifecycleObj;
280 unless ( defined $args{'Status'} && length $args{'Status'} ) {
281 $args{'Status'} = $cycle->DefaultOnCreate;
284 $args{'Status'} = lc $args{'Status'};
285 unless ( $cycle->IsValid( $args{'Status'} ) ) {
287 $self->loc("Status '[_1]' isn't a valid status for tickets in this queue.",
288 $self->loc($args{'Status'}))
292 unless ( $cycle->IsTransition( '' => $args{'Status'} ) ) {
294 $self->loc("New tickets can not have status '[_1]' in this queue.",
295 $self->loc($args{'Status'}))
301 #Since we have a queue, we can set queue defaults
304 # If there's no queue default initial priority and it's not set, set it to 0
305 $args{'InitialPriority'} = $QueueObj->InitialPriority || 0
306 unless defined $args{'InitialPriority'};
309 # If there's no queue default final priority and it's not set, set it to 0
310 $args{'FinalPriority'} = $QueueObj->FinalPriority || 0
311 unless defined $args{'FinalPriority'};
313 # Priority may have changed from InitialPriority, for the case
314 # where we're importing tickets (eg, from an older RT version.)
315 $args{'Priority'} = $args{'InitialPriority'}
316 unless defined $args{'Priority'};
319 #TODO we should see what sort of due date we're getting, rather +
320 # than assuming it's in ISO format.
322 #Set the due date. if we didn't get fed one, use the queue default due in
323 my $Due = RT::Date->new( $self->CurrentUser );
324 if ( defined $args{'Due'} ) {
325 $Due->Set( Format => 'ISO', Value => $args{'Due'} );
327 elsif ( my $due_in = $QueueObj->DefaultDueIn ) {
329 $Due->AddDays( $due_in );
332 my $Starts = RT::Date->new( $self->CurrentUser );
333 if ( defined $args{'Starts'} ) {
334 $Starts->Set( Format => 'ISO', Value => $args{'Starts'} );
337 my $Started = RT::Date->new( $self->CurrentUser );
338 if ( defined $args{'Started'} ) {
339 $Started->Set( Format => 'ISO', Value => $args{'Started'} );
342 my $WillResolve = RT::Date->new($self->CurrentUser );
343 if ( defined $args{'WillResolve'} ) {
344 $WillResolve->Set( Format => 'ISO', Value => $args{'WillResolve'} );
347 # If the status is not an initial status, set the started date
348 elsif ( !$cycle->IsInitial($args{'Status'}) ) {
352 my $Resolved = RT::Date->new( $self->CurrentUser );
353 if ( defined $args{'Resolved'} ) {
354 $Resolved->Set( Format => 'ISO', Value => $args{'Resolved'} );
357 #If the status is an inactive status, set the resolved date
358 elsif ( $cycle->IsInactive( $args{'Status'} ) )
360 $RT::Logger->debug( "Got a ". $args{'Status'}
361 ."(inactive) ticket with undefined resolved date. Setting to now."
366 # Dealing with time fields
367 $args{'TimeEstimated'} = 0 unless defined $args{'TimeEstimated'};
368 $args{'TimeWorked'} = 0 unless defined $args{'TimeWorked'};
369 $args{'TimeLeft'} = 0 unless defined $args{'TimeLeft'};
371 # Figure out users for roles
373 push @non_fatal_errors, $self->_ResolveRoles( $roles, %args );
375 $args{'Type'} = lc $args{'Type'}
376 if $args{'Type'} =~ /^(ticket|approval|reminder)$/i;
378 $args{'Subject'} =~ s/\n//g;
380 $RT::Handle->BeginTransaction();
383 Queue => $QueueObj->Id,
384 Subject => $args{'Subject'},
385 InitialPriority => $args{'InitialPriority'},
386 FinalPriority => $args{'FinalPriority'},
387 Priority => $args{'Priority'},
388 Status => $args{'Status'},
389 TimeWorked => $args{'TimeWorked'},
390 TimeEstimated => $args{'TimeEstimated'},
391 TimeLeft => $args{'TimeLeft'},
392 Type => $args{'Type'},
393 Starts => $Starts->ISO,
394 Started => $Started->ISO,
395 Resolved => $Resolved->ISO,
396 WillResolve => $WillResolve->ISO,
400 # Parameters passed in during an import that we probably don't want to touch, otherwise
401 foreach my $attr (qw(id Creator Created LastUpdated LastUpdatedBy)) {
402 $params{$attr} = $args{$attr} if $args{$attr};
405 # Delete null integer parameters
407 (qw(TimeWorked TimeLeft TimeEstimated InitialPriority FinalPriority))
409 delete $params{$attr}
410 unless ( exists $params{$attr} && $params{$attr} );
413 # Delete the time worked if we're counting it in the transaction
414 delete $params{'TimeWorked'} if $args{'_RecordTransaction'};
416 my ($id,$ticket_message) = $self->SUPER::Create( %params );
418 $RT::Logger->crit( "Couldn't create a ticket: " . $ticket_message );
419 $RT::Handle->Rollback();
421 $self->loc("Ticket could not be created due to an internal error")
425 #Set the ticket's effective ID now that we've created it.
426 my ( $val, $msg ) = $self->__Set(
427 Field => 'EffectiveId',
428 Value => ( $args{'EffectiveId'} || $id )
431 $RT::Logger->crit("Couldn't set EffectiveId: $msg");
432 $RT::Handle->Rollback;
434 $self->loc("Ticket could not be created due to an internal error")
438 # Create (empty) role groups
439 my $create_groups_ret = $self->_CreateRoleGroups();
440 unless ($create_groups_ret) {
441 $RT::Logger->crit( "Couldn't create ticket groups for ticket "
443 . ". aborting Ticket creation." );
444 $RT::Handle->Rollback();
446 $self->loc("Ticket could not be created due to an internal error")
450 # Codify what it takes to add each kind of group
453 Requestor => sub { 1 },
455 my $principal = shift;
456 return 1 if $self->CurrentUserHasRight('ModifyTicket');
457 return unless $self->CurrentUserHasRight("WatchAsAdminCc");
458 return unless $principal->id == $self->CurrentUser->PrincipalId;
462 my $principal = shift;
463 return 1 if $principal->id == RT->Nobody->PrincipalId;
464 return $principal->HasRight( Object => $self, Right => 'OwnTicket' );
468 # Populate up the role groups. This call modifies $roles.
469 push @non_fatal_errors, $self->_AddRolesOnCreate( $roles, %acls );
472 if ($args{'SquelchMailTo'}) {
473 my @squelch = ref( $args{'SquelchMailTo'} ) ? @{ $args{'SquelchMailTo'} }
474 : $args{'SquelchMailTo'};
475 $self->_SquelchMailTo( @squelch );
478 # Add all the custom fields
479 foreach my $arg ( keys %args ) {
480 next unless $arg =~ /^CustomField-(\d+)$/i;
482 my $cf = $self->LoadCustomFieldByIdentifier($cfid);
483 next unless $cf->ObjectTypeFromLookupType($cf->__Value('LookupType'))->isa(ref $self);
486 UNIVERSAL::isa( $args{$arg} => 'ARRAY' ) ? @{ $args{$arg} } : ( $args{$arg} ) )
488 next unless defined $value && length $value;
490 # Allow passing in uploaded LargeContent etc by hash reference
491 my ($status, $msg) = $self->_AddCustomFieldValue(
492 (UNIVERSAL::isa( $value => 'HASH' )
497 RecordTransaction => 0,
499 push @non_fatal_errors, $msg unless $status;
503 # Deal with setting up links
505 # TODO: Adding link may fire scrips on other end and those scrips
506 # could create transactions on this ticket before 'Create' transaction.
508 # We should implement different lifecycle: record 'Create' transaction,
509 # create links and only then fire create transaction's scrips.
511 # Ideal variant: add all links without firing scrips, record create
512 # transaction and only then fire scrips on the other ends of links.
515 push @non_fatal_errors, $self->_AddLinksOnCreate(\%args, {
516 Silent => !$args{'_RecordTransaction'} || ($self->Type || '') eq 'reminder',
521 # {{{ Deal with auto-customer association
523 #unless we already have (a) customer(s)...
524 unless ( $self->Customers->Count ) {
526 #first find any requestors with emails but *without* customer targets
527 my @NoCust_Requestors =
528 grep { $_->EmailAddress && ! $_->Customers->Count }
529 @{ $self->_Requestors->UserMembersObj->ItemsArrayRef };
531 for my $Requestor (@NoCust_Requestors) {
533 #perhaps the stuff in here should be in a User method??
535 &RT::URI::freeside::email_search( email=>$Requestor->EmailAddress );
537 foreach my $custnum ( map $_->{'custnum'}, @Customers ) {
539 ## false laziness w/RT/Interface/Web_Vendor.pm
540 my @link = ( 'Type' => 'MemberOf',
541 'Target' => "freeside://freeside/cust_main/$custnum",
544 my( $val, $msg ) = $Requestor->_AddLink(@link);
545 #XXX should do something with $msg# push @non_fatal_errors, $msg;
551 #find any requestors with customer targets
553 my %cust_target = ();
556 grep { $_->Customers->Count }
557 @{ $self->_Requestors->UserMembersObj->ItemsArrayRef };
559 foreach my $Requestor ( @Requestors ) {
560 foreach my $cust_link ( @{ $Requestor->Customers->ItemsArrayRef } ) {
561 $cust_target{ $cust_link->Target } = 1;
565 #and then auto-associate this ticket with those customers
567 foreach my $cust_target ( keys %cust_target ) {
569 my @link = ( 'Type' => 'MemberOf',
570 #'Target' => "freeside://freeside/cust_main/$custnum",
571 'Target' => $cust_target,
574 my( $val, $msg ) = $self->_AddLink(@link);
575 push @non_fatal_errors, $msg;
583 push @non_fatal_errors, $self->_AddLinksOnCreate(\%args, {
584 Silent => !$args{'_RecordTransaction'} || ($self->Type || '') eq 'reminder',
587 # Try to add roles once more.
588 push @non_fatal_errors, $self->_AddRolesOnCreate( $roles, %acls );
590 # Anything left is failure of ACLs; Cc and Requestor have no ACLs,
591 # so we don't bother checking them.
592 if (@{ $roles->{Owner} }) {
593 my $owner = $roles->{Owner}[0]->Object;
594 $RT::Logger->warning( "User " . $owner->Name . "(" . $owner->id
595 . ") was proposed as a ticket owner but has no rights to own "
596 . "tickets in " . $QueueObj->Name );
597 push @non_fatal_errors, $self->loc(
598 "Owner '[_1]' does not have rights to own this ticket.",
602 for my $principal (@{ $roles->{AdminCc} }) {
603 push @non_fatal_errors, $self->loc(
604 "No rights to add '[_1]' as an AdminCc on this ticket",
605 $principal->Object->Name
609 #don't make a transaction or fire off any scrips for reminders either
610 if ( $args{'_RecordTransaction'} && $self->Type ne 'reminder' ) {
612 # Add a transaction for the create
613 my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction(
615 TimeTaken => $args{'TimeWorked'},
616 MIMEObj => $args{'MIMEObj'},
617 CommitScrips => !$args{'DryRun'},
618 SquelchMailTo => $args{'TransSquelchMailTo'},
621 if ( $self->Id && $Trans ) {
623 $TransObj->UpdateCustomFields(%args);
625 $RT::Logger->info( "Ticket " . $self->Id . " created in queue '" . $QueueObj->Name . "' by " . $self->CurrentUser->Name );
626 $ErrStr = $self->loc( "Ticket [_1] created in queue '[_2]'", $self->Id, $QueueObj->Name );
627 $ErrStr = join( "\n", $ErrStr, @non_fatal_errors );
630 $RT::Handle->Rollback();
632 $ErrStr = join( "\n", $ErrStr, @non_fatal_errors );
633 $RT::Logger->error("Ticket couldn't be created: $ErrStr");
634 return ( 0, 0, $self->loc( "Ticket could not be created due to an internal error"));
637 if ( $args{'DryRun'} ) {
638 $RT::Handle->Rollback();
639 return ($self->id, $TransObj, $ErrStr);
641 $RT::Handle->Commit();
642 return ( $self->Id, $TransObj->Id, $ErrStr );
646 # Not going to record a transaction
647 $RT::Handle->Commit();
648 $ErrStr = $self->loc( "Ticket [_1] created in queue '[_2]'", $self->Id, $QueueObj->Name );
649 $ErrStr = join( "\n", $ErrStr, @non_fatal_errors );
650 return ( $self->Id, 0, $ErrStr );
659 # Force lowercase on internal RT types
661 if $value =~ /^(ticket|approval|reminder)$/i;
662 return $self->_Set(Field => 'Type', Value => $value, @_);
667 A constructor which returns an RT::Group object containing the owner of this ticket.
673 return $self->RoleGroup( 'Owner' );
677 sub _HasModifyWatcherRight {
679 my ($type, $principal) = @_;
681 # ModifyTicket works in any case
682 return 1 if $self->CurrentUserHasRight('ModifyTicket');
683 # If the watcher isn't the current user then the current user has no right
684 return 0 unless $self->CurrentUser->PrincipalId == $principal->id;
685 # If it's an AdminCc and they don't have 'WatchAsAdminCc', bail
686 return 0 if $type eq 'AdminCc' and not $self->CurrentUserHasRight('WatchAsAdminCc');
687 # If it's a Requestor or Cc and they don't have 'Watch', bail
688 return 0 if ($type eq "Cc" or $type eq 'Requestor')
689 and not $self->CurrentUserHasRight('Watch');
696 Applies access control checking, then calls
697 L<RT::Record::Role::Roles/AddRoleMember>. Additionally, C<Email> is
698 accepted as an alternative argument name for C<User>.
700 Returns a tuple of (status, message).
708 PrincipalId => undef,
713 $args{ACL} = sub { $self->_HasModifyWatcherRight( @_ ) };
714 $args{User} ||= delete $args{Email};
715 my ($principal, $msg) = $self->AddRoleMember(
717 InsideTransaction => 1,
719 return ( 0, $msg) unless $principal;
721 return ( 1, $self->loc('Added [_1] as a [_2] for this ticket',
722 $principal->Object->Name, $self->loc($args{'Type'})) );
728 Applies access control checking, then calls
729 L<RT::Record::Role::Roles/DeleteRoleMember>. Additionally, C<Email> is
730 accepted as an alternative argument name for C<User>.
732 Returns a tuple of (status, message).
740 my %args = ( Type => undef,
741 PrincipalId => undef,
745 $args{ACL} = sub { $self->_HasModifyWatcherRight( @_ ) };
746 $args{User} ||= delete $args{Email};
747 my ($principal, $msg) = $self->DeleteRoleMember( %args );
748 return ( 0, $msg ) unless $principal;
751 $self->loc( "[_1] is no longer a [_2] for this ticket.",
752 $principal->Object->Name,
753 $self->loc($args{'Type'}) ) );
760 =head2 SquelchMailTo [EMAIL]
762 Takes an optional email address to never email about updates to this ticket.
765 Returns an array of the RT::Attribute objects for this ticket's 'SquelchMailTo' attributes.
773 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
777 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
782 return $self->_SquelchMailTo(@_);
789 $self->AddAttribute( Name => 'SquelchMailTo', Content => $attr )
790 unless grep { $_->Content eq $attr }
791 $self->Attributes->Named('SquelchMailTo');
793 my @attributes = $self->Attributes->Named('SquelchMailTo');
794 return (@attributes);
798 =head2 UnsquelchMailTo ADDRESS
800 Takes an address and removes it from this ticket's "SquelchMailTo" list. If an address appears multiple times, each instance is removed.
802 Returns a tuple of (status, message)
806 sub UnsquelchMailTo {
810 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
811 return ( 0, $self->loc("Permission Denied") );
814 my ($val, $msg) = $self->Attributes->DeleteEntry ( Name => 'SquelchMailTo', Content => $address);
820 =head2 RequestorAddresses
822 B<Returns> String: All Ticket Requestor email addresses as a string.
826 sub RequestorAddresses {
829 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
833 return ( $self->Requestors->MemberEmailAddressesAsString );
837 =head2 AdminCcAddresses
839 returns String: All Ticket AdminCc email addresses as a string
843 sub AdminCcAddresses {
846 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
850 return ( $self->AdminCc->MemberEmailAddressesAsString )
856 returns String: All Ticket Ccs as a string of email addresses
863 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
866 return ( $self->Cc->MemberEmailAddressesAsString);
876 Returns this ticket's Requestors as an RT::Group object
882 return RT::Group->new($self->CurrentUser)
883 unless $self->CurrentUserHasRight('ShowTicket');
884 return $self->RoleGroup( 'Requestor' );
889 return $self->Requestor;
894 Private non-ACLed variant of Reqeustors so that we can look them up for the
895 purposes of customer auto-association during create.
902 my $group = RT::Group->new($RT::SystemUser);
903 $group->LoadTicketRoleGroup(Type => 'Requestor', Ticket => $self->Id);
910 Returns an RT::Group object which contains this ticket's Ccs.
911 If the user doesn't have "ShowTicket" permission, returns an empty group
918 return RT::Group->new($self->CurrentUser)
919 unless $self->CurrentUserHasRight('ShowTicket');
920 return $self->RoleGroup( 'Cc' );
928 Returns an RT::Group object which contains this ticket's AdminCcs.
929 If the user doesn't have "ShowTicket" permission, returns an empty group
936 return RT::Group->new($self->CurrentUser)
937 unless $self->CurrentUserHasRight('ShowTicket');
938 return $self->RoleGroup( 'AdminCc' );
944 # a generic routine to be called by IsRequestor, IsCc and IsAdminCc
946 =head2 IsWatcher { Type => TYPE, PrincipalId => PRINCIPAL_ID, Email => EMAIL }
948 Takes a param hash with the attributes Type and either PrincipalId or Email
950 Type is one of Requestor, Cc, AdminCc and Owner
952 PrincipalId is an RT::Principal id, and Email is an email address.
954 Returns true if the specified principal (or the one corresponding to the
955 specified address) is a member of the group Type for this ticket.
957 XX TODO: This should be Memoized.
964 my %args = ( Type => 'Requestor',
965 PrincipalId => undef,
970 # Load the relevant group.
971 my $group = $self->RoleGroup( $args{'Type'} );
973 # Find the relevant principal.
974 if (!$args{PrincipalId} && $args{Email}) {
975 # Look up the specified user.
976 my $user = RT::User->new($self->CurrentUser);
977 $user->LoadByEmail($args{Email});
979 $args{PrincipalId} = $user->PrincipalId;
982 # A non-existent user can't be a group member.
987 # Ask if it has the member in question
988 return $group->HasMember( $args{'PrincipalId'} );
993 =head2 IsRequestor PRINCIPAL_ID
995 Takes an L<RT::Principal> id.
997 Returns true if the principal is a requestor of the current ticket.
1005 return ( $self->IsWatcher( Type => 'Requestor', PrincipalId => $person ) );
1011 =head2 IsCc PRINCIPAL_ID
1013 Takes an RT::Principal id.
1014 Returns true if the principal is a Cc of the current ticket.
1023 return ( $self->IsWatcher( Type => 'Cc', PrincipalId => $cc ) );
1029 =head2 IsAdminCc PRINCIPAL_ID
1031 Takes an RT::Principal id.
1032 Returns true if the principal is an AdminCc of the current ticket.
1040 return ( $self->IsWatcher( Type => 'AdminCc', PrincipalId => $person ) );
1048 Takes an RT::User object. Returns true if that user is this ticket's owner.
1049 returns undef otherwise
1057 # no ACL check since this is used in acl decisions
1058 # unless ($self->CurrentUserHasRight('ShowTicket')) {
1062 #Tickets won't yet have owners when they're being created.
1063 unless ( $self->OwnerObj->id ) {
1067 if ( $person->id == $self->OwnerObj->id ) {
1079 =head2 TransactionAddresses
1081 Returns a composite hashref of the results of L<RT::Transaction/Addresses> for
1082 all this ticket's Create, Comment or Correspond transactions. The keys are
1083 stringified email addresses. Each value is an L<Email::Address> object.
1085 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.
1090 sub TransactionAddresses {
1092 my $txns = $self->Transactions;
1096 my $attachments = RT::Attachments->new( $self->CurrentUser );
1097 $attachments->LimitByTicket( $self->id );
1098 $attachments->Columns( qw( id Headers TransactionId));
1100 $attachments->Limit(
1101 ALIAS => $attachments->TransactionAlias,
1104 VALUE => [ qw(Create Comment Correspond) ],
1107 while ( my $att = $attachments->Next ) {
1108 foreach my $addrlist ( values %{$att->Addresses } ) {
1109 foreach my $addr (@$addrlist) {
1111 # Skip addresses without a phrase (things that are just raw addresses) if we have a phrase
1113 if ( $addresses{ $addr->address }
1114 && $addresses{ $addr->address }->phrase
1115 && not $addr->phrase );
1117 # skips "comment-only" addresses
1118 next unless ( $addr->address );
1119 $addresses{ $addr->address } = $addr;
1138 $RT::Logger->warning( " RT:::Queue::ValidateQueue called with a null value. this isn't ok.");
1142 my $QueueObj = RT::Queue->new( $self->CurrentUser );
1143 my $id = $QueueObj->Load($Value);
1157 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
1158 return ( 0, $self->loc("Permission Denied") );
1161 my ($ok, $msg, $status) = $self->_SetLifecycleColumn(
1163 RequireRight => "CreateTicket"
1167 # Clear the queue object cache;
1168 $self->{_queue_obj} = undef;
1169 my $queue = $self->QueueObj;
1171 # Untake the ticket if we have no permissions in the new queue
1172 unless ($self->OwnerObj->HasRight( Right => 'OwnTicket', Object => $queue )) {
1173 my $clone = RT::Ticket->new( RT->SystemUser );
1174 $clone->Load( $self->Id );
1175 unless ( $clone->Id ) {
1176 return ( 0, $self->loc("Couldn't load copy of ticket #[_1].", $self->Id) );
1178 my ($status, $msg) = $clone->SetOwner( RT->Nobody->Id, 'Force' );
1179 $RT::Logger->error("Couldn't set owner on queue change: $msg") unless $status;
1182 # On queue change, change queue for reminders too
1183 my $reminder_collection = $self->Reminders->Collection;
1184 while ( my $reminder = $reminder_collection->Next ) {
1185 my ($status, $msg) = $reminder->_Set( Field => 'Queue', Value => $queue->Id(), RecordTransaction => 0 );
1186 $RT::Logger->error('Queue change failed for reminder #' . $reminder->Id . ': ' . $msg) unless $status;
1189 # Pick up any changes made by the clones above
1190 $self->Load( $self->id );
1191 RT->Logger->error("Unable to reload ticket #" . $self->id)
1202 Takes nothing. returns this ticket's queue object
1209 if(!$self->{_queue_obj} || ! $self->{_queue_obj}->id) {
1211 $self->{_queue_obj} = RT::Queue->new( $self->CurrentUser );
1213 #We call __Value so that we can avoid the ACL decision and some deep recursion
1214 my ($result) = $self->{_queue_obj}->Load( $self->__Value('Queue') );
1216 return ($self->{_queue_obj});
1223 return $self->_Set( Field => 'Subject', Value => $value );
1228 Takes nothing. Returns SubjectTag for this ticket. Includes
1229 queue's subject tag or rtname if that is not set, ticket
1230 id and brackets, for example:
1232 [support.example.com #123456]
1240 . ($self->QueueObj->SubjectTag || RT->Config->Get('rtname'))
1249 Returns an RT::Date object containing this ticket's due date
1256 my $time = RT::Date->new( $self->CurrentUser );
1258 # -1 is RT::Date slang for never
1259 if ( my $due = $self->Due ) {
1260 $time->Set( Format => 'sql', Value => $due );
1263 $time->Set( Format => 'unix', Value => -1 );
1273 Returns this ticket's due date as a human readable string.
1275 B<DEPRECATED> and will be removed in 4.4; use C<<
1276 $ticket->DueObj->AsString >> instead.
1283 Instead => "->DueObj->AsString",
1286 return $self->DueObj->AsString();
1293 Returns an RT::Date object of this ticket's 'resolved' time.
1300 my $time = RT::Date->new( $self->CurrentUser );
1301 $time->Set( Format => 'sql', Value => $self->Resolved );
1305 =head2 FirstActiveStatus
1307 Returns the first active status that the ticket could transition to,
1308 according to its current Queue's lifecycle. May return undef if there
1309 is no such possible status to transition to, or we are already in it.
1310 This is used in L<RT::Action::AutoOpen>, for instance.
1314 sub FirstActiveStatus {
1317 my $lifecycle = $self->LifecycleObj;
1318 my $status = $self->Status;
1319 my @active = $lifecycle->Active;
1320 # no change if no active statuses in the lifecycle
1321 return undef unless @active;
1323 # no change if the ticket is already has first status from the list of active
1324 return undef if lc $status eq lc $active[0];
1326 my ($next) = grep $lifecycle->IsActive($_), $lifecycle->Transitions($status);
1330 =head2 FirstInactiveStatus
1332 Returns the first inactive status that the ticket could transition to,
1333 according to its current Queue's lifecycle. May return undef if there
1334 is no such possible status to transition to, or we are already in it.
1335 This is used in resolve action in UnsafeEmailCommands, for instance.
1339 sub FirstInactiveStatus {
1342 my $lifecycle = $self->LifecycleObj;
1343 my $status = $self->Status;
1344 my @inactive = $lifecycle->Inactive;
1345 # no change if no inactive statuses in the lifecycle
1346 return undef unless @inactive;
1348 # no change if the ticket is already has first status from the list of inactive
1349 return undef if lc $status eq lc $inactive[0];
1351 my ($next) = grep $lifecycle->IsInactive($_), $lifecycle->Transitions($status);
1357 Takes a date in ISO format or undef
1358 Returns a transaction id and a message
1359 The client calls "Start" to note that the project was started on the date in $date.
1360 A null date means "now"
1366 my $time = shift || 0;
1368 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
1369 return ( 0, $self->loc("Permission Denied") );
1372 #We create a date object to catch date weirdness
1373 my $time_obj = RT::Date->new( $self->CurrentUser() );
1375 $time_obj->Set( Format => 'ISO', Value => $time );
1378 $time_obj->SetToNow();
1381 return ( $self->_Set( Field => 'Started', Value => $time_obj->ISO ) );
1389 Returns an RT::Date object which contains this ticket's
1397 my $time = RT::Date->new( $self->CurrentUser );
1398 $time->Set( Format => 'sql', Value => $self->Started );
1406 Returns an RT::Date object which contains this ticket's
1414 my $time = RT::Date->new( $self->CurrentUser );
1415 $time->Set( Format => 'sql', Value => $self->Starts );
1423 Returns an RT::Date object which contains this ticket's
1431 my $time = RT::Date->new( $self->CurrentUser );
1432 $time->Set( Format => 'sql', Value => $self->Told );
1440 A convenience method that returns ToldObj->AsString
1442 B<DEPRECATED> and will be removed in 4.4; use C<<
1443 $ticket->ToldObj->AsString >> instead.
1450 Instead => "->ToldObj->AsString",
1453 if ( $self->Told ) {
1454 return $self->ToldObj->AsString();
1463 sub _DurationAsString {
1466 return "" unless $value;
1467 return RT::Date->new( $self->CurrentUser )
1468 ->DurationAsString( $value * 60 );
1471 =head2 TimeWorkedAsString
1473 Returns the amount of time worked on this ticket as a text string.
1477 sub TimeWorkedAsString {
1479 return $self->_DurationAsString( $self->TimeWorked );
1482 =head2 TimeLeftAsString
1484 Returns the amount of time left on this ticket as a text string.
1488 sub TimeLeftAsString {
1490 return $self->_DurationAsString( $self->TimeLeft );
1493 =head2 TimeEstimatedAsString
1495 Returns the amount of time estimated on this ticket as a text string.
1499 sub TimeEstimatedAsString {
1501 return $self->_DurationAsString( $self->TimeEstimated );
1509 Comment on this ticket.
1510 Takes a hash with the following attributes:
1511 If MIMEObj is undefined, Content will be used to build a MIME::Entity for this
1514 MIMEObj, TimeTaken, CcMessageTo, BccMessageTo, Content, DryRun
1516 If DryRun is defined, this update WILL NOT BE RECORDED. Scrips will not be committed.
1517 They will, however, be prepared and you'll be able to access them through the TransactionObj
1519 Returns: Transaction id, Error Message, Transaction Object
1520 (note the different order from Create()!)
1527 my %args = ( CcMessageTo => undef,
1528 BccMessageTo => undef,
1535 unless ( ( $self->CurrentUserHasRight('CommentOnTicket') )
1536 or ( $self->CurrentUserHasRight('ModifyTicket') ) ) {
1537 return ( 0, $self->loc("Permission Denied"), undef );
1539 $args{'NoteType'} = 'Comment';
1541 $RT::Handle->BeginTransaction();
1542 if ($args{'DryRun'}) {
1543 $args{'CommitScrips'} = 0;
1546 my @results = $self->_RecordNote(%args);
1547 if ($args{'DryRun'}) {
1548 $RT::Handle->Rollback();
1550 $RT::Handle->Commit();
1559 Correspond on this ticket.
1560 Takes a hashref with the following attributes:
1563 MIMEObj, TimeTaken, CcMessageTo, BccMessageTo, Content, DryRun
1565 if there's no MIMEObj, Content is used to build a MIME::Entity object
1567 If DryRun is defined, this update WILL NOT BE RECORDED. Scrips will not be committed.
1568 They will, however, be prepared and you'll be able to access them through the TransactionObj
1570 Returns: Transaction id, Error Message, Transaction Object
1571 (note the different order from Create()!)
1578 my %args = ( CcMessageTo => undef,
1579 BccMessageTo => undef,
1585 unless ( ( $self->CurrentUserHasRight('ReplyToTicket') )
1586 or ( $self->CurrentUserHasRight('ModifyTicket') ) ) {
1587 return ( 0, $self->loc("Permission Denied"), undef );
1589 $args{'NoteType'} = 'Correspond';
1591 $RT::Handle->BeginTransaction();
1592 if ($args{'DryRun'}) {
1593 $args{'CommitScrips'} = 0;
1596 my @results = $self->_RecordNote(%args);
1598 unless ( $results[0] ) {
1599 $RT::Handle->Rollback();
1603 #Set the last told date to now if this isn't mail from the requestor.
1604 #TODO: Note that this will wrongly ack mail from any non-requestor as a "told"
1605 unless ( $self->IsRequestor($self->CurrentUser->id) ) {
1607 $squelch{$_}++ for map {$_->Content} $self->SquelchMailTo, $results[2]->SquelchMailTo;
1609 if grep {not $squelch{$_}} $self->Requestors->MemberEmailAddresses;
1612 if ($args{'DryRun'}) {
1613 $RT::Handle->Rollback();
1615 $RT::Handle->Commit();
1626 the meat of both comment and correspond.
1628 Performs no access control checks. hence, dangerous.
1635 CcMessageTo => undef,
1636 BccMessageTo => undef,
1641 NoteType => 'Correspond',
1644 SquelchMailTo => undef,
1649 unless ( $args{'MIMEObj'} || $args{'Content'} ) {
1650 return ( 0, $self->loc("No message attached"), undef );
1653 unless ( $args{'MIMEObj'} ) {
1654 my $data = ref $args{'Content'}? $args{'Content'} : [ $args{'Content'} ];
1655 $args{'MIMEObj'} = MIME::Entity->build(
1656 Type => "text/plain",
1658 Data => [ map {Encode::encode("UTF-8", $_)} @{$data} ],
1662 $args{'MIMEObj'}->head->replace('X-RT-Interface' => 'API')
1663 unless $args{'MIMEObj'}->head->get('X-RT-Interface');
1665 # convert text parts into utf-8
1666 RT::I18N::SetMIMEEntityToUTF8( $args{'MIMEObj'} );
1668 # If we've been passed in CcMessageTo and BccMessageTo fields,
1669 # add them to the mime object for passing on to the transaction handler
1670 # The "NotifyOtherRecipients" scripAction will look for RT-Send-Cc: and
1671 # RT-Send-Bcc: headers
1674 foreach my $type (qw/Cc Bcc/) {
1675 if ( defined $args{ $type . 'MessageTo' } ) {
1677 my $addresses = join ', ', (
1678 map { RT::User->CanonicalizeEmailAddress( $_->address ) }
1679 Email::Address->parse( $args{ $type . 'MessageTo' } ) );
1680 $args{'MIMEObj'}->head->replace( 'RT-Send-' . $type, Encode::encode( "UTF-8", $addresses ) );
1684 foreach my $argument (qw(Encrypt Sign)) {
1685 $args{'MIMEObj'}->head->replace(
1686 "X-RT-$argument" => $args{ $argument } ? 1 : 0
1687 ) if defined $args{ $argument };
1690 # If this is from an external source, we need to come up with its
1691 # internal Message-ID now, so all emails sent because of this
1692 # message have a common Message-ID
1693 my $org = RT->Config->Get('Organization');
1694 my $msgid = Encode::decode( "UTF-8", $args{'MIMEObj'}->head->get('Message-ID') );
1695 unless (defined $msgid && $msgid =~ /<(rt-.*?-\d+-\d+)\.(\d+-0-0)\@\Q$org\E>/) {
1696 $args{'MIMEObj'}->head->replace(
1697 'RT-Message-ID' => Encode::encode( "UTF-8",
1698 RT::Interface::Email::GenMessageId( Ticket => $self )
1703 #Record the correspondence (write the transaction)
1704 my ( $Trans, $msg, $TransObj ) = $self->_NewTransaction(
1705 Type => $args{'NoteType'},
1706 Data => ( Encode::decode( "UTF-8", $args{'MIMEObj'}->head->get('Subject') ) || 'No Subject' ),
1707 TimeTaken => $args{'TimeTaken'},
1708 MIMEObj => $args{'MIMEObj'},
1709 CommitScrips => $args{'CommitScrips'},
1710 SquelchMailTo => $args{'SquelchMailTo'},
1711 CustomFields => $args{'CustomFields'},
1715 $RT::Logger->err("$self couldn't init a transaction $msg");
1716 return ( $Trans, $self->loc("Message could not be recorded"), undef );
1719 if ($args{NoteType} eq "Comment") {
1720 $msg = $self->loc("Comments added");
1722 $msg = $self->loc("Correspondence added");
1724 return ( $Trans, $msg, $TransObj );
1730 Builds a MIME object from the given C<UpdateSubject> and
1731 C<UpdateContent>, then calls L</Comment> or L</Correspond> with
1732 C<< DryRun => 1 >>, and returns the transaction so produced.
1740 if (($args{'UpdateType'} || $args{Action}) =~ /^respon(d|se)$/i ) {
1741 $action = 'Correspond';
1743 $action = 'Comment';
1746 my $Message = MIME::Entity->build(
1747 Subject => defined $args{UpdateSubject} ? Encode::encode( "UTF-8", $args{UpdateSubject} ) : "",
1748 Type => 'text/plain',
1750 Data => Encode::encode("UTF-8", $args{'UpdateContent'} || ""),
1753 my ( $Transaction, $Description, $Object ) = $self->$action(
1754 CcMessageTo => $args{'UpdateCc'},
1755 BccMessageTo => $args{'UpdateBcc'},
1756 MIMEObj => $Message,
1757 TimeTaken => $args{'UpdateTimeWorked'},
1759 SquelchMailTo => $args{'SquelchMailTo'},
1761 unless ( $Transaction ) {
1762 $RT::Logger->error("Couldn't fire '$action' action: $Description");
1770 Prepares a MIME mesage with the given C<Subject>, C<Cc>, and
1771 C<Content>, then calls L</Create> with C<< DryRun => 1 >> and returns
1772 the resulting L<RT::Transaction>.
1779 my $Message = MIME::Entity->build(
1780 Subject => defined $args{Subject} ? Encode::encode( "UTF-8", $args{'Subject'} ) : "",
1781 (defined $args{'Cc'} ?
1782 ( Cc => Encode::encode( "UTF-8", $args{'Cc'} ) ) : ()),
1783 Type => 'text/plain',
1785 Data => Encode::encode( "UTF-8", $args{'Content'} || ""),
1788 my ( $Transaction, $Object, $Description ) = $self->Create(
1789 Type => $args{'Type'} || 'ticket',
1790 Queue => $args{'Queue'},
1791 Owner => $args{'Owner'},
1792 Requestor => $args{'Requestors'},
1794 AdminCc => $args{'AdminCc'},
1795 InitialPriority => $args{'InitialPriority'},
1796 FinalPriority => $args{'FinalPriority'},
1797 TimeLeft => $args{'TimeLeft'},
1798 TimeEstimated => $args{'TimeEstimated'},
1799 TimeWorked => $args{'TimeWorked'},
1800 Subject => $args{'Subject'},
1801 Status => $args{'Status'},
1802 MIMEObj => $Message,
1805 unless ( $Transaction ) {
1806 $RT::Logger->error("Couldn't fire Create action: $Description");
1817 #TODO: Field isn't the right thing here. but I ahave no idea what mnemonic ---
1820 my $type = shift || "";
1822 my $cache_key = "$field$type";
1823 return $self->{ $cache_key } if $self->{ $cache_key };
1825 my $links = $self->{ $cache_key }
1826 = RT::Links->new( $self->CurrentUser );
1827 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
1828 $links->Limit( FIELD => 'id', VALUE => 0, SUBCLAUSE => 'acl' );
1832 # Maybe this ticket is a merge ticket
1833 my $limit_on = 'Local'. $field;
1834 # at least to myself
1838 VALUE => [ $self->id, $self->Merged ],
1850 MergeInto take the id of the ticket to merge this ticket into.
1856 my $ticket_id = shift;
1858 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
1859 return ( 0, $self->loc("Permission Denied") );
1862 # Load up the new ticket.
1863 my $MergeInto = RT::Ticket->new($self->CurrentUser);
1864 $MergeInto->Load($ticket_id);
1866 # make sure it exists.
1867 unless ( $MergeInto->Id ) {
1868 return ( 0, $self->loc("New ticket doesn't exist") );
1871 # Can't merge into yourself
1872 if ( $MergeInto->Id == $self->Id ) {
1873 return ( 0, $self->loc("Can't merge a ticket into itself") );
1876 # Make sure the current user can modify the new ticket.
1877 unless ( $MergeInto->CurrentUserHasRight('ModifyTicket') ) {
1878 return ( 0, $self->loc("Permission Denied") );
1881 delete $MERGE_CACHE{'effective'}{ $self->id };
1882 delete @{ $MERGE_CACHE{'merged'} }{
1883 $ticket_id, $MergeInto->id, $self->id
1886 $RT::Handle->BeginTransaction();
1888 my ($ok, $msg) = $self->_MergeInto( $MergeInto );
1890 $RT::Handle->Commit() if $ok;
1897 my $MergeInto = shift;
1900 # We use EffectiveId here even though it duplicates information from
1901 # the links table becasue of the massive performance hit we'd take
1902 # by trying to do a separate database query for merge info everytime
1905 #update this ticket's effective id to the new ticket's id.
1906 my ( $id_val, $id_msg ) = $self->__Set(
1907 Field => 'EffectiveId',
1908 Value => $MergeInto->Id()
1912 $RT::Handle->Rollback();
1913 return ( 0, $self->loc("Merge failed. Couldn't set EffectiveId") );
1916 ( $id_val, $id_msg ) = $self->__Set( Field => 'IsMerged', Value => 1 );
1918 $RT::Handle->Rollback();
1919 return ( 0, $self->loc("Merge failed. Couldn't set IsMerged") );
1922 my $force_status = $self->LifecycleObj->DefaultOnMerge;
1923 if ( $force_status && $force_status ne $self->__Value('Status') ) {
1924 my ( $status_val, $status_msg )
1925 = $self->__Set( Field => 'Status', Value => $force_status );
1927 unless ($status_val) {
1928 $RT::Handle->Rollback();
1930 "Couldn't set status to $force_status. RT's Database may be inconsistent."
1932 return ( 0, $self->loc("Merge failed. Couldn't set Status") );
1936 # update all the links that point to that old ticket
1937 my $old_links_to = RT::Links->new($self->CurrentUser);
1938 $old_links_to->Limit(FIELD => 'Target', VALUE => $self->URI);
1941 while (my $link = $old_links_to->Next) {
1942 if (exists $old_seen{$link->Base."-".$link->Type}) {
1945 elsif ($link->Base eq $MergeInto->URI) {
1948 # First, make sure the link doesn't already exist. then move it over.
1949 my $tmp = RT::Link->new(RT->SystemUser);
1950 $tmp->LoadByCols(Base => $link->Base, Type => $link->Type, LocalTarget => $MergeInto->id);
1954 $link->SetTarget($MergeInto->URI);
1955 $link->SetLocalTarget($MergeInto->id);
1957 $old_seen{$link->Base."-".$link->Type} =1;
1962 my $old_links_from = RT::Links->new($self->CurrentUser);
1963 $old_links_from->Limit(FIELD => 'Base', VALUE => $self->URI);
1965 while (my $link = $old_links_from->Next) {
1966 if (exists $old_seen{$link->Type."-".$link->Target}) {
1969 if ($link->Target eq $MergeInto->URI) {
1972 # First, make sure the link doesn't already exist. then move it over.
1973 my $tmp = RT::Link->new(RT->SystemUser);
1974 $tmp->LoadByCols(Target => $link->Target, Type => $link->Type, LocalBase => $MergeInto->id);
1978 $link->SetBase($MergeInto->URI);
1979 $link->SetLocalBase($MergeInto->id);
1980 $old_seen{$link->Type."-".$link->Target} =1;
1986 # Update time fields
1987 foreach my $type (qw(TimeEstimated TimeWorked TimeLeft)) {
1990 Value => ( $MergeInto->$type() || 0 ) + ( $self->$type() || 0 ),
1991 RecordTransaction => 0,
1995 # add all of this ticket's watchers to that ticket.
1996 for my $role ($self->Roles) {
1997 next if $self->RoleGroup($role)->SingleMemberRoleGroup;
1998 my $people = $self->RoleGroup($role)->MembersObj;
1999 while ( my $watcher = $people->Next ) {
2000 my ($val, $msg) = $MergeInto->AddRoleMember(
2003 PrincipalId => $watcher->MemberId,
2004 InsideTransaction => 1,
2007 $RT::Logger->debug($msg);
2012 #find all of the tickets that were merged into this ticket.
2013 my $old_mergees = RT::Tickets->new( $self->CurrentUser );
2014 $old_mergees->Limit(
2015 FIELD => 'EffectiveId',
2020 # update their EffectiveId fields to the new ticket's id
2021 while ( my $ticket = $old_mergees->Next() ) {
2022 my ( $val, $msg ) = $ticket->__Set(
2023 Field => 'EffectiveId',
2024 Value => $MergeInto->Id()
2028 #make a new link: this ticket is merged into that other ticket.
2029 $self->AddLink( Type => 'MergedInto', Target => $MergeInto->Id());
2031 $MergeInto->_SetLastUpdated;
2033 return ( 1, $self->loc("Merge Successful") );
2038 Returns list of tickets' ids that's been merged into this ticket.
2046 return @{ $MERGE_CACHE{'merged'}{ $id } }
2047 if $MERGE_CACHE{'merged'}{ $id };
2049 my $mergees = RT::Tickets->new( $self->CurrentUser );
2050 $mergees->LimitField(
2051 FIELD => 'EffectiveId',
2054 $mergees->LimitField(
2059 return @{ $MERGE_CACHE{'merged'}{ $id } ||= [] }
2060 = map $_->id, @{ $mergees->ItemsArrayRef || [] };
2069 Takes nothing and returns an RT::User object of
2077 #If this gets ACLed, we lose on a rights check in User.pm and
2078 #get deep recursion. if we need ACLs here, we need
2079 #an equiv without ACLs
2081 my $owner = RT::User->new( $self->CurrentUser );
2082 $owner->Load( $self->__Value('Owner') );
2084 #Return the owner object
2090 =head2 OwnerAsString
2092 Returns the owner's email address
2098 return ( $self->OwnerObj->EmailAddress );
2106 Takes two arguments:
2107 the Id or Name of the owner
2108 and (optionally) the type of the SetOwner Transaction. It defaults
2109 to 'Set'. 'Steal' is also a valid option.
2116 my $NewOwner = shift;
2117 my $Type = shift || "Set";
2119 $RT::Handle->BeginTransaction();
2121 $self->_SetLastUpdated(); # lock the ticket
2122 $self->Load( $self->id ); # in case $self changed while waiting for lock
2124 my $OldOwnerObj = $self->OwnerObj;
2126 my $NewOwnerObj = RT::User->new( $self->CurrentUser );
2127 $NewOwnerObj->Load( $NewOwner );
2129 my ( $val, $msg ) = $self->CurrentUserCanSetOwner(
2130 NewOwnerObj => $NewOwnerObj,
2134 $RT::Handle->Rollback();
2135 return ( $val, $msg );
2138 ($val, $msg ) = $self->OwnerGroup->_AddMember(
2139 PrincipalId => $NewOwnerObj->PrincipalId,
2140 InsideTransaction => 1,
2144 $RT::Handle->Rollback;
2145 return ( 0, $self->loc("Could not change owner: [_1]", $msg) );
2148 $msg = $self->loc( "Owner changed from [_1] to [_2]",
2149 $OldOwnerObj->Name, $NewOwnerObj->Name );
2151 $RT::Handle->Commit();
2153 return ( $val, $msg );
2156 =head2 CurrentUserCanSetOwner
2158 Confirm the current user can set the owner of the current ticket.
2160 There are several different rights to manage owner changes and
2161 this method evaluates these rights, guided by parameters provided.
2163 This method evaluates these rights in the context of the state of
2164 the current ticket. For example, it evaluates Take for tickets that
2165 are owned by Nobody because that is the context appropriate for the
2166 TakeTicket right. If you need to strictly test a user for a right,
2167 use HasRight to check for the right directly.
2169 For some custom types of owner changes (C<Take> and C<Steal>), it also
2170 verifies that those actions are possible given the current ticket owner.
2172 =head3 Rights to Set Owner
2174 The current user can set or change the Owner field in the following
2181 ReassignTicket unconditionally grants the right to set the owner
2182 to any user who has OwnTicket. This can be used to break an
2183 Owner lock held by another user (see below) and can be a convenient
2184 right for managers or administrators who need to assign tickets
2185 without necessarily owning them.
2189 ModifyTicket grants the right to set the owner to any user who
2190 has OwnTicket, provided the ticket is currently owned by the current
2191 user or is not owned (owned by Nobody). (See the details on the Force
2192 parameter below for exceptions to this.)
2196 If the ticket is currently not owned (owned by Nobody),
2197 TakeTicket is sufficient to set the owner to yourself (but not
2198 an arbitrary person), but only if you have OwnTicket. It is
2199 thus a subset of the possible changes provided by ModifyTicket.
2200 This exists to allow granting TakeTicket freely, and
2201 the broader ModifyTicket only to Owners.
2205 If the ticket is currently owned by someone who is not you or
2206 Nobody, StealTicket is sufficient to set the owner to yourself,
2207 but only if you have OwnTicket. This is hence non-overlapping
2208 with the changes provided by ModifyTicket, and is used to break
2209 a lock held by another user.
2215 This method returns ($result, $message) with $result containing
2216 true or false indicating if the current user can set owner and $message
2217 containing a message, typically in the case of a false response.
2219 If called with no parameters, this method determines if the current
2220 user could set the owner of the current ticket given any
2221 permutation of the rights described above. This can be useful
2222 when determining whether to make owner-setting options available
2225 This method accepts the following parameters as a paramshash:
2229 =item C<NewOwnerObj>
2231 Optional; an L<RT::User> object representing the proposed new owner of
2236 Optional; the type of set owner operation. Valid values are C<Take>,
2237 C<Steal>, or C<Force>. Note that if the type is C<Take>, this method
2238 will return false if the current user is already the owner; similarly,
2239 it will return false for C<Steal> if the ticket has no owner or the
2240 owner is the current user.
2244 As noted above, there are exceptions to the standard ticket-based rights
2245 described here. The Force option allows for these and is used
2246 when moving tickets between queues, for reminders (because the full
2247 owner rights system is too complex for them), and optionally during
2252 sub CurrentUserCanSetOwner {
2254 my %args = ( Type => '',
2256 my $OldOwnerObj = $self->OwnerObj;
2258 $args{NewOwnerObj} ||= $self->CurrentUser->UserObj
2259 if $args{Type} eq "Take" or $args{Type} eq "Steal";
2261 # Confirm rights for new owner if we got one
2262 if ( $args{'NewOwnerObj'} ){
2263 my ($ok, $message) = $self->_NewOwnerCanOwnTicket($args{'NewOwnerObj'}, $OldOwnerObj);
2264 return ($ok, $message) if not $ok;
2267 # ReassignTicket allows you to SetOwner, but we also need to check ticket's
2268 # current owner for Take and Steal Types
2269 return ( 1, undef ) if $self->CurrentUserHasRight('ReassignTicket')
2270 && $args{Type} ne 'Take' && $args{Type} ne 'Steal';
2273 if ( $OldOwnerObj->Id == RT->Nobody->Id ) {
2275 # Steal is not applicable for unowned tickets.
2276 if ( $args{'Type'} eq 'Steal' ){
2277 return ( 0, $self->loc("You can only steal a ticket owned by someone else") )
2280 # Can set owner to yourself with ModifyTicket, ReassignTicket,
2281 # or TakeTicket; in all of these cases, OwnTicket is checked by
2282 # _NewOwnerCanOwnTicket above.
2283 if ( $args{'Type'} eq 'Take'
2284 or ( $args{'NewOwnerObj'}
2285 and $args{'NewOwnerObj'}->id == $self->CurrentUser->id )) {
2286 unless ( $self->CurrentUserHasRight('ModifyTicket')
2287 or $self->CurrentUserHasRight('ReassignTicket')
2288 or $self->CurrentUserHasRight('TakeTicket') ) {
2289 return ( 0, $self->loc("Permission Denied") );
2292 # Nobody -> someone else requires ModifyTicket or ReassignTicket
2293 unless ( $self->CurrentUserHasRight('ModifyTicket')
2294 or $self->CurrentUserHasRight('ReassignTicket') ) {
2295 return ( 0, $self->loc("Permission Denied") );
2300 # Ticket is owned by someone else
2301 # Can set owner to yourself with ModifyTicket or StealTicket
2303 elsif ( $OldOwnerObj->Id != RT->Nobody->Id
2304 && $OldOwnerObj->Id != $self->CurrentUser->id ) {
2306 unless ( $self->CurrentUserHasRight('ModifyTicket')
2307 || $self->CurrentUserHasRight('ReassignTicket')
2308 || $self->CurrentUserHasRight('StealTicket') ) {
2309 return ( 0, $self->loc("Permission Denied") )
2312 if ( $args{'Type'} eq 'Steal' || $args{'Type'} eq 'Force' ){
2313 return ( 1, undef ) if $self->CurrentUserHasRight('OwnTicket');
2314 return ( 0, $self->loc("Permission Denied") );
2317 # Not a steal or force
2318 if ( $args{'Type'} eq 'Take'
2319 or ( $args{'NewOwnerObj'}
2320 and $args{'NewOwnerObj'}->id == $self->CurrentUser->id )) {
2321 return ( 0, $self->loc("You can only take tickets that are unowned") );
2324 unless ( $self->CurrentUserHasRight('ReassignTicket') ) {
2325 return ( 0, $self->loc( "You can only reassign tickets that you own or that are unowned"));
2329 # You own the ticket
2330 # Untake falls through to here, so we don't need to explicitly handle that Type
2332 if ( $args{'Type'} eq 'Take' || $args{'Type'} eq 'Steal' ) {
2333 return ( 0, $self->loc("You already own this ticket") );
2336 unless ( $self->CurrentUserHasRight('ModifyTicket')
2337 || $self->CurrentUserHasRight('ReassignTicket') ) {
2338 return ( 0, $self->loc("Permission Denied") );
2342 return ( 1, undef );
2345 # Verify the proposed new owner can own the ticket.
2347 sub _NewOwnerCanOwnTicket {
2349 my $NewOwnerObj = shift;
2350 my $OldOwnerObj = shift;
2352 unless ( $NewOwnerObj->Id ) {
2353 return ( 0, $self->loc("That user does not exist") );
2356 # The proposed new owner can't own the ticket
2357 if ( !$NewOwnerObj->HasRight( Right => 'OwnTicket', Object => $self ) ){
2358 return ( 0, $self->loc("That user may not own tickets in that queue") );
2361 # Ticket's current owner is the same as the new owner, nothing to do
2362 elsif ( $NewOwnerObj->Id == $OldOwnerObj->Id ) {
2363 return ( 0, $self->loc("That user already owns that ticket") );
2371 A convenince method to set the ticket's owner to the current user
2377 return ( $self->SetOwner( $self->CurrentUser->Id, 'Take' ) );
2384 Convenience method to set the owner to 'nobody' if the current user is the owner.
2390 return ( $self->SetOwner( RT->Nobody->UserObj->Id, 'Untake' ) );
2397 A convenience method to change the owner of the current ticket to the
2398 current user. Even if it's owned by another user.
2405 if ( $self->IsOwner( $self->CurrentUser ) ) {
2406 return ( 0, $self->loc("You already own this ticket") );
2409 return ( $self->SetOwner( $self->CurrentUser->Id, 'Steal' ) );
2415 =head2 SetStatus STATUS
2417 Set this ticket's status.
2419 Alternatively, you can pass in a list of named parameters (Status => STATUS, Force => FORCE, SetStarted => SETSTARTED ).
2420 If FORCE is true, ignore unresolved dependencies and force a status change.
2421 if SETSTARTED is true (it's the default value), set Started to current datetime if Started
2422 is not set and the status is changed from initial to not initial.
2430 $args{Status} = shift;
2436 # this only allows us to SetStarted, not we must SetStarted.
2437 # this option was added for rtir initially
2438 $args{SetStarted} = 1 unless exists $args{SetStarted};
2440 my ($valid, $msg) = $self->ValidateStatusChange($args{Status});
2441 return ($valid, $msg) unless $valid;
2443 my $lifecycle = $self->LifecycleObj;
2446 && !$lifecycle->IsInactive($self->Status)
2447 && $lifecycle->IsInactive($args{Status})
2448 && $self->HasUnresolvedDependencies )
2450 return ( 0, $self->loc('That ticket has unresolved dependencies') );
2453 return $self->_SetStatus(
2454 Status => $args{Status},
2455 SetStarted => $args{SetStarted},
2464 RecordTransaction => 1,
2465 Lifecycle => $self->LifecycleObj,
2468 $args{Status} = lc $args{Status} if defined $args{Status};
2469 $args{NewLifecycle} ||= $args{Lifecycle};
2471 my $now = RT::Date->new( $self->CurrentUser );
2474 my $raw_started = RT::Date->new(RT->SystemUser);
2475 $raw_started->Set(Format => 'ISO', Value => $self->__Value('Started'));
2477 my $old = $self->__Value('Status');
2479 # If we're changing the status from new, record that we've started
2480 if ( $args{SetStarted}
2481 && $args{Lifecycle}->IsInitial($old)
2482 && !$args{NewLifecycle}->IsInitial($args{Status})
2483 && !$raw_started->IsSet) {
2484 # Set the Started time to "now"
2488 RecordTransaction => 0
2492 # When we close a ticket, set the 'Resolved' attribute to now.
2493 # It's misnamed, but that's just historical.
2494 if ( $args{NewLifecycle}->IsInactive($args{Status}) ) {
2496 Field => 'Resolved',
2498 RecordTransaction => 0,
2502 # Actually update the status
2503 my ($val, $msg)= $self->_Set(
2505 Value => $args{Status},
2508 TransactionType => 'Status',
2509 RecordTransaction => $args{RecordTransaction},
2511 return ($val, $msg);
2518 my $taken = ($value||0) - ($self->__Value('TimeWorked')||0);
2521 Field => 'TimeWorked',
2523 TimeTaken => $taken,
2529 Takes no arguments. Marks this ticket for garbage collection
2535 unless ( $self->LifecycleObj->IsValid('deleted') ) {
2536 return (0, $self->loc('Delete operation is disabled by lifecycle configuration') ); #loc
2538 return ( $self->SetStatus('deleted') );
2542 =head2 SetTold ISO [TIMETAKEN]
2544 Updates the told and records a transaction
2551 $told = shift if (@_);
2552 my $timetaken = shift || 0;
2554 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
2555 return ( 0, $self->loc("Permission Denied") );
2558 my $datetold = RT::Date->new( $self->CurrentUser );
2560 $datetold->Set( Format => 'iso',
2564 $datetold->SetToNow();
2567 return ( $self->_Set( Field => 'Told',
2568 Value => $datetold->ISO,
2569 TimeTaken => $timetaken,
2570 TransactionType => 'Told' ) );
2575 Updates the told without a transaction or acl check. Useful when we're sending replies.
2582 my $now = RT::Date->new( $self->CurrentUser );
2585 #use __Set to get no ACLs ;)
2586 return ( $self->__Set( Field => 'Told',
2587 Value => $now->ISO ) );
2597 my $uid = $self->CurrentUser->id;
2598 my $attr = $self->FirstAttribute( "User-". $uid ."-SeenUpTo" );
2599 return if $attr && $attr->Content gt $self->LastUpdated;
2601 my $txns = $self->Transactions;
2602 $txns->Limit( FIELD => 'Type', VALUE => 'Comment' );
2603 $txns->Limit( FIELD => 'Type', VALUE => 'Correspond' );
2604 $txns->Limit( FIELD => 'Creator', OPERATOR => '!=', VALUE => $uid );
2608 VALUE => $attr->Content
2610 $txns->RowsPerPage(1);
2611 return $txns->First;
2614 =head2 RanTransactionBatch
2616 Acts as a guard around running TransactionBatch scrips.
2618 Should be false until you enter the code that runs TransactionBatch scrips
2620 Accepts an optional argument to indicate that TransactionBatch Scrips should no longer be run on this object.
2624 sub RanTransactionBatch {
2628 if ( defined $val ) {
2629 return $self->{_RanTransactionBatch} = $val;
2631 return $self->{_RanTransactionBatch};
2637 =head2 TransactionBatch
2639 Returns an array reference of all transactions created on this ticket during
2640 this ticket object's lifetime or since last application of a batch, or undef
2643 Only works when the C<UseTransactionBatch> config option is set to true.
2647 sub TransactionBatch {
2649 return $self->{_TransactionBatch};
2652 =head2 ApplyTransactionBatch
2654 Applies scrips on the current batch of transactions and shinks it. Usually
2655 batch is applied when object is destroyed, but in some cases it's too late.
2659 sub ApplyTransactionBatch {
2662 my $batch = $self->TransactionBatch;
2663 return unless $batch && @$batch;
2665 $self->_ApplyTransactionBatch;
2667 $self->{_TransactionBatch} = [];
2670 sub _ApplyTransactionBatch {
2673 return if $self->RanTransactionBatch;
2674 $self->RanTransactionBatch(1);
2676 my $still_exists = RT::Ticket->new( RT->SystemUser );
2677 $still_exists->Load( $self->Id );
2678 if (not $still_exists->Id) {
2679 # The ticket has been removed from the database, but we still
2680 # have pending TransactionBatch txns for it. Unfortunately,
2681 # because it isn't in the DB anymore, attempting to run scrips
2682 # on it may produce unpredictable results; simply drop the
2683 # batched transactions.
2684 $RT::Logger->warning("TransactionBatch was fired on a ticket that no longer exists; unable to run scrips! Call ->ApplyTransactionBatch before shredding the ticket, for consistent results.");
2688 my $batch = $self->TransactionBatch;
2691 my $types = join ',', grep !$seen{$_}++, grep defined, map $_->__Value('Type'), grep defined, @{$batch};
2694 RT::Scrips->new(RT->SystemUser)->Apply(
2695 Stage => 'TransactionBatch',
2697 TransactionObj => $batch->[0],
2701 # Entry point of the rule system
2702 my $rules = RT::Ruleset->FindAllRules(
2703 Stage => 'TransactionBatch',
2705 TransactionObj => $batch->[0],
2708 RT::Ruleset->CommitRules($rules);
2714 # DESTROY methods need to localize $@, or it may unset it. This
2715 # causes $m->abort to not bubble all of the way up. See perlbug
2716 # http://rt.perl.org/rt3/Ticket/Display.html?id=17650
2719 # The following line eliminates reentrancy.
2720 # It protects against the fact that perl doesn't deal gracefully
2721 # when an object's refcount is changed in its destructor.
2722 return if $self->{_Destroyed}++;
2724 if (in_global_destruction()) {
2725 unless ($ENV{'HARNESS_ACTIVE'}) {
2726 warn "Too late to safely run transaction-batch scrips!"
2727 ." This is typically caused by using ticket objects"
2728 ." at the top-level of a script which uses the RT API."
2729 ." Be sure to explicitly undef such ticket objects,"
2730 ." or put them inside of a lexical scope.";
2735 return $self->ApplyTransactionBatch;
2741 sub _OverlayAccessible {
2743 EffectiveId => { 'read' => 1, 'write' => 1, 'public' => 1 },
2744 Queue => { 'read' => 1, 'write' => 1 },
2745 Requestors => { 'read' => 1, 'write' => 1 },
2746 Owner => { 'read' => 1, 'write' => 1 },
2747 Subject => { 'read' => 1, 'write' => 1 },
2748 InitialPriority => { 'read' => 1, 'write' => 1 },
2749 FinalPriority => { 'read' => 1, 'write' => 1 },
2750 Priority => { 'read' => 1, 'write' => 1 },
2751 Status => { 'read' => 1, 'write' => 1 },
2752 TimeEstimated => { 'read' => 1, 'write' => 1 },
2753 TimeWorked => { 'read' => 1, 'write' => 1 },
2754 TimeLeft => { 'read' => 1, 'write' => 1 },
2755 Told => { 'read' => 1, 'write' => 1 },
2756 Resolved => { 'read' => 1 },
2757 Type => { 'read' => 1 },
2758 Starts => { 'read' => 1, 'write' => 1 },
2759 Started => { 'read' => 1, 'write' => 1 },
2760 Due => { 'read' => 1, 'write' => 1 },
2761 Creator => { 'read' => 1, 'auto' => 1 },
2762 Created => { 'read' => 1, 'auto' => 1 },
2763 LastUpdatedBy => { 'read' => 1, 'auto' => 1 },
2764 LastUpdated => { 'read' => 1, 'auto' => 1 }
2774 my %args = ( Field => undef,
2777 RecordTransaction => 1,
2779 TransactionType => 'Set',
2782 if ($args{'CheckACL'}) {
2783 unless ( $self->CurrentUserHasRight('ModifyTicket')) {
2784 return ( 0, $self->loc("Permission Denied"));
2788 # Avoid ACL loops using _Value
2789 my $Old = $self->SUPER::_Value($args{'Field'});
2792 my ( $ret, $msg ) = $self->SUPER::_Set(
2793 Field => $args{'Field'},
2794 Value => $args{'Value'}
2796 return ( 0, $msg ) unless $ret;
2798 return ( $ret, $msg ) unless $args{'RecordTransaction'};
2801 ( $ret, $msg, $trans ) = $self->_NewTransaction(
2802 Type => $args{'TransactionType'},
2803 Field => $args{'Field'},
2804 NewValue => $args{'Value'},
2806 TimeTaken => $args{'TimeTaken'},
2809 # Ensure that we can read the transaction, even if the change
2810 # just made the ticket unreadable to us
2811 $trans->{ _object_is_readable } = 1;
2813 return ( $ret, scalar $trans->BriefDescription );
2820 Takes the name of a table column.
2821 Returns its value as a string, if the user passes an ACL check
2830 #if the field is public, return it.
2831 if ( $self->_Accessible( $field, 'public' ) ) {
2833 #$RT::Logger->debug("Skipping ACL check for $field");
2834 return ( $self->SUPER::_Value($field) );
2838 #If the current user doesn't have ACLs, don't let em at it.
2840 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
2843 return ( $self->SUPER::_Value($field) );
2849 Customization of L<RT::Record/Attachments> for tickets.
2860 my $res = RT::Attachments->new( $self->CurrentUser );
2861 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
2866 ENTRYAGGREGATOR => 'AND'
2871 my @columns = grep { not /^(Headers|Content)$/ }
2872 RT::Attachment->ReadableAttributes;
2873 push @columns, 'Headers' if $args{'WithHeaders'};
2874 push @columns, 'Content' if $args{'WithContent'};
2876 $res->Columns( @columns );
2877 my $txn_alias = $res->TransactionAlias;
2879 ALIAS => $txn_alias,
2880 FIELD => 'ObjectType',
2881 VALUE => ref($self),
2883 my $ticket_alias = $res->Join(
2884 ALIAS1 => $txn_alias,
2885 FIELD1 => 'ObjectId',
2886 TABLE2 => 'Tickets',
2890 ALIAS => $ticket_alias,
2891 FIELD => 'EffectiveId',
2897 =head2 TextAttachments
2899 Customization of L<RT::Record/TextAttachments> for tickets.
2903 sub TextAttachments {
2906 my $res = $self->SUPER::TextAttachments( @_ );
2907 unless ( $self->CurrentUserHasRight('ShowTicketComments') ) {
2908 # if the user may not see comments do not return them
2911 ALIAS => $res->TransactionAlias,
2923 =head2 _UpdateTimeTaken
2925 This routine will increment the timeworked counter. it should
2926 only be called from _NewTransaction
2930 sub _UpdateTimeTaken {
2932 my $Minutes = shift;
2935 if ( my $txn = $rest{'Transaction'} ) {
2936 return if $txn->__Value('Type') eq 'Set' && $txn->__Value('Field') eq 'TimeWorked';
2939 my $Total = $self->__Value("TimeWorked");
2940 $Total = ( $Total || 0 ) + ( $Minutes || 0 );
2942 Field => "TimeWorked",
2944 RecordTransaction => 0,
2951 =head2 CurrentUserCanSee
2953 Returns true if the current user can see the ticket, using ShowTicket
2957 sub CurrentUserCanSee {
2959 my ($what, $txn) = @_;
2960 return 0 unless $self->CurrentUserHasRight('ShowTicket');
2962 return 1 if $what ne "Transaction";
2964 # If it's a comment, we need to be extra special careful
2965 my $type = $txn->__Value('Type');
2966 if ( $type eq 'Comment' ) {
2967 unless ( $self->CurrentUserHasRight('ShowTicketComments') ) {
2970 } elsif ( $type eq 'CommentEmailRecord' ) {
2971 unless ( $self->CurrentUserHasRight('ShowTicketComments')
2972 && $self->CurrentUserHasRight('ShowOutgoingEmail') ) {
2975 } elsif ( $type eq 'EmailRecord' ) {
2976 unless ( $self->CurrentUserHasRight('ShowOutgoingEmail') ) {
2985 Return the Reminders object for this ticket. (It's an RT::Reminders object.)
2986 It isn't acutally a searchbuilder collection itself.
2993 unless ($self->{'__reminders'}) {
2994 $self->{'__reminders'} = RT::Reminders->new($self->CurrentUser);
2995 $self->{'__reminders'}->Ticket($self->id);
2997 return $self->{'__reminders'};
3006 Returns an RT::Transactions object of all transactions on this ticket
3013 my $transactions = RT::Transactions->new( $self->CurrentUser );
3015 #If the user has no rights, return an empty object
3016 if ( $self->CurrentUserHasRight('ShowTicket') ) {
3017 $transactions->LimitToTicket($self->id);
3019 # if the user may not see comments do not return them
3020 unless ( $self->CurrentUserHasRight('ShowTicketComments') ) {
3021 $transactions->Limit(
3027 $transactions->Limit(
3031 VALUE => "CommentEmailRecord",
3032 ENTRYAGGREGATOR => 'AND'
3037 $transactions->Limit(
3041 ENTRYAGGREGATOR => 'AND'
3045 return ($transactions);
3051 =head2 TransactionCustomFields
3053 Returns the custom fields that transactions on tickets will have.
3057 sub TransactionCustomFields {
3059 my $cfs = $self->QueueObj->TicketTransactionCustomFields;
3060 $cfs->SetContextObject( $self );
3065 =head2 LoadCustomFieldByIdentifier
3067 Finds and returns the custom field of the given name for the ticket,
3068 overriding L<RT::Record/LoadCustomFieldByIdentifier> to look for
3069 queue-specific CFs before global ones.
3073 sub LoadCustomFieldByIdentifier {
3077 return $self->SUPER::LoadCustomFieldByIdentifier($field)
3078 if ref $field or $field =~ /^\d+$/;
3080 my $cf = RT::CustomField->new( $self->CurrentUser );
3081 $cf->SetContextObject( $self );
3084 LookupType => $self->CustomFieldLookupType,
3085 ObjectId => $self->Queue,
3092 =head2 CustomFieldLookupType
3094 Returns the RT::Ticket lookup type, which can be passed to
3095 RT::CustomField->Create() via the 'LookupType' hash key.
3100 sub CustomFieldLookupType {
3101 "RT::Queue-RT::Ticket";
3104 =head2 ACLEquivalenceObjects
3106 This method returns a list of objects for which a user's rights also apply
3107 to this ticket. Generally, this is only the ticket's queue, but some RT
3108 extensions may make other objects available too.
3110 This method is called from L<RT::Principal/HasRight>.
3114 sub ACLEquivalenceObjects {
3116 return $self->QueueObj;
3120 =head2 ModifyLinkRight
3124 sub ModifyLinkRight { "ModifyTicket" }
3126 =head2 Forward Transaction => undef, To => '', Cc => '', Bcc => ''
3128 Forwards transaction with all attachments as 'message/rfc822'.
3135 Transaction => undef,
3141 ContentType => 'text/plain',
3147 unless ( $self->CurrentUserHasRight('ForwardMessage') ) {
3148 return ( 0, $self->loc("Permission Denied") );
3151 $args{$_} = join ", ", map { $_->format } RT::EmailParser->ParseEmailAddress( $args{$_} || '' ) for qw(To Cc Bcc);
3153 return (0, $self->loc("Can't forward: no valid email addresses specified") )
3154 unless grep {length $args{$_}} qw/To Cc Bcc/;
3156 my $mime = MIME::Entity->build(
3157 Type => $args{ContentType},
3158 Data => Encode::encode( "UTF-8", $args{Content} ),
3161 $mime->head->replace( $_ => Encode::encode('UTF-8',$args{$_} ) )
3162 for grep defined $args{$_}, qw(Subject To Cc Bcc);
3163 $mime->head->replace(
3164 From => Encode::encode( 'UTF-8',
3165 RT::Interface::Email::GetForwardFrom(
3166 Transaction => $args{Transaction},
3172 if ($args{'DryRun'}) {
3173 $RT::Handle->BeginTransaction();
3174 $args{'CommitScrips'} = 0;
3177 my ( $ret, $msg ) = $self->_NewTransaction(
3180 Type => 'Forward Transaction',
3181 Field => $args{Transaction}->id,
3184 Type => 'Forward Ticket',
3187 Data => join( ', ', grep { length } $args{To}, $args{Cc}, $args{Bcc} ),
3189 CommitScrips => $args{'CommitScrips'},
3193 $RT::Logger->error("Failed to create transaction: $msg");
3196 if ($args{'DryRun'}) {
3197 $RT::Handle->Rollback();
3199 return ( $ret, $self->loc('Message recorded') );
3206 Jesse Vincent, jesse@bestpractical.com
3214 sub Table {'Tickets'}
3223 Returns the current value of id.
3224 (In the database, id is stored as int(11).)
3232 Returns the current value of EffectiveId.
3233 (In the database, EffectiveId is stored as int(11).)
3237 =head2 SetEffectiveId VALUE
3240 Set EffectiveId to VALUE.
3241 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
3242 (In the database, EffectiveId will be stored as a int(11).)
3250 Returns the current value of Queue.
3251 (In the database, Queue is stored as int(11).)
3255 =head2 SetQueue VALUE
3259 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
3260 (In the database, Queue will be stored as a int(11).)
3268 Returns the current value of Type.
3269 (In the database, Type is stored as varchar(16).)
3273 =head2 SetType VALUE
3277 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
3278 (In the database, Type will be stored as a varchar(16).)
3284 =head2 IssueStatement
3286 Returns the current value of IssueStatement.
3287 (In the database, IssueStatement is stored as int(11).)
3291 =head2 SetIssueStatement VALUE
3294 Set IssueStatement to VALUE.
3295 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
3296 (In the database, IssueStatement will be stored as a int(11).)
3304 Returns the current value of Resolution.
3305 (In the database, Resolution is stored as int(11).)
3309 =head2 SetResolution VALUE
3312 Set Resolution to VALUE.
3313 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
3314 (In the database, Resolution will be stored as a int(11).)
3322 Returns the current value of Owner.
3323 (In the database, Owner is stored as int(11).)
3327 =head2 SetOwner VALUE
3331 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
3332 (In the database, Owner will be stored as a int(11).)
3340 Returns the current value of Subject.
3341 (In the database, Subject is stored as varchar(200).)
3345 =head2 SetSubject VALUE
3348 Set Subject to VALUE.
3349 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
3350 (In the database, Subject will be stored as a varchar(200).)
3356 =head2 InitialPriority
3358 Returns the current value of InitialPriority.
3359 (In the database, InitialPriority is stored as int(11).)
3363 =head2 SetInitialPriority VALUE
3366 Set InitialPriority to VALUE.
3367 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
3368 (In the database, InitialPriority will be stored as a int(11).)
3374 =head2 FinalPriority
3376 Returns the current value of FinalPriority.
3377 (In the database, FinalPriority is stored as int(11).)
3381 =head2 SetFinalPriority VALUE
3384 Set FinalPriority to VALUE.
3385 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
3386 (In the database, FinalPriority will be stored as a int(11).)
3394 Returns the current value of Priority.
3395 (In the database, Priority is stored as int(11).)
3399 =head2 SetPriority VALUE
3402 Set Priority to VALUE.
3403 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
3404 (In the database, Priority will be stored as a int(11).)
3410 =head2 TimeEstimated
3412 Returns the current value of TimeEstimated.
3413 (In the database, TimeEstimated is stored as int(11).)
3417 =head2 SetTimeEstimated VALUE
3420 Set TimeEstimated to VALUE.
3421 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
3422 (In the database, TimeEstimated will be stored as a int(11).)
3430 Returns the current value of TimeWorked.
3431 (In the database, TimeWorked is stored as int(11).)
3435 =head2 SetTimeWorked VALUE
3438 Set TimeWorked to VALUE.
3439 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
3440 (In the database, TimeWorked will be stored as a int(11).)
3448 Returns the current value of Status.
3449 (In the database, Status is stored as varchar(64).)
3453 =head2 SetStatus VALUE
3456 Set Status to VALUE.
3457 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
3458 (In the database, Status will be stored as a varchar(64).)
3466 Returns the current value of TimeLeft.
3467 (In the database, TimeLeft is stored as int(11).)
3471 =head2 SetTimeLeft VALUE
3474 Set TimeLeft to VALUE.
3475 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
3476 (In the database, TimeLeft will be stored as a int(11).)
3484 Returns the current value of Told.
3485 (In the database, Told is stored as datetime.)
3489 =head2 SetTold VALUE
3493 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
3494 (In the database, Told will be stored as a datetime.)
3502 Returns the current value of Starts.
3503 (In the database, Starts is stored as datetime.)
3507 =head2 SetStarts VALUE
3510 Set Starts to VALUE.
3511 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
3512 (In the database, Starts will be stored as a datetime.)
3520 Returns the current value of Started.
3521 (In the database, Started is stored as datetime.)
3525 =head2 SetStarted VALUE
3528 Set Started to VALUE.
3529 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
3530 (In the database, Started will be stored as a datetime.)
3538 Returns the current value of Due.
3539 (In the database, Due is stored as datetime.)
3547 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
3548 (In the database, Due will be stored as a datetime.)
3556 Returns the current value of Resolved.
3557 (In the database, Resolved is stored as datetime.)
3561 =head2 SetResolved VALUE
3564 Set Resolved to VALUE.
3565 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
3566 (In the database, Resolved will be stored as a datetime.)
3572 =head2 LastUpdatedBy
3574 Returns the current value of LastUpdatedBy.
3575 (In the database, LastUpdatedBy is stored as int(11).)
3583 Returns the current value of LastUpdated.
3584 (In the database, LastUpdated is stored as datetime.)
3592 Returns the current value of Creator.
3593 (In the database, Creator is stored as int(11).)
3601 Returns the current value of Created.
3602 (In the database, Created is stored as datetime.)
3610 Returns the current value of Disabled.
3611 (In the database, Disabled is stored as smallint(6).)
3615 =head2 SetDisabled VALUE
3618 Set Disabled to VALUE.
3619 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
3620 (In the database, Disabled will be stored as a smallint(6).)
3627 sub _CoreAccessible {
3631 {read => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => ''},
3633 {read => 1, write => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'},
3635 {read => 1, write => 1, sql_type => 5, length => 6, is_blob => 0, is_numeric => 1, type => 'smallint(6)', default => undef},
3637 {read => 1, write => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'},
3639 {read => 1, write => 1, sql_type => 12, length => 16, is_blob => 0, is_numeric => 0, type => 'varchar(16)', default => ''},
3641 {read => 1, write => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'},
3643 {read => 1, write => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'},
3645 {read => 1, write => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'},
3647 {read => 1, write => 1, sql_type => 12, length => 200, is_blob => 0, is_numeric => 0, type => 'varchar(200)', default => '[no subject]'},
3649 {read => 1, write => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'},
3651 {read => 1, write => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'},
3653 {read => 1, write => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'},
3655 {read => 1, write => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'},
3657 {read => 1, write => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'},
3659 {read => 1, write => 1, sql_type => 12, length => 64, is_blob => 0, is_numeric => 0, type => 'varchar(64)', default => ''},
3661 {read => 1, write => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'},
3663 {read => 1, write => 1, sql_type => 11, length => 0, is_blob => 0, is_numeric => 0, type => 'datetime', default => ''},
3665 {read => 1, write => 1, sql_type => 11, length => 0, is_blob => 0, is_numeric => 0, type => 'datetime', default => ''},
3667 {read => 1, write => 1, sql_type => 11, length => 0, is_blob => 0, is_numeric => 0, type => 'datetime', default => ''},
3669 {read => 1, write => 1, sql_type => 11, length => 0, is_blob => 0, is_numeric => 0, type => 'datetime', default => ''},
3671 {read => 1, write => 1, sql_type => 11, length => 0, is_blob => 0, is_numeric => 0, type => 'datetime', default => ''},
3673 {read => 1, auto => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'},
3675 {read => 1, auto => 1, sql_type => 11, length => 0, is_blob => 0, is_numeric => 0, type => 'datetime', default => ''},
3677 {read => 1, auto => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'},
3679 {read => 1, auto => 1, sql_type => 11, length => 0, is_blob => 0, is_numeric => 0, type => 'datetime', default => ''},
3681 {read => 1, write => 1, sql_type => 5, length => 6, is_blob => 0, is_numeric => 1, type => 'smallint(6)', default => '0'},
3686 sub FindDependencies {
3688 my ($walker, $deps) = @_;
3690 $self->SUPER::FindDependencies($walker, $deps);
3693 my $links = RT::Links->new( $self->CurrentUser );
3695 SUBCLAUSE => "either",
3697 VALUE => $self->URI,
3698 ENTRYAGGREGATOR => 'OR'
3699 ) for qw/Base Target/;
3700 $deps->Add( in => $links );
3702 # Tickets which were merged in
3703 my $objs = RT::Tickets->new( $self->CurrentUser );
3704 $objs->Limit( FIELD => 'EffectiveId', VALUE => $self->Id );
3705 $objs->Limit( FIELD => 'id', OPERATOR => '!=', VALUE => $self->Id );
3706 $deps->Add( in => $objs );
3708 # Ticket role groups( Owner, Requestors, Cc, AdminCc )
3709 $objs = RT::Groups->new( $self->CurrentUser );
3710 $objs->Limit( FIELD => 'Domain', VALUE => 'RT::Ticket-Role', CASESENSITIVE => 0 );
3711 $objs->Limit( FIELD => 'Instance', VALUE => $self->Id );
3712 $deps->Add( in => $objs );
3715 $deps->Add( out => $self->QueueObj );
3718 $deps->Add( out => $self->OwnerObj );
3725 Dependencies => undef,
3728 my $deps = $args{'Dependencies'};
3731 # Tickets which were merged in
3732 my $objs = RT::Tickets->new( $self->CurrentUser );
3733 $objs->{'allow_deleted_search'} = 1;
3734 $objs->Limit( FIELD => 'EffectiveId', VALUE => $self->Id );
3735 $objs->Limit( FIELD => 'id', OPERATOR => '!=', VALUE => $self->Id );
3736 push( @$list, $objs );
3738 # Ticket role groups( Owner, Requestors, Cc, AdminCc )
3739 $objs = RT::Groups->new( $self->CurrentUser );
3740 $objs->Limit( FIELD => 'Domain', VALUE => 'RT::Ticket-Role', CASESENSITIVE => 0 );
3741 $objs->Limit( FIELD => 'Instance', VALUE => $self->Id );
3742 push( @$list, $objs );
3744 #TODO: Users, Queues if we wish export tool
3745 $deps->_PushDependencies(
3746 BaseObject => $self,
3747 Flags => RT::Shredder::Constants::DEPENDS_ON,
3748 TargetObjects => $list,
3749 Shredder => $args{'Shredder'}
3752 return $self->SUPER::__DependsOn( %args );
3758 my %store = $self->SUPER::Serialize(@_);
3760 my $obj = RT::Ticket->new( RT->SystemUser );
3761 $obj->Load( $store{EffectiveId} );
3762 $store{EffectiveId} = \($obj->UID);
3767 RT::Base->_ImportOverlays();