--- loncom/interface/statistics/lonproblemstatistics.pm 2002/10/30 18:37:00 1.34 +++ loncom/interface/statistics/lonproblemstatistics.pm 2013/12/30 14:05:21 1.122.2.2 @@ -1,7 +1,6 @@ # The LearningOnline Network with CAPA -# (Publication Handler # -# $Id: lonproblemstatistics.pm,v 1.34 2002/10/30 18:37:00 minaeibi Exp $ +# $Id: lonproblemstatistics.pm,v 1.122.2.2 2013/12/30 14:05:21 raeburn Exp $ # # Copyright Michigan State University Board of Trustees # @@ -26,843 +25,1805 @@ # http://www.lon-capa.org/ # # (Navigate problems for statistical reports -# YEAR=2001 -# 5/5,7/9,7/25/1,8/11,9/13,9/26,10/5,10/9,10/22,10/26 Behrouz Minaei -# 11/1,11/4,11/16,12/14,12/16,12/18,12/20,12/31 Behrouz Minaei -# YEAR=2002 -# 1/22,2/1,2/6,2/25,3/2,3/6,3/17,3/21,3/22,3/26,4/7,5/6 Behrouz Minaei -# 5/12,5/14,5/15,5/19,5/26,7/16,7/25,7/29,8/5 Behrouz Minaei # -### +############################################### +############################################### -package Apache::lonproblemstatistics; +=pod + +=head1 NAME + +lonproblemstatistics + +=head1 SYNOPSIS + +Routines to present problem statistics to instructors via tables, +Excel files, and plots. + +=over 4 + +=cut + +############################################### +############################################### + +package Apache::lonproblemstatistics; use strict; -use Apache::lonnet(); +use Apache::lonnet; +use Apache::loncommon(); use Apache::lonhtmlcommon; use Apache::loncoursedata; -use GDBM_File; - -my $jr; - -sub InitializeProblemStatistics { - my ($cacheDB, $students, $courseID, $c, $r)=@_; - my %cache; - - $jr = $r; - - unless(tie(%cache,'GDBM_File',$cacheDB,&GDBM_READER(),0640)) { - $r->print('Unable to tie database1.'); - return ('ERROR', undef); - } - - # Remove students who don't have the proper section. - my @sectionsSelected = split(':',$cache{'sectionsSelected'}); - for(my $studentIndex=((scalar @$students)-1); $studentIndex>=0; - $studentIndex--) { - my $value = $cache{$students->[$studentIndex].':section'}; - my $found = 0; - foreach (@sectionsSelected) { - if($_ eq 'none') { - if($value eq '' || !defined($value) || $value eq ' ') { - $found = 1; - last; - } - } else { - if($value eq $_) { - $found = 1; - last; - } +use Apache::lonstatistics; +use LONCAPA::lonmetadata(); +use Apache::lonlocal; +use Spreadsheet::WriteExcel; +use Apache::lonstathelpers(); +use Time::HiRes; +use LONCAPA; + + +my @StatsArray; +my %SeqStat; # keys are symbs, values are hash refs + +## +## Localization notes: +## +## in @Fields[0]->{'long_title'} is placed in Excel files and is used as the +## header for plots created with Graph.pm, both of which more than likely do +## not support localization. +## +## Additional Notes: +## Localization can be done and is done before passing the phrases +## to the output. +## This might conflict with special characters, e.g. German Umlaute or +## chinese characters. Do not use such characters in this case. +## If this failed, consider that the sequence and folder names +## are also passed to the output and would fail the same way. +## +# +# +## +## Description of Field attributes +## +## Attribute Required Value Meaning or Use +## +## name yes any scalar Used to uniquely identify field +## title yes any scalar This is what the user sees to identify +## the field. Passed through &mt(). +## long_title yes any scalar Used as graph heading and in excel +## output. Passed through &mt(). +## align no (left|right|center) HTML cell contents alignment +## color yes html color HTML cell background color +## used to visually group statistics +## special no (link) Indicates a link, target is name.link +## Currently set in &get_statistics() +## graphable no (yes|no) Can a bar graph of the field be +## produced? +## sortable no (yes|no) Should a sort link be put in the +## column header? +## selectable yes (yes|no) Can the column be removed from the +## statistics display? +## selected yes (yes|no) Is the column selected by default? +## +## format no sprintf format string +## +## excel_format no excel format type +## (see &Apache::loncommon::define_excel_formats +my @Fields = ( + { name => 'problem_num', + title => 'P#', + align => 'right', + color => '#FFFFE6', + selectable => 'no', + defaultselected => 'yes', + }, + { name => 'container', + title => 'Sequence or Folder', + align => 'left', + color => '#FFFFE6', + sortable => 'yes', + selectable => 'no', + defaultselected => 'yes', + }, + { name => 'title', + title => 'Title', + align => 'left', + color => '#FFFFE6', + special => 'link', + sortable => 'yes', + selectable => 'no', + defaultselected => 'yes', + }, + { name => 'part', + title => 'Part', + align => 'left', + color => '#FFFFE6', + selectable => 'no', + defaultselected => 'yes', + }, + { name => 'num_students', + title => '#Stdnts', + align => 'right', + color => '#EEFFCC', + format => '%d', + sortable => 'yes', + graphable => 'yes', + long_title => 'Number of Students Attempting Problem', + selectable => 'yes', + defaultselected => 'yes', + }, + { name => 'tries', + title => 'Tries', + align => 'right', + color => '#EEFFCC', + format => '%d', + sortable => 'yes', + graphable => 'yes', + long_title => 'Total Number of Tries', + selectable => 'yes', + defaultselected => 'yes', + }, + { name => 'max_tries', + title => 'Max Tries', + align => 'right', + color => '#DDFFFF', + format => '%d', + sortable => 'yes', + graphable => 'yes', + long_title => 'Maximum Number of Tries', + selectable => 'yes', + defaultselected => 'yes', + }, + { name => 'min_tries', + title => 'Min Tries', + align => 'right', + color => '#DDFFFF', + format => '%d', + sortable => 'yes', + graphable => 'yes', + long_title => 'Minumum Number of Tries', + selectable => 'yes', + defaultselected => 'yes', + }, + { name => 'mean_tries', + title => 'Mean Tries', + align => 'right', + color => '#DDFFFF', + format => '%5.2f', + sortable => 'yes', + graphable => 'yes', + long_title => 'Average Number of Tries', + selectable => 'yes', + defaultselected => 'yes', + }, + { name => 'std_tries', + title => 'S.D. tries', + align => 'right', + color => '#DDFFFF', + format => '%5.2f', + sortable => 'yes', + graphable => 'yes', + long_title => 'Standard Deviation of Number of Tries', + selectable => 'yes', + defaultselected => 'yes', + }, + { name => 'skew_tries', + title => 'Skew Tries', + align => 'right', + color => '#DDFFFF', + format => '%5.2f', + sortable => 'yes', + graphable => 'yes', + long_title => 'Skew of Number of Tries', + selectable => 'yes', + defaultselected => 'no', + }, + { name => 'num_solved', + title => '#YES', + align => 'right', + color => '#FFDDDD', + format => '%4.1f',# format => '%d', + sortable => 'yes', + graphable => 'yes', + long_title => 'Number of Students able to Solve', + selectable => 'yes', + defaultselected => 'yes', + }, + { name => 'num_override', + title => '#yes', + align => 'right', + color => '#FFDDDD', + format => '%4.1f',# format => '%d', + sortable => 'yes', + graphable => 'yes', + long_title => 'Number of Students given Override', + selectable => 'yes', + defaultselected => 'yes', + }, + { name => 'tries_per_correct', + title => 'tries/correct', + align => 'right', + color => '#FFDDDD', + format => '%4.1f', + sortable => 'yes', + graphable => 'yes', + long_title => 'Tries per Correct Answer', + selectable => 'yes', + defaultselected => 'yes', + }, + { name => 'num_wrong', + title => '#Wrng', + align => 'right', + color => '#FFDDDD', + format => '%4.1f', + sortable => 'yes', + graphable => 'yes', + long_title => 'Number of students whose final answer is wrong', + selectable => 'yes', + defaultselected => 'yes', + }, + { name => 'per_wrong', + title => '%Wrng', + align => 'right', + color => '#FFDDDD', + format => '%4.1f', + sortable => 'yes', + graphable => 'yes', + long_title => 'Percent of students whose final answer is wrong', + selectable => 'yes', + defaultselected => 'yes', + }, + { name => 'deg_of_diff', + title => 'DoDiff', + align => 'right', + color => '#FFFFE6', + format => '%5.2f', + sortable => 'yes', + graphable => 'yes', + long_title => 'Degree of Difficulty'. + ' ~[ 1 - ((#YES+#yes) / Tries) ~]', + selectable => 'yes', + defaultselected => 'yes', + }, + { name => 'deg_of_disc', + title => 'DoDisc', + align => 'right', + color => '#FFFFE6', + format => '%4.2f', + sortable => 'yes', + graphable => 'yes', + long_title => 'Degree of Discrimination', + selectable => 'yes', + defaultselected => 'yes', + }, +## duedate included for research purposes. Commented out most of the time. +# { name => 'duedate', +# title => 'Due Date', +# align => 'left', +# color => '#FFFFFF', +# sortable => 'yes', +# graphable => 'no', +# long_title => 'Due date of resource for instructor', +# selectable => 'no', +# defaultselected => 'yes', +# }, +## opendate included for research purposes. Commented out most of the time. +# { name => 'opendate', +# title => 'Open Date', +# align => 'left', +# color => '#FFFFFF', +# sortable => 'yes', +# graphable => 'no', +# long_title => 'date resource became answerable', +# selectable => 'no', +# defaultselected => 'yes', +# }, +## symb included for research purposes. Commented out most of the time. +# { name => 'symb', +# title => 'Symb', +# align => 'left', +# color => '#FFFFFF', +# sortable => 'yes', +# graphable => 'no', +# long_title => 'Unique LON-CAPA identifier for problem', +# selectable => 'no', +# defaultselected => 'yes', +# }, +## resptypes included for research purposes. Commented out most of the time. +# { name => 'resptypes', +# title => 'Response Types', +# align => 'left', +# color => '#FFFFFF', +# sortable => 'no', +# graphable => 'no', +# long_title => 'Response Types used in this problem', +# selectable => 'no', +# defaultselected => 'yes', +# }, +## maxtries included for research purposes. Commented out most of the time. +# { name => 'maxtries', +# title => 'Maxtries', +# align => 'left', +# color => '#FFFFFF', +# sortable => 'no', +# graphable => 'no', +# long_title => 'Maximum number of tries', +# selectable => 'no', +# defaultselected => 'yes', +# }, +## hinttries included for research purposes. Commented out most of the time. +# { name => 'hinttries', +# title => 'hinttries', +# align => 'left', +# color => '#FFFFFF', +# sortable => 'no', +# graphable => 'no', +# long_title => 'Number of tries before a hint appears', +# selectable => 'no', +# defaultselected => 'yes', +# }, +# +## problem weight for instructor + { name => 'weight', + title => 'weight', + align => 'right', + color => '#FFFFFF', + sortable => 'no', + graphable => 'no', + long_title => 'Problem weight (for instructor)', + selectable => 'yes', + defaultselected => 'yes', + }, +); + +my @SeqFields = ( + { name => 'title', + title => 'Sequence', + align => 'left', + color => '#FFFFE6', + special => 'no', + sortable => 'no', + selectable => 'yes', + defaultselected => 'no', + }, + { name => 'items', + title => '#Items', + align => 'right', + color => '#FFFFE6', + format => '%4d', + sortable => 'no', + graphable => 'no', + long_title => 'Number of Items in Sequence', + selectable => 'yes', + defaultselected => 'no', + }, + { name => 'scoremean', + title => 'Score Mean', + align => 'right', + color => '#FFFFE6', + format => '%4.2f', + sortable => 'no', + graphable => 'no', + long_title => 'Mean Sequence Score', + selectable => 'yes', + defaultselected => 'no', + }, + { name => 'scorestd', + title => 'Score STD', + align => 'right', + color => '#FFFFE6', + format => '%4.2f', + sortable => 'no', + graphable => 'no', + long_title => 'Standard Deviation of Sequence Scores', + selectable => 'yes', + defaultselected => 'no', + }, + { name => 'scoremax', + title => 'Score Max', + align => 'right', + color => '#FFFFE6', + format => '%4.2f', + sortable => 'no', + graphable => 'no', + long_title => 'Maximum Sequence Score', + selectable => 'yes', + defaultselected => 'no', + }, + { name => 'scoremin', + title => 'Score Min', + align => 'right', + color => '#FFFFE6', + format => '%4.2f', + sortable => 'no', + graphable => 'no', + long_title => 'Minumum Sequence Score', + selectable => 'yes', + defaultselected => 'no', + }, + { name => 'scorecount', + title => 'Score N', + align => 'right', + color => '#FFFFE6', + format => '%4d', + sortable => 'no', + graphable => 'no', + long_title => 'Number of Students in score computations', + selectable => 'yes', + defaultselected => 'no', + }, + { name => 'countmean', + title => 'Count Mean', + align => 'right', + color => '#FFFFFF', + format => '%4.2f', + sortable => 'no', + graphable => 'no', + long_title => 'Mean Sequence Score', + selectable => 'yes', + defaultselected => 'no', + }, + { name => 'countstd', + title => 'Count STD', + align => 'right', + color => '#FFFFFF', + format => '%4.2f', + sortable => 'no', + graphable => 'no', + long_title => 'Standard Deviation of Sequence Scores', + selectable => 'yes', + defaultselected => 'no', + }, + { name => 'countmax', + title => 'Count Max', + align => 'right', + color => '#FFFFFF', + format => '%4.2f', + sortable => 'no', + graphable => 'no', + long_title => 'Maximum Number of Correct Problems', + selectable => 'yes', + defaultselected => 'no', + }, + { name => 'countmin', + title => 'Count Min', + align => 'right', + color => '#FFFFFF', + format => '%4.2f', + sortable => 'no', + graphable => 'no', + long_title => 'Minumum Number of Correct Problems', + selectable => 'yes', + defaultselected => 'no', + }, + { name => 'count', + title => 'Count N', + align => 'right', + color => '#FFFFFF', + format => '%4d', + sortable => 'no', + graphable => 'no', + long_title => 'Number of Students in score computations', + selectable => 'yes', + defaultselected => 'no', + }, + { name => 'KR-21', + title => 'KR-21', + align => 'right', + color => '#FFAAAA', + format => '%4.2f', + sortable => 'no', + graphable => 'no', + long_title => 'KR-21 reliability statistic', + selectable => 'yes', + defaultselected => 'no', + }, +); + +my %SelectedFields; + +sub parse_field_selection { + # + # Pull out the defaults + if (! defined($env{'form.fieldselections'})) { + $env{'form.fieldselections'} = []; + foreach my $field (@Fields) { + next if ($field->{'selectable'} ne 'yes'); + if ($field->{'defaultselected'} eq 'yes') { + push(@{$env{'form.fieldselections'}},$field->{'name'}); } } - if($found == 0) { - splice(@$students, $studentIndex, 1); - } - } - - my $isNotCached = 0; - my $lastStatus = (defined($cache{'StatisticsLastStatus'})) ? - $cache{'StatisticsLastStatus'} : 'Nothing'; - my $whichStudents = join(':::',sort(@$students)); - if(!defined($cache{'StatisticsCached'}) || - $lastStatus ne $cache{'Status'} || - $whichStudents ne $cache{'StatisticsWhichStudents'}) { - $isNotCached = 1; } - - untie(%cache); - unless(tie(%cache,'GDBM_File',$cacheDB,&GDBM_WRCREAT(),0640)) { - $r->print('Unable to tie database.2'); - return ('ERROR', undef); - } - if($isNotCached && defined($cache{'StatisticsCached'})) { - my @statkeys = split(':::', $cache{'StatisticsKeys'}); - delete $cache{'StatisticsKeys'}; - delete $cache{'StatisticsCached'}; - foreach(@statkeys) { - delete $cache{$_}; + # + # Make sure the data we are plotting is there + my %NeededFields; + if (exists($env{'form.plot'}) && $env{'form.plot'} ne '' && + $env{'form.plot'} ne 'none') { + if ($env{'form.plot'} eq 'degrees') { + $NeededFields{'deg_of_diff'}++; + $NeededFields{'deg_of_disc'}++; + } elsif ($env{'form.plot'} eq 'tries statistics') { + $NeededFields{'mean_tries'}++; + $NeededFields{'std_tries'}++; + $NeededFields{'problem_num'}++; + } else { + $NeededFields{$env{'form.plot'}}++; } } - - untie(%cache); - if($isNotCached) { - &Apache::loncoursedata::DownloadStudentCourseDataSeparate($students, - 'true', - $cacheDB, - 'true', - 'true', - $courseID, - $r, $c); - } - if($c->aborted()) { return ('ERROR', undef); } - - unless(tie(%cache,'GDBM_File',$cacheDB,&GDBM_READER(),0640)) { - $r->print('Unable to tie database.3'); - return ('ERROR', undef); + # + # This should not happen, but in case it does... + if (ref($env{'form.fieldselections'}) ne 'ARRAY') { + $env{'form.fieldselections'} = [$env{'form.fieldselections'}]; + } + # + # Set the field data and the selected fields (for easier checking) + undef(%SelectedFields); + foreach my $field (@Fields) { + if ($field->{'selectable'} ne 'yes') { + $field->{'selected'} = 'yes'; + } else { + $field->{'selected'} = 'no'; + } + if (exists($NeededFields{$field->{'name'}})) { + $field->{'selected'} = 'yes'; + $SelectedFields{$field->{'name'}}++; + } + foreach my $selection (@{$env{'form.fieldselections'}}) { + if ($selection eq $field->{'name'} || $selection eq 'all') { + $field->{'selected'} = 'yes'; + $SelectedFields{$field->{'name'}}++; + } + } } - my $problemData; - if($isNotCached) { - ($problemData) = &ExtractStudentData(\%cache, $students); - &CalculateStatistics($problemData, \%cache, $courseID); + # + # Always show all the sequence statistics (for now) + foreach my $field (@SeqFields) { + $field->{'selected'} = 'yes'; } - untie(%cache); + return; +} - unless(tie(%cache,'GDBM_File',$cacheDB,&GDBM_WRCREAT(),0640)) { - $r->print('Unable to tie database.4'); - return ('ERROR', undef); - } - if($isNotCached) { - foreach(keys(%$problemData)) { - $cache{$_} = $problemData->{$_}; +sub field_selection_input { + my $Str = '\n"; +} - unless(tie(%cache,'GDBM_File',$cacheDB,&GDBM_READER(),0640)) { - $r->print('Unable to tie database.5'); - return ('ERROR', undef); - } +############################################### +############################################### + +=pod - my $orderedProblems = &SortProblems(\%cache, - $cache{'ProblemStatisticsSort'}, - $cache{'SortProblems'}, - $cache{'ProblemStatisticsAscend'}); - untie(%cache); +=item &CreateInterface() - return ('OK', $orderedProblems); +Create the main intereface for the statistics page. Allows the user to +select sections, maps, and output. + +=cut + +############################################### +############################################### +sub CreateInterface { + my ($r) = @_; + # + &parse_field_selection(); + # + my $Str = ''; + $Str .= '

