--- loncom/interface/loncommon.pm	2003/08/20 18:18:45	1.112
+++ loncom/interface/loncommon.pm	2003/11/04 19:01:01	1.142
@@ -1,7 +1,7 @@
 # The LearningOnline Network with CAPA
 # a pile of common routines
 #
-# $Id: loncommon.pm,v 1.112 2003/08/20 18:18:45 bowersj2 Exp $
+# $Id: loncommon.pm,v 1.142 2003/11/04 19:01:01 albertel Exp $
 #
 # Copyright Michigan State University Board of Trustees
 #
@@ -68,6 +68,9 @@ use POSIX qw(strftime mktime);
 use Apache::Constants qw(:common :http :methods);
 use Apache::lonmsg();
 use Apache::lonmenu();
+use Apache::lonlocal;
+use HTML::Entities;
+
 my $readit;
 
 =pod 
@@ -78,6 +81,7 @@ my $readit;
 
 # ----------------------------------------------- Filetypes/Languages/Copyright
 my %language;
+my %supported_language;
 my %cprtag;
 my %fe; my %fd;
 my %category_extensions;
@@ -144,8 +148,11 @@ BEGIN {
 	    while (<$fh>) {
 		next if /^\#/;
 		chomp;
-		my ($key,$two,$country,$three,$enc,$val)=(split(/\t/,$_));
+		my ($key,$two,$country,$three,$enc,$val,$sup)=(split(/\t/,$_));
 		$language{$key}=$val.' - '.$enc;
+		if ($sup) {
+		    $supported_language{$key}=$sup;
+		}
 	    }
 	}
     }
