Merge branch 'MDL-46481-master' of https://github.com/FMCorz/moodle
authorSam Hemelryk <sam@moodle.com>
Wed, 8 Oct 2014 19:51:40 +0000 (08:51 +1300)
committerSam Hemelryk <sam@moodle.com>
Wed, 8 Oct 2014 19:51:40 +0000 (08:51 +1300)
Conflicts:
theme/bootstrapbase/style/moodle.css

249 files changed:
admin/tool/messageinbound/index.php
blocks/navigation/tests/behat/expand_my_courses_setting.feature
blocks/navigation/tests/behat/view_my_courses.feature
grade/edit/tree/grade.php
grade/edit/tree/index.php
grade/edit/tree/lib.php
grade/import/csv/classes/load_data.php [new file with mode: 0644]
grade/import/csv/classes/output/renderer.php [new file with mode: 0644]
grade/import/csv/index.php
grade/import/csv/tests/fixtures/phpunit_gradeimport_csv_load_data.php [new file with mode: 0644]
grade/import/csv/tests/load_data_test.php [new file with mode: 0644]
grade/import/csv/version.php
grade/import/direct/classes/import_form.php [new file with mode: 0644]
grade/import/direct/classes/mapping_form.php [new file with mode: 0644]
grade/import/direct/db/access.php [new file with mode: 0644]
grade/import/direct/index.php [new file with mode: 0644]
grade/import/direct/lang/en/gradeimport_direct.php [new file with mode: 0644]
grade/import/direct/styles.css [new file with mode: 0644]
grade/import/direct/version.php [new file with mode: 0644]
grade/import/grade_import_form.php
grade/lib.php
grade/report/grader/ajax_callbacks.php
grade/report/grader/index.php
grade/report/grader/lang/en/gradereport_grader.php
grade/report/grader/lib.php
grade/report/grader/module.js
grade/report/grader/preferences.php
grade/report/grader/settings.php
grade/report/grader/styles.css
grade/report/grader/yui/build/moodle-gradereport_grader-gradereporttable/moodle-gradereport_grader-gradereporttable-debug.js [new file with mode: 0644]
grade/report/grader/yui/build/moodle-gradereport_grader-gradereporttable/moodle-gradereport_grader-gradereporttable-min.js [new file with mode: 0644]
grade/report/grader/yui/build/moodle-gradereport_grader-gradereporttable/moodle-gradereport_grader-gradereporttable.js [new file with mode: 0644]
grade/report/grader/yui/build/moodle-gradereport_grader-scrollview/moodle-gradereport_grader-scrollview-debug.js [deleted file]
grade/report/grader/yui/build/moodle-gradereport_grader-scrollview/moodle-gradereport_grader-scrollview-min.js [deleted file]
grade/report/grader/yui/build/moodle-gradereport_grader-scrollview/moodle-gradereport_grader-scrollview.js [deleted file]
grade/report/grader/yui/src/gradereporttable/build.json [new file with mode: 0644]
grade/report/grader/yui/src/gradereporttable/js/floatingheaders.js [new file with mode: 0644]
grade/report/grader/yui/src/gradereporttable/js/gradereporttable.js [new file with mode: 0644]
grade/report/grader/yui/src/gradereporttable/meta/gradereporttable.json [new file with mode: 0644]
grade/report/grader/yui/src/scrollview/build.json [deleted file]
grade/report/grader/yui/src/scrollview/js/scrollview.js [deleted file]
grade/report/grader/yui/src/scrollview/meta/scrollview.json [deleted file]
grade/tests/behat/grade_UI_settings.feature
grade/tests/behat/grade_aggregation.feature
grade/tests/behat/grade_calculated_weights.feature
grade/tests/behat/grade_mingrade.feature
grade/tests/behat/grade_natural_normalisation.feature
grade/tests/behat/grade_scales.feature
grade/tests/behat/grade_view.feature
lang/en/deprecated.txt
lang/en/grades.php
lib/classes/event/grade_deleted.php [new file with mode: 0644]
lib/classes/plugin_manager.php
lib/db/services.php
lib/google/Google/Auth/Abstract.php [moved from lib/google/auth/Google_Auth.php with 57% similarity, mode: 0755]
lib/google/Google/Auth/AppIdentity.php [new file with mode: 0755]
lib/google/Google/Auth/AssertionCredentials.php [moved from lib/google/auth/Google_AssertionCredentials.php with 59% similarity, mode: 0755]
lib/google/Google/Auth/Exception.php [new file with mode: 0755]
lib/google/Google/Auth/LoginTicket.php [moved from lib/google/auth/Google_LoginTicket.php with 82% similarity, mode: 0755]
lib/google/Google/Auth/OAuth2.php [new file with mode: 0755]
lib/google/Google/Auth/Simple.php [new file with mode: 0755]
lib/google/Google/Cache/Abstract.php [moved from lib/google/cache/Google_Cache.php with 82% similarity, mode: 0755]
lib/google/Google/Cache/Apc.php [new file with mode: 0755]
lib/google/Google/Cache/Exception.php [new file with mode: 0755]
lib/google/Google/Cache/File.php [new file with mode: 0755]
lib/google/Google/Cache/Memcache.php [new file with mode: 0755]
lib/google/Google/Cache/Null.php [new file with mode: 0755]
lib/google/Google/Client.php [new file with mode: 0755]
lib/google/Google/Collection.php [new file with mode: 0755]
lib/google/Google/Config.php [new file with mode: 0755]
lib/google/Google/Exception.php [moved from lib/google/service/Google_Service.php with 83% similarity, mode: 0755]
lib/google/Google/Http/Batch.php [moved from lib/google/service/Google_BatchRequest.php with 52% similarity, mode: 0755]
lib/google/Google/Http/CacheParser.php [moved from lib/google/io/Google_CacheParser.php with 87% similarity, mode: 0755]
lib/google/Google/Http/MediaFileUpload.php [new file with mode: 0755]
lib/google/Google/Http/REST.php [moved from lib/google/io/Google_REST.php with 61% similarity, mode: 0755]
lib/google/Google/Http/Request.php [new file with mode: 0755]
lib/google/Google/IO/Abstract.php [new file with mode: 0755]
lib/google/Google/IO/Curl.php [new file with mode: 0755]
lib/google/Google/IO/Exception.php [new file with mode: 0755]
lib/google/Google/IO/Stream.php [new file with mode: 0755]
lib/google/Google/IO/cacerts.pem [new file with mode: 0755]
lib/google/Google/Model.php [new file with mode: 0755]
lib/google/Google/Service.php [new file with mode: 0755]
lib/google/Google/Service/AdExchangeBuyer.php [new file with mode: 0755]
lib/google/Google/Service/AdExchangeSeller.php [new file with mode: 0755]
lib/google/Google/Service/AdSense.php [new file with mode: 0755]
lib/google/Google/Service/AdSenseHost.php [new file with mode: 0755]
lib/google/Google/Service/Admin.php [new file with mode: 0755]
lib/google/Google/Service/Analytics.php [new file with mode: 0755]
lib/google/Google/Service/AndroidPublisher.php [new file with mode: 0755]
lib/google/Google/Service/AppState.php [new file with mode: 0755]
lib/google/Google/Service/Appsactivity.php [new file with mode: 0755]
lib/google/Google/Service/Audit.php [new file with mode: 0755]
lib/google/Google/Service/Autoscaler.php [new file with mode: 0755]
lib/google/Google/Service/Bigquery.php [new file with mode: 0755]
lib/google/Google/Service/Blogger.php [new file with mode: 0755]
lib/google/Google/Service/Books.php [new file with mode: 0755]
lib/google/Google/Service/Calendar.php [new file with mode: 0755]
lib/google/Google/Service/CivicInfo.php [new file with mode: 0755]
lib/google/Google/Service/CloudMonitoring.php [new file with mode: 0755]
lib/google/Google/Service/Compute.php [new file with mode: 0755]
lib/google/Google/Service/Coordinate.php [new file with mode: 0755]
lib/google/Google/Service/Customsearch.php [new file with mode: 0755]
lib/google/Google/Service/Datastore.php [new file with mode: 0755]
lib/google/Google/Service/Dfareporting.php [new file with mode: 0755]
lib/google/Google/Service/Directory.php [new file with mode: 0755]
lib/google/Google/Service/Dns.php [new file with mode: 0755]
lib/google/Google/Service/DoubleClickBidManager.php [new file with mode: 0755]
lib/google/Google/Service/Doubleclicksearch.php [new file with mode: 0755]
lib/google/Google/Service/Drive.php [new file with mode: 0755]
lib/google/Google/Service/Exception.php [new file with mode: 0755]
lib/google/Google/Service/Freebase.php [new file with mode: 0755]
lib/google/Google/Service/Fusiontables.php [new file with mode: 0755]
lib/google/Google/Service/Games.php [new file with mode: 0755]
lib/google/Google/Service/GamesManagement.php [new file with mode: 0755]
lib/google/Google/Service/Genomics.php [new file with mode: 0755]
lib/google/Google/Service/Gmail.php [new file with mode: 0755]
lib/google/Google/Service/GroupsMigration.php [new file with mode: 0755]
lib/google/Google/Service/Groupssettings.php [new file with mode: 0755]
lib/google/Google/Service/IdentityToolkit.php [new file with mode: 0755]
lib/google/Google/Service/Licensing.php [new file with mode: 0755]
lib/google/Google/Service/Manager.php [new file with mode: 0755]
lib/google/Google/Service/MapsEngine.php [new file with mode: 0755]
lib/google/Google/Service/Mirror.php [new file with mode: 0755]
lib/google/Google/Service/Oauth2.php [new file with mode: 0755]
lib/google/Google/Service/Orkut.php [new file with mode: 0755]
lib/google/Google/Service/Pagespeedonline.php [new file with mode: 0755]
lib/google/Google/Service/Plus.php [new file with mode: 0755]
lib/google/Google/Service/PlusDomains.php [new file with mode: 0755]
lib/google/Google/Service/Prediction.php [new file with mode: 0755]
lib/google/Google/Service/Pubsub.php [new file with mode: 0755]
lib/google/Google/Service/QPXExpress.php [new file with mode: 0755]
lib/google/Google/Service/Replicapool.php [new file with mode: 0755]
lib/google/Google/Service/Reports.php [new file with mode: 0755]
lib/google/Google/Service/Reseller.php [new file with mode: 0755]
lib/google/Google/Service/Resource.php [new file with mode: 0755]
lib/google/Google/Service/Resourceviews.php [new file with mode: 0755]
lib/google/Google/Service/SQLAdmin.php [new file with mode: 0755]
lib/google/Google/Service/ShoppingContent.php [new file with mode: 0755]
lib/google/Google/Service/SiteVerification.php [new file with mode: 0755]
lib/google/Google/Service/Spectrum.php [new file with mode: 0755]
lib/google/Google/Service/Storage.php [new file with mode: 0755]
lib/google/Google/Service/Taskqueue.php [new file with mode: 0755]
lib/google/Google/Service/Tasks.php [new file with mode: 0755]
lib/google/Google/Service/Translate.php [new file with mode: 0755]
lib/google/Google/Service/Urlshortener.php [new file with mode: 0755]
lib/google/Google/Service/Webfonts.php [new file with mode: 0755]
lib/google/Google/Service/YouTube.php [new file with mode: 0755]
lib/google/Google/Service/YouTubeAnalytics.php [new file with mode: 0755]
lib/google/Google/Signer/Abstract.php [moved from lib/google/auth/Google_Signer.php with 91% similarity, mode: 0755]
lib/google/Google/Signer/P12.php [new file with mode: 0755]
lib/google/Google/Utils.php [moved from lib/google/service/Google_Utils.php with 78% similarity, mode: 0755]
lib/google/Google/Utils/URITemplate.php [new file with mode: 0755]
lib/google/Google/Verifier/Abstract.php [moved from lib/google/auth/Google_Verifier.php with 91% similarity, mode: 0755]
lib/google/Google/Verifier/Pem.php [moved from lib/google/auth/Google_PemVerifier.php with 72% similarity, mode: 0755]
lib/google/NOTICE [deleted file]
lib/google/README [deleted file]
lib/google/README.md [new file with mode: 0644]
lib/google/auth/Google_AuthNone.php [deleted file]
lib/google/auth/Google_OAuth2.php [deleted file]
lib/google/auth/Google_P12Signer.php [deleted file]
lib/google/cache/Google_ApcCache.php [deleted file]
lib/google/cache/Google_FileCache.php [deleted file]
lib/google/cache/Google_MemcacheCache.php [deleted file]
lib/google/contrib/Google_AdexchangebuyerService.php [deleted file]
lib/google/contrib/Google_AdsenseService.php [deleted file]
lib/google/contrib/Google_AdsensehostService.php [deleted file]
lib/google/contrib/Google_AnalyticsService.php [deleted file]
lib/google/contrib/Google_BigqueryService.php [deleted file]
lib/google/contrib/Google_BloggerService.php [deleted file]
lib/google/contrib/Google_BooksService.php [deleted file]
lib/google/contrib/Google_CalendarService.php [deleted file]
lib/google/contrib/Google_ComputeService.php [deleted file]
lib/google/contrib/Google_CustomsearchService.php [deleted file]
lib/google/contrib/Google_DriveService.php [deleted file]
lib/google/contrib/Google_FreebaseService.php [deleted file]
lib/google/contrib/Google_FusiontablesService.php [deleted file]
lib/google/contrib/Google_GanService.php [deleted file]
lib/google/contrib/Google_LatitudeService.php [deleted file]
lib/google/contrib/Google_LicensingService.php [deleted file]
lib/google/contrib/Google_ModeratorService.php [deleted file]
lib/google/contrib/Google_Oauth2Service.php [deleted file]
lib/google/contrib/Google_OrkutService.php [deleted file]
lib/google/contrib/Google_PagespeedonlineService.php [deleted file]
lib/google/contrib/Google_PlusMomentsService.php [deleted file]
lib/google/contrib/Google_PlusService.php [deleted file]
lib/google/contrib/Google_PredictionService.php [deleted file]
lib/google/contrib/Google_ShoppingService.php [deleted file]
lib/google/contrib/Google_SiteVerificationService.php [deleted file]
lib/google/contrib/Google_StorageService.php [deleted file]
lib/google/contrib/Google_TaskqueueService.php [deleted file]
lib/google/contrib/Google_TasksService.php [deleted file]
lib/google/contrib/Google_TranslateService.php [deleted file]
lib/google/contrib/Google_UrlshortenerService.php [deleted file]
lib/google/contrib/Google_WebfontsService.php [deleted file]
lib/google/contrib/Google_YoutubeService.php [deleted file]
lib/google/curlio.php
lib/google/external/URITemplateParser.php [deleted file]
lib/google/io/Google_CurlIO.php [deleted file]
lib/google/io/Google_HttpRequest.php [deleted file]
lib/google/io/Google_IO.php [deleted file]
lib/google/io/cacerts.pem [deleted file]
lib/google/lib.php [new file with mode: 0644]
lib/google/readme_moodle.txt
lib/google/service/Google_MediaFileUpload.php [deleted file]
lib/google/service/Google_Model.php [deleted file]
lib/google/service/Google_ServiceResource.php [deleted file]
lib/grade/grade_category.php
lib/grade/grade_grade.php
lib/grade/grade_item.php
lib/password_compat/lib/password.php
lib/password_compat/readme_moodle.txt
lib/password_compat/tests/PasswordGetInfoTest.php
lib/password_compat/tests/PasswordHashTest.php
lib/password_compat/tests/PasswordNeedsRehashTest.php
lib/password_compat/tests/PasswordVerifyTest.php
lib/tests/behat/behat_general.php
lib/tests/event_grade_deleted_test.php [new file with mode: 0644]
lib/tests/event_user_graded_test.php
lib/thirdpartylibs.xml
lib/timezone.txt
lib/upgrade.txt
lib/weblib.php
mod/assign/tests/events_test.php
mod/forum/db/services.php
mod/forum/externallib.php
mod/forum/tests/externallib_test.php
mod/lesson/format.php
mod/lesson/tests/behat/import_images.feature [new file with mode: 0644]
mod/lesson/tests/behat/questions_images.feature
mod/lesson/tests/fixtures/multichoice.xml [new file with mode: 0644]
mod/quiz/tests/behat/completion_condition_attempts_used.feature
mod/quiz/tests/behat/completion_condition_passing_grade.feature
mod/quiz/tests/events_test.php
mod/scorm/tests/events_test.php
my/tests/behat/add_blocks.feature
my/tests/behat/reset_page.feature
my/tests/behat/restrict_available_blocks.feature
phpunit.xml.dist
question/format/gift/tests/behat/import_export.feature [moved from question/format/gift/tests/behat/import.feature with 82% similarity]
question/format/xml/tests/behat/import_export.feature [moved from question/format/xml/tests/behat/import.feature with 92% similarity]
report/participation/index.php
repository/googledocs/lib.php
theme/base/style/grade.css
theme/bootstrapbase/less/moodle/grade.less
theme/bootstrapbase/style/moodle.css
theme/upgrade.txt
user/tests/behat/edituserpassword.feature
version.php

index 8aa16ad..45d8f7f 100644 (file)
@@ -62,11 +62,11 @@ if (empty($classname)) {
         $record->defaultexpiration = (int) $data->defaultexpiration;
 
         if ($handler->can_change_validateaddress()) {
-            $record->validateaddress = (int) $data->validateaddress;
+            $record->validateaddress = !empty($data->validateaddress);
         }
 
         if ($handler->can_change_enabled()) {
-            $record->enabled = (int) $data->enabled;
+            $record->enabled = !empty($data->enabled);
         }
         $DB->update_record('messageinbound_handlers', $record);
         redirect($PAGE->url);
index b1a0b7b..ad6e9f6 100644 (file)
@@ -22,7 +22,7 @@ Feature: Test expand my courses navigation setting
 
   Scenario: The My Courses branch is expanded on the My Moodle page by default
     When I log in as "student1"
-    And I follow "My home"
+    And I click on "My home" "link" in the "Navigation" "block"
     Then I should see "c1" in the "Navigation" "block"
     And I should see "c2" in the "Navigation" "block"
     And I should not see "c3" in the "Navigation" "block"
@@ -33,8 +33,8 @@ Feature: Test expand my courses navigation setting
     And I set the following administration settings values:
       | Show My courses expanded on My home | 0 |
     And I log out
-    Given I log in as "student1"
-    And I follow "My home"
+    When I log in as "student1"
+    And I click on "My home" "link" in the "Navigation" "block"
     Then I should not see "c1" in the "Navigation" "block"
     And I should not see "c2" in the "Navigation" "block"
     And I should not see "c3" in the "Navigation" "block"
@@ -45,8 +45,8 @@ Feature: Test expand my courses navigation setting
     And I set the following administration settings values:
       | Show My courses expanded on My home | 0 |
     And I log out
-    Given I log in as "student1"
-    And I follow "My home"
+    When I log in as "student1"
+    And I click on "My home" "link" in the "Navigation" "block"
     And I should not see "c1" in the "Navigation" "block"
     And I should not see "c2" in the "Navigation" "block"
     And I should not see "c3" in the "Navigation" "block"
index a629d9e..d20bbb0 100644 (file)
@@ -37,7 +37,7 @@ Feature: View my courses in navigation block
       | Show my course categories | 0 |
     And I log out
     And I log in as "student1"
-    When I follow "My home"
+    When I click on "My home" "link" in the "Navigation" "block"
     Then I should not see "cat1" in the "Navigation" "block"
     And I should not see "cat2" in the "Navigation" "block"
     And I should see "c1" in the "Navigation" "block"
@@ -53,7 +53,7 @@ Feature: View my courses in navigation block
       | Show my course categories | 1 |
     And I log out
     And I log in as "student1"
-    When I follow "My home"
+    When I click on "My home" "link" in the "Navigation" "block"
     Then I should see "cat1" in the "Navigation" "block"
     And I should see "cat3" in the "Navigation" "block"
     And I should not see "cat2" in the "Navigation" "block"
index 62536f6..dd9cce3 100644 (file)
@@ -265,14 +265,6 @@ if ($mform->is_cancelled()) {
         $grade_item->force_regrading();
     }
 
-    $grade_grade = new grade_grade(array('userid'=>$data->userid, 'itemid'=>$grade_item->id), true);
-    if ($old_grade_grade->finalgrade != $grade_grade->finalgrade
-        or empty($old_grade_grade->overridden) != empty($grade_grade->overridden)
-    ) {
-        $grade_grade->grade_item = $grade_item;
-        \core\event\user_graded::create_from_grade($grade_grade)->trigger();
-    }
-
     redirect($returnurl);
 }
 
index 849c23b..e73bc93 100644 (file)
@@ -244,7 +244,7 @@ if ($data = data_submitted() and confirm_sesskey()) {
     }
 }
 
-print_grade_page_head($courseid, 'settings', 'setup', get_string('setupgradeslayout', 'grades'));
+print_grade_page_head($courseid, 'settings', 'setup', get_string('categoriesanditems', 'grades'));
 
 // Print Table of categories and items
 echo $OUTPUT->box_start('gradetreebox generalbox');
