From 4496a23e58b830db4c2d10ad17dfb0d531b15859 Mon Sep 17 00:00:00 2001 From: Mark Wells Date: Thu, 8 Sep 2016 11:30:58 -0700 Subject: [PATCH] add email delivery of saved searches, #72101 --- FS/FS/Cron/send_subscribed.pm | 32 +++++++++++ FS/FS/Mason.pm | 1 - FS/FS/Schema.pm | 18 +----- FS/FS/log_context.pm | 2 + FS/FS/saved_search.pm | 92 ++++++++++++++++++++++++++----- FS/FS/saved_search_option.pm | 124 ------------------------------------------ FS/MANIFEST | 2 - FS/bin/freeside-daily | 4 ++ FS/t/saved_search_option.t | 5 -- 9 files changed, 118 insertions(+), 162 deletions(-) create mode 100644 FS/FS/Cron/send_subscribed.pm delete mode 100644 FS/FS/saved_search_option.pm delete mode 100644 FS/t/saved_search_option.t diff --git a/FS/FS/Cron/send_subscribed.pm b/FS/FS/Cron/send_subscribed.pm new file mode 100644 index 000000000..2b1f662e6 --- /dev/null +++ b/FS/FS/Cron/send_subscribed.pm @@ -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; diff --git a/FS/FS/Mason.pm b/FS/FS/Mason.pm index 4d910a356..bdae3938c 100644 --- a/FS/FS/Mason.pm +++ b/FS/FS/Mason.pm @@ -394,7 +394,6 @@ if ( -e $addl_handler_use_file ) { use FS::access_user_page_pref; use FS::part_svc_msgcat; use FS::saved_search; - use FS::saved_search_option; # Sammath Naur if ( $FS::Mason::addl_handler_use ) { diff --git a/FS/FS/Schema.pm b/FS/FS/Schema.pm index 76a370bb6..57f347555 100644 --- a/FS/FS/Schema.pm +++ b/FS/FS/Schema.pm @@ -5226,6 +5226,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', '', '', '', @@ -5241,23 +5242,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' => { diff --git a/FS/FS/log_context.pm b/FS/FS/log_context.pm index d7ea26b37..0d6220915 100644 --- a/FS/FS/log_context.pm +++ b/FS/FS/log_context.pm @@ -12,6 +12,8 @@ my @contexts = ( qw( FS::cust_main::Billing_Realtime::realtime_verify_bop FS::part_pkg FS::Misc::Geo::standardize_uscensus + FS::saved_search::send + FS::saved_search::render Cron::bill Cron::upload spool_upload diff --git a/FS/FS/saved_search.pm b/FS/FS/saved_search.pm index ffcd06b2a..b008594ed 100644 --- a/FS/FS/saved_search.pm +++ b/FS/FS/saved_search.pm @@ -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 = '

Error

There was an error generating the report "'.$self->searchname.'".

@@ -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; +} + #3.x sub access_user { my $self = shift; diff --git a/FS/FS/saved_search_option.pm b/FS/FS/saved_search_option.pm deleted file mode 100644 index f349af393..000000000 --- a/FS/FS/saved_search_option.pm +++ /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_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 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 - -=cut - -1; - diff --git a/FS/MANIFEST b/FS/MANIFEST index 8bee83b45..93835936a 100644 --- a/FS/MANIFEST +++ b/FS/MANIFEST @@ -812,5 +812,3 @@ FS/access_user_page_pref.pm t/access_user_page_pref.t FS/saved_search.pm t/saved_search.t -FS/saved_search_option.pm -t/saved_search_option.t diff --git a/FS/bin/freeside-daily b/FS/bin/freeside-daily index 1162e7911..4d432ef06 100755 --- a/FS/bin/freeside-daily +++ b/FS/bin/freeside-daily @@ -74,6 +74,10 @@ export_batch_submit(%opt); use FS::Cron::agent_email qw(agent_email); agent_email(%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 index f30bfb806..000000000 --- a/FS/t/saved_search_option.t +++ /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"; -- 2.11.0