1 # BEGIN BPS TAGGED BLOCK {{{
5 # This software is Copyright (c) 1996-2017 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});
1234 return $self->_Set( Field => 'Subject', Value => $value );
1239 Takes nothing. Returns SubjectTag for this ticket. Includes
1240 queue's subject tag or rtname if that is not set, ticket
1241 id and brackets, for example:
1243 [support.example.com #123456]
1251 . ($self->QueueObj->SubjectTag || RT->Config->Get('rtname'))
1260 Returns an RT::Date object containing this ticket's due date
1267 my $time = RT::Date->new( $self->CurrentUser );
1269 # -1 is RT::Date slang for never
1270 if ( my $due = $self->Due ) {
1271 $time->Set( Format => 'sql', Value => $due );
1274 $time->Set( Format => 'unix', Value => -1 );
1284 Returns this ticket's due date as a human readable string.
1286 B<DEPRECATED> and will be removed in 4.4; use C<<
1287 $ticket->DueObj->AsString >> instead.
1294 Instead => "->DueObj->AsString",
1297 return $self->DueObj->AsString();
1304 Returns an RT::Date object of this ticket's 'resolved' time.
1311 my $time = RT::Date->new( $self->CurrentUser );
1312 $time->Set( Format => 'sql', Value => $self->Resolved );
1316 =head2 FirstActiveStatus
1318 Returns the first active status that the ticket could transition to,
1319 according to its current Queue's lifecycle. May return undef if there
1320 is no such possible status to transition to, or we are already in it.
1321 This is used in L<RT::Action::AutoOpen>, for instance.
1325 sub FirstActiveStatus {
1328 my $lifecycle = $self->LifecycleObj;
1329 my $status = $self->Status;
1330 my @active = $lifecycle->Active;
1331 # no change if no active statuses in the lifecycle
1332 return undef unless @active;
1334 # no change if the ticket is already has first status from the list of active
1335 return undef if lc $status eq lc $active[0];
1337 my ($next) = grep $lifecycle->IsActive($_), $lifecycle->Transitions($status);
1341 =head2 FirstInactiveStatus
1343 Returns the first inactive status that the ticket could transition to,
1344 according to its current Queue's lifecycle. May return undef if there
1345 is no such possible status to transition to, or we are already in it.
1346 This is used in resolve action in UnsafeEmailCommands, for instance.
1350 sub FirstInactiveStatus {
1353 my $lifecycle = $self->LifecycleObj;
1354 my $status = $self->Status;
1355 my @inactive = $lifecycle->Inactive;
1356 # no change if no inactive statuses in the lifecycle
1357 return undef unless @inactive;
1359 # no change if the ticket is already has first status from the list of inactive
1360 return undef if lc $status eq lc $inactive[0];
1362 my ($next) = grep $lifecycle->IsInactive($_), $lifecycle->Transitions($status);
1368 Takes a date in ISO format or undef
1369 Returns a transaction id and a message
1370 The client calls "Start" to note that the project was started on the date in $date.
1371 A null date means "now"
1377 my $time = shift || 0;
1379 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
1380 return ( 0, $self->loc("Permission Denied") );
1383 #We create a date object to catch date weirdness
1384 my $time_obj = RT::Date->new( $self->CurrentUser() );
1386 $time_obj->Set( Format => 'ISO', Value => $time );
1389 $time_obj->SetToNow();
1392 return ( $self->_Set( Field => 'Started', Value => $time_obj->ISO ) );
1400 Returns an RT::Date object which contains this ticket's
1408 my $time = RT::Date->new( $self->CurrentUser );
1409 $time->Set( Format => 'sql', Value => $self->Started );
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->Starts );
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->Told );
1451 A convenience method that returns ToldObj->AsString
1453 B<DEPRECATED> and will be removed in 4.4; use C<<
1454 $ticket->ToldObj->AsString >> instead.
1461 Instead => "->ToldObj->AsString",
1464 if ( $self->Told ) {
1465 return $self->ToldObj->AsString();
1474 sub _DurationAsString {
1477 return "" unless $value;
1478 return RT::Date->new( $self->CurrentUser )
1479 ->DurationAsString( $value * 60 );
1482 =head2 TimeWorkedAsString
1484 Returns the amount of time worked on this ticket as a text string.
1488 sub TimeWorkedAsString {
1490 return $self->_DurationAsString( $self->TimeWorked );
1493 =head2 TimeLeftAsString
1495 Returns the amount of time left on this ticket as a text string.
1499 sub TimeLeftAsString {
1501 return $self->_DurationAsString( $self->TimeLeft );
1504 =head2 TimeEstimatedAsString
1506 Returns the amount of time estimated on this ticket as a text string.
1510 sub TimeEstimatedAsString {
1512 return $self->_DurationAsString( $self->TimeEstimated );
1520 Comment on this ticket.
1521 Takes a hash with the following attributes:
1522 If MIMEObj is undefined, Content will be used to build a MIME::Entity for this
1525 MIMEObj, TimeTaken, CcMessageTo, BccMessageTo, Content, DryRun
1527 If DryRun is defined, this update WILL NOT BE RECORDED. Scrips will not be committed.
1528 They will, however, be prepared and you'll be able to access them through the TransactionObj
1530 Returns: Transaction id, Error Message, Transaction Object
1531 (note the different order from Create()!)
1538 my %args = ( CcMessageTo => undef,
1539 BccMessageTo => undef,
1546 unless ( ( $self->CurrentUserHasRight('CommentOnTicket') )
1547 or ( $self->CurrentUserHasRight('ModifyTicket') ) ) {
1548 return ( 0, $self->loc("Permission Denied"), undef );
1550 $args{'NoteType'} = 'Comment';
1552 $RT::Handle->BeginTransaction();
1553 if ($args{'DryRun'}) {
1554 $args{'CommitScrips'} = 0;
1557 my @results = $self->_RecordNote(%args);
1558 if ($args{'DryRun'}) {
1559 $RT::Handle->Rollback();
1561 $RT::Handle->Commit();
1570 Correspond on this ticket.
1571 Takes a hashref with the following attributes:
1574 MIMEObj, TimeTaken, CcMessageTo, BccMessageTo, Content, DryRun
1576 if there's no MIMEObj, Content is used to build a MIME::Entity object
1578 If DryRun is defined, this update WILL NOT BE RECORDED. Scrips will not be committed.
1579 They will, however, be prepared and you'll be able to access them through the TransactionObj
1581 Returns: Transaction id, Error Message, Transaction Object
1582 (note the different order from Create()!)
1589 my %args = ( CcMessageTo => undef,
1590 BccMessageTo => undef,
1596 unless ( ( $self->CurrentUserHasRight('ReplyToTicket') )
1597 or ( $self->CurrentUserHasRight('ModifyTicket') ) ) {
1598 return ( 0, $self->loc("Permission Denied"), undef );
1600 $args{'NoteType'} = 'Correspond';
1602 $RT::Handle->BeginTransaction();
1603 if ($args{'DryRun'}) {
1604 $args{'CommitScrips'} = 0;
1607 my @results = $self->_RecordNote(%args);
1609 unless ( $results[0] ) {
1610 $RT::Handle->Rollback();
1614 #Set the last told date to now if this isn't mail from the requestor.
1615 #TODO: Note that this will wrongly ack mail from any non-requestor as a "told"
1616 unless ( $self->IsRequestor($self->CurrentUser->id) ) {
1618 $squelch{$_}++ for map {$_->Content} $self->SquelchMailTo, $results[2]->SquelchMailTo;
1620 if grep {not $squelch{$_}} $self->Requestors->MemberEmailAddresses;
1623 if ($args{'DryRun'}) {
1624 $RT::Handle->Rollback();
1626 $RT::Handle->Commit();
1637 the meat of both comment and correspond.
1639 Performs no access control checks. hence, dangerous.
1646 CcMessageTo => undef,
1647 BccMessageTo => undef,
1652 NoteType => 'Correspond',
1655 SquelchMailTo => undef,
1660 unless ( $args{'MIMEObj'} || $args{'Content'} ) {
1661 return ( 0, $self->loc("No message attached"), undef );
1664 unless ( $args{'MIMEObj'} ) {
1665 my $data = ref $args{'Content'}? $args{'Content'} : [ $args{'Content'} ];
1666 $args{'MIMEObj'} = MIME::Entity->build(
1667 Type => "text/plain",
1669 Data => [ map {Encode::encode("UTF-8", $_)} @{$data} ],
1673 $args{'MIMEObj'}->head->replace('X-RT-Interface' => 'API')
1674 unless $args{'MIMEObj'}->head->get('X-RT-Interface');
1676 # convert text parts into utf-8
1677 RT::I18N::SetMIMEEntityToUTF8( $args{'MIMEObj'} );
1679 # If we've been passed in CcMessageTo and BccMessageTo fields,
1680 # add them to the mime object for passing on to the transaction handler
1681 # The "NotifyOtherRecipients" scripAction will look for RT-Send-Cc: and
1682 # RT-Send-Bcc: headers
1685 foreach my $type (qw/Cc Bcc/) {
1686 if ( defined $args{ $type . 'MessageTo' } ) {
1688 my $addresses = join ', ', (
1689 map { RT::User->CanonicalizeEmailAddress( $_->address ) }
1690 Email::Address->parse( $args{ $type . 'MessageTo' } ) );
1691 $args{'MIMEObj'}->head->replace( 'RT-Send-' . $type, Encode::encode( "UTF-8", $addresses ) );
1695 foreach my $argument (qw(Encrypt Sign)) {
1696 $args{'MIMEObj'}->head->replace(
1697 "X-RT-$argument" => $args{ $argument } ? 1 : 0
1698 ) if defined $args{ $argument };
1701 # If this is from an external source, we need to come up with its
1702 # internal Message-ID now, so all emails sent because of this
1703 # message have a common Message-ID
1704 my $org = RT->Config->Get('Organization');
1705 my $msgid = Encode::decode( "UTF-8", $args{'MIMEObj'}->head->get('Message-ID') );
1706 unless (defined $msgid && $msgid =~ /<(rt-.*?-\d+-\d+)\.(\d+-0-0)\@\Q$org\E>/) {
1707 $args{'MIMEObj'}->head->replace(
1708 'RT-Message-ID' => Encode::encode( "UTF-8",
1709 RT::Interface::Email::GenMessageId( Ticket => $self )
1714 #Record the correspondence (write the transaction)
1715 my ( $Trans, $msg, $TransObj ) = $self->_NewTransaction(
1716 Type => $args{'NoteType'},
1717 Data => ( Encode::decode( "UTF-8", $args{'MIMEObj'}->head->get('Subject') ) || 'No Subject' ),
1718 TimeTaken => $args{'TimeTaken'},
1719 MIMEObj => $args{'MIMEObj'},
1720 CommitScrips => $args{'CommitScrips'},
1721 SquelchMailTo => $args{'SquelchMailTo'},
1722 CustomFields => $args{'CustomFields'},
1726 $RT::Logger->err("$self couldn't init a transaction $msg");
1727 return ( $Trans, $self->loc("Message could not be recorded"), undef );
1730 if ($args{NoteType} eq "Comment") {
1731 $msg = $self->loc("Comments added");
1733 $msg = $self->loc("Correspondence added");
1735 return ( $Trans, $msg, $TransObj );
1741 Builds a MIME object from the given C<UpdateSubject> and
1742 C<UpdateContent>, then calls L</Comment> or L</Correspond> with
1743 C<< DryRun => 1 >>, and returns the transaction so produced.
1751 if (($args{'UpdateType'} || $args{Action}) =~ /^respon(d|se)$/i ) {
1752 $action = 'Correspond';
1754 $action = 'Comment';
1757 my $Message = MIME::Entity->build(
1758 Subject => defined $args{UpdateSubject} ? Encode::encode( "UTF-8", $args{UpdateSubject} ) : "",
1759 Type => 'text/plain',
1761 Data => Encode::encode("UTF-8", $args{'UpdateContent'} || ""),
1764 my ( $Transaction, $Description, $Object ) = $self->$action(
1765 CcMessageTo => $args{'UpdateCc'},
1766 BccMessageTo => $args{'UpdateBcc'},
1767 MIMEObj => $Message,
1768 TimeTaken => $args{'UpdateTimeWorked'},
1770 SquelchMailTo => $args{'SquelchMailTo'},
1772 unless ( $Transaction ) {
1773 $RT::Logger->error("Couldn't fire '$action' action: $Description");
1781 Prepares a MIME mesage with the given C<Subject>, C<Cc>, and
1782 C<Content>, then calls L</Create> with C<< DryRun => 1 >> and returns
1783 the resulting L<RT::Transaction>.
1790 my $Message = MIME::Entity->build(
1791 Subject => defined $args{Subject} ? Encode::encode( "UTF-8", $args{'Subject'} ) : "",
1792 (defined $args{'Cc'} ?
1793 ( Cc => Encode::encode( "UTF-8", $args{'Cc'} ) ) : ()),
1794 Type => 'text/plain',
1796 Data => Encode::encode( "UTF-8", $args{'Content'} || ""),
1799 my ( $Transaction, $Object, $Description ) = $self->Create(
1800 Type => $args{'Type'} || 'ticket',
1801 Queue => $args{'Queue'},
1802 Owner => $args{'Owner'},
1803 Requestor => $args{'Requestors'},
1805 AdminCc => $args{'AdminCc'},
1806 InitialPriority => $args{'InitialPriority'},
1807 FinalPriority => $args{'FinalPriority'},
1808 TimeLeft => $args{'TimeLeft'},
1809 TimeEstimated => $args{'TimeEstimated'},
1810 TimeWorked => $args{'TimeWorked'},
1811 Subject => $args{'Subject'},
1812 Status => $args{'Status'},
1813 MIMEObj => $Message,
1816 unless ( $Transaction ) {
1817 $RT::Logger->error("Couldn't fire Create action: $Description");
1828 #TODO: Field isn't the right thing here. but I ahave no idea what mnemonic ---
1831 my $type = shift || "";
1833 my $cache_key = "$field$type";
1834 return $self->{ $cache_key } if $self->{ $cache_key };
1836 my $links = $self->{ $cache_key }
1837 = RT::Links->new( $self->CurrentUser );
1838 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
1839 $links->Limit( FIELD => 'id', VALUE => 0, SUBCLAUSE => 'acl' );
1843 # Maybe this ticket is a merge ticket
1844 my $limit_on = 'Local'. $field;
1845 # at least to myself
1849 VALUE => [ $self->id, $self->Merged ],
1861 MergeInto take the id of the ticket to merge this ticket into.
1867 my $ticket_id = shift;
1869 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
1870 return ( 0, $self->loc("Permission Denied") );
1873 # Load up the new ticket.
1874 my $MergeInto = RT::Ticket->new($self->CurrentUser);
1875 $MergeInto->Load($ticket_id);
1877 # make sure it exists.
1878 unless ( $MergeInto->Id ) {
1879 return ( 0, $self->loc("New ticket doesn't exist") );
1882 # Can't merge into yourself
1883 if ( $MergeInto->Id == $self->Id ) {
1884 return ( 0, $self->loc("Can't merge a ticket into itself") );
1887 # Make sure the current user can modify the new ticket.
1888 unless ( $MergeInto->CurrentUserHasRight('ModifyTicket') ) {
1889 return ( 0, $self->loc("Permission Denied") );
1892 delete $MERGE_CACHE{'effective'}{ $self->id };
1893 delete @{ $MERGE_CACHE{'merged'} }{
1894 $ticket_id, $MergeInto->id, $self->id
1897 $RT::Handle->BeginTransaction();
1899 my ($ok, $msg) = $self->_MergeInto( $MergeInto );
1901 $RT::Handle->Commit() if $ok;
1908 my $MergeInto = shift;
1911 # We use EffectiveId here even though it duplicates information from
1912 # the links table becasue of the massive performance hit we'd take
1913 # by trying to do a separate database query for merge info everytime
1916 #update this ticket's effective id to the new ticket's id.
1917 my ( $id_val, $id_msg ) = $self->__Set(
1918 Field => 'EffectiveId',
1919 Value => $MergeInto->Id()
1923 $RT::Handle->Rollback();
1924 return ( 0, $self->loc("Merge failed. Couldn't set EffectiveId") );
1927 ( $id_val, $id_msg ) = $self->__Set( Field => 'IsMerged', Value => 1 );
1929 $RT::Handle->Rollback();
1930 return ( 0, $self->loc("Merge failed. Couldn't set IsMerged") );
1933 my $force_status = $self->LifecycleObj->DefaultOnMerge;
1934 if ( $force_status && $force_status ne $self->__Value('Status') ) {
1935 my ( $status_val, $status_msg )
1936 = $self->__Set( Field => 'Status', Value => $force_status );
1938 unless ($status_val) {
1939 $RT::Handle->Rollback();
1941 "Couldn't set status to $force_status. RT's Database may be inconsistent."
1943 return ( 0, $self->loc("Merge failed. Couldn't set Status") );
1947 # update all the links that point to that old ticket
1948 my $old_links_to = RT::Links->new($self->CurrentUser);
1949 $old_links_to->Limit(FIELD => 'Target', VALUE => $self->URI);
1952 while (my $link = $old_links_to->Next) {
1953 if (exists $old_seen{$link->Base."-".$link->Type}) {
1956 elsif ($link->Base eq $MergeInto->URI) {
1959 # First, make sure the link doesn't already exist. then move it over.
1960 my $tmp = RT::Link->new(RT->SystemUser);
1961 $tmp->LoadByCols(Base => $link->Base, Type => $link->Type, LocalTarget => $MergeInto->id);
1965 $link->SetTarget($MergeInto->URI);
1966 $link->SetLocalTarget($MergeInto->id);
1968 $old_seen{$link->Base."-".$link->Type} =1;
1973 my $old_links_from = RT::Links->new($self->CurrentUser);
1974 $old_links_from->Limit(FIELD => 'Base', VALUE => $self->URI);
1976 while (my $link = $old_links_from->Next) {
1977 if (exists $old_seen{$link->Type."-".$link->Target}) {
1980 if ($link->Target eq $MergeInto->URI) {
1983 # First, make sure the link doesn't already exist. then move it over.
1984 my $tmp = RT::Link->new(RT->SystemUser);
1985 $tmp->LoadByCols(Target => $link->Target, Type => $link->Type, LocalBase => $MergeInto->id);
1989 $link->SetBase($MergeInto->URI);
1990 $link->SetLocalBase($MergeInto->id);
1991 $old_seen{$link->Type."-".$link->Target} =1;
1997 # Update time fields
1998 foreach my $type (qw(TimeEstimated TimeWorked TimeLeft)) {
2001 Value => ( $MergeInto->$type() || 0 ) + ( $self->$type() || 0 ),
2002 RecordTransaction => 0,
2006 # add all of this ticket's watchers to that ticket.
2007 for my $role ($self->Roles) {
2008 next if $self->RoleGroup($role)->SingleMemberRoleGroup;
2009 my $people = $self->RoleGroup($role)->MembersObj;
2010 while ( my $watcher = $people->Next ) {
2011 my ($val, $msg) = $MergeInto->AddRoleMember(
2014 PrincipalId => $watcher->MemberId,
2015 InsideTransaction => 1,
2018 $RT::Logger->debug($msg);
2023 #find all of the tickets that were merged into this ticket.
2024 my $old_mergees = RT::Tickets->new( $self->CurrentUser );
2025 $old_mergees->Limit(
2026 FIELD => 'EffectiveId',
2031 # update their EffectiveId fields to the new ticket's id
2032 while ( my $ticket = $old_mergees->Next() ) {
2033 my ( $val, $msg ) = $ticket->__Set(
2034 Field => 'EffectiveId',
2035 Value => $MergeInto->Id()
2039 #make a new link: this ticket is merged into that other ticket.
2040 $self->AddLink( Type => 'MergedInto', Target => $MergeInto->Id());
2042 $MergeInto->_SetLastUpdated;
2044 return ( 1, $self->loc("Merge Successful") );
2049 Returns list of tickets' ids that's been merged into this ticket.
2057 return @{ $MERGE_CACHE{'merged'}{ $id } }
2058 if $MERGE_CACHE{'merged'}{ $id };
2060 my $mergees = RT::Tickets->new( $self->CurrentUser );
2061 $mergees->LimitField(
2062 FIELD => 'EffectiveId',
2065 $mergees->LimitField(
2070 return @{ $MERGE_CACHE{'merged'}{ $id } ||= [] }
2071 = map $_->id, @{ $mergees->ItemsArrayRef || [] };
2080 Takes nothing and returns an RT::User object of
2088 #If this gets ACLed, we lose on a rights check in User.pm and
2089 #get deep recursion. if we need ACLs here, we need
2090 #an equiv without ACLs
2092 my $owner = RT::User->new( $self->CurrentUser );
2093 $owner->Load( $self->__Value('Owner') );
2095 #Return the owner object
2101 =head2 OwnerAsString
2103 Returns the owner's email address
2109 return ( $self->OwnerObj->EmailAddress );
2117 Takes two arguments:
2118 the Id or Name of the owner
2119 and (optionally) the type of the SetOwner Transaction. It defaults
2120 to 'Set'. 'Steal' is also a valid option.
2127 my $NewOwner = shift;
2128 my $Type = shift || "Set";
2130 $RT::Handle->BeginTransaction();
2132 $self->_SetLastUpdated(); # lock the ticket
2133 $self->Load( $self->id ); # in case $self changed while waiting for lock
2135 my $OldOwnerObj = $self->OwnerObj;
2137 my $NewOwnerObj = RT::User->new( $self->CurrentUser );
2138 $NewOwnerObj->Load( $NewOwner );
2140 my ( $val, $msg ) = $self->CurrentUserCanSetOwner(
2141 NewOwnerObj => $NewOwnerObj,
2145 $RT::Handle->Rollback();
2146 return ( $val, $msg );
2149 ($val, $msg ) = $self->OwnerGroup->_AddMember(
2150 PrincipalId => $NewOwnerObj->PrincipalId,
2151 InsideTransaction => 1,
2155 $RT::Handle->Rollback;
2156 return ( 0, $self->loc("Could not change owner: [_1]", $msg) );
2159 $msg = $self->loc( "Owner changed from [_1] to [_2]",
2160 $OldOwnerObj->Name, $NewOwnerObj->Name );
2162 $RT::Handle->Commit();
2164 return ( $val, $msg );
2167 =head2 CurrentUserCanSetOwner
2169 Confirm the current user can set the owner of the current ticket.
2171 There are several different rights to manage owner changes and
2172 this method evaluates these rights, guided by parameters provided.
2174 This method evaluates these rights in the context of the state of
2175 the current ticket. For example, it evaluates Take for tickets that
2176 are owned by Nobody because that is the context appropriate for the
2177 TakeTicket right. If you need to strictly test a user for a right,
2178 use HasRight to check for the right directly.
2180 For some custom types of owner changes (C<Take> and C<Steal>), it also
2181 verifies that those actions are possible given the current ticket owner.
2183 =head3 Rights to Set Owner
2185 The current user can set or change the Owner field in the following
2192 ReassignTicket unconditionally grants the right to set the owner
2193 to any user who has OwnTicket. This can be used to break an
2194 Owner lock held by another user (see below) and can be a convenient
2195 right for managers or administrators who need to assign tickets
2196 without necessarily owning them.
2200 ModifyTicket grants the right to set the owner to any user who
2201 has OwnTicket, provided the ticket is currently owned by the current
2202 user or is not owned (owned by Nobody). (See the details on the Force
2203 parameter below for exceptions to this.)
2207 If the ticket is currently not owned (owned by Nobody),
2208 TakeTicket is sufficient to set the owner to yourself (but not
2209 an arbitrary person), but only if you have OwnTicket. It is
2210 thus a subset of the possible changes provided by ModifyTicket.
2211 This exists to allow granting TakeTicket freely, and
2212 the broader ModifyTicket only to Owners.
2216 If the ticket is currently owned by someone who is not you or
2217 Nobody, StealTicket is sufficient to set the owner to yourself,
2218 but only if you have OwnTicket. This is hence non-overlapping
2219 with the changes provided by ModifyTicket, and is used to break
2220 a lock held by another user.
2226 This method returns ($result, $message) with $result containing
2227 true or false indicating if the current user can set owner and $message
2228 containing a message, typically in the case of a false response.
2230 If called with no parameters, this method determines if the current
2231 user could set the owner of the current ticket given any
2232 permutation of the rights described above. This can be useful
2233 when determining whether to make owner-setting options available
2236 This method accepts the following parameters as a paramshash:
2240 =item C<NewOwnerObj>
2242 Optional; an L<RT::User> object representing the proposed new owner of
2247 Optional; the type of set owner operation. Valid values are C<Take>,
2248 C<Steal>, or C<Force>. Note that if the type is C<Take>, this method
2249 will return false if the current user is already the owner; similarly,
2250 it will return false for C<Steal> if the ticket has no owner or the
2251 owner is the current user.
2255 As noted above, there are exceptions to the standard ticket-based rights
2256 described here. The Force option allows for these and is used
2257 when moving tickets between queues, for reminders (because the full
2258 owner rights system is too complex for them), and optionally during
2263 sub CurrentUserCanSetOwner {
2265 my %args = ( Type => '',
2267 my $OldOwnerObj = $self->OwnerObj;
2269 $args{NewOwnerObj} ||= $self->CurrentUser->UserObj
2270 if $args{Type} eq "Take" or $args{Type} eq "Steal";
2272 # Confirm rights for new owner if we got one
2273 if ( $args{'NewOwnerObj'} ){
2274 my ($ok, $message) = $self->_NewOwnerCanOwnTicket($args{'NewOwnerObj'}, $OldOwnerObj);
2275 return ($ok, $message) if not $ok;
2278 # ReassignTicket allows you to SetOwner, but we also need to check ticket's
2279 # current owner for Take and Steal Types
2280 return ( 1, undef ) if $self->CurrentUserHasRight('ReassignTicket')
2281 && $args{Type} ne 'Take' && $args{Type} ne 'Steal';
2284 if ( $OldOwnerObj->Id == RT->Nobody->Id ) {
2286 # Steal is not applicable for unowned tickets.
2287 if ( $args{'Type'} eq 'Steal' ){
2288 return ( 0, $self->loc("You can only steal a ticket owned by someone else") )
2291 # Can set owner to yourself with ModifyTicket, ReassignTicket,
2292 # or TakeTicket; in all of these cases, OwnTicket is checked by
2293 # _NewOwnerCanOwnTicket above.
2294 if ( $args{'Type'} eq 'Take'
2295 or ( $args{'NewOwnerObj'}
2296 and $args{'NewOwnerObj'}->id == $self->CurrentUser->id )) {
2297 unless ( $self->CurrentUserHasRight('ModifyTicket')
2298 or $self->CurrentUserHasRight('ReassignTicket')
2299 or $self->CurrentUserHasRight('TakeTicket') ) {
2300 return ( 0, $self->loc("Permission Denied") );
2303 # Nobody -> someone else requires ModifyTicket or ReassignTicket
2304 unless ( $self->CurrentUserHasRight('ModifyTicket')
2305 or $self->CurrentUserHasRight('ReassignTicket') ) {
2306 return ( 0, $self->loc("Permission Denied") );
2311 # Ticket is owned by someone else
2312 # Can set owner to yourself with ModifyTicket or StealTicket
2314 elsif ( $OldOwnerObj->Id != RT->Nobody->Id
2315 && $OldOwnerObj->Id != $self->CurrentUser->id ) {
2317 unless ( $self->CurrentUserHasRight('ModifyTicket')
2318 || $self->CurrentUserHasRight('ReassignTicket')
2319 || $self->CurrentUserHasRight('StealTicket') ) {
2320 return ( 0, $self->loc("Permission Denied") )
2323 if ( $args{'Type'} eq 'Steal' || $args{'Type'} eq 'Force' ){
2324 return ( 1, undef ) if $self->CurrentUserHasRight('OwnTicket');
2325 return ( 0, $self->loc("Permission Denied") );
2328 # Not a steal or force
2329 if ( $args{'Type'} eq 'Take'
2330 or ( $args{'NewOwnerObj'}
2331 and $args{'NewOwnerObj'}->id == $self->CurrentUser->id )) {
2332 return ( 0, $self->loc("You can only take tickets that are unowned") );
2335 unless ( $self->CurrentUserHasRight('ReassignTicket') ) {
2336 return ( 0, $self->loc( "You can only reassign tickets that you own or that are unowned"));
2340 # You own the ticket
2341 # Untake falls through to here, so we don't need to explicitly handle that Type
2343 if ( $args{'Type'} eq 'Take' || $args{'Type'} eq 'Steal' ) {
2344 return ( 0, $self->loc("You already own this ticket") );
2347 unless ( $self->CurrentUserHasRight('ModifyTicket')
2348 || $self->CurrentUserHasRight('ReassignTicket') ) {
2349 return ( 0, $self->loc("Permission Denied") );
2353 return ( 1, undef );
2356 # Verify the proposed new owner can own the ticket.
2358 sub _NewOwnerCanOwnTicket {
2360 my $NewOwnerObj = shift;
2361 my $OldOwnerObj = shift;
2363 unless ( $NewOwnerObj->Id ) {
2364 return ( 0, $self->loc("That user does not exist") );
2367 # The proposed new owner can't own the ticket
2368 if ( !$NewOwnerObj->HasRight( Right => 'OwnTicket', Object => $self ) ){
2369 return ( 0, $self->loc("That user may not own tickets in that queue") );
2372 # Ticket's current owner is the same as the new owner, nothing to do
2373 elsif ( $NewOwnerObj->Id == $OldOwnerObj->Id ) {
2374 return ( 0, $self->loc("That user already owns that ticket") );
2382 A convenince method to set the ticket's owner to the current user
2388 return ( $self->SetOwner( $self->CurrentUser->Id, 'Take' ) );
2395 Convenience method to set the owner to 'nobody' if the current user is the owner.
2401 return ( $self->SetOwner( RT->Nobody->UserObj->Id, 'Untake' ) );
2408 A convenience method to change the owner of the current ticket to the
2409 current user. Even if it's owned by another user.
2416 if ( $self->IsOwner( $self->CurrentUser ) ) {
2417 return ( 0, $self->loc("You already own this ticket") );
2420 return ( $self->SetOwner( $self->CurrentUser->Id, 'Steal' ) );
2426 =head2 SetStatus STATUS
2428 Set this ticket's status.
2430 Alternatively, you can pass in a list of named parameters (Status => STATUS, Force => FORCE, SetStarted => SETSTARTED ).
2431 If FORCE is true, ignore unresolved dependencies and force a status change.
2432 if SETSTARTED is true (it's the default value), set Started to current datetime if Started
2433 is not set and the status is changed from initial to not initial.
2441 $args{Status} = shift;
2447 # this only allows us to SetStarted, not we must SetStarted.
2448 # this option was added for rtir initially
2449 $args{SetStarted} = 1 unless exists $args{SetStarted};
2451 my ($valid, $msg) = $self->ValidateStatusChange($args{Status});
2452 return ($valid, $msg) unless $valid;
2454 my $lifecycle = $self->LifecycleObj;
2457 && !$lifecycle->IsInactive($self->Status)
2458 && $lifecycle->IsInactive($args{Status})
2459 && $self->HasUnresolvedDependencies )
2461 return ( 0, $self->loc('That ticket has unresolved dependencies') );
2464 return $self->_SetStatus(
2465 Status => $args{Status},
2466 SetStarted => $args{SetStarted},
2475 RecordTransaction => 1,
2476 Lifecycle => $self->LifecycleObj,
2479 $args{Status} = lc $args{Status} if defined $args{Status};
2480 $args{NewLifecycle} ||= $args{Lifecycle};
2482 my $now = RT::Date->new( $self->CurrentUser );
2485 my $raw_started = RT::Date->new(RT->SystemUser);
2486 $raw_started->Set(Format => 'ISO', Value => $self->__Value('Started'));
2488 my $old = $self->__Value('Status');
2490 # If we're changing the status from new, record that we've started
2491 if ( $args{SetStarted}
2492 && $args{Lifecycle}->IsInitial($old)
2493 && !$args{NewLifecycle}->IsInitial($args{Status})
2494 && !$raw_started->IsSet) {
2495 # Set the Started time to "now"
2499 RecordTransaction => 0
2503 # When we close a ticket, set the 'Resolved' attribute to now.
2504 # It's misnamed, but that's just historical.
2505 if ( $args{NewLifecycle}->IsInactive($args{Status}) ) {
2507 Field => 'Resolved',
2509 RecordTransaction => 0,
2513 # Actually update the status
2514 my ($val, $msg)= $self->_Set(
2516 Value => $args{Status},
2519 TransactionType => 'Status',
2520 RecordTransaction => $args{RecordTransaction},
2522 return ($val, $msg);
2529 my $taken = ($value||0) - ($self->__Value('TimeWorked')||0);
2532 Field => 'TimeWorked',
2534 TimeTaken => $taken,
2540 Takes no arguments. Marks this ticket for garbage collection
2546 unless ( $self->LifecycleObj->IsValid('deleted') ) {
2547 return (0, $self->loc('Delete operation is disabled by lifecycle configuration') ); #loc
2549 return ( $self->SetStatus('deleted') );
2553 =head2 SetTold ISO [TIMETAKEN]
2555 Updates the told and records a transaction
2562 $told = shift if (@_);
2563 my $timetaken = shift || 0;
2565 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
2566 return ( 0, $self->loc("Permission Denied") );
2569 my $datetold = RT::Date->new( $self->CurrentUser );
2571 $datetold->Set( Format => 'iso',
2575 $datetold->SetToNow();
2578 return ( $self->_Set( Field => 'Told',
2579 Value => $datetold->ISO,
2580 TimeTaken => $timetaken,
2581 TransactionType => 'Told' ) );
2586 Updates the told without a transaction or acl check. Useful when we're sending replies.
2593 my $now = RT::Date->new( $self->CurrentUser );
2596 #use __Set to get no ACLs ;)
2597 return ( $self->__Set( Field => 'Told',
2598 Value => $now->ISO ) );
2608 my $uid = $self->CurrentUser->id;
2609 my $attr = $self->FirstAttribute( "User-". $uid ."-SeenUpTo" );
2610 return if $attr && $attr->Content gt $self->LastUpdated;
2612 my $txns = $self->Transactions;
2613 $txns->Limit( FIELD => 'Type', VALUE => 'Comment' );
2614 $txns->Limit( FIELD => 'Type', VALUE => 'Correspond' );
2615 $txns->Limit( FIELD => 'Creator', OPERATOR => '!=', VALUE => $uid );
2619 VALUE => $attr->Content
2621 $txns->RowsPerPage(1);
2622 return $txns->First;
2625 =head2 RanTransactionBatch
2627 Acts as a guard around running TransactionBatch scrips.
2629 Should be false until you enter the code that runs TransactionBatch scrips
2631 Accepts an optional argument to indicate that TransactionBatch Scrips should no longer be run on this object.
2635 sub RanTransactionBatch {
2639 if ( defined $val ) {
2640 return $self->{_RanTransactionBatch} = $val;
2642 return $self->{_RanTransactionBatch};
2648 =head2 TransactionBatch
2650 Returns an array reference of all transactions created on this ticket during
2651 this ticket object's lifetime or since last application of a batch, or undef
2654 Only works when the C<UseTransactionBatch> config option is set to true.
2658 sub TransactionBatch {
2660 return $self->{_TransactionBatch};
2663 =head2 ApplyTransactionBatch
2665 Applies scrips on the current batch of transactions and shinks it. Usually
2666 batch is applied when object is destroyed, but in some cases it's too late.
2670 sub ApplyTransactionBatch {
2673 my $batch = $self->TransactionBatch;
2674 return unless $batch && @$batch;
2676 $self->_ApplyTransactionBatch;
2678 $self->{_TransactionBatch} = [];
2681 sub _ApplyTransactionBatch {
2684 return if $self->RanTransactionBatch;
2685 $self->RanTransactionBatch(1);
2687 my $still_exists = RT::Ticket->new( RT->SystemUser );
2688 $still_exists->Load( $self->Id );
2689 if (not $still_exists->Id) {
2690 # The ticket has been removed from the database, but we still
2691 # have pending TransactionBatch txns for it. Unfortunately,
2692 # because it isn't in the DB anymore, attempting to run scrips
2693 # on it may produce unpredictable results; simply drop the
2694 # batched transactions.
2695 $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.");
2699 my $batch = $self->TransactionBatch;
2702 my $types = join ',', grep !$seen{$_}++, grep defined, map $_->__Value('Type'), grep defined, @{$batch};
2705 RT::Scrips->new(RT->SystemUser)->Apply(
2706 Stage => 'TransactionBatch',
2708 TransactionObj => $batch->[0],
2712 # Entry point of the rule system
2713 my $rules = RT::Ruleset->FindAllRules(
2714 Stage => 'TransactionBatch',
2716 TransactionObj => $batch->[0],
2719 RT::Ruleset->CommitRules($rules);
2725 # DESTROY methods need to localize $@, or it may unset it. This
2726 # causes $m->abort to not bubble all of the way up. See perlbug
2727 # http://rt.perl.org/rt3/Ticket/Display.html?id=17650
2730 # The following line eliminates reentrancy.
2731 # It protects against the fact that perl doesn't deal gracefully
2732 # when an object's refcount is changed in its destructor.
2733 return if $self->{_Destroyed}++;
2735 if (in_global_destruction()) {
2736 unless ($ENV{'HARNESS_ACTIVE'}) {
2737 warn "Too late to safely run transaction-batch scrips!"
2738 ." This is typically caused by using ticket objects"
2739 ." at the top-level of a script which uses the RT API."
2740 ." Be sure to explicitly undef such ticket objects,"
2741 ." or put them inside of a lexical scope.";
2746 return $self->ApplyTransactionBatch;
2752 sub _OverlayAccessible {
2754 EffectiveId => { 'read' => 1, 'write' => 1, 'public' => 1 },
2755 Queue => { 'read' => 1, 'write' => 1 },
2756 Requestors => { 'read' => 1, 'write' => 1 },
2757 Owner => { 'read' => 1, 'write' => 1 },
2758 Subject => { 'read' => 1, 'write' => 1 },
2759 InitialPriority => { 'read' => 1, 'write' => 1 },
2760 FinalPriority => { 'read' => 1, 'write' => 1 },
2761 Priority => { 'read' => 1, 'write' => 1 },
2762 Status => { 'read' => 1, 'write' => 1 },
2763 TimeEstimated => { 'read' => 1, 'write' => 1 },
2764 TimeWorked => { 'read' => 1, 'write' => 1 },
2765 TimeLeft => { 'read' => 1, 'write' => 1 },
2766 Told => { 'read' => 1, 'write' => 1 },
2767 Resolved => { 'read' => 1 },
2768 Type => { 'read' => 1 },
2769 Starts => { 'read' => 1, 'write' => 1 },
2770 Started => { 'read' => 1, 'write' => 1 },
2771 Due => { 'read' => 1, 'write' => 1 },
2772 Creator => { 'read' => 1, 'auto' => 1 },
2773 Created => { 'read' => 1, 'auto' => 1 },
2774 LastUpdatedBy => { 'read' => 1, 'auto' => 1 },
2775 LastUpdated => { 'read' => 1, 'auto' => 1 }
2785 my %args = ( Field => undef,
2788 RecordTransaction => 1,
2790 TransactionType => 'Set',
2793 if ($args{'CheckACL'}) {
2794 unless ( $self->CurrentUserHasRight('ModifyTicket')) {
2795 return ( 0, $self->loc("Permission Denied"));
2799 # Avoid ACL loops using _Value
2800 my $Old = $self->SUPER::_Value($args{'Field'});
2803 my ( $ret, $msg ) = $self->SUPER::_Set(
2804 Field => $args{'Field'},
2805 Value => $args{'Value'}
2807 return ( 0, $msg ) unless $ret;
2809 return ( $ret, $msg ) unless $args{'RecordTransaction'};
2812 ( $ret, $msg, $trans ) = $self->_NewTransaction(
2813 Type => $args{'TransactionType'},
2814 Field => $args{'Field'},
2815 NewValue => $args{'Value'},
2817 TimeTaken => $args{'TimeTaken'},
2820 # Ensure that we can read the transaction, even if the change
2821 # just made the ticket unreadable to us
2822 $trans->{ _object_is_readable } = 1;
2824 return ( $ret, scalar $trans->BriefDescription );
2831 Takes the name of a table column.
2832 Returns its value as a string, if the user passes an ACL check
2841 #if the field is public, return it.
2842 if ( $self->_Accessible( $field, 'public' ) ) {
2844 #$RT::Logger->debug("Skipping ACL check for $field");
2845 return ( $self->SUPER::_Value($field) );
2849 #If the current user doesn't have ACLs, don't let em at it.
2851 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
2854 return ( $self->SUPER::_Value($field) );
2860 Customization of L<RT::Record/Attachments> for tickets.
2871 my $res = RT::Attachments->new( $self->CurrentUser );
2872 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
2877 ENTRYAGGREGATOR => 'AND'
2882 my @columns = grep { not /^(Headers|Content)$/ }
2883 RT::Attachment->ReadableAttributes;
2884 push @columns, 'Headers' if $args{'WithHeaders'};
2885 push @columns, 'Content' if $args{'WithContent'};
2887 $res->Columns( @columns );
2888 my $txn_alias = $res->TransactionAlias;
2890 ALIAS => $txn_alias,
2891 FIELD => 'ObjectType',
2892 VALUE => ref($self),
2894 my $ticket_alias = $res->Join(
2895 ALIAS1 => $txn_alias,
2896 FIELD1 => 'ObjectId',
2897 TABLE2 => 'Tickets',
2901 ALIAS => $ticket_alias,
2902 FIELD => 'EffectiveId',
2908 =head2 TextAttachments
2910 Customization of L<RT::Record/TextAttachments> for tickets.
2914 sub TextAttachments {
2917 my $res = $self->SUPER::TextAttachments( @_ );
2918 unless ( $self->CurrentUserHasRight('ShowTicketComments') ) {
2919 # if the user may not see comments do not return them
2922 ALIAS => $res->TransactionAlias,
2934 =head2 _UpdateTimeTaken
2936 This routine will increment the timeworked counter. it should
2937 only be called from _NewTransaction
2941 sub _UpdateTimeTaken {
2943 my $Minutes = shift;
2946 if ( my $txn = $rest{'Transaction'} ) {
2947 return if $txn->__Value('Type') eq 'Set' && $txn->__Value('Field') eq 'TimeWorked';
2950 my $Total = $self->__Value("TimeWorked");
2951 $Total = ( $Total || 0 ) + ( $Minutes || 0 );
2953 Field => "TimeWorked",
2955 RecordTransaction => 0,
2962 =head2 CurrentUserCanSee
2964 Returns true if the current user can see the ticket, using ShowTicket
2968 sub CurrentUserCanSee {
2970 my ($what, $txn) = @_;
2971 return 0 unless $self->CurrentUserHasRight('ShowTicket');
2973 return 1 if $what ne "Transaction";
2975 # If it's a comment, we need to be extra special careful
2976 my $type = $txn->__Value('Type');
2977 if ( $type eq 'Comment' ) {
2978 unless ( $self->CurrentUserHasRight('ShowTicketComments') ) {
2981 } elsif ( $type eq 'CommentEmailRecord' ) {
2982 unless ( $self->CurrentUserHasRight('ShowTicketComments')
2983 && $self->CurrentUserHasRight('ShowOutgoingEmail') ) {
2986 } elsif ( $type eq 'EmailRecord' ) {
2987 unless ( $self->CurrentUserHasRight('ShowOutgoingEmail') ) {
2996 Return the Reminders object for this ticket. (It's an RT::Reminders object.)
2997 It isn't acutally a searchbuilder collection itself.
3004 unless ($self->{'__reminders'}) {
3005 $self->{'__reminders'} = RT::Reminders->new($self->CurrentUser);
3006 $self->{'__reminders'}->Ticket($self->id);
3008 return $self->{'__reminders'};
3017 Returns an RT::Transactions object of all transactions on this ticket
3024 my $transactions = RT::Transactions->new( $self->CurrentUser );
3026 #If the user has no rights, return an empty object
3027 if ( $self->CurrentUserHasRight('ShowTicket') ) {
3028 $transactions->LimitToTicket($self->id);
3030 # if the user may not see comments do not return them
3031 unless ( $self->CurrentUserHasRight('ShowTicketComments') ) {
3032 $transactions->Limit(
3038 $transactions->Limit(
3042 VALUE => "CommentEmailRecord",
3043 ENTRYAGGREGATOR => 'AND'
3048 $transactions->Limit(
3052 ENTRYAGGREGATOR => 'AND'
3056 return ($transactions);
3062 =head2 TransactionCustomFields
3064 Returns the custom fields that transactions on tickets will have.
3068 sub TransactionCustomFields {
3070 my $cfs = $self->QueueObj->TicketTransactionCustomFields;
3071 $cfs->SetContextObject( $self );
3076 =head2 LoadCustomFieldByIdentifier
3078 Finds and returns the custom field of the given name for the ticket,
3079 overriding L<RT::Record/LoadCustomFieldByIdentifier> to look for
3080 queue-specific CFs before global ones.
3084 sub LoadCustomFieldByIdentifier {
3088 return $self->SUPER::LoadCustomFieldByIdentifier($field)
3089 if ref $field or $field =~ /^\d+$/;
3091 my $cf = RT::CustomField->new( $self->CurrentUser );
3092 $cf->SetContextObject( $self );
3095 LookupType => $self->CustomFieldLookupType,
3096 ObjectId => $self->Queue,
3103 =head2 CustomFieldLookupType
3105 Returns the RT::Ticket lookup type, which can be passed to
3106 RT::CustomField->Create() via the 'LookupType' hash key.
3111 sub CustomFieldLookupType {
3112 "RT::Queue-RT::Ticket";
3115 =head2 ACLEquivalenceObjects
3117 This method returns a list of objects for which a user's rights also apply
3118 to this ticket. Generally, this is only the ticket's queue, but some RT
3119 extensions may make other objects available too.
3121 This method is called from L<RT::Principal/HasRight>.
3125 sub ACLEquivalenceObjects {
3127 return $self->QueueObj;
3131 =head2 ModifyLinkRight
3135 sub ModifyLinkRight { "ModifyTicket" }
3137 =head2 Forward Transaction => undef, To => '', Cc => '', Bcc => ''
3139 Forwards transaction with all attachments as 'message/rfc822'.
3146 Transaction => undef,
3152 ContentType => 'text/plain',
3158 unless ( $self->CurrentUserHasRight('ForwardMessage') ) {
3159 return ( 0, $self->loc("Permission Denied") );
3162 $args{$_} = join ", ", map { $_->format } RT::EmailParser->ParseEmailAddress( $args{$_} || '' ) for qw(To Cc Bcc);
3164 return (0, $self->loc("Can't forward: no valid email addresses specified") )
3165 unless grep {length $args{$_}} qw/To Cc Bcc/;
3167 my $mime = MIME::Entity->build(
3168 Type => $args{ContentType},
3169 Data => Encode::encode( "UTF-8", $args{Content} ),
3172 $mime->head->replace( $_ => Encode::encode('UTF-8',$args{$_} ) )
3173 for grep defined $args{$_}, qw(Subject To Cc Bcc);
3174 $mime->head->replace(
3175 From => Encode::encode( 'UTF-8',
3176 RT::Interface::Email::GetForwardFrom(
3177 Transaction => $args{Transaction},
3183 if ($args{'DryRun'}) {
3184 $RT::Handle->BeginTransaction();
3185 $args{'CommitScrips'} = 0;
3188 my ( $ret, $msg ) = $self->_NewTransaction(
3191 Type => 'Forward Transaction',
3192 Field => $args{Transaction}->id,
3195 Type => 'Forward Ticket',
3198 Data => join( ', ', grep { length } $args{To}, $args{Cc}, $args{Bcc} ),
3200 CommitScrips => $args{'CommitScrips'},
3204 $RT::Logger->error("Failed to create transaction: $msg");
3207 if ($args{'DryRun'}) {
3208 $RT::Handle->Rollback();
3210 return ( $ret, $self->loc('Message recorded') );
3217 Jesse Vincent, jesse@bestpractical.com
3225 sub Table {'Tickets'}
3234 Returns the current value of id.
3235 (In the database, id is stored as int(11).)
3243 Returns the current value of EffectiveId.
3244 (In the database, EffectiveId is stored as int(11).)
3248 =head2 SetEffectiveId VALUE
3251 Set EffectiveId to VALUE.
3252 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
3253 (In the database, EffectiveId will be stored as a int(11).)
3261 Returns the current value of Queue.
3262 (In the database, Queue is stored as int(11).)
3266 =head2 SetQueue VALUE
3270 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
3271 (In the database, Queue will be stored as a int(11).)
3279 Returns the current value of Type.
3280 (In the database, Type is stored as varchar(16).)
3284 =head2 SetType VALUE
3288 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
3289 (In the database, Type will be stored as a varchar(16).)
3295 =head2 IssueStatement
3297 Returns the current value of IssueStatement.
3298 (In the database, IssueStatement is stored as int(11).)
3302 =head2 SetIssueStatement VALUE
3305 Set IssueStatement to VALUE.
3306 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
3307 (In the database, IssueStatement will be stored as a int(11).)
3315 Returns the current value of Resolution.
3316 (In the database, Resolution is stored as int(11).)
3320 =head2 SetResolution VALUE
3323 Set Resolution to VALUE.
3324 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
3325 (In the database, Resolution will be stored as a int(11).)
3333 Returns the current value of Owner.
3334 (In the database, Owner is stored as int(11).)
3338 =head2 SetOwner VALUE
3342 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
3343 (In the database, Owner will be stored as a int(11).)
3351 Returns the current value of Subject.
3352 (In the database, Subject is stored as varchar(200).)
3356 =head2 SetSubject VALUE
3359 Set Subject to VALUE.
3360 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
3361 (In the database, Subject will be stored as a varchar(200).)
3367 =head2 InitialPriority
3369 Returns the current value of InitialPriority.
3370 (In the database, InitialPriority is stored as int(11).)
3374 =head2 SetInitialPriority VALUE
3377 Set InitialPriority to VALUE.
3378 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
3379 (In the database, InitialPriority will be stored as a int(11).)
3385 =head2 FinalPriority
3387 Returns the current value of FinalPriority.
3388 (In the database, FinalPriority is stored as int(11).)
3392 =head2 SetFinalPriority VALUE
3395 Set FinalPriority to VALUE.
3396 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
3397 (In the database, FinalPriority will be stored as a int(11).)
3405 Returns the current value of Priority.
3406 (In the database, Priority is stored as int(11).)
3410 =head2 SetPriority VALUE
3413 Set Priority to VALUE.
3414 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
3415 (In the database, Priority will be stored as a int(11).)
3421 =head2 TimeEstimated
3423 Returns the current value of TimeEstimated.
3424 (In the database, TimeEstimated is stored as int(11).)
3428 =head2 SetTimeEstimated VALUE
3431 Set TimeEstimated to VALUE.
3432 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
3433 (In the database, TimeEstimated will be stored as a int(11).)
3441 Returns the current value of TimeWorked.
3442 (In the database, TimeWorked is stored as int(11).)
3446 =head2 SetTimeWorked VALUE
3449 Set TimeWorked to VALUE.
3450 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
3451 (In the database, TimeWorked will be stored as a int(11).)
3459 Returns the current value of Status.
3460 (In the database, Status is stored as varchar(64).)
3464 =head2 SetStatus VALUE
3467 Set Status to VALUE.
3468 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
3469 (In the database, Status will be stored as a varchar(64).)
3477 Returns the current value of TimeLeft.
3478 (In the database, TimeLeft is stored as int(11).)
3482 =head2 SetTimeLeft VALUE
3485 Set TimeLeft to VALUE.
3486 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
3487 (In the database, TimeLeft will be stored as a int(11).)
3495 Returns the current value of Told.
3496 (In the database, Told is stored as datetime.)
3500 =head2 SetTold VALUE
3504 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
3505 (In the database, Told will be stored as a datetime.)
3513 Returns the current value of Starts.
3514 (In the database, Starts is stored as datetime.)
3518 =head2 SetStarts VALUE
3521 Set Starts to VALUE.
3522 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
3523 (In the database, Starts will be stored as a datetime.)
3531 Returns the current value of Started.
3532 (In the database, Started is stored as datetime.)
3536 =head2 SetStarted VALUE
3539 Set Started to VALUE.
3540 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
3541 (In the database, Started will be stored as a datetime.)
3549 Returns the current value of Due.
3550 (In the database, Due is stored as datetime.)
3558 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
3559 (In the database, Due will be stored as a datetime.)
3567 Returns the current value of Resolved.
3568 (In the database, Resolved is stored as datetime.)
3572 =head2 SetResolved VALUE
3575 Set Resolved to VALUE.
3576 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
3577 (In the database, Resolved will be stored as a datetime.)
3583 =head2 LastUpdatedBy
3585 Returns the current value of LastUpdatedBy.
3586 (In the database, LastUpdatedBy is stored as int(11).)
3594 Returns the current value of LastUpdated.
3595 (In the database, LastUpdated is stored as datetime.)
3603 Returns the current value of Creator.
3604 (In the database, Creator is stored as int(11).)
3612 Returns the current value of Created.
3613 (In the database, Created is stored as datetime.)
3621 Returns the current value of Disabled.
3622 (In the database, Disabled is stored as smallint(6).)
3626 =head2 SetDisabled VALUE
3629 Set Disabled to VALUE.
3630 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
3631 (In the database, Disabled will be stored as a smallint(6).)
3638 sub _CoreAccessible {
3642 {read => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => ''},
3644 {read => 1, write => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'},
3646 {read => 1, write => 1, sql_type => 5, length => 6, is_blob => 0, is_numeric => 1, type => 'smallint(6)', default => undef},
3648 {read => 1, write => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'},
3650 {read => 1, write => 1, sql_type => 12, length => 16, is_blob => 0, is_numeric => 0, type => 'varchar(16)', default => ''},
3652 {read => 1, write => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'},
3654 {read => 1, write => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'},
3656 {read => 1, write => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'},
3658 {read => 1, write => 1, sql_type => 12, length => 200, is_blob => 0, is_numeric => 0, type => 'varchar(200)', default => '[no subject]'},
3660 {read => 1, write => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'},
3662 {read => 1, write => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'},
3664 {read => 1, write => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'},
3666 {read => 1, write => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'},
3668 {read => 1, write => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'},
3670 {read => 1, write => 1, sql_type => 12, length => 64, is_blob => 0, is_numeric => 0, type => 'varchar(64)', default => ''},
3672 {read => 1, write => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'},
3674 {read => 1, write => 1, sql_type => 11, length => 0, is_blob => 0, is_numeric => 0, type => 'datetime', default => ''},
3676 {read => 1, write => 1, sql_type => 11, length => 0, is_blob => 0, is_numeric => 0, type => 'datetime', default => ''},
3678 {read => 1, write => 1, sql_type => 11, length => 0, is_blob => 0, is_numeric => 0, type => 'datetime', default => ''},
3680 {read => 1, write => 1, sql_type => 11, length => 0, is_blob => 0, is_numeric => 0, type => 'datetime', default => ''},
3682 {read => 1, write => 1, sql_type => 11, length => 0, is_blob => 0, is_numeric => 0, type => 'datetime', default => ''},
3684 {read => 1, auto => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'},
3686 {read => 1, auto => 1, sql_type => 11, length => 0, is_blob => 0, is_numeric => 0, type => 'datetime', default => ''},
3688 {read => 1, auto => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'},
3690 {read => 1, auto => 1, sql_type => 11, length => 0, is_blob => 0, is_numeric => 0, type => 'datetime', default => ''},
3692 {read => 1, write => 1, sql_type => 5, length => 6, is_blob => 0, is_numeric => 1, type => 'smallint(6)', default => '0'},
3697 sub FindDependencies {
3699 my ($walker, $deps) = @_;
3701 $self->SUPER::FindDependencies($walker, $deps);
3704 my $links = RT::Links->new( $self->CurrentUser );
3706 SUBCLAUSE => "either",
3708 VALUE => $self->URI,
3709 ENTRYAGGREGATOR => 'OR'
3710 ) for qw/Base Target/;
3711 $deps->Add( in => $links );
3713 # Tickets which were merged in
3714 my $objs = RT::Tickets->new( $self->CurrentUser );
3715 $objs->Limit( FIELD => 'EffectiveId', VALUE => $self->Id );
3716 $objs->Limit( FIELD => 'id', OPERATOR => '!=', VALUE => $self->Id );
3717 $deps->Add( in => $objs );
3719 # Ticket role groups( Owner, Requestors, Cc, AdminCc )
3720 $objs = RT::Groups->new( $self->CurrentUser );
3721 $objs->Limit( FIELD => 'Domain', VALUE => 'RT::Ticket-Role', CASESENSITIVE => 0 );
3722 $objs->Limit( FIELD => 'Instance', VALUE => $self->Id );
3723 $deps->Add( in => $objs );
3726 $deps->Add( out => $self->QueueObj );
3729 $deps->Add( out => $self->OwnerObj );
3736 Dependencies => undef,
3739 my $deps = $args{'Dependencies'};
3742 # Tickets which were merged in
3743 my $objs = RT::Tickets->new( $self->CurrentUser );
3744 $objs->{'allow_deleted_search'} = 1;
3745 $objs->Limit( FIELD => 'EffectiveId', VALUE => $self->Id );
3746 $objs->Limit( FIELD => 'id', OPERATOR => '!=', VALUE => $self->Id );
3747 push( @$list, $objs );
3749 # Ticket role groups( Owner, Requestors, Cc, AdminCc )
3750 $objs = RT::Groups->new( $self->CurrentUser );
3751 $objs->Limit( FIELD => 'Domain', VALUE => 'RT::Ticket-Role', CASESENSITIVE => 0 );
3752 $objs->Limit( FIELD => 'Instance', VALUE => $self->Id );
3753 push( @$list, $objs );
3755 #TODO: Users, Queues if we wish export tool
3756 $deps->_PushDependencies(
3757 BaseObject => $self,
3758 Flags => RT::Shredder::Constants::DEPENDS_ON,
3759 TargetObjects => $list,
3760 Shredder => $args{'Shredder'}
3763 return $self->SUPER::__DependsOn( %args );
3769 my %store = $self->SUPER::Serialize(@_);
3771 my $obj = RT::Ticket->new( RT->SystemUser );
3772 $obj->Load( $store{EffectiveId} );
3773 $store{EffectiveId} = \($obj->UID);
3778 RT::Base->_ImportOverlays();