Merge branch 'wip-MDL-46585-master' of git://github.com/marinaglancy/moodle
authorDan Poltawski <dan@moodle.com>
Mon, 15 Sep 2014 12:00:24 +0000 (13:00 +0100)
committerDan Poltawski <dan@moodle.com>
Mon, 15 Sep 2014 12:00:24 +0000 (13:00 +0100)
44 files changed:
admin/settings/plugins.php
availability/classes/info_module.php
calendar/lib.php
cohort/index.php
cohort/lib.php
cohort/tests/behat/upload_cohorts.feature [new file with mode: 0644]
cohort/tests/fixtures/uploadcohorts1.csv [new file with mode: 0644]
cohort/tests/fixtures/uploadcohorts2.csv [new file with mode: 0644]
cohort/tests/fixtures/uploadcohorts3.csv [new file with mode: 0644]
cohort/tests/fixtures/uploadcohorts_test.csv [new file with mode: 0644]
cohort/upload.php [new file with mode: 0644]
cohort/upload_form.php [new file with mode: 0644]
grade/export/grade_export_form.php
grade/export/lib.php
grade/export/ods/export.php
grade/export/ods/grade_export_ods.php
grade/export/ods/index.php
grade/export/txt/export.php
grade/export/txt/grade_export_txt.php
grade/export/txt/index.php
grade/export/txt/tests/behat/export.feature
grade/export/upgrade.txt
grade/export/xls/export.php
grade/export/xls/grade_export_xls.php
grade/export/xls/index.php
grade/export/xml/export.php
grade/export/xml/grade_export_xml.php
grade/export/xml/index.php
grade/report/history/classes/filter_form.php
grade/report/history/lang/en/gradereport_history.php
lang/en/cohort.php
lang/en/grades.php
lang/en/question.php
lib/classes/plugininfo/tool.php
lib/tests/behat/behat_general.php
mod/assign/tests/locallib_test.php
mod/lti/view.php
question/behaviour/deferredcbm/lang/en/qbehaviour_deferredcbm.php
question/behaviour/deferredcbm/renderer.php
question/format/gift/format.php
question/format/gift/tests/behat/import.feature [new file with mode: 0644]
question/format/gift/tests/giftformat_test.php
question/format/xml/tests/behat/import.feature [new file with mode: 0644]
repository/skydrive/microsoftliveapi.php

index 3e2fde5..bee9753 100644 (file)
@@ -358,7 +358,7 @@ if ($hassiteconfig || has_capability('moodle/question:config', $systemcontext))
         2 => get_string('showmarkandmax', 'question'),
     );
     $settings->add(new admin_setting_configselect('question_preview/marks',
-            get_string('marks', 'question'), '', 1, $marksoptions));
+            get_string('marks', 'question'), '', 2, $marksoptions));
 
     $settings->add(new admin_setting_configselect('question_preview/markdp',
             get_string('decimalplacesingrades', 'question'), '', 2, array(0, 1, 2, 3, 4, 5, 6, 7)));
index 4091c74..201b7dc 100644 (file)
@@ -205,6 +205,13 @@ class info_module extends info {
         // As a result we cannot take short cuts any longer and must get
         // standard modinfo.
         $modinfo = get_fast_modinfo($cm->course, $userid);
-        return $modinfo->get_cm($cm->id)->uservisible;
+        $cms = $modinfo->get_cms();
+        if (!isset($cms[$cm->id])) {
+            // In some cases this might get called with a cmid that is no longer
+            // available, for example when a module is hidden at system level.
+            debugging('info_module::is_user_visible called with invalid cmid ' . $cm->id, DEBUG_DEVELOPER);
+            return false;
+        }
+        return $cms[$cm->id]->uservisible;
     }
 }
