################################################################################ # WeBWorK Online Homework Delivery System # Copyright © 2000-2003 The WeBWorK Project, http://openwebwork.sf.net/ # $CVSHeader: webwork-modperl/lib/WeBWorK/ContentGenerator.pm,v 1.111 2004/07/10 21:45:48 gage 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::ContentGenerator; =head1 NAME WeBWorK::ContentGenerator - base class for modules that generate page content. =head1 SYNOPSIS # start with a WeBWorK::Request object: $r use WeBWorK::ContentGenerator::SomeSubclass; my $cg = WeBWorK::ContentGenerator::SomeSubclass->new($r); my $result = $cg->go(); =head1 DESCRIPTION WeBWorK::ContentGenerator provides the framework for generating page content. "Content generators" are subclasses of this class which provide content for particular parts of the system. Default versions of methods used by the templating system are provided. Several useful methods are provided for rendering common output idioms and some miscellaneous utilities are provided. =cut use strict; use warnings; use Apache::Constants qw(:response); use Carp; use CGI::Pretty qw(*ul *li); use URI::Escape; use WeBWorK::Template qw(template); ################################################################################ use constant PROBLEM_SETS => "Homework Sets"; use constant OPTIONS => "Password/Email"; use constant GRADES => "Grades"; use constant LOG_OUT => "Logout"; use constant ADD_USERS => "Add Users"; use constant USER_LIST => "Class List Editor"; use constant SET_LIST => "Hmwk Sets Editor"; use constant SET_MAKER => "Library Browser"; use constant ASSIGNER => "Set Assigner"; use constant MAIL => "Email"; use constant SCORING => "Scoring Tools"; use constant STATS => "Statistics"; use constant PROGRESS =>"Student Progress"; use constant FILE_TRANSFER => "File Transfer"; # Note: Perl 5.6 doesn't seem to allow multiple definitions of constants. ############################################################################### =head1 CONSTRUCTOR =over =item new($r) Creates a new instance of a content generator. Supply a WeBWorK::Request object $r. =cut sub new { my ($invocant, $r) = @_; my $class = ref($invocant) || $invocant; my $self = { r => $r, # this is now a WeBWorK::Request ce => $r->ce(), # these three are here for db => $r->db(), # backward-compatability authz => $r->authz(), # with unconverted CGs noContent => undef, # this should get clobbered at some point }; bless $self, $class; return $self; } =back =cut ################################################################################ =head1 INVOCATION =over =item go() Generates a page, using methods from the particular subclass of ContentGenerator that is instantiated. Generatoion is broken up into several steps, to give subclasses ample control over the process. =over =item 1 go() will attempt to call the method pre_header_initialize(). This method may be implemented in subclasses which must do processing before the HTTP header is emitted. =item 2 go() will attempt to call the method header(). This method emits the HTTP header. It is defined in this class (see below), but may be overridden in subclasses which need to send different header information. For some reason, the return value of header() will be used as the result of this function, if it is defined. FIXME: figure out what the deal is with the return value of header(). If we sent a header, it's too late to set the status by returning. If we didn't, header() didn't perform its function! =item 3 At this point, go() will terminate if the request is a HEAD request or if the field $self->{noContent} contains a true value. FIXME: I don't think we'll need noContent after reply_with_redirect() is adopted by all modules. =item 4 go() then attempts to call the method initialize(). This method may be implemented in subclasses which must do processing after the HTTP header is sent but before any content is sent. =item 6 The method content() is called to send the page content to client. =back =cut sub go { my ($self) = @_; my $r = $self->r; my $ce = $r->ce; my $returnValue = OK; $self->pre_header_initialize(@_) if $self->can("pre_header_initialize"); # send a file instead of a normal reply (reply_with_file() sets this field) defined $self->{reply_with_file} and do { return $self->do_reply_with_file($self->{reply_with_file}); }; # send a Location: header instead of a normal reply (reply_with_redirect() sets this field) defined $self->{reply_with_redirect} and do { return $self->do_reply_with_redirect($self->{reply_with_redirect}); }; my $headerReturn = $self->header(@_); $returnValue = $headerReturn if defined $headerReturn; # FIXME: we won't need noContent after reply_with_redirect() is adopted return $returnValue if $r->header_only or $self->{noContent}; $self->initialize() if $self->can("initialize"); $self->content(); return $returnValue; } =item r() Returns a reference to the WeBWorK::Request object associated with this instance. =cut sub r { my ($self) = @_; return $self->{r}; } =item do_reply_with_file($fileHash) Handler for reply_with_file(), used by go(). DO NOT CALL THIS METHOD DIRECTLY. =cut sub do_reply_with_file { my ($self, $fileHash) = @_; my $r = $self->r; my $type = $fileHash->{type}; my $source = $fileHash->{source}; my $name = $fileHash->{name}; my $delete_after = $fileHash->{delete_after}; # if there was a problem, we return here and let go() worry about sending the reply return NOT_FOUND unless -e $source; return FORBIDDEN unless -r $source; # open the file now, so we can send the proper error status is we fail open my $fh, "<", $source or return SERVER_ERROR; # send our custom HTTP header $r->content_type($type); $r->header_out("Content-Disposition" => "attachment; filename=\"$name\""); $r->send_http_header; # send the file $r->send_fd($fh); # close the file and go home close $fh; if ($delete_after) { unlink $source or warn "failed to unlink $source after sending: $!"; } } =item do_reply_with_redirect($url) Handler for reply_with_redirect(), used by go(). DO NOT CALL THIS METHOD DIRECTLY. =cut sub do_reply_with_redirect { my ($self, $url) = @_; my $r = $self->r; $r->status(REDIRECT); $r->header_out(Location => $url); $r->send_http_header(); } =back =cut ################################################################################ =head1 DATA MODIFIERS Modifiers allow the caller to register a piece of data for later retrieval in a standard way. =over =item reply_with_file($type, $source, $name, $delete_after) Enables file sending mode, causing go() to send the file specified by $source to the client after calling pre_header_initialize(). The content type sent is $type, and the suggested client-side file name is $name. If $delete_after is true, $source is deleted after it is sent. Must be called before the HTTP header is sent. Usually called from pre_header_initialize(). =cut sub reply_with_file { my ($self, $type, $source, $name, $delete_after) = @_; $delete_after ||= ""; $self->{reply_with_file} = { type => $type, source => $source, name => $name, delete_after => $delete_after, }; } =item reply_with_redirect($url) Enables redirect mode, causing go() to redirect to the given URL after calling pre_header_initialize(). Must be called before the HTTP header is sent. Usually called from pre_header_initialize(). =cut sub reply_with_redirect { my ($self, $url) = @_; $self->{reply_with_redirect} = $url; } =item addmessage($message) Adds a message to the list of messages to be printed by the message() template escape handler. Must be called before the message() template escape is invoked. =cut # FIXME: we should probably sub addmessage { my ($self, $message) = @_; $self->{status_message} .= $message; } =item addgoodmessage($message) Adds a success message to the list of messages to be printed by the message() template escape handler. =cut sub addgoodmessage { my ($self, $message) = @_; $self->addmessage(CGI::div({class=>"ResultsWithoutError"}, $message)); } =item addbadmessage($message) Adds a failure message to the list of messages to be printed by the message() template escape handler. =cut sub addbadmessage { my ($self, $message) = @_; $self->addmessage(CGI::div({class=>"ResultsWithError"}, $message)); } =back =cut ################################################################################ =head1 STANDARD METHODS The following are the standard content generator methods. Some are defined here, but may be overridden in a subclass. Others are not defined unless they are defined in a subclass. =over =item pre_header_initialize() Not defined in this package. May be defined by a subclass to perform any processing that must occur before the HTTP header is sent. =cut #sub pre_header_initialize { } =item header() Defined in this package. Generates and sends a default HTTP header, specifying the "text/html" content type. =cut sub header { my $self = shift; my $r = $self->r; $r->content_type("text/html"); $r->send_http_header(); return OK; } =item initialize() Not defined in this package. May be defined by a subclass to perform any processing that must occur after the HTTP header is sent but before any content is sent. =cut #sub initialize { } =item content() Defined in this package. Print the content of the generated page. The implementation in this package uses WeBWorK::Template to define the content of the page. See WeBWorK::Template for details. If a method named templateName() exists, it it called to determine the name of the template to use. If not, the default template, "system", is used. The location of the template is looked up in the course environment. =cut sub content { my ($self) = @_; my $ce = $self->r->ce; # if the content generator specifies a custom template name, use that # field in the $ce->{templates} hash instead of "system" if it exists. my $templateName; if ($self->can("templateName")) { $templateName = $self->templateName; } else { $templateName = "system"; } $templateName = "system" unless exists $ce->{templates}->{$templateName}; template($ce->{templates}->{$templateName}, $self); } =back =cut # ------------------------------------------------------------------------------ =head2 Template escape handlers Template escape handlers are invoked when the template processor encounters a matching escape sequence in the template. The escapse sequence's arguments are passed to the methods as a reference to a hash. For more information, refer to WeBWorK::Template. The following template escapes handlers are defined here or may be defined in subclasses. For methods that are not defined in this package, the documentation defines the interface and behavior that any subclass implementation must follow. =over =item head() Not defined in this package. Any tags that should appear in the HEAD of the document. =cut #sub head { } =item info() Not defined in this package. Auxiliary information related to the content displayed in the C. =cut #sub info { } =item links() Defined in this package. Links that should appear on every page. =cut sub links { my ($self) = @_; my $r = $self->r; my $db = $r->db; my $authz = $r->authz; my $ce = $r->ce; my $urlpath = $r->urlpath; my $user = $r->param('user'); # we're linking to other places in the same course, so grab the courseID from the current path my $courseID = $urlpath->arg("courseID"); # to make things more concise my %args = ( courseID => $courseID ); my $pfx = "WeBWorK::ContentGenerator::"; my $sets = $urlpath->newFromModule("${pfx}ProblemSets", %args); my $options = $urlpath->newFromModule("${pfx}Options", %args); my $grades = $urlpath->newFromModule("${pfx}Grades", %args); my $logout = $urlpath->newFromModule("${pfx}Logout", %args); print "\n\n"; # only users with appropriate permissions can report bugs print CGI::p(CGI::a({style=>"font-size:larger", href=>$ce->{webworkURLs}{bugReporter}}, "Report bugs")),CGI::hr() if $authz->hasPermissions($user, "report_bugs"); print CGI::start_ul({class=>"LinksMenu"}); print CGI::li(CGI::span({style=>"font-size:larger"}, CGI::a({href=>$self->systemLink($sets)}, PROBLEM_SETS))); print CGI::li(CGI::a({href=>$self->systemLink($options)}, OPTIONS)); print CGI::li(CGI::a({href=>$self->systemLink($grades)}, GRADES)); print CGI::li(CGI::a({href=>$self->systemLink($logout)}, LOG_OUT)); if ($authz->hasPermissions($user, "access_instructor_tools")) { my $ipfx = "${pfx}Instructor::"; my $userID = $r->param("effectiveUser"); my $setID = $urlpath->arg("setID"); $setID = "" if (defined $setID && !(grep /$setID/, $db->listUserSets($userID))); my $problemID = $urlpath->arg("problemID"); $problemID = "" if (defined $problemID && !(grep /$problemID/, $db->listUserProblems($userID, $setID))); my $instr = $urlpath->newFromModule("${ipfx}Index", %args); my $addUsers = $urlpath->newFromModule("${ipfx}AddUsers", %args); my $userList = $urlpath->newFromModule("${ipfx}UserList", %args); # set list links my $setList = $urlpath->newFromModule("${ipfx}ProblemSetList", %args); my $setDetail = $urlpath->newFromModule("${ipfx}ProblemSetEditor", %args, setID => $setID); my $problemEditor = $urlpath->newFromModule("${ipfx}PGProblemEditor", %args, setID => $setID, problemID => $problemID); my $maker = $urlpath->newFromModule("${ipfx}SetMaker", %args); my $assigner = $urlpath->newFromModule("${ipfx}Assigner", %args); my $mail = $urlpath->newFromModule("${ipfx}SendMail", %args); my $scoring = $urlpath->newFromModule("${ipfx}Scoring", %args); # statistics links my $stats = $urlpath->newFromModule("${ipfx}Stats", %args); my $userStats = $urlpath->newFromModule("${ipfx}Stats", %args, statType => "student", userID => $userID); my $setStats = $urlpath->newFromModule("${ipfx}Stats", %args, statType => "set", setID => $setID); # progress links my $progress = $urlpath->newFromModule("${ipfx}StudentProgress", %args); my $userProgress = $urlpath->newFromModule("${ipfx}StudentProgress", %args, statType => "student", userID => $userID); my $setProgress = $urlpath->newFromModule("${ipfx}StudentProgress", %args, statType => "set", setID => $setID); my $files = $urlpath->newFromModule("${ipfx}FileXfer", %args); print CGI::hr(); print CGI::start_li(); print CGI::span({style=>"font-size:larger"}, CGI::a({href=>$self->systemLink($instr)}, space2nbsp($instr->name)) ); print CGI::start_ul(); #print CGI::li(CGI::a({href=>$self->systemLink($addUsers)}, ADD_USERS)) if $authz->hasPermissions($user, "modify_student_data"); print CGI::li(CGI::a({href=>$self->systemLink($userList)}, USER_LIST)); print CGI::start_li(); print CGI::a({href=>$self->systemLink($setList)}, SET_LIST); if (defined $setID and $setID ne "") { print CGI::start_ul(); print CGI::start_li(); print CGI::a({href=>$self->systemLink($setDetail)}, $setID); if (defined $problemID and $problemID ne "") { print CGI::ul( CGI::li(CGI::a({href=>$self->systemLink($problemEditor)}, $problemID)) ); } print CGI::end_li(); print CGI::end_ul(); } print CGI::end_li(); print CGI::li(CGI::a({href=>$self->systemLink($maker)}, SET_MAKER)) if $authz->hasPermissions($user, "modify_problem_sets"); print CGI::li(CGI::a({href=>$self->systemLink($assigner)}, ASSIGNER)) if $authz->hasPermissions($user, "assign_problem_sets"); print CGI::li(CGI::a({href=>$self->systemLink($stats)}, STATS)); # print CGI::start_li(); # if (defined $userID and $userID ne "") { # print CGI::ul( # CGI::li(CGI::a({href=>$self->systemLink($userStats)}, $userID)) # ); # } # if (defined $setID and $setID ne "") { # print CGI::ul( # CGI::li(CGI::a({href=>$self->systemLink($setStats)}, space2nbsp($setID))) # ); # } # print CGI::end_li(); ## Added Link for Student Progress print CGI::li(CGI::a({href=>$self->systemLink($progress)}, PROGRESS)); print CGI::start_li(); if (defined $userID and $userID ne "") { print CGI::ul( CGI::li(CGI::a({href=>$self->systemLink($userProgress)}, $userID)) ); } if (defined $setID and $setID ne "") { print CGI::ul( CGI::li(CGI::a({href=>$self->systemLink($setProgress)}, space2nbsp($setID))) ); } print CGI::end_li(); print CGI::li(CGI::a({href=>$self->systemLink($scoring)}, SCORING)) if $authz->hasPermissions($user, "score_sets"); print CGI::li(CGI::a({href=>$self->systemLink($mail)}, MAIL)) if $authz->hasPermissions($user, "send_mail"); print CGI::li(CGI::a({href=>$self->systemLink($files)}, FILE_TRANSFER)); print CGI::li( $self->helpMacro('instructor_links')); print CGI::end_ul(); } print CGI::end_ul(); print "\n"; return ""; } =item loginstatus() Defined in this package. Print a notification message announcing the current real user and effective user, a link to stop acting as the effective user, and a link to logout. =cut sub loginstatus { my ($self) = @_; my $r = $self->r; my $urlpath = $r->urlpath; my $key = $r->param("key"); if ($key) { my $courseID = $urlpath->arg("courseID"); my $userID = $r->param("user"); my $eUserID = $r->param("effectiveUser"); my $stopActingURL = $self->systemLink($urlpath, # current path params => { effectiveUser => $userID }, ); my $logoutURL = $self->systemLink($urlpath->newFromModule(__PACKAGE__ . "::Logout", courseID => $courseID)); print "\n\n"; print "Logged in as $userID. ", CGI::br(); print CGI::a({href=>$logoutURL}, "Log Out"); if ($eUserID ne $userID) { print " | Acting as $eUserID. "; print CGI::a({href=>$stopActingURL}, "Stop Acting"); } print "\n"; } return ""; } =item nav($args) Not defined in this package. Links to the previous, next, and parent objects. $args is a reference to a hash containing the following fields: style => text|image imageprefix => prefix to prepend to base image URL imagesuffix => suffix to append to base image URL separator => HTML to place in between links If C