--- trunk/webwork-modperl/lib/WeBWorK/DB.pm 2003/09/27 19:23:27 1541 +++ trunk/webwork-modperl/lib/WeBWorK/DB.pm 2003/12/17 20:21:15 1672 @@ -1,6 +1,17 @@ ################################################################################ -# WeBWorK mod_perl (c) 2000-2002 WeBWorK Project -# $Id$ +# WeBWorK Online Homework Delivery System +# Copyright © 2000-2003 The WeBWorK Project, http://openwebwork.sf.net/ +# $CVSHeader: webwork-modperl/lib/WeBWorK/DB.pm,v 1.42 2003/12/12 20:23:26 sh002i Exp $ +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of either: (a) the GNU General Public License as published by the +# Free Software Foundation; either version 2, or (at your option) any later +# version, or (b) the "Artistic License" which comes with this package. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See either the GNU General Public License or the +# Artistic License for more details. ################################################################################ package WeBWorK::DB; @@ -46,7 +57,7 @@ The top layer of the architecture is the DB module. It provides the methods listed below, and uses schema modules (via tables) to implement those methods. - / new* list* exists* add* get* put* delete* \ <- api + / new* list* exists* add* get* get*s put* delete* \ <- api +------------------------------------------------------------------+ | DB | +------------------------------------------------------------------+ @@ -243,8 +254,8 @@ =cut sub newPassword { - my ($self, $prototype) = @_; - return $self->{password}->{record}->new($prototype); + my ($self, @prototype) = @_; + return $self->{password}->{record}->new(@prototype); } =item listPasswords() @@ -279,21 +290,21 @@ unless @_ == 2; croak "addPassword: argument 1 must be of type ", $self->{password}->{record} unless ref $Password eq $self->{password}->{record}; + + checkKeyfields($Password); + croak "addPassword: password exists (perhaps you meant to use putPassword?)" if $self->{password}->exists($Password->user_id); croak "addPassword: user ", $Password->user_id, " not found" unless $self->{user}->exists($Password->user_id); - checkKeyfields($Password); - return $self->{password}->add($Password); } =item getPassword($userID) If a record with a matching user ID exists, a record object containting that -record's data will be returned. If no such record exists, an undefined value -will be returned. +record's data will be returned. If no such record exists, one will be created. =cut @@ -305,28 +316,46 @@ croak "getPassword: argument 1 must contain a user_id" unless defined $userID; - return $self->{password}->get($userID); + #return $self->{password}->get($userID); + return ( $self->getPasswords($userID) )[0]; } =item getPasswords(@uesrIDs) Return a list of password records associated with the user IDs given. If there -is no record associated with a given user ID, that element of the list will be -undefined. +is no record associated with a given user ID, one will be created. =cut sub getPasswords { my ($self, @userIDs) = @_; - croak "getPasswords: requires 1 or more argument" - unless @_ >= 2; + #croak "getPasswords: requires 1 or more argument" + # unless @_ >= 2; foreach my $i (0 .. $#userIDs) { croak "getPasswords: element $i of argument list must contain a user_id" unless defined $userIDs[$i]; } - return map { $self->getPassword($_) } @userIDs; + my @Passwords = $self->{password}->gets(map { [$_] } @userIDs); + + for (my $i = 0; $i < @Passwords; $i++) { + my $Password = $Passwords[$i]; + my $userID = $userIDs[$i]; + if (not defined $Password) { + #warn "not defined\n"; + if ($self->{user}->exists($userID)) { + #warn "user exists\n"; + $Password = $self->newPassword(user_id => $userID); + eval { $self->addPassword($Password) }; + if ($@ and $@ !~ m/password exists/) { + die "error while auto-creating password record for user $userID: \"$@\""; + } + } + } + } + + return @Passwords; } =item putPassword($Password) @@ -345,11 +374,12 @@ unless @_ == 2; croak "putPassword: argument 1 must be of type ", $self->{password}->{record} unless ref $Password eq $self->{password}->{record}; - croak "putPassword: password not found (perhaps you meant to use addPassword?)" - unless $self->{password}->exists($Password->user_id); checkKeyfields($Password); + croak "putPassword: password not found (perhaps you meant to use addPassword?)" + unless $self->{password}->exists($Password->user_id); + return $self->{password}->put($Password); } @@ -391,8 +421,8 @@ =cut sub newPermissionLevel { - my ($self, $prototype) = @_; - return $self->{permission}->{record}->new($prototype); + my ($self, @prototype) = @_; + return $self->{permission}->{record}->new(@prototype); } =item listPermissionLevels() @@ -427,21 +457,21 @@ unless @_ == 2; croak "addPermissionLevel: argument 1 must be of type ", $self->{permission}->{record} unless ref $PermissionLevel eq $self->{permission}->{record}; + + checkKeyfields($PermissionLevel); + croak "addPermissionLevel: permission level exists (perhaps you meant to use putPermissionLevel?)" if $self->{permission}->exists($PermissionLevel->user_id); croak "addPermissionLevel: user ", $PermissionLevel->user_id, " not found" unless $self->{user}->exists($PermissionLevel->user_id); - checkKeyfields($PermissionLevel); - return $self->{permission}->add($PermissionLevel); } =item getPermissionLevel($userID) If a record with a matching user ID exists, a record object containting that -record's data will be returned. If no such record exists, an undefined value -will be returned. +record's data will be returned. If no such record exists, one will be created. =cut @@ -453,28 +483,47 @@ croak "getPermissionLevel: argument 1 must contain a user_id" unless defined $userID; - return $self->{permission}->get($userID); + #return $self->{permission}->get($userID); + return ( $self->getPermissionLevels($userID) )[0]; } =item getPermissionLevels(@uesrIDs) Return a list of permission level records associated with the user IDs given. If -there is no record associated with a given user ID, that element of the list -will be undefined. +there is no record associated with a given user ID, one will be created. =cut sub getPermissionLevels { my ($self, @userIDs) = @_; - croak "getPermissionLevels: requires 1 or more argument" - unless @_ >= 2; + #croak "getPermissionLevels: requires 1 or more argument" + # unless @_ >= 2; foreach my $i (0 .. $#userIDs) { croak "getPermissionLevels: element $i of argument list must contain a user_id" unless defined $userIDs[$i]; } - return map { $self->getPermissionLevel($_) } @userIDs; + my @PermissionLevels = $self->{permission}->gets(map { [$_] } @userIDs); + + for (my $i = 0; $i < @PermissionLevels; $i++) { + my $PermissionLevel = $PermissionLevels[$i]; + my $userID = $userIDs[$i]; + if (not defined $PermissionLevel) { + #warn "not defined\n"; + if ($self->{user}->exists($userID)) { + #warn "user exists\n"; + $PermissionLevel = $self->newPermissionLevel(user_id => $userID); + warn $PermissionLevel->toString, "\n"; + eval { $self->addPermissionLevel($PermissionLevel) }; + if ($@ and $@ !~ m/permission level exists/) { + die "error while auto-creating permission level record for user $userID: \"$@\""; + } + } + } + } + + return @PermissionLevels; } =item putPermissionLevel($PermissionLevel) @@ -493,11 +542,12 @@ unless @_ == 2; croak "putPermissionLevel: argument 1 must be of type ", $self->{permission}->{record} unless ref $PermissionLevel eq $self->{permission}->{record}; - croak "putPermissionLevel: permission level not found (perhaps you meant to use addPermissionLevel?)" - unless $self->{permission}->exists($PermissionLevel->user_id); checkKeyfields($PermissionLevel); + croak "putPermissionLevel: permission level not found (perhaps you meant to use addPermissionLevel?)" + unless $self->{permission}->exists($PermissionLevel->user_id); + return $self->{permission}->put($PermissionLevel); } @@ -535,8 +585,8 @@ =cut sub newKey { - my ($self, $prototype) = @_; - return $self->{key}->{record}->new($prototype); + my ($self, @prototype) = @_; + return $self->{key}->{record}->new(@prototype); } =item listKeys() @@ -571,13 +621,14 @@ unless @_ == 2; croak "addKey: argument 1 must be of type ", $self->{key}->{record} unless ref $Key eq $self->{key}->{record}; + + checkKeyfields($Key); + croak "addKey: key exists (perhaps you meant to use putKey?)" if $self->{key}->exists($Key->user_id); croak "addKey: user ", $Key->user_id, " not found" unless $self->{user}->exists($Key->user_id); - checkKeyfields($Key); - return $self->{key}->add($Key); } @@ -611,14 +662,14 @@ sub getKeys { my ($self, @userIDs) = @_; - croak "getKeys: requires 1 or more argument" - unless @_ >= 2; + #croak "getKeys: requires 1 or more argument" + # unless @_ >= 2; foreach my $i (0 .. $#userIDs) { croak "getKeys: element $i of argument list must contain a user_id" unless defined $userIDs[$i]; } - return map { $self->getKey($_) } @userIDs; + return $self->{key}->gets(map { [$_] } @userIDs); } =item putKey($Key) @@ -636,11 +687,12 @@ unless @_ == 2; croak "putKey: argument 1 must be of type ", $self->{key}->{record} unless ref $Key eq $self->{key}->{record}; - croak "putKey: key not found (perhaps you meant to use addKey?)" - unless $self->{key}->exists($Key->user_id); checkKeyfields($Key); + croak "putKey: key not found (perhaps you meant to use addKey?)" + unless $self->{key}->exists($Key->user_id); + return $self->{key}->put($Key); } @@ -678,8 +730,8 @@ =cut sub newUser { - my ($self, $prototype) = @_; - return $self->{user}->{record}->new($prototype); + my ($self, @prototype) = @_; + return $self->{user}->{record}->new(@prototype); } =item listUsers() @@ -688,7 +740,7 @@ =cut -sub listUsers($) { +sub listUsers { my ($self) = @_; croak "listUsers: requires 0 arguments" @@ -706,18 +758,19 @@ =cut -sub addUser($$) { +sub addUser { my ($self, $User) = @_; croak "addUser: requires 1 argument" unless @_ == 2; croak "addUser: argument 1 must be of type ", $self->{user}->{record} unless ref $User eq $self->{user}->{record}; - croak "addUser: user exists (perhaps you meant to use putUser?)" - if $self->{user}->exists($User->user_id); checkKeyfields($User); + croak "addUser: user exists (perhaps you meant to use putUser?)" + if $self->{user}->exists($User->user_id); + return $self->{user}->add($User); } @@ -729,7 +782,7 @@ =cut -sub getUser($$) { +sub getUser { my ($self, $userID) = @_; croak "getUser: requires 1 argument" @@ -751,14 +804,14 @@ sub getUsers { my ($self, @userIDs) = @_; - croak "getUsers: requires 1 or more argument" - unless @_ >= 2; + #croak "getUsers: requires 1 or more argument" + # unless @_ >= 2; foreach my $i (0 .. $#userIDs) { croak "getUsers: element $i of argument list must contain a user_id" unless defined $userIDs[$i]; } - return map { $self->getUser($_) } @userIDs; + return $self->{user}->gets(map { [$_] } @userIDs); } =item putUser($User) @@ -769,18 +822,19 @@ =cut -sub putUser($$) { +sub putUser { my ($self, $User) = @_; croak "putUser: requires 1 argument" unless @_ == 2; croak "putUser: argument 1 must be of type ", $self->{user}->{record} unless ref $User eq $self->{user}->{record}; - croak "putUser: user not found (perhaps you meant to use addUser?)" - unless $self->{user}->exists($User->user_id); checkKeyfields($User); + croak "putUser: user not found (perhaps you meant to use addUser?)" + unless $self->{user}->exists($User->user_id); + return $self->{user}->put($User); } @@ -794,7 +848,7 @@ =cut -sub deleteUser($$) { +sub deleteUser { my ($self, $userID) = @_; croak "deleteUser: requires 1 argument" @@ -802,8 +856,6 @@ croak "deleteUser: argument 1 must contain a user_id" unless defined $userID; - #$self->deleteUserSet($userID, $_) - # foreach $self->listUserSets($userID); $self->deleteUserSet($userID, undef); $self->deletePassword($userID); $self->deletePermissionLevel($userID); @@ -811,16 +863,28 @@ return $self->{user}->delete($userID); } +=back + +=cut + ################################################################################ # set functions ################################################################################ +=head2 Global Set Methods + +FIXME: write this + +=over + +=cut + sub newGlobalSet { - my ($self, $prototype) = @_; - return $self->{set}->{record}->new($prototype); + my ($self, @prototype) = @_; + return $self->{set}->{record}->new(@prototype); } -sub listGlobalSets($) { +sub listGlobalSets { my ($self) = @_; croak "listGlobalSets: requires 0 arguments" @@ -830,22 +894,23 @@ $self->{set}->list(undef); } -sub addGlobalSet($$) { +sub addGlobalSet { my ($self, $GlobalSet) = @_; croak "addGlobalSet: requires 1 argument" unless @_ == 2; croak "addGlobalSet: argument 1 must be of type ", $self->{set}->{record} unless ref $GlobalSet eq $self->{set}->{record}; - croak "addGlobalSet: global set exists (perhaps you meant to use putGlobalSet?)" - if $self->{set}->exists($GlobalSet->set_id); checkKeyfields($GlobalSet); + croak "addGlobalSet: global set exists (perhaps you meant to use putGlobalSet?)" + if $self->{set}->exists($GlobalSet->set_id); + return $self->{set}->add($GlobalSet); } -sub getGlobalSet($$) { +sub getGlobalSet { my ($self, $setID) = @_; croak "getGlobalSet: requires 1 argument" @@ -858,41 +923,42 @@ =item getGlobalSets(@setIDs) -Return a list of global set records associated with the user IDs given. If there -is no record associated with a given user ID, that element of the list will be -undefined. +Return a list of global set records associated with the record IDs given. If +there is no record associated with a given record ID, that element of the list +will be undefined. =cut sub getGlobalSets { my ($self, @setIDs) = @_; - croak "getGlobalSets: requires 1 or more argument" - unless @_ >= 2; + #croak "getGlobalSets: requires 1 or more argument" + # unless @_ >= 2; foreach my $i (0 .. $#setIDs) { croak "getGlobalSets: element $i of argument list must contain a set_id" unless defined $setIDs[$i]; } - return map { $self->getGlobalSet($_) } @setIDs; + return $self->{set}->gets(map { [$_] } @setIDs); } -sub putGlobalSet($$) { +sub putGlobalSet { my ($self, $GlobalSet) = @_; croak "putGlobalSet: requires 1 argument" unless @_ == 2; croak "putGlobalSet: argument 1 must be of type ", $self->{set}->{record} unless ref $GlobalSet eq $self->{set}->{record}; - croak "putGlobalSet: global set not found (perhaps you meant to use addGlobalSet?)" - unless $self->{set}->exists($GlobalSet->set_id); checkKeyfields($GlobalSet); + croak "putGlobalSet: global set not found (perhaps you meant to use addGlobalSet?)" + unless $self->{set}->exists($GlobalSet->set_id); + return $self->{set}->put($GlobalSet); } -sub deleteGlobalSet($$) { +sub deleteGlobalSet { my ($self, $setID) = @_; croak "deleteGlobalSet: requires 1 argument" @@ -900,25 +966,33 @@ croak "deleteGlobalSet: argument 1 must contain a set_id" unless defined $setID or caller eq __PACKAGE__; - #$self->deleteUserSet($_, $setID) - # foreach $self->listSetUsers($setID); - #$self->deleteGlobalProblem($setID, $_) - # foreach $self->listGlobalProblems($setID); $self->deleteUserSet(undef, $setID); $self->deleteGlobalProblem($setID, undef); return $self->{set}->delete($setID); } +=back + +=cut + ################################################################################ # set_user functions ################################################################################ +=head2 User-Specific Set Methods + +FIXME: write this + +=over + +=cut + sub newUserSet { - my ($self, $prototype) = @_; - return $self->{set_user}->{record}->new($prototype); + my ($self, @prototype) = @_; + return $self->{set_user}->{record}->new(@prototype); } -sub listSetUsers($$) { +sub countSetUsers { my ($self, $setID) = @_; croak "listSetUsers: requires 1 argument" @@ -926,11 +1000,29 @@ croak "listSetUsers: argument 1 must contain a set_id" unless defined $setID; + # inefficient way + #return scalar $self->{set_user}->list(undef, $setID); + + # efficient way + return $self->{set_user}->count(undef, $setID); +} + +sub listSetUsers { + my ($self, $setID) = @_; + + carp "listSetUsers called in SCALAR context: use countSetUsers instead!\n" + unless wantarray; + + croak "listSetUsers: requires 1 argument" + unless @_ == 2; + croak "listSetUsers: argument 1 must contain a set_id" + unless defined $setID; + return map { $_->[0] } # extract user_id $self->{set_user}->list(undef, $setID); } -sub listUserSets($$) { +sub listUserSets { my ($self, $userID) = @_; croak "listUserSets: requires 1 argument" @@ -942,13 +1034,16 @@ $self->{set_user}->list($userID, undef); } -sub addUserSet($$) { +sub addUserSet { my ($self, $UserSet) = @_; croak "addUserSet: requires 1 argument" unless @_ == 2; croak "addUserSet: argument 1 must be of type ", $self->{set_user}->{record} unless ref $UserSet eq $self->{set_user}->{record}; + + checkKeyfields($UserSet); + croak "addUserSet: user set exists (perhaps you meant to use putUserSet?)" if $self->{set_user}->exists($UserSet->user_id, $UserSet->set_id); croak "addUserSet: user ", $UserSet->user_id, " not found" @@ -956,12 +1051,10 @@ croak "addUserSet: set ", $UserSet->set_id, " not found" unless $self->{set}->exists($UserSet->set_id); - checkKeyfields($UserSet); - return $self->{set_user}->add($UserSet); } -sub getUserSet($$$) { +sub getUserSet { my ($self, $userID, $setID) = @_; croak "getUserSet: requires 2 arguments" @@ -971,13 +1064,14 @@ croak "getUserSet: argument 2 must contain a set_id" unless defined $setID; - return $self->{set_user}->get($userID, $setID); + #return $self->{set_user}->get($userID, $setID); + return ( $self->getUserSets([$userID, $setID]) )[0]; } =item getUserSets(@userSetIDs) -Return a list of user set records associated with the user IDs given. If there -is no record associated with a given user ID, that element of the list will be +Return a list of user set records associated with the record IDs given. If there +is no record associated with a given record ID, that element of the list will be undefined. @userProblemIDs consists of references to arrays in which the first element is the user_id and the second element is the set_id. @@ -986,8 +1080,8 @@ sub getUserSets { my ($self, @userSetIDs) = @_; - croak "getUserSets: requires 1 or more argument" - unless @_ >= 2; + #croak "getUserSets: requires 1 or more argument" + # unless @_ >= 2; foreach my $i (0 .. $#userSetIDs) { croak "getUserSets: element $i of argument list must contain a pair" unless defined $userSetIDs[$i] @@ -997,16 +1091,19 @@ and defined $userSetIDs[$i]->[1]; } - return map { $self->getUserSet(@{$_}) } @userSetIDs; + return $self->{set_user}->gets(@userSetIDs); } -sub putUserSet($$) { +sub putUserSet { my ($self, $UserSet) = @_; croak "putUserSet: requires 1 argument" unless @_ == 2; croak "putUserSet: argument 1 must be of type ", $self->{set_user}->{record} unless ref $UserSet eq $self->{set_user}->{record}; + + checkKeyfields($UserSet); + croak "putUserSet: user set not found (perhaps you meant to use addUserSet?)" unless $self->{set_user}->exists($UserSet->user_id, $UserSet->set_id); croak "putUserSet: user ", $UserSet->user_id, " not found" @@ -1014,12 +1111,10 @@ croak "putUserSet: set ", $UserSet->set_id, " not found" unless $self->{set}->exists($UserSet->set_id); - checkKeyfields($UserSet); - return $self->{set_user}->put($UserSet); } -sub deleteUserSet($$$) { +sub deleteUserSet { my ($self, $userID, $setID) = @_; croak "getUserSet: requires 2 arguments" @@ -1029,22 +1124,32 @@ croak "getUserSet: argument 2 must contain a set_id" unless defined $userID or caller eq __PACKAGE__; - #$self->deleteUserProblem($userID, $setID, $_) - # foreach $self->listUserProblems($userID, $setID); $self->deleteUserProblem($userID, $setID, undef); return $self->{set_user}->delete($userID, $setID); } +=back + +=cut + ################################################################################ # problem functions ################################################################################ +=head2 Global Problem Methods + +FIXME: write this + +=over + +=cut + sub newGlobalProblem { - my ($self, $prototype) = @_; - return $self->{problem}->{record}->new($prototype); + my ($self, @prototype) = @_; + return $self->{problem}->{record}->new(@prototype); } -sub listGlobalProblems($$) { +sub listGlobalProblems { my ($self, $setID) = @_; croak "listGlobalProblems: requires 1 arguments" @@ -1056,24 +1161,25 @@ $self->{problem}->list($setID, undef); } -sub addGlobalProblem($$) { +sub addGlobalProblem { my ($self, $GlobalProblem) = @_; croak "addGlobalProblem: requires 1 argument" unless @_ == 2; croak "addGlobalProblem: argument 1 must be of type ", $self->{problem}->{record} unless ref $GlobalProblem eq $self->{problem}->{record}; + + checkKeyfields($GlobalProblem); + croak "addGlobalProblem: global problem exists (perhaps you meant to use putGlobalProblem?)" if $self->{problem}->exists($GlobalProblem->set_id, $GlobalProblem->problem_id); croak "addGlobalProblem: set ", $GlobalProblem->set_id, " not found" unless $self->{set}->exists($GlobalProblem->set_id); - checkKeyfields($GlobalProblem); - return $self->{problem}->add($GlobalProblem); } -sub getGlobalProblem($$$) { +sub getGlobalProblem { my ($self, $setID, $problemID) = @_; croak "getGlobalProblem: requires 2 arguments" @@ -1088,18 +1194,18 @@ =item getGlobalProblems(@problemIDs) -Return a list of global set records associated with the user IDs given. If there -is no record associated with a given user ID, that element of the list will be -undefined. @problemIDs consists of references to arrays in which the first -element is the set_id, and the second element is the problem_id. +Return a list of global set records associated with the record IDs given. If +there is no record associated with a given record ID, that element of the list +will be undefined. @problemIDs consists of references to arrays in which the +first element is the set_id, and the second element is the problem_id. =cut sub getGlobalProblems { my ($self, @problemIDs) = @_; - croak "getGlobalProblems: requires 1 or more argument" - unless @_ >= 2; + #croak "getGlobalProblems: requires 1 or more argument" + # unless @_ >= 2; foreach my $i (0 .. $#problemIDs) { croak "getUserSets: element $i of argument list must contain a pair" unless defined $problemIDs[$i] @@ -1109,27 +1215,52 @@ and defined $problemIDs[$i]->[1]; } - return map { $self->getGlobalProblem(@{$_}) } @problemIDs; + return $self->{problem}->gets(@problemIDs); +} + +=item getAllGlobalProblems($setID) + +Returns a list of Problem objects representing all the problems in the given +global set. When using the WW1Hash/GlobalTableEmulator schemas, this is far +more efficient than using listGlobalProblems and getGlobalProblems. + +=cut + +sub getAllGlobalProblems { + my ($self, $setID) = @_; + + croak "getAllGlobalProblems: requires 1 arguments" + unless @_ == 2; + croak "getAllGlobalProblems: argument 1 must contain a set_id" + unless defined $setID; + + if ($self->{problem}->can("getAll")) { + return $self->{problem}->getAll($setID); + } else { + my @problemIDPairs = $self->{problem}->list($setID, undef); + return $self->{problem}->gets(@problemIDPairs); + } } -sub putGlobalProblem($$) { +sub putGlobalProblem { my ($self, $GlobalProblem) = @_; croak "putGlobalProblem: requires 1 argument" unless @_ == 2; croak "putGlobalProblem: argument 1 must be of type ", $self->{problem}->{record} unless ref $GlobalProblem eq $self->{problem}->{record}; + + checkKeyfields($GlobalProblem); + croak "putGlobalProblem: global problem not found (perhaps you meant to use addGlobalProblem?)" unless $self->{problem}->exists($GlobalProblem->set_id, $GlobalProblem->problem_id); croak "putGlobalProblem: set ", $GlobalProblem->set_id, " not found" unless $self->{set}->exists($GlobalProblem->set_id); - checkKeyfields($GlobalProblem); - return $self->{problem}->put($GlobalProblem); } -sub deleteGlobalProblem($$$) { +sub deleteGlobalProblem { my ($self, $setID, $problemID) = @_; croak "deleteGlobalProblem: requires 2 arguments" @@ -1139,24 +1270,54 @@ croak "deleteGlobalProblem: argument 2 must contain a problem_id" unless defined $problemID or caller eq __PACKAGE__; - #$self->deleteUserProblem($_, $setID, $problemID) - # foreach $self->listProblemUsers($setID, $problemID); $self->deleteUserProblem(undef, $setID, $problemID); return $self->{problem}->delete($setID, $problemID); } +=back + +=cut + ################################################################################ # problem_user functions ################################################################################ +=head2 User-Specific Problem Methods + +FIXME: write this + +=over + +=cut + sub newUserProblem { - my ($self, $prototype) = @_; - return $self->{problem_user}->{record}->new($prototype); + my ($self, @prototype) = @_; + return $self->{problem_user}->{record}->new(@prototype); } -sub listProblemUsers($$$) { +sub countProblemUsers { my ($self, $setID, $problemID) = @_; + croak "countProblemUsers: requires 2 arguments" + unless @_ == 3; + croak "countProblemUsers: argument 1 must contain a set_id" + unless defined $setID; + croak "countProblemUsers: argument 2 must contain a problem_id" + unless defined $problemID; + + # the slow way + #return scalar $self->{problem_user}->list(undef, $setID, $problemID); + + # the fast way + return $self->{problem_user}->count(undef, $setID, $problemID); +} + +sub listProblemUsers { + my ($self, $setID, $problemID) = @_; + + carp "listProblemUsers called in SCALAR context: use countProblemUsers instead!\n" + unless wantarray; + croak "listProblemUsers: requires 2 arguments" unless @_ == 3; croak "listProblemUsers: argument 1 must contain a set_id" @@ -1168,7 +1329,7 @@ $self->{problem_user}->list(undef, $setID, $problemID); } -sub listUserProblems($$$) { +sub listUserProblems { my ($self, $userID, $setID) = @_; croak "listUserProblems: requires 2 arguments" @@ -1182,13 +1343,16 @@ $self->{problem_user}->list($userID, $setID, undef); } -sub addUserProblem($$) { +sub addUserProblem { my ($self, $UserProblem) = @_; croak "addUserProblem: requires 1 argument" unless @_ == 2; croak "addUserProblem: argument 1 must be of type ", $self->{problem_user}->{record} unless ref $UserProblem eq $self->{problem_user}->{record}; + + checkKeyfields($UserProblem); + croak "addUserProblem: user problem exists (perhaps you meant to use putUserProblem?)" if $self->{problem_user}->exists($UserProblem->user_id, $UserProblem->set_id, $UserProblem->problem_id); croak "addUserProblem: user set ", $UserProblem->set_id, " for user ", $UserProblem->user_id, " not found" @@ -1196,12 +1360,10 @@ croak "addUserProblem: problem ", $UserProblem->problem_id, " in set ", $UserProblem->set_id, " not found" unless $self->{problem}->exists($UserProblem->set_id, $UserProblem->problem_id); - checkKeyfields($UserProblem); - return $self->{problem_user}->add($UserProblem); } -sub getUserProblem($$$$) { +sub getUserProblem { my ($self, $userID, $setID, $problemID) = @_; croak "getUserProblem: requires 3 arguments" @@ -1213,7 +1375,7 @@ croak "getUserProblem: argument 3 must contain a problem_id" unless defined $problemID; - return $self->{problem_user}->get($userID, $setID, $problemID); + return ( $self->getUserProblems([$userID, $setID, $problemID]) )[0]; } =item getUserProblems(@userProblemIDs) @@ -1229,8 +1391,8 @@ sub getUserProblems { my ($self, @userProblemIDs) = @_; - croak "getUserProblems: requires 1 or more argument" - unless @_ >= 2; + #croak "getUserProblems: requires 1 or more argument" + # unless @_ >= 2; foreach my $i (0 .. $#userProblemIDs) { croak "getUserProblems: element $i of argument list must contain a triple" unless defined $userProblemIDs[$i] @@ -1241,16 +1403,45 @@ and defined $userProblemIDs[$i]->[2]; } - return map { $self->getUserProblem(@{$_}) } @userProblemIDs; + return $self->{problem_user}->gets(@userProblemIDs); } -sub putUserProblem($$) { +=item getAllUserProblems($userID, $setID) + +Returns a list of UserProblem objects representing all the problems in the +given set. When using the WW1Hash/GlobalTableEmulator schemas, this is far +more efficient than using listUserProblems and getUserProblems. + +=cut + +sub getAllUserProblems { + my ($self, $userID, $setID) = @_; + + croak "getAllUserProblems: requires 2 arguments" + unless @_ == 3; + croak "getAllUserProblems: argument 1 must contain a user_id" + unless defined $userID; + croak "getAllUserProblems: argument 2 must contain a set_id" + unless defined $setID; + + if ($self->{problem_user}->can("getAll")) { + return $self->{problem_user}->getAll($userID, $setID); + } else { + my @problemIDTriples = $self->{problem_user}->list($userID, $setID, undef); + return $self->{problem_user}->gets(@problemIDTriples); + } +} + +sub putUserProblem { my ($self, $UserProblem) = @_; croak "putUserProblem: requires 1 argument" unless @_ == 2; croak "putUserProblem: argument 1 must be of type ", $self->{problem_user}->{record} unless ref $UserProblem eq $self->{problem_user}->{record}; + + checkKeyfields($UserProblem); + croak "putUserProblem: user set ", $UserProblem->set_id, " for user ", $UserProblem->user_id, " not found" unless $self->{set_user}->exists($UserProblem->user_id, $UserProblem->set_id); croak "putUserProblem: user problem not found (perhaps you meant to use addUserProblem?)" @@ -1258,12 +1449,10 @@ croak "putUserProblem: problem ", $UserProblem->problem_id, " in set ", $UserProblem->set_id, " not found" unless $self->{problem}->exists($UserProblem->set_id, $UserProblem->problem_id); - checkKeyfields($UserProblem); - return $self->{problem_user}->put($UserProblem); } -sub deleteUserProblem($$$$) { +sub deleteUserProblem { my ($self, $userID, $setID, $problemID) = @_; croak "getUserProblem: requires 3 arguments" @@ -1278,20 +1467,39 @@ return $self->{problem_user}->delete($userID, $setID, $problemID); } +=back + +=cut + ################################################################################ # set+set_user functions ################################################################################ +=head2 Set Merging Methods + +These functions combine a global set and a user set to create a merged set, +which is returned. Any field that is not defined in the user set is taken from +the global set. Merged sets have the same type as user sets. + +=over + +=cut + sub getGlobalUserSet { carp "getGlobalUserSet: this method is deprecated -- use getMergedSet instead"; return shift->getMergedSet(@_); } +=item getMergedSet($userID, $setID) + +Returns a merged set record associated with the record IDs given. If there is no +record associated with a given record ID, the undefined value is returned. + +=cut + sub getMergedSet { my ($self, $userID, $setID) = @_; - #my $timer = WeBWorK::Timing->new("getMergedSet"); - croak "getMergedSet: requires 2 arguments" unless @_ == 3; croak "getMergedSet: argument 1 must contain a user_id" @@ -1299,42 +1507,23 @@ croak "getMergedSet: argument 2 must contain a set_id" unless defined $setID; - #$timer->start; - my $UserSet = $self->getUserSet($userID, $setID); - #$timer->continue("got user set"); - return unless $UserSet; - my $GlobalSet = $self->getGlobalSet($setID); - #$timer->continue("got global set"); - if ($GlobalSet) { - foreach ($UserSet->FIELDS()) { - next unless $GlobalSet->can($_); - next if $UserSet->$_(); - $UserSet->$_($GlobalSet->$_()); - } - } - #$timer->continue("merged records"); - #$timer->stop; - return $UserSet; + return ( $self->getMergedSets([$userID, $setID]) )[0]; } +=item getMegedSets(@userSetIDs) -=item geMegedSets(@userSetIDs) - - -Return a list of merged set records associated with the user IDs given. If there -is no record associated with a given user ID, that element of the list will be -undefined. @userSetIDs consists of references to arrays in which the first -element is the user_id and the second element is the set_id. - +Return a list of merged set records associated with the record IDs given. If +there is no record associated with a given record ID, that element of the list +will be undefined. @userSetIDs consists of references to arrays in which the +first element is the user_id and the second element is the set_id. =cut - sub getMergedSets { my ($self, @userSetIDs) = @_; - croak "getMergedSets: requires 1 or more argument" - unless @_ >= 2; + #croak "getMergedSets: requires 1 or more argument" + # unless @_ >= 2; foreach my $i (0 .. $#userSetIDs) { croak "getMergedSets: element $i of argument list must contain a pair" unless defined $userSetIDs[$i] @@ -1344,26 +1533,77 @@ and defined $userSetIDs[$i]->[1]; } - return map { $self->getMergedSet(@{$_}) } @userSetIDs; - + # a horrible, terrible hack ;) + if (ref $self->{set_user} eq "WeBWorK::DB::Schema::WW1Hash" + and ref $self->{set} eq "WeBWorK::DB::Schema::GlobalTableEmulator") { + #warn __PACKAGE__.": using a terrible hack.\n"; + $WeBWorK::timer->continue("DB: getsNoFilter start") if defined($WeBWorK::timer); + my @MergedSets = $self->{set_user}->getsNoFilter(@userSetIDs); + $WeBWorK::timer->continue("DB: getsNoFilter end") if defined($WeBWorK::timer); + return @MergedSets; + } + + $WeBWorK::timer->continue("DB: getUserSets start") if defined($WeBWorK::timer); + my @UserSets = $self->getUserSets(@userSetIDs); # checked + + $WeBWorK::timer->continue("DB: pull out set IDs start") if defined($WeBWorK::timer); + my @globalSetIDs = map { $_->[1] } @userSetIDs; + $WeBWorK::timer->continue("DB: getGlobalSets start") if defined($WeBWorK::timer); + my @GlobalSets = $self->getGlobalSets(@globalSetIDs); # checked + + $WeBWorK::timer->continue("DB: calc common fields start") if defined($WeBWorK::timer); + my %globalSetFields = map { $_ => 1 } $self->newGlobalSet->FIELDS; + my @commonFields = grep { exists $globalSetFields{$_} } $self->newUserSet->FIELDS; + + $WeBWorK::timer->continue("DB: merge start") if defined($WeBWorK::timer); + for (my $i = 0; $i < @UserSets; $i++) { + my $UserSet = $UserSets[$i]; + my $GlobalSet = $GlobalSets[$i]; + next unless defined $UserSet and defined $GlobalSet; + foreach my $field (@commonFields) { + next if defined $UserSet->$field; + $UserSet->$field($GlobalSet->$field); + } + } + $WeBWorK::timer->continue("DB: merge done!") if defined($WeBWorK::timer); + + return @UserSets; } +=back +=cut ################################################################################ # problem+problem_user functions ################################################################################ +=head2 Problem Merging Methods + +These functions combine a global problem and a user problem to create a merged +problem, which is returned. Any field that is not defined in the user problem is +taken from the global problem. Merged problems have the same type as user +problems. + +=over + +=cut + sub getGlobalUserProblem { carp "getGlobalUserProblem: this method is deprecated -- use getMergedProblem instead"; return shift->getMergedProblem(@_); } +=item getMergedProblem($userID, $setID, $problemID) + +Returns a merged problem record associated with the record IDs given. If there +is no record associated with a given record ID, the undefined value is returned. + +=cut + sub getMergedProblem { my ($self, $userID, $setID, $problemID) = @_; - #my $timer = WeBWorK::Timing->new("getMergedSet"); - croak "getGlobalUserSet: requires 3 arguments" unless @_ == 4; croak "getGlobalUserSet: argument 1 must contain a user_id" @@ -1373,39 +1613,24 @@ croak "getGlobalUserSet: argument 3 must contain a problem_id" unless defined $problemID; - #$timer->start; - my $UserProblem = $self->getUserProblem($userID, $setID, $problemID); - #$timer->continue("got user problem"); - return unless $UserProblem; - my $GlobalProblem = $self->getGlobalProblem($setID, $problemID); - #$timer->continue("got global problem"); - if ($GlobalProblem) { - foreach ($UserProblem->FIELDS()) { - next unless $GlobalProblem->can($_); - next if $UserProblem->$_(); - $UserProblem->$_($GlobalProblem->$_()); - } - } - #$timer->continue("merged records"); - #$timer->stop; - return $UserProblem; + return ( $self->getMergedProblems([$userID, $setID, $problemID]) )[0]; } =item getMergedProblems(@userProblemIDs) -Return a list of merged set records associated with the user IDs given. If there -is no record associated with a given user ID, that element of the list will be -undefined. @userProblemIDs consists of references to arrays in which the first -element is the user_id, the second element is the set_id, and the third element -is the problem_id. +Return a list of merged problem records associated with the record IDs given. If +there is no record associated with a given record ID, that element of the list +will be undefined. @userProblemIDs consists of references to arrays in which the +first element is the user_id, the second element is the set_id, and the third +element is the problem_id. =cut sub getMergedProblems { my ($self, @userProblemIDs) = @_; - croak "getMergedProblems: requires 1 or more argument" - unless @_ >= 2; + #croak "getMergedProblems: requires 1 or more argument" + # unless @_ >= 2; foreach my $i (0 .. $#userProblemIDs) { croak "getMergedProblems: element $i of argument list must contain a triple" unless defined $userProblemIDs[$i] @@ -1416,20 +1641,48 @@ and defined $userProblemIDs[$i]->[2]; } - return map { $self->getMergedProblem(@{$_}) } @userProblemIDs; + $WeBWorK::timer->continue("DB: getUserProblems start") if defined($WeBWorK::timer); + my @UserProblems = $self->getUserProblems(@userProblemIDs); # checked + + $WeBWorK::timer->continue("DB: pull out set/problem IDs start") if defined($WeBWorK::timer); + my @globalProblemIDs = map { [ $_->[1], $_->[2] ] } @userProblemIDs; + $WeBWorK::timer->continue("DB: getGlobalProblems start") if defined($WeBWorK::timer); + my @GlobalProblems = $self->getGlobalProblems(@globalProblemIDs); # checked + + $WeBWorK::timer->continue("DB: calc common fields start") if defined($WeBWorK::timer); + my %globalProblemFields = map { $_ => 1 } $self->newGlobalProblem->FIELDS; + my @commonFields = grep { exists $globalProblemFields{$_} } $self->newUserProblem->FIELDS; + + $WeBWorK::timer->continue("DB: merge start") if defined($WeBWorK::timer); + for (my $i = 0; $i < @UserProblems; $i++) { + my $UserProblem = $UserProblems[$i]; + my $GlobalProblem = $GlobalProblems[$i]; + next unless defined $UserProblem and defined $GlobalProblem; + foreach my $field (@commonFields) { + next if defined $UserProblem->$field; + $UserProblem->$field($GlobalProblem->$field); + } + } + $WeBWorK::timer->continue("DB: merge done!") if defined($WeBWorK::timer); + + return @UserProblems; } +=back + +=cut + ################################################################################ # debugging ################################################################################ -sub dumpDB($$) { - my ($self, $table) = @_; - return $self->{$table}->dumpDB(); -} +#sub dumpDB($$) { +# my ($self, $table) = @_; +# return $self->{$table}->dumpDB(); +#} ################################################################################ -# sanity checking +# utilities ################################################################################ sub checkKeyfields($) { @@ -1438,7 +1691,7 @@ my $value = $Record->$keyfield; croak "checkKeyfields: $keyfield is empty" unless defined $value and $value ne ""; - + if ($keyfield eq "problem_id") { croak "checkKeyfields: invalid characters in $keyfield field: $value (valid characters are [0-9])" unless $value =~ m/^\d*$/;