saved searches, core stuff, #72101
[freeside.git] / FS / FS / saved_search.pm
diff --git a/FS/FS/saved_search.pm b/FS/FS/saved_search.pm
new file mode 100644 (file)
index 0000000..075d759
--- /dev/null
@@ -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<FS::access_user> 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<hash> 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 = '<h3>Error</h3>
+  <p>There was an error generating the report "'.$self->searchname.'".</p>
+  <p>' . $self->path . '?' . $self->query_string . '</p>
+  <p>' . $_ . '</p>';
+  }
+
+  return $outbuf;
+}
+
+=back
+
+=head1 SEE ALSO
+
+L<FS::Record>
+
+=cut
+
+1;
+