index 177fdb4..aa4e077 100644 (file)
@@ -44,9 +44,6 @@ class grade_edit_tree {
 
     public $uses_weight = false;
 
-    /** @var bool indicates if tree has categories with aggregation method other than Natural. */
-    protected $uses_non_natural = false;
-
     public $table;
 
     public $categories = array();
@@ -73,10 +70,6 @@ class grade_edit_tree {
 
         $this->columns = array(grade_edit_tree_column::factory('name', array('deepest_level' => $this->deepest_level)));
 
-        if ($this->uses_non_natural) {
-            $this->columns[] = grade_edit_tree_column::factory('aggregation', array('flag' => true));
-        }
-
         if ($this->uses_weight) {
             $this->columns[] = grade_edit_tree_column::factory('weight', array('adv' => 'weight'));
         }
@@ -120,7 +113,7 @@ class grade_edit_tree {
 
         $object = $element['object'];
         $eid    = $element['eid'];
-        $object->name = $this->gtree->get_element_header($element, true, true, false);
+        $object->name = $this->gtree->get_element_header($element, true, true, true, true);
         $object->stripped_name = $this->gtree->get_element_header($element, false, false, false);
 
         $is_category_item = false;
@@ -523,10 +516,6 @@ class grade_edit_tree {
         $level++;
         $coefstring = $element['object']->get_coefstring();
         if ($element['type'] == 'category') {
-            if ($element['object']->aggregation != GRADE_AGGREGATE_SUM) {
-                $this->uses_non_natural = true;
-            }
-
             if ($coefstring == 'aggregationcoefweight' || $coefstring == 'aggregationcoefextraweightsum' ||
                     $coefstring == 'aggregationcoefextraweight') {
                 $this->uses_weight = true;
@@ -680,47 +669,6 @@ class grade_edit_tree_column_name extends grade_edit_tree_column {
     }
 }
 
-/**
- * Class grade_edit_tree_column_aggregation
- *
- * @package   core_grades
- * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-class grade_edit_tree_column_aggregation extends grade_edit_tree_column {
-
-    public function __construct($params) {
-        parent::__construct('aggregation');
-    }
-
-    public function get_header_cell() {
-        global $OUTPUT;
-        $headercell = clone($this->headercell);
-        $headercell->text = get_string('aggregation', 'grades').$OUTPUT->help_icon('aggregation', 'grades');
-        return $headercell;
-    }
-
-    public function get_category_cell($category, $levelclass, $params) {
-        global $CFG, $OUTPUT;
-        if (empty($params['id'])) {
-            throw new Exception('Array key (id) missing from 3rd param of grade_edit_tree_column_aggregation::get_category_cell($category, $levelclass, $params)');
-        }
-
-        $options = grade_helper::get_aggregation_strings();
-        $aggregation = $options[$category->aggregation];
-
-        $categorycell = parent::get_category_cell($category, $levelclass, $params);
-        $categorycell->text = $aggregation;
-        return $categorycell;
-
-    }
-
-    public function get_item_cell($item, $params) {
-        $itemcell = parent::get_item_cell($item, $params);
-        $itemcell->text = ' - ';
-        return $itemcell;
-    }
-}
-
 /**
  * Class grade_edit_tree_column_weight
  *
diff --git a/grade/import/csv/classes/load_data.php b/grade/import/csv/classes/load_data.php
new file mode 100644 (file)
index 0000000..3cbb069
--- /dev/null
@@ -0,0 +1,608 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle 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 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle 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 Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * A class for loading and preparing grade data from import.
+ *
+ * @package   gradeimport_csv
+ * @copyright 2014 Adrian Greeve <adrian@moodle.com>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * A class for loading and preparing grade data from import.
+ *
+ * @package   gradeimport_csv
+ * @copyright 2014 Adrian Greeve <adrian@moodle.com>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class gradeimport_csv_load_data {
+
+    /** @var string $error csv import error. */
+    protected $error;
+    /** @var int $iid Unique identifier for these csv records. */
+    protected $iid;
+    /** @var array $headers Column names for the data. */
+    protected $headers;
+    /** @var array $previewdata A subsection of the csv imported data. */
+    protected $previewdata;
+
+    // The map_user_data_with_value variables.
+    /** @var array $newgrades Grades to be inserted into the gradebook. */
+    protected $newgrades;
+    /** @var array $newfeedbacks Feedback to be inserted into the gradebook. */
+    protected $newfeedbacks;
+    /** @var int $studentid Student ID*/
+    protected $studentid;
+
+    // The prepare_import_grade_data() variables.
+    /** @var bool $status The current status of the import. True = okay, False = errors. */
+    protected $status;
+    /** @var int $importcode The code for this batch insert. */
+    protected $importcode;
+    /** @var array $gradebookerrors An array of errors from trying to import into the gradebook. */
+    protected $gradebookerrors;
+    /** @var array $newgradeitems An array of new grade items to be inserted into the gradebook. */
+    protected $newgradeitems;
+
+    /**
+     * Load CSV content for previewing.
+     *
+     * @param string $text The grade data being imported.
+     * @param string $encoding The type of encoding the file uses.
+     * @param string $separator The separator being used to define each field.
+     * @param int $previewrows How many rows are being previewed.
+     */
+    public function load_csv_content($text, $encoding, $separator, $previewrows) {
+        $this->raise_limits();
+
+        $this->iid = csv_import_reader::get_new_iid('grade');
+        $csvimport = new csv_import_reader($this->iid, 'grade');
+
+        $csvimport->load_csv_content($text, $encoding, $separator);
+        $this->error = $csvimport->get_error();
+
+        // Get header (field names).
+        $this->headers = $csvimport->get_columns();
+        $this->trim_headers();
+
+        $csvimport->init();
+        $this->previewdata = array();
+
+        for ($numlines = 0; $numlines <= $previewrows; $numlines++) {
+            $lines = $csvimport->next();
+            if ($lines) {
+                $this->previewdata[] = $lines;
+            }
+        }
+    }
+
+    /**
+     * Gets all of the grade items in this course.
+     *
+     * @param int $courseid Course id;
+     * @return array An array of grade items for the course.
+     */
+    public static function fetch_grade_items($courseid) {
+        $gradeitems = null;
+        if ($allgradeitems = grade_item::fetch_all(array('courseid' => $courseid))) {
+            foreach ($allgradeitems as $gradeitem) {
+                // Skip course type and category type.
+                if ($gradeitem->itemtype == 'course' || $gradeitem->itemtype == 'category') {
+                    continue;
+                }
+
+                $displaystring = null;
+                if (!empty($gradeitem->itemmodule)) {
+                    $displaystring = get_string('modulename', $gradeitem->itemmodule).get_string('labelsep', 'langconfig')
+                            .$gradeitem->get_name();
+                } else {
+                    $displaystring = $gradeitem->get_name();
+                }
+                $gradeitems[$gradeitem->id] = $displaystring;
+            }
+        }
+        return $gradeitems;
+    }
+
+    /**
+     * Cleans the column headers from the CSV file.
+     */
+    protected function trim_headers() {
+        foreach ($this->headers as $i => $h) {
+            $h = trim($h); // Remove whitespace.
+            $h = clean_param($h, PARAM_RAW); // Clean the header.
+            $this->headers[$i] = $h;
+        }
+    }
+
+    /**
+     * Raises the php execution time and memory limits for importing the CSV file.
+     */
+    protected function raise_limits() {
+        // Large files are likely to take their time and memory. Let PHP know
+        // that we'll take longer, and that the process should be recycled soon
+        // to free up memory.
+        core_php_time_limit::raise();
+        raise_memory_limit(MEMORY_EXTRA);
+    }
+
+    /**
+     * Inserts a record into the grade_import_values table. This also adds common record information.
+     *
+     * @param object $record The grade record being inserted into the database.
+     * @param int $studentid The student ID.
+     * @return bool|int true or insert id on success. Null if the grade value is too high.
+     */
+    protected function insert_grade_record($record, $studentid) {
+        global $DB, $USER, $CFG;
+        $record->importcode = $this->importcode;
+        $record->userid     = $studentid;
+        $record->importer   = $USER->id;
+        // By default the maximum grade is 100.
+        $gradepointmaximum = 100;
+        // If the grade limit has been increased then use the gradepointmax setting.
+        if ($CFG->unlimitedgrades) {
+            $gradepointmaximum = $CFG->gradepointmax;
+        }
+        // If the record final grade is set then check that the grade value isn't too high.
+        // Final grade will not be set if we are inserting feedback.
+        if (!isset($record->finalgrade) || $record->finalgrade <= $gradepointmaximum) {
+            return $DB->insert_record('grade_import_values', $record);
+        } else {
+            $this->cleanup_import(get_string('gradevaluetoobig', 'grades', $gradepointmaximum));
+            return null;
+        }
+    }
+
+    /**
+     * Insert the new grade into the grade item buffer table.
+     *
+     * @param array $header The column headers from the CSV file.
+     * @param int $key Current row identifier.
+     * @param string $value The value for this row (final grade).
+     * @return array new grades that are ready for commiting to the gradebook.
+     */
+    protected function import_new_grade_item($header, $key, $value) {
+        global $DB, $USER;
+
+        // First check if header is already in temp database.
+        if (empty($this->newgradeitems[$key])) {
+
+            $newgradeitem = new stdClass();
+            $newgradeitem->itemname = $header[$key];
+            $newgradeitem->importcode = $this->importcode;
+            $newgradeitem->importer = $USER->id;
+
+            // Insert into new grade item buffer.
+            $this->newgradeitems[$key] = $DB->insert_record('grade_import_newitem', $newgradeitem);
+        }
+        $newgrade = new stdClass();
+        $newgrade->newgradeitem = $this->newgradeitems[$key];
+
+        // If the user has a grade for this grade item.
+        if (trim($value) != '-') {
+            // Instead of omitting the grade we could insert one with finalgrade set to 0.
+            // We do not have access to grade item min grade.
+            $newgrade->finalgrade = $value;
+            $newgrades[] = $newgrade;
+        }
+        return $newgrades;
+    }
+
+    /**
+     * Check that the user is in the system.
+     *
+     * @param string $value The value, from the csv file, being mapped to identify the user.
+     * @param array $userfields Contains the field and label being mapped from.
+     * @return int Returns the user ID if it exists, otherwise null.
+     */
+    protected function check_user_exists($value, $userfields) {
+        global $DB;
+
+        $usercheckproblem = false;
+        $user = null;
+        // The user may use the incorrect field to match the user. This could result in an exception.
+        try {
+            $user = $DB->get_record('user', array($userfields['field'] => $value));
+        } catch (Exception $e) {
+            $usercheckproblem = true;
+        }
+        // Field may be fine, but no records were returned.
+        if (!$user || $usercheckproblem) {
+            $usermappingerrorobj = new stdClass();
+            $usermappingerrorobj->field = $userfields['label'];
+            $usermappingerrorobj->value = $value;
+            $this->cleanup_import(get_string('usermappingerror', 'grades', $usermappingerrorobj));
+            unset($usermappingerrorobj);
+            return null;
+        }
+        return $user->id;
+    }
+
+    /**
+     * Check to see if the feedback matches a grade item.
+     *
+     * @param int $courseid The course ID.
+     * @param int $itemid The ID of the grade item that the feedback relates to.
+     * @param string $value The actual feedback being imported.
+     * @return object Creates a feedback object with the item ID and the feedback value.
+     */
+    protected function create_feedback($courseid, $itemid, $value) {
+        // Case of an id, only maps id of a grade_item.
+        // This was idnumber.
+        if (!new grade_item(array('id' => $itemid, 'courseid' => $courseid))) {
+            // Supplied bad mapping, should not be possible since user
+            // had to pick mapping.
+            $this->cleanup_import(get_string('importfailed', 'grades'));
+            return null;
+        }
+
+        // The itemid is the id of the grade item.
+        $feedback = new stdClass();
+        $feedback->itemid   = $itemid;
+        $feedback->feedback = $value;
+        return $feedback;
+    }
+
+    /**
+     * This updates existing grade items.
+     *
+     * @param int $courseid The course ID.
+     * @param array $map Mapping information provided by the user.
+     * @param int $key The line that we are currently working on.
+     * @param bool $verbosescales Form setting for grading with scales.
+     * @param string $value The grade value .
+     * @return array grades to be updated.
+     */
+    protected function update_grade_item($courseid, $map, $key, $verbosescales, $value) {
+        // Case of an id, only maps id of a grade_item.
+        // This was idnumber.
+        if (!$gradeitem = new grade_item(array('id' => $map[$key], 'courseid' => $courseid))) {
+            // Supplied bad mapping, should not be possible since user
+            // had to pick mapping.
+            $this->cleanup_import(get_string('importfailed', 'grades'));
+            return null;
+        }
+
+        // Check if grade item is locked if so, abort.
+        if ($gradeitem->is_locked()) {
+            $this->cleanup_import(get_string('gradeitemlocked', 'grades'));
+            return null;
+        }
+
+        $newgrade = new stdClass();
+        $newgrade->itemid = $gradeitem->id;
+        if ($gradeitem->gradetype == GRADE_TYPE_SCALE and $verbosescales) {
+            if ($value === '' or $value == '-') {
+                $value = null; // No grade.
+            } else {
+                $scale = $gradeitem->load_scale();
+                $scales = explode(',', $scale->scale);
+                $scales = array_map('trim', $scales); // Hack - trim whitespace around scale options.
+                array_unshift($scales, '-'); // Scales start at key 1.
+                $key = array_search($value, $scales);
+                if ($key === false) {
+                    $this->cleanup_import(get_string('badgrade', 'grades'));
+                    return null;
+                }
+                $value = $key;
+            }
+            $newgrade->finalgrade = $value;
+        } else {
+            if ($value === '' or $value == '-') {
+                $value = null; // No grade.
+            } else {
+                // If the value has a local decimal or can correctly be unformatted, do it.
+                $validvalue = unformat_float($value, true);
+                if ($validvalue !== false) {
+                    $value = $validvalue;
+                } else {
+                    // Non numeric grade value supplied, possibly mapped wrong column.
+                    $this->cleanup_import(get_string('badgrade', 'grades'));
+                    return null;
+                }
+            }
+            $newgrade->finalgrade = $value;
+        }
+        $this->newgrades[] = $newgrade;
+        return $this->newgrades;
+    }
+
+    /**
+     * Clean up failed CSV grade import. Clears the temp table for inserting grades.
+     *
+     * @param string $notification The error message to display from the unsuccessful grade import.
+     */
+    protected function cleanup_import($notification) {
+        $this->status = false;
+        import_cleanup($this->importcode);
+        $this->gradebookerrors[] = $notification;
+    }
+
+    /**
+     * Check user mapping.
+     *
+     * @param string $mappingidentifier The user field that we are matching together.
+     * @param string $value The value we are checking / importing.
+     * @param array $header The column headers of the csv file.
+     * @param array $map Mapping information provided by the user.
+     * @param int $key Current row identifier.
+     * @param int $courseid The course ID.
+     * @param int $feedbackgradeid The ID of the grade item that the feedback relates to.
+     * @param bool $verbosescales Form setting for grading with scales.
+     */
+    protected function map_user_data_with_value($mappingidentifier, $value, $header, $map, $key, $courseid, $feedbackgradeid,
+            $verbosescales) {
+
+        // Fields that the user can be mapped from.
+        $userfields = array(
+            'userid' => array(
+                'field' => 'id',
+                'label' => 'id',
+            ),
+            'useridnumber' => array(
+                'field' => 'idnumber',
+                'label' => 'idnumber',
+            ),
+            'useremail' => array(
+                'field' => 'email',
+                'label' => 'email address',
+            ),
+            'username' => array(
+                'field' => 'username',
+                'label' => 'username',
+            ),
+        );
+
+        switch ($mappingidentifier) {
+            case 'userid':
+            case 'useridnumber':
+            case 'useremail':
+            case 'username':
+                // Skip invalid row with blank user field.
+                if (!empty($value)) {
+                    $this->studentid = $this->check_user_exists($value, $userfields[$mappingidentifier]);
+                }
+            break;
+            case 'new':
+                $this->newgrades = $this->import_new_grade_item($header, $key, $value);
+            break;
+            case 'feedback':
+                if ($feedbackgradeid) {
+                    $feedback = $this->create_feedback($courseid, $feedbackgradeid, $value);
+                    if (isset($feedback)) {
+                        $this->newfeedbacks[] = $feedback;
+                    }
+                }
+            break;
+            default:
+                // Existing grade items.
+                if (!empty($map[$key])) {
+                    $this->newgrades = $this->update_grade_item($courseid, $map, $key, $verbosescales, $value,
+                            $mappingidentifier);
+                }
+                // Otherwise, we ignore this column altogether because user has chosen
+                // to ignore them (e.g. institution, address etc).
+            break;
+        }
+    }
+
+    /**
+     * Checks and prepares grade data for inserting into the gradebook.
+     *
+     * @param array $header Column headers of the CSV file.
+     * @param object $formdata Mapping information from the preview page.
+     * @param object $csvimport csv import reader object for iterating over the imported CSV file.
+     * @param int $courseid The course ID.
+     * @param bool $separatemode If we have groups are they separate?
+     * @param mixed $currentgroup current group information.
+     * @param bool $verbosescales Form setting for grading with scales.
+     * @return bool True if the status for importing is okay, false if there are errors.
+     */
+    public function prepare_import_grade_data($header, $formdata, $csvimport, $courseid, $separatemode, $currentgroup,
+            $verbosescales) {
+        global $DB, $USER;
+
+        // The import code is used for inserting data into the grade tables.
+        $this->importcode = $formdata->importcode;
+        $this->status = true;
+        $this->headers = $header;
+        $this->studentid = null;
+        $this->gradebookerrors = null;
+        // Temporary array to keep track of what new headers are processed.
+        $this->newgradeitems = array();
+        $this->trim_headers();
+
+        $map = array();
+        // Loops mapping_0, mapping_1 .. mapping_n and construct $map array.
+        foreach ($header as $i => $head) {
+            if (isset($formdata->{'mapping_'.$i})) {
+                $map[$i] = $formdata->{'mapping_'.$i};
+            }
+        }
+
+        // If mapping information is supplied.
+        $map[clean_param($formdata->mapfrom, PARAM_RAW)] = clean_param($formdata->mapto, PARAM_RAW);
+
+        // Check for mapto collisions.
+        $maperrors = array();
+        foreach ($map as $i => $j) {
+            if ($j == 0) {
+                // You can have multiple ignores.
+                continue;
+            } else {
+                if (!isset($maperrors[$j])) {
+                    $maperrors[$j] = true;
+                } else {
+                    // Collision.
+                    print_error('cannotmapfield', '', '', $j);
+                }
+            }
+        }
+
+        $this->raise_limits();
+
+        $csvimport->init();
+
+        while ($line = $csvimport->next()) {
+            if (count($line) <= 1) {
+                // There is no data on this line, move on.
+                continue;
+            }
+
+            // Array to hold all grades to be inserted.
+            $this->newgrades = array();
+            // Array to hold all feedback.
+            $this->newfeedbacks = array();
+            // Each line is a student record.
+            foreach ($line as $key => $value) {
+
+                $value = clean_param($value, PARAM_RAW);
+                $value = trim($value);
+
+                /*
+                 * the options are
+                 * 1) userid, useridnumber, usermail, username - used to identify user row
+                 * 2) new - new grade item
+                 * 3) id - id of the old grade item to map onto
+                 * 3) feedback_id - feedback for grade item id
+                 */
+
+                // Explode the mapping for feedback into a label 'feedback' and the identifying number.
+                $mappingbase = explode("_", $map[$key]);
+                $mappingidentifier = $mappingbase[0];
+                // Set the feedback identifier if it exists.
+                if (isset($mappingbase[1])) {
+                    $feedbackgradeid = (int)$mappingbase[1];
+                } else {
+                    $feedbackgradeid = '';
+                }
+
+                $this->map_user_data_with_value($mappingidentifier, $value, $header, $map, $key, $courseid, $feedbackgradeid,
+                        $verbosescales);
+                if ($this->status === false) {
+                    return $this->status;
+                }
+            }
+
+            // No user mapping supplied at all, or user mapping failed.
+            if (empty($this->studentid) || !is_numeric($this->studentid)) {
+                // User not found, abort whole import.
+                $this->cleanup_import(get_string('usermappingerrorusernotfound', 'grades'));
+                break;
+            }
+
+            if ($separatemode and !groups_is_member($currentgroup, $this->studentid)) {
+                // Not allowed to import into this group, abort.
+                $this->cleanup_import(get_string('usermappingerrorcurrentgroup', 'grades'));
+                break;
+            }
+
+            // Insert results of this students into buffer.
+            if ($this->status and !empty($this->newgrades)) {
+
+                foreach ($this->newgrades as $newgrade) {
+
+                    // Check if grade_grade is locked and if so, abort.
+                    if (!empty($newgrade->itemid) and $gradegrade = new grade_grade(array('itemid' => $newgrade->itemid,
+                            'userid' => $this->studentid))) {
+                        if ($gradegrade->is_locked()) {
+                            // Individual grade locked.
+                            $this->cleanup_import(get_string('gradelocked', 'grades'));
+                            return $this->status;
+                        }
+                    }
+                    $insertid = self::insert_grade_record($newgrade, $this->studentid);
+                    // Check to see if the insert was successful.
+                    if (empty($insertid)) {
+                        return null;
+                    }
+                }
+            }
+
+            // Updating/inserting all comments here.
+            if ($this->status and !empty($this->newfeedbacks)) {
+                foreach ($this->newfeedbacks as $newfeedback) {
+                    $sql = "SELECT *
+                              FROM {grade_import_values}
+                             WHERE importcode=? AND userid=? AND itemid=? AND importer=?";
+                    if ($feedback = $DB->get_record_sql($sql, array($this->importcode, $this->studentid, $newfeedback->itemid,
+                            $USER->id))) {
+                        $newfeedback->id = $feedback->id;
+                        $DB->update_record('grade_import_values', $newfeedback);
+
+                    } else {
+                        // The grade item for this is not updated.
+                        $insertid = self::insert_grade_record($newfeedback, $this->studentid);
+                        // Check to see if the insert was successful.
+                        if (empty($insertid)) {
+                            return null;
+                        }
+                    }
+                }
+            }
+        }
+        return $this->status;
+    }
+
+    /**
+     * Returns the headers parameter for this class.
+     *
+     * @return array returns headers parameter for this class.
+     */
+    public function get_headers() {
+        return $this->headers;
+    }
+
+    /**
+     * Returns the error parameter for this class.
+     *
+     * @return string returns error parameter for this class.
+     */
+    public function get_error() {
+        return $this->error;
+    }
+
+    /**
+     * Returns the iid parameter for this class.
+     *
+     * @return int returns iid parameter for this class.
+     */
+    public function get_iid() {
+        return $this->iid;
+    }
+
+    /**
+     * Returns the preview_data parameter for this class.
+     *
+     * @return array returns previewdata parameter for this class.
+     */
+    public function get_previewdata() {
+        return $this->previewdata;
+    }
+
+    /**
+     * Returns the gradebookerrors parameter for this class.
+     *
+     * @return array returns gradebookerrors parameter for this class.
+     */
+    public function get_gradebookerrors() {
+        return $this->gradebookerrors;
+    }
+}
diff --git a/grade/import/csv/classes/output/renderer.php b/grade/import/csv/classes/output/renderer.php
new file mode 100644 (file)
index 0000000..b8750fd
--- /dev/null
@@ -0,0 +1,90 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle 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 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle 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 Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Renderers for the import of CSV files into the gradebook.
+ *
+ * @package   gradeimport_csv
+ * @copyright 2014 Adrian Greeve <adrian@moodle.com>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Renderers for the import of CSV files into the gradebook.
+ *
+ * @package   gradeimport_csv
+ * @copyright 2014 Adrian Greeve <adrian@moodle.com>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class gradeimport_csv_renderer extends plugin_renderer_base {
+
+    /**
+     * A renderer for the standard upload file form.
+     *
+     * @param object $course The course we are doing all of this action in.
+     * @param object $mform The mform for uploading CSV files.
+     * @return string html to be displayed.
+     */
+    public function standard_upload_file_form($course, $mform) {
+
+        $output = groups_print_course_menu($course, 'index.php?id=' . $course->id, true);
+        $output .= html_writer::start_tag('div', array('class' => 'clearer'));
+        $output .= html_writer::end_tag('div');
+
+        // Form.
+        ob_start();
+        $mform->display();
+        $output .= ob_get_contents();
+        ob_end_clean();
+
+        return $output;
+    }
+
+    /**
+     * A renderer for the CSV file preview.
+     *
+     * @param array $header Column headers from the CSV file.
+     * @param array $data The rest of the data from the CSV file.
+     * @return string html to be displayed.
+     */
+    public function import_preview_page($header, $data) {
+
+        $html = $this->output->heading(get_string('importpreview', 'grades'));
+
+        $table = new html_table();
+        $table->head = $header;
+        $table->data = $data;
+        $html .= html_writer::table($table);
+
+        return $html;
+    }
+
+    /**
+     * A renderer for errors generated trying to import the CSV file.
+     *
+     * @param array $errors Display import errors.
+     * @return string errors as html to be displayed.
+     */
+    public function errors($errors) {
+        $html = '';
+        foreach ($errors as $error) {
+            $html .= $this->output->notification($error);
+        }
+        return $html;
+    }
+}
index 5a0dcdd..21c7270 100644 (file)
@@ -22,7 +22,7 @@ require_once($CFG->dirroot. '/grade/import/grade_import_form.php');
 require_once($CFG->dirroot.'/grade/import/lib.php');
 require_once($CFG->libdir . '/csvlib.class.php');
 
-$id            = required_param('id', PARAM_INT); // course id
+$id            = required_param('id', PARAM_INT); // Course id.
 $separator     = optional_param('separator', '', PARAM_ALPHA);
 $verbosescales = optional_param('verbosescales', 1, PARAM_BOOL);
 $iid           = optional_param('iid', null, PARAM_INT);
@@ -46,92 +46,41 @@ $context = context_course::instance($id);
 require_capability('moodle/grade:import', $context);
 require_capability('gradeimport/csv:view', $context);
 
-$separatemode = (groups_get_course_groupmode($COURSE) == SEPARATEGROUPS and !has_capability('moodle/site:accessallgroups', $context));
+$separatemode = (groups_get_course_groupmode($COURSE) == SEPARATEGROUPS and
+        !has_capability('moodle/site:accessallgroups', $context));
 $currentgroup = groups_get_course_group($course);
 
 print_grade_page_head($course->id, 'import', 'csv', get_string('importcsv', 'grades'));
 
-// Set up the grade import mapping form.
-$gradeitems = array();
-if ($id) {
-    if ($grade_items = grade_item::fetch_all(array('courseid'=>$id))) {
-        foreach ($grade_items as $grade_item) {
-            // Skip course type and category type.
-            if ($grade_item->itemtype == 'course' || $grade_item->itemtype == 'category') {
-                continue;
-            }
+$renderer = $PAGE->get_renderer('gradeimport_csv');
 
-            $displaystring = null;
-            if (!empty($grade_item->itemmodule)) {
-                $displaystring = get_string('modulename', $grade_item->itemmodule).get_string('labelsep', 'langconfig')
-                        .$grade_item->get_name();
-            } else {
-                $displaystring = $grade_item->get_name();
-            }
-            $gradeitems[$grade_item->id] = $displaystring;
-        }
-    }
-}
-
-// Set up the import form.
-$mform = new grade_import_form(null, array('includeseparator' => true, 'verbosescales' => true, 'acceptedtypes' =>
-        array('.csv', '.txt')));
+// Get the grade items to be matched with the import mapping columns.
+$gradeitems = gradeimport_csv_load_data::fetch_grade_items($course->id);
 
 // If the csv file hasn't been imported yet then look for a form submission or
 // show the initial submission form.
 if (!$iid) {
-    // If the import form has been submitted.
-    if ($formdata = $mform->get_data()) {
 
-        // Large files are likely to take their time and memory. Let PHP know
-        // that we'll take longer, and that the process should be recycled soon
-        // to free up memory.
-        core_php_time_limit::raise();
-        raise_memory_limit(MEMORY_EXTRA);
-
-        // Use current (non-conflicting) time stamp.
-        $importcode = get_new_importcode();
+    // Set up the import form.
+    $mform = new grade_import_form(null, array('includeseparator' => true, 'verbosescales' => $verbosescales, 'acceptedtypes' =>
+            array('.csv', '.txt')));
 
+    // If the import form has been submitted.
+    if ($formdata = $mform->get_data()) {
         $text = $mform->get_file_content('userfile');
-        $iid = csv_import_reader::get_new_iid('grade');
-        $csvimport = new csv_import_reader($iid, 'grade');
-
-        $csvimport->load_csv_content($text, $formdata->encoding, $separator);
-
-        // --- get header (field names) ---
-        $header = $csvimport->get_columns();
-
-        // Print a preview of the data.
-        $numlines = 0; // 0 lines previewed so far.
-
-        echo $OUTPUT->heading(get_string('importpreview', 'grades'));
-
-        foreach ($header as $i => $h) {
-            $h = trim($h); // Remove whitespace.
-            $h = clean_param($h, PARAM_RAW); // Clean the header.
-            $header[$i] = $h;
-        }
-
-        $table = new html_table();
-        $table->head = $header;
-        $csvimport->init();
-        $previewdata = array();
-        while ($numlines <= $formdata->previewrows) {
-            $lines = $csvimport->next();
-            if ($lines) {
-                $previewdata[] = $lines;
-            }
-            $numlines ++;
+        $csvimport = new gradeimport_csv_load_data();
+        $csvimport->load_csv_content($text, $formdata->encoding, $separator, $formdata->previewrows);
+        $csvimporterror = $csvimport->get_error();
+        if (!empty($csvimporterror)) {
+            echo $renderer->errors(array($csvimport->get_error()));
+            echo $OUTPUT->footer();
+            die();
         }
-        $table->data = $previewdata;
-        echo html_writer::table($table);
+        $iid = $csvimport->get_iid();
+        echo $renderer->import_preview_page($csvimport->get_headers(), $csvimport->get_previewdata());
     } else {
         // Display the standard upload file form.
-        groups_print_course_menu($course, 'index.php?id='.$id);
-        echo html_writer::start_tag('div', array('class' => 'clearer'));
-        echo html_writer::end_tag('div');
-
-        $mform->display();
+        echo $renderer->standard_upload_file_form($course, $mform);
         echo $OUTPUT->footer();
         die();
     }
@@ -140,314 +89,37 @@ if (!$iid) {
 // Data has already been submitted so we can use the $iid to retrieve it.
 $csvimport = new csv_import_reader($iid, 'grade');
 $header = $csvimport->get_columns();
+// Get a new import code for updating to the grade book.
+if (empty($importcode)) {
+    $importcode = get_new_importcode();
+}
 
+$mappingformdata = array(
+    'gradeitems' => $gradeitems,
+    'header' => $header,
+    'iid' => $iid,
+    'id' => $id,
+    'importcode' => $importcode,
+    'verbosescales' => $verbosescales
+);
 // we create a form to handle mapping data from the file to the database.
-$mform2 = new grade_import_mapping_form(null, array('gradeitems'=>$gradeitems, 'header'=>$header));
-$mform2->set_data(array('iid' => $iid, 'id' => $id, 'importcode'=>$importcode, 'verbosescales' => $verbosescales));
+$mform2 = new grade_import_mapping_form(null, $mappingformdata);
 
 // Here, if we have data, we process the fields and enter the information into the database.
 if ($formdata = $mform2->get_data()) {
+    $gradeimport = new gradeimport_csv_load_data();
+    $status = $gradeimport->prepare_import_grade_data($header, $formdata, $csvimport, $course->id, $separatemode,
+            $currentgroup, $verbosescales);
 
-    foreach ($header as $i => $h) {
-        $h = trim($h); // Remove whitespace.
-        $h = clean_param($h, PARAM_RAW); // Clean the header.
-        $header[$i] = $h;
-    }
-
-    $map = array();
-    // loops mapping_0, mapping_1 .. mapping_n and construct $map array
-    foreach ($header as $i => $head) {
-        if (isset($formdata->{'mapping_'.$i})) {
-            $map[$i] = $formdata->{'mapping_'.$i};
-        }
-    }
-
-    // if mapping information is supplied
-    $map[clean_param($formdata->mapfrom, PARAM_RAW)] = clean_param($formdata->mapto, PARAM_RAW);
-
-    // check for mapto collisions
-    $maperrors = array();
-    foreach ($map as $i => $j) {
-        if ($j == 0) {
-            // you can have multiple ignores
-            continue;
-        } else {
-            if (!isset($maperrors[$j])) {
-                $maperrors[$j] = true;
-            } else {
-                // collision
-                print_error('cannotmapfield', '', '', $j);
-            }
-        }
-    }
-
-    // Large files are likely to take their time and memory. Let PHP know
-    // that we'll take longer, and that the process should be recycled soon
-    // to free up memory.
-    core_php_time_limit::raise();
-    raise_memory_limit(MEMORY_EXTRA);
-
-    $userfields = array(
-        'userid' => array(
-            'field' => 'id',
-            'label' => 'id',
-        ),
-        'useridnumber' => array(
-            'field' => 'idnumber',
-            'label' => 'idnumber',
-        ),
-        'useremail' => array(
-            'field' => 'email',
-            'label' => 'email address',
-        ),
-        'username' => array(
-            'field' => 'username',
-            'label' => 'username',
-        ),
-    );
-
-    $csvimport->init();
-
-    $newgradeitems = array(); // temporary array to keep track of what new headers are processed
-    $status = true;
-
-    while ($line = $csvimport->next()) {
-        if(count($line) <= 1){
-            // there is no data on this line, move on
-            continue;
-        }
-
-        // array to hold all grades to be inserted
-        $newgrades = array();
-        // array to hold all feedback
-        $newfeedbacks = array();
-        // each line is a student record
-        foreach ($line as $key => $value) {
-
-            $value = clean_param($value, PARAM_RAW);
-            $value = trim($value);
-
-            /*
-             * the options are
-             * 1) userid, useridnumber, usermail, username - used to identify user row
-             * 2) new - new grade item
-             * 3) id - id of the old grade item to map onto
-             * 3) feedback_id - feedback for grade item id
-             */
-
-            $t = explode("_", $map[$key]);
-            $t0 = $t[0];
-            if (isset($t[1])) {
-                $t1 = (int)$t[1];
-            } else {
-                $t1 = '';
-            }
-
-            switch ($t0) {
-                case 'userid':
-                case 'useridnumber':
-                case 'useremail':
-                case 'username':
-                    // Skip invalid row with blank user field.
-                    if (empty($value)) {
-                        continue 3;
-                    }
-
-                    if (!$user = $DB->get_record('user', array($userfields[$t0]['field'] => $value))) {
-                         // User not found, abort whole import.
-                        import_cleanup($importcode);
-                        $usermappingerrorobj = new stdClass();
-                        $usermappingerrorobj->field = $userfields[$t0]['label'];
-                        $usermappingerrorobj->value = $value;
-                        echo $OUTPUT->notification(get_string('usermappingerror', 'grades', $usermappingerrorobj));
-                        unset($usermappingerrorobj);
-                        $status = false;
-                        break 3;
-                    }
-                    $studentid = $user->id;
-                break;
-                case 'new':
-                    // first check if header is already in temp database
-
-                    if (empty($newgradeitems[$key])) {
-
-                        $newgradeitem = new stdClass();
-                        $newgradeitem->itemname = $header[$key];
-                        $newgradeitem->importcode = $importcode;
-                        $newgradeitem->importer   = $USER->id;
-
-                        // insert into new grade item buffer
-                        $newgradeitems[$key] = $DB->insert_record('grade_import_newitem', $newgradeitem);
-                    }
-                    $newgrade = new stdClass();
-                    $newgrade->newgradeitem = $newgradeitems[$key];
-
-                    // if the user has a grade for this grade item
-                    if (trim($value) != '-') {
-                        // instead of omitting the grade we could insert one with finalgrade set to 0
-                        // we do not have access to grade item min grade
-                        $newgrade->finalgrade   = $value;
-                        $newgrades[] = $newgrade;
-                    }
-                break;
-                case 'feedback':
-                    if ($t1) {
-                        // case of an id, only maps id of a grade_item
-                        // this was idnumber
-                        if (!$gradeitem = new grade_item(array('id'=>$t1, 'courseid'=>$course->id))) {
-                            // supplied bad mapping, should not be possible since user
-                            // had to pick mapping
-                            $status = false;
-                            import_cleanup($importcode);
-                            // Relying on the default import failed message below.
-                            break 3;
-                        }
-
-                        // t1 is the id of the grade item
-                        $feedback = new stdClass();
-                        $feedback->itemid   = $t1;
-                        $feedback->feedback = $value;
-                        $newfeedbacks[] = $feedback;
-                    }
-                break;
-                default:
-                    // existing grade items
-                    if (!empty($map[$key])) {
-                        // case of an id, only maps id of a grade_item
-                        // this was idnumber
-                        if (!$gradeitem = new grade_item(array('id'=>$map[$key], 'courseid'=>$course->id))) {
-                            // supplied bad mapping, should not be possible since user
-                            // had to pick mapping
-                            $status = false;
-                            import_cleanup($importcode);
-                            // Relying on the default import failed message below.
-                            break 3;
-                        }
-
-                        // check if grade item is locked if so, abort
-                        if ($gradeitem->is_locked()) {
-                            $status = false;
-                            import_cleanup($importcode);
-                            echo $OUTPUT->notification(get_string('gradeitemlocked', 'grades'));
-                            break 3;
-                        }
-
-                        $newgrade = new stdClass();
-                        $newgrade->itemid     = $gradeitem->id;
-                        if ($gradeitem->gradetype == GRADE_TYPE_SCALE and $verbosescales) {
-                            if ($value === '' or $value == '-') {
-                                $value = null; // no grade
-                            } else {
-                                $scale = $gradeitem->load_scale();
-                                $scales = explode(',', $scale->scale);
-                                $scales = array_map('trim', $scales); //hack - trim whitespace around scale options
-                                array_unshift($scales, '-'); // scales start at key 1
-                                $key = array_search($value, $scales);
-                                if ($key === false) {
-                                    echo "<br/>t0 is $t0";
-                                    echo "<br/>grade is $value";
-                                    $status = false;
-                                    import_cleanup($importcode);
-                                    echo $OUTPUT->notification(get_string('badgrade', 'grades'));
-                                    break 3;
-                                }
-                                $value = $key;
-                            }
-                            $newgrade->finalgrade = $value;
-                        } else {
-                            if ($value === '' or $value == '-') {
-                                $value = null; // No grade.
-                            } else {
-                                // If the value has a local decimal or can correctly be unformatted, do it.
-                                $validvalue = unformat_float($value, true);
-                                if ($validvalue !== false) {
-                                    $value = $validvalue;
-                                } else {
-                                    // Non numeric grade value supplied, possibly mapped wrong column.
-                                    echo "<br/>t0 is $t0";
-                                    echo "<br/>grade is $value";
-                                    $status = false;
-                                    import_cleanup($importcode);
-                                    echo $OUTPUT->notification(get_string('badgrade', 'grades'));
-                                    break 3;
-                                }
-                            }
-                            $newgrade->finalgrade = $value;
-                        }
-                        $newgrades[] = $newgrade;
-                    } // otherwise, we ignore this column altogether
-                      // because user has chosen to ignore them (e.g. institution, address etc)
-                break;
-            }
-        }
-
-        // no user mapping supplied at all, or user mapping failed
-        if (empty($studentid) || !is_numeric($studentid)) {
-            // user not found, abort whole import
-            $status = false;
-            import_cleanup($importcode);
-            echo $OUTPUT->notification(get_string('usermappingerrorusernotfound', 'grades'));
-            break;
-        }
-
-        if ($separatemode and !groups_is_member($currentgroup, $studentid)) {
-            // not allowed to import into this group, abort
-            $status = false;
-            import_cleanup($importcode);
-            echo $OUTPUT->notification(get_string('usermappingerrorcurrentgroup', 'grades'));
-            break;
-        }
-
-        // insert results of this students into buffer
-        if ($status and !empty($newgrades)) {
-
-            foreach ($newgrades as $newgrade) {
-
-                // check if grade_grade is locked and if so, abort
-                if (!empty($newgrade->itemid) and $grade_grade = new grade_grade(array('itemid'=>$newgrade->itemid, 'userid'=>$studentid))) {
-                    if ($grade_grade->is_locked()) {
-                        // individual grade locked
-                        $status = false;
-                        import_cleanup($importcode);
-                        echo $OUTPUT->notification(get_string('gradelocked', 'grades'));
-                        break 2;
-                    }
-                }
-
-                $newgrade->importcode = $importcode;
-                $newgrade->userid     = $studentid;
-                $newgrade->importer   = $USER->id;
-                $DB->insert_record('grade_import_values', $newgrade);
-            }
-        }
-
-        // updating/inserting all comments here
-        if ($status and !empty($newfeedbacks)) {
-            foreach ($newfeedbacks as $newfeedback) {
-                $sql = "SELECT *
-                          FROM {grade_import_values}
-                         WHERE importcode=? AND userid=? AND itemid=? AND importer=?";
-                if ($feedback = $DB->get_record_sql($sql, array($importcode, $studentid, $newfeedback->itemid, $USER->id))) {
-                    $newfeedback->id = $feedback->id;
-                    $DB->update_record('grade_import_values', $newfeedback);
-
-                } else {
-                    // the grade item for this is not updated
-                    $newfeedback->importcode = $importcode;
-                    $newfeedback->userid     = $studentid;
-                    $newfeedback->importer   = $USER->id;
-                    $DB->insert_record('grade_import_values', $newfeedback);
-                }
-            }
-        }
-    }
-
-    /// at this stage if things are all ok, we commit the changes from temp table
+    // At this stage if things are all ok, we commit the changes from temp table.
     if ($status) {
         grade_import_commit($course->id, $importcode);
     } else {
-        echo $OUTPUT->notification(get_string('importfailed', 'grades'));
+        $errors = $gradeimport->get_gradebookerrors();
+        $errors[] = get_string('importfailed', 'grades');
+        echo $renderer->errors($errors);
     }
+    echo $OUTPUT->footer();
 } else {
     // If data hasn't been submitted then display the data mapping form.
     $mform2->display();
diff --git a/grade/import/csv/tests/fixtures/phpunit_gradeimport_csv_load_data.php b/grade/import/csv/tests/fixtures/phpunit_gradeimport_csv_load_data.php
new file mode 100644 (file)
index 0000000..ad80e51
--- /dev/null
@@ -0,0 +1,122 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle 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 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle 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 Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+require_once($CFG->dirroot . '/grade/import/csv/classes/load_data.php');
+require_once($CFG->dirroot . '/grade/import/lib.php');
+
+/**
+ * Class to open up private methods in gradeimport_csv_load_data().
+ */
+class phpunit_gradeimport_csv_load_data extends gradeimport_csv_load_data {
+
+    /**
+     * Method to open up the appropriate method for unit testing.
+     *
+     * @param object $record
+     * @param int $studentid
+     */
+    public function test_insert_grade_record($record, $studentid) {
+        $this->importcode = 00001;
+        $this->insert_grade_record($record, $studentid);
+    }
+
+    /**
+     * Method to open up the appropriate method for unit testing.
+     */
+    public function get_importcode() {
+        return $this->importcode;
+    }
+
+    /**
+     * Method to open up the appropriate method for unit testing.
+     *
+     * @param array $header The column headers from the CSV file.
+     * @param int $key Current row identifier.
+     * @param string $value The value for this row (final grade).
+     * @return array new grades that are ready for commiting to the gradebook.
+     */
+    public function test_import_new_grade_item($header, $key, $value) {
+        $this->newgradeitems = null;
+        $this->importcode = 00001;
+        return $this->import_new_grade_item($header, $key, $value);
+    }
+
+    /**
+     * Method to open up the appropriate method for unit testing.
+     *
+     * @param string $value The value, from the csv file, being mapped to identify the user.
+     * @param array $userfields Contains the field and label being mapped from.
+     * @return int Returns the user ID if it exists, otherwise null.
+     */
+    public function test_check_user_exists($value, $userfields) {
+        return $this->check_user_exists($value, $userfields);
+    }
+
+    /**
+     * Method to open up the appropriate method for unit testing.
+     *
+     * @param int $courseid The course ID.
+     * @param int $itemid The ID of the grade item that the feedback relates to.
+     * @param string $value The actual feedback being imported.
+     * @return object Creates a feedback object with the item ID and the feedback value.
+     */
+    public function test_create_feedback($courseid, $itemid, $value) {
+        return $this->create_feedback($courseid, $itemid, $value);
+    }
+
+    /**
+     * Method to open up the appropriate method for unit testing.
+     */
+    public function test_update_grade_item($courseid, $map, $key, $verbosescales, $value) {
+        return $this->update_grade_item($courseid, $map, $key, $verbosescales, $value);
+    }
+
+    /**
+     * Method to open up the appropriate method for unit testing.
+     *
+     * @param int $courseid The course ID.
+     * @param array $map Mapping information provided by the user.
+     * @param int $key The line that we are currently working on.
+     * @param bool $verbosescales Form setting for grading with scales.
+     * @param string $value The grade value .
+     * @return array grades to be updated.
+     */
+    public function test_map_user_data_with_value($mappingidentifier, $value, $header, $map, $key, $courseid, $feedbackgradeid,
+            $verbosescales) {
+        // Set an import code.
+        $this->importcode = 00001;
+        $this->map_user_data_with_value($mappingidentifier, $value, $header, $map, $key, $courseid, $feedbackgradeid,
+                $verbosescales);
+
+        switch ($mappingidentifier) {
+            case 'userid':
+            case 'useridnumber':
+            case 'useremail':
+            case 'username':
+                return $this->studentid;
+            break;
+            case 'new':
+                return $this->newgrades;
+            break;
+            case 'feedback':
+                return $this->newfeedbacks;
+            break;
+            default:
+                return $this->newgrades;
+            break;
+        }
+    }
+}
diff --git a/grade/import/csv/tests/load_data_test.php b/grade/import/csv/tests/load_data_test.php
new file mode 100644 (file)
index 0000000..790f889
--- /dev/null
@@ -0,0 +1,430 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle 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 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle 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 Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Unit tests for the class in load_data.php
+ *
+ * @package    gradeimport_csv
+ * @category   phpunit
+ * @copyright  2014 Adrian Greeve
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+require_once($CFG->dirroot . '/grade/import/csv/tests/fixtures/phpunit_gradeimport_csv_load_data.php');
+require_once($CFG->libdir . '/csvlib.class.php');
+require_once($CFG->libdir . '/grade/grade_item.php');
+require_once($CFG->libdir . '/grade/tests/fixtures/lib.php');
+
+/**
+ * Unit tests for lib.php
+ *
+ * @package    gradeimport_csv
+ * @copyright  2014 Adrian Greeve
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class gradeimport_csv_load_data_testcase extends grade_base_testcase {
+
+    /** @var string $oktext Text to be imported. This data should have no issues being imported. */
+    protected $oktext = '"First name",Surname,"ID number",Institution,Department,"Email address","Assignment: Assignment for grape group", "Feedback: Assignment for grape group","Course total"
+Anne,Able,,"Moodle HQ","Rock on!",student7@mail.com,56.00,"We welcome feedback",56.00
+Bobby,Bunce,,"Moodle HQ","Rock on!",student5@mail.com,75.00,,75.00';
+
+    /** @var string $badtext Text to be imported. This data has an extra column and should not succeed in being imported. */
+    protected $badtext = '"First name",Surname,"ID number",Institution,Department,"Email address","Assignment: Assignment for grape group","Course total"
+Anne,Able,,"Moodle HQ","Rock on!",student7@mail.com,56.00,56.00,78.00
+Bobby,Bunce,,"Moodle HQ","Rock on!",student5@mail.com,75.00,75.00';
+
+    /** @var int $iid Import ID. */
+    protected $iid;
+
+    /** @var object $csvimport a csv_import_reader object that handles the csv import. */
+    protected $csvimport;
+
+    /** @var array $columns The first row of the csv file. These are the columns of the import file.*/
+    protected $columns;
+
+    /**
+     * Load up the above text through the csv import.
+     *
+     * @param string $content Text to be imported into the gradebook.
+     * @return array All text separated by commas now in an array.
+     */
+    protected function csv_load($content) {
+        // Import the csv strings.
+        $this->iid = csv_import_reader::get_new_iid('grade');
+        $this->csvimport = new csv_import_reader($this->iid, 'grade');
+
+        $this->csvimport->load_csv_content($content, 'utf8', 'comma');
+        $this->columns = $this->csvimport->get_columns();
+
+        $this->csvimport->init();
+        while ($line = $this->csvimport->next()) {
+            $testarray[] = $line;
+        }
+
+        return $testarray;
+    }
+
+    /**
+     * Test loading data and returning preview content.
+     */
+    public function test_load_csv_content() {
+        $encoding = 'utf8';
+        $separator = 'comma';
+        $previewrows = 5;
+        $csvpreview = new phpunit_gradeimport_csv_load_data();
+        $csvpreview->load_csv_content($this->oktext, $encoding, $separator, $previewrows);
+
+        $expecteddata = array(array(
+                'Anne',
+                'Able',
+                '',
+                'Moodle HQ',
+                'Rock on!',
+                'student7@mail.com',
+                56.00,
+                'We welcome feedback',
+                56.00
+            ),
+            array(
+                'Bobby',
+                'Bunce',
+                '',
+                'Moodle HQ',
+                'Rock on!',
+                'student5@mail.com',
+                75.00,
+                '',
+                75.00
+            )
+        );
+
+        $expectedheaders = array(
+            'First name',
+            'Surname',
+            'ID number',
+            'Institution',
+            'Department',
+            'Email address',
+            'Assignment: Assignment for grape group',
+            'Feedback: Assignment for grape group',
+            'Course total'
+        );
+        // Check that general data is returned as expected.
+        $this->assertEquals($csvpreview->get_previewdata(), $expecteddata);
+        // Check that headers are returned as expected.
+        $this->assertEquals($csvpreview->get_headers(), $expectedheaders);
+
+        // Check that errors are being recorded.
+        $csvpreview = new phpunit_gradeimport_csv_load_data();
+        $csvpreview->load_csv_content($this->badtext, $encoding, $separator, $previewrows);
+        // Columns shouldn't match.
+        $this->assertEquals($csvpreview->get_error(), get_string('csvweirdcolumns', 'error'));
+    }
+
+    /**
+     * Test fetching grade items for the course.
+     */
+    public function test_fetch_grade_items() {
+
+        $gradeitemsarray = grade_item::fetch_all(array('courseid' => $this->courseid));
+        $gradeitems = phpunit_gradeimport_csv_load_data::fetch_grade_items($this->courseid);
+
+        // Make sure that each grade item is located in the gradeitemsarray.
+        foreach ($gradeitems as $key => $gradeitem) {
+            $this->assertArrayHasKey($key, $gradeitemsarray);
+        }
+
+        // Get the key for a specific grade item.
+        $quizkey = null;
+        foreach ($gradeitemsarray as $key => $value) {
+            if ($value->itemname == "Quiz grade item") {
+                $quizkey = $key;
+            }
+        }
+
+        // Expected modified item name.
+        $testitemname = get_string('modulename', $gradeitemsarray[$quizkey]->itemmodule) . ': ' .
+                $gradeitemsarray[$quizkey]->itemname;
+        // Check that an item that is a module, is concatenated properly.
+        $this->assertEquals($testitemname, $gradeitems[$quizkey]);
+    }
+
+    /**
+     * Test the inserting of grade record data.
+     */
+    public function test_insert_grade_record() {
+        global $DB, $USER;
+
+        $user = $this->getDataGenerator()->create_user();
+        $this->setAdminUser();
+
+        $record = new stdClass();
+        $record->itemid = 4;
+        $record->newgradeitem = 25;
+        $record->finalgrade = 62.00;
+        $record->feedback = 'Some test feedback';
+
+        $testobject = new phpunit_gradeimport_csv_load_data();
+        $testobject->test_insert_grade_record($record, $user->id);
+
+        $gradeimportvalues = $DB->get_records('grade_import_values');
+        // Get the insert id.
+        $key = key($gradeimportvalues);
+
+        $testarray = array();
+        $testarray[$key] = new stdClass();
+        $testarray[$key]->id = $key;
+        $testarray[$key]->itemid = $record->itemid;
+        $testarray[$key]->newgradeitem = $record->newgradeitem;
+        $testarray[$key]->userid = $user->id;
+        $testarray[$key]->finalgrade = $record->finalgrade;
+        $testarray[$key]->feedback = $record->feedback;
+        $testarray[$key]->importcode = $testobject->get_importcode();
+        $testarray[$key]->importer = $USER->id;
+
+        // Check that the record was inserted into the database.
+        $this->assertEquals($gradeimportvalues, $testarray);
+    }
+
+    /**
+     * Test preparing a new grade item for import into the gradebook.
+     */
+    public function test_import_new_grade_item() {
+        global $DB;
+
+        $this->setAdminUser();
+        $this->csv_load($this->oktext);
+        $columns = $this->columns;
+
+        // The assignment is item 6.
+        $key = 6;
+        $testobject = new phpunit_gradeimport_csv_load_data();
+
+        // Key for this assessment.
+        $this->csvimport->init();
+        $testarray = array();
+        while ($line = $this->csvimport->next()) {
+            $testarray[] = $testobject->test_import_new_grade_item($columns, $key, $line[$key]);
+        }
+
+        // Query the database and check how many results were inserted.
+        $newgradeimportitems = $DB->get_records('grade_import_newitem');
+        $this->assertEquals(count($testarray), count($newgradeimportitems));
+    }
+
+    /**
+     * Check that the user matches a user in the system.
+     */
+    public function test_check_user_exists() {
+
+        // Need to add one of the users into the system.
+        $user = new stdClass();
+        $user->firstname = 'Anne';
+        $user->lastname = 'Able';
+        $user->email = 'student7@mail.com';
+        $userdetail = $this->getDataGenerator()->create_user($user);
+
+        $testobject = new phpunit_gradeimport_csv_load_data();
+
+        $testarray = $this->csv_load($this->oktext);
+
+        $userfields = array('field' => 'email', 'label' => 'Email address');
+        // If the user exists then the user id is returned.
+        $userid = $testobject->test_check_user_exists($testarray[0][5] , $userfields);
+        // Check that the user id returned matches with the user that we created.
+        $this->assertEquals($userid, $userdetail->id);
+
+        // Check for failure.
+        // Try for an exception.
+        $userfields = array('field' => 'id', 'label' => 'userid');
+        $userid = $testobject->test_check_user_exists($testarray[0][0], $userfields);
+        // Check that the userid is null.
+        $this->assertNull($userid);
+
+        // Expected error message.
+        $mappingobject = new stdClass();
+        $mappingobject->field = $userfields['label'];
+        $mappingobject->value = $testarray[0][0];
+        $expectederrormessage = get_string('usermappingerror', 'grades', $mappingobject);
+        // Check that expected error message and actual message match.
+        $gradebookerrors = $testobject->get_gradebookerrors();
+        $this->assertEquals($expectederrormessage, $gradebookerrors[0]);
+
+        // The field mapping is correct, but the student does not exist.
+        $userid = $testobject->test_check_user_exists($testarray[1][5], $userfields);
+        // Check that the userid is null.
+        $this->assertNull($userid);
+
+        // Expected error message.
+        $mappingobject = new stdClass();
+        $mappingobject->field = $userfields['label'];
+        $mappingobject->value = $testarray[1][5];
+        $expectederrormessage = get_string('usermappingerror', 'grades', $mappingobject);
+        // Check that expected error message and actual message match.
+        $gradebookerrors = $testobject->get_gradebookerrors();
+        // This is the second error in the array of gradebook errors.
+        $this->assertEquals($expectederrormessage, $gradebookerrors[1]);
+    }
+
+    /**
+     * Test preparing feedback for inserting / updating into the gradebook.
+     */
+    public function test_create_feedback() {
+
+        $testarray = $this->csv_load($this->oktext);
+        $testobject = new phpunit_gradeimport_csv_load_data();
+
+        // Try to insert some feedback for an assessment.
+        $feedback = $testobject->test_create_feedback($this->courseid, 1, $testarray[0][7]);
+
+        // Expected result.
+        $expectedfeedback = array('itemid' => 1, 'feedback' => $testarray[0][7]);
+        $this->assertEquals((array)$feedback, $expectedfeedback);
+    }
+
+    /**
+     * Test preparing grade_items for upgrading into the gradebook.
+     */
+    public function test_update_grade_item() {
+
+        $testarray = $this->csv_load($this->oktext);
+        $testobject = new phpunit_gradeimport_csv_load_data();
+
+        // We're not using scales so no to this option.
+        $verbosescales = 0;
+        // Map and key are to retrieve the grade_item that we are updating.
+        $map = array(1);
+        $key = 0;
+        // We return the new grade array for saving.
+        $newgrades = $testobject->test_update_grade_item($this->courseid, $map, $key, $verbosescales, $testarray[0][6]);
+
+        $expectedresult = array();
+        $expectedresult[0] = new stdClass();
+        $expectedresult[0]->itemid = 1;
+        $expectedresult[0]->finalgrade = $testarray[0][6];
+
+        $this->assertEquals($newgrades, $expectedresult);
+
+        // Try sending a bad grade value (A letter instead of a float / int).
+        $newgrades = $testobject->test_update_grade_item($this->courseid, $map, $key, $verbosescales, 'A');
+        // The $newgrades variable should be null.
+        $this->assertNull($newgrades);
+        $expectederrormessage = get_string('badgrade', 'grades');
+        // Check that the error message is what we expect.
+        $gradebookerrors = $testobject->get_gradebookerrors();
+        $this->assertEquals($expectederrormessage, $gradebookerrors[0]);
+    }
+
+    /**
+     * Test importing data and mapping it with items in the course.
+     */
+    public function test_map_user_data_with_value() {
+        // Need to add one of the users into the system.
+        $user = new stdClass();
+        $user->firstname = 'Anne';
+        $user->lastname = 'Able';
+        $user->email = 'student7@mail.com';
+        $userdetail = $this->getDataGenerator()->create_user($user);
+
+        $testarray = $this->csv_load($this->oktext);
+        $testobject = new phpunit_gradeimport_csv_load_data();
+
+        // We're not using scales so no to this option.
+        $verbosescales = 0;
+        // Map and key are to retrieve the grade_item that we are updating.
+        $map = array(1);
+        $key = 0;
+
+        // Test new user mapping. This should return the user id if there were no problems.
+        $userid = $testobject->test_map_user_data_with_value('useremail', $testarray[0][5], $this->columns, $map, $key,
+                $this->courseid, $map[$key], $verbosescales);
+        $this->assertEquals($userid, $userdetail->id);
+
+        $newgrades = $testobject->test_map_user_data_with_value('new', $testarray[0][6], $this->columns, $map, $key,
+                $this->courseid, $map[$key], $verbosescales);
+        // Check that the final grade is the same as the one inserted.
+        $this->assertEquals($testarray[0][6], $newgrades[0]->finalgrade);
+
+        $feedback = $testobject->test_map_user_data_with_value('feedback', $testarray[0][7], $this->columns, $map, $key,
+                $this->courseid, $map[$key], $verbosescales);
+        // Expected result.
+        $resultarray = array();
+        $resultarray[0] = new stdClass();
+        $resultarray[0]->itemid = 1;
+        $resultarray[0]->feedback = $testarray[0][7];
+        $this->assertEquals($feedback, $resultarray);
+
+        // Default behaviour (update a grade item).
+        $newgrades = $testobject->test_map_user_data_with_value('default', $testarray[0][6], $this->columns, $map, $key,
+                $this->courseid, $map[$key], $verbosescales);
+        $this->assertEquals($testarray[0][6], $newgrades[0]->finalgrade);
+    }
+
+    /**
+     * Test importing data into the gradebook.
+     */
+    public function test_prepare_import_grade_data() {
+        global $DB;
+
+        // Need to add one of the users into the system.
+        $user = new stdClass();
+        $user->firstname = 'Anne';
+        $user->lastname = 'Able';
+        $user->email = 'student7@mail.com';
+        // Insert user 1.
+        $this->getDataGenerator()->create_user($user);
+        $user = new stdClass();
+        $user->firstname = 'Bobby';
+        $user->lastname = 'Bunce';
+        $user->email = 'student5@mail.com';
+        // Insert user 2.
+        $this->getDataGenerator()->create_user($user);
+
+        $this->csv_load($this->oktext);
+
+        $importcode = 007;
+        $verbosescales = 0;
+
+        // Form data object.
+        $formdata = new stdClass();
+        $formdata->mapfrom = 5;
+        $formdata->mapto = 'useremail';
+        $formdata->mapping_0 = 0;
+        $formdata->mapping_1 = 0;
+        $formdata->mapping_2 = 0;
+        $formdata->mapping_3 = 0;
+        $formdata->mapping_4 = 0;
+        $formdata->mapping_5 = 0;
+        $formdata->mapping_6 = 'new';
+        $formdata->mapping_7 = 'feedback_2';
+        $formdata->mapping_8 = 0;
+        $formdata->map = 1;
+        $formdata->id = 2;
+        $formdata->iid = $this->iid;
+        $formdata->importcode = $importcode;
+
+        // Blam go time.
+        $testobject = new phpunit_gradeimport_csv_load_data();
+        $dataloaded = $testobject->prepare_import_grade_data($this->columns, $formdata, $this->csvimport, $this->courseid, '', '',
+                $verbosescales);
+        // If everything inserted properly then this should be true.
+        $this->assertTrue($dataloaded);
+    }
+}
index a840138..2db93cc 100644 (file)
@@ -25,6 +25,6 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$plugin->version   = 2014051200;        // The current plugin version (Date: YYYYMMDDXX)
+$plugin->version   = 2014093000;        // The current plugin version (Date: YYYYMMDDXX)
 $plugin->requires  = 2014050800;        // Requires this Moodle version
 $plugin->component = 'gradeimport_csv'; // Full name of the plugin (used for diagnostics)
diff --git a/grade/import/direct/classes/import_form.php b/grade/import/direct/classes/import_form.php
new file mode 100644 (file)
index 0000000..bab48ca
--- /dev/null
@@ -0,0 +1,71 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle 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 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle 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 Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+require_once($CFG->libdir.'/formslib.php');
+
+if (!defined('MOODLE_INTERNAL')) {
+    die('Direct access to this script is forbidden.');    // It must be included from a Moodle page.
+}
+
+/**
+ * Form for copying and pasting from a spreadsheet.
+ *
+ * @package   gradeimport_direct
+ * @copyright 2014 Adrian Greeve <adrian@moodle.com>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class gradeimport_direct_import_form extends moodleform {
+
+    /**
+     * Definition method.
+     */
+    public function definition() {
+        global $COURSE;
+
+        $mform = $this->_form;
+
+        if (isset($this->_customdata)) {  // Hardcoding plugin names here is hacky.
+            $features = $this->_customdata;
+        } else {
+            $features = array();
+        }
+
+        // Course id needs to be passed for auth purposes.
+        $mform->addElement('hidden', 'id', optional_param('id', 0, PARAM_INT));
+        $mform->setType('id', PARAM_INT);
+
+        $mform->addElement('header', 'general', get_string('pluginname', 'gradeimport_direct'));
+        // Data upload from copy/paste.
+        $mform->addElement('textarea', 'userdata', 'Data', array('rows' => 10, 'class' => 'gradeimport_data_area'));
+        $mform->addRule('userdata', null, 'required');
+        $mform->setType('userdata', PARAM_RAW);
+
+        $encodings = core_text::get_encodings();
+        $mform->addElement('select', 'encoding', get_string('encoding', 'grades'), $encodings);
+
+        if (!empty($features['verbosescales'])) {
+            $options = array(1 => get_string('yes'), 0 => get_string('no'));
+            $mform->addElement('select', 'verbosescales', get_string('verbosescales', 'grades'), $options);
+        }
+
+        $options = array('10' => 10, '20' => 20, '100' => 100, '1000' => 1000, '100000' => 100000);
+        $mform->addElement('select', 'previewrows', get_string('rowpreviewnum', 'grades'), $options);
+        $mform->setType('previewrows', PARAM_INT);
+        $mform->addElement('hidden', 'groupid', groups_get_course_group($COURSE));
+        $mform->setType('groupid', PARAM_INT);
+        $this->add_action_buttons(false, get_string('uploadgrades', 'grades'));
+    }
+}
diff --git a/grade/import/direct/classes/mapping_form.php b/grade/import/direct/classes/mapping_form.php
new file mode 100644 (file)
index 0000000..5dc7c56
--- /dev/null
@@ -0,0 +1,111 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle 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 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle 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 Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+require_once($CFG->libdir.'/formslib.php');
+require_once($CFG->libdir.'/gradelib.php');
+
+if (!defined('MOODLE_INTERNAL')) {
+    die('Direct access to this script is forbidden.');    // It must be included from a Moodle page.
+}
+
+/**
+ * Form for mapping columns to the fields in the table.
+ *
+ * @package   gradeimport_direct
+ * @copyright 2014 Adrian Greeve <adrian@moodle.com>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class gradeimport_direct_mapping_form extends moodleform {
+
+    /**
+     * Definition method.
+     */
+    public function definition() {
+        global $CFG, $COURSE;
+        $mform = $this->_form;
+
+        // This is an array of headers.
+        $header = $this->_customdata['header'];
+        // Course id.
+
+        $mform->addElement('header', 'general', get_string('identifier', 'grades'));
+        $mapfromoptions = array();
+
+        if ($header) {
+            foreach ($header as $i => $h) {
+                $mapfromoptions[$i] = s($h);
+            }
+        }
+        $mform->addElement('select', 'mapfrom', get_string('mapfrom', 'grades'), $mapfromoptions);
+
+        $maptooptions = array(
+            'userid'       => get_string('userid', 'grades'),
+            'username'     => get_string('username'),
+            'useridnumber' => get_string('idnumber'),
+            'useremail'    => get_string('email'),
+            '0'            => get_string('ignore', 'grades')
+        );
+        $mform->addElement('select', 'mapto', get_string('mapto', 'grades'), $maptooptions);
+
+        $mform->addElement('header', 'general', get_string('mappings', 'grades'));
+
+        // Add a feedback option.
+        $feedbacks = array();
+        if ($gradeitems = $this->_customdata['gradeitems']) {
+            foreach ($gradeitems as $itemid => $itemname) {
+                $feedbacks['feedback_'.$itemid] = get_string('feedbackforgradeitems', 'grades', $itemname);
+            }
+        }
+
+        if ($header) {
+            $i = 0;
+            foreach ($header as $h) {
+                $h = trim($h);
+                // This is what each header maps to.
+                $headermapsto = array(
+                    get_string('others', 'grades') => array(
+                        '0'   => get_string('ignore', 'grades'),
+                        'new' => get_string('newitem', 'grades')
+                    ),
+                    get_string('gradeitems', 'grades') => $gradeitems,
+                    get_string('feedbacks', 'grades')  => $feedbacks
+                );
+                $mform->addElement('selectgroups', 'mapping_'.$i, s($h), $headermapsto);
+                $i++;
+            }
+        }
+        // Course id needs to be passed for auth purposes.
+        $mform->addElement('hidden', 'map', 1);
+        $mform->setType('map', PARAM_INT);
+        $mform->setConstant('map', 1);
+        $mform->addElement('hidden', 'id', $this->_customdata['id']);
+        $mform->setType('id', PARAM_INT);
+        $mform->setConstant('id', $this->_customdata['id']);
+        $mform->addElement('hidden', 'iid', $this->_customdata['iid']);
+        $mform->setType('iid', PARAM_INT);
+        $mform->setConstant('iid', $this->_customdata['iid']);
+        $mform->addElement('hidden', 'importcode', $this->_customdata['importcode']);
+        $mform->setType('importcode', PARAM_FILE);
+        $mform->setConstant('importcode', $this->_customdata['importcode']);
+        $mform->addElement('hidden', 'verbosescales', 1);
+        $mform->setType('verbosescales', PARAM_INT);
+        $mform->setConstant('verbosescales', $this->_customdata['importcode']);
+        $mform->addElement('hidden', 'groupid', groups_get_course_group($COURSE));
+        $mform->setType('groupid', PARAM_INT);
+        $mform->setConstant('groupid', groups_get_course_group($COURSE));
+        $this->add_action_buttons(false, get_string('uploadgrades', 'grades'));
+    }
+}
diff --git a/grade/import/direct/db/access.php b/grade/import/direct/db/access.php
new file mode 100644 (file)
index 0000000..c45f006
--- /dev/null
@@ -0,0 +1,37 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle 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 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle 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 Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Capabilities gradeimport plugin.
+ *
+ * @package    gradeimport_direct
+ * @copyright  2014 Adrian Greeve <adrian@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+$capabilities = array(
+
+    'gradeimport/direct:view' => array(
+        'captype' => 'write',
+        'contextlevel' => CONTEXT_COURSE,
+        'archetypes' => array(
+            'editingteacher' => CAP_ALLOW,
+            'manager' => CAP_ALLOW
+        )
+    )
+);
\ No newline at end of file
diff --git a/grade/import/direct/index.php b/grade/import/direct/index.php
new file mode 100644 (file)
index 0000000..1a9fc67
--- /dev/null
@@ -0,0 +1,124 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle 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 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle 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 Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+require_once(__DIR__ . "../../../../config.php");
+require_once($CFG->libdir.'/gradelib.php');
+require_once($CFG->dirroot.'/grade/lib.php');
+require_once($CFG->dirroot.'/grade/import/lib.php');
+require_once($CFG->libdir . '/csvlib.class.php');
+
+$id            = required_param('id', PARAM_INT); // Course id.
+$verbosescales = optional_param('verbosescales', 1, PARAM_BOOL);
+$iid           = optional_param('iid', null, PARAM_INT);
+$importcode    = optional_param('importcode', '', PARAM_FILE);
+
+$url = new moodle_url('/grade/import/direct/index.php', array('id' => $id));
+
+if ($verbosescales !== 1) {
+    $url->param('verbosescales', $verbosescales);
+}
+
+$PAGE->set_url($url);
+
+if (!$course = $DB->get_record('course', array('id' => $id))) {
+    print_error('nocourseid');
+}
+
+require_login($course);
+$context = context_course::instance($id);
+require_capability('moodle/grade:import', $context);
+require_capability('gradeimport/direct:view', $context);
+
+$separatemode = (groups_get_course_groupmode($COURSE) == SEPARATEGROUPS and
+        !has_capability('moodle/site:accessallgroups', $context));
+$currentgroup = groups_get_course_group($course);
+
+print_grade_page_head($course->id, 'import', 'direct', get_string('pluginname', 'gradeimport_direct'), false, false, true,
+        'userdata', 'gradeimport_direct');
+
+$renderer = $PAGE->get_renderer('gradeimport_csv');
+
+// Get the grade items to be matched with the import mapping columns.
+$gradeitems = gradeimport_csv_load_data::fetch_grade_items($course->id);
+
+// If the csv file hasn't been imported yet then look for a form submission or
+// show the initial submission form.
+if (!$iid) {
+
+    // Set up the import form.
+    $mform = new gradeimport_direct_import_form(null, array('includeseparator' => true, 'verbosescales' => true, 'acceptedtypes' =>
+        array('.csv', '.txt')));
+
+    // If the import form has been submitted.
+    if ($formdata = $mform->get_data()) {
+        $text = $formdata->userdata;
+        $csvimport = new gradeimport_csv_load_data();
+        $csvimport->load_csv_content($text, $formdata->encoding, 'tab', $formdata->previewrows);
+        $csvimporterror = $csvimport->get_error();
+        if (!empty($csvimporterror)) {
+            echo $renderer->errors($csvimport->get_error());
+            echo $OUTPUT->footer();
+            die();
+        }
+        $iid = $csvimport->get_iid();
+        echo $renderer->import_preview_page($csvimport->get_headers(), $csvimport->get_previewdata());
+    } else {
+        // Display the standard upload file form.
+        echo $renderer->standard_upload_file_form($course, $mform);
+        echo $OUTPUT->footer();
+        die();
+    }
+}
+
+// Data has already been submitted so we can use the $iid to retrieve it.
+$csvimport = new csv_import_reader($iid, 'grade');
+$header = $csvimport->get_columns();
+// Get a new import code for updating to the grade book.
+if (empty($importcode)) {
+    $importcode = get_new_importcode();
+}
+
+$mappingformdata = array(
+    'gradeitems' => $gradeitems,
+    'header' => $header,
+    'iid' => $iid,
+    'id' => $id,
+    'importcode' => $importcode,
+    'verbosescales' => $verbosescales
+);
+// We create a form to handle mapping data from the file to the database.
+$mform2 = new gradeimport_direct_mapping_form(null, $mappingformdata);
+
+// Here, if we have data, we process the fields and enter the information into the database.
+if ($formdata = $mform2->get_data()) {
+    $gradeimport = new gradeimport_csv_load_data();
+    $status = $gradeimport->prepare_import_grade_data($header, $formdata, $csvimport, $course->id, $separatemode, $currentgroup,
+            $verbosescales);
+
+    // At this stage if things are all ok, we commit the changes from temp table.
+    if ($status) {
+        grade_import_commit($course->id, $importcode);
+    } else {
+        $errors = $gradeimport->get_gradebookerrors();
+        $errors[] = get_string('importfailed', 'grades');
+        echo $renderer->errors($errors);
+    }
+    echo $OUTPUT->footer();
+} else {
+    // If data hasn't been submitted then display the data mapping form.
+    $mform2->display();
+    echo $OUTPUT->footer();
+}
\ No newline at end of file
diff --git a/grade/import/direct/lang/en/gradeimport_direct.php b/grade/import/direct/lang/en/gradeimport_direct.php
new file mode 100644 (file)
index 0000000..1218dc0
--- /dev/null
@@ -0,0 +1,29 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle 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 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle 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 Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Strings for component 'gradeimport_direct', language 'en', branch 'MOODLE_28_STABLE'
+ *
+ * @package   gradeimport_direct
+ * @copyright 2014 Adrian Greeve <adrian@moodle.com>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+$string['direct:view'] = 'Import grades from CSV';
+$string['pluginname'] = 'Paste from spreadsheet';
+$string['userdata'] = 'Help copying data into this form.';
+$string['userdata_help'] = 'Grades may be copied and pasted from a spreadsheet into the gradebook. The spreadsheet should have a column containing user identity data - either username or ID number or email address. Each column for import should have a column header.';
+$string['userdata_link'] = 'grade/import/direct/index';
diff --git a/grade/import/direct/styles.css b/grade/import/direct/styles.css
new file mode 100644 (file)
index 0000000..a5b5a55
--- /dev/null
@@ -0,0 +1,5 @@
+.gradeimport_data_area {
+    margin: 0px 0px 10px;
+    width: 475px;
+    height: 209px;
+}
\ No newline at end of file
diff --git a/grade/import/direct/version.php b/grade/import/direct/version.php
new file mode 100644 (file)
index 0000000..023e95e
--- /dev/null
@@ -0,0 +1,30 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle 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 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle 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 Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Version details
+ *
+ * @package    gradeimport_direct
+ * @copyright  2014 Adrian Greeve <adrian@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+$plugin->version   = 2014080400;        // The current plugin version (Date: YYYYMMDDXX)
+$plugin->requires  = 2014050800;        // Requires this Moodle version
+$plugin->component = 'gradeimport_direct'; // Full name of the plugin (used for diagnostics).
+$plugin->dependencies = array('gradeimport_csv' => 2014093000); // Grade import csv is required for this plugin.
\ No newline at end of file
index 1aa5e7b..1bc546e 100644 (file)
@@ -136,16 +136,22 @@ class grade_import_mapping_form extends moodleform {
         // course id needs to be passed for auth purposes
         $mform->addElement('hidden', 'map', 1);
         $mform->setType('map', PARAM_INT);
-        $mform->addElement('hidden', 'id');
+        $mform->setConstant('map', 1);
+        $mform->addElement('hidden', 'id', $this->_customdata['id']);
         $mform->setType('id', PARAM_INT);
-        $mform->addElement('hidden', 'iid');
+        $mform->setConstant('id', $this->_customdata['id']);
+        $mform->addElement('hidden', 'iid', $this->_customdata['iid']);
         $mform->setType('iid', PARAM_INT);
-        $mform->addElement('hidden', 'importcode');
+        $mform->setConstant('iid', $this->_customdata['iid']);
+        $mform->addElement('hidden', 'importcode', $this->_customdata['importcode']);
         $mform->setType('importcode', PARAM_FILE);
+        $mform->setConstant('importcode', $this->_customdata['importcode']);
         $mform->addElement('hidden', 'verbosescales', 1);
         $mform->setType('verbosescales', PARAM_INT);
+        $mform->setConstant('verbosescales', $this->_customdata['verbosescales']);
         $mform->addElement('hidden', 'groupid', groups_get_course_group($COURSE));
         $mform->setType('groupid', PARAM_INT);
+        $mform->setConstant('groupid', groups_get_course_group($COURSE));
         $this->add_action_buttons(false, get_string('uploadgrades', 'grades'));
 
     }
index 643bdb9..50b1c55 100644 (file)
@@ -651,6 +651,10 @@ function grade_get_plugin_info($courseid, $active_type, $active_plugin) {
         $plugin_info['report'] = $reports;
     }
 
+    if ($settings = grade_helper::get_info_manage_settings($courseid)) {
+        $plugin_info['settings'] = $settings;
+    }
+
     if ($scale = grade_helper::get_info_scales($courseid)) {
         $plugin_info['scale'] = array('view'=>$scale);
     }
@@ -685,16 +689,6 @@ function grade_get_plugin_info($courseid, $active_type, $active_plugin) {
         }
     }
 
-    // Hide course settings if we're not in a course
-    if ($settings = grade_helper::get_info_manage_settings($courseid)) {
-        $plugin_info['settings'] = $settings;
-    }
-
-    // Put preferences last
-    if ($preferences = grade_helper::get_plugins_report_preferences($courseid)) {
-        $plugin_info['preferences'] = $preferences;
-    }
-
     foreach ($plugin_info as $plugin_type => $plugins) {
         if (!empty($plugins->id) && $active_plugin == $plugins->id) {
             $plugin_info['strings']['active_plugin_str'] = $plugins->string;
@@ -780,14 +774,21 @@ class grade_plugin_info {
  * @param string  $bodytags Additional attributes that will be added to the <body> tag
  * @param string  $buttons Additional buttons to display on the page
  * @param boolean $shownavigation should the gradebook navigation drop down (or tabs) be shown?
+ * @param string  $headerhelpidentifier The help string identifier if required.
+ * @param string  $headerhelpcomponent The component for the help string.
  *
  * @return string HTML code or nothing if $return == false
  */
 function print_grade_page_head($courseid, $active_type, $active_plugin=null,
                                $heading = false, $return=false,
-                               $buttons=false, $shownavigation=true) {
+                               $buttons=false, $shownavigation=true, $headerhelpidentifier = null, $headerhelpcomponent = null) {
     global $CFG, $OUTPUT, $PAGE;
 
+    if ($active_type === 'preferences') {
+        // In Moodle 2.8 report preferences were moved under 'settings'. Allow backward compatibility for 3rd party grade reports.
+        $active_type = 'settings';
+    }
+
     $plugin_info = grade_get_plugin_info($courseid, $active_type, $active_plugin);
 
     // Determine the string of the active plugin
@@ -816,6 +817,7 @@ function print_grade_page_head($courseid, $active_type, $active_plugin=null,
     }
 
     $returnval = $OUTPUT->header();
+
     if (!$return) {
         echo $returnval;
     }
@@ -831,10 +833,18 @@ function print_grade_page_head($courseid, $active_type, $active_plugin=null,
             $returnval .= print_grade_plugin_selector($plugin_info, $active_type, $active_plugin, $return);
         }
 
+        $output = '';
+        // Add a help dialogue box if provided.
+        if (isset($headerhelpidentifier)) {
+            $output = $OUTPUT->heading_with_help($heading, $headerhelpidentifier, $headerhelpcomponent);
+        } else {
+            $output = $OUTPUT->heading($heading);
+        }
+
         if ($return) {
-            $returnval .= $OUTPUT->heading($heading);
+            $returnval .= $output;
         } else {
-            echo $OUTPUT->heading($heading);
+            echo $output;
         }
 
         if ($courseid != SITEID &&
@@ -2405,6 +2415,13 @@ function grade_extend_settings($plugininfo, $courseid) {
         }
     }
 
+    if ($settings = grade_helper::get_info_manage_settings($courseid)) {
+        $settingsnode = $gradenode->add($strings['settings'], null, navigation_node::TYPE_CONTAINER);
+        foreach ($settings as $setting) {
+            $settingsnode->add($setting->string, $setting->link, navigation_node::TYPE_SETTING, null, $setting->id, new pix_icon('i/settings', ''));
+        }
+    }
+
     if ($imports = grade_helper::get_plugins_import($courseid)) {
         $importnode = $gradenode->add($strings['import'], null, navigation_node::TYPE_CONTAINER);
         foreach ($imports as $import) {
@@ -2419,20 +2436,6 @@ function grade_extend_settings($plugininfo, $courseid) {
         }
     }
 
-    if ($settings = grade_helper::get_info_manage_settings($courseid)) {
-        $settingsnode = $gradenode->add($strings['settings'], null, navigation_node::TYPE_CONTAINER);
-        foreach ($settings as $setting) {
-            $settingsnode->add($setting->string, $setting->link, navigation_node::TYPE_SETTING, null, $setting->id, new pix_icon('i/settings', ''));
-        }
-    }
-
-    if ($preferences = grade_helper::get_plugins_report_preferences($courseid)) {
-        $preferencesnode = $gradenode->add(get_string('myreportpreferences', 'grades'), null, navigation_node::TYPE_CONTAINER);
-        foreach ($preferences as $preference) {
-            $preferencesnode->add($preference->string, $preference->link, navigation_node::TYPE_SETTING, null, $preference->id, new pix_icon('i/settings', ''));
-        }
-    }
-
     if ($letters = grade_helper::get_info_letters($courseid)) {
         $letters = array_shift($letters);
         $gradenode->add($strings['letter'], $letters->link, navigation_node::TYPE_SETTING, null, $letters->id, new pix_icon('i/settings', ''));
@@ -2545,8 +2548,7 @@ abstract class grade_helper {
                 'letter' => get_string('letters', 'grades'),
                 'export' => get_string('export', 'grades'),
                 'import' => get_string('import'),
-                'preferences' => get_string('mypreferences', 'grades'),
-                'settings' => get_string('settings')
+                'settings' => get_string('edittree', 'grades')
             );
         }
         return self::$pluginstrings;
@@ -2587,12 +2589,18 @@ abstract class grade_helper {
         $context = context_course::instance($courseid);
         self::$managesetting = array();
         if ($courseid != SITEID && has_capability('moodle/grade:manage', $context)) {
+            self::$managesetting['categoriesanditems'] = new grade_plugin_info('setup',
+                new moodle_url('/grade/edit/tree/index.php', array('id' => $courseid)),
+                get_string('categoriesanditems', 'grades'));
             self::$managesetting['coursesettings'] = new grade_plugin_info('coursesettings',
                 new moodle_url('/grade/edit/settings/index.php', array('id'=>$courseid)),
                 get_string('coursegradesettings', 'grades'));
-            self::$managesetting['setup'] = new grade_plugin_info('setup',
-                new moodle_url('/grade/edit/tree/index.php', array('id' => $courseid)),
-                get_string('setupgradeslayout', 'grades'));
+        }
+        if (self::$gradereportpreferences === null) {
+            self::get_plugins_reports($courseid);
+        }
+        if (self::$gradereportpreferences) {
+            self::$managesetting = array_merge(self::$managesetting, self::$gradereportpreferences);
         }
         return self::$managesetting;
     }
@@ -2629,7 +2637,8 @@ abstract class grade_helper {
             // Add link to preferences tab if such a page exists
             if (file_exists($plugindir.'/preferences.php')) {
                 $url = new moodle_url('/grade/report/'.$plugin.'/preferences.php', array('id'=>$courseid));
-                $gradepreferences[$plugin] = new grade_plugin_info($plugin, $url, $pluginstr);
+                $gradepreferences[$plugin] = new grade_plugin_info($plugin, $url,
+                    get_string('mypreferences', 'grades') . ': ' . $pluginstr);
             }
         }
         if (count($gradereports) == 0) {
@@ -2646,19 +2655,7 @@ abstract class grade_helper {
         self::$gradereportpreferences = $gradepreferences;
         return self::$gradereports;
     }
-    /**
-     * Returns an array of grade plugin report preferences for plugin reports that
-     * support preferences
-     * @param int $courseid
-     * @return array
-     */
-    public static function get_plugins_report_preferences($courseid) {
-        if (self::$gradereportpreferences !== null) {
-            return self::$gradereportpreferences;
-        }
-        self::get_plugins_reports($courseid);
-        return self::$gradereportpreferences;
-    }
+
     /**
      * Get information on scales
      * @param int $courseid
index d49e265..f0c7188 100644 (file)
@@ -120,8 +120,6 @@ switch ($action) {
             } else {
                 $json_object->gradevalue = $finalvalue;
 
-                $old_grade_grade = new grade_grade(array('userid' => $userid, 'itemid' => $grade_item->id), true);
-
                 if ($grade_item->update_final_grade($userid, $finalgrade, 'gradebook', $feedback, FORMAT_MOODLE)) {
                     $json_object->result = 'success';
                     $json_object->message = false;
@@ -132,14 +130,6 @@ switch ($action) {
                     die();
                 }
 
-                $grade_grade = new grade_grade(array('userid' => $userid, 'itemid' => $grade_item->id), true);
-                if ($old_grade_grade->finalgrade != $grade_grade->finalgrade
-                    or empty($old_grade_grade->overridden) != empty($grade_grade->overridden)
-                ) {
-                    $grade_grade->load_grade_item();
-                    \core\event\user_graded::create_from_grade($grade_grade)->trigger();
-                }
-
                 // Get row data
                 $sql = "SELECT gg.id, gi.id AS itemid, gi.scaleid AS scale, gg.userid AS userid, finalgrade, gg.overridden AS overridden "
                      . "FROM {grade_grades} gg, {grade_items} gi WHERE "
index cc2f423..1174375 100644 (file)
@@ -52,7 +52,7 @@ if (isset($graderreportsilast)) {
 }
 
 $PAGE->set_url(new moodle_url('/grade/report/grader/index.php', array('id'=>$courseid)));
-$PAGE->requires->yui_module('moodle-gradereport_grader-scrollview', 'M.gradereport_grader.scrollview.init');
+$PAGE->requires->yui_module('moodle-gradereport_grader-gradereporttable', 'Y.M.gradereport_grader.init', null, null, true);
 
 // basic access checks
 if (!$course = $DB->get_record('course', array('id' => $courseid))) {
index 7f18997..5286ba3 100644 (file)
@@ -35,3 +35,4 @@ $string['pluginname'] = 'Grader report';
 $string['preferences'] = 'Grader report preferences';
 $string['useractivitygrade'] = '{$a} grade';
 $string['useractivityfeedback'] = '{$a} feedback';
+$string['overriddengrade'] = 'Overridden grade';
index 6aabf4c..f9feea8 100644 (file)
@@ -305,19 +305,8 @@ class grade_report_grader extends grade_report {
                         }
                     }
 
-                    $oldgradegrade = new grade_grade(array('userid' => $userid, 'itemid' => $gradeitem->id), true);
-
                     $gradeitem->update_final_grade($userid, $finalgrade, 'gradebook', $feedback, FORMAT_MOODLE);
 
-                    $gradegrade = new grade_grade(array('userid' => $userid, 'itemid' => $gradeitem->id), true);
-
-                    if ($oldgradegrade->finalgrade != $gradegrade->finalgrade
-                        or empty($oldgradegrade->overridden) != empty($gradegrade->overridden)
-                    ) {
-                        $gradegrade->grade_item = $gradeitem;
-                        \core\event\user_graded::create_from_grade($gradegrade)->trigger();
-                    }
-
                     // We can update feedback without reloading the grade item as it doesn't affect grade calculations
                     if ($datatype === 'feedback') {
                         $this->grades[$userid][$itemid]->feedback = $feedback;
@@ -636,13 +625,10 @@ class grade_report_grader extends grade_report {
 
         $rows = $this->get_left_icons_row($rows, $colspan);
 
-        $rowclasses = array('even', 'odd');
-
         $suspendedstring = null;
         foreach ($this->users as $userid => $user) {
             $userrow = new html_table_row();
             $userrow->id = 'fixed_user_'.$userid;
-            $userrow->attributes['class'] = 'r'.$this->rowcount++.' '.$rowclasses[$this->rowcount % 2];
 
             $usercell = new html_table_cell();
             $usercell->attributes['class'] = 'user';
@@ -655,7 +641,9 @@ class grade_report_grader extends grade_report {
             }
 
             $fullname = fullname($user);
-            $usercell->text .= html_writer::link(new moodle_url('/user/view.php', array('id' => $user->id, 'course' => $this->course->id)), $fullname);
+            $usercell->text .= html_writer::link(new moodle_url('/user/view.php', array('id' => $user->id, 'course' => $this->course->id)), $fullname, array(
+                'class' => 'username',
+            ));
 
             if (!empty($user->suspendedenrolment)) {
                 $usercell->attributes['class'] .= ' usersuspended';
@@ -691,6 +679,7 @@ class grade_report_grader extends grade_report {
                 $userrow->cells[] = $fieldcell;
             }
 
+            $userrow->attributes['data-uid'] = $userid;
             $rows[] = $userrow;
         }
 
@@ -722,7 +711,6 @@ class grade_report_grader extends grade_report {
         $arrows = $this->get_sort_arrows();
 
         $jsarguments = array(
-            'id'        => '#fixed_column',
             'cfg'       => array('ajaxenabled'=>false),
             'items'     => array(),
             'users'     => array(),
@@ -811,6 +799,7 @@ class grade_report_grader extends grade_report {
 
                     $itemcell = new html_table_cell();
                     $itemcell->attributes['class'] = $type . ' ' . $catlevel . ' highlightable'. ' i'. $element['object']->id;
+                    $itemcell->attributes['data-itemid'] = $element['object']->id;
 
                     if ($element['object']->is_hidden()) {
                         $itemcell->attributes['class'] .= ' dimmed_text';
@@ -852,8 +841,6 @@ class grade_report_grader extends grade_report {
         }
         $jsscales = $scalesarray;
 
-        $rowclasses = array('even', 'odd');
-
         foreach ($this->users as $userid => $user) {
 
             if ($this->canviewhidden) {
@@ -868,7 +855,6 @@ class grade_report_grader extends grade_report {
 
             $itemrow = new html_table_row();
             $itemrow->id = 'user_'.$userid;
-            $itemrow->attributes['class'] = $rowclasses[$this->rowcount % 2];
 
             $fullname = fullname($user);
             $jsarguments['users'][$userid] = $fullname;
@@ -880,6 +866,7 @@ class grade_report_grader extends grade_report {
                 $itemcell = new html_table_cell();
 
                 $itemcell->id = 'u'.$userid.'i'.$itemid;
+                $itemcell->attributes['data-itemid'] = $itemid;
 
                 // Get the decimal points preference for this item
                 $decimalpoints = $item->get_decimals();
@@ -928,16 +915,19 @@ class grade_report_grader extends grade_report {
                 }
                 if ($grade->is_overridden()) {
                     $itemcell->attributes['class'] .= ' overridden';
+                    $itemcell->attributes['aria-label'] = get_string('overriddengrade', 'gradereport_grader');
                 }
 
                 if (!empty($grade->feedback)) {
-                    //should we be truncating feedback? ie $short_feedback = shorten_text($feedback, $this->feedback_trunc_length);
-                    $jsarguments['feedback'][] = array('user'=>$userid, 'item'=>$itemid, 'content'=>wordwrap(trim(format_string($grade->feedback, $grade->feedbackformat)),
-                            34, '<br/ >'));
+                    $feedback = wordwrap(trim(format_string($grade->feedback, $grade->feedbackformat)), 34, '<br>');
+                    $itemcell->attributes['data-feedback'] = $feedback;
+                    $jsarguments['feedback'][] = array('user'=>$userid, 'item'=>$itemid, 'content' => $feedback);
                 }
 
                 if ($grade->is_excluded()) {
-                    $itemcell->text .= "<span class='excludedfloater'>" . $strexcludedgrades . "</span>";
+                    // Adding white spaces before and after to prevent a screenreader from
+                    // thinking that the words are attached to the next/previous <span> or text.
+                    $itemcell->text .= " <span class='excludedfloater'>" . $strexcludedgrades . "</span> ";
                 }
 
                 // Do not show any icons if no grade (no record in DB to match)
@@ -1123,36 +1113,21 @@ class grade_report_grader extends grade_report {
      */
     public function get_grade_table($displayaverages = false) {
         global $OUTPUT;
-        $fixedstudents = $this->is_fixed_students();
-
         $leftrows = $this->get_left_rows($displayaverages);
         $rightrows = $this->get_right_rows($displayaverages);
 
         $html = '';
 
-        if ($fixedstudents) {
-            $fixedcolumntable = new html_table();
-            $fixedcolumntable->id = 'fixed_column';
-            $fixedcolumntable->data = $leftrows;
-            $html .= $OUTPUT->container(html_writer::table($fixedcolumntable), 'left_scroller');
+        $fulltable = new html_table();
+        $fulltable->attributes['class'] = 'gradereport-grader-table';
+        $fulltable->id = 'user-grades';
 
-            $righttable = new html_table();
-            $righttable->id = 'user-grades';
-            $righttable->data = $rightrows;
-
-            $html .= $OUTPUT->container(html_writer::table($righttable), 'right_scroller');
-        } else {
-            $fulltable = new html_table();
-            $fulltable->attributes['class'] = 'gradestable flexible boxaligncenter generaltable';
-            $fulltable->id = 'user-grades';
-
-            // Extract rows from each side (left and right) and collate them into one row each
-            foreach ($leftrows as $key => $row) {
-                $row->cells = array_merge($row->cells, $rightrows[$key]->cells);
-                $fulltable->data[] = $row;
-            }
-            $html .= html_writer::table($fulltable);
+        // Extract rows from each side (left and right) and collate them into one row each
+        foreach ($leftrows as $key => $row) {
+            $row->cells = array_merge($row->cells, $rightrows[$key]->cells);
+            $fulltable->data[] = $row;
         }
+        $html .= html_writer::table($fulltable);
         return $OUTPUT->container($html, 'gradeparent');
     }
 
@@ -1748,18 +1723,6 @@ class grade_report_grader extends grade_report {
         return true;
     }
 
-    /**
-     * Returns whether or not to display fixed students column.
-     * Includes a browser check, because IE6 doesn't support the scrollbar.
-     *
-     * @return bool
-     */
-    public function is_fixed_students() {
-        global $CFG;
-
-        return $CFG->grade_report_fixedstudents;
-    }
-
     /**
      * Refactored function for generating HTML of sorting links with matching arrows.
      * Returns an array with 'studentname' and 'idnumber' as keys, with HTML ready
index 6263883..be277ac 100644 (file)
  * Grader report namespace
  */
 M.gradereport_grader = {
-    /**
-     * @param {Array} reports An array of instantiated report objects
-     */
-    reports : [],
     /**
      * @namespace M.gradereport_grader
      * @param {Object} reports A collection of classes used by the grader report module
      */
     classes : {},
-    /**
-     * @param {Object} tooltip Null or a tooltip object
-     */
-    tooltip : null,
     /**
      * Instantiates a new grader report
      *
      * @function
      * @param {YUI} Y
-     * @param {String} id The id attribute of the reports table
      * @param {Object} cfg A configuration object
      * @param {Array} An array of items in the report
      * @param {Array} An array of users on the report
      * @param {Array} An array of feedback objects
      * @param {Array} An array of student grades
      */
-    init_report : function(Y, id, cfg, items, users, feedback, grades) {
-        this.tooltip = this.tooltip || {
-            overlay : null, // Y.Overlay instance
-            /**
-             * Attaches the tooltip event to the provided cell
-             *
-             * @function M.gradereport_grader.tooltip.attach
-             * @this M.gradereport_grader
-             * @param {Y.Node} td The cell to attach the tooltip event to
-             */
-            attach : function(td, report) {
-                td.on('mouseenter', this.show, this, report);
-            },
-            /**
-             * Shows the tooltip: Callback from @see M.gradereport_grader.tooltip#attach
-             *
-             * @function M.gradereport_grader.tooltip.show
-             * @this {M.gradereport_grader.tooltip}
-             * @param {Event} e
-             * @param {M.gradereport_grader.classes.report} report
-             */
-            show : function(e, report) {
-                e.halt();
-
-                var properties = report.get_cell_info(e.target);
-                if (!properties) {
-                    return;
-                }
-
-                var content = '<div class="graderreportoverlay" role="tooltip" aria-describedby="' + properties.id + '">';
-                content += '<div class="fullname">'+properties.username+'</div><div class="itemname">'+properties.itemname+'</div>';
-                if (properties.feedback) {
-                    content += '<div class="feedback">'+properties.feedback+'</div>';
-                }
-                content += '</div>';
-
-                properties.cell.on('mouseleave', this.hide, this, properties.cell);
-                properties.cell.addClass('tooltipactive');
-
-                this.overlay = this.overlay || (function(){
-                    var overlay = new Y.Overlay({
-                        bodyContent : 'Loading',
-                        visible : false,
-                        zIndex : 2
-                    });
-                    overlay.render(report.table.ancestor('div'));
-                    return overlay;
-                })();
-                this.overlay.set('xy', [e.target.getX()+(e.target.get('offsetWidth')/2),e.target.getY()+e.target.get('offsetHeight')-5]);
-                this.overlay.set("bodyContent", content);
-                this.overlay.show();
-                this.overlay.get('boundingBox').setStyle('visibility', 'visible');
-            },
-            /**
-             * Hides the tooltip
-             *
-             * @function M.gradereport_grader.tooltip.hide
-             * @this {M.gradereport_grader.tooltip}
-             * @param {Event} e
-             * @param {Y.Node} cell
-             */
-            hide : function(e, cell) {
-                cell.removeClass('tooltipactive');
-                this.overlay.hide();
-                this.overlay.get('boundingBox').setStyle('visibility', 'hidden');
-            }
-        };
+    init_report : function(Y, cfg, items, users, feedback, grades) {
         // Create the actual report
-        this.reports[id] = new this.classes.report(Y, id, cfg, items, users, feedback, grades);
+        new this.classes.report(Y, cfg, items, users, feedback, grades);
     }
 };
 
@@ -111,13 +36,12 @@ M.gradereport_grader = {
  * @constructor
  * @this {M.gradereport_grader}
  * @param {YUI} Y
- * @param {int} id The id of the table to attach the report to
  * @param {Object} cfg Configuration variables
  * @param {Array} items An array containing grade items
  * @param {Array} users An array containing user information
  * @param {Array} feedback An array containing feedback information
  */
-M.gradereport_grader.classes.report = function(Y, id, cfg, items, users, feedback, grades) {
+M.gradereport_grader.classes.report = function(Y, cfg, items, users, feedback, grades) {
     this.Y = Y;
     this.isediting = (cfg.isediting);
     this.ajaxenabled = (cfg.ajaxenabled);
@@ -127,37 +51,6 @@ M.gradereport_grader.classes.report = function(Y, id, cfg, items, users, feedbac
     this.table = Y.one('#user-grades');
     this.grades = grades;
 
-    // Alias this so that we can use the correct scope in the coming
-    // node iteration
-    this.table.all('tr').each(function(tr){
-        // Check it is a user row
-        if (tr.getAttribute('id').match(/^(fixed_)?user_(\d+)$/)) {
-            // Highlight rows
-            tr.all('th.cell').on('click', this.table_highlight_row, this, tr);
-            // Display tooltips
-            tr.all('td.cell').each(function(cell){
-                M.gradereport_grader.tooltip.attach(cell, this);
-            }, this);
-        }
-    }, this);
-
-    // If the fixed table exists then map those rows to highlight the
-    // grades table rows
-    var fixed = this.Y.one(id);
-    if (fixed) {
-        fixed.all('tr').each(function(tr) {
-            if (tr.getAttribute('id').match(/^fixed_user_(\d+)$/)) {
-                tr.all('th.cell').on('click', this.table_highlight_row, this, this.Y.one(tr.getAttribute('id').replace(/^fixed_/, '#')));
-            }
-        }, this);
-    }
-
-    // Highlight columns
-    this.table.all('.highlightable').each(function(cell){
-        cell.on('click', this.table_highlight_column, this, cell);
-        cell.removeClass('highlightable');
-    }, this);
-
     // If ajax is enabled then initialise the ajax component
     if (this.ajaxenabled) {
         this.ajax = new M.gradereport_grader.classes.ajax(this, cfg);
@@ -172,31 +65,6 @@ M.gradereport_grader.classes.report.prototype.users = [];             // Array c
 M.gradereport_grader.classes.report.prototype.feedback = [];          // Array containing feedback items
 M.gradereport_grader.classes.report.prototype.ajaxenabled = false;    // True is AJAX is enabled for the report
 M.gradereport_grader.classes.report.prototype.ajax = null;            // An instance of the ajax class or null
-/**
- * Highlights a row in the report
- *
- * @function
- * @param {Event} e
- * @param {Y.Node} tr The table row to highlight
- */
-M.gradereport_grader.classes.report.prototype.table_highlight_row = function (e, tr) {
-    tr.all('.cell').toggleClass('hmarked');
-};
-/**
- * Highlights a column in the table
- *
- * @function
- * @param {Event} e
- * @param {Y.Node} cell
- */
-M.gradereport_grader.classes.report.prototype.table_highlight_column = function(e, cell) {
-    // Among cell classes find the one that matches pattern / i[\d]+ /
-    var itemclass = (' '+cell.getAttribute('class')+' ').match(/ (i[\d]+) /);
-    if (itemclass) {
-        // Toggle class .vmarked for all cells in the table with the same class
-        this.table.all('.cell.'+itemclass[1]).toggleClass('vmarked');
-    }
-};
 /**
  * Builds an object containing information at the relevant cell given either
  * the cell to get information for or an array containing userid and itemid
@@ -234,13 +102,6 @@ M.gradereport_grader.classes.report.prototype.get_cell_info = function(arg) {
         return null;
     }
 
-    for (i in this.feedback) {
-        if (this.feedback[i] && this.feedback[i].user == userid && this.feedback[i].item == itemid) {
-            feedback = this.feedback[i].content;
-            break;
-        }
-    }
-
     return {
         id : cell.getAttribute('id'),
         userid : userid,
@@ -250,7 +111,6 @@ M.gradereport_grader.classes.report.prototype.get_cell_info = function(arg) {
         itemtype : this.items[itemid].type,
         itemscale : this.items[itemid].scale,
         itemdp : this.items[itemid].decimals,
-        feedback : feedback,
         cell : cell
     };
 };
@@ -274,28 +134,6 @@ M.gradereport_grader.classes.report.prototype.update_feedback = function(userid,
     this.feedback.push({user:userid,item:itemid,content:newfeedback});
     return true;
 };
-
-/**
- * Updates or creates the grade JS structure for the given user/item
- *
- * @function
- * @this {M.gradereport_grader}
- * @param {Int} userid
- * @param {Int} itemid
- * @param {String} newgrade
- * @return {Bool}
- */
-/*M.gradereport_grader.classes.report.prototype.update_grade = function(userid, itemid, newgrade) {
-    for (var i in this.grades) {
-        if (this.grades[i].user == userid && this.grades[i].item == itemid) {
-            this.grades[i].content = newgrade;
-            return true;
-        }
-    }
-    this.grades.push({user:userid,item:itemid,content:newgrade});
-    return true;
-};*/
-
 /**
  * Initialises the AJAX component of this report
  * @class ajax
@@ -379,11 +217,8 @@ M.gradereport_grader.classes.ajax.prototype.make_editable = function(e) {
     }
     this.current.replace().attach_key_events();
 
-    // Making a field editable changes the grade table width.
-    // Update the top scroll bar to reflect the new table width.
-    Y.use('moodle-gradereport_grader-scrollview', function() {
-        M.gradereport_grader.scrollview.resize();
-    });
+    // Fire the global resized event for the gradereport_grader to update the table row/column sizes.
+    Y.Global.fire('moodle-gradereport_grader:resized');
 };
 /**
  * Callback function for the user pressing the enter key on an editable field
@@ -460,6 +295,9 @@ M.gradereport_grader.classes.ajax.prototype.process_editable_field = function(ne
     if (next) {
         this.make_editable(next, null);
     }
+
+    // Fire the global resized event for the gradereport_grader to update the table row/column sizes.
+    Y.Global.fire('moodle-gradereport_grader:resized');
 };
 /**
  * Gets the next cell that is editable (right)
index 1fc07f0..029afa4 100644 (file)
@@ -69,7 +69,7 @@ if ($mform->is_cancelled()){
     redirect($CFG->wwwroot . '/grade/report/grader/index.php?id='.$courseid);
 }
 
-print_grade_page_head($courseid, 'preferences', 'grader', get_string('preferences', 'gradereport_grader'));
+print_grade_page_head($courseid, 'settings', 'grader', get_string('preferences', 'gradereport_grader'));
 
 // If USER has admin capability, print a link to the site config page for this report
 if (has_capability('moodle/site:config', $systemcontext)) {
index cb70000..300543a 100644 (file)
@@ -44,9 +44,6 @@ if ($ADMIN->fulltree) {
     $settings->add(new admin_setting_configcheckbox('grade_report_showquickfeedback', get_string('quickfeedback', 'grades'),
                                                 get_string('showquickfeedback_help', 'grades'), 0));
 
-    $settings->add(new admin_setting_configcheckbox('grade_report_fixedstudents', get_string('fixedstudents', 'grades'),
-                                                get_string('fixedstudents_help', 'grades'), 0));
-
     $settings->add(new admin_setting_configselect('grade_report_meanselection', get_string('meanselection', 'grades'),
                                               get_string('meanselection_help', 'grades'), GRADE_REPORT_MEAN_GRADED,
                                               array(GRADE_REPORT_MEAN_ALL => get_string('meanall', 'grades'),
index 671817b..123db0b 100644 (file)
-.path-grade-report-grader .flexible th {
-    white-space: normal;
-}
-.gradestable {
-    margin-bottom: 0;
-}
-.gradestable th.user img {
-    width: 20px;
-    height: 20px;
-}
-.gradestable th img {
-    vertical-align: text-bottom;
-    padding-bottom: 0;
-}
-.gradestable th .grade_icons {
-    margin-top: .3em;
-}
-.gradestable th img.sorticon {
-    margin-left: .3em;
-}
-.dir-rtl .gradestable th img.sorticon {
-    margin-left: 0;
-    margin-right: .3em;
-}
-table#user-grades .catlevel2 {
-    background-color: #f9f9f9;
-}
-table#user-grades tr.range td.cell {
-    font-weight: 700;
-}
-table#user-grades tr.avg td.cell {
-    background-color: #efefff;
-    font-weight: 700;
-    color: #00008B;
-}
-table#user-grades tr.odd td.cell {
-    background-color: #efefef;
-    white-space: nowrap;
-}
-table#user-grades tr td.overridden {
-    background-color: #F3E4C0;
-}
-table#user-grades tr.odd td.overridden {
-    background-color: #EFD9A4;
-}
-table#user-grades tr td.ajaxoverridden {
-    background-color: #FFE3A0;
-}
-table#user-grades tr.odd td.ajaxoverridden {
-    background-color: #FFDA83;
-}
-table#user-grades tr.even td.excluded {
-    background-color: #EABFFF;
-}
-table#user-grades tr.odd td.excluded {
-    background-color: #E5AFFF;
-}
-table#user-grades tr.odd th.header {
-    background-color: #efefef;
-    background-image: none;
-}
-table#user-grades tr.even th.header {
-    background-image: none;
-}
-table#user-grades tr.groupavg td.cell {
-    background-color: #efffef;
-    font-weight: 700;
-    color: #006400;
-}
-table#user-grades td.cat,
-table#user-grades td.course {
-    font-weight: 700;
+/**
+ * Container.
+ */
+.path-grade-report-grader .gradeparent {
+    position: relative;
 }
-table#user-grades {
-    font-size: 10px;
+
+/**
+ * Tooltip and overlay.
+ */
+.path-grade-report-grader .gradeparent .grader-information-tooltip {
+    min-width: 200px;
+}
+.path-grade-report-grader .gradeparent .graderreportoverlay {
+    background-color: white;
     width: auto;
-    background-color: transparent;
-    border-style: solid;
-    border-width: 1px;
-    margin: 0;
-}
-.path-grade-report-grader #overDiv table {
-    margin: 0;
+    padding: 10px;
+    font-size: 12px;
+    border: 1px solid #ccc;
+    border-radius: 4px;
 }
-.path-grade-report-grader #overDiv table td.feedback {
-    border: 0;
-}
-.path-grade-report-grader #overDiv .feedback {
-    font-size: 70%;
-    background-color: #ABF;
-    color: #000;
-    font-family: Verdana;
-    font-weight: 400;
-}
-.path-grade-report-grader #overDiv .caption {
-    font-size: 70%;
-    background-color: #56C;
-    color: #CCF;
-    font-family: Arial;
-    font-weight: 700;
-}
-.path-grade-report-grader #overDiv .intersection {
-    font-size: 70%;
-    background-color: #ABF;
-    color: #000;
-    font-family: Verdana;
-    font-weight: 400;
-}
-.path-grade-report-grader #overDiv .intersectioncaption {
-    background-color: #56C;
-    color: #CCF;
-    font-family: Arial;
-    font-weight: 700;
-}
-.path-grade-report-grader div.submit {
-    margin-top: 20px;
-    text-align: center;
+
+/**
+ * The table.
+ */
+.path-grade-report-grader .gradeparent table {
+    border: 1px solid #ccc;
+    border-collapse: separate;
+    border-spacing: 0;
+    border-bottom-width: 0;
+    border-right-width: 0;
+    margin-bottom: 2em;
+}
+.dir-rtl .path-grade-report-grader .gradeparent table {
+    border-left-width: 0;
+    border-right-width: 1px;
 }
-table#user-grades td {
+
+/**
+ * All the cells.
+ */
+.path-grade-report-grader .gradeparent .cell {
+    border: 1px solid #ccc;
+    border-top-width: 0;
+    border-left-width: 0;
+    padding: 4px 5px;
+    vertical-align: middle;
     text-align: right;
-    border-style: solid;
-    border-width: 0 1px 1px 0;
-}
-table#user-grades th.category {
-    vertical-align: top;
-    border-style: solid;
-    border-width: 1px 1px 0;
+    white-space: nowrap;
 }
-table#user-grades th.user {
+.dir-rtl .path-grade-report-grader .gradeparent .cell {
+    border-left-width: 1px;
+    border-right-width: 0;
     text-align: left;
-    border-style: solid;
-    border-width: 1px 0;
 }
-table#user-grades th.userfield {
-    border-style: solid;
-    border-width: 1px;
-}
-table#user-grades th.categoryitem,
-table#user-grades td.topleft {
-    vertical-align: bottom;
-    border-style: solid;
-    border-width: 0 1px;
-}
-.path-grade-report-grader td,.path-grade-report-grader th {
-    border-color: #CECECE;
-}
-.path-grade-report-grader table#participants th {
-    vertical-align: top;
-    width: auto;
-}
-table#user-grades td.fillerfirst {
-    border-style: solid;
-    border-width: 0 0 0 1px;
-}
-table#user-grades td.fillerlast {
-    border-style: solid;
-    border-width: 0 1px 0 0;
-}
-table#user-grades th.item,
-table#user-grades th.categoryitem,
-table#user-grades th.courseitem {
-    border-bottom-color: #000;
-    vertical-align: bottom;
-    border-style: solid;
-    border-width: 1px;
-}
-div.gradertoggle {
-    display: inline;
-    margin-left: 20px;
-}
-table#user-grades th.range {
-    text-align: right;
-    border-style: solid;
-    border-width: 1px;
+
+/**
+ * Stripped table.
+ */
+.path-grade-report-grader .gradeparent tr:nth-of-type(even) .cell {
+    background-color: #f9f9f9;
 }
-table#user-grades .userpic {
-    display: inline;
-    margin-right: 10px;
+
+/**
+ * All the floating divs.
+ */
+.path-grade-report-grader .gradeparent .floater {
+    display: none;
 }
-table#user-grades .quickfeedback {
-    border: 1px dashed #000;
-    width: auto;
-    margin: 0;
-    padding: 0;
-    margin-left: 10px;
+.path-grade-report-grader .gradeparent .floating {
+    display: block;
 }
-.dir-rtl table#user-grades .quickfeedback {
-    margin-left: 0;
-    margin-right: 10px;
+
+/**
+ * All the headers + floating cells.
+ */
+.path-grade-report-grader .gradeparent .heading .cell,
+.path-grade-report-grader .gradeparent .avg .cell,
+.path-grade-report-grader .gradeparent .user.cell {
+    font-size: 14px;
+    font-weight: normal;
+    text-align: left;
 }
-.path-grade-report-grader #siteconfiglink {
+.dir-rtl .path-grade-report-grader .gradeparent .heading .cell,
+.dir-rtl .path-grade-report-grader .gradeparent .avg .cell,
+.dir-rtl .path-grade-report-grader .gradeparent .user.cell {
     text-align: right;
 }
-table#user-grades .datesubmitted {
-    font-size: .7em;
-}
-table#user-grades td.cell {
-    padding-left: 5px;
-    padding-right: 5px;
-    vertical-align: middle;
-}
-.path-grade-report-grader table {
-    border-collapse: collapse;
-    background-color: #fff;
-    border-color: #cecece;
-}
-.path-grade-report-grader th {
-    padding: 1px 10px;
-}
-.path-grade-report-grader span.inclusion-links {
-    margin: 0 5px 0 10px;
-}
-table#user-grades .item {
-    background-color: #e9e9e9;
-}
-.path-grade-report-grader table tr.odd th.header {
-    background-color: #efefef;
-    background-image: none;
-    border-width: 0 0 1px;
-}
-.path-grade-report-grader table tr.heading th.header {
-    border-top: 1px solid #cecece;
-}
-table#user-grades tr.heading th.categoryitem,
-table#user-grades tr.heading th.courseitem {
-    border-width: 0 0 0 1px;
+
+/**
+ * All the floating cells.
+ */
+.path-grade-report-grader .gradeparent .floater .cell {
+    background-color: #f9f9f9;
 }
-table#user-grades th.category.header.catlevel1 {
+
+/**
+ * The user cells.
+ */
+.path-grade-report-grader .gradeparent .user.cell {
+    min-width: 200px;
+    width: 200px;
+    white-space: normal;
     vertical-align: top;
-    border-style: solid;
-    border-width: 1px 1px 0 0;
 }
-.path-grade-report-grader div.left_scroller th.user a {
+.path-grade-report-grader .gradeparent .user.cell .userpicture {
+    margin: 0 4px;
+    border: none;
     vertical-align: middle;
-    margin: 0;
-    padding: 0;
-}
-table#user-grades th.categoryitem,
-table#user-grades th.courseitem,
-.path-grade-report-grader table td.topleft {
-    vertical-align: bottom;
-    border-color: #cecece #cecece #000;
-    border-style: solid;
-    border-width: 0 1px 1px;
-}
-.path-grade-report-grader .left_scroller table td.topleft {
-    background-color: #fff;
-    border-bottom-color: #cecece;
-}
-table#user-grades td.topleft {
-    background-color: #fff;
-}
-.path-grade-report-grader th.user img.userpicture {
-    border: 3px double #cecece;
-    vertical-align: top;
-    width: 2.7em;
-    height: 2.7em;
-    margin-right: 10px;
-}
-.path-grade-report-grader a.quickedit {
-    line-height: 1em;
-    display: block;
-    float: right;
-    clear: none;
-    font-size: 9px;
-    background-color: transparent;
-    margin: .1em 0 0;
-}
-.path-grade-report-grader a.quickedit2 {
-    display: block;
-    float: right;
-    clear: none;
-    background-color: transparent;
-    margin: 1.3em 0 0;
-}
-.path-grade-report-grader table#quick_edit {
-    border: 1px solid #cecece;
-    margin: 0 auto;
 }
-.path-grade-report-grader table#quick_edit td {
-    vertical-align: middle;
-    border: 1px solid #cecece;
+
+/**
+ * The additional user fields.
+ */
+.path-grade-report-grader .gradeparent .userfield {
+    font-weight: normal;
     text-align: left;
-    margin: 0;
-    padding: 5px;
-}
-.path-grade-report-grader table#quick_edit td img {
-    border: 3px double #cecece;
-    vertical-align: middle;
-    padding: 0;
 }
-.path-grade-report-grader td input.text {
-    border: 1px solid #666;
-    width: auto;
-    margin: 0;
-    padding: 0;
-    font-size: 12px;
-    height: 20px;
-    line-height: 20px;
-    text-align: center;
-}
-.path-grade-report-grader td input.submit {
-    margin: 10px 10px 0px 10px;
-}
-.path-grade-report-grader table#quick_edit td.fullname {
-    border-left: 0;
-    padding-left: 5px;
-}
-.path-grade-report-grader table#quick_edit td.picture {
-    border-right: 0;
-}
-.path-grade-report-grader table#quick_edit td.finalgrade input {
-    width: 5em;
+.dir-rtl .path-grade-report-grader .gradeparent .userfield {
+    text-align: right;
 }
