[system] / branches / rel-2-1-a1 / webwork-modperl / lib / WeBWorK / DB.pm Repository:
ViewVC logotype

View of /branches/rel-2-1-a1/webwork-modperl/lib/WeBWorK/DB.pm

Parent Directory Parent Directory | Revision Log Revision Log


Revision 2536 - (download) (as text) (annotate)
Sun Jul 18 13:35:24 2004 UTC (8 years, 10 months ago) by gage
File size: 73565 byte(s)
CAUTION.  Major update!!!
Modifications made up until the release of 2.0 on July 16, 2004
on the 2.0 branch have been incorporated into version 2.1 alpha 1.
A moderate amount of testing has been done.  It will take
some time to reconfigure your global.conf file once you update to
this version.

    1 ################################################################################
    2 # WeBWorK Online Homework Delivery System
    3 # Copyright © 2000-2003 The WeBWorK Project, http://openwebwork.sf.net/
    4 # $CVSHeader: webwork2/lib/WeBWorK/DB.pm,v 1.52 2004/06/17 20:11:17 sh002i 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::DB;
   18 
   19 =head1 NAME
   20 
   21 WeBWorK::DB - interface with the WeBWorK databases.
   22 
   23 =head1 SYNOPSIS
   24 
   25  my $db = WeBWorK::DB->new($dbLayout);
   26 
   27  my @userIDs = $db->listUsers();
   28  my $Sam = $db->{user}->{record}->new();
   29 
   30  $Sam->user_id("sammy");
   31  $Sam->first_name("Sam");
   32  $Sam->last_name("Hathaway");
   33  # etc.
   34 
   35  $db->addUser($User);
   36  my $Dennis = $db->getUser("dennis");
   37  $Dennis->status("C");
   38  $db->putUser->($Dennis);
   39 
   40  $db->deleteUser("sammy");
   41 
   42 =head1 DESCRIPTION
   43 
   44 WeBWorK::DB provides a consistent interface to a number of database backends.
   45 Access and modification functions are provided for each logical table used by
   46 the webwork system. The particular backend ("schema" and "driver"), record
   47 class, data source, and additional parameters are specified by the hash
   48 referenced by C<$dbLayout>, usually taken from the course environment.
   49 
   50 =head1 ARCHITECTURE
   51 
   52 The new database system uses a three-tier architecture to insulate each layer
   53 from the adjacent layers.
   54 
   55 =head2 Top Layer: DB
   56 
   57 The top layer of the architecture is the DB module. It provides the methods
   58 listed below, and uses schema modules (via tables) to implement those methods.
   59 
   60          / new* list* exists* add* get* get*s put* delete* \          <- api
   61  +------------------------------------------------------------------+
   62  |                                DB                                |
   63  +------------------------------------------------------------------+
   64   \ password permission key user set set_user problem problem_user /  <- tables
   65 
   66 =head2 Middle Layer: Schemas
   67 
   68 The middle layer of the architecture is provided by one or more schema modules.
   69 They are called "schema" modules because they control the structure of the data
   70 for a table. This includes odd things like the way multiple tables are encoded
   71 in a single hash in the WW1Hash schema, and the encoding scheme used.
   72 
   73 The schema modules provide an API that matches the requirements of the DB
   74 layer, on a per-table basis. Each schema module has a style that determines
   75 which drivers it can interface with. For example, WW1Hash is a "hash" style
   76 schema. SQL is a "dbi" style schema.
   77 
   78 =head3 Examples
   79 
   80 Both WeBWorK 1.x and 2.x courses use:
   81 
   82   / password  permission  key \        / user \      <- tables provided
   83  +-----------------------------+  +----------------+
   84  |          Auth1Hash          |  | Classlist1Hash |
   85  +-----------------------------+  +----------------+
   86              \ hash /                  \ hash /      <- driver style required
   87 
   88 WeBWorK 1.x courses also use:
   89 
   90   / set_user problem_user \       / set problem \
   91  +-------------------------+  +---------------------+
   92  |         WW1Hash         |  | GlobalTableEmulator |
   93  +-------------------------+  +---------------------+
   94            \ hash /                   \ null /
   95 
   96 The GlobalTableEmulator schema emulates the global set and problem tables using
   97 data from the set_user and problem_user tables.
   98 
   99 WeBWorK 2.x courses also use:
  100 
  101   / set set_user problem problem_user \
  102  +-------------------------------------+
  103  |               WW2Hash               |
  104  +-------------------------------------+
  105                  \ hash /
  106 
  107 =head2 Bottom Layer: Drivers
  108 
  109 Driver modules implement a style for a schema. They provide physical access to
  110 a data source containing the data for a table. The style of a driver determines
  111 what methods it provides. All drivers provide C<connect(MODE)> and
  112 C<disconnect()> methods. A hash style driver provides a C<hash()> method which
  113 returns the tied hash. A dbi style driver provides a C<handle()> method which
  114 returns the DBI handle.
  115 
  116 =head3 Examples
  117 
  118   / hash \    / hash \    / hash \  <- style
  119  +--------+  +--------+  +--------+
  120  |   DB   |  |  GDBM  |  |   DB3  |
  121  +--------+  +--------+  +--------+
  122 
  123   / dbi \    / ldap \
  124  +-------+  +--------+
  125  |  SQL  |  |  LDAP  |
  126  +-------+  +--------+
  127 
  128 =head2 Record Types
  129 
  130 In C<%dblayout>, each table is assigned a record class, used for passing
  131 complete records to and from the database. The default record classes are
  132 subclasses of the WeBWorK::DB::Record class, and are named as follows: User,
  133 Password, PermissionLevel, Key, Set, UserSet, Problem, UserProblem. In the
  134 following documentation, a reference the the record class for a table means the
  135 record class currently defined for that table in C<%dbLayout>.
  136 
  137 =cut
  138 
  139 use strict;
  140 use warnings;
  141 use Carp;
  142 use Data::Dumper;
  143 use WeBWorK::Timing;
  144 use WeBWorK::Utils qw(runtime_use);
  145 
  146 ################################################################################
  147 # constructor
  148 ################################################################################
  149 
  150 =head1 CONSTRUCTOR
  151 
  152 =over
  153 
  154 =item new($ce)
  155 
  156 The C<new> method creates a DB object and brings up the underlying
  157 schema/driver structure according to the C<%dbLayout> hash in C<$ce>, a
  158 WeBWorK::CourseEnvironment object.
  159 
  160 =back
  161 
  162 =head2 C<$dbLayout> Format
  163 
  164 C<$dbLayout> is a hash reference consisting of items keyed by table names. The
  165 value of each item is a reference to a hash containing the following items:
  166 
  167 =over
  168 
  169 =item record
  170 
  171 The name of a perl module to use for representing the data in a record.
  172 
  173 =item schema
  174 
  175 The name of a perl module to use for access to the table.
  176 
  177 =item driver
  178 
  179 The name of a perl module to use for access to the data source.
  180 
  181 =item source
  182 
  183 The location of the data source that should be used by the driver module.
  184 Depending on the driver, this may be a path, a url, or a DBI spec.
  185 
  186 =item params
  187 
  188 A reference to a hash containing extra information needed by the schema. Some
  189 schemas require parameters, some do not. Consult the documentation for the
  190 schema in question.
  191 
  192 =back
  193 
  194 For each table defined in C<$dbLayout>, C<new> loads the record, schema, and
  195 driver modules. It the schema module's C<tables> method lists the current table
  196 (or contains the string "*") and the output of the schema and driver modules'
  197 C<style> methods match, the table is installed. Otherwise, an exception is
  198 thrown.
  199 
  200 =cut
  201 
  202 sub new($$) {
  203   my ($invocant, $dbLayout) = @_;
  204   my $class = ref($invocant) || $invocant;
  205   my $self = {};
  206   bless $self, $class; # bless this here so we can pass it to the schema
  207 
  208   # load the modules required to handle each table, and create driver
  209   my %dbLayout = %$dbLayout;
  210   foreach my $table (keys %dbLayout) {
  211     my $layout = $dbLayout{$table};
  212     my $record = $layout->{record};
  213     my $schema = $layout->{schema};
  214     my $driver = $layout->{driver};
  215     my $source = $layout->{source};
  216     my $params = $layout->{params};
  217 
  218     runtime_use($record);
  219 
  220     runtime_use($driver);
  221     my $driverObject = eval { $driver->new($source, $params) };
  222     croak "error instantiating DB driver $driver for table $table: $@"
  223       if $@;
  224 
  225     runtime_use($schema);
  226     my $schemaObject = eval { $schema->new(
  227       $self, $driverObject, $table, $record, $params) };
  228     croak "error instantiating DB schema $schema for table $table: $@"
  229       if $@;
  230 
  231     $self->{$table} = $schemaObject;
  232   }
  233 
  234   return $self;
  235 }
  236 
  237 =head1 METHODS
  238 
  239 =cut
  240 
  241 ################################################################################
  242 # general functions
  243 ################################################################################
  244 
  245 =head2 General Methods
  246 
  247 =over
  248 
  249 =cut
  250 
  251 =item hashDatabaseOK($fix)
  252 
  253 If the schema module in use for the C<set> and C<problem> tables is
  254 WeBWorK::DB::Schema::GlobalTableEmulator, the database is checked to make sure
  255 that the "global user" exists and all sets and problems are assigned to it. If
  256 $fix is true, problems found will be fixed: A global user will be created and
  257 all sets/problems assigned to it.
  258 
  259 A list of values is returned. The first value is a boolean value indicating
  260 whether problems remain in the database after hashDatabaseOK() is called. The
  261 remaining values are a list of strings indicating the particular ways in which
  262 the database is (or was) broken.
  263 
  264 =cut
  265 
  266 sub hashDatabaseOK {
  267   my ($self, $fix) = @_;
  268 
  269   my $errorsExist;
  270   my @results;
  271 
  272   ##### do we need to run? #####
  273 
  274   unless (ref $self->{set} eq "WeBWorK::DB::Schema::GlobalTableEmulator") {
  275     #warn "hashDatabaseOK($fix): no checks necessary, set table does not use GlobalTableEmulator.\n";
  276     return 1;
  277   }
  278 
  279   ##### is globalUserID defined? #####
  280 
  281   my $globalUserID = $self->{set}->{params}->{globalUserID};
  282   if ($globalUserID eq "") {
  283     return 0, "globalUserID not specified (fix this in %dbLayout.)";
  284   } else {
  285     #warn "hashDatabaseOK($fix): globalUserID not empty ($globalUserID) -- good.\n";
  286   }
  287 
  288   ##### does a user with ID globalUserID exist? #####
  289 
  290   my $GlobalUser = $self->getUser($globalUserID);
  291   if (defined $GlobalUser) {
  292     #warn "hashDatabaseOK($fix): user with ID '$globalUserID' exists -- good.\n";
  293   } else {
  294     #warn "hashDatabaseOK($fix): user with ID '$globalUserID' not found -- bad!\n";
  295     if ($fix) {
  296       $self->addUser($self->newUser(
  297         user_id => $globalUserID,
  298         first_name => "Global",
  299         last_name => "User",
  300         email_address => "",
  301         student_id => $globalUserID,
  302         status => "D",
  303         section => "",
  304         recitation => "",
  305         comment => "This user is used to store data about global set and problem records when using a hash-style database.",
  306       ));
  307       push @results, "User $globalUserID does not exist -- FIXED.";
  308       #warn "hashDatabaseOK($fix): created user with ID '$globalUserID' -- good.\n";
  309     } else {
  310       # at this point, we don't go on. no global user means everything below is going to fail.
  311       return 0, "User $globalUserID does not exist.";
  312     }
  313   }
  314 
  315   ##### are all sets assigned to the user with ID globalUserID? #####
  316 
  317   # FIXME: this is way too slow!
  318   #my @userSetIDs = $self->{set_user}->list(undef, undef);
  319 
  320   # Timing Data
  321   #
  322   # old method:
  323   # TIMING 36119 1 1087502726.923311 (0.139117) mth143: WeBWorK::DB::hashDatabaseOK: about to get orphaned UserSets
  324   # TIMING 36119 1 1087502768.074221 (41.290027) mth143: WeBWorK::DB::hashDatabaseOK: done getting orphaned UserSets
  325   #
  326   # new method:
  327   # TIMING 36134 0 1087502854.579133 (0.141437) mth143: WeBWorK::DB::hashDatabaseOK: about to get orphaned UserSets
  328   # TIMING 36134 0 1087502856.852504 (2.414808) mth143: WeBWorK::DB::hashDatabaseOK: done getting orphaned UserSets
  329   #
  330   # yay!
  331 
  332   $WeBWorK::timer->continue(__PACKAGE__ . "::hashDatabaseOK: about to get orphaned UserSets") if defined $WeBWorK::timer;
  333 
  334   # ... so instead, we're going to do things manually
  335 
  336   # key: setID, value: hash of userIDs of users to whom this set is assigned
  337   my %orphanUserSets;
  338 
  339   if (ref $self->{set_user} eq "WeBWorK::DB::Schema::WW1Hash") {
  340     # we can only do this with WW1Hash
  341     #warn "the fast way!\n";
  342 
  343     # connect
  344     $self->{set_user}->{driver}->connect("ro")
  345       or return 0, @results, "Failed to connect to set_user database.";
  346 
  347     # get PSVNs for global user (ČN)
  348     my @globalUserPSVNs = $self->{set_user}->getPSVNsForUser($globalUserID);
  349     #warn "found ", scalar @globalUserPSVNs, " PSVNs for the global user.\n";
  350 
  351     # get setIDs for PSVNs (M)
  352     my @globalUserSetIDs;
  353     foreach my $PSVN (@globalUserPSVNs) {
  354       #warn "getting setID for PSVN '$PSVN'...\n";
  355       my $string = $self->{set_user}->fetchString($PSVN);
  356       my (undef, $setID) = $self->{set_user}->string2IDs($string); # discard userID, problemIDs
  357       push @globalUserSetIDs, $setID;
  358       #warn "got setID '$setID'\n";
  359     }
  360 
  361     # get PSVNs for each setID (ČN*M)
  362     my @okPSVNs = map { $self->{set_user}->getPSVNsForSet($_) } @globalUserSetIDs;
  363     #warn "found ", scalar @okPSVNs, " PSVNs for sets assigned to the global user.\n";
  364 
  365     # get all PSVNs (N*M)
  366     my @allPSVNs = $self->{set_user}->getAllPSVNs;
  367     #warn "found ", scalar @allPSVNs, " PSVNs total.\n";
  368 
  369     # eliminate PSVNs of sets that are assigned to the global user
  370     my %allPSVNs;
  371     @allPSVNs{@allPSVNs} = ();
  372 
  373     foreach my $PSVN (@okPSVNs) {
  374       delete $allPSVNs{$PSVN};
  375     }
  376 
  377     # get setIDs for orphan PSVNs
  378     foreach my $PSVN (keys %allPSVNs) {
  379       #warn "getting userID and setID for PSVN '$PSVN'...\n";
  380       my $string = $self->{set_user}->fetchString($PSVN);
  381       my ($userID, $setID) = $self->{set_user}->string2IDs($string);
  382       $orphanUserSets{$setID}->{$userID} = 1;
  383       #warn "got setID '$setID' for userID '$userID'\n";
  384     }
  385 
  386     # disconnect
  387     $self->{set_user}->{driver}->disconnect;
  388   } else {
  389     # otherwise, do it the slow way (maybe it's not slow with some other schema?)
  390     #warn "oddly enough, set_user isn't using WW1Hash, so we have to use the slow list() method";
  391     my @userSetIDs = $self->{set_user}->list(undef, undef);
  392 
  393     foreach my $userSetID (@userSetIDs) {
  394       my ($userID, $setID) = @$userSetID;
  395       $orphanUserSets{$setID}->{$userID} = 1;
  396     }
  397 
  398     foreach my $setID (keys %orphanUserSets) {
  399       delete $orphanUserSets{$setID}
  400         if exists $orphanUserSets{$setID}->{$globalUserID};
  401     }
  402   }
  403 
  404   $WeBWorK::timer->continue(__PACKAGE__ . "::hashDatabaseOK: done getting orphaned UserSets") if defined $WeBWorK::timer;
  405 
  406   if (keys %orphanUserSets) {
  407     if ($fix) {
  408       foreach my $setID (keys %orphanUserSets) {
  409         my $userID = ( keys %{$orphanUserSets{$setID}} )[0];
  410 
  411         # grab the first UserSet of this set (connect and disconnect required for get1*)
  412         $self->{set_user}->{driver}->connect("ro")
  413           or return 0, @results, "Failed to connect to set_user database.";
  414         my $RawUserSet = $self->{set_user}->get1NoFilter($userID, $setID);
  415         $self->{set_user}->{driver}->disconnect();
  416         unless ($RawUserSet) {
  417           #warn "failed to fetch UserSet '$setID' for user '$userID'!\n";
  418           next;
  419         }
  420 
  421         # change user ID to globalUserID and add to database
  422         $RawUserSet->user_id($globalUserID);
  423         $self->{set_user}->add($RawUserSet);
  424 
  425         push @results, "Set '$setID' not assigned to global user '$globalUserID' -- FIXED.";
  426 
  427         #warn "hashDatabaseOK($fix): assigned set '$setID' to global user '$globalUserID' -- good.\n";
  428       }
  429     } else {
  430       foreach my $setID (keys %orphanUserSets) {
  431         #warn "hashDatabaseOK($fix): set '$setID' not assigned to global user '$globalUserID' -- bad!\n";
  432         push @results, "Set '$setID' not assigned to global user '$globalUserID'.";
  433       }
  434       $errorsExist = 1;
  435     }
  436   } else {
  437     #warn "hashDatabaseOK($fix): all sets assigned to global user '$globalUserID' -- good.\n";
  438   }
  439 
  440   ##### done! #####
  441 
  442   my $status = not $errorsExist;
  443   return $status, @results;
  444 }
  445 
  446 =back
  447 
  448 =cut
  449 
  450 ################################################################################
  451 # password functions
  452 ################################################################################
  453 
  454 =head2 Password Methods
  455 
  456 =over
  457 
  458 =item newPassword()
  459 
  460 Returns a new, empty password object.
  461 
  462 =cut
  463 
  464 sub newPassword {
  465   my ($self, @prototype) = @_;
  466   return $self->{password}->{record}->new(@prototype);
  467 }
  468 
  469 =item listPasswords()
  470 
  471 Returns a list of user IDs representing the records in the password table.
  472 
  473 =cut
  474 
  475 sub listPasswords {
  476   my ($self) = @_;
  477 
  478   croak "listPasswords: requires 0 arguments"
  479     unless @_ == 1;
  480 
  481   return map { $_->[0] }
  482     $self->{password}->list(undef);
  483 }
  484 
  485 =item addPassword($Password)
  486 
  487 $Password is a record object. The password will be added to the password table
  488 if a password with the same user ID does not already exist. If one does exist,
  489 an exception is thrown. To add a password, a user with a matching user ID must
  490 exist in the user table.
  491 
  492 =cut
  493 
  494 sub addPassword {
  495   my ($self, $Password) = @_;
  496 
  497   croak "addPassword: requires 1 argument"
  498     unless @_ == 2;
  499   croak "addPassword: argument 1 must be of type ", $self->{password}->{record}
  500     unless ref $Password eq $self->{password}->{record};
  501 
  502   checkKeyfields($Password);
  503 
  504   croak "addPassword: password exists (perhaps you meant to use putPassword?)"
  505     if $self->{password}->exists($Password->user_id);
  506   croak "addPassword: user ", $Password->user_id, " not found"
  507     unless $self->{user}->exists($Password->user_id);
  508 
  509   return $self->{password}->add($Password);
  510 }
  511 
  512 =item getPassword($userID)
  513 
  514 If a record with a matching user ID exists, a record object containting that
  515 record's data will be returned. If no such record exists, one will be created.
  516 
  517 =cut
  518 
  519 sub getPassword {
  520   my ($self, $userID) = @_;
  521 
  522   croak "getPassword: requires 1 argument"
  523     unless @_ == 2;
  524   croak "getPassword: argument 1 must contain a user_id"
  525     unless defined $userID;
  526 
  527   #return $self->{password}->get($userID);
  528   return ( $self->getPasswords($userID) )[0];
  529 }
  530 
  531 =item getPasswords(@uesrIDs)
  532 
  533 Return a list of password records associated with the user IDs given. If there
  534 is no record associated with a given user ID, one will be created.
  535 
  536 =cut
  537 
  538 sub getPasswords {
  539   my ($self, @userIDs) = @_;
  540 
  541   #croak "getPasswords: requires 1 or more argument"
  542   # unless @_ >= 2;
  543   foreach my $i (0 .. $#userIDs) {
  544     croak "getPasswords: element $i of argument list must contain a user_id"
  545       unless defined $userIDs[$i];
  546   }
  547 
  548   my @Passwords = $self->{password}->gets(map { [$_] } @userIDs);
  549 
  550   for (my $i = 0; $i < @Passwords; $i++) {
  551     my $Password = $Passwords[$i];
  552     my $userID = $userIDs[$i];
  553     if (not defined $Password) {
  554       #warn "not defined\n";
  555       if ($self->{user}->exists($userID)) {
  556         #warn "user exists\n";
  557         $Password = $self->newPassword(user_id => $userID);
  558         eval { $self->addPassword($Password) };
  559         if ($@ and $@ !~ m/password exists/) {
  560           die "error while auto-creating password record for user $userID: \"$@\"";
  561         }
  562       }
  563     }
  564   }
  565 
  566   return @Passwords;
  567 }
  568 
  569 =item putPassword($Password)
  570 
  571 $Password is a record object. If a password record with the same user ID exists
  572 in the password table, the data in the record is replaced with the data in
  573 $Password. If a matching password record does not exist, an exception is
  574 thrown.
  575 
  576 =cut
  577 
  578 sub putPassword($$) {
  579   my ($self, $Password) = @_;
  580 
  581   croak "putPassword: requires 1 argument"
  582     unless @_ == 2;
  583   croak "putPassword: argument 1 must be of type ", $self->{password}->{record}
  584     unless ref $Password eq $self->{password}->{record};
  585 
  586   checkKeyfields($Password);
  587 
  588   croak "putPassword: password not found (perhaps you meant to use addPassword?)"
  589     unless $self->{password}->exists($Password->user_id);
  590 
  591   return $self->{password}->put($Password);
  592 }
  593 
  594 =item deletePassword($userID)
  595 
  596 If a password record with a user ID matching $userID exists in the password
  597 table, it is removed and the method returns a true value. If one does exist,
  598 a false value is returned.
  599 
  600 =cut
  601 
  602 sub deletePassword($$) {
  603   my ($self, $userID) = @_;
  604 
  605   croak "putPassword: requires 1 argument"
  606     unless @_ == 2;
  607   croak "deletePassword: argument 1 must contain a user_id"
  608     unless defined $userID;
  609 
  610   return $self->{password}->delete($userID);
  611 }
  612 
  613 =back
  614 
  615 =cut
  616 
  617 ################################################################################
  618 # permission functions
  619 ################################################################################
  620 
  621 =head2 Permission Level Methods
  622 
  623 =over
  624 
  625 =item newPermissionLevel()
  626 
  627 Returns a new, empty permission level object.
  628 
  629 =cut
  630 
  631 sub newPermissionLevel {
  632   my ($self, @prototype) = @_;
  633   return $self->{permission}->{record}->new(@prototype);
  634 }
  635 
  636 =item listPermissionLevels()
  637 
  638 Returns a list of user IDs representing the records in the permission table.
  639 
  640 =cut
  641 
  642 sub listPermissionLevels($) {
  643   my ($self) = @_;
  644 
  645   croak "listPermissionLevels: requires 0 arguments"
  646     unless @_ == 1;
  647 
  648   return map { $_->[0] }
  649     $self->{permission}->list(undef);
  650 }
  651 
  652 =item addPermissionLevel($PermissionLevel)
  653 
  654 $PermissionLevel is a record object. The permission level will be added to the
  655 permission table if a permission level with the same user ID does not already
  656 exist. If one does exist, an exception is thrown. To add a permission level, a
  657 user with a matching user ID must exist in the user table.
  658 
  659 =cut
  660 
  661 sub addPermissionLevel($$) {
  662   my ($self, $PermissionLevel) = @_;
  663 
  664   croak "addPermissionLevel: requires 1 argument"
  665     unless @_ == 2;
  666   croak "addPermissionLevel: argument 1 must be of type ", $self->{permission}->{record}
  667     unless ref $PermissionLevel eq $self->{permission}->{record};
  668 
  669   checkKeyfields($PermissionLevel);
  670 
  671   croak "addPermissionLevel: permission level exists (perhaps you meant to use putPermissionLevel?)"
  672     if $self->{permission}->exists($PermissionLevel->user_id);
  673   croak "addPermissionLevel: user ", $PermissionLevel->user_id, " not found"
  674     unless $self->{user}->exists($PermissionLevel->user_id);
  675 
  676   return $self->{permission}->add($PermissionLevel);
  677 }
  678 
  679 =item getPermissionLevel($userID)
  680 
  681 If a record with a matching user ID exists, a record object containting that
  682 record's data will be returned. If no such record exists, one will be created.
  683 
  684 =cut
  685 
  686 sub getPermissionLevel($$) {
  687   my ($self, $userID) = @_;
  688 
  689   croak "getPermissionLevel: requires 1 argument"
  690     unless @_ == 2;
  691   croak "getPermissionLevel: argument 1 must contain a user_id"
  692     unless defined $userID;
  693 
  694   #return $self->{permission}->get($userID);
  695   return ( $self->getPermissionLevels($userID) )[0];
  696 }
  697 
  698 =item getPermissionLevels(@uesrIDs)
  699 
  700 Return a list of permission level records associated with the user IDs given. If
  701 there is no record associated with a given user ID, one will be created.
  702 
  703 =cut
  704 
  705 sub getPermissionLevels {
  706   my ($self, @userIDs) = @_;
  707 
  708   #croak "getPermissionLevels: requires 1 or more argument"
  709   # unless @_ >= 2;
  710   foreach my $i (0 .. $#userIDs) {
  711     croak "getPermissionLevels: element $i of argument list must contain a user_id"
  712       unless defined $userIDs[$i];
  713   }
  714 
  715   my @PermissionLevels = $self->{permission}->gets(map { [$_] } @userIDs);
  716 
  717   for (my $i = 0; $i < @PermissionLevels; $i++) {
  718     my $PermissionLevel = $PermissionLevels[$i];
  719     my $userID = $userIDs[$i];
  720     if (not defined $PermissionLevel) {
  721       #warn "not defined\n";
  722       if ($self->{user}->exists($userID)) {
  723         #warn "user exists\n";
  724         $PermissionLevel = $self->newPermissionLevel(user_id => $userID);
  725         #warn $PermissionLevel->toString, "\n";
  726         eval { $self->addPermissionLevel($PermissionLevel) };
  727         if ($@ and $@ !~ m/permission level exists/) {
  728           die "error while auto-creating permission level record for user $userID: \"$@\"";
  729         }
  730         $PermissionLevels[$i] = $PermissionLevel;
  731       }
  732     }
  733   }
  734 
  735   return @PermissionLevels;
  736 }
  737 
  738 =item putPermissionLevel($PermissionLevel)
  739 
  740 $PermissionLevel is a record object. If a permission level record with the same
  741 user ID exists in the permission table, the data in the record is replaced with
  742 the data in $PermissionLevel. If a matching permission level record does not
  743 exist, an exception is thrown.
  744 
  745 =cut
  746 
  747 sub putPermissionLevel($$) {
  748   my ($self, $PermissionLevel) = @_;
  749 
  750   croak "putPermissionLevel: requires 1 argument"
  751     unless @_ == 2;
  752   croak "putPermissionLevel: argument 1 must be of type ", $self->{permission}->{record}
  753     unless ref $PermissionLevel eq $self->{permission}->{record};
  754 
  755   checkKeyfields($PermissionLevel);
  756 
  757   croak "putPermissionLevel: permission level not found (perhaps you meant to use addPermissionLevel?)"
  758     unless $self->{permission}->exists($PermissionLevel->user_id);
  759 
  760   return $self->{permission}->put($PermissionLevel);
  761 }
  762 
  763 =item deletePermissionLevel($userID)
  764 
  765 If a permission level record with a user ID matching $userID exists in the
  766 permission table, it is removed and the method returns a true value. If one
  767 does exist, a false value is returned.
  768 
  769 =cut
  770 
  771 sub deletePermissionLevel($$) {
  772   my ($self, $userID) = @_;
  773 
  774   croak "deletePermissionLevel: requires 1 argument"
  775     unless @_ == 2;
  776   croak "deletePermissionLevel: argument 1 must contain a user_id"
  777     unless defined $userID;
  778 
  779   return $self->{permission}->delete($userID);
  780 }
  781 
  782 ################################################################################
  783 # key functions
  784 ################################################################################
  785 
  786 =head2 Key Methods
  787 
  788 =over
  789 
  790 =item newKey()
  791 
  792 Returns a new, empty key object.
  793 
  794 =cut
  795 
  796 sub newKey {
  797   my ($self, @prototype) = @_;
  798   return $self->{key}->{record}->new(@prototype);
  799 }
  800 
  801 =item listKeys()
  802 
  803 Returns a list of user IDs representing the records in the key table.
  804 
  805 =cut
  806 
  807 sub listKeys($) {
  808   my ($self) = @_;
  809 
  810   croak "listKeys: requires 0 arguments"
  811     unless @_ == 1;
  812 
  813   return map { $_->[0] }
  814     $self->{key}->list(undef);
  815 }
  816 
  817 =item addKey($Key)
  818 
  819 $Key is a record object. The key will be added to the key table if a key with
  820 the same user ID does not already exist. If one does exist, an exception is
  821 thrown. To add a key, a user with a matching user ID must exist in the user
  822 table.
  823 
  824 (Addition for proctored tests:  also allow user IDs to match userID1,userID2
  825 where both userIDs are valid.)
  826 
  827 =cut
  828 
  829 sub addKey($$) {
  830   my ($self, $Key) = @_;
  831 
  832   croak "addKey: requires 1 argument"
  833     unless @_ == 2;
  834   croak "addKey: argument 1 must be of type ", $self->{key}->{record}
  835     unless ref $Key eq $self->{key}->{record};
  836 
  837   checkKeyfields($Key, 1);  # 1 flags the possibility of a comma
  838 
  839   croak "addKey: key exists (perhaps you meant to use putKey?)"
  840     if $self->{key}->exists($Key->user_id);
  841   if ( $Key->user_id !~ /,/ ) {
  842       croak "addKey: user ", $Key->user_id, " not found"
  843     unless $self->{user}->exists($Key->user_id);
  844   } else {
  845       my ( $userID, $proctorID ) = split(/,/, $Key->user_id);
  846       croak "addKey: user $userID not found"
  847     unless $self->{user}->exists($userID);
  848       croak "addKey: proctor $proctorID not found"
  849     unless $self->{user}->exists($proctorID);
  850   }
  851 
  852   return $self->{key}->add($Key);
  853 }
  854 
  855 =item getKey($userID)
  856 
  857 If a record with a matching user ID exists, a record object containting that
  858 record's data will be returned. If no such record exists, an undefined value
  859 will be returned.
  860 
  861 =cut
  862 
  863 sub getKey($$) {
  864   my ($self, $userID) = @_;
  865 
  866   croak "getKey: requires 1 argument"
  867     unless @_ == 2;
  868   croak "getKey: argument 1 must contain a user_id"
  869     unless defined $userID;
  870 
  871   return $self->{key}->get($userID);
  872 }
  873 
  874 =item getKeys(@uesrIDs)
  875 
  876 Return a list of key records associated with the user IDs given. If there is no
  877 record associated with a given user ID, that element of the list will be
  878 undefined.
  879 
  880 =cut
  881 
  882 sub getKeys {
  883   my ($self, @userIDs) = @_;
  884 
  885   #croak "getKeys: requires 1 or more argument"
  886   # unless @_ >= 2;
  887   foreach my $i (0 .. $#userIDs) {
  888     croak "getKeys: element $i of argument list must contain a user_id"
  889       unless defined $userIDs[$i];
  890   }
  891 
  892   return $self->{key}->gets(map { [$_] } @userIDs);
  893 }
  894 
  895 =item putKey($Key)
  896 
  897 $Key is a record object. If a key record with the same user ID exists in the
  898 key table, the data in the record is replaced with the data in $Key. If a
  899 matching key record does not exist, an exception is thrown.
  900 
  901 =cut
  902 
  903 sub putKey($$) {
  904   my ($self, $Key) = @_;
  905 
  906   croak "putKey: requires 1 argument"
  907     unless @_ == 2;
  908   croak "putKey: argument 1 must be of type ", $self->{key}->{record}
  909     unless ref $Key eq $self->{key}->{record};
  910 
  911   checkKeyfields($Key, 1);  # 1 to allow a comma
  912 
  913   croak "putKey: key not found (perhaps you meant to use addKey?)"
  914     unless $self->{key}->exists($Key->user_id);
  915 
  916   return $self->{key}->put($Key);
  917 }
  918 
  919 =item deleteKey($userID)
  920 
  921 If a key record with a user ID matching $userID exists in the key table, it is
  922 removed and the method returns a true value. If one does exist, a false value
  923 is returned.
  924 
  925 =cut
  926 
  927 sub deleteKey($$) {
  928   my ($self, $userID) = @_;
  929 
  930   croak "deleteKey: requires 1 argument"
  931     unless @_ == 2;
  932   croak "deleteKey: argument 1 must contain a user_id"
  933     unless defined $userID;
  934 
  935   return $self->{key}->delete($userID);
  936 }
  937 
  938 ################################################################################
  939 # user functions
  940 ################################################################################
  941 
  942 =head2 User Methods
  943 
  944 =over
  945 
  946 =item newUser()
  947 
  948 Returns a new, empty user object.
  949 
  950 =cut
  951 
  952 sub newUser {
  953   my ($self, @prototype) = @_;
  954   return $self->{user}->{record}->new(@prototype);
  955 }
  956 
  957 =item listUsers()
  958 
  959 Returns a list of user IDs representing the records in the user table.
  960 
  961 =cut
  962 
  963 sub listUsers {
  964   my ($self) = @_;
  965 
  966   croak "listUsers: requires 0 arguments"
  967     unless @_ == 1;
  968 
  969   return map { $_->[0] }
  970     $self->{user}->list(undef);
  971 }
  972 
  973 =item addUser($User)
  974 
  975 $User is a record object. The user will be added to the user table if a user
  976 with the same user ID does not already exist. If one does exist, an exception
  977 is thrown.
  978 
  979 =cut
  980 
  981 sub addUser {
  982   my ($self, $User) = @_;
  983 
  984   croak "addUser: requires 1 argument"
  985     unless @_ == 2;
  986   croak "addUser: argument 1 must be of type ", $self->{user}->{record}
  987     unless ref $User eq $self->{user}->{record};
  988 
  989   checkKeyfields($User);
  990 
  991   croak "addUser: user exists (perhaps you meant to use putUser?)"
  992     if $self->{user}->exists($User->user_id);
  993 
  994   return $self->{user}->add($User);
  995 }
  996 
  997 =item getUser($userID)
  998 
  999 If a record with a matching user ID exists, a record object containting that
 1000 record's data will be returned. If no such record exists, an undefined value
 1001 will be returned.
 1002 
 1003 =cut
 1004 
 1005 sub getUser {
 1006   my ($self, $userID) = @_;
 1007 
 1008   croak "getUser: requires 1 argument"
 1009     unless @_ == 2;
 1010   croak "getUser: argument 1 must contain a user_id"
 1011     unless defined $userID;
 1012 
 1013   return $self->{user}->get($userID);
 1014 }
 1015 
 1016 =item getUsers(@uesrIDs)
 1017 
 1018 Return a list of user records associated with the user IDs given. If there is no
 1019 record associated with a given user ID, that element of the list will be
 1020 undefined.
 1021 
 1022 =cut
 1023 
 1024 sub getUsers {
 1025   my ($self, @userIDs) = @_;
 1026 
 1027   #croak "getUsers: requires 1 or more argument"
 1028   # unless @_ >= 2;
 1029   foreach my $i (0 .. $#userIDs) {
 1030     croak "getUsers: element $i of argument list must contain a user_id"
 1031       unless defined $userIDs[$i];
 1032   }
 1033 
 1034   return $self->{user}->gets(map { [$_] } @userIDs);
 1035 }
 1036 
 1037 =item putUser($User)
 1038 
 1039 $User is a record object. If a user record with the same user ID exists in the
 1040 user table, the data in the record is replaced with the data in $User. If a
 1041 matching user record does not exist, an exception is thrown.
 1042 
 1043 =cut
 1044 
 1045 sub putUser {
 1046   my ($self, $User) = @_;
 1047 
 1048   croak "putUser: requires 1 argument"
 1049     unless @_ == 2;
 1050   croak "putUser: argument 1 must be of type ", $self->{user}->{record}
 1051     unless ref $User eq $self->{user}->{record};
 1052 
 1053   checkKeyfields($User);
 1054 
 1055   croak "putUser: user not found (perhaps you meant to use addUser?)"
 1056     unless $self->{user}->exists($User->user_id);
 1057 
 1058   return $self->{user}->put($User);
 1059 }
 1060 
 1061 =item deleteUser($userID)
 1062 
 1063 If a user record with a user ID matching $userID exists in the user table, it
 1064 is removed and the method returns a true value. If one does exist, a false
 1065 value is returned. When a user record is deleted, all records associated with
 1066 that user are also deleted. This includes the password, permission, and key
 1067 records, and all user set records for that user.
 1068 
 1069 =cut
 1070 
 1071 sub deleteUser {
 1072   my ($self, $userID) = @_;
 1073 
 1074   croak "deleteUser: requires 1 argument"
 1075     unless @_ == 2;
 1076   croak "deleteUser: argument 1 must contain a user_id"
 1077     unless defined $userID;
 1078 
 1079   $self->deleteUserSet($userID, undef);
 1080   $self->deletePassword($userID);
 1081   $self->deletePermissionLevel($userID);
 1082   $self->deleteKey($userID);
 1083   return $self->{user}->delete($userID);
 1084 }
 1085 
 1086 =back
 1087 
 1088 =cut
 1089 
 1090 ################################################################################
 1091 # set functions
 1092 ################################################################################
 1093 
 1094 =head2 Global Set Methods
 1095 
 1096 FIXME: write this
 1097 
 1098 =over
 1099 
 1100 =cut
 1101 
 1102 =item newGlobalSet()
 1103 
 1104 =cut
 1105 
 1106 sub newGlobalSet {
 1107   my ($self, @prototype) = @_;
 1108   return $self->{set}->{record}->new(@prototype);
 1109 }
 1110 
 1111 =item listGlobalSets()
 1112 
 1113 =cut
 1114 
 1115 sub listGlobalSets {
 1116   my ($self) = @_;
 1117 
 1118   croak "listGlobalSets: requires 0 arguments"
 1119     unless @_ == 1;
 1120 
 1121   return map { $_->[0] }
 1122     $self->{set}->list(undef);
 1123 }
 1124 
 1125 =item addGlobalSet($GlobalSet)
 1126 
 1127 =cut
 1128 
 1129 sub addGlobalSet {
 1130   my ($self, $GlobalSet) = @_;
 1131 
 1132   croak "addGlobalSet: requires 1 argument"
 1133     unless @_ == 2;
 1134   croak "addGlobalSet: argument 1 must be of type ", $self->{set}->{record}
 1135     unless ref $GlobalSet eq $self->{set}->{record};
 1136 
 1137   checkKeyfields($GlobalSet);
 1138 
 1139   croak "addGlobalSet: global set exists (perhaps you meant to use putGlobalSet?)"
 1140     if $self->{set}->exists($GlobalSet->set_id);
 1141 
 1142   return $self->{set}->add($GlobalSet);
 1143 }
 1144 
 1145 =item addGlobalSet($setID)
 1146 
 1147 =cut
 1148 
 1149 sub getGlobalSet {
 1150   my ($self, $setID) = @_;
 1151 
 1152   croak "getGlobalSet: requires 1 argument"
 1153     unless @_ == 2;
 1154   croak "getGlobalSet: argument 1 must contain a set_id"
 1155     unless defined $setID;
 1156 
 1157   return $self->{set}->get($setID);
 1158 }
 1159 
 1160 =item getGlobalSets(@setIDs)
 1161 
 1162 Return a list of global set records associated with the record IDs given. If
 1163 there is no record associated with a given record ID, that element of the list
 1164 will be undefined.
 1165 
 1166 =cut
 1167 
 1168 sub getGlobalSets {
 1169   my ($self, @setIDs) = @_;
 1170 
 1171   #croak "getGlobalSets: requires 1 or more argument"
 1172   # unless @_ >= 2;
 1173   foreach my $i (0 .. $#setIDs) {
 1174     croak "getGlobalSets: element $i of argument list must contain a set_id"
 1175       unless defined $setIDs[$i];
 1176   }
 1177 
 1178   return $self->{set}->gets(map { [$_] } @setIDs);
 1179 }
 1180 
 1181 =item addGlobalSet($GlobalSet)
 1182 
 1183 =cut
 1184 
 1185 sub putGlobalSet {
 1186   my ($self, $GlobalSet) = @_;
 1187 
 1188   croak "putGlobalSet: requires 1 argument"
 1189     unless @_ == 2;
 1190   croak "putGlobalSet: argument 1 must be of type ", $self->{set}->{record}
 1191     unless ref $GlobalSet eq $self->{set}->{record};
 1192 
 1193   checkKeyfields($GlobalSet);
 1194 
 1195   croak "putGlobalSet: global set not found (perhaps you meant to use addGlobalSet?)"
 1196     unless $self->{set}->exists($GlobalSet->set_id);
 1197 
 1198   return $self->{set}->put($GlobalSet);
 1199 }
 1200 
 1201 =item addGlobalSet($setID)
 1202 
 1203 =cut
 1204 
 1205 sub deleteGlobalSet {
 1206   my ($self, $setID) = @_;
 1207 
 1208   croak "deleteGlobalSet: requires 1 argument"
 1209     unless @_ == 2;
 1210   croak "deleteGlobalSet: argument 1 must contain a set_id"
 1211     unless defined $setID or caller eq __PACKAGE__;
 1212 
 1213   $self->deleteUserSet(undef, $setID);
 1214 
 1215   $self->deleteGlobalProblem($setID, undef);
 1216   return $self->{set}->delete($setID);
 1217 }
 1218 
 1219 =back
 1220 
 1221 =cut
 1222 
 1223 ################################################################################
 1224 # set_user functions
 1225 ################################################################################
 1226 
 1227 =head2 User-Specific Set Methods
 1228 
 1229 FIXME: write this
 1230 
 1231 =over
 1232 
 1233 =cut
 1234 
 1235 sub newUserSet {
 1236   my ($self, @prototype) = @_;
 1237   return $self->{set_user}->{record}->new(@prototype);
 1238 }
 1239 
 1240 sub countSetUsers {
 1241   my ($self, $setID) = @_;
 1242 
 1243   croak "countSetUsers: requires 1 argument"
 1244     unless @_ == 2;
 1245   croak "countSetUsers: argument 1 must contain a set_id"
 1246     unless defined $setID;
 1247 
 1248   # inefficient way
 1249   #return scalar $self->{set_user}->list(undef, $setID);
 1250 
 1251   # efficient way
 1252   return $self->{set_user}->count(undef, $setID);
 1253 }
 1254 
 1255 sub listSetUsers {
 1256   my ($self, $setID) = @_;
 1257 
 1258   carp "listSetUsers called in SCALAR context: use countSetUsers instead!\n"
 1259     unless wantarray;
 1260 
 1261   croak "listSetUsers: requires 1 argument"
 1262     unless @_ == 2;
 1263   croak "listSetUsers: argument 1 must contain a set_id"
 1264     unless defined $setID;
 1265 
 1266   return map { $_->[0] } # extract user_id
 1267     $self->{set_user}->list(undef, $setID);
 1268 }
 1269 
 1270 sub countUserSets {
 1271   my ($self, $userID) = @_;
 1272 
 1273   croak "countUserSets: requires 1 argument"
 1274     unless @_ == 2;
 1275   croak "countUserSets: argument 1 must contain a user_id"
 1276     unless defined $userID;
 1277 
 1278   return $self->{set_user}->count($userID, undef);
 1279 }
 1280 
 1281 sub listUserSets {
 1282   my ($self, $userID) = @_;
 1283 
 1284   croak "listUserSets: requires 1 argument"
 1285     unless @_ == 2;
 1286   croak "listUserSets: argument 1 must contain a user_id"
 1287     unless defined $userID;
 1288 
 1289   return map { $_->[1] } # extract set_id
 1290     $self->{set_user}->list($userID, undef);
 1291 }
 1292 
 1293 sub addUserSet {
 1294   my ($self, $UserSet) = @_;
 1295 
 1296   croak "addUserSet: requires 1 argument"
 1297     unless @_ == 2;
 1298   croak "addUserSet: argument 1 must be of type ", $self->{set_user}->{record}
 1299     unless ref $UserSet eq $self->{set_user}->{record};
 1300 
 1301   checkKeyfields($UserSet);
 1302 
 1303   croak "addUserSet: user set exists (perhaps you meant to use putUserSet?)"
 1304     if $self->{set_user}->exists($UserSet->user_id, $UserSet->set_id);
 1305   croak "addUserSet: user ", $UserSet->user_id, " not found"
 1306     unless $self->{user}->exists($UserSet->user_id);
 1307   croak "addUserSet: set ", $UserSet->set_id, " not found"
 1308     unless $self->{set}->exists($UserSet->set_id);
 1309 
 1310   return $self->{set_user}->add($UserSet);
 1311 }
 1312 
 1313 sub addVersionedUserSet {
 1314     my ($self, $UserSet) = @_;
 1315 
 1316 # this is the same as addUserSet,allowing for set names of the form setID,vN
 1317 
 1318     croak "addVersionedUserSet: requires 1 argument"
 1319   unless @_ == 2;
 1320     croak "addVersionedUserSet: argument 1 must be of type ",
 1321         $self->{set_user}->{record}
 1322   unless ref $UserSet eq $self->{set_user}->{record};
 1323 
 1324 # $versioned is a flag that we send in to allow commas in the set name
 1325 #    for versioned sets
 1326     my $versioned = 1;
 1327     checkKeyfields($UserSet, $versioned);
 1328     my ($nonVersionedSetName) = ($UserSet->set_id =~ /^(.*),v\d+$/);
 1329 
 1330     croak "addUserSet: user set exists (perhaps you meant to use putUserSet?)"
 1331   if $self->{set_user}->exists($UserSet->user_id, $UserSet->set_id);
 1332     croak "addUserSet: user ", $UserSet->user_id, " not found"
 1333   unless $self->{user}->exists($UserSet->user_id);
 1334 # croak "addUserSet: set ", $UserSet->set_id, " not found"
 1335 #   unless $self->{set}->exists($UserSet->set_id);
 1336 # here the appropriate check is whether a global set of the nonversioned set
 1337 #    name exists
 1338     croak "addVersionedUserSet: set ", $nonVersionedSetName, " not found"
 1339   unless $self->{set}->exists( $nonVersionedSetName );
 1340 
 1341     return $self->{set_user}->add($UserSet);
 1342 }
 1343 
 1344 sub getUserSet {
 1345   my ($self, $userID, $setID) = @_;
 1346 
 1347   croak "getUserSet: requires 2 arguments"
 1348     unless @_ == 3;
 1349   croak "getUserSet: argument 1 must contain a user_id"
 1350     unless defined $userID;
 1351   croak "getUserSet: argument 2 must contain a set_id"
 1352     unless defined $setID;
 1353 
 1354   #return $self->{set_user}->get($userID, $setID);
 1355   return ( $self->getUserSets([$userID, $setID]) )[0];
 1356 }
 1357 
 1358 =item getUserSets(@userSetIDs)
 1359 
 1360 Return a list of user set records associated with the record IDs given. If there
 1361 is no record associated with a given record ID, that element of the list will be
 1362 undefined. @userProblemIDs consists of references to arrays in which the first
 1363 element is the user_id and the second element is the set_id.
 1364 
 1365 =cut
 1366 
 1367 sub getUserSets {
 1368   my ($self, @userSetIDs) = @_;
 1369 
 1370   #croak "getUserSets: requires 1 or more argument"
 1371   # unless @_ >= 2;
 1372   foreach my $i (0 .. $#userSetIDs) {
 1373     croak "getUserSets: element $i of argument list must contain a <user_id, set_id> pair"
 1374       unless defined $userSetIDs[$i]
 1375              and ref $userSetIDs[$i] eq "ARRAY"
 1376              and @{$userSetIDs[$i]} == 2
 1377              and defined $userSetIDs[$i]->[0]
 1378              and defined $userSetIDs[$i]->[1];
 1379   }
 1380 
 1381   return $self->{set_user}->gets(@userSetIDs);
 1382 }
 1383 
 1384 sub getUserSetVersions {
 1385     my ( $self, $uid, $sid, $versionNum ) = @_;
 1386 # in:  $uid is a userID, $sid is a setID, and $versionNum is a version number
 1387 #      userID has set versions 1 through $versionNum defined
 1388 # out: an array of user set objects is returned for the indicated version
 1389 #      numbers
 1390 
 1391     croak "getUserSetVersions: requires three arguments, userID, setID, and " .
 1392   "versionNum" if ( @_ < 3 );
 1393 
 1394     my @userSetIDs = ();
 1395     foreach my $i ( 1 .. $versionNum ) {
 1396   push( @userSetIDs, [ $uid, "$sid,v$i" ] );
 1397     }
 1398 
 1399     return $self->getUserSets( @userSetIDs );
 1400 }
 1401 
 1402 sub putUserSet {
 1403   my ($self, $UserSet) = @_;
 1404 
 1405   croak "putUserSet: requires 1 argument"
 1406     unless @_ == 2;
 1407   croak "putUserSet: argument 1 must be of type ", $self->{set_user}->{record}
 1408     unless ref $UserSet eq $self->{set_user}->{record};
 1409 
 1410   checkKeyfields($UserSet);
 1411 
 1412   croak "putUserSet: user set not found (perhaps you meant to use addUserSet?)"
 1413     unless $self->{set_user}->exists($UserSet->user_id, $UserSet->set_id);
 1414   croak "putUserSet: user ", $UserSet->user_id, " not found"
 1415     unless $self->{user}->exists($UserSet->user_id);
 1416   croak "putUserSet: set ", $UserSet->set_id, " not found"
 1417     unless $self->{set}->exists($UserSet->set_id);
 1418 
 1419   return $self->{set_user}->put($UserSet);
 1420 }
 1421 
 1422 sub putVersionedUserSet {
 1423     my ($self, $UserSet) = @_;
 1424 # this exists separate from putUserSet only so that we can make it harder
 1425 #   for anyone else to use commas in setIDs
 1426 
 1427     croak "putUserSet: requires 1 argument"
 1428   unless @_ == 2;
 1429     croak "putUserSet: argument 1 must be of type ", $self->{set_user}->{record}
 1430   unless ref $UserSet eq $self->{set_user}->{record};
 1431 
 1432     # versioned allows us to have a wacked out setID
 1433     my $versioned = 1;
 1434     checkKeyfields($UserSet, $versioned);
 1435 
 1436     my $nonVersionedSetID = $UserSet->set_id;
 1437     $nonVersionedSetID =~ s/,v\d+$//;
 1438 #    my ($nonVersionedSetID) = ($UserSet->set_id =~ /^(.*)(,v\d+)?$/);
 1439     croak "putVersionedUserSet: user set not found (perhaps you meant " .
 1440         "to use addUserSet?)"
 1441   unless $self->{set_user}->exists($UserSet->user_id, $UserSet->set_id);
 1442     croak "putVersionedUserSet: user ", $UserSet->user_id, " not found"
 1443   unless $self->{user}->exists($UserSet->user_id);
 1444     croak "putVersionedUserSet: set $nonVersionedSetID not found"
 1445   unless $self->{set}->exists($nonVersionedSetID);
 1446 
 1447     return $self->{set_user}->put($UserSet);
 1448 }
 1449 
 1450 sub deleteUserSet {
 1451   my ($self, $userID, $setID, $skipVersionDel) = @_;
 1452 
 1453   croak "getUserSet: requires 2 arguments"
 1454     unless @_ == 3 or @_ == 4;
 1455   croak "getUserSet: argument 1 must contain a user_id"
 1456     unless defined $userID or caller eq __PACKAGE__;
 1457   croak "getUserSet: argument 2 must contain a set_id"
 1458     unless defined $userID or caller eq __PACKAGE__;
 1459 
 1460   $self->deleteUserSetVersions( $userID, $setID )
 1461       if ( defined($setID) && ! ( defined($skipVersionDel) &&
 1462      $skipVersionDel ) );
 1463   $self->deleteUserProblem($userID, $setID, undef);
 1464   return $self->{set_user}->delete($userID, $setID);
 1465 }
 1466 
 1467 sub deleteUserSetVersions {
 1468     my ($self, $userID, $setID) = @_;
 1469 
 1470 # this only gets called from deleteUserSet, so we don't worry about $setID
 1471 #    not being defined
 1472 
 1473 # make a list of all users to delete set versions for.  if we have a userID,
 1474 #    then just delete versions for that user
 1475     my @allUsers = ();
 1476     if ( defined( $userID ) ) {
 1477   push( @allUsers, $userID );
 1478     } else {
 1479 # otherwise, get a list of all users to whom the set is assigned, and delete
 1480 #    all versions for all of them
 1481   @allUsers = $self->listSetUsers( $setID );
 1482     }
 1483 
 1484 # skip version deletion when calling deleteUserSet from here
 1485     my $skipVersionDel = 1;
 1486 
 1487 # go through each userID and delete all versions of the set for each
 1488     foreach my $uid ( @allUsers ) {
 1489   my $setVersionNumber = $self->getUserSetVersionNumber($uid, $setID);
 1490   if ( $setVersionNumber ) {
 1491       for ( my $i=1; $i<=$setVersionNumber; $i++ ) {
 1492     eval { $self->deleteUserSet( $uid, "$setID,v$i",
 1493                $skipVersionDel ) };
 1494     return $@ if ( $@ );
 1495       }
 1496   }
 1497     }
 1498 }
 1499 
 1500 sub getUserSetVersionNumber {
 1501     my ( $self, $uid, $sid ) = @_;
 1502 # in:  uid and sid are user and set ids.  the setID is the 'global' setID
 1503 #      for the user, not a versioned value
 1504 # out: the latest version number of the set that has been assigned to the
 1505 #      user is returned.
 1506 
 1507     croak "getUserSetVersionNumber: requires 2 arguments, a user and set ID"
 1508   unless @_ == 3 && defined $uid && defined $sid;
 1509 
 1510 # is there a better way of doing this?  it seems like we need to know the
 1511 #    number of versions to be able to do a mass get.  something like a get
 1512 #    where sid looks like $sid,v\d would work... but is incompatible w/gdbm
 1513 #    my $i=1;
 1514 #    if ( $self->{set_user}->exists( $uid, $sid ) ) {
 1515 # while ( $self->{set_user}->exists( $uid, "$sid,v$i" ) ) {
 1516 #     $i++;
 1517 # }
 1518 #    }
 1519 #    return ($i-1);
 1520 # or, we can just get all sets for the user and figure out which of them
 1521 #    look like the sid.
 1522     my @allSetIDs = $self->listUserSets( $uid );
 1523     my @setIDs = sort( grep { /^$sid,v\d+$/ } @allSetIDs );
 1524 #    my $lastSetID = ( sort( @setIDs ) )[-1];
 1525     my $lastSetID = $setIDs[-1];
 1526 # I think this should be defined, unless the set hasn't been assigned to
 1527 #    the user at all, which we hope wouldn't have happened at this juncture
 1528     if ( not defined($lastSetID) ) {
 1529   return 0;
 1530     } else {
 1531   # we have to deal with the fact that 10 sorts to precede 2 (etc.)
 1532   my @vNums = map { /^$sid,v(\d+)$/ } @setIDs;
 1533   return ( ( sort {$a<=>$b} @vNums )[-1] );
 1534     }
 1535 }
 1536 
 1537 =back
 1538 
 1539 =cut
 1540 
 1541 ################################################################################
 1542 # problem functions
 1543 ################################################################################
 1544 
 1545 =head2 Global Problem Methods
 1546 
 1547 FIXME: write this
 1548 
 1549 =over
 1550 
 1551 =cut
 1552 
 1553 sub newGlobalProblem {
 1554   my ($self, @prototype) = @_;
 1555   return $self->{problem}->{record}->new(@prototype);
 1556 }
 1557 
 1558 sub listGlobalProblems {
 1559   my ($self, $setID) = @_;
 1560 
 1561   croak "listGlobalProblems: requires 1 arguments"
 1562     unless @_ == 2;
 1563   croak "listGlobalProblems: argument 1 must contain a set_id"
 1564     unless defined $setID;
 1565 
 1566   return map { $_->[1] }
 1567     $self->{problem}->list($setID, undef);
 1568 }
 1569 
 1570 sub addGlobalProblem {
 1571   my ($self, $GlobalProblem) = @_;
 1572 
 1573   croak "addGlobalProblem: requires 1 argument"
 1574     unless @_ == 2;
 1575   croak "addGlobalProblem: argument 1 must be of type ", $self->{problem}->{record}
 1576     unless ref $GlobalProblem eq $self->{problem}->{record};
 1577 
 1578   checkKeyfields($GlobalProblem);
 1579 
 1580   croak "addGlobalProblem: global problem exists (perhaps you meant to use putGlobalProblem?)"
 1581     if $self->{problem}->exists($GlobalProblem->set_id, $GlobalProblem->problem_id);
 1582   croak "addGlobalProblem: set ", $GlobalProblem->set_id, " not found"
 1583     unless $self->{set}->exists($GlobalProblem->set_id);
 1584 
 1585   return $self->{problem}->add($GlobalProblem);
 1586 }
 1587 
 1588 sub getGlobalProblem {
 1589   my ($self, $setID, $problemID) = @_;
 1590 
 1591   croak "getGlobalProblem: requires 2 arguments"
 1592     unless @_ == 3;
 1593   croak "getGlobalProblem: argument 1 must contain a set_id"
 1594     unless defined $setID;
 1595   croak "getGlobalProblem: argument 2 must contain a problem_id"
 1596     unless defined $problemID;
 1597 
 1598   return $self->{problem}->get($setID, $problemID);
 1599 }
 1600 
 1601 =item getGlobalProblems(@problemIDs)
 1602 
 1603 Return a list of global set records associated with the record IDs given. If
 1604 there is no record associated with a given record ID, that element of the list
 1605 will be undefined. @problemIDs consists of references to arrays in which the
 1606 first element is the set_id, and the second element is the problem_id.
 1607 
 1608 =cut
 1609 
 1610 sub getGlobalProblems {
 1611   my ($self, @problemIDs) = @_;
 1612 
 1613   #croak "getGlobalProblems: requires 1 or more argument"
 1614   # unless @_ >= 2;
 1615   foreach my $i (0 .. $#problemIDs) {
 1616     croak "getUserSets: element $i of argument list must contain a <set_id, problem_id> pair"
 1617       unless defined $problemIDs[$i]
 1618              and ref $problemIDs[$i] eq "ARRAY"
 1619              and @{$problemIDs[$i]} == 2
 1620              and defined $problemIDs[$i]->[0]
 1621              and defined $problemIDs[$i]->[1];
 1622   }
 1623 
 1624   return $self->{problem}->gets(@problemIDs);
 1625 }
 1626 
 1627 =item getAllGlobalProblems($setID)
 1628 
 1629 Returns a list of Problem objects representing all the problems in the given
 1630 global set. When using the WW1Hash/GlobalTableEmulator schemas, this is far
 1631 more efficient than using listGlobalProblems and getGlobalProblems.
 1632 
 1633 =cut
 1634 
 1635 sub getAllGlobalProblems {
 1636   my ($self, $setID) = @_;
 1637 
 1638   croak "getAllGlobalProblems: requires 1 arguments"
 1639     unless @_ == 2;
 1640   croak "getAllGlobalProblems: argument 1 must contain a set_id"
 1641     unless defined $setID;
 1642 
 1643   if ($self->{problem}->can("getAll")) {
 1644     return $self->{problem}->getAll($setID);
 1645   } else {
 1646     my @problemIDPairs = $self->{problem}->list($setID, undef);
 1647     return $self->{problem}->gets(@problemIDPairs);
 1648   }
 1649 }
 1650 
 1651 sub putGlobalProblem {
 1652   my ($self, $GlobalProblem) = @_;
 1653 
 1654   croak "putGlobalProblem: requires 1 argument"
 1655     unless @_ == 2;
 1656   croak "putGlobalProblem: argument 1 must be of type ", $self->{problem}->{record}
 1657     unless ref $GlobalProblem eq $self->{problem}->{record};
 1658 
 1659   checkKeyfields($GlobalProblem);
 1660 
 1661   croak "putGlobalProblem: global problem not found (perhaps you meant to use addGlobalProblem?)"
 1662     unless $self->{problem}->exists($GlobalProblem->set_id, $GlobalProblem->problem_id);
 1663   croak "putGlobalProblem: set ", $GlobalProblem->set_id, " not found"
 1664     unless $self->{set}->exists($GlobalProblem->set_id);
 1665 
 1666   return $self->{problem}->put($GlobalProblem);
 1667 }
 1668 
 1669 sub deleteGlobalProblem {
 1670   my ($self, $setID, $problemID) = @_;
 1671 
 1672   croak "deleteGlobalProblem: requires 2 arguments"
 1673     unless @_ == 3;
 1674   croak "deleteGlobalProblem: argument 1 must contain a set_id"
 1675     unless defined $setID or caller eq __PACKAGE__;
 1676   croak "deleteGlobalProblem: argument 2 must contain a problem_id"
 1677     unless defined $problemID or caller eq __PACKAGE__;
 1678 
 1679   $self->deleteUserProblem(undef, $setID, $problemID);
 1680   return $self->{problem}->delete($setID, $problemID);
 1681 }
 1682 
 1683 =back
 1684 
 1685 =cut
 1686 
 1687 ################################################################################
 1688 # problem_user functions
 1689 ################################################################################
 1690 
 1691 =head2 User-Specific Problem Methods
 1692 
 1693 FIXME: write this
 1694 
 1695 =over
 1696 
 1697 =cut
 1698 
 1699 sub newUserProblem {
 1700   my ($self, @prototype) = @_;
 1701   return $self->{problem_user}->{record}->new(@prototype);
 1702 }
 1703 
 1704 sub countProblemUsers {
 1705   my ($self, $setID, $problemID) = @_;
 1706 
 1707   croak "countProblemUsers: requires 2 arguments"
 1708     unless @_ == 3;
 1709   croak "countProblemUsers: argument 1 must contain a set_id"
 1710     unless defined $setID;
 1711   croak "countProblemUsers: argument 2 must contain a problem_id"
 1712     unless defined $problemID;
 1713 
 1714   # the slow way
 1715   #return scalar $self->{problem_user}->list(undef, $setID, $problemID);
 1716 
 1717   # the fast way
 1718   return $self->{problem_user}->count(undef, $setID, $problemID);
 1719 }
 1720 
 1721 sub listProblemUsers {
 1722   my ($self, $setID, $problemID) = @_;
 1723 
 1724   carp "listProblemUsers called in SCALAR context: use countProblemUsers instead!\n"
 1725     unless wantarray;
 1726 
 1727   croak "listProblemUsers: requires 2 arguments"
 1728     unless @_ == 3;
 1729   croak "listProblemUsers: argument 1 must contain a set_id"
 1730     unless defined $setID;
 1731   croak "listProblemUsers: argument 2 must contain a problem_id"
 1732     unless defined $problemID;
 1733 
 1734   return map { $_->[0] } # extract user_id
 1735     $self->{problem_user}->list(undef, $setID, $problemID);
 1736 }
 1737 
 1738 sub listUserProblems {
 1739   my ($self, $userID, $setID) = @_;
 1740 
 1741   croak "listUserProblems: requires 2 arguments"
 1742     unless @_ == 3;
 1743   croak "listUserProblems: argument 1 must contain a user_id"
 1744     unless defined $userID;
 1745   croak "listUserProblems: argument 2 must contain a set_id"
 1746     unless defined $setID;
 1747 
 1748   return map { $_->[2] } # extract problem_id
 1749     $self->{problem_user}->list($userID, $setID, undef);
 1750 }
 1751 
 1752 sub addUserProblem {
 1753   my ($self, $UserProblem) = @_;
 1754 
 1755   croak "addUserProblem: requires 1 argument"
 1756     unless @_ == 2;
 1757   croak "addUserProblem: argument 1 must be of type ", $self->{problem_user}->{record}
 1758     unless ref $UserProblem eq $self->{problem_user}->{record};
 1759 
 1760   my $setID = $UserProblem->set_id;
 1761   if ( $setID =~ /^(.*),v\d+/ ) {  # then it's a versioned set
 1762       $setID = $1;
 1763       checkKeyfields($UserProblem, 1);
 1764   } else {
 1765       checkKeyfields($UserProblem);
 1766   }
 1767 
 1768   croak "addUserProblem: user problem exists (perhaps you meant to use putUserProblem?)"
 1769     if $self->{problem_user}->exists($UserProblem->user_id, $UserProblem->set_id, $UserProblem->problem_id);
 1770   croak "addUserProblem: user set $setID for user ", $UserProblem->user_id, " not found"
 1771     unless $self->{set_user}->exists($UserProblem->user_id, $setID);
 1772   croak "addUserProblem: problem ", $UserProblem->problem_id, " in set ", $setID, " not found"
 1773     unless $self->{problem}->exists($setID, $UserProblem->problem_id);
 1774 
 1775   return $self->{problem_user}->add($UserProblem);
 1776 }
 1777 
 1778 sub getUserProblem {
 1779   my ($self, $userID, $setID, $problemID) = @_;
 1780 
 1781   croak "getUserProblem: requires 3 arguments"
 1782     unless @_ == 4;
 1783   croak "getUserProblem: argument 1 must contain a user_id"
 1784     unless defined $userID;
 1785   croak "getUserProblem: argument 2 must contain a set_id"
 1786     unless defined $setID;
 1787   croak "getUserProblem: argument 3 must contain a problem_id"
 1788     unless defined $problemID;
 1789 
 1790   return ( $self->getUserProblems([$userID, $setID, $problemID]) )[0];
 1791 }
 1792 
 1793 =item getUserProblems(@userProblemIDs)
 1794 
 1795 Return a list of user set records associated with the user IDs given. If there
 1796 is no record associated with a given user ID, that element of the list will be
 1797 undefined. @userProblemIDs consists of references to arrays in which the first
 1798 element is the user_id, the second element is the set_id, and the third element
 1799 is the problem_id.
 1800 
 1801 =cut
 1802 
 1803 sub getUserProblems {
 1804   my ($self, @userProblemIDs) = @_;
 1805 
 1806   #croak "getUserProblems: requires 1 or more argument"
 1807   # unless @_ >= 2;
 1808   foreach my $i (0 .. $#userProblemIDs) {
 1809     croak "getUserProblems: element $i of argument list must contain a <user_id, set_id, problem_id> triple"
 1810       unless defined $userProblemIDs[$i]
 1811              and ref $userProblemIDs[$i] eq "ARRAY"
 1812              and @{$userProblemIDs[$i]} == 3
 1813              and defined $userProblemIDs[$i]->[0]
 1814              and defined $userProblemIDs[$i]->[1]
 1815              and defined $userProblemIDs[$i]->[2];
 1816   }
 1817 
 1818   return $self->{problem_user}->gets(@userProblemIDs);
 1819 }
 1820 
 1821 =item getAllUserProblems($userID, $setID)
 1822 
 1823 Returns a list of UserProblem objects representing all the problems in the
 1824 given set. When using the WW1Hash/GlobalTableEmulator schemas, this is far
 1825 more efficient than using listUserProblems and getUserProblems.
 1826 
 1827 =cut
 1828 
 1829 sub getAllUserProblems {
 1830   my ($self, $userID, $setID) = @_;
 1831 
 1832   croak "getAllUserProblems: requires 2 arguments"
 1833     unless @_ == 3;
 1834   croak "getAllUserProblems: argument 1 must contain a user_id"
 1835     unless defined $userID;
 1836   croak "getAllUserProblems: argument 2 must contain a set_id"
 1837     unless defined $setID;
 1838 
 1839   if ($self->{problem_user}->can("getAll")) {
 1840     return $self->{problem_user}->getAll($userID, $setID);
 1841   } else {
 1842     my @problemIDTriples = $self->{problem_user}->list($userID, $setID, undef);
 1843     return $self->{problem_user}->gets(@problemIDTriples);
 1844   }
 1845 }
 1846 
 1847 sub putUserProblem {
 1848   my ($self, $UserProblem, $versioned) = @_;
 1849 # $versioned is an optional argument which lets us slip versioned setIDs
 1850 #    through checkKeyfields.  this makes the first croak message a little
 1851 #    disingenuous, of course.
 1852 
 1853   croak "putUserProblem: requires 1 argument"
 1854     unless @_ == 2 or @_ == 3;
 1855   croak "putUserProblem: argument 1 must be of type ", $self->{problem_user}->{record}
 1856     unless ref $UserProblem eq $self->{problem_user}->{record};
 1857 
 1858   checkKeyfields($UserProblem, $versioned);
 1859 
 1860   croak "putUserProblem: user set ", $UserProblem->set_id, " for user ", $UserProblem->user_id, " not found"
 1861     unless $self->{set_user}->exists($UserProblem->user_id, $UserProblem->set_id);
 1862   croak "putUserProblem: user problem not found (perhaps you meant to use addUserProblem?)"
 1863     unless $self->{problem_user}->exists($UserProblem->user_id, $UserProblem->set_id, $UserProblem->problem_id);
 1864 
 1865 # allow versioned set names when $versioned is defined and true
 1866   my $unversionedSetID = $UserProblem->set_id;
 1867   $unversionedSetID =~ s/,v\d+$// if ( defined($versioned) && $versioned );
 1868   croak "putUserProblem: problem ", $UserProblem->problem_id, " in set ", $UserProblem->set_id, " not found"
 1869     unless $self->{problem}->exists($unversionedSetID, $UserProblem->problem_id);
 1870 
 1871   return $self->{problem_user}->put($UserProblem);
 1872 }
 1873 
 1874 sub deleteUserProblem {
 1875   my ($self, $userID, $setID, $problemID) = @_;
 1876 
 1877   croak "getUserProblem: requires 3 arguments"
 1878     unless @_ == 4;
 1879   croak "getUserProblem: argument 1 must contain a user_id"
 1880     unless defined $userID or caller eq __PACKAGE__;
 1881   croak "getUserProblem: argument 2 must contain a set_id"
 1882     unless defined $setID or caller eq __PACKAGE__;
 1883   croak "getUserProblem: argument 3 must contain a problem_id"
 1884     unless defined $problemID or caller eq __PACKAGE__;
 1885 
 1886   return $self->{problem_user}->delete($userID, $setID, $problemID);
 1887 }
 1888 
 1889 =back
 1890 
 1891 =cut
 1892 
 1893 ################################################################################
 1894 # set+set_user functions
 1895 ################################################################################
 1896 
 1897 =head2 Set Merging Methods
 1898 
 1899 These functions combine a global set and a user set to create a merged set,
 1900 which is returned. Any field that is not defined in the user set is taken from
 1901 the global set. Merged sets have the same type as user sets.
 1902 
 1903 =over
 1904 
 1905 =cut
 1906 
 1907 sub getGlobalUserSet {
 1908   carp "getGlobalUserSet: this method is deprecated -- use getMergedSet instead";
 1909   return shift->getMergedSet(@_);
 1910 }
 1911 
 1912 =item getMergedSet($userID, $setID)
 1913 
 1914 Returns a merged set record associated with the record IDs given. If there is no
 1915 record associated with a given record ID, the undefined value is returned.
 1916 
 1917 =cut
 1918 
 1919 sub getMergedSet {
 1920   my ($self, $userID, $setID) = @_;
 1921 
 1922   croak "getMergedSet: requires 2 arguments"
 1923     unless @_ == 3;
 1924   croak "getMergedSet: argument 1 must contain a user_id"
 1925     unless defined $userID;
 1926   croak "getMergedSet: argument 2 must contain a set_id"
 1927     unless defined $setID;
 1928 
 1929   return ( $self->getMergedSets([$userID, $setID]) )[0];
 1930 }
 1931 
 1932 sub getMergedVersionedSet {
 1933     my ( $self, $userID, $setID, $versionNum ) = @_;
 1934 #
 1935 # getMergedVersionedSet( self, uid, sid [, versionNum] )
 1936 #    in:  userID uid, setID sid, and optionally version number versionNum
 1937 #    out: the merged set version for the user; if versionNum is specified,
 1938 #         return that set version and otherwise the latest version.  if
 1939 #         no versioned set exists for the user, return undef.
 1940 #    note that sid can be setid,vN, thereby specifying the version number
 1941 #      explicitly.  if this is the case, any specified versionNum is ignored
 1942 # we'd like to use getMergedSet to do the dirty work here, but that runs
 1943 #    into problems because we want to merge with both the template set
 1944 #    (that is, the userSet setID) and the global set
 1945 
 1946     croak "getMergedVersionedSet: requires at least two arguments, a userID " .
 1947   "and setID (missing setID)" if ( @_ < 3 || ! defined( $setID ) );
 1948 
 1949     my $versionedSetID = $setID;
 1950 
 1951     if ( ( ! defined($versionNum) || ! $versionNum ) && $setID !~ /,v\d+$/ ) {
 1952   $versionNum = $self->getUserSetVersionNumber( $userID, $setID );
 1953 
 1954   if ( ! $versionNum ) {
 1955       return undef;
 1956   } else {
 1957       $versionedSetID .= ",v$versionNum";
 1958   }
 1959     } elsif ( defined($versionNum) && $versionNum ) {
 1960   $versionedSetID = ($setID =~ /,v\d+$/ ? $setID : "$setID,v$versionNum");
 1961     } else {  # the last case is that $setID =~ /,v\d+$/
 1962   $setID =~ s/,v\d+//;
 1963     }
 1964 
 1965     croak "getMergedVersionedSet: requires at least two arguments, a userID " .
 1966   "and setID (missing userID)" if ( ! defined( $userID ) );
 1967 
 1968     return ( $self->getMergedVersionedSets( [$userID, $setID,
 1969                $versionedSetID] ) )[0];
 1970 }
 1971 
 1972 
 1973 =item getMegedSets(@userSetIDs)
 1974 
 1975 Return a list of merged set records associated with the record IDs given. If
 1976 there is no record associated with a given record ID, that element of the list
 1977 will be undefined. @userSetIDs consists of references to arrays in which the
 1978 first element is the user_id and the second element is the set_id.
 1979 
 1980 =cut
 1981 
 1982 sub getMergedSets {
 1983   my ($self, @userSetIDs) = @_;
 1984 
 1985   #croak "getMergedSets: requires 1 or more argument"
 1986   # unless @_ >= 2;
 1987   foreach my $i (0 .. $#userSetIDs) {
 1988     croak "getMergedSets: element $i of argument list must contain a <user_id, set_id> pair"
 1989       unless defined $userSetIDs[$i]
 1990              and ref $userSetIDs[$i] eq "ARRAY"
 1991              and @{$userSetIDs[$i]} == 2
 1992              and defined $userSetIDs[$i]->[0]
 1993              and defined $userSetIDs[$i]->[1];
 1994   }
 1995 
 1996   # a horrible, terrible hack ;)
 1997   if (ref $self->{set_user} eq "WeBWorK::DB::Schema::WW1Hash"
 1998       and ref $self->{set} eq "WeBWorK::DB::Schema::GlobalTableEmulator") {
 1999     #warn __PACKAGE__.": using a terrible hack.\n";
 2000     $WeBWorK::timer->continue("DB: getsNoFilter start") if defined($WeBWorK::timer);
 2001     my @MergedSets = $self->{set_user}->getsNoFilter(@userSetIDs);
 2002     $WeBWorK::timer->continue("DB: getsNoFilter end") if defined($WeBWorK::timer);
 2003     return @MergedSets;
 2004   }
 2005 
 2006   $WeBWorK::timer->continue("DB: getUserSets start") if defined($WeBWorK::timer);
 2007   my @UserSets = $self->getUserSets(@userSetIDs); # checked
 2008 
 2009   $WeBWorK::timer->continue("DB: pull out set IDs start") if defined($WeBWorK::timer);
 2010   my @globalSetIDs = map { $_->[1] } @userSetIDs;
 2011   $WeBWorK::timer->continue("DB: getGlobalSets start") if defined($WeBWorK::timer);
 2012   my @GlobalSets = $self->getGlobalSets(@globalSetIDs); # checked
 2013 
 2014   $WeBWorK::timer->continue("DB: calc common fields start") if defined($WeBWorK::timer);
 2015   my %globalSetFields = map { $_ => 1 } $self->newGlobalSet->FIELDS;
 2016   my @commonFields = grep { exists $globalSetFields{$_} } $self->newUserSet->FIELDS;
 2017 
 2018   $WeBWorK::timer->continue("DB: merge start") if defined($WeBWorK::timer);
 2019   for (my $i = 0; $i < @UserSets; $i++) {
 2020     my $UserSet = $UserSets[$i];
 2021     my $GlobalSet = $GlobalSets[$i];
 2022     next unless defined $UserSet and defined $GlobalSet;
 2023     foreach my $field (@commonFields) {
 2024       #next if defined $UserSet->$field;
 2025       # ok, now we're testing for emptiness as well as definedness.
 2026       next if defined $UserSet->$field and $UserSet->$field ne "";
 2027       $UserSet->$field($GlobalSet->$field);
 2028     }
 2029   }
 2030   $WeBWorK::timer->continue("DB: merge done!") if defined($WeBWorK::timer);
 2031 
 2032   return @UserSets;
 2033 }
 2034 
 2035 sub getMergedVersionedSets {
 2036     my ($self, @userSetIDs) = @_;
 2037 
 2038     foreach my $i (0 .. $#userSetIDs) {
 2039   croak "getMergedSets: element $i of argument list must contain a " .
 2040       "<user_id, set_id, versioned_set_id> triple"
 2041       unless( defined $userSetIDs[$i]
 2042         and ref $userSetIDs[$i] eq "ARRAY"
 2043         and @{$userSetIDs[$i]} == 3
 2044         and defined $userSetIDs[$i]->[0]
 2045         and defined $userSetIDs[$i]->[1]
 2046         and defined $userSetIDs[$i]->[2] );
 2047     }
 2048 
 2049 # these are [user_id, set_id] pairs
 2050     my @nonversionedUserSetIDs = map { [$_->[0], $_->[1]] } @userSetIDs;
 2051 # these are [user_id, versioned_set_id] pairs
 2052     my @versionedUserSetIDs = map { [$_->[0], $_->[2]] } @userSetIDs;
 2053 
 2054 # FIXME  as long as we're ignoring the global user for gdbm, this is ok...
 2055 # (are we?)  FIXME
 2056   # a horrible, terrible hack ;)
 2057     if (ref $self->{set_user} eq "WeBWorK::DB::Schema::WW1Hash"
 2058   and ref $self->{set} eq "WeBWorK::DB::Schema::GlobalTableEmulator") {
 2059     #warn __PACKAGE__.": using a terrible hack.\n";
 2060   $WeBWorK::timer->continue("DB: getsNoFilter start")
 2061       if defined($WeBWorK::timer);
 2062   my @MergedSets = $self->{set_user}->getsNoFilter(@versionedUserSetIDs);
 2063   $WeBWorK::timer->continue("DB: getsNoFilter end")
 2064       if defined($WeBWorK::timer);
 2065   return @MergedSets;
 2066     }
 2067 
 2068 # we merge the nonversioned ("template") user sets (user_id, set_id) and
 2069 #    the global data into the versioned user sets
 2070     $WeBWorK::timer->continue("DB: getUserSets start (nonversioned)")
 2071   if defined($WeBWorK::timer);
 2072     my @TemplateUserSets = $self->getUserSets(@nonversionedUserSetIDs);
 2073     $WeBWorK::timer->continue("DB: getUserSets start (versioned)")
 2074   if defined($WeBWorK::timer);
 2075 # these are the actual user sets that we want to use
 2076     my @versionedUserSets = $self->getUserSets(@versionedUserSetIDs);
 2077 
 2078     $WeBWorK::timer->continue("DB: pull out set IDs start")
 2079   if defined($WeBWorK::timer);
 2080     my @globalSetIDs = map { $_->[1] } @userSetIDs;
 2081     $WeBWorK::timer->continue("DB: getGlobalSets start")
 2082   if defined($WeBWorK::timer);
 2083     my @GlobalSets = $self->getGlobalSets(@globalSetIDs);
 2084 
 2085     $WeBWorK::timer->continue("DB: calc common fields start")
 2086   if defined($WeBWorK::timer);
 2087     my %globalSetFields = map { $_ => 1 } $self->newGlobalSet->FIELDS;
 2088     my @commonFields =
 2089   grep { exists $globalSetFields{$_} } $self->newUserSet->FIELDS;
 2090 
 2091     $WeBWorK::timer->continue("DB: merge start") if defined($WeBWorK::timer);
 2092     for (my $i = 0; $i < @TemplateUserSets; $i++) {
 2093   my $VersionedSet = $versionedUserSets[$i];
 2094   my $TemplateSet = $TemplateUserSets[$i];
 2095   my $GlobalSet = $GlobalSets[$i];
 2096     # shouldn't all of these necessarily be defined?  Hmm.
 2097   next unless( defined $VersionedSet and (defined $TemplateSet or
 2098             defined $GlobalSet) );
 2099   foreach my $field (@commonFields) {
 2100       next if defined $VersionedSet->$field;
 2101       $VersionedSet->$field($GlobalSet->$field) if (defined($GlobalSet));
 2102       $VersionedSet->$field($TemplateSet->$field)
 2103     if (defined($TemplateSet) && defined($TemplateSet->$field));
 2104   }
 2105     }
 2106     $WeBWorK::timer->continue("DB: merge done!") if defined($WeBWorK::timer);
 2107 
 2108     return @versionedUserSets;
 2109 }
 2110 
 2111 =back
 2112 
 2113 =cut
 2114 
 2115 ################################################################################
 2116 # problem+problem_user functions
 2117 ################################################################################
 2118 
 2119 =head2 Problem Merging Methods
 2120 
 2121 These functions combine a global problem and a user problem to create a merged
 2122 problem, which is returned. Any field that is not defined in the user problem is
 2123 taken from the global problem. Merged problems have the same type as user
 2124 problems.
 2125 
 2126 =over
 2127 
 2128 =cut
 2129 
 2130 sub getGlobalUserProblem {
 2131   carp "getGlobalUserProblem: this method is deprecated -- use getMergedProblem instead";
 2132   return shift->getMergedProblem(@_);
 2133 }
 2134 
 2135 =item getMergedProblem($userID, $setID, $problemID)
 2136 
 2137 Returns a merged problem record associated with the record IDs given. If there
 2138 is no record associated with a given record ID, the undefined value is returned.
 2139 
 2140 =cut
 2141 
 2142 sub getMergedProblem {
 2143   my ($self, $userID, $setID, $problemID) = @_;
 2144 
 2145   croak "getGlobalUserSet: requires 3 arguments"
 2146     unless @_ == 4;
 2147   croak "getGlobalUserSet: argument 1 must contain a user_id"
 2148     unless defined $userID;
 2149   croak "getGlobalUserSet: argument 2 must contain a set_id"
 2150     unless defined $setID;
 2151   croak "getGlobalUserSet: argument 3 must contain a problem_id"
 2152     unless defined $problemID;
 2153 
 2154   return ( $self->getMergedProblems([$userID, $setID, $problemID]) )[0];
 2155 }
 2156 
 2157 sub getMergedVersionedProblem {
 2158     my ($self, $userID, $setID, $setVersionID, $problemID) = @_;
 2159 
 2160 # this exists distinct from getMergedProblem only to be able to include the
 2161 #    setVersionID
 2162 
 2163     croak "getGlobalUserSet: requires 4 arguments"
 2164   unless @_ == 5;
 2165     croak "getGlobalUserSet: argument 1 must contain a user_id"
 2166   unless defined $userID;
 2167     croak "getGlobalUserSet: argument 2 must contain a set_id"
 2168   unless defined $setID;
 2169     croak "getGlobalUserSet: argument 3 must contain a set_id"
 2170   unless defined $setVersionID;
 2171     croak "getGlobalUserSet: argument 4 must contain a problem_id"
 2172   unless defined $problemID;
 2173 
 2174     return ($self->getMergedVersionedProblems([$userID, $setID, $setVersionID,
 2175                  $problemID]))[0];
 2176 }
 2177 
 2178 =item getMergedProblems(@userProblemIDs)
 2179 
 2180 Return a list of merged problem records associated with the record IDs given. If
 2181 there is no record associated with a given record ID, that element of the list
 2182 will be undefined. @userProblemIDs consists of references to arrays in which the
 2183 first element is the user_id, the second element is the set_id, and the third
 2184 element is the problem_id.
 2185 
 2186 =cut
 2187 
 2188 sub getMergedProblems {
 2189   my ($self, @userProblemIDs) = @_;
 2190 
 2191   #croak "getMergedProblems: requires 1 or more argument"
 2192   # unless @_ >= 2;
 2193   foreach my $i (0 .. $#userProblemIDs) {
 2194     croak "getMergedProblems: element $i of argument list must contain a <user_id, set_id, problem_id> triple"
 2195       unless defined $userProblemIDs[$i]
 2196              and ref $userProblemIDs[$i] eq "ARRAY"
 2197              and @{$userProblemIDs[$i]} == 3
 2198              and defined $userProblemIDs[$i]->[0]
 2199              and defined $userProblemIDs[$i]->[1]
 2200              and defined $userProblemIDs[$i]->[2];
 2201   }
 2202 
 2203   $WeBWorK::timer->continue("DB: getUserProblems start") if defined($WeBWorK::timer);
 2204   my @UserProblems = $self->getUserProblems(@userProblemIDs); # checked
 2205 
 2206   $WeBWorK::timer->continue("DB: pull out set/problem IDs start") if defined($WeBWorK::timer);
 2207   my @globalProblemIDs = map { [ $_->[1], $_->[2] ] } @userProblemIDs;
 2208   $WeBWorK::timer->continue("DB: getGlobalProblems start") if defined($WeBWorK::timer);
 2209   my @GlobalProblems = $self->getGlobalProblems(@globalProblemIDs); # checked
 2210 
 2211   $WeBWorK::timer->continue("DB: calc common fields start") if defined($WeBWorK::timer);
 2212   my %globalProblemFields = map { $_ => 1 } $self->newGlobalProblem->FIELDS;
 2213   my @commonFields = grep { exists $globalProblemFields{$_} } $self->newUserProblem->FIELDS;
 2214 
 2215   $WeBWorK::timer->continue("DB: merge start") if defined($WeBWorK::timer);
 2216   for (my $i = 0; $i < @UserProblems; $i++) {
 2217     my $UserProblem = $UserProblems[$i];
 2218     my $GlobalProblem = $GlobalProblems[$i];
 2219     next unless defined $UserProblem and defined $GlobalProblem;
 2220     foreach my $field (@commonFields) {
 2221       # FIXME: WW1Hash upgrades undefined fileds to "" when creating record objects
 2222       # Shouldn't we be testing for emptiness rather than definedness?
 2223       # I think the spec says that if a field is EMPTY the global value is used.
 2224       #next if defined $UserProblem->$field;
 2225       # ok, now we're testing for emptiness as well as definedness.
 2226       next if defined $UserProblem->$field and $UserProblem->$field ne "";
 2227       $UserProblem->$field($GlobalProblem->$field);
 2228     }
 2229   }
 2230   $WeBWorK::timer->continue("DB: merge done!") if defined($WeBWorK::timer);
 2231 
 2232   return @UserProblems;
 2233 }
 2234 
 2235 sub getMergedVersionedProblems {
 2236     my ($self, @userProblemIDs) = @_;
 2237 
 2238     foreach my $i (0 .. $#userProblemIDs) {
 2239   croak "getMergedProblems: element $i of argument list must contain a " .
 2240       "<user_id, set_id, versioned_set_id, problem_id> quadruple"
 2241       unless( defined $userProblemIDs[$i]
 2242         and ref $userProblemIDs[$i] eq "ARRAY"
 2243         and @{$userProblemIDs[$i]} == 4
 2244         and defined $userProblemIDs[$i]->[0]
 2245         and defined $userProblemIDs[$i]->[1]
 2246         and defined $userProblemIDs[$i]->[2]
 2247         and defined $userProblemIDs[$i]->[3] );
 2248     }
 2249 
 2250     $WeBWorK::timer->continue("DB: getUserProblems start")
 2251   if defined($WeBWorK::timer);
 2252 
 2253 # these are triples [user_id, set_id, problem_id]
 2254     my @nonversionedProblemIDs = map {[$_->[0],$_->[1],$_->[3]]} @userProblemIDs;
 2255 # these are triples [user_id, versioned_set_id, problem_id]
 2256     my @versionedProblemIDs = map {[$_->[0],$_->[2],$_->[3]]} @userProblemIDs;
 2257 
 2258 # these are the actual user problems for the version
 2259     my @versionUserProblems = $self->getUserProblems(@versionedProblemIDs);
 2260 
 2261 # get global problems (no user_id, set_id = nonversioned set_id) and
 2262 #    template problems (user_id, set_id = nonversioned set_id); we merge with
 2263 #    both of these, replacing global values with template values and not
 2264 #    taking either in the event that the versioned problem already has a
 2265 #    value for the field in question
 2266     $WeBWorK::timer->continue("DB: pull out set/problem IDs start")
 2267   if defined($WeBWorK::timer);
 2268     my @globalProblemIDs = map { [ $_->[1], $_->[2] ] } @nonversionedProblemIDs;
 2269     $WeBWorK::timer->continue("DB: getGlobalProblems start")
 2270   if defined($WeBWorK::timer);
 2271     my @GlobalProblems = $self->getGlobalProblems( @globalProblemIDs );
 2272     $WeBWorK::timer->continue("DB: getTemplateProblems start")
 2273   if defined($WeBWorK::timer);
 2274     my @TemplateProblems = $self->getUserProblems( @nonversionedProblemIDs );
 2275 
 2276     $WeBWorK::timer->continue("DB: calc common fields start")
 2277   if defined($WeBWorK::timer);
 2278 
 2279     my %globalProblemFields = map { $_ => 1 } $self->newGlobalProblem->FIELDS;
 2280     my @commonFields =
 2281   grep { exists $globalProblemFields{$_} } $self->newUserProblem->FIELDS;
 2282 
 2283     $WeBWorK::timer->continue("DB: merge start") if defined($WeBWorK::timer);
 2284     for (my $i = 0; $i < @versionUserProblems; $i++) {
 2285   my $UserProblem = $versionUserProblems[$i];
 2286   my $GlobalProblem = $GlobalProblems[$i];
 2287   my $TemplateProblem = $TemplateProblems[$i];
 2288   next unless defined $UserProblem and ( defined $GlobalProblem or
 2289                  defined $TemplateProblem );
 2290   foreach my $field (@commonFields) {
 2291       next if defined $UserProblem->$field;
 2292       $UserProblem->$field($GlobalProblem->$field)
 2293     if ( defined($GlobalProblem) && defined($GlobalProblem->$field)
 2294          && $GlobalProblem->$field ne '' );
 2295       $UserProblem->$field($TemplateProblem->$field)
 2296     if ( defined($TemplateProblem) &&
 2297          defined($TemplateProblem->$field) &&
 2298          $TemplateProblem->$field ne '' );
 2299   }
 2300     }
 2301     $WeBWorK::timer->continue("DB: merge done!") if defined($WeBWorK::timer);
 2302 
 2303     return @versionUserProblems;
 2304 }
 2305 
 2306 =back
 2307 
 2308 =cut
 2309 
 2310 ################################################################################
 2311 # debugging
 2312 ################################################################################
 2313 
 2314 #sub dumpDB($$) {
 2315 # my ($self, $table) = @_;
 2316 # return $self->{$table}->dumpDB();
 2317 #}
 2318 
 2319 ################################################################################
 2320 # utilities
 2321 ################################################################################
 2322 
 2323 sub checkKeyfields($;$) {
 2324   my ($Record, $versioned) = @_;
 2325   foreach my $keyfield ($Record->KEYFIELDS) {
 2326     my $value = $Record->$keyfield;
 2327     croak "checkKeyfields: $keyfield is empty"
 2328       unless defined $value and $value ne "";
 2329 
 2330     if ($keyfield eq "problem_id") {
 2331       croak "checkKeyfields: invalid characters in $keyfield field: $value (valid characters are [0-9])"
 2332         unless $value =~ m/^\d*$/;
 2333     } else {
 2334       croak "checkKeyfields: invalid characters in $keyfield field: $value (valid characters are [A-Za-z0-9_])"
 2335         # this logic is a bit ugly, but it enforces what we want,
 2336         #   which is that only versioned problem sets are allowed
 2337         #   to include commas in their names.  or, to allow for
 2338                     #   proctor keys, user_ids can have commas too
 2339         unless ( $value =~ m/^[\w-]*$/ ||
 2340            ( $value =~ m/^[\w,-]*$/ &&
 2341              (defined($versioned) && $versioned)
 2342              &&
 2343              ($keyfield eq "set_id" ||
 2344               $keyfield eq "user_id") ) );
 2345     }
 2346   }
 2347 }
 2348 
 2349 =head1 AUTHOR
 2350 
 2351 Written by Sam Hathaway, sh002i (at) math.rochester.edu.
 2352 
 2353 =cut
 2354 
 2355 1;

aubreyja at gmail dot com
ViewVC Help
Powered by ViewVC 1.0.9