ticket display sort order, #13199
[freeside.git] / FS / FS / TicketSystem / RT_Internal.pm
1 package FS::TicketSystem::RT_Internal;
2
3 use strict;
4 use vars qw( @ISA $DEBUG $me );
5 use Data::Dumper;
6 use MIME::Entity;
7 use FS::UID qw(dbh);
8 use FS::CGI qw(popurl);
9 use FS::TicketSystem::RT_Libs;
10
11 @ISA = qw( FS::TicketSystem::RT_Libs );
12
13 $DEBUG = 0;
14 $me = '[FS::TicketSystem::RT_Internal]';
15
16 sub sql_num_customer_tickets {
17   "( select count(*) from Tickets
18                      join Links on ( Tickets.id = Links.LocalBase )
19      where ( Status = 'new' or Status = 'open' or Status = 'stalled' )
20        and Target = 'freeside://freeside/cust_main/' || custnum
21    )";
22 }
23
24 sub baseurl {
25   #my $self = shift;
26   if ( $RT::URI::freeside::URL ) {
27     $RT::URI::freeside::URL. '/rt/';
28   } else {
29     'http://you_need_to_set_RT_URI_freeside_URL_in_SiteConfig.pm/';
30   }
31 }
32
33 #mapping/genericize??
34 #ShowConfigTab ModifySelf
35 sub access_right {
36   my( $self, $session, $right ) = @_;
37
38   return '' unless FS::Conf->new->config('ticket_system');
39
40   $session = $self->session($session);
41
42   #warn "$me access_right: CurrentUser ". $session->{'CurrentUser'}. ":\n".
43   #     ( $DEBUG>1 ? Dumper($session->{'CurrentUser'}) : '' )
44   #  if $DEBUG > 1;
45
46   $session->{'CurrentUser'}->HasRight( Right  => $right,
47                                        Object => $RT::System );
48 }
49
50 sub session {
51   my( $self, $session ) = @_;
52
53   if ( $session && $session->{'Current_User'} ) { # does this even work?
54     warn "$me session: using existing session and CurrentUser: \n".
55          Dumper($session->{'CurrentUser'})
56       if $DEBUG;
57  } else {
58     warn "$me session: loading session and CurrentUser\n" if $DEBUG > 1;
59     $session = $self->_web_external_auth($session);
60   }
61
62   $session;
63 }
64
65 my $firsttime = 1;
66
67 sub init {
68   my $self = shift;
69   if ( $firsttime ) {
70
71     # this part only needs to be done once
72     warn "$me init: loading RT libraries\n" if $DEBUG;
73     eval '
74       use lib ( "/opt/rt3/local/lib", "/opt/rt3/lib" );
75       use RT;
76
77       #for web external auth...
78       use RT::Interface::Web;
79     ';
80     die $@ if $@;
81
82     warn "$me init: loading RT config\n" if $DEBUG;
83     {
84       local $SIG{__DIE__};
85       eval 'RT::LoadConfig();';
86     }
87     die $@ if $@;
88
89     $firsttime = 0;
90   }
91
92   # this needs to be done on each fork
93   warn "$me init: initializing RT\n" if $DEBUG;
94   {
95     local $SIG{__DIE__};
96     eval 'RT::Init("NoSignalHandlers"=>1);';
97   }
98   die $@ if $@;
99
100   warn "$me init: complete" if $DEBUG;
101 }
102
103 =item customer_tickets CUSTNUM [ LIMIT ] [ PRIORITYVALUE ]
104
105 Replacement for the one in RT_External so that we can access custom fields 
106 properly.
107
108 =cut
109
110 sub _customer_tickets_search {
111   my ( $self, $custnum, $limit, $priority ) = @_;
112
113   $custnum =~ /^\d+$/ or die "invalid custnum: $custnum";
114   $limit =~ /^\d+$/ or die "invalid limit: $limit";
115
116   my $session = $self->session();
117   my $CurrentUser = $session->{CurrentUser} 
118     or die "unable to create an RT session";
119
120   my $Tickets = RT::Tickets->new($CurrentUser);
121
122   my $rtql = "MemberOf = 'freeside://freeside/cust_main/$custnum'";
123
124   if ( defined( $priority ) ) {
125     my $custom_priority = FS::Conf->new->config('ticket_system-custom_priority_field');
126     if ( length( $priority ) ) {
127       $rtql .= " AND CF.{$custom_priority} = '$priority'";
128     }
129     else {
130       $rtql .= " AND CF.{$custom_priority} IS NULL";
131     }
132   }
133
134   $rtql .= ' AND ( ' .
135            join(' OR ', map { "Status = '$_'" } $self->statuses) .
136            ' )';
137
138   warn "$me _customer_tickets_search:\n$rtql\n" if $DEBUG;
139   $Tickets->FromSQL($rtql);
140
141   $Tickets->RowsPerPage($limit);
142   warn "\n\n" . $Tickets->BuildSelectQuery . "\n\n" if $DEBUG > 1;
143
144   return $Tickets;
145 }
146
147 sub customer_tickets {
148   my $Tickets = _customer_tickets_search(@_);
149
150   my $conf = FS::Conf->new;
151   my $priority_order =
152     $conf->exists('ticket_system-priority_reverse') ? 'ASC' : 'DESC';
153
154   my @order_by = (
155     { FIELD => 'Priority', ORDER => $priority_order },
156     { FIELD => 'Id',       ORDER => 'DESC' },
157   );
158
159   $Tickets->OrderByCols(@order_by);
160
161   my @tickets;
162   while ( my $t = $Tickets->Next ) {
163     push @tickets, _ticket_info($t);
164   }
165
166   return \@tickets;
167 }
168
169 sub num_customer_tickets {
170   my ( $self, $custnum, $priority ) = @_;
171   my $Tickets = $self->_customer_tickets_search($custnum, 0, $priority);
172   return $Tickets->CountAll;
173 }
174
175 sub _ticket_info {
176   # Takes an RT::Ticket; returns a hashref of the ticket's fields, including 
177   # custom fields.  Also returns custom and selfservice priority values as 
178   # _custom_priority and _selfservice_priority.
179   my $t = shift;
180
181   my $custom_priority = 
182     FS::Conf->new->config('ticket_system-custom_priority_field') || '';
183   my $ss_priority = selfservice_priority();
184
185   my %ticket_info;
186   foreach my $name ( $t->ReadableAttributes ) {
187     # lowercase names, and skip attributes with non-scalar values
188     $ticket_info{lc($name)} = $t->$name if !ref($t->$name);
189   }
190   $ticket_info{'owner'} = $t->OwnerObj->Name;
191   $ticket_info{'queue'} = $t->QueueObj->Name;
192   foreach my $CF ( @{ $t->CustomFields->ItemsArrayRef } ) {
193     my $name = 'CF.{'.$CF->Name.'}';
194     $ticket_info{$name} = $t->CustomFieldValuesAsString($CF->Id);
195   }
196   # make this easy to find
197   if ( $custom_priority ) {
198     $ticket_info{'content'} = $ticket_info{"CF.{$custom_priority}"};
199   }
200   if ( $ss_priority ) {
201     $ticket_info{'_selfservice_priority'} = $ticket_info{"CF.{$ss_priority}"};
202   }
203   return \%ticket_info;
204 }
205
206 =item create_ticket SESSION_HASHREF, OPTION => VALUE ...
207
208 Class method.  Creates a ticket.  If there is an error, returns the scalar
209 error, otherwise returns the newly created RT::Ticket object.
210
211 Accepts the following options:
212
213 =over 4
214
215 =item queue
216
217 Queue name or Id
218
219 =item subject
220
221 Ticket subject
222
223 =item requestor
224
225 Requestor email address or arrayref of addresses
226
227 =item cc
228
229 Cc: email address or arrayref of addresses
230
231 =item message
232
233 Ticket message
234
235 =item mime_type
236
237 MIME type to use for message.  Defaults to text/plain.  Specifying text/html
238 can be useful to use HTML markup in message.
239
240 =item custnum
241
242 Customer number (see L<FS::cust_main>) to associate with ticket.
243
244 =item svcnum
245
246 Service number (see L<FS::cust_svc>) to associate with ticket.  Will also
247 associate the customer who has this service (unless the service is unlinked).
248
249 =back
250
251 =cut
252
253 sub create_ticket {
254   my($self, $session, %param) = @_;
255
256   $session = $self->session($session);
257
258   my $Queue = RT::Queue->new($session->{'CurrentUser'});
259   $Queue->Load( $param{'queue'} );
260
261   my $req = ref($param{'requestor'})
262               ? $param{'requestor'}
263               : ( $param{'requestor'} ? [ $param{'requestor'} ] : [] );
264
265   my $cc = ref($param{'cc'})
266              ? $param{'cc'}
267              : ( $param{'cc'} ? [ $param{'cc'} ] : [] );
268
269   my $mimeobj = MIME::Entity->build(
270     'Data' => $param{'message'},
271     'Type' => ( $param{'mime_type'} || 'text/plain' ),
272   );
273
274   my %ticket = (
275     'Queue'     => $Queue->Id,
276     'Subject'   => $param{'subject'},
277     'Requestor' => $req,
278     'Cc'        => $cc,
279     'MIMEObj'   => $mimeobj,
280   );
281   warn Dumper(\%ticket) if $DEBUG > 1;
282
283   my $Ticket = RT::Ticket->new($session->{'CurrentUser'});
284   my( $id, $Transaction, $ErrStr );
285   {
286     local $SIG{__DIE__};
287     ( $id, $Transaction, $ErrStr ) = $Ticket->Create( %ticket );
288   }
289   return $ErrStr if $id == 0;
290
291   warn "ticket got id $id\n" if $DEBUG;
292
293   #XXX check errors adding custnum/svcnum links (put it in a transaction)...
294   # but we do already know they're good
295
296   if ( $param{'custnum'} ) {
297     my( $val, $msg ) = $Ticket->_AddLink(
298      'Type'   => 'MemberOf',
299      'Target' => 'freeside://freeside/cust_main/'. $param{'custnum'},
300     );
301   }
302
303   if ( $param{'svcnum'} ) {
304     my( $val, $msg ) = $Ticket->_AddLink(
305      'Type'   => 'MemberOf',
306      'Target' => 'freeside://freeside/cust_svc/'. $param{'svcnum'},
307     );
308   }
309
310   $Ticket;
311 }
312
313 =item get_ticket SESSION_HASHREF, OPTION => VALUE ...
314
315 Class method. Retrieves a ticket. If there is an error, returns the scalar
316 error. Otherwise, currently returns a slightly tricky data structure containing
317 the ticket's attributes, a list of the linked customers, each transaction's 
318 content, description, and create time.
319
320 Accepts the following options:
321
322 =over 4
323
324 =item ticket_id 
325
326 The ticket id
327
328 =back
329
330 =cut
331
332 sub get_ticket {
333   my($self, $session, %param) = @_;
334
335   $session = $self->session($session);
336
337   my $Ticket = RT::Ticket->new($session->{'CurrentUser'});
338   my $ticketid = $Ticket->Load( $param{'ticket_id'} );
339   return 'Could not load ticket' unless $ticketid;
340
341   my @custs = ();
342   foreach my $link ( @{ $Ticket->Customers->ItemsArrayRef } ) {
343     my $cust = $link->Target;
344     push @custs, $1 if $cust =~ /\/(\d+)$/;
345   }
346
347   my @txns = ();
348   my $transactions = $Ticket->Transactions;
349   while ( my $transaction = $transactions->Next ) {
350     my $t = { created => $transaction->Created,
351         content => $transaction->Content,
352         description => $transaction->Description,
353         type => $transaction->Type,
354     };
355     push @txns, $t;
356   }
357
358   { txns => [ @txns ],
359     custs => [ @custs ],
360     fields => _ticket_info($Ticket),
361   };
362 }
363
364 =item get_ticket_object SESSION_HASHREF, OPTION => VALUE...
365
366 Class method.  Retrieve the RT::Ticket object with the specified 
367 ticket_id.  If custnum is supplied, will also check that the object 
368 is a member of that customer.  If there is no ticket or the custnum 
369 check fails, returns nothing.  The meaning of that case is 
370 "to this customer, the ticket does not exist".
371
372 Options:
373
374 =over 4
375
376 =item ticket_id
377
378 =item custnum
379
380 =back
381
382 =cut
383
384 sub get_ticket_object {
385   my $self = shift;
386   my ($session, %opt) = @_;
387   $session = $self->session(shift);
388   my $Ticket = RT::Ticket->new($session->{CurrentUser});
389   $Ticket->Load($opt{'ticket_id'});
390   return if ( !$Ticket->id );
391   my $custnum = $opt{'custnum'};
392   if ( defined($custnum) && $custnum =~ /^\d+$/ ) {
393     # probably the most efficient way to check ticket ownership
394     my $Link = RT::Link->new($session->{CurrentUser});
395     $Link->LoadByCols( LocalBase => $opt{'ticket_id'},
396                        Type      => 'MemberOf',
397                        Target    => "freeside://freeside/cust_main/$custnum",
398                      );
399     return if ( !$Link->id );
400   }
401   return $Ticket;
402 }
403
404
405 =item correspond_ticket SESSION_HASHREF, OPTION => VALUE ...
406
407 Class method. Correspond on a ticket. If there is an error, returns the scalar
408 error. Otherwise, returns the transaction id, error message, and
409 RT::Transaction object.
410
411 Accepts the following options:
412
413 =over 4
414
415 =item ticket_id 
416
417 The ticket id
418
419 =item content
420
421 Correspondence content
422
423 =back
424
425 =cut
426
427 sub correspond_ticket {
428   my($self, $session, %param) = @_;
429
430   $session = $self->session($session);
431
432   my $Ticket = RT::Ticket->new($session->{'CurrentUser'});
433   my $ticketid = $Ticket->Load( $param{'ticket_id'} );
434   return 'Could not load ticket' unless $ticketid;
435   return 'No content' unless $param{'content'};
436
437   $Ticket->Correspond( Content => $param{'content'} );
438 }
439
440 =item queues SESSION_HASHREF [, ACL ]
441
442 Retrieve a list of queues.  Pass the name of an RT access control right, 
443 such as 'CreateTicket', to return only queues on which the current user 
444 has that right.  Otherwise this will return all queues with the 'SeeQueue' 
445 right.
446
447 =cut
448
449 sub queues {
450   my( $self, $session, $acl ) = @_;
451   $session = $self->session($session);
452
453   my $showall = $acl ? 0 : 1;
454   my @result = ();
455   my $q = new RT::Queues($session->{'CurrentUser'});
456   $q->UnLimit;
457   while (my $queue = $q->Next) {
458     if ($showall || $queue->CurrentUserHasRight($acl)) {
459       push @result, {
460         Id          => $queue->Id,
461         Name        => $queue->Name,
462         Description => $queue->Description,
463       };
464     }
465   }
466   return map { $_->{Id} => $_->{Name} } @result;
467 }
468
469 #shameless false laziness w/RT::Interface::Web::AttemptExternalAuth
470 # to get logged into RT from afar
471 sub _web_external_auth {
472   my( $self, $session ) = @_;
473
474   my $user = $FS::CurrentUser::CurrentUser->username;
475
476   eval 'use RT::CurrentUser;';
477   die $@ if $@;
478
479   $session ||= {};
480   $session->{'CurrentUser'} = RT::CurrentUser->new();
481
482   warn "$me _web_external_auth loading RT user for $user\n"
483     if $DEBUG > 1;
484
485   $session->{'CurrentUser'}->Load($user);
486
487   if ( ! $session->{'CurrentUser'}->Id() ) {
488
489       # Create users on-the-fly
490
491       warn "can't load RT user for $user; auto-creating\n"
492         if $DEBUG;
493
494       my $UserObj = RT::User->new( RT::CurrentUser->new('RT_System') );
495
496       my ( $val, $msg ) = $UserObj->Create(
497           %{ ref($RT::AutoCreate) ? $RT::AutoCreate : {} },
498           Name  => $user,
499           Gecos => $user,
500       );
501
502       if ($val) {
503
504           # now get user specific information, to better create our user.
505           my $new_user_info
506               = RT::Interface::Web::WebExternalAutoInfo($user);
507
508           # set the attributes that have been defined.
509           # FIXME: this is a horrible kludge. I'm sure there's something cleaner
510           foreach my $attribute (
511               'Name',                  'Comments',
512               'Signature',             'EmailAddress',
513               'PagerEmailAddress',     'FreeformContactInfo',
514               'Organization',          'Disabled',
515               'Privileged',            'RealName',
516               'NickName',              'Lang',
517               'EmailEncoding',         'WebEncoding',
518               'ExternalContactInfoId', 'ContactInfoSystem',
519               'ExternalAuthId',        'Gecos',
520               'HomePhone',             'WorkPhone',
521               'MobilePhone',           'PagerPhone',
522               'Address1',              'Address2',
523               'City',                  'State',
524               'Zip',                   'Country'
525               )
526           {
527               #uhh, wrong root
528               #$m->comp( '/Elements/Callback', %ARGS,
529               #    _CallbackName => 'NewUser' );
530
531               my $method = "Set$attribute";
532               $UserObj->$method( $new_user_info->{$attribute} )
533                   if ( defined $new_user_info->{$attribute} );
534           }
535           $session->{'CurrentUser'}->Load($user);
536       }
537       else {
538
539          # we failed to successfully create the user. abort abort abort.
540           delete $session->{'CurrentUser'};
541
542           die "can't auto-create RT user"; #an error message would be nice :/
543           #$m->abort() unless $RT::WebFallbackToInternalAuth;
544           #$m->comp( '/Elements/Login', %ARGS,
545           #    Error => loc( 'Cannot create user: [_1]', $msg ) );
546       }
547   }
548
549   unless ( $session->{'CurrentUser'}->Id() ) {
550       delete $session->{'CurrentUser'};
551
552       die "can't auto-create RT user";
553       #$user = $orig_user;
554       # 
555       #if ($RT::WebExternalOnly) {
556       #    $m->comp( '/Elements/Login', %ARGS,
557       #        Error => loc('You are not an authorized user') );
558       #    $m->abort();
559       #}
560   }
561
562   $session;
563
564 }
565
566 =item selfservice_priority
567
568 Returns the configured self-service priority field.
569
570 =cut
571
572 my $selfservice_priority;
573
574 sub selfservice_priority {
575   return $selfservice_priority ||= do {
576     my $conf = FS::Conf->new;
577     $conf->config('ticket_system-selfservice_priority_field') || '';
578   }
579 }
580
581 1;
582