-.path-grade-report-grader h1 {
-    text-align: center;
-    clear: both;
+
+/**
+ * The footer's header.
+ */
+.path-grade-report-grader .gradeparent .range .header,
+.path-grade-report-grader .gradeparent .avg .header {
+    font-weight: bold;
 }
-.path-grade-report-grader input.center {
-    margin: 10px auto 0;
+
+/**
+ * The footer's floating cells.
+ */
+.path-grade-report-grader .gradeparent .avg.floating .cell {
+    border-top-width: 1px;
 }
-.path-grade-report-grader .lefttbody {
-    width: auto;
-    vertical-align: middle;
+
+/**
+ * The footer's cells.
+ */
+.path-grade-report-grader .gradeparent .avg .cell {
+    text-align: right;
 }
-table#user-grades th.fixedcolumn {
-    border: 1px solid #cecece;
-    vertical-align: middle;
+.dir-rtl .path-grade-report-grader .gradeparent .avg .cell {
+    text-align: left;
 }
-.path-grade-report-grader table#fixed_column th {
-    border: 1px solid #cecece;
-    vertical-align: middle;
-    border-right-color: #000;
+
+/**
+ * Content styling.
+ */
+.path-grade-report-grader .gradeparent .heading .cell .iconsmall {
+    /* Fixes inconsistencies in cell height on IE. */
+    padding-top: 0;
+    padding-bottom: 0;
 }
-.path-grade-report-grader table#fixed_column th.user{
-    border-right-color: #cecece;
+.path-grade-report-grader .gradeparent .sorticon {
+    margin-left: 3px;
 }
-.path-grade-report-grader table#fixed_column {
-    padding-top: 20px;
-    border-top: 1px solid #cecece;
-    background-color: #fff;
+.dir-rtl .path-grade-report-grader .gradeparent .sorticon {
+    margin-left: 0;
+    margin-right: 3px;
 }
-.path-grade-report-grader .left_scroller {
-    float: left;
-    clear: none;
+.path-grade-report-grader .gradeparent .gradevalue {
+    display: inline-block;
 }
-.path-grade-report-grader.dir-rtl .left_scroller {
-    float: right;
+.path-grade-report-grader .gradeparent tr:nth-child(n) td.overridden:nth-child(n) {
+    /* Made very specific to override the default stripped style of the table. */
+    background-color: #efd9a4;
 }
-.path-grade-report-grader .right_scroller {
-    width: auto;
-    clear: none;
-    overflow-x: scroll;
-    overflow-y: hidden;
-}
-.path-grade-report-grader table tr.avg,
-.path-grade-report-grader table tr.groupavg td,
-.path-grade-report-grader table tr.avg td,
-.path-grade-report-grader table tr.groupavg th,
-.path-grade-report-grader table tr.avg th,
-.path-grade-report-grader table tr.controls_row,
-.path-grade-report-grader table tr.controls_row th,
-.path-grade-report-grader table tr.range_row,
-.path-grade-report-grader table tr.range_row th,
-div.right_scroller tr {
-    height: 2em;
-}
-table#user-grades tr.groupavg td.cell,
-tr.groupavg th.header {
-    background-color: #efffef;
-}
-.path-grade-report-grader form td.excluded {
-    color: red;
+.path-grade-report-grader .gradeparent tr:nth-child(n) td.ajaxoverridden:nth-child(n) {
+    /* Made very specific to override the default stripped style of the table. */
+    background-color: #ffe3a0;
 }
-.path-grade-report-grader .excludedfloater {
-    font-weight: 700;
+.path-grade-report-grader .gradeparent .excludedfloater {
+    font-weight: bold;
     color: red;
     font-size: 9px;
     float: left;
 }
-.path-grade-report-grader span.gradepass {
-    color: #298721;
-}
-.path-grade-report-grader span.gradefail {
-    color: #890d0d;
-}
-.path-grade-report-grader .gradeweight {
-    color: #461d7c;
-    font-weight: 700;
-}
-.path-grade-report-grader td select {
-    font-size: 100%;
-    padding: 0;
-}
-.path-grade-report-grader .right_scroller td select {
-    font-size: 86%;
-    padding: 0;
-}
-.path-grade-report-grader tr.avg,
-.path-grade-report-grader tr.controls,
-.path-grade-report-grader td.controls,
-.path-grade-report-grader th.controls,
-.path-grade-report-grader tr.groupavg,
-.path-grade-report-grader tr.range,
-.path-grade-report-grader th.range,
-.path-grade-report-grader td.range,
-.path-grade-report-grader tr.heading th.range {
-    height: 2em!important;
-    white-space: nowrap;
-}
-.path-grade-report-grader .heading_name_row th {
-    white-space: nowrap;
-    width: 2000px;
-}
-
-/*MDL-21088 - IE 7 ignores nowraps on tds or ths so we put a span within it with a nowrap on it*/
-.path-grade-report-grader heading_name_row th span {
-    white-space: nowrap;
-}
-.path-grade-report-grader .grade_icons img.ajax {
+.dir-rtl .path-grade-report-grader .gradeparent .excludedfloater {
     float: right;
 }
-.path-grade-report-grader .gradestable th.user,
-.path-grade-report-grader .gradestable th.range,
-.path-grade-report-grader .flexible th,
-.path-grade-report-grader .flexible td,
-.path-grade-report-grader .flexible th a,
-.path-grade-report-grader .flexible td a,
-.path-grade-report-grader .gradestable th.range,
-.path-grade-report-grader td {
-    white-space: nowrap;
-}
-table#user-grades .catlevel1,
-table#user-grades .r1,
-.path-grade-report-grader table tr.even td.cell,
-.path-grade-report-grader table tr.even th {
-    background-color: #fff;
-}
-table#user-grades .catlevel3,
-.path-grade-report-grader table tr.odd td.cell {
-    background-color: #efefef;
-}
-table#fixed_column tr.odd th ,
-table#user-grades tr.odd th {
-    background-color: #efefef;
-}
-table#user-grades td.vmarked,
-table#user-grades tr.odd td.vmarked,
-table#user-grades tr.avg td.vmarked,
-table#user-grades tr.controls td.vmarked,
-table#user-grades .catlevel1.vmarked,
-table#user-grades .catlevel2.vmarked,
-table#user-grades .catlevel3.vmarked,
-table#user-grades tr.range td.vmarked,
-table#user-grades tr.groupavg td.vmarked {
-    background-color: #fc3;
-}
-table#user-grades td.hmarked,
-table#user-grades tr.odd td.hmarked,
-table#user-grades tr.even td.hmarked,
-table#user-grades tr.odd th.hmarked,
-table#user-grades tr.even th.hmarked {
-    background-color: #ff9;
-}
-table#user-grades td.hmarked.vmarked,
-table#user-grades tr.odd td.hmarked.vmarked,
-table#user-grades tr.even td.hmarked.vmarked {
-    background-color: #fc9;
-}
-table#user-grades tr.heading,
-table#user-grades .heading td {
-    border-style: solid;
-    border-width: 0;
-}
-table#user-grades td.userfield,
-table#user-grades th,
-.path-grade-report-grader div.gradeparent,
-.path-grade-report-grader .ie6 form,
-table#user-grades td.ajax {
-    text-align: left;
-}
-.dir-rtl table#user-grades td.userfield,
-.dir-rtl table#user-grades th,
-.path-grade-report-grader.dir-rtl  div.gradeparent,
-.path-grade-report-grader.dir-rtl  .ie6 form,
-.dir-rtl table#user-grades td.ajax {
-    text-align: right;
-}
-.path-grade-report-grader .gradeparent {
-    overflow: auto;
-}
-table#user-grades td.controls,
-.path-grade-report-grader table tr.avg .cell,
-.path-grade-report-grader table tr.range .cell {
+
+.path-grade-report .gradeparent .floater .controls.cell,
+.path-grade-report-grader .gradeparent .controls {
     background-color: #f3ead8;
 }
-.path-grade-report-grader div.left_scroller tr,
-.path-grade-report-grader div.right_scroller tr,
-.path-grade-report-grader div.left_scroller td,
-.path-grade-report-grader div.right_scroller td,
-.path-grade-report-grader div.left_scroller th,
-.path-grade-report-grader div.right_scroller th {
-    height: 4.5em;
-    font-size: 10px;
-}
-.path-grade-report-grader table th.user,
-.path-grade-report-grader table td.userfield {
+.path-grade-report-grader .gradeparent .category {
     text-align: left;
-    vertical-align: middle;
 }
-.path-grade-report-grader .usersuspended a:link,
-.path-grade-report-grader .usersuspended a:visited {
-    color: #666;
-}
-.path-grade-report-grader table th.usersuspended img.usersuspendedicon {
-    vertical-align: text-bottom;
-    margin-left: .45em;
-}
-.path-grade-report-grader .yui3-overlay {
-    background-color: #FFEE69;
-    border-color: #D4C237 #A6982B #A6982B;
-    border-style: solid;
-    border-width: 1px;
-    left: 0;
-    padding: 2px 5px;
-    font-size: 0.7em;
-}
-.path-grade-report-grader .yui3-overlay .fullname {
-    color: #5F3E00;
-    font-weight: bold;
-}
-.path-grade-report-grader .yui3-overlay .itemname {
-    color: #194F3E;
-    font-weight: bold;
-}
-.path-grade-report-grader .yui3-overlay .feedback {
-    color: #5F595E;
-}
-/* table#user-grades td */
-/* .grader-report-grader table#user-grades td .yui-panel div.hd { */
-.path-grade-report-grader #tooltipPanel {
-    text-align: left;
-}
-.path-grade-report-grader .yui3-overlay a.container-close {
-    margin-top: -3px;
-}
-.path-grade-report-grader #hiddentooltiproot,
-.tooltipDiv {
-    display: none;
-}
-.path-grade-report-grader.ie .right_scroller {
-    overflow-y: hidden;
-}
-.path-grade-report-grader.ie table#fixed_column th {
-    height: 4.5em;
-}
-.path-grade-report-grader.ie table#fixed_column tr.avg th {
-    height: 2.1em;
-}
-.path-grade-report-grader.ie div.left_scroller td {
-    height: 4.5em;
-}
-.path-grade-report-grader.ie6 div.right_scroller {
-    margin-top: 4em;
-    width: auto;
-    position: absolute;
-}
-.path-grade-report-grader.ie6 .excludedfloater {
-    font-size: 7px;
-}
-
-/** MDL-40071 **/
-.path-grade-report-grader.dir-rtl table th.user,
-.path-grade-report-grader.dir-rtl table td.userfield {
+.dir-rtl .path-grade-report-grader .gradeparent .category {
     text-align: right;
 }
 
-/** MDL-40180 **/
-.dir-rtl table#user-grades th.category,
-.dir-rtl table#user-grades th#studentheader,
-.dir-rtl table#user-grades th.user {
-    text-align: right;
-}
-.path-grade-report-grader.dir-rtl th.user img.userpicture {
-    margin-left: 0.5em;
-}
-
-.path-grade-report-grader .yui3-scrollview-scrollbar {
-    opacity: 1 !important;
-}
-
-.path-grade-report-grader .yui3-scrollview-scrollbar-horiz {
-    bottom: 0;
-}
-
-/** MDL-43824 **/
-#page-grade-report-grader-index table#fixed_column td.topleft.cell,
-#page-grade-report-grader-index.jsenabled table#fixed_column td.topleft.cell {
-    padding: 8px 5px;
-}
-#page-grade-report-grader-index table#fixed_column td.header.controls {
-    border-left: 1px solid #cecece;
-}
-.path-grade-report-grader .right_scroller td select {
-    line-height: 2;
+/**
+ * Editing fields.
+ */
+.path-grade-report-grader .gradeparent select {
     margin: 0;
     padding: 0;
-    height: 22px;
-}
-table#user-grades {
-    margin-bottom: 0;
-}
-table#user-grades td,
-table#user-grades th.category {
-    padding: 8px 5px;
-}
-.path-grade-report-grader .gradeparent {
-    border-top: 1px solid #cecece;
-}
-.path-grade-report-grader div.left_scroller tr,
-.path-grade-report-grader div.right_scroller tr,
-.path-grade-report-grader div.left_scroller td,
-.path-grade-report-grader div.right_scroller td,
-.path-grade-report-grader div.left_scroller th,
-.path-grade-report-grader div.right_scroller th {
-     padding: 8px 5px;
 }
