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