1 package FS::saved_search;
2 use base qw( FS::Record );
5 use FS::Record qw( qsearch qsearchs );
8 use FS::Misc qw(send_email);
10 use Class::Load 'load_class';
16 FS::saved_search - Object methods for saved_search records
22 $record = new FS::saved_search \%hash;
23 $record = new FS::saved_search { 'column' => 'value' };
25 $error = $record->insert;
27 $error = $new_record->replace($old_record);
29 $error = $record->delete;
31 $error = $record->check;
35 An FS::saved_search object represents a search (a page in the backoffice
36 UI, typically under search/ or browse/) which a user has saved for future
37 use or periodic email delivery.
39 FS::saved_search inherits from FS::Record. The following fields are
50 usernum of the L<FS::access_user> that created the search. Currently, email
51 reports will only be sent to this user.
59 The path to the page within the Mason document space.
63 The query string for the search.
67 'Y' to hide the search from the user's Reports / Saved menu.
71 A frequency for email delivery of this report: daily, weekly, or
72 monthly, or null to disable it.
76 The timestamp of the last time this report was sent.
80 'html', 'xls', or 'csv'. Not all reports support all of these.
90 Creates a new saved search. To add it to the database, see L<"insert">.
92 Note that this stores the hash reference, not a distinct copy of the hash it
93 points to. You can ask the object for a copy with the I<hash> method.
97 sub table { 'saved_search'; }
101 Adds this record to the database. If there is an error, returns the error,
102 otherwise returns false.
106 Delete this record from the database.
108 =item replace OLD_RECORD
110 Replaces the OLD_RECORD with this one in the database. If there is an error,
111 returns the error, otherwise returns false.
115 # the replace method can be inherited from FS::Record
119 Checks all fields to make sure this is a valid example. If there is
120 an error, returns the error, otherwise returns false. Called by the insert
125 # the check method should currently be supplied - FS::Record contains some
126 # data checking routines
132 $self->ut_numbern('searchnum')
133 || $self->ut_number('usernum')
134 #|| $self->ut_foreign_keyn('usernum', 'access_user', 'usernum')
135 || $self->ut_text('searchname')
136 || $self->ut_text('path')
137 || $self->ut_textn('params') # URL-escaped, so ut_textn
138 || $self->ut_flag('disabled')
139 || $self->ut_enum('freq', [ '', 'daily', 'weekly', 'monthly' ])
140 || $self->ut_numbern('last_sent')
141 || $self->ut_enum('format', [ '', 'html', 'csv', 'xls' ])
143 return $error if $error;
149 my ($new, $old) = @_;
150 if ($new->usernum != $old->usernum) {
151 return "can't change owner of a saved search";
158 Returns the next date this report should be sent next. If it's not set for
159 periodic email sending, returns undef. If it is set up but has never been
160 sent before, returns zero.
166 my $freq = $self->freq or return undef;
167 return 0 unless $self->last_sent;
168 my $dt = DateTime->from_epoch(epoch => $self->last_sent);
169 $dt->truncate(to => 'day');
170 if ($freq eq 'daily') {
172 } elsif ($freq eq 'weekly') {
173 $dt->add(weeks => 1);
174 } elsif ($freq eq 'monthly') {
175 $dt->add(months => 1);
182 Returns the CGI query string for the parameters to this report.
189 my $type = $self->format;
190 $type = 'html-print' if $type eq '' || $type eq 'html';
191 $type = '.xls' if $type eq 'xls';
192 my $query = "_type=$type";
193 $query .= ';' . $self->params if $self->params;
199 Returns the report content as an HTML or Excel file.
205 my $log = FS::Log->new('FS::saved_search::render');
209 load_class('FS::Mason');
213 # do this before setting QUERY_STRING/FSURL
214 my ($fs_interp) = FS::Mason::mason_interps('standalone',
217 $fs_interp->error_mode('fatal');
218 $fs_interp->error_format('text');
220 local $FS::CurrentUser::CurrentUser = $self->access_user;
221 local $FS::Mason::Request::QUERY_STRING = $self->query_string;
222 local $FS::Mason::Request::FSURL = $self->access_user->option('rooturl');
224 my $mason_request = $fs_interp->make_request(comp => '/' . $self->path);
225 $mason_request->notes('inline_stylesheet', 1);
228 eval { $mason_request->exec(); };
231 if ( ref($error) eq 'HTML::Mason::Exception' ) {
232 $error = $error->message;
235 $log->error("Error rendering " . $self->path .
236 " for " . $self->access_user->username .
238 # send it to the user anyway, so there's a way to diagnose the error
239 $outbuf = '<h3>Error</h3>
240 <p>There was an error generating the report "'.$self->searchname.'".</p>
241 <p>' . $self->path . '?' . $self->query_string . '</p>
247 Type => $mason_request->notes('header-content-type')
249 Disposition => 'inline',
251 if (my $disp = $mason_request->notes('header-content-disposition') ) {
252 $disp =~ /^(attachment|inline)\s*;\s*filename=(.*)$/;
253 $mime{Disposition} = $1;
255 $filename =~ s/^"(.*)"$/$1/;
256 $mime{Filename} = $filename;
258 if ($mime{Type} =~ /^text/) {
259 $mime{Encoding} = 'quoted-printable';
261 $mime{Encoding} = 'base64';
263 return MIME::Entity->build(%mime);
268 Sends the search by email. If anything fails, logs and returns an error.
274 my $log = FS::Log->new('FS::saved_search::send');
275 my $conf = FS::Conf->new;
276 my $user = $self->access_user;
277 my $username = $user->username;
278 my $user_email = $user->option('email_address');
281 $error = "User '$username' has no email address.";
285 $log->debug('Rendering saved search');
286 my $part = $self->render;
289 'from' => $conf->config('invoice_from'),
291 'subject' => $self->searchname,
293 'mimeparts' => [ $part ],
296 $log->debug('Sending to '.$user_email);
297 $error = send_email(%email_param);
299 # update the timestamp
300 $self->set('last_sent', time);
301 $error ||= $self->replace;
310 my $searchnum = shift;
311 my $self = FS::saved_search->by_key($searchnum)
312 or die "searchnum $searchnum not found\n";
319 qsearchs('access_user', { 'usernum' => $self->usernum });