From 99999c3f682802ca44a32218caa4537dc3f3aec8 Mon Sep 17 00:00:00 2001 From: Dan Scott Date: Wed, 20 Apr 2011 23:57:57 -0400 Subject: [PATCH] Merge social patches from Fall 2010 Nice to have something current to look at, despite all of the work that remains to be done. Signed-off-by: Dan Scott --- Open-ILS/examples/apache/eg_vhost.conf | 11 + Open-ILS/examples/apache/startup.pl | 1 + Open-ILS/examples/fm_IDL.xml | 73 ++ Open-ILS/src/Makefile.am | 1 + .../lib/OpenILS/Application/Storage/CDBI/biblio.pm | 1 - .../lib/OpenILS/Application/Storage/CDBI/social.pm | 45 ++ Open-ILS/src/perlmods/lib/OpenILS/WWW/Social.pm | 793 +++++++++++++++++++++ Open-ILS/src/sql/Pg/060.schema.social.sql | 105 +++ Open-ILS/src/sql/Pg/sql_file_manifest | 1 + Open-ILS/src/templates/social/user/about.tt2 | 103 +++ 10 files changed, 1133 insertions(+), 1 deletion(-) create mode 100644 Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/CDBI/social.pm create mode 100644 Open-ILS/src/perlmods/lib/OpenILS/WWW/Social.pm create mode 100644 Open-ILS/src/sql/Pg/060.schema.social.sql create mode 100644 Open-ILS/src/templates/social/user/about.tt2 diff --git a/Open-ILS/examples/apache/eg_vhost.conf b/Open-ILS/examples/apache/eg_vhost.conf index 7fea97cc3d..1f9437a5c5 100644 --- a/Open-ILS/examples/apache/eg_vhost.conf +++ b/Open-ILS/examples/apache/eg_vhost.conf @@ -263,6 +263,17 @@ RewriteRule . - [E=locale:en-US] # ---------------------------------------------------------------------------------- +# Social intelligence interface +# ---------------------------------------------------------------------------------- + + SetHandler perl-script + PerlHandler OpenILS::WWW::Social + Options +ExecCGI + PerlSendHeader On + allow from all + + +# ---------------------------------------------------------------------------------- # Supercat feeds # ---------------------------------------------------------------------------------- diff --git a/Open-ILS/examples/apache/startup.pl b/Open-ILS/examples/apache/startup.pl index f82844717f..03e1806ac7 100755 --- a/Open-ILS/examples/apache/startup.pl +++ b/Open-ILS/examples/apache/startup.pl @@ -9,6 +9,7 @@ use OpenILS::WWW::TemplateBatchBibUpdate qw( /openils/conf/opensrf_core.xml ); use OpenILS::WWW::EGWeb ('/openils/conf/oils_web.xml'); use OpenILS::WWW::PasswordReset ('/openils/conf/opensrf_core.xml'); use OpenILS::WWW::IDL2js ('/openils/conf/opensrf_core.xml'); +use OpenILS::WWW::Social ('/openils/conf/opensrf_core.xml'); # - Uncoment the following 2 lines to make use of the IP redirection code # - The IP file should to contain a map with the following format: diff --git a/Open-ILS/examples/fm_IDL.xml b/Open-ILS/examples/fm_IDL.xml index 858150e3fb..150b0ece02 100644 --- a/Open-ILS/examples/fm_IDL.xml +++ b/Open-ILS/examples/fm_IDL.xml @@ -4507,6 +4507,79 @@ SELECT usr, + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Open-ILS/src/Makefile.am b/Open-ILS/src/Makefile.am index 7d52ad5b24..2cce0ba465 100644 --- a/Open-ILS/src/Makefile.am +++ b/Open-ILS/src/Makefile.am @@ -170,6 +170,7 @@ ilscore-install: $(MKDIR_P) $(TEMPLATEDIR) cp -r @srcdir@/templates/marc $(TEMPLATEDIR) cp -r @srcdir@/templates/password-reset $(TEMPLATEDIR) + cp -r @srcdir@/templates/social $(TEMPLATEDIR) @echo "Installing string templates to $(TEMPLATEDIR)" $(MKDIR_P) $(TEMPLATEDIR) $(MKDIR_P) $(datadir)/overdue/ diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/CDBI/biblio.pm b/Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/CDBI/biblio.pm index 7780cbcb6a..f9cbd22f70 100644 --- a/Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/CDBI/biblio.pm +++ b/Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/CDBI/biblio.pm @@ -20,7 +20,6 @@ use base qw/biblio/; biblio::record_note->table( 'biblio_record_note' ); biblio::record_note->columns( Essential => qw/id record value creator editor create_date edit_date pub/ ); -#------------------------------------------------------------------------------- #------------------------------------------------------------------------------- package biblio::peer_type; diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/CDBI/social.pm b/Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/CDBI/social.pm new file mode 100644 index 0000000000..7a105f458b --- /dev/null +++ b/Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/CDBI/social.pm @@ -0,0 +1,45 @@ +package OpenILS::Application::Storage::CDBI::social; +our $VERSION = 1; + +#------------------------------------------------------------------------------- +package social; +use base qw/OpenILS::Application::Storage::CDBI/; +#------------------------------------------------------------------------------- +package social::user_rating; +use base qw/social/; + +social::user_rating->table('social_user_rating'); +social::user_rating->columns (Essential => qw/id record value creator + create_date edit_date/); + +#------------------------------------------------------------------------------- +package social::user_review; +use base qw/social/; + +social::user_review->table('social_user_review'); +social::user_review->columns (Essential => qw/id record value creator + approver approved create_date edit_date/); + +#------------------------------------------------------------------------------- +package social::tag; +use base qw/social/; + +social::tag->table('social_tag'); +social::tag->columns (Essential => qw/id value approver approved editor + edit_date/); + +#------------------------------------------------------------------------------- +package social::biblio_tag_map; +use base qw/social/; + +social::biblio_tag_map->table('social_user_tag'); +social::biblio_tag_map->columns (Essential => qw/id record tag creator create_date /); + +#------------------------------------------------------------------------------- +package social::activity_stream; +use base qw/social/; + +social::biblio_tag_map->table('social_activity_stream'); +social::biblio_tag_map->columns (Essential => qw/actor object target stamped activity/); + +1; diff --git a/Open-ILS/src/perlmods/lib/OpenILS/WWW/Social.pm b/Open-ILS/src/perlmods/lib/OpenILS/WWW/Social.pm new file mode 100644 index 0000000000..6e8f6219f1 --- /dev/null +++ b/Open-ILS/src/perlmods/lib/OpenILS/WWW/Social.pm @@ -0,0 +1,793 @@ +package OpenILS::WWW::Social; + +# Copyright (C) 2010 Laurentian University +# Dan Scott +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +use strict; use warnings; + +use Apache2::Log; +use Apache2::Const -compile => qw(OK REDIRECT DECLINED NOT_FOUND :log); +use APR::Const -compile => qw(:error SUCCESS); +use Apache2::RequestRec (); +use Apache2::RequestIO (); +use Apache2::RequestUtil; +use CGI; +use Template; + +use OpenSRF::EX qw(:try); +use OpenSRF::Utils qw/:datetime/; +use OpenSRF::Utils::Cache; +use OpenSRF::System; +use OpenSRF::AppSession; + +use OpenILS::Utils::Fieldmapper; +use OpenSRF::Utils::Logger qw/$logger/; +use OpenILS::Application::AppUtils; +use OpenILS::Utils::CStoreEditor qw/:funcs/; +use OpenILS::Utils::ModsParser; +use XML::LibXML; +use DateTime; + +my $log = 'OpenSRF::Utils::Logger'; +my $U = 'OpenILS::Application::AppUtils'; + +my ($bootstrap, $editor, $actor, $templates, $LocalTZ); +my $i18n = {}; +my $init_done = 0; # has child_init been called? + +# helper functions inserted into the TT environment +my $_TT_helpers = { + + # turns a date into something TT can understand + format_date => sub { + my $date = shift; + $date = DateTime::Format::ISO8601->new->parse_datetime(cleanse_ISO8601($date)); + return sprintf( + "%0.2d:%0.2d:%0.2d %0.2d-%0.2d-%0.4d", + $date->hour, + $date->minute, + $date->second, + $date->day, + $date->month, + $date->year + ); + }, + + # escapes a string for inclusion in an XML document. escapes &, <, and > characters + escape_xml => sub { + my $str = shift; + $str =~ s/&/&/sog; + $str =~ s//>/sog; + return $str; + }, + +}; + +sub import { + my $self = shift; + $bootstrap = shift; +} + +sub child_init { + OpenSRF::System->bootstrap_client( config_file => $bootstrap ); + + my $conf = OpenSRF::Utils::SettingsClient->new(); + my $idl = $conf->config_value("IDL"); + Fieldmapper->import(IDL => $idl); + $templates = $conf->config_value("dirs", "templates"); + OpenILS::Utils::CStoreEditor::init(); + $editor = new_editor(); + load_i18n(); + + # Getting the timezone is slow; do it once + $LocalTZ = DateTime::TimeZone->new( name => 'local' ); + + $init_done = 1; +} + +sub handler { + my $apache = shift; + + child_init() unless $init_done; + + return Apache2::Const::DECLINED if (-e $apache->filename); + + my $ctx = {}; + + $ctx->{'helpers'} = $_TT_helpers; + + $ctx->{'uri'} = $apache->uri; + + # Get our locale from the URL + (my $locale = $apache->path_info) =~ s{^.*?/([a-z]{2}-[A-Z]{2})/.*?$}{$1}; + if (!$locale) { + $locale = 'en-US'; + } + + # If locale exists, use it; otherwise fall back to en-US + if (exists $i18n->{$locale}) { + $ctx->{'i18n'} = $i18n->{$locale}; + } else { + $ctx->{'i18n'} = $i18n->{'en-US'}; + } + + my $tt = Template->new({ + INCLUDE_PATH => $templates + }) || die "$Template::ERROR\n"; + + # So what object are we dealing with here? + if ($apache->uri =~ m{/social(/[a-z]{2}-[A-Z]{2})?/user/}) { + if ($apache->uri =~ m{.atom$}) { + return user_activity_stream($apache); + } else { + return display_user($apache, $tt, $ctx); + } + } elsif ($apache->uri =~ m{/social(/[a-z]{2}-[A-Z]{2})?/thing/}) { + # A "thing" is represented by a biblio.record_entry + # It is not necessarily a book, thus "thing" + if ($apache->uri =~ m{.atom$}) { + return thing_activity_stream($apache); + } else { + return display_thing($apache, $tt, $ctx); + } + } +} + +# XXX Need to build an HTML-friendly template for displaying a thing +# and all of the social activity around it +sub display_thing { + return; +} + +# The user URIs identify the user by ID because usernames and email can change +# Grab the user object from the database while we're at it +sub _get_base_user { + my $apache = shift; + + (my $uid = $apache->uri) =~ s{.*/social(/[a-z]{2}-[A-Z]{2})?/user/(\d+).*?$}{$2}; + + my $user = $editor->retrieve_actor_user($uid); + + return $user; +} + +# Things are identified strictly by their record ID +sub thing_activity_stream { + my ($apache) = @_; + + (my $thing = $apache->uri) =~ s{.*/social(/[a-z]{2}-[A-Z]{2})?/thing/(\d+).*?$}{$2}; + + if (!$thing) { + return Apache2::Const::OK; + } + + $apache->content_type('application/atom+xml'); + + my $stream = create_thing_feed($apache, $thing); + + print $stream->toString(1); + + return Apache2::Const::OK; +} + +# Generate the Activity Stream for a given user +sub user_activity_stream { + my ($apache) = @_; + + my $user = _get_base_user($apache); + if (!$user) { + return Apache2::Const::OK; + } + + $apache->content_type('application/atom+xml'); + + my $stream = create_user_feed($apache, $user); + print $stream->toString(1); + + return Apache2::Const::OK; +} + +# Generate the Activity Stream for a given thing +sub create_thing_feed { + my ($apache, $thing) = @_; + + my ($doc, $feed) = base_feed($apache); + + my $title = $doc->createElementNS(undef, "title"); + $title->appendTextNode('Activity stream for the object "' . get_bib_and_mods($thing)->title . '"'); + $feed->addChild($title); + + create_thing_entries($apache, $doc, $thing); + + return $doc; +} + +# All Activity Stream feeds have a common base +# XXX We're assuming HTTP, not HTTPS here... +sub base_feed { + my ($apache) = @_; + + my $now = DateTime->now(time_zone => $LocalTZ); + + my $host = $apache->hostname; + my $uri = $apache->uri; + my $html_uri = $uri; + $html_uri =~ s/.atom$//; + + my $doc = XML::LibXML->createDocument(); + my $feed = $doc->createElementNS("http://www.w3.org/2005/Atom", "feed"); + $doc->setDocumentElement($feed); + + $feed->setNamespace("http://www.w3.org/2005/Atom"); + $feed->setNamespace("http://activitystrea.ms/spec/1.0/", "activity", 0); + $feed->setNamespace("http://portablecontacts.net/spec/1.0", "poco", 0); + + my $feed_id = $doc->createElementNS(undef, "id"); + $feed_id->appendTextNode(generate_tag($host, $uri, $now)); + $feed->addChild($feed_id); + + my $updated = $doc->createElementNS(undef, "updated"); + $updated->appendTextNode($now->strftime('%FT%TZ')); + $feed->addChild($updated); + + # Link to our HTML representation + my $l_html = $doc->createElementNS(undef, "link"); + $l_html->setAttributeNS(undef, "rel", "alternate"); + $l_html->setAttributeNS(undef, "type", "text/html"); + $l_html->setAttributeNS(undef, "href", "http://$host$html_uri"); + $feed->addChild($l_html); + + # Link to our self + my $l_self = $doc->createElementNS(undef, "link"); + $l_self->setAttributeNS(undef, "rel", "self"); + $l_self->setAttributeNS(undef, "type", "application/atom+xml"); + $l_self->setAttributeNS(undef, "href", "http://$host$uri"); + $feed->addChild($l_self); + + return ($doc, $feed); +} + +# Standardized form of representing the user's full name +sub generate_user_name { + my ($user) = @_; + + my $display_name = $user->first_given_name . ' '; + $display_name .= ($user->second_given_name || '') . ' '; + $display_name .= $user->family_name || ''; + $display_name =~ s/ +/ /g; + + return $display_name; +} + +sub create_user_feed { + my ($apache, $user) = @_; + + my ($doc, $feed) = base_feed($apache); + my $display_name = generate_user_name($user); + + my $title = $doc->createElementNS(undef, "title"); + $title->appendTextNode("Personal activity stream for " . ($display_name || ("person " . $user->id))); + $feed->addChild($title); + + add_author($doc, $user, $feed); + + create_user_entries($apache, $doc, $user, $display_name || $user->usrname); + + return $doc; +} + +sub add_author { + my ($doc, $user, $anchor) = @_; + + my $display_name = generate_user_name($user); + + # Introduce the actor (atom:author) + my $author = $doc->createElementNS(undef, "author"); + + # atom:name link, required in atom:author elements + my $aname = $doc->createElementNS(undef, "name"); + my $aname_done = 0; + + # Get the best name we can; flesh out Portable Contacts while we're at it + if ($display_name) { + my $dname = $doc->createElementNS(undef, "poco:displayName"); + $dname->appendTextNode($display_name); + $anchor->addChild($dname); + + $aname->appendTextNode($display_name); + $author->addChild($aname); + $aname_done = 1; + } + + my $uname = $doc->createElementNS(undef, "poco:preferredUsername"); + if ($user->usrname) { + $uname->appendTextNode($user->usrname); + if (!$aname_done) { + $aname->appendTextNode($user->usrname); + $author->addChild($aname); + $aname_done = 1; + } + } else { + # This is a poor excuse for an author name; oh well + $uname->appendTextNode($user->id); + if (!$aname_done) { + $aname->appendTextNode($user->id); + } + $author->addChild($aname); + } + $anchor->addChild($author); + + if ($user->email) { + my $email = $doc->createElementNS(undef, "email"); + $email->appendTextNode($user->email); + $author->addChild($email); + } + $anchor->addChild($uname); +} + +sub create_user_entries { + my ($apache, $doc, $user, $user_name) = @_; + + my $activities = $editor->search_social_activity_stream( + { actor => $user->id }, + { order_by => { socas => "stamped DESC" }, limit => 10 } + ); + + foreach my $activity (@$activities) { + generate_activity_entries($activity, $apache, $doc, $user, $user_name); + } +} + +sub create_thing_entries { + my ($apache, $doc, $thing) = @_; + + my $activities = $editor->search_social_activity_stream( + { object => $thing }, + { order_by => { socas => "stamped DESC" }, limit => 10 } + ); + + foreach my $activity (@$activities) { + my $user = $editor->retrieve_actor_user($activity->actor); + my $display_name = generate_user_name($user); + generate_activity_entries($activity, $apache, $doc, $user, $display_name || $user->usrname); + } +} + + +sub generate_activity_entries { + my ($activity, $apache, $doc, $user, $user_name) = @_; + + my $entry = $doc->createElementNS(undef, "entry"); + + if ($activity->activity eq "add_bookbag") { + _add_bookbag($activity, $user_name, $apache, $doc, $entry); + } elsif ($activity->activity eq "add_bookbag_item") { + _add_bookbag_item($activity, $user_name, $apache, $doc, $entry); + } elsif ($activity->activity eq "add_review") { + _add_review($activity, $user_name, $apache, $doc, $entry); + } elsif ($activity->activity eq "circ") { + _circ($activity, $user_name, $apache, $doc, $entry); + } + + # Required by Atom Activity spec + my $published = $doc->createElementNS(undef, "published"); + my $date = DateTime::Format::ISO8601->new->parse_datetime(cleanse_ISO8601($activity->stamped)); + $published->appendTextNode($date->strftime('%FT%TZ')); + $entry->addChild($published); + + # Required by Atom spec + my $updated = $doc->createElementNS(undef, "updated"); + $updated->appendTextNode($date->strftime('%FT%TZ')); + $entry->addChild($updated); + + add_author($doc, $user, $entry); + + $doc->documentElement()->addChild($entry); +} + +sub _add_review { + my ($activity, $user_name, $apache, $doc, $entry) = @_; + # verb = post (http://activitystrea.ms/schema/1.0/post) + # object = list (http://activitystrea.ms/schema/1.0/review) + + my $review = $editor->retrieve_social_user_review($activity->object); + my $mods = get_bib_and_mods($activity->target); + + my $title = $doc->createElementNS(undef, "title"); + $title->appendTextNode("$user_name reviewed " . $mods->title); + $entry->addChild($title); + + my $summary = $doc->createElementNS(undef, "summary"); + $summary->appendTextNode("$user_name reviewed " . $mods->title); + $entry->addChild($summary); + + my $content = $doc->createElementNS(undef, "content"); + $content->setAttributeNS(undef, "type", "text/html"); + $content->appendTextNode($review->value); + $entry->addChild($content); + + my $id = $doc->createElementNS(undef, "id"); + $id->appendTextNode(generate_tag($apache->hostname, '/opac/social/create/review/' . $activity->object)); + $entry->addChild($id); + + my $verb = $doc->createElementNS(undef, "activity:verb"); + $verb->appendTextNode("http://activitystrea.ms/schema/1.0/post"); + $entry->addChild($verb); + + my $object = $doc->createElementNS(undef, "activity:object"); + my $object_type = $doc->createElementNS(undef, "activity:object-type"); + $object_type->appendTextNode("http://activitystrea.ms/schema/1.0/review"); + $object->addChild($object_type); + + my $object_id = $doc->createElementNS(undef, "id"); + $object_id->appendTextNode(generate_tag($apache->hostname, '/opac/social/review/' . $activity->object)); + $object->addChild($object_id); + + my $object_content = $doc->createElementNS(undef, "content"); + $object_content->setAttributeNS(undef, "type", "text/html"); + $object_content->appendTextNode($review->value); + $object->addChild($object_content); + + my $object_link = $doc->createElementNS(undef, "link"); + $object_link->setAttributeNS(undef, "rel", "alternate"); + $object_link->setAttributeNS(undef, "type", "text/html"); + $object_link->setAttributeNS(undef, "href", 'http://' . $apache->hostname . '/opac/social/review/' . $activity->object); + $object->addChild($object_link); + + # Must be empty if the review does not have a user-generated title + my $object_title = $doc->createElementNS(undef, "title"); + $object->addChild($object_title); + $entry->addChild($object); + + my $target = $doc->createElementNS(undef, "activity:target"); + my $t_object_type = $doc->createElementNS(undef, "activity:object-type"); + $t_object_type->appendTextNode("http://activitystrea.ms/schema/1.0/list"); + $target->addChild($t_object_type); + + my $t_object_id = $doc->createElementNS(undef, "id"); + $t_object_id->appendTextNode(generate_tag($apache->hostname, '/opac/social/thing/' . $activity->target)); + $target->addChild($t_object_id); + + my $t_object_title = $doc->createElementNS(undef, "title"); + $t_object_title->appendTextNode($mods->title); + $target->addChild($t_object_title); + + $entry->addChild($target); + +} + + +sub _circ { + my ($activity, $user_name, $apache, $doc, $entry) = @_; + # verb = post (http://activitystrea.ms/schema/1.0/post) + # object = list (http://activitystrea.ms/schema/1.0/list) + + my $circ = $editor->retrieve_action_circulation($activity->object); + my $mods = get_bib_and_mods($activity->target); + + my $title = $doc->createElementNS(undef, "title"); + $title->appendTextNode("$user_name returned " . $mods->title); + $entry->addChild($title); + + my $summary = $doc->createElementNS(undef, "summary"); + $summary->appendTextNode("$user_name returned " . $mods->title); + $entry->addChild($summary); + + my $content = $doc->createElementNS(undef, "content"); + $content->appendTextNode("$user_name returned " . $mods->title); + $entry->addChild($content); + + my $id = $doc->createElementNS(undef, "id"); + $id->appendTextNode(generate_tag($apache->hostname, '/opac/social/create/circ/' . $activity->object)); + $entry->addChild($id); + + my $verb = $doc->createElementNS(undef, "activity:verb"); + $verb->appendTextNode("http://activitystrea.ms/schema/1.0/post"); + $entry->addChild($verb); + + my $object = $doc->createElementNS(undef, "activity:object"); + my $object_type = $doc->createElementNS(undef, "activity:object-type"); + $object_type->appendTextNode("http://activitystrea.ms/schema/1.0/list"); + $object->addChild($object_type); + + my $object_id = $doc->createElementNS(undef, "id"); + $object_id->appendTextNode(generate_tag($apache->hostname, '/opac/social/circ/' . $activity->object)); + $object->addChild($object_id); + + my $object_title = $doc->createElementNS(undef, "title"); + $object_title->appendTextNode("Circulation"); + $object->addChild($object_title); + $entry->addChild($object); +} + + +sub _add_bookbag { + my ($activity, $user_name, $apache, $doc, $entry) = @_; + # verb = post (http://activitystrea.ms/schema/1.0/post) + # object = list (http://activitystrea.ms/schema/1.0/list) + + my $bb = $editor->retrieve_container_biblio_record_entry_bucket($activity->object); + + my $title = $doc->createElementNS(undef, "title"); + $title->appendTextNode("$user_name created the bookbag " . $bb->name); + $entry->addChild($title); + + my $summary = $doc->createElementNS(undef, "summary"); + $summary->appendTextNode("$user_name created the bookbag " . $bb->name); + $entry->addChild($summary); + + my $content = $doc->createElementNS(undef, "content"); + $content->appendTextNode("$user_name created the bookbag " . $bb->name); + $entry->addChild($content); + + my $id = $doc->createElementNS(undef, "id"); + $id->appendTextNode(generate_tag($apache->hostname, '/opac/social/create/bookbag/' . $activity->object)); + $entry->addChild($id); + + my $verb = $doc->createElementNS(undef, "activity:verb"); + $verb->appendTextNode("http://activitystrea.ms/schema/1.0/post"); + $entry->addChild($verb); + + my $object = $doc->createElementNS(undef, "activity:object"); + my $object_type = $doc->createElementNS(undef, "activity:object-type"); + $object_type->appendTextNode("http://activitystrea.ms/schema/1.0/list"); + $object->addChild($object_type); + + my $object_id = $doc->createElementNS(undef, "id"); + $object_id->appendTextNode(generate_tag($apache->hostname, '/opac/social/bookbag/' . $activity->object)); + $object->addChild($object_id); + + my $object_title = $doc->createElementNS(undef, "title"); + $object_title->appendTextNode($bb->name); + $object->addChild($object_title); + $entry->addChild($object); +} + +sub _add_bookbag_item { + my ($activity, $user_name, $apache, $doc, $entry) = @_; + # verb = save (http://activitystrea.ms/schema/1.0/save) + # object = article (ugh) (http://activitystrea.ms/schema/1.0/article) + # target = list (http://activitystrea.ms/schema/1.0/list) + + my $bb = $editor->retrieve_container_biblio_record_entry_bucket($activity->target); + my $mods = get_bib_and_mods($activity->object); + + my $id = $doc->createElementNS(undef, "id"); + $id->appendTextNode(generate_tag($apache->hostname, '/opac/social/save/bookbag_item/' . $activity->target . '/' . $activity->object)); + $entry->addChild($id); + + my $title = $doc->createElementNS(undef, "title"); + $title->appendTextNode("$user_name added " . $mods->title . " to bookbag " . $bb->name); + $entry->addChild($title); + + my $summary = $doc->createElementNS(undef, "summary"); + $summary->appendTextNode("$user_name added " . $mods->title . " to bookbag " . $bb->name); + $entry->addChild($summary); + + my $content = $doc->createElementNS(undef, "content"); + $content->appendTextNode("$user_name added " . $mods->title . " to bookbag " . $bb->name); + $entry->addChild($content); + + my $verb = $doc->createElementNS(undef, "activity:verb"); + $verb->appendTextNode("http://activitystrea.ms/schema/1.0/save"); + $entry->addChild($verb); + + my $object = $doc->createElementNS(undef, "activity:object"); + my $object_type = $doc->createElementNS(undef, "activity:object-type"); + $object_type->appendTextNode("http://activitystrea.ms/schema/1.0/article"); + $object->addChild($object_type); + + my $object_id = $doc->createElementNS(undef, "id"); + $object_id->appendTextNode(generate_tag($apache->hostname, '/opac/social/bookbag/' . $activity->object)); + $object->addChild($object_id); + + my $object_title = $doc->createElementNS(undef, "title"); + $object_title->appendTextNode($mods->title); + $object->addChild($object_title); + + $entry->addChild($object); + + my $target = $doc->createElementNS(undef, "activity:target"); + my $t_object_type = $doc->createElementNS(undef, "activity:object-type"); + $t_object_type->appendTextNode("http://activitystrea.ms/schema/1.0/list"); + $target->addChild($t_object_type); + + my $t_object_id = $doc->createElementNS(undef, "id"); + $t_object_id->appendTextNode(generate_tag($apache->hostname, '/opac/social/thing/' . $activity->target)); + $target->addChild($t_object_id); + + my $t_object_title = $doc->createElementNS(undef, "title"); + $t_object_title->appendTextNode($bb->name); + $target->addChild($t_object_title); + + $entry->addChild($target); +} + + +# Generate a unique tag for this feed or feed entry +sub generate_tag { + my ($hostname, $uri, $now) = @_; + + if (!$now) { + $now = DateTime->now(time_zone => $LocalTZ); + } + + $hostname =~ s{^.*?([^\.]+\.[^\.]{2,4})/?$}{$1}; + $uri =~ s{#}{/}g; + my $date = $now->strftime('%F'); + return "tag:$hostname,$date:$uri"; +} + +=pod + +Need to build a $ctx structure for the user info like so: + $ctx->{'user'}->{'avatar'} + $ctx->{'user'}->{'email'} + $ctx->{'user'}->{'name'} + $ctx->{'user'}->{'usrname'} + $ctx->{'home_library'}->{'name'} + $ctx->{'circ_history'}->[ {'isbn', 'record_id', 'title', 'author', 'xact_finish' }, ] +=cut +sub display_user { + my ($apache, $tt, $ctx) = @_; + + my $user = _get_base_user($apache); + if (!$user) { + $tt->process('social/user/about.tt2', $ctx) || die $tt->error(); + return Apache2::Const::OK; + } + + # Check user settings here to prevent disclosing information they + # do not want to disclose + + $ctx->{'user'}{'usrname'} = $user->usrname; + $ctx->{'user'}{'email'} = $user->email || ''; + $ctx->{'user'}{'name'} = generate_user_name($user); + + my $home_ou = $editor->retrieve_actor_org_unit($user->home_ou); + $ctx->{'home_library'}{'name'} = $home_ou->name; + + # Now get circs - expose most recent 10 circs by default? + my $circs = $editor->search_action_circulation([ + { usr => $user->id, xact_finish => { '!=' => undef } }, + { order_by => { circ => 'xact_start DESC' }, limit => 10 } + ]); + + my @circ_history; + foreach my $circ (@$circs) { + my $title = $U->simple_scalar_request( + "open-ils.storage", + "open-ils.storage.fleshed.biblio.record_entry.retrieve_by_copy", + $circ->target_copy + ); + + next unless $title; + + my $mods = get_mods($title); + + push @circ_history, { + record_id => $title->id, + isbn => $mods->isbn, + title => $mods->title, + author => $mods->author, + xact_finish => $circ->xact_finish + }; + } + + if (@circ_history) { + $ctx->{'circ_history'} = \@circ_history; + } + + # Now expose public bookbags; set a reasonable limit + my @bookbags; + my $bbs = $editor->search_container_biblio_record_entry_bucket([ + { owner => $user->id, pub => 't', btype => 'bookbag' }, + { order_by => { cbreb => 'name ASC' }, limit => 100 } + ]); + + foreach my $bb (@$bbs) { + my $bbitems = $editor->search_container_biblio_record_entry_bucket_item([ + { bucket => $bb->id } + ]); + + push @bookbags, { id => $bb->id, name => $bb->name, count => (scalar @$bbitems) }; + + }; + + if (@bookbags) { + $ctx->{'bookbags'} = \@bookbags; + } +# my $bbitems = $editor->search_container_biblio_record_entry_bucket_item([ +# { bucket => $bb->id }, +# { order_by => { cbrebi => 'id ASC' }, limit => 100 } +# ]); +# +# foreach my $bbitem (@$bbitems) { +# my $title = $editor->retrieve_biblio_record_entry($bbitem->target_biblio_record_entry); +# get_mini_records($title, $bookbags); +# } + + # Now expose published reviews; set a reasonable limit + my @reviews; + my $revs = $editor->search_social_user_review([ + { creator => $user->id, approved => 't' }, + { order_by => { socr => 'create_date DESC' }, limit => 100 } + ]); + + foreach my $review (@$revs) { + my $mods = get_bib_and_mods($review->record); + + push @reviews, { + record_id => $review->record, + isbn => $mods->isbn, + title => $mods->title, + author => $mods->author, + review => $review->value + }; + }; + + if (@reviews) { + $ctx->{'reviews'} = \@reviews; + } + + $apache->content_type('text/html'); + + $tt->process('social/user/about.tt2', $ctx) || die $tt->error(); + return Apache2::Const::OK; +} + +sub get_bib_and_mods { + my ($bib_id) = @_; + + my $bre = $editor->retrieve_biblio_record_entry($bib_id); + + my $mods = get_mods($bre); +} + +sub get_mods { + my ($bre) = @_; + + my $u = OpenILS::Utils::ModsParser->new(); + $u->start_mods_batch($bre->marc()); + my $mods = $u->finish_mods_batch(); + $mods->doc_id($bre->id) if $mods; + my $isbn = $mods->isbn || ''; + $isbn =~ s/-//g; + $isbn =~ s/(\d+).*?$/$1/g; + $mods->isbn($isbn); + + return $mods; +} + +# Load our localized strings - lame, need to convert to Locale::Maketext +sub load_i18n { + foreach my $string_bundle (glob("$templates/password-reset/strings.*")) { + open(I18NFH, '<', $string_bundle); + (my $locale = $string_bundle) =~ s/^.*\.([a-z]{2}-[A-Z]{2})$/$1/; + $logger->debug("Loaded locale [$locale] from file: [$string_bundle]"); + while() { + my ($string_id, $string) = ($_ =~ m/^(.+?)=(.*?)$/); + $i18n->{$locale}{$string_id} = $string; + } + close(I18NFH); + } +} + +1; + +# vim: et:ts=4:sw=4 diff --git a/Open-ILS/src/sql/Pg/060.schema.social.sql b/Open-ILS/src/sql/Pg/060.schema.social.sql new file mode 100644 index 0000000000..7e8a29a7a3 --- /dev/null +++ b/Open-ILS/src/sql/Pg/060.schema.social.sql @@ -0,0 +1,105 @@ +/* + * Copyright (C) 2010 Laurentian University + * Dan Scott + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + */ + +DROP SCHEMA IF EXISTS social CASCADE; + +BEGIN; + +CREATE SCHEMA social; + +CREATE TABLE social.user_rating ( + id BIGSERIAL PRIMARY KEY, + record BIGINT NOT NULL REFERENCES biblio.record_entry (id), + value INT NOT NULL, + creator INT NOT NULL REFERENCES actor.usr (id), + create_date TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), + edit_date TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now() +); +CREATE INDEX social_user_rating_record_idx ON social.user_rating( record ); +CREATE INDEX social_user_rating_creator_idx ON social.user_rating( creator ); + +CREATE TABLE social.user_review ( + id BIGSERIAL PRIMARY KEY, + record BIGINT NOT NULL REFERENCES biblio.record_entry (id), + value TEXT NOT NULL, + creator INT NOT NULL REFERENCES actor.usr (id), + editor INT REFERENCES actor.usr (id), + approver INT REFERENCES actor.usr (id), + approved BOOL NOT NULL DEFAULT FALSE, + create_date TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), + edit_date TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now() +); +CREATE INDEX social_user_review_approved_idx ON social.user_review( approved ); +CREATE INDEX social_user_review_record_idx ON social.user_review( record ); +CREATE INDEX social_user_review_creator_idx ON social.user_review( creator ); + +CREATE TABLE social.tag ( + id BIGSERIAL PRIMARY KEY, + value TEXT NOT NULL, + approver INT REFERENCES actor.usr (id), + approved BOOL NOT NULL DEFAULT FALSE, + editor INT REFERENCES actor.usr (id), + edit_date TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now() +); +CREATE INDEX social_tag_approved_idx ON social.tag( approved ); + +CREATE TABLE social.biblio_tag_map ( + id BIGSERIAL PRIMARY KEY, + record BIGINT NOT NULL REFERENCES biblio.record_entry (id), + tag BIGINT NOT NULL REFERENCES social.tag (id), + creator INT NOT NULL REFERENCES actor.usr (id), + create_date TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now() +); +CREATE INDEX social_biblio_tag_map_tag_record_idx ON social.biblio_tag_map( record ); +CREATE INDEX social_biblio_tag_map_tag_creator_idx ON social.biblio_tag_map( creator ); + +CREATE OR REPLACE VIEW social.activity_stream AS +SELECT actor, object, target, stamped, activity FROM ( + ( + SELECT acirc.usr AS "actor", acirc.id AS "object", acn.record AS "target", acirc.xact_finish AS "stamped", 'circ' AS "activity" + FROM action.circulation acirc + INNER JOIN asset.copy ac ON ac.id = acirc.target_copy + INNER JOIN asset.call_number acn ON acn.id = ac.call_number + WHERE xact_finish IS NOT NULL + ) + UNION + ( + SELECT creator AS "actor", id AS "object", record AS "target", create_date AS "stamped", 'add_review' AS activity + FROM social.user_review + WHERE approved IS NOT NULL + ) + UNION + ( + SELECT creator AS "actor", id AS "object", record AS "target", edit_date AS "stamped", 'add_rating' AS activity + FROM social.user_rating + ) + UNION + ( + SELECT owner AS "actor", id AS "object", NULL AS "target", create_time, 'add_bookbag' AS "activity" + FROM container.biblio_record_entry_bucket + WHERE pub IS TRUE + ) + UNION + ( + SELECT cbreb.owner AS "actor", cbrebi.target_biblio_record_entry AS "object", cbrebi.bucket AS "target", cbrebi.create_time, 'add_bookbag_item' AS "activity" + FROM container.biblio_record_entry_bucket_item cbrebi + INNER JOIN container.biblio_record_entry_bucket cbreb ON cbrebi.bucket = cbreb.id + WHERE cbreb.pub IS TRUE + ) +) AS activities +ORDER BY 4 DESC; + +COMMIT; diff --git a/Open-ILS/src/sql/Pg/sql_file_manifest b/Open-ILS/src/sql/Pg/sql_file_manifest index 70d7dcda96..8b0d4f48f9 100644 --- a/Open-ILS/src/sql/Pg/sql_file_manifest +++ b/Open-ILS/src/sql/Pg/sql_file_manifest @@ -23,6 +23,7 @@ FTS_CONFIG_FILE 020.schema.functions.sql 030.schema.metabib.sql 040.schema.asset.sql +060.schema.social.sql 070.schema.container.sql 080.schema.money.sql 090.schema.action.sql diff --git a/Open-ILS/src/templates/social/user/about.tt2 b/Open-ILS/src/templates/social/user/about.tt2 new file mode 100644 index 0000000000..cf0772767e --- /dev/null +++ b/Open-ILS/src/templates/social/user/about.tt2 @@ -0,0 +1,103 @@ +[%- USE date -%] + + + +[% UNLESS user.usrname %] + User not found or not public + + +

