TPac: patron saved searches user/berick/tpac-user-saved-searches
authorBill Erickson <berick@esilibrary.com>
Tue, 20 Dec 2011 20:46:24 +0000 (15:46 -0500)
committerBill Erickson <berick@esilibrary.com>
Tue, 21 Feb 2012 22:30:27 +0000 (17:30 -0500)
Adds a "Save Search" option on search results pages.  When clicked,
users are prompted to log in (if not already), then given a chance to
save the search.  Saving is done from within a new "Saved Searches" tab
in My Account, where users can also see/delete/execute existing saved
searches.  The saved search UI also provides an RSS link.

Currently, only searches that build a query-parser query are save-able.
This excludes MARC expert search, at least until it can be expressed as
a query-parser query.

For more easier additions of query_type in the future, this commit also
changes the query_type column from TEXT w/ constraint to ENUM, which is
simpler to add types to.

Signed-off-by: Bill Erickson <berick@esilibrary.com>
Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader.pm
Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Account.pm
Open-ILS/src/sql/Pg/005.schema.actors.sql
Open-ILS/src/sql/Pg/upgrade/XXXX.schema.saved_search_qtype_constraint.sql [new file with mode: 0644]
Open-ILS/src/templates/opac/myopac/save_search.tt2 [new file with mode: 0644]
Open-ILS/src/templates/opac/parts/myopac/base.tt2
Open-ILS/src/templates/opac/results.tt2
Open-ILS/web/css/skin/default/opac/style.css

index a0413c6..0511d45 100644 (file)
@@ -160,6 +160,7 @@ sub load {
     return $self->load_myopac_prefs_settings if $path =~ m|opac/myopac/prefs_settings|;
     return $self->load_myopac_prefs if $path =~ m|opac/myopac/prefs|;
     return $self->load_sms_cn if $path =~ m|opac/sms_cn|;
+    return $self->load_myopac_save_search if $path =~ m|opac/myopac/save_search|;
 
     return Apache2::Const::OK;
 }
index 489594c..f7d3e25 100644 (file)
@@ -1957,4 +1957,108 @@ sub load_password_reset {
     return Apache2::Const::OK;
 }
 
+sub load_myopac_save_search {
+    my $self = shift;
+    my $e = $self->editor;
+    my $ctx = $self->ctx;
+    my $name = $self->cgi->param('name');
+    my $action = $self->cgi->param('action') || '';
+    my @selected = $self->cgi->param('selected');
+
+    my ($query) = 
+        OpenILS::WWW::EGCatLoader::_prepare_biblio_search($self->cgi, $ctx);
+
+    $ctx->{full_query} = $query;
+
+    $e->xact_begin; # for saving and master-db reading
+
+    my $saved = $e->search_actor_usr_saved_search([
+        {   owner => $e->requestor->id,
+            query_type => 'query_parser',
+            target => 'record'
+        }, {order_by => {auss => 'create_date DESC'}}
+    ]);
+
+    if ($name) { # save the current query
+        
+        # see if that name is already taken
+        if (grep { $_->name eq $name } @$saved) {
+            $ctx->{name_taken} = $name;
+            $e->rollback;
+
+        } else {
+
+            # all clear, create the list
+            my $ss = Fieldmapper::actor::usr_saved_search->new;
+            $ss->owner($e->requestor->id);
+            $ss->query_type('query_parser');
+            $ss->query_text($query);
+            $ss->name($name);
+            $ss->target('record');
+
+            unless ($ss = $e->create_actor_usr_saved_search($ss)) {
+                $logger->error("tpac: error creating saved search ".$e->die_event);
+                return Apache2::Const::HTTP_INTERNAL_SERVER_ERROR;
+            }
+
+            $e->commit;
+            unshift(@$saved, $ss);
+        }
+
+    } elsif ($action eq 'delete' and @selected) {
+        
+        # verify the deleted searches all belong to this user
+        for my $ss_id (@selected) {
+            my ($ss) = grep { $_->id eq $ss_id } @$saved;
+
+            unless ($ss) {
+                $logger->error("tpac: attempt made to delete another user's list");
+                $e->rollback;
+                return Apache2::Const::HTTP_BAD_REQUEST;
+            }
+
+            unless ($e->delete_actor_usr_saved_search($ss)) {
+                $logger->error("tpac: error deleting saved search ".$e->die_event);
+                return Apache2::Const::HTTP_INTERNAL_SERVER_ERROR;
+            }
+
+            # remove the deteled list from our collection
+            $saved = [ grep { $_->id ne $ss_id } @$saved ];
+        }
+
+        $ctx->{searches_deleted} = scalar(@selected);
+        $e->commit;
+        
+    } else {
+        $e->rollback; # read-only
+    }
+
+
+    # extract the site() and depth() information so links can be built
+    # (if desired) using the CGI params for those instead.
+    my @saved_scrubbed;
+    for my $search (@$saved) {
+        my %blob = (search => $search);
+        my $query = $search->query_text;
+
+        if (my ($site) = ($query =~ /site\(([^\)]+)\)/)) {
+            $self->apache->log->warn("site = $site");
+            my ($org) = grep { $_->shortname eq $site } @{$ctx->{aou_list}->()};
+            $blob{loc} = $org->id;
+            $query =~ s/site\(([^\)]+)\)//g;
+        }
+
+        if (my ($depth) = ($query =~ /depth\((\d+)\)/)) {
+            $blob{depth} = $depth;
+            $query =~ s/depth\((\d+)\)//g;
+        }
+
+        $blob{scrubbed_query} = $query;
+        push(@saved_scrubbed, \%blob);        
+    }
+
+    $ctx->{saved_searches} = \@saved_scrubbed;
+    return Apache2::Const::OK;
+}
+
 1;
