Merge branch 'MDL-34228-m23' of git://github.com/sammarshallou/moodle into MOODLE_23_...
authorDan Poltawski <dan@moodle.com>
Tue, 16 Oct 2012 08:19:33 +0000 (16:19 +0800)
committerDan Poltawski <dan@moodle.com>
Tue, 16 Oct 2012 08:19:33 +0000 (16:19 +0800)
142 files changed:
admin/cli/install.php
admin/renderer.php
admin/settings/users.php
admin/tool/customlang/locallib.php
admin/tool/xmldb/lang/en/tool_xmldb.php
admin/webservice/service_users.php
blocks/community/forms.php
blocks/dock.js
blocks/online_users/block_online_users.php
cohort/lib.php
comment/locallib.php
course/dndupload.js
course/dnduploadlib.php
course/externallib.php
course/lib.php
course/tests/courselib_test.php
course/view.php
course/yui/dragdrop/dragdrop.js
enrol/ajax.php
enrol/database/lib.php
enrol/externallib.php
enrol/locallib.php
enrol/manual/ajax.php
enrol/manual/externallib.php
enrol/manual/manage.php
grade/report/user/lib.php
group/lib.php
install/lang/es_mx/install.php
install/lang/fi/install.php
install/lang/he/error.php
install/lang/he/install.php
install/lang/ro/install.php
install/lang/sr_cr/install.php
install/lang/uk/install.php [new file with mode: 0644]
lang/en/admin.php
lang/en/mimetypes.php
lib/adminlib.php
lib/blocklib.php
lib/db/upgrade.php
lib/enrollib.php
lib/filebrowser/file_browser.php
lib/filebrowser/file_info.php
lib/filebrowser/file_info_context_course.php
lib/filebrowser/file_info_context_coursecat.php
lib/filebrowser/file_info_context_module.php
lib/filebrowser/file_info_stored.php
lib/filelib.php
lib/formslib.php
lib/grade/grade_category.php
lib/grade/tests/grade_category_test.php
lib/javascript-static.js
lib/modinfolib.php
lib/navigationlib.php
lib/outputcomponents.php
lib/pagelib.php
lib/phpunit/classes/data_generator.php
lib/phpunit/classes/util.php
lib/phpunit/tests/generator_test.php
lib/pluginlib.php
lib/questionlib.php
lib/tests/outputcomponents_test.php
lib/upgradelib.php
lib/yui/dragdrop/dragdrop.js
mod/assign/gradeform.php
mod/assign/gradingoptionsform.php
mod/assign/lang/en/assign.php
mod/assign/lib.php
mod/assign/locallib.php
mod/book/lang/en/book.php
mod/book/locallib.php
mod/book/tool/importhtml/index.php
mod/book/view.php
mod/data/field/date/field.class.php
mod/data/lang/en/data.php
mod/data/lib.php
mod/data/locallib.php
mod/data/tests/fixtures/test_data_records.csv
mod/data/tests/search_test.php
mod/data/view.php
mod/forum/lib.php
mod/forum/locallib.php
mod/glossary/lib.php
mod/glossary/locallib.php
mod/imscp/locallib.php
mod/lesson/importppt.php [deleted file]
mod/lesson/importpptlib.php [deleted file]
mod/lesson/renderer.php
mod/lesson/version.php
mod/quiz/cronlib.php
mod/quiz/locallib.php
mod/quiz/module.js
mod/quiz/styles.css
mod/quiz/tests/locallib_test.php
mod/resource/lib.php
mod/scorm/lib.php
mod/upgrade.txt
mod/workshop/fileinfolib.php
pix/f/epub.png [new file with mode: 0644]
question/behaviour/rendererbase.php
question/engine/datalib.php
question/engine/questionattemptstep.php
question/format/xhtml/format.php
question/type/calculated/lang/en/qtype_calculated.php
question/type/calculated/questiontype.php
question/type/essay/renderer.php
question/type/match/lang/en/qtype_match.php
question/type/match/renderer.php
question/type/multianswer/module.js
question/type/multianswer/renderer.php
question/type/multianswer/tests/helper.php
question/type/multianswer/tests/walkthrough_test.php
question/type/numerical/lang/en/qtype_numerical.php
question/type/numerical/question.php
question/type/numerical/renderer.php
question/type/numerical/tests/question_test.php
question/type/shortanswer/question.php
question/type/shortanswer/questiontype.php
question/type/shortanswer/renderer.php
repository/filepicker.js
repository/lib.php
repository/local/lib.php
repository/upload/lang/en/repository_upload.php
theme/anomaly/config.php
theme/anomaly/pix/menu/nav-arrow-left.jpg [new file with mode: 0644]
theme/anomaly/pix/menu/nav-arrow-right.jpg [new file with mode: 0644]
theme/anomaly/pix/menu/nav-arrowover-left.jpg [new file with mode: 0644]
theme/anomaly/pix/menu/nav-arrowover-right.jpg [new file with mode: 0644]
theme/anomaly/renderers.php
theme/anomaly/style/general.css
theme/anomaly/style/menu.css [new file with mode: 0644]
theme/base/style/core.css
theme/canvas/layout/frontpage.php
theme/canvas/layout/general.php
theme/canvas/layout/report.php
theme/formal_white/lang/en/theme_formal_white.php
theme/serenity/style/core.css
user/lib.php
user/selector/module.js
user/selector/search.php
user/tests/externallib_test.php [new file with mode: 0644]
version.php
webservice/lib.php

index 79ff0b7..38aa06e 100644 (file)
@@ -240,18 +240,8 @@ echo get_string('cliinstallheader', 'install', $CFG->target_release)."\n";
 if ($interactive) {
     cli_separator();
     $languages = get_string_manager()->get_list_of_translations();
-    // format the langs nicely - 3 per line
-    $c = 0;
-    $langlist = '';
-    foreach ($languages as $key=>$lang) {
-        $c++;
-        $length = iconv_strlen($lang, 'UTF-8');
-        $padded = $lang.str_repeat(' ', 38-$length);
-        $langlist .= $padded;
-        if ($c % 3 == 0) {
-            $langlist .= "\n";
-        }
-    }
+    // Do not put the langs into columns because it is not compatible with RTL.
+    $langlist = implode("\n", $languages);
     $default = $CFG->lang;
     cli_heading(get_string('availablelangs', 'install'));
     echo $langlist."\n";
index 3a3c2bd..56c9056 100644 (file)
@@ -470,7 +470,7 @@ class core_admin_renderer extends plugin_renderer_base {
         $copyrighttext = '<a href="http://moodle.org/">Moodle</a> '.
                          '<a href="http://docs.moodle.org/dev/Releases" title="'.$CFG->version.'">'.$CFG->release.'</a><br />'.
                          'Copyright &copy; 1999 onwards, Martin Dougiamas<br />'.
-                         'and <a href="http://docs.moodle.org/dev/Credits">many other contributors</a>.<br />'.
+                         'and <a href="http://moodle.org/dev">many other contributors</a>.<br />'.
                          '<a href="http://docs.moodle.org/dev/License">GNU Public License</a>';
         //////////////////////////////////////////////////////////////////////////////////////////////////
 
index f317102..232745d 100644 (file)
@@ -150,6 +150,7 @@ if ($hassiteconfig
                     'institution' => new lang_string('institution'),
                 )));
         $temp->add(new admin_setting_configcheckbox('enablegravatar', new lang_string('enablegravatar', 'admin'), new lang_string('enablegravatar_help', 'admin'), 0));
+        $temp->add(new admin_setting_configtext('gravatardefaulturl', new lang_string('gravatardefaulturl', 'admin'), new lang_string('gravatardefaulturl_help', 'admin'), 'mm'));
     }
 
     $ADMIN->add('roles', $temp);
