Merge branch 'FREESIDE_3_BRANCH' of git.freeside.biz:/home/git/freeside into 3.x
authorMark Wells <mark@freeside.biz>
Thu, 8 Sep 2016 21:54:58 +0000 (14:54 -0700)
committerMark Wells <mark@freeside.biz>
Thu, 8 Sep 2016 21:54:58 +0000 (14:54 -0700)
22 files changed:
FS/FS/CGI.pm
FS/FS/Cron/send_subscribed.pm [new file with mode: 0644]
FS/FS/Mason.pm
FS/FS/Schema.pm
FS/FS/access_user.pm
FS/FS/cdr/callplus.pm [new file with mode: 0644]
FS/FS/log_context.pm
FS/FS/saved_search.pm [new file with mode: 0644]
FS/MANIFEST
FS/bin/freeside-daily
FS/t/saved_search.t [new file with mode: 0644]
httemplate/browse/saved_search.html [new file with mode: 0644]
httemplate/edit/process/elements/process.html
httemplate/edit/process/saved_search.html [new file with mode: 0644]
httemplate/edit/saved_search.html [new file with mode: 0644]
httemplate/elements/header-popup.html
httemplate/elements/menu.html
httemplate/elements/tr-fixed-date.html
httemplate/misc/delete-saved_search.html [new file with mode: 0644]
httemplate/search/elements/search-html.html
httemplate/search/elements/search-xls.html
httemplate/search/elements/search.html

