Merge branch 'MDL-50145_master' of git://github.com/markn86/moodle
authorEloy Lafuente (stronk7) <stronk7@moodle.org>
Tue, 9 Jun 2015 18:40:56 +0000 (20:40 +0200)
committerEloy Lafuente (stronk7) <stronk7@moodle.org>
Tue, 9 Jun 2015 18:40:56 +0000 (20:40 +0200)
32 files changed:
admin/phpinfo.php
admin/tool/log/classes/log/manager.php
blocks/course_overview/block_course_overview.php
calendar/tests/behat/behat_calendar.php
composer.json
composer.lock
course/modedit.php
course/moodleform_mod.php
lang/en/badges.php
lang/en/calendar.php
lang/en/deprecated.txt
lang/en/grades.php
lang/en/portfolio.php
lang/en/question.php
lang/en/repository.php
lang/en/role.php
lib/amd/build/url.min.js
lib/amd/src/url.js
lib/db/services.php
lib/formslib.php
lib/tests/string_manager_standard_test.php
mod/book/tests/behat/create_chapters.feature
mod/lesson/report.php
mod/quiz/lang/en/deprecated.txt
mod/quiz/lang/en/quiz.php
repository/youtube/lang/en/repository_youtube.php
repository/youtube/lib.php
repository/youtube/tests/generator/lib.php
repository/youtube/version.php
theme/bootstrapbase/layout/secure.php
theme/clean/layout/secure.php
version.php

index 6b0ffa0..ea44b1f 100644 (file)
@@ -11,7 +11,7 @@
     echo '<div class="phpinfo">';
 
     ob_start();
-    phpinfo(INFO_GENERAL + INFO_CONFIGURATION + INFO_MODULES);
+    phpinfo(INFO_GENERAL + INFO_CONFIGURATION + INFO_MODULES + INFO_VARIABLES);
     $html = ob_get_contents();
     ob_end_clean();
 
index 9c286f0..4077347 100644 (file)
@@ -106,7 +106,40 @@ class manager implements \core\log\manager {
             if (empty($interface) || ($reader instanceof $interface)) {
                 $return[$plugin] = $reader;
             }
+            // TODO MDL-49291 These conditions should be removed as part of the 2nd stage deprecation.
+            if ($reader instanceof \core\log\sql_internal_reader) {
+                debugging('\core\log\sql_internal_reader has been deprecated in favour of \core\log\sql_internal_table_reader.' .
+                    ' Update ' . get_class($reader) . ' to use the new interface.', DEBUG_DEVELOPER);
+            } else if ($reader instanceof \core\log\sql_select_reader) {
+                debugging('\core\log\sql_select_reader has been deprecated in favour of \core\log\sql_reader. Update ' .
+                    get_class($reader) . ' to use the new interface.', DEBUG_DEVELOPER);
+            }
+        }
+
+        // TODO MDL-49291 This section below (until the final return) should be removed as part of the 2nd stage deprecation.
+        $isselectreader = (ltrim($interface, '\\') === 'core\log\sql_select_reader');
+        $isinternalreader = (ltrim($interface, '\\') === 'core\log\sql_internal_reader');
+        if ($isselectreader || $isinternalreader) {
+
+            if ($isselectreader) {
+                $alternative = '\core\log\sql_reader';
+            } else {
+                $alternative = '\core\log\sql_internal_table_reader';
+            }
+
+            if (count($return) === 0) {
+                // If there are no classes implementing the provided interface and the provided interface is one of
+                // the deprecated ones, we return the non-deprecated alternatives. It should be safe as the new interface
+                // is adding a new method but not changing the existing ones.
+                debugging($interface . ' has been deprecated in favour of ' . $alternative . '. Returning ' . $alternative .
+                    ' instances instead. Please call get_readers() using the new interface.', DEBUG_DEVELOPER);
+                $return = $this->get_readers($alternative);
+            } else {
+                debugging($interface . ' has been deprecated in favour of ' . $alternative .
+                    '. Please call get_readers() using the new interface.', DEBUG_DEVELOPER);
+            }
         }
+
         return $return;
     }
 
index 2812cce..ae244ef 100644 (file)
@@ -112,7 +112,7 @@ class block_course_overview extends block_base {
      * @return array
      */
     public function applicable_formats() {
-        return array('my-index' => true);
+        return array('my' => true);
     }
 
     /**
@@ -125,4 +125,4 @@ class block_course_overview extends block_base {
         $config = get_config('block_course_overview');
         return !empty($config->showwelcomearea);
     }
-}
\ No newline at end of file
+}
index 9eded3c..70e57b2 100644 (file)
@@ -106,7 +106,9 @@ class behat_calendar extends behat_base {
      * @return Given[]
      */
     public function i_hover_over_today_in_the_calendar() {
-        $todaysday = trim(strftime('%e'));
+        // For window's compatibility, using %d and not %e.
+        $todaysday = trim(strftime('%d'));
+        $todaysday = ltrim($todaysday, '0');
         return $this->i_hover_over_day_of_this_month_in_calendar($todaysday);
     }
 }