index fbe0504..eddf073 100644 (file)
@@ -587,6 +587,7 @@ $$;
 CREATE INDEX actor_usr_standing_penalty_usr_idx ON actor.usr_standing_penalty (usr);
 CREATE INDEX actor_usr_standing_penalty_staff_idx ON actor.usr_standing_penalty ( staff );
 
+CREATE TYPE actor.usr_saved_search_query_type AS ENUM ('URL', 'query_parser');
 
 CREATE TABLE actor.usr_saved_search (
     id              SERIAL          PRIMARY KEY,
@@ -596,10 +597,7 @@ CREATE TABLE actor.usr_saved_search (
        name            TEXT            NOT NULL,
        create_date     TIMESTAMPTZ     NOT NULL DEFAULT now(),
        query_text      TEXT            NOT NULL,
-       query_type      TEXT            NOT NULL
-                                       CONSTRAINT valid_query_text CHECK (
-                                       query_type IN ( 'URL' )) DEFAULT 'URL',
-                                       -- we may add other types someday
+       query_type      actor.usr_saved_search_query_type NOT NULL DEFAULT 'URL',
        target          TEXT            NOT NULL
                                        CONSTRAINT valid_target CHECK (
                                        target IN ( 'record', 'metarecord', 'callnumber' )),
diff --git a/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.saved_search_qtype_constraint.sql b/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.saved_search_qtype_constraint.sql
new file mode 100644 (file)
index 0000000..13da58c
--- /dev/null
@@ -0,0 +1,17 @@
+-- Evergreen DB patch XXXX.schema.saved_search_qtype_constraint.sql
+--
+BEGIN;
+
+-- check whether patch can be applied
+SELECT evergreen.upgrade_deps_block_check('XXXX', :eg_version);
+
+CREATE TYPE actor.usr_saved_search_query_type AS ENUM ('URL', 'query_parser');
+
+ALTER TABLE actor.usr_saved_search
+    DROP CONSTRAINT valid_query_text,
+    ALTER COLUMN query_type DROP DEFAULT,
+    ALTER COLUMN query_type TYPE actor.usr_saved_search_query_type
+        USING (query_type::actor.usr_saved_search_query_type),
+    ALTER COLUMN query_type SET DEFAULT 'URL';
+
+COMMIT;
diff --git a/Open-ILS/src/templates/opac/myopac/save_search.tt2 b/Open-ILS/src/templates/opac/myopac/save_search.tt2
new file mode 100644 (file)
index 0000000..ae95c3b
--- /dev/null
@@ -0,0 +1,81 @@
+[%  PROCESS "opac/parts/header.tt2";
+    PROCESS "opac/parts/misc_util.tt2";
+    WRAPPER "opac/parts/myopac/base.tt2";
+    myopac_page = "save_search"  %]
+<div id='myopac_save_search_div' style="padding:5px;">
+
+    [% IF CGI.param('save') OR ctx.name_taken %]
+    <form action="[% ctx.opac_root %]/myopac/save_search" method="POST">
+        [% FOR k IN CGI.Vars; # copy the search params into the form
+            NEXT IF !k OR k == 'save' OR k == 'name';
+            FOR val IN CGI.param(k) %]
+            <input type="hidden" name="[% k | html %]" value="[% val | html %]" />
+        [% END; END %]
+        <span>[% l('Save search as: ') %]</span>
+        <span class='pad-level-1'><input type='text' name='name'/></span>
+        <input type="submit" value="[% l('Save') %]" alt="[% l('Save') %]" class="opac-button"/>
+        [% IF ctx.name_taken %]
+            <span class='error'>
+                [% l('You already have a list named "[_1]".  Please choose another name.', ctx.name_taken) %]
+            </span>
+        [% END %]
+        <span class='pad-level-1'>
+            <b>[ <a href="[% mkurl(ctx.opac_root _ '/results') %]">[% l('Full Query : [_1]', ctx.full_query) | html %]</a> ]</b>
+        </span>
+        <hr/>
+    </form>
+    [% END %]
+
+    [% IF ctx.searches_deleted %]
+        <div class='pad-level-1 success'>
+            [% l('Successfully deleted [quant,_1,search,searches]', ctx.searches_deleted) %]
+        </div>
+    [% END %]
+
+    [% IF ctx.saved_searches.0 %]
+    <form method='POST'>
+        <div class="header_middle">[% l('Saved Searches') %]</div>
+        <input type="submit" value="[% l('Delete Selected') %]" alt="[% l('Delete Selected') %]" class="opac-button"/>
+        <input type='hidden' name='action' value='delete'/>
+        <table id='acct_saved_search_table' class='generic_table generic_table_header'>
+            <thead>
+                <tr>
+                    <th class='generic_table_checkbox'>
+                        <input type="checkbox" onclick="select_all_checkboxes('selected', this.checked)"/>
+                    </th>
+                    <th>[% l('Created On') %]</th>
+                    <th>[% l('Name') %]</th>
+                    <th>[% l('RSS') %]</th>
+                    <th>[% l('Full Query') %]</th>
+                </tr>
+            </thead>
+            <tbody>
+                [% FOR search_data IN ctx.saved_searches %]
+                <tr>
+                    <td class='generic_table_checkbox'>
+                        <input type="checkbox" name="selected" value="[% search_data.search.id %]"/>
+                    </td>
+                    <td>[% date.format(ctx.parse_datetime(search_data.search.create_date), DATE_FORMAT) %]</td>
+                    <td>
+                        <a href="[% mkurl(ctx.opac_root _ '/results', {
+                                query => search_data.scrubbed_query, 
+                                loc => search_data.loc, 
+                                depth => search_data.depth}, 1) 
+                        %]">[% search_data.search.name | html %]</a>
+                    </td>
+                    <td>
+                        <a target='_blank' 
+                            href='/opac/extras/opensearch/1.1/-/rss2-full?searchTerms=[% search_data.search.query_text | uri %]'><img
+                            alt="[% l('RSS Feed') %]" border="0"
+                            src="[% ctx.media_prefix %]/images/small-rss.png"/></a>
+                    </td>
+                    <td>[% search_data.search.query_text | html %]</td>
+                </tr>
+                [% END %]
+            </tbody>
+        </table>
+    </form>
+    [% END %]
+
+</div>
+[% END %]
index 9c11f13..800f239 100644 (file)
@@ -6,6 +6,7 @@
         {url => "holds", name => l("Holds")},
         {url => "prefs", name => l("Account Preferences")},
         {url => "lists", name => l("My Lists")}