index e1645f0..8be823a 100644 (file)
@@ -78,21 +78,17 @@ Sets an http header.
 
 sub http_header {
   my ( $header, $value ) = @_;
-  if (exists $ENV{MOD_PERL}) {
-    if ( defined $HTML::Mason::Commands::r  ) { #Mason
-      ## is this the correct pacakge for $r ???  for 1.0x and 1.1x ?
-      if ( $header =~ /^Content-Type$/ ) {
-        $HTML::Mason::Commands::r->content_type($value);
-      } else {
-        $HTML::Mason::Commands::r->header_out( $header => $value );
-      }
+  if ( defined $HTML::Mason::Commands::r  ) { #Mason + apache
+    if ( $header =~ /^Content-Type$/ ) {
+      $HTML::Mason::Commands::r->content_type($value);
     } else {
-      die "http_header called in unknown environment";
+      $HTML::Mason::Commands::r->header_out( $header => $value );
     }
+  } elsif ( defined $HTML::Mason::Commands::m ) {
+    $HTML::Mason::Commands::m->notes(lc("header-$header"), $value);
   } else {
-    die "http_header called not running under mod_perl";
+    warn "http_header($header, $value) called with no way to set headers\n";
   }
-
 }
 
 =item menubar ITEM, URL, ...
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 6fc4bf0..bdae393 100644 (file)
@@ -393,6 +393,7 @@ 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;
   # Sammath Naur
 
   if ( $FS::Mason::addl_handler_use ) {
index b7ec7df..57f3475 100644 (file)
@@ -5220,6 +5220,28 @@ sub tables_hashref {
                         ],
     },
 
+    'saved_search' => {
+      'columns' => [
+        'searchnum',    'serial',  '',          '', '', '',
+        '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',      '', '', '',
+        'format',       'varchar', 'NULL',      32, '', '',
+      ],
+      'primary_key'   => 'searchnum',
+      'unique'        => [],
+      'index'         => [],
+      'foreign_keys'  => [
+                           { columns => [ 'usernum' ],
+                             table   => 'access_user',
+                           },
+                         ],
+    },
+
     # name type nullability length default local
 
     #'new_table' => {
index d13549d..366ae7e 100644 (file)
@@ -831,6 +831,13 @@ sub set_page_pref {
   return $error;
 }
 
+#3.x
+
+sub saved_search {
+  my $self = shift;
+  qsearch('saved_search', { 'usernum' => $self->usernum });
+}
+
 =back
 
 =head1 BUGS
diff --git a/FS/FS/cdr/callplus.pm b/FS/FS/cdr/callplus.pm
new file mode 100644 (file)
index 0000000..fa6c799
--- /dev/null
@@ -0,0 +1,60 @@
+package FS::cdr::callplus;
+use base qw( FS::cdr );
+
+use strict;
+use vars qw( %info );
+use FS::Record qw( qsearchs );
+use Time::Local 'timelocal';
+
+# Date format in the Date/Time col: "13/07/2016 2:40:32 p.m."
+# d/m/y H:M:S, leading zeroes stripped, 12-hour with "a.m." or "p.m.".
+# There are also separate d/m/y and 24-hour time columns, but parsing
+# those separately is hard (DST issues).
+
+%info = (
+  'name'          => 'CallPlus',
+  'weight'        => 610,
+  'header'        => 1,
+  'type'          => 'csv',
+  'import_fields' => [
+    'uniqueid',           # ID
+    '',                   # Billing Group (charged_party?)
+    'src',                # Origin Number
+    'dst',                # Destination Number
+    '',                   # Description (seems to be dest caller id?)
+    '',                   # Status
+    '',                   # Terminated
+    '',                   # Date
+    '',                   # Time
+    sub {                 # Date/Time
+      # this format overlaps one of the existing parser cases, so give it
+      # its own special parser
+      my ($cdr, $value) = @_;
+      $value =~ m[^(\d{1,2})/(\d{1,2})/(\d{4}) (\d{1,2}):(\d{2}):(\d{2}) (a\.m\.|p\.m\.)$]
+        or die "unparseable date: $value";
+      my ($day, $mon, $year, $hour, $min, $sec) = ( $1, $2, $3, $4, $5, $6 );
+      $hour = $hour % 12;
+      if ($7 eq 'p.m.') {
+        $hour = 12;
+      }
+      $cdr->set('startdate',
+                timelocal($sec, $min, $hour, $day, $mon-1, $year)
+               );
+    },
+    sub {                 # Call Length (seconds)
+      my ($cdr, $value) = @_;
+      $cdr->set('duration', $value);
+      $cdr->set('billsec', $value);
+    },
+    sub {                 # Call Cost (NZD)
+      my ($cdr,$value) = @_;
+      $value =~ s/^\$//;
+      $cdr->upstream_price($value);
+    },
+    skip(4),              # Smartcode, Smartcode Description, Type, SubType
+  ],
+);
+
+sub skip { map {''} (1..$_[0]) }
+
+1;
index d7ea26b..0d62209 100644 (file)
@@ -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
new file mode 100644 (file)
index 0000000..ec090a9
--- /dev/null
@@ -0,0 +1,331 @@
+package FS::saved_search;
+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;
+
+=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 params
+
+The query string for the search.
+
+=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_textn('params') # URL-escaped, so ut_textn
+    || $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;
+}
+
+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
+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
+
+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";
+  $query .= ';' . $self->params if $self->params;
+  $query;
+}
+
+=item render
+
+Returns the report content as an HTML or Excel file.
+
+=cut
+
+sub render {
+  my $self = shift;
+  my $log = FS::Log->new('FS::saved_search::render');
+  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 = $self->access_user->option('rooturl');
+
+  my $mason_request = $fs_interp->make_request(comp => '/' . $self->path);
+  $mason_request->notes('inline_stylesheet', 1);
+
+  local $@;
+  eval { $mason_request->exec(); };
+  if ($@) {
+    my $error = $@;
+    if ( ref($error) eq 'HTML::Mason::Exception' ) {
+      $error = $error->message;
+    }
+
+    $log->error("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>';
+  }
+
+  my %mime = (
+    Data        => $outbuf,
+    Type        => $mason_request->notes('header-content-type')
+                   || 'text/html',
+    Disposition => 'inline',
+  );
+  if (my $disp = $mason_request->notes('header-content-disposition') ) {
+    $disp =~ /^(attachment|inline)\s*;\s*filename=(.*)$/;
+    $mime{Disposition} = $1;
+    my $filename = $2;
+    $filename =~ s/^"(.*)"$/$1/;
+    $mime{Filename} = $filename;
+  }
+  if ($mime{Type} =~ /^text/) {
+    $mime{Encoding} = 'quoted-printable';
+  } else {
+    $mime{Encoding} = 'base64';
+  }
+  return MIME::Entity->build(%mime);
+}
+
+=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 $part = $self->render;
+
+  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;
+  qsearchs('access_user', { 'usernum' => $self->usernum });
+}
+
+=back
+
+=head1 SEE ALSO
+
+L<FS::Record>
+
+=cut
+
+1;
+
index c060c14..9383593 100644 (file)
@@ -810,3 +810,5 @@ 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
index 1162e79..4d432ef 100755 (executable)
@@ -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.t b/FS/t/saved_search.t
new file mode 100644 (file)
index 0000000..8155c6d
--- /dev/null
@@ -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/httemplate/browse/saved_search.html b/httemplate/browse/saved_search.html
new file mode 100644 (file)
index 0000000..d2efa6e
--- /dev/null
@@ -0,0 +1,81 @@
+<& elements/browse.html,
+  'title'       => 'My saved searches',
+  'name'        => 'saved searches',
+  'query'       => { 'table'     => 'saved_search',
+                     'hashref'   => { usernum => $curuser->usernum },
+                   },
+  'count_query' => $count_query,
+  'header'      => [ '#',
+                     'Name',
+                     'Subscription',
+                     'Last sent',
+                     'Format',
+                     'Path',
+                     'Parameters',
+                   ],
+  'sort_fields' => [ 'searchnum',
+                     'searchname',
+                     'freq',
+                     'last_sent',
+                     'format',
+                     "path || '?' || 'params'",
+                     '',
+                   ],
+  'fields'      => [ 'searchnum',
+                     'searchname',
+                     'freq',
+                     sub { my $date = shift->get('last_sent');
+                           $date ? time2str('%b %o, %Y', $date) : '';
+                     },
+                     sub { $format_label{ shift->get('format') }
+                     },
+                     'path',
+                     sub { join('<BR>',
+                             sort
+                             map { encode_entities(uri_unescape($_)) }
+                             split(/[;&]/, shift->get('params') )
+                           )
+                     },
+                   ],
+  'size'        => [ '',
+                     '',
+                     '',
+                     '',
+                     '',
+                     '',
+                     '-1',
+                   ],
+  'links'                   => [ '', '' ],
+  'link_onclicks'           => [ '', $edit_popup ],
+#  'disableable'             => 1, # currrently unused
+#  'disabled_statuspos'      => 2,
+  'really_disable_download' => 1
+&>
+<%init>
+
+my $curuser = $FS::CurrentUser::CurrentUser;
+
+my $query = {
+  'table'   => 'saved_search',
+  'hashref' => { 'usernum' => $curuser->usernum },
+};
+my $count_query = "SELECT COUNT(*) FROM saved_search WHERE usernum = ".
+  $curuser->usernum;
+
+my %format_label = (
+  'html' => 'webpage',
+  'csv'  => 'CSV',
+  'xls'  => 'spreadsheet',
+);
+
+my $edit_popup = sub {
+  my $searchnum = shift->searchnum;
+  include('/elements/popup_link_onclick.html',
+    'action'        => $fsurl.'/edit/saved_search.html?'.$searchnum,
+    'actionlabel'   => 'Save this search',
+    'width'         => 650,
+    'height'        => 500,
+  );
+};
+
+</%init>
index 89a38f4..60aaf74 100644 (file)
@@ -79,6 +79,9 @@ Example:
    #return an error string or empty for no error
    'precheck_callback' => sub { my( $cgi ) = @_; },
 