-.path-grade-report-grader td.grade.overridden {
-    line-height: 20px;
-}
-
-/** MDL-46812 **/
-#page-grade-report-grader-index.jsenabled .right_scroller.topscroll {
-    background-color: #ececec;
-    height: 20px;
+.path-grade-report-grader .gradeparent .text {
+    border: 1px solid #666;
+    width: auto;
     margin: 0;
     padding: 0;
+    text-align: center;
 }
-#page-grade-report-grader-index.jsenabled .topscrollcontent {
-    background-color: #ececec;
-    height: 20px;
+.path-grade-report-grader .gradeparent .quickfeedback {
+    border: 1px dashed #000;
+    width: auto;
+    margin: 0;
+    padding: 0;
+    margin-left: 10px;
 }
-.jsenabled.path-grade-report-grader .left_scroller {
-    border-top: 20px solid #ececec;
+.dir-rtl .path-grade-report-grader .gradeparent .quickfeedback {
+    margin-left: 0;
+    margin-right: 10px;
 }
diff --git a/grade/report/grader/yui/build/moodle-gradereport_grader-gradereporttable/moodle-gradereport_grader-gradereporttable-debug.js b/grade/report/grader/yui/build/moodle-gradereport_grader-gradereporttable/moodle-gradereport_grader-gradereporttable-debug.js
new file mode 100644 (file)
index 0000000..9a3b574
Binary files /dev/null and b/grade/report/grader/yui/build/moodle-gradereport_grader-gradereporttable/moodle-gradereport_grader-gradereporttable-debug.js differ
diff --git a/grade/report/grader/yui/build/moodle-gradereport_grader-gradereporttable/moodle-gradereport_grader-gradereporttable-min.js b/grade/report/grader/yui/build/moodle-gradereport_grader-gradereporttable/moodle-gradereport_grader-gradereporttable-min.js
new file mode 100644 (file)
index 0000000..de4513f
Binary files /dev/null and b/grade/report/grader/yui/build/moodle-gradereport_grader-gradereporttable/moodle-gradereport_grader-gradereporttable-min.js differ
diff --git a/grade/report/grader/yui/build/moodle-gradereport_grader-gradereporttable/moodle-gradereport_grader-gradereporttable.js b/grade/report/grader/yui/build/moodle-gradereport_grader-gradereporttable/moodle-gradereport_grader-gradereporttable.js
new file mode 100644 (file)
index 0000000..0ed03c8
Binary files /dev/null and b/grade/report/grader/yui/build/moodle-gradereport_grader-gradereporttable/moodle-gradereport_grader-gradereporttable.js differ
diff --git a/grade/report/grader/yui/build/moodle-gradereport_grader-scrollview/moodle-gradereport_grader-scrollview-debug.js b/grade/report/grader/yui/build/moodle-gradereport_grader-scrollview/moodle-gradereport_grader-scrollview-debug.js
deleted file mode 100644 (file)
index 3fbb8b1..0000000
Binary files a/grade/report/grader/yui/build/moodle-gradereport_grader-scrollview/moodle-gradereport_grader-scrollview-debug.js and /dev/null differ
diff --git a/grade/report/grader/yui/build/moodle-gradereport_grader-scrollview/moodle-gradereport_grader-scrollview-min.js b/grade/report/grader/yui/build/moodle-gradereport_grader-scrollview/moodle-gradereport_grader-scrollview-min.js
deleted file mode 100644 (file)
index 33c8b8d..0000000
Binary files a/grade/report/grader/yui/build/moodle-gradereport_grader-scrollview/moodle-gradereport_grader-scrollview-min.js and /dev/null differ
diff --git a/grade/report/grader/yui/build/moodle-gradereport_grader-scrollview/moodle-gradereport_grader-scrollview.js b/grade/report/grader/yui/build/moodle-gradereport_grader-scrollview/moodle-gradereport_grader-scrollview.js
deleted file mode 100644 (file)
index 0ffaa7c..0000000
Binary files a/grade/report/grader/yui/build/moodle-gradereport_grader-scrollview/moodle-gradereport_grader-scrollview.js and /dev/null differ
diff --git a/grade/report/grader/yui/src/gradereporttable/build.json b/grade/report/grader/yui/src/gradereporttable/build.json
new file mode 100644 (file)
index 0000000..1c838e8
--- /dev/null
@@ -0,0 +1,11 @@
+{
+    "name": "moodle-gradereport_grader-gradereporttable",
+    "builds": {
+        "moodle-gradereport_grader-gradereporttable": {
+            "jsfiles": [
+                "gradereporttable.js",
+                "floatingheaders.js"
+            ]
+        }
+    }
+}
diff --git a/grade/report/grader/yui/src/gradereporttable/js/floatingheaders.js b/grade/report/grader/yui/src/gradereporttable/js/floatingheaders.js
new file mode 100644 (file)
index 0000000..da76d44
--- /dev/null
@@ -0,0 +1,941 @@
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle 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 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle 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 Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * @module moodle-gradereport_grader-gradereporttable
+ * @submodule floatingheaders
+ */
+
+/**
+ * Provides floating headers to the grader report.
+ *
+ * See {{#crossLink "M.gradereport_grader.ReportTable"}}{{/crossLink}} for details.
+ *
+ * @namespace M.gradereport_grader
+ * @class FloatingHeaders
+ */
+
+var HEIGHT = 'height',
+    WIDTH = 'width',
+    OFFSETWIDTH = 'offsetWidth',
+    OFFSETHEIGHT = 'offsetHeight';
+
+CSS.FLOATING = 'floating';
+
+function FloatingHeaders() {}
+
+FloatingHeaders.ATTRS= {
+};
+
+FloatingHeaders.prototype = {
+    /**
+     * The height of the page header if a fixed position, floating header
+     * was found.
+     *
+     * @property pageHeaderHeight
+     * @type Number
+     * @default 0
+     * @protected
+     */
+    pageHeaderHeight: 0,
+
+    /**
+     * A Node representing the container div.
+     *
+     * Positioning will be based on this element, which must have
+     * the CSS rule 'position: relative'.
+     *
+     * @property container
+     * @type Node
+     * @protected
+     */
+    container: null,
+
+    /**
+     * A Node representing the header cell.
+     *
+     * @property headerCell
+     * @type Node
+     * @protected
+     */
+    headerCell: null,
+
+    /**
+     * A Node representing the header row.
+     *
+     * @property headerRow
+     * @type Node
+     * @protected
+     */
+    headerRow: null,
+
+    /**
+     * A Node representing the first cell which contains user name information.
+     *
+     * @property firstUserCell
+     * @type Node
+     * @protected
+     */
+    firstUserCell: null,
+
+    /**
+     * A Node representing the first cell which does not contain a user header.
+     *
+     * @property firstNonUserCell
+     * @type Node
+     * @protected
+     */
+    firstNonUserCell: null,
+
+    /**
+     * The position of the left of the first non-header cell in a row - the one after the email address.
+     * This is used when processing the scroll event as an optimisation. It must be updated when
+     * additional rows are loaded, or the window changes in some fashion.
+     *
+     * @property firstNonUserCellLeft
+     * @type Number
+     * @protected
+     */
+    firstNonUserCellLeft: 0,
+
+    /**
+     * The width of the first non-header cell in a row - the one after the email address.
+     * This is used when processing the scroll event as an optimisation. It must be updated when
+     * additional rows are loaded, or the window changes in some fashion.
+     * This is only used for RTL calculations.
+     *
+     * @property firstNonUserCellWidth
+     * @type Number
+     * @protected
+     */
+    firstNonUserCellWidth: 0,
+
+    /**
+     * A Node representing the original table footer row.
+     *
+     * @property tableFooterRow
+     * @type Node
+     * @protected
+     */
+    tableFooterRow: null,
+
+    /**
+     * A Node representing the floating footer row in the grading table.
+     *
+     * @property footerRow
+     * @type Node
+     * @protected
+     */
+    footerRow: null,
+
+    /**
+     * A Node representing the floating grade item header.
+     *
+     * @property gradeItemHeadingContainer
+     * @type Node
+     * @protected
+     */
+    gradeItemHeadingContainer: null,
+
+    /**
+     * A Node representing the floating user header. This is the header with the Surname/First name
+     * sorting.
+     *
+     * @property userColumnHeader
+     * @type Node
+     * @protected
+     */
+    userColumnHeader: null,
+
+    /**
+     * A Node representing the floating user column. This is the column containing all of the user
+     * names.
+     *
+     * @property userColumn
+     * @type Node
+     * @protected
+     */
+    userColumn: null,
+
+    /**
+     * The position of the bottom of the first user cell.
+     * This is used when processing the scroll event as an optimisation. It must be updated when
+     * additional rows are loaded, or the window changes in some fashion.
+     *
+     * @property firstUserCellBottom
+     * @type Number
+     * @protected
+     */
+    firstUserCellBottom: 0,
+
+    /**
+     * The position of the left of the first user cell.
+     * This is used when processing the scroll event as an optimisation. It must be updated when
+     * additional rows are loaded, or the window changes in some fashion.
+     *
+     * @property firstUserCellLeft
+     * @type Number
+     * @protected
+     */
+    firstUserCellLeft: 0,
+
+    /**
+     * The width of the first user cell.
+     * This is used when processing the scroll event as an optimisation. It must be updated when
+     * additional rows are loaded, or the window changes in some fashion.
+     * This is only used for RTL calculations.
+     *
+     * @property firstUserCellWidth
+     * @type Number
+     * @protected
+     */
+    firstUserCellWidth: 0,
+
+    /**
+     * The position of the top of the final user cell.
+     * This is used when processing the scroll event as an optimisation. It must be updated when
+     * additional rows are loaded, or the window changes in some fashion.
+     *
+     * @property lastUserCellTop
+     * @type Number
+     * @protected
+     */
+    lastUserCellTop: 0,
+
+    /**
+     * A list of Nodes representing the generic floating rows.
+     *
+     * @property floatingHeaderRow
+     * @type Node{}
+     * @protected
+     */
+    floatingHeaderRow: null,
+
+    /**
+     * Array of EventHandles.
+     *
+     * @type EventHandle[]
+     * @property _eventHandles
+     * @protected
+     */
+    _eventHandles: [],
+
+    /**
+     * Setup the grader report table.
+     *
+     * @method setupFloatingHeaders
+     * @chainable
+     */
+    setupFloatingHeaders: function() {
+        // Grab references to commonly used Nodes.
+        this.firstUserCell = Y.one(SELECTORS.USERCELL);
+        this.container = Y.one(SELECTORS.GRADEPARENT);
+        this.firstNonUserCell = Y.one(SELECTORS.USERMAIL).next();
+
+        if (!this.firstUserCell) {
+            // No need for floating elements, there are no users.
+            return this;
+        }
+
+        // Generate floating elements.
+        this._setupFloatingUserColumn();
+        this._setupFloatingUserHeader();
+        this._setupFloatingAssignmentHeaders();
+        this._setupFloatingAssignmentFooter();
+
+        // Setup generic floating left-aligned headers.
+        this.floatingHeaderRow = {};
+
+        // The 'Controls' row (shown in editing mode when certain options are set).
+        this._setupFloatingLeftHeaders('.controls .controls');
+
+        // The 'Range' row (shown in editing mode when certain options are set).
+        this._setupFloatingLeftHeaders('.range .range');
+
+        // The 'Overall Average' field.
+        this._setupFloatingLeftHeaders(SELECTORS.FOOTERTITLE);
+
+        // Additional setup for the footertitle.
+        this._setupFloatingAssignmentFooterTitle();
+
+        // Calculate the positions of edge cells. These are used for positioning of the floating headers.
+        // This must be called after the floating headers are setup, but before the scroll event handler is invoked.
+        this._calculateCellPositions();
+
+        // Setup the floating element initial positions by simulating scroll.
+        this._handleScrollEvent();
+
+        // Setup the event handlers.
+        this._setupEventHandlers();
+
+        // Listen for a resize event globally - other parts of the code not in this YUI wrapper may make changes to the
+        // fields which result in size changes.
+        Y.Global.on('moodle-gradereport_grader:resized', this._handleResizeEvent, this);
+
+        return this;
+    },
+
+    /**
+     * Calculate the positions of some cells. These values are used heavily
+     * in scroll event handling.
+     *
+     * @method _calculateCellPositions
+     * @protected
+     */
+    _calculateCellPositions: function() {
+        // The header row shows the grade item headers and is floated to the top of the window.
+        this.headerRowTop = this.headerRow.getY();
+
+        // The footer row shows the grade averages and will be floated to the page bottom.
+        if (this.tableFooterRow) {
+            this.footerRowPosition = this.tableFooterRow.getY();
+        }
+
+        var userCellList = Y.all(SELECTORS.USERCELL);
+
+        // The left of the user cells matches the left of the headerRow.
+        this.firstUserCellLeft = this.firstUserCell.getX();
+        this.firstUserCellWidth = this.firstUserCell.get(OFFSETWIDTH);
+
+        // The left of the user cells matches the left of the footer title.
+        this.firstNonUserCellLeft = this.firstNonUserCell.getX();
+        this.firstNonUserCellWidth = this.firstNonUserCell.get(OFFSETWIDTH);
+
+        if (userCellList.size() > 1) {
+            // Use the top of the second cell for the bottom of the first cell.
+            // This is used when scrolling to fix the footer to the top edge of the window.
+            var firstUserCell = userCellList.item(1);
+            this.firstUserCellBottom = firstUserCell.getY() + parseInt(firstUserCell.getComputedStyle(HEIGHT), 10);
+
+            // Use the top of the penultimate cell when scrolling the header.
+            // The header is the same size as the cells.
+            this.lastUserCellTop = userCellList.item(userCellList.size() - 2).getY();
+        } else {
+            var firstItem = userCellList.item(0);
+            // We can't use the top of the second row as there is only one row.
+            this.lastUserCellTop = firstItem.getY();
+
+            if (this.tableFooterRow) {
+                // The footer is present so we can use that.
+                this.firstUserCellBottom = this.footerRowPosition + parseInt(this.tableFooterRow.getComputedStyle(HEIGHT), 10);
+            } else {
+                // No other clues - calculate the top instead.
+                this.firstUserCellBottom = firstItem.getY() + firstItem.get('offsetHeight');
+            }
+        }
+
+        // Check whether a header is present and whether it is floating.
+        var header = Y.one('header');
+        this.pageHeaderHeight = 0;
+        if (header) {
+            if (header.getComputedStyle('position') === 'fixed') {
+                this.pageHeaderHeight = header.get(OFFSETHEIGHT);
+            } else {
+                var navbar = Y.one('.navbar');
+
+                if (navbar) {
+                    // If the navbar exists and isn't fixed, we need to offset the page header to accommodate for it.
+                    this.pageHeaderHeight = navbar.get(OFFSETHEIGHT);
+                }
+            }
+        }
+    },
+
+    /**
+     * Get the relative XY of the node.
+     *
+     * @method _getRelativeXY
+     * @protected
+     * @param {Node} node The node to get the position of.
+     * @return {Array} Containing X and Y.
+     */
+    _getRelativeXY: function(node) {
+        return this._getRelativeXYFromXY(node.getX(), node.getY());
+    },
+
+    /**
+     * Get the relative positioning from coordinates.
+     *
+     * This gives the position according to the parent of the table, which must
+     * be set as position: relative.
+     *
+     * @method _getRelativeXYFromXY
+     * @protected
+     * @param {Number} x X position.
+     * @param {Number} y Y position.
+     * @return {Array} Containing X and Y.
+     */
+    _getRelativeXYFromXY: function(x, y) {
+        var parentXY = this.container.getXY();
+        return [x - parentXY[0], y - parentXY[1]];
+    },
+
+    /**
+     * Get the relative positioning of an elements from coordinates.
+     *
+     * @method _getRelativeXFromX
+     * @protected
+     * @param {Number} pos X position.
+     * @return {Number} relative X position.
+     */
+    _getRelativeXFromX: function(pos) {
+        return this._getRelativeXYFromXY(pos, 0)[0];
+    },
+
+    /**
+     * Get the relative positioning of an elements from coordinates.
+     *
+     * @method _getRelativeYFromY
+     * @protected
+     * @param {Number} pos Y position.
+     * @return {Number} relative Y position.
+     */
+    _getRelativeYFromY: function(pos) {
+        return this._getRelativeXYFromXY(0, pos)[1];
+    },
+
+    /**
+     * Return the size of the horizontal scrollbar.
+     *
+     * @method _getScrollBarHeight
+     * @protected
+     * @return {Number} Height of the scrollbar.
+     */
+    _getScrollBarHeight: function() {
+        if (Y.UA.ie && Y.UA.ie >= 10) {
+            // IE has transparent scrollbars, which sometimes disappear... it's better to ignore them.
+            return 0;
+        } else if (Y.config.doc.body.scrollWidth > Y.config.doc.body.clientWidth) {
+            // The document can be horizontally scrolled.
+            return Y.DOM.getScrollbarWidth();
+        }
+        return 0;
+    },
+
+    /**
+     * Setup the main event listeners.
+     * These deal with things like window events.
+     *
+     * @method _setupEventHandlers
+     * @protected
+     */
+    _setupEventHandlers: function() {
+        this._eventHandles.push(
+            // Listen for window scrolls, resizes, and rotation events.
+            Y.one(Y.config.win).on('scroll', this._handleScrollEvent, this),
+            Y.one(Y.config.win).on('resize', this._handleResizeEvent, this),
+            Y.one(Y.config.win).on('orientationchange', this._handleResizeEvent, this)
+        );
+    },
+
+    /**
+     * Create and setup the floating column of user names.
+     *
+     * @method _setupFloatingUserColumn
+     * @protected
+     */
+    _setupFloatingUserColumn: function() {
+        // Grab all cells in the user names column.
+        var userColumn = Y.all(SELECTORS.USERCELL),
+
+        // Create a floating table.
+            floatingUserColumn = Y.Node.create('<div aria-hidden="true" role="presentation" class="floater"></div>'),
+
+        // Get the XY for the floating element.
+            coordinates = this._getRelativeXY(this.firstUserCell);
+
+        // Generate the new fields.
+        userColumn.each(function(node) {
+            // Create and configure the new container.
+            var containerNode = Y.Node.create('<div></div>');
+            containerNode.set('innerHTML', node.get('innerHTML'))
+                    .setAttribute('class', node.getAttribute('class'))
+                    .setAttribute('data-uid', node.ancestor('tr').getData('uid'))
+                    .setStyles({
+                        height: node.getComputedStyle(HEIGHT),
+                        width:  node.getComputedStyle(WIDTH)
+                    });
+
+            // Add the new nodes to our floating table.
+            floatingUserColumn.appendChild(containerNode);
+        }, this);
+
+        // Style the floating user container.
+        floatingUserColumn.setStyles({
+            left:       coordinates[0] + 'px',
+            position:   'absolute',
+            top:        coordinates[1] + 'px'
+        });
+
+        // Append to the grader region.
+        this.graderRegion.append(floatingUserColumn);
+
+        // Store a reference to this for later - we use it in the event handlers.
+        this.userColumn = floatingUserColumn;
+    },
+
+    /**
+     * Create and setup the floating username header cell.
+     *
+     * @method _setupFloatingUserHeader
+     * @protected
+     */
+    _setupFloatingUserHeader: function() {
+        // We make various references to the header cells. Store it for later.
+        this.headerRow = Y.one(SELECTORS.HEADERROW);
+        this.headerCell = Y.one(SELECTORS.STUDENTHEADER);
+
+        // Create the floating row and cell.
+        var floatingUserHeaderRow = Y.Node.create('<div aria-hidden="true" role="presentation" class="floater heading"></div>'),
+            floatingUserHeaderCell = Y.Node.create('<div></div>'),
+            nodepos = this._getRelativeXY(this.headerCell)[0],
+            coordinates = this._getRelativeXY(this.headerRow),
+            gradeHeadersOffset = coordinates[0];
+
+        // Append the content and style to the floating cell.
+        floatingUserHeaderCell
+            .set('innerHTML', this.headerCell.getHTML())
+            .setAttribute('class', this.headerCell.getAttribute('class'))
+            .setStyles({
+                // The header is larger than the user cells, so we take the user cell.
+                width:      this.firstUserCell.getComputedStyle(WIDTH),
+                left:       (nodepos - gradeHeadersOffset) + 'px'
+            });
+
+        // Style the floating row.
+        floatingUserHeaderRow
+            .setStyles({
+                left:       coordinates[0] + 'px',
+                position:   'absolute',
+                top:        coordinates[1] + 'px'
+            });
+
+        // Append the cell to the row, and finally to the region.
+        floatingUserHeaderRow.append(floatingUserHeaderCell);
+        this.graderRegion.append(floatingUserHeaderRow);
+
+        // Store a reference to this for later - we use it in the event handlers.
+        this.userColumnHeader = floatingUserHeaderRow;
+    },
+
+    /**
+     * Create and setup the floating grade item header row.
+     *
+     * @method _setupFloatingAssignmentHeaders
+     * @protected
+     */
+    _setupFloatingAssignmentHeaders: function() {
+        this.headerRow = Y.one('#user-grades tr.heading');
+
+        var gradeHeaders = Y.all('#user-grades tr.heading .cell');
+
+        // Generate a floating headers
+        var floatingGradeHeaders = Y.Node.create('<div aria-hidden="true" role="presentation" class="floater heading"></div>');
+
+        var coordinates = this._getRelativeXY(this.headerRow);
+
+        var floatingGradeHeadersWidth = 0;
+        var floatingGradeHeadersHeight = 0;
+        var gradeHeadersOffset = coordinates[0];
+
+        gradeHeaders.each(function(node) {
+            var nodepos = this._getRelativeXY(node)[0];
+
+            var newnode = Y.Node.create('<div></div>');
+            newnode.append(node.getHTML())
+                .setAttribute('class', node.getAttribute('class'))
+                .setData('itemid', node.getData('itemid'))
+                .setStyles({
+                    height:     node.getComputedStyle(HEIGHT),
+                    left:       (nodepos - gradeHeadersOffset) + 'px',
+                    position:   'absolute',
+                    width:      node.getComputedStyle(WIDTH)
+                });
+
+            // Sum up total widths - these are used in the container styles.
+            // Use the offsetHeight and Width here as this contains the
+            // padding, margin, and borders.
+            floatingGradeHeadersWidth += parseInt(node.get(OFFSETWIDTH), 10);
+            floatingGradeHeadersHeight = node.get(OFFSETHEIGHT);
+
+            // Append to our floating table.
+            floatingGradeHeaders.appendChild(newnode);
+        }, this);
+
+        // Position header table.
+        floatingGradeHeaders.setStyles({
+            height:     floatingGradeHeadersHeight + 'px',
+            left:       coordinates[0] + 'px',
+            position:   'absolute',
+            top:        coordinates[1] + 'px',
+            width:      floatingGradeHeadersWidth + 'px'
+        });
+
+        // Insert in place before the grader headers.
+        this.userColumnHeader.insert(floatingGradeHeaders, 'before');
+
+        // Store a reference to this for later - we use it in the event handlers.
+        this.gradeItemHeadingContainer = floatingGradeHeaders;
+    },
+
+    /**
+     * Create and setup the floating header row of grade item titles.
+     *
+     * @method _setupFloatingAssignmentFooter
+     * @protected
+     */
+    _setupFloatingAssignmentFooter: function() {
+        this.tableFooterRow = Y.one('#user-grades .avg');
+        if (!this.tableFooterRow) {
+            Y.log('Averages footer not found - unable to float it.', 'warn', LOGNS);
+            return;
+        }
+
+        // Generate the sticky footer row.
+        var footerCells = this.tableFooterRow.all('.cell');
+
+        // Create a container.
+        var floatingGraderFooter = Y.Node.create('<div aria-hidden="true" role="presentation" class="floater avg"></div>');
+        var footerWidth = 0;
+        var coordinates = this._getRelativeXY(this.tableFooterRow);
+        var footerRowOffset = coordinates[0];
+        var floatingGraderFooterHeight = 0;
+
+        // Copy cell content.
+        footerCells.each(function(node) {
+            var newnode = Y.Node.create('<div></div>');
+            var nodepos = this._getRelativeXY(node)[0];
+            newnode.set('innerHTML', node.getHTML())
+                .setAttribute('class', node.getAttribute('class'))
+                .setStyles({
+                    height:     node.getComputedStyle(HEIGHT),
+                    left:       (nodepos - footerRowOffset) + 'px',
+                    position:   'absolute',
+                    width:      node.getComputedStyle(WIDTH)
+                });
+
+            floatingGraderFooter.append(newnode);
+            floatingGraderFooterHeight = node.get(OFFSETHEIGHT);
+            footerWidth += parseInt(node.get(OFFSETWIDTH), 10);
+        }, this);
+
+        // Position the row.
+        floatingGraderFooter.setStyles({
+            position:   'absolute',
+            left:       coordinates[0] + 'px',
+            bottom:     '1px',
+            height:     floatingGraderFooterHeight + 'px',
+            width:      footerWidth + 'px'
+        });
+
+        // Append to the grader region.
+        this.graderRegion.append(floatingGraderFooter);
+
+        this.footerRow = floatingGraderFooter;
+    },
+
+    /**
+     * Create and setup the floating footer title cell.
+     *
+     * @method _setupFloatingAssignmentFooterTitle
+     * @protected
+     */
+    _setupFloatingAssignmentFooterTitle: function() {
+        var floatingFooterRow = this.floatingHeaderRow[SELECTORS.FOOTERTITLE];
+        if (floatingFooterRow) {
+            // Style the floating row.
+            floatingFooterRow
+                .setStyles({
+                    bottom:     '1px'
+                });
+        }
+    },
+
+    /**
+     * Create and setup the floating left headers.
+     *
+     * @method _setupFloatingLeftHeaders
+     * @protected
+     */
+    _setupFloatingLeftHeaders: function(headerSelector) {
+        // We make various references to the origin cell. Store it for later.
+        var origin = Y.one(headerSelector);
+
+        if (!origin) {
+            return;
+        }
+
+        // Create the floating row and cell.
+        var floatingRow = Y.Node.create('<div aria-hidden="true" role="presentation" class="floater"></div>'),
+            floatingCell = Y.Node.create('<div></div>'),
+            coordinates = this._getRelativeXY(origin),
+            width = this.firstUserCell.getComputedStyle(WIDTH),
+            height = origin.get(OFFSETHEIGHT);
+
+        // Append the content and style to the floating cell.
+        floatingCell
+            .set('innerHTML', origin.getHTML())
+            .setAttribute('class', origin.getAttribute('class'))
+            .setStyles({
+                // The header is larger than the user cells, so we take the user cell.
+                width:      width
+            });
+
+        // Style the floating row.
+        floatingRow
+            .setStyles({
+                position:   'absolute',
+                top:        coordinates[1] + 'px',
+                left:       coordinates[0] + 'px',
+                height:     height + 'px'
+            })
+            // Add all classes from the parent to the row
+            .addClass(origin.get('parentNode').get('className'));
+
+        // Append the cell to the row, and finally to the region.
+        floatingRow.append(floatingCell);
+        this.graderRegion.append(floatingRow);
+
+        // Store a reference to this for later - we use it in the event handlers.
+        this.floatingHeaderRow[headerSelector] = floatingRow;
+    },
+
+    /**
+     * Process a Scroll Event on the window.
+     *
+     * @method _handleScrollEvent
+     * @protected
+     */
+    _handleScrollEvent: function() {
+        // Performance is important in this function as it is called frequently and in quick succesion.
+        // To prevent layout thrashing when the DOM is repeatedly updated and queried, updated and queried,
+        // updates must be batched.
+
+        // Next do all the calculations.
+        var gradeItemHeadingContainerStyles = {},
+            userColumnHeaderStyles = {},
+            userColumnStyles = {},
+            footerStyles = {},
+            coord = 0,
+            floatingUserTriggerPoint = 0,       // The X position at which the floating should start.
+            floatingUserRelativePoint = 0,      // The point to use when calculating the new position.
+            headerFloats = false,
+            userFloats = false,
+            footerFloats = false,
+            leftTitleFloats = false,
+            floatingHeaderStyles = {},
+            floatingFooterTitleStyles = {},
+            floatingFooterTitleRow = false;
+
+        // Header position.
+        gradeItemHeadingContainerStyles.left = this._getRelativeXFromX(this.headerRow.getX());
+        if (Y.config.win.pageYOffset + this.pageHeaderHeight > this.headerRowTop) {
+            headerFloats = true;
+            if (Y.config.win.pageYOffset + this.pageHeaderHeight < this.lastUserCellTop) {
+                coord = this._getRelativeYFromY(Y.config.win.pageYOffset + this.pageHeaderHeight);
+                gradeItemHeadingContainerStyles.top = coord + 'px';
+                userColumnHeaderStyles.top = coord + 'px';
+            } else {
+                coord = this._getRelativeYFromY(this.lastUserCellTop);
+                gradeItemHeadingContainerStyles.top = coord + 'px';
+                userColumnHeaderStyles.top = coord + 'px';
+            }
+        } else {
+            headerFloats = false;
+            coord = this._getRelativeYFromY(this.headerRowTop);
+            gradeItemHeadingContainerStyles.top = coord + 'px';
+            userColumnHeaderStyles.top = coord + 'px';
+        }
+
+        // User column position.
+        if (right_to_left()) {
+            floatingUserTriggerPoint = Y.config.win.innerWidth + Y.config.win.pageXOffset;
+            floatingUserRelativePoint = floatingUserTriggerPoint - this.firstUserCellWidth;
+            userFloats = floatingUserTriggerPoint < (this.firstUserCellLeft + this.firstUserCellWidth);
+            leftTitleFloats = (floatingUserTriggerPoint - this.firstNonUserCellWidth) <
+                              (this.firstNonUserCellLeft + this.firstUserCellWidth);
+        } else {
+            floatingUserTriggerPoint = Y.config.win.pageXOffset;
+            floatingUserRelativePoint = floatingUserTriggerPoint;
+            userFloats = floatingUserTriggerPoint > this.firstUserCellLeft;
+            leftTitleFloats = floatingUserTriggerPoint > (this.firstNonUserCellLeft - this.firstUserCellWidth);
+        }
+
+        if (userFloats) {
+            coord = this._getRelativeXFromX(floatingUserRelativePoint);
+            userColumnStyles.left = coord + 'px';
+            userColumnHeaderStyles.left = coord + 'px';
+        } else {
+            coord = this._getRelativeXFromX(this.firstUserCellLeft);
+            userColumnStyles.left = coord + 'px';
+            userColumnHeaderStyles.left = coord + 'px';
+        }
+
+        // Update the miscellaneous left-only floats.
+        Y.Object.each(this.floatingHeaderRow, function(origin, key) {
+            floatingHeaderStyles[key] = {
+                left: userColumnStyles.left
+            };
+        }, this);
+
+        // Update footer.
+        if (this.footerRow) {
+            footerStyles.left = this._getRelativeXFromX(this.headerRow.getX());
+
+            // Determine whether the footer should now be shown as sticky.
+            var pageHeight = Y.config.win.innerHeight,
+                pageOffset = Y.config.win.pageYOffset,
+                bottomScrollPosition = pageHeight - this._getScrollBarHeight() + pageOffset,
+                footerRowHeight = parseInt(this.footerRow.getComputedStyle(HEIGHT), 10),
+                footerBottomPosition = footerRowHeight + this.footerRowPosition;
+
+            floatingFooterTitleStyles = floatingHeaderStyles[SELECTORS.FOOTERTITLE];
+            floatingFooterTitleRow = this.floatingHeaderRow[SELECTORS.FOOTERTITLE];
+            if (bottomScrollPosition < footerBottomPosition && bottomScrollPosition > this.firstUserCellBottom) {
+                // We have not scrolled below the footer, nor above the first row.
+                footerStyles.bottom = Math.ceil(footerBottomPosition - bottomScrollPosition) + 'px';
+                footerFloats = true;
+            } else {
+                // The footer should not float any more.
+                footerStyles.bottom = '1px';
+                footerFloats = false;
+            }
+            if (floatingFooterTitleStyles) {
+                floatingFooterTitleStyles.bottom = footerStyles.bottom;
+                floatingFooterTitleStyles.top = null;
+            }
+            floatingHeaderStyles[SELECTORS.FOOTERTITLE] = floatingFooterTitleStyles;
+        }
+
+        // Apply the styles.
+        this.gradeItemHeadingContainer.setStyles(gradeItemHeadingContainerStyles);
+        this.userColumnHeader.setStyles(userColumnHeaderStyles);
+        this.userColumn.setStyles(userColumnStyles);
+        this.footerRow.setStyles(footerStyles);
+
+        // And apply the styles to the generic left headers.
+        Y.Object.each(floatingHeaderStyles, function(styles, key) {
+            this.floatingHeaderRow[key].setStyles(styles);
+        }, this);
+
+        // Mark the elements as floating, or not.
+        if (headerFloats) {
+            this.gradeItemHeadingContainer.addClass(CSS.FLOATING);
+        } else {
+            this.gradeItemHeadingContainer.removeClass(CSS.FLOATING);
+        }
+
+        if (userFloats) {
+            this.userColumnHeader.addClass(CSS.FLOATING);
+            this.userColumn.addClass(CSS.FLOATING);
+        } else {
+            this.userColumnHeader.removeClass(CSS.FLOATING);
+            this.userColumn.removeClass(CSS.FLOATING);
+        }
+
+        if (footerFloats) {
+            this.footerRow.addClass(CSS.FLOATING);
+        } else {
+            this.footerRow.removeClass(CSS.FLOATING);
+        }
+
+        Y.Object.each(this.floatingHeaderRow, function(value, key) {
+            if (leftTitleFloats) {
+                this.floatingHeaderRow[key].addClass(CSS.FLOATING);
+            } else {
+                this.floatingHeaderRow[key].removeClass(CSS.FLOATING);
+            }
+        }, this);
+
+        // The footer title has a more specific float setting.
+        if (floatingFooterTitleRow) {
+            if (leftTitleFloats) {
+                floatingFooterTitleRow.addClass(CSS.FLOATING);
+            } else {
+                floatingFooterTitleRow.removeClass(CSS.FLOATING);
+            }
+        }
+
+    },
+
+    /**
+     * Process a size change Event on the window.
+     *
+     * @method _handleResizeEvent
+     * @protected
+     */
+    _handleResizeEvent: function() {
+        // Recalculate the position of the edge cells for scroll positioning.
+        this._calculateCellPositions();
+
+        // Simulate a scroll.
+        this._handleScrollEvent();
+
+        // Resize user cells.
+        var userWidth = this.firstUserCell.getComputedStyle(WIDTH);
+        var userCells = Y.all(SELECTORS.USERCELL);
+        this.userColumnHeader.one('.cell').setStyle('width', userWidth);
+        this.userColumn.all('.cell').each(function(cell, idx) {
+            cell.setStyles({
+                width: userWidth,
+                height: userCells.item(idx).getComputedStyle(HEIGHT)
+            });
+        }, this);
+
+        // Resize headers & footers.
+        // This is an expensive operation, not expected to happen often.
+        var headers = this.gradeItemHeadingContainer.all('.cell');
+        var resizedcells = Y.all(SELECTORS.HEADERCELLS);
+
+        var headeroffsetleft = this.headerRow.getX();
+        var newcontainerwidth = 0;
+        resizedcells.each(function(cell, idx) {
+            var headercell = headers.item(idx);
+
+            newcontainerwidth += cell.get(OFFSETWIDTH);
+            var styles = {
+                width: cell.getComputedStyle(WIDTH),
+                left: cell.getX() - headeroffsetleft + 'px'
+            };
+            headercell.setStyles(styles);
+        });
+
+        if (this.footerRow) {
+            var footers = this.footerRow.all('.cell');
+            if (footers.size() !== 0) {
+                var resizedavgcells = Y.all(SELECTORS.FOOTERCELLS);
+
+                resizedavgcells.each(function(cell, idx) {
+                    var footercell = footers.item(idx);
+                    var styles = {
+                        width: cell.getComputedStyle(WIDTH),
+                        left: cell.getX() - headeroffsetleft + 'px'
+                    };
+                    footercell.setStyles(styles);
+                });
+            }
+        }
+
+        // Resize the title areas too.
+        Y.Object.each(this.floatingHeaderRow, function(row) {
+            row.one('div').setStyle('width', userWidth);
+        }, this);
+
+        this.gradeItemHeadingContainer.setStyle('width', newcontainerwidth);
+    }
+
+};
+
+Y.Base.mix(Y.M.gradereport_grader.ReportTable, [FloatingHeaders]);
diff --git a/grade/report/grader/yui/src/gradereporttable/js/gradereporttable.js b/grade/report/grader/yui/src/gradereporttable/js/gradereporttable.js
new file mode 100644 (file)
index 0000000..bbdbd8f
--- /dev/null
@@ -0,0 +1,141 @@
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle 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 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle 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 Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Grader Report Functionality.
+ *
+ * @module    moodle-gradereport_grader-gradereporttable
+ * @package   gradereport_grader
+ * @copyright 2014 UC Regents
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @author    Alfonso Roman <aroman@oid.ucla.edu>
+ */
+
+/**
+ * @module moodle-gradereport_grader-gradereporttable
+ */
+
+var SELECTORS = {
+        FOOTERTITLE: '.avg .header',
+        FOOTERCELLS: '#user-grades .avg .cell',
+        FOOTERROW: '#user-grades .avg',
+        GRADECELL: 'td.grade',
+        GRADERTABLE: '.gradeparent table',
+        GRADEPARENT: '.gradeparent',
+        HEADERCELLS: '#user-grades .heading .cell',
+        HEADERCELL: '.gradebook-header-cell',
+        HEADERROW: '#user-grades tr.heading',
+        STUDENTHEADER: '#studentheader',
+        USERCELL: '#user-grades .user.cell',
+        USERMAIL: '#user-grades .useremail'
+    },
+    CSS = {
+        OVERRIDDEN: 'overridden',
+        TOOLTIPACTIVE: 'tooltipactive'
+    };
+
+/**
+ * The Grader Report Table.
+ *
+ * @namespace M.gradereport_grader
+ * @class ReportTable
+ * @constructor
+ */
+function ReportTable() {
+    ReportTable.superclass.constructor.apply(this, arguments);
+}
+
+Y.extend(ReportTable, Y.Base, {
+    /**
+     * Array of EventHandles.
+     *
+     * @type EventHandle[]
+     * @property _eventHandles
+     * @protected
+     */
+    _eventHandles: [],
+
+    /**
+     * A Node reference to the grader table.
+     *
+     * @property graderTable
+     * @type Node
+     */
+    graderTable: null,
+
+    /**
+     * Setup the grader report table.
+     *
+     * @method initializer
+     */
+    initializer: function() {
+        // Some useful references within our target area.
+        this.graderRegion = Y.one(SELECTORS.GRADEPARENT);
+        this.graderTable = Y.one(SELECTORS.GRADERTABLE);
+
+        // Setup the floating headers.
+        this.setupFloatingHeaders();
+    },
+
+    /**
+     * Get the text content of the username for the specified grade item.
+     *
+     * @method getGradeUserName
+     * @param {Node} cell The grade item cell to obtain the username for
+     * @return {String} The string content of the username cell.
+     */
+    getGradeUserName: function(cell) {
+        var userrow = cell.ancestor('tr'),
+            usercell = userrow.one("th.user .username");
+
+        if (usercell) {
+            return usercell.get('text');
+        } else {
+            return '';
+        }
+    },
+
+    /**
+     * Get the text content of the item name for the specified grade item.
+     *
+     * @method getGradeItemName
+     * @param {Node} cell The grade item cell to obtain the item name for
+     * @return {String} The string content of the item name cell.
+     */
+    getGradeItemName: function(cell) {
+        var itemcell = Y.one("th.item[data-itemid='" + cell.getData('itemid') + "']");
+        if (itemcell) {
+            return itemcell.get('text');
+        } else {
+            return '';
+        }
+    },
+
+    /**
+     * Get the text content of any feedback associated with the grade item.
+     *
+     * @method getGradeFeedback
+     * @param {Node} cell The grade item cell to obtain the item name for
+     * @return {String} The string content of the feedback.
+     */
+    getGradeFeedback: function(cell) {
+        return cell.getData('feedback');
+    }
+});
+
+Y.namespace('M.gradereport_grader').ReportTable = ReportTable;
+Y.namespace('M.gradereport_grader').init = function(config) {
+    return new Y.M.gradereport_grader.ReportTable(config);
+};
diff --git a/grade/report/grader/yui/src/gradereporttable/meta/gradereporttable.json b/grade/report/grader/yui/src/gradereporttable/meta/gradereporttable.json
new file mode 100644 (file)
index 0000000..2fa70f2
--- /dev/null
@@ -0,0 +1,12 @@
+{
+    "moodle-gradereport_grader-gradereporttable": {
+        "requires": [
+            "base",
+            "node",
+            "event",
+            "handlebars",
+            "overlay",
+            "event-hover"
+        ]
+    }
+}
diff --git a/grade/report/grader/yui/src/scrollview/build.json b/grade/report/grader/yui/src/scrollview/build.json
deleted file mode 100644 (file)
index c84fb5b..0000000
+++ /dev/null
@@ -1,10 +0,0 @@
-{
-    "name": "moodle-gradereport_grader-scrollview",
-    "builds": {
-        "moodle-gradereport_grader-scrollview": {
-            "jsfiles": [
-                "scrollview.js"
-            ]
-        }
-    }
-}
diff --git a/grade/report/grader/yui/src/scrollview/js/scrollview.js b/grade/report/grader/yui/src/scrollview/js/scrollview.js
deleted file mode 100644 (file)
index 1cf46ac..0000000
+++ /dev/null
@@ -1,79 +0,0 @@
-// This file is part of Moodle - http://moodle.org/
-//
-// Moodle 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 3 of the License, or
-// (at your option) any later version.
-//
-// Moodle 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 Moodle.  If not, see <http://www.gnu.org/licenses/>.
-
-
-/**
- * Scrollview for grader table.
- *
- * @package   gradereport_grader
- * @copyright 2013 NetSpot Pty Ltd
- * @author    Adam Olley <adam.olley@netspot.com.au>
- * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-
-M.gradereport_grader = M.gradereport_grader || {};
-M.gradereport_grader.scrollview = {
-
-    /** Selectors. */
-    SELECTORS: {
-        CONTAINER: '.gradeparent',
-        STATIC:    '.gradeparent .right_scroller',
-        GRADETABLE: '#user-grades'
-    },
-
-    container : null,
-
-    /**
-     * Initialise the scrollview code.
-     */
-    init: function() {
-        this.container = Y.one(this.SELECTORS.CONTAINER);
-        if (!this.container) {
-            Y.log('No grade container found.');
-            return;
-        }
-
-        var topscroll = Y.Node.create('<div class="right_scroller topscroll"><div class="topscrollcontent"></div></div>');
-
-        var src = this.SELECTORS.CONTAINER;
-        if (Y.one(this.SELECTORS.STATIC)) {
-            src = this.SELECTORS.STATIC;
-        }
-
-        var node = Y.one(src).insert(topscroll, 'before');
-
-        if (!Y.one(this.SELECTORS.STATIC)) {
-            node = Y.one('.topscroll');
-        }
-
-        Y.on('domready', function () {
-            this.resize();
-        }, this);
-
-        Y.one(src).on('scroll', function() {
-            node.set('scrollLeft', Y.one(src).get('scrollLeft'));
-        });
-
-        node.on('scroll', function() {
-            Y.one(src).set('scrollLeft', node.get('scrollLeft'));
-        });
-
-    },
-
-    resize: function() {
-        var width = Y.one(this.SELECTORS.GRADETABLE).get('offsetWidth');
-        Y.one('.topscrollcontent').setStyle('width', width + 'px');
-    }
-};
diff --git a/grade/report/grader/yui/src/scrollview/meta/scrollview.json b/grade/report/grader/yui/src/scrollview/meta/scrollview.json
deleted file mode 100644 (file)
index 1a53521..0000000
+++ /dev/null
@@ -1,8 +0,0 @@
-{
-    "moodle-gradereport_grader-scrollview": {
-        "requires": [
-            "base",
-            "node"
-        ]
-    }
-}
index 0177e34..ee6ec76 100644 (file)
@@ -48,7 +48,7 @@ Feature: Site settings can be used to hide parts of the gradebook UI
 
   @javascript
   Scenario: Disable category overriding