index dcedc54..8bb2a63 100644 (file)
@@ -795,7 +795,7 @@ function calendar_get_events_by_id($eventids) {
  * @return string $content return available control for the calender in html
  */
 function calendar_top_controls($type, $data) {
-    global $PAGE;
+    global $PAGE, $OUTPUT;
 
     // Get the calendar type we are using.
     $calendartype = \core_calendar\type_factory::get_calendar_instance();
@@ -923,7 +923,8 @@ function calendar_top_controls($type, $data) {
             }
 
             $content .= html_writer::start_tag('div', array('class'=>'calendar-controls'));
-            $content .= $left . '<span class="hide"> | </span><h1 class="current">'.userdate($time, get_string('strftimemonthyear'))."</h1>";
+            $content .= $left . '<span class="hide"> | </span>';
+            $content .= $OUTPUT->heading(userdate($time, get_string('strftimemonthyear')), 2, 'current');
             $content .= '<span class="hide"> | </span>' . $right;
             $content .= '<span class="clearer"><!-- --></span>';
             $content .= html_writer::end_tag('div')."\n";
index 796dd03..f139e23 100644 (file)
@@ -25,7 +25,6 @@
 require('../config.php');
 require($CFG->dirroot.'/cohort/lib.php');
 require_once($CFG->libdir.'/adminlib.php');
-require_once($CFG->libdir.'/coursecatlib.php');
 
 $contextid = optional_param('contextid', 0, PARAM_INT);
 $page = optional_param('page', 0, PARAM_INT);
@@ -124,10 +123,10 @@ foreach($cohorts['cohorts'] as $cohort) {
     $cohortcontext = context::instance_by_id($cohort->contextid);
     if ($showall) {
         if ($cohortcontext->contextlevel == CONTEXT_COURSECAT) {
-            $cat = coursecat::get($cohortcontext->instanceid);
-            $line[] = html_writer::link(new moodle_url('/cohort/index.php' , array('contextid' => $cohort->contextid)), $cat->get_formatted_name());
+            $line[] = html_writer::link(new moodle_url('/cohort/index.php' ,
+                    array('contextid' => $cohort->contextid)), $cohortcontext->get_context_name(false));
         } else {
-            $line[] = get_string('coresystem');
+            $line[] = $cohortcontext->get_context_name(false);
         }
     }
     $line[] = format_string($cohort->name);
index 0110e9a..74d1553 100644 (file)
@@ -418,6 +418,12 @@ function cohort_edit_controls(context $context, moodle_url $currenturl) {
         if ($currenturl->get_path() === $addurl->get_path() && !$currenturl->param('id')) {
             $currenttab = 'addcohort';
         }
+
+        $uploadurl = new moodle_url('/cohort/upload.php', array('contextid' => $context->id));
+        $tabs[] = new tabobject('uploadcohorts', $uploadurl, get_string('uploadcohorts', 'cohort'));
+        if ($currenturl->get_path() === $uploadurl->get_path()) {
+            $currenttab = 'uploadcohorts';
+        }
     }
     if (count($tabs) > 1) {
         return new tabtree($tabs, $currenttab);
diff --git a/cohort/tests/behat/upload_cohorts.feature b/cohort/tests/behat/upload_cohorts.feature
new file mode 100644 (file)
index 0000000..2ca6592
--- /dev/null
@@ -0,0 +1,156 @@
+@core @core_cohort @_file_upload
+Feature: A privileged user can create cohorts using a CSV file
+  In order to create cohorts using a CSV file
+  As an admin
+  I need to be able to upload a CSV file and navigate through the upload process
+
+  Background:
+    Given the following "categories" exist:
+      | name  | category | idnumber |
+      | Cat 1 | 0        | CAT1     |
+      | Cat 2 | 0        | CAT2     |
+      | Cat 3 | CAT1     | CAT3     |
+
+  @javascript
+  Scenario: Upload cohorts with default System context as admin
+    When I log in as "admin"
+    And I navigate to "Cohorts" node in "Site administration > Users > Accounts"
+    And I follow "Upload cohorts"
+    And I upload "cohort/tests/fixtures/uploadcohorts1.csv" file to "File" filemanager
+    And I click on "Preview" "button"
+    Then the following should exist in the "previewuploadedcohorts" table:
+      | name          | idnumber  | description       | Context       | Status |
+      | cohort name 1 | cohortid1 | first description | System        |        |
+      | cohort name 2 | cohortid2 |                   | System        |        |
+      | cohort name 3 | cohortid3 |                   | Miscellaneous |        |
+      | cohort name 4 | cohortid4 |                   | Cat 1         |        |
+      | cohort name 5 | cohortid5 |                   | Cat 2         |        |
+      | cohort name 6 | cohortid6 |                   | Cat 3         |        |
+    And I press "Upload cohorts"
+    And I should see "Uploaded 6 cohorts"
+    And I press "Continue"
+    And the following should exist in the "cohorts" table:
+      | Name          | Cohort ID | Description | Cohort size | Source           |
+      | cohort name 1 | cohortid1 | first description | 0           | Created manually |
+      | cohort name 2 | cohortid2 |             | 0           | Created manually |
+    And I follow "All cohorts"
+    And the following should exist in the "cohorts" table:
+      | Category      | Name          | Cohort ID | Description       | Cohort size | Source           |
+      | System        | cohort name 1 | cohortid1 | first description | 0           | Created manually |
+      | System        | cohort name 2 | cohortid2 |                   | 0           | Created manually |
+      | Miscellaneous | cohort name 3 | cohortid3 |                   | 0           | Created manually |
+      | Cat 1         | cohort name 4 | cohortid4 |                   | 0           | Created manually |
+      | Cat 2         | cohort name 5 | cohortid5 |                   | 0           | Created manually |
+      | Cat 3         | cohort name 6 | cohortid6 |                   | 0           | Created manually |
+
+  @javascript
+  Scenario: Upload cohorts with default category context as admin
+    When I log in as "admin"
+    And I navigate to "Cohorts" node in "Site administration > Users > Accounts"
+    And I follow "Upload cohorts"
+    And I upload "cohort/tests/fixtures/uploadcohorts1.csv" file to "File" filemanager
+    And I set the field "Default context" to "Cat 1 / Cat 3"
+    And I click on "Preview" "button"
+    Then the following should exist in the "previewuploadedcohorts" table:
+      | name          | idnumber  | description       | Context                 | Status |
+      | cohort name 1 | cohortid1 | first description | Cat 3         |        |
+      | cohort name 2 | cohortid2 |                   | Cat 3         |        |
+      | cohort name 3 | cohortid3 |                   | Miscellaneous |        |
+      | cohort name 4 | cohortid4 |                   | Cat 1         |        |
+      | cohort name 5 | cohortid5 |                   | Cat 2         |        |
+      | cohort name 6 | cohortid6 |                   | Cat 3         |        |
+    And I press "Upload cohorts"
+    And I should see "Uploaded 6 cohorts"
+    And I press "Continue"
+    And I should see "Category: Cat 3: available cohorts (3)"
+    And I navigate to "Cohorts" node in "Site administration > Users > Accounts"
+    And I follow "All cohorts"
+    And the following should exist in the "cohorts" table:
+      | Category      | Name          | Cohort ID | Description       | Cohort size | Source           |
+      | Cat 3         | cohort name 1 | cohortid1 | first description | 0           | Created manually |
+      | Cat 3         | cohort name 2 | cohortid2 |                   | 0           | Created manually |
+      | Miscellaneous | cohort name 3 | cohortid3 |                   | 0           | Created manually |
+      | Cat 1         | cohort name 4 | cohortid4 |                   | 0           | Created manually |
+      | Cat 2         | cohort name 5 | cohortid5 |                   | 0           | Created manually |
+      | Cat 3         | cohort name 6 | cohortid6 |                   | 0           | Created manually |
+
+  @javascript
+  Scenario: Upload cohorts with default category context as manager
+    Given the following "users" exist:
+      | username | firstname | lastname | email                  |
+      | user1    | User      | 1        | user1@moodlemoodle.com |
+    And the following "role assigns" exist:
+      | user  | role    | contextlevel | reference |
+      | user1 | manager | Category     | CAT1      |
+    When I log in as "user1"
+    And I follow "Courses"
+    And I follow "Cat 1"
+    And I navigate to "Cohorts" node in "Category: Cat 1"
+    And I follow "Upload cohorts"
+    And I upload "cohort/tests/fixtures/uploadcohorts1.csv" file to "File" filemanager
+    And I click on "Preview" "button"
+    Then the following should exist in the "previewuploadedcohorts" table:
+      | name          | idnumber  | description       | Context | Status |
+      | cohort name 1 | cohortid1 | first description | Cat 1   |        |
+      | cohort name 2 | cohortid2 |                   | Cat 1   |        |
+      | cohort name 3 | cohortid3 |                   | Cat 1   | Category Miscellaneous not found or you don't have permission to create a cohort there. The default context will be used. |
+      | cohort name 4 | cohortid4 |                   | Cat 1   |        |
+      | cohort name 5 | cohortid5 |                   | Cat 1   | Category CAT2 not found or you don't have permission to create a cohort there. The default context will be used. |
+      | cohort name 6 | cohortid6 |                   | Cat 3   |        |
+    And I press "Upload cohorts"
+    And I should see "Uploaded 6 cohorts"
+
+  @javascript
+  Scenario: Upload cohorts with conflicting id number
+    Given the following "cohorts" exist:
+      | name   | idnumber  |
+      | Cohort | cohortid2 |
+    When I log in as "admin"
+    And I navigate to "Cohorts" node in "Site administration > Users > Accounts"
+    And I follow "Upload cohorts"
+    And I upload "cohort/tests/fixtures/uploadcohorts1.csv" file to "File" filemanager
+    And I click on "Preview" "button"
+    Then I should see "Errors were found in CSV data. See details below."
+    Then the following should exist in the "previewuploadedcohorts" table:
+      | name | idnumber | description | Context | Status |
+      | cohort name 1 | cohortid1 | first description | System |  |
+      | cohort name 2 | cohortid2 |  | System | Cohort with the same ID number already exists |
+      | cohort name 3 | cohortid3 |  | Miscellaneous |  |
+      | cohort name 4 | cohortid4 |  | Cat 1 |  |
+      | cohort name 5 | cohortid5 |  | Cat 2 |  |
+      | cohort name 6 | cohortid6 |  | Cat 3 |  |
+    And "Upload cohorts" "button" should not exist
+
+  @javascript
+  Scenario: Upload cohorts with different ways of specifying context
+    When I log in as "admin"
+    And I navigate to "Cohorts" node in "Site administration > Users > Accounts"
+    And I follow "Upload cohorts"
+    And I upload "cohort/tests/fixtures/uploadcohorts2.csv" file to "File" filemanager
+    And I click on "Preview" "button"
+    Then the following should exist in the "previewuploadedcohorts" table:
+      | name                         | idnumber  | description | Context       | Status |
+      | Specify category as name     | cohortid1 |             | Miscellaneous |        |
+      | Specify category as idnumber | cohortid2 |             | Cat 1         |        |
+      | Specify category as id       | cohortid3 |             | Miscellaneous |        |
+      | Specify category as path     | cohortid4 |             | Cat 3         |        |
+      | Specify category_id          | cohortid5 |             | Miscellaneous |        |
+      | Specify category_idnumber    | cohortid6 |             | Cat 1         |        |
+      | Specify category_path        | cohortid7 |             | Cat 3         |        |
+    And I should not see "not found or you"
+    And I press "Upload cohorts"
+    And I should see "Uploaded 7 cohorts"
+    And I press "Continue"
+    And I follow "Upload cohorts"
+    And I upload "cohort/tests/fixtures/uploadcohorts3.csv" file to "File" filemanager
+    And I click on "Preview" "button"
+    And the following should exist in the "previewuploadedcohorts" table:
+      | name                                         | idnumber   | description | Context | Status |
+      | Specify context as id (system)               | cohortid8  |             | System  |        |
+      | Specify context as name (system)             | cohortid9  |             | System  |        |
+      | Specify context as category name only        | cohortid10 |             | Cat 1   |        |
+      | Specify context as category path             | cohortid12 |             | Cat 3   |        |
+      | Specify context as category idnumber         | cohortid13 |             | Cat 2   |        |
+    And I should not see "not found or you"
+    And I press "Upload cohorts"
+    And I should see "Uploaded 5 cohorts"
diff --git a/cohort/tests/fixtures/uploadcohorts1.csv b/cohort/tests/fixtures/uploadcohorts1.csv
new file mode 100644 (file)
index 0000000..b9ed12f
--- /dev/null
@@ -0,0 +1,7 @@
+name,idnumber,description,category
+cohort name 1,cohortid1,first description,
+cohort name 2,cohortid2,,
+cohort name 3,cohortid3,,Miscellaneous
+cohort name 4,cohortid4,,CAT1
+cohort name 5,cohortid5,,CAT2
+cohort name 6,cohortid6,,CAT3
diff --git a/cohort/tests/fixtures/uploadcohorts2.csv b/cohort/tests/fixtures/uploadcohorts2.csv
new file mode 100644 (file)
index 0000000..f2fd9f3
--- /dev/null
@@ -0,0 +1,8 @@
+name,idnumber,description,category,category_id,category_idnumber,category_path
+Specify category as name,cohortid1,,Miscellaneous,,,
+Specify category as idnumber,cohortid2,,CAT1,,,
+Specify category as id,cohortid3,,1,,,
+Specify category as path,cohortid4,,Cat 1 / Cat 3,,,
+Specify category_id,cohortid5,,,1,,
+Specify category_idnumber,cohortid6,,,,CAT1,
+Specify category_path,cohortid7,,,,,Cat 1 / Cat 3
diff --git a/cohort/tests/fixtures/uploadcohorts3.csv b/cohort/tests/fixtures/uploadcohorts3.csv
new file mode 100644 (file)
index 0000000..9c6f40c
--- /dev/null
@@ -0,0 +1,6 @@
+name,idnumber,description,context
+Specify context as id (system),cohortid8,,1
+Specify context as name (system),cohortid9,,System
+Specify context as category name only,cohortid10,,Cat 1
+Specify context as category path,cohortid12,,Cat 1 / Cat 3
+Specify context as category idnumber,cohortid13,,CAT2
diff --git a/cohort/tests/fixtures/uploadcohorts_test.csv b/cohort/tests/fixtures/uploadcohorts_test.csv
new file mode 100644 (file)
index 0000000..567732c
--- /dev/null
@@ -0,0 +1,13 @@
+name,idnumber,description,category
+cohort name 1,cid1,first description,
+cohort name 2,cid2,,
+cohort name 3,cid3,,Miscellaneous
+cohort name 4,cid4,,CAT1
+cohort name 5,cid5,,CAT2
+cohort name 6,cid6,,CAT3
+cohort name 7,cid7,,CAT1
+cohort name 8,cid8,,CAT2
+cohort name 9,cid9,,CAT3
+cohort name 10,cid10,,CAT1
+cohort name 11,cid11,,CAT2
+cohort name 12,cid1,,CAT3
diff --git a/cohort/upload.php b/cohort/upload.php
new file mode 100644 (file)
index 0000000..d83b2ee
--- /dev/null
@@ -0,0 +1,93 @@
+<?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 form for cohort upload.
+ *
+ * @package    core_cohort
+ * @copyright  2014 Marina Glancy
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+require_once('../config.php');
+require_once($CFG->dirroot.'/cohort/lib.php');
+require_once($CFG->dirroot.'/cohort/upload_form.php');
+require_once($CFG->libdir . '/csvlib.class.php');
+
+$contextid = optional_param('contextid', 0, PARAM_INT);
+$returnurl = optional_param('returnurl', '', PARAM_URL);
+
+require_login();
+
+if ($contextid) {
+    $context = context::instance_by_id($contextid, MUST_EXIST);
+} else {
+    $context = context_system::instance();
+}
+if ($context->contextlevel != CONTEXT_COURSECAT && $context->contextlevel != CONTEXT_SYSTEM) {
+    print_error('invalidcontext');
+}
+
+require_capability('moodle/cohort:manage', $context);
+
+$PAGE->set_context($context);
+$baseurl = new moodle_url('/cohort/upload.php', array('contextid' => $context->id));
+$PAGE->set_url($baseurl);
+$PAGE->set_heading($COURSE->fullname);
+$PAGE->set_pagelayout('admin');
+
+if ($context->contextlevel == CONTEXT_COURSECAT) {
+    $PAGE->set_category_by_id($context->instanceid);
+    navigation_node::override_active_url(new moodle_url('/cohort/index.php', array('contextid' => $context->id)));
+} else {
+    navigation_node::override_active_url(new moodle_url('/cohort/index.php', array()));
+}
+
+$uploadform = new cohort_upload_form(null, array('contextid' => $context->id, 'returnurl' => $returnurl));
+
+if ($returnurl) {
+    $returnurl = new moodle_url($returnurl);
+} else {
+    $returnurl = new moodle_url('/cohort/index.php', array('contextid' => $context->id));
+}
+
+if ($uploadform->is_cancelled()) {
+    redirect($returnurl);
+}
+
+$strheading = get_string('uploadcohorts', 'cohort');
+$PAGE->navbar->add($strheading);
+
+echo $OUTPUT->header();
+echo $OUTPUT->heading_with_help($strheading, 'uploadcohorts', 'cohort');
+
+if ($editcontrols = cohort_edit_controls($context, $baseurl)) {
+    echo $OUTPUT->render($editcontrols);
+}
+
+if ($data = $uploadform->get_data()) {
+    $cohortsdata = $uploadform->get_cohorts_data();
+    foreach ($cohortsdata as $cohort) {
+        cohort_add_cohort($cohort);
+    }
+    echo $OUTPUT->notification(get_string('uploadedcohorts', 'cohort', count($cohortsdata)), 'notifysuccess');
+    echo $OUTPUT->continue_button($returnurl);
+} else {
+    $uploadform->display();
+}
+
+echo $OUTPUT->footer();
+
diff --git a/cohort/upload_form.php b/cohort/upload_form.php
new file mode 100644 (file)
index 0000000..fa041aa
--- /dev/null
@@ -0,0 +1,552 @@
+<?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 form for cohort upload.
+ *
+ * @package    core_cohort
+ * @copyright  2014 Marina Glancy
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once($CFG->libdir.'/formslib.php');
+
+/**
+ * Cohort upload form class
+ *
+ * @package    core_cohort
+ * @copyright  2014 Marina Glancy
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class cohort_upload_form extends moodleform {
+    /** @var array new cohorts that need to be created */
+    public $processeddata = null;
+    /** @var array cached list of available contexts */
+    protected $contextoptions = null;
+    /** @var array temporary cache for retrieved categories */
+    protected $categoriescache = array();
+
+    /**
+     * Form definition
+     */
+    public function definition() {
+        $mform = $this->_form;
+        $data  = (object)$this->_customdata;
+
+        $mform->addElement('hidden', 'returnurl');
+        $mform->setType('returnurl', PARAM_URL);
+
+        $mform->addElement('header', 'cohortfileuploadform', get_string('uploadafile'));
+
+        $filepickeroptions = array();
+        $filepickeroptions['filetypes'] = '*';
+        $filepickeroptions['maxbytes'] = get_max_upload_file_size();
+        $mform->addElement('filepicker', 'cohortfile', get_string('file'), null, $filepickeroptions);
+
+        $choices = csv_import_reader::get_delimiter_list();
+        $mform->addElement('select', 'delimiter', get_string('csvdelimiter', 'tool_uploadcourse'), $choices);
+        if (array_key_exists('cfg', $choices)) {
+            $mform->setDefault('delimiter', 'cfg');
+        } else if (get_string('listsep', 'langconfig') == ';') {
+            $mform->setDefault('delimiter', 'semicolon');
+        } else {
+            $mform->setDefault('delimiter', 'comma');
+        }
+        $mform->addHelpButton('delimiter', 'csvdelimiter', 'tool_uploadcourse');
+
+        $choices = core_text::get_encodings();
+        $mform->addElement('select', 'encoding', get_string('encoding', 'tool_uploadcourse'), $choices);
+        $mform->setDefault('encoding', 'UTF-8');
+        $mform->addHelpButton('encoding', 'encoding', 'tool_uploadcourse');
+
+        $options = $this->get_context_options();
+        $mform->addElement('select', 'contextid', get_string('defaultcontext', 'cohort'), $options);
+
+        $this->add_cohort_upload_buttons(true);
+        $this->set_data($data);
+    }
+
+    /**
+     * Add buttons to the form ("Upload cohorts", "Preview", "Cancel")
+     */
+    protected function add_cohort_upload_buttons() {
+        $mform = $this->_form;
+
+        $buttonarray = array();
+
+        $submitlabel = get_string('uploadcohorts', 'cohort');
+        $buttonarray[] = $mform->createElement('submit', 'submitbutton', $submitlabel);
+
+        $previewlabel = get_string('preview', 'cohort');
+        $buttonarray[] = $mform->createElement('submit', 'previewbutton', $previewlabel);
+        $mform->registerNoSubmitButton('previewbutton');
+
+        $buttonarray[] = $mform->createElement('cancel');
+
+        $mform->addGroup($buttonarray, 'buttonar', '', array(' '), false);
+        $mform->closeHeaderBefore('buttonar');
+    }
+
+    /**
+     * Process the uploaded file and allow the submit button only if it doest not have errors.
+     */
+    public function definition_after_data() {
+        $mform = $this->_form;
+        $cohortfile = $mform->getElementValue('cohortfile');
+        $allowsubmitform = false;
+        if ($cohortfile && ($file = $this->get_cohort_file($cohortfile))) {
+            // File was uploaded. Parse it.
+            $encoding = $mform->getElementValue('encoding')[0];
+            $delimiter = $mform->getElementValue('delimiter')[0];
+            $contextid = $mform->getElementValue('contextid')[0];
+            if (!empty($contextid) && ($context = context::instance_by_id($contextid, IGNORE_MISSING))) {
+                $this->processeddata = $this->process_upload_file($file, $encoding, $delimiter, $context);
+                if ($this->processeddata && count($this->processeddata) > 1 && !$this->processeddata[0]['errors']) {
+                    $allowsubmitform = true;
+                }
+            }
+        }
+        if (!$allowsubmitform) {
+            // Hide submit button.
+            $el = $mform->getElement('buttonar')->getElements()[0];
+            $el->setValue('');
+            $el->freeze();
+        } else {
+            $mform->setExpanded('cohortfileuploadform', false);
+        }
+
+    }
+
+    /**
+     * Returns the list of contexts where current user can create cohorts.
+     *
+     * @return array
+     */
+    protected function get_context_options() {
+        global $CFG;
+        require_once($CFG->libdir. '/coursecatlib.php');
+        if ($this->contextoptions === null) {
+            $this->contextoptions = array();
+            $displaylist = coursecat::make_categories_list('moodle/cohort:manage');
+            // We need to index the options array by context id instead of category id and add option for system context.
+            $syscontext = context_system::instance();
+            if (has_capability('moodle/cohort:manage', $syscontext)) {
+                $this->contextoptions[$syscontext->id] = $syscontext->get_context_name();
+            }
+            foreach ($displaylist as $cid => $name) {
+                $context = context_coursecat::instance($cid);
+                $this->contextoptions[$context->id] = $name;
+            }
+        }
+        return $this->contextoptions;
+    }
+
+    public function validation($data, $files) {
+        $errors = parent::validation($data, $files);
+        if (empty($errors)) {
+            if (empty($data['cohortfile']) || !($file = $this->get_cohort_file($data['cohortfile']))) {
+                $errors['cohortfile'] = get_string('required');
+            } else {
+                if (!empty($this->processeddata[0]['errors'])) {
+                    // Any value in $errors will notify that validation did not pass. The detailed errors will be shown in preview.
+                    $errors['dummy'] = '';
+                }
+            }
+        }
+        return $errors;
+    }
+
+    /**
+     * Returns the uploaded file if it is present.
+     *
+     * @param int $draftid
+     * @return stored_file|null
+     */
+    protected function get_cohort_file($draftid) {
+        global $USER;
+        // We can not use moodleform::get_file_content() method because we need the content before the form is validated.
+        if (!$draftid) {
+            return null;
+        }
+        $fs = get_file_storage();
+        $context = context_user::instance($USER->id);
+        if (!$files = $fs->get_area_files($context->id, 'user', 'draft', $draftid, 'id DESC', false)) {
+            return null;
+        }
+        $file = reset($files);
+
+        return $file;
+
+    }
+
+    /**
+     * Returns the list of prepared objects to be added as cohorts
+     *
+     * @return array of stdClass objects, each can be passed to {@link cohort_add_cohort()}
+     */
+    public function get_cohorts_data() {
+        $cohorts = array();
+        if ($this->processeddata) {
+            foreach ($this->processeddata as $idx => $line) {
+                if ($idx && !empty($line['data'])) {
+                    $cohorts[] = (object)$line['data'];
+                }
+            }
+        }
+        return $cohorts;
+    }
+
+    /**
+     * Displays the preview of the uploaded file
+     */
+    protected function preview_uploaded_cohorts() {
+        global $OUTPUT;
+        if (empty($this->processeddata)) {
+            return;
+        }
+        foreach ($this->processeddata[0]['errors'] as $error) {
+            echo $OUTPUT->notification($error);
+        }
+        foreach ($this->processeddata[0]['warnings'] as $warning) {
+            echo $OUTPUT->notification($warning, 'notifymessage');
+        }
+        $table = new html_table();
+        $table->id = 'previewuploadedcohorts';
+        $columns = $this->processeddata[0]['data'];
+        $columns['contextid'] = get_string('context', 'role');
+
+        // Add column names to the preview table.
+        $table->head = array('');
+        foreach ($columns as $key => $value) {
+            $table->head[] = $value;
+        }
+        $table->head[] = get_string('status');
+
+        // Add (some) rows to the preview table.
+        $previewdrows = $this->get_previewed_rows();
+        foreach ($previewdrows as $idx) {
+            $line = $this->processeddata[$idx];
+            $cells = array(new html_table_cell($idx));
+            $context = context::instance_by_id($line['data']['contextid']);
+            foreach ($columns as $key => $value) {
+                if ($key === 'contextid') {
+                    $text = html_writer::link(new moodle_url('/cohort/index.php', array('contextid' => $context->id)),
+                        $context->get_context_name(false));
+                } else {
+                    $text = s($line['data'][$key]);
+                }
+                $cells[] = new html_table_cell($text);
+            }
+            $text = '';
+            if ($line['errors']) {
+                $text .= html_writer::div(join('<br>', $line['errors']), 'notifyproblem');
+            }
+            if ($line['warnings']) {
+                $text .= html_writer::div(join('<br>', $line['warnings']));
+            }
+            $cells[] = new html_table_cell($text);
+            $table->data[] = new html_table_row($cells);
+        }
+        if ($notdisplayed = count($this->processeddata) - count($previewdrows) - 1) {
+            $cell = new html_table_cell(get_string('displayedrows', 'cohort',
+                (object)array('displayed' => count($previewdrows), 'total' => count($this->processeddata) - 1)));
+            $cell->colspan = count($columns) + 2;
+            $table->data[] = new html_table_row(array($cell));
+        }
+        echo html_writer::table($table);
+    }
+
+    /**
+     * Find up rows to show in preview
+     *
+     * Number of previewed rows is limited but rows with errors and warnings have priority.
+     *
+     * @return array
+     */
+    protected function get_previewed_rows() {
+        $previewlimit = 10;
+        if (count($this->processeddata) <= 1) {
+            $rows = array();
+        } else if (count($this->processeddata) < $previewlimit + 1) {
+            // Return all rows.
+            $rows = range(1, count($this->processeddata) - 1);
+        } else {
+            // First find rows with errors and warnings (no more than 10 of each).
+            $errorrows = $warningrows = array();
+            foreach ($this->processeddata as $rownum => $line) {
+                if ($rownum && $line['errors']) {
+                    $errorrows[] = $rownum;
+                    if (count($errorrows) >= $previewlimit) {
+                        return $errorrows;
+                    }
+                } else if ($rownum && $line['warnings']) {
+                    if (count($warningrows) + count($errorrows) < $previewlimit) {
+                        $warningrows[] = $rownum;
+                    }
+                }
+            }
+            // Include as many error rows as possible and top them up with warning rows.
+            $rows = array_merge($errorrows, array_slice($warningrows, 0, $previewlimit - count($errorrows)));
+            // Keep adding good rows until we reach limit.
+            for ($rownum = 1; count($rows) < $previewlimit; $rownum++) {
+                if (!in_array($rownum, $rows)) {
+                    $rows[] = $rownum;
+                }
+            }
+            asort($rows);
+        }
+        return $rows;
+    }
+
+    public function display() {
+        // Finalize the form definition if not yet done.
+        if (!$this->_definition_finalized) {
+            $this->_definition_finalized = true;
+            $this->definition_after_data();
+        }
+
+        // Difference from the parent display() method is that we want to show preview above the form if applicable.
+        $this->preview_uploaded_cohorts();
+
+        $this->_form->display();
+    }
+
+    /**
+     * @param stored_file $file
+     * @param string $encoding
+     * @param string $delimiter
+     * @param context $defaultcontext
+     * @return array
+     */
+    protected function process_upload_file($file, $encoding, $delimiter, $defaultcontext) {
+        global $CFG, $DB;
+        require_once($CFG->libdir . '/csvlib.class.php');
+
+        $cohorts = array(
+            0 => array('errors' => array(), 'warnings' => array(), 'data' => array())
+        );
+
+        // Read and parse the CSV file using csv library.
+        $content = $file->get_content();
+        if (!$content) {
+            $cohorts[0]['errors'][] = new lang_string('csvemptyfile', 'error');
+            return $cohorts;
+        }
+
+        $uploadid = csv_import_reader::get_new_iid('uploadcohort');
+        $cir = new csv_import_reader($uploadid, 'uploadcohort');
+        $readcount = $cir->load_csv_content($content, $encoding, $delimiter);
+        unset($content);
+        if (!$readcount) {
+            $cohorts[0]['errors'][] = get_string('csvloaderror', 'error', $cir->get_error());
+            return $cohorts;
+        }
+        $columns = $cir->get_columns();
+
+        // Check that columns include 'name' and warn about extra columns.
+        $allowedcolumns = array('contextid', 'name', 'idnumber', 'description', 'descriptionformat');
+        $additionalcolumns = array('context', 'category', 'category_id', 'category_idnumber', 'category_path');
+        $displaycolumns = array();
+        $extracolumns = array();
+        $columnsmapping = array();
+        foreach ($columns as $i => $columnname) {
+            $columnnamelower = preg_replace('/ /', '', core_text::strtolower($columnname));
+            $columnsmapping[$i] = null;
+            if (in_array($columnnamelower, $allowedcolumns)) {
+                $displaycolumns[$columnnamelower] = $columnname;
+                $columnsmapping[$i] = $columnnamelower;
+            } else if (in_array($columnnamelower, $additionalcolumns)) {
+                $columnsmapping[$i] = $columnnamelower;
+            } else {
+                $extracolumns[] = $columnname;
+            }
+        }
+        if (!in_array('name', $columnsmapping)) {
+            $cohorts[0]['errors'][] = new lang_string('namecolumnmissing', 'cohort');
+            return $cohorts;
+        }
+        if ($extracolumns) {
+            $cohorts[0]['warnings'][] = new lang_string('csvextracolumns', 'cohort', s(join(', ', $extracolumns)));
+        }
+
+        if (!isset($displaycolumns['contextid'])) {
+            $displaycolumns['contextid'] = 'contextid';
+        }
+        $cohorts[0]['data'] = $displaycolumns;
+
+        // Parse data rows.
+        $cir->init();
+        $rownum = 0;
+        $idnumbers = array();
+        $haserrors = false;
+        $haswarnings = false;
+        while ($row = $cir->next()) {
+            $rownum++;
+            $cohorts[$rownum] = array(
+                'errors' => array(),
+                'warnings' => array(),
+                'data' => array(),
+            );
+            $hash = array();
+            foreach ($row as $i => $value) {
+                if ($columnsmapping[$i]) {
+                    $hash[$columnsmapping[$i]] = $value;
+                }
+            }
+            $this->clean_cohort_data($hash);
+
+            $warnings = $this->resolve_context($hash, $defaultcontext);
+            $cohorts[$rownum]['warnings'] = array_merge($cohorts[$rownum]['warnings'], $warnings);
+
+            if (!empty($hash['idnumber'])) {
+                if (isset($idnumbers[$hash['idnumber']]) || $DB->record_exists('cohort', array('idnumber' => $hash['idnumber']))) {
+                    $cohorts[$rownum]['errors'][] = new lang_string('duplicateidnumber', 'cohort');
+                }
+                $idnumbers[$hash['idnumber']] = true;
+            }
+
+            if (empty($hash['name'])) {
+                $cohorts[$rownum]['errors'][] = new lang_string('namefieldempty', 'cohort');
+            }
+
+            $cohorts[$rownum]['data'] = array_intersect_key($hash, $cohorts[0]['data']);
+            $haserrors = $haserrors || !empty($cohorts[$rownum]['errors']);
+            $haswarnings = $haswarnings || !empty($cohorts[$rownum]['warnings']);
+        }
+
+        if ($haserrors) {
+            $cohorts[0]['errors'][] = new lang_string('csvcontainserrors', 'cohort');
+        }
+
+        if ($haswarnings) {
+            $cohorts[0]['warnings'][] = new lang_string('csvcontainswarnings', 'cohort');
+        }
+
+        return $cohorts;
+    }
+
+    /**
+     * Cleans input data about one cohort.
+     *
+     * @param array $hash
+     */
+    protected function clean_cohort_data(&$hash) {
+        foreach ($hash as $key => $value) {
+            switch ($key) {
+                case 'contextid': $hash[$key] = clean_param($value, PARAM_INT); break;
+                case 'name': $hash[$key] = core_text::substr(clean_param($value, PARAM_TEXT), 0, 254); break;
+                case 'idnumber': $hash[$key] = core_text::substr(clean_param($value, PARAM_RAW), 0, 254); break;
+                case 'description': $hash[$key] = clean_param($value, PARAM_RAW); break;
+                case 'descriptionformat': $hash[$key] = clean_param($value, PARAM_INT); break;
+            }
+        }
+    }
+
+    /**
+     * Determines in which context the particular cohort will be created
+     *
+     * @param array $hash
+     * @param context $defaultcontext
+     * @return array array of warning strings
+     */
+    protected function resolve_context(&$hash, $defaultcontext) {
+        global $DB;
+
+        $warnings = array();
+
+        if (!empty($hash['contextid'])) {
+            // Contextid was specified, verify we can post there.
+            $contextoptions = $this->get_context_options();
+            if (!isset($contextoptions[$hash['contextid']])) {
+                $warnings[] = new lang_string('contextnotfound', 'cohort', $hash['contextid']);
+                $hash['contextid'] = $defaultcontext->id;
+            }
+            return $warnings;
+        }
+
+        if (!empty($hash['context'])) {
+            $systemcontext = context_system::instance();
+            if ((core_text::strtolower(trim($hash['context'])) ===
+                    core_text::strtolower($systemcontext->get_context_name())) ||
+                    ('' . $hash['context'] === '' . $systemcontext->id)) {
+                // User meant system context.
+                $hash['contextid'] = $systemcontext->id;
+                $contextoptions = $this->get_context_options();
+                if (!isset($contextoptions[$hash['contextid']])) {
+                    $warnings[] = new lang_string('contextnotfound', 'cohort', $hash['context']);
+                    $hash['contextid'] = $defaultcontext->id;
+                }
+            } else {
+                // Assume it is a category.
+                $hash['category'] = trim($hash['context']);
+            }
+        }
+
+        if (!empty($hash['category_path'])) {
+            // We already have array with available categories, look up the value.
+            $contextoptions = $this->get_context_options();
+            if (!$hash['contextid'] = array_search($hash['category_path'], $contextoptions)) {
+                $warnings[] = new lang_string('categorynotfound', 'cohort', s($hash['category_path']));
+                $hash['contextid'] = $defaultcontext->id;
+            }
+            return $warnings;
+        }
+
+        if (!empty($hash['category'])) {
+            // Quick search by category path first.
+            // Do not issue warnings or return here, further we'll try to search by id or idnumber.
+            $contextoptions = $this->get_context_options();
+            if ($hash['contextid'] = array_search($hash['category'], $contextoptions)) {
+                return $warnings;
+            }
+        }
+
+        // Now search by category id or category idnumber.
+        if (!empty($hash['category_id'])) {
+            $field = 'id';
+            $value = clean_param($hash['category_id'], PARAM_INT);
+        } else if (!empty($hash['category_idnumber'])) {
+            $field = 'idnumber';
+            $value = $hash['category_idnumber'];
+        } else if (!empty($hash['category'])) {
+            $field = is_numeric($hash['category']) ? 'id' : 'idnumber';
+            $value = $hash['category'];
+        } else {
+            // No category field was specified, assume default category.
+            $hash['contextid'] = $defaultcontext->id;
+            return $warnings;
+        }
+
+        if (empty($this->categoriescache[$field][$value])) {
+            $record = $DB->get_record_sql("SELECT c.id, ctx.id contextid
+                FROM {context} ctx JOIN {course_categories} c ON ctx.contextlevel = ? AND ctx.instanceid = c.id
+                WHERE c.$field = ?", array(CONTEXT_COURSECAT, $value));
+            if ($record && ($contextoptions = $this->get_context_options()) && isset($contextoptions[$record->contextid])) {
+                $contextid = $record->contextid;
+            } else {
+                $warnings[] = new lang_string('categorynotfound', 'cohort', s($value));
+                $contextid = $defaultcontext->id;
+            }
+            // Next time when we can look up and don't search by this value again.
+            $this->categoriescache[$field][$value] = $contextid;
+        }
+        $hash['contextid'] = $this->categoriescache[$field][$value];
+
+        return $warnings;
+    }
+}
index 5b3cc55..5441dbb 100644 (file)
@@ -119,8 +119,24 @@ class grade_export_form extends moodleform {
             }
         }
         */
-        $mform->addElement('select', 'display', get_string('gradeexportdisplaytype', 'grades'), $options);
-        $mform->setDefault('display', $CFG->grade_export_displaytype);
+        if ($features['multipledisplaytypes']) {
+            /*
+             * Using advcheckbox because we need the grade display type (name) as key and grade display type (constant) as value.
+             * The method format_column_name requires the lang file string and the format_grade method requires the constant.
+             */
+            $checkboxes = array();
+            $checkboxes[] = $mform->createElement('advcheckbox', 'display[real]', null, get_string('real', 'grades'), null, array(0, GRADE_DISPLAY_TYPE_REAL));
+            $checkboxes[] = $mform->createElement('advcheckbox', 'display[percentage]', null, get_string('percentage', 'grades'), null, array(0, GRADE_DISPLAY_TYPE_PERCENTAGE));
+            $checkboxes[] = $mform->createElement('advcheckbox', 'display[letter]', null, get_string('letter', 'grades'), null, array(0, GRADE_DISPLAY_TYPE_LETTER));
+            $mform->addGroup($checkboxes, 'displaytypes', get_string('gradeexportdisplaytypes', 'grades'), ' ', false);
+            $mform->setDefault('display[real]', $CFG->grade_export_displaytype == GRADE_DISPLAY_TYPE_REAL);
+            $mform->setDefault('display[percentage]', $CFG->grade_export_displaytype == GRADE_DISPLAY_TYPE_PERCENTAGE);
+            $mform->setDefault('display[letter]', $CFG->grade_export_displaytype == GRADE_DISPLAY_TYPE_LETTER);
+        } else {
+            // Only used by XML grade export format.
+            $mform->addElement('select', 'display', get_string('gradeexportdisplaytype', 'grades'), $options);
+            $mform->setDefault('display', $CFG->grade_export_displaytype);
+        }
 
         //$default_gradedecimals = $CFG->grade_export_decimalpoints;
         $options = array(0=>0, 1=>1, 2=>2, 3=>3, 4=>4, 5=>5);
@@ -184,5 +200,30 @@ class grade_export_form extends moodleform {
 
         $this->add_action_buttons(false, $submitstring);
     }
+
+    /**
+     * Overrides the mform get_data method.
+     *
+     * Created to force a value since the validation method does not work with multiple checkbox.
+     *
+     * @return stdClass form data object.
+     */
+    public function get_data() {
+        global $CFG;
+        $data = parent::get_data();
+        if ($data && $this->_customdata['multipledisplaytypes']) {
+            if (count(array_filter($data->display)) == 0) {
+                // Ensure that a value was selected as the export plugins expect at least one value.
+                if ($CFG->grade_export_displaytype == GRADE_DISPLAY_TYPE_LETTER) {
+                    $data->display['letter'] = GRADE_DISPLAY_TYPE_LETTER;
+                } else if ($CFG->grade_export_displaytype == GRADE_DISPLAY_TYPE_PERCENTAGE) {
+                    $data->display['percentage'] = GRADE_DISPLAY_TYPE_PERCENTAGE;
+                } else {
+                    $data->display['real'] = GRADE_DISPLAY_TYPE_REAL;
+                }
+            }
+        }
+        return $data;
+    }
 }
 
index 11fe75c..f08b54c 100644 (file)
@@ -36,7 +36,17 @@ abstract class grade_export {
     public $userkey;         // export using private user key
 
     public $updatedgradesonly; // only export updated grades
-    public $displaytype; // display type (e.g. real, percentages, letter) for exports
+
+    /**
+     *  Grade display type (real, percentages or letter).
+     *
+     *  This attribute is an integer for XML file export. Otherwise is an array for all other formats (ODS, XLS and TXT).
+     *
+     *  @var $displaytype Grade display type constant (1, 2 or 3) or an array of display types where the key is the name
+     *                    and the value is the grade display type constant or 0 for unchecked display types.
+     * @access public.
+     */
+    public $displaytype;
     public $decimalpoints; // number of decimal points for exports
     public $onlyactive; // only include users with an active enrolment
     public $usercustomfields; // include users custom fields
@@ -184,6 +194,12 @@ abstract class grade_export {
 
         if (isset($formdata->display)) {
             $this->displaytype = $formdata->display;
+
+            // Used by grade exports which accept multiple display types.
+            // If the checkbox value is 0 (unchecked) then remove it.
+            if (is_array($formdata->display)) {
+                $this->displaytype = array_filter($formdata->display);
+            }
         }
 
         if (isset($formdata->updatedgradesonly)) {
@@ -212,31 +228,38 @@ abstract class grade_export {
 
     /**
      * Returns string representation of final grade
-     * @param $object $grade instance of grade_grade class
+     * @param object $grade instance of grade_grade class
+     * @param integer $gradedisplayconst grade display type constant.
      * @return string
      */
-    public function format_grade($grade) {
-        return grade_format_gradevalue($grade->finalgrade, $this->grade_items[$grade->itemid], false, $this->displaytype, $this->decimalpoints);
+    public function format_grade($grade, $gradedisplayconst = null) {
+        $displaytype = $this->displaytype;
+        if (is_array($this->displaytype) && !is_null($gradedisplayconst)) {
+            $displaytype = $gradedisplayconst;
+        }
+        return grade_format_gradevalue($grade->finalgrade, $this->grade_items[$grade->itemid], false, $displaytype, $this->decimalpoints);
     }
 
     /**
      * Returns the name of column in export
      * @param object $grade_item
-     * @param boolena $feedback feedback colum
-     * &return string
+     * @param boolean $feedback feedback colum
+     * @param string $gradedisplayname grade display name.
+     * @return string
      */
-    public function format_column_name($grade_item, $feedback=false) {
+    public function format_column_name($grade_item, $feedback=false, $gradedisplayname = null) {
+        $column = new stdClass();
+
         if ($grade_item->itemtype == 'mod') {
-            $name = get_string('modulename', $grade_item->itemmodule).get_string('labelsep', 'langconfig').$grade_item->get_name();
+            $column->name = get_string('modulename', $grade_item->itemmodule).get_string('labelsep', 'langconfig').$grade_item->get_name();
         } else {
-            $name = $grade_item->get_name();
+            $column->name = $grade_item->get_name();
         }
 
-        if ($feedback) {
-            $name .= ' ('.get_string('feedback').')';
-        }
+        // We can't have feedback and display type at the same time.
+        $column->extra = ($feedback) ? get_string('feedback') : get_string($gradedisplayname, 'grades');
 
-        return html_to_text($name, 0, false);
+        return html_to_text(get_string('gradeexportcolumntype', 'grades', $column), 0, false);
     }
 
     /**
index 82f512a..827bd67 100644 (file)
@@ -37,7 +37,7 @@ if (groups_get_course_groupmode($COURSE) == SEPARATEGROUPS and !has_capability('
         print_error('cannotaccessgroup', 'grades');
     }
 }
-$mform = new grade_export_form(null, array('publishing' => true, 'simpleui' => true));
+$mform = new grade_export_form(null, array('publishing' => true, 'simpleui' => true, 'multipledisplaytypes' => true));
 $data = $mform->get_data();
 
 // print all the exported data here
index ec81be8..468496c 100644 (file)
@@ -67,7 +67,9 @@ class grade_export_ods extends grade_export {
             $myxls->write_string(0, $pos++, get_string("suspended"));
         }
         foreach ($this->columns as $grade_item) {
-            $myxls->write_string(0, $pos++, $this->format_column_name($grade_item));
+            foreach ($this->displaytype as $gradedisplayname => $gradedisplayconst) {
+                $myxls->write_string(0, $pos++, $this->format_column_name($grade_item, false, $gradedisplayname));
+            }
 
             // Add a column_feedback column.
             if ($this->export_feedback) {
@@ -101,12 +103,13 @@ class grade_export_ods extends grade_export {
                     $status = $geub->track($grade);
                 }
 
-                $gradestr = $this->format_grade($grade);
-                if (is_numeric($gradestr)) {
-                    $myxls->write_number($i,$j++,$gradestr);
-                }
-                else {
-                    $myxls->write_string($i,$j++,$gradestr);
+                foreach ($this->displaytype as $gradedisplayconst) {
+                    $gradestr = $this->format_grade($grade, $gradedisplayconst);
+                    if (is_numeric($gradestr)) {
+                        $myxls->write_number($i, $j++, $gradestr);
+                    } else {
+                        $myxls->write_string($i, $j++, $gradestr);
+                    }
                 }
 
                 // writing feedback if requested
index 9901cca..29ee8a1 100644 (file)
@@ -43,7 +43,8 @@ if (!empty($CFG->gradepublishing)) {
 $actionurl = new moodle_url('/grade/export/ods/export.php');
 $formoptions = array(
     'publishing' => true,
-    'simpleui' => true
+    'simpleui' => true,
+    'multipledisplaytypes' => true
 );
 
 $mform = new grade_export_form($actionurl, $formoptions);
index 07d8670..bb3a776 100644 (file)
@@ -41,7 +41,8 @@ if (groups_get_course_groupmode($COURSE) == SEPARATEGROUPS and !has_capability('
 $params = array(
     'includeseparator'=>true,
     'publishing' => true,
-    'simpleui' => true
+    'simpleui' => true,
+    'multipledisplaytypes' => true
 );
 $mform = new grade_export_form(null, $params);
 $data = $mform->get_data();
index 0a379c2..109bce6 100644 (file)
@@ -67,9 +67,11 @@ class grade_export_txt extends grade_export {
             $exporttitle[] = get_string("suspended");
         }
 
-        // Add a feedback column.
+        // Add grades and feedback columns.
         foreach ($this->columns as $grade_item) {
-            $exporttitle[] = $this->format_column_name($grade_item);
+            foreach ($this->displaytype as $gradedisplayname => $gradedisplayconst) {
+                $exporttitle[] = $this->format_column_name($grade_item, false, $gradedisplayname);
+            }
             if ($this->export_feedback) {
                 $exporttitle[] = $this->format_column_name($grade_item, true);
             }
@@ -100,7 +102,9 @@ class grade_export_txt extends grade_export {
                     $status = $geub->track($grade);
                 }
 
-                $exportdata[] = $this->format_grade($grade);
+                foreach ($this->displaytype as $gradedisplayconst) {
+                    $exportdata[] = $this->format_grade($grade, $gradedisplayconst);
+                }
 
                 if ($this->export_feedback) {
                     $exportdata[] = $this->format_feedback($userdata->feedbacks[$itemid]);
index 571f49e..c5420ce 100644 (file)
@@ -44,7 +44,8 @@ $actionurl = new moodle_url('/grade/export/txt/export.php');
 $formoptions = array(
     'includeseparator'=>true,
     'publishing' => true,
-    'simpleui' => true
+    'simpleui' => true,
+    'multipledisplaytypes' => true
 );
 
 $mform = new grade_export_form($actionurl, $formoptions);
index db0ba72..c90e254 100644 (file)
@@ -39,11 +39,36 @@ Feature: I need to export grades as text
     And I should not see "80.00"
 
   @javascript
-  Scenario: Export grades as text using percentages
+  Scenario: Export grades as text using real
     When I set the field "Grade report" to "Plain text file"
     And I expand all fieldsets
-    And I set the field "Grade export display type" to "Percent"
+    And  I set the following fields to these values:
+      | Real        | 1                        |
     And I click on "Course total" "checkbox"
     And I press "Download"
     Then I should see "Student,1"
+    And I should see "80.00"
+
+  @javascript
+  Scenario: Export grades as text using percentages and letters
+    When I set the field "Grade report" to "Plain text file"
+    And  I set the following fields to these values:
+      | Percentage   | 1                        |
+      | Letter       | 1                        |
+    And I press "Download"
+    Then I should see "Student,1"
+    And I should see "80.00 %"
+    And I should see "B-"
+
+  @javascript
+  Scenario: Export grades as text using real, percentages and letters
+    When I set the field "Grade report" to "Plain text file"
+    And  I set the following fields to these values:
+      | Real         | 1                        |
+      | Percentage   | 1                        |
+      | Letter       | 1                        |
+    And I press "Download"
+    Then I should see "Student,1"
+    And I should see "80.00"
     And I should see "80.00 %"
+    And I should see "B-"
\ No newline at end of file
index f9d0401..1ce80f5 100644 (file)
@@ -4,4 +4,4 @@ information provided here is intended especially for developers.
 === 2.8 ===
 
 The UI for the grade export form was simplified down so it's all on one page. The export preview was removed because it was not useful (more useful on import than on export). To update your export plugins you must pass 'simpleui' => true as an option to the grade_export_form, and make your grade_export_form submit directly to your export script. It's easiest to look at a complete example - see "git show 1cc43058" for a complete example of updating the ods exporter.
-
+Also the grade export UI form was changed to support multiples display types. The combo box was removed from (Open Document, Plain Text, Excel) and for each selected display type a column is generated. We didn't extended this solution to XML export which remains with a combo and can only select a single display type.
\ No newline at end of file
index 3506805..409ce48 100644 (file)
@@ -37,7 +37,7 @@ if (groups_get_course_groupmode($COURSE) == SEPARATEGROUPS and !has_capability('
         print_error('cannotaccessgroup', 'grades');
     }
 }
-$mform = new grade_export_form(null, array('publishing' => true, 'simpleui' => true));
+$mform = new grade_export_form(null, array('publishing' => true, 'simpleui' => true, 'multipledisplaytypes' => true));
 $formdata = $mform->get_data();
 
 // print all the exported data here
index a3c75bd..384d768 100644 (file)
@@ -65,8 +65,9 @@ class grade_export_xls extends grade_export {
             $myxls->write_string(0, $pos++, get_string("suspended"));
         }
         foreach ($this->columns as $grade_item) {
-            $myxls->write_string(0, $pos++, $this->format_column_name($grade_item));
-
+            foreach ($this->displaytype as $gradedisplayname => $gradedisplayconst) {
+                $myxls->write_string(0, $pos++, $this->format_column_name($grade_item, false, $gradedisplayname));
+            }
             // Add a column_feedback column
             if ($this->export_feedback) {
                 $myxls->write_string(0, $pos++, $this->format_column_name($grade_item, true));
@@ -97,15 +98,14 @@ class grade_export_xls extends grade_export {
                 if ($export_tracking) {
                     $status = $geub->track($grade);
                 }
-
-                $gradestr = $this->format_grade($grade);
-                if (is_numeric($gradestr)) {
-                    $myxls->write_number($i,$j++,$gradestr);
+                foreach ($this->displaytype as $gradedisplayconst) {
+                    $gradestr = $this->format_grade($grade, $gradedisplayconst);
+                    if (is_numeric($gradestr)) {
+                        $myxls->write_number($i, $j++, $gradestr);
+                    } else {
+                        $myxls->write_string($i, $j++, $gradestr);
+                    }
                 }
-                else {
-                    $myxls->write_string($i,$j++,$gradestr);
-                }
-
                 // writing feedback if requested
                 if ($this->export_feedback) {
                     $myxls->write_string($i, $j++, $this->format_feedback($userdata->feedbacks[$itemid]));
index 517b95c..4d86ba8 100644 (file)
@@ -43,7 +43,8 @@ if (!empty($CFG->gradepublishing)) {
 $actionurl = new moodle_url('/grade/export/xls/export.php');
 $formoptions = array(
     'publishing' => true,
-    'simpleui' => true
+    'simpleui' => true,
+    'multipledisplaytypes' => true
 );
 
 $mform = new grade_export_form($actionurl, $formoptions);
index 5c44040..3ab49a4 100644 (file)
@@ -37,7 +37,7 @@ if (groups_get_course_groupmode($COURSE) == SEPARATEGROUPS and !has_capability('
         print_error('cannotaccessgroup', 'grades');
     }
 }
-$mform = new grade_export_form(null, array('publishing' => true, 'simpleui' => true));
+$mform = new grade_export_form(null, array('publishing' => true, 'simpleui' => true, 'multipledisplaytypes' => false));
 $formdata = $mform->get_data();
 
 // print all the exported data here
index de1b979..8b25939 100644 (file)
@@ -68,7 +68,7 @@ class grade_export_xml extends grade_export {
             foreach ($userdata->grades as $itemid => $grade) {
                 $grade_item = $this->grade_items[$itemid];
                 $grade->grade_item =& $grade_item;
-                $gradestr = $this->format_grade($grade); // no formating for now
+                $gradestr = $this->format_grade($grade, $this->displaytype); // no formating for now
 
                 // MDL-11669, skip exported grades or bad grades (if setting says so)
                 if ($export_tracking) {
index ff1c268..3b70fd7 100644 (file)
@@ -46,7 +46,8 @@ $formoptions = array(
     'idnumberrequired' => true,
     'updategradesonly' => true,
     'publishing' => true,
-    'simpleui' => true
+    'simpleui' => true,
+    'multipledisplaytypes' => false
 );
 
 $mform = new grade_export_form($actionurl, $formoptions);
index 0e51229..2eead9f 100644 (file)
@@ -62,7 +62,7 @@ class filter_form extends \moodleform {
         $mform->setType('grader', PARAM_INT);
 
         $mform->addElement('date_selector', 'datefrom', get_string('datefrom', 'gradereport_history'), array('optional' => true));
-        $mform->addElement('date_selector', 'datetill', get_string('datetill', 'gradereport_history'), array('optional' => true));
+        $mform->addElement('date_selector', 'datetill', get_string('dateto', 'gradereport_history'), array('optional' => true));
 
         $mform->addElement('checkbox', 'revisedonly', get_string('revisedonly', 'gradereport_history'));
         $mform->addHelpButton('revisedonly', 'revisedonly', 'gradereport_history');
index 063f897..0d35fdd 100644 (file)
@@ -26,7 +26,7 @@
 $string['allgradeitems'] = 'All grade items';
 $string['allgraders'] = 'All graders';
 $string['datefrom'] = 'Date from';
-$string['datetill'] = 'Date till';
+$string['dateto'] = 'Date to';
 $string['datetime'] = 'Date and time';
 $string['deleteditemid'] = 'Delete item with id {$a}';
 $string['errajaxsearch'] = 'Error when searching users';
index e76b528..31942de 100644 (file)
@@ -32,6 +32,7 @@ $string['assignto'] = 'Cohort \'{$a}\' members';
 $string['backtocohorts'] = 'Back to cohorts';
 $string['bulkadd'] = 'Add to cohort';
 $string['bulknocohort'] = 'No available cohorts found';
+$string['categorynotfound'] = 'Category <b>{$a}</b> not found or you don\'t have permission to create a cohort there. The default context will be used.';
 $string['cohort'] = 'Cohort';
 $string['cohorts'] = 'Cohorts';
 $string['cohortsin'] = '{$a}: available cohorts';
@@ -39,11 +40,17 @@ $string['cohort:assign'] = 'Assign cohort members';
 $string['cohort:manage'] = 'Manage cohorts';
 $string['cohort:view'] = 'Use cohorts and view members';
 $string['component'] = 'Source';
+$string['contextnotfound'] = 'Context <b>{$a}</b> not found or you don\'t have permission to create a cohort there. The default context will be used.';
+$string['csvcontainserrors'] = 'Errors were found in CSV data. See details below.';
+$string['csvcontainswarnings'] = 'Warnings were found in CSV data. See details below.';
+$string['csvextracolumns'] = 'Column(s) <b>{$a}</b> will be ignored.';
 $string['currentusers'] = 'Current users';
 $string['currentusersmatching'] = 'Current users matching';
+$string['defaultcontext'] = 'Default context';
 $string['delcohort'] = 'Delete cohort';
 $string['delconfirm'] = 'Do you really want to delete cohort \'{$a}\'?';
 $string['description'] = 'Description';
+$string['displayedrows'] = '{$a->displayed} rows displayed out of {$a->total}.';
 $string['duplicateidnumber'] = 'Cohort with the same ID number already exists';
 $string['editcohort'] = 'Edit cohort';
 $string['eventcohortcreated'] = 'Cohort created';
@@ -55,13 +62,26 @@ $string['external'] = 'External cohort';
 $string['idnumber'] = 'Cohort ID';
 $string['memberscount'] = 'Cohort size';
 $string['name'] = 'Name';
+$string['namecolumnmissing'] = 'There is something wrong with the format of the CSV file. Please check that it includes column names.';
+$string['namefieldempty'] = 'Field name can not be empty';
 $string['nocomponent'] = 'Created manually';
 $string['potusers'] = 'Potential users';
 $string['potusersmatching'] = 'Potential matching users';
+$string['preview'] = 'Preview';
 $string['removeuserwarning'] = 'Removing users from a cohort may result in unenrolling of users from multiple courses which includes deleting of user settings, grades, group membership and other user information from affected courses.';
 $string['selectfromcohort'] = 'Select members from cohort';
 $string['systemcohorts'] = 'System cohorts';
 $string['unknowncohort'] = 'Unknown cohort ({$a})!';
+$string['uploadcohorts'] = 'Upload cohorts';
+$string['uploadedcohorts'] = 'Uploaded {$a} cohorts';
 $string['useradded'] = 'User added to cohort "{$a}"';
 $string['search'] = 'Search';
 $string['searchcohort'] = 'Search cohort';
+$string['uploadcohorts_help'] = 'Cohorts may be uploaded via text file. The format of the file should be as follows:
+
+* Each line of the file contains one record
+* Each record is a series of data separated by commas (or other delimiters)
+* The first record contains a list of fieldnames defining the format of the rest of the file
+* Required fieldname is name
+* Optional fieldnames are idnumber, description, descriptionformat, context, category, category_id, category_idnumber, category_path
+';
\ No newline at end of file
index 84ae60a..dd379f8 100644 (file)
@@ -244,12 +244,14 @@ $string['gradedisplaytype_help'] = 'This setting determines how grades are displ
 * Letter - Letters or words are used to represent a range of grades';
 $string['gradedon'] = 'Graded: {$a}';
 $string['gradeexport'] = 'Grade export';
+$string['gradeexportcolumntype'] = '{$a->name} ({$a->extra})';
 $string['gradeexportcustomprofilefields'] = 'Grade export custom profile fields';
 $string['gradeexportcustomprofilefields_desc'] = 'Include these custom profile fields in the grade export, separated by commas.';
 $string['gradeexportdecimalpoints'] = 'Grade export decimal points';
 $string['gradeexportdecimalpoints_desc'] = 'The number of decimal points to display for export. This can be overridden during export.';
 $string['gradeexportdisplaytype'] = 'Grade export display type';
 $string['gradeexportdisplaytype_desc'] = 'Grades can be shown as real grades, as percentages (in reference to the minimum and maximum grades) or as letters (A, B, C etc..) during export. This can be overridden during export.';
+$string['gradeexportdisplaytypes'] = 'Grade export display types';
 $string['gradeexportuserprofilefields'] = 'Grade export user profile fields';
 $string['gradeexportuserprofilefields_desc'] = 'Include these user profile fields in the grade export, separated by commas.';
 $string['gradeforstudent'] = '{$a->student}<br />{$a->item}{$a->feedback}';
index 27153b4..edf009f 100644 (file)
@@ -342,6 +342,7 @@ $string['howquestionsbehave_help'] = 'Students can interact with the questions i
 Alternatively, you may wish for students to submit each question as they go along to get immediate feedback, and if they do not get it right immediately, have another try for fewer marks. That would be \'Interactive with multiple tries\' mode.
 
 Those are probably the two most commonly used modes of behaviour. ';
+$string['howquestionsbehave_link'] = 'question/behaviour';
 $string['importfromcoursefiles'] = '... or choose a course file to import.';
 $string['importfromupload'] = 'Select a file to upload ...';
 $string['includesubcategories'] = 'Also show questions from subcategories';
index d4e24ba..cc6ab3c 100644 (file)
@@ -36,6 +36,15 @@ class tool extends base {
         return true;
     }
 
+    /**
+     * Tools cannot be disabled.
+     *
+     * @return boolean
+     */
+    public function is_enabled() {
+        return true;
+    }
+
     /**
      * Return URL used for management of plugins of this type.
      * @return moodle_url
index f3a17ff..9f8dc93 100644 (file)
@@ -1000,11 +1000,15 @@ class behat_general extends behat_base {
         $tablenode = $this->get_selected_node('table', $table);
         $tablexpath = $tablenode->getXpath();
 
+        $rowliteral = $this->getSession()->getSelectorsHandler()->xpathLiteral($row);
+        $valueliteral = $this->getSession()->getSelectorsHandler()->xpathLiteral($value);
+        $columnliteral = $this->getSession()->getSelectorsHandler()->xpathLiteral($column);
+
         // Header can be in thead or tbody (first row), following xpath should work.
-        $theadheaderxpath = "thead/tr[1]/th[(normalize-space(.)='" . $column . "' or a[normalize-space(text())='" .
-            $column . "'])]";
-        $tbodyheaderxpath = "tbody/tr[1]/td[(normalize-space(.)='" . $column . "' or a[normalize-space(text())='" .
-            $column . "'])]";
+        $theadheaderxpath = "thead/tr[1]/th[(normalize-space(.)=" . $columnliteral . " or a[normalize-space(text())=" .
+            $columnliteral . "])]";
+        $tbodyheaderxpath = "tbody/tr[1]/td[(normalize-space(.)=" . $columnliteral . " or a[normalize-space(text())=" .
+            $columnliteral . "])]";
 
         // Check if column exists.
         $columnheaderxpath = $tablexpath . "[" . $theadheaderxpath . " | " . $tbodyheaderxpath . "]";
@@ -1016,21 +1020,21 @@ class behat_general extends behat_base {
 
         // Check if value exists in specific row/column.
         // Get row xpath.
-        $rowxpath = $tablexpath."/tbody/tr[th[normalize-space(.)='" . $row . "'] | td[normalize-space(.)='" . $row . "']]";
+        $rowxpath = $tablexpath."/tbody/tr[th[normalize-space(.)=" . $rowliteral . "] | td[normalize-space(.)=" . $rowliteral . "]]";
 
         // Following conditions were considered before finding column count.
         // 1. Table header can be in thead/tr/th or tbody/tr/td[1].
         // 2. First column can have th (Gradebook -> user report), so having lenient sibling check.
         $columnpositionxpath = "/child::*[position() = count(" . $tablexpath . "/" . $theadheaderxpath .
             "/preceding-sibling::*) + 1]";
-        $columnvaluexpath = $rowxpath . $columnpositionxpath . "[contains(normalize-space(.),'" . $value . "')]";
+        $columnvaluexpath = $rowxpath . $columnpositionxpath . "[contains(normalize-space(.)," . $valueliteral . ")]";
         // Looks for the requested node inside the container node.
         $coumnnode = $this->getSession()->getDriver()->find($columnvaluexpath);
         if (empty($coumnnode)) {
             // Check if tbody/tr[1] contains header selector.
             $columnpositionxpath = "/child::*[position() = count(" . $tablexpath . "/" . $tbodyheaderxpath .
                 "/preceding-sibling::*) + 1]";
-            $columnvaluexpath = $rowxpath . $columnpositionxpath . "[contains(normalize-space(.),'" . $value . "')]";
+            $columnvaluexpath = $rowxpath . $columnpositionxpath . "[contains(normalize-space(.)," . $valueliteral . ")]";
             $coumnnode = $this->getSession()->getDriver()->find($columnvaluexpath);
             if (empty($coumnnode)) {
                 $locatorexceptionmsg = $value . '" in "' . $row . '" row with column "' . $column;
index 5feee88..84a1c3e 100644 (file)
@@ -143,11 +143,12 @@ class mod_assign_locallib_testcase extends mod_assign_base_testcase {
         $this->assertContains(get_string('overdue', 'assign', format_time(4*24*60*60)), $output);
 
         // Grant an extension.
-        $assign->testable_save_user_extension($this->students[0]->id, time() + 2 * 24 * 60 * 60);
+        $extendedtime = time() + 2 * 24 * 60 * 60;
+        $assign->testable_save_user_extension($this->students[0]->id, $extendedtime);
         $gradingtable = new assign_grading_table($assign, 1, '', 0, true);
         $output = $assign->get_renderer()->render($gradingtable);
         $this->assertContains(get_string('submissionstatus_', 'assign'), $output);
-        $this->assertContains(get_string('userextensiondate', 'assign', userdate(time() + 2*24*60*60)), $output);
+        $this->assertContains(get_string('userextensiondate', 'assign', userdate($extendedtime)), $output);
 
         // Simulate a submission.
         $this->setUser($this->students[0]);
@@ -166,7 +167,7 @@ class mod_assign_locallib_testcase extends mod_assign_base_testcase {
         $gradingtable = new assign_grading_table($assign, 1, '', 0, true);
         $output = $assign->get_renderer()->render($gradingtable);
         $this->assertContains(get_string('submissionstatus_submitted', 'assign'), $output);
-        $this->assertContains(get_string('userextensiondate', 'assign', userdate(time() + 2*24*60*60)), $output);
+        $this->assertContains(get_string('userextensiondate', 'assign', userdate($extendedtime)), $output);
     }
 
     /**
index 9a1b0db..874ac0a 100644 (file)
@@ -130,10 +130,10 @@ if ( $launchcontainer == LTI_LAUNCH_CONTAINER_WINDOW ) {
     echo "</script>\n";
     echo "<p>".get_string("basiclti_in_new_window", "lti")."</p>\n";
 } else {
-    // Request the launch content with an object tag
-    echo '<object id="contentframe" height="600px" width="100%" type="text/html" data="launch.php?id='.$cm->id.'"></object>';
+    // Request the launch content with an iframe tag.
+    echo '<iframe id="contentframe" height="600px" width="100%" src="launch.php?id='.$cm->id.'"></iframe>';
 
-    //Output script to make the object tag be as large as possible
+    // Output script to make the iframe be as large as possible.
     $resize = '
         <script type="text/javascript">
         //<![CDATA[
index 120defd..47a4a97 100644 (file)
@@ -69,3 +69,4 @@ $string['pluginname'] = 'Deferred feedback with CBM';
 $string['slightlyoverconfident'] = 'a bit over-confident';
 $string['slightlyunderconfident'] = 'a bit under-confident';
 $string['underconfident'] = 'under-confident';
+$string['weightx'] = 'Weight {$a}';
index f5abbf5..b094a60 100644 (file)
@@ -95,13 +95,14 @@ class qbehaviour_deferredcbm_renderer extends qbehaviour_renderer {
 
     public function marked_out_of_max(question_attempt $qa, core_question_renderer $qoutput,
             question_display_options $options) {
-        return get_string('basemark', 'qbehaviour_deferredcbm', $qa->format_fraction_as_mark(
+        return get_string('weightx', 'qbehaviour_deferredcbm', $qa->format_fraction_as_mark(
                 question_cbm::adjust_fraction(1, question_cbm::default_certainty()),
                 $options->markdp));
     }
 
     public function mark_out_of_max(question_attempt $qa, core_question_renderer $qoutput,
             question_display_options $options) {
-        return get_string('cbmmark', 'qbehaviour_deferredcbm', $qa->format_mark($options->markdp));
+        return get_string('cbmmark', 'qbehaviour_deferredcbm', $qa->format_mark($options->markdp)) .
+                '<br>' . $this->marked_out_of_max($qa, $qoutput, $options);
     }
-}
\ No newline at end of file
+}
index 5b3ca64..ed4d4dc 100644 (file)
@@ -175,7 +175,7 @@ class qformat_gift extends qformat_default {
             }
         }
 
-        $text = trim(implode(' ', $lines));
+        $text = trim(implode("\n", $lines));
 
         if ($text == '') {
             return false;
diff --git a/question/format/gift/tests/behat/import.feature b/question/format/gift/tests/behat/import.feature
new file mode 100644 (file)
index 0000000..89873cb
--- /dev/null
@@ -0,0 +1,30 @@
+@qtype @qformat_gift
+Feature: Test importing questions from GIFT format.
+  In order to reuse questions
+  As an teacher
+  I need to be able to import them in GIFT format.
+
+  Background:
+    Given the following "courses" exist:
+      | fullname | shortname | format |
+      | Course 1 | C1        | topics |
+    And the following "users" exist:
+      | username | firstname |
+      | teacher  | Teacher   |
+    And the following "course enrolments" exist:
+      | user    | course | role           |
+      | teacher | C1     | editingteacher |
+    And I log in as "teacher"
+    And I follow "Course 1"
+
+  @javascript @_file_upload
+  Scenario: import some GIFT questions
+    When I navigate to "Import" node in "Course administration > Question bank"
+    And I set the field "id_format_gift" to "1"
+    And I upload "question/format/gift/tests/fixtures/questions.gift.txt" file to "Import" filemanager
+    And I press "id_submitbutton"
+    Then I should see "Parsing questions from import file."
+    And I should see "Importing 9 questions from file"
+    And I should see "What's between orange and green in the spectrum?"
+    When I press "Continue"
+    Then I should see "colours"
index 0235afb..9c7bc05 100644 (file)
@@ -1070,4 +1070,49 @@ FALSE#42 is the Ultimate Answer.#You gave the right answer.}";
 
         $this->assert(new question_check_specified_fields_expectation($expectedq), $q);
     }
+
+    public function test_import_pre_content() {
+        $gift = '
+::Q001::[html]<p>What would running the test method print?</p>
+<pre>
+    public void test() \{
+        method1();
+        method2();
+        method3();
+    \}
+</pre>
+{}';
+        $lines = preg_split('/[\\n\\r]/', str_replace("\r\n", "\n", $gift));
+
+        $importer = new qformat_gift();
+        $q = $importer->readquestion($lines);
+
+        $expectedq = (object) array(
+            'name' => 'Q001',
+            'questiontext' => '<p>What would running the test method print?</p>
+<pre>
+    public void test() {
+        method1();
+        method2();
+        method3();
+    }
+</pre>',
+            'questiontextformat' => FORMAT_HTML,
+            'generalfeedback' => '',
+            'generalfeedbackformat' => FORMAT_HTML,
+            'qtype' => 'essay',
+            'defaultmark' => 1,
+            'penalty' => 0.3333333,
+            'length' => 1,
+            'responseformat' => 'editor',
+            'responsefieldlines' => 15,
+            'attachments' => 0,
+            'graderinfo' => array(
+                'text' => '',
+                'format' => FORMAT_HTML,
+                'files' => array()),
+        );
+
+        $this->assert(new question_check_specified_fields_expectation($expectedq), $q);
+    }
 }
diff --git a/question/format/xml/tests/behat/import.feature b/question/format/xml/tests/behat/import.feature
new file mode 100644 (file)
index 0000000..f829536
--- /dev/null
@@ -0,0 +1,70 @@
+@qtype @qformat_xml
+Feature: Test importing questions from Moodle XML format.
+  In order to reuse questions
+  As an teacher
+  I need to be able to import them in XML format.
+
+  Background:
+    Given the following "courses" exist:
+      | fullname | shortname | format |
+      | Course 1 | C1        | topics |
+    And the following "users" exist:
+      | username | firstname |
+      | teacher  | Teacher   |
+    And the following "course enrolments" exist:
+      | user    | course | role           |
+      | teacher | C1     | editingteacher |
+    And I log in as "teacher"
+    And I follow "Course 1"
+
+  @javascript @_file_upload
+  Scenario: import some true/false questions from Moodle XML format
+    When I navigate to "Import" node in "Course administration > Question bank"
+    And I set the field "id_format_xml" to "1"
+    And I upload "question/format/xml/tests/fixtures/truefalse.xml" file to "Import" filemanager
+    And I press "id_submitbutton"
+    Then I should see "Parsing questions from import file."
+    And I should see "Importing 2 questions from file"
+    And I should see "is an acronym for Modular Object-Oriented Dynamic Learning Education"
+    And I should see "is an acronym for Modular Object-Oriented Dynamic Learning Environment"
+    When I press "Continue"
+    Then I should see "Moodle acronym (False)"
+    Then I should see "Moodle acronym (True)"
+
+  @javascript @_file_upload
+  Scenario: import some multiple choice questions from Moodle XML format
+    When I navigate to "Import" node in "Course administration > Question bank"
+    And I set the field "id_format_xml" to "1"
+    And I upload "question/format/xml/tests/fixtures/multichoice.xml" file to "Import" filemanager
+    And I press "id_submitbutton"
+    Then I should see "Parsing questions from import file."
+    And I should see "Importing 1 questions from file"
+    And I should see "What language is being spoken?"
+    When I press "Continue"
+    Then I should see "Greeting"
+
+  @javascript @_file_upload
+  Scenario: import some multi-answer questions from Moodle XML format
+    When I navigate to "Import" node in "Course administration > Question bank"
+    And I set the field "id_format_xml" to "1"
+    And I upload "question/format/xml/tests/fixtures/multianswer.xml" file to "Import" filemanager
+    And I press "id_submitbutton"
+    Then I should see "Parsing questions from import file."
+    And I should see "Importing 1 questions from file"
+    And I should see "Match the following cities with the correct state,"
+    When I press "Continue"
+    Then I should see "cloze with images"
+
+  @javascript @_file_upload
+  Scenario: import some questions with legacy-style images from Moodle XML format
+    When I navigate to "Import" node in "Course administration > Question bank"
+    And I set the field "id_format_xml" to "1"
+    And I upload "question/format/xml/tests/fixtures/sample_questions_with_old_image_tag.xml" file to "Import" filemanager
+    And I press "id_submitbutton"
+    Then I should see "Parsing questions from import file."
+    And I should see "Importing 2 questions from file"
+    And I should see "This is a multianswer question with an image in the old"
+    And I should see "This is a multichoice question with an image in the old"
+    When I press "Continue"
+    Then I should see "cloze question with image"
+    Then I should see "mcq with image"
index ef5b563..710777b 100644 (file)
@@ -77,7 +77,7 @@ class microsoft_skydrive extends oauth2_client {
      * @return string the auth url
      */
     protected function auth_url() {
-        return 'https://oauth.live.com/authorize';
+        return 'https://login.live.com/oauth20_authorize.srf';
     }
 
     /**
@@ -85,7 +85,7 @@ class microsoft_skydrive extends oauth2_client {
      * @return string the auth url
      */
     protected function token_url() {
-        return 'https://oauth.live.com/token';
+        return 'https://login.live.com/oauth20_token.srf';
     }
 
     /**