--- trunk/webwork2/lib/WeBWorK/ContentGenerator/Instructor/PGProblemEditor.pm 2004/05/18 05:19:33 2126 +++ trunk/webwork2/lib/WeBWorK/ContentGenerator/Instructor/PGProblemEditor.pm 2005/10/17 03:42:42 3718 @@ -1,7 +1,7 @@ ################################################################################ # WeBWorK Online Homework Delivery System # Copyright © 2000-2003 The WeBWorK Project, http://openwebwork.sf.net/ -# $CVSHeader: webwork-modperl/lib/WeBWorK/ContentGenerator/Instructor/PGProblemEditor.pm,v 1.34 2004/05/12 14:29:36 toenail Exp $ +# $CVSHeader: webwork-modperl/lib/WeBWorK/ContentGenerator/Instructor/PGProblemEditor.pm,v 1.57 2005/09/29 01:57:56 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 @@ -18,9 +18,10 @@ use base qw(WeBWorK::ContentGenerator::Instructor); + =head1 NAME -WeBWorK::ContentGenerator::Instructor::ProblemSetEditor - Edit a set definition list +WeBWorK::ContentGenerator::Instructor::PGProblemEditor - Edit a pg file =cut @@ -30,6 +31,8 @@ use WeBWorK::Utils qw(readFile surePathToFile); use Apache::Constants qw(:common REDIRECT); use HTML::Entities; +use URI::Escape; +use WeBWorK::Utils; use WeBWorK::Utils::Tasks qw(fake_set fake_problem); ########################################################### @@ -45,122 +48,343 @@ # # editMode = temporaryFile (view the temp file defined by course_info.txt.user_name.tmp # instead of the file course_info.txt) -# The editFileSuffix is "user_name.tmp" by default. It's definition should be moved to Instructor.pm #FIXME +# 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 ########################################################### -#our $libraryName; -#our $rowheight; +########################################################### +# 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. +# +# filte_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 '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 make_local_copy 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", + revert => "modify_student_data", +}; + sub pre_header_initialize { - my ($self) = @_; - my $r = $self->r; - my $ce = $r->ce; - my $urlpath = $r->urlpath; + 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"); + + 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 $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} = '123456' unless defined $self->{problemSeed} and $self->{problemSeed} =~/\S/; + + ############################################################################## + ############################################################################# + # 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 aso to be redirected to the ProblemSet.pm page + # Course_info files are redirected to the ProblemSets.pm page + ############################################################################## - my $submit_button = $r->param('submit'); # obtain submit command from form - my $file_type = $r->param("file_type") || ''; - # Save problem to permanent or temporary file, then redirect for viewing - if (defined($submit_button) and - ($submit_button eq 'Save' or $submit_button eq 'Refresh' - or ($submit_button eq 'Save as' and $file_type eq 'problem'))) { - my $setName = $r->urlpath->arg("setID"); - my $problemNumber = $r->urlpath->arg("problemID"); - - # write the necessary files - # return file path for viewing problem in $self->{currentSourceFilePath} - # obtain the appropriate seed - $self->saveFileChanges($setName, $problemNumber); - - ##### calculate redirect URL based on file type ##### - - # get some information - #my $hostname = $r->hostname(); - #my $port = $r->get_server_port(); - #my $uri = $r->uri; - my $courseName = $urlpath->arg("courseID"); - my $problemSeed = ($r->param('problemSeed')) ? $r->param('problemSeed') : ''; - my $displayMode = ($r->param('displayMode')) ? $r->param('displayMode') : ''; - - my $viewURL = ''; - - if($self->{file_type} eq 'problem') { - if($submit_button eq 'Save as') { # redirect to myself - my $sourceFile = $self->{problemPath}; - # strip off template directory prefix - my $edit_level = $r->param("edit_level") || 0; - $edit_level++; - $sourceFile =~ s|^$ce->{courseDirs}->{templates}/||; - my $problemPage = $urlpath->newFromModule("WeBWorK::ContentGenerator::Instructor::PGProblemEditor", - courseID => $courseName, setID => 'Undefined_Set', problemID => $problemNumber); - $viewURL = $self->systemLink($problemPage, params=>{sourceFilePath => $sourceFile, edit_level=>$edit_level}); - } else { # other problems redirect to Problem.pm - my $problemPage = $urlpath->newFromModule("WeBWorK::ContentGenerator::Problem", - courseID => $courseName, setID => $setName, problemID => $problemNumber); - $self->{currentSourceFilePath} =~ s|^$ce->{courseDirs}->{templates}/||; - $viewURL = $self->systemLink($problemPage, - params => { - displayMode => $displayMode, - problemSeed => $problemSeed, - editMode => ($submit_button eq "Save" ? "savedFile" : "temporaryFile"), - sourceFilePath => $self->{currentSourceFilePath}, - success => $self->{sucess}, - failure => $self->{failure}, - } - ); - } - } - - # set headers redirect to ProblemSet.pm - $self->{file_type} eq 'set_header' and do { - my $problemSetPage = $urlpath->newFromModule("WeBWorK::ContentGenerator::ProblemSet", - courseID => $courseName, setID => $setName); - $viewURL = $self->systemLink($problemSetPage, - params => { - displayMode => $displayMode, - problemSeed => $problemSeed, - editMode => ($submit_button eq "Save" ? "savedFile" : "temporaryFile"), - } - ); - }; - - # course info redirects to ProblemSets.pm - $self->{file_type} eq 'course_info' and do { - my $problemSetsPage = $urlpath->newFromModule("WeBWorK::ContentGenerator::ProblemSets", - courseID => $courseName); - $viewURL = $self->systemLink($problemSetsPage, - params => { - editMode => ($submit_button eq "Save" ? "savedFile" : "temporaryFile"), - } - ); - }; - - if ($viewURL) { - $self->reply_with_redirect($viewURL); - } else { - die "Invalid file_type ", $self->{file_type}, " specified by saveFileChanges"; + + ###################################### + # 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 (defined($file_type) and ($file_type =~/\S/)) { #file_type is defined and is not blank + # file type is already defined -- do nothing + } 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 ( defined($r->param('sourceFilePath') and $r->param('sourceFilePath') =~/\S/) ) { + $file_type ='source_path_for_problem_file'; + } 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'; + } + } } + die "The file_type variable has not been defined or is blank." unless defined($file_type) and $file_type =~/\S/; + # 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; + + ########################################## + # File type is one of: blank_problem course_info problem set_header hardcopy_header source_path_for_problem_file + ########################################## + # + # Determine the path to the file + # + ########################################### + $self->getFilePaths($setName, $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} = '123456' unless defined $self->{problemSeed} and $self->{problemSeed} =~/\S/; + + ############################################################################## + # 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'); - my $setName = $r->urlpath->arg("setID"); - my $problemNumber = $r->urlpath->arg("problemID"); + # Check permissions + return unless ($authz->hasPermissions($user, "access_instructor_tools")); + return unless ($authz->hasPermissions($user, "modify_problem_sets")); + + 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("This file: $inputFilePath, cannot be found."); + } elsif (not -w $inputFilePath ) { + $self->addbadmessage("This file '$inputFilePath' is protected! ".CGI::br()."To edit this text you must either 'Make a local copy' of this problem, or + use 'Save As' to save it to another file."); + } + - # if we got to initialize(), then saveFileChanges was not called in pre_header_initialize(). - # therefore we call it here: - $self->saveFileChanges($setName, $problemNumber); } +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 $problemNumber = $r->urlpath->arg("problemID"); my $file_type = $self->{'file_type'} || ''; - return "Set Header" if($file_type eq 'set_header'); + return "Set Header" if ($file_type eq 'set_header'); + return "Hardcopy Header" if ($file_type eq 'hardcopy_header'); return "Course Information" if($file_type eq 'course_info'); return 'Problem ' . $r->{urlpath}->name; } @@ -170,19 +394,57 @@ 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"); - # Gathering info - my $editFilePath = $self->{problemPath}; # path to the permanent file to be edited - my $inputFilePath = $self->{inputFilePath}; # path to the file currently being worked with (might be a .tmp file) - - my $header = CGI::i("Editing problem: $inputFilePath"); + 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. + $problemNumber = defined($problemNumber) ? $problemNumber : ''; + ######################################################################### # 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) { + eval { $problemContents = WeBWorK::Utils::readFile($tempFilePath) }; + $problemContents = $@ if $@; + $inputFilePath = $tempFilePath; + } elsif (-r $editFilePath and not -d $editFilePath) { + 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 $header = CGI::i("Editing problem".CGI::b("set $setName/ problem $problemNumber").CGI::br()." in file $inputFilePath"); + $header = ($self->isTempFilePath($inputFilePath) ) ? CGI::div({class=>'temporaryFile'},$header) : $header; # use colors if temporary file ######################################################################### # Format the page @@ -191,289 +453,959 @@ # Define parameters for textarea # FIXME # Should the seed be set from some particular user instance?? - # The mode list should be obtained from global.conf ultimately - my $rows = 20; - my $columns = 80; - my $mode_list = ['plainText','formattedText','images']; - my $displayMode = $self->{displayMode}; - my $problemSeed = $self->{problemSeed}; - my $uri = $r->uri; - my $edit_level = $r->param('edit_level') || 0; + 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 $file_type = $self->{file_type}; - my $force_field = defined($r->param('sourceFilePath')) ? + my $force_field = (defined($self->{sourceFilePath}) and $self->{sourceFilePath} ne "") ? CGI::hidden(-name=>'sourceFilePath', - -default=>$r->param('sourceFilePath')) : ''; - return CGI::p($header), - #CGI::start_form("POST",$r->uri,-target=>'_problem'), doesn't pass on the target parameter??? - # THIS IS BECAUSE TARGET IS NOT A PARAMETER OF