-    And ".r0 .course input[type='text']" "css_element" should exist
+    And ".r1 .course input[type='text']" "css_element" should exist
     Then I navigate to "Grade category settings" node in "Site administration > Grades"
     And I click on "Allow category grades to be manually overridden" "checkbox"
     And I press "Save changes"
index 1df72b7..6dffd30 100644 (file)
@@ -58,7 +58,7 @@ Feature: We can use calculated grade totals
       | Hidden | 1 |
     And I set the following settings for grade item "Test assignment eight":
       | Hidden | 1 |
-    And I navigate to "Course grade settings" node in "Grade administration > Settings"
+    And I navigate to "Course grade settings" node in "Grade administration > Setup"
     And I set the field "Grade display type" to "Real (percentage)"
     And I press "Save changes"
 
@@ -73,7 +73,7 @@ Feature: We can use calculated grade totals
       | Exclude empty grades | 0              |
     And I turn editing mode off
     Then I should see "30.00 (30.00 %)" in the ".course" "css_element"
-    And I navigate to "Course grade settings" node in "Grade administration > Settings"
+    And I navigate to "Course grade settings" node in "Grade administration > Setup"
     And I set the field "Hide totals if they contain hidden items" to "Show totals excluding hidden items"
     And I press "Save changes"
     And I log out