index 2e7bc2c..bdb434a 100644 (file)
@@ -8,6 +8,6 @@
     "require-dev": {
         "phpunit/phpunit": "3.7.*",
         "phpunit/dbUnit": "1.2.*",
-        "moodlehq/behat-extension": "1.29.5"
+        "moodlehq/behat-extension": "1.30.0"
     }
 }
index 7cb9c18..e0268e8 100644 (file)
@@ -4,7 +4,7 @@
         "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
         "This file is @generated automatically"
     ],
-    "hash": "e57a3c4f1934098d77de54215b059ba1",
+    "hash": "a8ea9182a53569160119098b9cb80407",
     "packages": [],
     "packages-dev": [
         {
         },
         {
             "name": "moodlehq/behat-extension",
-            "version": "v1.29.5",
+            "version": "v1.30.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/moodlehq/moodle-behat-extension.git",
index aaa87e9..4b9edaf 100644 (file)
@@ -262,6 +262,12 @@ if ($mform->is_cancelled()) {
         redirect(course_get_url($course, $cw->section, array('sr' => $sectionreturn)));
     }
 } else if ($fromform = $mform->get_data()) {
+    // Convert the grade pass value - we may be using a language which uses commas,
+    // rather than decimal points, in numbers. These need to be converted so that
+    // they can be added to the DB.
+    if (isset($fromform->gradepass)) {
+        $fromform->gradepass = unformat_float($fromform->gradepass);
+    }
 
     if (!empty($fromform->update)) {
         list($cm, $fromform) = update_moduleinfo($cm, $fromform, $course, $mform);
index da113c3..0f7ab3f 100644 (file)
@@ -299,10 +299,19 @@ abstract class moodleform_mod extends moodleform {
             $errors['assessed'] = get_string('scaleselectionrequired', 'rating');
         }
 
+        // Check that the grade pass is a valid number.
+        $gradepassvalid = false;
+        if (isset($data['gradepass'])) {
+            if (unformat_float($data['gradepass'], true) === false) {
+                $errors['gradepass'] = get_string('err_numeric', 'form');
+            } else {
+                $gradepassvalid = true;
+            }
+        }
+
         // Grade to pass: ensure that the grade to pass is valid for points and scales.
         // If we are working with a scale, convert into a positive number for validation.
-
-        if (isset($data['gradepass']) && (!empty($data['grade']) || !empty($data['scale']))) {
+        if ($gradepassvalid && isset($data['gradepass']) && (!empty($data['grade']) || !empty($data['scale']))) {
             $scale = !empty($data['grade']) ? $data['grade'] : $data['scale'];
             if ($scale < 0) {
                 $scalevalues = $DB->get_record('scale', array('id' => -$scale));
@@ -672,8 +681,7 @@ abstract class moodleform_mod extends moodleform {
             $mform->addElement('text', 'gradepass', get_string('gradepass', 'grades'));
             $mform->addHelpButton('gradepass', 'gradepass', 'grades');
             $mform->setDefault('gradepass', '');
-            $mform->setType('gradepass', PARAM_FLOAT);
-            $mform->addRule('gradepass', null, 'numeric', null, 'client');
+            $mform->setType('gradepass', PARAM_RAW);
             if (!$this->_features->rating) {
                 $mform->disabledIf('gradepass', 'grade[modgrade_type]', 'eq', 'none');
             }
index 8eeed52..83ab453 100644 (file)
@@ -392,6 +392,3 @@ $string['visible'] = 'Visible';
 $string['warnexpired'] = ' (This badge has expired!)';
 $string['year'] = 'Year(s)';
 
-// Deprecated since Moodle 2.8.
-
-$string['hidden'] = 'Hidden';
index 7215442..f60a5bf 100644 (file)
@@ -216,6 +216,3 @@ $string['weekthis'] = 'This week';
 $string['yesterday'] = 'Yesterday';
 $string['youcandeleteallrepeats'] = 'This event is part of a repeating event series. You can delete this event only, or all {$a} events in the series at once.';
 
-// Deprecated since Moodle 2.8.
-
-$string['hidden'] = 'hidden';
index bdc0b10..83a6cb4 100644 (file)
@@ -1,12 +1,3 @@
-hidden,core_badges
-hidden,core_calendar
-hidden,core_portfolio
-hidden,core_question
-hidden,core_repository
-hidden,core_role
-simpleview,core_grades
-fullview,core_grades
-categoriesedit,core_grades
 lockingmeans,core_cache
 lockmethod,core_cache
 lockmethod_help,core_cache
index 4349bbe..8870da6 100644 (file)
@@ -774,9 +774,3 @@ $string['writinggradebookinfo'] = 'Writing gradebook settings';
 $string['xml'] = 'XML';
 $string['yes'] = 'Yes';
 $string['yourgrade'] = 'Your grade';
-
-// Deprecated since Moodle 2.8
-
-$string['categoriesedit'] = 'Edit setup';
-$string['fullview'] = 'Full view';
-$string['simpleview'] = 'Simple view';
index b9f013f..8e1ea6b 100644 (file)
@@ -180,6 +180,3 @@ $string['wait'] = 'Wait';
 $string['wanttowait_high'] = 'It is not recommended that you wait for this transfer to complete, but you can if you\'re sure and know what you\'re doing';
 $string['wanttowait_moderate'] = 'Do you want to wait for this transfer? It might take a few minutes';
 
-// Deprecated since Moodle 2.8.
-
-$string['hidden'] = 'Hidden';
index 10f7c99..126fb08 100644 (file)
@@ -443,6 +443,3 @@ $string['withselected'] = 'With selected';
 $string['xoutofmax'] = '{$a->mark} out of {$a->max}';
 $string['yougotnright'] = 'You have correctly selected {$a->num}.';
 
-// Deprecated since Moodle 2.8.
-
-$string['hidden'] = 'Hidden';
index 7291f80..845f099 100644 (file)
@@ -243,8 +243,5 @@ $string['wrongcontext'] = 'You cannot access to this context';
 $string['xhtmlerror'] = 'You are probably using an XHTML strict header. Certain YUI components don\'t work in this mode; please turn it off.';
 $string['ziped'] = 'Compress folder successfully';
 
-// Deprecated since Moodle 2.8.
-$string['hidden'] = 'Hidden';
-
 // Deprecated since Moodle 2.9.
 $string['personalrepositories'] = 'Available repository instances';
index 57ef1c1..c3e2378 100644 (file)
@@ -422,6 +422,3 @@ $string['whydoesusernothavecap'] = 'Why does {$a->fullname} not have capability
 $string['xroleassignments'] = '{$a}\'s role assignments';
 $string['xuserswiththerole'] = 'Users with the role "{$a->role}"';
 
-// Deprecated since Moodle 2.8.
-
-$string['hidden'] = 'Hidden';
index e287766..ae868b2 100644 (file)
Binary files a/lib/amd/build/url.min.js and b/lib/amd/build/url.min.js differ
index 908c898..b3263ee 100644 (file)
@@ -60,7 +60,7 @@ define(['core/config'], function(config) {
          */
         relativeUrl: function(relativePath) {
 
-            if (relativePath.indexOf('http:') === 0 || relativePath.indexOf('https:') === 0 || relativePath.indexOf('://')) {
+            if (relativePath.indexOf('http:') === 0 || relativePath.indexOf('https:') === 0 || relativePath.indexOf('://') >= 0) {
                 throw new Error('relativeUrl function does not accept absolute urls');
             }
 
index 7d725ff..d618b30 100644 (file)
@@ -1128,6 +1128,7 @@ $services = array(
             'gradereport_user_view_grade_report',
             'core_rating_get_item_ratings',
             'mod_url_view_url',
+            'core_user_get_users_by_field',
             ),
         'enabled' => 0,
         'restrictedusers' => 0,
index 31f32d7..a12e20d 100644 (file)
@@ -2138,7 +2138,7 @@ class MoodleQuickForm extends HTML_QuickForm_DHTMLRulesTableless {
 
 var skipClientValidation = false;
 
-function qf_errorHandler(element, _qfMsg) {
+function qf_errorHandler(element, _qfMsg, escapedName) {
   div = element.parentNode;
 
   if ((div == undefined) || (element.name == undefined)) {
@@ -2147,10 +2147,10 @@ function qf_errorHandler(element, _qfMsg) {
   }
 
   if (_qfMsg != \'\') {
-    var errorSpan = document.getElementById(\'id_error_\'+element.name);
+    var errorSpan = document.getElementById(\'id_error_\' + escapedName);
     if (!errorSpan) {
       errorSpan = document.createElement("span");
-      errorSpan.id = \'id_error_\'+element.name;
+      errorSpan.id = \'id_error_\' + escapedName;
       errorSpan.className = "error";
       element.parentNode.insertBefore(errorSpan, element.parentNode.firstChild);
       document.getElementById(errorSpan.id).setAttribute(\'TabIndex\', \'0\');
@@ -2168,17 +2168,17 @@ function qf_errorHandler(element, _qfMsg) {
         div.className += " error";
         linebreak = document.createElement("br");
         linebreak.className = "error";
-        linebreak.id = \'id_error_break_\'+element.name;
+        linebreak.id = \'id_error_break_\' + escapedName;
         errorSpan.parentNode.insertBefore(linebreak, errorSpan.nextSibling);
     }
 
     return false;
   } else {
-    var errorSpan = document.getElementById(\'id_error_\'+element.name);
+    var errorSpan = document.getElementById(\'id_error_\' + escapedName);
     if (errorSpan) {
       errorSpan.parentNode.removeChild(errorSpan);
     }
-    var linebreak = document.getElementById(\'id_error_break_\'+element.name);
+    var linebreak = document.getElementById(\'id_error_break_\' + escapedName);
     if (linebreak) {
       linebreak.parentNode.removeChild(linebreak);
     }
@@ -2203,7 +2203,7 @@ function qf_errorHandler(element, _qfMsg) {
                 create_function('$matches', 'return sprintf("_%2x",ord($matches[0]));'),
                 $elementName);
             $js .= '
-function validate_' . $this->_formName . '_' . $escapedElementName . '(element) {
+function validate_' . $this->_formName . '_' . $escapedElementName . '(element, escapedName) {
   if (undefined == element) {
      //required element was not found, then let form be submitted without client side validation
      return true;
@@ -2218,7 +2218,7 @@ function validate_' . $this->_formName . '_' . $escapedElementName . '(element)
         frm = frm.parentNode;
       }
     ' . join("\n", $jsArr) . '
-      return qf_errorHandler(element, _qfMsg);
+      return qf_errorHandler(element, _qfMsg, escapedName);
   } else {
     //element name should be defined else error msg will not be displayed.
     return true;
@@ -2226,12 +2226,12 @@ function validate_' . $this->_formName . '_' . $escapedElementName . '(element)
 }
 ';
             $validateJS .= '
-  ret = validate_' . $this->_formName . '_' . $escapedElementName.'(frm.elements[\''.$elementName.'\']) && ret;
+  ret = validate_' . $this->_formName . '_' . $escapedElementName.'(frm.elements[\''.$elementName.'\'], \''.$escapedElementName.'\') && ret;
   if (!ret && !first_focus) {
     first_focus = true;
     Y.Global.fire(M.core.globalEvents.FORM_ERROR, {formid: \''. $this->_attributes['id'] .'\',
-                                                   elementid: \'id_error_'.$elementName.'\'});
-    document.getElementById(\'id_error_'.$elementName.'\').focus();
+                                                   elementid: \'id_error_'.$escapedElementName.'\'});
+    document.getElementById(\'id_error_'.$escapedElementName.'\').focus();
   }
 ';
 
@@ -2239,7 +2239,7 @@ function validate_' . $this->_formName . '_' . $escapedElementName . '(element)
             //unset($element);
             //$element =& $this->getElement($elementName);
             //end of fix
-            $valFunc = 'validate_' . $this->_formName . '_' . $escapedElementName . '(this)';
+            $valFunc = 'validate_' . $this->_formName . '_' . $escapedElementName . '(this, \''.$escapedElementName.'\')';
             $onBlur = $element->getAttribute('onBlur');
             $onChange = $element->getAttribute('onChange');
             $element->updateAttributes(array('onBlur' => $onBlur . $valFunc,
index ae7fc1b..e2a6776 100644 (file)
@@ -75,11 +75,11 @@ class core_string_manager_standard_testcase extends advanced_testcase {
         $this->assertFalse($stringman->string_deprecated('hidden', 'grades'));
 
         // Check deprecated string.
-        $this->assertTrue($stringman->string_deprecated('hidden', 'repository'));
-        $this->assertTrue($stringman->string_exists('hidden', 'repository'));
+        $this->assertTrue($stringman->string_deprecated('timelimitmin', 'mod_quiz'));
+        $this->assertTrue($stringman->string_exists('timelimitmin', 'mod_quiz'));
         $this->assertDebuggingNotCalled();
-        $this->assertEquals('Hidden', get_string('hidden', 'repository'));
-        $this->assertDebuggingCalled('String [hidden,core_repository] is deprecated. '.
+        $this->assertEquals('Time limit (minutes)', get_string('timelimitmin', 'mod_quiz'));
+        $this->assertDebuggingCalled('String [timelimitmin,mod_quiz] is deprecated. '.
             'Either you should no longer be using that string, or the string has been incorrectly deprecated, in which case you should report this as a bug. '.
             'Please refer to https://docs.moodle.org/dev/String_deprecation');
     }
index 60035f7..c4c45c9 100644 (file)
@@ -4,24 +4,26 @@ Feature: In a book, create chapters and sub chapters
   As a teacher
   I need to add chapters and subchapters to a book.
 
-  Scenario: create chapters and sub chapters and naviagte between them
-    Given the following "courses" exist:
-      | fullname | shortname | category | groupmode |
-      | Course 1 | C1 | 0 | 1 |
-    And the following "users" exist:
+  Background:
+    Given the following "users" exist:
       | username | firstname | lastname | email |
       | teacher1 | Teacher | 1 | teacher1@example.com |
+    And the following "courses" exist:
+      | fullname | shortname | format |
+      | Course 1 | C1 | topics |
     And the following "course enrolments" exist:
       | user | course | role |
       | teacher1 | C1 | editingteacher |
     And I log in as "teacher1"
     And I follow "Course 1"
     And I turn editing mode on
-    When I add a "Book" to section "1" and I fill the form with:
+    And I add a "Book" to section "1" and I fill the form with:
       | Name | Test book |
       | Description | A book about dreams! |
-    And I follow "Test book"
-    Then I should see "Add new chapter"
+
+  Scenario: Create chapters and sub chapters and navigate between them
+    Given I follow "Test book"
+    And I should see "Add new chapter"
     And I set the following fields to these values:
       | Chapter title | Dummy first chapter |
       | Content | Dream is the start of a journey |
@@ -51,9 +53,27 @@ Feature: In a book, create chapters and sub chapters
     And I follow "Test book"
     And I should not see "Previous" in the ".book_content" "css_element"
     And I should see "1 Dummy first chapter" in the "strong" "css_element"
-    And I click on "Next" "link"
-    And I should see "1.1 Dummy first subchapter" in the ".book_content" "css_element"
+    When I click on "Next" "link"
+    Then I should see "1.1 Dummy first subchapter" in the ".book_content" "css_element"
     And I should see "1.1 Dummy first subchapter" in the "strong" "css_element"
     And I click on "Previous" "link"
     And I should see "1 Dummy first chapter" in the ".book_content" "css_element"
-    And I should see "1 Dummy first chapter" in the "strong" "css_element"
\ No newline at end of file
+    And I should see "1 Dummy first chapter" in the "strong" "css_element"
+
+  Scenario: Change editing mode for an individual chapter
+    Given I follow "Test book"
+    And I should see "Add new chapter"
+    And I set the following fields to these values:
+      | Chapter title | Dummy first chapter |
+      | Content | Dream is the start of a journey |
+    And I press "Save changes"
+    And I should see "1 Dummy first chapter" in the "Table of contents" "block"
+    And "Edit chapter \"1 Dummy first chapter\"" "link" should exist in the "Table of contents" "block"
+    And "Delete chapter \"1 Dummy first chapter\"" "link" should exist in the "Table of contents" "block"
+    And "Hide chapter \"1 Dummy first chapter\"" "link" should exist in the "Table of contents" "block"
+    And "Add new chapter" "link" should exist in the "Table of contents" "block"
+    When I click on "Turn editing off" "link" in the "Administration" "block"
+    Then "Edit chapter \"1 Dummy first chapter\"" "link" should not exist in the "Table of contents" "block"
+    And "Delete chapter \"1 Dummy first chapter\"" "link" should not exist in the "Table of contents" "block"
+    And "Hide chapter \"1 Dummy first chapter\"" "link" should not exist in the "Table of contents" "block"
+    And "Add new chapter" "link" should not exist in the "Table of contents" "block"
\ No newline at end of file
index bdf859d..b325a08 100644 (file)
@@ -42,28 +42,6 @@ $currentgroup = groups_get_activity_group($cm, true);
 $context = context_module::instance($cm->id);
 require_capability('mod/lesson:viewreports', $context);
 
-// Only load students if there attempts for this lesson.
-if ($attempts = $DB->record_exists('lesson_attempts', array('lessonid' => $lesson->id))) {
-    list($esql, $params) = get_enrolled_sql($context, '', $currentgroup, true);
-    list($sort, $sortparams) = users_order_by_sql('u');
-
-    $params['lessonid'] = $lesson->id;
-    $ufields = user_picture::fields('u');
-    $sql = "SELECT DISTINCT $ufields
-            FROM {user} u
-            JOIN {lesson_attempts} a ON u.id = a.userid
-            JOIN ($esql) ue ON ue.id = a.userid
-            WHERE a.lessonid = :lessonid
-            ORDER BY $sort";
-
-    $students = $DB->get_recordset_sql($sql, $params);
-    if (!$students->valid()) {
-        $nothingtodisplay = true;
-    }
-} else {
-    $nothingtodisplay = true;
-}
-
 $url = new moodle_url('/mod/lesson/report.php', array('id'=>$id));
 $url->param('action', $action);
 if ($pageid !== null) {
@@ -77,32 +55,6 @@ if ($action == 'reportoverview') {
 
 $lessonoutput = $PAGE->get_renderer('mod_lesson');
 
-$attempts = $DB->get_recordset('lesson_attempts', array('lessonid' => $lesson->id), 'timeseen');
-if (!$attempts->valid()) {
-    $nothingtodisplay = true;
-}
-
-if (! $grades = $DB->get_records('lesson_grades', array('lessonid' => $lesson->id), 'completed')) {
-    $grades = array();
-}
-
-if (! $times = $DB->get_records('lesson_timer', array('lessonid' => $lesson->id), 'starttime')) {
-    $times = array();
-}
-
-if ($nothingtodisplay) {
-    echo $lessonoutput->header($lesson, $cm, $action, false, null, get_string('nolessonattempts', 'lesson'));
-    if (!empty($currentgroup)) {
-        $groupname = groups_get_group_name($currentgroup);
-        echo $OUTPUT->notification(get_string('nolessonattemptsgroup', 'lesson', $groupname));
-    } else {
-        echo $OUTPUT->notification(get_string('nolessonattempts', 'lesson'));
-    }
-    groups_print_activity_menu($cm, $url);
-    echo $OUTPUT->footer();
-    exit();
-}
-
 if ($action === 'delete') {
     /// Process any form data before fetching attempts, grades and times
     if (has_capability('mod/lesson:edit', $context) and $form = data_submitted() and confirm_sesskey()) {
@@ -163,6 +115,54 @@ if ($action === 'delete') {
     /**************************************************************************
     this action is for default view and overview view
     **************************************************************************/
+
+    // Only load students if there attempts for this lesson.
+    if ($attempts = $DB->record_exists('lesson_attempts', array('lessonid' => $lesson->id))) {
+        list($esql, $params) = get_enrolled_sql($context, '', $currentgroup, true);
+        list($sort, $sortparams) = users_order_by_sql('u');
+
+        $params['lessonid'] = $lesson->id;
+        $ufields = user_picture::fields('u');
+        $sql = "SELECT DISTINCT $ufields
+            FROM {user} u
+            JOIN {lesson_attempts} a ON u.id = a.userid
+            JOIN ($esql) ue ON ue.id = a.userid
+            WHERE a.lessonid = :lessonid
+            ORDER BY $sort";
+
+        $students = $DB->get_recordset_sql($sql, $params);
+        if (!$students->valid()) {
+            $student->close();
+            $nothingtodisplay = true;
+        }
+    } else {
+        $nothingtodisplay = true;
+    }
+
+    if ($nothingtodisplay) {
+        echo $lessonoutput->header($lesson, $cm, $action, false, null, get_string('nolessonattempts', 'lesson'));
+        if (!empty($currentgroup)) {
+            $groupname = groups_get_group_name($currentgroup);
+            echo $OUTPUT->notification(get_string('nolessonattemptsgroup', 'lesson', $groupname));
+        } else {
+            echo $OUTPUT->notification(get_string('nolessonattempts', 'lesson'));
+        }
+        groups_print_activity_menu($cm, $url);
+        echo $OUTPUT->footer();
+        exit();
+    }
+
+    // We have attempts and students, let's prepare all the information.
+    $attempts = $DB->get_recordset('lesson_attempts', array('lessonid' => $lesson->id), 'timeseen');
+
+    if (! $grades = $DB->get_records('lesson_grades', array('lessonid' => $lesson->id), 'completed')) {
+        $grades = array();
+    }
+
+    if (! $times = $DB->get_records('lesson_timer', array('lessonid' => $lesson->id), 'starttime')) {
+        $times = array();
+    }
+
     echo $lessonoutput->header($lesson, $cm, $action, false, null, get_string('overview', 'lesson'));
     groups_print_activity_menu($cm, $url);
 
index 8162c48..b983d16 100644 (file)
@@ -1,10 +1,2 @@
-categories,mod_quiz
-category,mod_quiz
-export,mod_quiz
-import,mod_quiz
-invalidnumericanswer,mod_quiz
-invalidnumerictolerance,mod_quiz
-multiplier,mod_quiz
 timelimitmin,mod_quiz
 timelimitsec,mod_quiz
-unusedcategorydeleted,mod_quiz
index 820aa13..0ce34a7 100644 (file)
@@ -892,8 +892,6 @@ $string['timelimit'] = 'Time limit';
 $string['timelimit_help'] = 'If enabled, the time limit is stated on the initial quiz page and a countdown timer is displayed in the quiz navigation block.';
 $string['timelimit_link'] = 'mod/quiz/timing';
 $string['timelimitexeeded'] = 'Sorry! Quiz time limit exceeded!';
-$string['timelimitmin'] = 'Time limit (minutes)';
-$string['timelimitsec'] = 'Time limit (seconds)';
 $string['timestr'] = '%H:%M:%S on %d/%m/%y';
 $string['timesup'] = 'Time is up!';
 $string['timetaken'] = 'Time taken';
@@ -937,13 +935,7 @@ $string['xhtml'] = 'XHTML';
 $string['youneedtoenrol'] = 'You need to enrol in this course before you can attempt this quiz';
 $string['yourfinalgradeis'] = 'Your final grade for this quiz is {$a}.';
 
-// Deprecated since Moodle 2.8.
+// Deprecated since Moodle 2.9.
 
-$string['categories'] = 'Categories';
-$string['category'] = 'Category';
-$string['export'] = 'Export';
-$string['import'] = 'Import';
-$string['invalidnumericanswer'] = 'One of the answers you entered was not a valid number.';
-$string['invalidnumerictolerance'] = 'One of the tolerances you entered was not a valid number.';
-$string['multiplier'] = 'Multiplier';
-$string['unusedcategorydeleted'] = 'This category has been deleted because, after deleting the course, its questions weren\'t used any more.';
+$string['timelimitmin'] = 'Time limit (minutes)';
+$string['timelimitsec'] = 'Time limit (seconds)';
index dfee1f8..0a4955d 100644 (file)
@@ -23,6 +23,9 @@
  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
+$string['apierror'] = '{$a}';
+$string['apikey'] = 'API key';
+$string['information'] = '<div>Get a <a href="https://developers.google.com/youtube/v3/getting-started">Google API Key</a> for your Moodle site.</div>';
 $string['pluginname'] = 'Youtube videos';
 $string['search'] = 'Search videos';
 $string['youtube:view'] = 'Use youtube in file picker';
index b780128..51a756f 100644 (file)
@@ -23,6 +23,7 @@
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 require_once($CFG->dirroot . '/repository/lib.php');
+require_once($CFG->libdir . '/google/lib.php');
 
 /**
  * repository_youtube class
@@ -37,6 +38,24 @@ class repository_youtube extends repository {
     /** @var int maximum number of thumbs per page */
     const YOUTUBE_THUMBS_PER_PAGE = 27;
 
+    /**
+     * API key for using the YouTube Data API.
+     * @var mixed
+     */
+    private $apikey;
+
+    /**
+     * Google Client.
+     * @var Google_Client
+     */
+    private $client = null;
+
+    /**
+     * YouTube Service.
+     * @var Google_Service_YouTube
+     */
+    private $service = null;
+
     /**
      * Youtube plugin constructor
      * @param int $repositoryid
@@ -45,6 +64,45 @@ class repository_youtube extends repository {
      */
     public function __construct($repositoryid, $context = SYSCONTEXTID, $options = array()) {
         parent::__construct($repositoryid, $context, $options);
+
+        $this->apikey = $this->get_option('apikey');
+        $this->client = get_google_client();
+        $this->client->setDeveloperKey($this->apikey);
+        $this->client->setScopes(array(Google_Service_YouTube::YOUTUBE_READONLY));
+        $this->service = new Google_Service_YouTube($this->client);
+
+        // Without an API key, don't show this repo to users as its useless without it.
+        if (empty($this->apikey)) {
+            $this->disabled = true;
+        }
+    }
+
+    /**
+     * Save apikey in config table.
+     * @param array $options
+     * @return boolean
+     */
+    public function set_option($options = array()) {
+        if (!empty($options['apikey'])) {
+            set_config('apikey', trim($options['apikey']), 'youtube');
+        }
+        unset($options['apikey']);
+        return parent::set_option($options);
+    }
+
+    /**
+     * Get apikey from config table.
+     *
+     * @param string $config
+     * @return mixed
+     */
+    public function get_option($config = '') {
+        if ($config === 'apikey') {
+            return trim(get_config('youtube', 'apikey'));
+        } else {
+            $options['apikey'] = trim(get_config('youtube', 'apikey'));
+        }
+        return parent::get_option($config);
     }
 
     public function check_login() {
@@ -100,40 +158,63 @@ class repository_youtube extends repository {
      * @param int $start
      * @param int $max max results
      * @param string $sort
+     * @throws moodle_exception If the google API returns an error.
      * @return array
      */
     private function _get_collection($keyword, $start, $max, $sort) {
+        global $SESSION;
+
+        // The new API doesn't use "page" numbers for browsing through results.
+        // It uses a prev and next token in each set that you need to use to
+        // request the next page of results.
+        $sesspagetoken = 'youtube_'.$this->id.'_nextpagetoken';
+        $pagetoken = '';
+        if ($start > 1 && isset($SESSION->{$sesspagetoken})) {
+            $pagetoken = $SESSION->{$sesspagetoken};
+        }
+
         $list = array();
-        $this->feed_url = 'http://gdata.youtube.com/feeds/api/videos?q=' . urlencode($keyword) . '&format=5&start-index=' . $start . '&max-results=' .$max . '&orderby=' . $sort;
-        $c = new curl(array('cache'=>true, 'module_cache'=>'repository'));
-        $content = $c->get($this->feed_url);
-        $xml = simplexml_load_string($content);
-        $media = $xml->entry->children('http://search.yahoo.com/mrss/');
-        $links = $xml->children('http://www.w3.org/2005/Atom');
-        foreach ($xml->entry as $entry) {
-            $media = $entry->children('http://search.yahoo.com/mrss/');
-            $title = (string)$media->group->title;
-            $description = (string)$media->group->description;
-            if (empty($description)) {
-                $description = $title;
+        $error = null;
+        try {
+            $response = $this->service->search->listSearch('id,snippet', array(
+                'q' => $keyword,
+                'maxResults' => $max,
+                'order' => $sort,
+                'pageToken' => $pagetoken,
+                'type' => 'video',
+                'videoEmbeddable' => 'true',
+            ));
+
+            // Track the next page token for the next request (when a user
+            // scrolls down in the file picker for more videos).
+            $SESSION->{$sesspagetoken} = $response['nextPageToken'];
+
+            foreach ($response['items'] as $result) {
+                $title = $result->snippet->title;
+                $source = 'http://www.youtube.com/v/' . $result->id->videoId . '#' . $title;
+                $thumb = $result->snippet->getThumbnails()->getDefault();
+
+                $list[] = array(
+                    'shorttitle' => $title,
+                    'thumbnail_title' => $result->snippet->description,
+                    'title' => $title.'.avi', // This is a hack so we accept this file by extension.
+                    'thumbnail' => $thumb->url,
+                    'thumbnail_width' => (int)$thumb->width,
+                    'thumbnail_height' => (int)$thumb->height,
+                    'size' => '',
+                    'date' => '',
+                    'source' => $source,
+                );
             }
-            $attrs = $media->group->thumbnail[2]->attributes();
-            $thumbnail = $attrs['url'];
-            $arr = explode('/', $entry->id);
-            $id = $arr[count($arr)-1];
-            $source = 'http://www.youtube.com/v/' . $id . '#' . $title;
-            $list[] = array(
-                'shorttitle'=>$title,
-                'thumbnail_title'=>$description,
-                'title'=>$title.'.avi', // this is a hack so we accept this file by extension
-                'thumbnail'=>(string)$attrs['url'],
-                'thumbnail_width'=>(int)$attrs['width'],
-                'thumbnail_height'=>(int)$attrs['height'],
-                'size'=>'',
-                'date'=>'',
-                'source'=>$source
-            );
+        } catch (Google_Service_Exception $e) {
+            // If we throw the google exception as-is, we may expose the apikey
+            // to end users. The full message in the google exception includes
+            // the apikey param, so we take just the part pertaining to the
+            // actual error.
+            $error = $e->getErrors()[0]['message'];
+            throw new moodle_exception('apierror', 'repository_youtube', '', $error);
         }
+
         return $list;
     }
 
@@ -166,7 +247,7 @@ class repository_youtube extends repository {
                 'label' => get_string('sortrelevance', 'repository_youtube')
             ),
             (object)array(
-                'value' => 'published',
+                'value' => 'date',
                 'label' => get_string('sortpublished', 'repository_youtube')
             ),
             (object)array(
@@ -212,4 +293,31 @@ class repository_youtube extends repository {
     public function contains_private_data() {
         return false;
     }
+
+    /**
+     * Add plugin settings input to Moodle form.
+     * @param object $mform
+     * @param string $classname
+     */
+    public static function type_config_form($mform, $classname = 'repository') {
+        parent::type_config_form($mform, $classname);
+        $apikey = get_config('youtube', 'apikey');
+        if (empty($apikey)) {
+            $apikey = '';
+        }
+
+        $mform->addElement('text', 'apikey', get_string('apikey', 'repository_youtube'), array('value' => $apikey, 'size' => '40'));
+        $mform->setType('apikey', PARAM_RAW_TRIMMED);
+        $mform->addRule('apikey', get_string('required'), 'required', null, 'client');
+
+        $mform->addElement('static', null, '',  get_string('information', 'repository_youtube'));
+    }
+
+    /**
+     * Names of the plugin settings
+     * @return array
+     */
+    public static function get_type_option_names() {
+        return array('apikey', 'pluginname');
+    }
 }
index 3df0ad0..8af5550 100644 (file)
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 class repository_youtube_generator extends testing_repository_generator {
+
+    /**
+     * Fill in type record defaults.
+     *
+     * @param array $record
+     * @return array
+     */
+    protected function prepare_type_record(array $record) {
+        $record = parent::prepare_type_record($record);
+        if (!isset($record['apikey'])) {
+            $record['apikey'] = 'apikey';
+        }
+        return $record;
+    }
+
 }
index 23ad7da..7c26ad1 100644 (file)
@@ -26,6 +26,6 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$plugin->version   = 2015051100;        // The current plugin version (Date: YYYYMMDDXX)
+$plugin->version   = 2015052200;        // The current plugin version (Date: YYYYMMDDXX)
 $plugin->requires  = 2015050500;        // Requires this Moodle version
 $plugin->component = 'repository_youtube'; // Full name of the plugin (used for diagnostics)
index 6259268..4d39ae7 100644 (file)
@@ -51,9 +51,9 @@ echo $OUTPUT->doctype() ?>
 <header role="banner" class="navbar navbar-fixed-top moodle-has-zindex">
     <nav role="navigation" class="navbar-inner">
         <div class="container-fluid">
-            <a class="brand" href="<?php echo $CFG->wwwroot;?>"><?php echo
+            <span class="brand"><?php echo
                 format_string($SITE->shortname, true, array('context' => context_course::instance(SITEID)));
-                ?></a>
+                ?></span>
             <a class="btn btn-navbar" data-toggle="collapse" data-target=".nav-collapse">
                 <span class="icon-bar"></span>
                 <span class="icon-bar"></span>
@@ -62,6 +62,7 @@ echo $OUTPUT->doctype() ?>
             <div class="nav-collapse collapse">
                 <ul class="nav pull-right">
                     <li><?php echo $OUTPUT->page_heading_menu(); ?></li>
+                    <li class="navbar-text"><?php echo $OUTPUT->login_info(false) ?></li>
                 </ul>
             </div>
         </div>
index 8e398b7..5aadab0 100644 (file)
@@ -54,9 +54,9 @@ echo $OUTPUT->doctype() ?>
 <header role="banner" class="navbar navbar-fixed-top moodle-has-zindex">
     <nav role="navigation" class="navbar-inner">
         <div class="container-fluid">
-            <a class="brand" href="<?php echo $CFG->wwwroot;?>"><?php echo
+            <span class="brand"><?php echo
                 format_string($SITE->shortname, true, array('context' => context_course::instance(SITEID)));
-                ?></a>
+                ?></span>
             <a class="btn btn-navbar" data-toggle="collapse" data-target=".nav-collapse">
                 <span class="icon-bar"></span>
                 <span class="icon-bar"></span>
index 07e5ce6..7506bfa 100644 (file)
@@ -29,7 +29,7 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$version  = 2015060400.00;              // YYYYMMDD      = weekly release date of this DEV branch.
+$version  = 2015060400.01;              // YYYYMMDD      = weekly release date of this DEV branch.
                                         //         RR    = release increments - 00 in DEV branches.
                                         //           .XX = incremental changes.