import rt 3.4.5
[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 ( $args{'Silent'} ) {
2505         return ( $val, $Msg );
2506     }
2507     else {
2508         my $remote_uri = RT::URI->new( $self->CurrentUser );
2509         $remote_uri->FromURI( $remote_link );
2510
2511         my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction(
2512             Type      => 'DeleteLink',
2513             Field => $LINKDIRMAP{$args{'Type'}}->{$direction},
2514             OldValue =>  $remote_uri->URI || $remote_link,
2515             TimeTaken => 0
2516         );
2517
2518         if ( $remote_uri->IsLocal ) {
2519
2520             my $OtherObj = $remote_uri->Object;
2521             my ( $val, $Msg ) = $OtherObj->_NewTransaction(Type  => 'DeleteLink',
2522                                                            Field => $direction eq 'Target' ? $LINKDIRMAP{$args{'Type'}}->{Base}
2523                                                                                            : $LINKDIRMAP{$args{'Type'}}->{Target},
2524                                                            OldValue => $self->URI,
2525                                                            ActivateScrips => ! $RT::LinkTransactionsRun1Scrip,
2526                                                            TimeTaken => 0 );
2527         }
2528
2529         return ( $Trans, $Msg );
2530     }
2531 }
2532
2533 # }}}
2534
2535 # {{{ sub AddLink
2536
2537 =head2 AddLink
2538
2539 Takes a paramhash of Type and one of Base or Target. Adds that link to this ticket.
2540
2541 =cut
2542
2543 sub AddLink {
2544     my $self = shift;
2545     my %args = ( Target => '',
2546                  Base   => '',
2547                  Type   => '',
2548                  Silent => undef,
2549                  @_ );
2550
2551
2552     unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
2553         return ( 0, $self->loc("Permission Denied") );
2554     }
2555
2556
2557     $self->_AddLink(%args);
2558 }
2559
2560 =head2 _AddLink  
2561
2562 Private non-acled variant of AddLink so that links can be added during create.
2563
2564 =cut
2565
2566 sub _AddLink {
2567     my $self = shift;
2568     my %args = ( Target => '',
2569                  Base   => '',
2570                  Type   => '',
2571                  Silent => undef,
2572                  @_ );
2573
2574     # {{{ If the other URI is an RT::Ticket, we want to make sure the user
2575     # can modify it too...
2576     my $other_ticket_uri = RT::URI->new($self->CurrentUser);
2577
2578     if ( $args{'Target'} ) {
2579         $other_ticket_uri->FromURI( $args{'Target'} );
2580
2581     }
2582     elsif ( $args{'Base'} ) {
2583         $other_ticket_uri->FromURI( $args{'Base'} );
2584     }
2585
2586     unless ( $other_ticket_uri->Resolver && $other_ticket_uri->Scheme ) {
2587         my $msg = $args{'Target'} ? $self->loc("Couldn't resolve target '[_1]' into a URI.", $args{'Target'})
2588           : $self->loc("Couldn't resolve base '[_1]' into a URI.", $args{'Base'});
2589         $RT::Logger->warning( "$self $msg\n" );
2590
2591         return( 0, $msg );
2592     }
2593
2594     if ( $other_ticket_uri->Resolver->Scheme eq 'fsck.com-rt') {
2595         my $object = $other_ticket_uri->Resolver->Object;
2596
2597         if (   UNIVERSAL::isa( $object, 'RT::Ticket' )
2598             && $object->id
2599             && !$object->CurrentUserHasRight('ModifyTicket') )
2600         {
2601             return ( 0, $self->loc("Permission Denied") );
2602         }
2603
2604     }
2605
2606     # }}}
2607
2608     my ($val, $Msg) = $self->SUPER::_AddLink(%args);
2609
2610     if (!$val) {
2611         return ($val, $Msg);
2612     }
2613
2614     my ($direction, $remote_link);
2615     if ( $args{'Target'} ) {
2616         $remote_link  = $args{'Target'};
2617         $direction    = 'Base';
2618     } elsif ( $args{'Base'} ) {
2619         $remote_link  = $args{'Base'};
2620         $direction    = 'Target';
2621     }
2622
2623     # Don't write the transaction if we're doing this on create
2624     if ( $args{'Silent'} ) {
2625         return ( $val, $Msg );
2626     }
2627     else {
2628         my $remote_uri = RT::URI->new( $self->CurrentUser );
2629         $remote_uri->FromURI( $remote_link );
2630
2631         #Write the transaction
2632         my ( $Trans, $Msg, $TransObj ) = 
2633             $self->_NewTransaction(Type  => 'AddLink',
2634                                    Field => $LINKDIRMAP{$args{'Type'}}->{$direction},
2635                                    NewValue =>  $remote_uri->URI || $remote_link,
2636                                    TimeTaken => 0 );
2637
2638         if ( $remote_uri->IsLocal ) {
2639
2640             my $OtherObj = $remote_uri->Object;
2641             my ( $val, $Msg ) = $OtherObj->_NewTransaction(Type  => 'AddLink',
2642                                                            Field => $direction eq 'Target' ? $LINKDIRMAP{$args{'Type'}}->{Base} 
2643                                                                                            : $LINKDIRMAP{$args{'Type'}}->{Target},
2644                                                            NewValue => $self->URI,
2645                                                            ActivateScrips => ! $RT::LinkTransactionsRun1Scrip,
2646                                                            TimeTaken => 0 );
2647         }
2648         return ( $val, $Msg );
2649     }
2650
2651 }
2652
2653 # }}}
2654
2655
2656 # {{{ sub MergeInto
2657
2658 =head2 MergeInto
2659
2660 MergeInto take the id of the ticket to merge this ticket into.
2661
2662
2663 =begin testing
2664
2665 my $t1 = RT::Ticket->new($RT::SystemUser);
2666 $t1->Create ( Subject => 'Merge test 1', Queue => 'general', Requestor => 'merge1@example.com');
2667 my $t1id = $t1->id;
2668 my $t2 = RT::Ticket->new($RT::SystemUser);
2669 $t2->Create ( Subject => 'Merge test 2', Queue => 'general', Requestor => 'merge2@example.com');
2670 my $t2id = $t2->id;
2671 my ($msg, $val) = $t1->MergeInto($t2->id);
2672 ok ($msg,$val);
2673 $t1 = RT::Ticket->new($RT::SystemUser);
2674 is ($t1->id, undef, "ok. we've got a blank ticket1");
2675 $t1->Load($t1id);
2676
2677 is ($t1->id, $t2->id);
2678
2679 is ($t1->Requestors->MembersObj->Count, 2);
2680
2681
2682 =end testing
2683
2684 =cut
2685
2686 sub MergeInto {
2687     my $self      = shift;
2688     my $ticket_id = shift;
2689
2690     unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
2691         return ( 0, $self->loc("Permission Denied") );
2692     }
2693
2694     # Load up the new ticket.
2695     my $MergeInto = RT::Ticket->new($RT::SystemUser);
2696     $MergeInto->Load($ticket_id);
2697
2698     # make sure it exists.
2699     unless ( $MergeInto->Id ) {
2700         return ( 0, $self->loc("New ticket doesn't exist") );
2701     }
2702
2703     # Make sure the current user can modify the new ticket.
2704     unless ( $MergeInto->CurrentUserHasRight('ModifyTicket') ) {
2705         return ( 0, $self->loc("Permission Denied") );
2706     }
2707
2708     $RT::Handle->BeginTransaction();
2709
2710     # We use EffectiveId here even though it duplicates information from
2711     # the links table becasue of the massive performance hit we'd take
2712     # by trying to do a separate database query for merge info everytime 
2713     # loaded a ticket. 
2714
2715     #update this ticket's effective id to the new ticket's id.
2716     my ( $id_val, $id_msg ) = $self->__Set(
2717         Field => 'EffectiveId',
2718         Value => $MergeInto->Id()
2719     );
2720
2721     unless ($id_val) {
2722         $RT::Handle->Rollback();
2723         return ( 0, $self->loc("Merge failed. Couldn't set EffectiveId") );
2724     }
2725
2726     my ( $status_val, $status_msg ) = $self->__Set( Field => 'Status', Value => 'resolved');
2727
2728     unless ($status_val) {
2729         $RT::Handle->Rollback();
2730         $RT::Logger->error( $self->loc("[_1] couldn't set status to resolved. RT's Database may be inconsistent.", $self) );
2731         return ( 0, $self->loc("Merge failed. Couldn't set Status") );
2732     }
2733
2734
2735     # update all the links that point to that old ticket
2736     my $old_links_to = RT::Links->new($self->CurrentUser);
2737     $old_links_to->Limit(FIELD => 'Target', VALUE => $self->URI);
2738
2739     my %old_seen;
2740     while (my $link = $old_links_to->Next) {
2741         if (exists $old_seen{$link->Base."-".$link->Type}) {
2742             $link->Delete;
2743         }   
2744         elsif ($link->Base eq $MergeInto->URI) {
2745             $link->Delete;
2746         } else {
2747             # First, make sure the link doesn't already exist. then move it over.
2748             my $tmp = RT::Link->new($RT::SystemUser);
2749             $tmp->LoadByCols(Base => $link->Base, Type => $link->Type, LocalTarget => $MergeInto->id);
2750             if ($tmp->id)   {
2751                     $link->Delete;
2752             } else { 
2753                 $link->SetTarget($MergeInto->URI);
2754                 $link->SetLocalTarget($MergeInto->id);
2755             }
2756             $old_seen{$link->Base."-".$link->Type} =1;
2757         }
2758
2759     }
2760
2761     my $old_links_from = RT::Links->new($self->CurrentUser);
2762     $old_links_from->Limit(FIELD => 'Base', VALUE => $self->URI);
2763
2764     while (my $link = $old_links_from->Next) {
2765         if (exists $old_seen{$link->Type."-".$link->Target}) {
2766             $link->Delete;
2767         }   
2768         if ($link->Target eq $MergeInto->URI) {
2769             $link->Delete;
2770         } else {
2771             # First, make sure the link doesn't already exist. then move it over.
2772             my $tmp = RT::Link->new($RT::SystemUser);
2773             $tmp->LoadByCols(Target => $link->Target, Type => $link->Type, LocalBase => $MergeInto->id);
2774             if ($tmp->id)   {
2775                     $link->Delete;
2776             } else { 
2777                 $link->SetBase($MergeInto->URI);
2778                 $link->SetLocalBase($MergeInto->id);
2779                 $old_seen{$link->Type."-".$link->Target} =1;
2780             }
2781         }
2782
2783     }
2784
2785     # Update time fields
2786     foreach my $type qw(TimeEstimated TimeWorked TimeLeft) {
2787
2788         my $mutator = "Set$type";
2789         $MergeInto->$mutator(
2790             ( $MergeInto->$type() || 0 ) + ( $self->$type() || 0 ) );
2791
2792     }
2793 #add all of this ticket's watchers to that ticket.
2794     foreach my $watcher_type qw(Requestors Cc AdminCc) {
2795
2796         my $people = $self->$watcher_type->MembersObj;
2797         my $addwatcher_type =  $watcher_type;
2798         $addwatcher_type  =~ s/s$//;
2799
2800         while ( my $watcher = $people->Next ) {
2801             
2802            my ($val, $msg) =  $MergeInto->_AddWatcher(
2803                 Type        => $addwatcher_type,
2804                 Silent => 1,
2805                 PrincipalId => $watcher->MemberId
2806             );
2807             unless ($val) {
2808                 $RT::Logger->warning($msg);
2809             }
2810     }
2811
2812     }
2813
2814     #find all of the tickets that were merged into this ticket. 
2815     my $old_mergees = new RT::Tickets( $self->CurrentUser );
2816     $old_mergees->Limit(
2817         FIELD    => 'EffectiveId',
2818         OPERATOR => '=',
2819         VALUE    => $self->Id
2820     );
2821
2822     #   update their EffectiveId fields to the new ticket's id
2823     while ( my $ticket = $old_mergees->Next() ) {
2824         my ( $val, $msg ) = $ticket->__Set(
2825             Field => 'EffectiveId',
2826             Value => $MergeInto->Id()
2827         );
2828     }
2829
2830     #make a new link: this ticket is merged into that other ticket.
2831     $self->AddLink( Type   => 'MergedInto', Target => $MergeInto->Id());
2832
2833     $MergeInto->_SetLastUpdated;    
2834
2835     $RT::Handle->Commit();
2836     return ( 1, $self->loc("Merge Successful") );
2837 }
2838
2839 # }}}
2840
2841 # }}}
2842
2843 # {{{ Routines dealing with ownership
2844
2845 # {{{ sub OwnerObj
2846
2847 =head2 OwnerObj
2848
2849 Takes nothing and returns an RT::User object of 
2850 this ticket's owner
2851
2852 =cut
2853
2854 sub OwnerObj {
2855     my $self = shift;
2856
2857     #If this gets ACLed, we lose on a rights check in User.pm and
2858     #get deep recursion. if we need ACLs here, we need
2859     #an equiv without ACLs
2860
2861     my $owner = new RT::User( $self->CurrentUser );
2862     $owner->Load( $self->__Value('Owner') );
2863
2864     #Return the owner object
2865     return ($owner);
2866 }
2867
2868 # }}}
2869
2870 # {{{ sub OwnerAsString 
2871
2872 =head2 OwnerAsString
2873
2874 Returns the owner's email address
2875
2876 =cut
2877
2878 sub OwnerAsString {
2879     my $self = shift;
2880     return ( $self->OwnerObj->EmailAddress );
2881
2882 }
2883
2884 # }}}
2885
2886 # {{{ sub SetOwner
2887
2888 =head2 SetOwner
2889
2890 Takes two arguments:
2891      the Id or Name of the owner 
2892 and  (optionally) the type of the SetOwner Transaction. It defaults
2893 to 'Give'.  'Steal' is also a valid option.
2894
2895 =begin testing
2896
2897 my $root = RT::User->new($RT::SystemUser);
2898 $root->Load('root');
2899 ok ($root->Id, "Loaded the root user");
2900 my $t = RT::Ticket->new($RT::SystemUser);
2901 $t->Load(1);
2902 $t->SetOwner('root');
2903 is ($t->OwnerObj->Name, 'root' , "Root owns the ticket");
2904 $t->Steal();
2905 is ($t->OwnerObj->id, $RT::SystemUser->id , "SystemUser owns the ticket");
2906 my $txns = RT::Transactions->new($RT::SystemUser);
2907 $txns->OrderBy(FIELD => 'id', ORDER => 'DESC');
2908 $txns->Limit(FIELD => 'ObjectId', VALUE => '1');
2909 $txns->Limit(FIELD => 'ObjectType', VALUE => 'RT::Ticket');
2910 my $steal  = $txns->First;
2911 ok($steal->OldValue == $root->Id , "Stolen from root");
2912 ok($steal->NewValue == $RT::SystemUser->Id , "Stolen by the systemuser");
2913
2914 =end testing
2915
2916 =cut
2917
2918 sub SetOwner {
2919     my $self     = shift;
2920     my $NewOwner = shift;
2921     my $Type     = shift || "Give";
2922
2923     # must have ModifyTicket rights
2924     # or TakeTicket/StealTicket and $NewOwner is self
2925     # see if it's a take
2926     if ( $self->OwnerObj->Id == $RT::Nobody->Id ) {
2927         unless (    $self->CurrentUserHasRight('ModifyTicket')
2928                  || $self->CurrentUserHasRight('TakeTicket') ) {
2929             return ( 0, $self->loc("Permission Denied") );
2930         }
2931     }
2932
2933     # see if it's a steal
2934     elsif (    $self->OwnerObj->Id != $RT::Nobody->Id
2935             && $self->OwnerObj->Id != $self->CurrentUser->id ) {
2936
2937         unless (    $self->CurrentUserHasRight('ModifyTicket')
2938                  || $self->CurrentUserHasRight('StealTicket') ) {
2939             return ( 0, $self->loc("Permission Denied") );
2940         }
2941     }
2942     else {
2943         unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
2944             return ( 0, $self->loc("Permission Denied") );
2945         }
2946     }
2947     my $NewOwnerObj = RT::User->new( $self->CurrentUser );
2948     my $OldOwnerObj = $self->OwnerObj;
2949
2950     $NewOwnerObj->Load($NewOwner);
2951     if ( !$NewOwnerObj->Id ) {
2952         return ( 0, $self->loc("That user does not exist") );
2953     }
2954
2955     #If thie ticket has an owner and it's not the current user
2956
2957     if (    ( $Type ne 'Steal' )
2958         and ( $Type ne 'Force' )
2959         and    #If we're not stealing
2960         ( $self->OwnerObj->Id != $RT::Nobody->Id ) and    #and the owner is set
2961         ( $self->CurrentUser->Id ne $self->OwnerObj->Id() )
2962       ) {                                                 #and it's not us
2963         return ( 0,
2964                  $self->loc(
2965 "You can only reassign tickets that you own or that are unowned" ) );
2966     }
2967
2968     #If we've specified a new owner and that user can't modify the ticket
2969     elsif ( ( $NewOwnerObj->Id )
2970             and ( !$NewOwnerObj->HasRight( Right  => 'OwnTicket',
2971                                            Object => $self ) )
2972       ) {
2973         return ( 0, $self->loc("That user may not own tickets in that queue") );
2974     }
2975
2976     #If the ticket has an owner and it's the new owner, we don't need
2977     #To do anything
2978     elsif (     ( $self->OwnerObj )
2979             and ( $NewOwnerObj->Id eq $self->OwnerObj->Id ) ) {
2980         return ( 0, $self->loc("That user already owns that ticket") );
2981     }
2982
2983     $RT::Handle->BeginTransaction();
2984
2985     # Delete the owner in the owner group, then add a new one
2986     # TODO: is this safe? it's not how we really want the API to work
2987     # for most things, but it's fast.
2988     my ( $del_id, $del_msg ) = $self->OwnerGroup->MembersObj->First->Delete();
2989     unless ($del_id) {
2990         $RT::Handle->Rollback();
2991         return ( 0, $self->loc("Could not change owner. ") . $del_msg );
2992     }
2993
2994     my ( $add_id, $add_msg ) = $self->OwnerGroup->_AddMember(
2995                                        PrincipalId => $NewOwnerObj->PrincipalId,
2996                                        InsideTransaction => 1 );
2997     unless ($add_id) {
2998         $RT::Handle->Rollback();
2999         return ( 0, $self->loc("Could not change owner. ") . $add_msg );
3000     }
3001
3002     # We call set twice with slightly different arguments, so
3003     # as to not have an SQL transaction span two RT transactions
3004
3005     my ( $val, $msg ) = $self->_Set(
3006                       Field             => 'Owner',
3007                       RecordTransaction => 0,
3008                       Value             => $NewOwnerObj->Id,
3009                       TimeTaken         => 0,
3010                       TransactionType   => $Type,
3011                       CheckACL          => 0,                  # don't check acl
3012     );
3013
3014     unless ($val) {
3015         $RT::Handle->Rollback;
3016         return ( 0, $self->loc("Could not change owner. ") . $msg );
3017     }
3018
3019     $RT::Handle->Commit();
3020
3021     my ( $trans, $msg, undef ) = $self->_NewTransaction(
3022                                                    Type     => $Type,
3023                                                    Field    => 'Owner',
3024                                                    NewValue => $NewOwnerObj->Id,
3025                                                    OldValue => $OldOwnerObj->Id,
3026                                                    TimeTaken => 0 );
3027
3028     if ($trans) {
3029         $msg = $self->loc( "Owner changed from [_1] to [_2]",
3030                            $OldOwnerObj->Name, $NewOwnerObj->Name );
3031
3032         # TODO: make sure the trans committed properly
3033     }
3034     return ( $trans, $msg );
3035
3036 }
3037
3038 # }}}
3039
3040 # {{{ sub Take
3041
3042 =head2 Take
3043
3044 A convenince method to set the ticket's owner to the current user
3045
3046 =cut
3047
3048 sub Take {
3049     my $self = shift;
3050     return ( $self->SetOwner( $self->CurrentUser->Id, 'Take' ) );
3051 }
3052
3053 # }}}
3054
3055 # {{{ sub Untake
3056
3057 =head2 Untake
3058
3059 Convenience method to set the owner to 'nobody' if the current user is the owner.
3060
3061 =cut
3062
3063 sub Untake {
3064     my $self = shift;
3065     return ( $self->SetOwner( $RT::Nobody->UserObj->Id, 'Untake' ) );
3066 }
3067
3068 # }}}
3069
3070 # {{{ sub Steal 
3071
3072 =head2 Steal
3073
3074 A convenience method to change the owner of the current ticket to the
3075 current user. Even if it's owned by another user.
3076
3077 =cut
3078
3079 sub Steal {
3080     my $self = shift;
3081
3082     if ( $self->IsOwner( $self->CurrentUser ) ) {
3083         return ( 0, $self->loc("You already own this ticket") );
3084     }
3085     else {
3086         return ( $self->SetOwner( $self->CurrentUser->Id, 'Steal' ) );
3087
3088     }
3089
3090 }
3091
3092 # }}}
3093
3094 # }}}
3095
3096 # {{{ Routines dealing with status
3097
3098 # {{{ sub ValidateStatus 
3099
3100 =head2 ValidateStatus STATUS
3101
3102 Takes a string. Returns true if that status is a valid status for this ticket.
3103 Returns false otherwise.
3104
3105 =cut
3106
3107 sub ValidateStatus {
3108     my $self   = shift;
3109     my $status = shift;
3110
3111     #Make sure the status passed in is valid
3112     unless ( $self->QueueObj->IsValidStatus($status) ) {
3113         return (undef);
3114     }
3115
3116     return (1);
3117
3118 }
3119
3120 # }}}
3121
3122 # {{{ sub SetStatus
3123
3124 =head2 SetStatus STATUS
3125
3126 Set this ticket\'s status. STATUS can be one of: new, open, stalled, resolved, rejected or deleted.
3127
3128 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.
3129
3130 =begin testing
3131
3132 my $tt = RT::Ticket->new($RT::SystemUser);
3133 my ($id, $tid, $msg)= $tt->Create(Queue => 'general',
3134             Subject => 'test');
3135 ok($id, $msg);
3136 is($tt->Status, 'new', "New ticket is created as new");
3137
3138 ($id, $msg) = $tt->SetStatus('open');
3139 ok($id, $msg);
3140 like($msg, qr/open/i, "Status message is correct");
3141 ($id, $msg) = $tt->SetStatus('resolved');
3142 ok($id, $msg);
3143 like($msg, qr/resolved/i, "Status message is correct");
3144 ($id, $msg) = $tt->SetStatus('resolved');
3145 ok(!$id,$msg);
3146
3147
3148 =end testing
3149
3150
3151 =cut
3152
3153 sub SetStatus {
3154     my $self   = shift;
3155     my %args;
3156
3157     if (@_ == 1) {
3158         $args{Status} = shift;
3159     }
3160     else {
3161         %args = (@_);
3162     }
3163
3164     #Check ACL
3165     if ( $args{Status} eq 'deleted') {
3166             unless ($self->CurrentUserHasRight('DeleteTicket')) {
3167             return ( 0, $self->loc('Permission Denied') );
3168        }
3169     } else {
3170             unless ($self->CurrentUserHasRight('ModifyTicket')) {
3171             return ( 0, $self->loc('Permission Denied') );
3172        }
3173     }
3174
3175     if (!$args{Force} && ($args{'Status'} eq 'resolved') && $self->HasUnresolvedDependencies) {
3176         return (0, $self->loc('That ticket has unresolved dependencies'));
3177     }
3178
3179     my $now = RT::Date->new( $self->CurrentUser );
3180     $now->SetToNow();
3181
3182     #If we're changing the status from new, record that we've started
3183     if ( ( $self->Status =~ /new/ ) && ( $args{Status} ne 'new' ) ) {
3184
3185         #Set the Started time to "now"
3186         $self->_Set( Field             => 'Started',
3187                      Value             => $now->ISO,
3188                      RecordTransaction => 0 );
3189     }
3190
3191     #When we close a ticket, set the 'Resolved' attribute to now.
3192     # It's misnamed, but that's just historical.
3193     if ( $self->QueueObj->IsInactiveStatus($args{Status}) ) {
3194         $self->_Set( Field             => 'Resolved',
3195                      Value             => $now->ISO,
3196                      RecordTransaction => 0 );
3197     }
3198
3199     #Actually update the status
3200    my ($val, $msg)= $self->_Set( Field           => 'Status',
3201                           Value           => $args{Status},
3202                           TimeTaken       => 0,
3203                           CheckACL      => 0,
3204                           TransactionType => 'Status'  );
3205
3206     return($val,$msg);
3207 }
3208
3209 # }}}
3210
3211 # {{{ sub Kill
3212
3213 =head2 Kill
3214
3215 Takes no arguments. Marks this ticket for garbage collection
3216
3217 =cut
3218
3219 sub Kill {
3220     my $self = shift;
3221     $RT::Logger->crit("'Kill' is deprecated. use 'Delete' instead at (". join(":",caller).").");
3222     return $self->Delete;
3223 }
3224
3225 sub Delete {
3226     my $self = shift;
3227     return ( $self->SetStatus('deleted') );
3228
3229     # TODO: garbage collection
3230 }
3231
3232 # }}}
3233
3234 # {{{ sub Stall
3235
3236 =head2 Stall
3237
3238 Sets this ticket's status to stalled
3239
3240 =cut
3241
3242 sub Stall {
3243     my $self = shift;
3244     return ( $self->SetStatus('stalled') );
3245 }
3246
3247 # }}}
3248
3249 # {{{ sub Reject
3250
3251 =head2 Reject
3252
3253 Sets this ticket's status to rejected
3254
3255 =cut
3256
3257 sub Reject {
3258     my $self = shift;
3259     return ( $self->SetStatus('rejected') );
3260 }
3261
3262 # }}}
3263
3264 # {{{ sub Open
3265
3266 =head2 Open
3267
3268 Sets this ticket\'s status to Open
3269
3270 =cut
3271
3272 sub Open {
3273     my $self = shift;
3274     return ( $self->SetStatus('open') );
3275 }
3276
3277 # }}}
3278
3279 # {{{ sub Resolve
3280
3281 =head2 Resolve
3282
3283 Sets this ticket\'s status to Resolved
3284
3285 =cut
3286
3287 sub Resolve {
3288     my $self = shift;
3289     return ( $self->SetStatus('resolved') );
3290 }
3291
3292 # }}}
3293
3294 # }}}
3295
3296         
3297 # {{{ Actions + Routines dealing with transactions
3298
3299 # {{{ sub SetTold and _SetTold
3300
3301 =head2 SetTold ISO  [TIMETAKEN]
3302
3303 Updates the told and records a transaction
3304
3305 =cut
3306
3307 sub SetTold {
3308     my $self = shift;
3309     my $told;
3310     $told = shift if (@_);
3311     my $timetaken = shift || 0;
3312
3313     unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
3314         return ( 0, $self->loc("Permission Denied") );
3315     }
3316
3317     my $datetold = new RT::Date( $self->CurrentUser );
3318     if ($told) {
3319         $datetold->Set( Format => 'iso',
3320                         Value  => $told );
3321     }
3322     else {
3323         $datetold->SetToNow();
3324     }
3325
3326     return ( $self->_Set( Field           => 'Told',
3327                           Value           => $datetold->ISO,
3328                           TimeTaken       => $timetaken,
3329                           TransactionType => 'Told' ) );
3330 }
3331
3332 =head2 _SetTold
3333
3334 Updates the told without a transaction or acl check. Useful when we're sending replies.
3335
3336 =cut
3337
3338 sub _SetTold {
3339     my $self = shift;
3340
3341     my $now = new RT::Date( $self->CurrentUser );
3342     $now->SetToNow();
3343
3344     #use __Set to get no ACLs ;)
3345     return ( $self->__Set( Field => 'Told',
3346                            Value => $now->ISO ) );
3347 }
3348
3349 # }}}
3350
3351 =head2 TransactionBatch
3352
3353   Returns an array reference of all transactions created on this ticket during
3354   this ticket object's lifetime, or undef if there were none.
3355
3356   Only works when the $RT::UseTransactionBatch config variable is set to true.
3357
3358 =cut
3359
3360 sub TransactionBatch {
3361     my $self = shift;
3362     return $self->{_TransactionBatch};
3363 }
3364
3365 sub DESTROY {
3366     my $self = shift;
3367
3368     # DESTROY methods need to localize $@, or it may unset it.  This
3369     # causes $m->abort to not bubble all of the way up.  See perlbug
3370     # http://rt.perl.org/rt3/Ticket/Display.html?id=17650
3371     local $@;
3372
3373     # The following line eliminates reentrancy.
3374     # It protects against the fact that perl doesn't deal gracefully
3375     # when an object's refcount is changed in its destructor.
3376     return if $self->{_Destroyed}++;
3377
3378     my $batch = $self->TransactionBatch or return;
3379     require RT::Scrips;
3380     RT::Scrips->new($RT::SystemUser)->Apply(
3381         Stage           => 'TransactionBatch',
3382         TicketObj       => $self,
3383         TransactionObj  => $batch->[0],
3384         Type            => join(',', (map { $_->Type } @{$batch}) )
3385     );
3386 }
3387
3388 # }}}
3389
3390 # {{{ PRIVATE UTILITY METHODS. Mostly needed so Ticket can be a DBIx::Record
3391
3392 # {{{ sub _OverlayAccessible
3393
3394 sub _OverlayAccessible {
3395     {
3396         EffectiveId       => { 'read' => 1,  'write' => 1,  'public' => 1 },
3397           Queue           => { 'read' => 1,  'write' => 1 },
3398           Requestors      => { 'read' => 1,  'write' => 1 },
3399           Owner           => { 'read' => 1,  'write' => 1 },
3400           Subject         => { 'read' => 1,  'write' => 1 },
3401           InitialPriority => { 'read' => 1,  'write' => 1 },
3402           FinalPriority   => { 'read' => 1,  'write' => 1 },
3403           Priority        => { 'read' => 1,  'write' => 1 },
3404           Status          => { 'read' => 1,  'write' => 1 },
3405           TimeEstimated      => { 'read' => 1,  'write' => 1 },
3406           TimeWorked      => { 'read' => 1,  'write' => 1 },
3407           TimeLeft        => { 'read' => 1,  'write' => 1 },
3408           Told            => { 'read' => 1,  'write' => 1 },
3409           Resolved        => { 'read' => 1 },
3410           Type            => { 'read' => 1 },
3411           Starts        => { 'read' => 1, 'write' => 1 },
3412           Started       => { 'read' => 1, 'write' => 1 },
3413           Due           => { 'read' => 1, 'write' => 1 },
3414           Creator       => { 'read' => 1, 'auto'  => 1 },
3415           Created       => { 'read' => 1, 'auto'  => 1 },
3416           LastUpdatedBy => { 'read' => 1, 'auto'  => 1 },
3417           LastUpdated   => { 'read' => 1, 'auto'  => 1 }
3418     };
3419
3420 }
3421
3422 # }}}
3423
3424 # {{{ sub _Set
3425
3426 sub _Set {
3427     my $self = shift;
3428
3429     my %args = ( Field             => undef,
3430                  Value             => undef,
3431                  TimeTaken         => 0,
3432                  RecordTransaction => 1,
3433                  UpdateTicket      => 1,
3434                  CheckACL          => 1,
3435                  TransactionType   => 'Set',
3436                  @_ );
3437
3438     if ($args{'CheckACL'}) {
3439       unless ( $self->CurrentUserHasRight('ModifyTicket')) {
3440           return ( 0, $self->loc("Permission Denied"));
3441       }
3442    }
3443
3444     unless ($args{'UpdateTicket'} || $args{'RecordTransaction'}) {
3445         $RT::Logger->error("Ticket->_Set called without a mandate to record an update or update the ticket");
3446         return(0, $self->loc("Internal Error"));
3447     }
3448
3449     #if the user is trying to modify the record
3450
3451     #Take care of the old value we really don't want to get in an ACL loop.
3452     # so ask the super::_Value
3453     my $Old = $self->SUPER::_Value("$args{'Field'}");
3454     
3455     my ($ret, $msg);
3456     if ( $args{'UpdateTicket'}  ) {
3457
3458         #Set the new value
3459         ( $ret, $msg ) = $self->SUPER::_Set( Field => $args{'Field'},
3460                                                 Value => $args{'Value'} );
3461     
3462         #If we can't actually set the field to the value, don't record
3463         # a transaction. instead, get out of here.
3464         return ( 0, $msg ) unless $ret;
3465     }
3466
3467     if ( $args{'RecordTransaction'} == 1 ) {
3468
3469         my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction(
3470                                                Type => $args{'TransactionType'},
3471                                                Field     => $args{'Field'},
3472                                                NewValue  => $args{'Value'},
3473                                                OldValue  => $Old,
3474                                                TimeTaken => $args{'TimeTaken'},
3475         );
3476         return ( $Trans, scalar $TransObj->BriefDescription );
3477     }
3478     else {
3479         return ( $ret, $msg );
3480     }
3481 }
3482
3483 # }}}
3484
3485 # {{{ sub _Value 
3486
3487 =head2 _Value
3488
3489 Takes the name of a table column.
3490 Returns its value as a string, if the user passes an ACL check
3491
3492 =cut
3493
3494 sub _Value {
3495
3496     my $self  = shift;
3497     my $field = shift;
3498
3499     #if the field is public, return it.
3500     if ( $self->_Accessible( $field, 'public' ) ) {
3501
3502         #$RT::Logger->debug("Skipping ACL check for $field\n");
3503         return ( $self->SUPER::_Value($field) );
3504
3505     }
3506
3507     #If the current user doesn't have ACLs, don't let em at it.  
3508
3509     unless ( $self->CurrentUserHasRight('ShowTicket') ) {
3510         return (undef);
3511     }
3512     return ( $self->SUPER::_Value($field) );
3513
3514 }
3515
3516 # }}}
3517
3518 # {{{ sub _UpdateTimeTaken
3519
3520 =head2 _UpdateTimeTaken
3521
3522 This routine will increment the timeworked counter. it should
3523 only be called from _NewTransaction 
3524
3525 =cut
3526
3527 sub _UpdateTimeTaken {
3528     my $self    = shift;
3529     my $Minutes = shift;
3530     my ($Total);
3531
3532     $Total = $self->SUPER::_Value("TimeWorked");
3533     $Total = ( $Total || 0 ) + ( $Minutes || 0 );
3534     $self->SUPER::_Set(
3535         Field => "TimeWorked",
3536         Value => $Total
3537     );
3538
3539     return ($Total);
3540 }
3541
3542 # }}}
3543
3544 # }}}
3545
3546 # {{{ Routines dealing with ACCESS CONTROL
3547
3548 # {{{ sub CurrentUserHasRight 
3549
3550 =head2 CurrentUserHasRight
3551
3552   Takes the textual name of a Ticket scoped right (from RT::ACE) and returns
3553 1 if the user has that right. It returns 0 if the user doesn't have that right.
3554
3555 =cut
3556
3557 sub CurrentUserHasRight {
3558     my $self  = shift;
3559     my $right = shift;
3560
3561     return (
3562         $self->HasRight(
3563             Principal => $self->CurrentUser->UserObj(),
3564             Right     => "$right"
3565           )
3566     );
3567
3568 }
3569
3570 # }}}
3571
3572 # {{{ sub HasRight 
3573
3574 =head2 HasRight
3575
3576  Takes a paramhash with the attributes 'Right' and 'Principal'
3577   'Right' is a ticket-scoped textual right from RT::ACE 
3578   'Principal' is an RT::User object
3579
3580   Returns 1 if the principal has the right. Returns undef if not.
3581
3582 =cut
3583
3584 sub HasRight {
3585     my $self = shift;
3586     my %args = (
3587         Right     => undef,
3588         Principal => undef,
3589         @_
3590     );
3591
3592     unless ( ( defined $args{'Principal'} ) and ( ref( $args{'Principal'} ) ) )
3593     {
3594         Carp::cluck;
3595         $RT::Logger->crit("Principal attrib undefined for Ticket::HasRight");
3596         return(undef);
3597     }
3598
3599     return (
3600         $args{'Principal'}->HasRight(
3601             Object => $self,
3602             Right     => $args{'Right'}
3603           )
3604     );
3605 }
3606
3607 # }}}
3608
3609 # }}}
3610
3611 # {{{ sub Transactions 
3612
3613 =head2 Transactions
3614
3615   Returns an RT::Transactions object of all transactions on this ticket
3616
3617 =cut
3618
3619 sub Transactions {
3620     my $self = shift;
3621
3622     my $transactions = RT::Transactions->new( $self->CurrentUser );
3623
3624     #If the user has no rights, return an empty object
3625     if ( $self->CurrentUserHasRight('ShowTicket') ) {
3626         $transactions->LimitToTicket($self->id);
3627
3628         # if the user may not see comments do not return them
3629         unless ( $self->CurrentUserHasRight('ShowTicketComments') ) {
3630             $transactions->Limit(
3631                 FIELD    => 'Type',
3632                 OPERATOR => '!=',
3633                 VALUE    => "Comment"
3634             );
3635             $transactions->Limit(
3636                 FIELD    => 'Type',
3637                 OPERATOR => '!=',
3638                 VALUE    => "CommentEmailRecord",
3639                 ENTRYAGGREGATOR => 'AND'
3640             );
3641
3642         }
3643     }
3644
3645     return ($transactions);
3646 }
3647
3648 # }}}
3649
3650
3651 # {{{ TransactionCustomFields
3652
3653 =head2 TransactionCustomFields
3654
3655     Returns the custom fields that transactions on tickets will ahve.
3656
3657 =cut
3658
3659 sub TransactionCustomFields {
3660     my $self = shift;
3661     return $self->QueueObj->TicketTransactionCustomFields;
3662 }
3663
3664 # }}}
3665
3666 # {{{ sub CustomFieldValues
3667
3668 =head2 CustomFieldValues
3669
3670 # Do name => id mapping (if needed) before falling back to
3671 # RT::Record's CustomFieldValues
3672
3673 See L<RT::Record>
3674
3675 =cut
3676
3677 sub CustomFieldValues {
3678     my $self  = shift;
3679     my $field = shift;
3680     if ( $field and $field !~ /^\d+$/ ) {
3681         my $cf = RT::CustomField->new( $self->CurrentUser );
3682         $cf->LoadByNameAndQueue( Name => $field, Queue => $self->QueueObj->Id );
3683         unless ( $cf->id ) {
3684             $cf->LoadByNameAndQueue( Name => $field, Queue => '0' );
3685         }
3686         $field = $cf->id;
3687         unless ( $field =~ /^\d+$/ ) {
3688           # If we didn't find a valid cfid, give up.
3689           return RT::CustomFieldValues->new($self->CurrentUser);
3690         }
3691     }
3692     return $self->SUPER::CustomFieldValues($field);
3693 }
3694
3695 # }}}
3696
3697 # {{{ sub CustomFieldLookupType
3698
3699 =head2 CustomFieldLookupType
3700
3701 Returns the RT::Ticket lookup type, which can be passed to 
3702 RT::CustomField->Create() via the 'LookupType' hash key.
3703
3704 =cut
3705
3706 # }}}
3707
3708 sub CustomFieldLookupType {
3709     "RT::Queue-RT::Ticket";
3710 }
3711
3712 1;
3713
3714 =head1 AUTHOR
3715
3716 Jesse Vincent, jesse@bestpractical.com
3717
3718 =head1 SEE ALSO
3719
3720 RT
3721
3722 =cut
3723