@@ -98,7 +98,7 @@ Feature: We can use calculated grade totals
       | Item weight | 3 |
     And I turn editing mode off
     Then I should see "27.14 (27.14 %)" in the ".course" "css_element"
-    And I navigate to "Course grade settings" node in "Grade administration > Settings"
+    And I navigate to "Course grade settings" node in "Grade administration > Setup"
     And I set the field "Hide totals if they contain hidden items" to "Show totals excluding hidden items"
     And I press "Save changes"
     And I log out
@@ -121,7 +121,7 @@ Feature: We can use calculated grade totals
       | Extra credit | 1 |
     And I turn editing mode off
     Then I should see "45.19 (45.19 %)" in the ".course" "css_element"
-    And I navigate to "Course grade settings" node in "Grade administration > Settings"
+    And I navigate to "Course grade settings" node in "Grade administration > Setup"
     And I set the field "Hide totals if they contain hidden items" to "Show totals excluding hidden items"
     And I press "Save changes"
     And I log out
@@ -144,7 +144,7 @@ Feature: We can use calculated grade totals
       | Extra credit weight  | 2 |
     And I turn editing mode off
     Then I should see "42.50 (42.50 %)" in the ".course" "css_element"
-    And I navigate to "Course grade settings" node in "Grade administration > Settings"
+    And I navigate to "Course grade settings" node in "Grade administration > Setup"
     And I set the field "Hide totals if they contain hidden items" to "Show totals excluding hidden items"
     And I press "Save changes"
     And I log out
