[system] / trunk / webwork2 / lib / WeBWorK / DB.pm Repository:
ViewVC logotype

View of /trunk/webwork2/lib/WeBWorK/DB.pm

Parent Directory Parent Directory | Revision Log Revision Log


Revision 7053 - (download) (as text) (annotate)
Fri Sep 30 23:11:57 2011 UTC (20 months, 2 weeks ago) by gage
File size: 69404 byte(s)
fixes to coloring of answer blanks.
merging work of Grant He on GatewayQuiz
Grades and problem set presentation


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

aubreyja at gmail dot com
ViewVC Help
Powered by ViewVC 1.0.9