[system] / branches / gage_dev / webwork2 / lib / WeBWorK / Utils / CourseManagement.pm Repository:
ViewVC logotype

View of /branches/gage_dev/webwork2/lib/WeBWorK/Utils/CourseManagement.pm

Parent Directory Parent Directory | Revision Log Revision Log


Revision 6939 - (download) (as text) (annotate)
Mon Jul 18 19:16:04 2011 UTC (23 months ago) by gage
File size: 40364 byte(s)
add localizing files -- merge with trunk


    1 ################################################################################
    2 # WeBWorK Online Homework Delivery System
    3 # Copyright © 2000-2007 The WeBWorK Project, http://openwebwork.sf.net/
    4 # $CVSHeader: webwork2/lib/WeBWorK/Utils/CourseManagement.pm,v 1.48 2009/10/01 21:28:46 gage Exp $
    5 #
    6 # This program is free software; you can redistribute it and/or modify it under
    7 # the terms of either: (a) the GNU General Public License as published by the
    8 # Free Software Foundation; either version 2, or (at your option) any later
    9 # version, or (b) the "Artistic License" which comes with this package.
   10 #
   11 # This program is distributed in the hope that it will be useful, but WITHOUT
   12 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
   13 # FOR A PARTICULAR PURPOSE.  See either the GNU General Public License or the
   14 # Artistic License for more details.
   15 ################################################################################
   16 
   17 package WeBWorK::Utils::CourseManagement;
   18 use base qw(Exporter);
   19 
   20 =head1 NAME
   21 
   22 WeBWorK::Utils::CourseManagement - create, rename, and delete courses.
   23 
   24 =cut
   25 
   26 use strict;
   27 use warnings;
   28 use Carp;
   29 use DBI;
   30 use File::Path qw(rmtree);
   31 use File::Spec;
   32 use String::ShellQuote;
   33 use WeBWorK::CourseEnvironment;
   34 use WeBWorK::Debug;
   35 use WeBWorK::Utils qw(runtime_use readDirectory pretty_print_rh);
   36 use UUID::Tiny qw(create_uuid_as_string);
   37 #use WeBWorK::Utils::DBUpgrade;
   38 use PGcore; # for not_null() macro
   39 
   40 our @EXPORT    = ();
   41 our @EXPORT_OK = qw(
   42   listCourses
   43   listArchivedCourses
   44   addCourse
   45   renameCourse
   46   deleteCourse
   47   archiveCourse
   48   unarchiveCourse
   49   dbLayoutSQLSources
   50   initNonNativeTables
   51 
   52 );
   53 
   54 #   checkCourseTables
   55 #   updateCourseTables
   56 #   checkCourseDirectories
   57 
   58 =head1 FUNCTIONS
   59 
   60 =over
   61 
   62 =cut
   63 
   64 ################################################################################
   65 
   66 =item listCourses($ce)
   67 
   68 Lists the courses defined.
   69 
   70 =cut
   71 
   72 sub listCourses {
   73   my ($ce) = @_;
   74   my $coursesDir = $ce->{webworkDirs}->{courses};
   75   return grep { not (m/^\./ or m/^CVS$/) and -d "$coursesDir/$_" } readDirectory($coursesDir);
   76 }
   77 
   78 =item listArchivedCourses($ce)
   79 
   80 Lists the courses which have been archived (end in .tar.gz).
   81 
   82 =cut
   83 
   84 sub listArchivedCourses {
   85   my ($ce) = @_;
   86   my $coursesDir = $ce->{webworkDirs}->{courses};
   87   return grep { m/\.tar\.gz$/ } readDirectory($coursesDir);
   88 }
   89 
   90 ################################################################################
   91 
   92 =item addCourse(%options)
   93 
   94 %options must contain:
   95 
   96  courseID => $courseID,
   97  ce => $ce,
   98  courseOptions => $courseOptions,
   99  dbOptions => $dbOptions,
  100  users => $users
  101 
  102 %options may contain:
  103 
  104  templatesFrom => $templatesCourseID,
  105 
  106 Create a new course named $courseID.
  107 
  108 $ce is a WeBWorK::CourseEnvironment object that describes the new course's
  109 environment.
  110 
  111 $courseOptions is a reference to a hash containing the following options:
  112 
  113  dbLayoutName         => $dbLayoutName
  114  allowedRecipients    => $mail{allowedRecipients}
  115  feedbackRecipients   => $mail{feedbackRecipients}
  116  PRINT_FILE_NAMES_FOR => $pg{specialPGEnvironmentVars}->{PRINT_FILE_NAMES_FOR}
  117 
  118 C<dbLayoutName> is required. C<allowedRecipients>, C<feedbackRecipients>, and
  119 C<PRINT_FILE_NAMES_FOR> are references to arrays.
  120 
  121 $dbOptions is a reference to a hash containing information required to create a
  122 database for the course. Current database layouts do not require additional
  123 information, so specify a reference to an empty hash. If $dbOptions is
  124 undefined, addCourse() assumes that the database has already been created, and
  125 skips that step in the course creation process.
  126 
  127 $users is a list of arrayrefs, each containing a User, Password, and
  128 PermissionLevel record for a single user:
  129 
  130  $users = [ $User, $Password, $PermissionLevel ]
  131 
  132 These users are added to the course.
  133 
  134 $templatesCourseID indicates the ID of a course from which the contents of the
  135 templates directory will be copied to the new course.
  136 
  137 =cut
  138 
  139 sub addCourse {
  140   my (%options) = @_;
  141 
  142   my $courseID = $options{courseID};
  143   my $ce = $options{ce};
  144   my %courseOptions = %{ $options{courseOptions} };
  145   my %dbOptions = defined $options{dbOptions} ? %{ $options{dbOptions} } : ();
  146   my @users = exists $options{users} ? @{ $options{users} } : ();
  147 
  148   # get the database layout out of the options hash
  149   my $dbLayoutName = $courseOptions{dbLayoutName};
  150 
  151   # collect some data
  152   my $coursesDir = $ce->{webworkDirs}->{courses};
  153   my $courseDir = "$coursesDir/$courseID";
  154 
  155   # fail if the course already exists
  156   # IMPORTANT: this must be the first check! if any check other than this one
  157   # fails, CourseAdmin deletes the course!! Oh no!!!
  158   # DO NOT CHANGE THE DIE MESSAGE -- CourseAdmin checks it to determine whether
  159   # a course was partially created and should be deleted!
  160   # FIXME -- this is bad, and addCourse should deal with cleaning up partially
  161   # created courses itself
  162   if (-e $courseDir) {
  163     croak "$courseID: course exists";
  164   }
  165 
  166   # fail if the course ID contains invalid characters
  167   croak "Invalid characters in course ID: '$courseID' (valid characters are [-A-Za-z0-9_])"
  168     unless $courseID =~ m/^[-A-Za-z0-9_]*$/;
  169 
  170   # if we didn't get a database layout, use the default one
  171   if (not defined $dbLayoutName) {
  172     $dbLayoutName = $ce->{dbLayoutName};
  173   }
  174 
  175   # fail if the database layout is invalid
  176   if (not exists $ce->{dbLayouts}->{$dbLayoutName}) {
  177     croak "$dbLayoutName: not found in \%dbLayouts";
  178   }
  179 
  180   ##### step 1: create course directory structure #####
  181 
  182   my %courseDirs = %{$ce->{courseDirs}};
  183 
  184   # deal with root directory first -- if we can't create it, we have to give up.
  185 
  186   exists $courseDirs{root} or croak "Can't create the course '$courseID' because no root directory is specified in the '%courseDirs' hash.";
  187   my $root = $courseDirs{root};
  188   delete $courseDirs{root};
  189   {
  190     # does the directory already exist?
  191     -e $root and croak "Can't create the course '$courseID' because the root directory '$root' already exists.";
  192     # is the parent directory writeable?
  193     my @rootElements = File::Spec->splitdir($root);
  194     pop @rootElements;
  195     my $rootParent = File::Spec->catdir(@rootElements);
  196     -w $rootParent or croak "Can't create the course '$courseID' because the courses directory '$rootParent' is not writeable.";
  197     # try to create it
  198     mkdir $root or croak "Can't create the course '$courseID' becasue the root directory '$root' could not be created: $!.";
  199   }
  200 
  201   # deal with the rest of the directories
  202 
  203   my @courseDirNames = sort { $courseDirs{$a} cmp $courseDirs{$b} } keys %courseDirs;
  204   foreach my $courseDirName (@courseDirNames) {
  205     my $courseDir = File::Spec->canonpath($courseDirs{$courseDirName});
  206 
  207     # does the directory already exist?
  208     if (-e $courseDir) {
  209       warn "Can't create $courseDirName directory '$courseDir', since it already exists. Using existing directory.\n";
  210       next;
  211     }
  212 
  213     # is the parent directory writeable?
  214     my @courseDirElements = File::Spec->splitdir($courseDir);
  215     pop @courseDirElements;
  216     my $courseDirParent = File::Spec->catdir(@courseDirElements);
  217     unless (-w $courseDirParent) {
  218       warn "Can't create $courseDirName directory '$courseDir', since the parent directory is not writeable. You will have to create this directory manually.\n";
  219       next;
  220     }
  221 
  222     # try to create it
  223     mkdir $courseDir or warn "Failed to create $courseDirName directory '$courseDir': $!. You will have to create this directory manually.\n";
  224   }
  225 
  226   ##### step 2: create course database #####
  227 
  228   my $db = new WeBWorK::DB($ce->{dbLayouts}->{$dbLayoutName});
  229   my $create_db_result = $db->create_tables;
  230   die "$courseID: course database creation failed.\n" unless $create_db_result;
  231 
  232   ##### step 3: populate course database #####
  233 
  234   if ($ce->{dbLayouts}{$dbLayoutName}{user}{params}{non_native}) {
  235     debug("not adding users to the course database: 'user' table is non-native.\n");
  236   } else {
  237     # see above
  238     #my $db = WeBWorK::DB->new($ce->{dbLayouts}->{$dbLayoutName});
  239 
  240     foreach my $userTriple (@users) {
  241       my ($User, $Password, $PermissionLevel) = @$userTriple;
  242 
  243       eval { $db->addUser($User)                       }; warn $@ if $@;
  244       eval { $db->addPassword($Password)               }; warn $@ if $@;
  245       eval { $db->addPermissionLevel($PermissionLevel) }; warn $@ if $@;
  246     }
  247   }
  248 
  249   ##### step 4: write course.conf file #####
  250 
  251   my $courseEnvFile = $ce->{courseFiles}->{environment};
  252   open my $fh, ">", $courseEnvFile
  253     or die "failed to open $courseEnvFile for writing.\n";
  254   writeCourseConf($fh, $ce, %courseOptions);
  255   close $fh;
  256 
  257   ##### step 5: copy templates #####
  258 
  259   if (exists $options{templatesFrom}) {
  260     my $sourceCourse = $options{templatesFrom};
  261     my $sourceCE = new WeBWorK::CourseEnvironment({
  262       get_SeedCE($ce),
  263       courseName => $sourceCourse,        # override courseName
  264     });
  265     my $sourceDir = $sourceCE->{courseDirs}->{templates};
  266 
  267     if (-d $sourceDir) {
  268       my $destDir = $ce->{courseDirs}{templates};
  269       my $cp_cmd = "2>&1 " . $ce->{externalPrograms}{cp} . " -R " . shell_quote($sourceDir) . "/* " . shell_quote($destDir);
  270       my $cp_out = readpipe $cp_cmd;
  271       if ($?) {
  272         my $exit = $? >> 8;
  273         my $signal = $? & 127;
  274         my $core = $? & 128;
  275         warn "Failed to copy templates from course '$sourceCourse' with command '$cp_cmd' (exit=$exit signal=$signal core=$core): $cp_out\n";
  276       }
  277     } else {
  278       warn "Failed to copy templates from course '$sourceCourse': templates directory '$sourceDir' does not exist.\n";
  279     }
  280   }
  281 }
  282 
  283 ################################################################################
  284 
  285 =item renameCourse(%options)
  286 
  287 %options must contain:
  288 
  289  courseID => $courseID,
  290  ce => $ce,
  291  dbOptions => $dbOptions,
  292  newCourseID => $newCourseID,
  293 
  294 %options may also contain:
  295 
  296  skipDBRename => $skipDBRename,
  297 
  298 Rename the course named $courseID to $newCourseID.
  299 
  300 $ce is a WeBWorK::CourseEnvironment object that describes the existing course's
  301 environment.
  302 
  303 $dbOptions is a reference to a hash containing information required to create
  304 the course's new database and delete the course's old database. Current database
  305 layouts do not require additional information, so specify a reference to an
  306 empty hash.
  307 
  308 The name of the course's directory is changed to $newCourseID.
  309 
  310 If the course's database layout is C<sql_single> or C<sql_moodle>, new tables
  311 are created in the current database, course data is copied from the old tables
  312 to the new tables, and the old tables are deleted.
  313 
  314 If the course's database layout is something else, no database changes are made.
  315 
  316 If $skipDBRename is true, no database changes are made. This is useful if a
  317 course is being unarchived and no database was found, or for renaming the
  318 modelCourse.
  319 
  320 Any errors encountered while renaming the course are returned.
  321 
  322 =cut
  323 
  324 sub renameCourse {
  325   my (%options) = @_;
  326 
  327   # renameCourseHelper needs:
  328   #    $fromCourseID ($oldCourseID)
  329   #    $fromCE ($oldCE)
  330   #    $toCourseID ($newCourseID)
  331   #    $toCE (construct from $oldCE)
  332   #    $dbLayoutName ($oldCE->{dbLayoutName})
  333   #    %options ($dbOptions)
  334 
  335   my $oldCourseID = $options{courseID};
  336   my $oldCE = $options{ce};
  337   my %dbOptions = defined $options{dbOptions} ? %{ $options{dbOptions} } : ();
  338   my $newCourseID = $options{newCourseID};
  339   my $skipDBRename = $options{skipDBRename} || 0;
  340 
  341   # get the database layout out of the options hash
  342   my $dbLayoutName = $oldCE->{dbLayoutName};
  343 
  344   # collect some data
  345   my $coursesDir = $oldCE->{webworkDirs}->{courses};
  346   my $oldCourseDir = "$coursesDir/$oldCourseID";
  347   my $newCourseDir = "$coursesDir/$newCourseID";
  348 
  349   # fail if the target course already exists
  350   if (-e $newCourseDir) {
  351     croak "$newCourseID: course exists";
  352   }
  353 
  354   # fail if the source course does not exist
  355   unless (-e $oldCourseDir) {
  356     croak "$oldCourseID: course not found";
  357   }
  358 
  359   ##### step 1: move course directory #####
  360 
  361   # move top-level course directory
  362   my $mv_cmd = "2>&1"." ".$oldCE->{externalPrograms}{mv}." ".shell_quote($oldCourseDir)." ".shell_quote($newCourseDir);
  363   debug("moving course dir: $mv_cmd");
  364   my $mv_out = readpipe $mv_cmd;
  365   if ($?) {
  366     my $exit = $? >> 8;
  367     my $signal = $? & 127;
  368     my $core = $? & 128;
  369     die "Failed to move course directory with command '$mv_cmd' (exit=$exit signal=$signal core=$core): $mv_out\n";
  370   }
  371 
  372   # get new course environment
  373   my $newCE = $oldCE->new(
  374     $oldCE->{webworkDirs}->{root},
  375     $oldCE->{webworkURLs}->{root},
  376     $oldCE->{pg}->{directories}->{root},
  377     $newCourseID,
  378   );
  379 
  380   # find the course dirs that still exist in their original locations
  381   # (i.e. are not subdirs of $courseDir)
  382   my %oldCourseDirs = %{ $oldCE->{courseDirs} };
  383   my %newCourseDirs = %{ $newCE->{courseDirs} };
  384   my @courseDirNames = sort { $oldCourseDirs{$a} cmp $oldCourseDirs{$b} } keys %oldCourseDirs;
  385   foreach my $courseDirName (@courseDirNames) {
  386     my $oldDir = File::Spec->canonpath($oldCourseDirs{$courseDirName});
  387     my $newDir = File::Spec->canonpath($newCourseDirs{$courseDirName});
  388     if (-e $oldDir) {
  389       debug("oldDir $oldDir still exists. might move it...\n");
  390 
  391       # check for a few likely error conditions, since the mv error is not that helpful
  392 
  393       # is the source really a directory
  394       unless (-d $oldDir) {
  395         warn "$courseDirName: Can't move '$oldDir' to '$newDir', since the source is not a directory. You will have to move this directory manually.\n";
  396         next;
  397       }
  398 
  399       # does the destination already exist?
  400       # (this should only happen on extra-coursedir directories, since we make sure the root dir doesn't exist above.)
  401       if (-e $newDir) {
  402         warn "$courseDirName: Can't move '$oldDir' to '$newDir', since the target already exists. You will have to move this directory manually.\n";
  403         next;
  404       }
  405 
  406       # is oldDir's parent writeable
  407       my @oldDirElements = File::Spec->splitdir($oldDir);
  408       pop @oldDirElements;
  409       my $oldDirParent = File::Spec->catdir(@oldDirElements);
  410       unless (-w $oldDirParent) {
  411         warn "$courseDirName: Can't move '$oldDir' to '$newDir', since the source parent directory is not writeable. You will have to move this directory manually.\n";
  412         next;
  413       }
  414 
  415       # is newDir's parent writeable?
  416       my @newDirElements = File::Spec->splitdir($newDir);
  417       pop @newDirElements;
  418       my $newDirParent = File::Spec->catdir(@newDirElements);
  419       unless (-w $newDirParent) {
  420         warn "$courseDirName: Can't move '$oldDir' to '$newDir', since the destination parent directory is not writeable. You will have to move this directory manually.\n";
  421         next;
  422       }
  423 
  424       # try to move the directory
  425       debug("Going to move $oldDir to $newDir...\n");
  426       my $mv_cmd = "2>&1"." ".$oldCE->{externalPrograms}{mv}." ".shell_quote($oldDir)." ".shell_quote($newDir);
  427       my $mv_out = readpipe $mv_cmd;
  428       if ($?) {
  429         my $exit = $? >> 8;
  430         my $signal = $? & 127;
  431         my $core = $? & 128;
  432         warn "Failed to move directory with command '$mv_cmd' (exit=$exit signal=$signal core=$core): $mv_out\n";
  433       }
  434     } else {
  435       debug("oldDir $oldDir was already moved.\n");
  436     }
  437   }
  438 
  439   ##### step 2: rename database #####
  440 
  441   unless ($skipDBRename) {
  442     my $oldDB = new WeBWorK::DB($oldCE->{dbLayouts}{$dbLayoutName});
  443     my $rename_db_result = $oldDB->rename_tables($newCE->{dbLayouts}{$dbLayoutName});
  444     die "$oldCourseID: course database renaming failed.\n" unless $rename_db_result;
  445   }
  446 }
  447 
  448 ################################################################################
  449 
  450 =item deleteCourse(%options)
  451 
  452 Options must contain:
  453 
  454  courseID => $courseID,
  455  ce => $ce,
  456  dbOptions => $dbOptions,
  457 
  458 $ce is a WeBWorK::CourseEnvironment object that describes the course's
  459 environment. It is your responsability to pass a course environment object that
  460 describes the course to be deleted. Do not pass the course environment object
  461 associated with the request, unless you are deleting the course you're currently
  462 using.
  463 
  464 $dbOptions is a reference to a hash containing information required to delete
  465 the database for the course. Current database layouts do not require additional
  466 information, so specify a reference to an empty hash. If $dbOptions is
  467 undefined, addCourse() assumes that the database has already been deleted, and
  468 skips that step in the course deletion process.
  469 
  470 Deletes the course named $courseID. The course directory is removed.
  471 
  472 Any errors encountered while deleting the course are returned.
  473 
  474 =cut
  475 
  476 sub deleteCourse {
  477   my (%options) = @_;
  478 
  479   my $courseID = $options{courseID};
  480   my $ce = $options{ce};
  481   my %dbOptions = defined $options{dbOptions} ? %{ $options{dbOptions} } : ();
  482 
  483   # make sure the user isn't brain damaged
  484   die "the course environment supplied doesn't appear to describe the course $courseID. can't proceed."
  485     unless $ce->{courseName} eq $courseID;
  486 
  487   my %courseDirs = %{$ce->{courseDirs}};
  488 
  489   ##### step 0: make sure course directory is deleteable #####
  490 
  491   # deal with root directory first -- if we won't be able to delete it, we have to give up.
  492 
  493   exists $courseDirs{root} or croak "Can't delete the course '$courseID' because no root directory is specified in the '%courseDirs' hash.";
  494   my $root = $courseDirs{root};
  495   if (-e $root) {
  496     # is the parent directory writeable?
  497     my @rootElements = File::Spec->splitdir($root);
  498     pop @rootElements;
  499     my $rootParent = File::Spec->catdir(@rootElements);
  500     -w $rootParent or croak "Can't delete the course '$courseID' because the courses directory '$rootParent' is not writeable.";
  501   } else {
  502     warn "Warning: the course root directory '$root' does not exist. Attempting to delete the course database and other course directories...\n";
  503   }
  504 
  505   ##### step 1: delete course database (if necessary) #####
  506 
  507   my $dbLayoutName = $ce->{dbLayoutName};
  508   my $db = new WeBWorK::DB($ce->{dbLayouts}->{$dbLayoutName});
  509   my $create_db_result = $db->delete_tables;
  510   die "$courseID: course database deletion failed.\n" unless $create_db_result;
  511 
  512   ##### step 2: delete course directory structure #####
  513 
  514   my @courseDirNames = sort { $courseDirs{$a} cmp $courseDirs{$b} } keys %courseDirs;
  515   foreach my $courseDirName (@courseDirNames) {
  516     my $courseDir = File::Spec->canonpath($courseDirs{$courseDirName});
  517     if (-e $courseDir) {
  518       debug("courseDir $courseDir still exists. might delete it...\n");
  519 
  520       # check for a few likely error conditions, since the mv error is not that helpful
  521 
  522       # is it really a directory
  523       unless (-d $courseDir) {
  524         warn "Can't delete $courseDirName directory '$courseDir', since is not a directory. If it is not wanted, you will have to delete it manually.\n";
  525         next;
  526       }
  527 
  528       # is the parent writeable
  529       my @courseDirElements = File::Spec->splitdir($courseDir);
  530       pop @courseDirElements;
  531       my $courseDirParent = File::Spec->catdir(@courseDirElements);
  532       unless (-w $courseDirParent) {
  533         warn "Can't delete $courseDirName directory '$courseDir', since its parent directory is not writeable. If it is not wanted, you will have to delete it manually.\n";
  534         next;
  535       }
  536 
  537       # try to delete the directory
  538       debug("Going to delete $courseDir...\n");
  539       rmtree($courseDir, 0, 1);
  540     } else {
  541       debug("courseDir $courseDir was already deleted.\n");
  542     }
  543   }
  544 }
  545 
  546 ################################################################################
  547 
  548 =item archiveCourse(%options)
  549 
  550 %options must contain:
  551 
  552  courseID  => $courseID,
  553  ce        => $ce,
  554 
  555 Creates a gzipped tar archive (.tar.gz) of the course $courseID in the WeBWorK
  556 courses directory. Before archiving, the course database is dumped into a
  557 subdirectory of the course's DATA directory.
  558 
  559 Only files and directories stored directly in the course directory are archived.
  560 The contents of linked files is not archived although the symbolic links
  561 themselves are saved.
  562 
  563 $courseID is the name of the course to archive.
  564 
  565 $ce is a WeBWorK::CourseEnvironment object that describes the course's
  566 environment. (This is used to access the course database and get path
  567 information.)
  568 
  569 If an error occurs, an exception is thrown.
  570 
  571 =cut
  572 
  573 sub archiveCourse {
  574   my (%options) = @_;
  575   my $courseID = $options{courseID};
  576   my $ce = $options{ce};
  577 
  578   # make sure the user isn't brain damaged
  579   croak "The course environment supplied doesn't appear to match the course $courseID. Can't proceed"
  580     unless $ce->{courseName} eq $courseID;
  581 
  582   # grab some values we'll need
  583   my $course_dir = $ce->{courseDirs}{root};
  584 
  585   # tmp_archive_path is used as the target of the tar.gz operation
  586   # After this is done the final tar.gz file is moved either to the course directory
  587   # or the course/myCourse/templates   directory (when saving individual courses)
  588   # this prevents us from tarring a directory to which we have just added a file
  589   # see bug #2022 -- for error messages on some operating systems
  590   my $uuidStub = create_uuid_as_string();
  591   my $tmp_archive_path = $ce->{webworkDirs}{courses} . "/ ${uuidStub}_$courseID.tar.gz";
  592   my $data_dir = $ce->{courseDirs}{DATA};
  593   my $dump_dir = "$data_dir/mysqldump";
  594   my $archive_path;
  595   if ( PGcore::not_null( $options{archive_path} ) ) {
  596     $archive_path = $options{archive_path};
  597   } else {
  598     $archive_path = $ce->{webworkDirs}{courses} . "/$courseID.tar.gz";
  599   }
  600 
  601 
  602   # fail if the source course does not exist
  603   unless (-e $course_dir) {
  604     croak "$courseID: course not found";
  605   }
  606 
  607     # replace previous archived file if it exists.
  608   if (-e $archive_path) {
  609     unlink($archive_path) if (-w $archive_path);
  610     unless (-e $archive_path) {
  611       print CGI::p({-style=>'color:red; font-weight:bold'}, "The archival version of '$courseID' has been replaced'.\n");
  612     } else {
  613       croak "Unable to replace the archival version of '$courseID'";
  614     }
  615   }
  616 
  617   #### step 1: dump tables #####
  618 
  619   unless (-e $dump_dir) {
  620     mkdir $dump_dir or croak "Failed to create course database dump directory '$dump_dir': $!";
  621   }
  622 
  623   my $db = new WeBWorK::DB($ce->{dbLayout});
  624   my $dump_db_result = $db->dump_tables($dump_dir);
  625   unless ($dump_db_result) {
  626     _archiveCourse_remove_dump_dir($ce, $dump_dir);
  627     croak "$courseID: course database dump failed.\n";
  628   }
  629 
  630   ##### step 2: tar and gzip course directory (including dumped database) #####
  631 
  632   # we want tar to run from the parent directory of the course directory
  633   my $chdir_to = "$course_dir/..";
  634 
  635   my $tar_cmd = "2>&1 " . $ce->{externalPrograms}{tar}
  636     . " -C " . shell_quote($chdir_to)
  637     . " -czf " . shell_quote($tmp_archive_path)
  638     . " " . shell_quote($courseID);
  639   my $tar_out = readpipe $tar_cmd;
  640   if ($?) {
  641     my $exit = $? >> 8;
  642     my $signal = $? & 127;
  643     my $core = $? & 128;
  644     _archiveCourse_remove_dump_dir($ce, $dump_dir);
  645     croak "Failed to archive course directory '$course_dir' with command '$tar_cmd' (exit=$exit signal=$signal core=$core): $tar_out\n";
  646   }
  647 
  648   ##### step 3: cleanup -- remove database dump files from course directory #####
  649 
  650   unless (-e $archive_path) {
  651     rename $tmp_archive_path, $archive_path;
  652   } else {
  653     croak "Failed to create archived file at  '$archive_path'. File already exists.";
  654     unlink($tmp_archive_path);  #clean up
  655   }
  656   _archiveCourse_remove_dump_dir($ce, $dump_dir);
  657 }
  658 
  659 sub _archiveCourse_remove_dump_dir {
  660   my ($ce, $dump_dir) = @_;
  661   my $rm_cmd = "2>&1 " . $ce->{externalPrograms}{rm}
  662     . " -rf " . shell_quote($dump_dir);
  663   my $rm_out = readpipe $rm_cmd;
  664   if ($?) {
  665     my $exit = $? >> 8;
  666     my $signal = $? & 127;
  667     my $core = $? & 128;
  668     carp "Failed to remove course database dump directory '$dump_dir' with command '$rm_cmd' (exit=$exit signal=$signal core=$core): $rm_out\n";
  669   }
  670 }
  671 
  672 ################################################################################
  673 
  674 =item unarchiveCourse(%options)
  675 
  676 %options must contain:
  677 
  678  oldCourseID => $oldCourseID,
  679  archivePath => $archivePath,
  680  ce          => $ce,
  681 
  682 %options may also contain:
  683 
  684  newCourseID => $newCourseID,
  685 
  686 Restores course $oldCourseID from a gzipped tar archive (.tar.gz) located at
  687 $archivePath. After unarchiving, the course database is restored from a
  688 subdirectory of the course's DATA directory.
  689 
  690 If $newCourseID is defined and differs from $oldCourseID, the course is renamed
  691 after unarchiving.
  692 
  693 $ce is a WeBWorK::CourseEnvironment object that describes the some course's
  694 environment. (Usually this would be the admin course.) This is used to access
  695 the course database and get path information.
  696 
  697 If an error occurs, an exception is thrown.
  698 
  699 =cut
  700 
  701 sub unarchiveCourse {
  702   my (%options) = @_;
  703 
  704   my $newCourseID = $options{newCourseID};
  705   my $currCourseID = $options{oldCourseID};
  706   my $archivePath = $options{archivePath};
  707   my $ce = $options{ce};
  708 
  709   my $coursesDir  = $ce->{webworkDirs}{courses};
  710 
  711   # Double check that the new course does not exist
  712   if (-e "$coursesDir/$newCourseID") {
  713     die "Cannot overwrite existing course $coursesDir/$newCourseID";
  714   }
  715 
  716   ##### step 1: move a conflicting course away #####
  717 
  718   # if this function returns undef, it means there was no course in the way
  719   my $restoreCourseData = _unarchiveCourse_move_away($ce, $currCourseID);
  720 
  721   ##### step 2: crack open the tarball #####
  722 
  723   my $tar_cmd = "2>&1 " . $ce->{externalPrograms}{tar}
  724     . " -C " . shell_quote($coursesDir)
  725     . " -xzf " . shell_quote($archivePath);
  726   my $tar_out = readpipe $tar_cmd;
  727   if ($?) {
  728     my $exit = $? >> 8;
  729     my $signal = $? & 127;
  730     my $core = $? & 128;
  731     _unarchiveCourse_move_back($restoreCourseData);
  732     die "Failed to unarchive course directory with command '$tar_cmd' (exit=$exit signal=$signal core=$core): $tar_out\n";
  733   }
  734 
  735   ##### step 3: read the course environment for this course #####
  736 
  737   my $ce2 = new WeBWorK::CourseEnvironment({
  738     get_SeedCE($ce),
  739     courseName => $currCourseID,
  740   });
  741 
  742   # pull out some useful stuff
  743   my $course_dir = $ce2->{courseDirs}{root};
  744   my $data_dir = $ce2->{courseDirs}{DATA};
  745   my $dump_dir = "$data_dir/mysqldump";
  746   my $old_dump_file = "$data_dir/${currCourseID}_mysql.database";
  747 
  748   ##### step 4: restore the database tables #####
  749 
  750   my $no_database;
  751   my $restore_db_result = 1;
  752   if (-e $dump_dir) {
  753     my $db = new WeBWorK::DB($ce2->{dbLayout});
  754     $restore_db_result = $db->restore_tables($dump_dir);
  755   } elsif (-e $old_dump_file) {
  756     my $dbLayoutName = $ce2->{dbLayoutName};
  757     if (ref getHelperRef("unarchiveCourseHelper", $dbLayoutName)) {
  758       eval {
  759         $restore_db_result = unarchiveCourseHelper($currCourseID, $ce2, $dbLayoutName,
  760           unarchiveDatabasePath=>$old_dump_file);
  761       };
  762       if ($@) {
  763         warn "failed to unarchive course database from dump file '$old_dump_file: $@\n";
  764       }
  765     } else {
  766       warn "course '$currCourseID' uses dbLayout '$dbLayoutName', which doesn't support restoring database tables. database tables will not be restored.\n";
  767       $no_database = 1;
  768     }
  769   } else {
  770     warn "course '$currCourseID' has no database dump in its data directory (checked for $dump_dir and $old_dump_file). database tables will not be restored.\n";
  771     $no_database = 1;
  772   }
  773 
  774   unless ($restore_db_result) {
  775     warn "database restore of course '$currCourseID' failed: the course will probably not be usable.\n";
  776   }
  777 
  778   ##### step 5: delete dump_dir and/or old_dump_file #####
  779 
  780   if (-e $dump_dir) {
  781     _archiveCourse_remove_dump_dir($ce, $dump_dir);
  782   }
  783   if (-e $old_dump_file) {
  784     unlink $old_dump_file or carp "Failed to unlink course database dump file '$old_dump_file: $_\n";
  785   }
  786 
  787   ##### step 6: rename course #####
  788 
  789   if (defined $newCourseID and $newCourseID ne $currCourseID) {
  790     renameCourse(
  791       courseID     => $currCourseID,
  792       ce           => $ce2,
  793       newCourseID  => $newCourseID,
  794       skipDBRename => $no_database,
  795     );
  796   }
  797 
  798   ##### step 7: return conflicting course to its rightful place #####
  799 
  800   _unarchiveCourse_move_back($restoreCourseData);
  801 }
  802 
  803 sub _unarchiveCourse_move_away {
  804   my ($ce, $courseID) = @_;
  805 
  806   # course environment for before the course is moved
  807   my $ce2 = new WeBWorK::CourseEnvironment({
  808     get_SeedCE($ce),
  809     courseName => $courseID,
  810   });
  811 
  812   # if course directory doesn't exist, we don't have to do anything
  813   return unless -e $ce2->{courseDirs}{root};
  814 
  815   # temporary name for course
  816   my $tmpCourseID = "${courseID}_tmp";
  817 
  818   debug("Temporarily moving $courseID to $tmpCourseID to make room for course unarchiving");
  819   renameCourse(
  820     courseID    => $courseID,
  821     ce          => $ce2,
  822     newCourseID => $tmpCourseID,
  823   );
  824 
  825   # course environment for after the course is moved
  826   my $ce3 = new WeBWorK::CourseEnvironment({
  827     get_SeedCE($ce),
  828     courseName => $tmpCourseID,
  829   });
  830 
  831   # data to pass to renameCourse when moving the course back to it's original name
  832   my $restore_course_data = {
  833     courseID    => $tmpCourseID,
  834     ce          => $ce3, # course environment for moved course
  835     newCourseID => $courseID,
  836   };
  837 
  838   return $restore_course_data;
  839 }
  840 
  841 sub _unarchiveCourse_move_back {
  842   my ($restore_course_data) = @_;
  843 
  844   return unless $restore_course_data;
  845 
  846   debug("Moving $$restore_course_data{courseID} back to $$restore_course_data{newCourseID} after course unarchiving");
  847   renameCourse(%$restore_course_data);
  848 }
  849 
  850 ################################################################################
  851 
  852 =item dbLayoutSQLSources($dbLayout)
  853 
  854 Retrun a hash of database sources for the sql and sql_single database layouts.
  855 Each element of the hash takes this form:
  856 
  857  dbi_source => {
  858      tables => [ 'table1', 'table2', ... ],
  859      username => 'username',
  860      password => 'password',
  861  }
  862 
  863 In the common case, there will only be one source returned.
  864 
  865 =cut
  866 
  867 sub dbLayoutSQLSources {
  868   my ($dbLayout) = @_;
  869 
  870   my %dbLayout = %$dbLayout;
  871   my @tables = keys %dbLayout;
  872 
  873   my %sources;
  874 
  875   foreach my $table (@tables) {
  876     my %table = %{ $dbLayout{$table} };
  877     my %params = %{ $table{params} };
  878 
  879     if ($params{non_native}) {
  880       debug("$table: marked non-native, skipping\n");
  881       next;
  882     }
  883 
  884     my $source = $table{source};
  885     my $username = $params{username};
  886     my $password = $params{password};
  887 
  888     push @{$sources{$source}{tables}}, $table;
  889 
  890     if (defined $sources{$source}{username}) {
  891       if ($sources{$source}{username} ne $username) {
  892         warn "conflicting usernames for source '$source':",
  893           " '$sources{$source}{username}', '$username'\n";
  894       } else {
  895         # it's all good
  896       }
  897     } else {
  898       $sources{$source}{username} = $username;
  899     }
  900 
  901     if (defined $sources{$source}{password}) {
  902       if ($sources{$source}{password} ne $password) {
  903         warn "conflicting passwords for source '$source':",
  904           " '$sources{$source}{password}', '$password'\n";
  905       } else {
  906         # it's all good
  907       }
  908     } else {
  909       $sources{$source}{password} = $password;
  910     }
  911   }
  912 
  913   return %sources;
  914 }
  915 
  916 =back
  917 
  918 =cut
  919 
  920 ################################################################################
  921 # database helpers
  922 ################################################################################
  923 
  924 =head1 DATABASE-LAYOUT SPECIFIC HELPER FUNCTIONS
  925 
  926 These functions are used to perform database-layout specific operations.
  927 
  928 The implementations in this class do nothing, but if an appropriate function
  929 exists in a class with the name
  930 WeBWorK::Utils::CourseManagement::I<$dbLayoutName>, it will be used instead.
  931 
  932 =over
  933 
  934 =item archiveCourseHelper($courseID, $ce, $dbLayoutName, %options)
  935 
  936 Perform database-layout specific operations for archiving the data in a course.
  937 
  938 =cut
  939 
  940 sub archiveCourseHelper {
  941   my ($courseID, $ce, $dbLayoutName, %options) = @_;
  942   my $result = callHelperIfExists("archiveCourseHelper", $dbLayoutName, @_);
  943   return $result;
  944 }
  945 
  946 =item unarchiveCourseHelper($courseID, $ce, $dbLayoutName, %options)
  947 
  948 Perform database-layout specific operations for unarchiving the data in a course
  949 and placing it in the database.
  950 
  951 =cut
  952 
  953 sub unarchiveCourseHelper {
  954   my ($courseID, $ce, $dbLayoutName, %options) = @_;
  955   my $result = callHelperIfExists("unarchiveCourseHelper", $dbLayoutName, @_);
  956   return $result;
  957 }
  958 
  959 =item initNonNativeTables($ce, $db, $dbLayoutName, %options)
  960 
  961 Perform database-layout specific operations for initializing non-native database tables
  962 that are not associated with a particular course
  963 
  964 =cut
  965 
  966 sub initNonNativeTables {
  967   my($ce, $dbLayoutName, %options) = @_;
  968   my $str = '';
  969   # Create a database handler
  970   my $db = new WeBWorK::DB($ce->{dbLayouts}->{$dbLayoutName});
  971 
  972    # lock database
  973 
  974   # Find the names of the non-native database tables
  975   foreach my $table (sort keys %$db) {
  976       next unless $db->{$table}{params}{non_native}; # only look at non-native tables
  977       my $database_table_name = (exists $db->{$table}->{params}->{tableOverride})? $db->{$table}->{params}->{tableOverride}:$table;
  978       #warn "table is $table";
  979       #warn "checking $database_table_name";
  980       my $database_table_exists = ($db->{$table}->tableExists) ? 1:0;
  981       unless ($database_table_exists ) { # exists means the table can be described;
  982         my $schema_obj = $db->{$table};
  983         if ($schema_obj->can("create_table")) {
  984           #warn "creating table $database_table_name  with object $schema_obj";
  985         $schema_obj->create_table;
  986         $str .= "Table '$table' created as '$database_table_name' in database.".CGI::br();
  987       } else {
  988         # warn "Skipping creation of '$table' table: no create_table method\n";
  989       }
  990 
  991       }
  992 
  993   }
  994 
  995   # unlock database
  996   $str;
  997 
  998 
  999 }
 1000 
 1001 
 1002 
 1003 ################################################################################
 1004 # utilities
 1005 ################################################################################
 1006 
 1007 =head1 UTILITIES
 1008 
 1009 These functions are used by this class's public functions and should not be
 1010 called directly.
 1011 
 1012 =over
 1013 
 1014 =item callHelperIfExists($helperName, $dbLayoutName, @args)
 1015 
 1016 Call a database-specific helper function, if a database-layout specific helper
 1017 class exists and contains a function named "${helperName}Helper".
 1018 
 1019 =cut
 1020 
 1021 sub callHelperIfExists {
 1022   my ($helperName, $dbLayoutName, @args) = @_;
 1023 
 1024   my $helperRef = getHelperRef($helperName, $dbLayoutName);
 1025   if (ref $helperRef) {
 1026     return $helperRef->(@args);
 1027   } else {
 1028     return $helperRef;
 1029   }
 1030 }
 1031 
 1032 =over
 1033 
 1034 =item getHelperRef($helperName, $dbLayoutName)
 1035 
 1036 Call a database-specific helper function, if a database-layout specific helper
 1037 class exists and contains a function named "${helperName}Helper".
 1038 
 1039 =cut
 1040 sub getHelperRef {
 1041   my ($helperName, $dbLayoutName) = @_;
 1042 
 1043   my $result;
 1044 
 1045   my $package = __PACKAGE__ . "::$dbLayoutName";
 1046 
 1047   eval { runtime_use $package };
 1048   if ($@) {
 1049     if ($@ =~ /^Can't locate/) {
 1050       debug("No database-layout specific library for layout '$dbLayoutName'.\n");
 1051       $result = 1;
 1052     } else {
 1053       warn "Failed to load database-layout specific library: $@\n";
 1054       $result = 0;
 1055     }
 1056   } else {
 1057     my %syms = do { no strict 'refs'; %{$package."::"} };
 1058     if (exists $syms{$helperName}) {
 1059       $result = do { no strict 'refs'; \&{$package."::".$helperName} };
 1060     } else {
 1061       debug("No helper defined for operation '$helperName'.\n");
 1062       $result = 1;
 1063     }
 1064   }
 1065 
 1066   #warn "getHelperRef = '$result'\n";
 1067   return $result;
 1068 }
 1069 
 1070 =item protectQString($string)
 1071 
 1072 Protects the contents of a single-quoted Perl string.
 1073 
 1074 =cut
 1075 
 1076 sub protectQString {
 1077   my ($string) = @_;
 1078   $string =~ s/'/\'/g;
 1079   return $string;
 1080 }
 1081 
 1082 =item writeCourseConf($fh, $ce, %options)
 1083 
 1084 Writes a course.conf file to $fh, a file handle, using defaults from the course
 1085 environment object $ce and overrides from %options. %options can contain any of
 1086 the pairs accepted in %courseOptions by addCourse(), above.
 1087 
 1088 =cut
 1089 
 1090 sub writeCourseConf {
 1091   my ($fh, $ce, %options) = @_;
 1092 
 1093   # several options should be defined no matter what
 1094   $options{dbLayoutName} = $ce->{dbLayoutName} unless defined $options{dbLayoutName};
 1095 
 1096   print $fh <<'EOF';
 1097 #!perl
 1098 ################################################################################
 1099 # WeBWorK Online Homework Delivery System
 1100 # Copyright © 2000-2007 The WeBWorK Project, http://openwebwork.sf.net/
 1101 #
 1102 # This program is free software; you can redistribute it and/or modify it under
 1103 # the terms of either: (a) the GNU General Public License as published by the
 1104 # Free Software Foundation; either version 2, or (at your option) any later
 1105 # version, or (b) the "Artistic License" which comes with this package.
 1106 #
 1107 # This program is distributed in the hope that it will be useful, but WITHOUT
 1108 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 1109 # FOR A PARTICULAR PURPOSE.  See either the GNU General Public License or the
 1110 # Artistic License for more details.
 1111 ################################################################################
 1112 
 1113 # This file is used to override the global WeBWorK course environment for
 1114 # requests to this course. All package variables set in this file are added to
 1115 # the course environment. If you wish to set a variable here but omit it from
 1116 # the course environment,  use the "my" keyword. Commonly changed configuration
 1117 # options are noted below.
 1118 
 1119 EOF
 1120 
 1121   print $fh <<'EOF';
 1122 # Database Layout (global value typically defined in global.conf)
 1123 #
 1124 # Several database are defined in the file conf/database.conf and stored in the
 1125 # hash %dbLayouts.
 1126 #
 1127 # The database layout is always set here, since one should be able to change the
 1128 # default value in global.conf without disrupting existing courses.
 1129 #
 1130 # global.conf values:
 1131 EOF
 1132 
 1133   print $fh "# \t", '$dbLayoutName = \'', protectQString($ce->{dbLayoutName}), '\';', "\n";
 1134   print $fh "# \t", '*dbLayout = $dbLayouts{$dbLayoutName};', "\n";
 1135   print $fh "\n";
 1136 
 1137   if (defined $options{dbLayoutName}) {
 1138     print $fh '$dbLayoutName = \'', protectQString($options{dbLayoutName}), '\';', "\n";
 1139     print $fh '*dbLayout = $dbLayouts{$dbLayoutName};', "\n";
 1140     print $fh "\n";
 1141   } else {
 1142     print $fh "\n\n\n";
 1143   }
 1144 
 1145   print $fh <<'EOF';
 1146 # Allowed Mail Recipients (global value typically not defined)
 1147 #
 1148 # Defines addresses to which the PG system is allowed to send mail. This should
 1149 # probably be set to the addresses of professors of this course. Sending mail
 1150 # from the PG system (i.e. questionaires, essay questions) will fail if this is
 1151 # not set.
 1152 #
 1153 # global.conf values:
 1154 EOF
 1155 
 1156   if (defined $ce->{mail}->{allowedRecipients}) {
 1157     print $fh "# \t", '$mail{allowedRecipients} = [',
 1158         join(", ", map { "'" . protectQString($_) . "'" } @{ $ce->{mail}->{allowedRecipients} }), '];', "\n";
 1159   } else {
 1160     print $fh "# \t", '$mail{allowedRecipients} = [  ];', "\n";
 1161   }
 1162   print $fh "\n";
 1163 
 1164   if (defined $options{allowedRecipients}) {
 1165     print $fh '$mail{allowedRecipients} = [',
 1166         join(", ", map { "'" . protectQString($_) . "'" } @{ $options{allowedRecipients} }), '];', "\n";
 1167     print $fh "\n";
 1168   } else {
 1169     print $fh "\n\n\n";
 1170   }
 1171 
 1172   print $fh <<'EOF';
 1173 # By default, feeback is sent to all users who have permission to
 1174 # receive_feedback. If this list is non-empty, feedback is also sent to the
 1175 # addresses specified here.
 1176 #
 1177 # * If you want to disable feedback altogether, leave this empty and set
 1178 #   $permissionLevels{submit_feeback} = undef;
 1179 #   This will cause the
 1180 #   feedback button to go away as well.
 1181 #
 1182 # * If you want to send email ONLY to addresses in this list, set
 1183 #   $permissionLevels{receive_feedback} = undef;
 1184 #
 1185 # It's often useful to set this in the course.conf to change the behavior of
 1186 # feedback for a specific course.
 1187 # global.conf values:
 1188 EOF
 1189 
 1190   if (defined $ce->{mail}->{feedbackRecipients}) {
 1191     print $fh "# \t", '$mail{feedbackRecipients} = [',
 1192         join(", ", map { "'" . protectQString($_) . "'" } @{ $ce->{mail}->{feedbackRecipients} }), '];', "\n";
 1193   } else {
 1194     print $fh "# \t", '$mail{feedbackRecipients} = [  ];', "\n";
 1195   }
 1196   print $fh "\n";
 1197 
 1198   if (defined $options{feedbackRecipients}) {
 1199     print $fh '$mail{feedbackRecipients} = [',
 1200         join(", ", map { "'" . protectQString($_) . "'" } @{ $options{feedbackRecipients} }), '];', "\n";
 1201     print $fh "\n";
 1202   } else {
 1203     print $fh "\n\n\n";
 1204   }
 1205 
 1206   print $fh <<'EOF';
 1207 # Users for whom to label problems with the PG file name (global value typically "professor")
 1208 #
 1209 # For users in this list, PG will display the source file name when rendering a problem.
 1210 #
 1211 # global.conf values:
 1212 EOF
 1213 
 1214   if (defined $ce->{pg}{specialPGEnvironmentVars}{PRINT_FILE_NAMES_FOR}) {
 1215     print $fh "# \t", '$pg{specialPGEnvironmentVars}{PRINT_FILE_NAMES_FOR} = [',
 1216         join(", ", map { "'" . protectQString($_) . "'" } @{ $ce->{pg}{specialPGEnvironmentVars}{PRINT_FILE_NAMES_FOR} }), '];', "\n";
 1217   } else {
 1218     print $fh "# \t", '$pg{specialPGEnvironmentVars}{PRINT_FILE_NAMES_FOR} = [  ];', "\n";
 1219   }
 1220   print $fh "\n";
 1221 
 1222   if (defined $options{PRINT_FILE_NAMES_FOR}) {
 1223     print $fh '$pg{specialPGEnvironmentVars}{PRINT_FILE_NAMES_FOR} = [',
 1224         join(", ", map { "'" . protectQString($_) . "'" } @{ $options{PRINT_FILE_NAMES_FOR} }), '];', "\n";
 1225     print $fh "\n";
 1226   } else {
 1227     print $fh "\n\n\n";
 1228   }
 1229 }
 1230 
 1231 
 1232 sub get_SeedCE {     # helper subroutine to produce a stripped down seed Course Environment from an arbitrary course environment
 1233   my $ce = shift;
 1234   warn "get_SeedCE needs current Course environment to create seed CE" unless ref($ce) ;
 1235   my %seedCE=();
 1236   my @conf_items = qw( webwork_dir webwork_url pg_dir courseName)   ;  # items to transfer. courseName is often overridden
 1237   foreach my $item (@conf_items) {
 1238       $seedCE{$item} = $ce->{$item};
 1239   }
 1240     return( %seedCE);
 1241 }
 1242 1;

aubreyja at gmail dot com
ViewVC Help
Powered by ViewVC 1.0.9