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