--- loncom/interface/loncommon.pm 2001/07/30 22:24:34 1.4
+++ loncom/interface/loncommon.pm 2011/01/12 04:58:54 1.948.2.24
@@ -1,19 +1,3417 @@
-# The LearningOnline Network
+# The LearningOnline Network with CAPA
# a pile of common routines
-# 2/13 Guy Albertelli
+#
+# $Id: loncommon.pm,v 1.948.2.24 2011/01/12 04:58:54 raeburn Exp $
+#
+# Copyright Michigan State University Board of Trustees
+#
+# This file is part of the LearningOnline Network with CAPA (LON-CAPA).
+#
+# LON-CAPA is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# LON-CAPA 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 the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with LON-CAPA; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+#
+# /home/httpd/html/adm/gpl.txt
+#
+# http://www.lon-capa.org/
+#
# Makes a table out of the previous attempts
# Inputs result_from_symbread, user, domain, course_id
+# Reads in non-network-related .tab files
+# POD header:
+
+=pod
+
+=head1 NAME
+
+Apache::loncommon - pile of common routines
+
+=head1 SYNOPSIS
+
+Common routines for manipulating connections, student answers,
+ domains, common Javascript fragments, etc.
+
+=head1 OVERVIEW
+
+A collection of commonly used subroutines that don't have a natural
+home anywhere else. This collection helps remove
+redundancy from other modules and increase efficiency of memory usage.
+
+=cut
+
+# End of POD header
package Apache::loncommon;
use strict;
-use Apache::Constants qw(:common);
-use Apache::lonmsg();
+use Apache::lonnet;
+use GDBM_File;
+use POSIX qw(strftime mktime);
+use Apache::lonmenu();
+use Apache::lonenc();
+use Apache::lonlocal;
+use Apache::lonnet();
+use HTML::Entities;
+use Apache::lonhtmlcommon();
+use Apache::loncoursedata();
+use Apache::lontexconvert();
+use Apache::lonclonecourse();
+use LONCAPA qw(:DEFAULT :match);
+use DateTime::TimeZone;
+use DateTime::Locale::Catalog;
+
+# ---------------------------------------------- Designs
+use vars qw(%defaultdesign);
+
+my $readit;
+
+
+##
+## Global Variables
+##
+
+
+# ----------------------------------------------- SSI with retries:
+#
+
+=pod
+
+=head1 Server Side include with retries:
+
+=over 4
+
+=item * &ssi_with_retries(resource,retries form)
+
+Performs an ssi with some number of retries. Retries continue either
+until the result is ok or until the retry count supplied by the
+caller is exhausted.
+
+Inputs:
+
+=over 4
+
+resource - Identifies the resource to insert.
+
+retries - Count of the number of retries allowed.
+
+form - Hash that identifies the rendering options.
+
+=back
+
+Returns:
+
+=over 4
+
+content - The content of the response. If retries were exhausted this is empty.
+
+response - The response from the last attempt (which may or may not have been successful.
+
+=back
+
+=back
+
+=cut
+
+sub ssi_with_retries {
+ my ($resource, $retries, %form) = @_;
+
+
+ my $ok = 0; # True if we got a good response.
+ my $content;
+ my $response;
+
+ # Try to get the ssi done. within the retries count:
+
+ do {
+ ($content, $response) = &Apache::lonnet::ssi($resource, %form);
+ $ok = $response->is_success;
+ if (!$ok) {
+ &Apache::lonnet::logthis("Failed ssi_with_retries on $resource: ".$response->is_success.', '.$response->code.', '.$response->message);
+ }
+ $retries--;
+ } while (!$ok && ($retries > 0));
+
+ if (!$ok) {
+ $content = ''; # On error return an empty content.
+ }
+ return ($content, $response);
+
+}
+
+
+
+# ----------------------------------------------- Filetypes/Languages/Copyright
+my %language;
+my %supported_language;
+my %cprtag;
+my %scprtag;
+my %fe; my %fd; my %fm;
+my %category_extensions;
+
+# ---------------------------------------------- Thesaurus variables
+#
+# %Keywords:
+# A hash used by &keyword to determine if a word is considered a keyword.
+# $thesaurus_db_file
+# Scalar containing the full path to the thesaurus database.
+
+my %Keywords;
+my $thesaurus_db_file;
+
+#
+# Initialize values from language.tab, copyright.tab, filetypes.tab,
+# thesaurus.tab, and filecategories.tab.
+#
+BEGIN {
+ # Variable initialization
+ $thesaurus_db_file = $Apache::lonnet::perlvar{'lonTabDir'}."/thesaurus.db";
+ #
+ unless ($readit) {
+# ------------------------------------------------------------------- languages
+ {
+ my $langtabfile = $Apache::lonnet::perlvar{'lonTabDir'}.
+ '/language.tab';
+ if ( open(my $fh,"<$langtabfile") ) {
+ while (my $line = <$fh>) {
+ next if ($line=~/^\#/);
+ chomp($line);
+ my ($key,$two,$country,$three,$enc,$val,$sup)=(split(/\t/,$line));
+ $language{$key}=$val.' - '.$enc;
+ if ($sup) {
+ $supported_language{$key}=$sup;
+ }
+ }
+ close($fh);
+ }
+ }
+# ------------------------------------------------------------------ copyrights
+ {
+ my $copyrightfile = $Apache::lonnet::perlvar{'lonIncludes'}.
+ '/copyright.tab';
+ if ( open (my $fh,"<$copyrightfile") ) {
+ while (my $line = <$fh>) {
+ next if ($line=~/^\#/);
+ chomp($line);
+ my ($key,$val)=(split(/\s+/,$line,2));
+ $cprtag{$key}=$val;
+ }
+ close($fh);
+ }
+ }
+# ----------------------------------------------------------- source copyrights
+ {
+ my $sourcecopyrightfile = $Apache::lonnet::perlvar{'lonIncludes'}.
+ '/source_copyright.tab';
+ if ( open (my $fh,"<$sourcecopyrightfile") ) {
+ while (my $line = <$fh>) {
+ next if ($line =~ /^\#/);
+ chomp($line);
+ my ($key,$val)=(split(/\s+/,$line,2));
+ $scprtag{$key}=$val;
+ }
+ close($fh);
+ }
+ }
+
+# -------------------------------------------------------------- default domain designs
+ my $designdir=$Apache::lonnet::perlvar{'lonTabDir'}.'/lonDomColors';
+ my $designfile = $designdir.'/default.tab';
+ if ( open (my $fh,"<$designfile") ) {
+ while (my $line = <$fh>) {
+ next if ($line =~ /^\#/);
+ chomp($line);
+ my ($key,$val)=(split(/\=/,$line));
+ if ($val) { $defaultdesign{$key}=$val; }
+ }
+ close($fh);
+ }
+
+# ------------------------------------------------------------- file categories
+ {
+ my $categoryfile = $Apache::lonnet::perlvar{'lonTabDir'}.
+ '/filecategories.tab';
+ if ( open (my $fh,"<$categoryfile") ) {
+ while (my $line = <$fh>) {
+ next if ($line =~ /^\#/);
+ chomp($line);
+ my ($extension,$category)=(split(/\s+/,$line,2));
+ push @{$category_extensions{lc($category)}},$extension;
+ }
+ close($fh);
+ }
+
+ }
+# ------------------------------------------------------------------ file types
+ {
+ my $typesfile = $Apache::lonnet::perlvar{'lonTabDir'}.
+ '/filetypes.tab';
+ if ( open (my $fh,"<$typesfile") ) {
+ while (my $line = <$fh>) {
+ next if ($line =~ /^\#/);
+ chomp($line);
+ my ($ending,$emb,$mime,$descr)=split(/\s+/,$line,4);
+ if ($descr ne '') {
+ $fe{$ending}=lc($emb);
+ $fd{$ending}=$descr;
+ if ($mime ne 'unk') { $fm{$ending}=$mime; }
+ }
+ }
+ close($fh);
+ }
+ }
+ &Apache::lonnet::logthis(
+ "INFO: Read file types");
+ $readit=1;
+ } # end of unless($readit)
+
+}
+
+###############################################################
+## HTML and Javascript Helper Functions ##
+###############################################################
+
+=pod
+
+=head1 HTML and Javascript Functions
+
+=over 4
+
+=item * &browser_and_searcher_javascript()
+
+XXReturns a string
+containing javascript with two functions, C and
+C. Returned string does not contain EscriptE
+tags.
+
+=item * &openbrowser(formname,elementname,only,omit) [javascript]
+
+inputs: formname, elementname, only, omit
+
+formname and elementname indicate the name of the html form and name of
+the element that the results of the browsing selection are to be placed in.
+
+Specifying 'only' will restrict the browser to displaying only files
+with the given extension. Can be a comma separated list.
+
+Specifying 'omit' will restrict the browser to NOT displaying files
+with the given extension. Can be a comma separated list.
+
+=item * &opensearcher(formname,elementname) [javascript]
+
+Inputs: formname, elementname
+
+formname and elementname specify the name of the html form and the name
+of the element the selection from the search results will be placed in.
+
+=cut
+
+sub browser_and_searcher_javascript {
+ my ($mode)=@_;
+ if (!defined($mode)) { $mode='edit'; }
+ my $resurl=&escape_single(&lastresurl());
+ return <
+END
+}
+
+sub lastresurl {
+ if ($env{'environment.lastresurl'}) {
+ return $env{'environment.lastresurl'}
+ } else {
+ return '/res';
+ }
+}
+
+sub storeresurl {
+ my $resurl=&Apache::lonnet::clutter(shift);
+ unless ($resurl=~/^\/res/) { return 0; }
+ $resurl=~s/\/$//;
+ &Apache::lonnet::put('environment',{'lastresurl' => $resurl});
+ &Apache::lonnet::appenv({'environment.lastresurl' => $resurl});
+ return 1;
+}
+
+sub studentbrowser_javascript {
+ unless (
+ (($env{'request.course.id'}) &&
+ (&Apache::lonnet::allowed('srm',$env{'request.course.id'})
+ || &Apache::lonnet::allowed('srm',$env{'request.course.id'}.
+ '/'.$env{'request.course.sec'})
+ ))
+ || ($env{'request.role'}=~/^(au|dc|su)/)
+ ) { return ''; }
+ return (<<'ENDSTDBRW');
+
+ENDSTDBRW
+}
+
+sub selectstudent_link {
+ my ($form,$unameele,$udomele,$courseadvonly)=@_;
+ my $callargs = "'".$form."','".$unameele."','".$udomele."'";
+ if ($env{'request.course.id'}) {
+ if (!&Apache::lonnet::allowed('srm',$env{'request.course.id'})
+ && !&Apache::lonnet::allowed('srm',$env{'request.course.id'}.
+ '/'.$env{'request.course.sec'})) {
+ return '';
+ }
+ if ($courseadvonly) {
+ $callargs .= ",'',1,1";
+ }
+ return ''.
+ ''.
+ &mt('Select User').'';
+ }
+ if ($env{'request.role'}=~/^(au|dc|su)/) {
+ $callargs .= ",1";
+ return ''.
+ ''.
+ &mt('Select User').'';
+ }
+ return '';
+}
+
+sub authorbrowser_javascript {
+ return <<"ENDAUTHORBRW";
+
+ENDAUTHORBRW
+}
+
+sub coursebrowser_javascript {
+ my ($domainfilter,$sec_element,$formname,$role_element,$crstype) = @_;
+ my $wintitle = 'Course_Browser';
+ if ($crstype eq 'Community') {
+ $wintitle = 'Community_Browser';
+ }
+ my $id_functions = &javascript_index_functions();
+ my $output = '
+';
+ return $output;
+}
+
+sub javascript_index_functions {
+ return <<"ENDJS";
+
+function getFormIdByName(formname) {
+ for (var i=0;i -1) {
+ var domid = getIndexByName(formid,udom);
+ if (domid > -1) {
+ if (document.forms[formid].elements[domid].type == 'select-one') {
+ userdom=document.forms[formid].elements[domid].options[document.forms[formid].elements[domid].selectedIndex].value;
+ }
+ if (document.forms[formid].elements[domid].type == 'hidden') {
+ userdom=document.forms[formid].elements[domid].value;
+ }
+ }
+ }
+ return userdom;
+}
+
+ENDJS
+
+}
+
+sub userbrowser_javascript {
+ my $id_functions = &javascript_index_functions();
+ return <<"ENDUSERBRW";
+
+function openuserbrowser(formname,uname,udom,ulast,ufirst,uemail,hideudom,crsdom,caller) {
+ var url = '/adm/pickuser?';
+ var userdom = getDomainFromSelectbox(formname,udom);
+ if (userdom != null) {
+ if (userdom != '') {
+ url += 'srchdom='+userdom+'&';
+ }
+ }
+ url += 'form=' + formname + '&unameelement='+uname+
+ '&udomelement='+udom+
+ '&ulastelement='+ulast+
+ '&ufirstelement='+ufirst+
+ '&uemailelement='+uemail+
+ '&hideudomelement='+hideudom+
+ '&coursedom='+crsdom;
+ if ((caller != null) && (caller != undefined)) {
+ url += '&caller='+caller;
+ }
+ var title = 'User_Browser';
+ var options = 'scrollbars=1,resizable=1,menubar=0';
+ options += ',width=700,height=600';
+ var stdeditbrowser = open(url,title,options,'1');
+ stdeditbrowser.focus();
+}
+
+function fix_domain (formname,udom,origdom,uname) {
+ var formid = getFormIdByName(formname);
+ if (formid > -1) {
+ var unameid = getIndexByName(formid,uname);
+ var domid = getIndexByName(formid,udom);
+ var hidedomid = getIndexByName(formid,origdom);
+ if (hidedomid > -1) {
+ var fixeddom = document.forms[formid].elements[hidedomid].value;
+ var unameval = document.forms[formid].elements[unameid].value;
+ if ((fixeddom != '') && (fixeddom != undefined) && (fixeddom != null) && (unameval != '') && (unameval != undefined) && (unameval != null)) {
+ if (domid > -1) {
+ var slct = document.forms[formid].elements[domid];
+ if (slct.type == 'select-one') {
+ var i;
+ for (i=0;i'
+ ."".$linktext.''
+ .'';
+}
+
+sub selectauthor_link {
+ my ($form,$udom)=@_;
+ return ''.
+ &mt('Select Author').'';
+}
+
+sub selectuser_link {
+ my ($form,$unameelem,$domelem,$lastelem,$firstelem,$emailelem,$hdomelem,
+ $coursedom,$linktext,$caller) = @_;
+ return ''.$linktext.'';
+}
+
+sub check_uncheck_jscript {
+ my $jscript = <<"ENDSCRT";
+function checkAll(field) {
+ if (field.length > 0) {
+ for (i = 0; i < field.length; i++) {
+ field[i].checked = true ;
+ }
+ } else {
+ field.checked = true
+ }
+}
+
+function uncheckAll(field) {
+ if (field.length > 0) {
+ for (i = 0; i < field.length; i++) {
+ field[i].checked = false ;
+ }
+ } else {
+ field.checked = false ;
+ }
+}
+ENDSCRT
+ return $jscript;
+}
+
+sub select_timezone {
+ my ($name,$selected,$onchange,$includeempty)=@_;
+ my $output='";
+ return $output;
+}
+
+sub select_datelocale {
+ my ($name,$selected,$onchange,$includeempty)=@_;
+ my $output='";
+ return $output;
+}
+
+sub select_language {
+ my ($name,$selected,$includeempty) = @_;
+ my %langchoices;
+ if ($includeempty) {
+ %langchoices = ('' => 'No language preference');
+ }
+ foreach my $id (&languageids()) {
+ my $code = &supportedlanguagecode($id);
+ if ($code) {
+ $langchoices{$code} = &plainlanguagedescription($id);
+ }
+ }
+ return &select_form($selected,$name,\%langchoices);
+}
+
+=pod
+
+=item * &linked_select_forms(...)
+
+linked_select_forms returns a string containing a block
+and html for two
'."\n".'
'.
+ &mt('LON-CAPA can make the required changes to your HTML file.').'
'.&mt('Updated [quant,_1,reference] in [_2].',
+ $count,''.
+ $container.'').'
';
+ } else {
+ $output = '
'.
+ &mt('Error: could not update [_1].',
+ ''.
+ $container.'').'
';
+ }
+ }
+ }
+ } else {
+ &logthis('Failed to parse '.$container.
+ ' to modify references: '.$parse_result);
+ }
+ }
+ return $output;
+}
+
+sub check_for_existing {
+ my ($path,$fname,$element) = @_;
+ my ($state,$msg);
+ if (-d $path.'/'.$fname) {
+ $state = 'exists';
+ $msg = &mt('Unable to upload [_1]. A directory by that name was found in [_2].',''.$fname.'',$path);
+ } elsif (-e $path.'/'.$fname) {
+ $state = 'exists';
+ $msg = &mt('Unable to upload [_1]. A file by that name was found in [_2].',''.$fname.'',$path);
+ }
+ if ($state eq 'exists') {
+ $msg = ''.$msg.' ';
+ }
+ return ($state,$msg);
+}
+
+sub check_for_upload {
+ my ($path,$fname,$group,$element,$portfolio_root,$port_path,
+ $disk_quota,$current_disk_usage,$uname,$udom) = @_;
+ my $filesize = length($env{'form.'.$element});
+ if (!$filesize) {
+ my $msg = ''.
+ &mt('Unable to upload [_1]. (size = [_2] bytes)',
+ ''.$fname.'',
+ $filesize).' '.
+ &mt('Either the file you attempted to upload was empty, or your web browser was unable to read its contents.').' ';
+ '';
+ return ('zero_bytes',$msg);
+ }
+ $filesize = $filesize/1000; #express in k (1024?)
+ my $getpropath = 1;
+ my @dir_list = &Apache::lonnet::dirlist($portfolio_root.$path,$udom,$uname,
+ $getpropath);
+ my $found_file = 0;
+ my $locked_file = 0;
+ my @lockers;
+ my $navmap;
+ if ($env{'request.course.id'}) {
+ $navmap = Apache::lonnavmaps::navmap->new();
+ }
+ foreach my $line (@dir_list) {
+ my ($file_name,$rest)=split(/\&/,$line,2);
+ if ($file_name eq $fname){
+ $file_name = $path.$file_name;
+ if ($group ne '') {
+ $file_name = $group.$file_name;
+ }
+ $found_file = 1;
+ if (&Apache::lonnet::is_locked($file_name,$udom,$uname,\@lockers) eq 'true') {
+ foreach my $lock (@lockers) {
+ if (ref($lock) eq 'ARRAY') {
+ my ($symb,$crsid) = @{$lock};
+ if ($crsid eq $env{'request.course.id'}) {
+ if (ref($navmap)) {
+ my $res = $navmap->getBySymb($symb);
+ foreach my $part (@{$res->parts()}) {
+ my ($slot_status,$slot_time,$slot_name)=$res->check_for_slot($part);
+ unless (($slot_status == $res->RESERVED) ||
+ ($slot_status == $res->RESERVED_LOCATION)) {
+ $locked_file = 1;
+ }
+ }
+ } else {
+ $locked_file = 1;
+ }
+ } else {
+ $locked_file = 1;
+ }
+ }
+ }
+ } else {
+ my @info = split(/\&/,$rest);
+ my $currsize = $info[6]/1000;
+ if ($currsize < $filesize) {
+ my $extra = $filesize - $currsize;
+ if (($current_disk_usage + $extra) > $disk_quota) {
+ my $msg = ''.
+ &mt('Unable to upload [_1]. (size = [_2] kilobytes). Disk quota will be exceeded if existing (smaller) file with same name (size = [_3] kilobytes) is replaced.',
+ ''.$fname.'',$filesize,$currsize).''.
+ ' '.&mt('Disk quota is [_1] kilobytes. Your current disk usage is [_2] kilobytes.',
+ $disk_quota,$current_disk_usage);
+ return ('will_exceed_quota',$msg);
+ }
+ }
+ }
+ }
+ }
+ if (($current_disk_usage + $filesize) > $disk_quota){
+ my $msg = ''.
+ &mt('Unable to upload [_1]. (size = [_2] kilobytes). Disk quota will be exceeded.',''.$fname.'',$filesize).''.
+ ' '.&mt('Disk quota is [_1] kilobytes. Your current disk usage is [_2] kilobytes.',$disk_quota,$current_disk_usage);
+ return ('will_exceed_quota',$msg);
+ } elsif ($found_file) {
+ if ($locked_file) {
+ my $msg = '';
+ $msg .= &mt('Unable to upload [_1]. A locked file by that name was found in [_2].',''.$fname.'',''.$port_path.$env{'form.currentpath'}.'');
+ $msg .= ' ';
+ $msg .= &mt('You will be able to rename or delete existing [_1] after a grade has been assigned.',''.$fname.'');
+ return ('file_locked',$msg);
+ } else {
+ my $msg = '';
+ $msg .= &mt(' A file by that name: [_1] was found in [_2].',''.$fname.'',$port_path.$env{'form.currentpath'});
+ $msg .= '';
+ return ('existingfile',$msg);
+ }
+ }
+}
+
+sub check_for_traversal {
+ my ($path,$url,$toplevel) = @_;
+ my @parts=split(/\//,$path);
+ my $cleanpath;
+ my $fullpath = $url;
+ for (my $i=0;$i<@parts;$i++) {
+ next if ($parts[$i] eq '.');
+ if ($parts[$i] eq '..') {
+ $fullpath =~ s{([^/]+/)$}{};
+ } else {
+ $fullpath .= $parts[$i].'/';
+ }
+ }
+ if ($fullpath =~ /^\Q$url\E(.*)$/) {
+ $cleanpath = $1;
+ } elsif ($fullpath =~ /^\Q$toplevel\E(.*)$/) {
+ my $curr_toprel = $1;
+ my @parts = split(/\//,$curr_toprel);
+ my ($url_toprel) = ($url =~ /^\Q$toplevel\E(.*)$/);
+ my @urlparts = split(/\//,$url_toprel);
+ my $doubledots;
+ my $startdiff = -1;
+ for (my $i=0; $i<@urlparts; $i++) {
+ if ($startdiff == -1) {
+ unless ($urlparts[$i] eq $parts[$i]) {
+ $startdiff = $i;
+ $doubledots .= '../';
+ }
+ } else {
+ $doubledots .= '../';
+ }
+ }
+ if ($startdiff > -1) {
+ $cleanpath = $doubledots;
+ for (my $i=$startdiff; $i<@parts; $i++) {
+ $cleanpath .= $parts[$i].'/';
+ }
+ }
+ }
+ $cleanpath =~ s{(/)$}{};
+ return $cleanpath;
+}
+
+=pod
+
+=back
+
+=head1 CSV Upload/Handling functions
+
+=over 4
+
+=item * &upfile_store($r)
+
+Store uploaded file, $r should be the HTTP Request object,
+needs $env{'form.upfile'}
+returns $datatoken to be put into hidden field
+
+=cut
+
+sub upfile_store {
+ my $r=shift;
+ $env{'form.upfile'}=~s/\r/\n/gs;
+ $env{'form.upfile'}=~s/\f/\n/gs;
+ $env{'form.upfile'}=~s/\n+/\n/gs;
+ $env{'form.upfile'}=~s/\n+$//gs;
+
+ my $datatoken=$env{'user.name'}.'_'.$env{'user.domain'}.
+ '_enroll_'.$env{'request.course.id'}.'_'.time.'_'.$$;
+ {
+ my $datafile = $r->dir_config('lonDaemons').
+ '/tmp/'.$datatoken.'.tmp';
+ if ( open(my $fh,">$datafile") ) {
+ print $fh $env{'form.upfile'};
+ close($fh);
+ }
+ }
+ return $datatoken;
+}
+
+=pod
+
+=item * &load_tmp_file($r)
+
+Load uploaded file from tmp, $r should be the HTTP Request object,
+needs $env{'form.datatoken'},
+sets $env{'form.upfile'} to the contents of the file
+
+=cut
+
+sub load_tmp_file {
+ my $r=shift;
+ my @studentdata=();
+ {
+ my $studentfile = $r->dir_config('lonDaemons').
+ '/tmp/'.$env{'form.datatoken'}.'.tmp';
+ if ( open(my $fh,"<$studentfile") ) {
+ @studentdata=<$fh>;
+ close($fh);
+ }
+ }
+ $env{'form.upfile'}=join('',@studentdata);
+}
+
+=pod
+
+=item * &upfile_record_sep()
+
+Separate uploaded file into records
+returns array of records,
+needs $env{'form.upfile'} and $env{'form.upfiletype'}
+
+=cut
+
+sub upfile_record_sep {
+ if ($env{'form.upfiletype'} eq 'xml') {
+ } else {
+ my @records;
+ foreach my $line (split(/\n/,$env{'form.upfile'})) {
+ if ($line=~/^\s*$/) { next; }
+ push(@records,$line);
+ }
+ return @records;
+ }
+}
+
+=pod
+
+=item * &record_sep($record)
+
+Separate a record into fields $record should be an item from the upfile_record_sep(), needs $env{'form.upfiletype'}
+
+=cut
+
+sub takeleft {
+ my $index=shift;
+ return substr('0000'.$index,-4,4);
+}
+
+sub record_sep {
+ my $record=shift;
+ my %components=();
+ if ($env{'form.upfiletype'} eq 'xml') {
+ } elsif ($env{'form.upfiletype'} eq 'space') {
+ my $i=0;
+ foreach my $field (split(/\s+/,$record)) {
+ $field=~s/^(\"|\')//;
+ $field=~s/(\"|\')$//;
+ $components{&takeleft($i)}=$field;
+ $i++;
+ }
+ } elsif ($env{'form.upfiletype'} eq 'tab') {
+ my $i=0;
+ foreach my $field (split(/\t/,$record)) {
+ $field=~s/^(\"|\')//;
+ $field=~s/(\"|\')$//;
+ $components{&takeleft($i)}=$field;
+ $i++;
+ }
+ } else {
+ my $separator=',';
+ if ($env{'form.upfiletype'} eq 'semisv') {
+ $separator=';';
+ }
+ my $i=0;
+# the character we are looking for to indicate the end of a quote or a record
+ my $looking_for=$separator;
+# do not add the characters to the fields
+ my $ignore=0;
+# we just encountered a separator (or the beginning of the record)
+ my $just_found_separator=1;
+# store the field we are working on here
+ my $field='';
+# work our way through all characters in record
+ foreach my $character ($record=~/(.)/g) {
+ if ($character eq $looking_for) {
+ if ($character ne $separator) {
+# Found the end of a quote, again looking for separator
+ $looking_for=$separator;
+ $ignore=1;
+ } else {
+# Found a separator, store away what we got
+ $components{&takeleft($i)}=$field;
+ $i++;
+ $just_found_separator=1;
+ $ignore=0;
+ $field='';
+ }
+ next;
+ }
+# single or double quotation marks after a separator indicate beginning of a quote
+# we are now looking for the end of the quote and need to ignore separators
+ if ((($character eq '"') || ($character eq "'")) && ($just_found_separator)) {
+ $looking_for=$character;
+ next;
+ }
+# ignore would be true after we reached the end of a quote
+ if ($ignore) { next; }
+ if (($just_found_separator) && ($character=~/\s/)) { next; }
+ $field.=$character;
+ $just_found_separator=0;
+ }
+# catch the very last entry, since we never encountered the separator
+ $components{&takeleft($i)}=$field;
+ }
+ return %components;
+}
+
+######################################################
+######################################################
+
+=pod
+
+=item * &upfile_select_html()
+
+Return HTML code to select a file from the users machine and specify
+the file type.
+
+=cut
+
+######################################################
+######################################################
+sub upfile_select_html {
+ my %Types = (
+ csv => &mt('CSV (comma separated values, spreadsheet)'),
+ semisv => &mt('Semicolon separated values'),
+ space => &mt('Space separated'),
+ tab => &mt('Tabulator separated'),
+# xml => &mt('HTML/XML'),
+ );
+ my $Str = ''.
+ ' '.&mt('Type').': \n";
+ return $Str;
+}
+
+sub get_samples {
+ my ($records,$toget) = @_;
+ my @samples=({});
+ my $got=0;
+ foreach my $rec (@$records) {
+ my %temp = &record_sep($rec);
+ if (! grep(/\S/, values(%temp))) { next; }
+ if (%temp) {
+ $samples[$got]=\%temp;
+ $got++;
+ if ($got == $toget) { last; }
+ }
+ }
+ return \@samples;
+}
+
+######################################################
+######################################################
+
+=pod
+
+=item * &csv_print_samples($r,$records)
+
+Prints a table of sample values from each column uploaded $r is an
+Apache Request ref, $records is an arrayref from
+&Apache::loncommon::upfile_record_sep
+
+=cut
+
+######################################################
+######################################################
+sub csv_print_samples {
+ my ($r,$records) = @_;
+ my $samples = &get_samples($records,5);
+
+ $r->print(&mt('Samples').' '.&start_data_table().
+ &start_data_table_header_row());
+ foreach my $sample (sort({$a <=> $b} keys(%{ $samples->[0] }))) {
+ $r->print('
');
+ if (defined($$hash{$sample})) { $r->print($$hash{$sample}); }
+ $r->print('
');
+ }
+ $r->print(&end_data_table_row());
+ }
+ $r->print(&end_data_table().' '."\n");
+}
+
+######################################################
+######################################################
+
+=pod
+
+=item * &csv_print_select_table($r,$records,$d)
+
+Prints a table to create associations between values and table columns.
+
+$r is an Apache Request ref,
+$records is an arrayref from &Apache::loncommon::upfile_record_sep,
+$d is an array of 2 element arrays (internal name, displayed name,defaultcol)
+
+=cut
+
+######################################################
+######################################################
+sub csv_print_select_table {
+ my ($r,$records,$d) = @_;
+ my $i=0;
+ my $samples = &get_samples($records,1);
+ $r->print(&mt('Associate columns with student attributes.')."\n".
+ &start_data_table().&start_data_table_header_row().
+ '
'.&mt('Attribute').'
'.
+ '
'.&mt('Column').'
'.
+ &end_data_table_header_row()."\n");
+ foreach my $array_ref (@$d) {
+ my ($value,$display,$defaultcol)=@{ $array_ref };
+ $r->print(&start_data_table_row().'
'.$display.'
');
+
+ $r->print('
'.&end_data_table_row()."\n");
+ $i++;
+ }
+ $r->print(&end_data_table());
+ $i--;
+ return $i;
+}
+
+######################################################
+######################################################
+
+=pod
+
+=item * &csv_samples_select_table($r,$records,$d)
+
+Prints a table of sample values from the upload and can make associate samples to internal names.
+
+$r is an Apache Request ref,
+$records is an arrayref from &Apache::loncommon::upfile_record_sep,
+$d is an array of 2 element arrays (internal name, displayed name)
+
+=cut
+
+######################################################
+######################################################
+sub csv_samples_select_table {
+ my ($r,$records,$d) = @_;
+ my $i=0;
+ #
+ my $max_samples = 5;
+ my $samples = &get_samples($records,$max_samples);
+ $r->print(&start_data_table().
+ &start_data_table_header_row().'
');
+ foreach my $line (0..($max_samples-1)) {
+ if (defined($samples->[$line]{$key})) {
+ $r->print($samples->[$line]{$key}." \n");
+ }
+ }
+ $r->print('
'.&end_data_table_row());
+ $i++;
+ }
+ $r->print(&end_data_table());
+ $i--;
+ return($i);
+}
+
+######################################################
+######################################################
+
+=pod
+
+=item * &clean_excel_name($name)
+
+Returns a replacement for $name which does not contain any illegal characters.
+
+=cut
+
+######################################################
+######################################################
+sub clean_excel_name {
+ my ($name) = @_;
+ $name =~ s/[:\*\?\/\\]//g;
+ if (length($name) > 31) {
+ $name = substr($name,0,31);
+ }
+ return $name;
+}
+
+=pod
+
+=item * &check_if_partid_hidden($id,$symb,$udom,$uname)
+
+Returns either 1 or undef
+
+1 if the part is to be hidden, undef if it is to be shown
+
+Arguments are:
+
+$id the id of the part to be checked
+$symb, optional the symb of the resource to check
+$udom, optional the domain of the user to check for
+$uname, optional the username of the user to check for
+
+=cut
+
+sub check_if_partid_hidden {
+ my ($id,$symb,$udom,$uname) = @_;
+ my $hiddenparts=&Apache::lonnet::EXT('resource.0.hiddenparts',
+ $symb,$udom,$uname);
+ my $truth=1;
+ #if the string starts with !, then the list is the list to show not hide
+ if ($hiddenparts=~s/^\s*!//) { $truth=undef; }
+ my @hiddenlist=split(/,/,$hiddenparts);
+ foreach my $checkid (@hiddenlist) {
+ if ($checkid =~ /^\s*\Q$id\E\s*$/) { return $truth; }
+ }
+ return !$truth;
+}
+
+
+############################################################
+############################################################
+
+=pod
+
+=back
+
+=head1 cgi-bin script and graphing routines
+
+=over 4
+
+=item * &get_cgi_id()
+
+Inputs: none
+
+Returns an id which can be used to pass environment variables
+to various cgi-bin scripts. These environment variables will
+be removed from the users environment after a given time by
+the routine &Apache::lonnet::transfer_profile_to_env.
+
+=cut
+
+############################################################
+############################################################
+my $uniq=0;
+sub get_cgi_id {
+ $uniq=($uniq+1)%100000;
+ return (time.'_'.$$.'_'.$uniq);
+}
+
+############################################################
+############################################################
+
+=pod
+
+=item * &DrawBarGraph()
+
+Facilitates the plotting of data in a (stacked) bar graph.
+Puts plot definition data into the users environment in order for
+graph.png to plot it. Returns an tag for the plot.
+The bars on the plot are labeled '1','2',...,'n'.
+
+Inputs:
+
+=over 4
+
+=item $Title: string, the title of the plot
+
+=item $xlabel: string, text describing the X-axis of the plot
+
+=item $ylabel: string, text describing the Y-axis of the plot
+
+=item $Max: scalar, the maximum Y value to use in the plot
+If $Max is < any data point, the graph will not be rendered.
+
+=item $colors: array ref holding the colors to be used for the data sets when
+they are plotted. If undefined, default values will be used.
+
+=item $labels: array ref holding the labels to use on the x-axis for the bars.
+
+=item @Values: An array of array references. Each array reference holds data
+to be plotted in a stacked bar chart.
+
+=item If the final element of @Values is a hash reference the key/value
+pairs will be added to the graph definition.
+
+=back
+
+Returns:
+
+An tag which references graph.png and the appropriate identifying
+information for the plot.
+
+=cut
+
+############################################################
+############################################################
+sub DrawBarGraph {
+ my ($Title,$xlabel,$ylabel,$Max,$colors,$labels,@Values)=@_;
+ #
+ if (! defined($colors)) {
+ $colors = ['#33ff00',
+ '#0033cc', '#990000', '#aaaa66', '#663399', '#ff9933',
+ '#66ccff', '#ff9999', '#cccc33', '#660000', '#33cc66',
+ ];
+ }
+ my $extra_settings = {};
+ if (ref($Values[-1]) eq 'HASH') {
+ $extra_settings = pop(@Values);
+ }
+ #
+ my $identifier = &get_cgi_id();
+ my $id = 'cgi.'.$identifier;
+ if (! @Values || ref($Values[0]) ne 'ARRAY') {
+ return '';
+ }
+ #
+ my @Labels;
+ if (defined($labels)) {
+ @Labels = @$labels;
+ } else {
+ for (my $i=0;$i<@{$Values[0]};$i++) {
+ push (@Labels,$i+1);
+ }
+ }
+ #
+ my $NumBars = scalar(@{$Values[0]});
+ if ($NumBars < scalar(@Labels)) { $NumBars = scalar(@Labels); }
+ my %ValuesHash;
+ my $NumSets=1;
+ foreach my $array (@Values) {
+ next if (! ref($array));
+ $ValuesHash{$id.'.data.'.$NumSets++} =
+ join(',',@$array);
+ }
+ #
+ my ($height,$width,$xskip,$bar_width) = (200,120,1,15);
+ if ($NumBars < 3) {
+ $width = 120+$NumBars*32;
+ $xskip = 1;
+ $bar_width = 30;
+ } elsif ($NumBars < 5) {
+ $width = 120+$NumBars*20;
+ $xskip = 1;
+ $bar_width = 20;
+ } elsif ($NumBars < 10) {
+ $width = 120+$NumBars*15;
+ $xskip = 1;
+ $bar_width = 15;
+ } elsif ($NumBars <= 25) {
+ $width = 120+$NumBars*11;
+ $xskip = 5;
+ $bar_width = 8;
+ } elsif ($NumBars <= 50) {
+ $width = 120+$NumBars*8;
+ $xskip = 5;
+ $bar_width = 4;
+ } else {
+ $width = 120+$NumBars*8;
+ $xskip = 5;
+ $bar_width = 4;
+ }
+ #
+ $Max = 1 if ($Max < 1);
+ if ( int($Max) < $Max ) {
+ $Max++;
+ $Max = int($Max);
+ }
+ $Title = '' if (! defined($Title));
+ $xlabel = '' if (! defined($xlabel));
+ $ylabel = '' if (! defined($ylabel));
+ $ValuesHash{$id.'.title'} = &escape($Title);
+ $ValuesHash{$id.'.xlabel'} = &escape($xlabel);
+ $ValuesHash{$id.'.ylabel'} = &escape($ylabel);
+ $ValuesHash{$id.'.y_max_value'} = $Max;
+ $ValuesHash{$id.'.NumBars'} = $NumBars;
+ $ValuesHash{$id.'.NumSets'} = $NumSets;
+ $ValuesHash{$id.'.PlotType'} = 'bar';
+ $ValuesHash{$id.'.Colors'} = join(',',@{$colors});
+ $ValuesHash{$id.'.height'} = $height;
+ $ValuesHash{$id.'.width'} = $width;
+ $ValuesHash{$id.'.xskip'} = $xskip;
+ $ValuesHash{$id.'.bar_width'} = $bar_width;
+ $ValuesHash{$id.'.labels'} = join(',',@Labels);
+ #
+ # Deal with other parameters
+ while (my ($key,$value) = each(%$extra_settings)) {
+ $ValuesHash{$id.'.'.$key} = $value;
+ }
+ #
+ &Apache::lonnet::appenv(\%ValuesHash);
+ return '';
+}
+
+############################################################
+############################################################
+
+=pod
+
+=item * &DrawXYGraph()
+
+Facilitates the plotting of data in an XY graph.
+Puts plot definition data into the users environment in order for
+graph.png to plot it. Returns an tag for the plot.
+
+Inputs:
+
+=over 4
+
+=item $Title: string, the title of the plot
+
+=item $xlabel: string, text describing the X-axis of the plot
+
+=item $ylabel: string, text describing the Y-axis of the plot
+
+=item $Max: scalar, the maximum Y value to use in the plot
+If $Max is < any data point, the graph will not be rendered.
+
+=item $colors: Array ref containing the hex color codes for the data to be
+plotted in. If undefined, default values will be used.
+
+=item $Xlabels: Array ref containing the labels to be used for the X-axis.
+
+=item $Ydata: Array ref containing Array refs.
+Each of the contained arrays will be plotted as a separate curve.
+
+=item %Values: hash indicating or overriding any default values which are
+passed to graph.png.
+Possible values are: width, xskip, x_ticks, x_tick_offset, among others.
+
+=back
+
+Returns:
+
+An tag which references graph.png and the appropriate identifying
+information for the plot.
+
+=cut
+
+############################################################
+############################################################
+sub DrawXYGraph {
+ my ($Title,$xlabel,$ylabel,$Max,$colors,$Xlabels,$Ydata,%Values)=@_;
+ #
+ # Create the identifier for the graph
+ my $identifier = &get_cgi_id();
+ my $id = 'cgi.'.$identifier;
+ #
+ $Title = '' if (! defined($Title));
+ $xlabel = '' if (! defined($xlabel));
+ $ylabel = '' if (! defined($ylabel));
+ my %ValuesHash =
+ (
+ $id.'.title' => &escape($Title),
+ $id.'.xlabel' => &escape($xlabel),
+ $id.'.ylabel' => &escape($ylabel),
+ $id.'.y_max_value'=> $Max,
+ $id.'.labels' => join(',',@$Xlabels),
+ $id.'.PlotType' => 'XY',
+ );
+ #
+ if (defined($colors) && ref($colors) eq 'ARRAY') {
+ $ValuesHash{$id.'.Colors'} = join(',',@{$colors});
+ }
+ #
+ if (! ref($Ydata) || ref($Ydata) ne 'ARRAY') {
+ return '';
+ }
+ my $NumSets=1;
+ foreach my $array (@{$Ydata}){
+ next if (! ref($array));
+ $ValuesHash{$id.'.data.'.$NumSets++} = join(',',@$array);
+ }
+ $ValuesHash{$id.'.NumSets'} = $NumSets-1;
+ #
+ # Deal with other parameters
+ while (my ($key,$value) = each(%Values)) {
+ $ValuesHash{$id.'.'.$key} = $value;
+ }
+ #
+ &Apache::lonnet::appenv(\%ValuesHash);
+ return '';
+}
+
+############################################################
+############################################################
+
+=pod
+
+=item * &DrawXYYGraph()
+
+Facilitates the plotting of data in an XY graph with two Y axes.
+Puts plot definition data into the users environment in order for
+graph.png to plot it. Returns an tag for the plot.
+
+Inputs:
+
+=over 4
+
+=item $Title: string, the title of the plot
+
+=item $xlabel: string, text describing the X-axis of the plot
+
+=item $ylabel: string, text describing the Y-axis of the plot
+
+=item $colors: Array ref containing the hex color codes for the data to be
+plotted in. If undefined, default values will be used.
+
+=item $Xlabels: Array ref containing the labels to be used for the X-axis.
+
+=item $Ydata1: The first data set
+
+=item $Min1: The minimum value of the left Y-axis
+
+=item $Max1: The maximum value of the left Y-axis
+
+=item $Ydata2: The second data set
+
+=item $Min2: The minimum value of the right Y-axis
+
+=item $Max2: The maximum value of the left Y-axis
+
+=item %Values: hash indicating or overriding any default values which are
+passed to graph.png.
+Possible values are: width, xskip, x_ticks, x_tick_offset, among others.
+
+=back
+
+Returns:
+
+An tag which references graph.png and the appropriate identifying
+information for the plot.
+
+=cut
+
+############################################################
+############################################################
+sub DrawXYYGraph {
+ my ($Title,$xlabel,$ylabel,$colors,$Xlabels,$Ydata1,$Min1,$Max1,
+ $Ydata2,$Min2,$Max2,%Values)=@_;
+ #
+ # Create the identifier for the graph
+ my $identifier = &get_cgi_id();
+ my $id = 'cgi.'.$identifier;
+ #
+ $Title = '' if (! defined($Title));
+ $xlabel = '' if (! defined($xlabel));
+ $ylabel = '' if (! defined($ylabel));
+ my %ValuesHash =
+ (
+ $id.'.title' => &escape($Title),
+ $id.'.xlabel' => &escape($xlabel),
+ $id.'.ylabel' => &escape($ylabel),
+ $id.'.labels' => join(',',@$Xlabels),
+ $id.'.PlotType' => 'XY',
+ $id.'.NumSets' => 2,
+ $id.'.two_axes' => 1,
+ $id.'.y1_max_value' => $Max1,
+ $id.'.y1_min_value' => $Min1,
+ $id.'.y2_max_value' => $Max2,
+ $id.'.y2_min_value' => $Min2,
+ );
+ #
+ if (defined($colors) && ref($colors) eq 'ARRAY') {
+ $ValuesHash{$id.'.Colors'} = join(',',@{$colors});
+ }
+ #
+ if (! ref($Ydata1) || ref($Ydata1) ne 'ARRAY' ||
+ ! ref($Ydata2) || ref($Ydata2) ne 'ARRAY'){
+ return '';
+ }
+ my $NumSets=1;
+ foreach my $array ($Ydata1,$Ydata2){
+ next if (! ref($array));
+ $ValuesHash{$id.'.data.'.$NumSets++} = join(',',@$array);
+ }
+ #
+ # Deal with other parameters
+ while (my ($key,$value) = each(%Values)) {
+ $ValuesHash{$id.'.'.$key} = $value;
+ }
+ #
+ &Apache::lonnet::appenv(\%ValuesHash);
+ return '';
+}
+
+############################################################
+############################################################
+
+=pod
+
+=back
+
+=head1 Statistics helper routines?
+
+Bad place for them but what the hell.
+
+=over 4
+
+=item * &chartlink()
+
+Returns a link to the chart for a specific student.
+
+Inputs:
+
+=over 4
+
+=item $linktext: The text of the link
+
+=item $sname: The students username
+
+=item $sdomain: The students domain
+
+=back
+
+=back
+
+=cut
+
+############################################################
+############################################################
+sub chartlink {
+ my ($linktext, $sname, $sdomain) = @_;
+ my $link = ''.$linktext.'';
+}
+
+#######################################################
+#######################################################
+
+=pod
+
+=head1 Course Environment Routines
+
+=over 4
+
+=item * &restore_course_settings()
+
+=item * &store_course_settings()
+
+Restores/Store indicated form parameters from the course environment.
+Will not overwrite existing values of the form parameters.
+
+Inputs:
+a scalar describing the data (e.g. 'chart', 'problem_analysis')
+
+a hash ref describing the data to be stored. For example:
+
+%Save_Parameters = ('Status' => 'scalar',
+ 'chartoutputmode' => 'scalar',
+ 'chartoutputdata' => 'scalar',
+ 'Section' => 'array',
+ 'Group' => 'array',
+ 'StudentData' => 'array',
+ 'Maps' => 'array');
+
+Returns: both routines return nothing
+
+=back
+
+=cut
+
+#######################################################
+#######################################################
+sub store_course_settings {
+ return &store_settings($env{'request.course.id'},@_);
+}
+
+sub store_settings {
+ # save to the environment
+ # appenv the same items, just to be safe
+ my $udom = $env{'user.domain'};
+ my $uname = $env{'user.name'};
+ my ($context,$prefix,$Settings) = @_;
+ my %SaveHash;
+ my %AppHash;
+ while (my ($setting,$type) = each(%$Settings)) {
+ my $basename = join('.','internal',$context,$prefix,$setting);
+ my $envname = 'environment.'.$basename;
+ if (exists($env{'form.'.$setting})) {
+ # Save this value away
+ if ($type eq 'scalar' &&
+ (! exists($env{$envname}) ||
+ $env{$envname} ne $env{'form.'.$setting})) {
+ $SaveHash{$basename} = $env{'form.'.$setting};
+ $AppHash{$envname} = $env{'form.'.$setting};
+ } elsif ($type eq 'array') {
+ my $stored_form;
+ if (ref($env{'form.'.$setting})) {
+ $stored_form = join(',',
+ map {
+ &escape($_);
+ } sort(@{$env{'form.'.$setting}}));
+ } else {
+ $stored_form =
+ &escape($env{'form.'.$setting});
+ }
+ # Determine if the array contents are the same.
+ if ($stored_form ne $env{$envname}) {
+ $SaveHash{$basename} = $stored_form;
+ $AppHash{$envname} = $stored_form;
+ }
+ }
+ }
+ }
+ my $put_result = &Apache::lonnet::put('environment',\%SaveHash,
+ $udom,$uname);
+ if ($put_result !~ /^(ok|delayed)/) {
+ &Apache::lonnet::logthis('unable to save form parameters, '.
+ 'got error:'.$put_result);
+ }
+ # Make sure these settings stick around in this session, too
+ &Apache::lonnet::appenv(\%AppHash);
+ return;
+}
+
+sub restore_course_settings {
+ return &restore_settings($env{'request.course.id'},@_);
+}
+
+sub restore_settings {
+ my ($context,$prefix,$Settings) = @_;
+ while (my ($setting,$type) = each(%$Settings)) {
+ next if (exists($env{'form.'.$setting}));
+ my $envname = 'environment.internal.'.$context.'.'.$prefix.
+ '.'.$setting;
+ if (exists($env{$envname})) {
+ if ($type eq 'scalar') {
+ $env{'form.'.$setting} = $env{$envname};
+ } elsif ($type eq 'array') {
+ $env{'form.'.$setting} = [
+ map {
+ &unescape($_);
+ } split(',',$env{$envname})
+ ];
+ }
+ }
+ }
+}
+
+#######################################################
+#######################################################
+
+=pod
+
+=head1 Domain E-mail Routines
+
+=over 4
+
+=item * &build_recipient_list()
+
+Build recipient lists for five types of e-mail:
+(a) Error Reports, (b) Package Updates, (c) lonstatus warnings/errors
+(d) Help requests, (e) Course requests needing approval, generated by
+lonerrorhandler.pm, CHECKRPMS, loncron, lonsupportreq.pm and
+loncoursequeueadmin.pm respectively.
+
+Inputs:
+defmail (scalar - email address of default recipient),
+mailing type (scalar - errormail, packagesmail, or helpdeskmail),
+defdom (domain for which to retrieve configuration settings),
+origmail (scalar - email address of recipient from loncapa.conf,
+i.e., predates configuration by DC via domainprefs.pm
+
+Returns: comma separated list of addresses to which to send e-mail.
+
+=back
+
+=cut
+
+############################################################
+############################################################
+sub build_recipient_list {
+ my ($defmail,$mailing,$defdom,$origmail) = @_;
+ my @recipients;
+ my $otheremails;
+ my %domconfig =
+ &Apache::lonnet::get_dom('configuration',['contacts'],$defdom);
+ if (ref($domconfig{'contacts'}) eq 'HASH') {
+ if (exists($domconfig{'contacts'}{$mailing})) {
+ if (ref($domconfig{'contacts'}{$mailing}) eq 'HASH') {
+ my @contacts = ('adminemail','supportemail');
+ foreach my $item (@contacts) {
+ if ($domconfig{'contacts'}{$mailing}{$item}) {
+ my $addr = $domconfig{'contacts'}{$item};
+ if (!grep(/^\Q$addr\E$/,@recipients)) {
+ push(@recipients,$addr);
+ }
+ }
+ $otheremails = $domconfig{'contacts'}{$mailing}{'others'};
+ }
+ }
+ } elsif ($origmail ne '') {
+ push(@recipients,$origmail);
+ }
+ } elsif ($origmail ne '') {
+ push(@recipients,$origmail);
+ }
+ if (defined($defmail)) {
+ if ($defmail ne '') {
+ push(@recipients,$defmail);
+ }
+ }
+ if ($otheremails) {
+ my @others;
+ if ($otheremails =~ /,/) {
+ @others = split(/,/,$otheremails);
+ } else {
+ push(@others,$otheremails);
+ }
+ foreach my $addr (@others) {
+ if (!grep(/^\Q$addr\E$/,@recipients)) {
+ push(@recipients,$addr);
+ }
+ }
+ }
+ my $recipientlist = join(',',@recipients);
+ return $recipientlist;
+}
+
+############################################################
+############################################################
+
+=pod
+
+=head1 Course Catalog Routines
+
+=over 4
+
+=item * &gather_categories()
+
+Converts category definitions - keys of categories hash stored in
+coursecategories in configuration.db on the primary library server in a
+domain - to an array. Also generates javascript and idx hash used to
+generate Domain Coordinator interface for editing Course Categories.
+
+Inputs:
+
+categories (reference to hash of category definitions).
+
+cats (reference to array of arrays/hashes which encapsulates hierarchy of
+ categories and subcategories).
+
+idx (reference to hash of counters used in Domain Coordinator interface for
+ editing Course Categories).
+
+jsarray (reference to array of categories used to create Javascript arrays for
+ Domain Coordinator interface for editing Course Categories).
+
+Returns: nothing
+
+Side effects: populates cats, idx and jsarray.
+
+=cut
+
+sub gather_categories {
+ my ($categories,$cats,$idx,$jsarray) = @_;
+ my %counters;
+ my $num = 0;
+ foreach my $item (keys(%{$categories})) {
+ my ($cat,$container,$depth) = map { &unescape($_); } split(/:/,$item);
+ if ($container eq '' && $depth == 0) {
+ $cats->[$depth][$categories->{$item}] = $cat;
+ } else {
+ $cats->[$depth]{$container}[$categories->{$item}] = $cat;
+ }
+ my ($escitem,$tail) = split(/:/,$item,2);
+ if ($counters{$tail} eq '') {
+ $counters{$tail} = $num;
+ $num ++;
+ }
+ if (ref($idx) eq 'HASH') {
+ $idx->{$item} = $counters{$tail};
+ }
+ if (ref($jsarray) eq 'ARRAY') {
+ push(@{$jsarray->[$counters{$tail}]},$item);
+ }
+ }
+ return;
+}
+
+=pod
+
+=item * &extract_categories()
+
+Used to generate breadcrumb trails for course categories.
+
+Inputs:
+
+categories (reference to hash of category definitions).
+
+cats (reference to array of arrays/hashes which encapsulates hierarchy of
+ categories and subcategories).
+
+trails (reference to array of breacrumb trails for each category).
+
+allitems (reference to hash - key is category key
+ (format: escaped(name):escaped(parent category):depth in hierarchy).
+
+idx (reference to hash of counters used in Domain Coordinator interface for
+ editing Course Categories).
+
+jsarray (reference to array of categories used to create Javascript arrays for
+ Domain Coordinator interface for editing Course Categories).
+
+subcats (reference to hash of arrays containing all subcategories within each
+ category, -recursive)
+
+Returns: nothing
+
+Side effects: populates trails and allitems hash references.
+
+=cut
+
+sub extract_categories {
+ my ($categories,$cats,$trails,$allitems,$idx,$jsarray,$subcats) = @_;
+ if (ref($categories) eq 'HASH') {
+ &gather_categories($categories,$cats,$idx,$jsarray);
+ if (ref($cats->[0]) eq 'ARRAY') {
+ for (my $i=0; $i<@{$cats->[0]}; $i++) {
+ my $name = $cats->[0][$i];
+ my $item = &escape($name).'::0';
+ my $trailstr;
+ if ($name eq 'instcode') {
+ $trailstr = &mt('Official courses (with institutional codes)');
+ } elsif ($name eq 'communities') {
+ $trailstr = &mt('Communities');
+ } else {
+ $trailstr = $name;
+ }
+ if ($allitems->{$item} eq '') {
+ push(@{$trails},$trailstr);
+ $allitems->{$item} = scalar(@{$trails})-1;
+ }
+ my @parents = ($name);
+ if (ref($cats->[1]{$name}) eq 'ARRAY') {
+ for (my $j=0; $j<@{$cats->[1]{$name}}; $j++) {
+ my $category = $cats->[1]{$name}[$j];
+ if (ref($subcats) eq 'HASH') {
+ push(@{$subcats->{$item}},&escape($category).':'.&escape($name).':1');
+ }
+ &recurse_categories($cats,2,$category,$trails,$allitems,\@parents,$subcats);
+ }
+ } else {
+ if (ref($subcats) eq 'HASH') {
+ $subcats->{$item} = [];
+ }
+ }
+ }
+ }
+ }
+ return;
+}
+
+=pod
+
+=item *&recurse_categories()
+
+Recursively used to generate breadcrumb trails for course categories.
+
+Inputs:
+
+cats (reference to array of arrays/hashes which encapsulates hierarchy of
+ categories and subcategories).
+
+depth (current depth in hierarchy of categories and sub-categories - 0 indexed).
+
+category (current course category, for which breadcrumb trail is being generated).
+
+trails (reference to array of breadcrumb trails for each category).
+
+allitems (reference to hash - key is category key
+ (format: escaped(name):escaped(parent category):depth in hierarchy).
+
+parents (array containing containers directories for current category,
+ back to top level).
+
+Returns: nothing
+
+Side effects: populates trails and allitems hash references
+
+=cut
+
+sub recurse_categories {
+ my ($cats,$depth,$category,$trails,$allitems,$parents,$subcats) = @_;
+ my $shallower = $depth - 1;
+ if (ref($cats->[$depth]{$category}) eq 'ARRAY') {
+ for (my $k=0; $k<@{$cats->[$depth]{$category}}; $k++) {
+ my $name = $cats->[$depth]{$category}[$k];
+ my $item = &escape($category).':'.&escape($parents->[-1]).':'.$shallower;
+ my $trailstr = join(' -> ',(@{$parents},$category));
+ if ($allitems->{$item} eq '') {
+ push(@{$trails},$trailstr);
+ $allitems->{$item} = scalar(@{$trails})-1;
+ }
+ my $deeper = $depth+1;
+ push(@{$parents},$category);
+ if (ref($subcats) eq 'HASH') {
+ my $subcat = &escape($name).':'.$category.':'.$depth;
+ for (my $j=@{$parents}; $j>=0; $j--) {
+ my $higher;
+ if ($j > 0) {
+ $higher = &escape($parents->[$j]).':'.
+ &escape($parents->[$j-1]).':'.$j;
+ } else {
+ $higher = &escape($parents->[$j]).'::'.$j;
+ }
+ push(@{$subcats->{$higher}},$subcat);
+ }
+ }
+ &recurse_categories($cats,$deeper,$name,$trails,$allitems,$parents,
+ $subcats);
+ pop(@{$parents});
+ }
+ } else {
+ my $item = &escape($category).':'.&escape($parents->[-1]).':'.$shallower;
+ my $trailstr = join(' -> ',(@{$parents},$category));
+ if ($allitems->{$item} eq '') {
+ push(@{$trails},$trailstr);
+ $allitems->{$item} = scalar(@{$trails})-1;
+ }
+ }
+ return;
+}
+
+=pod
+
+=item *&assign_categories_table()
+
+Create a datatable for display of hierarchical categories in a domain,
+with checkboxes to allow a course to be categorized.
+
+Inputs:
+
+cathash - reference to hash of categories defined for the domain (from
+ configuration.db)
+
+currcat - scalar with an & separated list of categories assigned to a course.
+
+type - scalar contains course type (Course or Community).
+
+Returns: $output (markup to be displayed)
+
+=cut
+
+sub assign_categories_table {
+ my ($cathash,$currcat,$type) = @_;
+ my $output;
+ if (ref($cathash) eq 'HASH') {
+ my (@cats,@trails,%allitems,%idx,@jsarray,@path,$maxdepth);
+ &extract_categories($cathash,\@cats,\@trails,\%allitems,\%idx,\@jsarray);
+ $maxdepth = scalar(@cats);
+ if (@cats > 0) {
+ my $itemcount = 0;
+ if (ref($cats[0]) eq 'ARRAY') {
+ my @currcategories;
+ if ($currcat ne '') {
+ @currcategories = split('&',$currcat);
+ }
+ my $table;
+ for (my $i=0; $i<@{$cats[0]}; $i++) {
+ my $parent = $cats[0][$i];
+ next if ($parent eq 'instcode');
+ if ($type eq 'Community') {
+ next unless ($parent eq 'communities');
+ } else {
+ next if ($parent eq 'communities');
+ }
+ my $css_class = $itemcount%2?' class="LC_odd_row"':'';
+ my $item = &escape($parent).'::0';
+ my $checked = '';
+ if (@currcategories > 0) {
+ if (grep(/^\Q$item\E$/,@currcategories)) {
+ $checked = ' checked="checked"';
+ }
+ }
+ my $parent_title = $parent;
+ if ($parent eq 'communities') {
+ $parent_title = &mt('Communities');
+ }
+ $table .= '
';
+ $itemcount ++;
+ }
+ if ($itemcount) {
+ $output = &Apache::loncommon::start_data_table().
+ $table.
+ &Apache::loncommon::end_data_table();
+ }
+ }
+ }
+ }
+ return $output;
+}
+
+=pod
+
+=item *&assign_category_rows()
+
+Create a datatable row for display of nested categories in a domain,
+with checkboxes to allow a course to be categorized,called recursively.
+
+Inputs:
+
+itemcount - track row number for alternating colors
+
+cats - reference to array of arrays/hashes which encapsulates hierarchy of
+ categories and subcategories.
+
+depth - current depth in hierarchy of categories and sub-categories - 0 indexed.
+
+parent - parent of current category item
+
+path - Array containing all categories back up through the hierarchy from the
+ current category to the top level.
+
+currcategories - reference to array of current categories assigned to the course
+
+Returns: $output (markup to be displayed).
+
+=cut
+
+sub assign_category_rows {
+ my ($itemcount,$cats,$depth,$parent,$path,$currcategories) = @_;
+ my ($text,$name,$item,$chgstr);
+ if (ref($cats) eq 'ARRAY') {
+ my $maxdepth = scalar(@{$cats});
+ if (ref($cats->[$depth]) eq 'HASH') {
+ if (ref($cats->[$depth]{$parent}) eq 'ARRAY') {
+ my $numchildren = @{$cats->[$depth]{$parent}};
+ my $css_class = $itemcount%2?' class="LC_odd_row"':'';
+ $text .= '
';
+ for (my $j=0; $j<$numchildren; $j++) {
+ $name = $cats->[$depth]{$parent}[$j];
+ $item = &escape($name).':'.&escape($parent).':'.$depth;
+ my $deeper = $depth+1;
+ my $checked = '';
+ if (ref($currcategories) eq 'ARRAY') {
+ if (@{$currcategories} > 0) {
+ if (grep(/^\Q$item\E$/,@{$currcategories})) {
+ $checked = ' checked="checked"';
+ }
+ }
+ }
+ $text .= '