Merge branch 'master' of git.freeside.biz:/home/git/freeside
authorMark Wells <mark@freeside.biz>
Thu, 8 Sep 2016 18:05:38 +0000 (11:05 -0700)
committerMark Wells <mark@freeside.biz>
Thu, 8 Sep 2016 18:05:38 +0000 (11:05 -0700)
18 files changed:
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]
httemplate/browse/saved_search.html [new file with mode: 0644]
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.html

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..caaf7fe 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
@@ -210,11 +219,10 @@ sub render {
 
   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;
+  local $FS::Mason::Request::FSURL = $self->access_user->option('rooturl');
 
-  my $mason_request = $fs_interp->make_request(comp => $self->path);
+  my $mason_request = $fs_interp->make_request(comp => '/' . $self->path);
+  $mason_request->notes('inline_stylesheet', 1);
 
   local $@;
   eval { $mason_request->exec(); };
@@ -224,9 +232,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 +245,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";
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>
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..cb6aa45
--- /dev/null
@@ -0,0 +1,113 @@
+<& 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    => 'hidden', # revisit this later
+#      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 839a636..327673b 100644 (file)
@@ -38,7 +38,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 fcfc9fb..d6ea068 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';
@@ -419,6 +434,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 b6ee7b3..0f71218 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 ) %>