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

View of /branches/rel-2-4-dev/webwork-modperl/lib/WeBWorK/DB.pm

Parent Directory Parent Directory | Revision Log Revision Log


Revision 5413 - (download) (as text) (annotate)
Sat Aug 25 18:15:01 2007 UTC (5 years, 9 months ago) by sh002i
File size: 63541 byte(s)
Resolve bug #1293 by actually throwing a RecordExists exception from
NewSQL::Std.

    1 ################################################################################
    2 # WeBWorK Online Homework Delivery System>
    3 # Copyright © 2000-2007 The WeBWorK Project, http://openwebwork.sf.net/
    4 # $CVSHeader: webwork2/lib/WeBWorK/DB.pm,v 1.104.2.1 2007/08/13 22:53:43 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.
   71 
   72 The schema modules provide an API that matches the requirements of the DB
   73 layer, on a per-table basis. Each schema module has a style that determines
   74 which drivers it can interface with. For example, SQL is an "dbi" style
   75 schema.
   76 
   77 =head2 Bottom Layer: Drivers
   78 
   79 Driver modules implement a style for a schema. They provide physical access to
   80 a data source containing the data for a table. The style of a driver determines
   81 what methods it provides. All drivers provide C<connect(MODE)> and
   82 C<disconnect()> methods. A dbi style driver provides a C<handle()> method which
   83 returns the DBI handle.
   84 
   85 =head2 Record Types
   86 
   87 In C<%dblayout>, each table is assigned a record class, used for passing
   88 complete records to and from the database. The default record classes are
   89 subclasses of the WeBWorK::DB::Record class, and are named as follows: User,
   90 Password, PermissionLevel, Key, Set, UserSet, Problem, UserProblem. In the
   91 following documentation, a reference the the record class for a table means the
   92 record class currently defined for that table in C<%dbLayout>.
   93 
   94 =cut
   95 
   96 use strict;
   97 use warnings;
   98 use Carp;
   99 use Data::Dumper;
  100 use WeBWorK::DB::Schema;
  101 use WeBWorK::DB::Utils qw/make_vsetID grok_vsetID grok_setID_from_vsetID_sql
  102   grok_versionID_from_vsetID_sql/;
  103 use WeBWorK::Debug;
  104 use WeBWorK::Utils qw(runtime_use);
  105 
  106 ################################################################################
  107 # constructor
  108 ################################################################################
  109 
  110 =head1 CONSTRUCTOR
  111 
  112 =over
  113 
  114 =item new($dbLayout)
  115 
  116 The C<new> method creates a DB object and brings up the underlying schema/driver
  117 structure according to the hash referenced by C<$dbLayout>.
  118 
  119 =back
  120 
  121 =head2 C<$dbLayout> Format
  122 
  123 C<$dbLayout> is a hash reference consisting of items keyed by table names. The
  124 value of each item is a reference to a hash containing the following items:
  125 
  126 =over
  127 
  128 =item record
  129 
  130 The name of a perl module to use for representing the data in a record.
  131 
  132 =item schema
  133 
  134 The name of a perl module to use for access to the table.
  135 
  136 =item driver
  137 
  138 The name of a perl module to use for access to the data source.
  139 
  140 =item source
  141 
  142 The location of the data source that should be used by the driver module.
  143 Depending on the driver, this may be a path, a url, or a DBI spec.
  144 
  145 =item params
  146 
  147 A reference to a hash containing extra information needed by the schema. Some
  148 schemas require parameters, some do not. Consult the documentation for the
  149 schema in question.
  150 
  151 =back
  152 
  153 For each table defined in C<$dbLayout>, C<new> loads the record, schema, and
  154 driver modules. It the schema module's C<tables> method lists the current table
  155 (or contains the string "*") and the output of the schema and driver modules'
  156 C<style> methods match, the table is installed. Otherwise, an exception is
  157 thrown.
  158 
  159 =cut
  160 
  161 sub new {
  162   my ($invocant, $dbLayout) = @_;
  163   my $class = ref($invocant) || $invocant;
  164   my $self = {};
  165   bless $self, $class; # bless this here so we can pass it to the schema
  166 
  167   # load the modules required to handle each table, and create driver
  168   foreach my $table (keys %$dbLayout) {
  169     $self->init_table($dbLayout, $table);
  170   }
  171 
  172   return $self;
  173 }
  174 
  175 sub init_table {
  176   my ($self, $dbLayout, $table) = @_;
  177 
  178   if (exists $self->{$table}) {
  179     if (defined $self->{$table}) {
  180       return;
  181     } else {
  182       die "loop in dbLayout table dependencies involving table '$table'\n";
  183     }
  184   }
  185 
  186   my $layout = $dbLayout->{$table};
  187   my $record = $layout->{record};
  188   my $schema = $layout->{schema};
  189   my $driver = $layout->{driver};
  190   my $source = $layout->{source};
  191   my $depend = $layout->{depend};
  192   my $params = $layout->{params};
  193 
  194   # add a key for this table to the self hash, but don't define it yet
  195   # this for loop detection
  196   $self->{$table} = undef;
  197 
  198   if ($depend) {
  199     foreach my $dep (@$depend) {
  200       $self->init_table($dbLayout, $dep);
  201     }
  202   }
  203 
  204   runtime_use($record);
  205 
  206   runtime_use($driver);
  207   my $driverObject = eval { $driver->new($source, $params) };
  208   croak "error instantiating DB driver $driver for table $table: $@"
  209     if $@;
  210 
  211   runtime_use($schema);
  212   my $schemaObject = eval { $schema->new(
  213     $self, $driverObject, $table, $record, $params) };
  214   croak "error instantiating DB schema $schema for table $table: $@"
  215     if $@;
  216 
  217   $self->{$table} = $schemaObject;
  218 }
  219 
  220 ################################################################################
  221 # methods that can be autogenerated
  222 ################################################################################
  223 
  224 sub gen_schema_accessor {
  225   my $schema = shift;
  226   return sub { shift->{$schema} };
  227 }
  228 
  229 sub gen_new {
  230   my $table = shift;
  231   return sub { shift->{$table}{record}->new(@_) };
  232 }
  233 
  234 sub gen_count_where {
  235   my $table = shift;
  236   return sub {
  237     my ($self, $where) = @_;
  238     return $self->{$table}->count_where($where);
  239   };
  240 }
  241 
  242 sub gen_exists_where {
  243   my $table = shift;
  244   return sub {
  245     my ($self, $where) = @_;
  246     return $self->{$table}->exists_where($where);
  247   };
  248 }
  249 
  250 sub gen_list_where {
  251   my $table = shift;
  252   return sub {
  253     my ($self, $where, $order) = @_;
  254     if (wantarray) {
  255       return $self->{$table}->list_where($where, $order);
  256     } else {
  257       return $self->{$table}->list_where_i($where, $order);
  258     }
  259   };
  260 }
  261 
  262 sub gen_get_records_where {
  263   my $table = shift;
  264   return sub {
  265     my ($self, $where, $order) = @_;
  266     if (wantarray) {
  267       return $self->{$table}->get_records_where($where, $order);
  268     } else {
  269       return $self->{$table}->get_records_where_i($where, $order);
  270     }
  271   };
  272 }
  273 
  274 ################################################################################
  275 # create/rename/delete tables
  276 ################################################################################
  277 
  278 sub create_tables {
  279   my ($self) = @_;
  280 
  281   foreach my $table (keys %$self) {
  282     next if $table =~ /^_/; # skip non-table self fields (none yet)
  283     next if $self->{$table}{params}{non_native}; # skip non-native tables
  284     my $schema_obj = $self->{$table};
  285     if ($schema_obj->can("create_table")) {
  286       $schema_obj->create_table;
  287     } else {
  288       warn "skipping creation of '$table' table: no create_table method\n";
  289     }
  290   }
  291 
  292   return 1;
  293 }
  294 
  295 sub rename_tables {
  296   my ($self, $new_dblayout) = @_;
  297 
  298   foreach my $table (keys %$self) {
  299     next if $table =~ /^_/; # skip non-table self fields (none yet)
  300     next if $self->{$table}{params}{non_native}; # skip non-native tables
  301     my $schema_obj = $self->{$table};
  302     if (exists $new_dblayout->{$table}) {
  303       if ($schema_obj->can("rename_table")) {
  304         # we look into the new dblayout to determine the new table names
  305         my $new_sql_table_name = defined $new_dblayout->{$table}{params}{tableOverride}
  306           ? $new_dblayout->{$table}{params}{tableOverride}
  307           : $table;
  308         $schema_obj->rename_table($new_sql_table_name);
  309       } else {
  310         warn "skipping renaming of '$table' table: no rename_table method\n";
  311       }
  312     } else {
  313       warn "skipping renaming of '$table' table: table doesn't exist in new dbLayout\n";
  314     }
  315   }
  316 
  317   return 1;
  318 }
  319 
  320 sub delete_tables {
  321   my ($self) = @_;
  322 
  323   foreach my $table (keys %$self) {
  324     next if $table =~ /^_/; # skip non-table self fields (none yet)
  325     next if $self->{$table}{params}{non_native}; # skip non-native tables
  326     my $schema_obj = $self->{$table};
  327     if ($schema_obj->can("delete_table")) {
  328       $schema_obj->delete_table;
  329     } else {
  330       warn "skipping deletion of '$table' table: no delete_table method\n";
  331     }
  332   }
  333 
  334   return 1;
  335 }
  336 
  337 ################################################################################
  338 # user functions
  339 ################################################################################
  340 
  341 BEGIN {
  342   *User = gen_schema_accessor("user");
  343   *newUser = gen_new("user");
  344   *countUsersWhere = gen_count_where("user");
  345   *existsUserWhere = gen_exists_where("user");
  346   *listUsersWhere = gen_list_where("user");
  347   *getUsersWhere = gen_get_records_where("user");
  348 }
  349 
  350 sub countUsers { return scalar shift->listUsers(@_) }
  351 
  352 sub listUsers {
  353   my ($self) = shift->checkArgs(\@_);
  354   if (wantarray) {
  355     return map { @$_ } $self->{user}->get_fields_where(["user_id"]);
  356   } else {
  357     return $self->{user}->count_where;
  358   }
  359 }
  360 
  361 sub existsUser {
  362   my ($self, $userID) = shift->checkArgs(\@_, qw/user_id/);
  363   return $self->{user}->exists($userID);
  364 }
  365 
  366 sub getUser {
  367   my ($self, $userID) = shift->checkArgs(\@_, qw/user_id/);
  368   return ( $self->getUsers($userID) )[0];
  369 }
  370 
  371 sub getUsers {
  372   my ($self, @userIDs) = shift->checkArgs(\@_, qw/user_id*/);
  373   return $self->{user}->gets(map { [$_] } @userIDs);
  374 }
  375 
  376 sub addUser {
  377   my ($self, $User) = shift->checkArgs(\@_, qw/REC:user/);
  378   eval {
  379     return $self->{user}->add($User);
  380   };
  381   if (my $ex = caught WeBWorK::DB::Schema::Ex::RecordExists) {
  382     croak "addUser: user exists (perhaps you meant to use putUser?)";
  383   } elsif ($@) {
  384     die $@;
  385   }
  386   # FIXME about these exceptions: eventually the exceptions should be part of
  387   # WeBWorK::DB rather than WeBWorK::DB::Schema, and we should just let them
  388   # through to the calling code. however, right now we have code that checks
  389   # for the string "... exists" in the error message, so we need to convert
  390   # here.
  391   #
  392   # WeBWorK::DB::Ex::RecordExists
  393   # WeBWorK::DB::Ex::DependencyNotFound - i.e. inserting a password for a nonexistent user
  394   # ?
  395 }
  396 
  397 sub putUser {
  398   my ($self, $User) = shift->checkArgs(\@_, qw/REC:user/);
  399   my $rows = $self->{user}->put($User); # DBI returns 0E0 for 0.
  400   if ($rows == 0) {
  401     croak "putUser: user not found (perhaps you meant to use addUser?)";
  402   } else {
  403     return $rows;
  404   }
  405 }
  406 
  407 sub deleteUser {
  408   my ($self, $userID) = shift->checkArgs(\@_, qw/user_id/);
  409   $self->deleteUserSet($userID, undef);
  410   $self->deletePassword($userID);
  411   $self->deletePermissionLevel($userID);
  412   $self->deleteKey($userID);
  413   return $self->{user}->delete($userID);
  414 }
  415 
  416 ################################################################################
  417 # password functions
  418 ################################################################################
  419 
  420 BEGIN {
  421   *Password = gen_schema_accessor("password");
  422   *newPassword = gen_new("password");
  423   *countPasswordsWhere = gen_count_where("password");
  424   *existsPasswordWhere = gen_exists_where("password");
  425   *listPasswordsWhere = gen_list_where("password");
  426   *getPasswordsWhere = gen_get_records_where("password");
  427 }
  428 
  429 sub countPasswords { return scalar shift->countPasswords(@_) }
  430 
  431 sub listPasswords {
  432   my ($self) = shift->checkArgs(\@_);
  433   if (wantarray) {
  434     return map { @$_ } $self->{password}->get_fields_where(["user_id"]);
  435   } else {
  436     return $self->{password}->count_where;
  437   }
  438 }
  439 
  440 sub existsPassword {
  441   my ($self, $userID) = shift->checkArgs(\@_, qw/user_id/);
  442   # FIXME should we claim that a password exists if the user exists, since
  443   # password records are auto-created?
  444   return $self->{password}->exists($userID);
  445 }
  446 
  447 sub getPassword {
  448   my ($self, $userID) = shift->checkArgs(\@_, qw/user_id/);
  449   return ( $self->getPasswords($userID) )[0];
  450 }
  451 
  452 sub getPasswords {
  453   my ($self, @userIDs) = shift->checkArgs(\@_, qw/user_id*/);
  454 
  455   my @Passwords = $self->{password}->gets(map { [$_] } @userIDs);
  456 
  457   # AUTO-CREATE missing password records
  458   # (this code is duplicated in getPermissionLevels, below)
  459   for (my $i = 0; $i < @Passwords; $i++) {
  460     my $Password = $Passwords[$i];
  461     my $userID = $userIDs[$i];
  462     if (not defined $Password) {
  463       if ($self->{user}->exists($userID)) {
  464         $Password = $self->newPassword(user_id => $userID);
  465         eval { $self->addPassword($Password) };
  466         if ($@ and $@ !~ m/password exists/) {
  467           die "error while auto-creating password record for user $userID: $@";
  468         }
  469         $Passwords[$i] = $Password;
  470       }
  471     }
  472   }
  473 
  474   return @Passwords;
  475 }
  476 
  477 sub addPassword {
  478   my ($self, $Password) = shift->checkArgs(\@_, qw/REC:password/);
  479 
  480   croak "addPassword: user ", $Password->user_id, " not found"
  481     unless $self->{user}->exists($Password->user_id);
  482 
  483   eval {
  484     return $self->{password}->add($Password);
  485   };
  486   if (my $ex = caught WeBWorK::DB::Schema::Ex::RecordExists) {
  487     croak "addPassword: password exists (perhaps you meant to use putPassword?)";
  488   } elsif ($@) {
  489     die $@;
  490   }
  491 }
  492 
  493 sub putPassword {
  494   my ($self, $Password) = shift->checkArgs(\@_, qw/REC:password/);
  495   my $rows = $self->{password}->put($Password); # DBI returns 0E0 for 0.
  496   if ($rows == 0) {
  497     # AUTO-CREATE permission level records
  498     return $self->addPassword($Password);
  499   } else {
  500     return $rows;
  501   }
  502 }
  503 
  504 sub deletePassword {
  505   my ($self, $userID) = shift->checkArgs(\@_, qw/user_id/);
  506   return $self->{password}->delete($userID);
  507 }
  508 
  509 ################################################################################
  510 # permission functions
  511 ################################################################################
  512 
  513 BEGIN {
  514   *PermissionLevel = gen_schema_accessor("permission");
  515   *newPermissionLevel = gen_new("permission");
  516   *countPermissionLevelsWhere = gen_count_where("permission");
  517   *existsPermissionLevelWhere = gen_exists_where("permission");
  518   *listPermissionLevelsWhere = gen_list_where("permission");
  519   *getPermissionLevelsWhere = gen_get_records_where("permission");
  520 }
  521 
  522 sub countPermissionLevels { return scalar shift->listPermissionLevels(@_) }
  523 
  524 sub listPermissionLevels {
  525   my ($self) = shift->checkArgs(\@_);
  526   if (wantarray) {
  527     return map { @$_ } $self->{permission}->get_fields_where(["user_id"]);
  528   } else {
  529     return $self->{permission}->count_where;
  530   }
  531 }
  532 
  533 sub existsPermissionLevel {
  534   my ($self, $userID) = shift->checkArgs(\@_, qw/user_id/);
  535   # FIXME should we claim that a permission level exists if the user exists,
  536   # since password records are auto-created?
  537   return $self->{permission}->exists($userID);
  538 }
  539 
  540 sub getPermissionLevel {
  541   my ($self, $userID) = shift->checkArgs(\@_, qw/user_id/);
  542   return ( $self->getPermissionLevels($userID) )[0];
  543 }
  544 
  545 sub getPermissionLevels {
  546   my ($self, @userIDs) = shift->checkArgs(\@_, qw/user_id*/);
  547 
  548   my @PermissionLevels = $self->{permission}->gets(map { [$_] } @userIDs);
  549 
  550   # AUTO-CREATE missing permission level records
  551   # (this code is duplicated in getPasswords, above)
  552   for (my $i = 0; $i < @PermissionLevels; $i++) {
  553     my $PermissionLevel = $PermissionLevels[$i];
  554     my $userID = $userIDs[$i];
  555     if (not defined $PermissionLevel) {
  556       if ($self->{user}->exists($userID)) {
  557         $PermissionLevel = $self->newPermissionLevel(user_id => $userID);
  558         eval { $self->addPermissionLevel($PermissionLevel) };
  559         if ($@ and $@ !~ m/permission level exists/) {
  560           die "error while auto-creating permission level record for user $userID: $@";
  561         }
  562         $PermissionLevels[$i] = $PermissionLevel;
  563       }
  564     }
  565   }
  566 
  567   return @PermissionLevels;
  568 }
  569 
  570 sub addPermissionLevel {
  571   my ($self, $PermissionLevel) = shift->checkArgs(\@_, qw/REC:permission/);
  572 
  573   croak "addPermissionLevel: user ", $PermissionLevel->user_id, " not found"
  574     unless $self->{user}->exists($PermissionLevel->user_id);
  575 
  576   eval {
  577     return $self->{permission}->add($PermissionLevel);
  578   };
  579   if (my $ex = caught WeBWorK::DB::Schema::Ex::RecordExists) {
  580     croak "addPermissionLevel: permission level exists (perhaps you meant to use putPermissionLevel?)";
  581   } elsif ($@) {
  582     die $@;
  583   }
  584 }
  585 
  586 sub putPermissionLevel {
  587   my ($self, $PermissionLevel) = shift->checkArgs(\@_, qw/REC:permission/);
  588   my $rows = $self->{permission}->put($PermissionLevel); # DBI returns 0E0 for 0.
  589   if ($rows == 0) {
  590     # AUTO-CREATE permission level records
  591     return $self->addPermissionLevel($PermissionLevel);
  592   } else {
  593     return $rows;
  594   }
  595 }
  596 
  597 sub deletePermissionLevel {
  598   my ($self, $userID) = shift->checkArgs(\@_, qw/user_id/);
  599   return $self->{permission}->delete($userID);
  600 }
  601 
  602 ################################################################################
  603 # key functions
  604 ################################################################################
  605 
  606 BEGIN {
  607   *Key = gen_schema_accessor("key");
  608   *newKey = gen_new("key");
  609   *countKeysWhere = gen_count_where("key");
  610   *existsKeyWhere = gen_exists_where("key");
  611   *listKeysWhere = gen_list_where("key");
  612   *getKeysWhere = gen_get_records_where("key");
  613 }
  614 
  615 sub countKeys { return scalar shift->listKeys(@_) }
  616 
  617 sub listKeys {
  618   my ($self) = shift->checkArgs(\@_);
  619   if (wantarray) {
  620     return map { @$_ } $self->{key}->get_fields_where(["user_id"]);
  621   } else {
  622     return $self->{key}->count_where;
  623   }
  624 }
  625 
  626 sub existsKey {
  627   my ($self, $userID) = shift->checkArgs(\@_, qw/user_id/);
  628   return $self->{key}->exists($userID);
  629 }
  630 
  631 sub getKey {
  632   my ($self, $userID) = shift->checkArgs(\@_, qw/user_id/);
  633   return ( $self->getKeys($userID) )[0];
  634 }
  635 
  636 sub getKeys {
  637   my ($self, @userIDs) = shift->checkArgs(\@_, qw/user_id*/);
  638   return $self->{key}->gets(map { [$_] } @userIDs);
  639 }
  640 
  641 sub addKey {
  642   # PROCTORING - allow comma in keyfields
  643   my ($self, $Key) = shift->checkArgs(\@_, qw/VREC:key/);
  644 
  645   # PROCTORING -  check for both user and proctor
  646   # we allow for two entries for proctor keys, one of the form
  647   #    userid,proctorid (which authorizes login), and the other
  648   #    of the form userid,proctorid,g (which authorizes grading)
  649   # (having two of these means that a proctored test will require
  650   #    authorization for both login and grading).
  651   if ($Key->user_id =~ /([^,]+)(?:,([^,]*))?(,g)?/) {
  652     my ($userID, $proctorID) = ($1, $2);
  653     croak "addKey: user $userID not found"
  654       unless $self->{user}->exists($userID);
  655     croak "addKey: proctor $proctorID not found"
  656       unless $self->{user}->exists($proctorID);
  657   } else {
  658     croak "addKey: user ", $Key->user_id, " not found"
  659       unless $self->{user}->exists($Key->user_id);
  660   }
  661 
  662   eval {
  663     return $self->{key}->add($Key);
  664   };
  665   if (my $ex = caught WeBWorK::DB::Schema::Ex::RecordExists) {
  666     croak "addKey: key exists (perhaps you meant to use putKey?)";
  667   } elsif ($@) {
  668     die $@;
  669   }
  670 }
  671 
  672 sub putKey {
  673   # PROCTORING - allow comma in keyfields
  674   my ($self, $Key) = shift->checkArgs(\@_, qw/VREC:key/);
  675   my $rows = $self->{key}->put($Key); # DBI returns 0E0 for 0.
  676   if ($rows == 0) {
  677     croak "putKey: key not found (perhaps you meant to use addKey?)";
  678   } else {
  679     return $rows;
  680   }
  681 }
  682 
  683 sub deleteKey {
  684   my ($self, $userID) = shift->checkArgs(\@_, qw/user_id/);
  685   return $self->{key}->delete($userID);
  686 }
  687 
  688 sub deleteAllProctorKeys {
  689   my ($self, $userID) = shift->checkArgs(\@_, qw/user_id/);
  690   my $where = [user_id_like => "$userID,%"];
  691 
  692   return $self->{key}->delete_where($where);
  693 }
  694 
  695 ################################################################################
  696 # locations functions
  697 ################################################################################
  698 # this database table is for ip restrictions by assignment
  699 # the locations table defines names of locations consisting of
  700 #    lists of ip masks (found in the location_addresses table)
  701 #    to which assignments can be restricted to or denied from.
  702 
  703 BEGIN {
  704   *Location = gen_schema_accessor("locations");
  705   *newLocation = gen_new("locations");
  706   *countLocationsWhere = gen_count_where("locations");
  707   *existsLocationWhere = gen_exists_where("locations");
  708   *listLocationsWhere = gen_list_where("locations");
  709   *getLocationsWhere = gen_get_records_where("locations");
  710 }
  711 
  712 sub countLocations { return scalar shift->listLocations(@_) }
  713 
  714 sub listLocations {
  715   my ( $self ) = shift->checkArgs(\@_);
  716   if ( wantarray ) {
  717       return map {@$_} $self->{locations}->get_fields_where(["location_id"]);
  718   } else {
  719     return $self->{locations}->count_where;
  720   }
  721 }
  722 
  723 sub existsLocation {
  724   my ( $self, $locationID ) = shift->checkArgs(\@_, qw/location_id/);
  725   return $self->{locations}->exists($locationID);
  726 }
  727 
  728 sub getLocation {
  729   my ( $self, $locationID ) = shift->checkArgs(\@_, qw/location_id/);
  730   return ( $self->getLocations($locationID) )[0];
  731 }
  732 
  733 sub getLocations {
  734   my ( $self, @locationIDs ) = shift->checkArgs(\@_, qw/location_id*/);
  735   return $self->{locations}->gets(map {[$_]} @locationIDs);
  736 }
  737 
  738 sub getAllLocations {
  739   my ( $self ) = shift->checkArgs(\@_);
  740   return $self->{locations}->get_records_where();
  741 }
  742 
  743 sub addLocation {
  744   my ( $self, $Location ) = shift->checkArgs(\@_, qw/REC:locations/);
  745 
  746   eval {
  747     return $self->{locations}->add($Location);
  748   };
  749   if ( my $ex = caught WeBWorK::DB::Schema::Ex::RecordExists ) {
  750     croak "addLocation: location exists (perhaps you meant to use putLocation?)";
  751   } elsif ($@) {
  752     die $@;
  753   }
  754 }
  755 
  756 sub putLocation {
  757   my ($self, $Location) = shift->checkArgs(\@_, qw/REC:locations/);
  758   my $rows = $self->{locations}->put($Location);
  759   if ( $rows == 0 ) {
  760     croak "putLocation: location not found (perhaps you meant to use addLocation?)";
  761   } else {
  762     return $rows;
  763   }
  764 }
  765 
  766 sub deleteLocation {
  767   # do we need to allow calls from this package?  I can't think of
  768   #    any case where that would happen, but we include it for other
  769   #    deletions, so I'll keep it here.
  770   my $U = caller eq __PACKAGE__ ? "!" : "";
  771   my ( $self, $locationID ) = shift->checkArgs(\@_, "location_id$U");
  772   $self->deleteGlobalSetLocation(undef, $locationID);
  773   $self->deleteUserSetLocation(undef, undef, $locationID);
  774 
  775   # NOTE: the one piece of this that we don't address is if this
  776   #    results in all of the locations in a set's restriction being
  777   #    cleared; in this case, we should probably also reset the
  778   #    set->restrict_ip setting as well.  but that requires going
  779   #    out and doing a bunch of manipulations that well exceed what
  780   #    we want to do in this routine, so we'll assume that the user
  781   #    is smart enough to deal with that on her own.
  782 
  783   # addresses in the location_addresses table also need to be cleared
  784   $self->deleteLocationAddress($locationID, undef);
  785 
  786   return $self->{locations}->delete($locationID);
  787 }
  788 
  789 ################################################################################
  790 # location_addresses functions
  791 ################################################################################
  792 # this database table is for ip restrictions by assignment
  793 # the location_addresses table defines the ipmasks associate
  794 #    with the locations that are used for restrictions.
  795 
  796 BEGIN {
  797   *LocationAddress = gen_schema_accessor("location_addresses");
  798   *newLocationAddress = gen_new("location_addresses");
  799   *countLocationAddressesWhere = gen_count_where("location_addresses");
  800   *existsLocationAddressWhere = gen_exists_where("location_addresses");
  801   *listLocationAddressesWhere = gen_list_where("location_addresses");
  802   *getLocationAddressesWhere = gen_get_records_where("location_addresses");
  803 }
  804 
  805 sub countAddressLocations { return scalar shift->listAddressLocations(@_) }
  806 
  807 sub listAddressLocations {
  808   my ($self, $ipmask) = shift->checkArgs(\@_, qw/ip_mask/);
  809   my $where = [ip_mask_eq => $ipmask];
  810   if ( wantarray ) {
  811     return map {@$_} $self->{location_addresses}->get_fields_where(["location_id"],$where);
  812   } else {
  813     return $self->{location_addresses}->count_where($where);
  814   }
  815 }
  816 
  817 sub countLocationAddresses { return scalar shift->listLocationAddresses(@_) }
  818 
  819 sub listLocationAddresses {
  820   my ($self, $locationID) = shift->checkArgs(\@_, qw/location_id/);
  821   my $where = [location_id_eq => $locationID];
  822   if ( wantarray ) {
  823     return map {@$_} $self->{location_addresses}->get_fields_where(["ip_mask"],$where);
  824   } else {
  825     return $self->{location_addresses}->count_where($where);
  826   }
  827 }
  828 
  829 sub existsLocationAddress {
  830   my ($self, $locationID, $ipmask) = shift->checkArgs(\@_, qw/location_id ip_mask/);
  831   return $self->{location_addresses}->exists($locationID, $ipmask);
  832 }
  833 
  834 # we wouldn't ever getLocationAddress or getLocationAddresses; to use those
  835 #   we would have to know all of the information that we're getting
  836 
  837 sub getAllLocationAddresses {
  838   my ($self, $locationID) = shift->checkArgs(\@_, qw/location_id/);
  839   my $where = [location_id_eq => $locationID];
  840   return $self->{location_addresses}->get_records_where($where);
  841 }
  842 
  843 sub addLocationAddress {
  844   my ($self, $LocationAddress) = shift->checkArgs(\@_, qw/REC:location_addresses/);
  845   croak "addLocationAddress: location ", $LocationAddress->location_id, " not found"
  846     unless $self->{locations}->exists($LocationAddress->location_id);
  847   eval {
  848     return $self->{location_addresses}->add($LocationAddress);
  849   };
  850   if (my $ex = caught WeBWorK::DB::Schema::Ex::RecordExists) {
  851     croak "addLocationAddress: location address exists (perhaps you meant to use putLocationAddress?)";
  852   } elsif ($@) {
  853     die $@;
  854   }
  855 }
  856 
  857 sub putLocationAddress {
  858   my ($self, $LocationAddress) = shift->checkArgs(\@_, qw/REC:location_addresses/);
  859   my $rows = $self->{location_addresses}->put($LocationAddress);
  860   if ( $rows == 0 ) {
  861     croak "putLocationAddress: location address not found (perhaps you meant to use addLocationAddress?)";
  862   } else {
  863     return $rows;
  864   }
  865 }
  866 
  867 sub deleteLocationAddress {
  868   # allow for undef values
  869   my $U = caller eq __PACKAGE__ ? "!" : "";
  870   my ($self, $locationID, $ipmask) = shift->checkArgs(\@_, "location_id$U", "ip_mask$U");
  871   return $self->{location_addresses}->delete($locationID, $ipmask);
  872 }
  873 
  874 
  875 ################################################################################
  876 # set functions
  877 ################################################################################
  878 
  879 BEGIN {
  880   *GlobalSet = gen_schema_accessor("set");
  881   *newGlobalSet = gen_new("set");
  882   *countGlobalSetsWhere = gen_count_where("set");
  883   *existsGlobalSetWhere = gen_exists_where("set");
  884   *listGlobalSetsWhere = gen_list_where("set");
  885   *getGlobalSetsWhere = gen_get_records_where("set");
  886 }
  887 
  888 sub countGlobalSets { return scalar shift->listGlobalSets(@_) }
  889 
  890 sub listGlobalSets {
  891   my ($self) = shift->checkArgs(\@_);
  892   if (wantarray) {
  893     return map { @$_ } $self->{set}->get_fields_where(["set_id"]);
  894   } else {
  895     return $self->{set}->count_where;
  896   }
  897 }
  898 
  899 sub existsGlobalSet {
  900   my ($self, $setID) = shift->checkArgs(\@_, qw/set_id/);
  901   return $self->{set}->exists($setID);
  902 }
  903 
  904 sub getGlobalSet {
  905   my ($self, $setID) = shift->checkArgs(\@_, qw/set_id/);
  906   return ( $self->getGlobalSets($setID) )[0];
  907 }
  908 
  909 sub getGlobalSets {
  910   my ($self, @setIDs) = shift->checkArgs(\@_, qw/set_id*/);
  911   return $self->{set}->gets(map { [$_] } @setIDs);
  912 }
  913 
  914 sub addGlobalSet {
  915   my ($self, $GlobalSet) = shift->checkArgs(\@_, qw/REC:set/);
  916 
  917   eval {
  918 
  919     return $self->{set}->add($GlobalSet);
  920   };
  921   if (my $ex = caught WeBWorK::DB::Schema::Ex::RecordExists) {
  922     croak "addGlobalSet: global set exists (perhaps you meant to use putGlobalSet?)";
  923   } elsif ($@) {
  924     die $@;
  925   }
  926 }
  927 
  928 sub putGlobalSet {
  929   my ($self, $GlobalSet) = shift->checkArgs(\@_, qw/REC:set/);
  930   my $rows = $self->{set}->put($GlobalSet); # DBI returns 0E0 for 0.
  931   if ($rows == 0) {
  932     croak "putGlobalSet: global set not found (perhaps you meant to use addGlobalSet?)";
  933   } else {
  934     return $rows;
  935   }
  936 }
  937 
  938 sub deleteGlobalSet {
  939   # setID can be undefined if being called from this package
  940   my $U = caller eq __PACKAGE__ ? "!" : "";
  941   my ($self, $setID) = shift->checkArgs(\@_, "set_id$U");
  942   $self->deleteUserSet(undef, $setID);
  943   $self->deleteGlobalProblem($setID, undef);
  944   return $self->{set}->delete($setID);
  945 }
  946 
  947 ################################################################################
  948 # set_user functions
  949 ################################################################################
  950 
  951 BEGIN {
  952   *UserSet = gen_schema_accessor("set_user");
  953   *newUserSet = gen_new("set_user");
  954   *countUserSetsWhere = gen_count_where("set_user");
  955   *existsUserSetWhere = gen_exists_where("set_user");
  956   *listUserSetsWhere = gen_list_where("set_user");
  957   *getUserSetsWhere = gen_get_records_where("set_user");
  958 }
  959 
  960 sub countSetUsers { return scalar shift->listSetUsers(@_) }
  961 
  962 sub listSetUsers {
  963   my ($self, $setID) = shift->checkArgs(\@_, qw/set_id/);
  964   my $where = [set_id_eq => $setID];
  965   if (wantarray) {
  966     return map { @$_ } $self->{set_user}->get_fields_where(["user_id"], $where);
  967   } else {
  968     return $self->{set_user}->count_where($where);
  969   }
  970 }
  971 
  972 sub countUserSets { return scalar shift->listUserSets(@_) }
  973 
  974 sub listUserSets {
  975   my ($self, $userID) = shift->checkArgs(\@_, qw/user_id/);
  976   my $where = [user_id_eq => $userID];
  977   if (wantarray) {
  978     return map { @$_ } $self->{set_user}->get_fields_where(["set_id"], $where);
  979   } else {
  980     return $self->{set_user}->count_where($where);
  981   }
  982 }
  983 
  984 sub existsUserSet {
  985   my ($self, $userID, $setID) = shift->checkArgs(\@_, qw/user_id set_id/);
  986   return $self->{set_user}->exists($userID, $setID);
  987 }
  988 
  989 sub getUserSet {
  990   my ($self, $userID, $setID) = shift->checkArgs(\@_, qw/user_id set_id/);
  991   return ( $self->getUserSets([$userID, $setID]) )[0];
  992 }
  993 
  994 sub getUserSets {
  995   my ($self, @userSetIDs) = shift->checkArgsRefList(\@_, qw/user_id set_id/);
  996   return $self->{set_user}->gets(@userSetIDs);
  997 }
  998 
  999 # the code from addUserSet() is duplicated in large part following in
 1000 # addVersionedUserSet; changes here should accordingly be propagated down there
 1001 sub addUserSet {
 1002   my ($self, $UserSet) = shift->checkArgs(\@_, qw/REC:set_user/);
 1003 
 1004   croak "addUserSet: user ", $UserSet->user_id, " not found"
 1005     unless $self->{user}->exists($UserSet->user_id);
 1006   croak "addUserSet: set ", $UserSet->set_id, " not found"
 1007     unless $self->{set}->exists($UserSet->set_id);
 1008 
 1009   eval {
 1010     return $self->{set_user}->add($UserSet);
 1011   };
 1012   if (my $ex = caught WeBWorK::DB::Schema::Ex::RecordExists) {
 1013     croak "addUserSet: user set exists (perhaps you meant to use putUserSet?)";
 1014   } elsif ($@) {
 1015     die $@;
 1016   }
 1017 }
 1018 
 1019 # the code from putUserSet() is duplicated in large part in the following
 1020 # putVersionedUserSet; c.f. that routine
 1021 sub putUserSet {
 1022   my ($self, $UserSet) = shift->checkArgs(\@_, qw/REC:set_user/);
 1023   my $rows = $self->{set_user}->put($UserSet); # DBI returns 0E0 for 0.
 1024   if ($rows == 0) {
 1025     croak "putUserSet: user set not found (perhaps you meant to use addUserSet?)";
 1026   } else {
 1027     return $rows;
 1028   }
 1029 }
 1030 
 1031 sub deleteUserSet {
 1032   # userID and setID can be undefined if being called from this package
 1033   my $U = caller eq __PACKAGE__ ? "!" : "";
 1034   my ($self, $userID, $setID) = shift->checkArgs(\@_, "user_id$U", "set_id$U");
 1035   $self->deleteSetVersion($userID, $setID, undef);
 1036   $self->deleteUserProblem($userID, $setID, undef);
 1037   return $self->{set_user}->delete($userID, $setID);
 1038 }
 1039 
 1040 ################################################################################
 1041 # set_merged functions
 1042 ################################################################################
 1043 
 1044 BEGIN {
 1045   *MergedSet = gen_schema_accessor("set_merged");
 1046   #*newMergedSet = gen_new("set_merged");
 1047   #*countMergedSetsWhere = gen_count_where("set_merged");
 1048   *existsMergedSetWhere = gen_exists_where("set_merged");
 1049   #*listMergedSetsWhere = gen_list_where("set_merged");
 1050   *getMergedSetsWhere = gen_get_records_where("set_merged");
 1051 }
 1052 
 1053 sub existsMergedSet {
 1054   my ($self, $userID, $setID) = shift->checkArgs(\@_, qw/user_id set_id/);
 1055   return $self->{set_merged}->exists($userID, $setID);
 1056 }
 1057 
 1058 sub getMergedSet {
 1059   my ($self, $userID, $setID) = shift->checkArgs(\@_, qw/user_id set_id/);
 1060   return ( $self->getMergedSets([$userID, $setID]) )[0];
 1061 }
 1062 
 1063 sub getMergedSets {
 1064   my ($self, @userSetIDs) = shift->checkArgsRefList(\@_, qw/user_id set_id/);
 1065   return $self->{set_merged}->gets(@userSetIDs);
 1066 }
 1067 
 1068 ################################################################################
 1069 # set_version functions (NEW)
 1070 ################################################################################
 1071 
 1072 BEGIN {
 1073   *SetVersion = gen_schema_accessor("set_version");
 1074   *newSetVersion = gen_new("set_version");
 1075   *countSetVersionsWhere = gen_count_where("set_version");
 1076   *existsSetVersionWhere = gen_exists_where("set_version");
 1077   *listSetVersionsWhere = gen_list_where("set_version");
 1078   *getSetVersionsWhere = gen_get_records_where("set_version");
 1079 }
 1080 
 1081 # versioned analog of countUserSets
 1082 sub countSetVersions { return scalar shift->listSetVersions(@_) }
 1083 
 1084 # versioned analog of listUserSets
 1085 sub listSetVersions {
 1086   my ($self, $userID, $setID) = shift->checkArgs(\@_, qw/user_id set_id/);
 1087   my $where = [user_id_eq_set_id_eq => $userID,$setID];
 1088   my $order = [ 'version_id' ];
 1089   if (wantarray) {
 1090     return map { @$_ } $self->{set_version}->get_fields_where(["version_id"], $where, $order);
 1091   } else {
 1092     return $self->{set_version}->count_where($where);
 1093   }
 1094 }
 1095 
 1096 # versioned analog of existsUserSet
 1097 sub existsSetVersion {
 1098   my ($self, $userID, $setID, $versionID) = shift->checkArgs(\@_, qw/user_id set_id version_id/);
 1099   return $self->{set_version}->exists($userID, $setID, $versionID);
 1100 }
 1101 
 1102 # versioned analog of getUserSet
 1103 sub getSetVersion {
 1104   my ($self, $userID, $setID, $versionID) = shift->checkArgs(\@_, qw/user_id set_id version_id/);
 1105   return ( $self->getSetVersions([$userID, $setID, $versionID]) )[0];
 1106 }
 1107 
 1108 # versioned analog of getUserSets
 1109 sub getSetVersions {
 1110   my ($self, @setVersionIDs) = shift->checkArgsRefList(\@_, qw/user_id set_id version_id/);
 1111   return $self->{set_version}->gets(@setVersionIDs);
 1112 }
 1113 
 1114 # versioned analog of addUserSet
 1115 sub addSetVersion {
 1116   my ($self, $SetVersion) = shift->checkArgs(\@_, qw/REC:set_version/);
 1117 
 1118   croak "addSetVersion: set ", $SetVersion->set_id, " not found for user ", $SetVersion->user_id
 1119     unless $self->{set_user}->exists($SetVersion->user_id, $SetVersion->set_id);
 1120 
 1121   eval {
 1122     return $self->{set_version}->add($SetVersion);
 1123   };
 1124   if (my $ex = caught WeBWorK::DB::Schema::Ex::RecordExists) {
 1125     croak "addSetVersion: set version exists (perhaps you meant to use putSetVersion?)";
 1126   } elsif ($@) {
 1127     die $@;
 1128   }
 1129 }
 1130 
 1131 # versioned analog of putUserSet
 1132 sub putSetVersion {
 1133   my ($self, $SetVersion) = shift->checkArgs(\@_, qw/REC:set_version/);
 1134   my $rows = $self->{set_version}->put($SetVersion); # DBI returns 0E0 for 0.
 1135   if ($rows == 0) {
 1136     croak "putSetVersion: set version not found (perhaps you meant to use addSetVersion?)";
 1137   } else {
 1138     return $rows;
 1139   }
 1140 }
 1141 
 1142 # versioned analog of deleteUserSet
 1143 sub deleteSetVersion {
 1144   # userID, setID, and versionID can be undefined if being called from this package
 1145   my $U = caller eq __PACKAGE__ ? "!" : "";
 1146   my ($self, $userID, $setID, $versionID) = shift->checkArgs(\@_, "user_id$U", "set_id$U", "version_id$U");
 1147   $self->deleteProblemVersion($userID, $setID, $versionID, undef);
 1148   return $self->{set_version}->delete($userID, $setID, $versionID);
 1149 }
 1150 
 1151 ################################################################################
 1152 # set_version_merged functions (NEW)
 1153 ################################################################################
 1154 
 1155 BEGIN {
 1156   *MergedSetVersion = gen_schema_accessor("set_version_merged");
 1157   #*newMergedSetVersion = gen_new("set_version_merged");
 1158   #*countMergedSetVersionsWhere = gen_count_where("set_version_merged");
 1159   *existsMergedSetVersionWhere = gen_exists_where("set_version_merged");
 1160   #*listMergedSetVersionsWhere = gen_list_where("set_version_merged");
 1161   *getMergedSetVersionsWhere = gen_get_records_where("set_version_merged");
 1162 }
 1163 
 1164 sub existsMergedSetVersion {
 1165   my ($self, $userID, $setID, $versionID) = shift->checkArgs(\@_, qw/user_id set_id version_id/);
 1166   return $self->{set_version_merged}->exists($userID, $setID, $versionID);
 1167 }
 1168 
 1169 sub getMergedSetVersion {
 1170   my ($self, $userID, $setID, $versionID) = shift->checkArgs(\@_, qw/user_id set_id version_id/);
 1171   return ( $self->getMergedSetVersions([$userID, $setID, $versionID]) )[0];
 1172 }
 1173 
 1174 sub getMergedSetVersions {
 1175   my ($self, @setVersionIDs) = shift->checkArgsRefList(\@_, qw/user_id set_id version_id/);
 1176   return $self->{set_version_merged}->gets(@setVersionIDs);
 1177 }
 1178 
 1179 ################################################################################
 1180 # set_locations functions
 1181 ################################################################################
 1182 # this database table is for ip restrictions by assignment
 1183 # the set_locations table defines the association between a
 1184 #    global set and the locations to which the set may be
 1185 #    restricted or denied.
 1186 
 1187 BEGIN {
 1188   *GlobalSetLocation = gen_schema_accessor("set_locations");
 1189   *newGlobalSetLocation = gen_new("set_locations");
 1190   *countGlobalSetLocationsWhere = gen_count_where("set_locations");
 1191   *existsGlobalSetLocationWhere = gen_exists_where("set_locations");
 1192   *listGlobalSetLocationsWhere = gen_list_where("set_locations");
 1193   *getGlobalSetLocationsWhere = gen_get_records_where("set_locations");
 1194 }
 1195 
 1196 sub countGlobalSetLocations { return scalar shift->listGlobalSetLocations(@_) }
 1197 
 1198 sub listGlobalSetLocations {
 1199   my ( $self, $setID ) = shift->checkArgs(\@_, qw/set_id/);
 1200   my $where = [set_id_eq => $setID];
 1201   if ( wantarray ) {
 1202     my $order = ['location_id'];
 1203     return map { @$_ } $self->{set_locations}->get_fields_where(["location_id"], $where, $order);
 1204   } else {
 1205     return $self->{set_user}->count_where( $where );
 1206   }
 1207 }
 1208 
 1209 sub existsGlobalSetLocation {
 1210   my ( $self, $setID, $locationID ) = shift->checkArgs(\@_, qw/set_id location_id/);
 1211   return $self->{set_locations}->exists( $setID, $locationID );
 1212 }
 1213 
 1214 sub getGlobalSetLocation {
 1215   my ( $self, $setID, $locationID ) = shift->checkArgs(\@_, qw/set_id location_id/);
 1216   return ( $self->getGlobalSetLocations([$setID, $locationID]) )[0];
 1217 }
 1218 
 1219 sub getGlobalSetLocations {
 1220   my ( $self, @locationIDs ) = shift->checkArgsRefList(\@_, qw/set_id location_id/);
 1221   return $self->{set_locations}->gets(@locationIDs);
 1222 }
 1223 
 1224 sub getAllGlobalSetLocations {
 1225   my ( $self, $setID ) = shift->checkArgs(\@_, qw/set_id/);
 1226   my $where = [set_id_eq => $setID];
 1227   return $self->{set_locations}->get_records_where( $where );
 1228 }
 1229 
 1230 sub addGlobalSetLocation {
 1231   my ( $self, $GlobalSetLocation ) = shift->checkArgs(\@_, qw/REC:set_locations/);
 1232   croak "addGlobalSetLocation: set ", $GlobalSetLocation->set_id, " not found"
 1233     unless $self->{set}->exists($GlobalSetLocation->set_id);
 1234 
 1235   eval {
 1236     return $self->{set_locations}->add($GlobalSetLocation);
 1237   };
 1238   if (my $ex = caught WeBWorK::DB::Schema::Ex::RecordExists) {
 1239     croak "addGlobalSetLocation: global set_location exists (perhaps you meant to use putGlobalSetLocation?)";
 1240   } elsif ($@) {
 1241     die $@;
 1242   }
 1243 }
 1244 
 1245 sub putGlobalSetLocation {
 1246   my ($self, $GlobalSetLocation) = shift->checkArgs(\@_, qw/REC:set_locations/);
 1247   my $rows = $self->{set_locations}->put($GlobalSetLocation); # DBI returns 0E0 for 0.
 1248   if ($rows == 0) {
 1249     croak "putGlobalSetLocation: global problem not found (perhaps you meant to use addGlobalSetLocation?)";
 1250   } else {
 1251     return $rows;
 1252   }
 1253 }
 1254 
 1255 sub deleteGlobalSetLocation {
 1256   # setID and locationID can be undefined if being called from this package
 1257   my $U = caller eq __PACKAGE__ ? "!" : "";
 1258   my ($self, $setID, $locationID) = shift->checkArgs(\@_, "set_id$U", "location_id$U");
 1259   $self->deleteUserSetLocation(undef, $setID, $locationID);
 1260   return $self->{set_locations}->delete($setID, $locationID);
 1261 }
 1262 
 1263 ################################################################################
 1264 # set_locations_user functions
 1265 ################################################################################
 1266 # this database table is for ip restrictions by assignment
 1267 # the set_locations_user table defines the set_user level
 1268 #    modifications to the set_locations defined for the
 1269 #    global set
 1270 
 1271 BEGIN {
 1272   *UserSetLocation = gen_schema_accessor("set_locations_user");
 1273   *newUserSetLocation = gen_new("set_locations_user");
 1274   *countUserSetLocationWhere = gen_count_where("set_locations_user");
 1275   *existsUserSetLocationWhere = gen_exists_where("set_locations_user");
 1276   *listUserSetLocationsWhere = gen_list_where("set_locations_user");
 1277   *getUserSetLocationsWhere = gen_get_records_where("set_locations_user");
 1278 }
 1279 
 1280 sub countSetLocationUsers { return scalar shift->listSetLocationUsers(@_) }
 1281 
 1282 sub listSetLocationUsers {
 1283   my ($self, $setID, $locationID) = shift->checkArgs(\@_, qw/set_id location_id/);
 1284   my $where = [set_id_eq_location_id_eq => $setID,$locationID];
 1285   if (wantarray) {
 1286     return map { @$_ } $self->{set_locations_user}->get_fields_where(["user_id"], $where);
 1287   } else {
 1288     return $self->{set_locations_user}->count_where($where);
 1289   }
 1290 }
 1291 
 1292 sub countUserSetLocations { return scalar shift->listUserSetLocations(@_) }
 1293 
 1294 sub listUserSetLocations {
 1295   my ($self, $userID, $setID) = shift->checkArgs(\@_, qw/user_id set_id/);
 1296   my $where = [user_id_eq_set_id_eq => $userID,$setID];
 1297   if (wantarray) {
 1298     return map { @$_ } $self->{set_locations_user}->get_fields_where(["location_id"], $where);
 1299   } else {
 1300     return $self->{set_locations_user}->count_where($where);
 1301   }
 1302 }
 1303 
 1304 sub existsUserSetLocation {
 1305   my ($self, $userID, $setID, $locationID) = shift->checkArgs(\@_, qw/user_id set_id location_id/);
 1306   return $self->{set_locations_user}->exists($userID,$setID,$locationID);
 1307 }
 1308 
 1309 # FIXME: we won't ever use this because all fields are key fields
 1310 sub getUserSetLocation {
 1311   my ($self, $userID, $setID, $locationID) = shift->checkArgs(\@_, qw/user_id set_id location_id/);
 1312   return( $self->getUserSetLocations([$userID, $setID, $locationID]) )[0];
 1313 }
 1314 
 1315 # FIXME: we won't ever use this because all fields are key fields
 1316 sub getUserSetLocations {
 1317   my ($self, @userSetLocationIDs) = shift->checkArgsRefList(\@_, qw/user_id set_id location_id/);
 1318   return $self->{set_locations_user}->gets(@userSetLocationIDs);
 1319 }
 1320 
 1321 sub getAllUserSetLocations {
 1322   my ($self, $userID, $setID) = shift->checkArgs(\@_, qw/user_id set_id/);
 1323   my $where = [user_id_eq_set_id_eq => $userID,$setID];
 1324   return $self->{set_locations_user}->get_records_where($where);
 1325 }
 1326 
 1327 sub addUserSetLocation {
 1328   # VERSIONING - accept versioned ID fields
 1329   my ($self, $UserSetLocation) = shift->checkArgs(\@_, qw/VREC:set_locations_user/);
 1330 
 1331   croak "addUserSetLocation: user set ", $UserSetLocation->set_id, " for user ", $UserSetLocation->user_id, " not found"
 1332     unless $self->{set_user}->exists($UserSetLocation->user_id, $UserSetLocation->set_id);
 1333 
 1334   eval {
 1335     return $self->{set_locations_user}->add($UserSetLocation);
 1336   };
 1337   if (my $ex = caught WeBWorK::DB::Schema::Ex::RecordExists) {
 1338     croak "addUserSetLocation: user set_location exists (perhaps you meant to use putUserSetLocation?)";
 1339   } elsif ($@) {
 1340     die $@;
 1341   }
 1342 }
 1343 
 1344 # FIXME: we won't ever use this because all fields are key fields
 1345 # versioned_ok is an optional argument which lets us slip versioned setIDs through checkArgs.
 1346 sub putUserSetLocation {
 1347   my $V = $_[2] ? "V" : "";
 1348   my ($self, $UserSetLocation, undef) = shift->checkArgs(\@_, "${V}REC:set_locations_user", "versioned_ok!?");
 1349 
 1350   my $rows = $self->{set_locations_user}->put($UserSetLocation); # DBI returns 0E0 for 0.
 1351   if ($rows == 0) {
 1352     croak "putUserSetLocation: user set location not found (perhaps you meant to use addUserSetLocation?)";
 1353   } else {
 1354     return $rows;
 1355   }
 1356 }
 1357 
 1358 sub deleteUserSetLocation {
 1359   # userID, setID, and locationID can be undefined if being called from this package
 1360   my $U = caller eq __PACKAGE__ ? "!" : "";
 1361   my ($self, $userID, $setID, $locationID) = shift->checkArgs(\@_, "user_id$U", "set_id$U", "set_locations_id$U");
 1362   return $self->{set_locations_user}->delete($userID,$setID,$locationID);
 1363 }
 1364 
 1365 ################################################################################
 1366 # set_locations_merged functions
 1367 ################################################################################
 1368 # this is different from other set_merged functions, because
 1369 #    in this case the only data that we have are the set_id,
 1370 #    location_id, and user_id, and we want to replace all
 1371 #    locations from GlobalSetLocations with those from
 1372 #    UserSetLocations if the latter exist.
 1373 
 1374 sub getAllMergedSetLocations {
 1375   my ($self, $userID, $setID) = shift->checkArgs(\@_, qw/user_id set_id/);
 1376 
 1377   if ( $self->countUserSetLocations($userID, $setID) ) {
 1378     return $self->getAllUserSetLocations( $userID, $setID );
 1379   } else {
 1380     return $self->getAllGlobalSetLocations( $setID );
 1381   }
 1382 }
 1383 
 1384 
 1385 ################################################################################
 1386 # problem functions
 1387 ################################################################################
 1388 
 1389 BEGIN {
 1390   *GlobalProblem = gen_schema_accessor("problem");
 1391   *newGlobalProblem = gen_new("problem");
 1392   *countGlobalProblemsWhere = gen_count_where("problem");
 1393   *existsGlobalProblemWhere = gen_exists_where("problem");
 1394   *listGlobalProblemsWhere = gen_list_where("problem");
 1395   *getGlobalProblemsWhere = gen_get_records_where("problem");
 1396 }
 1397 
 1398 sub countGlobalProblems { return scalar shift->listGlobalProblems(@_) }
 1399 
 1400 sub listGlobalProblems {
 1401   my ($self, $setID) = shift->checkArgs(\@_, qw/set_id/);
 1402   my $where = [set_id_eq => $setID];
 1403   if (wantarray) {
 1404     return map { @$_ } $self->{problem}->get_fields_where(["problem_id"], $where);
 1405   } else {
 1406     return $self->{problem}->count_where($where);
 1407   }
 1408 }
 1409 
 1410 sub existsGlobalProblem {
 1411   my ($self, $setID, $problemID) = shift->checkArgs(\@_, qw/set_id problem_id/);
 1412   return $self->{problem}->exists($setID, $problemID);
 1413 }
 1414 
 1415 sub getGlobalProblem {
 1416   my ($self, $setID, $problemID) = shift->checkArgs(\@_, qw/set_id problem_id/);
 1417   return ( $self->getGlobalProblems([$setID, $problemID]) )[0];
 1418 }
 1419 
 1420 sub getGlobalProblems {
 1421   my ($self, @problemIDs) = shift->checkArgsRefList(\@_, qw/set_id problem_id/);
 1422   return $self->{problem}->gets(@problemIDs);
 1423 }
 1424 
 1425 sub getAllGlobalProblems {
 1426   my ($self, $setID) = shift->checkArgs(\@_, qw/set_id/);
 1427   my $where = [set_id_eq => $setID];
 1428   return $self->{problem}->get_records_where($where);
 1429 }
 1430 
 1431 sub addGlobalProblem {  my ($self, $GlobalProblem) = shift->checkArgs(\@_, qw/REC:problem/);
 1432 
 1433   croak "addGlobalProblem: set ", $GlobalProblem->set_id, " not found"
 1434     unless $self->{set}->exists($GlobalProblem->set_id);
 1435 
 1436   eval {
 1437     return $self->{problem}->add($GlobalProblem);
 1438   };
 1439   if (my $ex = caught WeBWorK::DB::Schema::Ex::RecordExists) {
 1440     croak "addGlobalProblem: global problem exists (perhaps you meant to use putGlobalProblem?)";
 1441   } elsif ($@) {
 1442     die $@;
 1443   }
 1444 }
 1445 
 1446 sub putGlobalProblem {
 1447   my ($self, $GlobalProblem) = shift->checkArgs(\@_, qw/REC:problem/);
 1448   my $rows = $self->{problem}->put($GlobalProblem); # DBI returns 0E0 for 0.
 1449   if ($rows == 0) {
 1450     croak "putGlobalProblem: global problem not found (perhaps you meant to use addGlobalProblem?)";
 1451   } else {
 1452     return $rows;
 1453   }
 1454 }
 1455 
 1456 sub deleteGlobalProblem {
 1457   # userID and setID can be undefined if being called from this package
 1458   my $U = caller eq __PACKAGE__ ? "!" : "";
 1459   my ($self, $setID, $problemID) = shift->checkArgs(\@_, "set_id$U", "problem_id$U");
 1460   $self->deleteUserProblem(undef, $setID, $problemID);
 1461   return $self->{problem}->delete($setID, $problemID);
 1462 }
 1463 
 1464 ################################################################################
 1465 # problem_user functions
 1466 ################################################################################
 1467 
 1468 BEGIN {
 1469   *UserProblem = gen_schema_accessor("problem_user");
 1470   *newUserProblem = gen_new("problem_user");
 1471   *countUserProblemsWhere = gen_count_where("problem_user");
 1472   *existsUserProblemWhere = gen_exists_where("problem_user");
 1473   *listUserProblemsWhere = gen_list_where("problem_user");
 1474   *getUserProblemsWhere = gen_get_records_where("problem_user");
 1475 }
 1476 
 1477 sub countProblemUsers { return scalar shift->listProblemUsers(@_) }
 1478 
 1479 sub listProblemUsers {
 1480   my ($self, $setID, $problemID) = shift->checkArgs(\@_, qw/set_id problem_id/);
 1481   my $where = [set_id_eq_problem_id_eq => $setID,$problemID];
 1482   if (wantarray) {
 1483     return map { @$_ } $self->{problem_user}->get_fields_where(["user_id"], $where);
 1484   } else {
 1485     return $self->{problem_user}->count_where($where);
 1486   }
 1487 }
 1488 
 1489 sub countUserProblems { return scalar shift->listUserProblems(@_) }
 1490 
 1491 sub listUserProblems {
 1492   my ($self, $userID, $setID) = shift->checkArgs(\@_, qw/user_id set_id/);
 1493   my $where = [user_id_eq_set_id_eq => $userID,$setID];
 1494   if (wantarray) {
 1495     return map { @$_ } $self->{problem_user}->get_fields_where(["problem_id"], $where);
 1496   } else {
 1497     return $self->{problem_user}->count_where($where);
 1498   }
 1499 }
 1500 
 1501 sub existsUserProblem {
 1502   my ($self, $userID, $setID, $problemID) = shift->checkArgs(\@_, qw/user_id set_id problem_id/);
 1503   return $self->{problem_user}->exists($userID, $setID, $problemID);
 1504 }
 1505 
 1506 sub getUserProblem {
 1507   my ($self, $userID, $setID, $problemID) = shift->checkArgs(\@_, qw/user_id set_id problem_id/);
 1508   return ( $self->getUserProblems([$userID, $setID, $problemID]) )[0];
 1509 }
 1510 
 1511 sub getUserProblems {
 1512   my ($self, @userProblemIDs) = shift->checkArgsRefList(\@_, qw/user_id set_id problem_id/);
 1513   return $self->{problem_user}->gets(@userProblemIDs);
 1514 }
 1515 
 1516 sub getAllUserProblems {
 1517   my ($self, $userID, $setID) = shift->checkArgs(\@_, qw/user_id set_id/);
 1518   my $where = [user_id_eq_set_id_eq => $userID,$setID];
 1519   return $self->{problem_user}->get_records_where($where);
 1520 }
 1521 
 1522 sub addUserProblem {
 1523   # VERSIONING - accept versioned ID fields
 1524   my ($self, $UserProblem) = shift->checkArgs(\@_, qw/VREC:problem_user/);
 1525 
 1526   croak "addUserProblem: user set ", $UserProblem->set_id, " for user ", $UserProblem->user_id, " not found"
 1527     unless $self->{set_user}->exists($UserProblem->user_id, $UserProblem->set_id);
 1528 
 1529   my ( $nv_set_id, $versionNum ) = grok_vsetID( $UserProblem->set_id );
 1530 
 1531   croak "addUserProblem: problem ", $UserProblem->problem_id, " in set $nv_set_id not found"
 1532     unless $self->{problem}->exists($nv_set_id, $UserProblem->problem_id);
 1533 
 1534   eval {
 1535     return $self->{problem_user}->add($UserProblem);
 1536   };
 1537   if (my $ex = caught WeBWorK::DB::Schema::Ex::RecordExists) {
 1538     croak "addUserProblem: user problem exists (perhaps you meant to use putUserProblem?)";
 1539   } elsif ($@) {
 1540     die $@;
 1541   }
 1542 }
 1543 
 1544 # versioned_ok is an optional argument which lets us slip versioned setIDs through checkArgs.
 1545 sub putUserProblem {
 1546   my $V = $_[2] ? "V" : "";
 1547   my ($self, $UserProblem, undef) = shift->checkArgs(\@_, "${V}REC:problem_user", "versioned_ok!?");
 1548 
 1549   my $rows = $self->{problem_user}->put($UserProblem); # DBI returns 0E0 for 0.
 1550   if ($rows == 0) {
 1551     croak "putUserProblem: user problem not found (perhaps you meant to use addUserProblem?)";
 1552   } else {
 1553     return $rows;
 1554   }
 1555 }
 1556 
 1557 sub deleteUserProblem {
 1558   # userID, setID, and problemID can be undefined if being called from this package
 1559   my $U = caller eq __PACKAGE__ ? "!" : "";
 1560   my ($self, $userID, $setID, $problemID) = shift->checkArgs(\@_, "user_id$U", "set_id$U", "problem_id$U");
 1561   return $self->{problem_user}->delete($userID, $setID, $problemID);
 1562 }
 1563 
 1564 ################################################################################
 1565 # problem_merged functions
 1566 ################################################################################
 1567 
 1568 BEGIN {
 1569   *MergedProblem = gen_schema_accessor("problem_merged");
 1570   #*newMergedProblem = gen_new("problem_merged");
 1571   #*countMergedProblemsWhere = gen_count_where("problem_merged");
 1572   *existsMergedProblemWhere = gen_exists_where("problem_merged");
 1573   #*listMergedProblemsWhere = gen_list_where("problem_merged");
 1574   *getMergedProblemsWhere = gen_get_records_where("problem_merged");
 1575 }
 1576 
 1577 sub existsMergedProblem {
 1578   my ($self, $userID, $setID, $problemID) = shift->checkArgs(\@_, qw/user_id set_id problem_id/);
 1579   return $self->{problem_merged}->exists($userID, $setID, $problemID);
 1580 }
 1581 
 1582 sub getMergedProblem {
 1583   my ($self, $userID, $setID, $problemID) = shift->checkArgs(\@_, qw/user_id set_id problem_id/);
 1584   return ( $self->getMergedProblems([$userID, $setID, $problemID]) )[0];
 1585 }
 1586 
 1587 sub getMergedProblems {
 1588   my ($self, @userProblemIDs) = shift->checkArgsRefList(\@_, qw/user_id set_id problem_id/);
 1589   return $self->{problem_merged}->gets(@userProblemIDs);
 1590 }
 1591 
 1592 sub getAllMergedUserProblems {
 1593   my ($self, $userID, $setID) = shift->checkArgs(\@_, qw/user_id set_id/);
 1594   my $where = [user_id_eq_set_id_eq => $userID,$setID];
 1595   return $self->{problem_merged}->get_records_where($where);
 1596 }
 1597 
 1598 ################################################################################
 1599 # problem_version functions (NEW)
 1600 ################################################################################
 1601 
 1602 BEGIN {
 1603   *ProblemVersion = gen_schema_accessor("problem_version");
 1604   *newProblemVersion = gen_new("problem_version");
 1605   *countProblemVersionsWhere = gen_count_where("problem_version");
 1606   *existsProblemVersionWhere = gen_exists_where("problem_version");
 1607   *listProblemVersionsWhere = gen_list_where("problem_version");
 1608   *getProblemVersionsWhere = gen_get_records_where("problem_version");
 1609 }
 1610 
 1611 # versioned analog of countUserProblems
 1612 sub countProblemVersions { return scalar shift->listProblemVersions(@_) }
 1613 
 1614 # versioned analog of listUserProblems
 1615 # for consistency, we should name this "listProblemVersions", but that is
 1616 # confusing, as that sounds as if we're listing the versions of a problem.
 1617 # however, that's nonsensical, so we appropriate it here and don't worry
 1618 # about the confusion.
 1619 sub listProblemVersions {
 1620   my ($self, $userID, $setID, $versionID) = shift->checkArgs(\@_, qw/user_id set_id version_id/);
 1621   my $where = [user_id_eq_set_id_eq_version_id_eq => $userID,$setID,$versionID];
 1622   if (wantarray) {
 1623     return map { @$_ } $self->{problem_version}->get_fields_where(["problem_id"], $where);
 1624   } else {
 1625     return $self->{problem_version}->count_where($where);
 1626   }
 1627 }
 1628 
 1629 # this code returns a list of all problem versions with the given userID,
 1630 # setID, and problemID, but that is (darn well ought to be) the same as
 1631 # listSetVersions, so it's not so useful as all that; c.f. above.
 1632 # sub listProblemVersions {
 1633 #   my ($self, $userID, $setID, $problemID) = shift->checkArgs(\@_, qw/user_id set_id problem_id/);
 1634 #   my $where = [user_id_eq_set_id_eq_problem_id_eq => $userID,$setID,$problemID];
 1635 #   if (wantarray) {
 1636 #     return grep { @$_ } $self->{problem_version}->get_fields_where(["version_id"], $where);
 1637 #   } else {
 1638 #     return $self->{problem_version}->count_where($where);
 1639 #   }
 1640 # }
 1641 
 1642 # versioned analog of existsUserProblem
 1643 sub existsProblemVersion {
 1644   my ($self, $userID, $setID, $versionID, $problemID) = shift->checkArgs(\@_, qw/user_id set_id version_id problem_id/);
 1645   return $self->{problem_version}->exists($userID, $setID, $versionID, $problemID);
 1646 }
 1647 
 1648 # versioned analog of getUserProblem
 1649 sub getProblemVersion {
 1650   my ($self, $userID, $setID, $versionID, $problemID) = shift->checkArgs(\@_, qw/user_id set_id version_id problem_id/);
 1651   return ( $self->getProblemVersions([$userID, $setID, $versionID, $problemID]) )[0];
 1652 }
 1653 
 1654 # versioned analog of getUserProblems
 1655 sub getProblemVersions {
 1656   my ($self, @problemVersionIDs) = shift->checkArgsRefList(\@_, qw/user_id set_id version_id problem_id/);
 1657   return $self->{problem_version}->gets(@problemVersionIDs);
 1658 }
 1659 
 1660 # versioned analog of getAllUserProblems
 1661 sub getAllProblemVersions {
 1662   my ( $self, $userID, $setID, $versionID ) = shift->checkArgs(\@_, qw/user_id set_id version_id/);
 1663   my $where = [user_id_eq_set_id_eq_version_id_eq => $userID,$setID,$versionID];
 1664   my $order = ["problem_id"];
 1665   return $self->{problem_version_merged}->get_records_where($where,$order);
 1666 }
 1667 
 1668 
 1669 # versioned analog of addUserProblem
 1670 sub addProblemVersion {
 1671   my ($self, $ProblemVersion) = shift->checkArgs(\@_, qw/REC:problem_version/);
 1672 
 1673   croak "addProblemVersion: set version ", $ProblemVersion->version_id, " of set ", $ProblemVersion->set_id, " not found for user ", $ProblemVersion->user_id
 1674     unless $self->{set_version}->exists($ProblemVersion->user_id, $ProblemVersion->set_id, $ProblemVersion->version_id);
 1675   croak "addProblemVersion: problem ", $ProblemVersion->problem_id, " of set ", $ProblemVersion->set_id, " not found for user ", $ProblemVersion->user_id
 1676     unless $self->{problem_user}->exists($ProblemVersion->user_id, $ProblemVersion->set_id, $ProblemVersion->problem_id);
 1677 
 1678   eval {
 1679     return $self->{problem_version}->add($ProblemVersion);
 1680   };
 1681   if (my $ex = caught WeBWorK::DB::Schema::Ex::RecordExists) {
 1682     croak "addProblemVersion: problem version exists (perhaps you meant to use putProblemVersion?)";
 1683   } elsif ($@) {
 1684     die $@;
 1685   }
 1686 }
 1687 
 1688 # versioned analog of putUserProblem
 1689 sub putProblemVersion {
 1690   my ($self, $ProblemVersion) = shift->checkArgs(\@_, qw/REC:problem_version/);
 1691   my $rows = $self->{problem_version}->put($ProblemVersion); # DBI returns 0E0 for 0.
 1692   if ($rows == 0) {
 1693     croak "putProblemVersion: problem version not found (perhaps you meant to use addProblemVersion?)";
 1694   } else {
 1695     return $rows;
 1696   }
 1697 }
 1698 
 1699 # versioned analog of deleteUserProblem
 1700 sub deleteProblemVersion {
 1701   # userID, setID, versionID, and problemID can be undefined if being called from this package
 1702   my $U = caller eq __PACKAGE__ ? "!" : "";
 1703   my ($self, $userID, $setID, $versionID, $problemID) = shift->checkArgs(\@_, "user_id$U", "set_id$U", "version_id$U", "problem_id$U");
 1704   return $self->{problem_version}->delete($userID, $setID, $versionID, $problemID);
 1705 }
 1706 
 1707 ################################################################################
 1708 # problem_version_merged functions (NEW)
 1709 ################################################################################
 1710 
 1711 BEGIN {
 1712   *MergedProblemVersion = gen_schema_accessor("problem_version_merged");
 1713   #*newMergedProblemVersion = gen_new("problem_version_merged");
 1714   #*countMergedProblemVersionsWhere = gen_count_where("problem_version_merged");
 1715   *existsMergedProblemVersionWhere = gen_exists_where("problem_version_merged");
 1716   #*listMergedProblemVersionsWhere = gen_list_where("problem_version_merged");
 1717   *getMergedProblemVersionsWhere = gen_get_records_where("problem_version_merged");
 1718 }
 1719 
 1720 sub existsMergedProblemVersion {
 1721   my ($self, $userID, $setID, $versionID, $problemID) = shift->checkArgs(\@_, qw/user_id set_id version_id problem_id/);
 1722   return $self->{problem_version_merged}->exists($userID, $setID, $versionID, $problemID);
 1723 }
 1724 
 1725 sub getMergedProblemVersion {
 1726   my ($self, $userID, $setID, $versionID, $problemID) = shift->checkArgs(\@_, qw/user_id set_id version_id problem_id/);
 1727   return ( $self->getMergedProblemVersions([$userID, $setID, $versionID, $problemID]) )[0];
 1728 }
 1729 
 1730 sub getMergedProblemVersions {
 1731   my ($self, @problemVersionIDs) = shift->checkArgsRefList(\@_, qw/user_id set_id version_id problem_id/);
 1732   return $self->{problem_version_merged}->gets(@problemVersionIDs);
 1733 }
 1734 
 1735 sub getAllMergedProblemVersions {
 1736   my ($self, $userID, $setID, $versionID) = shift->checkArgs(\@_, qw/user_id set_id version_id/);
 1737   my $where = [user_id_eq_set_id_eq_version_id_eq => $userID,$setID,$versionID];
 1738   my $order = ["problem_id"];
 1739   return $self->{problem_version_merged}->get_records_where($where,$order);
 1740 }
 1741 
 1742 ################################################################################
 1743 # utilities
 1744 ################################################################################
 1745 
 1746 # the (optional) second argument to checkKeyfields is to support versioned
 1747 # (gateway) sets, which may include commas in certain fields (in particular,
 1748 # set names (e.g., setDerivativeGateway,v1) and user names (e.g.,
 1749 # username,proctorname)
 1750 
 1751 sub checkKeyfields($;$) {
 1752   my ($Record, $versioned) = @_;
 1753   foreach my $keyfield ($Record->KEYFIELDS) {
 1754     my $value = $Record->$keyfield;
 1755 
 1756     croak "undefined '$keyfield' field"
 1757       unless defined $value;
 1758     croak "empty '$keyfield' field"
 1759       unless $value ne "";
 1760 
 1761     if ($keyfield eq "problem_id") {
 1762       croak "invalid characters in '$keyfield' field: '$value' (valid characters are [0-9])"
 1763         unless $value =~ m/^[0-9]*$/;
 1764     } elsif ($versioned and $keyfield eq "set_id") {
 1765       croak "invalid characters in '$keyfield' field: '$value' (valid characters are [-a-zA-Z0-9_.,])"
 1766         unless $value =~ m/^[-a-zA-Z0-9_.,]*$/;
 1767     } elsif ($versioned and $keyfield eq "user_id") {
 1768       croak "invalid characters in '$keyfield' field: '$value' (valid characters are [-a-zA-Z0-9_.,])"
 1769           unless ( $value =~ m/^[-a-zA-Z0-9_.]*,?(set_id:)?[-a-zA-Z0-9_.]*(,g)?$/ );
 1770     } elsif ($keyfield eq "ip_mask") {
 1771       croak "invalid characters in '$keyfield' field: '$value' (valid characters are [-a-fA-F0-9_.:/])"
 1772         unless $value =~ m/^[-a-fA-F0-9_.:\/]*$/;
 1773     } elsif ($keyfield eq "user_id") {
 1774 
 1775 
 1776     } else {
 1777       croak "invalid characters in '$keyfield' field: '$value' (valid characters are [-a-zA-Z0-9_.])"
 1778         unless $value =~ m/^[-a-zA-Z0-9_.]*$/;
 1779     }
 1780   }
 1781 }
 1782 
 1783 # checkArgs spec syntax:
 1784 #
 1785 # spec = list_item | item*
 1786 # list_item = item is_list
 1787 # is_list = "*"
 1788 # item = item_name undef_ok? optional?
 1789 # item_name = record_item | bare_item
 1790 # record_item = is_versioned? "REC:" table
 1791 # is_versioned = "V"
 1792 # table = \w+
 1793 # bare_item = \w+
 1794 # undef_ok = "!"
 1795 # optional = "?"
 1796 #
 1797 # [[V]REC:]foo[!][?][*]
 1798 
 1799 sub checkArgs {
 1800   my ($self, $args, @spec) = @_;
 1801 
 1802   my $is_list = @spec == 1 && $spec[0] =~ s/\*$//;
 1803   my ($min_args, $max_args);
 1804   if ($is_list) {
 1805     $min_args = 0;
 1806   } else {
 1807     foreach my $i (0..$#spec) {
 1808       #print "$i - $spec[$i]\n";
 1809       if ($spec[$i] =~ s/\?$//) {
 1810         #print "$i - matched\n";
 1811         $min_args = $i unless defined $min_args;
 1812       }
 1813     }
 1814     $min_args = @spec unless defined $min_args;
 1815     $max_args = @spec;
 1816   }
 1817 
 1818   if (@$args < $min_args or defined $max_args and @$args > $max_args) {
 1819     if ($min_args == $max_args) {
 1820       my $s = $min_args == 1 ? "" : "s";
 1821       croak "requires $min_args argument$s";
 1822     } elsif (defined $max_args) {
 1823       croak "requires between $min_args and $max_args arguments";
 1824     } else {
 1825       my $s = $min_args == 1 ? "" : "s";
 1826       croak "requires at least $min_args argument$s";
 1827     }
 1828   }
 1829 
 1830   my ($name, $versioned, $table);
 1831   if ($is_list) {
 1832     $name = $spec[0];
 1833     ($versioned, $table) = $name =~ /^(V?)REC:(.*)/;
 1834   }
 1835 
 1836   foreach my $i (0..@$args-1) {
 1837     my $arg = $args->[$i];
 1838     my $pos = $i+1;
 1839 
 1840     unless ($is_list) {
 1841       $name = $spec[$i];
 1842       ($versioned, $table) = $name =~ /^(V?)REC:(.*)/;
 1843     }
 1844 
 1845     if (defined $table) {
 1846       my $class = $self->{$table}{record};
 1847       #print "arg=$arg class=$class\n";
 1848       croak "argument $pos must be of type $class"
 1849         unless defined $arg and ref $arg and $arg->isa($class);
 1850       eval { checkKeyfields($arg, $versioned) };
 1851       croak "argument $pos contains $@" if $@;
 1852     } else {
 1853       if ($name !~ /!$/) {
 1854         croak "argument $pos must contain a $name"
 1855           unless defined $arg;
 1856       }
 1857     }
 1858   }
 1859 
 1860   return $self, @$args;
 1861 }
 1862 
 1863 sub checkArgsRefList {
 1864   my ($self, $items, @spec) = @_;
 1865   foreach my $i (0..@$items-1) {
 1866     my $item = $items->[$i];
 1867     my $pos = $i+1;
 1868     croak "item $pos must be a reference to an array"
 1869       unless UNIVERSAL::isa($item, "ARRAY");
 1870     eval { $self->checkArgs($item, @spec) };
 1871     croak "item $pos $@" if $@;
 1872   }
 1873 
 1874   return $self, @$items;
 1875 }
 1876 
 1877 1;

aubreyja at gmail dot com
ViewVC Help
Powered by ViewVC 1.0.9