--- loncom/interface/lonfeedback.pm 2012/01/04 18:23:38 1.318 +++ loncom/interface/lonfeedback.pm 2012/01/09 19:56:28 1.330 @@ -1,7 +1,7 @@ # The LearningOnline Network # Feedback # -# $Id: lonfeedback.pm,v 1.318 2012/01/04 18:23:38 www Exp $ +# $Id: lonfeedback.pm,v 1.330 2012/01/09 19:56:28 www Exp $ # # Copyright Michigan State University Board of Trustees # @@ -759,11 +759,17 @@ END sub discussion_link { - my ($ressymb,$linktext,$cmd,$item,$flag,$prev,$adds)=@_; + my ($ressymb,$linktext,$cmd,$item,$flag,$prev,$adds,$title)=@_; my $link='/adm/feedback?inhibitmenu=yes&modal=yes&'.$cmd.'='.&escape($ressymb).':::'.$item; if ($flag) { $link .= '&previous='.$prev; } if ($adds) { $link .= $adds; } - return &Apache::loncommon::modal_link($link,$linktext,600,600); + my $width=600; + my $height=600; + if (($cmd eq 'hide') || ($cmd eq 'unhide') || ($cmd eq 'like') || ($cmd eq 'unlike')) { + $width=300; + $height=200; + } + return &Apache::loncommon::modal_link($link,$linktext,$width,$height,undef,undef,$title); } @@ -781,10 +787,12 @@ sub send_feedback_link { sub send_message_link { my ($ressymb) = @_; my $output = ''. - ' '.&mt('Send Feedback').''; + &discussion_link($ressymb, + ''.&mt('Send Feedback').'', + 'sendmessageonly'). + ''; return $output; } @@ -821,7 +829,17 @@ sub action_links_bar { $discussion .= '&previous='.$prevread; } $discussion .= &group_args($group); - $discussion .= '">'.&mt('Export').''; + $discussion .= '">'.&mt('Export').''; + if (&Apache::lonnet::allowed('rin',$env{'request.course.id'})) { + $discussion .= '  '; + $discussion .=''.&mt('Undelete all deleted entries').''; + } + $discussion.=''; if ($newpostsflag) { if (!$markondisp) { $discussion .=' -
+ $lt{'note'}
-$lt{'title'}: 

