]> git.evergreen-ils.org Git - Evergreen.git/blob - Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Course.pm
LP1849212: OPAC course browse, search, display improvements
[Evergreen.git] / Open-ILS / src / perlmods / lib / OpenILS / WWW / EGCatLoader / Course.pm
1 package OpenILS::WWW::EGCatLoader;
2 use strict; use warnings;
3 use Apache2::Const -compile => qw(OK DECLINED FORBIDDEN HTTP_GONE HTTP_INTERNAL_SERVER_ERROR REDIRECT HTTP_BAD_REQUEST HTTP_NOT_FOUND);
4 use OpenSRF::Utils::Logger qw/$logger/;
5 use OpenILS::Utils::CStoreEditor qw/:funcs/;
6 use OpenILS::Utils::Fieldmapper;
7 use OpenILS::Application::AppUtils;
8 use Net::HTTP::NB;
9 use IO::Select;
10 my $U = 'OpenILS::Application::AppUtils';
11
12 sub load_course {
13     my $self = shift;
14     my $ctx = $self->ctx;
15
16     $ctx->{page} = 'course';
17     $ctx->{readonly} = $self->cgi->param('readonly');
18
19     my $course_id = $ctx->{page_args}->[0];
20
21     return Apache2::Const::HTTP_BAD_REQUEST
22         unless $course_id and $course_id =~ /^\d+$/;
23
24     $ctx->{course} = $U->simplereq(
25         'open-ils.courses',
26         'open-ils.courses.courses.retrieve',
27         [$course_id]
28     )->[0];
29     
30     $ctx->{instructors} = $U->simplereq(
31         'open-ils.courses',
32         'open-ils.courses.course_users.retrieve',
33         $course_id
34     );
35
36     $ctx->{course_materials} = $U->simplereq(
37         'open-ils.courses',
38         'open-ils.courses.course_materials.retrieve.fleshed.atomic',
39         {course => $course_id}
40     );
41     return Apache2::Const::OK;
42 }
43
44 sub load_course_browse {
45     my $self = shift;
46     my $cgi = $self->cgi;
47     my $ctx = $self->ctx;
48     my $e = $self->editor;
49
50     my $browse_results = [];
51
52     # Are we searching? Cool, let's generate some links
53     if ($cgi->param('bterm')) {
54         my $bterm = $cgi->param('bterm');
55         my $qtype = $cgi->param('qtype');
56         # Search term is optional. If it's empty, start at the
57         # beginning. Otherwise, center results on a match.
58         # Regardless, we're listing everything, so retrieve all.
59         my $results;
60         my $instructors;
61         if ($qtype eq 'instructor') {
62             $instructors = $e->json_query({
63                 "from" => "acmcu",
64                 "select" => {"acmcu" => [
65                     'id',
66                     'usr',
67                 ]},
68                 # TODO: We need to support the chosen library as well...
69                 "where" => {'usr_role' => {'in' => {'from' => 'acmr', 'select' => {'acmr' => ['id']}, 'where' => {'+acmr' => 'is_public'}}}}
70             });
71             $results = $e->json_query({
72                 "from" => "au",
73                 "select" => {"au" => [
74                     'id',
75                     'pref_first_given_name',
76                     'first_given_name',
77                     'pref_second_given_name',
78                     'second_given_name',
79                     'pref_family_name',
80                     'family_name'
81                 ]},
82                 "order_by" => {'au' => ['pref_family_name', 'family_name']},
83                 "where" => {'-and' => [{
84                     "id" => { "in" => {
85                         "from" => "acmcu",
86                         "select" => {
87                             "acmcu" => ['usr']
88                         },
89                         "where" => {'-and' => [
90                             {'usr_role' => { 'in' => {
91                                 'from' => 'acmr',
92                                 "select" => {
93                                     "acmr" => ['id']
94                                 },
95                                 "where" => {'+acmr' => 'is_public'}}}},
96                             {"course" => { "in" =>{
97                                 "from" => "acmc",
98                                 "select" => {
99                                     "acmc" => ['id']
100                                 },
101                                 "where" => {'-not' => [{'+acmc' => 'is_archived'}]}
102                             }}}
103                         ]}
104                     }}
105                 }]}
106             });
107         } else {
108             $results = $e->json_query({
109                 "from" => "acmc",
110                 "select" => {"acmc" => [
111                     'id',
112                     'name',
113                     'course_number',
114                     'is_archived',
115                     'owning_lib'
116                 ]},
117                 "order_by" => {"acmc" => [$qtype]},
118                 # TODO: We need to support the chosen library as well...
119                 "where" => {'-not' => {'+acmc' => 'is_archived'}}
120             });
121         }
122         my $bterm_match = 0;
123         for my $result(@$results) {
124             my $value_exists = 0;
125             my $rqtype = $qtype;
126             my $entry = {
127                 'value' => '',
128                 'results_count' => 0,
129                 'match' => 0
130             };
131
132             if ($qtype eq 'instructor') {
133                 # Put together the name
134                 my $name_str = '';
135                 if ($result->{'pref_family_name'}) {
136                     $name_str = $result->{'pref_family_name'} . ", ";
137                 } elsif ($result->{'family_name'}) {
138                     $name_str = $result->{'family_name'} . ", ";
139                 }
140
141                 if ($result->{'pref_first_given_name'}) {
142                     $name_str .= $result->{'pref_first_given_name'};
143                 } elsif ($result->{'first_given_name'}) {
144                     $name_str .= $result->{'first_given_name'};
145                 }
146
147                 if ($result->{'pref_second_given_name'}) {
148                     $name_str .= " " . $result->{'pref_second_given_name'};
149                 } elsif ($result->{'second_given_name'}) {
150                     $name_str .= " " . $result->{'second_given_name'};
151                 }
152
153                 $result->{$rqtype} = $name_str;
154
155                 # Get an accurate count of matching courses
156                 for my $instructor(@$instructors) {
157                     if ($instructor->{'usr'} eq $result->{'id'}) {
158                         $entry->{'results_count'} += 1;
159                         last;
160                     }
161                 }
162             } else {
163                 $entry->{'results_count'} += 1;
164             }
165
166             for my $existing_entry(@$browse_results) {
167                 if ($existing_entry->{'value'} eq $result->{$rqtype} && $value_exists eq 0) {
168                     $value_exists = 1;
169                     $existing_entry->{'results_count'} += 1;
170                     last;
171                 }
172             }
173
174             if ($value_exists eq 0) {
175                 # For Name/Course Number browse queries...
176                 if ($bterm_match eq 0) {
177                     if ($result->{$qtype} =~ m/^$bterm./ || $result->{$qtype} eq $bterm) {
178                         $bterm_match = 1;
179                         $entry->{'match'} = 1;
180                     }
181                 }
182                 $entry->{'value'} = $result->{$rqtype};
183                 push @$browse_results, $entry;
184             }
185         }
186         # Feels a bit hacky, but we need the index of the matching entry
187         my $match_idx = 0;
188         if ($bterm_match) {
189             for my $i (0..$#$browse_results) {
190                 if ($browse_results->[$i]->{'match'}) {
191                     $match_idx = $i;
192                     last;
193                 }
194             }
195         }
196
197         for my $i(0..$#$browse_results) {
198             $browse_results->[$i]->{'browse_index'} = $i;
199         }
200         $ctx->{match_idx} = $match_idx;
201         $ctx->{browse_results} = $browse_results;
202     }
203
204     return Apache2::Const::OK;
205 }
206
207 sub load_cresults {
208     my $self = shift;
209     my %args = @_;
210     my $internal = $args{internal};
211     my $cgi = $self->cgi;
212     my $ctx = $self->ctx;
213     my $e = $self->editor;
214     my $limit = 10;
215
216     $ctx->{page} = 'cresult' unless $internal;
217     $ctx->{ids} = [];
218     $ctx->{courses} = [];
219     $ctx->{hit_count} = 0;
220     $ctx->{search_ou} = $self->_get_search_lib();
221     my $page = $cgi->param('page') || 0;
222     my $offset = $page * $limit;
223     my $results;
224     $ctx->{page_size} = $limit;
225     $ctx->{search_page} = $page;
226     $ctx->{pagable_limit} = 50;
227
228     # fetch this page plus the first hit from the next page
229     if ($internal) {
230         $limit = $offset + $limit + 1;
231         $offset = 0;
232     }
233
234     my ($user_query, $query, @queries, $modifiers) = _prepare_course_search($cgi, $ctx);
235
236     return Apache2::Const::OK unless $query;
237
238     $ctx->{user_query} = $user_query;
239     $ctx->{processed_search_query} = $query;
240     my $search_args = {};
241     my $course_numbers = ();
242     
243     my $where_clause;
244     my $and_terms = [];
245     my $or_terms = [];
246
247     # Handle is_archived checkbox and Org Selector
248     my $search_orgs = $U->get_org_descendants($ctx->{search_ou});
249     push @$and_terms, {'owning_lib' => $search_orgs};
250     push @$and_terms, {'-not' => {'+acmc' => 'is_archived'}} unless $query =~ qr\#include_archived\;
251
252     # Now let's push the actual queries
253     for my $query_obj (@queries) {
254         my $type = $query_obj->{'qtype'};
255         my $query = $query_obj->{'value'};
256         my $bool = $query_obj->{'bool'};
257         my $contains = $query_obj->{'contains'};
258         my $operator = ($contains eq 'nocontains') ? '!~*' : '~*';
259         my $search_query;
260         if ($type eq 'instructor') {
261             my $in = ($contains eq 'nocontains') ? "not in" : "in";
262             $search_query = {'id' => {$in => {
263                 'from' => 'acmcu',
264                 'select' => {'acmcu' => ['course']},
265                 'where' => {'usr' => {'in' => {
266                     'from' => 'au',
267                     'select' => {'au' => ['id']},
268                     'where' => {
269                         'name_kw_tsvector' => {
270                             '@@' => {'value' => [ 'plainto_tsquery', $query ] }
271                         }
272                     }
273                 }}}
274             }}};
275         } else {
276             $search_query = ($contains eq 'nocontains') ?
277               {'+acmc' => { $type => {$operator => $query}}} :
278               {$type => {$operator => $query}};
279         }
280
281         if ($bool eq 'or') {
282             push @$or_terms, $search_query;
283         }
284
285         if ($bool eq 'and') {
286             push @$and_terms, $search_query;
287         }
288     }
289
290     if ($or_terms and @$or_terms > 0) {
291         if ($and_terms and @$and_terms > 0) {
292             push @$or_terms, $and_terms;
293         }
294         $where_clause = {'-or' => $or_terms};
295     } else {
296         $where_clause = {'-and' => $and_terms};
297     }
298
299     my $hits = $e->json_query({
300         "from" => "acmc",
301         "select" => {"acmc" => ['id']},
302         "where" => $where_clause
303     });
304
305     my $results = $e->json_query({
306         "from" => "acmc",
307         "select" => {"acmc" => [
308             'id',
309             'name',
310             'course_number',
311             'section_number',
312             'is_archived',
313             'owning_lib'
314         ]},
315         "limit" => $limit,
316         "offset" => $offset,
317         "order_by" => {"acmc" => ['id']},
318         "where" => $where_clause
319     });
320     for my $result (@$results) {
321         push @{$ctx->{courses}}, {
322             id => $result->{id},
323             course_number => $result->{course_number},
324             section_number => $result->{section_number},
325             owning_lib => $result->{owning_lib},
326             name => $result->{name},
327             is_archived => $result->{is_archived},
328             instructors => []
329         }
330     }
331
332     #$ctx->{courses} = $@courses;#[{id=>10, name=>"test", course_number=>"LIT"}];
333     $ctx->{hit_count} = @$hits || 0;
334     #$ctx->{hit_count} = 0;
335     return Apache2::Const::OK;
336 }
337
338 sub _prepare_course_search {
339     my ($cgi, $ctx) = @_;
340
341     my ($user_query, @queries) = _prepare_query($cgi);
342     my $modifiers;
343     $user_query //= '';
344
345     my $query = $user_query;
346     $query .= ' ' . $ctx->{global_search_filter} if $ctx->{global_search_filter};
347
348     foreach ($cgi->param('modifier')) {
349         $query = ('#' . $_ . ' ' . $query) unless $query =~ qr/\#\Q$_/;
350
351     }
352     # filters
353     foreach (grep /^fi:/, $cgi->param) {
354         /:(-?\w+)$/ or next;
355         my $term = join(",", $cgi->param($_));
356         $query .= " $1($term)" if length $term;
357     }
358
359     return () unless $query;
360
361     return ($user_query, $query, @queries);
362 }
363
364 sub _prepare_query {
365     my $cgi = shift;
366
367     return $cgi->param('query') unless $cgi->param('qtype');
368
369     my %parts;
370     my @part_names = qw/qtype contains query bool modifier/;
371     $parts{$_} = [ $cgi->param($_) ] for (@part_names);
372
373     my $full_query = '';
374     my @queries;
375     for (my $i = 0; $i < scalar @{$parts{'qtype'}}; $i++) {
376         my ($qtype, $contains, $query, $bool, $modifier) = map { $parts{$_}->[$i] } @part_names;
377         next unless $query =~ /\S/;
378
379         $contains = "" unless defined $contains;
380
381         push @queries, {
382             contains => $contains,
383             bool => $bool,
384             qtype => $qtype,
385             value => $query
386         };
387
388         $bool = ($bool and $bool eq 'or') ? '||' : '&&';
389
390         $query = "$qtype:$query";
391
392         $full_query = $full_query ? "($full_query $bool $query)" : $query;
393     }
394
395     return ($full_query, @queries);
396 }