diff options
-rw-r--r-- | FS/FS/CGI.pm | 18 | ||||
-rw-r--r-- | FS/FS/Cron/send_subscribed.pm | 32 | ||||
-rw-r--r-- | FS/FS/Mason.pm | 1 | ||||
-rw-r--r-- | FS/FS/Schema.pm | 22 | ||||
-rw-r--r-- | FS/FS/access_user.pm | 7 | ||||
-rw-r--r-- | FS/FS/cdr/callplus.pm | 60 | ||||
-rw-r--r-- | FS/FS/log_context.pm | 2 | ||||
-rw-r--r-- | FS/FS/saved_search.pm | 331 | ||||
-rw-r--r-- | FS/MANIFEST | 2 | ||||
-rwxr-xr-x | FS/bin/freeside-daily | 4 | ||||
-rw-r--r-- | FS/t/saved_search.t | 5 | ||||
-rw-r--r-- | httemplate/browse/saved_search.html | 81 | ||||
-rw-r--r-- | httemplate/edit/process/elements/process.html | 7 | ||||
-rw-r--r-- | httemplate/edit/process/saved_search.html | 15 | ||||
-rw-r--r-- | httemplate/edit/saved_search.html | 112 | ||||
-rw-r--r-- | httemplate/elements/header-popup.html | 6 | ||||
-rw-r--r-- | httemplate/elements/menu.html | 17 | ||||
-rw-r--r-- | httemplate/elements/tr-fixed-date.html | 2 | ||||
-rw-r--r-- | httemplate/misc/delete-saved_search.html | 25 | ||||
-rw-r--r-- | httemplate/search/elements/search-html.html | 24 | ||||
-rw-r--r-- | httemplate/search/elements/search-xls.html | 2 | ||||
-rw-r--r-- | httemplate/search/elements/search.html | 1 |
22 files changed, 758 insertions, 18 deletions
diff --git a/FS/FS/CGI.pm b/FS/FS/CGI.pm index e1645f04c..8be823a4c 100644 --- a/FS/FS/CGI.pm +++ b/FS/FS/CGI.pm @@ -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 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 6fc4bf09f..bdae3938c 100644 --- a/FS/FS/Mason.pm +++ b/FS/FS/Mason.pm @@ -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 ) { diff --git a/FS/FS/Schema.pm b/FS/FS/Schema.pm index b7ec7df19..57f347555 100644 --- a/FS/FS/Schema.pm +++ b/FS/FS/Schema.pm @@ -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' => { diff --git a/FS/FS/access_user.pm b/FS/FS/access_user.pm index d13549dcf..366ae7ee8 100644 --- a/FS/FS/access_user.pm +++ b/FS/FS/access_user.pm @@ -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 index 000000000..fa6c799ad --- /dev/null +++ b/FS/FS/cdr/callplus.pm @@ -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; 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 new file mode 100644 index 000000000..ec090a9f1 --- /dev/null +++ b/FS/FS/saved_search.pm @@ -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; + diff --git a/FS/MANIFEST b/FS/MANIFEST index c060c140c..93835936a 100644 --- a/FS/MANIFEST +++ b/FS/MANIFEST @@ -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 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.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/httemplate/browse/saved_search.html b/httemplate/browse/saved_search.html new file mode 100644 index 000000000..d2efa6ed9 --- /dev/null +++ b/httemplate/browse/saved_search.html @@ -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/elements/process.html b/httemplate/edit/process/elements/process.html index 89a38f4f6..60aaf749a 100644 --- a/httemplate/edit/process/elements/process.html +++ b/httemplate/edit/process/elements/process.html @@ -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 index 000000000..7ae7e0d78 --- /dev/null +++ b/httemplate/edit/process/saved_search.html @@ -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 index 000000000..f8f0333c5 --- /dev/null +++ b/httemplate/edit/saved_search.html @@ -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> diff --git a/httemplate/elements/header-popup.html b/httemplate/elements/header-popup.html index 04709ce53..906b1ee51 100644 --- a/httemplate/elements/header-popup.html +++ b/httemplate/elements/header-popup.html @@ -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> diff --git a/httemplate/elements/menu.html b/httemplate/elements/menu.html index cdb1d733c..621165de4 100644 --- a/httemplate/elements/menu.html +++ b/httemplate/elements/menu.html @@ -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'); diff --git a/httemplate/elements/tr-fixed-date.html b/httemplate/elements/tr-fixed-date.html index ef599796d..731a3caa7 100644 --- a/httemplate/elements/tr-fixed-date.html +++ b/httemplate/elements/tr-fixed-date.html @@ -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 index 000000000..34567ec1c --- /dev/null +++ b/httemplate/misc/delete-saved_search.html @@ -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> diff --git a/httemplate/search/elements/search-html.html b/httemplate/search/elements/search-html.html index 12f6c1e04..3ea38aee8 100644 --- a/httemplate/search/elements/search-html.html +++ b/httemplate/search/elements/search-html.html @@ -136,22 +136,36 @@ <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> | % $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> | % 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> | % } % $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" ); % } diff --git a/httemplate/search/elements/search-xls.html b/httemplate/search/elements/search-xls.html index c4265e8c8..f2b0bad50 100644 --- a/httemplate/search/elements/search-xls.html +++ b/httemplate/search/elements/search-xls.html @@ -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; diff --git a/httemplate/search/elements/search.html b/httemplate/search/elements/search.html index 8b85324c9..4ef8c25d3 100644 --- a/httemplate/search/elements/search.html +++ b/httemplate/search/elements/search.html @@ -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 ) %> |