[system] / trunk / webwork-modperl / lib / WeBWorK / DB.pm Repository:
ViewVC logotype

View of /trunk/webwork-modperl/lib/WeBWorK/DB.pm

Parent Directory Parent Directory | Revision Log Revision Log


Revision 2348 - (download) (as text) (annotate)
Thu Jun 17 20:11:17 2004 UTC (9 years ago) by sh002i
File size: 57967 byte(s)
optimized the hell out of the database checking code

    1 ################################################################################
    2 # WeBWorK Online Homework Delivery System
    3 # Copyright © 2000-2003 The WeBWorK Project, http://openwebwork.sf.net/
    4 # $CVSHeader: webwork-modperl/lib/WeBWorK/DB.pm,v 1.51 2004/06/16 18:26:59 toenail 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 =cut
  825 
  826 sub addKey($$) {
  827   my ($self, $Key) = @_;
  828 
  829   croak "addKey: requires 1 argument"
  830     unless @_ == 2;
  831   croak "addKey: argument 1 must be of type ", $self->{key}->{record}
  832     unless ref $Key eq $self->{key}->{record};
  833 
  834   checkKeyfields($Key);
  835 
  836   croak "addKey: key exists (perhaps you meant to use putKey?)"
  837     if $self->{key}->exists($Key->user_id);
  838   croak "addKey: user ", $Key->user_id, " not found"
  839     unless $self->{user}->exists($Key->user_id);
  840 
  841   return $self->{key}->add($Key);
  842 }
  843 
  844 =item getKey($userID)
  845 
  846 If a record with a matching user ID exists, a record object containting that
  847 record's data will be returned. If no such record exists, an undefined value
  848 will be returned.
  849 
  850 =cut
  851 
  852 sub getKey($$) {
  853   my ($self, $userID) = @_;
  854 
  855   croak "getKey: requires 1 argument"
  856     unless @_ == 2;
  857   croak "getKey: argument 1 must contain a user_id"
  858     unless defined $userID;
  859 
  860   return $self->{key}->get($userID);
  861 }
  862 
  863 =item getKeys(@uesrIDs)
  864 
  865 Return a list of key records associated with the user IDs given. If there is no
  866 record associated with a given user ID, that element of the list will be
  867 undefined.
  868 
  869 =cut
  870 
  871 sub getKeys {
  872   my ($self, @userIDs) = @_;
  873 
  874   #croak "getKeys: requires 1 or more argument"
  875   # unless @_ >= 2;
  876   foreach my $i (0 .. $#userIDs) {
  877     croak "getKeys: element $i of argument list must contain a user_id"
  878       unless defined $userIDs[$i];
  879   }
  880 
  881   return $self->{key}->gets(map { [$_] } @userIDs);
  882 }
  883 
  884 =item putKey($Key)
  885 
  886 $Key is a record object. If a key record with the same user ID exists in the
  887 key table, the data in the record is replaced with the data in $Key. If a
  888 matching key record does not exist, an exception is thrown.
  889 
  890 =cut
  891 
  892 sub putKey($$) {
  893   my ($self, $Key) = @_;
  894 
  895   croak "putKey: requires 1 argument"
  896     unless @_ == 2;
  897   croak "putKey: argument 1 must be of type ", $self->{key}->{record}
  898     unless ref $Key eq $self->{key}->{record};
  899 
  900   checkKeyfields($Key);
  901 
  902   croak "putKey: key not found (perhaps you meant to use addKey?)"
  903     unless $self->{key}->exists($Key->user_id);
  904 
  905   return $self->{key}->put($Key);
  906 }
  907 
  908 =item deleteKey($userID)
  909 
  910 If a key record with a user ID matching $userID exists in the key table, it is
  911 removed and the method returns a true value. If one does exist, a false value
  912 is returned.
  913 
  914 =cut
  915 
  916 sub deleteKey($$) {
  917   my ($self, $userID) = @_;
  918 
  919   croak "deleteKey: requires 1 argument"
  920     unless @_ == 2;
  921   croak "deleteKey: argument 1 must contain a user_id"
  922     unless defined $userID;
  923 
  924   return $self->{key}->delete($userID);
  925 }
  926 
  927 ################################################################################
  928 # user functions
  929 ################################################################################
  930 
  931 =head2 User Methods
  932 
  933 =over
  934 
  935 =item newUser()
  936 
  937 Returns a new, empty user object.
  938 
  939 =cut
  940 
  941 sub newUser {
  942   my ($self, @prototype) = @_;
  943   return $self->{user}->{record}->new(@prototype);
  944 }
  945 
  946 =item listUsers()
  947 
  948 Returns a list of user IDs representing the records in the user table.
  949 
  950 =cut
  951 
  952 sub listUsers {
  953   my ($self) = @_;
  954 
  955   croak "listUsers: requires 0 arguments"
  956     unless @_ == 1;
  957 
  958   return map { $_->[0] }
  959     $self->{user}->list(undef);
  960 }
  961 
  962 =item addUser($User)
  963 
  964 $User is a record object. The user will be added to the user table if a user
  965 with the same user ID does not already exist. If one does exist, an exception
  966 is thrown.
  967 
  968 =cut
  969 
  970 sub addUser {
  971   my ($self, $User) = @_;
  972 
  973   croak "addUser: requires 1 argument"
  974     unless @_ == 2;
  975   croak "addUser: argument 1 must be of type ", $self->{user}->{record}
  976     unless ref $User eq $self->{user}->{record};
  977 
  978   checkKeyfields($User);
  979 
  980   croak "addUser: user exists (perhaps you meant to use putUser?)"
  981     if $self->{user}->exists($User->user_id);
  982 
  983   return $self->{user}->add($User);
  984 }
  985 
  986 =item getUser($userID)
  987 
  988 If a record with a matching user ID exists, a record object containting that
  989 record's data will be returned. If no such record exists, an undefined value
  990 will be returned.
  991 
  992 =cut
  993 
  994 sub getUser {
  995   my ($self, $userID) = @_;
  996 
  997   croak "getUser: requires 1 argument"
  998     unless @_ == 2;
  999   croak "getUser: argument 1 must contain a user_id"
 1000     unless defined $userID;
 1001 
 1002   return $self->{user}->get($userID);
 1003 }
 1004 
 1005 =item getUsers(@uesrIDs)
 1006 
 1007 Return a list of user records associated with the user IDs given. If there is no
 1008 record associated with a given user ID, that element of the list will be
 1009 undefined.
 1010 
 1011 =cut
 1012 
 1013 sub getUsers {
 1014   my ($self, @userIDs) = @_;
 1015 
 1016   #croak "getUsers: requires 1 or more argument"
 1017   # unless @_ >= 2;
 1018   foreach my $i (0 .. $#userIDs) {
 1019     croak "getUsers: element $i of argument list must contain a user_id"
 1020       unless defined $userIDs[$i];
 1021   }
 1022 
 1023   return $self->{user}->gets(map { [$_] } @userIDs);
 1024 }
 1025 
 1026 =item putUser($User)
 1027 
 1028 $User is a record object. If a user record with the same user ID exists in the
 1029 user table, the data in the record is replaced with the data in $User. If a
 1030 matching user record does not exist, an exception is thrown.
 1031 
 1032 =cut
 1033 
 1034 sub putUser {
 1035   my ($self, $User) = @_;
 1036 
 1037   croak "putUser: requires 1 argument"
 1038     unless @_ == 2;
 1039   croak "putUser: argument 1 must be of type ", $self->{user}->{record}
 1040     unless ref $User eq $self->{user}->{record};
 1041 
 1042   checkKeyfields($User);
 1043 
 1044   croak "putUser: user not found (perhaps you meant to use addUser?)"
 1045     unless $self->{user}->exists($User->user_id);
 1046 
 1047   return $self->{user}->put($User);
 1048 }
 1049 
 1050 =item deleteUser($userID)
 1051 
 1052 If a user record with a user ID matching $userID exists in the user table, it
 1053 is removed and the method returns a true value. If one does exist, a false
 1054 value is returned. When a user record is deleted, all records associated with
 1055 that user are also deleted. This includes the password, permission, and key
 1056 records, and all user set records for that user.
 1057 
 1058 =cut
 1059 
 1060 sub deleteUser {
 1061   my ($self, $userID) = @_;
 1062 
 1063   croak "deleteUser: requires 1 argument"
 1064     unless @_ == 2;
 1065   croak "deleteUser: argument 1 must contain a user_id"
 1066     unless defined $userID;
 1067 
 1068   $self->deleteUserSet($userID, undef);
 1069   $self->deletePassword($userID);
 1070   $self->deletePermissionLevel($userID);
 1071   $self->deleteKey($userID);
 1072   return $self->{user}->delete($userID);
 1073 }
 1074 
 1075 =back
 1076 
 1077 =cut
 1078 
 1079 ################################################################################
 1080 # set functions
 1081 ################################################################################
 1082 
 1083 =head2 Global Set Methods
 1084 
 1085 FIXME: write this
 1086 
 1087 =over
 1088 
 1089 =cut
 1090 
 1091 =item newGlobalSet()
 1092 
 1093 =cut
 1094 
 1095 sub newGlobalSet {
 1096   my ($self, @prototype) = @_;
 1097   return $self->{set}->{record}->new(@prototype);
 1098 }
 1099 
 1100 =item listGlobalSets()
 1101 
 1102 =cut
 1103 
 1104 sub listGlobalSets {
 1105   my ($self) = @_;
 1106 
 1107   croak "listGlobalSets: requires 0 arguments"
 1108     unless @_ == 1;
 1109 
 1110   return map { $_->[0] }
 1111     $self->{set}->list(undef);
 1112 }
 1113 
 1114 =item addGlobalSet($GlobalSet)
 1115 
 1116 =cut
 1117 
 1118 sub addGlobalSet {
 1119   my ($self, $GlobalSet) = @_;
 1120 
 1121   croak "addGlobalSet: requires 1 argument"
 1122     unless @_ == 2;
 1123   croak "addGlobalSet: argument 1 must be of type ", $self->{set}->{record}
 1124     unless ref $GlobalSet eq $self->{set}->{record};
 1125 
 1126   checkKeyfields($GlobalSet);
 1127 
 1128   croak "addGlobalSet: global set exists (perhaps you meant to use putGlobalSet?)"
 1129     if $self->{set}->exists($GlobalSet->set_id);
 1130 
 1131   return $self->{set}->add($GlobalSet);
 1132 }
 1133 
 1134 =item addGlobalSet($setID)
 1135 
 1136 =cut
 1137 
 1138 sub getGlobalSet {
 1139   my ($self, $setID) = @_;
 1140 
 1141   croak "getGlobalSet: requires 1 argument"
 1142     unless @_ == 2;
 1143   croak "getGlobalSet: argument 1 must contain a set_id"
 1144     unless defined $setID;
 1145 
 1146   return $self->{set}->get($setID);
 1147 }
 1148 
 1149 =item getGlobalSets(@setIDs)
 1150 
 1151 Return a list of global set records associated with the record IDs given. If
 1152 there is no record associated with a given record ID, that element of the list
 1153 will be undefined.
 1154 
 1155 =cut
 1156 
 1157 sub getGlobalSets {
 1158   my ($self, @setIDs) = @_;
 1159 
 1160   #croak "getGlobalSets: requires 1 or more argument"
 1161   # unless @_ >= 2;
 1162   foreach my $i (0 .. $#setIDs) {
 1163     croak "getGlobalSets: element $i of argument list must contain a set_id"
 1164       unless defined $setIDs[$i];
 1165   }
 1166 
 1167   return $self->{set}->gets(map { [$_] } @setIDs);
 1168 }
 1169 
 1170 =item addGlobalSet($GlobalSet)
 1171 
 1172 =cut
 1173 
 1174 sub putGlobalSet {
 1175   my ($self, $GlobalSet) = @_;
 1176 
 1177   croak "putGlobalSet: requires 1 argument"
 1178     unless @_ == 2;
 1179   croak "putGlobalSet: argument 1 must be of type ", $self->{set}->{record}
 1180     unless ref $GlobalSet eq $self->{set}->{record};
 1181 
 1182   checkKeyfields($GlobalSet);
 1183 
 1184   croak "putGlobalSet: global set not found (perhaps you meant to use addGlobalSet?)"
 1185     unless $self->{set}->exists($GlobalSet->set_id);
 1186 
 1187   return $self->{set}->put($GlobalSet);
 1188 }
 1189 
 1190 =item addGlobalSet($setID)
 1191 
 1192 =cut
 1193 
 1194 sub deleteGlobalSet {
 1195   my ($self, $setID) = @_;
 1196 
 1197   croak "deleteGlobalSet: requires 1 argument"
 1198     unless @_ == 2;
 1199   croak "deleteGlobalSet: argument 1 must contain a set_id"
 1200     unless defined $setID or caller eq __PACKAGE__;
 1201 
 1202   $self->deleteUserSet(undef, $setID);
 1203   $self->deleteGlobalProblem($setID, undef);
 1204   return $self->{set}->delete($setID);
 1205 }
 1206 
 1207 =back
 1208 
 1209 =cut
 1210 
 1211 ################################################################################
 1212 # set_user functions
 1213 ################################################################################
 1214 
 1215 =head2 User-Specific Set Methods
 1216 
 1217 FIXME: write this
 1218 
 1219 =over
 1220 
 1221 =cut
 1222 
 1223 sub newUserSet {
 1224   my ($self, @prototype) = @_;
 1225   return $self->{set_user}->{record}->new(@prototype);
 1226 }
 1227 
 1228 sub countSetUsers {
 1229   my ($self, $setID) = @_;
 1230 
 1231   croak "countSetUsers: requires 1 argument"
 1232     unless @_ == 2;
 1233   croak "countSetUsers: argument 1 must contain a set_id"
 1234     unless defined $setID;
 1235 
 1236   # inefficient way
 1237   #return scalar $self->{set_user}->list(undef, $setID);
 1238 
 1239   # efficient way
 1240   return $self->{set_user}->count(undef, $setID);
 1241 }
 1242 
 1243 sub listSetUsers {
 1244   my ($self, $setID) = @_;
 1245 
 1246   carp "listSetUsers called in SCALAR context: use countSetUsers instead!\n"
 1247     unless wantarray;
 1248 
 1249   croak "listSetUsers: requires 1 argument"
 1250     unless @_ == 2;
 1251   croak "listSetUsers: argument 1 must contain a set_id"
 1252     unless defined $setID;
 1253 
 1254   return map { $_->[0] } # extract user_id
 1255     $self->{set_user}->list(undef, $setID);
 1256 }
 1257 
 1258 sub countUserSets {
 1259   my ($self, $userID) = @_;
 1260 
 1261   croak "countUserSets: requires 1 argument"
 1262     unless @_ == 2;
 1263   croak "countUserSets: argument 1 must contain a user_id"
 1264     unless defined $userID;
 1265 
 1266   return $self->{set_user}->count($userID, undef);
 1267 }
 1268 
 1269 sub listUserSets {
 1270   my ($self, $userID) = @_;
 1271 
 1272   croak "listUserSets: requires 1 argument"
 1273     unless @_ == 2;
 1274   croak "listUserSets: argument 1 must contain a user_id"
 1275     unless defined $userID;
 1276 
 1277   return map { $_->[1] } # extract set_id
 1278     $self->{set_user}->list($userID, undef);
 1279 }
 1280 
 1281 sub addUserSet {
 1282   my ($self, $UserSet) = @_;
 1283 
 1284   croak "addUserSet: requires 1 argument"
 1285     unless @_ == 2;
 1286   croak "addUserSet: argument 1 must be of type ", $self->{set_user}->{record}
 1287     unless ref $UserSet eq $self->{set_user}->{record};
 1288 
 1289   checkKeyfields($UserSet);
 1290 
 1291   croak "addUserSet: user set exists (perhaps you meant to use putUserSet?)"
 1292     if $self->{set_user}->exists($UserSet->user_id, $UserSet->set_id);
 1293   croak "addUserSet: user ", $UserSet->user_id, " not found"
 1294     unless $self->{user}->exists($UserSet->user_id);
 1295   croak "addUserSet: set ", $UserSet->set_id, " not found"
 1296     unless $self->{set}->exists($UserSet->set_id);
 1297 
 1298   return $self->{set_user}->add($UserSet);
 1299 }
 1300 
 1301 sub getUserSet {
 1302   my ($self, $userID, $setID) = @_;
 1303 
 1304   croak "getUserSet: requires 2 arguments"
 1305     unless @_ == 3;
 1306   croak "getUserSet: argument 1 must contain a user_id"
 1307     unless defined $userID;
 1308   croak "getUserSet: argument 2 must contain a set_id"
 1309     unless defined $setID;
 1310 
 1311   #return $self->{set_user}->get($userID, $setID);
 1312   return ( $self->getUserSets([$userID, $setID]) )[0];
 1313 }
 1314 
 1315 =item getUserSets(@userSetIDs)
 1316 
 1317 Return a list of user set records associated with the record IDs given. If there
 1318 is no record associated with a given record ID, that element of the list will be
 1319 undefined. @userProblemIDs consists of references to arrays in which the first
 1320 element is the user_id and the second element is the set_id.
 1321 
 1322 =cut
 1323 
 1324 sub getUserSets {
 1325   my ($self, @userSetIDs) = @_;
 1326 
 1327   #croak "getUserSets: requires 1 or more argument"
 1328   # unless @_ >= 2;
 1329   foreach my $i (0 .. $#userSetIDs) {
 1330     croak "getUserSets: element $i of argument list must contain a <user_id, set_id> pair"
 1331       unless defined $userSetIDs[$i]
 1332              and ref $userSetIDs[$i] eq "ARRAY"
 1333              and @{$userSetIDs[$i]} == 2
 1334              and defined $userSetIDs[$i]->[0]
 1335              and defined $userSetIDs[$i]->[1];
 1336   }
 1337 
 1338   return $self->{set_user}->gets(@userSetIDs);
 1339 }
 1340 
 1341 sub putUserSet {
 1342   my ($self, $UserSet) = @_;
 1343 
 1344   croak "putUserSet: requires 1 argument"
 1345     unless @_ == 2;
 1346   croak "putUserSet: argument 1 must be of type ", $self->{set_user}->{record}
 1347     unless ref $UserSet eq $self->{set_user}->{record};
 1348 
 1349   checkKeyfields($UserSet);
 1350 
 1351   croak "putUserSet: user set not found (perhaps you meant to use addUserSet?)"
 1352     unless $self->{set_user}->exists($UserSet->user_id, $UserSet->set_id);
 1353   croak "putUserSet: user ", $UserSet->user_id, " not found"
 1354     unless $self->{user}->exists($UserSet->user_id);
 1355   croak "putUserSet: set ", $UserSet->set_id, " not found"
 1356     unless $self->{set}->exists($UserSet->set_id);
 1357 
 1358   return $self->{set_user}->put($UserSet);
 1359 }
 1360 
 1361 sub deleteUserSet {
 1362   my ($self, $userID, $setID) = @_;
 1363 
 1364   croak "getUserSet: requires 2 arguments"
 1365     unless @_ == 3;
 1366   croak "getUserSet: argument 1 must contain a user_id"
 1367     unless defined $userID or caller eq __PACKAGE__;
 1368   croak "getUserSet: argument 2 must contain a set_id"
 1369     unless defined $userID or caller eq __PACKAGE__;
 1370 
 1371   $self->deleteUserProblem($userID, $setID, undef);
 1372   return $self->{set_user}->delete($userID, $setID);
 1373 }
 1374 
 1375 =back
 1376 
 1377 =cut
 1378 
 1379 ################################################################################
 1380 # problem functions
 1381 ################################################################################
 1382 
 1383 =head2 Global Problem Methods
 1384 
 1385 FIXME: write this
 1386 
 1387 =over
 1388 
 1389 =cut
 1390 
 1391 sub newGlobalProblem {
 1392   my ($self, @prototype) = @_;
 1393   return $self->{problem}->{record}->new(@prototype);
 1394 }
 1395 
 1396 sub listGlobalProblems {
 1397   my ($self, $setID) = @_;
 1398 
 1399   croak "listGlobalProblems: requires 1 arguments"
 1400     unless @_ == 2;
 1401   croak "listGlobalProblems: argument 1 must contain a set_id"
 1402     unless defined $setID;
 1403 
 1404   return map { $_->[1] }
 1405     $self->{problem}->list($setID, undef);
 1406 }
 1407 
 1408 sub addGlobalProblem {
 1409   my ($self, $GlobalProblem) = @_;
 1410 
 1411   croak "addGlobalProblem: requires 1 argument"
 1412     unless @_ == 2;
 1413   croak "addGlobalProblem: argument 1 must be of type ", $self->{problem}->{record}
 1414     unless ref $GlobalProblem eq $self->{problem}->{record};
 1415 
 1416   checkKeyfields($GlobalProblem);
 1417 
 1418   croak "addGlobalProblem: global problem exists (perhaps you meant to use putGlobalProblem?)"
 1419     if $self->{problem}->exists($GlobalProblem->set_id, $GlobalProblem->problem_id);
 1420   croak "addGlobalProblem: set ", $GlobalProblem->set_id, " not found"
 1421     unless $self->{set}->exists($GlobalProblem->set_id);
 1422 
 1423   return $self->{problem}->add($GlobalProblem);
 1424 }
 1425 
 1426 sub getGlobalProblem {
 1427   my ($self, $setID, $problemID) = @_;
 1428 
 1429   croak "getGlobalProblem: requires 2 arguments"
 1430     unless @_ == 3;
 1431   croak "getGlobalProblem: argument 1 must contain a set_id"
 1432     unless defined $setID;
 1433   croak "getGlobalProblem: argument 2 must contain a problem_id"
 1434     unless defined $problemID;
 1435 
 1436   return $self->{problem}->get($setID, $problemID);
 1437 }
 1438 
 1439 =item getGlobalProblems(@problemIDs)
 1440 
 1441 Return a list of global set records associated with the record IDs given. If
 1442 there is no record associated with a given record ID, that element of the list
 1443 will be undefined. @problemIDs consists of references to arrays in which the
 1444 first element is the set_id, and the second element is the problem_id.
 1445 
 1446 =cut
 1447 
 1448 sub getGlobalProblems {
 1449   my ($self, @problemIDs) = @_;
 1450 
 1451   #croak "getGlobalProblems: requires 1 or more argument"
 1452   # unless @_ >= 2;
 1453   foreach my $i (0 .. $#problemIDs) {
 1454     croak "getUserSets: element $i of argument list must contain a <set_id, problem_id> pair"
 1455       unless defined $problemIDs[$i]
 1456              and ref $problemIDs[$i] eq "ARRAY"
 1457              and @{$problemIDs[$i]} == 2
 1458              and defined $problemIDs[$i]->[0]
 1459              and defined $problemIDs[$i]->[1];
 1460   }
 1461 
 1462   return $self->{problem}->gets(@problemIDs);
 1463 }
 1464 
 1465 =item getAllGlobalProblems($setID)
 1466 
 1467 Returns a list of Problem objects representing all the problems in the given
 1468 global set. When using the WW1Hash/GlobalTableEmulator schemas, this is far
 1469 more efficient than using listGlobalProblems and getGlobalProblems.
 1470 
 1471 =cut
 1472 
 1473 sub getAllGlobalProblems {
 1474   my ($self, $setID) = @_;
 1475 
 1476   croak "getAllGlobalProblems: requires 1 arguments"
 1477     unless @_ == 2;
 1478   croak "getAllGlobalProblems: argument 1 must contain a set_id"
 1479     unless defined $setID;
 1480 
 1481   if ($self->{problem}->can("getAll")) {
 1482     return $self->{problem}->getAll($setID);
 1483   } else {
 1484     my @problemIDPairs = $self->{problem}->list($setID, undef);
 1485     return $self->{problem}->gets(@problemIDPairs);
 1486   }
 1487 }
 1488 
 1489 sub putGlobalProblem {
 1490   my ($self, $GlobalProblem) = @_;
 1491 
 1492   croak "putGlobalProblem: requires 1 argument"
 1493     unless @_ == 2;
 1494   croak "putGlobalProblem: argument 1 must be of type ", $self->{problem}->{record}
 1495     unless ref $GlobalProblem eq $self->{problem}->{record};
 1496 
 1497   checkKeyfields($GlobalProblem);
 1498 
 1499   croak "putGlobalProblem: global problem not found (perhaps you meant to use addGlobalProblem?)"
 1500     unless $self->{problem}->exists($GlobalProblem->set_id, $GlobalProblem->problem_id);
 1501   croak "putGlobalProblem: set ", $GlobalProblem->set_id, " not found"
 1502     unless $self->{set}->exists($GlobalProblem->set_id);
 1503 
 1504   return $self->{problem}->put($GlobalProblem);
 1505 }
 1506 
 1507 sub deleteGlobalProblem {
 1508   my ($self, $setID, $problemID) = @_;
 1509 
 1510   croak "deleteGlobalProblem: requires 2 arguments"
 1511     unless @_ == 3;
 1512   croak "deleteGlobalProblem: argument 1 must contain a set_id"
 1513     unless defined $setID or caller eq __PACKAGE__;
 1514   croak "deleteGlobalProblem: argument 2 must contain a problem_id"
 1515     unless defined $problemID or caller eq __PACKAGE__;
 1516 
 1517   $self->deleteUserProblem(undef, $setID, $problemID);
 1518   return $self->{problem}->delete($setID, $problemID);
 1519 }
 1520 
 1521 =back
 1522 
 1523 =cut
 1524 
 1525 ################################################################################
 1526 # problem_user functions
 1527 ################################################################################
 1528 
 1529 =head2 User-Specific Problem Methods
 1530 
 1531 FIXME: write this
 1532 
 1533 =over
 1534 
 1535 =cut
 1536 
 1537 sub newUserProblem {
 1538   my ($self, @prototype) = @_;
 1539   return $self->{problem_user}->{record}->new(@prototype);
 1540 }
 1541 
 1542 sub countProblemUsers {
 1543   my ($self, $setID, $problemID) = @_;
 1544 
 1545   croak "countProblemUsers: requires 2 arguments"
 1546     unless @_ == 3;
 1547   croak "countProblemUsers: argument 1 must contain a set_id"
 1548     unless defined $setID;
 1549   croak "countProblemUsers: argument 2 must contain a problem_id"
 1550     unless defined $problemID;
 1551 
 1552   # the slow way
 1553   #return scalar $self->{problem_user}->list(undef, $setID, $problemID);
 1554 
 1555   # the fast way
 1556   return $self->{problem_user}->count(undef, $setID, $problemID);
 1557 }
 1558 
 1559 sub listProblemUsers {
 1560   my ($self, $setID, $problemID) = @_;
 1561 
 1562   carp "listProblemUsers called in SCALAR context: use countProblemUsers instead!\n"
 1563     unless wantarray;
 1564 
 1565   croak "listProblemUsers: requires 2 arguments"
 1566     unless @_ == 3;
 1567   croak "listProblemUsers: argument 1 must contain a set_id"
 1568     unless defined $setID;
 1569   croak "listProblemUsers: argument 2 must contain a problem_id"
 1570     unless defined $problemID;
 1571 
 1572   return map { $_->[0] } # extract user_id
 1573     $self->{problem_user}->list(undef, $setID, $problemID);
 1574 }
 1575 
 1576 sub listUserProblems {
 1577   my ($self, $userID, $setID) = @_;
 1578 
 1579   croak "listUserProblems: requires 2 arguments"
 1580     unless @_ == 3;
 1581   croak "listUserProblems: argument 1 must contain a user_id"
 1582     unless defined $userID;
 1583   croak "listUserProblems: argument 2 must contain a set_id"
 1584     unless defined $setID;
 1585 
 1586   return map { $_->[2] } # extract problem_id
 1587     $self->{problem_user}->list($userID, $setID, undef);
 1588 }
 1589 
 1590 sub addUserProblem {
 1591   my ($self, $UserProblem) = @_;
 1592 
 1593   croak "addUserProblem: requires 1 argument"
 1594     unless @_ == 2;
 1595   croak "addUserProblem: argument 1 must be of type ", $self->{problem_user}->{record}
 1596     unless ref $UserProblem eq $self->{problem_user}->{record};
 1597 
 1598   checkKeyfields($UserProblem);
 1599 
 1600   croak "addUserProblem: user problem exists (perhaps you meant to use putUserProblem?)"
 1601     if $self->{problem_user}->exists($UserProblem->user_id, $UserProblem->set_id, $UserProblem->problem_id);
 1602   croak "addUserProblem: user set ", $UserProblem->set_id, " for user ", $UserProblem->user_id, " not found"
 1603     unless $self->{set_user}->exists($UserProblem->user_id, $UserProblem->set_id);
 1604   croak "addUserProblem: problem ", $UserProblem->problem_id, " in set ", $UserProblem->set_id, " not found"
 1605     unless $self->{problem}->exists($UserProblem->set_id, $UserProblem->problem_id);
 1606 
 1607   return $self->{problem_user}->add($UserProblem);
 1608 }
 1609 
 1610 sub getUserProblem {
 1611   my ($self, $userID, $setID, $problemID) = @_;
 1612 
 1613   croak "getUserProblem: requires 3 arguments"
 1614     unless @_ == 4;
 1615   croak "getUserProblem: argument 1 must contain a user_id"
 1616     unless defined $userID;
 1617   croak "getUserProblem: argument 2 must contain a set_id"
 1618     unless defined $setID;
 1619   croak "getUserProblem: argument 3 must contain a problem_id"
 1620     unless defined $problemID;
 1621 
 1622   return ( $self->getUserProblems([$userID, $setID, $problemID]) )[0];
 1623 }
 1624 
 1625 =item getUserProblems(@userProblemIDs)
 1626 
 1627 Return a list of user set records associated with the user IDs given. If there
 1628 is no record associated with a given user ID, that element of the list will be
 1629 undefined. @userProblemIDs consists of references to arrays in which the first
 1630 element is the user_id, the second element is the set_id, and the third element
 1631 is the problem_id.
 1632 
 1633 =cut
 1634 
 1635 sub getUserProblems {
 1636   my ($self, @userProblemIDs) = @_;
 1637 
 1638   #croak "getUserProblems: requires 1 or more argument"
 1639   # unless @_ >= 2;
 1640   foreach my $i (0 .. $#userProblemIDs) {
 1641     croak "getUserProblems: element $i of argument list must contain a <user_id, set_id, problem_id> triple"
 1642       unless defined $userProblemIDs[$i]
 1643              and ref $userProblemIDs[$i] eq "ARRAY"
 1644              and @{$userProblemIDs[$i]} == 3
 1645              and defined $userProblemIDs[$i]->[0]
 1646              and defined $userProblemIDs[$i]->[1]
 1647              and defined $userProblemIDs[$i]->[2];
 1648   }
 1649 
 1650   return $self->{problem_user}->gets(@userProblemIDs);
 1651 }
 1652 
 1653 =item getAllUserProblems($userID, $setID)
 1654 
 1655 Returns a list of UserProblem objects representing all the problems in the
 1656 given set. When using the WW1Hash/GlobalTableEmulator schemas, this is far
 1657 more efficient than using listUserProblems and getUserProblems.
 1658 
 1659 =cut
 1660 
 1661 sub getAllUserProblems {
 1662   my ($self, $userID, $setID) = @_;
 1663 
 1664   croak "getAllUserProblems: requires 2 arguments"
 1665     unless @_ == 3;
 1666   croak "getAllUserProblems: argument 1 must contain a user_id"
 1667     unless defined $userID;
 1668   croak "getAllUserProblems: argument 2 must contain a set_id"
 1669     unless defined $setID;
 1670 
 1671   if ($self->{problem_user}->can("getAll")) {
 1672     return $self->{problem_user}->getAll($userID, $setID);
 1673   } else {
 1674     my @problemIDTriples = $self->{problem_user}->list($userID, $setID, undef);
 1675     return $self->{problem_user}->gets(@problemIDTriples);
 1676   }
 1677 }
 1678 
 1679 sub putUserProblem {
 1680   my ($self, $UserProblem) = @_;
 1681 
 1682   croak "putUserProblem: requires 1 argument"
 1683     unless @_ == 2;
 1684   croak "putUserProblem: argument 1 must be of type ", $self->{problem_user}->{record}
 1685     unless ref $UserProblem eq $self->{problem_user}->{record};
 1686 
 1687   checkKeyfields($UserProblem);
 1688 
 1689   croak "putUserProblem: user set ", $UserProblem->set_id, " for user ", $UserProblem->user_id, " not found"
 1690     unless $self->{set_user}->exists($UserProblem->user_id, $UserProblem->set_id);
 1691   croak "putUserProblem: user problem not found (perhaps you meant to use addUserProblem?)"
 1692     unless $self->{problem_user}->exists($UserProblem->user_id, $UserProblem->set_id, $UserProblem->problem_id);
 1693   croak "putUserProblem: problem ", $UserProblem->problem_id, " in set ", $UserProblem->set_id, " not found"
 1694     unless $self->{problem}->exists($UserProblem->set_id, $UserProblem->problem_id);
 1695 
 1696   return $self->{problem_user}->put($UserProblem);
 1697 }
 1698 
 1699 sub deleteUserProblem {
 1700   my ($self, $userID, $setID, $problemID) = @_;
 1701 
 1702   croak "getUserProblem: requires 3 arguments"
 1703     unless @_ == 4;
 1704   croak "getUserProblem: argument 1 must contain a user_id"
 1705     unless defined $userID or caller eq __PACKAGE__;
 1706   croak "getUserProblem: argument 2 must contain a set_id"
 1707     unless defined $setID or caller eq __PACKAGE__;
 1708   croak "getUserProblem: argument 3 must contain a problem_id"
 1709     unless defined $problemID or caller eq __PACKAGE__;
 1710 
 1711   return $self->{problem_user}->delete($userID, $setID, $problemID);
 1712 }
 1713 
 1714 =back
 1715 
 1716 =cut
 1717 
 1718 ################################################################################
 1719 # set+set_user functions
 1720 ################################################################################
 1721 
 1722 =head2 Set Merging Methods
 1723 
 1724 These functions combine a global set and a user set to create a merged set,
 1725 which is returned. Any field that is not defined in the user set is taken from
 1726 the global set. Merged sets have the same type as user sets.
 1727 
 1728 =over
 1729 
 1730 =cut
 1731 
 1732 sub getGlobalUserSet {
 1733   carp "getGlobalUserSet: this method is deprecated -- use getMergedSet instead";
 1734   return shift->getMergedSet(@_);
 1735 }
 1736 
 1737 =item getMergedSet($userID, $setID)
 1738 
 1739 Returns a merged set record associated with the record IDs given. If there is no
 1740 record associated with a given record ID, the undefined value is returned.
 1741 
 1742 =cut
 1743 
 1744 sub getMergedSet {
 1745   my ($self, $userID, $setID) = @_;
 1746 
 1747   croak "getMergedSet: requires 2 arguments"
 1748     unless @_ == 3;
 1749   croak "getMergedSet: argument 1 must contain a user_id"
 1750     unless defined $userID;
 1751   croak "getMergedSet: argument 2 must contain a set_id"
 1752     unless defined $setID;
 1753 
 1754   return ( $self->getMergedSets([$userID, $setID]) )[0];
 1755 }
 1756 
 1757 =item getMegedSets(@userSetIDs)
 1758 
 1759 Return a list of merged set records associated with the record IDs given. If
 1760 there is no record associated with a given record ID, that element of the list
 1761 will be undefined. @userSetIDs consists of references to arrays in which the
 1762 first element is the user_id and the second element is the set_id.
 1763 
 1764 =cut
 1765 
 1766 sub getMergedSets {
 1767   my ($self, @userSetIDs) = @_;
 1768 
 1769   #croak "getMergedSets: requires 1 or more argument"
 1770   # unless @_ >= 2;
 1771   foreach my $i (0 .. $#userSetIDs) {
 1772     croak "getMergedSets: element $i of argument list must contain a <user_id, set_id> pair"
 1773       unless defined $userSetIDs[$i]
 1774              and ref $userSetIDs[$i] eq "ARRAY"
 1775              and @{$userSetIDs[$i]} == 2
 1776              and defined $userSetIDs[$i]->[0]
 1777              and defined $userSetIDs[$i]->[1];
 1778   }
 1779 
 1780   # a horrible, terrible hack ;)
 1781   if (ref $self->{set_user} eq "WeBWorK::DB::Schema::WW1Hash"
 1782       and ref $self->{set} eq "WeBWorK::DB::Schema::GlobalTableEmulator") {
 1783     #warn __PACKAGE__.": using a terrible hack.\n";
 1784     $WeBWorK::timer->continue("DB: getsNoFilter start") if defined($WeBWorK::timer);
 1785     my @MergedSets = $self->{set_user}->getsNoFilter(@userSetIDs);
 1786     $WeBWorK::timer->continue("DB: getsNoFilter end") if defined($WeBWorK::timer);
 1787     return @MergedSets;
 1788   }
 1789 
 1790   $WeBWorK::timer->continue("DB: getUserSets start") if defined($WeBWorK::timer);
 1791   my @UserSets = $self->getUserSets(@userSetIDs); # checked
 1792 
 1793   $WeBWorK::timer->continue("DB: pull out set IDs start") if defined($WeBWorK::timer);
 1794   my @globalSetIDs = map { $_->[1] } @userSetIDs;
 1795   $WeBWorK::timer->continue("DB: getGlobalSets start") if defined($WeBWorK::timer);
 1796   my @GlobalSets = $self->getGlobalSets(@globalSetIDs); # checked
 1797 
 1798   $WeBWorK::timer->continue("DB: calc common fields start") if defined($WeBWorK::timer);
 1799   my %globalSetFields = map { $_ => 1 } $self->newGlobalSet->FIELDS;
 1800   my @commonFields = grep { exists $globalSetFields{$_} } $self->newUserSet->FIELDS;
 1801 
 1802   $WeBWorK::timer->continue("DB: merge start") if defined($WeBWorK::timer);
 1803   for (my $i = 0; $i < @UserSets; $i++) {
 1804     my $UserSet = $UserSets[$i];
 1805     my $GlobalSet = $GlobalSets[$i];
 1806     next unless defined $UserSet and defined $GlobalSet;
 1807     foreach my $field (@commonFields) {
 1808       #next if defined $UserSet->$field;
 1809       # ok, now we're testing for emptiness as well as definedness.
 1810       next if defined $UserSet->$field and $UserSet->$field ne "";
 1811       $UserSet->$field($GlobalSet->$field);
 1812     }
 1813   }
 1814   $WeBWorK::timer->continue("DB: merge done!") if defined($WeBWorK::timer);
 1815 
 1816   return @UserSets;
 1817 }
 1818 
 1819 =back
 1820 
 1821 =cut
 1822 
 1823 ################################################################################
 1824 # problem+problem_user functions
 1825 ################################################################################
 1826 
 1827 =head2 Problem Merging Methods
 1828 
 1829 These functions combine a global problem and a user problem to create a merged
 1830 problem, which is returned. Any field that is not defined in the user problem is
 1831 taken from the global problem. Merged problems have the same type as user
 1832 problems.
 1833 
 1834 =over
 1835 
 1836 =cut
 1837 
 1838 sub getGlobalUserProblem {
 1839   carp "getGlobalUserProblem: this method is deprecated -- use getMergedProblem instead";
 1840   return shift->getMergedProblem(@_);
 1841 }
 1842 
 1843 =item getMergedProblem($userID, $setID, $problemID)
 1844 
 1845 Returns a merged problem record associated with the record IDs given. If there
 1846 is no record associated with a given record ID, the undefined value is returned.
 1847 
 1848 =cut
 1849 
 1850 sub getMergedProblem {
 1851   my ($self, $userID, $setID, $problemID) = @_;
 1852 
 1853   croak "getGlobalUserSet: requires 3 arguments"
 1854     unless @_ == 4;
 1855   croak "getGlobalUserSet: argument 1 must contain a user_id"
 1856     unless defined $userID;
 1857   croak "getGlobalUserSet: argument 2 must contain a set_id"
 1858     unless defined $setID;
 1859   croak "getGlobalUserSet: argument 3 must contain a problem_id"
 1860     unless defined $problemID;
 1861 
 1862   return ( $self->getMergedProblems([$userID, $setID, $problemID]) )[0];
 1863 }
 1864 
 1865 =item getMergedProblems(@userProblemIDs)
 1866 
 1867 Return a list of merged problem records associated with the record IDs given. If
 1868 there is no record associated with a given record ID, that element of the list
 1869 will be undefined. @userProblemIDs consists of references to arrays in which the
 1870 first element is the user_id, the second element is the set_id, and the third
 1871 element is the problem_id.
 1872 
 1873 =cut
 1874 
 1875 sub getMergedProblems {
 1876   my ($self, @userProblemIDs) = @_;
 1877 
 1878   #croak "getMergedProblems: requires 1 or more argument"
 1879   # unless @_ >= 2;
 1880   foreach my $i (0 .. $#userProblemIDs) {
 1881     croak "getMergedProblems: element $i of argument list must contain a <user_id, set_id, problem_id> triple"
 1882       unless defined $userProblemIDs[$i]
 1883              and ref $userProblemIDs[$i] eq "ARRAY"
 1884              and @{$userProblemIDs[$i]} == 3
 1885              and defined $userProblemIDs[$i]->[0]
 1886              and defined $userProblemIDs[$i]->[1]
 1887              and defined $userProblemIDs[$i]->[2];
 1888   }
 1889 
 1890   $WeBWorK::timer->continue("DB: getUserProblems start") if defined($WeBWorK::timer);
 1891   my @UserProblems = $self->getUserProblems(@userProblemIDs); # checked
 1892 
 1893   $WeBWorK::timer->continue("DB: pull out set/problem IDs start") if defined($WeBWorK::timer);
 1894   my @globalProblemIDs = map { [ $_->[1], $_->[2] ] } @userProblemIDs;
 1895   $WeBWorK::timer->continue("DB: getGlobalProblems start") if defined($WeBWorK::timer);
 1896   my @GlobalProblems = $self->getGlobalProblems(@globalProblemIDs); # checked
 1897 
 1898   $WeBWorK::timer->continue("DB: calc common fields start") if defined($WeBWorK::timer);
 1899   my %globalProblemFields = map { $_ => 1 } $self->newGlobalProblem->FIELDS;
 1900   my @commonFields = grep { exists $globalProblemFields{$_} } $self->newUserProblem->FIELDS;
 1901 
 1902   $WeBWorK::timer->continue("DB: merge start") if defined($WeBWorK::timer);
 1903   for (my $i = 0; $i < @UserProblems; $i++) {
 1904     my $UserProblem = $UserProblems[$i];
 1905     my $GlobalProblem = $GlobalProblems[$i];
 1906     next unless defined $UserProblem and defined $GlobalProblem;
 1907     foreach my $field (@commonFields) {
 1908       # FIXME: WW1Hash upgrades undefined fileds to "" when creating record objects
 1909       # Shouldn't we be testing for emptiness rather than definedness?
 1910       # I think the spec says that if a field is EMPTY the global value is used.
 1911       #next if defined $UserProblem->$field;
 1912       # ok, now we're testing for emptiness as well as definedness.
 1913       next if defined $UserProblem->$field and $UserProblem->$field ne "";
 1914       $UserProblem->$field($GlobalProblem->$field);
 1915     }
 1916   }
 1917   $WeBWorK::timer->continue("DB: merge done!") if defined($WeBWorK::timer);
 1918 
 1919   return @UserProblems;
 1920 }
 1921 
 1922 =back
 1923 
 1924 =cut
 1925 
 1926 ################################################################################
 1927 # debugging
 1928 ################################################################################
 1929 
 1930 #sub dumpDB($$) {
 1931 # my ($self, $table) = @_;
 1932 # return $self->{$table}->dumpDB();
 1933 #}
 1934 
 1935 ################################################################################
 1936 # utilities
 1937 ################################################################################
 1938 
 1939 sub checkKeyfields($) {
 1940   my ($Record) = @_;
 1941   foreach my $keyfield ($Record->KEYFIELDS) {
 1942     my $value = $Record->$keyfield;
 1943     croak "checkKeyfields: $keyfield is empty"
 1944       unless defined $value and $value ne "";
 1945 
 1946     if ($keyfield eq "problem_id") {
 1947       croak "checkKeyfields: invalid characters in $keyfield field: $value (valid characters are [0-9])"
 1948         unless $value =~ m/^\d*$/;
 1949     } else {
 1950       croak "checkKeyfields: invalid characters in $keyfield field: $value (valid characters are [A-Za-z0-9_])"
 1951         unless $value =~ m/^[\w-]*$/;
 1952     }
 1953   }
 1954 }
 1955 
 1956 =head1 AUTHOR
 1957 
 1958 Written by Sam Hathaway, sh002i (at) math.rochester.edu.
 1959 
 1960 =cut
 1961 
 1962 1;

aubreyja at gmail dot com
ViewVC Help
Powered by ViewVC 1.0.9