User not found or not public

+ + +[% ELSE %] + About [% helpers.escape_xml(user.usrname) %] + + +

About [% helpers.escape_xml(user.usrname) %]

+

Personal

+ + [% IF user.avatar %] + + [% END %] + [% IF user.name %] + + [% END %] + [% IF user.email %] + + [% END %] + [% IF home_library %] + + [% END %] +
Name[% helpers.escape_xml(user.name) %]
Email[% helpers.escape_xml(user.email) %]
Home library[% helpers.escape_xml(home_library.name) %]
+

Circulation history

+ [% IF circ_history %] + + + + + + [% FOREACH circ = circ_history %] + + + + + + [% END %] + +
CoverTitleAuthorDate returned
[% helpers.escape_xml(circ.title) %][% helpers.escape_xml(circ.author) %][% date.format(helpers.format_date(circ.xact_finish), '%Y-%m-%d') %]
+ [% ELSE %] +
No circulation history available for this user
+ [% END %] +

Lists

+ [% IF bookbags %] + + [% ELSE %] +
This user has shared no lists
+ [% END %] +

Reviews written by this user

+ [% IF reviews %] + + + + + + [% FOREACH review = reviews %] + + + + + + [% END %] + +
CoverTitleAuthorReview
[% review.title %][% helpers.escape_xml(review.author) %][% helpers.escape_xml(review.review) %]
+ [% ELSE %] +
This user has not written any approved reviews
+ [% END %] + + +[% END %] + -- 2.11.0