[GIT] Evergreen ILS branch main updated. b10b089b32f7f920764177942965bd10af9b9c27

This is an automated email from the git hooks/post-receive script. It was generated because a ref change was pushed to the repository containing the project "Evergreen ILS". The branch, main has been updated via b10b089b32f7f920764177942965bd10af9b9c27 (commit) via 1715ac44996fbb7e710d91b5827896f20dbb53d9 (commit) via fa784dcbf647689ebffa11157afa4a7734dc08ce (commit) via 51b86f2ec966c7cdad2d575a19c7ae4e771ea82c (commit) via 72a8cc4bb0a25b1521e79e241a8ffe7401c164bd (commit) via ebc9613ff23a162d585ba21fc50d9bde2898595b (commit) from 2cd643df8a207eff8f232a97efa37471b9e65da8 (commit) Those revisions listed above that are new to this repository have not appeared on any other notification email; so we list those revisions in full, below. - Log ----------------------------------------------------------------- commit b10b089b32f7f920764177942965bd10af9b9c27 Author: Stephanie Leary <stephanie.leary@equinoxoli.org> Date: Fri Mar 21 20:31:44 2025 +0000 Stamping upgrade scripts for course link tracker Signed-off-by: Stephanie Leary <stephanie.leary@equinoxoli.org> diff --git a/Open-ILS/src/sql/Pg/002.schema.config.sql b/Open-ILS/src/sql/Pg/002.schema.config.sql index 27f620b41c..5a26813b54 100644 --- a/Open-ILS/src/sql/Pg/002.schema.config.sql +++ b/Open-ILS/src/sql/Pg/002.schema.config.sql @@ -92,7 +92,7 @@ CREATE TRIGGER no_overlapping_deps BEFORE INSERT OR UPDATE ON config.db_patch_dependencies FOR EACH ROW EXECUTE PROCEDURE evergreen.array_overlap_check ('deprecates'); -INSERT INTO config.upgrade_log (version, applied_to) VALUES ('1466', :eg_version); -- sleary/miker +INSERT INTO config.upgrade_log (version, applied_to) VALUES ('1467', :eg_version); -- sandbergja/blake/sleary CREATE TABLE config.bib_source ( id SERIAL PRIMARY KEY, diff --git a/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.link_click.sql b/Open-ILS/src/sql/Pg/upgrade/1467.schema.link_click.sql similarity index 95% rename from Open-ILS/src/sql/Pg/upgrade/XXXX.schema.link_click.sql rename to Open-ILS/src/sql/Pg/upgrade/1467.schema.link_click.sql index 6fd5f903ad..bfb609dd18 100644 --- a/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.link_click.sql +++ b/Open-ILS/src/sql/Pg/upgrade/1467.schema.link_click.sql @@ -1,6 +1,6 @@ BEGIN; --- SELECT evergreen.upgrade_deps_block_check('xxxx', :eg_version); +SELECT evergreen.upgrade_deps_block_check('1467', :eg_version); CREATE TABLE action.eresource_link_click ( id BIGSERIAL PRIMARY KEY, commit 1715ac44996fbb7e710d91b5827896f20dbb53d9 Author: Jane Sandberg <sandbergja@gmail.com> Date: Sat Jul 20 07:17:07 2024 -0700 LP1895695: Incorporate feedback from review * Move module out of OpenILS::Application into OpenILS::WWW, since it does not implement an OpenSRF application. * Add POD descriptions of both perl modules in this feature. * Remove the IDL required flag for a field with a DEFAULT. * Make action.eresource_link_click_course.course NULLable. Signed-off-by: Jane Sandberg <sandbergja@gmail.com> Signed-off-by: blake <blake@mobiusconsortium.org> Signed-off-by: Stephanie Leary <stephanie.leary@equinoxoli.org> diff --git a/Open-ILS/examples/fm_IDL.xml b/Open-ILS/examples/fm_IDL.xml index 56f0d16c2b..1a46e9a133 100644 --- a/Open-ILS/examples/fm_IDL.xml +++ b/Open-ILS/examples/fm_IDL.xml @@ -15902,7 +15902,7 @@ SELECT usr, reporter:label="Eresource link clicks"> <fields oils_persist:primary="id" oils_persist:sequence="action.eresource_link_click_id_seq"> <field reporter:label="ID" name="id" reporter:datatype="id" /> - <field reporter:label="Date/time of click" name="clicked_at" reporter:datatype="timestamp" oils_obj:required="true"/> + <field reporter:label="Date/time of click" name="clicked_at" reporter:datatype="timestamp"/> <field reporter:label="URL" name="url" reporter:datatype="text" oils_obj:required="true"/> <field reporter:label="Record" name="record" reporter:datatype="link"/> <field reporter:label="Courses" name="courses" oils_persist:virtual="true" reporter:datatype="link"/> diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Application/README.adoc b/Open-ILS/src/perlmods/lib/OpenILS/Application/README.adoc new file mode 100644 index 0000000000..a0d82e8122 --- /dev/null +++ b/Open-ILS/src/perlmods/lib/OpenILS/Application/README.adoc @@ -0,0 +1,7 @@ +This OpenILS::Application Perl module namespace is generally +reserved for modules (and module groups) that provide an OpenSRF +application implementation. + +If you are writing a Perl module that does not implement +an OpenSRF application, consider adding it to a different directory +and namespace. diff --git a/Open-ILS/src/perlmods/lib/OpenILS/WWW/EResourceLinkClick.pm b/Open-ILS/src/perlmods/lib/OpenILS/WWW/EResourceLinkClick.pm index 808886c118..61adb945ca 100644 --- a/Open-ILS/src/perlmods/lib/OpenILS/WWW/EResourceLinkClick.pm +++ b/Open-ILS/src/perlmods/lib/OpenILS/WWW/EResourceLinkClick.pm @@ -1,9 +1,19 @@ +=head1 NAME + +OpenILS::WWW::EResourceLinkClick + +=head1 DESCRIPTION + +This module is responsible for accepting data about +eresource link clicks from HTTP requests. +=cut + package OpenILS::WWW::EResourceLinkClick; use strict; use warnings; -use OpenILS::Application::EResourceLinkClick; +use OpenILS::WWW::EResourceLinkClick::Click; use Apache2::Const -compile => qw( OK HTTP_BAD_REQUEST HTTP_INTERNAL_SERVER_ERROR HTTP_NOT_IMPLEMENTED ); @@ -18,23 +28,23 @@ sub handler { my $referer = $cgi->http('Referer') || ''; my $user_agent = $cgi->http('User-Agent') || ''; - my $result = OpenILS::Application::EResourceLinkClick->add_click( + my $result = OpenILS::WWW::EResourceLinkClick::Click->add_click( $record_id, $url, $referer, $user_agent ); - if( $result eq OpenILS::Application::EResourceLinkClick::BadInput ) { + if( $result eq OpenILS::WWW::EResourceLinkClick::Click::BadInput ) { return Apache2::Const::HTTP_BAD_REQUEST; } - if( $result eq OpenILS::Application::EResourceLinkClick::InternalError ) { + if( $result eq OpenILS::WWW::EResourceLinkClick::Click::InternalError ) { return Apache2::Const::HTTP_INTERNAL_SERVER_ERROR; } - if( $result eq OpenILS::Application::EResourceLinkClick::NotConfigured ) { + if( $result eq OpenILS::WWW::EResourceLinkClick::Click::NotConfigured ) { return Apache2::Const::HTTP_NOT_IMPLEMENTED; } - if( $result eq OpenILS::Application::EResourceLinkClick::Success ) { + if( $result eq OpenILS::WWW::EResourceLinkClick::Click::Success ) { $r->content_type('text/plain'); $r->print('click recorded'); return Apache2::Const::OK; diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Application/EResourceLinkClick.pm b/Open-ILS/src/perlmods/lib/OpenILS/WWW/EResourceLinkClick/Click.pm similarity index 92% rename from Open-ILS/src/perlmods/lib/OpenILS/Application/EResourceLinkClick.pm rename to Open-ILS/src/perlmods/lib/OpenILS/WWW/EResourceLinkClick/Click.pm index 308de5a63d..0cfe221640 100644 --- a/Open-ILS/src/perlmods/lib/OpenILS/Application/EResourceLinkClick.pm +++ b/Open-ILS/src/perlmods/lib/OpenILS/WWW/EResourceLinkClick/Click.pm @@ -1,4 +1,14 @@ -package OpenILS::Application::EResourceLinkClick; +=head1 NAME + +OpenILS::WWW::EResourceLinkClick::Click.pm + +=head1 DESCRIPTION +This module is responsible for validating and +persisting information about a click on +an eresource link. +=cut + +package OpenILS::WWW::EResourceLinkClick::Click; use OpenILS::Application; use base qw/OpenILS::Application/; use OpenILS::Utils::CStoreEditor qw/:funcs/; diff --git a/Open-ILS/src/perlmods/live_t/40-eresource-link-click.t b/Open-ILS/src/perlmods/live_t/40-eresource-link-click.t index f3a26c1282..87768e47c2 100644 --- a/Open-ILS/src/perlmods/live_t/40-eresource-link-click.t +++ b/Open-ILS/src/perlmods/live_t/40-eresource-link-click.t @@ -6,7 +6,7 @@ use Test::More tests => 2; use OpenILS::Utils::TestUtils; use OpenILS::Utils::CStoreEditor qw/:funcs/; -diag('Test the EResourceLinkClick module'); +diag('Test the EResourceLinkClick::Click module'); my $script = OpenILS::Utils::TestUtils->new(); $script->bootstrap; @@ -14,7 +14,7 @@ our $apputils = "OpenILS::Application::AppUtils"; my $e = new_editor; $e->init; -BEGIN { use_ok('OpenILS::Application::EResourceLinkClick'); } +BEGIN { use_ok('OpenILS::WWW::EResourceLinkClick::Click'); } subtest('add_click', sub { plan tests => 2; @@ -23,14 +23,14 @@ subtest('add_click', sub { plan tests => 2; set_global_flag($e, 'f'); - my $response = OpenILS::Application::EResourceLinkClick->add_click( + my $response = OpenILS::WWW::EResourceLinkClick::Click->add_click( 238, 'http://example.com/ebookapi/t/001', 'https://my-evergreen.org/eg/opac/results', 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:123.0) Gecko/20100101 Firefox/123.0' ); - is($response, OpenILS::Application::EResourceLinkClick::NotConfigured, 'says that it is not configured'); + is($response, OpenILS::WWW::EResourceLinkClick::Click::NotConfigured, 'says that it is not configured'); assert_no_clicks_added_to_db($e); }); @@ -41,53 +41,53 @@ subtest('add_click', sub { subtest('when the referer did not come from the record or results page', sub { plan tests => 2; - my $response = OpenILS::Application::EResourceLinkClick->add_click( + my $response = OpenILS::WWW::EResourceLinkClick::Click->add_click( 238, 'http://example.com/ebookapi/t/001', 'https://some-non-eg-site/bad-path', 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:123.0) Gecko/20100101 Firefox/123.0' ); - is($response, OpenILS::Application::EResourceLinkClick::BadInput, 'says that the input is bad'); + is($response, OpenILS::WWW::EResourceLinkClick::Click::BadInput, 'says that the input is bad'); assert_no_clicks_added_to_db($e); }); subtest('when user agent is a bot', sub { plan tests => 2; - my $response = OpenILS::Application::EResourceLinkClick->add_click( + my $response = OpenILS::WWW::EResourceLinkClick::Click->add_click( 238, 'http://example.com/ebookapi/t/001', 'https://my-evergreen.org/eg/opac/results', 'Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)' ); - is($response, OpenILS::Application::EResourceLinkClick::BadInput, 'says that the input is bad'); + is($response, OpenILS::WWW::EResourceLinkClick::Click::BadInput, 'says that the input is bad'); assert_no_clicks_added_to_db($e); }); subtest('when url does not exist on the record in question', sub { plan tests => 2; - my $response = OpenILS::Application::EResourceLinkClick->add_click( + my $response = OpenILS::WWW::EResourceLinkClick::Click->add_click( 238, 'http://not-a-real-url/not-actually/on-the-record', 'https://my-evergreen.org/eg/opac/results', 'Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)' ); - is($response, OpenILS::Application::EResourceLinkClick::BadInput, 'says that the input is bad'); + is($response, OpenILS::WWW::EResourceLinkClick::Click::BadInput, 'says that the input is bad'); assert_no_clicks_added_to_db($e); }); subtest('when input is valid', sub { plan tests => 2; - my $response = OpenILS::Application::EResourceLinkClick->add_click( + my $response = OpenILS::WWW::EResourceLinkClick::Click->add_click( 238, 'http://example.com/ebookapi/t/001', 'https://my-evergreen.org/eg/opac/results', 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:123.0) Gecko/20100101 Firefox/123.0' ); - is($response, OpenILS::Application::EResourceLinkClick::Success, 'says that it is successful'); + is($response, OpenILS::WWW::EResourceLinkClick::Click::Success, 'says that it is successful'); my $rows = $e->search_action_eresource_link_click({record => 238}); is(scalar(@{ $rows }), 1, 'adds the click to the database'); @@ -112,14 +112,14 @@ subtest('add_click', sub { $e->create_asset_course_module_course_materials( $acmcm ); $e->xact_commit; - my $response = OpenILS::Application::EResourceLinkClick->add_click( + my $response = OpenILS::WWW::EResourceLinkClick::Click->add_click( 238, 'http://example.com/ebookapi/t/001', 'https://my-evergreen.org/eg/opac/results', 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:123.0) Gecko/20100101 Firefox/123.0' ); - is($response, OpenILS::Application::EResourceLinkClick::Success, 'says that it is successful'); + is($response, OpenILS::WWW::EResourceLinkClick::Click::Success, 'says that it is successful'); my $rows = $e->search_action_eresource_link_click_course({course => 12345}); is(scalar(@{ $rows }), 1, 'adds a click course mapping to the database'); is($rows->[0]->course_name, 'Introduction to cats', 'adds the course name to the mapping'); diff --git a/Open-ILS/src/sql/Pg/090.schema.action.sql b/Open-ILS/src/sql/Pg/090.schema.action.sql index 9c35cb73f9..714265bf20 100644 --- a/Open-ILS/src/sql/Pg/090.schema.action.sql +++ b/Open-ILS/src/sql/Pg/090.schema.action.sql @@ -1835,7 +1835,7 @@ CREATE TABLE action.eresource_link_click ( CREATE TABLE action.eresource_link_click_course ( id SERIAL PRIMARY KEY, click BIGINT NOT NULL REFERENCES action.eresource_link_click (id) ON DELETE CASCADE, - course INT NOT NULL, -- no REFERENCES, since the course could have been deleted + course INT REFERENCES asset.course_module_course (id) ON UPDATE CASCADE ON DELETE SET NULL, course_name TEXT NOT NULL, course_number TEXT NOT NULL ); diff --git a/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.link_click.sql b/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.link_click.sql index 5067383601..6fd5f903ad 100644 --- a/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.link_click.sql +++ b/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.link_click.sql @@ -12,7 +12,7 @@ CREATE TABLE action.eresource_link_click ( CREATE TABLE action.eresource_link_click_course ( id SERIAL PRIMARY KEY, click BIGINT NOT NULL REFERENCES action.eresource_link_click (id) ON DELETE CASCADE, - course INT NOT NULL, -- no REFERENCES, since the course could have been deleted + course INT REFERENCES asset.course_module_course (id) ON UPDATE CASCADE ON DELETE SET NULL, course_name TEXT NOT NULL, course_number TEXT NOT NULL ); commit fa784dcbf647689ebffa11157afa4a7734dc08ce Author: Jane Sandberg <sandbergja@gmail.com> Date: Sat Mar 9 05:43:21 2024 -0800 LP1895695: Add docs for the eresource link click feature Signed-off-by: Jane Sandberg <sandbergja@gmail.com> Signed-off-by: Stephanie Leary <stephanie.leary@equinoxoli.org> diff --git a/docs/RELEASE_NOTES_NEXT/OPAC/eresource_link_click_track.adoc b/docs/RELEASE_NOTES_NEXT/OPAC/eresource_link_click_track.adoc new file mode 100644 index 0000000000..35be9e5aff --- /dev/null +++ b/docs/RELEASE_NOTES_NEXT/OPAC/eresource_link_click_track.adoc @@ -0,0 +1,67 @@ +== Eresource link click tracking == + +This version of Evergreen introduces the ability +to track user clicks on eresources in the public +catalog. + +This feature provides usage data on eresources in +the catalog. One potential use for this feature +is to provide the equivalent of circulation +statistics for online course materials. + +Data can be accessed via the Evergreen reporter. + +=== Data collection + +This feature does not collect any personally +identifiable data about the user who clicks +on the link. The pieces of data that are +collected are: + +* The URL clicked +* The time it was clicked +* The bibliographic record that contains the +URL. +* The ID, name, and number of any courses +that use the bibliographic record. + +=== Enabling the feature + +The new tables can grow forever, so before +enabling this feature: + +* Ensure that you are monitoring disk space on +the server(s) that house your postgres database. +* Decide on a retention period for click data, +and set up the provided +``delete_old_eresource_link_clicks`` +script to delete old data regularly. + +To enable this feature: + +. Set the +``opac.eresources.link_click_tracking`` +global flag to true. +. Restart memcached and apache HTTP server. + + +=== Accuracy + +Statistics from this feature are collected +on a best efforts basis, and have certain +limitations when it comes to accuracy: + +* It does +not provide any guarantees against somebody +deliberately inflating the statistics of a +particular link, either through repeated network +calls or repeatedly clicking on a link they don't +actually intend to read. +* Clicks from certain very old, unsupported browsers +(notable Microsoft Internet Explorer) will +not be counted. +* Major bots are excluded, but uncommon bots and +bots that set a misleading User Agent header are +included unless they are blocked at the web server +or load balancer level. + diff --git a/docs/modules/opac/nav.adoc b/docs/modules/opac/nav.adoc index 848a0d704d..51cf46a8fb 100644 --- a/docs/modules/opac/nav.adoc +++ b/docs/modules/opac/nav.adoc @@ -8,5 +8,6 @@ ** xref:opac:advanced_features.adoc[Bibliographic Search Enhancements] ** xref:opac:tpac_meta_record_holds.adoc[TPAC Metarecord Search and Metarecord Level Holds] ** xref:opac:linked_libraries.adoc[Library Information Pages] +** xref:opac:eresource_link_click_track.adoc[Eresource Link Click Tracking] ** xref:opac:opensearch.adoc[Adding Evergreen Search to Web Browsers] ** xref:opac:search_form.adoc[Adding an Evergreen search form to a web page] diff --git a/docs/modules/opac/pages/eresource_link_click_track.adoc b/docs/modules/opac/pages/eresource_link_click_track.adoc new file mode 100644 index 0000000000..d017c59fa1 --- /dev/null +++ b/docs/modules/opac/pages/eresource_link_click_track.adoc @@ -0,0 +1,67 @@ += Eresource link click tracking = +:toc: + +Evergreen has a feature +to track user clicks on eresources in the public +catalog. + +This feature provides usage data on eresources in +the catalog. One potential use for this feature +is to provide the equivalent of circulation +statistics for online course materials. + +Data can be accessed via the Evergreen reporter. + +== Data collection == + +This feature does not collect any personally +identifiable data about the user who clicks +on the link. The pieces of data that are +collected are: + +* The URL clicked +* The time it was clicked +* The bibliographic record that contains the +URL. +* The ID, name, and number of any courses +that use the bibliographic record. + +== Enabling the feature == + +The new tables can grow forever, so before +enabling this feature: + +* Ensure that you are monitoring disk space on +the server(s) that house your postgres database. +* Decide on a retention period for click data, +and set up the provided +``delete_old_eresource_link_clicks`` +script to delete old data regularly. + +To enable this feature: + +. Set the +``opac.eresources.link_click_tracking`` +global flag to true. +. Restart memcached and apache HTTP server. + + +== Accuracy == + +Statistics from this feature are collected +on a best efforts basis, and have certain +limitations when it comes to accuracy: + +* It does +not provide any guarantees against somebody +deliberately inflating the statistics of a +particular link, either through repeated network +calls or repeatedly clicking on a link they don't +actually intend to read. +* Clicks from certain very old, unsupported browsers +(notable Microsoft Internet Explorer) will +not be counted. +* Major bots are excluded, but uncommon bots and +bots that set a misleading User Agent header are +included unless they are blocked at the web server +or load balancer level. commit 51b86f2ec966c7cdad2d575a19c7ae4e771ea82c Author: Jane Sandberg <sandbergja@gmail.com> Date: Fri Mar 8 21:45:06 2024 -0800 LP1895695: Add OPAC parts for the eresource click track feature Signed-off-by: Jane Sandberg <sandbergja@gmail.com> Signed-off-by: Stephanie Leary <stephanie.leary@equinoxoli.org> diff --git a/Open-ILS/src/templates-bootstrap/opac/parts/header.tt2 b/Open-ILS/src/templates-bootstrap/opac/parts/header.tt2 index 52c6c777be..98383b7763 100755 --- a/Open-ILS/src/templates-bootstrap/opac/parts/header.tt2 +++ b/Open-ILS/src/templates-bootstrap/opac/parts/header.tt2 @@ -137,6 +137,8 @@ want_dojo = 1; END; + eresource_click_track = ctx.get_cgf("opac.eresources.link_click_tracking"); + # ... and for interfaces that require manual trigger of action triggers IF can_call_action_trigger == 'true'; want_dojo = 1; diff --git a/Open-ILS/src/templates-bootstrap/opac/parts/js.tt2 b/Open-ILS/src/templates-bootstrap/opac/parts/js.tt2 index 6745dec584..71fe9b4cae 100755 --- a/Open-ILS/src/templates-bootstrap/opac/parts/js.tt2 +++ b/Open-ILS/src/templates-bootstrap/opac/parts/js.tt2 @@ -191,6 +191,13 @@ var aou_hash = { <script >if ($('#client_tz_id')) { $('#client_tz_id').value = OpenSRF.tz }</script> [%- END; # want_dojo -%] +[% IF eresource_click_track.enabled == "t"; %] +<script type="module"> + import {EresourceClickTrack} from '[% ctx.media_prefix %]/js/ui/default/opac/eresource_click_tracker.module.js'; + new EresourceClickTrack().setup('.uri_link'); +</script> +[% END; # eresource_click_track %] + [%- IF ctx.max_cart_size; %] <script >var max_cart_size = [% ctx.max_cart_size %];</script> [%- END; %] diff --git a/Open-ILS/src/templates-bootstrap/opac/parts/record/summary.tt2 b/Open-ILS/src/templates-bootstrap/opac/parts/record/summary.tt2 index d3932ec496..57e781ab71 100755 --- a/Open-ILS/src/templates-bootstrap/opac/parts/record/summary.tt2 +++ b/Open-ILS/src/templates-bootstrap/opac/parts/record/summary.tt2 @@ -534,7 +534,7 @@ ctx.metalinks.push(' [%- IF filtered_type.length > 0 -%] <strong> [% filtered_type %] </strong> [%- END -%] - <a href="[% filtered_href %]" class="uri_link" property="url"> + <a href="[% filtered_href %]" class="uri_link" property="url" data-record-id="[% ctx.bre_id %]"> [%- IF filtered_href != filtered_link; '<span property="description">' _ filtered_link _ '</span>'; ELSE; diff --git a/Open-ILS/src/templates-bootstrap/opac/parts/result/table.tt2 b/Open-ILS/src/templates-bootstrap/opac/parts/result/table.tt2 index 832c00abfe..7a7cd7b06e 100755 --- a/Open-ILS/src/templates-bootstrap/opac/parts/result/table.tt2 +++ b/Open-ILS/src/templates-bootstrap/opac/parts/result/table.tt2 @@ -285,7 +285,7 @@ [% END %] [% FOR uri IN args.uris %] <dt>[% l('Electronic resource') %]</dt> - <dd><a href="[% uri.href | html %]" class="uri_link" target="_blank">[% uri.link | html %]</a> + <dd><a href="[% uri.href | html %]" class="uri_link" data-record-id="[% rec.bre_id %]" target="_blank">[% uri.link | html %]</a> [%- IF uri.note -%] [%- '- <span property="description">' _ uri.note _ '</span>' %] [%- ELSE -%] diff --git a/Open-ILS/src/templates/opac/parts/header.tt2 b/Open-ILS/src/templates/opac/parts/header.tt2 index 804edcc6e8..19fb78e21d 100644 --- a/Open-ILS/src/templates/opac/parts/header.tt2 +++ b/Open-ILS/src/templates/opac/parts/header.tt2 @@ -137,6 +137,8 @@ want_dojo = 1; END; + eresource_click_track = ctx.get_cgf("opac.eresources.link_click_tracking"); + # ... and for interfaces that require manual trigger of action triggers IF can_call_action_trigger == 'true'; want_dojo = 1; diff --git a/Open-ILS/src/templates/opac/parts/js.tt2 b/Open-ILS/src/templates/opac/parts/js.tt2 index 5c6dcc30a8..1766befc88 100644 --- a/Open-ILS/src/templates/opac/parts/js.tt2 +++ b/Open-ILS/src/templates/opac/parts/js.tt2 @@ -208,6 +208,13 @@ var aou_hash = { <script type="text/javascript">if ($('client_tz_id')) { $('client_tz_id').value = OpenSRF.tz }</script> [%- END; # want_dojo -%] +[% IF eresource_click_track.enabled == "t"; %] +<script type="module"> + import {EresourceClickTrack} from '[% ctx.media_prefix %]/js/ui/default/opac/eresource_click_tracker.module.js'; + new EresourceClickTrack().setup('.uri_link'); +</script> +[% END; # eresource_click_track %] + [%- IF ctx.max_cart_size; %] <script type="text/javascript">var max_cart_size = [% ctx.max_cart_size %];</script> [%- END; %] diff --git a/Open-ILS/src/templates/opac/parts/record/summary.tt2 b/Open-ILS/src/templates/opac/parts/record/summary.tt2 index 530c10be66..2876c8e31e 100644 --- a/Open-ILS/src/templates/opac/parts/record/summary.tt2 +++ b/Open-ILS/src/templates/opac/parts/record/summary.tt2 @@ -232,7 +232,7 @@ IF num_uris > 0; [%- IF filtered_type.length > 0 -%] <strong> [% filtered_type %] </strong> [%- END -%] - <a href="[% filtered_href %]" class="uri_link" property="url" target="_blank" rel="noopener"> + <a href="[% filtered_href %]" class="uri_link" property="url" target="_blank" rel="noopener" data-record-id="[% ctx.bre_id %]"> [%- IF filtered_href != filtered_link; '<span property="description">' _ filtered_link _ '</span>'; ELSE; diff --git a/Open-ILS/src/templates/opac/parts/result/table.tt2 b/Open-ILS/src/templates/opac/parts/result/table.tt2 index 1b23e074e7..c9e2c62bad 100644 --- a/Open-ILS/src/templates/opac/parts/result/table.tt2 +++ b/Open-ILS/src/templates/opac/parts/result/table.tt2 @@ -351,7 +351,7 @@ END; <td valign='top'> <strong>[% l('Electronic resource') %]</strong> </td> - <td><a href="[% uri.href | html %]" class="uri_link" target="_blank" rel="noopener">[% uri.link | html %]</a> + <td><a href="[% uri.href | html %]" class="uri_link" target="_blank" data-record-id="[% rec.bre_id %]" rel="noopener">[% uri.link | html %]</a> [%- IF uri.note -%] [%- '- <span property="description">' _ uri.note _ '</span>' %] [%- ELSE -%] diff --git a/Open-ILS/web/js/ui/default/opac/eresource_click_tracker.module.js b/Open-ILS/web/js/ui/default/opac/eresource_click_tracker.module.js new file mode 100644 index 0000000000..efdb8dd902 --- /dev/null +++ b/Open-ILS/web/js/ui/default/opac/eresource_click_tracker.module.js @@ -0,0 +1,14 @@ +export class EresourceClickTrack { + setup(selector) { + if(window.navigator.sendBeacon) { + document.querySelectorAll(selector).forEach(link => { + link.addEventListener('click', () => { + const data = new FormData(); + data.append('record_id', link.getAttribute('data-record-id')); + data.append('url', link.getAttribute('href')); + window.navigator.sendBeacon('/opac/extras/eresource_link_click_track', data); + }); + }); + } + } +} diff --git a/Open-ILS/web/opac/tests/eresource_click_tracker.spec.module.js b/Open-ILS/web/opac/tests/eresource_click_tracker.spec.module.js new file mode 100644 index 0000000000..0b69506ca4 --- /dev/null +++ b/Open-ILS/web/opac/tests/eresource_click_tracker.spec.module.js @@ -0,0 +1,26 @@ +import { EresourceClickTrack } from "../../js/ui/default/opac/eresource_click_tracker.module.js"; +import { JSDOM } from '../deps/node_modules/jsdom/lib/api.js'; + +describe("eresourceClickTrack", () => { + it("sends a beacon on click", () => { + global.window = new JSDOM(`<!DOCTYPE html><body><a href="https://my-database" data-record-id="12345" id="link">Click here</a></body>`).window; + Object.defineProperty(global.window, 'navigator', { + value: {sendBeacon: () => Promise.resolve()} + }) + spyOn(global.window.navigator, 'sendBeacon'); + global.document = global.window.document; + + const expectedData = new FormData(); + expectedData.append('record_id', 12345); + expectedData.append('url', 'https://my-database'); + + new EresourceClickTrack().setup('#link'); + const clickEvent = new global.window.Event( 'click', { bubbles: true } ) + global.document.querySelector('#link').dispatchEvent(clickEvent); + + expect(global.window.navigator.sendBeacon).toHaveBeenCalledWith( + '/opac/extras/eresource_link_click_track', + expectedData + ); + }); +}); commit 72a8cc4bb0a25b1521e79e241a8ffe7401c164bd Author: Jane Sandberg <sandbergja@gmail.com> Date: Wed Mar 6 17:24:11 2024 -0800 LP1895695: Add perl and apache for the eresource link click feature Signed-off-by: Jane Sandberg <sandbergja@gmail.com> Signed-off-by: Stephanie Leary <stephanie.leary@equinoxoli.org> diff --git a/Open-ILS/examples/apache_24/eg_vhost.conf.in b/Open-ILS/examples/apache_24/eg_vhost.conf.in index 906bb841d5..857bbbeaad 100644 --- a/Open-ILS/examples/apache_24/eg_vhost.conf.in +++ b/Open-ILS/examples/apache_24/eg_vhost.conf.in @@ -85,6 +85,14 @@ OSRFTranslatorConfig @sysconfdir@/opensrf_core.xml Require all granted </Location> +# Eresource link click tracker +<Location /opac/extras/eresource_link_click_track> + SetHandler perl-script + PerlHandler OpenILS::WWW::EResourceLinkClick + PerlSendHeader On + Require all granted +</Location> + # Flattener service <Location /opac/extras/flattener> SetHandler perl-script diff --git a/Open-ILS/examples/crontab.example b/Open-ILS/examples/crontab.example index 9f94a7b863..057846f729 100644 --- a/Open-ILS/examples/crontab.example +++ b/Open-ILS/examples/crontab.example @@ -103,5 +103,10 @@ EG_BIN_DIR = /openils/bin # Clean out action.hold_request_reset_reason_entry table 0 1 * * * . ~/.bashrc && $EG_BIN_DIR/purge_hold_reset_reason_entries.srfsh +# Delete old link click analytics that are no longer needed after +# the given number of days (365 by default) +#0 1 * * * cd /openils/bin && /usr/bin/perl ./delete_old_eresource_link_clicks.pl > /dev/null + + # TODO: add other entries diff --git a/Open-ILS/src/Makefile.am b/Open-ILS/src/Makefile.am index f0a1d9136e..a573e41431 100644 --- a/Open-ILS/src/Makefile.am +++ b/Open-ILS/src/Makefile.am @@ -84,6 +84,7 @@ core_scripts = $(examples)/oils_ctl.sh \ $(supportscr)/pingest.pl \ $(supportscr)/ingest_ctl \ $(supportscr)/background_import_mgr.pl \ + $(supportscr)/delete_old_eresource_link_clicks.pl \ $(supportscr)/edi_fetcher.pl \ $(supportscr)/edi_order_pusher.pl \ $(supportscr)/edi_pusher.pl \ diff --git a/Open-ILS/src/extras/install/Makefile.debian-bookworm b/Open-ILS/src/extras/install/Makefile.debian-bookworm index 6236c7022f..cf41bfcd5a 100644 --- a/Open-ILS/src/extras/install/Makefile.debian-bookworm +++ b/Open-ILS/src/extras/install/Makefile.debian-bookworm @@ -118,7 +118,8 @@ export CPAN_MODULES = \ Text::Levenshtein::Damerau::XS \ Pass::OTP \ Authen::WebAuthn \ - Email::Send + Email::Send \ + Duadua export CPAN_MODULES_FORCE = \ Business::Stripe \ diff --git a/Open-ILS/src/extras/install/Makefile.debian-bullseye b/Open-ILS/src/extras/install/Makefile.debian-bullseye index d21d512d42..fbf4935b61 100644 --- a/Open-ILS/src/extras/install/Makefile.debian-bullseye +++ b/Open-ILS/src/extras/install/Makefile.debian-bullseye @@ -118,7 +118,8 @@ export CPAN_MODULES = \ Email::Send \ Pass::OTP \ Authen::WebAuthn \ - Locale::Country + Locale::Country \ + Duadua export CPAN_MODULES_FORCE = \ Business::Stripe \ diff --git a/Open-ILS/src/extras/install/Makefile.debian-buster b/Open-ILS/src/extras/install/Makefile.debian-buster index 0e69ad7566..c1ef3add99 100644 --- a/Open-ILS/src/extras/install/Makefile.debian-buster +++ b/Open-ILS/src/extras/install/Makefile.debian-buster @@ -118,7 +118,8 @@ export CPAN_MODULES = \ Text::Levenshtein::Damerau::XS \ Pass::OTP \ Authen::WebAuthn \ - Email::Send + Email::Send \ + Duadua export CPAN_MODULES_FORCE = \ Business::Stripe \ diff --git a/Open-ILS/src/extras/install/Makefile.fedora b/Open-ILS/src/extras/install/Makefile.fedora index 05db2b5f22..64543ee2c7 100644 --- a/Open-ILS/src/extras/install/Makefile.fedora +++ b/Open-ILS/src/extras/install/Makefile.fedora @@ -92,7 +92,8 @@ export CPAN_MODULES = \ Config::General \ Pass::OTP \ Authen::WebAuthn \ - Rose::URI + Rose::URI \ + Duadua export CPAN_MODULES_FORCE = \ Business::Stripe \ diff --git a/Open-ILS/src/extras/install/Makefile.ubuntu-jammy b/Open-ILS/src/extras/install/Makefile.ubuntu-jammy index 7d04e76bf7..90995526dd 100644 --- a/Open-ILS/src/extras/install/Makefile.ubuntu-jammy +++ b/Open-ILS/src/extras/install/Makefile.ubuntu-jammy @@ -117,7 +117,8 @@ export CPAN_MODULES = \ String::KeyboardDistance \ Pass::OTP \ Authen::WebAuthn \ - Text::Levenshtein::Damerau::XS + Text::Levenshtein::Damerau::XS \ + Duadua export CPAN_MODULES_FORCE = \ Business::Stripe \ diff --git a/Open-ILS/src/extras/install/Makefile.ubuntu-noble b/Open-ILS/src/extras/install/Makefile.ubuntu-noble index f708beb01b..59f2c955ad 100644 --- a/Open-ILS/src/extras/install/Makefile.ubuntu-noble +++ b/Open-ILS/src/extras/install/Makefile.ubuntu-noble @@ -118,7 +118,8 @@ export CPAN_MODULES = \ String::KeyboardDistance \ Pass::OTP \ Authen::WebAuthn \ - Text::Levenshtein::Damerau::XS + Text::Levenshtein::Damerau::XS \ + Duadua export CPAN_MODULES_FORCE = \ Business::Stripe \ diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Application/EResourceLinkClick.pm b/Open-ILS/src/perlmods/lib/OpenILS/Application/EResourceLinkClick.pm new file mode 100644 index 0000000000..308de5a63d --- /dev/null +++ b/Open-ILS/src/perlmods/lib/OpenILS/Application/EResourceLinkClick.pm @@ -0,0 +1,89 @@ +package OpenILS::Application::EResourceLinkClick; +use OpenILS::Application; +use base qw/OpenILS::Application/; +use OpenILS::Utils::CStoreEditor qw/:funcs/; +use Duadua; +use strict; use warnings; + +use OpenILS::Application::AppUtils; +my $apputils = "OpenILS::Application::AppUtils"; +my $U = $apputils; + +use constant Success => 'Success'; +use constant NotConfigured => 'NotConfigured'; +use constant BadInput => 'BadInput'; +use constant InternalError => 'InternalError'; + +sub add_click { + my ($self, $record_id, $url, $referer, $user_agent) = @_; + + # We do not check auth for this editor, since links + # can be clicked by unauthenticated users and we + # still want to record those in the db + my $editor = new_editor; + return NotConfigured unless ($self->_feature_enabled($editor)); + return BadInput if $self->_input_is_bad($record_id, $url, $referer, $user_agent); + + return $self->_create_link($record_id, $url, $editor) ? Success : InternalError; +} + + +sub _create_link { + my ($self, $record_id, $url, $editor) = @_; + my $click = Fieldmapper::action::eresource_link_click->new; + $click->record($record_id); + $click->url($url); + $editor->xact_begin; + $editor->create_action_eresource_link_click($click) or return 0; + my $associated_courses = $editor->search_asset_course_module_course_materials({record => $record_id}); + foreach(@{ $associated_courses }) { + my $course = $editor->retrieve_asset_course_module_course($_->course) or next; + my $click_course = Fieldmapper::action::eresource_link_click_course->new; + $click_course->click($click->id); + $click_course->course($course->id); + $click_course->course_name($course->name); + $click_course->course_number($course->course_number); + $editor->create_action_eresource_link_click_course($click_course); + } + $editor->xact_commit; + return 1; +} + +sub _input_is_bad { + my ($self, $record_id, $url, $referer, $user_agent) = @_; + return 1 unless ($self->_referer_valid($referer)); + return 1 if Duadua->new($user_agent)->is_bot; + return 1 unless $self->_url_exists_on_record($url, $record_id); + return 0; +} + +sub _feature_enabled { + my ($self, $editor) = @_; + $editor->init; + my $flag = $editor->retrieve_config_global_flag('opac.eresources.link_click_tracking'); + return ($flag->enabled eq 't'); +} + +sub _referer_valid { + my ($self, $referer) = @_; + return ($referer =~ /eg\/opac\/(record|results)/); +} + +# Confirm that the URL and record ID we received from +# the client actually match, since anybody could send +# a request to this endpoint with mismatched data, +# resulting in garbage for anybody running a report +sub _url_exists_on_record { + my ($self, $url, $record_id) = @_; + my $root_org = $U->get_org_tree->id; + my $uris = $apputils->simplereq( + 'open-ils.search', + 'open-ils.search.asset.uri.retrieve_by_bib.atomic', + $record_id, + $root_org + ); + my @matches = grep {$_->href eq $url} @{$uris}; + return scalar @matches; +} + +1; diff --git a/Open-ILS/src/perlmods/lib/OpenILS/WWW/EResourceLinkClick.pm b/Open-ILS/src/perlmods/lib/OpenILS/WWW/EResourceLinkClick.pm new file mode 100644 index 0000000000..808886c118 --- /dev/null +++ b/Open-ILS/src/perlmods/lib/OpenILS/WWW/EResourceLinkClick.pm @@ -0,0 +1,44 @@ +package OpenILS::WWW::EResourceLinkClick; + +use strict; +use warnings; + +use OpenILS::Application::EResourceLinkClick; +use Apache2::Const -compile => qw( + OK HTTP_BAD_REQUEST HTTP_INTERNAL_SERVER_ERROR HTTP_NOT_IMPLEMENTED +); +use CGI; + +sub handler { + my $r = shift; + my $cgi = new CGI; + + my $record_id = $cgi->param('record_id') || ''; + my $url = $cgi->param('url') || ''; + my $referer = $cgi->http('Referer') || ''; + my $user_agent = $cgi->http('User-Agent') || ''; + + my $result = OpenILS::Application::EResourceLinkClick->add_click( + $record_id, + $url, + $referer, + $user_agent + ); + + if( $result eq OpenILS::Application::EResourceLinkClick::BadInput ) { + return Apache2::Const::HTTP_BAD_REQUEST; + } + if( $result eq OpenILS::Application::EResourceLinkClick::InternalError ) { + return Apache2::Const::HTTP_INTERNAL_SERVER_ERROR; + } + if( $result eq OpenILS::Application::EResourceLinkClick::NotConfigured ) { + return Apache2::Const::HTTP_NOT_IMPLEMENTED; + } + if( $result eq OpenILS::Application::EResourceLinkClick::Success ) { + $r->content_type('text/plain'); + $r->print('click recorded'); + return Apache2::Const::OK; + } +} + +1; diff --git a/Open-ILS/src/perlmods/live_t/40-eresource-link-click.t b/Open-ILS/src/perlmods/live_t/40-eresource-link-click.t new file mode 100644 index 0000000000..f3a26c1282 --- /dev/null +++ b/Open-ILS/src/perlmods/live_t/40-eresource-link-click.t @@ -0,0 +1,169 @@ +#!perl + +use strict; use warnings; + +use Test::More tests => 2; +use OpenILS::Utils::TestUtils; +use OpenILS::Utils::CStoreEditor qw/:funcs/; + +diag('Test the EResourceLinkClick module'); + +my $script = OpenILS::Utils::TestUtils->new(); +$script->bootstrap; +our $apputils = "OpenILS::Application::AppUtils"; +my $e = new_editor; +$e->init; + +BEGIN { use_ok('OpenILS::Application::EResourceLinkClick'); } + +subtest('add_click', sub { + plan tests => 2; + + subtest('when the global flag is off', sub { + plan tests => 2; + set_global_flag($e, 'f'); + + my $response = OpenILS::Application::EResourceLinkClick->add_click( + 238, + 'http://example.com/ebookapi/t/001', + 'https://my-evergreen.org/eg/opac/results', + 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:123.0) Gecko/20100101 Firefox/123.0' + ); + + is($response, OpenILS::Application::EResourceLinkClick::NotConfigured, 'says that it is not configured'); + assert_no_clicks_added_to_db($e); + }); + + subtest('when the global flag is on', sub { + plan tests => 5; + set_global_flag($e, 't'); + + subtest('when the referer did not come from the record or results page', sub { + plan tests => 2; + + my $response = OpenILS::Application::EResourceLinkClick->add_click( + 238, + 'http://example.com/ebookapi/t/001', + 'https://some-non-eg-site/bad-path', + 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:123.0) Gecko/20100101 Firefox/123.0' + ); + + is($response, OpenILS::Application::EResourceLinkClick::BadInput, 'says that the input is bad'); + assert_no_clicks_added_to_db($e); + }); + + subtest('when user agent is a bot', sub { + plan tests => 2; + my $response = OpenILS::Application::EResourceLinkClick->add_click( + 238, + 'http://example.com/ebookapi/t/001', + 'https://my-evergreen.org/eg/opac/results', + 'Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)' + ); + + is($response, OpenILS::Application::EResourceLinkClick::BadInput, 'says that the input is bad'); + assert_no_clicks_added_to_db($e); + }); + + subtest('when url does not exist on the record in question', sub { + plan tests => 2; + my $response = OpenILS::Application::EResourceLinkClick->add_click( + 238, + 'http://not-a-real-url/not-actually/on-the-record', + 'https://my-evergreen.org/eg/opac/results', + 'Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)' + ); + + is($response, OpenILS::Application::EResourceLinkClick::BadInput, 'says that the input is bad'); + assert_no_clicks_added_to_db($e); + }); + + subtest('when input is valid', sub { + plan tests => 2; + my $response = OpenILS::Application::EResourceLinkClick->add_click( + 238, + 'http://example.com/ebookapi/t/001', + 'https://my-evergreen.org/eg/opac/results', + 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:123.0) Gecko/20100101 Firefox/123.0' + ); + + is($response, OpenILS::Application::EResourceLinkClick::Success, 'says that it is successful'); + my $rows = $e->search_action_eresource_link_click({record => 238}); + is(scalar(@{ $rows }), 1, 'adds the click to the database'); + + delete_test_rows($e); + }); + + subtest('when bib record is associated with a course', sub { + plan tests => 4; + + my $acmc = Fieldmapper::asset::course_module_course->new; + $acmc->id(12345); + $acmc->name('Introduction to cats'); + $acmc->course_number('CATS101'); + + my $acmcm = Fieldmapper::asset::course_module_course_materials->new; + $acmcm->course(12345); + $acmcm->id(5678); + $acmcm->record(238); + $acmcm->temporary_record(0); + $e->xact_begin; + $e->create_asset_course_module_course( $acmc ); + $e->create_asset_course_module_course_materials( $acmcm ); + $e->xact_commit; + + my $response = OpenILS::Application::EResourceLinkClick->add_click( + 238, + 'http://example.com/ebookapi/t/001', + 'https://my-evergreen.org/eg/opac/results', + 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:123.0) Gecko/20100101 Firefox/123.0' + ); + + is($response, OpenILS::Application::EResourceLinkClick::Success, 'says that it is successful'); + my $rows = $e->search_action_eresource_link_click_course({course => 12345}); + is(scalar(@{ $rows }), 1, 'adds a click course mapping to the database'); + is($rows->[0]->course_name, 'Introduction to cats', 'adds the course name to the mapping'); + is($rows->[0]->course_number, 'CATS101', 'adds the course number to the mapping'); + + delete_test_rows($e); + }); + }); +}); + +# Delete any rows that weren't deleted by the tests +# (e.g. if there was a test failure) +delete_test_rows($e); + +sub set_global_flag { + my ($editor, $value) = @_; + my $flag = $e->retrieve_config_global_flag('opac.eresources.link_click_tracking'); + $flag->enabled($value); + $editor->xact_begin; + $editor->update_config_global_flag($flag); + $editor->xact_commit; +} + +sub assert_no_clicks_added_to_db { + my $editor = shift; + my $rows = $e->search_action_eresource_link_click({record => 238}); + is(scalar(@{ $rows }), 0, 'does not add any clicks to the database'); +} + +sub delete_test_rows { + my $editor = shift; + my $rows = $e->search_action_eresource_link_click({record => 238}); + $editor->xact_begin; + foreach(@{$rows}) { + $editor->delete_action_eresource_link_click($_); + } + $rows = $e->search_asset_course_module_course_materials({course => 12345}); + foreach(@{$rows}) { + $editor->delete_asset_course_module_course_materials($_); + } + $rows = $e->search_asset_course_module_course({id => 12345}); + foreach(@{$rows}) { + $editor->delete_asset_course_module_course($_); + } + $editor->xact_commit; +} + diff --git a/Open-ILS/src/support-scripts/delete_old_eresource_link_clicks.pl b/Open-ILS/src/support-scripts/delete_old_eresource_link_clicks.pl new file mode 100755 index 0000000000..594583b9d6 --- /dev/null +++ b/Open-ILS/src/support-scripts/delete_old_eresource_link_clicks.pl @@ -0,0 +1,47 @@ +#!/usr/bin/perl + +use strict; +use warnings; +use Getopt::Long; +use OpenSRF::System; +use OpenILS::Utils::CStoreEditor; +use Carp; + +my $osrf_config = '/openils/conf/opensrf_core.xml'; +my $days = 365; +my $help; + +my $ops = GetOptions( + 'osrf-config=s' => \$osrf_config, + 'days=i' => \$days, + 'help' => \$help +); + +sub help { + print <<'END_HELP'; + + + Usage: + --osrf-config [/openils/conf/opensrf_core.xml] + + --days <number-of-days> + How many days of clicks to keep. For example, --days 7 + will keep only the most recent week of clicks. The default + is 365 days. + + --help + Show this message. +END_HELP + exit 0; +} + +help() if $help || !$ops; + +OpenSRF::System->bootstrap_client(config_file => $osrf_config); +OpenILS::Utils::CStoreEditor::init(); +my $e = OpenILS::Utils::CStoreEditor->new; +$e->json_query( + {'from' => ['action.delete_old_eresource_link_clicks', $days]} +) or croak('Deletion failed, ' . $e->event); + +1; commit ebc9613ff23a162d585ba21fc50d9bde2898595b Author: Jane Sandberg <sandbergja@gmail.com> Date: Wed Mar 6 09:47:22 2024 -0800 LP1895695: Add tables and global flag for the eresource link click feature Signed-off-by: Jane Sandberg <sandbergja@gmail.com> Signed-off-by: Stephanie Leary <stephanie.leary@equinoxoli.org> diff --git a/Open-ILS/examples/fm_IDL.xml b/Open-ILS/examples/fm_IDL.xml index 8e4f4a3206..56f0d16c2b 100644 --- a/Open-ILS/examples/fm_IDL.xml +++ b/Open-ILS/examples/fm_IDL.xml @@ -15895,6 +15895,42 @@ SELECT usr, </permacrud> </class> + <class id="aelc" + controller="open-ils.cstore" + oils_obj:fieldmapper="action::eresource_link_click" + oils_persist:tablename="action.eresource_link_click" + reporter:label="Eresource link clicks"> + <fields oils_persist:primary="id" oils_persist:sequence="action.eresource_link_click_id_seq"> + <field reporter:label="ID" name="id" reporter:datatype="id" /> + <field reporter:label="Date/time of click" name="clicked_at" reporter:datatype="timestamp" oils_obj:required="true"/> + <field reporter:label="URL" name="url" reporter:datatype="text" oils_obj:required="true"/> + <field reporter:label="Record" name="record" reporter:datatype="link"/> + <field reporter:label="Courses" name="courses" oils_persist:virtual="true" reporter:datatype="link"/> + </fields> + <links> + <link field="record" reltype="has_a" key="id" map="" class="bre"/> + <link field="courses" reltype="has_many" key="click" map="" class="aelcc"/> + </links> + </class> + + <class id="aelcc" + controller="open-ils.cstore" + oils_obj:fieldmapper="action::eresource_link_click_course" + oils_persist:tablename="action.eresource_link_click_course" + reporter:label="Eresource link click-course mapping"> + <fields oils_persist:primary="id" oils_persist:sequence="action.eresource_link_click_course_id_seq"> + <field reporter:label="ID" name="id" reporter:datatype="id" /> + <field reporter:label="Click" name="click" reporter:datatype="link" oils_obj:required="true"/> + <field reporter:label="Course" name="course" reporter:datatype="link"/> + <field reporter:label="Course name" name="course_name" reporter:datatype="text" oils_obj:required="true"/> + <field reporter:label="Course number" name="course_number" reporter:datatype="text" oils_obj:required="true"/> + </fields> + <links> + <link field="click" reltype="has_a" key="id" map="" class="aelc"/> + <link field="course" reltype="has_a" key="id" map="" class="acmc"/> + </links> + </class> + <class id="acqr_inv_totals" controller="open-ils.reporter" oils_persist:readonly="true" reporter:label="Invoice Totals"> <oils_persist:source_definition><![CDATA[ WITH invoice_entry_totals AS ( diff --git a/Open-ILS/src/sql/Pg/090.schema.action.sql b/Open-ILS/src/sql/Pg/090.schema.action.sql index dffddfcfd0..9c35cb73f9 100644 --- a/Open-ILS/src/sql/Pg/090.schema.action.sql +++ b/Open-ILS/src/sql/Pg/090.schema.action.sql @@ -1825,6 +1825,28 @@ CREATE TABLE action.batch_hold_event_map ( hold INT NOT NULL REFERENCES action.hold_request (id) ON UPDATE CASCADE ON DELETE CASCADE ); +CREATE TABLE action.eresource_link_click ( + id BIGSERIAL PRIMARY KEY, + clicked_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + url TEXT, + record BIGINT NOT NULL REFERENCES biblio.record_entry (id) +); + +CREATE TABLE action.eresource_link_click_course ( + id SERIAL PRIMARY KEY, + click BIGINT NOT NULL REFERENCES action.eresource_link_click (id) ON DELETE CASCADE, + course INT NOT NULL, -- no REFERENCES, since the course could have been deleted + course_name TEXT NOT NULL, + course_number TEXT NOT NULL +); + +CREATE FUNCTION action.delete_old_eresource_link_clicks(days integer) + RETURNS VOID AS + 'DELETE FROM action.eresource_link_click + WHERE clicked_at < current_timestamp + - ($1::text || '' days'')::interval' + LANGUAGE SQL + VOLATILE; CREATE TABLE action.ingest_queue ( id SERIAL PRIMARY KEY, 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 beefdd0cd9..c36d191c51 100644 --- a/Open-ILS/src/sql/Pg/950.data.seed-values.sql +++ b/Open-ILS/src/sql/Pg/950.data.seed-values.sql @@ -23674,6 +23674,15 @@ INSERT INTO config.global_flag (name, enabled, label) VALUES ( UPDATE config.global_flag SET value = '20' WHERE name = 'ingest.queued.max_threads'; +INSERT INTO config.global_flag (name, label, enabled) + VALUES ( + 'opac.eresources.link_click_tracking', + oils_i18n_gettext('opac.eresources.link_click_tracking', + 'Track clicks on eresources links. Before enabling this global flag, be sure that you are monitoring disk space on your database server and have a cron job set up to delete click records after the desired retention interval.', + 'cgf', 'label'), + FALSE + ); + INSERT INTO config.org_unit_setting_type (name, label, grp, description, datatype, fm_class) VALUES ( 'circ.custom_penalty_override.PATRON_EXCEEDS_FINES', oils_i18n_gettext('circ.custom_penalty_override.PATRON_EXCEEDS_FINES', diff --git a/Open-ILS/src/sql/Pg/t/action.delete_old_eresource_link_clicks.pg b/Open-ILS/src/sql/Pg/t/action.delete_old_eresource_link_clicks.pg new file mode 100644 index 0000000000..51736b7a07 --- /dev/null +++ b/Open-ILS/src/sql/Pg/t/action.delete_old_eresource_link_clicks.pg @@ -0,0 +1,44 @@ +\set ECHO none +\set QUIET 1 +-- Turn off echo and keep things quiet. + +-- Format the output for nice TAP. +\pset format unaligned +\pset tuples_only true +\pset pager + +-- Revert all changes on failure. +\set ON_ERROR_ROLLBACK 1 +\set ON_ERROR_STOP true +\set QUIET 1 + +-- Load the TAP functions. +BEGIN; + +-- Plan the tests. +SELECT plan(2); + +INSERT INTO action.eresource_link_click (clicked_at, url, record) VALUES + ('yesterday'::TIMESTAMP, 'http://yesterday.example.com', 1), + (now() - INTERVAL '10 DAYS', 'http://ten-days.example.com', 2), + (now() - INTERVAL '8 DAYS', 'http://eight-days.example.com', 2), + (now(), 'http://now.example.com', 3); + +SELECT results_eq( + 'SELECT url FROM action.eresource_link_click', + $$VALUES ('http://yesterday.example.com'), ('http://ten-days.example.com'), ('http://eight-days.example.com'), ('http://now.example.com')$$, + 'all four clicks are included in the table' +); + +SELECT * FROM action.delete_old_eresource_link_clicks(2); + +SELECT results_eq( + 'SELECT url FROM action.eresource_link_click', + $$VALUES ('http://yesterday.example.com'), ('http://now.example.com')$$, + 'only two clicks remain in the table' +); + + +-- Finish the tests and clean up. +SELECT * FROM finish(); +ROLLBACK; diff --git a/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.link_click.sql b/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.link_click.sql new file mode 100644 index 0000000000..5067383601 --- /dev/null +++ b/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.link_click.sql @@ -0,0 +1,38 @@ +BEGIN; + +-- SELECT evergreen.upgrade_deps_block_check('xxxx', :eg_version); + +CREATE TABLE action.eresource_link_click ( + id BIGSERIAL PRIMARY KEY, + clicked_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + url TEXT, + record BIGINT NOT NULL REFERENCES biblio.record_entry (id) +); + +CREATE TABLE action.eresource_link_click_course ( + id SERIAL PRIMARY KEY, + click BIGINT NOT NULL REFERENCES action.eresource_link_click (id) ON DELETE CASCADE, + course INT NOT NULL, -- no REFERENCES, since the course could have been deleted + course_name TEXT NOT NULL, + course_number TEXT NOT NULL +); + +INSERT INTO config.global_flag (name, label, enabled) + VALUES ( + 'opac.eresources.link_click_tracking', + oils_i18n_gettext('opac.eresources.link_click_tracking', + 'Track clicks on eresources links. Before enabling this global flag, be sure that you are monitoring disk space on your database server and have a cron job set up to delete click records after the desired retention interval.', + 'cgf', 'label'), + FALSE + ); + +CREATE FUNCTION action.delete_old_eresource_link_clicks(days integer) + RETURNS VOID AS + 'DELETE FROM action.eresource_link_click + WHERE clicked_at < current_timestamp + - ($1::text || '' days'')::interval' + LANGUAGE SQL + VOLATILE; + +COMMIT; + ----------------------------------------------------------------------- Summary of changes: Open-ILS/examples/apache_24/eg_vhost.conf.in | 8 + Open-ILS/examples/crontab.example | 5 + Open-ILS/examples/fm_IDL.xml | 36 +++++ Open-ILS/src/Makefile.am | 1 + .../src/extras/install/Makefile.debian-bookworm | 3 +- .../src/extras/install/Makefile.debian-bullseye | 3 +- Open-ILS/src/extras/install/Makefile.debian-buster | 3 +- Open-ILS/src/extras/install/Makefile.fedora | 3 +- Open-ILS/src/extras/install/Makefile.ubuntu-jammy | 3 +- Open-ILS/src/extras/install/Makefile.ubuntu-noble | 3 +- .../perlmods/lib/OpenILS/Application/README.adoc | 7 + .../perlmods/lib/OpenILS/WWW/EResourceLinkClick.pm | 54 +++++++ .../lib/OpenILS/WWW/EResourceLinkClick/Click.pm | 99 ++++++++++++ .../src/perlmods/live_t/40-eresource-link-click.t | 169 +++++++++++++++++++++ Open-ILS/src/sql/Pg/002.schema.config.sql | 2 +- Open-ILS/src/sql/Pg/090.schema.action.sql | 22 +++ Open-ILS/src/sql/Pg/950.data.seed-values.sql | 9 ++ .../t/action.delete_old_eresource_link_clicks.pg | 44 ++++++ .../src/sql/Pg/upgrade/1467.schema.link_click.sql | 38 +++++ .../delete_old_eresource_link_clicks.pl | 47 ++++++ .../src/templates-bootstrap/opac/parts/header.tt2 | 2 + Open-ILS/src/templates-bootstrap/opac/parts/js.tt2 | 7 + .../opac/parts/record/summary.tt2 | 2 +- .../opac/parts/result/table.tt2 | 2 +- Open-ILS/src/templates/opac/parts/header.tt2 | 2 + Open-ILS/src/templates/opac/parts/js.tt2 | 7 + .../src/templates/opac/parts/record/summary.tt2 | 2 +- Open-ILS/src/templates/opac/parts/result/table.tt2 | 2 +- .../default/opac/eresource_click_tracker.module.js | 14 ++ .../tests/eresource_click_tracker.spec.module.js | 26 ++++ .../OPAC/eresource_link_click_track.adoc | 67 ++++++++ docs/modules/opac/nav.adoc | 1 + .../opac/pages/eresource_link_click_track.adoc | 67 ++++++++ 33 files changed, 749 insertions(+), 11 deletions(-) create mode 100644 Open-ILS/src/perlmods/lib/OpenILS/Application/README.adoc create mode 100644 Open-ILS/src/perlmods/lib/OpenILS/WWW/EResourceLinkClick.pm create mode 100644 Open-ILS/src/perlmods/lib/OpenILS/WWW/EResourceLinkClick/Click.pm create mode 100644 Open-ILS/src/perlmods/live_t/40-eresource-link-click.t create mode 100644 Open-ILS/src/sql/Pg/t/action.delete_old_eresource_link_clicks.pg create mode 100644 Open-ILS/src/sql/Pg/upgrade/1467.schema.link_click.sql create mode 100755 Open-ILS/src/support-scripts/delete_old_eresource_link_clicks.pl create mode 100644 Open-ILS/web/js/ui/default/opac/eresource_click_tracker.module.js create mode 100644 Open-ILS/web/opac/tests/eresource_click_tracker.spec.module.js create mode 100644 docs/RELEASE_NOTES_NEXT/OPAC/eresource_link_click_track.adoc create mode 100644 docs/modules/opac/pages/eresource_link_click_track.adoc hooks/post-receive -- Evergreen ILS
participants (1)
-
Git User