File:  [LON-CAPA] / loncom / homework / radiobuttonresponse.pm
Revision 1.158: download - view: text, annotated - select for diffs
Mon Jan 19 15:35:53 2015 UTC (9 years, 3 months ago) by goltermann
Branches: MAIN
CVS tags: HEAD
authoring space overhaul
this update tries to improve the user experience of the authoring space.

added codemirror for xml editor and script tags in colorful editor
added possibility to deactivate codemirror in author settings
added dropdown menu to insert problem templates into xml editor (thanks to tobias reinhardt)
added feature of saving current scrollposition on save when editing problems
added possibility to fold blocks in colorful editor, this state will be saved and restored
added shortcuts to create empty problems, html files and directories

and other smaller features and bugfixes

# The LearningOnline Network with CAPA
# mutliple choice style responses
#
# $Id: radiobuttonresponse.pm,v 1.158 2015/01/19 15:35:53 goltermann Exp $
#
# Copyright Michigan State University Board of Trustees
#
# This file is part of the LearningOnline Network with CAPA (LON-CAPA).
#
# LON-CAPA is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# LON-CAPA is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with LON-CAPA; if not, write to the Free Software# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
#
# /home/httpd/html/adm/gpl.txt
#
# http://www.lon-capa.org/
#



package Apache::radiobuttonresponse;
use strict;
use HTML::Entities();
use Apache::lonlocal;
use Apache::lonnet;
use Apache::response;
use Apache::caparesponse;

my $default_bubbles_per_line = 10;
my @alphabet      = ( 'A' .. 'Z' ); # Foil labels.




BEGIN {
    &Apache::lonxml::register('Apache::radiobuttonresponse',('radiobuttonresponse'));
}

#---------------------------------------------------------------------------
#
#  Generic utility subs.

sub bubble_line_count {
    my ($numfoils, $bubbles_per_line) = @_;
    my $bubble_lines;
    $bubble_lines = int($numfoils / $bubbles_per_line);
    if (($numfoils % $bubbles_per_line) != 0) {
	$bubble_lines++;
    }
    return $bubble_lines;
    
}



#------------------------------------------------------------------------------
#
#  XML handlers.
sub start_radiobuttonresponse {
    my ($target,$token,$tagstack,$parstack,$parser,$safeeval,$style)=@_;
    my $result;

    #when in a radiobutton response use these
    &Apache::lonxml::register('Apache::radiobuttonresponse',('foilgroup','foil','conceptgroup'));
    push (@Apache::lonxml::namespace,'radiobuttonresponse');
    my $id = &Apache::response::start_response($parstack,$safeeval);

    %Apache::hint::radiobutton=();
    undef(%Apache::response::foilnames);
    if ($target eq 'meta') {
	$result=&Apache::response::meta_package_write('radiobuttonresponse');
    } elsif ($target eq 'edit' ) {
	$result.=&Apache::edit::start_table($token)
           .'<tr><td>'.&Apache::loncommon::insert_folding_button()
           .&Apache::lonxml::description($token)
           .&Apache::loncommon::help_open_topic('Radio_Response_Problems')
           .'</td>'
           .'<td><span class="LC_nobreak">'.&mt('Delete?').' '
           .&Apache::edit::deletelist($target,$token)
           .'</span></td>'
           .'<td>&nbsp;'.&Apache::edit::end_row()
           .&Apache::edit::start_spanning_row();
	$result.=
	    &Apache::edit::text_arg('Max Number Of Shown Foils:','max',
				    $token,'4').
	    &Apache::edit::select_arg('Randomize Foil Order:','randomize',
				      ['yes','no'],$token).
	    &Apache::edit::select_arg('Display Direction:','direction',
				      ['vertical','horizontal'],$token).
				      &Apache::edit::end_row().
				      &Apache::edit::start_spanning_row()."\n";
    } elsif ($target eq 'modified') {
	my $constructtag=&Apache::edit::get_new_args($token,$parstack,
						     $safeeval,'max',
						     'randomize','direction');
	if ($constructtag) { $result = &Apache::edit::rebuild_tag($token); }

   } elsif ( $target eq 'tex' ) {
        my $type =
          &Apache::lonxml::get_param( 'TeXtype', $parstack, $safeeval, undef,
            0 );
        if ( $type eq '1' ) {
            $result .= ' \renewcommand{\labelenumi}{\arabic{enumi}.}';
        }
        elsif ( $type eq 'A' ) {
            $result .= ' \renewcommand{\labelenumi}{\Alph{enumi}.}';
        }
        elsif ( $type eq 'a' ) {
            $result .= ' \renewcommand{\labelenumi}{\alph{enumi}.}';
        }
        elsif ( $type eq 'i' ) {
            $result .= ' \renewcommand{\labelenumi}{\roman{enumi}.}';
        }
        else {
            $result .= ' \renewcommand{\labelenumi}{\Alph{enumi}.}';
        }

    }
    elsif ( $target eq 'analyze' ) {
        my $part_id = "$Apache::inputtags::part.$id";
        $Apache::lonhomework::analyze{"$part_id.type"} = 'radiobuttonresponse';
	push (@{ $Apache::lonhomework::analyze{"parts"} },$part_id);
    }
    return $result;
}

sub end_radiobuttonresponse {
    my ($target,$token,$tagstack,$parstack,$parser,$safeeval,$style)=@_;
    my $result;
    if ( $target eq 'edit' ) { 
	$result = &Apache::edit::end_table(); 
    }
    &Apache::response::end_response;
    pop @Apache::lonxml::namespace;
    &Apache::lonxml::deregister('Apache::radiobuttonresponse',('foilgroup','foil','conceptgroup'));
    undef(%Apache::response::foilnames);
    return $result;
}

%Apache::response::foilgroup=();
sub start_foilgroup {
    my ($target,$token,$tagstack,$parstack,$parser,$safeeval,$style)=@_;
    %Apache::response::foilgroup=();
    $Apache::radiobuttonresponse::conceptgroup=0;
    &Apache::response::pushrandomnumber(undef,$target);
    return;
}

