add email delivery of saved searches, #72101
authorMark Wells <mark@freeside.biz>
Wed, 7 Sep 2016 22:08:14 +0000 (15:08 -0700)
committerMark Wells <mark@freeside.biz>
Wed, 7 Sep 2016 22:08:14 +0000 (15:08 -0700)
FS/FS/Cron/send_subscribed.pm [new file with mode: 0644]
FS/FS/Mason.pm
FS/FS/Schema.pm
FS/FS/log_context.pm
FS/FS/saved_search.pm
FS/FS/saved_search_option.pm [deleted file]
FS/MANIFEST
FS/bin/freeside-daily
FS/t/saved_search_option.t [deleted file]

diff --git a/FS/FS/Cron/send_subscribed.pm b/FS/FS/Cron/send_subscribed.pm
new file mode 100644 (file)
index 0000000..2b1f662
--- /dev/null
@@ -0,0 +1,32 @@
+package FS::Cron::send_subscribed;
+
+use strict;
+use base 'Exporter';
+use FS::saved_search;
+use FS::Record qw(qsearch);
+use FS::queue;
+
+our @EXPORT_OK = qw( send_subscribed );
+our $DEBUG = 1;
+
+sub send_subscribed {
+
+  my @subs = qsearch('saved_search', {
+    'disabled'  => '',
+    'freq'      => { op => '!=', value => '' },
+  });
+  foreach my $saved_search (@subs) {
+    my $date = $saved_search->next_send_date;
+    warn "checking '".$saved_search->searchname."' with date $date\n"
+      if $DEBUG;
+    
+    if ( $^T > $saved_search->next_send_date ) {
+      warn "queueing delivery\n";
+      my $job = FS::queue->new({ job => 'FS::saved_search::queueable_send' });
+      $job->insert( $saved_search->searchnum );
+    }
+  }
+
+}
+
+1;
index e8a1af6..ee87b2d 100644 (file)
@@ -416,7 +416,6 @@ if ( -e $addl_handler_use_file ) {
   use FS::commission_schedule;
   use FS::commission_rate;
   use FS::saved_search;
-  use FS::saved_search_option;
   # Sammath Naur
 
   if ( $FS::Mason::addl_handler_use ) {
index 4ff9db2..df987ff 100644 (file)
@@ -7492,6 +7492,7 @@ sub tables_hashref {
         'usernum',      'int',     'NULL',      '', '', '',
         'searchname',   'varchar', '',     $char_d, '', '',
         'path',         'varchar', '',     $char_d, '', '',
+        'params',       'text',    'NULL',      '', '', '',
         'disabled',     'char',    'NULL',       1, '', '',
         'freq',         'varchar', 'NULL',      16, '', '',
         'last_sent',    'int',     'NULL',      '', '', '',
@@ -7507,23 +7508,6 @@ sub tables_hashref {
                          ],
     },
 
-    'saved_search_option' => {
-      'columns' => [
-        'optionnum',  'serial',  '',          '', '', '',
-        'searchnum',  'int',     '',          '', '', '',
-        'optionname', 'varchar', '', $char_d, '', '', 
-        'optionvalue', 'text', 'NULL', '', '', '', 
-      ],
-      'primary_key'   => 'optionnum',
-      'unique'        => [ [ 'searchnum', 'optionname' ] ],
-      'index'         => [],
-      'foreign_keys'  => [
-                           { columns => [ 'searchnum' ],
-                             table   => 'saved_search',
-                           },
-                         ],
-    },
     # name type nullability length default local
 
     #'new_table' => {
index 37befb5..ee3e413 100644 (file)
@@ -13,6 +13,8 @@ my @contexts = ( qw(
   FS::pay_batch::import_from_gateway
   FS::part_pkg
   FS::Misc::Geo::standardize_uscensus
+  FS::saved_search::send
+  FS::saved_search::render
   Cron::bill
   Cron::backup
   Cron::upload
index 075d759..252dc71 100644 (file)
@@ -1,13 +1,15 @@
 package FS::saved_search;
-use base qw( FS::option_Common FS::Record );
+use base qw( FS::Record );
 
 use strict;
 use FS::Record qw( qsearch qsearchs );
 use FS::Conf;
+use FS::Log;
+use FS::Misc qw(send_email);
+use MIME::Entity;
 use Class::Load 'load_class';
 use URI::Escape;
 use DateTime;
-use Try::Tiny;
 
 =head1 NAME
 
@@ -56,6 +58,10 @@ A descriptive name.
 
 The path to the page within the Mason document space.
 
+=item params
+
+The query string for the search.
+
 =item disabled
 
 'Y' to hide the search from the user's Reports / Saved menu.
@@ -128,6 +134,7 @@ sub check {
     #|| $self->ut_foreign_keyn('usernum', 'access_user', 'usernum')
     || $self->ut_text('searchname')
     || $self->ut_text('path')
+    || $self->ut_textn('params') # URL-escaped, so ut_textn
     || $self->ut_flag('disabled')
     || $self->ut_enum('freq', [ '', 'daily', 'weekly', 'monthly' ])
     || $self->ut_numbern('last_sent')
@@ -138,6 +145,14 @@ sub check {
   $self->SUPER::check;
 }
 
+sub replace_check {
+  my ($new, $old) = @_;
+  if ($new->usernum != $old->usernum) {
+    return "can't change owner of a saved search";
+  }
+  '';
+}
+
 =item next_send_date
 
 Returns the next date this report should be sent next. If it's not set for
@@ -168,8 +183,6 @@ Returns the CGI query string for the parameters to this report.
 
 =cut
 
-# multivalued options are newline-separated in the database
-
 sub query_string {
   my $self = shift;
 
@@ -177,12 +190,7 @@ sub query_string {
   $type = 'html-print' if $type eq '' || $type eq 'html';
   $type = '.xls' if $type eq 'xls';
   my $query = "_type=$type";
-  my %options = $self->options;
-  foreach my $k (keys %options) {
-    foreach my $v (split("\n", $options{$k})) {
-      $query .= ';' . uri_escape($k) . '=' . uri_escape($v);
-    }
-  }
+  $query .= ';' . $self->params if $self->params;
   $query;
 }
 
@@ -194,6 +202,7 @@ Returns the report content as an HTML or Excel file.
 
 sub render {
   my $self = shift;
+  my $log = FS::Log->new('FS::saved_search::render');
   my $outbuf;
 
   # delayed loading
@@ -214,7 +223,7 @@ sub render {
 #  local $ENV{SERVER_NAME} = 'localhost'; #?
 #  local $ENV{SCRIPT_NAME} = '/freeside'. $self->path;
 
-  my $mason_request = $fs_interp->make_request(comp => $self->path);
+  my $mason_request = $fs_interp->make_request(comp => '/' . $self->path);
 
   local $@;
   eval { $mason_request->exec(); };
@@ -224,9 +233,9 @@ sub render {
       $error = $error->message;
     }
 
-    warn "Error rendering " . $self->path .
+    $log->error("Error rendering " . $self->path .
          " for " . $self->access_user->username .
-         ":\n$error\n";
+         ":\n$error\n");
     # send it to the user anyway, so there's a way to diagnose the error
     $outbuf = '<h3>Error</h3>
   <p>There was an error generating the report "'.$self->searchname.'".</p>
@@ -237,6 +246,63 @@ sub render {
   return $outbuf;
 }
 
+=item send
+
+Sends the search by email. If anything fails, logs and returns an error.
+
+=cut
+
+sub send {
+  my $self = shift;
+  my $log = FS::Log->new('FS::saved_search::send');
+  my $conf = FS::Conf->new;
+  my $user = $self->access_user;
+  my $username = $user->username;
+  my $user_email = $user->option('email_address');
+  my $error;
+  if (!$user_email) {
+    $error = "User '$username' has no email address.";
+    $log->error($error);
+    return $error;
+  }
+  $log->debug('Rendering saved search');
+  my $content = $self->render;
+  # XXX come back to this for content-type options
+  my $part = MIME::Entity->build(
+    'Type'        => 'text/html',
+    'Encoding'    => 'quoted-printable', # change this for spreadsheet
+    'Disposition' => 'inline',
+    'Data'        => $content,
+  );
+
+  my %email_param = (
+    'from'      => $conf->config('invoice_from'),
+    'to'        => $user_email,
+    'subject'   => $self->searchname,
+    'nobody'    => 1,
+    'mimeparts' => [ $part ],
+  );
+
+  $log->debug('Sending to '.$user_email);
+  $error = send_email(%email_param);
+
+  # update the timestamp
+  $self->set('last_sent', time);
+  $error ||= $self->replace;
+  if ($error) {
+    $log->error($error);
+    return $error;
+  }
+
+}
+
+sub queueable_send {
+  my $searchnum = shift;
+  my $self = FS::saved_search->by_key($searchnum)
+    or die "searchnum $searchnum not found\n";
+  $self->send;
+}
+
 =back
 
 =head1 SEE ALSO
diff --git a/FS/FS/saved_search_option.pm b/FS/FS/saved_search_option.pm
deleted file mode 100644 (file)
index f349af3..0000000
+++ /dev/null
@@ -1,124 +0,0 @@
-package FS::saved_search_option;
-use base qw( FS::Record );
-
-use strict;
-use FS::Record qw( qsearch qsearchs );
-
-=head1 NAME
-
-FS::saved_search_option - Object methods for saved_search_option records
-
-=head1 SYNOPSIS
-
-  use FS::saved_search_option;
-
-  $record = new FS::saved_search_option \%hash;
-  $record = new FS::saved_search_option { 'column' => 'value' };
-
-  $error = $record->insert;
-
-  $error = $new_record->replace($old_record);
-
-  $error = $record->delete;
-
-  $error = $record->check;
-
-=head1 DESCRIPTION
-
-An FS::saved_search_option object represents a CGI parameter for a report
-saved in L<FS::saved_search>.  FS::saved_search_option inherits from
-FS::Record.  The following fields are currently supported:
-
-=over 4
-
-=item optionnum
-
-primary key
-
-=item searchnum
-
-searchnum
-
-=item optionname
-
-optionname
-
-=item optionvalue
-
-optionvalue
-
-
-=back
-
-=head1 METHODS
-
-=over 4
-
-=item new HASHREF
-
-Creates a new parameter.  To add the record to the database, see
-L<"insert">.
-
-Note that this stores the hash reference, not a distinct copy of the hash it
-points to.  You can ask the object for a copy with the I<hash> method.
-
-=cut
-
-# the new method can be inherited from FS::Record, if a table method is defined
-
-sub table { 'saved_search_option'; }
-
-=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 example.  If there is
-an error, returns the error, otherwise returns false.  Called by the insert
-and replace methods.
-
-=cut
-
-# the check method should currently be supplied - FS::Record contains some
-# data checking routines
-
-sub check {
-  my $self = shift;
-
-# unpack these from the format used by CGI
-  my $optionvalue = $self->optionvalue;
-  $optionvalue =~ s/\0/\n/g;
-
-  my $error = 
-    $self->ut_numbern('optionnum')
-    || $self->ut_number('searchnum')
-#   || $self->ut_foreign_key('searchnum', 'saved_search', 'searchnum')
-    || $self->ut_text('optionname')
-    || $self->ut_textn('optionvalue')
-  ;
-  return $error if $error;
-
-  $self->SUPER::check;
-}
-
-=back
-
-=head1 SEE ALSO
-
-L<FS::Record>
-
-=cut
-
-1;
-
index d06f263..73a740f 100644 (file)
@@ -876,5 +876,3 @@ FS/commission_rate.pm
 t/commission_rate.t
 FS/saved_search.pm
 t/saved_search.t
-FS/saved_search_option.pm
-t/saved_search_option.t
index 6a2daf9..03d2350 100755 (executable)
@@ -79,6 +79,10 @@ pay_batch_receive(%opt);
 use FS::Cron::export_batch qw(export_batch_submit);
 export_batch_submit(%opt);
 
+#does nothing unless there are users with subscribed searches
+use FS::Cron::send_subscribed qw(send_subscribed);
+send_subscribed(%opt);
+
 #clears out cacti imports & deletes select database cache files
 use FS::Cron::cleanup qw( cleanup cleanup_before_backup );
 cleanup_before_backup();
diff --git a/FS/t/saved_search_option.t b/FS/t/saved_search_option.t
deleted file mode 100644 (file)
index f30bfb8..0000000
+++ /dev/null
@@ -1,5 +0,0 @@
-BEGIN { $| = 1; print "1..1\n" }
-END {print "not ok 1\n" unless $loaded;}
-use FS::saved_search_option;
-$loaded=1;
-print "ok 1\n";