From 78aa70d55d6f9a05e5bb841b719899039133c107 Mon Sep 17 00:00:00 2001 From: Mark Wells Date: Thu, 8 Sep 2016 11:25:07 -0700 Subject: [PATCH] saved searches, core stuff, #72101 --- FS/FS/Mason.pm | 2 + FS/FS/Schema.pm | 38 +++++++ FS/FS/saved_search.pm | 249 +++++++++++++++++++++++++++++++++++++++++++ FS/FS/saved_search_option.pm | 124 +++++++++++++++++++++ FS/MANIFEST | 4 + FS/t/saved_search.t | 5 + FS/t/saved_search_option.t | 5 + 7 files changed, 427 insertions(+) create mode 100644 FS/FS/saved_search.pm create mode 100644 FS/FS/saved_search_option.pm create mode 100644 FS/t/saved_search.t create mode 100644 FS/t/saved_search_option.t diff --git a/FS/FS/Mason.pm b/FS/FS/Mason.pm index 6fc4bf09f..4d910a356 100644 --- a/FS/FS/Mason.pm +++ b/FS/FS/Mason.pm @@ -393,6 +393,8 @@ if ( -e $addl_handler_use_file ) { use FS::olt_site; 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 b7ec7df19..76a370bb6 100644 --- a/FS/FS/Schema.pm +++ b/FS/FS/Schema.pm @@ -5220,6 +5220,44 @@ sub tables_hashref { ], }, + 'saved_search' => { + 'columns' => [ + 'searchnum', 'serial', '', '', '', '', + 'usernum', 'int', 'NULL', '', '', '', + 'searchname', 'varchar', '', $char_d, '', '', + 'path', 'varchar', '', $char_d, '', '', + 'disabled', 'char', 'NULL', 1, '', '', + 'freq', 'varchar', 'NULL', 16, '', '', + 'last_sent', 'int', 'NULL', '', '', '', + 'format', 'varchar', 'NULL', 32, '', '', + ], + 'primary_key' => 'searchnum', + 'unique' => [], + 'index' => [], + 'foreign_keys' => [ + { columns => [ 'usernum' ], + table => 'access_user', + }, + ], + }, + + '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/saved_search.pm b/FS/FS/saved_search.pm new file mode 100644 index 000000000..075d759f6 --- /dev/null +++ b/FS/FS/saved_search.pm @@ -0,0 +1,249 @@ +package FS::saved_search; +use base qw( FS::option_Common FS::Record ); + +use strict; +use FS::Record qw( qsearch qsearchs ); +use FS::Conf; +use Class::Load 'load_class'; +use URI::Escape; +use DateTime; +use Try::Tiny; + +=head1 NAME + +FS::saved_search - Object methods for saved_search records + +=head1 SYNOPSIS + + use FS::saved_search; + + $record = new FS::saved_search \%hash; + $record = new FS::saved_search { 'column' => 'value' }; + + $error = $record->insert; + + $error = $new_record->replace($old_record); + + $error = $record->delete; + + $error = $record->check; + +=head1 DESCRIPTION + +An FS::saved_search object represents a search (a page in the backoffice +UI, typically under search/ or browse/) which a user has saved for future +use or periodic email delivery. + +FS::saved_search inherits from FS::Record. The following fields are +currently supported: + +=over 4 + +=item searchnum + +primary key + +=item usernum + +usernum of the L that created the search. Currently, email +reports will only be sent to this user. + +=item searchname + +A descriptive name. + +=item path + +The path to the page within the Mason document space. + +=item disabled + +'Y' to hide the search from the user's Reports / Saved menu. + +=item freq + +A frequency for email delivery of this report: daily, weekly, or +monthly, or null to disable it. + +=item last_sent + +The timestamp of the last time this report was sent. + +=item format + +'html', 'xls', or 'csv'. Not all reports support all of these. + +=back + +=head1 METHODS + +=over 4 + +=item new HASHREF + +Creates a new saved search. To add it 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 + +sub table { 'saved_search'; } + +=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. + +=cut + +# the replace method can be inherited from FS::Record + +=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; + + my $error = + $self->ut_numbern('searchnum') + || $self->ut_number('usernum') + #|| $self->ut_foreign_keyn('usernum', 'access_user', 'usernum') + || $self->ut_text('searchname') + || $self->ut_text('path') + || $self->ut_flag('disabled') + || $self->ut_enum('freq', [ '', 'daily', 'weekly', 'monthly' ]) + || $self->ut_numbern('last_sent') + || $self->ut_enum('format', [ '', 'html', 'csv', 'xls' ]) + ; + return $error if $error; + + $self->SUPER::check; +} + +=item next_send_date + +Returns the next date this report should be sent next. If it's not set for +periodic email sending, returns undef. If it is set up but has never been +sent before, returns zero. + +=cut + +sub next_send_date { + my $self = shift; + my $freq = $self->freq or return undef; + return 0 unless $self->last_sent; + my $dt = DateTime->from_epoch(epoch => $self->last_sent); + $dt->truncate(to => 'day'); + if ($freq eq 'daily') { + $dt->add(days => 1); + } elsif ($freq eq 'weekly') { + $dt->add(weeks => 1); + } elsif ($freq eq 'monthly') { + $dt->add(months => 1); + } + $dt->epoch; +} + +=item query_string + +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; + + my $type = $self->format; + $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; +} + +=item render + +Returns the report content as an HTML or Excel file. + +=cut + +sub render { + my $self = shift; + my $outbuf; + + # delayed loading + load_class('FS::Mason'); + RT::LoadConfig(); + RT::Init(); + + # do this before setting QUERY_STRING/FSURL + my ($fs_interp) = FS::Mason::mason_interps('standalone', + outbuf => \$outbuf + ); + $fs_interp->error_mode('fatal'); + $fs_interp->error_format('text'); + + local $FS::CurrentUser::CurrentUser = $self->access_user; + local $FS::Mason::Request::QUERY_STRING = $self->query_string; + local $FS::Mason::Request::FSURL = ''; #? +# local $ENV{SERVER_NAME} = 'localhost'; #? +# local $ENV{SCRIPT_NAME} = '/freeside'. $self->path; + + my $mason_request = $fs_interp->make_request(comp => $self->path); + + local $@; + eval { $mason_request->exec(); }; + if ($@) { + my $error = $@; + if ( ref($error) eq 'HTML::Mason::Exception' ) { + $error = $error->message; + } + + warn "Error rendering " . $self->path . + " for " . $self->access_user->username . + ":\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.'".