sub storesurvey {
    my ($style) = @_;
    if ( !&Apache::response::submitted() ) { return ''; }
    my $response = $env{'form.HWVAL_'.$Apache::inputtags::response['-1']};
    &Apache::lonxml::debug("Here I am!:$response:");
    if ( $response !~ /[0-9]+/) { return ''; }
    my $part = $Apache::inputtags::part;
    my $id = $Apache::inputtags::response['-1'];
    my @whichfoils=@{ $Apache::response::foilgroup{'names'} };
    my %responsehash;
    $responsehash{$whichfoils[$response]}=$response;
    my $responsestr=&Apache::lonnet::hash2str(%responsehash);
    $Apache::lonhomework::results{"resource.$part.$id.submission"}=
	$responsestr;
    my %previous=&Apache::response::check_for_previous($responsestr,$part,$id);
    my $ad;
    if ($style eq 'anonsurvey') {
        $ad=$Apache::lonhomework::results{"resource.$part.$id.awarddetail"}='ANONYMOUS';
    } elsif ($style eq 'anonsurveycred') {
        $ad=$Apache::lonhomework::results{"resource.$part.$id.awarddetail"}='ANONYMOUS_CREDIT';
    } elsif ($style eq 'surveycred') {
        $ad=$Apache::lonhomework::results{"resource.$part.$id.awarddetail"}='SUBMITTED_CREDIT';
    } else {
        $ad=$Apache::lonhomework::results{"resource.$part.$id.awarddetail"}='SUBMITTED';
    }
    &Apache::response::handle_previous(\%previous,$ad);
    &Apache::lonxml::debug("submitted a $response<br />\n");
    return '';
}


sub grade_response {
    my ($answer, $whichfoils, $bubbles_per_line)=@_;

    if ( !&Apache::response::submitted() ) { return; }
    my $response;
    
    if ($env{'form.submitted'} eq 'scantron') {
	$response = &Apache::response::getresponse(1,undef,
						   &bubble_line_count(scalar(@{ $whichfoils}),
								      $bubbles_per_line),
						   $bubbles_per_line);

    } else {
	$response = $env{'form.HWVAL_'.$Apache::inputtags::response['-1']};
    }


    if ( $response !~ /[0-9]+/) { return; }
    my $part=$Apache::inputtags::part;
    my $id = $Apache::inputtags::response['-1'];
    my %responsehash;
    $responsehash{$whichfoils->[$response]}=$response;
    my $responsestr=&Apache::lonnet::hash2str(%responsehash);
    my %previous=&Apache::response::check_for_previous($responsestr,
						       $part,$id);
    $Apache::lonhomework::results{"resource.$part.$id.submission"}=
	$responsestr;
    &Apache::lonxml::debug("submitted a $response<br />\n");
    my $ad;
    if ($response == $answer) {
	$ad='EXACT_ANS';
    } else {
	$ad='INCORRECT';
    }
    $Apache::lonhomework::results{"resource.$part.$id.awarddetail"}=$ad;
    &Apache::response::handle_previous(\%previous,$ad);
}

sub end_foilgroup {
    my ($target,$token,$tagstack,$parstack,$parser,$safeeval,$style)=@_;

    my $result;
    my $bubble_lines;
    my $answer_count;
    my $id   = $Apache::inputtags::response['-1'];
    my $part = $Apache::inputtags::part;
    my $bubbles_per_line = &getbubblesnum($part,$id);


    if ($target eq 'grade' || $target eq 'web' || $target eq 'answer' ||
	$target eq 'tex' || $target eq 'analyze') {
	my $style = $Apache::lonhomework::type;
	my $direction = &Apache::lonxml::get_param('direction',$parstack,
						   $safeeval,'-2');
	if ( (($style eq 'survey') || ($style eq 'surveycred') || 
              ($style eq 'anonsurvey') || ($style eq 'anonsurveycred')) 
             && ($target ne 'analyze')) {
	    if ($target eq 'web' || $target eq 'tex') {
		$result=&displayallfoils($direction, $target);
	    } elsif ( $target eq 'answer' ) {
		$result=&displayallanswers();
	    } elsif ( $target eq 'grade' ) {
		$result=&storesurvey($style);
	    }
	    $answer_count = scalar(@{$Apache::response::foilgroup{'names'}});

	} else {

	    my $name;
	    my $max = &Apache::lonxml::get_param('max',$parstack,$safeeval,
						 '-2');
	    my $randomize = &Apache::lonxml::get_param('randomize',$parstack,
						       $safeeval,'-2');
	    my ($answer, @shown) = &whichfoils($max, $randomize);
	    $answer_count = scalar(@shown);

	    if ($target eq 'web' || $target eq 'tex') {
                $result=&displayfoils($target,
				      $answer, \@shown,
				      $direction,
				      $bubbles_per_line);
	    } elsif ($target eq 'answer' ) {
		$result=&displayanswers($answer, \@shown, $bubbles_per_line);
	    } elsif ( $target eq 'grade') {
		&grade_response($answer, \@shown, $bubbles_per_line);
	    }  elsif ( $target eq 'analyze') {
		my $bubble_lines = &bubble_line_count($answer_count, 
						      $bubbles_per_line);
		&Apache::response::analyze_store_foilgroup(\@shown,
							   ['text','value','location']);
		my $part_id="$part.$id";
		push (@{ $Apache::lonhomework::analyze{"$part_id.options"} },
		      ('true','false'));

	    }
	}
	$Apache::lonxml::post_evaluate=0;
    }
    if ($target eq 'web') {
	&Apache::response::setup_prior_tries_hash(\&format_prior_answer,
						  [\%Apache::response::foilgroup]);
    }
    &Apache::response::poprandomnumber();
    $bubble_lines = &bubble_line_count($answer_count, $bubbles_per_line);
    &Apache::lonxml::increment_counter($bubble_lines,
				       "$part.$id");
    if ($target eq 'analyze') {
	&Apache::lonhomework::set_bubble_lines();
    }
    return $result;
}
sub getbubblesnum {
    my ($part,$id) = @_;
    my $bubbles_per_line;
    my $default_numbubbles = $default_bubbles_per_line;
    if (($env{'form.bubbles_per_row'} =~ /^\d+$/) &&
        ($env{'form.bubbles_per_row'} > 0)) {
        $default_numbubbles = $env{'form.bubbles_per_row'};
    }
    $bubbles_per_line =
        &Apache::response::get_response_param($part."_$id",'numbubbles',
                                              $default_numbubbles);
    return $bubbles_per_line;
}

