saved searches, core stuff, #72101
[freeside.git] / FS / FS / saved_search.pm
1 package FS::saved_search;
2 use base qw( FS::option_Common FS::Record );
3
4 use strict;
5 use FS::Record qw( qsearch qsearchs );
6 use FS::Conf;
7 use Class::Load 'load_class';
8 use URI::Escape;
9 use DateTime;
10 use Try::Tiny;
11
12 =head1 NAME
13
14 FS::saved_search - Object methods for saved_search records
15
16 =head1 SYNOPSIS
17
18   use FS::saved_search;
19
20   $record = new FS::saved_search \%hash;
21   $record = new FS::saved_search { 'column' => 'value' };
22
23   $error = $record->insert;
24
25   $error = $new_record->replace($old_record);
26
27   $error = $record->delete;
28
29   $error = $record->check;
30
31 =head1 DESCRIPTION
32
33 An FS::saved_search object represents a search (a page in the backoffice
34 UI, typically under search/ or browse/) which a user has saved for future
35 use or periodic email delivery.
36
37 FS::saved_search inherits from FS::Record.  The following fields are
38 currently supported:
39
40 =over 4
41
42 =item searchnum
43
44 primary key
45
46 =item usernum
47
48 usernum of the L<FS::access_user> that created the search. Currently, email
49 reports will only be sent to this user.
50
51 =item searchname
52
53 A descriptive name.
54
55 =item path
56
57 The path to the page within the Mason document space.
58
59 =item disabled
60
61 'Y' to hide the search from the user's Reports / Saved menu.
62
63 =item freq
64
65 A frequency for email delivery of this report: daily, weekly, or
66 monthly, or null to disable it.
67
68 =item last_sent
69
70 The timestamp of the last time this report was sent.
71
72 =item format
73
74 'html', 'xls', or 'csv'. Not all reports support all of these.
75
76 =back
77
78 =head1 METHODS
79
80 =over 4
81
82 =item new HASHREF
83
84 Creates a new saved search.  To add it to the database, see L<"insert">.
85
86 Note that this stores the hash reference, not a distinct copy of the hash it
87 points to.  You can ask the object for a copy with the I<hash> method.
88
89 =cut
90
91 sub table { 'saved_search'; }
92
93 =item insert
94
95 Adds this record to the database.  If there is an error, returns the error,
96 otherwise returns false.
97
98 =item delete
99
100 Delete this record from the database.
101
102 =item replace OLD_RECORD
103
104 Replaces the OLD_RECORD with this one in the database.  If there is an error,
105 returns the error, otherwise returns false.
106
107 =cut
108
109 # the replace method can be inherited from FS::Record
110
111 =item check
112
113 Checks all fields to make sure this is a valid example.  If there is
114 an error, returns the error, otherwise returns false.  Called by the insert
115 and replace methods.
116
117 =cut
118
119 # the check method should currently be supplied - FS::Record contains some
120 # data checking routines
121
122 sub check {
123   my $self = shift;
124
125   my $error = 
126     $self->ut_numbern('searchnum')
127     || $self->ut_number('usernum')
128     #|| $self->ut_foreign_keyn('usernum', 'access_user', 'usernum')
129     || $self->ut_text('searchname')
130     || $self->ut_text('path')
131     || $self->ut_flag('disabled')
132     || $self->ut_enum('freq', [ '', 'daily', 'weekly', 'monthly' ])
133     || $self->ut_numbern('last_sent')
134     || $self->ut_enum('format', [ '', 'html', 'csv', 'xls' ])
135   ;
136   return $error if $error;
137
138   $self->SUPER::check;
139 }
140
141 =item next_send_date
142
143 Returns the next date this report should be sent next. If it's not set for
144 periodic email sending, returns undef. If it is set up but has never been
145 sent before, returns zero.
146
147 =cut
148
149 sub next_send_date {
150   my $self = shift;
151   my $freq = $self->freq or return undef;
152   return 0 unless $self->last_sent;
153   my $dt = DateTime->from_epoch(epoch => $self->last_sent);
154   $dt->truncate(to => 'day');
155   if ($freq eq 'daily') {
156     $dt->add(days => 1);
157   } elsif ($freq eq 'weekly') {
158     $dt->add(weeks => 1);
159   } elsif ($freq eq 'monthly') {
160     $dt->add(months => 1);
161   }
162   $dt->epoch;
163 }
164
165 =item query_string
166
167 Returns the CGI query string for the parameters to this report.
168
169 =cut
170
171 # multivalued options are newline-separated in the database
172
173 sub query_string {
174   my $self = shift;
175
176   my $type = $self->format;
177   $type = 'html-print' if $type eq '' || $type eq 'html';
178   $type = '.xls' if $type eq 'xls';
179   my $query = "_type=$type";
180   my %options = $self->options;
181   foreach my $k (keys %options) {
182     foreach my $v (split("\n", $options{$k})) {
183       $query .= ';' . uri_escape($k) . '=' . uri_escape($v);
184     }
185   }
186   $query;
187 }
188
189 =item render
190
191 Returns the report content as an HTML or Excel file.
192
193 =cut
194
195 sub render {
196   my $self = shift;
197   my $outbuf;
198
199   # delayed loading
200   load_class('FS::Mason');
201   RT::LoadConfig();
202   RT::Init();
203
204   # do this before setting QUERY_STRING/FSURL
205   my ($fs_interp) = FS::Mason::mason_interps('standalone',
206     outbuf => \$outbuf
207   );
208   $fs_interp->error_mode('fatal');
209   $fs_interp->error_format('text');
210
211   local $FS::CurrentUser::CurrentUser = $self->access_user;
212   local $FS::Mason::Request::QUERY_STRING = $self->query_string;
213   local $FS::Mason::Request::FSURL = ''; #?
214 #  local $ENV{SERVER_NAME} = 'localhost'; #?
215 #  local $ENV{SCRIPT_NAME} = '/freeside'. $self->path;
216
217   my $mason_request = $fs_interp->make_request(comp => $self->path);
218
219   local $@;
220   eval { $mason_request->exec(); };
221   if ($@) {
222     my $error = $@;
223     if ( ref($error) eq 'HTML::Mason::Exception' ) {
224       $error = $error->message;
225     }
226
227     warn "Error rendering " . $self->path .
228          " for " . $self->access_user->username .
229          ":\n$error\n";
230     # send it to the user anyway, so there's a way to diagnose the error
231     $outbuf = '<h3>Error</h3>
232   <p>There was an error generating the report "'.$self->searchname.'".</p>
233   <p>' . $self->path . '?' . $self->query_string . '</p>
234   <p>' . $_ . '</p>';
235   }
236
237   return $outbuf;
238 }
239
240 =back
241
242 =head1 SEE ALSO
243
244 L<FS::Record>
245
246 =cut
247
248 1;
249