+   #after the new object is created
+   'post_new_object_callback' => sub { my( $cgi, $object ) = @_; },
+
    #after everything's inserted
    'noerror_callback' => sub { my( $cgi, $object ) = @_; },
 
@@ -269,6 +272,10 @@ foreach my $value ( @values ) {
       }
     }
 
+    if ( $opt{'post_new_object_callback'} ) {
+      &{ $opt{'post_new_object_callback'} }( $cgi, $new );
+    }
+
     if ( $opt{'agent_virt'} ) {
 
       if ( ! $new->agentnum
diff --git a/httemplate/edit/process/saved_search.html b/httemplate/edit/process/saved_search.html
new file mode 100644 (file)
index 0000000..7ae7e0d
--- /dev/null
@@ -0,0 +1,15 @@
+<& elements/process.html,
+  'table'         => 'saved_search',
+  'popup_reload'  => 'Saving',
+  'post_new_object_callback' => $callback,
+&>
+<%init>
+
+my $callback = sub {
+  my ($cgi, $obj) = @_;
+  $obj->usernum( $FS::CurrentUser::CurrentUser->usernum );
+  # if this would change it from its existing owner, replace_check
+  # will refuse
+};
+
+</%init>
diff --git a/httemplate/edit/saved_search.html b/httemplate/edit/saved_search.html
new file mode 100644 (file)
index 0000000..f8f0333
--- /dev/null
@@ -0,0 +1,112 @@
+<& elements/edit.html,
+  'name'   => 'saved search',
+  'table'  => 'saved_search',
+  'popup'  => 1,
+  'fields' => [
+    { field   => 'searchname',
+      type    => 'text',
+      size    => 40,
+    },
+    { field   => 'freq',
+      type    => 'select',
+      options => [ '', 'daily', 'weekly', 'monthly' ],
+      labels  => { '' => 'no' },
+    },
+    { field   => 'emailaddress',
+      type    => 'fixed',
+      curr_value_callback => sub {
+        $curuser->option('email_address')
+        || 'no email address configured'
+      },
+    },
+    { field   => 'last_sent',
+      type    => 'fixed-date',
+    },
+    { field   => 'format',
+      type    => 'select',
+      options => [ 'html', 'xls', 'csv' ],
+      labels => {
+        'html' => 'webpage',
+        'xls'  => 'spreadsheet',
+        'csv'  => 'CSV',
+      },
+    },
+    { field => 'disabled', # currently unused
+      type  => 'hidden',
+    },
+    { type  => 'tablebreak-tr-title' },
+    { field => 'path',
+      type  => 'fixed',
+      cell_style => 'font-size: small',
+    },
+    { field => 'params',
+      type  => 'fixed',
+      cell_style => 'font-size: small',
+    },
+  ],
+  'labels' => {
+    'searchnum'         => 'Saved search',
+    'searchname'        => 'Name this search',
+    'path'              => 'Search page',
+    'params'            => 'Parameters',
+    'freq'              => 'Subscribe by email',
+    'last_sent'         => 'Last sent on',
+    'emailaddress'      => 'Will be sent to',
+    'format'            => 'Report format',
+  },
+  'new_object_callback' => $new_object,
+  'delete_url'          => $fsurl.'misc/delete-saved_search.html',
+&>
+<%init>
+
+my $curuser = $FS::CurrentUser::CurrentUser;
+# remember the user's rooturl() when accessing the UI. this will be the
+# base URL for sending email reports to that user so that links work.
+my $rooturl_pref = qsearchs('access_user_pref', {
+  usernum   => $curuser->usernum,
+  prefname  => 'rooturl',
+});
+my $error;
+if ($rooturl_pref) {
+  if ($rooturl_pref->prefvalue ne rooturl()) {
+    $rooturl_pref->set('prefvalue', rooturl());
+    $error = $rooturl_pref->replace;
+  } # else don't update it
+} else {
+  $rooturl_pref = FS::access_user_pref->new({
+    usernum   => $curuser->usernum,
+    prefname  => 'rooturl',
+    prefvalue => rooturl(),
+  });
+  $error = $rooturl_pref->insert;
+}
+
+warn "error updating rooturl pref: $error" if $error;
+
+# prefix to the freeside document root (usually '/freeside/')
+my $root = URI->new($fsurl)->path;
+
+# alternatively, could do all this on the client using window.top.location
+my $new_object = sub {
+  my $cgi = shift;
+  my $hashref = shift;
+  my $fields = shift;
+  for (grep { $_->{field} eq 'last_sent' } @$fields) {
+    $_->{type} = 'hidden';
+  }
+  my $url = $r->header_in('Referer')
+    or die "no referring page found";
+  $url = URI->new($url);
+  my $path = $url->path;
+  $path =~ s/^$root//; # path should not have a leading slash
+  my $title = $cgi->param('title');
+  return FS::saved_search->new({
+    'usernum'     => $curuser->usernum,
+    'path'        => $path,
+    'params'      => $url->query,
+    'format'      => 'html',
+    'searchname'  => $title,
+  });
+};
+
+</%init>
index 04709ce..906b1ee 100644 (file)
@@ -37,7 +37,13 @@ Example:
     <% $head |n %>
   </HEAD>
   <BODY <% $etc |n %>>
+%   if ($m->notes('inline_stylesheet')) { # for email delivery
+    <style type="text/css">
+    <& /elements/freeside.css &>
+    </style>
+%   } else {
     <link href="<%$fsurl%>elements/freeside.css" type="text/css" rel="stylesheet">
+%   }
 % if ( $title || $title_noescape ) {
     <FONT SIZE=6>
       <CENTER><% encode_entities($title) || $title_noescape |n %></CENTER>
index cdb1d73..621165d 100644 (file)
@@ -87,6 +87,21 @@ my $mobile = $opt{'mobile'} || 0;
 
 my $curuser = $FS::CurrentUser::CurrentUser;
 
+# saved searches
+tie my %report_saved_searches, 'Tie::IxHash';
+if ( my @searches = grep { $_->disabled eq '' } $curuser->saved_search ) {
+  foreach my $search (@searches) {
+    $report_saved_searches{ $search->searchname } = [
+      # don't use query_string here; we don't want to override the format
+      $fsurl . $search->path . '?' . $search->params , ''
+    ];
+  }
+  $report_saved_searches{'separator'} = '';
+  $report_saved_searches{'My saved searches'} =
+    [ $fsurl. 'browse/saved_search.html',
+      'Manage saved searches and subscriptions' ];
+}
+
 #XXX Active tickets not assigned to a customer
 
 tie my %report_prospects, 'Tie::IxHash';
@@ -412,6 +427,8 @@ $report_logs{'Outgoing messages'} = [ $fsurl.'search/cust_msg.html', 'View outgo
   || $curuser->access_right('Configuration');
 
 tie my %report_menu, 'Tie::IxHash';
+$report_menu{'Saved searches'} = [ \%report_saved_searches, 'My saved searches' ]
+  if keys(%report_saved_searches);
 $report_menu{'Prospects'}      = [ \%report_prospects, 'Prospect reports' ]
   if $curuser->access_right('List prospects')
   || $curuser->access_right('List contacts');
index ef59979..731a3ca 100644 (file)
@@ -14,6 +14,6 @@ my $value = $opt{'curr_value'} || $opt{'value'};
 my $conf = new FS::Conf;
 my $date_format = $opt{'format'} || $conf->config('date_format') || '%m/%d/%Y';
 
-$opt{'formatted_value'} = time2str($date_format, $value);
+$opt{'formatted_value'} = $value > 0 ? time2str($date_format, $value) : '';
 
 </%init>
diff --git a/httemplate/misc/delete-saved_search.html b/httemplate/misc/delete-saved_search.html
new file mode 100644 (file)
index 0000000..34567ec
--- /dev/null
@@ -0,0 +1,25 @@
+% if ( $error ) {
+<& /elements/errorpage-popup.html, $error &>
+% } else {
+<& /elements/header-popup.html, 'Saved search deleted' &>
+  <script type="text/javascript">
+  topreload();
+  </script>
+</body>
+</html>
+% }
+<%init>
+
+my $curuser = $FS::CurrentUser::CurrentUser;
+
+my($query) = $cgi->keywords;
+$query =~ /^(\d+)$/ || die "Illegal searchnum";
+my $searchnum = $1;
+
+my $search = qsearchs('saved_search', {
+  'searchnum' => $searchnum,
+  'usernum'   => $curuser->usernum,
+});
+my $error = $search->delete;
+
+</%init>
index 12f6c1e..3ea38ae 100644 (file)
 
               <TD ALIGN="right" CLASS="noprint">
 
-                <% $opt{'download_label'} || 'Download full results' %><BR>
+                <% $opt{'download_label'} || 'Download results:' %>
 
 %               $cgi->param('_type', "$xlsname.xls" ); 
-                as <A HREF="<% "$self_url?". $cgi->query_string %>">Excel spreadsheet</A><BR>
+                <A HREF="<% "$self_url?". $cgi->query_string %>">Spreadsheet</A>&nbsp;|&nbsp;
 
 %               $cgi->param('_type', 'csv'); 
-                as <A HREF="<% "$self_url?". $cgi->query_string %>">CSV file</A><BR>
+                <A HREF="<% "$self_url?". $cgi->query_string %>">CSV</A>&nbsp;|&nbsp;
 
 %             if ( defined($opt{xml_elements}) ) {
 %               $cgi->param('_type', 'xml'); 
-                as <A HREF="<% "$self_url?". $cgi->query_string %>">XML file</A><BR>
+                <A HREF="<% "$self_url?". $cgi->query_string %>">XML</A>&nbsp;|&nbsp;
 %             }
 
 %               $cgi->param('_type', 'html-print'); 
-                as <A HREF="<% "$self_url?". $cgi->query_string %>">printable copy</A>
+                <A HREF="<% "$self_url?". $cgi->query_string %>">webpage</A>
 
+%# "save search" -- for now, obey disable_download and the 'Download
+%# report data' ACL, because saving a search allows the user to receive
+%# copies of the data.
+                <BR>
+%# XXX should do a check here on whether the user already has this
+%# search saved...
+                <& /elements/popup_link.html,
+                  'action'        => $fsurl.'/edit/saved_search.html?title='.
+                                       uri_escape($opt{title}),
+                  'label'         => 'Save this search',
+                  'actionlabel'   => 'Save this search',
+                  'width'         => 650,
+                  'height'        => 500,
+                &>
               </TD>
 %             $cgi->param('_type', "html" ); 
 %           } 
index c4265e8..f2b0bad 100644 (file)
@@ -22,7 +22,7 @@ http_header('Content-Disposition' => qq!attachment;filename="$filename"! );
  
 #http://support.microsoft.com/kb/812935
 #http://support.microsoft.com/kb/323308
-$HTML::Mason::Commands::r->headers_out->{'Cache-control'} = 'max-age=0';
+http_header('Cache-control' => 'max-age=0');
 
 my $data = '';
 my $XLS = new IO::Scalar \$data;
index 8b85324..4ef8c25 100644 (file)
@@ -179,6 +179,7 @@ Example:
   &>
 
 </%doc>
+% # if changing this, also update saved search behavior to match!
 % if ( $type eq 'csv' ) {
 %
 <% include('search-csv.html',  header=>$header, rows=>$rows, opt=>\%opt ) %>