1 # BEGIN BPS TAGGED BLOCK {{{
5 # This software is Copyright (c) 1996-2019 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'};
320 my $Now = RT::Date->new( $self->CurrentUser );
323 #TODO we should see what sort of due date we're getting, rather +
324 # than assuming it's in ISO format.
326 #Set the due date. if we didn't get fed one, use the queue default due in
327 my $Due = RT::Date->new( $self->CurrentUser );
328 if ( defined $args{'Due'} ) {
329 $Due->Set( Format => 'ISO', Value => $args{'Due'} );
331 elsif ( my $due_in = $QueueObj->DefaultDueIn ) {
332 $Due->Set( Format => 'ISO', Value => $Now->ISO );
333 $Due->AddDays( $due_in );
336 my $Starts = RT::Date->new( $self->CurrentUser );
337 if ( defined $args{'Starts'} ) {
338 $Starts->Set( Format => 'ISO', Value => $args{'Starts'} );
341 my $Started = RT::Date->new( $self->CurrentUser );
342 if ( defined $args{'Started'} ) {
343 $Started->Set( Format => 'ISO', Value => $args{'Started'} );
346 my $WillResolve = RT::Date->new($self->CurrentUser );
347 if ( defined $args{'WillResolve'} ) {
348 $WillResolve->Set( Format => 'ISO', Value => $args{'WillResolve'} );
351 # If the status is not an initial status, set the started date
352 elsif ( !$cycle->IsInitial($args{'Status'}) ) {
353 $Started->Set( Format => 'ISO', Value => $Now->ISO );
356 my $Resolved = RT::Date->new( $self->CurrentUser );
357 if ( defined $args{'Resolved'} ) {
358 $Resolved->Set( Format => 'ISO', Value => $args{'Resolved'} );
361 #If the status is an inactive status, set the resolved date
362 elsif ( $cycle->IsInactive( $args{'Status'} ) )
364 $RT::Logger->debug( "Got a ". $args{'Status'}
365 ."(inactive) ticket with undefined resolved date. Setting to now."
367 $Resolved->Set( Format => 'ISO', Value => $Now->ISO );
370 # Dealing with time fields
371 $args{'TimeEstimated'} = 0 unless defined $args{'TimeEstimated'};
372 $args{'TimeWorked'} = 0 unless defined $args{'TimeWorked'};
373 $args{'TimeLeft'} = 0 unless defined $args{'TimeLeft'};
375 # Figure out users for roles
377 push @non_fatal_errors, $self->_ResolveRoles( $roles, %args );
379 $args{'Type'} = lc $args{'Type'}
380 if $args{'Type'} =~ /^(ticket|approval|reminder)$/i;
382 $args{'Subject'} =~ s/\n//g;
384 $RT::Handle->BeginTransaction();
387 Queue => $QueueObj->Id,
388 Subject => $args{'Subject'},
389 InitialPriority => $args{'InitialPriority'},
390 FinalPriority => $args{'FinalPriority'},
391 Priority => $args{'Priority'},
392 Status => $args{'Status'},
393 TimeWorked => $args{'TimeWorked'},
394 TimeEstimated => $args{'TimeEstimated'},
395 TimeLeft => $args{'TimeLeft'},
396 Type => $args{'Type'},
397 Created => $Now->ISO,
398 Starts => $Starts->ISO,
399 Started => $Started->ISO,
400 Resolved => $Resolved->ISO,
401 WillResolve => $WillResolve->ISO,
405 # Parameters passed in during an import that we probably don't want to touch, otherwise
406 foreach my $attr (qw(id Creator Created LastUpdated LastUpdatedBy)) {
407 $params{$attr} = $args{$attr} if $args{$attr};
410 # Delete null integer parameters
412 (qw(TimeWorked TimeLeft TimeEstimated InitialPriority FinalPriority))
414 delete $params{$attr}
415 unless ( exists $params{$attr} && $params{$attr} );
418 # Delete the time worked if we're counting it in the transaction
419 delete $params{'TimeWorked'} if $args{'_RecordTransaction'};
421 my ($id,$ticket_message) = $self->SUPER::Create( %params );
423 $RT::Logger->crit( "Couldn't create a ticket: " . $ticket_message );
424 $RT::Handle->Rollback();
426 $self->loc("Ticket could not be created due to an internal error")
430 #Set the ticket's effective ID now that we've created it.
431 my ( $val, $msg ) = $self->__Set(
432 Field => 'EffectiveId',
433 Value => ( $args{'EffectiveId'} || $id )
436 $RT::Logger->crit("Couldn't set EffectiveId: $msg");
437 $RT::Handle->Rollback;
439 $self->loc("Ticket could not be created due to an internal error")
443 # Create (empty) role groups
444 my $create_groups_ret = $self->_CreateRoleGroups();
445 unless ($create_groups_ret) {
446 $RT::Logger->crit( "Couldn't create ticket groups for ticket "
448 . ". aborting Ticket creation." );
449 $RT::Handle->Rollback();
451 $self->loc("Ticket could not be created due to an internal error")
455 # Codify what it takes to add each kind of group
458 Requestor => sub { 1 },
460 my $principal = shift;
461 return 1 if $self->CurrentUserHasRight('ModifyTicket');
462 return unless $self->CurrentUserHasRight("WatchAsAdminCc");
463 return unless $principal->id == $self->CurrentUser->PrincipalId;
467 my $principal = shift;
468 return 1 if $principal->id == RT->Nobody->PrincipalId;
469 return $principal->HasRight( Object => $self, Right => 'OwnTicket' );
473 # Populate up the role groups. This call modifies $roles.
474 push @non_fatal_errors, $self->_AddRolesOnCreate( $roles, %acls );
477 if ($args{'SquelchMailTo'}) {
478 my @squelch = ref( $args{'SquelchMailTo'} ) ? @{ $args{'SquelchMailTo'} }
479 : $args{'SquelchMailTo'};
480 $self->_SquelchMailTo( @squelch );
483 # Add all the custom fields
484 foreach my $arg ( keys %args ) {
485 next unless $arg =~ /^CustomField-(\d+)$/i;
487 my $cf = $self->LoadCustomFieldByIdentifier($cfid);
488 next unless $cf->ObjectTypeFromLookupType($cf->__Value('LookupType'))->isa(ref $self);
491 UNIVERSAL::isa( $args{$arg} => 'ARRAY' ) ? @{ $args{$arg} } : ( $args{$arg} ) )
493 next unless defined $value && length $value;
495 # Allow passing in uploaded LargeContent etc by hash reference
496 my ($status, $msg) = $self->_AddCustomFieldValue(
497 (UNIVERSAL::isa( $value => 'HASH' )
502 RecordTransaction => 0,
504 push @non_fatal_errors, $msg unless $status;
508 # Deal with setting up links
510 # TODO: Adding link may fire scrips on other end and those scrips
511 # could create transactions on this ticket before 'Create' transaction.
513 # We should implement different lifecycle: record 'Create' transaction,
514 # create links and only then fire create transaction's scrips.
516 # Ideal variant: add all links without firing scrips, record create
517 # transaction and only then fire scrips on the other ends of links.
520 push @non_fatal_errors, $self->_AddLinksOnCreate(\%args, {
521 Silent => !$args{'_RecordTransaction'} || ($self->Type || '') eq 'reminder',
526 # {{{ Deal with auto-customer association
528 #unless we already have (a) customer(s)...
529 unless ( $self->Customers->Count ) {
531 #first find any requestors with emails but *without* customer targets
532 my @NoCust_Requestors =
533 grep { $_->EmailAddress && ! $_->Customers->Count }
534 @{ $self->_Requestors->UserMembersObj->ItemsArrayRef };
536 for my $Requestor (@NoCust_Requestors) {
538 #perhaps the stuff in here should be in a User method??
540 &RT::URI::freeside::email_search( email=>$Requestor->EmailAddress );
542 foreach my $custnum ( map $_->{'custnum'}, @Customers ) {
544 ## false laziness w/RT/Interface/Web_Vendor.pm
545 my @link = ( 'Type' => 'MemberOf',
546 'Target' => "freeside://freeside/cust_main/$custnum",
549 my( $val, $msg ) = $Requestor->_AddLink(@link);
550 #XXX should do something with $msg# push @non_fatal_errors, $msg;
556 #find any requestors with customer targets
558 my %cust_target = ();
561 grep { $_->Customers->Count }
562 @{ $self->_Requestors->UserMembersObj->ItemsArrayRef };
564 foreach my $Requestor ( @Requestors ) {
565 foreach my $cust_link ( @{ $Requestor->Customers->ItemsArrayRef } ) {
566 $cust_target{ $cust_link->Target } = 1;
570 #and then auto-associate this ticket with those customers
572 foreach my $cust_target ( keys %cust_target ) {
574 my @link = ( 'Type' => 'MemberOf',
575 #'Target' => "freeside://freeside/cust_main/$custnum",
576 'Target' => $cust_target,
579 my( $val, $msg ) = $self->_AddLink(@link);
580 push @non_fatal_errors, $msg;
588 push @non_fatal_errors, $self->_AddLinksOnCreate(\%args, {
589 Silent => !$args{'_RecordTransaction'} || ($self->Type || '') eq 'reminder',
592 # Try to add roles once more.
593 push @non_fatal_errors, $self->_AddRolesOnCreate( $roles, %acls );
595 # Anything left is failure of ACLs; Cc and Requestor have no ACLs,
596 # so we don't bother checking them.
597 if (@{ $roles->{Owner} }) {
598 my $owner = $roles->{Owner}[0]->Object;
599 $RT::Logger->warning( "User " . $owner->Name . "(" . $owner->id
600 . ") was proposed as a ticket owner but has no rights to own "
601 . "tickets in " . $QueueObj->Name );
602 push @non_fatal_errors, $self->loc(
603 "Owner '[_1]' does not have rights to own this ticket.",
607 for my $principal (@{ $roles->{AdminCc} }) {
608 push @non_fatal_errors, $self->loc(
609 "No rights to add '[_1]' as an AdminCc on this ticket",
610 $principal->Object->Name
614 #don't make a transaction or fire off any scrips for reminders either
615 if ( $args{'_RecordTransaction'} && $self->Type ne 'reminder' ) {
617 # Add a transaction for the create
618 my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction(
620 TimeTaken => $args{'TimeWorked'},
621 MIMEObj => $args{'MIMEObj'},
622 CommitScrips => !$args{'DryRun'},
623 SquelchMailTo => $args{'TransSquelchMailTo'},
626 if ( $self->Id && $Trans ) {
628 $TransObj->UpdateCustomFields(%args);
630 $RT::Logger->info( "Ticket " . $self->Id . " created in queue '" . $QueueObj->Name . "' by " . $self->CurrentUser->Name );
631 $ErrStr = $self->loc( "Ticket [_1] created in queue '[_2]'", $self->Id, $QueueObj->Name );
632 $ErrStr = join( "\n", $ErrStr, @non_fatal_errors );
635 $RT::Handle->Rollback();
637 $ErrStr = join( "\n", $ErrStr, @non_fatal_errors );
638 $RT::Logger->error("Ticket couldn't be created: $ErrStr");
639 return ( 0, 0, $self->loc( "Ticket could not be created due to an internal error"));
642 if ( $args{'DryRun'} ) {
643 $RT::Handle->Rollback();
644 return ($self->id, $TransObj, $ErrStr);
646 $RT::Handle->Commit();
647 return ( $self->Id, $TransObj->Id, $ErrStr );
651 # Not going to record a transaction
652 $RT::Handle->Commit();
653 $ErrStr = $self->loc( "Ticket [_1] created in queue '[_2]'", $self->Id, $QueueObj->Name );
654 $ErrStr = join( "\n", $ErrStr, @non_fatal_errors );
655 return ( $self->Id, 0, $ErrStr );
664 # Force lowercase on internal RT types
666 if $value =~ /^(ticket|approval|reminder)$/i;
667 return $self->_Set(Field => 'Type', Value => $value, @_);
672 A constructor which returns an RT::Group object containing the owner of this ticket.
678 return $self->RoleGroup( 'Owner' );
682 sub _HasModifyWatcherRight {
684 my ($type, $principal) = @_;
686 # ModifyTicket works in any case
687 return 1 if $self->CurrentUserHasRight('ModifyTicket');
688 # If the watcher isn't the current user then the current user has no right
689 return 0 unless $self->CurrentUser->PrincipalId == $principal->id;
690 # If it's an AdminCc and they don't have 'WatchAsAdminCc', bail
691 return 0 if $type eq 'AdminCc' and not $self->CurrentUserHasRight('WatchAsAdminCc');
692 # If it's a Requestor or Cc and they don't have 'Watch', bail
693 return 0 if ($type eq "Cc" or $type eq 'Requestor')
694 and not $self->CurrentUserHasRight('Watch');
701 Applies access control checking, then calls
702 L<RT::Record::Role::Roles/AddRoleMember>. Additionally, C<Email> is
703 accepted as an alternative argument name for C<User>.
705 Returns a tuple of (status, message).
713 PrincipalId => undef,
718 $args{ACL} = sub { $self->_HasModifyWatcherRight( @_ ) };
719 $args{User} ||= delete $args{Email};
720 my ($principal, $msg) = $self->AddRoleMember(
722 InsideTransaction => 1,
724 return ( 0, $msg) unless $principal;
726 return ( 1, $self->loc('Added [_1] as a [_2] for this ticket',
727 $principal->Object->Name, $self->loc($args{'Type'})) );
733 Applies access control checking, then calls
734 L<RT::Record::Role::Roles/DeleteRoleMember>. Additionally, C<Email> is
735 accepted as an alternative argument name for C<User>.
737 Returns a tuple of (status, message).
745 my %args = ( Type => undef,
746 PrincipalId => undef,
750 $args{ACL} = sub { $self->_HasModifyWatcherRight( @_ ) };
751 $args{User} ||= delete $args{Email};
752 my ($principal, $msg) = $self->DeleteRoleMember( %args );
753 return ( 0, $msg ) unless $principal;
756 $self->loc( "[_1] is no longer a [_2] for this ticket.",
757 $principal->Object->Name,
758 $self->loc($args{'Type'}) ) );
765 =head2 SquelchMailTo ADDRESSES
767 Takes a list of email addresses to never email about updates to this ticket.
768 Subsequent calls to this method add, rather than replace, the list of
771 Returns an array of the L<RT::Attribute> objects for this ticket's
772 'SquelchMailTo' attributes.
779 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
783 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
788 return $self->_SquelchMailTo(@_);
795 $self->AddAttribute( Name => 'SquelchMailTo', Content => $attr )
796 unless grep { $_->Content eq $attr }
797 $self->Attributes->Named('SquelchMailTo');
799 my @attributes = $self->Attributes->Named('SquelchMailTo');
800 return (@attributes);
804 =head2 UnsquelchMailTo ADDRESS
806 Takes an address and removes it from this ticket's "SquelchMailTo" list. If an address appears multiple times, each instance is removed.
808 Returns a tuple of (status, message)
812 sub UnsquelchMailTo {
816 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
817 return ( 0, $self->loc("Permission Denied") );
820 my ($val, $msg) = $self->Attributes->DeleteEntry ( Name => 'SquelchMailTo', Content => $address);
826 =head2 RequestorAddresses
828 B<Returns> String: All Ticket Requestor email addresses as a string.
832 sub RequestorAddresses {
835 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
839 return ( $self->Requestors->MemberEmailAddressesAsString );
843 =head2 AdminCcAddresses
845 returns String: All Ticket AdminCc email addresses as a string
849 sub AdminCcAddresses {
852 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
856 return ( $self->AdminCc->MemberEmailAddressesAsString )
862 returns String: All Ticket Ccs as a string of email addresses
869 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
872 return ( $self->Cc->MemberEmailAddressesAsString);
882 Returns this ticket's Requestors as an RT::Group object
888 return RT::Group->new($self->CurrentUser)
889 unless $self->CurrentUserHasRight('ShowTicket');
890 return $self->RoleGroup( 'Requestor' );
895 return $self->Requestor;
900 Private non-ACLed variant of Reqeustors so that we can look them up for the
901 purposes of customer auto-association during create.
908 my $group = RT::Group->new($RT::SystemUser);
909 $group->LoadTicketRoleGroup(Type => 'Requestor', Ticket => $self->Id);
916 Returns an RT::Group object which contains this ticket's Ccs.
917 If the user doesn't have "ShowTicket" permission, returns an empty group
924 return RT::Group->new($self->CurrentUser)
925 unless $self->CurrentUserHasRight('ShowTicket');
926 return $self->RoleGroup( 'Cc' );
934 Returns an RT::Group object which contains this ticket's AdminCcs.
935 If the user doesn't have "ShowTicket" permission, returns an empty group
942 return RT::Group->new($self->CurrentUser)
943 unless $self->CurrentUserHasRight('ShowTicket');
944 return $self->RoleGroup( 'AdminCc' );
950 # a generic routine to be called by IsRequestor, IsCc and IsAdminCc
952 =head2 IsWatcher { Type => TYPE, PrincipalId => PRINCIPAL_ID, Email => EMAIL }
954 Takes a param hash with the attributes Type and either PrincipalId or Email
956 Type is one of Requestor, Cc, AdminCc and Owner
958 PrincipalId is an RT::Principal id, and Email is an email address.
960 Returns true if the specified principal (or the one corresponding to the
961 specified address) is a member of the group Type for this ticket.
963 XX TODO: This should be Memoized.
970 my %args = ( Type => 'Requestor',
971 PrincipalId => undef,
976 # Load the relevant group.
977 my $group = $self->RoleGroup( $args{'Type'} );
979 # Find the relevant principal.
980 if (!$args{PrincipalId} && $args{Email}) {
981 # Look up the specified user.
982 my $user = RT::User->new($self->CurrentUser);
983 $user->LoadByEmail($args{Email});
985 $args{PrincipalId} = $user->PrincipalId;
988 # A non-existent user can't be a group member.
993 # Ask if it has the member in question
994 return $group->HasMember( $args{'PrincipalId'} );
999 =head2 IsRequestor PRINCIPAL_ID
1001 Takes an L<RT::Principal> id.
1003 Returns true if the principal is a requestor of the current ticket.
1011 return ( $self->IsWatcher( Type => 'Requestor', PrincipalId => $person ) );
1017 =head2 IsCc PRINCIPAL_ID
1019 Takes an RT::Principal id.
1020 Returns true if the principal is a Cc of the current ticket.
1029 return ( $self->IsWatcher( Type => 'Cc', PrincipalId => $cc ) );
1035 =head2 IsAdminCc PRINCIPAL_ID
1037 Takes an RT::Principal id.
1038 Returns true if the principal is an AdminCc of the current ticket.
1046 return ( $self->IsWatcher( Type => 'AdminCc', PrincipalId => $person ) );
1054 Takes an RT::User object. Returns true if that user is this ticket's owner.
1055 returns undef otherwise
1063 # no ACL check since this is used in acl decisions
1064 # unless ($self->CurrentUserHasRight('ShowTicket')) {
1068 #Tickets won't yet have owners when they're being created.
1069 unless ( $self->OwnerObj->id ) {
1073 if ( $person->id == $self->OwnerObj->id ) {
1085 =head2 TransactionAddresses
1087 Returns a composite hashref of the results of L<RT::Transaction/Addresses> for
1088 all this ticket's Create, Comment or Correspond transactions. The keys are
1089 stringified email addresses. Each value is an L<Email::Address> object.
1091 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.
1096 sub TransactionAddresses {
1098 my $txns = $self->Transactions;
1102 my $attachments = RT::Attachments->new( $self->CurrentUser );
1103 $attachments->LimitByTicket( $self->id );
1104 $attachments->Columns( qw( id Headers TransactionId));
1106 $attachments->Limit(
1111 $attachments->Limit(
1112 ALIAS => $attachments->TransactionAlias,
1115 VALUE => [ qw(Create Comment Correspond) ],
1118 while ( my $att = $attachments->Next ) {
1119 foreach my $addrlist ( values %{$att->Addresses } ) {
1120 foreach my $addr (@$addrlist) {
1122 # Skip addresses without a phrase (things that are just raw addresses) if we have a phrase
1124 if ( $addresses{ $addr->address }
1125 && $addresses{ $addr->address }->phrase
1126 && not $addr->phrase );
1128 # skips "comment-only" addresses
1129 next unless ( $addr->address );
1130 $addresses{ $addr->address } = $addr;
1149 $RT::Logger->warning( " RT:::Queue::ValidateQueue called with a null value. this isn't ok.");
1153 my $QueueObj = RT::Queue->new( $self->CurrentUser );
1154 my $id = $QueueObj->Load($Value);
1168 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
1169 return ( 0, $self->loc("Permission Denied") );
1172 my ($ok, $msg, $status) = $self->_SetLifecycleColumn(
1174 RequireRight => "CreateTicket"
1178 # Clear the queue object cache;
1179 $self->{_queue_obj} = undef;
1180 my $queue = $self->QueueObj;
1182 # Untake the ticket if we have no permissions in the new queue
1183 unless ($self->OwnerObj->HasRight( Right => 'OwnTicket', Object => $queue )) {
1184 my $clone = RT::Ticket->new( RT->SystemUser );
1185 $clone->Load( $self->Id );
1186 unless ( $clone->Id ) {
1187 return ( 0, $self->loc("Couldn't load copy of ticket #[_1].", $self->Id) );
1189 my ($status, $msg) = $clone->SetOwner( RT->Nobody->Id, 'Force' );
1190 $RT::Logger->error("Couldn't set owner on queue change: $msg") unless $status;
1193 # On queue change, change queue for reminders too
1194 my $reminder_collection = $self->Reminders->Collection;
1195 while ( my $reminder = $reminder_collection->Next ) {
1196 my ($status, $msg) = $reminder->_Set( Field => 'Queue', Value => $queue->Id(), RecordTransaction => 0 );
1197 $RT::Logger->error('Queue change failed for reminder #' . $reminder->Id . ': ' . $msg) unless $status;
1200 # Pick up any changes made by the clones above
1201 $self->Load( $self->id );
1202 RT->Logger->error("Unable to reload ticket #" . $self->id)
1213 Takes nothing. returns this ticket's queue object
1220 if(!$self->{_queue_obj} || ! $self->{_queue_obj}->id) {
1222 $self->{_queue_obj} = RT::Queue->new( $self->CurrentUser );
1224 #We call __Value so that we can avoid the ACL decision and some deep recursion
1225 my ($result) = $self->{_queue_obj}->Load( $self->__Value('Queue') );
1227 return ($self->{_queue_obj});
1233 my $subject = $self->_Value( 'Subject' );
1234 return $subject if defined $subject;
1236 if ( RT->Config->Get( 'DatabaseType' ) eq 'Oracle' && $self->CurrentUserHasRight( 'ShowTicket' ) ) {
1238 # Oracle treats empty strings as NULL, so it returns undef for empty subjects.
1239 # Since '' is the default Subject value, returning '' is more correct.
1251 return $self->_Set( Field => 'Subject', Value => $value );
1256 Takes nothing. Returns SubjectTag for this ticket. Includes
1257 queue's subject tag or rtname if that is not set, ticket
1258 id and brackets, for example:
1260 [support.example.com #123456]
1268 . ($self->QueueObj->SubjectTag || RT->Config->Get('rtname'))
1277 Returns an RT::Date object containing this ticket's due date
1284 my $time = RT::Date->new( $self->CurrentUser );
1286 # -1 is RT::Date slang for never
1287 if ( my $due = $self->Due ) {
1288 $time->Set( Format => 'sql', Value => $due );
1291 $time->Set( Format => 'unix', Value => -1 );
1301 Returns this ticket's due date as a human readable string.
1303 B<DEPRECATED> and will be removed in 4.4; use C<<
1304 $ticket->DueObj->AsString >> instead.
1311 Instead => "->DueObj->AsString",
1314 return $self->DueObj->AsString();
1321 Returns an RT::Date object of this ticket's 'resolved' time.
1328 my $time = RT::Date->new( $self->CurrentUser );
1329 $time->Set( Format => 'sql', Value => $self->Resolved );
1333 =head2 FirstActiveStatus
1335 Returns the first active status that the ticket could transition to,
1336 according to its current Queue's lifecycle. May return undef if there
1337 is no such possible status to transition to, or we are already in it.
1338 This is used in L<RT::Action::AutoOpen>, for instance.
1342 sub FirstActiveStatus {
1345 my $lifecycle = $self->LifecycleObj;
1346 my $status = $self->Status;
1347 my @active = $lifecycle->Active;
1348 # no change if no active statuses in the lifecycle
1349 return undef unless @active;
1351 # no change if the ticket is already has first status from the list of active
1352 return undef if lc $status eq lc $active[0];
1354 my ($next) = grep $lifecycle->IsActive($_), $lifecycle->Transitions($status);
1358 =head2 FirstInactiveStatus
1360 Returns the first inactive status that the ticket could transition to,
1361 according to its current Queue's lifecycle. May return undef if there
1362 is no such possible status to transition to, or we are already in it.
1363 This is used in resolve action in UnsafeEmailCommands, for instance.
1367 sub FirstInactiveStatus {
1370 my $lifecycle = $self->LifecycleObj;
1371 my $status = $self->Status;
1372 my @inactive = $lifecycle->Inactive;
1373 # no change if no inactive statuses in the lifecycle
1374 return undef unless @inactive;
1376 # no change if the ticket is already has first status from the list of inactive
1377 return undef if lc $status eq lc $inactive[0];
1379 my ($next) = grep $lifecycle->IsInactive($_), $lifecycle->Transitions($status);
1385 Takes a date in ISO format or undef
1386 Returns a transaction id and a message
1387 The client calls "Start" to note that the project was started on the date in $date.
1388 A null date means "now"
1394 my $time = shift || 0;
1396 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
1397 return ( 0, $self->loc("Permission Denied") );
1400 #We create a date object to catch date weirdness
1401 my $time_obj = RT::Date->new( $self->CurrentUser() );
1403 $time_obj->Set( Format => 'ISO', Value => $time );
1406 $time_obj->SetToNow();
1409 return ( $self->_Set( Field => 'Started', Value => $time_obj->ISO ) );
1417 Returns an RT::Date object which contains this ticket's
1425 my $time = RT::Date->new( $self->CurrentUser );
1426 $time->Set( Format => 'sql', Value => $self->Started );
1434 Returns an RT::Date object which contains this ticket's
1442 my $time = RT::Date->new( $self->CurrentUser );
1443 $time->Set( Format => 'sql', Value => $self->Starts );
1451 Returns an RT::Date object which contains this ticket's
1459 my $time = RT::Date->new( $self->CurrentUser );
1460 $time->Set( Format => 'sql', Value => $self->Told );
1468 A convenience method that returns ToldObj->AsString
1470 B<DEPRECATED> and will be removed in 4.4; use C<<
1471 $ticket->ToldObj->AsString >> instead.
1478 Instead => "->ToldObj->AsString",
1481 if ( $self->Told ) {
1482 return $self->ToldObj->AsString();
1491 sub _DurationAsString {
1494 return "" unless $value;
1495 return RT::Date->new( $self->CurrentUser )
1496 ->DurationAsString( $value * 60 );
1499 =head2 TimeWorkedAsString
1501 Returns the amount of time worked on this ticket as a text string.
1505 sub TimeWorkedAsString {
1507 return $self->_DurationAsString( $self->TimeWorked );
1510 =head2 TimeLeftAsString
1512 Returns the amount of time left on this ticket as a text string.
1516 sub TimeLeftAsString {
1518 return $self->_DurationAsString( $self->TimeLeft );
1521 =head2 TimeEstimatedAsString
1523 Returns the amount of time estimated on this ticket as a text string.
1527 sub TimeEstimatedAsString {
1529 return $self->_DurationAsString( $self->TimeEstimated );
1537 Comment on this ticket.
1538 Takes a hash with the following attributes:
1539 If MIMEObj is undefined, Content will be used to build a MIME::Entity for this
1542 MIMEObj, TimeTaken, CcMessageTo, BccMessageTo, Content, DryRun
1544 If DryRun is defined, this update WILL NOT BE RECORDED. Scrips will not be committed.
1545 They will, however, be prepared and you'll be able to access them through the TransactionObj
1547 Returns: Transaction id, Error Message, Transaction Object
1548 (note the different order from Create()!)
1555 my %args = ( CcMessageTo => undef,
1556 BccMessageTo => undef,
1563 unless ( ( $self->CurrentUserHasRight('CommentOnTicket') )
1564 or ( $self->CurrentUserHasRight('ModifyTicket') ) ) {
1565 return ( 0, $self->loc("Permission Denied"), undef );
1567 $args{'NoteType'} = 'Comment';
1569 $RT::Handle->BeginTransaction();
1570 if ($args{'DryRun'}) {
1571 $args{'CommitScrips'} = 0;
1574 my @results = $self->_RecordNote(%args);
1575 if ($args{'DryRun'}) {
1576 $RT::Handle->Rollback();
1578 $RT::Handle->Commit();
1587 Correspond on this ticket.
1588 Takes a hashref with the following attributes:
1591 MIMEObj, TimeTaken, CcMessageTo, BccMessageTo, Content, DryRun
1593 if there's no MIMEObj, Content is used to build a MIME::Entity object
1595 If DryRun is defined, this update WILL NOT BE RECORDED. Scrips will not be committed.
1596 They will, however, be prepared and you'll be able to access them through the TransactionObj
1598 Returns: Transaction id, Error Message, Transaction Object
1599 (note the different order from Create()!)
1606 my %args = ( CcMessageTo => undef,
1607 BccMessageTo => undef,
1613 unless ( ( $self->CurrentUserHasRight('ReplyToTicket') )
1614 or ( $self->CurrentUserHasRight('ModifyTicket') ) ) {
1615 return ( 0, $self->loc("Permission Denied"), undef );
1617 $args{'NoteType'} = 'Correspond';
1619 $RT::Handle->BeginTransaction();
1620 if ($args{'DryRun'}) {
1621 $args{'CommitScrips'} = 0;
1624 my @results = $self->_RecordNote(%args);
1626 unless ( $results[0] ) {
1627 $RT::Handle->Rollback();
1631 #Set the last told date to now if this isn't mail from the requestor.
1632 #TODO: Note that this will wrongly ack mail from any non-requestor as a "told"
1633 unless ( $self->IsRequestor($self->CurrentUser->id) ) {
1635 $squelch{$_}++ for map {$_->Content} $self->SquelchMailTo, $results[2]->SquelchMailTo;
1637 if grep {not $squelch{$_}} $self->Requestors->MemberEmailAddresses;
1640 if ($args{'DryRun'}) {
1641 $RT::Handle->Rollback();
1643 $RT::Handle->Commit();
1654 the meat of both comment and correspond.
1656 Performs no access control checks. hence, dangerous.
1663 CcMessageTo => undef,
1664 BccMessageTo => undef,
1669 NoteType => 'Correspond',
1672 SquelchMailTo => undef,
1677 unless ( $args{'MIMEObj'} || $args{'Content'} ) {
1678 return ( 0, $self->loc("No message attached"), undef );
1681 unless ( $args{'MIMEObj'} ) {
1682 my $data = ref $args{'Content'}? $args{'Content'} : [ $args{'Content'} ];
1683 $args{'MIMEObj'} = MIME::Entity->build(
1684 Type => "text/plain",
1686 Data => [ map {Encode::encode("UTF-8", $_)} @{$data} ],
1690 $args{'MIMEObj'}->head->replace('X-RT-Interface' => 'API')
1691 unless $args{'MIMEObj'}->head->get('X-RT-Interface');
1693 # convert text parts into utf-8
1694 RT::I18N::SetMIMEEntityToUTF8( $args{'MIMEObj'} );
1696 # If we've been passed in CcMessageTo and BccMessageTo fields,
1697 # add them to the mime object for passing on to the transaction handler
1698 # The "NotifyOtherRecipients" scripAction will look for RT-Send-Cc: and
1699 # RT-Send-Bcc: headers
1702 foreach my $type (qw/Cc Bcc/) {
1703 if ( defined $args{ $type . 'MessageTo' } ) {
1705 my $addresses = join ', ', (
1706 map { RT::User->CanonicalizeEmailAddress( $_->address ) }
1707 Email::Address->parse( $args{ $type . 'MessageTo' } ) );
1708 $args{'MIMEObj'}->head->replace( 'RT-Send-' . $type, Encode::encode( "UTF-8", $addresses ) );
1712 foreach my $argument (qw(Encrypt Sign)) {
1713 $args{'MIMEObj'}->head->replace(
1714 "X-RT-$argument" => $args{ $argument } ? 1 : 0
1715 ) if defined $args{ $argument };
1718 # If this is from an external source, we need to come up with its
1719 # internal Message-ID now, so all emails sent because of this
1720 # message have a common Message-ID
1721 my $org = RT->Config->Get('Organization');
1722 my $msgid = Encode::decode( "UTF-8", $args{'MIMEObj'}->head->get('Message-ID') );
1723 unless (defined $msgid && $msgid =~ /<(rt-.*?-\d+-\d+)\.(\d+-0-0)\@\Q$org\E>/) {
1724 $args{'MIMEObj'}->head->replace(
1725 'RT-Message-ID' => Encode::encode( "UTF-8",
1726 RT::Interface::Email::GenMessageId( Ticket => $self )
1731 #Record the correspondence (write the transaction)
1732 my ( $Trans, $msg, $TransObj ) = $self->_NewTransaction(
1733 Type => $args{'NoteType'},
1734 Data => ( Encode::decode( "UTF-8", $args{'MIMEObj'}->head->get('Subject') ) || 'No Subject' ),
1735 TimeTaken => $args{'TimeTaken'},
1736 MIMEObj => $args{'MIMEObj'},
1737 CommitScrips => $args{'CommitScrips'},
1738 SquelchMailTo => $args{'SquelchMailTo'},
1739 CustomFields => $args{'CustomFields'},
1743 $RT::Logger->err("$self couldn't init a transaction $msg");
1744 return ( $Trans, $self->loc("Message could not be recorded"), undef );
1747 if ($args{NoteType} eq "Comment") {
1748 $msg = $self->loc("Comments added");
1750 $msg = $self->loc("Correspondence added");
1752 return ( $Trans, $msg, $TransObj );
1758 Builds a MIME object from the given C<UpdateSubject> and
1759 C<UpdateContent>, then calls L</Comment> or L</Correspond> with
1760 C<< DryRun => 1 >>, and returns the transaction so produced.
1768 if (($args{'UpdateType'} || $args{Action}) =~ /^respon(d|se)$/i ) {
1769 $action = 'Correspond';
1771 $action = 'Comment';
1774 my $Message = MIME::Entity->build(
1775 Subject => defined $args{UpdateSubject} ? Encode::encode( "UTF-8", $args{UpdateSubject} ) : "",
1776 Type => 'text/plain',
1778 Data => Encode::encode("UTF-8", $args{'UpdateContent'} || ""),
1781 my ( $Transaction, $Description, $Object ) = $self->$action(
1782 CcMessageTo => $args{'UpdateCc'},
1783 BccMessageTo => $args{'UpdateBcc'},
1784 MIMEObj => $Message,
1785 TimeTaken => $args{'UpdateTimeWorked'},
1787 SquelchMailTo => $args{'SquelchMailTo'},
1789 unless ( $Transaction ) {
1790 $RT::Logger->error("Couldn't fire '$action' action: $Description");
1798 Prepares a MIME mesage with the given C<Subject>, C<Cc>, and
1799 C<Content>, then calls L</Create> with C<< DryRun => 1 >> and returns
1800 the resulting L<RT::Transaction>.
1807 my $Message = MIME::Entity->build(
1808 Subject => defined $args{Subject} ? Encode::encode( "UTF-8", $args{'Subject'} ) : "",
1809 (defined $args{'Cc'} ?
1810 ( Cc => Encode::encode( "UTF-8", $args{'Cc'} ) ) : ()),
1811 Type => 'text/plain',
1813 Data => Encode::encode( "UTF-8", $args{'Content'} || ""),
1816 my ( $Transaction, $Object, $Description ) = $self->Create(
1817 Type => $args{'Type'} || 'ticket',
1818 Queue => $args{'Queue'},
1819 Owner => $args{'Owner'},
1820 Requestor => $args{'Requestors'},
1822 AdminCc => $args{'AdminCc'},
1823 InitialPriority => $args{'InitialPriority'},
1824 FinalPriority => $args{'FinalPriority'},
1825 TimeLeft => $args{'TimeLeft'},
1826 TimeEstimated => $args{'TimeEstimated'},
1827 TimeWorked => $args{'TimeWorked'},
1828 Subject => $args{'Subject'},
1829 Status => $args{'Status'},
1830 MIMEObj => $Message,
1833 unless ( $Transaction ) {
1834 $RT::Logger->error("Couldn't fire Create action: $Description");
1845 #TODO: Field isn't the right thing here. but I ahave no idea what mnemonic ---
1848 my $type = shift || "";
1850 my $cache_key = "$field$type";
1851 return $self->{ $cache_key } if $self->{ $cache_key };
1853 my $links = $self->{ $cache_key }
1854 = RT::Links->new( $self->CurrentUser );
1855 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
1856 $links->Limit( FIELD => 'id', VALUE => 0, SUBCLAUSE => 'acl' );
1860 # Maybe this ticket is a merge ticket
1861 my $limit_on = 'Local'. $field;
1862 # at least to myself
1866 VALUE => [ $self->id, $self->Merged ],
1878 MergeInto take the id of the ticket to merge this ticket into.
1884 my $ticket_id = shift;
1886 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
1887 return ( 0, $self->loc("Permission Denied") );
1890 # Load up the new ticket.
1891 my $MergeInto = RT::Ticket->new($self->CurrentUser);
1892 $MergeInto->Load($ticket_id);
1894 # make sure it exists.
1895 unless ( $MergeInto->Id ) {
1896 return ( 0, $self->loc("New ticket doesn't exist") );
1899 # Can't merge into yourself
1900 if ( $MergeInto->Id == $self->Id ) {
1901 return ( 0, $self->loc("Can't merge a ticket into itself") );
1904 # Make sure the current user can modify the new ticket.
1905 unless ( $MergeInto->CurrentUserHasRight('ModifyTicket') ) {
1906 return ( 0, $self->loc("Permission Denied") );
1909 delete $MERGE_CACHE{'effective'}{ $self->id };
1910 delete @{ $MERGE_CACHE{'merged'} }{
1911 $ticket_id, $MergeInto->id, $self->id
1914 $RT::Handle->BeginTransaction();
1916 my ($ok, $msg) = $self->_MergeInto( $MergeInto );
1918 $RT::Handle->Commit() if $ok;
1925 my $MergeInto = shift;
1928 # We use EffectiveId here even though it duplicates information from
1929 # the links table becasue of the massive performance hit we'd take
1930 # by trying to do a separate database query for merge info everytime
1933 #update this ticket's effective id to the new ticket's id.
1934 my ( $id_val, $id_msg ) = $self->__Set(
1935 Field => 'EffectiveId',
1936 Value => $MergeInto->Id()
1940 $RT::Handle->Rollback();
1941 return ( 0, $self->loc("Merge failed. Couldn't set EffectiveId") );
1944 ( $id_val, $id_msg ) = $self->__Set( Field => 'IsMerged', Value => 1 );
1946 $RT::Handle->Rollback();
1947 return ( 0, $self->loc("Merge failed. Couldn't set IsMerged") );
1950 my $force_status = $self->LifecycleObj->DefaultOnMerge;
1951 if ( $force_status && $force_status ne $self->__Value('Status') ) {
1952 my ( $status_val, $status_msg )
1953 = $self->__Set( Field => 'Status', Value => $force_status );
1955 unless ($status_val) {
1956 $RT::Handle->Rollback();
1958 "Couldn't set status to $force_status. RT's Database may be inconsistent."
1960 return ( 0, $self->loc("Merge failed. Couldn't set Status") );
1964 # update all the links that point to that old ticket
1965 my $old_links_to = RT::Links->new($self->CurrentUser);
1966 $old_links_to->Limit(FIELD => 'Target', VALUE => $self->URI);
1969 while (my $link = $old_links_to->Next) {
1970 if (exists $old_seen{$link->Base."-".$link->Type}) {
1973 elsif ($link->Base eq $MergeInto->URI) {
1976 # First, make sure the link doesn't already exist. then move it over.
1977 my $tmp = RT::Link->new(RT->SystemUser);
1978 $tmp->LoadByCols(Base => $link->Base, Type => $link->Type, LocalTarget => $MergeInto->id);
1982 $link->SetTarget($MergeInto->URI);
1983 $link->SetLocalTarget($MergeInto->id);
1985 $old_seen{$link->Base."-".$link->Type} =1;
1990 my $old_links_from = RT::Links->new($self->CurrentUser);
1991 $old_links_from->Limit(FIELD => 'Base', VALUE => $self->URI);
1993 while (my $link = $old_links_from->Next) {
1994 if (exists $old_seen{$link->Type."-".$link->Target}) {
1997 if ($link->Target eq $MergeInto->URI) {
2000 # First, make sure the link doesn't already exist. then move it over.
2001 my $tmp = RT::Link->new(RT->SystemUser);
2002 $tmp->LoadByCols(Target => $link->Target, Type => $link->Type, LocalBase => $MergeInto->id);
2006 $link->SetBase($MergeInto->URI);
2007 $link->SetLocalBase($MergeInto->id);
2008 $old_seen{$link->Type."-".$link->Target} =1;
2014 # Update time fields
2015 foreach my $type (qw(TimeEstimated TimeWorked TimeLeft)) {
2018 Value => ( $MergeInto->$type() || 0 ) + ( $self->$type() || 0 ),
2019 RecordTransaction => 0,
2023 # add all of this ticket's watchers to that ticket.
2024 for my $role ($self->Roles) {
2025 next if $self->RoleGroup($role)->SingleMemberRoleGroup;
2026 my $people = $self->RoleGroup($role)->MembersObj;
2027 while ( my $watcher = $people->Next ) {
2028 my ($val, $msg) = $MergeInto->AddRoleMember(
2031 PrincipalId => $watcher->MemberId,
2032 InsideTransaction => 1,
2035 $RT::Logger->debug($msg);
2040 #find all of the tickets that were merged into this ticket.
2041 my $old_mergees = RT::Tickets->new( $self->CurrentUser );
2042 $old_mergees->Limit(
2043 FIELD => 'EffectiveId',
2048 # update their EffectiveId fields to the new ticket's id
2049 while ( my $ticket = $old_mergees->Next() ) {
2050 my ( $val, $msg ) = $ticket->__Set(
2051 Field => 'EffectiveId',
2052 Value => $MergeInto->Id()
2056 #make a new link: this ticket is merged into that other ticket.
2057 $self->AddLink( Type => 'MergedInto', Target => $MergeInto->Id());
2059 $MergeInto->_SetLastUpdated;
2061 return ( 1, $self->loc("Merge Successful") );
2066 Returns list of tickets' ids that's been merged into this ticket.
2074 return @{ $MERGE_CACHE{'merged'}{ $id } }
2075 if $MERGE_CACHE{'merged'}{ $id };
2077 my $mergees = RT::Tickets->new( $self->CurrentUser );
2078 $mergees->LimitField(
2079 FIELD => 'EffectiveId',
2082 $mergees->LimitField(
2087 return @{ $MERGE_CACHE{'merged'}{ $id } ||= [] }
2088 = map $_->id, @{ $mergees->ItemsArrayRef || [] };
2097 Takes nothing and returns an RT::User object of
2105 #If this gets ACLed, we lose on a rights check in User.pm and
2106 #get deep recursion. if we need ACLs here, we need
2107 #an equiv without ACLs
2109 my $owner = RT::User->new( $self->CurrentUser );
2110 $owner->Load( $self->__Value('Owner') );
2112 #Return the owner object
2118 =head2 OwnerAsString
2120 Returns the owner's email address
2126 return ( $self->OwnerObj->EmailAddress );
2134 Takes two arguments:
2135 the Id or Name of the owner
2136 and (optionally) the type of the SetOwner Transaction. It defaults
2137 to 'Set'. 'Steal' is also a valid option.
2144 my $NewOwner = shift;
2145 my $Type = shift || "Set";
2147 $RT::Handle->BeginTransaction();
2149 $self->_SetLastUpdated(); # lock the ticket
2150 $self->Load( $self->id ); # in case $self changed while waiting for lock
2152 my $OldOwnerObj = $self->OwnerObj;
2154 my $NewOwnerObj = RT::User->new( $self->CurrentUser );
2155 $NewOwnerObj->Load( $NewOwner );
2157 my ( $val, $msg ) = $self->CurrentUserCanSetOwner(
2158 NewOwnerObj => $NewOwnerObj,
2162 $RT::Handle->Rollback();
2163 return ( $val, $msg );
2166 ($val, $msg ) = $self->OwnerGroup->_AddMember(
2167 PrincipalId => $NewOwnerObj->PrincipalId,
2168 InsideTransaction => 1,
2172 $RT::Handle->Rollback;
2173 return ( 0, $self->loc("Could not change owner: [_1]", $msg) );
2176 $msg = $self->loc( "Owner changed from [_1] to [_2]",
2177 $OldOwnerObj->Name, $NewOwnerObj->Name );
2179 $RT::Handle->Commit();
2181 return ( $val, $msg );
2184 =head2 CurrentUserCanSetOwner
2186 Confirm the current user can set the owner of the current ticket.
2188 There are several different rights to manage owner changes and
2189 this method evaluates these rights, guided by parameters provided.
2191 This method evaluates these rights in the context of the state of
2192 the current ticket. For example, it evaluates Take for tickets that
2193 are owned by Nobody because that is the context appropriate for the
2194 TakeTicket right. If you need to strictly test a user for a right,
2195 use HasRight to check for the right directly.
2197 For some custom types of owner changes (C<Take> and C<Steal>), it also
2198 verifies that those actions are possible given the current ticket owner.
2200 =head3 Rights to Set Owner
2202 The current user can set or change the Owner field in the following
2209 ReassignTicket unconditionally grants the right to set the owner
2210 to any user who has OwnTicket. This can be used to break an
2211 Owner lock held by another user (see below) and can be a convenient
2212 right for managers or administrators who need to assign tickets
2213 without necessarily owning them.
2217 ModifyTicket grants the right to set the owner to any user who
2218 has OwnTicket, provided the ticket is currently owned by the current
2219 user or is not owned (owned by Nobody). (See the details on the Force
2220 parameter below for exceptions to this.)
2224 If the ticket is currently not owned (owned by Nobody),
2225 TakeTicket is sufficient to set the owner to yourself (but not
2226 an arbitrary person), but only if you have OwnTicket. It is
2227 thus a subset of the possible changes provided by ModifyTicket.
2228 This exists to allow granting TakeTicket freely, and
2229 the broader ModifyTicket only to Owners.
2233 If the ticket is currently owned by someone who is not you or
2234 Nobody, StealTicket is sufficient to set the owner to yourself,
2235 but only if you have OwnTicket. This is hence non-overlapping
2236 with the changes provided by ModifyTicket, and is used to break
2237 a lock held by another user.
2243 This method returns ($result, $message) with $result containing
2244 true or false indicating if the current user can set owner and $message
2245 containing a message, typically in the case of a false response.
2247 If called with no parameters, this method determines if the current
2248 user could set the owner of the current ticket given any
2249 permutation of the rights described above. This can be useful
2250 when determining whether to make owner-setting options available
2253 This method accepts the following parameters as a paramshash:
2257 =item C<NewOwnerObj>
2259 Optional; an L<RT::User> object representing the proposed new owner of
2264 Optional; the type of set owner operation. Valid values are C<Take>,
2265 C<Steal>, or C<Force>. Note that if the type is C<Take>, this method
2266 will return false if the current user is already the owner; similarly,
2267 it will return false for C<Steal> if the ticket has no owner or the
2268 owner is the current user.
2272 As noted above, there are exceptions to the standard ticket-based rights
2273 described here. The Force option allows for these and is used
2274 when moving tickets between queues, for reminders (because the full
2275 owner rights system is too complex for them), and optionally during
2280 sub CurrentUserCanSetOwner {
2282 my %args = ( Type => '',
2284 my $OldOwnerObj = $self->OwnerObj;
2286 $args{NewOwnerObj} ||= $self->CurrentUser->UserObj
2287 if $args{Type} eq "Take" or $args{Type} eq "Steal";
2289 # Confirm rights for new owner if we got one
2290 if ( $args{'NewOwnerObj'} ){
2291 my ($ok, $message) = $self->_NewOwnerCanOwnTicket($args{'NewOwnerObj'}, $OldOwnerObj);
2292 return ($ok, $message) if not $ok;
2295 # ReassignTicket allows you to SetOwner, but we also need to check ticket's
2296 # current owner for Take and Steal Types
2297 return ( 1, undef ) if $self->CurrentUserHasRight('ReassignTicket')
2298 && $args{Type} ne 'Take' && $args{Type} ne 'Steal';
2301 if ( $OldOwnerObj->Id == RT->Nobody->Id ) {
2303 # Steal is not applicable for unowned tickets.
2304 if ( $args{'Type'} eq 'Steal' ){
2305 return ( 0, $self->loc("You can only steal a ticket owned by someone else") )
2308 # Can set owner to yourself with ModifyTicket, ReassignTicket,
2309 # or TakeTicket; in all of these cases, OwnTicket is checked by
2310 # _NewOwnerCanOwnTicket above.
2311 if ( $args{'Type'} eq 'Take'
2312 or ( $args{'NewOwnerObj'}
2313 and $args{'NewOwnerObj'}->id == $self->CurrentUser->id )) {
2314 unless ( $self->CurrentUserHasRight('ModifyTicket')
2315 or $self->CurrentUserHasRight('ReassignTicket')
2316 or $self->CurrentUserHasRight('TakeTicket') ) {
2317 return ( 0, $self->loc("Permission Denied") );
2320 # Nobody -> someone else requires ModifyTicket or ReassignTicket
2321 unless ( $self->CurrentUserHasRight('ModifyTicket')
2322 or $self->CurrentUserHasRight('ReassignTicket') ) {
2323 return ( 0, $self->loc("Permission Denied") );
2328 # Ticket is owned by someone else
2329 # Can set owner to yourself with ModifyTicket or StealTicket
2331 elsif ( $OldOwnerObj->Id != RT->Nobody->Id
2332 && $OldOwnerObj->Id != $self->CurrentUser->id ) {
2334 unless ( $self->CurrentUserHasRight('ModifyTicket')
2335 || $self->CurrentUserHasRight('ReassignTicket')
2336 || $self->CurrentUserHasRight('StealTicket') ) {
2337 return ( 0, $self->loc("Permission Denied") )
2340 if ( $args{'Type'} eq 'Steal' || $args{'Type'} eq 'Force' ){
2341 return ( 1, undef ) if $self->CurrentUserHasRight('OwnTicket');
2342 return ( 0, $self->loc("Permission Denied") );
2345 # Not a steal or force
2346 if ( $args{'Type'} eq 'Take'
2347 or ( $args{'NewOwnerObj'}
2348 and $args{'NewOwnerObj'}->id == $self->CurrentUser->id )) {
2349 return ( 0, $self->loc("You can only take tickets that are unowned") );
2352 unless ( $self->CurrentUserHasRight('ReassignTicket') ) {
2353 return ( 0, $self->loc( "You can only reassign tickets that you own or that are unowned"));
2357 # You own the ticket
2358 # Untake falls through to here, so we don't need to explicitly handle that Type
2360 if ( $args{'Type'} eq 'Take' || $args{'Type'} eq 'Steal' ) {
2361 return ( 0, $self->loc("You already own this ticket") );
2364 unless ( $self->CurrentUserHasRight('ModifyTicket')
2365 || $self->CurrentUserHasRight('ReassignTicket') ) {
2366 return ( 0, $self->loc("Permission Denied") );
2370 return ( 1, undef );
2373 # Verify the proposed new owner can own the ticket.
2375 sub _NewOwnerCanOwnTicket {
2377 my $NewOwnerObj = shift;
2378 my $OldOwnerObj = shift;
2380 unless ( $NewOwnerObj->Id ) {
2381 return ( 0, $self->loc("That user does not exist") );
2384 # The proposed new owner can't own the ticket
2385 if ( !$NewOwnerObj->HasRight( Right => 'OwnTicket', Object => $self ) ){
2386 return ( 0, $self->loc("That user may not own tickets in that queue") );
2389 # Ticket's current owner is the same as the new owner, nothing to do
2390 elsif ( $NewOwnerObj->Id == $OldOwnerObj->Id ) {
2391 return ( 0, $self->loc("That user already owns that ticket") );
2399 A convenince method to set the ticket's owner to the current user
2405 return ( $self->SetOwner( $self->CurrentUser->Id, 'Take' ) );
2412 Convenience method to set the owner to 'nobody' if the current user is the owner.
2418 return ( $self->SetOwner( RT->Nobody->UserObj->Id, 'Untake' ) );
2425 A convenience method to change the owner of the current ticket to the
2426 current user. Even if it's owned by another user.
2433 if ( $self->IsOwner( $self->CurrentUser ) ) {
2434 return ( 0, $self->loc("You already own this ticket") );
2437 return ( $self->SetOwner( $self->CurrentUser->Id, 'Steal' ) );
2443 =head2 SetStatus STATUS
2445 Set this ticket's status.
2447 Alternatively, you can pass in a list of named parameters (Status => STATUS, Force => FORCE, SetStarted => SETSTARTED ).
2448 If FORCE is true, ignore unresolved dependencies and force a status change.
2449 if SETSTARTED is true (it's the default value), set Started to current datetime if Started
2450 is not set and the status is changed from initial to not initial.
2458 $args{Status} = shift;
2464 # this only allows us to SetStarted, not we must SetStarted.
2465 # this option was added for rtir initially
2466 $args{SetStarted} = 1 unless exists $args{SetStarted};
2468 my ($valid, $msg) = $self->ValidateStatusChange($args{Status});
2469 return ($valid, $msg) unless $valid;
2471 my $lifecycle = $self->LifecycleObj;
2474 && !$lifecycle->IsInactive($self->Status)
2475 && $lifecycle->IsInactive($args{Status})
2476 && $self->HasUnresolvedDependencies )
2478 return ( 0, $self->loc('That ticket has unresolved dependencies') );
2481 return $self->_SetStatus(
2482 Status => $args{Status},
2483 SetStarted => $args{SetStarted},
2492 RecordTransaction => 1,
2493 Lifecycle => $self->LifecycleObj,
2496 $args{Status} = lc $args{Status} if defined $args{Status};
2497 $args{NewLifecycle} ||= $args{Lifecycle};
2499 my $now = RT::Date->new( $self->CurrentUser );
2502 my $raw_started = RT::Date->new(RT->SystemUser);
2503 $raw_started->Set(Format => 'ISO', Value => $self->__Value('Started'));
2505 my $old = $self->__Value('Status');
2507 # If we're changing the status from new, record that we've started
2508 if ( $args{SetStarted}
2509 && $args{Lifecycle}->IsInitial($old)
2510 && !$args{NewLifecycle}->IsInitial($args{Status})
2511 && !$raw_started->IsSet) {
2512 # Set the Started time to "now"
2516 RecordTransaction => 0
2520 # When we close a ticket, set the 'Resolved' attribute to now.
2521 # It's misnamed, but that's just historical.
2522 if ( $args{NewLifecycle}->IsInactive($args{Status}) ) {
2524 Field => 'Resolved',
2526 RecordTransaction => 0,
2530 # Actually update the status
2531 my ($val, $msg)= $self->_Set(
2533 Value => $args{Status},
2536 TransactionType => 'Status',
2537 RecordTransaction => $args{RecordTransaction},
2539 return ($val, $msg);
2546 my $taken = ($value||0) - ($self->__Value('TimeWorked')||0);
2549 Field => 'TimeWorked',
2551 TimeTaken => $taken,
2557 Takes no arguments. Marks this ticket for garbage collection
2563 unless ( $self->LifecycleObj->IsValid('deleted') ) {
2564 return (0, $self->loc('Delete operation is disabled by lifecycle configuration') ); #loc
2566 return ( $self->SetStatus('deleted') );
2570 =head2 SetTold ISO [TIMETAKEN]
2572 Updates the told and records a transaction
2579 $told = shift if (@_);
2580 my $timetaken = shift || 0;
2582 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
2583 return ( 0, $self->loc("Permission Denied") );
2586 my $datetold = RT::Date->new( $self->CurrentUser );
2588 $datetold->Set( Format => 'iso',
2592 $datetold->SetToNow();
2595 return ( $self->_Set( Field => 'Told',
2596 Value => $datetold->ISO,
2597 TimeTaken => $timetaken,
2598 TransactionType => 'Told' ) );
2603 Updates the told without a transaction or acl check. Useful when we're sending replies.
2610 my $now = RT::Date->new( $self->CurrentUser );
2613 #use __Set to get no ACLs ;)
2614 return ( $self->__Set( Field => 'Told',
2615 Value => $now->ISO ) );
2625 my $uid = $self->CurrentUser->id;
2626 my $attr = $self->FirstAttribute( "User-". $uid ."-SeenUpTo" );
2627 return if $attr && $attr->Content gt $self->LastUpdated;
2629 my $txns = $self->Transactions;
2630 $txns->Limit( FIELD => 'Type', VALUE => 'Comment' );
2631 $txns->Limit( FIELD => 'Type', VALUE => 'Correspond' );
2632 $txns->Limit( FIELD => 'Creator', OPERATOR => '!=', VALUE => $uid );
2636 VALUE => $attr->Content
2638 $txns->RowsPerPage(1);
2639 return $txns->First;
2642 =head2 RanTransactionBatch
2644 Acts as a guard around running TransactionBatch scrips.
2646 Should be false until you enter the code that runs TransactionBatch scrips
2648 Accepts an optional argument to indicate that TransactionBatch Scrips should no longer be run on this object.
2652 sub RanTransactionBatch {
2656 if ( defined $val ) {
2657 return $self->{_RanTransactionBatch} = $val;
2659 return $self->{_RanTransactionBatch};
2665 =head2 TransactionBatch
2667 Returns an array reference of all transactions created on this ticket during
2668 this ticket object's lifetime or since last application of a batch, or undef
2671 Only works when the C<UseTransactionBatch> config option is set to true.
2675 sub TransactionBatch {
2677 return $self->{_TransactionBatch};
2680 =head2 ApplyTransactionBatch
2682 Applies scrips on the current batch of transactions and shinks it. Usually
2683 batch is applied when object is destroyed, but in some cases it's too late.
2687 sub ApplyTransactionBatch {
2690 my $batch = $self->TransactionBatch;
2691 return unless $batch && @$batch;
2693 $self->_ApplyTransactionBatch;
2695 $self->{_TransactionBatch} = [];
2698 sub _ApplyTransactionBatch {
2701 return if $self->RanTransactionBatch;
2702 $self->RanTransactionBatch(1);
2704 my $still_exists = RT::Ticket->new( RT->SystemUser );
2705 $still_exists->Load( $self->Id );
2706 if (not $still_exists->Id) {
2707 # The ticket has been removed from the database, but we still
2708 # have pending TransactionBatch txns for it. Unfortunately,
2709 # because it isn't in the DB anymore, attempting to run scrips
2710 # on it may produce unpredictable results; simply drop the
2711 # batched transactions.
2712 $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.");
2716 my $batch = $self->TransactionBatch;
2719 my $types = join ',', grep !$seen{$_}++, grep defined, map $_->__Value('Type'), grep defined, @{$batch};
2722 RT::Scrips->new(RT->SystemUser)->Apply(
2723 Stage => 'TransactionBatch',
2725 TransactionObj => $batch->[0],
2729 # Entry point of the rule system
2730 my $rules = RT::Ruleset->FindAllRules(
2731 Stage => 'TransactionBatch',
2733 TransactionObj => $batch->[0],
2736 RT::Ruleset->CommitRules($rules);
2742 # DESTROY methods need to localize $@, or it may unset it. This
2743 # causes $m->abort to not bubble all of the way up. See perlbug
2744 # http://rt.perl.org/rt3/Ticket/Display.html?id=17650
2747 # The following line eliminates reentrancy.
2748 # It protects against the fact that perl doesn't deal gracefully
2749 # when an object's refcount is changed in its destructor.
2750 return if $self->{_Destroyed}++;
2752 if (in_global_destruction()) {
2753 unless ($ENV{'HARNESS_ACTIVE'}) {
2754 warn "Too late to safely run transaction-batch scrips!"
2755 ." This is typically caused by using ticket objects"
2756 ." at the top-level of a script which uses the RT API."
2757 ." Be sure to explicitly undef such ticket objects,"
2758 ." or put them inside of a lexical scope.";
2763 return $self->ApplyTransactionBatch;
2769 sub _OverlayAccessible {
2771 EffectiveId => { 'read' => 1, 'write' => 1, 'public' => 1 },
2772 Queue => { 'read' => 1, 'write' => 1 },
2773 Requestors => { 'read' => 1, 'write' => 1 },
2774 Owner => { 'read' => 1, 'write' => 1 },
2775 Subject => { 'read' => 1, 'write' => 1 },
2776 InitialPriority => { 'read' => 1, 'write' => 1 },
2777 FinalPriority => { 'read' => 1, 'write' => 1 },
2778 Priority => { 'read' => 1, 'write' => 1 },
2779 Status => { 'read' => 1, 'write' => 1 },
2780 TimeEstimated => { 'read' => 1, 'write' => 1 },
2781 TimeWorked => { 'read' => 1, 'write' => 1 },
2782 TimeLeft => { 'read' => 1, 'write' => 1 },
2783 Told => { 'read' => 1, 'write' => 1 },
2784 Resolved => { 'read' => 1 },
2785 Type => { 'read' => 1 },
2786 Starts => { 'read' => 1, 'write' => 1 },
2787 Started => { 'read' => 1, 'write' => 1 },
2788 Due => { 'read' => 1, 'write' => 1 },
2789 Creator => { 'read' => 1, 'auto' => 1 },
2790 Created => { 'read' => 1, 'auto' => 1 },
2791 LastUpdatedBy => { 'read' => 1, 'auto' => 1 },
2792 LastUpdated => { 'read' => 1, 'auto' => 1 }
2802 my %args = ( Field => undef,
2805 RecordTransaction => 1,
2807 TransactionType => 'Set',
2810 if ($args{'CheckACL'}) {
2811 unless ( $self->CurrentUserHasRight('ModifyTicket')) {
2812 return ( 0, $self->loc("Permission Denied"));
2816 # Avoid ACL loops using _Value
2817 my $Old = $self->SUPER::_Value($args{'Field'});
2820 my ( $ret, $msg ) = $self->SUPER::_Set(
2821 Field => $args{'Field'},
2822 Value => $args{'Value'}
2824 return ( 0, $msg ) unless $ret;
2826 return ( $ret, $msg ) unless $args{'RecordTransaction'};
2829 ( $ret, $msg, $trans ) = $self->_NewTransaction(
2830 Type => $args{'TransactionType'},
2831 Field => $args{'Field'},
2832 NewValue => $args{'Value'},
2834 TimeTaken => $args{'TimeTaken'},
2837 # Ensure that we can read the transaction, even if the change
2838 # just made the ticket unreadable to us
2839 $trans->{ _object_is_readable } = 1;
2841 return ( $ret, scalar $trans->BriefDescription );
2848 Takes the name of a table column.
2849 Returns its value as a string, if the user passes an ACL check
2858 #if the field is public, return it.
2859 if ( $self->_Accessible( $field, 'public' ) ) {
2861 #$RT::Logger->debug("Skipping ACL check for $field");
2862 return ( $self->SUPER::_Value($field) );
2866 #If the current user doesn't have ACLs, don't let em at it.
2868 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
2871 return ( $self->SUPER::_Value($field) );
2877 Customization of L<RT::Record/Attachments> for tickets.
2888 my $res = RT::Attachments->new( $self->CurrentUser );
2889 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
2894 ENTRYAGGREGATOR => 'AND'
2899 my @columns = grep { not /^(Headers|Content)$/ }
2900 RT::Attachment->ReadableAttributes;
2901 push @columns, 'Headers' if $args{'WithHeaders'};
2902 push @columns, 'Content' if $args{'WithContent'};
2904 $res->Columns( @columns );
2905 my $txn_alias = $res->TransactionAlias;
2907 ALIAS => $txn_alias,
2908 FIELD => 'ObjectType',
2909 VALUE => ref($self),
2911 my $ticket_alias = $res->Join(
2912 ALIAS1 => $txn_alias,
2913 FIELD1 => 'ObjectId',
2914 TABLE2 => 'Tickets',
2918 ALIAS => $ticket_alias,
2919 FIELD => 'EffectiveId',
2925 =head2 TextAttachments
2927 Customization of L<RT::Record/TextAttachments> for tickets.
2931 sub TextAttachments {
2934 my $res = $self->SUPER::TextAttachments( @_ );
2935 unless ( $self->CurrentUserHasRight('ShowTicketComments') ) {
2936 # if the user may not see comments do not return them
2939 ALIAS => $res->TransactionAlias,
2951 =head2 _UpdateTimeTaken
2953 This routine will increment the timeworked counter. it should
2954 only be called from _NewTransaction
2958 sub _UpdateTimeTaken {
2960 my $Minutes = shift;
2963 if ( my $txn = $rest{'Transaction'} ) {
2964 return if $txn->__Value('Type') eq 'Set' && $txn->__Value('Field') eq 'TimeWorked';
2967 my $Total = $self->__Value("TimeWorked");
2968 $Total = ( $Total || 0 ) + ( $Minutes || 0 );
2970 Field => "TimeWorked",
2972 RecordTransaction => 0,
2979 =head2 CurrentUserCanSee
2981 Returns true if the current user can see the ticket, using ShowTicket
2985 sub CurrentUserCanSee {
2987 my ($what, $txn) = @_;
2988 return 0 unless $self->CurrentUserHasRight('ShowTicket');
2990 return 1 if $what ne "Transaction";
2992 # If it's a comment, we need to be extra special careful
2993 my $type = $txn->__Value('Type');
2994 if ( $type eq 'Comment' ) {
2995 unless ( $self->CurrentUserHasRight('ShowTicketComments') ) {
2998 } elsif ( $type eq 'CommentEmailRecord' ) {
2999 unless ( $self->CurrentUserHasRight('ShowTicketComments')
3000 && $self->CurrentUserHasRight('ShowOutgoingEmail') ) {
3003 } elsif ( $type eq 'EmailRecord' ) {
3004 unless ( $self->CurrentUserHasRight('ShowOutgoingEmail') ) {
3013 Return the Reminders object for this ticket. (It's an RT::Reminders object.)
3014 It isn't acutally a searchbuilder collection itself.
3021 unless ($self->{'__reminders'}) {
3022 $self->{'__reminders'} = RT::Reminders->new($self->CurrentUser);
3023 $self->{'__reminders'}->Ticket($self->id);
3025 return $self->{'__reminders'};
3034 Returns an RT::Transactions object of all transactions on this ticket
3041 my $transactions = RT::Transactions->new( $self->CurrentUser );
3043 #If the user has no rights, return an empty object
3044 if ( $self->CurrentUserHasRight('ShowTicket') ) {
3045 $transactions->LimitToTicket($self->id);
3047 # if the user may not see comments do not return them
3048 unless ( $self->CurrentUserHasRight('ShowTicketComments') ) {
3049 $transactions->Limit(
3055 $transactions->Limit(
3059 VALUE => "CommentEmailRecord",
3060 ENTRYAGGREGATOR => 'AND'
3065 $transactions->Limit(
3069 ENTRYAGGREGATOR => 'AND'
3073 return ($transactions);
3079 =head2 TransactionCustomFields
3081 Returns the custom fields that transactions on tickets will have.
3085 sub TransactionCustomFields {
3087 my $cfs = $self->QueueObj->TicketTransactionCustomFields;
3088 $cfs->SetContextObject( $self );
3093 =head2 LoadCustomFieldByIdentifier
3095 Finds and returns the custom field of the given name for the ticket,
3096 overriding L<RT::Record/LoadCustomFieldByIdentifier> to look for
3097 queue-specific CFs before global ones.
3101 sub LoadCustomFieldByIdentifier {
3105 return $self->SUPER::LoadCustomFieldByIdentifier($field)
3106 if ref $field or $field =~ /^\d+$/;
3108 my $cf = RT::CustomField->new( $self->CurrentUser );
3109 $cf->SetContextObject( $self );
3112 LookupType => $self->CustomFieldLookupType,
3113 ObjectId => $self->Queue,
3120 =head2 CustomFieldLookupType
3122 Returns the RT::Ticket lookup type, which can be passed to
3123 RT::CustomField->Create() via the 'LookupType' hash key.
3128 sub CustomFieldLookupType {
3129 "RT::Queue-RT::Ticket";
3132 =head2 ACLEquivalenceObjects
3134 This method returns a list of objects for which a user's rights also apply
3135 to this ticket. Generally, this is only the ticket's queue, but some RT
3136 extensions may make other objects available too.
3138 This method is called from L<RT::Principal/HasRight>.
3142 sub ACLEquivalenceObjects {
3144 return $self->QueueObj;
3148 =head2 ModifyLinkRight
3152 sub ModifyLinkRight { "ModifyTicket" }
3154 =head2 Forward Transaction => undef, To => '', Cc => '', Bcc => ''
3156 Forwards transaction with all attachments as 'message/rfc822'.
3163 Transaction => undef,
3169 ContentType => 'text/plain',
3175 unless ( $self->CurrentUserHasRight('ForwardMessage') ) {
3176 return ( 0, $self->loc("Permission Denied") );
3179 $args{$_} = join ", ", map { $_->format } RT::EmailParser->ParseEmailAddress( $args{$_} || '' ) for qw(To Cc Bcc);
3181 return (0, $self->loc("Can't forward: no valid email addresses specified") )
3182 unless grep {length $args{$_}} qw/To Cc Bcc/;
3184 my $mime = MIME::Entity->build(
3185 Type => $args{ContentType},
3186 Data => Encode::encode( "UTF-8", $args{Content} ),
3189 $mime->head->replace( $_ => Encode::encode('UTF-8',$args{$_} ) )
3190 for grep defined $args{$_}, qw(Subject To Cc Bcc);
3191 $mime->head->replace(
3192 From => Encode::encode( 'UTF-8',
3193 RT::Interface::Email::GetForwardFrom(
3194 Transaction => $args{Transaction},
3200 if ($args{'DryRun'}) {
3201 $RT::Handle->BeginTransaction();
3202 $args{'CommitScrips'} = 0;
3205 my ( $ret, $msg ) = $self->_NewTransaction(
3208 Type => 'Forward Transaction',
3209 Field => $args{Transaction}->id,
3212 Type => 'Forward Ticket',
3215 Data => join( ', ', grep { length } $args{To}, $args{Cc}, $args{Bcc} ),
3217 CommitScrips => $args{'CommitScrips'},
3221 $RT::Logger->error("Failed to create transaction: $msg");
3224 if ($args{'DryRun'}) {
3225 $RT::Handle->Rollback();
3227 return ( $ret, $self->loc('Message recorded') );
3234 Jesse Vincent, jesse@bestpractical.com
3242 sub Table {'Tickets'}
3251 Returns the current value of id.
3252 (In the database, id is stored as int(11).)
3260 Returns the current value of EffectiveId.
3261 (In the database, EffectiveId is stored as int(11).)
3265 =head2 SetEffectiveId VALUE
3268 Set EffectiveId to VALUE.
3269 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
3270 (In the database, EffectiveId will be stored as a int(11).)
3278 Returns the current value of Queue.
3279 (In the database, Queue is stored as int(11).)
3283 =head2 SetQueue VALUE
3287 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
3288 (In the database, Queue will be stored as a int(11).)
3296 Returns the current value of Type.
3297 (In the database, Type is stored as varchar(16).)
3301 =head2 SetType VALUE
3305 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
3306 (In the database, Type will be stored as a varchar(16).)
3312 =head2 IssueStatement
3314 Returns the current value of IssueStatement.
3315 (In the database, IssueStatement is stored as int(11).)
3319 =head2 SetIssueStatement VALUE
3322 Set IssueStatement to VALUE.
3323 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
3324 (In the database, IssueStatement will be stored as a int(11).)
3332 Returns the current value of Resolution.
3333 (In the database, Resolution is stored as int(11).)
3337 =head2 SetResolution VALUE
3340 Set Resolution to VALUE.
3341 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
3342 (In the database, Resolution will be stored as a int(11).)
3350 Returns the current value of Owner.
3351 (In the database, Owner is stored as int(11).)
3355 =head2 SetOwner VALUE
3359 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
3360 (In the database, Owner will be stored as a int(11).)
3368 Returns the current value of Subject.
3369 (In the database, Subject is stored as varchar(200).)
3373 =head2 SetSubject VALUE
3376 Set Subject to VALUE.
3377 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
3378 (In the database, Subject will be stored as a varchar(200).)
3384 =head2 InitialPriority
3386 Returns the current value of InitialPriority.
3387 (In the database, InitialPriority is stored as int(11).)
3391 =head2 SetInitialPriority VALUE
3394 Set InitialPriority to VALUE.
3395 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
3396 (In the database, InitialPriority will be stored as a int(11).)
3402 =head2 FinalPriority
3404 Returns the current value of FinalPriority.
3405 (In the database, FinalPriority is stored as int(11).)
3409 =head2 SetFinalPriority VALUE
3412 Set FinalPriority to VALUE.
3413 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
3414 (In the database, FinalPriority will be stored as a int(11).)
3422 Returns the current value of Priority.
3423 (In the database, Priority is stored as int(11).)
3427 =head2 SetPriority VALUE
3430 Set Priority to VALUE.
3431 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
3432 (In the database, Priority will be stored as a int(11).)
3438 =head2 TimeEstimated
3440 Returns the current value of TimeEstimated.
3441 (In the database, TimeEstimated is stored as int(11).)
3445 =head2 SetTimeEstimated VALUE
3448 Set TimeEstimated to VALUE.
3449 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
3450 (In the database, TimeEstimated will be stored as a int(11).)
3458 Returns the current value of TimeWorked.
3459 (In the database, TimeWorked is stored as int(11).)
3463 =head2 SetTimeWorked VALUE
3466 Set TimeWorked to VALUE.
3467 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
3468 (In the database, TimeWorked will be stored as a int(11).)
3476 Returns the current value of Status.
3477 (In the database, Status is stored as varchar(64).)
3481 =head2 SetStatus VALUE
3484 Set Status to VALUE.
3485 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
3486 (In the database, Status will be stored as a varchar(64).)
3494 Returns the current value of TimeLeft.
3495 (In the database, TimeLeft is stored as int(11).)
3499 =head2 SetTimeLeft VALUE
3502 Set TimeLeft to VALUE.
3503 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
3504 (In the database, TimeLeft will be stored as a int(11).)
3512 Returns the current value of Told.
3513 (In the database, Told is stored as datetime.)
3517 =head2 SetTold VALUE
3521 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
3522 (In the database, Told will be stored as a datetime.)
3530 Returns the current value of Starts.
3531 (In the database, Starts is stored as datetime.)
3535 =head2 SetStarts VALUE
3538 Set Starts to VALUE.
3539 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
3540 (In the database, Starts will be stored as a datetime.)
3548 Returns the current value of Started.
3549 (In the database, Started is stored as datetime.)
3553 =head2 SetStarted VALUE
3556 Set Started to VALUE.
3557 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
3558 (In the database, Started will be stored as a datetime.)
3566 Returns the current value of Due.
3567 (In the database, Due is stored as datetime.)
3575 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
3576 (In the database, Due will be stored as a datetime.)
3584 Returns the current value of Resolved.
3585 (In the database, Resolved is stored as datetime.)
3589 =head2 SetResolved VALUE
3592 Set Resolved to VALUE.
3593 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
3594 (In the database, Resolved will be stored as a datetime.)
3600 =head2 LastUpdatedBy
3602 Returns the current value of LastUpdatedBy.
3603 (In the database, LastUpdatedBy is stored as int(11).)
3611 Returns the current value of LastUpdated.
3612 (In the database, LastUpdated is stored as datetime.)
3620 Returns the current value of Creator.
3621 (In the database, Creator is stored as int(11).)
3629 Returns the current value of Created.
3630 (In the database, Created is stored as datetime.)
3638 Returns the current value of Disabled.
3639 (In the database, Disabled is stored as smallint(6).)
3643 =head2 SetDisabled VALUE
3646 Set Disabled to VALUE.
3647 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
3648 (In the database, Disabled will be stored as a smallint(6).)
3655 sub _CoreAccessible {
3659 {read => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', 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 => 5, length => 6, is_blob => 0, is_numeric => 1, type => 'smallint(6)', default => undef},
3665 {read => 1, write => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'},
3667 {read => 1, write => 1, sql_type => 12, length => 16, is_blob => 0, is_numeric => 0, type => 'varchar(16)', default => ''},
3669 {read => 1, write => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'},
3671 {read => 1, write => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'},
3673 {read => 1, write => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'},
3675 {read => 1, write => 1, sql_type => 12, length => 200, is_blob => 0, is_numeric => 0, type => 'varchar(200)', default => '[no subject]'},
3677 {read => 1, write => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'},
3679 {read => 1, write => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'},
3681 {read => 1, write => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'},
3683 {read => 1, write => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'},
3685 {read => 1, write => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'},
3687 {read => 1, write => 1, sql_type => 12, length => 64, is_blob => 0, is_numeric => 0, type => 'varchar(64)', default => ''},
3689 {read => 1, write => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'},
3691 {read => 1, write => 1, sql_type => 11, length => 0, is_blob => 0, is_numeric => 0, type => 'datetime', default => ''},
3693 {read => 1, write => 1, sql_type => 11, length => 0, is_blob => 0, is_numeric => 0, type => 'datetime', default => ''},
3695 {read => 1, write => 1, sql_type => 11, length => 0, is_blob => 0, is_numeric => 0, type => 'datetime', default => ''},
3697 {read => 1, write => 1, sql_type => 11, length => 0, is_blob => 0, is_numeric => 0, type => 'datetime', default => ''},
3699 {read => 1, write => 1, sql_type => 11, length => 0, is_blob => 0, is_numeric => 0, type => 'datetime', default => ''},
3701 {read => 1, auto => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'},
3703 {read => 1, auto => 1, sql_type => 11, length => 0, is_blob => 0, is_numeric => 0, type => 'datetime', default => ''},
3705 {read => 1, auto => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'},
3707 {read => 1, auto => 1, sql_type => 11, length => 0, is_blob => 0, is_numeric => 0, type => 'datetime', default => ''},
3709 {read => 1, write => 1, sql_type => 5, length => 6, is_blob => 0, is_numeric => 1, type => 'smallint(6)', default => '0'},
3714 sub FindDependencies {
3716 my ($walker, $deps) = @_;
3718 $self->SUPER::FindDependencies($walker, $deps);
3721 my $links = RT::Links->new( $self->CurrentUser );
3723 SUBCLAUSE => "either",
3725 VALUE => $self->URI,
3726 ENTRYAGGREGATOR => 'OR'
3727 ) for qw/Base Target/;
3728 $deps->Add( in => $links );
3730 # Tickets which were merged in
3731 my $objs = RT::Tickets->new( $self->CurrentUser );
3732 $objs->Limit( FIELD => 'EffectiveId', VALUE => $self->Id );
3733 $objs->Limit( FIELD => 'id', OPERATOR => '!=', VALUE => $self->Id );
3734 $deps->Add( in => $objs );
3736 # Ticket role groups( Owner, Requestors, Cc, AdminCc )
3737 $objs = RT::Groups->new( $self->CurrentUser );
3738 $objs->Limit( FIELD => 'Domain', VALUE => 'RT::Ticket-Role', CASESENSITIVE => 0 );
3739 $objs->Limit( FIELD => 'Instance', VALUE => $self->Id );
3740 $deps->Add( in => $objs );
3743 $deps->Add( out => $self->QueueObj );
3746 $deps->Add( out => $self->OwnerObj );
3753 Dependencies => undef,
3756 my $deps = $args{'Dependencies'};
3759 # Tickets which were merged in
3760 my $objs = RT::Tickets->new( $self->CurrentUser );
3761 $objs->{'allow_deleted_search'} = 1;
3762 $objs->Limit( FIELD => 'EffectiveId', VALUE => $self->Id );
3763 $objs->Limit( FIELD => 'id', OPERATOR => '!=', VALUE => $self->Id );
3764 push( @$list, $objs );
3766 # Ticket role groups( Owner, Requestors, Cc, AdminCc )
3767 $objs = RT::Groups->new( $self->CurrentUser );
3768 $objs->Limit( FIELD => 'Domain', VALUE => 'RT::Ticket-Role', CASESENSITIVE => 0 );
3769 $objs->Limit( FIELD => 'Instance', VALUE => $self->Id );
3770 push( @$list, $objs );
3772 #TODO: Users, Queues if we wish export tool
3773 $deps->_PushDependencies(
3774 BaseObject => $self,
3775 Flags => RT::Shredder::Constants::DEPENDS_ON,
3776 TargetObjects => $list,
3777 Shredder => $args{'Shredder'}
3780 return $self->SUPER::__DependsOn( %args );
3786 my %store = $self->SUPER::Serialize(@_);
3788 my $obj = RT::Ticket->new( RT->SystemUser );
3789 $obj->Load( $store{EffectiveId} );
3790 $store{EffectiveId} = \($obj->UID);
3795 RT::Base->_ImportOverlays();