################################################################################ # 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.52 2005/01/29 01:23:57 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::PGProblemEditor; use base qw(WeBWorK::ContentGenerator::Instructor); =head1 NAME WeBWorK::ContentGenerator::Instructor::PGProblemEditor - Edit a pg file =cut use strict; use warnings; use CGI qw(); 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); ########################################################### # 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 'problem_with_source' # This is the same as the 'problem' file type except that the source for the problem is found in # the parameter $r->param('sourceFilePath'). # # 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} ############################################################# # submit button actions -- these and the file_type determine the state of the module # Save ---- action = save # Save as ---- action = save_as # View Problem ---- action = refresh # Add this problem to: ---- action = add_problem_to_set # Make this set header for: ---- action = add_set_header_to_set # 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 # # $problemPath is also used as is editFilePath. let's try to regularize these. #(sourceFile) (problemPath)(tempFilePath)(editFilePath)(forcedSourceFile)(problemPath) #input parameter can be: sourceFilePath ################################################################# # params read # user # effectiveUser # submit # file_type # problemSeed # displayMode # edit_level # make_local_copy # sourceFilePath # problemContents # save_to_new_file # #our $libraryName; #our $rowheight; our $TEMPFILESUFFIX; 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'); $TEMPFILESUFFIX = $user.'.tmp'; my $submit_button = $r->param('submit'); # obtain submit command from form my $file_type = $r->param("file_type") || ''; my $setName = $r->urlpath->arg("setID") ; # using $r->urlpath->arg("setID") ||'' causes trouble with set 0!!! my $problemNumber = $r->urlpath->arg("problemID"); # Check permissions return unless ($authz->hasPermissions($user, "access_instructor_tools")); return unless ($authz->hasPermissions($user, "modify_problem_sets")); ############################################################################# # 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 ############################################################################## ###################################### # 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 "sourcFilePath" 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/; $self->{file_type} = $file_type; ########################################## # File type is one of: blank_problem course_info problem set_header hardcopy_header problem_with_source ########################################## # # Determine the path to the file # ########################################### $self->getFilePaths($setName, $problemNumber, $file_type,$TEMPFILESUFFIX); # result stored in $self->{editFilePath}, and $self->{tempFilePath} ########################################## # # Determine action # ########################################### # Submit button is one of: "add this problem to" , "add this set header to ", "Refresh" "Revert" "Save" "Save As" $submit_button = $r->param('submit'); SUBMIT_CASE: { (! defined($submit_button) ) and do { # fresh problem to edit $self->{action} = 'fresh_edit'; last SUBMIT_CASE; }; ($submit_button eq 'Add this problem to: ') and do { $self->{action} = 'add_problem_to_set'; last SUBMIT_CASE; }; ($submit_button eq 'Make this the set header for: ') and do { $self->{action} = 'add_set_header_to_set'; last SUBMIT_CASE; }; ($submit_button eq 'View problem') and do { $self->{action} ='refresh'; last SUBMIT_CASE; }; ($submit_button eq 'Revert') and do { $self->{action} = 'revert'; last SUBMIT_CASE; }; ($submit_button eq 'Save') and do { $self->{action} = 'save'; last SUBMIT_CASE; }; ($submit_button eq 'Save as') and do { $self->{action} = 'save_as'; last SUBMIT_CASE; }; ($submit_button eq 'Add problem to: ') and do { $self->{action} = 'add_problem_to_set'; last SUBMIT_CASE; }; # else die "Unrecognized submit command: |$submit_button|"; } # END SUBMIT_CASE ########################################### # Save file ###################################### # The subroutine below writes the necessary files and obtains the appropriate seed. # and returns # $self->{problemPath} --- file path for viewing problem in $self->{problemPath} # $self->{failure} $self->saveFileChanges($setName, $problemNumber, $file_type,$TEMPFILESUFFIX); ############################################################################## # 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: revert, fresh_edit my $action = $self->{action}; return unless $action eq 'save' or $action eq 'refresh' or $action eq 'save_as' or $action eq 'add_problem_to_set' or $action eq 'add_set_header_to_set'; ###################################### # calculate redirect URL based on file type ###################################### my $courseName = $urlpath->arg("courseID"); my $problemSeed = ($r->param('problemSeed')) ? $r->param('problemSeed') : ''; my $displayMode = ($r->param('displayMode')) ? $r->param('displayMode') : ''; my $viewURL = ''; ###################################### # problem file_type # redirect to Problem.pm with setID = "Undefined_Set if "Save As" option is chosen # redirect to Problem.pm with setID = current $setID if "Save" or "Revert" or "Refresh is chosen" ###################################### REDIRECT_CASES: { ($file_type eq 'problem' or $file_type eq 'source_path_for_problem_file' or $file_type eq 'blank_problem') and do { my $sourceFilePath = $self->{problemPath}; # strip off template directory prefix $sourceFilePath =~ s|^$ce->{courseDirs}->{templates}/||; if ($action eq 'save_as') { # redirect to myself my $edit_level = $r->param("edit_level") || 0; $edit_level++; my $problemPage = $urlpath->newFromModule("WeBWorK::ContentGenerator::Instructor::PGProblemEditor", courseID => $courseName, setID => 'Undefined_Set', problemID => 'Undefined_Problem' ); $viewURL = $self->systemLink($problemPage, params=>{ sourceFilePath => $sourceFilePath, edit_level => $edit_level, file_type => 'source_path_for_problem_file', status_message => uri_escape($self->{status_message}) } ); } elsif ( $action eq 'add_problem_to_set') { my $targetSetName = $r->param('target_set'); my $problemPage = $urlpath->newFromModule("WeBWorK::ContentGenerator::Problem", courseID => $courseName, setID => $targetSetName, problemID => WeBWorK::Utils::max( $r->db->listGlobalProblems($targetSetName)) ); $viewURL = $self->systemLink($problemPage, params => { displayMode => $displayMode, problemSeed => $problemSeed, editMode => "savedFile", sourceFilePath => $sourceFilePath, status_message => uri_escape($self->{status_message}) } ); } elsif ( $action eq 'add_set_header_to_set') { my $targetSetName = $r->param('target_set'); my $problemPage = $urlpath->newFromModule("WeBWorK::ContentGenerator::ProblemSet", courseID => $courseName, setID => $targetSetName ); $viewURL = $self->systemLink($problemPage, params => { displayMode => $displayMode, editMode => "savedFile", status_message => uri_escape($self->{status_message}) } ); } else { # saved problems and refreshed problems redirect to Problem.pm my $problemPage = $urlpath->newFromModule("WeBWorK::ContentGenerator::Problem", courseID => $courseName, setID => $setName, problemID => $problemNumber ); $viewURL = $self->systemLink($problemPage, params => { displayMode => $displayMode, problemSeed => $problemSeed, editMode => ($action eq "save" ? "savedFile" : "temporaryFile"), sourceFilePath => $sourceFilePath, status_message => uri_escape($self->{status_message}) } ); } last REDIRECT_CASES; }; ###################################### # blank_problem file_type # redirect to Problem.pm ###################################### $file_type eq 'blank_problem' and do { return; # no redirect is needed }; ###################################### # set headers file_type # redirect to ProblemSet.pm ###################################### ($file_type eq 'set_header' or $file_type eq 'hardcopy_header' ) and do { if ($action eq 'save_as') { # redirect to myself my $sourceFilePath = $self->{problemPath}; # strip off template directory prefix $sourceFilePath =~ s|^$ce->{courseDirs}->{templates}/||; my $edit_level = $r->param("edit_level") || 0; $edit_level++; my $problemPage = $urlpath->newFromModule("WeBWorK::ContentGenerator::Instructor::PGProblemEditor", courseID => $courseName, setID => 'Undefined_Set', problemID => 'Undefined_Problem' ); $viewURL = $self->systemLink($problemPage, params=>{ sourceFilePath => $sourceFilePath, edit_level => $edit_level, file_type => 'source_path_for_problem_file', status_message => uri_escape($self->{status_message}) } ); } elsif ( $action eq 'add_set_header_to_set') { my $targetSetName = $r->param('target_set'); my $problemPage = $urlpath->newFromModule("WeBWorK::ContentGenerator::ProblemSet", courseID => $courseName, setID => $targetSetName ); $viewURL = $self->systemLink($problemPage, params => { displayMode => $displayMode, editMode => "savedFile", status_message => uri_escape($self->{status_message}) } ); } else { my $problemSetPage = $urlpath->newFromModule("WeBWorK::ContentGenerator::ProblemSet", courseID => $courseName, setID => $setName); $viewURL = $self->systemLink($problemSetPage, params => { displayMode => $displayMode, problemSeed => $problemSeed, editMode => ($action eq "save" ? "savedFile" : "temporaryFile"), status_message => uri_escape($self->{status_message}) } ); } last REDIRECT_CASES; }; ###################################### # course_info file type # redirect to ProblemSets.pm ###################################### $file_type eq 'course_info' and do { my $problemSetsPage = $urlpath->newFromModule("WeBWorK::ContentGenerator::ProblemSets", courseID => $courseName); $viewURL = $self->systemLink($problemSetsPage, params => { editMode => ($action eq "save" ? "savedFile" : "temporaryFile"), status_message => uri_escape($self->{status_message}) } ); last REDIRECT_CASES; }; # else if no redirect needed -- there must be an error. die "The file_type $file_type does not have a defined redirect procedure."; } # End REDIRECT_CASES if ($viewURL) { $self->reply_with_redirect($viewURL); } else { die "Invalid file_type $file_type specified by saveFileChanges"; } } 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 $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 $protected_file = (not -w $inputFilePath ) and -e $inputFilePath; #FIXME -- let's try to insure that the input file always exists, even for revert. $self->addbadmessage("Changes in this file have not yet been permanently saved.") if -r $tempFilePath; $self->addbadmessage("This file |$inputFilePath| is protected! To edit this text you must first use 'Save As' to save it to another file.") if $protected_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 $problemNumber = $r->urlpath->arg("problemID"); my $file_type = $self->{'file_type'} || ''; 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; } 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 = $r->urlpath->arg("setID") ; my $problemNumber = $r->urlpath->arg("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 = ($inputFilePath =~ /$TEMPFILESUFFIX/) ? 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 $file_type = $self->{file_type}; my $force_field = defined($r->param('sourceFilePath')) ? CGI::hidden(-name=>'sourceFilePath', -default=>$r->param('sourceFilePath')) : ''; my @allSetNames = sort $db->listGlobalSets; for (my $j=0; $j"POST", name=>"editor", action=>"$uri", target=>$target, enctype=>"application/x-www-form-urlencoded"}). $self->hidden_authen_fields. $force_field. CGI::hidden(-name=>'file_type',-default=>$self->{file_type}). CGI::hidden(-name=>'problemSeed',-default=>$problemSeed). CGI::hidden(-name=>'displayMode',-default=>$displayMode). CGI::hidden(-name=>'problemContents',-default=>$problemContents). CGI::submit(-value=>'View problem',-name=>'submit'). CGI::end_form(); # Prepare add to set buttons my $add_files_to_set_buttons = ''; if ($file_type eq 'problem' or $file_type eq 'source_path_for_problem_file' ) { $add_files_to_set_buttons .= CGI::submit(-value=>'Add problem to: ',-name=>'submit' ) ; } if ($file_type eq 'set_header' # set header or the problem number is not a regular positive number or ( $file_type =~ /problem/ and ($problemNumber =~ /\D|^0$|^$/ )) ){ $add_files_to_set_buttons .=CGI::submit(-value=>'Make this the set header for: ',-name=>'submit' ); } # Add pop-up menu for the target set if either of these buttons has been revealed. $add_files_to_set_buttons .= CGI::popup_menu(-name=>'target_set',-values=>\@allSetNames) if $add_files_to_set_buttons; return CGI::p($header), CGI::start_form({method=>"POST", name=>"editor", action=>"$uri", target=>$target, enctype=>"application/x-www-form-urlencoded"}), $self->hidden_authen_fields, $force_field, CGI::hidden(-name=>'file_type',-default=>$self->{file_type}), CGI::div( 'Seed: ', CGI::textfield(-name=>'problemSeed',-value=>$problemSeed), 'Mode: ', CGI::popup_menu(-name=>'displayMode', -values=>$mode_list, -default=>$displayMode), CGI::a({-href=>'http://webwork.math.rochester.edu/docs/docs/pglanguage/manpages/',-target=>"manpage_window"}, 'Manpages', ) ), CGI::p( CGI::textarea( -name => 'problemContents', -default => $problemContents, -rows => $rows, -columns => $columns, -override => 1, ), ), CGI::p( $add_files_to_set_buttons, CGI::br(), CGI::submit(-value=>'View problem',-name=>'submit'), $protected_file ? CGI::submit(-value=>'Save',-name=>'submit', -disabled=>1) : CGI::submit(-value=>'Save',-name=>'submit'), CGI::submit(-value=>'Revert', -name=>'submit'), CGI::submit(-value=>'Save as',-name=>'submit'), CGI::textfield(-name=>'save_to_new_file', -size=>40, -value=>""), ), CGI::end_form(); } ################################################################################ # Utilities ################################################################################ # 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. sub getFilePaths { my ($self, $setName, $problemNumber, $file_type, $TEMPFILESUFFIX) = @_; 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; die 'Internal error to PGProblemEditor -- file type is not defined' unless defined $file_type; ########################################################## # Determine path to the input file to be edited. # set EditFilePath to this value # # There are potentially four files in play # The permanent path of the input file == $editFilePath == $self->{problemPath} # A temporary path to the input file == $tempFilePath== "$editFilePath.$TEMPFILESUFFIX"== $self->{problemPath} ########################################################## # 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 -- $self->{problemPath} = $editFilePath; # path to file being read (temporary or permanent) # --- $self->{problemPath} = $problemPath; # contents of the file being read --- $problemContents # $self->{r_problemContents} = \$problemContents; # $self->{TEMPFILESUFFIX} = $TEMPFILESUFFIX; ########################################################################### 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 'blank_problem') and do { $editFilePath = $ce->{webworkFiles}->{screenSnippets}->{blankProblem}; $self->addbadmessage("$editFilePath is blank problem template file and cannot be edited directly."); $self->addbadmessage("Any changes you make will have to be saved as another file."); 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 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 "") { $editFilePath .= '/' . $header_file; } else { # if the set record doesn't specify the filename # then the set uses the default from snippets # so we'll load that file, but change where it will be saved # to and grey out the "Save" button # FIXME why does the make_local_copy variable need to be checked? # Isn't it automatic that a local copy has to be made? #if ($r->param('make_local_copy')) { $editFilePath = $ce->{webworkFiles}->{screenSnippets}->{setHeader} if $file_type eq 'set_header'; $editFilePath = $ce->{webworkFiles}->{hardcopySnippets}->{setHeader} if $file_type eq 'hardcopy_header'; $self->addbadmessage("$editFilePath is the default header file and cannot be edited directly."); $self->addbadmessage("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 = $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 = $r->param('sourceFilePath'); # bail if no source path for the problem is found ; die "Cannot find a file path to save to" unless( defined($forcedSourceFile) and ($forcedSourceFile =~ /\S/) ); $self->{problemSeed} = 1234; $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 $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 $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 = "$editFilePath.$TEMPFILESUFFIX"; $self->{editFilePath} = $editFilePath; $self->{tempFilePath} = $tempFilePath; $self->{inputFilePath} = (-r "$editFilePath.$TEMPFILESUFFIX") ? $tempFilePath : $editFilePath; } sub saveFileChanges { my ($self, $setName, $problemNumber, $file_type, $TEMPFILESUFFIX) = @_; 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; $file_type = '' unless defined $file_type; my $action = $self->{action}; my $editFilePath = $self->{editFilePath}; my $tempFilePath = $self->{tempFilePath}; ############################################################################## # 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. # These .tmp files are # removed when the file is finally saved. # Place the path of the file to be read in $problemPath. ############################################################################## my $problemContents = ''; my $outputFilePath = undef; # this is actually the output file for this subroutine # it is then read in as source in the body of this module my $do_not_save = 0; # flag to prevent saving of file my $editErrors = ''; ########################################################################## # For each of the actions define the following variables: # # path to permanent file -- $self->{problemPath} = $editFilePath; # path to file being read (temporary or permanent) # --- $self->{problemPath} = $problemPath; # contents of the file being read --- $problemContents # $self->{r_problemContents} = \$problemContents; # ################################# # handle button clicks ##### # Read contents of file ################################# ACTION_CASES: { ($action eq 'fresh_edit') and do { # this is a fresh editing job # the original file will be read in the body last ACTION_CASES; }; ($action eq 'revert') and do { # this is also fresh editing job $outputFilePath = undef; $self->addgoodmessage("Reverting to original file $editFilePath"); $self->{problemPath} = $editFilePath; last ACTION_CASES; }; ($action eq 'refresh') and do { # grab the problemContents from the form in order to save it to the tmp file # store tmp file name in the $self->problemPath for use in body $problemContents = $r->param('problemContents'); $outputFilePath = "$editFilePath.$TEMPFILESUFFIX"; $self->{problemPath} = $outputFilePath; last ACTION_CASES; }; ($action eq 'save') and do { # grab the problemContents from the form in order to save it to the permanent file # later we will unlink (delete) the temporary file # store permanent file name in the $self->problemPath for use in body $problemContents = $r->param('problemContents'); $outputFilePath = "$editFilePath"; $self->{problemPath} = $outputFilePath; #$self->addgoodmessage("Saving to file $outputFilePath"); last ACTION_CASES; }; ($action eq 'save_as') and do { my $new_file_name =$r->param('save_to_new_file') || ''; ################################################# #bail unless this new file name has been defined ################################################# if ( $new_file_name !~ /\S/) { # need a non-blank file name # setting $self->{failure} stops saving and any redirects $do_not_save = 1; warn "new file name is $new_file_name"; $self->addbadmessage(CGI::p("Please specify a file to save to.")); last ACTION_CASES; #stop processing } ################################################# # 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 # store new permanent file name in the $self->problemPath for use in body ################################################# $problemContents = $r->param('problemContents'); ################################################# # Rescue the user in case they forgot to end the file name with .pg ################################################# if($self->{file_type} eq 'problem' or $self->{file_type} eq 'blank_problem' or $self->{file_type} eq 'set_header') { $new_file_name =~ s/\.pg$//; # remove it if it is there $new_file_name .= '.pg'; # put it there } ################################################# # check to prevent overwrites: ################################################# $outputFilePath = $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 $outputFilePath exists. File not saved.")); } else { #$self->addgoodmessage("Saving to file $outputFilePath."); } $self->{problemPath} = $outputFilePath; last ACTION_CASES; }; ($action eq 'add_problem_to_set') and do { my $sourceFile = $editFilePath; my $targetSetName = $r->param('target_set'); my $freeProblemID = WeBWorK::Utils::max($db->listGlobalProblems($targetSetName)) + 1; $sourceFile =~ s|^$ce->{courseDirs}->{templates}/||; my $problemRecord = $self->addProblemToSet( setName => $targetSetName, sourceFile => $sourceFile, problemID => $freeProblemID ); $self->assignProblemToAllSetUsers($problemRecord); $self->addgoodmessage("Added $sourceFile to ". $targetSetName. " as problem $freeProblemID") ; $outputFilePath = undef; # don't save any files $self->{problemPath} = $editFilePath; }; ($action eq 'add_set_header_to_set') and do { my $sourceFile = $editFilePath; my $targetSetName = $r->param('target_set'); $sourceFile =~ s|^$ce->{courseDirs}->{templates}/||; my $setRecord = $db->getGlobalSet($targetSetName); $setRecord->set_header($sourceFile); if( $db->putGlobalSet($setRecord) ) { $self->addgoodmessage("Added $sourceFile to ". $targetSetName. " as new set header ") ; } else { $do_not_save = 1 ; $self->addbadmessage("Unable to make $sourceFile the set header for $targetSetName"); } # change file type to set_header if it not already so $self->{file_type} = 'set_header'; $outputFilePath = undef; # don't save any files $self->{problemPath} = $editFilePath; }; last ACTION_CASES; die "Unrecognized action command: $action"; }; # end ACTION_CASES ############################################################################## # 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 ( defined($outputFilePath) and $outputFilePath =~/\S/ and ! $do_not_save ) { # 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 $outputFilePath = WeBWorK::Utils::surePathToFile($ce->{courseDirs}->{templates}, $outputFilePath); 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->{failure} = $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 $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 $outputFilePath. Changes must be saved to another file for viewing."; } else { $errorMessage = "Unable to write to $outputFilePath: $writeFileErrors"; } $self->{failure} = 1; $self->addbadmessage(CGI::p($errorMessage)); } 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' or $action eq 'revert') { unlink($self->{tempFilePath}) ; } if ( defined($outputFilePath) and ! $self->{failure} ) { my $msg = "Saved to file: $outputFilePath"; $self->addgoodmessage($msg); } } # return values for use in the body subroutine # The path to the current permanent file being edited: # $self->{problemPath} = $editFilePath; # The path to the current temporary file (if any). If no temporary file this the same # as the permanent file path: # $self->{outputFilePath} = $outputFilePath; # $self->{r_problemContents} = \$problemContents; } # end saveFileChanges 1;