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