import rt 3.6.4
[freeside.git] / rt / lib / RT / Ticket_Overlay.pm
1 # BEGIN BPS TAGGED BLOCK {{{
2
3 # COPYRIGHT:
4 #  
5 # This software is Copyright (c) 1996-2007 Best Practical Solutions, LLC 
6 #                                          <jesse@bestpractical.com>
7
8 # (Except where explicitly superseded by other copyright notices)
9
10
11 # LICENSE:
12
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
16 # from www.gnu.org.
17
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.
22
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/copyleft/gpl.html.
28
29
30 # CONTRIBUTION SUBMISSION POLICY:
31
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.)
37
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.
46
47 # END BPS TAGGED BLOCK }}}
48 # {{{ Front Material 
49
50 =head1 SYNOPSIS
51
52   use RT::Ticket;
53   my $ticket = new RT::Ticket($CurrentUser);
54   $ticket->Load($ticket_id);
55
56 =head1 DESCRIPTION
57
58 This module lets you manipulate RT\'s ticket object.
59
60
61 =head1 METHODS
62
63 =begin testing
64
65 use_ok ( RT::Queue);
66 ok(my $testqueue = RT::Queue->new($RT::SystemUser));
67 ok($testqueue->Create( Name => 'ticket tests'));
68 ok($testqueue->Id != 0);
69 use_ok(RT::CustomField);
70 ok(my $testcf = RT::CustomField->new($RT::SystemUser));
71 my ($ret, $cmsg) = $testcf->Create( Name => 'selectmulti',
72                     Queue => $testqueue->id,
73                                Type => 'SelectMultiple');
74 ok($ret,"Created the custom field - ".$cmsg);
75 ($ret,$cmsg) = $testcf->AddValue ( Name => 'Value1',
76                         SortOrder => '1',
77                         Description => 'A testing value');
78
79 ok($ret, "Added a value - ".$cmsg);
80
81 ok($testcf->AddValue ( Name => 'Value2',
82                         SortOrder => '2',
83                         Description => 'Another testing value'));
84 ok($testcf->AddValue ( Name => 'Value3',
85                         SortOrder => '3',
86                         Description => 'Yet Another testing value'));
87                        
88 ok($testcf->Values->Count == 3);
89
90 use_ok(RT::Ticket);
91
92 my $u = RT::User->new($RT::SystemUser);
93 $u->Load("root");
94 ok ($u->Id, "Found the root user");
95 ok(my $t = RT::Ticket->new($RT::SystemUser));
96 ok(my ($id, $msg) = $t->Create( Queue => $testqueue->Id,
97                Subject => 'Testing',
98                Owner => $u->Id
99               ));
100 ok($id != 0);
101 ok ($t->OwnerObj->Id == $u->Id, "Root is the ticket owner");
102 ok(my ($cfv, $cfm) =$t->AddCustomFieldValue(Field => $testcf->Id,
103                            Value => 'Value1'));
104 ok($cfv != 0, "Custom field creation didn't return an error: $cfm");
105 ok($t->CustomFieldValues($testcf->Id)->Count == 1);
106 ok($t->CustomFieldValues($testcf->Id)->First &&
107     $t->CustomFieldValues($testcf->Id)->First->Content eq 'Value1');;
108
109 ok(my ($cfdv, $cfdm) = $t->DeleteCustomFieldValue(Field => $testcf->Id,
110                         Value => 'Value1'));
111 ok ($cfdv != 0, "Deleted a custom field value: $cfdm");
112 ok($t->CustomFieldValues($testcf->Id)->Count == 0);
113
114 ok(my $t2 = RT::Ticket->new($RT::SystemUser));
115 ok($t2->Load($id));
116 is($t2->Subject, 'Testing');
117 is($t2->QueueObj->Id, $testqueue->id);
118 ok($t2->OwnerObj->Id == $u->Id);
119
120 my $t3 = RT::Ticket->new($RT::SystemUser);
121 my ($id3, $msg3) = $t3->Create( Queue => $testqueue->Id,
122                                 Subject => 'Testing',
123                                 Owner => $u->Id);
124 my ($cfv1, $cfm1) = $t->AddCustomFieldValue(Field => $testcf->Id,
125  Value => 'Value1');
126 ok($cfv1 != 0, "Adding a custom field to ticket 1 is successful: $cfm");
127 my ($cfv2, $cfm2) = $t3->AddCustomFieldValue(Field => $testcf->Id,
128  Value => 'Value2');
129 ok($cfv2 != 0, "Adding a custom field to ticket 2 is successful: $cfm");
130 my ($cfv3, $cfm3) = $t->AddCustomFieldValue(Field => $testcf->Id,
131  Value => 'Value3');
132 ok($cfv3 != 0, "Adding a custom field to ticket 1 is successful: $cfm");
133 ok($t->CustomFieldValues($testcf->Id)->Count == 2,
134    "This ticket has 2 custom field values");
135 ok($t3->CustomFieldValues($testcf->Id)->Count == 1,
136    "This ticket has 1 custom field value");
137
138 =end testing
139
140 =cut
141
142
143 package RT::Ticket;
144
145 use strict;
146 no warnings qw(redefine);
147
148 use RT::Queue;
149 use RT::User;
150 use RT::Record;
151 use RT::Links;
152 use RT::Date;
153 use RT::CustomFields;
154 use RT::Tickets;
155 use RT::Transactions;
156 use RT::Reminders;
157 use RT::URI::fsck_com_rt;
158 use RT::URI;
159 use MIME::Entity;
160
161 =begin testing
162
163
164 ok(require RT::Ticket, "Loading the RT::Ticket library");
165
166 =end testing
167
168 =cut
169
170 # }}}
171
172 # {{{ LINKTYPEMAP
173 # A helper table for links mapping to make it easier
174 # to build and parse links between tickets
175
176 use vars '%LINKTYPEMAP';
177
178 %LINKTYPEMAP = (
179     MemberOf => { Type => 'MemberOf',
180                   Mode => 'Target', },
181     Parents => { Type => 'MemberOf',
182                  Mode => 'Target', },
183     Members => { Type => 'MemberOf',
184                  Mode => 'Base', },
185     Children => { Type => 'MemberOf',
186                   Mode => 'Base', },
187     HasMember => { Type => 'MemberOf',
188                    Mode => 'Base', },
189     RefersTo => { Type => 'RefersTo',
190                   Mode => 'Target', },
191     ReferredToBy => { Type => 'RefersTo',
192                       Mode => 'Base', },
193     DependsOn => { Type => 'DependsOn',
194                    Mode => 'Target', },
195     DependedOnBy => { Type => 'DependsOn',
196                       Mode => 'Base', },
197     MergedInto => { Type => 'MergedInto',
198                    Mode => 'Target', },
199
200 );
201
202 # }}}
203
204 # {{{ LINKDIRMAP
205 # A helper table for links mapping to make it easier
206 # to build and parse links between tickets
207
208 use vars '%LINKDIRMAP';
209
210 %LINKDIRMAP = (
211     MemberOf => { Base => 'MemberOf',
212                   Target => 'HasMember', },
213     RefersTo => { Base => 'RefersTo',
214                 Target => 'ReferredToBy', },
215     DependsOn => { Base => 'DependsOn',
216                    Target => 'DependedOnBy', },
217     MergedInto => { Base => 'MergedInto',
218                    Target => 'MergedInto', },
219
220 );
221
222 # }}}
223
224 sub LINKTYPEMAP   { return \%LINKTYPEMAP   }
225 sub LINKDIRMAP   { return \%LINKDIRMAP   }
226
227 # {{{ sub Load
228
229 =head2 Load
230
231 Takes a single argument. This can be a ticket id, ticket alias or 
232 local ticket uri.  If the ticket can't be loaded, returns undef.
233 Otherwise, returns the ticket id.
234
235 =cut
236
237 sub Load {
238     my $self = shift;
239     my $id   = shift;
240
241     #TODO modify this routine to look at EffectiveId and do the recursive load
242     # thing. be careful to cache all the interim tickets we try so we don't loop forever.
243
244
245     #If it's a local URI, turn it into a ticket id
246     if ( $RT::TicketBaseURI && $id =~ /^$RT::TicketBaseURI(\d+)$/ ) {
247         $id = $1;
248     }
249
250     #If it's a remote URI, we're going to punt for now
251     elsif ( $id =~ '://' ) {
252         return (undef);
253     }
254
255     #If we have an integer URI, load the ticket
256     if ( $id =~ /^\d+$/ ) {
257         my ($ticketid,$msg) = $self->LoadById($id);
258
259         unless ($self->Id) {
260             $RT::Logger->crit("$self tried to load a bogus ticket: $id\n");
261             return (undef);
262         }
263     }
264
265     #It's not a URI. It's not a numerical ticket ID. Punt!
266     else {
267         $RT::Logger->warning("Tried to load a bogus ticket id: '$id'");
268         return (undef);
269     }
270
271     #If we're merged, resolve the merge.
272     if ( ( $self->EffectiveId ) and ( $self->EffectiveId != $self->Id ) ) {
273         $RT::Logger->debug ("We found a merged ticket.". $self->id ."/".$self->EffectiveId);
274         return ( $self->Load( $self->EffectiveId ) );
275     }
276
277     #Ok. we're loaded. lets get outa here.
278     return ( $self->Id );
279
280 }
281
282 # }}}
283
284 # {{{ sub LoadByURI
285
286 =head2 LoadByURI
287
288 Given a local ticket URI, loads the specified ticket.
289
290 =cut
291
292 sub LoadByURI {
293     my $self = shift;
294     my $uri  = shift;
295
296     if ( $uri =~ /^$RT::TicketBaseURI(\d+)$/ ) {
297         my $id = $1;
298         return ( $self->Load($id) );
299     }
300     else {
301         return (undef);
302     }
303 }
304
305 # }}}
306
307 # {{{ sub Create
308
309 =head2 Create (ARGS)
310
311 Arguments: ARGS is a hash of named parameters.  Valid parameters are:
312
313   id 
314   Queue  - Either a Queue object or a Queue Name
315   Requestor -  A reference to a list of  email addresses or RT user Names
316   Cc  - A reference to a list of  email addresses or Names
317   AdminCc  - A reference to a  list of  email addresses or Names
318   Type -- The ticket\'s type. ignore this for now
319   Owner -- This ticket\'s owner. either an RT::User object or this user\'s id
320   Subject -- A string describing the subject of the ticket
321   Priority -- an integer from 0 to 99
322   InitialPriority -- an integer from 0 to 99
323   FinalPriority -- an integer from 0 to 99
324   Status -- any valid status (Defined in RT::Queue)
325   TimeEstimated -- an integer. estimated time for this task in minutes
326   TimeWorked -- an integer. time worked so far in minutes
327   TimeLeft -- an integer. time remaining in minutes
328   Starts -- an ISO date describing the ticket\'s start date and time in GMT
329   Due -- an ISO date describing the ticket\'s due date and time in GMT
330   MIMEObj -- a MIME::Entity object with the content of the initial ticket request.
331   CustomField-<n> -- a scalar or array of values for the customfield with the id <n>
332
333 Ticket links can be set up during create by passing the link type as a hask key and
334 the ticket id to be linked to as a value (or a URI when linking to other objects).
335 Multiple links of the same type can be created by passing an array ref. For example:
336
337   Parent => 45,
338   DependsOn => [ 15, 22 ],
339   RefersTo => 'http://www.bestpractical.com',
340
341 Supported link types are C<MemberOf>, C<HasMember>, C<RefersTo>, C<ReferredToBy>,
342 C<DependsOn> and C<DependedOnBy>. Also, C<Parents> is alias for C<MemberOf> and
343 C<Members> and C<Children> are aliases for C<HasMember>.
344
345 Returns: TICKETID, Transaction Object, Error Message
346
347 =begin testing
348
349 my $t = RT::Ticket->new($RT::SystemUser);
350
351 ok( $t->Create(Queue => 'General', Due => '2002-05-21 00:00:00', ReferredToBy => 'http://www.cpan.org', RefersTo => 'http://fsck.com', Subject => 'This is a subject'), "Ticket Created");
352
353 ok ( my $id = $t->Id, "Got ticket id");
354 ok ($t->RefersTo->First->Target =~ /fsck.com/, "Got refers to");
355 ok ($t->ReferredToBy->First->Base =~ /cpan.org/, "Got referredtoby");
356 ok ($t->ResolvedObj->Unix == -1, "It hasn't been resolved - ". $t->ResolvedObj->Unix);
357
358 =end testing
359
360 =cut
361
362 sub Create {
363     my $self = shift;
364
365     my %args = (
366         id                 => undef,
367         EffectiveId        => undef,
368         Queue              => undef,
369         Requestor          => undef,
370         Cc                 => undef,
371         AdminCc            => undef,
372         Type               => 'ticket',
373         Owner              => undef,
374         Subject            => '',
375         InitialPriority    => undef,
376         FinalPriority      => undef,
377         Priority           => undef,
378         Status             => 'new',
379         TimeWorked         => "0",
380         TimeLeft           => 0,
381         TimeEstimated      => 0,
382         Due                => undef,
383         Starts             => undef,
384         Started            => undef,
385         Resolved           => undef,
386         MIMEObj            => undef,
387         _RecordTransaction => 1,
388         @_
389     );
390
391     my ( $ErrStr, $Owner, $resolved );
392     my (@non_fatal_errors);
393
394     my $QueueObj = RT::Queue->new($RT::SystemUser);
395
396     if ( ( defined( $args{'Queue'} ) ) && ( !ref( $args{'Queue'} ) ) ) {
397         $QueueObj->Load( $args{'Queue'} );
398     }
399     elsif ( ref( $args{'Queue'} ) eq 'RT::Queue' ) {
400         $QueueObj->Load( $args{'Queue'}->Id );
401     }
402     else {
403         $RT::Logger->debug( $args{'Queue'} . " not a recognised queue object." );
404     }
405
406     #Can't create a ticket without a queue.
407     unless ( defined($QueueObj) && $QueueObj->Id ) {
408         $RT::Logger->debug("$self No queue given for ticket creation.");
409         return ( 0, 0, $self->loc('Could not create ticket. Queue not set') );
410     }
411
412     #Now that we have a queue, Check the ACLS
413     unless (
414         $self->CurrentUser->HasRight(
415             Right  => 'CreateTicket',
416             Object => $QueueObj
417         )
418       )
419     {
420         return (
421             0, 0,
422             $self->loc( "No permission to create tickets in the queue '[_1]'", $QueueObj->Name));
423     }
424
425     unless ( $QueueObj->IsValidStatus( $args{'Status'} ) ) {
426         return ( 0, 0, $self->loc('Invalid value for status') );
427     }
428
429     #Since we have a queue, we can set queue defaults
430     #Initial Priority
431
432     # If there's no queue default initial priority and it's not set, set it to 0
433     $args{'InitialPriority'} = ( $QueueObj->InitialPriority || 0 )
434       unless ( $args{'InitialPriority'} );
435
436     #Final priority
437
438     # If there's no queue default final priority and it's not set, set it to 0
439     $args{'FinalPriority'} = ( $QueueObj->FinalPriority || 0 )
440       unless ( $args{'FinalPriority'} );
441
442     # Priority may have changed from InitialPriority, for the case
443     # where we're importing tickets (eg, from an older RT version.)
444     my $priority = $args{'Priority'} || $args{'InitialPriority'};
445
446     # {{{ Dates
447     #TODO we should see what sort of due date we're getting, rather +
448     # than assuming it's in ISO format.
449
450     #Set the due date. if we didn't get fed one, use the queue default due in
451     my $Due = new RT::Date( $self->CurrentUser );
452
453     if ( $args{'Due'} ) {
454         $Due->Set( Format => 'ISO', Value => $args{'Due'} );
455     }
456     elsif ( my $due_in = $QueueObj->DefaultDueIn ) {
457         $Due->SetToNow;
458         $Due->AddDays( $due_in );
459     }
460
461     my $Starts = new RT::Date( $self->CurrentUser );
462     if ( defined $args{'Starts'} ) {
463         $Starts->Set( Format => 'ISO', Value => $args{'Starts'} );
464     }
465
466     my $Started = new RT::Date( $self->CurrentUser );
467     if ( defined $args{'Started'} ) {
468         $Started->Set( Format => 'ISO', Value => $args{'Started'} );
469     }
470
471     my $Resolved = new RT::Date( $self->CurrentUser );
472     if ( defined $args{'Resolved'} ) {
473         $Resolved->Set( Format => 'ISO', Value => $args{'Resolved'} );
474     }
475
476     #If the status is an inactive status, set the resolved date
477     if ( $QueueObj->IsInactiveStatus( $args{'Status'} ) && !$args{'Resolved'} )
478     {
479         $RT::Logger->debug( "Got a ". $args{'Status'}
480             ." ticket with undefined resolved date. Setting to now."
481         );
482         $Resolved->SetToNow;
483     }
484
485     # }}}
486
487     # {{{ Dealing with time fields
488
489     $args{'TimeEstimated'} = 0 unless defined $args{'TimeEstimated'};
490     $args{'TimeWorked'}    = 0 unless defined $args{'TimeWorked'};
491     $args{'TimeLeft'}      = 0 unless defined $args{'TimeLeft'};
492
493     # }}}
494
495     # {{{ Deal with setting the owner
496
497     if ( ref( $args{'Owner'} ) eq 'RT::User' ) {
498         $Owner = $args{'Owner'};
499     }
500
501     #If we've been handed something else, try to load the user.
502     elsif ( $args{'Owner'} ) {
503         $Owner = RT::User->new( $self->CurrentUser );
504         $Owner->Load( $args{'Owner'} );
505
506         push( @non_fatal_errors,
507                 $self->loc("Owner could not be set.") . " "
508               . $self->loc( "User '[_1]' could not be found.", $args{'Owner'} )
509           )
510           unless ( $Owner->Id );
511     }
512
513     #If we have a proposed owner and they don't have the right
514     #to own a ticket, scream about it and make them not the owner
515     if (
516             ( defined($Owner) )
517         and ( $Owner->Id )
518         and ( $Owner->Id != $RT::Nobody->Id )
519         and (
520             !$Owner->HasRight(
521                 Object => $QueueObj,
522                 Right  => 'OwnTicket'
523             )
524         )
525       )
526     {
527
528         $RT::Logger->warning( "User "
529               . $Owner->Name . "("
530               . $Owner->id
531               . ") was proposed "
532               . "as a ticket owner but has no rights to own "
533               . "tickets in "
534               . $QueueObj->Name );
535
536         push @non_fatal_errors,
537           $self->loc( "Owner '[_1]' does not have rights to own this ticket.",
538             $Owner->Name
539           );
540
541         $Owner = undef;
542     }
543
544     #If we haven't been handed a valid owner, make it nobody.
545     unless ( defined($Owner) && $Owner->Id ) {
546         $Owner = new RT::User( $self->CurrentUser );
547         $Owner->Load( $RT::Nobody->Id );
548     }
549
550     # }}}
551
552 # We attempt to load or create each of the people who might have a role for this ticket
553 # _outside_ the transaction, so we don't get into ticket creation races
554     foreach my $type ( "Cc", "AdminCc", "Requestor" ) {
555         next unless ( defined $args{$type} );
556         foreach my $watcher (
557             ref( $args{$type} ) ? @{ $args{$type} } : ( $args{$type} ) )
558         {
559             my $user = RT::User->new($RT::SystemUser);
560             $user->LoadOrCreateByEmail($watcher)
561               if ( $watcher && $watcher !~ /^\d+$/ );
562         }
563     }
564
565     $RT::Handle->BeginTransaction();
566
567     my %params = (
568         Queue           => $QueueObj->Id,
569         Owner           => $Owner->Id,
570         Subject         => $args{'Subject'},
571         InitialPriority => $args{'InitialPriority'},
572         FinalPriority   => $args{'FinalPriority'},
573         Priority        => $priority,
574         Status          => $args{'Status'},
575         TimeWorked      => $args{'TimeWorked'},
576         TimeEstimated   => $args{'TimeEstimated'},
577         TimeLeft        => $args{'TimeLeft'},
578         Type            => $args{'Type'},
579         Starts          => $Starts->ISO,
580         Started         => $Started->ISO,
581         Resolved        => $Resolved->ISO,
582         Due             => $Due->ISO
583     );
584
585 # Parameters passed in during an import that we probably don't want to touch, otherwise
586     foreach my $attr qw(id Creator Created LastUpdated LastUpdatedBy) {
587         $params{$attr} = $args{$attr} if ( $args{$attr} );
588     }
589
590     # Delete null integer parameters
591     foreach my $attr
592       qw(TimeWorked TimeLeft TimeEstimated InitialPriority FinalPriority) {
593         delete $params{$attr}
594           unless ( exists $params{$attr} && $params{$attr} );
595     }
596
597     # Delete the time worked if we're counting it in the transaction
598     delete $params{TimeWorked} if $args{'_RecordTransaction'};
599     
600     my ($id,$ticket_message) = $self->SUPER::Create( %params);
601     unless ($id) {
602         $RT::Logger->crit( "Couldn't create a ticket: " . $ticket_message );
603         $RT::Handle->Rollback();
604         return ( 0, 0,
605             $self->loc("Ticket could not be created due to an internal error")
606         );
607     }
608
609     #Set the ticket's effective ID now that we've created it.
610     my ( $val, $msg ) = $self->__Set(
611         Field => 'EffectiveId',
612         Value => ( $args{'EffectiveId'} || $id )
613     );
614
615     unless ($val) {
616         $RT::Logger->crit("$self ->Create couldn't set EffectiveId: $msg\n");
617         $RT::Handle->Rollback();
618         return ( 0, 0,
619             $self->loc("Ticket could not be created due to an internal error")
620         );
621     }
622
623     my $create_groups_ret = $self->_CreateTicketGroups();
624     unless ($create_groups_ret) {
625         $RT::Logger->crit( "Couldn't create ticket groups for ticket "
626               . $self->Id
627               . ". aborting Ticket creation." );
628         $RT::Handle->Rollback();
629         return ( 0, 0,
630             $self->loc("Ticket could not be created due to an internal error")
631         );
632     }
633
634 # Set the owner in the Groups table
635 # We denormalize it into the Ticket table too because doing otherwise would
636 # kill performance, bigtime. It gets kept in lockstep thanks to the magic of transactionalization
637
638     $self->OwnerGroup->_AddMember(
639         PrincipalId       => $Owner->PrincipalId,
640         InsideTransaction => 1
641     );
642
643     # {{{ Deal with setting up watchers
644
645     foreach my $type ( "Cc", "AdminCc", "Requestor" ) {
646         next unless ( defined $args{$type} );
647         foreach my $watcher (
648             ref( $args{$type} ) ? @{ $args{$type} } : ( $args{$type} ) )
649         {
650
651             # If there is an empty entry in the list, let's get out of here.
652             next unless $watcher;
653
654             # we reason that all-digits number must be a principal id, not email
655             # this is the only way to can add
656             my $field = 'Email';
657             $field = 'PrincipalId' if $watcher =~ /^\d+$/;
658
659             my ( $wval, $wmsg );
660
661             if ( $type eq 'AdminCc' ) {
662
663         # Note that we're using AddWatcher, rather than _AddWatcher, as we
664         # actually _want_ that ACL check. Otherwise, random ticket creators
665         # could make themselves adminccs and maybe get ticket rights. that would
666         # be poor
667                 ( $wval, $wmsg ) = $self->AddWatcher(
668                     Type   => $type,
669                     $field => $watcher,
670                     Silent => 1
671                 );
672             }
673             else {
674                 ( $wval, $wmsg ) = $self->_AddWatcher(
675                     Type   => $type,
676                     $field => $watcher,
677                     Silent => 1
678                 );
679             }
680
681             push @non_fatal_errors, $wmsg unless ($wval);
682         }
683     }
684
685     # }}}
686     # {{{ Deal with setting up links
687
688     foreach my $type ( keys %LINKTYPEMAP ) {
689         next unless ( defined $args{$type} );
690         foreach my $link (
691             ref( $args{$type} ) ? @{ $args{$type} } : ( $args{$type} ) )
692         {
693             # Check rights on the other end of the link if we must
694             # then run _AddLink that doesn't check for ACLs
695             if ( $RT::StrictLinkACL ) {
696                 my ($val, $msg, $obj) = $self->__GetTicketFromURI( URI => $link );
697                 unless ( $val ) {
698                     push @non_fatal_errors, $msg;
699                     next;
700                 }
701                 if ( $obj && !$obj->CurrentUserHasRight('ModifyTicket') ) {
702                     push @non_fatal_errors, $self->loc('Linking. Permission denied');
703                     next;
704                 }
705             }
706             
707             my ( $wval, $wmsg ) = $self->_AddLink(
708                 Type                          => $LINKTYPEMAP{$type}->{'Type'},
709                 $LINKTYPEMAP{$type}->{'Mode'} => $link,
710                 Silent                        => 1
711             );
712
713             push @non_fatal_errors, $wmsg unless ($wval);
714         }
715     }
716
717     # }}}
718
719     # {{{ Add all the custom fields
720
721     foreach my $arg ( keys %args ) {
722         next unless ( $arg =~ /^CustomField-(\d+)$/i );
723         my $cfid = $1;
724         foreach
725           my $value ( UNIVERSAL::isa( $args{$arg} => 'ARRAY' ) ? @{ $args{$arg} } : ( $args{$arg} ) )
726         {
727             next unless ( length($value) );
728
729             # Allow passing in uploaded LargeContent etc by hash reference
730             $self->_AddCustomFieldValue(
731                 (UNIVERSAL::isa( $value => 'HASH' )
732                     ? %$value
733                     : (Value => $value)
734                 ),
735                 Field             => $cfid,
736                 RecordTransaction => 0,
737             );
738         }
739     }
740
741     # }}}
742
743     if ( $args{'_RecordTransaction'} ) {
744
745         # {{{ Add a transaction for the create
746         my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction(
747                                                      Type      => "Create",
748                                                      TimeTaken => $args{'TimeWorked'},
749                                                      MIMEObj => $args{'MIMEObj'}
750         );
751
752         if ( $self->Id && $Trans ) {
753
754             $TransObj->UpdateCustomFields(ARGSRef => \%args);
755
756             $RT::Logger->info( "Ticket " . $self->Id . " created in queue '" . $QueueObj->Name . "' by " . $self->CurrentUser->Name );
757             $ErrStr = $self->loc( "Ticket [_1] created in queue '[_2]'", $self->Id, $QueueObj->Name );
758             $ErrStr = join( "\n", $ErrStr, @non_fatal_errors );
759         }
760         else {
761             $RT::Handle->Rollback();
762
763             $ErrStr = join( "\n", $ErrStr, @non_fatal_errors );
764             $RT::Logger->error("Ticket couldn't be created: $ErrStr");
765             return ( 0, 0, $self->loc( "Ticket could not be created due to an internal error"));
766         }
767
768         $RT::Handle->Commit();
769         return ( $self->Id, $TransObj->Id, $ErrStr );
770
771         # }}}
772     }
773     else {
774
775         # Not going to record a transaction
776         $RT::Handle->Commit();
777         $ErrStr = $self->loc( "Ticket [_1] created in queue '[_2]'", $self->Id, $QueueObj->Name );
778         $ErrStr = join( "\n", $ErrStr, @non_fatal_errors );
779         return ( $self->Id, 0, $ErrStr );
780
781     }
782 }
783
784
785 # }}}
786
787
788 # {{{ UpdateFrom822 
789
790 =head2 UpdateFrom822 $MESSAGE
791
792 Takes an RFC822 format message as a string and uses it to make a bunch of changes to a ticket.
793 Returns an um. ask me again when the code exists
794
795
796 =begin testing
797
798 my $simple_update = <<EOF;
799 Subject: target
800 AddRequestor: jesse\@example.com
801 EOF
802
803 my $ticket = RT::Ticket->new($RT::SystemUser);
804 my ($id,$msg) =$ticket->Create(Subject => 'first', Queue => 'general');
805 ok($ticket->Id, "Created the test ticket - ".$id ." - ".$msg);
806 $ticket->UpdateFrom822($simple_update);
807 is($ticket->Subject, 'target', "changed the subject");
808 my $jesse = RT::User->new($RT::SystemUser);
809 $jesse->LoadByEmail('jesse@example.com');
810 ok ($jesse->Id, "There's a user for jesse");
811 ok($ticket->Requestors->HasMember( $jesse->PrincipalObj), "It has the jesse principal object as a requestor ");
812
813 =end testing
814
815
816 =cut
817
818 sub UpdateFrom822 {
819         my $self = shift;
820         my $content = shift;
821         my %args = $self->_Parse822HeadersForAttributes($content);
822
823         
824     my %ticketargs = (
825         Queue           => $args{'queue'},
826         Subject         => $args{'subject'},
827         Status          => $args{'status'},
828         Due             => $args{'due'},
829         Starts          => $args{'starts'},
830         Started         => $args{'started'},
831         Resolved        => $args{'resolved'},
832         Owner           => $args{'owner'},
833         Requestor       => $args{'requestor'},
834         Cc              => $args{'cc'},
835         AdminCc         => $args{'admincc'},
836         TimeWorked      => $args{'timeworked'},
837         TimeEstimated   => $args{'timeestimated'},
838         TimeLeft        => $args{'timeleft'},
839         InitialPriority => $args{'initialpriority'},
840         Priority => $args{'priority'},
841         FinalPriority   => $args{'finalpriority'},
842         Type            => $args{'type'},
843         DependsOn       => $args{'dependson'},
844         DependedOnBy    => $args{'dependedonby'},
845         RefersTo        => $args{'refersto'},
846         ReferredToBy    => $args{'referredtoby'},
847         Members         => $args{'members'},
848         MemberOf        => $args{'memberof'},
849         MIMEObj         => $args{'mimeobj'}
850     );
851
852     foreach my $type qw(Requestor Cc Admincc) {
853
854         foreach my $action ( 'Add', 'Del', '' ) {
855
856             my $lctag = lc($action) . lc($type);
857             foreach my $list ( $args{$lctag}, $args{ $lctag . 's' } ) {
858
859                 foreach my $entry ( ref($list) ? @{$list} : ($list) ) {
860                     push @{$ticketargs{ $action . $type }} , split ( /\s*,\s*/, $entry );
861                 }
862
863             }
864
865             # Todo: if we're given an explicit list, transmute it into a list of adds/deletes
866
867         }
868     }
869
870     # Add custom field entries to %ticketargs.
871     # TODO: allow named custom fields
872     map {
873         /^customfield-(\d+)$/
874           && ( $ticketargs{ "CustomField-" . $1 } = $args{$_} );
875     } keys(%args);
876
877 # for each ticket we've been told to update, iterate through the set of
878 # rfc822 headers and perform that update to the ticket.
879
880
881     # {{{ Set basic fields 
882     my @attribs = qw(
883       Subject
884       FinalPriority
885       Priority
886       TimeEstimated
887       TimeWorked
888       TimeLeft
889       Status
890       Queue
891       Type
892     );
893
894
895     # Resolve the queue from a name to a numeric id.
896     if ( $ticketargs{'Queue'} and ( $ticketargs{'Queue'} !~ /^(\d+)$/ ) ) {
897         my $tempqueue = RT::Queue->new($RT::SystemUser);
898         $tempqueue->Load( $ticketargs{'Queue'} );
899         $ticketargs{'Queue'} = $tempqueue->Id() if ( $tempqueue->id );
900     }
901
902     my @results;
903
904     foreach my $attribute (@attribs) {
905         my $value = $ticketargs{$attribute};
906
907         if ( $value ne $self->$attribute() ) {
908
909             my $method = "Set$attribute";
910             my ( $code, $msg ) = $self->$method($value);
911
912             push @results, $self->loc($attribute) . ': ' . $msg;
913
914         }
915     }
916
917     # We special case owner changing, so we can use ForceOwnerChange
918     if ( $ticketargs{'Owner'} && ( $self->Owner != $ticketargs{'Owner'} ) ) {
919         my $ChownType = "Give";
920         $ChownType = "Force" if ( $ticketargs{'ForceOwnerChange'} );
921
922         my ( $val, $msg ) = $self->SetOwner( $ticketargs{'Owner'}, $ChownType );
923         push ( @results, $msg );
924     }
925
926     # }}}
927 # Deal with setting watchers
928
929
930 # Acceptable arguments:
931 #  Requestor
932 #  Requestors
933 #  AddRequestor
934 #  AddRequestors
935 #  DelRequestor
936  
937  foreach my $type qw(Requestor Cc AdminCc) {
938
939         # If we've been given a number of delresses to del, do it.
940                 foreach my $address (@{$ticketargs{'Del'.$type}}) {
941                 my ($id, $msg) = $self->DeleteWatcher( Type => $type, Email => $address);
942                 push (@results, $msg) ;
943                 }
944
945         # If we've been given a number of addresses to add, do it.
946                 foreach my $address (@{$ticketargs{'Add'.$type}}) {
947                 $RT::Logger->debug("Adding $address as a $type");
948                 my ($id, $msg) = $self->AddWatcher( Type => $type, Email => $address);
949                 push (@results, $msg) ;
950
951         }
952
953
954 }
955
956
957 }
958 # }}}
959
960 # {{{ _Parse822HeadersForAttributes Content
961
962 =head2 _Parse822HeadersForAttributes Content
963
964 Takes an RFC822 style message and parses its attributes into a hash.
965
966 =cut
967
968 sub _Parse822HeadersForAttributes {
969     my $self    = shift;
970     my $content = shift;
971     my %args;
972
973     my @lines = ( split ( /\n/, $content ) );
974     while ( defined( my $line = shift @lines ) ) {
975         if ( $line =~ /^(.*?):(?:\s+(.*))?$/ ) {
976             my $value = $2;
977             my $tag   = lc($1);
978
979             $tag =~ s/-//g;
980             if ( defined( $args{$tag} ) )
981             {    #if we're about to get a second value, make it an array
982                 $args{$tag} = [ $args{$tag} ];
983             }
984             if ( ref( $args{$tag} ) )
985             {    #If it's an array, we want to push the value
986                 push @{ $args{$tag} }, $value;
987             }
988             else {    #if there's nothing there, just set the value
989                 $args{$tag} = $value;
990             }
991         } elsif ($line =~ /^$/) {
992
993             #TODO: this won't work, since "" isn't of the form "foo:value"
994
995                 while ( defined( my $l = shift @lines ) ) {
996                     push @{ $args{'content'} }, $l;
997                 }
998             }
999         
1000     }
1001
1002     foreach my $date qw(due starts started resolved) {
1003         my $dateobj = RT::Date->new($RT::SystemUser);
1004         if ( $args{$date} =~ /^\d+$/ ) {
1005             $dateobj->Set( Format => 'unix', Value => $args{$date} );
1006         }
1007         else {
1008             $dateobj->Set( Format => 'unknown', Value => $args{$date} );
1009         }
1010         $args{$date} = $dateobj->ISO;
1011     }
1012     $args{'mimeobj'} = MIME::Entity->new();
1013     $args{'mimeobj'}->build(
1014         Type => ( $args{'contenttype'} || 'text/plain' ),
1015         Data => ($args{'content'} || '')
1016     );
1017
1018     return (%args);
1019 }
1020
1021 # }}}
1022
1023 # {{{ sub Import
1024
1025 =head2 Import PARAMHASH
1026
1027 Import a ticket. 
1028 Doesn\'t create a transaction. 
1029 Doesn\'t supply queue defaults, etc.
1030
1031 Returns: TICKETID
1032
1033 =cut
1034
1035 sub Import {
1036     my $self = shift;
1037     my ( $ErrStr, $QueueObj, $Owner );
1038
1039     my %args = (
1040         id              => undef,
1041         EffectiveId     => undef,
1042         Queue           => undef,
1043         Requestor       => undef,
1044         Type            => 'ticket',
1045         Owner           => $RT::Nobody->Id,
1046         Subject         => '[no subject]',
1047         InitialPriority => undef,
1048         FinalPriority   => undef,
1049         Status          => 'new',
1050         TimeWorked      => "0",
1051         Due             => undef,
1052         Created         => undef,
1053         Updated         => undef,
1054         Resolved        => undef,
1055         Told            => undef,
1056         @_
1057     );
1058
1059     if ( ( defined( $args{'Queue'} ) ) && ( !ref( $args{'Queue'} ) ) ) {
1060         $QueueObj = RT::Queue->new($RT::SystemUser);
1061         $QueueObj->Load( $args{'Queue'} );
1062
1063         #TODO error check this and return 0 if it\'s not loading properly +++
1064     }
1065     elsif ( ref( $args{'Queue'} ) eq 'RT::Queue' ) {
1066         $QueueObj = RT::Queue->new($RT::SystemUser);
1067         $QueueObj->Load( $args{'Queue'}->Id );
1068     }
1069     else {
1070         $RT::Logger->debug(
1071             "$self " . $args{'Queue'} . " not a recognised queue object." );
1072     }
1073
1074     #Can't create a ticket without a queue.
1075     unless ( defined($QueueObj) and $QueueObj->Id ) {
1076         $RT::Logger->debug("$self No queue given for ticket creation.");
1077         return ( 0, $self->loc('Could not create ticket. Queue not set') );
1078     }
1079
1080     #Now that we have a queue, Check the ACLS
1081     unless (
1082         $self->CurrentUser->HasRight(
1083             Right    => 'CreateTicket',
1084             Object => $QueueObj
1085         )
1086       )
1087     {
1088         return ( 0,
1089             $self->loc("No permission to create tickets in the queue '[_1]'"
1090               , $QueueObj->Name));
1091     }
1092
1093     # {{{ Deal with setting the owner
1094
1095     # Attempt to take user object, user name or user id.
1096     # Assign to nobody if lookup fails.
1097     if ( defined( $args{'Owner'} ) ) {
1098         if ( ref( $args{'Owner'} ) ) {
1099             $Owner = $args{'Owner'};
1100         }
1101         else {
1102             $Owner = new RT::User( $self->CurrentUser );
1103             $Owner->Load( $args{'Owner'} );
1104             if ( !defined( $Owner->id ) ) {
1105                 $Owner->Load( $RT::Nobody->id );
1106             }
1107         }
1108     }
1109
1110     #If we have a proposed owner and they don't have the right 
1111     #to own a ticket, scream about it and make them not the owner
1112     if (
1113         ( defined($Owner) )
1114         and ( $Owner->Id != $RT::Nobody->Id )
1115         and (
1116             !$Owner->HasRight(
1117                 Object => $QueueObj,
1118                 Right    => 'OwnTicket'
1119             )
1120         )
1121       )
1122     {
1123
1124         $RT::Logger->warning( "$self user "
1125               . $Owner->Name . "("
1126               . $Owner->id
1127               . ") was proposed "
1128               . "as a ticket owner but has no rights to own "
1129               . "tickets in '"
1130               . $QueueObj->Name . "'\n" );
1131
1132         $Owner = undef;
1133     }
1134
1135     #If we haven't been handed a valid owner, make it nobody.
1136     unless ( defined($Owner) ) {
1137         $Owner = new RT::User( $self->CurrentUser );
1138         $Owner->Load( $RT::Nobody->UserObj->Id );
1139     }
1140
1141     # }}}
1142
1143     unless ( $self->ValidateStatus( $args{'Status'} ) ) {
1144         return ( 0, $self->loc("'[_1]' is an invalid value for status", $args{'Status'}) );
1145     }
1146
1147     $self->{'_AccessibleCache'}{Created}       = { 'read' => 1, 'write' => 1 };
1148     $self->{'_AccessibleCache'}{Creator}       = { 'read' => 1, 'auto'  => 1 };
1149     $self->{'_AccessibleCache'}{LastUpdated}   = { 'read' => 1, 'write' => 1 };
1150     $self->{'_AccessibleCache'}{LastUpdatedBy} = { 'read' => 1, 'auto'  => 1 };
1151
1152     # If we're coming in with an id, set that now.
1153     my $EffectiveId = undef;
1154     if ( $args{'id'} ) {
1155         $EffectiveId = $args{'id'};
1156
1157     }
1158
1159     my $id = $self->SUPER::Create(
1160         id              => $args{'id'},
1161         EffectiveId     => $EffectiveId,
1162         Queue           => $QueueObj->Id,
1163         Owner           => $Owner->Id,
1164         Subject         => $args{'Subject'},            # loc
1165         InitialPriority => $args{'InitialPriority'},    # loc
1166         FinalPriority   => $args{'FinalPriority'},      # loc
1167         Priority        => $args{'InitialPriority'},    # loc
1168         Status          => $args{'Status'},             # loc
1169         TimeWorked      => $args{'TimeWorked'},         # loc
1170         Type            => $args{'Type'},               # loc
1171         Created         => $args{'Created'},            # loc
1172         Told            => $args{'Told'},               # loc
1173         LastUpdated     => $args{'Updated'},            # loc
1174         Resolved        => $args{'Resolved'},           # loc
1175         Due             => $args{'Due'},                # loc
1176     );
1177
1178     # If the ticket didn't have an id
1179     # Set the ticket's effective ID now that we've created it.
1180     if ( $args{'id'} ) {
1181         $self->Load( $args{'id'} );
1182     }
1183     else {
1184         my ( $val, $msg ) =
1185           $self->__Set( Field => 'EffectiveId', Value => $id );
1186
1187         unless ($val) {
1188             $RT::Logger->err(
1189                 $self . "->Import couldn't set EffectiveId: $msg\n" );
1190         }
1191     }
1192
1193     my $create_groups_ret = $self->_CreateTicketGroups();
1194     unless ($create_groups_ret) {
1195         $RT::Logger->crit(
1196             "Couldn't create ticket groups for ticket " . $self->Id );
1197     }
1198
1199     $self->OwnerGroup->_AddMember( PrincipalId => $Owner->PrincipalId );
1200
1201     my $watcher;
1202     foreach $watcher ( @{ $args{'Cc'} } ) {
1203         $self->_AddWatcher( Type => 'Cc', Email => $watcher, Silent => 1 );
1204     }
1205     foreach $watcher ( @{ $args{'AdminCc'} } ) {
1206         $self->_AddWatcher( Type => 'AdminCc', Email => $watcher,
1207             Silent => 1 );
1208     }
1209     foreach $watcher ( @{ $args{'Requestor'} } ) {
1210         $self->_AddWatcher( Type => 'Requestor', Email => $watcher,
1211             Silent => 1 );
1212     }
1213
1214     return ( $self->Id, $ErrStr );
1215 }
1216
1217 # }}}
1218
1219 # {{{ Routines dealing with watchers.
1220
1221 # {{{ _CreateTicketGroups 
1222
1223 =head2 _CreateTicketGroups
1224
1225 Create the ticket groups and links for this ticket. 
1226 This routine expects to be called from Ticket->Create _inside of a transaction_
1227
1228 It will create four groups for this ticket: Requestor, Cc, AdminCc and Owner.
1229
1230 It will return true on success and undef on failure.
1231
1232 =begin testing
1233
1234 my $ticket = RT::Ticket->new($RT::SystemUser);
1235 my ($id, $msg) = $ticket->Create(Subject => "Foo",
1236                 Owner => $RT::SystemUser->Id,
1237                 Status => 'open',
1238                 Requestor => ['jesse@example.com'],
1239                 Queue => '1'
1240                 );
1241 ok ($id, "Ticket $id was created");
1242 ok(my $group = RT::Group->new($RT::SystemUser));
1243 ok($group->LoadTicketRoleGroup(Ticket => $id, Type=> 'Requestor'));
1244 ok ($group->Id, "Found the requestors object for this ticket");
1245
1246 ok(my $jesse = RT::User->new($RT::SystemUser), "Creating a jesse rt::user");
1247 $jesse->LoadByEmail('jesse@example.com');
1248 ok($jesse->Id,  "Found the jesse rt user");
1249
1250
1251 ok ($ticket->IsWatcher(Type => 'Requestor', PrincipalId => $jesse->PrincipalId), "The ticket actually has jesse at fsck.com as a requestor");
1252 ok ((my $add_id, $add_msg) = $ticket->AddWatcher(Type => 'Requestor', Email => 'bob@fsck.com'), "Added bob at fsck.com as a requestor");
1253 ok ($add_id, "Add succeeded: ($add_msg)");
1254 ok(my $bob = RT::User->new($RT::SystemUser), "Creating a bob rt::user");
1255 $bob->LoadByEmail('bob@fsck.com');
1256 ok($bob->Id,  "Found the bob rt user");
1257 ok ($ticket->IsWatcher(Type => 'Requestor', PrincipalId => $bob->PrincipalId), "The ticket actually has bob at fsck.com as a requestor");;
1258 ok ((my $add_id, $add_msg) = $ticket->DeleteWatcher(Type =>'Requestor', Email => 'bob@fsck.com'), "Added bob at fsck.com as a requestor");
1259 ok (!$ticket->IsWatcher(Type => 'Requestor', Principal => $bob->PrincipalId), "The ticket no longer has bob at fsck.com as a requestor");;
1260
1261
1262 $group = RT::Group->new($RT::SystemUser);
1263 ok($group->LoadTicketRoleGroup(Ticket => $id, Type=> 'Cc'));
1264 ok ($group->Id, "Found the cc object for this ticket");
1265 $group = RT::Group->new($RT::SystemUser);
1266 ok($group->LoadTicketRoleGroup(Ticket => $id, Type=> 'AdminCc'));
1267 ok ($group->Id, "Found the AdminCc object for this ticket");
1268 $group = RT::Group->new($RT::SystemUser);
1269 ok($group->LoadTicketRoleGroup(Ticket => $id, Type=> 'Owner'));
1270 ok ($group->Id, "Found the Owner object for this ticket");
1271 ok($group->HasMember($RT::SystemUser->UserObj->PrincipalObj), "the owner group has the member 'RT_System'");
1272
1273 =end testing
1274
1275 =cut
1276
1277
1278 sub _CreateTicketGroups {
1279     my $self = shift;
1280     
1281     my @types = qw(Requestor Owner Cc AdminCc);
1282
1283     foreach my $type (@types) {
1284         my $type_obj = RT::Group->new($self->CurrentUser);
1285         my ($id, $msg) = $type_obj->CreateRoleGroup(Domain => 'RT::Ticket-Role',
1286                                                        Instance => $self->Id, 
1287                                                        Type => $type);
1288         unless ($id) {
1289             $RT::Logger->error("Couldn't create a ticket group of type '$type' for ticket ".
1290                                $self->Id.": ".$msg);     
1291             return(undef);
1292         }
1293      }
1294     return(1);
1295     
1296 }
1297
1298 # }}}
1299
1300 # {{{ sub OwnerGroup
1301
1302 =head2 OwnerGroup
1303
1304 A constructor which returns an RT::Group object containing the owner of this ticket.
1305
1306 =cut
1307
1308 sub OwnerGroup {
1309     my $self = shift;
1310     my $owner_obj = RT::Group->new($self->CurrentUser);
1311     $owner_obj->LoadTicketRoleGroup( Ticket => $self->Id,  Type => 'Owner');
1312     return ($owner_obj);
1313 }
1314
1315 # }}}
1316
1317
1318 # {{{ sub AddWatcher
1319
1320 =head2 AddWatcher
1321
1322 AddWatcher takes a parameter hash. The keys are as follows:
1323
1324 Type        One of Requestor, Cc, AdminCc
1325
1326 PrinicpalId The RT::Principal id of the user or group that's being added as a watcher
1327
1328 Email       The email address of the new watcher. If a user with this 
1329             email address can't be found, a new nonprivileged user will be created.
1330
1331 If the watcher you\'re trying to set has an RT account, set the Owner paremeter to their User Id. Otherwise, set the Email parameter to their Email address.
1332
1333 =cut
1334
1335 sub AddWatcher {
1336     my $self = shift;
1337     my %args = (
1338         Type  => undef,
1339         PrincipalId => undef,
1340         Email => undef,
1341         @_
1342     );
1343
1344     # XXX, FIXME, BUG: if only email is provided then we only check
1345     # for ModifyTicket right, but must try to get PrincipalId and
1346     # check Watch* rights too if user exist
1347
1348     # {{{ Check ACLS
1349     #If the watcher we're trying to add is for the current user
1350     if ( $self->CurrentUser->PrincipalId == ($args{'PrincipalId'} || 0)
1351        or    lc( $self->CurrentUser->UserObj->EmailAddress )
1352           eq lc( RT::User::CanonicalizeEmailAddress(undef, $args{'Email'}) || '' ) )
1353     {
1354         #  If it's an AdminCc and they don't have 
1355         #   'WatchAsAdminCc' or 'ModifyTicket', bail
1356         if ( $args{'Type'} eq 'AdminCc' ) {
1357             unless ( $self->CurrentUserHasRight('ModifyTicket')
1358                 or $self->CurrentUserHasRight('WatchAsAdminCc') ) {
1359                 return ( 0, $self->loc('Permission Denied'))
1360             }
1361         }
1362
1363         #  If it's a Requestor or Cc and they don't have
1364         #   'Watch' or 'ModifyTicket', bail
1365         elsif ( ( $args{'Type'} eq 'Cc' ) or ( $args{'Type'} eq 'Requestor' ) ) {
1366
1367             unless ( $self->CurrentUserHasRight('ModifyTicket')
1368                 or $self->CurrentUserHasRight('Watch') ) {
1369                 return ( 0, $self->loc('Permission Denied'))
1370             }
1371         }
1372         else {
1373             $RT::Logger->warning( "$self -> AddWatcher got passed a bogus type");
1374             return ( 0, $self->loc('Error in parameters to Ticket->AddWatcher') );
1375         }
1376     }
1377
1378     # If the watcher isn't the current user 
1379     # and the current user  doesn't have 'ModifyTicket'
1380     # bail
1381     else {
1382         unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
1383             return ( 0, $self->loc("Permission Denied") );
1384         }
1385     }
1386
1387     # }}}
1388
1389     return ( $self->_AddWatcher(%args) );
1390 }
1391
1392 #This contains the meat of AddWatcher. but can be called from a routine like
1393 # Create, which doesn't need the additional acl check
1394 sub _AddWatcher {
1395     my $self = shift;
1396     my %args = (
1397         Type   => undef,
1398         Silent => undef,
1399         PrincipalId => undef,
1400         Email => undef,
1401         @_
1402     );
1403
1404
1405     my $principal = RT::Principal->new($self->CurrentUser);
1406     if ($args{'Email'}) {
1407         my $user = RT::User->new($RT::SystemUser);
1408         my ($pid, $msg) = $user->LoadOrCreateByEmail($args{'Email'});
1409         # If we can't load the user by email address, let's try to load by username     
1410         unless ($pid) { 
1411                 ($pid,$msg) = $user->Load($args{'Email'})
1412         }
1413         if ($pid) {
1414             $args{'PrincipalId'} = $pid; 
1415         }
1416     }
1417     if ($args{'PrincipalId'}) {
1418         $principal->Load($args{'PrincipalId'});
1419     } 
1420
1421  
1422     # If we can't find this watcher, we need to bail.
1423     unless ($principal->Id) {
1424             $RT::Logger->error("Could not load create a user with the email address '".$args{'Email'}. "' to add as a watcher for ticket ".$self->Id);
1425         return(0, $self->loc("Could not find or create that user"));
1426     }
1427
1428
1429     my $group = RT::Group->new($self->CurrentUser);
1430     $group->LoadTicketRoleGroup(Type => $args{'Type'}, Ticket => $self->Id);
1431     unless ($group->id) {
1432         return(0,$self->loc("Group not found"));
1433     }
1434
1435     if ( $group->HasMember( $principal)) {
1436
1437         return ( 0, $self->loc('That principal is already a [_1] for this ticket', $self->loc($args{'Type'})) );
1438     }
1439
1440
1441     my ( $m_id, $m_msg ) = $group->_AddMember( PrincipalId => $principal->Id,
1442                                                InsideTransaction => 1 );
1443     unless ($m_id) {
1444         $RT::Logger->error("Failed to add ".$principal->Id." as a member of group ".$group->Id."\n".$m_msg);
1445
1446         return ( 0, $self->loc('Could not make that principal a [_1] for this ticket', $self->loc($args{'Type'})) );
1447     }
1448
1449     unless ( $args{'Silent'} ) {
1450         $self->_NewTransaction(
1451             Type     => 'AddWatcher',
1452             NewValue => $principal->Id,
1453             Field    => $args{'Type'}
1454         );
1455     }
1456
1457         return ( 1, $self->loc('Added principal as a [_1] for this ticket', $self->loc($args{'Type'})) );
1458 }
1459
1460 # }}}
1461
1462
1463 # {{{ sub DeleteWatcher
1464
1465 =head2 DeleteWatcher { Type => TYPE, PrincipalId => PRINCIPAL_ID, Email => EMAIL_ADDRESS }
1466
1467
1468 Deletes a Ticket watcher.  Takes two arguments:
1469
1470 Type  (one of Requestor,Cc,AdminCc)
1471
1472 and one of
1473
1474 PrincipalId (an RT::Principal Id of the watcher you want to remove)
1475     OR
1476 Email (the email address of an existing wathcer)
1477
1478
1479 =cut
1480
1481
1482 sub DeleteWatcher {
1483     my $self = shift;
1484
1485     my %args = ( Type        => undef,
1486                  PrincipalId => undef,
1487                  Email       => undef,
1488                  @_ );
1489
1490     unless ( $args{'PrincipalId'} || $args{'Email'} ) {
1491         return ( 0, $self->loc("No principal specified") );
1492     }
1493     my $principal = RT::Principal->new( $self->CurrentUser );
1494     if ( $args{'PrincipalId'} ) {
1495
1496         $principal->Load( $args{'PrincipalId'} );
1497     }
1498     else {
1499         my $user = RT::User->new( $self->CurrentUser );
1500         $user->LoadByEmail( $args{'Email'} );
1501         $principal->Load( $user->Id );
1502     }
1503
1504     # If we can't find this watcher, we need to bail.
1505     unless ( $principal->Id ) {
1506         return ( 0, $self->loc("Could not find that principal") );
1507     }
1508
1509     my $group = RT::Group->new( $self->CurrentUser );
1510     $group->LoadTicketRoleGroup( Type => $args{'Type'}, Ticket => $self->Id );
1511     unless ( $group->id ) {
1512         return ( 0, $self->loc("Group not found") );
1513     }
1514
1515     # {{{ Check ACLS
1516     #If the watcher we're trying to add is for the current user
1517     if ( $self->CurrentUser->PrincipalId == $principal->id ) {
1518
1519         #  If it's an AdminCc and they don't have
1520         #   'WatchAsAdminCc' or 'ModifyTicket', bail
1521         if ( $args{'Type'} eq 'AdminCc' ) {
1522             unless (    $self->CurrentUserHasRight('ModifyTicket')
1523                      or $self->CurrentUserHasRight('WatchAsAdminCc') ) {
1524                 return ( 0, $self->loc('Permission Denied') );
1525             }
1526         }
1527
1528         #  If it's a Requestor or Cc and they don't have
1529         #   'Watch' or 'ModifyTicket', bail
1530         elsif ( ( $args{'Type'} eq 'Cc' ) or ( $args{'Type'} eq 'Requestor' ) )
1531         {
1532             unless (    $self->CurrentUserHasRight('ModifyTicket')
1533                      or $self->CurrentUserHasRight('Watch') ) {
1534                 return ( 0, $self->loc('Permission Denied') );
1535             }
1536         }
1537         else {
1538             $RT::Logger->warn("$self -> DeleteWatcher got passed a bogus type");
1539             return ( 0,
1540                      $self->loc('Error in parameters to Ticket->DeleteWatcher') );
1541         }
1542     }
1543
1544     # If the watcher isn't the current user
1545     # and the current user  doesn't have 'ModifyTicket' bail
1546     else {
1547         unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
1548             return ( 0, $self->loc("Permission Denied") );
1549         }
1550     }
1551
1552     # }}}
1553
1554     # see if this user is already a watcher.
1555
1556     unless ( $group->HasMember($principal) ) {
1557         return ( 0,
1558                  $self->loc( 'That principal is not a [_1] for this ticket',
1559                              $args{'Type'} ) );
1560     }
1561
1562     my ( $m_id, $m_msg ) = $group->_DeleteMember( $principal->Id );
1563     unless ($m_id) {
1564         $RT::Logger->error( "Failed to delete "
1565                             . $principal->Id
1566                             . " as a member of group "
1567                             . $group->Id . "\n"
1568                             . $m_msg );
1569
1570         return (0,
1571                 $self->loc(
1572                     'Could not remove that principal as a [_1] for this ticket',
1573                     $args{'Type'} ) );
1574     }
1575
1576     unless ( $args{'Silent'} ) {
1577         $self->_NewTransaction( Type     => 'DelWatcher',
1578                                 OldValue => $principal->Id,
1579                                 Field    => $args{'Type'} );
1580     }
1581
1582     return ( 1,
1583              $self->loc( "[_1] is no longer a [_2] for this ticket.",
1584                          $principal->Object->Name,
1585                          $args{'Type'} ) );
1586 }
1587
1588
1589
1590 # }}}
1591
1592
1593 =head2 SquelchMailTo [EMAIL]
1594
1595 Takes an optional email address to never email about updates to this ticket.
1596
1597
1598 Returns an array of the RT::Attribute objects for this ticket's 'SquelchMailTo' attributes.
1599
1600 =begin testing
1601
1602 my $t = RT::Ticket->new($RT::SystemUser);
1603 ok($t->Create(Queue => 'general', Subject => 'SquelchTest'));
1604
1605 is($#{$t->SquelchMailTo}, -1, "The ticket has no squelched recipients");
1606
1607 my @returned = $t->SquelchMailTo('nobody@example.com');
1608
1609 is($#returned, 0, "The ticket has one squelched recipients");
1610
1611 my @names = $t->Attributes->Names;
1612 is(shift @names, 'SquelchMailTo', "The attribute we have is SquelchMailTo");
1613 @returned = $t->SquelchMailTo('nobody@example.com');
1614
1615
1616 is($#returned, 0, "The ticket has one squelched recipients");
1617
1618 @names = $t->Attributes->Names;
1619 is(shift @names, 'SquelchMailTo', "The attribute we have is SquelchMailTo");
1620
1621
1622 my ($ret, $msg) = $t->UnsquelchMailTo('nobody@example.com');
1623 ok($ret, "Removed nobody as a squelched recipient - ".$msg);
1624 @returned = $t->SquelchMailTo();
1625 is($#returned, -1, "The ticket has no squelched recipients". join(',',@returned));
1626
1627
1628 =end testing
1629
1630 =cut
1631
1632 sub SquelchMailTo {
1633     my $self = shift;
1634     if (@_) {
1635         unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
1636             return undef;
1637         }
1638         my $attr = shift;
1639         $self->AddAttribute( Name => 'SquelchMailTo', Content => $attr )
1640           unless grep { $_->Content eq $attr }
1641           $self->Attributes->Named('SquelchMailTo');
1642
1643     }
1644     unless ( $self->CurrentUserHasRight('ShowTicket') ) {
1645         return undef;
1646     }
1647     my @attributes = $self->Attributes->Named('SquelchMailTo');
1648     return (@attributes);
1649 }
1650
1651
1652 =head2 UnsquelchMailTo ADDRESS
1653
1654 Takes an address and removes it from this ticket's "SquelchMailTo" list. If an address appears multiple times, each instance is removed.
1655
1656 Returns a tuple of (status, message)
1657
1658 =cut
1659
1660 sub UnsquelchMailTo {
1661     my $self = shift;
1662
1663     my $address = shift;
1664     unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
1665         return ( 0, $self->loc("Permission Denied") );
1666     }
1667
1668     my ($val, $msg) = $self->Attributes->DeleteEntry ( Name => 'SquelchMailTo', Content => $address);
1669     return ($val, $msg);
1670 }
1671
1672
1673 # {{{ a set of  [foo]AsString subs that will return the various sorts of watchers for a ticket/queue as a comma delineated string
1674
1675 =head2 RequestorAddresses
1676
1677  B<Returns> String: All Ticket Requestor email addresses as a string.
1678
1679 =cut
1680
1681 sub RequestorAddresses {
1682     my $self = shift;
1683
1684     unless ( $self->CurrentUserHasRight('ShowTicket') ) {
1685         return undef;
1686     }
1687
1688     return ( $self->Requestors->MemberEmailAddressesAsString );
1689 }
1690
1691
1692 =head2 AdminCcAddresses
1693
1694 returns String: All Ticket AdminCc email addresses as a string
1695
1696 =cut
1697
1698 sub AdminCcAddresses {
1699     my $self = shift;
1700
1701     unless ( $self->CurrentUserHasRight('ShowTicket') ) {
1702         return undef;
1703     }
1704
1705     return ( $self->AdminCc->MemberEmailAddressesAsString )
1706
1707 }
1708
1709 =head2 CcAddresses
1710
1711 returns String: All Ticket Ccs as a string of email addresses
1712
1713 =cut
1714
1715 sub CcAddresses {
1716     my $self = shift;
1717
1718     unless ( $self->CurrentUserHasRight('ShowTicket') ) {
1719         return undef;
1720     }
1721
1722     return ( $self->Cc->MemberEmailAddressesAsString);
1723
1724 }
1725
1726 # }}}
1727
1728 # {{{ Routines that return RT::Watchers objects of Requestors, Ccs and AdminCcs
1729
1730 # {{{ sub Requestors
1731
1732 =head2 Requestors
1733
1734 Takes nothing.
1735 Returns this ticket's Requestors as an RT::Group object
1736
1737 =cut
1738
1739 sub Requestors {
1740     my $self = shift;
1741
1742     my $group = RT::Group->new($self->CurrentUser);
1743     if ( $self->CurrentUserHasRight('ShowTicket') ) {
1744         $group->LoadTicketRoleGroup(Type => 'Requestor', Ticket => $self->Id);
1745     }
1746     return ($group);
1747
1748 }
1749
1750 # }}}
1751
1752 # {{{ sub Cc
1753
1754 =head2 Cc
1755
1756 Takes nothing.
1757 Returns an RT::Group object which contains this ticket's Ccs.
1758 If the user doesn't have "ShowTicket" permission, returns an empty group
1759
1760 =cut
1761
1762 sub Cc {
1763     my $self = shift;
1764
1765     my $group = RT::Group->new($self->CurrentUser);
1766     if ( $self->CurrentUserHasRight('ShowTicket') ) {
1767         $group->LoadTicketRoleGroup(Type => 'Cc', Ticket => $self->Id);
1768     }
1769     return ($group);
1770
1771 }
1772
1773 # }}}
1774
1775 # {{{ sub AdminCc
1776
1777 =head2 AdminCc
1778
1779 Takes nothing.
1780 Returns an RT::Group object which contains this ticket's AdminCcs.
1781 If the user doesn't have "ShowTicket" permission, returns an empty group
1782
1783 =cut
1784
1785 sub AdminCc {
1786     my $self = shift;
1787
1788     my $group = RT::Group->new($self->CurrentUser);
1789     if ( $self->CurrentUserHasRight('ShowTicket') ) {
1790         $group->LoadTicketRoleGroup(Type => 'AdminCc', Ticket => $self->Id);
1791     }
1792     return ($group);
1793
1794 }
1795
1796 # }}}
1797
1798 # }}}
1799
1800 # {{{ IsWatcher,IsRequestor,IsCc, IsAdminCc
1801
1802 # {{{ sub IsWatcher
1803 # a generic routine to be called by IsRequestor, IsCc and IsAdminCc
1804
1805 =head2 IsWatcher { Type => TYPE, PrincipalId => PRINCIPAL_ID, Email => EMAIL }
1806
1807 Takes a param hash with the attributes Type and either PrincipalId or Email
1808
1809 Type is one of Requestor, Cc, AdminCc and Owner
1810
1811 PrincipalId is an RT::Principal id, and Email is an email address.
1812
1813 Returns true if the specified principal (or the one corresponding to the
1814 specified address) is a member of the group Type for this ticket.
1815
1816 XX TODO: This should be Memoized. 
1817
1818 =cut
1819
1820 sub IsWatcher {
1821     my $self = shift;
1822
1823     my %args = ( Type  => 'Requestor',
1824         PrincipalId    => undef,
1825         Email          => undef,
1826         @_
1827     );
1828
1829     # Load the relevant group. 
1830     my $group = RT::Group->new($self->CurrentUser);
1831     $group->LoadTicketRoleGroup(Type => $args{'Type'}, Ticket => $self->id);
1832
1833     # Find the relevant principal.
1834     my $principal = RT::Principal->new($self->CurrentUser);
1835     if (!$args{PrincipalId} && $args{Email}) {
1836         # Look up the specified user.
1837         my $user = RT::User->new($self->CurrentUser);
1838         $user->LoadByEmail($args{Email});
1839         if ($user->Id) {
1840             $args{PrincipalId} = $user->PrincipalId;
1841         }
1842         else {
1843             # A non-existent user can't be a group member.
1844             return 0;
1845         }
1846     }
1847     $principal->Load($args{'PrincipalId'});
1848
1849     # Ask if it has the member in question
1850     return ($group->HasMember($principal));
1851 }
1852
1853 # }}}
1854
1855 # {{{ sub IsRequestor
1856
1857 =head2 IsRequestor PRINCIPAL_ID
1858   
1859   Takes an RT::Principal id
1860   Returns true if the principal is a requestor of the current ticket.
1861
1862
1863 =cut
1864
1865 sub IsRequestor {
1866     my $self   = shift;
1867     my $person = shift;
1868
1869     return ( $self->IsWatcher( Type => 'Requestor', PrincipalId => $person ) );
1870
1871 };
1872
1873 # }}}
1874
1875 # {{{ sub IsCc
1876
1877 =head2 IsCc PRINCIPAL_ID
1878
1879   Takes an RT::Principal id.
1880   Returns true if the principal is a requestor of the current ticket.
1881
1882
1883 =cut
1884
1885 sub IsCc {
1886     my $self = shift;
1887     my $cc   = shift;
1888
1889     return ( $self->IsWatcher( Type => 'Cc', PrincipalId => $cc ) );
1890
1891 }
1892
1893 # }}}
1894
1895 # {{{ sub IsAdminCc
1896
1897 =head2 IsAdminCc PRINCIPAL_ID
1898
1899   Takes an RT::Principal id.
1900   Returns true if the principal is a requestor of the current ticket.
1901
1902 =cut
1903
1904 sub IsAdminCc {
1905     my $self   = shift;
1906     my $person = shift;
1907
1908     return ( $self->IsWatcher( Type => 'AdminCc', PrincipalId => $person ) );
1909
1910 }
1911
1912 # }}}
1913
1914 # {{{ sub IsOwner
1915
1916 =head2 IsOwner
1917
1918   Takes an RT::User object. Returns true if that user is this ticket's owner.
1919 returns undef otherwise
1920
1921 =cut
1922
1923 sub IsOwner {
1924     my $self   = shift;
1925     my $person = shift;
1926
1927     # no ACL check since this is used in acl decisions
1928     # unless ($self->CurrentUserHasRight('ShowTicket')) {
1929     #   return(undef);
1930     #   }       
1931
1932     #Tickets won't yet have owners when they're being created.
1933     unless ( $self->OwnerObj->id ) {
1934         return (undef);
1935     }
1936
1937     if ( $person->id == $self->OwnerObj->id ) {
1938         return (1);
1939     }
1940     else {
1941         return (undef);
1942     }
1943 }
1944
1945 # }}}
1946
1947 # }}}
1948
1949 # }}}
1950
1951 # {{{ Routines dealing with queues 
1952
1953 # {{{ sub ValidateQueue
1954
1955 sub ValidateQueue {
1956     my $self  = shift;
1957     my $Value = shift;
1958
1959     if ( !$Value ) {
1960         $RT::Logger->warning( " RT:::Queue::ValidateQueue called with a null value. this isn't ok.");
1961         return (1);
1962     }
1963
1964     my $QueueObj = RT::Queue->new( $self->CurrentUser );
1965     my $id       = $QueueObj->Load($Value);
1966
1967     if ($id) {
1968         return (1);
1969     }
1970     else {
1971         return (undef);
1972     }
1973 }
1974
1975 # }}}
1976
1977 # {{{ sub SetQueue  
1978
1979 sub SetQueue {
1980     my $self     = shift;
1981     my $NewQueue = shift;
1982
1983     #Redundant. ACL gets checked in _Set;
1984     unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
1985         return ( 0, $self->loc("Permission Denied") );
1986     }
1987
1988     my $NewQueueObj = RT::Queue->new( $self->CurrentUser );
1989     $NewQueueObj->Load($NewQueue);
1990
1991     unless ( $NewQueueObj->Id() ) {
1992         return ( 0, $self->loc("That queue does not exist") );
1993     }
1994
1995     if ( $NewQueueObj->Id == $self->QueueObj->Id ) {
1996         return ( 0, $self->loc('That is the same value') );
1997     }
1998     unless (
1999         $self->CurrentUser->HasRight(
2000             Right    => 'CreateTicket',
2001             Object => $NewQueueObj
2002         )
2003       )
2004     {
2005         return ( 0, $self->loc("You may not create requests in that queue.") );
2006     }
2007
2008     unless (
2009         $self->OwnerObj->HasRight(
2010             Right    => 'OwnTicket',
2011             Object => $NewQueueObj
2012         )
2013       )
2014     {
2015         my $clone = RT::Ticket->new( $RT::SystemUser );
2016         $clone->Load( $self->Id );
2017         unless ( $clone->Id ) {
2018             return ( 0, $self->loc("Couldn't load copy of ticket #[_1].", $self->Id) );
2019         }
2020         my ($status, $msg) = $clone->SetOwner( $RT::Nobody->Id, 'Force' );
2021         $RT::Logger->error("Couldn't set owner on queue change: $msg") unless $status;
2022     }
2023
2024     return ( $self->_Set( Field => 'Queue', Value => $NewQueueObj->Id() ) );
2025 }
2026
2027 # }}}
2028
2029 # {{{ sub QueueObj
2030
2031 =head2 QueueObj
2032
2033 Takes nothing. returns this ticket's queue object
2034
2035 =cut
2036
2037 sub QueueObj {
2038     my $self = shift;
2039
2040     my $queue_obj = RT::Queue->new( $self->CurrentUser );
2041
2042     #We call __Value so that we can avoid the ACL decision and some deep recursion
2043     my ($result) = $queue_obj->Load( $self->__Value('Queue') );
2044     return ($queue_obj);
2045 }
2046
2047 # }}}
2048
2049 # }}}
2050
2051 # {{{ Date printing routines
2052
2053 # {{{ sub DueObj
2054
2055 =head2 DueObj
2056
2057   Returns an RT::Date object containing this ticket's due date
2058
2059 =cut
2060
2061 sub DueObj {
2062     my $self = shift;
2063
2064     my $time = new RT::Date( $self->CurrentUser );
2065
2066     # -1 is RT::Date slang for never
2067     if ( $self->Due ) {
2068         $time->Set( Format => 'sql', Value => $self->Due );
2069     }
2070     else {
2071         $time->Set( Format => 'unix', Value => -1 );
2072     }
2073
2074     return $time;
2075 }
2076
2077 # }}}
2078
2079 # {{{ sub DueAsString 
2080
2081 =head2 DueAsString
2082
2083 Returns this ticket's due date as a human readable string
2084
2085 =cut
2086
2087 sub DueAsString {
2088     my $self = shift;
2089     return $self->DueObj->AsString();
2090 }
2091
2092 # }}}
2093
2094 # {{{ sub ResolvedObj
2095
2096 =head2 ResolvedObj
2097
2098   Returns an RT::Date object of this ticket's 'resolved' time.
2099
2100 =cut
2101
2102 sub ResolvedObj {
2103     my $self = shift;
2104
2105     my $time = new RT::Date( $self->CurrentUser );
2106     $time->Set( Format => 'sql', Value => $self->Resolved );
2107     return $time;
2108 }
2109
2110 # }}}
2111
2112 # {{{ sub SetStarted
2113
2114 =head2 SetStarted
2115
2116 Takes a date in ISO format or undef
2117 Returns a transaction id and a message
2118 The client calls "Start" to note that the project was started on the date in $date.
2119 A null date means "now"
2120
2121 =cut
2122
2123 sub SetStarted {
2124     my $self = shift;
2125     my $time = shift || 0;
2126
2127     unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
2128         return ( 0, $self->loc("Permission Denied") );
2129     }
2130
2131     #We create a date object to catch date weirdness
2132     my $time_obj = new RT::Date( $self->CurrentUser() );
2133     if ( $time ) {
2134         $time_obj->Set( Format => 'ISO', Value => $time );
2135     }
2136     else {
2137         $time_obj->SetToNow();
2138     }
2139
2140     #Now that we're starting, open this ticket
2141     #TODO do we really want to force this as policy? it should be a scrip
2142
2143     #We need $TicketAsSystem, in case the current user doesn't have
2144     #ShowTicket
2145     #
2146     my $TicketAsSystem = new RT::Ticket($RT::SystemUser);
2147     $TicketAsSystem->Load( $self->Id );
2148     if ( $TicketAsSystem->Status eq 'new' ) {
2149         $TicketAsSystem->Open();
2150     }
2151
2152     return ( $self->_Set( Field => 'Started', Value => $time_obj->ISO ) );
2153
2154 }
2155
2156 # }}}
2157
2158 # {{{ sub StartedObj
2159
2160 =head2 StartedObj
2161
2162   Returns an RT::Date object which contains this ticket's 
2163 'Started' time.
2164
2165 =cut
2166
2167 sub StartedObj {
2168     my $self = shift;
2169
2170     my $time = new RT::Date( $self->CurrentUser );
2171     $time->Set( Format => 'sql', Value => $self->Started );
2172     return $time;
2173 }
2174
2175 # }}}
2176
2177 # {{{ sub StartsObj
2178
2179 =head2 StartsObj
2180
2181   Returns an RT::Date object which contains this ticket's 
2182 'Starts' time.
2183
2184 =cut
2185
2186 sub StartsObj {
2187     my $self = shift;
2188
2189     my $time = new RT::Date( $self->CurrentUser );
2190     $time->Set( Format => 'sql', Value => $self->Starts );
2191     return $time;
2192 }
2193
2194 # }}}
2195
2196 # {{{ sub ToldObj
2197
2198 =head2 ToldObj
2199
2200   Returns an RT::Date object which contains this ticket's 
2201 'Told' time.
2202
2203 =cut
2204
2205 sub ToldObj {
2206     my $self = shift;
2207
2208     my $time = new RT::Date( $self->CurrentUser );
2209     $time->Set( Format => 'sql', Value => $self->Told );
2210     return $time;
2211 }
2212
2213 # }}}
2214
2215 # {{{ sub ToldAsString
2216
2217 =head2 ToldAsString
2218
2219 A convenience method that returns ToldObj->AsString
2220
2221 TODO: This should be deprecated
2222
2223 =cut
2224
2225 sub ToldAsString {
2226     my $self = shift;
2227     if ( $self->Told ) {
2228         return $self->ToldObj->AsString();
2229     }
2230     else {
2231         return ("Never");
2232     }
2233 }
2234
2235 # }}}
2236
2237 # {{{ sub TimeWorkedAsString
2238
2239 =head2 TimeWorkedAsString
2240
2241 Returns the amount of time worked on this ticket as a Text String
2242
2243 =cut
2244
2245 sub TimeWorkedAsString {
2246     my $self = shift;
2247     return "0" unless $self->TimeWorked;
2248
2249     #This is not really a date object, but if we diff a number of seconds 
2250     #vs the epoch, we'll get a nice description of time worked.
2251
2252     my $worked = new RT::Date( $self->CurrentUser );
2253
2254     #return the  #of minutes worked turned into seconds and written as
2255     # a simple text string
2256
2257     return ( $worked->DurationAsString( $self->TimeWorked * 60 ) );
2258 }
2259
2260 # }}}
2261
2262 # }}}
2263
2264 # {{{ Routines dealing with correspondence/comments
2265
2266 # {{{ sub Comment
2267
2268 =head2 Comment
2269
2270 Comment on this ticket.
2271 Takes a hashref with the following attributes:
2272 If MIMEObj is undefined, Content will be used to build a MIME::Entity for this
2273 commentl
2274
2275 MIMEObj, TimeTaken, CcMessageTo, BccMessageTo, Content, DryRun
2276
2277 If DryRun is defined, this update WILL NOT BE RECORDED. Scrips will not be committed.
2278 They will, however, be prepared and you'll be able to access them through the TransactionObj
2279
2280 Returns: Transaction id, Error Message, Transaction Object
2281 (note the different order from Create()!)
2282
2283 =cut
2284
2285 sub Comment {
2286     my $self = shift;
2287
2288     my %args = ( CcMessageTo  => undef,
2289                  BccMessageTo => undef,
2290                  MIMEObj      => undef,
2291                  Content      => undef,
2292                  TimeTaken => 0,
2293                  DryRun     => 0, 
2294                  @_ );
2295
2296     unless (    ( $self->CurrentUserHasRight('CommentOnTicket') )
2297              or ( $self->CurrentUserHasRight('ModifyTicket') ) ) {
2298         return ( 0, $self->loc("Permission Denied"), undef );
2299     }
2300     $args{'NoteType'} = 'Comment';
2301
2302     if ($args{'DryRun'}) {
2303         $RT::Handle->BeginTransaction();
2304         $args{'CommitScrips'} = 0;
2305     }
2306
2307     my @results = $self->_RecordNote(%args);
2308     if ($args{'DryRun'}) {
2309         $RT::Handle->Rollback();
2310     }
2311
2312     return(@results);
2313 }
2314 # }}}
2315
2316 # {{{ sub Correspond
2317
2318 =head2 Correspond
2319
2320 Correspond on this ticket.
2321 Takes a hashref with the following attributes:
2322
2323
2324 MIMEObj, TimeTaken, CcMessageTo, BccMessageTo, Content, DryRun
2325
2326 if there's no MIMEObj, Content is used to build a MIME::Entity object
2327
2328 If DryRun is defined, this update WILL NOT BE RECORDED. Scrips will not be committed.
2329 They will, however, be prepared and you'll be able to access them through the TransactionObj
2330
2331 Returns: Transaction id, Error Message, Transaction Object
2332 (note the different order from Create()!)
2333
2334
2335 =cut
2336
2337 sub Correspond {
2338     my $self = shift;
2339     my %args = ( CcMessageTo  => undef,
2340                  BccMessageTo => undef,
2341                  MIMEObj      => undef,
2342                  Content      => undef,
2343                  TimeTaken    => 0,
2344                  @_ );
2345
2346     unless (    ( $self->CurrentUserHasRight('ReplyToTicket') )
2347              or ( $self->CurrentUserHasRight('ModifyTicket') ) ) {
2348         return ( 0, $self->loc("Permission Denied"), undef );
2349     }
2350
2351     $args{'NoteType'} = 'Correspond'; 
2352     if ($args{'DryRun'}) {
2353         $RT::Handle->BeginTransaction();
2354         $args{'CommitScrips'} = 0;
2355     }
2356
2357     my @results = $self->_RecordNote(%args);
2358
2359     #Set the last told date to now if this isn't mail from the requestor.
2360     #TODO: Note that this will wrongly ack mail from any non-requestor as a "told"
2361     $self->_SetTold unless ( $self->IsRequestor($self->CurrentUser->id));
2362
2363     if ($args{'DryRun'}) {
2364         $RT::Handle->Rollback();
2365     }
2366
2367     return (@results);
2368
2369 }
2370
2371 # }}}
2372
2373 # {{{ sub _RecordNote
2374
2375 =head2 _RecordNote
2376
2377 the meat of both comment and correspond. 
2378
2379 Performs no access control checks. hence, dangerous.
2380
2381 =cut
2382
2383 sub _RecordNote {
2384
2385     my $self = shift;
2386     my %args = ( CcMessageTo  => undef,
2387                  BccMessageTo => undef,
2388                  MIMEObj      => undef,
2389                  Content      => undef,
2390                  TimeTaken    => 0,
2391                  CommitScrips => 1,
2392                  @_ );
2393
2394     unless ( $args{'MIMEObj'} || $args{'Content'} ) {
2395             return ( 0, $self->loc("No message attached"), undef );
2396     }
2397     unless ( $args{'MIMEObj'} ) {
2398             $args{'MIMEObj'} = MIME::Entity->build( Data => (
2399                                                           ref $args{'Content'}
2400                                                           ? $args{'Content'}
2401                                                           : [ $args{'Content'} ]
2402                                                     ) );
2403         }
2404
2405     # convert text parts into utf-8
2406     RT::I18N::SetMIMEEntityToUTF8( $args{'MIMEObj'} );
2407
2408 # If we've been passed in CcMessageTo and BccMessageTo fields,
2409 # add them to the mime object for passing on to the transaction handler
2410 # The "NotifyOtherRecipients" scripAction will look for RT-Send-Cc: and RT-Send-Bcc:
2411 # headers
2412
2413     $args{'MIMEObj'}->head->add( 'RT-Send-Cc', RT::User::CanonicalizeEmailAddress(
2414                                                      undef, $args{'CcMessageTo'}
2415                                  ) )
2416       if defined $args{'CcMessageTo'};
2417     $args{'MIMEObj'}->head->add( 'RT-Send-Bcc',
2418                                  RT::User::CanonicalizeEmailAddress(
2419                                                     undef, $args{'BccMessageTo'}
2420                                  ) )
2421       if defined $args{'BccMessageTo'};
2422
2423     # If this is from an external source, we need to come up with its
2424     # internal Message-ID now, so all emails sent because of this
2425     # message have a common Message-ID
2426     unless ( ($args{'MIMEObj'}->head->get('Message-ID') || '')
2427             =~ /<(rt-.*?-\d+-\d+)\.(\d+-0-0)\@\Q$RT::Organization>/ )
2428     {
2429         $args{'MIMEObj'}->head->set( 'RT-Message-ID',
2430             "<rt-"
2431             . $RT::VERSION . "-"
2432             . $$ . "-"
2433             . CORE::time() . "-"
2434             . int(rand(2000)) . '.'
2435             . $self->id . "-"
2436             . "0" . "-"  # Scrip
2437             . "0" . "@"  # Email sent
2438             . $RT::Organization
2439             . ">" );
2440     }
2441
2442     #Record the correspondence (write the transaction)
2443     my ( $Trans, $msg, $TransObj ) = $self->_NewTransaction(
2444              Type => $args{'NoteType'},
2445              Data => ( $args{'MIMEObj'}->head->get('subject') || 'No Subject' ),
2446              TimeTaken => $args{'TimeTaken'},
2447              MIMEObj   => $args{'MIMEObj'}, 
2448              CommitScrips => $args{'CommitScrips'},
2449     );
2450
2451     unless ($Trans) {
2452         $RT::Logger->err("$self couldn't init a transaction $msg");
2453         return ( $Trans, $self->loc("Message could not be recorded"), undef );
2454     }
2455
2456     return ( $Trans, $self->loc("Message recorded"), $TransObj );
2457 }
2458
2459 # }}}
2460
2461 # }}}
2462
2463 # {{{ sub _Links 
2464
2465 sub _Links {
2466     my $self = shift;
2467
2468     #TODO: Field isn't the right thing here. but I ahave no idea what mnemonic ---
2469     #tobias meant by $f
2470     my $field = shift;
2471     my $type  = shift || "";
2472
2473     unless ( $self->{"$field$type"} ) {
2474         $self->{"$field$type"} = new RT::Links( $self->CurrentUser );
2475         if ( $self->CurrentUserHasRight('ShowTicket') ) {
2476             # Maybe this ticket is a merged ticket
2477             my $Tickets = new RT::Tickets( $self->CurrentUser );
2478             # at least to myself
2479             $self->{"$field$type"}->Limit( FIELD => $field,
2480                                            VALUE => $self->URI,
2481                                            ENTRYAGGREGATOR => 'OR' );
2482             $Tickets->Limit( FIELD => 'EffectiveId',
2483                              VALUE => $self->EffectiveId );
2484             while (my $Ticket = $Tickets->Next) {
2485                 $self->{"$field$type"}->Limit( FIELD => $field,
2486                                                VALUE => $Ticket->URI,
2487                                                ENTRYAGGREGATOR => 'OR' );
2488             }
2489             $self->{"$field$type"}->Limit( FIELD => 'Type',
2490                                            VALUE => $type )
2491               if ($type);
2492         }
2493     }
2494     return ( $self->{"$field$type"} );
2495 }
2496
2497 # }}}
2498
2499 # {{{ sub DeleteLink 
2500
2501 =head2 DeleteLink
2502
2503 Delete a link. takes a paramhash of Base, Target and Type.
2504 Either Base or Target must be null. The null value will 
2505 be replaced with this ticket\'s id
2506
2507 =cut 
2508
2509 sub DeleteLink {
2510     my $self = shift;
2511     my %args = (
2512         Base   => undef,
2513         Target => undef,
2514         Type   => undef,
2515         @_
2516     );
2517
2518     unless ( $args{'Target'} || $args{'Base'} ) {
2519         $RT::Logger->error("Base or Target must be specified\n");
2520         return ( 0, $self->loc('Either base or target must be specified') );
2521     }
2522
2523     #check acls
2524     my $right = 0;
2525     $right++ if $self->CurrentUserHasRight('ModifyTicket');
2526     if ( !$right && $RT::StrictLinkACL ) {
2527         return ( 0, $self->loc("Permission Denied") );
2528     }
2529
2530     # If the other URI is an RT::Ticket, we want to make sure the user
2531     # can modify it too...
2532     my ($status, $msg, $other_ticket) = $self->__GetTicketFromURI( URI => $args{'Target'} || $args{'Base'} );
2533     return (0, $msg) unless $status;
2534     if ( !$other_ticket || $other_ticket->CurrentUserHasRight('ModifyTicket') ) {
2535         $right++;
2536     }
2537     if ( ( !$RT::StrictLinkACL && $right == 0 ) ||
2538          ( $RT::StrictLinkACL && $right < 2 ) )
2539     {
2540         return ( 0, $self->loc("Permission Denied") );
2541     }
2542
2543     my ($val, $Msg) = $self->SUPER::_DeleteLink(%args);
2544
2545     if ( !$val ) {
2546         $RT::Logger->debug("Couldn't find that link\n");
2547         return ( 0, $Msg );
2548     }
2549
2550     my ($direction, $remote_link);
2551
2552     if ( $args{'Base'} ) {
2553         $remote_link = $args{'Base'};
2554         $direction = 'Target';
2555     }
2556     elsif ( $args{'Target'} ) {
2557         $remote_link = $args{'Target'};
2558         $direction='Base';
2559     }
2560
2561     if ( $args{'Silent'} ) {
2562         return ( $val, $Msg );
2563     }
2564     else {
2565         my $remote_uri = RT::URI->new( $self->CurrentUser );
2566         $remote_uri->FromURI( $remote_link );
2567
2568         my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction(
2569             Type      => 'DeleteLink',
2570             Field => $LINKDIRMAP{$args{'Type'}}->{$direction},
2571             OldValue =>  $remote_uri->URI || $remote_link,
2572             TimeTaken => 0
2573         );
2574
2575         if ( $remote_uri->IsLocal ) {
2576
2577             my $OtherObj = $remote_uri->Object;
2578             my ( $val, $Msg ) = $OtherObj->_NewTransaction(Type  => 'DeleteLink',
2579                                                            Field => $direction eq 'Target' ? $LINKDIRMAP{$args{'Type'}}->{Base}
2580                                                                                            : $LINKDIRMAP{$args{'Type'}}->{Target},
2581                                                            OldValue => $self->URI,
2582                                                            ActivateScrips => ! $RT::LinkTransactionsRun1Scrip,
2583                                                            TimeTaken => 0 );
2584         }
2585
2586         return ( $Trans, $Msg );
2587     }
2588 }
2589
2590 # }}}
2591
2592 # {{{ sub AddLink
2593
2594 =head2 AddLink
2595
2596 Takes a paramhash of Type and one of Base or Target. Adds that link to this ticket.
2597
2598 =cut
2599
2600 sub AddLink {
2601     my $self = shift;
2602     my %args = ( Target => '',
2603                  Base   => '',
2604                  Type   => '',
2605                  Silent => undef,
2606                  @_ );
2607
2608     unless ( $args{'Target'} || $args{'Base'} ) {
2609         $RT::Logger->error("Base or Target must be specified\n");
2610         return ( 0, $self->loc('Either base or target must be specified') );
2611     }
2612
2613     my $right = 0;
2614     $right++ if $self->CurrentUserHasRight('ModifyTicket');
2615     if ( !$right && $RT::StrictLinkACL ) {
2616         return ( 0, $self->loc("Permission Denied") );
2617     }
2618
2619     # If the other URI is an RT::Ticket, we want to make sure the user
2620     # can modify it too...
2621     my ($status, $msg, $other_ticket) = $self->__GetTicketFromURI( URI => $args{'Target'} || $args{'Base'} );
2622     return (0, $msg) unless $status;
2623     if ( !$other_ticket || $other_ticket->CurrentUserHasRight('ModifyTicket') ) {
2624         $right++;
2625     }
2626     if ( ( !$RT::StrictLinkACL && $right == 0 ) ||
2627          ( $RT::StrictLinkACL && $right < 2 ) )
2628     {
2629         return ( 0, $self->loc("Permission Denied") );
2630     }
2631
2632     return $self->_AddLink(%args);
2633 }
2634
2635 sub __GetTicketFromURI {
2636     my $self = shift;
2637     my %args = ( URI => '', @_ );
2638
2639     # If the other URI is an RT::Ticket, we want to make sure the user
2640     # can modify it too...
2641     my $uri_obj = RT::URI->new( $self->CurrentUser );
2642     $uri_obj->FromURI( $args{'URI'} );
2643
2644     unless ( $uri_obj->Resolver && $uri_obj->Scheme ) {
2645             my $msg = $self->loc( "Couldn't resolve '[_1]' into a URI.", $args{'URI'} );
2646         $RT::Logger->warning( "$msg\n" );
2647         return( 0, $msg );
2648     }
2649     my $obj = $uri_obj->Resolver->Object;
2650     unless ( UNIVERSAL::isa($obj, 'RT::Ticket') && $obj->id ) {
2651         return (1, 'Found not a ticket', undef);
2652     }
2653     return (1, 'Found ticket', $obj);
2654 }
2655
2656 =head2 _AddLink  
2657
2658 Private non-acled variant of AddLink so that links can be added during create.
2659
2660 =cut
2661
2662 sub _AddLink {
2663     my $self = shift;
2664     my %args = ( Target => '',
2665                  Base   => '',
2666                  Type   => '',
2667                  Silent => undef,
2668                  @_ );
2669
2670     my ($val, $msg, $exist) = $self->SUPER::_AddLink(%args);
2671     return ($val, $msg) if !$val || $exist;
2672
2673     my ($direction, $remote_link);
2674     if ( $args{'Target'} ) {
2675         $remote_link  = $args{'Target'};
2676         $direction    = 'Base';
2677     } elsif ( $args{'Base'} ) {
2678         $remote_link  = $args{'Base'};
2679         $direction    = 'Target';
2680     }
2681
2682     # Don't write the transaction if we're doing this on create
2683     if ( $args{'Silent'} ) {
2684         return ( $val, $msg );
2685     }
2686     else {
2687         my $remote_uri = RT::URI->new( $self->CurrentUser );
2688         $remote_uri->FromURI( $remote_link );
2689
2690         #Write the transaction
2691         my ( $Trans, $Msg, $TransObj ) = 
2692             $self->_NewTransaction(Type  => 'AddLink',
2693                                    Field => $LINKDIRMAP{$args{'Type'}}->{$direction},
2694                                    NewValue =>  $remote_uri->URI || $remote_link,
2695                                    TimeTaken => 0 );
2696
2697         if ( $remote_uri->IsLocal ) {
2698
2699             my $OtherObj = $remote_uri->Object;
2700             my ( $val, $Msg ) = $OtherObj->_NewTransaction(Type  => 'AddLink',
2701                                                            Field => $direction eq 'Target' ? $LINKDIRMAP{$args{'Type'}}->{Base} 
2702                                                                                            : $LINKDIRMAP{$args{'Type'}}->{Target},
2703                                                            NewValue => $self->URI,
2704                                                            ActivateScrips => ! $RT::LinkTransactionsRun1Scrip,
2705                                                            TimeTaken => 0 );
2706         }
2707         return ( $val, $Msg );
2708     }
2709
2710 }
2711
2712 # }}}
2713
2714
2715 # {{{ sub MergeInto
2716
2717 =head2 MergeInto
2718
2719 MergeInto take the id of the ticket to merge this ticket into.
2720
2721
2722 =begin testing
2723
2724 my $t1 = RT::Ticket->new($RT::SystemUser);
2725 $t1->Create ( Subject => 'Merge test 1', Queue => 'general', Requestor => 'merge1@example.com');
2726 my $t1id = $t1->id;
2727 my $t2 = RT::Ticket->new($RT::SystemUser);
2728 $t2->Create ( Subject => 'Merge test 2', Queue => 'general', Requestor => 'merge2@example.com');
2729 my $t2id = $t2->id;
2730 my ($msg, $val) = $t1->MergeInto($t2->id);
2731 ok ($msg,$val);
2732 $t1 = RT::Ticket->new($RT::SystemUser);
2733 is ($t1->id, undef, "ok. we've got a blank ticket1");
2734 $t1->Load($t1id);
2735
2736 is ($t1->id, $t2->id);
2737
2738 is ($t1->Requestors->MembersObj->Count, 2);
2739
2740
2741 =end testing
2742
2743 =cut
2744
2745 sub MergeInto {
2746     my $self      = shift;
2747     my $ticket_id = shift;
2748
2749     unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
2750         return ( 0, $self->loc("Permission Denied") );
2751     }
2752
2753     # Load up the new ticket.
2754     my $MergeInto = RT::Ticket->new($RT::SystemUser);
2755     $MergeInto->Load($ticket_id);
2756
2757     # make sure it exists.
2758     unless ( $MergeInto->Id ) {
2759         return ( 0, $self->loc("New ticket doesn't exist") );
2760     }
2761
2762     # Make sure the current user can modify the new ticket.
2763     unless ( $MergeInto->CurrentUserHasRight('ModifyTicket') ) {
2764         return ( 0, $self->loc("Permission Denied") );
2765     }
2766
2767     $RT::Handle->BeginTransaction();
2768
2769     # We use EffectiveId here even though it duplicates information from
2770     # the links table becasue of the massive performance hit we'd take
2771     # by trying to do a separate database query for merge info everytime 
2772     # loaded a ticket. 
2773
2774     #update this ticket's effective id to the new ticket's id.
2775     my ( $id_val, $id_msg ) = $self->__Set(
2776         Field => 'EffectiveId',
2777         Value => $MergeInto->Id()
2778     );
2779
2780     unless ($id_val) {
2781         $RT::Handle->Rollback();
2782         return ( 0, $self->loc("Merge failed. Couldn't set EffectiveId") );
2783     }
2784
2785
2786     if ( $self->__Value('Status') ne 'resolved' ) {
2787
2788         my ( $status_val, $status_msg )
2789             = $self->__Set( Field => 'Status', Value => 'resolved' );
2790
2791         unless ($status_val) {
2792             $RT::Handle->Rollback();
2793             $RT::Logger->error(
2794                 $self->loc(
2795                     "[_1] couldn't set status to resolved. RT's Database may be inconsistent.",
2796                     $self
2797                 )
2798             );
2799             return ( 0, $self->loc("Merge failed. Couldn't set Status") );
2800         }
2801     }
2802
2803     # update all the links that point to that old ticket
2804     my $old_links_to = RT::Links->new($self->CurrentUser);
2805     $old_links_to->Limit(FIELD => 'Target', VALUE => $self->URI);
2806
2807     my %old_seen;
2808     while (my $link = $old_links_to->Next) {
2809         if (exists $old_seen{$link->Base."-".$link->Type}) {
2810             $link->Delete;
2811         }   
2812         elsif ($link->Base eq $MergeInto->URI) {
2813             $link->Delete;
2814         } else {
2815             # First, make sure the link doesn't already exist. then move it over.
2816             my $tmp = RT::Link->new($RT::SystemUser);
2817             $tmp->LoadByCols(Base => $link->Base, Type => $link->Type, LocalTarget => $MergeInto->id);
2818             if ($tmp->id)   {
2819                     $link->Delete;
2820             } else { 
2821                 $link->SetTarget($MergeInto->URI);
2822                 $link->SetLocalTarget($MergeInto->id);
2823             }
2824             $old_seen{$link->Base."-".$link->Type} =1;
2825         }
2826
2827     }
2828
2829     my $old_links_from = RT::Links->new($self->CurrentUser);
2830     $old_links_from->Limit(FIELD => 'Base', VALUE => $self->URI);
2831
2832     while (my $link = $old_links_from->Next) {
2833         if (exists $old_seen{$link->Type."-".$link->Target}) {
2834             $link->Delete;
2835         }   
2836         if ($link->Target eq $MergeInto->URI) {
2837             $link->Delete;
2838         } else {
2839             # First, make sure the link doesn't already exist. then move it over.
2840             my $tmp = RT::Link->new($RT::SystemUser);
2841             $tmp->LoadByCols(Target => $link->Target, Type => $link->Type, LocalBase => $MergeInto->id);
2842             if ($tmp->id)   {
2843                     $link->Delete;
2844             } else { 
2845                 $link->SetBase($MergeInto->URI);
2846                 $link->SetLocalBase($MergeInto->id);
2847                 $old_seen{$link->Type."-".$link->Target} =1;
2848             }
2849         }
2850
2851     }
2852
2853     # Update time fields
2854     foreach my $type qw(TimeEstimated TimeWorked TimeLeft) {
2855
2856         my $mutator = "Set$type";
2857         $MergeInto->$mutator(
2858             ( $MergeInto->$type() || 0 ) + ( $self->$type() || 0 ) );
2859
2860     }
2861 #add all of this ticket's watchers to that ticket.
2862     foreach my $watcher_type qw(Requestors Cc AdminCc) {
2863
2864         my $people = $self->$watcher_type->MembersObj;
2865         my $addwatcher_type =  $watcher_type;
2866         $addwatcher_type  =~ s/s$//;
2867
2868         while ( my $watcher = $people->Next ) {
2869             
2870            my ($val, $msg) =  $MergeInto->_AddWatcher(
2871                 Type        => $addwatcher_type,
2872                 Silent => 1,
2873                 PrincipalId => $watcher->MemberId
2874             );
2875             unless ($val) {
2876                 $RT::Logger->warning($msg);
2877             }
2878     }
2879
2880     }
2881
2882     #find all of the tickets that were merged into this ticket. 
2883     my $old_mergees = new RT::Tickets( $self->CurrentUser );
2884     $old_mergees->Limit(
2885         FIELD    => 'EffectiveId',
2886         OPERATOR => '=',
2887         VALUE    => $self->Id
2888     );
2889
2890     #   update their EffectiveId fields to the new ticket's id
2891     while ( my $ticket = $old_mergees->Next() ) {
2892         my ( $val, $msg ) = $ticket->__Set(
2893             Field => 'EffectiveId',
2894             Value => $MergeInto->Id()
2895         );
2896     }
2897
2898     #make a new link: this ticket is merged into that other ticket.
2899     $self->AddLink( Type   => 'MergedInto', Target => $MergeInto->Id());
2900
2901     $MergeInto->_SetLastUpdated;    
2902
2903     $RT::Handle->Commit();
2904     return ( 1, $self->loc("Merge Successful") );
2905 }
2906
2907 # }}}
2908
2909 # }}}
2910
2911 # {{{ Routines dealing with ownership
2912
2913 # {{{ sub OwnerObj
2914
2915 =head2 OwnerObj
2916
2917 Takes nothing and returns an RT::User object of 
2918 this ticket's owner
2919
2920 =cut
2921
2922 sub OwnerObj {
2923     my $self = shift;
2924
2925     #If this gets ACLed, we lose on a rights check in User.pm and
2926     #get deep recursion. if we need ACLs here, we need
2927     #an equiv without ACLs
2928
2929     my $owner = new RT::User( $self->CurrentUser );
2930     $owner->Load( $self->__Value('Owner') );
2931
2932     #Return the owner object
2933     return ($owner);
2934 }
2935
2936 # }}}
2937
2938 # {{{ sub OwnerAsString 
2939
2940 =head2 OwnerAsString
2941
2942 Returns the owner's email address
2943
2944 =cut
2945
2946 sub OwnerAsString {
2947     my $self = shift;
2948     return ( $self->OwnerObj->EmailAddress );
2949
2950 }
2951
2952 # }}}
2953
2954 # {{{ sub SetOwner
2955
2956 =head2 SetOwner
2957
2958 Takes two arguments:
2959      the Id or Name of the owner 
2960 and  (optionally) the type of the SetOwner Transaction. It defaults
2961 to 'Give'.  'Steal' is also a valid option.
2962
2963 =begin testing
2964
2965 my $root = RT::User->new($RT::SystemUser);
2966 $root->Load('root');
2967 ok ($root->Id, "Loaded the root user");
2968 my $t = RT::Ticket->new($RT::SystemUser);
2969 $t->Load(1);
2970 $t->SetOwner('root');
2971 is ($t->OwnerObj->Name, 'root' , "Root owns the ticket");
2972 $t->Steal();
2973 is ($t->OwnerObj->id, $RT::SystemUser->id , "SystemUser owns the ticket");
2974 my $txns = RT::Transactions->new($RT::SystemUser);
2975 $txns->OrderBy(FIELD => 'id', ORDER => 'DESC');
2976 $txns->Limit(FIELD => 'ObjectId', VALUE => '1');
2977 $txns->Limit(FIELD => 'ObjectType', VALUE => 'RT::Ticket');
2978 $txns->Limit(FIELD => 'Type', OPERATOR => '!=',  VALUE => 'EmailRecord');
2979
2980 my $steal  = $txns->First;
2981 ok($steal->OldValue == $root->Id , "Stolen from root");
2982 ok($steal->NewValue == $RT::SystemUser->Id , "Stolen by the systemuser");
2983
2984 =end testing
2985
2986 =cut
2987
2988 sub SetOwner {
2989     my $self     = shift;
2990     my $NewOwner = shift;
2991     my $Type     = shift || "Give";
2992
2993     $RT::Handle->BeginTransaction();
2994
2995     $self->_SetLastUpdated(); # lock the ticket
2996     $self->Load( $self->id ); # in case $self changed while waiting for lock
2997
2998     my $OldOwnerObj = $self->OwnerObj;
2999
3000     my $NewOwnerObj = RT::User->new( $self->CurrentUser );
3001     $NewOwnerObj->Load( $NewOwner );
3002     unless ( $NewOwnerObj->Id ) {
3003         $RT::Handle->Rollback();
3004         return ( 0, $self->loc("That user does not exist") );
3005     }
3006
3007
3008     # must have ModifyTicket rights
3009     # or TakeTicket/StealTicket and $NewOwner is self
3010     # see if it's a take
3011     if ( $OldOwnerObj->Id == $RT::Nobody->Id ) {
3012         unless (    $self->CurrentUserHasRight('ModifyTicket')
3013                  || $self->CurrentUserHasRight('TakeTicket') ) {
3014             $RT::Handle->Rollback();
3015             return ( 0, $self->loc("Permission Denied") );
3016         }
3017     }
3018
3019     # see if it's a steal
3020     elsif (    $OldOwnerObj->Id != $RT::Nobody->Id
3021             && $OldOwnerObj->Id != $self->CurrentUser->id ) {
3022
3023         unless (    $self->CurrentUserHasRight('ModifyTicket')
3024                  || $self->CurrentUserHasRight('StealTicket') ) {
3025             $RT::Handle->Rollback();
3026             return ( 0, $self->loc("Permission Denied") );
3027         }
3028     }
3029     else {
3030         unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
3031             $RT::Handle->Rollback();
3032             return ( 0, $self->loc("Permission Denied") );
3033         }
3034     }
3035
3036     # If we're not stealing and the ticket has an owner and it's not
3037     # the current user
3038     if ( $Type ne 'Steal' and $Type ne 'Force'
3039          and $OldOwnerObj->Id != $RT::Nobody->Id
3040          and $OldOwnerObj->Id != $self->CurrentUser->Id )
3041     {
3042         $RT::Handle->Rollback();
3043         return ( 0, $self->loc("You can only take tickets that are unowned") )
3044             if $NewOwnerObj->id == $self->CurrentUser->id;
3045         return (
3046             0,
3047             $self->loc("You can only reassign tickets that you own or that are unowned" )
3048         );
3049     }
3050
3051     #If we've specified a new owner and that user can't modify the ticket
3052     elsif ( !$NewOwnerObj->HasRight( Right => 'OwnTicket', Object => $self ) ) {
3053         $RT::Handle->Rollback();
3054         return ( 0, $self->loc("That user may not own tickets in that queue") );
3055     }
3056
3057     # If the ticket has an owner and it's the new owner, we don't need
3058     # To do anything
3059     elsif ( $NewOwnerObj->Id == $OldOwnerObj->Id ) {
3060         $RT::Handle->Rollback();
3061         return ( 0, $self->loc("That user already owns that ticket") );
3062     }
3063
3064     # Delete the owner in the owner group, then add a new one
3065     # TODO: is this safe? it's not how we really want the API to work
3066     # for most things, but it's fast.
3067     my ( $del_id, $del_msg ) = $self->OwnerGroup->MembersObj->First->Delete();
3068     unless ($del_id) {
3069         $RT::Handle->Rollback();
3070         return ( 0, $self->loc("Could not change owner. ") . $del_msg );
3071     }
3072
3073     my ( $add_id, $add_msg ) = $self->OwnerGroup->_AddMember(
3074                                        PrincipalId => $NewOwnerObj->PrincipalId,
3075                                        InsideTransaction => 1 );
3076     unless ($add_id) {
3077         $RT::Handle->Rollback();
3078         return ( 0, $self->loc("Could not change owner. ") . $add_msg );
3079     }
3080
3081     # We call set twice with slightly different arguments, so
3082     # as to not have an SQL transaction span two RT transactions
3083
3084     my ( $val, $msg ) = $self->_Set(
3085                       Field             => 'Owner',
3086                       RecordTransaction => 0,
3087                       Value             => $NewOwnerObj->Id,
3088                       TimeTaken         => 0,
3089                       TransactionType   => $Type,
3090                       CheckACL          => 0,                  # don't check acl
3091     );
3092
3093     unless ($val) {
3094         $RT::Handle->Rollback;
3095         return ( 0, $self->loc("Could not change owner. ") . $msg );
3096     }
3097
3098     ($val, $msg) = $self->_NewTransaction(
3099         Type      => $Type,
3100         Field     => 'Owner',
3101         NewValue  => $NewOwnerObj->Id,
3102         OldValue  => $OldOwnerObj->Id,
3103         TimeTaken => 0,
3104     );
3105
3106     if ( $val ) {
3107         $msg = $self->loc( "Owner changed from [_1] to [_2]",
3108                            $OldOwnerObj->Name, $NewOwnerObj->Name );
3109     }
3110     else {
3111         $RT::Handle->Rollback();
3112         return ( 0, $msg );
3113     }
3114
3115     $RT::Handle->Commit();
3116
3117     return ( $val, $msg );
3118 }
3119
3120 # }}}
3121
3122 # {{{ sub Take
3123
3124 =head2 Take
3125
3126 A convenince method to set the ticket's owner to the current user
3127
3128 =cut
3129
3130 sub Take {
3131     my $self = shift;
3132     return ( $self->SetOwner( $self->CurrentUser->Id, 'Take' ) );
3133 }
3134
3135 # }}}
3136
3137 # {{{ sub Untake
3138
3139 =head2 Untake
3140
3141 Convenience method to set the owner to 'nobody' if the current user is the owner.
3142
3143 =cut
3144
3145 sub Untake {
3146     my $self = shift;
3147     return ( $self->SetOwner( $RT::Nobody->UserObj->Id, 'Untake' ) );
3148 }
3149
3150 # }}}
3151
3152 # {{{ sub Steal 
3153
3154 =head2 Steal
3155
3156 A convenience method to change the owner of the current ticket to the
3157 current user. Even if it's owned by another user.
3158
3159 =cut
3160
3161 sub Steal {
3162     my $self = shift;
3163
3164     if ( $self->IsOwner( $self->CurrentUser ) ) {
3165         return ( 0, $self->loc("You already own this ticket") );
3166     }
3167     else {
3168         return ( $self->SetOwner( $self->CurrentUser->Id, 'Steal' ) );
3169
3170     }
3171
3172 }
3173
3174 # }}}
3175
3176 # }}}
3177
3178 # {{{ Routines dealing with status
3179
3180 # {{{ sub ValidateStatus 
3181
3182 =head2 ValidateStatus STATUS
3183
3184 Takes a string. Returns true if that status is a valid status for this ticket.
3185 Returns false otherwise.
3186
3187 =cut
3188
3189 sub ValidateStatus {
3190     my $self   = shift;
3191     my $status = shift;
3192
3193     #Make sure the status passed in is valid
3194     unless ( $self->QueueObj->IsValidStatus($status) ) {
3195         return (undef);
3196     }
3197
3198     return (1);
3199
3200 }
3201
3202 # }}}
3203
3204 # {{{ sub SetStatus
3205
3206 =head2 SetStatus STATUS
3207
3208 Set this ticket\'s status. STATUS can be one of: new, open, stalled, resolved, rejected or deleted.
3209
3210 Alternatively, you can pass in a list of named parameters (Status => STATUS, Force => FORCE).  If FORCE is true, ignore unresolved dependencies and force a status change.
3211
3212 =begin testing
3213
3214 my $tt = RT::Ticket->new($RT::SystemUser);
3215 my ($id, $tid, $msg)= $tt->Create(Queue => 'general',
3216             Subject => 'test');
3217 ok($id, $msg);
3218 is($tt->Status, 'new', "New ticket is created as new");
3219
3220 ($id, $msg) = $tt->SetStatus('open');
3221 ok($id, $msg);
3222 like($msg, qr/open/i, "Status message is correct");
3223 ($id, $msg) = $tt->SetStatus('resolved');
3224 ok($id, $msg);
3225 like($msg, qr/resolved/i, "Status message is correct");
3226 ($id, $msg) = $tt->SetStatus('resolved');
3227 ok(!$id,$msg);
3228
3229
3230 =end testing
3231
3232
3233 =cut
3234
3235 sub SetStatus {
3236     my $self   = shift;
3237     my %args;
3238
3239     if (@_ == 1) {
3240         $args{Status} = shift;
3241     }
3242     else {
3243         %args = (@_);
3244     }
3245
3246     #Check ACL
3247     if ( $args{Status} eq 'deleted') {
3248             unless ($self->CurrentUserHasRight('DeleteTicket')) {
3249             return ( 0, $self->loc('Permission Denied') );
3250        }
3251     } else {
3252             unless ($self->CurrentUserHasRight('ModifyTicket')) {
3253             return ( 0, $self->loc('Permission Denied') );
3254        }
3255     }
3256
3257     if (!$args{Force} && ($args{'Status'} eq 'resolved') && $self->HasUnresolvedDependencies) {
3258         return (0, $self->loc('That ticket has unresolved dependencies'));
3259     }
3260
3261     my $now = RT::Date->new( $self->CurrentUser );
3262     $now->SetToNow();
3263
3264     #If we're changing the status from new, record that we've started
3265     if ( ( $self->Status =~ /new/ ) && ( $args{Status} ne 'new' ) ) {
3266
3267         #Set the Started time to "now"
3268         $self->_Set( Field             => 'Started',
3269                      Value             => $now->ISO,
3270                      RecordTransaction => 0 );
3271     }
3272
3273     #When we close a ticket, set the 'Resolved' attribute to now.
3274     # It's misnamed, but that's just historical.
3275     if ( $self->QueueObj->IsInactiveStatus($args{Status}) ) {
3276         $self->_Set( Field             => 'Resolved',
3277                      Value             => $now->ISO,
3278                      RecordTransaction => 0 );
3279     }
3280
3281     #Actually update the status
3282    my ($val, $msg)= $self->_Set( Field           => 'Status',
3283                           Value           => $args{Status},
3284                           TimeTaken       => 0,
3285                           CheckACL      => 0,
3286                           TransactionType => 'Status'  );
3287
3288     return($val,$msg);
3289 }
3290
3291 # }}}
3292
3293 # {{{ sub Kill
3294
3295 =head2 Kill
3296
3297 Takes no arguments. Marks this ticket for garbage collection
3298
3299 =cut
3300
3301 sub Kill {
3302     my $self = shift;
3303     $RT::Logger->crit("'Kill' is deprecated. use 'Delete' instead at (". join(":",caller).").");
3304     return $self->Delete;
3305 }
3306
3307 sub Delete {
3308     my $self = shift;
3309     return ( $self->SetStatus('deleted') );
3310
3311     # TODO: garbage collection
3312 }
3313
3314 # }}}
3315
3316 # {{{ sub Stall
3317
3318 =head2 Stall
3319
3320 Sets this ticket's status to stalled
3321
3322 =cut
3323
3324 sub Stall {
3325     my $self = shift;
3326     return ( $self->SetStatus('stalled') );
3327 }
3328
3329 # }}}
3330
3331 # {{{ sub Reject
3332
3333 =head2 Reject
3334
3335 Sets this ticket's status to rejected
3336
3337 =cut
3338
3339 sub Reject {
3340     my $self = shift;
3341     return ( $self->SetStatus('rejected') );
3342 }
3343
3344 # }}}
3345
3346 # {{{ sub Open
3347
3348 =head2 Open
3349
3350 Sets this ticket\'s status to Open
3351
3352 =cut
3353
3354 sub Open {
3355     my $self = shift;
3356     return ( $self->SetStatus('open') );
3357 }
3358
3359 # }}}
3360
3361 # {{{ sub Resolve
3362
3363 =head2 Resolve
3364
3365 Sets this ticket\'s status to Resolved
3366
3367 =cut
3368
3369 sub Resolve {
3370     my $self = shift;
3371     return ( $self->SetStatus('resolved') );
3372 }
3373
3374 # }}}
3375
3376 # }}}
3377
3378         
3379 # {{{ Actions + Routines dealing with transactions
3380
3381 # {{{ sub SetTold and _SetTold
3382
3383 =head2 SetTold ISO  [TIMETAKEN]
3384
3385 Updates the told and records a transaction
3386
3387 =cut
3388
3389 sub SetTold {
3390     my $self = shift;
3391     my $told;
3392     $told = shift if (@_);
3393     my $timetaken = shift || 0;
3394
3395     unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
3396         return ( 0, $self->loc("Permission Denied") );
3397     }
3398
3399     my $datetold = new RT::Date( $self->CurrentUser );
3400     if ($told) {
3401         $datetold->Set( Format => 'iso',
3402                         Value  => $told );
3403     }
3404     else {
3405         $datetold->SetToNow();
3406     }
3407
3408     return ( $self->_Set( Field           => 'Told',
3409                           Value           => $datetold->ISO,
3410                           TimeTaken       => $timetaken,
3411                           TransactionType => 'Told' ) );
3412 }
3413
3414 =head2 _SetTold
3415
3416 Updates the told without a transaction or acl check. Useful when we're sending replies.
3417
3418 =cut
3419
3420 sub _SetTold {
3421     my $self = shift;
3422
3423     my $now = new RT::Date( $self->CurrentUser );
3424     $now->SetToNow();
3425
3426     #use __Set to get no ACLs ;)
3427     return ( $self->__Set( Field => 'Told',
3428                            Value => $now->ISO ) );
3429 }
3430
3431 # }}}
3432
3433 =head2 TransactionBatch
3434
3435   Returns an array reference of all transactions created on this ticket during
3436   this ticket object's lifetime, or undef if there were none.
3437
3438   Only works when the $RT::UseTransactionBatch config variable is set to true.
3439
3440 =cut
3441
3442 sub TransactionBatch {
3443     my $self = shift;
3444     return $self->{_TransactionBatch};
3445 }
3446
3447 sub DESTROY {
3448     my $self = shift;
3449
3450     # DESTROY methods need to localize $@, or it may unset it.  This
3451     # causes $m->abort to not bubble all of the way up.  See perlbug
3452     # http://rt.perl.org/rt3/Ticket/Display.html?id=17650
3453     local $@;
3454
3455     # The following line eliminates reentrancy.
3456     # It protects against the fact that perl doesn't deal gracefully
3457     # when an object's refcount is changed in its destructor.
3458     return if $self->{_Destroyed}++;
3459
3460     my $batch = $self->TransactionBatch or return;
3461     return unless @$batch;
3462
3463     require RT::Scrips;
3464     RT::Scrips->new($RT::SystemUser)->Apply(
3465         Stage           => 'TransactionBatch',
3466         TicketObj       => $self,
3467         TransactionObj  => $batch->[0],
3468         Type            => join(',', (map { $_->Type } @{$batch}) )
3469     );
3470 }
3471
3472 # }}}
3473
3474 # {{{ PRIVATE UTILITY METHODS. Mostly needed so Ticket can be a DBIx::Record
3475
3476 # {{{ sub _OverlayAccessible
3477
3478 sub _OverlayAccessible {
3479     {
3480         EffectiveId       => { 'read' => 1,  'write' => 1,  'public' => 1 },
3481           Queue           => { 'read' => 1,  'write' => 1 },
3482           Requestors      => { 'read' => 1,  'write' => 1 },
3483           Owner           => { 'read' => 1,  'write' => 1 },
3484           Subject         => { 'read' => 1,  'write' => 1 },
3485           InitialPriority => { 'read' => 1,  'write' => 1 },
3486           FinalPriority   => { 'read' => 1,  'write' => 1 },
3487           Priority        => { 'read' => 1,  'write' => 1 },
3488           Status          => { 'read' => 1,  'write' => 1 },
3489           TimeEstimated      => { 'read' => 1,  'write' => 1 },
3490           TimeWorked      => { 'read' => 1,  'write' => 1 },
3491           TimeLeft        => { 'read' => 1,  'write' => 1 },
3492           Told            => { 'read' => 1,  'write' => 1 },
3493           Resolved        => { 'read' => 1 },
3494           Type            => { 'read' => 1 },
3495           Starts        => { 'read' => 1, 'write' => 1 },
3496           Started       => { 'read' => 1, 'write' => 1 },
3497           Due           => { 'read' => 1, 'write' => 1 },
3498           Creator       => { 'read' => 1, 'auto'  => 1 },
3499           Created       => { 'read' => 1, 'auto'  => 1 },
3500           LastUpdatedBy => { 'read' => 1, 'auto'  => 1 },
3501           LastUpdated   => { 'read' => 1, 'auto'  => 1 }
3502     };
3503
3504 }
3505
3506 # }}}
3507
3508 # {{{ sub _Set
3509
3510 sub _Set {
3511     my $self = shift;
3512
3513     my %args = ( Field             => undef,
3514                  Value             => undef,
3515                  TimeTaken         => 0,
3516                  RecordTransaction => 1,
3517                  UpdateTicket      => 1,
3518                  CheckACL          => 1,
3519                  TransactionType   => 'Set',
3520                  @_ );
3521
3522     if ($args{'CheckACL'}) {
3523       unless ( $self->CurrentUserHasRight('ModifyTicket')) {
3524           return ( 0, $self->loc("Permission Denied"));
3525       }
3526    }
3527
3528     unless ($args{'UpdateTicket'} || $args{'RecordTransaction'}) {
3529         $RT::Logger->error("Ticket->_Set called without a mandate to record an update or update the ticket");
3530         return(0, $self->loc("Internal Error"));
3531     }
3532
3533     #if the user is trying to modify the record
3534
3535     #Take care of the old value we really don't want to get in an ACL loop.
3536     # so ask the super::_Value
3537     my $Old = $self->SUPER::_Value("$args{'Field'}");
3538     
3539     my ($ret, $msg);
3540     if ( $args{'UpdateTicket'}  ) {
3541
3542         #Set the new value
3543         ( $ret, $msg ) = $self->SUPER::_Set( Field => $args{'Field'},
3544                                                 Value => $args{'Value'} );
3545     
3546         #If we can't actually set the field to the value, don't record
3547         # a transaction. instead, get out of here.
3548         return ( 0, $msg ) unless $ret;
3549     }
3550
3551     if ( $args{'RecordTransaction'} == 1 ) {
3552
3553         my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction(
3554                                                Type => $args{'TransactionType'},
3555                                                Field     => $args{'Field'},
3556                                                NewValue  => $args{'Value'},
3557                                                OldValue  => $Old,
3558                                                TimeTaken => $args{'TimeTaken'},
3559         );
3560         return ( $Trans, scalar $TransObj->BriefDescription );
3561     }
3562     else {
3563         return ( $ret, $msg );
3564     }
3565 }
3566
3567 # }}}
3568
3569 # {{{ sub _Value 
3570
3571 =head2 _Value
3572
3573 Takes the name of a table column.
3574 Returns its value as a string, if the user passes an ACL check
3575
3576 =cut
3577
3578 sub _Value {
3579
3580     my $self  = shift;
3581     my $field = shift;
3582
3583     #if the field is public, return it.
3584     if ( $self->_Accessible( $field, 'public' ) ) {
3585
3586         #$RT::Logger->debug("Skipping ACL check for $field\n");
3587         return ( $self->SUPER::_Value($field) );
3588
3589     }
3590
3591     #If the current user doesn't have ACLs, don't let em at it.  
3592
3593     unless ( $self->CurrentUserHasRight('ShowTicket') ) {
3594         return (undef);
3595     }
3596     return ( $self->SUPER::_Value($field) );
3597
3598 }
3599
3600 # }}}
3601
3602 # {{{ sub _UpdateTimeTaken
3603
3604 =head2 _UpdateTimeTaken
3605
3606 This routine will increment the timeworked counter. it should
3607 only be called from _NewTransaction 
3608
3609 =cut
3610
3611 sub _UpdateTimeTaken {
3612     my $self    = shift;
3613     my $Minutes = shift;
3614     my ($Total);
3615
3616     $Total = $self->SUPER::_Value("TimeWorked");
3617     $Total = ( $Total || 0 ) + ( $Minutes || 0 );
3618     $self->SUPER::_Set(
3619         Field => "TimeWorked",
3620         Value => $Total
3621     );
3622
3623     return ($Total);
3624 }
3625
3626 # }}}
3627
3628 # }}}
3629
3630 # {{{ Routines dealing with ACCESS CONTROL
3631
3632 # {{{ sub CurrentUserHasRight 
3633
3634 =head2 CurrentUserHasRight
3635
3636   Takes the textual name of a Ticket scoped right (from RT::ACE) and returns
3637 1 if the user has that right. It returns 0 if the user doesn't have that right.
3638
3639 =cut
3640
3641 sub CurrentUserHasRight {
3642     my $self  = shift;
3643     my $right = shift;
3644
3645     return (
3646         $self->HasRight(
3647             Principal => $self->CurrentUser->UserObj(),
3648             Right     => "$right"
3649           )
3650     );
3651
3652 }
3653
3654 # }}}
3655
3656 # {{{ sub HasRight 
3657
3658 =head2 HasRight
3659
3660  Takes a paramhash with the attributes 'Right' and 'Principal'
3661   'Right' is a ticket-scoped textual right from RT::ACE 
3662   'Principal' is an RT::User object
3663
3664   Returns 1 if the principal has the right. Returns undef if not.
3665
3666 =cut
3667
3668 sub HasRight {
3669     my $self = shift;
3670     my %args = (
3671         Right     => undef,
3672         Principal => undef,
3673         @_
3674     );
3675
3676     unless ( ( defined $args{'Principal'} ) and ( ref( $args{'Principal'} ) ) )
3677     {
3678         Carp::cluck;
3679         $RT::Logger->crit("Principal attrib undefined for Ticket::HasRight");
3680         return(undef);
3681     }
3682
3683     return (
3684         $args{'Principal'}->HasRight(
3685             Object => $self,
3686             Right     => $args{'Right'}
3687           )
3688     );
3689 }
3690
3691 # }}}
3692
3693 # }}}
3694
3695 =head2 Reminders
3696
3697 Return the Reminders object for this ticket. (It's an RT::Reminders object.)
3698 It isn't acutally a searchbuilder collection itself.
3699
3700 =cut
3701
3702 sub Reminders {
3703     my $self = shift;
3704     
3705     unless ($self->{'__reminders'}) {
3706         $self->{'__reminders'} = RT::Reminders->new($self->CurrentUser);
3707         $self->{'__reminders'}->Ticket($self->id);
3708     }
3709     return $self->{'__reminders'};
3710
3711 }
3712
3713
3714
3715 # {{{ sub Transactions 
3716
3717 =head2 Transactions
3718
3719   Returns an RT::Transactions object of all transactions on this ticket
3720
3721 =cut
3722
3723 sub Transactions {
3724     my $self = shift;
3725
3726     my $transactions = RT::Transactions->new( $self->CurrentUser );
3727
3728     #If the user has no rights, return an empty object
3729     if ( $self->CurrentUserHasRight('ShowTicket') ) {
3730         $transactions->LimitToTicket($self->id);
3731
3732         # if the user may not see comments do not return them
3733         unless ( $self->CurrentUserHasRight('ShowTicketComments') ) {
3734             $transactions->Limit(
3735                 FIELD    => 'Type',
3736                 OPERATOR => '!=',
3737                 VALUE    => "Comment"
3738             );
3739             $transactions->Limit(
3740                 FIELD    => 'Type',
3741                 OPERATOR => '!=',
3742                 VALUE    => "CommentEmailRecord",
3743                 ENTRYAGGREGATOR => 'AND'
3744             );
3745
3746         }
3747     }
3748
3749     return ($transactions);
3750 }
3751
3752 # }}}
3753
3754
3755 # {{{ TransactionCustomFields
3756
3757 =head2 TransactionCustomFields
3758
3759     Returns the custom fields that transactions on tickets will have.
3760
3761 =cut
3762
3763 sub TransactionCustomFields {
3764     my $self = shift;
3765     return $self->QueueObj->TicketTransactionCustomFields;
3766 }
3767
3768 # }}}
3769
3770 # {{{ sub CustomFieldValues
3771
3772 =head2 CustomFieldValues
3773
3774 # Do name => id mapping (if needed) before falling back to
3775 # RT::Record's CustomFieldValues
3776
3777 See L<RT::Record>
3778
3779 =cut
3780
3781 sub CustomFieldValues {
3782     my $self  = shift;
3783     my $field = shift;
3784     if ( $field and $field !~ /^\d+$/ ) {
3785         my $cf = RT::CustomField->new( $self->CurrentUser );
3786         $cf->LoadByNameAndQueue( Name => $field, Queue => $self->Queue );
3787         unless ( $cf->id ) {
3788             $cf->LoadByNameAndQueue( Name => $field, Queue => 0 );
3789         }
3790         unless ( $cf->id ) {
3791             # If we didn't find a valid cfid, give up.
3792             return RT::CustomFieldValues->new($self->CurrentUser);
3793         }
3794     }
3795     return $self->SUPER::CustomFieldValues($field);
3796 }
3797
3798 # }}}
3799
3800 # {{{ sub CustomFieldLookupType
3801
3802 =head2 CustomFieldLookupType
3803
3804 Returns the RT::Ticket lookup type, which can be passed to 
3805 RT::CustomField->Create() via the 'LookupType' hash key.
3806
3807 =cut
3808
3809 # }}}
3810
3811 sub CustomFieldLookupType {
3812     "RT::Queue-RT::Ticket";
3813 }
3814
3815 1;
3816
3817 =head1 AUTHOR
3818
3819 Jesse Vincent, jesse@bestpractical.com
3820
3821 =head1 SEE ALSO
3822
3823 RT
3824
3825 =cut
3826