'; + $Str .= &Apache::loncommon::start_data_table(); + $Str .= &Apache::loncommon::start_data_table_header_row(); + $Str .= ''.&mt('Sections').''; + $Str .= ''.&mt('Groups').''; + $Str .= ''.&mt('Access Status').''; + $Str .= ''.&mt('Sequences and Folders').''; + $Str .= ''.&mt('Statistics').''; + $Str .= ''.&mt('Plot Graph').''; + $Str .= ''.&mt('Time Period').''; + $Str .= &Apache::loncommon::end_data_table_header_row(); + # + $Str .= &Apache::loncommon::start_data_table_row(); + $Str .= ''."\n"; + $Str .= &Apache::lonstatistics::SectionSelect('Section','multiple',5); + $Str .= ''; + $Str .= &Apache::lonstatistics::GroupSelect('Group','multiple',5); + $Str .= ''; + $Str .= &Apache::lonhtmlcommon::StatusOptions(undef,undef,5); + $Str .= ''; + # + $Str .= &Apache::lonstatistics::map_select('Maps','multiple,all',5); + $Str .= ''; + $Str .= &field_selection_input(); + $Str .= ''; + $Str .= &plot_dropdown(); + $Str .= ''."\n"; + $Str .= ''; + $Str .= &Apache::lonstathelpers::limit_by_time_form(); + $Str .= ''."\n"; + $Str .= &Apache::loncommon::end_data_table_row(); + $Str .= &Apache::loncommon::end_data_table(); + # + $Str .= '