sub getfoilcounts {
    my @names;
    my $truecnt=0;
    my $falsecnt=0;
    my $name;
    if ( $Apache::response::foilgroup{'names'} ) {
	@names= @{ $Apache::response::foilgroup{'names'} };
    }
    foreach $name (@names) {
	if ($Apache::response::foilgroup{$name.'.value'} eq 'true') {
	    $truecnt++;
	} elsif ($Apache::response::foilgroup{$name.'.value'} eq 'false') {
	    $falsecnt++;
	}
    }
    return ($truecnt,$falsecnt);
}

sub format_prior_answer {
    my ($mode,$answer,$other_data) = @_;
    my $foil_data = $other_data->[0];
    my %response = &Apache::lonnet::str2hash($answer);
    my ($name)   = keys(%response);
    return '<span class="LC_prior_radiobutton">'.
	$foil_data->{$name.'.text'}.'</span>';

}


## 
# Return the last survey response.  The logic is slightly different than that of 
# get_last_responses.  TODO: See if there are chunks of code betweenthis and
# get_last_reponses that are common and can be factored.
#
# @param $part - Problem part under consideration.
# @param $showanswer - True if answers should be shown.
# @param $id         - Problem id.
#
# @return hash reference.
# @retval reference to the has indexed by answer selection that 
#         indicates the most recent answer.
#
sub get_last_survey_response {
    my ($part, $showanswer, $id) = @_;

    my $newvariation;
    my $lastresponse;		# stringified last response.

    if (
        (
            (
                $Apache::lonhomework::history{"resource.$part.type"} eq
                'randomizetry'
            )
            || ( $Apache::lonhomework::type eq 'randomizetry' )
        )
        && ( $Apache::inputtags::status[-1] eq 'CAN_ANSWER' )
      )
    {
        if ( $env{ 'form.' . $part . '.rndseed' } ne
            $Apache::lonhomework::history{"resource.$part.rndseed"} )
        {
            $newvariation = 1;
        }
    }
    $showanswer = &Apache::response::show_answer();
    unless ($newvariation && !$showanswer) {
        if ((($env{'form.grade_username'} eq '') && ($env{'form.grade_domain'} eq '')) ||
            (($env{'form.grade_username'} eq $env{'user.name'}) &&
             ($env{'form.grade_domain'} eq $env{'user.domain'}))) {
            $lastresponse = $Apache::lonhomework::history{"resource.$part.$id.submission"};
        } else {
            unless (($Apache::lonhomework::history{"resource.$part.type"} eq 'anonsurvey') ||
                    ($Apache::lonhomework::history{"resource.$part.type"} eq 'anonsurveycred')) {
                $lastresponse = $Apache::lonhomework::history{"resource.$part.$id.submission"};
            }
        }
    }
    my %lastresponse = &Apache::lonnet::str2hash($lastresponse);

    return \%lastresponse;

}
##
# Removes the names from a foil group that are marked as unused.
#
# @param $names - reference to the array of names to filter.
#
# @return arrayref
# @retval reference to the filtered array.
#
sub remove_unused {
    my ($names) = @_;
    my @result;

    foreach my $name (@{$names}) {
	if ($Apache::response::foilgroup{$name . '.value'} ne 'unused') {
	    push(@result, $name);
	}
    }
    return \@result;
}
## 
# Displays all foils in a survey type problem for HTML rendition.
# TODO: See if there is any logic in this sub that can be shared
#      with display_foils_html
#
# @param $names        - ref to array of names of the foils to display.
# @param $part         - Problem part number.
# @param $showanswer   - If true, show the answers.
# @param $lastresponse - Ref to the last response hash.
# @param $direction    - Display direction of the radiobuttons.
#
# @return string
# @retval HTML required to display the resource in a browser.
#
sub display_survey_html {
    my ($names, $part, $showanswer, $lastresponse, $direction) = @_;
    my $result;

    # Figure out a few fragments of html that depend onthe 
    # orientation of the radiobuttons:
    # closing_html - HTML to emit at the end of the resource.
    # pre_foil     - HTML to emit prior to each foil.
    # post_foil    - HTML to emit following each foil.
    #
    #  The opening HTML is just added to the $result now
    #
    #  Figuring these outin advance compresses the loop over foils into something
    #  pretty simple:
    #
    # NOTE: There's probably a really cool way to do this with style sheets
    #       and picking the selector based on the orientation, if someone wants to puzzle
    #       that out.  In that case, probably the whole thing lives in a <div> and each
    #       foil lives in a <p>
    #


    my ($opening_html, $closing_html, $pre_foil, $post_foil) = 
	&html_direction_fragments($direction);

    $result = $opening_html;

    # Different rendering depending on whether answers are shown:
    # I played with different factorings but this seems the most concise/clear...
    # although I don't like the $showanswer conditino inside the loop.  Other things I tried
    #  - two loops..much longer code..no gain in clarity.
    #  - Using a visitor patttern passing it the rendering code chunklets and
    #    an anonymous hash reference for state data etc. Very cool but
    #    quite a bit more code and quite a bit less clear.
    
    my $temp = 0;
    foreach my $name (@{$names}) {
	$result .= $pre_foil;

	if ($showanswer) {
	    my $foiltext =  $Apache::response::foilgroup{$name . '.text'};

	    # Bold the prior  response:

	    if (defined($lastresponse->{$name})) {
		$result .= '<b>' . $foiltext . '</b>';
	    } else {
		$result .= $foiltext;
	    }
	} else {
	    $result .= &html_radiobutton(
		$part, $Apache::inputtags::response['-1'], $name, $lastresponse, $temp
	     );
	}

	$result .= $post_foil;
	$temp++;
    }


    $result .= $closing_html;
    return $result;

}

##
#  Generate LaTeX for surveys.
#  
#   @param $names - names of the foils to display.
#   @param $showanswer - flag that is true to display answers.
#   @param $lastresponse - Reference to a hash the indicates the last response.
#   @param $direction    - Orientation of foils ('horiztonal' or otherwise).
#   @param $venv         - LaTeX name for vertical env.
#
#   @return string
#   @retval LaTeX rendering of the survey question.