@@ -165,7 +165,7 @@ Feature: We can use calculated grade totals
       | Exclude empty grades | 0                |
     And I turn editing mode off
     Then I should see "26.67 (26.67 %)" in the ".course" "css_element"
-    And I navigate to "Course grade settings" node in "Grade administration > Settings"
+    And I navigate to "Course grade settings" node in "Grade administration > Setup"
     And I set the field "Hide totals if they contain hidden items" to "Show totals excluding hidden items"
     And I press "Save changes"
     And I log out
@@ -190,7 +190,7 @@ Feature: We can use calculated grade totals
       | Hidden | 1 |
     And I turn editing mode off
     Then I should see "0.00 (0.00 %)" in the ".course" "css_element"
-    And I navigate to "Course grade settings" node in "Grade administration > Settings"
+    And I navigate to "Course grade settings" node in "Grade administration > Setup"
     And I set the field "Hide totals if they contain hidden items" to "Show totals excluding hidden items"
     And I press "Save changes"
     And I log out
@@ -213,7 +213,7 @@ Feature: We can use calculated grade totals
       | Hidden | 1 |
     And I turn editing mode off
     Then I should see "50.00 (50.00 %)" in the ".course" "css_element"
-    And I navigate to "Course grade settings" node in "Grade administration > Settings"
+    And I navigate to "Course grade settings" node in "Grade administration > Setup"
     And I set the field "Hide totals if they contain hidden items" to "Show totals excluding hidden items"
     And I press "Save changes"
     And I log out
@@ -236,7 +236,7 @@ Feature: We can use calculated grade totals
       | Hidden | 1 |
     And I turn editing mode off
     Then I should see "50.00 (50.00 %)" in the ".course" "css_element"
-    And I navigate to "Course grade settings" node in "Grade administration > Settings"
+    And I navigate to "Course grade settings" node in "Grade administration > Setup"
     And I set the field "Hide totals if they contain hidden items" to "Show totals excluding hidden items"
     And I press "Save changes"
     And I log out
@@ -264,7 +264,7 @@ Feature: We can use calculated grade totals
       | Extra credit | 1 |
     And I turn editing mode off
     Then I should see "152.68 (24.43 %)" in the ".course" "css_element"
-    And I navigate to "Course grade settings" node in "Grade administration > Settings"
+    And I navigate to "Course grade settings" node in "Grade administration > Setup"
     And I set the field "report_overview_showtotalsifcontainhidden" to "Show totals excluding hidden items"
     And I set the field "report_user_showtotalsifcontainhidden" to "Show totals excluding hidden items"
     And I set the field "Show contribution to course total" to "Show"
@@ -317,7 +317,7 @@ Feature: We can use calculated grade totals
     And I set the following settings for grade item "Course 1":
       | Aggregation          | Natural |
       | Exclude empty grades | 0       |
-    And I navigate to "Set up grades layout" node in "Grade administration > Settings"
+    And I navigate to "Categories and items" node in "Grade administration > Setup"
     And I press "Add category"
     And I click on "Show more" "link"
     And I set the following fields to these values:
@@ -372,7 +372,7 @@ Feature: We can use calculated grade totals
     And I press "Save changes"
     And I turn editing mode off
     And I should see "250.00 (25.25 %)" in the ".course" "css_element"
-    And I navigate to "Set up grades layout" node in "Grade administration > Settings"
+    And I navigate to "Categories and items" node in "Grade administration > Setup"
     And I press "Add category"
     And I set the following fields to these values:
       | Category name | Sub sub category 1 |
@@ -383,7 +383,7 @@ Feature: We can use calculated grade totals
 
   @javascript
   Scenario: Natural aggregation from the setup screen
-    And I set the field "Grade report" to "Set up grades layout"
+    And I set the field "Grade report" to "Categories and items"
     And I follow "Edit   Course 1"
     And I set the field "Aggregation" to "Natural"
     And I press "Save changes"
@@ -448,7 +448,7 @@ Feature: We can use calculated grade totals
       | Aggregation          | Natural |
       | Exclude empty grades | 0       |
     And I turn editing mode off
-    And I set the field "Grade report" to "Set up grades layout"
+    And I set the field "Grade report" to "Categories and items"
     And I set the field "Override weight of Test assignment one" to "1"
     And I set the field "Weight of Test assignment one" to "0"
     And I set the field "Override weight of Test assignment six" to "1"
@@ -456,7 +456,7 @@ Feature: We can use calculated grade totals
     And I set the field "Override weight of Test assignment nine" to "1"
     And I set the field "Weight of Test assignment nine" to "100"
     And I press "Save changes"
-    And I navigate to "Course grade settings" node in "Grade administration > Settings"
+    And I navigate to "Course grade settings" node in "Grade administration > Setup"
     And I set the field "report_overview_showtotalsifcontainhidden" to "Show totals excluding hidden items"
     And I set the field "report_user_showtotalsifcontainhidden" to "Show totals excluding hidden items"
     And I set the field "Show contribution to course total" to "Show"
index ccab3c0..fc61a26 100644 (file)
@@ -39,11 +39,11 @@ Feature: We can understand the gradebook user report
     And I give the grade "70.00" to the user "Student 1" for the grade item "Test assignment five"
     And I give the grade "30.00" to the user "Student 1" for the grade item "Test assignment six"
     And I press "Save changes"
-    And I navigate to "Course grade settings" node in "Grade administration > Settings"
+    And I navigate to "Course grade settings" node in "Grade administration > Setup"
     And I set the field "Show weightings" to "Show"
     And I set the field "Show contribution to course total" to "Show"
     And I press "Save changes"
-    And I set the field "Grade report" to "Set up grades layout"
+    And I set the field "Grade report" to "Categories and items"
     And I press "Add category"
     And I set the field "Category name" to "Sub category"
     And I press "Save changes"
@@ -226,7 +226,7 @@ Feature: We can understand the gradebook user report
       | Test assignment five | 33.33 % | 70.00 | 23.33 |
       | Test assignment six | 33.33 % | 30.00 | 10.00 |
       | Category totalWeighted mean of grades. | 33.33 % | 36.67 | - |
-      | Course totalNatural. | - | 156.67 | - |
+      | Course total | - | 156.67 | - |
 
   @javascript
   Scenario: View user report with natural aggregation
@@ -244,5 +244,5 @@ Feature: We can understand the gradebook user report
       | Test assignment four | 33.33 % | 10.00 | 10.00 |
       | Test assignment five | 33.33 % | 70.00 | 70.00 |
       | Test assignment six | 33.33 % | 30.00 | 30.00 |
-      | Category totalNatural. | 60.00 % | 110.00 | - |
-      | Course totalNatural. | - | 230.00 | - |
+      | Category total | 60.00 % | 110.00 | - |
+      | Course total | - | 230.00 | - |
index 9be0f0b..05d8052 100644 (file)
@@ -28,7 +28,7 @@ Feature: We can use a minimum grade different than zero
     And I am on homepage
     And I follow "Course 1"
     And I follow "Grades"
-    And I navigate to "Set up grades layout" node in "Grade administration > Settings"
+    And I navigate to "Categories and items" node in "Grade administration > Setup"
     And I press "Add grade item"
     And I set the following fields to these values:
       | Item name | Manual item 1 |
@@ -66,14 +66,14 @@ Feature: We can use a minimum grade different than zero
       | Minimum grade | 50 |
       | Grade category | Sub category 2 |
     And I press "Save changes"
-    And I navigate to "Course grade settings" node in "Grade administration > Settings"
+    And I navigate to "Course grade settings" node in "Grade administration > Setup"
     And I set the field "Show weightings" to "Show"
     And I set the field "Show contribution to course total" to "Show"
     And I press "Save changes"
 
   @javascript
   Scenario: Natural aggregation with negative and positive grade
-    And I navigate to "Set up grades layout" node in "Grade administration > Settings"
+    And I navigate to "Categories and items" node in "Grade administration > Setup"
     And I set the following settings for grade item "Sub category 1":
       | Aggregation          | Natural |
       | Exclude empty grades | 0       |
index d806c40..5512a3f 100644 (file)
@@ -33,7 +33,7 @@ Feature: We can use natural aggregation and weights will be normalised to a tota
     And I log in as "teacher1"
     And I follow "Course 1"
     And I follow "Grades"
-    And I set the field "Grade report" to "Set up grades layout"
+    And I set the field "Grade report" to "Categories and items"
 
   @javascript
   Scenario: Setting all weights in a category to exactly one hundred in total.
index 3c76991..1421da7 100644 (file)
@@ -85,9 +85,9 @@ Feature: View gradebook when scales are used
     And the following should exist in the "user-grade" table:
       | Grade item          | Grade | Range | Percentage |
       | Test assignment one | C     | F–A   | 50.00 %    |
-      | Category totalNatural.      | 3.00  | 0–5   | 60.00 %    |
-      | Course totalNatural.        | 3.00  | 0–5   | 60.00 %    |
-    And I set the field "jump" to "Set up grades layout"
+      | Category total      | 3.00  | 0–5   | 60.00 %    |
+      | Course total        | 3.00  | 0–5   | 60.00 %    |
+    And I set the field "jump" to "Categories and items"
     And the following should exist in the "grade_edit_tree_table" table:
       | Name                | Max grade |
       | Test assignment one | 5.00      |
@@ -100,8 +100,8 @@ Feature: View gradebook when scales are used
     And the following should exist in the "user-grade" table:
       | Grade item          | Grade | Range | Percentage |
       | Test assignment one | B     | F–A   | 75.00 %    |
-      | Category totalNatural.      | 4.00  | 0–5   | 80.00 %    |
-      | Course totalNatural.        | 4.00  | 0–5   | 80.00 %    |
+      | Category total      | 4.00  | 0–5   | 80.00 %    |
+      | Course total        | 4.00  | 0–5   | 80.00 %    |
 
   @javascript
   Scenario Outline: Test displaying scales in gradebook in all other aggregation methods
@@ -135,12 +135,12 @@ Feature: View gradebook when scales are used
       | Test assignment one | C              | F–A   | 50.00 %       |
       | Category total<aggregation>.      | 3.00           | 1–5   | 50.00 %       |
       | Course total<aggregation>.        | <coursetotal3> | 0–100 | <courseperc3> |
-    And I set the field "jump" to "Set up grades layout"
+    And I set the field "jump" to "Categories and items"
     And the following should exist in the "grade_edit_tree_table" table:
       | Name                | Max grade |
       | Test assignment one | A (5)     |
-      | Category total      |           |
-      | Course total        |           |
+      | Category total<aggregation>. |           |
+      | Course total<aggregation>.   |           |
     And I log out
     And I log in as "student2"
     And I follow "Course 1"
index cece51a..9ca28d4 100644 (file)
@@ -72,23 +72,23 @@ Feature: We can enter in grades and view reports from the gradebook
       | Grade item | Grade | Range | Percentage |
       | Test assignment name 1 | 80.00 | 0–100 | 80.00 % |
       | Test assignment name 2 | 90.00 | 0–100 | 90.00 % |
-      | Course totalNatural. | 170.00 | 0–200 | 85.00 % |
+      | Course total | 170.00 | 0–200 | 85.00 % |
     And the following should not exist in the "user-grade" table:
       | Grade item | Grade | Range | Percentage |
-      | Course totalNatural. | 90.00 | 0–100 | 90.00 % |
+      | Course total | 90.00 | 0–100 | 90.00 % |
     And I set the field "Grade report" to "Overview report"
     And "C1" row "Grade" column of "overview-grade" table should contain "170.00"
     And "C1" row "Grade" column of "overview-grade" table should not contain "90.00"
 
   @javascript
   Scenario: We can add a weighting to a grade item and it is displayed properly in the user report
-    When I set the field "Grade report" to "Set up grades layout"
+    When I set the field "Grade report" to "Categories and items"
     And I set the following settings for grade item "Course 1":
       | Aggregation | Weighted mean of grades |
     And I set the field "Extra credit value for Test assignment name" to "0.72"
     And I press "Save changes"
     And I set the field "Grade report" to "User report"
-    And I navigate to "Course grade settings" node in "Grade administration > Settings"
+    And I navigate to "Course grade settings" node in "Grade administration > Setup"
     And I set the following fields to these values:
       | Show weightings | Show |
     And I press "Save changes"
index c2b5e6a..a17e036 100644 (file)
@@ -4,8 +4,6 @@ hidden,core_portfolio
 hidden,core_question
 hidden,core_repository
 hidden,core_role
-categoriesanditems,core_grades
 simpleview,core_grades
 fullview,core_grades
 categoriesedit,core_grades
-edittree,core_grades
index fa07263..118d366 100644 (file)
@@ -116,6 +116,7 @@ $string['calculationsaved'] = 'Calculation saved';
 $string['calculationview'] = 'View calculation';
 $string['cannotaccessgroup'] = 'Can not access grades of selected group, sorry.';
 $string['categories'] = 'Categories';
+$string['categoriesanditems'] = 'Categories and items';
 $string['category'] = 'Category';
 $string['categoryedit'] = 'Edit category';
 $string['categoryname'] = 'Category name';
