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