From raeburn at source.lon-capa.org Mon May 14 15:56:06 2018 From: raeburn at source.lon-capa.org (raeburn) Date: Mon, 14 May 2018 19:56:06 -0000 Subject: [LON-CAPA-cvs] cvs: loncom /lti ltiauth.pm Message-ID: raeburn Mon May 14 19:56:06 2018 EDT Modified files: /loncom/lti ltiauth.pm Log: - Fix typo. Index: loncom/lti/ltiauth.pm diff -u loncom/lti/ltiauth.pm:1.10 loncom/lti/ltiauth.pm:1.11 --- loncom/lti/ltiauth.pm:1.10 Tue May 8 00:44:14 2018 +++ loncom/lti/ltiauth.pm Mon May 14 19:56:05 2018 @@ -1,7 +1,7 @@ # The LearningOnline Network # Basic LTI Authentication Module # -# $Id: ltiauth.pm,v 1.10 2018/05/08 00:44:14 raeburn Exp $ +# $Id: ltiauth.pm,v 1.11 2018/05/14 19:56:05 raeburn Exp $ # # Copyright Michigan State University Board of Trustees # @@ -432,7 +432,7 @@ my @lcroleorder = ('cc','in','ta','ep','st'); my @ltiroleorder = ('Instructor','TeachingAssistant','Mentor','Learner'); if ($params->{'roles'} =~ /,/) { - my @possltiroles = split(/\s*,\s*/,$params->{'role'}); + my @possltiroles = split(/\s*,\s*/,$params->{'roles'}); foreach my $ltirole (@ltiroleorder) { if (grep(/^\Q$ltirole\E$/, at possltiroles)) { push(@ltiroles,$ltirole); From raeburn at source.lon-capa.org Mon May 14 16:10:15 2018 From: raeburn at source.lon-capa.org (raeburn) Date: Mon, 14 May 2018 20:10:15 -0000 Subject: [LON-CAPA-cvs] cvs: loncom /lti ltiauth.pm Message-ID: raeburn Mon May 14 20:10:15 2018 EDT Modified files: /loncom/lti ltiauth.pm Log: - Bug 6754 LON-CAPA as LTI Provider - Support LTI Consumer using OAuth version 1.0a (sends oauth_callback) Index: loncom/lti/ltiauth.pm diff -u loncom/lti/ltiauth.pm:1.11 loncom/lti/ltiauth.pm:1.12 --- loncom/lti/ltiauth.pm:1.11 Mon May 14 19:56:05 2018 +++ loncom/lti/ltiauth.pm Mon May 14 20:10:15 2018 @@ -1,7 +1,7 @@ # The LearningOnline Network # Basic LTI Authentication Module # -# $Id: ltiauth.pm,v 1.11 2018/05/14 19:56:05 raeburn Exp $ +# $Id: ltiauth.pm,v 1.12 2018/05/14 20:10:15 raeburn Exp $ # # Copyright Michigan State University Board of Trustees # @@ -294,6 +294,12 @@ $protocol = 'https'; } + if (exists($params->{'oauth_callback'})) { + $Net::OAuth::PROTOCOL_VERSION = Net::OAuth::PROTOCOL_VERSION_1_0A; + } else { + $Net::OAuth::PROTOCOL_VERSION = Net::OAuth::PROTOCOL_VERSION_1_0; + } + my ($itemid,$consumer_key,$secret); $consumer_key = $params->{'oauth_consumer_key'}; if (ref($lti_by_key{$consumer_key}) eq 'ARRAY') { From raeburn at source.lon-capa.org Tue May 15 00:33:18 2018 From: raeburn at source.lon-capa.org (raeburn) Date: Tue, 15 May 2018 04:33:18 -0000 Subject: [LON-CAPA-cvs] cvs: loncom /lti ltiutils.pm Message-ID: raeburn Tue May 15 04:33:18 2018 EDT Modified files: /loncom/lti ltiutils.pm Log: - Seed rand before generating nonce. Index: loncom/lti/ltiutils.pm diff -u loncom/lti/ltiutils.pm:1.8 loncom/lti/ltiutils.pm:1.9 --- loncom/lti/ltiutils.pm:1.8 Tue Apr 17 14:03:01 2018 +++ loncom/lti/ltiutils.pm Tue May 15 04:33:17 2018 @@ -1,7 +1,7 @@ # The LearningOnline Network with CAPA # Utility functions for managing LON-CAPA LTI interactions # -# $Id: ltiutils.pm,v 1.8 2018/04/17 14:03:01 raeburn Exp $ +# $Id: ltiutils.pm,v 1.9 2018/05/15 04:33:17 raeburn Exp $ # # Copyright Michigan State University Board of Trustees # @@ -324,6 +324,7 @@ if ($sigmethod eq '') { $sigmethod = 'HMAC-SHA1'; } + srand( time() ^ ($$ + ($$ << 15)) ); # Seed rand. my $nonce = Digest::SHA::sha1_hex(sprintf("%06x%06x",rand(0xfffff0),rand(0xfffff0))); my $request = Net::OAuth->request("request token")->new( consumer_key => $key, From raeburn at source.lon-capa.org Tue May 15 00:59:22 2018 From: raeburn at source.lon-capa.org (raeburn) Date: Tue, 15 May 2018 04:59:22 -0000 Subject: [LON-CAPA-cvs] cvs: loncom /homework lonhomework.pm structuretags.pm /lti ltiutils.pm Message-ID: raeburn Tue May 15 04:59:22 2018 EDT Modified files: /loncom/homework structuretags.pm lonhomework.pm /loncom/lti ltiutils.pm Log: - Bug 6754 LON-CAPA as LTI Provider - Pass grades back to the LTI Consumer which launched the LTI session. - Any problemstatus settings of "No" are ignored. -------------- next part -------------- Index: loncom/homework/structuretags.pm diff -u loncom/homework/structuretags.pm:1.559 loncom/homework/structuretags.pm:1.560 --- loncom/homework/structuretags.pm:1.559 Fri Mar 30 23:50:13 2018 +++ loncom/homework/structuretags.pm Tue May 15 04:59:14 2018 @@ -1,7 +1,7 @@ # The LearningOnline Network with CAPA # definition of tags that give a structure to a document # -# $Id: structuretags.pm,v 1.559 2018/03/30 23:50:13 raeburn Exp $ +# $Id: structuretags.pm,v 1.560 2018/05/15 04:59:14 raeburn Exp $ # # Copyright Michigan State University Board of Trustees # @@ -1117,6 +1117,7 @@ delete(@Apache::lonhomework::results{@remove}); my ($symb,$courseid,$domain,$name) = &Apache::lonnet::whichuser($given_symb); + my ($passback,$ltiscope,$ltimap,$ltisymb,$ltiref,$total,$possible); if ($env{'request.state'} eq 'construct' || $symb eq '' || $Apache::lonhomework::type eq 'practice') { @@ -1126,17 +1127,23 @@ $namespace,'',$domain,$name); &Apache::lonxml::debug('Construct Store return message:'.$result); } else { - my ($laststore,$checkedparts, at parts,%postcorrect); + my ($laststore,$checkedparts, at parts,%postcorrect,%record); if (($env{'user.name'} eq $name) && ($env{'user.domain'} eq $domain) && (!$Apache::lonhomework::scantronmode) && (!defined($env{'form.grade_symb'})) && (!defined($env{'form.grade_courseid'}))) { + if ($env{'request.lti.login'}) { + my ($map)=&Apache::lonnet::decode_symb($symb); + $map = &Apache::lonnet::clutter($map); + ($passback,$ltiscope,$ltimap,$ltisymb,$ltiref) = + &needs_lti_passback($courseid,$symb,$map); + } if ($Apache::lonhomework::history{'version'}) { $laststore = $Apache::lonhomework::history{'version'}.'='. $Apache::lonhomework::history{'timestamp'}; } else { $laststore = '0=0'; } - my %record = &Apache::lonnet::restore($symb,$courseid,$domain,$name); + %record = &Apache::lonnet::restore($symb,$courseid,$domain,$name); if ($record{'version'}) { my ($newversion,$oldversion,$oldtimestamp); if ($Apache::lonhomework::history{'version'}) { @@ -1212,8 +1219,70 @@ } } } + if (($passback) && ($ltiscope eq 'resource') && ($ltisymb eq $symb)) { + $total = 0; + $possible = 0; + my $navmap = Apache::lonnavmaps::navmap->new(); + if (ref($navmap)) { + my $res = $navmap->getBySymb($symb); + if (ref($res)) { + my $partlist = $res->parts(); + if (ref($partlist) eq 'ARRAY') { + foreach my $part (@{$partlist}) { + unless (exists($Apache::lonhomework::results{"resource.$part.solved"})) { + next if ($Apache::lonhomework::record{"resource.$part.solved"} =~/^excused/); + my $weight = &Apache::lonnet::EXT("resource.$part.weight",$symb); + $possible += $weight; + if (($record{'version'}) && (exists($record{"resource.$part.awarded"}))) { + my $awarded = $record{"resource.$part.awarded"}; + if ($awarded) { + $total += $weight * $awarded; + } + } + } + } + } + } + } + foreach my $key (keys(%Apache::lonhomework::results)) { + if ($key =~ /^resource\.([^\.]+)\.awarded$/) { + my $part = $1; + my $weight = &Apache::lonnet::EXT("resource.$part.weight",$symb); + $possible += $weight; + my $awarded = $Apache::lonhomework::results{$key}; + if ($awarded) { + $total += $weight * $awarded; + } + } + } + } &Apache::lonxml::debug('Store return message:'.$result); &store_aggregates($symb,$courseid); + if ($passback) { + my $scoreformat = 'decimal'; + if (ref($ltiref) eq 'HASH') { + if ($ltiref->{'scoreformat'} =~ /^(decimal|ratio|percentage)$/) { + $scoreformat = $1; + } + } + my $ltigrade = { + 'lti' => $ltiref, + 'cid' => $courseid, + 'uname' => $env{'user.name'}, + 'udom' => $env{'user.domain'}, + 'pbid' => $env{'request.lti.passbackid'}, + 'pburl' => $env{'request.lti.passbackurl'}, + 'scope' => $ltiscope, + 'ltimap' => $ltimap, + 'ltisymb' => $ltisymb, + 'format' => $scoreformat, + }; + if ($ltiscope eq 'resource') { + $ltigrade->{'total'} = $total; + $ltigrade->{'possible'} = $possible; + } + push(@Apache::lonhomework::ltipassback,$ltigrade); + } } } else { &Apache::lonxml::debug('Nothing to store'); @@ -1221,6 +1290,40 @@ return $result; } +sub needs_lti_passback { + my ($courseid,$symb,$map) = @_; + if (($env{'request.lti.passbackid'}) && ($env{'request.lti.passbackurl'})) { + if ($courseid =~ /^($LONCAPA::match_domain)_($LONCAPA::match_courseid)$/) { + my ($cdom,$cnum) = ($1,$2); + my %lti = &Apache::lonnet::get_domain_lti($cdom,'provider'); + if (ref($lti{$env{'request.lti.login'}}) eq 'HASH') { + if ($lti{$env{'request.lti.login'}}{'passback'}) { + my ($ltiscope,$ltiuri,$ltisymb) = + &LONCAPA::ltiutils::lti_provider_scope($env{'request.lti.uri'}, + $cdom,$cnum,1); + my ($passback,$ltimap); + if ($ltiscope eq 'resource') { + if ($ltisymb eq $symb) { + $passback = 1; + } + } elsif ($ltiscope eq 'map') { + if ($ltiuri eq $map) { + $passback = 1; + $ltimap = $map; + } + } elsif ($ltiscope eq 'course') { + if (($env{'request.lti.uri'} eq "/$cdom/$cnum") || ($env{'request.lti.uri'} eq '')) { + $passback = 1; + } + } + return ($passback,$ltiscope,$ltimap,$ltisymb,$lti{$env{'request.lti.login'}}); + } + } + } + } + return; +} + =pod =item check_correctness_changes() Index: loncom/homework/lonhomework.pm diff -u loncom/homework/lonhomework.pm:1.370 loncom/homework/lonhomework.pm:1.371 --- loncom/homework/lonhomework.pm:1.370 Wed Jan 31 15:28:28 2018 +++ loncom/homework/lonhomework.pm Tue May 15 04:59:14 2018 @@ -1,7 +1,7 @@ # The LearningOnline Network with CAPA # The LON-CAPA Homework handler # -# $Id: lonhomework.pm,v 1.370 2018/01/31 15:28:28 raeburn Exp $ +# $Id: lonhomework.pm,v 1.371 2018/05/15 04:59:14 raeburn Exp $ # # Copyright Michigan State University Board of Trustees # @@ -53,13 +53,17 @@ use Apache::Constants qw(:common); use Apache::loncommon(); use Apache::lonparmset(); +use Apache::lonnavmaps(); use Apache::lonlocal; +use LONCAPA qw(:DEFAULT :match); +use LONCAPA::ltiutils(); use Time::HiRes qw( gettimeofday tv_interval ); use HTML::Entities(); use File::Copy(); # FIXME - improve commenting +my $registered_cleanup; BEGIN { &Apache::lonxml::register_insert(); @@ -1671,7 +1675,15 @@ } # just render the page normally outside of construction space &Apache::lonxml::debug("not construct"); + undef(@Apache::lonhomework::ltipassback); &renderpage($request,$file,undef,undef,$donemsg); + if (@Apache::lonhomework::ltipassback) { + unless ($registered_cleanup) { + my $handlers = $request->get_handlers('PerlCleanupHandler'); + $request->set_handlers('PerlCleanupHandler' => + [\&do_ltipassback,@{$handlers}]); + } + } } #my $td=&tv_interval($t0); #&Apache::lonxml::debug("Spent $td seconds processing"); @@ -1859,5 +1871,81 @@ return $return; } +sub do_ltipassback { + if (@Apache::lonhomework::ltipassback) { + foreach my $item (@Apache::lonhomework::ltipassback) { + if (ref($item) eq 'HASH') { + if ((ref($item->{'lti'}) eq 'HASH') && ($item->{'cid'} =~ /^($match_domain)_($match_courseid)$/)) { + my ($cdom,$cnum) = ($1,$2); + my $ckey = $item->{'lti'}->{'key'}; + my $secret = $item->{'lti'}->{'secret'}; + my $id = $item->{'pbid'}; + my $url = $item->{'pburl'}; + my $scope = $item->{'scope'}; + my $map = $item->{'ltimap'}; + my $symb = $item->{'ltisymb'}; + my $uname = $item->{'uname'}; + my $udom = $item->{'udom'}; + my $scoretype = $item->{'format'}; + my ($total,$possible); + if ($scope eq 'resource') { + $total = $item->{'total'}; + $possible = $item->{'possible'}; + } elsif ($scope eq 'map') { + ($total,$possible) = &get_lti_score($uname,$udom,$map); + } elsif ($scope eq 'course') { + ($total,$possible) = &get_lti_score($uname,$udom); + } + if (($ckey ne '') && ($secret ne '') && ($id ne '') && ($url ne '') && ($possible)) { + &LONCAPA::ltiutils::send_grade($id,$url,$ckey,$secret,$scoretype,$total,$possible); + } + } + } + } + undef(@Apache::lonhomework::ltipassback); + } +} + +sub get_lti_score { + my ($uname,$udom,$mapurl) = @_; + my $navmap = Apache::lonnavmaps::navmap->new($uname,$udom); + if (ref($navmap)) { + my $iterator; + if ($mapurl ne '') { + my $map = $navmap->getResourceByUrl($mapurl); + my $firstres = $map->map_start(); + my $finishres = $map->map_finish(); + $iterator = $navmap->getIterator($firstres,$finishres,undef,1); + } else { + $iterator = $navmap->getIterator(undef,undef,undef,1); + } + if (ref($iterator)) { + my $depth = 1; + my $total = 0; + my $possible = 0; + $iterator->next(); # ignore first BEGIN_MAP + my $curRes = $iterator->next(); + while ( $depth > 0 ) { + if ($curRes == $iterator->BEGIN_MAP()) {$depth++;} + if ($curRes == $iterator->END_MAP()) { $depth--; } + if (ref($curRes) && $curRes->is_gradable() && !$curRes->randomout) { + my $parts = $curRes->parts(); + foreach my $part (@{$parts}) { + next if ($curRes->solved($part) eq 'excused'); + $total += $curRes->weight($part) * $curRes->awarded($part); + $possible += $curRes->weight($part); + } + } + $curRes = $iterator->next(); + } + if ($total > $possible) { + $total = $possible; + } + return ($total,$possible); + } + } + return; +} + 1; __END__ Index: loncom/lti/ltiutils.pm diff -u loncom/lti/ltiutils.pm:1.9 loncom/lti/ltiutils.pm:1.10 --- loncom/lti/ltiutils.pm:1.9 Tue May 15 04:33:17 2018 +++ loncom/lti/ltiutils.pm Tue May 15 04:59:22 2018 @@ -1,7 +1,7 @@ # The LearningOnline Network with CAPA # Utility functions for managing LON-CAPA LTI interactions # -# $Id: ltiutils.pm,v 1.9 2018/05/15 04:33:17 raeburn Exp $ +# $Id: ltiutils.pm,v 1.10 2018/05/15 04:59:22 raeburn Exp $ # # Copyright Michigan State University Board of Trustees # @@ -34,6 +34,7 @@ use UUID::Tiny ':std'; use Apache::lonnet; use Apache::loncommon; +use Math::Round(); use LONCAPA qw(:DEFAULT :match); # @@ -474,9 +475,9 @@ # sub lti_provider_scope { - my ($tail,$cdom,$cnum) = @_; - my ($scope,$realuri); - if ($tail =~ m{^/uploaded/$cdom/$cnum/(?:default|supplemental)(?:|_\d+)\.(?:sequence|page)(|___\d+___.+)$}) { + my ($tail,$cdom,$cnum,$getunenc) = @_; + my ($scope,$realuri,$passkey,$unencsymb); + if ($tail =~ m{^/?uploaded/$cdom/$cnum/(?:default|supplemental)(?:|_\d+)\.(?:sequence|page)(|___\d+___.+)$}) { my $rest = $1; if ($rest eq '') { $scope = 'map'; @@ -489,9 +490,13 @@ } else { $scope = 'resource'; $realuri .= '?symb='.$tail; + $passkey = $tail; + if ($getunenc) { + $unencsymb = $tail; + } } } - } elsif ($tail =~ m{^/res/$match_domain/$match_username/.+\.(?:sequence|page)(|___\d+___.+)$}) { + } elsif ($tail =~ m{^/?res/$match_domain/$match_username/.+\.(?:sequence|page)(|___\d+___.+)$}) { my $rest = $1; if ($rest eq '') { $scope = 'map'; @@ -504,6 +509,10 @@ } else { $scope = 'resource'; $realuri .= '?symb='.$tail; + $passkey = $tail; + if ($getunenc) { + $unencsymb = $tail; + } } } } elsif ($tail =~ m{^/tiny/$cdom/(\w+)$}) { @@ -528,6 +537,7 @@ } else { $scope = 'resource'; } + $passkey = $symb; if ((&Apache::lonnet::EXT('resource.0.encrypturl',$symb) =~ /^yes$/i) && (!$env{'request.role.adv'})) { $realuri = &Apache::lonenc::encrypted(&Apache::lonnet::clutter($url)); @@ -540,12 +550,63 @@ $realuri .= '?symb='.$symb; } } + if ($getunenc) { + $unencsymb = $symb; + } } - } elsif ($tail =~ m{^/$cdom/$cnum$}) { + } elsif (($tail =~ m{^/$cdom/$cnum$}) || ($tail eq '')) { $scope = 'course'; $realuri = '/adm/navmaps'; + $passkey = $tail; + } + if ($scope eq 'map') { + $passkey = $realuri; + } + if (wantarray) { + return ($scope,$realuri,$unencsymb); + } else { + return $passkey; + } +} + +sub send_grade { + my ($id,$url,$ckey,$secret,$scoretype,$total,$possible) = @_; + my $score; + if ($possible > 0) { + if ($scoretype eq 'ratio') { + $score = Math::Round::round($total).'/'.Math::Round::round($possible); + } elsif ($scoretype eq 'percentage') { + $score = (100.0*$total)/$possible; + $score = Math::Round::round($score); + } else { + $score = $total/$possible; + $score = sprintf("%.2f",$score); + } + } + my $date = &Apache::loncommon::utc_string(time); + my %ltiparams = ( + lti_version => 'LTI-1p0', + lti_message_type => 'basic-lis-updateresult', + sourcedid => $id, + result_resultscore_textstring => $score, + result_resultscore_language => 'en-US', + result_resultvaluesourcedid => $scoretype, + result_statusofresult => 'final', + result_date => $date, + ); + my $hashref = &sign_params($url,$ckey,$secret,\%ltiparams); + if (ref($hashref) eq 'HASH') { + my $request=new HTTP::Request('POST',$url); + $request->content(join('&',map { + my $name = escape($_); + "$name=" . ( ref($hashref->{$_}) eq 'ARRAY' + ? join("&$name=", map {escape($_) } @{$hashref->{$_}}) + : &escape($hashref->{$_}) ); + } keys(%{$hashref}))); + my $response = &LONCAPA::LWPReq::makerequest('',$request,'','',10); + my $message=$response->status_line; +#FIXME Handle case where pass back of score to LTI Consumer failed. } - return ($scope,$realuri); } 1; From raeburn at source.lon-capa.org Thu May 17 19:01:25 2018 From: raeburn at source.lon-capa.org (raeburn) Date: Thu, 17 May 2018 23:01:25 -0000 Subject: [LON-CAPA-cvs] cvs: doc /loncapafiles loncapafiles.lpml Message-ID: raeburn Thu May 17 23:01:25 2018 EDT Modified files: /doc/loncapafiles loncapafiles.lpml Log: - MathJax 2.7.4 Index: doc/loncapafiles/loncapafiles.lpml diff -u doc/loncapafiles/loncapafiles.lpml:1.964 doc/loncapafiles/loncapafiles.lpml:1.965 --- doc/loncapafiles/loncapafiles.lpml:1.964 Wed Jan 31 15:29:28 2018 +++ doc/loncapafiles/loncapafiles.lpml Thu May 17 23:01:25 2018 @@ -2,7 +2,7 @@ "http://lpml.sourceforge.net/DTD/lpml.dtd"> - +