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