From 9f273271093aa9226c5a27613bb5f7f1d3f0e7db Mon Sep 17 00:00:00 2001 From: Lebbeous Fogle-Weekley Date: Fri, 16 Sep 2011 18:31:56 -0400 Subject: [PATCH] Bookbag enhancements in TTOPAC Bookbags have descriptions now (and they're reflected in feeds). Bookbag item notes are editable. Bookbags can now be sorted by title or author using QP tricks. You can export a bookbag as CSV. Signed-off-by: Lebbeous Fogle-Weekley Signed-off-by: Mike Rylander --- Open-ILS/examples/fm_IDL.xml | 4 + .../OpenILS/Application/Actor/Container.pm | 56 +++-- .../lib/OpenILS/Application/AppUtils.pm | 52 ++++ .../OpenILS/Application/Trigger/Reactor.pm | 17 ++ .../Trigger/Reactor/ContainerCSV.pm | 49 ++++ .../perlmods/lib/OpenILS/WWW/EGCatLoader.pm | 1 + .../lib/OpenILS/WWW/EGCatLoader/Account.pm | 229 ++++++++++++++++-- .../src/perlmods/lib/OpenILS/WWW/SuperCat.pm | 1 + .../perlmods/lib/OpenILS/WWW/SuperCat/Feed.pm | 7 +- Open-ILS/src/sql/Pg/070.schema.container.sql | 4 + Open-ILS/src/sql/Pg/950.data.seed-values.sql | 50 ++++ .../upgrade/YYYY.schema.bookbag-goodies.sql | 69 ++++++ .../src/templates/opac/myopac/list/print.tt2 | 1 + Open-ILS/src/templates/opac/myopac/lists.tt2 | 70 +++++- .../templates/opac/parts/advanced/search.tt2 | 2 +- .../src/templates/opac/parts/filtersort.tt2 | 9 +- Open-ILS/web/css/skin/default/opac/style.css | 2 + 17 files changed, 573 insertions(+), 50 deletions(-) create mode 100644 Open-ILS/src/perlmods/lib/OpenILS/Application/Trigger/Reactor/ContainerCSV.pm create mode 100644 Open-ILS/src/sql/Pg/upgrade/YYYY.schema.bookbag-goodies.sql create mode 100644 Open-ILS/src/templates/opac/myopac/list/print.tt2 diff --git a/Open-ILS/examples/fm_IDL.xml b/Open-ILS/examples/fm_IDL.xml index 8c587e306b..97875eece9 100644 --- a/Open-ILS/examples/fm_IDL.xml +++ b/Open-ILS/examples/fm_IDL.xml @@ -3648,6 +3648,7 @@ SELECT usr, + @@ -4605,6 +4606,7 @@ SELECT usr, + @@ -4711,6 +4713,7 @@ SELECT usr, + @@ -5400,6 +5403,7 @@ SELECT usr, + diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Application/Actor/Container.pm b/Open-ILS/src/perlmods/lib/OpenILS/Application/Actor/Container.pm index ec381abf02..537f35fba0 100644 --- a/Open-ILS/src/perlmods/lib/OpenILS/Application/Actor/Container.pm +++ b/Open-ILS/src/perlmods/lib/OpenILS/Application/Actor/Container.pm @@ -131,29 +131,53 @@ __PACKAGE__->register_method( sub item_note_cud { my($self, $conn, $auth, $class, $note) = @_; + + return new OpenILS::Event("BAD_PARAMS") unless + $note->class_name =~ /bucket_item_note$/; + my $e = new_editor(authtoken => $auth, xact => 1); return $e->die_event unless $e->checkauth; - my $meth = 'retrieve_' . $ctypes{$class}; - my $nclass = $note->class_name; - (my $iclass = $nclass) =~ s/n$//og; + my $meat = $ctypes{$class} . "_item_note"; + my $meth = "retrieve_$meat"; - my $db_note = $e->$meth($note->id, { - flesh => 2, - flesh_fields => { - $nclass => ['item'], - $iclass => ['bucket'] - } - }); + my $item_meat = $ctypes{$class} . "_item"; + my $item_meth = "retrieve_$item_meat"; + + my $nhint = $Fieldmapper::fieldmap->{$note->class_name}->{hint}; + (my $ihint = $nhint) =~ s/n$//og; + + my ($db_note, $item); + + if ($note->isnew) { + $db_note = $note; + + $item = $e->$item_meth([ + $note->item, { + flesh => 1, flesh_fields => {$ihint => ["bucket"]} + } + ]) or return $e->die_event; + } else { + $db_note = $e->$meth([ + $note->id, { + flesh => 2, + flesh_fields => { + $nhint => ['item'], + $ihint => ['bucket'] + } + } + ]) or return $e->die_event; + + $item = $db_note->item; + } - if($db_note->item->bucket->owner ne $e->requestor->id) { - return $e->die_event unless - $e->allowed('UPDATE_CONTAINER', $db_note->item->bucket); + if($item->bucket->owner ne $e->requestor->id) { + return $e->die_event unless $e->allowed("UPDATE_CONTAINER"); } - $meth = 'create_' . $ctypes{$class} if $note->isnew; - $meth = 'update_' . $ctypes{$class} if $note->ischanged; - $meth = 'delete_' . $ctypes{$class} if $note->isdeleted; + $meth = 'create_' . $meat if $note->isnew; + $meth = 'update_' . $meat if $note->ischanged; + $meth = 'delete_' . $meat if $note->isdeleted; return $e->die_event unless $e->$meth($note); $e->commit; } diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Application/AppUtils.pm b/Open-ILS/src/perlmods/lib/OpenILS/Application/AppUtils.pm index d31bc2a99e..c0dbf7b2d9 100644 --- a/Open-ILS/src/perlmods/lib/OpenILS/Application/AppUtils.pm +++ b/Open-ILS/src/perlmods/lib/OpenILS/Application/AppUtils.pm @@ -1854,5 +1854,57 @@ sub get_bre_attrs { return $attrs; } +sub bib_container_items_via_search { + my ($class, $container_id, $search_query, $search_args) = @_; + + # First, Use search API to get container items sorted in any way that crad + # sorters support. + my $search_result = $class->simplereq( + "open-ils.search", "open-ils.search.biblio.multiclass.query", + $search_args, $search_query + ); + unless ($search_result) { + # empty result sets won't cause this, but actual errors should. + $logger->warn("bib_container_items_via_search() got nothing from search"); + return; + } + + # Throw away other junk from search, keeping only bib IDs. + my $id_list = [ map { pop @$_ } @{$search_result->{ids}} ]; + + return [] unless @$id_list; + + # Now get the bib container items themselves... + my $e = new OpenILS::Utils::CStoreEditor; + unless ($e) { + $logger->warn("bib_container_items_via_search() couldn't get cstoreeditor"); + return; + } + + my $items = $e->search_container_biblio_record_entry_bucket_item([ + { + "target_biblio_record_entry" => $id_list, + "bucket" => $container_id + }, { + flesh => 1, + flesh_fields => {"cbrebi" => [qw/notes target_biblio_record_entry/]} + } + ]); + unless ($items) { + $logger->warn( + "bib_container_items_via_search() couldn't get bucket items: " . + $e->die_event->{textcode} + ); + return; + } + + $e->disconnect; + + # ... and put them in the same order that the search API said they + # should be in. + my %ordering_hash = map { $_->target_biblio_record_entry->id, $_ } @$items; + return [map { $ordering_hash{$_} } @$id_list]; +} + 1; diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Application/Trigger/Reactor.pm b/Open-ILS/src/perlmods/lib/OpenILS/Application/Trigger/Reactor.pm index cfeeca64b4..4214cabe6c 100644 --- a/Open-ILS/src/perlmods/lib/OpenILS/Application/Trigger/Reactor.pm +++ b/Open-ILS/src/perlmods/lib/OpenILS/Application/Trigger/Reactor.pm @@ -5,6 +5,7 @@ use Template; use DateTime; use DateTime::Format::ISO8601; use Unicode::Normalize; +use XML::LibXML; use OpenSRF::Utils qw/:datetime/; use OpenSRF::Utils::Logger qw(:logger); use OpenILS::Application::AppUtils; @@ -199,6 +200,22 @@ my $_TT_helpers = { return; }, + csv_datum => sub { + my ($str) = @_; + + if ($str =~ /\,/ || $str =~ /"/) { + $str =~ s/"/""/g; + $str = '"' . $str . '"'; + } + + return $str; + }, + + xml_doc => sub { + my ($str) = @_; + return $str ? (new XML::LibXML)->parse_string($str) : undef; + } + }; diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Application/Trigger/Reactor/ContainerCSV.pm b/Open-ILS/src/perlmods/lib/OpenILS/Application/Trigger/Reactor/ContainerCSV.pm new file mode 100644 index 0000000000..28a341927c --- /dev/null +++ b/Open-ILS/src/perlmods/lib/OpenILS/Application/Trigger/Reactor/ContainerCSV.pm @@ -0,0 +1,49 @@ +package OpenILS::Application::Trigger::Reactor::ContainerCSV; +use base "OpenILS::Application::Trigger::Reactor"; +use strict; +use warnings; +use OpenSRF::Utils::Logger qw/:logger/; +use Data::Dumper; +$Data::Dumper::Indent = 0; +my $U = "OpenILS::Application::AppUtils"; + +sub ABOUT { + return q| + +The ContainerCSV Reactor Module processes the configured template after +fetching the items from the bookbag refererred to in $env->{target} +by using the search api with the query in $env->{params}{search}. It's +the event-creator's responsibility to build a correct search query and check +permissions and do that sort of thing. + +open-ils.trigger is not a public service, so that should be ok. + +The output, like all processed templates, is stored in the event_output table. + +|; +} + +sub handler { + my ($self, $env) = @_; + + # get items for bookbags (bib containers of btype bookbag) + if ($env->{user_data}{item_search}) { + # use the search api for bib container items + my $items = $U->bib_container_items_via_search( + $env->{target}->id, $env->{user_data}{item_search} + ) or return 0; # TODO build error output for db? + + $env->{items} = $items; + } else { + # XXX TODO If we're going to support other types of containers here, + # we'll probably just want to flesh those containers' items directly, + # not involve the search API. + + $logger->warn("ContainerCSV reactor used without item_search, doesn't know what to do."); # XXX + } + + return 1 if $self->run_TT($env); + return 0; +} + +1; diff --git a/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader.pm b/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader.pm index 651ac483e3..b6281ce562 100644 --- a/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader.pm +++ b/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader.pm @@ -145,6 +145,7 @@ sub load { return $self->load_myopac_update_password if $path =~ m|opac/myopac/update_password|; return $self->load_myopac_update_username if $path =~ m|opac/myopac/update_username|; return $self->load_myopac_bookbags if $path =~ m|opac/myopac/lists|; + return $self->load_myopac_bookbag_print if $path =~ m|opac/myopac/list/print|; return $self->load_myopac_bookbag_update if $path =~ m|opac/myopac/list/update|; return $self->load_myopac_circ_history if $path =~ m|opac/myopac/circ_history|; return $self->load_myopac_hold_history if $path =~ m|opac/myopac/hold_history|; diff --git a/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Account.pm b/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Account.pm index 92858eb59b..4b0477e8d0 100644 --- a/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Account.pm +++ b/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Account.pm @@ -225,6 +225,34 @@ sub _load_user_with_prefs { return undef; } +sub _get_bookbag_sort_params { + my ($self) = @_; + + # The interface that feeds this cgi parameter will provide a single + # argument for a QP sort filter, and potentially a modifier after a period. + # In practice this means the "sort" parameter will be something like + # "titlesort" or "authorsort.descending". + my $sorter = $self->cgi->param("sort") || ""; + my $modifier; + if ($sorter) { + $sorter =~ s/^(.*?)\.(.*)/$1/; + $modifier = $2 || undef; + } + + return ($sorter, $modifier); +} + +sub _prepare_bookbag_container_query { + my ($self, $container_id, $sorter, $modifier) = @_; + + return sprintf( + "container(bre,bookbag,%d,%s)%s%s", + $container_id, $self->editor->authtoken, + ($sorter ? " sort($sorter)" : ""), + ($modifier ? "#$modifier" : "") + ); +} + sub load_myopac_prefs_settings { my $self = shift; @@ -1214,6 +1242,7 @@ sub load_myopac_bookbags { my $e = $self->editor; my $ctx = $self->ctx; + my ($sorter, $modifier) = $self->_get_bookbag_sort_params; $e->xact_begin; # replication... my $rv = $self->load_mylist; @@ -1222,17 +1251,14 @@ sub load_myopac_bookbags { return $rv; } - my $args = { - order_by => {cbreb => 'name'}, - limit => $self->cgi->param('limit') || 10, - offset => $self->cgi->param('offset') || 0 - }; - $ctx->{bookbags} = $e->search_container_biblio_record_entry_bucket( [ - {owner => $self->editor->requestor->id, btype => 'bookbag'}, - {"flesh" => 1, "flesh_fields" => {"cbreb" => ["items"]}, %$args} - ], + {owner => $e->requestor->id, btype => 'bookbag'}, { + order_by => {cbreb => 'name'}, + limit => $self->cgi->param('limit') || 10, + offset => $self->cgi->param('offset') || 0 + } + ], {substream => 1} ); @@ -1241,17 +1267,33 @@ sub load_myopac_bookbags { return Apache2::Const::HTTP_INTERNAL_SERVER_ERROR; } - # get unique record IDs - my %rec_ids = (); - foreach my $bbag (@{$ctx->{bookbags}}) { - foreach my $rec_id ( - map { $_->target_biblio_record_entry } @{$bbag->items} - ) { - $rec_ids{$rec_id} = 1; + # Here is the loop that uses search to find the bib records in each + # bookbag. XXX This should be parallelized. Should this be done + # with OpenSRF::MultiSession, or is it enough to use OpenSRF::AppSession + # and call ->request() without calling ->gather() on any of those objects + # until all the requests have been issued? + + foreach my $bookbag (@{$ctx->{bookbags}}) { + my $query = $self->_prepare_bookbag_container_query( + $bookbag->id, $sorter, $modifier + ); + + # XXX we need to limit the number of records per bbag; use third arg + # of bib_container_items_via_search() i think. + my $items = $U->bib_container_items_via_search($bookbag->id, $query) + or return Apache2::Const::HTTP_INTERNAL_SERVER_ERROR; + + # Maybe save a little memory by creating only one XML::LibXML::Document + # instance for each record, even if record is repeated across bookbags. + + foreach my $rec (map { $_->target_biblio_record_entry } @$items) { + next if $ctx->{bookbags_marc_xml}{$rec->id}; + $ctx->{bookbags_marc_xml}{$rec->id} = + (new XML::LibXML)->parse_string($rec->marc); } - } - $ctx->{bookbags_marc_xml} = $self->fetch_marc_xml_by_id([keys %rec_ids]); + $bookbag->items($items); + } $e->rollback; return Apache2::Const::OK; @@ -1265,19 +1307,33 @@ sub load_myopac_bookbag_update { my $e = $self->editor; my $cgi = $self->cgi; + # save_notes is effectively another action, but is passed in a separate + # CGI parameter for what are really just layout reasons. + $action = 'save_notes' if $cgi->param('save_notes'); $action ||= $cgi->param('action'); + $list_id ||= $cgi->param('list'); my @add_rec = $cgi->param('add_rec') || $cgi->param('record'); my @selected_item = $cgi->param('selected_item'); my $shared = $cgi->param('shared'); my $name = $cgi->param('name'); + my $description = $cgi->param('description'); my $success = 0; my $list; - if($action eq 'create') { + # This url intentionally leaves off the edit_notes parameter, but + # may need to add some back in for paging. + + my $url = "https://" . $self->apache->hostname . + $self->ctx->{opac_root} . "/myopac/lists?"; + + $url .= 'sort=' . uri_escape($cgi->param("sort")) if $cgi->param("sort"); + + if ($action eq 'create') { $list = Fieldmapper::container::biblio_record_entry_bucket->new; $list->name($name); + $list->description($description); $list->owner($e->requestor->id); $list->btype('bookbag'); $list->pub($shared ? 't' : 'f'); @@ -1352,13 +1408,144 @@ sub load_myopac_bookbag_update { ); last unless $success; } + } elsif ($action eq 'save_notes') { + $success = $self->update_bookbag_item_notes; } - return $self->generic_redirect if $success; + return $self->generic_redirect($url) if $success; + + # XXX FIXME Bucket failure doesn't have a page to show the user anything + # right now. User just sees a 404 currently. $self->ctx->{bucket_action} = $action; $self->ctx->{bucket_action_failed} = 1; return Apache2::Const::OK; } -1 +sub update_bookbag_item_notes { + my ($self) = @_; + my $e = $self->editor; + + my @note_keys = grep /^note-\d+/, keys(%{$self->cgi->Vars}); + my @item_keys = grep /^item-\d+/, keys(%{$self->cgi->Vars}); + + # We're going to leverage an API call that's already been written to check + # permissions appropriately. + + my $a = create OpenSRF::AppSession("open-ils.actor"); + my $method = "open-ils.actor.container.item_note.cud"; + + for my $note_key (@note_keys) { + my $note; + + my $id = ($note_key =~ /(\d+)/)[0]; + + if (!($note = + $e->retrieve_container_biblio_record_entry_bucket_item_note($id))) { + my $event = $e->die_event; + $self->apache->log->warn( + "error retrieving cbrebin id $id, got event " . + $event->{textcode} + ); + $a->kill_me; + $self->ctx->{bucket_action_event} = $event; + return; + } + + if (length($self->cgi->param($note_key))) { + $note->ischanged(1); + $note->note($self->cgi->param($note_key)); + } else { + $note->isdeleted(1); + } + + my $r = $a->request($method, $e->authtoken, "biblio", $note)->gather(1); + + if (defined $U->event_code($r)) { + $self->apache->log->warn( + "attempt to modify cbrebin " . $note->id . + " returned event " . $r->{textcode} + ); + $e->rollback; + $a->kill_me; + $self->ctx->{bucket_action_event} = $r; + return; + } + } + + for my $item_key (@item_keys) { + my $id = int(($item_key =~ /(\d+)/)[0]); + my $text = $self->cgi->param($item_key); + + chomp $text; + next unless length $text; + + my $note = new Fieldmapper::container::biblio_record_entry_bucket_item_note; + $note->isnew(1); + $note->item($id); + $note->note($text); + + my $r = $a->request($method, $e->authtoken, "biblio", $note)->gather(1); + + if (defined $U->event_code($r)) { + $self->apache->log->warn( + "attempt to create cbrebin for item " . $note->item . + " returned event " . $r->{textcode} + ); + $e->rollback; + $a->kill_me; + $self->ctx->{bucket_action_event} = $r; + return; + } + } + + $a->kill_me; + return 1; # success +} + +sub load_myopac_bookbag_print { + my ($self) = @_; + + $self->apache->content_type("text/plain; encoding=utf8"); + + my $id = int($self->cgi->param("list")); + + my ($sorter, $modifier) = $self->_get_bookbag_sort_params; + + my $item_search = + $self->_prepare_bookbag_container_query($id, $sorter, $modifier); + + my $bbag; + + # Get the bookbag object itself, assuming we're allowed to. + if ($self->editor->allowed("VIEW_CONTAINER")) { + + $bbag = $self->editor->retrieve_container_biblio_record_entry_bucket($id) or return Apache2::Const::HTTP_INTERNAL_SERVER_ERROR; + } else { + my $bookbags = $self->editor->search_container_biblio_record_entry_bucket( + { + "id" => $id, + "-or" => { + "owner" => $self->editor->requestor->id, + "pub" => "t" + } + } + ) or return Apache2::Const::HTTP_INTERNAL_SERVER_ERROR; + + $bbag = pop @$bookbags; + } + + # If we have a bookbag we're allowed to look at, issue the A/T event + # to get CSV, passing as a user param that search query we built before. + if ($bbag) { + $self->ctx->{csv} = $U->fire_object_event( + undef, "container.biblio_record_entry_bucket.csv", + $bbag, $self->editor->requestor->home_ou, + undef, {"item_search" => $item_search} + ); + } + + return Apache2::Const::OK; +} + +1; diff --git a/Open-ILS/src/perlmods/lib/OpenILS/WWW/SuperCat.pm b/Open-ILS/src/perlmods/lib/OpenILS/WWW/SuperCat.pm index e148565d14..4ca4b9d24d 100644 --- a/Open-ILS/src/perlmods/lib/OpenILS/WWW/SuperCat.pm +++ b/Open-ILS/src/perlmods/lib/OpenILS/WWW/SuperCat.pm @@ -1024,6 +1024,7 @@ sub bookbag_feed { $feed->id($bucket_tag); $feed->title("Items in Book Bag [".$bucket->name."]"); + $feed->description($bucket->description || ("Items in Book Bag [".$bucket->name."]")); $feed->creator($host); $feed->update_ts(); diff --git a/Open-ILS/src/perlmods/lib/OpenILS/WWW/SuperCat/Feed.pm b/Open-ILS/src/perlmods/lib/OpenILS/WWW/SuperCat/Feed.pm index cd11a7542a..fde04eb461 100644 --- a/Open-ILS/src/perlmods/lib/OpenILS/WWW/SuperCat/Feed.pm +++ b/Open-ILS/src/perlmods/lib/OpenILS/WWW/SuperCat/Feed.pm @@ -322,8 +322,11 @@ sub title { my $self = shift; my $text = shift; $self->_create_node('/rss/channel',undef,'title', $text); - # RSS2 demands a /channel/description element; just dupe title until we give - # users the ability to provide a description for their bookbags +} + +sub description { + my $self = shift; + my $text = shift; $self->_create_node('/rss/channel',undef,'description', $text); } diff --git a/Open-ILS/src/sql/Pg/070.schema.container.sql b/Open-ILS/src/sql/Pg/070.schema.container.sql index 6bbd17c76c..0e21c5fda4 100644 --- a/Open-ILS/src/sql/Pg/070.schema.container.sql +++ b/Open-ILS/src/sql/Pg/070.schema.container.sql @@ -35,6 +35,7 @@ CREATE TABLE container.copy_bucket ( INITIALLY DEFERRED, name TEXT NOT NULL, btype TEXT NOT NULL DEFAULT 'misc' REFERENCES container.copy_bucket_type (code) DEFERRABLE INITIALLY DEFERRED, + description TEXT, pub BOOL NOT NULL DEFAULT FALSE, create_time TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(), CONSTRAINT cb_name_once_per_owner UNIQUE (owner,name,btype) @@ -88,6 +89,7 @@ CREATE TABLE container.call_number_bucket ( INITIALLY DEFERRED, name TEXT NOT NULL, btype TEXT NOT NULL DEFAULT 'misc' REFERENCES container.call_number_bucket_type (code) DEFERRABLE INITIALLY DEFERRED, + description TEXT, pub BOOL NOT NULL DEFAULT FALSE, create_time TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(), CONSTRAINT cnb_name_once_per_owner UNIQUE (owner,name,btype) @@ -142,6 +144,7 @@ CREATE TABLE container.biblio_record_entry_bucket ( INITIALLY DEFERRED, name TEXT NOT NULL, btype TEXT NOT NULL DEFAULT 'misc' REFERENCES container.biblio_record_entry_bucket_type (code) DEFERRABLE INITIALLY DEFERRED, + description TEXT, pub BOOL NOT NULL DEFAULT FALSE, create_time TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(), CONSTRAINT breb_name_once_per_owner UNIQUE (owner,name,btype) @@ -194,6 +197,7 @@ CREATE TABLE container.user_bucket ( INITIALLY DEFERRED, name TEXT NOT NULL, btype TEXT NOT NULL DEFAULT 'misc' REFERENCES container.user_bucket_type (code) DEFERRABLE INITIALLY DEFERRED, + description TEXT, pub BOOL NOT NULL DEFAULT FALSE, create_time TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(), CONSTRAINT ub_name_once_per_owner UNIQUE (owner,name,btype) diff --git a/Open-ILS/src/sql/Pg/950.data.seed-values.sql b/Open-ILS/src/sql/Pg/950.data.seed-values.sql index 0dd096c3cb..42e029c427 100644 --- a/Open-ILS/src/sql/Pg/950.data.seed-values.sql +++ b/Open-ILS/src/sql/Pg/950.data.seed-values.sql @@ -10046,6 +10046,56 @@ INSERT INTO action_trigger.environment ( event_def, path) VALUES ( ,( 47, 'record.queue.owner') ; +INSERT INTO action_trigger.hook (key, core_type, description, passive) +VALUES ( + 'container.biblio_record_entry_bucket.csv', + 'cbreb', + oils_i18n_gettext( + 'container.biblio_record_entry_bucket.csv', + 'Produce a CSV file representing a bookbag', + 'ath', + 'description' + ), + FALSE +); + +INSERT INTO action_trigger.reactor (module, description) +VALUES ( + 'ContainerCSV', + oils_i18n_gettext( + 'ContainerCSV', + 'Facilitates produce a CSV file representing a bookbag by introducing an "items" variable into the TT environment, sorted as dictated according to user params', + 'atr', + 'description' + ) +); + +INSERT INTO action_trigger.event_definition ( + id, active, owner, + name, hook, reactor, + validator, template +) VALUES ( + 48, TRUE, 1, + 'Bookbag CSV', 'container.biblio_record_entry_bucket.csv', 'ContainerCSV', + 'NOOP_True', +$$ +[%- +# target is the bookbag itself. The 'items' variable does not need to be in +# the environment because a special reactor will take care of filling it in. + +FOR item IN items; + bibxml = helpers.xml_doc(item.target_biblio_record_entry.marc); + title = ""; + FOR part IN bibxml.findnodes('//*[@tag="245"]/*[@code="a" or @code="b"]'); + title = title _ part.textContent; + END; + author = bibxml.findnodes('//*[@tag="100"]/*[@code="a"]').textContent; + + helpers.csv_datum(title) %],[% helpers.csv_datum(author) %],[% FOR note IN item.notes; helpers.csv_datum(note.note); ","; END; "\n"; +END -%] +$$ +); + SELECT SETVAL('authority.control_set_id_seq'::TEXT, 100); SELECT SETVAL('authority.control_set_authority_field_id_seq'::TEXT, 1000); SELECT SETVAL('authority.control_set_bib_field_id_seq'::TEXT, 1000); diff --git a/Open-ILS/src/sql/Pg/upgrade/YYYY.schema.bookbag-goodies.sql b/Open-ILS/src/sql/Pg/upgrade/YYYY.schema.bookbag-goodies.sql new file mode 100644 index 0000000000..df4e45687e --- /dev/null +++ b/Open-ILS/src/sql/Pg/upgrade/YYYY.schema.bookbag-goodies.sql @@ -0,0 +1,69 @@ +-- Evergreen DB patch YYYY.schema.bookbag-goodies.sql + +BEGIN; + +SELECT evergreen.upgrade_deps_block_check('YYYY', :eg_version); + +ALTER TABLE container.biblio_record_entry_bucket + ADD COLUMN description TEXT; + +ALTER TABLE container.call_number_bucket + ADD COLUMN description TEXT; + +ALTER TABLE container.copy_bucket + ADD COLUMN description TEXT; + +ALTER TABLE container.user_bucket + ADD COLUMN description TEXT; + +INSERT INTO action_trigger.hook (key, core_type, description, passive) +VALUES ( + 'container.biblio_record_entry_bucket.csv', + 'cbreb', + oils_i18n_gettext( + 'container.biblio_record_entry_bucket.csv', + 'Produce a CSV file representing a bookbag', + 'ath', + 'description' + ), + FALSE +); + +INSERT INTO action_trigger.reactor (module, description) +VALUES ( + 'ContainerCSV', + oils_i18n_gettext( + 'ContainerCSV', + 'Facilitates produce a CSV file representing a bookbag by introducing an "items" variable into the TT environment, sorted as dictated according to user params', + 'atr', + 'description' + ) +); + +INSERT INTO action_trigger.event_definition ( + id, active, owner, + name, hook, reactor, + validator, template +) VALUES ( + 48, TRUE, 1, + 'Bookbag CSV', 'container.biblio_record_entry_bucket.csv', 'ContainerCSV', + 'NOOP_True', +$$ +[%- +# target is the bookbag itself. The 'items' variable does not need to be in +# the environment because a special reactor will take care of filling it in. + +FOR item IN items; + bibxml = helpers.xml_doc(item.target_biblio_record_entry.marc); + title = ""; + FOR part IN bibxml.findnodes('//*[@tag="245"]/*[@code="a" or @code="b"]'); + title = title _ part.textContent; + END; + author = bibxml.findnodes('//*[@tag="100"]/*[@code="a"]').textContent; + + helpers.csv_datum(title) %],[% helpers.csv_datum(author) %],[% FOR note IN item.notes; helpers.csv_datum(note.note); ","; END; "\n"; +END -%] +$$ +); + +COMMIT; diff --git a/Open-ILS/src/templates/opac/myopac/list/print.tt2 b/Open-ILS/src/templates/opac/myopac/list/print.tt2 new file mode 100644 index 0000000000..fa3988f7f0 --- /dev/null +++ b/Open-ILS/src/templates/opac/myopac/list/print.tt2 @@ -0,0 +1 @@ +[%- ctx.csv.template_output.data -%] diff --git a/Open-ILS/src/templates/opac/myopac/lists.tt2 b/Open-ILS/src/templates/opac/myopac/lists.tt2 index 0c22f6d93a..863bb1a957 100644 --- a/Open-ILS/src/templates/opac/myopac/lists.tt2 +++ b/Open-ILS/src/templates/opac/myopac/lists.tt2 @@ -27,6 +27,8 @@ + + @@ -51,9 +53,27 @@ class="opac-button" /> + + + + + + + +