'; + $Str .= ''; + $Str .= (' 'x10); + # + return $Str; } -sub BuildProblemStatisticsPage { - my ($cacheDB, $students, $courseID, $c, $r)=@_; +############################################### +############################################### + +=pod + +=item &BuildProblemStatisticsPage() - my @Header = ("Homework Sets Order","#Stdnts","Tries","Mod", - "Mean","#YES","#yes","%Wrng","DoDiff", - "S.D.","Skew.","D.F.1st","D.F.2nd"); - my $color=&setbgcolor(0); - my %cache; +Main interface to problem statistics. - unless(tie(%cache,'GDBM_File',$cacheDB,&GDBM_READER(),0640)) { - $r->print('Unable to tie database.6'); +=cut + +############################################### +############################################### +my $navmap; +my @sequences; + +sub clean_up { + undef($navmap); + undef(@sequences); +} + +sub BuildProblemStatisticsPage { + my ($r,$c)=@_; + undef($navmap); + undef(@sequences); + # + my %Saveable_Parameters = ('Status' => 'scalar', + 'statsoutputmode' => 'scalar', + 'Section' => 'array', + 'Groups' => 'array', + 'StudentData' => 'array', + 'Maps' => 'array', + 'fieldselections'=> 'array'); + &Apache::loncommon::store_course_settings('statistics', + \%Saveable_Parameters); + &Apache::loncommon::restore_course_settings('statistics', + \%Saveable_Parameters); + # + &Apache::lonstatistics::PrepareClasslist(); + # + # Clear the package variables + undef(@StatsArray); + undef(%SeqStat); + # + # Finally let the user know we are here + $r->print(&Apache::lonhtmlcommon::breadcrumbs('Overall Problem Statistics', + 'Statistics_Overall_Key')); + + my $interface = &CreateInterface($r); + $r->print($interface); + $r->print(''); + # + my @CacheButtonHTML = + &Apache::lonstathelpers::manage_caches($r,'Statistics','stats_status'); + my $Str; + foreach my $html (@CacheButtonHTML) { + $Str.=$html.(' 'x5); + } + # + $r->print($Str); + if (! exists($env{'form.firstrun'})) { + $r->print('

'. + &mt('Press "Generate Statistics" when you are ready.'). + '

'. + '

'. + &mt('It may take some time to update the student data '. + 'for the first analysis. Future analysis this session '. + 'will not have this delay.'). + '

'); + &clean_up(); return; } - my $Ptr = ''; - $Ptr .= ''; - $Ptr .= ''."\n"; - $Ptr .= ''."\n"; - $Ptr .= ''."\n"; - $Ptr .= ''."\n"; - $Ptr .= ''."\n"; - $Ptr .= ''."\n"; - $Ptr .= &ProblemStatisticsButtons($cache{'DisplayFormat'}, - $cache{'DisplayLegend'}, - $cache{'SortProblems'}); - $Ptr .= '
Select Map'; - $Ptr .= &Apache::lonhtmlcommon::MapOptions(\%cache, 'Statistics', - 'Statistics'); - $Ptr .= '
Sorting Type:'."\n"; - $Ptr .= &Apache::lonhtmlcommon::AscendOrderOptions( - $cache{'ProblemStatisticsAscend'}, - 'ProblemStatistics', - 'Statistics'); - $Ptr .= '
Select Sections'; - $Ptr .= ''."\n"; - my @sections = split(':',$cache{'sectionList'}); - my @sectionsSelected = split(':',$cache{'sectionsSelected'}); - $Ptr .= &Apache::lonhtmlcommon::MultipleSectionSelect(\@sections, - \@sectionsSelected, - 'Statistics'); - $Ptr .= '
'; - if($cache{'DisplayLegend'} eq 'Show Legend') { - $Ptr .= &ProblemStatisticsLegend(); - } - $r->print($Ptr); $r->rflush(); - untie(%cache); - - my ($result, $orderedProblems) = - &InitializeProblemStatistics($cacheDB, $students, $courseID, $c, $r); - if($result ne 'OK') { + # + # This probably does not need to be done each time we are called, but + # it does not slow things down noticably. + &Apache::loncoursedata::populate_weight_table(); + # + ($navmap,@sequences) = + &Apache::lonstatistics::selected_sequences_with_assessments(); + if (! ref($navmap)) { + $r->print('
'.&mt('A course-wide error occurred.').'
'. + '

'.$navmap.'

'); + &clean_up(); return; } - - unless(tie(%cache,'GDBM_File',$cacheDB,&GDBM_READER(),0640)) { - $r->print('Unable to tie database.6'); - return; + if (exists($env{'form.Excel'})) { + $r->print('

'. + &Apache::lonstatistics::section_and_enrollment_description(). + '

'); + &Excel_output($r); + } else { + $r->print(''.' 'x5); + $r->rflush(); + $r->print('

'. + &Apache::lonstatistics::section_and_enrollment_description(). + '

'); + my $count = 0; + foreach my $seq (@sequences) { + my @resources = + &Apache::lonstathelpers::get_resources($navmap,$seq); + $count += scalar(@resources); + } + if ($count > 10) { + $r->print('

'. + &mt('Compiling statistics for [quant,_1,problem]',$count). + '

'); + if ($count > 30) { + $r->print('

'.&mt('This will take some time.').'

'); + } + $r->rflush(); + } + # + my $sortby = $env{'form.sortby'}; + $sortby = 'container' if (! defined($sortby) || $sortby =~ /^\s*$/); + my $plot = $env{'form.plot'}; + if ($plot eq '' || $plot eq 'none') { + undef($plot); + } + if ($sortby eq 'container' && ! defined($plot)) { + &output_sequence_statistics($r); + &output_html_by_sequence($r); + } else { + if (defined($plot)) { + &make_plot($r,$plot); + } + &output_html_stats($r); + &output_sequence_statistics($r); + } } - &BuildStatisticsTable(\%cache, $cache{'DisplayFormat'}, - $cache{'SortProblems'}, $orderedProblems, - \@Header, $r, $color); - untie(%cache); - + &clean_up(); return; } -sub BuildGraphicChart { - my ($graph,$cacheDB,$courseDescription,$students,$courseID,$r,$c)=@_; - my %cache; - my $max = 0; - unless(tie(%cache,'GDBM_File',$cacheDB,&GDBM_READER(),0640)) { - return 'Unable to tie database.7'; +sub output_sequence_statistics { + my ($r) = @_; + my $c=$r->connection(); + $r->print('

'.&mt('Sequence Statistics'). + &Apache::loncommon::help_open_topic('Statistics_Sequence'). + '

'); + $r->print(&Apache::loncommon::start_data_table().&Apache::loncommon::start_data_table_header_row()); + $r->print(&sequence_html_header()); + $r->print(&Apache::loncommon::end_data_table_header_row()); + foreach my $seq (@sequences) { + last if ($c->aborted); + &compute_sequence_statistics($seq); + $r->print(&sequence_html_output($seq)); } + $r->print(&Apache::loncommon::end_data_table()); + $r->rflush(); + return; +} -# my @problems = split(':::', $cache{'problemList'}); - my $title = ''; - if($graph eq 'DoDiffGraph') { - $title = 'Degree-of-Difficulty'; - } else { - $title = 'Wrong-Percentage'; +########################################################## +########################################################## +## +## HTML output routines +## +########################################################## +########################################################## +sub output_html_by_sequence { + my ($r) = @_; + my $c = $r->connection(); + $r->print('
'.&html_preamble()); + # + foreach my $seq (@sequences) { + last if ($c->aborted); + $r->print("

".$seq->compTitle."