+        {url => "save_search", name => l("Saved Searches")}
     ];
     skin_root = "../"
 %]
index 566936a..9cc2e02 100644 (file)
                             [% CGI.param('modifier').grep('available').size ? ' checked="checked"' : '' %] />
                         [% l('Limit to available items') %]
                     </label>
+
+                    [% IF CGI.param('query') %]
+                    <div class="results_header_div"></div>
+                    <div class='results_header_btns'>
+                        <a href="[% mkurl(ctx.opac_root _ '/myopac/save_search', {save => 1}) %]">[% l('Save Search') %]</a>
+                    </div>
+                    [% END %]
+
                 <div class="clear-both"></div>
             </div>
         </div>
index d9f2120..1b8c3e4 100644 (file)
@@ -904,6 +904,30 @@ div.result_place_hold {
        padding: 8px 0px 7px 0px;
 }
 
+.generic_table {
+    border-collapse: collapse;
+}
+
+.generic_table_header th {
+    text-align: left;
+       font-weight:bold;
+       text-transform:uppercase;
+       background: #d8d8d8;
+       font-size: 10px;
+       padding: 8px;
+}
+.generic_table_checkbox {
+    width: 1%;
+    padding-left:10px;
+}
+
+#acct_saved_search_table {
+    width: 75%;
+}
+#acct_saved_search_table td {
+    padding : 8px;
+}
+
 #acct_list_header select, #acct_list_header_anon select {
        font-weight:normal;
        text-transform:none;