[% l("Your existing lists") %]

+

+

+ + [% INCLUDE "opac/parts/filtersort.tt2" + value=CGI.param('sort') %] + +
+

+ [% INCLUDE "opac/parts/anon_list.tt2" %] [% IF ctx.bookbags.size %]
@@ -79,6 +99,13 @@
+
+
+ + + +
+
[% IF bbag.pub == 't' %] @@ -88,6 +115,7 @@ [% bbag.name | html %] [% END %] + [% IF bbag.description %]
[% bbag.description | html %][% END %]
[% IF bbag.pub == 't'; %] @@ -100,6 +128,7 @@
+ @@ -111,8 +140,18 @@ inputs[i].checked = this.checked;}"/> - - + + + [% END %] [% FOR item IN bbag.items; - rec_id = item.target_biblio_record_entry; + rec_id = item.target_biblio_record_entry.id; attrs = {marc_xml => ctx.bookbags_marc_xml.$rec_id}; PROCESS get_marc_attrs args=attrs %] - + @@ -145,13 +184,34 @@ authorquery = attrs.author | replace('[,\.:;]', ''); mkurl(ctx.opac_root _ '/results', {qtype => 'author', query => authorquery}, ['page']) -%]">[% attrs.author | html %] + [% IF CGI.param("edit_notes") == bbag.id %] + + [% ELSE %] + + [% END %] + + [% END %] + [% IF CGI.param("edit_notes") == bbag.id %] + + + [% END %]
[% l('Title') %][% l('Author(s)') %] + [% l('Title') %] + + [% l('Author(s)') %] + + [% l('Notes') %] + [% IF CGI.param("edit_notes") != bbag.id %] + | [% l('Edit') %] + [% END %] +
+ [% FOR note IN item.notes %] + + [% END %] + + + [% FOR note IN item.notes %] +
[% note.note | html %]
+ [% END %] +
+
-

