diff --git a/Dockerfile b/Dockerfile index fcb98918ea..8e677956ac 100644 --- a/Dockerfile +++ b/Dockerfile @@ -109,7 +109,6 @@ RUN apt-get update \ libgd-perl \ libhtml-scrubber-perl \ libhtml-template-perl \ - libhttp-async-perl \ libio-compress-perl \ libiterator-perl \ libiterator-util-perl \ @@ -133,7 +132,6 @@ RUN apt-get update \ libnet-ip-perl \ libnet-ldap-perl \ libnet-oauth-perl \ - libossp-uuid-perl \ libpadwalker-perl \ libpandoc-wrapper-perl \ libpath-class-perl \ @@ -231,18 +229,18 @@ COPY --from=base /opt/base/pg $APP_ROOT/pg RUN echo "PATH=$PATH:$APP_ROOT/webwork2/bin" >> /root/.bashrc \ && mkdir /run/webwork2 /etc/ssl/local \ && cd $APP_ROOT/webwork2/ \ - && chown www-data DATA ../courses logs tmp /etc/ssl/local /run/webwork2 \ - && chmod -R u+w DATA ../courses logs tmp /run/webwork2 /etc/ssl/local \ + && chown www-data DATA ../courses logs tmp /etc/ssl/local /run/webwork2 \ + && chmod -R u+w DATA ../courses logs tmp /run/webwork2 /etc/ssl/local \ && echo "en_US ISO-8859-1\nen_US.UTF-8 UTF-8" > /etc/locale.gen \ - && /usr/sbin/locale-gen \ - && echo "locales locales/default_environment_locale select en_US.UTF-8\ndebconf debconf/frontend select Noninteractive" > /tmp/preseed.txt \ - && debconf-set-selections /tmp/preseed.txt \ + && /usr/sbin/locale-gen \ + && echo "locales locales/default_environment_locale select en_US.UTF-8\ndebconf debconf/frontend select Noninteractive" > /tmp/preseed.txt \ + && debconf-set-selections /tmp/preseed.txt \ && rm -f /etc/localtime /etc/timezone && echo "Etc/UTC" > /etc/timezone \ - && dpkg-reconfigure -f noninteractive tzdata \ + && dpkg-reconfigure -f noninteractive tzdata \ && cd $WEBWORK_ROOT/htdocs \ - && npm install \ + && npm install \ && cd $PG_ROOT/htdocs \ - && npm install + && npm install # ================================================================== # Phase 7 - Final setup and prepare docker-entrypoint.sh diff --git a/DockerfileStage1 b/DockerfileStage1 index d6ab207151..609996fe8b 100644 --- a/DockerfileStage1 +++ b/DockerfileStage1 @@ -71,7 +71,6 @@ RUN apt-get update \ libgd-perl \ libhtml-scrubber-perl \ libhtml-template-perl \ - libhttp-async-perl \ libio-compress-perl \ libiterator-perl \ libiterator-util-perl \ @@ -94,7 +93,6 @@ RUN apt-get update \ libnet-ip-perl \ libnet-ldap-perl \ libnet-oauth-perl \ - libossp-uuid-perl \ libpadwalker-perl \ libpandoc-wrapper-perl \ libpath-class-perl \ diff --git a/bin/check_modules.pl b/bin/check_modules.pl index 0ea9acf800..e2f4ba2112 100755 --- a/bin/check_modules.pl +++ b/bin/check_modules.pl @@ -129,12 +129,6 @@ =head1 DESCRIPTION ubuntu => 'libdata-structure-util-perl' } }, - 'Data::UUID' => { - package => { - ubuntu => 'libossp-uuid-perl', - rhel => 'perl-Data-UUID' - } - }, 'Date::Format' => { package => { ubuntu => 'libtimedate-perl', @@ -283,11 +277,6 @@ =head1 DESCRIPTION rhel => 'perl-HTML-Parser' } }, - 'HTTP::Async' => { - package => { - ubuntu => 'libhttp-async-perl' - } - }, 'IO::File' => { package => { ubuntu => 'perl-base', diff --git a/conf/localOverrides.conf.dist b/conf/localOverrides.conf.dist index 8c72492d7c..af2a7ab977 100644 --- a/conf/localOverrides.conf.dist +++ b/conf/localOverrides.conf.dist @@ -738,28 +738,35 @@ $mail{feedbackRecipients} = [ # Webwork Caliper ################################################################################ -# enable/disable Caliper for install -#$caliper{enabled} = 0; -# base_url should ideally be hard coded to a persistent url pointing to the webwork root -# (important to keep it consistent over time) -#$caliper{base_url} = 'https://webwork.elearning.ubc.ca/webwork2/'; -# LRS endpoint -#$caliper{host} = 'http://caliper.example.host.org/api/endpoint'; +# Set this to 1 to enable Caliper. Note that the +# Mojolicious::WeBWorK::Plugin::Caliper plugin must also be enabled in +# conf/webwork2.mojolicous.yml in order for this to work, and all of the +# $caliper settings below must be uncommented and set except the +# $caliper{custom_actor_generator}. Note that this and all of the settings below +# can also be configured per course in the course.conf files. +$caliper{enabled} = 0; + +# LRS endpoint to which the Caliper telemetry data will be sent. +#$caliper{host} = 'https://caliper.example.host.org/api/endpoint'; + # LRS endpoint Bearer API key #$caliper{api_key} = '1234567890abcdefg'; -# log file for caliper errors -#$caliper{errorlog} = $webworkDirs{logs} . "/caliper_errors.log"; -# customized Caliper actor. Useful if persistent identifiers for students are available in WebWork -# $caliper{custom_actor_generator} = sub { -# my ($ce, $db, $user) = @_; -# # set caliper id as needed -# my $caliper_id = 'http://www.ubc.ca/' . $user->user_id(); -# return { -# 'id' => $caliper_id, -# 'type' => 'Person', -# 'name' => $user->first_name() . " " . $user->last_name(), -# }; -# }; + +# Log file for caliper errors +#$caliper{errorlog} = "$webworkDirs{logs}/caliper_errors.log"; + +# Custom Caliper actor generator. This is useful if persistent identifiers for +# students are available in WebWork. +#$caliper{custom_actor_generator} = sub { +# my ($ce, $db, $user) = @_; +# # set caliper id as needed +# my $caliper_id = 'http://yourschool.edu/' . $user->user_id; +# return { +# id => $caliper_id, +# type => 'Person', +# name => $user->first_name . ' ' . $user->last_name, +# }; +#}; ############################################################################### # Test settings diff --git a/conf/webwork2.mojolicious.dist.yml b/conf/webwork2.mojolicious.dist.yml index c96103c411..7fabe81fc9 100644 --- a/conf/webwork2.mojolicious.dist.yml +++ b/conf/webwork2.mojolicious.dist.yml @@ -259,3 +259,8 @@ hardcopy: # external websites with usernames and passwords embedded in them such as for # PreTeXt textbooks. allow_unsecured_rpc: 0 + +plugins: + # Uncomment the following to enable Caliper. Also set the Caliper settings in + # localOverrides.conf (or per course in course.conf files). + #- Mojolicious::WeBWorK::Plugin::Caliper: {} diff --git a/lib/Caliper/Actor.pm b/lib/Caliper/Actor.pm index db00f75231..de03c90e36 100644 --- a/lib/Caliper/Actor.pm +++ b/lib/Caliper/Actor.pm @@ -1,46 +1,25 @@ package Caliper::Actor; - -##### Library Imports ##### -use strict; -use warnings; -use WeBWorK::CourseEnvironment; -use WeBWorK::DB; -use Data::Dumper; +use Mojo::Base -signatures; use Caliper::ResourceIri; -sub generate_anonymous_actor { +sub generate_default_actor ($c, $user) { return { - 'id' => 'http://purl.imsglobal.org/caliper/Person', - 'type' => 'Person', + id => Caliper::ResourceIri->new($c->ce)->actor_homepage($user->user_id), + type => 'Person', + name => $user->first_name . ' ' . $user->last_name }; } -sub generate_default_actor { - my ($ce, $db, $user) = @_; - my $resource_iri = Caliper::ResourceIri->new($ce); - - return { - 'id' => $resource_iri->actor_homepage($user->user_id()), - 'type' => 'Person', - 'name' => $user->first_name() . " " . $user->last_name(), - }; -} +sub generate_actor ($c, $user_id) { + return { id => 'http://purl.imsglobal.org/caliper/Person', type => 'Person' } unless defined $user_id; -sub generate_actor { - my ($ce, $db, $user_id) = @_; + my $user = $c->db->getUser($user_id); - if (!defined($user_id)) { - return Caliper::Entity::generate_anonymous_actor(); - } else { - my $user = $db->getUser($user_id); + return $c->ce->{caliper}{custom_actor_generator}($c->ce, $c->db, $user) + if ref $c->ce->{caliper}{custom_actor_generator} eq 'CODE'; - if (defined($ce->{caliper}{custom_actor_generator})) { - return $ce->{caliper}{custom_actor_generator}($ce, $db, $user); - } else { - return generate_default_actor($ce, $db, $user); - } - } + return generate_default_actor($c, $user); } 1; diff --git a/lib/Caliper/Entity.pm b/lib/Caliper/Entity.pm index 182fd8cf59..5e57f0814e 100644 --- a/lib/Caliper/Entity.pm +++ b/lib/Caliper/Entity.pm @@ -1,401 +1,332 @@ package Caliper::Entity; - -##### Library Imports ##### -use strict; -use warnings; +use Mojo::Base -signatures; use Digest::SHA qw(sha1_base64); -use WeBWorK::CourseEnvironment; -use WeBWorK::DB; use WeBWorK::Utils::Tags; use WeBWorK::Utils::Sets qw(grade_set grade_gateway); use Caliper::ResourceIri; use Caliper::Sensor; use Caliper::Actor; -sub webwork_app { - my ($ce, $db) = @_; - my $resource_iri = Caliper::ResourceIri->new($ce); - - # $ce doesn't have WW_VERSION when doing login/logout for some reason - my $seed_ce = WeBWorK::CourseEnvironment->new; - my $ww_version = $seed_ce->{WW_VERSION} || "unknown"; - +sub webwork_app ($c) { return { - 'id' => $resource_iri->webwork(), - 'type' => 'SoftwareApplication', - 'name' => 'WeBWorK', - 'version' => $ww_version, + id => Caliper::ResourceIri->new($c->ce)->webwork, + type => 'SoftwareApplication', + name => 'WeBWorK', + version => $c->ce->{WW_VERSION} }; } -sub session { - my ($ce, $db, $actor, $session_key) = @_; - my $resource_iri = Caliper::ResourceIri->new($ce); +sub session ($c, $actor, $session_key) { my $session_key_hash = sha1_base64($session_key); - return { - 'id' => $resource_iri->user_session($session_key_hash), - 'type' => 'Session', - 'user' => $actor, - 'client' => Caliper::Entity::client($ce, $db, $session_key_hash), + id => Caliper::ResourceIri->new($c->ce)->user_session($session_key_hash), + type => 'Session', + user => $actor, + client => Caliper::Entity::client($c, $session_key_hash) }; } -sub client { - my ($ce, $db, $session_key_hash) = @_; - my $resource_iri = Caliper::ResourceIri->new($ce); - - my $ip_address = ''; - if ($ENV{HTTP_X_FORWARDED_FOR}) { - $ip_address = $ENV{HTTP_X_FORWARDED_FOR}; - } elsif ($ENV{REMOTE_ADDR}) { - $ip_address = $ENV{REMOTE_ADDR}; - } elsif ($ENV{HTTP_CLIENT_IP}) { - $ip_address = $ENV{HTTP_CLIENT_IP}; - } - +sub client ($c, $session_key_hash) { return { - 'id' => $resource_iri->user_client($session_key_hash), - 'type' => 'SoftwareApplication', - 'userAgent' => $ENV{HTTP_USER_AGENT}, - 'ipAddress' => $ip_address, - 'host' => $ENV{HTTP_HOST}, + id => Caliper::ResourceIri->new($c->ce)->user_client($session_key_hash), + type => 'SoftwareApplication', + userAgent => $c->req->headers->user_agent, + ipAddress => $c->tx->remote_address, + host => $c->req->url->to_abs->host }; } -sub membership { - my ($ce, $db, $actor, $user_id) = @_; - my $resource_iri = Caliper::ResourceIri->new($ce); +sub membership ($c, $actor, $user_id) { + my $resource_iri = Caliper::ResourceIri->new($c->ce); - my $user = $db->getUser($user_id); - my $permission = $db->getPermissionLevel($user_id); + my $user = $c->db->getUser($user_id); + my $permission = $c->db->getPermissionLevel($user_id); - my $roles = []; - my $status = ''; + my $roles = []; - if ($user->status() ne 'D') { - $status = 'Active'; - } else { - $status = 'Inactive'; - } - - if ($permission->permission() == $ce->{userRoles}{admin}) { + if ($permission->permission == $c->ce->{userRoles}{admin}) { push @$roles, 'Administrator'; - } elsif ($permission->permission() == $ce->{userRoles}{professor}) { + } elsif ($permission->permission == $c->ce->{userRoles}{professor}) { push @$roles, 'Instructor'; - } elsif ($permission->permission() == $ce->{userRoles}{ta}) { + } elsif ($permission->permission == $c->ce->{userRoles}{ta}) { push @$roles, 'Instructor'; push @$roles, 'Instructor#TeachingAssistant'; - } elsif ($permission->permission() == $ce->{userRoles}{grade_proctor}) { + } elsif ($permission->permission == $c->ce->{userRoles}{grade_proctor}) { push @$roles, 'Instructor'; push @$roles, 'Instructor#Grader'; - } elsif ($permission->permission() == $ce->{userRoles}{login_proctor}) { + } elsif ($permission->permission == $c->ce->{userRoles}{login_proctor}) { push @$roles, 'Instructor'; push @$roles, 'Instructor#GuestInstructor'; - } elsif ($permission->permission() == $ce->{userRoles}{student}) { + } elsif ($permission->permission == $c->ce->{userRoles}{student}) { push @$roles, 'Learner'; } # guest and nobody aren't tracked return { - 'id' => $resource_iri->user_membership($user_id), - 'type' => 'Membership', - 'member' => $actor, - 'organization' => $resource_iri->course(), - 'roles' => $roles, - 'status' => $status, + id => $resource_iri->user_membership($user_id), + type => 'Membership', + member => $actor, + organization => $resource_iri->course, + roles => $roles, + status => $user->status ne 'D' ? 'Active' : 'Inactive' }; } -sub course { - my ($ce, $db) = @_; - my $resource_iri = Caliper::ResourceIri->new($ce); - +sub course ($c) { my $course_entity = { - 'id' => $resource_iri->course(), - 'type' => 'CourseOffering', + id => Caliper::ResourceIri->new($c->ce)->course, + type => 'CourseOffering' }; - - if ($db->settingExists('courseTitle')) { - $course_entity->{'name'} = $db->getSettingValue('courseTitle'); - } - + $course_entity->{name} = $c->db->getSettingValue('courseTitle') if $c->db->settingExists('courseTitle'); return $course_entity; } -sub problem_set { - my ($ce, $db, $set_id) = @_; - my $resource_iri = Caliper::ResourceIri->new($ce); +sub problem_set ($c, $set_id) { + my $resource_iri = Caliper::ResourceIri->new($c->ce); - my $problem_set = $db->getGlobalSet($set_id); + my $problem_set = $c->db->getGlobalSet($set_id); my $items = []; - my @problem_ids = $db->listGlobalProblems($set_id); + my @problem_ids = $c->db->listGlobalProblems($set_id); for my $problem_id (@problem_ids) { push( @$items, { - 'id' => $resource_iri->problem($set_id, $problem_id), - 'type' => 'AssessmentItem', + id => $resource_iri->problem($set_id, $problem_id), + type => 'AssessmentItem' } ); } my $problem_set_entity = { - 'id' => $resource_iri->problem_set($set_id), - 'type' => 'Assessment', - 'isPartOf' => Caliper::Entity::course($ce, $db), - 'name' => $set_id, - 'items' => $items, - 'dateToStartOn' => Caliper::Sensor::formatted_timestamp($problem_set->open_date()), - 'dateToSubmit' => Caliper::Sensor::formatted_timestamp($problem_set->due_date()), - 'extensions' => { - 'answer_date' => $problem_set->answer_date(), - 'reduced_scoring_date' => $problem_set->reduced_scoring_date(), - 'visible' => $problem_set->visible(), - 'enable_reduced_scoring' => $problem_set->enable_reduced_scoring(), - 'description' => $problem_set->description(), - 'restricted_release' => $problem_set->restricted_release(), - 'restricted_status' => $problem_set->restricted_status(), - 'attempts_per_version' => $problem_set->attempts_per_version(), - 'time_interval' => $problem_set->time_interval(), - 'versions_per_interval' => $problem_set->versions_per_interval(), - 'version_time_limit' => $problem_set->version_time_limit(), - 'version_creation_time' => $problem_set->version_creation_time(), - 'problem_randorder' => $problem_set->problem_randorder(), - 'version_last_attempt_time' => $problem_set->version_last_attempt_time(), - 'problems_per_page' => $problem_set->problems_per_page(), - 'hide_score' => $problem_set->hide_score(), - 'hide_score_by_problem' => $problem_set->hide_score_by_problem(), - 'hide_work' => $problem_set->hide_work(), - 'time_limit_cap' => $problem_set->time_limit_cap(), - 'restrict_ip' => $problem_set->restrict_ip(), - 'relax_restrict_ip' => $problem_set->relax_restrict_ip(), - 'restricted_login_proctor' => $problem_set->restricted_login_proctor(), - 'hide_hint' => $problem_set->hide_hint(), - 'restrict_prob_progression' => $problem_set->restrict_prob_progression(), + id => $resource_iri->problem_set($set_id), + type => 'Assessment', + isPartOf => Caliper::Entity::course($c), + name => $set_id, + items => $items, + dateToStartOn => Caliper::Sensor::formatted_timestamp($problem_set->open_date), + dateToSubmit => Caliper::Sensor::formatted_timestamp($problem_set->due_date), + extensions => { + answer_date => $problem_set->answer_date, + reduced_scoring_date => $problem_set->reduced_scoring_date, + visible => $problem_set->visible, + enable_reduced_scoring => $problem_set->enable_reduced_scoring, + description => $problem_set->description, + restricted_release => $problem_set->restricted_release, + restricted_status => $problem_set->restricted_status, + attempts_per_version => $problem_set->attempts_per_version, + time_interval => $problem_set->time_interval, + versions_per_interval => $problem_set->versions_per_interval, + version_time_limit => $problem_set->version_time_limit, + version_creation_time => $problem_set->version_creation_time, + problem_randorder => $problem_set->problem_randorder, + version_last_attempt_time => $problem_set->version_last_attempt_time, + problems_per_page => $problem_set->problems_per_page, + hide_score => $problem_set->hide_score, + hide_score_by_problem => $problem_set->hide_score_by_problem, + hide_work => $problem_set->hide_work, + time_limit_cap => $problem_set->time_limit_cap, + restrict_ip => $problem_set->restrict_ip, + relax_restrict_ip => $problem_set->relax_restrict_ip, + restricted_login_proctor => $problem_set->restricted_login_proctor, + hide_hint => $problem_set->hide_hint, + restrict_prob_progression => $problem_set->restrict_prob_progression } }; - if (defined($problem_set->description()) && $problem_set->description() ne '') { - $problem_set_entity->{'description'} = $problem_set->description(); - } + $problem_set_entity->{description} = $problem_set->description + if defined $problem_set->description && $problem_set->description ne ''; return $problem_set_entity; } -sub problem { - my ($ce, $db, $set_id, $problem_id) = @_; - my $resource_iri = Caliper::ResourceIri->new($ce); +sub problem ($c, $set_id, $problem_id) { + my $problem = $c->db->getGlobalProblem($set_id, $problem_id); - my $problem = $db->getGlobalProblem($set_id, $problem_id); + my $tags = { keywords => [] }; - my $keywords = []; - my $unblessed_tags; - - # Problem groups use a "group:problemGroupName" pseudo source file (see - # WeBWorK::Utils::Instructor::assignProblemToUserSetVersion) which is not a real file, so attempting to read tags - # from it would die. - if ($problem->source_file() !~ /^group:/) { - my $templateDir = $ce->{courseDirs}->{templates}; - my $tags = WeBWorK::Utils::Tags->new($templateDir . '/' . $problem->source_file()); - $keywords = $tags->{'keywords'}; - $_ =~ s/(^[\s"']+)|([\s"']+$)//g for @$keywords; - - $unblessed_tags = {%$tags}; + if ($problem->source_file !~ /^group:/) { + $tags = WeBWorK::Utils::Tags->new($c->ce->{courseDirs}{templates} . '/' . $problem->source_file); + $_ =~ s/(^[\s"']+)|([\s"']+$)//g for @{ $tags->{keywords} }; } return { - 'id' => $resource_iri->problem($set_id, $problem_id), - 'type' => 'AssessmentItem', - 'name' => 'Problem ' . $problem_id, - 'isPartOf' => Caliper::Entity::problem_set($ce, $db, $set_id), - 'keywords' => $keywords, - 'extensions' => { - 'source_file' => $problem->source_file(), - 'value' => $problem->value(), - 'max_attempts' => $problem->max_attempts(), - 'att_to_open_children' => $problem->att_to_open_children(), - 'counts_parent_grade' => $problem->counts_parent_grade(), - 'showMeAnother' => $problem->showMeAnother(), - 'showMeAnotherCount' => $problem->showMeAnotherCount(), - 'showHintsAfter' => $problem->showHintsAfter(), - 'prPeriod' => $problem->prPeriod(), - 'prCount' => $problem->prCount(), - 'flags' => $problem->flags(), - 'tags' => $unblessed_tags, - }, + id => Caliper::ResourceIri->new($c->ce)->problem($set_id, $problem_id), + type => 'AssessmentItem', + name => "Problem $problem_id", + isPartOf => Caliper::Entity::problem_set($c, $set_id), + keywords => $tags->{keywords}, + extensions => { + source_file => $problem->source_file, + value => $problem->value, + max_attempts => $problem->max_attempts, + att_to_open_children => $problem->att_to_open_children, + counts_parent_grade => $problem->counts_parent_grade, + showMeAnother => $problem->showMeAnother, + showMeAnotherCount => $problem->showMeAnotherCount, + showHintsAfter => $problem->showHintsAfter, + prPeriod => $problem->prPeriod, + prCount => $problem->prCount, + flags => $problem->flags, + tags => \%$tags + } }; } -sub problem_user { - my ($ce, $db, $set_id, $version_id, $problem_id, $user_id, $pg) = @_; - my $resource_iri = Caliper::ResourceIri->new($ce); - +sub problem_user ($c, $set_id, $version_id, $problem_id, $user_id, $pg) { my $problem_user = $version_id - ? $db->getMergedProblemVersion($user_id, $set_id, $version_id, $problem_id) - : $db->getMergedProblem($user_id, $set_id, $problem_id); - - my $templateDir = $ce->{courseDirs}->{templates}; - my $tags = WeBWorK::Utils::Tags->new($templateDir . '/' . $problem_user->source_file()); - my $keywords = $tags->{'keywords'}; - $_ =~ s/(^[\s"']+)|([\s"']+$)//g for @$keywords; + ? $c->db->getMergedProblemVersion($user_id, $set_id, $version_id, $problem_id) + : $c->db->getMergedProblem($user_id, $set_id, $problem_id); - my %tags_ref = %$tags; - my $unblessed_tags = \%tags_ref; + my $tags = WeBWorK::Utils::Tags->new($c->ce->{courseDirs}{templates} . '/' . $problem_user->source_file); + $_ =~ s/(^[\s"']+)|([\s"']+$)//g for @{ $tags->{keywords} }; my $correct_answers = []; - foreach my $ans_id (@{ $pg->{flags}->{ANSWER_ENTRY_ORDER} // [] }) { - push @$correct_answers, $pg->{'answers'}->{$ans_id}->{'correct_value'}; + for my $ans_id (@{ $pg->{flags}->{ANSWER_ENTRY_ORDER} // [] }) { + push @$correct_answers, $pg->{answers}->{$ans_id}->{correct_value}; } return { - 'id' => $resource_iri->problem_user($set_id, $problem_id, $user_id), - 'type' => 'AssessmentItem', - 'name' => 'Problem ' . $problem_id, - 'isPartOf' => Caliper::Entity::problem($ce, $db, $set_id, $problem_id), - 'keywords' => $keywords, - 'extensions' => { - 'correct_answers' => $correct_answers, - 'source_file' => $problem_user->source_file(), - 'value' => $problem_user->value(), - 'max_attempts' => $problem_user->max_attempts(), - 'att_to_open_children' => $problem_user->att_to_open_children(), - 'counts_parent_grade' => $problem_user->counts_parent_grade(), - 'showMeAnother' => $problem_user->showMeAnother(), - 'showMeAnotherCount' => $problem_user->showMeAnotherCount(), - 'showHintsAfter' => $problem_user->showHintsAfter(), - 'prPeriod' => $problem_user->prPeriod(), - 'prCount' => $problem_user->prCount(), - 'flags' => $problem_user->flags(), - 'tags' => $unblessed_tags, - 'problem_seed' => $problem_user->problem_seed(), - 'source_text' => $problem_user->status(), - 'problem_source_code' => $pg->{problem_source_code}, - 'problem_html_text' => $pg->{'body_text'}, - 'status' => $problem_user->status(), - 'attempted' => $problem_user->attempted(), - 'last_answer' => $problem_user->last_answer(), - 'num_correct' => $problem_user->num_correct(), - 'num_incorrect' => $problem_user->num_incorrect(), - 'sub_status' => $problem_user->sub_status(), + id => Caliper::ResourceIri->new($c->ce)->problem_user($set_id, $problem_id, $user_id), + type => 'AssessmentItem', + name => "Problem $problem_id", + isPartOf => Caliper::Entity::problem($c, $set_id, $problem_id), + keywords => $tags->{keywords}, + extensions => { + correct_answers => $correct_answers, + source_file => $problem_user->source_file, + value => $problem_user->value, + max_attempts => $problem_user->max_attempts, + att_to_open_children => $problem_user->att_to_open_children, + counts_parent_grade => $problem_user->counts_parent_grade, + showMeAnother => $problem_user->showMeAnother, + showMeAnotherCount => $problem_user->showMeAnotherCount, + showHintsAfter => $problem_user->showHintsAfter, + prPeriod => $problem_user->prPeriod, + prCount => $problem_user->prCount, + flags => $problem_user->flags, + tags => \%$tags, + problem_seed => $problem_user->problem_seed, + source_text => $problem_user->status, + problem_html_text => $pg->{'body_text'}, + status => $problem_user->status, + attempted => $problem_user->attempted, + last_answer => $problem_user->last_answer, + num_correct => $problem_user->num_correct, + num_incorrect => $problem_user->num_incorrect, + sub_status => $problem_user->sub_status } }; } -sub answer { - my ($ce, $db, $set_id, $version_id, $problem_id, $user_id, $pg, $start_time, $end_time) = @_; - my $resource_iri = Caliper::ResourceIri->new($ce); - +sub answer ($c, $set_id, $version_id, $problem_id, $user_id, $pg, $start_time, $end_time) { my $last_answer_id = - $db->latestProblemPastAnswer($user_id, ($version_id ? "$set_id,v$version_id" : $set_id), $problem_id); - my $last_answer = $db->getPastAnswer($last_answer_id); - my @answers = split(/\t/, $last_answer->answer_string()); + $c->db->latestProblemPastAnswer($user_id, ($version_id ? "$set_id,v$version_id" : $set_id), $problem_id); + my $last_answer = $c->db->getPastAnswer($last_answer_id); + my @answers = split(/\t/, $last_answer->answer_string); my $pg_answers_hash = {}; - foreach my $key (keys %{ $pg->{'answers'} }) { - my %answer_ref = %{ $pg->{'answers'}->{$key} }; + for my $key (keys %{ $pg->{answers} }) { + my %answer_ref = %{ $pg->{answers}->{$key} }; my $unblessed_answer = \%answer_ref; $pg_answers_hash->{$key} = $unblessed_answer; } return { - 'id' => $resource_iri->answer($set_id, $problem_id, $user_id), - 'type' => 'FillinBlankResponse', - 'attempt' => Caliper::Entity::answer_attempt( - $ce, $db, $set_id, $version_id, $problem_id, $user_id, $pg, $start_time, $end_time + id => Caliper::ResourceIri->new($c->ce)->answer($set_id, $problem_id, $user_id), + type => 'FillinBlankResponse', + attempt => Caliper::Entity::answer_attempt( + $c, $set_id, $version_id, $problem_id, $user_id, $pg, $start_time, $end_time ), - 'values' => \@answers, - 'extensions' => { - 'source_file' => $last_answer->source_file(), - 'scores' => $last_answer->scores(), - 'comment' => $last_answer->comment_string(), - 'pg_answers_hash' => $pg_answers_hash, + values => \@answers, + extensions => { + source_file => $last_answer->source_file, + scores => $last_answer->scores, + comment => $last_answer->comment_string, + pg_answers_hash => $pg_answers_hash } }; } -sub answer_attempt { - my ($ce, $db, $set_id, $version_id, $problem_id, $user_id, $pg, $start_time, $end_time) = @_; - my $resource_iri = Caliper::ResourceIri->new($ce); +sub answer_attempt ($c, $set_id, $version_id, $problem_id, $user_id, $pg, $start_time, $end_time) { + my $resource_iri = Caliper::ResourceIri->new($c->ce); my $problem_user = $version_id - ? $db->getMergedProblemVersion($user_id, $set_id, $version_id, $problem_id) - : $db->getMergedProblem($user_id, $set_id, $problem_id); + ? $c->db->getMergedProblemVersion($user_id, $set_id, $version_id, $problem_id) + : $c->db->getMergedProblem($user_id, $set_id, $problem_id); my $last_answer_id = - $db->latestProblemPastAnswer($user_id, ($version_id ? "$set_id,v$version_id" : $set_id), $problem_id); - my $last_answer = $db->getPastAnswer($last_answer_id); - my $attempt = $version_id ? $version_id : scalar $db->listProblemPastAnswers($user_id, $set_id, $problem_id); + $c->db->latestProblemPastAnswer($user_id, ($version_id ? "$set_id,v$version_id" : $set_id), $problem_id); + my $last_answer = $c->db->getPastAnswer($last_answer_id); + my $attempt = $version_id ? $version_id : scalar $c->db->listProblemPastAnswers($user_id, $set_id, $problem_id); my $score = $problem_user->status || 0; $score = 0 if ($score > 1 || $score < 0); my $answer_attempt = { - 'id' => $resource_iri->answer_attempt($set_id, $problem_id, $user_id, $last_answer->answer_id()), - 'type' => 'Attempt', - 'assignee' => Caliper::Actor::generate_actor($ce, $db, $user_id), - 'assignable' => $resource_iri->problem_user($set_id, $problem_id, $user_id), - 'count' => $attempt + 0, #ensure int - 'dateCreated' => Caliper::Sensor::formatted_timestamp($last_answer->timestamp()), - 'extensions' => { - 'attempt_score' => $score, - } + id => $resource_iri->answer_attempt($set_id, $problem_id, $user_id, $last_answer->answer_id), + type => 'Attempt', + assignee => Caliper::Actor::generate_actor($c, $user_id), + assignable => $resource_iri->problem_user($set_id, $problem_id, $user_id), + count => $attempt + 0, # Make sure this is an int + dateCreated => Caliper::Sensor::formatted_timestamp($last_answer->timestamp), + extensions => { attempt_score => $score } }; if ($start_time) { - $answer_attempt->{'startedAtTime'} = Caliper::Sensor::formatted_timestamp($start_time); + $answer_attempt->{startedAtTime} = Caliper::Sensor::formatted_timestamp($start_time); if ($end_time) { - $answer_attempt->{'endedAtTime'} = Caliper::Sensor::formatted_timestamp($end_time); - $answer_attempt->{'duration'} = Caliper::Sensor::formatted_duration($end_time - $start_time); + $answer_attempt->{endedAtTime} = Caliper::Sensor::formatted_timestamp($end_time); + $answer_attempt->{duration} = Caliper::Sensor::formatted_duration($end_time - $start_time); } } return $answer_attempt; } -sub problem_set_attempt { - my ($ce, $db, $set_id, $version_id, $user_id, $start_time, $end_time) = @_; - my $resource_iri = Caliper::ResourceIri->new($ce); +sub problem_set_attempt ($c, $set_id, $version_id, $user_id, $start_time, $end_time) { + my $resource_iri = Caliper::ResourceIri->new($c->ce); my $problem_set_user = - $version_id ? $db->getMergedSetVersion($user_id, $set_id, $version_id) : $db->getMergedSet($user_id, $set_id); + $version_id + ? $c->db->getMergedSetVersion($user_id, $set_id, $version_id) + : $c->db->getMergedSet($user_id, $set_id); my $attempt = 0; if ($version_id) { $attempt = $version_id; } else { - my @problem_ids = $db->listGlobalProblems($set_id); + my @problem_ids = $c->db->listGlobalProblems($set_id); for my $problem_id (@problem_ids) { - $attempt += scalar $db->listProblemPastAnswers($user_id, $set_id, $problem_id); + $attempt += scalar $c->db->listProblemPastAnswers($user_id, $set_id, $problem_id); } } - my $score = grade_set($db, $problem_set_user, $user_id, $version_id ? 1 : 0); - my $extensions = { 'attempt_score' => $score, }; + my $score = grade_set($c->db, $problem_set_user, $user_id, $version_id ? 1 : 0); + my $extensions = { attempt_score => $score, }; if ($version_id) { - $extensions->{'gateway_score'} = grade_gateway($db, $problem_set_user->set_id, $user_id); + $extensions->{gateway_score} = grade_gateway($c->db, $problem_set_user->set_id, $user_id); } my $problem_set_attempt = { - 'id' => $resource_iri->problem_set_attempt($set_id, $user_id, $attempt), - 'type' => 'Attempt', - 'assignee' => Caliper::Actor::generate_actor($ce, $db, $user_id), - 'assignable' => $resource_iri->problem_set($set_id), - 'count' => $attempt + 0, #ensure int - 'extensions' => $extensions, + id => $resource_iri->problem_set_attempt($set_id, $user_id, $attempt), + type => 'Attempt', + assignee => Caliper::Actor::generate_actor($c, $user_id), + assignable => $resource_iri->problem_set($set_id), + count => $attempt + 0, # Make sure this is an int + extensions => $extensions }; if ($start_time) { - $problem_set_attempt->{'startedAtTime'} = Caliper::Sensor::formatted_timestamp($start_time); + $problem_set_attempt->{startedAtTime} = Caliper::Sensor::formatted_timestamp($start_time); if ($end_time) { - $problem_set_attempt->{'endedAtTime'} = Caliper::Sensor::formatted_timestamp($end_time); - $problem_set_attempt->{'duration'} = Caliper::Sensor::formatted_duration($end_time - $start_time); + $problem_set_attempt->{endedAtTime} = Caliper::Sensor::formatted_timestamp($end_time); + $problem_set_attempt->{duration} = Caliper::Sensor::formatted_duration($end_time - $start_time); } } diff --git a/lib/Caliper/Event.pm b/lib/Caliper/Event.pm index 35b1f69eeb..8c4773ceb8 100644 --- a/lib/Caliper/Event.pm +++ b/lib/Caliper/Event.pm @@ -1,47 +1,26 @@ package Caliper::Event; +use Mojo::Base -signatures; -##### Library Imports ##### -use strict; -use warnings; -use WeBWorK::CourseEnvironment; -use WeBWorK::DB; -use Data::Dumper; -use Data::UUID; +use UUID::Tiny ':std'; use Caliper::Actor; +use Caliper::Entity; use Caliper::Sensor; -# Constructor -sub add_defaults { - my ($c, $event_hash) = @_; - my $ce = $c->ce; - my $db = $c->db; - my $ug = Data::UUID->new; +sub add_defaults ($c, $event_hash) { + my $user_id = $c->param('user'); + my $actor = Caliper::Actor::generate_actor($c, $user_id); - my $user_id = $c->param('user'); - my $session_key = $c->param('key'); - my $uuid = $ug->create_str; - my $actor = Caliper::Actor::generate_actor($ce, $db, $user_id); - - if (!exists($event_hash->{'@context'})) { - $event_hash->{'@context'} = 'http://purl.imsglobal.org/ctx/caliper/v1p2'; - } - $event_hash->{'id'} = 'urn:uuid:' . $uuid; - $event_hash->{'actor'} = $actor; - $event_hash->{'session'} = Caliper::Entity::session($ce, $db, $actor, $session_key); - $event_hash->{'edApp'} = Caliper::Entity::webwork_app($ce, $db); - $event_hash->{'group'} = Caliper::Entity::course($ce, $db); - $event_hash->{'membership'} = Caliper::Entity::membership($ce, $db, $actor, $user_id); - if (!exists($event_hash->{'eventTime'})) { - $event_hash->{'eventTime'} = Caliper::Sensor::formatted_timestamp(time()); - } - - if (!exists($event_hash->{'extensions'})) { - $event_hash->{'extensions'} = (); - } - if (defined($ENV{HTTP_REFERER})) { - $event_hash->{'extensions'}{'referer'} = $ENV{HTTP_REFERER}; - } + $event_hash->{'@context'} = 'http://purl.imsglobal.org/ctx/caliper/v1p2' unless exists $event_hash->{'@context'}; + $event_hash->{id} = 'urn:uuid:' . create_uuid_as_string(UUID_V4); + $event_hash->{actor} = $actor; + $event_hash->{session} = Caliper::Entity::session($c, $actor, $c->param('key') // ''); + $event_hash->{edApp} = Caliper::Entity::webwork_app($c); + $event_hash->{group} = Caliper::Entity::course($c); + $event_hash->{membership} = Caliper::Entity::membership($c, $actor, $user_id); + $event_hash->{eventTime} = Caliper::Sensor::formatted_timestamp(time) unless exists $event_hash->{eventTime}; + $event_hash->{extensions} = () unless exists $event_hash->{extensions}; + $event_hash->{extensions}{referer} = $c->req->headers->referer if defined $c->req->headers->referer; return; } diff --git a/lib/Caliper/ResourceIri.pm b/lib/Caliper/ResourceIri.pm index f73d5df2f3..95e232e6d3 100644 --- a/lib/Caliper/ResourceIri.pm +++ b/lib/Caliper/ResourceIri.pm @@ -1,101 +1,65 @@ package Caliper::ResourceIri; +use Mojo::Base -signatures; -##### Library Imports ##### -use strict; -use warnings; -use WeBWorK::CourseEnvironment; -use WeBWorK::DB; -use Data::Dumper; - -# Constructor -sub new { - my ($class, $ce) = @_; - - # need to use $seed_ce in case of logout - my $seed_ce = WeBWorK::CourseEnvironment->new; - my $base_url = $seed_ce->{server_root_url} . $seed_ce->{webwork_url}; - if (defined($seed_ce->{caliper}{base_url}) && $seed_ce->{caliper}{base_url} ne '') { - $base_url = $seed_ce->{caliper}{base_url}; - } - if (substr($base_url, -1, 1) ne "/") { - $base_url .= "/"; - } - - my $self = { - ce => $ce, - base_url => $base_url, - }; - bless $self, $class; - return $self; +sub new ($class, $ce) { + my $base_url = $ce->{server_root_url} . $ce->{webwork_url}; + $base_url .= '/' if substr($base_url, -1, 1) ne '/'; + return bless { ce => $ce, base_url => $base_url }, $class; } -sub getBaseUrl { - my $self = shift; +sub getBaseUrl ($self) { return $self->{base_url}; } -sub webwork { - my $self = shift; - return $self->getBaseUrl(); +sub webwork ($self) { + return $self->getBaseUrl; } -sub course { - my $self = shift; - return $self->getBaseUrl() . $self->{ce}->{"courseName"} . '/'; +sub course ($self) { + return $self->getBaseUrl . $self->{ce}{courseName} . '/'; } -sub actor_homepage { - my ($self, $user_id) = @_; - return $self->course() . 'users/' . $user_id; +sub actor_homepage ($self, $user_id) { + return $self->course . 'users/' . $user_id; } -sub user_session { - my ($self, $session_key_hash) = @_; - return $self->getBaseUrl() . 'session/' . $session_key_hash; +sub user_session ($self, $session_key_hash) { + return $self->getBaseUrl . 'session/' . $session_key_hash; } -sub user_client { - my ($self, $session_key_hash) = @_; +sub user_client ($self, $session_key_hash) { return $self->user_session($session_key_hash) . '/client'; } -sub user_membership { - my ($self, $user_id) = @_; - return $self->course() . 'instructor/users2/?visible_users=' . $user_id; +sub user_membership ($self, $user_id) { + return $self->course . 'instructor/users2/?visible_users=' . $user_id; } -sub problem_set { - my ($self, $set_id) = @_; - return $self->course() . $set_id . '/'; +sub problem_set ($self, $set_id) { + return $self->course . $set_id . '/'; } -sub problem_set_user { - my ($self, $set_id, $user_id) = @_; +sub problem_set_user ($self, $set_id, $user_id) { return $self->problem_set($set_id) . '?effectiveUser=' . $user_id; } -sub problem { - my ($self, $set_id, $problem_id) = @_; +sub problem ($self, $set_id, $problem_id) { return $self->problem_set($set_id) . $problem_id . '/'; } -sub problem_user { - my ($self, $set_id, $problem_id, $user_id) = @_; +sub problem_user ($self, $set_id, $problem_id, $user_id) { return $self->problem($set_id, $problem_id) . '?effectiveUser=' . $user_id; } -sub answer { - my ($self, $set_id, $problem_id, $user_id) = @_; +sub answer ($self, $set_id, $problem_id, $user_id) { return $self->problem($set_id, $problem_id) . 'answer/' . '?effectiveUser=' . $user_id; } -sub answer_attempt { - my ($self, $set_id, $problem_id, $user_id, $answer_id) = @_; +sub answer_attempt ($self, $set_id, $problem_id, $user_id, $answer_id) { return $self->answer($set_id, $problem_id, $user_id) . '&answer_id=' . $answer_id; } -sub problem_set_attempt { - my ($self, $set_id, $user_id, $attempt) = @_; +sub problem_set_attempt ($self, $set_id, $user_id, $attempt) { return $self->problem_set_user($set_id, $user_id) . '&attempt=' . $attempt; } diff --git a/lib/Caliper/Sensor.pm b/lib/Caliper/Sensor.pm index 9913b2f0a2..7c89d072cd 100644 --- a/lib/Caliper/Sensor.pm +++ b/lib/Caliper/Sensor.pm @@ -1,107 +1,99 @@ package Caliper::Sensor; +use Mojo::Base -signatures, -async_await; -##### Library Imports ##### -use strict; -use warnings; -use WeBWorK::CourseEnvironment; -use WeBWorK::DB; -use WeBWorK::Debug qw(debug); -use Data::Dumper; -use Mojo::JSON qw(encode_json); -use Time::HiRes qw/gettimeofday/; +use Mojo::UserAgent; +use Mojo::Promise; +use Time::HiRes qw(gettimeofday); use Date::Format; -use HTTP::Request::Common; -use HTTP::Async; - +use WeBWorK::Debug qw(debug); use Caliper::Event; use Caliper::ResourceIri; -# Constructor -sub new { - my ($class, $ce) = @_; +sub new ($class, $c) { my $self = { - ce => $ce, - enabled => $ce->{caliper}{enabled}, - host => $ce->{caliper}{host}, - api_key => $ce->{caliper}{api_key} + c => $c, + enabled => $c->ce->{caliper}{enabled}, + host => $c->ce->{caliper}{host}, + api_key => $c->ce->{caliper}{api_key} }; bless $self, $class; return $self; } -sub caliperEnabled { - my $self = shift; +sub caliperEnabled ($self) { return $self->{enabled} && exists $self->{host} && exists $self->{api_key}; } -sub sendEvent { - my ($self, $c, $event_hash) = @_; - - return $self->sendEvents($c, [$event_hash]); +async sub sendEvent ($self, $event_hash) { + return await $self->sendEvents([$event_hash]); } -sub sendEvents { - my ($self, $c, $array_of_events) = @_; - return 0 unless $self->caliperEnabled(); +async sub sendEvents ($self, $array_of_events) { + return 0 unless $self->caliperEnabled; + my $c = $self->{c}; for my $event_hash (@$array_of_events) { Caliper::Event::add_defaults($c, $event_hash); } - my $ce = $c->ce; - my $resource_iri = Caliper::ResourceIri->new($ce); - my $async = HTTP::Async->new; - $async->timeout(5); - $async->max_request_time(10); + my $ce = $c->ce; + my $ua = Mojo::UserAgent->new; + $ua->inactivity_timeout(5); + $ua->request_timeout(10); - # chunk events to prevent size issues (send a maximum of 3 events at a time) + # Chunk events to prevent size issues (send a maximum of 3 events at a time). my $event_chunks = []; push(@$event_chunks, [ splice @$array_of_events, 0, 3 ]) while @$array_of_events; + my @promises; + for my $event_chunk (@$event_chunks) { - my $envelope = { - 'sensor' => $resource_iri->webwork(), - 'sendTime' => Caliper::Sensor::formatted_timestamp(time()), - 'dataVersion' => 'http://purl.imsglobal.org/ctx/caliper/v1p2', - 'data' => $event_chunk, - }; - - my $json_payload = encode_json($envelope); - # debug("Caliper event json_payload: " . $json_payload); - - my $HTTPRequest = HTTP::Request->new( - 'POST', - $self->{host}, - [ - 'Accept' => '*/*', - 'Authorization' => 'Bearer ' . $self->{api_key}, - 'Content-Type' => 'application/json', - ], - $json_payload + push( + @promises, + $ua->post_p( + $self->{host}, + { + Accept => '*/*', + Authorization => 'Bearer ' . $self->{api_key}, + 'Content-Type' => 'application/json', + }, + json => { + sensor => Caliper::ResourceIri->new($ce)->webwork, + sendTime => formatted_timestamp(time), + dataVersion => 'http://purl.imsglobal.org/ctx/caliper/v1p2', + data => $event_chunk + } + ) ); - $async->add($HTTPRequest); } - while (my $response = $async->wait_for_next_response) { - if (!$response->is_success) { - debug("Caliper event post failed. Error Message: " . $response->message); - debug($response->content); - $self->log_error("Caliper event post failed. Error Message: " - . $response->message + my @responses = await Mojo::Promise->all(@promises)->catch(sub { + my $err = shift; + $self->log_error(ref $err ? $err->message : $err); + return; + }); + + for my $response (@responses) { + my $result = $response->[0]->result; + if (!$result->is_success) { + debug('Caliper event post failed. Error Message: ' . $result->message); + debug($result->body); + $self->log_error('Caliper event post failed. Error Message: ' + . $result->message . "\nResponse Content: " - . $response->content); + . $result->body); } else { - debug("Caliper event post success. Success Message: " . $response->message); - debug($response->content); + debug('Caliper event post success. Success Message: ' . $result->message); + debug($result->body); } } + return; } -sub log_error { - my ($self, $error_message) = @_; - my $ce = $self->{ce}; +sub log_error ($self, $error_message) { + my $ce = $self->{c}->ce; my $logfile = $ce->{caliper}{errorlog}; my ($sec, $msec) = gettimeofday; @@ -110,11 +102,11 @@ sub log_error { # create if necessary unless (-e $logfile) { - open my $fc, ">", $logfile; + open my $fc, '>', $logfile; close $fc; } # append message - if (open my $f, ">>", $logfile) { + if (open my $f, '>>', $logfile) { print $f $msg; close $f; } else { @@ -123,28 +115,20 @@ sub log_error { return; } -sub formatted_timestamp { - my ($time_value) = @_; - # Note: webwork epoch timestamps do not include milliseconds - return POSIX::strftime("%Y-%m-%dT%H:%M:%S.000Z", gmtime($time_value)); +sub formatted_timestamp ($time_value) { + return POSIX::strftime('%Y-%m-%dT%H:%M:%S.000Z', gmtime($time_value)); } -sub formatted_duration { - my ($duration) = @_; - - # generate the time portion of a ISO 8601 formatted duration +sub formatted_duration ($duration) { + # Generate the time portion of a ISO 8601 formatted duration. my $seconds = $duration % 60; my $minutes = int($duration / 60) % 60; my $hours = int($duration / 3600); - my $output = "PT"; - if ($hours > 0) { - $output .= $hours . "H"; - } - if ($hours > 0 || $minutes > 0) { - $output .= $minutes . "M"; - } - $output .= $seconds . "S"; + my $output = 'PT'; + $output .= $hours . 'H' if $hours > 0; + $output .= $minutes . 'M' if $hours > 0 || $minutes > 0; + $output .= $seconds . 'S'; return $output; } diff --git a/lib/Mojolicious/WeBWorK/Plugin/Caliper.pm b/lib/Mojolicious/WeBWorK/Plugin/Caliper.pm new file mode 100644 index 0000000000..4f69cfb177 --- /dev/null +++ b/lib/Mojolicious/WeBWorK/Plugin/Caliper.pm @@ -0,0 +1,206 @@ +package Mojolicious::WeBWorK::Plugin::Caliper; +use Mojo::Base 'Mojolicious::Plugin', -signatures, -async_await; + +use Caliper::Sensor; +use Caliper::Entity; + +sub register ($plugin, $app, $config) { + $app->hook( + user_login => async sub ($c) { + my $caliper_sensor = Caliper::Sensor->new($c); + return unless $caliper_sensor->caliperEnabled; + await $caliper_sensor->sendEvents([ { + type => 'SessionEvent', + action => 'LoggedIn', + profile => 'SessionProfile', + object => Caliper::Entity::webwork_app($c) + } ]); + return; + } + ); + + $app->hook( + user_logout => async sub ($c) { + my $caliper_sensor = Caliper::Sensor->new($c); + return unless $caliper_sensor->caliperEnabled; + await $caliper_sensor->sendEvents([ { + type => 'SessionEvent', + action => 'LoggedOut', + profile => 'SessionProfile', + object => Caliper::Entity::webwork_app($c) + } ]); + return; + } + ); + + $app->hook( + answer_submitted => async sub ($c) { + my $ce = $c->ce; + my $db = $c->db; + + my $caliper_sensor = Caliper::Sensor->new($c); + if ($caliper_sensor->caliperEnabled + && defined $ce->{courseFiles}{logs}{answer_log} + && !$c->authz->hasPermissions($c->param('effectiveUser'), 'dont_log_past_answers')) + { + # Make sure the transaction does not go out of scope so + # that the startTime parameter can be reset afterward. + my $tx = $c->render_later->tx; + + my $startTime = $c->param('startTime'); + my $endTime = time; + + await $caliper_sensor->sendEvents([ + { + type => 'AssessmentItemEvent', + action => 'Completed', + profile => 'AssessmentProfile', + object => Caliper::Entity::problem_user( + $c, + $c->{problem}->set_id, + 0, # Version is 0 for non-gateway problems. + $c->{problem}->problem_id, + $c->{problem}->user_id, + $c->{pg} + ), + generated => Caliper::Entity::answer( + $c, + $c->{problem}->set_id, + 0, # Version is 0 for non-gateway problems. + $c->{problem}->problem_id, + $c->{problem}->user_id, + $c->{pg}, + $startTime, + $endTime + ), + }, + { + type => 'AssessmentEvent', + action => 'Submitted', + profile => 'AssessmentProfile', + object => Caliper::Entity::problem_set($c, $c->{problem}->set_id), + generated => Caliper::Entity::problem_set_attempt( + $c, + $c->{problem}->set_id, + 0, # Version is 0 for non-gateway problems. + $c->{problem}->user_id, + $startTime, + $endTime + ), + }, + { + type => 'ToolUseEvent', + action => 'Used', + profile => 'ToolUseProfile', + object => Caliper::Entity::webwork_app($c) + } + ]); + + # Reset start time. + $c->param('startTime', ''); + } + + return; + } + ); + + $app->hook( + test_answers_submitted => async sub ($c) { + my $ce = $c->ce; + my $db = $c->db; + + my $caliper_sensor = Caliper::Sensor->new($c); + if ($caliper_sensor->caliperEnabled && defined $ce->{courseFiles}{logs}{answer_log}) { + # Make sure the transaction does not go out of scope so + # that the startTime parameter can be reset afterward. + my $tx = $c->render_later->tx; + + my $events = []; + + my $setID = $c->stash('setID') =~ s/,v\d+$//r; + + my $startTime = $c->param('startTime'); + my $endTime = int($c->submitTime); + if ($c->{submitAnswers} && $c->{will}{recordAnswers}) { + for my $i (0 .. $#{ $c->stash->{problems} }) { + my $problem = $c->stash->{problems}[ $c->stash->{probOrder}[$i] ]; + my $pg = $c->stash->{pg_results}[ $c->stash->{probOrder}[$i] ]; + push( + @$events, + { + type => 'AssessmentItemEvent', + action => 'Completed', + profile => 'AssessmentProfile', + object => Caliper::Entity::problem_user( + $c, $problem->set_id, $c->{set}->version_id, + $problem->problem_id, $problem->user_id, $pg + ), + generated => Caliper::Entity::answer( + $c, + $problem->set_id, + $c->{set}->version_id, + $problem->problem_id, + $problem->user_id, + $pg, + 0, + 0 # Don't track start/end time for gateway problems (multiple answers per page). + ), + } + ); + } + push( + @$events, + { + type => 'AssessmentEvent', + action => 'Submitted', + profile => 'AssessmentProfile', + object => Caliper::Entity::problem_set($c, $setID), + generated => Caliper::Entity::problem_set_attempt( + $c, $setID, + $c->{set}->version_id, + $c->param('effectiveUser'), + $startTime, $endTime + ), + } + ); + } else { + push( + @$events, + { + type => 'AssessmentEvent', + action => 'Paused', + profile => 'AssessmentProfile', + object => Caliper::Entity::problem_set($c, $setID), + generated => Caliper::Entity::problem_set_attempt( + $c, $setID, + $c->{set}->version_id, + $c->param('effectiveUser'), + $startTime, $endTime + ), + } + ); + } + push( + @$events, + { + type => 'ToolUseEvent', + action => 'Used', + profile => 'ToolUseProfile', + object => Caliper::Entity::webwork_app($c) + } + ); + + await $caliper_sensor->sendEvents($events); + + # Reset start time. + $c->param('startTime', ''); + } + + return; + } + ); + + return; +} + +1; diff --git a/lib/WeBWorK/Authen.pm b/lib/WeBWorK/Authen.pm index f1f72f2546..5b0b3924f1 100644 --- a/lib/WeBWorK/Authen.pm +++ b/lib/WeBWorK/Authen.pm @@ -45,8 +45,6 @@ use WeBWorK::Utils qw(x runtime_use utf8Crypt cryptPassword); use WeBWorK::Utils::Logs qw(writeCourseLog); use WeBWorK::Utils::TOTP; use WeBWorK::Localize; -use Caliper::Sensor; -use Caliper::Entity; use constant GENERIC_ERROR_MESSAGE => x('Invalid user ID or password.'); @@ -202,18 +200,7 @@ sub verify { $c->stash(authen_error => $self->{error}) if $self->{error} && $self->{error} =~ /\S/; } - my $caliper_sensor = Caliper::Sensor->new($c->ce); - if ($caliper_sensor->caliperEnabled && $self->{was_verified} && $self->{initial_login}) { - $caliper_sensor->sendEvents( - $c, - [ { - 'type' => 'SessionEvent', - 'action' => 'LoggedIn', - 'profile' => 'SessionProfile', - 'object' => Caliper::Entity::webwork_app() - } ] - ); - } + $c->app->plugins->emit_hook(user_login => $c) if $self->{initial_login} && $self->{was_verified}; debug("END VERIFY"); debug("result $self->{was_verified}"); @@ -964,18 +951,7 @@ sub killSession { my $c = $self->{c}; my $ce = $c->{ce}; - my $caliper_sensor = Caliper::Sensor->new($ce); - if ($caliper_sensor->caliperEnabled) { - $caliper_sensor->sendEvents( - $c, - [ { - 'type' => 'SessionEvent', - 'action' => 'LoggedOut', - 'profile' => 'SessionProfile', - 'object' => Caliper::Entity::webwork_app() - } ] - ); - } + $c->app->plugins->emit_hook(user_logout => $c); $self->forget_verification; $self->killCookie; diff --git a/lib/WeBWorK/ContentGenerator/GatewayQuiz.pm b/lib/WeBWorK/ContentGenerator/GatewayQuiz.pm index f18b4ca492..2ac78ada2a 100644 --- a/lib/WeBWorK/ContentGenerator/GatewayQuiz.pm +++ b/lib/WeBWorK/ContentGenerator/GatewayQuiz.pm @@ -23,8 +23,6 @@ use WeBWorK::DB::Utils qw(global2user fake_set fake_set_version f use WeBWorK::Debug qw(debug); use PGrandom; use WeBWorK::Authen::LTI::GradePassback qw(passbackGradeOnSubmit); -use Caliper::Sensor; -use Caliper::Entity; # Disable links for gateway tests. sub can ($c, $arg) { @@ -1136,72 +1134,7 @@ async sub pre_header_initialize ($c) { } } - my $caliper_sensor = Caliper::Sensor->new($c->ce); - if ($caliper_sensor->caliperEnabled() && defined $answer_log) { - my $events = []; - - my $startTime = $c->param('startTime'); - my $endTime = int($c->submitTime); - if ($c->{submitAnswers} && $will{recordAnswers}) { - for my $i (0 .. $#problems) { - my $problem = $problems[ $probOrder[$i] ]; - my $pg = $pg_results[ $probOrder[$i] ]; - my $completed_question_event = { - 'type' => 'AssessmentItemEvent', - 'action' => 'Completed', - 'profile' => 'AssessmentProfile', - 'object' => Caliper::Entity::problem_user( - $c->ce, $db, $problem->set_id(), $versionID, $problem->problem_id(), - $problem->user_id(), $pg - ), - 'generated' => Caliper::Entity::answer( - $c->ce, - $db, - $problem->set_id(), - $versionID, - $problem->problem_id(), - $problem->user_id(), - $pg, - 0, # don't track start/end time for gateway problems (multiple answers per page) - 0 # don't track start/end time for gateway problems (multiple answers per page) - ), - }; - push @$events, $completed_question_event; - } - my $submitted_set_event = { - 'type' => 'AssessmentEvent', - 'action' => 'Submitted', - 'profile' => 'AssessmentProfile', - 'object' => Caliper::Entity::problem_set($c->ce, $db, $setID), - 'generated' => Caliper::Entity::problem_set_attempt( - $c->ce, $db, $setID, $versionID, $effectiveUserID, $startTime, $endTime - ), - }; - push @$events, $submitted_set_event; - } else { - my $paused_set_event = { - 'type' => 'AssessmentEvent', - 'action' => 'Paused', - 'profile' => 'AssessmentProfile', - 'object' => Caliper::Entity::problem_set($c->ce, $db, $setID), - 'generated' => Caliper::Entity::problem_set_attempt( - $c->ce, $db, $setID, $versionID, $effectiveUserID, $startTime, $endTime - ), - }; - push @$events, $paused_set_event; - } - my $tool_use_event = { - 'type' => 'ToolUseEvent', - 'action' => 'Used', - 'profile' => 'ToolUseProfile', - 'object' => Caliper::Entity::webwork_app(), - }; - push @$events, $tool_use_event; - $caliper_sensor->sendEvents($c, $events); - - # Reset start time - $c->param('startTime', ''); - } + $c->app->plugins->emit_hook('test_answers_submitted' => $c); } debug('end answer processing'); diff --git a/lib/WeBWorK/Utils/ProblemProcessing.pm b/lib/WeBWorK/Utils/ProblemProcessing.pm index 6aa4d0d302..9f4a3241a9 100644 --- a/lib/WeBWorK/Utils/ProblemProcessing.pm +++ b/lib/WeBWorK/Utils/ProblemProcessing.pm @@ -18,8 +18,6 @@ use WeBWorK::Utils::DateTime qw(before after); use WeBWorK::Utils::JITAR qw(jitar_id_to_seq jitar_problem_adjusted_status); use WeBWorK::Utils::Logs qw(writeLog writeCourseLog); use WeBWorK::Authen::LTI::GradePassback qw(passbackGradeOnSubmit); -use Caliper::Sensor; -use Caliper::Entity; our @EXPORT_OK = qw( process_and_log_answer @@ -149,66 +147,7 @@ async sub process_and_log_answer ($c) { . $pureProblem->num_correct . "\t" . $pureProblem->num_incorrect); - if ($ce->{caliper}{enabled} - && defined($answer_log) - && !$authz->hasPermissions($effectiveUser, 'dont_log_past_answers')) - { - my $caliper_sensor = Caliper::Sensor->new($ce); - my $startTime = $c->param('startTime'); - my $endTime = time(); - - my $completed_question_event = { - type => 'AssessmentItemEvent', - action => 'Completed', - profile => 'AssessmentProfile', - object => Caliper::Entity::problem_user( - $ce, - $db, - $problem->set_id(), - 0, #version is 0 for non-gateway problems - $problem->problem_id(), - $problem->user_id(), - $pg - ), - generated => Caliper::Entity::answer( - $ce, - $db, - $problem->set_id(), - 0, #version is 0 for non-gateway problems - $problem->problem_id(), - $problem->user_id(), - $pg, - $startTime, - $endTime - ), - }; - my $submitted_set_event = { - type => 'AssessmentEvent', - action => 'Submitted', - profile => 'AssessmentProfile', - object => Caliper::Entity::problem_set($ce, $db, $problem->set_id()), - generated => Caliper::Entity::problem_set_attempt( - $ce, - $db, - $problem->set_id(), - 0, #version is 0 for non-gateway problems - $problem->user_id(), - $startTime, - $endTime - ), - }; - my $tool_use_event = { - type => 'ToolUseEvent', - action => 'Used', - profile => 'ToolUseProfile', - object => Caliper::Entity::webwork_app(), - }; - $caliper_sensor->sendEvents($c, - [ $completed_question_event, $submitted_set_event, $tool_use_event ]); - - # reset start time - $c->param('startTime', ''); - } + $c->app->plugins->emit_hook('answer_submitted' => $c); # Send the score for this set to the LMS if enabled. if ($ce->{LTIGradeMode}) { diff --git a/lib/WeBWorK/Utils/Rendering.pm b/lib/WeBWorK/Utils/Rendering.pm index bdad7c6164..394cd3702d 100644 --- a/lib/WeBWorK/Utils/Rendering.pm +++ b/lib/WeBWorK/Utils/Rendering.pm @@ -272,9 +272,6 @@ sub renderPG ($c, $effectiveUser, $set, $problem, $psvn, $formFields, $translati $ret->{render_fail} = 1; } - # Save the problem source. This is used by Caliper::Entity. Why? - $ret->{problem_source_code} = $pg->{translator}{source} if ref $pg->{translator}; - $pg->free; return $ret; })->catch(sub ($err) {