import rt 3.4.6
[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 ( $RT::TicketBaseURI && $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 ( my $due_in = $QueueObj->DefaultDueIn ) {
444         $Due->SetToNow;
445         $Due->AddDays( $due_in );
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     # XXX, FIXME, BUG: if only email is provided then we only check
1319     # for ModifyTicket right, but must try to get PrincipalId and
1320     # check Watch* rights too if user exist
1321
1322     # {{{ Check ACLS
1323     #If the watcher we're trying to add is for the current user
1324     if ( $self->CurrentUser->PrincipalId  eq $args{'PrincipalId'}) {
1325         #  If it's an AdminCc and they don't have 
1326         #   'WatchAsAdminCc' or 'ModifyTicket', bail
1327         if ( $args{'Type'} eq 'AdminCc' ) {
1328             unless ( $self->CurrentUserHasRight('ModifyTicket')
1329                 or $self->CurrentUserHasRight('WatchAsAdminCc') ) {
1330                 return ( 0, $self->loc('Permission Denied'))
1331             }
1332         }
1333
1334         #  If it's a Requestor or Cc and they don't have
1335         #   'Watch' or 'ModifyTicket', bail
1336         elsif ( ( $args{'Type'} eq 'Cc' ) or ( $args{'Type'} eq 'Requestor' ) ) {
1337
1338             unless ( $self->CurrentUserHasRight('ModifyTicket')
1339                 or $self->CurrentUserHasRight('Watch') ) {
1340                 return ( 0, $self->loc('Permission Denied'))
1341             }
1342         }
1343         else {
1344             $RT::Logger->warning( "$self -> AddWatcher got passed a bogus type");
1345             return ( 0, $self->loc('Error in parameters to Ticket->AddWatcher') );
1346         }
1347     }
1348
1349     # If the watcher isn't the current user 
1350     # and the current user  doesn't have 'ModifyTicket'
1351     # bail
1352     else {
1353         unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
1354             return ( 0, $self->loc("Permission Denied") );
1355         }
1356     }
1357
1358     # }}}
1359
1360     return ( $self->_AddWatcher(%args) );
1361 }
1362
1363 #This contains the meat of AddWatcher. but can be called from a routine like
1364 # Create, which doesn't need the additional acl check
1365 sub _AddWatcher {
1366     my $self = shift;
1367     my %args = (
1368         Type   => undef,
1369         Silent => undef,
1370         PrincipalId => undef,
1371         Email => undef,
1372         @_
1373     );
1374
1375
1376     my $principal = RT::Principal->new($self->CurrentUser);
1377     if ($args{'Email'}) {
1378         my $user = RT::User->new($RT::SystemUser);
1379         my ($pid, $msg) = $user->LoadOrCreateByEmail($args{'Email'});
1380         # If we can't load the user by email address, let's try to load by username     
1381         unless ($pid) { 
1382                 ($pid,$msg) = $user->Load($args{'Email'})
1383         }
1384         if ($pid) {
1385             $args{'PrincipalId'} = $pid; 
1386         }
1387     }
1388     if ($args{'PrincipalId'}) {
1389         $principal->Load($args{'PrincipalId'});
1390     } 
1391
1392  
1393     # If we can't find this watcher, we need to bail.
1394     unless ($principal->Id) {
1395             $RT::Logger->error("Could not load create a user with the email address '".$args{'Email'}. "' to add as a watcher for ticket ".$self->Id);
1396         return(0, $self->loc("Could not find or create that user"));
1397     }
1398
1399
1400     my $group = RT::Group->new($self->CurrentUser);
1401     $group->LoadTicketRoleGroup(Type => $args{'Type'}, Ticket => $self->Id);
1402     unless ($group->id) {
1403         return(0,$self->loc("Group not found"));
1404     }
1405
1406     if ( $group->HasMember( $principal)) {
1407
1408         return ( 0, $self->loc('That principal is already a [_1] for this ticket', $self->loc($args{'Type'})) );
1409     }
1410
1411
1412     my ( $m_id, $m_msg ) = $group->_AddMember( PrincipalId => $principal->Id,
1413                                                InsideTransaction => 1 );
1414     unless ($m_id) {
1415         $RT::Logger->error("Failed to add ".$principal->Id." as a member of group ".$group->Id."\n".$m_msg);
1416
1417         return ( 0, $self->loc('Could not make that principal a [_1] for this ticket', $self->loc($args{'Type'})) );
1418     }
1419
1420     unless ( $args{'Silent'} ) {
1421         $self->_NewTransaction(
1422             Type     => 'AddWatcher',
1423             NewValue => $principal->Id,
1424             Field    => $args{'Type'}
1425         );
1426     }
1427
1428         return ( 1, $self->loc('Added principal as a [_1] for this ticket', $self->loc($args{'Type'})) );
1429 }
1430
1431 # }}}
1432
1433
1434 # {{{ sub DeleteWatcher
1435
1436 =head2 DeleteWatcher { Type => TYPE, PrincipalId => PRINCIPAL_ID, Email => EMAIL_ADDRESS }
1437
1438
1439 Deletes a Ticket watcher.  Takes two arguments:
1440
1441 Type  (one of Requestor,Cc,AdminCc)
1442
1443 and one of
1444
1445 PrincipalId (an RT::Principal Id of the watcher you want to remove)
1446     OR
1447 Email (the email address of an existing wathcer)
1448
1449
1450 =cut
1451
1452
1453 sub DeleteWatcher {
1454     my $self = shift;
1455
1456     my %args = ( Type        => undef,
1457                  PrincipalId => undef,
1458                  Email       => undef,
1459                  @_ );
1460
1461     unless ( $args{'PrincipalId'} || $args{'Email'} ) {
1462         return ( 0, $self->loc("No principal specified") );
1463     }
1464     my $principal = RT::Principal->new( $self->CurrentUser );
1465     if ( $args{'PrincipalId'} ) {
1466
1467         $principal->Load( $args{'PrincipalId'} );
1468     }
1469     else {
1470         my $user = RT::User->new( $self->CurrentUser );
1471         $user->LoadByEmail( $args{'Email'} );
1472         $principal->Load( $user->Id );
1473     }
1474
1475     # If we can't find this watcher, we need to bail.
1476     unless ( $principal->Id ) {
1477         return ( 0, $self->loc("Could not find that principal") );
1478     }
1479
1480     my $group = RT::Group->new( $self->CurrentUser );
1481     $group->LoadTicketRoleGroup( Type => $args{'Type'}, Ticket => $self->Id );
1482     unless ( $group->id ) {
1483         return ( 0, $self->loc("Group not found") );
1484     }
1485
1486     # {{{ Check ACLS
1487     #If the watcher we're trying to add is for the current user
1488     if ( $self->CurrentUser->PrincipalId eq $args{'PrincipalId'} ) {
1489
1490         #  If it's an AdminCc and they don't have
1491         #   'WatchAsAdminCc' or 'ModifyTicket', bail
1492         if ( $args{'Type'} eq 'AdminCc' ) {
1493             unless (    $self->CurrentUserHasRight('ModifyTicket')
1494                      or $self->CurrentUserHasRight('WatchAsAdminCc') ) {
1495                 return ( 0, $self->loc('Permission Denied') );
1496             }
1497         }
1498
1499         #  If it's a Requestor or Cc and they don't have
1500         #   'Watch' or 'ModifyTicket', bail
1501         elsif ( ( $args{'Type'} eq 'Cc' ) or ( $args{'Type'} eq 'Requestor' ) )
1502         {
1503             unless (    $self->CurrentUserHasRight('ModifyTicket')
1504                      or $self->CurrentUserHasRight('Watch') ) {
1505                 return ( 0, $self->loc('Permission Denied') );
1506             }
1507         }
1508         else {
1509             $RT::Logger->warn("$self -> DeleteWatcher got passed a bogus type");
1510             return ( 0,
1511                      $self->loc('Error in parameters to Ticket->DeleteWatcher') );
1512         }
1513     }
1514
1515     # If the watcher isn't the current user
1516     # and the current user  doesn't have 'ModifyTicket' bail
1517     else {
1518         unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
1519             return ( 0, $self->loc("Permission Denied") );
1520         }
1521     }
1522
1523     # }}}
1524
1525     # see if this user is already a watcher.
1526
1527     unless ( $group->HasMember($principal) ) {
1528         return ( 0,
1529                  $self->loc( 'That principal is not a [_1] for this ticket',
1530                              $args{'Type'} ) );
1531     }
1532
1533     my ( $m_id, $m_msg ) = $group->_DeleteMember( $principal->Id );
1534     unless ($m_id) {
1535         $RT::Logger->error( "Failed to delete "
1536                             . $principal->Id
1537                             . " as a member of group "
1538                             . $group->Id . "\n"
1539                             . $m_msg );
1540
1541         return (0,
1542                 $self->loc(
1543                     'Could not remove that principal as a [_1] for this ticket',
1544                     $args{'Type'} ) );
1545     }
1546
1547     unless ( $args{'Silent'} ) {
1548         $self->_NewTransaction( Type     => 'DelWatcher',
1549                                 OldValue => $principal->Id,
1550                                 Field    => $args{'Type'} );
1551     }
1552
1553     return ( 1,
1554              $self->loc( "[_1] is no longer a [_2] for this ticket.",
1555                          $principal->Object->Name,
1556                          $args{'Type'} ) );
1557 }
1558
1559
1560
1561 # }}}
1562
1563
1564 =head2 SquelchMailTo [EMAIL]
1565
1566 Takes an optional email address to never email about updates to this ticket.
1567
1568
1569 Returns an array of the RT::Attribute objects for this ticket's 'SquelchMailTo' attributes.
1570
1571 =begin testing
1572
1573 my $t = RT::Ticket->new($RT::SystemUser);
1574 ok($t->Create(Queue => 'general', Subject => 'SquelchTest'));
1575
1576 is($#{$t->SquelchMailTo}, -1, "The ticket has no squelched recipients");
1577
1578 my @returned = $t->SquelchMailTo('nobody@example.com');
1579
1580 is($#returned, 0, "The ticket has one squelched recipients");
1581
1582 my @names = $t->Attributes->Names;
1583 is(shift @names, 'SquelchMailTo', "The attribute we have is SquelchMailTo");
1584 @returned = $t->SquelchMailTo('nobody@example.com');
1585
1586
1587 is($#returned, 0, "The ticket has one squelched recipients");
1588
1589 @names = $t->Attributes->Names;
1590 is(shift @names, 'SquelchMailTo', "The attribute we have is SquelchMailTo");
1591
1592
1593 my ($ret, $msg) = $t->UnsquelchMailTo('nobody@example.com');
1594 ok($ret, "Removed nobody as a squelched recipient - ".$msg);
1595 @returned = $t->SquelchMailTo();
1596 is($#returned, -1, "The ticket has no squelched recipients". join(',',@returned));
1597
1598
1599 =end testing
1600
1601 =cut
1602
1603 sub SquelchMailTo {
1604     my $self = shift;
1605     if (@_) {
1606         unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
1607             return undef;
1608         }
1609         my $attr = shift;
1610         $self->AddAttribute( Name => 'SquelchMailTo', Content => $attr )
1611           unless grep { $_->Content eq $attr }
1612           $self->Attributes->Named('SquelchMailTo');
1613
1614     }
1615     unless ( $self->CurrentUserHasRight('ShowTicket') ) {
1616         return undef;
1617     }
1618     my @attributes = $self->Attributes->Named('SquelchMailTo');
1619     return (@attributes);
1620 }
1621
1622
1623 =head2 UnsquelchMailTo ADDRESS
1624
1625 Takes an address and removes it from this ticket's "SquelchMailTo" list. If an address appears multiple times, each instance is removed.
1626
1627 Returns a tuple of (status, message)
1628
1629 =cut
1630
1631 sub UnsquelchMailTo {
1632     my $self = shift;
1633
1634     my $address = shift;
1635     unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
1636         return ( 0, $self->loc("Permission Denied") );
1637     }
1638
1639     my ($val, $msg) = $self->Attributes->DeleteEntry ( Name => 'SquelchMailTo', Content => $address);
1640     return ($val, $msg);
1641 }
1642
1643
1644 # {{{ a set of  [foo]AsString subs that will return the various sorts of watchers for a ticket/queue as a comma delineated string
1645
1646 =head2 RequestorAddresses
1647
1648  B<Returns> String: All Ticket Requestor email addresses as a string.
1649
1650 =cut
1651
1652 sub RequestorAddresses {
1653     my $self = shift;
1654
1655     unless ( $self->CurrentUserHasRight('ShowTicket') ) {
1656         return undef;
1657     }
1658
1659     return ( $self->Requestors->MemberEmailAddressesAsString );
1660 }
1661
1662
1663 =head2 AdminCcAddresses
1664
1665 returns String: All Ticket AdminCc email addresses as a string
1666
1667 =cut
1668
1669 sub AdminCcAddresses {
1670     my $self = shift;
1671
1672     unless ( $self->CurrentUserHasRight('ShowTicket') ) {
1673         return undef;
1674     }
1675
1676     return ( $self->AdminCc->MemberEmailAddressesAsString )
1677
1678 }
1679
1680 =head2 CcAddresses
1681
1682 returns String: All Ticket Ccs as a string of email addresses
1683
1684 =cut
1685
1686 sub CcAddresses {
1687     my $self = shift;
1688
1689     unless ( $self->CurrentUserHasRight('ShowTicket') ) {
1690         return undef;
1691     }
1692
1693     return ( $self->Cc->MemberEmailAddressesAsString);
1694
1695 }
1696
1697 # }}}
1698
1699 # {{{ Routines that return RT::Watchers objects of Requestors, Ccs and AdminCcs
1700
1701 # {{{ sub Requestors
1702
1703 =head2 Requestors
1704
1705 Takes nothing.
1706 Returns this ticket's Requestors as an RT::Group object
1707
1708 =cut
1709
1710 sub Requestors {
1711     my $self = shift;
1712
1713     my $group = RT::Group->new($self->CurrentUser);
1714     if ( $self->CurrentUserHasRight('ShowTicket') ) {
1715         $group->LoadTicketRoleGroup(Type => 'Requestor', Ticket => $self->Id);
1716     }
1717     return ($group);
1718
1719 }
1720
1721 # }}}
1722
1723 # {{{ sub Cc
1724
1725 =head2 Cc
1726
1727 Takes nothing.
1728 Returns an RT::Group object which contains this ticket's Ccs.
1729 If the user doesn't have "ShowTicket" permission, returns an empty group
1730
1731 =cut
1732
1733 sub Cc {
1734     my $self = shift;
1735
1736     my $group = RT::Group->new($self->CurrentUser);
1737     if ( $self->CurrentUserHasRight('ShowTicket') ) {
1738         $group->LoadTicketRoleGroup(Type => 'Cc', Ticket => $self->Id);
1739     }
1740     return ($group);
1741
1742 }
1743
1744 # }}}
1745
1746 # {{{ sub AdminCc
1747
1748 =head2 AdminCc
1749
1750 Takes nothing.
1751 Returns an RT::Group object which contains this ticket's AdminCcs.
1752 If the user doesn't have "ShowTicket" permission, returns an empty group
1753
1754 =cut
1755
1756 sub AdminCc {
1757     my $self = shift;
1758
1759     my $group = RT::Group->new($self->CurrentUser);
1760     if ( $self->CurrentUserHasRight('ShowTicket') ) {
1761         $group->LoadTicketRoleGroup(Type => 'AdminCc', Ticket => $self->Id);
1762     }
1763     return ($group);
1764
1765 }
1766
1767 # }}}
1768
1769 # }}}
1770
1771 # {{{ IsWatcher,IsRequestor,IsCc, IsAdminCc
1772
1773 # {{{ sub IsWatcher
1774 # a generic routine to be called by IsRequestor, IsCc and IsAdminCc
1775
1776 =head2 IsWatcher { Type => TYPE, PrincipalId => PRINCIPAL_ID, Email => EMAIL }
1777
1778 Takes a param hash with the attributes Type and either PrincipalId or Email
1779
1780 Type is one of Requestor, Cc, AdminCc and Owner
1781
1782 PrincipalId is an RT::Principal id, and Email is an email address.
1783
1784 Returns true if the specified principal (or the one corresponding to the
1785 specified address) is a member of the group Type for this ticket.
1786
1787 XX TODO: This should be Memoized. 
1788
1789 =cut
1790
1791 sub IsWatcher {
1792     my $self = shift;
1793
1794     my %args = ( Type  => 'Requestor',
1795         PrincipalId    => undef,
1796         Email          => undef,
1797         @_
1798     );
1799
1800     # Load the relevant group. 
1801     my $group = RT::Group->new($self->CurrentUser);
1802     $group->LoadTicketRoleGroup(Type => $args{'Type'}, Ticket => $self->id);
1803
1804     # Find the relevant principal.
1805     my $principal = RT::Principal->new($self->CurrentUser);
1806     if (!$args{PrincipalId} && $args{Email}) {
1807         # Look up the specified user.
1808         my $user = RT::User->new($self->CurrentUser);
1809         $user->LoadByEmail($args{Email});
1810         if ($user->Id) {
1811             $args{PrincipalId} = $user->PrincipalId;
1812         }
1813         else {
1814             # A non-existent user can't be a group member.
1815             return 0;
1816         }
1817     }
1818     $principal->Load($args{'PrincipalId'});
1819
1820     # Ask if it has the member in question
1821     return ($group->HasMember($principal));
1822 }
1823
1824 # }}}
1825
1826 # {{{ sub IsRequestor
1827
1828 =head2 IsRequestor PRINCIPAL_ID
1829   
1830   Takes an RT::Principal id
1831   Returns true if the principal is a requestor of the current ticket.
1832
1833
1834 =cut
1835
1836 sub IsRequestor {
1837     my $self   = shift;
1838     my $person = shift;
1839
1840     return ( $self->IsWatcher( Type => 'Requestor', PrincipalId => $person ) );
1841
1842 };
1843
1844 # }}}
1845
1846 # {{{ sub IsCc
1847
1848 =head2 IsCc PRINCIPAL_ID
1849
1850   Takes an RT::Principal id.
1851   Returns true if the principal is a requestor of the current ticket.
1852
1853
1854 =cut
1855
1856 sub IsCc {
1857     my $self = shift;
1858     my $cc   = shift;
1859
1860     return ( $self->IsWatcher( Type => 'Cc', PrincipalId => $cc ) );
1861
1862 }
1863
1864 # }}}
1865
1866 # {{{ sub IsAdminCc
1867
1868 =head2 IsAdminCc PRINCIPAL_ID
1869
1870   Takes an RT::Principal id.
1871   Returns true if the principal is a requestor of the current ticket.
1872
1873 =cut
1874
1875 sub IsAdminCc {
1876     my $self   = shift;
1877     my $person = shift;
1878
1879     return ( $self->IsWatcher( Type => 'AdminCc', PrincipalId => $person ) );
1880
1881 }
1882
1883 # }}}
1884
1885 # {{{ sub IsOwner
1886
1887 =head2 IsOwner
1888
1889   Takes an RT::User object. Returns true if that user is this ticket's owner.
1890 returns undef otherwise
1891
1892 =cut
1893
1894 sub IsOwner {
1895     my $self   = shift;
1896     my $person = shift;
1897
1898     # no ACL check since this is used in acl decisions
1899     # unless ($self->CurrentUserHasRight('ShowTicket')) {
1900     #   return(undef);
1901     #   }       
1902
1903     #Tickets won't yet have owners when they're being created.
1904     unless ( $self->OwnerObj->id ) {
1905         return (undef);
1906     }
1907
1908     if ( $person->id == $self->OwnerObj->id ) {
1909         return (1);
1910     }
1911     else {
1912         return (undef);
1913     }
1914 }
1915
1916 # }}}
1917
1918 # }}}
1919
1920 # }}}
1921
1922 # {{{ Routines dealing with queues 
1923
1924 # {{{ sub ValidateQueue
1925
1926 sub ValidateQueue {
1927     my $self  = shift;
1928     my $Value = shift;
1929
1930     if ( !$Value ) {
1931         $RT::Logger->warning( " RT:::Queue::ValidateQueue called with a null value. this isn't ok.");
1932         return (1);
1933     }
1934
1935     my $QueueObj = RT::Queue->new( $self->CurrentUser );
1936     my $id       = $QueueObj->Load($Value);
1937
1938     if ($id) {
1939         return (1);
1940     }
1941     else {
1942         return (undef);
1943     }
1944 }
1945
1946 # }}}
1947
1948 # {{{ sub SetQueue  
1949
1950 sub SetQueue {
1951     my $self     = shift;
1952     my $NewQueue = shift;
1953
1954     #Redundant. ACL gets checked in _Set;
1955     unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
1956         return ( 0, $self->loc("Permission Denied") );
1957     }
1958
1959     my $NewQueueObj = RT::Queue->new( $self->CurrentUser );
1960     $NewQueueObj->Load($NewQueue);
1961
1962     unless ( $NewQueueObj->Id() ) {
1963         return ( 0, $self->loc("That queue does not exist") );
1964     }
1965
1966     if ( $NewQueueObj->Id == $self->QueueObj->Id ) {
1967         return ( 0, $self->loc('That is the same value') );
1968     }
1969     unless (
1970         $self->CurrentUser->HasRight(
1971             Right    => 'CreateTicket',
1972             Object => $NewQueueObj
1973         )
1974       )
1975     {
1976         return ( 0, $self->loc("You may not create requests in that queue.") );
1977     }
1978
1979     unless (
1980         $self->OwnerObj->HasRight(
1981             Right    => 'OwnTicket',
1982             Object => $NewQueueObj
1983         )
1984       )
1985     {
1986         my $clone = RT::Ticket->new( $RT::SystemUser );
1987         $clone->Load( $self->Id );
1988         unless ( $clone->Id ) {
1989             return ( 0, $self->loc("Couldn't load copy of ticket #[_1].", $self->Id) );
1990         }
1991         my ($status, $msg) = $clone->SetOwner( $RT::Nobody->Id, 'Force' );
1992         $RT::Logger->error("Couldn't set owner on queue change: $msg") unless $status;
1993     }
1994
1995     return ( $self->_Set( Field => 'Queue', Value => $NewQueueObj->Id() ) );
1996 }
1997
1998 # }}}
1999
2000 # {{{ sub QueueObj
2001
2002 =head2 QueueObj
2003
2004 Takes nothing. returns this ticket's queue object
2005
2006 =cut
2007
2008 sub QueueObj {
2009     my $self = shift;
2010
2011     my $queue_obj = RT::Queue->new( $self->CurrentUser );
2012
2013     #We call __Value so that we can avoid the ACL decision and some deep recursion
2014     my ($result) = $queue_obj->Load( $self->__Value('Queue') );
2015     return ($queue_obj);
2016 }
2017
2018 # }}}
2019
2020 # }}}
2021
2022 # {{{ Date printing routines
2023
2024 # {{{ sub DueObj
2025
2026 =head2 DueObj
2027
2028   Returns an RT::Date object containing this ticket's due date
2029
2030 =cut
2031
2032 sub DueObj {
2033     my $self = shift;
2034
2035     my $time = new RT::Date( $self->CurrentUser );
2036
2037     # -1 is RT::Date slang for never
2038     if ( $self->Due ) {
2039         $time->Set( Format => 'sql', Value => $self->Due );
2040     }
2041     else {
2042         $time->Set( Format => 'unix', Value => -1 );
2043     }
2044
2045     return $time;
2046 }
2047
2048 # }}}
2049
2050 # {{{ sub DueAsString 
2051
2052 =head2 DueAsString
2053
2054 Returns this ticket's due date as a human readable string
2055
2056 =cut
2057
2058 sub DueAsString {
2059     my $self = shift;
2060     return $self->DueObj->AsString();
2061 }
2062
2063 # }}}
2064
2065 # {{{ sub ResolvedObj
2066
2067 =head2 ResolvedObj
2068
2069   Returns an RT::Date object of this ticket's 'resolved' time.
2070
2071 =cut
2072
2073 sub ResolvedObj {
2074     my $self = shift;
2075
2076     my $time = new RT::Date( $self->CurrentUser );
2077     $time->Set( Format => 'sql', Value => $self->Resolved );
2078     return $time;
2079 }
2080
2081 # }}}
2082
2083 # {{{ sub SetStarted
2084
2085 =head2 SetStarted
2086
2087 Takes a date in ISO format or undef
2088 Returns a transaction id and a message
2089 The client calls "Start" to note that the project was started on the date in $date.
2090 A null date means "now"
2091
2092 =cut
2093
2094 sub SetStarted {
2095     my $self = shift;
2096     my $time = shift || 0;
2097
2098     unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
2099         return ( 0, self->loc("Permission Denied") );
2100     }
2101
2102     #We create a date object to catch date weirdness
2103     my $time_obj = new RT::Date( $self->CurrentUser() );
2104     if ( $time != 0 ) {
2105         $time_obj->Set( Format => 'ISO', Value => $time );
2106     }
2107     else {
2108         $time_obj->SetToNow();
2109     }
2110
2111     #Now that we're starting, open this ticket
2112     #TODO do we really want to force this as policy? it should be a scrip
2113
2114     #We need $TicketAsSystem, in case the current user doesn't have
2115     #ShowTicket
2116     #
2117     my $TicketAsSystem = new RT::Ticket($RT::SystemUser);
2118     $TicketAsSystem->Load( $self->Id );
2119     if ( $TicketAsSystem->Status eq 'new' ) {
2120         $TicketAsSystem->Open();
2121     }
2122
2123     return ( $self->_Set( Field => 'Started', Value => $time_obj->ISO ) );
2124
2125 }
2126
2127 # }}}
2128
2129 # {{{ sub StartedObj
2130
2131 =head2 StartedObj
2132
2133   Returns an RT::Date object which contains this ticket's 
2134 'Started' time.
2135
2136 =cut
2137
2138 sub StartedObj {
2139     my $self = shift;
2140
2141     my $time = new RT::Date( $self->CurrentUser );
2142     $time->Set( Format => 'sql', Value => $self->Started );
2143     return $time;
2144 }
2145
2146 # }}}
2147
2148 # {{{ sub StartsObj
2149
2150 =head2 StartsObj
2151
2152   Returns an RT::Date object which contains this ticket's 
2153 'Starts' time.
2154
2155 =cut
2156
2157 sub StartsObj {
2158     my $self = shift;
2159
2160     my $time = new RT::Date( $self->CurrentUser );
2161     $time->Set( Format => 'sql', Value => $self->Starts );
2162     return $time;
2163 }
2164
2165 # }}}
2166
2167 # {{{ sub ToldObj
2168
2169 =head2 ToldObj
2170
2171   Returns an RT::Date object which contains this ticket's 
2172 'Told' time.
2173
2174 =cut
2175
2176 sub ToldObj {
2177     my $self = shift;
2178
2179     my $time = new RT::Date( $self->CurrentUser );
2180     $time->Set( Format => 'sql', Value => $self->Told );
2181     return $time;
2182 }
2183
2184 # }}}
2185
2186 # {{{ sub ToldAsString
2187
2188 =head2 ToldAsString
2189
2190 A convenience method that returns ToldObj->AsString
2191
2192 TODO: This should be deprecated
2193
2194 =cut
2195
2196 sub ToldAsString {
2197     my $self = shift;
2198     if ( $self->Told ) {
2199         return $self->ToldObj->AsString();
2200     }
2201     else {
2202         return ("Never");
2203     }
2204 }
2205
2206 # }}}
2207
2208 # {{{ sub TimeWorkedAsString
2209
2210 =head2 TimeWorkedAsString
2211
2212 Returns the amount of time worked on this ticket as a Text String
2213
2214 =cut
2215
2216 sub TimeWorkedAsString {
2217     my $self = shift;
2218     return "0" unless $self->TimeWorked;
2219
2220     #This is not really a date object, but if we diff a number of seconds 
2221     #vs the epoch, we'll get a nice description of time worked.
2222
2223     my $worked = new RT::Date( $self->CurrentUser );
2224
2225     #return the  #of minutes worked turned into seconds and written as
2226     # a simple text string
2227
2228     return ( $worked->DurationAsString( $self->TimeWorked * 60 ) );
2229 }
2230
2231 # }}}
2232
2233 # }}}
2234
2235 # {{{ Routines dealing with correspondence/comments
2236
2237 # {{{ sub Comment
2238
2239 =head2 Comment
2240
2241 Comment on this ticket.
2242 Takes a hashref with the following attributes:
2243 If MIMEObj is undefined, Content will be used to build a MIME::Entity for this
2244 commentl
2245
2246 MIMEObj, TimeTaken, CcMessageTo, BccMessageTo, Content, DryRun
2247
2248 If DryRun is defined, this update WILL NOT BE RECORDED. Scrips will not be committed.
2249 They will, however, be prepared and you'll be able to access them through the TransactionObj
2250
2251 Returns: Transaction id, Error Message, Transaction Object
2252 (note the different order from Create()!)
2253
2254 =cut
2255
2256 sub Comment {
2257     my $self = shift;
2258
2259     my %args = ( CcMessageTo  => undef,
2260                  BccMessageTo => undef,
2261                  MIMEObj      => undef,
2262                  Content      => undef,
2263                  TimeTaken => 0,
2264                  DryRun     => 0, 
2265                  @_ );
2266
2267     unless (    ( $self->CurrentUserHasRight('CommentOnTicket') )
2268              or ( $self->CurrentUserHasRight('ModifyTicket') ) ) {
2269         return ( 0, $self->loc("Permission Denied"), undef );
2270     }
2271     $args{'NoteType'} = 'Comment';
2272
2273     if ($args{'DryRun'}) {
2274         $RT::Handle->BeginTransaction();
2275         $args{'CommitScrips'} = 0;
2276     }
2277
2278     my @results = $self->_RecordNote(%args);
2279     if ($args{'DryRun'}) {
2280         $RT::Handle->Rollback();
2281     }
2282
2283     return(@results);
2284 }
2285 # }}}
2286
2287 # {{{ sub Correspond
2288
2289 =head2 Correspond
2290
2291 Correspond on this ticket.
2292 Takes a hashref with the following attributes:
2293
2294
2295 MIMEObj, TimeTaken, CcMessageTo, BccMessageTo, Content, DryRun
2296
2297 if there's no MIMEObj, Content is used to build a MIME::Entity object
2298
2299 If DryRun is defined, this update WILL NOT BE RECORDED. Scrips will not be committed.
2300 They will, however, be prepared and you'll be able to access them through the TransactionObj
2301
2302 Returns: Transaction id, Error Message, Transaction Object
2303 (note the different order from Create()!)
2304
2305
2306 =cut
2307
2308 sub Correspond {
2309     my $self = shift;
2310     my %args = ( CcMessageTo  => undef,
2311                  BccMessageTo => undef,
2312                  MIMEObj      => undef,
2313                  Content      => undef,
2314                  TimeTaken    => 0,
2315                  @_ );
2316
2317     unless (    ( $self->CurrentUserHasRight('ReplyToTicket') )
2318              or ( $self->CurrentUserHasRight('ModifyTicket') ) ) {
2319         return ( 0, $self->loc("Permission Denied"), undef );
2320     }
2321
2322     $args{'NoteType'} = 'Correspond'; 
2323     if ($args{'DryRun'}) {
2324         $RT::Handle->BeginTransaction();
2325         $args{'CommitScrips'} = 0;
2326     }
2327
2328     my @results = $self->_RecordNote(%args);
2329
2330     #Set the last told date to now if this isn't mail from the requestor.
2331     #TODO: Note that this will wrongly ack mail from any non-requestor as a "told"
2332     $self->_SetTold unless ( $self->IsRequestor($self->CurrentUser->id));
2333
2334     if ($args{'DryRun'}) {
2335         $RT::Handle->Rollback();
2336     }
2337
2338     return (@results);
2339
2340 }
2341
2342 # }}}
2343
2344 # {{{ sub _RecordNote
2345
2346 =head2 _RecordNote
2347
2348 the meat of both comment and correspond. 
2349
2350 Performs no access control checks. hence, dangerous.
2351
2352 =cut
2353
2354 sub _RecordNote {
2355
2356     my $self = shift;
2357     my %args = ( CcMessageTo  => undef,
2358                  BccMessageTo => undef,
2359                  MIMEObj      => undef,
2360                  Content      => undef,
2361                  TimeTaken    => 0,
2362                  CommitScrips => 1,
2363                  @_ );
2364
2365     unless ( $args{'MIMEObj'} || $args{'Content'} ) {
2366             return ( 0, $self->loc("No message attached"), undef );
2367     }
2368     unless ( $args{'MIMEObj'} ) {
2369             $args{'MIMEObj'} = MIME::Entity->build( Data => (
2370                                                           ref $args{'Content'}
2371                                                           ? $args{'Content'}
2372                                                           : [ $args{'Content'} ]
2373                                                     ) );
2374         }
2375
2376     # convert text parts into utf-8
2377     RT::I18N::SetMIMEEntityToUTF8( $args{'MIMEObj'} );
2378
2379 # If we've been passed in CcMessageTo and BccMessageTo fields,
2380 # add them to the mime object for passing on to the transaction handler
2381 # The "NotifyOtherRecipients" scripAction will look for RT-Send-Cc: and RT-Send-Bcc:
2382 # headers
2383
2384     $args{'MIMEObj'}->head->add( 'RT-Send-Cc', RT::User::CanonicalizeEmailAddress(
2385                                                      undef, $args{'CcMessageTo'}
2386                                  ) )
2387       if defined $args{'CcMessageTo'};
2388     $args{'MIMEObj'}->head->add( 'RT-Send-Bcc',
2389                                  RT::User::CanonicalizeEmailAddress(
2390                                                     undef, $args{'BccMessageTo'}
2391                                  ) )
2392       if defined $args{'BccMessageTo'};
2393
2394     # If this is from an external source, we need to come up with its
2395     # internal Message-ID now, so all emails sent because of this
2396     # message have a common Message-ID
2397     unless ($args{'MIMEObj'}->head->get('Message-ID')
2398             =~ /<(rt-.*?-\d+-\d+)\.(\d+-0-0)\@$RT::Organization>/) {
2399         $args{'MIMEObj'}->head->set( 'RT-Message-ID',
2400             "<rt-"
2401             . $RT::VERSION . "-"
2402             . $$ . "-"
2403             . CORE::time() . "-"
2404             . int(rand(2000)) . '.'
2405             . $self->id . "-"
2406             . "0" . "-"  # Scrip
2407             . "0" . "@"  # Email sent
2408             . $RT::Organization
2409             . ">" );
2410     }
2411
2412     #Record the correspondence (write the transaction)
2413     my ( $Trans, $msg, $TransObj ) = $self->_NewTransaction(
2414              Type => $args{'NoteType'},
2415              Data => ( $args{'MIMEObj'}->head->get('subject') || 'No Subject' ),
2416              TimeTaken => $args{'TimeTaken'},
2417              MIMEObj   => $args{'MIMEObj'}, 
2418              CommitScrips => $args{'CommitScrips'},
2419     );
2420
2421     unless ($Trans) {
2422         $RT::Logger->err("$self couldn't init a transaction $msg");
2423         return ( $Trans, $self->loc("Message could not be recorded"), undef );
2424     }
2425
2426     return ( $Trans, $self->loc("Message recorded"), $TransObj );
2427 }
2428
2429 # }}}
2430
2431 # }}}
2432
2433 # {{{ sub _Links 
2434
2435 sub _Links {
2436     my $self = shift;
2437
2438     #TODO: Field isn't the right thing here. but I ahave no idea what mnemonic ---
2439     #tobias meant by $f
2440     my $field = shift;
2441     my $type  = shift || "";
2442
2443     unless ( $self->{"$field$type"} ) {
2444         $self->{"$field$type"} = new RT::Links( $self->CurrentUser );
2445         if ( $self->CurrentUserHasRight('ShowTicket') ) {
2446             # Maybe this ticket is a merged ticket
2447             my $Tickets = new RT::Tickets( $self->CurrentUser );
2448             # at least to myself
2449             $self->{"$field$type"}->Limit( FIELD => $field,
2450                                            VALUE => $self->URI,
2451                                            ENTRYAGGREGATOR => 'OR' );
2452             $Tickets->Limit( FIELD => 'EffectiveId',
2453                              VALUE => $self->EffectiveId );
2454             while (my $Ticket = $Tickets->Next) {
2455                 $self->{"$field$type"}->Limit( FIELD => $field,
2456                                                VALUE => $Ticket->URI,
2457                                                ENTRYAGGREGATOR => 'OR' );
2458             }
2459             $self->{"$field$type"}->Limit( FIELD => 'Type',
2460                                            VALUE => $type )
2461               if ($type);
2462         }
2463     }
2464     return ( $self->{"$field$type"} );
2465 }
2466
2467 # }}}
2468
2469 # {{{ sub DeleteLink 
2470
2471 =head2 DeleteLink
2472
2473 Delete a link. takes a paramhash of Base, Target and Type.
2474 Either Base or Target must be null. The null value will 
2475 be replaced with this ticket\'s id
2476
2477 =cut 
2478
2479 sub DeleteLink {
2480     my $self = shift;
2481     my %args = (
2482         Base   => undef,
2483         Target => undef,
2484         Type   => undef,
2485         @_
2486     );
2487
2488     #check acls
2489     unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
2490         $RT::Logger->debug("No permission to delete links\n");
2491         return ( 0, $self->loc('Permission Denied'))
2492
2493     }
2494
2495     my ($val, $Msg) = $self->SUPER::_DeleteLink(%args);
2496
2497     if ( !$val ) {
2498         $RT::Logger->debug("Couldn't find that link\n");
2499         return ( 0, $Msg );
2500     }
2501
2502     my ($direction, $remote_link);
2503
2504     if ( $args{'Base'} ) {
2505         $remote_link = $args{'Base'};
2506         $direction = 'Target';
2507     }
2508     elsif ( $args{'Target'} ) {
2509         $remote_link = $args{'Target'};
2510         $direction='Base';
2511     }
2512
2513     if ( $args{'Silent'} ) {
2514         return ( $val, $Msg );
2515     }
2516     else {
2517         my $remote_uri = RT::URI->new( $self->CurrentUser );
2518         $remote_uri->FromURI( $remote_link );
2519
2520         my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction(
2521             Type      => 'DeleteLink',
2522             Field => $LINKDIRMAP{$args{'Type'}}->{$direction},
2523             OldValue =>  $remote_uri->URI || $remote_link,
2524             TimeTaken => 0
2525         );
2526
2527         if ( $remote_uri->IsLocal ) {
2528
2529             my $OtherObj = $remote_uri->Object;
2530             my ( $val, $Msg ) = $OtherObj->_NewTransaction(Type  => 'DeleteLink',
2531                                                            Field => $direction eq 'Target' ? $LINKDIRMAP{$args{'Type'}}->{Base}
2532                                                                                            : $LINKDIRMAP{$args{'Type'}}->{Target},
2533                                                            OldValue => $self->URI,
2534                                                            ActivateScrips => ! $RT::LinkTransactionsRun1Scrip,
2535                                                            TimeTaken => 0 );
2536         }
2537
2538         return ( $Trans, $Msg );
2539     }
2540 }
2541
2542 # }}}
2543
2544 # {{{ sub AddLink
2545
2546 =head2 AddLink
2547
2548 Takes a paramhash of Type and one of Base or Target. Adds that link to this ticket.
2549
2550 =cut
2551
2552 sub AddLink {
2553     my $self = shift;
2554     my %args = ( Target => '',
2555                  Base   => '',
2556                  Type   => '',
2557                  Silent => undef,
2558                  @_ );
2559
2560
2561     unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
2562         return ( 0, $self->loc("Permission Denied") );
2563     }
2564
2565
2566     $self->_AddLink(%args);
2567 }
2568
2569 =head2 _AddLink  
2570
2571 Private non-acled variant of AddLink so that links can be added during create.
2572
2573 =cut
2574
2575 sub _AddLink {
2576     my $self = shift;
2577     my %args = ( Target => '',
2578                  Base   => '',
2579                  Type   => '',
2580                  Silent => undef,
2581                  @_ );
2582
2583     # {{{ If the other URI is an RT::Ticket, we want to make sure the user
2584     # can modify it too...
2585     my $other_ticket_uri = RT::URI->new($self->CurrentUser);
2586
2587     if ( $args{'Target'} ) {
2588         $other_ticket_uri->FromURI( $args{'Target'} );
2589
2590     }
2591     elsif ( $args{'Base'} ) {
2592         $other_ticket_uri->FromURI( $args{'Base'} );
2593     }
2594
2595     unless ( $other_ticket_uri->Resolver && $other_ticket_uri->Scheme ) {
2596         my $msg = $args{'Target'} ? $self->loc("Couldn't resolve target '[_1]' into a URI.", $args{'Target'})
2597           : $self->loc("Couldn't resolve base '[_1]' into a URI.", $args{'Base'});
2598         $RT::Logger->warning( "$self $msg\n" );
2599
2600         return( 0, $msg );
2601     }
2602
2603     if ( $other_ticket_uri->Resolver->Scheme eq 'fsck.com-rt') {
2604         my $object = $other_ticket_uri->Resolver->Object;
2605
2606         if (   UNIVERSAL::isa( $object, 'RT::Ticket' )
2607             && $object->id
2608             && !$object->CurrentUserHasRight('ModifyTicket') )
2609         {
2610             return ( 0, $self->loc("Permission Denied") );
2611         }
2612
2613     }
2614
2615     # }}}
2616
2617     my ($val, $Msg) = $self->SUPER::_AddLink(%args);
2618
2619     if (!$val) {
2620         return ($val, $Msg);
2621     }
2622
2623     my ($direction, $remote_link);
2624     if ( $args{'Target'} ) {
2625         $remote_link  = $args{'Target'};
2626         $direction    = 'Base';
2627     } elsif ( $args{'Base'} ) {
2628         $remote_link  = $args{'Base'};
2629         $direction    = 'Target';
2630     }
2631
2632     # Don't write the transaction if we're doing this on create
2633     if ( $args{'Silent'} ) {
2634         return ( $val, $Msg );
2635     }
2636     else {
2637         my $remote_uri = RT::URI->new( $self->CurrentUser );
2638         $remote_uri->FromURI( $remote_link );
2639
2640         #Write the transaction
2641         my ( $Trans, $Msg, $TransObj ) = 
2642             $self->_NewTransaction(Type  => 'AddLink',
2643                                    Field => $LINKDIRMAP{$args{'Type'}}->{$direction},
2644                                    NewValue =>  $remote_uri->URI || $remote_link,
2645                                    TimeTaken => 0 );
2646
2647         if ( $remote_uri->IsLocal ) {
2648
2649             my $OtherObj = $remote_uri->Object;
2650             my ( $val, $Msg ) = $OtherObj->_NewTransaction(Type  => 'AddLink',
2651                                                            Field => $direction eq 'Target' ? $LINKDIRMAP{$args{'Type'}}->{Base} 
2652                                                                                            : $LINKDIRMAP{$args{'Type'}}->{Target},
2653                                                            NewValue => $self->URI,
2654                                                            ActivateScrips => ! $RT::LinkTransactionsRun1Scrip,
2655                                                            TimeTaken => 0 );
2656         }
2657         return ( $val, $Msg );
2658     }
2659
2660 }
2661
2662 # }}}
2663
2664
2665 # {{{ sub MergeInto
2666
2667 =head2 MergeInto
2668
2669 MergeInto take the id of the ticket to merge this ticket into.
2670
2671
2672 =begin testing
2673
2674 my $t1 = RT::Ticket->new($RT::SystemUser);
2675 $t1->Create ( Subject => 'Merge test 1', Queue => 'general', Requestor => 'merge1@example.com');
2676 my $t1id = $t1->id;
2677 my $t2 = RT::Ticket->new($RT::SystemUser);
2678 $t2->Create ( Subject => 'Merge test 2', Queue => 'general', Requestor => 'merge2@example.com');
2679 my $t2id = $t2->id;
2680 my ($msg, $val) = $t1->MergeInto($t2->id);
2681 ok ($msg,$val);
2682 $t1 = RT::Ticket->new($RT::SystemUser);
2683 is ($t1->id, undef, "ok. we've got a blank ticket1");
2684 $t1->Load($t1id);
2685
2686 is ($t1->id, $t2->id);
2687
2688 is ($t1->Requestors->MembersObj->Count, 2);
2689
2690
2691 =end testing
2692
2693 =cut
2694
2695 sub MergeInto {
2696     my $self      = shift;
2697     my $ticket_id = shift;
2698
2699     unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
2700         return ( 0, $self->loc("Permission Denied") );
2701     }
2702
2703     # Load up the new ticket.
2704     my $MergeInto = RT::Ticket->new($RT::SystemUser);
2705     $MergeInto->Load($ticket_id);
2706
2707     # make sure it exists.
2708     unless ( $MergeInto->Id ) {
2709         return ( 0, $self->loc("New ticket doesn't exist") );
2710     }
2711
2712     # Make sure the current user can modify the new ticket.
2713     unless ( $MergeInto->CurrentUserHasRight('ModifyTicket') ) {
2714         return ( 0, $self->loc("Permission Denied") );
2715     }
2716
2717     $RT::Handle->BeginTransaction();
2718
2719     # We use EffectiveId here even though it duplicates information from
2720     # the links table becasue of the massive performance hit we'd take
2721     # by trying to do a separate database query for merge info everytime 
2722     # loaded a ticket. 
2723
2724     #update this ticket's effective id to the new ticket's id.
2725     my ( $id_val, $id_msg ) = $self->__Set(
2726         Field => 'EffectiveId',
2727         Value => $MergeInto->Id()
2728     );
2729
2730     unless ($id_val) {
2731         $RT::Handle->Rollback();
2732         return ( 0, $self->loc("Merge failed. Couldn't set EffectiveId") );
2733     }
2734
2735     my ( $status_val, $status_msg ) = $self->__Set( Field => 'Status', Value => 'resolved');
2736
2737     unless ($status_val) {
2738         $RT::Handle->Rollback();
2739         $RT::Logger->error( $self->loc("[_1] couldn't set status to resolved. RT's Database may be inconsistent.", $self) );
2740         return ( 0, $self->loc("Merge failed. Couldn't set Status") );
2741     }
2742
2743
2744     # update all the links that point to that old ticket
2745     my $old_links_to = RT::Links->new($self->CurrentUser);
2746     $old_links_to->Limit(FIELD => 'Target', VALUE => $self->URI);
2747
2748     my %old_seen;
2749     while (my $link = $old_links_to->Next) {
2750         if (exists $old_seen{$link->Base."-".$link->Type}) {
2751             $link->Delete;
2752         }   
2753         elsif ($link->Base eq $MergeInto->URI) {
2754             $link->Delete;
2755         } else {
2756             # First, make sure the link doesn't already exist. then move it over.
2757             my $tmp = RT::Link->new($RT::SystemUser);
2758             $tmp->LoadByCols(Base => $link->Base, Type => $link->Type, LocalTarget => $MergeInto->id);
2759             if ($tmp->id)   {
2760                     $link->Delete;
2761             } else { 
2762                 $link->SetTarget($MergeInto->URI);
2763                 $link->SetLocalTarget($MergeInto->id);
2764             }
2765             $old_seen{$link->Base."-".$link->Type} =1;
2766         }
2767
2768     }
2769
2770     my $old_links_from = RT::Links->new($self->CurrentUser);
2771     $old_links_from->Limit(FIELD => 'Base', VALUE => $self->URI);
2772
2773     while (my $link = $old_links_from->Next) {
2774         if (exists $old_seen{$link->Type."-".$link->Target}) {
2775             $link->Delete;
2776         }   
2777         if ($link->Target eq $MergeInto->URI) {
2778             $link->Delete;
2779         } else {
2780             # First, make sure the link doesn't already exist. then move it over.
2781             my $tmp = RT::Link->new($RT::SystemUser);
2782             $tmp->LoadByCols(Target => $link->Target, Type => $link->Type, LocalBase => $MergeInto->id);
2783             if ($tmp->id)   {
2784                     $link->Delete;
2785             } else { 
2786                 $link->SetBase($MergeInto->URI);
2787                 $link->SetLocalBase($MergeInto->id);
2788                 $old_seen{$link->Type."-".$link->Target} =1;
2789             }
2790         }
2791
2792     }
2793
2794     # Update time fields
2795     foreach my $type qw(TimeEstimated TimeWorked TimeLeft) {
2796
2797         my $mutator = "Set$type";
2798         $MergeInto->$mutator(
2799             ( $MergeInto->$type() || 0 ) + ( $self->$type() || 0 ) );
2800
2801     }
2802 #add all of this ticket's watchers to that ticket.
2803     foreach my $watcher_type qw(Requestors Cc AdminCc) {
2804
2805         my $people = $self->$watcher_type->MembersObj;
2806         my $addwatcher_type =  $watcher_type;
2807         $addwatcher_type  =~ s/s$//;
2808
2809         while ( my $watcher = $people->Next ) {
2810             
2811            my ($val, $msg) =  $MergeInto->_AddWatcher(
2812                 Type        => $addwatcher_type,
2813                 Silent => 1,
2814                 PrincipalId => $watcher->MemberId
2815             );
2816             unless ($val) {
2817                 $RT::Logger->warning($msg);
2818             }
2819     }
2820
2821     }
2822
2823     #find all of the tickets that were merged into this ticket. 
2824     my $old_mergees = new RT::Tickets( $self->CurrentUser );
2825     $old_mergees->Limit(
2826         FIELD    => 'EffectiveId',
2827         OPERATOR => '=',
2828         VALUE    => $self->Id
2829     );
2830
2831     #   update their EffectiveId fields to the new ticket's id
2832     while ( my $ticket = $old_mergees->Next() ) {
2833         my ( $val, $msg ) = $ticket->__Set(
2834             Field => 'EffectiveId',
2835             Value => $MergeInto->Id()
2836         );
2837     }
2838
2839     #make a new link: this ticket is merged into that other ticket.
2840     $self->AddLink( Type   => 'MergedInto', Target => $MergeInto->Id());
2841
2842     $MergeInto->_SetLastUpdated;    
2843
2844     $RT::Handle->Commit();
2845     return ( 1, $self->loc("Merge Successful") );
2846 }
2847
2848 # }}}
2849
2850 # }}}
2851
2852 # {{{ Routines dealing with ownership
2853
2854 # {{{ sub OwnerObj
2855
2856 =head2 OwnerObj
2857
2858 Takes nothing and returns an RT::User object of 
2859 this ticket's owner
2860
2861 =cut
2862
2863 sub OwnerObj {
2864     my $self = shift;
2865
2866     #If this gets ACLed, we lose on a rights check in User.pm and
2867     #get deep recursion. if we need ACLs here, we need
2868     #an equiv without ACLs
2869
2870     my $owner = new RT::User( $self->CurrentUser );
2871     $owner->Load( $self->__Value('Owner') );
2872
2873     #Return the owner object
2874     return ($owner);
2875 }
2876
2877 # }}}
2878
2879 # {{{ sub OwnerAsString 
2880
2881 =head2 OwnerAsString
2882
2883 Returns the owner's email address
2884
2885 =cut
2886
2887 sub OwnerAsString {
2888     my $self = shift;
2889     return ( $self->OwnerObj->EmailAddress );
2890
2891 }
2892
2893 # }}}
2894
2895 # {{{ sub SetOwner
2896
2897 =head2 SetOwner
2898
2899 Takes two arguments:
2900      the Id or Name of the owner 
2901 and  (optionally) the type of the SetOwner Transaction. It defaults
2902 to 'Give'.  'Steal' is also a valid option.
2903
2904 =begin testing
2905
2906 my $root = RT::User->new($RT::SystemUser);
2907 $root->Load('root');
2908 ok ($root->Id, "Loaded the root user");
2909 my $t = RT::Ticket->new($RT::SystemUser);
2910 $t->Load(1);
2911 $t->SetOwner('root');
2912 is ($t->OwnerObj->Name, 'root' , "Root owns the ticket");
2913 $t->Steal();
2914 is ($t->OwnerObj->id, $RT::SystemUser->id , "SystemUser owns the ticket");
2915 my $txns = RT::Transactions->new($RT::SystemUser);
2916 $txns->OrderBy(FIELD => 'id', ORDER => 'DESC');
2917 $txns->Limit(FIELD => 'ObjectId', VALUE => '1');
2918 $txns->Limit(FIELD => 'ObjectType', VALUE => 'RT::Ticket');
2919 my $steal  = $txns->First;
2920 ok($steal->OldValue == $root->Id , "Stolen from root");
2921 ok($steal->NewValue == $RT::SystemUser->Id , "Stolen by the systemuser");
2922
2923 =end testing
2924
2925 =cut
2926
2927 sub SetOwner {
2928     my $self     = shift;
2929     my $NewOwner = shift;
2930     my $Type     = shift || "Give";
2931
2932     # must have ModifyTicket rights
2933     # or TakeTicket/StealTicket and $NewOwner is self
2934     # see if it's a take
2935     if ( $self->OwnerObj->Id == $RT::Nobody->Id ) {
2936         unless (    $self->CurrentUserHasRight('ModifyTicket')
2937                  || $self->CurrentUserHasRight('TakeTicket') ) {
2938             return ( 0, $self->loc("Permission Denied") );
2939         }
2940     }
2941
2942     # see if it's a steal
2943     elsif (    $self->OwnerObj->Id != $RT::Nobody->Id
2944             && $self->OwnerObj->Id != $self->CurrentUser->id ) {
2945
2946         unless (    $self->CurrentUserHasRight('ModifyTicket')
2947                  || $self->CurrentUserHasRight('StealTicket') ) {
2948             return ( 0, $self->loc("Permission Denied") );
2949         }
2950     }
2951     else {
2952         unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
2953             return ( 0, $self->loc("Permission Denied") );
2954         }
2955     }
2956     my $NewOwnerObj = RT::User->new( $self->CurrentUser );
2957     my $OldOwnerObj = $self->OwnerObj;
2958
2959     $NewOwnerObj->Load($NewOwner);
2960     if ( !$NewOwnerObj->Id ) {
2961         return ( 0, $self->loc("That user does not exist") );
2962     }
2963
2964     #If thie ticket has an owner and it's not the current user
2965
2966     if (    ( $Type ne 'Steal' )
2967         and ( $Type ne 'Force' )
2968         and    #If we're not stealing
2969         ( $self->OwnerObj->Id != $RT::Nobody->Id ) and    #and the owner is set
2970         ( $self->CurrentUser->Id ne $self->OwnerObj->Id() )
2971       ) {                                                 #and it's not us
2972         return ( 0,
2973                  $self->loc(
2974 "You can only reassign tickets that you own or that are unowned" ) );
2975     }
2976
2977     #If we've specified a new owner and that user can't modify the ticket
2978     elsif ( ( $NewOwnerObj->Id )
2979             and ( !$NewOwnerObj->HasRight( Right  => 'OwnTicket',
2980                                            Object => $self ) )
2981       ) {
2982         return ( 0, $self->loc("That user may not own tickets in that queue") );
2983     }
2984
2985     #If the ticket has an owner and it's the new owner, we don't need
2986     #To do anything
2987     elsif (     ( $self->OwnerObj )
2988             and ( $NewOwnerObj->Id eq $self->OwnerObj->Id ) ) {
2989         return ( 0, $self->loc("That user already owns that ticket") );
2990     }
2991
2992     $RT::Handle->BeginTransaction();
2993
2994     # Delete the owner in the owner group, then add a new one
2995     # TODO: is this safe? it's not how we really want the API to work
2996     # for most things, but it's fast.
2997     my ( $del_id, $del_msg ) = $self->OwnerGroup->MembersObj->First->Delete();
2998     unless ($del_id) {
2999         $RT::Handle->Rollback();
3000         return ( 0, $self->loc("Could not change owner. ") . $del_msg );
3001     }
3002
3003     my ( $add_id, $add_msg ) = $self->OwnerGroup->_AddMember(
3004                                        PrincipalId => $NewOwnerObj->PrincipalId,
3005                                        InsideTransaction => 1 );
3006     unless ($add_id) {
3007         $RT::Handle->Rollback();
3008         return ( 0, $self->loc("Could not change owner. ") . $add_msg );
3009     }
3010
3011     # We call set twice with slightly different arguments, so
3012     # as to not have an SQL transaction span two RT transactions
3013
3014     my ( $val, $msg ) = $self->_Set(
3015                       Field             => 'Owner',
3016                       RecordTransaction => 0,
3017                       Value             => $NewOwnerObj->Id,
3018                       TimeTaken         => 0,
3019                       TransactionType   => $Type,
3020                       CheckACL          => 0,                  # don't check acl
3021     );
3022
3023     unless ($val) {
3024         $RT::Handle->Rollback;
3025         return ( 0, $self->loc("Could not change owner. ") . $msg );
3026     }
3027
3028     $RT::Handle->Commit();
3029
3030     ($val, $msg) = $self->_NewTransaction(
3031         Type      => $Type,
3032         Field     => 'Owner',
3033         NewValue  => $NewOwnerObj->Id,
3034         OldValue  => $OldOwnerObj->Id,
3035         TimeTaken => 0,
3036     );
3037
3038     if ( $val ) {
3039         $msg = $self->loc( "Owner changed from [_1] to [_2]",
3040                            $OldOwnerObj->Name, $NewOwnerObj->Name );
3041
3042         # TODO: make sure the trans committed properly
3043     }
3044     return ( $val, $msg );
3045 }
3046
3047 # }}}
3048
3049 # {{{ sub Take
3050
3051 =head2 Take
3052
3053 A convenince method to set the ticket's owner to the current user
3054
3055 =cut
3056
3057 sub Take {
3058     my $self = shift;
3059     return ( $self->SetOwner( $self->CurrentUser->Id, 'Take' ) );
3060 }
3061
3062 # }}}
3063
3064 # {{{ sub Untake
3065
3066 =head2 Untake
3067
3068 Convenience method to set the owner to 'nobody' if the current user is the owner.
3069
3070 =cut
3071
3072 sub Untake {
3073     my $self = shift;
3074     return ( $self->SetOwner( $RT::Nobody->UserObj->Id, 'Untake' ) );
3075 }
3076
3077 # }}}
3078
3079 # {{{ sub Steal 
3080
3081 =head2 Steal
3082
3083 A convenience method to change the owner of the current ticket to the
3084 current user. Even if it's owned by another user.
3085
3086 =cut
3087
3088 sub Steal {
3089     my $self = shift;
3090
3091     if ( $self->IsOwner( $self->CurrentUser ) ) {
3092         return ( 0, $self->loc("You already own this ticket") );
3093     }
3094     else {
3095         return ( $self->SetOwner( $self->CurrentUser->Id, 'Steal' ) );
3096
3097     }
3098
3099 }
3100
3101 # }}}
3102
3103 # }}}
3104
3105 # {{{ Routines dealing with status
3106
3107 # {{{ sub ValidateStatus 
3108
3109 =head2 ValidateStatus STATUS
3110
3111 Takes a string. Returns true if that status is a valid status for this ticket.
3112 Returns false otherwise.
3113
3114 =cut
3115
3116 sub ValidateStatus {
3117     my $self   = shift;
3118     my $status = shift;
3119
3120     #Make sure the status passed in is valid
3121     unless ( $self->QueueObj->IsValidStatus($status) ) {
3122         return (undef);
3123     }
3124
3125     return (1);
3126
3127 }
3128
3129 # }}}
3130
3131 # {{{ sub SetStatus
3132
3133 =head2 SetStatus STATUS
3134
3135 Set this ticket\'s status. STATUS can be one of: new, open, stalled, resolved, rejected or deleted.
3136
3137 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.
3138
3139 =begin testing
3140
3141 my $tt = RT::Ticket->new($RT::SystemUser);
3142 my ($id, $tid, $msg)= $tt->Create(Queue => 'general',
3143             Subject => 'test');
3144 ok($id, $msg);
3145 is($tt->Status, 'new', "New ticket is created as new");
3146
3147 ($id, $msg) = $tt->SetStatus('open');
3148 ok($id, $msg);
3149 like($msg, qr/open/i, "Status message is correct");
3150 ($id, $msg) = $tt->SetStatus('resolved');
3151 ok($id, $msg);
3152 like($msg, qr/resolved/i, "Status message is correct");
3153 ($id, $msg) = $tt->SetStatus('resolved');
3154 ok(!$id,$msg);
3155
3156
3157 =end testing
3158
3159
3160 =cut
3161
3162 sub SetStatus {
3163     my $self   = shift;
3164     my %args;
3165
3166     if (@_ == 1) {
3167         $args{Status} = shift;
3168     }
3169     else {
3170         %args = (@_);
3171     }
3172
3173     #Check ACL
3174     if ( $args{Status} eq 'deleted') {
3175             unless ($self->CurrentUserHasRight('DeleteTicket')) {
3176             return ( 0, $self->loc('Permission Denied') );
3177        }
3178     } else {
3179             unless ($self->CurrentUserHasRight('ModifyTicket')) {
3180             return ( 0, $self->loc('Permission Denied') );
3181        }
3182     }
3183
3184     if (!$args{Force} && ($args{'Status'} eq 'resolved') && $self->HasUnresolvedDependencies) {
3185         return (0, $self->loc('That ticket has unresolved dependencies'));
3186     }
3187
3188     my $now = RT::Date->new( $self->CurrentUser );
3189     $now->SetToNow();
3190
3191     #If we're changing the status from new, record that we've started
3192     if ( ( $self->Status =~ /new/ ) && ( $args{Status} ne 'new' ) ) {
3193
3194         #Set the Started time to "now"
3195         $self->_Set( Field             => 'Started',
3196                      Value             => $now->ISO,
3197                      RecordTransaction => 0 );
3198     }
3199
3200     #When we close a ticket, set the 'Resolved' attribute to now.
3201     # It's misnamed, but that's just historical.
3202     if ( $self->QueueObj->IsInactiveStatus($args{Status}) ) {
3203         $self->_Set( Field             => 'Resolved',
3204                      Value             => $now->ISO,
3205                      RecordTransaction => 0 );
3206     }
3207
3208     #Actually update the status
3209    my ($val, $msg)= $self->_Set( Field           => 'Status',
3210                           Value           => $args{Status},
3211                           TimeTaken       => 0,
3212                           CheckACL      => 0,
3213                           TransactionType => 'Status'  );
3214
3215     return($val,$msg);
3216 }
3217
3218 # }}}
3219
3220 # {{{ sub Kill
3221
3222 =head2 Kill
3223
3224 Takes no arguments. Marks this ticket for garbage collection
3225
3226 =cut
3227
3228 sub Kill {
3229     my $self = shift;
3230     $RT::Logger->crit("'Kill' is deprecated. use 'Delete' instead at (". join(":",caller).").");
3231     return $self->Delete;
3232 }
3233
3234 sub Delete {
3235     my $self = shift;
3236     return ( $self->SetStatus('deleted') );
3237
3238     # TODO: garbage collection
3239 }
3240
3241 # }}}
3242
3243 # {{{ sub Stall
3244
3245 =head2 Stall
3246
3247 Sets this ticket's status to stalled
3248
3249 =cut
3250
3251 sub Stall {
3252     my $self = shift;
3253     return ( $self->SetStatus('stalled') );
3254 }
3255
3256 # }}}
3257
3258 # {{{ sub Reject
3259
3260 =head2 Reject
3261
3262 Sets this ticket's status to rejected
3263
3264 =cut
3265
3266 sub Reject {
3267     my $self = shift;
3268     return ( $self->SetStatus('rejected') );
3269 }
3270
3271 # }}}
3272
3273 # {{{ sub Open
3274
3275 =head2 Open
3276
3277 Sets this ticket\'s status to Open
3278
3279 =cut
3280
3281 sub Open {
3282     my $self = shift;
3283     return ( $self->SetStatus('open') );
3284 }
3285
3286 # }}}
3287
3288 # {{{ sub Resolve
3289
3290 =head2 Resolve
3291
3292 Sets this ticket\'s status to Resolved
3293
3294 =cut
3295
3296 sub Resolve {
3297     my $self = shift;
3298     return ( $self->SetStatus('resolved') );
3299 }
3300
3301 # }}}
3302
3303 # }}}
3304
3305         
3306 # {{{ Actions + Routines dealing with transactions
3307
3308 # {{{ sub SetTold and _SetTold
3309
3310 =head2 SetTold ISO  [TIMETAKEN]
3311
3312 Updates the told and records a transaction
3313
3314 =cut
3315
3316 sub SetTold {
3317     my $self = shift;
3318     my $told;
3319     $told = shift if (@_);
3320     my $timetaken = shift || 0;
3321
3322     unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
3323         return ( 0, $self->loc("Permission Denied") );
3324     }
3325
3326     my $datetold = new RT::Date( $self->CurrentUser );
3327     if ($told) {
3328         $datetold->Set( Format => 'iso',
3329                         Value  => $told );
3330     }
3331     else {
3332         $datetold->SetToNow();
3333     }
3334
3335     return ( $self->_Set( Field           => 'Told',
3336                           Value           => $datetold->ISO,
3337                           TimeTaken       => $timetaken,
3338                           TransactionType => 'Told' ) );
3339 }
3340
3341 =head2 _SetTold
3342
3343 Updates the told without a transaction or acl check. Useful when we're sending replies.
3344
3345 =cut
3346
3347 sub _SetTold {
3348     my $self = shift;
3349
3350     my $now = new RT::Date( $self->CurrentUser );
3351     $now->SetToNow();
3352
3353     #use __Set to get no ACLs ;)
3354     return ( $self->__Set( Field => 'Told',
3355                            Value => $now->ISO ) );
3356 }
3357
3358 # }}}
3359
3360 =head2 TransactionBatch
3361
3362   Returns an array reference of all transactions created on this ticket during
3363   this ticket object's lifetime, or undef if there were none.
3364
3365   Only works when the $RT::UseTransactionBatch config variable is set to true.
3366
3367 =cut
3368
3369 sub TransactionBatch {
3370     my $self = shift;
3371     return $self->{_TransactionBatch};
3372 }
3373
3374 sub DESTROY {
3375     my $self = shift;
3376
3377     # DESTROY methods need to localize $@, or it may unset it.  This
3378     # causes $m->abort to not bubble all of the way up.  See perlbug
3379     # http://rt.perl.org/rt3/Ticket/Display.html?id=17650
3380     local $@;
3381
3382     # The following line eliminates reentrancy.
3383     # It protects against the fact that perl doesn't deal gracefully
3384     # when an object's refcount is changed in its destructor.
3385     return if $self->{_Destroyed}++;
3386
3387     my $batch = $self->TransactionBatch or return;
3388     require RT::Scrips;
3389     RT::Scrips->new($RT::SystemUser)->Apply(
3390         Stage           => 'TransactionBatch',
3391         TicketObj       => $self,
3392         TransactionObj  => $batch->[0],
3393         Type            => join(',', (map { $_->Type } @{$batch}) )
3394     );
3395 }
3396
3397 # }}}
3398
3399 # {{{ PRIVATE UTILITY METHODS. Mostly needed so Ticket can be a DBIx::Record
3400
3401 # {{{ sub _OverlayAccessible
3402
3403 sub _OverlayAccessible {
3404     {
3405         EffectiveId       => { 'read' => 1,  'write' => 1,  'public' => 1 },
3406           Queue           => { 'read' => 1,  'write' => 1 },
3407           Requestors      => { 'read' => 1,  'write' => 1 },
3408           Owner           => { 'read' => 1,  'write' => 1 },
3409           Subject         => { 'read' => 1,  'write' => 1 },
3410           InitialPriority => { 'read' => 1,  'write' => 1 },
3411           FinalPriority   => { 'read' => 1,  'write' => 1 },
3412           Priority        => { 'read' => 1,  'write' => 1 },
3413           Status          => { 'read' => 1,  'write' => 1 },
3414           TimeEstimated      => { 'read' => 1,  'write' => 1 },
3415           TimeWorked      => { 'read' => 1,  'write' => 1 },
3416           TimeLeft        => { 'read' => 1,  'write' => 1 },
3417           Told            => { 'read' => 1,  'write' => 1 },
3418           Resolved        => { 'read' => 1 },
3419           Type            => { 'read' => 1 },
3420           Starts        => { 'read' => 1, 'write' => 1 },
3421           Started       => { 'read' => 1, 'write' => 1 },
3422           Due           => { 'read' => 1, 'write' => 1 },
3423           Creator       => { 'read' => 1, 'auto'  => 1 },
3424           Created       => { 'read' => 1, 'auto'  => 1 },
3425           LastUpdatedBy => { 'read' => 1, 'auto'  => 1 },
3426           LastUpdated   => { 'read' => 1, 'auto'  => 1 }
3427     };
3428
3429 }
3430
3431 # }}}
3432
3433 # {{{ sub _Set
3434
3435 sub _Set {
3436     my $self = shift;
3437
3438     my %args = ( Field             => undef,
3439                  Value             => undef,
3440                  TimeTaken         => 0,
3441                  RecordTransaction => 1,
3442                  UpdateTicket      => 1,
3443                  CheckACL          => 1,
3444                  TransactionType   => 'Set',
3445                  @_ );
3446
3447     if ($args{'CheckACL'}) {
3448       unless ( $self->CurrentUserHasRight('ModifyTicket')) {
3449           return ( 0, $self->loc("Permission Denied"));
3450       }
3451    }
3452
3453     unless ($args{'UpdateTicket'} || $args{'RecordTransaction'}) {
3454         $RT::Logger->error("Ticket->_Set called without a mandate to record an update or update the ticket");
3455         return(0, $self->loc("Internal Error"));
3456     }
3457
3458     #if the user is trying to modify the record
3459
3460     #Take care of the old value we really don't want to get in an ACL loop.
3461     # so ask the super::_Value
3462     my $Old = $self->SUPER::_Value("$args{'Field'}");
3463     
3464     my ($ret, $msg);
3465     if ( $args{'UpdateTicket'}  ) {
3466
3467         #Set the new value
3468         ( $ret, $msg ) = $self->SUPER::_Set( Field => $args{'Field'},
3469                                                 Value => $args{'Value'} );
3470     
3471         #If we can't actually set the field to the value, don't record
3472         # a transaction. instead, get out of here.
3473         return ( 0, $msg ) unless $ret;
3474     }
3475
3476     if ( $args{'RecordTransaction'} == 1 ) {
3477
3478         my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction(
3479                                                Type => $args{'TransactionType'},
3480                                                Field     => $args{'Field'},
3481                                                NewValue  => $args{'Value'},
3482                                                OldValue  => $Old,
3483                                                TimeTaken => $args{'TimeTaken'},
3484         );
3485         return ( $Trans, scalar $TransObj->BriefDescription );
3486     }
3487     else {
3488         return ( $ret, $msg );
3489     }
3490 }
3491
3492 # }}}
3493
3494 # {{{ sub _Value 
3495
3496 =head2 _Value
3497
3498 Takes the name of a table column.
3499 Returns its value as a string, if the user passes an ACL check
3500
3501 =cut
3502
3503 sub _Value {
3504
3505     my $self  = shift;
3506     my $field = shift;
3507
3508     #if the field is public, return it.
3509     if ( $self->_Accessible( $field, 'public' ) ) {
3510
3511         #$RT::Logger->debug("Skipping ACL check for $field\n");
3512         return ( $self->SUPER::_Value($field) );
3513
3514     }
3515
3516     #If the current user doesn't have ACLs, don't let em at it.  
3517
3518     unless ( $self->CurrentUserHasRight('ShowTicket') ) {
3519         return (undef);
3520     }
3521     return ( $self->SUPER::_Value($field) );
3522
3523 }
3524
3525 # }}}
3526
3527 # {{{ sub _UpdateTimeTaken
3528
3529 =head2 _UpdateTimeTaken
3530
3531 This routine will increment the timeworked counter. it should
3532 only be called from _NewTransaction 
3533
3534 =cut
3535
3536 sub _UpdateTimeTaken {
3537     my $self    = shift;
3538     my $Minutes = shift;
3539     my ($Total);
3540
3541     $Total = $self->SUPER::_Value("TimeWorked");
3542     $Total = ( $Total || 0 ) + ( $Minutes || 0 );
3543     $self->SUPER::_Set(
3544         Field => "TimeWorked",
3545         Value => $Total
3546     );
3547
3548     return ($Total);
3549 }
3550
3551 # }}}
3552
3553 # }}}
3554
3555 # {{{ Routines dealing with ACCESS CONTROL
3556
3557 # {{{ sub CurrentUserHasRight 
3558
3559 =head2 CurrentUserHasRight
3560
3561   Takes the textual name of a Ticket scoped right (from RT::ACE) and returns
3562 1 if the user has that right. It returns 0 if the user doesn't have that right.
3563
3564 =cut
3565
3566 sub CurrentUserHasRight {
3567     my $self  = shift;
3568     my $right = shift;
3569
3570     return (
3571         $self->HasRight(
3572             Principal => $self->CurrentUser->UserObj(),
3573             Right     => "$right"
3574           )
3575     );
3576
3577 }
3578
3579 # }}}
3580
3581 # {{{ sub HasRight 
3582
3583 =head2 HasRight
3584
3585  Takes a paramhash with the attributes 'Right' and 'Principal'
3586   'Right' is a ticket-scoped textual right from RT::ACE 
3587   'Principal' is an RT::User object
3588
3589   Returns 1 if the principal has the right. Returns undef if not.
3590
3591 =cut
3592
3593 sub HasRight {
3594     my $self = shift;
3595     my %args = (
3596         Right     => undef,
3597         Principal => undef,
3598         @_
3599     );
3600
3601     unless ( ( defined $args{'Principal'} ) and ( ref( $args{'Principal'} ) ) )
3602     {
3603         Carp::cluck;
3604         $RT::Logger->crit("Principal attrib undefined for Ticket::HasRight");
3605         return(undef);
3606     }
3607
3608     return (
3609         $args{'Principal'}->HasRight(
3610             Object => $self,
3611             Right     => $args{'Right'}
3612           )
3613     );
3614 }
3615
3616 # }}}
3617
3618 # }}}
3619
3620 # {{{ sub Transactions 
3621
3622 =head2 Transactions
3623
3624   Returns an RT::Transactions object of all transactions on this ticket
3625
3626 =cut
3627
3628 sub Transactions {
3629     my $self = shift;
3630
3631     my $transactions = RT::Transactions->new( $self->CurrentUser );
3632
3633     #If the user has no rights, return an empty object
3634     if ( $self->CurrentUserHasRight('ShowTicket') ) {
3635         $transactions->LimitToTicket($self->id);
3636
3637         # if the user may not see comments do not return them
3638         unless ( $self->CurrentUserHasRight('ShowTicketComments') ) {
3639             $transactions->Limit(
3640                 FIELD    => 'Type',
3641                 OPERATOR => '!=',
3642                 VALUE    => "Comment"
3643             );
3644             $transactions->Limit(
3645                 FIELD    => 'Type',
3646                 OPERATOR => '!=',
3647                 VALUE    => "CommentEmailRecord",
3648                 ENTRYAGGREGATOR => 'AND'
3649             );
3650
3651         }
3652     }
3653
3654     return ($transactions);
3655 }
3656
3657 # }}}
3658
3659
3660 # {{{ TransactionCustomFields
3661
3662 =head2 TransactionCustomFields
3663
3664     Returns the custom fields that transactions on tickets will ahve.
3665
3666 =cut
3667
3668 sub TransactionCustomFields {
3669     my $self = shift;
3670     return $self->QueueObj->TicketTransactionCustomFields;
3671 }
3672
3673 # }}}
3674
3675 # {{{ sub CustomFieldValues
3676
3677 =head2 CustomFieldValues
3678
3679 # Do name => id mapping (if needed) before falling back to
3680 # RT::Record's CustomFieldValues
3681
3682 See L<RT::Record>
3683
3684 =cut
3685
3686 sub CustomFieldValues {
3687     my $self  = shift;
3688     my $field = shift;
3689     if ( $field and $field !~ /^\d+$/ ) {
3690         my $cf = RT::CustomField->new( $self->CurrentUser );
3691         $cf->LoadByNameAndQueue( Name => $field, Queue => $self->Queue );
3692         unless ( $cf->id ) {
3693             $cf->LoadByNameAndQueue( Name => $field, Queue => 0 );
3694         }
3695         unless ( $cf->id ) {
3696             # If we didn't find a valid cfid, give up.
3697             return RT::CustomFieldValues->new($self->CurrentUser);
3698         }
3699         $field = $cf->id;
3700     }
3701     return $self->SUPER::CustomFieldValues($field);
3702 }
3703
3704 # }}}
3705
3706 # {{{ sub CustomFieldLookupType
3707
3708 =head2 CustomFieldLookupType
3709
3710 Returns the RT::Ticket lookup type, which can be passed to 
3711 RT::CustomField->Create() via the 'LookupType' hash key.
3712
3713 =cut
3714
3715 # }}}
3716
3717 sub CustomFieldLookupType {
3718     "RT::Queue-RT::Ticket";
3719 }
3720
3721 1;
3722
3723 =head1 AUTHOR
3724
3725 Jesse Vincent, jesse@bestpractical.com
3726
3727 =head1 SEE ALSO
3728
3729 RT
3730
3731 =cut
3732