diff --git a/.gitignore b/.gitignore index 84c76f9601..831eaae24c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,8 @@ -*.conf -*.apache2-config -*.log -tmp/** -DATA/** -logs/** - +.svn +.DS_Store +.* +htdocs/tmp/* +tmp/* +logs/* +conf/*.conf +conf/*.apache2-config diff --git a/clients/input.txt b/clients/input.txt index 592526419b..4638ddf0ff 100644 --- a/clients/input.txt +++ b/clients/input.txt @@ -11,7 +11,7 @@ loadMacros( "PGauxiliaryFunctions.pl" ); -TEXT(&beginproblem); +TEXT(beginproblem()); $showPartialCorrectAnswers = 1; $a = random(-10,-1,1); $b = random(1,11,1); diff --git a/clients/renderProblem.pl b/clients/renderProblem.pl index 09d7f7ff47..9878ee82e4 100755 --- a/clients/renderProblem.pl +++ b/clients/renderProblem.pl @@ -44,6 +44,7 @@ =head1 NAME use lib '/opt/webwork/webwork2/lib'; #use Crypt::SSLeay; # needed for https use WebworkClient; +use MIME::Base64 qw( encode_base64 decode_base64); ############################################# @@ -51,22 +52,22 @@ =head1 NAME ############################################# ############################################################ -# configure the local output file and display command !!!!!!!! + # configure the local output file and display command !!!!!!!! ############################################################ - + # Path to a temporary file for storing the output of renderProblem.pl use constant TEMPOUTPUTFILE => '/Users/gage/Desktop/renderProblemOutput.html'; # Command line for displaying the temporary file in a browser. - use constant DISPLAY_COMMAND => 'open -a firefox '; #browser opens tempoutputfile above - # use constant DISPLAY_COMMAND => "open -a 'Google Chrome' "; + # use constant DISPLAY_COMMAND => 'open -a firefox '; #browser opens tempoutputfile above + use constant DISPLAY_COMMAND => "open -a 'Google Chrome' "; ############################################################ my $use_site; - $use_site = 'test_webwork'; # select a rendering site + #$use_site = 'test_webwork'; # select a rendering site #$use_site = 'local'; # select a rendering site - #$use_site = 'rochester_test'; # select a rendering site + $use_site = 'rochester_test'; # select a rendering site ############################################################ @@ -108,17 +109,17 @@ =head1 NAME # result. A different name can be used but the course must exist on the server. -our ( $XML_URL,$FORM_ACTION_URL, $XML_PASSWORD, $XML_COURSE); +our ( $XML_URL,$FORM_ACTION_URL, $XML_PASSWORD, $XML_COURSE, %credentials); if ($use_site eq 'local') { -# the rest can work!! + # the rest can work!! $XML_URL = 'http://localhost:80'; $FORM_ACTION_URL = 'http://localhost:80/webwork2/html2xml'; $XML_PASSWORD = 'xmlwebwork'; $XML_COURSE = 'daemon_course'; } elsif ($use_site eq 'rochester_test') { - $XML_URL = 'http://128.151.231.2'; - $FORM_ACTION_URL = 'http://128.151.231.2/webwork2/html2xml'; + $XML_URL = 'https://hosted2.webwork.rochester.edu'; + $FORM_ACTION_URL = 'https://hosted2.webwork.rochester.edu/webwork2/html2xml'; $XML_PASSWORD = 'xmlwebwork'; $XML_COURSE = 'daemon_course'; @@ -135,6 +136,50 @@ =head1 NAME # END configuration section for client ################################################## +our $UNIT_TESTS_ON = 0; + +#################################################### +# get credentials +#################################################### + + +my $credential_path; +my @path_list = ('.ww_credentials', '/Users/gage/.ww_credentials', '/Users/gage/ww_session_credentials'); +foreach my $path (@path_list) { + if (-r "$path" ) { + $credential_path = $path; + last; + } +} +unless ( $credential_path ) { + die < "my login name for the webwork course", + password => "my password ", + courseID => "the name of the webwork course", +); +1; +--------------------------------------------------------- +EOF +} + +eval{require $credential_path}; +if ($@ or not defined %credentials) { + +print STDERR < "my login name for the webwork course", + password => "my password ", + courseID => "the name of the webwork course", +); +1; +EOF +die; +} use constant DISPLAYMODE => 'images'; # jsMath is another possibilities. @@ -148,9 +193,6 @@ =head1 NAME ################################################## - -our $xmlrpc_client = new WebworkClient; - ################################################## # input/output section ################################################## @@ -160,29 +202,49 @@ =head1 NAME our $rh_result; # filter mode main code -undef $/; -$source = <>; #slurp input -$/ =1; -$xmlrpc_client->encodeSource($source); -$xmlrpc_client->url($XML_URL); -$xmlrpc_client->{form_action_url}= $FORM_ACTION_URL; -$xmlrpc_client->{displayMode} = DISPLAYMODE(); -$xmlrpc_client->{user} = 'xmluser'; -$xmlrpc_client->{password} = $XML_PASSWORD; -$xmlrpc_client->{course} = $XML_COURSE; +{ + local($/); + $source = <>; #slurp standard input + #print $source; # return input to BBedit +} +############################################ +# Build client +############################################ +our $xmlrpc_client = new WebworkClient ( + url => $XML_URL, + form_action_url => $FORM_ACTION_URL, + displayMode => DISPLAYMODE(), + site_password => $credentials{site_password}, + courseID => $credentials{courseID}, + userID => $credentials{userID}, + session_key => $credentials{session_key}, +); + + $xmlrpc_client->encodeSource($source); + + my $input = { + userID => $credentials{userID}||'', + session_key => $credentials{session_key}||'', + courseID => $credentials{courseID}||'', + courseName => $credentials{courseID}||'', + password => $credentials{password}||'', + site_password => $credentials{site_password}||'', + }; #xmlrpcCall('renderProblem'); our $output; -if ( $xmlrpc_client->xmlrpcCall('renderProblem') ) { - +our $result; +if ( $result = $xmlrpc_client->xmlrpcCall('renderProblem', $input) ) { + print "\n\n Result of renderProblem \n\n" if $UNIT_TESTS_ON; $output = $xmlrpc_client->formatRenderedProblem; + ###HACK fixme + print pretty_print_rh($result) if $UNIT_TESTS_ON; + } else { + print "\n\n ERRORS in renderProblem \n\n"; $output = $xmlrpc_client->{output}; # error report } - - - local(*FH); open(FH, '>'.TEMPOUTPUTFILE) or die "Can't open file ".TEMPOUTPUTFILE()." for writing"; print FH $output; @@ -194,359 +256,46 @@ =head1 NAME # end input/output section ################################################## +sub pretty_print_rh { + shift if UNIVERSAL::isa($_[0] => __PACKAGE__); + my $rh = shift; + my $indent = shift || 0; + my $out = ""; + my $type = ref($rh); + + if (defined($type) and $type) { + $out .= " type = $type; "; + } elsif (! defined($rh )) { + $out .= " type = UNDEFINED; "; + } + return $out." " unless defined($rh); + + if ( ref($rh) =~/HASH/ or "$rh" =~/HASH/ ) { + $out .= "{\n"; + $indent++; + foreach my $key (sort keys %{$rh}) { + $out .= " "x$indent."$key => " . pretty_print_rh( $rh->{$key}, $indent ) . "\n"; + } + $indent--; + $out .= "\n"." "x$indent."}\n"; + + } elsif (ref($rh) =~ /ARRAY/ or "$rh" =~/ARRAY/) { + $out .= " ( "; + foreach my $elem ( @{$rh} ) { + $out .= pretty_print_rh($elem, $indent); + + } + $out .= " ) \n"; + } elsif ( ref($rh) =~ /SCALAR/ ) { + $out .= "scalar reference ". ${$rh}; + } elsif ( ref($rh) =~/Base64/ ) { + $out .= "base64 reference " .$$rh; + } else { + $out .= $rh; + } + + return $out." "; +} -################################################## -# XMLRPC client -- -# the code below is identical between renderProblem.pl and renderViaXMLRPC.pm???? -# and has been included in WebworkClient.pm -################################################## - -# package WeBWorK::ContentGenerator::renderViaXMLRPC_client; -# -# use Crypt::SSLeay; # needed for https -# use XMLRPC::Lite; -# use MIME::Base64 qw( encode_base64 decode_base64); -# -# use constant TRANSPORT_METHOD => 'XMLRPC::Lite'; -# use constant REQUEST_CLASS => 'WebworkXMLRPC'; # WebworkXMLRPC is used for soap also!! -# use constant REQUEST_URI => 'mod_xmlrpc'; -# -# sub new { -# my $self = { -# output => '', -# encodedSource => '', -# url => '', -# password => '', -# course => '', -# displayMode => '', -# inputs_ref => { AnSwEr0001 => '', -# AnSwEr0002 => '', -# AnSwEr0003 => '', -# }, -# }; -# -# bless $self; -# } -# -# -# our $result; -# -# ################################################## -# # Utilities -- -# # this code is identical between renderProblem.pl and renderViaXMLRPC.pm -# ################################################## -# -# sub xmlrpcCall { -# my $self = shift; -# my $command = shift; -# $command = 'listLibraries' unless $command; -# -# my $requestResult = TRANSPORT_METHOD -# -> proxy($self->{url}.'/'.REQUEST_URI); -# -# my $input = $self->setInputTable(); -# local( $result); -# # use eval to catch errors -# eval { $result = $requestResult->call(REQUEST_CLASS.'.'.$command,$input) }; -# if ($@) { -# print STDERR "There were a lot of errors for $command\n" ; -# print STDERR "Errors: \n $@\n End Errors\n" ; -# return 0 #failure -# } -# -# unless (ref($result) and $result->fault) { -# my $rh_result = $result->result(); -# #print STDERR pretty_print_rh($rh_result); -# $self->{output} = $rh_result; #$self->formatRenderedProblem($rh_result); -# return 1; # success -# -# } else { -# $self->{output} = 'Error from server: '. join( ",\n ", -# $result->faultcode, -# $result->faultstring); -# return 0; #failure -# } -# } -# -# sub encodeSource { -# my $self = shift; -# my $source = shift; -# $self->{encodedSource} =encode_base64($source); -# } -# sub url { -# my $self = shift; -# my $new_url = shift; -# $self->{url} = $new_url if defined($new_url) and $new_url =~ /\S/; -# $self->{url}; -# } -# sub pretty_print { # provides html output -- NOT a method -# my $r_input = shift; -# my $level = shift; -# $level = 4 unless defined($level); -# $level--; -# return '' unless $level > 0; # only print three levels of hashes (safety feature) -# my $out = ''; -# if ( not ref($r_input) ) { -# $out = $r_input if defined $r_input; # not a reference -# $out =~ s/"; -# -# -# foreach my $key ( sort ( keys %$r_input )) { -# $out .= " $key=> ".pretty_print($r_input->{$key}) . ""; -# } -# $out .=""; -# } elsif (ref($r_input) eq 'ARRAY' ) { -# my @array = @$r_input; -# $out .= "( " ; -# while (@array) { -# $out .= pretty_print(shift @array, $level) . " , "; -# } -# $out .= " )"; -# } elsif (ref($r_input) eq 'CODE') { -# $out = "$r_input"; -# } else { -# $out = $r_input; -# $out =~ s/ $self->{password}, -# set => 'set0', -# library_name => 'Library', -# command => 'all', -# }; -# -# $out; -# } -# sub setInputTable { -# my $self = shift; -# my $out = { -# pw => $self->{password}, -# library_name => 'Library', -# command => 'renderProblem', -# answer_form_submitted => 1, -# course => $self->{course}, -# extra_packages_to_load => [qw( AlgParserWithImplicitExpand Expr -# ExprWithImplicitExpand AnswerEvaluator -# AnswerEvaluatorMaker -# )], -# mode => $self->{displayMode}, -# modules_to_evaluate => [ qw( -# Exporter -# DynaLoader -# GD -# WWPlot -# Fun -# Circle -# Label -# PGrandom -# Units -# Hermite -# List -# Match -# Multiple -# Select -# AlgParser -# AnswerHash -# Fraction -# VectorField -# Complex1 -# Complex -# MatrixReal1 Matrix -# Distributions -# Regression -# -# )], -# envir => $self->environment(), -# problem_state => { -# -# num_of_correct_ans => 0, -# num_of_incorrect_ans => 4, -# recorded_score => 1.0, -# }, -# source => $self->{encodedSource}, #base64 encoded -# -# -# -# }; -# -# $out; -# } -# -# sub environment { -# my $self = shift; -# my $envir = { -# answerDate => '4014438528', -# CAPA_Graphics_URL=>'http://webwork-db.math.rochester.edu/capa_graphics/', -# CAPA_GraphicsDirectory =>'/ww/webwork/CAPA/CAPA_Graphics/', -# CAPA_MCTools=>'/ww/webwork/CAPA/CAPA_MCTools/', -# CAPA_Tools=>'/ww/webwork/CAPA/CAPA_Tools/', -# cgiDirectory=>'Not defined', -# cgiURL => 'Not defined', -# classDirectory=> 'Not defined', -# courseName=>'Not defined', -# courseScriptsDirectory=>'not defined', -# displayMode=>$self->{displayMode}, -# dueDate=> '4014438528', -# effectivePermissionLevel => 10, -# externalGif2EpsPath=>'not defined', -# externalPng2EpsPath=>'not defined', -# externalTTHPath=>'/usr/local/bin/tth', -# fileName=>'set0/prob1a.pg', -# formattedAnswerDate=>'6/19/00', -# formattedDueDate=>'6/19/00', -# formattedOpenDate=>'6/19/00', -# functAbsTolDefault=> 0.0000001, -# functLLimitDefault=>0, -# functMaxConstantOfIntegration=> 1000000000000.0, -# functNumOfPoints=> 5, -# functRelPercentTolDefault=> 0.000001, -# functULimitDefault=>1, -# functVarDefault=> 'x', -# functZeroLevelDefault=> 0.000001, -# functZeroLevelTolDefault=>0.000001, -# htmlDirectory =>'not defined', -# htmlURL =>'not defined', -# inputs_ref => $self->{inputs_ref}, -# macroDirectory=>'not defined', -# numAbsTolDefault=>0.0000001, -# numFormatDefault=>'%0.13g', -# numOfAttempts=> 0, -# numRelPercentTolDefault => 0.0001, -# numZeroLevelDefault =>0.000001, -# numZeroLevelTolDefault =>0.000001, -# openDate=> '3014438528', -# permissionLevel =>10, -# PRINT_FILE_NAMES_FOR => [ 'gage'], -# probFileName => 'set0/prob1a.pg', -# problemSeed => 1234, -# problemValue =>1, -# probNum => 13, -# psvn => 54321, -# psvn=> 54321, -# questionNumber => 1, -# scriptDirectory => 'Not defined', -# sectionName => 'Gage', -# sectionNumber => 1, -# sessionKey=> 'Not defined', -# setNumber =>'not defined', -# studentLogin =>'gage', -# studentName => 'Mike Gage', -# tempDirectory => 'not defined', -# templateDirectory=>'not defined', -# tempURL=>'not defined', -# webworkDocsURL => 'not defined', -# -# showHints => 1, # extra options -- usually passed from the input form -# showSolutions => 1, -# -# }; -# $envir; -# }; -# -# sub formatAnswerRow { -# my $self = shift; -# my $rh_answer = shift; -# my $problemNumber = shift; -# my $answerString = $rh_answer->{original_student_ans}||' '; -# my $correctAnswer = $rh_answer->{correct_ans}||''; -# my $ans_message = $rh_answer->{ans_message}||''; -# my $score = ($rh_answer->{score}) ? 'Correct' : 'Incorrect'; -# my $row = qq{ -# -# -# Prob: $problemNumber -# -# -# $answerString -# -# -# $score -# -# -# Correct answer is $correctAnswer -# -# -# $ans_message -# -# \n -# }; -# $row; -# } -# -# sub formatRenderedProblem { -# my $self = shift; -# my $rh_result = $self->{output}; # wrap problem in formats -# my $problemText = decode_base64($rh_result->{text}); -# my $rh_answers = $rh_result->{answers}; -# my $encodedSource = $self->{encodedSource}||'foobar'; -# my $warnings = ''; -# if ( defined ($rh_result->{WARNINGS}) and $rh_result->{WARNINGS} ){ -# $warnings = "
-#

WARNINGS

".decode_base64($rh_result->{WARNINGS})."

"; -# } -# #warn "keys: ", join(" | ", sort keys %{$rh_result }); -# my $debug_messages = $rh_result->{flags}->{DEBUG_messages} || []; -# $debug_messages = join("
\n", @{ $debug_messages } ); -# my $internal_debug_messages = $rh_result->{internal_debug_messages} || []; -# $internal_debug_messages = join("
\n", @{ $internal_debug_messages } ); -# # collect answers -# my $answerTemplate = q{
ANSWERS }; -# my $problemNumber = 1; -# foreach my $key (sort keys %{$rh_answers}) { -# $answerTemplate .= $self->formatAnswerRow($rh_answers->{$key}, $problemNumber++); -# } -# $answerTemplate .= q{

}; -# -# my $FULL_URL = $self->url; -# my $FORM_ACTION_URL = "$FULL_URL/webwork2/html2xml"; -# my $problemTemplate = < -# -# -# WeBWorK Editor using host $HOSTNAME -# -# -# $answerTemplate -#
-# $problemText -# -# -# -# -# -#

-#
-#
-#

Warning section

-# $warnings -#

-# Debug message section -#

-# $debug_messages -#

-# internal errors -#

-# $internal_debug_messages -# -# -# -# -# ENDPROBLEMTEMPLATE -# -# -# -# $problemTemplate; -# } -# 1; diff --git a/clients/webwork_xmlrpc_client.pl b/clients/webwork_xmlrpc_client.pl index dea4d2b0a5..aee6c73902 100755 --- a/clients/webwork_xmlrpc_client.pl +++ b/clients/webwork_xmlrpc_client.pl @@ -15,6 +15,8 @@ =cut use feature ":5.10"; +use lib "/opt/webwork/webwork2/lib"; +use WebworkClient; use XMLRPC::Lite; use MIME::Base64 qw( encode_base64 decode_base64); @@ -22,80 +24,166 @@ use constant PROTOCOL => 'http'; # or 'http'; use constant HOSTURL => 'localhost'; use constant HOSTPORT => '80'; # or 80 -use constant TRANSPORT_METHOD => 'XMLRPC::Lite'; -use constant REQUEST_CLASS =>'WebworkXMLRPC'; # WebworkXMLRPC is used for soap also!! -use constant REQUEST_URI =>'mod_xmlrpc'; -use constant TEMPOUTPUTFILE => '/Users/gage/Desktop/renderProblemOutput.html'; -use constant COURSENAME => 'gage_course'; +use constant TRANSPORT_METHOD => 'XMLRPC::Lite'; +use constant REQUEST_CLASS => 'WebworkXMLRPC'; # WebworkXMLRPC is used for soap also!! +use constant REQUEST_URI => 'mod_xmlrpc'; +use constant TEMPOUTPUTFILE => '/Users/gage/Desktop/renderProblemOutput.html'; + +our $XML_URL = 'http://localhost:80'; +our $FORM_ACTION_URL = 'http://localhost:80/webwork2/html2xml'; +our $XML_PASSWORD = 'xmlwebwork'; +our $XML_COURSE = 'gage_course'; + +our $UNIT_TESTS_ON = 0; + +#################################################### +# get credentials +#################################################### + +my $credential_path; +my @path_list = ('.ww_credentials', '/Users/gage/.ww_credentials', '/Users/gage/ww_session_credentials'); +foreach my $path (@path_list) { + if (-r "$path" ) { + $credential_path = $path; + last; + } +} +unless ( $credential_path ) { + die < "my login name for the webwork course", + password => "my password ", + courseID => "the name of the webwork course", +); +1; +--------------------------------------------------------- +EOF +} -my $credential_path = ".ww_credentials"; eval{require $credential_path}; -if ($@ ) { +if ($@ or not defined %credentials) { + print STDERR < "my login name for the webwork course", password => "my password ", courseID => "the name of the webwork course", -) +); +1; --------------------------------------------------------- EOF die; } -# print "credentials: ", join(" | ", %credentials), "\n"; +#print "credentials: ", join(" | ", %credentials), "\n"; my @COMMANDS = qw( listLibraries renderProblem listLib readFile tex2pdf ); -# $pg{displayModes} = [ -# "plainText", # display raw TeX for math expressions -# "formattedText", # format math expressions using TtH -# "images", # display math expressions as images generated by dvipng -# "jsMath", # render TeX math expressions on the client side using jsMath -# "asciimath", # render TeX math expressions on the client side using ASCIIMathML -# ]; use constant DISPLAYMODE => 'images'; # end configuration section -our $courseName = $credentials{courseID}; +our $courseID = $credentials{courseID}; print STDERR "inputs are ", join (" | ", @ARGV), "\n"; our $source; +############################################ +# Build client +############################################ +our $xmlrpc_client = new WebworkClient ( + url => $XML_URL, + form_action_url => $FORM_ACTION_URL, + displayMode => DISPLAYMODE(), + + site_password => $credentials{site_password}, + courseID => $credentials{courseID}, + userID => $credentials{userID}, + session_key => $credentials{session_key}, +); + +# prepare additional input values + + + if (@ARGV) { my $command = $ARGV[0]; my $result; print "executing WebworkXMLRPC.$command \n\n-----------------------\n\n"; given($command) { - when ('renderProblem') { if ( defined $ARGV[1]) { - if (-r $ARGV[1] ) { - $source = `cat $ARGV[1]`; - xmlrpcCall($command); - } else { - print STDERR "Can't read source file $ARGV[1]\n"; - } - } else { - print STDERR "Useage: ./webwork_xmlrpc_client.pl command \n"; - } + when ('renderProblem') { + if ( defined $ARGV[1]) { + if (-r $ARGV[1] ) { + $source = `cat $ARGV[1]`; + $xmlrpc_client->encodeSource($source); + my $input = { + userID => $credentials{userID}||'', + session_key => $credentials{session_key}||'', + courseID => $credentials{courseID}||'', + courseName => $credentials{courseID}||'', + password => $credentials{password}||'', + site_password => $credentials{site_password}||'', + }; + #print STDERR "input is ", %$input,"\n"; + $result = $xmlrpc_client->xmlrpcCall($command, $input); + print "\n\n Result of renderProblem \n\n"; + print pretty_print_rh($result); + } else { + print STDERR "Can't read source file $ARGV[1]\n"; + } + } else { + print STDERR "Useage: ./webwork_xmlrpc_client.pl command \n"; + } + } when ('listLibraries') { + my $input = { + userID => $credentials{userID}||'', + session_key => $credentials{session_key}||'', + courseID => $credentials{courseID}||'', + password => $credentials{password}||'', + site_password => $credentials{site_password}||'', + }; + # print STDERR "ww_xmlrpc_client: input for listLibraries command is ", %$input,"\n"; + eval { + $result = $xmlrpc_client->xmlrpcCall($command, $input); + }; + if (defined($result) ) { + my @lib_array = @ { $result->{ra_out} }; + print STDOUT "ww_xmlrpc_client: The libraries available in course $courseID are:\n\t ", join("\n\t ", @lib_array ), "\n"; + } else { + print STDOUT "ww_xmlrpc_client: No libraries available for course $courseID\n"; + } + } when ('listLib') { + $result = listLib( @ARGV ); + my $command = $ARGV[1]; + print "listLib returned\n"; + print pretty_print_rh($result); + print "\n"; + + } when ('listSets') { + $input = { site_password => 'xmluser', + password => $credentials{password}, + userID => $credentials{userID}, + courseID => $credentials{courseID}, + }; + my $result = $xmlrpc_client->xmlrpcCall($command, $input); + print pretty_print_rh($result); + } when ('readFile') { + print STDERR "Command $command not yet implemented\n" + } when ('tex2pdf') { + print STDERR "Command $command not yet implemented\n" + } default { + print STDERR "Command '$command' not recognized. Commands ",@COMMANDS; } - when ('listLibraries') {$result = xmlrpcCall($command); - if (defined($result) ) { - print STDOUT "The libraries available in course $courseName are:\n\t ", join("\n\t ", @$result), "\n"; - } else { - print STDOUT "No libraries available for course $courseName\n"; - } - } - when ('listLib') {listLib( @ARGV );} - when ('readFile') {print STDERR "Command $command not yet implemented\n"} - when ('tex2pdf') {print STDERR "Command $command not yet implemented\n"} } -} else { + } else { print STDERR "Useage: ./webwork_xmlrpc_client.pl command [file_name]\n"; print STDERR "For example: ./webwork_xmlrpc_client.pl renderProblem uri('http://'.HOSTURL.':'.HOSTPORT.'/'.REQUEST_CLASS) - -> proxy(PROTOCOL.'://'.HOSTURL.':'.HOSTPORT.'/'.REQUEST_URI); - - # my $test = [3,4,5,6]; - - # print "displayMode=",$input->{envir}->{displayMode},"\n"; - local( $result); - # use eval to catch errors - eval { $result = $requestResult->call(REQUEST_CLASS.'.'.$command,$input) }; - print STDERR "There were a lot of errors\n" if $@; - print "Errors: \n $@\n End Errors\n" if $@; - - print "result is <|", ref($result),"|>\n"; - - unless (ref($result) and $result->fault) { - - if (ref($result->result())=~/HASH/ and defined($result->result()->{text}) ) { - $result->result()->{text} = decode_base64($result->result()->{text}); - } - print pretty_print_rh($result->result()),"\n"; #$result->result() - return $result->result(); - } else { - print 'oops ', join ', ', - $result->faultcode, - $result->faultstring; - return undef; - } -} - + sub source { return "" unless $source; return encode_base64($source); @@ -155,48 +205,59 @@ sub source { sub listLib { my @ARGS = @_; #print "args for listLib are ", join(" ", @ARGS), "\n"; + my $result; given($ARGS[1]) { - when ("all") { $input = { pw => 'xmluser', + when ("all") { + $input = { site_password => 'xmluser', password => $credentials{password}, - user => $credentials{userID}, + userID => $credentials{userID}, courseID => $credentials{courseID}, command => 'all', }; - xmlrpcCall("listLib", $input); - } - when ('dirOnly') { $input = { pw => 'xmluser', + $result = $xmlrpc_client->xmlrpcCall("listLib", $input); + } + when ('dirOnly') { + my %options = @ARGS[2..$#ARGS]; + my $path = $options{-path} || ''; + my $maxdepth = defined($options{-depth}) ? $options{-depth}: 10000; + $input = { site_password => 'xmluser', password => $credentials{password}, - user => $credentials{userID}, + userID => $credentials{userID}, courseID => $credentials{courseID}, command => 'dirOnly', + dirPath => $path, + maxdepth => $maxdepth, }; - xmlrpcCall("listLib", $input); - } - when('files') { if ($ARGS[2] ) { - my $dirPath = $ARGS[2]; - $input = { pw => 'xmluser', - password => $credentials{password}, - user => $credentials{userID}, - courseID => $credentials{courseID}, - command => 'files', - dirPath => $dirPath, - }; - xmlrpcCall("listLib", $input); - } else { - print STDERR "Usage: webwork_xmlrpc_client listLib files \n" - } - - } - - - - - default {print "The possible arguments for listLib are:". + $result = $xmlrpc_client->xmlrpcCall("listLib", $input); + } + when('files') { + if ($ARGS[2] ) { + my %options = @ARGS[2..$#ARGS]; + my $path = $options{-path} || ''; + $input = { site_password => 'xmluser', + password => $credentials{password}, + userID => $credentials{userID}, + courseID => $credentials{courseID}, + command => 'files', + dirPath => $path, + }; + $result = $xmlrpc_client->xmlrpcCall("listLib", $input); + } else { + print STDERR "Usage: webwork_xmlrpc_client listLib files \n"; + $result = ""; + } + } + default {print "The possible arguments for listLib are:". "\n\t all -- print all paths". - "\n\t dirOnly -- print only directories". - "\n\t files -- print .pg files in the given directory \n" + "\n\t dirOnly [options]-- print only directories below Library/path". + "\n\t\t options: -depth depth -path directoryPath". + "\n\t\t\t depth counts the number of slashes in the relative path". + "\n\t files -- print .pg files in the given directory \n". + "\n\t\t options: -path directoryPath"; + $result = ""; } } + return $result; } sub pretty_print_rh { shift if UNIVERSAL::isa($_[0] => __PACKAGE__); @@ -239,10 +300,53 @@ sub pretty_print_rh { return $out." "; } +sub pretty_print_json { + shift if UNIVERSAL::isa($_[0] => __PACKAGE__); + my $rh = shift; + my $indent = shift || 0; + my $out = ""; + my $type = ref($rh); + + if (defined($type) and $type) { + $out .= " type = $type; "; + } elsif (! defined($rh )) { + $out .= " type = UNDEFINED; "; + } + return $out." " unless defined($rh); + + if ( ref($rh) =~/HASH/ or "$rh" =~/HASH/ ) { + $out .= "{\n"; + $indent++; + foreach my $key (sort keys %{$rh}) { + $out .= " "x$indent."$key => " . pretty_print_json( $rh->{$key}, $indent ) . "\n"; + } + $indent--; + $out .= "\n"." "x$indent."}\n"; + + } elsif (ref($rh) =~ /ARRAY/ or "$rh" =~/ARRAY/) { + $out .= " ( "; + foreach my $elem ( @{$rh} ) { + $out .= pretty_print_json($elem, $indent); + + } + $out .= " ) \n"; + } elsif ( ref($rh) =~ /SCALAR/ ) { + $out .= "scalar reference ". ${$rh}; + } elsif ( ref($rh) =~/Base64/ ) { + $out .= "base64 reference " .$$rh; + } else { + my $jsonString = $rh; + $jsonString =~ s/(\\|\/)/\./g; + $out .= "Library.".$jsonString.";"; + } + + return $out." "; +} + sub standard_input { $out = { - pw => 'xmluser', + site_password => 'xmluser', password => $credentials{password}, userID => $credentials{userID}, set => 'set0', diff --git a/conf/global.conf.dist b/conf/global.conf.dist index 49727da403..f9af763b4b 100644 --- a/conf/global.conf.dist +++ b/conf/global.conf.dist @@ -935,6 +935,7 @@ $pg{directories}{macrosPath} = [ "$courseDirs{templates}/Library/macros/TCNJ", "$courseDirs{templates}/Library/macros/NAU", "$courseDirs{templates}/Library/macros/Dartmouth", + "$courseDirs{templates}/Library/macros/WHFreeman", ]; # The applet search path. If a full URL is given, it is used unmodified. If an diff --git a/conf/templates/dgage/dgage.template b/conf/templates/dgage/dgage.template new file mode 100755 index 0000000000..39460df231 --- /dev/null +++ b/conf/templates/dgage/dgage.template @@ -0,0 +1,128 @@ + + + + + + + + +/css/dgage.css"/> +<!--#path style="text" text=" : " textonly="1"--> + + + + + +
+
+ +
+ +
+ + + + + + + + + + +
+ + + + + + + + + + + +
+

+ Warning -- there may be something wrong with this question. Please inform your instructor + including the warning messages below. +

+ +
+ + + +
+ + + + +
+
+ +
+ + + +
+ +
+ + + +
+ + +
+
+ + diff --git a/conf/templates/dgage/lbtwo.template b/conf/templates/dgage/lbtwo.template new file mode 100755 index 0000000000..cefb511505 --- /dev/null +++ b/conf/templates/dgage/lbtwo.template @@ -0,0 +1,132 @@ + + + + + + + + +/css/dgage.css"/> +<!--#path style="text" text=" : " textonly="1"--> + + + + + +
+
+ +
+ +
+ + + + + + + + + + +
+ + + + + + + + + + + +
+

+ Warning -- there may be something wrong with this question. Please inform your instructor + including the warning messages below. +

+ +
+ + + +
+ + + + +
+
+ +
+ + + +
+ +
+ + + +
+ +

Page generated at

+ +
+ +
+
+ + diff --git a/conf/templates/math/lbtwo.template b/conf/templates/math/lbtwo.template new file mode 100755 index 0000000000..cefb511505 --- /dev/null +++ b/conf/templates/math/lbtwo.template @@ -0,0 +1,132 @@ + + + + + + + + +/css/dgage.css"/> +<!--#path style="text" text=" : " textonly="1"--> + + + + + +
+
+ +
+ +
+ + + + + + + + + + +
+ + + + + + + + + + + +
+

+ Warning -- there may be something wrong with this question. Please inform your instructor + including the warning messages below. +

+ +
+ + + +
+ + + + +
+
+ +
+ + + +
+ +
+ + + +
+ +

Page generated at

+ +
+ +
+
+ + diff --git a/conf/templates/math2/lbtwo.template b/conf/templates/math2/lbtwo.template new file mode 100755 index 0000000000..cefb511505 --- /dev/null +++ b/conf/templates/math2/lbtwo.template @@ -0,0 +1,132 @@ + + + + + + + + +/css/dgage.css"/> +<!--#path style="text" text=" : " textonly="1"--> + + + + + +
+
+ +
+ +
+ + + + + + + + + + +
+ + + + + + + + + + + +
+

+ Warning -- there may be something wrong with this question. Please inform your instructor + including the warning messages below. +

+ +
+ + + +
+ + + + +
+
+ +
+ + + +
+ +
+ + + +
+ +

Page generated at

+ +
+ +
+
+ + diff --git a/conf/templates/math2/math2.css b/conf/templates/math2/math2.css index 255417681f..2c7c9445e4 100644 --- a/conf/templates/math2/math2.css +++ b/conf/templates/math2/math2.css @@ -25,7 +25,7 @@ body { } h1, h2, h3, h4, h5, h6 { - font-family: "Trebuchet MS", "Arial", "Helvetica", sans-serif; + font-family: "Trebuchet MS", "Arial", "Helvetica", serif; /* FIXME "You have no background-color with your color" */ color: #225; background: transparent; @@ -381,7 +381,7 @@ clear: both; #copyright { font-size: 75%; margin: 0;} #last-modified { clear: both; - font-family: "Trebuchet MS", "Arial", "Helvetica", sans-serif; + font-family: "Trebuchet MS", "Arial", "Helvetica", serif; font-size: 75%; background-color: inherit; color: #444; @@ -403,8 +403,7 @@ clear: both; #InfoPanel { font-size:100%; float: right; - width: 100%; - max-width:400px; + max-width: 400px; overflow: auto; margin-right: -1px; background-color: #fff; @@ -520,7 +519,7 @@ font.hidden { font-style: italic; font-weight: normal; color: #aaaaaa; backgroun .Enrolled { font-weight: normal; color: black; background-color: inherit; } .Drop { font-style: italic; color: gray; background-color: inherit; } -/*Styles for the login form*/ +/*Styles for the login form -- mainly small alignment changes for all of the input elements on the login page*/ #login_form input { margin-top: 7px; @@ -540,6 +539,7 @@ font.hidden { font-style: italic; font-weight: normal; color: #aaaaaa; backgroun /*Styles for the edit forms, found on the homework sets editor page*/ +/*Following are mostly alignment fixes for the input elements and their lables on this page.*/ .edit_form input,select{ margin-top: 5px; } @@ -558,6 +558,7 @@ font.hidden { font-style: italic; font-weight: normal; color: #aaaaaa; backgroun /*Styles for the classlist editor page*/ +/*The styles here correctly align the table elements in the set table*/ .set_table{ margin-right: 5px; } @@ -578,16 +579,24 @@ font.hidden { font-style: italic; font-weight: normal; color: #aaaaaa; backgroun /*General styles, for elements such as table captions input buttons, columns, etc.*/ +/*General styles for table captions*/ table caption{ font-weight: bold; text-decoration: underline; } +/*Styling the content that is found in the login page.*/ +.p_content{ + width: 640px; +} + +/*General styles for all input buttons*/ .button_input{ margin-right: 5px; margin-left: 5px; } +/*General styles for all column divs, for pages which use column layouts*/ .column { float: left; width: 50%; @@ -596,14 +605,18 @@ table caption{ margin-top: 10px; } +/*General styles for the wrapper div element which goes around the InfoBoxes*/ .info-wrapper{ float: right; min-height: 100px; - width: 40%; + width: 39%; overflow: hidden; margin-left: 5px; + position: absolute; + left: 810px; } +/*The following styles create the links which look like buttons, which replaced the image elements which were previously used for the navigation buttons*/ a.nav_button{ color: black; background-color: #DDDDDD; @@ -636,11 +649,12 @@ span.gray_button{ opacity: 0.3; } +/*an ID for elements that should have no style, if it is necessary to specifically specify such an element.*/ #none{ /*Please do not do anything with elements with id "none"*/ } -/*Styles for the PGProblemEditor Page*/ +/*Styles for the PGProblemEditor Page #editor{ background-color: #FFFFFF; diff --git a/conf/templates/math2/system.template b/conf/templates/math2/system.template index 8e0250716d..29f521cd0f 100644 --- a/conf/templates/math2/system.template +++ b/conf/templates/math2/system.template @@ -29,20 +29,22 @@ /images/favicon.ico"/> /css/math2.css"/> - - + + /css/tabber.css"/> - - + + <!--#path style="text" text=" : " textonly="1"--> - + + + +
-
").css({position:"absolute",visibility:"visible",left:-f*(h/d),top:-e*(i/c)}).parent().addClass("ui-effects-explode").css({position:"absolute",overflow:"hidden",width:h/d,height:i/c,left:g.left+f*(h/d)+(a.options.mode=="show"?(f-Math.floor(d/2))*(h/d):0),top:g.top+e*(i/c)+(a.options.mode=="show"?(e-Math.floor(c/2))*(i/c):0),opacity:a.options.mode=="show"?0:1}).animate({left:g.left+f*(h/d)+(a.options.mode=="show"?0:(f-Math.floor(d/2))*(h/d)),top:g.top+ +e*(i/c)+(a.options.mode=="show"?0:(e-Math.floor(c/2))*(i/c)),opacity:a.options.mode=="show"?1:0},a.duration||500);setTimeout(function(){a.options.mode=="show"?b.css({visibility:"visible"}):b.css({visibility:"visible"}).hide();a.callback&&a.callback.apply(b[0]);b.dequeue();j("div.ui-effects-explode").remove()},a.duration||500)})}})(jQuery); +;/* + * jQuery UI Effects Fade 1.8.16 + * + * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about) + * Dual licensed under the MIT or GPL Version 2 licenses. + * http://jquery.org/license + * + * http://docs.jquery.com/UI/Effects/Fade + * + * Depends: + * jquery.effects.core.js + */ +(function(b){b.effects.fade=function(a){return this.queue(function(){var c=b(this),d=b.effects.setMode(c,a.options.mode||"hide");c.animate({opacity:d},{queue:false,duration:a.duration,easing:a.options.easing,complete:function(){a.callback&&a.callback.apply(this,arguments);c.dequeue()}})})}})(jQuery); +;/* + * jQuery UI Effects Fold 1.8.16 + * + * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about) + * Dual licensed under the MIT or GPL Version 2 licenses. + * http://jquery.org/license + * + * http://docs.jquery.com/UI/Effects/Fold + * + * Depends: + * jquery.effects.core.js + */ +(function(c){c.effects.fold=function(a){return this.queue(function(){var b=c(this),j=["position","top","bottom","left","right"],d=c.effects.setMode(b,a.options.mode||"hide"),g=a.options.size||15,h=!!a.options.horizFirst,k=a.duration?a.duration/2:c.fx.speeds._default/2;c.effects.save(b,j);b.show();var e=c.effects.createWrapper(b).css({overflow:"hidden"}),f=d=="show"!=h,l=f?["width","height"]:["height","width"];f=f?[e.width(),e.height()]:[e.height(),e.width()];var i=/([0-9]+)%/.exec(g);if(i)g=parseInt(i[1], +10)/100*f[d=="hide"?0:1];if(d=="show")e.css(h?{height:0,width:g}:{height:g,width:0});h={};i={};h[l[0]]=d=="show"?f[0]:g;i[l[1]]=d=="show"?f[1]:0;e.animate(h,k,a.options.easing).animate(i,k,a.options.easing,function(){d=="hide"&&b.hide();c.effects.restore(b,j);c.effects.removeWrapper(b);a.callback&&a.callback.apply(b[0],arguments);b.dequeue()})})}})(jQuery); +;/* + * jQuery UI Effects Highlight 1.8.16 + * + * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about) + * Dual licensed under the MIT or GPL Version 2 licenses. + * http://jquery.org/license + * + * http://docs.jquery.com/UI/Effects/Highlight + * + * Depends: + * jquery.effects.core.js + */ +(function(b){b.effects.highlight=function(c){return this.queue(function(){var a=b(this),e=["backgroundImage","backgroundColor","opacity"],d=b.effects.setMode(a,c.options.mode||"show"),f={backgroundColor:a.css("backgroundColor")};if(d=="hide")f.opacity=0;b.effects.save(a,e);a.show().css({backgroundImage:"none",backgroundColor:c.options.color||"#ffff99"}).animate(f,{queue:false,duration:c.duration,easing:c.options.easing,complete:function(){d=="hide"&&a.hide();b.effects.restore(a,e);d=="show"&&!b.support.opacity&& +this.style.removeAttribute("filter");c.callback&&c.callback.apply(this,arguments);a.dequeue()}})})}})(jQuery); +;/* + * jQuery UI Effects Pulsate 1.8.16 + * + * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about) + * Dual licensed under the MIT or GPL Version 2 licenses. + * http://jquery.org/license + * + * http://docs.jquery.com/UI/Effects/Pulsate + * + * Depends: + * jquery.effects.core.js + */ +(function(d){d.effects.pulsate=function(a){return this.queue(function(){var b=d(this),c=d.effects.setMode(b,a.options.mode||"show");times=(a.options.times||5)*2-1;duration=a.duration?a.duration/2:d.fx.speeds._default/2;isVisible=b.is(":visible");animateTo=0;if(!isVisible){b.css("opacity",0).show();animateTo=1}if(c=="hide"&&isVisible||c=="show"&&!isVisible)times--;for(c=0;c
').appendTo(document.body).addClass(a.options.className).css({top:d.top,left:d.left,height:b.innerHeight(),width:b.innerWidth(),position:"absolute"}).animate(c,a.duration,a.options.easing,function(){f.remove();a.callback&&a.callback.apply(b[0],arguments); +b.dequeue()})})}})(jQuery); +; \ No newline at end of file diff --git a/htdocs/js/library_browser.js b/htdocs/js/library_browser.js new file mode 100644 index 0000000000..b2ee37383d --- /dev/null +++ b/htdocs/js/library_browser.js @@ -0,0 +1,1171 @@ +/** + * file variables + */ +var USER = "user-needs-to-be-defined-in-hidden-variable-id=hidden_user"; +var COURSE = "course-needs-to-be-defined-in-hidden-variable-id=hidden_courseID"; +var SESSIONKEY = "session-key-needs-to-be-defined-in-hidden-variable-id=hidden_key" +var PASSWORD = "who-cares-what-the-password-is"; +// request object, I'm naming them assuming there may be different versions +var listLibRequest = { + "xml_command" : "listLib", + "pw" : "", + "password" : PASSWORD, + "session_key" : SESSIONKEY, + "user" : "user-needs-to-be-defined", + "library_name" : "Library", + "courseID" : COURSE, + "set" : "set0", + "new_set_name" : "new set", + "command" : "buildtree", +} + +var undoing = false; +var undo_stack = new Array(); +var redo_stack = new Array(); + +var cardCatalog; +var setList; +var webserviceURL = "../../../instructorXMLHandler"; +var tabs; + +var problemPlaceholder = false; + +/** + * Utilities + */ +var uniqueCounter = 0; +function generateUniqueID() { + uniqueCounter++; + return uniqueCounter.toString(16); +} + +Object.size = function(obj) { + var size = 0, key; + for (key in obj) { + if (obj.hasOwnProperty(key)) + size++; + } + return size; +}; + +/** + * Global stuff + */ +// undo and redo functions +function undo() { + // pop the stack and call the function, that's it + var undoFunc = undo_stack.pop(); + undoing = true; + undoFunc(); +} + +function redo() { + var redoFunc = redo_stack.pop(); + redoFunc(); +} + +function highlightSets(event) { + //console.log(this.getAttribute("data-path")); + var problemID = this.getAttribute("data-path"); + $(".contains_problem").removeClass("contains_problem"); + for ( var key in setList.sets) { + if (setList.sets[key].problems.hasOwnProperty(problemID)) { + //console.log("found one " + setList.sets[key].name + setList.sets[key].id); + $( + document.getElementById(setList.sets[key].name + + setList.sets[key].id)).addClass( + "contains_problem"); + } + } +} +function unHighlightSets(event) { + $(".contains_problem").removeClass("contains_problem"); +} + +function updateMessage(message) { + var messageBox = document.getElementById("messages"); + messageBox.innerHTML = message; + $(messageBox).effect("pulsate", { + times : 3 + }, 500); +} + +function showErrorResponse(data){ + var myWindow = window.open('', '', 'width=500,height=800'); + myWindow.document.write(data); + myWindow.focus(); +} + +/** + * unobtrusively start up our javascript + */ +$(document).ready(function() { + // do stuff when DOM is ready + + // get usernames and keys from hidden variables: + var myUser = document.getElementById("hidden_user").value; + var mySessionKey = document.getElementById("hidden_key").value; + var myCourseID = document.getElementById("hidden_courseID").value; + // check to make sure that our credentials are available. + if (myUser && mySessionKey && myCourseID) { + listLibRequest.user = myUser; + listLibRequest.session_key = mySessionKey; + listLibRequest.courseID = myCourseID; + } else { + updateMessage("missing hidden credentials: user " + + myUser + " session_key " + mySessionKey + + " courseID" + myCourseID); + } + + // get our sets + setList = new SetList(); + + // attach function for deleting selected problems + document.getElementById("delete_problem").addEventListener("click", function(event) { + var currentTabId = $("#problems_container div.ui-tabs-panel:not(.ui-tabs-hide)")[0].id; + console.log(currentTabId); + // only care if it's a set + if (currentTabId != "library_tab") { + + var problems = $(".ww_selected"); + var set = setList.sets[document + .getElementById( + currentTabId) + .getAttribute("data-uid")]; + console.log(problems); + problems.each(function(index) { + set.removeProblem($(this).attr( + "data-path")); + }); + } + }, false); + + // attach undo and redo + document.getElementById("undo_button").addEventListener("click", undo, false); + document.getElementById("redo_button").addEventListener("click", redo, false); + + $("#dialog").dialog({ + autoOpen : false, + modal : true + }); + // create set button listner + document.getElementById("create_set").addEventListener("click", function() { + Set.createSet(setList, false); + $("#dialog").dialog('close'); + }); + + document.getElementById("new_problem_set").addEventListener("click", function() { + setList.createSet(); + }, false); + + // this stil doesn't work + $("#new_problem_set").droppable({ + tolerance : 'pointer', + drop : function(event, ui) { + problemPlaceholder = ui.draggable.attr("data-path") + $("#dialog").dialog('open'); + } + }); + + // some window set up: + $tabs = $("#problems_container") + .tabs( + { + closable : true, + add : function(event, ui) { + document + .getElementById( + "library_link") + .removeChild( + document + .getElementById("library_link").lastChild); + console.log("adding a tab"); + $tabs.tabs('select', '#' + + ui.panel.id); + $(".ww_selected").removeClass( + "ww_selected");// probably reduntant but I want to make sure nothing stays selected + }, + create : function(event, ui) { + document + .getElementById( + "library_link") + .removeChild( + document + .getElementById("library_link").lastChild); + $(".ww_selected").removeClass( + "ww_selected"); + }, + select : function(event, ui) { + $(".ww_selected").removeClass( + "ww_selected"); + }, + remove : function(event, ui) { + document + .getElementById( + "library_link") + .removeChild( + document + .getElementById("library_link").lastChild); + $(".ww_selected").removeClass( + "ww_selected"); + } + }); + + $("#problems_container").removeClass("ui-corner-all"); + + listLibRequest.xml_command = "listLibraries"; + + updateMessage("Loading libraries... may take some time"); + + $.post(webserviceURL, listLibRequest, + function(data) { + console.log(data); + try { + var response = $.parseJSON(data); + console.log("result: " + response.server_response); + updateMessage(response.server_response); + cardCatalog = new CardCatolog(response.result_data.split(",")); + } catch (err) { + console.log(err); + var myWindow = window.open('', '', + 'width=500,height=800'); + myWindow.document.write(data); + myWindow.focus(); + } + }); + + +}); + +function CardCatolog(libNames) { + this.searchBox = new Search(); + this.displayBox = document.getElementById("library_list"); + this.listBox = document.getElementById("library_list_box"); + + //this.library = new Library(libName); + //this.working_library = this.library; + var topLibraries = new Array(); + for(var i = 0; i < libNames.length; i++){ + topLibraries.push(new Library(libNames[i])); + } + + + // set up unobtrusive controlls: + + this.searchButton = document.getElementById("run_search"); + + this.nextButton = document.getElementById("nextList"); + this.prevButton = document.getElementById("prevList"); + this.probsPerPage = document.getElementById("prob_per_page"); + this.topProbIndex = 0; + + var workAroundTheClosure = this; + + this.buildLibraryBox(topLibraries); + //this.library.loadChildren(function(){workAroundTheClosure.buildSelectBox(workAroundTheClosure.library)}); + + this.searchButton.addEventListener('click', function() {workAroundTheClosure.searchBox.go();}, false); + + document.getElementById("load_problems").addEventListener('click', function(){ + if(workAroundTheClosure.working_library.problems > 0){ + console.log("loaded"); + console.log(workAroundTheClosure.working_library.problems); + workAroundTheClosure.renderProblems(workAroundTheClosure.topProbIndex, parseInt(workAroundTheClosure.probsPerPage.options[workAroundTheClosure.probsPerPage.selectedIndex].value)); + } + else { + workAroundTheClosure.working_library.loadProblems(function() { + console.log("callback!"); + workAroundTheClosure.renderProblems(workAroundTheClosure.topProbIndex, parseInt(workAroundTheClosure.probsPerPage.options[workAroundTheClosure.probsPerPage.selectedIndex].value)); + }); + } + }, false); + + this.nextButton.addEventListener('click', function() { + console.log("Next Button was clicked"); + // then load new problems? yes because we shouldn't + // even be able to click on it if we can't + workAroundTheClosure.topProbIndex += parseInt(workAroundTheClosure.probsPerPage.options[workAroundTheClosure.probsPerPage.selectedIndex].value); + workAroundTheClosure.renderProblems(workAroundTheClosure.topProbIndex, parseInt(workAroundTheClosure.probsPerPage.options[workAroundTheClosure.probsPerPage.selectedIndex].value)); + }, false); + document.getElementById("prevList").addEventListener('click', function() { + workAroundTheClosure.topProbIndex -= parseInt(workAroundTheClosure.probsPerPage.options[workAroundTheClosure.probsPerPage.selectedIndex].value); + if (workAroundTheClosure.topProbIndex < 0) + workAroundTheClosure.topProbIndex = 0; + workAroundTheClosure.renderProblems(workAroundTheClosure.topProbIndex, parseInt(workAroundTheClosure.probsPerPage.options[workAroundTheClosure.probsPerPage.selectedIndex].value)); + }, false); +} + +CardCatolog.prototype.updateMoveButtons = function() { + if (this.topProbIndex + parseInt(this.probsPerPage.options[this.probsPerPage.selectedIndex].value) < this.working_library.problems.length) { + this.nextButton.removeAttribute("disabled"); + } else { + this.nextButton.setAttribute("disabled", true); + } + if (this.topProbIndex > 0) { + this.prevButton.removeAttribute("disabled"); + } else { + this.prevButton.setAttribute("disabled", true); + } +} + +CardCatolog.prototype.buildLibraryBox = function(topLibraries){ + var newLibList = document.createElement("select"); + newLibList.id = "topLibList" + (this.displayBox.childNodes.length + 1); + //newLibList.setAttribute("data-propName", currentLibrary.path); + var workAroundTheClosure = this; + + for (var i = 0; i < topLibraries.length; i++) { + var option = document.createElement("option") + option.value = topLibraries[i].name; + option.innerHTML = topLibraries[i].name; + newLibList.add(option, null); + } + + newLibList.addEventListener("change", function(event) { + var changedLib = event.target;// should be the select we just created + var listBox = event.target.parentNode; + + var count = 1; + //get rid of all other select boxes + while (listBox.childNodes.length > count + 1) { + listBox.removeChild(listBox.lastChild); + } + //interesting, this should let topLibraries always exist without it being global + workAroundTheClosure.working_library = topLibraries[changedLib.selectedIndex]; + workAroundTheClosure.working_library.loadChildren(function(){workAroundTheClosure.buildSelectBox(workAroundTheClosure.working_library)}); + }, false); + + this.listBox.appendChild(newLibList); + this.working_library = topLibraries[0]; + this.working_library.loadChildren(function(){workAroundTheClosure.buildSelectBox(workAroundTheClosure.working_library)}); + +} + +CardCatolog.prototype.buildSelectBox = function(currentLibrary) { + var newLibList = document.createElement("select"); + newLibList.id = "libList" + (this.displayBox.childNodes.length + 1); + newLibList.setAttribute("data-propName", currentLibrary.path); + var workAroundTheClosure = this; + newLibList.addEventListener("change", function(event) { + workAroundTheClosure.onLibSelect(event, currentLibrary); + }, false); + + for ( var name in currentLibrary.children) { + if (!name.match(/\./)) { + var option = document.createElement("option") + option.value = name; + option.innerHTML = name; + newLibList.add(option, null); + } + } + if (newLibList.childNodes.length > 0) { + var emptyOption = document.createElement("option"); + newLibList.add(emptyOption, newLibList.firstChild); + this.listBox.appendChild(newLibList); + } +} + +// start:index to start at, limit:number of problems to list +CardCatolog.prototype.renderProblems = function(start, limit) { + $('a[href="#library_tab"] span').text("Library ("+start+" - "+ (start+limit) +" of " + this.working_library.problems.length + ") "); + while (this.displayBox.hasChildNodes()) { + this.displayBox.removeChild(this.displayBox.lastChild); + } + for(var i = start; i < start+limit && i < this.working_library.problems.length; i++){ + this.working_library.problems[i].render(this.displayBox); + } + this.updateMoveButtons(); +}; + + +CardCatolog.prototype.onLibSelect = function(event, currentLibrary) { + this.topProbIndex = 0; + //this.library.problems.list = new Object(); + //this.updateLibrary(); + var changedLib = event.target;// should be the select + var listBox = event.target.parentNode; + var libChoices = listBox.childNodes; + //var currentObject = this.treeRoot; + + var count = 0; + var key; + for ( var i = 0; i < libChoices.length; i++) { + if (libChoices[i].tagName == "SELECT") { + count = i; + if (libChoices[i] == changedLib) + break; + } + } + while (listBox.childNodes.length > count + 1) { + listBox.removeChild(listBox.lastChild); + } + + if (currentLibrary.children.hasOwnProperty(changedLib.options[changedLib.selectedIndex].value)) { + var child = currentLibrary.children[changedLib.options[changedLib.selectedIndex].value]; + this.working_library = child; + //right now this reloads if there are no subdirectories, can be fixed by a count on serverside. + if(Object.size(child.children) > 0){ + console.log("didn't have to build"); + this.buildSelectBox(child); + } else { + var workAroundTheClosure = this; + child.loadChildren(function() { + workAroundTheClosure.buildSelectBox(child); + }); + } + } else { + this.working_library = currentLibrary; + } +}; + +function Search(){ + this.problems = new Array(); + this.subjectBox = document.getElementById("subjectBox"); + this.chaptersBox = document.getElementById("chaptersBox"); + this.sectionsBox = document.getElementById("sectionsBox"); + this.textbooksBox = document.getElementById("textbooksBox"); + this.textChaptersBox = document.getElementById("textChaptersBox"); + this.textSectionsBox = document.getElementById("textSectionsBox"); + this.keywordsBox = document.getElementById("keywordsBox"); + + + + var workAroundTheClosure = this; + subjectBox.addEventListener("change", function() { + //update inputs + workAroundTheClosure.updateInputs(); + //update lists + workAroundTheClosure.updateChaptersBox(); + workAroundTheClosure.updateSectionsBox(); + }, false); + chaptersBox.addEventListener("change", function() { + //update inputs + workAroundTheClosure.updateInputs(); + //update lists + workAroundTheClosure.updateSectionsBox(); + }, false); + sectionsBox.addEventListener("change", function() { + //update inputs + workAroundTheClosure.updateInputs(); + }, false); + /*textbooksBox.addEventListener("change", function() { + //update inputs + workAroundTheClosure.updateInputs(); + //update lists + workAroundTheClosure.updateAll(); + }, false);*/ + this.updateSubjectBox(); + this.updateChaptersBox(); + this.updateSectionsBox(); + +} + +function SearchResult(){ + this.searchName = "search" + generateUniqueID(); + this.displayBox; + this.problems; +} + +SearchResult.prototype.createPageControls = function(){ + + this.nextButton = document.createElement("button"); + // + this.nextButton.id = this.searchName + "nextList"; + this.nextButton.type = "button"; + this.nextButton.innerHTML = "Next"; + this.nextButton.setAttribute("disabled", true); + + this.prevButton = document.createElement("button"); + // + this.prevButton.id = this.searchName + "prevList"; + this.prevButton.type = "button"; + this.prevButton.innerHTML = "Previous"; + this.prevButton.setAttribute("disabled", true); + + var thisContainer = document.getElementById(this.searchName); + thisContainer.appendChild(this.prevButton); + thisContainer.appendChild(this.nextButton); + + + //hard coded for now + this.probsPerPage = 10;//document.getElementById("prob_per_page"); + this.topProbIndex = 0; + + //attach event listeners: + var workAroundTheClosure = this; + this.nextButton.addEventListener('click', function() { + console.log("Next Button was clicked"); + // then load new problems? yes because we shouldn't + // even be able to click on it if we can't + workAroundTheClosure.topProbIndex += workAroundTheClosure.probsPerPage; + workAroundTheClosure.renderProblems(workAroundTheClosure.topProbIndex, workAroundTheClosure.probsPerPage); + }, false); + this.prevButton.addEventListener('click', function() { + workAroundTheClosure.topProbIndex -= workAroundTheClosure.probsPerPage; + if (workAroundTheClosure.topProbIndex < 0) + workAroundTheClosure.topProbIndex = 0; + workAroundTheClosure.renderProblems(workAroundTheClosure.topProbIndex, workAroundTheClosure.probsPerPage); + }, false); + + +} + + +Search.prototype.go = function() { + this.updateInputs(); + listLibRequest.xml_command = "searchLib"; + listLibRequest.subcommand = "getDBListings"; + var workAroundTheClosure = this; + $.post(webserviceURL, listLibRequest,function(data) { + console.log(data); + //try { + var response = $.parseJSON(data); + console.log("result: " + response.server_response); + updateMessage(response.server_response); + var results = response.result_data.split(","); + + var newSearchResult = new SearchResult(); + + $tabs.tabs("add", "#"+newSearchResult.searchName, "Search (" + results.length + ")"); + var thisContainer = document.getElementById(newSearchResult.searchName); + var displayList = document.createElement("ul"); + thisContainer.appendChild(displayList); + + + + newSearchResult.createPageControls(); + + newSearchResult.displayBox = displayList; + newSearchResult.problems = new Array(); + for(var i = 0; i < results.length; i++){ + newSearchResult.problems.push(new Problem(results[i])); + } + newSearchResult.renderProblems(newSearchResult.topProbIndex, newSearchResult.probsPerPage); + //callback(); + /*} catch (err) { + console.log(err); + var myWindow = window.open('', '', 'width=500,height=800'); + myWindow.document.write(data); + myWindow.focus(); + }*/ + }); +}; + +SearchResult.prototype.renderProblems = function(start, limit) { + //$('#'+this.searchName+' a').text("Other text"); + $('a[href="#'+this.searchName+'"] span').text("Search ("+start+" - "+ (start+limit) +" of " + this.problems.length + ") "); + console.log($('#'+this.searchName+' a')); + while (this.displayBox.hasChildNodes()) { + this.displayBox.removeChild(this.displayBox.lastChild); + } + for(var i = start; i < start+limit && i < this.problems.length; i++){ + this.problems[i].render(this.displayBox); + } + this.updateMoveButtons(); +}; + +SearchResult.prototype.updateMoveButtons = function() { + if ((this.topProbIndex + this.probsPerPage) < this.problems.length) { + this.nextButton.removeAttribute("disabled"); + } else { + this.nextButton.setAttribute("disabled", true); + } + if (this.topProbIndex > 0) { + this.prevButton.removeAttribute("disabled"); + } else { + this.prevButton.setAttribute("disabled", true); + } +}; + +Search.prototype.updateInputs = function(){ + listLibRequest.library_subjects = this.subjectBox.options[this.subjectBox.selectedIndex].value; + listLibRequest.library_chapters = this.chaptersBox.options[this.chaptersBox.selectedIndex].value; + listLibRequest.library_sections = this.sectionsBox.options[this.sectionsBox.selectedIndex].value; +// listLibRequest.library_textbook = this.textbooksBox.options[this.textbooksBox.selectedIndex].value; +// listLibRequest.library_textchapter = this.textChaptersBox.options[this.textChaptersBox.selectedIndex].value; +// listLibRequest.library_textsection = this.textSectionsBox.options[this.textSectionsBox.selectedIndex].value; +// listLibRequest.library_keywords = this.keywordsBox.value; +}; + + + +Search.prototype.updateSubjectBox = function(){ + listLibRequest.xml_command = "searchLib"; + listLibRequest.subcommand = "getAllDBsubjects"; + this.update(this.subjectBox, "All Subjects"); +}; + +Search.prototype.updateChaptersBox = function(){ + listLibRequest.xml_command = "searchLib"; + listLibRequest.subcommand = "getAllDBchapters"; + this.update(this.chaptersBox, "All Chapters"); +}; + +Search.prototype.updateSectionsBox = function(){ + listLibRequest.xml_command = "searchLib"; + listLibRequest.subcommand = "getSectionListings"; + this.update(this.sectionsBox, "All Sections"); +}; + +Search.prototype.updateTextbookBox = function(){ + listLibRequest.xml_command = "searchLib"; + listLibRequest.subcommand = "getDBTextbooks"; + this.update(this.textbooksBox, "All Textbooks"); +}; + +Search.prototype.update = function(box, blankName){ + $.post(webserviceURL, listLibRequest,function(data) { + console.log(data); + try { + var response = $.parseJSON(data); + console.log("result: " + response.server_response); + updateMessage(response.server_response); + + box.options.length = 0; + var options = response.result_data.split(","); + for (var i = 0; i < options.length; i++) { + if (!name.match(/\./)) { + var option = document.createElement("option") + option.value = options[i]; + option.innerHTML = options[i]; + box.add(option, null); + } + } + if (box.childNodes.length > 0) { + var emptyOption = document.createElement("option"); + emptyOption.innerHTML = blankName; + emptyOption.value = ""; + box.add(emptyOption, box.firstChild); + } + //callback(); + } catch (err) { + console.log(err); + var myWindow = window.open('', '', 'width=500,height=800'); + myWindow.document.write(data); + myWindow.focus(); + } + }); +}; + +/* + * needed functions: Both: getProblems + * + * Library: markInSet markRemovedFromSet + * + * Set: addProblem removeProblem reorderProblem view + * + * Problem: (not sure about this yet) view source view problem + * + * we're kind of following an mvc, check out pure for templating should make + * life easy + */ + +/******************************************************************************* + * The library object + ******************************************************************************/ +// object +function Library(name, parent) { + this.name = name; + this.parent = false; + + if(parent){ + this.parent = parent; + this.path = parent.path + "/" + name; + } else { + this.path = name; + } + this.children = new Object(); + + this.problems = new Array(); +} + + + +Library.prototype.loadChildren = function(callback){ + listLibRequest.xml_command = "listLib"; + listLibRequest.command = "dirOnly"; + listLibRequest.maxdepth = 0; + listLibRequest.library_name = this.path; + + updateMessage("Loading libraries... may take some time"); + + var workAroundLibrary = this; + $.post(webserviceURL, listLibRequest, + function(data) { + //console.log(data); + try { + var response = $.parseJSON(data); + console.log("result: " + response.server_response); + updateMessage(response.server_response); + for(var key in response.result_data){ + workAroundLibrary.children[key] = new Library(key, workAroundLibrary); + } + callback(); + } catch (err) { + console.log(err); + var myWindow = window.open('', '', + 'width=500,height=800'); + myWindow.document.write(data); + myWindow.focus(); + } + }); +} + +/* +Right now this is going to store all the files under each library. +This is reduntant! The benifits of this should be discussed at a later date +and a fix (or not) should be decided on. +*/ +Library.prototype.loadProblems = function(callback){ + listLibRequest.xml_command = "listLib"; + listLibRequest.command = "files"; + listLibRequest.maxdepth = 0; + listLibRequest.library_name = this.path+"/"; + + updateMessage("Loading problems"); + console.log(listLibRequest.library_name); + var workAroundLibrary = this; + $.post(webserviceURL, listLibRequest, + function(data) { + console.log(data); + try { + var response = $.parseJSON(data); + console.log("result: " + response.server_response); + updateMessage(response.server_response); + //for(var key in response.result_data){ + // workAroundLibrary.children[key] = new Library(key, workAroundLibrary); + //} + var problemList = response.result_data.split(","); + workAroundLibrary.problems = new Array(); + for(var i = 0; i < problemList.length; i++){ + workAroundLibrary.problems.push(new Problem(problemList[i])); + } + //console.log("Problems:"); + //console.log(workAroundLibrary.problems); + callback(); + } catch (err) { + console.log(err); + var myWindow = window.open('', '', + 'width=500,height=800'); + myWindow.document.write(data); + myWindow.focus(); + } + }); +} + + + +/******************************************************************************* + * The set object + ******************************************************************************/ +// object +function Set(setName) {// id might not exist..use date if nessisary + this.id = generateUniqueID(); + this.name = setName; + this.problems = new Object(); // a hash of problems {id: problemInfo} + this.problemArray = new Array();// redunant but I don't have any better + // ideas atm for keeping order + this.displayBox; + this.previousOrder; +} +// controller +Set.prototype.renderSet = function() { + /* + * sudo code for display stuff: create a tab next to library_box make it's + * id setID load in the problems from the set add the nessisary listeners + * etc to the problems switch to that tab + */ + + if (document.getElementById(this.name) + && $.contains(document.getElementById("problems_container"), + document.getElementById(this.name))) { + // might as well reload the problems + while (this.displayBox.hasChildNodes()) { + this.displayBox.removeChild(this.displayBox.lastChild); + } + for ( var i = 0; i < this.problemArray.length; i++) { + this.renderProblem(this.problemArray[i]); + } + } else { + $tabs.tabs("add", "#" + this.name, this.name + " (" + this.problemArray.length + ")"); + var thisContainer = document.getElementById(this.name); + thisContainer.setAttribute("data-uid", this.id); + this.displayBox = document.createElement("ul"); + this.displayBox.id = this.name + "_list"; + var workAroundSet = this; + $(this.displayBox).sortable({ + axis: 'y', + start : function(event, ui) { + workAroundSet.previousOrder = $(this).sortable('toArray'); + }, + update : function(event, ui) { + workAroundSet.reorderProblems($(this).sortable('toArray')); + } + });// sortable code + thisContainer.appendChild(this.displayBox); + for ( var i = 0; i < this.problemArray.length; i++) { + this.renderProblem(this.problemArray[i]); + } + } +} + +Set.prototype.renderProblem = function(problem) { + var newSetItem = problem.render(this.displayBox); + newSetItem.addEventListener("click", function(event) { + if (!event.altKey) { + $(".ww_selected").removeClass("ww_selected"); + } + $(this).addClass("ww_selected"); + }, false); + $(this.displayBox).sortable("refresh"); +} + +Set.prototype.loadProblems = function(shouldRender) { + listLibRequest.xml_command = "listSetProblems"; + listLibRequest.set = this.name; + this.problems = new Object(); + this.problemArray = new Array(); + var workAroundTheClosure = this; + $.post(webserviceURL, listLibRequest, + function(data) { + try { + var response = $.parseJSON(data); + console.log("result: " + response.server_response); + + var problems = response.result_data.split(","); + for ( var i = 0; i < problems.length; i++) { + if (problems[i] != "") { + var newProblem = new Problem(problems[i]); + workAroundTheClosure.problems[newProblem.path] = newProblem; + workAroundTheClosure.problemArray.push(newProblem); + } + } + document.getElementById(workAroundTheClosure.name + workAroundTheClosure.id).innerHTML = workAroundTheClosure.name + " (" + workAroundTheClosure.problemArray.length + ")"; + if (shouldRender) { + $('a[href="#'+workAroundTheClosure.name+'"] span').text(workAroundTheClosure.name+" ("+ workAroundTheClosure.problemArray.length + ") "); + workAroundTheClosure.renderSet(); + } + } catch (err) { + var myWindow = window.open('', '', + 'width=500,height=800'); + myWindow.document.write(data); + myWindow.focus(); + } + }); +} + +Set.prototype.addProblem = function(probPath) { + listLibRequest.set = this.name;// switch to data attribute + listLibRequest.problemPath = probPath; + listLibRequest.xml_command = "addProblem"; + var workAroundSet = this; + $.post(webserviceURL, listLibRequest, function(data) { + try { + var response = $.parseJSON(data); + console.log("result: " + response.server_response); + updateMessage(response.server_response); + // still have to test for success..everywhere + if (undoing) {// might be a better way to do this later + redo_stack.push(function() { + workAroundSet.removeProblem(probPath); + }); + undoing = false; + } else { + undo_stack.push(function() { + workAroundSet.removeProblem(probPath); + }); + } + workAroundSet.loadProblems($.contains(document.getElementById("problems_container"), document.getElementById(workAroundSet.name))); + } catch (err) { + showErrorResponse(data); + } + }); +} + +Set.prototype.removeProblem = function(probPath) { + listLibRequest.set = this.name;// switch to data attribute + listLibRequest.problemPath = probPath; + listLibRequest.xml_command = "deleteProblem"; + var workAroundSet = this; + $.post(webserviceURL, listLibRequest, function(data) { + try { + var response = $.parseJSON(data); + console.log("result: " + response.server_response); + updateMessage(response.server_response); + // still have to test for success.... + if (undoing) { + redo_stack.push(function() { + workAroundSet.addProblem(probPath); + }); + undoing = false; + } else { + undo_stack.push(function() { + workAroundSet.addProblem(probPath); + }); + } + workAroundSet.loadProblems($.contains(document + .getElementById("problems_container"), document + .getElementById(workAroundSet.name))); + } catch (err) { + showErrorResponse(data); + } + }); +} + +Set.prototype.reorderProblems = function(setOrder) { + var workAroundOrder = this.previousOrder; + var workAroundSet = this; + if(document.getElementById(workAroundSet.displayBox.id)){ + if (undoing) { + redo_stack.push(function() { + // resort the list + if(document.getElementById(workAroundSet.displayBox.id)){ + for ( var i = 0; i < workAroundOrder.length; i++) { + var tempProblem = document.getElementById(workAroundOrder[i]); + workAroundSet.displayBox.removeChild(tempProblem); + workAroundSet.displayBox.appendChild(tempProblem); + } + $(workAroundSet.displayBox).sortable("refresh"); + } + workAroundSet.reorderProblems(workAroundOrder); + }); + undoing = false; + } else { + undo_stack.push(function() { + // resort the list + if(document.getElementById(workAroundSet.displayBox.id)){ + for ( var i = 0; i < workAroundOrder.length; i++) { + var tempProblem = document.getElementById(workAroundOrder[i]); + workAroundSet.displayBox.removeChild(tempProblem); + workAroundSet.displayBox.appendChild(tempProblem); + } + $(workAroundSet.displayBox).sortable("refresh"); + } + // $(workAroundSet.displayBox).sortable( "refreshPositions" ) + workAroundSet.reorderProblems(workAroundOrder); + }); + } + // load problems: + // var problems = this.displayBox.childNodes; + var probList = new Array(); + for ( var i = 0; i < setOrder.length; i++) { + probList.push(document.getElementById(setOrder[i]).getAttribute( + "data-path")); + } + + var probListString = probList.join(","); + listLibRequest.probList = probListString; + listLibRequest.xml_command = "reorderProblems"; + listLibRequest.set = this.name; + $.post(webserviceURL, listLibRequest, function(data) { + try { + var response = $.parseJSON(data); + console.log("result: " + response.server_response); + updateMessage(response.server_response); + } catch (err) { + showErrorResponse(data); + } + }); + this.previousOrder = $(workAroundSet.displayBox).sortable('toArray'); + } +} + +Set.createSet = function(refreshList, callback) {//change callback to work with strings.. + listLibRequest.xml_command = "createNewSet"; + listLibRequest.new_set_name = document.getElementById("dialog_text").value; + $.post(webserviceURL, listLibRequest, function(data) { + try { + var response = $.parseJSON(data); + console.log("result: " + response.server_response); + updateMessage(response.server_response); + } catch (err) { + showErrorResponse(data); + } + if(refreshList){ + refreshList.refresh(problemPlaceholder); + problemPlaceholder = false; + } + if(callback){ + callback(); + } + + }); +} + +/******************************************************************************* + * SetList object needed variables: sets, displaybox, needed functions: create + * set + */ + +function SetList() { + this.setNames = new Array(); + this.sets = new Object(); + this.displayBox = document.getElementById("my_sets_list"); + listLibRequest.xml_command = "listSets"; + var workAroundSetList = this; + console.log("starting set list"); + $.post(webserviceURL, listLibRequest, function(data) { + try { + var response = $.parseJSON(data); + console.log("result: " + response.server_response); + workAroundSetList.setNames = response.result_data.split(","); + workAroundSetList.setNames.sort(); + console.log("found these sets: " + workAroundSetList.setNames); + for ( var i = 0; i < workAroundSetList.setNames.length; i++) { + workAroundSetList.renderList(workAroundSetList.setNames[i]); + } + } catch (err) { + showErrorResponse(data); + } + }); +} + +SetList.prototype.refresh = function(problemPath) { + listLibRequest.xml_command = "listSets"; + var workAroundSetList = this; + $.post(webserviceURL, listLibRequest, function(data) { + try { + var response = $.parseJSON(data); + console.log("result: " + response.server_response); + var newSetList = response.result_data.split(","); + var newSets = new Array(); + for ( var i = 0; i < newSetList.length; i++) { + if ($.inArray(newSetList[i], workAroundSetList.setNames) < 0) { + console.log("rendering set " + newSetList[i] + ""); + newSets.push(newSetList[i]); + } + } + var recievingSet; + for ( var j = 0; j < newSets.length; j++) { + workAroundSetList.setNames.push(newSets[j]); + var recievingSet = workAroundSetList.renderList(newSets[j]); + } + if (recievingSet && problemPath) { + recievingSet.addProblem(problemPath); + } + } catch (err) { + showErrorResponse(data); + } + }); + // how do we check if we already have the set? +} + +SetList.prototype.renderList = function(setName) { + + var addingSet = document.createElement("li"); + + var newSet = new Set(setName); + newSet.loadProblems(false); + addingSet.innerHTML = setName; + addingSet.setAttribute("data-uid", newSet.id); + addingSet.id = newSet.name + newSet.id; + this.sets[newSet.id] = newSet; + + var workAroundSetList = this.sets; + addingSet.addEventListener("click", function(event) { + var clickedSet = workAroundSetList[this.getAttribute("data-uid")]; + clickedSet.loadProblems(true); + $("#problems_container").tabs("select", clickedSet.name); + }, false); + + $(addingSet).insertBefore("#new_problem_set"); + // this.displayBox.appendChild(addingSet); + $(addingSet).droppable( + { + tolerance : 'pointer', + + hoverClass: 'drophover', + + drop : function(event, ui) { + var recievingSet = workAroundSetList[this + .getAttribute("data-uid")]; + recievingSet.addProblem(ui.draggable.attr("data-path")); + } + }); + return newSet; + +}; + +SetList.prototype.createSet = function() { + $("#dialog").dialog('open'); +}; + +/******************************************************************************* + * The problem object + ******************************************************************************/ +// object +function Problem(path) { + this.path = path; + this.data = false; + this.id = generateUniqueID(); +} + +Problem.prototype.render = function(displayBox) { + var problem = this; + listLibRequest.set = problem.path; + listLibRequest.problemSource = problem.path; + listLibRequest.xml_command = "renderProblem"; + var workAroundDisplayBox = displayBox; + var newItem = document.createElement('li'); + newItem.setAttribute("data-path", problem.path); + newItem.setAttribute("data-uid", problem.id); + newItem.id = problem.path + problem.id; + //set a loading image while we wait for the problem + var loadingImage = document.createElement("img"); + loadingImage.src = '/webwork2_files/images/ajax-loader-small.gif'; + newItem.appendChild(loadingImage); + //add the item to the box so that we dont lose our place + workAroundDisplayBox.appendChild(newItem); + + var handle = document.createElement("div"); + handle.className = "handle"; + var container = document.createElement("div"); + + if (!problem.data) { + //if we haven't gotten this problem yet, ask for it + $.post(webserviceURL, listLibRequest, function(data) { + //console.log(data); + //newItem.innerHTML = data; + container.innerHTML = data; + newItem.innerHTML = null; + newItem.appendChild(handle); + newItem.appendChild(container); + $(newItem).draggable({ + helper : 'clone', + handle : 'div.handle', + revert : true, + appendTo : 'body', + cursorAt : { + top : 0, + left : 0 + }, + opacity : 0.35 + }); + problem.data = data; + }); + } else { + console.log("didn't have to go to the server"); + //if we've gotten it just load up the stored data + //newItem.innerHTML = data; + container.innerHTML = problem.data; + newItem.innerHTML = null; + newItem.appendChild(handle); + newItem.appendChild(container); + $(newItem).draggable({ + helper : 'clone', + revert : true, + handle : 'div.handle', + appendTo : 'body', + cursorAt : { + top : 0, + left : 0 + }, + opacity : 0.35 + }); + } + + newItem.addEventListener("mouseover", highlightSets, false); + //newItem.addEventListener("mouseout", unHighlightSets, false); + + return newItem; +} diff --git a/htdocs/js/modernizr-2.0.6.js b/htdocs/js/modernizr-2.0.6.js new file mode 100644 index 0000000000..f9e57c81b4 --- /dev/null +++ b/htdocs/js/modernizr-2.0.6.js @@ -0,0 +1,1116 @@ +/*! + * Modernizr v2.0.6 + * http://www.modernizr.com + * + * Copyright (c) 2009-2011 Faruk Ates, Paul Irish, Alex Sexton + * Dual-licensed under the BSD or MIT licenses: www.modernizr.com/license/ + */ + +/* + * Modernizr tests which native CSS3 and HTML5 features are available in + * the current UA and makes the results available to you in two ways: + * as properties on a global Modernizr object, and as classes on the + * element. This information allows you to progressively enhance + * your pages with a granular level of control over the experience. + * + * Modernizr has an optional (not included) conditional resource loader + * called Modernizr.load(), based on Yepnope.js (yepnopejs.com). + * To get a build that includes Modernizr.load(), as well as choosing + * which tests to include, go to www.modernizr.com/download/ + * + * Authors Faruk Ates, Paul Irish, Alex Sexton, + * Contributors Ryan Seddon, Ben Alman + */ + +window.Modernizr = (function( window, document, undefined ) { + + var version = '2.0.6', + + Modernizr = {}, + + // option for enabling the HTML classes to be added + enableClasses = true, + + docElement = document.documentElement, + docHead = document.head || document.getElementsByTagName('head')[0], + + /** + * Create our "modernizr" element that we do most feature tests on. + */ + mod = 'modernizr', + modElem = document.createElement(mod), + mStyle = modElem.style, + + /** + * Create the input element for various Web Forms feature tests. + */ + inputElem = document.createElement('input'), + + smile = ':)', + + toString = Object.prototype.toString, + + // List of property values to set for css tests. See ticket #21 + prefixes = ' -webkit- -moz- -o- -ms- -khtml- '.split(' '), + + // Following spec is to expose vendor-specific style properties as: + // elem.style.WebkitBorderRadius + // and the following would be incorrect: + // elem.style.webkitBorderRadius + + // Webkit ghosts their properties in lowercase but Opera & Moz do not. + // Microsoft foregoes prefixes entirely <= IE8, but appears to + // use a lowercase `ms` instead of the correct `Ms` in IE9 + + // More here: http://github.com/Modernizr/Modernizr/issues/issue/21 + domPrefixes = 'Webkit Moz O ms Khtml'.split(' '), + + ns = {'svg': 'http://www.w3.org/2000/svg'}, + + tests = {}, + inputs = {}, + attrs = {}, + + classes = [], + + featureName, // used in testing loop + + + // Inject element with style element and some CSS rules + injectElementWithStyles = function( rule, callback, nodes, testnames ) { + + var style, ret, node, + div = document.createElement('div'); + + if ( parseInt(nodes, 10) ) { + // In order not to give false positives we create a node for each test + // This also allows the method to scale for unspecified uses + while ( nodes-- ) { + node = document.createElement('div'); + node.id = testnames ? testnames[nodes] : mod + (nodes + 1); + div.appendChild(node); + } + } + + // '].join(''); + div.id = mod; + div.innerHTML += style; + docElement.appendChild(div); + + ret = callback(div, rule); + div.parentNode.removeChild(div); + + return !!ret; + + }, + + + // adapted from matchMedia polyfill + // by Scott Jehl and Paul Irish + // gist.github.com/786768 + testMediaQuery = function( mq ) { + + if ( window.matchMedia ) { + return matchMedia(mq).matches; + } + + var bool; + + injectElementWithStyles('@media ' + mq + ' { #' + mod + ' { position: absolute; } }', function( node ) { + bool = (window.getComputedStyle ? + getComputedStyle(node, null) : + node.currentStyle)['position'] == 'absolute'; + }); + + return bool; + + }, + + + /** + * isEventSupported determines if a given element supports the given event + * function from http://yura.thinkweb2.com/isEventSupported/ + */ + isEventSupported = (function() { + + var TAGNAMES = { + 'select': 'input', 'change': 'input', + 'submit': 'form', 'reset': 'form', + 'error': 'img', 'load': 'img', 'abort': 'img' + }; + + function isEventSupported( eventName, element ) { + + element = element || document.createElement(TAGNAMES[eventName] || 'div'); + eventName = 'on' + eventName; + + // When using `setAttribute`, IE skips "unload", WebKit skips "unload" and "resize", whereas `in` "catches" those + var isSupported = eventName in element; + + if ( !isSupported ) { + // If it has no `setAttribute` (i.e. doesn't implement Node interface), try generic element + if ( !element.setAttribute ) { + element = document.createElement('div'); + } + if ( element.setAttribute && element.removeAttribute ) { + element.setAttribute(eventName, ''); + isSupported = is(element[eventName], 'function'); + + // If property was created, "remove it" (by setting value to `undefined`) + if ( !is(element[eventName], undefined) ) { + element[eventName] = undefined; + } + element.removeAttribute(eventName); + } + } + + element = null; + return isSupported; + } + return isEventSupported; + })(); + + // hasOwnProperty shim by kangax needed for Safari 2.0 support + var _hasOwnProperty = ({}).hasOwnProperty, hasOwnProperty; + if ( !is(_hasOwnProperty, undefined) && !is(_hasOwnProperty.call, undefined) ) { + hasOwnProperty = function (object, property) { + return _hasOwnProperty.call(object, property); + }; + } + else { + hasOwnProperty = function (object, property) { /* yes, this can give false positives/negatives, but most of the time we don't care about those */ + return ((property in object) && is(object.constructor.prototype[property], undefined)); + }; + } + + /** + * setCss applies given styles to the Modernizr DOM node. + */ + function setCss( str ) { + mStyle.cssText = str; + } + + /** + * setCssAll extrapolates all vendor-specific css strings. + */ + function setCssAll( str1, str2 ) { + return setCss(prefixes.join(str1 + ';') + ( str2 || '' )); + } + + /** + * is returns a boolean for if typeof obj is exactly type. + */ + function is( obj, type ) { + return typeof obj === type; + } + + /** + * contains returns a boolean for if substr is found within str. + */ + function contains( str, substr ) { + return !!~('' + str).indexOf(substr); + } + + /** + * testProps is a generic CSS / DOM property test; if a browser supports + * a certain property, it won't return undefined for it. + * A supported CSS property returns empty string when its not yet set. + */ + function testProps( props, prefixed ) { + for ( var i in props ) { + if ( mStyle[ props[i] ] !== undefined ) { + return prefixed == 'pfx' ? props[i] : true; + } + } + return false; + } + + /** + * testPropsAll tests a list of DOM properties we want to check against. + * We specify literally ALL possible (known and/or likely) properties on + * the element including the non-vendor prefixed one, for forward- + * compatibility. + */ + function testPropsAll( prop, prefixed ) { + + var ucProp = prop.charAt(0).toUpperCase() + prop.substr(1), + props = (prop + ' ' + domPrefixes.join(ucProp + ' ') + ucProp).split(' '); + + return testProps(props, prefixed); + } + + /** + * testBundle tests a list of CSS features that require element and style injection. + * By bundling them together we can reduce the need to touch the DOM multiple times. + */ + /*>>testBundle*/ + var testBundle = (function( styles, tests ) { + var style = styles.join(''), + len = tests.length; + + injectElementWithStyles(style, function( node, rule ) { + var style = document.styleSheets[document.styleSheets.length - 1], + // IE8 will bork if you create a custom build that excludes both fontface and generatedcontent tests. + // So we check for cssRules and that there is a rule available + // More here: https://github.com/Modernizr/Modernizr/issues/288 & https://github.com/Modernizr/Modernizr/issues/293 + cssText = style.cssRules && style.cssRules[0] ? style.cssRules[0].cssText : style.cssText || "", + children = node.childNodes, hash = {}; + + while ( len-- ) { + hash[children[len].id] = children[len]; + } + + /*>>touch*/ Modernizr['touch'] = ('ontouchstart' in window) || hash['touch'].offsetTop === 9; /*>>touch*/ + /*>>csstransforms3d*/ Modernizr['csstransforms3d'] = hash['csstransforms3d'].offsetLeft === 9; /*>>csstransforms3d*/ + /*>>generatedcontent*/Modernizr['generatedcontent'] = hash['generatedcontent'].offsetHeight >= 1; /*>>generatedcontent*/ + /*>>fontface*/ Modernizr['fontface'] = /src/i.test(cssText) && + cssText.indexOf(rule.split(' ')[0]) === 0; /*>>fontface*/ + }, len, tests); + + })([ + // Pass in styles to be injected into document + /*>>fontface*/ '@font-face {font-family:"font";src:url("https://")}' /*>>fontface*/ + + /*>>touch*/ ,['@media (',prefixes.join('touch-enabled),('),mod,')', + '{#touch{top:9px;position:absolute}}'].join('') /*>>touch*/ + + /*>>csstransforms3d*/ ,['@media (',prefixes.join('transform-3d),('),mod,')', + '{#csstransforms3d{left:9px;position:absolute}}'].join('')/*>>csstransforms3d*/ + + /*>>generatedcontent*/,['#generatedcontent:after{content:"',smile,'";visibility:hidden}'].join('') /*>>generatedcontent*/ + ], + [ + /*>>fontface*/ 'fontface' /*>>fontface*/ + /*>>touch*/ ,'touch' /*>>touch*/ + /*>>csstransforms3d*/ ,'csstransforms3d' /*>>csstransforms3d*/ + /*>>generatedcontent*/,'generatedcontent' /*>>generatedcontent*/ + + ]);/*>>testBundle*/ + + + /** + * Tests + * ----- + */ + + tests['flexbox'] = function() { + /** + * setPrefixedValueCSS sets the property of a specified element + * adding vendor prefixes to the VALUE of the property. + * @param {Element} element + * @param {string} property The property name. This will not be prefixed. + * @param {string} value The value of the property. This WILL be prefixed. + * @param {string=} extra Additional CSS to append unmodified to the end of + * the CSS string. + */ + function setPrefixedValueCSS( element, property, value, extra ) { + property += ':'; + element.style.cssText = (property + prefixes.join(value + ';' + property)).slice(0, -property.length) + (extra || ''); + } + + /** + * setPrefixedPropertyCSS sets the property of a specified element + * adding vendor prefixes to the NAME of the property. + * @param {Element} element + * @param {string} property The property name. This WILL be prefixed. + * @param {string} value The value of the property. This will not be prefixed. + * @param {string=} extra Additional CSS to append unmodified to the end of + * the CSS string. + */ + function setPrefixedPropertyCSS( element, property, value, extra ) { + element.style.cssText = prefixes.join(property + ':' + value + ';') + (extra || ''); + } + + var c = document.createElement('div'), + elem = document.createElement('div'); + + setPrefixedValueCSS(c, 'display', 'box', 'width:42px;padding:0;'); + setPrefixedPropertyCSS(elem, 'box-flex', '1', 'width:10px;'); + + c.appendChild(elem); + docElement.appendChild(c); + + var ret = elem.offsetWidth === 42; + + c.removeChild(elem); + docElement.removeChild(c); + + return ret; + }; + + // On the S60 and BB Storm, getContext exists, but always returns undefined + // http://github.com/Modernizr/Modernizr/issues/issue/97/ + + tests['canvas'] = function() { + var elem = document.createElement('canvas'); + return !!(elem.getContext && elem.getContext('2d')); + }; + + tests['canvastext'] = function() { + return !!(Modernizr['canvas'] && is(document.createElement('canvas').getContext('2d').fillText, 'function')); + }; + + // This WebGL test may false positive. + // But really it's quite impossible to know whether webgl will succeed until after you create the context. + // You might have hardware that can support a 100x100 webgl canvas, but will not support a 1000x1000 webgl + // canvas. So this feature inference is weak, but intentionally so. + + // It is known to false positive in FF4 with certain hardware and the iPad 2. + + tests['webgl'] = function() { + return !!window.WebGLRenderingContext; + }; + + /* + * The Modernizr.touch test only indicates if the browser supports + * touch events, which does not necessarily reflect a touchscreen + * device, as evidenced by tablets running Windows 7 or, alas, + * the Palm Pre / WebOS (touch) phones. + * + * Additionally, Chrome (desktop) used to lie about its support on this, + * but that has since been rectified: http://crbug.com/36415 + * + * We also test for Firefox 4 Multitouch Support. + * + * For more info, see: http://modernizr.github.com/Modernizr/touch.html + */ + + tests['touch'] = function() { + return Modernizr['touch']; + }; + + /** + * geolocation tests for the new Geolocation API specification. + * This test is a standards compliant-only test; for more complete + * testing, including a Google Gears fallback, please see: + * http://code.google.com/p/geo-location-javascript/ + * or view a fallback solution using google's geo API: + * http://gist.github.com/366184 + */ + tests['geolocation'] = function() { + return !!navigator.geolocation; + }; + + // Per 1.6: + // This used to be Modernizr.crosswindowmessaging but the longer + // name has been deprecated in favor of a shorter and property-matching one. + // The old API is still available in 1.6, but as of 2.0 will throw a warning, + // and in the first release thereafter disappear entirely. + tests['postmessage'] = function() { + return !!window.postMessage; + }; + + // Web SQL database detection is tricky: + + // In chrome incognito mode, openDatabase is truthy, but using it will + // throw an exception: http://crbug.com/42380 + // We can create a dummy database, but there is no way to delete it afterwards. + + // Meanwhile, Safari users can get prompted on any database creation. + // If they do, any page with Modernizr will give them a prompt: + // http://github.com/Modernizr/Modernizr/issues/closed#issue/113 + + // We have chosen to allow the Chrome incognito false positive, so that Modernizr + // doesn't litter the web with these test databases. As a developer, you'll have + // to account for this gotcha yourself. + tests['websqldatabase'] = function() { + var result = !!window.openDatabase; + /* if (result){ + try { + result = !!openDatabase( mod + "testdb", "1.0", mod + "testdb", 2e4); + } catch(e) { + } + } */ + return result; + }; + + // Vendors had inconsistent prefixing with the experimental Indexed DB: + // - Webkit's implementation is accessible through webkitIndexedDB + // - Firefox shipped moz_indexedDB before FF4b9, but since then has been mozIndexedDB + // For speed, we don't test the legacy (and beta-only) indexedDB + tests['indexedDB'] = function() { + for ( var i = -1, len = domPrefixes.length; ++i < len; ){ + if ( window[domPrefixes[i].toLowerCase() + 'IndexedDB'] ){ + return true; + } + } + return !!window.indexedDB; + }; + + // documentMode logic from YUI to filter out IE8 Compat Mode + // which false positives. + tests['hashchange'] = function() { + return isEventSupported('hashchange', window) && (document.documentMode === undefined || document.documentMode > 7); + }; + + // Per 1.6: + // This used to be Modernizr.historymanagement but the longer + // name has been deprecated in favor of a shorter and property-matching one. + // The old API is still available in 1.6, but as of 2.0 will throw a warning, + // and in the first release thereafter disappear entirely. + tests['history'] = function() { + return !!(window.history && history.pushState); + }; + + tests['draganddrop'] = function() { + return isEventSupported('dragstart') && isEventSupported('drop'); + }; + + // Mozilla is targeting to land MozWebSocket for FF6 + // bugzil.la/659324 + tests['websockets'] = function() { + for ( var i = -1, len = domPrefixes.length; ++i < len; ){ + if ( window[domPrefixes[i] + 'WebSocket'] ){ + return true; + } + } + return 'WebSocket' in window; + }; + + + // http://css-tricks.com/rgba-browser-support/ + tests['rgba'] = function() { + // Set an rgba() color and check the returned value + + setCss('background-color:rgba(150,255,150,.5)'); + + return contains(mStyle.backgroundColor, 'rgba'); + }; + + tests['hsla'] = function() { + // Same as rgba(), in fact, browsers re-map hsla() to rgba() internally, + // except IE9 who retains it as hsla + + setCss('background-color:hsla(120,40%,100%,.5)'); + + return contains(mStyle.backgroundColor, 'rgba') || contains(mStyle.backgroundColor, 'hsla'); + }; + + tests['multiplebgs'] = function() { + // Setting multiple images AND a color on the background shorthand property + // and then querying the style.background property value for the number of + // occurrences of "url(" is a reliable method for detecting ACTUAL support for this! + + setCss('background:url(https://),url(https://),red url(https://)'); + + // If the UA supports multiple backgrounds, there should be three occurrences + // of the string "url(" in the return value for elemStyle.background + + return /(url\s*\(.*?){3}/.test(mStyle.background); + }; + + + // In testing support for a given CSS property, it's legit to test: + // `elem.style[styleName] !== undefined` + // If the property is supported it will return an empty string, + // if unsupported it will return undefined. + + // We'll take advantage of this quick test and skip setting a style + // on our modernizr element, but instead just testing undefined vs + // empty string. + + + tests['backgroundsize'] = function() { + return testPropsAll('backgroundSize'); + }; + + tests['borderimage'] = function() { + return testPropsAll('borderImage'); + }; + + + // Super comprehensive table about all the unique implementations of + // border-radius: http://muddledramblings.com/table-of-css3-border-radius-compliance + + tests['borderradius'] = function() { + return testPropsAll('borderRadius'); + }; + + // WebOS unfortunately false positives on this test. + tests['boxshadow'] = function() { + return testPropsAll('boxShadow'); + }; + + // FF3.0 will false positive on this test + tests['textshadow'] = function() { + return document.createElement('div').style.textShadow === ''; + }; + + + tests['opacity'] = function() { + // Browsers that actually have CSS Opacity implemented have done so + // according to spec, which means their return values are within the + // range of [0.0,1.0] - including the leading zero. + + setCssAll('opacity:.55'); + + // The non-literal . in this regex is intentional: + // German Chrome returns this value as 0,55 + // https://github.com/Modernizr/Modernizr/issues/#issue/59/comment/516632 + return /^0.55$/.test(mStyle.opacity); + }; + + + tests['cssanimations'] = function() { + return testPropsAll('animationName'); + }; + + + tests['csscolumns'] = function() { + return testPropsAll('columnCount'); + }; + + + tests['cssgradients'] = function() { + /** + * For CSS Gradients syntax, please see: + * http://webkit.org/blog/175/introducing-css-gradients/ + * https://developer.mozilla.org/en/CSS/-moz-linear-gradient + * https://developer.mozilla.org/en/CSS/-moz-radial-gradient + * http://dev.w3.org/csswg/css3-images/#gradients- + */ + + var str1 = 'background-image:', + str2 = 'gradient(linear,left top,right bottom,from(#9f9),to(white));', + str3 = 'linear-gradient(left top,#9f9, white);'; + + setCss( + (str1 + prefixes.join(str2 + str1) + prefixes.join(str3 + str1)).slice(0, -str1.length) + ); + + return contains(mStyle.backgroundImage, 'gradient'); + }; + + + tests['cssreflections'] = function() { + return testPropsAll('boxReflect'); + }; + + + tests['csstransforms'] = function() { + return !!testProps(['transformProperty', 'WebkitTransform', 'MozTransform', 'OTransform', 'msTransform']); + }; + + + tests['csstransforms3d'] = function() { + + var ret = !!testProps(['perspectiveProperty', 'WebkitPerspective', 'MozPerspective', 'OPerspective', 'msPerspective']); + + // Webkit’s 3D transforms are passed off to the browser's own graphics renderer. + // It works fine in Safari on Leopard and Snow Leopard, but not in Chrome in + // some conditions. As a result, Webkit typically recognizes the syntax but + // will sometimes throw a false positive, thus we must do a more thorough check: + if ( ret && 'webkitPerspective' in docElement.style ) { + + // Webkit allows this media query to succeed only if the feature is enabled. + // `@media (transform-3d),(-o-transform-3d),(-moz-transform-3d),(-ms-transform-3d),(-webkit-transform-3d),(modernizr){ ... }` + ret = Modernizr['csstransforms3d']; + } + return ret; + }; + + + tests['csstransitions'] = function() { + return testPropsAll('transitionProperty'); + }; + + + /*>>fontface*/ + // @font-face detection routine by Diego Perini + // http://javascript.nwbox.com/CSSSupport/ + tests['fontface'] = function() { + return Modernizr['fontface']; + }; + /*>>fontface*/ + + // CSS generated content detection + tests['generatedcontent'] = function() { + return Modernizr['generatedcontent']; + }; + + + + // These tests evaluate support of the video/audio elements, as well as + // testing what types of content they support. + // + // We're using the Boolean constructor here, so that we can extend the value + // e.g. Modernizr.video // true + // Modernizr.video.ogg // 'probably' + // + // Codec values from : http://github.com/NielsLeenheer/html5test/blob/9106a8/index.html#L845 + // thx to NielsLeenheer and zcorpan + + // Note: in FF 3.5.1 and 3.5.0, "no" was a return value instead of empty string. + // Modernizr does not normalize for that. + + tests['video'] = function() { + var elem = document.createElement('video'), + bool = false; + + // IE9 Running on Windows Server SKU can cause an exception to be thrown, bug #224 + try { + if ( bool = !!elem.canPlayType ) { + bool = new Boolean(bool); + bool.ogg = elem.canPlayType('video/ogg; codecs="theora"'); + + // Workaround required for IE9, which doesn't report video support without audio codec specified. + // bug 599718 @ msft connect + var h264 = 'video/mp4; codecs="avc1.42E01E'; + bool.h264 = elem.canPlayType(h264 + '"') || elem.canPlayType(h264 + ', mp4a.40.2"'); + + bool.webm = elem.canPlayType('video/webm; codecs="vp8, vorbis"'); + } + + } catch(e) { } + + return bool; + }; + + tests['audio'] = function() { + var elem = document.createElement('audio'), + bool = false; + + try { + if ( bool = !!elem.canPlayType ) { + bool = new Boolean(bool); + bool.ogg = elem.canPlayType('audio/ogg; codecs="vorbis"'); + bool.mp3 = elem.canPlayType('audio/mpeg;'); + + // Mimetypes accepted: + // https://developer.mozilla.org/En/Media_formats_supported_by_the_audio_and_video_elements + // http://bit.ly/iphoneoscodecs + bool.wav = elem.canPlayType('audio/wav; codecs="1"'); + bool.m4a = elem.canPlayType('audio/x-m4a;') || elem.canPlayType('audio/aac;'); + } + } catch(e) { } + + return bool; + }; + + + // Firefox has made these tests rather unfun. + + // In FF4, if disabled, window.localStorage should === null. + + // Normally, we could not test that directly and need to do a + // `('localStorage' in window) && ` test first because otherwise Firefox will + // throw http://bugzil.la/365772 if cookies are disabled + + // However, in Firefox 4 betas, if dom.storage.enabled == false, just mentioning + // the property will throw an exception. http://bugzil.la/599479 + // This looks to be fixed for FF4 Final. + + // Because we are forced to try/catch this, we'll go aggressive. + + // FWIW: IE8 Compat mode supports these features completely: + // http://www.quirksmode.org/dom/html5.html + // But IE8 doesn't support either with local files + + tests['localstorage'] = function() { + try { + return !!localStorage.getItem; + } catch(e) { + return false; + } + }; + + tests['sessionstorage'] = function() { + try { + return !!sessionStorage.getItem; + } catch(e){ + return false; + } + }; + + + tests['webworkers'] = function() { + return !!window.Worker; + }; + + + tests['applicationcache'] = function() { + return !!window.applicationCache; + }; + + + // Thanks to Erik Dahlstrom + tests['svg'] = function() { + return !!document.createElementNS && !!document.createElementNS(ns.svg, 'svg').createSVGRect; + }; + + // specifically for SVG inline in HTML, not within XHTML + // test page: paulirish.com/demo/inline-svg + tests['inlinesvg'] = function() { + var div = document.createElement('div'); + div.innerHTML = ''; + return (div.firstChild && div.firstChild.namespaceURI) == ns.svg; + }; + + // Thanks to F1lt3r and lucideer, ticket #35 + tests['smil'] = function() { + return !!document.createElementNS && /SVG/.test(toString.call(document.createElementNS(ns.svg, 'animate'))); + }; + + tests['svgclippaths'] = function() { + // Possibly returns a false positive in Safari 3.2? + return !!document.createElementNS && /SVG/.test(toString.call(document.createElementNS(ns.svg, 'clipPath'))); + }; + + // input features and input types go directly onto the ret object, bypassing the tests loop. + // Hold this guy to execute in a moment. + function webforms() { + // Run through HTML5's new input attributes to see if the UA understands any. + // We're using f which is the element created early on + // Mike Taylr has created a comprehensive resource for testing these attributes + // when applied to all input types: + // http://miketaylr.com/code/input-type-attr.html + // spec: http://www.whatwg.org/specs/web-apps/current-work/multipage/the-input-element.html#input-type-attr-summary + + // Only input placeholder is tested while textarea's placeholder is not. + // Currently Safari 4 and Opera 11 have support only for the input placeholder + // Both tests are available in feature-detects/forms-placeholder.js + Modernizr['input'] = (function( props ) { + for ( var i = 0, len = props.length; i < len; i++ ) { + attrs[ props[i] ] = !!(props[i] in inputElem); + } + return attrs; + })('autocomplete autofocus list placeholder max min multiple pattern required step'.split(' ')); + + // Run through HTML5's new input types to see if the UA understands any. + // This is put behind the tests runloop because it doesn't return a + // true/false like all the other tests; instead, it returns an object + // containing each input type with its corresponding true/false value + + // Big thanks to @miketaylr for the html5 forms expertise. http://miketaylr.com/ + Modernizr['inputtypes'] = (function(props) { + + for ( var i = 0, bool, inputElemType, defaultView, len = props.length; i < len; i++ ) { + + inputElem.setAttribute('type', inputElemType = props[i]); + bool = inputElem.type !== 'text'; + + // We first check to see if the type we give it sticks.. + // If the type does, we feed it a textual value, which shouldn't be valid. + // If the value doesn't stick, we know there's input sanitization which infers a custom UI + if ( bool ) { + + inputElem.value = smile; + inputElem.style.cssText = 'position:absolute;visibility:hidden;'; + + if ( /^range$/.test(inputElemType) && inputElem.style.WebkitAppearance !== undefined ) { + + docElement.appendChild(inputElem); + defaultView = document.defaultView; + + // Safari 2-4 allows the smiley as a value, despite making a slider + bool = defaultView.getComputedStyle && + defaultView.getComputedStyle(inputElem, null).WebkitAppearance !== 'textfield' && + // Mobile android web browser has false positive, so must + // check the height to see if the widget is actually there. + (inputElem.offsetHeight !== 0); + + docElement.removeChild(inputElem); + + } else if ( /^(search|tel)$/.test(inputElemType) ){ + // Spec doesnt define any special parsing or detectable UI + // behaviors so we pass these through as true + + // Interestingly, opera fails the earlier test, so it doesn't + // even make it here. + + } else if ( /^(url|email)$/.test(inputElemType) ) { + // Real url and email support comes with prebaked validation. + bool = inputElem.checkValidity && inputElem.checkValidity() === false; + + } else if ( /^color$/.test(inputElemType) ) { + // chuck into DOM and force reflow for Opera bug in 11.00 + // github.com/Modernizr/Modernizr/issues#issue/159 + docElement.appendChild(inputElem); + docElement.offsetWidth; + bool = inputElem.value != smile; + docElement.removeChild(inputElem); + + } else { + // If the upgraded input compontent rejects the :) text, we got a winner + bool = inputElem.value != smile; + } + } + + inputs[ props[i] ] = !!bool; + } + return inputs; + })('search tel url email datetime date month week time datetime-local number range color'.split(' ')); + } + + + // End of test definitions + // ----------------------- + + + + // Run through all tests and detect their support in the current UA. + // todo: hypothetically we could be doing an array of tests and use a basic loop here. + for ( var feature in tests ) { + if ( hasOwnProperty(tests, feature) ) { + // run the test, throw the return value into the Modernizr, + // then based on that boolean, define an appropriate className + // and push it into an array of classes we'll join later. + featureName = feature.toLowerCase(); + Modernizr[featureName] = tests[feature](); + + classes.push((Modernizr[featureName] ? '' : 'no-') + featureName); + } + } + + // input tests need to run. + Modernizr.input || webforms(); + + + /** + * addTest allows the user to define their own feature tests + * the result will be added onto the Modernizr object, + * as well as an appropriate className set on the html element + * + * @param feature - String naming the feature + * @param test - Function returning true if feature is supported, false if not + */ + Modernizr.addTest = function ( feature, test ) { + if ( typeof feature == "object" ) { + for ( var key in feature ) { + if ( hasOwnProperty( feature, key ) ) { + Modernizr.addTest( key, feature[ key ] ); + } + } + } else { + + feature = feature.toLowerCase(); + + if ( Modernizr[feature] !== undefined ) { + // we're going to quit if you're trying to overwrite an existing test + // if we were to allow it, we'd do this: + // var re = new RegExp("\\b(no-)?" + feature + "\\b"); + // docElement.className = docElement.className.replace( re, '' ); + // but, no rly, stuff 'em. + return; + } + + test = typeof test == "boolean" ? test : !!test(); + + docElement.className += ' ' + (test ? '' : 'no-') + feature; + Modernizr[feature] = test; + + } + + return Modernizr; // allow chaining. + }; + + + // Reset modElem.cssText to nothing to reduce memory footprint. + setCss(''); + modElem = inputElem = null; + + //>>BEGIN IEPP + // Enable HTML 5 elements for styling (and printing) in IE. + if ( window.attachEvent && (function(){ var elem = document.createElement('div'); + elem.innerHTML = ''; + return elem.childNodes.length !== 1; })() ) { + + // iepp v2 by @jon_neal & afarkas : github.com/aFarkas/iepp/ + (function(win, doc) { + win.iepp = win.iepp || {}; + var iepp = win.iepp, + elems = iepp.html5elements || 'abbr|article|aside|audio|canvas|datalist|details|figcaption|figure|footer|header|hgroup|mark|meter|nav|output|progress|section|summary|time|video', + elemsArr = elems.split('|'), + elemsArrLen = elemsArr.length, + elemRegExp = new RegExp('(^|\\s)('+elems+')', 'gi'), + tagRegExp = new RegExp('<(\/*)('+elems+')', 'gi'), + filterReg = /^\s*[\{\}]\s*$/, + ruleRegExp = new RegExp('(^|[^\\n]*?\\s)('+elems+')([^\\n]*)({[\\n\\w\\W]*?})', 'gi'), + docFrag = doc.createDocumentFragment(), + html = doc.documentElement, + head = html.firstChild, + bodyElem = doc.createElement('body'), + styleElem = doc.createElement('style'), + printMedias = /print|all/, + body; + function shim(doc) { + var a = -1; + while (++a < elemsArrLen) + // Use createElement so IE allows HTML5-named elements in a document + doc.createElement(elemsArr[a]); + } + + iepp.getCSS = function(styleSheetList, mediaType) { + if(styleSheetList+'' === undefined){return '';} + var a = -1, + len = styleSheetList.length, + styleSheet, + cssTextArr = []; + while (++a < len) { + styleSheet = styleSheetList[a]; + //currently no test for disabled/alternate stylesheets + if(styleSheet.disabled){continue;} + mediaType = styleSheet.media || mediaType; + // Get css from all non-screen stylesheets and their imports + if (printMedias.test(mediaType)) cssTextArr.push(iepp.getCSS(styleSheet.imports, mediaType), styleSheet.cssText); + //reset mediaType to all with every new *not imported* stylesheet + mediaType = 'all'; + } + return cssTextArr.join(''); + }; + + iepp.parseCSS = function(cssText) { + var cssTextArr = [], + rule; + while ((rule = ruleRegExp.exec(cssText)) != null){ + // Replace all html5 element references with iepp substitute classnames + cssTextArr.push(( (filterReg.exec(rule[1]) ? '\n' : rule[1]) +rule[2]+rule[3]).replace(elemRegExp, '$1.iepp_$2')+rule[4]); + } + return cssTextArr.join('\n'); + }; + + iepp.writeHTML = function() { + var a = -1; + body = body || doc.body; + while (++a < elemsArrLen) { + var nodeList = doc.getElementsByTagName(elemsArr[a]), + nodeListLen = nodeList.length, + b = -1; + while (++b < nodeListLen) + if (nodeList[b].className.indexOf('iepp_') < 0) + // Append iepp substitute classnames to all html5 elements + nodeList[b].className += ' iepp_'+elemsArr[a]; + } + docFrag.appendChild(body); + html.appendChild(bodyElem); + // Write iepp substitute print-safe document + bodyElem.className = body.className; + bodyElem.id = body.id; + // Replace HTML5 elements with which is print-safe and shouldn't conflict since it isn't part of html5 + bodyElem.innerHTML = body.innerHTML.replace(tagRegExp, '<$1font'); + }; + + + iepp._beforePrint = function() { + // Write iepp custom print CSS + styleElem.styleSheet.cssText = iepp.parseCSS(iepp.getCSS(doc.styleSheets, 'all')); + iepp.writeHTML(); + }; + + iepp.restoreHTML = function(){ + // Undo everything done in onbeforeprint + bodyElem.innerHTML = ''; + html.removeChild(bodyElem); + html.appendChild(body); + }; + + iepp._afterPrint = function(){ + // Undo everything done in onbeforeprint + iepp.restoreHTML(); + styleElem.styleSheet.cssText = ''; + }; + + + + // Shim the document and iepp fragment + shim(doc); + shim(docFrag); + + // + if(iepp.disablePP){return;} + + // Add iepp custom print style element + head.insertBefore(styleElem, head.firstChild); + styleElem.media = 'print'; + styleElem.className = 'iepp-printshim'; + win.attachEvent( + 'onbeforeprint', + iepp._beforePrint + ); + win.attachEvent( + 'onafterprint', + iepp._afterPrint + ); + })(window, document); + } + //>>END IEPP + + // Assign private properties to the return object with prefix + Modernizr._version = version; + + // expose these for the plugin API. Look in the source for how to join() them against your input + Modernizr._prefixes = prefixes; + Modernizr._domPrefixes = domPrefixes; + + // Modernizr.mq tests a given media query, live against the current state of the window + // A few important notes: + // * If a browser does not support media queries at all (eg. oldIE) the mq() will always return false + // * A max-width or orientation query will be evaluated against the current state, which may change later. + // * You must specify values. Eg. If you are testing support for the min-width media query use: + // Modernizr.mq('(min-width:0)') + // usage: + // Modernizr.mq('only screen and (max-width:768)') + Modernizr.mq = testMediaQuery; + + // Modernizr.hasEvent() detects support for a given event, with an optional element to test on + // Modernizr.hasEvent('gesturestart', elem) + Modernizr.hasEvent = isEventSupported; + + // Modernizr.testProp() investigates whether a given style property is recognized + // Note that the property names must be provided in the camelCase variant. + // Modernizr.testProp('pointerEvents') + Modernizr.testProp = function(prop){ + return testProps([prop]); + }; + + // Modernizr.testAllProps() investigates whether a given style property, + // or any of its vendor-prefixed variants, is recognized + // Note that the property names must be provided in the camelCase variant. + // Modernizr.testAllProps('boxSizing') + Modernizr.testAllProps = testPropsAll; + + + + // Modernizr.testStyles() allows you to add custom styles to the document and test an element afterwards + // Modernizr.testStyles('#modernizr { position:absolute }', function(elem, rule){ ... }) + Modernizr.testStyles = injectElementWithStyles; + + + // Modernizr.prefixed() returns the prefixed or nonprefixed property name variant of your input + // Modernizr.prefixed('boxSizing') // 'MozBoxSizing' + + // Properties must be passed as dom-style camelcase, rather than `box-sizing` hypentated style. + // Return values will also be the camelCase variant, if you need to translate that to hypenated style use: + // + // str.replace(/([A-Z])/g, function(str,m1){ return '-' + m1.toLowerCase(); }).replace(/^ms-/,'-ms-'); + + // If you're trying to ascertain which transition end event to bind to, you might do something like... + // + // var transEndEventNames = { + // 'WebkitTransition' : 'webkitTransitionEnd', + // 'MozTransition' : 'transitionend', + // 'OTransition' : 'oTransitionEnd', + // 'msTransition' : 'msTransitionEnd', // maybe? + // 'transition' : 'transitionEnd' + // }, + // transEndEventName = transEndEventNames[ Modernizr.prefixed('transition') ]; + + Modernizr.prefixed = function(prop){ + return testPropsAll(prop, 'pfx'); + }; + + + + // Remove "no-js" class from element, if it exists: + docElement.className = docElement.className.replace(/\bno-js\b/, '') + + // Add the new classes to the element. + + (enableClasses ? ' js ' + classes.join(' ') : ''); + + return Modernizr; + +})(this, this.document); diff --git a/htdocs/js/tabber.js b/htdocs/js/tabber.js index df2d8f71a6..d6b8148c3d 100644 --- a/htdocs/js/tabber.js +++ b/htdocs/js/tabber.js @@ -395,6 +395,9 @@ tabberObj.prototype.tabShow = function(tabberIndex) /* Get the div that holds this tab */ div = this.tabs[tabberIndex].div; + + var radio = div.getElementsByTagName("input")[0]; + radio.checked = true; /* Remove classTabHide from the div */ div.className = div.className.replace(this.REclassTabHide, ''); @@ -476,6 +479,15 @@ function tabberAutomatic(tabberArgs) return this; } +/* Makes all hidden radio buttons selected -- not in the original tabber.js */ + +function radioSelect(){ + var title = this.getAttribute("title"); + var id = title.toLowerCase() + "_id"; + var radio = document.getElementById(id); + radio.setAttribute("checked", "checked"); +} + /*==================================================*/ @@ -494,12 +506,10 @@ function tabberAutomaticOnLoad(tabberArgs) oldOnLoad = window.onload; if (typeof window.onload != 'function') { window.onload = function() { - if (typeof(initializeWWquestion) == 'function') {initializeWWquestion();} tabberAutomatic(tabberArgs); }; } else { window.onload = function() { - if (typeof(initializeWWquestion) == 'function') {initializeWWquestion();} oldOnLoad(); tabberAutomatic(tabberArgs); }; @@ -523,3 +533,12 @@ if (typeof tabberOptions == 'undefined') { } } + +/* Makes all hidden radio buttons selected -- not in the original tabber.js */ + +function radioSelect(){ + var title = this.getAttribute("title"); + var id = title.toLowerCase() + "_id"; + var radio = document.getElementById(id); + radio.setAttribute("checked", "checked"); +} diff --git a/htdocs/js/ui.tabs.closable.min.js b/htdocs/js/ui.tabs.closable.min.js new file mode 100644 index 0000000000..ea6ec51d4b --- /dev/null +++ b/htdocs/js/ui.tabs.closable.min.js @@ -0,0 +1,10 @@ +/*! + * Copyright (c) 2010 Andrew Watts + * + * Dual licensed under the MIT (MIT_LICENSE.txt) + * and GPL (GPL_LICENSE.txt) licenses + * + * http://github.com/andrewwatts/ui.tabs.closable + */ +(function(){var c=$.ui.tabs.prototype._tabify;$.extend($.ui.tabs.prototype,{_tabify:function(){var a=this;c.apply(this,arguments);a.options.closable===true&&this.lis.filter(function(){return $("span.ui-icon-circle-close",this).length===0}).each(function(){$(this).append('').find("a:last").hover(function(){$(this).css("cursor","pointer")},function(){$(this).css("cursor","default")}).click(function(){var b=a.lis.index($(this).parent()); +if(b>-1){if(false===a._trigger("closableClick",null,a._ui($(a.lis[b]).find("a")[0],a.panels[b])))return;a.remove(b)}return false}).end()})}})})(jQuery); \ No newline at end of file diff --git a/htdocs/js/ww_applet_support.js b/htdocs/js/ww_applet_support.js index 6250119ffe..45147e4db3 100644 --- a/htdocs/js/ww_applet_support.js +++ b/htdocs/js/ww_applet_support.js @@ -711,4 +711,14 @@ ww_applet.prototype.safe_applet_initialize = function(i) { function iamhere() { alert( "javaScript loaded. functions still work"); -} \ No newline at end of file +} + +//Initialize the WWquestion. + +function initWW(){ + if (typeof(initializeWWquestion) == 'function') { + initializeWWquestion(); + } +} + +addOnLoadEvent(initWW); \ No newline at end of file diff --git a/lib/WeBWorK/Authen/Shibboleth.pm b/lib/WeBWorK/Authen/Shibboleth.pm new file mode 100644 index 0000000000..a579c7fbcb --- /dev/null +++ b/lib/WeBWorK/Authen/Shibboleth.pm @@ -0,0 +1,200 @@ +################################################################################ +# WeBWorK Online Homework Delivery System +# Copyright © 2000-2007 The WeBWorK Project, http://openwebwork.sf.net/ +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of either: (a) the GNU General Public License as published by the +# Free Software Foundation; either version 2, or (at your option) any later +# version, or (b) the "Artistic License" which comes with this package. +# +# This program 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 either the GNU General Public License or the +# Artistic License for more details. +################################################################################ + +package WeBWorK::Authen::Shibboleth; +use base qw/WeBWorK::Authen/; + +=head1 NAME + +WeBWorK::Authen::Shibboleth - Authentication plug in for Shibboleth. +This is basd on Cosign.pm + +to use: include in global.conf or course.conf + $authen{user_module} = "WeBWorK::Authen::Shibboleth"; +and add /webwork2/courseName as a Shibboleth Protected +Location + +if $r->ce->{shiboff} is set for a course, authentication reverts +to standard WeBWorK authentication. + +add the following to global.conf to setup the Shibboleth + +$shibboleth{logout_script} = "/Shibboleth.sso/Logout"?return=".$server_root_url.$webwork_url; # return URL after logout +$shibboleth{session_header} = "Shib-Session-ID"; # the header to identify if there is an existing shibboleth session +$shibboleth{manage_session_timeout} = 1; # allow shib to manage session time instead of webwork +$shibboleth{hash_user_id_method} = "MD5"; # possible values none, MD5. Use it when you want to hide real user_ids from showing in url. +$shibboleth{hash_user_id_salt} = ""; # salt for hash function +#define mapping between shib and webwork +$shibboleth{mapping}{user_id} = "username"; + +=cut + +use strict; +use warnings; +use WeBWorK::Debug; +use Data::Dumper; + +# this is similar to the method in the base class, except that Shibboleth +# ensures that we don't get to the address without a login. this means +# that we can't allow guest logins, but don't have to do any password +# checking or cookie management. + +sub get_credentials { + my ($self) = @_; + my $r = $self->{r}; + my $ce = $r->ce; + my $db = $r->db; + + if ( $ce->{shiboff} || $r->param('bypassShib')) { + return $self->SUPER::get_credentials( @_ ); + } else { + debug("Shib is on!"); + + # set external auth parameter so that Login.pm knows + # not to rely on internal logins if there's a check_user + # failure. + $self->{external_auth} = 1; + + if ( $r->param("user") && ! $r->param("force_passwd_authen") ) { + return $self->SUPER::get_credentials( @_ ); + } + + if ( defined ($ENV{$ce->{shibboleth}{session_header}}) && defined( $ENV{$ce->{shibboleth}{mapping}{user_id}} ) ) { + debug('Got shib header and user_id'); + my $user_id = $ENV{$ce->{shibboleth}{mapping}{user_id}}; + if ( defined ($ce->{shibboleth}{hash_user_id_method}) && + $ce->{shibboleth}{hash_user_id_method} ne "none" && + $ce->{shibboleth}{hash_user_id_method} ne "" ) { + use Digest; + my $digest = Digest->new($ce->{shibboleth}{hash_user_id_method}); + $digest->add(uc($user_id). ( defined $ce->{shibboleth}{hash_user_id_salt} ? $ce->{shibboleth}{hash_user_id_salt} : "")); + $user_id = $digest->hexdigest; + } + $self->{'user_id'} = $user_id; + $self->{r}->param("user", $user_id); + + # set external auth parameter so that login.pm + # knows not to rely on internal logins if + # there's a check_user failure. + $self->{session_key} = undef; + $self->{password} = "youwouldneverpickthispassword"; + $self->{login_type} = "normal"; + $self->{credential_source} = "params"; + } else { + debug("Couldn't shib header or user_id"); + return 0; + } + + # the session key isn't used (Shibboleth is managing this + # for us), and we want to force checking against the + # site_checkPassword + $self->{'session_key'} = undef; + $self->{'password'} = 1; + $self->{'credential_source'} = "params"; + + return 1; + } +} + +sub site_checkPassword { + my ( $self, $userID, $clearTextPassword ) = @_; + + if ( $self->{r}->ce->{shiboff} ) { + return $self->SUPER::checkPassword( @_ ); + } else { + # this is easy; if we're here at all, we've authenticated + # through shib + return 1; + } +} + +# disable cookie functionality +sub maybe_send_cookie { + my ($self, @args) = @_; + if ( $self->{r}->ce->{shiboff} ) { + return $self->SUPER::maybe_send_cookie( @_ ); + } else { + # nothing to do here + } +} +sub fetchCookie { + my ($self, @args) = @_; + if ( $self->{r}->ce->{shiboff} ) { + return $self->SUPER::fetchCookie( @_ ); + } else { + # nothing to do here + } +} +sub sendCookie { + my ($self, @args) = @_; + if ( $self->{r}->ce->{shiboff} ) { + return $self->SUPER::sendCookie( @_ ); + } else { + # nothing to do here + } +} +sub killCookie { + my ($self, @args) = @_; + if ( $self->{r}->ce->{shiboff} ) { + return $self->SUPER::killCookie( @_ ); + } else { + # nothing to do here + } +} + +# this is a bit of a cheat, because it does the redirect away from the +# logout script or what have you, but I don't see a way around that. +sub forget_verification { + my ($self, @args) = @_; + my $r = $self->{r}; + + if ( $r->ce->{shiboff} ) { + return $self->SUPER::forget_verification( @_ ); + } else { + $self->{was_verified} = 0; + $self->{redirect} = $r->ce->{shibboleth}{logout_script}; + } +} + +# returns ($sessionExists, $keyMatches, $timestampValid) +# if $updateTimestamp is true, the timestamp on a valid session is updated +# override function: allow shib to handle the session time out +sub check_session { + my ($self, $userID, $possibleKey, $updateTimestamp) = @_; + my $ce = $self->{r}->ce; + my $db = $self->{r}->db; + + if ( $ce->{shiboff} ) { + return $self->SUPER::check_session( @_ ); + } else { + my $Key = $db->getKey($userID); # checked + return 0 unless defined $Key; + + my $keyMatches = (defined $possibleKey and $possibleKey eq $Key->key); + my $timestampValid = (time <= $Key->timestamp()+$ce->{sessionKeyTimeout}); + if ($ce->{shibboleth}{manage_session_timeout}) { + # always valid to allow shib to take control of timeout + $timestampValid = 1; + } + + if ($keyMatches and $timestampValid and $updateTimestamp) { + $Key->timestamp(time); + $db->putKey($Key); + } + return (1, $keyMatches, $timestampValid); + } +} + +1; diff --git a/lib/WeBWorK/Constants.pm b/lib/WeBWorK/Constants.pm index 032180b94d..31a64e2d31 100644 --- a/lib/WeBWorK/Constants.pm +++ b/lib/WeBWorK/Constants.pm @@ -134,7 +134,7 @@ $WeBWorK::ContentGenerator::Instructor::Config::ConfigValues = [ type => 'text'}, { var => 'defaultTheme', doc => 'Theme (refresh page after saving changes to reveal new theme.)', - doc2 => 'There are currently five themes (or skins) to choose from: ur, math, math2, and dgage. The theme + doc2 => 'There are currently five themes (or skins) to choose from: ur, math, math2, math3 union. The theme specifies a unified look and feel for the WeBWorK course web pages.', values => [qw(math math2 math3 ur dgage union)], type => 'popuplist'}, @@ -239,13 +239,7 @@ You must use at least one display mode. If you select only one, then the option if student input is sin(pi/2). If this is set to false, e.g. to save space in the response area, the student can still see their evaluated answer by hovering the mouse pointer over the typeset version of their answer.', type => 'boolean'}, - - { var => 'pg{options}{showEvaluatedAnswers}', - doc => 'Display the evaluated student answer', - doc2 => 'Set to true to display the "Entered" column which automatically shows the evaluated student answer, e.g. 1 - if student input is sin(pi/2). If this is set to false, e.g. to save space in the response area, the student can - still see their evaluated answer by hovering the mouse pointer over the typeset version of their answer.', - type => 'boolean'}, + { var => 'pg{ansEvalDefaults}{useBaseTenLog}', doc => 'Use log base 10 instead of base e', diff --git a/lib/WeBWorK/ContentGenerator.pm b/lib/WeBWorK/ContentGenerator.pm index 13ccbb135d..310c6efc0c 100644 --- a/lib/WeBWorK/ContentGenerator.pm +++ b/lib/WeBWorK/ContentGenerator.pm @@ -1,6 +1,6 @@ ################################################################################ # WeBWorK Online Homework Delivery System -# Copyright © 2000-2007 The WeBWorK Project, http://openwebwork.sf.net/ +# Copyright © 2000-2007 The WeBWorK Project, http://openwebwork.sf.net/ # $CVSHeader: webwork2/lib/WeBWorK/ContentGenerator.pm,v 1.196 2009/06/04 01:33:15 gage Exp $ # # This program is free software; you can redistribute it and/or modify it under @@ -707,9 +707,13 @@ sub links { print CGI::start_ul(); print CGI::li(&$makelink("${pfx}UserList", urlpath_args=>{%args}, systemlink_args=>\%systemlink_args)); + print CGI::li(&$makelink("${pfx}UserList2", urlpath_args=>{%args}, systemlink_args=>\%systemlink_args)); print CGI::start_li(); # Homework Set Editor print &$makelink("${pfx}ProblemSetList", urlpath_args=>{%args}, systemlink_args=>\%systemlink_args); + print "
"; + print &$makelink("${pfx}ProblemSetList2", urlpath_args=>{%args}, systemlink_args=>\%systemlink_args); + ## only show editor link for non-versioned sets if (defined $setID && $setID !~ /,v\d+$/ ) { print CGI::start_ul(); @@ -721,6 +725,11 @@ sub links { print CGI::li(&$makelink("${pfx}PGProblemEditor", text=>"$problemID", urlpath_args=>{%args,setID=>$setID,problemID=>$problemID}, systemlink_args=>\%systemlink_args, target=>"WW_Editor")); print CGI::end_ul(); } + if (defined $problemID) { + print CGI::start_ul(); + print CGI::li(&$makelink("${pfx}PGProblemEditor2", text=>"--$problemID", urlpath_args=>{%args,setID=>$setID,problemID=>$problemID}, systemlink_args=>\%systemlink_args, target=>"WW_Editor2")); + print CGI::end_ul(); + } print CGI::end_li(); # end $setID print CGI::end_ul(); @@ -729,6 +738,7 @@ sub links { print CGI::li(&$makelink("${pfx}SetMaker", text=>$r->maketext("Library Browser"), urlpath_args=>{%args}, systemlink_args=>\%systemlink_args)); print CGI::li(&$makelink("${pfx}SetMaker2", text=>$r->maketext("Library Browser 2"), urlpath_args=>{%args}, systemlink_args=>\%systemlink_args)); + print CGI::li(&$makelink("${pfx}SetMaker3", text=>$r->maketext("Library Browser 3"), urlpath_args=>{%args}, systemlink_args=>\%systemlink_args)); print CGI::start_li(); # Stats print &$makelink("${pfx}Stats", urlpath_args=>{%args}, systemlink_args=>\%systemlink_args); if ($userID ne $eUserID or defined $setID) { @@ -1226,8 +1236,10 @@ handled. sub if_warnings { my ($self, $arg) = @_; my $r = $self->r; - - if (MP2 ? $r->notes->get("warnings") : $r->notes("warnings")) { + + if ( (MP2 ? $r->notes->get("warnings") : $r->notes("warnings")) + or ($self->{pgerrors}) ) + { return $arg; } else { !$arg; @@ -1605,7 +1617,9 @@ sub hidden_fields { # $html .= CGI::hidden($param, @values); #MEG # warn "$param ", join(" ", @values) if @values >1; #this should never happen!!! my $value = $r->param($param); - $html .= CGI::hidden($param, $value); # (can't name these items when using real CGI) +# $html .= CGI::hidden($param, $value); # (can't name these items when using real CGI) + $html .= CGI::hidden(-name=>$param, -default=>$value, -id=>"hidden_".$param); # (can't name these items when using real CGI) + } return $html; } diff --git a/lib/WeBWorK/ContentGenerator/CourseAdmin.pm b/lib/WeBWorK/ContentGenerator/CourseAdmin.pm index 0946238b57..44173e4715 100644 --- a/lib/WeBWorK/ContentGenerator/CourseAdmin.pm +++ b/lib/WeBWorK/ContentGenerator/CourseAdmin.pm @@ -40,6 +40,9 @@ use WeBWorK::Utils::CourseIntegrityCheck; #use WeBWorK::Utils::DBImportExport qw(dbExport dbImport); # needed for location management use Net::IP; +use File::Path 'remove_tree'; +use File::stat; +use Time::localtime; use constant IMPORT_EXPORT_WARNING => "The ability to import and export databases is still under development. It seems to work but it is VERY @@ -159,6 +162,9 @@ sub pre_header_initialize { } else { $method_to_call = "do_delete_course"; } + } + elsif (defined ($r->param("delete_course_refresh"))) { + $method_to_call = "delete_course_form"; } else { # form only $method_to_call = "delete_course_form"; @@ -230,6 +236,9 @@ sub pre_header_initialize { } else { $method_to_call = "archive_course_confirm"; # upgrade and recheck tables & directories. } + } + elsif (defined ($r->param("archive_course_refresh"))) { + $method_to_call = "archive_course_form"; } else { # form only $method_to_call = "archive_course_form"; @@ -292,6 +301,31 @@ sub pre_header_initialize { $method_to_call = "manage_location_form"; } } + elsif ($subDisplay eq "hide_inactive_course") { +# warn "subDisplay is $subDisplay"; + if (defined ($r->param("hide_course"))) { + @errors = $self->hide_course_validate; + if (@errors) { + $method_to_call = "hide_inactive_course_form"; + } else { + $method_to_call = "do_hide_inactive_course"; + } + } + elsif (defined ($r->param("unhide_course"))) { + @errors = $self->unhide_course_validate; + if (@errors) { + $method_to_call = "hide_inactive_course_form"; + } else { + $method_to_call = "do_unhide_inactive_course"; + } + } + elsif (defined ($r->param("hide_course_refresh"))) { + $method_to_call = "hide_inactive_course_form"; + } + else{ + $method_to_call = "hide_inactive_course_form"; + } + } elsif ($subDisplay eq "registration") { if (defined ($r->param("register_site"))) { $method_to_call = "do_registration"; @@ -394,6 +428,8 @@ sub body { CGI::a({href=>$self->systemLink($urlpath, params=>{subDisplay=>"upgrade_course"})}, "Upgrade courses"), "|", CGI::a({href=>$self->systemLink($urlpath, params=>{subDisplay=>"manage_locations"})}, "Manage Locations"), + "|", + CGI::a({href=>$self->systemLink($urlpath, params=>{subDisplay=>"hide_inactive_course"})}, "Hide Inactive courses"), CGI::hr(), $methodMessage, @@ -890,11 +926,12 @@ sub rename_course_form { my %courseLabels; # records... heh. foreach my $courseID (@courseIDs) { - my $tempCE = new WeBWorK::CourseEnvironment({ - %WeBWorK::SeedCE, - courseName => $courseID, - }); - $courseLabels{$courseID} = "$courseID (" . $tempCE->{dbLayoutName} . ")"; +# my $tempCE = new WeBWorK::CourseEnvironment({ +# %WeBWorK::SeedCE, +# courseName => $courseID, +# }); +# $courseLabels{$courseID} = "$courseID (" . $tempCE->{dbLayoutName} . ")"; + $courseLabels{$courseID} = "$courseID"; } print CGI::h2("Rename Course"); @@ -1167,6 +1204,12 @@ sub do_rename_course { print CGI::div({class=>"ResultsWithoutError"}, CGI::p("Successfully renamed the course $rename_oldCourseID to $rename_newCourseID"), ); + writeLog($ce, "hosted_courses", join("\t", + "\tRenamed", + "", + "", + "$rename_oldCourseID to $rename_newCourseID", + )); my $newCoursePath = $urlpath->newFromModule("WeBWorK::ContentGenerator::ProblemSets", $r, courseID => $rename_newCourseID); my $newCourseURL = $self->systemLink($newCoursePath, authen => 0); @@ -1178,6 +1221,9 @@ sub do_rename_course { ################################################################################ +my %coursesData; +sub byLoginActivity {$coursesData{$a}{'epoch_modify_time'} <=> $coursesData{$b}{'epoch_modify_time'}} + sub delete_course_form { my ($self) = @_; my $r = $self->r; @@ -1188,26 +1234,81 @@ sub delete_course_form { my $delete_courseID = $r->param("delete_courseID") || ""; + my $coursesDir = $ce->{webworkDirs}->{courses}; my @courseIDs = listCourses($ce); - @courseIDs = sort {lc($a) cmp lc ($b) } @courseIDs; #make sort case insensitive - - my %courseLabels; # records... heh. +# @courseIDs = sort {lc($a) cmp lc ($b) } @courseIDs; #make sort case insensitive + my $delete_listing_format = $r->param("delete_listing_format"); + unless (defined $delete_listing_format) {$delete_listing_format = 'alphabetically';} #use the default + +# my %courseLabels; # records... heh. +# foreach my $courseID (@courseIDs) { +# my $tempCE = new WeBWorK::CourseEnvironment({ +# %WeBWorK::SeedCE, +# courseName => $courseID, +# }); +# $courseLabels{$courseID} = "$courseID (" . $tempCE->{dbLayoutName} . ")"; +# } + # Get and store last modify time for login.log for all courses. Also get visibility status. + my %courseLabels; + my @noLoginLogIDs = (); + my @loginLogIDs = (); + + my ($loginLogFile, $epoch_modify_time, $courseDir); foreach my $courseID (@courseIDs) { - my $tempCE = new WeBWorK::CourseEnvironment({ - %WeBWorK::SeedCE, - courseName => $courseID, - }); - $courseLabels{$courseID} = "$courseID (" . $tempCE->{dbLayoutName} . ")"; + $loginLogFile = "$coursesDir/$courseID/logs/login.log"; + if (-e $loginLogFile) { #this should always exist except for the model course + $epoch_modify_time = stat($loginLogFile)->mtime; + $coursesData{$courseID}{'epoch_modify_time'} = $epoch_modify_time; + $coursesData{$courseID}{'local_modify_time'} = ctime($epoch_modify_time); + push(@loginLogIDs,$courseID); + } else { + $coursesData{$courseID}{'local_modify_time'} = 'no login.log'; #this should never be the case except for the model course + push(@noLoginLogIDs,$courseID); + } + if (-f "$coursesDir/$courseID/hide_directory") { + $coursesData{$courseID}{'status'} = 'hidden'; + } else { + $coursesData{$courseID}{'status'} = 'visible'; + } + $courseLabels{$courseID} = "$courseID ($coursesData{$courseID}{'status'} :: $coursesData{$courseID}{'local_modify_time'}) "; } - + if ($delete_listing_format eq 'last_login') { + @noLoginLogIDs = sort {lc($a) cmp lc ($b) } @noLoginLogIDs; #this should be an empty arrey except for the model course + @loginLogIDs = sort byLoginActivity @loginLogIDs; # oldest first + @courseIDs = (@noLoginLogIDs,@loginLogIDs); + } else { # in this case we sort alphabetically + @courseIDs = sort {lc($a) cmp lc ($b) } @courseIDs; + } + print CGI::h2("Delete Course"); print CGI::start_form(-method=>"POST", -action=>$r->uri); + my %list_labels = ( + alphabetically => 'alphabetically', + last_login => 'by last login date', + ); + print CGI::p( + 'Courses are listed either alphabetically or in order by the time of most recent login activity, oldest first. + To change the listing order check the mode you want and click "Refresh Listing". The listing format is: Course_Name + (status :: date/time of most recent login) where status is "hidden" or "visible".'); + + print CGI::table( + CGI::Tr({}, + CGI::p("Select a listing format:"), + CGI::radio_group(-name=>'delete_listing_format', + -values=>['alphabetically', 'last_login'], + -default=>'alphabetically', + -labels=>\%list_labels, + ), + ), + ); + print CGI::p({style=>"text-align: center"}, CGI::submit(-name=>"delete_course_refresh", -value=>"Refresh Listing"), + CGI::submit(-name=>"delete_course", -value=>"Delete Course")); print $self->hidden_authen_fields; print $self->hidden_fields("subDisplay"); + print CGI::p("Select a course to delete."); - print CGI::table({class=>"FormLayout"}, CGI::Tr({}, CGI::th({class=>"LeftHeader"}, "Course Name:"), @@ -1216,7 +1317,7 @@ sub delete_course_form { -name => "delete_courseID", -values => \@courseIDs, -default => $delete_courseID, - -size => 10, + -size => 15, -multiple => 0, -labels => \%courseLabels, ), @@ -1224,7 +1325,8 @@ sub delete_course_form { ), ); - print CGI::p({style=>"text-align: center"}, CGI::submit(-name=>"delete_course", -value=>"Delete Course")); + print CGI::p({style=>"text-align: center"}, CGI::submit(-name=>"delete_course_refresh", -value=>"Refresh Listing"), + CGI::submit(-name=>"delete_course", -value=>"Delete Course")); print CGI::end_form(); } @@ -1370,33 +1472,88 @@ sub archive_course_form { my $archive_courseID = $r->param("archive_courseID") || ""; + my $coursesDir = $ce->{webworkDirs}->{courses}; my @courseIDs = listCourses($ce); - @courseIDs = sort {lc($a) cmp lc ($b) } @courseIDs; #make sort case insensitive - - my %courseLabels; # records... heh. +# @courseIDs = sort {lc($a) cmp lc ($b) } @courseIDs; #make sort case insensitive + my $archive_listing_format = $r->param("archive_listing_format"); + unless (defined $archive_listing_format) {$archive_listing_format = 'alphabetically';} #use the default + +# my %courseLabels; # records... heh. +# foreach my $courseID (@courseIDs) { +# my $tempCE = new WeBWorK::CourseEnvironment({ +# %WeBWorK::SeedCE, +# courseName => $courseID, +# }); +# $courseLabels{$courseID} = "$courseID (" . $tempCE->{dbLayoutName} . ")"; +# } + # Get and store last modify time for login.log for all courses. Also get visibility status. + my %courseLabels; + my @noLoginLogIDs = (); + my @loginLogIDs = (); + + my ($loginLogFile, $epoch_modify_time, $courseDir); foreach my $courseID (@courseIDs) { - my $tempCE = new WeBWorK::CourseEnvironment({ - %WeBWorK::SeedCE, - courseName => $courseID, - }); - $courseLabels{$courseID} = "$courseID (" . $tempCE->{dbLayoutName} . ")"; + $loginLogFile = "$coursesDir/$courseID/logs/login.log"; + if (-e $loginLogFile) { #this should always exist except for the model course + $epoch_modify_time = stat($loginLogFile)->mtime; + $coursesData{$courseID}{'epoch_modify_time'} = $epoch_modify_time; + $coursesData{$courseID}{'local_modify_time'} = ctime($epoch_modify_time); + push(@loginLogIDs,$courseID); + } else { + $coursesData{$courseID}{'local_modify_time'} = 'no login.log'; #this should never be the case except for the model course + push(@noLoginLogIDs,$courseID); + } + if (-f "$coursesDir/$courseID/hide_directory") { + $coursesData{$courseID}{'status'} = 'hidden'; + } else { + $coursesData{$courseID}{'status'} = 'visible'; + } + $courseLabels{$courseID} = "$courseID ($coursesData{$courseID}{'status'} :: $coursesData{$courseID}{'local_modify_time'}) "; + } + if ($archive_listing_format eq 'last_login') { + @noLoginLogIDs = sort {lc($a) cmp lc ($b) } @noLoginLogIDs; #this should be an empty arrey except for the model course + @loginLogIDs = sort byLoginActivity @loginLogIDs; # oldest first + @courseIDs = (@noLoginLogIDs,@loginLogIDs); + } else { # in this case we sort alphabetically + @courseIDs = sort {lc($a) cmp lc ($b) } @courseIDs; } print CGI::h2("archive Course"); print CGI::p( - "Creates a gzipped tar archive (.tar.gz) of a course in the WeBWorK + 'Creates a gzipped tar archive (.tar.gz) of a course in the WeBWorK courses directory. Before archiving, the course database is dumped into - a subdirectory of the course's DATA directory. Currently the archive + a subdirectory of the course\'s DATA directory. Currently the archive facility is only available for mysql databases. It depends on the - mysqldump application." + mysqldump application.' ); - + print CGI::p( + 'Courses are listed either alphabetically or in order by the time of most recent login activity, oldest first. + To change the listing order check the mode you want and click "Refresh Listing". The listing format is: Course_Name + (status :: date/time of most recent login) where status is "hidden" or "visible".'); + print CGI::start_form(-method=>"POST", -action=>$r->uri); + my %list_labels = ( + alphabetically => 'alphabetically', + last_login => 'by last login date', + ); + + print CGI::table( + CGI::Tr({}, + CGI::p("Select a listing format:"), + CGI::radio_group(-name=>'archive_listing_format', + -values=>['alphabetically', 'last_login'], + -default=>'alphabetically', + -labels=>\%list_labels, + ), + ), + ); + print CGI::p({style=>"text-align: center"}, CGI::submit(-name=>"archive_course_refresh", -value=>"Refresh Listing"), + CGI::submit(-name=>"archive_course", -value=>"Archive Courses")); print $self->hidden_authen_fields; print $self->hidden_fields("subDisplay"); - print CGI::p("Select a course to archive."); + print CGI::p("Select course(s) to archive."); print CGI::table({class=>"FormLayout"}, CGI::Tr({}, @@ -1406,7 +1563,7 @@ sub archive_course_form { -name => "archive_courseIDs", -values => \@courseIDs, -default => $archive_courseID, - -size => 10, + -size => 15, -multiple => 1, -labels => \%courseLabels, ), @@ -1426,7 +1583,8 @@ sub archive_course_form { ) ); - print CGI::p({style=>"text-align: center"}, CGI::submit(-name=>"archive_course", -value=>"archive Course")); + print CGI::p({style=>"text-align: center"}, CGI::submit(-name=>"archive_course_refresh", -value=>"Refresh Listing"), + CGI::submit(-name=>"archive_course", -value=>"Archive Courses")); print CGI::end_form(); } @@ -1673,6 +1831,15 @@ sub do_archive_course { courseName => $archive_courseID, }); + # Remove course specific temp files before archiving + my $courseTempDir = $ce2->{courseDirs}{html_temp}; + remove_tree("$courseTempDir"); + # Remove the original default tmp directory if it exists + my $orgDefaultCourseTempDir = "$ce2->{courseDirs}{html}/tmp"; + if (-d "$orgDefaultCourseTempDir") { + remove_tree("$orgDefaultCourseTempDir"); + } + # this is kinda left over from when we had 'gdbm' and 'sql' database layouts # below this line, we would grab values from getopt and put them in this hash # but for now the hash can remain empty @@ -2741,6 +2908,273 @@ sub edit_location_handler { } } +sub hide_inactive_course_form { + my ($self) = @_; + my $r = $self->r; + my $ce = $r->ce; + + my $coursesDir = $ce->{webworkDirs}->{courses}; + my @courseIDs = listCourses($ce); + my $hide_listing_format = $r->param("hide_listing_format"); + unless (defined $hide_listing_format) {$hide_listing_format = 'last_login';} #use the default +# warn "hide_listing_format is $hide_listing_format"; + + # Get and store last modify time for login.log for all courses. Also get visibility status. + my %courseLabels; + my @noLoginLogIDs = (); + my @loginLogIDs = (); + my @hideCourseIDs = (); + my ($loginLogFile, $epoch_modify_time, $courseDir); + foreach my $courseID (@courseIDs) { + $loginLogFile = "$coursesDir/$courseID/logs/login.log"; + if (-e $loginLogFile) { #this should always exist except for the model course + $epoch_modify_time = stat($loginLogFile)->mtime; + $coursesData{$courseID}{'epoch_modify_time'} = $epoch_modify_time; + $coursesData{$courseID}{'local_modify_time'} = ctime($epoch_modify_time); + push(@loginLogIDs,$courseID); + } else { + $coursesData{$courseID}{'local_modify_time'} = 'no login.log'; #this should never be the case except for the model course + push(@noLoginLogIDs,$courseID); + } + if (-f "$coursesDir/$courseID/hide_directory") { + $coursesData{$courseID}{'status'} = 'hidden'; + } else { + $coursesData{$courseID}{'status'} = 'visible'; + } + $courseLabels{$courseID} = "$courseID ($coursesData{$courseID}{'status'} :: $coursesData{$courseID}{'local_modify_time'}) "; + } + if ($hide_listing_format eq 'last_login') { + @noLoginLogIDs = sort {lc($a) cmp lc ($b) } @noLoginLogIDs; #this should be an empty arrey except for the model course + @loginLogIDs = sort byLoginActivity @loginLogIDs; # oldest first + @hideCourseIDs = (@noLoginLogIDs,@loginLogIDs); + } else { # in this case we sort alphabetically + @hideCourseIDs = sort {lc($a) cmp lc ($b) } @courseIDs; + } + + print CGI::h2("Hide Inactive courses"); + + print CGI::p( + 'Select the course(s) you want to hide (or unhide) and then click "Hide Courses" (or "Unhide Courses"). Hiding + a course that is already hidden does no harm (the action is skipped). Likewise unhiding a course that is + already visible does no harm (the action is skipped). Hidden courses are still active but are not listed in + the list of WeBWorK courses on the opening page. To access the course, an instructor or student must know the + full URL address for the course.' + ); + + print CGI::p( + 'Courses are listed either alphabetically or in order by the time of most recent login activity, oldest first. + To change the listing order check the mode you want and click "Refresh Listing". The listing format is: + Course_Name (status :: date/time of most recent login) where status is "hidden" or "visible".'); + + + print CGI::start_form(-method=>"POST", -action=>$r->uri); + + my %list_labels = ( + alphabetically => 'alphabetically', + last_login => 'by last login date', + ); + + print CGI::table( + CGI::Tr({}, + CGI::p("Select a listing format:"), + CGI::radio_group(-name=>'hide_listing_format', + -values=>['alphabetically', 'last_login'], + -default=>'last_login', + -labels=>\%list_labels, + ), + ), + ); + print CGI::p({style=>"text-align: center"}, CGI::submit(-name=>"hide_course_refresh", -value=>"Refresh Listing"), CGI::submit(-name=>"hide_course", -value=>"Hide Courses"), + CGI::submit(-name=>"unhide_course", -value=>"Unhide Courses")); + print $self->hidden_authen_fields; + print $self->hidden_fields("subDisplay"); + + print CGI::p("Select course(s) to hide or unhide."); + + print CGI::table({class=>"FormLayout"}, + CGI::Tr({}, + CGI::td( + CGI::scrolling_list( + -name => "hide_courseIDs", + -values => \@hideCourseIDs, + -size => 15, + -multiple => 1, + -labels => \%courseLabels, + ), + ), + + ), + ); + + print CGI::p({style=>"text-align: center"}, CGI::submit(-name=>"hide_course_refresh", -value=>"Refresh Listing"), CGI::submit(-name=>"hide_course", -value=>"Hide Courses"), + CGI::submit(-name=>"unhide_course", -value=>"Unhide Courses")); + + print CGI::end_form(); +} + +sub hide_course_validate { + my ($self) = @_; + my $r = $self->r; + my $ce = $r->ce; + #my $db = $r->db; + #my $authz = $r->authz; + my $urlpath = $r->urlpath; + + my @hide_courseIDs = $r->param("hide_courseIDs"); + @hide_courseIDs = () unless @hide_courseIDs; + + my @errors; + + unless (@hide_courseIDs) { + push @errors, "You must specify a course name."; + } + return @errors; +} + + +sub do_hide_inactive_course { + my ($self) = @_; + my $r = $self->r; + my $ce = $r->ce; + + my $coursesDir = $ce->{webworkDirs}->{courses}; + + my $hide_courseID; + my @hide_courseIDs = $r->param("hide_courseIDs"); + @hide_courseIDs = () unless @hide_courseIDs; + + my $hideDirFileContent = 'Place a file named "hide_directory" in a course or other directory +and it will not show up in the courses list on the WeBWorK home page. +It will still appear in the Course Administration listing.'; + my @succeeded_courses = (); + my $succeeded_count = 0; + my @failed_courses = (); + my $already_hidden_count = 0; + + foreach $hide_courseID (@hide_courseIDs) { + my $hideDirFile = "$coursesDir/$hide_courseID/hide_directory"; + if (-f $hideDirFile) { + $already_hidden_count++; + next; + } else { + local *HIDEFILE; + if (open (HIDEFILE, ">", $hideDirFile)) { + print HIDEFILE "$hideDirFileContent"; + close HIDEFILE; + push @succeeded_courses,$hide_courseID; + $succeeded_count++; + } else { + push @failed_courses,$hide_courseID; + } + } + } + + if (@failed_courses) { + print CGI::div({class=>"ResultsWithError"}, + CGI::p("Errors occured while hiding the courses listed below when attempting to create the file hide_directory in the course's directory. +Check the ownership and permissions of the course's directory, e.g $coursesDir/$failed_courses[0]/"), + CGI::p("@failed_courses"), + ); + } + my $succeeded_message = ''; + + if ($succeeded_count < 1 and $already_hidden_count > 0) { + $succeeded_message = "Except for possible errors listed above, all selected courses are already hidden."; + } + + if ($succeeded_count) { + if ($succeeded_count < 6) { + $succeeded_message = "The following courses were successfully hidden: @succeeded_courses"; + } else { + $succeeded_message = "$succeeded_count courses were successfully hidden."; + } + } + if ($succeeded_count or $already_hidden_count) { + print CGI::div({class=>"ResultsWithoutError"}, + CGI::p("$succeeded_message"), + ); + } +} + +sub unhide_course_validate { + my ($self) = @_; + my $r = $self->r; + my $ce = $r->ce; + #my $db = $r->db; + #my $authz = $r->authz; + my $urlpath = $r->urlpath; + + my @unhide_courseIDs = $r->param("hide_courseIDs"); + @unhide_courseIDs = () unless @unhide_courseIDs; + + my @errors; + + unless (@unhide_courseIDs) { + push @errors, "You must specify a course name."; + } + return @errors; +} + + +sub do_unhide_inactive_course { + my ($self) = @_; + my $r = $self->r; + my $ce = $r->ce; + + my $coursesDir = $ce->{webworkDirs}->{courses}; + + my $unhide_courseID; + my @unhide_courseIDs = $r->param("hide_courseIDs"); + @unhide_courseIDs = () unless @unhide_courseIDs; + + my @succeeded_courses = (); + my $succeeded_count = 0; + my @failed_courses = (); + my $already_visible_count = 0; + + foreach $unhide_courseID (@unhide_courseIDs) { + my $hideDirFile = "$coursesDir/$unhide_courseID/hide_directory"; + unless (-f $hideDirFile) { + $already_visible_count++; + next; + } + remove_tree("$hideDirFile", {error => \my $err}); + if (@$err) { + push @failed_courses,$unhide_courseID; + } else { + push @succeeded_courses,$unhide_courseID; + $succeeded_count++; + } + } + my $succeeded_message = ''; + + if ($succeeded_count < 1 and $already_visible_count > 0) { + $succeeded_message = "Except for possible errors listed above, all selected courses are already unhidden."; + } + + if ($succeeded_count) { + if ($succeeded_count < 6) { + $succeeded_message = "The following courses were successfully unhidden: @succeeded_courses"; + } else { + $succeeded_message = "$succeeded_count courses were successfully unhidden."; + } + } + if ($succeeded_count or $already_visible_count) { + print CGI::div({class=>"ResultsWithoutError"}, + CGI::p("$succeeded_message"), + ); + } + if (@failed_courses) { + print CGI::div({class=>"ResultsWithError"}, + CGI::p("Errors occured while unhiding the courses listed below when attempting delete the file hide_directory in the course's directory. +Check the ownership and permissions of the course's directory, e.g $coursesDir/$failed_courses[0]/"), + CGI::p("@failed_courses"), + ); + } +} + + + ################################################################################ # registration forms added by Mike Gage 5-5-2008 ################################################################################ @@ -2777,9 +3211,6 @@ sub display_registration_form { !; - - - } sub registration_form { diff --git a/lib/WeBWorK/ContentGenerator/Hardcopy.pm b/lib/WeBWorK/ContentGenerator/Hardcopy.pm index 6d55167526..f9257c89a2 100644 --- a/lib/WeBWorK/ContentGenerator/Hardcopy.pm +++ b/lib/WeBWorK/ContentGenerator/Hardcopy.pm @@ -62,6 +62,7 @@ our $HC_DEFAULT_FORMAT = "pdf"; # problems if this is not an allowed format for our %HC_FORMATS = ( tex => { name => "TeX Source", subr => "generate_hardcopy_tex" }, pdf => { name => "Adobe PDF", subr => "generate_hardcopy_pdf" }, + tikz =>{ name => "TIKZ PDF file", subr => "generate_hardcopy_tigz"}, ); # custom fields used in $self hash diff --git a/lib/WeBWorK/ContentGenerator/Instructor/Index.pm b/lib/WeBWorK/ContentGenerator/Instructor/Index.pm index 1a73b173ce..c0c410f6ed 100644 --- a/lib/WeBWorK/ContentGenerator/Instructor/Index.pm +++ b/lib/WeBWorK/ContentGenerator/Instructor/Index.pm @@ -100,7 +100,7 @@ sub pre_header_initialize { defined param $r "edit_users" and do { if ($nusers >= 1) { - $module = "${ipfx}::UserList"; + $module = "${ipfx}::UserList2"; $params{visible_users} = \@selectedUserIDs; $params{editMode} = 1; } else { diff --git a/lib/WeBWorK/ContentGenerator/Instructor/PGProblemEditor2.pm b/lib/WeBWorK/ContentGenerator/Instructor/PGProblemEditor2.pm new file mode 100644 index 0000000000..ff12a154c6 --- /dev/null +++ b/lib/WeBWorK/ContentGenerator/Instructor/PGProblemEditor2.pm @@ -0,0 +1,2035 @@ +################################################################################ +# WeBWorK Online Homework Delivery System +# Copyright © 2000-2007 The WeBWorK Project, http://openwebwork.sf.net/ +# $CVSHeader: webwork2/lib/WeBWorK/ContentGenerator/Instructor/PGProblemEditor.pm,v 1.99 2010/05/18 18:39:40 apizer Exp $ +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of either: (a) the GNU General Public License as published by the +# Free Software Foundation; either version 2, or (at your option) any later +# version, or (b) the "Artistic License" which comes with this package. +# +# This program 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 either the GNU General Public License or the +# Artistic License for more details. +################################################################################ + +package WeBWorK::ContentGenerator::Instructor::PGProblemEditor2; +use base qw(WeBWorK); +use base qw(WeBWorK::ContentGenerator::Instructor); +use base qw(WeBWorK::ContentGenerator::renderViaXMLRPC); + +use constant DEFAULT_SEED => 123456; + +=head1 NAME + +WeBWorK::ContentGenerator::Instructor::PGProblemEditor2 - Edit a pg file + +=cut + +use strict; +use warnings; +#use CGI qw(-nosticky ); +use WeBWorK::CGI; +use WeBWorK::Utils qw(readFile surePathToFile path_is_subdir); +use HTML::Entities; +use URI::Escape; +use WeBWorK::Utils qw(has_aux_files not_blank); +use File::Copy; +use WeBWorK::Utils::Tasks qw(fake_user fake_set renderProblems); +use Data::Dumper; +use Fcntl; + + + +########################################################### +# This editor will edit problem files or set header files or files, such as course_info +# whose name is defined in the global.conf database +# +# Only files under the template directory ( or linked to this location) can be edited. +# +# The course information and problems are located in the course templates directory. +# Course information has the name defined by courseFiles->{course_info} +# +# Only files under the template directory ( or linked to this location) can be edited. +# +# editMode = temporaryFile (view the temp file defined by course_info.txt.user_name.tmp +# instead of the file course_info.txt) +# this flag is read by Problem.pm and ProblemSet.pm, perhaps others +# The TEMPFILESUFFIX is "user_name.tmp" by default. It's definition should be moved to Instructor.pm #FIXME +########################################################### + +########################################################### +# The behavior of this module is essentially defined +# by the values of $file_type and the submit button which is placed in $action +############################################################# +# File types which can be edited +# +# file_type eq 'problem' +# this is the most common type -- this editor can be called by an instructor when viewing any problem. +# the information for retrieving the source file is found using the problemID in order to look +# look up the source file path. +# +# file_type eq 'source_path_for_problem_file' +# This is the same as the 'problem' file type except that the source for the problem is found in +# the parameter $r->param('sourceFilePath'). This path is relative to the templates directory +# +# file_type eq 'set_header' +# This is a special case of editing the problem. The set header is often listed as problem 0 in the set's list of problems. +# +# file_type eq 'hardcopy_header' +# This is a special case of editing the problem. The hardcopy_header is often listed as problem 0 in the set's list of problems. +# But it is used instead of set_header when producing a hardcopy of the problem set in the TeX format, instead of producing HTML +# formatted version for use on the computer screen. +# +# file_type eq 'course_info' +# This allows editing of the course_info.txt file which gives general information about the course. It is called from the +# ProblemSets.pm module. +# +# file_type eq 'options_info' +# This allows editing of the options_info.txt file which gives general information about the course. It is called from the +# Options.pm module. +# +# file_type eq 'blank_problem' +# This is a special call which allows one to create and edit a new PG problem. The "stationery" source for this problem is +# stored in the conf/snippets directory and defined in global.conf by $webworkFiles{screenSnippets}{blankProblem} +############################################################# +# Requested actions -- these and the file_type determine the state of the module +# Save ---- action = save +# Save as ---- action = save_as +# View Problem ---- action = view +# Add this problem to: ---- action = add_problem +# Make this set header for: ---- action = add_problem +# Revert ---- action = revert +# no submit button defined ---- action = fresh_edit +################################################### +# +# Determining which is the correct path to the file is a mess!!! FIXME +# The path to the file to be edited is eventually put in tempFilePath +# +# (tempFilePath)(editFilePath)(forcedSourceFile) +#input parameter is: sourceFilePath +################################################################# +# params read +# user +# effectiveUser +# submit +# file_type +# problemSeed +# displayMode +# edit_level +# make_local_copy +# sourceFilePath +# problemContents +# save_to_new_file +# + +use constant ACTION_FORMS => [qw(view add_problem save save_as revert)]; +#[qw(view save save_as revert add_problem add_header make_local_copy)]; + +# permissions needed to perform a given action +use constant FORM_PERMS => { + view => "modify_student_data", + add_problem => "modify_student_data", + make_local_copy => "modify_student_data", + save => "modify_student_data", + save_as => "modify_student_data", +# rename => "modify_student_data", + revert => "modify_student_data", +}; + +our $BLANKPROBLEM = 'blankProblem.pg'; +# use constant BLANKPROBLEM => 'blankProblem.pg'; # doesn't work because this constant needs to be used inside a match. +sub pre_header_initialize { + my ($self) = @_; + my $r = $self->r; + my $ce = $r->ce; + my $urlpath = $r->urlpath; + my $authz = $r->authz; + my $user = $r->param('user'); + $self->{courseID} = $urlpath->arg("courseID"); + $self->{setID} = $r->urlpath->arg("setID") ; # using $r->urlpath->arg("setID") ||'' causes trouble with set 0!!! + $self->{problemID} = $r->urlpath->arg("problemID"); + + # parse setID, which may come in with version data + + my $fullSetID = $self->{setID}; + if (defined($fullSetID) ) { + + if ( $fullSetID =~ /,v(\d+)$/ ) { + $self->{versionID} = $1; + $self->{setID} =~ s/,v\d+$//; + } + $self->{fullSetID} = $fullSetID; + } + my $submit_button = $r->param('submit'); # obtain submit command from form + my $actionID = $r->param('action'); + my $file_type = $r->param("file_type") || ''; + my $setName = $self->{setID}; + my $versionedSetName = $self->{fullSetID}; + my $problemNumber = $self->{problemID}; + + # Check permissions + return unless ($authz->hasPermissions($user, "access_instructor_tools")); + return unless ($authz->hasPermissions($user, "modify_problem_sets")); + + ############################################################################## + # displayMode and problemSeed + # + # Determine the display mode + # If $self->{problemSeed} was obtained within saveFileChanges from the problem_record + # then it can be overridden by the value obtained from the form. + # Insure that $self->{problemSeed} has some non-empty value + # displayMode and problemSeed + # will be needed for viewing the problem via redirect. + # They are also two of the parameters which can be set by the editor + ############################################################################## + + if (defined $r->param('displayMode')) { + $self->{displayMode} = $r->param('displayMode'); + } else { + $self->{displayMode} = $ce->{pg}->{options}->{displayMode}; + } + + # form version of problemSeed overrides version obtained from the the problem_record + # inside saveFileChanges + $self->{problemSeed} = $r->param('problemSeed') if (defined $r->param('problemSeed')); + # Make sure that the problem seed has some value + $self->{problemSeed} = DEFAULT_SEED() unless not_blank($self->{problemSeed}); + + ############################################################################## + ############################################################################# + # Save file to permanent or temporary file, then redirect for viewing + ############################################################################# + # + # Any file "saved as" should be assigned to "Undefined_Set" and redirectoed to be viewed again in the editor + # + # Problems "saved" or 'refreshed' are to be redirected to the Problem.pm module + # Set headers which are "saved" are to be redirected to the ProblemSet.pm page + # Hardcopy headers which are "saved" are also to be redirected to the ProblemSet.pm page + # Course_info files are redirected to the ProblemSets.pm page + # Options_info files are redirected to the Options.pm page + ############################################################################## + + + + ###################################### + # Insure that file_type is defined + ###################################### + # We have already read in the file_type parameter from the form + # + # If this has not been defined we are dealing with a set header + # or regular problem + if ( not_blank($file_type) ) { #file_type is defined and is not blank + # file type is already defined -- do nothing + #warn "file type already defined as $file_type" #FIXME debug + } else { + # if "sourceFilePath" is defined in the form, then we are getting the path directly. + # if the problem number is defined and is 0 + # then we are dealing with some kind of + # header file. The default is 'set_header' which prints properly + # to the screen. + # If the problem number is not zero, we are dealing with a real problem + ###################################### + if ( not_blank($r->param('sourceFilePath') ) ) { + $file_type ='source_path_for_problem_file'; + $file_type = 'set_header' if $r->param('sourceFilePath') =~ m!/headers/|Header\.pg$!; #FIXME this need to be cleaned up + } elsif ( defined($problemNumber) ) { + if ( $problemNumber =~/^\d+$/ and $problemNumber == 0 ) { # if problem number is numeric and zero + $file_type = 'set_header' unless $file_type eq 'set_header' + or $file_type eq 'hardcopy_header'; + } else { + $file_type = 'problem'; + #warn "setting file type to 'problem'\n"; #FIXME debug + } + + } + } + die "The file_type variable |$file_type| has not been defined or is blank." unless not_blank($file_type); + # clean up sourceFilePath, just in case + # double check that sourceFilePath is relative to the templates file + if ($file_type eq 'source_path_for_problem_file' ) { + my $templatesDirectory = $ce->{courseDirs}->{templates}; + my $sourceFilePath = $r->param('sourceFilePath'); + $sourceFilePath =~ s/$templatesDirectory//; + $sourceFilePath =~ s|^/||; # remove intial / + $self->{sourceFilePath} = $sourceFilePath; + } + $self->{file_type} = $file_type; + # $self->addgoodmessage("file type is $file_type"); #FIXME debug + # warn "file type is $file_type\n parameter is ".$self->r->param("file_type"); + ########################################## + # File type is one of: blank_problem course_info options_info problem set_header hardcopy_header source_path_for_problem_file + ########################################## + # + # Determine the path to the file + # + ########################################### + $self->getFilePaths($versionedSetName, $problemNumber, $file_type); + #defines $self->{editFilePath} # path to the permanent file to be edited + # $self->{tempFilePath} # path to the permanent file to be edited has .tmp suffix + # $self->{inputFilePath} # path to the file for input, (might be a .tmp file) + + + + ########################################## + # Default problem contents + ########################################## + $self->{r_problemContents}= undef; + + ########################################## + # + # Determine action + # + ########################################### + + if ($actionID) { + unless (grep { $_ eq $actionID } @{ ACTION_FORMS() } ) { + die "Action $actionID not found"; + } + # Check permissions + if (not FORM_PERMS()->{$actionID} or $authz->hasPermissions($user, FORM_PERMS()->{$actionID})) { + my $actionHandler = "${actionID}_handler"; + my %genericParams =(); +# foreach my $param (qw(selected_users)) { +# $genericParams{$param} = [ $r->param($param) ]; +# } + my %actionParams = $self->getActionParams($actionID); + my %tableParams = (); # $self->getTableParams(); + $self->{action}= $actionID; + $self->$actionHandler(\%genericParams, \%actionParams, \%tableParams); + } else { + $self->addbadmessage( "You are not authorized to perform this action."); + } + } else { + $self->{action}='fresh_edit'; + my $actionHandler = "fresh_edit_handler"; + my %genericParams; + my %actionParams = (); #$self->getActionParams($actionID); + my %tableParams = (); # $self->getTableParams(); + my $problemContents = ''; + $self->{r_problemContents}=\$problemContents; + $self->$actionHandler(\%genericParams, \%actionParams, \%tableParams); + } + + + ############################################################################## + # displayMode and problemSeed + # + # Determine the display mode + # If $self->{problemSeed} was obtained within saveFileChanges from the problem_record + # then it can be overridden by the value obtained from the form. + # Insure that $self->{problemSeed} has some non-empty value + # displayMode and problemSeed + # will be needed for viewing the problem via redirect. + # They are also two of the parameters which can be set by the editor + ############################################################################## + + if (defined $r->param('displayMode')) { + $self->{displayMode} = $r->param('displayMode'); + } else { + $self->{displayMode} = $ce->{pg}->{options}->{displayMode}; + } + + # form version of problemSeed overrides version obtained from the the problem_record + # inside saveFileChanges + $self->{problemSeed} = $r->param('problemSeed') if (defined $r->param('problemSeed')); + # Make sure that the problem seed has some value + $self->{problemSeed} = DEFAULT_SEED() unless not_blank( $self->{problemSeed}); + + ############################################################################## + # Return + # If file saving fails or + # if no redirects are required. No further processing takes place in this subroutine. + # Redirects are required only for the following submit values + # 'Save' + # 'Save as' + # 'Refresh' + # add problem to set + # add set header to set + # + ######################################### + + return if $self->{failure}; + # FIXME: even with an error we still open a new page because of the target specified in the form + + + # Some cases do not need a redirect: save, refresh, save_as, add_problem_to_set, add_header_to_set,make_local_copy + my $action = $self->{action}; + return ; + +} + + +sub initialize { + my ($self) = @_; + my $r = $self->r; + my $authz = $r->authz; + my $user = $r->param('user'); + + # Check permissions + return unless ($authz->hasPermissions($user, "access_instructor_tools")); + return unless ($authz->hasPermissions($user, "modify_problem_sets")); + + my $file_type = $r->param('file_type') || ""; + my $tempFilePath = $self->{tempFilePath}; # path to the file currently being worked with (might be a .tmp file) + my $inputFilePath = $self->{inputFilePath}; # path to the file for input, (might be a .tmp file) + + $self->addmessage($r->param('status_message') ||''); # record status messages carried over if this is a redirect + $self->addbadmessage("Changes in this file have not yet been permanently saved.") if -r $tempFilePath; + if ( not( -e $inputFilePath) ) { + $self->addbadmessage("The file '".$self->shortPath($inputFilePath)."' cannot be found."); + } elsif ((not -w $inputFilePath) && $file_type ne 'blank_problem' ) { + + $self->addbadmessage("The file '".$self->shortPath($inputFilePath)."' is protected! ".CGI::br(). + "To edit this text you must first make a copy of this file using the 'Save as' action below."); + + } + if ($inputFilePath =~/$BLANKPROBLEM$/ && $file_type ne 'blank_problem') { +# $self->addbadmessage("This file '$inputFilePath' is a blank problem! ".CGI::br()."To edit this text you must + $self->addbadmessage("The file '".$self->shortPath($inputFilePath)."' is a blank problem! ".CGI::br()."To edit this text you must + use the 'Save AS' action below to save it to another file."); + } + +} + +sub path { + my ($self, $args) = @_; + my $r = $self->r; + my $urlpath = $r->urlpath; + my $courseName = $urlpath->arg("courseID"); + my $setName = $r->urlpath->arg("setID") || ''; + my $problemNumber = $r->urlpath->arg("problemID") || ''; + + # we need to build a path to the problem being edited by hand, since it is not the same as the urlpath + # For this page the bread crum path leads back to the problem being edited, not to the Instructor tool. + my @path = ( 'WeBWork', $r->location, + "$courseName", $r->location."/$courseName", + "$setName", $r->location."/$courseName/$setName", + "$problemNumber", $r->location."/$courseName/$setName/$problemNumber", + "Editor", "" + ); + + #print "\n\n"; + print $self->pathMacro($args, @path); + #print "\n"; + + return ""; +} +sub title { + my $self = shift; + my $r = $self->r; + my $courseName = $r->urlpath->arg("courseID"); + my $setID = $r->urlpath->arg("setID"); + my $problemNumber = $r->urlpath->arg("problemID"); + my $file_type = $self->{'file_type'} || ''; + + return "Set Header for set $setID" if ($file_type eq 'set_header'); + return "Hardcopy Header for set $setID" if ($file_type eq 'hardcopy_header'); + return "Course Information for course $courseName" if ($file_type eq 'course_info'); + return "Options Information" if ($file_type eq 'options_info'); + + return 'Problem ' . $r->{urlpath}->name; +} + +sub body { + my ($self) = @_; + my $r = $self->r; + my $db = $r->db; + my $ce = $r->ce; + my $authz = $r->authz; + my $user = $r->param('user'); + my $make_local_copy = $r->param('make_local_copy'); + + # Check permissions + return CGI::div({class=>"ResultsWithError"}, "You are not authorized to access the Instructor tools.") + unless $authz->hasPermissions($user, "access_instructor_tools"); + + return CGI::div({class=>"ResultsWithError"}, "You are not authorized to modify problems.") + unless $authz->hasPermissions($user, "modify_student_data"); + + + + + # Gathering info + my $editFilePath = $self->{editFilePath}; # path to the permanent file to be edited + my $tempFilePath = $self->{tempFilePath}; # path to the file currently being worked with (might be a .tmp file) + my $inputFilePath = $self->{inputFilePath}; # path to the file for input, (might be a .tmp file) + my $setName = $self->{setID} ; + my $problemNumber = $self->{problemID} ; + $setName = defined($setName) ? $setName : ''; # we need this instead of using the || construction + # to keep set 0 from being set to the + # empty string. + my $fullSetName = defined( $self->{fullSetID} ) ? $self->{fullSetID} : $setName; + $problemNumber = defined($problemNumber) ? $problemNumber : ''; + + + + ######################################################################### + # Construct url for reporting bugs: + ######################################################################### + + my $libraryName = ''; + if ($editFilePath =~ m|([^/]*)Library|) { #find the path to the file + # find the library, if any exists in the path name (first library is picked) + my $tempLibraryName = $1; + $libraryName = ( not_blank($tempLibraryName) ) ? + $tempLibraryName : "Library"; + # things that start /Library/setFoo/probBar are labeled as component "Library" + # which refers to the SQL based problem library. (is nationalLibrary a better name?) + } else { + $libraryName = 'Library'; # make sure there is some default component defined. + } + + my $BUGZILLA = "$ce->{webworkURLs}{bugReporter}?product=Problem%20libraries". + "&component=$libraryName&bug_file_loc=${editFilePath}_with_problemSeed=".$self->{problemSeed}; + #FIXME # The construction of this URL is somewhat fragile. A separate module could be devoted to + # intelligent bug reporting. + + ######################################################################### + # Construct reference row for PGproblemEditor. + ######################################################################### + + my @PG_Editor_Reference_Links = ( + {label => 'Problem Techniques' , + url => 'http://webwork.maa.org/wiki/Category:Problem_Techniques' , + target => 'techniques_window' , + tooltip => 'Snippets of PG code illustrating specific techniques' , + }, + {label => 'Math Objects' , + url => 'http://webwork.maa.org/wiki/Category:MathObjects' , + target => 'math_objects' , + tooltip => 'Wiki summary page for MathObjects' , + }, + {label => 'POD' , + url => 'http://webwork.maa.org/pod/pg_TRUNK/' , + target => 'pod_docs' , + tooltip => 'Documentation from source code for PG modules and macro files. Often the most up-to-date information.' , + }, + {label => 'PGLab' , + url => 'http://hosted2.webwork.rochester.edu/webwork2/wikiExamples/MathObjectsLabs2/2/?login_practice_user=true' , + target => 'PGLab' , + tooltip => 'Test snippets of PG code in interactive lab. Good way to learn PG language.' , + }, + {label => 'PGML' , + url => 'https://courses.webwork.maa.org/webwork2/cervone_course/PGML/1/?login_practice_user=true', + target => 'PGML' , + tooltip => 'PG mark down syntax used to format WeBWorK questions. This interactive lab can help you to learn the techniques.' , + }, + {label => 'Author Info' , + url => 'http://webwork.maa.org/wiki/Category:Authors' , + target => 'author_info' , + tooltip => 'Top level of author information on the wiki.' , + }, + {label => 'report bugs in this problem' , + url => $BUGZILLA , + target => 'bug_report' , + tooltip => 'Report bugs in a WeBWorK question/problem using this link.
The very first time you do this you will need to register with an email address so that information on the bug fix can be reported back to you.' , + }, + + + + ); + my $PG_Editor_Reference_String = ''; + foreach my $link (@PG_Editor_Reference_Links) { + $PG_Editor_Reference_String .= ' | '. + CGI::a({-href=>$link->{url} ,-target=> $link->{target}, + -onmouseover=>q!Tip('! . $link->{tooltip} . + q!',SHADOW, true, + DELAY, 100, FADEIN, 300, FADEOUT, 300, STICKY, 0, OFFSETX, -20, CLICKCLOSE, true, + BGCOLOR, '#F4FF91', TITLE, 'Reference:',TITLEBGCOLOR, '#F4FF91', TITLEFONTCOLOR, '#000000')! + }, + ' '.$link->{label}.' '); + } + $PG_Editor_Reference_String .=' | '; + ######################################################################### + # Find the text for the problem, either in the tmp file, if it exists + # or in the original file in the template directory + # or in the problem contents gathered in the initialization phase. + ######################################################################### + + my $problemContents = ${$self->{r_problemContents}}; + + unless ( $problemContents =~/\S/) { # non-empty contents + if (-r $tempFilePath and not -d $tempFilePath) { + die "tempFilePath is unsafe!" unless path_is_subdir($tempFilePath, $ce->{courseDirs}->{templates}, 1); # 1==path can be relative to dir + eval { $problemContents = WeBWorK::Utils::readFile($tempFilePath) }; + $problemContents = $@ if $@; + $inputFilePath = $tempFilePath; + } elsif (-r $editFilePath and not -d $editFilePath) { + die "editFilePath is unsafe!" unless path_is_subdir($editFilePath, $ce->{courseDirs}->{templates}, 1) # 1==path can be relative to dir + || $editFilePath eq $ce->{webworkFiles}{screenSnippets}{setHeader} + || $editFilePath eq $ce->{webworkFiles}{hardcopySnippets}{setHeader} + || $editFilePath eq $ce->{webworkFiles}{screenSnippets}{blankProblem}; + eval { $problemContents = WeBWorK::Utils::readFile($editFilePath) }; + $problemContents = $@ if $@; + $inputFilePath = $editFilePath; + } else { # file not existing is not an error + #warn "No file exists"; + $problemContents = ''; + } + } else { + #warn "obtaining input from r_problemContents"; + } + + my $protected_file = not -w $inputFilePath; + + my $file_type = $self->{file_type}; + my %titles = ( + problem => CGI::b("set $fullSetName/problem $problemNumber"), + blank_problem => "blank problem", + set_header => "header file", + hardcopy_header => "hardcopy header file", + course_info => "course information", + options_info => "options information", + '' => 'Unknown file type', + source_path_for_problem_file => " unassigned problem file: ".CGI::b("set $setName/problem $problemNumber"), + ); + my $header = CGI::i("Editing $titles{$file_type} in file '".$self->shortPath($inputFilePath)."'"); + $header = ($self->isTempEditFilePath($inputFilePath) ) ? CGI::div({class=>'temporaryFile'},$header) : $header; # use colors if temporary file + + ######################################################################### + # Format the page + ######################################################################### + + # Define parameters for textarea + # FIXME + # Should the seed be set from some particular user instance?? + my $rows = 20; + my $columns = 80; + my $mode_list = $ce->{pg}->{displayModes}; + my $displayMode = $self->{displayMode}; + my $problemSeed = $self->{problemSeed}; + my $uri = $r->uri; + my $edit_level = $r->param('edit_level') || 0; + + my $force_field = (not_blank( $self->{sourceFilePath}) ) ? # path is a non-blank string + CGI::hidden(-name=>'sourceFilePath', + -default=>$self->{sourceFilePath}) : ''; + + # FIXME this isn't used at all! --sam + my @allSetNames = sort $db->listGlobalSets; + for (my $j=0; $j{webworkURLs}->{htdocs}; + print qq!!; + print CGI::script(<"POST", id=>"editor", name=>"editor", action=>"$uri", enctype=>"application/x-www-form-urlencoded"}), + + $self->hidden_authen_fields, + $force_field, + CGI::hidden(-name=>'file_type',-default=>$self->{file_type}), + CGI::div({},$PG_Editor_Reference_String), +# CGI::a({-href=>'http://webwork.math.rochester.edu/docs/docs/pglanguage/manpages/',-target=>"manpage_window"}, +# ' Manpages ', +# )," | ", +# CGI::a({-href=>'http://devel.webwork.rochester.edu/twiki/bin/view/Webwork/PGmacrosByFile',-target=>"manpage_window"}, +# ' macro list ', +# )," | ", +# CGI::a({-href=>'http://webwork.maa.org/wiki/Category:Authors',-target=>"wiki_window"}, +# ' authoring info & help ', +# )," | ", +# CGI::a({-href=>'http://hosted2.webwork.rochester.edu/webwork2/wikiExamples/MathObjectsLabs2/2/?login_practice_user=true',-target=>"lab_window", +# }, +# ' testing lab ' +# )," | ", +# CGI::a({-href=>'http://devel.webwork.rochester.edu/doc/cvs/pg_HEAD/',-target=>"doc_window"}, +# ' pod docs ', +# )," | ", +# CGI::a({-href=>$BUGZILLA,-target=>"bugs_window"}, +# ' report problem bugs ', +# )," | ", +# ), + CGI::p( + CGI::textarea( + -name => 'problemContents', -default => $problemContents, + -rows => $rows, -cols => $columns, -override => 1, + ), + ); + + + +######### print action forms + + #print CGI::Tr({}, CGI::td({-colspan=>2}, "Select an action to perform:")); + + my @formsToShow = @{ ACTION_FORMS() }; + my $default_choice = $formsToShow[0]; + my $i = 0; + +###################################################################COLUMN STYLE BEGIN############################################################### + + # foreach my $actionID (@formsToShow) { + # # Check permissions + # #next if FORM_PERMS()->{$actionID} and not $authz->hasPermissions($user, FORM_PERMS()->{$actionID}); + # my $actionForm = "${actionID}_form"; + # my $newWindow = ($actionID =~ m/^(view|add_problem|save)$/)? 1: 0; + # my $onChange = "setRadio($i,$newWindow)"; + # my %actionParams = $self->getActionParams($actionID); + # my $line_contents = $self->$actionForm($onChange, %actionParams); + # my $radio_params = {-type=>"radio", -name=>"action", -value=>$actionID}; + # $radio_params->{checked}=1 if ($actionID eq $default_choice) ; + # $radio_params->{onclick} = "setTarget($newWindow)"; + # $radio_params->{id} = "action$i"; + # # print CGI::Tr({-valign=>"top"}, + # # CGI::td({}, CGI::input($radio_params)), + # # CGI::td({}, $line_contents) + # # ) if $line_contents; + # if($line_contents){ + # print CGI::start_div({-class=>"column"}); + # print CGI::div({-class=>"pg_editor_input_span"},WeBWorK::CGI_labeled_input(-type=>"radio", -id=>$actionForm."_id", -label_text=>ucfirst(WeBWorK::underscore_to_whitespace($actionForm)), -input_attr=>$radio_params),CGI::br()); + # print CGI::div({-class=>"pg_editor_input_div"},$line_contents); + # print CGI::br(); + # print CGI::end_div(); + # } + # $i++; + # } + # my $checkbox = WeBWorK::CGI_labeled_input(-type=>"checkbox", -id=>"newWindow", -label_text=>"Open in new window", -input_attr=>{-checked=>"checked", -onchange=>"updateTarget()"}); + # $checkbox =~ s/\n//; # remove unwanted linebreak + # print CGI::div({-class=>"pd_editor_input_div", -id=>"submit_input_div"}, $checkbox, CGI::br(), WeBWorK::CGI_labeled_input(-type=>"submit", -id=>"submit_button_id", -input_attr=>{-name=>'submit', -value=>"Take Action!"})); + +########################################################COLUMN STYLE END########################################################################### + + +########################################################TABBER STYLE BEGIN######################################################################### + + my @divArr = (); + + foreach my $actionID (@formsToShow) { + # Check permissions + #next if FORM_PERMS()->{$actionID} and not $authz->hasPermissions($user, FORM_PERMS()->{$actionID}); + my $actionForm = "${actionID}_form"; + my $newWindow = ($actionID =~ m/^(view|add_problem|save)$/)? 1: 0; + my $onChange = "setRadio($i,$newWindow)"; + my %actionParams = $self->getActionParams($actionID); + my $line_contents = $self->$actionForm($onChange, %actionParams); + my $radio_params = {-type=>"radio", -name=>"action", -value=>$actionID}; + $radio_params->{checked}=1 if ($actionID eq $default_choice) ; + $radio_params->{onclick} = "setTarget($newWindow)"; + $radio_params->{id} = "action$i"; + # print CGI::Tr({-valign=>"top"}, + # CGI::td({}, CGI::input($radio_params)), + # CGI::td({}, $line_contents) + # ) if $line_contents; + if($line_contents){ + my @titleArr = split(" ", ucfirst(WeBWorK::underscore_to_whitespace($actionForm))); + my $title = $titleArr[0]; + push @divArr, join("", + CGI::h3($title), + CGI::div({-class=>"pg_editor_input_span"},WeBWorK::CGI_labeled_input(-type=>"radio", -id=>$actionForm."_id", -label_text=>ucfirst(WeBWorK::underscore_to_whitespace($actionForm)), -input_attr=>$radio_params),CGI::br()), + CGI::div({-class=>"pg_editor_input_div"},$line_contents), + CGI::br()) + } + $i++; + } + + my $divArrRef = \@divArr; + + print CGI::div({-class=>"tabber"}, + CGI::div({-class=>"tabbertab"},$divArrRef) + ); + +###################################################TABBER STYLE END############################################################################## + + my $checkbox = WeBWorK::CGI_labeled_input(-type=>"checkbox", -id=>"newWindow", -label_text=>"Open in new window", -input_attr=>{-checked=>"checked", -onchange=>"updateTarget()"}); + $checkbox =~ s/\n//; # remove unwanted linebreak + print CGI::div({-class=>"pd_editor_input_div", -id=>"submit_input_div"}, $checkbox, CGI::br(), WeBWorK::CGI_labeled_input(-type=>"submit", -id=>"submit_button_id", -input_attr=>{-name=>'submit', -value=>"Take Action!"})); + + + print CGI::end_form(); + + print CGI::script("updateTarget()"); + return ""; + + +} + +# +# Convert long paths to [TMPL], etc. +# +sub shortPath { + my $self = shift; my $file = shift; + my $tmpl = $self->r->ce->{courseDirs}{templates}; + my $root = $self->r->ce->{courseDirs}{root}; + my $ww = $self->r->ce->{webworkDirs}{root}; + $file =~ s|^$tmpl|[TMPL]|; $file =~ s|^$root|[COURSE]|; $file =~ s|^$ww|[WW]|; + return $file; +} + +################################################################################ +# Utilities +################################################################################ + +sub getRelativeSourceFilePath { + my ($self, $sourceFilePath) = @_; + + my $templatesDir = $self->r->ce->{courseDirs}->{templates}; + $sourceFilePath =~ s|^$templatesDir/*||; # remove templates path and any slashes that follow + + return $sourceFilePath; +} + +# determineLocalFilePath constructs a local file path parallel to a library file path + +# +sub determineLocalFilePath { + my $self= shift; die "determineLocalFilePath is a method" unless ref($self); + my $path = shift; + my $default_screen_header_path = $self->r->ce->{webworkFiles}->{hardcopySnippets}->{setHeader}; + my $default_hardcopy_header_path = $self->r->ce->{webworkFiles}->{screenSnippets}->{setHeader}; + my $setID = $self->{setID}; + $setID = int(rand(1000)) unless $setID =~/\S/; # setID can be 0 + if ($path =~ /Library/) { + #$path =~ s|^.*?Library/||; # truncate the url up to a segment such as ...rochesterLibrary/....... + $path =~ s|^.*?Library/|local/|; # truncate the url up to a segment such as ...rochesterLibrary/....... and prepend local + } elsif ($path eq $default_screen_header_path) { + $path = "set$setID/setHeader.pg"; + } elsif ($path eq $default_hardcopy_header_path) { + $path = "set$setID/hardcopyHeader.tex"; + } else { # if its not in a library we'll just save it locally + $path = "new_problem_".int(rand(1000)).".pg"; #l hope there aren't any collisions. + } + $path; + +} + +sub determineTempEditFilePath { # this does not create the directories in the path to the file + # it returns an absolute path to the file + my $self = shift; die "determineTempEditFilePath is a method" unless ref($self); + my $path =shift; # this should be an absolute path to the file + my $user = $self->r->param("user"); + $user = int(rand(1000)) unless defined $user; + my $setID = $self->{setID} || int(rand(1000)); + my $courseDirectory = $self->r->ce->{courseDirs}; + ############### + # Calculate the location of the temporary file + ############### + my $templatesDirectory = $courseDirectory->{templates}; + my $blank_file_path = $self->r->ce->{webworkFiles}->{screenSnippets}->{blankProblem}; + my $default_screen_header_path = $self->r->ce->{webworkFiles}->{hardcopySnippets}->{setHeader}; + my $default_hardcopy_header_path = $self->r->ce->{webworkFiles}->{screenSnippets}->{setHeader}; + my $tmpEditFileDirectory = $self->getTempEditFileDirectory(); + $self->addbadmessage("The path to the original file should be absolute") unless $path =~m|^/|; # debug + if ($path =~/^$tmpEditFileDirectory/) { + $self->addbadmessage("Error: This path is already in the temporary edit directory -- no new temporary file is created. path = $path"); + + } else { + if ($path =~ /^$templatesDirectory/ ) { + $path =~ s|^$templatesDirectory||; + $path =~ s|^/||; # remove the initial slash if any + $path = "$tmpEditFileDirectory/$path.$user.tmp"; + } elsif ($path eq $blank_file_path) { + $path = "$tmpEditFileDirectory/blank.$setID.$user.tmp"; # handle the case of the blank problem + } elsif ($path eq $default_screen_header_path) { + $path = "$tmpEditFileDirectory/screenHeader.$setID.$user.tmp"; # handle the case of the screen header in snippets + } elsif ($path eq $default_hardcopy_header_path) { + $path = "$tmpEditFileDirectory/hardcopyHeader.$setID.$user.tmp"; # handle the case of the hardcopy header in snippets + } else { + die "determineTempEditFilePath should only be used on paths within the templates directory, not on $path"; + } + } + $path; +} + +sub determineOriginalEditFilePath { # determine the original path to a file corresponding to a temporary edit file + # returns path relative to the template directory + my $self = shift; + my $path = shift; + my $user = $self->r->param("user"); + $self->addbadmessage("Can't determine user of temporary edit file $path.") unless defined($user); + my $templatesDirectory = $self->r->ce->{courseDirs} ->{templates}; + my $tmpEditFileDirectory = $self->getTempEditFileDirectory(); + # unless path is absolute assume that it is relative to the template directory + my $newpath = $path; + unless ($path =~ m|^/| ) { + $newpath = "$templatesDirectory/$path"; + } + if ($self->isTempEditFilePath($newpath) ) { + $newpath =~ s|^$tmpEditFileDirectory/||; # delete temp edit directory + if ($newpath =~m|blank\.[^/]*$|) { # handle the case of the blank problem + $newpath = $self->r->ce->{webworkFiles}->{screenSnippets}->{blankProblem}; + } elsif (($newpath =~m|hardcopyHeader\.[^/]*$|)) { # handle the case of the hardcopy header in snippets + $newpath = $self->r->ce->{webworkFiles}->{hardcopySnippets}->{setHeader}; + } elsif (($newpath =~m|screenHeader\.[^/]*$|)) { # handle the case of the screen header in snippets + $newpath = $self->r->ce->{webworkFiles}->{screenSnippets}->{setHeader}; + } else { + $newpath =~ s|\.$user\.tmp$||; # delete suffix + + } + #$self->addgoodmessage("Original file path is $newpath"); #FIXME debug + } else { + $self->addbadmessage("This path |$newpath| is not the path to a temporary edit file."); + # returns original path + } + $newpath; +} + +sub getTempEditFileDirectory { + my $self = shift; + my $courseDirectory = $self->r->ce->{courseDirs}; + my $templatesDirectory = $courseDirectory->{templates}; + my $tmpEditFileDirectory = (defined ($courseDirectory->{tmpEditFileDir}) ) ? $courseDirectory->{tmpEditFileDir} : "$templatesDirectory/tmpEdit"; + $tmpEditFileDirectory; +} +sub isTempEditFilePath { + my $self = shift; + my $path = shift; + my $templatesDirectory = $self->r->ce->{courseDirs} ->{templates}; + # unless path is absolute assume that it is relative to the template directory + unless ($path =~ m|^/| ) { + $path = "$templatesDirectory/$path"; + } + my $tmpEditFileDirectory = $self->getTempEditFileDirectory(); + + ($path =~/^$tmpEditFileDirectory/) ? 1: 0; +} +sub getFilePaths { + my ($self, $setName, $problemNumber, $file_type) = @_; + my $r = $self->r; + my $ce = $r->ce; + my $db = $r->db; + my $urlpath = $r->urlpath; + my $courseName = $urlpath->arg("courseID"); + my $user = $r->param('user'); + my $effectiveUserName = $r->param('effectiveUser'); + + $setName = '' unless defined $setName; + $problemNumber = '' unless defined $problemNumber; + + # parse possibly versioned set names + my $fullSetName = $setName; + my $editSetVersion = 0; + if ( $setName =~ /,v(\d)+$/ ) { + $editSetVersion = $1; + $setName =~ s/,v\d+$//; + } + + die 'Internal error to PGProblemEditor2 -- file type is not defined' unless defined $file_type; + #$self->addgoodmessage("file type is $file_type"); #FIXME remove + ########################################################## + # Determine path to the input file to be edited. + # The permanent path of the input file == $editFilePath + # A temporary path to the input file == $tempFilePath + ########################################################## + # Relevant parameters + # $r->param("displayMode") + # $r->param('problemSeed') + # $r->param('submit') + # $r->param('make_local_copy') + # $r->param('sourceFilePath') + # $r->param('problemContents') + # $r->param('save_to_new_file') + ########################################################################## + # Define the following variables + # path to regular file -- $editFilePath; + # path to file being read (temporary or permanent) + # contents of the file being read --- $problemContents + # $self->{r_problemContents} = \$problemContents; + ########################################################################### + + my $editFilePath = $ce->{courseDirs}->{templates}; + + ########################################################################## + # Determine path to regular file, place it in $editFilePath + # problemSeed is defined for the file_type = 'problem' and 'source_path_to_problem' + ########################################################################## + CASE: + { + ($file_type eq 'course_info') and do { + # we are editing the course_info file + # value of courseFiles::course_info is relative to templates directory + $editFilePath .= '/' . $ce->{courseFiles}->{course_info}; + last CASE; + }; + + ($file_type eq 'options_info') and do { + # we are editing the options_info file + # value of courseFiles::options_info is relative to templates directory + $editFilePath .= '/' . $ce->{courseFiles}->{options_info}; + last CASE; + }; + + ($file_type eq 'blank_problem') and do { + $editFilePath = $ce->{webworkFiles}->{screenSnippets}->{blankProblem}; + $self->addbadmessage("This is a blank problem template file and can not be edited directly. " + ."Use the 'Save as' action below to create a local copy of the file and add it to the current problem set." + ); + last CASE; + }; + + ($file_type eq 'set_header' or $file_type eq 'hardcopy_header') and do { + # first try getting the merged set for the effective user + # FIXME merged set is overwritten immediately with global value... WTF? --sam + my $set_record = $db->getMergedSet($effectiveUserName, $setName); # checked + # if that doesn't work (the set is not yet assigned), get the global record + $set_record = $db->getGlobalSet($setName); # checked + # bail if no set is found + die "Cannot find a set record for set $setName" unless defined($set_record); + + my $header_file = ""; + $header_file = $set_record->{$file_type}; + if ($header_file && $header_file ne "" && $header_file ne "defaultHeader") { + if ( $header_file =~ m|^/| ) { # if absolute address + $editFilePath = $header_file; + } else { + $editFilePath .= '/' . $header_file; + } + } else { + # if the set record doesn't specify the filename for a header + # then the set uses the default from snippets + + $editFilePath = $ce->{webworkFiles}->{screenSnippets}->{setHeader} if $file_type eq 'set_header'; + $editFilePath = $ce->{webworkFiles}->{hardcopySnippets}->{setHeader} if $file_type eq 'hardcopy_header'; + +# $self->addbadmessage("'".$self->shortPath($editFilePath)."' is the default header file and cannot be edited directly.".CGI::br()."Any changes you make will have to be saved as another file."); + #} + + } + last CASE; + }; #end 'set_header, hardcopy_header' case + + ($file_type eq 'problem') and do { + + # first try getting the merged problem for the effective user + my $problem_record; + if ( $editSetVersion ) { + $problem_record = $db->getMergedProblemVersion($effectiveUserName, $setName, $editSetVersion, $problemNumber); # checked + } else { + $problem_record = $db->getMergedProblem($effectiveUserName, $setName, $problemNumber); # checked + } + + # if that doesn't work (the problem is not yet assigned), get the global record + $problem_record = $db->getGlobalProblem($setName, $problemNumber) unless defined($problem_record); # checked + # bail if no source path for the problem is found ; + die "Cannot find a problem record for set $setName / problem $problemNumber" unless defined($problem_record); + $editFilePath .= '/' . $problem_record->source_file; + # define the problem seed for later use + $self->{problemSeed}= $problem_record->problem_seed if defined($problem_record) and $problem_record->can('problem_seed') ; + last CASE; + }; # end 'problem' case + + ($file_type eq 'source_path_for_problem_file') and do { + my $forcedSourceFile = $self->{sourceFilePath}; + # if the source file is in the temporary edit directory find the original source file + # the source file is relative to the templates directory. + if ($self->isTempEditFilePath($forcedSourceFile) ) { + $forcedSourceFile = $self->determineOriginalEditFilePath($forcedSourceFile); # original file path + $self->addgoodmessage("the original path to the file is $forcedSourceFile"); #FIXME debug + } + # bail if no source path for the problem is found ; + die "Cannot find a file path to save to" unless( not_blank($forcedSourceFile) ); + $self->{problemSeed} = DEFAULT_SEED(); + $editFilePath .= '/' . $forcedSourceFile; + last CASE; + }; # end 'source_path_for_problem_file' case + } # end CASE: statement + + + # if a set record or problem record contains an empty blank for a header or problem source_file + # we could find ourselves trying to edit /blah/templates/.toenail.tmp or something similar + # which is almost undoubtedly NOT desirable + + if (-d $editFilePath) { + my $msg = "The file '".$self->shortPath($editFilePath)."' is a directory!"; + $self->{failure} = 1; + $self->addbadmessage($msg); + } + if (-e $editFilePath and not -r $editFilePath) { #it's ok if the file doesn't exist, perhaps we're going to create it + # with save as + my $msg = "The file '".$self->shortPath($editFilePath)."' cannot be read!"; + $self->{failure} = 1; + $self->addbadmessage($msg); + } + ################################################# + # The path to the permanent file is now verified and stored in $editFilePath + # Whew!!! + ################################################# + + my $tempFilePath = $self->determineTempEditFilePath($editFilePath); #"$editFilePath.$TEMPFILESUFFIX"; + $self->{editFilePath} = $editFilePath; + $self->{tempFilePath} = $tempFilePath; + $self->{inputFilePath} = (-r $tempFilePath) ? $tempFilePath : $editFilePath; + #warn "editfile path is $editFilePath and tempFile is $tempFilePath and inputFilePath is ". $self->{inputFilePath}; +} +sub saveFileChanges { + +################################################################################ +# saveFileChanges does most of the work. it is a separate method so that it can +# be called from either pre_header_initialize() or initilize(), depending on +# whether a redirect is needed or not. +# +# it actually does a lot more than save changes to the file being edited, and +# sometimes less. +################################################################################ + + my ($self, $outputFilePath, $problemContents ) = @_; + my $r = $self->r; + my $ce = $r->ce; + + my $action = $self->{action}||'no action'; + # my $editFilePath = $self->{editFilePath}; # not used?? + my $sourceFilePath = $self->{sourceFilePath}; + my $tempFilePath = $self->{tempFilePath}; + + if (defined($problemContents) and ref($problemContents) ) { + $problemContents = ${$problemContents}; + } elsif( ! not_blank($problemContents) ) { # if the problemContents is undefined or empty + $problemContents = ${$self->{r_problemContents}}; + } + ############################################################################## + # read and update the targetFile and targetFile.tmp files in the directory + # if a .tmp file already exists use that, unless the revert button has been pressed. + # The .tmp files are removed when the file is or when the revert occurs. + ############################################################################## + + + unless (not_blank($outputFilePath) ) { + $self->addbadmessage("You must specify an file name in order to save a new file."); + return ""; + } + my $do_not_save = 0 ; # flag to prevent saving of file + my $editErrors = ''; + + ############################################################################## + # write changes to the approriate files + # FIXME make sure that the permissions are set correctly!!! + # Make sure that the warning is being transmitted properly. + ############################################################################## + + my $writeFileErrors; + if ( not_blank($outputFilePath) ) { # save file + # Handle the problem of line endings. + # Make sure that all of the line endings are of unix type. + # Convert \r\n to \n + #$problemContents =~ s/\r\n/\n/g; + #$problemContents =~ s/\r/\n/g; + + # make sure any missing directories are created + WeBWorK::Utils::surePathToFile($ce->{courseDirs}->{templates}, + $outputFilePath); + die "outputFilePath is unsafe!" unless path_is_subdir($outputFilePath, $ce->{courseDirs}->{templates}, 1); # 1==path can be relative to dir + + eval { + local *OUTPUTFILE; + open OUTPUTFILE, ">$outputFilePath" + or die "Failed to open $outputFilePath"; + print OUTPUTFILE $problemContents; + close OUTPUTFILE; + # any errors are caught in the next block + }; + + $writeFileErrors = $@ if $@; + } + + ########################################################### + # Catch errors in saving files, clean up temp files + ########################################################### + + $self->{saveError} = $do_not_save; # don't do redirects if the file was not saved. + # don't unlink files or send success messages + + if ($writeFileErrors) { + # get the current directory from the outputFilePath + $outputFilePath =~ m|^(/.*?/)[^/]+$|; + my $currentDirectory = $1; + + my $errorMessage; + # check why we failed to give better error messages + if ( not -w $ce->{courseDirs}->{templates} ) { + $errorMessage = "Write permissions have not been enabled in the templates directory. No changes can be made."; + } elsif ( not -w $currentDirectory ) { + $errorMessage = "Write permissions have not been enabled in '".$self->shortPath($currentDirectory)."'. Changes must be saved to a different directory for viewing."; + } elsif ( -e $outputFilePath and not -w $outputFilePath ) { + $errorMessage = "Write permissions have not been enabled for '".$self->shortPath($outputFilePath)."'. Changes must be saved to another file for viewing."; + } else { + $errorMessage = "Unable to write to '".$self->shortPath($outputFilePath)."': $writeFileErrors"; + } + + $self->{failure} = 1; + $self->addbadmessage(CGI::p($errorMessage)); + + } + ########################################################### + # FIXME if the file is accompanied by auxiliary files transfer them as well + # if the filepath ends in foobar/foobar.pg then we assume there are auxiliary files + # copy the contents of the original foobar directory to the new one + # + ########################################################### + # If things have worked so far determine if the file might be accompanied by auxiliary files + # a path ending in foo/foo.pg is assumed to contain auxilliary files + # + my $auxiliaryFilesExist = has_aux_files($outputFilePath); + + if ($auxiliaryFilesExist and not $do_not_save ) { + my $sourceDirectory = $sourceFilePath; + my $outputDirectory = $outputFilePath; + $sourceDirectory =~ s|/[^/]+\.pg$||; + $outputDirectory =~ s|/[^/]+\.pg$||; +############## +# Transfer this to Utils::copyAuxiliaryFiles($sourceDirectory, $destinationDirectory) +############## + my @filesToCopy; + @filesToCopy = WeBWorK::Utils::readDirectory($sourceDirectory) if -d $sourceDirectory; + foreach my $file (@filesToCopy) { + next if $file =~ /\.pg$/; # .pg file should already be transferred + my $fromPath = "$sourceDirectory/$file"; + my $toPath = "$outputDirectory/$file"; + if (-f $fromPath and -r $fromPath and not -e $toPath) { # don't copy directories, don't copy files that have already been copied + copy($fromPath, $toPath) or $writeFileErrors.= "
Error copying $fromPath to $toPath"; + # need to use binary transfer for gif files. File::Copy does this. + #warn "copied from $fromPath to $toPath"; + #warn "files are different ",system("diff $fromPath $toPath"); + } + $self->addbadmessage($writeFileErrors) if not_blank($writeFileErrors); + + + } + $self->addgoodmessage("Copied auxiliary files from $sourceDirectory to new location at $outputDirectory"); + + } + ############## + ############## + + ########################################################### + # clean up temp files on revert, save and save_as + ########################################################### + unless( $writeFileErrors or $do_not_save) { # everything worked! unlink and announce success! + # unlink the temporary file if there are no errors and the save button has been pushed + if (($action eq 'save' or $action eq 'save_as') and (-w $self->{tempFilePath}) ) { + + $self->addgoodmessage("Deleting temp file at " . $self->shortPath($self->{tempFilePath})); + die "tempFilePath is unsafe!" unless path_is_subdir($self->{tempFilePath}, $ce->{courseDirs}->{templates}, 1); # 1==path can be relative to dir + unlink($self->{tempFilePath}) ; + } + + if ( defined($outputFilePath) and ! $self->{failure} and not $self->isTempEditFilePath($outputFilePath) ) { + # don't announce saving of temporary editing files + my $msg = "Saved to file '".$self->shortPath($outputFilePath)."'."; + + $self->addgoodmessage($msg); + #$self->{inputFilePath} = $outputFilePath; ## DPVC -- avoid file-not-found message + } + + } + + +} # end saveFileChanges + + + + + +sub getActionParams { + my ($self, $actionID) = @_; + my $r = $self->{r}; + + my %actionParams=(); + foreach my $param ($r->param) { + next unless $param =~ m/^action\.$actionID\./; + $actionParams{$param} = [ $r->param($param) ]; + } + return %actionParams; +} + +sub fixProblemContents { + #NOT a method + my $problemContents = shift; + # Handle the problem of line endings. + # Make sure that all of the line endings are of unix type. + # Convert \r\n to \n + $problemContents =~ s/\r\n/\n/g; + $problemContents =~ s/\r/\n/g; + $problemContents; +} + +sub fresh_edit_handler { + my ($self, $genericParams, $actionParams, $tableParams) = @_; + #$self->addgoodmessage("fresh_edit_handler called"); +} +sub view_form { + my ($self, $onChange, %actionParams) = @_; + my $file_type = $self->{file_type}; + return "" if $file_type eq 'hardcopy_header'; # these can't yet be edited from temporary files #FIXME + my $output_string = ""; + unless ($file_type eq 'course_info' || $file_type eq 'options_info') { + + $output_string .= join(" ", + # "Use what seed?: ", + # CGI::textfield(-name=>'action.view.seed',-value=>$self->{problemSeed},-onfocus=>$onChange), + WeBWorK::CGI_labeled_input(-type=>"text", -id=>"action_view_seed_id", -label_text=>"Using what seed?: ", -input_attr=>{-name=>'action.view.seed',-value=>$self->{problemSeed},-onfocus=>$onChange}),CGI::br(), + # "and display mode ", + # CGI::popup_menu(-name=>'action.view.displayMode', -values=>$self->r->ce->{pg}->{displayModes}, + # -default=>$self->{displayMode}, -onmousedown=>$onChange) + WeBWorK::CGI_labeled_input(-type=>"select", -id=>"action_view_displayMode_id", -label_text=>"Using what display mode?: ", -input_attr=>{-name=>'action.view.displayMode', -values=>$self->r->ce->{pg}->{displayModes}, -default=>$self->{displayMode}, -onmousedown=>$onChange}),CGI::br(), + ); + } + + return $output_string; #FIXME add -labels to the pop up menu +} + +sub view_handler { + my ($self, $genericParams, $actionParams, $tableParams) = @_; + my $r = $self->r; + my $courseName = $self->{courseID}; + my $setName = $self->{setID}; + my $fullSetName = $self->{fullSetID}; + my $problemNumber = $self->{problemID}; + my $problemSeed = ($actionParams->{'action.view.seed'}) ? + $actionParams->{'action.view.seed'}->[0] + : DEFAULT_SEED(); + my $displayMode = ($actionParams->{'action.view.displayMode'}) ? + $actionParams->{'action.view.displayMode'}->[0] + : $self->r->ce->{pg}->{options}->{displayMode}; + + my $editFilePath = $self->{editFilePath}; + my $tempFilePath = $self->{tempFilePath}; + ######################################################## + # grab the problemContents from the form in order to save it to the tmp file + ######################################################## + my $problemContents = fixProblemContents($self->r->param('problemContents')); + $self->{r_problemContents} = \$problemContents; + + + my $do_not_save = 0; + my $file_type = $self->{file_type}; + $self->saveFileChanges($tempFilePath,); + + ######################################################## + # construct redirect URL and redirect + ######################################################## + my $edit_level = $self->r->param("edit_level") || 0; + $edit_level++; + my $viewURL; + + my $relativeTempFilePath = $self->getRelativeSourceFilePath($tempFilePath); + + if ($file_type eq 'problem' or $file_type eq 'source_path_for_problem_file') { # redirect to Problem.pm or GatewayQuiz.pm + + # we need to know if the set is a gateway set to determine the redirect + my $globalSet = $self->r->db->getGlobalSet( $setName ); + + my $problemPage; + if ( defined($globalSet) && $globalSet->assignment_type =~ /gateway/ ) { + $problemPage = $self->r->urlpath->newFromModule("WeBWorK::ContentGenerator::GatewayQuiz",$r, + courseID => $courseName, setID => "Undefined_Set"); + } else { + $problemPage = $self->r->urlpath->newFromModule("WeBWorK::ContentGenerator::Problem",$r, + courseID => $courseName, setID => $setName, problemID => $problemNumber + ); + } + + $viewURL = $self->systemLink($problemPage, + params => { + displayMode => $displayMode, + problemSeed => $problemSeed, + editMode => "temporaryFile", + edit_level => $edit_level, + sourceFilePath => $relativeTempFilePath, + status_message => uri_escape($self->{status_message}) + + } + ); + } elsif ($file_type eq 'set_header' ) { # redirect to ProblemSet + my $problemPage = $self->r->urlpath->newFromModule("WeBWorK::ContentGenerator::ProblemSet",$r, + courseID => $courseName, setID => $setName, + ); + + $viewURL = $self->systemLink($problemPage, + params => { + set_header => $tempFilePath, + displayMode => $displayMode, + problemSeed => $problemSeed, + editMode => "temporaryFile", + edit_level => $edit_level, + sourceFilePath => $relativeTempFilePath, + status_message => uri_escape($self->{status_message}) + + } + ); + } elsif ($file_type eq 'hardcopy_header') { # redirect to ProblemSet?? # it's difficult to view temporary changes for hardcopy headers + my $problemPage = $self->r->urlpath->newFromModule("WeBWorK::ContentGenerator::ProblemSet",$r, + courseID => $courseName, setID => $setName, + ); + + $viewURL = $self->systemLink($problemPage, + params => { + set_header => $tempFilePath, + displayMode => $displayMode, + problemSeed => $problemSeed, + editMode => "temporaryFile", + edit_level => $edit_level, + sourceFilePath => $relativeTempFilePath, + status_message => uri_escape($self->{status_message}) + + } + ); + + } elsif ($file_type eq 'course_info') { # redirec to ProblemSets.pm + my $problemSetsPage = $self->r->urlpath->newFromModule("WeBWorK::ContentGenerator::ProblemSets",$r, + courseID => $courseName); + $viewURL = $self->systemLink($problemSetsPage, + params => { + + course_info => $tempFilePath, + editMode => "temporaryFile", + edit_level => $edit_level, + sourceFilePath => $relativeTempFilePath, + status_message => uri_escape($self->{status_message}) + } + ); + } elsif ($file_type eq 'options_info') { # redirec to Options.pm + my $optionsPage = $self->r->urlpath->newFromModule("WeBWorK::ContentGenerator::Options",$r, + courseID => $courseName); + $viewURL = $self->systemLink($optionsPage, + params => { + options_info => $tempFilePath, + editMode => "temporaryFile", + edit_level => $edit_level, + sourceFilePath => $relativeTempFilePath, + status_message => uri_escape($self->{status_message}) + } + ); + } else { + die "I don't know how to redirect this file type $file_type "; + } + + $self->reply_with_redirect($viewURL); +} + +sub add_problem_form { + my $self = shift; + my ($onChange, %actionParams) = @_; + my $r = $self->r; + my $setName = $self->{setID} ; + my $problemNumber = $self->{problemID} ; + $setName = defined($setName) ? $setName : ''; # we need this instead of using the || construction + # to keep set 0 from being set to the + # empty string. + my $filePath = $self->{inputFilePath}; + $setName =~ s|^set||; + my @allSetNames = sort $r->db->listGlobalSets; + for (my $j=0; $j 'problem', + set_header => 'set header', + hardcopy_header => 'hardcopy header', + }; + return "" if $self->{file_type} eq 'course_info' || $self->{file_type} eq 'options_info'; + return join(" ", + WeBWorK::CGI_labeled_input(-type=>"select", -id=>"action_add_problem_target_set_id", -label_text=>"Add to what set?: ", -input_attr=>{name=>'action.add_problem.target_set', values=>\@allSetNames, default=>$setName, onmousedown=>$onChange}),CGI::br(), + WeBWorK::CGI_labeled_input(-type=>"select", -id=>"action_add_problem_file_type_id", -label_text=>"Add as what filetype?: ", -input_attr=>{name=>'action.add_problem.file_type', values=>['problem','set_header', 'hardcopy_header'], labels=>$labels, default=>$self->{file_type}, onmousedown=>$onChange}), + CGI::br() + ); #FIXME add -lables to the pop up menu + return ""; +} + +sub add_problem_handler { + my ($self, $genericParams, $actionParams, $tableParams) = @_; + my $r=>$self->r; + #$self->addgoodmessage("add_problem_handler called"); + my $courseName = $self->{courseID}; + my $setName = $self->{setID}; + my $problemNumber = $self->{problemID}; + my $sourceFilePath = $self->{editFilePath}; + my $displayMode = $self->{displayMode}; + my $problemSeed = $self->{problemSeed}; + + my $targetSetName = $actionParams->{'action.add_problem.target_set'}->[0]; + my $targetFileType = $actionParams->{'action.add_problem.file_type'}->[0]; + my $templatesPath = $self->r->ce->{courseDirs}->{templates}; + $sourceFilePath =~ s|^$templatesPath/||; + + my $edit_level = $self->r->param("edit_level") || 0; + $edit_level++; + + my $viewURL =''; + if ($targetFileType eq 'problem') { + my $targetProblemNumber = 1+ WeBWorK::Utils::max( $self->r->db->listGlobalProblems($targetSetName)); + + ################################################# + # Update problem record + ################################################# + my $problemRecord = $self->addProblemToSet( + setName => $targetSetName, + sourceFile => $sourceFilePath, + problemID => $targetProblemNumber, #added to end of set + ); + $self->assignProblemToAllSetUsers($problemRecord); + $self->addgoodmessage("Added $sourceFilePath to ". $targetSetName. " as problem $targetProblemNumber") ; + $self->{file_type} = 'problem'; # change file type to problem -- if it's not already that + + ################################################# + # Set up redirect Problem.pm + ################################################# + my $problemPage = $self->r->urlpath->newFromModule("WeBWorK::ContentGenerator::Problem",$r, + courseID => $courseName, + setID => $targetSetName, + problemID => $targetProblemNumber, + ); + my $relativeSourceFilePath = $self->getRelativeSourceFilePath($sourceFilePath); + $viewURL = $self->systemLink($problemPage, + params => { + displayMode => $displayMode, + problemSeed => $problemSeed, + editMode => "savedFile", + edit_level => $edit_level, + sourceFilePath => $relativeSourceFilePath, + status_message => uri_escape($self->{status_message}) + + } + ); + } elsif ($targetFileType eq 'set_header') { + ################################################# + # Update set record + ################################################# + my $setRecord = $self->r->db->getGlobalSet($targetSetName); + $setRecord->set_header($sourceFilePath); + if( $self->r->db->putGlobalSet($setRecord) ) { + $self->addgoodmessage("Added '".$self->shortPath($sourceFilePath)."' to ". $targetSetName. " as new set header ") ; + } else { + $self->addbadmessage("Unable to make '".$self->shortPath($sourceFilePath)."' the set header for $targetSetName"); + } + $self->{file_type} = 'set_header'; # change file type to set_header if it not already so + ################################################# + # Set up redirect + ################################################# + my $problemPage = $self->r->urlpath->newFromModule("WeBWorK::ContentGenerator::ProblemSet",$r, + courseID => $courseName, setID => $targetSetName + ); + $viewURL = $self->systemLink($problemPage, + params => { + displayMode => $displayMode, + editMode => "savedFile", + edit_level => $edit_level, + status_message => uri_escape($self->{status_message}) + } + ); + } else { + die "Don't know what to do with target file type $targetFileType"; + } + + $self->reply_with_redirect($viewURL); +} + + +sub save_form { + my ($self, $onChange, %actionParams) = @_; + my $r => $self->r; + #return "" unless defined($self->{tempFilePath}) and -e $self->{tempFilePath}; + if ($self->{editFilePath} =~ /$BLANKPROBLEM$/ ) { + return ""; #Can't save blank problems without changing names + } elsif (-w $self->{editFilePath}) { + + return "Save ".CGI::b($self->shortPath($self->{editFilePath}))." and View"; + + } else { + return ""; #"Can't save -- No write permission"; + } + +} + +sub save_handler { + my ($self, $genericParams, $actionParams, $tableParams) = @_; + my $r= $self->r; + #$self->addgoodmessage("save_handler called"); + my $courseName = $self->{courseID}; + my $setName = $self->{setID}; + my $fullSetName = $self->{fullSetID}; + my $problemNumber = $self->{problemID}; + my $displayMode = $self->{displayMode}; + my $problemSeed = $self->{problemSeed}; + + ################################################# + # grab the problemContents from the form in order to save it to a new permanent file + # later we will unlink (delete) the current temporary file + ################################################# + my $problemContents = fixProblemContents($self->r->param('problemContents')); + $self->{r_problemContents} = \$problemContents; + + ################################################# + # Construct the output file path + ################################################# + my $editFilePath = $self->{editFilePath}; + my $outputFilePath = $editFilePath; + + my $do_not_save = 0; + my $file_type = $self->{file_type}; + $self->saveFileChanges($outputFilePath); + ################################################# + # Set up redirect to Problem.pm + ################################################# + my $viewURL; + ######################################################## + # construct redirect URL and redirect + ######################################################## + if ($file_type eq 'problem' || $file_type eq 'source_path_for_problem_file') { # redirect to Problem.pm + + # we need to know if the set is a gateway set to determine the redirect + my $globalSet = $self->r->db->getGlobalSet( $setName ); + my $problemPage; + if ( defined( $globalSet) && $globalSet->assignment_type =~ /gateway/ ) { + $problemPage = $self->r->urlpath->newFromModule("WeBWorK::ContentGenerator::GatewayQuiz",$r, + courseID => $courseName, setID => "Undefined_Set"); + # courseID => $courseName, setID => $fullSetName); + } else { + $problemPage = $self->r->urlpath->newFromModule("WeBWorK::ContentGenerator::Problem",$r, + courseID => $courseName, setID => $setName, problemID => $problemNumber ); + } + + my $relativeEditFilePath = $self->getRelativeSourceFilePath($editFilePath); + + $viewURL = $self->systemLink($problemPage, + params => { + displayMode => $displayMode, + problemSeed => $problemSeed, + editMode => "savedFile", + edit_level => 0, + sourceFilePath => $relativeEditFilePath, + status_message => uri_escape($self->{status_message}) + + } + ); + } elsif ($file_type eq 'set_header' ) { # redirect to ProblemSet + my $problemPage = $self->r->urlpath->newFromModule("WeBWorK::ContentGenerator::ProblemSet",$r, + courseID => $courseName, setID => $setName, + ); + + $viewURL = $self->systemLink($problemPage, + params => { + displayMode => $displayMode, + problemSeed => $problemSeed, + editMode => "savedFile", + edit_level => 0, + status_message => uri_escape($self->{status_message}) + + } + ); + } elsif ( $file_type eq 'hardcopy_header') { # redirect to ProblemSet + my $problemPage = $self->r->urlpath->newFromModule('WeBWorK::ContentGenerator::Hardcopy',$r, + courseID => $courseName, setID => $setName, + ); + + $viewURL = $self->systemLink($problemPage, + params => { + displayMode => $displayMode, + problemSeed => $problemSeed, + editMode => "savedFile", + edit_level => 0, + status_message => uri_escape($self->{status_message}) + + } + ); + + } elsif ($file_type eq 'course_info') { # redirect to ProblemSets.pm + my $problemSetsPage = $self->r->urlpath->newFromModule("WeBWorK::ContentGenerator::ProblemSets",$r, + courseID => $courseName); + $viewURL = $self->systemLink($problemSetsPage, + params => { + editMode => ("savedFile"), + edit_level => 0, + status_message => uri_escape($self->{status_message}) + } + ); + } elsif ($file_type eq 'options_info') { # redirect to Options.pm + my $optionsPage = $self->r->urlpath->newFromModule("WeBWorK::ContentGenerator::Options",$r, + courseID => $courseName); + $viewURL = $self->systemLink($optionsPage, + params => { + editMode => ("savedFile"), + edit_level => 0, + status_message => uri_escape($self->{status_message}) + } + ); + } elsif ($file_type eq 'source_path_for_problem_file') { # redirect to ProblemSets.pm + my $problemPage = $self->r->urlpath->newFromModule("WeBWorK::ContentGenerator::Instructor::PGProblemEditor2",$r, + courseID => $courseName, setID => $setName, problemID => $problemNumber + ); + my $viewURL = $self->systemLink($problemPage, + params=>{ + displayMode => $displayMode, + problemSeed => $problemSeed, + editMode => "savedFile", + edit_level => 0, + sourceFilePath => $outputFilePath, #The path relative to the templates directory is required. + file_type => 'source_path_for_problem_file', + status_message => uri_escape($self->{status_message}) + + } + ); + + } else { + die "I don't know how to redirect this file type $file_type "; + } + + $self->reply_with_redirect($viewURL); +} + + + +# sub make_local_copy_form { +# my ($self, $genericParams, $actionParams, $tableParams) = @_; +# my $editFilePath = $self->{editFilePath}; # path to the permanent file to be edited +# #warn "editFilePath $editFilePath inputFilePath",$self->{inputFilePath}; +# return "" unless -e $editFilePath; +# return "" if -w $editFilePath; +# return "" unless $self->{file_type} eq 'problem' # need problem structure to make local copy in most cases +# or $self->{file_type} eq 'blank_problem' # $editFilePath eq $self->r->cr->{webworkFiles}{screenSnippets}{blankProblem} +# or $self->{file_type} eq 'set_header' # $editFilePath eq $self->r->ce->{webworkFiles}->{hardcopySnippets}->{setHeader} # special case to make copy of screen header +# or $self->{file_type} eq 'hardcopy_header'; # $editFilePath eq $self->r->ce->{webworkFiles}->{screenSnippets}->{setHeader} ; # special case to make copy of hardcopy header +# # or $self->{file_type} eq 'source_path_for_problem_file'; # need setID and problemID to make local copy -- can't be done in this case. +# # make sure setID is well defined before allowing local copy +# my $setID = $self->{setID}; +# my $probNum = ($self->{file_type} eq 'problem')? "/problem $self->{problemID}" : ""; +# return "" unless not_blank($setID) && $setID ne 'Undefined_Set'; +# +# return join ("", +# "Save local editable copy as: [TMPL]/".($self->determineLocalFilePath($editFilePath)).CGI::br()." and use in ".CGI::b("set $setID$probNum"), +# CGI::hidden(-name=>'action.make_local_copy.target_file', -value=>$self->determineLocalFilePath($editFilePath) ), +# CGI::hidden(-name=>'action.make_local_copy.source_file', -value=>$editFilePath ), +# CGI::hidden(-name=>'action.make_local_copy.file_type',-value=>$self->{file_type}), +# CGI::hidden(-name=>'action.make_local_copy.saveMode',-value=>'rename') +# ); +# } +# +# +# sub make_local_copy_handler { +# my ($self, $genericParams, $actionParams, $tableParams) = @_; +# foreach my $key (qw(target_file file_type saveMode source_file)) { +# $actionParams->{"action.save_as.$key"}->[0] = $actionParams->{"action.make_local_copy.$key"}->[0]; +# #warn "action.make_local_copy.$key", @{$actionParams->{"action.make_local_copy.$key"}} +# } +# save_as_handler($self, $genericParams, $actionParams, $tableParams); +# +# +# } +sub save_as_form { # calls the save_as_handler + my ($self, $onChange, %actionParams) = @_; + my $editFilePath = $self->{editFilePath}; +# return "" unless -w $editFilePath; ## DPVC -- we don't need to be able to write the original in order to make a copy + + + my $templatesDir = $self->r->ce->{courseDirs}->{templates}; + my $setID = $self->{setID}; + my $fullSetID = $self->{fullSetID}; + + + my $shortFilePath = $editFilePath; + $shortFilePath =~ s|^$templatesDir/||; + $shortFilePath = 'local/'.$shortFilePath unless( $shortFilePath =~m|^local/|); # suggest that modifications be saved to the "local" subdirectory + $shortFilePath =~ s|^.*/|| if $shortFilePath =~ m|^/|; # if it is still an absolute path don't suggest a file path to save to. + + + my $probNum = ($self->{file_type} eq 'problem')? "/problem $self->{problemID}" : ""; + my $andRelink = ''; +# $andRelink = CGI::br().' and '.CGI::checkbox( +# -name => "action.save_as.saveMode", +# -value => "rename", +# -label => "", +# -checked => 1, +# -onclick=>$onChange +# ). +# " use in ".CGI::b("set $fullSetID$probNum") +# if not_blank($setID) && $setID ne 'Undefined_Set' && +# $self->{file_type} ne 'blank_problem'; + + my $can_add_problem_to_set = not_blank($setID) && $setID ne 'Undefined_Set' && $self->{file_type} ne 'blank_problem'; + # don't addor replace problems to sets if the set is the Undefined_Set or if the problem is the blank_problem. + + my $replace_problem_in_set = ($can_add_problem_to_set)? + # CGI::input({ + # -type => 'radio', + # -name => "action.save_as.saveMode", + # -value => "rename", + # -label => '', + # },"and replace ".CGI::b("set $fullSetID$probNum").',') + WeBWorK::CGI_labeled_input(-type=>'radio', -id=>'action_save_as_saveMode_rename_id', -label_text=>"Replace ".CGI::b("set $fullSetID$probNum"), -input_attr=>{ + -name => "action.save_as.saveMode", + -value => "rename", + }).CGI::br() : '' + ; + my $add_problem_to_set = ($can_add_problem_to_set)? + # CGI::input({ + # -type => 'radio', + # -name => "action.save_as.saveMode", + # -value => 'add_to_set_as_new_problem', + # -label => '', + # -onfocus => $onChange, + # },"and append to end of set $fullSetID",) : '' + WeBWorK::CGI_labeled_input(-type=>'radio', -id=>"action_save_as_saveMode_new_problem_id", -label_text=>"Append to end of set $fullSetID", -input_attr=>{ + -name => "action.save_as.saveMode", + -value => 'add_to_set_as_new_problem', + -onfocus => $onChange, + }).CGI::br() : '' + ; + my $rh_new_problem_options = { + # -type => 'radio', + -name => "action.save_as.saveMode", + -value => "new_independent_problem", + -onfocus => $onChange, + }; + $rh_new_problem_options->{checked}=1 unless $can_add_problem_to_set; + my $create_new_problem = WeBWorK::CGI_labeled_input(-type=>'radio', -id=>"action_save_as_saveMode_independent_problem_id", -label_text=>"Append as new independent problem", -input_attr=>$rh_new_problem_options).CGI::br(); #CGI::input($rh_new_problem_options,"as a new independent problem"); + + $andRelink = CGI::br(). $replace_problem_in_set . $add_problem_to_set . $create_new_problem; + + return #'Save AS [TMPL]/'. + # CGI::textfield( + # -name=>'action.save_as.target_file', -size=>60, -value=>"$shortFilePath", + # -onfocus=>$onChange + # ).",". + WeBWorK::CGI_labeled_input(-type=>"text", -id=>"action_save_as_target_file_id", -label_text=>"Save AS [TMPL]/", -input_attr=>{ + -name=>'action.save_as.target_file', -size=>60, -value=>"$shortFilePath", + -onfocus=>$onChange + }). + CGI::hidden(-name=>'action.save_as.source_file', -value=>$editFilePath ). + CGI::hidden(-name=>'action.save_as.file_type',-value=>$self->{file_type}). + $andRelink; +} +# suggestions for improvement +# save as ...... +# ¥replacing foobar (rename) ¥ and add to set (add_new_problem) ¥ as an independent file (new_independent_problem) + +sub save_as_handler { + my ($self, $genericParams, $actionParams, $tableParams) = @_; + my $r = $self->r; + #$self->addgoodmessage("save_as_handler called"); + $self->{status_message} = ''; ## DPVC -- remove bogus old messages + my $courseName = $self->{courseID}; + my $setName = $self->{setID}; + my $fullSetName = $self->{fullSetID}; + my $problemNumber = $self->{problemID}; + my $displayMode = $self->{displayMode}; + my $problemSeed = $self->{problemSeed}; + my $effectiveUserName = $self->r->param('effectiveUser'); + + my $do_not_save = 0; + my $saveMode = $actionParams->{'action.save_as.saveMode'}->[0] || 'no_save_mode_selected'; + my $new_file_name = $actionParams->{'action.save_as.target_file'}->[0] || ''; + my $sourceFilePath = $actionParams->{'action.save_as.source_file'}->[0] || ''; + my $file_type = $actionParams->{'action.save_as.file_type'}->[0] || ''; + $self ->{sourceFilePath} = $sourceFilePath; # store for use in saveFileChanges + $new_file_name =~ s/^\s*//; #remove initial and final white space + $new_file_name =~ s/\s*$//; + if ( $new_file_name !~ /\S/) { # need a non-blank file name + # setting $self->{failure} stops saving and any redirects + $do_not_save = 1; + $self->addbadmessage(CGI::p("Please specify a file to save to.")); + } + + ################################################# + # grab the problemContents from the form in order to save it to a new permanent file + # later we will unlink (delete) the current temporary file + ################################################# + my $problemContents = fixProblemContents($self->r->param('problemContents')); + $self->{r_problemContents} = \$problemContents; + warn "problem contents is empty" unless $problemContents; + ################################################# + # Rescue the user in case they forgot to end the file name with .pg + ################################################# + + if($file_type eq 'problem' + or $file_type eq 'blank_problem' + or $file_type eq 'set_header') { + $new_file_name =~ s/\.pg$//; # remove it if it is there + $new_file_name .= '.pg'; # put it there + + } + ################################################# + # Construct the output file path + ################################################# + my $outputFilePath = $self->r->ce->{courseDirs}->{templates} . '/' . + $new_file_name; + if (defined $outputFilePath and -e $outputFilePath) { + # setting $do_not_save stops saving and any redirects + $do_not_save = 1; + $self->addbadmessage(CGI::p("File '".$self->shortPath($outputFilePath)."' exists. + File not saved. No changes have been made. + You can change the file path for this problem manually from the 'Hmwk Sets Editor' page")); + $self->addgoodmessage(CGI::p("The text box now contains the source of the original problem.". + " You can recover lost edits by using the Back button on your browser.")); + } else { + $self->{editFilePath} = $outputFilePath; + $self->{tempFilePath} = ''; # nothing needs to be unlinked. + $self->{inputFilePath} = ''; + } + + + unless ($do_not_save ) { + $self->saveFileChanges($outputFilePath); + + if ($saveMode eq 'rename' and -r $outputFilePath) { + ################################################# + # Modify source file path in problem + ################################################# + if ($file_type eq 'set_header' ) { + my $setRecord = $self->r->db->getGlobalSet($setName); + $setRecord->set_header($new_file_name); + if ($self->r->db->putGlobalSet($setRecord)) { + $self->addgoodmessage("The set header for set $setName has been renamed to '".$self->shortPath($outputFilePath)."'.") ; + } else { + $self->addbadmessage("Unable to change the set header for set $setName. Unknown error."); + } + } elsif ($file_type eq 'hardcopy_header' ) { + my $setRecord = $self->r->db->getGlobalSet($setName); + $setRecord->hardcopy_header($new_file_name); + if ($self->r->db->putGlobalSet($setRecord)) { + $self->addgoodmessage("The hardcopy header for set $setName has been renamed to '".$self->shortPath($outputFilePath)."'.") ; + } else { + $self->addbadmessage("Unable to change the hardcopy header for set $setName. Unknown error."); + } + } else { + my $problemRecord; + if ( $fullSetName =~ /,v(\d+)$/ ) { + $problemRecord = $self->r->db->getMergedProblemVersion($effectiveUserName, $setName, $1, $problemNumber); + } else { + $problemRecord = $self->r->db->getGlobalProblem($setName,$problemNumber); + } + $problemRecord->source_file($new_file_name); + my $result = ( $fullSetName =~ /,v(\d+)$/ ) ? + $self->r->db->putProblemVersion($problemRecord) : + $self->r->db->putGlobalProblem($problemRecord); + if ( $result ) { + $self->addgoodmessage("The source file for 'set $fullSetName / problem $problemNumber' has been changed from ". + $self->shortPath($sourceFilePath)." to '".$self->shortPath($outputFilePath)."'.") ; + } else { + $self->addbadmessage("Unable to change the source file path for set $fullSetName, problem $problemNumber. Unknown error."); + } + } + } elsif ($saveMode eq 'add_to_set_as_new_problem') { + my $targetProblemNumber = 1+ WeBWorK::Utils::max( $self->r->db->listGlobalProblems($setName)); + my $problemRecord = $self->addProblemToSet( + setName => $setName, + sourceFile => $new_file_name, + problemID => $targetProblemNumber, #added to end of set + ); + $self->assignProblemToAllSetUsers($problemRecord); + $self->addgoodmessage("Added $new_file_name to ". $setName. " as problem $targetProblemNumber") ; + } elsif ($saveMode eq 'new_independent_problem') { + ################################################# + # Don't modify source file path in problem -- just report + ################################################# + + #$self->{status_message} = ''; ## DPVC remove old messages + $self->addgoodmessage("A new file has been created at '".$self->shortPath($outputFilePath). + "' with the contents below. No changes have been made to set $setName."); + } else { + $self->addbadmessage("Don't recognize saveMode: |$saveMode|. Unknown error."); + } + + } + my $edit_level = $self->r->param("edit_level") || 0; + $edit_level++; + + ################################################# + # Set up redirect + # The redirect gives the server time to detect that the new file exists. + ################################################# + my $problemPage; + my $new_file_type; + + if ($saveMode eq 'new_independent_problem' ) { + $problemPage = $self->r->urlpath->newFromModule("WeBWorK::ContentGenerator::Instructor::PGProblemEditor2",$r, + courseID => $courseName, setID => 'Undefined_Set', problemID => 'Undefined_Set' + ); + $new_file_type = 'source_path_for_problem_file'; + } elsif ($saveMode eq 'rename') { + $problemPage = $self->r->urlpath->newFromModule("WeBWorK::ContentGenerator::Instructor::PGProblemEditor2",$r, + courseID => $courseName, setID => $setName, problemID => $problemNumber + ); + $new_file_type = $file_type; + } elsif ($saveMode eq 'add_to_set_as_new_problem') { + $problemPage = $self->r->urlpath->newFromModule("WeBWorK::ContentGenerator::Instructor::PGProblemEditor2",$r, + courseID => $courseName, setID => $setName, problemID => $problemNumber + ); + $new_file_type = $file_type; + } else { + $self->addbadmessage(" Please use radio buttons to choose the method for saving this file. Can't recognize saveMode: |$saveMode|."); + # can't continue since paths have not been properly defined. + return ""; + } + + #warn "save mode is $saveMode"; + + my $relativeOutputFilePath = $self->getRelativeSourceFilePath($outputFilePath); + + my $viewURL = $self->systemLink($problemPage, + params=>{ + sourceFilePath => $relativeOutputFilePath, #The path relative to the templates directory is required. + problemSeed => $problemSeed, + edit_level => $edit_level, + file_type => $new_file_type, + status_message => uri_escape($self->{status_message}) + + } + ); + + $self->reply_with_redirect($viewURL); + return ""; # no redirect needed +} +sub revert_form { + my ($self, $onChange, %actionParams) = @_; + my $editFilePath = $self->{editFilePath}; + return "Error: The original file $editFilePath cannot be read." unless -r $editFilePath; + return "" unless defined($self->{tempFilePath}) and -e $self->{tempFilePath} ; + return "Revert to ".$self->shortPath($editFilePath) ; + +} +sub revert_handler { + my ($self, $genericParams, $actionParams, $tableParams) = @_; + my $ce = $self->r->ce; + #$self->addgoodmessage("revert_handler called"); + my $editFilePath = $self->{editFilePath}; + $self->{inputFilePath} = $editFilePath; + # unlink the temp files; + die "tempFilePath is unsafe!" unless path_is_subdir($self->{tempFilePath}, $ce->{courseDirs}->{templates}, 1); # 1==path can be relative to dir + unlink($self->{tempFilePath}); + $self->addgoodmessage("Deleting temp file at " . $self->shortPath($self->{tempFilePath})); + $self->{tempFilePath} = ''; + my $problemContents =''; + $self->{r_problemContents} = \$problemContents; + $self->addgoodmessage("Reverting to original file '".$self->shortPath($editFilePath)."'"); + # no redirect is needed +} + +sub output_JS{ + my $self = shift; + my $r = $self->r; + my $ce = $r->ce; + + my $site_url = $ce->{webworkURLs}->{htdocs}; + print CGI::start_script({type=>"text/javascript", src=>"$site_url/js/addOnLoadEvent.js"}), CGI::end_script(); + print CGI::start_script({type=>"text/javascript", src=>"$site_url/js/tabber.js"}), CGI::end_script(); + + return ""; +} + +sub output_tabber_CSS{ + return ""; +} + + +1; diff --git a/lib/WeBWorK/ContentGenerator/Instructor/ProblemSetDetail.pm b/lib/WeBWorK/ContentGenerator/Instructor/ProblemSetDetail.pm index 1d48afa2d1..9c8b0a5bb6 100644 --- a/lib/WeBWorK/ContentGenerator/Instructor/ProblemSetDetail.pm +++ b/lib/WeBWorK/ContentGenerator/Instructor/ProblemSetDetail.pm @@ -80,7 +80,7 @@ use constant GATEWAY_SET_FIELD_ORDER => [qw(version_time_limit time_limit_cap at use constant BLANKPROBLEM => 'blankProblem.pg'; -use constant FIELD_PROPERTIES => { +use constant FIELD_PROPERTIES => { # Set information set_header => { name => "Set Header", @@ -373,8 +373,8 @@ sub FieldTable { if ($forUsers) { $output .= CGI::Tr({}, CGI::th({colspan=>"2"}, " "), - CGI::th({colspan=>"1"}, "User Values"), - CGI::th({}, "Class values"), + CGI::th({colspan=>"1"}, $r->maketext("User Values")), + CGI::th({}, $r->maketext("Class values")), ); } foreach my $field (@fieldOrder) { @@ -421,7 +421,7 @@ sub FieldTable { if (defined $problemID) { #my $problemRecord = $r->{db}->getUserProblem($userID, $setID, $problemID); my $problemRecord = $userRecord; # we get this from the caller, hopefully - $output .= CGI::Tr({}, CGI::td({}, ["","Attempts", ($problemRecord->num_correct || 0) + ($problemRecord->num_incorrect || 0)])) if $forOneUser; + $output .= CGI::Tr({}, CGI::td({}, ["",$r->maketext("Attempts"), ($problemRecord->num_correct || 0) + ($problemRecord->num_incorrect || 0)])) if $forOneUser; } $output .= CGI::end_table(); @@ -452,8 +452,8 @@ sub FieldHTML { # #$mergedRecord = $db->getMergedSet($userID, $setID); # never user --sam #} - return "No data exists for set $setID and problem $problemID" unless $globalRecord; - return "No user specific data exists for user $userID" if $forOneUser and $globalRecord and not $userRecord; + return $r->maketext("No data exists for set [_1] and problem [_2]", $setID, $problemID) unless $globalRecord; + return $r->maketext("No user specific data exists for user [_1]", $userID) if $forOneUser and $globalRecord and not $userRecord; my %properties = %{ FIELD_PROPERTIES()->{$field} }; my %labels = %{ $properties{labels} }; @@ -551,6 +551,10 @@ sub FieldHTML { } elsif ( ! $value ) { $value = ($forUsers && $userRecord->$field ne '' ? $userRecord->$field : $globalRecord->$field); } + + foreach my $l (keys %labels){ + $labels{$l} = $r->maketext($labels{$l}); + } $inputType = CGI::popup_menu({ name => "$recordType.$recordID.$field", @@ -571,7 +575,7 @@ sub FieldHTML { value => $field, checked => $r->param("$recordType.$recordID.$field.override") || ($userValue ne ($labels{""} || $blankfield) ? 1 : 0), }) : "", - $properties{name}, + $r->maketext($properties{name}), $inputType, $forUsers ? " $gDisplVal" : "", ); @@ -582,6 +586,7 @@ sub FieldHTML { sub extraSetFields { my ($self,$userID,$setID,$globalRecord,$userRecord,$forUsers) = @_; my $db = $self->r->{db}; + my $r = $self->r; my ($gwFields, $ipFields, $ipDefaults, $numLocations, $ipOverride, $procFields) = ( '', '', '', 0, '', '' ); @@ -612,7 +617,7 @@ sub extraSetFields { } } $gwhdr .= CGI::Tr({},CGI::td({colspan=>$nF}, - CGI::em("Gateway parameters"))) + CGI::em($r->maketext("Gateway parameters")))) if ( $nF ); $gwFields = "$gwhdr$gwFields\n" . "\n"; @@ -624,11 +629,7 @@ sub extraSetFields { my $nfm1 = $nF - 1; $procFields = CGI::Tr({},CGI::td({},''), CGI::td({colspan=>$nfm1}, - CGI::em("Proctored tests require proctor " . - "authorization to start and to " . - "grade. Provide a password to have " . - "a single password for all students " . - "to start a proctored test."))); + CGI::em($r->maketext("Proctored tests require proctor authorization to start and to grade. Provide a password to have a single password for all students to start a proctored test.")))); # we use a routine other than FieldHTML because of getting # the default value here my @fieldData = @@ -678,7 +679,7 @@ sub extraSetFields { checked => $ipOverride }) : ''; $ipFields .= CGI::Tr({-valign=>'top'}, CGI::td({}, [ $override, - 'Restrict Locations', + $r->maketext('Restrict Locations'), $ipSelector, $forUsers ? " $ipDefaults" : '', ] @@ -708,7 +709,7 @@ sub proctoredFieldHTML { } return( ( '', - 'Password (Leave blank for regular proctoring)', + $r->maketext('Password (Leave blank for regular proctoring)'), CGI::input({ name=>"set.$setID.restricted_login_proctor_password", value=>$value, size=>10, @@ -727,6 +728,8 @@ sub problem_number_popup { # handles rearrangement necessary after changes to problem ordering sub handle_problem_numbers { + my $self = shift; + my $r = $self->r; my $newProblemNumbersref = shift; my %newProblemNumbers = %$newProblemNumbersref; my $maxNum = shift; @@ -754,7 +757,7 @@ sub handle_problem_numbers { push @sortme, [$j, $val]; # replace new problem numbers in hash with the (global) problems themselves $newProblemNumbers{$j} = $db->getGlobalProblem($setID, $j); - die "global $j for set $setID not found." unless $newProblemNumbers{$j}; + die $r->maketext("global [_1] for set [_2] not found.", $j, $setID) unless $newProblemNumbers{$j}; } # we don't have to do anything if we're not getting rid of holes @@ -813,7 +816,7 @@ sub handle_problem_numbers { $db->putUserProblem($problist[$sortme[$j][0]]); } } else { - warn "UserProblem missing for user=$user set=$setID problem=$sortme[$j][0]. This may indicate database corruption.\n"; + warn $r->maketext("UserProblem missing for user=[_1] set=[_2] problem=[_3]. This may indicate database corruption.", $user, $setID, $sortme[$j][0])."\n"; # when a problem doesn't exist in the target slot, a new problem gets added there, but the original problem # never gets overwritten (because there wan't a problem it would have to get exchanged with) # i think this can get pretty complex. consider 1=>2, 2=>3, 3=>4, 4=>1 where problem 1 doesn't exist for some user: @@ -864,7 +867,7 @@ sub initialize { } my $setRecord = $db->getGlobalSet($setID); # checked - die "global set $setID not found." unless $setRecord; + die $r->maketext("global set [_1] not found.", $setID) unless $setRecord; $self->{set} = $setRecord; my @editForUser = $r->param('editForUser'); @@ -918,12 +921,12 @@ sub initialize { # make sure dates are numeric by using ||0 if ($answer_date < $due_date || $answer_date < $open_date) { - $self->addbadmessage("Answers cannot be made available until on or after the due date!"); + $self->addbadmessage($r->maketext("Answers cannot be made available until on or after the due date!")); $error = $r->param('submit_changes'); } if ($due_date < $open_date) { - $self->addbadmessage("Answers cannot be due until on or after the open date!"); + $self->addbadmessage($r->maketext("Answers cannot be due until on or after the open date!")); $error = $r->param('submit_changes'); } @@ -932,21 +935,21 @@ sub initialize { my $seconds_per_year = 31_556_926; my $cutoff = $curr_time + $seconds_per_year*10; if ($open_date > $cutoff) { - $self->addbadmessage("Error: open date cannot be more than 10 years from now in set $setID"); + $self->addbadmessage($r->maketext("Error: open date cannot be more than 10 years from now in set [_1]", $setID)); $error = $r->param('submit_changes'); } if ($due_date > $cutoff) { - $self->addbadmessage("Error: due date cannot be more than 10 years from now in set $setID"); + $self->addbadmessage($r->maketext("Error: due date cannot be more than 10 years from now in set [_1]", $setID)); $error = $r->param('submit_changes'); } if ($answer_date > $cutoff) { - $self->addbadmessage("Error: answer date cannot be more than 10 years from now in set $setID"); + $self->addbadmessage($r->maketext("Error: answer date cannot be more than 10 years from now in set [_1]", $setID)); $error = $r->param('submit_changes'); } } if ($error) { - $self->addbadmessage("No changes were saved!"); + $self->addbadmessage($r->maketext("No changes were saved!")); } if (defined $r->param('submit_changes') && !$error) { @@ -1194,7 +1197,7 @@ sub initialize { my $dbPass; eval { $dbPass = $db->getPassword($procID) }; if ( $@ ) { - $self->addbadmessage("Error getting old set-proctor password from the database: $@. No update to the password was done."); + $self->addbadmessage($r->maketext("Error getting old set-proctor password from the database: [_1]. No update to the password was done.", $@)); } else { $dbPass->password(cryptPassword($pass)); $db->putPassword($dbPass); @@ -1218,9 +1221,7 @@ sub initialize { # put these into the database eval { $db->addUser($procUser) }; if ( $@ ) { - $self->addbadmessage("Error " . - "adding set-level " . - "proctor: $@"); + $self->addbadmessage($r->maketext("Error adding set-level proctor: [_1]", $@)); } else { $db->addPermissionLevel($procPerm); $db->addPassword($procPass); @@ -1254,7 +1255,7 @@ sub initialize { my @problemRecords = $db->getGlobalProblems(map { [$setID, $_] } @problemIDs); foreach my $problemRecord (@problemRecords) { my $problemID = $problemRecord->problem_id; - die "Global problem $problemID for set $setID not found." unless $problemRecord; + die $r->maketext("Global problem [_1] for set [_2] not found.", $problemID, $setID) unless $problemRecord; if ($forUsers) { # Since we're editing for specific users, we don't allow the GlobalProblem record to be altered on that same page @@ -1291,7 +1292,7 @@ sub initialize { if ($field eq 'source_file') { # add message if ( $param =~ /\.\./ || $param =~ /^\// ) { - $self->addbadmessage( "Source file paths cannot include .. or start with /: your source file path was modified." ); + $self->addbadmessage( $r->maketext("Source file paths cannot include .. or start with /: your source file path was modified.") ); } $param =~ s|\.\.||g; # prevent access to files above template $param =~ s|^/||; # prevent access to files above template @@ -1317,7 +1318,7 @@ sub initialize { if ($field eq 'source_file') { # add message if ( $param =~ /\.\./ || $param =~ /^\// ) { - $self->addbadmessage( "Source file paths cannot include .. or start with /: your source file path was modified." ); + $self->addbadmessage( $r->maketext("Source file paths cannot include .. or start with /: your source file path was modified.")); } $param =~ s|\.\.||g; # prevent access to files above template $param =~ s|^/||; # prevent access to files above template @@ -1351,7 +1352,7 @@ sub initialize { if ($field eq 'source_file') { # add message if ( $param =~ /\.\./ || $param =~ /^\// ) { - $self->addbadmessage( "Source file paths cannot include .. or start with /: your source file path was modified." ); + $self->addbadmessage( $r->maketext("Source file paths cannot include .. or start with /: your source file path was modified.") ); } $param =~ s|\.\.||g; # prevent access to files above template $param =~ s|^/||; # prevent access to files above template @@ -1463,7 +1464,7 @@ sub initialize { my $new_file_path = "set$setID/".BLANKPROBLEM(); my $fullPath = WeBWorK::Utils::surePathToFile($ce->{courseDirs}->{templates},'/'.$new_file_path); local(*TEMPFILE); - open(TEMPFILE, ">$fullPath") or warn "Can't write to file $fullPath"; + open(TEMPFILE, ">$fullPath") or warn $r->maketext("Can't write to file [_1]", $fullPath); print TEMPFILE $problemContents; close(TEMPFILE); @@ -1476,10 +1477,10 @@ sub initialize { problemID => $targetProblemNumber, #added to end of set ); $self->assignProblemToAllSetUsers($problemRecord); - $self->addgoodmessage("Added $new_file_path to ". $setID. " as problem $targetProblemNumber") ; + $self->addgoodmessage($r->maketext("Added [_1] to [_2] as problem [_3]", $new_file_path, $setID, $targetProblemNumber)) ; } } else { - $self->addbadmessage("Could not add $newBlankProblems problems to this set. The number must be between 1 and $MAX_NEW_PROBLEMS"); + $self->addbadmessage($r->maketext("Could not add [_1] problems to this set. The number must be between 1 and [_2]", $newBlankProblems, $MAX_NEW_PROBLEMS)); } } @@ -1554,8 +1555,8 @@ sub checkFile ($) { my $r = $self->r; my $ce = $r->ce; - return "No source filePath specified" unless $filePath; - return "Problem source is drawn from a grouping set" if $filePath =~ /^group/; + return $r->maketext("No source filePath specified") unless $filePath; + return $r->maketext("Problem source is drawn from a grouping set") if $filePath =~ /^group/; if ( $filePath eq "defaultHeader" ) { if ($headerType eq 'set_header') { @@ -1563,20 +1564,19 @@ sub checkFile ($) { } elsif ($headerType eq 'hardcopy_header') { $filePath = $ce->{webworkFiles}{hardcopySnippets}{setHeader}; } else { - return "Invalid headerType $headerType" + return $r->maketext("Invalid headerType [_1]", $headerType); } } else { # $filePath = $ce->{courseDirs}->{templates} . '/' . $filePath unless $filePath =~ m|^/|; # bug: 1725 allows access to all files e.g. /etc/passwd $filePath = $ce->{courseDirs}->{templates} . '/' . $filePath ; # only filePaths in template directory can be accessed } - my $text = "This source file "; my $fileError; return "" if -e $filePath && -f $filePath && -r $filePath; - return $text . "is not readable!" if -e $filePath && -f $filePath; - return $text . "is a directory!" if -d $filePath; - return $text . "does not exist!" unless -e $filePath; - return $text . "is not a plain file!"; + return $r->maketext("This source file is not readable!") if -e $filePath && -f $filePath; + return $r->maketext("This source file is a directory!") if -d $filePath; + return $r->maketext("This source file does not exist!") unless -e $filePath; + return $r->maketext("This source file is not a plain file!"); } # don't show view options -- we provide display mode controls for headers/problems separately @@ -1608,20 +1608,19 @@ sub body { $setID =~ s/,v(\d+)$//; } - my $setRecord = $db->getGlobalSet($setID) or die "No record for global set $setID."; + my $setRecord = $db->getGlobalSet($setID) or die $r->maketext("No record for global set [_1].", $setID); - my $userRecord = $db->getUser($userID) or die "No record for user $userID."; + my $userRecord = $db->getUser($userID) or die $r->maketext("No record for user [_1].", $userID); # Check permissions - return CGI::div({class=>"ResultsWithError"}, "You are not authorized to access the Instructor tools.") + return CGI::div({class=>"ResultsWithError"}, $r->maketext("You are not authorized to access the Instructor tools.")) unless $authz->hasPermissions($userRecord->user_id, "access_instructor_tools"); - return CGI::div({class=>"ResultsWithError"}, "You are not authorized to modify problems.") + return CGI::div({class=>"ResultsWithError"}, $r->maketext("You are not authorized to modify problems.")) unless $authz->hasPermissions($userRecord->user_id, "modify_problem_sets"); my @editForUser = $r->param('editForUser'); - return CGI::div({class=>"ResultsWithError"}, "Versions of a set can only be " . - "edited for one user at a time.") if ( $editingSetVersion && @editForUser != 1 ); + return CGI::div({class=>"ResultsWithError"}, $r->maketext("Versions of a set can only be edited for one user at a time.")) if ( $editingSetVersion && @editForUser != 1 ); # Check that every user that we're editing for has a valid UserSet my @assignedUsers; @@ -1639,10 +1638,10 @@ sub body { $r->param("editForUser", \@editForUser); if (scalar @editForUser && scalar @unassignedUsers) { - print CGI::div({class=>"ResultsWithError"}, "The following users are NOT assigned to this set and will be ignored: " . CGI::b(join(", ", @unassignedUsers))); + print CGI::div({class=>"ResultsWithError"}, $r->maketext("The following users are NOT assigned to this set and will be ignored: [_1]", CGI::b(join(", ", @unassignedUsers))) ); } elsif (scalar @editForUser == 0) { - print CGI::div({class=>"ResultsWithError"}, "None of the selected users are assigned to this set: " . CGI::b(join(", ", @unassignedUsers))); - print CGI::div({class=>"ResultsWithError"}, "Global set data will be shown instead of user specific data"); + print CGI::div({class=>"ResultsWithError"}, $r->maketext("None of the selected users are assigned to this set: [_1]", CGI::b(join(", ", @unassignedUsers)))); + print CGI::div({class=>"ResultsWithError"}, $r->maketext("Global set data will be shown instead of user specific data")); } } @@ -1653,7 +1652,7 @@ sub body { # and check that if we're editing a set version for a user, that # it exists as well if ( $editingSetVersion && ! $db->existsSetVersion( $editForUser[0], $setID, $editingSetVersion ) ) { - return CGI::div({class=>"ResultsWithError"}, "The set-version ($setID, version $editingSetVersion) is not assigned to user $editForUser[0]."); + return CGI::div({class=>"ResultsWithError"}, $r->maketext("The set-version ([_1], version [_2]) is not assigned to user [_3].", $setID, $editingSetVersion, $editForUser[0])); } # If you're editing for users, initially their records will be different but @@ -1689,8 +1688,8 @@ sub body { my $userCountMessage = CGI::a({href=>$editUsersAssignedToSetURL}, $self->userCountMessage($setUserCount, $userCount)); my $setCountMessage = CGI::a({href=>$editSetsAssignedToUserURL}, $self->setCountMessage($userSetCount, $setCount)) if $forOneUser; - $userCountMessage = "The set $setID is assigned to " . $userCountMessage . "."; - $setCountMessage = "The user $editForUser[0] has been assigned " . $setCountMessage . "." if $forOneUser; + $userCountMessage = $r->maketext("The set [_1] is assigned to [_2].", $setID, $userCountMessage); + $setCountMessage = $r->maketext("The user [_1] has been assigned [_2].", $editForUser[0], $setCountMessage) if $forOneUser; if ($forUsers) { ############################################## @@ -1704,7 +1703,7 @@ sub body { CGI::a({-href=>"mailto:$email_address"},"email "). $u->user_id . "). "; if ( ! $editingSetVersion ) { - $line .= "Assigned to "; + $line .= $r->maketext("Assigned to "); my $editSetsAssignedToUserURL = $self->systemLink( $urlpath->newFromModule( "WeBWorK::ContentGenerator::Instructor::UserDetail", $r, @@ -1716,8 +1715,7 @@ sub body { my $editSetLink = $self->systemLink( $setDetailPage, params=>{effectiveUser=>$u->user_id, editForUser =>$u->user_id} ); - $line .= "Edit set " . CGI::a({href=>$editSetLink},$setID) . - " for this user."; + $line .= $r->maketext("Edit set [_1] for this user.", CGI::a({href=>$editSetLink},$setID)); } unshift @userLinks,$line; } @@ -1725,16 +1723,14 @@ sub body { # handy messages when editing gateway sets my $gwmsg = ( $isGatewaySet && ! $editingSetVersion ) ? - CGI::br() . CGI::em("To edit a specific student version of this set, " . - "edit (all of) her/his assigned sets.") : ""; - my $vermsg = ( $editingSetVersion ) ? ", test $editingSetVersion" : ""; + CGI::br() . CGI::em($r->maketext("To edit a specific student version of this set, edit (all of) her/his assigned sets.")) : ""; + my $vermsg = ( $editingSetVersion ) ? ", $editingSetVersion" : ""; print CGI::table({border=>2,cellpadding=>10}, CGI::Tr({}, CGI::td([ - "Editing problem set ".CGI::strong($setID . $vermsg)." data for these individual students:".CGI::br(). - CGI::strong(join CGI::br(), @userLinks), - CGI::a({href=>$self->systemLink($setDetailPage) },"Edit set ".CGI::strong($setID)." data for ALL students assigned to this set.") . $gwmsg, + $r->maketext("Editing problem set [_1] data for these individual students: [_2]", CGI::strong($setID . $vermsg), CGI::br().CGI::strong(join CGI::br(), @userLinks)), + CGI::a({href=>$self->systemLink($setDetailPage) },$r->maketext("Edit set [_1] data for ALL students assigned to this set.", CGI::strong($setID))) . $gwmsg, ]) ) @@ -1743,8 +1739,8 @@ sub body { print CGI::table({border=>2,cellpadding=>10}, CGI::Tr({}, CGI::td([ - "This set ".CGI::strong($setID)." is assigned to ".$self->userCountMessage($setUserCount, $userCount).'.' , - 'Edit '.CGI::a({href=>$editUsersAssignedToSetURL},'individual versions '). "of set $setID.", + $r->maketext("This set [_1] is assigned to [_2].", CGI::strong($setID), $self->userCountMessage($setUserCount, $userCount)) , + $r->maketext("Edit [_1] of set [_2].", CGI::a({href=>$editUsersAssignedToSetURL},$r->maketext('individual versions')), $setID), ]) ) @@ -1762,15 +1758,15 @@ sub body { } my $forceRenumber = $r->param('force_renumber') || 0; - handle_problem_numbers(\%newProblemNumbers, $maxProblemNumber, $db, $setID, $forceRenumber) unless defined $r->param('undo_changes'); + handle_problem_numbers($self,\%newProblemNumbers, $maxProblemNumber, $db, $setID, $forceRenumber) unless defined $r->param('undo_changes'); my %properties = %{ FIELD_PROPERTIES() }; my %display_modes = %{WeBWorK::PG::DISPLAY_MODES()}; my @active_modes = grep { exists $display_modes{$_} } @{$r->ce->{pg}->{displayModes}}; - push @active_modes, 'None'; - my $default_header_mode = $r->param('header.displaymode') || 'None'; - my $default_problem_mode = $r->param('problem.displaymode') || 'None'; + push @active_modes, $r->maketext('None'); + my $default_header_mode = $r->param('header.displaymode') || $r->maketext('None'); + my $default_problem_mode = $r->param('problem.displaymode') || $r->maketext('None'); ##################################################################### # Browse available header/problem files @@ -1790,17 +1786,16 @@ sub body { # Display a useful warning message if ($forUsers) { - print CGI::p(CGI::b("Any changes made below will be reflected in the set for ONLY the student" . - ($forOneUser ? "" : "s") . " listed above.")); + print CGI::p(CGI::b($r->maketext("Any changes made below will be reflected in the set for ONLY the student(s) listed above."))); } else { - print CGI::p(CGI::b("Any changes made below will be reflected in the set for ALL students.")); + print CGI::p(CGI::b($r->maketext("Any changes made below will be reflected in the set for ALL students."))); } print CGI::start_form({method=>"POST", action=>$setDetailURL}); print $self->hiddenEditForUserFields(@editForUser); print $self->hidden_authen_fields; - print CGI::input({type=>"submit", name=>"submit_changes", value=>"Save Changes"}); - print CGI::input({type=>"submit", name=>"undo_changes", value => "Reset Form"}); + print CGI::input({type=>"submit", name=>"submit_changes", value=>$r->maketext("Save Changes")}); + print CGI::input({type=>"submit", name=>"undo_changes", value => $r->maketext("Reset Form")}); # spacing print CGI::p(); @@ -1811,7 +1806,7 @@ sub body { print CGI::start_table({border=>1, cellpadding=>4}); print CGI::Tr({}, CGI::th({}, [ - "General Information", + $r->maketext("General Information"), ])); # this is kind of a hack -- we need to get a user record here, so we can @@ -1846,11 +1841,11 @@ sub body { print CGI::start_table({border=>1, cellpadding=>4}); print CGI::Tr({}, CGI::th({}, [ - "Headers", + $r->maketext("Headers"), # "Data", "Display Mode: " . CGI::popup_menu(-name => "header.displaymode", -values => \@active_modes, -default => $default_header_mode) . ' '. - CGI::input({type => "submit", name => "refresh", value => "Refresh Display"}), + CGI::input({type => "submit", name => "refresh", value => $r->maketext("Refresh Display")}), ])); my %header_html; @@ -1903,17 +1898,16 @@ sub body { if ( $headerType eq 'set_header' && $guaranteed_set->assignment_type =~ /gateway/ ) { print CGI::Tr({}, CGI::td({}, - [ "Set Header", - "Set headers are not used in " . - "display of gateway tests."])); + [ $r->maketext("Set Header"), + $r->maketext("Set headers are not used in display of gateway tests.")])); next; } print CGI::Tr({}, CGI::td({}, [ CGI::start_table({border => 0, cellpadding => 0}) . CGI::Tr({}, CGI::td({}, $properties{$headerType}->{name})) . - CGI::Tr({}, CGI::td({}, CGI::a({href => $editHeaderLink, target=>"WW_Editor"}, "Edit it"))) . - CGI::Tr({}, CGI::td({}, CGI::a({href => $viewHeaderLink, target=>"WW_View"}, "View it"))) . + CGI::Tr({}, CGI::td({}, CGI::a({href => $editHeaderLink, target=>"WW_Editor"}, $r->maketext("Edit it")))) . + CGI::Tr({}, CGI::td({}, CGI::a({href => $viewHeaderLink, target=>"WW_View"}, $r->maketext("View it")))) . CGI::end_table(), comboBox({ @@ -1922,7 +1916,7 @@ sub body { default => $r->param("set.$setID.$headerType") || $setRecord->{$headerType} || "defaultHeader", multiple => 0, values => ["defaultHeader", @headerFileList], - labels => { "defaultHeader" => "Use Default Header File" }, + labels => { "defaultHeader" => $r->maketext("Use Default Header File") }, }) . ($error{$headerType} ? CGI::div({class=>"ResultsWithError", style=>"font-weight: bold"}, $error{$headerType}) @@ -1933,7 +1927,7 @@ sub body { print CGI::end_table(); } else { - print CGI::p(CGI::b("Screen and Hardcopy set header information can not be overridden for individual students.")); + print CGI::p(CGI::b($r->maketext("Screen and Hardcopy set header information can not be overridden for individual students."))); } # spacing @@ -1972,11 +1966,11 @@ sub body { print CGI::start_table({border=>1, cellpadding=>4}); print CGI::Tr({}, CGI::th({}, [ - "Problems", - "Data", + $r->maketext("Problems"), + $r->maketext("Data"), "Display Mode: " . CGI::popup_menu(-name => "problem.displaymode", -values => \@active_modes, -default => $default_problem_mode) . ' '. - CGI::input({type => "submit", name => "refresh", value => "Refresh Display"}), + CGI::input({type => "submit", name => "refresh", value => $r->maketext("Refresh Display")}), ])); my %shownYet; @@ -2038,7 +2032,7 @@ sub body { $problemFile =~ s|\.\.||g; # warn of repeat problems if (defined $shownYet{$problemFile}) { - $repeatFile = "This problem uses the same source file as number " . $shownYet{$problemFile} . "."; + $repeatFile = $r->maketext("This problem uses the same source file as number [_1].", $shownYet{$problemFile}); } else { $shownYet{$problemFile} = $problemID; $repeatFile = ""; @@ -2072,12 +2066,12 @@ sub body { CGI::start_table({border => 0, cellpadding => 1}) . CGI::Tr({}, CGI::td({}, problem_number_popup($problemID, $maxProblemNumber))) . CGI::Tr({}, CGI::td({}, - $showLinks ? CGI::a({href => $editProblemLink, target=>"WW_Editor"}, "Edit it") : "" )) . + $showLinks ? CGI::a({href => $editProblemLink, target=>"WW_Editor"}, $r->maketext("Edit it")) : "" )) . CGI::Tr({}, CGI::td({}, - $showLinks ? CGI::a({href => $viewProblemLink, target=>"WW_View"}, "Try it" . ($forOneUser ? " (as $editForUser[0])" : "")) : "" )) . - ($forUsers ? "" : CGI::Tr({}, CGI::td({}, CGI::checkbox({name => "deleteProblem", value => $problemID, label => "Delete it?"})))) . + $showLinks ? CGI::a({href => $viewProblemLink, target=>"WW_View"}, $r->maketext("Try it") . ($forOneUser ? " (as $editForUser[0])" : "")) : "" )) . + ($forUsers ? "" : CGI::Tr({}, CGI::td({}, CGI::checkbox({name => "deleteProblem", value => $problemID, label => $r->maketext("Delete it?")})))) . # CGI::Tr({}, CGI::td({}, "Delete it?" . CGI::input({type => "checkbox", name => "deleteProblem", value => $problemID}))) . - ($forOneUser ? "" : CGI::Tr({}, CGI::td({}, CGI::checkbox({name => "markCorrect", value => $problemID, label => "Mark Correct?"})))) . + ($forOneUser ? "" : CGI::Tr({}, CGI::td({}, CGI::checkbox({name => "markCorrect", value => $problemID, label => $r->maketext("Mark Correct?")})))) . CGI::end_table(), $self->FieldTable($userToShow, $setID, $problemID, $GlobalProblems{$problemID}, $problemToShow, $isGatewaySet), # A comprehensive list of problems is just TOO big to be handled well @@ -2110,19 +2104,14 @@ sub body { # print final lines print CGI::end_table(); print CGI::checkbox({ - label=> "Force problems to be numbered consecutively from one (always done when reordering problems)", + label=> $r->maketext("Force problems to be numbered consecutively from one (always done when reordering problems)"), name=>"force_renumber", value=>"1"}); - print CGI::p(<open_date>time()); - print CGI::p("When changing problem numbers, we will move the problem to be ". CGI::em("before"). " the chosen number."); + print CGI::p($r->maketext("Any time problem numbers are intentionally changed, the problems will always be renumbered consecutively, starting from one. When deleting problems, gaps will be left in the numbering unless the box above is checked.")); + print CGI::p($r->maketext("It is before the open date. You probably want to renumber the problems if you are deleting some from the middle.")) if ($setRecord->open_date>time()); + print CGI::p($r->maketext("When changing problem numbers, we will move the problem to be [_1] the chosen number.",CGI::em($r->maketext("before")))); } else { - print CGI::p(CGI::b("This set doesn't contain any problems yet.")); + print CGI::p(CGI::b($r->maketext("This set doesn't contain any problems yet."))); } # always allow one to add a new problem, unless we're editing a set version if ( ! $editingSetVersion ) { @@ -2132,13 +2121,13 @@ EOF name=>"add_n_problems", size=>2, value=>1 }, - "blank problem template(s) to end of homework set" + $r->maketext("blank problem template(s) to end of homework set") ); } print CGI::br(),CGI::br(), CGI::input({type=>"submit", name=>"submit_changes", value=>"Save Changes"}), CGI::input({type=>"submit", name=>"handle_numbers", value=>"Reorder problems only"}), - "(Any unsaved changes will be lost.)"; + $r->maketext("(Any unsaved changes will be lost.)"); #my $editNewProblemPage = $urlpath->new(type => 'instructor_problem_editor_withset_withproblem', args => { courseID => $courseID, setID => $setID, problemID =>'new_problem' }); #my $editNewProblemLink = $self->systemLink($editNewProblemPage, params => { make_local_copy => 1, file_type => 'blank_problem' }); diff --git a/lib/WeBWorK/ContentGenerator/Instructor/ProblemSetList2.pm b/lib/WeBWorK/ContentGenerator/Instructor/ProblemSetList2.pm new file mode 100644 index 0000000000..74e53d7415 --- /dev/null +++ b/lib/WeBWorK/ContentGenerator/Instructor/ProblemSetList2.pm @@ -0,0 +1,2493 @@ +################################################################################ +# WeBWorK Online Homework Delivery System +# Copyright © 2000-2007 The WeBWorK Project, http://openwebwork.sf.net/ +# $CVSHeader: +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of either: (a) the GNU General Public License as published by the +# Free Software Foundation; either version 2, or (at your option) any later +# version, or (b) the "Artistic License" which comes with this package. +# +# This program 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 either the GNU General Public License or the +# Artistic License for more details. +################################################################################ + +package WeBWorK::ContentGenerator::Instructor::ProblemSetList2; +use base qw(WeBWorK); +use base qw(WeBWorK::ContentGenerator::Instructor); + +=head1 NAME + +WeBWorK::ContentGenerator::Instructor::ProblemSetList2 - Entry point for Set-specific +data editing/viewing + +=cut + +=for comment + +What do we want to be able to do here? + +filter sort edit publish import create delete + +Filter what sets are shown: + - none, all, selected + - matching set_id, visible to students, hidden from students + +Sort sets by: + - set name + - open date + - due date + - answer date + - header files + - visibility to students + +Switch from view mode to edit mode: + - showing visible sets + - showing selected sets +Switch from edit mode to view and save changes +Switch from edit mode to view and abandon changes + +Make sets visible to or hidden from students: + - all, selected + +Import sets: + - replace: + - any users + - visible users + - selected users + - no users + - add: + - any users + - no users + +Score sets: + - all + - visible + - selected + +Create a set with a given name + +Delete sets: + - visible + - selected + +=cut + +# FIXME: rather than having two types of boolean modes $editMode and $exportMode +# make one $mode variable that contains a string like "edit", "view", or "export" + +use strict; +use warnings; +#use CGI qw(-nosticky ); +use WeBWorK::CGI; +use WeBWorK::Debug; +use WeBWorK::Utils qw(timeToSec readFile listFilesRecursive cryptPassword sortByName); + +use constant HIDE_SETS_THRESHOLD => 500; +use constant DEFAULT_VISIBILITY_STATE => 1; +use constant DEFAULT_ENABLED_REDUCED_SCORING_STATE => 0; +use constant ONE_WEEK => 60*60*24*7; + +use constant EDIT_FORMS => [qw(cancelEdit saveEdit)]; +use constant VIEW_FORMS => [qw(filter sort edit publish import export score create delete)]; +use constant EXPORT_FORMS => [qw(cancelExport saveExport)]; + +use constant VIEW_FIELD_ORDER => [ qw( select set_id problems users visible enable_reduced_scoring open_date due_date answer_date) ]; +use constant EDIT_FIELD_ORDER => [ qw( set_id visible enable_reduced_scoring open_date due_date answer_date) ]; +use constant EXPORT_FIELD_ORDER => [ qw( select set_id filename) ]; + +# permissions needed to perform a given action +use constant FORM_PERMS => { + saveEdit => "modify_problem_sets", + edit => "modify_problem_sets", + publish => "modify_problem_sets", + import => "create_and_delete_problem_sets", + export => "modify_set_def_files", + saveExport => "modify_set_def_files", + score => "score_sets", + create => "create_and_delete_problem_sets", + delete => "create_and_delete_problem_sets", +}; + +# permissions needed to view a given field +use constant FIELD_PERMS => { + problems => "modify_problem_sets", + users => "assign_problem_sets", +}; + +use constant STATE_PARAMS => [qw(user effectiveUser key visible_sets no_visible_sets prev_visible_sets no_prev_visible_set editMode exportMode primarySortField secondarySortField)]; + +use constant SORT_SUBS => { + set_id => \&bySetID, +# set_header => \&bySetHeader, # can't figure out why these are useful +# hardcopy_header => \&byHardcopyHeader, # can't figure out why these are useful + open_date => \&byOpenDate, + due_date => \&byDueDate, + answer_date => \&byAnswerDate, + visible => \&byVisible, + +}; + +# note that field_properties for some fields, in particular, gateway +# parameters, are not currently shown in the edit or display tables +use constant FIELD_PROPERTIES => { + set_id => { + type => "text", + size => 8, + access => "readonly", + }, + set_header => { + type => "filelist", + size => 10, + access => "readonly", + }, + hardcopy_header => { + type => "filelist", + size => 10, + access => "readonly", + }, + open_date => { + type => "text", + size => 26, + access => "readwrite", + }, + due_date => { + type => "text", + size => 26, + access => "readwrite", + }, + answer_date => { + type => "text", + size => 26, + access => "readwrite", + }, + visible => { + type => "checked", + size => 4, + access => "readwrite", + }, + enable_reduced_scoring => { + type => "checked", + size => 4, + access => "readwrite", + }, + assignment_type => { + type => "text", + size => 20, + access => "readwrite", + }, + attempts_per_version => { + type => "text", + size => 4, + access => "readwrite", + }, + time_interval => { + type => "text", + size => 10, + access => "readwrite", + }, + versions_per_interval => { + type => "text", + size => 4, + access => "readwrite", + }, + version_time_limit => { + type => "text", + size => 10, + access => "readwrite", + }, + problem_randorder => { + type => "text", + size => 4, + access => "readwrite", + }, + problems_per_page => { + type => "text", + size => 4, + access => "readwrite", + }, + version_creation_time => { + type => "text", + size => 10, + access => "readonly", + }, + version_last_attempt_time => { + type => "text", + size => 10, + access => "readonly", + }, + # hide_score and hide_work should be drop down selects with + # options 'N', 'Y' and 'BeforeAnswerDate'. in that we don't + # allow editing of these fields in this module, this is moot. + hide_score => { + type => "text", + size => 16, + access => "readwrite", + }, + hide_work => { + type => "text", + size => 16, + access => "readwrite", + }, + time_limit_cap => { + type => "checked", + size => 4, + access => "readwrite", + }, + # this should be 'No', 'RestrictTo' or 'DenyFrom' + restrict_ip => { + type => "text", + size => 10, + access => "readwrite", + } +}; + +sub pre_header_initialize { + my ($self) = @_; + my $r = $self->r; + my $db = $r->db; + my $ce = $r->ce; + my $authz = $r->authz; + my $urlpath = $r->urlpath; + my $user = $r->param('user'); + my $courseName = $urlpath->arg("courseID"); + + + # Check permissions + return unless $authz->hasPermissions($user, "access_instructor_tools"); + + if (defined $r->param("action") and $r->param("action") eq "score" and $authz->hasPermissions($user, "score_sets")) { + my $scope = $r->param("action.score.scope"); + my @setsToScore = (); + + if ($scope eq "none") { + return $r->maketext("No sets selected for scoring"."."); + } elsif ($scope eq "all") { + @setsToScore = @{ $r->param("allSetIDs") }; + } elsif ($scope eq "visible") { + @setsToScore = @{ $r->param("visibleSetIDs") }; + } elsif ($scope eq "selected") { + @setsToScore = $r->param("selected_sets"); + } + + my $uri = $self->systemLink( $urlpath->newFromModule('WeBWorK::ContentGenerator::Instructor::Scoring', $r, courseID=>$courseName), + params=>{ + scoreSelected=>"ScoreSelected", + selectedSet=>\@setsToScore, +# recordSingleSetScores=>'' + } + ); + + $self->reply_with_redirect($uri); + } + +} + +sub initialize { + + my ($self) = @_; + my $r = $self->r; + my $urlpath = $r->urlpath; + my $db = $r->db; + my $ce = $r->ce; + my $authz = $r->authz; + my $courseName = $urlpath->arg("courseID"); + my $setID = $urlpath->arg("setID"); + my $user = $r->param('user'); + + + my $root = $ce->{webworkURLs}->{root}; + + # templates for getting field names + my $setTemplate = $self->{setTemplate} = $db->newGlobalSet; + + return CGI::div({class => "ResultsWithError"}, $r->maketext("You are not authorized to access the instructor tools.")) + unless $authz->hasPermissions($user, "access_instructor_tools"); + + ########## set initial values for state fields + + my @allSetIDs = $db->listGlobalSets; + # DBFIXME count would suffice here :P + my @users = $db->listUsers; + $self->{allSetIDs} = \@allSetIDs; + $self->{totalUsers} = scalar @users; + + if (defined $r->param("visible_sets")) { + $self->{visibleSetIDs} = [ $r->param("visible_sets") ]; + } elsif (defined $r->param("no_visible_sets")) { + $self->{visibleSetIDs} = []; + } else { + if (@allSetIDs > HIDE_SETS_THRESHOLD) { + $self->{visibleSetIDs} = []; + } else { + $self->{visibleSetIDs} = [ @allSetIDs ]; + } + } + + $self->{prevVisibleSetIDs} = $self->{visibleSetIDs}; + + if (defined $r->param("selected_sets")) { + $self->{selectedSetIDs} = [ $r->param("selected_sets") ]; + } else { + $self->{selectedSetIDs} = []; + } + + $self->{editMode} = $r->param("editMode") || 0; + + return CGI::div({class=>"ResultsWithError"}, CGI::p($r->maketext("You are not authorized to modify homework sets."))) + if $self->{editMode} and not $authz->hasPermissions($user, "modify_problem_sets"); + + $self->{exportMode} = $r->param("exportMode") || 0; + + return CGI::div({class=>"ResultsWithError"}, CGI::p($r->maketext("You are not authorized to modify set definition files."))) + if $self->{exportMode} and not $authz->hasPermissions($user, "modify_set_def_files"); + + $self->{primarySortField} = $r->param("primarySortField") || "due_date"; + $self->{secondarySortField} = $r->param("secondarySortField") || "open_date"; + + + ######################################### + # collect date information from sets + ######################################### + + my @allSets = $db->getGlobalSets(@allSetIDs); + + my (%open_dates, %due_dates, %answer_dates); + foreach my $Set (@allSets) { + push @{$open_dates{defined $Set->open_date ? $Set->open_date : ""}}, $Set->set_id; + push @{$due_dates{defined $Set->due_date ? $Set->due_date : ""}}, $Set->set_id; + push @{$answer_dates{defined $Set->answer_date ? $Set->answer_date : ""}}, $Set->set_id; + } + $self->{open_dates} = \%open_dates; + $self->{due_dates} = \%due_dates; + $self->{answer_dates} = \%answer_dates; + + ######################################### + # call action handler + ######################################### + + my $actionID = $r->param("action"); + $self->{actionID} = $actionID; + if ($actionID) { + unless (grep { $_ eq $actionID } @{ VIEW_FORMS() }, @{ EDIT_FORMS() }, @{ EXPORT_FORMS() }) { + die $r->maketext("Action [_1] not found", $actionID); + } + # Check permissions + if (not FORM_PERMS()->{$actionID} or $authz->hasPermissions($user, FORM_PERMS()->{$actionID})) { + my $actionHandler = "${actionID}_handler"; + my %genericParams; + foreach my $param (qw(selected_sets)) { + $genericParams{$param} = [ $r->param($param) ]; + } + my %actionParams = $self->getActionParams($actionID); + my %tableParams = $self->getTableParams(); + $self->addmessage(CGI::div({class=>"Message"}, $r->maketext("Results of last action performed").": ")); + $self->addmessage($self->$actionHandler(\%genericParams, \%actionParams, \%tableParams)); + } else { + return CGI::div({class=>"ResultsWithError"}, CGI::p($r->maketext("You are not authorized to perform this action."))); + } + + + + } else { + + $self->addgoodmessage($r->maketext("Please select action to be performed.")); + } + + +} + +sub body { + my ($self) = @_; + my $r = $self->r; + my $urlpath = $r->urlpath; + my $db = $r->db; + my $ce = $r->ce; + my $authz = $r->authz; + my $courseName = $urlpath->arg("courseID"); + my $setID = $urlpath->arg("setID"); + my $user = $r->param('user'); + + my $root = $ce->{webworkURLs}->{root}; + + # templates for getting field names + my $setTemplate = $self->{setTemplate} = $db->newGlobalSet; + + return CGI::div({class => "ResultsWithError"}, $r->maketext("You are not authorized to access the instructor tools.")) + unless $authz->hasPermissions($user, "access_instructor_tools"); + + # This table can be consulted when display-ready forms of field names are needed. + my %prettyFieldNames = map { $_ => $_ } + $setTemplate->FIELDS(); + + @prettyFieldNames{qw( + problems + users + filename + set_id + set_header + hardcopy_header + open_date + due_date + answer_date + visible + enable_reduced_scoring + )} = ( + $r->maketext("Edit Problems"), + $r->maketext("Edit Assigned Users"), + $r->maketext("Set Definition Filename"), + $r->maketext("Edit Set Data"), + $r->maketext("Set Header"), + $r->maketext("Hardcopy Header"), + $r->maketext("Open Date"), + $r->maketext("Due Date"), + $r->maketext("Answer Date"), + $r->maketext("Visible"), + $r->maketext("Reduced Credit Enabled") + ); + + + + my $actionID = $self->{actionID}; + + ########## retrieve possibly changed values for member fields + + my @allSetIDs = @{ $self->{allSetIDs} }; # do we need this one? YES, deleting or importing a set will change this. + my @visibleSetIDs = @{ $self->{visibleSetIDs} }; + my @prevVisibleSetIDs = @{ $self->{prevVisibleSetIDs} }; + my @selectedSetIDs = @{ $self->{selectedSetIDs} }; + my $editMode = $self->{editMode}; + my $exportMode = $self->{exportMode}; + my $primarySortField = $self->{primarySortField}; + my $secondarySortField = $self->{secondarySortField}; + + #warn "visibleSetIDs=@visibleSetIDs\n"; + #warn "prevVisibleSetIDs=@prevVisibleSetIDs\n"; + #warn "selectedSetIDs=@selectedSetIDs\n"; + #warn "editMode=$editMode\n"; + #warn "exportMode = $exportMode\n"; + + ########## get required users + + # DBFIXME use an iterator + my @Sets = grep { defined $_ } @visibleSetIDs ? $db->getGlobalSets(@visibleSetIDs) : (); + + # presort users + my %sortSubs = %{ SORT_SUBS() }; + my $primarySortSub = $sortSubs{$primarySortField}; + my $secondarySortSub = $sortSubs{$secondarySortField}; + + # don't forget to sort in opposite order of importance + if ($secondarySortField eq "set_id") { + @Sets = sortByName("set_id", @Sets); + } else { + @Sets = sort $secondarySortSub @Sets; + } + + if ($primarySortField eq "set_id") { + @Sets = sortByName("set_id", @Sets); + } else { + @Sets = sort $primarySortSub @Sets; + } + + ########## print site identifying information + + print WeBWorK::CGI_labeled_input(-type=>"button", -id=>"show_hide", -input_attr=>{-value=>$r->maketext("Show/Hide Site Description"), -class=>"button_input"}); + print CGI::p({-id=>"site_description", -style=>"display:none"}, CGI::em($r->maketext("_HMWKSETS_EDITOR_DESCRIPTION"))); + + ########## print beginning of form + + print CGI::start_form({method=>"post", action=>$self->systemLink($urlpath,authen=>0), name=>"problemsetlist", -class=>"edit_form"}); + print $self->hidden_authen_fields(); + + ########## print state data + + print "\n\n"; + + if (@visibleSetIDs) { + print CGI::hidden(-name=>"visible_sets", -value=>\@visibleSetIDs); + } else { + print CGI::hidden(-name=>"no_visible_sets", -value=>"1"); + } + + if (@prevVisibleSetIDs) { + print CGI::hidden(-name=>"prev_visible_sets", -value=>\@prevVisibleSetIDs); + } else { + print CGI::hidden(-name=>"no_prev_visible_sets", -value=>"1"); + } + + print CGI::hidden(-name=>"editMode", -value=>$editMode); + print CGI::hidden(-name=>"exportMode", -value=>$exportMode); + + print CGI::hidden(-name=>"primarySortField", -value=>$primarySortField); + print CGI::hidden(-name=>"secondarySortField", -value=>$secondarySortField); + + print "\n\n"; + + ########## print action forms + + print CGI::p(CGI::b($r->maketext("Any changes made below will be reflected in the set for ALL students."))) if $editMode; + + # print CGI::start_table({}); + print CGI::p($r->maketext("Select an action to perform").":"); + + my @formsToShow; + if ($editMode) { + @formsToShow = @{ EDIT_FORMS() }; + } else { + @formsToShow = @{ VIEW_FORMS() }; + } + + if ($exportMode) { + @formsToShow = @{ EXPORT_FORMS() }; + } + + my $i = 0; + my @divArr = (); + + foreach my $actionID (@formsToShow) { + # Check permissions + next if FORM_PERMS()->{$actionID} and not $authz->hasPermissions($user, FORM_PERMS()->{$actionID}); + my $actionForm = "${actionID}_form"; + #my $onChange = "document.problemsetlist.action[$i].checked=true"; + my $onChange = ""; + my %actionParams = $self->getActionParams($actionID); + + # print CGI::Tr({-valign=>"top"}, + # CGI::td({}, CGI::input({-type=>"radio", -name=>"action", -value=>$actionID})), + # CGI::td({}, $self->$actionForm($onChange, %actionParams)) + # ); + + my $extraspace = (ucfirst(WeBWorK::split_cap($actionID)) eq "Filter") ? "" : CGI::br(); + + push @divArr, join("", + CGI::h3($r->maketext(ucfirst(WeBWorK::split_cap($actionID)))), + CGI::span({-class=>"radio_span"}, WeBWorK::CGI_labeled_input(-type=>"radio", -id=>$actionID."_id", -label_text=>$r->maketext(ucfirst(WeBWorK::split_cap($actionID))), -input_attr=>{-name=>"action", -value=>$actionID}, -label_attr=>{-class=>"radio_label"})), + CGI::br(), + $self->$actionForm($onChange, %actionParams), + CGI::br(), + $extraspace, + ); + $i++; + } + + my $divArrRef = \@divArr; + + print CGI::div({-class=>"tabber"}, + CGI::div({-class=>"tabbertab"},$divArrRef) + ); + + my $selectAll =WeBWorK::CGI_labeled_input(-type=>'button', -id=>"select_all", -input_attr=>{-name=>'check_all', -value=>$r->maketext('Select all sets'), + onClick => "for (i in document.problemsetlist.elements) { + if (document.problemsetlist.elements[i].name =='selected_sets') { + document.problemsetlist.elements[i].checked = true + } + }" }); + my $selectNone =WeBWorK::CGI_labeled_input(-type=>'button', -id=>"select_none", -input_attr=>{-name=>'check_none', -value=>$r->maketext('Unselect all sets'), + onClick => "for (i in document.problemsetlist.elements) { + if (document.problemsetlist.elements[i].name =='selected_sets') { + document.problemsetlist.elements[i].checked = false + } + }" }); + unless ($editMode or $exportMode) { + print $selectAll." ". $selectNone; + } + print WeBWorK::CGI_labeled_input(-type=>"reset", -id=>"clear_entries", -input_attr=>{-value=>$r->maketext("Clear"), -class=>"button_input"}); + print WeBWorK::CGI_labeled_input(-type=>"submit", -id=>"take_action", -input_attr=>{-value=>$r->maketext("Take Action!"), -class=>"button_input"}).CGI::br().CGI::br(); + + ########## print table + + ########## first adjust heading if in editMode + $prettyFieldNames{set_id} = $r->maketext("Edit All Set Data") if $editMode; + $prettyFieldNames{enable_reduced_scoring} = $r->maketext('Enable Reduced Credit') if $editMode; + + + print CGI::p({},$r->maketext("Showing [_1] out of [_2] sets.", scalar @visibleSetIDs, scalar @allSetIDs)); + + $self->printTableHTML(\@Sets, \%prettyFieldNames, + editMode => $editMode, + exportMode => $exportMode, + selectedSetIDs => \@selectedSetIDs, + ); + + + ########## print end of form + + print CGI::end_form(); + + return ""; +} + +################################################################################ +# extract particular params and put them in a hash (values are ARRAYREFs!) +################################################################################ + +sub getActionParams { + my ($self, $actionID) = @_; + my $r = $self->{r}; + + my %actionParams; + foreach my $param ($r->param) { + next unless $param =~ m/^action\.$actionID\./; + $actionParams{$param} = [ $r->param($param) ]; + } + return %actionParams; +} + +sub getTableParams { + my ($self) = @_; + my $r = $self->{r}; + + my %tableParams; + foreach my $param ($r->param) { + next unless $param =~ m/^(?:set)\./; + $tableParams{$param} = [ $r->param($param) ]; + } + return %tableParams; +} + +################################################################################ +# actions and action triggers +################################################################################ + +# filter, edit, cancelEdit, and saveEdit should stay with the display module and +# not be real "actions". that way, all actions are shown in view mode and no +# actions are shown in edit mode. + +sub filter_form { + my ($self, $onChange, %actionParams) = @_; + my $r = $self->r; + + return join("", + WeBWorK::CGI_labeled_input( + -type=>"select", + -id=>"filter_select", + -label_text=>$r->maketext("Show which sets?").": ", + -input_attr=>{ + -name => "action.filter.scope", + -values => [qw(all none selected match_ids visible unvisible)], + -default => $actionParams{"action.filter.scope"}->[0] || "match_ids", + -labels => { + all => $r->maketext("all sets"), + none => $r->maketext("no sets"), + selected => $r->maketext("selected sets"), + visible => $r->maketext("visible sets"), + unvisible => $r->maketext("hidden sets"), + match_ids => $r->maketext("enter matching set IDs below"), + }, + -onchange => $onChange, + } + ), + CGI::br(), + " ", + CGI::div({-id=>"filter_elements"}, + WeBWorK::CGI_labeled_input( + -type=>"text", + -id=>"filter_text", + -label_text=>$r->maketext("Match on what? (separate multiple IDs with commas)").": ", + -input_attr=>{ + -name => "action.filter.set_ids", + -value => $actionParams{"action.filter.set_ids"}->[0] || "",, + -width => "50", + -onchange => $onChange, + } + ), CGI::span({-id=>"filter_err_msg", -class=>"ResultsWithError"}, $r->maketext("Please enter in a value to match in the filter field.")), + ), + ); +} + +# this action handler modifies the "visibleUserIDs" field based on the contents +# of the "action.filter.scope" parameter and the "selected_users" +sub filter_handler { + my ($self, $genericParams, $actionParams, $tableParams) = @_; + + my $r = $self->r ; + my $db = $r->db; + + my $result; + + my $scope = $actionParams->{"action.filter.scope"}->[0]; + + if ($scope eq "all") { + $result = $r->maketext("showing all sets"); + $self->{visibleSetIDs} = $self->{allSetIDs}; + } elsif ($scope eq "none") { + $result = $r->maketext("showing no sets"); + $self->{visibleSetIDs} = []; + } elsif ($scope eq "selected") { + $result = $r->maketext("showing selected sets"); + $self->{visibleSetIDs} = $genericParams->{selected_sets}; # an arrayref + } elsif ($scope eq "match_ids") { + #my @setIDs = split /\s*,\s*/, $actionParams->{"action.filter.set_ids"}->[0]; + my @setIDs = split /\s*,\s*/, $actionParams->{"action.filter.set_ids"}->[0]; + $self->{visibleSetIDs} = \@setIDs; + } elsif ($scope eq "match_open_date") { + my $open_date = $actionParams->{"action.filter.open_date"}->[0]; + $self->{visibleSetIDs} = $self->{open_dates}->{$open_date}; # an arrayref + } elsif ($scope eq "match_due_date") { + my $due_date = $actionParams->{"action.filter.due_date"}->[0]; + $self->{visibleSetIDs} = $self->{due_date}->{$due_date}; # an arrayref + } elsif ($scope eq "match_answer_date") { + my $answer_date = $actionParams->{"action.filter.answer_date"}->[0]; + $self->{visibleSetIDs} = $self->{answer_dates}->{$answer_date}; # an arrayref + } elsif ($scope eq "visible") { + # DBFIXME do filtering in the database, please! + my @setRecords = $db->getGlobalSets(@{$self->{allSetIDs}}); + my @visibleSetIDs = map { $_->visible ? $_->set_id : ""} @setRecords; + $self->{visibleSetIDs} = \@visibleSetIDs; + } elsif ($scope eq "unvisible") { + # DBFIXME do filtering in the database, please! + my @setRecords = $db->getGlobalSets(@{$self->{allSetIDs}}); + my @unvisibleSetIDs = map { (not $_->visible) ? $_->set_id : ""} @setRecords; + $self->{visibleSetIDs} = \@unvisibleSetIDs; + } + + return $result; +} + +sub sort_form { + my ($self, $onChange, %actionParams) = @_; + my $r = $self->r; + return join ("", + WeBWorK::CGI_labeled_input( + -type=>"select", + -id=>"sort_select_1", + -label_text=>$r->maketext("Sort by").": ", + -input_attr=>{ + -name => "action.sort.primary", + -values => [qw(set_id set_header hardcopy_header open_date due_date answer_date visible)], + -default => $actionParams{"action.sort.primary"}->[0] || "due_date", + -labels => { + set_id => $r->maketext("Set Name"), + set_header => $r->maketext("Set Header"), + hardcopy_header => $r->maketext("Hardcopy Header"), + open_date => $r->maketext("Open Date"), + due_date => $r->maketext("Due Date"), + answer_date => $r->maketext("Answer Date"), + visible => $r->maketext("Visibility"), + }, + -onchange => $onChange, + } + ), + CGI::br(), + WeBWorK::CGI_labeled_input( + -type=>"select", + -id=>"sort_select_2", + -label_text=>$r->maketext("Then by").": ", + -input_attr=>{ + -name => "action.sort.secondary", + -values => [qw(set_id set_header hardcopy_header open_date due_date answer_date visible)], + -default => $actionParams{"action.sort.secondary"}->[0] || "open_date", + -labels => { + set_id => $r->maketext("Set Name"), + set_header => $r->maketext("Set Header"), + hardcopy_header => $r->maketext("Hardcopy Header"), + open_date => $r->maketext("Open Date"), + due_date => $r->maketext("Due Date"), + answer_date => $r->maketext("Answer Date"), + visible => $r->maketext("Visibility"), + }, + -onchange => $onChange, + } + ), + ); +} + +sub sort_handler { + my ($self, $genericParams, $actionParams, $tableParams) = @_; + my $r = $self->r; + + my $primary = $actionParams->{"action.sort.primary"}->[0]; + my $secondary = $actionParams->{"action.sort.secondary"}->[0]; + + $self->{primarySortField} = $primary; + $self->{secondarySortField} = $secondary; + + my %names = ( + set_id => $r->maketext("Set Name"), + set_header => $r->maketext("Set Header"), + hardcopy_header => $r->maketext("Hardcopy Header"), + open_date => $r->maketext("Open Date"), + due_date => $r->maketext("Due Date"), + answer_date => $r->maketext("Answer Date"), + visible => $r->maketext("Visibility"), + ); + + return $r->maketext("Sort by [_1] and then by [_2]", $names{$primary}, $names{$secondary}); +} + + +sub edit_form { + my ($self, $onChange, %actionParams) = @_; + my $r = $self->r; + + return join("", + WeBWorK::CGI_labeled_input( + -type=>"select", + -id=>"edit_select", + -label_text=>$r->maketext("Edit which sets?").": ", + -input_attr=>{ + -name => "action.edit.scope", + -values => [qw(all visible selected)], + -default => $actionParams{"action.edit.scope"}->[0] || "selected", + -labels => { + all => $r->maketext("all sets"), + visible => $r->maketext("visible sets"), + selected => $r->maketext("selected sets"), + }, + -onchange => $onChange, + } + ), + ); +} + +sub edit_handler { + my ($self, $genericParams, $actionParams, $tableParams) = @_; + my $r = $self->r; + + my $result; + + my $scope = $actionParams->{"action.edit.scope"}->[0]; + if ($scope eq "all") { + $result = $r->maketext("editing all sets"); + $self->{visibleSetIDs} = $self->{allSetIDs}; + } elsif ($scope eq "visible") { + $result = $r->maketext("editing visible sets"); + # leave visibleUserIDs alone + } elsif ($scope eq "selected") { + $result = $r->maketext("editing selected sets"); + $self->{visibleSetIDs} = $genericParams->{selected_sets}; # an arrayref + } + $self->{editMode} = 1; + + return $result; +} + +sub publish_form { + my ($self, $onChange, %actionParams) = @_; + my $r = $self->r; + + return join ("", + WeBWorK::CGI_labeled_input( + -type=>"select", + -id=>"publish_filter_select", + -label_text=>$r->maketext("Choose which sets to be affected").": ", + -input_attr=>{ + -name => "action.publish.scope", + -values => [ qw(none all selected) ], + -default => $actionParams{"action.publish.scope"}->[0] || "selected", + -labels => { + none => $r->maketext("no sets"), + all => $r->maketext("all sets"), +# visible => "visible sets", + selected => $r->maketext("selected sets"), + }, + -onchange => $onChange, + } + ), + CGI::br(), + WeBWorK::CGI_labeled_input( + -type=>"select", + -id=>"publish_visibility_select", + -label_text=>$r->maketext("Choose visibility of the sets to be affected").": ", + -input_attr=>{ + -name => "action.publish.value", + -values => [ 0, 1 ], + -default => $actionParams{"action.publish.value"}->[0] || "1", + -labels => { + 0 => $r->maketext("Hidden"), + 1 => $r->maketext("Visible"), + }, + -onchange => $onChange, + } + ), + ); +} + +sub publish_handler { + my ($self, $genericParams, $actionParams, $tableParams) = @_; + + my $r = $self->r; + my $db = $r->db; + + my $result = ""; + + my $scope = $actionParams->{"action.publish.scope"}->[0]; + my $value = $actionParams->{"action.publish.value"}->[0]; + + my $verb = $value ? $r->maketext("made visible for") : $r->maketext("hidden from"); + + my @setIDs; + + if ($scope eq "none") { # FIXME: double negative "Make no sets hidden" might make professor expect all sets to be made visible. + @setIDs = (); + $result = CGI::div({class=>"ResultsWithError"},$r->maketext("No change made to any set")); + } elsif ($scope eq "all") { + @setIDs = @{ $self->{allSetIDs} }; + $result = CGI::div({class=>"ResultsWithoutError"},$r->maketext("All sets [_1] all students", $verb)); + } elsif ($scope eq "visible") { + @setIDs = @{ $self->{visibleSetIDs} }; + $result = CGI::div({class=>"ResultsWithoutError"},$r->maketext("All visible sets [_1] all students", $verb)); + } elsif ($scope eq "selected") { + @setIDs = @{ $genericParams->{selected_sets} }; + $result = CGI::div({class=>"ResultsWithoutError"},$r->maketext("All selected sets [_1] all students", $verb)); + } + + # can we use UPDATE here, instead of fetch/change/store? + my @sets = $db->getGlobalSets(@setIDs); + + map { $_->visible("$value") if $_; $db->putGlobalSet($_); } @sets; + + return $result + +} +sub enable_reduced_scoring_form { + my ($self, $onChange, %actionParams) = @_; + my $r = $self->r; + + return join ("", + WeBWorK::CGI_labeled_input( + -type=>"select", + -id=>"reduced_scoring_filter_select", + -label_text=>$r->maketext("Choose which sets to be affected").": ", + -input_attr=>{ + -name => "action.enable_reduced_scoring.scope", + -values => [ qw(none all selected) ], + -default => $actionParams{"action.enable_reduced_scoring.scope"}->[0] || "selected", + -labels => { + none => $r->maketext("no sets"), + all => $r->maketext("all sets"), +# visible => "visible sets", + selected => $r->maketext("selected sets"), + }, + -onchange => $onChange, + } + ), + CGI::br(), + WeBWorK::CGI_labeled_input( + -type=>"select", + -id=>"reduced_scoring_enable_disable_select", + -label_text=>$r->maketext("Enable/Disable reduced scoring for selected sets").": ", + -input_attr=>{ + -name => "action.enable_reduced_scoring.value", + -values => [ 0, 1 ], + -default => $actionParams{"action.enable_reduced_scoring.value"}->[0] || "1", + -labels => { + 0 => $r->maketext("Disable"), + 1 => $r->maketext("Enable"), + }, + -onchange => $onChange, + } + ), + ); +} + +sub enable_reduced_scoring_handler { + my ($self, $genericParams, $actionParams, $tableParams) = @_; + + my $r = $self->r; + my $db = $r->db; + + my $result = ""; + + my $scope = $actionParams->{"action.enable_reduced_scoring.scope"}->[0]; + my $value = $actionParams->{"action.enable_reduced_scoring.value"}->[0]; + + my $verb = $value ? $r->maketext("enabled") : $r->maketext("disabled"); + + my @setIDs; + + if ($scope eq "none") { # FIXME: double negative "Make no sets hidden" might make professor expect all sets to be made visible. + @setIDs = (); + $result = CGI::div({class=>"ResultsWithError"}, $r->maketext("No change made to any set")); + } elsif ($scope eq "all") { + @setIDs = @{ $self->{allSetIDs} }; + $result = CGI::div({class=>"ResultsWithoutError"},$r->maketext("Reduced Credit [_1] for all sets", $verb)); + } elsif ($scope eq "visible") { + @setIDs = @{ $self->{visibleSetIDs} }; + $result = CGI::div({class=>"ResultsWithoutError"},$r->maketext("Reduced Credit [_1] for visable sets", $verb)); + } elsif ($scope eq "selected") { + @setIDs = @{ $genericParams->{selected_sets} }; + $result = CGI::div({class=>"ResultsWithoutError"},$r->maketext("Reduced Credit [_1] for selected sets", $verb)); + } + + # can we use UPDATE here, instead of fetch/change/store? + my @sets = $db->getGlobalSets(@setIDs); + + map { $_->enable_reduced_scoring("$value") if $_; $db->putGlobalSet($_); } @sets; + + return $result + +} + +sub score_form { + my ($self, $onChange, %actionParams) = @_; + my $r = $self->r; + + return join ("", + WeBWorK::CGI_labeled_input( + -type=>"select", + -id=>"score_select", + -label_text=>$r->maketext("Score which sets?").": ", + -input_attr=>{ + -name => "action.score.scope", + -values => [qw(none all selected)], + -default => $actionParams{"action.score.scope"}->[0] || "none", + -labels => { + none => $r->maketext("no sets"), + all => $r->maketext("all sets"), + selected => $r->maketext("selected sets"), + }, + -onchange => $onChange, + } + ), + ); + + + +} + +sub score_handler { + my ($self, $genericParams, $actionParams, $tableParams) = @_; + + my $r = $self->r; + my $urlpath = $r->urlpath; + my $courseName = $urlpath->arg("courseID"); + + my $scope = $actionParams->{"action.score.scope"}->[0]; + my @setsToScore; + + if ($scope eq "none") { + @setsToScore = (); + return $r->maketext("No sets selected for scoring"); + } elsif ($scope eq "all") { + @setsToScore = @{ $self->{allSetIDs} }; + } elsif ($scope eq "visible") { + @setsToScore = @{ $self->{visibleSetIDs} }; + } elsif ($scope eq "selected") { + @setsToScore = @{ $genericParams->{selected_sets} }; + } + + my $uri = $self->systemLink( $urlpath->newFromModule('WeBWorK::ContentGenerator::Instructor::Scoring',$r, courseID=>$courseName), + params=>{ + scoreSelected=>"Score Selected", + selectedSet=>\@setsToScore, +# recordSingleSetScores=>'' + } + ); + + + return $uri; +} + + +sub delete_form { + my ($self, $onChange, %actionParams) = @_; + my $r = $self->r; + + return join("", + CGI::span({-class=>"ResultsWithError"}, CGI::em($r->maketext("Warning: Deletion destroys all user-related data and is not undoable!"))),CGI::br(), + WeBWorK::CGI_labeled_input( + -type=>"select", + -id=>"delete_select", + -label_text=>$r->maketext("Delete how many?").": ", + -input_attr=>{ + -name => "action.delete.scope", + -values => [qw(none selected)], + -default => "none", # don't make it easy to delete # $actionParams{"action.delete.scope"}->[0] || "none", + -labels => { + none => $r->maketext("no sets"), + #visible => "visible sets.", + selected => $r->maketext("selected sets"), + }, + -onchange => $onChange, + } + ), + ); +} + +sub delete_handler { + my ($self, $genericParams, $actionParams, $tableParams) = @_; + + my $r = $self->r; + my $db = $r->db; + + my $scope = $actionParams->{"action.delete.scope"}->[0]; + + + my @setIDsToDelete = (); + + if ($scope eq "selected") { + @setIDsToDelete = @{ $self->{selectedSetIDs} }; + } + + my %allSetIDs = map { $_ => 1 } @{ $self->{allSetIDs} }; + my %visibleSetIDs = map { $_ => 1 } @{ $self->{visibleSetIDs} }; + my %selectedSetIDs = map { $_ => 1 } @{ $self->{selectedSetIDs} }; + + foreach my $setID (@setIDsToDelete) { + delete $allSetIDs{$setID}; + delete $visibleSetIDs{$setID}; + delete $selectedSetIDs{$setID}; + $db->deleteGlobalSet($setID); + } + + $self->{allSetIDs} = [ keys %allSetIDs ]; + $self->{visibleSetIDs} = [ keys %visibleSetIDs ]; + $self->{selectedSetIDs} = [ keys %selectedSetIDs ]; + + my $num = @setIDsToDelete; + return CGI::div({class=>"ResultsWithoutError"}, $r->maketext("deleted [_1] sets", $num) ); +} + +sub create_form { + my ($self, $onChange, %actionParams) = @_; + my $r = $self->r; + + return join("", + WeBWorK::CGI_labeled_input( + -type=>"text", + -id=>"create_text", + -label_text=>$r->maketext("Name the new set").": ", + -input_attr=>{ + -name => "action.create.name", + -value => $actionParams{"action.create.name"}->[0] || "", + -width => "50", + -onchange => $onChange, + } + ), + CGI::br(), + WeBWorK::CGI_labeled_input( + -type=>"select", + -id=>"create_select", + -label_text=>$r->maketext("Create as what type of set?").": ", + -input_attr=>{ + -name => "action.create.type", + -values => [qw(empty copy)], + -default => $actionParams{"action.create.type"}->[0] || "empty", + -labels => { + empty => $r->maketext("a new empty set"), + copy => $r->maketext("a duplicate of the first selected set"), + }, + -onchange => $onChange, + } + ), + ); + +} + +sub create_handler { + my ($self, $genericParams, $actionParams, $tableParams) = @_; + + my $r = $self->r; + my $db = $r->db; + + my $newSetID = $actionParams->{"action.create.name"}->[0]; + return CGI::div({class => "ResultsWithError"}, $r->maketext("Failed to create new set: no set name specified!")) unless $newSetID =~ /\S/; + return CGI::div({class => "ResultsWithError"}, $r->maketext("Set [_1] exists. No set created", $newSetID)) if $db->existsGlobalSet($newSetID); + my $newSetRecord = $db->newGlobalSet; + my $oldSetID = $self->{selectedSetIDs}->[0]; + + my $type = $actionParams->{"action.create.type"}->[0]; + # It's convenient to set the open date one week from now so that it is + # not accidentally available to students. We set the due and answer date + # to be two weeks from now. + + + if ($type eq "empty") { + $newSetRecord->set_id($newSetID); + $newSetRecord->set_header("defaultHeader"); + $newSetRecord->hardcopy_header("defaultHeader"); + $newSetRecord->open_date(time + ONE_WEEK()); + $newSetRecord->due_date(time + 2*ONE_WEEK() ); + $newSetRecord->answer_date(time + 2*ONE_WEEK() ); + $newSetRecord->visible(DEFAULT_VISIBILITY_STATE()); # don't want students to see an empty set + $newSetRecord->enable_reduced_scoring(DEFAULT_ENABLED_REDUCED_SCORING_STATE()); + $db->addGlobalSet($newSetRecord); + } elsif ($type eq "copy") { + return CGI::div({class => "ResultsWithError"}, $r->maketext("Failed to duplicate set: no set selected for duplication!")) unless $oldSetID =~ /\S/; + $newSetRecord = $db->getGlobalSet($oldSetID); + $newSetRecord->set_id($newSetID); + $db->addGlobalSet($newSetRecord); + + # take all the problems from the old set and make them part of the new set + foreach ($db->getAllGlobalProblems($oldSetID)) { + $_->set_id($newSetID); + $db->addGlobalProblem($_); + } + + # also copy any set_location restrictions and set-level proctor + # information + foreach ($db->getAllGlobalSetLocations($oldSetID)) { + $_->set_id($newSetID); + $db->addGlobalSetLocation($_); + } + if ( $newSetRecord->restricted_login_proctor eq 'Yes' ) { + my $procUser = $db->getUser("set_id:$oldSetID"); + $procUser->user_id("set_id:$newSetID"); + eval { $db->addUser( $procUser ) }; + if ( ! $@ ) { + my $procPerm = $db->getPermissionLevel("set_id:$oldSetID"); + $procPerm->user_id("set_id:$newSetID"); + $db->addPermissionLevel($procPerm); + my $procPass = $db->getPassword("set_id:$oldSetID"); + $procPass->user_id("set_id:$newSetID"); + $db->addPassword($procPass); + } + } + } + # Assign set to current active user + my $userName = $r->param('user'); # FIXME possible security risk + $self->assignSetToUser($userName, $newSetRecord); # cures weird date error when no-one assigned to set + $self->addgoodmessage("Set $newSetID was assigned to $userName."); # not currently used + + push @{ $self->{visibleSetIDs} }, $newSetID; + push @{ $self->{allSetIds} }, $newSetID; + + return CGI::div({class => "ResultsWithError"}, $r->maketext("Failed to create new set: [_1]", $@)) if $@; + + return CGI::div({class=>"ResultsWithoutError"},$r->maketext("Successfully created new set [_1]", $newSetID)); + +} + +sub import_form { + my ($self, $onChange, %actionParams) = @_; + + my $r = $self->r; + my $authz = $r->authz; + my $user = $r->param('user'); + + # this will make the popup menu alternate between a single selection and a multiple selection menu + # Note: search by name is required since document.problemsetlist.action.import.number is not seen as + # a valid reference to the object named 'action.import.number' + my $importScript = join (" ", + "var number = document.getElementsByName('action.import.number')[0].value;", + "document.getElementsByName('action.import.source')[0].size = number;", + "document.getElementsByName('action.import.source')[0].multiple = (number > 1 ? true : false);", + "document.getElementsByName('action.import.name')[0].value = (number > 1 ? '(taken from filenames)' : '');", + ); + + return join(" ", + WeBWorK::CGI_labeled_input( + -type=>"select", + -id=>"import_amt_select", + -label_text=>$r->maketext("Import how many sets?").": ", + -input_attr=>{ + -name => "action.import.number", + -values => [ 1, 8 ], + -default => $actionParams{"action.import.number"}->[0] || "1", + -labels => { + 1 => $r->maketext("a single set"), + 8 => $r->maketext("multiple sets"), + }, + -onchange => "$onChange;$importScript", + } + ), + CGI::br(), + WeBWorK::CGI_labeled_input( + -type=>"select", + -id=>"import_source_select", + -label_text=>$r->maketext("Import from where?").": ", + -input_attr=>{ + -name => "action.import.source", + -values => [ "", $self->getDefList() ], + -labels => { "" => $r->maketext("Enter filenames below") }, + -default => $actionParams{"action.import.source"}->[0] || "", + -size => $actionParams{"action.import.number"}->[0] || "1", + -onchange => $onChange, + }, + -label_attr=>{-id=>"import_source_select_label"} + ), + CGI::br(), + WeBWorK::CGI_labeled_input( + -type=>"text", + -id=>"import_text", + -label_text=>$r->maketext("Import sets with names").": ", + -input_attr=>{ + -name => "action.import.name", + -value => $actionParams{"action.import.name"}->[0] || "", + -width => "50", + -onchange => $onChange, + } + ), + CGI::br(), + ($authz->hasPermissions($user, "assign_problem_sets")) + ? + WeBWorK::CGI_labeled_input( + -type=>"select", + -id=>"import_users_select", + -label_text=>$r->maketext("Assign this set to which users?").": ", + -input_attr=>{ + -name => "action.import.assign", + -value => [qw(all none)], + -default => $actionParams{"action.import.assign"}->[0] || "none", + -labels => { + all => $r->maketext("all users"), + none => $r->maketext("no users"), + }, + -onchange => $onChange, + } + ) + : + "" #user does not have permissions to assign problem sets + ); +} + +sub import_handler { + my ($self, $genericParams, $actionParams, $tableParams) = @_; + my $r = $self->r; + + my @fileNames = @{ $actionParams->{"action.import.source"} }; + my $newSetName = $actionParams->{"action.import.name"}->[0]; + $newSetName = "" if $actionParams->{"action.import.number"}->[0] > 1; # cannot assign set names to multiple imports + my $assign = $actionParams->{"action.import.assign"}->[0]; + + my ($added, $skipped) = $self->importSetsFromDef($newSetName, $assign, @fileNames); + + # make new sets visible... do we really want to do this? probably. + push @{ $self->{visibleSetIDs} }, @$added; + push @{ $self->{allSetIDs} }, @$added; + + my $numAdded = @$added; + my $numSkipped = @$skipped; + + return CGI::div( + {class=>"ResultsWithoutError"}, $r->maketext("[_1] sets added, [_2] sets skipped. Skipped sets: ([_3])", $numAdded, $numSkipped, join(", ", @$skipped))); +} + +sub export_form { + my ($self, $onChange, %actionParams) = @_; + my $r = $self->r; + + return join("", + WeBWorK::CGI_labeled_input( + -type=>"select", + -id=>"export_select", + -label_text=>$r->maketext("Export which sets?").": ", + -input_attr=>{ + -name => "action.export.scope", + -values => [qw(all visible selected)], + -default => $actionParams{"action.export.scope"}->[0] || "visible", + -labels => { + all => $r->maketext("all sets"), + visible => $r->maketext("visible sets"), + selected => $r->maketext("selected sets"), + }, + -onchange => $onChange, + } + ), + ); +} + +# this does not actually export any files, rather it sends us to a new page in order to export the files +sub export_handler { + my ($self, $genericParams, $actionParams, $tableParams) = @_; + my $r = $self->r; + + my $result; + + my $scope = $actionParams->{"action.export.scope"}->[0]; + if ($scope eq "all") { + $result = $r->maketext("exporting all sets"); + $self->{selectedSetIDs} = $self->{visibleSetIDs} = $self->{allSetIDs}; + + } elsif ($scope eq "visible") { + $result = $r->maketext("exporting visible sets"); + $self->{selectedSetIDs} = $self->{visibleSetIDs}; + } elsif ($scope eq "selected") { + $result = $r->maketext("exporting selected sets"); + $self->{selectedSetIDs} = $self->{visibleSetIDs} = $genericParams->{selected_sets}; # an arrayref + } + $self->{exportMode} = 1; + + return CGI::div({class=>"ResultsWithoutError"}, $result); +} + +sub cancelExport_form { + my ($self, $onChange, %actionParams) = @_; + my $r = $self->r; + return CGI::span($r->maketext("Abandon export")); +} + +sub cancelExport_handler { + my ($self, $genericParams, $actionParams, $tableParams) = @_; + my $r = $self->r; + + #$self->{selectedSetIDs) = $self->{visibleSetIDs}; + # only do the above if we arrived here via "edit selected users" + if (defined $r->param("prev_visible_sets")) { + $self->{visibleSetIDs} = [ $r->param("prev_visible_sets") ]; + } elsif (defined $r->param("no_prev_visible_sets")) { + $self->{visibleSetIDs} = []; + } else { + # leave it alone + } + $self->{exportMode} = 0; + + return CGI::div({class=>"ResultsWithError"}, $r->maketext("export abandoned")); +} + +sub saveExport_form { + my ($self, $onChange, %actionParams) = @_; + my $r = $self->r; + return CGI::span($r->maketext("Export selected sets")); +} + +sub saveExport_handler { + my ($self, $genericParams, $actionParams, $tableParams) = @_; + my $r = $self->r; + my $db = $r->db; + + my @setIDsToExport = @{ $self->{selectedSetIDs} }; + + my %filenames = map { $_ => (@{ $tableParams->{"set.$_"} }[0] || $_) } @setIDsToExport; + + my ($exported, $skipped, $reason) = $self->exportSetsToDef(%filenames); + + if (defined $r->param("prev_visible_sets")) { + $self->{visibleSetIDs} = [ $r->param("prev_visible_sets") ]; + } elsif (defined $r->param("no_prev_visble_sets")) { + $self->{visibleSetIDs} = []; + } else { + # leave it alone + } + + $self->{exportMode} = 0; + + my $numExported = @$exported; + my $numSkipped = @$skipped; + my $resultFont = ($numSkipped)? "ResultsWithError" : "ResultsWithoutError"; + + my @reasons = map { "set $_ - " . $reason->{$_} } keys %$reason; + + return CGI::div({class=>$resultFont}, $r->maketext("[_1] sets exported, [_2] sets skipped. Skipped sets: ([_3])", $numExported, $numSkipped, (($numSkipped) ? CGI::ul(CGI::li(\@reasons)) : ""))); + +} + +sub cancelEdit_form { + my ($self, $onChange, %actionParams) = @_; + my $r = $self->r; + return CGI::span($r->maketext("Abandon changes")); +} + +sub cancelEdit_handler { + my ($self, $genericParams, $actionParams, $tableParams) = @_; + my $r = $self->r; + + #$self->{selectedSetIDs) = $self->{visibleSetIDs}; + # only do the above if we arrived here via "edit selected users" + if (defined $r->param("prev_visible_sets")) { + $self->{visibleSetIDs} = [ $r->param("prev_visible_sets") ]; + } elsif (defined $r->param("no_prev_visible_sets")) { + $self->{visibleSetIDs} = []; + } else { + # leave it alone + } + $self->{editMode} = 0; + + return CGI::div({class=>"ResultsWithError"}, $r->maketext("changes abandoned")); +} + +sub saveEdit_form { + my ($self, $onChange, %actionParams) = @_; + my $r = $self->r; + return CGI::span($r->maketext("Save changes")); +} + +sub saveEdit_handler { + my ($self, $genericParams, $actionParams, $tableParams) = @_; + my $r = $self->r; + my $db = $r->db; + + my @visibleSetIDs = @{ $self->{visibleSetIDs} }; + foreach my $setID (@visibleSetIDs) { + my $Set = $db->getGlobalSet($setID); # checked + # FIXME: we may not want to die on bad sets, they're not as bad as bad users + die "record for visible set $setID not found" unless $Set; + + foreach my $field ($Set->NONKEYFIELDS()) { + my $param = "set.${setID}.${field}"; + if (defined $tableParams->{$param}->[0]) { + if ($field =~ /_date/) { + $Set->$field($self->parseDateTime($tableParams->{$param}->[0])); + } else { + $Set->$field($tableParams->{$param}->[0]); + } + } + } + + # make sure the dates are not more than 10 years in the future + my $curr_time = time; + my $seconds_per_year = 31_556_926; + my $cutoff = $curr_time + $seconds_per_year*10; + return CGI::div({class=>'ResultsWithError'}, $r->maketext("Error: open date cannot be more than 10 years from now in set [_1]", $setID)) + if $Set->open_date > $cutoff; + return CGI::div({class=>'ResultsWithError'}, $r->maketext("Error: due date cannot be more than 10 years from now in set [_1]", $setID)) + if $Set->due_date > $cutoff; + return CGI::div({class=>'ResultsWithError'}, $r->maketext("Error: answer date cannot be more than 10 years from now in set [_1]", $setID)) + if $Set->answer_date > $cutoff; + + # Check that the open, due and answer dates are in increasing order. + # Bail if this is not correct. + if ($Set->open_date > $Set->due_date) { + return CGI::div({class=>'ResultsWithError'}, $r->maketext("Error: Due date must come after open date in set [_1]", $setID)); + } + if ($Set->due_date > $Set->answer_date) { + return CGI::div({class=>'ResultsWithError'}, $r->maketext("Error: Answer date must come after due date in set [_1]", $setID)); + } + + $db->putGlobalSet($Set); + } + + if (defined $r->param("prev_visible_sets")) { + $self->{visibleSetIDs} = [ $r->param("prev_visible_sets") ]; + } elsif (defined $r->param("no_prev_visble_sets")) { + $self->{visibleSetIDs} = []; + } else { + # leave it alone + } + + $self->{editMode} = 0; + + return CGI::div({class=>"ResultsWithError"}, $r->maketext("changes saved") ); +} + +sub duplicate_form { + my ($self, $onChange, %actionParams) = @_; + + my $r = $self->r; + my @visible_sets = $r->param('visible_sets'); + + return "" unless @visible_sets == 1; + + return join ("", + WeBWorK::CGI_labeled_input( + -type=>"text", + -id=>"duplicate_text", + -label_text=>$r->maketext("Duplicate this set and name it").": ", + -input_attr=>{ + -name => "action.duplicate.name", + -value => $actionParams{"action.duplicate.name"}->[0] || "", + -width => "50", + -onchange => $onChange, + } + ), + ); +} + +sub duplicate_handler { + my ($self, $genericParams, $actionParams, $tableParams) = @_; + + my $r = $self->r; + my $db = $r->db; + + my $oldSetID = $self->{selectedSetIDs}->[0]; + return CGI::div({class => "ResultsWithError"}, $r->maketext("Failed to duplicate set: no set selected for duplication!")) unless defined($oldSetID) and $oldSetID =~ /\S/; + my $newSetID = $actionParams->{"action.duplicate.name"}->[0]; + return CGI::div({class => "ResultsWithError"}, $r->maketext("Failed to duplicate set: no set name specified!")) unless $newSetID =~ /\S/; + # DBFIXME checking for existence -- don't need to fetch + return CGI::div({class => "ResultsWithError"}, $r->maketext("Failed to duplicate set: set [_1] already exists!", $newSetID)) if defined $db->getGlobalSet($newSetID); + + my $newSet = $db->getGlobalSet($oldSetID); + $newSet->set_id($newSetID); + eval {$db->addGlobalSet($newSet)}; + + # take all the problems from the old set and make them part of the new set + foreach ($db->getAllGlobalProblems($oldSetID)) { + $_->set_id($newSetID); + $db->addGlobalProblem($_); + } + + push @{ $self->{visibleSetIDs} }, $newSetID; + + return CGI::div({class => "ResultsWithError"}, $r->maketext("Failed to duplicate set: [_1]", $@)) if $@; + + return $r->maketext("Success"); +} + +################################################################################ +# sorts +################################################################################ + +sub bySetID { $a->set_id cmp $b->set_id } + +# I can't figure out why these are useful + +# sub bySetHeader { $a->set_header cmp $b->set_header } +# sub byHardcopyHeader { $a->hardcopy_header cmp $b->hardcopy_header } +#FIXME eventually we may be able to remove these checks, if we can trust +# that the dates are always defined +# dates which are the empty string '' or undefined are treated as 0 +sub byOpenDate { + my $result = eval{( $a->open_date || 0 ) <=> ( $b->open_date || 0 ) }; + return $result unless $@; + warn "Open date not correctly defined."; + return 0; +} +sub byDueDate { + my $result = eval{( $a->due_date || 0 ) <=> ( $b->due_date || 0 ) }; + return $result unless $@; + warn "Due date not correctly defined."; + return 0; +} +sub byAnswerDate { + my $result = eval{( $a->answer_date || 0) <=> ( $b->answer_date || 0 ) }; + return $result unless $@; + warn "Answer date not correctly defined."; + return 0; +} +sub byVisible { + my $result = eval{$a->visible cmp $b->visible }; + return $result unless $@; + warn "Visibility status not correctly defined."; + return 0; +} + +sub byOpenDue { &byOpenDate || &byDueDate } + +################################################################################ +# utilities +################################################################################ + +# generate labels for open_date/due_date/answer_date popup menus +sub menuLabels { + my ($self, $hashRef) = @_; + my %hash = %$hashRef; + + my %result; + foreach my $key (keys %hash) { + my $count = @{ $hash{$key} }; + my $displayKey = $self->formatDateTime($key) || ""; + $result{$key} = "$displayKey ($count sets)"; + } + return %result; +} + +sub importSetsFromDef { + my ($self, $newSetName, $assign, @setDefFiles) = @_; + my $r = $self->r; + my $ce = $r->ce; + my $db = $r->db; + my $dir = $ce->{courseDirs}->{templates}; + + # if the user includes "following files" in a multiple selection + # it shows up here as "" which causes the importing to die + # so, we select on filenames containing non-whitespace + @setDefFiles = grep(/\S/, @setDefFiles); + + # FIXME: do we really want everything to fail on one bad file name? + foreach my $fileName (@setDefFiles) { + die $r->maketext("won't be able to read from file [_1]/[_2]: does it exist? is it readable?", $dir, $fileName) + unless -r "$dir/$fileName"; + } + + my @allSetIDs = $db->listGlobalSets(); + # FIXME: getGlobalSets takes a lot of time just for checking to see if a set already exists + # this could be avoided by waiting until the call to addGlobalSet below + # and checking to see if the error message says that the set already exists + # but if the error message is ever changed the code here might be broken + # then again, one call to getGlobalSets and skipping unnecessary calls to addGlobalSet + # could be faster than no call to getGlobalSets and lots of unnecessary calls to addGlobalSet + # DBFIXME all we need here is set IDs, right? why fetch entire records? + my %allSets = map { $_->set_id => 1 if $_} $db->getGlobalSets(@allSetIDs); # checked + + my (@added, @skipped); + + foreach my $set_definition_file (@setDefFiles) { + + debug("$set_definition_file: reading set definition file"); + # read data in set definition file + my ($setName, $paperHeaderFile, $screenHeaderFile, $openDate, $dueDate, $answerDate, $ra_problemData, $assignmentType, $attemptsPerVersion, $timeInterval, $versionsPerInterval, $versionTimeLimit, $problemRandOrder, $problemsPerPage, $hideScore, $hideWork,$timeCap,$restrictIP,$restrictLoc,$relaxRestrictIP) = $self->readSetDef($set_definition_file); + my @problemList = @{$ra_problemData}; + + # Use the original name if form doesn't specify a new one. + # The set acquires the new name specified by the form. A blank + # entry on the form indicates that the imported set name will be used. + $setName = $newSetName if $newSetName; + + if ($allSets{$setName}) { + # this set already exists!! + push @skipped, $setName; + next; + } else { + push @added, $setName; + } + + debug("$set_definition_file: adding set"); + # add the data to the set record + my $newSetRecord = $db->newGlobalSet; + $newSetRecord->set_id($setName); + $newSetRecord->set_header($screenHeaderFile); + $newSetRecord->hardcopy_header($paperHeaderFile); + $newSetRecord->open_date($openDate); + $newSetRecord->due_date($dueDate); + $newSetRecord->answer_date($answerDate); + $newSetRecord->visible(DEFAULT_VISIBILITY_STATE); + $newSetRecord->enable_reduced_scoring(DEFAULT_ENABLED_REDUCED_SCORING_STATE); + + # gateway/version data. these should are all initialized to '' + # by readSetDef, so for non-gateway/versioned sets they'll just + # be stored as null + $newSetRecord->assignment_type($assignmentType); + $newSetRecord->attempts_per_version($attemptsPerVersion); + $newSetRecord->time_interval($timeInterval); + $newSetRecord->versions_per_interval($versionsPerInterval); + $newSetRecord->version_time_limit($versionTimeLimit); + $newSetRecord->problem_randorder($problemRandOrder); + $newSetRecord->problems_per_page($problemsPerPage); + $newSetRecord->hide_score($hideScore); + $newSetRecord->hide_work($hideWork); + $newSetRecord->time_limit_cap($timeCap); + $newSetRecord->restrict_ip($restrictIP); + $newSetRecord->relax_restrict_ip($relaxRestrictIP); + + #create the set + eval {$db->addGlobalSet($newSetRecord)}; + die $r->maketext("addGlobalSet [_1] in ProblemSetList: [_2]", $setName, $@) if $@; + + #do we need to add locations to the set_locations table? + if ( $restrictIP ne 'No' && $restrictLoc ) { + if ($db->existsLocation( $restrictLoc ) ) { + if ( ! $db->existsGlobalSetLocation($setName,$restrictLoc) ) { + my $newSetLocation = $db->newGlobalSetLocation; + $newSetLocation->set_id( $setName ); + $newSetLocation->location_id( $restrictLoc ); + eval {$db->addGlobalSetLocation($newSetLocation)}; + warn($r->maketext("error adding set location [_1] for set [_2]: [_3]", $restrictLoc, $setName, $@)) if $@; + } else { + # this should never happen. + warn($r->maketext("input set location [_1] already exists for set [_2].", $restrictLoc, $setName)."\n"); + } + } else { + warn($r->maketext("restriction location [_1] does not exist. IP restrictions have been ignored.", $restrictLoc)."\n"); + $newSetRecord->restrict_ip('No'); + $newSetRecord->relax_restrict_ip('No'); + eval { $db->putGlobalSet($newSetRecord) }; + # we ignore error messages here; if the set + # added without error before, we assume + # (ha) that it will put without trouble + } + } + + debug("$set_definition_file: adding problems to database"); + # add problems + my $freeProblemID = WeBWorK::Utils::max($db->listGlobalProblems($setName)) + 1; + foreach my $rh_problem (@problemList) { + $self->addProblemToSet( + setName => $setName, + sourceFile => $rh_problem->{source_file}, + problemID => $freeProblemID++, + value => $rh_problem->{value}, + maxAttempts => $rh_problem->{max_attempts}); + } + + + if ($assign eq "all") { + $self->assignSetToAllUsers($setName); + } + else { + my $userName = $r->param('user'); + $self->assignSetToUser($userName, $newSetRecord); ## always assign set to instructor + } + } + + return \@added, \@skipped; +} + +sub readSetDef { + my ($self, $fileName) = @_; + my $templateDir = $self->{ce}->{courseDirs}->{templates}; + my $filePath = "$templateDir/$fileName"; + my $value_default = $self->{ce}->{problemDefaults}->{value}; + my $max_attempts_default = $self->{ce}->{problemDefaults}->{max_attempts}; + + my $setName = ''; + + my $r = $self->r; + + if ($fileName =~ m|^set([.\w-]+)\.def$|) { + $setName = $1; + } else { + $self->addbadmessage( + qq{The setDefinition file name must begin with set}, + qq{and must end with .def . Every thing in between becomes the name of the set. }, + qq{For example set1.def, setExam.def, and setsample7.def }, + qq{define sets named 1, Exam, and sample7 respectively. }, + qq{The filename, $fileName, you entered is not legal\n } + ); + + } + + my ($line, $name, $value, $attemptLimit, $continueFlag); + my $paperHeaderFile = ''; + my $screenHeaderFile = ''; + my ($dueDate, $openDate, $answerDate); + my @problemData; + +# added fields for gateway test/versioned set definitions: + my ( $assignmentType, $attemptsPerVersion, $timeInterval, + $versionsPerInterval, $versionTimeLimit, $problemRandOrder, + $problemsPerPage, $restrictLoc, + ) = + ('')x8; # initialize these to '' + my ( $timeCap, $restrictIP, $relaxRestrictIP ) = ( 0, 'No', 'No'); +# additional fields currently used only by gateways; later, the world? + my ( $hideScore, $hideWork, ) = ( 'N', 'N' ); + + my %setInfo; + if ( open (SETFILENAME, "$filePath") ) { + ##################################################################### + # Read and check set data + ##################################################################### + while () { + + chomp($line = $_); + $line =~ s|(#.*)||; ## don't read past comments + unless ($line =~ /\S/) {next;} ## skip blank lines + $line =~ s|\s*$||; ## trim trailing spaces + $line =~ m|^\s*(\w+)\s*=\s*(.*)|; + + ###################### + # sanity check entries + ###################### + my $item = $1; + $item = '' unless defined $item; + my $value = $2; + $value = '' unless defined $value; + + if ($item eq 'setNumber') { + next; + } elsif ($item eq 'paperHeaderFile') { + $paperHeaderFile = $value; + } elsif ($item eq 'screenHeaderFile') { + $screenHeaderFile = $value; + } elsif ($item eq 'dueDate') { + $dueDate = $value; + } elsif ($item eq 'openDate') { + $openDate = $value; + } elsif ($item eq 'answerDate') { + $answerDate = $value; + } elsif ($item eq 'assignmentType') { + $assignmentType = $value; + } elsif ($item eq 'attemptsPerVersion') { + $attemptsPerVersion = $value; + } elsif ($item eq 'timeInterval') { + $timeInterval = $value; + } elsif ($item eq 'versionsPerInterval') { + $versionsPerInterval = $value; + } elsif ($item eq 'versionTimeLimit') { + $versionTimeLimit = $value; + } elsif ($item eq 'problemRandOrder') { + $problemRandOrder = $value; + } elsif ($item eq 'problemsPerPage') { + $problemsPerPage = $value; + } elsif ($item eq 'hideScore') { + $hideScore = ( $value ) ? $value : 'N'; + } elsif ($item eq 'hideWork') { + $hideWork = ( $value ) ? $value : 'N'; + } elsif ($item eq 'capTimeLimit') { + $timeCap = ( $value ) ? 1 : 0; + } elsif ($item eq 'restrictIP') { + $restrictIP = ( $value ) ? $value : 'No'; + } elsif ($item eq 'restrictLocation' ) { + $restrictLoc = ( $value ) ? $value : ''; + } elsif ( $item eq 'relaxRestrictIP' ) { + $relaxRestrictIP = ( $value ) ? $value : 'No'; + } elsif ($item eq 'problemList') { + last; + } else { + warn $r->maketext("readSetDef error, can't read the line: ||[_1]||", $line); + } + } + + ##################################################################### + # Check and format dates + ##################################################################### + my ($time1, $time2, $time3) = map { $self->parseDateTime($_); } ($openDate, $dueDate, $answerDate); + + unless ($time1 <= $time2 and $time2 <= $time3) { + warn $r->maketext("The open date: [_1], due date: [_2], and answer date: [_3] must be defined and in chronological order.", $openDate, $dueDate, $answerDate); + } + + # Check header file names + $paperHeaderFile =~ s/(.*?)\s*$/$1/; #remove trailing white space + $screenHeaderFile =~ s/(.*?)\s*$/$1/; #remove trailing white space + + ##################################################################### + # Gateway/version variable cleanup: convert times into seconds + $timeInterval = WeBWorK::Utils::timeToSec( $timeInterval ) + if ( $timeInterval ); + $versionTimeLimit = WeBWorK::Utils::timeToSec($versionTimeLimit) + if ( $versionTimeLimit ); + + # check that the values for hideWork and hideScore are valid + if ( $hideScore ne 'N' && $hideScore ne 'Y' && + $hideScore ne 'BeforeAnswerDate' ) { + warn($r->maketext("The value [_1] for the hideScore option is not valid; it will be replaced with 'N'.", $hideScore)."\n"); + $hideScore = 'N'; + } + if ( $hideWork ne 'N' && $hideWork ne 'Y' && + $hideWork ne 'BeforeAnswerDate' ) { + warn($r->maketext("The value [_1] for the hideWork option is not valid; it will be replaced with 'N'.", $hideWork)."\n"); + $hideWork = 'N'; + } + if ( $timeCap ne '0' && $timeCap ne '1' ) { + warn($r->maketext("The value [_1] for the capTimeLimit option is not valid; it will be replaced with '0'.", $timeCap)."\n"); + $timeCap = '0'; + } + if ( $restrictIP ne 'No' && $restrictIP ne 'DenyFrom' && + $restrictIP ne 'RestrictTo' ) { + warn($r->maketext("The value [_1] for the restrictIP option is not valid; it will be replaced with 'No'.", $restrictIP)."\n"); + $restrictIP = 'No'; + $restrictLoc = ''; + $relaxRestrictIP = 'No'; + } + if ( $relaxRestrictIP ne 'No' && + $relaxRestrictIP ne 'AfterAnswerDate' && + $relaxRestrictIP ne 'AfterVersionAnswerDate' ) { + warn($r->maketext("The value [_1] for the relaxRestrictIP option is not valid; it will be replaced with 'No'.", $relaxRestrictIP)."\n"); + $relaxRestrictIP = 'No'; + } + # to verify that restrictLoc is valid requires a database + # call, so we defer that until we return to add the set + + ##################################################################### + # Read and check list of problems for the set + ##################################################################### + while() { + chomp($line=$_); + $line =~ s/(#.*)//; ## don't read past comments + unless ($line =~ /\S/) {next;} ## skip blank lines + + # commas are valid in filenames, so we have to handle commas + # using backslash escaping, so \X will be replaced with X + my @line = (); + my $curr = ''; + for (my $i = 0; $i < length $line; $i++) { + my $c = substr($line,$i,1); + if ($c eq '\\') { + $curr .= substr($line,++$i,1); + } elsif ($c eq ',') { + push @line, $curr; + $curr = ''; + } else { + $curr .= $c; + } + } + ## anything left? + push(@line, $curr) if ( $curr ); + + ($name, $value, $attemptLimit, $continueFlag) = @line; + ##################### + # clean up problem values + ########################### + $name =~ s/\s*//g; + $value = "" unless defined($value); + $value =~ s/[^\d\.]*//g; + unless ($value =~ /\d+/) {$value = $value_default;} + $attemptLimit = "" unless defined($attemptLimit); + $attemptLimit =~ s/[^\d-]*//g; + unless ($attemptLimit =~ /\d+/) {$attemptLimit = $max_attempts_default;} + $continueFlag = "0" unless( defined($continueFlag) && @problemData ); + # can't put continuation flag onto the first problem + push(@problemData, {source_file => $name, + value => $value, + max_attempts =>, $attemptLimit, + continuation => $continueFlag + }); + } + close(SETFILENAME); + ($setName, + $paperHeaderFile, + $screenHeaderFile, + $time1, + $time2, + $time3, + \@problemData, + $assignmentType, $attemptsPerVersion, $timeInterval, + $versionsPerInterval, $versionTimeLimit, $problemRandOrder, + $problemsPerPage, + $hideScore, + $hideWork, + $timeCap, + $restrictIP, + $restrictLoc, + $relaxRestrictIP, + ); + } else { + warn $r->maketext("Can't open file [_1]", $filePath)."\n"; + } +} + +sub exportSetsToDef { + my ($self, %filenames) = @_; + + my $r = $self->r; + my $ce = $r->ce; + my $db = $r->db; + + my (@exported, @skipped, %reason); + +SET: foreach my $set (keys %filenames) { + + my $fileName = $filenames{$set}; + $fileName .= ".def" unless $fileName =~ m/\.def$/; + $fileName = "set" . $fileName unless $fileName =~ m/^set/; + # files can be exported to sub directories but not parent directories + if ($fileName =~ /\.\./) { + push @skipped, $set; + $reason{$set} = $r->maketext("Illegal filename contains '..'"); + next SET; + } + + my $setRecord = $db->getGlobalSet($set); + unless (defined $setRecord) { + push @skipped, $set; + $reason{$set} = $r->maketext("No record found."); + next SET; + } + my $filePath = $ce->{courseDirs}->{templates} . '/' . $fileName; + + # back up existing file + if(-e $filePath) { + rename($filePath, "$filePath.bak") or + $reason{$set} = $r->maketext("Existing file [_1] could not be backed up and was lost.", $filePath); + } + + my $openDate = $self->formatDateTime($setRecord->open_date); + my $dueDate = $self->formatDateTime($setRecord->due_date); + my $answerDate = $self->formatDateTime($setRecord->answer_date); + my $setHeader = $setRecord->set_header; + my $paperHeader = $setRecord->hardcopy_header; + my @problemList = $db->listGlobalProblems($set); + + my $problemList = ''; + foreach my $prob (sort {$a <=> $b} @problemList) { + # DBFIXME use an iterator? + my $problemRecord = $db->getGlobalProblem($set, $prob); # checked + unless (defined $problemRecord) { + push @skipped, $set; + $reason{$set} = $r->maketext("No record found for problem [_1].", $prob); + next SET; + } + my $source_file = $problemRecord->source_file(); + my $value = $problemRecord->value(); + my $max_attempts = $problemRecord->max_attempts(); + + # backslash-escape commas in fields + $source_file =~ s/([,\\])/\\$1/g; + $value =~ s/([,\\])/\\$1/g; + $max_attempts =~ s/([,\\])/\\$1/g; + $problemList .= "$source_file, $value, $max_attempts \n"; + } + + # gateway fields + my $assignmentType = $setRecord->assignment_type; + my $gwFields = ''; + if ( $assignmentType =~ /gateway/ ) { + my $attemptsPerV = $setRecord->attempts_per_version; + my $timeInterval = $setRecord->time_interval; + my $vPerInterval = $setRecord->versions_per_interval; + my $vTimeLimit = $setRecord->version_time_limit; + my $probRandom = $setRecord->problem_randorder; + my $probPerPage = $setRecord->problems_per_page; + my $hideScore = $setRecord->hide_score; + my $hideWork = $setRecord->hide_work; + my $timeCap = $setRecord->time_limit_cap; + $gwFields =<restrict_ip; + my $restrictFields = ''; + if ( $restrictIP && $restrictIP ne 'No' ) { + # only store the first location + my $restrictLoc = ($db->listGlobalSetLocations($setRecord->set_id))[0]; + my $relaxRestrict = $setRecord->relax_restrict_ip; + $restrictLoc || ($restrictLoc = ''); + $restrictFields = "restrictIP = $restrictIP" . + "\nrestrictLocation = $restrictLoc\n" . + "relaxRestrictIP = $relaxRestrict\n"; + } + + my $fileContents = <{courseDirs}->{templates}, $filePath); + eval { + local *SETDEF; + open SETDEF, ">$filePath" or die $r->maketext("Failed to open [_1]", $filePath); + print SETDEF $fileContents; + close SETDEF; + }; + + if ($@) { + push @skipped, $set; + $reason{$set} = $@; + } else { + push @exported, $set; + } + + } + + return \@exported, \@skipped, \%reason; + +} + +################################################################################ +# "display" methods +################################################################################ + +sub fieldEditHTML { + my ($self, $fieldName, $value, $properties) = @_; + my $size = $properties->{size}; + my $type = $properties->{type}; + my $access = $properties->{access}; + my $items = $properties->{items}; + my $synonyms = $properties->{synonyms}; + my $headerFiles = $self->{headerFiles}; + + if ($access eq "readonly") { + return $value; + } + + if ($type eq "number" or $type eq "text") { + return CGI::input({type=>"text", name=>$fieldName, value=>$value, size=>$size}); + } + + if ($type eq "filelist") { + return WeBWorK::CGI_labeled_input( + -type=>"select", + -id=>$fieldName."_id", + -label_text=>ucfirst($fieldName), + -input_attr=>{ + name => $fieldName, + value => [ sort keys %$headerFiles ], + labels => $headerFiles, + default => $value || 0, + } + ), + } + + if ($type eq "enumerable") { + my $matched = undef; # Whether a synonym match has occurred + + # Process synonyms for enumerable objects + foreach my $synonym (keys %$synonyms) { + if ($synonym ne "*" and $value =~ m/$synonym/) { + $value = $synonyms->{$synonym}; + $matched = 1; + } + } + + if (!$matched and exists $synonyms->{"*"}) { + $value = $synonyms->{"*"}; + } + + return WeBWorK::CGI_labeled_input( + -type=>"select", + -id=>$fieldName."_id", + -label_text=>ucfirst($fieldName), + -input_attr=>{ + name => $fieldName, + values => [keys %$items], + default => $value, + labels => $items, + } + ), + } + + if ($type eq "checked") { + + # FIXME: kludge (R) + # if the checkbox is checked it returns a 1, if it is unchecked it returns nothing + # in which case the hidden field overrides the parameter with a 0 + return WeBWorK::CGI_labeled_input( + -type=>"checkbox", + -id=>$fieldName."_id", + -label_text=>ucfirst($fieldName), + -input_attr=>{ + -name => $fieldName, + -checked => $value, + -label => "", + -value => 1 + } + ) . CGI::hidden( + -name => $fieldName, + -value => 0 + ); + } +} + +sub recordEditHTML { + my ($self, $Set, %options) = @_; + my $r = $self->r; + my $urlpath = $r->urlpath; + my $ce = $r->ce; + my $db = $r->db; + my $authz = $r->authz; + my $user = $r->param('user'); + my $root = $ce->{webworkURLs}->{root}; + my $courseName = $urlpath->arg("courseID"); + + my $editMode = $options{editMode}; + my $exportMode = $options{exportMode}; + my $setSelected = $options{setSelected}; + + my $visibleClass = $Set->visible ? $r->maketext("visible") : $r->maketext("hidden"); + my $enable_reduced_scoringClass = $Set->enable_reduced_scoring ? $r->maketext('Reduced Credit Enabled') : $r->maketext('Reduced Credit Disabled'); + + my $users = $db->countSetUsers($Set->set_id); + my $totalUsers = $self->{totalUsers}; + # DBFIXME count would suffice + my $problems = $db->listGlobalProblems($Set->set_id); + + my $usersAssignedToSetURL = $self->systemLink($urlpath->new(type=>'instructor_users_assigned_to_set', args=>{courseID => $courseName, setID => $Set->set_id} )); + my $problemListURL = $self->systemLink($urlpath->new(type=>'instructor_set_detail', args=>{courseID => $courseName, setID => $Set->set_id} )); + my $problemSetListURL = $self->systemLink($urlpath->new(type=>'instructor_set_list2', args=>{courseID => $courseName, setID => $Set->set_id})) . "&editMode=1&visible_sets=" . $Set->set_id; + my $imageURL = $ce->{webworkURLs}->{htdocs}."/images/edit.gif"; + my $imageLink = CGI::a({href => $problemSetListURL}, CGI::img({src=>$imageURL, border=>0})); + + my @tableCells; + my %fakeRecord; + my $set_id = $Set->set_id; + + $fakeRecord{select} = CGI::checkbox(-name => "selected_sets", -value => $set_id, -checked => $setSelected, -label => "", ); +# $fakeRecord{set_id} = CGI::font({class=>$visibleClass}, $set_id) . ($editMode ? "" : $imageLink); + $fakeRecord{set_id} = $editMode + ? CGI::a({href=>$problemListURL}, "$set_id") + : CGI::font({class=>$visibleClass}, $set_id) . $imageLink; + $fakeRecord{problems} = (FIELD_PERMS()->{problems} and not $authz->hasPermissions($user, FIELD_PERMS()->{problems})) + ? "$problems" + : CGI::a({href=>$problemListURL}, "$problems"); + $fakeRecord{users} = (FIELD_PERMS()->{users} and not $authz->hasPermissions($user, FIELD_PERMS()->{users})) + ? "$users/$totalUsers" + : CGI::a({href=>$usersAssignedToSetURL}, "$users/$totalUsers"); + $fakeRecord{filename} = CGI::input({-name => "set.$set_id", -value=>"set$set_id.def", -size=>60}); + + + # Select + if ($editMode) { + # column not there + } else { + # selection checkbox + # Set ID + my $label = ""; + if ($editMode) { + $label = CGI::a({href=>$problemListURL}, "$set_id"); + } else { + $label = CGI::font({class=>$visibleClass}, $set_id . $imageLink); + } + + push @tableCells, WeBWorK::CGI_labeled_input( + -type=>"checkbox", + -id=>$set_id."_id", + -label_text=>$label, + -input_attr=>$setSelected ? + { + -name => "selected_sets", + -value => $set_id, + -checked => "checked", + -class => "table_checkbox", + } + : + { + -name => "selected_sets", + -value => $set_id, + -class => "table_checkbox", + } + ); + } + + # Problems link + if ($editMode) { + # column not there + } else { + # "problem list" link + push @tableCells, CGI::a({href=>$problemListURL}, "$problems"); + } + + # Users link + if ($editMode) { + # column not there + } else { + # "edit users assigned to set" link + push @tableCells, CGI::a({href=>$usersAssignedToSetURL}, "$users/$totalUsers"); + } + + # determine which non-key fields to show + my @fieldsToShow; + if ($editMode) { + @fieldsToShow = @{ EDIT_FIELD_ORDER() }; + } elsif ($exportMode) { + @fieldsToShow = @{ EXPORT_FIELD_ORDER() }; + } else { + @fieldsToShow = @{ VIEW_FIELD_ORDER() }; + } + + # make a hash out of this so we can test membership easily + my %nonkeyfields; @nonkeyfields{$Set->NONKEYFIELDS} = (); + + # Set Fields + foreach my $field (@fieldsToShow) { + next unless exists $nonkeyfields{$field}; + my $fieldName = "set." . $set_id . "." . $field, + my $fieldValue = $Set->$field; + my %properties = %{ FIELD_PROPERTIES()->{$field} }; + $properties{access} = "readonly" unless $editMode; + $fieldValue = $self->formatDateTime($fieldValue) if $field =~ /_date/; + $fieldValue =~ s/ / /g unless $editMode; + $fieldValue = ($fieldValue) ? $r->maketext("Yes") : $r->maketext("No") if $field =~ /visible/ and not $editMode; + $fieldValue = ($fieldValue) ? $r->maketext("Yes") : $r->maketext("No") if $field =~ /enable_reduced_scoring/ and not $editMode; + push @tableCells, CGI::font({class=>$visibleClass}, $self->fieldEditHTML($fieldName, $fieldValue, \%properties)); + #$fakeRecord{$field} = CGI::font({class=>$visibleClass}, $self->fieldEditHTML($fieldName, $fieldValue, \%properties)); + } + + #@tableCells = map { $fakeRecord{$_} } @fieldsToShow; + + return CGI::Tr({}, CGI::td({}, \@tableCells)); +} + +sub printTableHTML { + my ($self, $SetsRef, $fieldNamesRef, %options) = @_; + my $r = $self->r; + my $authz = $r->authz; + my $user = $r->param('user'); + my $setTemplate = $self->{setTemplate}; + my @Sets = @$SetsRef; + my %fieldNames = %$fieldNamesRef; + + my $editMode = $options{editMode}; + my $exportMode = $options{exportMode}; + my %selectedSetIDs = map { $_ => 1 } @{ $options{selectedSetIDs} }; + my $currentSort = $options{currentSort}; + + # names of headings: + my @realFieldNames = ( + $setTemplate->KEYFIELDS, + $setTemplate->NONKEYFIELDS, + ); + + if ($editMode) { + @realFieldNames = @{ EDIT_FIELD_ORDER() }; + } else { + @realFieldNames = @{ VIEW_FIELD_ORDER() }; + } + + if ($exportMode) { + @realFieldNames = @{ EXPORT_FIELD_ORDER() }; + } + + + my %sortSubs = %{ SORT_SUBS() }; + + # FIXME: should this always presume to use the templates directory? + # (no, but that can wait until we have an abstract ProblemLibrary API -- sam) + my $templates_dir = $r->ce->{courseDirs}->{templates}; + my $exempt_dirs = join "|", keys %{ $r->ce->{courseFiles}->{problibs} }; + my @headers = listFilesRecursive( + $templates_dir, + qr/header.*\.pg$/i, # match these files + qr/^(?:$exempt_dirs|CVS)$/, # prune these directories + 0, # match against file name only + 1, # prune against path relative to $templates_dir + ); + + @headers = sort @headers; + my %headers = map { $_ => $_ } @headers; + $headers{""} = $r->maketext("Use System Default"); + $self->{headerFiles} = \%headers; # store these header files so we don't have to look for them later. + + + my @tableHeadings = map { $fieldNames{$_} } @realFieldNames; + shift @tableHeadings; + + # prepend selection checkbox? only if we're NOT editing! +# unshift @tableHeadings, "Select", "Set", "Problems" unless $editMode; + + # print the table + if ($editMode or $exportMode) { + print CGI::start_table({-class=>"set_table", -summary=>$r->maketext("_PROBLEM_SET_SUMMARY") });#"This is a table showing the current Homework sets for this class. The fields from left to right are: Edit Set Data, Edit Problems, Edit Assigned Users, Visibility to students, Reduced Credit Enabled, Date it was opened, Date it is due, and the Date during which the answers are posted. The Edit Set Data field contains checkboxes for selection and a link to the set data editing page. The cells in the Edit Problems fields contain links which take you to a page where you can edit the containing problems, and the cells in the edit assigned users field contains links which take you to a page where you can edit what students the set is assigned to."}); + } else { + print CGI::start_table({-border=>1, -class=>"set_table", -summary=>$r->maketext("_PROBLEM_SET_SUMMARY") }); #"This is a table showing the current Homework sets for this class. The fields from left to right are: Edit Set Data, Edit Problems, Edit Assigned Users, Visibility to students, Reduced Credit Enabled, Date it was opened, Date it is due, and the Date during which the answers are posted. The Edit Set Data field contains checkboxes for selection and a link to the set data editing page. The cells in the Edit Problems fields contain links which take you to a page where you can edit the containing problems, and the cells in the edit assigned users field contains links which take you to a page where you can edit what students the set is assigned to."}); + } + + print CGI::caption($r->maketext("Set List")); + + print CGI::Tr({}, CGI::th({}, \@tableHeadings)); + + + for (my $i = 0; $i < @Sets; $i++) { + my $Set = $Sets[$i]; + + print $self->recordEditHTML($Set, + editMode => $editMode, + exportMode => $exportMode, + setSelected => exists $selectedSetIDs{$Set->set_id} + ); + } + + print CGI::end_table(); + ######################################### + # if there are no users shown print message + # + ########################################## + + print CGI::p( + CGI::i($r->maketext("No sets shown. Choose one of the options above to list the sets in the course.")) + ) unless @Sets; +} + +# output_JS subroutine + +# outputs all of the Javascript required for this page + +sub output_JS{ + my $self = shift; + my $r = $self->r; + my $ce = $r->ce; + + my $site_url = $ce->{webworkURLs}->{htdocs}; + print CGI::start_script({type=>"text/javascript", src=>"$site_url/js/addOnLoadEvent.js"}), CGI::end_script(); + print CGI::start_script({type=>"text/javascript", src=>"$site_url/js/tabber.js"}), CGI::end_script(); + print CGI::start_script({type=>"text/javascript", src=>"$site_url/js/form_checker_hmwksets.js"}), CGI::end_script(); + print CGI::start_script({type=>"text/javascript", src=>"$site_url/js/hmwksets_handlers.js"}), CGI::end_script(); + print CGI::start_script({type=>"text/javascript", src=>"$site_url/js/show_hide.js"}), CGI::end_script(); + return ""; +} + +# Just tells template to output the stylesheet for Tabber +sub output_tabber_CSS{ + return ""; +} + +1; + +=head1 AUTHOR + +Written by Robert Van Dam, toenail (at) cif.rochester.edu + +=cut diff --git a/lib/WeBWorK/ContentGenerator/Instructor/SetMaker3.pm b/lib/WeBWorK/ContentGenerator/Instructor/SetMaker3.pm new file mode 100755 index 0000000000..92d051e54f --- /dev/null +++ b/lib/WeBWorK/ContentGenerator/Instructor/SetMaker3.pm @@ -0,0 +1,215 @@ +################################################################################ +# WeBWorK Online Homework Delivery System +# Copyright � 2000-2007 The WeBWorK Project, http://openwebwork.sf.net/ +# $CVSHeader: webwork2/lib/WeBWorK/ContentGenerator/Instructor/SetMaker.pm,v 1.85 2008/07/01 13:18:52 glarose Exp $ +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of either: (a) the GNU General Public License as published by the +# Free Software Foundation; either version 2, or (at your option) any later +# version, or (b) the "Artistic License" which comes with this package. +# +# This program 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 either the GNU General Public License or the +# Artistic License for more details. +################################################################################ + + +package WeBWorK::ContentGenerator::Instructor::SetMaker3; +use base qw(WeBWorK::ContentGenerator::Instructor); + +=head1 NAME + +WeBWorK::ContentGenerator::Instructor::SetMaker3 - Make homework sets. + +=cut + +use strict; +use warnings; + + +#use CGI qw(-nosticky); +use WeBWorK::CGI; +use WeBWorK::Debug; +use WeBWorK::Form; +use WeBWorK::Utils qw(readDirectory max sortByName); +use WeBWorK::Utils::Tasks qw(renderProblems); +use File::Find; + +require WeBWorK::Utils::ListingDB; + +use constant SHOW_HINTS_DEFAULT => 0; +use constant SHOW_SOLUTIONS_DEFAULT => 0; +use constant MAX_SHOW_DEFAULT => 20; +use constant NO_LOCAL_SET_STRING => 'No sets in this course yet'; +use constant SELECT_SET_STRING => 'Select a Set from this Course'; +use constant SELECT_LOCAL_STRING => 'Select a Problem Collection'; +use constant MY_PROBLEMS => ' My Problems '; +use constant MAIN_PROBLEMS => ' Unclassified Problems '; +use constant CREATE_SET_BUTTON => 'Create New Set'; +use constant ALL_CHAPTERS => 'All Chapters'; +use constant ALL_SUBJECTS => 'All Subjects'; +use constant ALL_SECTIONS => 'All Sections'; +use constant ALL_TEXTBOOKS => 'All Textbooks'; + +use constant LIB2_DATA => { + 'dbchapter' => {name => 'library_chapters', all => 'All Chapters'}, + 'dbsection' => {name => 'library_sections', all =>'All Sections' }, + 'dbsubject' => {name => 'library_subjects', all => 'All Subjects' }, + 'textbook' => {name => 'library_textbook', all => 'All Textbooks'}, + 'textchapter' => {name => 'library_textchapter', all => 'All Chapters'}, + 'textsection' => {name => 'library_textsection', all => 'All Sections'}, + 'keywords' => {name => 'library_keywords', all => '' }, + }; + + +## for additional problib buttons +my %problib; ## filled in in global.conf +my %ignoredir = ( + '.' => 1, '..' => 1, 'Library' => 1, 'CVS' => 1, 'tmpEdit' => 1, + 'headers' => 1, 'macros' => 1, 'email' => 1, '.svn' => 1, +); + +# template method +sub templateName { + return "lbtwo"; +} + +sub prepare_activity_entry { + my $self=shift; + my $r = $self->r; + my $user = $self->r->param('user') || 'NO_USER'; + return("In SetMaker3 as user $user"); +} + + + +sub title { + return "Library Browser v3"; +} + +# hide view options panel since it distracts from SetMaker's built-in view options +sub options { + return ""; +} + +sub head { + print ''; + print ''; + print ''; + + print ''; + #print ''; + #print ''; + print ''; + print ''; + #my ($self) = @_; + #my $r = $self->r; + #start a timer to save people's stuff idk if people want this + #print ""; + #print ''; + print ''; + print ''; + #print ''; + return ""; +} + +sub body { + my ($self) = @_; + + my $r = $self->r; + my $urlpath =$r->urlpath; + my $courseID = $urlpath->arg("courseID"); + + ########## Loading Screen + #print '
'; + + ########## toolbar + print '
'; + print ''; + print ' + + + + BUGS! + '; + print '
'; + + #big wrapper div that will hopefully fix theme issues + print '
'; + + ########## Top part + #print ''; + #print ''; + #print 'Hover Magnification: 1'; + #print ''; + #print ''; + #'

In the target set you can drag problems to reorder them.
The problem will be placed in front of the one you drop it on,
or at the end of the list if you drop it on an empty space in the table.

', + + print '
'; + print ''; + print ''; + print ''; + print '
Library directories:
Library search:
'; + ########################################### + # Library repository controls + ########################################### + #print 'all', + # 'none'; + ########################################### + + print '
'; + print '
', + '', + '', + '
'; + print '
'; + print 'Target Sets'; + print '
    '; + print '
  • New Problem Set
  • '; + print '
'; + print '
'; + print '

||

'; + print '
'; + #List of tabs + print ''; + print '
'; + print '

Problems

'; + ########## Now print problems + print '
    '; + + print '
'; +#4 problems across + print '

';#might be a better way to do the perpage + print '
'; + print '
'; + ########## Finish things off + print '
'; + print '
'; + print '
'; + print $self->hidden_authen_fields; + print CGI::hidden({id=>'hidden_courseID',name=>'courseID',default=>$courseID }); + return ""; +} + +=head1 AUTHOR + +Written by John Jones, jj (at) asu.edu. +Edited by David Gage + +=cut + +1; diff --git a/lib/WeBWorK/ContentGenerator/Instructor/Stats.pm b/lib/WeBWorK/ContentGenerator/Instructor/Stats.pm index fe81befe72..68bcb35982 100644 --- a/lib/WeBWorK/ContentGenerator/Instructor/Stats.pm +++ b/lib/WeBWorK/ContentGenerator/Instructor/Stats.pm @@ -58,7 +58,7 @@ sub initialize { my $setName = $r->urlpath->arg("setID") || 0; $self->{setName} = $setName; my $setRecord = $db->getGlobalSet($setName); # checked - die "global set $setName not found." unless $setRecord; + die $r->maketext("global set [_1] not found.", $setName) unless $setRecord; $self->{set_due_date} = $setRecord->due_date; $self->{setRecord} = $setRecord; } @@ -76,13 +76,13 @@ sub title { return "" unless $authz->hasPermissions($user, "access_instructor_tools"); my $type = $self->{type}; - my $string = "Statistics for ".$self->{ce}->{courseName}." "; + my $string = $r->maketext("Statistics for").$self->{ce}->{courseName}." "; if ($type eq 'student') { - $string .= "student ".$self->{studentName}; + $string .= $r->maketext("student")." ".$self->{studentName}; } elsif ($type eq 'set' ) { - $string .= "set ".$self->{setName}; - $string .= ".    Due ". $self->formatDateTime($self->{set_due_date}); + $string .= $r->maketext("set")." ".$self->{setName}; + $string .= ".    ".$r->maketext("Due")." ".$self->formatDateTime($self->{set_due_date}); } return $string; } @@ -136,27 +136,27 @@ sub body { my $type = $self->{type}; # Check permissions - return CGI::div({class=>"ResultsWithError"}, CGI::p("You are not authorized to access instructor tools")) + return CGI::div({class=>"ResultsWithError"}, CGI::p($r->maketext("You are not authorized to access instructor tools"))) unless $authz->hasPermissions($user, "access_instructor_tools"); if ($type eq 'student') { my $studentName = $self->{studentName}; my $studentRecord = $db->getUser($studentName) # checked - or die "record for user $studentName not found"; + or die $r->maketext("record for user [_1] not found", $studentName); my $fullName = $studentRecord->full_name; my $courseHomePage = $urlpath->new(type => 'set_list', args => {courseID=>$courseName}); my $email = $studentRecord->email_address; print CGI::a({-href=>"mailto:$email"}, $email), CGI::br(), - "Section: ", $studentRecord->section, CGI::br(), - "Recitation: ", $studentRecord->recitation, CGI::br(); + $r->maketext("Section:")." ", $studentRecord->section, CGI::br(), + $r->maketext("Recitation:")." ", $studentRecord->recitation, CGI::br(); if ($authz->hasPermissions($user, "become_student")) { my $act_as_student_url = $self->systemLink($courseHomePage, params => {effectiveUser=>$studentName}); - print 'Act as: ', CGI::a({-href=>$act_as_student_url},$studentRecord->user_id); + print $r->maketext('Act as:')." ", CGI::a({-href=>$act_as_student_url},$studentRecord->user_id); } print WeBWorK::ContentGenerator::Grades::displayStudentStats($self,$studentName); @@ -165,7 +165,7 @@ sub body { } elsif ($type eq '') { $self->index; } else { - warn "Don't recognize statistics display type: |$type|"; + warn $r->maketext("Don't recognize statistics display type: |[_1]|", $type); } return ''; @@ -208,11 +208,11 @@ sub index { CGI::start_table({-border=>2, -cellpadding=>20}), CGI::Tr({}, CGI::td({-valign=>'top'}, - CGI::h3({-align=>'center'},'View statistics by set'), + CGI::h3({-align=>'center'},$r->maketext('View statistics by set')), CGI::ul( CGI::li( [@setLinks] ) ), ), CGI::td({-valign=>'top'}, - CGI::h3({-align=>'center'},'View statistics by student'), + CGI::h3({-align=>'center'},$r->maketext('View statistics by student')), CGI::ul(CGI::li( [ @studentLinks ] ) ), ), ), @@ -502,22 +502,22 @@ sub displaySets { print - CGI::p('The percentage of active students with correct answers for each problem'), + CGI::p($r->maketext('The percentage of active students with correct answers for each problem')), CGI::start_table({-border=>1}), CGI::Tr(CGI::td( - ['Problem #', + [$r->maketext('Problem').' #', map {CGI::a({ href=>$self->systemLink($problemPage{$_}) },$_)} @problemIDs ] )), CGI::Tr(CGI::td( - [ '% correct',map {($number_of_students_attempting_problem{$_}) + [ '% '.$r->maketext('correct'),map {($number_of_students_attempting_problem{$_}) ? sprintf("%0.0f",100*$correct_answers_for_problem{$_}/$number_of_students_attempting_problem{$_}) : '-'} @problemIDs ] )), CGI::Tr(CGI::td( - [ 'avg attempts',map {($number_of_students_attempting_problem{$_}) + [ $r->maketext('avg attempts'),map {($number_of_students_attempting_problem{$_}) ? sprintf("%0.1f",$number_of_attempts_for_problem{$_}/$number_of_students_attempting_problem{$_}) : '-'} @problemIDs @@ -530,20 +530,19 @@ print ##################################################################################### print - CGI::p(CGI::i('The percentage of students receiving at least these scores.
- The median score is in the 50% column. ')), + CGI::p(CGI::i($r->maketext('The percentage of students receiving at least these scores. The median score is in the 50% column. '))), CGI::start_table({-border=>1}), CGI::Tr( - CGI::td( ['% students', + CGI::td( ['% '.$r->maketext('students'), (map { " ".$_ } @brackets1) , - 'top score ', + $r->maketext('top score').' ', ] ) ), CGI::Tr( CGI::td( [ - 'Score', + $r->maketext('Score'), (prevent_repeats map { sprintf("%0.0f",100*$score_percentiles{$_}) } @brackets1), sprintf("%0.0f",100), ] @@ -551,7 +550,7 @@ print ), CGI::Tr( CGI::td( [ - 'Success Index', + $r->maketext('Success Index'), (prevent_repeats map { sprintf("%0.0f",100*$index_percentiles{$_}) } @brackets1), sprintf("%0.0f",100), ] @@ -568,10 +567,10 @@ print ##################################################################################### print - CGI::p(CGI::i('Percentile cutoffs for number of attempts.
The 50% column shows the median number of attempts')), + CGI::p(CGI::i($r->maketext('Percentile cutoffs for number of attempts.
The 50% column shows the median number of attempts'))), CGI::start_table({-border=>1}), CGI::Tr( - CGI::td( ['% students', + CGI::td( ['% '.$r->maketext('students'), (map { " ".($_) } @brackets2) , ] diff --git a/lib/WeBWorK/ContentGenerator/Instructor/UserList2.pm b/lib/WeBWorK/ContentGenerator/Instructor/UserList2.pm new file mode 100644 index 0000000000..b533380fff --- /dev/null +++ b/lib/WeBWorK/ContentGenerator/Instructor/UserList2.pm @@ -0,0 +1,1902 @@ +################################################################################ +# WeBWorK Online Homework Delivery System +# Copyright © 2000-2007 The WeBWorK Project, http://openwebwork.sf.net/ +# $CVSHeader: webwork2/lib/WeBWorK/ContentGenerator/Instructor/UserList2.pm,v 1.96 2010/05/14 00:52:48 gage Exp $ +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of either: (a) the GNU General Public License as published by the +# Free Software Foundation; either version 2, or (at your option) any later +# version, or (b) the "Artistic License" which comes with this package. +# +# This program 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 either the GNU General Public License or the +# Artistic License for more details. +################################################################################ + +package WeBWorK::ContentGenerator::Instructor::UserList2; +use base qw(WeBWorK); +use base qw(WeBWorK::ContentGenerator); +use base qw(WeBWorK::ContentGenerator::Instructor); + +=head1 NAME + +WeBWorK::ContentGenerator::Instructor::UserList2 - Entry point for User-specific +data editing (ghe3 version) + +=cut + +=for comment + +What do we want to be able to do here? + +Filter what users are shown: + - none, all, selected + - matching user_id, matching section, matching recitation +Switch from view mode to edit mode: + - showing visible users + - showing selected users +Switch from edit mode to view and save changes +Switch from edit mode to view and abandon changes +Switch from view mode to password mode: + - showing visible users + - showing selected users +Switch from password mode to view and save changes +Switch from password mode to view and abandon changes +Delete users: + - visible + - selected +Import users: + - replace: + - any users + - visible users + - selected users + - no users + - add: + - any users + - no users +Export users: + - export: + - all + - visible + - selected + - to: + - existing file on server (overwrite): [ list of files ] + - new file on server (create): [ filename ] + +=cut + +use strict; +use warnings; +#use CGI qw(-nosticky ); +use WeBWorK::CGI; +use WeBWorK::File::Classlist; +use WeBWorK::DB qw(check_user_id); +use WeBWorK::Utils qw(readFile readDirectory cryptPassword); +use constant HIDE_USERS_THRESHHOLD => 200; +use constant EDIT_FORMS => [qw(cancelEdit saveEdit)]; +use constant PASSWORD_FORMS => [qw(cancelPassword savePassword)]; +use constant VIEW_FORMS => [qw(filter sort edit password import export add delete)]; + +# permissions needed to perform a given action +use constant FORM_PERMS => { + saveEdit => "modify_student_data", + edit => "modify_student_data", + savePassword => "change_password", + password => "change_password", + import => "modify_student_data", + export => "modify_classlist_files", + add => "modify_student_data", + delete => "modify_student_data", +}; + +# permissions needed to view a given field +use constant FIELD_PERMS => { + act_as => "become_student", + sets => "assign_problem_sets", +}; + +use constant STATE_PARAMS => [qw(user effectiveUser key visible_users no_visible_users prev_visible_users no_prev_visible_users editMode passwordMode primarySortField secondarySortField ternarySortField labelSortMethod)]; + +use constant SORT_SUBS => { + user_id => \&byUserID, + first_name => \&byFirstName, + last_name => \&byLastName, + email_address => \&byEmailAddress, + student_id => \&byStudentID, + status => \&byStatus, + section => \&bySection, + recitation => \&byRecitation, + comment => \&byComment, + permission => \&byPermission, +}; + +use constant FIELD_PROPERTIES => { + user_id => { + type => "text", + size => 8, + access => "readonly", + }, + first_name => { + type => "text", + size => 10, + access => "readwrite", + }, + last_name => { + type => "text", + size => 10, + access => "readwrite", + }, + email_address => { + type => "text", + size => 20, + access => "readwrite", + }, + student_id => { + type => "text", + size => 11, + access => "readwrite", + }, + status => { + #type => "enumerable", + type => "status", + size => 4, + access => "readwrite", + #items => { + # "C" => "Enrolled", + # "D" => "Drop", + # "A" => "Audit", + #}, + #synonyms => { + # qr/^[ce]/i => "C", + # qr/^[dw]/i => "D", + # qr/^a/i => "A", + # "*" => "C", + #} + }, + section => { + type => "text", + size => 4, + access => "readwrite", + }, + recitation => { + type => "text", + size => 4, + access => "readwrite", + }, + comment => { + type => "text", + size => 20, + access => "readwrite", + }, + permission => { +# this really should be read from $r->ce, but that's not available here + type => "permission", + access => "readwrite", +# type => "number", +# size => 2, +# access => "readwrite", + } +}; +sub pre_header_initialize { + my $self = shift; + my $r = $self->r; + my $urlpath = $r->urlpath; + my $authz = $r->authz; + my $ce = $r->ce; + my $courseName = $urlpath->arg("courseID"); + my $user = $r->param('user'); + # Handle redirects, if any. + ############################## + # Redirect to the addUser page + ################################## + + # Check permissions + return unless $authz->hasPermissions($user, "access_instructor_tools"); + + defined($r->param('action')) && $r->param('action') eq 'add' && do { + # fix url and redirect + my $root = $ce->{webworkURLs}->{root}; + + my $numberOfStudents = $r->param('number_of_students'); + warn $r->maketext("number of students not defined") unless defined $numberOfStudents; + + my $uri=$self->systemLink( $urlpath->newFromModule('WeBWorK::ContentGenerator::Instructor::AddUsers', $r, courseID=>$courseName), + params=>{ + number_of_students=>$numberOfStudents, + } + ); + #FIXME does the display mode need to be defined? + #FIXME url_authen_args also includes an effective user, so the new one must come first. + # even that might not work with every browser since there are two effective User assignments. + $self->reply_with_redirect($uri); + return; + }; +} + +sub initialize { + my ($self) = @_; + my $r = $self->r; + my $db = $r->db; + my $ce = $r->ce; + my $authz = $r->authz; + my $user = $r->param('user'); + + # Check permissions + return unless $authz->hasPermissions($user, "access_instructor_tools"); + + #if (defined($r->param('addStudent'))) { + # my $newUser = $db->newUser; + # my $newPermissionLevel = $db->newPermissionLevel; + # my $newPassword = $db->newPassword; + # $newUser->user_id($r->param('newUserID')); + # $newPermissionLevel->user_id($r->param('newUserID')); + # $newPassword->user_id($r->param('newUserID')); + # $newUser->status('C'); + # $newPermissionLevel->permission(0); + # $db->addUser($newUser); + # $db->addPermissionLevel($newPermissionLevel); + # $db->addPassword($newPassword); + #} +} + + + +sub body { + my ($self) = @_; + my $r = $self->r; + my $urlpath = $r->urlpath; + my $db = $r->db; + my $ce = $r->ce; + my $authz = $r->authz; + my $courseName = $urlpath->arg("courseID"); + my $setID = $urlpath->arg("setID"); + my $user = $r->param('user'); + + my $root = $ce->{webworkURLs}->{root}; + + # templates for getting field names + my $userTemplate = $self->{userTemplate} = $db->newUser; + my $permissionLevelTemplate = $self->{permissionLevelTemplate} = $db->newPermissionLevel; + + return CGI::div({class=>"ResultsWithError"}, CGI::p($r->maketext("You are not authorized to access the instructor tools."))) + unless $authz->hasPermissions($user, "access_instructor_tools"); + + # This table can be consulted when display-ready forms of field names are needed. + my %prettyFieldNames = map { $_ => $_ } + $userTemplate->FIELDS(), + $permissionLevelTemplate->FIELDS(); + + @prettyFieldNames{qw( + user_id + first_name + last_name + email_address + student_id + status + section + recitation + comment + permission + )} = ( + $r->maketext("Login Name"), + $r->maketext("First Name"), + $r->maketext("Last Name"), + $r->maketext("Email Address"), + $r->maketext("Student ID"), + $r->maketext("Status"), + $r->maketext("Section"), + $r->maketext("Recitation"), + $r->maketext("Comment"), + $r->maketext("Permission Level") + ); + + $self->{prettyFieldNames} = \%prettyFieldNames; + ########## set initial values for state fields + + # exclude set-level proctors + my @allUserIDs = grep {$_ !~ /^set_id:/} $db->listUsers; + # DBFIXME count would work + $self->{totalSets} = $db->listGlobalSets; # save for use in "assigned sets" links + $self->{allUserIDs} = \@allUserIDs; + + # DBFIXME filter in the database + if (defined $r->param("visable_user_string")) { + my @visableUserIDs = split /:/, $r->param("visable_user_string"); + $self->{visibleUserIDs} = [ @visableUserIDs ]; + } elsif (defined $r->param("visible_users")) { + $self->{visibleUserIDs} = [ $r->param("visible_users") ]; + } elsif (defined $r->param("no_visible_users")) { + $self->{visibleUserIDs} = []; + } else { + if ((@allUserIDs > HIDE_USERS_THRESHHOLD) and (not defined $r->param("show_all_users") )) { + $self->{visibleUserIDs} = []; + } else { + $self->{visibleUserIDs} = [ @allUserIDs ]; + } + } + + $self->{prevVisibleUserIDs} = $self->{visibleUserIDs}; + + if (defined $r->param("selected_users")) { + $self->{selectedUserIDs} = [ $r->param("selected_users") ]; + } else { + $self->{selectedUserIDs} = []; + } + + $self->{editMode} = $r->param("editMode") || 0; + + return CGI::div({class=>"ResultsWithError"}, CGI::p($r->maketext("You are not authorized to modify student data"))) + if $self->{editMode} and not $authz->hasPermissions($user, "modify_student_data"); + + + $self->{passwordMode} = $r->param("passwordMode") || 0; + + return CGI::div({class=>"ResultsWithError"}, CGI::p($r->maketext("You are not authorized to modify student data"))) + if $self->{passwordMode} and not $authz->hasPermissions($user, "modify_student_data"); + + if (defined $r->param("labelSortMethod")) { + $self->{primarySortField} = $r->param("labelSortMethod"); + $self->{secondarySortField} = $r->param("primarySortField"); + $self->{ternarySortField} = $r->param("secondarySortField"); + } + else { + $self->{primarySortField} = $r->param("primarySortField") || "last_name"; + $self->{secondarySortField} = $r->param("secondarySortField") || "first_name"; + $self->{ternarySortField} = $r->param("ternarySortField") || "student_id"; + } + + # DBFIXME use an iterator + my @allUsers = $db->getUsers(@allUserIDs); + my (%sections, %recitations); + foreach my $User (@allUsers) { + push @{$sections{defined $User->section ? $User->section : ""}}, $User->user_id; + push @{$recitations{defined $User->recitation ? $User->recitation : ""}}, $User->user_id; + } + $self->{sections} = \%sections; + $self->{recitations} = \%recitations; + + ########## call action handler + + my $actionID = $r->param("action"); + if ($actionID) { + unless (grep { $_ eq $actionID } @{ VIEW_FORMS() }, @{ EDIT_FORMS() }, @{ PASSWORD_FORMS() } ) { + die $r->maketext("Action [_1] not found", $actionID); + } + # Check permissions + if (not FORM_PERMS()->{$actionID} or $authz->hasPermissions($user, FORM_PERMS()->{$actionID})) { + my $actionHandler = "${actionID}_handler"; + my %genericParams; + foreach my $param (qw(selected_users)) { + $genericParams{$param} = [ $r->param($param) ]; + } + my %actionParams = $self->getActionParams($actionID); + my %tableParams = $self->getTableParams(); + print CGI::p( + CGI::div({-style=>"color:green"}, $r->maketext("Result of last action performed: [_1]", CGI::i($self->$actionHandler(\%genericParams, \%actionParams, \%tableParams)))), + CGI::hr() + ); + } else { + return CGI::div({class=>"ResultsWithError"}, CGI::p($r->maketext("You are not authorized to perform this action."))); + } + } + + ########## retrieve possibly changed values for member fields + + #@allUserIDs = @{ $self->{allUserIDs} }; # do we need this one? + # DBFIXME instead of re-listing, why not add added users to $self->{allUserIDs} ? + # exclude set-level proctors + @allUserIDs = grep {$_ !~ /^set_id:/} $db->listUsers; # recompute value in case some were added + my @visibleUserIDs = @{ $self->{visibleUserIDs} }; + my @prevVisibleUserIDs = @{ $self->{prevVisibleUserIDs} }; + my @selectedUserIDs = @{ $self->{selectedUserIDs} }; + my $editMode = $self->{editMode}; + my $passwordMode = $self->{passwordMode}; + my $primarySortField = $self->{primarySortField}; + my $secondarySortField = $self->{secondarySortField}; + my $ternarySortField = $self->{ternarySortField}; + + #warn "visibleUserIDs=@visibleUserIDs\n"; + #warn "prevVisibleUserIDs=@prevVisibleUserIDs\n"; + #warn "selectedUserIDs=@selectedUserIDs\n"; + #warn "editMode=$editMode\n"; + #warn "passwordMode=$passwordMode\n"; + #warn "primarySortField=$primarySortField\n"; + #warn "secondarySortField=$secondarySortField\n"; + #warn "ternarySortField=$ternarySortField\n"; + + ########## get required users + + my @Users = grep { defined $_ } @visibleUserIDs ? $db->getUsers(@visibleUserIDs) : (); + + my %sortSubs = %{ SORT_SUBS() }; + my $primarySortSub = $sortSubs{$primarySortField}; + my $secondarySortSub = $sortSubs{$secondarySortField}; + my $ternarySortSub = $sortSubs{$ternarySortField}; + + # add permission level to user record hash so we can sort it if necessary + # DBFIXME this calls for a join... (i'd like the User record to contain permission level info) + if ($primarySortField eq 'permission' or $secondarySortField eq 'permission' or $ternarySortField eq 'permission') { + foreach my $User (@Users) { + next unless $User; + my $permissionLevel = $db->getPermissionLevel($User->user_id); + $User->{permission} = $permissionLevel->permission; + } + } + + +# # don't forget to sort in opposite order of importance +# @Users = sort $secondarySortSub @Users; +# @Users = sort $primarySortSub @Users; +# #@Users = sort byLnFnUid @Users; + +# Always have a definite sort order even if first three sorts don't determine things + @Users = sort { + &$primarySortSub + || + &$secondarySortSub + || + &$ternarySortSub + || + byLastName + || + byFirstName + || + byUserID + } + @Users; + + my @PermissionLevels; + + for (my $i = 0; $i < @Users; $i++) { + my $User = $Users[$i]; + # DBFIX we maybe already have the permission level from above (for use in sorting) + my $PermissionLevel = $db->getPermissionLevel($User->user_id); # checked + + # DBFIXME this should go in the DB layer + unless ($PermissionLevel) { + # uh oh! no permission level record found! + warn $r->maketext("added missing permission level for user"), $User->user_id, "\n"; + + # create a new permission level record + $PermissionLevel = $db->newPermissionLevel; + $PermissionLevel->user_id($User->user_id); + $PermissionLevel->permission(0); + + # add it to the database + $db->addPermissionLevel($PermissionLevel); + } + + $PermissionLevels[$i] = $PermissionLevel; + } + + ########## print site identifying information + + print WeBWorK::CGI_labeled_input(-type=>"button", -id=>"show_hide", -input_attr=>{-value=>$r->maketext("Show/Hide Site Description"), -class=>"button_input"}); + print CGI::p({-id=>"site_description", -style=>"display:none"}, CGI::em($r->maketext("_CLASSLIST_EDITOR_DESCRIPTION"))); + + ########## print beginning of form + + print CGI::start_form({method=>"post", action=>$self->systemLink($urlpath,authen=>0), name=>"userlist", class=>"edit_form"}); + print $self->hidden_authen_fields(); + + ########## print state data + + print "\n\n"; + + if (@visibleUserIDs) { + print CGI::hidden(-name=>"visible_users", -value=>\@visibleUserIDs); + } else { + print CGI::hidden(-name=>"no_visible_users", -value=>"1"); + } + + if (@prevVisibleUserIDs) { + print CGI::hidden(-name=>"prev_visible_users", -value=>\@prevVisibleUserIDs); + } else { + print CGI::hidden(-name=>"no_prev_visible_users", -value=>"1"); + } + + print CGI::hidden(-name=>"editMode", -value=>$editMode); + + print CGI::hidden(-name=>"passwordMode", -value=>$passwordMode); + + print CGI::hidden(-name=>"primarySortField", -value=>$primarySortField); + print CGI::hidden(-name=>"secondarySortField", -value=>$secondarySortField); + print CGI::hidden(-name=>"ternarySortField", -value=>$ternarySortField); + + print "\n\n"; + + ########## print action forms + + # print CGI::start_table({}); + print CGI::p($r->maketext("Select an action to perform").":"); + + my @formsToShow; + if ($editMode) { + @formsToShow = @{ EDIT_FORMS() }; + }elsif ($passwordMode) { + @formsToShow = @{ PASSWORD_FORMS() }; + } else { + @formsToShow = @{ VIEW_FORMS() }; + } + + my $i = 0; + foreach my $actionID (@formsToShow) { + # Check permissions + next if FORM_PERMS()->{$actionID} and not $authz->hasPermissions($user, FORM_PERMS()->{$actionID}); + my $actionForm = "${actionID}_form"; + my $onChange = "document.userlist.action[$i].checked=true"; + my %actionParams = $self->getActionParams($actionID); + + print CGI::div({-class=>"column"},WeBWorK::CGI_labeled_input(-type=>"radio", -id=>$actionID."_id", -label_text=>$r->maketext(ucfirst(WeBWorK::split_cap($actionID))), -input_attr=>{-name=>"action", -value=>$actionID}, -label_attr=>{-class=>"radio_label"}),CGI::br(),$self->$actionForm($onChange, %actionParams),CGI::br()); + + $i++; + } + my $selectAll =WeBWorK::CGI_labeled_input(-type=>'button', -id=>"select_all", -input_attr=>{-name=>'check_all', -class=>"button_input", -value=>$r->maketext('Select all users'), + onClick => "for (i in document.userlist.elements) { + if (document.userlist.elements[i].name =='selected_users') { + document.userlist.elements[i].checked = true + } + }" }); + my $selectNone =WeBWorK::CGI_labeled_input(-type=>'button', -id=>"select_none", -input_attr=>{-name=>'check_none', -class=>"button_input", -value=>$r->maketext('Unselect all users'), + onClick => "for (i in document.userlist.elements) { + if (document.userlist.elements[i].name =='selected_users') { + document.userlist.elements[i].checked = false + } + }" }); + unless ($editMode or $passwordMode) { + print $selectAll." ". $selectNone; + } + print WeBWorK::CGI_labeled_input(-type=>"reset", -id=>"clear_entries", -input_attr=>{-value=>$r->maketext("Clear"), -class=>"button_input"}); + print WeBWorK::CGI_labeled_input(-type=>"submit", -id=>"take_action", -input_attr=>{-value=>$r->maketext("Take Action!"), -class=>"button_input"}).CGI::br().CGI::br(); + # print CGI::end_table(); + + ########## print table + + print CGI::p({},$r->maketext("Showing [_1] out of [_2] users", scalar @Users, scalar @allUserIDs)); + + print CGI::p($r->maketext("If a password field is left blank, the student's current password will be maintained.")) if $passwordMode; + if ($editMode) { + + + print CGI::p($r->maketext('Click on the login name to edit individual problem set data, (e.g. due dates) for these students.')); + } + $self->printTableHTML(\@Users, \@PermissionLevels, \%prettyFieldNames, + editMode => $editMode, + passwordMode => $passwordMode, + selectedUserIDs => \@selectedUserIDs, + primarySortField => $primarySortField, + secondarySortField => $secondarySortField, + visableUserIDs => \@visibleUserIDs, + ); + + + ########## print end of form + + print CGI::end_form(); + + return ""; +} + +################################################################################ +# extract particular params and put them in a hash (values are ARRAYREFs!) +################################################################################ + +sub getActionParams { + my ($self, $actionID) = @_; + my $r = $self->{r}; + + my %actionParams; + foreach my $param ($r->param) { + next unless $param =~ m/^action\.$actionID\./; + $actionParams{$param} = [ $r->param($param) ]; + } + return %actionParams; +} + +sub getTableParams { + my ($self) = @_; + my $r = $self->{r}; + + my %tableParams; + foreach my $param ($r->param) { + next unless $param =~ m/^(?:user|permission)\./; + $tableParams{$param} = [ $r->param($param) ]; + } + return %tableParams; +} + +################################################################################ +# actions and action triggers +################################################################################ + +# filter, edit, cancelEdit, and saveEdit should stay with the display module and +# not be real "actions". that way, all actions are shown in view mode and no +# actions are shown in edit mode. + +sub filter_form { + my ($self, $onChange, %actionParams) = @_; + #return CGI::table({}, CGI::Tr({-valign=>"top"}, + # CGI::td({}, + my $r = $self->r; + + my %prettyFieldNames = %{ $self->{prettyFieldNames} }; + + return join("", + WeBWorK::CGI_labeled_input( + -type=>"select", + -id=>"filter_select", + -label_text=>$r->maketext("Show Which Users?").": ", + -input_attr=>{ + -name => "action.filter.scope", + -values => [qw(all none selected match_regex)], + -default => $actionParams{"action.filter.scope"}->[0] || "match_regex", + -labels => { + all => $r->maketext("all users"), + none => $r->maketext("no users"), + selected => $r->maketext("selected users"), + match_regex => $r->maketext("users who match on selected field"), + }, + -onchange => $onChange, + } + ), + CGI::br(), + " ", + CGI::div({-id=>"filter_elements"}, + WeBWorK::CGI_labeled_input( + -type=>"select", + -id=>"filter_type_select", + -label_text=>$r->maketext("What field should filtered users match on?").": ", + -input_attr=>{ + -name => "action.filter.field", + -value => [ keys %{ FIELD_PROPERTIES() } ], + -default => $actionParams{"action.filter.field"}->[0] || "user_id", + -labels => \%prettyFieldNames, + -onchange => $onChange + } + ), + CGI::br(), + WeBWorK::CGI_labeled_input( + -type=>"text", + -id=>"filter_text", + -label_text=>$r->maketext("Filter by what text?").": ", + -input_attr=>{ + -name => "action.filter.user_ids", + -value => $actionParams{"action.filter.user_ids"}->[0] || "",, + -width => "50", + -onchange => $onChange, + } + ), + CGI::br(), + ), + ); +} + +# this action handler modifies the "visibleUserIDs" field based on the contents +# of the "action.filter.scope" parameter and the "selected_users" +# DBFIXME filtering should happen in the database! +sub filter_handler { + my ($self, $genericParams, $actionParams, $tableParams) = @_; + + my $r = $self->r; + my $db = $r->db; + + my $result; + + my $scope = $actionParams->{"action.filter.scope"}->[0]; + if ($scope eq "all") { + $result = $r->maketext("showing all users"); + $self->{visibleUserIDs} = $self->{allUserIDs}; + } elsif ($scope eq "none") { + $result = $r->maketext("showing no users"); + $self->{visibleUserIDs} = []; + } elsif ($scope eq "selected") { + $result = $r->maketext("showing selected users"); + $self->{visibleUserIDs} = $genericParams->{selected_users}; # an arrayref + } elsif ($scope eq "match_regex") { + $result = $r->maketext("showing matching users"); + my $regex = $actionParams->{"action.filter.user_ids"}->[0]; + my $field = $actionParams->{"action.filter.field"}->[0]; + my @userRecords = $db->getUsers(@{$self->{allUserIDs}}); + my @userIDs; + foreach my $record (@userRecords) { + next unless $record; + + # add permission level to user record hash so we can match it if necessary + if ($field eq "permission") { + my $permissionLevel = $db->getPermissionLevel($record->user_id); + $record->{permission} = $permissionLevel->permission; + } + push @userIDs, $record->user_id if $record->{$field} =~ /^$regex/i; + } + $self->{visibleUserIDs} = \@userIDs; + } elsif ($scope eq "match_ids") { + my @userIDs = split /\s*,\s*/, $actionParams->{"action.filter.user_ids"}->[0]; + $self->{visibleUserIDs} = \@userIDs; + } elsif ($scope eq "match_section") { + my $section = $actionParams->{"action.filter.section"}->[0]; + $self->{visibleUserIDs} = $self->{sections}->{$section}; # an arrayref + } elsif ($scope eq "match_recitation") { + my $recitation = $actionParams->{"action.filter.recitation"}->[0]; + $self->{visibleUserIDs} = $self->{recitations}->{$recitation}; # an arrayref + } + + return $result; +} + +sub sort_form { + my ($self, $onChange, %actionParams) = @_; + my $r = $self->r; + + return join ("", + WeBWorK::CGI_labeled_input( + -type=>"select", + -id=>"sort_select_1", + -label_text=>$r->maketext("Sort by").": ", + -input_attr=>{ + -name => "action.sort.primary", + -values => [qw(user_id first_name last_name email_address student_id status section recitation comment permission)], + -default => $actionParams{"action.sort.primary"}->[0] || "last_name", + -labels => { + user_id => $r->maketext("Login Name"), + first_name => $r->maketext("First Name"), + last_name => $r->maketext("Last Name"), + email_address => $r->maketext("Email Address"), + student_id => $r->maketext("Student ID"), + status => $r->maketext("Enrollment Status"), + section => $r->maketext("Section"), + recitation => $r->maketext("Recitation"), + comment => $r->maketext("Comment"), + permission => $r->maketext("Permission Level") + }, + -onchange => $onChange, + }, + ), + CGI::br(), + WeBWorK::CGI_labeled_input( + -type=>"select", + -id=>"sort_select_2", + -label_text=>$r->maketext("Then by").": ", + -input_attr=>{ + -name => "action.sort.secondary", + -values => [qw(user_id first_name last_name email_address student_id status section recitation comment permission)], + -default => $actionParams{"action.sort.secondary"}->[0] || "first_name", + -labels => { + user_id => $r->maketext("Login Name"), + first_name => $r->maketext("First Name"), + last_name => $r->maketext("Last Name"), + email_address => $r->maketext("Email Address"), + student_id => $r->maketext("Student ID"), + status => $r->maketext("Enrollment Status"), + section => $r->maketext("Section"), + recitation => $r->maketext("Recitation"), + comment => $r->maketext("Comment"), + permission => $r->maketext("Permission Level") + }, + -onchange => $onChange, + }, + ), + CGI::br(), + WeBWorK::CGI_labeled_input( + -type=>"select", + -id=>"sort_select_3", + -label_text=>$r->maketext("Then by").": ", + -input_attr=>{ + -name => "action.sort.ternary", + -values => [qw(user_id first_name last_name email_address student_id status section recitation comment permission)], + -default => $actionParams{"action.sort.ternary"}->[0] || "user_id", + -labels => { + user_id => $r->maketext("Login Name"), + first_name => $r->maketext("First Name"), + last_name => $r->maketext("Last Name"), + email_address => $r->maketext("Email Address"), + student_id => $r->maketext("Student ID"), + status => $r->maketext("Enrollment Status"), + section => $r->maketext("Section"), + recitation => $r->maketext("Recitation"), + comment => $r->maketext("Comment"), + permission => $r->maketext("Permission Level") + }, + -onchange => $onChange, + }, + ), + ); +} + +sub sort_handler { + my ($self, $genericParams, $actionParams, $tableParams) = @_; + my $r = $self->r; + + my $primary = $actionParams->{"action.sort.primary"}->[0]; + my $secondary = $actionParams->{"action.sort.secondary"}->[0]; + my $ternary = $actionParams->{"action.sort.ternary"}->[0]; + + $self->{primarySortField} = $primary; + $self->{secondarySortField} = $secondary; + $self->{ternarySortField} = $ternary; + + my %names = ( + user_id => $r->maketext("Login Name"), + first_name => $r->maketext("First Name"), + last_name => $r->maketext("Last Name"), + email_address => $r->maketext("Email Address"), + student_id => $r->maketext("Student ID"), + status => $r->maketext("Enrollment Status"), + section => $r->maketext("Section"), + recitation => $r->maketext("Recitation"), + comment => $r->maketext("Comment"), + permission => $r->maketext("Permission Level") + ); + + return $r->maketext("Users sorted by [_1], then by [_2], then by [_3]", $names{$primary}, $names{$secondary}, $names{$ternary}); +} + +sub edit_form { + my ($self, $onChange, %actionParams) = @_; + my $r = $self->r; + + return join("", + WeBWorK::CGI_labeled_input( + -type=>"select", + -id=>"edit_select", + -label_text=>$r->maketext("Edit Which Users?").": ", + -input_attr=>{ + -name => "action.edit.scope", + -values => [qw(all visible selected)], + -default => $actionParams{"action.edit.scope"}->[0] || "selected", + -labels => { + all => $r->maketext("all users"), + visible => $r->maketext("visible users"), + selected => $r->maketext("selected users") + }, + -onchange => $onChange, + } + ), + ); +} + +sub edit_handler { + my ($self, $genericParams, $actionParams, $tableParams) = @_; + my $r = $self->r; + + my $result; + + my $scope = $actionParams->{"action.edit.scope"}->[0]; + if ($scope eq "all") { + $result = $r->maketext("editing all users"); + $self->{visibleUserIDs} = $self->{allUserIDs}; + } elsif ($scope eq "visible") { + $result = $r->maketext("editing visible users"); + # leave visibleUserIDs alone + } elsif ($scope eq "selected") { + $result = $r->maketext("editing selected users"); + $self->{visibleUserIDs} = $genericParams->{selected_users}; # an arrayref + } + $self->{editMode} = 1; + + return $result; +} + + +sub password_form { + my ($self, $onChange, %actionParams) = @_; + my $r = $self->r; + + return join("", + WeBWorK::CGI_labeled_input( + -type=>"select", + -id=>"password_select", + -label_text=>$r->maketext("Give new password to which users?").": ", + -input_attr=>{ + -name => "action.password.scope", + -values => [qw(all visible selected)], + -default => $actionParams{"action.password.scope"}->[0] || "selected", + -labels => { + all => $r->maketext("all users"), + visible => $r->maketext("visible users"), + selected => $r->maketext("selected users") + }, + -onchange => $onChange, + }, + ), + ); +} + +sub password_handler { + my ($self, $genericParams, $actionParams, $tableParams) = @_; + my $r = $self->r; + + my $result; + + my $scope = $actionParams->{"action.password.scope"}->[0]; + if ($scope eq "all") { + $result = $r->maketext("giving new passwords to all users"); + $self->{visibleUserIDs} = $self->{allUserIDs}; + } elsif ($scope eq "visible") { + $result = $r->maketext("giving new passwords to visible users"); + # leave visibleUserIDs alone + } elsif ($scope eq "selected") { + $result = $r->maketext("giving new passwords to selected users"); + $self->{visibleUserIDs} = $genericParams->{selected_users}; # an arrayref + } + $self->{passwordMode} = 1; + + return $result; +} + +sub delete_form { + my ($self, $onChange, %actionParams) = @_; + my $r = $self->r; + + return join("", + CGI::span({-class=>"ResultsWithError"}, CGI::em($r->maketext("Warning: Deletion destroys all user-related data and is not undoable!"))),CGI::br(), + WeBWorK::CGI_labeled_input( + -type=>"select", + -id=>"delete_select", + -label_text=>$r->maketext("Delete how many?").": ", + -input_attr=>{ + -name => "action.delete.scope", + -values => [qw(none selected)], + -default => $actionParams{"action.delete.scope"}->[0] || "none", + -labels => { + none => $r->maketext("no users"), + # visible => "visible users", + selected => $r->maketext("selected users") + }, + -onchange => $onChange, + }, + ), + ); +} + +sub delete_handler { + my ($self, $genericParams, $actionParams, $tableParams) = @_; + my $r = $self->r; + my $db = $r->db; + my $user = $r->param('user'); + my $scope = $actionParams->{"action.delete.scope"}->[0]; + + my @userIDsToDelete = (); + #if ($scope eq "visible") { + # @userIDsToDelete = @{ $self->{visibleUserIDs} }; + #} elsif ($scope eq "selected") { + if ($scope eq "selected") { + @userIDsToDelete = @{ $self->{selectedUserIDs} }; + } + + my %allUserIDs = map { $_ => 1 } @{ $self->{allUserIDs} }; + my %visibleUserIDs = map { $_ => 1 } @{ $self->{visibleUserIDs} }; + my %selectedUserIDs = map { $_ => 1 } @{ $self->{selectedUserIDs} }; + + my $error = ""; + my $num = 0; + foreach my $userID (@userIDsToDelete) { + if ($user eq $userID) { # don't delete yourself!! + $error = $r->maketext("You cannot delete yourself!"); + next; + } + delete $allUserIDs{$userID}; + delete $visibleUserIDs{$userID}; + delete $selectedUserIDs{$userID}; + $db->deleteUser($userID); + $num++; + } + + $self->{allUserIDs} = [ keys %allUserIDs ]; + $self->{visibleUserIDs} = [ keys %visibleUserIDs ]; + $self->{selectedUserIDs} = [ keys %selectedUserIDs ]; + + return $error ? $error : $r->maketext("deleted [_1] users", $num); +} +sub add_form { + my ($self, $onChange, %actionParams) = @_; + my $r = $self->r; + + return WeBWorK::CGI_labeled_input(-type=>"text", -id=>"add_entry", -label_text=>$r->maketext("Add how many students?").": ", -input_attr=>{name=>'number_of_students', value=>1,size => 3}); +} + +sub add_handler { + my ($self, $genericParams, $actionParams, $tableParams) = @_; + # This action is redirected to the addUser.pm module using ../instructor/add_user/... + return "Nothing done by add student handler"; +} + +sub import_form { + my ($self, $onChange, %actionParams) = @_; + my $r = $self->r; + + return join(" ", + WeBWorK::CGI_labeled_input( + -type=>"select", + -id=>"import_select_source", + -label_text=>$r->maketext("Import users from what file?").": ", + -input_attr=>{ + -name => "action.import.source", + -values => [ $self->getCSVList() ], + -default => $actionParams{"action.import.source"}->[0] || "", + -onchange => $onChange, + } + ), + CGI::br(), + WeBWorK::CGI_labeled_input( + -type=>"select", + -id=>"import_select_replace", + -label_text=>$r->maketext("Replace which users?").": ", + -input_attr=>{ + -name => "action.import.replace", + -values => [qw(any visible selected none)], + -default => $actionParams{"action.import.replace"}->[0] || "none", + -labels => { + any => $r->maketext("any users"), + visible => $r->maketext("visible users"), + selected => $r->maketext("selected users"), + none => $r->maketext("no users"), + }, + -onchange => $onChange, + } + ), + CGI::br(), + WeBWorK::CGI_labeled_input( + -type=>"select", + -id=>"import_select_add", + -label_text=>$r->maketext("Add which new users?").": ", + -input_attr=>{ + -name => "action.import.add", + -values => [qw(any none)], + -default => $actionParams{"action.import.add"}->[0] || "any", + -labels => { + any => $r->maketext("any users"), + none => $r->maketext("no users"), + }, + -onchange => $onChange, + } + ), + ); +} + +sub import_handler { + my ($self, $genericParams, $actionParams, $tableParams) = @_; + my $r = $self->r; + + my $source = $actionParams->{"action.import.source"}->[0]; + my $add = $actionParams->{"action.import.add"}->[0]; + my $replace = $actionParams->{"action.import.replace"}->[0]; + + my $fileName = $source; + my $createNew = $add eq "any"; + my $replaceExisting; + my @replaceList; + if ($replace eq "any") { + $replaceExisting = "any"; + } elsif ($replace eq "none") { + $replaceExisting = "none"; + } elsif ($replace eq "visible") { + $replaceExisting = "listed"; + @replaceList = @{ $self->{visibleUserIDs} }; + } elsif ($replace eq "selected") { + $replaceExisting = "listed"; + @replaceList = @{ $self->{selectedUserIDs} }; + } + + my ($replaced, $added, $skipped) + = $self->importUsersFromCSV($fileName, $createNew, $replaceExisting, @replaceList); + + # make new users visible... do we really want to do this? probably. + push @{ $self->{visibleUserIDs} }, @$added; + + my $numReplaced = @$replaced; + my $numAdded = @$added; + my $numSkipped = @$skipped; + + return $r->maketext("[_1] users replaced, [_2] users added, [_3] users skipped. Skipped users: ([_4])", $numReplaced, $numAdded, $numSkipped, join (", ", @$skipped)); +} + +sub export_form { + my ($self, $onChange, %actionParams) = @_; + my $r = $self->r; + + return join("", + WeBWorK::CGI_labeled_input( + -type=>"select", + -id=>"export_select_scope", + -label_text=>$r->maketext("Export which users?").": ", + -input_attr=>{ + -name => "action.export.scope", + -values => [qw(all visible selected)], + -default => $actionParams{"action.export.scope"}->[0] || "visible", + -labels => { + all => $r->maketext("all users"), + visible => $r->maketext("visible users"), + selected => $r->maketext("selected users") + }, + -onchange => $onChange, + } + ), + CGI::br(), + WeBWorK::CGI_labeled_input( + -type=>"select", + -id=>"export_select_target", + -label_text=>$r->maketext("Export to what kind of file?").": ", + -input_attr=>{ + -name=>"action.export.target", + -values => [ "new", $self->getCSVList() ], + -labels => { new => $r->maketext("Enter filename below") }, + -default => $actionParams{"action.export.target"}->[0] || "", + -onchange => $onChange, + } + ), + CGI::br(), + CGI::div({-id=>"export_elements"}, + WeBWorK::CGI_labeled_input( + -type=>"text", + -id=>"export_filename", + -label_text=>$r->maketext("Filename").": ", + -input_attr=>{ + -name => "action.export.new", + -value => $actionParams{"action.export.new"}->[0] || "",, + -width => "50", + -onchange => $onChange, + } + ), + CGI::tt(".lst"), + ), + ); +} + +sub export_handler { + my ($self, $genericParams, $actionParams, $tableParams) = @_; + my $r = $self->r; + my $ce = $r->ce; + my $dir = $ce->{courseDirs}->{templates}; + + my $scope = $actionParams->{"action.export.scope"}->[0]; + my $target = $actionParams->{"action.export.target"}->[0]; + my $new = $actionParams->{"action.export.new"}->[0]; + + #get name of templates directory as it appears in file manager + $dir =~ s|.*/||; + + my $fileName; + if ($target eq "new") { + $fileName = $new; + } else { + $fileName = $target; + } + + $fileName .= ".lst" unless $fileName =~ m/\.lst$/; + + my @userIDsToExport; + if ($scope eq "all") { + @userIDsToExport = @{ $self->{allUserIDs} }; + } elsif ($scope eq "visible") { + @userIDsToExport = @{ $self->{visibleUserIDs} }; + } elsif ($scope eq "selected") { + @userIDsToExport = @{ $self->{selectedUserIDs} }; + } + + $self->exportUsersToCSV($fileName, @userIDsToExport); + + return $r->maketext("[_1] users exported to file [_2]/[_3]", scalar @userIDsToExport, $dir, $fileName); +} + +sub cancelEdit_form { + my ($self, $onChange, %actionParams) = @_; + my $r = $self->r; + return CGI::span("-".$r->maketext("Abandon changes")); +} + +sub cancelEdit_handler { + my ($self, $genericParams, $actionParams, $tableParams) = @_; + my $r = $self->r; + + #$self->{selectedUserIDs} = $self->{visibleUserIDs}; + # only do the above if we arrived here via "edit selected users" + if (defined $r->param("prev_visible_users")) { + $self->{visibleUserIDs} = [ $r->param("prev_visible_users") ]; + } elsif (defined $r->param("no_prev_visible_users")) { + $self->{visibleUserIDs} = []; + } else { + # leave it alone + } + $self->{editMode} = 0; + + return $r->maketext("Changes abandoned"); +} + +sub saveEdit_form { + my ($self, $onChange, %actionParams) = @_; + my $r = $self->r; + return CGI::span("-".$r->maketext("Save changes")); +} + +sub saveEdit_handler { + my ($self, $genericParams, $actionParams, $tableParams) = @_; + my $r = $self->r; + my $db = $r->db; + + my @visibleUserIDs = @{ $self->{visibleUserIDs} }; + foreach my $userID (@visibleUserIDs) { + my $User = $db->getUser($userID); # checked + die $r->maketext("record for visible user [_1] not found", $userID) unless $User; + my $PermissionLevel = $db->getPermissionLevel($userID); # checked + die $r->maketext("permissions for [_1] not defined", $userID) unless defined $PermissionLevel; + foreach my $field ($User->NONKEYFIELDS()) { + my $param = "user.${userID}.${field}"; + if (defined $tableParams->{$param}->[0]) { + $User->$field($tableParams->{$param}->[0]); + } + } + + foreach my $field ($PermissionLevel->NONKEYFIELDS()) { + my $param = "permission.${userID}.${field}"; + if (defined $tableParams->{$param}->[0]) { + $PermissionLevel->$field($tableParams->{$param}->[0]); + } + } + + $db->putUser($User); + $db->putPermissionLevel($PermissionLevel); + } + + if (defined $r->param("prev_visible_users")) { + $self->{visibleUserIDs} = [ $r->param("prev_visible_users") ]; + } elsif (defined $r->param("no_prev_visible_users")) { + $self->{visibleUserIDs} = []; + } else { + # leave it alone + } + + $self->{editMode} = 0; + + return $r->maketext("Changes saved"); +} + +sub cancelPassword_form { + my ($self, $onChange, %actionParams) = @_; + my $r = $self->r; + return CGI::span("-".$r->maketext("Abandon changes")); +} + +sub cancelPassword_handler { + my ($self, $genericParams, $actionParams, $tableParams) = @_; + my $r = $self->r; + + #$self->{selectedUserIDs} = $self->{visibleUserIDs}; + # only do the above if we arrived here via "edit selected users" + if (defined $r->param("prev_visible_users")) { + $self->{visibleUserIDs} = [ $r->param("prev_visible_users") ]; + } elsif (defined $r->param("no_prev_visible_users")) { + $self->{visibleUserIDs} = []; + } else { + # leave it alone + } + $self->{passwordMode} = 0; + + return $r->maketext("Changes abandoned"); +} + +sub savePassword_form { + my ($self, $onChange, %actionParams) = @_; + my $r = $self->r; + return CGI::span("-".$r->maketext("Save changes")); +} + +sub savePassword_handler { + my ($self, $genericParams, $actionParams, $tableParams) = @_; + my $r = $self->r; + my $db = $r->db; + + my @visibleUserIDs = @{ $self->{visibleUserIDs} }; + foreach my $userID (@visibleUserIDs) { + my $User = $db->getUser($userID); # checked + die $r->maketext("record for visible user [_1] not found", $userID) unless $User; + my $param = "user.${userID}.new_password"; + if ((defined $tableParams->{$param}->[0]) and ($tableParams->{$param}->[0])) { + my $newP = $tableParams->{$param}->[0]; + my $Password = eval {$db->getPassword($User->user_id)}; # checked + my $cryptPassword = cryptPassword($newP); + $Password->password(cryptPassword($newP)); + eval { $db->putPassword($Password) }; + } + } + + if (defined $r->param("prev_visible_users")) { + $self->{visibleUserIDs} = [ $r->param("prev_visible_users") ]; + } elsif (defined $r->param("no_prev_visible_users")) { + $self->{visibleUserIDs} = []; + } else { + # leave it alone + } + + $self->{passwordMode} = 0; + + return $r->maketext("New passwords saved"); +} + + +################################################################################ +# sorts +################################################################################ + +sub byUserID { lc $a->user_id cmp lc $b->user_id } +sub byFirstName { (defined $a->first_name && defined $b->first_name) ? lc $a->first_name cmp lc $b->first_name : 0; } +sub byLastName { (defined $a->last_name && defined $b->last_name ) ? lc $a->last_name cmp lc $b->last_name : 0; } +sub byEmailAddress { lc $a->email_address cmp lc $b->email_address } +sub byStudentID { lc $a->student_id cmp lc $b->student_id } +sub byStatus { lc $a->status cmp lc $b->status } +sub bySection { lc $a->section cmp lc $b->section } +sub byRecitation { lc $a->recitation cmp lc $b->recitation } +sub byComment { lc $a->comment cmp lc $b->comment } +sub byPermission { $a->{permission} <=> $b->{permission} } ## permission level is added to user record hash so we can sort it if necessary + +# sub byLnFnUid { &byLastName || &byFirstName || &byUserID } + +################################################################################ +# utilities +################################################################################ + +# generate labels for section/recitation popup menus +sub menuLabels { + my ($self, $hashRef) = @_; + my %hash = %$hashRef; + + my %result; + foreach my $key (keys %hash) { + my $count = @{ $hash{$key} }; + my $displayKey = $key || ""; + $result{$key} = "$displayKey ($count users)"; + } + return %result; +} + +# FIXME REFACTOR this belongs in a utility class so that addcourse can use it! +# (we need a whole suite of higher-level import/export functions somewhere) +sub importUsersFromCSV { + my ($self, $fileName, $createNew, $replaceExisting, @replaceList) = @_; + my $r = $self->r; + my $ce = $r->ce; + my $db = $r->db; + my $dir = $ce->{courseDirs}->{templates}; + my $user = $r->param('user'); + + die $r->maketext("illegal character in input: '/'") if $fileName =~ m|/|; + die $r->maketext("won't be able to read from file [_1]/[_2]: does it exist? is it readable?", $dir, $fileName) + unless -r "$dir/$fileName"; + + my %allUserIDs = map { $_ => 1 } @{ $self->{allUserIDs} }; + my %replaceOK; + if ($replaceExisting eq "none") { + %replaceOK = (); + } elsif ($replaceExisting eq "listed") { + %replaceOK = map { $_ => 1 } @replaceList; + } elsif ($replaceExisting eq "any") { + %replaceOK = %allUserIDs; + } + + my $default_permission_level = $ce->{default_permission_level}; + + my (@replaced, @added, @skipped); + + # get list of hashrefs representing lines in classlist file + my @classlist = parse_classlist("$dir/$fileName"); + + # Default status is enrolled -- fetch abbreviation for enrolled + my $default_status_abbrev = $ce->{statuses}->{Enrolled}->{abbrevs}->[0]; + + foreach my $record (@classlist) { + my %record = %$record; + my $user_id = $record{user_id}; + + unless (WeBWorK::DB::check_user_id($user_id) ) { # try to catch lines with bad characters + push @skipped, $user_id; + next; + } + if ($user_id eq $user) { # don't replace yourself!! + push @skipped, $user_id; + next; + } + + if (exists $allUserIDs{$user_id} and not exists $replaceOK{$user_id}) { + push @skipped, $user_id; + next; + } + + if (not exists $allUserIDs{$user_id} and not $createNew) { + push @skipped, $user_id; + next; + } + + # set default status is status field is "empty" + $record{status} = $default_status_abbrev + unless defined $record{status} and $record{status} ne ""; + + # set password from student ID if password field is "empty" + if (not defined $record{password} or $record{password} eq "") { + if (defined $record{student_id} and $record{student_id} ne "") { + # crypt the student ID and use that + $record{password} = cryptPassword($record{student_id}); + } else { + # an empty password field in the database disables password login + $record{password} = ""; + } + } + + # set default permission level if permission level is "empty" + $record{permission} = $default_permission_level + unless defined $record{permission} and $record{permission} ne ""; + + my $User = $db->newUser(%record); + my $PermissionLevel = $db->newPermissionLevel(user_id => $user_id, permission => $record{permission}); + my $Password = $db->newPassword(user_id => $user_id, password => $record{password}); + + # DBFIXME use REPLACE + if (exists $allUserIDs{$user_id}) { + $db->putUser($User); + $db->putPermissionLevel($PermissionLevel); + $db->putPassword($Password); + push @replaced, $user_id; + } else { + $db->addUser($User); + $db->addPermissionLevel($PermissionLevel); + $db->addPassword($Password); + push @added, $user_id; + } + } + + return \@replaced, \@added, \@skipped; +} + +sub exportUsersToCSV { + my ($self, $fileName, @userIDsToExport) = @_; + my $r = $self->r; + my $ce = $r->ce; + my $db = $r->db; + my $dir = $ce->{courseDirs}->{templates}; + + die $r->maketext("illegal character in input: '/'") if $fileName =~ m|/|; + + my @records; + + # DBFIXME use an iterator here + my @Users = $db->getUsers(@userIDsToExport); + my @Passwords = $db->getPasswords(@userIDsToExport); + my @PermissionLevels = $db->getPermissionLevels(@userIDsToExport); + foreach my $i (0 .. $#userIDsToExport) { + my $User = $Users[$i]; + my $Password = $Passwords[$i]; + my $PermissionLevel = $PermissionLevels[$i]; + next unless defined $User; + my %record = ( + defined $PermissionLevel ? $PermissionLevel->toHash : (), + defined $Password ? $Password->toHash : (), + $User->toHash, + ); + push @records, \%record; + } + + write_classlist("$dir/$fileName", @records); +} + +################################################################################ +# "display" methods +################################################################################ + +sub fieldEditHTML { + my ($self, $fieldName, $value, $properties) = @_; + my $r = $self->r; + my $ce = $self->r->ce; + my $size = $properties->{size}; + my $type = $properties->{type}; + my $access = $properties->{access}; + my $items = $properties->{items}; + my $synonyms = $properties->{synonyms}; + + if ($type eq "email") { + if ($value eq ' ') { + return $value;} + else { + return CGI::a({-href=>"mailto:$value"},$value); + } + } + + if ($access eq "readonly") { + # hack for status + if ($type eq "status") { + my $status_name = $ce->status_abbrev_to_name($value); + if (defined $status_name) { + $value = "$status_name ($value)"; + } + } + return $value; + } + + if ($type eq "number" or $type eq "text") { + return WeBWorK::CGI_labeled_input(-type=>"text", -id=>$fieldName."_id", -label_text=>$r->maketext("Edit").":", -input_attr=>{name=>$fieldName, value=>$value, size=>$size}); + } + + if ($type eq "enumerable") { + my $matched = undef; # Whether a synonym match has occurred + + # Process synonyms for enumerable objects + foreach my $synonym (keys %$synonyms) { + if ($synonym ne "*" and $value =~ m/$synonym/) { + $value = $synonyms->{$synonym}; + $matched = 1; + } + } + + if (!$matched and exists $synonyms->{"*"}) { + $value = $synonyms->{"*"}; + } + + return WeBWorK::CGI_labeled_input( + -type=>"select", + -id=>$fieldName."_id", + -label_text=>$r->maketext(ucfirst($fieldName)), + -input_attr=>{ + name => $fieldName, + values => [keys %$items], + default => $value, + labels => $items, + } + ), + } + + if ($type eq "status") { + # we used to surreptitously map synonyms to a canonical value... + # so should we continue to do that? + my $status_name = $ce->status_abbrev_to_name($value); + if (defined $status_name) { + $value = ($ce->status_name_to_abbrevs($status_name))[0]; + } + + my (@values, %labels); + while (my ($k, $v) = each %{$ce->{statuses}}) { + my @abbrevs = @{$v->{abbrevs}}; + push @values, $abbrevs[0]; + foreach my $abbrev (@abbrevs) { + $labels{$abbrev} = $r->maketext($k); + } + } + + return WeBWorK::CGI_labeled_input( + -type=>"select", + -id=>$fieldName."_id", + -label_text=>$r->maketext("Edit").":", + -input_attr=>{ + name => $fieldName, + values => \@values, + default => $value, + labels => \%labels, + } + ), + } + + if ($type eq "permission") { + my ($default, @values, %labels); + my %roles = %{$ce->{userRoles}}; + foreach my $role (sort {$roles{$a}<=>$roles{$b}} keys(%roles) ) { + my $val = $roles{$role}; + + push(@values, $val); + $labels{$val} = $role; + $default = $val if ( $value eq $role ); + } + + return WeBWorK::CGI_labeled_input( + -type=>"select", + -id=>$fieldName."_id", + -label_text=>$r->maketext("Edit").":", + -input_attr=>{ + -name => $fieldName, + -values => \@values, + -default => [$default], # force default of 0 to be a selector value (instead of + # being considered as a null -- now works with CGI 3.42 + #-default => $default, # works with CGI 3.49 (but the above does not, go figure + -labels => \%labels, + -override => 1, # force default value to be selected. (corrects bug on newer CGI + } + ), + } +} + +sub recordEditHTML { + my ($self, $User, $PermissionLevel, %options) = @_; + my $r = $self->r; + my $urlpath = $r->urlpath; + my $db = $r->db; + my $ce = $r->ce; + my $authz = $r->authz; + my $user = $r->param('user'); + my $root = $ce->{webworkURLs}->{root}; + my $courseName = $urlpath->arg("courseID"); + + my $editMode = $options{editMode}; + my $passwordMode = $options{passwordMode}; + my $userSelected = $options{userSelected}; + + my $statusClass = $ce->status_abbrev_to_name($User->status); + + my $sets = $db->countUserSets($User->user_id); + my $totalSets = $self->{totalSets}; + + my $changeEUserURL = $self->systemLink($urlpath->new(type=>'set_list',args=>{courseID=>$courseName}), + params => {effectiveUser => $User->user_id} + ); + + my $setsAssignedToUserURL = $self->systemLink($urlpath->new(type=>'instructor_user_detail', + args=>{courseID => $courseName, + userID => $User->user_id + }), + params => {effectiveUser => $User->user_id} + ); + + my $userListURL = $self->systemLink($urlpath->new(type=>'instructor_user_list2', args=>{courseID => $courseName} )) . "&editMode=1&visible_users=" . $User->user_id; + + my $imageURL = $ce->{webworkURLs}->{htdocs}."/images/edit.gif"; + my $imageLink = CGI::a({href => $userListURL}, CGI::img({src=>$imageURL, border=>0, alt=>"Link to Edit Page for ".$User->user_id})); + + my @tableCells; + + # Select + if ($editMode or $passwordMode) { + # column not there + } else { + # selection checkbox + # push @tableCells, CGI::checkbox( + # -name => "selected_users", + # -value => $User->user_id, + # -checked => $userSelected, + # -label => "", + my $label = ""; + if ( FIELD_PERMS()->{act_as} and not $authz->hasPermissions($user, FIELD_PERMS()->{act_as}) ){ + $label = $User->user_id . $imageLink; + } else { + $label = CGI::a({href=>$changeEUserURL}, $User->user_id) . $imageLink; + } + + push @tableCells, WeBWorK::CGI_labeled_input( + -type=>"checkbox", + -id=>$User->user_id."_checkbox", + -label_text=>$label, + -input_attr=> $userSelected ? + { + -name => "selected_users", + -value => $User->user_id, + -checked => "checked", + -class=>"table_checkbox", + } + : + { + -name => "selected_users", + -value => $User->user_id, + -class=>"table_checkbox", + } + ); + } + + # Act As + # if ($editMode or $passwordMode) { + # # column not there + # } else { + # # selection checkbox + # if ( FIELD_PERMS()->{act_as} and not $authz->hasPermissions($user, FIELD_PERMS()->{act_as}) ){ + # push @tableCells, $User->user_id . $imageLink; + # } else { + # push @tableCells, CGI::a({href=>$changeEUserURL}, $User->user_id) . $imageLink; + # } + # } + + # Login Status + if ($editMode or $passwordMode) { + # column not there + } else { + # check to see if a user is currently logged in + # DBFIXME use a WHERE clause + my $Key = $db->getKey($User->user_id); + my $is_active = ($Key and time <= $Key->timestamp()+$ce->{sessionKeyTimeout}); # cribbed from check_session + push @tableCells, $is_active ? CGI::b($r->maketext("Active")) : CGI::em($r->maketext("Inactive")); + } + + # change password (only in password mode) + if ($passwordMode) { + if ($User->user_id eq $user) { + push @tableCells, CGI::div({-class=>"ResultsWithError"},$r->maketext("You may not change your own password here!")) # don't allow a professor to change their own password from this form + } + else { + my $fieldName = 'user.' . $User->user_id . '.' . 'new_password'; + push @tableCells, WeBWorK::CGI_labeled_input(-type=>"text", -id=>"password_edit", -label_text=>$r->maketext("New Password").": ", -input_attr=>{name=>$fieldName, size=>14}); + } + } + # User ID (edit mode) or Assigned Sets (otherwise) + if ( $passwordMode) { + # straight user ID + push @tableCells, CGI::div({class=>$statusClass}, $User->user_id); + } elsif ($editMode) { + # straight user ID + my $userDetailPage = $urlpath->new(type =>'instructor_user_detail', + args =>{ + courseID => $courseName, + userID => $User->user_id, #FIXME eventually this should be a list?? + } + ); + my $userDetailUrl = $self->systemLink($userDetailPage,params =>{}); + push @tableCells, CGI::a({href=>$userDetailUrl}, $User->user_id); + + } else { + # "edit sets assigned to user" link + #push @tableCells, CGI::a({href=>$setsAssignedToUserURL}, "Edit sets"); + if ( FIELD_PERMS()->{sets} and not $authz->hasPermissions($user, FIELD_PERMS()->{sets}) ) { + push @tableCells, "$sets/$totalSets"; + } else { + push @tableCells, CGI::a({href=>$setsAssignedToUserURL}, "$sets/$totalSets"); + } + } + + # User Fields + foreach my $field ($User->NONKEYFIELDS) { + my $fieldName = 'user.' . $User->user_id . '.' . $field, + my $fieldValue = $User->$field; + my %properties = %{ FIELD_PROPERTIES()->{$field} }; + $properties{access} = 'readonly' unless $editMode; + $properties{type} = 'email' if ($field eq 'email_address' and !$editMode and !$passwordMode); + $fieldValue = $self->nbsp($fieldValue) unless $editMode; + push @tableCells, CGI::div({class=>$statusClass}, $self->fieldEditHTML($fieldName, $fieldValue, \%properties)); + } + + # PermissionLevel Fields + foreach my $field ($PermissionLevel->NONKEYFIELDS) { + my $fieldName = 'permission.' . $PermissionLevel->user_id . '.' . $field, + my $fieldValue = $PermissionLevel->$field; + # get name out of permission level + if ( $field eq 'permission' ) { + ($fieldValue) = grep { $ce->{userRoles}->{$_} eq $fieldValue } ( keys ( %{$ce->{userRoles}} ) ); + } + my %properties = %{ FIELD_PROPERTIES()->{$field} }; + $properties{access} = 'readonly' unless $editMode; + $fieldValue = $self->nbsp($fieldValue) unless $editMode; + push @tableCells, CGI::div({class=>$statusClass}, $self->fieldEditHTML($fieldName, $fieldValue, \%properties)); + } + + return CGI::Tr({}, CGI::td({nowrap=>1}, \@tableCells)); +} + +sub printTableHTML { + my ($self, $UsersRef, $PermissionLevelsRef, $fieldNamesRef, %options) = @_; + my $r = $self->r; + my $urlpath = $r->urlpath; + my $courseName = $urlpath->arg("courseID"); + my $userTemplate = $self->{userTemplate}; + my $permissionLevelTemplate = $self->{permissionLevelTemplate}; + my @Users = @$UsersRef; + my @PermissionLevels = @$PermissionLevelsRef; + my %fieldNames = %$fieldNamesRef; + + my $editMode = $options{editMode}; + my $passwordMode = $options{passwordMode}; + my %selectedUserIDs = map { $_ => 1 } @{ $options{selectedUserIDs} }; +# my $currentSort = $options{currentSort}; + my $primarySortField = $options{primarySortField}; + my $secondarySortField = $options{secondarySortField}; + my @visableUserIDs = @{ $options{visableUserIDs} }; + + # names of headings: + my @realFieldNames = ( + $userTemplate->KEYFIELDS, + $userTemplate->NONKEYFIELDS, + $permissionLevelTemplate->NONKEYFIELDS, + ); + +# my %sortSubs = %{ SORT_SUBS() }; + #my @stateParams = @{ STATE_PARAMS() }; + #my $hrefPrefix = $r->uri . "?" . $self->url_args(@stateParams); # $self->url_authen_args + my @tableHeadings; + foreach my $field (@realFieldNames) { + my $result = $fieldNames{$field}; + push @tableHeadings, $result; + }; + + # prepend selection checkbox? only if we're NOT editing! + unless($editMode or $passwordMode) { + + #warn "line 1582 visibleUserIDs=@visableUserIDs \n"; + my %current_state =(); + if (@visableUserIDs) { + # This is a hack to get around: Maximum URL Length Is 2,083 Characters in Internet Explorer. + # Without passing visable users the URL is about 250 characters. If the total URL is under the limit + # we will pass visable users. If it is over, we will not pass any and all users will be displayed. + # Maybe we should replace the GET method by POST (but this doesn't look good) --- AKP + + my $visableUserIDsString = join ':', @visableUserIDs; + if (length($visableUserIDsString) < 1830) { + %current_state = ( + primarySortField => "$primarySortField", + secondarySortField => "$secondarySortField", + visable_user_string => "$visableUserIDsString" + ); + } else { + %current_state = ( + primarySortField => "$primarySortField", + secondarySortField => "$secondarySortField", + show_all_users => "1" + ); + } + } else { + %current_state = ( + primarySortField => "$primarySortField", + secondarySortField => "$secondarySortField", + no_visible_users => "1" + ); + } + @tableHeadings = ( + #"Select", + CGI::a({href => $self->systemLink($urlpath->new(type=>'instructor_user_list2', args=>{courseID => $courseName,} ), params=>{labelSortMethod=>'user_id', %current_state})}, $r->maketext('Login Name')), + $r->maketext("Login Status"), + $r->maketext("Assigned Sets"), + CGI::a({href => $self->systemLink($urlpath->new(type=>'instructor_user_list2', args=>{courseID => $courseName,} ), params=>{labelSortMethod=>'first_name', %current_state})}, $r->maketext('First Name')), + CGI::a({href => $self->systemLink($urlpath->new(type=>'instructor_user_list2', args=>{courseID => $courseName,} ), params=>{labelSortMethod=>'last_name', %current_state})}, $r->maketext('Last Name')), + CGI::a({href => $self->systemLink($urlpath->new(type=>'instructor_user_list2', args=>{courseID => $courseName,} ), params=>{labelSortMethod=>'email_address', %current_state})}, $r->maketext('Email Address')), + CGI::a({href => $self->systemLink($urlpath->new(type=>'instructor_user_list2', args=>{courseID => $courseName,} ), params=>{labelSortMethod=>'student_id', %current_state})}, $r->maketext('Student ID')), + CGI::a({href => $self->systemLink($urlpath->new(type=>'instructor_user_list2', args=>{courseID => $courseName,} ), params=>{labelSortMethod=>'status', %current_state})}, $r->maketext('Status')), + CGI::a({href => $self->systemLink($urlpath->new(type=>'instructor_user_list2', args=>{courseID => $courseName,} ), params=>{labelSortMethod=>'section', %current_state})}, $r->maketext('Section')), + CGI::a({href => $self->systemLink($urlpath->new(type=>'instructor_user_list2', args=>{courseID => $courseName,} ), params=>{labelSortMethod=>'recitation', %current_state})}, $r->maketext('Recitation')), + CGI::a({href => $self->systemLink($urlpath->new(type=>'instructor_user_list2', args=>{courseID => $courseName,} ), params=>{labelSortMethod=>'comment', %current_state})}, $r->maketext('Comment')), + CGI::a({href => $self->systemLink($urlpath->new(type=>'instructor_user_list2', args=>{courseID => $courseName,} ), params=>{labelSortMethod=>'permission', %current_state})}, $r->maketext('Permission Level')), + ) + } + if($passwordMode) { + unshift @tableHeadings, $r->maketext("New Password"); + } + + # print the table + if ($editMode or $passwordMode) { + print CGI::start_table({-nowrap=>0, -class=>"set_table", -summary=>$r->maketext("_USER_TABLE_SUMMARY") });# "A table showing all the current users along with several fields of user information. The fields from left to right are: Login Name, Login Status, Assigned Sets, First Name, Last Name, Email Address, Student ID, Enrollment Status, Section, Recitation, Comments, and Permission Level. Clicking on the links in the column headers will sort the table by the field it corresponds to. The Login Name fields contain checkboxes for selecting the user. Clicking the link of the name itself will allow you to act as the selected user. There will also be an image link following the name which will take you to a page where you can edit the selected user's information. Clicking the emails will allow you to email the corresponding user. Clicking the links in the entries in the assigned sets columns will take you to a page where you can view and reassign the sets for the selected user."}); + } else { + print CGI::start_table({-border=>1, -nowrap=>1, -class=>"set_table", -summary=>$r->maketext("_USER_TABLE_SUMMARY") });#"A table showing all the current users along with several fields of user information. The fields from left to right are: Login Name, Login Status, Assigned Sets, First Name, Last Name, Email Address, Student ID, Enrollment Status, Section, Recitation, Comments, and Permission Level. Clicking on the links in the column headers will sort the table by the field it corresponds to. The Login Name fields contain checkboxes for selecting the user. Clicking the link of the name itself will allow you to act as the selected user. There will also be an image link following the name which will take you to a page where you can edit the selected user's information. Clicking the emails will allow you to email the corresponding user. Clicking the links in the entries in the assigned sets columns will take you to a page where you can view and reassign the sets for the selected user."}); + } + + print CGI::caption($r->maketext("Users List")); + + print CGI::Tr({}, CGI::th({}, \@tableHeadings)); + + + for (my $i = 0; $i < @Users; $i++) { + my $User = $Users[$i]; + my $PermissionLevel = $PermissionLevels[$i]; + + print $self->recordEditHTML($User, $PermissionLevel, + editMode => $editMode, + passwordMode => $passwordMode, + userSelected => exists $selectedUserIDs{$User->user_id} + ); + } + + print CGI::end_table(); + ######################################### + # if there are no users shown print message + # + ########################################## + + print CGI::p( + CGI::i($r->maketext("No students shown. Choose one of the options above to list the students in the course.")) + ) unless @Users; +} + +# output_JS subroutine + +# prints out the necessary JS for this page + +sub output_JS{ + my $self = shift; + my $r = $self->r; + my $ce = $r->ce; + + my $site_url = $ce->{webworkURLs}->{htdocs}; + print CGI::start_script({type=>"text/javascript", src=>"$site_url/js/addOnLoadEvent.js"}), CGI::end_script(); + print CGI::start_script({type=>"text/javascript", src=>"$site_url/js/show_hide.js"}), CGI::end_script(); + print CGI::start_script({type=>"text/javascript", src=>"$site_url/js/classlist_handlers.js"}), CGI::end_script(); + return ""; +} + +1; + diff --git a/lib/WeBWorK/ContentGenerator/Login.pm b/lib/WeBWorK/ContentGenerator/Login.pm index 27ad28f744..d77c846a71 100644 --- a/lib/WeBWorK/ContentGenerator/Login.pm +++ b/lib/WeBWorK/ContentGenerator/Login.pm @@ -145,12 +145,15 @@ sub body { ); } + print CGI::start_div({-class=>"p_content"}); + if ( $externalAuth ) { print CGI::p({}, $r->maketext("_EXTERNAL_AUTH_MESSAGE", CGI::strong($r->maketext($course)))); - + print CGI::end_div(); } else { print CGI::p($r->maketext("Please enter your username and password for [_1] below:", CGI::b($r->maketext($course)))); print CGI::p($r->maketext("_LOGIN_MESSAGE", CGI::b($r->maketext("Remember Me")))); + print CGI::end_div(); print CGI::startform({-method=>"POST", -action=>$r->uri, -id=>"login_form"}); diff --git a/lib/WeBWorK/ContentGenerator/Options.pm b/lib/WeBWorK/ContentGenerator/Options.pm index 9a795e6e7b..e68c8d9d0e 100644 --- a/lib/WeBWorK/ContentGenerator/Options.pm +++ b/lib/WeBWorK/ContentGenerator/Options.pm @@ -87,12 +87,7 @@ sub body { } else { print CGI::div({class=>"ResultsWithError"}, CGI::p( - $r->maketext("The passwords you entered in the - [_1] and [_2] - fields don't match. Please retype your new password and try - again.", - CGI::b($r->maketext("[_1]'s New Password",$e_user_name)), - CGI::b($r->maketext("Confirm [_1]'s New Password",$e_user_name))) + $r->maketext("The passwords you entered in the [_1] and [_2] fields don't match. Please retype your new password and try again.", CGI::b($r->maketext("[_1]'s New Password",$e_user_name)), CGI::b($r->maketext("Confirm [_1]'s New Password",$e_user_name))) ), ); } @@ -103,11 +98,7 @@ sub body { } } else { print CGI::div({class=>"ResultsWithError"}, - CGI::p($r->maketext("The password you entered in the [_1] - field does not match your current - password. Please retype your current password and try - again.", - CGI::b($r->maketext("[_1]'s Current Password",$user_name))) + CGI::p($r->maketext("The password you entered in the [_1] field does not match your current password. Please retype your current password and try again.", CGI::b($r->maketext("[_1]'s Current Password",$user_name))) ), ); } diff --git a/lib/WeBWorK/ContentGenerator/Problem.pm b/lib/WeBWorK/ContentGenerator/Problem.pm index 1f95404ee7..a82756f04e 100644 --- a/lib/WeBWorK/ContentGenerator/Problem.pm +++ b/lib/WeBWorK/ContentGenerator/Problem.pm @@ -261,9 +261,9 @@ sub attemptResults { $answerMessage =~ s/\n/
/g; $numCorrect += $answerScore >= 1; $numBlanks++ unless $studentAnswer =~/\S/ || $answerScore >= 1; # unless student answer contains entry - my $resultString = $answerScore >= 1 ? CGI::span({class=>"ResultsWithoutError"}, $r->maketext("correct")) : + my $resultString = $answerScore >= 1 ? CGI::span({class=>"ResultsWithoutError"}, $r->maketext("Correct")) : $answerScore > 0 ? $r->maketext("[_1]% correct", int($answerScore*100)) : - CGI::span({class=>"ResultsWithError"}, $r->maketext("incorrect")); + CGI::span({class=>"ResultsWithError"}, $r->maketext("Incorrect")); $fully = $r->maketext("completely") if $answerScore >0 and $answerScore < 1; push @correct_ids, $name if $answerScore == 1; @@ -600,7 +600,7 @@ sub pre_header_initialize { $problem->problem_seed($problemSeed); } - my $visiblityStateClass = ($set->visible) ? $r->maketext("visible") : $r->maketext("hidden"); + my $visiblityStateClass = ($set->visible) ? $r->maketext("Visible") : $r->maketext("Hidden"); my $visiblityStateText = ($set->visible) ? $r->maketext("visible to students")."." : $r->maketext("hidden from students")."."; $self->addmessage(CGI::span($r->maketext("This set is [_1]", CGI::font({class=>$visiblityStateClass}, $visiblityStateText)))); @@ -722,6 +722,17 @@ sub pre_header_initialize { effectivePermissionLevel => $db->getPermissionLevel($effectiveUserName)->permission, }, ); + # sometimes, for example if the file can't be read, $pg->{pgcore} won't be defined + # because the PG file is never run + # + if (defined ($pg->{pgcore}) ) { + my @debug_msgs = @{ $pg->{pgcore}->get_debug_messages}; + $self->addmessage(join(CGI::br(),@debug_msgs) ) if @debug_msgs; + $self->{pgdebug} = $pg->{pgcore}->get_debug_messages; + $self->{pgwarning} = $pg->{pgcore}->get_warning_messages; + $self->{pginternalerrors} = $pg->{pgcore}->get_internal_debug_messages ; + $self->{pgerrors} = @{$self->{pgdebug}} || @{$self->{pgwarning}} || @{$self->{pginternalerrors}}; + } debug("end pg processing"); @@ -739,17 +750,39 @@ sub pre_header_initialize { $self->{will} = \%will; $self->{pg} = $pg; } - -sub if_errors($$) { - my ($self, $arg) = @_; +sub warnings { + my $self = shift; + $self->SUPER::warnings(); + my $r = $self->r; + my $pg = $self->{pg}; - if ($self->{isOpen}) { - return $self->{pg}->{flags}->{error_flag} ? $arg : !$arg; - } else { - return !$arg; + my @pgdebug = @{ $self->{pgdebug} }; + my @pgwarning = @{ $self->{pgwarning} }; + my @pginternalerrors = @{ $self->{pginternalerrors} }; +# my $pgerrordiv = $pgdebug||$pgwarning||$pginternalerrors; # is 1 if any of these are non-empty + # print warning messages + if ( $self->{pgerrors} ) { + print CGI::start_div(); + print CGI::h3({style=>"color:red;"}, $r->maketext("Additional Error Messages")); + print CGI::p(CGI::h3("PG debug messages"), CGI::br(), @pgdebug ) if @pgdebug ; + print CGI::p(CGI::h3("PG warning messages"), CGI::br(), @pgwarning ) if @pgwarning ; + print CGI::p(CGI::h3("PG internal errors"), CGI::br(), @pginternalerrors ) if @pginternalerrors; + print CGI::end_div(); } + ""; } +### #FIXME not clear this is ever used +# sub if_errors($$) { +# my ($self, $arg) = @_; +# +# if ($self->{isOpen}) { +# return $self->{pg}->{flags}->{error_flag} ? $arg : !$arg; +# } else { +# return !$arg; +# } +# } + sub head { my ($self) = @_; @@ -757,12 +790,6 @@ sub head { return $self->{pg}->{head_text} if $self->{pg}->{head_text}; } -sub post_header_text { - my ($self) = @_; - return "" if ( $self->{invalidSet} ); - return $self->{pg}->{post_header_text} if $self->{pg}->{post_header_text}; -} - sub options { my ($self) = @_; #warn "doing options in Problem"; @@ -989,10 +1016,10 @@ sub output_message{ sub output_editorLink{ my $self = shift; - - my $set = $self->{set}; - my $problem = $self->{problem}; - my $pg = $self->{pg}; + + my $set = $self->{set}; + my $problem = $self->{problem}; + my $pg = $self->{pg}; my $r = $self->r; @@ -1059,10 +1086,10 @@ sub output_checkboxes{ -name => "showCorrectAnswers", -value => 1, } - )," "; + ); } if ($can{showHints}) { - print CGI::span({style=>"color:red"}, + print CGI::div({style=>"color:red"}, WeBWorK::CGI_labeled_input( -type => "checkbox", -id => "showHints_id", @@ -1075,11 +1102,11 @@ sub output_checkboxes{ } : { - -name => "showHints", + -name => "showCorrectAnswers", -value => 1, } ) - )," "; + ); } if ($can{showSolutions}) { print WeBWorK::CGI_labeled_input( @@ -1094,10 +1121,10 @@ sub output_checkboxes{ } : { - -name => "showSolutions", + -name => "showCorrectAnswers", -value => 1, } - )," "; + ); } if ($can{showCorrectAnswers} or $can{showHints} or $can{showSolutions}) { @@ -1127,7 +1154,7 @@ sub output_submit_buttons{ if ($user ne $effectiveUser) { # if acting as a student, make it clear that answer submissions will # apply to the student's records, not the professor's. - print WeBWorK::CGI_labeled_input(-type=>"submit", -id=>"submitAnswers_id", -input_attr=>{-name=>$r->maketext("submitAnswers"), -value=>$r->maketext("Submit Answers for [_1]", $effectiveUser)}); + print WeBWorK::CGI_labeled_input(-type=>"submit", -id=>"submitAnswers_id", -input_attr=>{-name=>"submitAnswers", -value=>$r->maketext("Submit Answers for [_1]", $effectiveUser)}); } else { #print CGI::submit(-name=>"submitAnswers", -label=>"Submit Answers", -onclick=>"alert('submit button clicked')"); print WeBWorK::CGI_labeled_input(-type=>"submit", -id=>"submitAnswers_id", -input_attr=>{-name=>"submitAnswers", -value=>$r->maketext("Submit Answers"), -onclick=>""}); @@ -1149,18 +1176,29 @@ sub output_score_summary{ my $problem = $self->{problem}; my $set = $self->{set}; my $pg = $self->{pg}; - my $scoreRecordedMessage = WeBWorK::ContentGenerator::ProblemUtil::ProblemUtil::process_and_log_answer($self) || ""; + my $scoreRecordedMessage = ""; + if (defined $self->{scoreRecordedMessage}) { + $scoreRecordedMessage = $self->{scoreRecordedMessage}; + } else { + $scoreRecordedMessage = WeBWorK::ContentGenerator::ProblemUtil::ProblemUtil::process_and_log_answer($self) || ""; + } my $submitAnswers = $self->{submitAnswers}; # score summary - warn "num_correct =", $problem->num_correct,"num_incorrect=",$problem->num_incorrect - unless defined($problem->num_correct) and defined($problem->num_incorrect) ; my $attempts = $problem->num_correct + $problem->num_incorrect; #my $attemptsNoun = $attempts != 1 ? $r->maketext("times") : $r->maketext("time"); my $problem_status = $problem->status || 0; my $lastScore = sprintf("%.0f%%", $problem_status * 100); # Round to whole number + #my ($attemptsLeft, $attemptsLeftNoun); my $attemptsLeft = $problem->max_attempts - $attempts; - +# if ($problem->max_attempts == -1) { +# # unlimited attempts +# $attemptsLeft = $r->maketext("unlimited"); +# $attemptsLeftNoun = $r->maketext("attempts"); +# } else { +# $attemptsLeft = $problem->max_attempts - $attempts; +# $attemptsLeftNoun = $attemptsLeft == 1 ? $r->maketext("attempt") : $r->maketext("attempts"); +# } my $setClosed = 0; my $setClosedMessage; @@ -1190,6 +1228,7 @@ sub output_score_summary{ $problem->attempted ? $r->maketext("Your overall recorded score is [_1]. [_2]",$lastScore,$notCountedMessage) . CGI::br() : "", +# $setClosed ? $setClosedMessage : $r->maketext("You have [_1] [_2] remaining.",$attemptsLeft,$attemptsLeftNoun) $setClosed ? $setClosedMessage : $r->maketext("You have [negquant,_1,unlimited attempts,attempt,attempts] remaining.",$attemptsLeft) )); }else { @@ -1213,18 +1252,6 @@ sub output_misc{ my %will = %{ $self->{will} }; my $user = $r->param('user'); - print CGI::start_div(); - - my $pgdebug = join(CGI::br(), @{$pg->{pgcore}->{flags}->{DEBUG_messages}} ); - my $pgwarning = join(CGI::br(), @{$pg->{pgcore}->{flags}->{WARNING_messages}} ); - my $pginternalerrors = join(CGI::br(), @{$pg->{pgcore}->get_internal_debug_messages} ); - my $pgerrordiv = $pgdebug||$pgwarning||$pginternalerrors; # is 1 if any of these are non-empty - - print CGI::p({style=>"color:red;"}, $r->maketext("Checking additional error messages")) if $pgerrordiv ; - print CGI::p("pg debug
$pgdebug" ) if $pgdebug ; - print CGI::p("pg warning
$pgwarning" ) if $pgwarning ; - print CGI::p("pg internal errors
$pginternalerrors") if $pginternalerrors; - print CGI::end_div() if $pgerrordiv ; # save state for viewOptions print CGI::hidden( @@ -1261,7 +1288,8 @@ sub output_misc{ # output_summary subroutine -# prints out the summary of the questions that the student has answered for the current problem, along with available information about correctness +# prints out the feedback on the questions that the student has answered for +# the current problem, along with available information about correctness sub output_summary{ @@ -1284,7 +1312,7 @@ sub output_summary{ #FIXME -- the following is a kludge: if showPartialCorrectAnswers is negative don't show anything. # until after the due date # do I need to check $will{showCorrectAnswers} to make preflight work?? - if (($pg->{flags}->{showPartialCorrectAnswers} >= 0 and $submitAnswers) ) { + if (defined($pg->{flags}->{showPartialCorrectAnswers}) and ($pg->{flags}->{showPartialCorrectAnswers} >= 0 and $submitAnswers) ) { # print this if user submitted answers OR requested correct answers print $self->attemptResults($pg, 1, @@ -1325,7 +1353,7 @@ sub output_custom_edit_message{ # custom message for editor if ($authz->hasPermissions($user, "modify_problem_sets") and defined $editMode) { if ($editMode eq "temporaryFile") { - print CGI::p(CGI::div({class=>'temporaryFile'}, $r->maketext("Viewing temporary file: "), $problem->source_file)); + print CGI::p(CGI::div({class=>'temporaryFile'}, $r->maketext("Viewing temporary file"), $problem->source_file)); } elsif ($editMode eq "savedFile") { # taken care of in the initialization phase } @@ -1334,9 +1362,6 @@ sub output_custom_edit_message{ return ""; } - - - # output_past_answer_button # prints out the "Show Past Answers" button @@ -1406,14 +1431,11 @@ sub output_email_instructor{ sub output_hidden_info{ my $self = shift; my $previewAnswers = $self->{previewAnswers}; - my $checkAnswers = $self->{checkAnswers}; - my $showPartialCorrectAnswers = $self->{pg}->{flags}->{showPartialCorrectAnswers}; - if($previewAnswers){ # never color previewed answers + + if($previewAnswers){ return ""; } - elsif ( ($checkAnswers ) - or $showPartialCorrectAnswers ) { # color answers when partialCorrectAnswers is set - # or when checkAnswers is submitted + else{ if(defined $self->{correct_ids}){ my $correctRef = $self->{correct_ids}; my @correct = @$correctRef; @@ -1429,55 +1451,65 @@ sub output_hidden_info{ } } return ""; - } else { - return ""; } } # output_JS subroutine -# prints out the wz_tooltip.js script for the current site. +# outputs all of the Javascript needed for this page. The main javascript needed here is color.js, which colors input fields based on whether or not they are correct when answers are submitted. When a problem attempts results, it prints out hidden fields containing identification information for the fields that were correct and the fields that were incorrect. color.js collects of the correct and incorrect fields into two arrays using the information gathered from the hidden fields, and then loops through and changes the styles so that the colors will show up correctly. -sub output_wztooltip_JS{ - +sub output_JS{ my $self = shift; my $r = $self->r; my $ce = $r->ce; my $site_url = $ce->{webworkURLs}->{htdocs}; - print CGI::start_script({type=>"text/javascript", src=>"$site_url/js/wz_tooltip.js"}), CGI::end_script(); + # This file declares a function called addOnLoadEvent which allows multiple different scripts to add to a single onLoadEvent handler on a page. + print CGI::start_script({type=>"text/javascript", src=>"$site_url/js/addOnLoadEvent.js"}), CGI::end_script(); + + print CGI::start_script({type=>"text/javascript", src=>"$site_url/js/removeDuplicates.js"}), CGI::end_script(); + + # The color.js file, which uses javascript to color the input fields based on whether they are correct or incorrect. + print CGI::start_script({type=>"text/javascript", src=>"$site_url/js/color.js"}), CGI::end_script(); + return ""; } -# outputs all of the Javascript needed for this page. -# The main javascript needed here is color.js, which colors input fields based on whether or not -# they are correct when answers are submitted. When a problem attempts results, it prints out hidden fields containing identification -# information for the fields that were correct and the fields that were incorrect. color.js collects of the correct and incorrect fields into -# two arrays using the information gathered from the hidden fields, and then loops through and changes the styles so -# that the colors will show up correctly. +# Simply here to indicate to the template that this page has body part methods which can be called -sub output_JS{ +sub can_body_parts{ + return ""; +} + +# output_wztooltip_JS subroutine + +# prints out the wz_tooltip.js script for the current site. + +sub output_wztooltip_JS{ + my $self = shift; my $r = $self->r; my $ce = $r->ce; my $site_url = $ce->{webworkURLs}->{htdocs}; - # This file declares a function called addOnLoadEvent which allows multiple different scripts to add to a single onLoadEvent handler on a page. - print CGI::start_script({type=>"text/javascript", src=>"$site_url/js/addOnLoadEvent.js"}), CGI::end_script(); - - # This is a file which initializes the proper JAVA applets should they be needed for the current problem. - print CGI::start_script({type=>"tesxt/javascript", src=>"$site_url/js/java_init.js"}), CGI::end_script(); - - # The color.js file, which uses javascript to color the input fields based on whether they are correct or incorrect. - print CGI::start_script({type=>"text/javascript", src=>"$site_url/js/color.js"}), CGI::end_script(); + print CGI::start_script({type=>"text/javascript", src=>"$site_url/js/wz_tooltip.js"}), CGI::end_script(); return ""; } -# Simply here to indicate to the template that this page has body part methods which can be called +# output_removDupli_JS -sub can_body_parts{ +# outputs the removeDuplicates JS function + +sub output_removDupli_JS{ + my $self = shift; + my $r = $self->r; + my $ce = $r->ce; + + my $site_url = $ce->{webworkURLs}->{htdocs}; + + print CGI::start_script({type=>"text/javascript", src=>"$site_url/js/removeDuplicates.js"}), CGI::end_script(); return ""; } diff --git a/lib/WeBWorK/ContentGenerator/ProblemSets.pm b/lib/WeBWorK/ContentGenerator/ProblemSets.pm index 8cd23b473e..c032c5c294 100644 --- a/lib/WeBWorK/ContentGenerator/ProblemSets.pm +++ b/lib/WeBWorK/ContentGenerator/ProblemSets.pm @@ -222,7 +222,7 @@ sub body { # and send the start of the table # UPDATE - ghe3 # This table now contains a summary and a caption, scope attributes for the column headers, and no longer prints a column for 'Sel.' (due to it having been merged with the second column for accessibility purposes). - print CGI::start_table({-summary=>"This table lists out the available homework sets for this class, along with its current status. Click on the link on the name of the homework sets to take you to the problems in that homework set. Clicking on the links in the table headings will sort the table by the field it corresponds to. You can also select sets for download to PDF or TeX format using the radio buttons or checkboxes next to the problem set names, and then clicking on the 'Download PDF or TeX Hardcopy for Selected Sets' button at the end of the table. There is also a clear button and an Email instructor button at the end of the table.", -class=>"problem_set_table"}); + print CGI::start_table({ -class=>"problem_set_table", -summary=>"This table lists out the available homework sets for this class, along with its current status. Click on the link on the name of the homework sets to take you to the problems in that homework set. Clicking on the links in the table headings will sort the table by the field it corresponds to. You can also select sets for download to PDF or TeX format using the radio buttons or checkboxes next to the problem set names, and then clicking on the 'Download PDF or TeX Hardcopy for Selected Sets' button at the end of the table. There is also a clear button and an Email instructor button at the end of the table."}); print CGI::caption($r->maketext("Homework Sets")); if ( ! $existVersions ) { print CGI::Tr({}, diff --git a/lib/WeBWorK/ContentGenerator/instructorXMLHandler.pm b/lib/WeBWorK/ContentGenerator/instructorXMLHandler.pm new file mode 100644 index 0000000000..1837012814 --- /dev/null +++ b/lib/WeBWorK/ContentGenerator/instructorXMLHandler.pm @@ -0,0 +1,454 @@ +################################################################################ +# WeBWorK Online Homework Delivery System +# Copyright © 2000-2007 The WeBWorK Project, http://openwebwork.sf.net/ +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of either: (a) the GNU General Public License as published by the +# Free Software Foundation; either version 2, or (at your option) any later +# version, or (b) the "Artistic License" which comes with this package. +# +# This program 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 either the GNU General Public License or the +# Artistic License for more details. +################################################################################ + +=head1 NAME + +WeBWorK::ContentGenerator::ProblemRenderer - renderViaXMLRPC is an HTML +front end for calls to the xmlrpc webservice + +=cut + +use strict; +use warnings; + +package WeBWorK::ContentGenerator::instructorXMLHandler; +use base qw(WeBWorK::ContentGenerator); +use MIME::Base64 qw( encode_base64 decode_base64); + +our $UNIT_TESTS_ON = 0; # should be called DEBUG?? FIXME + +#use Crypt::SSLeay; +#use XMLRPC::Lite; +#use MIME::Base64 qw( encode_base64 decode_base64); + + +use strict; +use warnings; +use WebworkClient; + + +=head1 Description + + +################################################# + instructorXMLHandler -- a front end for the Webservice that accepts HTML forms + + receives WeBWorK problems presented as HTML forms, usually created with js xmlhttprequests, + packages the form variables into an XML_RPC request + suitable for all of the webservices in WebworkWebservices + returns xml resutls +################################################# + +=cut + +# To configure the target webwork server two URLs are required +# 1. $XML_URL http://test.webwork.maa.org/mod_xmlrpc +# points to the Webservice.pm and Webservice/RenderProblem modules +# Is used by the client to send the original XML request to the webservice +# +# 2. $FORM_ACTION_URL http:http://test.webwork.maa.org/webwork2/html2xml +# points to the renderViaXMLRPC.pm module. +# +# This url is placed as form action url when the rendered HTML from the original +# request is returned to the client from Webservice/RenderProblem. The client +# reorganizes the XML it receives into an HTML page (with a WeBWorK form) and +# pipes it through a local browser. +# +# The browser uses this url to resubmit the problem (with answers) via the standard +# HTML webform used by WeBWorK to the renderViaXMLRPC.pm handler. +# +# This renderViaXMLRPC.pm handler acts as an intermediary between the browser +# and the webservice. It interprets the HTML form sent by the browser, +# rewrites the form data in XML format, submits it to the WebworkWebservice.pm +# which processes it and sends the the resulting HTML back to renderViaXMLRPC.pm +# which in turn passes it back to the browser. +# 3. The second time a problem is submitted renderViaXMLRPC.pm receives the WeBWorK form +# submitted directly by the browser. +# The renderViaXMLRPC.pm translates the WeBWorK form, has it processes by the webservice +# and returns the result to the browser. +# The The client renderProblem.pl script is no longer involved. +# 4. Summary: renderProblem.pl is only involved in the first round trip +# of the submitted problem. After that the communication is between the browser and +# renderViaXMLRPC using HTML forms and between renderViaXMLRPC and the WebworkWebservice.pm +# module using XML_RPC. + + +# Determine the root directory for webwork on this machine (e.g. /opt/webwork/webwork2 ) +# this is set in webwork.apache2-config +# it specifies the address of the webwork root directory + +#my $webwork_dir = $ENV{WEBWORK_ROOT}; +my $webwork_dir = $WeBWorK::Constants::WEBWORK_DIRECTORY; +unless ($webwork_dir) { + die "renderViaXMLRPC.pm requires that the top WeBWorK directory be set in ". + "\$WeBWorK::Constants::WEBWORK_DIRECTORY by webwork.apache-config or webwork.apache2-config\n"; +} + +# read the webwork2/conf/global.conf file to determine other parameters +# +my $seed_ce = new WeBWorK::CourseEnvironment({ webwork_dir => $webwork_dir }); +my $server_root_url = $seed_ce->{server_root_url}; +unless ($server_root_url) { + die "unable to determine apache server url using course environment |$seed_ce|.". + "check that the variable \$server_root_url has been properly set in conf/global.conf\n"; +} + +############################ +# These variables are set when the child process is started +# and remain constant through all of the calls handled by the +# child +############################ + +our ($XML_URL,$FORM_ACTION_URL, $XML_PASSWORD, $XML_COURSE); + + $XML_PASSWORD = 'xmluser'; + $XML_COURSE = 'daemon_course'; + + + + $XML_URL = "$server_root_url/mod_xmlrpc"; + $FORM_ACTION_URL = "$server_root_url/webwork2/instructorXMLHandler"; + +use constant DISPLAYMODE => 'images'; # jsMath is another possibilities. + + + +our @COMMANDS = qw( listLibraries renderProblem ); #listLib readFile tex2pdf + + +# error formatting +sub format_hash_ref { + my $hash = shift; + warn "Use a hash reference".caller() unless ref($hash) =~/HASH/; + my $out_str=""; + my $count =4; + foreach my $key ( sort keys %$hash) { + my $value = defined($hash->{$key})? $hash->{$key}:"--"; + $out_str.= " $key=>$value "; + $count--; + unless($count) { $out_str.="\n ";$count =4;} + } + $out_str; +} +# template method +sub templateName { + return ""; +} +################################################## +# end configuration section +################################################## + + +sub pre_header_initialize { + my ($self) = @_; + my $r = $self->r; + + + + + + ####################### + # setup xmlrpc client + ####################### + my $xmlrpc_client = new WebworkClient; + + $xmlrpc_client->url($XML_URL); + $xmlrpc_client->{form_action_url}= $FORM_ACTION_URL; + $xmlrpc_client->{displayMode} = DISPLAYMODE(); + $xmlrpc_client->{user} = 'xmluser'; + $xmlrpc_client->{password} = $XML_PASSWORD; + $xmlrpc_client->{course} = $r->param('courseID'); + # print STDERR WebworkClient::pretty_print($r->{paramcache}); + + #create input table for the request + #my $input = { + # pw => $XML_PASSWORD, + # set => 'set0', + # library_name => 'Library', + # command => 'all', + #}; + + my $input = {#can I just use $->param? it looks like a hash + pw => $r->param('pw') ||undef, + session_key => $r->param("session_key") ||undef, + userID => $r->param("user") ||undef, + library_name => $r->param( "library_name") ||undef, + user => $r->param("user") ||undef, + set => $r->param("set") ||undef, + fileName => $r->param("file_name") ||undef, + new_set_name => $r->param("new_set_name") ||undef, + probList => $r->param("probList") ||undef, + command => $r->param("command") ||undef, + subcommand => $r->param("subcommand") ||undef, + maxdepth => $r->param("maxdepth") || 0, + library_subjects => $r->param("library_subjects") ||undef, + library_chapters => $r->param("library_chapters") ||undef, + library_sections => $r->param("library_sections") ||undef, + library_textbook => $r->param("library_textbook") ||undef, + library_keywords => $r->param("library_keywords") ||undef, + library_textchapter => $r->param("library_textchapter") ||undef, + library_textsection => $r->param("library_textsection") ||undef, + source => '', + }; + if ($UNIT_TESTS_ON) { + print STDERR "instructorXMLHandler.pm ".__LINE__." values obtained from form parameters\n\t", + format_hash_ref($input); + } + my $source = ""; + #print $r->param('problemSource'); + my $problem = $r->param('problemSource'); + if (defined($problem) and $problem and -r $problem ) { + $source = `cat $problem`; + #print "SOURCE\n".$source; + $input->{source} = encode_base64($source); + } + + my $std_input = standard_input(); + $input = {%$std_input, %$input}; + + ########################################## + # FIXME hack to get fileName or filePath param("set") contains the path + my $problemPath = $input->{set}; # FIXME should rename this ???? + $problemPath =~ m|templates/(.*)|; + $problemPath = $1; # get everything in the path after templates + $input->{envir}->{fileName}= $problemPath; + ################################################## + $input->{courseID} = $r->param('courseID'); + + ############################## + # xmlrpc_client calls webservice with the requested command + # + # and stores the resulting XML output in $self->{output} + # from which it will eventually be returned to the browser + # + ############################## + #if ( $xmlrpc_client->jsXmlrpcCall($r->param("xml_command"), $input) ) { + # print "tried to render a problem"; + #$self->{output} = $xmlrpc_client->formatRenderedProblem;#not sure what to do here just yet. + #} else { + # $self->{output} = $xmlrpc_client->{output}; # error report + # print $xmlrpc_client->{output}; + #} + if($r->param('xml_command') eq "addProblem" || $r->param('xml_command') eq "deleteProblem"){ + $input->{path} = $r->param('problemPath'); + } + + if($r->param('xml_command') eq "renderProblem"){ + if (my $problemPath = $r->param('problemPath')) { + $problemPath =~ m|templates/(.*)|; + $problemPath = $1; # get everything in the path after templates + $input->{envir}->{fileName}=$problemPath; + } + $self->{output}->{problem_out} = $xmlrpc_client->xmlrpcCall('renderProblem', $input); + + #$self->{output}->{text} = "Rendered problem"; + } else { + $self->{output} = $xmlrpc_client->xmlrpcCall($r->param("xml_command"), $input); + } + ################################ + } + + +sub standard_input { + my $out = { + pw => '', # not needed + password => '', # not needed + session_key => '', + userID => '', # not needed + set => '', + library_name => 'Library', + command => 'all', + answer_form_submitted => 1, + extra_packages_to_load => [qw( AlgParserWithImplicitExpand Expr + ExprWithImplicitExpand AnswerEvaluator + AnswerEvaluatorMaker + )], + mode => 'images', + modules_to_evaluate => [ qw( +Exporter +DynaLoader +GD +WWPlot +Fun +Circle +Label +PGrandom +Units +Hermite +List +Match +Multiple +Select +AlgParser +AnswerHash +Fraction +VectorField +Complex1 +Complex +MatrixReal1 Matrix +Distributions +Regression + + )], + envir => environment(), + problem_state => { + + num_of_correct_ans => 2, + num_of_incorrect_ans => 4, + recorded_score => 1.0, + }, + source => '', #base64 encoded + + + + }; + + $out; +} + +sub environment { + my $envir = { + answerDate => '4014438528', + CAPA_Graphics_URL=>'http://webwork-db.math.rochester.edu/capa_graphics/', + CAPA_GraphicsDirectory =>'/ww/webwork/CAPA/CAPA_Graphics/', + CAPA_MCTools=>'/ww/webwork/CAPA/CAPA_MCTools/', + CAPA_Tools=>'/ww/webwork/CAPA/CAPA_Tools/', + cgiDirectory=>'Not defined', + cgiURL => 'Not defined', + classDirectory=> 'Not defined', + courseName=>'Not defined', + courseScriptsDirectory=>'/ww/webwork/system/courseScripts/', + displayMode=>DISPLAYMODE, + dueDate=> '4014438528', + externalGif2EpsPath=>'not defined', + externalPng2EpsPath=>'not defined', + externalTTHPath=>'/usr/local/bin/tth', + fileName=>'the XMLHandlerenvironment->{fileName} should be set', + formattedAnswerDate=>'6/19/00', + formattedDueDate=>'6/19/00', + formattedOpenDate=>'6/19/00', + functAbsTolDefault=> 0.0000001, + functLLimitDefault=>0, + functMaxConstantOfIntegration=> 1000000000000.0, + functNumOfPoints=> 5, + functRelPercentTolDefault=> 0.000001, + functULimitDefault=>1, + functVarDefault=> 'x', + functZeroLevelDefault=> 0.000001, + functZeroLevelTolDefault=>0.000001, + htmlDirectory =>'/ww/webwork/courses/gage_course/html/', + htmlURL =>'http://webwork-db.math.rochester.edu/gage_course/', + inputs_ref => { + AnSwEr1 => '', + AnSwEr2 => '', + AnSwEr3 => '', + }, + macroDirectory=>'/ww/webwork/courses/gage_course/templates/macros/', + numAbsTolDefault=>0.0000001, + numFormatDefault=>'%0.13g', + numOfAttempts=> 0, + numRelPercentTolDefault => 0.0001, + numZeroLevelDefault =>0.000001, + numZeroLevelTolDefault =>0.000001, + openDate=> '3014438528', + PRINT_FILE_NAMES_FOR => [ 'gage'], + probFileName => 'probFileName should not be used --use fileName instead', + problemSeed => 1234, + problemValue =>1, + probNum => 13, + psvn => 54321, + psvn=> 54321, + questionNumber => 1, + scriptDirectory => 'Not defined', + sectionName => 'Gage', + sectionNumber => 1, + sessionKey=> 'Not defined', + setNumber =>'MAAtutorial', + studentLogin =>'gage', + studentName => 'Mike Gage', + tempDirectory => '/ww/htdocs/tmp/gage_course/', + templateDirectory=>'/ww/webwork/courses/gage_course/templates/', + tempURL=>'http://webwork-db.math.rochester.edu/tmp/gage_course/', + webworkDocsURL => 'http://webwork.math.rochester.edu/webwork_gage_system_html', + }; + $envir; +}; + +sub pretty_print_json { + shift if UNIVERSAL::isa($_[0] => __PACKAGE__); + my $rh = shift; + my $indent = shift || 0; + my $out = ""; + my $type = ref($rh); + + if (defined($type) and $type) { + #$out .= " type = $type; "; + } elsif (! defined($rh )) { + #$out .= " type = UNDEFINED; "; + } + return $out."" unless defined($rh); + + if ( ref($rh) =~/HASH/ or "$rh" =~/HASH/ ) { + $indent++; + foreach my $key (sort keys %{$rh}) { + $out .= " ".'"'.$key.'" : '. pretty_print_json( $rh->{$key}) . ","; + } + $indent--; + #get rid of the last comma + chop $out; + $out = "{\n$out\n"."}\n"; + + } elsif (ref($rh) =~ /ARRAY/ or "$rh" =~/ARRAY/) { + foreach my $elem ( @{$rh} ) { + $out .= pretty_print_json($elem).","; + + } + #get rid of the last comma + chop $out; + $out = '"'.$out.'"'; + } elsif ( ref($rh) =~ /SCALAR/ ) { + $out .= "scalar reference ". ${$rh}; + } elsif ( ref($rh) =~/Base64/ ) { + $out .= "base64 reference " .$$rh; + } else { + $out .= $rh; + } + + return $out.""; +} + +sub content { + ########################### + # Return content of rendered problem to the browser that requested it + ########################### + my $self = shift; + #for handling errors...i'm to lazy to make it work right now + if($self->{output}->{problem_out}){ + print $self->{output}->{problem_out}->{text}; + } else { + print '{"server_response":"'.$self->{output}->{text}.'",'; + if($self->{output}->{ra_out}){ + print '"result_data":'.pretty_print_json($self->{output}->{ra_out}).'}'; + } else { + print '"result_data":""}'; + } + } + #print "".pretty_print_json($self->{output}->{ra_out}); +} + + + + +1; diff --git a/lib/WeBWorK/ContentGenerator/renderViaXMLRPC.pm b/lib/WeBWorK/ContentGenerator/renderViaXMLRPC.pm index 7e95f72a8b..60447070fb 100755 --- a/lib/WeBWorK/ContentGenerator/renderViaXMLRPC.pm +++ b/lib/WeBWorK/ContentGenerator/renderViaXMLRPC.pm @@ -135,24 +135,30 @@ sub pre_header_initialize { my ($self) = @_; my $r = $self->r; + my %inputs_ref; + my @keys = $r->mutable_param; # $r->param; + foreach my $key ( @keys ) { + $inputs_ref{$key} = $r->param("$key"); + } + my $user_id = $inputs_ref{userID}; + my $session_key = $inputs_ref{session_key}; + my $courseName = $inputs_ref{courseID}; + ####################### # setup xmlrpc client ####################### my $xmlrpc_client = new WebworkClient; - $xmlrpc_client->{encodedSource} = $r->param('problemSource') ; # this source has already been encoded + $xmlrpc_client->{encodedSource} = $r->param('problemSource') ; # this source has already been encoded $xmlrpc_client->url($XML_URL); - $xmlrpc_client->{form_action_url}= $FORM_ACTION_URL; - $xmlrpc_client->{displayMode} = DISPLAYMODE(); - $xmlrpc_client->{user} = 'xmluser'; - $xmlrpc_client->{password} = $XML_PASSWORD; - $xmlrpc_client->{course} = $XML_COURSE; - my %inputs_ref; - my @keys = $r->mutable_param; # $r->param; - foreach my $key ( @keys ) { - $inputs_ref{$key} = $r->param("$key"); - } + $xmlrpc_client->{form_action_url} = $FORM_ACTION_URL; + $xmlrpc_client->{displayMode} = DISPLAYMODE(); + $xmlrpc_client->{userID} = $inputs_ref{userID}; +# $xmlrpc_client->{password} = $XML_PASSWORD; + $xmlrpc_client->{session_key} = $inputs_ref{session_key}; + $xmlrpc_client->{courseID} = $inputs_ref{courseID}; + $xmlrpc_client->{inputs_ref} = \%inputs_ref; # print STDERR WebworkClient::pretty_print($r->{paramcache}); @@ -163,7 +169,7 @@ sub pre_header_initialize { # from which it will eventually be returned to the browser # ############################## - if ( $xmlrpc_client->xmlrpcCall('renderProblem') ) { + if ( $xmlrpc_client->xmlrpcCall('renderProblem', $xmlrpc_client->{inputs_ref}) ) { $self->{output} = $xmlrpc_client->formatRenderedProblem; } else { $self->{output} = $xmlrpc_client->{output}; # error report diff --git a/lib/WeBWorK/DB/Schema/NewSQL/Std.pm b/lib/WeBWorK/DB/Schema/NewSQL/Std.pm index b022572873..3bb50b3204 100644 --- a/lib/WeBWorK/DB/Schema/NewSQL/Std.pm +++ b/lib/WeBWorK/DB/Schema/NewSQL/Std.pm @@ -278,10 +278,10 @@ sub _get_db_info { $my_cnf->unlink_on_destroy(1); chmod 0600, $my_cnf or die "failed to chmod 0600 $my_cnf: $!"; # File::Temp objects stringify with ->filename print $my_cnf "[client]\n"; - print $my_cnf "user=$username\n" if defined $username and length($username) > 0; - print $my_cnf "password=$password\n" if defined $password and length($password) > 0; - print $my_cnf "host=$dsn{host}\n" if defined $dsn{host} and length($dsn{host}) > 0; - print $my_cnf "port=$dsn{port}\n" if defined $dsn{port} and length($dsn{port}) > 0; + print $my_cnf "user=\"$username\"\n" if defined $username and length($username) > 0; + print $my_cnf "password=\"$password\"\n" if defined $password and length($password) > 0; + print $my_cnf "host=\"$dsn{host}\"\n" if defined $dsn{host} and length($dsn{host}) > 0; + print $my_cnf "port=\"$dsn{port}\"\n" if defined $dsn{port} and length($dsn{port}) > 0; return ($my_cnf, $dsn{database}); } diff --git a/lib/WeBWorK/Localize.pm b/lib/WeBWorK/Localize.pm index 1705b897bf..0daaf123fc 100644 --- a/lib/WeBWorK/Localize.pm +++ b/lib/WeBWorK/Localize.pm @@ -1,6 +1,3 @@ - - - package WeBWorK::Localize; @@ -95,10 +92,14 @@ tr: This assignment had a Reduced Credit Period that began [_1] and ended on the due date, [_2]. During that period all additional work done counted [_3]\% of the original. }, -"_GUEST_LOGIN_MESSAGE" => q{tr: This course supports guest logins. Click [_1] to log into this course as a guest.}, +"_GUEST_LOGIN_MESSAGE" => q{tr: This course supports guest logins. Click [_1] to log into this course as a guest.}, "_EXTERNAL_AUTH_MESSAGE" => q{[_1] uses an external authentication system. You've authenticated through that system, but aren't allowed to log in to this course.}, +"_PROBLEM_SET_SUMMARY" => q{This is a table showing the current Homework sets for this class. The fields from left to right are: Edit Set Data, Edit Problems, Edit Assigned Users, Visibility to students, Reduced Credit Enabled, Date it was opened, Date it is due, and the Date during which the answers are posted. The Edit Set Data field contains checkboxes for selection and a link to the set data editing page. The cells in the Edit Problems fields contain links which take you to a page where you can edit the containing problems, and the cells in the edit assigned users field contains links which take you to a page where you can edit what students the set is assigned to.}, + +"_USER_TABLE_SUMMARY" => q{A table showing all the current users along with several fields of user information. The fields from left to right are: Login Name, Login Status, Assigned Sets, First Name, Last Name, Email Address, Student ID, Enrollment Status, Section, Recitation, Comments, and Permission Level. Clicking on the links in the column headers will sort the table by the field it corresponds to. The Login Name fields contain checkboxes for selecting the user. Clicking the link of the name itself will allow you to act as the selected user. There will also be an image link following the name which will take you to a page where you can edit the selected user's information. Clicking the emails will allow you to email the corresponding user. Clicking the links in the entries in the assigned sets columns will take you to a page where you can view and reassign the sets for the selected user.}, + ); package WeBWorK::Localize::I18N; use base(WeBWorK::Localize); diff --git a/lib/WeBWorK/Localize/fr.po b/lib/WeBWorK/Localize/fr.po index 44fb2de0fa..cef395da20 100644 --- a/lib/WeBWorK/Localize/fr.po +++ b/lib/WeBWorK/Localize/fr.po @@ -9,7 +9,7 @@ msgid "" msgstr "" "Project-Id-Version: WeBWorK Online Homework System\n" "POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" -"PO-Revision-Date: 2011-12-22 12:16-0500\n" +"PO-Revision-Date: 2012-01-06 21:59-0500\n" "Last-Translator: Michael Gage \n" "Language-Team: WeBWorK language team \n" "Language: \n" @@ -19,7 +19,6 @@ msgstr "" "X-Poedit-Language: French\n" "X-Poedit-Country: CANADA\n" -# #. ($name, $vnum) #: ContentGenerator/ProblemSets.pm:416 #, fuzzy @@ -29,14 +28,12 @@ msgstr "tr: %1 (test %2)" # #. ($numAdded, $numSkipped, join(", ", @$skipped) #: ContentGenerator/Instructor/ProblemSetList.pm:1581 -#, fuzzy msgid "%1 sets added, %2 sets skipped. Skipped sets: (%3)" msgstr "%1 devoirs ajoutés, %2 devoirs ignorés. Devoirs ignorés : (%3)" # #. ($numExported, $numSkipped, (($numSkipped) #: ContentGenerator/Instructor/ProblemSetList.pm:1701 -#, fuzzy msgid "%1 sets exported, %2 sets skipped. Skipped sets: (%3)" msgstr "%1 devoirs exportés, %2 devoirs ignorés. Devoirs ignorés : (%3)" @@ -72,56 +69,48 @@ msgstr "%1% correct" # #. ($e_user_name) #: ContentGenerator/Options.pm:171 -#, fuzzy msgid "%1's Current Address" -msgstr "Courriel actuel %1" +msgstr "Courriel actuel de %1" # #. ($user_name) #: ContentGenerator/Options.pm:126 -#, fuzzy msgid "%1's Current Password" -msgstr "Mot de passe actuel %1" +msgstr "Mot de passe actuel de %1" # #. ($e_user_name) #: ContentGenerator/Options.pm:175 -#, fuzzy msgid "%1's New Address" -msgstr "Nouveau courriel %1" +msgstr "Nouveau courriel de %1" # #. ($e_user_name) #: ContentGenerator/Options.pm:130 -#, fuzzy msgid "%1's New Password" -msgstr "Nouveau mot de passe %1" +msgstr "Nouveau mot de passe de %1" # #. ($e_user_name) #: ContentGenerator/Options.pm:101 -#, fuzzy msgid "%1's new password cannot be blank." -msgstr "Le nouveau mot de passe ne peut rester vide. %1" +msgstr "Le nouveau mot de passe de %1 ne peut rester vide." # #. ($e_user_name) #: ContentGenerator/Options.pm:84 -#, fuzzy msgid "%1's password has been changed." msgstr "Le mot de passe de l'usager %1 a été modifié." # #. ($setID, $problemID) #: ContentGenerator/Problem.pm:884 -#, fuzzy msgid "%1: Problem %2" msgstr "%1: Problème %2." # #. ($numBlanks) #: ContentGenerator/Problem.pm:319 -#, fuzzy msgid "%quant(%1,of the questions remains,of the questions remain) unanswered." msgstr "Certaines questions %1 restent sans réponse." @@ -150,7 +139,6 @@ msgstr "Annuler l'exportation" # #. ($eUserID) #: ContentGenerator.pm:848 -#, fuzzy msgid "Acting as %1. " msgstr "Dans le rôle de %1." @@ -177,21 +165,18 @@ msgstr "Les réponses ci-dessus sont toutes correctes." # #. ($verb) #: ContentGenerator/Instructor/ProblemSetList.pm:1052 -#, fuzzy msgid "All selected sets %1 all students" msgstr "Tous les devoirs sélectionnés %1 à tous les étudiants" # #. ($verb) #: ContentGenerator/Instructor/ProblemSetList.pm:1046 -#, fuzzy msgid "All sets %1 all students" msgstr "Tous les devoirs %1 à tous les étudiants" # #. ($verb) #: ContentGenerator/Instructor/ProblemSetList.pm:1049 -#, fuzzy msgid "All visible sets %1 all students" msgstr "Tous les devoirs visibles %1 à tous les étudiants" @@ -224,7 +209,6 @@ msgstr "Assigner ce devoir à quels usagers?" # #: ContentGenerator/Instructor/UserList.pm:2064 -#, fuzzy msgid "Assigned Sets" msgstr "Devoirs assignés" @@ -242,14 +226,12 @@ msgstr "Essais" # #. ($eUserID,$@) #: ContentGenerator/Options.pm:71 -#, fuzzy msgid "Can't get password record for effective user '%1': %2" msgstr "Impossible d'accéder au mot de passe pour l'usager actuel '%1': %2" # #. ($userID,$@) #: ContentGenerator/Options.pm:68 -#, fuzzy msgid "Can't get password record for user '%1': %2" msgstr "Impossible d'accéder au mot de passe pour l'usager '%1': %2" @@ -266,13 +248,11 @@ msgstr "Changer le mot de passe" # #: ContentGenerator/Instructor/UserList.pm:1409 #: ContentGenerator/Instructor/UserList.pm:1481 -#, fuzzy msgid "Changes abandoned" msgstr "Les modifications ont été annulées" # #: ContentGenerator/Instructor/UserList.pm:1457 -#, fuzzy msgid "Changes saved" msgstr "Les modifications ont été sauvegardées" @@ -284,18 +264,18 @@ msgstr "Vérifier les réponses" # #: ContentGenerator/Problem.pm:1227 msgid "Checking additional error messages" -msgstr "tr: Checking additional error messages" +msgstr "Vérification des messages d'erreurs additionnels" # #: ContentGenerator/Instructor/ProblemSetList.pm:1011 msgid "Choose visibility of the sets to be affected" -msgstr "tr: Choose visibility of the sets to be affected" +msgstr "Modifier la visibilité des devoirs choisis." # #: ContentGenerator/Instructor/ProblemSetList.pm:1084 #: ContentGenerator/Instructor/ProblemSetList.pm:982 msgid "Choose which sets to be affected" -msgstr "tr: Choose which sets to be affected" +msgstr "Choisir les devoirs à modifier" # #: ContentGenerator/Instructor/ProblemSetList.pm:597 @@ -306,7 +286,6 @@ msgstr "Effacer" # #: ContentGenerator/Instructor/UserList.pm:562 -#, fuzzy msgid "Click on the login name to edit individual problem set data, (e.g. due dates) for these students." msgstr "Cliquer sur le nom d'usager pour éditer les devoirs individuellement (par ex. la date de remise) pour ces étudiants" @@ -328,14 +307,13 @@ msgstr "Commenter" #. ($e_user_name) #: ContentGenerator/Options.pm:134 #: ContentGenerator/Options.pm:95 -#, fuzzy msgid "Confirm %1's New Password" -msgstr "Confirmer le nouveau mot de passe %1" +msgstr "Confirmer le nouveau mot de passe de %1" # #: ContentGenerator/Login.pm:206 msgid "Continue" -msgstr "Continuer" +msgstr "Connexion" # #: ContentGenerator/Problem.pm:243 @@ -351,7 +329,6 @@ msgstr "Il n'a pas été possible de modifier le mot de passe : %2 de l'usager : # #. ($@) #: ContentGenerator/Options.pm:153 -#, fuzzy msgid "Couldn't change your email address: %1" msgstr "Il n'a pas été possible de modifier votre courriel : %1" @@ -370,7 +347,7 @@ msgstr "Cours" # #: ContentGenerator/Instructor/ProblemSetList.pm:1351 msgid "Create as what type of set?" -msgstr "tr: Create as what type of set?" +msgstr "Créer en tant que quel type de devoir?" # #: ContentGenerator/Instructor/ProblemSetList.pm:1263 @@ -403,7 +380,7 @@ msgstr "Télécharger une copie PDF ou TeX pour le devoir en cours" # #: ContentGenerator/ProblemSets.pm:289 msgid "Download PDF or TeX Hardcopy for Selected Sets" -msgstr "Télécharger une copie PDF ou TeX pour les devoir sélectionnés" +msgstr "Télécharger une copie PDF ou TeX pour les devoirs sélectionnés" # #: ContentGenerator/Instructor/ProblemSetList.pm:445 @@ -483,12 +460,12 @@ msgstr "Activer" # #: ContentGenerator/Instructor/ProblemSetList.pm:604 msgid "Enable Reduced Credit" -msgstr "tr: Enable Reduced Credit" +msgstr "Permettre les points partiels." # #: ContentGenerator/Instructor/ProblemSetList.pm:1113 msgid "Enable/Disable reduced scoring for selected sets" -msgstr "tr: Enable/Disable reduced scoring for selected sets" +msgstr "Activer/Désactiver les points partiels pour les devoirs sélectionnés" # #: ContentGenerator/Instructor/UserList.pm:815 @@ -535,7 +512,6 @@ msgstr "Exporter quels usagers?" # #. ($@) #: ContentGenerator/Instructor/ProblemSetList.pm:1435 -#, fuzzy msgid "Failed to create new set: %1" msgstr "Impossible de créer le nouveau devoir : %1" @@ -547,7 +523,6 @@ msgstr "Impossible de créer le nouveau devoir : aucun nom n'a été spécifié! # #. ($@) #: ContentGenerator/Instructor/ProblemSetList.pm:1848 -#, fuzzy msgid "Failed to duplicate set: %1" msgstr "Impossible de dupliquer le devoir : %1" @@ -565,7 +540,6 @@ msgstr "Impossible de dupliquer le devoir : aucun devoir n'a été sélectionné # #. ($newSetID) #: ContentGenerator/Instructor/ProblemSetList.pm:1834 -#, fuzzy msgid "Failed to duplicate set: set %1 already exists!" msgstr "Impossible de dupliquer le devoir : le devoir %1 existe déjà!" @@ -686,16 +660,14 @@ msgstr "Se déconnecter" #. ($userID) #: ContentGenerator.pm:845 #: ContentGenerator.pm:847 -#, fuzzy msgid "Logged in as %1. " -msgstr "Connecté sous le nom %1" +msgstr "Connecté sous le nom %1." # #: ContentGenerator/Login.pm:74 #: ContentGenerator/Login.pm:77 -#, fuzzy msgid "Login Info" -msgstr "tr: Login Info" +msgstr "Paramètres du compte" # #: ContentGenerator/Instructor/UserList.pm:2062 @@ -708,7 +680,6 @@ msgstr "Nom d'usager" # #: ContentGenerator/Instructor/UserList.pm:2063 -#, fuzzy msgid "Login Status" msgstr "Statut" @@ -719,6 +690,7 @@ msgstr "Menu principal" # #: ContentGenerator/Instructor/ProblemSetList.pm:705 +#, fuzzy msgid "Match on what? (separate multiple IDs with commas)" msgstr "tr: Match on what? (separate multiple IDs with commas)" @@ -775,9 +747,8 @@ msgstr "Aucun devoir sélectionné pour la notation" # #: ContentGenerator/Instructor/ProblemSetList.pm:266 -#, fuzzy msgid "No sets selected for scoring." -msgstr "Aucun devoir sélectionné pour la notation." +msgstr "Aucun devoir sélectionné pour évaluation." # #: ContentGenerator/Instructor/ProblemSetList.pm:2722 @@ -786,7 +757,6 @@ msgstr "Aucun devoir affiché. Choisir l'une des options ci-dessus pour afficher # #: ContentGenerator/Instructor/UserList.pm:2110 -#, fuzzy msgid "" "No students shown. Choose one of the options above to \n" "\t list the students in the course." @@ -817,15 +787,13 @@ msgstr "Visualiser seulement -- SANS ENREGISTRER LES RÉPONSES" # #. (timestamp($self) #: ContentGenerator.pm:953 -#, fuzzy msgid "Page generated at %1" -msgstr "Page generée à %1" +msgstr "Page génerée à %1" # #: ContentGenerator/Login.pm:202 -#, fuzzy msgid "Password" -msgstr "Nouveau mot de passe" +msgstr "Mot de passe" # #: ContentGenerator/Instructor/UserList.pm:2073 @@ -839,14 +807,13 @@ msgstr "Niveau de permission" # #. (CGI::b($r->maketext($course) #: ContentGenerator/Login.pm:154 -#, fuzzy msgid "Please enter your username and password for %1 below:" -msgstr "Lütfen %1 dersi için kullanici adi ve sifrenizi giriniz:" +msgstr "Veuillez écrire votre nom d'utilisateur et mot de passe pour %1 ci-dessous:" # #: ContentGenerator/Instructor/ProblemSetList.pm:396 msgid "Please select action to be performed." -msgstr "tr: Please select action to be performed." +msgstr "Veuillez sélectionner une action à effectuer." # #: ContentGenerator/Problem.pm:1116 @@ -864,9 +831,8 @@ msgstr "Problème précédent" #: ContentGenerator.pm:686 #: ContentGenerator/Problem.pm:808 #: ContentGenerator/ProblemSet.pm:424 -#, fuzzy msgid "Problem %1" -msgstr "tr: Problem" +msgstr "Problème %1" # #: ContentGenerator/Problem.pm:856 @@ -886,47 +852,44 @@ msgstr "Problèmes" #: ContentGenerator/Instructor/UserList.pm:817 #: ContentGenerator/Instructor/UserList.pm:860 #: ContentGenerator/Instructor/UserList.pm:903 +#, fuzzy msgid "Recitation" msgstr "tr: Recitation" # #. ($verb) #: ContentGenerator/Instructor/ProblemSetList.pm:1148 -#, fuzzy msgid "Reduced Credit %1 for all sets" -msgstr "tr: Reduced Credit %1 for all sets" +msgstr "Points partiels %1 pour tous les devoirs" # #. ($verb) #: ContentGenerator/Instructor/ProblemSetList.pm:1154 -#, fuzzy msgid "Reduced Credit %1 for selected sets" -msgstr "tr: Reduced Credit %1 for selected sets" +msgstr "Points partiels %1 pour les devoirs sélectionnés" # #. ($verb) #: ContentGenerator/Instructor/ProblemSetList.pm:1151 -#, fuzzy msgid "Reduced Credit %1 for visable sets" -msgstr "tr: Reduced Credit %1 for visable sets" +msgstr "Points partiels %1 pour les devoirs visibles" # #: ContentGenerator/Instructor/ProblemSetList.pm:2523 msgid "Reduced Credit Disabled" -msgstr "tr: Reduced Credit Disabled" +msgstr "Points partiels désactivés" # #: ContentGenerator/Instructor/ProblemSetList.pm:2523 #: ContentGenerator/Instructor/ProblemSetList.pm:448 msgid "Reduced Credit Enabled" -msgstr "tr: Reduced Credit Enabled" +msgstr "Points partiels activés" # #. ($beginReducedScoringPeriod) #: ContentGenerator/ProblemSets.pm:459 -#, fuzzy msgid "Reduced Credit Starts: %1" -msgstr "tr: Reduced Credit Starts: %1" +msgstr "Points partiels débute: %1" # #: ContentGenerator/ProblemSet.pm:351 @@ -936,12 +899,12 @@ msgstr "Nombre d'essais restants" # #: ContentGenerator/Login.pm:204 msgid "Remember Me" -msgstr "Souvien mois" +msgstr "Rester connecté" # #: ContentGenerator/Instructor/UserList.pm:1190 msgid "Replace which users?" -msgstr "tr: Replace which users?" +msgstr "Remplacer quels usagers?" # #: ContentGenerator.pm:799 @@ -956,26 +919,25 @@ msgstr "Résultat" # #. (CGI::i($self->$actionHandler(\%genericParams, \%actionParams, \%tableParams) #: ContentGenerator/Instructor/UserList.pm:376 -#, fuzzy msgid "Result of last action performed: %1" -msgstr "tr: Result of last action performed: %1" +msgstr "Résultat de la dernière action effectuée: %1" # #: ContentGenerator/Instructor/ProblemSetList.pm:386 msgid "Results of last action performed" -msgstr "tr: Results of last action performed" +msgstr "Résultats de la dernière action effectuée" # #: ContentGenerator/Instructor/ProblemSetList.pm:1732 #: ContentGenerator/Instructor/UserList.pm:1415 #: ContentGenerator/Instructor/UserList.pm:1487 msgid "Save changes" -msgstr "tr: Save changes" +msgstr "Sauvegarder les modifications" # #: ContentGenerator/Instructor/ProblemSetList.pm:1186 msgid "Score which sets?" -msgstr "tr: Score which sets?" +msgstr "Évaluer quels devoirs?" # #: ContentGenerator/Instructor/UserList.pm:2070 @@ -984,56 +946,59 @@ msgstr "tr: Score which sets?" #: ContentGenerator/Instructor/UserList.pm:859 #: ContentGenerator/Instructor/UserList.pm:902 msgid "Section" -msgstr "tr: Section" +msgstr "Section" # #: ContentGenerator/Instructor/ProblemSetList.pm:582 msgid "Select all sets" -msgstr "tr: Select all sets" +msgstr "Sélectionner tous les devoirs" # #: ContentGenerator/Instructor/UserList.pm:535 msgid "Select all users" -msgstr "tr: Select all users" +msgstr "Sélectionner tous les usagers" # #: ContentGenerator/Instructor/ProblemSetList.pm:534 #: ContentGenerator/Instructor/UserList.pm:512 msgid "Select an action to perform" -msgstr "tr: Select an action to perform" +msgstr "Sélectionner une action à effectuer" # #. ($newSetID) #: ContentGenerator/Instructor/ProblemSetList.pm:1375 -#, fuzzy msgid "Set %1 exists. No set created" -msgstr "tr: Set %1 exists. No set created" +msgstr "Le devoir %1 existe. Aucun devoir créé" #: ContentGenerator/Instructor/ProblemSetList.pm:440 msgid "Set Definition Filename" -msgstr "" +msgstr "Modifier le fichier de définition" # #: ContentGenerator/Instructor/ProblemSetList.pm:442 #: ContentGenerator/Instructor/ProblemSetList.pm:828 #: ContentGenerator/Instructor/ProblemSetList.pm:866 +#, fuzzy msgid "Set Header" -msgstr "tr: Set Header" +msgstr "En-tête" # #: ContentGenerator/ProblemSet.pm:273 #: ContentGenerator/ProblemSet.pm:275 +#, fuzzy msgid "Set Info" msgstr "tr: Set Info" # #: ContentGenerator/Instructor/ProblemSetList.pm:2700 +#, fuzzy msgid "Set List" msgstr "tr: Set List" # #: ContentGenerator/Instructor/ProblemSetList.pm:827 #: ContentGenerator/Instructor/ProblemSetList.pm:865 +#, fuzzy msgid "Set Name" msgstr "tr: Set Name" @@ -1060,7 +1025,7 @@ msgstr "Afficher les solutions" # #: ContentGenerator/Instructor/UserList.pm:645 msgid "Show Which Users?" -msgstr "tr: Show Which Users?" +msgstr "Montrer quels usagers?" # #: ContentGenerator/Problem.pm:1044 @@ -1070,47 +1035,44 @@ msgstr "Montrer les bonnes réponses" # #: ContentGenerator/Instructor/ProblemSetList.pm:683 msgid "Show which sets?" -msgstr "tr: Show which sets?" +msgstr "Montrer quels devoirs?" # #: ContentGenerator/Instructor/ProblemSetList.pm:497 #: ContentGenerator/Instructor/UserList.pm:475 msgid "Show/Hide Site Description" -msgstr "tr: Show/Hide Site Description" +msgstr "Montrer/cacher la description du site web" # #. (scalar @visibleSetIDs, scalar @allSetIDs) #: ContentGenerator/Instructor/ProblemSetList.pm:607 -#, fuzzy msgid "Showing %1 out of %2 sets." -msgstr "tr: Showing %1 out of %2 sets." +msgstr "%1 devoirs affichés sur un total de %2." # #. (scalar @Users, scalar @allUserIDs) #: ContentGenerator/Instructor/UserList.pm:556 -#, fuzzy msgid "Showing %1 out of %2 users" -msgstr "tr: Showing %1 out of %2 users" +msgstr "%1 usagers affichés sur un total de %2." # #: ContentGenerator/Login.pm:96 #: ContentGenerator/Login.pm:99 #, fuzzy msgid "Site Information" -msgstr "tr: Options Information" +msgstr "tr: Site Information" # #: ContentGenerator/Instructor/ProblemSetList.pm:821 #: ContentGenerator/Instructor/UserList.pm:804 msgid "Sort by" -msgstr "tr: Sort by" +msgstr "Trier selon" # #. ($names{$primary}, $names{$secondary}) #: ContentGenerator/Instructor/ProblemSetList.pm:899 -#, fuzzy msgid "Sort by %1 and then by %2" -msgstr "tr: Sort by %1 and then by %2" +msgstr "Trier selon %1 et ensuite selon %2" # #: ContentGenerator/Instructor/UserList.pm:2069 @@ -1133,7 +1095,7 @@ msgstr "Cesser de jouer le rôle" #: ContentGenerator/Instructor/UserList.pm:857 #: ContentGenerator/Instructor/UserList.pm:900 msgid "Student ID" -msgstr "tr: Student ID" +msgstr "ID de l'étudiant" # #: ContentGenerator/Problem.pm:1127 @@ -1143,54 +1105,51 @@ msgstr "Soumettre ses réponses" # #. ($effectiveUser) #: ContentGenerator/Problem.pm:1124 -#, fuzzy msgid "Submit Answers for %1" -msgstr "%1 Soumettre ses réponses" +msgstr "Soumettre ses réponses de %1" # #: ContentGenerator/Instructor/ProblemSetList.pm:1850 msgid "Success" -msgstr "tr: Success" +msgstr "Succès" #. ($newSetID) #: ContentGenerator/Instructor/ProblemSetList.pm:1437 msgid "Successfully created new set %1" -msgstr "" +msgstr "Création réussie du nouveau devoir %1" # #. ($name) #: ContentGenerator/ProblemSets.pm:424 #: ContentGenerator/ProblemSets.pm:433 #: ContentGenerator/ProblemSets.pm:439 -#, fuzzy msgid "Take %1 test" -msgstr "tr: Take %1 test" +msgstr "Faire le test %1" # #: ContentGenerator/Instructor/ProblemSetList.pm:598 #: ContentGenerator/Instructor/UserList.pm:551 msgid "Take Action!" -msgstr "tr: Take Action!" +msgstr "Effectuer l'action!" # #: ContentGenerator/ProblemSets.pm:236 msgid "Test Date" -msgstr "tr: Test Date" +msgstr "Date du test" # #: ContentGenerator/ProblemSets.pm:235 msgid "Test Score" -msgstr "tr: Test Score" +msgstr "Résultat du test" # #: ContentGenerator.pm:954 msgid "The WeBWorK Project" -msgstr "tr: The WeBWorK Project" +msgstr "Le projet WeBWorK" # #. ($fully) #: ContentGenerator/Problem.pm:307 -#, fuzzy msgid "The answer above is NOT %1correct." msgstr "La réponse ci-dessus N'EST PAS %1correcte." @@ -1202,7 +1161,6 @@ msgstr "La réponse ci-dessus est correcte." # #. (CGI::b($r->maketext("[_1]'s Current Password",$user_name) #: ContentGenerator/Options.pm:107 -#, fuzzy msgid "" "The password you entered in the %1 \n" "\t\t\t\t\t\tfield does not match your current\n" @@ -1213,7 +1171,6 @@ msgstr "Le mot de passe que vous avez entré dans la case %1 est erroné. Veuill # #. (CGI::b($r->maketext("[_1]'s New Password",$e_user_name) #: ContentGenerator/Options.pm:91 -#, fuzzy msgid "" "The passwords you entered in the \n" "\t\t\t\t\t\t\t\t%1 and %2\n" @@ -1224,16 +1181,15 @@ msgstr "Les mots de passe saisis dans les champs %1 et %2 sont différents. Veui # #. ($setName,$effectiveUser) #: ContentGenerator/ProblemSet.pm:308 -#, fuzzy msgid "The selected problem set (%1) is not a valid set for %2" -msgstr "tr: The selected problem set (%1) is not a valid set for %2" +msgstr "Le devoir sélectionné (%1) n'est pas un devoir valide pour %2" # #: ContentGenerator/Instructor/ProblemSetList.pm:859 #: ContentGenerator/Instructor/UserList.pm:847 #: ContentGenerator/Instructor/UserList.pm:890 msgid "Then by" -msgstr "tr: Then by" +msgstr "Ensuite selon" # #: ContentGenerator/ProblemSet.pm:365 @@ -1259,47 +1215,44 @@ msgstr "Ce problème n'est pas noté." #. (CGI::font({class=>$visiblityStateClass}, $visiblityStateText) #: ContentGenerator/Problem.pm:605 #: ContentGenerator/ProblemSet.pm:88 -#, fuzzy msgid "This set is %1" -msgstr "tr: This set is %1" +msgstr "Ce devoir est %1" # #: ContentGenerator/Instructor/ProblemSetList.pm:588 msgid "Unselect all sets" -msgstr "tr: Unselect all sets" +msgstr "Dessélectionner tous les devoirs" # #: ContentGenerator/Instructor/UserList.pm:541 msgid "Unselect all users" -msgstr "tr: Unselect all users" +msgstr "Dessélectionner pour les usagers" # #: ContentGenerator/Instructor/ProblemSetList.pm:2683 msgid "Use System Default" -msgstr "tr: Use System Default" +msgstr "Utiliser les paramètres par défaut" # #: ContentGenerator/Login.pm:200 -#, fuzzy msgid "Username" -msgstr "tr: Filename" +msgstr "Nom d'usager" # #: ContentGenerator/Instructor/UserList.pm:2087 msgid "Users List" -msgstr "tr: Users List" +msgstr "Liste d'usagers" # #. ($names{$primary}, $names{$secondary}, $names{$ternary}) #: ContentGenerator/Instructor/UserList.pm:938 -#, fuzzy msgid "Users sorted by %1, then by %2, then by %3" -msgstr "tr: Users sorted by %1, then by %2, then by %3" +msgstr "Usagers triés selon %1, ensuite selon %2 et ensuite selon %3" # #: ContentGenerator/ProblemSet.pm:230 msgid "Viewing temporary file" -msgstr "tr: Viewing temporary file" +msgstr "Affichage du fichier temporaire" # #: ContentGenerator/Problem.pm:1332 @@ -1311,19 +1264,19 @@ msgstr "Visionnement du fichier temporaire : " #: ContentGenerator/Instructor/ProblemSetList.pm:833 #: ContentGenerator/Instructor/ProblemSetList.pm:871 msgid "Visibility" -msgstr "tr: Visibility" +msgstr "Visibilité" # #: ContentGenerator/Instructor/ProblemSetList.pm:1018 #: ContentGenerator/Instructor/ProblemSetList.pm:447 msgid "Visible" -msgstr "tr: Visible" +msgstr "Visible" # #: ContentGenerator/Instructor/ProblemSetList.pm:1259 #: ContentGenerator/Instructor/UserList.pm:1078 msgid "Warning: Deletion destroys all user-related data and is not undoable!" -msgstr "tr: Warning: Deletion destroys all user-related data and is not undoable!" +msgstr "Avertissement: la suppression détruit toutes les données des usagers et est irréversible" # #: ContentGenerator/Home.pm:87 @@ -1332,6 +1285,7 @@ msgstr "Bienvenue sur WeBWorK!" # #: ContentGenerator/Instructor/UserList.pm:665 +#, fuzzy msgid "What field should filtered users match on?" msgstr "tr: What field should filtered users match on?" @@ -1351,35 +1305,34 @@ msgstr "Oui" #: ContentGenerator/Instructor/ProblemSetList.pm:418 #: ContentGenerator/Instructor/UserList.pm:262 msgid "You are not authorized to access the instructor tools." -msgstr "tr: You are not authorized to access the instructor tools." +msgstr "Vous n'êtes pas autorisé à accéder aux outils de l'enseignant." # #: ContentGenerator/Instructor/ProblemSetList.pm:339 msgid "You are not authorized to modify homework sets." -msgstr "tr: You are not authorized to modify homework sets." +msgstr "Vous n'êtes pas autorisé à modifier les devoirs." # #: ContentGenerator/Instructor/ProblemSetList.pm:344 msgid "You are not authorized to modify set definition files." -msgstr "tr: You are not authorized to modify set definition files." +msgstr "Vous n'êtes pas autorisé à modifier les fichiers de définition." # #: ContentGenerator/Instructor/UserList.pm:329 #: ContentGenerator/Instructor/UserList.pm:335 -#, fuzzy msgid "You are not authorized to modify student data" -msgstr "tr: You are not authorized to modify student data." +msgstr "Vous n'êtes pas autorisé à modifier les données des étudiants." # #: ContentGenerator/Instructor/ProblemSetList.pm:389 #: ContentGenerator/Instructor/UserList.pm:380 msgid "You are not authorized to perform this action." -msgstr "tr: You are not authorized to perform this action." +msgstr "Vous n'êtes pas autorisé à effectuer cette action." # #: ContentGenerator/Instructor/UserList.pm:1121 msgid "You cannot delete yourself!" -msgstr "tr: You cannot delete yourself!" +msgstr "Vous ne pouvez pas vous supprimer vous-même!" # #: ContentGenerator/Options.pm:163 @@ -1396,7 +1349,6 @@ msgstr "Vos droits ne vous permettent pas de modifier votre mot de passe." # #. ($attemptsLeft,$attemptsLeftNoun) #: ContentGenerator/Problem.pm:1196 -#, fuzzy msgid "You have %1 %2 remaining." msgstr "Il reste %1 %2." @@ -1422,7 +1374,7 @@ msgstr "Vous êtes déconnecté de WeBWorK." # #: ContentGenerator/Instructor/UserList.pm:1933 msgid "You may not change your own password here!" -msgstr "tr: You may not change your own password here!" +msgstr "Vous ne pouvez pas modifier votre propre mot de passe ici!" # #: Authen.pm:317 @@ -1432,7 +1384,6 @@ msgstr "Spécifier le code d'usager." # #. (sprintf("%.0f%%", $pg->{result}->{score} * 100) #: ContentGenerator/Problem.pm:1192 -#, fuzzy msgid "You received a score of %1 for this attempt." msgstr "Résultat pour cette tentative : %1." @@ -1444,7 +1395,6 @@ msgstr "Votre courriel a été modifié." # #. ($lastScore,$notCountedMessage) #: ContentGenerator/Problem.pm:1194 -#, fuzzy msgid "Your overall recorded score is %1. %2" msgstr "Le résultat final est : %1. %2" @@ -1456,7 +1406,6 @@ msgstr "Cette session est échue. Veuillez vous reconnecter" # #: ContentGenerator/ProblemSet.pm:273 #: ContentGenerator/ProblemSets.pm:71 -#, fuzzy msgid "[edit]" msgstr "~[modifier~]" @@ -1508,17 +1457,17 @@ msgstr "WebWork a rencontré une erreur logicielle lors de la manipulation de ce # #: ContentGenerator/Instructor/ProblemSetList.pm:1358 msgid "a duplicate of the first selected set" -msgstr "tr: a duplicate of the first selected set" +msgstr "une copie du premier devoir sélectionné" # #: ContentGenerator/Instructor/ProblemSetList.pm:1357 msgid "a new empty set" -msgstr "tr: a new empty set" +msgstr "un nouveau devoir vide" # #: ContentGenerator/Instructor/ProblemSetList.pm:1479 msgid "a single set" -msgstr "tr: a single set" +msgstr "un seul devoir" # #: ContentGenerator/Instructor/ProblemSetList.pm:1091 @@ -1528,7 +1477,7 @@ msgstr "tr: a single set" #: ContentGenerator/Instructor/ProblemSetList.pm:929 #: ContentGenerator/Instructor/ProblemSetList.pm:989 msgid "all sets" -msgstr "tr: all sets" +msgstr "tous les devoirs" # #: ContentGenerator/Instructor/ProblemSetList.pm:1551 @@ -1537,13 +1486,13 @@ msgstr "tr: all sets" #: ContentGenerator/Instructor/UserList.pm:651 #: ContentGenerator/Instructor/UserList.pm:967 msgid "all users" -msgstr "tr: all users" +msgstr "tous les usagers" # #: ContentGenerator/Instructor/UserList.pm:1196 #: ContentGenerator/Instructor/UserList.pm:1226 msgid "any users" -msgstr "tr: any users" +msgstr "n'importe quel usager" # #: ContentGenerator/Problem.pm:1165 @@ -1559,12 +1508,12 @@ msgstr "essais" # #: ContentGenerator/Instructor/ProblemSetList.pm:1726 msgid "changes abandoned" -msgstr "tr: changes abandoned" +msgstr "modificiations annulées" # #: ContentGenerator/Instructor/ProblemSetList.pm:1790 msgid "changes saved" -msgstr "tr: changes saved" +msgstr "modifications sauvegardées" # #: ContentGenerator/ProblemSets.pm:468 @@ -1574,7 +1523,6 @@ msgstr "échu, réponses disponibles" # #. ($self->formatDateTime($set->answer_date) #: ContentGenerator/ProblemSets.pm:464 -#, fuzzy msgid "closed, answers on %1" msgstr "échu, réponses disponibles le %1" @@ -1585,14 +1533,13 @@ msgstr "échu, réponses disponibles depuis peu" # #: ContentGenerator/ProblemSets.pm:402 -#, fuzzy msgid "completed." -msgstr "tr: completely" +msgstr "complet." # #: ContentGenerator/Problem.pm:267 msgid "completely" -msgstr "tr: completely" +msgstr "complètement" # #: ContentGenerator/Problem.pm:264 @@ -1602,104 +1549,102 @@ msgstr "correct" # #. ($num) #: ContentGenerator/Instructor/ProblemSetList.pm:1310 -#, fuzzy msgid "deleted %1 sets" -msgstr "tr: deleted %1 sets" +msgstr "suppression de %1 devoirs" # #. ($num) #: ContentGenerator/Instructor/UserList.pm:1135 -#, fuzzy msgid "deleted %1 users" -msgstr "tr: deleted %1 users" +msgstr "suppression de %1 usagers" # #: ContentGenerator/Instructor/ProblemSetList.pm:947 msgid "editing all sets" -msgstr "tr: editing all sets" +msgstr "modification de tous les devoirs" # #: ContentGenerator/Instructor/UserList.pm:985 msgid "editing all users" -msgstr "tr: editing all users" +msgstr "modification de tous les usagers" # #: ContentGenerator/Instructor/ProblemSetList.pm:953 msgid "editing selected sets" -msgstr "tr: editing selected sets" +msgstr "modification des devoirs sélectionnés" # #: ContentGenerator/Instructor/UserList.pm:991 msgid "editing selected users" -msgstr "tr: editing selected users" +msgstr "modification des usagers sélectionnés" # #: ContentGenerator/Instructor/ProblemSetList.pm:950 msgid "editing visible sets" -msgstr "tr: editing visible sets" +msgstr "modifications des devoirs visibles" # #: ContentGenerator/Instructor/UserList.pm:988 msgid "editing visible users" -msgstr "tr: editing visible users" +msgstr "modification des usagers visibles" # #: ContentGenerator/Instructor/ProblemSetList.pm:694 +#, fuzzy msgid "enter matching set IDs below" msgstr "tr: enter matching set IDs below" # #: ContentGenerator/Instructor/ProblemSetList.pm:1665 msgid "export abandoned" -msgstr "tr: export abandoned" +msgstr "exportation annulée" # #: ContentGenerator/Instructor/ProblemSetList.pm:1629 msgid "exporting all sets" -msgstr "tr: exporting all sets" +msgstr "exportation de tous les devoirs" # #: ContentGenerator/Instructor/ProblemSetList.pm:1636 msgid "exporting selected sets" -msgstr "tr: exporting selected sets" +msgstr "exportation des devoirs sélectionnés" # #: ContentGenerator/Instructor/ProblemSetList.pm:1633 msgid "exporting visible sets" -msgstr "tr: exporting visible sets" +msgstr "exportation des devoirs visibles" # #: ContentGenerator/Instructor/UserList.pm:1044 msgid "giving new passwords to all users" -msgstr "tr: giving new passwords to all users" +msgstr "assignation de nouveaux mots de passe à tous les usagers" # #: ContentGenerator/Instructor/UserList.pm:1050 msgid "giving new passwords to selected users" -msgstr "tr: giving new passwords to selected users" +msgstr "assignation de nouveaux mots de passe aux usagers sélectionnés" # #: ContentGenerator/Instructor/UserList.pm:1047 msgid "giving new passwords to visible users" -msgstr "tr: giving new passwords to visible users" +msgstr "assignation de nouveaux mots de passe aux usagers visibles" # #: ContentGenerator/Instructor/ProblemSetList.pm:2522 #: ContentGenerator/Problem.pm:603 -#, fuzzy msgid "hidden" -msgstr "tr: Hidden" +msgstr "caché" # #: ContentGenerator/Problem.pm:604 #: ContentGenerator/ProblemSet.pm:86 msgid "hidden from students" -msgstr "tr: hidden from students" +msgstr "caché pour les étudiants" # #: ContentGenerator/Instructor/ProblemSetList.pm:693 msgid "hidden sets" -msgstr "tr: hidden sets" +msgstr "devoirs cachés" # #: ContentGenerator/Problem.pm:266 @@ -1709,7 +1654,7 @@ msgstr "erroné" # #: ContentGenerator/Instructor/ProblemSetList.pm:1480 msgid "multiple sets" -msgstr "tr: multiple sets" +msgstr "devoirs multiples" # #: ContentGenerator/Problem.pm:864 @@ -1753,7 +1698,7 @@ msgstr "Haut" #: ContentGenerator/Instructor/ProblemSetList.pm:690 #: ContentGenerator/Instructor/ProblemSetList.pm:988 msgid "no sets" -msgstr "tr: no sets" +msgstr "aucun devoir" # #: ContentGenerator/Instructor/ProblemSetList.pm:1552 @@ -1762,7 +1707,7 @@ msgstr "tr: no sets" #: ContentGenerator/Instructor/UserList.pm:1227 #: ContentGenerator/Instructor/UserList.pm:652 msgid "no users" -msgstr "tr: no users" +msgstr "aucun usager" # #: ContentGenerator/ProblemSets.pm:430 @@ -1773,7 +1718,6 @@ msgstr "Disponible, date de remise : " # #. ($self->formatDateTime($set->due_date() #: ContentGenerator/ProblemSets.pm:408 -#, fuzzy msgid "open: complete by %1" msgstr "Disponible : date de remise : %1" @@ -1791,7 +1735,7 @@ msgstr "Le temps est écoulé. Fin." #: ContentGenerator/Instructor/ProblemSetList.pm:931 #: ContentGenerator/Instructor/ProblemSetList.pm:991 msgid "selected sets" -msgstr "tr: selected sets" +msgstr "devoirs sélectionnés" # #: ContentGenerator/Instructor/UserList.pm:1028 @@ -1801,46 +1745,45 @@ msgstr "tr: selected sets" #: ContentGenerator/Instructor/UserList.pm:653 #: ContentGenerator/Instructor/UserList.pm:969 msgid "selected users" -msgstr "tr: selected users" +msgstr "usagers sélectionnés" # #: ContentGenerator/Instructor/ProblemSetList.pm:762 msgid "showing all sets" -msgstr "tr: showing all sets" +msgstr "affichage de tous les devoirs" # #: ContentGenerator/Instructor/UserList.pm:738 msgid "showing all users" -msgstr "tr: showing all users" +msgstr "affichage de tous les usagers" # #: ContentGenerator/Instructor/UserList.pm:747 msgid "showing matching users" -msgstr "tr: showing matching users" +msgstr "affichage des usagers correspondant" # #: ContentGenerator/Instructor/ProblemSetList.pm:765 msgid "showing no sets" -msgstr "tr: showing no sets" +msgstr "affichage d'aucun devoir" # #: ContentGenerator/Instructor/UserList.pm:741 msgid "showing no users" -msgstr "tr: showing no users" +msgstr "affichage d'aucun usager" # #: ContentGenerator/Instructor/ProblemSetList.pm:768 msgid "showing selected sets" -msgstr "tr: showing selected sets" +msgstr "affichage des devoirs sélectionnés" # #: ContentGenerator/Instructor/UserList.pm:744 msgid "showing selected users" -msgstr "tr: showing selected users" +msgstr "affichage des usagers sélectionnés" # #: ContentGenerator/Problem.pm:1124 -#, fuzzy msgid "submitAnswers" msgstr "Soumettre ses réponses" @@ -1863,27 +1806,26 @@ msgstr "illimité" # #: ContentGenerator/Instructor/UserList.pm:654 msgid "users who match on selected field" -msgstr "tr: users who match on selected field" +msgstr "usagers qui correspondent pour les champs sélectionnés" # #: ContentGenerator/Instructor/ProblemSetList.pm:2522 #: ContentGenerator/Problem.pm:603 -#, fuzzy msgid "visible" -msgstr "tr: Visible" +msgstr "visible" # #: ContentGenerator/Instructor/ProblemSetList.pm:1611 #: ContentGenerator/Instructor/ProblemSetList.pm:692 #: ContentGenerator/Instructor/ProblemSetList.pm:930 msgid "visible sets" -msgstr "tr: visible sets" +msgstr "devoirs visibles" # #: ContentGenerator/Problem.pm:604 #: ContentGenerator/ProblemSet.pm:86 msgid "visible to students" -msgstr "tr: visible to students" +msgstr "visible pour les étudiants" # #: ContentGenerator/Instructor/UserList.pm:1027 @@ -1891,28 +1833,27 @@ msgstr "tr: visible to students" #: ContentGenerator/Instructor/UserList.pm:1299 #: ContentGenerator/Instructor/UserList.pm:968 msgid "visible users" -msgstr "tr: visible users" +msgstr "usagers visibles" # #. ($self->formatDateTime($set->open_date) #: ContentGenerator/ProblemSets.pm:420 #: ContentGenerator/ProblemSets.pm:448 -#, fuzzy msgid "will open on %1" msgstr "sera disponible le %1" # # msgid "Add to which set?" -msgstr "tr: Add to which set?" +msgstr "Ajouter à quel devoir?" # msgid "Course Information for course [_1]" -msgstr "tr: Course Information for course %1" +msgstr "Information du cours pour le cours %1" # msgid "report bugs in this problem" -msgstr "tr: report bugs in this problem" +msgstr "signaler un bogue pour ce problème" # msgid "Problem [_1]" @@ -1920,68 +1861,71 @@ msgstr "Problème %1" # msgid "Cancel Password" -msgstr "tr: Cancel Password" +msgstr "Annuler le mot de passe" # msgid "WeBWorK" msgstr "WeBWorK" # +#, fuzzy msgid "Audit" msgstr "tr: Enrolled" # +#, fuzzy msgid "Hardcopy Header for set [_1]" msgstr "tr: Hardcopy Header for set %1" # msgid "Report bugs in a WeBWorK question/problem using this link. The very first time you do this you will need to register with an email address so that information on the bug fix can be reported back to you." -msgstr "tr: Report bugs in a WeBWorK question/problem using this link. The very first time you do this you will need to register with an email address so that information on the bug fix can be reported back to you." +msgstr "Signaler un bogue ou encore poser une question à WeBWorK en suivant ce lien. À la première utilisation, il sera nécessaire de s'enregistrer en fournissant une adresse courriel valide, de telle sorte d'assurer un suivi sur les correctifs." # msgid "Pad Fields" msgstr "Remplir les champs" # +#, fuzzy msgid "Set Header for set [_1]" msgstr "tr: Set Header for set %1" # -#, fuzzy msgid "Statistics for [_1]" -msgstr "Statistiques" +msgstr "Statistiques pour %1" # msgid "Filter" -msgstr "tr: Filter" +msgstr "Filtre" # msgid "set [_1]/problem [_2]" -msgstr "tr: set %1/problem %2" +msgstr "devoir %1/problème %2" # msgid "You are not authorized to modify problems." -msgstr "tr: You are not authorized to modify problems." +msgstr "Vous n'êtes pas autorisé à modifier des problèmes." # +#, fuzzy msgid "hardcopy header file" msgstr "tr: hardcopy header file" # msgid "Write permissions have not been enabled in the templates directory. No changes can be made." -msgstr "tr: Write permissions have not been enabled in the templates directory. No changes can be made." +msgstr "Les droit d'écriture n'ont pas été activés pour le répertoire templates. Aucune modification ne peut être effectuée." # msgid "Save [_1] and View" -msgstr "tr: Save %1 and View" +msgstr "Sauvegarder %1 et visualiser" # msgid "Create" -msgstr "tr: Create" +msgstr "Créer" # msgid "Changes in this file have not yet been permanently saved." -msgstr "tr: Changes in this file have not yet been permanently saved." +msgstr "Les modifications dans ce fichier n'ont pas été sauvegardées." # msgid "Instructor Tools" @@ -2004,9 +1948,8 @@ msgid "Unpublished" msgstr "Non publié" # -#, fuzzy msgid "Login" -msgstr "tr: Login Name" +msgstr "Nom d'utilisateur" # msgid "Email" @@ -2018,11 +1961,11 @@ msgstr "Gestionnaire de fichiers" # msgid "Unable to change the hardcopy header for set [_1]. Unknown error." -msgstr "Impossible de modifier l'entête d'impression du devoir %1. Erreur inconnue." +msgstr "Impossible de modifier l'en-tête d'impression du devoir %1. Erreur inconnue." # msgid "Unable to make '[_1]' the set header for [_2]" -msgstr "Impossible de faire de '%1' l'entête du devoir %2" +msgstr "Impossible de faire de '%1' l'en-tête du devoir %2" # msgid "Can't determine user of temporary edit file [_1]." @@ -2041,6 +1984,7 @@ msgid "The file '[_1]' is protected!" msgstr "Le fichier '%1' est protégé." # +#, fuzzy msgid "Top level of author information on the wiki." msgstr "tr: Top level of author information on the wiki." @@ -2066,7 +2010,7 @@ msgstr "aucune action" # msgid "Added '[_1]' to [_2] as new set header" -msgstr "tr: Added '%1' to %2 as new set header" +msgstr "Ajout de '%1' à %2 en tant que nouvel en-tête" # msgid "View statistics by set" @@ -2154,7 +2098,7 @@ msgstr "Ce devoir est %1 pour les étudiants." # msgid "header file" -msgstr "Fichier d'entête" +msgstr "Fichier d'en-tête" # msgid "Include Index" @@ -2189,6 +2133,7 @@ msgid "Write permissions have not been enabled for '[_1]'. Changes must be save msgstr "Il n'est pas permis d'écrire dans '%1'. Les modifications doivent d'abord être enregistrées dans un autre fichier." # +#, fuzzy msgid "Note: this problem viewer is for viewing purposes only. As of right now, testing functionality is not possible." msgstr "tr: Note: this problem viewer is for viewing purposes only. As of right now, testing functionality is not possible." @@ -2201,6 +2146,7 @@ msgid "webwork" msgstr "webwork" # +#, fuzzy msgid "Problem Viewer" msgstr "tr: Problem Viewer" @@ -2209,9 +2155,8 @@ msgid "submit button clicked" msgstr "On a appuyé sur le bouton Soumission" # -#, fuzzy msgid "Student Progress for [_1]" -msgstr "Progrès des étudiants" +msgstr "Progrès des étudiants pour %1" # msgid "A new file has been created at '[_1]' with the contents below. No changes have been made to set [_2]." @@ -2254,15 +2199,15 @@ msgid "Test snippets of PG code in interactive lab. Good way to learn PG langua msgstr "Tester des bouts de code PG dans un labo interactif. Une bonne façon d'apprendre le langage PG." # -#, fuzzy msgid "Page generated at [_1] at [_2]" -msgstr "tr: Page generated at %1" +msgstr "Page générée à %1 le %2" # msgid "Import" msgstr "Importer" # +#, fuzzy msgid "View Using Seed Number" msgstr "tr: View Using Seed Number" @@ -2332,7 +2277,7 @@ msgstr "Votre résultat n'a pas été enregistré à cause d'une défaillance da # msgid "The hardcopy header for set [_1] has been renamed to '[_2]'." -msgstr "L'entête d'impression du devoir %1 a été renommée '%2'." +msgstr "L'en-tête d'impression du devoir %1 a été renommé '%2'." # msgid "Save as" @@ -2360,7 +2305,7 @@ msgstr "Le fichier '%1' existe déjà. Le fichier ne sera pas enregistré. Aucun # msgid "The set header for set [_1] has been renamed to '[_2]'." -msgstr "L'entête de devoir pour le devoir %1 a été renommée par '%2'." +msgstr "L'en-tête de devoir pour le devoir %1 a été renommé par '%2'." # msgid "Error: This path is already in the temporary edit directory -- no new temporary file is created. path = [_1]" @@ -2395,7 +2340,6 @@ msgid "hidden from" msgstr "invisible" # -#, fuzzy msgid "View student progress by set" msgstr "Progrès des étudiants" @@ -2440,6 +2384,7 @@ msgid "Options Information" msgstr "Information sur les options" # +#, fuzzy msgid "Proctor" msgstr "tr: Proctor" @@ -2448,6 +2393,7 @@ msgid "Student Progress" msgstr "Progrès des étudiants" # +#, fuzzy msgid "Revert" msgstr "tr: Revert" @@ -2505,7 +2451,7 @@ msgstr "Erreur : Le fichier original %1 ne peut être lu." # msgid "Unable to change the set header for set [_1]. Unknown error." -msgstr "Impossible de modifier l'entête du devoir %1. Erreur inconnue." +msgstr "Impossible de modifier l'en-tête du devoir %1. Erreur inconnue." # msgid "options information" @@ -2528,6 +2474,7 @@ msgid "Change User Options" msgstr "Changer les options d'usager" # +#, fuzzy msgid "Drop" msgstr "tr: Drop" diff --git a/lib/WeBWorK/Localize/tr.po b/lib/WeBWorK/Localize/tr.po index 4e9219a458..70069a005b 100644 --- a/lib/WeBWorK/Localize/tr.po +++ b/lib/WeBWorK/Localize/tr.po @@ -1,2562 +1,2522 @@ -# SOME DESCRIPTIVE TITLE. -# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER -# This file is distributed under the same license as the PACKAGE package. -# FIRST AUTHOR , YEAR. -# -msgid "" -msgstr "" -"Project-Id-Version: WeBWorK Online Homework System\n" -"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" -"PO-Revision-Date: 2011-12-20 11:29-0500\n" -"Last-Translator: Michael Gage \n" -"Language-Team: Webwork Turkish language team >\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Language: \n" -"X-Poedit-Language: Turkish\n" -"X-Poedit-Country: TURKEY\n" - -# -#. ($name, $vnum) -#: ContentGenerator/ProblemSets.pm:416 -msgid "%1 (test %2)" -msgstr "%1 (test %2)" - -# -#. ($numAdded, $numSkipped, join(", ", @$skipped) -#: ContentGenerator/Instructor/ProblemSetList.pm:1581 -msgid "%1 sets added, %2 sets skipped. Skipped sets: (%3)" -msgstr "%1 set eklendi, %2 sets atlandı. Atlanan setler: (%3)" - -# -#. ($numExported, $numSkipped, (($numSkipped) -#: ContentGenerator/Instructor/ProblemSetList.pm:1701 -msgid "%1 sets exported, %2 sets skipped. Skipped sets: (%3)" -msgstr "%1 set aktarıldı, %2 set atlandı. Atlanan setler: (%3)" - -# -#. ($name) -#: ContentGenerator/ProblemSets.pm:427 -#: ContentGenerator/ProblemSets.pm:441 -msgid "%1 test" -msgstr " %1 test" - -# -#. (scalar @userIDsToExport, $dir, $fileName) -#: ContentGenerator/Instructor/UserList.pm:1385 -msgid "%1 users exported to file %2/%3" -msgstr "%1 kullanıcı %2/%3 dosyasına aktarıldı." - -# -#. ($numReplaced, $numAdded, $numSkipped, join (", ", @$skipped) -#: ContentGenerator/Instructor/UserList.pm:1269 -msgid "%1 users replaced, %2 users added, %3 users skipped. Skipped users: (%4)" -msgstr "%1 kullanıcı deÄŸiÅŸtirildi, %2 kullanıcı eklendi, %3 kullanıcı atlandı. Atlanan kullanıcılar: (%4)" - -#. (int($answerScore*100) -#: ContentGenerator/Problem.pm:265 -#, fuzzy -msgid "%1% correct" -msgstr "%1% doÄŸru" - -# -#. ($e_user_name) -#: ContentGenerator/Options.pm:171 -#, fuzzy -msgid "%1's Current Address" -msgstr "%1 için Mevcut Adres" - -# -#. ($user_name) -#: ContentGenerator/Options.pm:126 -#, fuzzy -msgid "%1's Current Password" -msgstr "%1 için Mevcut Åžifre" - -# -#. ($e_user_name) -#: ContentGenerator/Options.pm:175 -#, fuzzy -msgid "%1's New Address" -msgstr "%1 için Yeni Adres" - -# -#. ($e_user_name) -#: ContentGenerator/Options.pm:130 -#, fuzzy -msgid "%1's New Password" -msgstr "%1 için Yeni Åžifre" - -# -#. ($e_user_name) -#: ContentGenerator/Options.pm:101 -#, fuzzy -msgid "%1's new password cannot be blank." -msgstr "%1 için yeni ÅŸifre boÅŸ bırakılamaz." - -# -#. ($e_user_name) -#: ContentGenerator/Options.pm:84 -#, fuzzy -msgid "%1's password has been changed." -msgstr "%1 adlı kullanıcının ÅŸifresi deÄŸiÅŸtirildi." - -# -#. ($setID, $problemID) -#: ContentGenerator/Problem.pm:884 -#, fuzzy -msgid "%1: Problem %2" -msgstr "%1: Soru %2." - -# -#. ($numBlanks) -#: ContentGenerator/Problem.pm:319 -#, fuzzy -msgid "%quant(%1,of the questions remains,of the questions remain) unanswered." -msgstr "Soruların %1 tanesi yanıtsız bırakıldı." - -# -#: ContentGenerator/Problem.pm:1188 -msgid "(This problem will not count towards your grade.)" -msgstr "(Bu soru notunuzu etkilemeyecektir.)" - -# -#: ContentGenerator/Problem.pm:1299 -msgid "ANSWERS ONLY CHECKED -- ANSWERS NOT RECORDED" -msgstr "YANITLAR SADECE KONTROL EDÄ°LÄ°YOR -- YANITLAR KAYDEDÄ°LMEDÄ°" - -# -#: ContentGenerator/Instructor/ProblemSetList.pm:1708 -#: ContentGenerator/Instructor/UserList.pm:1391 -#: ContentGenerator/Instructor/UserList.pm:1463 -msgid "Abandon changes" -msgstr "DeÄŸiÅŸiklikleri kaydetme" - -# -#: ContentGenerator/Instructor/ProblemSetList.pm:1647 -msgid "Abandon export" -msgstr "Dışa aktarımı iptal et" - -# -#. ($eUserID) -#: ContentGenerator.pm:848 -#, fuzzy -msgid "Acting as %1. " -msgstr "%1 gibi davranılıyor." - -# -#: ContentGenerator/Instructor/UserList.pm:1927 -msgid "Active" -msgstr "Aktif" - -# -#: ContentGenerator/Instructor/UserList.pm:1142 -msgid "Add how many students?" -msgstr "Kaç öğrenci eklenecek?" - -# -#: ContentGenerator/Instructor/UserList.pm:1220 -msgid "Add which new users?" -msgstr "Hangi yeni kullanıcılar eklenecek?" - -# -#: ContentGenerator/Problem.pm:311 -msgid "All of the answers above are correct." -msgstr "Yanıtların tümü doÄŸru" - -# -#. ($verb) -#: ContentGenerator/Instructor/ProblemSetList.pm:1052 -msgid "All selected sets %1 all students" -msgstr "Bütün seçili setler her e %1" - -# -#. ($verb) -#: ContentGenerator/Instructor/ProblemSetList.pm:1046 -msgid "All sets %1 all students" -msgstr "Bütün setler %1 all students" - -# -#. ($verb) -#: ContentGenerator/Instructor/ProblemSetList.pm:1049 -msgid "All visible sets %1 all students" -msgstr "Görünür bütün setler her öğrenciye %1" - -# -#: ContentGenerator/Instructor/ProblemSetList.pm:446 -#: ContentGenerator/Instructor/ProblemSetList.pm:832 -#: ContentGenerator/Instructor/ProblemSetList.pm:870 -msgid "Answer Date" -msgstr "Cevap tarihi" - -# -#: ContentGenerator/Problem.pm:242 -msgid "Answer Preview" -msgstr "Gösterim" - -# -#: ContentGenerator/Instructor/ProblemSetList.pm:531 -msgid "Any changes made below will be reflected in the set for ALL students." -msgstr "AÅŸağıda yapılan deÄŸiÅŸiklikler seti çözen HER öğrenci için uygulanacakç" - -# -#: ContentGenerator.pm:1481 -msgid "Apply Options" -msgstr "Seçenekleri Uygula" - -# -#: ContentGenerator/Instructor/ProblemSetList.pm:1545 -msgid "Assign this set to which users?" -msgstr "Bu set hangi kullanıcılara ödev verilecek?" - -# -#: ContentGenerator/Instructor/UserList.pm:2064 -msgid "Assigned Sets" -msgstr "Ödev verilmiÅŸ setler" - -# -#. ($fully) -#: ContentGenerator/Problem.pm:315 -#, fuzzy -msgid "At least one of the answers above is NOT %1correct." -msgstr "Yanıtların en az bir tanesi %1doÄŸru deÄŸil." - -# -#: ContentGenerator/ProblemSet.pm:350 -msgid "Attempts" -msgstr "Deneme sayısı" - -# -#. ($eUserID,$@) -#: ContentGenerator/Options.pm:71 -msgid "Can't get password record for effective user '%1': %2" -msgstr "Mevcut kullanıcı için ÅŸifre bilgileri alınamadı '%1': %2" - -# -#. ($userID,$@) -#: ContentGenerator/Options.pm:68 -msgid "Can't get password record for user '%1': %2" -msgstr "kullanıcı için ÅŸifre bilgileri alınamadı '%1': %2" - -# -#: ContentGenerator/Options.pm:142 -msgid "Change Email Address" -msgstr "E-posta Adresi DeÄŸiÅŸtir" - -# -#: ContentGenerator/Options.pm:61 -msgid "Change Password" -msgstr "Åžifre DeÄŸiÅŸtir" - -# -#: ContentGenerator/Instructor/UserList.pm:1409 -#: ContentGenerator/Instructor/UserList.pm:1481 -msgid "Changes abandoned" -msgstr "DeÄŸiÅŸiklikler kaydedilmedi" - -# -#: ContentGenerator/Instructor/UserList.pm:1457 -msgid "Changes saved" -msgstr "DeÄŸiÅŸiklikler kaydedildi" - -# -#: ContentGenerator/Problem.pm:1118 -msgid "Check Answers" -msgstr "Yanıtları kontrol et" - -# -#: ContentGenerator/Problem.pm:1227 -msgid "Checking additional error messages" -msgstr "DiÄŸer hata mesajları kontrol ediliyor" - -# -#: ContentGenerator/Instructor/ProblemSetList.pm:1011 -msgid "Choose visibility of the sets to be affected" -msgstr "Etkilenecek setlerin görünürlüğünü seçin" - -# -#: ContentGenerator/Instructor/ProblemSetList.pm:1084 -#: ContentGenerator/Instructor/ProblemSetList.pm:982 -msgid "Choose which sets to be affected" -msgstr "Hangi setlerin etkileneceÄŸini seçin" - -# -#: ContentGenerator/Instructor/ProblemSetList.pm:597 -#: ContentGenerator/Instructor/UserList.pm:550 -#: ContentGenerator/ProblemSets.pm:288 -msgid "Clear" -msgstr "Temizle" - -# -#: ContentGenerator/Instructor/UserList.pm:562 -msgid "Click on the login name to edit individual problem set data, (e.g. due dates) for these students." -msgstr "Bu öğrenciler için problem setlerinde deÄŸiÅŸiklik (teslim tarihi vb.) yapmak için isimlerinin üzerine tıklayın." - -# -#: ContentGenerator/ProblemSets.pm:435 -msgid "Closed" -msgstr "Kapalı" - -# -#: ContentGenerator/Instructor/UserList.pm:2072 -#: ContentGenerator/Instructor/UserList.pm:290 -#: ContentGenerator/Instructor/UserList.pm:818 -#: ContentGenerator/Instructor/UserList.pm:861 -#: ContentGenerator/Instructor/UserList.pm:904 -msgid "Comment" -msgstr "Yorum" - -# -#. ($e_user_name) -#: ContentGenerator/Options.pm:134 -#: ContentGenerator/Options.pm:95 -#, fuzzy -msgid "Confirm %1's New Password" -msgstr "%1 için Åžifre Onayı" - -# -#: ContentGenerator/Login.pm:206 -msgid "Continue" -msgstr "Devam" - -# -#: ContentGenerator/Problem.pm:243 -msgid "Correct" -msgstr "DoÄŸru yanıt" - -# -#. ($e_user_name,$@) -#: ContentGenerator/Options.pm:80 -#, fuzzy -msgid "Couldn't change %1's password: %2" -msgstr "%1 adlı kullanıcının ÅŸifresi deÄŸiÅŸtirilemedi: %2" - -# -#. ($@) -#: ContentGenerator/Options.pm:153 -#, fuzzy -msgid "Couldn't change your email address: %1" -msgstr "E-posta adresiniz deÄŸiÅŸtirilemedi: %1" - -# -#: ContentGenerator/ProblemSets.pm:71 -#: ContentGenerator/ProblemSets.pm:73 -msgid "Course Info" -msgstr "Ders bilgileri" - -# -#: ContentGenerator.pm:657 -#: ContentGenerator/Home.pm:94 -msgid "Courses" -msgstr "Dersler" - -# -#: ContentGenerator/Instructor/ProblemSetList.pm:1351 -msgid "Create as what type of set?" -msgstr "Ne tip set oluÅŸturulacak?" - -# -#: ContentGenerator/Instructor/ProblemSetList.pm:1263 -#: ContentGenerator/Instructor/UserList.pm:1082 -msgid "Delete how many?" -msgstr "Kaç tane silinecek?" - -# -#: ContentGenerator/Instructor/ProblemSetList.pm:1119 -msgid "Disable" -msgstr "Devre diÅŸi bırak" - -# -#: ContentGenerator.pm:1442 -msgid "Display Options" -msgstr "Görünüm Seçenekleri" - -# -#. ($pl) -#: ContentGenerator/ProblemSets.pm:283 -msgid "Download Hardcopy for Selected %plural(%1,Set,Sets)" -msgstr "Seçili Setleri Yazdırmak için dosya indir: [plural,_1,Set,Sets]" - -# -#: ContentGenerator/ProblemSet.pm:321 -msgid "Download PDF or TeX Hardcopy for Current Set" -msgstr "Mevcut seti Yazdırmak için dosya indir" - -# -#: ContentGenerator/ProblemSets.pm:289 -msgid "Download PDF or TeX Hardcopy for Selected Sets" -msgstr "Seçili Setleri Yazdırmak için dosya indir" - -# -#: ContentGenerator/Instructor/ProblemSetList.pm:445 -#: ContentGenerator/Instructor/ProblemSetList.pm:831 -#: ContentGenerator/Instructor/ProblemSetList.pm:869 -msgid "Due Date" -msgstr "Teslim Tarihi" - -# -#: ContentGenerator/Instructor/ProblemSetList.pm:1812 -msgid "Duplicate this set and name it" -msgstr "Bu seti çoÄŸalt ve isimlendir" - -# -#: ContentGenerator/Instructor/UserList.pm:1720 -#: ContentGenerator/Instructor/UserList.pm:1785 -#: ContentGenerator/Instructor/UserList.pm:1818 -msgid "Edit" -msgstr "Düzenle" - -# -#: ContentGenerator/Instructor/ProblemSetList.pm:603 -msgid "Edit All Set Data" -msgstr "Bütün set bilgilerini düzenle" - -# -#: ContentGenerator/Instructor/ProblemSetList.pm:439 -msgid "Edit Assigned Users" -msgstr "Ödev verilmiÅŸ kullanıcıları düzenle" - -# -#: ContentGenerator/Instructor/ProblemSetList.pm:438 -msgid "Edit Problems" -msgstr "Soruları Düzenle" - -# -#: ContentGenerator/Instructor/ProblemSetList.pm:441 -msgid "Edit Set Data" -msgstr "Set Bilgilerini Düzenle" - -# -#: ContentGenerator/Instructor/UserList.pm:961 -msgid "Edit Which Users?" -msgstr "Hangi Kullanıcılar Düzenlenecek?" - -# -#: ContentGenerator/Problem.pm:1011 -msgid "Edit this problem" -msgstr "Bu soruyu düzenle" - -# -#: ContentGenerator/Instructor/ProblemSetList.pm:923 -msgid "Edit which sets?" -msgstr "Hangi setler düzenlecek?" - -# -#: ContentGenerator/Instructor/UserList.pm:2067 -#: ContentGenerator/Instructor/UserList.pm:285 -#: ContentGenerator/Instructor/UserList.pm:813 -#: ContentGenerator/Instructor/UserList.pm:856 -#: ContentGenerator/Instructor/UserList.pm:899 -msgid "Email Address" -msgstr "E-posta Adresi" - -# -#: ContentGenerator.pm:1526 -#: ContentGenerator.pm:1549 -#: ContentGenerator.pm:1572 -msgid "Email instructor" -msgstr "EÄŸitmene E-posta" - -# -#: ContentGenerator/Instructor/ProblemSetList.pm:1120 -msgid "Enable" -msgstr "EtkinleÅŸtir" - -# -#: ContentGenerator/Instructor/ProblemSetList.pm:604 -msgid "Enable Reduced Credit" -msgstr "AzaltılmiÅŸ Notu EtkinleÅŸtir" - -# -#: ContentGenerator/Instructor/ProblemSetList.pm:1113 -msgid "Enable/Disable reduced scoring for selected sets" -msgstr "Seçili setler için azaltılmış not seçenegini etkinleÅŸtir/kaldır" - -# -#: ContentGenerator/Instructor/UserList.pm:815 -#: ContentGenerator/Instructor/UserList.pm:858 -#: ContentGenerator/Instructor/UserList.pm:901 -msgid "Enrollment Status" -msgstr "Kayıt Durumu" - -# -#: ContentGenerator/Instructor/UserList.pm:1321 -msgid "Enter filename below" -msgstr "AÅŸağıya dosya adı girin" - -# -#: ContentGenerator/Instructor/ProblemSetList.pm:1502 -msgid "Enter filenames below" -msgstr "AÅŸağıya dosya isimlerini girin" - -# -#: ContentGenerator/Problem.pm:240 -msgid "Entered" -msgstr "Yanıt" - -# -#: ContentGenerator/Instructor/ProblemSetList.pm:1671 -msgid "Export selected sets" -msgstr "Åžeçili setleri dışarı aktar" - -# -#: ContentGenerator/Instructor/UserList.pm:1317 -msgid "Export to what kind of file?" -msgstr "Ne çeÅŸit dosyaya aktarılacak?" - -# -#: ContentGenerator/Instructor/ProblemSetList.pm:1604 -msgid "Export which sets?" -msgstr "Hangı setler dışarı aktarılacak?" - -# -#: ContentGenerator/Instructor/UserList.pm:1292 -msgid "Export which users?" -msgstr "Hangi kullanıcılar dışarı aktarılacak?" - -# -#. ($@) -#: ContentGenerator/Instructor/ProblemSetList.pm:1435 -msgid "Failed to create new set: %1" -msgstr "Yeni set yaratılamadı: %1" - -# -#: ContentGenerator/Instructor/ProblemSetList.pm:1374 -msgid "Failed to create new set: no set name specified!" -msgstr "Yeni set oluÅŸturulamadı: set adı belirtilmemiÅŸ!" - -# -#. ($@) -#: ContentGenerator/Instructor/ProblemSetList.pm:1848 -msgid "Failed to duplicate set: %1" -msgstr "Set çoÄŸaltılamadı: %1" - -# -#: ContentGenerator/Instructor/ProblemSetList.pm:1832 -msgid "Failed to duplicate set: no set name specified!" -msgstr "Set çoÄŸaltılamadı: set adı belirtilmemiÅŸ!" - -# -#: ContentGenerator/Instructor/ProblemSetList.pm:1396 -#: ContentGenerator/Instructor/ProblemSetList.pm:1830 -msgid "Failed to duplicate set: no set selected for duplication!" -msgstr "Set çoÄŸaltılamadı: herhangi bir set seçili deÄŸil!" - -# -#. ($newSetID) -#: ContentGenerator/Instructor/ProblemSetList.pm:1834 -msgid "Failed to duplicate set: set %1 already exists!" -msgstr "Set çoÄŸaltılamadı: set %1 zaten mevcut!" - -# -#: ContentGenerator/Instructor/UserList.pm:1339 -msgid "Filename" -msgstr "Dosya adı" - -# -#: ContentGenerator/Instructor/UserList.pm:678 -msgid "Filter by what text?" -msgstr "Hangi metin ile filtrelenecek?" - -# -#: ContentGenerator/Instructor/UserList.pm:2065 -#: ContentGenerator/Instructor/UserList.pm:283 -#: ContentGenerator/Instructor/UserList.pm:811 -#: ContentGenerator/Instructor/UserList.pm:854 -#: ContentGenerator/Instructor/UserList.pm:897 -msgid "First Name" -msgstr "Ä°sim" - -# -#: ContentGenerator/Instructor/UserList.pm:1020 -msgid "Give new password to which users?" -msgstr "Hangi kullanıcılara yeni ÅŸifre verilecek?" - -# -#: ContentGenerator/Login.pm:231 -msgid "Guest Login" -msgstr "Misafir Kullanıcı GiriÅŸi" - -# -#: ContentGenerator/Instructor/ProblemSetList.pm:443 -#: ContentGenerator/Instructor/ProblemSetList.pm:829 -#: ContentGenerator/Instructor/ProblemSetList.pm:867 -msgid "Hardcopy Header" -msgstr "Çıktı Dosyası BaÅŸlığı" - -# -#: ContentGenerator/Instructor/ProblemSetList.pm:1017 -msgid "Hidden" -msgstr "Gizli" - -# -#: ContentGenerator.pm:667 -#: ContentGenerator/ProblemSet.pm:110 -#: ContentGenerator/ProblemSets.pm:226 -msgid "Homework Sets" -msgstr "Soru Grupları" - -# -#: ContentGenerator/Instructor/UserList.pm:558 -msgid "If a password field is left blank, the student's current password will be maintained." -msgstr "Åžifre alanı boÅŸ bırakılırsa kullanıcının mevcut ÅŸifresi kullanılır." - -# -#: ContentGenerator/Instructor/ProblemSetList.pm:1498 -msgid "Import from where?" -msgstr "Nereden aktarılacak?" - -# -#: ContentGenerator/Instructor/ProblemSetList.pm:1473 -msgid "Import how many sets?" -msgstr "Kaç set aktarılacak?" - -# -#: ContentGenerator/Instructor/ProblemSetList.pm:1520 -msgid "Import sets with names" -msgstr "Setleri isimleriyle beraber aktar" - -# -#: ContentGenerator/Instructor/UserList.pm:1165 -msgid "Import users from what file?" -msgstr "Kullanıcılar hangi dosyadan aktarılacak?" - -# -#: ContentGenerator/Instructor/UserList.pm:1927 -msgid "Inactive" -msgstr "Aktif DeÄŸil" - -# -#: Authen.pm:148 -msgid "Invalid user ID or password." -msgstr "Yanlış kullanıcı adı ya da ÅŸifre." - -# -#: ContentGenerator/Instructor/UserList.pm:2066 -#: ContentGenerator/Instructor/UserList.pm:284 -#: ContentGenerator/Instructor/UserList.pm:812 -#: ContentGenerator/Instructor/UserList.pm:855 -#: ContentGenerator/Instructor/UserList.pm:898 -msgid "Last Name" -msgstr "Soyadı" - -# -#: ContentGenerator.pm:730 -msgid "Library Browser" -msgstr "Soru Kütüphaneleri" - -# -#: ContentGenerator.pm:731 -msgid "Library Browser 2" -msgstr "Soru Kütüphane Tarayıcısı 2" - -# -#: ContentGenerator/Logout.pm:124 -msgid "Log In Again" -msgstr "Tekrar giriÅŸ yap" - -# -#: ContentGenerator.pm:845 -#: ContentGenerator.pm:847 -msgid "Log Out" -msgstr "Çıkış Yap" - -# -#. ($userID) -#: ContentGenerator.pm:845 -#: ContentGenerator.pm:847 -#, fuzzy -msgid "Logged in as %1. " -msgstr "%1 olarak giriÅŸ yaptınız." - -# -#: ContentGenerator/Login.pm:74 -#: ContentGenerator/Login.pm:77 -msgid "Login Info" -msgstr "GiriÅŸ Bilgileri" - -# -#: ContentGenerator/Instructor/UserList.pm:2062 -#: ContentGenerator/Instructor/UserList.pm:282 -#: ContentGenerator/Instructor/UserList.pm:810 -#: ContentGenerator/Instructor/UserList.pm:853 -#: ContentGenerator/Instructor/UserList.pm:896 -msgid "Login Name" -msgstr "Kullanıcı adı" - -# -#: ContentGenerator/Instructor/UserList.pm:2063 -#, fuzzy -msgid "Login Status" -msgstr "Durumu" - -# -#: ContentGenerator.pm:654 -msgid "Main Menu" -msgstr "Ana Menü" - -# -#: ContentGenerator/Instructor/ProblemSetList.pm:705 -msgid "Match on what? (separate multiple IDs with commas)" -msgstr "Hangi özellikler eÅŸleÅŸtirilecek? (birden fazla kullanıcı adını virgülle ayırın)" - -# -#: ContentGenerator/Problem.pm:245 -msgid "Messages" -msgstr "Mesajlar" - -# -#: ContentGenerator/ProblemSet.pm:349 -#: ContentGenerator/ProblemSets.pm:212 -#: ContentGenerator/ProblemSets.pm:213 -msgid "Name" -msgstr "Grup Adı" - -# -#: ContentGenerator/Instructor/ProblemSetList.pm:1328 -msgid "Name the new set" -msgstr "Yeni seti adlandırın" - -# -#: ContentGenerator/Instructor/UserList.pm:1937 -#: ContentGenerator/Instructor/UserList.pm:2077 -msgid "New Password" -msgstr "Yeni Åžifre" - -# -#: ContentGenerator/Instructor/UserList.pm:1519 -msgid "New passwords saved" -msgstr "Yeni ÅŸifreler kaydedildi" - -# -#: ContentGenerator/Problem.pm:864 -#: ContentGenerator/Problem.pm:866 -msgid "Next Problem" -msgstr "Sonraki Soru" - -# -#: ContentGenerator/Instructor/ProblemSetList.pm:2625 -#: ContentGenerator/Instructor/ProblemSetList.pm:2626 -msgid "No" -msgstr "Hayır" - -# -#: ContentGenerator/Instructor/ProblemSetList.pm:1043 -#: ContentGenerator/Instructor/ProblemSetList.pm:1145 -msgid "No change made to any set" -msgstr "Herhangi bir sete deÄŸiÅŸiklik yapılmadı" - -# -#: ContentGenerator/Instructor/ProblemSetList.pm:1217 -msgid "No sets selected for scoring" -msgstr "Notlamak için bir set seçili deÄŸil" - -# -#: ContentGenerator/Instructor/ProblemSetList.pm:266 -msgid "No sets selected for scoring." -msgstr "Notlama için bir set seçili deÄŸil." - -# -#: ContentGenerator/Instructor/ProblemSetList.pm:2722 -msgid "No sets shown. Choose one of the options above to list the sets in the course." -msgstr "Hiç set gösterilmiyor. Dersteki setleri görmek için yukarıdaki seçeneklerden birini iÅŸaretleyin." - -# -#: ContentGenerator/Instructor/UserList.pm:2110 -msgid "" -"No students shown. Choose one of the options above to \n" -"\t list the students in the course." -msgstr "Hiç öğrenci gösterilmiyor. Dersteki öğrencileri görmek için yukarıdaki seçeneklerden birisini seçin." - -# -#: ContentGenerator.pm:851 -msgid "Not logged in." -msgstr "GiriÅŸ yapmadınız." - -# -#: ContentGenerator/Problem.pm:975 -msgid "Note" -msgstr "Not" - -# -#: ContentGenerator/Instructor/ProblemSetList.pm:444 -#: ContentGenerator/Instructor/ProblemSetList.pm:830 -#: ContentGenerator/Instructor/ProblemSetList.pm:868 -msgid "Open Date" -msgstr "Açılış tarihi" - -# -#: ContentGenerator/Problem.pm:1307 -msgid "PREVIEW ONLY -- ANSWERS NOT RECORDED" -msgstr "GÖSTERÄ°M -- YANITLAR KAYDEDÄ°LMEDÄ° " - -# -#. (timestamp($self) -#: ContentGenerator.pm:953 -msgid "Page generated at %1" -msgstr "Sayfa %1de yaratıldı" - -# -#: ContentGenerator/Login.pm:202 -msgid "Password" -msgstr "Sifre" - -# -#: ContentGenerator/Instructor/UserList.pm:2073 -#: ContentGenerator/Instructor/UserList.pm:291 -#: ContentGenerator/Instructor/UserList.pm:819 -#: ContentGenerator/Instructor/UserList.pm:862 -#: ContentGenerator/Instructor/UserList.pm:905 -msgid "Permission Level" -msgstr "Yetki seviyesi" - -# -#. (CGI::b($r->maketext($course) -#: ContentGenerator/Login.pm:154 -#, fuzzy -msgid "Please enter your username and password for %1 below:" -msgstr "Lütfen %1 dersi için kullanici adi ve sifrenizi giriniz:" - -# -#: ContentGenerator/Instructor/ProblemSetList.pm:396 -msgid "Please select action to be performed." -msgstr "Lütfen gerçekleÅŸtirilecek iÅŸlemi seçin." - -# -#: ContentGenerator/Problem.pm:1116 -msgid "Preview Answers" -msgstr "Yanıtları görüntüle" - -# -#: ContentGenerator/Problem.pm:850 -#: ContentGenerator/Problem.pm:852 -msgid "Previous Problem" -msgstr "Önceki Soru" - -# -#. ($problemID) -#: ContentGenerator.pm:686 -#: ContentGenerator/Problem.pm:808 -#: ContentGenerator/ProblemSet.pm:424 -msgid "Problem %1" -msgstr "Soru %1" - -# -#: ContentGenerator/Problem.pm:856 -#: ContentGenerator/Problem.pm:858 -msgid "Problem List" -msgstr "Soru Listesi" - -# -#: ContentGenerator/Problem.pm:796 -#: ContentGenerator/ProblemSet.pm:346 -msgid "Problems" -msgstr "Sorular" - -# -#: ContentGenerator/Instructor/UserList.pm:2071 -#: ContentGenerator/Instructor/UserList.pm:289 -#: ContentGenerator/Instructor/UserList.pm:817 -#: ContentGenerator/Instructor/UserList.pm:860 -#: ContentGenerator/Instructor/UserList.pm:903 -msgid "Recitation" -msgstr "Problem cozum dersi" - -# -#. ($verb) -#: ContentGenerator/Instructor/ProblemSetList.pm:1148 -msgid "Reduced Credit %1 for all sets" -msgstr "Bütün setler için notlar %1 azaltıldı" - -# -#. ($verb) -#: ContentGenerator/Instructor/ProblemSetList.pm:1154 -msgid "Reduced Credit %1 for selected sets" -msgstr "Seçili setler için notlar %1 azaltıldı" - -# -#. ($verb) -#: ContentGenerator/Instructor/ProblemSetList.pm:1151 -msgid "Reduced Credit %1 for visable sets" -msgstr "Görünür setler için notlar %1 azaltıldı" - -# -#: ContentGenerator/Instructor/ProblemSetList.pm:2523 -msgid "Reduced Credit Disabled" -msgstr "Azaltılmış not etkin deÄŸil" - -# -#: ContentGenerator/Instructor/ProblemSetList.pm:2523 -#: ContentGenerator/Instructor/ProblemSetList.pm:448 -msgid "Reduced Credit Enabled" -msgstr "Azaltılmış not etkin" - -# -#. ($beginReducedScoringPeriod) -#: ContentGenerator/ProblemSets.pm:459 -msgid "Reduced Credit Starts: %1" -msgstr "AzaltılmiÅŸ not baÅŸlangıç tarihi: %1" - -# -#: ContentGenerator/ProblemSet.pm:351 -msgid "Remaining" -msgstr "Kalan hak" - -# -#: ContentGenerator/Login.pm:204 -msgid "Remember Me" -msgstr "Beni hatırla" - -# -#: ContentGenerator/Instructor/UserList.pm:1190 -msgid "Replace which users?" -msgstr "Hangi kullanıcılar deÄŸiÅŸtirilecek?" - -# -#: ContentGenerator.pm:799 -msgid "Report bugs" -msgstr "Hataları bildir" - -# -#: ContentGenerator/Problem.pm:244 -msgid "Result" -msgstr "Sonuç" - -# -#. (CGI::i($self->$actionHandler(\%genericParams, \%actionParams, \%tableParams) -#: ContentGenerator/Instructor/UserList.pm:376 -msgid "Result of last action performed: %1" -msgstr "En son gerçekleÅŸtilen iÅŸlemin sonucu: %1" - -# -#: ContentGenerator/Instructor/ProblemSetList.pm:386 -msgid "Results of last action performed" -msgstr "En son gerçekleÅŸtirilen iÅŸlemin sonuçları" - -# -#: ContentGenerator/Instructor/ProblemSetList.pm:1732 -#: ContentGenerator/Instructor/UserList.pm:1415 -#: ContentGenerator/Instructor/UserList.pm:1487 -msgid "Save changes" -msgstr "DeÄŸiÅŸiklikleri kaydet" - -# -#: ContentGenerator/Instructor/ProblemSetList.pm:1186 -msgid "Score which sets?" -msgstr "Hangi setler notlanacak?" - -# -#: ContentGenerator/Instructor/UserList.pm:2070 -#: ContentGenerator/Instructor/UserList.pm:288 -#: ContentGenerator/Instructor/UserList.pm:816 -#: ContentGenerator/Instructor/UserList.pm:859 -#: ContentGenerator/Instructor/UserList.pm:902 -msgid "Section" -msgstr "Bölüm" - -# -#: ContentGenerator/Instructor/ProblemSetList.pm:582 -msgid "Select all sets" -msgstr "Bütün setleri seç" - -# -#: ContentGenerator/Instructor/UserList.pm:535 -msgid "Select all users" -msgstr "Bütün kullanıcıları seç" - -# -#: ContentGenerator/Instructor/ProblemSetList.pm:534 -#: ContentGenerator/Instructor/UserList.pm:512 -msgid "Select an action to perform" -msgstr "Yapılacak iÅŸlemi seç" - -# -#. ($newSetID) -#: ContentGenerator/Instructor/ProblemSetList.pm:1375 -msgid "Set %1 exists. No set created" -msgstr "Set %1 mevcut. Yeni set oluÅŸturulmadı" - -#: ContentGenerator/Instructor/ProblemSetList.pm:440 -msgid "Set Definition Filename" -msgstr "Set Tanım Dosyası Adı" - -# -#: ContentGenerator/Instructor/ProblemSetList.pm:442 -#: ContentGenerator/Instructor/ProblemSetList.pm:828 -#: ContentGenerator/Instructor/ProblemSetList.pm:866 -msgid "Set Header" -msgstr "Set BaÅŸlığı" - -# -#: ContentGenerator/ProblemSet.pm:273 -#: ContentGenerator/ProblemSet.pm:275 -msgid "Set Info" -msgstr "Set Bilgileri" - -# -#: ContentGenerator/Instructor/ProblemSetList.pm:2700 -msgid "Set List" -msgstr "Set Listesi" - -# -#: ContentGenerator/Instructor/ProblemSetList.pm:827 -#: ContentGenerator/Instructor/ProblemSetList.pm:865 -msgid "Set Name" -msgstr "Set Adı" - -# -#: ContentGenerator/ProblemSet.pm:153 -msgid "Sets" -msgstr "Setler" - -# -#: ContentGenerator/Problem.pm:1063 -msgid "Show Hints" -msgstr "Ä°puçlarını göster" - -# -#: ContentGenerator/Problem.pm:1370 -msgid "Show Past Answers" -msgstr "Önceki Yanıtları Göster" - -# -#: ContentGenerator/Problem.pm:1082 -msgid "Show Solutions" -msgstr "Çözümleri göster" - -# -#: ContentGenerator/Instructor/UserList.pm:645 -msgid "Show Which Users?" -msgstr "Hangi kullanıcılar gösterilecek?" - -# -#: ContentGenerator/Problem.pm:1044 -msgid "Show correct answers" -msgstr "DoÄŸru yanıtları göster" - -# -#: ContentGenerator/Instructor/ProblemSetList.pm:683 -msgid "Show which sets?" -msgstr "Hangi setler gösterilecek?" - -# -#: ContentGenerator/Instructor/ProblemSetList.pm:497 -#: ContentGenerator/Instructor/UserList.pm:475 -msgid "Show/Hide Site Description" -msgstr "Web Alanı Açıklamasını Göster/Gizle" - -# -#. (scalar @visibleSetIDs, scalar @allSetIDs) -#: ContentGenerator/Instructor/ProblemSetList.pm:607 -msgid "Showing %1 out of %2 sets." -msgstr "%2 setin %1 tanesi gösteriliyor." - -# -#. (scalar @Users, scalar @allUserIDs) -#: ContentGenerator/Instructor/UserList.pm:556 -msgid "Showing %1 out of %2 users" -msgstr "%2 kullanıcının %1 tanesi gösteriliyor." - -# -#: ContentGenerator/Login.pm:96 -#: ContentGenerator/Login.pm:99 -msgid "Site Information" -msgstr "Web Alanı Bilgileri" - -# -#: ContentGenerator/Instructor/ProblemSetList.pm:821 -#: ContentGenerator/Instructor/UserList.pm:804 -msgid "Sort by" -msgstr "Kriterlere Göre Sırala" - -# -#. ($names{$primary}, $names{$secondary}) -#: ContentGenerator/Instructor/ProblemSetList.pm:899 -msgid "Sort by %1 and then by %2" -msgstr "Önce %1 sonra %2 kriteriyle sırala" - -# -#: ContentGenerator/Instructor/UserList.pm:2069 -#: ContentGenerator/Instructor/UserList.pm:287 -#: ContentGenerator/ProblemSet.pm:353 -#: ContentGenerator/ProblemSets.pm:215 -#: ContentGenerator/ProblemSets.pm:216 -msgid "Status" -msgstr "Durumu" - -# -#: ContentGenerator.pm:848 -msgid "Stop Acting" -msgstr "Rol Yapmayı bırak" - -# -#: ContentGenerator/Instructor/UserList.pm:2068 -#: ContentGenerator/Instructor/UserList.pm:286 -#: ContentGenerator/Instructor/UserList.pm:814 -#: ContentGenerator/Instructor/UserList.pm:857 -#: ContentGenerator/Instructor/UserList.pm:900 -msgid "Student ID" -msgstr "Kullanıcı Adı" - -# -#: ContentGenerator/Problem.pm:1127 -msgid "Submit Answers" -msgstr "Yanıtları Gönder" - -# -#. ($effectiveUser) -#: ContentGenerator/Problem.pm:1124 -#, fuzzy -msgid "Submit Answers for %1" -msgstr "%1 için Yanıtları Gönder" - -# -#: ContentGenerator/Instructor/ProblemSetList.pm:1850 -msgid "Success" -msgstr "BaÅŸarılı" - -#. ($newSetID) -#: ContentGenerator/Instructor/ProblemSetList.pm:1437 -msgid "Successfully created new set %1" -msgstr "Yeni set %1 baÅŸarıyla yaratıldı" - -# -#. ($name) -#: ContentGenerator/ProblemSets.pm:424 -#: ContentGenerator/ProblemSets.pm:433 -#: ContentGenerator/ProblemSets.pm:439 -msgid "Take %1 test" -msgstr "%1 testini çöz" - -# -#: ContentGenerator/Instructor/ProblemSetList.pm:598 -#: ContentGenerator/Instructor/UserList.pm:551 -msgid "Take Action!" -msgstr "Ä°ÅŸlemi GerçekleÅŸtir!" - -# -#: ContentGenerator/ProblemSets.pm:236 -msgid "Test Date" -msgstr "Sınav Tarihi" - -# -#: ContentGenerator/ProblemSets.pm:235 -msgid "Test Score" -msgstr "Sınav Notu" - -# -#: ContentGenerator.pm:954 -msgid "The WeBWorK Project" -msgstr "WeBWorK Projesi" - -# -#. ($fully) -#: ContentGenerator/Problem.pm:307 -#, fuzzy -msgid "The answer above is NOT %1correct." -msgstr "Yukarıdaki yanıt %1doÄŸru deÄŸil." - -# -#: ContentGenerator/Problem.pm:305 -msgid "The answer above is correct." -msgstr "Yukarıdaki yanıt doÄŸru." - -# -#. (CGI::b($r->maketext("[_1]'s Current Password",$user_name) -#: ContentGenerator/Options.pm:107 -#, fuzzy -msgid "" -"The password you entered in the %1 \n" -"\t\t\t\t\t\tfield does not match your current\n" -"\t\t\t\t\t\tpassword. Please retype your current password and try\n" -"\t\t\t\t\t\tagain." -msgstr "%1 alanına girdiÄŸiniz ÅŸifre ile mevcut ÅŸifreniz uyuÅŸmamaktadır. Lütfen ÅŸirfenizi girerek tekrar deneyiniz." - -# -#. (CGI::b($r->maketext("[_1]'s New Password",$e_user_name) -#: ContentGenerator/Options.pm:91 -#, fuzzy -msgid "" -"The passwords you entered in the \n" -"\t\t\t\t\t\t\t\t%1 and %2\n" -"\t\t\t\t\t\t\t\tfields don't match. Please retype your new password and try\n" -"\t\t\t\t\t\t\t\tagain." -msgstr "%1 ve %2 alanlarına girdiÄŸiniz ÅŸifreler uyuÅŸmadı. Yeni ÅŸifrenizi girerek tekrar deneyiniz." - -# -#. ($setName,$effectiveUser) -#: ContentGenerator/ProblemSet.pm:308 -msgid "The selected problem set (%1) is not a valid set for %2" -msgstr "SeçtiÄŸiniz soru seti (%1) set %2 için geçerli deÄŸil" - -# -#: ContentGenerator/Instructor/ProblemSetList.pm:859 -#: ContentGenerator/Instructor/UserList.pm:847 -#: ContentGenerator/Instructor/UserList.pm:890 -msgid "Then by" -msgstr "sonra" - -# -#: ContentGenerator/ProblemSet.pm:365 -msgid "This homework set contains no problems." -msgstr "Bu soru grubunda soru bulunmamaktadır." - -# -#: ContentGenerator/Problem.pm:1175 -msgid "This homework set is closed." -msgstr "Bu soru grubu kapalı." - -# -#: ContentGenerator/Problem.pm:1173 -msgid "This homework set is not yet open." -msgstr "Bu soru grubu henüz açık deÄŸil." - -# -#: ContentGenerator/Problem.pm:611 -msgid "This problem will not count towards your grade." -msgstr "Bu soru, notunuzu etkilemeyecektir." - -# -#. (CGI::font({class=>$visiblityStateClass}, $visiblityStateText) -#: ContentGenerator/Problem.pm:605 -#: ContentGenerator/ProblemSet.pm:88 -msgid "This set is %1" -msgstr "Bu set %1" - -# -#: ContentGenerator/Instructor/ProblemSetList.pm:588 -msgid "Unselect all sets" -msgstr "Bütün setlerdeki seçimi kaldır" - -# -#: ContentGenerator/Instructor/UserList.pm:541 -msgid "Unselect all users" -msgstr "Bütün kullanıcılardaki seçimi kaldir" - -# -#: ContentGenerator/Instructor/ProblemSetList.pm:2683 -msgid "Use System Default" -msgstr "Varsayılan Ayarları Kullan" - -# -#: ContentGenerator/Login.pm:200 -msgid "Username" -msgstr "Kullanici adi" - -# -#: ContentGenerator/Instructor/UserList.pm:2087 -msgid "Users List" -msgstr "Kullanıcı Listesi" - -# -#. ($names{$primary}, $names{$secondary}, $names{$ternary}) -#: ContentGenerator/Instructor/UserList.pm:938 -msgid "Users sorted by %1, then by %2, then by %3" -msgstr "Kullanıcılar sırasıyla %1, %2 ve %3 kriterlerine göre sıları" - -# -#: ContentGenerator/ProblemSet.pm:230 -msgid "Viewing temporary file" -msgstr "Geçici dosya gösteriliyor" - -# -#: ContentGenerator/Problem.pm:1332 -#: ContentGenerator/ProblemSets.pm:63 -msgid "Viewing temporary file: " -msgstr "Geçici dosya görüntüleniyor: " - -# -#: ContentGenerator/Instructor/ProblemSetList.pm:833 -#: ContentGenerator/Instructor/ProblemSetList.pm:871 -msgid "Visibility" -msgstr "Görünürlük" - -# -#: ContentGenerator/Instructor/ProblemSetList.pm:1018 -#: ContentGenerator/Instructor/ProblemSetList.pm:447 -msgid "Visible" -msgstr "Görünür" - -# -#: ContentGenerator/Instructor/ProblemSetList.pm:1259 -#: ContentGenerator/Instructor/UserList.pm:1078 -msgid "Warning: Deletion destroys all user-related data and is not undoable!" -msgstr "Dikkat: Silme iÅŸlemi kullanıcının bütün bilgilerini yok eder ve bu iÅŸlem geri döndürelemez!" - -# -#: ContentGenerator/Home.pm:87 -msgid "Welcome to WeBWorK!" -msgstr "WeBWorK sitemize hoÅŸgeldiniz!" - -# -#: ContentGenerator/Instructor/UserList.pm:665 -msgid "What field should filtered users match on?" -msgstr "Seçili kullanıcılar hangi kriterlerde eÅŸleÅŸmeli?" - -# -#: ContentGenerator/ProblemSet.pm:352 -msgid "Worth" -msgstr "DeÄŸeri" - -# -#: ContentGenerator/Instructor/ProblemSetList.pm:2625 -#: ContentGenerator/Instructor/ProblemSetList.pm:2626 -msgid "Yes" -msgstr "Evet" - -# -#: ContentGenerator/Instructor/ProblemSetList.pm:306 -#: ContentGenerator/Instructor/ProblemSetList.pm:418 -#: ContentGenerator/Instructor/UserList.pm:262 -msgid "You are not authorized to access the instructor tools." -msgstr "EÄŸitmen araçlarına eriÅŸim yetkiniz yok." - -# -#: ContentGenerator/Instructor/ProblemSetList.pm:339 -msgid "You are not authorized to modify homework sets." -msgstr "Ödev setlerini deÄŸiÅŸtirme yetkiniz yok." - -# -#: ContentGenerator/Instructor/ProblemSetList.pm:344 -msgid "You are not authorized to modify set definition files." -msgstr "Set tanım dosyalarını deÄŸiÅŸtirme yetkiniz yok." - -# -#: ContentGenerator/Instructor/UserList.pm:329 -#: ContentGenerator/Instructor/UserList.pm:335 -msgid "You are not authorized to modify student data" -msgstr "Öğrenci bilgilerini deÄŸiÅŸtirme yetkisine sahip deÄŸilsiniz." - -# -#: ContentGenerator/Instructor/ProblemSetList.pm:389 -#: ContentGenerator/Instructor/UserList.pm:380 -msgid "You are not authorized to perform this action." -msgstr "Bu iÅŸlemi gerçekleÅŸtirme yetkiniz yok." - -# -#: ContentGenerator/Instructor/UserList.pm:1121 -msgid "You cannot delete yourself!" -msgstr "Kendinizi silemezsiniz!" - -# -#: ContentGenerator/Options.pm:163 -#: ContentGenerator/Options.pm:180 -msgid "You do not have permission to change email addresses." -msgstr "Adres deÄŸiÅŸtirmek için yetkiniz yok." - -# -#: ContentGenerator/Options.pm:117 -#: ContentGenerator/Options.pm:139 -msgid "You do not have permission to change your password." -msgstr "Åžifre deÄŸiÅŸtirmek için yetkiniz yok." - -# -#. ($attemptsLeft,$attemptsLeftNoun) -#: ContentGenerator/Problem.pm:1196 -#, fuzzy -msgid "You have %1 %2 remaining." -msgstr "%1 %2 kaldı." - -# -#. ($attemptsLeft) -#: ContentGenerator/Problem.pm:1197 -#, fuzzy -msgid "You have %negquant(%1,unlimited attempts,attempt,attempts) remaining." -msgstr "[negquant,_1,Sınırsız deneme hakkı,deneme hakkı,deneme hakkı] kaldı." - -# -#. ($attempts) -#: ContentGenerator/Problem.pm:1191 -#, fuzzy -msgid "You have attempted this problem %quant(%1,time,times)." -msgstr "Bu soru için [quant,_1,deneme hakkı,deneme hakkı] kullandınız." - -# -#: ContentGenerator/Logout.pm:119 -msgid "You have been logged out of WeBWorK." -msgstr "WeBWorK sisteminden çıkış yaptınız." - -# -#: ContentGenerator/Instructor/UserList.pm:1933 -msgid "You may not change your own password here!" -msgstr "Åžifrenizi buradan deÄŸiÅŸtiremezsiniz!" - -# -#: Authen.pm:317 -msgid "You must specify a user ID." -msgstr "Bir kullanıcı adı girmelisiniz." - -# -#. (sprintf("%.0f%%", $pg->{result}->{score} * 100) -#: ContentGenerator/Problem.pm:1192 -#, fuzzy -msgid "You received a score of %1 for this attempt." -msgstr "Bu soru için %1 puan aldınız." - -# -#: ContentGenerator/Options.pm:157 -msgid "Your email address has been changed." -msgstr "E-posta adresiniz deÄŸiÅŸtirildi." - -# -#. ($lastScore,$notCountedMessage) -#: ContentGenerator/Problem.pm:1194 -#, fuzzy -msgid "Your overall recorded score is %1. %2" -msgstr "Ortalama puanınız: %1. %2" - -# -#: Authen.pm:416 -msgid "Your session has timed out due to inactivity. Please log in again." -msgstr "Oturumunuz zaman aşımına uÄŸradı. Lütfen tekrar giriÅŸ yapınız." - -# -#: ContentGenerator/ProblemSet.pm:273 -#: ContentGenerator/ProblemSets.pm:71 -#, fuzzy -msgid "[edit]" -msgstr "~[düzenle~]" - -# -#: ContentGenerator/Instructor/UserList.pm:476 -msgid "_CLASSLIST_EDITOR_DESCRIPTION" -msgstr "Bu sayfa sınıf listesi editörü. Burada derse kayıtlı öğrencilerin bilgilerini görebilir ve deÄŸiÅŸtirebilirsiniz. Sayfanın yukarısında hangi öğrencileri görmek istediÄŸinizi belirleyebileceÄŸiniz filtre formları var. Bunları kullanarak öğrencileri belirlediÄŸiniz kriterlere göre sıralayabilir, öğrenci bilgilerini deÄŸiÅŸtirebilir,öğrencilere yeni ÅŸifre verebilir, dışarıdan öğrenci bilgileri alabilir yahut derse yeni öğrenciler ekleyip çıkarabilirsiniz. Hangi iÅŸlemi yapmak istiyorsanız seçin ve gerekli bilgileri girin daha sonra \"Ä°ÅŸlemi GerçekleÅŸtir!\" tuÅŸuna basın. Sayfanın altında öğrencilerin bilgilerini gösteren bir tablo var." - -# -#. (CGI::strong($r->maketext($course) -#: ContentGenerator/Login.pm:151 -msgid "_EXTERNAL_AUTH_MESSAGE" -msgstr "%1 harici bir kimlik doÄŸrulama sistemi kullanıyor. Bu sistemle kimliÄŸinizi doÄŸruladınız fakat bu derse giriÅŸ yapma izniniz yok." - -# -#. (CGI::b($r->maketext("Guest Login") -#: ContentGenerator/Login.pm:230 -msgid "_GUEST_LOGIN_MESSAGE" -msgstr "Bu derse misafir olarak girebilirsiniz. Misafir olarak girmek için %1 linkine basın. " - -# -#: ContentGenerator/Instructor/ProblemSetList.pm:498 -msgid "_HMWKSETS_EDITOR_DESCRIPTION" -msgstr "Bu sayfa ödev setleri editörü. Burada bu derse ait ödev setlerini görebilir ve üzerlerinde deÄŸiÅŸiklikler yapabilirsiniz. Yukarıdaki formları kullanarak setleri istediÄŸiniz kriterlere göre görüntüleyebilir, setleri düzenleyebilir, yeni setler yayımlayabilir, dışarıdan ödev setleri aktarabilir, ödevleri notlayabilir yahut var olan setleri silebilirsiniz. Hangi iÅŸlemi yapmak istiyorsanız seçin ve gerekli bilgileri girin daha sonra \"Ä°ÅŸlemi GerçekleÅŸtir!\" tuÅŸuna basın. Sayfanın aÅŸağısında mevcut setler hakkında bir takım bilgiler var." - -# -#. (CGI::b($r->maketext("Remember Me") -#: ContentGenerator/Login.pm:155 -msgid "_LOGIN_MESSAGE" -msgstr " EÄŸer %1 seçeneÄŸini iÅŸaretlerseniz, giriÅŸ bilgileriniz kullandığınız tarayıcı tarafından hatırlanacak ve sonraki giriÅŸlerinizde kullanıcı adı ve ÅŸifre girmeden WebWork sayfalarını kullanabileceksiniz. Bu özellik, ortak kullanıma açık bilgisayarlar, güvenli olmayan bilgisayarlar, ve doÄŸrudan kontrole sahip olmadığınız bilgisayarlarda kullanmak için güvenli deÄŸildir." - -# -#. ($beginReducedScoringPeriod,$dueDate,$reducedScoringPerCent) -#: ContentGenerator/ProblemSet.pm:333 -msgid "_REDUCED_CREDIT_MESSAGE_1" -msgstr "Bu ödev seti için Azaltılmış Not süresi var. Bu süre %1 tarihinde baÅŸlıyor ve %2 tarihinde son buluyor. Bu süre içinde cevaplanan bütün sorular asıl deÄŸerlerinin yüzde %3 deÄŸerinde olacak." - -# -#. ($beginReducedScoringPeriod,$dueDate,$reducedScoringPerCent) -#: ContentGenerator/ProblemSet.pm:335 -msgid "_REDUCED_CREDIT_MESSAGE_2" -msgstr "Bu ödev seti için Azaltılmış Not süresi vardı. Bu süre %1 tarihinde baÅŸladı ve %2 tarihinde bitti. Bu süre içinde cevaplanan bütün sorular asıl deÄŸerlerinin yüzde %3 deÄŸerinde notlandı." - -# -#: ContentGenerator.pm:1951 -msgid "_REQUEST_ERROR" -msgstr " WebWork bu problemi iÅŸlerken bir yazılım hatası ile karşılaÅŸtı. Problemin kendisinde bir hata olması muhtemeldir. EÄŸer bir öğrenci iseniz bu hatayı ilgili kiÅŸilere bildiriniz. EÄŸer yetkili bir kiÅŸiyseniz daha fazla bilgi için alttaki hata raporunu inceleyiniz." - -# -#: ContentGenerator/Instructor/ProblemSetList.pm:1358 -msgid "a duplicate of the first selected set" -msgstr "Ä°lk seçilen setin bir kopyası" - -# -#: ContentGenerator/Instructor/ProblemSetList.pm:1357 -msgid "a new empty set" -msgstr "BoÅŸ bir yeni set" - -# -#: ContentGenerator/Instructor/ProblemSetList.pm:1479 -msgid "a single set" -msgstr "tek bir set" - -# -#: ContentGenerator/Instructor/ProblemSetList.pm:1091 -#: ContentGenerator/Instructor/ProblemSetList.pm:1193 -#: ContentGenerator/Instructor/ProblemSetList.pm:1610 -#: ContentGenerator/Instructor/ProblemSetList.pm:689 -#: ContentGenerator/Instructor/ProblemSetList.pm:929 -#: ContentGenerator/Instructor/ProblemSetList.pm:989 -msgid "all sets" -msgstr "bütün setler" - -# -#: ContentGenerator/Instructor/ProblemSetList.pm:1551 -#: ContentGenerator/Instructor/UserList.pm:1026 -#: ContentGenerator/Instructor/UserList.pm:1298 -#: ContentGenerator/Instructor/UserList.pm:651 -#: ContentGenerator/Instructor/UserList.pm:967 -msgid "all users" -msgstr "bütün kullanıcılar" - -# -#: ContentGenerator/Instructor/UserList.pm:1196 -#: ContentGenerator/Instructor/UserList.pm:1226 -msgid "any users" -msgstr "her kullanıcı" - -# -#: ContentGenerator/Problem.pm:1165 -msgid "attempt" -msgstr "deneme hakkı" - -# -#: ContentGenerator/Problem.pm:1162 -#: ContentGenerator/Problem.pm:1165 -msgid "attempts" -msgstr "deneme hakkı" - -# -#: ContentGenerator/Instructor/ProblemSetList.pm:1726 -msgid "changes abandoned" -msgstr "deÄŸiÅŸiklikler kaydedilmedi" - -# -#: ContentGenerator/Instructor/ProblemSetList.pm:1790 -msgid "changes saved" -msgstr "deÄŸiÅŸiklikler kaydedildi" - -# -#: ContentGenerator/ProblemSets.pm:468 -msgid "closed, answers available" -msgstr "kapalı, cevaplar açıklandı" - -# -#. ($self->formatDateTime($set->answer_date) -#: ContentGenerator/ProblemSets.pm:464 -#, fuzzy -msgid "closed, answers on %1" -msgstr "kapalı, cevaplar %1 tarihinde" - -# -#: ContentGenerator/ProblemSets.pm:466 -msgid "closed, answers recently available" -msgstr "kapalı, cevaplar yeni açıklandı" - -# -#: ContentGenerator/ProblemSets.pm:402 -msgid "completed." -msgstr "tamamlandı." - -# -#: ContentGenerator/Problem.pm:267 -msgid "completely" -msgstr "tamamen" - -# -#: ContentGenerator/Problem.pm:264 -msgid "correct" -msgstr "doÄŸru" - -# -#. ($num) -#: ContentGenerator/Instructor/ProblemSetList.pm:1310 -msgid "deleted %1 sets" -msgstr "%1 set silindi." - -# -#. ($num) -#: ContentGenerator/Instructor/UserList.pm:1135 -msgid "deleted %1 users" -msgstr "%1 kullanıcı silindi." - -# -#: ContentGenerator/Instructor/ProblemSetList.pm:947 -msgid "editing all sets" -msgstr "bütün setler düzenleniyor" - -# -#: ContentGenerator/Instructor/UserList.pm:985 -msgid "editing all users" -msgstr "bütün kullancılar düzenleniyor" - -# -#: ContentGenerator/Instructor/ProblemSetList.pm:953 -msgid "editing selected sets" -msgstr "seçili setler düzenleniyor" - -# -#: ContentGenerator/Instructor/UserList.pm:991 -msgid "editing selected users" -msgstr "seçili kullanıcılar düzenleniyor" - -# -#: ContentGenerator/Instructor/ProblemSetList.pm:950 -msgid "editing visible sets" -msgstr "görünür setler düzenleniyor" - -# -#: ContentGenerator/Instructor/UserList.pm:988 -msgid "editing visible users" -msgstr "görünür kullanıcılar düzenleniyor" - -# -#: ContentGenerator/Instructor/ProblemSetList.pm:694 -msgid "enter matching set IDs below" -msgstr "eÅŸleÅŸen ödev setlerini aÅŸağıya girin" - -# -#: ContentGenerator/Instructor/ProblemSetList.pm:1665 -msgid "export abandoned" -msgstr "dışarı aktarım yapılmadı" - -# -#: ContentGenerator/Instructor/ProblemSetList.pm:1629 -msgid "exporting all sets" -msgstr "bürün setler dışarı aktarılıyor" - -# -#: ContentGenerator/Instructor/ProblemSetList.pm:1636 -msgid "exporting selected sets" -msgstr "seçili setler dışarı aktarılıyor" - -# -#: ContentGenerator/Instructor/ProblemSetList.pm:1633 -msgid "exporting visible sets" -msgstr "görünür setler dışarı aktarılıyor" - -# -#: ContentGenerator/Instructor/UserList.pm:1044 -msgid "giving new passwords to all users" -msgstr "bütün kullanıcılara yeni ÅŸifre veriliyor" - -# -#: ContentGenerator/Instructor/UserList.pm:1050 -msgid "giving new passwords to selected users" -msgstr "seçili kullanıcılara yeni ÅŸifre veriliyor" - -# -#: ContentGenerator/Instructor/UserList.pm:1047 -msgid "giving new passwords to visible users" -msgstr "görünür kullanıcılara yeni ÅŸifre veriliyor" - -# -#: ContentGenerator/Instructor/ProblemSetList.pm:2522 -#: ContentGenerator/Problem.pm:603 -msgid "hidden" -msgstr "gizli" - -# -#: ContentGenerator/Problem.pm:604 -#: ContentGenerator/ProblemSet.pm:86 -msgid "hidden from students" -msgstr "öğrenciler tarafından görülemez" - -# -#: ContentGenerator/Instructor/ProblemSetList.pm:693 -msgid "hidden sets" -msgstr "gizli setler" - -# -#: ContentGenerator/Problem.pm:266 -msgid "incorrect" -msgstr "yanlış" - -# -#: ContentGenerator/Instructor/ProblemSetList.pm:1480 -msgid "multiple sets" -msgstr "birden fazla set" - -# -#: ContentGenerator/Problem.pm:864 -msgid "navNext" -msgstr " Sonraki" - -# -#: ContentGenerator/Problem.pm:866 -msgid "navNextGrey" -msgstr " Sonraki" - -# -#: ContentGenerator/Problem.pm:850 -msgid "navPrev" -msgstr " Ã–nceki" - -# -#: ContentGenerator/Problem.pm:852 -msgid "navPrevGrey" -msgstr " Ã–nceki" - -# -#: ContentGenerator/Problem.pm:856 -msgid "navProbList" -msgstr " Sorular" - -# -#: ContentGenerator/Problem.pm:858 -msgid "navProbListGrey" -msgstr " Sorular" - -# -#: ContentGenerator/ProblemSet.pm:110 -msgid "navUp" -msgstr "tr: Yukarı" - -# -#: ContentGenerator/Instructor/ProblemSetList.pm:1090 -#: ContentGenerator/Instructor/ProblemSetList.pm:1192 -#: ContentGenerator/Instructor/ProblemSetList.pm:1269 -#: ContentGenerator/Instructor/ProblemSetList.pm:690 -#: ContentGenerator/Instructor/ProblemSetList.pm:988 -msgid "no sets" -msgstr "Set yok" - -# -#: ContentGenerator/Instructor/ProblemSetList.pm:1552 -#: ContentGenerator/Instructor/UserList.pm:1088 -#: ContentGenerator/Instructor/UserList.pm:1199 -#: ContentGenerator/Instructor/UserList.pm:1227 -#: ContentGenerator/Instructor/UserList.pm:652 -msgid "no users" -msgstr "kullanıcı yok" - -# -#: ContentGenerator/ProblemSets.pm:430 -#: ContentGenerator/ProblemSets.pm:452 -msgid "now open, due " -msgstr "açık, bitiÅŸ tarihi: " - -# -#. ($self->formatDateTime($set->due_date() -#: ContentGenerator/ProblemSets.pm:408 -#, fuzzy -msgid "open: complete by %1" -msgstr "açık: %1 tarihine kadar tamamlayın" - -# -#: ContentGenerator/ProblemSets.pm:405 -msgid "over time: closed." -msgstr "süre bitti: kapalı." - -# -#: ContentGenerator/Instructor/ProblemSetList.pm:1093 -#: ContentGenerator/Instructor/ProblemSetList.pm:1194 -#: ContentGenerator/Instructor/ProblemSetList.pm:1271 -#: ContentGenerator/Instructor/ProblemSetList.pm:1612 -#: ContentGenerator/Instructor/ProblemSetList.pm:691 -#: ContentGenerator/Instructor/ProblemSetList.pm:931 -#: ContentGenerator/Instructor/ProblemSetList.pm:991 -msgid "selected sets" -msgstr "seçili setler" - -# -#: ContentGenerator/Instructor/UserList.pm:1028 -#: ContentGenerator/Instructor/UserList.pm:1090 -#: ContentGenerator/Instructor/UserList.pm:1198 -#: ContentGenerator/Instructor/UserList.pm:1300 -#: ContentGenerator/Instructor/UserList.pm:653 -#: ContentGenerator/Instructor/UserList.pm:969 -msgid "selected users" -msgstr "seçili kullanıcılar" - -# -#: ContentGenerator/Instructor/ProblemSetList.pm:762 -msgid "showing all sets" -msgstr "bütün setler gösteriliyor" - -# -#: ContentGenerator/Instructor/UserList.pm:738 -msgid "showing all users" -msgstr "bütün kullanıcılar gösteriliyor" - -# -#: ContentGenerator/Instructor/UserList.pm:747 -msgid "showing matching users" -msgstr "eÅŸleÅŸen kullanıcılar gösteriliyor" - -# -#: ContentGenerator/Instructor/ProblemSetList.pm:765 -msgid "showing no sets" -msgstr "hiçbir set gösterilmiyor" - -# -#: ContentGenerator/Instructor/UserList.pm:741 -msgid "showing no users" -msgstr "hiçbir kullanıcı gösterilmiyor" - -# -#: ContentGenerator/Instructor/ProblemSetList.pm:768 -msgid "showing selected sets" -msgstr "seçili setler gösteriliyor" - -# -#: ContentGenerator/Instructor/UserList.pm:744 -msgid "showing selected users" -msgstr "seçili kullanıcılar gösteriliyor" - -# -#: ContentGenerator/Problem.pm:1124 -#, fuzzy -msgid "submitAnswers" -msgstr "Yanıtları Gönder" - -# -#: ContentGenerator/Problem.pm:1154 -msgid "time" -msgstr "deneme hakkı" - -# -#: ContentGenerator/Problem.pm:1154 -msgid "times" -msgstr "deneme hakkı" - -# -#: ContentGenerator/Problem.pm:1161 -#: ContentGenerator/ProblemSet.pm:427 -msgid "unlimited" -msgstr "sınırsız" - -# -#: ContentGenerator/Instructor/UserList.pm:654 -msgid "users who match on selected field" -msgstr "seçili alanla eÅŸleÅŸen kullanıcılar" - -# -#: ContentGenerator/Instructor/ProblemSetList.pm:2522 -#: ContentGenerator/Problem.pm:603 -msgid "visible" -msgstr "Görünür" - -# -#: ContentGenerator/Instructor/ProblemSetList.pm:1611 -#: ContentGenerator/Instructor/ProblemSetList.pm:692 -#: ContentGenerator/Instructor/ProblemSetList.pm:930 -msgid "visible sets" -msgstr "görünür setler" - -# -#: ContentGenerator/Problem.pm:604 -#: ContentGenerator/ProblemSet.pm:86 -msgid "visible to students" -msgstr "öğrencilere görüntüleyebilir" - -# -#: ContentGenerator/Instructor/UserList.pm:1027 -#: ContentGenerator/Instructor/UserList.pm:1197 -#: ContentGenerator/Instructor/UserList.pm:1299 -#: ContentGenerator/Instructor/UserList.pm:968 -msgid "visible users" -msgstr "kullanıcılar görüntüleyebilir" - -# -#. ($self->formatDateTime($set->open_date) -#: ContentGenerator/ProblemSets.pm:420 -#: ContentGenerator/ProblemSets.pm:448 -#, fuzzy -msgid "will open on %1" -msgstr "%1 tarihinde açılacak" - -# -msgid "Add to which set?" -msgstr "Hangi sete eklenecek?" - -# -msgid "Course Information for course [_1]" -msgstr "%1 dersinin bilgileri" - -# -msgid "report bugs in this problem" -msgstr "Soruda hata olduÄŸunu bildir" - -# -msgid "Problem [_1]" -msgstr "Soru %1" - -# -msgid "Cancel Password" -msgstr "Åžifreyi iptal et" - -# -msgid "WeBWorK" -msgstr "WeBWorK" - -# -msgid "Audit" -msgstr "Denetle" - -# -msgid "Hardcopy Header for set [_1]" -msgstr "Set %1 için yazdırılabilir dosya baÅŸlığı" - -# -msgid "Report bugs in a WeBWorK question/problem using this link. The very first time you do this you will need to register with an email address so that information on the bug fix can be reported back to you." -msgstr "WebWork veya soruyla ilgili hata bildirmek için bu linki kullanın. Bunu ilk kez yaparken e-posta adresinizle kayıt olmanız gerekmektedir." - -# -msgid "Pad Fields" -msgstr "tr: Pad Fields" - -# -msgid "Set Header for set [_1]" -msgstr "Set %1 baÅŸlığı" - -# -msgid "Statistics for [_1]" -msgstr "Set %1 istatistikleri" - -# -msgid "Filter" -msgstr "Filtre" - -# -msgid "set [_1]/problem [_2]" -msgstr " set %1/soru %2" - -# -msgid "You are not authorized to modify problems." -msgstr "Sorularda deÄŸiÅŸklik yapma yetkiniz yok." - -# -msgid "hardcopy header file" -msgstr "Yazdırılabilir ödev baÅŸlığı dosyası" - -# -msgid "Write permissions have not been enabled in the templates directory. No changes can be made." -msgstr "Åžablon klasörü yazmaya kapalı. DeÄŸiÅŸiklik yapılamaz." - -# -msgid "Save [_1] and View" -msgstr "%1 Kaydet ve Görüntüle" - -# -msgid "Create" -msgstr "OluÅŸtur" - -# -msgid "Changes in this file have not yet been permanently saved." -msgstr "Dosyadaki deÄŸiÅŸiklikler henüz kalıcı olarak kaydedilmedi." - -# -msgid "Instructor Tools" -msgstr "EÄŸitmen Araçları" - -# -msgid "Score" -msgstr "Not" - -# -msgid "Show in another window" -msgstr "Yani pencerede göster" - -# -msgid "Copied auxiliary files from [_1] to new location at [_2]" -msgstr "Ek dosyalar %1 dizininden %2 dizinine kopyalandı" - -# -msgid "Unpublished" -msgstr "Yayınlanmadı" - -# -msgid "Login" -msgstr "Devam et" - -# -msgid "Email" -msgstr "E-posta" - -# -msgid "File Manager" -msgstr "Dosya Yöneticisi" - -# -msgid "Unable to change the hardcopy header for set [_1]. Unknown error." -msgstr "%1 seti için yazdırılabilir dosya baÅŸlığı deÄŸiÅŸtirilemedi. Bilinmeyen hata." - -# -msgid "Unable to make '[_1]' the set header for [_2]" -msgstr "'%1' set %2 için dosya baÅŸlığı yapılamadı" - -# -msgid "Can't determine user of temporary edit file [_1]." -msgstr "Geçici dosya %1 için kullanıcı belirlenemedi." - -# -msgid "Download a hardcopy of this homework set." -msgstr "Bu set için yazdırılabilir dosya indirin." - -# -msgid "Problem Editor" -msgstr "Soru Düzenleyici" - -# -msgid "The file '[_1]' is protected!" -msgstr "'%1' dosyası korumalı!" - -# -msgid "Top level of author information on the wiki." -msgstr "Wikide yazar bilgilerinin en üst seviyesi." - -# -msgid "Score selected set(s) and save to:" -msgstr "Seçili setleri notla ve dosyaya kaydet:" - -# -msgid "Save Password" -msgstr "Åžifreyi kaydet" - -# -msgid "The file '[_1]' is a blank problem!" -msgstr "'%1' dosyası boÅŸ bir soru!" - -# -msgid "Add as what filetype?" -msgstr "Hangi dosya tipi olarak eklenecek?" - -# -msgid "no action" -msgstr "Ä°ÅŸlem yapma" - -# -msgid "Added '[_1]' to [_2] as new set header" -msgstr "'%1' dosyası %2 dosyasına yeni set baÅŸlığı olarak eklendi" - -# -msgid "View statistics by set" -msgstr "Setlere göre istatistikleri göster" - -# -msgid "Set" -msgstr "Set" - -# -msgid "This is a blank problem template file and can not be edited directly. Use the 'Save as' action below to create a local copy of the file and add it to the current problem set." -msgstr "Bu boÅŸ bir soru ÅŸablonu ve üzerinde deÄŸiÅŸiklik yapılamaz. AÅŸağıdaki farklı kaydet iÅŸlemini kullarak dosyanın yerel bir kopyasını oluÅŸturdaktan sonra üzerinde deÄŸiÅŸiklik yapabilirsiniz." - -# -msgid "Logout" -msgstr "Çıkış" - -# -msgid "You do not have permission to view the details of this error." -msgstr "Bu hatanın detaylarını görmeye yetkiniz yok." - -# -msgid "Reverting to original file '[_1]'" -msgstr "Asıl dosyaya geri dönülüyor ('%1')" - -# -msgid "Delete" -msgstr "Sil" - -# -msgid "Wiki summary page for MathObjects" -msgstr "MathObjects için özet wiki sayfası" - -# -msgid "Your score was recorded." -msgstr "Notunuz kaydedildi." - -# -msgid "You must specify an file name in order to save a new file." -msgstr "Yeni dosya kaydetmek için dosya adı belirtmelisiniz." - -# -msgid "To edit this text you must first make a copy of this file using the 'Save as' action below." -msgstr "tr: To edit this text you must first make a copy of this file using the 'Save as' action below." - -# -msgid "unassigned problem file" -msgstr "tr: unassigned problem file" - -# -msgid "View statistics by student" -msgstr "tr: View statistics by student" - -# -msgid "Publish" -msgstr "tr: Publish" - -# -msgid "Password/Email" -msgstr "Åžifre/E-posta" - -# -msgid "Cancel Edit" -msgstr "tr: Cancel Edit" - -# -msgid "Math Objects" -msgstr "tr: Math Objects" - -# -msgid "Snippets of PG code illustrating specific techniques" -msgstr "tr: Snippets of PG code illustrating specific techniques" - -# -msgid "Error copying [_1] to [_2]" -msgstr "tr: Error copying %1 to %2" - -# -msgid "Hmwk Sets Editor" -msgstr "Ödev Setleri Düzenleyici" - -# -msgid "This set is [_1] students." -msgstr "Bu soru grubu öğrenciler tarafından %1." - -# -msgid "header file" -msgstr "tr: header file" - -# -msgid "Include Index" -msgstr "tr: Include Index" - -# -msgid "Display Mode" -msgstr "tr: Display Mode" - -# -msgid "You can earn partial credit on this problem." -msgstr "Bu sorudan kısmi puan alabilirsiniz." - -# -msgid "Write permissions have not been enabled in '[_1]'. Changes must be saved to a different directory for viewing." -msgstr "tr: Write permissions have not been enabled in '%1'. Changes must be saved to a different directory for viewing." - -# -msgid "blank problem" -msgstr "tr: blank problem" - -# -msgid "Save Edit" -msgstr "tr: Save Edit" - -# -msgid "Enrolled" -msgstr "tr: Enrolled" - -# -msgid "Write permissions have not been enabled for '[_1]'. Changes must be saved to another file for viewing." -msgstr "tr: Write permissions have not been enabled for '%1'. Changes must be saved to another file for viewing." - -# -msgid "Note: this problem viewer is for viewing purposes only. As of right now, testing functionality is not possible." -msgstr "tr: Note: this problem viewer is for viewing purposes only. As of right now, testing functionality is not possible." - -# -msgid "Save Export" -msgstr "tr: Save Export" - -# -msgid "webwork" -msgstr "webwork" - -# -msgid "Problem Viewer" -msgstr "tr: Problem Viewer" - -# -msgid "submit button clicked" -msgstr "gönder düğmesine basıldı" - -# -msgid "Student Progress for [_1]" -msgstr "tr: Student Progress for %1" - -# -msgid "A new file has been created at '[_1]' with the contents below. No changes have been made to set [_2]." -msgstr "tr: A new file has been created at '%1' with the contents below. No changes have been made to set %2." - -# -msgid "Your score was not recorded because this problem has not been assigned to you." -msgstr "Notunuz kaydedilmedi çünkü bu soru size yöneltilmedi." - -# -msgid "Sort" -msgstr "tr: Sort" - -# -msgid "Add problem" -msgstr "tr: Add problem" - -# -msgid "Author Info" -msgstr "tr: Author Info" - -# -msgid "Replace [_1]" -msgstr "tr: Replace %1" - -# -msgid "Help" -msgstr "tr: Help" - -# -msgid "Add Users" -msgstr "Kullanıcı Ekle" - -# -msgid "Problem Source Code" -msgstr "tr: Problem Source Code" - -# -msgid "Test snippets of PG code in interactive lab. Good way to learn PG language." -msgstr "tr: Test snippets of PG code in interactive lab. Good way to learn PG language." - -# -msgid "Page generated at [_1] at [_2]" -msgstr "tr: Page generated at %1 at %2" - -# -msgid "Import" -msgstr "tr: Import" - -# -msgid "View Using Seed Number" -msgstr "tr: View Using Seed Number" - -# -msgid "Grades" -msgstr "Notlar" - -# -msgid "Published" -msgstr "Yayınlandı" - -# -msgid "Your score was not recorded because this homework set is closed." -msgstr "Notunuz kaydedilemedi çünkü bu soru grubu kapalı." - -# -msgid "The text box now contains the source of the original problem. You can recover lost edits by using the Back button on your browser." -msgstr "tr: The text box now contains the source of the original problem. You can recover lost edits by using the Back button on your browser." - -# -msgid "The file '[_1]' is a directory!" -msgstr "tr: The file '%1' is a directory!" - -# -msgid "Save As" -msgstr "tr: Save As [TMPL]/" - -# -msgid "Documentation from source code for PG modules and macro files. Often the most up-to-date information." -msgstr "tr: Documentation from source code for PG modules and macro files. Often the most up-to-date information." - -# -msgid "Problem Techniques" -msgstr "tr: Problem Techniques" - -# -msgid "Course Configuration" -msgstr "Ders Seçenekleri" - -# -msgid "Feedback" -msgstr "Geri Bildirim" - -# -msgid "Export" -msgstr "tr: Export" - -# -msgid "PG mark down syntax used to format WeBWorK questions. This interactive lab can help you to learn the techniques." -msgstr "tr: PG mark down syntax used to format WeBWorK questions. This interactive lab can help you to learn the techniques." - -# -msgid "Record Scores for Single Sets" -msgstr "tr: Record Scores for Single Sets" - -# -msgid "Append to end of set [_1]" -msgstr "tr: Append to end of set %1" - -# -msgid "Unrecognized saveMode: |[_1]|. Unknown error." -msgstr "tr: Unrecognized saveMode: |%1|. Unknown error." - -# -msgid "Your score was not recorded because there was a failure in storing the problem record to the database." -msgstr "Notunuz kaydedilemedi, çünkü notun veritabanına kaydı sırasında bir hata oluÅŸtu." - -# -msgid "The hardcopy header for set [_1] has been renamed to '[_2]'." -msgstr "tr: The hardcopy header for set %1 has been renamed to '%2'." - -# -msgid "Save as" -msgstr "tr: Save as" - -# -msgid "Select action below" -msgstr "tr: Select action below" - -# -msgid "The file '[_1]' cannot be found." -msgstr "tr: The file '%1' cannot be found." - -# -msgid "Unable to write to '[_1]': [_2]" -msgstr "tr: Unable to write to '%1': %2" - -# -msgid "Save" -msgstr "tr: Save" - -# -msgid "File '[_1]' exists. File not saved. No changes have been made. You can change the file path for this problem manually from the 'Hmwk Sets Editor' page" -msgstr "tr: File '%1' exists. File not saved. No changes have been made. You can change the file path for this problem manually from the 'Hmwk Sets Editor' page" - -# -msgid "The set header for set [_1] has been renamed to '[_2]'." -msgstr "tr: The set header for set %1 has been renamed to '%2'." - -# -msgid "Error: This path is already in the temporary edit directory -- no new temporary file is created. path = [_1]" -msgstr "tr: Error: This path is already in the temporary edit directory -- no new temporary file is created. path = %1" - -# -msgid "Please use radio buttons to choose the method for saving this file. Can't recognize saveMode: |[_1]|." -msgstr "tr: Please use radio buttons to choose the method for saving this file. Can't recognize saveMode: |%1|." - -# -msgid "Saved to file '[_1]'" -msgstr "tr: Saved to file '%1'" - -# -msgid "Please specify a file to save to." -msgstr "tr: Please specify a file to save to." - -# -msgid "The selected problem ([_1]) is not a valid problem for set [_2]." -msgstr "Seçilen problem (%1), %2 soru drubu için geçerli bir soru deÄŸil." - -# -msgid "Editing [_1] in file '[_2]'" -msgstr "tr: Editing %1 in file '%2'" - -# -msgid "Deleting temp file at [_1]" -msgstr "tr: Deleting temp file at %1" - -# -msgid "hidden from" -msgstr "görüntülenmiyor" - -# -msgid "View student progress by set" -msgstr "tr: View student progress by set" - -# -msgid "View" -msgstr "tr: View" - -# -msgid "Resources" -msgstr "tr: Resources" - -# -msgid "Scoring Download" -msgstr "Notları Ä°ndir" - -# -msgid "Duplicate" -msgstr "tr: Duplicate" - -# -msgid "This path |[_1]| is not the path to a temporary edit file." -msgstr "tr: This path |%1| is not the path to a temporary edit file." - -# -msgid "visible to" -msgstr "görüntüleniyor" - -# -msgid "Unable to change the source file path for set [_1], problem [_2]. Unknown error." -msgstr "tr: Unable to change the source file path for set %1, problem %2. Unknown error." - -# -msgid "Course Administration" -msgstr "Ders Yönetimi" - -# -msgid "Added [_1] to [_2] as problem [_3]" -msgstr "tr: Added %1 to %2 as problem %3" - -# -msgid "Options Information" -msgstr "tr: Options Information" - -# -msgid "Proctor" -msgstr "tr: Proctor" - -# -msgid "Student Progress" -msgstr "tr: Student Progress" - -# -msgid "Revert" -msgstr "tr: Revert" - -# -msgid "Statistics" -msgstr "Ä°statistikler" - -# -msgid "Save as new independent problem" -msgstr "tr: Save as new independent problem" - -# -msgid "Scoring Tools" -msgstr "Notlama Araçları" - -# -msgid "The source file for 'set [_1] / problem [_2]' has been changed from [_3] to '[_4]'." -msgstr "tr: The source file for 'set %1 / problem %2' has been changed from %3 to '%4'." - -# -msgid "Cancel Export" -msgstr "tr: Cancel Export" - -# -msgid "View student progress by student" -msgstr "tr: View student progress by student" - -# -msgid "Revert to [_1]" -msgstr "tr: Revert to %1" - -# -msgid "Classlist Editor" -msgstr "Sınıf Listesi Düzenleyici" - -# -msgid "Add" -msgstr "tr: Add" - -# -msgid "the original path to the file is [_1]" -msgstr "tr: the original path to the file is %1" - -# -msgid "The path to the original file should be absolute" -msgstr "tr: The path to the original file should be absolute" - -# -msgid "To edit this text you must use the 'Save AS' action below to save it to another file." -msgstr "tr: To edit this text you must use the 'Save AS' action below to save it to another file." - -# -msgid "Error: The original file [_1] cannot be read." -msgstr "tr: Error: The original file %1 cannot be read." - -# -msgid "Unable to change the set header for set [_1]. Unknown error." -msgstr "tr: Unable to change the set header for set %1. Unknown error." - -# -msgid "options information" -msgstr "tr: options information" - -# -msgid "The selected problem([_1]) is not a valid problem for set [_2]." -msgstr "tr: The selected problem(%1) is not a valid problem for set %2." - -# -msgid "Unknown file type" -msgstr "tr: Unknown file type" - -# -msgid "course information" -msgstr "tr: course information" - -# -msgid "Change User Options" -msgstr "Kullanıcı bilgilerini güncelle" - -# -msgid "Drop" -msgstr "tr: Drop" - -# -#, fuzzy -msgid "Solutions" -msgstr "Çözümleri göster" - -# -msgid "Show:" -msgstr "" - -# -msgid "WeBWorK Assignment [_1] is due : [_2]." -msgstr "" - -# -#, fuzzy -msgid "Hardcopy Generator" -msgstr "Çıktı Dosyası BaÅŸlığı" - -# -#, fuzzy -msgid "This set is visible to students." -msgstr "Bu soru grubu öğrenciler tarafından %1." - -# -msgid "View equations as" -msgstr "" - -# -msgid "From:" -msgstr "" - -# -msgid "Generate Hardcopy" -msgstr "" - -# -msgid "You may choose to show any of the following data. Correct answers and solutions are only available after the answer date of the homework set." -msgstr "" - -# -#, fuzzy -msgid "E-mail Instructor" -msgstr "EÄŸitmene E-posta" - -# -msgid "Send E-mail:" -msgstr "" - -# -#, fuzzy -msgid "Cancel Email" -msgstr "tr: Cancel Edit" - -# -#, fuzzy -msgid "E-mail:" -msgstr "E-posta" - -# -#, fuzzy -msgid "Correct answers" -msgstr "DoÄŸru yanıtları göster" - -# -msgid "Use this form to report to your professor a problem with the WeBWorK system or an error in a problem you are attempting. Along with your message, additional information about the state of the system will be included." -msgstr "" - -# -#, fuzzy -msgid "Student answers" -msgstr "Yanıtları Gönder" - -# -#, fuzzy -msgid "Hints" -msgstr "Ä°puçlarını göster" - -# -#, fuzzy -msgid "Hardcopy Format:" -msgstr "Çıktı Dosyası BaÅŸlığı" - +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +msgid "" +msgstr "" +"Project-Id-Version: WeBWorK Online Homework System\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: 2011-12-29 12:35-0500\n" +"Last-Translator: Michael Gage \n" +"Language-Team: Webwork Turkish language team >\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: \n" +"X-Poedit-Language: Turkish\n" +"X-Poedit-Country: TURKEY\n" + +# +#. ($name, $vnum) +#: ContentGenerator/ProblemSets.pm:416 +msgid "%1 (test %2)" +msgstr "%1 (test %2)" + +# +#. ($numAdded, $numSkipped, join(", ", @$skipped) +#: ContentGenerator/Instructor/ProblemSetList.pm:1581 +msgid "%1 sets added, %2 sets skipped. Skipped sets: (%3)" +msgstr "%1 set eklendi, %2 sets atlandı. Atlanan setler: (%3)" + +# +#. ($numExported, $numSkipped, (($numSkipped) +#: ContentGenerator/Instructor/ProblemSetList.pm:1701 +msgid "%1 sets exported, %2 sets skipped. Skipped sets: (%3)" +msgstr "%1 set aktarıldı, %2 set atlandı. Atlanan setler: (%3)" + +# +#. ($name) +#: ContentGenerator/ProblemSets.pm:427 +#: ContentGenerator/ProblemSets.pm:441 +msgid "%1 test" +msgstr " %1 test" + +# +#. (scalar @userIDsToExport, $dir, $fileName) +#: ContentGenerator/Instructor/UserList.pm:1385 +msgid "%1 users exported to file %2/%3" +msgstr "%1 kullanıcı %2/%3 dosyasına aktarıldı." + +# +#. ($numReplaced, $numAdded, $numSkipped, join (", ", @$skipped) +#: ContentGenerator/Instructor/UserList.pm:1269 +msgid "%1 users replaced, %2 users added, %3 users skipped. Skipped users: (%4)" +msgstr "%1 kullanıcı deÄŸiÅŸtirildi, %2 kullanıcı eklendi, %3 kullanıcı atlandı. Atlanan kullanıcılar: (%4)" + +# +#. (int($answerScore*100) +#: ContentGenerator/Problem.pm:265 +msgid "%1% correct" +msgstr "%1% doÄŸru" + +# +#. ($e_user_name) +#: ContentGenerator/Options.pm:171 +msgid "%1's Current Address" +msgstr "%1 için Mevcut Adres" + +# +#. ($user_name) +#: ContentGenerator/Options.pm:126 +msgid "%1's Current Password" +msgstr "%1 için Mevcut Åžifre" + +# +#. ($e_user_name) +#: ContentGenerator/Options.pm:175 +msgid "%1's New Address" +msgstr "%1 için Yeni Adres" + +# +#. ($e_user_name) +#: ContentGenerator/Options.pm:130 +msgid "%1's New Password" +msgstr "%1 için Yeni Åžifre" + +# +#. ($e_user_name) +#: ContentGenerator/Options.pm:101 +msgid "%1's new password cannot be blank." +msgstr "%1 için yeni ÅŸifre boÅŸ bırakılamaz." + +# +#. ($e_user_name) +#: ContentGenerator/Options.pm:84 +msgid "%1's password has been changed." +msgstr "%1 adlı kullanıcının ÅŸifresi deÄŸiÅŸtirildi." + +# +#. ($setID, $problemID) +#: ContentGenerator/Problem.pm:884 +msgid "%1: Problem %2" +msgstr "%1: Soru %2." + +# +#. ($numBlanks) +#: ContentGenerator/Problem.pm:319 +msgid "%quant(%1,of the questions remains,of the questions remain) unanswered." +msgstr "Soruların %1 tanesi yanıtsız bırakıldı." + +# +#: ContentGenerator/Problem.pm:1188 +msgid "(This problem will not count towards your grade.)" +msgstr "(Bu soru notunuzu etkilemeyecektir.)" + +# +#: ContentGenerator/Problem.pm:1299 +msgid "ANSWERS ONLY CHECKED -- ANSWERS NOT RECORDED" +msgstr "YANITLAR SADECE KONTROL EDÄ°LÄ°YOR -- YANITLAR KAYDEDÄ°LMEDÄ°" + +# +#: ContentGenerator/Instructor/ProblemSetList.pm:1708 +#: ContentGenerator/Instructor/UserList.pm:1391 +#: ContentGenerator/Instructor/UserList.pm:1463 +msgid "Abandon changes" +msgstr "DeÄŸiÅŸiklikleri kaydetme" + +# +#: ContentGenerator/Instructor/ProblemSetList.pm:1647 +msgid "Abandon export" +msgstr "Dışa aktarımı iptal et" + +# +#. ($eUserID) +#: ContentGenerator.pm:848 +msgid "Acting as %1. " +msgstr "%1 gibi davranılıyor." + +# +#: ContentGenerator/Instructor/UserList.pm:1927 +msgid "Active" +msgstr "Aktif" + +# +#: ContentGenerator/Instructor/UserList.pm:1142 +msgid "Add how many students?" +msgstr "Kaç öğrenci eklenecek?" + +# +#: ContentGenerator/Instructor/UserList.pm:1220 +msgid "Add which new users?" +msgstr "Hangi yeni kullanıcılar eklenecek?" + +# +#: ContentGenerator/Problem.pm:311 +msgid "All of the answers above are correct." +msgstr "Yanıtların tümü doÄŸru" + +# +#. ($verb) +#: ContentGenerator/Instructor/ProblemSetList.pm:1052 +msgid "All selected sets %1 all students" +msgstr "Bütün seçili setler her e %1" + +# +#. ($verb) +#: ContentGenerator/Instructor/ProblemSetList.pm:1046 +msgid "All sets %1 all students" +msgstr "Bütün setler %1 all students" + +# +#. ($verb) +#: ContentGenerator/Instructor/ProblemSetList.pm:1049 +msgid "All visible sets %1 all students" +msgstr "Görünür bütün setler her öğrenciye %1" + +# +#: ContentGenerator/Instructor/ProblemSetList.pm:446 +#: ContentGenerator/Instructor/ProblemSetList.pm:832 +#: ContentGenerator/Instructor/ProblemSetList.pm:870 +msgid "Answer Date" +msgstr "Cevap tarihi" + +# +#: ContentGenerator/Problem.pm:242 +msgid "Answer Preview" +msgstr "Gösterim" + +# +#: ContentGenerator/Instructor/ProblemSetList.pm:531 +msgid "Any changes made below will be reflected in the set for ALL students." +msgstr "AÅŸağıda yapılan deÄŸiÅŸiklikler seti çözen HER öğrenci için uygulanacakç" + +# +#: ContentGenerator.pm:1481 +msgid "Apply Options" +msgstr "Seçenekleri Uygula" + +# +#: ContentGenerator/Instructor/ProblemSetList.pm:1545 +msgid "Assign this set to which users?" +msgstr "Bu set hangi kullanıcılara ödev verilecek?" + +# +#: ContentGenerator/Instructor/UserList.pm:2064 +msgid "Assigned Sets" +msgstr "Ödev verilmiÅŸ setler" + +# +#. ($fully) +#: ContentGenerator/Problem.pm:315 +msgid "At least one of the answers above is NOT %1correct." +msgstr "Yanıtların en az bir tanesi %1doÄŸru deÄŸil." + +# +#: ContentGenerator/ProblemSet.pm:350 +msgid "Attempts" +msgstr "Deneme sayısı" + +# +#. ($eUserID,$@) +#: ContentGenerator/Options.pm:71 +msgid "Can't get password record for effective user '%1': %2" +msgstr "Mevcut kullanıcı için ÅŸifre bilgileri alınamadı '%1': %2" + +# +#. ($userID,$@) +#: ContentGenerator/Options.pm:68 +msgid "Can't get password record for user '%1': %2" +msgstr "kullanıcı için ÅŸifre bilgileri alınamadı '%1': %2" + +# +#: ContentGenerator/Options.pm:142 +msgid "Change Email Address" +msgstr "E-posta Adresi DeÄŸiÅŸtir" + +# +#: ContentGenerator/Options.pm:61 +msgid "Change Password" +msgstr "Åžifre DeÄŸiÅŸtir" + +# +#: ContentGenerator/Instructor/UserList.pm:1409 +#: ContentGenerator/Instructor/UserList.pm:1481 +msgid "Changes abandoned" +msgstr "DeÄŸiÅŸiklikler kaydedilmedi" + +# +#: ContentGenerator/Instructor/UserList.pm:1457 +msgid "Changes saved" +msgstr "DeÄŸiÅŸiklikler kaydedildi" + +# +#: ContentGenerator/Problem.pm:1118 +msgid "Check Answers" +msgstr "Yanıtları kontrol et" + +# +#: ContentGenerator/Problem.pm:1227 +msgid "Checking additional error messages" +msgstr "DiÄŸer hata mesajları kontrol ediliyor" + +# +#: ContentGenerator/Instructor/ProblemSetList.pm:1011 +msgid "Choose visibility of the sets to be affected" +msgstr "Etkilenecek setlerin görünürlüğünü seçin" + +# +#: ContentGenerator/Instructor/ProblemSetList.pm:1084 +#: ContentGenerator/Instructor/ProblemSetList.pm:982 +msgid "Choose which sets to be affected" +msgstr "Hangi setlerin etkileneceÄŸini seçin" + +# +#: ContentGenerator/Instructor/ProblemSetList.pm:597 +#: ContentGenerator/Instructor/UserList.pm:550 +#: ContentGenerator/ProblemSets.pm:288 +msgid "Clear" +msgstr "Temizle" + +# +#: ContentGenerator/Instructor/UserList.pm:562 +msgid "Click on the login name to edit individual problem set data, (e.g. due dates) for these students." +msgstr "Bu öğrenciler için problem setlerinde deÄŸiÅŸiklik (teslim tarihi vb.) yapmak için isimlerinin üzerine tıklayın." + +# +#: ContentGenerator/ProblemSets.pm:435 +msgid "Closed" +msgstr "Kapalı" + +# +#: ContentGenerator/Instructor/UserList.pm:2072 +#: ContentGenerator/Instructor/UserList.pm:290 +#: ContentGenerator/Instructor/UserList.pm:818 +#: ContentGenerator/Instructor/UserList.pm:861 +#: ContentGenerator/Instructor/UserList.pm:904 +msgid "Comment" +msgstr "Yorum" + +# +#. ($e_user_name) +#: ContentGenerator/Options.pm:134 +#: ContentGenerator/Options.pm:95 +msgid "Confirm %1's New Password" +msgstr "%1 için Åžifre Onayı" + +# +#: ContentGenerator/Login.pm:206 +msgid "Continue" +msgstr "Devam" + +# +#: ContentGenerator/Problem.pm:243 +msgid "Correct" +msgstr "DoÄŸru yanıt" + +# +#. ($e_user_name,$@) +#: ContentGenerator/Options.pm:80 +msgid "Couldn't change %1's password: %2" +msgstr "%1 adlı kullanıcının ÅŸifresi deÄŸiÅŸtirilemedi: %2" + +# +#. ($@) +#: ContentGenerator/Options.pm:153 +msgid "Couldn't change your email address: %1" +msgstr "E-posta adresiniz deÄŸiÅŸtirilemedi: %1" + +# +#: ContentGenerator/ProblemSets.pm:71 +#: ContentGenerator/ProblemSets.pm:73 +msgid "Course Info" +msgstr "Ders bilgileri" + +# +#: ContentGenerator.pm:657 +#: ContentGenerator/Home.pm:94 +msgid "Courses" +msgstr "Dersler" + +# +#: ContentGenerator/Instructor/ProblemSetList.pm:1351 +msgid "Create as what type of set?" +msgstr "Ne tip set oluÅŸturulacak?" + +# +#: ContentGenerator/Instructor/ProblemSetList.pm:1263 +#: ContentGenerator/Instructor/UserList.pm:1082 +msgid "Delete how many?" +msgstr "Kaç tane silinecek?" + +# +#: ContentGenerator/Instructor/ProblemSetList.pm:1119 +msgid "Disable" +msgstr "Devre diÅŸi bırak" + +# +#: ContentGenerator.pm:1442 +msgid "Display Options" +msgstr "Görünüm Seçenekleri" + +# +#. ($pl) +#: ContentGenerator/ProblemSets.pm:283 +msgid "Download Hardcopy for Selected %plural(%1,Set,Sets)" +msgstr "Seçili Setleri Yazdırmak için dosya indir: [plural,_1,Set,Sets]" + +# +#: ContentGenerator/ProblemSet.pm:321 +msgid "Download PDF or TeX Hardcopy for Current Set" +msgstr "Mevcut seti Yazdırmak için dosya indir" + +# +#: ContentGenerator/ProblemSets.pm:289 +msgid "Download PDF or TeX Hardcopy for Selected Sets" +msgstr "Seçili Setleri Yazdırmak için dosya indir" + +# +#: ContentGenerator/Instructor/ProblemSetList.pm:445 +#: ContentGenerator/Instructor/ProblemSetList.pm:831 +#: ContentGenerator/Instructor/ProblemSetList.pm:869 +msgid "Due Date" +msgstr "Teslim Tarihi" + +# +#: ContentGenerator/Instructor/ProblemSetList.pm:1812 +msgid "Duplicate this set and name it" +msgstr "Bu seti çoÄŸalt ve isimlendir" + +# +#: ContentGenerator/Instructor/UserList.pm:1720 +#: ContentGenerator/Instructor/UserList.pm:1785 +#: ContentGenerator/Instructor/UserList.pm:1818 +msgid "Edit" +msgstr "Düzenle" + +# +#: ContentGenerator/Instructor/ProblemSetList.pm:603 +msgid "Edit All Set Data" +msgstr "Bütün set bilgilerini düzenle" + +# +#: ContentGenerator/Instructor/ProblemSetList.pm:439 +msgid "Edit Assigned Users" +msgstr "Ödev verilmiÅŸ kullanıcıları düzenle" + +# +#: ContentGenerator/Instructor/ProblemSetList.pm:438 +msgid "Edit Problems" +msgstr "Soruları Düzenle" + +# +#: ContentGenerator/Instructor/ProblemSetList.pm:441 +msgid "Edit Set Data" +msgstr "Set Bilgilerini Düzenle" + +# +#: ContentGenerator/Instructor/UserList.pm:961 +msgid "Edit Which Users?" +msgstr "Hangi Kullanıcılar Düzenlenecek?" + +# +#: ContentGenerator/Problem.pm:1011 +msgid "Edit this problem" +msgstr "Bu soruyu düzenle" + +# +#: ContentGenerator/Instructor/ProblemSetList.pm:923 +msgid "Edit which sets?" +msgstr "Hangi setler düzenlecek?" + +# +#: ContentGenerator/Instructor/UserList.pm:2067 +#: ContentGenerator/Instructor/UserList.pm:285 +#: ContentGenerator/Instructor/UserList.pm:813 +#: ContentGenerator/Instructor/UserList.pm:856 +#: ContentGenerator/Instructor/UserList.pm:899 +msgid "Email Address" +msgstr "E-posta Adresi" + +# +#: ContentGenerator.pm:1526 +#: ContentGenerator.pm:1549 +#: ContentGenerator.pm:1572 +msgid "Email instructor" +msgstr "EÄŸitmene E-posta" + +# +#: ContentGenerator/Instructor/ProblemSetList.pm:1120 +msgid "Enable" +msgstr "EtkinleÅŸtir" + +# +#: ContentGenerator/Instructor/ProblemSetList.pm:604 +msgid "Enable Reduced Credit" +msgstr "AzaltılmiÅŸ Notu EtkinleÅŸtir" + +# +#: ContentGenerator/Instructor/ProblemSetList.pm:1113 +msgid "Enable/Disable reduced scoring for selected sets" +msgstr "Seçili setler için azaltılmış not seçenegini etkinleÅŸtir/kaldır" + +# +#: ContentGenerator/Instructor/UserList.pm:815 +#: ContentGenerator/Instructor/UserList.pm:858 +#: ContentGenerator/Instructor/UserList.pm:901 +msgid "Enrollment Status" +msgstr "Kayıt Durumu" + +# +#: ContentGenerator/Instructor/UserList.pm:1321 +msgid "Enter filename below" +msgstr "AÅŸağıya dosya adı girin" + +# +#: ContentGenerator/Instructor/ProblemSetList.pm:1502 +msgid "Enter filenames below" +msgstr "AÅŸağıya dosya isimlerini girin" + +# +#: ContentGenerator/Problem.pm:240 +msgid "Entered" +msgstr "Yanıt" + +# +#: ContentGenerator/Instructor/ProblemSetList.pm:1671 +msgid "Export selected sets" +msgstr "Åžeçili setleri dışarı aktar" + +# +#: ContentGenerator/Instructor/UserList.pm:1317 +msgid "Export to what kind of file?" +msgstr "Ne çeÅŸit dosyaya aktarılacak?" + +# +#: ContentGenerator/Instructor/ProblemSetList.pm:1604 +msgid "Export which sets?" +msgstr "Hangı setler dışarı aktarılacak?" + +# +#: ContentGenerator/Instructor/UserList.pm:1292 +msgid "Export which users?" +msgstr "Hangi kullanıcılar dışarı aktarılacak?" + +# +#. ($@) +#: ContentGenerator/Instructor/ProblemSetList.pm:1435 +msgid "Failed to create new set: %1" +msgstr "Yeni set yaratılamadı: %1" + +# +#: ContentGenerator/Instructor/ProblemSetList.pm:1374 +msgid "Failed to create new set: no set name specified!" +msgstr "Yeni set oluÅŸturulamadı: set adı belirtilmemiÅŸ!" + +# +#. ($@) +#: ContentGenerator/Instructor/ProblemSetList.pm:1848 +msgid "Failed to duplicate set: %1" +msgstr "Set çoÄŸaltılamadı: %1" + +# +#: ContentGenerator/Instructor/ProblemSetList.pm:1832 +msgid "Failed to duplicate set: no set name specified!" +msgstr "Set çoÄŸaltılamadı: set adı belirtilmemiÅŸ!" + +# +#: ContentGenerator/Instructor/ProblemSetList.pm:1396 +#: ContentGenerator/Instructor/ProblemSetList.pm:1830 +msgid "Failed to duplicate set: no set selected for duplication!" +msgstr "Set çoÄŸaltılamadı: herhangi bir set seçili deÄŸil!" + +# +#. ($newSetID) +#: ContentGenerator/Instructor/ProblemSetList.pm:1834 +msgid "Failed to duplicate set: set %1 already exists!" +msgstr "Set çoÄŸaltılamadı: set %1 zaten mevcut!" + +# +#: ContentGenerator/Instructor/UserList.pm:1339 +msgid "Filename" +msgstr "Dosya adı" + +# +#: ContentGenerator/Instructor/UserList.pm:678 +msgid "Filter by what text?" +msgstr "Hangi metin ile filtrelenecek?" + +# +#: ContentGenerator/Instructor/UserList.pm:2065 +#: ContentGenerator/Instructor/UserList.pm:283 +#: ContentGenerator/Instructor/UserList.pm:811 +#: ContentGenerator/Instructor/UserList.pm:854 +#: ContentGenerator/Instructor/UserList.pm:897 +msgid "First Name" +msgstr "Ä°sim" + +# +#: ContentGenerator/Instructor/UserList.pm:1020 +msgid "Give new password to which users?" +msgstr "Hangi kullanıcılara yeni ÅŸifre verilecek?" + +# +#: ContentGenerator/Login.pm:231 +msgid "Guest Login" +msgstr "Misafir Kullanıcı GiriÅŸi" + +# +#: ContentGenerator/Instructor/ProblemSetList.pm:443 +#: ContentGenerator/Instructor/ProblemSetList.pm:829 +#: ContentGenerator/Instructor/ProblemSetList.pm:867 +msgid "Hardcopy Header" +msgstr "Çıktı Dosyası BaÅŸlığı" + +# +#: ContentGenerator/Instructor/ProblemSetList.pm:1017 +msgid "Hidden" +msgstr "Gizli" + +# +#: ContentGenerator.pm:667 +#: ContentGenerator/ProblemSet.pm:110 +#: ContentGenerator/ProblemSets.pm:226 +msgid "Homework Sets" +msgstr "Soru Grupları" + +# +#: ContentGenerator/Instructor/UserList.pm:558 +msgid "If a password field is left blank, the student's current password will be maintained." +msgstr "Åžifre alanı boÅŸ bırakılırsa kullanıcının mevcut ÅŸifresi kullanılır." + +# +#: ContentGenerator/Instructor/ProblemSetList.pm:1498 +msgid "Import from where?" +msgstr "Nereden aktarılacak?" + +# +#: ContentGenerator/Instructor/ProblemSetList.pm:1473 +msgid "Import how many sets?" +msgstr "Kaç set aktarılacak?" + +# +#: ContentGenerator/Instructor/ProblemSetList.pm:1520 +msgid "Import sets with names" +msgstr "Setleri isimleriyle beraber aktar" + +# +#: ContentGenerator/Instructor/UserList.pm:1165 +msgid "Import users from what file?" +msgstr "Kullanıcılar hangi dosyadan aktarılacak?" + +# +#: ContentGenerator/Instructor/UserList.pm:1927 +msgid "Inactive" +msgstr "Aktif DeÄŸil" + +# +#: Authen.pm:148 +msgid "Invalid user ID or password." +msgstr "Yanlış kullanıcı adı ya da ÅŸifre." + +# +#: ContentGenerator/Instructor/UserList.pm:2066 +#: ContentGenerator/Instructor/UserList.pm:284 +#: ContentGenerator/Instructor/UserList.pm:812 +#: ContentGenerator/Instructor/UserList.pm:855 +#: ContentGenerator/Instructor/UserList.pm:898 +msgid "Last Name" +msgstr "Soyadı" + +# +#: ContentGenerator.pm:730 +msgid "Library Browser" +msgstr "Soru Kütüphaneleri" + +# +#: ContentGenerator.pm:731 +msgid "Library Browser 2" +msgstr "Soru Kütüphane Tarayıcısı 2" + +# +#: ContentGenerator/Logout.pm:124 +msgid "Log In Again" +msgstr "Tekrar giriÅŸ yap" + +# +#: ContentGenerator.pm:845 +#: ContentGenerator.pm:847 +msgid "Log Out" +msgstr "Çıkış Yap" + +# +#. ($userID) +#: ContentGenerator.pm:845 +#: ContentGenerator.pm:847 +msgid "Logged in as %1. " +msgstr "%1 olarak giriÅŸ yaptınız." + +# +#: ContentGenerator/Login.pm:74 +#: ContentGenerator/Login.pm:77 +msgid "Login Info" +msgstr "GiriÅŸ Bilgileri" + +# +#: ContentGenerator/Instructor/UserList.pm:2062 +#: ContentGenerator/Instructor/UserList.pm:282 +#: ContentGenerator/Instructor/UserList.pm:810 +#: ContentGenerator/Instructor/UserList.pm:853 +#: ContentGenerator/Instructor/UserList.pm:896 +msgid "Login Name" +msgstr "Kullanıcı adı" + +# +#: ContentGenerator/Instructor/UserList.pm:2063 +msgid "Login Status" +msgstr "Durumu" + +# +#: ContentGenerator.pm:654 +msgid "Main Menu" +msgstr "Ana Menü" + +# +#: ContentGenerator/Instructor/ProblemSetList.pm:705 +msgid "Match on what? (separate multiple IDs with commas)" +msgstr "Hangi özellikler eÅŸleÅŸtirilecek? (birden fazla kullanıcı adını virgülle ayırın)" + +# +#: ContentGenerator/Problem.pm:245 +msgid "Messages" +msgstr "Mesajlar" + +# +#: ContentGenerator/ProblemSet.pm:349 +#: ContentGenerator/ProblemSets.pm:212 +#: ContentGenerator/ProblemSets.pm:213 +msgid "Name" +msgstr "Grup Adı" + +# +#: ContentGenerator/Instructor/ProblemSetList.pm:1328 +msgid "Name the new set" +msgstr "Yeni seti adlandırın" + +# +#: ContentGenerator/Instructor/UserList.pm:1937 +#: ContentGenerator/Instructor/UserList.pm:2077 +msgid "New Password" +msgstr "Yeni Åžifre" + +# +#: ContentGenerator/Instructor/UserList.pm:1519 +msgid "New passwords saved" +msgstr "Yeni ÅŸifreler kaydedildi" + +# +#: ContentGenerator/Problem.pm:864 +#: ContentGenerator/Problem.pm:866 +msgid "Next Problem" +msgstr "Sonraki Soru" + +# +#: ContentGenerator/Instructor/ProblemSetList.pm:2625 +#: ContentGenerator/Instructor/ProblemSetList.pm:2626 +msgid "No" +msgstr "Hayır" + +# +#: ContentGenerator/Instructor/ProblemSetList.pm:1043 +#: ContentGenerator/Instructor/ProblemSetList.pm:1145 +msgid "No change made to any set" +msgstr "Herhangi bir sete deÄŸiÅŸiklik yapılmadı" + +# +#: ContentGenerator/Instructor/ProblemSetList.pm:1217 +msgid "No sets selected for scoring" +msgstr "Notlamak için bir set seçili deÄŸil" + +# +#: ContentGenerator/Instructor/ProblemSetList.pm:266 +msgid "No sets selected for scoring." +msgstr "Notlama için bir set seçili deÄŸil." + +# +#: ContentGenerator/Instructor/ProblemSetList.pm:2722 +msgid "No sets shown. Choose one of the options above to list the sets in the course." +msgstr "Hiç set gösterilmiyor. Dersteki setleri görmek için yukarıdaki seçeneklerden birini iÅŸaretleyin." + +# +#: ContentGenerator/Instructor/UserList.pm:2110 +msgid "" +"No students shown. Choose one of the options above to \n" +"\t list the students in the course." +msgstr "Hiç öğrenci gösterilmiyor. Dersteki öğrencileri görmek için yukarıdaki seçeneklerden birisini seçin." + +# +#: ContentGenerator.pm:851 +msgid "Not logged in." +msgstr "GiriÅŸ yapmadınız." + +# +#: ContentGenerator/Problem.pm:975 +msgid "Note" +msgstr "Not" + +# +#: ContentGenerator/Instructor/ProblemSetList.pm:444 +#: ContentGenerator/Instructor/ProblemSetList.pm:830 +#: ContentGenerator/Instructor/ProblemSetList.pm:868 +msgid "Open Date" +msgstr "Açılış tarihi" + +# +#: ContentGenerator/Problem.pm:1307 +msgid "PREVIEW ONLY -- ANSWERS NOT RECORDED" +msgstr "GÖSTERÄ°M -- YANITLAR KAYDEDÄ°LMEDÄ° " + +# +#. (timestamp($self) +#: ContentGenerator.pm:953 +msgid "Page generated at %1" +msgstr "Sayfa %1de yaratıldı" + +# +#: ContentGenerator/Login.pm:202 +msgid "Password" +msgstr "Sifre" + +# +#: ContentGenerator/Instructor/UserList.pm:2073 +#: ContentGenerator/Instructor/UserList.pm:291 +#: ContentGenerator/Instructor/UserList.pm:819 +#: ContentGenerator/Instructor/UserList.pm:862 +#: ContentGenerator/Instructor/UserList.pm:905 +msgid "Permission Level" +msgstr "Yetki seviyesi" + +# +#. (CGI::b($r->maketext($course) +#: ContentGenerator/Login.pm:154 +msgid "Please enter your username and password for %1 below:" +msgstr "Lütfen %1 dersi için kullanici adi ve sifrenizi giriniz:" + +# +#: ContentGenerator/Instructor/ProblemSetList.pm:396 +msgid "Please select action to be performed." +msgstr "Lütfen gerçekleÅŸtirilecek iÅŸlemi seçin." + +# +#: ContentGenerator/Problem.pm:1116 +msgid "Preview Answers" +msgstr "Yanıtları görüntüle" + +# +#: ContentGenerator/Problem.pm:850 +#: ContentGenerator/Problem.pm:852 +msgid "Previous Problem" +msgstr "Önceki Soru" + +# +#. ($problemID) +#: ContentGenerator.pm:686 +#: ContentGenerator/Problem.pm:808 +#: ContentGenerator/ProblemSet.pm:424 +msgid "Problem %1" +msgstr "Soru %1" + +# +#: ContentGenerator/Problem.pm:856 +#: ContentGenerator/Problem.pm:858 +msgid "Problem List" +msgstr "Soru Listesi" + +# +#: ContentGenerator/Problem.pm:796 +#: ContentGenerator/ProblemSet.pm:346 +msgid "Problems" +msgstr "Sorular" + +# +#: ContentGenerator/Instructor/UserList.pm:2071 +#: ContentGenerator/Instructor/UserList.pm:289 +#: ContentGenerator/Instructor/UserList.pm:817 +#: ContentGenerator/Instructor/UserList.pm:860 +#: ContentGenerator/Instructor/UserList.pm:903 +msgid "Recitation" +msgstr "Problem cozum dersi" + +# +#. ($verb) +#: ContentGenerator/Instructor/ProblemSetList.pm:1148 +msgid "Reduced Credit %1 for all sets" +msgstr "Bütün setler için notlar %1 azaltıldı" + +# +#. ($verb) +#: ContentGenerator/Instructor/ProblemSetList.pm:1154 +msgid "Reduced Credit %1 for selected sets" +msgstr "Seçili setler için notlar %1 azaltıldı" + +# +#. ($verb) +#: ContentGenerator/Instructor/ProblemSetList.pm:1151 +msgid "Reduced Credit %1 for visable sets" +msgstr "Görünür setler için notlar %1 azaltıldı" + +# +#: ContentGenerator/Instructor/ProblemSetList.pm:2523 +msgid "Reduced Credit Disabled" +msgstr "Azaltılmış not etkin deÄŸil" + +# +#: ContentGenerator/Instructor/ProblemSetList.pm:2523 +#: ContentGenerator/Instructor/ProblemSetList.pm:448 +msgid "Reduced Credit Enabled" +msgstr "Azaltılmış not etkin" + +# +#. ($beginReducedScoringPeriod) +#: ContentGenerator/ProblemSets.pm:459 +msgid "Reduced Credit Starts: %1" +msgstr "AzaltılmiÅŸ not baÅŸlangıç tarihi: %1" + +# +#: ContentGenerator/ProblemSet.pm:351 +msgid "Remaining" +msgstr "Kalan hak" + +# +#: ContentGenerator/Login.pm:204 +msgid "Remember Me" +msgstr "Beni hatırla" + +# +#: ContentGenerator/Instructor/UserList.pm:1190 +msgid "Replace which users?" +msgstr "Hangi kullanıcılar deÄŸiÅŸtirilecek?" + +# +#: ContentGenerator.pm:799 +msgid "Report bugs" +msgstr "Hataları bildir" + +# +#: ContentGenerator/Problem.pm:244 +msgid "Result" +msgstr "Sonuç" + +# +#. (CGI::i($self->$actionHandler(\%genericParams, \%actionParams, \%tableParams) +#: ContentGenerator/Instructor/UserList.pm:376 +msgid "Result of last action performed: %1" +msgstr "En son gerçekleÅŸtilen iÅŸlemin sonucu: %1" + +# +#: ContentGenerator/Instructor/ProblemSetList.pm:386 +msgid "Results of last action performed" +msgstr "En son gerçekleÅŸtirilen iÅŸlemin sonuçları" + +# +#: ContentGenerator/Instructor/ProblemSetList.pm:1732 +#: ContentGenerator/Instructor/UserList.pm:1415 +#: ContentGenerator/Instructor/UserList.pm:1487 +msgid "Save changes" +msgstr "DeÄŸiÅŸiklikleri kaydet" + +# +#: ContentGenerator/Instructor/ProblemSetList.pm:1186 +msgid "Score which sets?" +msgstr "Hangi setler notlanacak?" + +# +#: ContentGenerator/Instructor/UserList.pm:2070 +#: ContentGenerator/Instructor/UserList.pm:288 +#: ContentGenerator/Instructor/UserList.pm:816 +#: ContentGenerator/Instructor/UserList.pm:859 +#: ContentGenerator/Instructor/UserList.pm:902 +msgid "Section" +msgstr "Bölüm" + +# +#: ContentGenerator/Instructor/ProblemSetList.pm:582 +msgid "Select all sets" +msgstr "Bütün setleri seç" + +# +#: ContentGenerator/Instructor/UserList.pm:535 +msgid "Select all users" +msgstr "Bütün kullanıcıları seç" + +# +#: ContentGenerator/Instructor/ProblemSetList.pm:534 +#: ContentGenerator/Instructor/UserList.pm:512 +msgid "Select an action to perform" +msgstr "Yapılacak iÅŸlemi seç" + +# +#. ($newSetID) +#: ContentGenerator/Instructor/ProblemSetList.pm:1375 +msgid "Set %1 exists. No set created" +msgstr "Set %1 mevcut. Yeni set oluÅŸturulmadı" + +#: ContentGenerator/Instructor/ProblemSetList.pm:440 +msgid "Set Definition Filename" +msgstr "Set Tanım Dosyası Adı" + +# +#: ContentGenerator/Instructor/ProblemSetList.pm:442 +#: ContentGenerator/Instructor/ProblemSetList.pm:828 +#: ContentGenerator/Instructor/ProblemSetList.pm:866 +msgid "Set Header" +msgstr "Set BaÅŸlığı" + +# +#: ContentGenerator/ProblemSet.pm:273 +#: ContentGenerator/ProblemSet.pm:275 +msgid "Set Info" +msgstr "Set Bilgileri" + +# +#: ContentGenerator/Instructor/ProblemSetList.pm:2700 +msgid "Set List" +msgstr "Set Listesi" + +# +#: ContentGenerator/Instructor/ProblemSetList.pm:827 +#: ContentGenerator/Instructor/ProblemSetList.pm:865 +msgid "Set Name" +msgstr "Set Adı" + +# +#: ContentGenerator/ProblemSet.pm:153 +msgid "Sets" +msgstr "Setler" + +# +#: ContentGenerator/Problem.pm:1063 +msgid "Show Hints" +msgstr "Ä°puçlarını göster" + +# +#: ContentGenerator/Problem.pm:1370 +msgid "Show Past Answers" +msgstr "Önceki Yanıtları Göster" + +# +#: ContentGenerator/Problem.pm:1082 +msgid "Show Solutions" +msgstr "Çözümleri göster" + +# +#: ContentGenerator/Instructor/UserList.pm:645 +msgid "Show Which Users?" +msgstr "Hangi kullanıcılar gösterilecek?" + +# +#: ContentGenerator/Problem.pm:1044 +msgid "Show correct answers" +msgstr "DoÄŸru yanıtları göster" + +# +#: ContentGenerator/Instructor/ProblemSetList.pm:683 +msgid "Show which sets?" +msgstr "Hangi setler gösterilecek?" + +# +#: ContentGenerator/Instructor/ProblemSetList.pm:497 +#: ContentGenerator/Instructor/UserList.pm:475 +msgid "Show/Hide Site Description" +msgstr "Web Alanı Açıklamasını Göster/Gizle" + +# +#. (scalar @visibleSetIDs, scalar @allSetIDs) +#: ContentGenerator/Instructor/ProblemSetList.pm:607 +msgid "Showing %1 out of %2 sets." +msgstr "%2 setin %1 tanesi gösteriliyor." + +# +#. (scalar @Users, scalar @allUserIDs) +#: ContentGenerator/Instructor/UserList.pm:556 +msgid "Showing %1 out of %2 users" +msgstr "%2 kullanıcının %1 tanesi gösteriliyor." + +# +#: ContentGenerator/Login.pm:96 +#: ContentGenerator/Login.pm:99 +msgid "Site Information" +msgstr "Web Alanı Bilgileri" + +# +#: ContentGenerator/Instructor/ProblemSetList.pm:821 +#: ContentGenerator/Instructor/UserList.pm:804 +msgid "Sort by" +msgstr "Kriterlere Göre Sırala" + +# +#. ($names{$primary}, $names{$secondary}) +#: ContentGenerator/Instructor/ProblemSetList.pm:899 +msgid "Sort by %1 and then by %2" +msgstr "Önce %1 sonra %2 kriteriyle sırala" + +# +#: ContentGenerator/Instructor/UserList.pm:2069 +#: ContentGenerator/Instructor/UserList.pm:287 +#: ContentGenerator/ProblemSet.pm:353 +#: ContentGenerator/ProblemSets.pm:215 +#: ContentGenerator/ProblemSets.pm:216 +msgid "Status" +msgstr "Durumu" + +# +#: ContentGenerator.pm:848 +msgid "Stop Acting" +msgstr "Rol Yapmayı bırak" + +# +#: ContentGenerator/Instructor/UserList.pm:2068 +#: ContentGenerator/Instructor/UserList.pm:286 +#: ContentGenerator/Instructor/UserList.pm:814 +#: ContentGenerator/Instructor/UserList.pm:857 +#: ContentGenerator/Instructor/UserList.pm:900 +msgid "Student ID" +msgstr "Kullanıcı Adı" + +# +#: ContentGenerator/Problem.pm:1127 +msgid "Submit Answers" +msgstr "Yanıtları Gönder" + +# +#. ($effectiveUser) +#: ContentGenerator/Problem.pm:1124 +msgid "Submit Answers for %1" +msgstr "%1 için Yanıtları Gönder" + +# +#: ContentGenerator/Instructor/ProblemSetList.pm:1850 +msgid "Success" +msgstr "BaÅŸarılı" + +#. ($newSetID) +#: ContentGenerator/Instructor/ProblemSetList.pm:1437 +msgid "Successfully created new set %1" +msgstr "Yeni set %1 baÅŸarıyla yaratıldı" + +# +#. ($name) +#: ContentGenerator/ProblemSets.pm:424 +#: ContentGenerator/ProblemSets.pm:433 +#: ContentGenerator/ProblemSets.pm:439 +msgid "Take %1 test" +msgstr "%1 testini çöz" + +# +#: ContentGenerator/Instructor/ProblemSetList.pm:598 +#: ContentGenerator/Instructor/UserList.pm:551 +msgid "Take Action!" +msgstr "Ä°ÅŸlemi GerçekleÅŸtir!" + +# +#: ContentGenerator/ProblemSets.pm:236 +msgid "Test Date" +msgstr "Sınav Tarihi" + +# +#: ContentGenerator/ProblemSets.pm:235 +msgid "Test Score" +msgstr "Sınav Notu" + +# +#: ContentGenerator.pm:954 +msgid "The WeBWorK Project" +msgstr "WeBWorK Projesi" + +# +#. ($fully) +#: ContentGenerator/Problem.pm:307 +msgid "The answer above is NOT %1correct." +msgstr "Yukarıdaki yanıt %1doÄŸru deÄŸil." + +# +#: ContentGenerator/Problem.pm:305 +msgid "The answer above is correct." +msgstr "Yukarıdaki yanıt doÄŸru." + +# +#. (CGI::b($r->maketext("[_1]'s Current Password",$user_name) +#: ContentGenerator/Options.pm:107 +msgid "" +"The password you entered in the %1 \n" +"\t\t\t\t\t\tfield does not match your current\n" +"\t\t\t\t\t\tpassword. Please retype your current password and try\n" +"\t\t\t\t\t\tagain." +msgstr "%1 alanına girdiÄŸiniz ÅŸifre ile mevcut ÅŸifreniz uyuÅŸmamaktadır. Lütfen ÅŸirfenizi girerek tekrar deneyiniz." + +# +#. (CGI::b($r->maketext("[_1]'s New Password",$e_user_name) +#: ContentGenerator/Options.pm:91 +msgid "" +"The passwords you entered in the \n" +"\t\t\t\t\t\t\t\t%1 and %2\n" +"\t\t\t\t\t\t\t\tfields don't match. Please retype your new password and try\n" +"\t\t\t\t\t\t\t\tagain." +msgstr "%1 ve %2 alanlarına girdiÄŸiniz ÅŸifreler uyuÅŸmadı. Yeni ÅŸifrenizi girerek tekrar deneyiniz." + +# +#. ($setName,$effectiveUser) +#: ContentGenerator/ProblemSet.pm:308 +msgid "The selected problem set (%1) is not a valid set for %2" +msgstr "SeçtiÄŸiniz soru seti (%1) set %2 için geçerli deÄŸil" + +# +#: ContentGenerator/Instructor/ProblemSetList.pm:859 +#: ContentGenerator/Instructor/UserList.pm:847 +#: ContentGenerator/Instructor/UserList.pm:890 +msgid "Then by" +msgstr "sonra" + +# +#: ContentGenerator/ProblemSet.pm:365 +msgid "This homework set contains no problems." +msgstr "Bu soru grubunda soru bulunmamaktadır." + +# +#: ContentGenerator/Problem.pm:1175 +msgid "This homework set is closed." +msgstr "Bu soru grubu kapalı." + +# +#: ContentGenerator/Problem.pm:1173 +msgid "This homework set is not yet open." +msgstr "Bu soru grubu henüz açık deÄŸil." + +# +#: ContentGenerator/Problem.pm:611 +msgid "This problem will not count towards your grade." +msgstr "Bu soru, notunuzu etkilemeyecektir." + +# +#. (CGI::font({class=>$visiblityStateClass}, $visiblityStateText) +#: ContentGenerator/Problem.pm:605 +#: ContentGenerator/ProblemSet.pm:88 +msgid "This set is %1" +msgstr "Bu set %1" + +# +#: ContentGenerator/Instructor/ProblemSetList.pm:588 +msgid "Unselect all sets" +msgstr "Bütün setlerdeki seçimi kaldır" + +# +#: ContentGenerator/Instructor/UserList.pm:541 +msgid "Unselect all users" +msgstr "Bütün kullanıcılardaki seçimi kaldir" + +# +#: ContentGenerator/Instructor/ProblemSetList.pm:2683 +msgid "Use System Default" +msgstr "Varsayılan Ayarları Kullan" + +# +#: ContentGenerator/Login.pm:200 +msgid "Username" +msgstr "Kullanici adi" + +# +#: ContentGenerator/Instructor/UserList.pm:2087 +msgid "Users List" +msgstr "Kullanıcı Listesi" + +# +#. ($names{$primary}, $names{$secondary}, $names{$ternary}) +#: ContentGenerator/Instructor/UserList.pm:938 +msgid "Users sorted by %1, then by %2, then by %3" +msgstr "Kullanıcılar sırasıyla %1, %2 ve %3 kriterlerine göre sıları" + +# +#: ContentGenerator/ProblemSet.pm:230 +msgid "Viewing temporary file" +msgstr "Geçici dosya gösteriliyor" + +# +#: ContentGenerator/Problem.pm:1332 +#: ContentGenerator/ProblemSets.pm:63 +msgid "Viewing temporary file: " +msgstr "Geçici dosya görüntüleniyor: " + +# +#: ContentGenerator/Instructor/ProblemSetList.pm:833 +#: ContentGenerator/Instructor/ProblemSetList.pm:871 +msgid "Visibility" +msgstr "Görünürlük" + +# +#: ContentGenerator/Instructor/ProblemSetList.pm:1018 +#: ContentGenerator/Instructor/ProblemSetList.pm:447 +msgid "Visible" +msgstr "Görünür" + +# +#: ContentGenerator/Instructor/ProblemSetList.pm:1259 +#: ContentGenerator/Instructor/UserList.pm:1078 +msgid "Warning: Deletion destroys all user-related data and is not undoable!" +msgstr "Dikkat: Silme iÅŸlemi kullanıcının bütün bilgilerini yok eder ve bu iÅŸlem geri döndürelemez!" + +# +#: ContentGenerator/Home.pm:87 +msgid "Welcome to WeBWorK!" +msgstr "WeBWorK sitemize hoÅŸgeldiniz!" + +# +#: ContentGenerator/Instructor/UserList.pm:665 +msgid "What field should filtered users match on?" +msgstr "Seçili kullanıcılar hangi kriterlerde eÅŸleÅŸmeli?" + +# +#: ContentGenerator/ProblemSet.pm:352 +msgid "Worth" +msgstr "DeÄŸeri" + +# +#: ContentGenerator/Instructor/ProblemSetList.pm:2625 +#: ContentGenerator/Instructor/ProblemSetList.pm:2626 +msgid "Yes" +msgstr "Evet" + +# +#: ContentGenerator/Instructor/ProblemSetList.pm:306 +#: ContentGenerator/Instructor/ProblemSetList.pm:418 +#: ContentGenerator/Instructor/UserList.pm:262 +msgid "You are not authorized to access the instructor tools." +msgstr "EÄŸitmen araçlarına eriÅŸim yetkiniz yok." + +# +#: ContentGenerator/Instructor/ProblemSetList.pm:339 +msgid "You are not authorized to modify homework sets." +msgstr "Ödev setlerini deÄŸiÅŸtirme yetkiniz yok." + +# +#: ContentGenerator/Instructor/ProblemSetList.pm:344 +msgid "You are not authorized to modify set definition files." +msgstr "Set tanım dosyalarını deÄŸiÅŸtirme yetkiniz yok." + +# +#: ContentGenerator/Instructor/UserList.pm:329 +#: ContentGenerator/Instructor/UserList.pm:335 +msgid "You are not authorized to modify student data" +msgstr "Öğrenci bilgilerini deÄŸiÅŸtirme yetkisine sahip deÄŸilsiniz." + +# +#: ContentGenerator/Instructor/ProblemSetList.pm:389 +#: ContentGenerator/Instructor/UserList.pm:380 +msgid "You are not authorized to perform this action." +msgstr "Bu iÅŸlemi gerçekleÅŸtirme yetkiniz yok." + +# +#: ContentGenerator/Instructor/UserList.pm:1121 +msgid "You cannot delete yourself!" +msgstr "Kendinizi silemezsiniz!" + +# +#: ContentGenerator/Options.pm:163 +#: ContentGenerator/Options.pm:180 +msgid "You do not have permission to change email addresses." +msgstr "Adres deÄŸiÅŸtirmek için yetkiniz yok." + +# +#: ContentGenerator/Options.pm:117 +#: ContentGenerator/Options.pm:139 +msgid "You do not have permission to change your password." +msgstr "Åžifre deÄŸiÅŸtirmek için yetkiniz yok." + +# +#. ($attemptsLeft,$attemptsLeftNoun) +#: ContentGenerator/Problem.pm:1196 +msgid "You have %1 %2 remaining." +msgstr "%1 %2 kaldı." + +# +#. ($attemptsLeft) +#: ContentGenerator/Problem.pm:1197 +msgid "You have %negquant(%1,unlimited attempts,attempt,attempts) remaining." +msgstr "[negquant,_1,Sınırsız deneme hakkı,deneme hakkı,deneme hakkı] kaldı." + +# +#. ($attempts) +#: ContentGenerator/Problem.pm:1191 +msgid "You have attempted this problem %quant(%1,time,times)." +msgstr "Bu soru için [quant,_1,deneme hakkı,deneme hakkı] kullandınız." + +# +#: ContentGenerator/Logout.pm:119 +msgid "You have been logged out of WeBWorK." +msgstr "WeBWorK sisteminden çıkış yaptınız." + +# +#: ContentGenerator/Instructor/UserList.pm:1933 +msgid "You may not change your own password here!" +msgstr "Åžifrenizi buradan deÄŸiÅŸtiremezsiniz!" + +# +#: Authen.pm:317 +msgid "You must specify a user ID." +msgstr "Bir kullanıcı adı girmelisiniz." + +# +#. (sprintf("%.0f%%", $pg->{result}->{score} * 100) +#: ContentGenerator/Problem.pm:1192 +msgid "You received a score of %1 for this attempt." +msgstr "Bu soru için %1 puan aldınız." + +# +#: ContentGenerator/Options.pm:157 +msgid "Your email address has been changed." +msgstr "E-posta adresiniz deÄŸiÅŸtirildi." + +# +#. ($lastScore,$notCountedMessage) +#: ContentGenerator/Problem.pm:1194 +msgid "Your overall recorded score is %1. %2" +msgstr "Ortalama puanınız: %1. %2" + +# +#: Authen.pm:416 +msgid "Your session has timed out due to inactivity. Please log in again." +msgstr "Oturumunuz zaman aşımına uÄŸradı. Lütfen tekrar giriÅŸ yapınız." + +# +#: ContentGenerator/ProblemSet.pm:273 +#: ContentGenerator/ProblemSets.pm:71 +msgid "[edit]" +msgstr "~[düzenle~]" + +# +#: ContentGenerator/Instructor/UserList.pm:476 +msgid "_CLASSLIST_EDITOR_DESCRIPTION" +msgstr "Bu sayfa sınıf listesi editörü. Burada derse kayıtlı öğrencilerin bilgilerini görebilir ve deÄŸiÅŸtirebilirsiniz. Sayfanın yukarısında hangi öğrencileri görmek istediÄŸinizi belirleyebileceÄŸiniz filtre formları var. Bunları kullanarak öğrencileri belirlediÄŸiniz kriterlere göre sıralayabilir, öğrenci bilgilerini deÄŸiÅŸtirebilir,öğrencilere yeni ÅŸifre verebilir, dışarıdan öğrenci bilgileri alabilir yahut derse yeni öğrenciler ekleyip çıkarabilirsiniz. Hangi iÅŸlemi yapmak istiyorsanız seçin ve gerekli bilgileri girin daha sonra \"Ä°ÅŸlemi GerçekleÅŸtir!\" tuÅŸuna basın. Sayfanın altında öğrencilerin bilgilerini gösteren bir tablo var." + +# +#. (CGI::strong($r->maketext($course) +#: ContentGenerator/Login.pm:151 +msgid "_EXTERNAL_AUTH_MESSAGE" +msgstr "%1 harici bir kimlik doÄŸrulama sistemi kullanıyor. Bu sistemle kimliÄŸinizi doÄŸruladınız fakat bu derse giriÅŸ yapma izniniz yok." + +# +#. (CGI::b($r->maketext("Guest Login") +#: ContentGenerator/Login.pm:230 +msgid "_GUEST_LOGIN_MESSAGE" +msgstr "Bu derse misafir olarak girebilirsiniz. Misafir olarak girmek için %1 linkine basın. " + +# +#: ContentGenerator/Instructor/ProblemSetList.pm:498 +msgid "_HMWKSETS_EDITOR_DESCRIPTION" +msgstr "Bu sayfa ödev setleri editörü. Burada bu derse ait ödev setlerini görebilir ve üzerlerinde deÄŸiÅŸiklikler yapabilirsiniz. Yukarıdaki formları kullanarak setleri istediÄŸiniz kriterlere göre görüntüleyebilir, setleri düzenleyebilir, yeni setler yayımlayabilir, dışarıdan ödev setleri aktarabilir, ödevleri notlayabilir yahut var olan setleri silebilirsiniz. Hangi iÅŸlemi yapmak istiyorsanız seçin ve gerekli bilgileri girin daha sonra \"Ä°ÅŸlemi GerçekleÅŸtir!\" tuÅŸuna basın. Sayfanın aÅŸağısında mevcut setler hakkında bir takım bilgiler var." + +# +#. (CGI::b($r->maketext("Remember Me") +#: ContentGenerator/Login.pm:155 +msgid "_LOGIN_MESSAGE" +msgstr " EÄŸer %1 seçeneÄŸini iÅŸaretlerseniz, giriÅŸ bilgileriniz kullandığınız tarayıcı tarafından hatırlanacak ve sonraki giriÅŸlerinizde kullanıcı adı ve ÅŸifre girmeden WebWork sayfalarını kullanabileceksiniz. Bu özellik, ortak kullanıma açık bilgisayarlar, güvenli olmayan bilgisayarlar, ve doÄŸrudan kontrole sahip olmadığınız bilgisayarlarda kullanmak için güvenli deÄŸildir." + +# +#. ($beginReducedScoringPeriod,$dueDate,$reducedScoringPerCent) +#: ContentGenerator/ProblemSet.pm:333 +msgid "_REDUCED_CREDIT_MESSAGE_1" +msgstr "Bu ödev seti için Azaltılmış Not süresi var. Bu süre %1 tarihinde baÅŸlıyor ve %2 tarihinde son buluyor. Bu süre içinde cevaplanan bütün sorular asıl deÄŸerlerinin yüzde %3 deÄŸerinde olacak." + +# +#. ($beginReducedScoringPeriod,$dueDate,$reducedScoringPerCent) +#: ContentGenerator/ProblemSet.pm:335 +msgid "_REDUCED_CREDIT_MESSAGE_2" +msgstr "Bu ödev seti için Azaltılmış Not süresi vardı. Bu süre %1 tarihinde baÅŸladı ve %2 tarihinde bitti. Bu süre içinde cevaplanan bütün sorular asıl deÄŸerlerinin yüzde %3 deÄŸerinde notlandı." + +# +#: ContentGenerator.pm:1951 +msgid "_REQUEST_ERROR" +msgstr " WebWork bu problemi iÅŸlerken bir yazılım hatası ile karşılaÅŸtı. Problemin kendisinde bir hata olması muhtemeldir. EÄŸer bir öğrenci iseniz bu hatayı ilgili kiÅŸilere bildiriniz. EÄŸer yetkili bir kiÅŸiyseniz daha fazla bilgi için alttaki hata raporunu inceleyiniz." + +# +#: ContentGenerator/Instructor/ProblemSetList.pm:1358 +msgid "a duplicate of the first selected set" +msgstr "Ä°lk seçilen setin bir kopyası" + +# +#: ContentGenerator/Instructor/ProblemSetList.pm:1357 +msgid "a new empty set" +msgstr "BoÅŸ bir yeni set" + +# +#: ContentGenerator/Instructor/ProblemSetList.pm:1479 +msgid "a single set" +msgstr "tek bir set" + +# +#: ContentGenerator/Instructor/ProblemSetList.pm:1091 +#: ContentGenerator/Instructor/ProblemSetList.pm:1193 +#: ContentGenerator/Instructor/ProblemSetList.pm:1610 +#: ContentGenerator/Instructor/ProblemSetList.pm:689 +#: ContentGenerator/Instructor/ProblemSetList.pm:929 +#: ContentGenerator/Instructor/ProblemSetList.pm:989 +msgid "all sets" +msgstr "bütün setler" + +# +#: ContentGenerator/Instructor/ProblemSetList.pm:1551 +#: ContentGenerator/Instructor/UserList.pm:1026 +#: ContentGenerator/Instructor/UserList.pm:1298 +#: ContentGenerator/Instructor/UserList.pm:651 +#: ContentGenerator/Instructor/UserList.pm:967 +msgid "all users" +msgstr "bütün kullanıcılar" + +# +#: ContentGenerator/Instructor/UserList.pm:1196 +#: ContentGenerator/Instructor/UserList.pm:1226 +msgid "any users" +msgstr "her kullanıcı" + +# +#: ContentGenerator/Problem.pm:1165 +msgid "attempt" +msgstr "deneme hakkı" + +# +#: ContentGenerator/Problem.pm:1162 +#: ContentGenerator/Problem.pm:1165 +msgid "attempts" +msgstr "deneme hakkı" + +# +#: ContentGenerator/Instructor/ProblemSetList.pm:1726 +msgid "changes abandoned" +msgstr "deÄŸiÅŸiklikler kaydedilmedi" + +# +#: ContentGenerator/Instructor/ProblemSetList.pm:1790 +msgid "changes saved" +msgstr "deÄŸiÅŸiklikler kaydedildi" + +# +#: ContentGenerator/ProblemSets.pm:468 +msgid "closed, answers available" +msgstr "kapalı, cevaplar açıklandı" + +# +#. ($self->formatDateTime($set->answer_date) +#: ContentGenerator/ProblemSets.pm:464 +msgid "closed, answers on %1" +msgstr "kapalı, cevaplar %1 tarihinde" + +# +#: ContentGenerator/ProblemSets.pm:466 +msgid "closed, answers recently available" +msgstr "kapalı, cevaplar yeni açıklandı" + +# +#: ContentGenerator/ProblemSets.pm:402 +msgid "completed." +msgstr "tamamlandı." + +# +#: ContentGenerator/Problem.pm:267 +msgid "completely" +msgstr "tamamen" + +# +#: ContentGenerator/Problem.pm:264 +msgid "correct" +msgstr "doÄŸru" + +# +#. ($num) +#: ContentGenerator/Instructor/ProblemSetList.pm:1310 +msgid "deleted %1 sets" +msgstr "%1 set silindi." + +# +#. ($num) +#: ContentGenerator/Instructor/UserList.pm:1135 +msgid "deleted %1 users" +msgstr "%1 kullanıcı silindi." + +# +#: ContentGenerator/Instructor/ProblemSetList.pm:947 +msgid "editing all sets" +msgstr "bütün setler düzenleniyor" + +# +#: ContentGenerator/Instructor/UserList.pm:985 +msgid "editing all users" +msgstr "bütün kullancılar düzenleniyor" + +# +#: ContentGenerator/Instructor/ProblemSetList.pm:953 +msgid "editing selected sets" +msgstr "seçili setler düzenleniyor" + +# +#: ContentGenerator/Instructor/UserList.pm:991 +msgid "editing selected users" +msgstr "seçili kullanıcılar düzenleniyor" + +# +#: ContentGenerator/Instructor/ProblemSetList.pm:950 +msgid "editing visible sets" +msgstr "görünür setler düzenleniyor" + +# +#: ContentGenerator/Instructor/UserList.pm:988 +msgid "editing visible users" +msgstr "görünür kullanıcılar düzenleniyor" + +# +#: ContentGenerator/Instructor/ProblemSetList.pm:694 +msgid "enter matching set IDs below" +msgstr "eÅŸleÅŸen ödev setlerini aÅŸağıya girin" + +# +#: ContentGenerator/Instructor/ProblemSetList.pm:1665 +msgid "export abandoned" +msgstr "dışarı aktarım yapılmadı" + +# +#: ContentGenerator/Instructor/ProblemSetList.pm:1629 +msgid "exporting all sets" +msgstr "bürün setler dışarı aktarılıyor" + +# +#: ContentGenerator/Instructor/ProblemSetList.pm:1636 +msgid "exporting selected sets" +msgstr "seçili setler dışarı aktarılıyor" + +# +#: ContentGenerator/Instructor/ProblemSetList.pm:1633 +msgid "exporting visible sets" +msgstr "görünür setler dışarı aktarılıyor" + +# +#: ContentGenerator/Instructor/UserList.pm:1044 +msgid "giving new passwords to all users" +msgstr "bütün kullanıcılara yeni ÅŸifre veriliyor" + +# +#: ContentGenerator/Instructor/UserList.pm:1050 +msgid "giving new passwords to selected users" +msgstr "seçili kullanıcılara yeni ÅŸifre veriliyor" + +# +#: ContentGenerator/Instructor/UserList.pm:1047 +msgid "giving new passwords to visible users" +msgstr "görünür kullanıcılara yeni ÅŸifre veriliyor" + +# +#: ContentGenerator/Instructor/ProblemSetList.pm:2522 +#: ContentGenerator/Problem.pm:603 +msgid "hidden" +msgstr "gizli" + +# +#: ContentGenerator/Problem.pm:604 +#: ContentGenerator/ProblemSet.pm:86 +msgid "hidden from students" +msgstr "öğrenciler tarafından görülemez" + +# +#: ContentGenerator/Instructor/ProblemSetList.pm:693 +msgid "hidden sets" +msgstr "gizli setler" + +# +#: ContentGenerator/Problem.pm:266 +msgid "incorrect" +msgstr "yanlış" + +# +#: ContentGenerator/Instructor/ProblemSetList.pm:1480 +msgid "multiple sets" +msgstr "birden fazla set" + +# +#: ContentGenerator/Problem.pm:864 +msgid "navNext" +msgstr " Sonraki" + +# +#: ContentGenerator/Problem.pm:866 +msgid "navNextGrey" +msgstr " Sonraki" + +# +#: ContentGenerator/Problem.pm:850 +msgid "navPrev" +msgstr " Ã–nceki" + +# +#: ContentGenerator/Problem.pm:852 +msgid "navPrevGrey" +msgstr " Ã–nceki" + +# +#: ContentGenerator/Problem.pm:856 +msgid "navProbList" +msgstr " Sorular" + +# +#: ContentGenerator/Problem.pm:858 +msgid "navProbListGrey" +msgstr " Sorular" + +# +#: ContentGenerator/ProblemSet.pm:110 +msgid "navUp" +msgstr " Yukarı" + +# +#: ContentGenerator/Instructor/ProblemSetList.pm:1090 +#: ContentGenerator/Instructor/ProblemSetList.pm:1192 +#: ContentGenerator/Instructor/ProblemSetList.pm:1269 +#: ContentGenerator/Instructor/ProblemSetList.pm:690 +#: ContentGenerator/Instructor/ProblemSetList.pm:988 +msgid "no sets" +msgstr "Set yok" + +# +#: ContentGenerator/Instructor/ProblemSetList.pm:1552 +#: ContentGenerator/Instructor/UserList.pm:1088 +#: ContentGenerator/Instructor/UserList.pm:1199 +#: ContentGenerator/Instructor/UserList.pm:1227 +#: ContentGenerator/Instructor/UserList.pm:652 +msgid "no users" +msgstr "kullanıcı yok" + +# +#: ContentGenerator/ProblemSets.pm:430 +#: ContentGenerator/ProblemSets.pm:452 +msgid "now open, due " +msgstr "açık, bitiÅŸ tarihi: " + +# +#. ($self->formatDateTime($set->due_date() +#: ContentGenerator/ProblemSets.pm:408 +msgid "open: complete by %1" +msgstr "açık: %1 tarihine kadar tamamlayın" + +# +#: ContentGenerator/ProblemSets.pm:405 +msgid "over time: closed." +msgstr "süre bitti: kapalı." + +# +#: ContentGenerator/Instructor/ProblemSetList.pm:1093 +#: ContentGenerator/Instructor/ProblemSetList.pm:1194 +#: ContentGenerator/Instructor/ProblemSetList.pm:1271 +#: ContentGenerator/Instructor/ProblemSetList.pm:1612 +#: ContentGenerator/Instructor/ProblemSetList.pm:691 +#: ContentGenerator/Instructor/ProblemSetList.pm:931 +#: ContentGenerator/Instructor/ProblemSetList.pm:991 +msgid "selected sets" +msgstr "seçili setler" + +# +#: ContentGenerator/Instructor/UserList.pm:1028 +#: ContentGenerator/Instructor/UserList.pm:1090 +#: ContentGenerator/Instructor/UserList.pm:1198 +#: ContentGenerator/Instructor/UserList.pm:1300 +#: ContentGenerator/Instructor/UserList.pm:653 +#: ContentGenerator/Instructor/UserList.pm:969 +msgid "selected users" +msgstr "seçili kullanıcılar" + +# +#: ContentGenerator/Instructor/ProblemSetList.pm:762 +msgid "showing all sets" +msgstr "bütün setler gösteriliyor" + +# +#: ContentGenerator/Instructor/UserList.pm:738 +msgid "showing all users" +msgstr "bütün kullanıcılar gösteriliyor" + +# +#: ContentGenerator/Instructor/UserList.pm:747 +msgid "showing matching users" +msgstr "eÅŸleÅŸen kullanıcılar gösteriliyor" + +# +#: ContentGenerator/Instructor/ProblemSetList.pm:765 +msgid "showing no sets" +msgstr "hiçbir set gösterilmiyor" + +# +#: ContentGenerator/Instructor/UserList.pm:741 +msgid "showing no users" +msgstr "hiçbir kullanıcı gösterilmiyor" + +# +#: ContentGenerator/Instructor/ProblemSetList.pm:768 +msgid "showing selected sets" +msgstr "seçili setler gösteriliyor" + +# +#: ContentGenerator/Instructor/UserList.pm:744 +msgid "showing selected users" +msgstr "seçili kullanıcılar gösteriliyor" + +# +#: ContentGenerator/Problem.pm:1124 +msgid "submitAnswers" +msgstr "Yanıtları Gönder" + +# +#: ContentGenerator/Problem.pm:1154 +msgid "time" +msgstr "deneme hakkı" + +# +#: ContentGenerator/Problem.pm:1154 +msgid "times" +msgstr "deneme hakkı" + +# +#: ContentGenerator/Problem.pm:1161 +#: ContentGenerator/ProblemSet.pm:427 +msgid "unlimited" +msgstr "sınırsız" + +# +#: ContentGenerator/Instructor/UserList.pm:654 +msgid "users who match on selected field" +msgstr "seçili alanla eÅŸleÅŸen kullanıcılar" + +# +#: ContentGenerator/Instructor/ProblemSetList.pm:2522 +#: ContentGenerator/Problem.pm:603 +msgid "visible" +msgstr "Görünür" + +# +#: ContentGenerator/Instructor/ProblemSetList.pm:1611 +#: ContentGenerator/Instructor/ProblemSetList.pm:692 +#: ContentGenerator/Instructor/ProblemSetList.pm:930 +msgid "visible sets" +msgstr "görünür setler" + +# +#: ContentGenerator/Problem.pm:604 +#: ContentGenerator/ProblemSet.pm:86 +msgid "visible to students" +msgstr "öğrencilere görüntüleyebilir" + +# +#: ContentGenerator/Instructor/UserList.pm:1027 +#: ContentGenerator/Instructor/UserList.pm:1197 +#: ContentGenerator/Instructor/UserList.pm:1299 +#: ContentGenerator/Instructor/UserList.pm:968 +msgid "visible users" +msgstr "kullanıcılar görüntüleyebilir" + +# +#. ($self->formatDateTime($set->open_date) +#: ContentGenerator/ProblemSets.pm:420 +#: ContentGenerator/ProblemSets.pm:448 +msgid "will open on %1" +msgstr "%1 tarihinde açılacak" + +# +msgid "Add to which set?" +msgstr "Hangi sete eklenecek?" + +# +msgid "Course Information for course [_1]" +msgstr "%1 dersinin bilgileri" + +# +msgid "report bugs in this problem" +msgstr "Soruda hata olduÄŸunu bildir" + +# +msgid "Problem [_1]" +msgstr "Soru %1" + +# +msgid "Cancel Password" +msgstr "Åžifreyi iptal et" + +# +msgid "WeBWorK" +msgstr "WeBWorK" + +# +msgid "Audit" +msgstr "Denetle" + +# +msgid "Hardcopy Header for set [_1]" +msgstr "Set %1 için yazdırılabilir dosya baÅŸlığı" + +# +msgid "Report bugs in a WeBWorK question/problem using this link. The very first time you do this you will need to register with an email address so that information on the bug fix can be reported back to you." +msgstr "WebWork veya soruyla ilgili hata bildirmek için bu linki kullanın. Bunu ilk kez yaparken e-posta adresinizle kayıt olmanız gerekmektedir." + +# +msgid "Pad Fields" +msgstr "tr: Pad Fields" + +# +msgid "Set Header for set [_1]" +msgstr "Set %1 baÅŸlığı" + +# +msgid "Statistics for [_1]" +msgstr "Set %1 istatistikleri" + +# +msgid "Filter" +msgstr "Filtre" + +# +msgid "set [_1]/problem [_2]" +msgstr " set %1/soru %2" + +# +msgid "You are not authorized to modify problems." +msgstr "Sorularda deÄŸiÅŸklik yapma yetkiniz yok." + +# +msgid "hardcopy header file" +msgstr "Yazdırılabilir ödev baÅŸlığı dosyası" + +# +msgid "Write permissions have not been enabled in the templates directory. No changes can be made." +msgstr "Åžablon klasörü yazmaya kapalı. DeÄŸiÅŸiklik yapılamaz." + +# +msgid "Save [_1] and View" +msgstr "%1 Kaydet ve Görüntüle" + +# +msgid "Create" +msgstr "OluÅŸtur" + +# +msgid "Changes in this file have not yet been permanently saved." +msgstr "Dosyadaki deÄŸiÅŸiklikler henüz kalıcı olarak kaydedilmedi." + +# +msgid "Instructor Tools" +msgstr "EÄŸitmen Araçları" + +# +msgid "Score" +msgstr "Not" + +# +msgid "Show in another window" +msgstr "Yani pencerede göster" + +# +msgid "Copied auxiliary files from [_1] to new location at [_2]" +msgstr "Ek dosyalar %1 dizininden %2 dizinine kopyalandı" + +# +msgid "Unpublished" +msgstr "Yayınlanmadı" + +# +msgid "Login" +msgstr "Devam et" + +# +msgid "Email" +msgstr "E-posta" + +# +msgid "File Manager" +msgstr "Dosya Yöneticisi" + +# +msgid "Unable to change the hardcopy header for set [_1]. Unknown error." +msgstr "%1 seti için yazdırılabilir dosya baÅŸlığı deÄŸiÅŸtirilemedi. Bilinmeyen hata." + +# +msgid "Unable to make '[_1]' the set header for [_2]" +msgstr "'%1' set %2 için dosya baÅŸlığı yapılamadı" + +# +msgid "Can't determine user of temporary edit file [_1]." +msgstr "Geçici dosya %1 için kullanıcı belirlenemedi." + +# +msgid "Download a hardcopy of this homework set." +msgstr "Bu set için yazdırılabilir dosya indirin." + +# +msgid "Problem Editor" +msgstr "Soru Düzenleyici" + +# +msgid "The file '[_1]' is protected!" +msgstr "'%1' dosyası korumalı!" + +# +msgid "Top level of author information on the wiki." +msgstr "Wikide yazar bilgilerinin en üst seviyesi." + +# +msgid "Score selected set(s) and save to:" +msgstr "Seçili setleri notla ve dosyaya kaydet:" + +# +msgid "Save Password" +msgstr "Åžifreyi kaydet" + +# +msgid "The file '[_1]' is a blank problem!" +msgstr "'%1' dosyası boÅŸ bir soru!" + +# +msgid "Add as what filetype?" +msgstr "Hangi dosya tipi olarak eklenecek?" + +# +msgid "no action" +msgstr "Ä°ÅŸlem yapma" + +# +msgid "Added '[_1]' to [_2] as new set header" +msgstr "'%1' dosyası %2 dosyasına yeni set baÅŸlığı olarak eklendi" + +# +msgid "View statistics by set" +msgstr "Setlere göre istatistikleri göster" + +# +msgid "Set" +msgstr "Set" + +# +msgid "This is a blank problem template file and can not be edited directly. Use the 'Save as' action below to create a local copy of the file and add it to the current problem set." +msgstr "Bu boÅŸ bir soru ÅŸablonu ve üzerinde deÄŸiÅŸiklik yapılamaz. AÅŸağıdaki farklı kaydet iÅŸlemini kullarak dosyanın yerel bir kopyasını oluÅŸturdaktan sonra üzerinde deÄŸiÅŸiklik yapabilirsiniz." + +# +msgid "Logout" +msgstr "Çıkış" + +# +msgid "You do not have permission to view the details of this error." +msgstr "Bu hatanın detaylarını görmeye yetkiniz yok." + +# +msgid "Reverting to original file '[_1]'" +msgstr "Asıl dosyaya geri dönülüyor ('%1')" + +# +msgid "Delete" +msgstr "Sil" + +# +msgid "Wiki summary page for MathObjects" +msgstr "MathObjects için özet wiki sayfası" + +# +msgid "Your score was recorded." +msgstr "Notunuz kaydedildi." + +# +msgid "You must specify an file name in order to save a new file." +msgstr "Yeni dosya kaydetmek için dosya adı belirtmelisiniz." + +# +msgid "To edit this text you must first make a copy of this file using the 'Save as' action below." +msgstr "Bu dosyada deÄŸiÅŸiklik yapmak için önce 'Farklı Kaydet' seçeneÄŸini kullanarak kaydetmeniz lazım." + +# +msgid "unassigned problem file" +msgstr "Ödev verilmemiÅŸ soru dosyası" + +# +msgid "View statistics by student" +msgstr "Ä°statistikleri öğrencilere göre göster" + +# +msgid "Publish" +msgstr "Yayımla" + +# +msgid "Password/Email" +msgstr "Åžifre/E-posta" + +# +msgid "Cancel Edit" +msgstr "Düzenlemeyi iptal et" + +# +msgid "Math Objects" +msgstr "Matematik Nesneleri" + +# +msgid "Snippets of PG code illustrating specific techniques" +msgstr "Özel teknikleri gösteren PG yazılımı" + +# +msgid "Error copying [_1] to [_2]" +msgstr "Dosya %1, %2ye kopyalanarken hata oluÅŸtu" + +# +msgid "Hmwk Sets Editor" +msgstr "Ödev Setleri Düzenleyici" + +# +msgid "This set is [_1] students." +msgstr "Bu soru grubu öğrenciler tarafından %1." + +# +msgid "header file" +msgstr "BaÅŸlık dosyası" + +# +msgid "Include Index" +msgstr "Indeksi dahil et" + +# +msgid "Display Mode" +msgstr "Görünüm Modu" + +# +msgid "You can earn partial credit on this problem." +msgstr "Bu sorudan kısmi puan alabilirsiniz." + +# +msgid "Write permissions have not been enabled in '[_1]'. Changes must be saved to a different directory for viewing." +msgstr "'%1' için yazma izinleri kapalı. DeÄŸiÅŸiklikler önce baÅŸka bir dizine kaydedilmeli." + +# +msgid "blank problem" +msgstr "boÅŸ soru" + +# +msgid "Save Edit" +msgstr "Düzenlemeyi kaydet" + +# +msgid "Enrolled" +msgstr "Kayıtlı" + +# +msgid "Write permissions have not been enabled for '[_1]'. Changes must be saved to another file for viewing." +msgstr "'%1' için yazma izinleri kapalı. DeÄŸiÅŸiklikler önce baÅŸka bir dosyaya kaydedilmeli." + +# +msgid "Note: this problem viewer is for viewing purposes only. As of right now, testing functionality is not possible." +msgstr "Not: Bu soru görüntüleyici ÅŸu an sadece soruları görüntüleyebilir, soru çözme fonksiyonu ÅŸu an mevcut deÄŸil." + +# +msgid "Save Export" +msgstr "Dışa aktarımı kaydet" + +# +msgid "webwork" +msgstr "webwork" + +# +msgid "Problem Viewer" +msgstr "Soru Görüntüleyici" + +# +msgid "submit button clicked" +msgstr "gönder düğmesine basıldı" + +# +msgid "Student Progress for [_1]" +msgstr "%1 için öğrencilerin durumu" + +# +msgid "A new file has been created at '[_1]' with the contents below. No changes have been made to set [_2]." +msgstr "AÅŸağıdaki içerikle '%1' dosyası yaratıldı. Set %2de deÄŸiÅŸiklik yapılmadı." + +# +msgid "Your score was not recorded because this problem has not been assigned to you." +msgstr "Notunuz kaydedilmedi çünkü bu soru size yöneltilmedi." + +# +msgid "Sort" +msgstr "Sırala" + +# +msgid "Add problem" +msgstr "Soru ekle" + +# +msgid "Author Info" +msgstr "Yazar Bilgileri" + +# +msgid "Replace [_1]" +msgstr "%1in yerine koy" + +# +msgid "Help" +msgstr "Yardım" + +# +msgid "Add Users" +msgstr "Kullanıcı Ekle" + +# +msgid "Problem Source Code" +msgstr "Soru Kaynak Kodu" + +# +msgid "Test snippets of PG code in interactive lab. Good way to learn PG language." +msgstr "Interaktif labaratuarda PG test yazılımları. PG yazılım dilini öğrenmek içi iyi bir kaynak." + +# +msgid "Page generated at [_1] at [_2]" +msgstr "Sayfa %1 at %2" + +# +msgid "Import" +msgstr "İçeri Aktar" + +# +msgid "View Using Seed Number" +msgstr "Tohum numarasını kullanarak göster" + +# +msgid "Grades" +msgstr "Notlar" + +# +msgid "Published" +msgstr "Yayınlandı" + +# +msgid "Your score was not recorded because this homework set is closed." +msgstr "Notunuz kaydedilemedi çünkü bu soru grubu kapalı." + +# +msgid "The text box now contains the source of the original problem. You can recover lost edits by using the Back button on your browser." +msgstr "Åžu anda sorunun orijinal hali gösteriliyor. KaydedilmemiÅŸ deÄŸiÅŸiklikleri kurtarmak için internet tarayıcınızın \"Geri\" tuÅŸunu kullanın." + +# +msgid "The file '[_1]' is a directory!" +msgstr "'%1' dosyası bir dizin!" + +# +msgid "Save As" +msgstr "Farklı Kaydet: [TMPL]/" + +# +msgid "Documentation from source code for PG modules and macro files. Often the most up-to-date information." +msgstr "PG modülleri ve makro dosyaları hakkında bilgi dosyası. Genellikle en son bilgileri içerir." + +# +msgid "Problem Techniques" +msgstr "Soru Teknikleri" + +# +msgid "Course Configuration" +msgstr "Ders Seçenekleri" + +# +msgid "Feedback" +msgstr "Geri Bildirim" + +# +msgid "Export" +msgstr "Dışa aktar" + +# +msgid "PG mark down syntax used to format WeBWorK questions. This interactive lab can help you to learn the techniques." +msgstr "WebWork sorularında kullanılan PG kodu sözdizimi. Bu interaktif labaratuar, teknikleri öğrenmek için kullanılabilir." + +# +msgid "Record Scores for Single Sets" +msgstr "Tek set için notları kaydet" + +# +msgid "Append to end of set [_1]" +msgstr "Set %1in sonuna ekle" + +# +msgid "Unrecognized saveMode: |[_1]|. Unknown error." +msgstr "Tanımlanmamış kaydetme modu: |%1|. Bilinmeyen hata." + +# +msgid "Your score was not recorded because there was a failure in storing the problem record to the database." +msgstr "Notunuz kaydedilemedi, çünkü notun veritabanına kaydı sırasında bir hata oluÅŸtu." + +# +msgid "The hardcopy header for set [_1] has been renamed to '[_2]'." +msgstr "Set %1 için yazdırılabilir dosya baÅŸlığı '%2' olarak deÄŸiÅŸtirildi." + +# +msgid "Save as" +msgstr "Farklı kaydet" + +# +msgid "Select action below" +msgstr "AÅŸağıdan iÅŸlem seç" + +# +msgid "The file '[_1]' cannot be found." +msgstr "'%1' dosyası bulunamadı." + +# +msgid "Unable to write to '[_1]': [_2]" +msgstr "'%1' dosyasına yazılamadı: %2" + +# +msgid "Save" +msgstr "Kaydet" + +# +msgid "File '[_1]' exists. File not saved. No changes have been made. You can change the file path for this problem manually from the 'Hmwk Sets Editor' page" +msgstr "'%1' dosyası mevcut. Dosya kaydedilmedi. Hiçbir deÄŸiÅŸiklik yapılmadı. Bu sorunun dosya konumunu \"Set Düzenleyicisi\" sayfasından deÄŸiÅŸtirebilirsiniz." + +# +msgid "The set header for set [_1] has been renamed to '[_2]'." +msgstr "Set %1 baÅŸlığı '%2' olarak deÄŸiÅŸtirildi." + +# +msgid "Error: This path is already in the temporary edit directory -- no new temporary file is created. path = [_1]" +msgstr "Hata: Bu yol belirteci geçici düzenleme klasöründe mevcut -- yeni bir geçici dosya oluÅŸturuldu. yol = %1" + +# +msgid "Please use radio buttons to choose the method for saving this file. Can't recognize saveMode: |[_1]|." +msgstr "Bu dosya için kaydetme modunu seçin. Kaydetme modu tanınmadı: |%1|." + +# +msgid "Saved to file '[_1]'" +msgstr "'%1' dosyasına kaydedildi" + +# +msgid "Please specify a file to save to." +msgstr "Lütfen kaydedilecek dosyayın belirtin." + +# +msgid "The selected problem ([_1]) is not a valid problem for set [_2]." +msgstr "Seçilen problem (%1), %2 soru drubu için geçerli bir soru deÄŸil." + +# +msgid "Editing [_1] in file '[_2]'" +msgstr "'%2' dosyası içinde %1 düzenleniyor" + +# +msgid "Deleting temp file at [_1]" +msgstr "%1 klasörindeki geçici dosya siliniyor" + +# +msgid "hidden from" +msgstr "görüntülenmiyor" + +# +msgid "View student progress by set" +msgstr "Setlere göre öğrenci geliÅŸimini göster" + +# +msgid "View" +msgstr "Göster" + +# +msgid "Resources" +msgstr "Kaynaklar" + +# +msgid "Scoring Download" +msgstr "Notları Ä°ndir" + +# +msgid "Duplicate" +msgstr "ÇoÄŸalt" + +# +msgid "This path |[_1]| is not the path to a temporary edit file." +msgstr "Bu yol belirteci |%1| geçici düzenleme dosyası yolu deÄŸil." + +# +msgid "visible to" +msgstr "görüntüleniyor" + +# +msgid "Unable to change the source file path for set [_1], problem [_2]. Unknown error." +msgstr "Set %1, soru %2 için kaynak kodu yolu deÄŸiÅŸtirilemiyor. Bilinmeyen hata." + +# +msgid "Course Administration" +msgstr "Ders Yönetimi" + +# +msgid "Added [_1] to [_2] as problem [_3]" +msgstr "%1 sorusu %2 setine soru %3 olarak eklendi" + +# +msgid "Options Information" +msgstr "Seçenek Bilgileri" + +# +msgid "Proctor" +msgstr "Gözetmen" + +# +msgid "Student Progress" +msgstr "Öğrenci GeliÅŸimi" + +# +msgid "Revert" +msgstr "Ters çevir" + +# +msgid "Statistics" +msgstr "Ä°statistikler" + +# +msgid "Save as new independent problem" +msgstr "Yeni bir soru olarak kaydet" + +# +msgid "Scoring Tools" +msgstr "Notlama Araçları" + +# +msgid "The source file for 'set [_1] / problem [_2]' has been changed from [_3] to '[_4]'." +msgstr "'set %1 / soru %2' için kaynak kodu %3'ten '%4'e deÄŸiÅŸtirildi." + +# +msgid "Cancel Export" +msgstr "Dışa aktarımı iptal et" + +# +msgid "View student progress by student" +msgstr "tr: View student progress by student" + +# +msgid "Revert to [_1]" +msgstr "%1'e geri dön" + +# +msgid "Classlist Editor" +msgstr "Sınıf Listesi Düzenleyici" + +# +msgid "Add" +msgstr "Ekle" + +# +msgid "the original path to the file is [_1]" +msgstr "Dosyanın özgün yol belirteci: %1" + +# +msgid "The path to the original file should be absolute" +msgstr "Sorunun özgün yolu kesin olmalı" + +# +msgid "To edit this text you must use the 'Save AS' action below to save it to another file." +msgstr "Bu metni düzenlemek için aÅŸağıdaki 'Farklı Kaydet' iÅŸlemini kullanarak baÅŸka bir dosya olarak kaydetmeniz gerekiyor." + +# +msgid "Error: The original file [_1] cannot be read." +msgstr "Hata: %1 dosyası okunamıyor." + +# +msgid "Unable to change the set header for set [_1]. Unknown error." +msgstr "Set %1'in baÅŸlığı deÄŸiÅŸtirilemedi. Bilinmeyen hata." + +# +msgid "options information" +msgstr "Seçenek bilgileri" + +# +msgid "The selected problem([_1]) is not a valid problem for set [_2]." +msgstr "Seçili soru (%1) set %2 için geçerli deÄŸil." + +# +msgid "Unknown file type" +msgstr "Bilinmeyen dosya tipi" + +# +msgid "course information" +msgstr "Ders Bilgileri" + +# +msgid "Change User Options" +msgstr "Kullanıcı bilgilerini güncelle" + +# +msgid "Drop" +msgstr "Bırak" + +# +msgid "Solutions" +msgstr "Çözümleri göster" + +# +msgid "Show:" +msgstr "Göster" + +# +msgid "WeBWorK Assignment [_1] is due : [_2]." +msgstr "WebWork Ödevi [_1] teslim tarihi: [_2]" + +# +msgid "Hardcopy Generator" +msgstr "Çıktı Dosyası BaÅŸlığı" + +# +msgid "This set is visible to students." +msgstr "Bu soru grubu öğrenciler tarafından %1." + +# +msgid "View equations as" +msgstr "EÅŸitlikler ne ÅŸekilde gösterilecek" + +# +msgid "From:" +msgstr "Gönderen:" + +# +msgid "Generate Hardcopy" +msgstr "Yazdırılabilir dosya oluÅŸtur" + +# +msgid "You may choose to show any of the following data. Correct answers and solutions are only available after the answer date of the homework set." +msgstr "AÅŸağıdaki bilgilerden herhangi bir tanesini görüntülemeyi seçebilirsiniz. DoÄŸru cevaplar ve çözümler ödev setinin cevaplarının açıklanma tarihinden itibaren görülebilir." + +# +msgid "E-mail Instructor" +msgstr "EÄŸitmene E-posta" + +# +msgid "Send E-mail:" +msgstr "E-posta yolla:" + +# +msgid "Cancel Email" +msgstr "Düzenlemeyi iptal et" + +# +msgid "E-mail:" +msgstr "E-posta" + +# +msgid "Correct answers" +msgstr "DoÄŸru yanıtları göster" + +# +msgid "Use this form to report to your professor a problem with the WeBWorK system or an error in a problem you are attempting. Along with your message, additional information about the state of the system will be included." +msgstr "Bu formu eÄŸitmeninize WebWork sistemiyle ya da ÅŸu anda yapmakta olduÄŸunuz soruda bir hata olduÄŸunu bildirmek için kullanabilirsiniz. " + +# +msgid "Student answers" +msgstr "Yanıtları Gönder" + +# +msgid "Hints" +msgstr "Ä°puçlarını göster" + +# +msgid "Hardcopy Format:" +msgstr "Çıktı Dosyası BaÅŸlığı" + diff --git a/lib/WeBWorK/URLPath.pm b/lib/WeBWorK/URLPath.pm index 021df7fd80..fb9f5ec787 100644 --- a/lib/WeBWorK/URLPath.pm +++ b/lib/WeBWorK/URLPath.pm @@ -1,6 +1,6 @@ ################################################################################ # WeBWorK Online Homework Delivery System -# Copyright © 2000-2007 The WeBWorK Project, http://openwebwork.sf.net/ +# Copyright © 2000-2007 The WeBWorK Project, http://openwebwork.sf.net/ # $CVSHeader: webwork2/lib/WeBWorK/URLPath.pm,v 1.36 2008/04/29 19:27:34 sh002i Exp $ # # This program is free software; you can redistribute it and/or modify it under @@ -46,6 +46,7 @@ PLEASE FOR THE LOVE OF GOD UPDATE THIS IF YOU CHANGE THE HEIRARCHY BELOW!!! course_admin /admin/ -> logout, options, instructor_tools html2xml /html2xml/ + instructorXMLHandler /instructorXMLHandler/ set_list /$courseID/ equation_display /$courseID/equation/ @@ -67,16 +68,26 @@ PLEASE FOR THE LOVE OF GOD UPDATE THIS IF YOU CHANGE THE HEIRARCHY BELOW!!! instructor_user_detail /$courseID/instructor/users/$userID/ instructor_sets_assigned_to_user /$courseID/instructor/users/$userID/sets/ + instructor_user_list2 /$courseID/instructor/users2/ + instructor_user_detail2 /$courseID/instructor/users2/$userID/ #not created yet + instructor_sets_assigned_to_user2 /$courseID/instructor/users2/$userID/sets/ #not created yet + + instructor_set_list /$courseID/instructor/sets/ instructor_set_detail /$courseID/instructor/sets/$setID/ instructor_users_assigned_to_set /$courseID/instructor/sets/$setID/users/ + instructor_set_list2 /$courseID/instructor/sets2/ + instructor_set_detail2 /$courseID/instructor/sets2/$setID/ #not created yet + instructor_users_assigned_to_set2 /$courseID/instructor/sets2/$setID/users/ #not created yet + instructor_add_users /$courseID/instructor/add_users/ instructor_set_assigner /$courseID/instructor/assigner/ instructor_file_transfer /$courseID/instructor/files/ instructor_file_manager /$courseID/instructor/file_manager/ instructor_set_maker /$courseID/instructor/setmaker/ instructor_set_maker2 /$courseID/instructor/setmaker2/ + instructor_set_maker3 /$courseID/instructor/setmaker3/ instructor_get_target_set_problems /$courseID/instructor/GetTargetSetProblems/ instructor_get_library_set_problems /$courseID/instructor/GetLibrarySetProblems/ instructor_config /$courseID/instructor/config/ @@ -86,6 +97,11 @@ PLEASE FOR THE LOVE OF GOD UPDATE THIS IF YOU CHANGE THE HEIRARCHY BELOW!!! instructor_problem_editor_withset /$courseID/instructor/pgProblemEditor/$setID/ instructor_problem_editor_withset_withproblem /$courseID/instructor/pgProblemEditor/$setID/$problemID/ + + instructor_problem_editor2 /$courseID/instructor/pgProblemEditor2/ + instructor_problem_editor2_withset /$courseID/instructor/pgProblemEditor2/$setID/ + instructor_problem_editor2_withset_withproblem + /$courseID/instructor/pgProblemEditor2/$setID/$problemID/ instructor_scoring /$courseID/instructor/scoring/ instructor_scoring_download /$courseID/instructor/scoringDownload/ @@ -114,7 +130,7 @@ our %pathTypes = ( root => { name => 'WeBWorK', parent => '', - kids => [ qw/course_admin html2xml set_list / ], + kids => [ qw/course_admin html2xml instructorXMLHandler set_list / ], match => qr|^/|, capture => [ qw// ], produce => '/', @@ -140,7 +156,15 @@ our %pathTypes = ( produce => 'html2xml/', display => 'WeBWorK::ContentGenerator::renderViaXMLRPC', }, - + instructorXMLHandler => { + name => 'instructorXMLHandler', + parent => 'root', + kids => [ qw// ], + match => qr|^instructorXMLHandler/|, + capture => [ qw// ], + produce => 'instructorXMLHandler/', + display => 'WeBWorK::ContentGenerator::instructorXMLHandler', + }, set_list => { name => '$courseID', parent => 'root', @@ -269,9 +293,12 @@ our %pathTypes = ( instructor_tools => { name => 'Instructor Tools', parent => 'set_list', - kids => [ qw/instructor_user_list instructor_set_list instructor_add_users + kids => [ qw/instructor_user_list instructor_user_list2 instructor_set_list instructor_set_list2 + instructor_add_users instructor_set_assigner instructor_file_manager - instructor_problem_editor instructor_set_maker instructor_set_maker2 instructor_get_target_set_problems instructor_get_library_set_problems instructor_compare + instructor_problem_editor instructor_problem_editor2 + instructor_set_maker instructor_set_maker2 instructor_set_maker3 + instructor_get_target_set_problems instructor_get_library_set_problems instructor_compare instructor_config instructor_scoring instructor_scoring_download instructor_mail_merge instructor_answer_log instructor_preflight instructor_statistics @@ -294,6 +321,15 @@ our %pathTypes = ( produce => 'users/', display => 'WeBWorK::ContentGenerator::Instructor::UserList', }, + instructor_user_list2 => { + name => 'Classlist Editor2', + parent => 'instructor_tools', + kids => [ qw/instructor_user_detail/ ], + match => qr|^users2/|, + capture => [ qw// ], + produce => 'users2/', + display => 'WeBWorK::ContentGenerator::Instructor::UserList2', + }, instructor_user_detail => { name => 'Sets assigned to $userID', parent => 'instructor_user_list', @@ -324,6 +360,15 @@ our %pathTypes = ( produce => 'sets/', display => 'WeBWorK::ContentGenerator::Instructor::ProblemSetList', }, + instructor_set_list2 => { + name => 'Hmwk Sets Editor2', + parent => 'instructor_tools', + kids => [ qw/instructor_set_detail/ ], + match => qr|^sets2/|, + capture => [ qw// ], + produce => 'sets2/', + display => 'WeBWorK::ContentGenerator::Instructor::ProblemSetList2', + }, instructor_set_detail => { name => 'Set Detail for set $setID', parent => 'instructor_set_list', @@ -399,6 +444,15 @@ our %pathTypes = ( capture => [ qw// ], produce => 'setmaker2/', display => 'WeBWorK::ContentGenerator::Instructor::SetMaker2', + }, + instructor_set_maker3 => { + name => 'Library Browser 3', + parent => 'instructor_tools', + kids => [ qw// ], + match => qr|^setmaker3/|, + capture => [ qw// ], + produce => 'setmaker3/', + display => 'WeBWorK::ContentGenerator::Instructor::SetMaker3', }, instructor_get_target_set_problems => { name => 'Get Target Set Problems', @@ -436,6 +490,15 @@ our %pathTypes = ( produce => 'pgProblemEditor/', display => 'WeBWorK::ContentGenerator::Instructor::PGProblemEditor', }, + instructor_problem_editor2 => { + name => 'Problem Editor2', + parent => 'instructor_tools', + kids => [ qw/instructor_problem_editor2_withset/ ], + match => qr|^pgProblemEditor2/|, + capture => [ qw// ], + produce => 'pgProblemEditor2/', + display => 'WeBWorK::ContentGenerator::Instructor::PGProblemEditor2', + }, instructor_problem_editor_withset => { name => '$setID', parent => 'instructor_problem_editor', @@ -445,6 +508,15 @@ our %pathTypes = ( produce => '$setID/', display => undef, }, + instructor_problem_editor2_withset => { + name => '$setID', + parent => 'instructor_problem_editor2', + kids => [ qw/instructor_problem_editor2_withset_withproblem/ ], + match => qr|^([^/]+)/|, + capture => [ qw/setID/ ], + produce => '$setID/', + display => undef, + }, instructor_problem_editor_withset_withproblem => { name => '$problemID', parent => 'instructor_problem_editor_withset', @@ -454,6 +526,15 @@ our %pathTypes = ( produce => '$problemID/', display => 'WeBWorK::ContentGenerator::Instructor::PGProblemEditor', }, + instructor_problem_editor2_withset_withproblem => { + name => '$problemID', + parent => 'instructor_problem_editor2_withset', + kids => [ qw// ], + match => qr|^([^/]+)/|, + capture => [ qw/problemID/ ], + produce => '$problemID/', + display => 'WeBWorK::ContentGenerator::Instructor::PGProblemEditor2', + }, instructor_scoring => { name => 'Scoring Tools', parent => 'instructor_tools', diff --git a/lib/WebworkClient.pm b/lib/WebworkClient.pm index 945b722a27..bba400691c 100755 --- a/lib/WebworkClient.pm +++ b/lib/WebworkClient.pm @@ -87,7 +87,18 @@ use constant TRANSPORT_METHOD => 'XMLRPC::Lite'; use constant REQUEST_CLASS => 'WebworkXMLRPC'; # WebworkXMLRPC is used for soap also!! use constant REQUEST_URI => 'mod_xmlrpc'; +our $UNIT_TESTS_ON = 0; + +# error formatting +sub format_hash_ref { + my $hash = shift; + warn "Use a hash reference" unless ref($hash) =~/HASH/; + return join(" ", map {$_="--" unless defined($_);$_ } %$hash),"\n"; +} + sub new { + my $invocant = shift; + my $class = ref $invocant || $invocant; my $self = { output => '', encodedSource => '', @@ -99,9 +110,10 @@ sub new { AnSwEr0002 => '', AnSwEr0003 => '', }, + @_, # options and overloads }; - bless $self; + bless $self, $class; } @@ -112,15 +124,77 @@ our $result; # this code is identical between renderProblem.pl and renderViaXMLRPC.pm ################################################## + + sub xmlrpcCall { - my $self = shift; + my $self = shift; + my $command = shift; + my $input = shift||{}; + + $command = 'listLibraries' unless defined $command; + my $input2 = $self->setInputTable(); + $input = {%$input2, %$input}; + + my $requestResult; + eval { + $requestResult= TRANSPORT_METHOD + #->uri('http://'.HOSTURL.':'.HOSTPORT.'/'.REQUEST_CLASS) + #-> proxy(PROTOCOL.'://'.HOSTURL.':'.HOSTPORT.'/'.REQUEST_URI); + -> proxy(($self->url).'/'.REQUEST_URI); + }; + print STDERR "WebworkClient: Initiating xmlrpc request to url ",($self->url).'/'.REQUEST_URI, " \n Error: $@\n" if $@; + + + if ($UNIT_TESTS_ON) { + print STDERR "WebworkClient.pm ".__LINE__." xmlrpcCall sent to ", $self->{url},"\n"; + print STDERR "WebworkClient.pm ".__LINE__." xmlrpcCall issued with command $command\n"; + print STDERR "WebworkClient.pm ".__LINE__." input is: ",join(" ", %$input),"\n"; + print STDERR "WebworkClient.pm ".__LINE__." xmlrpcCall $command initiated webwork webservice object $requestResult\n"; + } + + + local( $result); + # use eval to catch errors + #print STDERR "WebworkClient: issue command ", REQUEST_CLASS.'.'.$command, " ",join(" ", %$input),"\n"; + eval { $result = $requestResult->call(REQUEST_CLASS.'.'.$command, $input) }; + print STDERR "There were a lot of errors\n" if $@; + print "Errors: \n $@\n End Errors\n" if $@; + + unless (ref($result) and $result->fault) { + + if (ref($result->result())=~/HASH/ and defined($result->result()->{text}) ) { + $result->result()->{text} = decode_base64($result->result()->{text}); + } + #print pretty_print($result->result()),"\n"; #$result->result() + $self->{output}= $result->result(); + return $result->result(); + } else { + print STDERR 'Error message for ', + join( ', ', + "command:", + $command, + "\nfaultcode:", + $result->faultcode, + "\nfaultstring:", + $result->faultstring, "\nEnd error message\n" + ); + return undef; + } +} + +sub jsXmlrpcCall { + my $self = shift; my $command = shift; + my $input = shift; $command = 'listLibraries' unless $command; + if ($UNIT_TESTS_ON) { + print STDERR "WebworkClient.pm ".__LINE__." jsXmlrpcCall issued with command $command\n"; + } + print "the command was $command"; my $requestResult = TRANSPORT_METHOD -> proxy(($self->url).'/'.REQUEST_URI); - my $input = $self->setInputTable(); local( $result); # use eval to catch errors eval { $result = $requestResult->call(REQUEST_CLASS.'.'.$command,$input) }; @@ -129,15 +203,16 @@ sub xmlrpcCall { print STDERR "Errors: \n $@\n End Errors\n" ; return 0 #failure } - + print "hmm $result"; unless (ref($result) and $result->fault) { my $rh_result = $result->result(); - #print STDERR pretty_print_rh($rh_result); + print "\n success \n"; + print pretty_print($rh_result->{'ra_out'}); $self->{output} = $rh_result; #$self->formatRenderedProblem($rh_result); return 1; # success } else { - $self->{output} = 'Error in xmlrpcCall to server: '. join( ",\n ", + $self->{output} = 'Error from server: '. join( ",\n ", $result->faultcode, $result->faultstring); return 0; #failure @@ -277,7 +352,7 @@ sub environment { externalGif2EpsPath=>'not defined', externalPng2EpsPath=>'not defined', externalTTHPath=>'/usr/local/bin/tth', - fileName=>'set0/prob1a.pg', + fileName=>'WebworkClient.pm:: define fileName in environment', formattedAnswerDate=>'6/19/00', formattedDueDate=>'6/19/00', formattedOpenDate=>'6/19/00', @@ -303,7 +378,7 @@ sub environment { openDate=> '3014438528', permissionLevel =>10, PRINT_FILE_NAMES_FOR => [ 'gage'], - probFileName => 'set0/prob1a.pg', + probFileName => 'WebworkClient.pm:: define probFileName in environment', problemSeed => 1234, problemValue =>1, probNum => 13, @@ -360,12 +435,29 @@ sub formatAnswerRow { $row; } +sub formatRenderedLibraries { + my $self = shift; + #my @rh_result = @{$self->{output}}; # wrap problem in formats + my %rh_result = %{$self->{output}}; + my $result = ""; + foreach my $key (sort keys %rh_result) { + $result .= "$key"; + $result .= $rh_result{$key}; + } + return $result; +} + sub formatRenderedProblem { my $self = shift; - my $rh_result = $self->{output}; # wrap problem in formats - my $problemText = decode_base64($rh_result->{text}); + my $rh_result = $self->{output}|| {}; # wrap problem in formats + my $problemText = "No output from rendered Problem"; + if (ref($rh_result) and $rh_result->{text} ) { + $problemText = $rh_result->{text}; + } else { + $problemText = "Unable to decode problem text",format_hash_ref($rh_result); + } my $rh_answers = $rh_result->{answers}; - my $encodedSource = $self->{encodedSource}||'foobar'; + my $encodedSource = $self->{encodedSource}||'encodedSourceIsMissing'; my $warnings = ''; ################################################# # regular Perl warning messages generated with warn @@ -411,6 +503,9 @@ sub formatRenderedProblem { my $XML_URL = $self->url; my $FORM_ACTION_URL = $self->{form_action_url}; + my $courseID = $self->{courseID}; + my $userID = $self->{userID}; + my $session_key = $rh_result->{session_key}; my $problemTemplate = < + + + +


diff --git a/lib/WebworkWebservice.pm b/lib/WebworkWebservice.pm index 23809ea177..27482c46e6 100644 --- a/lib/WebworkWebservice.pm +++ b/lib/WebworkWebservice.pm @@ -1,6 +1,5 @@ #!/user/bin/perl -w - BEGIN { @@ -27,6 +26,7 @@ BEGIN { eval "use lib '$webwork_directory/lib'"; die $@ if $@; eval "use WeBWorK::CourseEnvironment"; die $@ if $@; my $seed_ce = new WeBWorK::CourseEnvironment({ webwork_dir => $webwork_directory }); + die "Can't create seed course environment for webwork in $webwork_directory" unless ref($seed_ce); my $webwork_url = $seed_ce->{webwork_url}; my $pg_dir = $seed_ce->{pg_dir}; eval "use lib '$pg_dir/lib'"; die $@ if $@; @@ -37,18 +37,23 @@ BEGIN { ############################################################################### - $WebworkWebservice::PASSWORD = 'xmluser'; # default password - $WebworkWebservice::COURSENAME = 'gage_course'; # default course + $WebworkWebservice::SITE_PASSWORD = 'xmluser'; # default password + $WebworkWebservice::COURSENAME = 'the-course-should-be-determined-at-run-time'; # default course + + + } use strict; use warnings; +use WeBWorK::Localize; +our $UNIT_TESTS_ON = 0; + +# error formatting - - ############################################################################### ############################################################################### @@ -101,38 +106,69 @@ sub pretty_print_rh { use WebworkWebservice::RenderProblem; use WebworkWebservice::LibraryActions; use WebworkWebservice::MathTranslators; +use WebworkWebservice::SetActions; ############################################################################### package WebworkXMLRPC; use base qw(WebworkWebservice); use WeBWorK::Utils qw(runtime_use writeTimingLogEntry); - +sub format_hash_ref { + my $hash = shift; + my $out_str=""; + my $count =4; + foreach my $key ( sort keys %$hash) { + my $value = defined($hash->{$key})? $hash->{$key}:"--"; + $out_str.= " $key=>$value "; + $count--; + unless($count) { $out_str.="\n ";$count =4;} + } + $out_str; +} ########################################################################### -# authentication +# authentication and authorization ########################################################################### sub initiate_session { my ($invocant, @args) = @_; my $class = ref $invocant || $invocant; + ######### trace commands ###### + my @caller = caller(1); # caller data + my $calling_function = $caller[3]; + #print STDERR "\n\nWebworkWebservice.pm ".__LINE__." initiate_session called from $calling_function\n"; + ############################### + my $rh_input = $args[0]; - my $user_id = $rh_input->{userID}; - my $password = $rh_input->{password}; + # print STDERR "input from the form is ", format_hash_ref($rh_input), "\n"; + + # obtain input from the hidden fields on the HTML page + my $user_id = $rh_input ->{userID}; + my $session_key = $rh_input ->{session_key}; my $courseName = $rh_input ->{courseID}; - my $ce = $class->create_course_environment($courseName); - my $db = new WeBWorK::DB($ce->{dbLayout}); + my $password = $rh_input ->{password}; + my $ce = $class->create_course_environment($courseName); + my $db = new WeBWorK::DB($ce->{dbLayout}); my $language= $ce->{language} || "en"; - my $language_handle = WeBWorK::Localize->get_handle($language) ; + my $language_handle = WeBWorK::Localize::getLoc($language) ; my $user_authen_module = WeBWorK::Authen::class($ce, "user_module"); runtime_use $user_authen_module; - + +if ($UNIT_TESTS_ON) { + print STDERR "WebworkWebservice.pl ".__LINE__." site_password is " , $rh_input->{site_password},"\n"; + print STDERR "WebworkWebservice.pl ".__LINE__." courseID is " , $rh_input->{courseID},"\n"; + print STDERR "WebworkWebservice.pl ".__LINE__." userID is " , $rh_input->{userID},"\n"; + print STDERR "WebworkWebservice.pl ".__LINE__." session_key is " , $rh_input->{session_key},"\n"; +} + +# This structure needs to mimic the structure expected by Authen my $self = { courseName => $courseName, user_id => $user_id, - password => $password, + password => $password, + session_key => $session_key, ce => $ce, db => $db, language_handle => $language_handle, @@ -145,8 +181,16 @@ sub initiate_session { my $authen = $user_authen_module->new($self); my $authz = WeBWorK::Authz->new($self); - $self->{authen} = $authen; + $self->{authen} = $authen; $self->{authz} = $authz; + + + if ($UNIT_TESTS_ON) { + print STDERR "WebworkWebservice.pm ".__LINE__." initiate data:\n "; + print STDERR "class type is ", $class, "\n"; + print STDERR "Self has type ", ref($self), "\n"; + print STDERR "self has data: \n", format_hash_ref($self), "\n"; + } # we need to trick some of the methods within the webwork framework # since we are not coming in with a standard apache request # FIXME: can/should we change this???? @@ -155,7 +199,6 @@ sub initiate_session { # # - # now, here's the problem... WeBWorK::Authen looks at $r->params directly, whereas we # need to look at $user and $sent_pw. this is a perfect opportunity for a mixin, i think. my $authenOK; @@ -171,7 +214,21 @@ sub initiate_session { $self->{authenOK} = $authenOK; $self->{authzOK} = $authz->hasPermissions($user_id, "access_instructor_tools"); - return $self; + +# Update the credentials -- in particular the session_key may have changed. + $self->{session_key} = $authen->{session_key}; + + if ($UNIT_TESTS_ON) { + print STDERR "WebworkWebservice.pm ".__LINE__." authentication for ",$self->{user_id}, " in course ", $self->{courseName}, " is = ", $self->{authenOK},"\n"; + print STDERR "WebworkWebservice.pm ".__LINE__."authorization as instructor for ", $self->{user_id}, " is ", $self->{authzOK},"\n"; + print STDERR "WebworkWebservice.pm ".__LINE__." authentication contains ", format_hash_ref($authen),"\n"; + print STDERR "self has new data \n", format_hash_ref($self), "\n"; + } + # Is there a way to transmit a number as well as a message? + # Could be useful for nandling errors. + die "Could not authenticate user $user_id with key $session_key " unless $self->{authenOK}; + die "User $user_id does not have professor privileges in this course $courseName " unless $self->{authzOK}; + return $self; } @@ -189,7 +246,7 @@ sub create_course_environment { {webwork_dir => $WebworkWebservice::WW_DIRECTORY, courseName => $courseName }); - # error messages + #warn "Unable to find environment for course: |$courseName|" unless ref($ce); return ($ce); } @@ -204,13 +261,19 @@ sub db { my $self = shift; $self->{db}; } +sub param { # imitate get behavior of the request object params method + my $self =shift; + my $param = shift; + $self->{$param}; +} sub authz { my $self = shift; $self->{authz}; } sub maketext { my $self = shift; - $self->{language_handle}->maketext(@_); + #$self->{language_handle}->maketext(@_); + &{ $self->{language_handle} }(@_); } @@ -218,10 +281,10 @@ sub get_credentials { my $self = shift; # self is an Authen object it contains an object r which is the WebworkXMLRPC object # confusing isn't it? - $self->{user_id} = $self->{r}->{user_id}; - $self->{session_key} = ""; - $self->{password} = $self->{r}->{password}; - $self->{login_type} = "normal"; + $self->{user_id} = $self->{r}->{user_id}; + $self->{session_key} = $self->{r}->{session_key}; + $self->{password} = $self->{r}->{password}; #"the-pass-word-can-be-provided-via-a-pop-up--call-back"; + $self->{login_type} = "normal"; $self->{credential_source} = "params"; return 1; } @@ -234,36 +297,100 @@ sub check_authorization { } - +sub do { # process and return result + # make sure that credentials are returned + # for every call + # $result -> xmlrpcCall(command, in); + # $result->{output}->{foo} is defined for foo = courseID userID and session_key + my $self = shift; + my $result = shift; + $result->{session_key} = $self->{session_key}; + $result->{userID} = $self->{user_id}; + $result->{courseID} = $self->{courseName}; + return($result); +} # respond to xmlrpc requests +# Add routines for handling errors if the authentication fails or if the authorization is not appropriate. + +sub searchLib { + my $class = shift; + my $in = shift; + my $self = $class->initiate_session($in); + #warn "\n incoming request to listLib: class is ",ref($self) if $UNIT_TESTS_ON ; + return $self->do( WebworkWebservice::LibraryActions::searchLib($self, $in) ); +} + sub listLib { my $class = shift; my $in = shift; my $self = $class->initiate_session($in); - warn "incomiing request to listLib: class is ",ref($self); - warn "authentication for ",$self->{user_id}, " in course ", $self->{courseName}, " is = ", $self->{authenOK}; - warn "authorization as instructor for ", $self->{user_id}, " is ", $self->{authzOK}; - return( WebworkWebservice::LibraryActions::listLib($in) ); + #warn "\n incoming request to listLib: class is ",ref($self) if $UNIT_TESTS_ON ; + return $self->do( WebworkWebservice::LibraryActions::listLib($self, $in) ); } sub listLibraries { # returns a list of libraries for the default course - my $self = shift; + my $class = shift; my $in = shift; - return( WebworkWebservice::LibraryActions::listLibraries($in) ); + my $self = $class->initiate_session($in); + #warn "incoming request to listLibraries: class is ",ref($self) if $UNIT_TESTS_ON ; + return $self->do( WebworkWebservice::LibraryActions::listLibraries($self, $in) ); +} +sub listSets { + my $class = shift; + my $in = shift; + my $self = $class->initiate_session($in); + return $self->do(WebworkWebservice::SetActions::listLocalSets($self)); +} +sub listSetProblems { + my $class = shift; + my $in = shift; + my $self = $class->initiate_session($in); + return $self->do(WebworkWebservice::SetActions::listLocalSetProblems($self, $in)); +} + +sub createNewSet{ + my $class = shift; + my $in = shift; + my $self = $class->initiate_session($in); + return $self->do(WebworkWebservice::SetActions::createNewSet($self, $in)); +} + +sub reorderProblems{ + my $class = shift; + my $in = shift; + my $self = $class->initiate_session($in); + return $self->do(WebworkWebservice::SetActions::reorderProblems($self, $in)); +} + +sub addProblem { + my $class = shift; + my $in = shift; + my $self = $class->initiate_session($in); + return $self->do(WebworkWebservice::SetActions::addProblem($self, $in)); +} + +sub deleteProblem{ + my $class = shift; + my $in = shift; + my $self = $class->initiate_session($in); + return $self->do(WebworkWebservice::SetActions::deleteProblem($self, $in)); } sub renderProblem { - my $self = shift; + my $class = shift; my $in = shift; - return( WebworkWebservice::RenderProblem::renderProblem($in) ); + my $self = $class->initiate_session($in); + return $self->do( WebworkWebservice::RenderProblem::renderProblem($self,$in) ); } sub readFile { - my $self = shift; + my $class = shift; my $in = shift; - return( WebworkWebservice::LibraryActions::readFile($in) ); + my $self = $class->initiate_session($in); + return $self->do( WebworkWebservice::LibraryActions::readFile($self,$in) ); } sub tex2pdf { - my $self = shift; - my $in = shift; - return( WebworkWebservice::MathTranslators::tex2pdf($in) ); + my $class = shift; + my $in = shift; + my $self = $class->initiate_session($in); + return $self->do( WebworkWebservice::MathTranslators::tex2pdf($self,$in) ); } # -- SOAP::Lite -- guide.soaplite.com -- Copyright (C) 2001 Paul Kulchenko -- @@ -344,33 +471,6 @@ sub readFile { # } -# sub tth { -# shift if UNIVERSAL::isa($_[0] => __PACKAGE__); -# my $in = shift; -# my $tthpath = "/usr/local/bin/tth"; -# my $out; -# $inputString = "</dev/null"; -# #it's not clear why another command is needed. -# -# if (-x $tthpath ) { -# my $tthcmd = "$tthpath -L -f5 -r 2>/dev/null " . $inputString; -# if (open(TTH, "$tthcmd |")) { -# local($/); -# $/ = undef; -# $out = ; -# $/ = "\n"; -# close(TTH); -# }else { -# $out = "
there has been an error in executing $tthcmd
"; -# } -# } else { -# $out = "
Can't execute the program tth at |$tthpath|
"; -# } -# -# #return "\r\n\r\n" . $out . "\r\n\r\n"; -# return $out; -# -# } package Filter; diff --git a/lib/WebworkWebservice/LibraryActions.pm b/lib/WebworkWebservice/LibraryActions.pm index fed487e000..1db1b5047f 100644 --- a/lib/WebworkWebservice/LibraryActions.pm +++ b/lib/WebworkWebservice/LibraryActions.pm @@ -11,7 +11,9 @@ #use lib '/home/gage/webwork/webwork-modperl/lib'; package WebworkWebservice::LibraryActions; + use WebworkWebservice; +use WeBWorK::Utils::ListingDB; use base qw(WebworkWebservice); use strict; @@ -33,43 +35,67 @@ our $WW_DIRECTORY = $WebworkWebservice::WW_DIRECTORY; our $PG_DIRECTORY = $WebworkWebservice::PG_DIRECTORY; our $COURSENAME = $WebworkWebservice::COURSENAME; our $HOST_NAME = $WebworkWebservice::HOST_NAME; -our $PASSWORD = $WebworkWebservice::PASSWORD; -our $ce = WeBWorK::CourseEnvironment->new($WW_DIRECTORY, "", "", $COURSENAME); +our $PASSWORD = "we-don't-need-no-stinking-passowrd"; + + +#our $ce = WeBWorK::CourseEnvironment->new($WW_DIRECTORY, "", "", $COURSENAME); # warn "library ce \n ", WebworkWebservice::pretty_print_rh($ce); # warn "LibraryActions is ready"; ############################################## # Obtain basic information about local libraries ############################################## - my %prob_libs = %{$ce->{courseFiles}->{problibs} }; +# my %prob_libs = %{$ce->{courseFiles}->{problibs} }; #warn pretty_print_rh(\%prob_libs); # replace library names with full paths - my $templateDir = $ce->{courseDirs}->{templates}; +# my $templateDir = $ce->{courseDirs}->{templates}; # warn "template Directory is $templateDir"; - foreach my $key (keys %prob_libs) { - $prob_libs{$key} = "$templateDir/$key"; - } +# foreach my $key (keys %prob_libs) { +# $prob_libs{$key} = "$templateDir/$key"; +# } #warn "prob libraries", WebworkWebservice::pretty_print_rh(\%prob_libs); sub listLibraries { # list the problem libraries that are available. + my $self = shift; my $rh = shift; - return [sort keys %prob_libs]; + #my $my_ce = $self->{ce}; + my %libraries = %{$self->{ce}->{courseFiles}->{problibs}}; + + my $templateDirectory = $self->{ce}->{courseDirs}{templates}; + + foreach my $key (keys %libraries) { + $libraries{$key} = "$templateDirectory/$key"; + } + + my @outListLib = sort keys %libraries; + my $out = {}; + $out->{ra_out} = \@outListLib; + $out->{text} = encode_base64("success"); + return $out; } use File::stat; sub readFile { - my $rh = shift; + my $self = shift; + my $rh = shift; local($|)=1; my $out = {}; my $filePath = $rh->{filePath}; - unless ($rh->{pw} eq $PASSWORD ) { - $out->{error} =404; - return($out); - } - if ( defined($prob_libs{$rh->{library_name}} ) ) { - $filePath = $prob_libs{$rh->{library_name}} .'/'. $filePath; + + my %libraries = %{$self->{ce}->{courseFiles}->{problibs}}; + + my $templateDirectory = $self->{ce}->{courseDirs}{templates}; + + foreach my $key (keys %libraries) { + $libraries{$key} = "$templateDirectory/$key"; + } + + + + if ( defined($libraries{$rh->{library_name}} ) ) { + $filePath = $libraries{$rh->{library_name}} .'/'. $filePath; } else { $out->{error} = "Could not find library:".$rh->{library_name}.":"; return($out); @@ -92,82 +118,182 @@ sub readFile { return($out); } - - use File::Find; +#idea from http://www.perlmonks.org/index.pl?node=How%20to%20map%20a%20directory%20tree%20to%20a%20perl%20hash%20tree +sub build_tree { + my $node = $_[0] = {}; + my @s; + find({wanted=>sub { + # fixed 'if' => 'while' -- thanks Rudif + unless ($File::Find::dir =~/.svn/ || $File::Find::name =~/.svn/) { + $node = (pop @s)->[1] while @s and $File::Find::dir ne $s[-1][0]; + return $node->{$_} = -s if -f; + push @s, [ $File::Find::name, $node ]; + $node = $node->{$_} = {}; + } + }, follow_fast=>1}, $_[1]); + $_[0]{$_[1]} = delete $_[0]{'.'}; +} + sub listLib { + my $self = shift; my $rh = shift; my $out = {}; - my $dirPath; - unless ($rh->{pw} eq $PASSWORD ) { - $out->{error}=" 404 $PASSWORD and ".$rh->{pw}; - return($out); - } - - if ( defined($prob_libs{$rh->{library_name}} ) ) { - $dirPath = $prob_libs{$rh->{library_name}} ; - } else { - $out->{error} = "Could not find library:".$rh->{library_name}.":"; - return($out); - } - warn "library directory path for ",$rh->{library_name}," is $dirPath"; + my $dirPath = $self->{ce}->{courseDirs}{templates}."/".$rh->{library_name}; + my $maxdepth= $rh->{maxdepth}; + my $dirPath2 = $dirPath . ( ($rh->{dirPath}) ? '/'.$rh->{dirPath} : '' ) ; + my @tare = $dirPath2=~m|/|g; + my $tare = @tare; # counts number of "/" in dirPath prefix my @outListLib; my %libDirectoryList; - my $wanted = sub { + my $depthfinder = sub { # counts depth below the current directory + my $path = shift; + my @count = $path=~m|/|g; + my $depth = @count; + return $depth - $tare; + }; + my $wanted = sub { # find .pg files unless ($File::Find::dir =~/.svn/ ) { my $name = $File::Find::name; if ($name =~/\S/ ) { - $name =~ s|^$dirPath/*||; # cut the first directory + #$name =~ s|^$dirPath2/*||; # cut the first directory push(@outListLib, "$name") if $name =~/\.pg/; } } }; + my $wanted_directory = sub { + $File::Find::prune =1 if &$depthfinder($File::Find::dir) > $maxdepth; unless ($File::Find::dir =~/.svn/ ) { my $dir = $File::Find::dir; if ($dir =~/\S/ ) { - $dir =~ s|^$dirPath/*||; # cut the first directory - $libDirectoryList{$dir}++; + $dir =~ s|^$dirPath2/*||; # cut the first directory + + $libDirectoryList{$dir} = {}; } } }; my $command = $rh->{command}; - warn "the command being executed is ' $command '"; + #warn "the command being executed is ' $command '"; $command = 'all' unless defined($command); - $command eq 'all' && do { - find({wanted=>$wanted,follow_fast=>1 }, $dirPath); - @outListLib = sort @outListLib; - $out->{ra_out} = \@outListLib; - $out->{text} = encode_base64( join("\n", @outListLib) ); - return($out); - }; - $command eq 'dirOnly' && do { - find({wanted=>$wanted_directory,follow_fast=>1 }, $dirPath); - @outListLib = sort keys %libDirectoryList; - $out->{ra_out} = \@outListLib; - $out->{text} = encode_base64( join("\n", @outListLib) ); + + $command eq 'all' && do { + $out->{command}="all -- list all pg files in $dirPath"; + find({wanted=>$wanted,follow_fast=>1 }, $dirPath); + @outListLib = sort @outListLib; + $out->{ra_out} = \@outListLib; + $out->{text} = encode_base64( join("\n", @outListLib) ); + return($out); + }; + $command eq 'dirOnly' && do { + if ( -e $dirPath2) { + find({wanted=>$wanted_directory,follow_fast=>1 }, $dirPath2); + #@outListLib = grep {/\S/} sort keys %libDirectoryList; #omit blanks + #foreach my $key (grep {/\S/} sort keys %libDirectoryList) { + # push @outListLib, "$key"; # number of subnodes + #} + delete $libDirectoryList{""}; + $out->{ra_out} = \%libDirectoryList; + $out->{text} = encode_base64("Loaded libraries"); return($out); - }; - - $command eq 'files' && do { @outListLib=(); - my $separator = ($dirPath =~m|/$|) ?'' : '/'; - my $dirPath2 = $dirPath . $separator . $rh->{dirPath}; - if ( -e $dirPath2) { - find($wanted, $dirPath2); - @outListLib = sort @outListLib; - $out ->{text} = encode_base64( join("\n", @outListLib ) ); - $out->{ra_out} = \@outListLib; - } else { - $out->{error} = "Can't open directory $dirPath2"; - } - return($out); - - }; - # else - $out->{error}="Unrecognized command $command"; - return( $out ); + } else { + $out->{error} = "Can't open directory $dirPath2"; + } + }; +# use File::Find::Rule; + # find all the subdirectories of a given directory +# my @subdirs = File::Find::Rule->directory->in( $dirPath ); +# $command eq 'dirOnly' && do { +# my @subdirs = File::Find::Rule->directory->in( ($dirPath) ); +# $out->{ra_out} = \@subdirs; +# $out->{text} = encode_base64("Loaded libraries".$dirPath); +# return($out); +# }; + $command eq 'buildtree' && do { + #find({wanted=>$wanted_directory,follow_fast=>1 }, $dirPath); + build_tree(my $tree, $dirPath); + #@outListLib = sort keys %libDirectoryList; + $out->{ra_out} = $tree; + $out->{text} = encode_base64("Loaded libraries"); + return($out); + }; + + $command eq 'files' && do { @outListLib=(); + #my $separator = ($dirPath =~m|/$|) ?'' : '/'; + #my $dirPath2 = $dirPath . $separator . $rh->{dirPath}; + if ( -e $dirPath2) { + find($wanted, $dirPath2); + @outListLib = sort @outListLib; + #$out ->{text} = encode_base64( join("", @outListLib ) ); + $out ->{text} = encode_base64( "Problems loaded" ); + $out->{ra_out} = \@outListLib; + } else { + $out->{error} = "Can't open directory $dirPath2"; + } + return($out); + + }; + # else + $out->{error}="Unrecognized command $command"; + return( $out ); +} + +sub searchLib { #API for searching the NPL database + my $self = shift; + my $rh = shift; + my $out = {}; + my $ce = $self->{ce}; + my $subcommand = $rh->{subcommand}; + 'getDBTextbooks' eq $subcommand && do { + $self->{library_subjects} = $rh->{library_subjects}; + $self->{library_chapters} = $rh->{library_chapters}; + $self->{library_sections} = $rh->{library_sections}; + $self->{library_textchapter} = $rh->{library_textchapter}; + my @textbooks = WeBWorK::Utils::ListingDB::getDBTextbooks($self); + $out->{ra_out} = \@textbooks; + return($out); + }; + 'getAllDBsubjects' eq $subcommand && do { + my @subjects = WeBWorK::Utils::ListingDB::getAllDBsubjects($self); + $out->{ra_out} = \@subjects; + $out->{text} = encode_base64("Subjects loaded."); + return($out); + }; + 'getAllDBchapters' eq $subcommand && do { + $self->{library_subjects} = $rh->{library_subjects}; + my @chaps = WeBWorK::Utils::ListingDB::getAllDBchapters($self); + $out->{ra_out} = \@chaps; + return($out); + }; + 'getDBListings' eq $subcommand && do { + my $templateDir = $self->{ce}->{courseDirs}->{templates}; + $self->{library_subjects} = $rh->{library_subjects}; + $self->{library_chapters} = $rh->{library_chapters}; + $self->{library_sections} = $rh->{library_sections}; + $self->{library_keywords} = $rh->{library_keywords}; + $self->{library_textbook} = $rh->{library_textbook}; + $self->{library_textchapter} = $rh->{library_textchapter}; + $self->{library_textsection} = $rh->{library_textsection}; + my @listings = WeBWorK::Utils::ListingDB::getDBListings($self); + my @output = map {$templateDir."/Library/".$_->{path}."/".$_->{filename}} @listings; + #change the hard coding!!!....just saying + $out->{ra_out} = \@output; + return($out); + }; + 'getSectionListings' eq $subcommand && do { + $self->{library_subjects} = $rh->{library_subjects}; + $self->{library_chapters} = $rh->{library_chapters}; + $self->{library_sections} = $rh->{library_sections}; + + my @section_listings = WeBWorK::Utils::ListingDB::getAllDBsections($self); + $out->{ra_out} = \@section_listings; + return($out); + }; + # else + $out->{error}="Unrecognized command $subcommand"; + return( $out ); } diff --git a/lib/WebworkWebservice/MathTranslators.pm b/lib/WebworkWebservice/MathTranslators.pm index 3c60e5b9e0..531ddfe665 100644 --- a/lib/WebworkWebservice/MathTranslators.pm +++ b/lib/WebworkWebservice/MathTranslators.pm @@ -56,7 +56,7 @@ my $tmp_directory_permission = $Global::tmp_directory_permission; my $numericalGroupID = $Global::numericalGroupID; # group ID for webadmin sub tex2pdf { - + my $self = shift; my $rh = shift; local($|) = 1; my $out = {}; diff --git a/lib/WebworkWebservice/RenderProblem.pm b/lib/WebworkWebservice/RenderProblem.pm index bd79e063bf..ee72cf750d 100644 --- a/lib/WebworkWebservice/RenderProblem.pm +++ b/lib/WebworkWebservice/RenderProblem.pm @@ -59,6 +59,11 @@ our $PROTOCOL = $WebworkWebservice::PROTOCOL; our $HOST_NAME = $WebworkWebservice::HOST_NAME; our $PORT = $WebworkWebservice::HOST_PORT; our $HOSTURL = "$PROTOCOL://$HOST_NAME:$PORT"; + + + + +our $UNIT_TESTS_ON =0; # # #our $ce = $WebworkWebservice::SeedCE; # # create a local course environment for some course @@ -119,21 +124,8 @@ use constant DISPLAY_MODE_FAILOVER => { sub renderProblem { - + my $self = shift; my $rh = shift; -############################################################################### -# set up warning handler -############################################################################### - my $warning_messages=""; - - my $warning_handler = sub { - my ($warning) = @_; - CORE::warn $warning; - chomp $warning; - $warning_messages .="$warning\n"; - }; - - local $SIG{__WARN__} = $warning_handler; ########################################### # Grab the course name, if this request is going to depend on @@ -144,12 +136,22 @@ sub renderProblem { my $db; my $user; my $beginTime = new Benchmark; - if (defined($rh->{course}) and $rh->{course}=~/\S/ ) { - $courseName = $rh->{course}; - } else { - $courseName = $COURSENAME; - # use the default $ce - } +# if (defined($self->{courseName}) and $self->{courseName} ) { +# $courseName = $self->{courseName}; +# } elsif (defined($rh->{course}) and $rh->{course}=~/\S/ ) { +# $courseName = $rh->{course}; +# } else { +# $courseName = $COURSENAME; +# # use the default $ce +# } + if (defined($self->{courseName}) and $self->{courseName} ) { + $courseName = $self->{courseName}; + } + + # It's better not to get the course in too many places. :-) + # High level information about the course should come from $self + # Lower level information should come from $rh (i.e. passed by $in at WebworkWebservice) + #FIXME put in check to make sure the course exists. eval { $ce = WeBWorK::CourseEnvironment->new({webwork_dir=>$WW_DIRECTORY, courseName=> $courseName}); @@ -160,25 +162,43 @@ sub renderProblem { # $ce->{pg}->{options}->{catchWarnings}=1; #FIXME warnings aren't automatically caught # when using xmlrpc -- turn this on in the daemon2_course. #^FIXME need better way of determining whether the course actually exists. + + # The UNIT_TEST_ON snippets are the closest thing we have to a real unit test. + # warn "Unable to create course $courseName. Error: $@" if $@; - my $user = $rh->{user}; - $user = 'practice1' unless defined $user and $user =~/\S/; + # my $user = $rh->{user}; + # $user = 'practice1' unless defined $user and $user =~/\S/; + + my $user = $self->{user_id}; ########################################### -# Authenticate this request +# Authenticate this request -- done by initiate in WebworkWebservice ########################################### ########################################### -# Determine the authorization level (permissions) +# Determine the authorization level (permissions) -- done by initiate in WebworkWebservice ########################################### +############################################################################### +# set up warning handler +############################################################################### + my $warning_messages=""; + + my $warning_handler = sub { + my ($warning) = @_; + CORE::warn $warning; + chomp $warning; + $warning_messages .="$warning\n"; + }; + + local $SIG{__WARN__} = $warning_handler; ########################################### -# Determine the method for accessing data +# Determine the method for accessing data ???? what was this ########################################### my $problem_source_access = $rh->{problem_source_access}; # One of @@ -207,6 +227,18 @@ sub renderProblem { $effectiveUserName = 'foobar'; } ################################################## + if ($UNIT_TESTS_ON) { + print STDERR "RenderProblem.pm: user = $user\n"; + print STDERR "RenderProblem.pm: courseName = $courseName\n"; + print STDERR "RenderProblem.pm: effectiveUserName = $effectiveUserName\n"; + } + + ################################################################# + # The effectiveUser is the student the this problem version was written for + # The user might also be the effective user but it could be + # an instructor checking out how well the problem is working. + ################################################################# + my $effectiveUser = $db->getUser($effectiveUserName); # checked my $effectiveUserPermissionLevel; my $effectiveUserPassword; @@ -226,10 +258,11 @@ sub renderProblem { $effectiveUser->recitation($rh->{envir}->{recitation} ||''); $effectiveUser->comment(''); $effectiveUser->status('C'); - $effectiveUser->password($rh->{envir}->{studentID}|| 'foobar'); + #$effectiveUser->password($rh->{envir}->{studentID}|| 'foobar'); dunno what's going on here $effectiveUserPermissionLevel->permission(0); } #FIXME these will fail if the keys are not defined within the environment. + ########################################### # Insure that set and problem are defined # Define the set and problem information from @@ -309,7 +342,10 @@ sub renderProblem { $problemRecord->source_file($rh->{sourceFilePath}); } $problemRecord->source_file('foobar') unless defined($problemRecord->source_file); - + if ($UNIT_TESTS_ON){ + print STDERR "RenderProblem.pm: source file is ", $problemRecord->source_file,"\n"; + print STDERR "RenderProblem.pm: problem source is included in the request \n" if defined($rh->{source}); + } #warn "problem Record is $problemRecord"; # now we're sure we have valid UserSet and UserProblem objects # yay! @@ -411,8 +447,8 @@ sub renderProblem { if ($debugXmlCode) { my $logDirectory =$ce->{courseDirs}->{logs}; my $xmlDebugLog = "$logDirectory/xml_debug.txt"; - warn "Opening debug log $xmlDebugLog\n" ; - open (DEBUGCODE, ">>$xmlDebugLog") || die "Can't open $xmlDebugLog"; + #warn "RenderProblem.pm: Opening debug log $xmlDebugLog\n" ; + open (DEBUGCODE, ">>$xmlDebugLog") || die "Can't open debug log $xmlDebugLog"; print DEBUGCODE "\n\nStart xml encoding\n"; } diff --git a/lib/WebworkWebservice/SetActions.pm b/lib/WebworkWebservice/SetActions.pm new file mode 100644 index 0000000000..47004bcd1b --- /dev/null +++ b/lib/WebworkWebservice/SetActions.pm @@ -0,0 +1,380 @@ +#!/usr/local/bin/perl -w + +# Copyright (C) 2002 Michael Gage + +############################################################################### +# Web service which fetches, adds, removes and moves WeBWorK problems when working with a Set. +############################################################################### + + +#use lib '/home/gage/webwork/pg/lib'; +#use lib '/home/gage/webwork/webwork-modperl/lib'; + +package WebworkWebservice::SetActions; +use WebworkWebservice; +use base qw(WebworkWebservice); +use WeBWorK::Utils qw(readDirectory max sortByName); +use WeBWorK::Utils::Tasks qw(renderProblems); + +use strict; +use sigtrap; +use Carp; +use WWSafe; +#use Apache; +use WeBWorK::Utils; +use WeBWorK::CourseEnvironment; +use WeBWorK::PG::Translator; +use WeBWorK::DB::Utils qw(initializeUserProblem); +use WeBWorK::PG::IO; +use Benchmark; +use MIME::Base64 qw( encode_base64 decode_base64); + +############################################## +# Some of this may have to be moved, to allow for flexability +# Obtain basic information about directories, course name and host +############################################## +our $WW_DIRECTORY = $WebworkWebservice::WW_DIRECTORY; +our $PG_DIRECTORY = $WebworkWebservice::PG_DIRECTORY; +our $COURSENAME = $WebworkWebservice::COURSENAME; +our $HOST_NAME = $WebworkWebservice::HOST_NAME; +our $PASSWORD = $WebworkWebservice::PASSWORD; +our $ce = WeBWorK::CourseEnvironment->new($WW_DIRECTORY, "", "", $COURSENAME); + +our $UNIT_TESTS_ON =1; + +sub listLocalSets{ + my $self = shift; + my $db = $self->{db}; + my @found_sets; + @found_sets = $db->listGlobalSets; + my $out = {}; + $out->{ra_out} = \@found_sets; + $out->{text} = encode_base64("Sets for course: ".$self->{courseName}); + return $out; +} + +sub listLocalSetProblems{ + my $self = shift; + my $in = shift; + my $db = $self->{db}; + my @found_problems; + my $selectedSet = $in->{set}; + warn "Finding problems for set ", $in->{set} if $UNIT_TESTS_ON; + my $templateDir = $self->{ce}->{courseDirs}->{templates}; + @found_problems = $db->listGlobalProblems($selectedSet); + my $problem; + my @pg_files=(); + for $problem (@found_problems) { + my $problemRecord = $db->getGlobalProblem($selectedSet, $problem); # checked + die "global $problem for set $selectedSet not found." unless + $problemRecord; + push @pg_files, $templateDir."/".$problemRecord->source_file; + + } + #@pg_files = sortByName(undef,@pg_files); + + my $out = {}; + $out->{ra_out} = \@pg_files; + $out->{text} = encode_base64("Sets for course: ".$self->{courseName}); + return $out; +} + +sub createNewSet{ + my $self = shift; + my $in = shift; + my $db = $self->{db}; + my $out; + if ($in->{new_set_name} !~ /^[\w .-]*$/) { + $out->{text} = "need a different name";#not sure the best way to handle and error + } else { + my $newSetName = $in->{new_set_name}; + # if we want to munge the input set name, do it here + $newSetName =~ s/\s/_/g; + #debug("local_sets was ", $r->param('local_sets')); + #$r->param('local_sets',$newSetName); ## use of two parameter param + #debug("new value of local_sets is ", $r->param('local_sets')); + my $newSetRecord = $db->getGlobalSet($newSetName); + if (defined($newSetRecord)) { + $out->{text} = encode_base64("Failed to create set, you may need to try another name."); + } else { # Do it! + # DBFIXME use $db->newGlobalSet + $newSetRecord = $db->{set}->{record}->new(); + $newSetRecord->set_id($newSetName); + $newSetRecord->set_header("defaultHeader"); + $newSetRecord->hardcopy_header("defaultHeader"); + $newSetRecord->open_date(time()+60*60*24*7); # in one week + $newSetRecord->due_date(time()+60*60*24*7*2); # in two weeks + $newSetRecord->answer_date(time()+60*60*24*7*3); # in three weeks + eval {$db->addGlobalSet($newSetRecord)}; + if ($@) { + $out->{text} = encode_base64("Failed to create set, you may need to try another name."); + #$self->addbadmessage("Problem creating set $newSetName
$@"); + } else { + #figure this bit out later + #$self->addgoodmessage("Set $newSetName has been created."); + my $selfassign = $in->{selfassign}; + $selfassign = "" if($selfassign =~ /false/i); # deal with javascript false + if($selfassign) { + $self->assignSetToUser($self->{user}, $newSetRecord); + #$self->addgoodmessage("Set $newSetName was assigned to $userName."); + } + } + } + } +} + + +sub reorderProblems { + my $self = shift; + my $in = shift; + my $db = $self->{db}; + my $setID = $in->{set}; + my $problemListString = $in->{probList}; + my @problemList = split(/,/, $problemListString); + my $topdir = $self->{ce}->{courseDirs}{templates}; + #my (@problemIDList) = @_; + #my ($prob1, $prob2, $prob); + my $index = 1; + #get all the problems + my @problems = (); + foreach my $path (@problemList) { + $path =~ s|^$topdir/*||; + #this will work if i can get problems by name + my $problemRecord; # checked + foreach my $problem ($db->listGlobalProblems($setID)) { + my $tempProblem = $db->getGlobalProblem($setID, $problem); + if($tempProblem->source_file eq $path){ + $problemRecord = $tempProblem; + } + } + die "global " .$path ." for set $setID not found." unless $problemRecord; + #print "found this problem to be reordered: ".$problemRecord."\n"; + push @problems, $problemRecord; + $index = $index + 1; + } + #then change their info + my @setUsers = $db->listSetUsers($setID); + my $user; + $index = 1; + foreach my $problem (@problems) { + $problem->problem_id($index); + die "global $problem not found." unless $problem; + #print "problem to be reordered: ".$problem."\n"; + $db->putGlobalProblem($problem); + + #need to deal with users? + foreach $user (@setUsers) { + #my $prob1 = $db->getUserProblem($user, $setID, $index); + #die " problem $index for set $setID and effective user $user not found" unless $prob1; + #$prob1->problem_id($index); + #$db->putUserProblem($prob1); + } + $index = $index + 1; + } + my $out->{text} = encode_base64("Successfully reordered problems"); + return $out; +} + + +#problem utils from Instructor.pm +sub assignProblemToUser { + my $self = shift; + my $userID = shift; + my $GlobalProblem = shift; + my $seed = shift;; + my $db = $self->{db}; + + my $UserProblem = $db->newUserProblem; + $UserProblem->user_id($userID); + $UserProblem->set_id($GlobalProblem->set_id); + $UserProblem->problem_id($GlobalProblem->problem_id); + initializeUserProblem($UserProblem, $seed); + + eval { $db->addUserProblem($UserProblem) }; + if ($@) { + if ($@ =~ m/user problem exists/) { + return "problem " . $GlobalProblem->problem_id + . " in set " . $GlobalProblem->set_id + . " is already assigned to user $userID."; + } else { + die $@; + } + } + + return (); +} + +sub assignProblemToAllSetUsers { + my $self = shift; + my $GlobalProblem = shift; + my $db = $self->{db}; + my $setID = $GlobalProblem->set_id; + my @userIDs = $db->listSetUsers($setID); + + my @results; + + foreach my $userID (@userIDs) { + my @result = assignProblemToUser($self, $userID, $GlobalProblem); + push @results, @result if @result; + } + + return @results; +} + +sub addProblemToSet { + my $self = shift; + my $args = shift; + my $db = $self->{db}; + my $value_default = $self->{ce}->{problemDefaults}->{value}; + my $max_attempts_default = $self->{ce}->{problemDefaults}->{max_attempts}; + + + die "addProblemToSet called without specifying the set name." if $args->{setName} eq ""; + my $setName = $args->{setName}; + + my $sourceFile = $args->{sourceFile} or + die "addProblemToSet called without specifying the sourceFile."; + + # The rest of the arguments are optional + +# my $value = $args{value} || $value_default; + my $value = $value_default; + if (defined($args->{value})){$value = $args->{value};} # 0 is a valid value for $args{value} + + my $maxAttempts = $args->{maxAttempts} || $max_attempts_default; + my $problemID = $args->{problemID}; + + unless ($problemID) { + $problemID = WeBWorK::Utils::max($db->listGlobalProblems($setName)) + 1; + } + + my $problemRecord = $db->newGlobalProblem; + $problemRecord->problem_id($problemID); + $problemRecord->set_id($setName); + $problemRecord->source_file($sourceFile); + $problemRecord->value($value); + $problemRecord->max_attempts($maxAttempts); + $db->addGlobalProblem($problemRecord); + + return $problemRecord; +} + +sub addProblem { + my $self = shift; + my $in = shift; + my $db = $self->{db}; + my $setName = $in->{set}; + my $file = $in->{path}; + my $topdir = $self->{ce}->{courseDirs}{templates}; + $file =~ s|^$topdir/*||; + my $freeProblemID; + # DBFIXME count would work just as well + $freeProblemID = max($db->listGlobalProblems($setName)) + 1; + my $problemRecord = addProblemToSet($self, {setName => $setName, sourceFile => $file, problemID => $freeProblemID}); + assignProblemToAllSetUsers($self, $problemRecord); + my $out->{text} = encode_base64("Problem added to ".$setName); + return $out; +} + +sub deleteProblem { + my $self = shift; + my $in = shift; + my $db = $self->{db}; + my $setName = $in->{set}; + + my $file = $in->{path}; + my $topdir = $self->{ce}->{courseDirs}{templates}; + $file =~ s|^$topdir/*||; + # DBFIXME count would work just as well + foreach my $problem ($db->listGlobalProblems($setName)) { + my $problemRecord = $db->getGlobalProblem($setName, $problem); + + if($problemRecord->source_file eq $file){ + #print "found it"; + $db->deleteGlobalProblem($setName, $problemRecord->problem_id); + } + } + my $out->{text} = encode_base64("Problem removed from ".$setName); + return $out; +} + + +## Search for set definition files +use File::Find; +sub get_set_defs { + my $self = shift; + my $topdir = $self->{ce}->{courseDirs}{templates};#shift #sort of hard coded for now; + my @found_set_defs; + # get_set_defs_wanted is a closure over @found_set_defs + my $get_set_defs_wanted = sub { + #my $fn = $_; + #my $fdir = $File::Find::dir; + #return() if($fn !~ /^set.*\.def$/); + ##return() if(not -T $fn); + #push @found_set_defs, "$fdir/$fn"; + push @found_set_defs, $_ if m|/set[^/]*\.def$|; + }; + find({ wanted => $get_set_defs_wanted, follow_fast=>1, no_chdir=>1}, $topdir); + map { $_ =~ s|^$topdir/?|| } @found_set_defs; + my $out = {}; + $out->{ra_out} = \@found_set_defs; + return $out; +} + +## Try to make reading of set defs more flexible. Additional strategies +## for fixing a path can be added here. + +sub munge_pg_file_path { + my $self = shift; + my $pg_path = shift; + my $path_to_set_def = shift; + my $end_path = $pg_path; + # if the path is ok, don't fix it + return($pg_path) if(-e $self->r->ce->{courseDirs}{templates}."/$pg_path"); + # if we have followed a link into a self contained course to get + # to the set.def file, we need to insert the start of the path to + # the set.def file + $end_path = "$path_to_set_def/$pg_path"; + return($end_path) if(-e $self->r->ce->{courseDirs}{templates}."/$end_path"); + # if we got this far, this path is bad, but we let it produce + # an error so the user knows there is a troublesome path in the + # set.def file. + return($pg_path); +} + +## Read a set definition file. This could be abstracted since it happens +## elsewhere. Here we don't have to process so much of the file. + +sub read_set_def { + my $self = shift; + my $r = $self->r; + my $filePathOrig = shift; + my $filePath = $r->ce->{courseDirs}{templates}."/$filePathOrig"; + $filePathOrig =~ s/set.*\.def$//; + $filePathOrig =~ s|/$||; + $filePathOrig = "." if ($filePathOrig !~ /\S/); + my @pg_files = (); + my ($line, $got_to_pgs, $name, @rest) = ("", 0, ""); + if ( open (SETFILENAME, "$filePath") ) { + while($line = ) { + chomp($line); + $line =~ s|(#.*)||; # don't read past comments + if($got_to_pgs) { + unless ($line =~ /\S/) {next;} # skip blank lines + ($name,@rest) = split (/\s*,\s*/,$line); + $name =~ s/\s*//g; + push @pg_files, $name; + } else { + $got_to_pgs = 1 if ($line =~ /problemList\s*=/); + } + } + } else { + $self->addbadmessage("Cannot open $filePath"); + } + # This is where we would potentially munge the pg file paths + # One possibility + @pg_files = map { $self->munge_pg_file_path($_, $filePathOrig) } @pg_files; + return(@pg_files); +} + +