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