Parent Directory
|
Revision Log
ordering, normalizers, booleans, documentation. details: * changed order of table classes so that the has_a() part of a relationship occurs before the has_many() part. * added WeBWorK::DBv3::NormalizerMixin, simiar to Class::Trigger, to manage normalizer subroutines for per-column normalization. * overloaded normalize_column_values to all normalizers for changed fields. * implemented predefined has_a_boolean() normalizer definition. * defined boolean fields in tables using has_a_boolean(). * added/clarified docs. still to do: * add inflators/deflators for durations * add triggers for setting creation dates
1 ################################################################################ 2 # WeBWorK Online Homework Delivery System 3 # Copyright © 2000-2003 The WeBWorK Project, http://openwebwork.sf.net/ 4 # $CVSHeader: webwork2/lib/WeBWorK/DBv3.pm,v 1.2 2004/11/25 05:50:01 sh002i Exp $ 5 # 6 # This program is free software; you can redistribute it and/or modify it under 7 # the terms of either: (a) the GNU General Public License as published by the 8 # Free Software Foundation; either version 2, or (at your option) any later 9 # version, or (b) the "Artistic License" which comes with this package. 10 # 11 # This program is distributed in the hope that it will be useful, but WITHOUT 12 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 13 # FOR A PARTICULAR PURPOSE. See either the GNU General Public License or the 14 # Artistic License for more details. 15 ################################################################################ 16 17 package WeBWorK::DBv3; 18 use base 'Class::DBI'; 19 use WeBWorK::DBv3::NormalizerMixin; 20 21 =head1 NAME 22 23 WeBWorK::DBv3 - Class::DBI interface to WWDBv3. 24 25 =head1 SYNOPSIS 26 27 use WeBWorK::DBv3; 28 29 my $wwdbv3_settings = { 30 dsn => "dbi:mysql:wwdbv3", 31 user => "wwdbv3", 32 pass => "SeCrEt", 33 attr => { }, # optional 34 upgrade_lock => "/path/to/wwdbv3_upgrade.lock", # prevent concurrent schema upgrades 35 }; 36 37 WeBWorK::DBv3::init($wwdbv3_settings); 38 39 # --- any time after init() as been called... --- 40 41 my $course = WeBWorK::DBv3::Course->find({name => "Sam's course"}) 42 or die "course not found!"; 43 44 my @participants = $course->participants; 45 46 my $participant = WeBWorK::DBv3::Participant->find({login_name => "sam.hathaway"}); 47 $participant->first_name("Sam"); 48 $participant->last_name("Hathaway"); 49 $participant->update; 50 51 my @set_assignments = $participant->set_assignments; 52 53 =head1 DESCRIPTION 54 55 WeBWorK::DBv3 provides a Class::DBI-based interface to the third-generation 56 WeBWorK database. (WeBWorK::DB provided an interface to the second-generation 57 database, the first-generation database was that used by WeBWorK 1.x.) 58 59 The database schema is described at 60 <http://devel.webwork.rochester.edu/twiki/bin/view/Webwork/DatabaseSchemaV3>. 61 62 WeBWorK::DBv3 supports automatic schema upgrades by checking the value of the 63 C<db_version> record in the C<setting> table and applying SQL deltas to the 64 database. 65 66 =cut 67 68 use strict; 69 use warnings; 70 use vars qw/$dbh $dt_format/; 71 use DateTime::Format::DBI; 72 use WeBWorK::DBv3::Utils; 73 74 =head1 INITIALIZATION 75 76 The init($wwdbv3_settings) function allows the user to set up the details of the 77 database connection at runtime rather than at compile time. This lets us use 78 values from the WeBWorK::CourseEnvironment (loaded at runtime) in specifying the 79 database connection. 80 81 $wwdbv3_settings is a reference to a hash containing the following values: 82 83 dsn => The DBI data source, e.g. "dbi:mysql:wwdbv3" 84 user => The user name with which to connect. 85 pass => The password to supply to connect. 86 attr => A reference to a hash containing DBI attributes. 87 See L<DBI> for more information. 88 upgrade_lock => Path to a file which is flock()'d while performing 89 database upgrades. 90 91 =cut 92 93 sub init { 94 my ($wwdbv3_settings) = @_; 95 96 my $dsn = $wwdbv3_settings->{dsn}; 97 my $user = $wwdbv3_settings->{user}; 98 my $pass = $wwdbv3_settings->{pass}; 99 my %attr = ( 100 RootClass => "DBIx::ContextualFetch", # this is supposedly important to Class::DBI 101 RaiseError => 1, # we don't want to have to test return values 102 %{ $wwdbv3_settings->{attr} }, # allow user-specified attributes to override 103 ); 104 105 $dbh = DBI->connect_cached($dsn, $user, $pass, \%attr); 106 107 $dt_format = new DateTime::Format::DBI($dbh); 108 109 my $lockfile = $wwdbv3_settings->{upgrade_lock}; 110 upgrade_schema($dbh, $lockfile); 111 } 112 113 # override db_Main to get database handle initialized in init() above. note that 114 # Class::DBI->connection() is never called. 115 sub db_Main { 116 return $dbh; 117 } 118 119 ################################################################################ 120 121 =head1 PUBLIC CLASS::DBI EXTENSIONS 122 123 WeBWorK::DBv3 extends Class::DBI to provide several features useful to users to 124 the WWDBv3 system. 125 126 =head2 TABLE LOCKING 127 128 When using a table type that doesn't support transactions, we need to be able to 129 do table-level locks. The currently implementations are from 130 Class::DBI::Extension and are MySQL-specific. 131 132 =over 133 134 =item lock_table() 135 136 Write-lock the current table. 137 138 =cut 139 140 __PACKAGE__->set_sql(LockTable => "LOCK TABLES %s WRITE"); 141 142 sub lock_table { 143 my $class = shift; 144 $class->sql_LockTable($class->table)->execute; 145 } 146 147 =item unlock_table() 148 149 Unlock I<all> locked tables. 150 151 =cut 152 153 __PACKAGE__->set_sql(UnlockTable => "UNLOCK TABLES"); 154 155 sub unlock_table { 156 my $class = shift; 157 $class->sql_UnlockTable->execute; 158 } 159 160 =back 161 162 =cut 163 164 ################################################################################ 165 166 =head1 INTERNAL IMPROVEMENTS 167 168 WeBWorK::DBv3 extends Class::DBI to provide several features useful in the 169 definition of table classes. 170 171 =head2 DATETIME SUPPORT 172 173 The method has_a_datetime() has been defined as a shortcut to specifying that a 174 DATETIME column should be inflated to and deflated from a DateTime.pm object. 175 176 __PACKAGE__->has_a_datetime("open_date"); 177 178 =cut 179 180 sub datetime_inflate { 181 my $dt = $dt_format->parse_datetime($_[0]) or _croak("invalid date: '$_[0]'"); 182 return $dt->set_time_zone("UTC"); 183 } 184 185 sub datetime_deflate { 186 my $dt = $_[0]->clone->set_time_zone("UTC"); # clone to avoid changing timezone of original object 187 return $dt_format->format_datetime($dt); 188 } 189 190 # this declares a column to be of type DateTime and defines inflation/deflation 191 # subroutines for it 192 sub has_a_datetime { 193 my ($class, $field) = @_; 194 return unless $field; 195 196 $class->has_a( 197 $field => "DateTime", 198 inflate => \&datetime_inflate, 199 deflate => \&datetime_deflate, 200 ); 201 } 202 203 =head2 PER-COLUMN NORMALIZATION SUPPORT 204 205 WeBWorK::DBv3 adds per-column normalization support to Class::DBI via 206 WeBWorK::DBv3::NormalizerMixin. The interface and implementation are similar to 207 that of Class::DBI triggers (via Class::Trigger). 208 209 To add a normalizer to a field: 210 211 __PACKAGE__->add_normalizer(field => \&normalizer_sub); 212 213 &normalizer_sub takes one argument, the value to be normalized. It should return 214 the normalized value. For example: 215 216 sub bool_normalizer { $_[0] ? 1 : 0 } 217 WeBWorK::DBv3::Course->add_normalizer(visible => \&bool_normalizer); 218 219 Like triggers, multiple normalizers can be added for a single field. However, 220 you cannot specify the order in which they will be run. 221 222 =cut 223 224 sub normalize_column_values { 225 my ($self, $column_values) = @_; 226 227 my @errors; 228 229 foreach my $column (keys %$column_values) { 230 #warn "callig normalizers for column '$column'.\n"; 231 eval { $self->call_normalizer($column_values, $column) }; 232 push @errors, $column => $@ if $@; 233 } 234 235 return unless @errors; 236 $self->_croak( 237 "normalize_column_values error: " . join(" ", @errors), 238 method => "normalize_column_values", 239 data => { @errors }, 240 ); 241 } 242 243 =head3 PREDEFINED NORMALIZERS 244 245 Several normalizers are conveniently predefined using a syntax similar to that 246 of has_a() relationship declarations. 247 248 =over 249 250 =item has_a_boolean($field) 251 252 True values will be normalized to C<1>, false values to C<0>. 253 254 =cut 255 256 sub bool_normalizer { $_[0] ? 1 : 0 } 257 sub has_a_boolean { 258 my ($class, $field) = @_; 259 return unless $field; 260 261 $class->add_normalizer($field => \&bool_normalizer); 262 } 263 264 =back 265 266 =cut 267 268 ################################################################################ 269 # Table classes: each table in the database is a subclass of WeBWorK::DBv3. 270 # (http://devel.webwork.rochester.edu/twiki/bin/view/Webwork/DatabaseSchemaV3) 271 # 272 # These are in the reverse order from the order in DatabaseSchemaV3, to ensure 273 # that the has_a() part of a relationship occurs before the has_many() part. 274 # 275 # From C<Class::DBI/has_many>: 276 # 277 # When setting up the relationship we examine the foreign class's has_a() 278 # declarations to discover which of its columns reference our class. (Note that 279 # because this happens at compile time, if the foreign class is defined in the 280 # same file, the class with the has_a() must be defined earlier than the class 281 # with the has_many(). If the classes are in different files, Class::DBI should 282 # be able to do the right thing). 283 ################################################################################ 284 285 package WeBWorK::DBv3::ProblemAttempt; 286 use base 'WeBWorK::DBv3'; 287 288 __PACKAGE__->table("problem_attempt"); 289 __PACKAGE__->columns(All => qw/id problem_version creation_date score data/); 290 291 __PACKAGE__->has_a(problem_version => "WeBWorK::DBv3::ProblemVersion"); 292 __PACKAGE__->has_a_datetime("creation_date"); 293 294 # FIXME need trigger to set creation_date 295 296 ################################################################################ 297 298 package WeBWorK::DBv3::ProblemVersion; 299 use base 'WeBWorK::DBv3'; 300 301 __PACKAGE__->table("problem_version"); 302 __PACKAGE__->columns(All => qw/id set_version problem_assignment creation_date 303 source_file seed/); 304 305 __PACKAGE__->has_a(set_version => "WeBWorK::DBv3::SetVersion"); 306 __PACKAGE__->has_a(problem_assignment => "WeBWorK::DBv3::ProblemAssignment"); 307 __PACKAGE__->has_a_datetime("creation_date"); 308 309 __PACKAGE__->has_many(problem_attempts => "WeBWorK::DBv3::ProblemAttempt"); 310 311 # FIXME need trigger to set creation_date 312 313 ################################################################################ 314 315 package WeBWorK::DBv3::SetVersion; 316 use base 'WeBWorK::DBv3'; 317 318 __PACKAGE__->table("set_version"); 319 __PACKAGE__->columns(All => qw/id set_assignment problem_order creation_date/); 320 321 __PACKAGE__->has_a(set_assignment => "WeBWorK::DBv3::SetAssignment"); 322 __PACKAGE__->has_a_datetime("creation_date"); 323 324 __PACKAGE__->has_many(problem_versions => "WeBWorK::DBv3::ProblemVersion"); 325 326 # FIXME need trigger to set creation_date 327 328 sub problem_order_list { 329 my ($self, @problem_order) = @_; 330 if (@problem_order) { 331 return $self->problem_order(join(",", @problem_order)); 332 } else { 333 return split(",", $self->problem_order); 334 } 335 } 336 337 ################################################################################ 338 339 package WeBWorK::DBv3::ProblemOverride; 340 use base 'WeBWorK::DBv3'; 341 342 __PACKAGE__->table("problem_override"); 343 __PACKAGE__->columns(All => qw/id abstract_problem section recitation 344 participant source_type source_file source_group_set_id weight 345 max_attempts_per_version version_creation_interval versions_per_interval 346 version_due_date_offset version_answer_date_offset/); 347 348 __PACKAGE__->has_a(abstract_problem => "WeBWorK::DBv3::AbstractProblem"); 349 __PACKAGE__->has_a(section => "WeBWorK::DBv3::Section"); 350 __PACKAGE__->has_a(recitation => "WeBWorK::DBv3::Recitation"); 351 __PACKAGE__->has_a(participant => "WeBWorK::DBv3::Participant"); 352 353 # FIXME need to make version_due_date_offset/version_answer_date_offset 354 # DateTime::Offset objects 355 356 ################################################################################ 357 358 package WeBWorK::DBv3::SetOverride; 359 use base 'WeBWorK::DBv3'; 360 361 __PACKAGE__->table("set_override"); 362 __PACKAGE__->columns(All => qw/id abstract_set section recitation participant 363 set_header hardcopy_header open_date due_date answer_date published 364 problem_order reorder_type reorder_subset_size atomicity 365 max_attempts_per_version version_creation_interval versions_per_interval 366 version_due_date_offset version_answer_date_offset/); 367 368 __PACKAGE__->has_a(abstract_set => "WeBWorK::DBv3::AbstractSet"); 369 __PACKAGE__->has_a(section => "WeBWorK::DBv3::Section"); 370 __PACKAGE__->has_a(recitation => "WeBWorK::DBv3::Recitation"); 371 __PACKAGE__->has_a(participant => "WeBWorK::DBv3::Participant"); 372 373 __PACKAGE__->has_a_datetime("open_date"); 374 __PACKAGE__->has_a_datetime("due_date"); 375 __PACKAGE__->has_a_datetime("answer_date"); 376 377 # FIXME need to make version_due_date_offset/version_answer_date_offset 378 # DateTime::Offset objects 379 380 sub problem_order_list { 381 my ($self, @problem_order) = @_; 382 if (@problem_order) { 383 return $self->problem_order(join(",", @problem_order)); 384 } else { 385 return split(",", $self->problem_order); 386 } 387 } 388 389 ################################################################################ 390 391 package WeBWorK::DBv3::ProblemAssignment; 392 use base 'WeBWorK::DBv3'; 393 394 __PACKAGE__->table("problem_assignment"); 395 __PACKAGE__->columns(All => qw/id set_assignment abstract_problem source_file/); 396 397 __PACKAGE__->has_a(set_assignment => "WeBWorK::DBv3::SetAssignment"); 398 __PACKAGE__->has_a(abstract_problem => "WeBWorK::DBv3::AbstractProblem"); 399 400 __PACKAGE__->has_many(problem_overrides => "WeBWorK::DBv3::ProblemOverride"); 401 __PACKAGE__->has_many(problem_versions => "WeBWorK::DBv3::ProblemVersion"); 402 403 ################################################################################ 404 405 package WeBWorK::DBv3::SetAssignment; 406 use base 'WeBWorK::DBv3'; 407 408 __PACKAGE__->table("set_assignment"); 409 __PACKAGE__->columns(All => qw/id abstract_set participant problem_order/); 410 411 __PACKAGE__->has_a(abstract_set => "WeBWorK::DBv3::AbstractSet"); 412 __PACKAGE__->has_a(participant => "WeBWorK::DBv3::Participant"); 413 414 __PACKAGE__->has_many(problem_assignments => "WeBWorK::DBv3::ProblemAssignment"); 415 __PACKAGE__->has_many(set_overrides => "WeBWorK::DBv3::SetOverride"); 416 __PACKAGE__->has_many(set_versions => "WeBWorK::DBv3::SetVersion"); 417 418 sub problem_order_list { 419 my ($self, @problem_order) = @_; 420 if (@problem_order) { 421 return $self->problem_order(join(",", @problem_order)); 422 } else { 423 return split(",", $self->problem_order); 424 } 425 } 426 427 ################################################################################ 428 429 package WeBWorK::DBv3::AbstractProblem; 430 use base 'WeBWorK::DBv3'; 431 432 __PACKAGE__->table("abstract_problem"); 433 __PACKAGE__->columns(All => qw/id abstract_set name source_type source_file 434 source_group_set_id source_group_select_time weight max_attempts_per_version 435 version_creation_interval versions_per_interval version_due_date_offset 436 version_answer_date_offset/); 437 438 __PACKAGE__->has_a(abstract_set => "WeBWorK::DBv3::AbstractSet"); 439 440 __PACKAGE__->has_many(problem_assignments => "WeBWorK::DBv3::ProblemAssignment"); 441 442 # FIXME need to make version_due_date_offset/version_answer_date_offset 443 # DateTime::Offset objects 444 445 ################################################################################ 446 447 package WeBWorK::DBv3::AbstractSet; 448 use base 'WeBWorK::DBv3'; 449 450 __PACKAGE__->table("abstract_set"); 451 __PACKAGE__->columns(All => qw/id course name set_header problem_header 452 open_date due_date answer_date published problem_order reorder_type 453 reorder_subset_size reorder_time atomicity max_attempts_per_version 454 version_creation_interval versions_per_interval version_due_date_offset 455 version_answer_date_offset/); 456 457 __PACKAGE__->has_a(course => "WeBWorK::DBv3::Course"); 458 __PACKAGE__->has_a_datetime("open_date"); 459 __PACKAGE__->has_a_datetime("due_date"); 460 __PACKAGE__->has_a_datetime("answer_date"); 461 __PACKAGE__->has_a_boolean("published"); 462 463 __PACKAGE__->has_many(abstract_problems => "WeBWorK::DBv3::AbstractProblem"); 464 __PACKAGE__->has_many(set_assignments => "WeBWorK::DBv3::SetAssignment"); 465 466 # FIXME need to make version_due_date_offset/version_answer_date_offset 467 # DateTime::Offset objects 468 469 sub problem_order_list { 470 my ($self, @problem_order) = @_; 471 if (@problem_order) { 472 return $self->problem_order(join(",", @problem_order)); 473 } else { 474 return split(",", $self->problem_order); 475 } 476 } 477 478 ################################################################################ 479 480 package WeBWorK::DBv3::Participant; 481 use base 'WeBWorK::DBv3'; 482 483 __PACKAGE__->table("participant"); 484 __PACKAGE__->columns(All => qw/id course user status role section recitation 485 last_access comment/); 486 487 __PACKAGE__->has_a(course => "WeBWorK::DBv3::Course"); 488 __PACKAGE__->has_a(user => "WeBWorK::DBv3::User"); 489 __PACKAGE__->has_a(status => "WeBWorK::DBv3::Status"); 490 __PACKAGE__->has_a(role => "WeBWorK::DBv3::Role"); 491 __PACKAGE__->has_a(section => "WeBWorK::DBv3::Section"); 492 __PACKAGE__->has_a(recitation => "WeBWorK::DBv3::Recitation"); 493 494 __PACKAGE__->has_many(set_assignments => "WeBWorK::DBv3::SetAssignment"); 495 __PACKAGE__->has_many(set_overrides => "WeBWorK::DBv3::SetOverride"); 496 __PACKAGE__->has_many(problem_overrides => "WeBWorK::DBv3::ProblemOverride"); 497 498 ################################################################################ 499 500 package WeBWorK::DBv3::Recitation; 501 use base 'WeBWorK::DBv3'; 502 503 __PACKAGE__->table("recitation"); 504 __PACKAGE__->columns(All => qw/id course name/); 505 506 __PACKAGE__->has_a(course => "WeBWorK::DBv3::Course"); 507 508 __PACKAGE__->has_many(participants => "WeBWorK::DBv3::Participant"); 509 __PACKAGE__->has_many(set_overrides => "WeBWorK::DBv3::SetOverride"); 510 __PACKAGE__->has_many(problem_overrides => "WeBWorK::DBv3::ProblemOverride"); 511 512 ################################################################################ 513 514 package WeBWorK::DBv3::Section; 515 use base 'WeBWorK::DBv3'; 516 517 __PACKAGE__->table("section"); 518 __PACKAGE__->columns(All => qw/id course name/); 519 520 __PACKAGE__->has_a(course => "WeBWorK::DBv3::Course"); 521 522 __PACKAGE__->has_many(participants => "WeBWorK::DBv3::Participant"); 523 __PACKAGE__->has_many(set_overrides => "WeBWorK::DBv3::SetOverride"); 524 __PACKAGE__->has_many(problem_overrides => "WeBWorK::DBv3::ProblemOverride"); 525 526 ################################################################################ 527 528 package WeBWorK::DBv3::Role; 529 use base 'WeBWorK::DBv3'; 530 531 __PACKAGE__->table("role"); 532 __PACKAGE__->columns(All => qw/id course name privs/); 533 534 __PACKAGE__->has_a(course => "WeBWorK::DBv3::Course"); 535 536 __PACKAGE__->has_many(participants => "WeBWorK::DBv3::Participant"); 537 538 sub priv_list { 539 my ($self, @privs) = @_; 540 if (@privs) { 541 return $self->privs(join(",", @privs)); 542 } else { 543 return split(",", $self->privs); 544 } 545 } 546 547 ################################################################################ 548 549 package WeBWorK::DBv3::Status; 550 use base 'WeBWorK::DBv3'; 551 552 __PACKAGE__->table("status"); 553 __PACKAGE__->columns(All => qw/id course name allow_course_access 554 include_in_assignment include_in_stats include_in_scoring/); 555 556 __PACKAGE__->has_a(course => "WeBWorK::DBv3::Course"); 557 __PACKAGE__->has_a_boolean("allow_course_access"); 558 __PACKAGE__->has_a_boolean("include_in_assignment"); 559 __PACKAGE__->has_a_boolean("include_in_stats"); 560 __PACKAGE__->has_a_boolean("include_in_scoring"); 561 562 __PACKAGE__->has_many(participants => "WeBWorK::DBv3::Participant"); 563 564 ################################################################################ 565 566 package WeBWorK::DBv3::User; 567 use base 'WeBWorK::DBv3'; 568 569 __PACKAGE__->table("user"); 570 __PACKAGE__->columns(All => qw/id first_name last_name email_address student_id 571 login_id password display_mode show_old_answers/); 572 573 __PACKAGE__->has_a_boolean("show_old_answers"); 574 575 __PACKAGE__->has_many(participants => "WeBWorK::DBv3::Participant"); 576 577 ################################################################################ 578 579 package WeBWorK::DBv3::Course; 580 use base 'WeBWorK::DBv3'; 581 582 __PACKAGE__->table("course"); 583 __PACKAGE__->columns(All => qw/id name visible locked archived/); 584 585 __PACKAGE__->has_a_boolean("visible"); 586 __PACKAGE__->has_a_boolean("locked"); 587 __PACKAGE__->has_a_boolean("archived"); 588 589 __PACKAGE__->has_many(statuses => "WeBWorK::DBv3::Status"); 590 __PACKAGE__->has_many(roles => "WeBWorK::DBv3::Role"); 591 __PACKAGE__->has_many(sections => "WeBWorK::DBv3::Section"); 592 __PACKAGE__->has_many(recitations => "WeBWorK::DBv3::Recitation"); 593 __PACKAGE__->has_many(participants => "WeBWorK::DBv3::Participant"); 594 __PACKAGE__->has_many(abstract_sets => "WeBWorK::DBv3::AbstractSet"); 595 596 ################################################################################ 597 598 package WeBWorK::DBv3::EquationCache; 599 use base 'WeBWorK::DBv3'; 600 601 __PACKAGE__->table("equation_cache"); 602 __PACKAGE__->columns(All => qw/id tex width height depth/); 603 604 ################################################################################ 605 606 package WeBWorK::DBv3::Setting; 607 use base 'WeBWorK::DBv3'; 608 609 __PACKAGE__->table("setting"); 610 __PACKAGE__->columns(All => qw/name val/); 611 612 ################################################################################ 613 614 =head1 AUTHOR 615 616 Written by Sam Hathaway, sh002i (at) math.rochester.edu. 617 618 =cut 619 620 1; 621 622 __END__ 623
| aubreyja at gmail dot com | ViewVC Help |
| Powered by ViewVC 1.0.9 |