sub latex_survey {
    my ($names, $showanswer, $lastresponse, $direction, $venv) = @_;

    my $result;
    if ($showanswer) {
	$result .= "\\begin{$venv}";
	foreach my $name (@{$names}) {
	    
	    
	    $result .= '\item \vskip -2mm ';
	    
	    if ( defined( $lastresponse->{$name} ) ) {
		$result .= '}';
	    }
	    $result .= $Apache::response::foilgroup{ $name . '.text' } . ' ';
	}
	$result .= "\\end{$venv}";

    } elsif ( $env{'form.pdfFormFields'} eq 'yes'
	      && $Apache::inputtags::status[-1] eq 'CAN_ANSWER') {
	$result .= &display_pdf_form($names, $direction, $venv);
    } else {
	if ($direction eq 'horizontal') {
	    my @foil_texts = &get_foil_texts($names);
	    $result .=  &Apache::caparesponse::make_horizontal_latex_bubbles(
	    $names, \@foil_texts, '$\bigcirc$');
	} else {
	    $result .= "\\begin{$venv}";

	    my $temp = 0;
	    my $i    = 0;
	    foreach my $name (@{$names}) {

		$result .= '\item \vskip -2mm ';
		
		if ($env{'form.pdfFormFields'} ne 'yes'
		    or $Apache::inputtags::status[-1] ne 'CAN_ANSWER' )
		{
		    $result .=
			'$\bigcirc$'
			. $Apache::response::foilgroup{ $name . '.text' }
		    . '\\\\';    #' stupid emacs
		}
		
		$i++;	    
		$temp++;
		
		$result .= '\vskip 0 mm ';
	    }
	    $result .= "\\end{$venv}";
	}	
    }
    return $result;
}
##
#  Figure out the LaTeX environment in which to wrap the LaTeX vertical output.
#
# @return string
# @retval the environment name.  The LaTeX should be wrapped a 
#    \begin{retval} \end{retval} pair.
#
sub latex_vertical_environment {
    if ($env{'form.pdfFormFields'} eq 'yes'
	&& $Apache::inputtags::status[-1] eq 'CAN_ANSWER') {
	return 'itemize';
    } else {
	return 'enumerate';
    }
}

##
# Figure out the key html fragments that depend on the rendering direction:
#
# @param $direction - 'horizontal' for horizontal direction.
#
# @return list
# @retval (part_start, part_end, foil_start, foil_end)
# Where:
#   - part_start is the HTML to emit at the start of the part.
#   - part_end   is the HTML to emit at the end of the part.
#   - foil_start is the HTML to emit prior to each foil.
#   - foil_end is the HTML to emit after each foil
#
sub html_direction_fragments {
    my $direction = shift;
    if ($direction eq 'horizontal') {
	return ('<table><tr>', '</tr></table>', '<td>', '</td>');
    } else {
	return ('', '<br />', '<br />', '');
    }
}

##
#
#  Displays all the foils of a problem in a format suitable for
#   surveys, surveys for credit, anonymous surveys and anonymous surveys for credit.
#
#  @param $direction - Display direction of the choices ('horiztonal' or not).
#  @param $target    - Rendering target.
#
#  @return string
#  @retval Text that renders for the selected target.
# 
sub displayallfoils{
    my ( $direction, $target ) = @_;
    my $result;
    &Apache::lonxml::debug("survey style display");

    my @names;

    if ( $Apache::response::foilgroup{'names'} ) {
        @names = @{ $Apache::response::foilgroup{'names'} };
    }


    my $id   = $Apache::inputtags::response['-1'];
    my $part = $Apache::inputtags::part;
    
    my $showanswer = &Apache::response::show_answer();
    my $lastresponse = &get_last_survey_response($part, $showanswer, $id);
    my $used_names = &remove_unused(\@names);


    if ($target ne 'tex') {
	$result .= &display_survey_html(
	    $used_names, $part, $showanswer, $lastresponse, $direction
	);
    } else {	

	my $vertical_env = &latex_vertical_environment();
	$result .= &latex_survey(
	    $used_names, $showanswer, $lastresponse, $direction, $vertical_env
	);

    }
    return $result;
}