+$lt{'title'}: 
ENDDISCUSS if ($env{'form.origpage'}) { @@ -925,16 +943,26 @@ sub build_posting_display { my $skip_group_check = 0; my $symb=&Apache::lonenc::check_decrypt($ressymb); my $escsymb=&escape($ressymb); +# These are the discussion contributions my %contrib=&Apache::lonnet::restore($symb,$env{'request.course.id'}, $env{'course.'.$env{'request.course.id'}.'.domain'}, $env{'course.'.$env{'request.course.id'}.'.num'}); - +# And these are the likes/unlikes + my %likes=&Apache::lonnet::dump('disclikes', + $env{'course.'.$env{'request.course.id'}.'.domain'}, + $env{'course.'.$env{'request.course.id'}.'.num'}, + '^'.$symb.':'); + my $thisuser=$env{'user.name'}.':'.$env{'user.domain'}; +# Array with likes to figure out averages, etc. + my @theselikes=(); +# Is the user allowed to see the real name behind anonymous postings? my $see_anonymous = &Apache::lonnet::allowed('rin',$env{'request.course.id'}.($env{'request.course.sec'}?'/'.$env{'request.course.sec'}:'')); if ((@{$grouppick} == 0) || (grep(/^all$/,@{$grouppick}))) { $skip_group_check = 1; } +# Deletions and hiddens are just lists. Split them up into a hash for quicker lookup my (%deletions,%hiddens); if ($contrib{'deleted'}) { my $deleted = $contrib{'deleted'}; @@ -948,6 +976,7 @@ sub build_posting_display { $hidden =~ s/\.$//; %hiddens = map { $_ => 1 } (split(/\.\./,$hidden)); } +# Versions if store/restore are used to actually store the messages. if ($contrib{'version'}) { my $oldest = $contrib{'1:timestamp'}; if ($prevread eq '0') { @@ -958,11 +987,48 @@ sub build_posting_display { ($skiptest,$roleregexp,$secregexp,$statusregexp) = &filter_regexp($rolefilter,$sectionpick,$statusfilter); $rolematch = $roleregexp.':'.$secregexp.':'.$statusregexp; - } + } +# We need to go through this twice, first to get the likes/dislikes, then to actually build the display + for (my $id=1;$id<=$contrib{'version'};$id++) { + my $idx=$id; + next if ($contrib{$idx.':deleted'}); + next if ($contrib{$idx.':hidden'}); + unless ((($hiddens{$idx}) && (!$seeid)) || ($deletions{$idx}) || (!$contrib{$idx.':message'})) { + push(@theselikes,$likes{$symb.':'.$idx.':likes'}); + } + } +# Figure out average likes and standard deviation if there are enough discussions to warrant that + my $ave=0; + my $stddev=10000; + if ($#theselikes>1) { + my $sum=0; + my $num=$#theselikes+1; + foreach my $thislike (@theselikes) { + $sum+=$thislike; + } + $ave=$sum/$num; + my $sumsq=0; + foreach my $thislike (@theselikes) { + $sumsq+=($thislike-$ave)*($thislike-$ave); + } + $stddev=sqrt($sumsq/$num); + } +# Now we know the average likes $ave and the standard deviation $stddev +# Get the boundaries for markup + my $oneplus=$ave+$stddev; + my $twoplus=$ave+2.*$stddev; + my $oneminus=$ave-$stddev; + my $twominus=$ave-2.*$stddev; +# &Apache::lonnet::logthis(join(',',@theselikes)." Ave $ave StdDev $stddev $twominus $oneminus $oneplus $twoplus"); +# +# This is now the real loop. Go through all entries, pick up what we need +# for (my $id=1;$id<=$contrib{'version'};$id++) { my $idx=$id; next if ($contrib{$idx.':deleted'}); next if ($contrib{$idx.':hidden'}); +# If we get here, we are actually going to display the message - we don't know where and we don't know if we display +# previous edits, but it counts as one entry my $posttime = $contrib{$idx.':timestamp'}; if ($prevread <= $posttime) { $$newpostsflag = 1; @@ -1090,6 +1156,18 @@ sub build_posting_display { @{$$namesort{$lastname}{$firstname}} = ("$idx"); } if ($outputtarget ne 'tex') { + unless ($likes{$symb.':'.$idx.':likers'}=~/\,\Q$thisuser\E\,/) { + $sender.=' '.&discussion_link($symb,''.&mt('Like').'','like',$idx,$$newpostsflag,$prevread,&group_args($group),&mt("Like")); + } + unless ($likes{$symb.':'.$idx.':unlikers'}=~/\,\Q$thisuser\E\,/) { + $sender.=' '.&discussion_link($symb,''.&mt('Unlike').'',,'unlike',$idx,$$newpostsflag,$prevread,&group_args($group),&mt("Unlike")); + } + my $thislikes=$likes{$symb.':'.$idx.':likes'}; + if ($thislikes>0) { + $sender.=' ('.&mt("[_1] likes",$thislikes).')'; + } elsif ($thislikes<0) { + $sender.=' ('.&mt("[_1] unlikes",abs($thislikes)).')'; + } if (&editing_allowed($escsymb.':::'.$idx,$group)) { if (($env{'user.domain'} eq $contrib{$idx.':senderdomain'}) && ($env{'user.name'} eq $contrib{$idx.':sendername'})) { $sender.=' '. @@ -1272,8 +1350,23 @@ sub build_posting_display { if ($$dischash{$toggkey}) { $$discussionitems[$idx].='  '.$ctlink; } +# Figure out size based on likes + my $thislikes=$likes{$symb.':'.$idx.':likes'}; + my $likesize="100"; + if ($thislikes>$twoplus) { + $likesize="200"; + } elsif ($thislikes>$oneplus) { + $likesize="150"; + } + if ($thislikes<$twominus) { + $likesize="50"; + } elsif ($thislikes<$oneminus) { + $likesize="75"; + } $$discussionitems[$idx].= '
'. - $message.'
'; + "
". + $message. + '
'; if ($contrib{$idx.':history'}) { my @postversions = (); $$discussionitems[$idx] .= &mt('This post has been edited by the author.'); @@ -1292,12 +1385,19 @@ sub build_posting_display { $$discussionitems[$idx] .= ''.$version.'. - '.&Apache::lonlocal::locallocaltime($postversions[$i]).' '; } } +# end of unless ($$notshown ...) } +# end of if ($message) ... } +# end of the else-branch of target being export } +# end of unless hidden or deleted } +# end of the loop over all discussion entries } +# end of "if there actually are any discussions } +# end of subroutine "build_posting_display" } sub filter_regexp { @@ -2625,8 +2725,8 @@ sub screen_header { unless (($env{'form.replydisc'}) || ($env{'form.editdisc'})) { if (($feedurl=~/^\/res\//) && ($feedurl!~/^\/res\/adm/) && ($env{'user.adv'})) { $msgoptions= - '

'; + '
'; } my %optionhash=(); foreach my $type ('question','comment','policy') { @@ -2634,20 +2734,20 @@ sub screen_header { } if (&feedback_available(1)) { $msgoptions.= - '

'; + '
'; } if (&feedback_available(0,1)) { $msgoptions.= - '

'; + '
'; } if (&feedback_available(0,0,1)) { $msgoptions.= - '

'; + '
'; } } if (($env{'request.course.id'}) && (!$env{'form.sendmessageonly'})) { @@ -2787,35 +2887,78 @@ sub send_msg { } } -# Records of number of postings, etc, are kept under the "symb" called "_feedback" +# Records of number of feedback messages are kept under the "symb" called "_feedback" # There are two entries within the framework of a course: # - the URLs for which feedback was provided # - the total number of contributions - my %record=&Apache::lonnet::restore('_feedback'); - my ($temp)=keys(%record); - unless ($temp=~/^error\:/) { - my %newrecord=(); - $newrecord{'resource'}=$feedurl; - $newrecord{'subnumber'}=$record{'subnumber'}+1; - unless (&Apache::lonnet::cstore(\%newrecord,'_feedback') eq 'ok') { - $status.='
'.&mt('Not registered').'
'; - } + if ($sendsomething) { + my %record=&getfeedbackrecords(); + my ($temp)=keys(%record); + unless ($temp=~/^error\:/) { + my %newrecord=(); + $newrecord{'resource'}=$feedurl; + $newrecord{'subnumber'}=$record{'subnumber'}+1; + unless (&Apache::lonnet::cstore(\%newrecord,'_feedback') eq 'ok') { + $status.='
'.&mt('Not registered').'
'; + } + } } - return ($status,$sendsomething); } -# Routine to get the complete discussion records +# Routine to get the complete feedback records -sub getdiscrecords { +sub getfeedbackrecords { my ($uname,$udom,$course)=@_; unless ($uname) { $uname=$env{'user.name'}; } unless ($udom) { $udom=$env{'user.domain'}; } unless ($course) { $course=$env{'request.course.id'}; } my %record=&Apache::lonnet::restore('_feedback',$course,$udom,$uname); - return %record; + return %record; +} + +# Routine to get feedback statistics + +sub getfeedbackstats { + my %record=&getfeedbackrecords(@_); + return ($record{'subnumber'},$record{'points'},$record{'totallikes'}); +} + +# Store feedback credit + +sub storefeedbackpoints { + my ($points,$uname,$udom,$course)=@_; + unless ($points) { $points=0; } + unless ($uname) { $uname=$env{'user.name'}; } + unless ($udom) { $udom=$env{'user.domain'}; } + unless ($course) { $course=$env{'request.course.id'}; } + my %record=('grader_user' => $env{'user.name'}, + 'grader_domain' => $env{'user.domain'}, + 'points' => $points); + return &Apache::lonnet::cstore(\%record,'_feedback'); +} + +# Store feedback "likes" + +sub storefeedbacklikes { + my ($likes,$uname,$udom,$course)=@_; + unless ($likes) { $likes=0; } + if ($likes>0) { $likes=1; } + if ($likes<0) { $likes=-1; } + unless ($uname) { $uname=$env{'user.name'}; } + unless ($udom) { $udom=$env{'user.domain'}; } + unless ($course) { $course=$env{'request.course.id'}; } + my %record=&getfeedbackrecords($uname,$udom,$course); + my $totallikes=$record{'totallikes'}; + $totallikes+=$likes; + my %newrecord=('likes_user' => $env{'user.name'}, + 'likes_domain' => $env{'user.domain'}, + 'likes' => $likes, + 'totallikes' => $totallikes); + return &Apache::lonnet::cstore(\%newrecord,'_feedback'); } + sub adddiscuss { my ($symb,$email,$anon,$attachmenturl,$subject,$group)=@_; my $status=''; @@ -2914,7 +3057,7 @@ sub adddiscuss { $env{'course.'.$env{'request.course.id'}.'.domain'}, $env{'course.'.$env{'request.course.id'}.'.num'}); } - my %record=&Apache::lonnet::restore('_discussion'); + my %record=&getdiscussionrecords(); my ($temp)=keys(%record); unless ($temp=~/^error\:/) { my %newrecord=(); @@ -2929,6 +3072,60 @@ sub adddiscuss { return $status.'
'; } + +# Routine to get the complete discussion records + +sub getdiscussionrecords { + my ($uname,$udom,$course)=@_; + unless ($uname) { $uname=$env{'user.name'}; } + unless ($udom) { $udom=$env{'user.domain'}; } + unless ($course) { $course=$env{'request.course.id'}; } + my %record=&Apache::lonnet::restore('_discussion',$course,$udom,$uname); + return %record; +} + +# Routine to get discussion statistics + +sub getdiscussionstats { + my %record=&getdiscussionrecords(@_); + return ($record{'subnumber'},$record{'points'},$record{'totallikes'}); +} + + +# Store discussion credit + +sub storediscussionpoints { + my ($points,$uname,$udom,$course)=@_; + unless ($points) { $points=0; } + unless ($uname) { $uname=$env{'user.name'}; } + unless ($udom) { $udom=$env{'user.domain'}; } + unless ($course) { $course=$env{'request.course.id'}; } + my %record=('grader_user' => $env{'user.name'}, + 'grader_domain' => $env{'user.domain'}, + 'points' => $points); + return &Apache::lonnet::cstore(\%record,'_discussion'); +} + +# Store discussion "likes" + +sub storediscussionlikes { + my ($likes,$uname,$udom,$course)=@_; + unless ($likes) { $likes=0; } + if ($likes>0) { $likes=1; } + if ($likes<0) { $likes=-1; } + unless ($uname) { $uname=$env{'user.name'}; } + unless ($udom) { $udom=$env{'user.domain'}; } + unless ($course) { $course=$env{'request.course.id'}; } + my %record=&getdiscussionrecords($uname,$udom,$course); + my $totallikes=$record{'totallikes'}; + $totallikes+=$likes; + my %newrecord=('likes_user' => $env{'user.name'}, + 'likes_domain' => $env{'user.domain'}, + 'likes' => $likes, + 'totallikes' => $totallikes); + return &Apache::lonnet::cstore(\%newrecord,'_discussion'); +} + sub get_discussion_info { my ($idx,%contrib) = @_; my $changelast = 0; @@ -3457,7 +3654,7 @@ sub handler { # --------------------------- Get query string for limited number of parameters &Apache::loncommon::get_unprocessed_cgi($ENV{'QUERY_STRING'}, - ['modal','hide','unhide','deldisc','postdata','preview','replydisc','editdisc','cmd','symb','onlyunread','allposts','onlyunmark','previous','markread','markonread','markondisp','toggoff','toggon','modifydisp','changes','navtime','navmaps','navurl','sortposts','applysort','rolefilter','statusfilter','sectionpick','groupick','posterlist','userpick','attach','origpage','currnewattach','deloldattach','keepold','allversions','export','sendmessageonly','group','ref']); + ['like','unlike','modal','hide','unhide','deldisc','undeleteall','postdata','preview','replydisc','editdisc','cmd','symb','onlyunread','allposts','onlyunmark','previous','markread','markonread','markondisp','toggoff','toggon','modifydisp','changes','navtime','navmaps','navurl','sortposts','applysort','rolefilter','statusfilter','sectionpick','groupick','posterlist','userpick','attach','origpage','currnewattach','deloldattach','keepold','allversions','export','sendmessageonly','group','ref']); my $group = $env{'form.group'}; my %attachmax = ( text => &mt('(128 KB max size)'), @@ -3732,6 +3929,81 @@ ENDREDIR '0','0','','',$env{'form.previous'},undef,undef,undef, undef,undef,undef,$group); return OK; + } elsif (($env{'form.like'}) || ($env{'form.unlike'})) { +# ----------------------------------------------------------------- Like/unlike + my $entry=$env{'form.like'}?$env{'form.like'}:$env{'form.unlike'}; + my ($symb,$idx)=split(/\:\:\:/,$entry); + ($symb,my $feedurl)=&get_feedurl_and_clean_symb($symb); +# +# Likes and unlikes are in db-file "disclikes" of the course +# The prefix is the $symb to identify the resource discussion, +# and the $idx to identify the entry +# + my $prefix=$symb.':'.$idx.':'; + my %contrib=&Apache::lonnet::dump('disclikes', + $env{'course.'.$env{'request.course.id'}.'.domain'}, + $env{'course.'.$env{'request.course.id'}.'.num'}, + '^'.$prefix); +# Get all who like or unlike this + my $currentlikers=$contrib{$prefix.'likers'}; + my $currentunlikers=$contrib{$prefix.'unlikers'}; +# Get the current "likes" count + my $likes=$contrib{$prefix.'likes'}; +# Find out if they already voted +# Users cannot like a post twice, or unlike it twice. They can change their mind, though + my $alreadyflag=0; + my $thisuser=$env{'user.name'}.':'.$env{'user.domain'}; + if ($env{'form.like'}) { + if ($currentlikers=~/\,\Q$thisuser\E\,/) { + $alreadyflag=1; + } else { + if ($currentunlikers=~/\,\Q$thisuser\E\,/) { + $currentunlikers=~s/\,\Q$thisuser\E\,//g; + } else { + $currentlikers.=','.$thisuser.','; + } + $likes++; + } + } else { + if ($currentunlikers=~/\,\Q$thisuser\E\,/) { + $alreadyflag=1; + } else { + if ($currentlikers=~/\,\Q$thisuser\E\,/) { + $currentlikers=~s/\,\Q$thisuser\E\,//g; + } else { + $currentunlikers.=','.$thisuser.','; + } + $likes--; + } + } + my $result; +# $alreadyflag would be 1 if they tried to double-like or double-unlike + unless ($alreadyflag) { + my %newhash=($prefix.'likes' => $likes, + $prefix.'likers' => $currentlikers, + $prefix.'unlikers' => $currentunlikers); +# Store data in db-file "disclikes" + if (&Apache::lonnet::put('disclikes', + \%newhash, + $env{'course.'.$env{'request.course.id'}.'.domain'}, + $env{'course.'.$env{'request.course.id'}.'.num'}) eq 'ok') { +# Also store with the person who posted the liked/unliked entry + if ($env{'form.like'}) { + &storediscussionlikes(1,$contrib{$idx.':sendername'},$contrib{$idx.':senderdomain'}); + $result=&mt("Registered 'Like'"); + } else { + &storediscussionlikes(-1,$contrib{$idx.':sendername'},$contrib{$idx.':senderdomain'}); + $result=&mt("Registered 'Unlike'"); + } + } else { +# Oops, something went wrong + $result=&mt("Failed to register vote"); + } + } + &redirect_back($r,$feedurl,$result.'
', + '0','0','','',$env{'form.previous'},undef,undef,undef, + undef,undef,undef,$group); + return OK; } elsif ($env{'form.cmd'}=~/^(threadedoff|threadedon)$/) { my ($symb,$feedurl)=&get_feedurl_and_clean_symb($env{'form.symb'}); if ($env{'form.cmd'} eq 'threadedon') { @@ -3815,6 +4087,24 @@ ENDREDIR &Apache::loncommon::end_page(); $r->print($start_page.$discussion.$end_page); return OK; + + } elsif ($env{'form.undeleteall'}) { + &Apache::loncommon::content_type($r,'text/html'); + $r->send_http_header; + my ($symb,$feedurl) = &get_feedurl_and_clean_symb($env{'form.undeleteall'}); + $r->print(&Apache::loncommon::start_page('Undelete all deleted discussion entries')); + if (&Apache::lonnet::allowed('rin',$env{'request.course.id'})) { + if (&Apache::lonnet::store({'deleted' => ''},$symb,$env{'request.course.id'}, + $env{'course.'.$env{'request.course.id'}.'.domain'}, + $env{'course.'.$env{'request.course.id'}.'.num'}) eq 'ok') { + $r->print(&Apache::lonhtmlcommon::confirm_success(&mt("Undeleted all entries"))); + } else { + $r->print(&Apache::lonhtmlcommon::confirm_success(&mt("Failed to undelete entries"),1)); + } + $r->print("
".&mt("Return and reload").""); + } + $r->print(&Apache::loncommon::end_page()); + return OK; } else { # ------------------------------------------------------------- Normal feedback my $feedurl=$env{'form.postdata'}; @@ -3830,6 +4120,8 @@ ENDREDIR $symb=(split(/\:\:\:/,$env{'form.editdisc'}))[0]; } elsif ($env{'form.origpage'}) { $symb=""; + } elsif ($env{'form.sendmessageonly'}) { + $symb=(split(/\:\:\:/,$env{'form.sendmessageonly'}))[0]; } else { $symb=&Apache::lonnet::symbread($feedurl); }