". + &Apache::loncommon::start_data_table(). + &Apache::loncommon::start_data_table_header_row(). + &statistics_table_header('no container'). + &Apache::loncommon::end_data_table_header_row()."\n"); + my @Data = &compute_statistics_on_sequence($seq); + foreach my $data (@Data) { + $r->print(&Apache::loncommon::start_data_table_row(). + &statistics_html_table_data($data,'no container'). + &Apache::loncommon::end_data_table_row()."\n"); + } + $r->print(&Apache::loncommon::end_data_table()."\n"); + $r->rflush(); } + return; +} +sub output_html_stats { + my ($r)=@_; + &compute_all_statistics($r); + $r->print(&html_preamble()); + &sort_data($env{'form.sortby'}); + # + my $count=0; + foreach my $data (@StatsArray) { + if ($count++ % 50 == 0) { + $r->print(&Apache::loncommon::end_data_table()); + $r->print(&Apache::loncommon::start_data_table(). + &Apache::loncommon::start_data_table_row(). + &statistics_table_header(). + &Apache::loncommon::end_data_table_row()); + } + $r->print(&Apache::loncommon::start_data_table_row(). + &statistics_html_table_data($data). + &Apache::loncommon::end_data_table_row()); + } + $r->print(&Apache::loncommon::end_data_table_row()); + return; +} - # foreach (@problems) { - # my $data = 0; - # if($graph eq 'DoDiffGraph') { - # $data = sprintf("%.2f", $cache{$_.':degreeOfDifficulty'}), - # } else { - # $data = sprintf("%.1f", $cache{$_.':percentWrong'}), - # } - # if($max < $data) { - # $max = $data; - # } - # push(@values, $data); - # } - - - my $count = 1; - my $currentSequence = -1; - my $sortProblems = 'Sort Within Sequence'; - - my ($result, $orderedProblems) = - &InitializeProblemStatistics($cacheDB, $students, $courseID, $c, $r); - if($result ne 'OK') { - return; +sub html_preamble { + my $Str=''; + $Str .= "

". + $env{'course.'.$env{'request.course.id'}.'.description'}. + "

\n"; + my ($starttime,$endtime) = &Apache::lonstathelpers::get_time_limits(); + if (defined($starttime) || defined($endtime)) { + # Inform the user what the time limits on the data are. + $Str .= '

'.&mt('Statistics on submissions from [_1] to [_2]', + &Apache::lonlocal::locallocaltime($starttime), + &Apache::lonlocal::locallocaltime($endtime) + ).'

'; } + $Str .= "

".&mt('Compiled on [_1]', + &Apache::lonlocal::locallocaltime(time))."

"; + return $Str; +} - my @values = (); - foreach(@$orderedProblems) { - my ($sequence,$problem,$part)=split(':', $_); - if($cache{'StatisticsMaps'} ne 'All Maps' && - $cache{'StatisticsMaps'} ne $cache{$sequence.':title'}) { - next; - } - - if($currentSequence == -1 || - ($sortProblems eq 'Sort Within Sequence' && - $currentSequence != $sequence)) { - if($currentSequence ne -1) { - #$r->print('
finish a graph
'); - } - if($sortProblems eq 'Sort Within Sequence') { - $r->print(''.$cache{$sequence.':title'}.''); +############################################### +############################################### +## +## Misc HTML output routines +## +############################################### +############################################### +sub statistics_html_table_data { + my ($data,$options) = @_; + my $row = ''; + foreach my $field (@Fields) { + next if ($options =~ /no $field->{'name'}/); + next if ($field->{'selected'} ne 'yes'); + $row .= '{'align'})) { + $row .= ' align="'.$field->{'align'}.'"'; } - - my $sendValues = join(',', @values); - my $sendCount = scalar(@values); - - my @GData = ($courseDescription, 'Problems', - $title, $max, $sendCount, $sendValues); - $r->print(''."\n"); - $r->print(''); - $r->print('
'."\n"); - $currentSequence = $sequence; - @values = (); - - } - my $data = 0; - if($graph eq 'DoDiffGraph') { - $data = sprintf("%.2f", $cache{$_.':degreeOfDifficulty'}), + $row .= '>'; + if (exists($field->{'special'}) && $field->{'special'} eq 'link') { + $row .= ''; + } + if (exists($field->{'format'}) && $data->{$field->{'name'}} !~ /[A-Z]/i) { + $row .= sprintf($field->{'format'},$data->{$field->{'name'}}); } else { - $data = sprintf("%.1f", $cache{$_.':percentWrong'}), + $row .= $data->{$field->{'name'}}; } - if($max < $data) { - $max = $data; + if (exists($field->{'special'}) && $field->{'special'} eq 'link') { + $row.= ''; } - push(@values, $data); + $row .= ''; + } + return $row; +} + +sub statistics_table_header { + my ($options) = @_; + my $header_row; + foreach my $field (@Fields) { + next if ($options =~ /no $field->{'name'}/); + next if ($field->{'selected'} ne 'yes'); + $header_row .= ''; + if (exists($field->{'sortable'}) && $field->{'sortable'} eq 'yes') { + $header_row .= '{'name'}."'". + ';document.Statistics.submit();">'; + } + $header_row .= &mt($field->{'title'}); + if ($options =~ /sortable/) { + $header_row.= ''; + } + if ($options !~ /no plots/ && + exists($field->{'graphable'}) && + $field->{'graphable'} eq 'yes') { + $header_row.=' ('; + $header_row .= ''; + $header_row .= &mt('plot').')'; + } + $header_row .= ''; + } + return $header_row; +} +sub sequence_html_header { + my $Str .= ''; + foreach my $field (@SeqFields) { +# next if ($field->{'selected'} ne 'yes'); + $Str .= '{'title'}.''; } + $Str .= ''; + return $Str; +} -#$r->print('
'); - untie(%cache); +sub sequence_html_output { + my ($seq) = @_; + my $data = $SeqStat{$seq->symb}; + my $row = ''; + foreach my $field (@SeqFields) { + next if ($field->{'selected'} ne 'yes'); + $row .= '{'align'})) { + $row .= ' align="'.$field->{'align'}.'"'; + } + $row .= '>'; + if (exists($field->{'format'})) { + $row .= sprintf($field->{'format'},$data->{$field->{'name'}}); + } else { + $row .= $data->{$field->{'name'}}; + } + $row .= ''; + } + $row .= ''."\n"; + return $row; +} +#################################################### +#################################################### +## +## Plotting Routines +## +#################################################### +#################################################### +sub make_plot { + my ($r,$plot) = @_; + &compute_all_statistics($r); + &sort_data($env{'form.sortby'}); + if ($plot eq 'degrees') { + °rees_plot($r); + } elsif ($plot eq 'tries statistics') { + &tries_data_plot($r); + } else { + &make_single_stat_plot($r,$plot); + } return; } +sub make_single_stat_plot { + my ($r,$datafield) = @_; + # + my $title; my $yaxis; + foreach my $field (@Fields) { + next if ($field->{'name'} ne $datafield); + $title = &mt($field->{'long_title'}); + $yaxis = &mt($field->{'title'}); + last; + } + if ($title eq '' || $yaxis eq '') { + # datafield is something we do not know enough about to plot + $r->print('

'. + &mt('Unable to plot the requested statistic.'). + '

'); + return; + } + # + # Build up the data sets to plot + my @Labels; + my @Data; + my $max = 1; + foreach my $data (@StatsArray) { + push(@Labels,$data->{'problem_num'}); + push(@Data,$data->{$datafield}); + if ($data->{$datafield}>$max) { + $max = $data->{$datafield}; + } + } + foreach (1,2,3,4,5,10,15,20,25,40,50,75,100,150,200,250,300,500,600,750, + 1000,1500,2000,2500,3000,3500,4000,5000,7500,10000,15000,20000) { + if ($max <= $_) { + $max = $_; + last; + } + } + if ($max > 20000) { + $max = 10000*(int($max/10000)+1); + } + # + $r->print("

".&Apache::loncommon::DrawBarGraph($title, + &mt('Problem Number'), + $yaxis, + $max, + undef, # colors + \@Labels, + \@Data)."

\n"); + return; +} -#---- Problem Statistics Web Page --------------------------------------- - -sub CreateProblemStatisticsTableHeading { - my ($headings,$r)=@_; - - my $Str=''; - $Str .= ''."\n"; - $Str .= 'P#'."\n"; - foreach(@$headings) { - $Str .= ''; - $Str .= ''.$_.' '."\n"; +sub degrees_plot { + my ($r)=@_; + my $count = scalar(@StatsArray); + my $width = 50 + 10*$count; + $width = 300 if ($width < 300); + my $height = 300; + my $plot = ''; + my $ymax = 0; + my $ymin = 0; + my @Disc; my @Diff; my @Labels; + foreach my $data (@StatsArray) { + push(@Labels,$data->{'problem_num'}); + my $disc = $data->{'deg_of_disc'}; + my $diff = $data->{'deg_of_diff'}; + push(@Disc,$disc); + push(@Diff,$diff); + # + $ymin = $disc if ($ymin > $disc); + $ymin = $diff if ($ymin > $diff); + $ymax = $disc if ($ymax < $disc); + $ymax = $diff if ($ymax < $diff); + } + # + # Make sure we show relevant information. + if ($ymin < 0) { + if (abs($ymin) < 0.05) { + $ymin = 0; + } else { + $ymin = -1; + } + } + if ($ymax > 0) { + if (abs($ymax) < 0.05) { + $ymax = 0; + } else { + $ymax = 1; + } } - $Str .= "\n".''."\n"; + # + my $xmax = $Labels[-1]; + if ($xmax > 50) { + if ($xmax % 10 != 0) { + $xmax = 10 * (int($xmax/10)+1); + } + } else { + if ($xmax % 5 != 0) { + $xmax = 5 * (int($xmax/5)+1); + } + } + # + my $discdata .= ''.join(',',@Labels).''.$/. + ''.join(',',@Disc).''.$/; + # + my $diffdata .= ''.join(',',@Labels).''.$/. + ''.join(',',@Diff).''.$/; + # + my $title = &mt('Degree of Discrimination[_1]and Degree of Difficulty','\n'); + if ($xmax > 50) { + $title = &mt('Degree of Discrimination and Degree of Difficulty'); + } + my %lt = &Apache::lonlocal::texthash( + 'alttag' => 'Degree of Discrimination and Degree of Difficulty Plot', + 'xlabel' => 'Problem Number', + ); + # + $plot=<<"END"; + + + $title + + $lt{'xlabel'} + + $discdata + + + $diffdata + + +END + my $plotresult = + '