sub whichfoils {
    my ($max,$randomize)=@_;

    my @truelist;
    my @falselist;
    my @whichfalse =();
    my ($truecnt,$falsecnt) = &getfoilcounts();
    my $count=0;
    # we will add in 1 of the true statements
    if ( $max>0 && ($falsecnt+1)>$max) { $count=$max } else { $count=$falsecnt+1; $max=$count; }
    my $answer=int(&Math::Random::random_uniform() * ($count));
    &Apache::lonxml::debug("Count is $count, $answer is $answer");
    my @names;
    if ( $Apache::response::foilgroup{'names'} ) {
	@names= @{ $Apache::response::foilgroup{'names'} };
    }
    if (&Apache::response::showallfoils()) {
	@whichfalse=@names;
    } elsif ($randomize eq 'no') {
	&Apache::lonxml::debug("No randomization");
	my $havetrue=0;
	foreach my $name (@names) {
	    if ($Apache::response::foilgroup{$name.'.value'} eq 'true') {
		if (!$havetrue ) {
		    push(@whichfalse,$name); $havetrue++; $answer=$#whichfalse;
		}
	    } elsif ($Apache::response::foilgroup{$name.'.value'} eq 'false') {
		push (@whichfalse,$name);
	    } elsif ($Apache::response::foilgroup{$name.'.value'} eq 'unused') {
	    } else {
		&Apache::lonxml::error(&HTML::Entities::encode("No valid value assigned ($Apache::response::foilgroup{$name.'.value'}) for foil $name in <foilgroup>",'<>&"'));
	    }
	}
	if ((!$havetrue) && 
            ($Apache::lonhomework::type ne 'survey') && 
            ($Apache::lonhomework::type ne 'surveycred') &&
            ($Apache::lonhomework::type ne 'anonsurvey') &&
            ($Apache::lonhomework::type ne 'anonsurveycred')) {
	    &Apache::lonxml::error(&mt('There are no true statements available.').'<br />');
	}
    } else {
	my $current=0;
	&Apache::lonhomework::showhash(%Apache::response::foilgroup);
	my (%top,%bottom);
	#first find out where everyone wants to be
	foreach my $name (@names) {
	    $current++;
	    if ($Apache::response::foilgroup{$name.'.value'} eq 'true') {
		push (@truelist,$name);
		if ($Apache::response::foilgroup{$name.'.location'} eq 'top') {
		    $top{$name}=$current;
		} elsif ($Apache::response::foilgroup{$name.'.location'} eq 'bottom') {
		    $bottom{$name}=$current;
		}
	    } elsif ($Apache::response::foilgroup{$name.'.value'} eq 'false') {
		push (@falselist,$name);
		if ($Apache::response::foilgroup{$name.'.location'} eq 'top') {
		    $top{$name}=$current;
		} elsif ($Apache::response::foilgroup{$name.'.location'} eq 'bottom') {
		    $bottom{$name}=$current;
		}
	    } elsif ($Apache::response::foilgroup{$name.'.value'} eq 'unused') {
	    } else {
		&Apache::lonxml::error(&HTML::Entities::encode("No valid value assigned ($Apache::response::foilgroup{$name.'.value'}) for foil $name in <foilgroup>",'<>&"'));
	    }
	}
	#pick a true statement
	my $notrue=0;
	if (scalar(@truelist) == 0) { $notrue=1; }
	my $whichtrue = int(&Math::Random::random_uniform() * ($#truelist+1));
	&Apache::lonxml::debug("Max is $max, From $#truelist elms, picking $whichtrue");
	my (@toplist, @bottomlist);
	my $topcount=0;
	my $bottomcount=0;
	# assign everyone to either toplist/bottomlist or whichfalse
	# which false is randomized, toplist bottomlist are in order
	while ((($#whichfalse+$topcount+$bottomcount) < $max-2) && ($#falselist > -1)) {
	    &Apache::lonxml::debug("Have $#whichfalse max is $max");
	    my $afalse=int(&Math::Random::random_uniform() * ($#falselist+1));
	    &Apache::lonxml::debug("From $#falselist elms, picking $afalse");
	    $afalse=splice(@falselist,$afalse,1);
	    &Apache::lonxml::debug("Picked $afalse");
	    &Apache::lonhomework::showhash(('names'=>\@names));
	    &Apache::lonhomework::showhash(%top);
	    if ($top{$afalse}) {
		$toplist[$top{$afalse}]=$afalse;
		$topcount++;
	    } elsif ($bottom{$afalse}) {
		$bottomlist[$bottom{$afalse}]=$afalse;
		$bottomcount++;
	    } else {
		push (@whichfalse,$afalse);
	    }
	}
	&Apache::lonxml::debug("Answer wants $answer");
	my $truename=$truelist[$whichtrue];
	my $dosplice=1;
	if (($notrue) && 
            ($Apache::lonhomework::type ne 'survey') &&
            ($Apache::lonhomework::type ne 'surveycred') &&
            ($Apache::lonhomework::type ne 'anonsurvey') &&
            ($Apache::lonhomework::type ne 'anonsurveycred')) {
	    $dosplice=0;
	    &Apache::lonxml::error(&mt('There are no true statements available.').'<br />');
	}
	#insert the true statement, keeping track of where it wants to be
	if ($Apache::response::foilgroup{$truename.'.location'} eq 'top' && $dosplice) {
	    $toplist[$top{$truename}]=$truename;
	    $answer=-1;
	    foreach my $top (reverse(@toplist)) {
		if ($top) { $answer++;}
		if ($top eq $truename) { last; }
	    }
	    $dosplice=0;
	} elsif ($Apache::response::foilgroup{$truename.'.location'} eq 'bottom' && $dosplice) {
	    $bottomlist[$bottom{$truename}]=$truename;
	    $answer=-1;
	    foreach my $bot (@bottomlist) {
		if ($bot) { $answer++;}
		if ($bot eq $truename) { last; }
	    }
	    $answer+=$topcount+$#whichfalse+1;
	    $dosplice=0;
	} else {
	    if ($topcount>0 || $bottomcount>0) {
                my $inc = 1;
                if (($bottomcount > 0) && ($Apache::lonhomework::type ne 'exam')) {
                    $inc = 2;
                }
                $answer=int(&Math::Random::random_uniform() * ($#whichfalse+$inc))
                        + $topcount;
	    }
	}
	&Apache::lonxml::debug("Answer now wants $answer");
	#add the top items to the top, bottom items to the bottom
	for (my $i=0;$i<=$#toplist;$i++) {
	    if ($toplist[$i]) { unshift(@whichfalse,$toplist[$i]) }
	}
	for (my $i=0;$i<=$#bottomlist;$i++) {
	    if ($bottomlist[$i]) { push(@whichfalse,$bottomlist[$i]) }
	}
	#if the true statement is randomized insert it into the list
	if ($dosplice) { splice(@whichfalse,$answer,0,$truelist[$whichtrue]); }
    }
    &Apache::lonxml::debug("Answer is $answer");
    return ($answer,@whichfalse);
}

## 
#  Return a list  of foil texts given foil names.
#  
# @param $whichfoils - Reference to a list of foil names.
#
# @return array
# @retval foil texts
#
sub get_foil_texts {
    my ($whichfoils) = @_;
    my @foil_texts;

    foreach my $name (@{$whichfoils}) {
	push(@foil_texts, $Apache::response::foilgroup{$name . '.text'});
    }
    return @foil_texts;
}

##
# Generate the HTML for a single html foil.
# @param $part           - The part for which the response is being generated.
# @param $fieldname      - The basename of the radiobutton field
# @param $name           - The foilname.
# @param $last_responses - Reference to a hash that holds the most recent
#                          responses.
# @param $value          - radiobutton value.
# 
# @return text
# @retval The generated html.
#
sub html_radiobutton {
    my ($part, $fieldname, $name, $last_responses, $value) = @_;

    my $result='<label>';
    
    $result .= '<input type="radio"
                onchange="javascript:setSubmittedPart(' . "'$part');\""
		. 'name="HWVAL_' . $fieldname . '"'
		. "value='$value'";

    if (defined($last_responses->{$name})) {
	$result .= '  checked="checked" ';
    }
    $result .= ' />';
    $result .= $Apache::response::foilgroup{$name . '.text'};
    $result .= '</label>';

    return $result;

}
##
# Return a reference to the last response hash. This hash has exactly
# one or zero entries.  The one entry is keyed by the foil 'name' of
# the prior response
#
# @param $part - Number of the problem part.
# 
# @return reference to a hash.
# @retval see above.
#
sub get_last_response {
    my ($part) = @_;

    my $id = $Apache::inputtags::response['-1'];
    my ( $lastresponse, $newvariation );
    
    if ((( $Apache::lonhomework::history{"resource.$part.type"} eq  'randomizetry')
	 || ( $Apache::lonhomework::type eq 'randomizetry' )
	)
	&& ( $Apache::inputtags::status[-1] eq 'CAN_ANSWER' )
	)
    {
	
	if ( $env{ 'form.' . $part . '.rndseed' } ne
	     $Apache::lonhomework::history{"resource.$part.rndseed"} )
	{
	    $newvariation = 1;
	}
    }
    unless ($newvariation) {
        if ((($env{'form.grade_username'} eq '') && ($env{'form.grade_domain'} eq '')) ||
            (($env{'form.grade_username'} eq $env{'user.name'}) &&
             ($env{'form.grade_domain'} eq $env{'user.domain'}))) {
            $lastresponse =
                $Apache::lonhomework::history{"resource.$part.$id.submission"};
        } else {
            unless (($Apache::lonhomework::history{"resource.$part.type"} eq 'anonsurvey') ||
                    ($Apache::lonhomework::history{"resource.$part.type"} eq 'anonsurveycred')) {
                $lastresponse =
                    $Apache::lonhomework::history{"resource.$part.$id.submission"};

            }
        }
    }

    my %lastresponse = &Apache::lonnet::str2hash($lastresponse);

    return \%lastresponse;
}

##
# Display foils in html rendition.:
#
# @param $whichfoils - Set of foils to display.
# @param $target     - Rendition target...there are several html targets.
# @param $direction  - 'horizontal' if layout is horizontal.
# @param $part       - Part of the problem that's being displayed.
# @param $show_answer- True if answers should be shown.
#
# @return string
# @retval generated html.
#
sub display_foils_html {
    my ($whichfoils, $target, $direction, $part, $show_answer) = @_;
    my $result;


    # if the answers get shown, we need to label each item as correct or
    # incorrect.

    my ($opening_html, $finalclose, $item_pretext, $item_posttext) = 
	&html_direction_fragments($direction);

    $result .= $opening_html;


    if ($show_answer) {

	foreach my $name (@{$whichfoils}) {

	    # If the item gets further surrounded by tags, this 
	    # holds the closures for those tages.

	    my $item_closetag = '';

	    $result .= $item_pretext;

	    # Label each foil as correct or incorrect:

	    if ($Apache::response::foilgroup{$name . '.value'} eq 'true') {
		$result .= &mt('Correct:') . '<b>';
		$item_closetag .= '</b>';
		
	    } else {
		$result .= &mt('Incorrect');
	    }

	    # Web rendition encloses the 
	    # item text in a label tag as well:

	    if ($target eq 'web') {
		$result .= '<label>';
		$item_closetag = '</label>' . $item_closetag;
	    }
	    $result .= $Apache::response::foilgroup{$name . '.text'};
	    $result .= $item_closetag;
	    $result .= $item_posttext;
	    $result .= "\n";	# make the html a bit more readable.
	}


    } else {
	my $lastresponse = &get_last_response($part);
	
	my $item_no = 0;
	foreach my $name (@{$whichfoils}) {
	    $result .= $item_pretext;
	    $result .= &html_radiobutton(
		$part, $Apache::inputtags::response[-1],
		$name, $lastresponse, $item_no
		);
	    $result .= $item_posttext;
	    $item_no++;
	}
	
    }
    $result .= $finalclose;

    return $result;
}
##
#  Display foils in exam mode for latex
#
# @param $whichfoils       - Reference to an array that contains the foil names to display
# @param $bubbles_per_line - Number of bubbles on a line.
# @param $direction        - Rendering direction 'horizontal' is what we're looking for.
# @param $venv             - Name of LaTeX environment to use for vertical rendering.
#
# @return string
# @return the latex rendering of the exam problem.
#
#
sub display_latex_exam {
    my ($whichfoils, $bubbles_per_line, $direction, $venv) = @_;
    my $result;
    my $numlines;
    my $bubble_number = 0;
    my $line          = 0;
    my $i             = 0;

    
    if ($direction eq  'horizontal') {

	# Marshall the display text for each foil and turn things over to
	# Apache::response::make_horizontal_bubbles:

	my @foil_texts = &get_foil_texts($whichfoils);
	$result .= &Apache::caparesponse::make_horizontal_latex_bubbles(
	    $whichfoils, \@foil_texts, '$\bigcirc$');

    } else {
	
	$result .= '\vskip 2mm \noindent';

	# This section puts out the prefix that tells the user
	# (if necessary) to only choose one bubble in the next n lines
	# for problems with more than one line worth of bubbles in the grid sheet:
	
	my $numitems = scalar( @{$whichfoils} );
	$numlines = int( $numitems / $bubbles_per_line );
	if ( ( $numitems % $bubbles_per_line ) != 0 ) {
	    $numlines++;
	}
	if ( $numlines < 1 ) {
	    $numlines = 1;
	}
	if ( $numlines > 1 ) {
	    my $linetext;
	    for ( my $i = 0 ; $i < $numlines ; $i++ ) {
		$linetext .= $Apache::lonxml::counter + $i . ', ';
	    }
	    $linetext =~ s/,\s$//;
	    $result .=
		'\small {\textbf{'
		. $linetext . '}} '
		. ' {\footnotesize '
		. &mt( '(Bubble once in [_1] lines)', $numlines )
		. '} \hspace*{\fill} \\\\';
	}
	else {
	    $result .= '\textbf{' . $Apache::lonxml::counter . '}.';
	}
	
	# Now output the bubbles themselves:
	
	foreach my $name (@{$whichfoils}) {
	    if ( $bubble_number >= $bubbles_per_line ) {
		$line++;
		$i             = 0;
		$bubble_number = 0;
	    }
	    my $identifier;
	    if ( $numlines > 1 ) {
		$identifier = $Apache::lonxml::counter + $line;
	    }
	    my $preindent;
	    if ($bubble_number > 0) {
		$preindent = '\hspace*{3 mm}';
	    }
	    my $foiltext = $Apache::response::foilgroup{$name . '.text'};
	    $foiltext =~ s/\\noindent//; # forgive me for I have sinned..
	    $result .= '{\small \textbf{'
		. $identifier  .$preindent
		. $alphabet[$i]
		. '}}$\bigcirc$'
		. $foiltext
	    . '\\\\';    #' stupid emacs -- it thinks it needs that apostrophe to close the quote
	    
	    $i++;
	    $bubble_number++;
	}

    }	

    return $result;
    
}

##
#  Display latex when exam mode is not on.
#
#  @param $whichfoils - The foils to display
#  @param $direction  - Display direction ('horizontal' is what matters to us).
#  @param $venv       - Vertical env. to use for vertical rendering.
#  @param  $vend      - End the vertical environment being used.
#
#  @return string
#  @retval - The LaTeX rendering of the resource.'
#
sub display_latex {
    my ($whichfoils, $direction, $venv) = @_;
    my $result;

    # how we render depends on the direction.
    # Vertical is some kind of list environment determined by vbegin/vend.
    # Horizontal is a table that is generated by 
    # Apache::caparesponse::make_horizontal_latex_bubbles with an empty string
    # for the actual bubble text.

    if ($direction eq 'horizontal') {
	my @foil_texts = &get_foil_texts($whichfoils);
	$result .= &Apache::caparesponse::make_horizontal_latex_bubbles(
	    $whichfoils, \@foil_texts, '');
    } else {
	$result .= "\\begin{$venv}";
	foreach my $name (@{$whichfoils}) {
	    $result .=  '\vspace*{-2 mm}\item '
		. $Apache::response::foilgroup{ $name . '.text' };
	}
	
	$result .= "\\end{$venv}";
    }
    return $result;
}


##
#  Render foils for a PDF form. This is a variant of tex rednering that provides
#  sufficient markup that the final PDF is a form that can be filled in online,
#  or offline.
#
# @param $whichfoils - References an array of foils to display in the order in which
#                     they should be displayed.
# @param $direction  - Rendering direction.  'horiztonal' means inputs are laid out
#                      horizontally otherwise they are stacked vertically.
# @param $venv       - Vertical environment in which to wrap the foils.
#
# @return string
# @retval String containing the rendering of the resource.
#
# TODO: Take into account direction!!!
#
sub display_pdf_form {
    my ($whichfoils, $direction, $venv) = @_;
    my $temp = 0;
    my $result;

    $result .= "\\begin{$venv}";
    foreach my $name ( @{$whichfoils} ) {
	
	my $fieldname =
	    $env{'request.symb'} 
	. '&part_'
	    . $Apache::inputtags::part
	    . '&radiobuttonresponse'
	    . '&HWVAL_'
	    . $Apache::inputtags::response['-1'];
	$result .= '\item[{'
	    . &Apache::lonxml::print_pdf_radiobutton( $fieldname,
						      $temp )
	    . '}]'
	    . $Apache::response::foilgroup{ $name . '.text' }
	. "\n";
	
	$temp++;
    }
    $result .= "\\end{$venv}";

    return $result;
}


##
# Display selected foils:  This is really just a dispatchter to appropriate renderers
#  
# @param $target   - Target (e.g. 'tex'...).
# @param $answer   - True if answers should be shown.
# @param $whichfoils - Array of foil selectors that indicate which foils shouild be
#                      rendered, in rendering order.
# @param $direction- Rendering direction ('horizontal' is the one we look for,
#                    otherwise foils are rendered one per line vertically.
# @param $bubbles_per_line - number of exam bubbles per line.
#
# @return string
# @retval The rendered problem.

sub displayfoils {
    my ($target,$answer,$whichfoils,$direction, $bubbles_per_line)=@_;
    my $result;


    my $part   = $Apache::inputtags::part;
    my $solved = $Apache::lonhomework::history{"resource.$part.solved"};

    # Show answers html.

    if ( ( $target ne 'tex' )
        && &Apache::response::show_answer() )
    {

	$result = &display_foils_html(
	    $whichfoils, $target, $direction, $part,  1);
	
	# other html
    }  elsif ($target ne 'tex') {
	    $result = &display_foils_html($whichfoils, $target, $direction, $part,
					  0, 0);

       # LaTeX rendering:
    } else {


        my $id            = $Apache::inputtags::response['-1'];
        my $part          = $Apache::inputtags::part;
	my $numlines;
	
	# Decide how to bracket the list of foils:

	my $vertical_env = &latex_vertical_environment();

	# Rendering for latex exams.
	
	if ( ( $Apache::lonhomework::type eq 'exam' ) )
	{
	    $result .= &display_latex_exam(
		$whichfoils, $bubbles_per_line, $direction, $vertical_env);

	    $result .= '\vskip 0mm ';
	    
	} else {	    

	    # Different rendering for PDF form than for a
	    # 'regular' answer direction is honored in both of those
	    #

	    if ( ($env{'form.pdfFormFields'} eq 'yes')
		    && ($Apache::inputtags::status[-1] eq 'CAN_ANSWER'))
	    {
		$result .= &display_pdf_form($whichfoils, $direction, $vertical_env);
	    } else {
		$result .= &display_latex($whichfoils,  $direction, $vertical_env );
	    }
	    $result .= '\vskip 0 mm '; 
       
	}
    }
    return $result;
}

sub displayallanswers {
    my @names;
    if ( $Apache::response::foilgroup{'names'} ) {
	@names= @{ $Apache::response::foilgroup{'names'} };
    }
    my $result=&Apache::response::answer_header('radiobuttonresponse');
    foreach my $name (@names) {
	$result.=&Apache::response::answer_part('radiobuttonresponse',
				$Apache::response::foilgroup{$name.'.value'});
    }
    $result.=&Apache::response::answer_footer('radiobuttonresponse');
    return $result;
}

sub displayanswers {
    my ($answer, $whichopt, $bubbles_per_line)=@_;
    my $result;

    if ($Apache::lonhomework::type eq 'exam') {
	my $line = int($answer/$bubbles_per_line);
	my $correct = ('A'..'Z')[$answer%$bubbles_per_line];
	$result .= &Apache::response::answer_header('radiobuttonresponse',
						    $line);
	$result .= &Apache::response::answer_part('radiobuttonresponse',
						  $correct);
    } else {
	$result .= &Apache::response::answer_header('radiobuttonresponse');
    }
    foreach my $name (@{ $whichopt }) {
	$result.=&Apache::response::answer_part('radiobuttonresponse',
						$Apache::response::foilgroup{$name.'.value'});
    }
    $result.=&Apache::response::answer_footer('radiobuttonresponse');
    return $result;
}

sub start_conceptgroup {
    my ($target,$token,$tagstack,$parstack,$parser,$safeeval,$style)=@_;
    $Apache::radiobuttonresponse::conceptgroup=1;
    %Apache::response::conceptgroup=();
    my $result;
    if ($target eq 'edit') {
	$result.=&Apache::edit::tag_start($target,$token);
	$result.=&Apache::edit::text_arg('Concept:','concept',$token,'50').
	    &Apache::edit::end_row().&Apache::edit::start_spanning_row();
    } elsif ($target eq 'modified') {
	my $constructtag=&Apache::edit::get_new_args($token,$parstack,
						     $safeeval,'concept');
	if ($constructtag) { $result = &Apache::edit::rebuild_tag($token); }
    }
    return $result;
}

sub end_conceptgroup {
    my ($target,$token,$tagstack,$parstack,$parser,$safeeval,$style)=@_;
    $Apache::radiobuttonresponse::conceptgroup=0;
    my $result;
    if ($target eq 'web' || $target eq 'grade' || $target eq 'answer'  ||
	$target eq 'tex' || $target eq 'analyze') {
	&Apache::response::pick_foil_for_concept($target,
						 ['value','text','location'],
						 \%Apache::hint::radiobutton,
						 $parstack,$safeeval);
    } elsif ($target eq 'edit') {
	$result=&Apache::edit::end_table();
    }
    return $result;
}

sub insert_conceptgroup {
    my $result="\n\t\t<conceptgroup concept=\"\">".&insert_foil()."\n\t\t</conceptgroup>\n";
    return $result;
}

sub start_foil {
    my ($target,$token,$tagstack,$parstack,$parser,$safeeval,$style)=@_;
    my $result='';
    if ($target eq 'web' || $target eq 'tex' || $target eq 'analyze') {
	&Apache::lonxml::startredirection;
	if ($target eq 'analyze') {
	    &Apache::response::check_if_computed($token,$parstack,$safeeval,'value');
	}
    } elsif ($target eq 'edit') {
	$result=&Apache::edit::tag_start($target,$token);
	$result.=&Apache::edit::text_arg('Name:','name',$token);
	$result.=&Apache::edit::select_or_text_arg('Correct Option:','value',
						   ['unused','true','false'],
						   $token);
	my $randomize=&Apache::lonxml::get_param('randomize',$parstack,
						 $safeeval,'-3');
	if ($randomize ne 'no') {
	    $result.=&Apache::edit::select_arg('Location:','location',
					       ['random','top','bottom'],$token);
	}
	$result.=&Apache::edit::end_row().&Apache::edit::start_spanning_row();
    } elsif ($target eq 'modified') {
	my $constructtag=&Apache::edit::get_new_args($token,$parstack,
						     $safeeval,'value','name',
						     'location');
	if ($constructtag) { $result = &Apache::edit::rebuild_tag($token); }
    } 
    return $result;
}

sub end_foil {
    my ($target,$token,$tagstack,$parstack,$parser,$safeeval,$style)=@_;
    my $text='';
    if ($target eq 'web' || $target eq 'tex' || $target eq 'analyze') {
	$text=&Apache::lonxml::endredirection;
    }
    if ($target eq 'web' || $target eq 'grade' || $target eq 'answer'
	|| $target eq 'tex' || $target eq 'analyze') {
	my $value = &Apache::lonxml::get_param('value',$parstack,$safeeval);
	if ($value ne 'unused') {
	    my $name = &Apache::lonxml::get_param('name',$parstack,$safeeval);
	    if ($name eq "") {
		&Apache::lonxml::warning(&mt('Foils without names exist. This can cause problems to malfunction.'));
		$name=$Apache::lonxml::curdepth;
	    }
	    if (defined($Apache::response::foilnames{$name})) {
		&Apache::lonxml::error(&mt('Foil name [_1] appears more than once. Foil names need to be unique.','<b><tt>'.$name.'</tt></b>'));
	    }
	    $Apache::response::foilnames{$name}++;
	    my $location =&Apache::lonxml::get_param('location',$parstack,
						     $safeeval);
	    if ( $Apache::radiobuttonresponse::conceptgroup
		 && !&Apache::response::showallfoils() ) {
		push @{ $Apache::response::conceptgroup{'names'} }, $name;
		$Apache::response::conceptgroup{"$name.value"} = $value;
		$Apache::response::conceptgroup{"$name.text"} = $text;	
		$Apache::response::conceptgroup{"$name.location"} = $location;
	    } else {
		push @{ $Apache::response::foilgroup{'names'} }, $name;
		$Apache::response::foilgroup{"$name.value"} = $value;
		$Apache::response::foilgroup{"$name.text"} = $text;
		$Apache::response::foilgroup{"$name.location"} = $location;
	    }
	}
    }
    return '';
}

sub insert_foil {
    return '
<foil name="" value="unused">
<startouttext />
<endouttext />
</foil>';
}

1;
__END__



=head1 NAME

Apache::radiobuttonresponse

=head1 SYNOPSIS

Handles multiple-choice style responses.

This is part of the LearningOnline Network with CAPA project
described at http://www.lon-capa.org.

=head1 SUBROUTINES

=over

=item start_radiobuttonresponse()

=item bubble_line_count()

=item end_radiobuttonresponse()

=item start_foilgroup()

=item storesurvey()

=item grade_response()

=item end_foilgroup()

=item getfoilcounts()

=item format_prior_answer()

=item displayallfoils()

=item &whichfoils($max,$randomize)

Randomizes the list of foils.
Respects
  - each foils desire to be randomized
  - the existance of Concept groups of foils (select 1 foil from each)
  - and selects a single correct statement from all possilble true statments
  - and limits it to a toal of $max foils

WARNING: this routine uses the random number generator, it should only
be called once per target, otherwise it can cause randomness changes in
homework problems.

Arguments
  $max - maximum number of foils to select (including the true one)
         (so a max of 5 is: 1 true, 4 false)

  $randomize - whether to randomize the listing of foils, by default
               will randomize, only if randomize is 'no' will it not

Returns
  $answer - location in the array of the correct answer
  @foils  - array of foil names in to display order

=item displayfoils()

=item displayallanswers()

=item displayanswers()

=item start_conceptgroup()

=item end_conceptgroup()

=item insert_conceptgroup()

=item start_foil()

=item end_foil()

=item insert_foil()

=back

=cut
 

FreeBSD-CVSweb <freebsd-cvsweb@FreeBSD.org>