#!/usr/bin/env perl ################################################################################ # WeBWorK Online Homework Delivery System # Copyright © 2000-2003 The WeBWorK Project, http://openwebwork.sf.net/ # $CVSHeader: webwork2/bin/ww_db_v2_to_v3,v 1.1 2004/11/25 05:50:51 sh002i 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. ################################################################################ =head1 NAME ww_db_v2_to_v3 - convert a WWDBv2 database to a WWDBv3 database. =head1 SYNOPSIS ww_db_v2_to_v3 course ... =head1 DESCRIPTION Copies course data from legacy WWDBv2 database(s) to a WWDBv3 database. This may take a long time. You must disallow login to the WeBWorK system while the transfer is taking place. To disable logins for all courses, set the permission level necessary for C to $nobody in F. (It is usually set to $student.) =head1 OPTIONS =over =item -c If an error occurs while copying a course's data, continue copying with the next course. =item -r Update role table in WWDBv3 database from permission level information in F. =item -s Update status table in WWDBv3 database from the status information in F. =item -u When importing a user that already exists in the WWDBv3 database, replace the existing information (including the password) with the information in the user record being imported. If this option is not specified, existing users are not updated. =item -v Verbose operation. =item course ... Data from these courses will be copied. =back =head1 BEHAVIOR =head2 ROLES =over =item * Roles created with the -r switch are created as system-wide roles. =item * Roles are created by observing the %permissionLevels hash in F, and collecting the privileges granted at each permission level into sets. Each set of privileges becomes a WWDBv3 role record. =item * When a role with the same set of permissions already exists in the WWDBv3 database, a new one is not created. =back =head2 STATUSES =over =item * Statuses created with the -s switch are created as system-wide statuses. =item * Statuses are created by observing the %{$siteDefaults{status}} hash in F, and =item * A status named "Enrolled" is imported into the database with the C, C, C, and C flags set. =item * A status named "Audit" is imported into the database with the C, C, and C, flags set, and the C flag unset. =item * A status named "Drop" is imported into the database with the C, C, C, and C flags unset. =item * Statuses with other names are imported into the database with the same flags set as the "Enrolled" flag. =back =head2 USERS =over =item * WWDBv2 user IDs are converted to login IDs. WWDBv2 set IDs are converted to set names. WWDBv2 problem IDs are used to determine the C in the problem's abstract set. =item * Users with the same user ID in different courses are assumed to be the same user. =back =cut use strict; use warnings; use Data::Dumper; use Getopt::Std; BEGIN { die "WEBWORK_ROOT not found in environment.\n" unless exists $ENV{WEBWORK_ROOT}; } use lib "$ENV{WEBWORK_ROOT}/lib"; use WeBWorK::CourseEnvironment; use WeBWorK::DB; use WeBWorK::DBv3; # map statuses from course environment to sets of status privileges use constant STATUS_MAP => { Enrolled => { allow_course_access => 1, include_in_assignment => 1, include_in_stats => 1, include_in_scoring => 1 }, Audit => { allow_course_access => 1, include_in_assignment => 1, include_in_stats => 1, include_in_scoring => 0 }, Drop => { allow_course_access => 0, include_in_assignment => 0, include_in_stats => 0, include_in_scoring => 0 }, }; use constant DEFAULT_STATUS => "Enrolled"; use constant DEFAULT_PERMISSION_LEVEL => "0"; our ($opt_c, $opt_r, $opt_s, $opt_u, $opt_v); getopts("crsuv"); sub debug { print STDERR @_ if $opt_v } sub usage { print STDERR "usage: $0 [-crsuv] course ...\n"; exit 1 } main(@ARGV); sub main { my (@courseIDs) = @_; usage() unless @courseIDs; my $ce = WeBWorK::CourseEnvironment->new({webwork_dir => $ENV{WEBWORK_ROOT}}); WeBWorK::DBv3::init($ce->{wwdbv3_settings}); my %abbrev_to_status_id = set_up_statuses($ce->{siteDefaults}{status}); warn "abbrev_to_status_id: ", Dumper(\%abbrev_to_status_id); my %level_to_role_id = set_up_roles($ce->{permissionLevels}); warn "level_to_role_id: ", Dumper(\%level_to_role_id); foreach my $courseID (@courseIDs) { eval { copy_course_data($courseID, \%abbrev_to_status_id, \%level_to_role_id) }; if ($@) { warn "An error occured while copying data from course '$courseID':\n\n$@\n\n"; if ($opt_c) { warn "Continuing with the next course...\n"; } else { warn "Exiting.\n"; exit 2; } } } } ################################################################################ sub set_up_roles { my ($permissionLevels) = @_; my %permissionLevels = %$permissionLevels; my %level_to_role_id; # reverse the permission levels hash, resulting in a hash mapping # permissions levels to arrayrefs containing privileges my %levels = reverse_hash(%permissionLevels); # copy up the privileges at each level to the next-higher level # also sort each level my @level_names = sort { $a <=> $b } keys %levels; foreach my $i (0 .. $#level_names-1) { my $this_level = $level_names[$i]; my $next_level = $level_names[$i+1]; push @{ $levels{$next_level} }, @{ $levels{$this_level} }; } # sort the privileges in each level debug("I found the following permission levels:\n"); foreach my $level (keys %levels) { my @sorted = sort @{ $levels{$level} }; $levels{$level} = [ @sorted ]; debug("\t$level => @sorted\n"); } # keep track of role names so we know if we need to rename any of our new ones my %role_names; # look at existing roles to see if we can avoid adding some new ones my $i = retrieve_all WeBWorK::DBv3::Role; while (my $Role = $i->next) { $role_names{$Role->name} = 1; my @role_privs = sort $Role->priv_list; foreach my $level (keys %levels) { if (listeq($levels{$level}, \@role_privs)) { debug("Permission level '$level' is already represented as role '", $Role->name, "' (ID $Role) -- skipping.\n"); delete $levels{$level}; $level_to_role_id{$level} = $Role->id; } } } if ($opt_r) { debug("Updating role table (as per -r switch).\n"); foreach my $level (keys %levels) { my $name = "Legacy permission level $level"; if (exists $role_names{$name}) { my $i = 2; while (1) { my $try_name = "$name (#$i)"; if (not exists $role_names{$try_name}) { $name = $try_name; last; } } } my @privs = @{ $levels{$level} }; my $Role = create WeBWorK::DBv3::Role({name => $name }); $Role->priv_list(@privs); $Role->update; debug("Added role '", $Role->name, "' (ID $Role) with privileges '@privs'.\n"); $level_to_role_id{$level} = $Role->id; } } else { debug("Not updating role table (as per lack of -r switch).\n"); debug("I might run into users with permission levels that don't map to roles later.\n"); } return %level_to_role_id; } sub set_up_statuses { my ($abbrevs) = @_; my %abbrevs = %$abbrevs; my %abbrev_to_status_id; # reverse the statuses hash, resulting in a hash mapping statuses to # arrayrefs containing abbreviations my %statuses = reverse_hash(%abbrevs); # look at existing statuses to see if we can avoid adding some new ones my $i = retrieve_all WeBWorK::DBv3::Status; while (my $Status = $i->next) { if (exists $statuses{$Status->name}) { debug("Status '", $Status->name, "' (ID $Status) already exists in the database -- skipping.\n"); # add entries mapping abbreviations to the ID of this status foreach my $abbrev (@{$statuses{$Status->name}}) { $abbrev_to_status_id{$abbrev} = $Status->id; } delete $statuses{$Status->name}; } } if ($opt_s) { debug("Updating status table (as per -s switch).\n"); foreach my $status (keys %statuses) { my %flags; %flags = %{ STATUS_MAP->{$status} } if exists STATUS_MAP->{$status}; my $Status = create WeBWorK::DBv3::Status({name => $status, %flags}); my @flags = grep { $flags{$_} } keys %flags; debug("Added status '", $Status->name, "' (ID $Status) with flags '@flags'.\n"); # add entries mapping abbreviations to the ID of this status foreach my $abbrev (@{$statuses{$status}}) { $abbrev_to_status_id{$abbrev} = $Status->id; } } } else { debug("Not updating status table (as per lack of -s switch).\n"); debug("I might run into users with status abbreviations that don't map to statuses later.\n"); } return %abbrev_to_status_id; } sub reverse_hash { my (%hash) = @_; my %reverse_hash; foreach my $key (keys %hash) { my $value = $hash{$key}; if (defined $value and not ref $value) { push @{ $reverse_hash{$value} }, $key; #} else { # my $val_string = defined $value ? $value : "UNDEF"; # warn "pair ( $key => $val_string ) skipped.\n"; } } return %reverse_hash; } ################################################################################ sub copy_course_data { my ($courseID, $abbrev_to_status_id, $level_to_role_id) = @_; debug("Processing course '$courseID'...\n"); my $course_ce = WeBWorK::CourseEnvironment->new({ webwork_dir => $ENV{WEBWORK_ROOT}, courseName => $courseID, }); my $course_db = WeBWorK::DB->new($course_ce->{dbLayout}); # First we see if this course already exists. If it does, there's a problem # and we throw an exception. if (WeBWorK::DBv3::Course->search(name => $courseID)) { die "Course '$courseID' exists in v3 DB"; } debug("Course '$courseID' doesn't exist in v3 DB -- adding.\n"); my $v3Course = WeBWorK::DBv3::Course->create({name => $courseID}); copy_users($course_db, $v3Course, $abbrev_to_status_id, $level_to_role_id); copy_abstract_data($course_db, $v3Course); } sub copy_users { my ($course_db, $v3Course, $abbrev_to_status_id, $level_to_role_id) = @_; my @userIDs = $course_db->listUsers; my %Users; @Users{@userIDs} = $course_db->getUsers(@userIDs); my %Passwords; @Passwords{@userIDs} = $course_db->getPasswords(@userIDs); my %PermissionLevels; @PermissionLevels{@userIDs} = $course_db->getPermissionLevels(@userIDs); foreach my $userID (keys %Users) { my $User = $Users{$userID}; my $Password = $Passwords{$userID}; my $PermissionLevel = $PermissionLevels{$userID}; unless (defined $User) { debug("User record for user ID '$userID' not found -- skipping.\n"); next; } debug("Processing user '$userID'...\n"); # create/update user record my ($v3User) = WeBWorK::DBv3::User->search(login_id => $userID); if ($v3User) { debug("A user with login_id '$userID' exists in v3 database -- "); if ($opt_u) { debug("updating (as per -u switch).\n"); $v3User->first_name($User->first_name) if $User->first_name ne ""; $v3User->last_name($User->first_name) if $User->last_name ne ""; $v3User->email_address($User->email_address) if $User->email_address ne ""; $v3User->student_id($User->student_id) if $User->student_id ne ""; $v3User->password($Password->password) if $Password->password ne ""; $v3User->update; } else { debug("not updating (as per lack of -u switch).\n"); } } else { debug("No user with login_id '$userID' exists in v3 database -- adding.\n"); $v3User = WeBWorK::DBv3::User->create({ first_name => $User->first_name, last_name => $User->last_name, email_address => $User->email_address, student_id => $User->student_id, login_id => $User->user_id, password => $Password->password, }); } # get status my $status = $User->status; if ($status eq "") { $status = DEFAULT_STATUS; debug("This user has no status -- using '$status'.\n"); } my $v3Status_id = $abbrev_to_status_id->{$status}; my $v3Status; if (defined $v3Status_id) { debug("This user has status '", $User->status, "', which maps to v3 status ID '$v3Status_id'.\n"); $v3Status = WeBWorK::DBv3::Status->retrieve($v3Status_id); } else { debug("This user has status '", $User->status, "', which doesn't map to any v3 status.\n"); } # get role my $level = $PermissionLevel->permission; if ($level eq "") { $level = DEFAULT_PERMISSION_LEVEL; debug("This user has no permission level -- using '$level'.\n"); } my $v3Role_id = $level_to_role_id->{$level}; my $v3Role; if (defined $v3Role_id) { debug("This user has permission level '", $PermissionLevel->permission, "', which maps to v3 role ID '$v3Role_id'.\n"); $v3Role = WeBWorK::DBv3::Role->retrieve($v3Role_id); } else { debug("This user has permission level '", $PermissionLevel->permission, "', which doesn't map to any v3 role.\n"); } # find/create section record my $section = $User->section; my $v3Section; if ($section ne "") { debug("This user has section '$section'.\n"); ($v3Section) = WeBWorK::DBv3::Section->search(course => $v3Course->id, name => $section); if ($v3Section) { debug("This corresponds to existing section ID $v3Section in v3 database.\n"); } else { debug("No corresponding section exists in v3 DB -- adding.\n"); $v3Section = WeBWorK::DBv3::Section->create({ course => $v3Course->id, name => $section, }); debug("Added section '", $v3Section->name, "' (ID $v3Section).\n"); } } else { debug("This user has no section.\n"); } # find/create recitation record my $recitation = $User->recitation; my $v3Recitation; if ($recitation ne "") { debug("This user has recitation '$recitation'.\n"); ($v3Recitation) = WeBWorK::DBv3::Recitation->search(course => $v3Course->id, name => $User->recitation); if ($v3Recitation) { debug("This correponds to existing recitation ID $v3Recitation in v3 database.\n"); } else { debug("No corresponding recitation exists in v3 DB -- adding.\n"); $v3Recitation = WeBWorK::DBv3::Recitation->create({ course => $v3Course->id, name => $User->recitation, }); debug("Added recitation '", $v3Recitation->name, "' (ID $v3Recitation).\n"); } } else { debug("This user has no recitation.\n"); } # create participant record debug("Adding participant record for user '", $v3User->login_id, "' (ID $v3User).\n"); my $sectionID = $v3Section->id if defined $v3Section; my $recitationID = $v3Recitation->id if defined $v3Recitation; my $v3Participant = WeBWorK::DBv3::Participant->create({ course => $v3Course->id, user => $v3User->id, status => $v3Status->id, role => $v3Role->id, section => $sectionID, recitation => $recitationID, comment => $User->comment, }); } } sub copy_abstract_data { my ($course_db, $v3Course) = @_; } ################################################################################ sub listeq { my ($a, $b) = @_; return "" unless @$a == @$b; for (my $i = 0; $i < @$a; $i++) { return "" unless $a->[$i] eq $b->[$i]; } return 1; }