Parent Directory
|
Revision Log
refactored status/role lookups, added abstract set/problem support
1 #!/usr/bin/env perl 2 ################################################################################ 3 # WeBWorK Online Homework Delivery System 4 # Copyright © 2000-2003 The WeBWorK Project, http://openwebwork.sf.net/ 5 # $CVSHeader: webwork2/bin/ww_db_v2_to_v3,v 1.1 2004/11/25 05:50:51 sh002i Exp $ 6 # 7 # This program is free software; you can redistribute it and/or modify it under 8 # the terms of either: (a) the GNU General Public License as published by the 9 # Free Software Foundation; either version 2, or (at your option) any later 10 # version, or (b) the "Artistic License" which comes with this package. 11 # 12 # This program is distributed in the hope that it will be useful, but WITHOUT 13 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 14 # FOR A PARTICULAR PURPOSE. See either the GNU General Public License or the 15 # Artistic License for more details. 16 ################################################################################ 17 18 =head1 NAME 19 20 ww_db_v2_to_v3 - convert a WWDBv2 database to a WWDBv3 database. 21 22 =head1 SYNOPSIS 23 24 ww_db_v2_to_v3 -crsuv course ... 25 26 =head1 DESCRIPTION 27 28 Copies course data from legacy WWDBv2 database(s) to a WWDBv3 database. This may 29 take a long time. 30 31 You must disallow login to the WeBWorK system while the transfer is taking 32 place. To disable logins for all courses, set the permission level necessary for 33 C<login> to $nobody in F<global.conf>. (It is usually set to $student.) 34 35 =head1 OPTIONS 36 37 =over 38 39 =item -c 40 41 If an error occurs while copying a course's data, continue copying with the next 42 course. 43 44 =item -r 45 46 Update role table in WWDBv3 database from permission level information in 47 F<global.conf>. 48 49 =item -s 50 51 Update status table in WWDBv3 database from the status information in 52 F<global.conf>. 53 54 =item -u 55 56 When importing a user that already exists in the WWDBv3 database, replace the 57 existing information (including the password) with the information in the user 58 record being imported. 59 60 If this option is not specified, existing users are not updated. 61 62 =item -v 63 64 Verbose operation. 65 66 =item course ... 67 68 Data from these courses will be copied. 69 70 =back 71 72 =head1 BEHAVIOR 73 74 =head2 ROLES 75 76 =over 77 78 =item * 79 80 Roles created with the -r switch are created as system-wide roles. 81 82 =item * 83 84 Roles are created by observing the %permissionLevels hash in F<global.conf>, and 85 collecting the privileges granted at each permission level into sets. Each set 86 of privileges becomes a WWDBv3 role record. 87 88 =item * 89 90 When a role with the same set of permissions already exists in the WWDBv3 91 database, a new one is not created. 92 93 =back 94 95 =head2 STATUSES 96 97 =over 98 99 =item * 100 101 Statuses created with the -s switch are created as system-wide statuses. 102 103 =item * 104 105 Statuses are created by observing the %{$siteDefaults{status}} hash in 106 F<global.conf>, and 107 108 =item * 109 110 A status named "Enrolled" is imported into the database with the 111 C<allow_course_access>, C<include_in_assignment>, C<include_in_stats>, and 112 C<include_in_scoring> flags set. 113 114 =item * 115 116 A status named "Audit" is imported into the database with the 117 C<allow_course_access>, C<include_in_assignment>, and C<include_in_stats>, flags 118 set, and the C<include_in_scoring> flag unset. 119 120 =item * 121 122 A status named "Drop" is imported into the database with the 123 C<allow_course_access>, C<include_in_assignment>, C<include_in_stats>, and 124 C<include_in_scoring> flags unset. 125 126 =item * 127 128 Statuses with other names are imported into the database with the same flags set 129 as the "Enrolled" flag. 130 131 =back 132 133 =head2 USERS 134 135 =over 136 137 =item * 138 139 WWDBv2 user IDs are converted to login IDs. 140 141 =item * 142 143 Users with the same v2 user ID in different courses are assumed to be the same 144 user. 145 146 =item * 147 148 A user's permission level is used to determine the role to assign to their v3 149 participant record. (See L<ROLES>.) If the user has an empty permission level, 150 they are assigned the role associated with permission level "0". 151 152 =item * 153 154 A user's status abbreviation is used to determine the status to assign to their 155 v3 participant record. (See L<STATUSES>.) If the user has an empty status, they 156 are assigned the status "Enrolled". 157 158 =item * 159 160 If a user has a non-empty section or recitation, their v3 participant record 161 will be assigned to the section or recitation with a matching name. 162 163 =back 164 165 =cut 166 167 use strict; 168 use warnings; 169 use Data::Dumper; 170 use DateTime; 171 use Getopt::Std; 172 173 BEGIN { 174 die "WEBWORK_ROOT not found in environment.\n" 175 unless exists $ENV{WEBWORK_ROOT}; 176 } 177 178 use lib "$ENV{WEBWORK_ROOT}/lib"; 179 use WeBWorK::CourseEnvironment; 180 use WeBWorK::DB; 181 use WeBWorK::DBv3; 182 183 # map statuses from course environment to sets of status privileges 184 use constant STATUS_MAP => { 185 Enrolled => { allow_course_access => 1, include_in_assignment => 1, include_in_stats => 1, include_in_scoring => 1 }, 186 Audit => { allow_course_access => 1, include_in_assignment => 1, include_in_stats => 1, include_in_scoring => 0 }, 187 Drop => { allow_course_access => 0, include_in_assignment => 0, include_in_stats => 0, include_in_scoring => 0 }, 188 }; 189 190 use constant DEFAULT_STATUS => "C"; 191 use constant DEFAULT_PERMISSION_LEVEL => "0"; 192 193 our ($opt_c, $opt_r, $opt_s, $opt_u, $opt_v); 194 getopts("crsuv"); 195 196 sub debug { print STDERR @_ if $opt_v } 197 sub usage { print STDERR "usage: $0 [-crsuv] course ...\n"; exit 1 } 198 199 main(@ARGV); 200 201 sub main { 202 my (@courseIDs) = @_; 203 204 usage() unless @courseIDs; 205 206 my $ce = WeBWorK::CourseEnvironment->new({webwork_dir => $ENV{WEBWORK_ROOT}}); 207 208 WeBWorK::DBv3::init($ce->{wwdbv3_settings}); 209 210 my %abbrev_to_status_id = set_up_statuses($ce->{siteDefaults}{status}); 211 warn "abbrev_to_status_id: ", Dumper(\%abbrev_to_status_id); 212 213 my %level_to_role_id = set_up_roles($ce->{permissionLevels}); 214 warn "level_to_role_id: ", Dumper(\%level_to_role_id); 215 216 foreach my $courseID (@courseIDs) { 217 eval { copy_course_data($courseID, \%abbrev_to_status_id, \%level_to_role_id) }; 218 if ($@) { 219 warn "An error occured while copying data from course '$courseID':\n\n$@\n\n"; 220 if ($opt_c) { 221 warn "Continuing with the next course...\n"; 222 } else { 223 warn "Exiting.\n"; 224 exit 2; 225 } 226 } 227 } 228 } 229 230 ################################################################################ 231 232 sub reverse_hash { 233 my (%hash) = @_; 234 235 my %reverse_hash; 236 foreach my $key (keys %hash) { 237 my $value = $hash{$key}; 238 if (defined $value and not ref $value) { 239 push @{ $reverse_hash{$value} }, $key; 240 #} else { 241 # my $val_string = defined $value ? $value : "UNDEF"; 242 # warn "pair ( $key => $val_string ) skipped.\n"; 243 } 244 } 245 246 return %reverse_hash; 247 } 248 249 sub listeq { 250 my ($a, $b) = @_; 251 return "" unless @$a == @$b; 252 for (my $i = 0; $i < @$a; $i++) { 253 return "" unless $a->[$i] eq $b->[$i]; 254 } 255 return 1; 256 } 257 258 sub is_empty { 259 my ($val) = @_; 260 return (not defined $val or $val eq ""); 261 } 262 263 ################################################################################ 264 265 sub set_up_roles { 266 my ($permissionLevels) = @_; 267 my %permissionLevels = %$permissionLevels; 268 269 my %level_to_role_id; 270 271 # reverse the permission levels hash, resulting in a hash mapping 272 # permissions levels to arrayrefs containing privileges 273 my %levels = reverse_hash(%permissionLevels); 274 275 # copy up the privileges at each level to the next-higher level 276 # also sort each level 277 my @level_names = sort { $a <=> $b } keys %levels; 278 foreach my $i (0 .. $#level_names-1) { 279 my $this_level = $level_names[$i]; 280 my $next_level = $level_names[$i+1]; 281 push @{ $levels{$next_level} }, @{ $levels{$this_level} }; 282 } 283 284 # sort the privileges in each level 285 debug("I found the following permission levels:\n"); 286 foreach my $level (keys %levels) { 287 my @sorted = sort @{ $levels{$level} }; 288 $levels{$level} = [ @sorted ]; 289 debug("\t$level => @sorted\n"); 290 } 291 292 # keep track of role names so we know if we need to rename any of our new ones 293 my %role_names; 294 295 # look at existing roles to see if we can avoid adding some new ones 296 my $i = retrieve_all WeBWorK::DBv3::Role; 297 while (my $Role = $i->next) { 298 $role_names{$Role->name} = 1; 299 my @role_privs = sort $Role->priv_list; 300 301 foreach my $level (keys %levels) { 302 if (listeq($levels{$level}, \@role_privs)) { 303 debug("Permission level '$level' is already represented as role '", 304 $Role->name, "' (ID $Role) -- skipping.\n"); 305 delete $levels{$level}; 306 $level_to_role_id{$level} = $Role->id; 307 } 308 } 309 } 310 311 if ($opt_r) { 312 debug("Updating role table (as per -r switch).\n"); 313 foreach my $level (keys %levels) { 314 my $name = "Legacy permission level $level"; 315 if (exists $role_names{$name}) { 316 my $i = 2; 317 while (1) { 318 my $try_name = "$name (#$i)"; 319 if (not exists $role_names{$try_name}) { 320 $name = $try_name; 321 last; 322 } 323 } 324 } 325 326 my @privs = @{ $levels{$level} }; 327 328 my $Role = create WeBWorK::DBv3::Role({name => $name }); 329 $Role->priv_list(@privs); 330 $Role->update; 331 debug("Added role '", $Role->name, "' (ID $Role) with privileges '@privs'.\n"); 332 $level_to_role_id{$level} = $Role->id; 333 } 334 } else { 335 debug("Not updating role table (as per lack of -r switch).\n"); 336 debug("I might run into users with permission levels that don't map to roles later.\n"); 337 } 338 339 return %level_to_role_id; 340 } 341 342 sub set_up_statuses { 343 my ($abbrevs) = @_; 344 my %abbrevs = %$abbrevs; 345 346 my %abbrev_to_status_id; 347 348 # reverse the statuses hash, resulting in a hash mapping statuses to 349 # arrayrefs containing abbreviations 350 my %statuses = reverse_hash(%abbrevs); 351 352 # look at existing statuses to see if we can avoid adding some new ones 353 my $i = retrieve_all WeBWorK::DBv3::Status; 354 while (my $Status = $i->next) { 355 if (exists $statuses{$Status->name}) { 356 debug("Status '", $Status->name, "' (ID $Status) already exists in the database -- skipping.\n"); 357 # add entries mapping abbreviations to the ID of this status 358 foreach my $abbrev (@{$statuses{$Status->name}}) { 359 $abbrev_to_status_id{$abbrev} = $Status->id; 360 } 361 362 delete $statuses{$Status->name}; 363 } 364 } 365 366 if ($opt_s) { 367 debug("Updating status table (as per -s switch).\n"); 368 foreach my $status (keys %statuses) { 369 my %flags; 370 %flags = %{ STATUS_MAP->{$status} } if exists STATUS_MAP->{$status}; 371 my $Status = create WeBWorK::DBv3::Status({name => $status, %flags}); 372 373 my @flags = grep { $flags{$_} } keys %flags; 374 debug("Added status '", $Status->name, "' (ID $Status) with flags '@flags'.\n"); 375 376 # add entries mapping abbreviations to the ID of this status 377 foreach my $abbrev (@{$statuses{$status}}) { 378 $abbrev_to_status_id{$abbrev} = $Status->id; 379 } 380 } 381 } else { 382 debug("Not updating status table (as per lack of -s switch).\n"); 383 debug("I might run into users with status abbreviations that don't map to statuses later.\n"); 384 } 385 386 return %abbrev_to_status_id; 387 } 388 389 ################################################################################ 390 391 sub copy_course_data { 392 my ($courseID, $abbrev_to_status_id, $level_to_role_id) = @_; 393 394 debug("Processing course '$courseID'...\n"); 395 396 my $course_ce = WeBWorK::CourseEnvironment->new({ 397 webwork_dir => $ENV{WEBWORK_ROOT}, 398 courseName => $courseID, 399 }); 400 401 my $course_db = WeBWorK::DB->new($course_ce->{dbLayout}); 402 403 # First we see if this course already exists. If it does, there's a problem 404 # and we throw an exception. 405 if (search WeBWorK::DBv3::Course(name => $courseID)) { 406 die "Course '$courseID' exists in v3 DB"; 407 } 408 409 debug("Course '$courseID' doesn't exist in v3 DB -- adding.\n"); 410 my $v3Course = create WeBWorK::DBv3::Course({name => $courseID}); 411 412 copy_users($course_db, $v3Course, $abbrev_to_status_id, $level_to_role_id); 413 414 copy_abstract_data($course_db, $v3Course); 415 } 416 417 ################################################################################ 418 419 sub copy_users { 420 my ($course_db, $v3Course, $abbrev_to_status_id, $level_to_role_id) = @_; 421 422 my $DefaultStatus = find_status(DEFAULT_STATUS, $abbrev_to_status_id); 423 die "Default status '", DEFAULT_STATUS, "' does not correspond to any v3 status.\n" 424 unless $DefaultStatus; 425 426 my $DefaultRole = find_role(DEFAULT_PERMISSION_LEVEL, $level_to_role_id); 427 die "Default permission level '", DEFAULT_PERMISSION_LEVEL, "' does not correspond to any v3 role.\n" 428 unless $DefaultRole; 429 430 my @userIDs = $course_db->listUsers; 431 my %Users; @Users{@userIDs} = $course_db->getUsers(@userIDs); 432 my %Passwords; @Passwords{@userIDs} = $course_db->getPasswords(@userIDs); 433 my %PermissionLevels; @PermissionLevels{@userIDs} = $course_db->getPermissionLevels(@userIDs); 434 435 foreach my $userID (keys %Users) { 436 my $User = $Users{$userID}; 437 my $Password = $Passwords{$userID}; 438 my $PermissionLevel = $PermissionLevels{$userID}; 439 440 unless (defined $User) { 441 debug("User record for user ID '$userID' not found -- skipping.\n"); 442 next; 443 } 444 445 debug("Processing user '$userID'...\n"); 446 447 # create/update user record 448 my ($v3User) = search WeBWorK::DBv3::User(login_id => $userID); 449 if ($v3User) { 450 debug("A user with login_id '$userID' exists in v3 database -- "); 451 if ($opt_u) { 452 # password record might not exist (annoying...) 453 my $password = defined $Password ? $Password->password : ""; 454 455 debug("updating (as per -u switch).\n"); 456 $v3User->first_name($User->first_name) unless is_empty($User->first_name); 457 $v3User->last_name($User->first_name) unless is_empty($User->last_name); 458 $v3User->email_address($User->email_address) unless is_empty($User->email_address); 459 $v3User->student_id($User->student_id) unless is_empty($User->student_id); 460 $v3User->password($password) unless is_empty($password); 461 $v3User->update; 462 } else { 463 debug("not updating (as per lack of -u switch).\n"); 464 } 465 } else { 466 # password record might not exist (annoying...) 467 my $password = defined $Password ? $Password->password : ""; 468 469 debug("No user with login_id '$userID' exists in v3 database -- adding.\n"); 470 $v3User = create WeBWorK::DBv3::User({ 471 first_name => $User->first_name, 472 last_name => $User->last_name, 473 email_address => $User->email_address, 474 student_id => $User->student_id, 475 login_id => $User->user_id, 476 password => $password, 477 }); 478 } 479 480 # get status 481 my $v3Status = find_status($User->status, $abbrev_to_status_id); 482 unless ($v3Status) { 483 debug("Using default status '", $DefaultStatus->name, "'.\n"); 484 $v3Status = $DefaultStatus; 485 } 486 487 # get role 488 my $level = defined $PermissionLevel ? $PermissionLevel->permission : ""; 489 my $v3Role = find_role($level, $level_to_role_id); 490 unless ($v3Role) { 491 debug("Using default role '", $DefaultRole->name, "'.\n"); 492 $v3Role = $DefaultRole; 493 } 494 495 # find/create section record 496 my $section = $User->section; 497 my $v3Section; 498 if (is_empty($section)) { 499 debug("This user has section '$section'.\n"); 500 ($v3Section) = search WeBWorK::DBv3::Section(course => $v3Course, name => $section); 501 if ($v3Section) { 502 debug("This corresponds to existing section ID $v3Section in v3 database.\n"); 503 } else { 504 debug("No corresponding section exists in v3 DB -- adding.\n"); 505 $v3Section = create WeBWorK::DBv3::Section({ 506 course => $v3Course, 507 name => $section, 508 }); 509 debug("Added section '", $v3Section->name, "' (ID $v3Section).\n"); 510 } 511 } else { 512 debug("This user has no section.\n"); 513 } 514 515 # find/create recitation record 516 my $recitation = $User->recitation; 517 my $v3Recitation; 518 if (is_empty($recitation)) { 519 debug("This user has recitation '$recitation'.\n"); 520 ($v3Recitation) = search WeBWorK::DBv3::Recitation(course => $v3Course, name => $User->recitation); 521 if ($v3Recitation) { 522 debug("This correponds to existing recitation ID $v3Recitation in v3 database.\n"); 523 } else { 524 debug("No corresponding recitation exists in v3 DB -- adding.\n"); 525 $v3Recitation = create WeBWorK::DBv3::Recitation({ 526 course => $v3Course, 527 name => $User->recitation, 528 }); 529 debug("Added recitation '", $v3Recitation->name, "' (ID $v3Recitation).\n"); 530 } 531 } else { 532 debug("This user has no recitation.\n"); 533 } 534 535 # create participant record 536 debug("Adding participant record for user '$userID'..."); 537 #my $sectionID = $v3Section->id if defined $v3Section; 538 #my $recitationID = $v3Recitation->id if defined $v3Recitation; 539 my $v3Participant = create WeBWorK::DBv3::Participant({ 540 course => $v3Course, 541 user => $v3User, 542 status => $v3Status, 543 role => $v3Role, 544 section => $v3Section, 545 recitation => $v3Recitation, 546 comment => $User->comment, 547 }); 548 debug(" added participant ID $v3Participant.\n"); 549 } 550 } 551 552 sub find_status { 553 my ($status, $abbrev_to_status_id) = @_; 554 555 return if is_empty($status); 556 557 my $v3Status_id = $abbrev_to_status_id->{$status}; 558 my $v3Status; 559 if (defined $v3Status_id) { 560 #debug("Status '$status' maps to v3 status ID '$v3Status_id'.\n"); 561 $v3Status = retrieve WeBWorK::DBv3::Status($v3Status_id); 562 } else { 563 #debug("Status '$status' doesn't map to any v3 status.\n"); 564 } 565 566 return $v3Status; 567 } 568 569 sub find_role { 570 my ($level, $level_to_role_id) = @_; 571 572 return if is_empty($level); 573 574 my $v3Role_id = $level_to_role_id->{$level}; 575 my $v3Role; 576 if (defined $v3Role_id) { 577 #debug("Permission level '$level' maps to v3 role ID '$v3Role_id'.\n"); 578 $v3Role = retrieve WeBWorK::DBv3::Role($v3Role_id); 579 } else { 580 #debug("Permission level '$level' doesn't map to any v3 role.\n"); 581 } 582 583 return $v3Role; 584 } 585 586 ################################################################################ 587 588 sub copy_abstract_data { 589 my ($course_db, $v3Course) = @_; 590 591 my @globalSetIDs = $course_db->listGlobalSets; 592 my %GlobalSets; @GlobalSets{@globalSetIDs} = $course_db->getGlobalSets(@globalSetIDs); 593 594 foreach my $globalSetID (keys %GlobalSets) { 595 my $GlobalSet = $GlobalSets{$globalSetID}; 596 597 unless (defined $GlobalSet) { 598 debug("Global set record for global set ID '$globalSetID' not found -- skipping.\n"); 599 next; 600 } 601 602 debug("Processing global set '$globalSetID'...\n"); 603 604 # set up some fields that need setting up 605 # (if the conditional is false, the variable is left undefined) 606 607 # convert empty strings to undefined values 608 my $set_header = $GlobalSet->set_header unless is_empty($GlobalSet->set_header); 609 my $hardcopy_header = $GlobalSet->hardcopy_header unless is_empty($GlobalSet->hardcopy_header); 610 611 # convert 612 my $open_date = DateTime->from_epoch(epoch => $GlobalSet->open_date); 613 my $due_date = DateTime->from_epoch(epoch => $GlobalSet->due_date); 614 my $answer_date = DateTime->from_epoch(epoch => $GlobalSet->answer_date); 615 616 # create abstract_set record 617 debug("Adding abstract_set record for global set '$globalSetID'..."); 618 my $v3AbsSet = create WeBWorK::DBv3::AbstractSet({ 619 course => $v3Course, 620 name => $GlobalSet->set_id, 621 set_header => $set_header, 622 hardcopy_header => $hardcopy_header, 623 open_date => $open_date, 624 due_date => $due_date, 625 answer_date => $answer_date, 626 published => $GlobalSet->published, 627 }); 628 debug(" added abstract_set ID '$v3AbsSet'.\n"); 629 630 my @globalProblemIDs = sort { $a <=> $b } $course_db->listGlobalProblems($globalSetID); 631 warn "globalProblemIDs=@globalProblemIDs\n"; 632 my %GlobalProblems; @GlobalProblems{@globalProblemIDs} 633 = $course_db->getGlobalProblems(map { [ $globalSetID, $_ ] } @globalProblemIDs); 634 635 my @problem_order; 636 637 foreach my $globalProblemID (@globalProblemIDs) { 638 my $GlobalProblem = $GlobalProblems{$globalProblemID}; 639 640 unless (defined $GlobalProblem) { 641 warn "Global problem record for global problem ID '$globalProblemID' in set ID '$globalSetID' not found -- skipping.\n"; 642 next; 643 } 644 645 debug("Processing global problem '$globalProblemID'...\n"); 646 647 # convert max_attempts of -1 to undef 648 my $max_attempts_per_version = $GlobalProblem->max_attempts 649 if $GlobalProblem->max_attempts >= 0; 650 651 # create abstract_problem record 652 debug("Adding abstract_set record for global problem '$globalProblemID'..."); 653 my $v3AbsProb = create WeBWorK::DBv3::AbstractProblem({ 654 abstract_set => $v3AbsSet, 655 name => "Legacy problem $globalProblemID", 656 source_type => "file", 657 source_file => $GlobalProblem->source_file, 658 weight => $GlobalProblem->value, 659 max_attempts_per_version => $max_attempts_per_version, 660 version_creation_interval => undef, 661 versions_per_interval => 1, 662 version_due_date_offset => undef, 663 version_answer_date_offset => undef, 664 }); 665 debug(" added abstract_problem ID '$v3AbsProb'.\n"); 666 667 push @problem_order, $v3AbsProb->id; 668 } 669 670 # update problem order 671 debug("Setting problem order to: '@problem_order'..."); 672 $v3AbsSet->problem_order_list(@problem_order); 673 $v3AbsSet->update; 674 debug(" done.\n"); 675 } 676 } 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691
| aubreyja at gmail dot com | ViewVC Help |
| Powered by ViewVC 1.0.9 |