+

' . $self->path . '?' . $self->query_string . '

+

' . $_ . '

'; + } + + return $outbuf; +} + +=back + +=head1 SEE ALSO + +L + +=cut + +1; + diff --git a/FS/FS/saved_search_option.pm b/FS/FS/saved_search_option.pm new file mode 100644 index 000000000..f349af393 --- /dev/null +++ b/FS/FS/saved_search_option.pm @@ -0,0 +1,124 @@ +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 c060c140c..8bee83b45 100644 --- a/FS/MANIFEST +++ b/FS/MANIFEST @@ -810,3 +810,7 @@ FS/webservice_log.pm t/webservice_log.t 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/t/saved_search.t b/FS/t/saved_search.t new file mode 100644 index 000000000..8155c6d76 --- /dev/null +++ b/FS/t/saved_search.t @@ -0,0 +1,5 @@ +BEGIN { $| = 1; print "1..1\n" } +END {print "not ok 1\n" unless $loaded;} +use FS::saved_search; +$loaded=1; +print "ok 1\n"; diff --git a/FS/t/saved_search_option.t b/FS/t/saved_search_option.t new file mode 100644 index 000000000..f30bfb806 --- /dev/null +++ b/FS/t/saved_search_option.t @@ -0,0 +1,5 @@ +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