OPAC Course Search Page
authorKyle Huckins <khuckins@catalyte.io>
Mon, 9 Dec 2019 00:01:44 +0000 (00:01 +0000)
committerJane Sandberg <sandbej@linnbenton.edu>
Wed, 26 Aug 2020 22:53:49 +0000 (15:53 -0700)
- Implement Course Search OPAC page, based on Advanced Catalog
Search
- Edit Searchbar to include entry for Course Search

Signed-off-by: Kyle Huckins <khuckins@catalyte.io>
 Changes to be committed:
modified:   Open-ILS/src/perlmods/lib/OpenILS/Application/Circ.pm
modified:   Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader.pm
modified:   Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Course.pm
new file:   Open-ILS/src/templates/opac/course/results.tt2
new file:   Open-ILS/src/templates/opac/course_search.tt2
modified:   Open-ILS/src/templates/opac/css/style.css.tt2
new file:   Open-ILS/src/templates/opac/parts/course_search/global_row.tt2
new file:   Open-ILS/src/templates/opac/parts/course_search/qtype_selector.tt2
modified:   Open-ILS/src/templates/opac/parts/searchbar.tt2

Open-ILS/src/perlmods/lib/OpenILS/Application/Circ.pm
Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader.pm
Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Course.pm
Open-ILS/src/templates/opac/course/results.tt2 [new file with mode: 0644]
Open-ILS/src/templates/opac/course_search.tt2 [new file with mode: 0644]
Open-ILS/src/templates/opac/css/style.css.tt2
Open-ILS/src/templates/opac/parts/course_search/global_row.tt2 [new file with mode: 0644]
Open-ILS/src/templates/opac/parts/course_search/qtype_selector.tt2 [new file with mode: 0644]
Open-ILS/src/templates/opac/parts/searchbar.tt2

index 7110a8b..c48f6ce 100644 (file)
@@ -1063,15 +1063,6 @@ __PACKAGE__->register_method(
         @params args     : Supplied object to filter search.
     /);
 