index 47bbdfa..878eb4c 100644 (file)
@@ -488,7 +488,7 @@ class tool_customlang_translator implements renderable {
         list($insql, $inparams) = $DB->get_in_or_equal($filter->component, SQL_PARAMS_NAMED);
 
         $csql = "SELECT COUNT(*)";
-        $fsql = "SELECT s.id, s.*, c.name AS component";
+        $fsql = "SELECT s.*, c.name AS component";
         $sql  = "  FROM {tool_customlang_components} c
                    JOIN {tool_customlang} s ON s.componentid = c.id
                   WHERE s.lang = :lang
index 7b384b2..c1475be 100644 (file)
@@ -111,10 +111,13 @@ $string['checkindexes'] = 'Check indexes';
 $string['check_indexes'] = 'Look for missing DB indexes';
 $string['checkoraclesemantics'] = 'Check semantics';
 $string['check_oracle_semantics'] = 'Look for incorrect length semantics';
+$string['duplicateindexname'] = 'Duplicate index name';
 $string['incorrectfieldname'] = 'Incorrect name';
 $string['index'] = 'Index';
 $string['indexes'] = 'Indexes';
+$string['indexnameempty'] = 'Index name is empty';
 $string['integerincorrectlength'] = 'Incorrect length for integer field';
+$string['incorrectindexname'] = 'Incorrect index name';
 $string['incorrectkeyname'] = 'Incorrect key name';
 $string['incorrecttablename'] = 'Incorrect table name';
 $string['key'] = 'Key';
index 12db2b0..ad67c66 100644 (file)
@@ -97,7 +97,7 @@ $usersmissingcaps = $webservicemanager->get_missing_capabilities_by_users($allow
 
 //add the missing capabilities to the allowed users object to be displayed by renderer
 foreach ($allowedusers as &$alloweduser) {
-    if (!is_siteadmin($alloweduser->id) and key_exists($alloweduser->id, $usersmissingcaps)) {
+    if (!is_siteadmin($alloweduser->id) and array_key_exists($alloweduser->id, $usersmissingcaps)) {
         $alloweduser->missingcapabilities = implode(', ', $usersmissingcaps[$alloweduser->id]);
     }
 }
index fafab8a..5026d5b 100644 (file)
@@ -140,7 +140,7 @@ class community_hub_search_form extends moodleform {
             $options = array();
             $firsthub = false;
             foreach ($hubs as $hub) {
-                if (key_exists('id', $hub)) {
+                if (array_key_exists('id', $hub)) {
                     $params = array('hubid' => $hub['id'],
                         'filetype' => HUB_HUBSCREENSHOT_FILE_TYPE);
                     $imgurl = new moodle_url(HUB_HUBDIRECTORYURL .
index 4eff68c..3c8a31b 100644 (file)
@@ -958,7 +958,7 @@ M.core_dock.genericblock.prototype = {
         placeholder.replace(this.Y.Node.getDOMNode(this.cachedcontentnode));
         this.cachedcontentnode = this.Y.one('#'+this.cachedcontentnode.get('id'));
 
-        var commands = this.cachedcontentnode.one('.title .commands');
+        var commands = dockitem.commands;
         if (commands) {
             commands.all('.hidepanelicon').remove();
             commands.all('.moveto').remove();
index cbf1dac..2efe9a2 100644 (file)
@@ -31,7 +31,8 @@ class block_online_users extends block_base {
         if (isset($CFG->block_online_users_timetosee)) {
             $timetoshowusers = $CFG->block_online_users_timetosee * 60;
         }
-        $timefrom = 100 * floor((time()-$timetoshowusers) / 100); // Round to nearest 100 seconds for better query cache
+        $now = time();
+        $timefrom = 100 * floor(($now - $timetoshowusers) / 100); // Round to nearest 100 seconds for better query cache
 
         //Calculate if we are in separate groups
         $isseparategroups = ($this->page->course->groupmode == SEPARATEGROUPS
@@ -53,18 +54,23 @@ class block_online_users extends block_base {
         }
 
         $userfields = user_picture::fields('u', array('username'));
-
+        $params['now'] = $now;
+        $params['timefrom'] = $timefrom;
         if ($this->page->course->id == SITEID or $this->page->context->contextlevel < CONTEXT_COURSE) {  // Site-level
             $sql = "SELECT $userfields, MAX(u.lastaccess) AS lastaccess
                       FROM {user} u $groupmembers
-                     WHERE u.lastaccess > $timefrom
+                     WHERE u.lastaccess > :timefrom
+                           AND u.lastaccess <= :now
+                           AND u.deleted = 0
                            $groupselect
                   GROUP BY $userfields
                   ORDER BY lastaccess DESC ";
 
            $csql = "SELECT COUNT(u.id)
                       FROM {user} u $groupmembers
-                     WHERE u.lastaccess > $timefrom
+                     WHERE u.lastaccess > :timefrom
+                           AND u.lastaccess <= :now
+                           AND u.deleted = 0
                            $groupselect";
 
         } else {
@@ -77,9 +83,11 @@ class block_online_users extends block_base {
             $sql = "SELECT $userfields, MAX(ul.timeaccess) AS lastaccess
                       FROM {user_lastaccess} ul $groupmembers, {user} u
                       JOIN ($esqljoin) euj ON euj.id = u.id
-                     WHERE ul.timeaccess > $timefrom
+                     WHERE ul.timeaccess > :timefrom
                            AND u.id = ul.userid
                            AND ul.courseid = :courseid
+                           AND ul.timeaccess <= :now
+                           AND u.deleted = 0
                            $groupselect
                   GROUP BY $userfields
                   ORDER BY lastaccess DESC";
@@ -87,9 +95,11 @@ class block_online_users extends block_base {
            $csql = "SELECT COUNT(u.id)
                       FROM {user_lastaccess} ul $groupmembers, {user} u
                       JOIN ($esqljoin) euj ON euj.id = u.id
-                     WHERE ul.timeaccess > $timefrom
+                     WHERE ul.timeaccess > :timefrom
                            AND u.id = ul.userid
                            AND ul.courseid = :courseid
+                           AND ul.timeaccess <= :now
+                           AND u.deleted = 0
                            $groupselect";
 
             $params['courseid'] = $this->page->course->id;
@@ -138,7 +148,7 @@ class block_online_users extends block_base {
             }
             foreach ($users as $user) {
                 $this->content->text .= '<li class="listentry">';
-                $timeago = format_time(time() - $user->lastaccess); //bruno to calculate correctly on frontpage
+                $timeago = format_time($now - $user->lastaccess); //bruno to calculate correctly on frontpage
 
                 if (isguestuser($user)) {
                     $this->content->text .= '<div class="user">'.$OUTPUT->user_picture($user, array('size'=>16));
index 367b19e..4ab8ab7 100644 (file)
@@ -126,7 +126,7 @@ function cohort_delete_category($category) {
 }
 
 /**
- * Remove cohort member
+ * Add cohort member
  * @param  int $cohortid
  * @param  int $userid
  * @return void
@@ -143,7 +143,7 @@ function cohort_add_member($cohortid, $userid) {
 }
 
 /**
- * Add cohort member
+ * Remove cohort member
  * @param  int $cohortid
  * @param  int $userid
  * @return void
index 5a8b0d7..31b2799 100644 (file)
@@ -94,7 +94,7 @@ class comment_manager {
      */
     private function setup_course($courseid) {
         global $PAGE, $DB;
-        if (!empty($this->course)) {
+        if (!empty($this->course) && $this->course->id == $courseid) {
             // already set, stop
             return;
         }
index 7bfef1d..2dcaa13 100644 (file)
@@ -445,6 +445,7 @@ M.course_dndupload = {
             a: document.createElement('a'),
             icon: document.createElement('img'),
             namespan: document.createElement('span'),
+            groupingspan: document.createElement('span'),
             progressouter: document.createElement('span'),
             progress: document.createElement('span')
         };
@@ -469,6 +470,9 @@ M.course_dndupload = {
 
         resel.div.appendChild(document.createTextNode(' '));
 
+        resel.groupingspan.className = 'groupinglabel';
+        resel.div.appendChild(resel.groupingspan);
+
         resel.progressouter.className = 'dndupload-progress-outer';
         resel.progress.className = 'dndupload-progress-inner';
         resel.progress.innerHTML = '&nbsp;';
@@ -724,6 +728,13 @@ M.course_dndupload = {
                             resel.icon.src = result.icon;
                             resel.a.href = result.link;
                             resel.namespan.innerHTML = result.name;
+
+                            if (result.groupingname) {
+                                resel.groupingspan.innerHTML = '(' + result.groupingname + ')';
+                            } else {
+                                resel.div.removeChild(resel.groupingspan);
+                            }
+
                             resel.div.removeChild(resel.progressouter);
                             resel.li.id = result.elementid;
                             resel.div.innerHTML += result.commands;
@@ -904,6 +915,13 @@ M.course_dndupload = {
                             resel.icon.src = result.icon;
                             resel.a.href = result.link;
                             resel.namespan.innerHTML = result.name;
+
+                            if (result.groupingname) {
+                                resel.groupingspan.innerHTML = '(' + result.groupingname + ')';
+                            } else {
+                                resel.div.removeChild(resel.groupingspan);
+                            }
+
                             resel.div.removeChild(resel.progressouter);
                             resel.li.id = result.elementid;
                             resel.div.innerHTML += result.commands;
index 816924b..038c09b 100644 (file)
@@ -667,6 +667,12 @@ class dndupload_ajax_processor {
         $resp->commands = make_editing_buttons($mod, true, true, 0, $mod->sectionnum);
         $resp->onclick = $mod->get_on_click();
 
+        // if using groupings, then display grouping name
+        if (!empty($mod->groupingid) && has_capability('moodle/course:managegroups', $this->context)) {
+            $groupings = groups_get_all_groupings($this->course->id);
+            $resp->groupingname = format_string($groupings[$mod->groupingid]->name);
+        }
+
         echo $OUTPUT->header();
         echo json_encode($resp);
         die();
index b18c0a4..88fd0fa 100644 (file)
@@ -285,7 +285,7 @@ class core_course_external extends external_api {
                         array('options' => $options));
 
         //retrieve courses
-        if (!key_exists('ids', $params['options'])
+        if (!array_key_exists('ids', $params['options'])
                 or empty($params['options']['ids'])) {
             $courses = $DB->get_records('course');
         } else {
@@ -523,12 +523,12 @@ class core_course_external extends external_api {
             require_capability('moodle/course:create', $context);
 
             // Make sure lang is valid
-            if (key_exists('lang', $course) and empty($availablelangs[$course['lang']])) {
+            if (array_key_exists('lang', $course) and empty($availablelangs[$course['lang']])) {
                 throw new moodle_exception('errorinvalidparam', 'webservice', '', 'lang');
             }
 
             // Make sure theme is valid
-            if (key_exists('forcetheme', $course)) {
+            if (array_key_exists('forcetheme', $course)) {
                 if (!empty($CFG->allowcoursethemes)) {
                     if (empty($availablethemes[$course['forcetheme']])) {
                         throw new moodle_exception('errorinvalidparam', 'webservice', '', 'forcetheme');
@@ -547,10 +547,10 @@ class core_course_external extends external_api {
             //set default value for completion
             $courseconfig = get_config('moodlecourse');
             if (completion_info::is_enabled_for_site()) {
-                if (!key_exists('enablecompletion', $course)) {
+                if (!array_key_exists('enablecompletion', $course)) {
                     $course['enablecompletion'] = $courseconfig->enablecompletion;
                 }
-                if (!key_exists('completionstartonenrol', $course)) {
+                if (!array_key_exists('completionstartonenrol', $course)) {
                     $course['completionstartonenrol'] = $courseconfig->completionstartonenrol;
                 }
             } else {
index 955b58c..ef150d0 100644 (file)
@@ -531,7 +531,7 @@ function print_mnet_log($hostid, $course, $user=0, $date=0, $order="l.time ASC",
 
 function print_log_csv($course, $user, $date, $order='l.time DESC', $modname,
                         $modid, $modaction, $groupid) {
-    global $DB;
+    global $DB, $CFG;
 
     $text = get_string('course')."\t".get_string('time')."\t".get_string('ip_address')."\t".
             get_string('fullnameuser')."\t".get_string('action')."\t".get_string('info');
@@ -600,6 +600,8 @@ function print_log_csv($course, $user, $date, $order='l.time DESC', $modname,
         $firstField = format_string($courses[$log->course], true, array('context' => $coursecontext));
         $fullname = fullname($log, has_capability('moodle/site:viewfullnames', $coursecontext));
         $row = array($firstField, userdate($log->time, $strftimedatetime), $log->ip, $fullname, $log->module.' '.$log->action, $log->info);
+        $actionurl = $CFG->wwwroot. make_log_url($log->module,$log->url);\r
+        $row = array($firstField, userdate($log->time, $strftimedatetime), $log->ip, $fullname, $log->module.' '.$log->action.' ('.$actionurl.')', $log->info);
         $text = implode("\t", $row);
         echo $text." \n";
     }
@@ -710,7 +712,8 @@ function print_log_xls($course, $user, $date, $order='l.time DESC', $modname,
         $myxls->write($row, 2, $log->ip, '');
         $fullname = fullname($log, has_capability('moodle/site:viewfullnames', $coursecontext));
         $myxls->write($row, 3, $fullname, '');
-        $myxls->write($row, 4, $log->module.' '.$log->action, '');
+        $actionurl = $CFG->wwwroot. make_log_url($log->module,$log->url);
+        $myxls->write($row, 4, $log->module.' '.$log->action.' ('.$actionurl.')', '');
         $myxls->write($row, 5, $log->info, '');
 
         $row++;
@@ -823,7 +826,8 @@ function print_log_ods($course, $user, $date, $order='l.time DESC', $modname,
         $myxls->write_string($row, 2, $log->ip);
         $fullname = fullname($log, has_capability('moodle/site:viewfullnames', $coursecontext));
         $myxls->write_string($row, 3, $fullname);
-        $myxls->write_string($row, 4, $log->module.' '.$log->action);
+        $actionurl = $CFG->wwwroot. make_log_url($log->module,$log->url);
+        $myxls->write_string($row, 4, $log->module.' '.$log->action.' ('.$actionurl.')');
         $myxls->write_string($row, 5, $log->info);
 
         $row++;
@@ -1572,7 +1576,7 @@ function print_section($course, $section, $mods, $modnamesused, $absolute=false,
                     // Display link itself
                     echo '<a ' . $linkcss . $mod->extra . $onclick .
                             ' href="' . $url . '"><img src="' . $mod->get_icon_url() .
-                            '" class="activityicon" alt="" /> ' .
+                            '" class="activityicon" alt="' . $modulename . '" /> ' .
                             $accesstext . '<span class="instancename">' .
                             $instancename . $altname . '</span></a>';
 
index 42b8057..acf5963 100644 (file)
@@ -36,7 +36,7 @@ class courselib_testcase extends advanced_testcase {
         $course = $this->getDataGenerator()->create_course(array('numsections'=>10), array('createsections'=>true));
         $oldsections = array();
         $sections = array();
-        foreach ($DB->get_records('course_sections', array('course'=>$course->id)) as $section) {
+        foreach ($DB->get_records('course_sections', array('course'=>$course->id), 'id') as $section) {
             $oldsections[$section->section] = $section->id;
             $sections[$section->id] = $section->section;
         }
index cb978d8..18f0313 100644 (file)
@@ -20,6 +20,7 @@
     $marker      = optional_param('marker',-1 , PARAM_INT);
     $switchrole  = optional_param('switchrole',-1, PARAM_INT);
     $modchooser  = optional_param('modchooser', -1, PARAM_BOOL);
+    $return      = optional_param('return', 0, PARAM_LOCALURL);
 
     $params = array();
     if (!empty($name)) {
             // Redirect to site root if Editing is toggled on frontpage
             if ($course->id == SITEID) {
                 redirect($CFG->wwwroot .'/?redirect=0');
+            } else if (!empty($return)) {
+                redirect($CFG->wwwroot . $return);
             } else {
                 $url = new moodle_url($PAGE->url, array('notifyeditingon' => 1));
                 redirect($url);
             // Redirect to site root if Editing is toggled on frontpage
             if ($course->id == SITEID) {
                 redirect($CFG->wwwroot .'/?redirect=0');
+            } else if (!empty($return)) {
+                redirect($CFG->wwwroot . $return);
             } else {
                 redirect($PAGE->url);
             }
index f52497e..bd35bd9 100644 (file)
@@ -294,13 +294,13 @@ YUI.add('moodle-course-dragdrop', function(Y) {
                     var resources = Y.Node.create('<ul></ul>');
                     resources.addClass(CSS.SECTION);
                     sectionnode.one('.'+CSS.CONTENT+' div.'+CSS.SUMMARY).insert(resources, 'after');
-                    // Define empty ul as droptarget, so that item could be moved to empty list
-                    var tar = new Y.DD.Drop({
-                        node: resources,
-                        groups: this.groups,
-                        padding: '20 0 20 0'
-                    });
                 }
+                // Define empty ul as droptarget, so that item could be moved to empty list
+                var tar = new Y.DD.Drop({
+                    node: resources,
+                    groups: this.groups,
+                    padding: '20 0 20 0'
+                });
 
                 // Initialise each resource/activity in this section
                 this.setup_for_resource('#'+sectionnode.get('id')+' li.'+CSS.ACTIVITY);
index 8a0bdfc..48b3fca 100644 (file)
@@ -54,9 +54,9 @@ echo $OUTPUT->header(); // send headers
 
 $manager = new course_enrolment_manager($PAGE, $course);
 
-$outcome = new stdClass;
+$outcome = new stdClass();
 $outcome->success = true;
-$outcome->response = new stdClass;
+$outcome->response = new stdClass();
 $outcome->error = '';
 
 switch ($action) {
@@ -99,6 +99,10 @@ switch ($action) {
             $user->fullname = fullname($user);
             unset($user->id);
         }
+        // Chrome will display users in the order of the array keys, so we need
+        // to ensure that the results ordered array keys. Fortunately, the JavaScript
+        // does not care what the array keys are. It uses user.id where necessary.
+        $outcome->response['users'] = array_values($outcome->response['users']);
         $outcome->success = true;
         break;
     default:
index 1755a89..286bb00 100644 (file)
@@ -481,9 +481,8 @@ class enrol_database_plugin extends enrol_plugin {
                 }
                 $rs->Close();
             } else {
-                mtrace('Error while communicating with external enrolment database');
-                $extdb->Close();
-                return;
+                mtrace("  error: skipping course '$course->mapping' - could not match with external database");
+                continue;
             }
             unset($user_mapping);
 
@@ -630,7 +629,7 @@ class enrol_database_plugin extends enrol_plugin {
         if ($idnumber) {
             $sqlfields[] = $idnumber;
         }
-        $sql = $this->db_get_sql($table, array(), $sqlfields);
+        $sql = $this->db_get_sql($table, array(), $sqlfields, true);
         $createcourses = array();
         if ($rs = $extdb->Execute($sql)) {
             if (!$rs->EOF) {
index 7d2fb5a..7e8dd4c 100644 (file)
@@ -388,7 +388,7 @@ class core_role_external extends external_api {
             // throw an exception if user is not able to assign the role in this context
             $roles = get_assignable_roles($context, ROLENAME_SHORT);
 
-            if (!key_exists($assignment['roleid'], $roles)) {
+            if (!array_key_exists($assignment['roleid'], $roles)) {
                 throw new invalid_parameter_exception('Can not assign roleid='.$assignment['roleid'].' in contextid='.$assignment['contextid']);
             }
 
@@ -451,7 +451,7 @@ class core_role_external extends external_api {
 
             // throw an exception if user is not able to unassign the role in this context
             $roles = get_assignable_roles($context, ROLENAME_SHORT);
-            if (!key_exists($unassignment['roleid'], $roles)) {
+            if (!array_key_exists($unassignment['roleid'], $roles)) {
                 throw new invalid_parameter_exception('Can not unassign roleid='.$unassignment['roleid'].' in contextid='.$unassignment['contextid']);
             }
 
index 41b7337..e923c2e 100644 (file)
@@ -766,13 +766,15 @@ class course_enrolment_manager {
 
         $users = array();
         foreach ($userroles as $userrole) {
+            $contextid = $userrole->contextid;
+            unset($userrole->contextid); // This would collide with user avatar.
             if (!array_key_exists($userrole->id, $users)) {
                 $users[$userrole->id] = $this->prepare_user_for_display($userrole, $extrafields, $now);
             }
             $a = new stdClass;
             $a->role = $roles[$userrole->roleid]->localname;
             $changeable = ($userrole->component == '');
-            if ($userrole->contextid == $this->context->id) {
+            if ($contextid == $this->context->id) {
                 $roletext = get_string('rolefromthiscourse', 'enrol', $a);
             } else {
                 $changeable = false;
index a9d5bb7..b599b95 100644 (file)
@@ -53,9 +53,9 @@ echo $OUTPUT->header(); // send headers
 
 $manager = new course_enrolment_manager($PAGE, $course);
 
-$outcome = new stdClass;
+$outcome = new stdClass();
 $outcome->success = true;
-$outcome->response = new stdClass;
+$outcome->response = new stdClass();
 $outcome->error = '';
 
 switch ($action) {
@@ -79,6 +79,10 @@ switch ($action) {
             }
             $user->extrafields = implode(', ', $fieldvalues);
         }
+        // Chrome will display users in the order of the array keys, so we need
+        // to ensure that the results ordered array keys. Fortunately, the JavaScript
+        // does not care what the array keys are. It uses user.id where necessary.
+        $outcome->response['users'] = array_values($outcome->response['users']);
         $outcome->success = true;
         break;
     case 'enrol':
@@ -135,4 +139,4 @@ switch ($action) {
         throw new enrol_ajax_exception('unknowajaxaction');
 }
 
-echo json_encode($outcome);
\ No newline at end of file
+echo json_encode($outcome);
index 44d90ac..81907a8 100644 (file)
@@ -101,7 +101,7 @@ class enrol_manual_external extends external_api {
 
             //throw an exception if user is not able to assign the role
             $roles = get_assignable_roles($context);
-            if (!key_exists($enrolment['roleid'], $roles)) {
+            if (!array_key_exists($enrolment['roleid'], $roles)) {
                 $errorparams = new stdClass();
                 $errorparams->roleid = $enrolment['roleid'];
                 $errorparams->courseid = $enrolment['courseid'];
index f7ea7e2..f458a91 100644 (file)
@@ -64,7 +64,7 @@ $PAGE->set_heading($course->fullname);
 navigation_node::override_active_url(new moodle_url('/enrol/users.php', array('id'=>$course->id)));
 
 // Create the user selector objects.
-$options = array('enrolid' => $enrolid);
+$options = array('enrolid' => $enrolid, 'accesscontext' => $context);
 
 $potentialuserselector = new enrol_manual_potential_participant('addselect', $options);
 $currentuserselector = new enrol_manual_current_participant('removeselect', $options);
index 915aefd..cd2e951 100644 (file)
@@ -341,23 +341,25 @@ class grade_report_user extends grade_report {
                 $hidden = ' hidden';
             }
 
+            $hide = false;
             // If this is a hidden grade item, hide it completely from the user.
             if ($grade_grade->is_hidden() && !$this->canviewhidden && (
                     $this->showhiddenitems == GRADE_REPORT_USER_HIDE_HIDDEN ||
                     ($this->showhiddenitems == GRADE_REPORT_USER_HIDE_UNTIL && !$grade_grade->is_hiddenuntil()))) {
-                // return false;
-            } else {
-                // The grade object can be marked visible but still be hidden
-                // if "enablegroupmembersonly" is on and its an activity assigned to a grouping the user is not in
-                if (!empty($grade_object->itemmodule) && !empty($grade_object->iteminstance)) {
-                    $instances = $this->gtree->modinfo->get_instances();
-                    if (!empty($instances[$grade_object->itemmodule][$grade_object->iteminstance])) {
-                        $cm = $instances[$grade_object->itemmodule][$grade_object->iteminstance];
-                        if (!$cm->uservisible) {
-                            return false;
-                        }
+                $hide = true;
+            } else if (!empty($grade_object->itemmodule) && !empty($grade_object->iteminstance)) {
+                // The grade object can be marked visible but still be hidden if "enablegroupmembersonly"
+                // is on and it's an activity assigned to a grouping the user is not in.
+                $instances = $this->gtree->modinfo->get_instances_of($grade_object->itemmodule);
+                if (!empty($instances[$grade_object->iteminstance])) {
+                    $cm = $instances[$grade_object->iteminstance];
+                    if ($cm->is_user_access_restricted_by_group()) {
+                        $hide = true;
                     }
                 }
+            }
+
+            if (!$hide) {
                 /// Excluded Item
                 if ($grade_grade->is_excluded()) {
                     $fullname .= ' ['.get_string('excluded', 'grades').']';
index 8addc93..024e81b 100644 (file)
@@ -239,17 +239,18 @@ function groups_update_group_icon($group, $data, $editform) {
     $context = get_context_instance(CONTEXT_COURSE, $group->courseid, MUST_EXIST);
 
     //TODO: it would make sense to allow picture deleting too (skodak)
-
-    if ($iconfile = $editform->save_temp_file('imagefile')) {
-        if (process_new_icon($context, 'group', 'icon', $group->id, $iconfile)) {
-            $DB->set_field('groups', 'picture', 1, array('id'=>$group->id));
-            $group->picture = 1;
-        } else {
-            $fs->delete_area_files($context->id, 'group', 'icon', $group->id);
-            $DB->set_field('groups', 'picture', 0, array('id'=>$group->id));
-            $group->picture = 0;
+    if (!empty($CFG->gdversion)) {
+        if ($iconfile = $editform->save_temp_file('imagefile')) {
+            if (process_new_icon($context, 'group', 'icon', $group->id, $iconfile)) {
+                $DB->set_field('groups', 'picture', 1, array('id'=>$group->id));
+                $group->picture = 1;
+            } else {
+                $fs->delete_area_files($context->id, 'group', 'icon', $group->id);
+                $DB->set_field('groups', 'picture', 0, array('id'=>$group->id));
+                $group->picture = 0;
+            }
+            @unlink($iconfile);
         }
-        @unlink($iconfile);
     }
 }
 
index 91a4a09..7de314e 100644 (file)
@@ -85,7 +85,7 @@ $string['phpversionhelp'] = '<p>Moodle requiere al menos una versión de PHP 4.3
 (En caso de 5.0.x podría también revertir a la versión 4.4.x)</p>';
 $string['welcomep10'] = '{$a->installername} ({$a->installerversion})';
 $string['welcomep20'] = 'Si está viendo esta página es porque ha podido ejecutar el paquete <strong>{$a->packname} {$a->packversion}</strong> en su computadora. !Enhorabuena!';
-$string['welcomep30'] = 'Esta versión de <strong>{$a->installername}</strong> incluye las aplicaciones necesarias para que <strong>Moodle</strong> funcione en su computadora principalmente:';
+$string['welcomep30'] = 'Esta versión de <strong>{$a->installername}</strong> incluye las aplicaciones necesarias para que <strong>Moodle</strong> funcione en su computadora principalmente:';
 $string['welcomep40'] = 'El paquete también incluye <strong>Moodle {$a->moodlerelease} ({$a->moodleversion})</strong>.';
 $string['welcomep50'] = 'El uso de todas las aplicaciones del paquete está gobernado por sus respectivas
     licencias. El programa <strong>{$a->installername}</strong> es
index cffb9b0..57673d4 100644 (file)
@@ -82,9 +82,7 @@ $string['pathsunsecuredataroot'] = 'Dataroot-sijainti on turvallinen';
 $string['pathswrongadmindir'] = 'Admin-hakemistoa ei ole';
 $string['phpextension'] = '{$a} PHP-lisäosa';
 $string['phpversion'] = 'PHP versio';
-$string['phpversionhelp'] = '<p>Moodle vaatii vähintään PHP version 4.1.0.</p>
-<p>Käytät parhaillaan versiota {$a}</p>
-<p>Sinun täytyy päivittää PHP tai siirtää isäntä uudemman PHP version kanssa!</p>';
+$string['phpversionhelp'] = '<p>Moodle vaatii vähintään PHP-version 4.3.0 tai 5.1.0 (5.0.x sisältää monia tunnettuja ongelmia).</p> <p>Käytössäsi on versio {$a}</p> <p>Sinun pitää päivittää PHP tai siirtää palvelimelle jossa on uudempi PHP.<br /> (Jos käytössäsi on 5.0.x voit myös päivittää alaspäin 4.4.x -versioon)</p>';
 $string['welcomep10'] = '{$a->installername} ({$a->installerversion})';
 $string['welcomep20'] = 'Näet tämän sivun koska olet onnistuneesti asentanut ja käynnistänyt <strong>{$a->packname} {$a->packversion}</strong> paketin tietokoneellasi.
 Onnittelut!';
index 6a2e548..fe6d20a 100644 (file)
@@ -33,13 +33,13 @@ defined('MOODLE_INTERNAL') || die();
 $string['cannotcreatelangdir'] = 'לא ניתן ליצור סיפריית שפה.';
 $string['cannotcreatetempdir'] = 'לא ניתן ליצור סיפרייה זמנית.';
 $string['cannotdownloadcomponents'] = 'לא ניתן להוריד רכיבים.';
-$string['cannotdownloadzipfile'] = 'לא ניתן להוריד קובץ ZIP.';
+$string['cannotdownloadzipfile'] = 'לא ניתן להוריד קובץ 7Zip';
 $string['cannotfindcomponent'] = 'הרכיב לא נמצא.';
 $string['cannotsavemd5file'] = 'לא ניתן לשמור קובץ md5.';
 $string['cannotsavezipfile'] = 'לא ניתן לשמור קובץ ZIP.';
 $string['cannotunzipfile'] = 'לא ניתן לפתוח את קובץ ה-ZIP.';
 $string['componentisuptodate'] = 'הרכיב מעודכן.';
-$string['downloadedfilecheckfailed'] = '× ×\9bש×\9c×\94 ×\91×\93×\99קת ×\94ק×\95×\91×¥ ×\94×\9e×\95ר×\93.';
+$string['downloadedfilecheckfailed'] = '×\94ק×\95×\91×¥ ×\90שר ×\99ר×\93 × ×\9eצ×\90 ×©×\92×\95×\99';
 $string['invalidmd5'] = 'md5 לא חוקי';
 $string['missingrequiredfield'] = 'חסר שדה נדרש כלשהו';
 $string['remotedownloaderror'] = 'הורדת הרכיב לשרת שלך כשלה, אנא וודא את הגדרות ה-proxy שלך. תוספת PHP cURL מומלצת מאוד להתקנה.
index d6cb0ae..52508ae 100644 (file)
@@ -101,11 +101,9 @@ $string['welcomep40'] = 'החבילה כוללת בנוסף
 $string['welcomep50'] = 'השימוש בכל היישומים בחבילה זו מפוקח ע"י הרשיונות המתאימים להם. החבילה
 <strong>{$a->installername}</strong>
 השלמה היא
-<a href="http://www.opensource.org/docs/definition_plain.html"> קוד פתוח
-</a>
-והיא מבוזרת תחת רישיון
-<a>
-href="http://www.gnu.org/copyleft/gpl.html">GPL</a>';
+<a href="http://www.opensource.org/docs/definition_plain.html">קוד פתוח</a>
+והיא מופצת תחת רשיון
+<a href="http://www.gnu.org/copyleft/gpl.html">GPL</a>';
 $string['welcomep60'] = 'העמודים הבאים יובילו אותך בצורה פשוטה דרך כמה צעדים לעיצוב הגדרות <strong>Moodle</strong> במחשבך.
 תוכל לאשר את הגדרות  ברירת המחדל או, באפשרותך, לשנותם לפי צרכיך.';
 $string['welcomep70'] = 'הקש על לחצן ה"המשך" למטה כדי להמשיך עם הגדרת ה-<strong>Moodle</strong>';
index 1971fb3..ec01ae5 100644 (file)
 
 defined('MOODLE_INTERNAL') || die();
 
-$string['admindirname'] = 'Admin Directory';
+$string['admindirname'] = 'Director Admin';
 $string['availablelangs'] = 'Pachete de limbă disponibile';
 $string['chooselanguagehead'] = 'Selectare limbă';
 $string['chooselanguagesub'] = 'Vă rugăm selectaţi limba pentru interfaţa de instalare, limba selectată va fi folosită EXCLUSIV în cadrul procedurii de instalare. Ulterior veţi putea selecta limba în care doriţi să fie afişată interfaţa.';
-$string['dataroot'] = 'Data Directory';
+$string['databasehost'] = 'Gazdă baza de date';
+$string['databasename'] = 'Nume baza de date';
+$string['databasetypehead'] = 'Alegere driver baza de date';
+$string['dataroot'] = 'Director date';
+$string['datarootpermission'] = 'Permisiuni directoare date';
 $string['dbprefix'] = 'Prefix tabele';
 $string['dirroot'] = 'Director Moodle';
 $string['environmenthead'] = 'Se verifică mediul...';
 $string['installation'] = 'Instalare';
+$string['paths'] = 'Căi';
+$string['pathshead'] = 'Confirmare căi';
+$string['phpextension'] = 'extensie PHP {$a}';
 $string['phpversion'] = 'Versiune PHP';
 $string['welcomep10'] = '{$a->installername} ({$a->installerversion})';
 $string['wwwroot'] = 'Adresă Web';
index 1dc0ec5..6a243d6 100644 (file)
@@ -45,7 +45,7 @@ $string['datarootpermission'] = 'Овлашћења над директориј
 $string['dbprefix'] = 'Префикс табеле';
 $string['dirroot'] = 'Moodle директоријум';
 $string['environmenthead'] = 'Проверавање Вашег окружења...';
-$string['environmentsub2'] = 'Свака верзија Moodlea има минимум захтева по питању одговарајуће PHP верѕије и неколико обавезних PHP екстензија.
+$string['environmentsub2'] = 'Свака верзија Moodlea има минимум захтева по питању одговарајуће PHP верзије и неколико обавезних PHP екстензија.
 Пуна провера окружења се врши пре сваке инсталације или ажурирања постојеће верзије. Уколико не знате како да инсталирате нову верзију или омогућите PHP ектензије контактирајте Вашег сервер администратора.';
 $string['errorsinenvironment'] = 'Провера окружења није прошла!';
 $string['installation'] = 'Инсталација';
@@ -75,7 +75,7 @@ $string['pathssubwwwroot'] = 'Пуна веб адреса путем које 
 Ако је адреса нетачна промените URL у свом веб читачу да бисте поново покренули инсталацију са другачијом вредношћу.';
 $string['pathsunsecuredataroot'] = 'Dataroot локација није безбедна';
 $string['pathswrongadmindir'] = 'Админ директоријум не постоји';
-$string['phpextension'] = '{$a} PHP екстенѕија';
+$string['phpextension'] = '{$a} PHP екстензија';
 $string['phpversion'] = 'PHP верзија';
 $string['phpversionhelp'] = '<p>Moodle захтева најмање PHP верзију 4.3.0 или 5.1.0 (5.0.x има  бројне уочене проблеме).</p>
 <p>Тренутно користите верзију {$a}</p>
diff --git a/install/lang/uk/install.php b/install/lang/uk/install.php
new file mode 100644 (file)
index 0000000..c132e32
--- /dev/null
@@ -0,0 +1,34 @@
+<?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/>.
+
+/**
+ * Automatically generated strings for Moodle installer
+ *
+ * Do not edit this file manually! It contains just a subset of strings
+ * needed during the very first steps of installation. This file was
+ * generated automatically by export-installer.php (which is part of AMOS
+ * {@link http://docs.moodle.org/dev/Languages/AMOS}) using the
+ * list of strings defined in /install/stringnames.txt.
+ *
+ * @package   installer
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+$string['databasehost'] = 'Сервер бази даних';
+$string['databasename'] = 'Ім’я бази даних';
index 5d26680..8a6a935 100644 (file)
@@ -551,6 +551,8 @@ $string['googlemapkey3_help'] = 'You need to enter a special key to use Google M
 $string['gotofirst'] = 'Go to first missing string';
 $string['gradebook'] = 'Gradebook';
 $string['gradebookroles'] = 'Graded roles';
+$string['gravatardefaulturl'] = 'Gravatar default image URL';
+$string['gravatardefaulturl_help'] = 'Gravatar needs a default image to display if it is unable to find a picture for a given user. Provide a full URL for an image. If you leave this setting empty, Moodle will attempt to use the most appropriate default image for the page you are viewing. Note also that Gravatar has a number of codes which can be used to <a href="https://en.gravatar.com/site/implement/images/#default-image">generate default images</a>.';
 $string['gradeexport'] = 'Primary grade export methods';
 $string['guestroleid'] = 'Role for guest';
 $string['guestroleid_help'] = 'This role is automatically assigned to the guest user. It is also temporarily assigned to not enrolled users that enter the course via guest enrolment plugin.';
index 9cb966d..64624fb 100644 (file)
@@ -46,6 +46,7 @@ $string['application/vnd.openxmlformats-officedocument.presentationml.slideshow'
 $string['application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'] = 'Excel spreadsheet';
 $string['application/vnd.openxmlformats-officedocument.spreadsheetml.template'] = 'Excel template';
 $string['application/vnd.openxmlformats-officedocument.wordprocessingml.document'] = 'Word document';
+$string['application/epub_zip'] = 'EPUB ebook';
 $string['archive'] = 'Archive ({$a->EXT})';
 $string['audio'] = 'Audio file ({$a->EXT})';
 $string['default'] = '{$a->mimetype}';
index 2e8121b..c74e557 100644 (file)
@@ -7561,7 +7561,7 @@ class admin_setting_managewebservicetokens extends admin_setting {
                         array(array('id' => $token->userid)), $token->serviceid);
 
                 if (!is_siteadmin($token->userid) and
-                        key_exists($token->userid, $usermissingcaps)) {
+                        array_key_exists($token->userid, $usermissingcaps)) {
                     $missingcapabilities = implode(', ',
                             $usermissingcaps[$token->userid]);
                     if (!empty($missingcapabilities)) {
index 47885c9..9413201 100644 (file)
@@ -454,7 +454,9 @@ class block_manager {
      * @return bool True if all of the blocks within that region are docked
      */
     public function region_completely_docked($region, $output) {
-        if (!$this->page->theme->enable_dock) {
+        global $CFG;
+        // If theme doesn't allow docking or allowblockstodock is not set, then return.
+        if (!$this->page->theme->enable_dock || empty($CFG->allowblockstodock)) {
             return false;
         }
 
index e617e1b..91263db 100644 (file)
@@ -969,5 +969,21 @@ function xmldb_main_upgrade($oldversion) {
         upgrade_main_savepoint(true, 2012062502.03);
     }
 
+    if ($oldversion < 2012062502.07) {
+        // Find all orphaned blog associations that might exist.
+        $sql = "SELECT ba.id
+                  FROM {blog_association} ba
+             LEFT JOIN {post} p
+                    ON p.id = ba.blogid
+                 WHERE p.id IS NULL";
+        $orphanedrecordids = $DB->get_records_sql($sql);
+        // Now delete these associations.
+        foreach ($orphanedrecordids as $orphanedrecord) {
+            $DB->delete_records('blog_association', array('id' => $orphanedrecord->id));
+        }
+
+        upgrade_main_savepoint(true, 2012062502.07);
+    }
+
     return true;
 }
index b6736c4..bb609fc 100644 (file)
@@ -1626,7 +1626,7 @@ abstract class enrol_plugin {
         $participants->close();
 
         // now clean up all remainders that were not removed correctly
-        $DB->delete_records('role_assignments', array('itemid'=>$instance->id, 'component'=>$name));
+        $DB->delete_records('role_assignments', array('itemid'=>$instance->id, 'component'=>'enrol_'.$name));
         $DB->delete_records('user_enrolments', array('enrolid'=>$instance->id));
 
         // finally drop the enrol row
index f2105cb..d5ac755 100644 (file)
@@ -201,8 +201,13 @@ class file_browser {
     private function get_file_info_context_module($context, $component, $filearea, $itemid, $filepath, $filename) {
         global $COURSE, $DB, $CFG;
 
+        static $cachedmodules = array();
 
-        if (!$cm = get_coursemodule_from_id('', $context->instanceid)) {
+        if (!array_key_exists($context->instanceid, $cachedmodules)) {
+            $cachedmodules[$context->instanceid] = get_coursemodule_from_id('', $context->instanceid);
+        }
+
+        if (!($cm = $cachedmodules[$context->instanceid])) {
             return null;
         }
 
index d5bcade..a6af3f7 100644 (file)
@@ -88,6 +88,116 @@ abstract class file_info {
      */
     public abstract function get_children();
 
+    /**
+     * Builds SQL sub query (WHERE clause) for selecting files with the specified extensions
+     *
+     * If $extensions == '*' (any file), the result is array('', array())
+     * otherwise the result is something like array('AND filename ...', array(...))
+     *
+     * @param string|array $extensions - either '*' or array of lowercase extensions, i.e. array('.gif','.jpg')
+     * @param string $prefix prefix for DB table files in the query (empty by default)
+     * @return array of two elements: $sql - sql where clause and $params - array of parameters
+     */
+    protected function build_search_files_sql($extensions, $prefix = null) {
+        global $DB;
+        if (strlen($prefix)) {
+            $prefix = $prefix.'.';
+        } else {
+            $prefix = '';
+        }
+        $sql = '';
+        $params = array();
+        if (is_array($extensions) && !in_array('*', $extensions)) {
+            $likes = array();
+            $cnt = 0;
+            foreach ($extensions as $ext) {
+                $cnt++;
+                $likes[] = $DB->sql_like($prefix.'filename', ':filename'.$cnt, false);
+                $params['filename'.$cnt] = '%'.$ext;
+            }
+            $sql .= ' AND (' . join(' OR ', $likes) . ')';
+        }
+        return array($sql, $params);
+     }
+
+    /**
+     * Returns list of children which are either files matching the specified extensions
+     * or folders that contain at least one such file.
+     *
+     * It is recommended to overwrite this function so it uses a proper SQL
+     * query and does not create unnecessary file_info objects (might require a lot of time
+     * and memory usage on big sites).
+     *
+     * @param string|array $extensions, either '*' or array of lowercase extensions, i.e. array('.gif','.jpg')
+     * @return array of file_info instances
+     */
+    public function get_non_empty_children($extensions = '*') {
+        $list = $this->get_children();
+        $nonemptylist = array();
+        foreach ($list as $fileinfo) {
+            if ($fileinfo->is_directory()) {
+                if ($fileinfo->count_non_empty_children($extensions)) {
+                    $nonemptylist[] = $fileinfo;
+                }
+            } else if ($extensions === '*') {
+                $nonemptylist[] = $fileinfo;
+            } else {
+                $filename = $fileinfo->get_visible_name();
+                $extension = textlib::strtolower(pathinfo($filename, PATHINFO_EXTENSION));
+                if (!empty($extension) && in_array('.' . $extension, $extensions)) {
+                    $nonemptylist[] = $fileinfo;
+                }
+            }
+        }
+        return $nonemptylist;
+    }
+
+    /**
+     * Returns the number of children which are either files matching the specified extensions
+     * or folders containing at least one such file.
+     *
+     * We usually don't need the exact number of non empty children if it is >=2 (see param $limit)
+     * This function is used by repository_local to evaluate if the folder is empty. But
+     * it also can be used to check if folder has only one subfolder because in some cases
+     * this subfolder can be skipped.
+     *
+     * It is strongly recommended to overwrite this function so it uses a proper SQL
+     * query and does not create file_info objects (later might require a lot of time
+     * and memory usage on big sites).
+     *
+     * @param string|array $extensions, for example '*' or array('.gif','.jpg')
+     * @param int $limit stop counting after at least $limit non-empty children are found
+     * @return int
+     */
+    public function count_non_empty_children($extensions = '*', $limit = 1) {
+        $list = $this->get_children();
+        $cnt = 0;
+        // first loop through files
+        foreach ($list as $fileinfo) {
+            if (!$fileinfo->is_directory()) {
+                if ($extensions !== '*') {
+                    $filename = $fileinfo->get_visible_name();
+                    $extension = textlib::strtolower(pathinfo($filename, PATHINFO_EXTENSION));
+                    if (empty($extension) || !in_array('.' . $extension, $extensions)) {
+                        continue;
+                    }
+                }
+                if ((++$cnt) >= $limit) {
+                    return $cnt;
+                }
+            }
+        }
+        // now loop through directories
+        foreach ($list as $fileinfo) {
+            if ($fileinfo->is_directory() && $fileinfo->count_non_empty_children($extensions)) {
+                if ((++$cnt) >= $limit) {
+                    return $cnt;
+                }
+            }
+        }
+        return $cnt;
+    }
+
     /**
      * Returns parent file_info instance
      *
@@ -103,12 +213,12 @@ abstract class file_info {
     public function get_params_rawencoded() {
         $params = $this->get_params();
         $encoded = array();
-        $encoded[] = 'contextid='.$params['contextid'];
-        $encoded[] = 'component='.$params['component'];
-        $encoded[] = 'filearea='.$params['filearea'];
-        $encoded[] = 'itemid='.(is_null($params['itemid']) ? -1 : $params['itemid']);
-        $encoded[] = 'filepath='.(is_null($params['filepath']) ? '' : rawurlencode($params['filepath']));
-        $encoded[] = 'filename='.((is_null($params['filename']) or $params['filename'] === '.') ? '' : rawurlencode($params['filename']));
+        $encoded[] = 'contextid=' . $params['contextid'];
+        $encoded[] = 'component=' . $params['component'];
+        $encoded[] = 'filearea=' . $params['filearea'];
+        $encoded[] = 'itemid=' . (is_null($params['itemid']) ? -1 : $params['itemid']);
+        $encoded[] = 'filepath=' . (is_null($params['filepath']) ? '' : rawurlencode($params['filepath']));
+        $encoded[] = 'filename=' . ((is_null($params['filename']) or $params['filename'] === '.') ? '' : rawurlencode($params['filename']));
 
         return $encoded;
     }
index 50876b3..5e61f56 100644 (file)
@@ -350,48 +350,90 @@ class file_info_context_course extends file_info {
      * @return array of file_info instances
      */
     public function get_children() {
-        $children = array();
+        return $this->get_filtered_children('*', false, true);
+    }
 
-        if ($child = $this->get_area_course_summary(0, '/', '.')) {
-            $children[] = $child;
-        }
-        if ($child = $this->get_area_course_section(null, null, null)) {
-            $children[] = $child;
-        }
-        if ($child = $this->get_area_backup_section(null, null, null)) {
-            $children[] = $child;
-        }
-        if ($child = $this->get_area_backup_course(0, '/', '.')) {
-            $children[] = $child;
-        }
-        if ($child = $this->get_area_backup_automated(0, '/', '.')) {
-            $children[] = $child;
-        }
-        if ($child = $this->get_area_course_legacy(0, '/', '.')) {
-            $children[] = $child;
+    /**
+     * Help function to return files matching extensions or their count
+     *
+     * @param string|array $extensions, either '*' or array of lowercase extensions, i.e. array('.gif','.jpg')
+     * @param bool|int $countonly if false returns the children, if an int returns just the
+     *    count of children but stops counting when $countonly number of children is reached
+     * @param bool $returnemptyfolders if true returns items that don't have matching files inside
+     * @return array|int array of file_info instances or the count
+     */
+    private function get_filtered_children($extensions = '*', $countonly = false, $returnemptyfolders = false) {
+        $areas = array(
+            array('course', 'summary'),
+            array('course', 'section'),
+            array('backup', 'section'),
+            array('backup', 'course'),
+            array('backup', 'automated'),
+            array('course', 'legacy')
+        );
+        $children = array();
+        foreach ($areas as $area) {
+            if ($child = $this->get_file_info($area[0], $area[1], 0, '/', '.')) {
+                if ($returnemptyfolders || $child->count_non_empty_children($extensions)) {
+                    $children[] = $child;
+                    if (($countonly !== false) && count($children) >= $countonly) {
+                        return $countonly;
+                    }
+                }
+            }
         }
 
         if (!has_capability('moodle/course:managefiles', $this->context)) {
             // 'managefiles' capability is checked in every activity module callback.
             // Don't even waste time on retrieving the modules if we can't browse the files anyway
-            return $children;
-        }
-
-        // now list all modules
-        $modinfo = get_fast_modinfo($this->course);
-        foreach ($modinfo->cms as $cminfo) {
-            if (empty($cminfo->uservisible)) {
-                continue;
-            }
-            $modcontext = get_context_instance(CONTEXT_MODULE, $cminfo->id);
-            if ($child = $this->browser->get_file_info($modcontext)) {
-                $children[] = $child;
+        } else {
+            // now list all modules
+            $modinfo = get_fast_modinfo($this->course);
+            foreach ($modinfo->cms as $cminfo) {
+                if (empty($cminfo->uservisible)) {
+                    continue;
+                }
+                $modcontext = context_module::instance($cminfo->id, IGNORE_MISSING);
+                if ($child = $this->browser->get_file_info($modcontext)) {
+                    if ($returnemptyfolders || $child->count_non_empty_children($extensions)) {
+                        $children[] = $child;
+                        if (($countonly !== false) && count($children) >= $countonly) {
+                            return $countonly;
+                        }
+                    }
+                }
             }
         }
 
+        if ($countonly !== false) {
+            return count($children);
+        }
         return $children;
     }
 
+    /**
+     * Returns list of children which are either files matching the specified extensions
+     * or folders that contain at least one such file.
+     *
+     * @param string|array $extensions, either '*' or array of lowercase extensions, i.e. array('.gif','.jpg')
+     * @return array of file_info instances
+     */
+    public function get_non_empty_children($extensions = '*') {
+        return $this->get_filtered_children($extensions, false);
+    }
+
+    /**
+     * Returns the number of children which are either files matching the specified extensions
+     * or folders containing at least one such file.
+     *
+     * @param string|array $extensions, for example '*' or array('.gif','.jpg')
+     * @param int $limit stop counting after at least $limit non-empty children are found
+     * @return int
+     */
+    public function count_non_empty_children($extensions = '*', $limit = 1) {
+        return $this->get_filtered_children($extensions, $limit);
+    }
+
     /**
      * Returns parent file_info instance
      *
@@ -473,6 +515,37 @@ class file_info_area_course_legacy extends file_info_stored {
 
         return $result;
     }
+
+    /**
+     * Returns list of children which are either files matching the specified extensions
+     * or folders that contain at least one such file.
+     *
+     * @param string|array $extensions, either '*' or array of lowercase extensions, i.e. array('.gif','.jpg')
+     * @return array of file_info instances
+     */
+    public function get_non_empty_children($extensions = '*') {
+        if (!$this->lf->is_directory()) {
+            return array();
+        }
+
+        $result = array();
+        $fs = get_file_storage();
+
+        $storedfiles = $fs->get_directory_files($this->context->id, 'course', 'legacy', 0,
+                                                $this->lf->get_filepath(), false, true, "filepath, filename");
+        foreach ($storedfiles as $file) {
+            $extension = textlib::strtolower(pathinfo($file->get_filename(), PATHINFO_EXTENSION));
+            if ($file->is_directory() || $extensions === '*' || (!empty($extension) && in_array('.'.$extension, $extensions))) {
+                $fileinfo = new file_info_area_course_legacy($this->browser, $this->context, $file, $this->urlbase, $this->topvisiblename,
+                                                 $this->itemidused, $this->readaccess, $this->writeaccess, false);
+                if (!$file->is_directory() || $fileinfo->count_non_empty_children($extensions)) {
+                    $result[] = $fileinfo;
+                }
+            }
+        }
+
+        return $result;
+    }
 }
 
 /**
@@ -578,6 +651,41 @@ class file_info_area_course_section extends file_info {
         return $children;
     }
 
+    /**
+     * Returns the number of children which are either files matching the specified extensions
+     * or folders containing at least one such file.
+     *
+     * @param string|array $extensions, for example '*' or array('.gif','.jpg')
+     * @param int $limit stop counting after at least $limit non-empty children are found
+     * @return int
+     */
+    public function count_non_empty_children($extensions = '*', $limit = 1) {
+        global $DB;
+        $params1 = array(
+            'courseid' => $this->course->id,
+            'contextid' => $this->context->id,
+            'component' => 'course',
+            'filearea' => 'section',
+            'emptyfilename' => '.');
+        $sql1 = "SELECT DISTINCT cs.id FROM {files} f, {course_sections} cs
+            WHERE cs.course = :courseid
+            AND f.contextid = :contextid
+            AND f.component = :component
+            AND f.filearea = :filearea
+            AND f.itemid = cs.id
+            AND f.filename <> :emptyfilename";
+        list($sql2, $params2) = $this->build_search_files_sql($extensions);
+        $rs = $DB->get_recordset_sql($sql1. ' '. $sql2, array_merge($params1, $params2));
+        $cnt = 0;
+        foreach ($rs as $record) {
+            if ((++$cnt) >= $limit) {
+                break;
+            }
+        }
+        $rs->close();
+        return $cnt;
+    }
+
     /**
      * Returns parent file_info instance
      *
@@ -689,6 +797,41 @@ class file_info_area_backup_section extends file_info {
         return $children;
     }
 
+    /**
+     * Returns the number of children which are either files matching the specified extensions
+     * or folders containing at least one such file.
+     *
+     * @param string|array $extensions, for example '*' or array('.gif','.jpg')
+     * @param int $limit stop counting after at least $limit non-empty children are found
+     * @return int
+     */
+    public function count_non_empty_children($extensions = '*', $limit = 1) {
+        global $DB;
+        $params1 = array(
+            'courseid' => $this->course->id,
+            'contextid' => $this->context->id,
+            'component' => 'backup',
+            'filearea' => 'section',
+            'emptyfilename' => '.');
+        $sql1 = "SELECT DISTINCT cs.id sectionid FROM {files} f, {course_sections} cs
+            WHERE cs.course = :courseid
+            AND f.contextid = :contextid
+            AND f.component = :component
+            AND f.filearea = :filearea
+            AND f.itemid = cs.id
+            AND f.filename <> :emptyfilename";
+        list($sql2, $params2) = $this->build_search_files_sql($extensions);
+        $rs = $DB->get_recordset_sql($sql1. ' '. $sql2, array_merge($params1, $params2));
+        $cnt = 0;
+        foreach ($rs as $record) {
+            if ((++$cnt) >= $limit) {
+                break;
+            }
+        }
+        $rs->close();
+        return $cnt;
+    }
+
     /**
      * Returns parent file_info instance
      *
index 99b6d1f..e8d542c 100644 (file)
@@ -188,6 +188,66 @@ class file_info_context_coursecat extends file_info {
         return $children;
     }
 
+    /**
+     * Returns the number of children which are either files matching the specified extensions
+     * or folders containing at least one such file.
+     *
+     * @param string|array $extensions, for example '*' or array('.gif','.jpg')
+     * @param int $limit stop counting after at least $limit non-empty children are found
+     * @return int
+     */
+    public function count_non_empty_children($extensions = '*', $limit = 1) {
+        global $DB;
+        $cnt = 0;
+        if (($child = $this->get_area_coursecat_description(0, '/', '.'))
+                && $child->count_non_empty_children($extensions) && (++$cnt) >= $limit) {
+            return $cnt;
+        }
+
+        $rs = $DB->get_recordset_sql('SELECT ctx.id contextid, c.visible
+                FROM {context} ctx, {course} c
+                WHERE ctx.instanceid = c.id
+                AND ctx.contextlevel = :courselevel
+                AND c.category = :categoryid
+                ORDER BY c.visible DESC', // retrieve visible courses first
+                array('categoryid' => $this->category->id, 'courselevel' => CONTEXT_COURSE));
+        foreach ($rs as $record) {
+            $context = context::instance_by_id($record->contextid);
+            if (!$record->visible and !has_capability('moodle/course:viewhiddencourses', $context)) {
+                continue;
+            }
+            if (($child = $this->browser->get_file_info($context))
+                    && $child->count_non_empty_children($extensions) && (++$cnt) >= $limit) {
+                break;
+            }
+        }
+        $rs->close();
+        if ($cnt >= $limit) {
+            return $cnt;
+        }
+
+        $rs = $DB->get_recordset_sql('SELECT ctx.id contextid, cat.visible
+                FROM {context} ctx, {course_categories} cat
+                WHERE ctx.instanceid = cat.id
+                AND ctx.contextlevel = :catlevel
+                AND cat.parent = :categoryid
+                ORDER BY cat.visible DESC', // retrieve visible categories first
+                array('categoryid' => $this->category->id, 'catlevel' => CONTEXT_COURSECAT));
+        foreach ($rs as $record) {
+            $context = context::instance_by_id($record->contextid);
+            if (!$record->visible and !has_capability('moodle/category:viewhiddencategories', $context)) {
+                continue;
+            }
+            if (($child = $this->browser->get_file_info($context))
+                    && $child->count_non_empty_children($extensions) && (++$cnt) >= $limit) {
+                break;
+            }
+        }
+        $rs->close();
+
+        return $cnt;
+    }
+
     /**
      * Returns parent file_info instance
      *
index 0357ee3..f4b2e52 100644 (file)
@@ -41,6 +41,8 @@ class file_info_context_module extends file_info {
     protected $modname;
     /** @var array Available file areas */
     protected $areas;
+    /** @var array caches the result of last call to get_non_empty_children() */
+    protected $nonemptychildren;
 
     /**
      * Constructor
@@ -58,6 +60,7 @@ class file_info_context_module extends file_info {
         $this->course  = $course;
         $this->cm      = $cm;
         $this->modname = $modname;
+        $this->nonemptychildren = null;
 
         include_once("$CFG->dirroot/mod/$modname/lib.php");
 
@@ -258,24 +261,91 @@ class file_info_context_module extends file_info {
      * @return array of file_info instances
      */
     public function get_children() {
-        $children = array();
+        return $this->get_filtered_children('*', false, true);
+    }
 
-        if ($child = $this->get_area_backup(0, '/', '.')) {
-            $children[] = $child;
-        }
-        if ($child = $this->get_area_intro(0, '/', '.')) {
-            $children[] = $child;
+    /**
+     * Help function to return files matching extensions or their count
+     *
+     * @param string|array $extensions, either '*' or array of lowercase extensions, i.e. array('.gif','.jpg')
+     * @param bool|int $countonly if false returns the children, if an int returns just the
+     *    count of children but stops counting when $countonly number of children is reached
+     * @param bool $returnemptyfolders if true returns items that don't have matching files inside
+     * @return array|int array of file_info instances or the count
+     */
+    private function get_filtered_children($extensions = '*', $countonly = false, $returnemptyfolders = false) {
+        global $DB;
+        // prepare list of areas including intro and backup
+        $areas = array(
+            array('mod_'.$this->modname, 'intro'),
+            array('backup', 'activity')
+        );
+        foreach ($this->areas as $area => $desctiption) {
+            $areas[] = array('mod_'.$this->modname, $area);
         }
 
-        foreach ($this->areas as $area=>$desctiption) {
-            if ($child = $this->get_file_info('mod_'.$this->modname, $area, null, null, null)) {
-                $children[] = $child;
+        $params1 = array('contextid' => $this->context->id, 'emptyfilename' => '.');
+        list($sql2, $params2) = $this->build_search_files_sql($extensions);
+        $children = array();
+        foreach ($areas as $area) {
+            if (!$returnemptyfolders) {
+                // fast pre-check if there are any files in the filearea
+                $params1['component'] = $area[0];
+                $params1['filearea'] = $area[1];
+                if (!$DB->record_exists_sql('SELECT 1 from {files}
+                        WHERE contextid = :contextid
+                        AND filename <> :emptyfilename
+                        AND component = :component
+                        AND filearea = :filearea '.$sql2,
+                        array_merge($params1, $params2))) {
+                    continue;
+                }
+            }
+            if ($child = $this->get_file_info($area[0], $area[1], null, null, null)) {
+                if ($returnemptyfolders || $child->count_non_empty_children($extensions)) {
+                    $children[] = $child;
+                    if ($countonly !== false && count($children) >= $countonly) {
+                        break;
+                    }
+                }
             }
         }
-
+        if ($countonly !== false) {
+            return count($children);
+        }
         return $children;
     }
 
+    /**
+     * Returns list of children which are either files matching the specified extensions
+     * or folders that contain at least one such file.
+     *
+     * @param string|array $extensions either '*' or array of lowercase extensions, i.e. array('.gif','.jpg')
+     * @return array of file_info instances
+     */
+    public function get_non_empty_children($extensions = '*') {
+        if ($this->nonemptychildren !== null) {
+            return $this->nonemptychildren;
+        }
+        $this->nonemptychildren = $this->get_filtered_children($extensions);
+        return $this->nonemptychildren;
+    }
+
+    /**
+     * Returns the number of children which are either files matching the specified extensions
+     * or folders containing at least one such file.
+     *
+     * @param string|array $extensions for example '*' or array('.gif','.jpg')
+     * @param int $limit stop counting after at least $limit non-empty children are found
+     * @return int
+     */
+    public function count_non_empty_children($extensions = '*', $limit = 1) {
+        if ($this->nonemptychildren !== null) {
+            return count($this->nonemptychildren);
+        }
+        return $this->get_filtered_children($extensions, $limit);
+    }
+
     /**
      * Returns parent file_info instance
      *
index 44fa566..e9caf4c 100644 (file)
@@ -350,6 +350,84 @@ class file_info_stored extends file_info {
         return $result;
     }
 
+    /**
+     * Returns list of children which are either files matching the specified extensions
+     * or folders that contain at least one such file.
+     *
+     * @param string|array $extensions, either '*' or array of lowercase extensions, i.e. array('.gif','.jpg')
+     * @return array of file_info instances
+     */
+    public function get_non_empty_children($extensions = '*') {
+        $result = array();
+        if (!$this->lf->is_directory()) {
+            return $result;
+        }
+
+        $fs = get_file_storage();
+
+        $storedfiles = $fs->get_directory_files($this->context->id, $this->lf->get_component(), $this->lf->get_filearea(), $this->lf->get_itemid(),
+                                                $this->lf->get_filepath(), false, true, "filepath, filename");
+        foreach ($storedfiles as $file) {
+            $extension = textlib::strtolower(pathinfo($file->get_filename(), PATHINFO_EXTENSION));
+            if ($file->is_directory() || $extensions === '*' || (!empty($extension) && in_array('.'.$extension, $extensions))) {
+                $fileinfo = new file_info_stored($this->browser, $this->context, $file, $this->urlbase, $this->topvisiblename,
+                                                 $this->itemidused, $this->readaccess, $this->writeaccess, false);
+                if (!$file->is_directory() || $fileinfo->count_non_empty_children($extensions)) {
+                    $result[] = $fileinfo;
+                }
+            }
+        }
+
+        return $result;
+    }
+
+    /**
+     * Returns the number of children which are either files matching the specified extensions
+     * or folders containing at least one such file.
+     *
+     * @param string|array $extensions, for example '*' or array('.gif','.jpg')
+     * @param int $limit stop counting after at least $limit non-empty children are found
+     * @return int
+     */
+    public function count_non_empty_children($extensions = '*', $limit = 1) {
+        global $DB;
+        if (!$this->lf->is_directory()) {
+            return 0;
+        }
+
+        $filepath = $this->lf->get_filepath();
+        $length = textlib::strlen($filepath);
+        $sql = "SELECT filepath, filename
+                  FROM {files} f
+                 WHERE f.contextid = :contextid AND f.component = :component AND f.filearea = :filearea AND f.itemid = :itemid
+                       AND ".$DB->sql_substr("f.filepath", 1, $length)." = :filepath
+                       AND filename <> '.' ";
+        $params = array('contextid' => $this->context->id,
+            'component' => $this->lf->get_component(),
+            'filearea' => $this->lf->get_filearea(),
+            'itemid' => $this->lf->get_itemid(),
+            'filepath' => $filepath);
+        list($sql2, $params2) = $this->build_search_files_sql($extensions);
+        $rs = $DB->get_recordset_sql($sql.' '.$sql2, array_merge($params, $params2));
+        $children = array();
+        foreach ($rs as $record) {
+            // we don't need to check access to individual files here, since the user can access parent
+            if ($record->filepath === $filepath) {
+                $children[] = $record->filename;
+            } else {
+                $path = explode('/', textlib::substr($record->filepath, $length));
+                if (!in_array($path[0], $children)) {
+                    $children[] = $path[0];
+                }
+            }
+            if (count($children) >= $limit) {
+                break;
+            }
+        }
+        $rs->close();
+        return count($children);
+    }
+
     /**
      * Returns parent file_info instance
      *
index 7c2087e..465ba24 100644 (file)
@@ -1404,6 +1404,7 @@ function &get_mimetypes_array() {
         'dir'  => array ('type'=>'application/x-director', 'icon'=>'flash'),
         'dxr'  => array ('type'=>'application/x-director', 'icon'=>'flash'),
         'eps'  => array ('type'=>'application/postscript', 'icon'=>'eps'),
+        'epub' => array ('type'=>'application/epub+zip', 'icon'=>'epub', 'groups'=>array('document')),
         'fdf'  => array ('type'=>'application/pdf', 'icon'=>'pdf'),
         'flv'  => array ('type'=>'video/x-flv', 'icon'=>'flash', 'groups'=>array('video','web_video'), 'string'=>'video'),
         'f4v'  => array ('type'=>'video/mp4', 'icon'=>'flash', 'groups'=>array('video','web_video'), 'string'=>'video'),
@@ -1817,10 +1818,14 @@ function get_mimetype_description($obj, $capitalise=false) {
         $a[strtoupper($key)] = strtoupper($value);
         $a[ucfirst($key)] = ucfirst($value);
     }
-    if (get_string_manager()->string_exists($mimetype, 'mimetypes')) {
-        $result = get_string($mimetype, 'mimetypes', (object)$a);
-    } else if (get_string_manager()->string_exists($mimetypestr, 'mimetypes')) {
-        $result = get_string($mimetypestr, 'mimetypes', (object)$a);
+
+    // MIME types may include + symbol but this is not permitted in string ids.
+    $safemimetype = str_replace('+', '_', $mimetype);
+    $safemimetypestr = str_replace('+', '_', $mimetypestr);
+    if (get_string_manager()->string_exists($safemimetype, 'mimetypes')) {
+        $result = get_string($safemimetype, 'mimetypes', (object)$a);
+    } else if (get_string_manager()->string_exists($safemimetypestr, 'mimetypes')) {
+        $result = get_string($safemimetypestr, 'mimetypes', (object)$a);
     } else if (get_string_manager()->string_exists('default', 'mimetypes')) {
         $result = get_string('default', 'mimetypes', (object)$a);
     } else {
index 23f65cb..c81dfb7 100644 (file)
@@ -1279,6 +1279,9 @@ class MoodleQuickForm extends HTML_QuickForm_DHTMLRulesTableless {
     /** @var bool Whether to display advanced elements (on page load) */
     var $_showAdvanced = null;
 
+    /** @var bool whether to automatically initialise M.formchangechecker for this form. */
+    protected $_use_form_change_checker = true;
+
     /**
      * The form name is derived from the class name of the wrapper minus the trailing form
      * It is a name with words joined by underscores whereas the id attribute is words joined by underscores.
@@ -1406,8 +1409,32 @@ class MoodleQuickForm extends HTML_QuickForm_DHTMLRulesTableless {
         return $this->_showAdvanced;
     }
 
+    /**
+     * Call this method if you don't want the formchangechecker JavaScript to be
+     * automatically initialised for this form.
+     */
+    public function disable_form_change_checker() {
+        $this->_use_form_change_checker = false;
+    }
 
-   /**
+    /**
+     * If you have called {@link disable_form_change_checker()} then you can use
+     * this method to re-enable it. It is enabled by default, so normally you don't
+     * need to call this.
+     */
+    public function enable_form_change_checker() {
+        $this->_use_form_change_checker = true;
+    }
+
+    /**
+     * @return bool whether this form should automatically initialise
+     *      formchangechecker for itself.
+     */
+    public function is_form_change_checker_enabled() {
+        return $this->_use_form_change_checker;
+    }
+
+    /**
     * Accepts a renderer
     *
     * @param HTML_QuickForm_Renderer $renderer An HTML_QuickForm_Renderer object
@@ -2312,13 +2339,15 @@ class MoodleQuickForm_Renderer extends HTML_QuickForm_Renderer_Tableless{
             $this->_hiddenHtml .= $form->_pageparams;
         }
 
-        $PAGE->requires->yui_module('moodle-core-formchangechecker',
-                'M.core_formchangechecker.init',
-                array(array(
-                    'formid' => $form->getAttribute('id')
-                ))
-        );
-        $PAGE->requires->string_for_js('changesmadereallygoaway', 'moodle');
+        if ($form->is_form_change_checker_enabled()) {
+            $PAGE->requires->yui_module('moodle-core-formchangechecker',
+                    'M.core_formchangechecker.init',
+                    array(array(
+                        'formid' => $form->getAttribute('id')
+                    ))
+            );
+            $PAGE->requires->string_for_js('changesmadereallygoaway', 'moodle');
+        }
     }
 
     /**
index 95b99f8..3fa19ec 100644 (file)
@@ -911,7 +911,10 @@ class grade_category extends grade_object {
 
                 $i = 1;
                 while ($originalindex+$i < count($grade_keys)) {
+
                     $possibleitemid = $grade_keys[$originalindex+$i];
+                    $i++;
+
                     if ($grade_values[$founditemid] != $grade_values[$possibleitemid]) {
                         // The next grade item has a different grade. Stop looking.
                         break;
@@ -928,8 +931,6 @@ class grade_category extends grade_object {
                         $founditemid = $possibleitemid;
                         // Continue searching to see if there is an even higher grademax...
                     }
-
-                    $i++;
                 }
 
                 // Now drop whatever grade item we have found
index 727e698..e4281a0 100644 (file)
@@ -516,6 +516,21 @@ class grade_category_testcase extends grade_base_testcase {
         $this->assertEquals(count($grades), 1);
         $this->assertEquals($grades[$this->grade_items[2]->id], 6);
 
+        // MDL-35667 - There was an infinite loop if several items had the same grade and at least one was extra credit
+        $category = new grade_category();
+        $category->droplow     = 1;
+        $category->aggregation = GRADE_AGGREGATE_WEIGHTED_MEAN2; // simple weighted mean
+        $items[$this->grade_items[1]->id]->aggregationcoef = 1; // Mark grade item 1 as "extra credit"
+        $grades = array($this->grade_items[0]->id=>1, // 1 out of 110. Should be excluded from aggregation.
+                        $this->grade_items[1]->id=>1, // 1 out of 100. Extra credit. Should be retained.
+                        $this->grade_items[2]->id=>1, // 1 out of 6. Should be retained.
+                        $this->grade_items[4]->id=>1);// 1 out of 100. Should be retained.
+        $category->apply_limit_rules($grades, $items);
+        $this->assertEquals(count($grades), 3);
+        $this->assertEquals($grades[$this->grade_items[1]->id], 1);
+        $this->assertEquals($grades[$this->grade_items[2]->id], 1);
+        $this->assertEquals($grades[$this->grade_items[4]->id], 1);
+
     }
 
     /**
index 24ac3fa..4189c37 100644 (file)
@@ -1190,9 +1190,12 @@ function openpopup(event, args) {
         }
     }
 
-    // Cleans window name because IE does not support funky ones.
+    // Make sure the name argument is set and valid.
     var nameregex = /[^a-z0-9_]/i;
-    if (args.name.match(nameregex)) {
+    if (typeof args.name !== 'string') {
+        args.name = '_blank';
+    } else if (args.name.match(nameregex)) {
+        // Cleans window name because IE does not support funky ones.
         args.name = args.name.replace(nameregex, '_');
         if (M.cfg.developerdebug) {
             alert('DEVELOPER NOTICE: Invalid \'name\' passed to openpopup()');
index 0739f76..c787896 100644 (file)
@@ -1095,17 +1095,31 @@ class cm_info extends stdClass {
         }
         // Check group membership. The grouping option makes the activity
         // completely invisible as it does not apply to the user at all.
+        if ($this->is_user_access_restricted_by_group()) {
+            $this->uservisible = false;
+            // Ensure activity is completely hidden from user.
+            $this->showavailability = 0;
+        }
+    }
+
+    /**
+     * Checks whether the module group settings restrict the user access.
+     * @return bool true if the user access is restricted
+     */
+    public function is_user_access_restricted_by_group() {
+        global $CFG;
+        $modcontext = context_module::instance($this->id);
+        $userid = $this->modinfo->get_user_id();
         if (!empty($CFG->enablegroupmembersonly) and !empty($this->groupmembersonly)
                 and !has_capability('moodle/site:accessallgroups', $modcontext, $userid)) {
             // If the activity has 'group members only' and you don't have accessallgroups...
             $groups = $this->modinfo->get_groups($this->groupingid);
             if (empty($groups)) {
                 // ...and you don't belong to a group, then set it so you can't see/access it
-                $this->uservisible = false;
-                // Ensure activity is completely hidden from user.
-                $this->showavailability = 0;
+                return true;
             }
         }
+        return false;
     }
 
     /**
index b24c112..b95aae6 100644 (file)
@@ -1197,7 +1197,7 @@ class global_navigation extends navigation_node {
                 break;
             case CONTEXT_COURSECAT :
                 // This has already been loaded we just need to map the variable
-                if ($showcategories) {
+                if ($this->show_categories()) {
                     $this->load_all_categories($this->page->context->instanceid, true);
                 }
                 break;
@@ -3561,7 +3561,7 @@ class settings_navigation extends navigation_node {
                 $baseurl->param('sesskey', sesskey());
             } else {
                 // Edit on the main course page.
-                $baseurl = new moodle_url('/course/view.php', array('id'=>$course->id, 'sesskey'=>sesskey()));
+                $baseurl = new moodle_url('/course/view.php', array('id'=>$course->id, 'return'=>$this->page->url->out_as_local_url(false), 'sesskey'=>sesskey()));
             }
 
             $editurl = clone($baseurl);
index a20bd7c..5e1cf96 100644 (file)
@@ -384,12 +384,26 @@ class user_picture implements renderable {
             // Hash the users email address
             $md5 = md5(strtolower(trim($this->user->email)));
             // Build a gravatar URL with what we know.
+
+            // Find the best default image URL we can (MDL-35669)
+            if (empty($CFG->gravatardefaulturl)) {
+                $absoluteimagepath = $page->theme->resolve_image_location('u/'.$filename, 'core');
+                if (strpos($absoluteimagepath, $CFG->dirroot) === 0) {
+                    $gravatardefault = $CFG->wwwroot . substr($absoluteimagepath, strlen($CFG->dirroot));
+                } else {
+                    $gravatardefault = $CFG->wwwroot . '/pix/u/' . $filename . '.png';
+                }
+            } else {
+                $gravatardefault = $CFG->gravatardefaulturl;
+            }
+
             // If the currently requested page is https then we'll return an
             // https gravatar page.
             if (strpos($CFG->httpswwwroot, 'https:') === 0) {
-                return new moodle_url("https://secure.gravatar.com/avatar/{$md5}", array('s' => $size, 'd' => $defaulturl->out(false)));
+                $gravatardefault = str_replace($CFG->wwwroot, $CFG->httpswwwroot, $gravatardefault); // Replace by secure url.
+                return new moodle_url("https://secure.gravatar.com/avatar/{$md5}", array('s' => $size, 'd' => $gravatardefault));
             } else {
-                return new moodle_url("http://www.gravatar.com/avatar/{$md5}", array('s' => $size, 'd' => $defaulturl->out(false)));
+                return new moodle_url("http://www.gravatar.com/avatar/{$md5}", array('s' => $size, 'd' => $gravatardefault));
             }
         }
 
index 36172c7..ba0c596 100644 (file)
@@ -1073,6 +1073,7 @@ class moodle_page {
      */
     public function set_title($title) {
         $title = format_string($title);
+        $title = strip_tags($title);
         $title = str_replace('"', '&quot;', $title);
         $this->_title = $title;
     }
index 8225f23..8bfd798 100644 (file)
@@ -156,9 +156,11 @@ EOD;
         }
 
         if (!isset($record['username'])) {
-            $record['username'] = textlib::strtolower($record['firstname']).textlib::strtolower($record['lastname']);
+            $record['username'] = 'username'.$i;
+            $j = 2;
             while ($DB->record_exists('user', array('username'=>$record['username'], 'mnethostid'=>$record['mnethostid']))) {
-                $record['username'] = $record['username'].'_'.$i;
+                $record['username'] = 'username'.$i.'_'.$j;
+                $j++;
             }
         }
 
index b580259..f9dc68d 100644 (file)
@@ -544,6 +544,9 @@ class phpunit_util {
     public static function reset_all_data($logchanges = false) {
         global $DB, $CFG, $USER, $SITE, $COURSE, $PAGE, $OUTPUT, $SESSION, $GROUPLIB_CACHE;
 
+        // Release memory and indirectly call destroy() methods to release resource handles, etc.
+        gc_collect_cycles();
+
         // reset global $DB in case somebody mocked it
         $DB = self::get_global_backup('DB');
 
@@ -618,6 +621,10 @@ class phpunit_util {
         $GROUPLIB_CACHE = null;
         //TODO MDL-25290: add more resets here and probably refactor them to new core function
 
+        // Reset course and module caches.
+        $reset = 'reset';
+        get_fast_modinfo($reset);
+
         // purge dataroot directory
         self::reset_dataroot();
 
index 316ee70..3787b34 100644 (file)
@@ -44,6 +44,11 @@ class core_phpunit_generator_testcase extends advanced_testcase {
         $count = $DB->count_records('user');
         $user = $generator->create_user();
         $this->assertEquals($count+1, $DB->count_records('user'));
+        $this->assertSame($user->username, clean_param($user->username, PARAM_USERNAME));
+        $this->assertSame($user->email, clean_param($user->email, PARAM_EMAIL));
+        $user = $generator->create_user(array('firstname'=>'Žluťoučký', 'lastname'=>'Koníček'));
+        $this->assertSame($user->username, clean_param($user->username, PARAM_USERNAME));
+        $this->assertSame($user->email, clean_param($user->email, PARAM_EMAIL));
 
         $count = $DB->count_records('course_categories');
         $category = $generator->create_category();
index b224d6d..470fa88 100644 (file)
@@ -2514,12 +2514,4 @@ class plugininfo_local extends plugininfo_base {
     public function get_uninstall_url() {
         return new moodle_url('/admin/localplugins.php', array('delete' => $this->name, 'sesskey' => sesskey()));
     }
-
-    public function get_settings_url() {
-        if (file_exists($this->full_path('settings.php'))) {
-            return new moodle_url('/admin/settings.php', array('section' => 'local_' . $this->name));
-        } else {
-            return parent::get_settings_url();
-        }
-    }
 }
index c8aeacd..311629d 100644 (file)
@@ -1179,12 +1179,25 @@ function question_add_tops($categories, $pcontexts) {
 function question_categorylist($categoryid) {
     global $DB;
 
-    $subcategories = $DB->get_records('question_categories',
-            array('parent' => $categoryid), 'sortorder ASC', 'id, 1');
+    // final list of category IDs
+    $categorylist = array();
 
-    $categorylist = array($categoryid);
-    foreach ($subcategories as $subcategory) {
-        $categorylist = array_merge($categorylist, question_categorylist($subcategory->id));
+    // a list of category IDs to check for any sub-categories
+    $subcategories = array($categoryid);
+
+    while ($subcategories) {
+        foreach ($subcategories as $subcategory) {
+            // if anything from the temporary list was added already, then we have a loop
+            if (isset($categorylist[$subcategory])) {
+                throw new coding_exception("Category id=$subcategory is already on the list - loop of categories detected.");
+            }
+            $categorylist[$subcategory] = $subcategory;
+        }
+
+        list ($in, $params) = $DB->get_in_or_equal($subcategories);
+
+        $subcategories = $DB->get_records_select_menu('question_categories',
+                "parent $in", $params, NULL, 'id,id AS id2');
     }
 
     return $categorylist;
index c515888..d5d50bf 100644 (file)
@@ -126,6 +126,7 @@ class user_picture_testcase extends advanced_testcase {
         $this->assertEquals('http://www.example.com/moodle', $CFG->wwwroot);
         $this->assertEquals($CFG->wwwroot, $CFG->httpswwwroot);
         $this->assertEquals(0, $CFG->enablegravatar);
+        $this->assertEquals('mm', $CFG->gravatardefaulturl);
 
         // create some users
         $page = new moodle_page();
@@ -197,21 +198,25 @@ class user_picture_testcase extends advanced_testcase {
         // test gravatar
         set_config('enablegravatar', 1);
 
-        $up2 = new user_picture($user2);
-        $this->assertEquals('http://www.gravatar.com/avatar/ab53a2911ddf9b4817ac01ddcd3d975f?s=35&d=http%3A%2F%2Fwww.example.com%2Fmoodle%2Ftheme%2Fimage.php%2Fstandard%2Fcore%2F1%2Fu%2Ff2', $up2->get_url($page, $renderer)->out(false));
-
-        // uploaded image takes precedence before gravatar
-        $up1 = new user_picture($user1);
-        $this->assertEquals($CFG->wwwroot.'/pluginfile.php/15/user/icon/standard/f2?rev=11', $up1->get_url($page, $renderer)->out(false));
-
         // deleted user can not have gravatar
         $user3->email = 'deleted';
         $user3->picture = 0;
         $up3 = new user_picture($user3);
         $this->assertEquals($CFG->wwwroot.'/theme/image.php/standard/core/1/u/f2', $up3->get_url($page, $renderer)->out(false));
 
+        // verify defaults to misteryman (mm)
+        $up2 = new user_picture($user2);
+        $this->assertEquals('http://www.gravatar.com/avatar/ab53a2911ddf9b4817ac01ddcd3d975f?s=35&d=mm', $up2->get_url($page, $renderer)->out(false));
+
+        // without gravatardefaulturl, verify we pick own file
+        set_config('gravatardefaulturl', '');
+        $up2 = new user_picture($user2);
+        $this->assertEquals('http://www.gravatar.com/avatar/ab53a2911ddf9b4817ac01ddcd3d975f?s=35&d=http%3A%2F%2Fwww.example.com%2Fmoodle%2Fpix%2Fu%2Ff2.png', $up2->get_url($page, $renderer)->out(false));
+        // uploaded image takes precedence before gravatar
+        $up1 = new user_picture($user1);
+        $this->assertEquals($CFG->wwwroot.'/pluginfile.php/15/user/icon/standard/f2?rev=11', $up1->get_url($page, $renderer)->out(false));
 
-        // https versions
+        // https version
         $CFG->httpswwwroot = str_replace('http:', 'https:', $CFG->wwwroot);
 
         $up1 = new user_picture($user1);
@@ -221,7 +226,26 @@ class user_picture_testcase extends advanced_testcase {
         $this->assertEquals($CFG->httpswwwroot.'/theme/image.php/standard/core/1/u/f2', $up3->get_url($page, $renderer)->out(false));
 
         $up2 = new user_picture($user2);
-        $this->assertEquals('https://secure.gravatar.com/avatar/ab53a2911ddf9b4817ac01ddcd3d975f?s=35&d=https%3A%2F%2Fwww.example.com%2Fmoodle%2Ftheme%2Fimage.php%2Fstandard%2Fcore%2F1%2Fu%2Ff2', $up2->get_url($page, $renderer)->out(false));
+        $this->assertEquals('https://secure.gravatar.com/avatar/ab53a2911ddf9b4817ac01ddcd3d975f?s=35&d=https%3A%2F%2Fwww.example.com%2Fmoodle%2Fpix%2Fu%2Ff2.png', $up2->get_url($page, $renderer)->out(false));
+
+        // now test gravatar with one theme having own images (afterburner)
+        $CFG->httpswwwroot = $CFG->wwwroot;
+        $this->assertTrue(file_exists("$CFG->dirroot/theme/afterburner/config.php"));
+        set_config('theme', 'afterburner');
+        $page = new moodle_page();
+        $page->set_url('/user/profile.php');
+        $page->set_context(context_system::instance());
+        $renderer = $page->get_renderer('core');
+
+        $up2 = new user_picture($user2);
+        $this->assertEquals('http://www.gravatar.com/avatar/ab53a2911ddf9b4817ac01ddcd3d975f?s=35&d=http%3A%2F%2Fwww.example.com%2Fmoodle%2Ftheme%2Fafterburner%2Fpix_core%2Fu%2Ff2.png', $up2->get_url($page, $renderer)->out(false));
+
+        // https version
+        $CFG->httpswwwroot = str_replace('http:', 'https:', $CFG->wwwroot);
+
+        $up2 = new user_picture($user2);
+        $this->assertEquals('https://secure.gravatar.com/avatar/ab53a2911ddf9b4817ac01ddcd3d975f?s=35&d=https%3A%2F%2Fwww.example.com%2Fmoodle%2Ftheme%2Fafterburner%2Fpix_core%2Fu%2Ff2.png', $up2->get_url($page, $renderer)->out(false));
+        // end of gravatar tests
 
         // test themed images
         set_config('enablegravatar', 0);
index c299ffe..cde67d9 100644 (file)
@@ -964,7 +964,7 @@ function external_update_descriptions($component) {
             $dbfunction->classpath = $function['classpath'];
             $update = true;
         }
-        $functioncapabilities = key_exists('capabilities', $function)?$function['capabilities']:'';
+        $functioncapabilities = array_key_exists('capabilities', $function)?$function['capabilities']:'';
         if ($dbfunction->capabilities != $functioncapabilities) {
             $dbfunction->capabilities = $functioncapabilities;
             $update = true;
@@ -980,7 +980,7 @@ function external_update_descriptions($component) {
         $dbfunction->methodname = $function['methodname'];
         $dbfunction->classpath  = empty($function['classpath']) ? null : $function['classpath'];
         $dbfunction->component  = $component;
-        $dbfunction->capabilities = key_exists('capabilities', $function)?$function['capabilities']:'';
+        $dbfunction->capabilities = array_key_exists('capabilities', $function)?$function['capabilities']:'';
         $dbfunction->id = $DB->insert_record('external_functions', $dbfunction);
     }
     unset($functions);
index f8b664d..0772f18 100644 (file)
@@ -167,6 +167,10 @@ YUI.add('moodle-core-dragdrop', function(Y) {
             // this.lastdroptarget (ghost node we use for indicating where to drop)
             e.drag = e.target;
             e.drop = this.lastdroptarget;
+            // Check that drag object belongs to correct group
+            if (!this.in_group(e.drag)) {
+                return;
+            }
             // Check that drop object belong to correct group
             if (!e.drop || !e.drop.inGroup(this.groups)) {
                 return;
index 8f73ac1..366d5d8 100644 (file)
@@ -59,6 +59,17 @@ class mod_assign_grade_form extends moodleform {
         }
     }
 
+    /**
+     * This is required so when using "Save and next", each form is not defaulted to the previous form.
+     * Giving each form a unique identitifer is enough to prevent this (include the rownum in the form name).
+     *
+     * @return string - The unique identifier for this form.
+     */
+    protected function get_form_identifier() {
+        $params = $this->_customdata[2];
+        return get_class($this) . '_' . $params['rownum'];
+    }
+
     /**
      * Perform minimal validation on the grade form
      * @param array $data
index 3e6e54b..b24f4fa 100644 (file)
@@ -44,16 +44,17 @@ class mod_assign_grading_options_form extends moodleform {
     function definition() {
         $mform = $this->_form;
         $instance = $this->_customdata;
+        $dirtyclass = array('class'=>'ignoredirty');
 
         $mform->addElement('header', 'general', get_string('gradingoptions', 'assign'));
         // visible elements
         $options = array(-1=>get_string('all'),10=>'10', 20=>'20', 50=>'50', 100=>'100');
-        $mform->addElement('select', 'perpage', get_string('assignmentsperpage', 'assign'), $options);
+        $mform->addElement('select', 'perpage', get_string('assignmentsperpage', 'assign'), $options, $dirtyclass);
         $options = array('' => get_string('filternone', 'assign'),
                          ASSIGN_FILTER_SUBMITTED => get_string('filtersubmitted', 'assign'),
                          ASSIGN_FILTER_REQUIRE_GRADING => get_string('filterrequiregrading', 'assign'));
         if ($instance['submissionsenabled']) {
-            $mform->addElement('select', 'filter', get_string('filter', 'assign'), $options);
+            $mform->addElement('select', 'filter', get_string('filter', 'assign'), $options, $dirtyclass);
         }
 
         // quickgrading
index 96c1c22..f362421 100644 (file)
@@ -78,6 +78,7 @@ $string['couldnotfindassignmenttoupgrade'] = 'Could not find old assignment inst
 $string['currentgrade'] = 'Current grade in gradebook';
 $string['defaultplugins'] = 'Default assignment settings';
 $string['defaultplugins_help'] = 'These settings define the defaults for all new assignments.';
+$string['deleteallsubmissions'] = 'Delete all submissions';
 $string['deletepluginareyousure'] = 'Delete assignment plugin {$a}: are you sure?';
 $string['deletepluginareyousuremessage'] = 'You are about to completely delete the assignment plugin {$a}. This will completely delete everything in the database associated with this assignment plugin. Are you SURE you want to continue?';
 $string['deletingplugin'] = 'Deleting plugin {$a}.';
index 070dc50..92490ec 100644 (file)
@@ -54,6 +54,72 @@ function assign_delete_instance($id) {
     return $assignment->delete_instance();
 }
 
+/**
+ * This function is used by the reset_course_userdata function in moodlelib.
+ * This function will remove all assignment submissions and feedbacks in the database
+ * and clean up any related data.
+ * @param $data the data submitted from the reset course.
+ * @return array status array
+ */
+function assign_reset_userdata($data) {
+    global $CFG, $DB;
+    require_once($CFG->dirroot . '/mod/assign/locallib.php');
+
+    $status = array();
+    $params = array('courseid'=>$data->courseid);
+    $sql = "SELECT a.id FROM {assign} a WHERE a.course=:courseid";
+    $course = $DB->get_record('course', array('id'=> $data->courseid), '*', MUST_EXIST);
+    if ($assigns = $DB->get_records_sql($sql,$params)) {
+        foreach ($assigns as $assign) {
+            $cm = get_coursemodule_from_instance('assign', $assign->id, $data->courseid, false, MUST_EXIST);
+            $context = context_module::instance($cm->id);
+            $assignment = new assign($context, $cm, $course);
+            $status = array_merge($status, $assignment->reset_userdata($data));
+        }
+    }
+    return $status;
+}
+
+/**
+ * Removes all grades from gradebook
+ *
+ * @param int $courseid The ID of the course to reset
+ * @param string $type Optional type of assignment to limit the reset to a particular assignment type
+ */
+function assign_reset_gradebook($courseid, $type='') {
+    global $CFG, $DB;
+
+    $params = array('moduletype'=>'assign','courseid'=>$courseid);
+    $sql = 'SELECT a.*, cm.idnumber as cmidnumber, a.course as courseid
+            FROM {assign} a, {course_modules} cm, {modules} m
+            WHERE m.name=:moduletype AND m.id=cm.module AND cm.instance=a.id AND a.course=:courseid';
+
+    if ($assignments = $DB->get_records_sql($sql,$params)) {
+        foreach ($assignments as $assignment) {
+            assign_grade_item_update($assignment, 'reset');
+        }
+    }
+}
+
+/**
+ * Implementation of the function for printing the form elements that control
+ * whether the course reset functionality affects the assignment.
+ * @param $mform form passed by reference
+ */
+function assign_reset_course_form_definition(&$mform) {
+    $mform->addElement('header', 'assignheader', get_string('modulenameplural', 'assign'));
+    $mform->addElement('advcheckbox', 'reset_assign_submissions', get_string('deleteallsubmissions','assign'));
+}
+
+/**
+ * Course reset form defaults.
+ * @param  object $course
+ * @return array
+ */
+function assign_reset_course_form_defaults($course) {
+    return array('reset_assign_submissions'=>1);
+}
+
 /**
  * Update an assignment instance
  *
@@ -757,7 +823,9 @@ function assign_get_user_grades($assign, $userid=0) {
     global $CFG;
     require_once($CFG->dirroot . '/mod/assign/locallib.php');
 
-    $assignment = new assign(null, null, null);
+    $cm = get_coursemodule_from_instance('assign', $assign->id, 0, false, MUST_EXIST);
+    $context = context_module::instance($cm->id);
+    $assignment = new assign($context, null, null);
     $assignment->set_instance($assign);
     return $assignment->get_user_grades_for_gradebook($userid);
 }
index 8ba72d4..7238b70 100644 (file)
@@ -505,6 +505,82 @@ class assign {
         return $result;
     }
 
+    /**
+    * Actual implementation of the reset course functionality, delete all the
+    * assignment submissions for course $data->courseid.
+    *
+    * @param $data the data submitted from the reset course.
+    * @return array status array
+    */
+    public function reset_userdata($data) {
+        global $CFG,$DB;
+
+        $componentstr = get_string('modulenameplural', 'assign');
+        $status = array();
+
+        $fs = get_file_storage();
+        if (!empty($data->reset_assign_submissions)) {
+            // Delete files associated with this assignment.
+            foreach ($this->submissionplugins as $plugin) {
+                $fileareas = array();
+                $plugincomponent = $plugin->get_subtype() . '_' . $plugin->get_type();
+                $fileareas = $plugin->get_file_areas();
+                foreach ($fileareas as $filearea) {
+                    $fs->delete_area_files($this->context->id, $plugincomponent, $filearea);
+                }
+
+                if (!$plugin->delete_instance()) {
+                    $status[] = array('component'=>$componentstr,
+                                      'item'=>get_string('deleteallsubmissions','assign'),
+                                      'error'=>$plugin->get_error());
+                }
+            }
+
+            foreach ($this->feedbackplugins as $plugin) {
+                $fileareas = array();
+                $plugincomponent = $plugin->get_subtype() . '_' . $plugin->get_type();
+                $fileareas = $plugin->get_file_areas();
+                foreach ($fileareas as $filearea) {
+                    $fs->delete_area_files($this->context->id, $plugincomponent, $filearea);
+                }
+
+                if (!$plugin->delete_instance()) {
+                    $status[] = array('component'=>$componentstr,
+                                      'item'=>get_string('deleteallsubmissions','assign'),
+                                      'error'=>$plugin->get_error());
+                }
+            }
+
+            $assignssql = "SELECT a.id
+                             FROM {assign} a
+                           WHERE a.course=:course";
+            $params = array ("course" => $data->courseid);
+
+            $DB->delete_records_select('assign_submission', "assignment IN ($assignssql)", $params);
+            $status[] = array('component'=>$componentstr,
+                              'item'=>get_string('deleteallsubmissions','assign'),
+                              'error'=>false);
+
+            if (empty($data->reset_gradebook_grades)) {
+                // Remove all grades from gradebook.
+                require_once($CFG->dirroot.'/mod/assign/lib.php');
+                assign_reset_gradebook($data->courseid);
+            }
+        }
+        // Updating dates - shift may be negative too.
+        if ($data->timeshift) {
+            shift_course_mod_dates('assign',
+                                    array('duedate', 'allowsubmissionsfromdate'),
+                                    $data->timeshift,
+                                    $data->courseid);
+            $status[] = array('component'=>$componentstr,
+                              'item'=>get_string('datechanged'),
+                              'error'=>false);
+        }
+
+        return $status;
+    }
+
     /**
      * Update the settings for a single plugin
      *
index 470e18e..e15de10 100644 (file)
@@ -47,6 +47,7 @@ $string['editingchapter'] = 'Editing chapter';
 $string['chaptertitle'] = 'Chapter title';
 $string['content'] = 'Content';
 $string['subchapter'] = 'Subchapter';
+$string['nocontent'] = 'No content has been added to this book yet.';
 $string['numbering'] = 'Chapter formatting';
 $string['numbering_help'] = '* None - Chapter and subchapter titles have no formatting
 * Numbers - Chapters and subchapter titles are numbered 1, 1.1, 1.2, 2, ...
index 26d0e40..9502805 100644 (file)
@@ -448,18 +448,84 @@ class book_file_info extends file_info {
      * @return array of file_info instances
      */
     public function get_children() {
+        return $this->get_filtered_children('*', false, true);
+    }
+
+    /**
+     * Help function to return files matching extensions or their count
+     *
+     * @param string|array $extensions, either '*' or array of lowercase extensions, i.e. array('.gif','.jpg')
+     * @param bool|int $countonly if false returns the children, if an int returns just the
+     *    count of children but stops counting when $countonly number of children is reached
+     * @param bool $returnemptyfolders if true returns items that don't have matching files inside
+     * @return array|int array of file_info instances or the count
+     */
+    private function get_filtered_children($extensions = '*', $countonly = false, $returnemptyfolders = false) {
         global $DB;
+        $params = array('contextid' => $this->context->id,
+            'component' => 'mod_book',
+            'filearea' => $this->filearea,
+            'bookid' => $this->cm->instance);
+        $sql = 'SELECT DISTINCT bc.id, bc.pagenum
+                    FROM {files} f, {book_chapters} bc
+                    WHERE f.contextid = :contextid
+                    AND f.component = :component
+                    AND f.filearea = :filearea
+                    AND bc.bookid = :bookid
+                    AND bc.id = f.itemid';
+        if (!$returnemptyfolders) {
+            $sql .= ' AND filename <> :emptyfilename';
+            $params['emptyfilename'] = '.';
+        }
+        list($sql2, $params2) = $this->build_search_files_sql($extensions, 'f');
+        $sql .= ' '.$sql2;
+        $params = array_merge($params, $params2);
+        if ($countonly === false) {
+            $sql .= ' ORDER BY bc.pagenum';
+        }
 
+        $rs = $DB->get_recordset_sql($sql, $params);
         $children = array();
-        $chapters = $DB->get_records('book_chapters', array('bookid'=>$this->cm->instance), 'pagenum', 'id, pagenum');
-        foreach ($chapters as $itemid => $unused) {
-            if ($child = $this->browser->get_file_info($this->context, 'mod_book', $this->filearea, $itemid)) {
-                $children[] = $child;
+        foreach ($rs as $record) {
+            if ($child = $this->browser->get_file_info($this->context, 'mod_book', $this->filearea, $record->id)) {
+                if ($returnemptyfolders || $child->count_non_empty_children($extensions)) {
+                    $children[] = $child;
+                }
+            }
+            if ($countonly !== false && count($children) >= $countonly) {
+                break;
             }
         }
+        $rs->close();
+        if ($countonly !== false) {
+            return count($children);
+        }
         return $children;
     }
 
+    /**
+     * Returns list of children which are either files matching the specified extensions
+     * or folders that contain at least one such file.
+     *
+     * @param string|array $extensions, either '*' or array of lowercase extensions, i.e. array('.gif','.jpg')
+     * @return array of file_info instances
+     */
+    public function get_non_empty_children($extensions = '*') {
+        return $this->get_filtered_children($extensions, false);
+    }
+
+    /**
+     * Returns the number of children which are either files matching the specified extensions
+     * or folders containing at least one such file.
+     *
+     * @param string|array $extensions, for example '*' or array('.gif','.jpg')
+     * @param int $limit stop counting after at least $limit non-empty children are found
+     * @return int
+     */
+    public function count_non_empty_children($extensions = '*', $limit = 1) {
+        return $this->get_filtered_children($extensions, $limit);
+    }
+
     /**
      * Returns parent file_info instance
      * @return file_info or null for root
index eaae261..25ee6f7 100644 (file)
@@ -60,9 +60,9 @@ $mform = new booktool_importhtml_form(null, array('id'=>$id, 'chapterid'=>$chapt
 // If data submitted, then process and store.
 if ($mform->is_cancelled()) {
     if (empty($chapter->id)) {
-        redirect("/mod/book/view.php?id=$cm->id");
+        redirect($CFG->wwwroot."/mod/book/view.php?id=$cm->id");
     } else {
-        redirect("/mod/book/view.php?id=$cm->id&chapterid=$chapter->id");
+        redirect($CFG->wwwroot."/mod/book/view.php?id=$cm->id&chapterid=$chapter->id");
     }
 
 } else if ($data = $mform->get_data()) {
index 91b83b8..6b9365d 100644 (file)
@@ -87,13 +87,16 @@ if ($chapterid == '0') { // Go to first chapter if no given.
     }
 }
 
-if (!$chapterid or !$chapter = $DB->get_record('book_chapters', array('id'=>$chapterid, 'bookid'=>$book->id))) {
-    print_error('errorchapter', 'mod_book', new moodle_url('/course/view.php', array('id'=>$course->id)));
-}
+$courseurl = new moodle_url('/course/view.php', array('id' => $course->id));
 
-// chapter is hidden for students
-if ($chapter->hidden and !$viewhidden) {
-    print_error('errorchapter', 'mod_book', new moodle_url('/course/view.php', array('id'=>$course->id)));
+// No content in the book.
+if (!$chapterid) {
+    $PAGE->set_url('/mod/book/view.php', array('id' => $id));
+    notice(get_string('nocontent', 'mod_book'), $courseurl->out(false));
+}
+// Chapter doesnt exist or it is hidden for students
+if ((!$chapter = $DB->get_record('book_chapters', array('id' => $chapterid, 'bookid' => $book->id))) or ($chapter->hidden and !$viewhidden)) {
+    print_error('errorchapter', 'mod_book', $courseurl);
 }
 
 $PAGE->set_url('/mod/book/view.php', array('id'=>$id, 'chapterid'=>$chapterid));
index 313058c..2fa245d 100644 (file)
@@ -55,12 +55,13 @@ class data_field_date extends data_field_base {
 
     //Enable the following three functions once core API issues have been addressed.
     function display_search_field($value=0) {
-        $selectors = html_writer::select_time('days', 'f_'.$this->field->id.'_d', $value)
-           . html_writer::select_time('months', 'f_'.$this->field->id.'_m', $value)
-           . html_writer::select_time('years', 'f_'.$this->field->id.'_y', $value);
-       return $selectors;
+        $selectors = html_writer::select_time('days', 'f_'.$this->field->id.'_d', $value['timestamp'])
+           . html_writer::select_time('months', 'f_'.$this->field->id.'_m', $value['timestamp'])
+           . html_writer::select_time('years', 'f_'.$this->field->id.'_y', $value['timestamp']);
+        $datecheck = html_writer::checkbox('f_'.$this->field->id.'_z', 1, $value['usedate']);
+        $str = $selectors . ' ' . $datecheck . ' ' . get_string('usedate', 'data');
 
-        //return print_date_selector('f_'.$this->field->id.'_d', 'f_'.$this->field->id.'_m', 'f_'.$this->field->id.'_y', $value, true);
+        return $str;
     }
 
     function generate_sql($tablealias, $value) {
@@ -70,21 +71,22 @@ class data_field_date extends data_field_base {
         $i++;
         $name = "df_date_$i";
         $varcharcontent = $DB->sql_compare_text("{$tablealias}.content");
-        return array(" ({$tablealias}.fieldid = {$this->field->id} AND $varcharcontent = :$name) ", array($name=>$value));
+        return array(" ({$tablealias}.fieldid = {$this->field->id} AND $varcharcontent = :$name) ", array($name => $value['timestamp']));
     }
 
     function parse_search_field() {
-
         $day   = optional_param('f_'.$this->field->id.'_d', 0, PARAM_INT);
         $month = optional_param('f_'.$this->field->id.'_m', 0, PARAM_INT);
         $year  = optional_param('f_'.$this->field->id.'_y', 0, PARAM_INT);
-        if (!empty($day) && !empty($month) && !empty($year)) {
-            return make_timestamp($year, $month, $day, 12, 0, 0, 0, false);
-        }
-        else {
+        $usedate = optional_param('f_'.$this->field->id.'_z', 0, PARAM_INT);
+        $data = array();
+        if (!empty($day) && !empty($month) && !empty($year) && $usedate == 1) {
+            $data['timestamp'] = make_timestamp($year, $month, $day, 12, 0, 0, 0, false);
+            $data['usedate'] = 1;
+            return $data;
+        } else {
             return 0;
         }
-
     }
 
     function update_content($recordid, $value, $name='') {
index ed005e6..53887ec 100644 (file)
@@ -338,6 +338,7 @@ $string['uploadrecords_help'] = 'Entries may be uploaded via text file. The form
 The field enclosure is a character that surrounds each field in each record. It can normally be left unset.';
 $string['uploadrecords_link'] = 'mod/data/import';
 $string['url'] = 'Url';
+$string['usedate'] = 'Include in search.';
 $string['usestandard'] = 'Use a preset';
 $string['usestandard_help'] = 'To use a preset available to the whole site, select it from the list. (If you have added a preset to the list using the save as preset feature then you have the option of deleting it.)';
 $string['viewfromdate'] = 'Read only from';
index fc26f78..583ea73 100644 (file)
@@ -3408,21 +3408,28 @@ function data_page_type_list($pagetype, $parentcontext, $currentcontext) {
 /**
  * Get all of the record ids from a database activity.
  *
- * @param int $dataid      The dataid of the database module.
- * @return array $idarray  An array of record ids
+ * @param int    $dataid      The dataid of the database module.
+ * @param object $selectdata  Contains an additional sql statement for the
+ *                            where clause for group and approval fields.
+ * @param array  $params      Parameters that coincide with the sql statement.
+ * @return array $idarray     An array of record ids
  */
-function data_get_all_recordids($dataid) {
+function data_get_all_recordids($dataid, $selectdata = '', $params = null) {
     global $DB;
-    $initsql = 'SELECT c.recordid
-                  FROM {data_fields} f,
-                       {data_content} c
-                 WHERE f.dataid = :dataid
-                   AND f.id = c.fieldid
-              GROUP BY c.recordid';
-    $initrecord = $DB->get_recordset_sql($initsql, array('dataid' => $dataid));
+    $initsql = 'SELECT r.id
+                  FROM {data_records} r
+                 WHERE r.dataid = :dataid';
+    if ($selectdata != '') {
+        $initsql .= $selectdata;
+        $params = array_merge(array('dataid' => $dataid), $params);
+    } else {
+        $params = array('dataid' => $dataid);
+    }
+    $initsql .= ' GROUP BY r.id';
+    $initrecord = $DB->get_recordset_sql($initsql, $params);
     $idarray = array();
     foreach ($initrecord as $data) {
-        $idarray[] = $data->recordid;
+        $idarray[] = $data->id;
     }
     // Close the record set and free up resources.
     $initrecord->close();
@@ -3567,8 +3574,7 @@ function data_get_advanced_search_sql($sort, $data, $recordids, $selectdata, $so
     } else {
         list($insql, $inparam) = $DB->get_in_or_equal(array('-1'), SQL_PARAMS_NAMED);
     }
-    $nestfromsql .= ' AND c.recordid ' . $insql . $groupsql;
-    $nestfromsql = "$nestfromsql $selectdata";
+    $nestfromsql .= ' AND c.recordid ' . $insql . $selectdata . $groupsql;
     $sqlselect['sql'] = "$nestselectsql $nestfromsql $sortorder";
     $sqlselect['params'] = $inparam;
     return $sqlselect;
index e06430a..55a2070 100644 (file)
@@ -493,20 +493,79 @@ class data_file_info_container extends file_info {
      * @return array of file_info instances
      */
     public function get_children() {
+        return $this->get_filtered_children('*', false, true);
+    }
+
+    /**
+     * Help function to return files matching extensions or their count
+     *
+     * @param string|array $extensions, either '*' or array of lowercase extensions, i.e. array('.gif','.jpg')
+     * @param bool|int $countonly if false returns the children, if an int returns just the
+     *    count of children but stops counting when $countonly number of children is reached
+     * @param bool $returnemptyfolders if true returns items that don't have matching files inside
+     * @return array|int array of file_info instances or the count
+     */
+    private function get_filtered_children($extensions = '*', $countonly = false, $returnemptyfolders = false) {
         global $DB;
+        $params = array('contextid' => $this->context->id,
+            'component' => $this->component,
+            'filearea' => $this->filearea);
+        $sql = 'SELECT DISTINCT itemid
+                    FROM {files}
+                    WHERE contextid = :contextid
+                    AND component = :component
+                    AND filearea = :filearea';
+        if (!$returnemptyfolders) {
+            $sql .= ' AND filename <> :emptyfilename';
+            $params['emptyfilename'] = '.';
+        }
+        list($sql2, $params2) = $this->build_search_files_sql($extensions);
+        $sql .= ' '.$sql2;
+        $params = array_merge($params, $params2);
+        if ($countonly === false) {
+            $sql .= ' ORDER BY itemid DESC';
+        }
 
+        $rs = $DB->get_recordset_sql($sql, $params);
         $children = array();
-        $itemids = $DB->get_records('files', array('contextid' => $this->context->id, 'component' => $this->component,
-            'filearea' => $this->filearea), 'itemid DESC', "DISTINCT itemid");
-        foreach ($itemids as $itemid => $unused) {
-            if ($child = $this->browser->get_file_info($this->context, 'mod_data', $this->filearea, $itemid)) {
+        foreach ($rs as $record) {
+            if ($child = $this->browser->get_file_info($this->context, 'mod_data', $this->filearea, $record->itemid)) {
                 $children[] = $child;
             }
+            if ($countonly !== false && count($children) >= $countonly) {
+                break;
+            }
+        }
+        $rs->close();
+        if ($countonly !== false) {
+            return count($children);
         }
-
         return $children;
     }
 
+    /**
+     * Returns list of children which are either files matching the specified extensions
+     * or folders that contain at least one such file.
+     *
+     * @param string|array $extensions, either '*' or array of lowercase extensions, i.e. array('.gif','.jpg')
+     * @return array of file_info instances
+     */
+    public function get_non_empty_children($extensions = '*') {
+        return $this->get_filtered_children($extensions, false);
+    }
+
+    /**
+     * Returns the number of children which are either files matching the specified extensions
+     * or folders containing at least one such file.
+     *
+     * @param string|array $extensions, for example '*' or array('.gif','.jpg')
+     * @param int $limit stop counting after at least $limit non-empty children are found
+     * @return int
+     */
+    public function count_non_empty_children($extensions = '*', $limit = 1) {
+        return $this->get_filtered_children($extensions, $limit);
+    }
+
     /**
      * Returns parent file_info instance
      *
index 5ffa5bf..f740429 100644 (file)
 id,userid,groupid,dataid,timecreated,timemodified,approved
 1,1,0,1,1234567891,1234567892,1
-2,2,0,1,1234567891,1234567892,1
-3,3,0,1,1234567891,1234567892,1
-4,4,0,1,1234567891,1234567892,1
-5,5,0,1,1234567891,1234567892,1
-6,6,0,1,1234567891,1234567892,1
+2,2,1,1,1234567891,1234567892,1
+3,3,2,1,1234567891,1234567892,1
+4,4,1,1,1234567891,1234567892,1
+5,5,1,1,1234567891,1234567892,1
+6,6,2,1,1234567891,1234567892,1
 7,7,0,1,1234567891,1234567892,1
-8,8,0,1,1234567891,1234567892,1
-9,9,0,1,1234567891,1234567892,1
-10,10,0,1,1234567891,1234567892,1
-11,11,0,1,1234567891,1234567892,1
-12,12,0,1,1234567891,1234567892,1
-13,13,0,1,1234567891,1234567892,1
+8,8,2,1,1234567891,1234567892,1
+9,9,1,1,1234567891,1234567892,1
+10,10,1,1,1234567891,1234567892,1
+11,11,1,1,1234567891,1234567892,1
+12,12,2,1,1234567891,1234567892,1
+13,13,2,1,1234567891,1234567892,1
 14,14,0,1,1234567891,1234567892,1
-15,15,0,1,1234567891,1234567892,1
+15,15,2,1,1234567891,1234567892,1
 16,16,0,1,1234567891,1234567892,1
-17,17,0,1,1234567891,1234567892,1
-18,18,0,1,1234567891,1234567892,1
+17,17,1,1,1234567891,1234567892,1
+18,18,2,1,1234567891,1234567892,1
 19,19,0,1,1234567891,1234567892,1
-20,20,0,1,1234567891,1234567892,1
+20,20,1,1,1234567891,1234567892,1
 21,21,0,1,1234567891,1234567892,1
-22,22,0,1,1234567891,1234567892,1
+22,22,2,1,1234567891,1234567892,1
 23,23,0,1,1234567891,1234567892,1
-24,24,0,1,1234567891,1234567892,1
-25,25,0,1,1234567891,1234567892,1
+24,24,2,1,1234567891,1234567892,1
+25,25,2,1,1234567891,1234567892,1
 26,26,0,1,1234567891,1234567892,1
-27,27,0,1,1234567891,1234567892,1
-28,28,0,1,1234567891,1234567892,1
+27,27,1,1,1234567891,1234567892,1
+28,28,2,1,1234567891,1234567892,1
 29,29,0,1,1234567891,1234567892,1
-30,30,0,1,1234567891,1234567892,1
+30,30,2,1,1234567891,1234567892,1
 31,31,0,1,1234567891,1234567892,1
-32,32,0,1,1234567891,1234567892,1
-33,33,0,1,1234567891,1234567892,1
-34,34,0,1,1234567891,1234567892,1
-35,35,0,1,1234567891,1234567892,1
+32,32,1,1,1234567891,1234567892,1
+33,33,1,1,1234567891,1234567892,1
+34,34,2,1,1234567891,1234567892,1
+35,35,1,1,1234567891,1234567892,1
 36,36,0,1,1234567891,1234567892,1
 37,37,0,1,1234567891,1234567892,1
-38,38,0,1,1234567891,1234567892,1
-39,39,0,1,1234567891,1234567892,1
-40,40,0,1,1234567891,1234567892,1
+38,38,2,1,1234567891,1234567892,1
+39,39,1,1,1234567891,1234567892,1
+40,40,1,1,1234567891,1234567892,1
 41,41,0,1,1234567891,1234567892,1
-42,42,0,1,1234567891,1234567892,1
+42,42,1,1,1234567891,1234567892,1
 43,43,0,1,1234567891,1234567892,1
-44,44,0,1,1234567891,1234567892,1
+44,44,1,1,1234567891,1234567892,1
 45,45,0,1,1234567891,1234567892,1
 46,46,0,1,1234567891,1234567892,1
-47,47,0,1,1234567891,1234567892,1
+47,47,1,1,1234567891,1234567892,1
 48,48,0,1,1234567891,1234567892,1
 49,49,0,1,1234567891,1234567892,1
-50,50,0,1,1234567891,1234567892,1
+50,50,1,1,1234567891,1234567892,1
 51,51,0,1,1234567891,1234567892,1
 52,52,0,1,1234567891,1234567892,1
-53,53,0,1,1234567891,1234567892,1
-54,54,0,1,1234567891,1234567892,1
+53,53,1,1,1234567891,1234567892,1
+54,54,1,1,1234567891,1234567892,1
 55,55,0,1,1234567891,1234567892,1
-56,56,0,1,1234567891,1234567892,1
-57,57,0,1,1234567891,1234567892,1
-58,58,0,1,1234567891,1234567892,1
-59,59,0,1,1234567891,1234567892,1
-60,60,0,1,1234567891,1234567892,1
+56,56,2,1,1234567891,1234567892,1
+57,57,2,1,1234567891,1234567892,1
+58,58,2,1,1234567891,1234567892,1
+59,59,1,1,1234567891,1234567892,1
+60,60,1,1,1234567891,1234567892,1
 61,61,0,1,1234567891,1234567892,1
-62,62,0,1,1234567891,1234567892,1
+62,62,2,1,1234567891,1234567892,1
 63,63,0,1,1234567891,1234567892,1
 64,64,0,1,1234567891,1234567892,1
-65,65,0,1,1234567891,1234567892,1
-66,66,0,1,1234567891,1234567892,1
+65,65,1,1,1234567891,1234567892,1
+66,66,1,1,1234567891,1234567892,1
 67,67,0,1,1234567891,1234567892,1
 68,68,0,1,1234567891,1234567892,1
-69,69,0,1,1234567891,1234567892,1
-70,70,0,1,1234567891,1234567892,1
+69,69,2,1,1234567891,1234567892,1
+70,70,2,1,1234567891,1234567892,1
 71,71,0,1,1234567891,1234567892,1
-72,72,0,1,1234567891,1234567892,1
-73,73,0,1,1234567891,1234567892,1
+72,72,1,1,1234567891,1234567892,1
+73,73,1,1,1234567891,1234567892,1
 74,74,0,1,1234567891,1234567892,1
 75,75,0,1,1234567891,1234567892,1
-76,76,0,1,1234567891,1234567892,1
-77,77,0,1,1234567891,1234567892,1
+76,76,2,1,1234567891,1234567892,1
+77,77,2,1,1234567891,1234567892,1
 78,78,0,1,1234567891,1234567892,1
-79,79,0,1,1234567891,1234567892,1
-80,80,0,1,1234567891,1234567892,1
+79,79,1,1,1234567891,1234567892,1
+80,80,1,1,1234567891,1234567892,1
 81,81,0,1,1234567891,1234567892,1
-82,82,0,1,1234567891,1234567892,1
-83,83,0,1,1234567891,1234567892,1
-84,84,0,1,1234567891,1234567892,1
-85,85,0,1,1234567891,1234567892,1
+82,82,1,1,1234567891,1234567892,1
+83,83,1,1,1234567891,1234567892,1
+84,84,1,1,1234567891,1234567892,1
+85,85,1,1,1234567891,1234567892,1
 86,86,0,1,1234567891,1234567892,1
 87,87,0,1,1234567891,1234567892,1
 88,88,0,1,1234567891,1234567892,1
-89,89,0,1,1234567891,1234567892,1
-90,90,0,1,1234567891,1234567892,1
-91,91,0,1,1234567891,1234567892,1
-92,92,0,1,1234567891,1234567892,1
-93,93,0,1,1234567891,1234567892,1
-94,94,0,1,1234567891,1234567892,1
-95,95,0,1,1234567891,1234567892,1
-96,96,0,1,1234567891,1234567892,1
-97,97,0,1,1234567891,1234567892,1
-98,98,0,1,1234567891,1234567892,1
-99,99,0,1,1234567891,1234567892,1
-100,100,0,1,1234567891,1234567892,1
+89,89,1,1,1234567891,1234567892,1
+90,90,1,1,1234567891,1234567892,0
+91,91,2,1,1234567891,1234567892,0
+92,92,0,1,1234567891,1234567892,0
+93,93,2,1,1234567891,1234567892,0
+94,94,1,1,1234567891,1234567892,0
+95,95,1,1,1234567891,1234567892,0
+96,96,1,1,1234567891,1234567892,0
+97,97,0,1,1234567891,1234567892,0
+98,98,1,1,1234567891,1234567892,0
+99,99,2,1,1234567891,1234567892,0
+100,100,0,1,1234567891,1234567892,0
index 150bc5d..2980643 100644 (file)
@@ -70,6 +70,11 @@ class data_advanced_search_sql_test extends advanced_testcase {
      */
     public $datarecordcount = 100;
 
+    /**
+     * @var int $groupdatarecordcount  The number of records in the database in groups 0 and 1.
+     */
+    public $groupdatarecordcount = 75;
+
     /**
      * @var array $datarecordset   Expected record IDs.
      */
@@ -80,6 +85,11 @@ class data_advanced_search_sql_test extends advanced_testcase {
      */
     public $finalrecord = array();
 
+    /**
+     * @var int $approvedatarecordcount  The number of approved records in the database.
+     */
+    public $approvedatarecordcount = 89;
+
     /**
      * Set up function. In this instance we are setting up database
      * records to be used in the unit tests.
@@ -169,6 +179,13 @@ class data_advanced_search_sql_test extends advanced_testcase {
      * Test 4: data_get_advanced_search_sql provides an array which contains an sql string to be used for displaying records
      * to the user when they use the advanced search criteria and the parameters that go with the sql statement. This test
      * takes that information and does a search on the database, returning a record.
+     *
+     * Test 5: Returning to data_get_all_recordids(). Here we are ensuring that the total amount of record ids is reduced to
+     * match the group conditions that are provided. There are 25 entries which relate to group 2. They are removed
+     * from the total so we should only have 75 records total.
+     *
+     * Test 6: data_get_all_recordids() again. This time we are testing approved database records. We only want to
+     * display the records that have been approved. In this record set we have 89 approved records.
      */
     function test_advanced_search_sql_section() {
         global $DB;
@@ -193,5 +210,16 @@ class data_advanced_search_sql_test extends advanced_testcase {
         $allparams = array_merge($html['params'], array('dataid' => $this->recorddata->id));
         $records = $DB->get_records_sql($html['sql'], $allparams);
         $this->assertEquals($records, $this->finalrecord);
+
+        // Test 5
+        $groupsql = " AND (r.groupid = :currentgroup OR r.groupid = 0)";
+        $params = array('currentgroup' => 1);
+        $recordids = data_get_all_recordids($this->recorddata->id, $groupsql, $params);
+        $this->assertEquals($this->groupdatarecordcount, count($recordids));
+
+        // Test 6
+        $approvesql = ' AND r.approved=1 ';
+        $recordids = data_get_all_recordids($this->recorddata->id, $approvesql, $params);
+        $this->assertEquals($this->approvedatarecordcount, count($recordids));
     }
 }
index 3e9146e..1b2d85b 100644 (file)
     groups_print_activity_menu($cm, $returnurl);
     $currentgroup = groups_get_activity_group($cm);
     $groupmode = groups_get_activity_groupmode($cm);
+    // If a student is not part of a group and seperate groups is enabled, we don't
+    // want them seeing all records.
+    if ($currentgroup == 0 && $groupmode == 1 && !has_capability('mod/data:manageentries', $context)) {
+        $canviewallrecords = false;
+    } else {
+        $canviewallrecords = true;
+    }
 
     // detect entries not approved yet and show hint instead of not found error
     if ($record and $data->approval and !$record->approved and $record->userid != $USER->id and !has_capability('mod/data:manageentries', $context)) {
@@ -465,7 +472,13 @@ if ($showactivity) {
             $groupselect = " AND (r.groupid = :currentgroup OR r.groupid = 0)";
             $params['currentgroup'] = $currentgroup;
         } else {
-            $groupselect = ' ';
+            if ($canviewallrecords) {
+                $groupselect = ' ';
+            } else {
+                // If separate groups are enabled and the user isn't in a group or
+                // a teacher, manager, admin etc, then just show them entries for 'All participants'.
+                $groupselect = " AND r.groupid = 0";
+            }
         }
 
         // Init some variables to be used by advanced search
@@ -590,10 +603,19 @@ if ($showactivity) {
         $sqlmax     = "SELECT $count FROM $tables $where $groupselect $approveselect"; // number of all recoirds user may see
         $allparams  = array_merge($params, $advparams);
 
-        $recordids = data_get_all_recordids($data->id);
+        // Provide initial sql statements and parameters to reduce the number of total records.
+        $selectdata = $groupselect . $approveselect;
+        $initialparams = array();
+        if ($currentgroup) {
+            $initialparams['currentgroup'] = $params['currentgroup'];
+        }
+        if (!$approvecap && $data->approval && isloggedin()) {
+            $initialparams['myid1'] = $params['myid1'];
+        }
+
+        $recordids = data_get_all_recordids($data->id, $selectdata, $initialparams);
         $newrecordids = data_get_advance_search_ids($recordids, $search_array, $data->id);
         $totalcount = count($newrecordids);
-        $selectdata = $groupselect . $approveselect;
 
         if (!empty($advanced)) {
             $advancedsearchsql = data_get_advanced_search_sql($sort, $data, $newrecordids, $selectdata, $sortorder);
index 9c3e801..7cfb638 100644 (file)
@@ -5322,7 +5322,7 @@ function forum_user_can_see_post($forum, $discussion, $post, $user=NULL, $cm=NUL
 
         return (($userfirstpost !== false && (time() - $userfirstpost >= $CFG->maxeditingtime)) ||
                 $firstpost->id == $post->id || $post->userid == $user->id || $firstpost->userid == $user->id ||
-                has_capability('mod/forum:viewqandawithoutposting', $modcontext, $user->id, false));
+                has_capability('mod/forum:viewqandawithoutposting', $modcontext, $user->id));
     }
     return true;
 }
index dd1b3af..406352a 100644 (file)
@@ -484,20 +484,79 @@ class forum_file_info_container extends file_info {
      * @return array of file_info instances
      */
     public function get_children() {
+        return $this->get_filtered_children('*', false, true);
+    }
+    /**
+     * Help function to return files matching extensions or their count
+     *
+     * @param string|array $extensions, either '*' or array of lowercase extensions, i.e. array('.gif','.jpg')
+     * @param bool|int $countonly if false returns the children, if an int returns just the
+     *    count of children but stops counting when $countonly number of children is reached
+     * @param bool $returnemptyfolders if true returns items that don't have matching files inside
+     * @return array|int array of file_info instances or the count
+     */
+    private function get_filtered_children($extensions = '*', $countonly = false, $returnemptyfolders = false) {
         global $DB;
+        $params = array('contextid' => $this->context->id,
+            'component' => $this->component,
+            'filearea' => $this->filearea);
+        $sql = 'SELECT DISTINCT itemid
+                    FROM {files}
+                    WHERE contextid = :contextid
+                    AND component = :component
+                    AND filearea = :filearea';
+        if (!$returnemptyfolders) {
+            $sql .= ' AND filename <> :emptyfilename';
+            $params['emptyfilename'] = '.';
+        }
+        list($sql2, $params2) = $this->build_search_files_sql($extensions);
+        $sql .= ' '.$sql2;
+        $params = array_merge($params, $params2);
+        if ($countonly !== false) {
+            $sql .= ' ORDER BY itemid DESC';
+        }
 
+        $rs = $DB->get_recordset_sql($sql, $params);
         $children = array();
-        $itemids = $DB->get_records('files', array('contextid' => $this->context->id, 'component' => $this->component,
-            'filearea' => $this->filearea), 'itemid DESC', "DISTINCT itemid");
-        foreach ($itemids as $itemid => $unused) {
-            if ($child = $this->browser->get_file_info($this->context, 'mod_forum', $this->filearea, $itemid)) {
+        foreach ($rs as $record) {
+            if (($child = $this->browser->get_file_info($this->context, 'mod_forum', $this->filearea, $record->itemid))
+                    && ($returnemptyfolders || $child->count_non_empty_children($extensions))) {
                 $children[] = $child;
             }
+            if ($countonly !== false && count($children) >= $countonly) {
+                break;
+            }
+        }
+        $rs->close();
+        if ($countonly !== false) {
+            return count($children);
         }
-
         return $children;
     }
 
+    /**
+     * Returns list of children which are either files matching the specified extensions
+     * or folders that contain at least one such file.
+     *
+     * @param string|array $extensions, either '*' or array of lowercase extensions, i.e. array('.gif','.jpg')
+     * @return array of file_info instances
+     */
+    public function get_non_empty_children($extensions = '*') {
+        return $this->get_filtered_children($extensions, false);
+    }
+
+    /**
+     * Returns the number of children which are either files matching the specified extensions
+     * or folders containing at least one such file.
+     *
+     * @param string|array $extensions, for example '*' or array('.gif','.jpg')
+     * @param int $limit stop counting after at least $limit non-empty children are found
+     * @return int
+     */
+    public function count_non_empty_children($extensions = '*', $limit = 1) {
+        return $this->get_filtered_children($extensions, $limit);
+    }
+
     /**
      * Returns parent file_info instance
      *
index 2a307bd..35e7197 100644 (file)
@@ -1282,7 +1282,9 @@ function glossary_print_entry_icons($course, $cm, $glossary, $entry, $mode='',$h
             }
         }
         $fs = get_file_storage();
-        if ($files = $fs->get_area_files($filecontext->id, 'mod_glossary', 'attachment', $entry->id, "timemodified", false)) {
+        if ($files = $fs->get_area_files($filecontext->id, 'mod_glossary', 'attachment', $entry->id, "timemodified", false)
+         || $files = $fs->get_area_files($filecontext->id, 'mod_glossary', 'entry', $entry->id, "timemodified", false)) {
+
             $button->set_formats(PORTFOLIO_FORMAT_RICHHTML);
         } else {
             $button->set_formats(PORTFOLIO_FORMAT_PLAINHTML);
index 2f0b448..5b04e57 100644 (file)
@@ -266,6 +266,12 @@ class glossary_entry_portfolio_caller extends portfolio_module_caller_base {
             $fs->get_area_files($context->id, 'mod_glossary', 'attachment', $this->entry->id, "timemodified", false),
             $fs->get_area_files($context->id, 'mod_glossary', 'entry', $this->entry->id, "timemodified", false)
         );
+
+        if (!empty($this->multifiles)) {
+            $this->add_format(PORTFOLIO_FORMAT_RICHHTML);
+        } else {
+            $this->add_format(PORTFOLIO_FORMAT_PLAINHTML);
+        }
     }
 
     /**
@@ -540,30 +546,86 @@ class glossary_file_info_container extends file_info {
      * @return array of file_info instances
      */
     public function get_children() {
-        global $DB;
+        return $this->get_filtered_children('*', false, true);
+    }
 
-        $sql = "SELECT DISTINCT f.itemid, ge.concept
+    /**
+     * Help function to return files matching extensions or their count
+     *
+     * @param string|array $extensions, either '*' or array of lowercase extensions, i.e. array('.gif','.jpg')
+     * @param bool|int $countonly if false returns the children, if an int returns just the
+     *    count of children but stops counting when $countonly number of children is reached
+     * @param bool $returnemptyfolders if true returns items that don't have matching files inside
+     * @return array|int array of file_info instances or the count
+     */
+    private function get_filtered_children($extensions = '*', $countonly = false, $returnemptyfolders = false) {
+        global $DB;
+        $sql = 'SELECT DISTINCT f.itemid, ge.concept
                   FROM {files} f
-                  JOIN {modules} m ON (m.name = 'glossary' AND m.visible = 1)
-                  JOIN {course_modules} cm ON (cm.module = m.id AND cm.id = ?)
+                  JOIN {modules} m ON (m.name = :modulename AND m.visible = 1)
+                  JOIN {course_modules} cm ON (cm.module = m.id AND cm.id = :instanceid)
                   JOIN {glossary} g ON g.id = cm.instance
                   JOIN {glossary_entries} ge ON (ge.glossaryid = g.id AND ge.id = f.itemid)
-                 WHERE f.contextid = ? AND f.component = ? AND f.filearea = ?
-              ORDER BY ge.concept, f.itemid";
-        $params = array($this->context->instanceid, $this->context->id, $this->component, $this->filearea);
+                 WHERE f.contextid = :contextid
+                  AND f.component = :component
+                  AND f.filearea = :filearea';
+        $params = array(
+            'modulename' => 'glossary',
+            'instanceid' => $this->context->instanceid,
+            'contextid' => $this->context->id,
+            'component' => $this->component,
+            'filearea' => $this->filearea);
+        if (!$returnemptyfolders) {
+            $sql .= ' AND f.filename <> :emptyfilename';
+            $params['emptyfilename'] = '.';
+        }
+        list($sql2, $params2) = $this->build_search_files_sql($extensions, 'f');
+        $sql .= ' '.$sql2;
+        $params = array_merge($params, $params2);
+        if ($countonly !== false) {
+            $sql .= ' ORDER BY ge.concept, f.itemid';
+        }
 
         $rs = $DB->get_recordset_sql($sql, $params);
         $children = array();
-        foreach ($rs as $file) {
-            if ($child = $this->browser->get_file_info($this->context, 'mod_glossary', $this->filearea, $file->itemid)) {
+        foreach ($rs as $record) {
+            if ($child = $this->browser->get_file_info($this->context, 'mod_glossary', $this->filearea, $record->itemid)) {
                 $children[] = $child;
             }
+            if ($countonly !== false && count($children) >= $countonly) {
+                break;
+            }
         }
         $rs->close();
-
+        if ($countonly !== false) {
+            return count($children);
+        }
         return $children;
     }
 
+    /**
+     * Returns list of children which are either files matching the specified extensions
+     * or folders that contain at least one such file.
+     *
+     * @param string|array $extensions, either '*' or array of lowercase extensions, i.e. array('.gif','.jpg')
+     * @return array of file_info instances
+     */
+    public function get_non_empty_children($extensions = '*') {
+        return $this->get_filtered_children($extensions, false);
+    }
+
+    /**
+     * Returns the number of children which are either files matching the specified extensions
+     * or folders containing at least one such file.
+     *
+     * @param string|array $extensions, for example '*' or array('.gif','.jpg')
+     * @param int $limit stop counting after at least $limit non-empty children are found
+     * @return int
+     */
+    public function count_non_empty_children($extensions = '*', $limit = 1) {
+        return $this->get_filtered_children($extensions, $limit);
+    }
+
     /**
      * Returns parent file_info instance
      *
index 6eb6ebb..04b16fb 100644 (file)
@@ -273,18 +273,79 @@ class imscp_file_info extends file_info {
      * @return array of file_info instances
      */
     public function get_children() {
+        return $this->get_filtered_children('*', false, true);
+    }
+
+    /**
+     * Help function to return files matching extensions or their count
+     *
+     * @param string|array $extensions, either '*' or array of lowercase extensions, i.e. array('.gif','.jpg')
+     * @param bool|int $countonly if false returns the children, if an int returns just the
+     *    count of children but stops counting when $countonly number of children is reached
+     * @param bool $returnemptyfolders if true returns items that don't have matching files inside
+     * @return array|int array of file_info instances or the count
+     */
+    private function get_filtered_children($extensions = '*', $countonly = false, $returnemptyfolders = false) {
         global $DB;
+        $params = array('contextid' => $this->context->id,
+            'component' => 'mod_imscp',
+            'filearea' => $this->filearea);
+        $sql = 'SELECT DISTINCT itemid
+                    FROM {files}
+                    WHERE contextid = :contextid
+                    AND component = :component
+                    AND filearea = :filearea';
+        if (!$returnemptyfolders) {
+            $sql .= ' AND filename <> :emptyfilename';
+            $params['emptyfilename'] = '.';
+        }
+        list($sql2, $params2) = $this->build_search_files_sql($extensions);
+        $sql .= ' '.$sql2;
+        $params = array_merge($params, $params2);
+        if ($countonly !== false) {
+            $sql .= ' ORDER BY itemid';
+        }
 
+        $rs = $DB->get_recordset_sql($sql, $params);
         $children = array();
-        $itemids = $DB->get_records('files', array('contextid'=>$this->context->id, 'component'=>'mod_imscp', 'filearea'=>$this->filearea), 'itemid', "DISTINCT itemid");
-        foreach ($itemids as $itemid=>$unused) {
-            if ($child = $this->browser->get_file_info($this->context, 'mod_imscp', $this->filearea, $itemid)) {
+        foreach ($rs as $record) {
+            if ($child = $this->browser->get_file_info($this->context, 'mod_imscp', $this->filearea, $record->itemid)) {
                 $children[] = $child;
+                if ($countonly !== false && count($children) >= $countonly) {
+                    break;
+                }
             }
         }
+        $rs->close();
+        if ($countonly !== false) {
+            return count($children);
+        }
         return $children;
     }
 
+    /**
+     * Returns list of children which are either files matching the specified extensions
+     * or folders that contain at least one such file.
+     *
+     * @param string|array $extensions, either '*' or array of lowercase extensions, i.e. array('.gif','.jpg')
+     * @return array of file_info instances
+     */
+    public function get_non_empty_children($extensions = '*') {
+        return $this->get_filtered_children($extensions, false);
+    }
+
+    /**
+     * Returns the number of children which are either files matching the specified extensions
+     * or folders containing at least one such file.
+     *
+     * @param string|array $extensions, for example '*' or array('.gif','.jpg')
+     * @param int $limit stop counting after at least $limit non-empty children are found
+     * @return int
+     */
+    public function count_non_empty_children($extensions = '*', $limit = 1) {
+        return $this->get_filtered_children($extensions, $limit);
+    }
+
     /**
      * Returns parent file_info instance
      * @return file_info or null for root
diff --git a/mod/lesson/importppt.php b/mod/lesson/importppt.php
deleted file mode 100644 (file)
index 84bd61b..0000000
+++ /dev/null
@@ -1,211 +0,0 @@
-<?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/>.
-
-/**
- * This is a very rough importer for powerpoint slides
- * Export a powerpoint presentation with powerpoint as html pages
- * Do it with office 2002 (I think?) and no special settings
- * Then zip the directory with all of the html pages
- * and the zip file is what you want to upload
- *
- * The script supports book and lesson.
- *
- * @package    mod
- * @subpackage lesson
- * @copyright 1999 onwards Martin Dougiamas  {@link http://moodle.com}
- * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- **/
-
-/** include required files */
-require_once("../../config.php");
-require_once($CFG->dirroot.'/mod/lesson/locallib.php');
-require_once($CFG->dirroot.'/mod/lesson/importpptlib.php');
-
-$id     = required_param('id', PARAM_INT);         // Course Module ID
-$pageid = optional_param('pageid', '', PARAM_INT); // Page ID
-
-$url = new moodle_url('/mod/lesson/importppt.php', array('id'=>$id));
-if ($pageid !== '') {
-    $url->param('pageid', $pageid);
-}
-$PAGE->set_url($url);
-
-$cm = get_coursemodule_from_id('lesson', $id, 0, false, MUST_EXIST);;
-$course = $DB->get_record('course', array('id' => $cm->course), '*', MUST_EXIST);
-$lesson = new lesson($DB->get_record('lesson', array('id' => $cm->instance), '*', MUST_EXIST));
-
-$modname = 'lesson';
-$mod = $cm;
-require_login($course, false, $cm);
-
-require_login($course, false, $cm);
-$context = get_context_instance(CONTEXT_MODULE, $cm->id);
-require_capability('mod/lesson:edit', $context);
-
-$strimportppt = get_string("importppt", "lesson");
-$strlessons = get_string("modulenameplural", "lesson");
-
-$data = new stdClass;
-$data->id = $cm->id;
-$data->pageid = $pageid;
-$mform = new lesson_importppt_form();
-$mform->set_data($data);
-
-if ($data = $mform->get_data()) {
-    $manager = lesson_page_type_manager::get($lesson);
-    if (!$filename = $mform->get_new_filename('pptzip')) {
-        print_error('invalidfile', 'lesson');
-    }
-    if (!$package = $mform->save_stored_file('pptzip', $context->id, 'mod_lesson', 'ppt_imports', $lesson->id, '/', $filename, true)) {
-        print_error('unabletosavefile', 'lesson');
-    }
-    // extract package content
-    $packer = get_file_packer('application/zip');
-    $package->extract_to_storage($packer, $context->id, 'mod_lesson', 'imported_files', $lesson->id, '/');
-
-    $fs = get_file_storage();
-    if ($files = $fs->get_area_files($context->id, 'mod_lesson', 'imported_files', $lesson->id)) {
-
-        $pages = array();
-        foreach ($files as $key=>$file) {
-            if ($file->get_mimetype() != 'text/html') {
-                continue;
-            }
-            $filenameinfo = pathinfo($file->get_filepath().$file->get_filename());
-
-            $page = new stdClass;
-            $page->title = '';
-            $page->contents = array();
-            $page->images = array();
-            $page->source = $filenameinfo['basename'];
-
-            $string = strip_tags($file->get_content(),'<div><img>');
-            $imgs = array();
-            preg_match_all("/<img[^>]*(src\=\"(".$filenameinfo['filename']."\_image[^>^\"]*)\"[^>]*)>/i", $string, $imgs);
-            foreach ($imgs[2] as $img) {
-                $imagename = basename($img);
-                foreach ($files as $file) {
-                    if ($imagename === $file->get_filename()) {
-                        $page->images[] = clone($file);
-                    }
-                }
-            }
-
-            $matches = array();
-            // this will look for a non nested tag that is closed
-            // want to allow <b><i>(maybe more) tags but when we do that
-            // the preg_match messes up.
-            preg_match_all("/(<([\w]+)[^>]*>)([^<\\2>]*)(<\/\\2>)/", $string, $matches);
-            $countmatches = count($matches[1]);
-            for($i = 0; $i < $countmatches; $i++) { // go through all of our div matches
-
-                $class = lesson_importppt_isolate_class($matches[1][$i]); // first step in isolating the class
-
-                // check for any static classes
-                switch ($class) {
-                    case 'T':  // class T is used for Titles
-                        $page->title = $matches[3][$i];
-                        break;
-                    case 'B':  // I would guess that all bullet lists would start with B then go to B1, B2, etc
-                    case 'B1': // B1-B4 are just insurance, should just hit B and all be taken care of
-                    case 'B2':
-                    case 'B3':
-                    case 'B4':
-                        $page->contents[] = lesson_importppt_build_list($matches, '<ul>', $i, 0);  // this is a recursive function that will grab all the bullets and rebuild the list in html
-                        break;
-                    default:
-                        if ($matches[3][$i] != '&#13;') {  // odd crap generated... sigh
-                            if (substr($matches[3][$i], 0, 1) == ':') {  // check for leading :    ... hate MS ...
-                                $page->contents[] = substr($matches[3][$i], 1);  // get rid of :
-                            } else {
-                                $page->contents[] = $matches[3][$i];
-                            }
-                        }
-                        break;
-                }
-            }
-            $pages[] = $page;
-        }
-
-        $branchtables = lesson_create_objects($pages, $lesson->id);
-
-        // first set up the prevpageid and nextpageid
-        if (empty($pageid)) { // adding it to the top of the lesson
-            $prevpageid = 0;
-            // get the id of the first page.  If not found, then no pages in the lesson
-            if (!$nextpageid = $DB->get_field('lesson_pages', 'id', array('prevpageid' => 0, 'lessonid' => $lesson->id))) {
-                $nextpageid = 0;
-            }
-        } else {
-            // going after an actual page
-            $prevpageid = $pageid;
-            $nextpageid = $DB->get_field('lesson_pages', 'nextpageid', array('id' => $pageid));
-        }
-
-        foreach ($branchtables as $branchtable) {
-
-            // set the doubly linked list
-            $branchtable->page->nextpageid = $nextpageid;
-            $branchtable->page->prevpageid = $prevpageid;
-
-            // insert the page
-            $id = $DB->insert_record('lesson_pages', $branchtable->page);
-
-            if (!empty($branchtable->page->images)) {
-                $changes = array('contextid'=>$context->id, 'component'=>'mod_lesson', 'filearea'=>'page_contents', 'itemid'=>$id, 'timemodified'=>time());
-                foreach ($branchtable->page->images as $image) {
-                    $fs->create_file_from_storedfile($changes, $image);
-                }
-            }
-
-            // update the link of the page previous to the one we just updated
-            if ($prevpageid != 0) {  // if not the first page
-                $DB->set_field("lesson_pages", "nextpageid", $id, array("id" => $prevpageid));
-            }
-
-            // insert the answers
-            foreach ($branchtable->answers as $answer) {
-                $answer->pageid = $id;
-                $DB->insert_record('lesson_answers', $answer);
-            }
-
-            $prevpageid = $id;
-        }
-
-        // all done with inserts.  Now check to update our last page (this is when we import between two lesson pages)
-        if ($nextpageid != 0) {  // if the next page is not the end of lesson
-            $DB->set_field("lesson_pages", "prevpageid", $id, array("id" => $nextpageid));
-        }
-    }
-
-    // Remove all unzipped files!
-    $fs->delete_area_files($context->id, 'mod_lesson', 'imported_files', $lesson->id);
-
-    redirect("$CFG->wwwroot/mod/$modname/view.php?id=$cm->id", get_string('pptsuccessfullimport', 'lesson'), 5);
-}
-
-$PAGE->navbar->add($strimportppt);
-$PAGE->set_title($strimportppt);
-$PAGE->set_heading($strimportppt);
-echo $OUTPUT->header();
-
-/// Print upload form
-echo $OUTPUT->heading_with_help($strimportppt, 'importppt', 'lesson');
-echo $OUTPUT->box_start('generalbox boxaligncenter');
-$mform->display();
-echo $OUTPUT->box_end();
-echo $OUTPUT->footer();
diff --git a/mod/lesson/importpptlib.php b/mod/lesson/importpptlib.php
deleted file mode 100644 (file)
index 8550e61..0000000
+++ /dev/null
@@ -1,236 +0,0 @@
-<?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/>.
-
-/**
- * Contains functions used by importppt.php that naturally pertain to importing
- * powerpoint presentations into the lesson module
- *
- * @package    mod
- * @subpackage lesson
- * @copyright  2009 Sam Hemelryk
- * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- **/
-
-defined('MOODLE_INTERNAL') || die();
-
-/**
- * A recursive function to build a html list
- *
- * @param array $matches
- * @param string $list
- * @param int $i
- * @param int $depth
- * @return string
- */
-function lesson_importppt_build_list(array &$matches, $list, &$i, $depth) {
-    while($i < count($matches[1])) {
-
-        $class = lesson_importppt_isolate_class($matches[1][$i]);
-
-        if (strstr($class, 'B')) {  // make sure we are still working with bullet classes
-            if ($class == 'B') {
-                $this_depth = 0;  // calling class B depth 0
-            } else {
-                // set the depth number.  So B1 is depth 1 and B2 is depth 2 and so on
-                $this_depth = substr($class, 1);
-                if (!is_numeric($this_depth)) {
-                    print_error('invalidnum');
-                }
-            }
-            if ($this_depth < $depth) {
-                // we are moving back a level in the nesting
-                break;
-            }
-            if ($this_depth > $depth) {
-                // we are moving in a lvl in nesting
-                $list .= '<ul>';
-                $list = lesson_importppt_build_list($matches, $list, $i, $this_depth);
-                // once we return back, should go to the start of the while
-                continue;
-            }
-            // no depth changes, so add the match to our list
-            if ($cleanstring = lesson_importppt_clean_text($matches[3][$i])) {
-                $list .= '<li>'.lesson_importppt_clean_text($matches[3][$i]).'</li>';
-            }
-            $i++;
-        } else {
-            // not a B class, so get out of here...
-            break;
-        }
-    }
-    // end the list and return it
-    $list .= '</ul>';
-    return $list;
-
-}
-
-/**
- * Given an html tag, this function will
- *
- * @param string $string
- * @return string
- */
-function lesson_importppt_isolate_class($string) {
-    if($class = strstr($string, 'class=')) { // first step in isolating the class
-        $class = substr($class, strpos($class, '=')+1);  // this gets rid of <div blawblaw class=  there are no "" or '' around the class name   ...sigh...
-        if (strstr($class, ' ')) {
-            // spaces found, so cut off everything off after the first space
-            return substr($class, 0, strpos($class, ' '));
-        } else {
-            // no spaces so nothing else in the div tag, cut off the >
-            return substr($class, 0, strpos($class, '>'));
-        }
-    } else {
-        // no class defined in the tag
-        return '';
-    }
-}
-
-/**
- * This function strips off the random chars that ppt puts infront of bullet lists
- *
- * @param string $string
- * @return string
- */
-function lesson_importppt_clean_text($string) {
-    $chop = 1; // default: just a single char infront of the content
-
-    // look for any other crazy things that may be infront of the content
-    if (strstr($string, '&lt;') and strpos($string, '&lt;') == 0) {  // look for the &lt; in the sting and make sure it is in the front
-        $chop = 4;  // increase the $chop
-    }
-    // may need to add more later....
-
-    $string = substr($string, $chop);
-
-    if ($string != '&#13;') {
-        return $string;
-    } else {
-        return false;
-    }
-}
-
-/**
- *  Creates objects an object with the page and answers that are to be inserted into the database
- *
- * @param array $pageobjects
- * @param int $lessonid
- * @return array
- */
-function lesson_create_objects($pageobjects, $lessonid) {
-
-    $branchtables = array();
-    $branchtable = new stdClass;
-
-    // all pages have this info
-    $page = new stdClass();
-    $page->lessonid = $lessonid;
-    $page->prevpageid = 0;
-    $page->nextpageid = 0;
-    $page->qtype = LESSON_PAGE_BRANCHTABLE;
-    $page->qoption = 0;
-    $page->layout = 1;
-    $page->display = 1;
-    $page->timecreated = time();
-    $page->timemodified = 0;
-
-    // all answers are the same
-    $answer = new stdClass();
-    $answer->lessonid = $lessonid;
-    $answer->jumpto = LESSON_NEXTPAGE;
-    $answer->grade = 0;
-    $answer->score = 0;
-    $answer->flags = 0;
-    $answer->timecreated = time();
-    $answer->timemodified = 0;
-    $answer->answer = "Next";
-    $answer->response = "";
-
-    $answers[] = clone($answer);
-
-    $answer->jumpto = LESSON_PREVIOUSPAGE;
-    $answer->answer = "Previous";
-
-    $answers[] = clone($answer);
-
-    $branchtable->answers = $answers;
-
-    $i = 1;
-
-    foreach ($pageobjects as $pageobject) {
-        if ($pageobject->title == '') {
-            $page->title = "Page $i";  // no title set so make a generic one
-        } else {
-            $page->title = $pageobject->title;
-        }
-        $page->contents = '';
-
-        // nab all the images first
-        $page->images = $pageobject->images;
-        foreach ($page->images as $image) {
-            $imagetag = '<img src="@@PLUGINFILE@@'.$image->get_filepath().$image->get_filename().'" title="'.$image->get_filename().'" />';
-            $imagetag = str_replace("\n", '', $imagetag);
-            $imagetag = str_replace("\r", '', $imagetag);
-            $imagetag = str_replace("'", '"', $imagetag);  // imgstyle
-            $page->contents .= $imagetag;
-        }
-        // go through the contents array and put <p> tags around each element and strip out \n which I have found to be unneccessary
-        foreach ($pageobject->contents as $content) {
-            $content = str_replace("\n", '', $content);
-            $content = str_replace("\r", '', $content);
-            $content = str_replace('&#13;', '', $content);  // puts in returns?
-            $content = '<p>'.$content.'</p>';
-            $page->contents .= $content;
-        }
-
-        $branchtable->page = clone($page);  // add the page
-        $branchtables[] = clone($branchtable);  // add it all to our array
-        $i++;
-    }
-
-    return $branchtables;
-}
-
-/**
- * Form displayed to the user asking them to select a file to upload
- *
- * @copyright  2009 Sam Hemelryk
- * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-class lesson_importppt_form extends moodleform {
-
-    public function definition() {
-        global $COURSE;
-
-        $mform = $this->_form;
-
-        $mform->addElement('hidden', 'id');
-        $mform->setType('id', PARAM_INT);
-
-        $mform->addElement('hidden', 'pageid');
-        $mform->setType('pageid', PARAM_INT);
-
-        $filepickeroptions = array();
-        $filepickeroptions['filetypes'] = array('*.zip');
-        $filepickeroptions['maxbytes'] = $COURSE->maxbytes;
-        $mform->addElement('filepicker', 'pptzip', get_string('upload'), null, $filepickeroptions);
-        $mform->addRule('pptzip', null, 'required', null, 'client');
-
-        $this->add_action_buttons(null, get_string("uploadthisfile"));
-    }
-
-}
\ No newline at end of file
index 765b7ca..a37f40b 100644 (file)
@@ -364,9 +364,6 @@ class mod_lesson_renderer extends plugin_renderer_base {
         $importquestionsurl = new moodle_url('/mod/lesson/import.php',array('id'=>$this->page->cm->id, 'pageid'=>$prevpageid));
         $links[] = html_writer::link($importquestionsurl, get_string('importquestions', 'lesson'));
 
-        $importppturl = new moodle_url('/mod/lesson/importppt.php',array('id'=>$this->page->cm->id, 'pageid'=>$prevpageid));
-        $links[] = html_writer::link($importppturl, get_string('importppt', 'lesson'));
-
         $manager = lesson_page_type_manager::get($lesson);
         foreach ($manager->get_add_page_type_links($prevpageid) as $link) {
             $link['addurl']->param('firstpage', 1);
index e663af6..d75f2aa 100644 (file)
@@ -25,7 +25,7 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$module->version   = 2012061700;       // The current module version (Date: YYYYMMDDXX)
+$module->version   = 2012061701;       // The current module version (Date: YYYYMMDDXX)
 $module->requires  = 2012061700;    // Requires this Moodle version
 $module->component = 'mod_lesson'; // Full name of the plugin (used for diagnostics)
 $module->cron      = 0;
index 7913b63..9e4e6a4 100644 (file)
@@ -117,8 +117,8 @@ class mod_quiz_overdue_attempt_updater {
            FROM {quiz_attempts} iquiza
            JOIN {quiz} quiz ON quiz.id = iquiza.quiz
       LEFT JOIN {quiz_overrides} quo ON quo.quiz = quiz.id AND quo.userid = iquiza.userid
-      LEFT JOIN {quiz_overrides} qgo ON qgo.quiz = quiz.id
-      LEFT JOIN {groups_members} gm ON gm.userid = iquiza.userid AND gm.groupid = qgo.groupid
+      LEFT JOIN {groups_members} gm ON gm.userid = iquiza.userid
+      LEFT JOIN {quiz_overrides} qgo ON qgo.quiz = quiz.id AND qgo.groupid = gm.groupid
 
           WHERE iquiza.state IN ('inprogress', 'overdue')
             AND iquiza.timemodified >= :processfrom
index f535e9b..f55f590 100644 (file)
@@ -987,7 +987,7 @@ function quiz_get_flag_option($attempt, $context) {
  *      IMMEDIATELY_AFTER, LATER_WHILE_OPEN or AFTER_CLOSE constants.
  */
 function quiz_attempt_state($quiz, $attempt) {
-    if ($attempt->state != quiz_attempt::FINISHED) {
+    if ($attempt->state == quiz_attempt::IN_PROGRESS) {
         return mod_quiz_display_options::DURING;
     } else if (time() < $attempt->timefinish + 120) {
         return mod_quiz_display_options::IMMEDIATELY_AFTER;
@@ -1456,13 +1456,14 @@ function quiz_get_js_module() {
         'name' => 'mod_quiz',
         'fullpath' => '/mod/quiz/module.js',
         'requires' => array('base', 'dom', 'event-delegate', 'event-key',
-                'core_question_engine'),
+                'core_question_engine', 'moodle-core-formchangechecker'),
         'strings' => array(
             array('cancel', 'moodle'),
             array('flagged', 'question'),
             array('functiondisabledbysecuremode', 'quiz'),
             array('startattempt', 'quiz'),
             array('timesup', 'quiz'),
+            array('changesmadereallygoaway', 'moodle'),
         ),
     );
 }
index 89fa16a..ce0d359 100644 (file)
@@ -27,6 +27,7 @@ M.mod_quiz = M.mod_quiz || {};
 M.mod_quiz.init_attempt_form = function(Y) {
     M.core_question_engine.init_form(Y, '#responseform');
     Y.on('submit', M.mod_quiz.timer.stop, '#responseform');
+    M.core_formchangechecker.init({formid: 'responseform'});
 };
 
 M.mod_quiz.init_review_form = function(Y) {
index d07442b..322ccdd 100644 (file)
@@ -11,6 +11,8 @@
 
 #page-mod-quiz-attempt .submitbtns,
 #page-mod-quiz-review .submitbtns {clear: left; text-align: left; padding-top: 1.5em;}
+#page-mod-quiz-attempt.dir-rtl .submitbtns,
+#page-mod-quiz-review.dir-rtl .submitbtns {text-align: right;}
 
 body.jsenabled .questionflagcheckbox {display: none;}
 
@@ -27,6 +29,7 @@ body.jsenabled .questionflagcheckbox {display: none;}
 .path-mod-quiz #user-picture img {width: auto;height: auto;float: left;}
 
 .path-mod-quiz .qnbutton {display: block; position: relative; float: left; width: 1.5em; height: 1.5em; overflow: hidden; margin: 0.3em 0.3em 0.3em 0; padding: 0; border: 1px solid #bbb; background: #ddd; text-align: center; vertical-align: middle;line-height: 1.5em !important; font-weight: bold; text-decoration: none;}
+.path-mod-quiz.dir-rtl  .qnbutton {float: right;}
 
 .path-mod-quiz .qnbutton .trafficlight,
 .path-mod-quiz .qnbutton .thispageholder {display: block; position: absolute; top: 0; bottom: 0; left: 0; right: 0;}
@@ -113,6 +116,7 @@ table.quizattemptsummary .noreviewmessage {color: gray;}
 #page-mod-quiz-view .generalbox#feedback {width:70%;margin-left:auto;margin-right:auto;padding-bottom:15px;}
 #page-mod-quiz-view .generalbox#feedback h2 {margin: 0;}
 #page-mod-quiz-view .generalbox#feedback h3 {text-align: left;}
+#page-mod-quiz-view.dir-rtl .generalbox#feedback h3 {text-align: center;}
 #page-mod-quiz-view .generalbox#feedback .overriddennotice {text-align: center;font-size: 0.7em;}
 .quizstartbuttondiv.quizsecuremoderequired input { display: none; }
 .jsenabled .quizstartbuttondiv.quizsecuremoderequired input { display: inline; }
@@ -135,6 +139,7 @@ table.quizattemptsummary .noreviewmessage {color: gray;}
 table.quizreviewsummary {width: 100%;}
 table.quizreviewsummary th.cell {padding: 1px 0.5em 1px 1em;font-weight: bold;text-align: right;width: 10em;background: #f0f0f0;}
 table.quizreviewsummary td.cell {padding: 1px 1em 1px 0.5em;text-align: left;background: #fafafa;}
+.dir-rtl table.quizreviewsummary td.cell {text-align: right;}
 
 /** Mod quiz make comment or override grade popup. **/
 #page-mod-quiz-comment .mform {width: 100%;}
@@ -247,6 +252,7 @@ table#categoryquestions {width: 100%;overflow: hidden;table-layout: fixed;}
 #page-mod-quiz-edit div.question div.content .singlequestion .questiontext{display:inline-block;}
 #page-mod-quiz-edit div.question div.content .singlequestion .questionpreview{background-color:#eee;}
 #page-mod-quiz-edit div.question div.content .questiontype{display:block;clear:left;float:left;}
+#page-mod-quiz-edit.dir-rtl div.question div.content .questiontype {clear: right;float: right;}
 #page-mod-quiz-edit div.question div.content .questionpreview {display:block;float:left;margin-left:0.3em;padding-left:0.2em;padding-right:0.2em;}
 #page-mod-quiz-edit div.question div.content .questionpreview a{background-color:#eee;}
 #page-mod-quiz-edit div.question div.content div.quiz_randomquestion .questionpreview{display:inline;float:none;}
@@ -395,7 +401,7 @@ bank window's title is prominent enough*/
 #page-mod-quiz-edit.dir-rtl div.question {clear: right;}
 #page-mod-quiz-edit.dir-rtl div.question div.qnum {float: right;}
 #page-mod-quiz-edit.dir-rtl div.editq div.question div.content {float: right;height: 40px;}
-#page-mod-quiz-edit.dir-rtl div.question div.content div.points {left: 50px;right:auto;}
+#page-mod-quiz-edit.dir-rtl div.question div.content div.points {left: 5px;right:auto;}
 #page-mod-quiz-edit.dir-rtl div.question div.content div.questioncontrols {float: left;left: 0.3em; right:auto;}
 #page-mod-quiz-edit.dir-rtl .editq div.question div.content .singlequestion .questioneditbutton .questionname,
 #page-mod-quiz-edit.dir-rtl .editq div.question div.content .singlequestion .questioneditbutton .questiontext {float: right; padding-right: 0.3em;}
index d058b3c..177947c 100644 (file)
@@ -151,4 +151,92 @@ class mod_quiz_locallib_testcase extends basic_testcase {
         $this->assertEquals(1, quiz_get_slot_for_question($quiz, 1));
         $this->assertEquals(3, quiz_get_slot_for_question($quiz, 7));
     }
+
+    public function test_quiz_attempt_state_in_progress() {
+        $attempt = new stdClass();
+        $attempt->state = quiz_attempt::IN_PROGRESS;
+        $attempt->timefinish = 0;
+
+        $quiz = new stdClass();
+        $quiz->timeclose = 0;
+
+        $this->assertEquals(mod_quiz_display_options::DURING, quiz_attempt_state($quiz, $attempt));
+    }
+
+    public function test_quiz_attempt_state_recently_submitted() {
+        $attempt = new stdClass();
+        $attempt->state = quiz_attempt::FINISHED;
+        $attempt->timefinish = time() - 10;
+
+        $quiz = new stdClass();
+        $quiz->timeclose = 0;
+
+        $this->assertEquals(mod_quiz_display_options::IMMEDIATELY_AFTER, quiz_attempt_state($quiz, $attempt));
+    }
+
+    public function test_quiz_attempt_state_sumitted_quiz_never_closes() {
+        $attempt = new stdClass();
+        $attempt->state = quiz_attempt::FINISHED;
+        $attempt->timefinish = time() - 7200;
+
+        $quiz = new stdClass();
+        $quiz->timeclose = 0;
+
+        $this->assertEquals(mod_quiz_display_options::LATER_WHILE_OPEN, quiz_attempt_state($quiz, $attempt));
+    }
+
+    public function test_quiz_attempt_state_sumitted_quiz_closes_later() {
+        $attempt = new stdClass();
+        $attempt->state = quiz_attempt::FINISHED;
+        $attempt->timefinish = time() - 7200;
+
+        $quiz = new stdClass();
+        $quiz->timeclose = time() + 3600;
+
+        $this->assertEquals(mod_quiz_display_options::LATER_WHILE_OPEN, quiz_attempt_state($quiz, $attempt));
+    }
+
+    public function test_quiz_attempt_state_sumitted_quiz_closed() {
+        $attempt = new stdClass();
+        $attempt->state = quiz_attempt::FINISHED;
+        $attempt->timefinish = time() - 7200;
+
+        $quiz = new stdClass();
+        $quiz->timeclose = time() - 3600;
+
+        $this->assertEquals(mod_quiz_display_options::AFTER_CLOSE, quiz_attempt_state($quiz, $attempt));
+    }
+
+    public function test_quiz_attempt_state_never_sumitted_quiz_never_closes() {
+        $attempt = new stdClass();
+        $attempt->state = quiz_attempt::ABANDONED;
+        $attempt->timefinish = 1000; // A very long time ago!
+
+        $quiz = new stdClass();
+        $quiz->timeclose = 0;
+
+        $this->assertEquals(mod_quiz_display_options::LATER_WHILE_OPEN, quiz_attempt_state($quiz, $attempt));
+    }
+
+    public function test_quiz_attempt_state_never_sumitted_quiz_closes_later() {
+        $attempt = new stdClass();
+        $attempt->state = quiz_attempt::ABANDONED;
+        $attempt->timefinish = time() - 7200;
+
+        $quiz = new stdClass();
+        $quiz->timeclose = time() + 3600;
+
+        $this->assertEquals(mod_quiz_display_options::LATER_WHILE_OPEN, quiz_attempt_state($quiz, $attempt));
+    }
+
+    public function test_quiz_attempt_state_never_sumitted_quiz_closed() {
+        $attempt = new stdClass();
+        $attempt->state = quiz_attempt::ABANDONED;
+        $attempt->timefinish = time() - 7200;
+
+        $quiz = new stdClass();
+        $quiz->timeclose = time() - 3600;
+
+        $this->assertEquals(mod_quiz_display_options::AFTER_CLOSE, quiz_attempt_state($quiz, $attempt));
+    }
 }
index b500784..cac3069 100644 (file)
@@ -512,8 +512,8 @@ function resource_dndupload_handle($uploadinfo) {
     $data->popupwidth = $config->popupwidth;
     $data->printheading = $config->printheading;
     $data->printintro = $config->printintro;
-    $data->showsize = $config->showsize;
-    $data->showtype = $config->showtype;
+    $data->showsize = (isset($config->showsize)) ? $config->showsize : 0;
+    $data->showtype = (isset($config->showtype)) ? $config->showtype : 0;
     $data->filterfiles = $config->filterfiles;
 
     return resource_add_instance($data, null);
index c587b68..27b4722 100644 (file)
@@ -150,7 +150,7 @@ function scorm_add_instance($scorm, $mform=null) {
 
     scorm_parse($record, true);
 
-    scorm_grade_item_update($record, null, false);
+    scorm_grade_item_update($record);
 
     return $record->id;
 }
@@ -591,18 +591,21 @@ function scorm_get_user_grades($scorm, $userid=0) {
  * @param bool $nullifnone
  */
 function scorm_update_grades($scorm, $userid=0, $nullifnone=true) {
-    global $CFG, $DB;
+    global $CFG;
     require_once($CFG->libdir.'/gradelib.php');
+    require_once($CFG->libdir.'/completionlib.php');
 
     if ($grades = scorm_get_user_grades($scorm, $userid)) {
         scorm_grade_item_update($scorm, $grades);
-
+        //set complete
+        scorm_set_completion($scorm, $userid, COMPLETION_COMPLETE, $grades);
     } else if ($userid and $nullifnone) {
         $grade = new stdClass();
         $grade->userid   = $userid;
         $grade->rawgrade = null;
         scorm_grade_item_update($scorm, $grade);
-
+        //set incomplete.
+        scorm_set_completion($scorm, $userid, COMPLETION_INCOMPLETE);
     } else {
         scorm_grade_item_update($scorm);
     }
@@ -646,10 +649,9 @@ function scorm_upgrade_grades() {
  * @uses GRADE_TYPE_NONE
  * @param object $scorm object with extra cmidnumber
  * @param mixed $grades optional array/object of grade(s); 'reset' means reset grades in gradebook
- * @param boolean $updatecompletion  set whether to update completion stuff
  * @return object grade_item
  */
-function scorm_grade_item_update($scorm, $grades=null, $updatecompletion=true) {
+function scorm_grade_item_update($scorm, $grades=null) {
     global $CFG, $DB;
     require_once($CFG->dirroot.'/mod/scorm/locallib.php');
     if (!function_exists('grade_update')) { //workaround for buggy PHP versions
@@ -680,19 +682,6 @@ function scorm_grade_item_update($scorm, $grades=null, $updatecompletion=true) {
         $grades = null;
     }
 
-    // Update activity completion if applicable
-    if ($updatecompletion) {
-        // Get course info
-        $course = new stdClass();
-        $course->id = $scorm->course;
-
-        $cm = get_coursemodule_from_instance('scorm', $scorm->id, $course->id);
-        if (!empty($cm)) {
-            $completion = new completion_info($course);
-            $completion->update_state($cm, COMPLETION_COMPLETE);
-        }
-    }
-
     return grade_update('mod/scorm', $scorm->course, 'mod', 'scorm', $scorm->id, 0, $grades, $params);
 }
 
@@ -1329,3 +1318,32 @@ function scorm_dndupload_handle($uploadinfo) {
 
     return scorm_add_instance($scorm, null);
 }
+
+/**
+ * Sets activity completion state
+ *
+ * @param object $scorm object
+ * @param int $userid User ID
+ * @param int $completionstate Completion state
+ * @param array $grades grades array of users with grades - used when $userid = 0
+ */
+function scorm_set_completion($scorm, $userid, $completionstate = COMPLETION_COMPLETE, $grades = array()) {
+    if (!completion_info::is_enabled()) {
+        return;
+    }
+
+    $course = new stdClass();
+    $course->id = $scorm->course;
+
+    $cm = get_coursemodule_from_instance('scorm', $scorm->id, $scorm->course);
+    if (!empty($cm)) {
+        $completion = new completion_info($course);
+        if (empty($userid)) { //we need to get all the relevant users from $grades param.
+            foreach ($grades as $grade) {
+                $completion->update_state($cm, $completionstate, $grade->userid);
+            }
+        } else {
+            $completion->update_state($cm, $completionstate, $userid);
+        }
+    }
+}
index 72411c8..6eac247 100644 (file)
@@ -21,6 +21,9 @@ optional - no changes needed:
   xxx_dndupload_register() and xxx_dndupload_handle($uploadinfo) see:
   http://docs.moodle.org/dev/Implementing_Course_drag_and_drop_upload_support_in_a_module
 
+* mod/data/lib.php data_get_all_recordids() now has two new optional variables:  $selectdata and $params.
+
+
 === 2.2 ===
 
 required changes in code:
@@ -29,12 +32,20 @@ required changes in code:
 * textlib->asort() replaced by specialized collatorlib::asort()
 * use new make_temp_directory() and make_cache_directory()
 
+optional - no changes needed:
+
+* mod/data/lib.php data_get_all_recordids() now has two new optional variables:  $selectdata and $params.
+
 
 === 2.1 ===
 
 required changes in code:
 * add new support for basic restore from 1.9
 
+optional - no changes needed:
+
+* mod/data/lib.php data_get_all_recordids() now has two new optional variables:  $selectdata and $params.
+
 
 === 2.0 ===
 
index 5a8b4a2..0427819 100644 (file)
@@ -87,24 +87,90 @@ class workshop_file_info_submissions_container extends file_info {
         return true;
     }
 
+
     /**
-     * Returns list of children.
+     * Returns list of children nodes
+     *
      * @return array of file_info instances
      */
     public function get_children() {
+        return $this->get_filtered_children('*', false, true);
+    }
+
+    /**
+     * Helper function to return files matching extensions or their count
+     *
+     * @param string|array $extensions, either '*' or array of lowercase extensions, i.e. array('.gif','.jpg')
+     * @param bool|int $countonly if false returns the children, if an int returns just the
+     *    count of children but stops counting when $countonly number of children is reached
+     * @param bool $returnemptyfolders if true returns items that don't have matching files inside
+     * @return array|int array of file_info instances or the count
+     */
+    private function get_filtered_children($extensions = '*', $countonly = false, $returnemptyfolders = false) {
         global $DB;
+        $params = array('contextid' => $this->context->id,
+            'component' => 'mod_workshop',
+            'filearea' => $this->filearea);
+        $sql = 'SELECT DISTINCT itemid
+                    FROM {files}
+                    WHERE contextid = :contextid
+                    AND component = :component
+                    AND filearea = :filearea';
+        if (!$returnemptyfolders) {
+            $sql .= ' AND filename <> :emptyfilename';
+            $params['emptyfilename'] = '.';
+        }
+        list($sql2, $params2) = $this->build_search_files_sql($extensions);
+        $sql .= ' '.$sql2;
+        $params = array_merge($params, $params2);
+        if ($countonly !== false) {
+            $sql .= ' ORDER BY itemid DESC';
+        }
 
+        $rs = $DB->get_recordset_sql($sql, $params);
         $children = array();
-        $itemids = $DB->get_records('files', array('contextid' => $this->context->id, 'component' => 'mod_workshop', 'filearea' => $this->filearea),
-            'itemid', "DISTINCT itemid");
-        foreach ($itemids as $itemid => $unused) {
-            if ($child = $this->browser->get_file_info($this->context, 'mod_workshop', $this->filearea, $itemid)) {
+        foreach ($rs as $record) {
+            if (($child = $this->browser->get_file_info($this->context, 'mod_workshop', $this->filearea, $record->itemid))
+                    && ($returnemptyfolders || $child->count_non_empty_children($extensions))) {
                 $children[] = $child;
+                if ($countonly !== false && count($children) >= $countonly) {
+                    break;
+                }
             }
         }
+        $rs->close();
+        if ($countonly !== false) {
+            return count($children);
+        }
         return $children;
     }
 
+    /**
+     * Returns list of children which are either files matching the specified extensions
+     * or folders that contain at least one such file.
+     *
+     * @param string|array $extensions, either '*' or array of lowercase extensions, i.e. array('.gif','.jpg')
+     * @return array of file_info instances
+     */
+    public function get_non_empty_children($extensions = '*') {
+        return $this->get_filtered_children($extensions, false);
+    }
+
+    /**
+     * Returns the number of children which are either files matching the specified extensions
+     * or folders containing at least one such file.
+     *
+     * NOTE: We don't need the exact number of non empty children if it is >=2
+     * In this function 1 is never returned to avoid skipping the single subfolder
+     *
+     * @param string|array $extensions, for example '*' or array('.gif','.jpg')
+     * @param int $limit stop counting after at least $limit non-empty children are found
+     * @return int
+     */
+    public function count_non_empty_children($extensions = '*', $limit = 1) {
+        return $this->get_filtered_children($extensions, $limit);
+    }
+
     /**
      * Returns parent file_info instance
      * @return file_info or null for root
diff --git a/pix/f/epub.png b/pix/f/epub.png
new file mode 100644 (file)
index 0000000..a660c73
Binary files /dev/null and b/pix/f/epub.png differ
index adf9363..e04a638 100644 (file)
@@ -86,24 +86,30 @@ abstract class qbehaviour_renderer extends plugin_renderer_base {
 
         $commenteditor = html_writer::tag('div', html_writer::tag('textarea', s($commenttext),
                 array('id' => $id, 'name' => $inputname, 'rows' => 10, 'cols' => 60)));
+        $commenteditor .= html_writer::end_tag('div');
 
-        $commenteditor .= html_writer::start_tag('div');
-        if (count($formats == 1)) {
+        $editorformat = '';
+        if (count($formats) == 1) {
             reset($formats);
-            $commenteditor .= html_writer::empty_tag('input', array('type' => 'hidden',
+            $editorformat .= html_writer::empty_tag('input', array('type' => 'hidden',
                     'name' => $inputname . 'format', 'value' => key($formats)));
-
         } else {
-            $commenteditor .= html_writer::select(
-                    $formats, $inputname . 'format', $commentformat, '');
+            $editorformat = html_writer::start_tag('div', array('class' => 'fitem'));
+            $editorformat .= html_writer::start_tag('div', array('class' => 'fitemtitle'));
+            $editorformat .= html_writer::tag('label', get_string('format'), array('for'=>'menu'.$inputname.'format'));
+            $editorformat .= html_writer::end_tag('div');
+            $editorformat .= html_writer::start_tag('div', array('class' => 'felement fhtmleditor'));
+            $editorformat .= html_writer::select($formats, $inputname.'format', $commentformat, '');
+            $editorformat .= html_writer::end_tag('div');
+            $editorformat .= html_writer::end_tag('div');
         }
-        $commenteditor .= html_writer::end_tag('div');
 
         $comment = html_writer::tag('div', html_writer::tag('div',
                 html_writer::tag('label', get_string('comment', 'question'),
                 array('for' => $id)), array('class' => 'fitemtitle')) .
                 html_writer::tag('div', $commenteditor, array('class' => 'felement fhtmleditor')),
                 array('class' => 'fitem'));
+        $comment .= $editorformat;
 
         $mark = '';
         if ($qa->get_max_mark()) {
@@ -117,6 +123,7 @@ abstract class qbehaviour_renderer extends plugin_renderer_base {
                 'type' => 'text',
                 'size' => $fieldsize,
                 'name' => $markfield,
+                'id'=> $markfield
             );
             if (!is_null($currentmark)) {
                 $attributes['value'] = $qa->format_fraction_as_mark(
index 6fbccbf..86d58f3 100644 (file)
@@ -117,6 +117,7 @@ class question_engine_data_mapper {
         $record->responsesummary = $qa->get_response_summary();
         $record->timemodified = time();
         $record->id = $this->db->insert_record('question_attempts', $record);
+        $qa->set_database_id($record->id);
 
         foreach ($qa->get_step_iterator() as $seq => $step) {
             $this->insert_question_attempt_step($step, $record->id, $seq, $context);
index 4cd813a..20a34da 100644 (file)
@@ -386,7 +386,7 @@ class question_attempt_step {
         $record = $currentrec;
         $data = array();
         while ($currentrec && $currentrec->attemptstepid == $attemptstepid) {
-            if ($currentrec->name) {
+            if (!is_null($currentrec->name)) {
                 $data[$currentrec->name] = $currentrec->value;
             }
             $records->next();
index 0b70c45..7c929d6 100644 (file)
@@ -95,12 +95,14 @@ class qformat_xhtml extends qformat_default {
             break;
         case SHORTANSWER:
             $expout .= "<ul class=\"shortanswer\">\n";
-            $expout .= "  <li><input name=\"quest_$id\" type=\"text\" /></li>\n";
+            $expout .= "  <li>" . html_writer::label(get_string('answer'), 'quest_'.$id, false, array('class' => 'accesshide'));
+            $expout .= "    <input id=\"quest_$id\" name=\"quest_$id\" type=\"text\" /></li>\n";
             $expout .= "</ul>\n";
             break;
         case NUMERICAL:
             $expout .= "<ul class=\"numerical\">\n";
-            $expout .= "  <li><input name=\"quest_$id\" type=\"text\" /></li>\n";
+            $expout .= "  <li>" . html_writer::label(get_string('answer'), 'quest_'.$id, false, array('class' => 'accesshide'));
+            $expout .= "    <input id=\"quest_$id\" name=\"quest_$id\" type=\"text\" /></li>\n";
             $expout .= "</ul>\n";
             break;
         case MATCH:
@@ -113,18 +115,22 @@ class qformat_xhtml extends qformat_default {
             }
             shuffle( $ans_list ); // random display order
 
-            // build drop down for answers
-            $dropdown = "<select name=\"quest_$id\">\n";
+            // Build select options.
+            $selectoptions = '';
             foreach($ans_list as $ans) {
-                $dropdown .= "<option value=\"" . s($ans) . "\">" . s($ans) . "</option>\n";
+                $selectoptions .= "<option value=\"" . s($ans) . "\">" . s($ans) . "</option>\n";
             }
-            $dropdown .= "</select>\n";
 
-            // finally display
+            // display
+            $option = 0;
             foreach($question->options->subquestions as $subquestion) {
-              $quest_text = $this->repchar( $subquestion->questiontext );
-              $expout .= "  <li>$quest_text</li>\n";
-              $expout .= $dropdown;
+                // build drop down for answers
+                $quest_text = $this->repchar( $subquestion->questiontext );
+                $dropdown = html_writer::label(get_string('answer', 'qtype_match', $option+1), 'quest_'.$id.'_'.$option, false, array('class' => 'accesshide'));
+                $dropdown .= "<select id=\"quest_{$id}_{$option}\" name=\"quest_{$id}_{$option}\">\n".$selectoptions."</select>\n";
+                $expout .= "  <li>$quest_text</li>\n";
+                $expout .= $dropdown;
+                $option++;
             }
             $expout .= "</ul>\n";
             break;
index 36e2d1d..a27d9be 100644 (file)
@@ -49,6 +49,7 @@ $string['datasetrole']= ' The wild cards <strong>{x..}</strong> will be substitu
 $string['decimals'] = 'with {$a}';
 $string['deleteitem'] = 'Delete item';
 $string['deletelastitem'] = 'Delete last item';
+$string['distributionoption'] = 'Select distribution option';
 $string['editdatasets'] = 'Edit the wildcards datasets';
 $string['editdatasets_help'] = 'Wildcard values may be created by entering a number in each wild card field then clicking the add button. To automatically generate 10 or more values, select the number of values required before clicking the add button. A uniform distribution means any value between the limits is equally likely to be generated; a loguniform distribution means that values towards the lower limit are more likely.';
 $string['editdatasets_link'] = 'question/type/calculated';
@@ -79,6 +80,7 @@ $string['keptlocal1'] = 'will use the same existing private dataset as before';
 $string['keptlocal2'] = 'a file from the same question private set of files as before';
 $string['keptlocal3'] = 'a link from the same question private set of links as before';
 $string['lastitem(s)'] = 'last items(s)';
+$string['lengthoption'] = 'Select length option';
 $string['loguniform'] = 'Loguniform';
 $string['loguniformbit'] = 'digits, from a loguniform distribution';
 $string['makecopynextpage'] = 'Next page (new question)';
index a4655a4..c0ab753 100644 (file)
@@ -751,11 +751,13 @@ class qtype_calculated extends question_type {
                     ? 'decimals'
                     : 'significantfigures'), 'qtype_calculated', $i);
             }
-            $menu1 = html_writer::select($lengthoptions, 'calclength[]', $regs[4], null);
+            $menu1 = html_writer::label(get_string('lengthoption', 'qtype_calculated'), 'menucalclength', false, array('class' => 'accesshide'));
+            $menu1 .= html_writer::select($lengthoptions, 'calclength[]', $regs[4], null);
 
             $options = array('uniform' => get_string('uniformbit', 'qtype_calculated'),
                     'loguniform' => get_string('loguniformbit', 'qtype_calculated'));
-            $menu2 = html_writer::select($options, 'calcdistribution[]', $regs[1], null);
+            $menu2 = html_writer::label(get_string('distributionoption', 'qtype_calculated'), 'menucalcdistribution', false, array('class' => 'accesshide'));
+            $menu2 .= html_writer::select($options, 'calcdistribution[]', $regs[1], null);
             return '<input type="submit" onclick="'
                 . "getElementById('addform').regenerateddefid.value='$defid'; return true;"
                 .'" value="'. get_string('generatevalue', 'qtype_calculated') . '"/><br/>'
index ac2bac9..1905e2e 100644 (file)
@@ -219,12 +219,14 @@ class qtype_essay_format_editor_renderer extends plugin_renderer_base {
                 array('id' => $id, 'name' => $inputname, 'rows' => $lines, 'cols' => 60)));
 
         $output .= html_writer::start_tag('div');
-        if (count($formats == 1)) {
+        if (count($formats) == 1) {
             reset($formats);
             $output .= html_writer::empty_tag('input', array('type' => 'hidden',
                     'name' => $inputname . 'format', 'value' => key($formats)));
 
         } else {
+            $output .= html_writer::label(get_string('format'), 'menu' . $inputname . 'format', false);
+            $output .= ' ';
             $output .= html_writer::select($formats, $inputname . 'format', $responseformat, '');
         }
         $output .= html_writer::end_tag('div');
index a096607..5dd3c4a 100644 (file)
@@ -24,6 +24,7 @@
  */
 
 $string['addmoreqblanks'] = '{no} More Sets of Blanks';
+$string['answer'] = 'Answer {$a}';
 $string['availablechoices'] = 'Available choices';
 $string['correctansweris'] = 'The correct answer is: {$a}';
 $string['filloutthreeqsandtwoas'] = 'You must provide at least two questions and three answers. You can provide extra wrong answers by giving an answer with a blank question. Entries where both the question and the answer are blank will be ignored.';
index 8a3372c..2cadb7e 100644 (file)
@@ -53,6 +53,7 @@ class qtype_match_renderer extends qtype_with_combined_feedback_renderer {
         $result .= html_writer::start_tag('tbody');
 
         $parity = 0;
+        $i = 1;
         foreach ($stemorder as $key => $stemid) {
 
             $result .= html_writer::start_tag('tr', array('class' => 'r' . $parity));
@@ -80,12 +81,14 @@ class qtype_match_renderer extends qtype_with_combined_feedback_renderer {
             }
 
             $result .= html_writer::tag('td',
+                    html_writer::label(get_string('answer', 'qtype_match', $i), 'menu' . $qa->get_qt_field_name('sub' . $key), false, array('class' => 'accesshide')) .
                     html_writer::select($choices, $qa->get_qt_field_name('sub' . $key), $selected,
                             array('0' => 'choose'), array('disabled' => $options->readonly)) .
                     ' ' . $feedbackimage, array('class' => $classes));
 
             $result .= html_writer::end_tag('tr');
             $parity = 1 - $parity;
+            $i++;
         }
         $result .= html_writer::end_tag('tbody');
         $result .= html_writer::end_tag('table');
index beb3788..2fbe4e0 100644 (file)
@@ -27,7 +27,7 @@ M.qtype_multianswer = M.qtype_multianswer || {};
 
 
 M.qtype_multianswer.init = function (Y, questiondiv) {
-    Y.one(questiondiv).all('label.subq').each(function(subqspan, i) {
+    Y.one(questiondiv).all('span.subquestion').each(function(subqspan, i) {
         var feedbackspan = subqspan.one('.feedbackspan');
         if (!feedbackspan) {
             return;
index 547a370..5afe4ff 100644 (file)
@@ -177,13 +177,18 @@ class qtype_multianswer_textfield_renderer extends qtype_multianswer_subq_render
         if ($subq->qtype->name() == 'shortanswer') {
             $matchinganswer = $subq->get_matching_answer(array('answer' => $response));
         } else if ($subq->qtype->name() == 'numerical') {
-            $matchinganswer = $subq->get_matching_answer($response, 1);
+            list($value, $unit, $multiplier) = $subq->ap->apply_units($response, '');
+            $matchinganswer = $subq->get_matching_answer($value, 1);
         } else {
             $matchinganswer = $subq->get_matching_answer($response);
         }
 
         if (!$matchinganswer) {
-            $matchinganswer = new question_answer(0, '', null, '', FORMAT_HTML);
+            if (is_null($response) || $response === '') {
+                $matchinganswer = new question_answer(0, '', null, '', FORMAT_HTML);
+            } else {
+                $matchinganswer = new question_answer(0, '', 0.0, '', FORMAT_HTML);
+            }
         }
 
         // Work out a good input field size.
@@ -222,12 +227,13 @@ class qtype_multianswer_textfield_renderer extends qtype_multianswer_subq_render
                         $qa, 'question', 'answerfeedback', $matchinganswer->id),
                 s($correctanswer->answer), $options);
 
-        $output = '';
-        $output .= html_writer::start_tag('label', array('class' => 'subq'));
+        $output = html_writer::start_tag('span', array('class' => 'subquestion'));
+        $output .= html_writer::tag('label', get_string('answer'),
+                array('class' => 'subq accesshide', 'for' => $inputattributes['id']));
         $output .= html_writer::empty_tag('input', $inputattributes);
         $output .= $feedbackimg;
         $output .= $feedbackpopup;
-        $output .= html_writer::end_tag('label');
+        $output .= html_writer::end_tag('span');
 
         return $output;
     }
@@ -274,25 +280,28 @@ class qtype_multianswer_multichoice_inline_renderer
             $inputattributes['class'] = $this->feedback_class($matchinganswer->fraction);
             $feedbackimg = $this->feedback_image($matchinganswer->fraction);
         }
-
         $select = html_writer::select($choices, $qa->get_qt_field_name($fieldname),
                 $response, array('' => ''), $inputattributes);
 
         $order = $subq->get_order($qa);
         $correctresponses = $subq->get_correct_response();
         $rightanswer = $subq->answers[$order[reset($correctresponses)]];
+        if (!$matchinganswer) {
+            $matchinganswer = new question_answer(0, '', null, '', FORMAT_HTML);
+        }
         $feedbackpopup = $this->feedback_popup($subq, $matchinganswer->fraction,
                 $subq->format_text($matchinganswer->feedback, $matchinganswer->feedbackformat,
                         $qa, 'question', 'answerfeedback', $matchinganswer->id),
                 $subq->format_text($rightanswer->answer, $rightanswer->answerformat,
                         $qa, 'question', 'answer', $rightanswer->id), $options);
 
-        $output = '';
-        $output .= html_writer::start_tag('label', array('class' => 'subq'));
+        $output = html_writer::start_tag('span', array('class' => 'subquestion'));
+        $output .= html_writer::tag('label', get_string('answer'),
+                array('class' => 'subq accesshide', 'for' => $inputattributes['id']));
         $output .= $select;
         $output .= $feedbackimg;
         $output .= $feedbackpopup;
-        $output .= html_writer::end_tag('label');
+        $output .= html_writer::end_tag('span');
 
         return $output;
     }
@@ -366,16 +375,30 @@ class qtype_multianswer_multichoice_vertical_renderer extends qtype_multianswer_
 
         $result .= $this->all_choices_wrapper_end();
 
+        $feedback = array();
         if ($options->feedback && $options->marks >= question_display_options::MARK_AND_MAX &&
                 $subq->maxmark > 0) {
             $a = new stdClass();
             $a->mark = format_float($fraction * $subq->maxmark, $options->markdp);
             $a->max =  format_float($subq->maxmark, $options->markdp);
 
-            $result .= html_writer::tag('div', get_string('markoutofmax', 'question', $a),
-                    array('class' => 'outcome'));
+            $feedback[] = html_writer::tag('div', get_string('markoutofmax', 'question', $a));
         }
 
+        if ($options->rightanswer) {
+            foreach ($subq->answers as $ans) {
+                if (question_state::graded_state_for_fraction($ans->fraction) ==
+                        question_state::$gradedright) {
+                    $feedback[] = get_string('correctansweris', 'qtype_multichoice',
+                            $subq->format_text($ans->answer, $ans->answerformat,
+                                    $qa, 'question', 'answer', $ansid));
+                    break;
+                }
+            }
+        }
+
+        $result .= html_writer::nonempty_tag('div', implode('<br />', $feedback), array('class' => 'outcome'));
+
         return $result;
     }
 
index 75a3d7d..3a77001 100644 (file)
@@ -38,7 +38,7 @@ require_once($CFG->dirroot . '/question/type/multianswer/question.php');
  */
 class qtype_multianswer_test_helper extends question_test_helper {
     public function get_test_questions() {
-        return array('twosubq', 'fourmc');
+        return array('twosubq', 'fourmc', 'numericalzero');
     }
 
     /**
@@ -287,4 +287,48 @@ class qtype_multianswer_test_helper extends question_test_helper {
 
         return $q;
     }
+
+    /**
+     * Makes a multianswer question with one numerical subquestion, right answer 0.
+     * This is used for testing the MDL-35370 bug.
+     * @return qtype_multianswer_question
+     */
+    public function make_multianswer_question_numericalzero() {
+        question_bank::load_question_definition_classes('multianswer');
+        $q = new qtype_multianswer_question();
+        test_question_maker::initialise_a_question($q);
+        $q->name = 'Numerical zero';
+        $q->questiontext =
+                'Enter zero: {#1}.';
+        $q->generalfeedback = '';
+        $q->qtype = question_bank::get_qtype('multianswer');
+
+        $q->textfragments = array(
+            'Enter zero: ',
+            '.',
+        );
+        $q->places = array('1' => '1');
+
+        // Numerical subquestion.
+        question_bank::load_question_definition_classes('numerical');
+        $sub = new qtype_numerical_question();
+        test_question_maker::initialise_a_question($sub);
+        $sub->name = 'Numerical zero';
+        $sub->questiontext = '{1:NUMERICAL:=0:0}';
+        $sub->questiontextformat = FORMAT_HTML;
+        $sub->generalfeedback = '';
+        $sub->generalfeedbackformat = FORMAT_HTML;
+        $sub->answers = array(
+            13 => new qtype_numerical_answer(13, '0', 1.0, '', FORMAT_HTML, 0),
+        );
+        $sub->qtype = question_bank::get_qtype('numerical');
+        $sub->ap = new qtype_numerical_answer_processor(array());
+        $sub->maxmark = 1;
+
+        $q->subquestions = array(
+            1 => $sub,
+        );
+
+        return $q;
+    }
 }
index 19af3c7..9c9d291 100644 (file)
@@ -37,9 +37,15 @@ require_once($CFG->dirroot . '/question/engine/tests/helpers.php');
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 class qtype_multianswer_walkthrough_test extends qbehaviour_walkthrough_test_base {
+
+    protected function get_contains_subq_status(question_state $state) {
+        return new question_pattern_expectation('~' .
+                preg_quote($state->default_string(true), '~') . '<br />~');
+    }
+
     public function test_deferred_feedback() {
 
-        // Create a gapselect question.
+        // Create a multianswer question.
         $q = test_question_maker::make_question('multianswer', 'fourmc');
         $this->start_attempt_at_question($q, 'deferredfeedback', 4);
 
@@ -86,4 +92,110 @@ class qtype_multianswer_walkthrough_test extends qbehaviour_walkthrough_test_bas
                 $this->get_contains_partcorrect_expectation(),
                 $this->get_does_not_contain_validation_error_expectation());
     }
+
+    public function test_deferred_feedback_numericalzero_not_answered() {
+        // Tests the situation found in MDL-35370.
+
+        // Create a multianswer question with one numerical subquestion, right answer zero.
+        $q = test_question_maker::make_question('multianswer', 'numericalzero');
+        $this->start_attempt_at_question($q, 'deferredfeedback', 1);
+
+        // Check the initial state.
+        $this->check_current_state(question_state::$todo);
+        $this->check_current_mark(null);
+        $this->check_current_output(
+                $this->get_contains_marked_out_of_summary(),
+                $this->get_does_not_contain_feedback_expectation(),
+                $this->get_does_not_contain_validation_error_expectation());
+
+        // Now submit all and finish.
+        $this->process_submission(array('-finish' => 1));
+
+        // Verify.
+        $this->check_current_state(question_state::$gaveup);
+        $this->check_current_mark(null);
+        $this->check_current_output(
+                $this->get_contains_marked_out_of_summary(),
+                new question_pattern_expectation('~<input[^>]* class="incorrect" [^>]*/>~'),
+                $this->get_contains_subq_status(question_state::$gaveup),
+                $this->get_does_not_contain_validation_error_expectation());
+    }
+
+    public function test_deferred_feedback_numericalzero_0_answer() {
+        // Tests the situation found in MDL-35370.
+
+        // Create a multianswer question with one numerical subquestion, right answer zero.
+        $q = test_question_maker::make_question('multianswer', 'numericalzero');
+        $this->start_attempt_at_question($q, 'deferredfeedback', 1);
+
+        // Check the initial state.
+        $this->check_current_state(question_state::$todo);
+        $this->check_current_mark(null);
+        $this->check_current_output(
+                $this->get_contains_marked_out_of_summary(),
+                $this->get_does_not_contain_feedback_expectation(),
+                $this->get_does_not_contain_validation_error_expectation());
+
+        // Save a the correct answer.
+        $this->process_submission(array('sub1_answer' => '0'));
+
+        // Verify.
+        $this->check_current_state(question_state::$complete);
+        $this->check_current_mark(null);
+        $this->check_current_output(
+                $this->get_contains_marked_out_of_summary(),
+                $this->get_does_not_contain_feedback_expectation(),
+                $this->get_does_not_contain_validation_error_expectation());
+
+        // Now submit all and finish.
+        $this->process_submission(array('-finish' => 1));
+
+        // Verify.
+        $this->check_current_state(question_state::$gradedright);
+        $this->check_current_mark(1);
+        $this->check_current_output(
+                $this->get_contains_mark_summary(1),
+                $this->get_contains_correct_expectation(),
+                $this->get_contains_subq_status(question_state::$gradedright),
+                $this->get_does_not_contain_validation_error_expectation());
+    }
+
+    public function test_deferred_feedback_numericalzero_0_wrong() {
+        // Tests the situation found in MDL-35370.
+
+        // Create a multianswer question with one numerical subquestion, right answer zero.
+        $q = test_question_maker::make_question('multianswer', 'numericalzero');
+        $this->start_attempt_at_question($q, 'deferredfeedback', 1);
+
+        // Check the initial state.
+        $this->check_current_state(question_state::$todo);
+        $this->check_current_mark(null);
+        $this->check_current_output(
+                $this->get_contains_marked_out_of_summary(),
+                $this->get_does_not_contain_feedback_expectation(),
+                $this->get_does_not_contain_validation_error_expectation());
+
+        // Save a the correct answer.
+        $this->process_submission(array('sub1_answer' => '42'));
+
+        // Verify.
+        $this->check_current_state(question_state::$complete);
+        $this->check_current_mark(null);
+        $this->check_current_output(
+                $this->get_contains_marked_out_of_summary(),
+                $this->get_does_not_contain_feedback_expectation(),
+                $this->get_does_not_contain_validation_error_expectation());
+
+        // Now submit all and finish.
+        $this->process_submission(array('-finish' => 1));
+
+        // Verify.
+        $this->check_current_state(question_state::$gradedwrong);
+        $this->check_current_mark(0);
+        $this->check_current_output(
+                $this->get_contains_mark_summary(0),
+                $this->get_contains_incorrect_expectation(),
+                $this->get_contains_subq_status(question_state::$gradedwrong),
+                $this->get_does_not_contain_validation_error_expectation());
+    }
 }
index 75dc457..fb4784b 100644 (file)
@@ -26,6 +26,7 @@
 $string['acceptederror'] = 'Accepted error';
 $string['addmoreanswerblanks'] = 'Blanks for {no} More Answers';
 $string['addmoreunitblanks'] = 'Blanks for {no} More Units';
+$string['answercolon'] = 'Answer:';
 $string['answermustbenumberorstar'] = 'The answer must be a number, for example -1.234 or 3e8, or \'*\'.';
 $string['answerno'] = 'Answer {$a}';
 $string['decfractionofquestiongrade'] = 'as a fraction (0-1) of the question grade';
index 110cfdc..0e48155 100644 (file)
@@ -179,6 +179,10 @@ class qtype_numerical_question extends question_graded_automatically {
      * @return question_answer the matching answer.
      */
     public function get_matching_answer($value, $multiplier) {
+        if (is_null($value) || $value === '') {
+            return null;
+        }
+
         if (!is_null($multiplier)) {
             $scaledvalue = $value * $multiplier;
         } else {
@@ -193,6 +197,7 @@ class qtype_numerical_question extends question_graded_automatically {
                 return $answer;
             }
         }
+
         return null;
     }
 
@@ -273,18 +278,17 @@ class qtype_numerical_question extends question_graded_automatically {
     public function check_file_access($qa, $options, $component, $filearea, $args,
             $forcedownload) {
         if ($component == 'question' && $filearea == 'answerfeedback') {
-            $question = $qa->get_question();
             $currentanswer = $qa->get_last_qt_var('answer');
             if ($this->has_separate_unit_field()) {
                 $selectedunit = $qa->get_last_qt_var('unit');
             } else {
                 $selectedunit = null;
             }
-            list($value, $unit, $multiplier) = $question->ap->apply_units(
+            list($value, $unit, $multiplier) = $this->ap->apply_units(
                     $currentanswer, $selectedunit);
-            $answer = $question->get_matching_answer($value, $multiplier);
+            $answer = $this->get_matching_answer($value, $multiplier);
             $answerid = reset($args); // itemid is answer id.
-            return $options->feedback && $answerid == $answer->id;
+            return $options->feedback && $answer && $answerid == $answer->id;
 
         } else if ($component == 'question' && $filearea == 'hint') {
             return $this->check_hint_file_access($qa, $options, $args);
index 7689a2c..d1b7c0a 100644 (file)
@@ -98,7 +98,9 @@ class qtype_numerical_renderer extends qtype_renderer {
                         array('class' => 'unitchoices'));
 
             } else if ($question->unitdisplay == qtype_numerical::UNITSELECT) {
-                $unitchoice = html_writer::select($question->ap->get_unit_options(),
+                $unitchoice = html_writer::label(get_string('selectunit', 'qtype_numerical'),
+                        'menu' . $qa->get_qt_field_name('unit'), false, array('class' => 'accesshide'));
+                $unitchoice .= html_writer::select($question->ap->get_unit_options(),
                         $qa->get_qt_field_name('unit'), $selectedunit, array(''=>'choosedots'),
                         array('disabled' => $options->readonly));
             }
@@ -111,7 +113,10 @@ class qtype_numerical_renderer extends qtype_renderer {
         }
 
         if ($placeholder) {
-            $questiontext = substr_replace($questiontext, $input,
+            $inputinplace = html_writer::tag('label', get_string('answer'),
+                    array('for' => $inputattributes['id'], 'class' => 'accesshide'));
+            $inputinplace .= $input;
+            $questiontext = substr_replace($questiontext, $inputinplace,
                     strpos($questiontext, $placeholder), strlen($placeholder));
         }
 
@@ -119,8 +124,8 @@ class qtype_numerical_renderer extends qtype_renderer {
 
         if (!$placeholder) {
             $result .= html_writer::start_tag('div', array('class' => 'ablock'));
-            $result .= get_string('answer', 'qtype_shortanswer',
-                    html_writer::tag('div', $input, array('class' => 'answer')));
+            $result .= html_writer::tag('label', get_string('answercolon', 'qtype_numerical'), array('for' => $inputattributes['id']));
+            $result .= html_writer::tag('span', $input, array('class' => 'answer'));
             $result .= html_writer::end_tag('div');
         }
 
index d4ba6f8..84fc7d0 100644 (file)
@@ -181,6 +181,11 @@ class qtype_numerical_question_test extends advanced_testcase {
         $this->assertEquals('3.1', $num->summarise_response(array('answer' => '3.1')));
     }
 
+    public function test_summarise_response_zero() {
+        $num = test_question_maker::make_question('numerical');
+        $this->assertEquals('0', $num->summarise_response(array('answer' => '0')));
+    }
+
     public function test_summarise_response_unit() {
         $num = test_question_maker::make_question('numerical', 'unit');
         $this->assertEquals('3.1', $num->summarise_response(array('answer' => '3.1')));
index a875c8c..72f0f4a 100644 (file)
@@ -78,6 +78,10 @@ class qtype_shortanswer_question extends question_graded_by_strategy
     }
 
     public function compare_response_with_answer(array $response, question_answer $answer) {
+        if (!array_key_exists('answer', $response) || is_null($response['answer'])) {
+            return false;
+        }
+
         return self::compare_string_with_wildcard(
                 $response['answer'], $answer->answer, !$this->usecase);
     }
@@ -129,7 +133,7 @@ class qtype_shortanswer_question extends question_graded_by_strategy
             $currentanswer = $qa->get_last_qt_var('answer');
             $answer = $qa->get_question()->get_matching_answer(array('answer' => $currentanswer));
             $answerid = reset($args); // itemid is answer id.
-            return $options->feedback && $answerid == $answer->id;
+            return $options->feedback && $answer && $answerid == $answer->id;
 
         } else if ($component == 'question' && $filearea == 'hint') {
             return $this->check_hint_file_access($qa, $options, $args);
index 31212ce..2cf77d7 100644 (file)
@@ -117,7 +117,7 @@ class qtype_shortanswer extends question_type {
 
         $this->save_hints($question);
 
-        // Perform sanity checks on fractional grades
+        // Perform sanity checks on fractional grades.
         if ($maxfraction != 1) {
             $result->noticeyesno = get_string('fractionsnomax', 'question', $maxfraction * 100);
             return $result;
index e7879bf..cfdca17 100644 (file)
@@ -71,11 +71,13 @@ class qtype_shortanswer_renderer extends qtype_renderer {
             $placeholder = $matches[0];
             $inputattributes['size'] = round(strlen($placeholder) * 1.1);
         }
-
         $input = html_writer::empty_tag('input', $inputattributes) . $feedbackimg;
 
         if ($placeholder) {
-            $questiontext = substr_replace($questiontext, $input,
+            $inputinplace = html_writer::tag('label', get_string('answer'),
+                    array('for' => $inputattributes['id'], 'class' => 'accesshide'));
+            $inputinplace .= $input;
+            $questiontext = substr_replace($questiontext, $inputinplace,
                     strpos($questiontext, $placeholder), strlen($placeholder));
         }
 
@@ -83,8 +85,9 @@ class qtype_shortanswer_renderer extends qtype_renderer {
 
         if (!$placeholder) {
             $result .= html_writer::start_tag('div', array('class' => 'ablock'));
-            $result .= get_string('answer', 'qtype_shortanswer',
-                    html_writer::tag('div', $input, array('class' => 'answer')));
+            $result .= html_writer::tag('label', get_string('answer', 'qtype_shortanswer',
+                    html_writer::tag('span', $input, array('class' => 'answer'))),
+                    array('for' => $inputattributes['id']));
             $result .= html_writer::end_tag('div');
         }
 
index 9654045..5e848cf 100644 (file)
@@ -16,6 +16,7 @@
  * this.viewmode, store current view mode
  * this.pathbar, reference to the Node with path bar
  * this.pathnode, a Node element representing one folder in a path bar (not attached anywhere, just used for template)
+ * this.currentpath, the current path in the repository (or last requested path)
  *
  * Filepicker options:
  * =====
@@ -39,7 +40,7 @@
  * this.filelist, cached filelist
  * this.pages
  * this.page
- * this.filepath, current path
+ * this.filepath, current path (each element of the array is a part of the breadcrumb)
  * this.logindata, cached login form
  */
 
@@ -932,6 +933,7 @@ M.core_filepicker.init = function(Y, options) {
                         // save current path and filelist (in case we want to jump to other viewmode)
                         this.filepath = e.node.origpath;
                         this.filelist = e.node.origlist;
+                        this.currentpath = e.node.path;
                         this.print_path();
                         this.content_scrolled();
                     }
@@ -967,7 +969,6 @@ M.core_filepicker.init = function(Y, options) {
                         if (this.active_repo.dynload) {
                             this.list({'path':node.path});
                         } else {
-                            this.filepath = node.path;
                             this.filelist = node.children;
                             this.view_files();
                         }
@@ -1002,7 +1003,6 @@ M.core_filepicker.init = function(Y, options) {
                         if (this.active_repo.dynload) {
                             this.list({'path':node.path});
                         } else {
-                            this.filepath = node.path;
                             this.filelist = node.children;
                             this.view_files();
                         }
@@ -1022,9 +1022,13 @@ M.core_filepicker.init = function(Y, options) {
             }
             this.active_repo.nextpagerequested = true;
             var nextpage = this.active_repo.page+1;
-            var args = {page:nextpage, repo_id:this.active_repo.id, path:this.active_repo.path};
+            var args = {
+                page: nextpage,
+                repo_id: this.active_repo.id,
+            };
             var action = this.active_repo.issearchresult ? 'search' : 'list';
             this.request({
+                path: this.currentpath,
                 scope: this,
                 action: action,
                 client_id: this.options.client_id,
@@ -1032,10 +1036,20 @@ M.core_filepicker.init = function(Y, options) {
                 params: args,
                 callback: function(id, obj, args) {
                     var scope = args.scope;
-                    // check that we are still in the same repository and are expecting this page
+                    // Check that we are still in the same repository and are expecting this page. We have no way
+                    // to compare the requested page and the one returned, so we assume that if the last chunk
+                    // of the breadcrumb is similar, then we probably are on the same page.
+                    var samepage = true;
+                    if (obj.path && scope.filepath) {
+                        var pathbefore = scope.filepath[scope.filepath.length-1];
+                        var pathafter = obj.path[obj.path.length-1];
+                        if (pathbefore.path != pathafter.path) {
+                            samepage = false;
+                        }
+                    }
                     if (scope.active_repo.hasmorepages && obj.list && obj.page &&
                             obj.repo_id == scope.active_repo.id &&
-                            obj.page == scope.active_repo.page+1 && obj.path == scope.path) {
+                            obj.page == scope.active_repo.page+1 && samepage) {
                         scope.parse_repository_options(obj, true);
                         scope.view_files(obj.list)
                     }
@@ -1564,6 +1578,10 @@ M.core_filepicker.init = function(Y, options) {
             if (!args.repo_id) {
                 args.repo_id = this.active_repo.id;
             }
+            if (!args.path) {
+                args.path = '';
+            }
+            this.currentpath = args.path;
             this.request({
                 action: 'list',
                 client_id: this.options.client_id,
@@ -1707,7 +1725,7 @@ M.core_filepicker.init = function(Y, options) {
             toolbar.one('.fp-tb-refresh').one('a,button').on('click', function(e) {
                 e.preventDefault();
                 if (!this.active_repo.norefresh) {
-                    this.list();
+                    this.list({ path: this.currentpath });
                 }
             }, this);
             toolbar.one('.fp-tb-search form').
index b416284..f60bed4 100644 (file)
@@ -2018,59 +2018,56 @@ abstract class repository {
     public function get_listing($path = '', $page = '') {
     }
 
+
     /**
-     * Prepares list of files before passing it to AJAX, makes sure data is in the correct
-     * format and stores formatted values.
+     * Prepare the breadcrumb.
      *
-     * @param array|stdClass $listing result of get_listing() or search() or file_get_drafarea_files()
-     * @return array
+     * @param array $breadcrumb contains each element of the breadcrumb.
+     * @return array of breadcrumb elements.
+     * @since 2.3.3
      */
-    public static function prepare_listing($listing) {
+    protected static function prepare_breadcrumb($breadcrumb) {
         global $OUTPUT;
-
-        $defaultfoldericon = $OUTPUT->pix_url(file_folder_icon(24))->out(false);
-        // prepare $listing['path'] or $listing->path
-        if (is_array($listing) && isset($listing['path']) && is_array($listing['path'])) {
-            $path = &$listing['path'];
-        } else if (is_object($listing) && isset($listing->path) && is_array($listing->path)) {
-            $path = &$listing->path;
-        }
-        if (isset($path)) {
-            $len = count($path);
-            for ($i=0; $i<$len; $i++) {
-                if (is_array($path[$i]) && !isset($path[$i]['icon'])) {
-                    $path[$i]['icon'] = $defaultfoldericon;
-                } else if (is_object($path[$i]) && !isset($path[$i]->icon)) {
-                    $path[$i]->icon = $defaultfoldericon;
-                }
+        $foldericon = $OUTPUT->pix_url(file_folder_icon(24))->out(false);
+        $len = count($breadcrumb);
+        for ($i = 0; $i < $len; $i++) {
+            if (is_array($breadcrumb[$i]) && !isset($breadcrumb[$i]['icon'])) {
+                $breadcrumb[$i]['icon'] = $foldericon;
+            } else if (is_object($breadcrumb[$i]) && !isset($breadcrumb[$i]->icon)) {
+                $breadcrumb[$i]->icon = $foldericon;
             }
         }
+        return $breadcrumb;
+    }
 
-        // prepare $listing['list'] or $listing->list
-        if (is_array($listing) && isset($listing['list']) && is_array($listing['list'])) {
-            $listing['list'] = array_values($listing['list']); // convert to array
-            $files = &$listing['list'];
-        } else if (is_object($listing) && isset($listing->list) && is_array($listing->list)) {
-            $listing->list = array_values($listing->list); // convert to array
-            $files = &$listing->list;
-        } else {
-            return $listing;
-        }
-        $len = count($files);
-        for ($i=0; $i<$len; $i++) {
-            if (is_object($files[$i])) {
-                $file = (array)$files[$i];
+    /**
+     * Prepare the file/folder listing.
+     *
+     * @param array $list of files and folders.
+     * @return array of files and folders.
+     * @since 2.3.3
+     */
+    protected static function prepare_list($list) {
+        global $OUTPUT;
+        $foldericon = $OUTPUT->pix_url(file_folder_icon(24))->out(false);
+
+        // Reset the array keys because non-numeric keys will create an object when converted to JSON.
+        $list = array_values($list);
+
+        $len = count($list);
+        for ($i = 0; $i < $len; $i++) {
+            if (is_object($list[$i])) {
+                $file = (array)$list[$i];
                 $converttoobject = true;
             } else {
-                $file = & $files[$i];
+                $file =& $list[$i];
                 $converttoobject = false;
             }
             if (isset($file['size'])) {
                 $file['size'] = (int)$file['size'];
                 $file['size_f'] = display_size($file['size']);
             }
-            if (isset($file['license']) &&
-                    get_string_manager()->string_exists($file['license'], 'license')) {
+            if (isset($file['license']) && get_string_manager()->string_exists($file['license'], 'license')) {
                 $file['license_f'] = get_string($file['license'], 'license');
             }
             if (isset($file['image_width']) && isset($file['image_height'])) {
@@ -2105,15 +2102,53 @@ abstract class repository {
             }
             if (!isset($file['icon'])) {
                 if ($isfolder) {
-                    $file['icon'] = $defaultfoldericon;
+                    $file['icon'] = $foldericon;
                 } else if ($filename) {
                     $file['icon'] = $OUTPUT->pix_url(file_extension_icon($filename, 24))->out(false);
                 }
             }
+
+            // Recursively loop over children.
+            if (isset($file['children'])) {
+                $file['children'] = self::prepare_list($file['children']);
+            }
+
+            // Convert the array back to an object.
             if ($converttoobject) {
-                $files[$i] = (object)$file;
+                $list[$i] = (object)$file;
             }
         }
+        return $list;
+    }
+
+    /**
+     * Prepares list of files before passing it to AJAX, makes sure data is in the correct
+     * format and stores formatted values.
+     *
+     * @param array|stdClass $listing result of get_listing() or search() or file_get_drafarea_files()
+     * @return array
+     */
+    public static function prepare_listing($listing) {
+        $wasobject = false;
+        if (is_object($listing)) {
+            $listing = (array) $listing;
+            $wasobject = true;
+        }
+
+        // Prepare the breadcrumb, passed as 'path'.
+        if (isset($listing['path']) && is_array($listing['path'])) {
+            $listing['path'] = self::prepare_breadcrumb($listing['path']);
+        }
+
+        // Prepare the listing of objects.
+        if (isset($listing['list']) && is_array($listing['list'])) {
+            $listing['list'] = self::prepare_list($listing['list']);
+        }
+
+        // Convert back to an object.
+        if ($wasobject) {
+            $listing = (object) $listing;
+        }
         return $listing;
     }
 
index d8898bf..b41100c 100644 (file)
@@ -29,22 +29,16 @@ require_once($CFG->dirroot . '/repository/lib.php');
  *
  * @since 2.0
  * @package    repository_local
+ * @copyright  2012 Marina Glancy
  * @copyright  2009 Dongsheng Cai {@link http://dongsheng.org}
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 class repository_local extends repository {
-    /**
-     * local plugin doesn't require login, so list all files
-     * @return mixed
-     */
-    public function print_login() {
-        return $this->get_listing();
-    }
-
     /**
      * Get file listing
      *
      * @param string $encodedpath
+     * @param string $page no paging is used in repository_local
      * @return mixed
      */
     public function get_listing($encodedpath = '', $page = '') {
@@ -53,11 +47,17 @@ class repository_local extends repository {
         $ret['dynload'] = true;
         $ret['nosearch'] = true;
         $ret['nologin'] = true;
-        $list = array();
+        $ret['list'] = array();
+
+        $itemid   = null;
+        $filename = null;
+        $filearea = null;
+        $filepath = null;
+        $component = null;
 
         if (!empty($encodedpath)) {
             $params = unserialize(base64_decode($encodedpath));
-            if (is_array($params)) {
+            if (is_array($params) && isset($params['contextid'])) {
                 $component = is_null($params['component']) ? NULL : clean_param($params['component'], PARAM_COMPONENT);
                 $filearea  = is_null($params['filearea']) ? NULL : clean_param($params['filearea'], PARAM_AREA);
                 $itemid    = is_null($params['itemid']) ? NULL : clean_param($params['itemid'], PARAM_INT);
@@ -65,52 +65,51 @@ class repository_local extends repository {
                 $filename  = is_null($params['filename']) ? NULL : clean_param($params['filename'], PARAM_FILE);
                 $context = get_context_instance_by_id(clean_param($params['contextid'], PARAM_INT));
             }
+        }
+        if (empty($context) && !empty($this->context)) {
+            $context = $this->context->get_course_context(false);
+        }
+        if (empty($context)) {
+            $context = context_system::instance();
+        }
+
+        // prepare list of allowed extensions: $extensions is either string '*'
+        // or array of lowercase extensions, i.e. array('.gif','.jpg')
+        $extensions = optional_param_array('accepted_types', '', PARAM_RAW);
+        if (empty($extensions) || $extensions === '*' || (is_array($extensions) && in_array('*', $extensions))) {
+            $extensions = '*';
         } else {
-            $itemid   = null;
-            $filename = null;
-            $filearea = null;
-            $filepath = null;
-            $component = null;
-            if (!empty($this->context)) {
-                list($context, $course, $cm) = get_context_info_array($this->context->id);
-                if (is_object($course)) {
-                    $context = get_context_instance(CONTEXT_COURSE, $course->id);
-                } else {
-                    $context = get_system_context();
-                }
-            } else {
-                $context = get_system_context();
+            if (!is_array($extensions)) {
+                $extensions = array($extensions);
             }
+            $extensions = array_map('textlib::strtolower', $extensions);
         }
 
+        // build file tree
         $browser = get_file_browser();
-
-        $list = array();
-        if ($fileinfo = $browser->get_file_info($context, $component, $filearea, $itemid, $filepath, $filename)) {
-            // build file tree
-            $element = repository_local_file::retrieve_file_info($fileinfo, $this);
-            $nonemptychildren = $element->get_non_empty_children();
-            foreach ($nonemptychildren as $child) {
-                $list[] = (array)$child->get_node();
-            }
-        } else {
+        if (!($fileinfo = $browser->get_file_info($context, $component, $filearea, $itemid, $filepath, $filename))) {
             // if file doesn't exist, build path nodes root of current context
             $fileinfo = $browser->get_file_info($context, null, null, null, null, null);
         }
+        $ret['list'] = $this->get_non_empty_children($fileinfo, $extensions);
+
         // build path navigation
+        $path = array();
+        for ($level = $fileinfo; $level; $level = $level->get_parent()) {
+            array_unshift($path, $level);
+        }
+        array_unshift($path, null);
         $ret['path'] = array();
-        $element = repository_local_file::retrieve_file_info($fileinfo, $this);
-        for ($level = $element; $level; $level = $level->get_parent()) {
-            if ($level == $element || !$level->can_skip()) {
-                array_unshift($ret['path'], $level->get_node_path());
+        for ($i=1; $i<count($path); $i++) {
+            if ($path[$i] == $fileinfo || !$this->can_skip($path[$i], $extensions, $path[$i-1])) {
+                $ret['path'][] = $this->get_node_path($path[$i]);
             }
         }
-        $ret['list'] = array_filter($list, array($this, 'filter'));
         return $ret;
     }
 
     /**
-     * Local file don't support to link to external links
+     * Tells how the file can be picked from this repository
      *
      * @return int
      */
@@ -137,111 +136,112 @@ class repository_local extends repository {
         // this should be realtime
         return 0;
     }
-}
-
-/**
- * Class to cache some information about file
- *
- * This class is a wrapper to instances of file_info. It caches such information as
- * parent and list of children. It also stores an array of already retrieved elements.
- *
- * It also implements more comprehensive algorithm for checking if folder is empty
- * (taking into account the filtering of the files). To decrease number of levels
- * we check if some subfolders can be skipped from the tree.
- *
- * As a result we display in Server files repository only non-empty folders and skip
- * filearea folders if this is the only filearea in the module.
- * For non-admin the course categories are not shown as well (courses are shown as a list)
- *
- * @package    repository_local
- * @copyright  2012 Marina Glancy
- * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-class repository_local_file {
-    /** @var array stores already retrieved files */
-    private static $cachedfiles = array();
-    /** @var file_info Stores the original file */
-    public $fileinfo;
-    /** @var bool whether this file is directory */
-    private $isdir;
-    /** @var array caches retrieved children */
-    private $children = null;
-    /** @var array caches retrieved information whether this file is an empty directory */
-    protected $isempty = null;
-    /** @var repository link to the container repository (for filtering the results) */
-    private $repository;
-    /** @var repository_local_file link to parent directory */
-    protected $parent;
-    /** @var bool caches calculated information on whether this directory must be skipped in the tree */
-    private $skip = null;
 
     /**
-     * Creates (or retrieves from cache) the repository_local_file object for $file_info
+     * Returns all children elements that have one of the specified extensions
+     *
+     * This function may skip subfolders and recursively add their children
+     * {@link repository_local::can_skip()}
      *
      * @param file_info $fileinfo
-     * @param repository $repository
-     * @param repository_local_file $parent
-     * @return repository_local_file
+     * @param string|array $extensions, for example '*' or array('.gif','.jpg')
+     * @return array array of file_info elements
      */
-    public static function retrieve_file_info(file_info $fileinfo, repository $repository, repository_local_file $parent = null) {
-        $encodedpath = base64_encode(serialize($fileinfo->get_params()));
-        if (!isset(self::$cachedfiles[$encodedpath])) {
-            self::$cachedfiles[$encodedpath] = new repository_local_file($fileinfo, $repository, $parent);
+    private function get_non_empty_children(file_info $fileinfo, $extensions) {
+        $nonemptychildren = $fileinfo->get_non_empty_children($extensions);
+        $list = array();
+        foreach ($nonemptychildren as $child) {
+            if ($this->can_skip($child, $extensions, $fileinfo)) {
+                $list = array_merge($list, $this->get_non_empty_children($child, $extensions));
+            } else {
+                $list[] = $this->get_node($child);
+            }
         }
-        return self::$cachedfiles[$encodedpath];
+        return $list;
     }
 
     /**
-     * Creates an object
+     * Whether this folder may be skipped in folder hierarchy
+     *
+     * 1. Skip the name of a single filearea in a module
+     * 2. Skip course categories for non-admins who do not have navshowmycoursecategories setting
      *
      * @param file_info $fileinfo
-     * @param repository $repository
-     * @param repository_local_file $parent
+     * @param string|array $extensions, for example '*' or array('.gif','.jpg')
+     * @param file_info|int $parent specify parent here if we know it to avoid creating extra objects
+     * @return bool
      */
-    private function __construct(file_info $fileinfo, repository $repository, repository_local_file $parent = null) {
-        $this->repository = $repository;
-        $this->fileinfo = $fileinfo;
-        $this->isdir = $fileinfo->is_directory();
-        if (!$this->isdir) {
-            $node = array('title' => $this->fileinfo->get_visible_name());
-            $this->isempty = !$repository->filter($node);
-            $this->skip = false;
+    private function can_skip(file_info $fileinfo, $extensions, $parent = -1) {
+        global $CFG;
+        if (!$fileinfo->is_directory()) {
+            // do not skip files
+            return false;
         }
+        if ($fileinfo instanceof file_info_context_coursecat) {
+            // This is a course category. For non-admins we do not display categories
+            return empty($CFG->navshowmycoursecategories) &&
+                            !has_capability('moodle/course:update', context_system::instance());
+        } else if ($fileinfo instanceof file_info_context_course ||
+                $fileinfo instanceof file_info_context_user ||
+                $fileinfo instanceof file_info_area_course_legacy ||
+                $fileinfo instanceof file_info_context_module ||
+                $fileinfo instanceof file_info_context_system) {
+            // these instances can never be filearea inside an activity, they will never be skipped
+            return false;
+        } else {
+            $params = $fileinfo->get_params();
+            if (strlen($params['filearea']) &&
+                    ($params['filepath'] === '/' || empty($params['filepath'])) &&
+                    ($params['filename'] === '.' || empty($params['filename'])) &&
+                    context::instance_by_id($params['contextid'])->contextlevel == CONTEXT_MODULE) {
+                if ($parent === -1) {
+                    $parent = $fileinfo->get_parent();
+                }
+                // This is a filearea inside an activity, it can be skipped if it has no non-empty siblings
+                if ($parent && ($parent instanceof file_info_context_module)) {
+                    if ($parent->count_non_empty_children($extensions, 2) <= 1) {
+                        return true;
+                    }
+                }
+            }
+        }
+        return false;
     }
 
     /**
-&n