+

[% END %] diff --git a/Open-ILS/src/templates/opac/parts/advanced/search.tt2 b/Open-ILS/src/templates/opac/parts/advanced/search.tt2 index 6b97540b65..1c28bb5312 100644 --- a/Open-ILS/src/templates/opac/parts/advanced/search.tt2 +++ b/Open-ILS/src/templates/opac/parts/advanced/search.tt2 @@ -51,7 +51,7 @@ [% INCLUDE "opac/parts/filtersort.tt2" - value=CGI.param('sort') %] + value=CGI.param('sort') class='results_header_sel' %] diff --git a/Open-ILS/src/templates/opac/parts/filtersort.tt2 b/Open-ILS/src/templates/opac/parts/filtersort.tt2 index 664be17fc3..74d9c8a6f4 100644 --- a/Open-ILS/src/templates/opac/parts/filtersort.tt2 +++ b/Open-ILS/src/templates/opac/parts/filtersort.tt2 @@ -1,16 +1,15 @@ - - + - + - + diff --git a/Open-ILS/web/css/skin/default/opac/style.css b/Open-ILS/web/css/skin/default/opac/style.css index 3664dff9ff..6a0cec6980 100644 --- a/Open-ILS/web/css/skin/default/opac/style.css +++ b/Open-ILS/web/css/skin/default/opac/style.css @@ -966,6 +966,7 @@ a.dash-link:hover { text-decoration: underline !important; } .hold-editor-controls a { padding-left: 2em; } .text-right { text-align: right; } +.text-right-top { text-align: right; vertical-align: top; } .rdetail-author-div { padding-bottom: 10px; } .invisible { visibility: hidden; } @@ -1030,3 +1031,4 @@ a.opac-button { } +.bookbag-item-row td { vertical-align: top; } -- 2.43.2