-__PACKAGE__->register_method(
-    method          => 'fetch_courses',
-    autoritative    => 1,
-    api_name        => 'open-ils.circ.courses.retrieve',
-    signature       => q/
-        Returns an array of course materials.
-        @params course_id: The id of the course we want to retrieve
-    /);
-
 sub fetch_course_materials {
     my ($self, $conn, $args) = @_;
     my $e = new_editor();
@@ -1126,6 +1117,15 @@ sub fetch_course_materials {
     return $targets;
 }
 
+__PACKAGE__->register_method(
+    method          => 'fetch_courses',
+    autoritative    => 1,
+    api_name        => 'open-ils.circ.courses.retrieve',
+    signature       => q/
+        Returns an array of course materials.
+        @params course_id: The id of the course we want to retrieve
+    /);
+
 sub fetch_courses {
     my ($self, $conn, @course_ids) = @_;
     my $e = new_editor();
@@ -1169,7 +1169,7 @@ sub fetch_course_users {
         unless ($self->api_name =~ /\.staff/) and $e->allowed('MANAGE_RESERVES');
     
     
-    $users->{list} =  $e->search_asset_course_module_course_users($filter, {order_by => {acmcu => 'course'}});
+    $users->{list} =  $e->search_asset_course_module_course_users($filter, {order_by => {acmcu => 'id'}});
     for my $course_user (@{$users->{list}}) {
         my $patron = {};
         $patron->{id} = $course_user->id;
index 2546172..e7118c4 100644 (file)
@@ -141,6 +141,8 @@ sub load {
 
     return $self->load_simple("home") if $path =~ m|opac/home|;
     return $self->load_simple("css") if $path =~ m|opac/css|;
+    return $self->load_cresults if $path =~ m|opac/course/results|;
+    return $self->load_simple("course_search") if $path =~ m|opac/course_search|;
     return $self->load_simple("advanced") if
         $path =~ m:opac/(advanced|numeric|expert):;
 
index bd340ea..496c043 100644 (file)
@@ -39,4 +39,200 @@ sub load_course {
         {course => $course_id}
     );
     return Apache2::Const::OK;
+}
+
+sub load_cresults {
+    my $self = shift;
+    my %args = @_;
+    my $internal = $args{internal};
+    my $cgi = $self->cgi;
+    my $ctx = $self->ctx;
+    my $e = $self->editor;
+    my $limit = 10;
+
+    $ctx->{page} = 'cresult' unless $internal;
+    $ctx->{ids} = [];
+    $ctx->{courses} = [];
+    $ctx->{hit_count} = 0;
+    $ctx->{search_ou} = $self->_get_search_lib();
+    my $page = $cgi->param('page') || 0;
+    my $offset = $page * $limit;
+    my $results;
+    $ctx->{page_size} = $limit;
+    $ctx->{search_page} = $page;
+    $ctx->{pagable_limit} = 50;
+
+    # fetch this page plus the first hit from the next page
+    if ($internal) {
+        $limit = $offset + $limit + 1;
+        $offset = 0;
+    }
+
+    my ($user_query, $query, @queries, $modifiers) = _prepare_course_search($cgi, $ctx);
+
+    return Apache2::Const::OK unless $query;
+
+    $ctx->{user_query} = $user_query;
+    $ctx->{processed_search_query} = $query;
+    my $search_args = {};
+    my $course_numbers = ();
+    
+    my $where_clause;
+    my $and_terms = [];
+    my $or_terms = [];
+
+    # Handle is_archived checkbox and Org Selector
+    my $search_orgs = $U->get_org_descendants($ctx->{search_ou});
+    push @$and_terms, {'owning_lib' => $search_orgs};
+    push @$and_terms, {'-not' => {'+acmc' => 'is_archived'}} unless $query =~ qr\#include_archived\;
+
+    # Now let's push the actual queries
+    for my $query_obj (@queries) {
+        my $type = $query_obj->{'qtype'};
+        my $query = $query_obj->{'value'};
+        my $bool = $query_obj->{'bool'};
+        my $contains = $query_obj->{'contains'};
+        my $operator = ($contains eq 'nocontains') ? '!~*' : '~*';
+        my $search_query;
+        if ($type eq 'instructor') {
+            my $in = ($contains eq 'nocontains') ? "not in" : "in";
+            $search_query = {'id' => {$in => {
+                'from' => 'acmcu',
+                'select' => {'acmcu' => ['course']},
+                'where' => {'usr' => {'in' => {
+                    'from' => 'au',
+                    'select' => {'au' => ['id']},
+                    'where' => {
+                        '-or' => [
+                            {'pref_first_given_name' => {'~*' => $query}},
+                            {'first_given_name' => {'~*' => $query}},
+                            {'pref_second_given_name' => {'~*' => $query}},
+                            {'second_given_name' => {'~*' => $query}},
+                            {'pref_family_name' => {'~*' => $query}},
+                            {'family_name' => {'~*' => $query}}
+                        ]
+                    }
+                }}}
+            }}};
+        } else {
+            $search_query = ($contains eq 'nocontains') ?
+              {'+acmc' => { $type => {$operator => $query}}} :
+              {$type => {$operator => $query}};
+        }
+
+        if ($bool eq 'or') {
+            push @$or_terms, $search_query;
+        }
+
+        if ($bool eq 'and') {
+            push @$and_terms, $search_query;
+        }
+    }
+
+    if ($or_terms and @$or_terms > 0) {
+        if ($and_terms and @$and_terms > 0) {
+            push @$or_terms, $and_terms;
+        }
+        $where_clause = {'-or' => $or_terms};
+    } else {
+        $where_clause = {'-and' => $and_terms};
+    }
+
+    my $hits = $e->json_query({
+        "from" => "acmc",
+        "select" => {"acmc" => ['id']},
+        "where" => $where_clause
+    });
+
+    my $results = $e->json_query({
+        "from" => "acmc",
+        "select" => {"acmc" => [
+            'id',
+            'name',
+            'course_number',
+            'section_number',
+            'is_archived',
+            'owning_lib'
+        ]},
+        "limit" => $limit,
+        "offset" => $offset,
+        "order_by" => {"acmc" => ['id']},
+        "where" => $where_clause
+    });
+    for my $result (@$results) {
+        push @{$ctx->{courses}}, {
+            id => $result->{id},
+            course_number => $result->{course_number},
+            section_number => $result->{section_number},
+            owning_lib => $result->{owning_lib},
+            name => $result->{name},
+            is_archived => $result->{is_archived},
+            instructors => []
+        }
+    }
+
+    #$ctx->{courses} = $@courses;#[{id=>10, name=>"test", course_number=>"LIT"}];
+    $ctx->{hit_count} = @$hits || 0;
+    #$ctx->{hit_count} = 0;
+    return Apache2::Const::OK;
+}
+
+sub _prepare_course_search {
+    my ($cgi, $ctx) = @_;
+
+    my ($user_query, @queries) = _prepare_query($cgi);
+    my $modifiers;
+    $user_query //= '';
+
+    my $query = $user_query;
+    $query .= ' ' . $ctx->{global_search_filter} if $ctx->{global_search_filter};
+
+    foreach ($cgi->param('modifier')) {
+        $query = ('#' . $_ . ' ' . $query) unless $query =~ qr/\#\Q$_/;
+
+    }
+    # filters
+    foreach (grep /^fi:/, $cgi->param) {
+        /:(-?\w+)$/ or next;
+        my $term = join(",", $cgi->param($_));
+        $query .= " $1($term)" if length $term;
+    }
+
+    return () unless $query;
+
+    return ($user_query, $query, @queries);
+}
+
+sub _prepare_query {
+    my $cgi = shift;
+
+    return $cgi->param('query') unless $cgi->param('qtype');
+
+    my %parts;
+    my @part_names = qw/qtype contains query bool modifier/;
+    $parts{$_} = [ $cgi->param($_) ] for (@part_names);
+
+    my $full_query = '';
+    my @queries;
+    for (my $i = 0; $i < scalar @{$parts{'qtype'}}; $i++) {
+        my ($qtype, $contains, $query, $bool, $modifier) = map { $parts{$_}->[$i] } @part_names;
+        next unless $query =~ /\S/;
+
+        $contains = "" unless defined $contains;
+
+        push @queries, {
+            contains => $contains,
+            bool => $bool,
+            qtype => $qtype,
+            value => $query
+        };
+
+        $bool = ($bool and $bool eq 'or') ? '||' : '&&';
+
+        $query = "$qtype:$query";
+
+        $full_query = $full_query ? "($full_query $bool $query)" : $query;
+    }
+
+    return ($full_query, @queries);
 }
\ No newline at end of file
diff --git a/Open-ILS/src/templates/opac/course/results.tt2 b/Open-ILS/src/templates/opac/course/results.tt2
new file mode 100644 (file)
index 0000000..f8ae3b9
--- /dev/null
@@ -0,0 +1,114 @@
+[%- PROCESS "opac/parts/header.tt2";
+    WRAPPER "opac/parts/base.tt2";
+    INCLUDE "opac/parts/topnav.tt2";
+    ctx.page_title = l("Course Search Results");
+    page = CGI.param('page');
+    page = page.match('^\d+$') ? page : 0; # verify page is a sane value
+
+    page_count = (!ctx.page_size.defined || !ctx.hit_count.defined || ctx.page_size == 0) ? 1 : POSIX.ceil(ctx.hit_count / ctx.page_size);
+
+    # We don't want search engines indexing search results
+    ctx.metalinks.push('<meta name="robots" content="noindex,follow">');
+
+    PROCESS "opac/parts/misc_util.tt2";
+    PROCESS get_library;
+    ctx.result_start = 1 + ctx.page_size * page;
+    ctx.result_stop = ctx.page_size * (page + 1);
+    IF ctx.result_stop > ctx.hit_count; ctx.result_stop = ctx.hit_count; END;
+
+    result_count = ctx.result_start;
+-%]
+<h2 class="sr-only">[% l('Course Search Results') %]</h2>
+[% INCLUDE "opac/parts/searchbar.tt2" %]
+<div class="almost-content-wrapper">
+  <div id="results_header_bar">
+    <div id="results_header_inner">
+      <div class="results_header_btns">
+        <a href="[% mkurl(ctx.opac_root _ '/course_search', {$loc_name => loc_value}, 1) %]">[% l('Another Search') %]</a>
+      </div>
+    </div>
+  </div>
+</div>
+<div id="content-wrapper">
+  <div id="main-content">
+    <div id="results-page">
+      [% PROCESS "opac/parts/result/paginate.tt2" %] 
+      [% ctx.results_count_header = PROCESS results_count_header;
+    ctx.results_count_header %]
+      <div id="result_table_div">
+      <div id="result_block" class="result_block_visible">
+
+      <table id="result_table_table" title="[% l('Search Results') %]"
+        class="table_no_border_space table_no_cell_pad">
+        <thead class="sr-only">
+          <tr>
+            <th>[% l('Search result number') %]</th>
+            <th>[% l('Course details') %]</th>
+          </tr>
+        </thead>
+        <tbody id="result_table">
+        [% FOR course IN ctx.courses %]
+          [% course_url_path = ctx.opac_root _ '/course/' _ course.id; %]
+          [% # Do not pass "advanced params" to result detail code.
+          # Instead, pass the scrubed query in one-line form
+          del_parms = del_parms.merge(['query', 'bool', 
+                   'qtype', 'contains', '_adv']);
+          add_parms.import(
+           {query => ctx.naive_query_scrub(ctx.user_query)} );
+                  %]
+          <tr class="result_table_row">
+            <td class="results_row_count" name="results_row_count">
+              [% result_count; result_count = result_count + 1 %].
+            </td>
+            <td class="result_table_pic_header"></td>
+            <td class="result_table_title_cell" name="result_table_title_cell">
+              <div class="result_metadata">
+                <a class="record_title search_link" name="course_[% course.id %]"
+                  href="[% mkurl(course_url_path) %]"
+                  [% html_text_attr('title', l('Display course details for "[_1]"', course.name)) %]>
+                  [% course.course_number %]:  [% l(course.name) %]
+                </a>
+                <div>
+                  [% FOR instructor IN course.instructors %]
+                    [% instructorString = '';
+                      IF instructor.pref_family_name;
+                        instructorString = instructorString _ instructor.pref_family_name _ ', ';
+                      ELSE;
+                        instructorString = instructorString _ instructor.family_name _ ', ';
+                      END;
+                      IF instructor.pref_first_given_name;
+                        instructorString = instructorString _ instructor.pref_first_given_name;
+                      ELSE;
+                        instructorString = instructorString _ instructor.first_given_name;
+                      END; %]
+                  <a title="[% l('Perform an Instructor Search') %]"
+                    class="record_author"
+                    href="[%
+                       mkurl(ctx.opac_root _ '/results', {qtype => 'instructor', query => instructorString})
+                    %]" rel="nofollow" vocab="">
+                  [% instructorString %] ([% l(instructor.usr_role) %])</a>. 
+                  [% END %]
+                </div>
+                <div>
+                  <span><strong>[% l('Course Number') %]</strong>: [% course.course_number %]</span>
+                </div>
+                <div>
+                  <span><strong>[% l('Section Number') %]</strong>: [% course.section_number %]</span>
+                </div>
+              </div>
+            </td>
+            <td>
+              [% ctx.get_aou(course.owning_lib).name %]
+            </td>
+          </tr>
+        [% END %]
+        </tbody>
+      </table>
+      </div>
+      </div>
+    </div>
+    <div class="common-full-pad"></div>
+  </div>
+  <br class="clear-both" />
+</div>
+[%- END %]
diff --git a/Open-ILS/src/templates/opac/course_search.tt2 b/Open-ILS/src/templates/opac/course_search.tt2
new file mode 100644 (file)
index 0000000..979930a
--- /dev/null
@@ -0,0 +1,95 @@
+[%- PROCESS "opac/parts/header.tt2";
+    WRAPPER "opac/parts/base.tt2";
+    INCLUDE "opac/parts/topnav.tt2";
+    ctx.page_title = l("Course Search");
+
+    PROCESS "opac/parts/misc_util.tt2";
+    PROCESS get_library;
+    ctx.metalinks.push('<meta name="robots" content="noindex,follow">');
+-%]
+
+<h2 class="sr-only">[% l('Course Search') %]</h2>
+
+<div id="search-wrapper">
+  <div id="search-box">
+    <span class="search_catalog_lbl mobile_hide">[% l('Search the Catalog') %]</span>
+    <span class="adv_search_catalog_lbl"><a href="[% mkurl(ctx.opac_root _ '/home') %]"
+      id="home_adv_search_link">[%l('Basic Search')%]</a></span>
+    <span class="adv_search_catalog_lbl"><a href="[% mkurl(ctx.opac_root _ '/advanced', {}, depart_list) %]"
+      id="home_adv_search_link">[%l('Advanced Search')%]</a></span>
+    <span class="browse_the_catalog_lbl"><a href="[% mkurl(ctx.opac_root _ '/browse') %]">[%
+      l('Browse the Catalog')%]</a></span>
+    [% INCLUDE 'opac/parts/cart.tt2' %]
+  </div>
+</div>
+
+<div id="content-wrapper">
+  <div id="main-content">
+  <form action="[% ctx.opac_root %]/course/results" method="get">
+    <!-- TODO: Refactor simple.js addSearchRow to better handle
+      tables with IDs that aren't adv_... -->
+    <div class="header_middle" id="adv_search_input">
+      [% l("Course Search Input") %]
+    </div>
+
+    <div id="adv_search_filters" class="adv_filter_block">
+      <div class="adv_filter_block_item">
+        <div>
+          <strong><label for="ord.id">[% l('Search Library') %]</label></strong>
+        </div>
+        <div>
+          [% PROCESS "opac/parts/org_selector.tt2" %]
+          [% INCLUDE build_org_selector show_loc_groups=1 id=org.id %]
+          <span class="course_search_archived">
+            <input type="checkbox" name="modifier" value="include_archived"
+              [% CGI.param('modifier').grep('include_archived').size ? ' checked="checked"' : '' %]
+              id="opac.course_result.include_archived" />
+            <label for="opac.course_result.include_archived">
+              [% l('Include Archived Courses?') %]
+            </label>
+          </span>
+        </div>
+      </div>
+    </div>
+    
+    <div class="advanced_div">
+      <div id='adv_global_search' class='data_grid data_grid_center'>
+        <div id='adv_search_rows'>
+          <div class='adv_global_input_container'>
+            <table id="adv_global_input_table" role="presentation">
+              <tbody id='adv_global_tbody'>
+                [% INCLUDE "opac/parts/course_search/global_row.tt2" %]
+                <!-- add a new row -->
+                <tr id="adv_global_addrow">
+                  <td class="td-search-left">
+                    <a href="javascript:;" id="myopac_new_global_row" 
+                      onclick='addSearchRow();'>
+                      [% l('Add Search Row') %]
+                    </a>
+                  </td>
+                </tr>
+                <tr>
+              </tbody>
+            </table>
+          </div>
+        </div>
+
+        <div id='course_search_submit'>
+          <input type="hidden" name="_course" value="1" />
+          <span>
+            <input id='search-submit-go' type="submit" value="[% l('Search') %]" title="[% l('Search') %]" class="opac-button"
+              onclick='setTimeout(function(){$("search-submit-spinner").className=""; $("search-submit-go").className="hidden"}, 2000)'/>
+            <img id='search-submit-spinner' src='/opac/images/progressbar_green.gif[% ctx.cache_key %]'
+              class='hidden' alt="[% l('Search in progress icon') %]"/>
+          </span>
+          <a href="[% mkurl(ctx.opac_root _ '/course_search', {$loc_name => loc_value}, 1) %]"
+            class="opac-button">[% l('Clear Form') %]</a>
+        </div>
+      </div>
+    </div>
+    <div class="common-full-pad"></div>
+  </form>
+  </div>
+</div>
+
+[% END %]
\ No newline at end of file
index f998b7f..3cb686a 100644 (file)
@@ -1427,6 +1427,10 @@ div.result_table_utils_cont {
     font-size: [% css_fonts.size_bigger %];
 }
 
+.search_courses_label {
+    font-size: [% css_fonts.size_bigger %];
+}
+
 .lbl1 {
     font-size: [% css_fonts.size_bigger %];
     font-weight:bold;
diff --git a/Open-ILS/src/templates/opac/parts/course_search/global_row.tt2 b/Open-ILS/src/templates/opac/parts/course_search/global_row.tt2
new file mode 100644 (file)
index 0000000..d68842f
--- /dev/null
@@ -0,0 +1,55 @@
+[%
+    contains_options = [
+        {value => 'contains', label => l('Contains')},
+        {value => 'nocontains', label => l('Does not contain')}
+    ];
+    contains = CGI.param('contains');
+    queries = CGI.param('query');
+    bools = CGI.param('bool');
+    qtypes = CGI.param('qtype');
+    rowcount = 3;
+
+    # scalar.merge treats the scalar as a 1-item array
+    WHILE queries.size < rowcount; queries = queries.merge(['']); END;
+    WHILE bools.size < rowcount; bools = bools.merge(['and']); END;
+    WHILE qtypes.size < rowcount; qtypes = qtypes.merge(search.default_qtypes.${qtypes.size} ? [search.default_qtypes.${qtypes.size}] : ['keyword']); END;
+
+    FOR qtype IN qtypes;
+        c = contains.shift;
+        b = bools.shift;
+        q = queries.shift; %]
+
+<!-- tag the second row so the bool column won't be hidden -->
+<tr[% IF loop.index == 1 %] id="adv_global_row"[% END %]>
+    <td class="td-left">
+
+        <!-- bool selector.  hide for first row.  safe to ignore first bool value in form submission -->
+        <select title="[% l('Boolean search operator') %]" 
+          name='bool' style='width: auto' [% IF loop.first %] class='invisible' [% END %]>
+            <option value='and' [% b == 'and' ? 'selected="selected"' : '' %]>[% l('And') %]</option>
+            <option value='or' [% b == 'or' ? 'selected="selected"' : '' %]>[% l('Or') %]</option>
+        </select>
+
+        <!-- keyword, subject, etc. selector -->
+        <span class="qtype_selector_margin">
+            [% INCLUDE "opac/parts/course_search/qtype_selector.tt2"
+                query_type=qtype %]
+        </span>
+
+        <select title="[% l('Search phrase match strictness') %]"
+          name='contains' style='margin-right: 7px;'>
+            [% FOR o IN contains_options; -%]
+            <option value="[% o.value %]" [% c == o.value ? ' selected="selected"' : '' %]>[% o.label %]</option>
+            [% END %]
+        </select>
+        <input title="[% l('Search term') %]" aria-label="[% l('Search term') %]"
+          type='text' size='18' name='query' value="[% q | html %]" x-webkit-speech [% IF loop.index == 0 %] autofocus [% END %] />
+        <a href="javascript:;" class="row-remover"
+            title="[% l('Remove row') %]" alt="[% l('Remove row') %]"
+            onclick='return killRowIfAtLeast(2, this);'>
+                <img src="[% ctx.media_prefix %]/images/expert_row_close_btn.png[% ctx.cache_key %]"
+                    alt="[% l('Remove row') %]"/>
+            </a>
+    </td>
+</tr>
+[% END %]
diff --git a/Open-ILS/src/templates/opac/parts/course_search/qtype_selector.tt2 b/Open-ILS/src/templates/opac/parts/course_search/qtype_selector.tt2
new file mode 100644 (file)
index 0000000..a9770b1
--- /dev/null
@@ -0,0 +1,20 @@
+[%  query_types = [
+    {value => "name", label => l("Title"), plural_label => l("Titles"), browse => 1},
+    {value => "instructor", label => l("Instructor"), plural_label => l('Instructors')},
+    {value => "course_number", label => l("Course Number")}
+];
+-%]
+<select name="[% name || 'qtype' %]"[% IF id; ' id="'; id ; '"' ; END -%]
+    title="[% l('Select query type:') %]">
+    [%  query_type = query_type || CGI.param('qtype') || search.default_qtypes.0;
+      FOR qt IN query_types;
+        NEXT IF browse_only AND NOT qt.browse -%]
+    <option value='[% qt.value | html %]'[%
+        query_type == qt.value ? ' selected="selected"' : ''
+    %]>[% IF plural AND qt.plural_label;
+        qt.plural_label | html;
+    ELSE;
+        qt.label | html;
+    END %]</option>
+    [% END -%]
+</select>
index d86bd7d..7213986 100644 (file)
@@ -44,6 +44,9 @@ END;
         <span class="adv_search_catalog_lbl"><a href="[% mkurl(ctx.opac_root _ '/advanced', {},  expert_search_parms.merge(browse_search_parms, facet_search_parms)) %]"
             id="home_adv_search_link">[% l('Advanced Search') %]</a></span>
         <span class="browse_the_catalog_lbl"><a href="[% mkurl(ctx.opac_root _ '/browse', {}, expert_search_parms.merge(general_search_parms, facet_search_parms, ['fi:has_browse_entry'])) %]">[% l('Browse the Catalog') %]</a></span>
+        [% IF ctx.get_org_setting(ctx.aou_tree.id, 'circ.course_materials_opt_in') == 1 %]
+        <span class="search_courses_label"><a href="[% mkurl(ctx.opac_root _ '/course_search') %]">[% l('Search for Courses') %]</a></span>
+        [% END %]
         [% INCLUDE 'opac/parts/cart.tt2' %]
     </div>
     <div class="searchbar [% is_home_page ? 'searchbar-home' : '' %]">