[system] / trunk / webwork2 / lib / WeBWorK / ContentGenerator / Instructor / ProblemSetDetail.pm Repository:
ViewVC logotype

Diff of /trunk/webwork2/lib/WeBWorK/ContentGenerator/Instructor/ProblemSetDetail.pm

Parent Directory Parent Directory | Revision Log Revision Log | View Patch Patch

Revision 2841 Revision 2901
28use CGI qw(); 28use CGI qw();
29use WeBWorK::HTML::ComboBox qw/comboBox/; 29use WeBWorK::HTML::ComboBox qw/comboBox/;
30use WeBWorK::Utils qw(readDirectory list2hash listFilesRecursive max); 30use WeBWorK::Utils qw(readDirectory list2hash listFilesRecursive max);
31use WeBWorK::DB::Record::Set; 31use WeBWorK::DB::Record::Set;
32use WeBWorK::Utils::Tasks qw(renderProblems); 32use WeBWorK::Utils::Tasks qw(renderProblems);
33use WeBWorK::Debug;
33 34
34# Important Note: the following two sets of constants may seem similar 35# Important Note: the following two sets of constants may seem similar
35# but they are functionally and semantically different 36# but they are functionally and semantically different
36 37
37# these constants determine which fields belong to what type of record 38# these constants determine which fields belong to what type of record
79 default => "", 80 default => "",
80 }, 81 },
81 open_date => { 82 open_date => {
82 name => "Opens", 83 name => "Opens",
83 type => "edit", 84 type => "edit",
84 size => "24", 85 size => "26",
85 override => "any", 86 override => "any",
86 labels => { 87 labels => {
87 0 => "None Specified", 88 0 => "None Specified",
88 "" => "None Specified", 89 "" => "None Specified",
89 }, 90 },
90 }, 91 },
91 due_date => { 92 due_date => {
92 name => "Answers Due", 93 name => "Answers Due",
93 type => "edit", 94 type => "edit",
94 size => "24", 95 size => "26",
95 override => "any", 96 override => "any",
96 labels => { 97 labels => {
97 0 => "None Specified", 98 0 => "None Specified",
98 "" => "None Specified", 99 "" => "None Specified",
99 }, 100 },
100 }, 101 },
101 answer_date => { 102 answer_date => {
102 name => "Answers Available", 103 name => "Answers Available",
103 type => "edit", 104 type => "edit",
104 size => "24", 105 size => "26",
105 override => "any", 106 override => "any",
106 labels => { 107 labels => {
107 0 => "None Specified", 108 0 => "None Specified",
108 "" => "None Specified", 109 "" => "None Specified",
109 }, 110 },
127 default => "", 128 default => "",
128 }, 129 },
129 value => { 130 value => {
130 name => "Weight", 131 name => "Weight",
131 type => "edit", 132 type => "edit",
132 size => 5, 133 size => 6,
133 override => "any", 134 override => "any",
134 }, 135 },
135 max_attempts => { 136 max_attempts => {
136 name => "Max attempts", 137 name => "Max attempts",
137 type => "edit", 138 type => "edit",
138 size => 5, 139 size => 6,
139 override => "any", 140 override => "any",
140 labels => { 141 labels => {
141 "-1" => "unlimited", 142 "-1" => "unlimited",
142 }, 143 },
143 }, 144 },
144 problem_seed => { 145 problem_seed => {
145 name => "Seed", 146 name => "Seed",
146 type => "edit", 147 type => "edit",
147 size => 5, 148 size => 6,
148 override => "one", 149 override => "one",
149 150
150 }, 151 },
151 status => { 152 status => {
152 name => "Status", 153 name => "Status",
153 type => "edit", 154 type => "edit",
154 size => 5, 155 size => 6,
155 override => "any", 156 override => "one",
156 default => 0, 157 default => 0,
157 }, 158 },
158 attempted => { 159 attempted => {
159 name => "Attempted", 160 name => "Attempted",
160 type => "hidden", 161 type => "hidden",
498 my @values = $r->param("set.$setID.$_"); 499 my @values = $r->param("set.$setID.$_");
499 my $value = $values[0] || $values[1] || ""; 500 my $value = $values[0] || $values[1] || "";
500 $r->param("set.$setID.$_", $value); 501 $r->param("set.$setID.$_", $value);
501 } 502 }
502 503
504 #####################################################################
505 # Check date information
506 #####################################################################
507
508 my ($open_date, $due_date, $answer_date);
509 my $error = 0;
503 if (defined $r->param('submit_changes')) { 510 if (defined $r->param('submit_changes')) {
504 511
512 my $od_param = $r->param("set.$setID.open_date");
513 my $dd_param = $r->param("set.$setID.due_date");
514 my $ad_param = $r->param("set.$setID.answer_date");
515 my $setRecord = $db->getGlobalSet($setID);
516
517 $open_date = $od_param ? $self->parseDateTime($od_param) : $setRecord->open_date;
518 $due_date = $dd_param ? $self->parseDateTime($dd_param) : $setRecord->due_date;
519 $answer_date = $ad_param ? $self->parseDateTime($ad_param) : $setRecord->answer_date;
520
521 if ($answer_date < $due_date || $answer_date < $open_date) {
522 $self->addbadmessage("Answers cannot be made available until on or after the due date!");
523 $error = $r->param('submit_changes');
524 }
525
526 if ($due_date < $open_date) {
527 $self->addbadmessage("Answers cannot be due until on or after the open date!");
528 $error = $r->param('submit_changes');
529 }
530
531 if ($error) {
532 $self->addbadmessage("No changes were saved!");
533 }
534 }
535
536
537 if (defined $r->param('submit_changes') && !$error) {
538
505 my $setRecord = $db->getGlobalSet($setID); 539 my $setRecord = $db->getGlobalSet($setID);
506 540
507 ##################################################################### 541 #####################################################################
508 # Save general set information (including headers) 542 # Save general set information (including headers)
509 ##################################################################### 543 #####################################################################
529 } 563 }
530 } 564 }
531 $db->putUserSet($record); 565 $db->putUserSet($record);
532 } 566 }
533 } else { 567 } else {
534
535 foreach my $field ( @{ SET_FIELDS() } ) { 568 foreach my $field ( @{ SET_FIELDS() } ) {
536 next unless canChange($forUsers, $field); 569 next unless canChange($forUsers, $field);
537 570
538 my $param = $r->param("set.$setID.$field"); 571 my $param = $r->param("set.$setID.$field");
539 $param = $properties{$field}->{default} || "" unless defined $param && $param ne ""; 572 $param = $properties{$field}->{default} || "" unless defined $param && $param ne "";
549 582
550 ##################################################################### 583 #####################################################################
551 # Save problem information 584 # Save problem information
552 ##################################################################### 585 #####################################################################
553 586
554 my @problemIDs = $db->listGlobalProblems($setID); 587 my @problemIDs = sort { $a <=> $b } $db->listGlobalProblems($setID);;
555 my @problemRecords = $db->getGlobalProblems(map { [$setID, $_] } @problemIDs); 588 my @problemRecords = $db->getGlobalProblems(map { [$setID, $_] } @problemIDs);
556 foreach my $problemRecord (@problemRecords) { 589 foreach my $problemRecord (@problemRecords) {
557 my $problemID = $problemRecord->problem_id; 590 my $problemID = $problemRecord->problem_id;
558 die "Global problem $problemID for set $setID not found." unless $problemRecord; 591 die "Global problem $problemID for set $setID not found." unless $problemRecord;
559 592
560 if ($forUsers) { 593 if ($forUsers) {
561
562 # Since we're editing for specific users, we don't allow the GlobalProblem record to be altered on that same page 594 # Since we're editing for specific users, we don't allow the GlobalProblem record to be altered on that same page
563 # So we only need to make changes to the UserProblem record and only then if we are overriding a value 595 # So we only need to make changes to the UserProblem record and only then if we are overriding a value
564 # in the GlobalProblem record or for fields unique to the UserProblem record. 596 # in the GlobalProblem record or for fields unique to the UserProblem record.
565 597
566 my @userIDs = @editForUser; 598 my @userIDs = @editForUser;
597 $record->$field($param); 629 $record->$field($param);
598 } 630 }
599 $db->putUserProblem($record) if $changed; 631 $db->putUserProblem($record) if $changed;
600 } 632 }
601 } else { 633 } else {
602
603 # Since we're editing for ALL set users, we will make changes to the GlobalProblem record. 634 # Since we're editing for ALL set users, we will make changes to the GlobalProblem record.
604 # We may also have instances where a field is unique to the UserProblem record but we want 635 # We may also have instances where a field is unique to the UserProblem record but we want
605 # all users to (at least initially) have the same value 636 # all users to (at least initially) have the same value
606 637
607 # this only edits a globalProblem record 638 # this only edits a globalProblem record
635 if (keys %useful) { 666 if (keys %useful) {
636 my @userIDs = $db->listProblemUsers($setID, $problemID); 667 my @userIDs = $db->listProblemUsers($setID, $problemID);
637 my @userProblemIDs = map { [$_, $setID, $problemID] } @userIDs; 668 my @userProblemIDs = map { [$_, $setID, $problemID] } @userIDs;
638 my @userProblemRecords = $db->getUserProblems(@userProblemIDs); 669 my @userProblemRecords = $db->getUserProblems(@userProblemIDs);
639 foreach my $record (@userProblemRecords) { 670 foreach my $record (@userProblemRecords) {
640 my $copy = $record;
641 my $changed = 0; # keep track of any changes, if none are made, avoid unnecessary db accesses 671 my $changed = 0; # keep track of any changes, if none are made, avoid unnecessary db accesses
642 foreach my $field ( @{ USER_PROBLEM_FIELDS() } ) { 672 foreach my $field ( keys %useful ) {
643 next unless canChange($forUsers, $field); 673 next unless canChange($forUsers, $field);
644 next unless $useful{$field}; 674
645
646 my $param = $r->param("problem.$problemID.$field"); 675 my $param = $r->param("problem.$problemID.$field");
647 $param = $properties{$field}->{default} || "" unless defined $param && $param ne ""; 676 $param = $properties{$field}->{default} || "" unless defined $param && $param ne "";
648 $param = $undoLabels{$field}->{$param} || $param; 677 $param = $undoLabels{$field}->{$param} || $param;
649 $changed ||= changed($record->$field, $param); 678 $changed ||= changed($record->$field, $param);
650 $record->$field($param); 679 $record->$field($param);
662 691
663 # Sets the specified header to "" so that the default file will get used. 692 # Sets the specified header to "" so that the default file will get used.
664 foreach my $header ($r->param('defaultHeader')) { 693 foreach my $header ($r->param('defaultHeader')) {
665 $setRecord->$header(""); 694 $setRecord->$header("");
666 } 695 }
667 } elsif (defined $r->param('undo_changes')) {
668 696
669 # reset all the parameters dealing with set/problem/header information 697 # Mark the specified problems as correct for all users
670 # if the current naming scheme is changed/broken, this could reek havoc 698 foreach my $problemID ($r->param('markCorrect')) {
671 # on all kinds of things 699 my @userProblemIDs = map { [$_, $setID, $problemID] } ($forUsers ? @editForUser : $db->listProblemUsers($setID, $problemID));
672 foreach my $param ($r->param) { 700 my @userProblemRecords = $db->getUserProblems(@userProblemIDs);
673 $r->param($param, "") if $param =~ /^(set|problem|header)\./; 701 foreach my $record (@userProblemRecords) {
702$self->addbadmessage($record->user_id);
703 if (defined $record && ($record->status eq "" || $record->status < 1)) {
704 $record->status(1);
705 $record->attempted(1);
706 $db->putUserProblem($record);
707 }
674 } 708 }
675 } 709 }
710 }
676 711
677# Leftover code from when there were up/down buttons 712# Leftover code from when there were up/down buttons
678 713
679# } else { 714# } else {
680# # Look for up and down buttons 715# # Look for up and down buttons
693# } 728# }
694# $index++; 729# $index++;
695# } 730# }
696# } 731# }
697 732
698
699 733
700 # handle renumbering of problems if necessary 734 # This erases any sticky fields if the user saves changes, resets the form, or reorders problems
701 print CGI::a({name=>"problems"}); 735 # It may not be obvious why this is necessary when saving changes or reordering problems
702 736 # but when the problems are reorder the param problem.1.source_file needs to be the source
703 my %newProblemNumbers = (); 737 # file of the problem that is NOW #1 and not the problem that WAS #1.
704 my $maxProblemNumber = -1; 738 unless (defined $r->param('refresh')) {
705 for my $jj ($db->listGlobalProblems($setID)) { 739
706 $newProblemNumbers{$jj} = $r->param('problem_num_' . $jj); 740 # reset all the parameters dealing with set/problem/header information
707 $maxProblemNumber = $jj if $jj > $maxProblemNumber; 741 # if the current naming scheme is changed/broken, this could reek havoc
742 # on all kinds of things
743 foreach my $param ($r->param) {
744 $r->param($param, "") if $param =~ /^(set|problem|header)\./;
708 } 745 }
709 746 }
710 my $forceRenumber = $r->param('force_renumber') || 0; 747
711 handle_problem_numbers(\%newProblemNumbers, $maxProblemNumber, $db, $setID, $forceRenumber);
712 $self->{maxProblemNumber} = $maxProblemNumber;
713} 748}
714 749
715# helper method for debugging 750# helper method for debugging
716sub debug ($) { 751sub definedness ($) {
717 my ($variable) = @_; 752 my ($variable) = @_;
718 753
719 return "undefined" unless defined $variable; 754 return "undefined" unless defined $variable;
720 return "empty" unless $variable ne ""; 755 return "empty" unless $variable ne "";
721 return $variable; 756 return $variable;
782 my $db = $r->db; 817 my $db = $r->db;
783 my $ce = $r->ce; 818 my $ce = $r->ce;
784 my $authz = $r->authz; 819 my $authz = $r->authz;
785 my $userID = $r->param('user'); 820 my $userID = $r->param('user');
786 my $urlpath = $r->urlpath; 821 my $urlpath = $r->urlpath;
787 my $courseID = $urlpath->arg("courseID"); 822 my $courseID = $urlpath->arg("courseID");
788 my $setID = $urlpath->arg("setID"); 823 my $setID = $urlpath->arg("setID");
789 my $setRecord = $db->getGlobalSet($setID); 824 my $setRecord = $db->getGlobalSet($setID) or die "No record for global set $setID.";
790 die "Global set $setID not found." unless $setRecord; 825
826 my $userRecord = $db->getUser($userID) or die "No record for user $userID.";
827 # Check permissions
828 return CGI::div({class=>"ResultsWithError"}, "You are not authorized to access the Instructor tools.")
829 unless $authz->hasPermissions($userRecord->user_id, "access_instructor_tools");
830
831 return CGI::div({class=>"ResultsWithError"}, "You are not authorized to modify problems.")
832 unless $authz->hasPermissions($userRecord->user_id, "modify_problem_sets");
833
791 my @editForUser = $r->param('editForUser'); 834 my @editForUser = $r->param('editForUser');
792 835
836 # Check that every user that we're editing for has a valid UserSet
837 my @assignedUsers;
838 my @unassignedUsers;
839 if (scalar @editForUser) {
840 foreach my $ID (@editForUser) {
841 if ($db->getUserSet($ID, $setID)) {
842 unshift @assignedUsers, $ID;
843 } else {
844 unshift @unassignedUsers, $ID;
845 }
846 }
847 @editForUser = @assignedUsers;
848 $r->param("editForUser", \@editForUser);
849
850 if (scalar @editForUser && scalar @unassignedUsers) {
851 print CGI::div({class=>"ResultsWithError"}, "The following users are NOT assigned to this set and will be ignored: " . CGI::b(join(", ", @unassignedUsers)));
852 } elsif (scalar @editForUser == 0) {
853 print CGI::div({class=>"ResultsWithError"}, "None of the selected users are assigned to this set: " . CGI::b(join(", ", @unassignedUsers)));
854 print CGI::div({class=>"ResultsWithError"}, "Global set data will be shown instead of user specific data");
855 }
856 }
857
793 # some useful booleans 858 # some useful booleans
794 my $forUsers = scalar(@editForUser); 859 my $forUsers = scalar(@editForUser);
795 my $forOneUser = $forUsers == 1; 860 my $forOneUser = $forUsers == 1;
796 861
797 # If you're editing for users, initially they're records will be different but 862 # If you're editing for users, initially their records will be different but
798 # if you make any changes to them they will be the same. 863 # if you make any changes to them they will be the same.
799 # if you're editing for one user, the problems shown should be his/hers 864 # if you're editing for one user, the problems shown should be his/hers
800 my $userToShow = $forUsers ? $editForUser[0] : $userID; 865 my $userToShow = $forUsers ? $editForUser[0] : $userID;
801
802 # Check permissions
803 return CGI::div({class=>"ResultsWithError"}, "You are not authorized to access the Instructor tools.")
804 unless $authz->hasPermissions($r->param("user"), "access_instructor_tools");
805 866
806 return CGI::div({class=>"ResultsWithError"}, "You are not authorized to modify problems.")
807 unless $authz->hasPermissions($r->param("user"), "modify_problem_sets");
808
809
810
811
812 my $userCount = $db->listUsers(); 867 my $userCount = $db->listUsers();
813 my $setCount = $db->listGlobalSets() if $forOneUser; 868 my $setCount = $db->listGlobalSets() if $forOneUser;
814 my $setUserCount = $db->countSetUsers($setID); 869 my $setUserCount = $db->countSetUsers($setID);
815 my $userSetCount = $db->countUserSets($editForUser[0]) if $forOneUser; 870 my $userSetCount = $db->countUserSets($editForUser[0]) if $forOneUser;
871
872
816 my $editUsersAssignedToSetURL = $self->systemLink( 873 my $editUsersAssignedToSetURL = $self->systemLink(
817 $urlpath->newFromModule( 874 $urlpath->newFromModule(
818 "WeBWorK::ContentGenerator::Instructor::UsersAssignedToSet", 875 "WeBWorK::ContentGenerator::Instructor::UsersAssignedToSet",
819 courseID => $courseID, setID => $setID)); 876 courseID => $courseID, setID => $setID));
820 my $editSetsAssignedToUserURL = $self->systemLink( 877 my $editSetsAssignedToUserURL = $self->systemLink(
840 } 897 }
841 } else { 898 } else {
842 print CGI::p($userCountMessage); 899 print CGI::p($userCountMessage);
843 } 900 }
844 901
845 902 # handle renumbering of problems if necessary
903 print CGI::a({name=>"problems"});
904
905 my %newProblemNumbers = ();
906 my $maxProblemNumber = -1;
907 for my $jj (sort { $a <=> $b } $db->listGlobalProblems($setID)) {
908 $newProblemNumbers{$jj} = $r->param('problem_num_' . $jj);
909 $maxProblemNumber = $jj if $jj > $maxProblemNumber;
910 }
911
912 my $forceRenumber = $r->param('force_renumber') || 0;
913 handle_problem_numbers(\%newProblemNumbers, $maxProblemNumber, $db, $setID, $forceRenumber) unless defined $r->param('undo_changes');
846 914
847 my %properties = %{ FIELD_PROPERTIES() }; 915 my %properties = %{ FIELD_PROPERTIES() };
848 916
849 my %display_modes = %{WeBWorK::PG::DISPLAY_MODES()}; 917 my %display_modes = %{WeBWorK::PG::DISPLAY_MODES()};
850 my @active_modes = grep { exists $display_modes{$_} } @{$r->ce->{pg}->{displayModes}}; 918 my @active_modes = grep { exists $display_modes{$_} } @{$r->ce->{pg}->{displayModes}};
995 1063
996 ##################################################################### 1064 #####################################################################
997 # Display problem information 1065 # Display problem information
998 ##################################################################### 1066 #####################################################################
999 1067
1000 my @problemIDList = $db->listGlobalProblems($setID); 1068 my @problemIDList = sort { $a <=> $b } $db->listGlobalProblems($setID);
1001 if (scalar @problemIDList) { 1069 if (scalar @problemIDList) {
1002
1003 my $maxProblemNumber = $self->{maxProblemNumber};
1004 1070
1005 print CGI::start_table({border=>1, cellpadding=>4}); 1071 print CGI::start_table({border=>1, cellpadding=>4});
1006 print CGI::Tr({}, CGI::th({}, [ 1072 print CGI::Tr({}, CGI::th({}, [
1007 "Problems", 1073 "Problems",
1008 "Data", 1074 "Data",
1019 if ($forOneUser) { 1085 if ($forOneUser) {
1020 $problemRecord = $db->getMergedProblem($editForUser[0], $setID, $problemID); 1086 $problemRecord = $db->getMergedProblem($editForUser[0], $setID, $problemID);
1021 } else { 1087 } else {
1022 $problemRecord = $db->getGlobalProblem($setID, $problemID); 1088 $problemRecord = $db->getGlobalProblem($setID, $problemID);
1023 } 1089 }
1090
1091#$self->addgoodmessage("");
1092#$self->addbadmessage($problemRecord->toString());
1093
1024 1094
1025 my $editProblemPage = $urlpath->new(type => 'instructor_problem_editor_withset_withproblem', args => { courseID => $courseID, setID => $setID, problemID => $problemID }); 1095 my $editProblemPage = $urlpath->new(type => 'instructor_problem_editor_withset_withproblem', args => { courseID => $courseID, setID => $setID, problemID => $problemID });
1026 my $editProblemLink = $self->systemLink($editProblemPage, params => { make_local_copy => 0 }); 1096 my $editProblemLink = $self->systemLink($editProblemPage, params => { make_local_copy => 0 });
1027 1097
1028 # FIXME: should we have an "act as" type link here when editing for multiple users? 1098 # FIXME: should we have an "act as" type link here when editing for multiple users?
1059 CGI::Tr({}, CGI::td({}, problem_number_popup($problemID, $maxProblemNumber))) . 1129 CGI::Tr({}, CGI::td({}, problem_number_popup($problemID, $maxProblemNumber))) .
1060 CGI::Tr({}, CGI::td({}, CGI::a({href => $editProblemLink}, "Edit it"))) . 1130 CGI::Tr({}, CGI::td({}, CGI::a({href => $editProblemLink}, "Edit it"))) .
1061 CGI::Tr({}, CGI::td({}, CGI::a({href => $viewProblemLink}, "Try it" . ($forOneUser ? " (as $editForUser[0])" : "")))) . 1131 CGI::Tr({}, CGI::td({}, CGI::a({href => $viewProblemLink}, "Try it" . ($forOneUser ? " (as $editForUser[0])" : "")))) .
1062 ($forUsers ? "" : CGI::Tr({}, CGI::td({}, CGI::checkbox({name => "deleteProblem", value => $problemID, label => "Delete it?"})))) . 1132 ($forUsers ? "" : CGI::Tr({}, CGI::td({}, CGI::checkbox({name => "deleteProblem", value => $problemID, label => "Delete it?"})))) .
1063# CGI::Tr({}, CGI::td({}, "Delete&nbsp;it?" . CGI::input({type => "checkbox", name => "deleteProblem", value => $problemID}))) . 1133# CGI::Tr({}, CGI::td({}, "Delete&nbsp;it?" . CGI::input({type => "checkbox", name => "deleteProblem", value => $problemID}))) .
1134 ($forOneUser ? "" : CGI::Tr({}, CGI::td({}, CGI::checkbox({name => "markCorrect", value => $problemID, label => "Mark Correct?"})))) .
1064 CGI::end_table(), 1135 CGI::end_table(),
1065 $self->FieldTable($userToShow, $setID, $problemID), 1136 $self->FieldTable($userToShow, $setID, $problemID),
1066# A comprehensive list of problems is just TOO big to be handled well 1137# A comprehensive list of problems is just TOO big to be handled well
1067# comboBox({ 1138# comboBox({
1068# name => "set.$setID.$problemID", 1139# name => "set.$setID.$problemID",
1087 label=> "Force problems to be numbered consecutively from one", 1158 label=> "Force problems to be numbered consecutively from one",
1088 name=>"force_renumber", value=>"1"}), 1159 name=>"force_renumber", value=>"1"}),
1089 1160
1090 CGI::br(); 1161 CGI::br();
1091 print CGI::input({type=>"submit", name=>"submit_changes", value=>"Save Changes"}); 1162 print CGI::input({type=>"submit", name=>"submit_changes", value=>"Save Changes"});
1163 print CGI::input({type=>"submit", name=>"handle_numbers", value=>"Reorder problems only"}) . "(Any unsaved changes will be lost.)";
1092 print CGI::p(<<HERE); 1164 print CGI::p(<<HERE);
1093Any time problem numbers are intentionally changed, the problems will 1165Any time problem numbers are intentionally changed, the problems will
1094always be renumbered consecutively, starting from one. When deleting 1166always be renumbered consecutively, starting from one. When deleting
1095problems, gaps will be left in the numbering unless the box above is 1167problems, gaps will be left in the numbering unless the box above is
1096checked. 1168checked.

Legend:
Removed from v.2841  
changed lines
  Added in v.2901

aubreyja at gmail dot com
ViewVC Help
Powered by ViewVC 1.0.9