'.&Apache::lonxml::xmlparse($r,'web',$plot).'

'.$/; + $r->print($plotresult); + return; +} - return $Str; +sub tries_data_plot { + my ($r)=@_; + my $count = scalar(@StatsArray); + my $width = 50 + 10*$count; + $width = 300 if ($width < 300); + my $height = 300; + my $plot = ''; + my @STD; my @Mean; my @Max; my @Min; + my @Labels; + my $ymax = 5; + foreach my $data (@StatsArray) { + my $max = $data->{'mean_tries'} + $data->{'std_tries'}; + $ymax = $max if ($ymax < $max); + $ymax = $max if ($ymax < $max); + push(@Labels,$data->{'problem_num'}); + push(@STD,$data->{'std_tries'}); + push(@Mean,$data->{'mean_tries'}); + } + # + # Make sure we show relevant information. + my $xmax = $Labels[-1]; + if ($xmax > 50) { + if ($xmax % 10 != 0) { + $xmax = 10 * (int($xmax/10)+1); + } + } else { + if ($xmax % 5 != 0) { + $xmax = 5 * (int($xmax/5)+1); + } + } + $ymax = int($ymax)+1+2; + # + my $std_data .= ''.join(',',@Labels).''.$/. + ''.join(',',@Mean).''.$/; + # + my $std_error_data .= ''.join(',',@Labels).''.$/. + ''.join(',',@Mean).''.$/. + ''.join(',',@STD).''.$/; + # + my $title = &mt('Mean and S.D. of Tries'); + if ($xmax > 30) { + $title = &mt('Mean and Standard Deviation of Tries'); + } + # + my %lt = &Apache::lonlocal::texthash( + 'alttag' => 'Mean and S.D of Tries Plot', + 'xlabel' => 'Problem Number', + 'ylabel' => 'Number of Tries', + ); + $plot=<<"END"; + + $title + + $lt{'xlabel'} + $lt{'ylabel'} + + $std_error_data + + + $std_data + + +END + my $plotresult = + '

'.&Apache::lonxml::xmlparse($r,'web',$plot).'

'.$/; + $r->print($plotresult); + return; } -sub BuildStatisticsTable { - my ($cache,$displayFormat,$sortProblems,$orderedProblems,$headings, - $r,$color)=@_; - - my $count = 1; - my $currentSequence = -1; - foreach(@$orderedProblems) { - my ($sequence,$problem,$part)=split(':', $_); - if($cache->{'StatisticsMaps'} ne 'All Maps' && - $cache->{'StatisticsMaps'} ne $cache->{$sequence.':title'}) { +sub plot_dropdown { + my $current = ''; + my $title; + # + if (defined($env{'form.plot'})) { + $current = $env{'form.plot'}; + } + # + my @Additional_Plots = ( + { graphable=>'yes', + name => 'degrees', + title => 'Difficulty Indexes' }, + { graphable=>'yes', + name => 'tries statistics', + title => 'Tries Statistics' }); + # + my $Str= "\n".''."\n"; + return $Str; +} - if($currentSequence == -1 || - ($sortProblems eq 'Sort Within Sequence' && - $currentSequence != $sequence)) { - if($displayFormat ne 'Display CSV Format') { - if($currentSequence ne -1) { - $r->print(''); - $r->print('
'); - } - if($sortProblems eq 'Sort Within Sequence') { - $r->print(''.$cache->{$sequence.':title'}.''); - } - $r->print('
'."\n"); - $r->print(''."\n"); - $r->print(&CreateProblemStatisticsTableHeading($headings, $r)); - } else { - if($sortProblems eq 'Sort Within Sequence') { - $r->print('"'.$cache->{$sequence.':title'}.'"'); - } - $r->print('
'); +############################################### +############################################### +## +## Excel output routines +## +############################################### +############################################### +sub Excel_output { + my ($r) = @_; + $r->print('

'.&mt('Preparing Excel Spreadsheet').'

