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