commit b18c3700b0d34ee00c078028e1382c71e266bacf
Author: Francois Marier <francois@catalyst.net.nz>
Date:   Thu Nov 27 17:28:33 2008 +1300

    Grade export: refactoring the export plugins to allow exporting data from more than one course at a time

diff --git a/grade/export/lib.php b/grade/export/lib.php
index 3a5a7e6..79319b5 100755
--- a/grade/export/lib.php
+++ b/grade/export/lib.php
@@ -36,7 +36,7 @@ class grade_export {
 
     var $grade_items; // list of all course grade items
     var $groupid;     // groupid, 0 means all groups
-    var $course;      // course object
+    var $courses;     // array of course objects
     var $columns;     // array of grade_items selected for export
 
     var $previewrows;     // number of rows in preview
@@ -49,17 +49,26 @@ class grade_export {
     var $decimalpoints; // number of decimal points for exports
     /**
      * Constructor should set up all the private variables ready to be pulled
-     * @param object $course
+     * @param mixed either a course object or an array of course objects
      * @param int $groupid id of selected group, 0 means all
      * @param string $itemlist comma separated list of item ids, empty means all
      * @param boolean $export_feedback
      * @param boolean $export_letters
      * @note Exporting as letters will lead to data loss if that exported set it re-imported.
      */
-    function grade_export($course, $groupid=0, $itemlist='', $export_feedback=false, $updatedgradesonly = false, $displaytype = GRADE_DISPLAY_TYPE_REAL, $decimalpoints = 2) {
-        $this->course = $course;
+    function grade_export($courses, $groupid=0, $itemlist='', $export_feedback=false, $updatedgradesonly = false, $displaytype = GRADE_DISPLAY_TYPE_REAL, $decimalpoints = 2) {
+        if (is_array($courses)) {
+            $this->courses = $courses;
+        } else {
+            $this->courses = array($courses);
+        }
         $this->groupid = $groupid;
-        $this->grade_items = grade_item::fetch_all(array('courseid'=>$this->course->id));
+
+        $this->grade_items = array();
+        foreach ($this->courses as $course) {
+            $gradeitems = grade_item::fetch_all(array('courseid'=>$course->id));
+            $this->grade_items += $gradeitems;
+        }
 
         $this->columns = array();
         if (!empty($itemlist)) {
@@ -108,7 +117,11 @@ class grade_export {
         if (isset($formdata->key)) {
             if ($formdata->key == 1 && isset($formdata->iprestriction) && isset($formdata->validuntil)) {
                 // Create a new key
-                $formdata->key = create_user_key('grade/export', $USER->id, $this->course->id, $formdata->iprestriction, $formdata->validuntil);
+                if (1 == count($this->courses)) {
+                    $formdata->key = create_user_key('grade/export', $USER->id, $this->courses[0]->id, $formdata->iprestriction, $formdata->validuntil);
+                } else {
+                    $formdata->key = null;
+                }
             }
             $this->userkey = $formdata->key;
         }
@@ -216,7 +229,7 @@ class grade_export {
         /// Print all the lines of data.
 
         $i = 0;
-        $gui = new graded_users_iterator($this->course, $this->columns, $this->groupid);
+        $gui = new graded_users_iterator($this->courses, $this->columns, $this->groupid);
         $gui->init();
         while ($userdata = $gui->next_user()) {
             // number of preview rows
@@ -273,7 +286,18 @@ class grade_export {
     function get_export_params() {
         $itemids = array_keys($this->columns);
 
-        $params = array('id'                =>$this->course->id,
+        $courseid = 0;
+        if (1 == count($this->courses)) {
+            $courseid = $this->courses[0]->id;
+        } else {
+            $courseids = array();
+            foreach ($this->courses as $course) {
+                $courseids[] = $course->id;
+            }
+            $courseid = implode(',', $courseids);
+        }
+
+        $params = array('id'                =>$courseid,
                         'groupid'           =>$this->groupid,
                         'itemids'           =>implode(',', $itemids),
                         'export_letters'    =>$this->export_letters,
diff --git a/grade/export/ods/export.php b/grade/export/ods/export.php
index 3b163b7..493acf1 100755
--- a/grade/export/ods/export.php
+++ b/grade/export/ods/export.php
@@ -27,7 +27,7 @@ require_once '../../../config.php';
 require_once $CFG->dirroot.'/grade/export/lib.php';
 require_once 'grade_export_ods.php';
 
-$id                = required_param('id', PARAM_INT); // course id
+$id                = required_param('id', PARAM_RAW); // course IDs
 $groupid           = optional_param('groupid', 0, PARAM_INT);
 $itemids           = required_param('itemids', PARAM_RAW);
 $export_feedback   = optional_param('export_feedback', 0, PARAM_BOOL);
@@ -35,19 +35,13 @@ $updatedgradesonly = optional_param('updatedgradesonly', false, PARAM_BOOL);
 $displaytype       = optional_param('displaytype', $CFG->grade_export_displaytype, PARAM_INT);
 $decimalpoints     = optional_param('decimalpoints', $CFG->grade_export_decimalpoints, PARAM_INT);
 
-if (!$course = get_record('course', 'id', $id)) {
+$whereclause = get_course_ids_string($id, array('moodle/grade:export', 'gradeexport/ods:view'));
+if (!$courses = get_records_select('course', $whereclause)) {
     print_error('nocourseid');
 }
 
-require_login($course);
-$context = get_context_instance(CONTEXT_COURSE, $id);
-
-require_capability('moodle/grade:export', $context);
-require_capability('gradeexport/ods:view', $context);
-
-
 // print all the exported data here
-$export = new grade_export_ods($course, $groupid, $itemids, $export_feedback, $updatedgradesonly, $displaytype, $decimalpoints);
+$export = new grade_export_ods($courses, $groupid, $itemids, $export_feedback, $updatedgradesonly, $displaytype, $decimalpoints);
 $export->print_grades();
 
 ?>
diff --git a/grade/export/ods/grade_export_ods.php b/grade/export/ods/grade_export_ods.php
index 2fd11f7..d42a0f4 100755
--- a/grade/export/ods/grade_export_ods.php
+++ b/grade/export/ods/grade_export_ods.php
@@ -39,8 +39,13 @@ class grade_export_ods extends grade_export {
 
         $strgrades = get_string('grades');
 
+        $filename = $strgrades;
+        if (count($this->courses) == 1) {
+            $filename = reset($this->courses)->shortname . " $strgrades";
+        }
+
     /// Calculate file name
-        $downloadfilename = clean_filename("{$this->course->shortname} $strgrades.ods");
+        $downloadfilename = clean_filename("$filename.ods");
     /// Creating a workbook
         $workbook = new MoodleODSWorkbook("-");
     /// Sending HTTP headers
@@ -68,7 +73,7 @@ class grade_export_ods extends grade_export {
     /// Print all the lines of data.
         $i = 0;
         $geub = new grade_export_update_buffer();
-        $gui = new graded_users_iterator($this->course, $this->columns, $this->groupid);
+        $gui = new graded_users_iterator($this->courses, $this->columns, $this->groupid);
         $gui->init();
         while ($userdata = $gui->next_user()) {
             $i++;
diff --git a/grade/export/txt/export.php b/grade/export/txt/export.php
index 37b0beb..401ddfd 100755
--- a/grade/export/txt/export.php
+++ b/grade/export/txt/export.php
@@ -27,7 +27,7 @@ require_once '../../../config.php';
 require_once $CFG->dirroot.'/grade/export/lib.php';
 require_once 'grade_export_txt.php';
 
-$id                = required_param('id', PARAM_INT); // course id
+$id                = required_param('id', PARAM_RAW); // course IDs
 $groupid           = optional_param('groupid', 0, PARAM_INT);
 $itemids           = required_param('itemids', PARAM_RAW);
 $export_feedback   = optional_param('export_feedback', 0, PARAM_BOOL);
@@ -36,19 +36,13 @@ $updatedgradesonly = optional_param('updatedgradesonly', false, PARAM_BOOL);
 $displaytype       = optional_param('displaytype', $CFG->grade_export_displaytype, PARAM_INT);
 $decimalpoints     = optional_param('decimalpoints', $CFG->grade_export_decimalpoints, PARAM_INT);
 
-if (!$course = get_record('course', 'id', $id)) {
+$whereclause = get_course_ids_string($id, array('moodle/grade:export', 'gradeexport/txt:view'));
+if (!$courses = get_records_select('course', $whereclause)) {
     print_error('nocourseid');
 }
 
-require_login($course);
-$context = get_context_instance(CONTEXT_COURSE, $id);
-
-require_capability('moodle/grade:export', $context);
-require_capability('gradeexport/txt:view', $context);
-
-
 // print all the exported data here
-$export = new grade_export_txt($course, $groupid, $itemids, $export_feedback, $updatedgradesonly, $displaytype, $decimalpoints, $separator);
+$export = new grade_export_txt($courses, $groupid, $itemids, $export_feedback, $updatedgradesonly, $displaytype, $decimalpoints, $separator);
 $export->print_grades();
 
 ?>
diff --git a/grade/export/txt/grade_export_txt.php b/grade/export/txt/grade_export_txt.php
index 29dcdcc..76da6b7 100755
--- a/grade/export/txt/grade_export_txt.php
+++ b/grade/export/txt/grade_export_txt.php
@@ -30,8 +30,8 @@ class grade_export_txt extends grade_export {
 
     var $separator; // default separator
 
-    function grade_export_txt($course, $groupid=0, $itemlist='', $export_feedback=false, $updatedgradesonly = false, $displaytype = GRADE_DISPLAY_TYPE_REAL, $decimalpoints = 2, $separator='comma') {
-        $this->grade_export($course, $groupid, $itemlist, $export_feedback, $updatedgradesonly, $displaytype, $decimalpoints);
+    function grade_export_txt($courses, $groupid=0, $itemlist='', $export_feedback=false, $updatedgradesonly = false, $displaytype = GRADE_DISPLAY_TYPE_REAL, $decimalpoints = 2, $separator='comma') {
+        $this->grade_export($courses, $groupid, $itemlist, $export_feedback, $updatedgradesonly, $displaytype, $decimalpoints);
         $this->separator = $separator;
     }
 
@@ -55,6 +55,11 @@ class grade_export_txt extends grade_export {
 
         $strgrades = get_string('grades');
 
+        $filename = $strgrades;
+        if (count($this->courses) == 1) {
+            $filename = reset($this->courses)->shortname . " $strgrades";
+        }
+
         switch ($this->separator) {
             case 'comma':
                 $separator = ",";
@@ -69,7 +74,7 @@ class grade_export_txt extends grade_export {
         @header('Expires: '. gmdate('D, d M Y H:i:s', 0) .' GMT');
         @header('Pragma: no-cache');
         header("Content-Type: application/download\n");
-        $downloadfilename = clean_filename("{$this->course->shortname} $strgrades");
+        $downloadfilename = clean_filename($filename);
         header("Content-Disposition: attachment; filename=\"$downloadfilename.txt\"");
 
 /// Print names of all the fields
@@ -92,7 +97,7 @@ class grade_export_txt extends grade_export {
 
 /// Print all the lines of data.
         $geub = new grade_export_update_buffer();
-        $gui = new graded_users_iterator($this->course, $this->columns, $this->groupid);
+        $gui = new graded_users_iterator($this->courses, $this->columns, $this->groupid);
         $gui->init();
         while ($userdata = $gui->next_user()) {
 
diff --git a/grade/export/xls/export.php b/grade/export/xls/export.php
index 3774fde..3cba33e 100755
--- a/grade/export/xls/export.php
+++ b/grade/export/xls/export.php
@@ -27,7 +27,7 @@ require_once '../../../config.php';
 require_once $CFG->dirroot.'/grade/export/lib.php';
 require_once 'grade_export_xls.php';
 
-$id                = required_param('id', PARAM_INT); // course id
+$id                = required_param('id', PARAM_RAW); // course IDs
 $groupid           = optional_param('groupid', 0, PARAM_INT);
 $itemids           = required_param('itemids', PARAM_RAW);
 $export_feedback   = optional_param('export_feedback', 0, PARAM_BOOL);
@@ -35,19 +35,13 @@ $updatedgradesonly = optional_param('updatedgradesonly', false, PARAM_BOOL);
 $displaytype       = optional_param('displaytype', $CFG->grade_export_displaytype, PARAM_INT);
 $decimalpoints     = optional_param('decimalpoints', $CFG->grade_export_decimalpoints, PARAM_INT);
 
-if (!$course = get_record('course', 'id', $id)) {
+$whereclause = get_course_ids_string($id, array('moodle/grade:export', 'gradeexport/xls:view'));
+if (!$courses = get_records_select('course', $whereclause)) {
     print_error('nocourseid');
 }
 
-require_login($course);
-$context = get_context_instance(CONTEXT_COURSE, $id);
-
-require_capability('moodle/grade:export', $context);
-require_capability('gradeexport/xls:view', $context);
-
-
 // print all the exported data here
-$export = new grade_export_xls($course, $groupid, $itemids, $export_feedback, $updatedgradesonly, $displaytype, $decimalpoints);
+$export = new grade_export_xls($courses, $groupid, $itemids, $export_feedback, $updatedgradesonly, $displaytype, $decimalpoints);
 $export->print_grades();
 
 ?>
diff --git a/grade/export/xls/grade_export_xls.php b/grade/export/xls/grade_export_xls.php
index 2f473d3..268c03b 100755
--- a/grade/export/xls/grade_export_xls.php
+++ b/grade/export/xls/grade_export_xls.php
@@ -40,8 +40,13 @@ class grade_export_xls extends grade_export {
 
         $strgrades = get_string('grades');
 
+        $filename = $strgrades;
+        if (count($this->courses) == 1) {
+            $filename = reset($this->courses)->shortname . " $strgrades";
+        }
+
     /// Calculate file name
-        $downloadfilename = clean_filename("{$this->course->shortname} $strgrades.xls");
+        $downloadfilename = clean_filename("$filename.xls");
     /// Creating a workbook
         $workbook = new MoodleExcelWorkbook("-");
     /// Sending HTTP headers
@@ -69,7 +74,7 @@ class grade_export_xls extends grade_export {
     /// Print all the lines of data.
         $i = 0;
         $geub = new grade_export_update_buffer();
-        $gui = new graded_users_iterator($this->course, $this->columns, $this->groupid);
+        $gui = new graded_users_iterator($this->courses, $this->columns, $this->groupid);
         $gui->init();
         while ($userdata = $gui->next_user()) {
             $i++;
diff --git a/grade/export/xml/export.php b/grade/export/xml/export.php
index 42a7419..aadd160 100755
--- a/grade/export/xml/export.php
+++ b/grade/export/xml/export.php
@@ -27,7 +27,7 @@ require_once '../../../config.php';
 require_once $CFG->dirroot.'/grade/export/lib.php';
 require_once 'grade_export_xml.php';
 
-$id                = required_param('id', PARAM_INT); // course id
+$id                = required_param('id', PARAM_RAW); // course IDs
 $groupid           = optional_param('groupid', 0, PARAM_INT);
 $itemids           = required_param('itemids', PARAM_RAW);
 $export_feedback   = optional_param('export_feedback', 0, PARAM_BOOL);
@@ -35,19 +35,13 @@ $updatedgradesonly = optional_param('updatedgradesonly', false, PARAM_BOOL);
 $displaytype       = optional_param('displaytype', $CFG->grade_export_displaytype, PARAM_INT);
 $decimalpoints     = optional_param('decimalpoints', $CFG->grade_export_decimalpoints, PARAM_INT);
 
-if (!$course = get_record('course', 'id', $id)) {
+$whereclause = get_course_ids_string($id, array('moodle/grade:export', 'gradeexport/xml:view'));
+if (!$courses = get_records_select('course', $whereclause)) {
     print_error('nocourseid');
 }
 
-require_login($course);
-$context = get_context_instance(CONTEXT_COURSE, $id);
-
-require_capability('moodle/grade:export', $context);
-require_capability('gradeexport/xml:view', $context);
-
-
 // print all the exported data here
-$export = new grade_export_xml($course, $groupid, $itemids, $export_feedback, $updatedgradesonly, $displaytype, $decimalpoints);
+$export = new grade_export_xml($courses, $groupid, $itemids, $export_feedback, $updatedgradesonly, $displaytype, $decimalpoints);
 $export->print_grades();
 
 ?>
diff --git a/grade/export/xml/grade_export_xml.php b/grade/export/xml/grade_export_xml.php
index 0f50578..0fb620d 100755
--- a/grade/export/xml/grade_export_xml.php
+++ b/grade/export/xml/grade_export_xml.php
@@ -44,8 +44,13 @@ class grade_export_xml extends grade_export {
 
         $strgrades = get_string('grades');
 
+        $filename = $strgrades;
+        if (count($this->courses) == 1) {
+            $filename = reset($this->courses)->shortname . " $strgrades";
+        }
+
         /// Calculate file name
-        $downloadfilename = clean_filename("{$this->course->shortname} $strgrades.xml");
+        $downloadfilename = clean_filename("$filename.xml");
 
         make_upload_directory('temp/gradeexport', false);
         $tempfilename = $CFG->dataroot .'/temp/gradeexport/'. md5(sesskey().microtime().$downloadfilename);
@@ -60,7 +65,7 @@ class grade_export_xml extends grade_export {
         $export_buffer = array();
 
         $geub = new grade_export_update_buffer();
-        $gui = new graded_users_iterator($this->course, $this->columns, $this->groupid);
+        $gui = new graded_users_iterator($this->courses, $this->columns, $this->groupid);
         $gui->init();
         while ($userdata = $gui->next_user()) {
             $user = $userdata->user;
diff --git a/grade/lib.php b/grade/lib.php
index 79228c7..0c8ffe6 100644
--- a/grade/lib.php
+++ b/grade/lib.php
@@ -30,7 +30,7 @@ require_once $CFG->libdir.'/gradelib.php';
  * Returns detailed info about users and their grades.
  */
 class graded_users_iterator {
-    var $course;
+    var $courses;
     var $grade_items;
     var $groupid;
     var $users_rs;
@@ -43,7 +43,7 @@ class graded_users_iterator {
 
     /**
      * Constructor
-     * @param $course object
+     * @param mixed $courses single course object or array of course objects
      * @param array grade_items array of grade items, if not specified only user info returned
      * @param int $groupid iterate only group users if present
      * @param string $sortfield1 The first field of the users table by which the array of users will be sorted
@@ -51,8 +51,12 @@ class graded_users_iterator {
      * @param string $sortfield2 The second field of the users table by which the array of users will be sorted
      * @param string $sortorder2 The order in which the second sorting field will be sorted (ASC or DESC)
      */
-    function graded_users_iterator($course, $grade_items=null, $groupid=0, $sortfield1='lastname', $sortorder1='ASC', $sortfield2='firstname', $sortorder2='ASC') {
-        $this->course      = $course;
+    function graded_users_iterator($courses, $grade_items=null, $groupid=0, $sortfield1='lastname', $sortorder1='ASC', $sortfield2='firstname', $sortorder2='ASC') {
+        if (is_array($courses)) {
+            $this->courses = $courses;
+        } else {
+            $this->courses = array($courses);
+        }
         $this->grade_items = $grade_items;
         $this->groupid     = $groupid;
         $this->sortfield1  = $sortfield1;
@@ -72,11 +76,15 @@ class graded_users_iterator {
 
         $this->close();
 
-        grade_regrade_final_grades($this->course->id);
-        $course_item = grade_item::fetch_course_item($this->course->id);
-        if ($course_item->needsupdate) {
-            // can not calculate all final grades - sorry
-            return false;
+        // Make sure all courses are fully graded
+        foreach ($this->courses as $course) {
+            grade_regrade_final_grades($course->id);
+
+            $course_item = grade_item::fetch_course_item($course->id);
+            if ($course_item->needsupdate) {
+                // can not calculate all final grades - sorry
+                return false;
+            }
         }
 
         if (strpos($CFG->gradebookroles, ',') === false) {
@@ -85,7 +93,7 @@ class graded_users_iterator {
             $gradebookroles = " IN ({$CFG->gradebookroles})";
         }
 
-        $relatedcontexts = get_related_contexts_string(get_context_instance(CONTEXT_COURSE, $this->course->id));
+        $relatedcontexts = get_related_course_contexts_string($this->courses);
 
         if ($this->groupid) {
             $groupsql = "INNER JOIN {$CFG->prefix}groups_members gm ON gm.userid = u.id";
@@ -114,7 +122,11 @@ class graded_users_iterator {
             }
         }
 
-        $users_sql = "SELECT u.* $ofields
+        // This list cannot include any TEXT fields since they don't work with DISTINCT on Oracle
+        $userfields = 'u.id, u.lastname, u.firstname, u.username, u.email, u.address,
+                       u.institution, u.department, u.city, u.country, u.idnumber';
+
+        $users_sql = "SELECT DISTINCT $userfields $ofields
                         FROM {$CFG->prefix}user u
                              INNER JOIN {$CFG->prefix}role_assignments ra ON u.id = ra.userid
                              $groupsql
diff --git a/lib/gradelib.php b/lib/gradelib.php
index 49dc217..62e0c11 100644
--- a/lib/gradelib.php
+++ b/lib/gradelib.php
@@ -1357,4 +1357,70 @@ function grade_floats_different($f1, $f2) {
     return (grade_floatval($f1) !== grade_floatval($f2));
 }
 
+/**
+ * Gets a string for sql calls, searching for stuff in these courses or above
+ * @param array $courses array of course IDs
+ * @return string
+ */
+function get_related_course_contexts_string($courses) {
+
+    $allcontexts = array();
+    foreach ($courses as $course) {
+        $context = get_context_instance(CONTEXT_COURSE, $course->id);
+
+        $parentcontexts = substr($context->path, 1); // kill leading slash
+        $parentcontexts = explode('/', $parentcontexts);
+
+        $allcontexts = array_merge($allcontexts, $parentcontexts);
+    }
+    $allcontexts = array_unique($allcontexts);
+
+    if (count($allcontexts) > 1) {
+        return (' IN ('.implode(',', $allcontexts).')');
+    } else {
+        return (' = '.$allcontexts[0]);
+    }
+}
+
+/**
+ * Go through the course ids passed as a parameter and check each of
+ * them to make sure that we can view and export the grades for each
+ * then return the SQL code fragment which should be used in the WHERE
+ * clause.
+ *
+ * @param string $rawids       list of course IDs separated by commas
+ * @param array  $capabilities list of capabilities to check for each
+ *                             course (or false to disable these checks)
+ * @return string SQL code for the WHERE clause
+ */
+function get_course_ids_string($rawids, $capabilities=false) {
+    $selectedcourseids = array();
+    foreach (explode(',', $rawids) as $rawcourseid) {
+        $courseid = clean_param($rawcourseid, PARAM_INT);
+        if (empty($courseid) || ($courseid < 1)) {
+            continue; // bogus ID
+        }
+
+        // Make sure we have the necessary permissions on these courses
+        if ($capabilities) {
+            $coursecontext = get_context_instance(CONTEXT_COURSE, $courseid);
+            foreach ($capabilities as $cap) {
+                require_capability($cap, $coursecontext);
+            }
+        }
+
+        $selectedcourseids[] = $courseid;
+    }
+    if (count($selectedcourseids) > 1) {
+        return 'id IN ('.implode(',', $selectedcourseids).')';
+    }
+    else if (count($selectedcourseids) > 0) {
+        return 'id = '.$selectedcourseids[0];
+    }
+    else {
+        // No valid courses found, make sure the query is unsuccessful
+        return '1 = 0';
+    }
+}
+
 ?>