@@ -173,6 +174,7 @@ $string['editgradeletters'] = 'Edit grade letters';
 $string['editoutcome'] = 'Edit outcome';
 $string['editoutcomes'] = 'Edit outcomes';
 $string['editscale'] = 'Edit scale';
+$string['edittree'] = 'Setup';
 $string['editverbose'] = 'Edit {$a->category} {$a->itemmodule} {$a->itemname}';
 $string['enableajax'] = 'Enable AJAX';
 $string['enableajax_help'] = 'Adds a layer of AJAX functionality to the grader report, simplifying and speeding up common operations. Depends on Javascript being switched on at the user\'s browser level.';
@@ -194,8 +196,9 @@ $string['errorupdatinggradecategoryaggregateoutcomes'] = 'Error updating the "In
 $string['errorupdatinggradecategoryaggregatesubcats'] = 'Error updating the "Aggregate including subcategories" setting of grade category ID {$a->id}';
 $string['errorupdatinggradecategoryaggregation'] = 'Error updating the aggregation type of grade category ID {$a->id}';
 $string['errorupdatinggradeitemaggregationcoef'] = 'Error updating the aggregation coefficient (weight or extra credit) of grade item ID {$a->id}';
+$string['eventgradedeleted'] = 'Grade deleted';
 $string['eventgradeviewed'] = 'Grades were viewed in the gradebook';
-$string['eventusergraded'] = 'User grade edited in gradebook';
+$string['eventusergraded'] = 'User graded';
 $string['excluded'] = 'Excluded';
 $string['excluded_help'] = 'If ticked, the grade will not be included in any aggregation.';
 $string['expand'] = 'Expand category';
@@ -220,8 +223,6 @@ $string['feedbacksaved'] = 'Feedback saved';
 $string['feedbackview'] = 'View feedback';
 $string['finalgrade'] = 'Final grade';
 $string['finalgrade_help'] = 'If the overridden checkbox is ticked, a grade may be added or amended.';
-$string['fixedstudents'] = 'Static students column';
-$string['fixedstudents_help'] = 'Allows grades to scroll horizontally without losing sight of the students column, by making it static.';
 $string['forceoff'] = 'Force: Off';
 $string['forceon'] = 'Force: On';
 $string['forelementtypes'] = 'for the selected {$a}';
@@ -319,6 +320,7 @@ $string['gradetype_help'] = 'There are 4 grade types:
 * Text - Feedback only
 
 Only value and scale grade types may be aggregated. The grade type for an activity-based grade item is set on the activity settings page.';
+$string['gradevaluetoobig'] = 'One of the grade values is larger than the allowed grade maximum of {$a}';
 $string['gradeview'] = 'View grade';
 $string['gradeweighthelp'] = 'Grade weight help';
 $string['groupavg'] = 'Group average';
@@ -593,7 +595,6 @@ $string['setgradeletters'] = 'Set grade letters';
 $string['setpreferences'] = 'Set preferences';
 $string['setting'] = 'Setting';
 $string['settings'] = 'Settings';
-$string['setupgradeslayout'] = 'Set up grades layout';
 $string['setweights'] = 'Set weights';
 $string['showanalysisicon'] = 'Show grade analysis icon';
 $string['showanalysisicon_desc'] = 'Whether to show grade analysis icon by default. If the activity module supports it, the grade analysis icon links to a page with more detailed explanation of the grade and how it was obtained.';
@@ -739,8 +740,6 @@ $string['yourgrade'] = 'Your grade';
 
 // Deprecated since Moodle 2.8
 
-$string['categoriesanditems'] = 'Setup';
 $string['categoriesedit'] = 'Edit setup';
-$string['edittree'] = 'Setup';
 $string['fullview'] = 'Full view';
 $string['simpleview'] = 'Simple view';
diff --git a/lib/classes/event/grade_deleted.php b/lib/classes/event/grade_deleted.php
new file mode 100644 (file)
index 0000000..ffb6543
--- /dev/null
@@ -0,0 +1,134 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle 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 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle 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 Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Grade deleted event.
+ *
+ * @package    core
+ * @copyright  2014 Mark Nelson <markn@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\event;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Grade deleted event class.
+ *
+ * @property-read array $other {
+ *      Extra information about the event.
+ *
+ *      - int itemid: grade item id.
+ *      - bool overridden: is this a grade override?
+ *      - float finalgrade: (optional) the final grade value.
+ * }
+ *
+ * @package    core
+ * @since      Moodle 2.8
+ * @copyright  2014 Mark Nelson <markn@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class grade_deleted extends base {
+
+    /** @var \grade_grade $grade */
+    protected $grade;
+
+    /**
+     * Init method.
+     *
+     * @return void
+     */
+    protected function init() {
+        $this->data['objecttable'] = 'grade_grades';
+        $this->data['crud'] = 'd';
+        $this->data['edulevel'] = self::LEVEL_TEACHING;
+    }
+
+    /**
+     * Utility method to create new event.
+     *
+     * @param \grade_grade $grade
+     * @return user_graded
+     */
+    public static function create_from_grade(\grade_grade $grade) {
+        $event = self::create(array(
+            'objectid'      => $grade->id,
+            'context'       => \context_course::instance($grade->grade_item->courseid),
+            'relateduserid' => $grade->userid,
+            'other'         => array(
+                'itemid'     => $grade->itemid,
+                'overridden' => !empty($grade->overridden),
+                'finalgrade' => $grade->finalgrade),
+        ));
+        $event->grade = $grade;
+        return $event;
+    }
+
+    /**
+     * Get grade object.
+     *
+     * @throws \coding_exception
+     * @return \grade_grade
+     */
+    public function get_grade() {
+        if ($this->is_restored()) {
+            throw new \coding_exception('get_grade() is intended for event observers only');
+        }
+        return $this->grade;
+    }
+
+    /**
+     * Return localised event name.
+     *
+     * @return string
+     */
+    public static function get_name() {
+        return get_string('eventgradedeleted', 'core_grades');
+    }
+
+    /**
+     * Returns description of what happened.
+     *
+     * @return string
+     */
+    public function get_description() {
+        return "The user with id '$this->userid' deleted the grade with id '$this->objectid' for the user with " .
+            "id '$this->relateduserid' for the grade item with id '{$this->other['itemid']}'.";
+    }
+
+    /**
+     * Custom validation.
+     *
+     * @throws \coding_exception when validation does not pass.
+     * @return void
+     */
+    protected function validate_data() {
+        parent::validate_data();
+
+        if (!isset($this->relateduserid)) {
+            throw new \coding_exception('The \'relateduserid\' must be set.');
+        }
+
+        if (!isset($this->other['itemid'])) {
+            throw new \coding_exception('The \'itemid\' value must be set in other.');
+        }
+
+        if (!isset($this->other['overridden'])) {
+            throw new \coding_exception('The \'overridden\' value must be set in other.');
+        }
+    }
+}
index be8cb4f..334de7c 100644 (file)
@@ -1034,7 +1034,7 @@ class core_plugin_manager {
             ),
 
             'gradeimport' => array(
-                'csv', 'xml'
+                'csv', 'direct', 'xml'
             ),
 
             'gradereport' => array(
index 2c6b696..e01a493 100644 (file)
@@ -965,7 +965,7 @@ $services = array(
             'core_grades_get_grades',
             'core_grades_update_grades',
             'mod_forum_get_forums_by_courses',
-            'mod_forum_get_forum_discussions',
+            'mod_forum_get_forum_discussions_paginated',
             'mod_forum_get_forum_discussion_posts',
             'core_files_get_files',
             'core_message_get_messages'),
old mode 100644 (file)
new mode 100755 (executable)
similarity index 57%
rename from lib/google/auth/Google_Auth.php
rename to lib/google/Google/Auth/Abstract.php
index 010782d..0832df3
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-
-require_once "Google_AuthNone.php";
-require_once "Google_OAuth2.php";
+require_once "Google/Http/Request.php";
 
 /**
  * Abstract class for the Authentication in the API client
  * @author Chris Chabot <chabotc@google.com>
  *
  */
-abstract class Google_Auth {
-  abstract public function authenticate($service);
-  abstract public function sign(Google_HttpRequest $request);
-  abstract public function createAuthUrl($scope);
-
-  abstract public function getAccessToken();
-  abstract public function setAccessToken($accessToken);
-  abstract public function setDeveloperKey($developerKey);
-  abstract public function refreshToken($refreshToken);
-  abstract public function revokeToken();
+abstract class Google_Auth_Abstract
+{
+  /**
+   * An utility function that first calls $this->auth->sign($request) and then
+   * executes makeRequest() on that signed request. Used for when a request
+   * should be authenticated
+   * @param Google_Http_Request $request
+   * @return Google_Http_Request $request
+   */
+  abstract public function authenticatedRequest(Google_Http_Request $request);
+  abstract public function sign(Google_Http_Request $request);
 }
diff --git a/lib/google/Google/Auth/AppIdentity.php b/lib/google/Google/Auth/AppIdentity.php
new file mode 100755 (executable)
index 0000000..0be5917
--- /dev/null
@@ -0,0 +1,100 @@
+<?php
+/*
+ * Copyright 2014 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/*
+ * WARNING - this class depends on the Google App Engine PHP library
+ * which is 5.3 and above only, so if you include this in a PHP 5.2
+ * setup or one without 5.3 things will blow up.
+ */
+use google\appengine\api\app_identity\AppIdentityService;
+
+require_once "Google/Auth/Abstract.php";
+require_once "Google/Http/Request.php";
+
+/**
+ * Authentication via the Google App Engine App Identity service.
+ */
+class Google_Auth_AppIdentity extends Google_Auth_Abstract
+{
+  const CACHE_PREFIX = "Google_Auth_AppIdentity::";
+  const CACHE_LIFETIME = 1500;
+  private $key = null;
+  private $client;
+  private $token = false;
+  private $tokenScopes = false;
+
+  public function __construct(Google_Client $client, $config = null)
+  {
+    $this->client = $client;
+  }
+
+  /**
+   * Retrieve an access token for the scopes supplied.
+   */
+  public function authenticateForScope($scopes)
+  {
+    if ($this->token && $this->tokenScopes == $scopes) {
+      return $this->token;
+    }
+    $memcache = new Memcached();
+    $this->token = $memcache->get(self::CACHE_PREFIX . $scopes);
+    if (!$this->token) {
+      $this->token = AppIdentityService::getAccessToken($scopes);
+      if ($this->token) {
+        $memcache_key = self::CACHE_PREFIX;
+        if (is_string($scopes)) {
+          $memcache_key .= $scopes;
+        } else if (is_array($scopes)) {
+          $memcache_key .= implode(":", $scopes);
+        }
+        $memcache->set($memcache_key, $this->token, self::CACHE_LIFETIME);
+      }
+    }
+    $this->tokenScopes = $scopes;
+    return $this->token;
+  }
+
+  /**
+   * Perform an authenticated / signed apiHttpRequest.
+   * This function takes the apiHttpRequest, calls apiAuth->sign on it
+   * (which can modify the request in what ever way fits the auth mechanism)
+   * and then calls apiCurlIO::makeRequest on the signed request
+   *
+   * @param Google_Http_Request $request
+   * @return Google_Http_Request The resulting HTTP response including the
+   * responseHttpCode, responseHeaders and responseBody.
+   */
+  public function authenticatedRequest(Google_Http_Request $request)
+  {
+    $request = $this->sign($request);
+    return $this->io->makeRequest($request);
+  }
+
+  public function sign(Google_Http_Request $request)
+  {
+    if (!$this->token) {
+      // No token, so nothing to do.
+      return $request;
+    }
+    // Add the OAuth2 header to the request
+    $request->setRequestHeaders(
+        array('Authorization' => 'Bearer ' . $this->token['access_token'])
+    );
+
+    return $request;
+  }
+}
old mode 100644 (file)
new mode 100755 (executable)
similarity index 59%
rename from lib/google/auth/Google_AssertionCredentials.php
rename to lib/google/Google/Auth/AssertionCredentials.php
index 0d7aeb3..3db0a77
  * limitations under the License.
  */
 
+require_once "Google/Auth/OAuth2.php";
+require_once "Google/Signer/P12.php";
+require_once "Google/Utils.php";
+
 /**
  * Credentials object used for OAuth 2.0 Signed JWT assertion grants.
  *
  * @author Chirag Shah <chirags@google.com>
  */
-class Google_AssertionCredentials {
+class Google_Auth_AssertionCredentials
+{
   const MAX_TOKEN_LIFETIME_SECS = 3600;
 
   public $serviceAccountName;
@@ -28,7 +33,13 @@ class Google_AssertionCredentials {
   public $privateKey;
   public $privateKeyPassword;
   public $assertionType;
+  public $sub;
+  /**
+   * @deprecated
+   * @link http://tools.ietf.org/html/draft-ietf-oauth-json-web-token-06
+   */
   public $prn;
+  private $useCache;
 
   /**
    * @param $serviceAccountName
@@ -36,8 +47,10 @@ class Google_AssertionCredentials {
    * @param $privateKey
    * @param string $privateKeyPassword
    * @param string $assertionType
-   * @param bool|string $prn The email address of the user for which the
-   *               application is requesting delegated access.
+   * @param bool|string $sub The email address of the user for which the
+   *              application is requesting delegated access.
+   * @param bool useCache Whether to generate a cache key and allow
+   *              automatic caching of the generated token.
    */
   public function __construct(
       $serviceAccountName,
@@ -45,27 +58,51 @@ class Google_AssertionCredentials {
       $privateKey,
       $privateKeyPassword = 'notasecret',
       $assertionType = 'http://oauth.net/grant_type/jwt/1.0/bearer',
-      $prn = false) {
+      $sub = false,
+      $useCache = true
+  ) {
     $this->serviceAccountName = $serviceAccountName;
     $this->scopes = is_string($scopes) ? $scopes : implode(' ', $scopes);
     $this->privateKey = $privateKey;
     $this->privateKeyPassword = $privateKeyPassword;
     $this->assertionType = $assertionType;
-    $this->prn = $prn;
+    $this->sub = $sub;
+    $this->prn = $sub;
+    $this->useCache = $useCache;
+  }
+  
+  /**
+   * Generate a unique key to represent this credential.
+   * @return string
+   */
+  public function getCacheKey()
+  {
+    if (!$this->useCache) {
+      return false;
+    }
+    $h = $this->sub;
+    $h .= $this->assertionType;
+    $h .= $this->privateKey;
+    $h .= $this->scopes;
+    $h .= $this->serviceAccountName;
+    return md5($h);
   }
 
-  public function generateAssertion() {
+  public function generateAssertion()
+  {
     $now = time();
 
     $jwtParams = array(
-          'aud' => Google_OAuth2::OAUTH2_TOKEN_URI,
+          'aud' => Google_Auth_OAuth2::OAUTH2_TOKEN_URI,
           'scope' => $this->scopes,
           'iat' => $now,
           'exp' => $now + self::MAX_TOKEN_LIFETIME_SECS,
           'iss' => $this->serviceAccountName,
     );
 
-    if ($this->prn !== false) {
+    if ($this->sub !== false) {
+      $jwtParams['sub'] = $this->sub;
+    } else if ($this->prn !== false) {
       $jwtParams['prn'] = $this->prn;
     }
 
@@ -77,16 +114,22 @@ class Google_AssertionCredentials {
    * @param array $payload
    * @return string The signed JWT.
    */
-  private function makeSignedJwt($payload) {
+  private function makeSignedJwt($payload)
+  {
     $header = array('typ' => 'JWT', 'alg' => 'RS256');
 
+    $payload = json_encode($payload);
+    // Handle some overzealous escaping in PHP json that seemed to cause some errors
+    // with claimsets.
+    $payload = str_replace('\/', '/', $payload);
+
     $segments = array(
       Google_Utils::urlSafeB64Encode(json_encode($header)),
-      Google_Utils::urlSafeB64Encode(json_encode($payload))
+      Google_Utils::urlSafeB64Encode($payload)
     );
 
     $signingInput = implode('.', $segments);
-    $signer = new Google_P12Signer($this->privateKey, $this->privateKeyPassword);
+    $signer = new Google_Signer_P12($this->privateKey, $this->privateKeyPassword);
     $signature = $signer->sign($signingInput);
     $segments[] = Google_Utils::urlSafeB64Encode($signature);
 
diff --git a/lib/google/Google/Auth/Exception.php b/lib/google/Google/Auth/Exception.php
new file mode 100755 (executable)
index 0000000..65067ee
--- /dev/null
@@ -0,0 +1,22 @@
+<?php
+/*
+ * Copyright 2013 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+require_once "Google/Exception.php";
+
+class Google_Auth_Exception extends Google_Exception
+{
+}
old mode 100644 (file)
new mode 100755 (executable)
similarity index 82%
rename from lib/google/auth/Google_LoginTicket.php
rename to lib/google/Google/Auth/LoginTicket.php
index c0ce614..bcf798a
  * limitations under the License.
  */
 
+require_once "Google/Auth/Exception.php";
+
 /**
  * Class to hold information about an authenticated login.
  *
  * @author Brian Eaton <beaton@google.com>
  */
-class Google_LoginTicket {
-  const USER_ATTR = "id";
+class Google_Auth_LoginTicket
+{
+  const USER_ATTR = "sub";
 
   // Information from id token envelope.
   private $envelope;
@@ -35,21 +38,23 @@ class Google_LoginTicket {
    * @param string $envelope Header from a verified authentication token.
    * @param string $payload Information from a verified authentication token.
    */
-  public function __construct($envelope, $payload) {
+  public function __construct($envelope, $payload)
+  {
     $this->envelope = $envelope;
     $this->payload = $payload;
   }
 
   /**
    * Returns the numeric identifier for the user.
-   * @throws Google_AuthException
+   * @throws Google_Auth_Exception
    * @return
    */
-  public function getUserId() {
+  public function getUserId()
+  {
     if (array_key_exists(self::USER_ATTR, $this->payload)) {
       return $this->payload[self::USER_ATTR];
     }
-    throw new Google_AuthException("No user_id in token");
+    throw new Google_Auth_Exception("No user_id in token");
   }
 
   /**
@@ -57,7 +62,8 @@ class Google_LoginTicket {
    * various information about the user session.
    * @return array
    */
-  public function getAttributes() {
+  public function getAttributes()
+  {
     return array("envelope" => $this->envelope, "payload" => $this->payload);
   }
 }
diff --git a/lib/google/Google/Auth/OAuth2.php b/lib/google/Google/Auth/OAuth2.php
new file mode 100755 (executable)
index 0000000..e5b6031
--- /dev/null
@@ -0,0 +1,595 @@
+<?php
+/*
+ * Copyright 2008 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+require_once "Google/Auth/Abstract.php";
+require_once "Google/Auth/AssertionCredentials.php";
+require_once "Google/Auth/Exception.php";
+require_once "Google/Auth/LoginTicket.php";
+require_once "Google/Client.php";
+require_once "Google/Http/Request.php";
+require_once "Google/Utils.php";
+require_once "Google/Verifier/Pem.php";
+
+/**
+ * Authentication class that deals with the OAuth 2 web-server authentication flow
+ *
+ * @author Chris Chabot <chabotc@google.com>
+ * @author Chirag Shah <chirags@google.com>
+ *
+ */
+class Google_Auth_OAuth2 extends Google_Auth_Abstract
+{
+  const OAUTH2_REVOKE_URI = 'https://accounts.google.com/o/oauth2/revoke';
+  const OAUTH2_TOKEN_URI = 'https://accounts.google.com/o/oauth2/token';
+  const OAUTH2_AUTH_URL = 'https://accounts.google.com/o/oauth2/auth';
+  const CLOCK_SKEW_SECS = 300; // five minutes in seconds
+  const AUTH_TOKEN_LIFETIME_SECS = 300; // five minutes in seconds
+  const MAX_TOKEN_LIFETIME_SECS = 86400; // one day in seconds
+  const OAUTH2_ISSUER = 'accounts.google.com';
+
+  /** @var Google_Auth_AssertionCredentials $assertionCredentials */
+  private $assertionCredentials;
+
+  /**
+   * @var string The state parameters for CSRF and other forgery protection.
+   */
+  private $state;
+
+  /**
+   * @var array The token bundle.
+   */
+  private $token = array();
+
+  /**
+   * @var Google_Client the base client
+   */
+  private $client;
+
+  /**
+   * Instantiates the class, but does not initiate the login flow, leaving it
+   * to the discretion of the caller.
+   */
+  public function __construct(Google_Client $client)
+  {
+    $this->client = $client;
+  }
+
+  /**
+   * Perform an authenticated / signed apiHttpRequest.
+   * This function takes the apiHttpRequest, calls apiAuth->sign on it
+   * (which can modify the request in what ever way fits the auth mechanism)
+   * and then calls apiCurlIO::makeRequest on the signed request
+   *
+   * @param Google_Http_Request $request
+   * @return Google_Http_Request The resulting HTTP response including the
+   * responseHttpCode, responseHeaders and responseBody.
+   */
+  public function authenticatedRequest(Google_Http_Request $request)
+  {
+    $request = $this->sign($request);
+    return $this->client->getIo()->makeRequest($request);
+  }
+
+  /**
+   * @param string $code
+   * @throws Google_Auth_Exception
+   * @return string
+   */
+  public function authenticate($code)
+  {
+    if (strlen($code) == 0) {
+      throw new Google_Auth_Exception("Invalid code");
+    }
+
+    // We got here from the redirect from a successful authorization grant,
+    // fetch the access token
+    $request = new Google_Http_Request(
+        self::OAUTH2_TOKEN_URI,
+        'POST',
+        array(),
+        array(
+          'code' => $code,
+          'grant_type' => 'authorization_code',
+          'redirect_uri' => $this->client->getClassConfig($this, 'redirect_uri'),
+          'client_id' => $this->client->getClassConfig($this, 'client_id'),
+          'client_secret' => $this->client->getClassConfig($this, 'client_secret')
+        )
+    );
+    $request->disableGzip();
+    $response = $this->client->getIo()->makeRequest($request);
+
+    if ($response->getResponseHttpCode() == 200) {
+      $this->setAccessToken($response->getResponseBody());
+      $this->token['created'] = time();
+      return $this->getAccessToken();
+    } else {
+      $decodedResponse = json_decode($response->getResponseBody(), true);
+      if ($decodedResponse != null && $decodedResponse['error']) {
+        $decodedResponse = $decodedResponse['error'];
+      }
+      throw new Google_Auth_Exception(
+          sprintf(
+              "Error fetching OAuth2 access token, message: '%s'",
+              $decodedResponse
+          ),
+          $response->getResponseHttpCode()
+      );
+    }
+  }
+
+  /**
+   * Create a URL to obtain user authorization.
+   * The authorization endpoint allows the user to first
+   * authenticate, and then grant/deny the access request.
+   * @param string $scope The scope is expressed as a list of space-delimited strings.
+   * @return string
+   */
+  public function createAuthUrl($scope)
+  {
+    $params = array(
+        'response_type' => 'code',
+        'redirect_uri' => $this->client->getClassConfig($this, 'redirect_uri'),
+        'client_id' => $this->client->getClassConfig($this, 'client_id'),
+        'scope' => $scope,
+        'access_type' => $this->client->getClassConfig($this, 'access_type'),
+        'approval_prompt' => $this->client->getClassConfig($this, 'approval_prompt'),
+    );
+
+    $login_hint = $this->client->getClassConfig($this, 'login_hint');
+    if ($login_hint != '') {
+      $params['login_hint'] = $login_hint;
+    }
+
+    // If the list of scopes contains plus.login, add request_visible_actions
+    // to auth URL.
+    $rva = $this->client->getClassConfig($this, 'request_visible_actions');
+    if (strpos($scope, 'plus.login') && strlen($rva) > 0) {
+        $params['request_visible_actions'] = $rva;
+    }
+
+    if (isset($this->state)) {
+      $params['state'] = $this->state;
+    }
+
+    return self::OAUTH2_AUTH_URL . "?" . http_build_query($params, '', '&');
+  }
+
+  /**
+   * @param string $token
+   * @throws Google_Auth_Exception
+   */
+  public function setAccessToken($token)
+  {
+    $token = json_decode($token, true);
+    if ($token == null) {
+      throw new Google_Auth_Exception('Could not json decode the token');
+    }
+    if (! isset($token['access_token'])) {
+      throw new Google_Auth_Exception("Invalid token format");
+    }
+    $this->token = $token;
+  }
+
+  public function getAccessToken()
+  {
+    return json_encode($this->token);
+  }
+
+  public function setState($state)
+  {
+    $this->state = $state;
+  }
+
+  public function setAssertionCredentials(Google_Auth_AssertionCredentials $creds)
+  {
+    $this->assertionCredentials = $creds;
+  }
+
+  /**
+   * Include an accessToken in a given apiHttpRequest.
+   * @param Google_Http_Request $request
+   * @return Google_Http_Request
+   * @throws Google_Auth_Exception
+   */
+  public function sign(Google_Http_Request $request)
+  {
+    // add the developer key to the request before signing it
+    if ($this->client->getClassConfig($this, 'developer_key')) {
+      $request->setQueryParam('key', $this->client->getClassConfig($this, 'developer_key'));
+    }
+
+    // Cannot sign the request without an OAuth access token.
+    if (null == $this->token && null == $this->assertionCredentials) {
+      return $request;
+    }
+
+    // Check if the token is set to expire in the next 30 seconds
+    // (or has already expired).
+    if ($this->isAccessTokenExpired()) {
+      if ($this->assertionCredentials) {
+        $this->refreshTokenWithAssertion();
+      } else {
+        if (! array_key_exists('refresh_token', $this->token)) {
+            throw new Google_Auth_Exception(
+                "The OAuth 2.0 access token has expired,"
+                ." and a refresh token is not available. Refresh tokens"
+                ." are not returned for responses that were auto-approved."
+            );
+        }
+        $this->refreshToken($this->token['refresh_token']);
+      }
+    }
+
+    // Add the OAuth2 header to the request
+    $request->setRequestHeaders(
+        array('Authorization' => 'Bearer ' . $this->token['access_token'])
+    );
+
+    return $request;
+  }
+
+  /**
+   * Fetches a fresh access token with the given refresh token.
+   * @param string $refreshToken
+   * @return void
+   */
+  public function refreshToken($refreshToken)
+  {
+    $this->refreshTokenRequest(
+        array(
+          'client_id' => $this->client->getClassConfig($this, 'client_id'),
+          'client_secret' => $this->client->getClassConfig($this, 'client_secret'),
+          'refresh_token' => $refreshToken,
+          'grant_type' => 'refresh_token'
+        )
+    );
+  }
+
+  /**
+   * Fetches a fresh access token with a given assertion token.
+   * @param Google_Auth_AssertionCredentials $assertionCredentials optional.
+   * @return void
+   */
+  public function refreshTokenWithAssertion($assertionCredentials = null)
+  {
+    if (!$assertionCredentials) {
+      $assertionCredentials = $this->assertionCredentials;
+    }
+
+    $cacheKey = $assertionCredentials->getCacheKey();
+
+    if ($cacheKey) {
+      // We can check whether we have a token available in the
+      // cache. If it is expired, we can retrieve a new one from
+      // the assertion.
+      $token = $this->client->getCache()->get($cacheKey);
+      if ($token) {
+        $this->setAccessToken($token);
+      }
+      if (!$this->isAccessTokenExpired()) {
+        return;
+      }
+    }
+
+    $this->refreshTokenRequest(
+        array(
+          'grant_type' => 'assertion',
+          'assertion_type' => $assertionCredentials->assertionType,
+          'assertion' => $assertionCredentials->generateAssertion(),
+        )
+    );
+
+    if ($cacheKey) {
+      // Attempt to cache the token.
+      $this->client->getCache()->set(
+          $cacheKey,
+          $this->getAccessToken()
+      );
+    }
+  }
+
+  private function refreshTokenRequest($params)
+  {
+    $http = new Google_Http_Request(
+        self::OAUTH2_TOKEN_URI,
+        'POST',
+        array(),
+        $params
+    );
+    $http->disableGzip();
+    $request = $this->client->getIo()->makeRequest($http);
+
+    $code = $request->getResponseHttpCode();
+    $body = $request->getResponseBody();
+    if (200 == $code) {
+      $token = json_decode($body, true);
+      if ($token == null) {
+        throw new Google_Auth_Exception("Could not json decode the access token");
+      }
+
+      if (! isset($token['access_token']) || ! isset($token['expires_in'])) {
+        throw new Google_Auth_Exception("Invalid token format");
+      }
+
+      if (isset($token['id_token'])) {
+        $this->token['id_token'] = $token['id_token'];
+      }
+      $this->token['access_token'] = $token['access_token'];
+      $this->token['expires_in'] = $token['expires_in'];
+      $this->token['created'] = time();
+    } else {
+      throw new Google_Auth_Exception("Error refreshing the OAuth2 token, message: '$body'", $code);
+    }
+  }
+
+  /**
+   * Revoke an OAuth2 access token or refresh token. This method will revoke the current access
+   * token, if a token isn't provided.
+   * @throws Google_Auth_Exception
+   * @param string|null $token The token (access token or a refresh token) that should be revoked.
+   * @return boolean Returns True if the revocation was successful, otherwise False.
+   */
+  public function revokeToken($token = null)
+  {
+    if (!$token) {
+      if (!$this->token) {
+        // Not initialized, no token to actually revoke
+        return false;
+      } elseif (array_key_exists('refresh_token', $this->token)) {
+        $token = $this->token['refresh_token'];
+      } else {
+        $token = $this->token['access_token'];
+      }
+    }
+    $request = new Google_Http_Request(
+        self::OAUTH2_REVOKE_URI,
+        'POST',
+        array(),
+        "token=$token"
+    );
+    $request->disableGzip();
+    $response = $this->client->getIo()->makeRequest($request);
+    $code = $response->getResponseHttpCode();
+    if ($code == 200) {
+      $this->token = null;
+      return true;
+    }
+
+    return false;
+  }
+
+  /**
+   * Returns if the access_token is expired.
+   * @return bool Returns True if the access_token is expired.
+   */
+  public function isAccessTokenExpired()
+  {
+    if (!$this->token || !isset($this->token['created'])) {
+      return true;
+    }
+
+    // If the token is set to expire in the next 30 seconds.
+    $expired = ($this->token['created']
+        + ($this->token['expires_in'] - 30)) < time();
+
+    return $expired;
+  }
+