diff options
author | mark <mark> | 2011-06-27 07:11:01 +0000 |
---|---|---|
committer | mark <mark> | 2011-06-27 07:11:01 +0000 |
commit | 12f4cc4b100b849de3584d5d1a2376cebcd8729f (patch) | |
tree | 9b6c5c8badd6114034e59bffe5277696f93de4b1 /FS/FS | |
parent | c1e316ef66e35dadb888b4e59047ba51082f198a (diff) |
self-service ticket priority and edit subject, #13199
Diffstat (limited to 'FS/FS')
-rw-r--r-- | FS/FS/ClientAPI/MyAccount.pm | 168 | ||||
-rw-r--r-- | FS/FS/Conf.pm | 38 | ||||
-rw-r--r-- | FS/FS/Schema.pm | 1 | ||||
-rw-r--r-- | FS/FS/TicketSystem/RT_External.pm | 4 | ||||
-rw-r--r-- | FS/FS/TicketSystem/RT_Internal.pm | 214 | ||||
-rw-r--r-- | FS/FS/cust_main.pm | 4 |
6 files changed, 341 insertions, 88 deletions
diff --git a/FS/FS/ClientAPI/MyAccount.pm b/FS/FS/ClientAPI/MyAccount.pm index fa0bbb8a7..a788a5c05 100644 --- a/FS/FS/ClientAPI/MyAccount.pm +++ b/FS/FS/ClientAPI/MyAccount.pm @@ -74,7 +74,7 @@ sub skin_info { or die "no agentnum for custnum $custnum"; #} elsif ( $context eq 'agent' ) { - } elsif ( $p->{'agentnum'} =~ /^(\d+)$/ ) { + } elsif ( defined($p->{'agentnum'}) and $p->{'agentnum'} =~ /^(\d+)$/ ) { $agentnum = $1; } @@ -97,7 +97,7 @@ sub skin_info { $skin_info_cache_agent = { 'agentnum' => $agentnum, ( map { $_ => scalar( $conf->config($_, $agentnum) ) } - qw( company_name ) ), + qw( company_name date_format ) ), ( map { $_ => scalar( $conf->config("selfservice-$_", $agentnum ) ) } qw( body_bgcolor box_bgcolor text_color link_color vlink_color hlink_color alink_color @@ -295,6 +295,10 @@ sub access_info { $info->{'self_suspend_reason'} = $conf->config('selfservice-self_suspend_reason', $cust_main->agentnum); + $info->{'edit_ticket_subject'} = + $conf->exists('ticket_system-selfservice_edit_subject') && + $cust_main->edit_subject; + return { %$info, 'custnum' => $custnum, 'access_pkgnum' => $session->{'pkgnum'}, @@ -316,7 +320,12 @@ sub customer_info { }else{ $return{'require_address2'} = ''; } - + + if ( $conf->exists('ticket_system') ) { + warn "$me customer_info: initializing ticket system\n" if $DEBUG; + FS::TicketSystem->init(); + } + if ( $custnum ) { #customer record my $search = { 'custnum' => $custnum }; @@ -1909,13 +1918,13 @@ sub create_ticket { ); if ( ref($err_or_ticket) ) { - warn "$me create_ticket: sucessful: ". $err_or_ticket->id. "\n" + warn "$me create_ticket: successful: ". $err_or_ticket->id. "\n" if $DEBUG; return { 'error' => '', 'ticket_id' => $err_or_ticket->id, }; } else { - warn "$me create_ticket: unsucessful: $err_or_ticket\n" + warn "$me create_ticket: unsuccessful: $err_or_ticket\n" if $DEBUG; return { 'error' => $err_or_ticket }; } @@ -2009,62 +2018,135 @@ sub get_ticket { warn "$me get_ticket: initializing ticket system\n" if $DEBUG; FS::TicketSystem->init(); + return { 'error' => 'get_ticket configuration error' } + if $FS::TicketSystem::system ne 'RT_Internal'; + + # check existence and ownership as part of this + warn "$me get_ticket: fetching ticket\n" if $DEBUG; + my $rt_session = FS::TicketSystem->session(''); + my $Ticket = FS::TicketSystem->get_ticket_object( + $rt_session, + ticket_id => $p->{'ticket_id'}, + custnum => $custnum + ); + return { 'error' => 'ticket not found' } if !$Ticket; + + if ( length( $p->{'subject'} || '' ) ) { + # subject change + if ( $p->{'subject'} ne $Ticket->Subject ) { + my ($val, $msg) = $Ticket->SetSubject($p->{'subject'}); + return { 'error' => "unable to set subject: $msg" } if !$val; + } + } if(length($p->{'reply'})) { -# currently this allows anyone to correspond on any ticket as fs_selfservice -# probably bad... - my @err_or_res = FS::TicketSystem->correspond_ticket( - '', #create RT session based on FS CurrentUser (fs_selfservice) - 'ticket_id' => $p->{'ticket_id'}, - 'content' => $p->{'reply'}, - ); - + my @err_or_res = FS::TicketSystem->correspond_ticket( + $rt_session, + 'ticket_id' => $p->{'ticket_id'}, + 'content' => $p->{'reply'}, + ); + return { 'error' => 'unable to reply to ticket' } - unless ( $err_or_res[0] != 0 && defined $err_or_res[2] ); + unless ( $err_or_res[0] != 0 && defined $err_or_res[2] ); } - warn "$me get_ticket: getting ticket\n" if $DEBUG; + warn "$me get_ticket: getting ticket history\n" if $DEBUG; my $err_or_ticket = FS::TicketSystem->get_ticket( - '', #create RT session based on FS CurrentUser (fs_selfservice) + $rt_session, 'ticket_id' => $p->{'ticket_id'}, ); - if ( ref($err_or_ticket) ) { + if ( !ref($err_or_ticket) ) { # there is no way this should ever happen + warn "$me get_ticket: unsuccessful: $err_or_ticket\n" + if $DEBUG; + return { 'error' => $err_or_ticket }; + } -# since we're bypassing the RT security/permissions model by always using -# fs_selfservice as the RT user (as opposed to a requestor, which we -# can't do since we want all tickets linked to a cust), we check below whether -# the requested ticket was actually linked to this customer - my @custs = @{$err_or_ticket->{'custs'}}; - my @txns = @{$err_or_ticket->{'txns'}}; - my @filtered_txns; + my @custs = @{$err_or_ticket->{'custs'}}; + my @txns = @{$err_or_ticket->{'txns'}}; + my @filtered_txns; - return { 'error' => 'no customer' } unless ( $custnum && scalar(@custs) ); + # superseded by check in get_ticket_object + #return { 'error' => 'invalid ticket requested' } + #unless grep($_ eq $custnum, @custs); - return { 'error' => 'invalid ticket requested' } - unless grep($_ eq $custnum, @custs); + foreach my $txn ( @txns ) { + push @filtered_txns, $txn + if ($txn->{'type'} eq 'EmailRecord' + || $txn->{'type'} eq 'Correspond' + || $txn->{'type'} eq 'Create'); + } - foreach my $txn ( @txns ) { - push @filtered_txns, $txn - if ($txn->{'type'} eq 'EmailRecord' - || $txn->{'type'} eq 'Correspond' - || $txn->{'type'} eq 'Create'); + warn "$me get_ticket: successful: \n" + if $DEBUG; + return { 'error' => '', + 'transactions' => \@filtered_txns, + 'ticket_fields' => $err_or_ticket->{'fields'}, + 'ticket_id' => $p->{'ticket_id'}, + }; +} + +sub adjust_ticket_priority { + my $p = shift; + my($context, $session, $custnum) = _custoragent_session_custnum($p); + return { 'error' => $session } if $context eq 'error'; + + warn "$me adjust_ticket_priority: initializing ticket system\n" if $DEBUG; + FS::TicketSystem->init; + my $ss_priority = FS::TicketSystem->selfservice_priority; + + return { 'error' => 'adjust_ticket_priority configuration error' } + if $FS::TicketSystem::system ne 'RT_Internal' + or !$ss_priority; + + my $values = $p->{'values'}; #hashref, id => priority value + my %ticket_error; + + foreach my $id (keys %$values) { + warn "$me adjust_ticket_priority: fetching ticket $id\n" if $DEBUG; + my $Ticket = FS::TicketSystem->get_ticket_object('', + 'ticket_id' => $id, + 'custnum' => $custnum, + ); + if ( !$Ticket ) { + $ticket_error{$id} = 'ticket not found'; + next; + } + + # RT API stuff--would we gain anything by wrapping this in FS::TicketSystem? + # We're not going to implement it for RT_External. + my $old_value = $Ticket->FirstCustomFieldValue($ss_priority); + my $new_value = $values->{$id}; + next if $old_value eq $new_value; + + warn "$me adjust_ticket_priority: updating ticket $id\n" if $DEBUG; + + # AddCustomFieldValue works fine (replacing any existing value) if it's + # a single-valued custom field, which it should be. If it's not, you're + # doing something wrong. + my ($val, $msg); + if ( length($new_value) ) { + ($val, $msg) = $Ticket->AddCustomFieldValue( + Field => $ss_priority, + Value => $new_value, + ); + } + else { + ($val, $msg) = $Ticket->DeleteCustomFieldValue( + Field => $ss_priority, + Value => $old_value, + ); } - warn "$me get_ticket: sucessful: \n" - if $DEBUG; - return { 'error' => '', - 'transactions' => \@filtered_txns, - 'ticket_id' => $p->{'ticket_id'}, - }; - } else { - warn "$me create_ticket: unsucessful: $err_or_ticket\n" - if $DEBUG; - return { 'error' => $err_or_ticket }; + $ticket_error{$id} = $msg if !$val; + warn "$me adjust_ticket_priority: $id: $msg\n" if $DEBUG and !$val; } + return { 'error' => '', + 'ticket_error' => \%ticket_error, + %{ customer_info($p) } # send updated customer info back + } } - #-- sub _custoragent_session_custnum { diff --git a/FS/FS/Conf.pm b/FS/FS/Conf.pm index 7d9d6c736..170d88479 100644 --- a/FS/FS/Conf.pm +++ b/FS/FS/Conf.pm @@ -2540,7 +2540,7 @@ and customer address. Include units.', { 'key' => 'ticket_system', - 'section' => '', + 'section' => 'ticketing', 'description' => 'Ticketing system integration. <b>RT_Internal</b> uses the built-in RT ticketing system (see the <a href="http://www.freeside.biz/mediawiki/index.php/Freeside:2.1:Documentation:RT_Installation">integrated ticketing installation instructions</a>). <b>RT_External</b> accesses an external RT installation in a separate database (local or remote).', 'type' => 'select', #'select_enum' => [ '', qw(RT_Internal RT_Libs RT_External) ], @@ -2558,7 +2558,7 @@ and customer address. Include units.', { 'key' => 'ticket_system-default_queueid', - 'section' => '', + 'section' => 'ticketing', 'description' => 'Default queue used when creating new customer tickets.', 'type' => 'select-sub', 'options_sub' => sub { @@ -2584,13 +2584,13 @@ and customer address. Include units.', }, { 'key' => 'ticket_system-force_default_queueid', - 'section' => '', + 'section' => 'ticketing', 'description' => 'Disallow queue selection when creating new tickets from customer view.', 'type' => 'checkbox', }, { 'key' => 'ticket_system-selfservice_queueid', - 'section' => '', + 'section' => 'ticketing', 'description' => 'Queue used when creating new customer tickets from self-service. Defautls to ticket_system-default_queueid if not specified.', #false laziness w/above 'type' => 'select-sub', @@ -2618,49 +2618,63 @@ and customer address. Include units.', { 'key' => 'ticket_system-requestor', - 'section' => '', + 'section' => 'ticketing', 'description' => 'Email address to use as the requestor for new tickets. If blank, the customer\'s invoicing address(es) will be used.', 'type' => 'text', }, { 'key' => 'ticket_system-priority_reverse', - 'section' => '', + 'section' => 'ticketing', 'description' => 'Enable this to consider lower numbered priorities more important. A bad habit we picked up somewhere. You probably want to avoid it and use the default.', 'type' => 'checkbox', }, { 'key' => 'ticket_system-custom_priority_field', - 'section' => '', + 'section' => 'ticketing', 'description' => 'Custom field from the ticketing system to use as a custom priority classification.', 'type' => 'text', }, { 'key' => 'ticket_system-custom_priority_field-values', - 'section' => '', + 'section' => 'ticketing', 'description' => 'Values for the custom field from the ticketing system to break down and sort customer ticket lists.', 'type' => 'textarea', }, { 'key' => 'ticket_system-custom_priority_field_queue', - 'section' => '', + 'section' => 'ticketing', 'description' => 'Ticketing system queue in which the custom field specified in ticket_system-custom_priority_field is located.', 'type' => 'text', }, { + 'key' => 'ticket_system-selfservice_priority_field', + 'section' => 'ticketing', + 'description' => 'Custom field from the ticket system to use as a customer-managed priority field.', + 'type' => 'text', + }, + + { + 'key' => 'ticket_system-selfservice_edit_subject', + 'section' => 'ticketing', + 'description' => 'Allow customers to edit ticket subjects through selfservice.', + 'type' => 'checkbox', + }, + + { 'key' => 'ticket_system-escalation', - 'section' => '', + 'section' => 'ticketing', 'description' => 'Enable priority escalation of tickets as part of daily batch processing.', 'type' => 'checkbox', }, { 'key' => 'ticket_system-rt_external_datasrc', - 'section' => '', + 'section' => 'ticketing', 'description' => 'With external RT integration, the DBI data source for the external RT installation, for example, <code>DBI:Pg:user=rt_user;password=rt_word;host=rt.example.com;dbname=rt</code>', 'type' => 'text', @@ -2668,7 +2682,7 @@ and customer address. Include units.', { 'key' => 'ticket_system-rt_external_url', - 'section' => '', + 'section' => 'ticketing', 'description' => 'With external RT integration, the URL for the external RT installation, for example, <code>https://rt.example.com/rt</code>', 'type' => 'text', }, diff --git a/FS/FS/Schema.pm b/FS/FS/Schema.pm index bf84f0b2b..c786176ec 100644 --- a/FS/FS/Schema.pm +++ b/FS/FS/Schema.pm @@ -881,6 +881,7 @@ sub tables_hashref { 'email_csv_cdr', 'char', 'NULL', 1, '', '', 'accountcode_cdr', 'char', 'NULL', 1, '', '', 'billday', 'int', 'NULL', '', '', '', + 'edit_subject', 'char', 'NULL', 1, '', '', ], 'primary_key' => 'custnum', 'unique' => [ [ 'agentnum', 'agent_custid' ] ], diff --git a/FS/FS/TicketSystem/RT_External.pm b/FS/FS/TicketSystem/RT_External.pm index 8a8c3ffb4..f976ac0e3 100644 --- a/FS/FS/TicketSystem/RT_External.pm +++ b/FS/FS/TicketSystem/RT_External.pm @@ -403,5 +403,9 @@ sub create_ticket { return 'create_ticket unimplemented w/external RT (write something w/RT::Client::REST?)'; } +sub init { } #unimplemented + +sub selfservice_priority { '' } #unimplemented + 1; diff --git a/FS/FS/TicketSystem/RT_Internal.pm b/FS/FS/TicketSystem/RT_Internal.pm index 6ae8881a4..220b4a011 100644 --- a/FS/FS/TicketSystem/RT_Internal.pm +++ b/FS/FS/TicketSystem/RT_Internal.pm @@ -35,7 +35,6 @@ sub baseurl { sub access_right { my( $self, $session, $right ) = @_; - #return '' unless $conf->config('ticket_system'); return '' unless FS::Conf->new->config('ticket_system'); $session = $self->session($session); @@ -63,41 +62,34 @@ sub session { $session; } +my $firsttime = 1; + sub init { my $self = shift; + if ( $firsttime ) { + + # this part only needs to be done once + warn "$me init: loading RT libraries\n" if $DEBUG; + eval ' + use lib ( "/opt/rt3/local/lib", "/opt/rt3/lib" ); + use RT; + + #for web external auth... + use RT::Interface::Web; + '; + die $@ if $@; + + warn "$me init: loading RT config\n" if $DEBUG; + { + local $SIG{__DIE__}; + eval 'RT::LoadConfig();'; + } + die $@ if $@; - warn "$me init: loading RT libraries\n" if $DEBUG; - eval ' - use lib ( "/opt/rt3/local/lib", "/opt/rt3/lib" ); - use RT; - #it looks like the rest are taken care of these days in RT::InitClasses - #use RT::Ticket; - #use RT::Transactions; - #use RT::Users; - #use RT::CurrentUser; - #use RT::Templates; - #use RT::Queues; - #use RT::ScripActions; - #use RT::ScripConditions; - #use RT::Scrips; - #use RT::Groups; - #use RT::GroupMembers; - #use RT::CustomFields; - #use RT::CustomFieldValues; - #use RT::ObjectCustomFieldValues; - - #for web external auth... - use RT::Interface::Web; - '; - die $@ if $@; - - warn "$me init: loading RT config\n" if $DEBUG; - { - local $SIG{__DIE__}; - eval 'RT::LoadConfig();'; + $firsttime = 0; } - die $@ if $@; + # this needs to be done on each fork warn "$me init: initializing RT\n" if $DEBUG; { local $SIG{__DIE__}; @@ -108,6 +100,106 @@ sub init { warn "$me init: complete" if $DEBUG; } +=item customer_tickets CUSTNUM [ LIMIT ] [ PRIORITYVALUE ] + +Replacement for the one in RT_External so that we can access custom fields +properly. + +=cut + +sub _customer_tickets_search { + my ( $self, $custnum, $limit, $priority ) = @_; + + $custnum =~ /^\d+$/ or die "invalid custnum: $custnum"; + $limit =~ /^\d+$/ or die "invalid limit: $limit"; + + my $session = $self->session(); + my $CurrentUser = $session->{CurrentUser} + or die "unable to create an RT session"; + + my $Tickets = RT::Tickets->new($CurrentUser); + + my $rtql = "MemberOf = 'freeside://freeside/cust_main/$custnum'"; + + if ( defined( $priority ) ) { + my $custom_priority = FS::Conf->new->config('ticket_system-custom_priority_field'); + $rtql .= " AND CF.{$custom_priority} = '$priority'"; + } + + $rtql .= ' AND ( ' . + join(' OR ', map { "Status = '$_'" } $self->statuses) . + ' )'; + + $Tickets->FromSQL($rtql); + + $Tickets->RowsPerPage($limit); + + return $Tickets; +} + +sub customer_tickets { + my $Tickets = _customer_tickets_search(@_); + + my $conf = FS::Conf->new; + my $priority_order = + $conf->exists('ticket_system-priority_reverse') ? 'ASC' : 'DESC'; + my $custom_priority = + $conf->config('ticket_system-custom_priority_field') || ''; + + my @order_by; + my $ss_priority = selfservice_priority(); + push @order_by, { FIELD => "CF.{$ss_priority}", ORDER => $priority_order } + if $ss_priority; + push @order_by, + { FIELD => 'Priority', ORDER => $priority_order }, + { FIELD => 'Id', ORDER => 'DESC' }, + ; + + $Tickets->OrderByCols(@order_by); + + my @tickets; + while ( my $t = $Tickets->Next ) { + push @tickets, _ticket_info($t); + } + return \@tickets; +} + +sub num_customer_tickets { + my $Tickets = _customer_tickets_search(@_); + return $Tickets->CountAll; +} + +sub _ticket_info { + # Takes an RT::Ticket; returns a hashref of the ticket's fields, including + # custom fields. Also returns custom and selfservice priority values as + # _custom_priority and _selfservice_priority. + my $t = shift; + + my $custom_priority = + FS::Conf->new->config('ticket_system-custom_priority_field') || ''; + my $ss_priority = selfservice_priority(); + + my %ticket_info; + foreach my $name ( $t->ReadableAttributes ) { + # lowercase names, and skip attributes with non-scalar values + $ticket_info{lc($name)} = $t->$name if !ref($t->$name); + } + $ticket_info{'owner'} = $t->OwnerObj->Name; + $ticket_info{'queue'} = $t->QueueObj->Name; + foreach my $CF ( @{ $t->CustomFields->ItemsArrayRef } ) { + my $name = 'CF.{'.$CF->Name.'}'; + $ticket_info{$name} = $t->CustomFieldValuesAsString($CF->Id); + } + # make this easy to find + if ( $custom_priority ) { + $ticket_info{'_custom_priority'} = $ticket_info{"CF.{$custom_priority}"}; + } + if ( $ss_priority ) { + $ticket_info{'_selfservice_priority'} = $ticket_info{"CF.{$ss_priority}"}; + } + return \%ticket_info; +} + =item create_ticket SESSION_HASHREF, OPTION => VALUE ... Class method. Creates a ticket. If there is an error, returns the scalar @@ -219,8 +311,8 @@ sub create_ticket { Class method. Retrieves a ticket. If there is an error, returns the scalar error. Otherwise, currently returns a slightly tricky data structure containing -a list of the linked customers and each transaction's content, description, and -create time. +the ticket's attributes, a list of the linked customers, each transaction's +content, description, and create time. Accepts the following options: @@ -262,9 +354,50 @@ sub get_ticket { { txns => [ @txns ], custs => [ @custs ], + fields => _ticket_info($Ticket), }; } +=item get_ticket_object SESSION_HASHREF, OPTION => VALUE... + +Class method. Retrieve the RT::Ticket object with the specified +ticket_id. If custnum is supplied, will also check that the object +is a member of that customer. If there is no ticket or the custnum +check fails, returns nothing. The meaning of that case is +"to this customer, the ticket does not exist". + +Options: + +=over 4 + +=item ticket_id + +=item custnum + +=back + +=cut + +sub get_ticket_object { + my $self = shift; + my ($session, %opt) = @_; + $session = $self->session(shift); + my $Ticket = RT::Ticket->new($session->{CurrentUser}); + $Ticket->Load($opt{'ticket_id'}); + return if ( !$Ticket->id ); + my $custnum = $opt{'custnum'}; + if ( defined($custnum) && $custnum =~ /^\d+$/ ) { + # probably the most efficient way to check ticket ownership + my $Link = RT::Link->new($session->{CurrentUser}); + $Link->LoadByCols( LocalBase => $opt{'ticket_id'}, + Type => 'MemberOf', + Target => "freeside://freeside/cust_main/$custnum", + ); + return if ( !$Link->id ); + } + return $Ticket; +} + =item correspond_ticket SESSION_HASHREF, OPTION => VALUE ... @@ -427,5 +560,20 @@ sub _web_external_auth { } +=item selfservice_priority + +Returns the configured self-service priority field. + +=cut + +my $selfservice_priority; + +sub selfservice_priority { + return $selfservice_priority ||= do { + my $conf = FS::Conf->new; + $conf->config('ticket_system-selfservice_priority_field') || ''; + } +} + 1; diff --git a/FS/FS/cust_main.pm b/FS/FS/cust_main.pm index 4a7ad2ead..b1f71fd3f 100644 --- a/FS/FS/cust_main.pm +++ b/FS/FS/cust_main.pm @@ -324,6 +324,10 @@ A suggestion to events (see L<FS::part_bill_event">) to delay until this unix ti Discourage individual CDR printing, empty or `Y' +=item edit_subject + +Allow self-service editing of ticket subjects, empty or 'Y' + =back =head1 METHODS |