@@ -268,7 +275,7 @@ of the element the selection from the se
 sub browser_and_searcher_javascript {
     return <<END;
     var editbrowser = null;
-    function openbrowser(formname,elementname,only,omit) {
+    function openbrowser(formname,elementname,only,omit,titleelement) {
         var url = '/res/?';
         if (editbrowser == null) {
             url += 'launch=1&';
@@ -282,6 +289,9 @@ sub browser_and_searcher_javascript {
         if (omit != null) {
             url += 'omit=' + omit + '&';
         }
+        if (titleelement != null) {
+            url += 'titleelement=' + titleelement + '&';
+        }
         url += 'element=' + elementname + '';
         var title = 'Browser';
         var options = 'scrollbars=1,resizable=1,menubar=0';
@@ -290,7 +300,7 @@ sub browser_and_searcher_javascript {
         editbrowser.focus();
     }
     var editsearcher;
-    function opensearcher(formname,elementname) {
+    function opensearcher(formname,elementname,titleelement) {
         var url = '/adm/searchcat?';
         if (editsearcher == null) {
             url += 'launch=1&';
@@ -298,6 +308,9 @@ sub browser_and_searcher_javascript {
         url += 'catalogmode=interactive&';
         url += 'mode=edit&';
         url += 'form=' + formname + '&';
+        if (titleelement != null) {
+            url += 'titleelement=' + titleelement + '&';
+        }
         url += 'element=' + elementname + '';
         var title = 'Search';
         var options = 'scrollbars=1,resizable=1,menubar=0';
@@ -346,17 +359,18 @@ sub selectstudent_link {
 	   return '';
        }
        return "<a href='".'javascript:openstdbrowser("'.$form.'","'.$unameele.
-        '","'.$udomele.'");'."'>Select User</a>";
+        '","'.$udomele.'");'."'>".&mt('Select User')."</a>";
    }
    if ($ENV{'request.role'}=~/^(au|dc|su)/) {
        return "<a href='".'javascript:openstdbrowser("'.$form.'","'.$unameele.
-        '","'.$udomele.'",1);'."'>Select User</a>";
+        '","'.$udomele.'",1);'."'>".&mt('Select User')."</a>";
    }
    return '';
 }
 
 sub coursebrowser_javascript {
-   return (<<'ENDSTDBRW');
+    my ($domainfilter)=@_;
+   return (<<ENDSTDBRW);
 <script type="text/javascript" language="Javascript" >
     var stdeditbrowser;
     function opencrsbrowser(formname,uname,udom) {
@@ -367,6 +381,12 @@ sub coursebrowser_javascript {
                url += 'filter='+filter+'&';
 	   }
         }
+        var domainfilter='$domainfilter';
+        if (domainfilter != null) {
+           if (domainfilter != '') {
+               url += 'domainfilter='+domainfilter+'&';
+	   }
+        }
         url += 'form=' + formname + '&cnumelement='+uname+
                                     '&cdomelement='+udom;
         var title = 'Course_Browser';
@@ -382,7 +402,7 @@ ENDSTDBRW
 sub selectcourse_link {
    my ($form,$unameele,$udomele)=@_;
     return "<a href='".'javascript:opencrsbrowser("'.$form.'","'.$unameele.
-        '","'.$udomele.'");'."'>Select Course</a>";
+        '","'.$udomele.'");'."'>".&mt('Select Course')."</a>";
 }
 
 =pod
@@ -514,7 +534,7 @@ END
     foreach my $value (sort(keys(%$hashref))) {
         $result.="    <option value=\"$value\" ";
         $result.=" selected=\"true\" " if ($value eq $firstdefault);
-        $result.=">$hashref->{$value}->{'text'}</option>\n";
+        $result.=">".&mt($hashref->{$value}->{'text'})."</option>\n";
     }
     $result .= "</select>\n";
     my %select2 = %{$hashref->{$firstdefault}->{'select2'}};
@@ -524,7 +544,7 @@ END
     foreach my $value (sort(keys(%select2))) {
         $result.="    <option value=\"$value\" ";        
         $result.=" selected=\"true\" " if ($value eq $seconddefault);
-        $result.=">$select2{$value}</option>\n";
+        $result.=">".&mt($select2{$value})."</option>\n";
     }
     $result .= "</select>\n";
     #    return $debug;
@@ -624,8 +644,6 @@ sub helpLatexCheatsheet {
 Translate $text to allow it to be output as a 'comma seperated values' 
 format.
 
-=back
-
 =cut
 
 sub csv_translate {
@@ -635,6 +653,91 @@ sub csv_translate {
     return $text;
 }
 
+=pod
+
+=item * change_content_javascript():
+
+This and the next function allow you to create small sections of an
+otherwise static HTML page that you can update on the fly with
+Javascript, even in Netscape 4.
+
+The Javascript fragment returned by this function (no E<lt>scriptE<gt> tag)
+must be written to the HTML page once. It will prove the Javascript
+function "change(name, content)". Calling the change function with the
+name of the section 
+you want to update, matching the name passed to C<changable_area>, and
+the new content you want to put in there, will put the content into
+that area.
+
+B<Note>: Netscape 4 only reserves enough space for the changable area
+to contain room for the original contents. You need to "make space"
+for whatever changes you wish to make, and be B<sure> to check your
+code in Netscape 4. This feature in Netscape 4 is B<not> powerful;
+it's adequate for updating a one-line status display, but little more.
+This script will set the space to 100% width, so you only need to
+worry about height in Netscape 4.
+
+Modern browsers are much less limiting, and if you can commit to the
+user not using Netscape 4, this feature may be used freely with
+pretty much any HTML.
+
+=cut
+
+sub change_content_javascript {
+    # If we're on Netscape 4, we need to use Layer-based code
+    if ($ENV{'browser.type'} eq 'netscape' &&
+	$ENV{'browser.version'} =~ /^4\./) {
+	return (<<NETSCAPE4);
+	function change(name, content) {
+	    doc = document.layers[name+"___escape"].layers[0].document;
+	    doc.open();
+	    doc.write(content);
+	    doc.close();
+	}
+NETSCAPE4
+    } else {
+	# Otherwise, we need to use semi-standards-compliant code
+	# (technically, "innerHTML" isn't standard but the equivalent
+	# is really scary, and every useful browser supports it
+	return (<<DOMBASED);
+	function change(name, content) {
+	    element = document.getElementById(name);
+	    element.innerHTML = content;
+	}
+DOMBASED
+    }
+}
+
+=pod
+
+=item * changable_area($name, $origContent):
+
+This provides a "changable area" that can be modified on the fly via
+the Javascript code provided in C<change_content_javascript>. $name is
+the name you will use to reference the area later; do not repeat the
+same name on a given HTML page more then once. $origContent is what
+the area will originally contain, which can be left blank.
+
+=cut
+
+sub changable_area {
+    my ($name, $origContent) = @_;
+
+    if ($ENV{'browser.type'} eq 'netscape' &&
+	$ENV{'browser.version'} =~ /^4\./) {
+	# If this is netscape 4, we need to use the Layer tag
+	return "<ilayer width='100%' id='${name}___escape' overflow='none'><layer width='100%' id='$name' overflow='none'>$origContent</layer></ilayer>";
+    } else {
+	return "<span id='$name'>$origContent</span>";
+    }
+}
+
+=pod
+
+=back
+
+=cut
+
 ###############################################################
 ##        Home server <option> list generating code          ##
 ###############################################################
@@ -679,10 +782,16 @@ See lonrights.pm for an example invocati
 sub select_form {
     my ($def,$name,%hash) = @_;
     my $selectform = "<select name=\"$name\" size=\"1\">\n";
-    foreach (sort keys %hash) {
+    my @keys;
+    if (exists($hash{'select_form_order'})) {
+	@keys=@{$hash{'select_form_order'}};
+    } else {
+	@keys=sort(keys(%hash));
+    }
+    foreach (@keys) {
         $selectform.="<option value=\"$_\" ".
             ($_ eq $def ? 'selected' : '').
-                ">".$hash{$_}."</option>\n";
+                ">".&mt($hash{$_})."</option>\n";
     }
     $selectform.="</select>";
     return $selectform;
@@ -1420,7 +1529,10 @@ returns description of a specified langu
 =cut
 
 sub languagedescription {
-    return $language{shift(@_)};
+    my $code=shift;
+    return  ($supported_language{$code}?'* ':'').
+            $language{$code}.
+	    ($supported_language{$code}?' ('.&mt('interface available').')':'');
 }
 
 =pod
@@ -1528,16 +1640,8 @@ sub fileextensions {
 
 sub display_languages {
     my %languages=();
-    if ($ENV{'environment.languages'}) {
-	foreach (split(/\s*(\,|\;|\:)\s*/,$ENV{'environment.languages'})) {
-	    $languages{$_}=1;
-        }
-    }
-    if ($ENV{'course.'.$ENV{'request.course.id'}.'.languages'}) {
-	foreach (split(/\s*(\,|\;|\:)\s*/,
-	$ENV{'course.'.$ENV{'request.course.id'}.'.languages'})) {
-	    $languages{$_}=1;
-        }
+    foreach (&preferred_languages()) {
+	$languages{$_}=1;
     }
     &get_unprocessed_cgi($ENV{'QUERY_STRING'},['displaylanguage']);
     if ($ENV{'form.displaylanguage'}) {
@@ -1548,6 +1652,45 @@ sub display_languages {
     return %languages;
 }
 
+sub preferred_languages {
+    my @languages=();
+    if ($ENV{'environment.languages'}) {
+	@languages=split(/\s*(\,|\;|\:)\s*/,$ENV{'environment.languages'});
+    }
+    if ($ENV{'course.'.$ENV{'request.course.id'}.'.languages'}) {
+	@languages=(@languages,split(/\s*(\,|\;|\:)\s*/,
+	         $ENV{'course.'.$ENV{'request.course.id'}.'.languages'}));
+    }
+    my $browser=(split(/\;/,$ENV{'HTTP_ACCEPT_LANGUAGE'}))[0];
+    if ($browser) {
+	@languages=(@languages,split(/\s*(\,|\;|\:)\s*/,$browser));
+    }
+    if ($Apache::lonnet::domain_lang_def{$ENV{'user.domain'}}) {
+	@languages=(@languages,
+		$Apache::lonnet::domain_lang_def{$ENV{'user.domain'}});
+    }
+    if ($Apache::lonnet::domain_lang_def{$ENV{'request.role.domain'}}) {
+	@languages=(@languages,
+		$Apache::lonnet::domain_lang_def{$ENV{'request.role.domain'}});
+    }
+    if ($Apache::lonnet::domain_lang_def{
+	                          $Apache::lonnet::perlvar{'lonDefDomain'}}) {
+	@languages=(@languages,
+		$Apache::lonnet::domain_lang_def{
+                                  $Apache::lonnet::perlvar{'lonDefDomain'}});
+    }
+# turn "en-ca" into "en-ca,en"
+    my @genlanguages;
+    foreach (@languages) {
+	unless ($_=~/\w/) { next; }
+	push (@genlanguages,$_);
+	if ($_=~/(\-|\_)/) {
+	    push (@genlanguages,(split(/(\-|\_)/,$_))[0]);
+	}
+    }
+    return @genlanguages;
+}
+
 ###############################################################
 ##               Student Answer Attempts                     ##
 ###############################################################
@@ -1628,7 +1771,7 @@ sub get_previous_attempt {
 	       } else {
 		  $value=$returnhash{$version.':'.$_};
 	       }
-	       $prevattempts.='<td>'.$value.'&nbsp;</td>';   
+	       $prevattempts.='<td>'.&Apache::lonnet::unescape($value).'&nbsp;</td>';   
 	    }
 	 }
       }
@@ -1640,6 +1783,7 @@ sub get_previous_attempt {
 	} else {
 	  $value=$lasthash{$_};
 	}
+	$value=&Apache::lonnet::unescape($value);
 	if ($_ =~/$regexp$/ && (defined &$gradesub)) {$value = &$gradesub($value)}
 	$prevattempts.='<td>'.$value.'&nbsp;</td>';
       }
@@ -1697,7 +1841,7 @@ show a snapshot of what student was look
 
 sub get_student_view {
   my ($symb,$username,$domain,$courseid,$target) = @_;
-  my ($map,$id,$feedurl) = split(/___/,$symb);
+  my ($map,$id,$feedurl) = &Apache::lonnet::decode_symb($symb);
   my (%old,%moreenv);
   my @elements=('symb','courseid','domain','username');
   foreach my $element (@elements) {
@@ -1733,7 +1877,7 @@ show a snapshot of how student was answe
 
 sub get_student_answers {
   my ($symb,$username,$domain,$courseid,%form) = @_;
-  my ($map,$id,$feedurl) = split(/___/,$symb);
+  my ($map,$id,$feedurl) = &Apache::lonnet::decode_symb($symb);
   my (%old,%moreenv);
   my @elements=('symb','courseid','domain','username');
   foreach my $element (@elements) {
@@ -1752,6 +1896,30 @@ sub get_student_answers {
 
 =pod
 
+=item * &submlink()
+
+Inputs: $text $uname $udom $symb
+
+Returns: A link to grades.pm such as to see the SUBM view of a student
+
+=cut
+
+###############################################
+sub submlink {
+    my ($text,$uname,$udom,$symb)=@_;
+    if (!($uname && $udom)) {
+	(my $cursymb, my $courseid,$udom,$uname)=
+	    &Apache::lonxml::whichuser($symb);
+	if (!$symb) { $symb=$cursymb; }
+    }
+    if (!$symb) { $symb=&symbread(); }
+    return '<a href="/adm/grades?symb='.$symb.'&student='.$uname.
+	'&userdom='.$udom.'&command=submission">'.$text.'</a>';
+}
+##############################################
+
+=pod
+
 =back
 
 =cut
@@ -1952,6 +2120,7 @@ other decorations will be returned.
 
 sub bodytag {
     my ($title,$function,$addentries,$bodyonly,$domain,$forcereg)=@_;
+    $title=&mt($title);
     unless ($function) {
 	$function='student';
         if ($ENV{'request.role'}=~/^(cc|in|ta|ep)/) {
@@ -2180,6 +2349,14 @@ sub no_cache {
   #$r->header_out("Expires" => $date);
 }
 
+sub content_type {
+  my ($r,$type,$charset) = @_;
+  unless ($charset) {
+      $charset=&Apache::lonlocal::current_encoding;
+  }
+  $r->content_type($type.($charset?'; charset='.$charset:''));
+}
+
 =pod
 
 =item * add_to_env($name,$value) 
@@ -2460,6 +2637,23 @@ sub csv_samples_select_table {
 
 =pod
 
+=item clean_excel_name($name)
+
+Returns a replacement for $name which does not contain any illegal characters.
+
+=cut
+
+sub clean_excel_name {
+    my ($name) = @_;
+    $name =~ s/[:\*\?\/\\]//g;
+    if (length($name) > 31) {
+        $name = substr($name,0,31);
+    }
+    return $name;
+}
+
+=pod
+
 =item * check_if_partid_hidden($id,$symb,$udom,$uname)
 
 Returns either 1 or undef
@@ -2477,15 +2671,392 @@ $uname, optional the username of the use
 
 sub check_if_partid_hidden {
     my ($id,$symb,$udom,$uname) = @_;
-    my $hiddenparts=&Apache::lonnet::EXT('resource.0.parameter_hiddenparts',
+    my $hiddenparts=&Apache::lonnet::EXT('resource.0.hiddenparts',
 					 $symb,$udom,$uname);
+    my $truth=1;
+    #if the string starts with !, then the list is the list to show not hide
+    if ($hiddenparts=~s/^\s*!//) { $truth=undef; }
     my @hiddenlist=split(/,/,$hiddenparts);
     foreach my $checkid (@hiddenlist) {
-	if ($checkid =~ /^\s*\Q$id\E\s*$/) { return 1; }
+	if ($checkid =~ /^\s*\Q$id\E\s*$/) { return $truth; }
     }
-    return undef;
+    return !$truth;
 }
 
+
+############################################################
+############################################################
+
+=pod
+
+=head1 cgi-bin script and graphing routines
+
+=item get_cgi_id
+
+Inputs: none
+
+Returns an id which can be used to pass environment variables
+to various cgi-bin scripts.  These environment variables will
+be removed from the users environment after a given time by
+the routine &Apache::lonnet::transfer_profile_to_env.
+
+=cut
+
+############################################################
+############################################################
+
+sub get_cgi_id {
+    return (time.'_'.int(rand(1000)));
+}
+
+############################################################
+############################################################
+
+=pod
+
+=item DrawBarGraph
+
+Facilitates the plotting of data in a (stacked) bar graph.
+Puts plot definition data into the users environment in order for 
+graph.png to plot it.  Returns an <img> tag for the plot.
+The bars on the plot are labeled '1','2',...,'n'.
+
+Inputs:
+
+=over 4
+
+=item $Title: string, the title of the plot
+
+=item $xlabel: string, text describing the X-axis of the plot
+
+=item $ylabel: string, text describing the Y-axis of the plot
+
+=item $Max: scalar, the maximum Y value to use in the plot
+If $Max is < any data point, the graph will not be rendered.
+
+=item $colors: array ref holding the colors to be used for the data sets when
+they are plotted.  If undefined, default values will be used.
+
+=item @Values: An array of array references.  Each array reference holds data
+to be plotted in a stacked bar chart.
+
+=back
+
+Returns:
+
+An <img> tag which references graph.png and the appropriate identifying
+information for the plot.
+
+=cut
+
+############################################################
+############################################################
+sub DrawBarGraph {
+    my ($Title,$xlabel,$ylabel,$Max,$colors,@Values)=@_;
+    #
+    if (! defined($colors)) {
+        $colors = ['#33ff00', 
+                  '#0033cc', '#990000', '#aaaa66', '#663399', '#ff9933',
+                  '#66ccff', '#ff9999', '#cccc33', '#660000', '#33cc66',
+                  ]; 
+    }
+    #
+    my $identifier = &get_cgi_id();
+    my $id = 'cgi.'.$identifier;        
+    if (! @Values || ref($Values[0]) ne 'ARRAY') {
+        return '';
+    }
+    my $NumBars = scalar(@{$Values[0]});
+    my %ValuesHash;
+    my $NumSets=1;
+    foreach my $array (@Values) {
+        next if (! ref($array));
+        $ValuesHash{$id.'.data.'.$NumSets++} = 
+            join(',',@$array);
+    }
+    #
+    my ($height,$width,$xskip,$bar_width) = (200,120,1,15);
+    if ($NumBars < 10) {
+        $width = 120+$NumBars*15;
+        $xskip = 1;
+        $bar_width = 15;
+    } elsif ($NumBars <= 25) {
+        $width = 120+$NumBars*11;
+        $xskip = 5;
+        $bar_width = 8;
+    } elsif ($NumBars <= 50) {
+        $width = 120+$NumBars*8;
+        $xskip = 5;
+        $bar_width = 4;
+    } else {
+        $width = 120+$NumBars*8;
+        $xskip = 5;
+        $bar_width = 4;
+    }
+    #
+    my @Labels;
+    for (my $i=0;$i<@{$Values[0]};$i++) {
+        push (@Labels,$i+1);
+    }
+    #
+    $Max = 1 if ($Max < 1);
+    if ( int($Max) < $Max ) {
+        $Max++;
+        $Max = int($Max);
+    }
+    $Title  = '' if (! defined($Title));
+    $xlabel = '' if (! defined($xlabel));
+    $ylabel = '' if (! defined($ylabel));
+    $ValuesHash{$id.'.title'}    = &Apache::lonnet::escape($Title);
+    $ValuesHash{$id.'.xlabel'}   = &Apache::lonnet::escape($xlabel);
+    $ValuesHash{$id.'.ylabel'}   = &Apache::lonnet::escape($ylabel);
+    $ValuesHash{$id.'.y_max_value'} = $Max;
+    $ValuesHash{$id.'.NumBars'}  = $NumBars;
+    $ValuesHash{$id.'.NumSets'}  = $NumSets;
+    $ValuesHash{$id.'.PlotType'} = 'bar';
+    $ValuesHash{$id.'.Colors'}   = join(',',@{$colors});
+    $ValuesHash{$id.'.height'}   = $height;
+    $ValuesHash{$id.'.width'}    = $width;
+    $ValuesHash{$id.'.xskip'}    = $xskip;
+    $ValuesHash{$id.'.bar_width'} = $bar_width;
+    $ValuesHash{$id.'.labels'} = join(',',@Labels);
+    #
+    &Apache::lonnet::appenv(%ValuesHash);
+    return '<img src="/cgi-bin/graph.png?'.$identifier.'" border="1" />';
+}
+
+############################################################
+############################################################
+
+=pod
+
+=item DrawXYGraph
+
+Facilitates the plotting of data in an XY graph.
+Puts plot definition data into the users environment in order for 
+graph.png to plot it.  Returns an <img> tag for the plot.
+
+Inputs:
+
+=over 4
+
+=item $Title: string, the title of the plot
+
+=item $xlabel: string, text describing the X-axis of the plot
+
+=item $ylabel: string, text describing the Y-axis of the plot
+
+=item $Max: scalar, the maximum Y value to use in the plot
+If $Max is < any data point, the graph will not be rendered.
+
+=item $colors: Array ref containing the hex color codes for the data to be 
+plotted in.  If undefined, default values will be used.
+
+=item $Xlabels: Array ref containing the labels to be used for the X-axis.
+
+=item $Ydata: Array ref containing Array refs.  
+Each of the contained arrays will be plotted as a seperate curve.
+
+=item %Values: hash indicating or overriding any default values which are 
+passed to graph.png.  
+Possible values are: width, xskip, x_ticks, x_tick_offset, among others.
+
+=back
+
+Returns:
+
+An <img> tag which references graph.png and the appropriate identifying
+information for the plot.
+
+=cut
+
+############################################################
+############################################################
+sub DrawXYGraph {
+    my ($Title,$xlabel,$ylabel,$Max,$colors,$Xlabels,$Ydata,%Values)=@_;
+    #
+    # Create the identifier for the graph
+    my $identifier = &get_cgi_id();
+    my $id = 'cgi.'.$identifier;
+    #
+    $Title  = '' if (! defined($Title));
+    $xlabel = '' if (! defined($xlabel));
+    $ylabel = '' if (! defined($ylabel));
+    my %ValuesHash = 
+        (
+         $id.'.title'  => &Apache::lonnet::escape($Title),
+         $id.'.xlabel' => &Apache::lonnet::escape($xlabel),
+         $id.'.ylabel' => &Apache::lonnet::escape($ylabel),
+         $id.'.y_max_value'=> $Max,
+         $id.'.labels'     => join(',',@$Xlabels),
+         $id.'.PlotType'   => 'XY',
+         );
+    #
+    if (defined($colors) && ref($colors) eq 'ARRAY') {
+        $ValuesHash{$id.'.Colors'}   = join(',',@{$colors});
+    }
+    #
+    if (! ref($Ydata) || ref($Ydata) ne 'ARRAY') {
+        return '';
+    }
+    my $NumSets=1;
+    foreach my $array (@{$Ydata}){
+        next if (! ref($array));
+        $ValuesHash{$id.'.data.'.$NumSets++} = join(',',@$array);
+    }
+    $ValuesHash{$id.'.NumSets'} = $NumSets-1;
+    #
+    # Deal with other parameters
+    while (my ($key,$value) = each(%Values)) {
+        $ValuesHash{$id.'.'.$key} = $value;
+    }
+    #
+    &Apache::lonnet::appenv(%ValuesHash);
+    return '<img src="/cgi-bin/graph.png?'.$identifier.'" border="1" />';
+}
+
+############################################################
+############################################################
+
+=pod
+
+=item DrawXYYGraph
+
+Facilitates the plotting of data in an XY graph with two Y axes.
+Puts plot definition data into the users environment in order for 
+graph.png to plot it.  Returns an <img> tag for the plot.
+
+Inputs:
+
+=over 4
+
+=item $Title: string, the title of the plot
+
+=item $xlabel: string, text describing the X-axis of the plot
+
+=item $ylabel: string, text describing the Y-axis of the plot
+
+=item $colors: Array ref containing the hex color codes for the data to be 
+plotted in.  If undefined, default values will be used.
+
+=item $Xlabels: Array ref containing the labels to be used for the X-axis.
+
+=item $Ydata1: The first data set
+
+=item $Min1: The minimum value of the left Y-axis
+
+=item $Max1: The maximum value of the left Y-axis
+
+=item $Ydata2: The second data set
+
+=item $Min2: The minimum value of the right Y-axis
+
+=item $Max2: The maximum value of the left Y-axis
+
+=item %Values: hash indicating or overriding any default values which are 
+passed to graph.png.  
+Possible values are: width, xskip, x_ticks, x_tick_offset, among others.
+
+=back
+
+Returns:
+
+An <img> tag which references graph.png and the appropriate identifying
+information for the plot.
+
+=cut
+
+############################################################
+############################################################
+sub DrawXYYGraph {
+    my ($Title,$xlabel,$ylabel,$colors,$Xlabels,$Ydata1,$Min1,$Max1,
+                                        $Ydata2,$Min2,$Max2,%Values)=@_;
+    #
+    # Create the identifier for the graph
+    my $identifier = &get_cgi_id();
+    my $id = 'cgi.'.$identifier;
+    #
+    $Title  = '' if (! defined($Title));
+    $xlabel = '' if (! defined($xlabel));
+    $ylabel = '' if (! defined($ylabel));
+    my %ValuesHash = 
+        (
+         $id.'.title'  => &Apache::lonnet::escape($Title),
+         $id.'.xlabel' => &Apache::lonnet::escape($xlabel),
+         $id.'.ylabel' => &Apache::lonnet::escape($ylabel),
+         $id.'.labels' => join(',',@$Xlabels),
+         $id.'.PlotType' => 'XY',
+         $id.'.NumSets' => 2,
+         $id.'.two_axes' => 1,
+         $id.'.y1_max_value' => $Max1,
+         $id.'.y1_min_value' => $Min1,
+         $id.'.y2_max_value' => $Max2,
+         $id.'.y2_min_value' => $Min2,
+         );
+    #
+    if (defined($colors) && ref($colors) eq 'ARRAY') {
+        $ValuesHash{$id.'.Colors'}   = join(',',@{$colors});
+    }
+    #
+    if (! ref($Ydata1) || ref($Ydata1) ne 'ARRAY' ||
+        ! ref($Ydata2) || ref($Ydata2) ne 'ARRAY'){
+        return '';
+    }
+    my $NumSets=1;
+    foreach my $array ($Ydata1,$Ydata2){
+        next if (! ref($array));
+        $ValuesHash{$id.'.data.'.$NumSets++} = join(',',@$array);
+    }
+    #
+    # Deal with other parameters
+    while (my ($key,$value) = each(%Values)) {
+        $ValuesHash{$id.'.'.$key} = $value;
+    }
+    #
+    &Apache::lonnet::appenv(%ValuesHash);
+    return '<img src="/cgi-bin/graph.png?'.$identifier.'" border="1" />';
+}
+
+############################################################
+############################################################
+
+=pod
+
+=head1 Statistics helper routines?  
+
+Bad place for them but what the hell.
+
+=item &chartlink
+
+Returns a link to the chart for a specific student.  
+
+Inputs:
+
+=over 4
+
+=item $linktext: The text of the link
+
+=item $sname: The students username
+
+=item $sdomain: The students domain
+
+=back
+
+=cut
+
+############################################################
+############################################################
+sub chartlink {
+    my ($linktext, $sname, $sdomain) = @_;
+    my $link = '<a href="/adm/statistics?reportSelected=student_assessment'.
+        '&SelectedStudent='.&Apache::lonnet::escape($sname.':'.$sdomain).
+        '&chartoutputmode='.HTML::Entities::encode('html, with all links').
+       '">'.$linktext.'</a>';
+}
+
+############################################################
+############################################################
+
 =pod
 
 =back