1 # BEGIN BPS TAGGED BLOCK {{{
5 # This software is Copyright (c) 1996-2016 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 [EMAIL]
767 Takes an optional email address to never email about updates to this ticket.
770 Returns an array of the RT::Attribute objects for this ticket's 'SquelchMailTo' attributes.
778 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
782 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
787 return $self->_SquelchMailTo(@_);
794 $self->AddAttribute( Name => 'SquelchMailTo', Content => $attr )
795 unless grep { $_->Content eq $attr }
796 $self->Attributes->Named('SquelchMailTo');
798 my @attributes = $self->Attributes->Named('SquelchMailTo');
799 return (@attributes);
803 =head2 UnsquelchMailTo ADDRESS
805 Takes an address and removes it from this ticket's "SquelchMailTo" list. If an address appears multiple times, each instance is removed.
807 Returns a tuple of (status, message)
811 sub UnsquelchMailTo {
815 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
816 return ( 0, $self->loc("Permission Denied") );
819 my ($val, $msg) = $self->Attributes->DeleteEntry ( Name => 'SquelchMailTo', Content => $address);
825 =head2 RequestorAddresses
827 B<Returns> String: All Ticket Requestor email addresses as a string.
831 sub RequestorAddresses {
834 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
838 return ( $self->Requestors->MemberEmailAddressesAsString );
842 =head2 AdminCcAddresses
844 returns String: All Ticket AdminCc email addresses as a string
848 sub AdminCcAddresses {
851 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
855 return ( $self->AdminCc->MemberEmailAddressesAsString )
861 returns String: All Ticket Ccs as a string of email addresses
868 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
871 return ( $self->Cc->MemberEmailAddressesAsString);
881 Returns this ticket's Requestors as an RT::Group object
887 return RT::Group->new($self->CurrentUser)
888 unless $self->CurrentUserHasRight('ShowTicket');
889 return $self->RoleGroup( 'Requestor' );
894 return $self->Requestor;
899 Private non-ACLed variant of Reqeustors so that we can look them up for the
900 purposes of customer auto-association during create.
907 my $group = RT::Group->new($RT::SystemUser);
908 $group->LoadTicketRoleGroup(Type => 'Requestor', Ticket => $self->Id);
915 Returns an RT::Group object which contains this ticket's Ccs.
916 If the user doesn't have "ShowTicket" permission, returns an empty group
923 return RT::Group->new($self->CurrentUser)
924 unless $self->CurrentUserHasRight('ShowTicket');
925 return $self->RoleGroup( 'Cc' );
933 Returns an RT::Group object which contains this ticket's AdminCcs.
934 If the user doesn't have "ShowTicket" permission, returns an empty group
941 return RT::Group->new($self->CurrentUser)
942 unless $self->CurrentUserHasRight('ShowTicket');
943 return $self->RoleGroup( 'AdminCc' );
949 # a generic routine to be called by IsRequestor, IsCc and IsAdminCc
951 =head2 IsWatcher { Type => TYPE, PrincipalId => PRINCIPAL_ID, Email => EMAIL }
953 Takes a param hash with the attributes Type and either PrincipalId or Email
955 Type is one of Requestor, Cc, AdminCc and Owner
957 PrincipalId is an RT::Principal id, and Email is an email address.
959 Returns true if the specified principal (or the one corresponding to the
960 specified address) is a member of the group Type for this ticket.
962 XX TODO: This should be Memoized.
969 my %args = ( Type => 'Requestor',
970 PrincipalId => undef,
975 # Load the relevant group.
976 my $group = $self->RoleGroup( $args{'Type'} );
978 # Find the relevant principal.
979 if (!$args{PrincipalId} && $args{Email}) {
980 # Look up the specified user.
981 my $user = RT::User->new($self->CurrentUser);
982 $user->LoadByEmail($args{Email});
984 $args{PrincipalId} = $user->PrincipalId;
987 # A non-existent user can't be a group member.
992 # Ask if it has the member in question
993 return $group->HasMember( $args{'PrincipalId'} );
998 =head2 IsRequestor PRINCIPAL_ID
1000 Takes an L<RT::Principal> id.
1002 Returns true if the principal is a requestor of the current ticket.
1010 return ( $self->IsWatcher( Type => 'Requestor', PrincipalId => $person ) );
1016 =head2 IsCc PRINCIPAL_ID
1018 Takes an RT::Principal id.
1019 Returns true if the principal is a Cc of the current ticket.
1028 return ( $self->IsWatcher( Type => 'Cc', PrincipalId => $cc ) );
1034 =head2 IsAdminCc PRINCIPAL_ID
1036 Takes an RT::Principal id.
1037 Returns true if the principal is an AdminCc of the current ticket.
1045 return ( $self->IsWatcher( Type => 'AdminCc', PrincipalId => $person ) );
1053 Takes an RT::User object. Returns true if that user is this ticket's owner.
1054 returns undef otherwise
1062 # no ACL check since this is used in acl decisions
1063 # unless ($self->CurrentUserHasRight('ShowTicket')) {
1067 #Tickets won't yet have owners when they're being created.
1068 unless ( $self->OwnerObj->id ) {
1072 if ( $person->id == $self->OwnerObj->id ) {
1084 =head2 TransactionAddresses
1086 Returns a composite hashref of the results of L<RT::Transaction/Addresses> for
1087 all this ticket's Create, Comment or Correspond transactions. The keys are
1088 stringified email addresses. Each value is an L<Email::Address> object.
1090 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.
1095 sub TransactionAddresses {
1097 my $txns = $self->Transactions;
1101 my $attachments = RT::Attachments->new( $self->CurrentUser );
1102 $attachments->LimitByTicket( $self->id );
1103 $attachments->Columns( qw( id Headers TransactionId));
1105 $attachments->Limit(
1106 ALIAS => $attachments->TransactionAlias,
1109 VALUE => [ qw(Create Comment Correspond) ],
1112 while ( my $att = $attachments->Next ) {
1113 foreach my $addrlist ( values %{$att->Addresses } ) {
1114 foreach my $addr (@$addrlist) {
1116 # Skip addresses without a phrase (things that are just raw addresses) if we have a phrase
1118 if ( $addresses{ $addr->address }
1119 && $addresses{ $addr->address }->phrase
1120 && not $addr->phrase );
1122 # skips "comment-only" addresses
1123 next unless ( $addr->address );
1124 $addresses{ $addr->address } = $addr;
1143 $RT::Logger->warning( " RT:::Queue::ValidateQueue called with a null value. this isn't ok.");
1147 my $QueueObj = RT::Queue->new( $self->CurrentUser );
1148 my $id = $QueueObj->Load($Value);
1162 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
1163 return ( 0, $self->loc("Permission Denied") );
1166 my ($ok, $msg, $status) = $self->_SetLifecycleColumn(
1168 RequireRight => "CreateTicket"
1172 # Clear the queue object cache;
1173 $self->{_queue_obj} = undef;
1174 my $queue = $self->QueueObj;
1176 # Untake the ticket if we have no permissions in the new queue
1177 unless ($self->OwnerObj->HasRight( Right => 'OwnTicket', Object => $queue )) {
1178 my $clone = RT::Ticket->new( RT->SystemUser );
1179 $clone->Load( $self->Id );
1180 unless ( $clone->Id ) {
1181 return ( 0, $self->loc("Couldn't load copy of ticket #[_1].", $self->Id) );
1183 my ($status, $msg) = $clone->SetOwner( RT->Nobody->Id, 'Force' );
1184 $RT::Logger->error("Couldn't set owner on queue change: $msg") unless $status;
1187 # On queue change, change queue for reminders too
1188 my $reminder_collection = $self->Reminders->Collection;
1189 while ( my $reminder = $reminder_collection->Next ) {
1190 my ($status, $msg) = $reminder->_Set( Field => 'Queue', Value => $queue->Id(), RecordTransaction => 0 );
1191 $RT::Logger->error('Queue change failed for reminder #' . $reminder->Id . ': ' . $msg) unless $status;
1194 # Pick up any changes made by the clones above
1195 $self->Load( $self->id );
1196 RT->Logger->error("Unable to reload ticket #" . $self->id)
1207 Takes nothing. returns this ticket's queue object
1214 if(!$self->{_queue_obj} || ! $self->{_queue_obj}->id) {
1216 $self->{_queue_obj} = RT::Queue->new( $self->CurrentUser );
1218 #We call __Value so that we can avoid the ACL decision and some deep recursion
1219 my ($result) = $self->{_queue_obj}->Load( $self->__Value('Queue') );
1221 return ($self->{_queue_obj});
1228 return $self->_Set( Field => 'Subject', Value => $value );
1233 Takes nothing. Returns SubjectTag for this ticket. Includes
1234 queue's subject tag or rtname if that is not set, ticket
1235 id and brackets, for example:
1237 [support.example.com #123456]
1245 . ($self->QueueObj->SubjectTag || RT->Config->Get('rtname'))
1254 Returns an RT::Date object containing this ticket's due date
1261 my $time = RT::Date->new( $self->CurrentUser );
1263 # -1 is RT::Date slang for never
1264 if ( my $due = $self->Due ) {
1265 $time->Set( Format => 'sql', Value => $due );
1268 $time->Set( Format => 'unix', Value => -1 );
1278 Returns this ticket's due date as a human readable string.
1280 B<DEPRECATED> and will be removed in 4.4; use C<<
1281 $ticket->DueObj->AsString >> instead.
1288 Instead => "->DueObj->AsString",
1291 return $self->DueObj->AsString();
1298 Returns an RT::Date object of this ticket's 'resolved' time.
1305 my $time = RT::Date->new( $self->CurrentUser );
1306 $time->Set( Format => 'sql', Value => $self->Resolved );
1310 =head2 FirstActiveStatus
1312 Returns the first active status that the ticket could transition to,
1313 according to its current Queue's lifecycle. May return undef if there
1314 is no such possible status to transition to, or we are already in it.
1315 This is used in L<RT::Action::AutoOpen>, for instance.
1319 sub FirstActiveStatus {
1322 my $lifecycle = $self->LifecycleObj;
1323 my $status = $self->Status;
1324 my @active = $lifecycle->Active;
1325 # no change if no active statuses in the lifecycle
1326 return undef unless @active;
1328 # no change if the ticket is already has first status from the list of active
1329 return undef if lc $status eq lc $active[0];
1331 my ($next) = grep $lifecycle->IsActive($_), $lifecycle->Transitions($status);
1335 =head2 FirstInactiveStatus
1337 Returns the first inactive status that the ticket could transition to,
1338 according to its current Queue's lifecycle. May return undef if there
1339 is no such possible status to transition to, or we are already in it.
1340 This is used in resolve action in UnsafeEmailCommands, for instance.
1344 sub FirstInactiveStatus {
1347 my $lifecycle = $self->LifecycleObj;
1348 my $status = $self->Status;
1349 my @inactive = $lifecycle->Inactive;
1350 # no change if no inactive statuses in the lifecycle
1351 return undef unless @inactive;
1353 # no change if the ticket is already has first status from the list of inactive
1354 return undef if lc $status eq lc $inactive[0];
1356 my ($next) = grep $lifecycle->IsInactive($_), $lifecycle->Transitions($status);
1362 Takes a date in ISO format or undef
1363 Returns a transaction id and a message
1364 The client calls "Start" to note that the project was started on the date in $date.
1365 A null date means "now"
1371 my $time = shift || 0;
1373 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
1374 return ( 0, $self->loc("Permission Denied") );
1377 #We create a date object to catch date weirdness
1378 my $time_obj = RT::Date->new( $self->CurrentUser() );
1380 $time_obj->Set( Format => 'ISO', Value => $time );
1383 $time_obj->SetToNow();
1386 return ( $self->_Set( Field => 'Started', Value => $time_obj->ISO ) );
1394 Returns an RT::Date object which contains this ticket's
1402 my $time = RT::Date->new( $self->CurrentUser );
1403 $time->Set( Format => 'sql', Value => $self->Started );
1411 Returns an RT::Date object which contains this ticket's
1419 my $time = RT::Date->new( $self->CurrentUser );
1420 $time->Set( Format => 'sql', Value => $self->Starts );
1428 Returns an RT::Date object which contains this ticket's
1436 my $time = RT::Date->new( $self->CurrentUser );
1437 $time->Set( Format => 'sql', Value => $self->Told );
1445 A convenience method that returns ToldObj->AsString
1447 B<DEPRECATED> and will be removed in 4.4; use C<<
1448 $ticket->ToldObj->AsString >> instead.
1455 Instead => "->ToldObj->AsString",
1458 if ( $self->Told ) {
1459 return $self->ToldObj->AsString();
1468 sub _DurationAsString {
1471 return "" unless $value;
1472 return RT::Date->new( $self->CurrentUser )
1473 ->DurationAsString( $value * 60 );
1476 =head2 TimeWorkedAsString
1478 Returns the amount of time worked on this ticket as a text string.
1482 sub TimeWorkedAsString {
1484 return $self->_DurationAsString( $self->TimeWorked );
1487 =head2 TimeLeftAsString
1489 Returns the amount of time left on this ticket as a text string.
1493 sub TimeLeftAsString {
1495 return $self->_DurationAsString( $self->TimeLeft );
1498 =head2 TimeEstimatedAsString
1500 Returns the amount of time estimated on this ticket as a text string.
1504 sub TimeEstimatedAsString {
1506 return $self->_DurationAsString( $self->TimeEstimated );
1514 Comment on this ticket.
1515 Takes a hash with the following attributes:
1516 If MIMEObj is undefined, Content will be used to build a MIME::Entity for this
1519 MIMEObj, TimeTaken, CcMessageTo, BccMessageTo, Content, DryRun
1521 If DryRun is defined, this update WILL NOT BE RECORDED. Scrips will not be committed.
1522 They will, however, be prepared and you'll be able to access them through the TransactionObj
1524 Returns: Transaction id, Error Message, Transaction Object
1525 (note the different order from Create()!)
1532 my %args = ( CcMessageTo => undef,
1533 BccMessageTo => undef,
1540 unless ( ( $self->CurrentUserHasRight('CommentOnTicket') )
1541 or ( $self->CurrentUserHasRight('ModifyTicket') ) ) {
1542 return ( 0, $self->loc("Permission Denied"), undef );
1544 $args{'NoteType'} = 'Comment';
1546 $RT::Handle->BeginTransaction();
1547 if ($args{'DryRun'}) {
1548 $args{'CommitScrips'} = 0;
1551 my @results = $self->_RecordNote(%args);
1552 if ($args{'DryRun'}) {
1553 $RT::Handle->Rollback();
1555 $RT::Handle->Commit();
1564 Correspond on this ticket.
1565 Takes a hashref with the following attributes:
1568 MIMEObj, TimeTaken, CcMessageTo, BccMessageTo, Content, DryRun
1570 if there's no MIMEObj, Content is used to build a MIME::Entity object
1572 If DryRun is defined, this update WILL NOT BE RECORDED. Scrips will not be committed.
1573 They will, however, be prepared and you'll be able to access them through the TransactionObj
1575 Returns: Transaction id, Error Message, Transaction Object
1576 (note the different order from Create()!)
1583 my %args = ( CcMessageTo => undef,
1584 BccMessageTo => undef,
1590 unless ( ( $self->CurrentUserHasRight('ReplyToTicket') )
1591 or ( $self->CurrentUserHasRight('ModifyTicket') ) ) {
1592 return ( 0, $self->loc("Permission Denied"), undef );
1594 $args{'NoteType'} = 'Correspond';
1596 $RT::Handle->BeginTransaction();
1597 if ($args{'DryRun'}) {
1598 $args{'CommitScrips'} = 0;
1601 my @results = $self->_RecordNote(%args);
1603 unless ( $results[0] ) {
1604 $RT::Handle->Rollback();
1608 #Set the last told date to now if this isn't mail from the requestor.
1609 #TODO: Note that this will wrongly ack mail from any non-requestor as a "told"
1610 unless ( $self->IsRequestor($self->CurrentUser->id) ) {
1612 $squelch{$_}++ for map {$_->Content} $self->SquelchMailTo, $results[2]->SquelchMailTo;
1614 if grep {not $squelch{$_}} $self->Requestors->MemberEmailAddresses;
1617 if ($args{'DryRun'}) {
1618 $RT::Handle->Rollback();
1620 $RT::Handle->Commit();
1631 the meat of both comment and correspond.
1633 Performs no access control checks. hence, dangerous.
1640 CcMessageTo => undef,
1641 BccMessageTo => undef,
1646 NoteType => 'Correspond',
1649 SquelchMailTo => undef,
1654 unless ( $args{'MIMEObj'} || $args{'Content'} ) {
1655 return ( 0, $self->loc("No message attached"), undef );
1658 unless ( $args{'MIMEObj'} ) {
1659 my $data = ref $args{'Content'}? $args{'Content'} : [ $args{'Content'} ];
1660 $args{'MIMEObj'} = MIME::Entity->build(
1661 Type => "text/plain",
1663 Data => [ map {Encode::encode("UTF-8", $_)} @{$data} ],
1667 $args{'MIMEObj'}->head->replace('X-RT-Interface' => 'API')
1668 unless $args{'MIMEObj'}->head->get('X-RT-Interface');
1670 # convert text parts into utf-8
1671 RT::I18N::SetMIMEEntityToUTF8( $args{'MIMEObj'} );
1673 # If we've been passed in CcMessageTo and BccMessageTo fields,
1674 # add them to the mime object for passing on to the transaction handler
1675 # The "NotifyOtherRecipients" scripAction will look for RT-Send-Cc: and
1676 # RT-Send-Bcc: headers
1679 foreach my $type (qw/Cc Bcc/) {
1680 if ( defined $args{ $type . 'MessageTo' } ) {
1682 my $addresses = join ', ', (
1683 map { RT::User->CanonicalizeEmailAddress( $_->address ) }
1684 Email::Address->parse( $args{ $type . 'MessageTo' } ) );
1685 $args{'MIMEObj'}->head->replace( 'RT-Send-' . $type, Encode::encode( "UTF-8", $addresses ) );
1689 foreach my $argument (qw(Encrypt Sign)) {
1690 $args{'MIMEObj'}->head->replace(
1691 "X-RT-$argument" => $args{ $argument } ? 1 : 0
1692 ) if defined $args{ $argument };
1695 # If this is from an external source, we need to come up with its
1696 # internal Message-ID now, so all emails sent because of this
1697 # message have a common Message-ID
1698 my $org = RT->Config->Get('Organization');
1699 my $msgid = Encode::decode( "UTF-8", $args{'MIMEObj'}->head->get('Message-ID') );
1700 unless (defined $msgid && $msgid =~ /<(rt-.*?-\d+-\d+)\.(\d+-0-0)\@\Q$org\E>/) {
1701 $args{'MIMEObj'}->head->replace(
1702 'RT-Message-ID' => Encode::encode( "UTF-8",
1703 RT::Interface::Email::GenMessageId( Ticket => $self )
1708 #Record the correspondence (write the transaction)
1709 my ( $Trans, $msg, $TransObj ) = $self->_NewTransaction(
1710 Type => $args{'NoteType'},
1711 Data => ( Encode::decode( "UTF-8", $args{'MIMEObj'}->head->get('Subject') ) || 'No Subject' ),
1712 TimeTaken => $args{'TimeTaken'},
1713 MIMEObj => $args{'MIMEObj'},
1714 CommitScrips => $args{'CommitScrips'},
1715 SquelchMailTo => $args{'SquelchMailTo'},
1716 CustomFields => $args{'CustomFields'},
1720 $RT::Logger->err("$self couldn't init a transaction $msg");
1721 return ( $Trans, $self->loc("Message could not be recorded"), undef );
1724 if ($args{NoteType} eq "Comment") {
1725 $msg = $self->loc("Comments added");
1727 $msg = $self->loc("Correspondence added");
1729 return ( $Trans, $msg, $TransObj );
1735 Builds a MIME object from the given C<UpdateSubject> and
1736 C<UpdateContent>, then calls L</Comment> or L</Correspond> with
1737 C<< DryRun => 1 >>, and returns the transaction so produced.
1745 if (($args{'UpdateType'} || $args{Action}) =~ /^respon(d|se)$/i ) {
1746 $action = 'Correspond';
1748 $action = 'Comment';
1751 my $Message = MIME::Entity->build(
1752 Subject => defined $args{UpdateSubject} ? Encode::encode( "UTF-8", $args{UpdateSubject} ) : "",
1753 Type => 'text/plain',
1755 Data => Encode::encode("UTF-8", $args{'UpdateContent'} || ""),
1758 my ( $Transaction, $Description, $Object ) = $self->$action(
1759 CcMessageTo => $args{'UpdateCc'},
1760 BccMessageTo => $args{'UpdateBcc'},
1761 MIMEObj => $Message,
1762 TimeTaken => $args{'UpdateTimeWorked'},
1764 SquelchMailTo => $args{'SquelchMailTo'},
1766 unless ( $Transaction ) {
1767 $RT::Logger->error("Couldn't fire '$action' action: $Description");
1775 Prepares a MIME mesage with the given C<Subject>, C<Cc>, and
1776 C<Content>, then calls L</Create> with C<< DryRun => 1 >> and returns
1777 the resulting L<RT::Transaction>.
1784 my $Message = MIME::Entity->build(
1785 Subject => defined $args{Subject} ? Encode::encode( "UTF-8", $args{'Subject'} ) : "",
1786 (defined $args{'Cc'} ?
1787 ( Cc => Encode::encode( "UTF-8", $args{'Cc'} ) ) : ()),
1788 Type => 'text/plain',
1790 Data => Encode::encode( "UTF-8", $args{'Content'} || ""),
1793 my ( $Transaction, $Object, $Description ) = $self->Create(
1794 Type => $args{'Type'} || 'ticket',
1795 Queue => $args{'Queue'},
1796 Owner => $args{'Owner'},
1797 Requestor => $args{'Requestors'},
1799 AdminCc => $args{'AdminCc'},
1800 InitialPriority => $args{'InitialPriority'},
1801 FinalPriority => $args{'FinalPriority'},
1802 TimeLeft => $args{'TimeLeft'},
1803 TimeEstimated => $args{'TimeEstimated'},
1804 TimeWorked => $args{'TimeWorked'},
1805 Subject => $args{'Subject'},
1806 Status => $args{'Status'},
1807 MIMEObj => $Message,
1810 unless ( $Transaction ) {
1811 $RT::Logger->error("Couldn't fire Create action: $Description");
1822 #TODO: Field isn't the right thing here. but I ahave no idea what mnemonic ---
1825 my $type = shift || "";
1827 my $cache_key = "$field$type";
1828 return $self->{ $cache_key } if $self->{ $cache_key };
1830 my $links = $self->{ $cache_key }
1831 = RT::Links->new( $self->CurrentUser );
1832 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
1833 $links->Limit( FIELD => 'id', VALUE => 0, SUBCLAUSE => 'acl' );
1837 # Maybe this ticket is a merge ticket
1838 my $limit_on = 'Local'. $field;
1839 # at least to myself
1843 VALUE => [ $self->id, $self->Merged ],
1855 MergeInto take the id of the ticket to merge this ticket into.
1861 my $ticket_id = shift;
1863 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
1864 return ( 0, $self->loc("Permission Denied") );
1867 # Load up the new ticket.
1868 my $MergeInto = RT::Ticket->new($self->CurrentUser);
1869 $MergeInto->Load($ticket_id);
1871 # make sure it exists.
1872 unless ( $MergeInto->Id ) {
1873 return ( 0, $self->loc("New ticket doesn't exist") );
1876 # Can't merge into yourself
1877 if ( $MergeInto->Id == $self->Id ) {
1878 return ( 0, $self->loc("Can't merge a ticket into itself") );
1881 # Make sure the current user can modify the new ticket.
1882 unless ( $MergeInto->CurrentUserHasRight('ModifyTicket') ) {
1883 return ( 0, $self->loc("Permission Denied") );
1886 delete $MERGE_CACHE{'effective'}{ $self->id };
1887 delete @{ $MERGE_CACHE{'merged'} }{
1888 $ticket_id, $MergeInto->id, $self->id
1891 $RT::Handle->BeginTransaction();
1893 my ($ok, $msg) = $self->_MergeInto( $MergeInto );
1895 $RT::Handle->Commit() if $ok;
1902 my $MergeInto = shift;
1905 # We use EffectiveId here even though it duplicates information from
1906 # the links table becasue of the massive performance hit we'd take
1907 # by trying to do a separate database query for merge info everytime
1910 #update this ticket's effective id to the new ticket's id.
1911 my ( $id_val, $id_msg ) = $self->__Set(
1912 Field => 'EffectiveId',
1913 Value => $MergeInto->Id()
1917 $RT::Handle->Rollback();
1918 return ( 0, $self->loc("Merge failed. Couldn't set EffectiveId") );
1921 ( $id_val, $id_msg ) = $self->__Set( Field => 'IsMerged', Value => 1 );
1923 $RT::Handle->Rollback();
1924 return ( 0, $self->loc("Merge failed. Couldn't set IsMerged") );
1927 my $force_status = $self->LifecycleObj->DefaultOnMerge;
1928 if ( $force_status && $force_status ne $self->__Value('Status') ) {
1929 my ( $status_val, $status_msg )
1930 = $self->__Set( Field => 'Status', Value => $force_status );
1932 unless ($status_val) {
1933 $RT::Handle->Rollback();
1935 "Couldn't set status to $force_status. RT's Database may be inconsistent."
1937 return ( 0, $self->loc("Merge failed. Couldn't set Status") );
1941 # update all the links that point to that old ticket
1942 my $old_links_to = RT::Links->new($self->CurrentUser);
1943 $old_links_to->Limit(FIELD => 'Target', VALUE => $self->URI);
1946 while (my $link = $old_links_to->Next) {
1947 if (exists $old_seen{$link->Base."-".$link->Type}) {
1950 elsif ($link->Base eq $MergeInto->URI) {
1953 # First, make sure the link doesn't already exist. then move it over.
1954 my $tmp = RT::Link->new(RT->SystemUser);
1955 $tmp->LoadByCols(Base => $link->Base, Type => $link->Type, LocalTarget => $MergeInto->id);
1959 $link->SetTarget($MergeInto->URI);
1960 $link->SetLocalTarget($MergeInto->id);
1962 $old_seen{$link->Base."-".$link->Type} =1;
1967 my $old_links_from = RT::Links->new($self->CurrentUser);
1968 $old_links_from->Limit(FIELD => 'Base', VALUE => $self->URI);
1970 while (my $link = $old_links_from->Next) {
1971 if (exists $old_seen{$link->Type."-".$link->Target}) {
1974 if ($link->Target eq $MergeInto->URI) {
1977 # First, make sure the link doesn't already exist. then move it over.
1978 my $tmp = RT::Link->new(RT->SystemUser);
1979 $tmp->LoadByCols(Target => $link->Target, Type => $link->Type, LocalBase => $MergeInto->id);
1983 $link->SetBase($MergeInto->URI);
1984 $link->SetLocalBase($MergeInto->id);
1985 $old_seen{$link->Type."-".$link->Target} =1;
1991 # Update time fields
1992 foreach my $type (qw(TimeEstimated TimeWorked TimeLeft)) {
1995 Value => ( $MergeInto->$type() || 0 ) + ( $self->$type() || 0 ),
1996 RecordTransaction => 0,
2000 # add all of this ticket's watchers to that ticket.
2001 for my $role ($self->Roles) {
2002 next if $self->RoleGroup($role)->SingleMemberRoleGroup;
2003 my $people = $self->RoleGroup($role)->MembersObj;
2004 while ( my $watcher = $people->Next ) {
2005 my ($val, $msg) = $MergeInto->AddRoleMember(
2008 PrincipalId => $watcher->MemberId,
2009 InsideTransaction => 1,
2012 $RT::Logger->debug($msg);
2017 #find all of the tickets that were merged into this ticket.
2018 my $old_mergees = RT::Tickets->new( $self->CurrentUser );
2019 $old_mergees->Limit(
2020 FIELD => 'EffectiveId',
2025 # update their EffectiveId fields to the new ticket's id
2026 while ( my $ticket = $old_mergees->Next() ) {
2027 my ( $val, $msg ) = $ticket->__Set(
2028 Field => 'EffectiveId',
2029 Value => $MergeInto->Id()
2033 #make a new link: this ticket is merged into that other ticket.
2034 $self->AddLink( Type => 'MergedInto', Target => $MergeInto->Id());
2036 $MergeInto->_SetLastUpdated;
2038 return ( 1, $self->loc("Merge Successful") );
2043 Returns list of tickets' ids that's been merged into this ticket.
2051 return @{ $MERGE_CACHE{'merged'}{ $id } }
2052 if $MERGE_CACHE{'merged'}{ $id };
2054 my $mergees = RT::Tickets->new( $self->CurrentUser );
2055 $mergees->LimitField(
2056 FIELD => 'EffectiveId',
2059 $mergees->LimitField(
2064 return @{ $MERGE_CACHE{'merged'}{ $id } ||= [] }
2065 = map $_->id, @{ $mergees->ItemsArrayRef || [] };
2074 Takes nothing and returns an RT::User object of
2082 #If this gets ACLed, we lose on a rights check in User.pm and
2083 #get deep recursion. if we need ACLs here, we need
2084 #an equiv without ACLs
2086 my $owner = RT::User->new( $self->CurrentUser );
2087 $owner->Load( $self->__Value('Owner') );
2089 #Return the owner object
2095 =head2 OwnerAsString
2097 Returns the owner's email address
2103 return ( $self->OwnerObj->EmailAddress );
2111 Takes two arguments:
2112 the Id or Name of the owner
2113 and (optionally) the type of the SetOwner Transaction. It defaults
2114 to 'Set'. 'Steal' is also a valid option.
2121 my $NewOwner = shift;
2122 my $Type = shift || "Set";
2124 $RT::Handle->BeginTransaction();
2126 $self->_SetLastUpdated(); # lock the ticket
2127 $self->Load( $self->id ); # in case $self changed while waiting for lock
2129 my $OldOwnerObj = $self->OwnerObj;
2131 my $NewOwnerObj = RT::User->new( $self->CurrentUser );
2132 $NewOwnerObj->Load( $NewOwner );
2134 my ( $val, $msg ) = $self->CurrentUserCanSetOwner(
2135 NewOwnerObj => $NewOwnerObj,
2139 $RT::Handle->Rollback();
2140 return ( $val, $msg );
2143 ($val, $msg ) = $self->OwnerGroup->_AddMember(
2144 PrincipalId => $NewOwnerObj->PrincipalId,
2145 InsideTransaction => 1,
2149 $RT::Handle->Rollback;
2150 return ( 0, $self->loc("Could not change owner: [_1]", $msg) );
2153 $msg = $self->loc( "Owner changed from [_1] to [_2]",
2154 $OldOwnerObj->Name, $NewOwnerObj->Name );
2156 $RT::Handle->Commit();
2158 return ( $val, $msg );
2161 =head2 CurrentUserCanSetOwner
2163 Confirm the current user can set the owner of the current ticket.
2165 There are several different rights to manage owner changes and
2166 this method evaluates these rights, guided by parameters provided.
2168 This method evaluates these rights in the context of the state of
2169 the current ticket. For example, it evaluates Take for tickets that
2170 are owned by Nobody because that is the context appropriate for the
2171 TakeTicket right. If you need to strictly test a user for a right,
2172 use HasRight to check for the right directly.
2174 For some custom types of owner changes (C<Take> and C<Steal>), it also
2175 verifies that those actions are possible given the current ticket owner.
2177 =head3 Rights to Set Owner
2179 The current user can set or change the Owner field in the following
2186 ReassignTicket unconditionally grants the right to set the owner
2187 to any user who has OwnTicket. This can be used to break an
2188 Owner lock held by another user (see below) and can be a convenient
2189 right for managers or administrators who need to assign tickets
2190 without necessarily owning them.
2194 ModifyTicket grants the right to set the owner to any user who
2195 has OwnTicket, provided the ticket is currently owned by the current
2196 user or is not owned (owned by Nobody). (See the details on the Force
2197 parameter below for exceptions to this.)
2201 If the ticket is currently not owned (owned by Nobody),
2202 TakeTicket is sufficient to set the owner to yourself (but not
2203 an arbitrary person), but only if you have OwnTicket. It is
2204 thus a subset of the possible changes provided by ModifyTicket.
2205 This exists to allow granting TakeTicket freely, and
2206 the broader ModifyTicket only to Owners.
2210 If the ticket is currently owned by someone who is not you or
2211 Nobody, StealTicket is sufficient to set the owner to yourself,
2212 but only if you have OwnTicket. This is hence non-overlapping
2213 with the changes provided by ModifyTicket, and is used to break
2214 a lock held by another user.
2220 This method returns ($result, $message) with $result containing
2221 true or false indicating if the current user can set owner and $message
2222 containing a message, typically in the case of a false response.
2224 If called with no parameters, this method determines if the current
2225 user could set the owner of the current ticket given any
2226 permutation of the rights described above. This can be useful
2227 when determining whether to make owner-setting options available
2230 This method accepts the following parameters as a paramshash:
2234 =item C<NewOwnerObj>
2236 Optional; an L<RT::User> object representing the proposed new owner of
2241 Optional; the type of set owner operation. Valid values are C<Take>,
2242 C<Steal>, or C<Force>. Note that if the type is C<Take>, this method
2243 will return false if the current user is already the owner; similarly,
2244 it will return false for C<Steal> if the ticket has no owner or the
2245 owner is the current user.
2249 As noted above, there are exceptions to the standard ticket-based rights
2250 described here. The Force option allows for these and is used
2251 when moving tickets between queues, for reminders (because the full
2252 owner rights system is too complex for them), and optionally during
2257 sub CurrentUserCanSetOwner {
2259 my %args = ( Type => '',
2261 my $OldOwnerObj = $self->OwnerObj;
2263 $args{NewOwnerObj} ||= $self->CurrentUser->UserObj
2264 if $args{Type} eq "Take" or $args{Type} eq "Steal";
2266 # Confirm rights for new owner if we got one
2267 if ( $args{'NewOwnerObj'} ){
2268 my ($ok, $message) = $self->_NewOwnerCanOwnTicket($args{'NewOwnerObj'}, $OldOwnerObj);
2269 return ($ok, $message) if not $ok;
2272 # ReassignTicket allows you to SetOwner, but we also need to check ticket's
2273 # current owner for Take and Steal Types
2274 return ( 1, undef ) if $self->CurrentUserHasRight('ReassignTicket')
2275 && $args{Type} ne 'Take' && $args{Type} ne 'Steal';
2278 if ( $OldOwnerObj->Id == RT->Nobody->Id ) {
2280 # Steal is not applicable for unowned tickets.
2281 if ( $args{'Type'} eq 'Steal' ){
2282 return ( 0, $self->loc("You can only steal a ticket owned by someone else") )
2285 # Can set owner to yourself with ModifyTicket, ReassignTicket,
2286 # or TakeTicket; in all of these cases, OwnTicket is checked by
2287 # _NewOwnerCanOwnTicket above.
2288 if ( $args{'Type'} eq 'Take'
2289 or ( $args{'NewOwnerObj'}
2290 and $args{'NewOwnerObj'}->id == $self->CurrentUser->id )) {
2291 unless ( $self->CurrentUserHasRight('ModifyTicket')
2292 or $self->CurrentUserHasRight('ReassignTicket')
2293 or $self->CurrentUserHasRight('TakeTicket') ) {
2294 return ( 0, $self->loc("Permission Denied") );
2297 # Nobody -> someone else requires ModifyTicket or ReassignTicket
2298 unless ( $self->CurrentUserHasRight('ModifyTicket')
2299 or $self->CurrentUserHasRight('ReassignTicket') ) {
2300 return ( 0, $self->loc("Permission Denied") );
2305 # Ticket is owned by someone else
2306 # Can set owner to yourself with ModifyTicket or StealTicket
2308 elsif ( $OldOwnerObj->Id != RT->Nobody->Id
2309 && $OldOwnerObj->Id != $self->CurrentUser->id ) {
2311 unless ( $self->CurrentUserHasRight('ModifyTicket')
2312 || $self->CurrentUserHasRight('ReassignTicket')
2313 || $self->CurrentUserHasRight('StealTicket') ) {
2314 return ( 0, $self->loc("Permission Denied") )
2317 if ( $args{'Type'} eq 'Steal' || $args{'Type'} eq 'Force' ){
2318 return ( 1, undef ) if $self->CurrentUserHasRight('OwnTicket');
2319 return ( 0, $self->loc("Permission Denied") );
2322 # Not a steal or force
2323 if ( $args{'Type'} eq 'Take'
2324 or ( $args{'NewOwnerObj'}
2325 and $args{'NewOwnerObj'}->id == $self->CurrentUser->id )) {
2326 return ( 0, $self->loc("You can only take tickets that are unowned") );
2329 unless ( $self->CurrentUserHasRight('ReassignTicket') ) {
2330 return ( 0, $self->loc( "You can only reassign tickets that you own or that are unowned"));
2334 # You own the ticket
2335 # Untake falls through to here, so we don't need to explicitly handle that Type
2337 if ( $args{'Type'} eq 'Take' || $args{'Type'} eq 'Steal' ) {
2338 return ( 0, $self->loc("You already own this ticket") );
2341 unless ( $self->CurrentUserHasRight('ModifyTicket')
2342 || $self->CurrentUserHasRight('ReassignTicket') ) {
2343 return ( 0, $self->loc("Permission Denied") );
2347 return ( 1, undef );
2350 # Verify the proposed new owner can own the ticket.
2352 sub _NewOwnerCanOwnTicket {
2354 my $NewOwnerObj = shift;
2355 my $OldOwnerObj = shift;
2357 unless ( $NewOwnerObj->Id ) {
2358 return ( 0, $self->loc("That user does not exist") );
2361 # The proposed new owner can't own the ticket
2362 if ( !$NewOwnerObj->HasRight( Right => 'OwnTicket', Object => $self ) ){
2363 return ( 0, $self->loc("That user may not own tickets in that queue") );
2366 # Ticket's current owner is the same as the new owner, nothing to do
2367 elsif ( $NewOwnerObj->Id == $OldOwnerObj->Id ) {
2368 return ( 0, $self->loc("That user already owns that ticket") );
2376 A convenince method to set the ticket's owner to the current user
2382 return ( $self->SetOwner( $self->CurrentUser->Id, 'Take' ) );
2389 Convenience method to set the owner to 'nobody' if the current user is the owner.
2395 return ( $self->SetOwner( RT->Nobody->UserObj->Id, 'Untake' ) );
2402 A convenience method to change the owner of the current ticket to the
2403 current user. Even if it's owned by another user.
2410 if ( $self->IsOwner( $self->CurrentUser ) ) {
2411 return ( 0, $self->loc("You already own this ticket") );
2414 return ( $self->SetOwner( $self->CurrentUser->Id, 'Steal' ) );
2420 =head2 SetStatus STATUS
2422 Set this ticket's status.
2424 Alternatively, you can pass in a list of named parameters (Status => STATUS, Force => FORCE, SetStarted => SETSTARTED ).
2425 If FORCE is true, ignore unresolved dependencies and force a status change.
2426 if SETSTARTED is true (it's the default value), set Started to current datetime if Started
2427 is not set and the status is changed from initial to not initial.
2435 $args{Status} = shift;
2441 # this only allows us to SetStarted, not we must SetStarted.
2442 # this option was added for rtir initially
2443 $args{SetStarted} = 1 unless exists $args{SetStarted};
2445 my ($valid, $msg) = $self->ValidateStatusChange($args{Status});
2446 return ($valid, $msg) unless $valid;
2448 my $lifecycle = $self->LifecycleObj;
2451 && !$lifecycle->IsInactive($self->Status)
2452 && $lifecycle->IsInactive($args{Status})
2453 && $self->HasUnresolvedDependencies )
2455 return ( 0, $self->loc('That ticket has unresolved dependencies') );
2458 return $self->_SetStatus(
2459 Status => $args{Status},
2460 SetStarted => $args{SetStarted},
2469 RecordTransaction => 1,
2470 Lifecycle => $self->LifecycleObj,
2473 $args{Status} = lc $args{Status} if defined $args{Status};
2474 $args{NewLifecycle} ||= $args{Lifecycle};
2476 my $now = RT::Date->new( $self->CurrentUser );
2479 my $raw_started = RT::Date->new(RT->SystemUser);
2480 $raw_started->Set(Format => 'ISO', Value => $self->__Value('Started'));
2482 my $old = $self->__Value('Status');
2484 # If we're changing the status from new, record that we've started
2485 if ( $args{SetStarted}
2486 && $args{Lifecycle}->IsInitial($old)
2487 && !$args{NewLifecycle}->IsInitial($args{Status})
2488 && !$raw_started->IsSet) {
2489 # Set the Started time to "now"
2493 RecordTransaction => 0
2497 # When we close a ticket, set the 'Resolved' attribute to now.
2498 # It's misnamed, but that's just historical.
2499 if ( $args{NewLifecycle}->IsInactive($args{Status}) ) {
2501 Field => 'Resolved',
2503 RecordTransaction => 0,
2507 # Actually update the status
2508 my ($val, $msg)= $self->_Set(
2510 Value => $args{Status},
2513 TransactionType => 'Status',
2514 RecordTransaction => $args{RecordTransaction},
2516 return ($val, $msg);
2523 my $taken = ($value||0) - ($self->__Value('TimeWorked')||0);
2526 Field => 'TimeWorked',
2528 TimeTaken => $taken,
2534 Takes no arguments. Marks this ticket for garbage collection
2540 unless ( $self->LifecycleObj->IsValid('deleted') ) {
2541 return (0, $self->loc('Delete operation is disabled by lifecycle configuration') ); #loc
2543 return ( $self->SetStatus('deleted') );
2547 =head2 SetTold ISO [TIMETAKEN]
2549 Updates the told and records a transaction
2556 $told = shift if (@_);
2557 my $timetaken = shift || 0;
2559 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
2560 return ( 0, $self->loc("Permission Denied") );
2563 my $datetold = RT::Date->new( $self->CurrentUser );
2565 $datetold->Set( Format => 'iso',
2569 $datetold->SetToNow();
2572 return ( $self->_Set( Field => 'Told',
2573 Value => $datetold->ISO,
2574 TimeTaken => $timetaken,
2575 TransactionType => 'Told' ) );
2580 Updates the told without a transaction or acl check. Useful when we're sending replies.
2587 my $now = RT::Date->new( $self->CurrentUser );
2590 #use __Set to get no ACLs ;)
2591 return ( $self->__Set( Field => 'Told',
2592 Value => $now->ISO ) );
2602 my $uid = $self->CurrentUser->id;
2603 my $attr = $self->FirstAttribute( "User-". $uid ."-SeenUpTo" );
2604 return if $attr && $attr->Content gt $self->LastUpdated;
2606 my $txns = $self->Transactions;
2607 $txns->Limit( FIELD => 'Type', VALUE => 'Comment' );
2608 $txns->Limit( FIELD => 'Type', VALUE => 'Correspond' );
2609 $txns->Limit( FIELD => 'Creator', OPERATOR => '!=', VALUE => $uid );
2613 VALUE => $attr->Content
2615 $txns->RowsPerPage(1);
2616 return $txns->First;
2619 =head2 RanTransactionBatch
2621 Acts as a guard around running TransactionBatch scrips.
2623 Should be false until you enter the code that runs TransactionBatch scrips
2625 Accepts an optional argument to indicate that TransactionBatch Scrips should no longer be run on this object.
2629 sub RanTransactionBatch {
2633 if ( defined $val ) {
2634 return $self->{_RanTransactionBatch} = $val;
2636 return $self->{_RanTransactionBatch};
2642 =head2 TransactionBatch
2644 Returns an array reference of all transactions created on this ticket during
2645 this ticket object's lifetime or since last application of a batch, or undef
2648 Only works when the C<UseTransactionBatch> config option is set to true.
2652 sub TransactionBatch {
2654 return $self->{_TransactionBatch};
2657 =head2 ApplyTransactionBatch
2659 Applies scrips on the current batch of transactions and shinks it. Usually
2660 batch is applied when object is destroyed, but in some cases it's too late.
2664 sub ApplyTransactionBatch {
2667 my $batch = $self->TransactionBatch;
2668 return unless $batch && @$batch;
2670 $self->_ApplyTransactionBatch;
2672 $self->{_TransactionBatch} = [];
2675 sub _ApplyTransactionBatch {
2678 return if $self->RanTransactionBatch;
2679 $self->RanTransactionBatch(1);
2681 my $still_exists = RT::Ticket->new( RT->SystemUser );
2682 $still_exists->Load( $self->Id );
2683 if (not $still_exists->Id) {
2684 # The ticket has been removed from the database, but we still
2685 # have pending TransactionBatch txns for it. Unfortunately,
2686 # because it isn't in the DB anymore, attempting to run scrips
2687 # on it may produce unpredictable results; simply drop the
2688 # batched transactions.
2689 $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.");
2693 my $batch = $self->TransactionBatch;
2696 my $types = join ',', grep !$seen{$_}++, grep defined, map $_->__Value('Type'), grep defined, @{$batch};
2699 RT::Scrips->new(RT->SystemUser)->Apply(
2700 Stage => 'TransactionBatch',
2702 TransactionObj => $batch->[0],
2706 # Entry point of the rule system
2707 my $rules = RT::Ruleset->FindAllRules(
2708 Stage => 'TransactionBatch',
2710 TransactionObj => $batch->[0],
2713 RT::Ruleset->CommitRules($rules);
2719 # DESTROY methods need to localize $@, or it may unset it. This
2720 # causes $m->abort to not bubble all of the way up. See perlbug
2721 # http://rt.perl.org/rt3/Ticket/Display.html?id=17650
2724 # The following line eliminates reentrancy.
2725 # It protects against the fact that perl doesn't deal gracefully
2726 # when an object's refcount is changed in its destructor.
2727 return if $self->{_Destroyed}++;
2729 if (in_global_destruction()) {
2730 unless ($ENV{'HARNESS_ACTIVE'}) {
2731 warn "Too late to safely run transaction-batch scrips!"
2732 ." This is typically caused by using ticket objects"
2733 ." at the top-level of a script which uses the RT API."
2734 ." Be sure to explicitly undef such ticket objects,"
2735 ." or put them inside of a lexical scope.";
2740 return $self->ApplyTransactionBatch;
2746 sub _OverlayAccessible {
2748 EffectiveId => { 'read' => 1, 'write' => 1, 'public' => 1 },
2749 Queue => { 'read' => 1, 'write' => 1 },
2750 Requestors => { 'read' => 1, 'write' => 1 },
2751 Owner => { 'read' => 1, 'write' => 1 },
2752 Subject => { 'read' => 1, 'write' => 1 },
2753 InitialPriority => { 'read' => 1, 'write' => 1 },
2754 FinalPriority => { 'read' => 1, 'write' => 1 },
2755 Priority => { 'read' => 1, 'write' => 1 },
2756 Status => { 'read' => 1, 'write' => 1 },
2757 TimeEstimated => { 'read' => 1, 'write' => 1 },
2758 TimeWorked => { 'read' => 1, 'write' => 1 },
2759 TimeLeft => { 'read' => 1, 'write' => 1 },
2760 Told => { 'read' => 1, 'write' => 1 },
2761 Resolved => { 'read' => 1 },
2762 Type => { 'read' => 1 },
2763 Starts => { 'read' => 1, 'write' => 1 },
2764 Started => { 'read' => 1, 'write' => 1 },
2765 Due => { 'read' => 1, 'write' => 1 },
2766 Creator => { 'read' => 1, 'auto' => 1 },
2767 Created => { 'read' => 1, 'auto' => 1 },
2768 LastUpdatedBy => { 'read' => 1, 'auto' => 1 },
2769 LastUpdated => { 'read' => 1, 'auto' => 1 }
2779 my %args = ( Field => undef,
2782 RecordTransaction => 1,
2784 TransactionType => 'Set',
2787 if ($args{'CheckACL'}) {
2788 unless ( $self->CurrentUserHasRight('ModifyTicket')) {
2789 return ( 0, $self->loc("Permission Denied"));
2793 # Avoid ACL loops using _Value
2794 my $Old = $self->SUPER::_Value($args{'Field'});
2797 my ( $ret, $msg ) = $self->SUPER::_Set(
2798 Field => $args{'Field'},
2799 Value => $args{'Value'}
2801 return ( 0, $msg ) unless $ret;
2803 return ( $ret, $msg ) unless $args{'RecordTransaction'};
2806 ( $ret, $msg, $trans ) = $self->_NewTransaction(
2807 Type => $args{'TransactionType'},
2808 Field => $args{'Field'},
2809 NewValue => $args{'Value'},
2811 TimeTaken => $args{'TimeTaken'},
2814 # Ensure that we can read the transaction, even if the change
2815 # just made the ticket unreadable to us
2816 $trans->{ _object_is_readable } = 1;
2818 return ( $ret, scalar $trans->BriefDescription );
2825 Takes the name of a table column.
2826 Returns its value as a string, if the user passes an ACL check
2835 #if the field is public, return it.
2836 if ( $self->_Accessible( $field, 'public' ) ) {
2838 #$RT::Logger->debug("Skipping ACL check for $field");
2839 return ( $self->SUPER::_Value($field) );
2843 #If the current user doesn't have ACLs, don't let em at it.
2845 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
2848 return ( $self->SUPER::_Value($field) );
2854 Customization of L<RT::Record/Attachments> for tickets.
2865 my $res = RT::Attachments->new( $self->CurrentUser );
2866 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
2871 ENTRYAGGREGATOR => 'AND'
2876 my @columns = grep { not /^(Headers|Content)$/ }
2877 RT::Attachment->ReadableAttributes;
2878 push @columns, 'Headers' if $args{'WithHeaders'};
2879 push @columns, 'Content' if $args{'WithContent'};
2881 $res->Columns( @columns );
2882 my $txn_alias = $res->TransactionAlias;
2884 ALIAS => $txn_alias,
2885 FIELD => 'ObjectType',
2886 VALUE => ref($self),
2888 my $ticket_alias = $res->Join(
2889 ALIAS1 => $txn_alias,
2890 FIELD1 => 'ObjectId',
2891 TABLE2 => 'Tickets',
2895 ALIAS => $ticket_alias,
2896 FIELD => 'EffectiveId',
2902 =head2 TextAttachments
2904 Customization of L<RT::Record/TextAttachments> for tickets.
2908 sub TextAttachments {
2911 my $res = $self->SUPER::TextAttachments( @_ );
2912 unless ( $self->CurrentUserHasRight('ShowTicketComments') ) {
2913 # if the user may not see comments do not return them
2916 ALIAS => $res->TransactionAlias,
2928 =head2 _UpdateTimeTaken
2930 This routine will increment the timeworked counter. it should
2931 only be called from _NewTransaction
2935 sub _UpdateTimeTaken {
2937 my $Minutes = shift;
2940 if ( my $txn = $rest{'Transaction'} ) {
2941 return if $txn->__Value('Type') eq 'Set' && $txn->__Value('Field') eq 'TimeWorked';
2944 my $Total = $self->__Value("TimeWorked");
2945 $Total = ( $Total || 0 ) + ( $Minutes || 0 );
2947 Field => "TimeWorked",
2949 RecordTransaction => 0,
2956 =head2 CurrentUserCanSee
2958 Returns true if the current user can see the ticket, using ShowTicket
2962 sub CurrentUserCanSee {
2964 my ($what, $txn) = @_;
2965 return 0 unless $self->CurrentUserHasRight('ShowTicket');
2967 return 1 if $what ne "Transaction";
2969 # If it's a comment, we need to be extra special careful
2970 my $type = $txn->__Value('Type');
2971 if ( $type eq 'Comment' ) {
2972 unless ( $self->CurrentUserHasRight('ShowTicketComments') ) {
2975 } elsif ( $type eq 'CommentEmailRecord' ) {
2976 unless ( $self->CurrentUserHasRight('ShowTicketComments')
2977 && $self->CurrentUserHasRight('ShowOutgoingEmail') ) {
2980 } elsif ( $type eq 'EmailRecord' ) {
2981 unless ( $self->CurrentUserHasRight('ShowOutgoingEmail') ) {
2990 Return the Reminders object for this ticket. (It's an RT::Reminders object.)
2991 It isn't acutally a searchbuilder collection itself.
2998 unless ($self->{'__reminders'}) {
2999 $self->{'__reminders'} = RT::Reminders->new($self->CurrentUser);
3000 $self->{'__reminders'}->Ticket($self->id);
3002 return $self->{'__reminders'};
3011 Returns an RT::Transactions object of all transactions on this ticket
3018 my $transactions = RT::Transactions->new( $self->CurrentUser );
3020 #If the user has no rights, return an empty object
3021 if ( $self->CurrentUserHasRight('ShowTicket') ) {
3022 $transactions->LimitToTicket($self->id);
3024 # if the user may not see comments do not return them
3025 unless ( $self->CurrentUserHasRight('ShowTicketComments') ) {
3026 $transactions->Limit(
3032 $transactions->Limit(
3036 VALUE => "CommentEmailRecord",
3037 ENTRYAGGREGATOR => 'AND'
3042 $transactions->Limit(
3046 ENTRYAGGREGATOR => 'AND'
3050 return ($transactions);
3056 =head2 TransactionCustomFields
3058 Returns the custom fields that transactions on tickets will have.
3062 sub TransactionCustomFields {
3064 my $cfs = $self->QueueObj->TicketTransactionCustomFields;
3065 $cfs->SetContextObject( $self );
3070 =head2 LoadCustomFieldByIdentifier
3072 Finds and returns the custom field of the given name for the ticket,
3073 overriding L<RT::Record/LoadCustomFieldByIdentifier> to look for
3074 queue-specific CFs before global ones.
3078 sub LoadCustomFieldByIdentifier {
3082 return $self->SUPER::LoadCustomFieldByIdentifier($field)
3083 if ref $field or $field =~ /^\d+$/;
3085 my $cf = RT::CustomField->new( $self->CurrentUser );
3086 $cf->SetContextObject( $self );
3089 LookupType => $self->CustomFieldLookupType,
3090 ObjectId => $self->Queue,
3097 =head2 CustomFieldLookupType
3099 Returns the RT::Ticket lookup type, which can be passed to
3100 RT::CustomField->Create() via the 'LookupType' hash key.
3105 sub CustomFieldLookupType {
3106 "RT::Queue-RT::Ticket";
3109 =head2 ACLEquivalenceObjects
3111 This method returns a list of objects for which a user's rights also apply
3112 to this ticket. Generally, this is only the ticket's queue, but some RT
3113 extensions may make other objects available too.
3115 This method is called from L<RT::Principal/HasRight>.
3119 sub ACLEquivalenceObjects {
3121 return $self->QueueObj;
3125 =head2 ModifyLinkRight
3129 sub ModifyLinkRight { "ModifyTicket" }
3131 =head2 Forward Transaction => undef, To => '', Cc => '', Bcc => ''
3133 Forwards transaction with all attachments as 'message/rfc822'.
3140 Transaction => undef,
3146 ContentType => 'text/plain',
3152 unless ( $self->CurrentUserHasRight('ForwardMessage') ) {
3153 return ( 0, $self->loc("Permission Denied") );
3156 $args{$_} = join ", ", map { $_->format } RT::EmailParser->ParseEmailAddress( $args{$_} || '' ) for qw(To Cc Bcc);
3158 return (0, $self->loc("Can't forward: no valid email addresses specified") )
3159 unless grep {length $args{$_}} qw/To Cc Bcc/;
3161 my $mime = MIME::Entity->build(
3162 Type => $args{ContentType},
3163 Data => Encode::encode( "UTF-8", $args{Content} ),
3166 $mime->head->replace( $_ => Encode::encode('UTF-8',$args{$_} ) )
3167 for grep defined $args{$_}, qw(Subject To Cc Bcc);
3168 $mime->head->replace(
3169 From => Encode::encode( 'UTF-8',
3170 RT::Interface::Email::GetForwardFrom(
3171 Transaction => $args{Transaction},
3177 if ($args{'DryRun'}) {
3178 $RT::Handle->BeginTransaction();
3179 $args{'CommitScrips'} = 0;
3182 my ( $ret, $msg ) = $self->_NewTransaction(
3185 Type => 'Forward Transaction',
3186 Field => $args{Transaction}->id,
3189 Type => 'Forward Ticket',
3192 Data => join( ', ', grep { length } $args{To}, $args{Cc}, $args{Bcc} ),
3194 CommitScrips => $args{'CommitScrips'},
3198 $RT::Logger->error("Failed to create transaction: $msg");
3201 if ($args{'DryRun'}) {
3202 $RT::Handle->Rollback();
3204 return ( $ret, $self->loc('Message recorded') );
3211 Jesse Vincent, jesse@bestpractical.com
3219 sub Table {'Tickets'}
3228 Returns the current value of id.
3229 (In the database, id is stored as int(11).)
3237 Returns the current value of EffectiveId.
3238 (In the database, EffectiveId is stored as int(11).)
3242 =head2 SetEffectiveId VALUE
3245 Set EffectiveId to VALUE.
3246 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
3247 (In the database, EffectiveId will be stored as a int(11).)
3255 Returns the current value of Queue.
3256 (In the database, Queue is stored as int(11).)
3260 =head2 SetQueue VALUE
3264 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
3265 (In the database, Queue will be stored as a int(11).)
3273 Returns the current value of Type.
3274 (In the database, Type is stored as varchar(16).)
3278 =head2 SetType VALUE
3282 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
3283 (In the database, Type will be stored as a varchar(16).)
3289 =head2 IssueStatement
3291 Returns the current value of IssueStatement.
3292 (In the database, IssueStatement is stored as int(11).)
3296 =head2 SetIssueStatement VALUE
3299 Set IssueStatement to VALUE.
3300 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
3301 (In the database, IssueStatement will be stored as a int(11).)
3309 Returns the current value of Resolution.
3310 (In the database, Resolution is stored as int(11).)
3314 =head2 SetResolution VALUE
3317 Set Resolution to VALUE.
3318 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
3319 (In the database, Resolution will be stored as a int(11).)
3327 Returns the current value of Owner.
3328 (In the database, Owner is stored as int(11).)
3332 =head2 SetOwner VALUE
3336 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
3337 (In the database, Owner will be stored as a int(11).)
3345 Returns the current value of Subject.
3346 (In the database, Subject is stored as varchar(200).)
3350 =head2 SetSubject VALUE
3353 Set Subject to VALUE.
3354 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
3355 (In the database, Subject will be stored as a varchar(200).)
3361 =head2 InitialPriority
3363 Returns the current value of InitialPriority.
3364 (In the database, InitialPriority is stored as int(11).)
3368 =head2 SetInitialPriority VALUE
3371 Set InitialPriority to VALUE.
3372 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
3373 (In the database, InitialPriority will be stored as a int(11).)
3379 =head2 FinalPriority
3381 Returns the current value of FinalPriority.
3382 (In the database, FinalPriority is stored as int(11).)
3386 =head2 SetFinalPriority VALUE
3389 Set FinalPriority to VALUE.
3390 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
3391 (In the database, FinalPriority will be stored as a int(11).)
3399 Returns the current value of Priority.
3400 (In the database, Priority is stored as int(11).)
3404 =head2 SetPriority VALUE
3407 Set Priority to VALUE.
3408 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
3409 (In the database, Priority will be stored as a int(11).)
3415 =head2 TimeEstimated
3417 Returns the current value of TimeEstimated.
3418 (In the database, TimeEstimated is stored as int(11).)
3422 =head2 SetTimeEstimated VALUE
3425 Set TimeEstimated to VALUE.
3426 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
3427 (In the database, TimeEstimated will be stored as a int(11).)
3435 Returns the current value of TimeWorked.
3436 (In the database, TimeWorked is stored as int(11).)
3440 =head2 SetTimeWorked VALUE
3443 Set TimeWorked to VALUE.
3444 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
3445 (In the database, TimeWorked will be stored as a int(11).)
3453 Returns the current value of Status.
3454 (In the database, Status is stored as varchar(64).)
3458 =head2 SetStatus VALUE
3461 Set Status to VALUE.
3462 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
3463 (In the database, Status will be stored as a varchar(64).)
3471 Returns the current value of TimeLeft.
3472 (In the database, TimeLeft is stored as int(11).)
3476 =head2 SetTimeLeft VALUE
3479 Set TimeLeft to VALUE.
3480 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
3481 (In the database, TimeLeft will be stored as a int(11).)
3489 Returns the current value of Told.
3490 (In the database, Told is stored as datetime.)
3494 =head2 SetTold VALUE
3498 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
3499 (In the database, Told will be stored as a datetime.)
3507 Returns the current value of Starts.
3508 (In the database, Starts is stored as datetime.)
3512 =head2 SetStarts VALUE
3515 Set Starts to VALUE.
3516 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
3517 (In the database, Starts will be stored as a datetime.)
3525 Returns the current value of Started.
3526 (In the database, Started is stored as datetime.)
3530 =head2 SetStarted VALUE
3533 Set Started to VALUE.
3534 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
3535 (In the database, Started will be stored as a datetime.)
3543 Returns the current value of Due.
3544 (In the database, Due is stored as datetime.)
3552 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
3553 (In the database, Due will be stored as a datetime.)
3561 Returns the current value of Resolved.
3562 (In the database, Resolved is stored as datetime.)
3566 =head2 SetResolved VALUE
3569 Set Resolved to VALUE.
3570 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
3571 (In the database, Resolved will be stored as a datetime.)
3577 =head2 LastUpdatedBy
3579 Returns the current value of LastUpdatedBy.
3580 (In the database, LastUpdatedBy is stored as int(11).)
3588 Returns the current value of LastUpdated.
3589 (In the database, LastUpdated is stored as datetime.)
3597 Returns the current value of Creator.
3598 (In the database, Creator is stored as int(11).)
3606 Returns the current value of Created.
3607 (In the database, Created is stored as datetime.)
3615 Returns the current value of Disabled.
3616 (In the database, Disabled is stored as smallint(6).)
3620 =head2 SetDisabled VALUE
3623 Set Disabled to VALUE.
3624 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
3625 (In the database, Disabled will be stored as a smallint(6).)
3632 sub _CoreAccessible {
3636 {read => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => ''},
3638 {read => 1, write => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'},
3640 {read => 1, write => 1, sql_type => 5, length => 6, is_blob => 0, is_numeric => 1, type => 'smallint(6)', default => undef},
3642 {read => 1, write => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'},
3644 {read => 1, write => 1, sql_type => 12, length => 16, is_blob => 0, is_numeric => 0, type => 'varchar(16)', default => ''},
3646 {read => 1, write => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'},
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 => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'},
3652 {read => 1, write => 1, sql_type => 12, length => 200, is_blob => 0, is_numeric => 0, type => 'varchar(200)', default => '[no subject]'},
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 => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'},
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 => 12, length => 64, is_blob => 0, is_numeric => 0, type => 'varchar(64)', default => ''},
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 => 11, length => 0, is_blob => 0, is_numeric => 0, type => 'datetime', default => ''},
3670 {read => 1, write => 1, sql_type => 11, length => 0, is_blob => 0, is_numeric => 0, type => 'datetime', default => ''},
3672 {read => 1, write => 1, sql_type => 11, length => 0, is_blob => 0, is_numeric => 0, type => 'datetime', default => ''},
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, auto => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'},
3680 {read => 1, auto => 1, sql_type => 11, length => 0, is_blob => 0, is_numeric => 0, type => 'datetime', default => ''},
3682 {read => 1, auto => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'},
3684 {read => 1, auto => 1, sql_type => 11, length => 0, is_blob => 0, is_numeric => 0, type => 'datetime', default => ''},
3686 {read => 1, write => 1, sql_type => 5, length => 6, is_blob => 0, is_numeric => 1, type => 'smallint(6)', default => '0'},
3691 sub FindDependencies {
3693 my ($walker, $deps) = @_;
3695 $self->SUPER::FindDependencies($walker, $deps);
3698 my $links = RT::Links->new( $self->CurrentUser );
3700 SUBCLAUSE => "either",
3702 VALUE => $self->URI,
3703 ENTRYAGGREGATOR => 'OR'
3704 ) for qw/Base Target/;
3705 $deps->Add( in => $links );
3707 # Tickets which were merged in
3708 my $objs = RT::Tickets->new( $self->CurrentUser );
3709 $objs->Limit( FIELD => 'EffectiveId', VALUE => $self->Id );
3710 $objs->Limit( FIELD => 'id', OPERATOR => '!=', VALUE => $self->Id );
3711 $deps->Add( in => $objs );
3713 # Ticket role groups( Owner, Requestors, Cc, AdminCc )
3714 $objs = RT::Groups->new( $self->CurrentUser );
3715 $objs->Limit( FIELD => 'Domain', VALUE => 'RT::Ticket-Role', CASESENSITIVE => 0 );
3716 $objs->Limit( FIELD => 'Instance', VALUE => $self->Id );
3717 $deps->Add( in => $objs );
3720 $deps->Add( out => $self->QueueObj );
3723 $deps->Add( out => $self->OwnerObj );
3730 Dependencies => undef,
3733 my $deps = $args{'Dependencies'};
3736 # Tickets which were merged in
3737 my $objs = RT::Tickets->new( $self->CurrentUser );
3738 $objs->{'allow_deleted_search'} = 1;
3739 $objs->Limit( FIELD => 'EffectiveId', VALUE => $self->Id );
3740 $objs->Limit( FIELD => 'id', OPERATOR => '!=', VALUE => $self->Id );
3741 push( @$list, $objs );
3743 # Ticket role groups( Owner, Requestors, Cc, AdminCc )
3744 $objs = RT::Groups->new( $self->CurrentUser );
3745 $objs->Limit( FIELD => 'Domain', VALUE => 'RT::Ticket-Role', CASESENSITIVE => 0 );
3746 $objs->Limit( FIELD => 'Instance', VALUE => $self->Id );
3747 push( @$list, $objs );
3749 #TODO: Users, Queues if we wish export tool
3750 $deps->_PushDependencies(
3751 BaseObject => $self,
3752 Flags => RT::Shredder::Constants::DEPENDS_ON,
3753 TargetObjects => $list,
3754 Shredder => $args{'Shredder'}
3757 return $self->SUPER::__DependsOn( %args );
3763 my %store = $self->SUPER::Serialize(@_);
3765 my $obj = RT::Ticket->new( RT->SystemUser );
3766 $obj->Load( $store{EffectiveId} );
3767 $store{EffectiveId} = \($obj->UID);
3772 RT::Base->_ImportOverlays();