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