summaryrefslogtreecommitdiff
path: root/FS/FS
diff options
context:
space:
mode:
authormark <mark>2011-06-27 07:11:01 +0000
committermark <mark>2011-06-27 07:11:01 +0000
commit12f4cc4b100b849de3584d5d1a2376cebcd8729f (patch)
tree9b6c5c8badd6114034e59bffe5277696f93de4b1 /FS/FS
parentc1e316ef66e35dadb888b4e59047ba51082f198a (diff)
self-service ticket priority and edit subject, #13199
Diffstat (limited to 'FS/FS')
-rw-r--r--FS/FS/ClientAPI/MyAccount.pm168
-rw-r--r--FS/FS/Conf.pm38
-rw-r--r--FS/FS/Schema.pm1
-rw-r--r--FS/FS/TicketSystem/RT_External.pm4
-rw-r--r--FS/FS/TicketSystem/RT_Internal.pm214
-rw-r--r--FS/FS/cust_main.pm4
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