[system] / branches / rel-2-3-dev / webwork2 / lib / WeBWorK / ContentGenerator / Instructor / SendMail.pm Repository:
ViewVC logotype

View of /branches/rel-2-3-dev/webwork2/lib/WeBWorK/ContentGenerator/Instructor/SendMail.pm

Parent Directory Parent Directory | Revision Log Revision Log


Revision 4938 - (download) (as text) (annotate)
Tue Apr 24 20:01:56 2007 UTC (6 years, 1 month ago) by sh002i
File size: 41457 byte(s)
backport (sh002i): wrap message for preview. resolves bug #1147.

    1 ################################################################################
    2 # WeBWorK Online Homework Delivery System
    3 # Copyright © 2000-2006 The WeBWorK Project, http://openwebwork.sf.net/
    4 # $CVSHeader$
    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::ContentGenerator::Instructor::SendMail;
   18 use base qw(WeBWorK::ContentGenerator::Instructor);
   19 
   20 =head1 NAME
   21 
   22 WeBWorK::ContentGenerator::Instructor::SendMail - Entry point for User-specific data editing
   23 
   24 =cut
   25 
   26 use strict;
   27 use warnings;
   28 #use CGI qw(-nosticky );
   29 use WeBWorK::CGI;
   30 use Email::Address;
   31 use HTML::Entities;
   32 use Mail::Sender;
   33 use Socket qw/unpack_sockaddr_in inet_ntoa/; # for remote host/port info
   34 use Text::Wrap qw(wrap);
   35 use WeBWorK::HTML::ScrollingRecordList qw/scrollingRecordList/;
   36 use WeBWorK::Utils::FilterRecords qw/filterRecords/;
   37 
   38 use mod_perl;
   39 use constant MP2 => ( exists $ENV{MOD_PERL_API_VERSION} and $ENV{MOD_PERL_API_VERSION} >= 2 );
   40 
   41 #my $REFRESH_RESIZE_BUTTON = "Set preview to: ";  # handle submit value idiocy
   42 my $UPDATE_SETTINGS_BUTTON = "Update settings and refresh page"; # handle submit value idiocy
   43 sub initialize {
   44   my ($self) = @_;
   45   my $r      = $self->r;
   46   my $db     = $r->db;
   47   my $ce     = $r->ce;
   48   my $authz  = $r->authz;
   49   my $user   = $r->param('user');
   50 
   51   my @selected_filters;
   52   if (defined ($r->param('classList!filter'))){ @selected_filters = $r->param('classList!filter');}
   53   else {@selected_filters = ("all");}
   54 
   55 
   56   # Check permissions
   57   return unless $authz->hasPermissions($user, "access_instructor_tools");
   58   return unless $authz->hasPermissions($user, "send_mail");
   59 
   60 #############################################################################################
   61 # gather directory data
   62 #############################################################################################
   63   my $emailDirectory    =    $ce->{courseDirs}->{email};
   64   my $scoringDirectory  =    $ce->{courseDirs}->{scoring};
   65   my $templateDirectory =    $ce->{courseDirs}->{templates};
   66 
   67   my $action            =    $r->param('action') ;
   68   my $openfilename      =    $r->param('openfilename');
   69   my $savefilename      =    $r->param('savefilename');
   70 
   71 
   72   #FIXME  get these values from global course environment (see subroutines as well)
   73   my $default_msg_file       =    'default.msg';
   74   my $old_default_msg_file   =    'old_default.msg';
   75 
   76 
   77   #  get user record
   78   my $ur = $self->{db}->getUser($user);
   79 
   80   # store data
   81   $self->{defaultFrom}            =   $ur->rfc822_mailbox;
   82   $self->{defaultReply}           =   $ur->rfc822_mailbox;
   83   $self->{defaultSubject}         =   $self->r->urlpath->arg("courseID") . " notice";
   84 
   85   $self->{rows}                   =   (defined($r->param('rows'))) ? $r->param('rows') : $ce->{mail}->{editor_window_rows};
   86   $self->{columns}                =   (defined($r->param('columns'))) ? $r->param('columns') : $ce->{mail}->{editor_window_columns};
   87   $self->{default_msg_file}     =   $default_msg_file;
   88   $self->{old_default_msg_file}   =   $old_default_msg_file;
   89   $self->{merge_file}             =   (defined($r->param('merge_file'  )))    ? $r->param('merge_file')   : 'None';
   90   #$self->{preview_user}           =   (defined($r->param('preview_user')))    ? $r->param('preview_user') : $user;
   91   # an expermiment -- share the scrolling list for preivew and sendTo actions.
   92   my @classList                   =   (defined($r->param('classList')))    ? $r->param('classList') : ($user);
   93   $self->{preview_user}           =   $classList[0] || $user;
   94 
   95 #############################################################################################
   96 # gather database data
   97 #############################################################################################
   98   # FIXME  this might be better done in body? We don't always need all of this data. or do we?
   99   my @users =  $db->listUsers;
  100   my @Users = $db->getUsers(@users);
  101   # filter out users who don't get included in email (fixes bug #938)
  102   @Users = grep { $ce->status_abbrev_has_behavior($_->status, "include_in_email") } @Users;
  103   my @user_records = ();
  104 
  105 ## Mark's code to prefilter userlist
  106 
  107 
  108   my (@viewable_sections,@viewable_recitations);
  109 
  110   if (defined @{$ce->{viewable_sections}->{$user}})
  111     {@viewable_sections = @{$ce->{viewable_sections}->{$user}};}
  112   if (defined @{$ce->{viewable_recitations}->{$user}})
  113     {@viewable_recitations = @{$ce->{viewable_recitations}->{$user}};}
  114 
  115   if (@viewable_sections or @viewable_recitations){
  116     foreach my $student (@Users){
  117       my $keep = 0;
  118       foreach my $sec (@viewable_sections){
  119         if ($student->section() eq $sec){$keep = 1;}
  120       }
  121       foreach my $rec (@viewable_recitations){
  122         if ($student->recitation() eq $rec){$keep = 1;}
  123       }
  124       if ($keep) {push @user_records, $student;}
  125     }
  126   }
  127   else {@user_records = @Users;}
  128 
  129 ## End Mark's code
  130 
  131 # foreach my $userName (@users) {
  132 #   my $userRecord = $db->getUser($userName); # checked
  133 #   die "record for user $userName not found" unless $userRecord;
  134 #   push(@user_records, $userRecord);
  135 # }
  136   ###########################
  137   # Sort the users for presentation in the select list
  138   ###########################
  139 # if (defined $r->param("sort_by") ) {
  140 #   my $sort_method = $r->param("sort_by");
  141 #   if ($sort_method eq 'section') {
  142 #     @user_records = sort { (lc($a->section) cmp lc($b->section)) || (lc($a->last_name) cmp lc($b->last_name)) } @user_records;
  143 #   } elsif ($sort_method eq 'recitation') {
  144 #     @user_records = sort { (lc($a->recitation) cmp lc($b->recitation)) || (lc($a->last_name) cmp lc($b->last_name)) } @user_records;
  145 #   } elsif ($sort_method eq 'alphabetical') {
  146 #     @user_records = sort {  (lc($a->last_name) cmp lc($b->last_name)) } @user_records;
  147 #   } elsif ($sort_method eq 'id' )          {
  148 #       @user_records = sort { $a->user_id cmp $b->user_id }  @user_records;
  149 #   }
  150 # } else {
  151 #   @user_records = sort { $a->user_id cmp $b->user_id }  @user_records;
  152 # }
  153 
  154 
  155   # replace the user names by a sorted version.
  156   @users                         =  map {$_->user_id} @user_records;
  157   # store data
  158   $self->{ra_users}              =   \@users;
  159   $self->{ra_user_records}       =   \@user_records;
  160 
  161 #############################################################################################
  162 # gather list of recipients
  163 #############################################################################################
  164   my @send_to                    =   ();
  165   #FIXME  this (radio) is a lousy name
  166   my $recipients                 = $r->param('radio');
  167   if (defined($recipients) and $recipients eq 'all_students') {  #only active students #FIXME status check??
  168 
  169 ## Add code so that only people who pass the current filters are added to our list of recipients.
  170 # @user_records = filterRecords({filter=\@selected_filters},@user_records);
  171 #  I wasn't able to make this work
  172 #  I edited the selection button to make that clear.
  173 #
  174 
  175     foreach my $ur (@user_records) {
  176       push(@send_to,$ur->user_id)
  177         if $ce->status_abbrev_has_behavior($ur->status, "include_in_email")
  178           and not $ur->user_id =~ /practice/;
  179     }
  180   } elsif (defined($recipients) and $recipients eq 'studentID' ) {
  181     @send_to                   = $r->param('classList');
  182   } else {
  183     # no recipients have been defined -- probably the first time on the page
  184   }
  185   $self->{ra_send_to}               = \@send_to;
  186 #################################################################
  187 # Check the validity of the input file name
  188 #################################################################
  189   my $input_file = '';
  190   #make sure an input message file was submitted and exists
  191   #else use the default message
  192   if ( defined($openfilename) ) {
  193     if ( -e "${emailDirectory}/$openfilename") {
  194       if ( -R "${emailDirectory}/$openfilename") {
  195         $input_file = $openfilename;
  196       } else {
  197         $self->addbadmessage(CGI::p(join("",
  198           "The file ${emailDirectory}/$openfilename is not readable by the webserver.",CGI::br(),
  199           "Check that it's permissions are set correctly.",
  200         )));
  201       }
  202     } else {
  203       $input_file = $default_msg_file;
  204       $self->addbadmessage(CGI::p(join("",
  205           "The file ${emailDirectory}/$openfilename cannot be found.",CGI::br(),
  206           "Check whether it exists and whether the directory $emailDirectory can be read by the webserver.",CGI::br(),
  207           "Using contents of the default message $default_msg_file instead.",
  208       )));
  209     }
  210   } else {
  211     $input_file     = $default_msg_file;
  212   }
  213   $self->{input_file} =$input_file;
  214 
  215 #################################################################
  216 # Determine the file name to save message into
  217 #################################################################
  218   my $output_file      = 'FIXME no output file specified';
  219   if (defined($action) and $action eq 'Save as Default') {
  220     $output_file  = $default_msg_file;
  221   } elsif ( defined($action) and ($action =~/save/i)) {
  222     if (defined($savefilename) and $savefilename ) {
  223       $output_file  = $savefilename;
  224     } else {
  225       $self->addbadmessage(CGI::p("No filename was specified for saving!  The message was not saved."));
  226     }
  227   } elsif ( defined($input_file) ) {
  228     $output_file  = $input_file;
  229   }
  230 
  231   #################################################################
  232   # Sanity check on save file name
  233   #################################################################
  234 
  235   if ($output_file =~ /^[~.]/ || $output_file =~ /\.\./) {
  236     $self->addbadmessage(CGI::p("For security reasons, you cannot specify a message file from a directory",
  237             "higher than the email directory (you can't use ../blah/blah for example). ",
  238             "Please specify a different file or move the needed file to the email directory",));
  239   }
  240   unless ($output_file =~ m|\.msg$| ) {
  241     $self->addbadmessage(CGI::p("Invalid file name.",
  242                             "The file name \"$output_file\" does not have a \".msg\" extension",
  243                 "All email file names must end in the extension \".msg\"",
  244                 "choose a file name with a \".msg\" extension.",
  245                 "The message was not saved.",));
  246   }
  247 
  248   $self->{output_file} = $output_file;  # this is ok.  It will be put back in the text input box for re-editing.
  249 
  250 
  251 #############################################################################################
  252 # Determine input source
  253 #############################################################################################
  254   #warn "Action = $action";
  255   my $input_source;
  256   if ($action){
  257     $input_source =  ( defined( $r->param('body') ) and $action ne 'Open' ) ? 'form' : 'file';}
  258   else { $input_source = ( defined($r->param('body')) ) ? 'form' : 'file';}
  259 
  260 #############################################################################################
  261 # Get inputs
  262 #############################################################################################
  263   my($from, $replyTo, $r_text, $subject);
  264   if ($input_source eq 'file') {
  265 
  266     ($from, $replyTo,$subject,$r_text) = $self->read_input_file("$emailDirectory/$input_file");
  267 
  268 
  269   } elsif ($input_source eq 'form') {
  270     # read info from the form
  271     # bail if there is no message body
  272 
  273     $from              =    $r->param('from');
  274     $replyTo           =    $r->param('replyTo');
  275     $subject           =    $r->param('subject');
  276     my $body           =    $r->param('body');
  277     # Sanity check: body must contain non-white space
  278     $self->addbadmessage(CGI::p('You didn\'t enter any message.')) unless ($r->param('body') =~ /\S/);
  279     $r_text               =    \$body;
  280 
  281   }
  282 
  283   my $remote_host;
  284   if (MP2) {
  285     $remote_host = $r->connection->remote_addr->ip_get || "UNKNOWN";
  286   } else {
  287     (undef, $remote_host) = unpack_sockaddr_in($r->connection->remote_addr);
  288     $remote_host = defined $remote_host ? inet_ntoa($remote_host) : "UNKNOWN";
  289   }
  290 
  291   # store data
  292   $self->{from}                   =    $from;
  293   $self->{replyTo}                =    $replyTo;
  294   $self->{subject}                =    $subject;
  295   $self->{remote_host}            =    $remote_host;
  296   $self->{r_text}                 =    $r_text;
  297 
  298 
  299 
  300 ###################################################################################
  301 #Determine the appropriate script action from the buttons
  302 ###################################################################################
  303 #     first time actions
  304 #          open new file
  305 #          open default file
  306 #     choose merge file actions
  307 #          chose merge button
  308 #     option actions
  309 #       'reset rows'
  310 
  311 #     save actions
  312 #   "save" button
  313 #   "save as" button
  314 #   "save as default" button
  315 #     preview actions
  316 #   'preview' button
  317 #     email actions
  318 #   'entire class'
  319 #   'selected studentIDs'
  320 #     error actions (various)
  321 
  322 
  323 #############################################################################################
  324 # if no form is submitted, gather data needed to produce the mail form and return
  325 #############################################################################################
  326   my $to                =    $r->param('To');
  327   my $script_action     = '';
  328 
  329 
  330   if(not defined($action) or $action eq 'Open'
  331      or $action eq $UPDATE_SETTINGS_BUTTON ){
  332 
  333     return '';
  334   }
  335 
  336 
  337 
  338 
  339 
  340 #############################################################################################
  341 # If form is submitted deal with filled out forms
  342 # and various actions resulting from different buttons
  343 #############################################################################################
  344 
  345 
  346   if ($action eq 'Save' or $action eq 'Save as:' or $action eq 'Save as Default') {
  347 
  348 #   warn "FIXME Saving files  action = $action  outputFileName=$output_file";
  349 
  350     #################################################################
  351     # construct message body
  352     #################################################################
  353     my $temp_body = ${ $r_text };
  354     $temp_body =~ s/\r\n/\n/g;
  355     $temp_body = join("",
  356            "From: $from \nReply-To: $replyTo\n" ,
  357            "Subject: $subject\n" ,
  358            "Message: \n    $temp_body");
  359 #   warn "FIXME from $from | subject $subject |reply $replyTo|msg $temp_body";
  360     #################################################################
  361     # overwrite protection
  362     #################################################################
  363     if ($action eq 'Save as:' and -e "$emailDirectory/$output_file") {
  364       $self->addbadmessage(CGI::p("The file $emailDirectory/$output_file already exists and cannot be overwritten",
  365                                "The message was not saved"));
  366       return;
  367     }
  368 
  369     #################################################################
  370       # Back up existing file?
  371       #################################################################
  372       if ($action eq 'Save as Default' and -e "$emailDirectory/$default_msg_file") {
  373         rename("$emailDirectory/$default_msg_file","$emailDirectory/$old_default_msg_file") or
  374                die "Can't rename $emailDirectory/$default_msg_file to $emailDirectory/$old_default_msg_file ",
  375                    "Check permissions for webserver on directory $emailDirectory. $!";
  376         $self->addgoodmessage(CGI::p("Backup file <code>$emailDirectory/$old_default_msg_file</code> created." . CGI::br()));
  377       }
  378       #################################################################
  379       # Save the message
  380     #################################################################
  381     $self->saveProblem($temp_body, "${emailDirectory}/$output_file" ) unless ($output_file =~ /^[~.]/ || $output_file =~ /\.\./ || not $output_file =~ m|\.msg$|);
  382     unless ( $self->{submit_message} or not -w "${emailDirectory}/$output_file" )  {  # if there are no errors report success
  383       $self->addgoodmessage(CGI::p("Message saved to file <code>${emailDirectory}/$output_file</code>."));
  384     }
  385 
  386   } elsif ($action eq 'Preview message') {
  387     $self->{response}         = 'preview';
  388 
  389   } elsif ($action eq 'Send Email') {
  390     # verify format of From address (one valid rfc2822 address)
  391     my @parsed_from_addrs = Email::Address->parse($self->{from});
  392     unless (@parsed_from_addrs == 1) {
  393       $self->addbadmessage("From field must contain one valid email address.");
  394       return;
  395     }
  396 
  397     # verify format of Reply-to address (zero or more valid rfc2822 addresses)
  398     if (defined $self->{replyTo} and $self->{replyTo} ne "") {
  399       my @parsed_replyto_addrs = Email::Address->parse($self->{replyTo});
  400       unless (@parsed_replyto_addrs > 0) {
  401         $self->addbadmessage("Invalid Reply-to address.");
  402         return;
  403       }
  404     }
  405 
  406       # check that recipients have been selected.
  407     my @recipients            = @{$self->{ra_send_to}};
  408     unless (@recipients) {
  409       $self->addbadmessage(CGI::p("No recipients selected. Please select one or more recipients from the list below."));
  410       return;
  411     }
  412 
  413     #  get merge file
  414     my $merge_file      = ( defined($self->{merge_file}) ) ? $self->{merge_file} : 'None';
  415     my $delimiter       = ',';
  416     my $rh_merge_data   = $self->read_scoring_file("$merge_file", "$delimiter");
  417     unless (ref($rh_merge_data) ) {
  418       $self->addbadmessage(CGI::p("No merge data file"));
  419       $self->addbadmessage(CGI::p("Can't read merge file $merge_file. No message sent"));
  420       return;
  421     } ;
  422     $self->{rh_merge_data} = $rh_merge_data;
  423 
  424     # we don't set the response until we're sure that email can be sent
  425     $self->{response}         = 'send_email';
  426 
  427     # FIXME i'm not sure why we're pulling this out here -- mail_message_to_recipients does have
  428     # access to the course environment and should just grab it directly
  429     $self->{smtpServer}    = $ce->{mail}->{smtpServer};
  430 
  431     # do actual mailing in the cleanup phase, since it could take a long time
  432     # FIXME we need to do a better job providing status notifications for long-running email jobs
  433     my $post_connection_action = sub {
  434       my $r = shift;
  435       # catch exceptions generated during the sending process
  436       my $result_message = eval { $self->mail_message_to_recipients() };
  437       if ($@) {
  438         # add the die message to the result message
  439         $result_message = "An error occurred while trying to send email.\n"
  440           . "The error message is:\n\n$@\n\n";
  441         # and also write it to the apache log
  442         $self->r->log->error("An error occurred while trying to send email: $@");
  443       }
  444       # this could fail too...
  445       eval { $self->email_notification($result_message) };
  446       if ($@) {
  447         $self->r->log->error("An error occured while trying to send the email notification: $@");
  448       }
  449     };
  450     if (MP2) {
  451       $r->connection->pool->cleanup_register($post_connection_action);
  452     } else {
  453       $r->post_connection($post_connection_action);
  454     }
  455 #     foreach my $recipient (@recipients) {
  456 #       #warn "FIXME sending email to $recipient";
  457 #       my $ur      = $self->{db}->getUser($recipient); #checked
  458 #       die "record for user $recipient not found" unless $ur;
  459 #       unless ($ur->email_address) {
  460 #         $self->addbadmessage(CGI::p("user $recipient does not have an email address -- skipping"));
  461 #         next;
  462 #       }
  463 #       my ($msg, $preview_header);
  464 #       eval{ ($msg,$preview_header) = $self->process_message($ur,$rh_merge_data); };
  465 #       $self->addbadmessage(CGI::p("There were errors in processing user $ur, merge file $merge_file. $@")) if $@;
  466 #       my $mailer = Mail::Sender->new({
  467 #         from    =>   $from,
  468 #         to      =>   $ur->email_address,
  469 #         smtp    =>   $ce->{mail}->{smtpServer},
  470 #         subject =>   $subject,
  471 #         headers =>   "X-Remote-Host: ".$r->get_remote_host(),
  472 #       });
  473 #       unless (ref $mailer) {
  474 #         $self->addbadmessage(CGI::p("Failed to create a mailer for user $recipient: $Mail::Sender::Error"));
  475 #         next;
  476 #       }
  477 #       unless (ref $mailer->Open()) {
  478 #         $self->addbadmessage(CGI::p("Failed to open the mailer for user $recipient: $Mail::Sender::Error"));
  479 #         next;
  480 #       }
  481 #       my $MAIL = $mailer->GetHandle() or $self->addbadmessage(CGI::p("Couldn't get handle"));
  482 #       print $MAIL  $msg || $self->addbadmessage(CGI::p("Couldn't print to $MAIL"));
  483 #       close $MAIL || $self->addbadmessage(CGI::p("Couldn't close $MAIL"));
  484 #         #warn "FIXME mailed to ", $ur->email_address, "from $from subject $subject";
  485 #
  486 #     }
  487 
  488   } else {
  489     $self->addbadmessage(CGI::p("Didn't recognize button $action"));
  490   }
  491 
  492 
  493 
  494 }  #end initialize
  495 
  496 
  497 
  498 
  499 
  500 sub body {
  501   my ($self)          = @_;
  502   my $r               = $self->r;
  503   my $urlpath         = $r->urlpath;
  504   my $authz           = $r->authz;
  505   my $setID           = $urlpath->arg("setID");
  506   my $response        = (defined($self->{response}))? $self->{response} : '';
  507   my $user            = $r->param('user');
  508 
  509   # Check permissions
  510   return CGI::div({class=>"ResultsWithError"}, CGI::p("You are not authorized to access instructor tools"))
  511     unless $authz->hasPermissions($user, "access_instructor_tools");
  512 
  513   return CGI::div({class=>"ResultsWithError"}, CGI::p("You are not authorized to send mail to students"))
  514     unless $authz->hasPermissions($user, "send_mail");
  515 
  516   if ($response eq 'preview') {
  517     $self->print_preview($setID);
  518   } elsif ($response eq 'send_email' and $self->{ra_send_to} and @{$self->{ra_send_to}}){
  519     my $message = CGI::i("Email is being sent to ".  scalar(@{$self->{ra_send_to}})." recipient(s). You will be notified"
  520                  ." by email when the task is completed.  This may take several minutes if the class is large."
  521     );
  522     $self->addgoodmessage($message);
  523     $self->{message} .= $message;
  524 
  525     $self->print_form($setID);
  526   } else {
  527     $self->print_form($setID);
  528   }
  529 
  530 }
  531 sub print_preview {
  532   my ($self)          = @_;
  533   my $r               = $self->r;
  534   my $urlpath         = $r->urlpath;
  535   my $setID           = $urlpath->arg("setID");
  536 
  537   #  get preview user
  538   my $ur      = $r->db->getUser($self->{preview_user}); #checked
  539   die "record for preview user ".$self->{preview_user}. " not found." unless $ur;
  540 
  541   #  get merge file
  542   my $merge_file      = ( defined($self->{merge_file}) ) ? $self->{merge_file} : 'None';
  543   my $delimiter       = ',';
  544   my $rh_merge_data   = $self->read_scoring_file("$merge_file", "$delimiter");
  545 
  546   my ($msg, $preview_header) = $self->process_message($ur,$rh_merge_data,1); # 1 == for preview
  547 
  548   my $recipients  = join(" ",@{$self->{ra_send_to} });
  549   my $errorMessage =  defined($self->{submit_message}) ?  CGI::i($self->{submit_message} ) : '' ;
  550   $msg = join("",
  551      $errorMessage,
  552      $preview_header,
  553      "To: "             , $ur->email_address,"\n",
  554        "From: "           , $self->{from} , "\n" ,
  555        "Reply-To: "       , $self->{replyTo} , "\n" ,
  556        "Subject:  "       , $self->{subject} , "\n" ,"\n" ,
  557      $msg , "\n"
  558   );
  559 
  560   return join("", '<pre>',wrap("","",$msg),"\n","\n",
  561            '</pre>',
  562            CGI::p('Use browser back button to return from preview mode'),
  563            CGI::h3('Emails to be sent to the following:'),
  564            $recipients, "\n",
  565 
  566   );
  567 
  568 }
  569 sub print_form {
  570   my ($self)          = @_;
  571   my $r               = $self->r;
  572   my $urlpath         = $r->urlpath;
  573   my $authz           = $r->authz;
  574   my $db              = $r->db;
  575   my $ce              = $r->ce;
  576   my $courseName      = $urlpath->arg("courseID");
  577   my $setID           = $urlpath->arg("setID");
  578   my $user            = $r->param('user');
  579 
  580   my $root            = $ce->{webworkURLs}->{root};
  581   my $sendMailPage    = $urlpath->newFromModule($urlpath->module,courseID=>$courseName);
  582   my $sendMailURL     = $self->systemLink($sendMailPage, authen => 0);
  583 
  584         return CGI::em("You are not authorized to access the Instructor tools.") unless $authz->hasPermissions($user, "access_instructor_tools");
  585 
  586   my $userTemplate = $db->newUser;
  587   my $permissionLevelTemplate = $db->newPermissionLevel;
  588 
  589   # This code will require changing if the permission and user tables ever have different keys.
  590   my @users                 = sort @{ $self->{ra_users} };
  591   my $ra_user_records       = $self->{ra_user_records};
  592   my %classlistLabels       = ();#  %$hr_classlistLabels;
  593   foreach my $ur (@{ $ra_user_records }) {
  594     $classlistLabels{$ur->user_id} = $ur->user_id.': '.$ur->last_name. ', '. $ur->first_name.' -- '.$ur->section." / ".$ur->recitation;
  595   }
  596 
  597 ## Mark edit define scrolling list
  598   my $scrolling_user_list = scrollingRecordList({
  599     name => "classList",      ## changed from classList to action
  600     request => $r,
  601     default_sort => "lnfn",
  602     default_format => "lnfn_uid",
  603     default_filters => ["all"],
  604     size => 5,
  605     multiple => 1,
  606     refresh_button_name =>'Update settings and refresh page',
  607   }, @{$ra_user_records});
  608 
  609 ##############################################################################################################
  610 
  611 
  612   my $from            = $self->{from};
  613   my $subject         = $self->{subject};
  614   my $replyTo         = $self->{replyTo};
  615   my $columns         = $self->{columns};
  616   my $rows            = $self->{rows};
  617   my $text            = defined($self->{r_text}) ? ${ $self->{r_text} }: 'FIXME no text was produced by initialization!!';
  618   my $input_file      = $self->{input_file};
  619   my $output_file     = $self->{output_file};
  620   my @sorted_messages = $self->get_message_file_names;
  621   my @sorted_merge_files = $self->get_merge_file_names;
  622   my $merge_file      = ( defined($self->{merge_file}) ) ? $self->{merge_file} : 'None';
  623   my $delimiter       = ',';
  624   my $rh_merge_data   = $self->read_scoring_file("$merge_file", "$delimiter");
  625   my @merge_keys      = keys %$rh_merge_data;
  626   my $preview_user    = $self->{preview_user};
  627   my $preview_record   = $db->getUser($preview_user); # checked
  628   die "record for preview user ".$self->{preview_user}. " not found." unless $preview_record;
  629 
  630 
  631 #############################################################################################
  632 
  633   print CGI::start_form({method=>"post", action=>$sendMailURL});
  634   print $self->hidden_authen_fields();
  635 #############################################################################################
  636 # begin upper table
  637 #############################################################################################
  638 
  639     print CGI::start_table({-border=>'2', -cellpadding=>'4'});
  640   print CGI::Tr({-align=>'left',-valign=>'top'},
  641 #############################################################################################
  642 # first column
  643 #############################################################################################
  644 
  645        CGI::td({},
  646            CGI::strong("Message file: "), $input_file,"\n",CGI::br(),
  647          CGI::submit(-name=>'action', -value=>'Open'), '&nbsp;&nbsp;&nbsp;&nbsp;',"\n",
  648          CGI::popup_menu(-name=>'openfilename',
  649                          -values=>\@sorted_messages,
  650                          -default=>$input_file
  651          ),
  652          "\n",CGI::br(),
  653          CGI::strong("Save file to: "), $output_file,
  654          "\n",CGI::br(),
  655          CGI::strong('Merge file: '), $merge_file,
  656          CGI::br(),
  657          CGI::popup_menu(-name=>'merge_file',
  658                          -values=>\@sorted_merge_files,
  659                          -default=>$merge_file,
  660          ), "\n",
  661          "\n",
  662          #CGI::hr(),
  663          CGI::div({style=>"background-color: #CCCCCC"},
  664            "\n", 'From:','&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;',  CGI::textfield(-name=>"from", -size=>30, -value=>$from, -override=>1),
  665            "\n", CGI::br(),'Reply-To: ', CGI::textfield(-name=>"replyTo", -size=>30, -value=>$replyTo, -override=>1),
  666            "\n", CGI::br(),'Subject:  ', CGI::br(), CGI::textarea(-name=>'subject', -default=>$subject, -rows=>3,-cols=>30, -override=>1),
  667         ),
  668         #CGI::hr(),
  669         CGI::submit(-name=>'action', -value=>$UPDATE_SETTINGS_BUTTON),
  670 
  671       ),
  672 #############################################################################################
  673 # second column
  674 #############################################################################################
  675 
  676 ## Edit by Mark to insert scrolling list
  677           CGI::td({-style=>"width:33%"},
  678                CGI::strong("Send to:"),
  679                       CGI::radio_group(-name=>'radio',
  680                                        -values=>['all_students','studentID'],
  681                                        -labels=>{all_students=>'All students in course',studentID => 'Selected students'},
  682                                        -default=>'studentID', -linebreak=>0),
  683               CGI::br(),$scrolling_user_list,
  684               CGI::i("Preview set to: "), $preview_record->last_name,
  685               CGI::submit(-name=>'action', -value=>'preview',-label=>'Preview message'),'&nbsp;&nbsp;',
  686           ),
  687 
  688 ## Edit here to insert filtering
  689 ## be sure to fail GRACEFULLY!
  690 #
  691 #
  692 #               CGI::input({type=>'submit',value=>'Sort by',name=>'action'}),,
  693 #             CGI::radio_group(-name=>'sort_by', -values=>['id','alphabetical','section','recitation'],
  694 #               -labels=>{id=>'Login',alphabetical=>'Alph.',section => 'Sec.',recitation=>'Rec.'},
  695 #               -default=>defined($r->param("sort_by")) ? $r->param("sort_by") : 'id',
  696 #               -linebreak=>0
  697 #             ),
  698 #
  699 #           CGI::br(),CGI::br(),
  700 #       CGI::popup_menu(-name=>'classList',
  701 #              -values=>\@users,
  702 #              -labels=>\%classlistLabels,
  703 #              -size  => 10,
  704 #              -multiple => 1,
  705 #              -default=>$user
  706 #       ),
  707 #     ),
  708 
  709 
  710 
  711 
  712 #############################################################################################
  713 # third column
  714 #############################################################################################
  715       CGI::td({align=>'left'},
  716 
  717         " Rows: ", CGI::textfield(-name=>'rows', -size=>3, -value=>$rows),
  718         " Columns: ", CGI::textfield(-name=>'columns', -size=>3, -value=>$columns),
  719         CGI::br(),
  720 #       CGI::i('Press any action button to update display'),CGI::br(),
  721       #show available macros
  722         CGI::popup_menu(
  723             -name=>'dummyName',
  724             -values=>['', '$SID', '$FN', '$LN', '$SECTION', '$RECITATION','$STATUS', '$EMAIL', '$LOGIN', '$COL[3]', '$COL[-1]'],
  725             -labels=>{''=>'list of insertable macros',
  726               '$SID'=>'$SID - Student ID',
  727               '$FN'=>'$FN - First name',
  728               '$LN'=>'$LN - Last name',
  729               '$SECTION'=>'$SECTION',
  730               '$RECITATION'=>'$RECITATION',
  731               '$STATUS'=>'$STATUS - C, Audit, Drop, etc.',
  732               '$EMAIL'=>'$EMAIL - Email address',
  733               '$LOGIN'=>'$LOGIN - Login',
  734               '$COL[3]'=>'$COL[3] - 3rd col',
  735               '$COL[-1]'=>'$COL[-1] - Last column'
  736               }
  737         ), "\n",
  738       ),
  739 
  740   ); # end Tr
  741   print CGI::end_table();
  742 #############################################################################################
  743 # end upper table
  744 #############################################################################################
  745 
  746 # show merge file
  747 #         print  "<pre>",(map {$_ =~s/\s/\./g;$_}     map {sprintf('%-8.8s',$_);}  0..8),"</pre>";
  748 #     print  CGI::popup_menu(
  749 #             -name=>'dummyName2',
  750 #             -values=>\@merge_keys,
  751 #             -labels=>$rh_merge_data,
  752 #             -multiple=>1,
  753 #             -size    =>2,
  754 #
  755 #         ), "\n",CGI::br();
  756 #       warn "merge keys ", join( " ",@merge_keys);
  757 #############################################################################################
  758 # merge file fragment and message text area field
  759 #############################################################################################
  760     my @tmp2;
  761         eval{  @tmp2= @{$rh_merge_data->{ $db->getUser($preview_user)->student_id  }  };}; # checked
  762         if ($@ and $merge_file ne 'None') {
  763       print "No merge data for $preview_user in merge file: &lt;$merge_file&gt;",CGI::br();
  764         } else {
  765       print CGI::pre("",data_format(1..($#tmp2+1)),"<br>", data_format2(@tmp2));
  766     }
  767 #create a textbox with the subject and a textarea with the message
  768 #print actual body of message
  769 
  770   print  "\n", CGI::p( $self->{message}) if defined($self->{message});
  771     print  "\n", CGI::p( CGI::textarea(-name=>'body', -default=>$text, -rows=>$rows, -cols=>$columns, -override=>1));
  772 
  773 #############################################################################################
  774 # action button table
  775 #############################################################################################
  776   print    CGI::table( { -border=>2,-cellpadding=>4},
  777          CGI::Tr( {},
  778            CGI::td({}, CGI::submit(-name=>'action', -value=>'Send Email') ), "\n",
  779            CGI::td({}, CGI::submit(-name=>'action', -value=>'Save')," to $output_file"), " \n",
  780            CGI::td({}, CGI::submit(-name=>'action', -value=>'Save as:'),
  781                    CGI::textfield(-name=>'savefilename', -size => 20, -value=> "$output_file", -override=>1)
  782            ), "\n",
  783            CGI::td(CGI::submit(-name=>'action', -value=>'Save as Default')),
  784         )
  785   );
  786 
  787 ##############################################################################################################
  788 
  789   print CGI::end_form();
  790   return "";
  791 }
  792 
  793 ##############################################################################
  794 # Utility methods
  795 ##############################################################################
  796 
  797 sub saveProblem {
  798     my $self      = shift;
  799   my ($body, $probFileName)= @_;
  800   local(*PROBLEM);
  801   open (PROBLEM, ">$probFileName") ||
  802     $self->addbadmessage(CGI::p("Could not open $probFileName for writing.
  803             Check that the  permissions for this problem are 660 (-rw-rw----)"));
  804   print PROBLEM $body if -w $probFileName;
  805   close PROBLEM;
  806   chmod 0660, "$probFileName" ||
  807                $self->addbadmessage(CGI::p("CAN'T CHANGE PERMISSIONS ON FILE $probFileName"));
  808 }
  809 
  810 sub read_input_file {
  811   my $self         = shift;
  812   my $filePath     = shift;
  813   my ($text, @text);
  814   my $header = '';
  815   my ($subject, $from, $replyTo);
  816   local(*FILE);
  817   if (-e "$filePath" and -r "$filePath") {
  818     open FILE, "$filePath" || do { $self->addbadmessage(CGI::p("Can't open $filePath")); return};
  819     while ($header !~ s/Message:\s*$//m and not eof(FILE)) {
  820       $header .= <FILE>;
  821     }
  822     $text = join( '', <FILE>);
  823     $text =~ s/^\s*//; # remove initial white space if any.
  824     $header         =~ /^From:\s(.*)$/m;
  825     $from           = $1 or $from = $self->{defaultFrom};
  826 
  827     $header         =~ /^Reply-To:\s(.*)$/m;
  828     $replyTo        = $1 or $replyTo = $self->{defaultReply};
  829 
  830     $header         =~ /^Subject:\s(.*)$/m;
  831     $subject        = $1;
  832 
  833   } else {
  834     $from           = $self->{defaultFrom};
  835     $replyTo        = $self->{defaultReply};
  836     $text           =  (-e "$filePath") ? "FIXME file $filePath can't be read" :"FIXME file $filePath doesn't exist";
  837     $subject        = $self->{defaultSubject};
  838   }
  839   return ($from, $replyTo, $subject, \$text);
  840 }
  841 
  842 
  843 sub get_message_file_names {
  844   my $self         = shift;
  845   return $self->read_dir($self->{ce}->{courseDirs}->{email}, '\\.msg$');
  846 }
  847 sub get_merge_file_names   {
  848   my $self         = shift;
  849   return 'None', $self->read_dir($self->{ce}->{courseDirs}->{scoring}, '\\.csv$'); #FIXME ? check that only readable files are listed.
  850 }
  851 
  852 sub mail_message_to_recipients {
  853   my $self                  = shift;
  854   my $r                     = $self->r;
  855   my $ce                    = $r->ce;
  856   my $subject               = $self->{subject};
  857   my $from                  = $self->{from};
  858   my @recipients            = @{$self->{ra_send_to}};
  859   my $rh_merge_data         = $self->{rh_merge_data};
  860   my $merge_file            = $self->{merge_file};
  861   my $result_message        = '';
  862   my $failed_messages        = 0;
  863 
  864   foreach my $recipient (@recipients) {
  865       # warn "FIXME sending email to $recipient";
  866       my $error_messages = '';
  867       my $ur      = $self->{db}->getUser($recipient); #checked
  868       unless ($ur) {
  869         $error_messages .= "Record for user $recipient not found\n";
  870         next;
  871       }
  872       unless ($ur->email_address) {
  873         $error_messages .="User $recipient does not have an email address -- skipping\n";
  874         next;
  875       }
  876       my $msg = eval { $self->process_message($ur,$rh_merge_data) };
  877       $error_messages .= "There were errors in processing user $recipient, merge file $merge_file. \n$@\n" if $@;
  878       my $mailer = Mail::Sender->new({
  879         from      => $ce->{mail}{smtpSender},
  880         fake_from => $from,
  881         to        => $ur->email_address,
  882         smtp      => $self->{smtpServer},
  883         subject   => $subject,
  884         headers   => "X-Remote-Host: ".$self->{remote_host},
  885       });
  886       unless (ref $mailer) {
  887         $error_messages .= "Failed to create a mailer for user $recipient: $Mail::Sender::Error\n";
  888         next;
  889       }
  890       unless (ref $mailer->Open()) {
  891         $error_messages .= "Failed to open the mailer for user $recipient: $Mail::Sender::Error\n";
  892         next;
  893       }
  894       my $MAIL         = $mailer->GetHandle() || ($error_messages .= "Couldn't get mailer handle \n");
  895       print $MAIL        $msg                 || ($error_messages .= "Couldn't print to $MAIL");
  896       close $MAIL                             || ($error_messages .= "Couldn't close $MAIL");
  897         #warn "FIXME mailed to $recipient: ", $ur->email_address, " from $from subject $subject Errors: $error_messages";
  898         $failed_messages++ if $error_messages;
  899         $result_message .= $error_messages;
  900     }
  901     my $courseName = $self->r->urlpath->arg("courseID");
  902     my $number_of_recipients = scalar(@recipients) - $failed_messages;
  903     $result_message = <<EndText.$result_message;
  904 
  905       A message with the subject line
  906                    $subject
  907       has been sent to
  908       $number_of_recipients recipient(s) in the class $courseName.
  909       There were $failed_messages message(s) that could not be delivered.
  910 
  911 EndText
  912 
  913 }
  914 sub email_notification {
  915   my $self = shift;
  916   my $result_message = shift;
  917   # find info on mailer and sender
  918   # use the defaultFrom address.
  919 
  920   # find info on instructor recipient and message
  921   my $subject="WeBWorK email sent";
  922 
  923   my $mailing_errors = "";
  924   # open MAIL handle
  925   my $mailer = Mail::Sender->new({
  926     from => $self->{defaultFrom},
  927     to   => $self->{defaultFrom},
  928     smtp    => $self->{smtpServer},
  929     subject => $subject,
  930     headers => "X-Remote-Host: ".$self->{remote_host},
  931   });
  932   unless (ref $mailer) {
  933     $mailing_errors .= "Failed to create a mailer: $Mail::Sender::Error";
  934     return "";
  935   }
  936   unless (ref $mailer->Open()) {
  937     $mailing_errors .= "Failed to open the mailer: $Mail::Sender::Error";
  938     return "";
  939   }
  940   my $MAIL = $mailer->GetHandle();
  941   # print message
  942   print $MAIL $result_message;
  943   # clean up
  944   close $MAIL;
  945 
  946     warn "instructor message sent to ", $self->{defaultFrom};
  947 
  948 }
  949 sub getRecord {
  950   my $self    = shift;
  951   my $line    = shift;
  952   my $delimiter   = shift;
  953   $delimiter       = ',' unless defined($delimiter);
  954 
  955         #       Takes a delimited line as a parameter and returns an
  956         #       array.  Note that all white space is removed.  If the
  957         #       last field is empty, the last element of the returned
  958         #       array is also empty (unlike what the perl split command
  959         #       would return).  E.G. @lineArray=&getRecord(\$delimitedLine).
  960 
  961         my(@lineArray);
  962         $line.="${delimiter}___";                       # add final field which must be non-empty
  963         @lineArray = split(/\s*${delimiter}\s*/,$line); # split line into fields
  964         $lineArray[0] =~s/^\s*//;                       # remove white space from first element
  965         pop @lineArray;                                 # remove the last artificial field
  966         @lineArray;
  967 }
  968 
  969 sub process_message {
  970   my $self          = shift;
  971   my $ur            = shift;
  972   my $rh_merge_data = shift;
  973   my $for_preview   = shift;
  974   my $text          = defined($self->{r_text}) ? ${ $self->{r_text} }:
  975                           'FIXME no text was produced by initialization!!';
  976   my $merge_file      = ( defined($self->{merge_file}) ) ? $self->{merge_file} : 'None';
  977 
  978   my $status_name = $self->r->ce->status_abbrev_to_name($ur->status);
  979   $status_name = $ur->status unless defined $status_name;
  980 
  981   #user macros that can be used in the email message
  982   my $SID           = $ur->student_id;
  983   my $FN            = $ur->first_name;
  984   my $LN            = $ur->last_name;
  985   my $SECTION       = $ur->section;
  986   my $RECITATION    = $ur->recitation;
  987   my $STATUS        = $status_name;
  988   my $EMAIL         = $ur->email_address;
  989   my $LOGIN         = $ur->user_id;
  990 
  991   # get record from merge file
  992   # FIXME this is inefficient.  The info should be cached
  993   my @COL            = defined($rh_merge_data->{$SID}) ? @{$rh_merge_data->{$SID} } : ();
  994   if ($merge_file ne 'None' and not defined($rh_merge_data->{$SID}) and $for_preview) {
  995     $self->addbadmessage(CGI::p("No merge data for student id:$SID; name:$FN $LN; login:$LOGIN"));
  996   }
  997   unshift(@COL,"");     ## this makes COL[1] the first column
  998   my $endCol = @COL;
  999   # for safety, only evaluate special variables
 1000   my $msg = $text;
 1001   $msg =~ s/\$SID/$SID/ge;
 1002   $msg =~ s/\$LN/$LN/ge;
 1003   $msg =~ s/\$FN/$FN/ge;
 1004   $msg =~ s/\$STATUS/$STATUS/ge;
 1005   $msg =~ s/\$SECTION/$SECTION/ge;
 1006   $msg =~ s/\$RECITATION/$RECITATION/ge;
 1007   $msg =~ s/\$EMAIL/$EMAIL/ge;
 1008   $msg =~ s/\$LOGIN/$LOGIN/ge;
 1009   if (defined($COL[1])) {   # prevents extraneous error messages.
 1010     $msg =~ s/\$COL\[(\-?\d+)\]/$COL[$1]/ge
 1011   }
 1012   else {            # prevents extraneous $COL's in email message
 1013     $msg =~ s/\$COL\[(\-?\d+)\]//g
 1014   }
 1015 
 1016   $msg =~ s/\r//g;
 1017 
 1018   if ($for_preview) {
 1019     my @preview_COL = @COL;
 1020     shift @preview_COL; ## shift back for preview
 1021     my $preview_header =  CGI::pre({},data_format(1..($#COL)),"<br>", data_format2(@preview_COL)).
 1022                           CGI::h3( "This sample mail would be sent to $EMAIL");
 1023     return $msg, $preview_header;
 1024   } else {
 1025     return $msg;
 1026   }
 1027 }
 1028 
 1029 
 1030 # Ý sub data_format {
 1031 #
 1032 # Ý Ý Ý Ý Ýmap {$_ =~s/\s/\./g;$_} Ý Ý map {sprintf('%-8.8s',$_);} Ý@_;
 1033  sub data_format {
 1034       map {"COL[$_]".'&nbsp;'x(3-length($_));}  @_;  # problems if $_ has length bigger than 4
 1035  }
 1036   sub data_format2 {
 1037     map {$_ =~s/\s/&nbsp;/g;$_}  map {sprintf('%-8.8s',$_);} @_;
 1038  }
 1039 1;

aubreyja at gmail dot com
ViewVC Help
Powered by ViewVC 1.0.9