RT#38217: Send email when logging conditions are met
authorJonathan Prykop <jonathan@freeside.biz>
Wed, 21 Oct 2015 01:56:13 +0000 (20:56 -0500)
committerJonathan Prykop <jonathan@freeside.biz>
Wed, 21 Oct 2015 01:56:13 +0000 (20:56 -0500)
13 files changed:
FS/FS/Schema.pm
FS/FS/log.pm
FS/FS/log_email.pm [new file with mode: 0644]
FS/FS/msg_template.pm
FS/FS/msg_template/email.pm
httemplate/browse/log_email.html [new file with mode: 0644]
httemplate/edit/log_email.html [new file with mode: 0644]
httemplate/edit/msg_template/email.html
httemplate/edit/process/log_email.html [new file with mode: 0644]
httemplate/elements/menu.html
httemplate/elements/tr-select-msg_template.html [new file with mode: 0644]
httemplate/misc/delete-log_email.html [new file with mode: 0644]
httemplate/search/log.html

index ceb347d..7dc54f7 100644 (file)
@@ -6563,6 +6563,25 @@ sub tables_hashref {
                         ],
     },
 
                         ],
     },
 
+    'log_email' => {
+      'columns' => [
+        'logemailnum', 'serial', '', '', '', '',
+        'context', 'varchar', 'NULL', $char_d, '', '',
+        'min_level', 'int',  'NULL', '', '', '',
+        'msgnum', 'int', '',  '', '', '',
+        'to_addr', 'varchar', 'NULL',     255, '', '',
+      ],
+      'primary_key'  => 'logemailnum',
+      'unique'       => [],
+      'index'        => [ ['context'], ['min_level'] ],
+      'foreign_keys' => [
+                          { columns    => [ 'msgnum' ],
+                            table      => 'msg_template',
+                            references => [ 'msgnum' ],
+                          },
+                        ],
+    },
+
     'svc_alarm' => {
       'columns' => [
 #       name               type        null   length   default local
     'svc_alarm' => {
       'columns' => [
 #       name               type        null   length   default local
index a4ad214..b079105 100644 (file)
@@ -5,6 +5,7 @@ use base qw( FS::Record );
 use FS::Record qw( qsearch qsearchs dbdef );
 use FS::UID qw( dbh driver_name );
 use FS::log_context;
 use FS::Record qw( qsearch qsearchs dbdef );
 use FS::UID qw( dbh driver_name );
 use FS::log_context;
+use FS::log_email;
 
 =head1 NAME
 
 
 =head1 NAME
 
@@ -71,6 +72,8 @@ otherwise returns false.
 
 CONTEXT may be a list of context tags to attach to this record.
 
 
 CONTEXT may be a list of context tags to attach to this record.
 
+Will send emails according to the conditions in L<FS::log_email>.
+
 =cut
 
 sub insert {
 =cut
 
 sub insert {
@@ -78,6 +81,7 @@ sub insert {
   my $self = shift;
   my $error = $self->SUPER::insert;
   return $error if $error;
   my $self = shift;
   my $error = $self->SUPER::insert;
   return $error if $error;
+  my $contexts = {}; #for quick checks when sending emails
   foreach ( @_ ) {
     my $context = FS::log_context->new({
         'lognum'  => $self->lognum,
   foreach ( @_ ) {
     my $context = FS::log_context->new({
         'lognum'  => $self->lognum,
@@ -85,11 +89,40 @@ sub insert {
     });
     $error = $context->insert;
     return $error if $error;
     });
     $error = $context->insert;
     return $error if $error;
+    $contexts->{$_} = 1;
+  }
+  foreach my $log_email (
+    qsearch('log_email',
+      {
+        'disabled' => '',
+        'min_level' => {
+          'op' => '<=',
+          'value' => $self->level,
+        },
+      }
+    )
+  ) {
+    # shouldn't be a lot of these, so not packing this into the qsearch
+    next if $log_email->context && !$contexts->{$log_email->context};
+    my $msg_template = qsearchs('msg_template',{ 'msgnum' => $log_email->msgnum });
+    unless ($msg_template) {
+      warn "Could not send email when logging, could not load message template for logemailnum " . $log_email->logemailnum;
+      next;
+    }
+    my $emailerror = $msg_template->send(
+      'to'            => $log_email->to_addr,
+      'substitutions' => {
+        'loglevel'   => $FS::Log::LEVELS[$self->level], # which has hopefully been loaded...
+        'logcontext' => $log_email->context, # use the one that triggered the email
+        'logmessage' => $self->message,
+      },
+    );
+    warn "Could not send email when logging: $emailerror" if $emailerror;
   }
   '';
 }
 
   }
   '';
 }
 
-# the insert method can be inherited from FS::Record
+# these methods can be inherited from FS::Record
 
 sub delete  { die "Log entries can't be modified." };
 
 
 sub delete  { die "Log entries can't be modified." };
 
diff --git a/FS/FS/log_email.pm b/FS/FS/log_email.pm
new file mode 100644 (file)
index 0000000..9c53c23
--- /dev/null
@@ -0,0 +1,108 @@
+package FS::log_email;
+
+use strict;
+use base qw( FS::Record );
+use FS::Record qw( qsearch qsearchs dbdef );
+use FS::UID qw( dbh driver_name );
+
+=head1 NAME
+
+FS::log_email - Object methods for log email records
+
+=head1 SYNOPSIS
+
+  use FS::log_email;
+
+  $record = new FS::log_email \%hash;
+  $record = new FS::log_email { 'column' => 'value' };
+
+  $error = $record->insert;
+
+  $error = $new_record->replace($old_record);
+
+  $error = $record->delete;
+
+  $error = $record->check;
+
+=head1 DESCRIPTION
+
+An FS::log object represents the conditions for sending an email
+when a log entry is created.  FS::log inherits from FS::Record.  
+The following fields are currently supported:
+
+=over 4
+
+=item logemailnum - primary key
+
+=item context - the context that will trigger the email (all contexts if unspecified)
+
+=item min_level - the minimum log level that will trigger the email (all levels if unspecified)
+
+=item msgnum - the msg_template that will be used to send the email
+
+=item to_addr - who the email will be sent to (in addition to any bcc on the template)
+
+=back
+
+=head1 METHODS
+
+=over 4
+
+=item new HASHREF
+
+Creates a new log_email entry.
+
+=cut
+
+sub table { 'log_email'; }
+
+=item insert
+
+Adds this record to the database.  If there is an error, returns the error,
+otherwise returns false.
+
+=item delete
+
+Delete this record from the database.
+
+=item replace OLD_RECORD
+
+Replaces the OLD_RECORD with this one in the database.  If there is an error,
+returns the error, otherwise returns false.
+
+=item check
+
+Checks all fields to make sure this is a valid record.  If there is
+an error, returns the error, otherwise returns false.  Called by the insert
+and replace methods.
+
+=cut
+
+sub check {
+  my $self = shift;
+
+  my $error = 
+    $self->ut_numbern('logemailnum')
+    || $self->ut_textn('context') # not validating against list of contexts in log_context,
+                                  # because not even log_context check currently does so
+    || $self->ut_number('min_level')
+    || $self->ut_foreign_key('msgnum', 'msg_template', 'msgnum')
+    || $self->ut_textn('to_addr')
+  ;
+  return $error if $error;
+
+  $self->SUPER::check;
+}
+
+=back
+
+=head1 BUGS
+
+=head1 SEE ALSO
+
+L<FS::Record>, schema.html from the base documentation.
+
+=cut
+
+1;
+
index d17fd41..1d357b1 100644 (file)
@@ -274,7 +274,7 @@ Options are passed as a list of name/value pairs:
 
 =item cust_main
 
 
 =item cust_main
 
-Customer object (required).
+Customer object
 
 =item object
 
 
 =item object
 
@@ -324,7 +324,7 @@ sub prepare_substitutions {
   my( $self, %opt ) = @_;
 
   my $cust_main = $opt{'cust_main'}; # or die 'cust_main required';
   my( $self, %opt ) = @_;
 
   my $cust_main = $opt{'cust_main'}; # or die 'cust_main required';
-  my $object = $opt{'object'} or die 'object required';
+  my $object = $opt{'object'}; # or die 'object required';
 
   warn "preparing substitutions for '".$self->msgname."'\n"
     if $DEBUG;
 
   warn "preparing substitutions for '".$self->msgname."'\n"
     if $DEBUG;
index 377dbb1..83ff18f 100644 (file)
@@ -164,7 +164,7 @@ Options are passed as a list of name/value pairs:
 
 =item cust_main
 
 
 =item cust_main
 
-Customer object (required).
+Customer object
 
 =item object
 
 
 =item object
 
@@ -215,7 +215,7 @@ sub prepare {
   my( $self, %opt ) = @_;
 
   my $cust_main = $opt{'cust_main'}; # or die 'cust_main required';
   my( $self, %opt ) = @_;
 
   my $cust_main = $opt{'cust_main'}; # or die 'cust_main required';
-  my $object = $opt{'object'} or die 'object required';
+  my $object = $opt{'object'}; # or die 'object required';
 
   my $hashref = $self->prepare_substitutions(%opt);
 
 
   my $hashref = $self->prepare_substitutions(%opt);
 
@@ -365,7 +365,7 @@ sub prepare {
   my $env_to = join(', ', @to);
 
   my $cust_msg = FS::cust_msg->new({
   my $env_to = join(', ', @to);
 
   my $cust_msg = FS::cust_msg->new({
-      'custnum'   => $cust_main->custnum,
+      'custnum'   => $cust_main ? $cust_main->custnum : '',
       'msgnum'    => $self->msgnum,
       '_date'     => $time,
       'env_from'  => $env_from,
       'msgnum'    => $self->msgnum,
       '_date'     => $time,
       'env_from'  => $env_from,
diff --git a/httemplate/browse/log_email.html b/httemplate/browse/log_email.html
new file mode 100644 (file)
index 0000000..0f64dd4
--- /dev/null
@@ -0,0 +1,92 @@
+<% include('/elements/init_overlib.html') %>
+<% include('/browse/elements/browse.html',
+     'title'         => 'Log email condition configuration',
+     'name_singular' => 'condition',
+     'html_init'     => '<P STYLE="margin-top: 0">'
+                        . $add_condition_link
+                        . ' | '
+                        . $system_log_link
+                        . '</P>'
+                        . '<SCRIPT>'
+                        . $areyousure
+                        . '</SCRIPT>',
+     'query'         => $query,
+     'count_query'   => $count_query,
+     'header'      => [ '#',
+                        'Context', 
+                        'Min. Level', 
+                        'Template', 
+                        'To',
+                        '',
+                      ],
+     'fields'      => [ 'logemailnum',
+                        sub { $_[0]->context || '(all)' },
+                        sub { $FS::Log::LEVELS[$_[0]->min_level] },
+                        'msgname',
+                        'to_addr',
+                        $actions,
+                      ],
+     'sort_fields' => [ 'logemailnum',
+                        'context',
+                        'min_level',
+                        'msgname',
+                        'to_addr',
+                        '',
+                      ],
+     'links'       => [ $editlink,
+                        $editlink,
+                        $editlink,
+                        $editlink,
+                        $editlink,
+                        '',
+                      ],
+
+   ) %>
+
+<%init>
+
+my $curuser = $FS::CurrentUser::CurrentUser;
+
+die "access denied"
+  unless $curuser->access_right([ 'View system logs', 'Configuration' ]);
+
+my $add_condition_link = include('/elements/popup_link.html',
+  'action' => $p.'edit/log_email.html?popup=1',
+  'label'  => 'Add log email condition',
+  'actionlabel' => 'Add log email condition',
+);
+
+my $system_log_link = qq(<A HREF="${p}search/log.html">System Log</A>);
+
+my $query = {
+  'table'   => 'log_email',
+  'select'  => '*',
+  'addl_from' => 'LEFT JOIN msg_template USING (msgnum)',
+  'hashref' => { },
+};
+
+my $count_query = "SELECT COUNT(*) FROM log_email";
+
+my $actions = sub {
+  my $log_email = shift;
+  my $logemailnum = $log_email->logemailnum;
+  qq!<A HREF="javascript:areyousure_delete_log_email($logemailnum)">(delete)</A>!;
+};
+
+my $areyousure_onclick = include('/elements/popup_link_onclick.html',
+  'js_action' => q(') . $p . q(misc/delete-log_email.html?logemailnum=' + logemailnum),
+  'actionlabel' => 'Delete log email condition',
+);
+
+my $areyousure = <<EOF;
+function areyousure_delete_log_email(logemailnum) {
+  if (confirm('Are you sure you want to delete log email condition #'+logemailnum+'?')) {
+${areyousure_onclick}
+  }
+}
+EOF
+
+my $editlink = [ $p.'edit/log_email.html?logemailnum=', 'logemailnum' ];
+
+</%init>
+
diff --git a/httemplate/edit/log_email.html b/httemplate/edit/log_email.html
new file mode 100644 (file)
index 0000000..bbce7c7
--- /dev/null
@@ -0,0 +1,45 @@
+<% include( 'elements/edit.html',
+              'name_singular' => 'log email condition',
+              'table'  => 'log_email',
+              'fields' => [
+                            { 'field' => 'context',
+                              'type' => 'select',
+                              'options' => [ '', @contexts ],
+                              'labels' => { '' => '(all)', map { $_ => $_ } @contexts },
+                              'curr_value' => scalar($cgi->param('context')),
+                            },
+                            { 'field' => 'min_level',
+                              'type'  => 'select',
+                              'options' => [ 0..7 ],
+                              'labels' => { map {$_ => $FS::Log::LEVELS[$_]} 0..7 },
+                              'curr_value' => scalar($cgi->param('min_level')),
+                            },
+                            'to_addr',
+                            { 'field' => 'msgnum',
+                              'type' => 'select-msg_template',
+                              'empty_label' => 'Select template',
+                              'required' => 1,
+                            },
+                          ],
+              'labels' => { 
+                            'context' => 'Context',
+                            'min_level' => 'Min. Level',
+                            'to_addr' => 'To',
+                            'msgnum' => 'Message',
+                          },
+              'viewall_dir' => 'browse',
+              'popup' => $opts{'popup'},
+              'form_init' => $opts{'popup'} ? q(<INPUT TYPE="hidden" NAME="popup" VALUE="1">) : '',
+           )
+%>
+<%once>
+my @contexts = sort FS::log_context->contexts;
+</%once>
+<%init>
+
+my %opts = @_;
+
+die "access denied"
+  unless $FS::CurrentUser::CurrentUser->access_right([ 'View system logs', 'Configuration' ]);
+
+</%init>
index dc70ef6..b0c1aa3 100644 (file)
@@ -302,6 +302,11 @@ my %substitutions = (
     '$payinfo'        => 'Card/account# (masked)',
     '$error'          => 'Decline reason',
   ],
     '$payinfo'        => 'Card/account# (masked)',
     '$error'          => 'Decline reason',
   ],
+  'system_log' => [
+    '$logmessage'     => 'Log entry message',
+    '$loglevel'       => 'Log entry level',
+    '$logcontext'     => 'Log entry context',
+  ],
 );
 
 tie my %sections, 'Tie::IxHash', (
 );
 
 tie my %sections, 'Tie::IxHash', (
@@ -315,6 +320,7 @@ tie my %sections, 'Tie::IxHash', (
 'svc_domain'=> 'Domain service fields',
 'svc_phone' => 'Phone service fields',
 'svc_broadband' => 'Broadband service fields',
 'svc_domain'=> 'Domain service fields',
 'svc_phone' => 'Phone service fields',
 'svc_broadband' => 'Broadband service fields',
+'system_log' => 'System log fields',
 );
 
 my $widget = new HTML::Widgets::SelectLayers(
 );
 
 my $widget = new HTML::Widgets::SelectLayers(
diff --git a/httemplate/edit/process/log_email.html b/httemplate/edit/process/log_email.html
new file mode 100644 (file)
index 0000000..769e180
--- /dev/null
@@ -0,0 +1,18 @@
+<% include('elements/process.html',
+    'table' => 'log_email',
+    %processopts
+   ) %>
+<%init>
+
+my %opts = @_;
+
+my $curuser = $FS::CurrentUser::CurrentUser;
+
+die "access denied"
+  unless $curuser->access_right([ 'View system logs', 'Configuration' ]);
+
+my %processopts = $opts{'popup'}
+  ? ( 'popup_reload' => 'Logging email added' )
+  : ( 'redirect' => $fsurl.'browse/log_email.html?' ); # id will be needlessly appended, should be harmless
+
+</%init>
index ea69331..dcc02c2 100644 (file)
@@ -787,6 +787,10 @@ $config_misc{'Inventory classes and inventory'} = [ $fsurl.'browse/inventory_cla
 $config_misc{'Upload targets'} = [ $fsurl.'browse/upload_target.html', 'Billing and payment upload destinations' ]
   if $curuser->access_right('Configuration');
 
 $config_misc{'Upload targets'} = [ $fsurl.'browse/upload_target.html', 'Billing and payment upload destinations' ]
   if $curuser->access_right('Configuration');
 
+$config_misc{'System log emails'} = [ $fsurl.'browse/log_email.html', 'Configure conditions for sending email when logging' ]
+  if $curuser->access_right('View system logs')
+  || $curuser->access_right('Configuration');
+
 tie my %config_menu, 'Tie::IxHash';
 if ( $curuser->access_right('Configuration' ) ) {
   %config_menu = (
 tie my %config_menu, 'Tie::IxHash';
 if ( $curuser->access_right('Configuration' ) ) {
   %config_menu = (
diff --git a/httemplate/elements/tr-select-msg_template.html b/httemplate/elements/tr-select-msg_template.html
new file mode 100644 (file)
index 0000000..1f899e0
--- /dev/null
@@ -0,0 +1,12 @@
+<% include('/elements/tr-td-label.html',
+     'label'    => $opt{'label'} || 'Message template: ',
+     'required' => $opt{'required'} ) %>
+  <TD><% include('select-msg_template.html', %opt) %></TD>
+</TR>
+
+<%init>
+
+my %opt = @_;
+
+</%init>
+
diff --git a/httemplate/misc/delete-log_email.html b/httemplate/misc/delete-log_email.html
new file mode 100644 (file)
index 0000000..cc17b15
--- /dev/null
@@ -0,0 +1,20 @@
+% if ($error) {
+<P STYLE="color: red"><% $error %></P>
+% } else {
+<H1>Log email condition deleted</H1>
+<SCRIPT>
+window.top.location.reload();
+</SCRIPT>
+% }
+
+<%init>
+die "access denied"
+  unless $FS::CurrentUser::CurrentUser->access_right([ 'View system logs', 'Configuration' ]);
+
+my $logemailnum = $cgi->param('logemailnum');
+$logemailnum =~ /^\d+$/ or die "bad logemailnum '$logemailnum'";
+my $log_email = FS::log_email->by_key($logemailnum)
+  or die "logemailnum '$logemailnum' not found";
+my $error = $log_email->delete;
+</%init>
+
index d1bfb6c..a707928 100644 (file)
@@ -1,6 +1,7 @@
 <& elements/search.html, 
   'title'         => 'System Log',
   'name_singular' => 'event',
 <& elements/search.html, 
   'title'         => 'System Log',
   'name_singular' => 'event',
+  'menubar'       => \@menubar,
   'html_init'     => include('.head'),
   'query'         => $query,
   'count_query'   => $count_query,
   'html_init'     => include('.head'),
   'query'         => $query,
   'count_query'   => $count_query,
@@ -204,6 +205,9 @@ my $curuser = $FS::CurrentUser::CurrentUser;
 die "access denied"
   unless $curuser->access_right([ 'View system logs', 'Configuration' ]);
 
 die "access denied"
   unless $curuser->access_right([ 'View system logs', 'Configuration' ]);
 
+my @menubar = ();
+push @menubar, qq(<A HREF="${fsurl}browse/log_email.html" STYLE="text-decoration: underline;">Configure conditions for sending email when logging</A>),
+
 $cgi->param('min_level', 0) unless defined($cgi->param('min_level'));
 $cgi->param('max_level', 7) unless defined($cgi->param('max_level'));
 
 $cgi->param('min_level', 0) unless defined($cgi->param('min_level'));
 $cgi->param('max_level', 7) unless defined($cgi->param('max_level'));