'); + ## + ## Compute the statistics + &compute_all_statistics($r); + my $c = $r->connection; + return if ($c->aborted()); + # + my ($starttime,$endtime) = &Apache::lonstathelpers::get_time_limits(); + ## + ## Create the excel workbook + my ($excel_workbook,$filename,$format) = + &Apache::loncommon::create_workbook($r); + return if (! defined($excel_workbook)); + # + # Add a worksheet + my $sheetname = $env{'course.'.$env{'request.course.id'}.'.description'}; + if (length($sheetname) > 31) { + $sheetname = substr($sheetname,0,31); + } + my $excel_sheet = $excel_workbook->addworksheet( + &Apache::loncommon::clean_excel_name($sheetname)); + ## + ## Begin creating excel sheet + ## + my ($rows_output,$cols_output) = (0,0); + # + # Put the course description in the header + $excel_sheet->write($rows_output,$cols_output++, + $env{'course.'.$env{'request.course.id'}.'.description'}, + $format->{'h1'}); + $cols_output += 3; + # + # Put a description of the sections listed + my $sectionstring = ''; + $excel_sheet->write($rows_output,$cols_output++, + &Apache::lonstatistics::section_and_enrollment_description('plaintext'), + $format->{'h3'}); + $cols_output += scalar(&Apache::lonstatistics::get_selected_sections()); + $cols_output += scalar(&Apache::lonstatistics::get_selected_groups()); + # + # Time restrictions + my $time_string; + if (defined($starttime)) { + # call localtime but not lonlocal:locallocaltime because excel probably + # cannot handle localized text. Probably. + $time_string .= 'Data collected from '.localtime($time_string); + if (defined($endtime)) { + $time_string .= ' to '.localtime($endtime); + } + $time_string .= '.'; + } elsif (defined($endtime)) { + # See note above about lonlocal:locallocaltime + $time_string .= 'Data collected before '.localtime($endtime).'.'; + } + if (defined($time_string)) { + $excel_sheet->write($rows_output,$cols_output++,$time_string); + $cols_output+= 5; + } + # + # Put the date in there too + $excel_sheet->write($rows_output,$cols_output++, + 'Compiled on '.localtime(time)); + # + $rows_output++; + $cols_output=0; + ## + ## Sequence Statistics + ## + &write_headers($excel_sheet,$format,\$rows_output,\$cols_output, + \@SeqFields); + foreach my $seq (@sequences) { + my $data = $SeqStat{$seq->symb}; + $cols_output=0; + foreach my $field (@SeqFields) { + next if ($field->{'selected'} ne 'yes'); + my $fieldformat = undef; + if (exists($field->{'excel_format'})) { + $fieldformat = $format->{$field->{'excel_format'}}; } - $currentSequence = $sequence; + $excel_sheet->write($rows_output,$cols_output++, + $data->{$field->{'name'}},$fieldformat); } - - my $ref = ''.$cache->{$problem.':title'}.''; - my $title = $cache->{$problem.':title'}; - if($part != 0) { - $title .= ' Part '.$part; - } - my $source = $cache->{$problem.':source'}; - my $tableData = join('&', $ref, $title, $source, - $cache->{$_.':studentCount'}, - $cache->{$_.':totalTries'}, - $cache->{$_.':maxTries'}, - $cache->{$_.':mean'}, - $cache->{$_.':correct'}, - $cache->{$_.':correctByOverride'}, - $cache->{$_.':percentWrong'}, - $cache->{$_.':degreeOfDifficulty'}, - $cache->{$_.':standardDeviation'}, - $cache->{$_.':skewness'}, - $cache->{$_.':discriminationFactor1'}, - $cache->{$_.':discriminationFactor2'}); - - &TableRow($displayFormat,$tableData,$count,$r,$color); - - $count++; - } - if($displayFormat ne 'Display CSV Format') { - $r->print('
'."\n"); - $r->print('
'); - } else { - $r->print('
'); + $rows_output++; + $cols_output=0; } - + ## + ## Resource Statistics + ## + $rows_output++; + $cols_output=0; + &write_headers($excel_sheet,$format,\$rows_output,\$cols_output, + \@Fields); + # + foreach my $data (@StatsArray) { + $cols_output=0; + foreach my $field (@Fields) { + next if ($field->{'selected'} ne 'yes'); + next if ($field->{'name'} eq 'problem_num'); + my $fieldformat = undef; + if (exists($field->{'excel_format'})) { + $fieldformat = $format->{$field->{'excel_format'}}; + } + $excel_sheet->write($rows_output,$cols_output++, + $data->{$field->{'name'}},$fieldformat); + } + $rows_output++; + $cols_output=0; + } + # + $excel_workbook->close(); + # + # Tell the user where to get their excel file + $r->print('
'. + ''. + &mt('Your Excel Spreadsheet').''."\n"); + $r->rflush(); return; } -sub TableRow { - my ($displayFormat,$Str,$RealIdx,$r,$color)=@_; - my($ref,$title,$source,$StdNo,$TotalTries,$MxTries,$Avg,$YES,$Override, - $Wrng,$DoD,$SD,$Sk,$_D1,$_D2)=split(/\&/,$Str); - my $Ptr; - if($displayFormat eq 'Display CSV Format') { - $Ptr='"'.$RealIdx.'",'."\n". - '"'.$title.'",'."\n". - '"'.$source.'",'."\n". - '"'.$StdNo.'",'."\n". - '"'.$TotalTries.'",'."\n". - '"'.$MxTries.'",'."\n". - '"'.$Avg.'",'."\n". - '"'.$YES.'",'."\n". - '"'.$Override.'",'."\n". - '"'.$Wrng.'",'."\n". - '"'.$DoD.'",'."\n". - '"'.$SD.'",'."\n". - '"'.$Sk.'",'."\n". - '"'.$_D1.'",'."\n". - '"'.$_D2.'"'."\n". - "
\n"; - - $r->print("\n".$Ptr); - } else { - $Ptr=''."\n". - ''.$RealIdx.''."\n". - ''.$ref.''."\n". - ' '.$StdNo.''."\n". - ''.$TotalTries.''."\n". - ''.$MxTries.''."\n". - ''.$Avg.''."\n". - ' '.$YES.''."\n". - ' '.$Override.''."\n". - ' '.$Wrng.''."\n". - ' '.$DoD.''."\n". - ' '.$SD.''."\n". - ' '.$Sk.''."\n". - ' '.$_D1.''."\n". - ' '.$_D2.''."\n"; - $r->print($Ptr.''."\n"); +## +## &write_headers +## +sub write_headers { + my ($excel_sheet,$format,$rows_output,$cols_output,$Fields) = @_; + ## + ## First the long titles + foreach my $field (@{$Fields}) { + next if ($field->{'name'} eq 'problem_num'); + next if ($field->{'selected'} ne 'yes'); + if (exists($field->{'long_title'})) { + $excel_sheet->write($$rows_output,${$cols_output}, + $field->{'long_title'}, + $format->{'bold'}); + } else { + $excel_sheet->write($$rows_output,${$cols_output},''); + } + ${$cols_output}+= 1; } - + ${$cols_output} =0; + ${$rows_output}+=1; + ## + ## Then the short titles + foreach my $field (@{$Fields}) { + next if ($field->{'selected'} ne 'yes'); + next if ($field->{'name'} eq 'problem_num'); + # Use english for excel as I am not sure how well excel handles + # other character sets.... + $excel_sheet->write($$rows_output,$$cols_output, + $field->{'title'}, + $format->{'bold'}); + $$cols_output+=1; + } + ${$cols_output} =0; + ${$rows_output}+=1; return; } -# For loading the colored table for display or un-colored for print -sub setbgcolor { - my $PrintTable=shift; - my %color; - if ($PrintTable){ - $color{"gb"}="#FFFFFF"; - $color{"red"}="#FFFFFF"; - $color{"yellow"}="#FFFFFF"; - $color{"green"}="#FFFFFF"; - $color{"purple"}="#FFFFFF"; - } else { - $color{"gb"}="#DDFFFF"; - $color{"red"}="#FFDDDD"; - $color{"yellow"}="#EEFFCC"; - $color{"green"}="#DDFFDD"; - $color{"purple"}="#FFDDFF"; - } - - return \%color; -} - -sub ProblemStatisticsButtons { - my ($displayFormat, $displayLegend, $sortProblems)=@_; - - my $Ptr = ''; - $Ptr .= 'parts}) { + next if (($res->is_survey($part)) || ($res->is_anonsurvey($part))) ; + # + # This is where all the work happens + my $data = &get_statistics($seq,$res,$part,scalar(@StatsArray)+1); + push (@Data,$data); + push (@StatsArray,$data); + } } - $Ptr .= ''; - $Ptr .= ' 0) { + # Assume we have already computed the statistics + return; } - $Ptr .= ''; - $Ptr .= 'connection; + foreach my $seq (@sequences) { + last if ($c->aborted); + &compute_sequence_statistics($seq); + &compute_statistics_on_sequence($seq); } - $Ptr .= ''; - - return $Ptr; } -sub ProblemStatisticsLegend { - my $Ptr = ''; - $Ptr = ''; - $Ptr .= ''; - $Ptr .= ''; - $Ptr .= ''; - $Ptr .= ''; - $Ptr .= ''; - $Ptr .= ''; - $Ptr .= ''; - $Ptr .= ''; - $Ptr .= ''; - $Ptr .= ''; - $Ptr .= ''; - $Ptr .= ''; - $Ptr .= ''; - $Ptr .= '
'; - $Ptr .= '#StdntsTotal number of students attempted the problem.'; - $Ptr .= '
'; - $Ptr .= 'TriesTotal number of tries for solving the problem.'; - $Ptr .= '
'; - $Ptr .= 'ModLargest number of tries for solving the problem by a student.'; - $Ptr .= '
'; - $Ptr .= 'MeanAverage number of tries. [ Tries / #Stdnts ]'; - $Ptr .= '
'; - $Ptr .= '#YESNumber of students solved the problem correctly.'; - $Ptr .= '
'; - $Ptr .= '#yesNumber of students solved the problem by override.'; - $Ptr .= '
'; - $Ptr .= '%WrongPercentage of students who tried to solve the problem '; - $Ptr .= 'but is still incorrect. [ 100*((#Stdnts-(#YES+#yes))/#Stdnts) ]'; - $Ptr .= '
'; - $Ptr .= 'DoDiffDegree of Difficulty of the problem. '; - $Ptr .= '[ 1 - ((#YES+#yes) / Tries) ]'; - $Ptr .= '
'; - $Ptr .= 'S.D.Standard Deviation of the tries. '; - $Ptr .= '[ sqrt(sum((Xi - Mean)^2)) / (#Stdnts-1) '; - $Ptr .= 'where Xi denotes every student\'s tries ]'; - $Ptr .= '
'; - $Ptr .= 'Skew.Skewness of the students tries.'; - $Ptr .= '[(sqrt( sum((Xi - Mean)^3) / #Stdnts)) / (S.D.^3)]'; - $Ptr .= '
'; - $Ptr .= 'Dis.F.Discrimination Factor: A Standard for evaluating the '; - $Ptr .= 'problem according to a Criterion
'; - $Ptr .= '[Criterion to group students into %27 Upper Students - '; - $Ptr .= 'and %27 Lower Students]
'; - $Ptr .= '1st Criterion for Sorting the Students: '; - $Ptr .= 'Sum of Partial Credit Awarded / Total Number of Tries
'; - $Ptr .= '2nd Criterion for Sorting the Students: '; - $Ptr .= 'Total number of Correct Answers / Total Number of Tries'; - $Ptr .= '
Disc.Number of Students had at least one discussion.'; - $Ptr .= '
'; - - return $Ptr; -} - -sub ExtractStudentData { - my ($cache, $students)=@_; - - my @problemList=(); - my %problemData; - foreach my $sequence (split(':', $cache->{'orderedSequences'})) { - foreach my $problemID (split(':', $cache->{$sequence.':problems'})) { - foreach my $part (split(/\:/,$cache->{$sequence.':'. - $problemID. - ':parts'})) { - my $id = $sequence.':'.$problemID.':'.$part; - push(@problemList, $id); - my $totalTries = 0; - my $totalAwarded = 0; - my $correct = 0; - my $correctByOverride = 0; - my $studentCount = 0; - my $maxTries = 0; - my $totalFirst = 0; - my @studentTries=(); - foreach(@$students) { - my $code = $cache->{"$_:$problemID:$part:code"}; - - if(defined($cache->{$_.':error'}) || $code eq ' ' || - $cache->{"$_:$problemID:NoVersion"} eq 'true') { - next; - } - - $studentCount++; - my $tries = $cache->{"$_:$problemID:$part:tries"}; - if($maxTries < $tries) { - $maxTries = $tries; +sub sort_data { + my ($sortkey) = @_; + return if (! @StatsArray); + # + # Sort the data + my $sortby = undef; + foreach my $field (@Fields) { + if ($sortkey eq $field->{'name'}) { + $sortby = $field->{'name'}; + } + } + if (! defined($sortby) || $sortby eq '' || $sortby eq 'problem_num') { + $sortby = 'container'; + } + if ($sortby ne 'container') { + # $sortby is already defined, so we can charge ahead + if ($sortby =~ /^(title|part)$/i) { + # Alpha comparison + @StatsArray = sort { + lc($a->{$sortby}) cmp lc($b->{$sortby}) || + lc($a->{'title'}) cmp lc($b->{'title'}) || + lc($a->{'part'}) cmp lc($b->{'part'}); + } @StatsArray; + } else { + # Numerical comparison + @StatsArray = sort { + my $retvalue = 0; + if ($b->{$sortby} eq 'nan') { + if ($a->{$sortby} ne 'nan') { + $retvalue = -1; + } else { + $retvalue = 0; } - $totalTries += $tries; - push(@studentTries, $tries); - - my $awarded = $cache->{"$_:$problemID:$part:awarded"}; - $totalAwarded += $awarded; - - if($code eq '*') { - $correct++; - if($tries == 1) { - $totalFirst++; - } - } elsif($code eq '+') { - $correctByOverride++; + } + if ($a->{$sortby} eq 'nan') { + if ($b->{$sortby} ne 'nan') { + $retvalue = 1; } } - - my $studentTriesJoined = join(':::', @studentTries); - $problemData{$id.':sequenceTitle'} = - $cache->{$sequence.':title'}; - $problemData{$id.':studentCount'} = $studentCount; - $problemData{$id.':totalTries'} = $totalTries; - $problemData{$id.':studentTries'} = $studentTriesJoined; - $problemData{$id.':totalAwarded'} = $totalAwarded; - $problemData{$id.':correct'} = $correct; - $problemData{$id.':correctByOverride'} = $correctByOverride; - $problemData{$id.':wrong'} = $studentCount - - ($correct + $correctByOverride); - $problemData{$id.':maxTries'} = $maxTries; - $problemData{$id.':totalFirst'} = $totalFirst; - } + if ($retvalue eq '0') { + $retvalue = $b->{$sortby} <=> $a->{$sortby} || + lc($a->{'title'}) <=> lc($b->{'title'}) || + lc($a->{'part'}) <=> lc($b->{'part'}); + } + $retvalue; + } @StatsArray; } } + # + # Renumber the data set + my $count; + foreach my $data (@StatsArray) { + $data->{'problem_num'} = ++$count; + } + return; +} - my @upperStudents1=(); - my @lowerStudents1=(); - my @upperStudents2=(); - my @lowerStudents2=(); - my $upperCount = int(0.27*scalar(@$students)); - # Discriminant Factor criterion 1 - my $sortedStudents = &SortDivideByTries($students,$cache,':totalAwarded'); - - for(my $i=0; $i<$upperCount; $i++) { - push(@lowerStudents1, $sortedStudents->[$i]); - push(@upperStudents1, $sortedStudents->[(scalar(@$students)-$i-1)]); - } - - $problemData{'studentsUpperListCriterion1'}=join(':::', @upperStudents1); - $problemData{'studentsLowerListCriterion1'}=join(':::', @lowerStudents1); - - # Discriminant Factor criterion 2 - $sortedStudents = &SortDivideByTries($students, $cache, ':totalSolved'); - - for(my $i=0; $i<$upperCount; $i++) { - push(@lowerStudents2, $sortedStudents->[$i]); - push(@upperStudents2, $sortedStudents->[(scalar(@$students)-$i-1)]); - } - $problemData{'studentsUpperListCriterion2'}=join(':::', @upperStudents2); - $problemData{'studentsLowerListCriterion2'}=join(':::', @lowerStudents2); - - $problemData{'problemList'} = join(':::', @problemList); - - return \%problemData; -} - -sub SortDivideByTries { - my ($toSort, $data, $sortOn)=@_; - my @orderedData = sort { ($data->{$a.':totalTries'}) ? - ($data->{$a.$sortOn}/$data->{$a.':totalTries'}):0 - <=> - ($data->{$b.':totalTries'}) ? - ($data->{$b.$sortOn}/$data->{$b.':totalTries'}):0 - } @$toSort; - - return \@orderedData; -} - -sub SortProblems { - my ($problemData,$sortBy,$sortProblems,$ascend)=@_; - - my @problems = split(':::', $problemData->{'problemList'}); - if($sortBy eq "Homework Sets Order") { - return \@problems; - } - - my $data; - - if ($sortBy eq "#Stdnts") { $data = ':studentCount'; } - elsif($sortBy eq "Tries") { $data = ':totalTries'; } - elsif($sortBy eq "Mod") { $data = ':maxTries'; } - elsif($sortBy eq "Mean") { $data = ':mean'; } - elsif($sortBy eq "#YES") { $data = ':correct'; } - elsif($sortBy eq "#yes") { $data = ':correctByOverride'; } - elsif($sortBy eq "%Wrng") { $data = ':percentWrong'; } - elsif($sortBy eq "DoDiff") { $data = ':degreeOfDifficulty'; } - elsif($sortBy eq "S.D.") { $data = ':standardDeviation'; } - elsif($sortBy eq "Skew.") { $data = ':skewness'; } - elsif($sortBy eq "D.F.1st") { $data = ':discriminationFactor1'; } - elsif($sortBy eq "D.F.2nd") { $data = ':discriminationFactor2'; } - else { return \@problems; } - - my %temp; - my @sequenceList=(); - foreach(@problems) { - my ($sequence) = split(':', $_); - - my @array=(); - my $tempArray; - if(defined($temp{$sequence})) { - $tempArray = $temp{$sequence}; - } else { - push(@sequenceList, $sequence); - $tempArray = \@array; - $temp{$sequence} = $tempArray; - } - - push(@$tempArray, $_); - } - - my @orderedProblems; - if($sortProblems eq "Sort Within Sequence") { - foreach(keys(%temp)) { - my $tempArray = $temp{$_}; - my @tempOrder = - sort { $problemData->{$a.$data} <=> $problemData->{$b.$data} } - @$tempArray; - $temp{$_} = \@tempOrder; - } - foreach(@sequenceList) { - my $tempArray = $temp{$_}; - @orderedProblems = (@orderedProblems, @$tempArray); - } +######################################################## +######################################################## + +=pod + +=item &get_statistics() + +Wrapper routine from the call to loncoursedata::get_problem_statistics. +Calls lonstathelpers::get_time_limits() to limit the data set by time +and &compute_discrimination_factor + +Inputs: $sequence, $resource, $part, $problem_num + +Returns: Hash reference with statistics data from +loncoursedata::get_problem_statistics. + +=cut + +######################################################## +######################################################## +sub get_statistics { + my ($sequence,$resource,$part,$problem_num) = @_; + # + my ($starttime,$endtime) = &Apache::lonstathelpers::get_time_limits(); + my $symb = $resource->symb; + my $courseid = $env{'request.course.id'}; + # + my $data = &Apache::loncoursedata::get_problem_statistics + ([&Apache::lonstatistics::get_selected_sections()], + [&Apache::lonstatistics::get_selected_groups()], + $Apache::lonstatistics::enrollment_status, + $symb,$part,$courseid,$starttime,$endtime); + $data->{'symb'} = $symb; + $data->{'part'} = $part; + $data->{'problem_num'} = $problem_num; + $data->{'container'} = $sequence->compTitle; + $data->{'title'} = $resource->compTitle; + $data->{'title.link'} = $resource->src.'?symb='. + &escape($resource->symb); + # + if ($SelectedFields{'deg_of_disc'}) { + $data->{'deg_of_disc'} = + &compute_discrimination_factor($resource,$part,$sequence); + } + # + # Store in metadata if computations were done for all students + if ($data->{'num_students'} > 1) { + my @Sections = &Apache::lonstatistics::get_selected_sections(); + my $sections = '"'.join(' ',@Sections).'"'; + $sections =~ s/&+/_/g; # Ensure no special characters + $data->{'sections'}=$sections; + $data->{'course'} = $env{'request.course.id'}; + my $urlres=(&Apache::lonnet::decode_symb($resource->symb))[2]; + my %storestats = + &LONCAPA::lonmetadata::dynamic_metadata_storage($data); + my ($dom,$user) = ($urlres=~m{^($LONCAPA::domain_re)/($LONCAPA::username_re)}); + &Apache::lonnet::put('nohist_resevaldata',\%storestats,$dom,$user); + } + # + $data->{'tries_per_correct'} = $data->{'tries'} / + ($data->{'num_solved'}+0.1); + # + # Get the due date for research purposes (commented out most of the time) +# my $duedate = &Apache::lonnet::EXT('resource.'.$part.'.duedate',$symb);; +# my $opendate = &Apache::lonnet::EXT('resource.'.$part.'.opendate',$symb); +# my $maxtries = &Apache::lonnet::EXT('resource.'.$part.'.maxtries',$symb); +# my $hinttries = &Apache::lonnet::EXT('resource.'.$part.'.hinttries',$symb); + my $weight = &Apache::lonnet::EXT('resource.'.$part.'.weight',$symb); + $data->{'weight'} = $weight; +# $data->{'duedate'} = $duedate; +# $data->{'opendate'} = $opendate; +# $data->{'maxtries'} = $maxtries; +# $data->{'hinttries'} = $hinttries; +# $data->{'resptypes'} = join(',',@{$resource->{'partdata'}->{$part}->{'ResponseTypes'}}); + return $data; +} + +############################################### +############################################### + +=pod + +=item &compute_discrimination_factor() + +Inputs: $Resource, $Sequence + +Returns: integer between -1 and 1 + +=cut + +############################################### +############################################### +sub compute_discrimination_factor { + my ($resource,$part,$seq) = @_; + my $symb = $resource->symb; + my @Resources; + foreach my $res (&Apache::lonstathelpers::get_resources($navmap,$seq)){ + next if ($res->symb eq $symb); + push (@Resources,$res->symb); + } + # + # rank + my ($starttime,$endtime) = &Apache::lonstathelpers::get_time_limits(); + my $ranking = + &Apache::loncoursedata::rank_students_by_scores_on_resources + (\@Resources, + [&Apache::lonstatistics::get_selected_sections()], + [&Apache::lonstatistics::get_selected_groups()], + $Apache::lonstatistics::enrollment_status,undef, + $starttime,$endtime, $symb); + # + # compute their percent scores on the problems in the sequence, + my $number_to_grab = int(scalar(@{$ranking})/4); + my $num_students = scalar(@{$ranking}); + my @BottomSet = map { $_->[&Apache::loncoursedata::RNK_student()]; + } @{$ranking}[0..$number_to_grab]; + my @TopSet = + map { + $_->[&Apache::loncoursedata::RNK_student()]; + } @{$ranking}[-$number_to_grab..0]; + if (! @BottomSet || (@BottomSet == 1 && $BottomSet[0] eq '') || + ! @TopSet || (@TopSet == 1 && $TopSet[0] eq '')) { + return 'nan'; + } + my ($bottom_sum,$bottom_max) = + &Apache::loncoursedata::get_sum_of_scores($symb,$part,\@BottomSet, + undef,$starttime,$endtime); + my ($top_sum,$top_max) = + &Apache::loncoursedata::get_sum_of_scores($symb,$part,\@TopSet, + undef,$starttime,$endtime); + my $deg_of_disc; + if ($top_max == 0 || $bottom_max==0) { + $deg_of_disc = 'nan'; } else { - @orderedProblems = - sort { $problemData->{$a.$data} <=> $problemData->{$b.$data} } - @problems; + $deg_of_disc = ($top_sum/$top_max) - ($bottom_sum/$bottom_max); } + #&Apache::lonnet::logthis(' '.$top_sum.'/'.$top_max. + # ' - '.$bottom_sum.'/'.$bottom_max); + return $deg_of_disc; +} - if($ascend eq 'Descending') { - @orderedProblems = reverse(@orderedProblems); +############################################### +############################################### +## +## Compute KR-21 +## +## To compute KR-21, you need the following information: +## +## K=the number of items in your test +## M=the mean score on the test +## s=the standard deviation of the scores on your test +## +## then: +## +## KR-21 rk= [K/(K-1)] * [1- (M*(K-M))/(K*s^2))] +## +############################################### +############################################### +sub compute_sequence_statistics { + my ($seq) = @_; + my $symb = $seq->symb; + my @Resources; + my $part_count; + foreach my $res (&Apache::lonstathelpers::get_resources($navmap,$seq)) { + push (@Resources,$res->symb); + $part_count += scalar(@{$res->parts}); + } + my ($starttime,$endtime) = &Apache::lonstathelpers::get_time_limits(); + # + # First compute statistics based on student scores + my ($smin,$smax,$sMean,$sSTD,$scount,$sMAX) = + &Apache::loncoursedata::score_stats + ([&Apache::lonstatistics::get_selected_sections()], + [&Apache::lonstatistics::get_selected_groups()], + $Apache::lonstatistics::enrollment_status, + \@Resources,$starttime,$endtime,undef); + $SeqStat{$symb}->{'title'} = $seq->compTitle; + $SeqStat{$symb}->{'scoremax'} = $smax; + $SeqStat{$symb}->{'scoremin'} = $smin; + $SeqStat{$symb}->{'scoremean'} = $sMean; + $SeqStat{$symb}->{'scorestd'} = $sSTD; + $SeqStat{$symb}->{'scorecount'} = $scount; + $SeqStat{$symb}->{'max_possible'} = $sMAX; + # + # Compute statistics based on the number of correct problems + # 'correct' is taken to mean + my ($cmin,$cmax,$cMean,$cSTD,$ccount)= + &Apache::loncoursedata::count_stats + ([&Apache::lonstatistics::get_selected_sections()], + [&Apache::lonstatistics::get_selected_groups()], + $Apache::lonstatistics::enrollment_status, + \@Resources,$starttime,$endtime,undef); + my $K = $part_count; + my $kr_21; + if ($K > 1 && $cSTD > 0) { + $kr_21 = ($K/($K-1)) * (1 - $cMean*($K-$cMean)/($K*$cSTD**2)); + } else { + $kr_21 = 'nan'; } - - return \@orderedProblems; + $SeqStat{$symb}->{'countmax'} = $cmax; + $SeqStat{$symb}->{'countmin'} = $cmin; + $SeqStat{$symb}->{'countstd'} = $cSTD; + $SeqStat{$symb}->{'countmean'} = $cMean; + $SeqStat{$symb}->{'count'} = $ccount; + $SeqStat{$symb}->{'items'} = $K; + $SeqStat{$symb}->{'KR-21'}=$kr_21; + return; } -sub CalculateStatistics { - my ($data, $cache, $courseID)=@_; - my @problems = split(':::', $data->{'problemList'}); - foreach(@problems) { - # Mean - my $mean = ($data->{$_.':studentCount'}) ? - ($data->{$_.':totalTries'} / $data->{$_.':studentCount'}) : 0; - $data->{$_.':mean'} = sprintf("%.2f", $mean); - # %Wrong - my $pw = ($data->{$_.':studentCount'}) ? - (($data->{$_.':wrong'} / $data->{$_.':studentCount'}) * 100.0) : - 100.0; - $data->{$_.':percentWrong'} = sprintf("%.1f", $pw); +=pod - # Degree of Difficulty - my $dod = ($data->{$_.':totalTries'}) ? - (1 - (($data->{$_.':correct'} + $data->{$_.':correctByOverride'}) / - $data->{$_.':totalTries'})) : 0; +=item ProblemStatisticsLegend - $data->{$_.':degreeOfDifficulty'} = sprintf("%.2f", $dod); +=over 4 - # Factor in mean - my @studentTries = split(':::', $data->{$_.':studentTries'}); - foreach(my $index=0; $index < scalar(@studentTries); $index++) { - $studentTries[$index] -= $mean; - } - my $sumSquared = 0; - my $sumCubed = 0; - foreach(@studentTries) { - my $squared = ($_ * $_); - my $cubed = ($squared * $_); - $sumSquared += $squared; - $sumCubed += $cubed; - } +=item #Stdnts +Total number of students attempted the problem. - # Standard deviation - my $standardDeviation; - if($data->{$_.':studentCount'} - 1 > 0) { - $standardDeviation = (sqrt($sumSquared)) / - ($data->{$_.':studentCount'} - 1); - } else { - $standardDeviation = 0.0; - } - $data->{$_.':standardDeviation'} = sprintf("%.1f", $standardDeviation); +=item Tries +Total number of tries for solving the problem. - # Skewness - my $skew; - if($standardDeviation > 0.0999 && $data->{$_.':studentCount'} > 0) { - $skew = (((sqrt($sumSquared)) / $data->{$_.':studentCount'}) / - ($standardDeviation * - $standardDeviation * - $standardDeviation)); - } else { - $skew = 0.0; - } +=item Max Tries +Largest number of tries for solving the problem by a student. - $data->{$_.':skewness'} = sprintf("%.1f", $skew); +=item Mean +Average number of tries. [ Tries / #Stdnts ] - # Discrimination Factor 1 - my ($sequence, $problem, $part) = split(':', $_); +=item #YES +Number of students solved the problem correctly. - my @upper1 = split(':::', $data->{'studentsUpperListCriterion1'}); - my @lower1 = split(':::', $data->{'studentsLowerListCriterion1'}); +=item #yes +Number of students solved the problem by override. - my $upper1Sum=0; - foreach my $name (@upper1) { - $upper1Sum += $cache->{"$name:$problem:$part:awarded"}; - } - $upper1Sum = (scalar(@upper1)) ? ($upper1Sum/(scalar(@upper1))) : 0; +=item %Wrong +Percentage of students who tried to solve the problem +but is still incorrect. [ 100*((#Stdnts-(#YES+#yes))/#Stdnts) ] - my $lower1Sum=0; - foreach my $name (@lower1) { - $lower1Sum += $cache->{"$name:$problem:$part:awarded"}; - } - $lower1Sum = (scalar(@lower1)) ? ($lower1Sum/(scalar(@lower1))) : 0; +=item DoDiff +Degree of Difficulty of the problem. +[ 1 - ((#YES+#yes) / Tries) ] - my $df1 = $upper1Sum - $lower1Sum; - $data->{$_.':discriminationFactor1'} = sprintf("%.2f", $df1); +=item S.D. +Standard Deviation of the tries. +[ sqrt(sum((Xi - Mean)^2)) / (#Stdnts-1) +where Xi denotes every student\'s tries ] - # Discrimination Factor 2 - my @upper2 = split(':::', $data->{'studentsUpperListCriterion2'}); - my @lower2 = split(':::', $data->{'studentsLowerListCriterion2'}); +=item Skew. +Skewness of the students tries. +[(sqrt( sum((Xi - Mean)^3) / #Stdnts)) / (S.D.^3)] - my $upper2Sum=0; - foreach my $name (@upper2) { - $upper2Sum += $cache->{"$name:$problem:$part:awarded"}; - } - $upper2Sum = (scalar(@upper2)) ? ($upper2Sum/(scalar(@upper2))) : 0; +=item Dis.F. +Discrimination Factor: A Standard for evaluating the +problem according to a Criterion
- my $lower2Sum=0; - foreach my $name (@lower2) { - $lower2Sum += $cache->{"$name:$problem:$part:awarded"}; - } - $lower2Sum = (scalar(@lower2)) ? ($lower2Sum/(scalar(@lower2))) : 0; +=item [Criterion to group students into %27 Upper Students - +and %27 Lower Students] +1st Criterion for Sorting the Students: +Sum of Partial Credit Awarded / Total Number of Tries +2nd Criterion for Sorting the Students: +Total number of Correct Answers / Total Number of Tries - my $df2 = $upper2Sum - $lower2Sum; - $data->{$_.':discriminationFactor2'} = sprintf("%.2f", $df2); +=item Disc. +Number of Students had at least one discussion. - my %storestats; - my $Average = ($data->{$_.':studentCount'}) ? - $data->{$_.':totalTries'}/$data->{$_.':studentCount'} : 0; - $storestats{$courseID.'___'.$cache->{$sequence.':source'}. - '___timestamp'}=time; - $storestats{$courseID.'___'.$cache->{$sequence.':source'}. - '___stdno'}=$data->{$_.':studentCount'}; - $storestats{$courseID.'___'.$cache->{$sequence.':source'}. - '___avetries'}=$Average; - $storestats{$courseID.'___'.$cache->{$sequence.':source'}. - '___difficulty'}=$data->{$_.':degreeOfDifficulty'}; - $cache->{$sequence.':source'} =~ /^(\w+)\/(\w+)/; - if($data->{$_.':studentCount'}) { - &Apache::lonnet::put('nohist_resevaldata',\%storestats,$1,$2); - } - } +=back - return; -} +=cut -#---- END Problem Statistics Web Page ---------------------------------------- +############################################################ +############################################################ 1; __END__ 500 Internal Server Error

Internal Server Error

The server encountered an internal error or misconfiguration and was unable to complete your request.

Please contact the server administrator at root@localhost to inform them of the time this error occurred, and the actions you performed just before this error.

More information about this error may be available in the server error log.