Index: mod/forum/db/upgrade.php =================================================================== RCS file: /cvsroot/moodle/moodle/mod/forum/db/upgrade.php,v retrieving revision 1.17 diff -u -r1.17 upgrade.php --- mod/forum/db/upgrade.php 24 Jul 2008 08:38:08 -0000 1.17 +++ mod/forum/db/upgrade.php 28 Jul 2008 12:05:08 -0000 @@ -21,6 +21,8 @@ global $CFG, $THEME, $DB; + $dbman = $DB->get_manager(); // loads ddl manager and xmldb classes + $result = true; /// And upgrade begins here. For each one, you'll need one @@ -75,7 +77,7 @@ upgrade_mod_savepoint($result, 2007101512, 'forum'); } - + if ($result and $oldversion < 2008072401) { $eventdata = new object(); $eventdata->modulename = 'forum'; @@ -85,6 +87,35 @@ upgrade_mod_savepoint($result, 2008072401, 'forum'); } + if ($result && $oldversion < 2008072800) { + /// Define field completiondiscussions to be added to forum + $table = new XMLDBTable('forum'); + $field = new XMLDBField('completiondiscussions'); + $field->setAttributes(XMLDB_TYPE_INTEGER, '9', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, null, null, '0', 'draft'); + + /// Launch add field completiondiscussions + if(!$dbman->field_exists($table,$field)) { + $dbman->add_field($table, $field); + } + + $field = new XMLDBField('completionreplies'); + $field->setAttributes(XMLDB_TYPE_INTEGER, '9', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, null, null, '0', 'completiondiscussions'); + + /// Launch add field completionreplies + if(!$dbman->field_exists($table,$field)) { + $dbman->add_field($table, $field); + } + + /// Define field completionposts to be added to forum + $field = new XMLDBField('completionposts'); + $field->setAttributes(XMLDB_TYPE_INTEGER, '9', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, null, null, '0', 'completionreplies'); + + /// Launch add field completionposts + if(!$dbman->field_exists($table,$field)) { + $dbman->add_field($table, $field); + } + upgrade_mod_savepoint($result, 2008072800, 'forum'); + } return $result; Index: mod/forum/db/install.xml =================================================================== RCS file: /cvsroot/moodle/moodle/mod/forum/db/install.xml,v retrieving revision 1.6 diff -u -r1.6 install.xml --- mod/forum/db/install.xml 10 Oct 2007 02:52:27 -0000 1.6 +++ mod/forum/db/install.xml 28 Jul 2008 12:05:08 -0000 @@ -23,7 +23,10 @@ - + + + + Index: mod/quiz/view.php =================================================================== RCS file: /cvsroot/moodle/moodle/mod/quiz/view.php,v retrieving revision 1.143 diff -u -r1.143 view.php --- mod/quiz/view.php 8 Jul 2008 17:46:59 -0000 1.143 +++ mod/quiz/view.php 28 Jul 2008 12:05:09 -0000 @@ -373,6 +373,12 @@ // Should we not be seeing if we need to print right-hand-side blocks? finish_page($course); + + // Mark module as viewed (note, we do this here and not in finish_page, + // otherwise the 'not enrolled' error conditions would result in marking + // 'viewed', I think it's better if they don't.) + $completion=new completion_info($course); + $completion->set_module_viewed(cm); // Utility functions ================================================================= Index: mod/quiz/lib.php =================================================================== RCS file: /cvsroot/moodle/moodle/mod/quiz/lib.php,v retrieving revision 1.307 diff -u -r1.307 lib.php --- mod/quiz/lib.php 24 Jul 2008 21:59:14 -0000 1.307 +++ mod/quiz/lib.php 28 Jul 2008 12:05:09 -0000 @@ -58,7 +58,7 @@ /// FUNCTIONS /////////////////////////////////////////////////////////////////// -/** +/** * Code to be executed when a module is installed * now is just used to register the module as message provider */ @@ -1211,6 +1211,18 @@ } /** + * @param string $feature FEATURE_xx constant for requested feature + * @return bool True if quiz supports feature + */ +function quiz_supports($feature) { + switch($feature) { + case FEATURE_GRADE_HAS_GRADE: return true; + case FEATURE_COMPLETION_TRACKS_VIEWS: return true; + default: return false; + } +} + +/** * Returns all other caps used in module */ function quiz_get_extra_capabilities() { Index: mod/forum/view.php =================================================================== RCS file: /cvsroot/moodle/moodle/mod/forum/view.php,v retrieving revision 1.124 diff -u -r1.124 view.php --- mod/forum/view.php 21 Jul 2008 08:03:42 -0000 1.124 +++ mod/forum/view.php 28 Jul 2008 12:05:08 -0000 @@ -268,6 +268,8 @@ break; } + $completion=new completion_info($course); + $completion->set_module_viewed($cm); print_footer($course); ?> Index: mod/forum/post.php =================================================================== RCS file: /cvsroot/moodle/moodle/mod/forum/post.php,v retrieving revision 1.169 diff -u -r1.169 post.php --- mod/forum/post.php 2 Jul 2008 22:06:22 -0000 1.169 +++ mod/forum/post.php 28 Jul 2008 12:05:08 -0000 @@ -280,14 +280,15 @@ notice("Sorry, but you are not allowed to delete that discussion!", forum_go_back_to("discuss.php?d=$post->discussion")); } - forum_delete_discussion($discussion); + forum_delete_discussion($discussion,false,$course,$cm,$forum); add_to_log($discussion->course, "forum", "delete discussion", "view.php?id=$cm->id", "$forum->id", $cm->id); - + redirect("view.php?f=$discussion->forum"); - } else if (forum_delete_post($post, has_capability('mod/forum:deleteanypost', $modcontext))) { + } else if (forum_delete_post($post, has_capability('mod/forum:deleteanypost', $modcontext), + $course, $cm, $forum)) { if ($forum->type == 'single') { // Single discussion forums are an exception. We show @@ -402,7 +403,7 @@ add_to_log($discussion->course, "forum", "prune post", "discuss.php?d=$newid", "$post->id", $cm->id); - + redirect(forum_go_back_to("discuss.php?d=$newid")); } else { // User just asked to prune something @@ -554,6 +555,13 @@ } add_to_log($course->id, "forum", "add post", "$discussionurl&parent=$fromform->id", "$fromform->id", $cm->id); + + // Update completion state + $completion=new completion_info($course); + if($completion->is_enabled($cm) && + ($forum->completionreplies || $forum->completionposts)) { + $completion->update_state($cm,COMPLETION_COMPLETE); + } redirect(forum_go_back_to("$discussionurl#p$fromform->id"), $message.$subscribemessage, $timemessage); @@ -605,6 +613,13 @@ $timemessage = 4; } + // Update completion status + $completion=new completion_info($course); + if($completion->is_enabled($cm) && + ($forum->completiondiscussions || $forum->completionposts)) { + $completion->update_state($cm,COMPLETION_COMPLETE); + } + redirect(forum_go_back_to("view.php?f=$fromform->forum"), $message.$subscribemessage, $timemessage); } else { Index: mod/forum/mod_form.php =================================================================== RCS file: /cvsroot/moodle/moodle/mod/forum/mod_form.php,v retrieving revision 1.30 diff -u -r1.30 mod_form.php --- mod/forum/mod_form.php 26 Jul 2008 15:15:26 -0000 1.30 +++ mod/forum/mod_form.php 28 Jul 2008 12:05:08 -0000 @@ -13,7 +13,7 @@ $mform->addElement('text', 'name', get_string('forumname', 'forum'), array('size'=>'64')); if (!empty($CFG->formatstringstriptags)) { - $mform->setType('name', PARAM_TEXT); + $mform->setType('name', PARAM_TEXT); } else { $mform->setType('name', PARAM_CLEAN); } @@ -178,7 +178,80 @@ $default_values['ratingtime']= ($default_values['assesstimestart'] && $default_values['assesstimefinish']) ? 1 : 0; } + + // Set up the completion checkboxes which aren't part of standard data. + // We also make the default value (if you turn on the checkbox) for those + // numbers to be 1, this will not apply unless checkbox is ticked. + $default_values['completiondiscussionsenabled']= + !empty($default_values['completiondiscussions']) ? 1 : 0; + if(empty($default_values['completiondiscussions'])) { + $default_values['completiondiscussions']=1; + } + $default_values['completionrepliesenabled']= + !empty($default_values['completionreplies']) ? 1 : 0; + if(empty($default_values['completionreplies'])) { + $default_values['completionreplies']=1; + } + $default_values['completionpostsenabled']= + !empty($default_values['completionposts']) ? 1 : 0; + if(empty($default_values['completionposts'])) { + $default_values['completionposts']=1; + } + } + + function add_completion_rules() { + $mform =& $this->_form; + + $group=array(); + $group[] =& $mform->createElement('checkbox', 'completionpostsenabled', '', get_string('completionposts','forum')); + $group[] =& $mform->createElement('text', 'completionposts', '', array('size'=>3)); + $mform->setType('completionposts',PARAM_INT); + $mform->addGroup($group, 'completionpostsgroup', get_string('completionpostsgroup','forum'), array(' '), false); + $mform->setHelpButton('completionpostsgroup', array('completion', get_string('completionpostshelp', 'forum'), 'forum')); + $mform->disabledIf('completionposts','completionpostsenabled','notchecked'); + + $group=array(); + $group[] =& $mform->createElement('checkbox', 'completiondiscussionsenabled', '', get_string('completiondiscussions','forum')); + $group[] =& $mform->createElement('text', 'completiondiscussions', '', array('size'=>3)); + $mform->setType('completiondiscussions',PARAM_INT); + $mform->addGroup($group, 'completiondiscussionsgroup', get_string('completiondiscussionsgroup','forum'), array(' '), false); + $mform->setHelpButton('completiondiscussionsgroup', array('completion', get_string('completiondiscussionshelp', 'forum'), 'forum')); + $mform->disabledIf('completiondiscussions','completiondiscussionsenabled','notchecked'); + + $group=array(); + $group[] =& $mform->createElement('checkbox', 'completionrepliesenabled', '', get_string('completionreplies','forum')); + $group[] =& $mform->createElement('text', 'completionreplies', '', array('size'=>3)); + $mform->setType('completionreplies',PARAM_INT); + $mform->addGroup($group, 'completionrepliesgroup', get_string('completionrepliesgroup','forum'), array(' '), false); + $mform->setHelpButton('completionrepliesgroup', array('completion', get_string('completionreplieshelp', 'forum'), 'forum')); + $mform->disabledIf('completionreplies','completionrepliesenabled','notchecked'); + + return array('completiondiscussionsgroup','completionrepliesgroup','completionpostsgroup'); } + function completion_rule_enabled($data) { + return (!empty($data['completiondiscussionsenabled']) && $data['completiondiscussions']!=0) || + (!empty($data['completionrepliesenabled']) && $data['completionreplies']!=0) || + (!empty($data['completionpostsenabled']) && $data['completionposts']!=0); + } + + function get_data($slashed=true) { + $data=parent::get_data($slashed); + if(!$data) { + return false; + } + // Turn off completion settings if the checkboxes aren't ticked + $autocompletion=!empty($data->completion) && $data->completion==COMPLETION_TRACKING_AUTOMATIC; + if(empty($data->completiondiscussionsenabled) || !$autocompletion) { + $data->completiondiscussions=0; + } + if(empty($data->completionrepliesenabled) || !$autocompletion) { + $data->completionreplies=0; + } + if(empty($data->completionpostsenabled) || !$autocompletion) { + $data->completionposts=0; + } + return $data; + } } ?> Index: mod/forum/lib.php =================================================================== RCS file: /cvsroot/moodle/moodle/mod/forum/lib.php,v retrieving revision 1.689 diff -u -r1.689 lib.php --- mod/forum/lib.php 25 Jul 2008 15:17:24 -0000 1.689 +++ mod/forum/lib.php 28 Jul 2008 12:05:08 -0000 @@ -29,7 +29,7 @@ /// STANDARD FUNCTIONS /////////////////////////////////////////////////////////// -/** +/** * Code to be executed when a module is installed * now is just used to register the module as message provider */ @@ -210,6 +210,83 @@ /** + * Indicates API features that the forum supports. + * + * @param string $feature + * @return mixed True if yes (some features may use other values) + */ +function forum_supports($feature) { + switch($feature) { + case FEATURE_COMPLETION_TRACKS_VIEWS: return true; + case FEATURE_COMPLETION_HAS_RULES: return true; + default: return false; + } +} + + +/** + * Obtains the automatic completion state for this forum based on any conditions + * in forum settings. + * + * @param object $course Course + * @param object $cm Course-module + * @param int $userid User ID + * @param bool $type Type of comparison (or/and; can be used as return value if no conditions) + * @return bool True if completed, false if not. (If no conditions, then return + * value depends on comparison type) + */ +function forum_get_completion_state($course,$cm,$userid,$type) { + global $CFG,$DB; + + // Get forum details + if(!($forum=$DB->get_record('forum',array('id'=>$cm->instance)))) { + throw new Exception("Can't find forum {$cm->instance}"); + } + + $result=$type; // Default return value + + $postcountparams=array('userid'=>$userid,'forumid'=>$forum->id); + $postcountsql=" +SELECT + COUNT(1) +FROM + {$CFG->prefix}forum_posts fp + INNER JOIN {$CFG->prefix}forum_discussions fd ON fp.discussion=fd.id +WHERE + fp.userid=:userid AND fd.forum=:forumid"; + + if($forum->completiondiscussions) { + $value = $forum->completiondiscussions <= + $DB->count_records('forum_discussions',array('forum'=>$forum->id,'userid'=>$userid)); + if($type==COMPLETION_AND) { + $result=$result && $value; + } else { + $result=$result || $value; + } + } + if($forum->completionreplies) { + $value = $forum->completionreplies <= + $DB->get_field_sql( $postcountsql.' AND fp.parent<>0',$postcountparams); + if($type==COMPLETION_AND) { + $result=$result && $value; + } else { + $result=$result || $value; + } + } + if($forum->completionposts) { + $value = $forum->completionposts <= $DB->get_field_sql($postcountsql,$postcountparams); + if($type==COMPLETION_AND) { + $result=$result && $value; + } else { + $result=$result || $value; + } + } + + return $result; +} + + +/** * Function to be run periodically according to the moodle cron * Finds all posts that have yet to be mailed out, and mails them * out to all subscribers @@ -3981,10 +4058,14 @@ /** - * + * Deletes a discussion and handles all associated cleanup. + * @param object $discussion Discussion to delete + * @param bool $fulldelete True when deleting entire forum + * @param object $course Course (required if fulldelete is false) + * @param object $cm Course-module (required if fulldelete is false) + * @param object $forum Forum (required if fulldelete is false) */ -function forum_delete_discussion($discussion, $fulldelete=false) { -// $discussion is a discussion record object +function forum_delete_discussion($discussion, $fulldelete=false,$course=null,$cm=null,$forum=null) { global $DB; $result = true; @@ -3992,10 +4073,7 @@ foreach ($posts as $post) { $post->course = $discussion->course; $post->forum = $discussion->forum; - if (! $DB->delete_records("forum_ratings", array("post" => "$post->id"))) { - $result = false; - } - if (! forum_delete_post($post, $fulldelete)) { + if (! forum_delete_post($post, 'ignore',$course, $cm, $forum, $fulldelete)) { $result = false; } } @@ -4007,26 +4085,45 @@ $result = false; } + // Update completion state if we are tracking completion based on number of posts + $completion=new completion_info($course); + if(!$fulldelete && // But don't bother when deleting whole thing + $completion->is_enabled($cm)==COMPLETION_TRACKING_AUTOMATIC && + ($forum->completiondiscussions || $forum->completionreplies || $forum->completionposts)) { + $completion->update_state($cm,COMPLETION_INCOMPLETE,$discussion->userid); + } + return $result; } /** - * + * Deletes a single forum post. + * @param object $post Forum post object + * @param mixed $children Whether to delete children. If false, returns false + * if there are any children (without deleting the post). If true, + * recursively deletes all children. If set to special value 'ignore', deletes + * post regardless of children (this is for use only when deleting all posts + * in a disussion). + * @param object $course Course + * @param object $cm Course-module + * @param object $forum Forum + * @param bool $skipcompletion True to skip updating completion state if it + * would otherwise be updated, i.e. when deleting entire forum anyway. */ -function forum_delete_post($post, $children=false) { +function forum_delete_post($post, $children, $course, $cm, $forum, $skipcompletion=false) { global $DB; - if ($childposts = $DB->get_records('forum_posts', array('parent' => $post->id))) { + if ($children!='ignore' && ($childposts = $DB->get_records('forum_posts', array('parent'=>$post->id)))) { if ($children) { foreach ($childposts as $childpost) { - forum_delete_post($childpost, true); + forum_delete_post($childpost, true, $course, $cm, $forum, $skipcompletion); } } else { return false; } } if ($DB->delete_records("forum_posts", array("id" => $post->id))) { - $DB->delete_records("forum_ratings", array("post" => $post->id)); // Just in case + $DB->delete_records("forum_ratings", array("post" => $post->id)); forum_tp_delete_read_records(-1, $post->id); @@ -4040,6 +4137,14 @@ // Just in case we are deleting the last post forum_discussion_update_last_post($post->discussion); + // Update completion state if we are tracking completion based on number of posts + $completion=new completion_info($course); + if(!$skipcompletion && // But don't bother when deleting whole thing + $completion->is_enabled($cm)==COMPLETION_TRACKING_AUTOMATIC && + ($forum->completiondiscussions || $forum->completionreplies || $forum->completionposts)) { + $completion->update_state($cm,COMPLETION_INCOMPLETE,$post->userid); + } + return true; } return false; @@ -4345,8 +4450,8 @@ WHERE p.userid = :userid AND d.forum = :forumid"; return $DB->record_exists_sql($sql, array('forumid'=>$forumid,'userid'=>$userid)); } else { - return $DB->record_exists('forum_posts', array('discussion'=>$did,'userid'=>$userid)); - } + return $DB->record_exists('forum_posts', array('discussion'=>$did,'userid'=>$userid)); +} } /** Index: mod/forum/version.php =================================================================== RCS file: /cvsroot/moodle/moodle/mod/forum/version.php,v retrieving revision 1.74 diff -u -r1.74 version.php --- mod/forum/version.php 24 Jul 2008 08:38:08 -0000 1.74 +++ mod/forum/version.php 28 Jul 2008 12:05:08 -0000 @@ -5,7 +5,7 @@ // This fragment is called by /admin/index.php //////////////////////////////////////////////////////////////////////////////// -$module->version = 2008072401; +$module->version = 2008072800; $module->requires = 2008072401; // Requires this Moodle version $module->cron = 60; Index: lib/db/upgrade.php =================================================================== RCS file: /cvsroot/moodle/moodle/lib/db/upgrade.php,v retrieving revision 1.214 diff -u -r1.214 upgrade.php --- lib/db/upgrade.php 27 Jul 2008 22:02:20 -0000 1.214 +++ lib/db/upgrade.php 28 Jul 2008 12:05:07 -0000 @@ -191,7 +191,7 @@ $result = $DB->delete_records_select('role_names', $DB->sql_isempty('role_names', 'name', false, false)); upgrade_main_savepoint($result, 2008070300); } - + if ($result && $oldversion < 2008070700) { if (isset($CFG->defaultuserroleid) and isset($CFG->guestroleid) and $CFG->defaultuserroleid == $CFG->guestroleid) { // guest can not be selected in defaultuserroleid! @@ -347,6 +347,82 @@ upgrade_main_savepoint($result, 2008072400); } + if ($result && $oldversion < 2008072800) { + + /// Define field enablecompletion to be added to course + $table = new xmldb_table('course'); + $field = new xmldb_field('enablecompletion'); + $field->set_attributes(XMLDB_TYPE_INTEGER, '1', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, null, null, '0', 'defaultrole'); + + /// Launch add field enablecompletion + if(!$dbman->field_exists($table,$field)) { + $dbman->add_field($table, $field); + } + + /// Define field completion to be added to course_modules + $table = new xmldb_table('course_modules'); + $field = new xmldb_field('completion'); + $field->set_attributes(XMLDB_TYPE_INTEGER, '1', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, null, null, '0', 'groupmembersonly'); + + /// Launch add field completion + if(!$dbman->field_exists($table,$field)) { + $dbman->add_field($table, $field); + } + + /// Define field completiongradeitemnumber to be added to course_modules + $field = new xmldb_field('completiongradeitemnumber'); + $field->set_attributes(XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, null, null, null, null, null, 'completion'); + + /// Launch add field completiongradeitemnumber + if(!$dbman->field_exists($table,$field)) { + $dbman->add_field($table, $field); + } + + /// Define field completionview to be added to course_modules + $field = new xmldb_field('completionview'); + $field->set_attributes(XMLDB_TYPE_INTEGER, '1', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, null, null, '0', 'completiongradeitemnumber'); + + /// Launch add field completionview + if(!$dbman->field_exists($table,$field)) { + $dbman->add_field($table, $field); + } + + /// Define field completionexpected to be added to course_modules + $field = new xmldb_field('completionexpected'); + $field->set_attributes(XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, null, null, '0', 'completionview'); + + /// Launch add field completionexpected + if(!$dbman->field_exists($table,$field)) { + $dbman->add_field($table, $field); + } + + /// Define table course_modules_completion to be created + $table = new xmldb_table('course_modules_completion'); + if(!$dbman->table_exists($table)) { + + /// Adding fields to table course_modules_completion + $table->addFieldInfo('id', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, XMLDB_SEQUENCE, null, null, null); + $table->addFieldInfo('coursemoduleid', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, null, null, null); + $table->addFieldInfo('userid', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, null, null, null); + $table->addFieldInfo('completionstate', XMLDB_TYPE_INTEGER, '1', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, null, null, null); + $table->addFieldInfo('viewed', XMLDB_TYPE_INTEGER, '1', XMLDB_UNSIGNED, null, null, null, null, null); + $table->addFieldInfo('timemodified', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, null, null, null); + + /// Adding keys to table course_modules_completion + $table->addKeyInfo('primary', XMLDB_KEY_PRIMARY, array('id')); + + /// Adding indexes to table course_modules_completion + $table->addIndexInfo('coursemoduleid', XMLDB_INDEX_NOTUNIQUE, array('coursemoduleid')); + $table->addIndexInfo('userid', XMLDB_INDEX_NOTUNIQUE, array('userid')); + + /// Launch create table for course_modules_completion + $dbman->create_table($table); + } + + /// Main savepoint reached + upgrade_main_savepoint($result, 2008072800); + } + /* * TODO: Index: lib/db/install.xml =================================================================== RCS file: /cvsroot/moodle/moodle/lib/db/install.xml,v retrieving revision 1.155 diff -u -r1.155 install.xml --- lib/db/install.xml 24 Jul 2008 08:38:05 -0000 1.155 +++ lib/db/install.xml 28 Jul 2008 12:05:07 -0000 @@ -73,7 +73,9 @@ - + + @@ -131,7 +133,7 @@ - +
@@ -146,7 +148,14 @@ - + + + + + @@ -160,7 +169,33 @@
- +
+ + + + + + + + + + + + + + + + + +
+ @@ -1680,11 +1715,11 @@ - - - - -
+ + + + + @@ -1790,4 +1825,4 @@ - + \ No newline at end of file Index: lib/db/access.php =================================================================== RCS file: /cvsroot/moodle/moodle/lib/db/access.php,v retrieving revision 1.85 diff -u -r1.85 access.php --- lib/db/access.php 24 Jul 2008 21:59:16 -0000 1.85 +++ lib/db/access.php 28 Jul 2008 12:05:06 -0000 @@ -539,6 +539,19 @@ ) ), + 'moodle/course:viewprogress' => array( + + 'riskbitmask' => RISK_PERSONAL, + + 'captype' => 'read', + 'contextlevel' => CONTEXT_COURSE, + 'legacy' => array( + 'teacher' => CAP_ALLOW, + 'editingteacher' => CAP_ALLOW, + 'admin' => CAP_ALLOW + ) + ), + 'moodle/course:viewhiddencourses' => array( 'captype' => 'read', @@ -830,7 +843,7 @@ 'admin' => CAP_ALLOW ) ), - + //capabilities designed for the new message system configuration 'moodle/user:editmessageprofile' => array( Index: lib/adminlib.php =================================================================== RCS file: /cvsroot/moodle/moodle/lib/adminlib.php,v retrieving revision 1.227 diff -u -r1.227 adminlib.php --- lib/adminlib.php 26 Jul 2008 16:50:33 -0000 1.227 +++ lib/adminlib.php 28 Jul 2008 12:05:05 -0000 @@ -201,7 +201,7 @@ $dbdirs[] = $CFG->dirroot.'/'.$CFG->admin.'/report/'.$plugin.'/db'; } } - + /// Now quiz report plugins (mod/quiz/report/xxx/db) if ($plugins = get_list_of_plugins('mod/quiz/report', 'db')) { foreach ($plugins as $plugin) { @@ -1883,7 +1883,7 @@ $data = 0; } // $data is a string - $validated = $this->validate($data); + $validated = $this->validate($data); if ($validated !== true) { return $validated; } @@ -1944,7 +1944,7 @@ $defaultinfo = $default; if (!is_null($default) and $default !== '') { $defaultinfo = "\n".$default; - } + } return format_admin_setting($this, $this->visiblename, '
', @@ -2221,7 +2221,7 @@ } } } - + $options = array(); $defaults = array(); foreach($this->choices as $key=>$description) { @@ -2258,7 +2258,7 @@ $return .= ''; return format_admin_setting($this, $this->visiblename, $return, $this->description, false, '', $defaultinfo, $query); - + } } @@ -2353,7 +2353,7 @@ if (strpos($textlib->strtolower($value), $query) !== false) { return true; } - } + } return false; } @@ -2800,7 +2800,7 @@ function write_setting($data) { global $DB; $data = trim($data); - $validated = $this->validate($data); + $validated = $this->validate($data); if ($validated !== true) { return $validated; } @@ -3388,12 +3388,22 @@ /** - * Graded roles in gradebook + * Admin setting that allows a user to pick appropriate roles for something. */ -class admin_setting_special_gradebookroles extends admin_setting_configmulticheckbox { - function admin_setting_special_gradebookroles() { - parent::admin_setting_configmulticheckbox('gradebookroles', get_string('gradebookroles', 'admin'), - get_string('configgradebookroles', 'admin'), NULL, NULL); +class admin_setting_pickroles extends admin_setting_configmulticheckbox { + private $types; + + /** + * @param string $name Name of config variable + * @param string $visiblename Display name + * @param string $description Description + * @param array $types Array of capabilities (usually moodle/legacy:something) + * which identify roles that will be enabled by default. Default is the + * student role + */ + function admin_setting_pickroles($name, $visiblename, $description,$types=array('moodle/legacy:student')) { + parent::admin_setting_configmulticheckbox($name, $visiblename, $description, NULL, NULL); + $this->types=$types; } function load_choices() { @@ -3418,18 +3428,33 @@ function get_defaultsetting() { global $CFG; if (empty($CFG->rolesactive)) { - return NULL; + return array(0); } $result = array(); - if ($studentroles = get_roles_with_capability('moodle/legacy:student', CAP_ALLOW)) { - foreach ($studentroles as $studentrole) { - $result[$studentrole->id] = '1'; + foreach($this->types as $capability) { + if ($caproles = get_roles_with_capability($capability, CAP_ALLOW)) { + foreach ($caproles as $caprole) { + if(!in_array($caprole->id,$result)) { + $result[] = $caprole->id; + } + } } } return $result; } } +/** + * Graded roles in gradebook + */ +class admin_setting_special_gradebookroles extends admin_setting_pickroles { + function admin_setting_special_gradebookroles() { + parent::admin_setting_pickroles('gradebookroles', get_string('gradebookroles', 'admin'), + get_string('configgradebookroles', 'admin')); + } +} + + class admin_setting_regradingcheckbox extends admin_setting_configcheckbox { function write_setting($data) { global $CFG, $DB; @@ -3444,45 +3469,17 @@ } return $return; - } + } } /** * Which roles to show on course decription page */ -class admin_setting_special_coursemanager extends admin_setting_configmulticheckbox { +class admin_setting_special_coursemanager extends admin_setting_pickroles { function admin_setting_special_coursemanager() { - parent::admin_setting_configmulticheckbox('coursemanager', get_string('coursemanager', 'admin'), - get_string('configcoursemanager', 'admin'), NULL, NULL); - } - - function load_choices() { - global $DB; - if (is_array($this->choices)) { - return true; - } - if ($roles = $DB->get_records('role',null,'sortorder')) { - $this->choices = array(); - foreach($roles as $role) { - $this->choices[$role->id] = format_string($role->name); - } - return true; - } - return false; - } - - function get_defaultsetting() { - global $CFG; - if (empty($CFG->rolesactive)) { - return NULL; - } - $result = array(); - if ($teacherroles = get_roles_with_capability('moodle/legacy:editingteacher', CAP_ALLOW)) { - foreach ($teacherroles as $teacherrole) { - $result[$teacherrole->id] = '1'; - } - } - return $result; + parent::admin_setting_pickroles('coursemanager', get_string('coursemanager', 'admin'), + get_string('configcoursemanager', 'admin'), + 'moodle/legacy:editingteacher'); } } @@ -3589,7 +3586,7 @@ $defaultinfo[] = get_string('advanced'); } $defaultinfo = implode(', ', $defaultinfo); - + } else { $defaultinfo = NULL; } @@ -4792,7 +4789,7 @@ /** * Prints tables of detected plugins, one table per plugin type, - * and prints whether they are part of the standard Moodle + * and prints whether they are part of the standard Moodle * distribution or not. */ function print_plugin_tables() { @@ -4813,7 +4810,7 @@ 'scorm', 'survey', 'wiki'); - + $plugins_standard['blocks'] = array('activity_modules', 'admin', 'admin_bookmarks', @@ -4845,7 +4842,7 @@ 'tag_flickr', 'tag_youtube', 'tags'); - + $plugins_standard['filter'] = array('activitynames', 'algebra', 'censor', @@ -4872,14 +4869,14 @@ $plugins_ondisk['mod'] = get_list_of_plugins('mod', 'db'); $plugins_ondisk['blocks'] = get_list_of_plugins('blocks', 'db'); $plugins_ondisk['filter'] = get_list_of_plugins('filter', 'db'); - + $strstandard = get_string('standard'); $strnonstandard = get_string('nonstandard'); $strmissingfromdisk = '(' . get_string('missingfromdisk') . ')'; $strabouttobeinstalled = '(' . get_string('abouttobeinstalled') . ')'; $html = ''; - + $html .= '
'; foreach ($plugins_ondisk as $cat => $list_ondisk) { @@ -4895,8 +4892,8 @@ $html .= '\n" . '\n" . '\n\n"; - - $row = 1; + + $row = 1; foreach ($list_ondisk as $k => $plugin) { $status = 'ok'; @@ -4906,15 +4903,15 @@ if (!in_array($plugin, $plugins_standard[$cat])) { $standard = 'nonstandard'; $status = 'warning'; - } - + } + // Get real name and full path of plugin $plugin_name = "[[$plugin]]"; - + $plugin_path = "$cat/$plugin"; - + $plugin_name = get_plugin_name($plugin, $cat); - + // Determine if the plugin is about to be installed if ($cat != 'filter' && !in_array($plugin, $plugins_installed[$cat])) { $note = $strabouttobeinstalled; @@ -4930,11 +4927,11 @@ // If the plugin was both on disk and in the db, unset the value from the installed plugins list if ($key = array_search($plugin, $plugins_installed[$cat])) { unset($plugins_installed[$cat][$key]); - } - } + } + } // If there are plugins left in the plugins_installed list, it means they are missing from disk - foreach ($plugins_installed[$cat] as $k => $missing_plugin) { + foreach ($plugins_installed[$cat] as $k => $missing_plugin) { // Make sure the plugin really is missing from disk if (!in_array($missing_plugin, $plugins_ondisk[$cat])) { $standard = 'standard'; @@ -4949,15 +4946,15 @@ . "\n" . "\n" . "\n\n"; - $row++; + $row++; } } $html .= '
' . get_string('directory') . "' . get_string('name') . "' . get_string('status') . "
?$plugin_name" . ${'str' . $standard} . " $strmissingfromdisk
'; } - + $html .= '
'; - + echo $html; } Index: lib/moodlelib.php =================================================================== RCS file: /cvsroot/moodle/moodle/lib/moodlelib.php,v retrieving revision 1.1074 diff -u -r1.1074 moodlelib.php --- lib/moodlelib.php 26 Jul 2008 16:50:33 -0000 1.1074 +++ lib/moodlelib.php 28 Jul 2008 12:05:06 -0000 @@ -3345,7 +3345,7 @@ $courseid = $courseorid; if (!$course = $DB->get_record('course', array('id'=>$courseid))) { return false; - } + } } // frontpage course can not be deleted!! @@ -5938,6 +5938,48 @@ return $plugins; } +// Feature constants +/** True if module can provide a grade */ +define('FEATURE_GRADE_HAS_GRADE','grade_has_grade'); +/** True if module has code to track whether somebody viewed it */ +define('FEATURE_COMPLETION_TRACKS_VIEWS','completion_tracks_views'); +/** True if module has custom completion rules */ +define('FEATURE_COMPLETION_HAS_RULES','completion_has_rules'); + +/** + * Checks whether a plugin supports a specified feature. + * + * @param string $type Plugin type e.g. 'mod' + * @param string $name Plugin name + * @param string $feature Feature code (FEATURE_xx constant) + * @return Feature result (false if not supported, usually true but may have + * other feature-specific value otherwise) + */ +function plugin_supports($type,$name,$feature) { + global $CFG; + switch($type) { + case 'mod' : + $file=$CFG->dirroot.'/mod/'.$name.'/lib.php'; + $function=$name.'_supports'; + break; + default: + throw new Exception('Unsupported plugin type ('.$type.')'); + } + + // Load library and look for function + require_once($file); + if(function_exists($function)) { + // Function exists, so just return function result + return $function($feature); + } else { + switch($feature) { + // If some features can also be checked in other ways + // for legacy support, this could be added here + default: return false; + } + } +} + /** * Returns true if the current version of PHP is greater that the specified one. * @@ -5949,13 +5991,13 @@ } /** - * Checks to see if is the browser operating system matches the specified + * Checks to see if is the browser operating system matches the specified * brand. - * + * * Known brand: 'Windows','Linux','Macintosh','SGI','SunOS','HP-UX' * * @uses $_SERVER - * @param string $brand The operating system identifier being tested + * @param string $brand The operating system identifier being tested * @return bool true if the given brand below to the detected operating system */ function check_browser_operating_system($brand) { @@ -5966,8 +6008,8 @@ if (preg_match("/$brand/i", $_SERVER['HTTP_USER_AGENT'])) { return true; } - - return false; + + return false; } /** @@ -6572,7 +6614,7 @@ // if the words shouldn't be cut in the middle... if (!$exact) { // ...search the last occurance of a space... - for ($k=strlen($truncate);$k>0;$k--) { + for ($k=strlen($truncate);$k>0;$k--) { if (!empty($truncate[$k]) && ($char = $truncate[$k])) { if ($char == '.' or $char == ' ') { $breakpos = $k+1; @@ -6582,23 +6624,23 @@ break; // character boundary. } } - } + } - if (isset($breakpos)) { + if (isset($breakpos)) { // ...and cut the text in this position $truncate = substr($truncate, 0, $breakpos); - } - } + } + } // add the defined ending to the text - $truncate .= $ending; + $truncate .= $ending; // close all unclosed html-tags foreach ($open_tags as $tag) { $truncate .= ''; } - return $truncate; + return $truncate; } Index: course/moodleform_mod.php =================================================================== RCS file: /cvsroot/moodle/moodle/course/moodleform_mod.php,v retrieving revision 1.32 diff -u -r1.32 moodleform_mod.php --- course/moodleform_mod.php 1 Jun 2008 21:36:14 -0000 1.32 +++ course/moodleform_mod.php 28 Jul 2008 12:05:03 -0000 @@ -32,6 +32,11 @@ * List of modform features */ var $_features; + + /** + * @var array Custom completion-rule elements, if enabled + */ + var $_customcompletionelements; function moodleform_mod($instance, $section, $cm) { $this->_instance = $instance; @@ -117,6 +122,56 @@ $mform->removeElement('groupingid'); } } + + // Completion: If necessary, freeze fields + $completion=new completion_info($COURSE); + if($completion->is_enabled()) { + // If anybody has completed the activity, these options will be 'locked' + $completedcount = empty($this->_cm) + ? 0 + : $completion->count_user_data($this->_cm); + + $freeze=false; + if(!$completedcount) { + if($mform->elementExists('unlockcompletion')) { + $mform->removeElement('unlockcompletion'); + } + } else { + // Has the element been unlocked? + if($mform->exportValue('unlockcompletion')) { + // Yes, add in warning text and set the hidden variable + $mform->insertElementBefore( + $mform->createElement('static','completedunlocked', + get_string('completedunlocked','completion'), + get_string('completedunlockedtext','completion')), + 'unlockcompletion'); + $mform->removeElement('unlockcompletion'); + $mform->getElement('completionunlocked')->setValue(1); + } else { + // No, add in the warning text with the count (now we know + // it) before the unlock button + $mform->insertElementBefore( + $mform->createElement('static','completedwarning', + get_string('completedwarning','completion'), + get_string('completedwarningtext','completion',$completedcount)), + 'unlockcompletion'); + $mform->setHelpButton('completedwarning', array('completionlocked', get_string('help_completionlocked', 'completion'), 'completion')); + + $freeze=true; + } + } + + if($freeze) { + $mform->freeze('completion'); + if($mform->elementExists('completionview')) { + $mform->freeze('completionview'); // don't use hardFreeze or checkbox value gets lost + } + if($mform->elementExists('completionusegrade')) { + $mform->freeze('completionusegrade'); + } + $mform->freeze($this->_customcompletionelements); + } + } } // form verification @@ -149,6 +204,15 @@ $errors['cmidnumber'] = get_string('idnumbertaken'); } } + + // Completion: Don't let them choose automatic completion without turning + // on some conditions + if(array_key_exists('completion',$data) && $data['completion']==COMPLETION_TRACKING_AUTOMATIC) { + if(empty($data['completionview']) && empty($data['completionusegrade']) && + !$this->completion_rule_enabled($data)) { + $errors['completion']=get_string('badautocompletion','completion'); + } + } return $errors; } @@ -171,12 +235,23 @@ /** * Adds all the standard elements to a form to edit the settings for an activity module. * - * @param mixed array or object describing supported features - groups, groupings, groupmembersonly, etc. + * @param mixed $features array or object describing supported features - groups, groupings, groupmembersonly, etc. + * @param string $modname Name of module e.g. 'label' */ - function standard_coursemodule_elements($features=null){ + function standard_coursemodule_elements($features=null,$modname=null){ global $COURSE, $CFG, $DB; $mform =& $this->_form; + // Guess module name if not supplied + if(!$modname) { + $matches=array(); + if(!preg_match('/^mod_([^_]+)_mod_form$/',$this->_formname,$matches)) { + debugging('Use $modname parameter or rename form to mod_xx_mod_form, where xx is name of your module'); + error('Unknown module name for form'); + } + $modname=$matches[1]; + } + // deal with legacy $supportgroups param if ($features === true or $features === false) { $groupmode = $features; @@ -216,6 +291,10 @@ if (!isset($this->_features->idnumber)) { $this->_features->idnumber = true; } + + if(!isset($this->_features->defaultcompletion)) { + $this->_features->defaultcompletion = true; + } $outcomesused = false; if (!empty($CFG->enableoutcomes) and $this->_features->outcomes) { @@ -269,8 +348,94 @@ $mform->addElement('select', 'gradecat', get_string('gradecategory', 'grades'), $categories); } + // Conditional activities: completion tracking section + require_once($CFG->libdir.'/completionlib.php'); + $completion=new completion_info($COURSE); + if($completion->is_enabled()) { + $mform->addElement('header', '', get_string('activitycompletion', 'completion')); + + // Unlock button for if people have completed it (will + // be removed in definition_after_data if they haven't) + $mform->addElement('submit','unlockcompletion',get_string('unlockcompletion','completion')); + $mform->registerNoSubmitButton('unlockcompletion'); + $mform->addElement('hidden','completionunlocked',0); + + $mform->addElement('select', 'completion', get_string('completion','completion'), + array(COMPLETION_TRACKING_NONE=>get_string('completion_none','completion'), + COMPLETION_TRACKING_MANUAL=>get_string('completion_manual','completion'))); + $mform->setHelpButton('completion', array('completion', get_string('help_completion', 'completion'), 'completion')); + $mform->setDefault('completion',$this->_features->defaultcompletion + ? COMPLETION_TRACKING_MANUAL + : COMPLETION_TRACKING_NONE); + + // Automatic completion once you view it + $gotcompletionoptions=false; + if(plugin_supports('mod',$modname,FEATURE_COMPLETION_TRACKS_VIEWS)) { + $mform->addElement('checkbox', 'completionview', get_string('completionview','completion'), + get_string('completionview_text','completion')); + $mform->setHelpButton('completionview', array('completionview', get_string('help_completionview', 'completion'), 'completion')); + $mform->disabledIf('completionview','completion','ne',COMPLETION_TRACKING_AUTOMATIC); + $gotcompletionoptions=true; + } + + // Automatic completion once it's graded + if(plugin_supports('mod',$modname,FEATURE_GRADE_HAS_GRADE)) { + $mform->addElement('checkbox', 'completionusegrade', get_string('completionusegrade','completion'), + get_string('completionusegrade_text','completion')); + $mform->setHelpButton('completionusegrade', array('completionusegrade', get_string('help_completionusegrade', 'completion'), 'completion')); + $mform->disabledIf('completionusegrade','completion','ne',COMPLETION_TRACKING_AUTOMATIC); + $gotcompletionoptions=true; + } + + // Automatic completion according to module-specific rules + $this->_customcompletionelements = $this->add_completion_rules(); + foreach($this->_customcompletionelements as $element) { + $mform->disabledIf($element,'completion','ne',COMPLETION_TRACKING_AUTOMATIC); + } + + $gotcompletionoptions = $gotcompletionoptions || + count($this->_customcompletionelements)>0; + + // Automatic option only appears if possible + if($gotcompletionoptions) { + $mform->getElement('completion')->addOption( + get_string('completion_automatic','completion'), + COMPLETION_TRACKING_AUTOMATIC); + } + + // Completion expected at particular date? (For progress tracking) + $mform->addElement('date_selector', 'completionexpected', get_string('completionexpected','completion'), array('optional'=>true)); + $mform->setHelpButton('completionexpected', array('completionexpected', get_string('help_completionexpected', 'completion'), 'completion')); + $mform->disabledIf('completionexpected','completion','eq',COMPLETION_TRACKING_NONE); + } + $this->standard_hidden_coursemodule_elements(); } + + /** + * Can be overridden to add custom completion rules if the module wishes + * them. If overriding this, you should also override completion_rule_enabled. + *

+ * Just add elements to the form as needed and return the list of IDs. The + * system will call disabledIf and handle other behaviour for each returned + * ID. + * @return array Array of string IDs of added items, empty array if none + */ + function add_completion_rules() { + return array(); + } + + /** + * Called during validation. Override to indicate, based on the data, whether + * a custom completion rule is enabled (selected). + * + * @param array $data Input data (not yet validated) + * @return bool True if one or more rules is enabled, false if none are; + * default returns false + */ + function completion_rule_enabled(&$data) { + return false; + } function standard_hidden_coursemodule_elements(){ $mform =& $this->_form; Index: course/view.php =================================================================== RCS file: /cvsroot/moodle/moodle/course/view.php,v retrieving revision 1.113 diff -u -r1.113 view.php --- course/view.php 2 Jun 2008 08:13:25 -0000 1.113 +++ course/view.php 28 Jul 2008 12:05:04 -0000 @@ -195,6 +195,15 @@ $PAGE->print_header(get_string('course').': %fullname%', NULL, '', $bodytags); + $completion=new completion_info($course); + if($completion->is_enabled() && ajaxenabled()) { + require_js(array('yui_yahoo','yui_event','yui_connection','yui_dom')); + // Need to do this after the header because it requires the YUI stuff + // to be loaded already + print ''. + ''; + } + // Course wrapper start. echo '

'; Index: course/edit_form.php =================================================================== RCS file: /cvsroot/moodle/moodle/course/edit_form.php,v retrieving revision 1.48 diff -u -r1.48 edit_form.php --- course/edit_form.php 1 Jun 2008 15:44:59 -0000 1.48 +++ course/edit_form.php 28 Jul 2008 12:05:03 -0000 @@ -351,6 +351,18 @@ $mform->addElement('select', 'lang', get_string('forcelanguage'), $languages); //-------------------------------------------------------------------------------- + require_once($CFG->libdir.'/completionlib.php'); + if(completion_info::is_enabled_for_site()) { + $mform->addElement('header','', get_string('progress','completion')); + $mform->addElement('select', 'enablecompletion', get_string('completion','completion'), + array(0=>get_string('completiondisabled','completion'), 1=>get_string('completionenabled','completion'))); + $mform->setDefault('enablecompletion',1); + } else { + $mform->addElement('hidden', 'enablecompletion'); + $mform->setDefault('enablecompletion',0); + } + +//-------------------------------------------------------------------------------- if (has_capability('moodle/site:config', $systemcontext) && ((!empty($course->requested) && $CFG->restrictmodulesfor == 'requested') || $CFG->restrictmodulesfor == 'all')) { $mform->addElement('header', '', get_string('restrictmodules')); Index: course/modedit.php =================================================================== RCS file: /cvsroot/moodle/moodle/course/modedit.php,v retrieving revision 1.55 diff -u -r1.55 modedit.php --- course/modedit.php 21 Jul 2008 13:00:41 -0000 1.55 +++ course/modedit.php 28 Jul 2008 12:05:03 -0000 @@ -118,6 +118,10 @@ $form->instance = $cm->instance; $form->return = $return; $form->update = $update; + $form->completion = $cm->completion; + $form->completionview = $cm->completionview; + $form->completionexpected = $cm->completionexpected; + $form->completionusegrade = is_null($cm->completiongradeitemnumber) ? 0 : 1; if ($items = grade_item::fetch_all(array('itemtype'=>'mod', 'itemmodule'=>$form->modulename, 'iteminstance'=>$form->instance, 'courseid'=>$COURSE->id))) { @@ -241,6 +245,19 @@ if (!isset($fromform->name)) { //label $fromform->name = $fromform->modulename; } + + if (!isset($fromform->completion)) { + $fromform->completion=COMPLETION_DISABLED; + } + if (!isset($fromform->completionview)) { + $fromform->completionview=COMPLETION_VIEW_NOT_REQUIRED; + } + + // Convert the 'use grade' checkbox into a grade-item number: 0 if + // checked, null if not + $fromform->completiongradeitemnumber = + isset($fromform->completionusegrade) && $fromform->completionusegrade + ? 0 : null; if (!empty($fromform->update)) { @@ -262,6 +279,18 @@ set_coursemodule_groupingid($fromform->coursemodule, $fromform->groupingid); set_coursemodule_groupmembersonly($fromform->coursemodule, $fromform->groupmembersonly); + // Handle completion settings. If necessary, wipe existing completion + // data first. + if(!empty($fromform->completionunlocked)) { + $completion=new completion_info($course); + $completion->reset_all_state($cm); + } + set_coursemodule_completion($fromform->coursemodule, $fromform->completion); + set_coursemodule_completionview($fromform->coursemodule, $fromform->completionview); + set_coursemodule_completionexpected($fromform->coursemodule, $fromform->completionexpected); + set_coursemodule_completiongradeitemnumber( + $fromform->coursemodule,$fromform->completiongradeitemnumber); + if (isset($fromform->cmidnumber)) { //label // set cm idnumber set_coursemodule_idnumber($fromform->coursemodule, $fromform->cmidnumber); Index: course/lib.php =================================================================== RCS file: /cvsroot/moodle/moodle/course/lib.php,v retrieving revision 1.609 diff -u -r1.609 lib.php --- course/lib.php 26 Jul 2008 11:36:35 -0000 1.609 +++ course/lib.php 28 Jul 2008 12:05:03 -0000 @@ -1,6 +1,7 @@ libdir.'/completionlib.php'); define('COURSE_MAX_LOG_DISPLAY', 150); // days define('COURSE_MAX_LOGS_PER_PAGE', 1000); // records @@ -1276,7 +1277,7 @@ /** * Prints a section full of activity modules */ -function print_section($course, $section, $mods, $modnamesused, $absolute=false, $width="100%") { +function print_section($course, $section, $mods, $modnamesused, $absolute=false, $width="100%", $hidecompletion=false) { global $CFG, $USER, $DB; static $initialised; @@ -1365,7 +1366,7 @@ $extra = ''; if (!empty($modinfo->cms[$modnumber]->extra)) { - $extra = $modinfo->cms[$modnumber]->extra; + $extra = $modinfo->cms[$modnumber]->extra; } if ($mod->modname == "label") { @@ -1453,6 +1454,66 @@ echo '  '; echo make_editing_buttons($mod, $absolute, true, $mod->indent, $section->section); } + + // Completion + $completioninfo=new completion_info($course); + $completion=$hidecompletion + ? COMPLETION_TRACKING_NONE + : $completioninfo->is_enabled($mod); + if($completion!=COMPLETION_TRACKING_NONE) { + $completiondata=$completioninfo->get_data($mod,true); + $completionicon=''; + if($isediting) { + switch($completion) { + case COMPLETION_TRACKING_MANUAL : + $completionicon='manual-enabled'; break; + case COMPLETION_TRACKING_AUTOMATIC : + $completionicon='auto-enabled'; break; + default: // wtf + } + } else if($completion==COMPLETION_TRACKING_MANUAL) { + switch($completiondata->completionstate) { + case COMPLETION_INCOMPLETE: + $completionicon='manual-n'; break; + case COMPLETION_COMPLETE: + $completionicon='manual-y'; break; + } + } else { // Automatic + switch($completiondata->completionstate) { + case COMPLETION_INCOMPLETE: + $completionicon='auto-n'; break; + case COMPLETION_COMPLETE: + $completionicon='auto-y'; break; + case COMPLETION_COMPLETE_PASS: + $completionicon='auto-pass'; break; + case COMPLETION_COMPLETE_FAIL: + $completionicon='auto-fail'; break; + } + } + if($completionicon) { + $imgsrc=$CFG->pixpath.'/i/completion-'.$completionicon.'.gif'; + $imgalt=get_string('completion-alt-'.$completionicon,'completion'); + if($completion==COMPLETION_TRACKING_MANUAL && !$isediting) { + $imgtitle=get_string('completion-title-'.$completionicon,'completion'); + $newstate= + $completiondata->completionstate==COMPLETION_COMPLETE + ? COMPLETION_INCOMPLETE + : COMPLETION_COMPLETE; + // In manual mode the icon is a toggle form. + echo " +
+ + + +
"; + } else { + // In auto mode, or when editing, the icon is just an image + echo " +$imgalt"; + } + } + } + echo "\n"; } @@ -2228,6 +2289,27 @@ global $DB; return $DB->set_field("course_modules", "idnumber", $idnumber, array("id"=>$id)); } + +function set_coursemodule_completion($id, $completion) { + global $DB; + return $DB->set_field("course_modules", "completion", $completion, array('id'=>$id)); +} + +function set_coursemodule_completionview($id, $completionview) { + global $DB; + return $DB->set_field("course_modules", "completionview", $completionview, array('id'=>$id)); +} + +function set_coursemodule_completiongradeitemnumber($id, $completiongradeitemnumber) { + global $DB; + return $DB->set_field("course_modules", "completiongradeitemnumber", $completiongradeitemnumber, array('id'=>$id)); +} + +function set_coursemodule_completionexpected($id, $completionexpected) { + global $DB; + return $DB->set_field("course_modules", "completionexpected", $completionexpected, array('id'=>$id)); +} + /** * $prevstateoverrides = true will set the visibility of the course module * to what is defined in visibleold. This enables us to remember the current Index: course/report.php =================================================================== RCS file: /cvsroot/moodle/moodle/course/report.php,v retrieving revision 1.8 diff -u -r1.8 report.php --- course/report.php 1 Jun 2008 17:59:13 -0000 1.8 +++ course/report.php 28 Jul 2008 12:05:03 -0000 @@ -11,7 +11,8 @@ require_login($course->id); - require_capability('moodle/site:viewreports', get_context_instance(CONTEXT_COURSE, $course->id)); + $context=get_context_instance(CONTEXT_COURSE, $course->id); + require_capability('moodle/site:viewreports', $context); $strreports = get_string('reports'); Index: lib/grade/grade_grade.php =================================================================== RCS file: /cvsroot/moodle/moodle/lib/grade/grade_grade.php,v retrieving revision 1.43 diff -u -r1.43 grade_grade.php --- lib/grade/grade_grade.php 30 May 2008 17:43:00 -0000 1.43 +++ lib/grade/grade_grade.php 28 Jul 2008 12:05:07 -0000 @@ -735,5 +735,59 @@ $this->rawgrademax = grade_floatval($this->rawgrademax); return parent::update($source); } + + /** + * Used to notify the completion system (if necessary) that a user's grade + * has changed. + * @param bool deleted True if grade was actually deleted + */ + function notify_changed($deleted) { + // Ignore during restore + global $restore; + if(!empty($restore->backup_unique_code)) { + return; + } + global $CFG,$COURSE,$DB; + require_once($CFG->libdir.'/completionlib.php'); + + // Use $COURSE if available otherwise get it via item fields + if(!empty($COURSE)) { + $course=$COURSE; + } else { + $this->load_grade_item(); + $course=get_record('course','id',$grade_item->courseid); + } + + // Bail out immediately if completion is not enabled for course + $completion=new completion_info($course); + if(!$completion->is_enabled()) { + return; + } + + // Get the grade item and course-module which we will need + $this->load_grade_item(); + if($this->grade_item->itemtype!='mod') { + return; + } + $cm=$DB->get_record_sql(" +SELECT + cm.*,m.name AS modname +FROM + {$CFG->prefix}modules m + INNER JOIN {$CFG->prefix}course_modules cm ON m.id=cm.module +WHERE + m.name=? AND cm.instance=? AND cm.course=?", + array($this->grade_item->itemmodule,$this->grade_item->iteminstance, + $this->grade_item->courseid)); + if(!$cm) { + debugging("Couldn't find course-module for module + '{$this->grade_item->itemmodule}', instance '{$this->grade_item->iteminstance}', + course '{$this->grade_item->courseid}'"); + return; + } + + // Pass information on to completion system + $completion->inform_grade_changed($cm,$this->grade_item,$this,$deleted); + } } ?> Index: lib/grade/grade_object.php =================================================================== RCS file: /cvsroot/moodle/moodle/lib/grade/grade_object.php,v retrieving revision 1.45 diff -u -r1.45 grade_object.php --- lib/grade/grade_object.php 9 Jun 2008 16:53:59 -0000 1.45 +++ lib/grade/grade_object.php 28 Jul 2008 12:05:07 -0000 @@ -173,6 +173,7 @@ global $DB; if ($datas = $DB->get_records_select($table, $wheresql, $params)) { + $result = array(); foreach($datas as $data) { $instance = new $classname(); @@ -182,6 +183,7 @@ return $result; } else { + return false; } } @@ -215,6 +217,7 @@ $DB->insert_record($this->table.'_history', $data); } + $this->notify_changed(false); return true; } @@ -244,6 +247,7 @@ $data->userlogged = $USER->id; $DB->insert_record($this->table.'_history', $data); } + $this->notify_changed(true); return true; } else { @@ -306,6 +310,7 @@ $DB->insert_record($this->table.'_history', $data); } + $this->notify_changed(false); return $this->id; } @@ -344,5 +349,15 @@ } } } + + /** + * Called immediately after the object data has been inserted, updated, or + * deleted in the database. Default does nothing, can be overridden to + * hook in special behaviour. + * + * @param bool $deleted + */ + function notify_changed($deleted) { + } } ?> Index: theme/standard/styles_layout.css =================================================================== RCS file: /cvsroot/moodle/moodle/theme/standard/styles_layout.css,v retrieving revision 1.592 diff -u -r1.592 styles_layout.css --- theme/standard/styles_layout.css 25 Jul 2008 08:14:13 -0000 1.592 +++ theme/standard/styles_layout.css 28 Jul 2008 12:05:09 -0000 @@ -91,11 +91,11 @@ } #redirect #message { - + } #redirect #continue { - + } /* .clearfix {display: inline-table;} */ @@ -1803,6 +1803,29 @@ #course-view .section .weekdates { } +#course-view li.activity { + margin-right:20px; /* Space allowed for completion icons if enabled */ + position:relative; +} +#course-view li.activity form.togglecompletion, +#course-view li.activity span.autocompletion { + display:inline; + position:absolute; + right:-20px; + z-index:10; +} +#course-view li.activity form.togglecompletion div { + display:inline; +} +#course-view .completion-saved-display { + position:absolute; + top:0; left:0; + border:1px solid black; + padding: 1px 2px; + background:white; + font-size:0.85em; +} + #course-view ul.section, #site-index ul.section { margin: 0; @@ -1845,7 +1868,7 @@ padding: 0; } .weeks-format .block_calendar_month .minicalendar th, -.topics-format .block_calendar_month .minicalendar th, +.topics-format .block_calendar_month .minicalendar th, .weeks-format .block_calendar_month .minicalendar td, .topics-format .block_calendar_month .minicalendar td { padding: 0.1em 0 0.1em 1px; @@ -1896,7 +1919,7 @@ .weeks .right, .topics .right { float: right; -} +} .section .activity img.activityicon { vertical-align:middle; @@ -2277,7 +2300,7 @@ } #grade-aggregation-help dt { - margin-top: 15px; + margin-top: 15px; } #grade-aggregation-help dd.example { @@ -2685,6 +2708,36 @@ } /*** + *** Completion progress report + ***/ + +#course-report-progress-index th, +#course-report-progress-index td { + padding:2px 4px; + font-weight:normal; + border-right: 1px solid #EEE; +} +.completion-expired { + background:#fdd; +} +.completion-expected { + font-size:0.75em; +} +.completion-sortchoice { + font-size:0.75em; + vertical-align:bottom; +} +.completion-progresscell { + text-align:right; +} +.completion-expired .completion-expected { + font-weight:bold; +} +#course-report-progress-index .progress-actions { + text-align:center; +} + +/*** *** Logs ***/ @@ -2778,7 +2831,7 @@ } -.tabrow0 .here a:link, +.tabrow0 .here a:link, .tabrow0 .here a:visited, .tabrow0 .here a.nolink { position:relative; @@ -4035,17 +4088,17 @@ padding: 4px; } -#mod-quiz-grading table#grading .header .commands +#mod-quiz-grading table#grading .header .commands { display: inline; } -#mod-quiz-grading table#grading .picture +#mod-quiz-grading table#grading .picture { width: 40px; } -#mod-quiz-grading table#grading td +#mod-quiz-grading table#grading td { border-left-width: 1px; border-right-width: 1px; Index: backup/restorelib.php =================================================================== RCS file: /cvsroot/moodle/moodle/backup/restorelib.php,v retrieving revision 1.345 diff -u -r1.345 restorelib.php --- backup/restorelib.php 27 Jul 2008 16:27:10 -0000 1.345 +++ backup/restorelib.php 28 Jul 2008 12:05:03 -0000 @@ -752,6 +752,7 @@ $course->enrolenddate += $restore->course_startdateoffset; } $course->enrolperiod = $course_header->course_enrolperiod; + $course->enablecompletion = isset($course_header->course_enablecompletion) ? $course_header->course_enablecompletion : 0; //Put as last course in category $course->sortorder = $category->sortorder + MAX_COURSES_IN_CATEGORY - 1; @@ -1119,11 +1120,17 @@ //print_object($course_module); //Debug //Save it to db if ($mod->idnumber) { + $mod->idnumber=backup_todb($mod->idnumber); if (grade_verify_idnumber($mod->idnumber, $restore->course_id)) { $course_module->idnumber = $mod->idnumber; } } + $course_module->completion=$mod->completion; + $course_module->completiongradeitemnumber=backup_todb($mod->completiongradeitemnumber); + $course_module->completionview=$mod->completionview; + $course_module->completionexpected=$mod->completionexpected; + $newidmod = $DB->insert_record("course_modules", $course_module); if ($newidmod) { //save old and new module id @@ -1162,6 +1169,43 @@ } } } + + // Now that we have IDs for everything, store any completion data + if($status && !empty($info->completiondata)) { + foreach($info->completiondata as $data) { + // Convert cmid + $newcmid=backup_getid($restore->backup_unique_code, 'course_modules', $data->coursemoduleid); + if($newcmid) { + $data->coursemoduleid=$newcmid->new_id; + } else { + if (!defined('RESTORE_SILENTLY')) { + echo "

Can't find new ID for cm $data->coursemoduleid.

"; + } + $status=false; + continue; + } + + // Convert userid + $newuserid=backup_getid($restore->backup_unique_code, 'user', $data->userid); + if($newuserid) { + $data->userid=$newuserid->new_id; + } else { + // Skip missing users + debugging("Not restoring completion data for missing user {$data->userid}",DEBUG_DEVELOPER); + continue; + } + + // Add record + if(!$DB->insert_record('course_modules_completion',$data)) { + if (!defined('RESTORE_SILENTLY')) { + echo "

Failed to insert completion data record.

"; + } + $status=false; + continue; + } + } + } + } else { $status = false; } @@ -5164,6 +5208,9 @@ case "ENROLPERIOD": $this->info->course_enrolperiod = $this->getContents(); break; + case "ENABLECOMPLETION": + $this->info->course_enablecompletion = $this->getContents(); + break; } } if ($this->tree[4] == "CATEGORY") { @@ -5539,7 +5586,15 @@ $this->info->tempmod->groupmembersonly; $this->info->tempsection->mods[$this->info->tempmod->id]->idnumber = $this->info->tempmod->idnumber; - + $this->info->tempsection->mods[$this->info->tempmod->id]->completion = + isset($this->info->tempmod->completion) ? $this->info->tempmod->completion : 0; + $this->info->tempsection->mods[$this->info->tempmod->id]->completiongradeitemnumber = + isset($this->info->tempmod->completiongradeitemnumber) ? $this->info->tempmod->completiongradeitemnumber : null; + $this->info->tempsection->mods[$this->info->tempmod->id]->completionview = + isset($this->info->tempmod->completionview) ? $this->info->tempmod->completionview : 0; + $this->info->tempsection->mods[$this->info->tempmod->id]->completionexpected = + isset($this->info->tempmod->completionexpected) ? $this->info->tempmod->completionexpected : 0; + unset($this->info->tempmod); } } @@ -5577,6 +5632,18 @@ case "IDNUMBER": $this->info->tempmod->idnumber = $this->getContents(); break; + case "COMPLETION": + $this->info->tempmod->completion = $this->getContents(); + break; + case "COMPLETIONGRADEITEMNUMBER": + $this->info->tempmod->completiongradeitemnumber = $this->getContents(); + break; + case "COMPLETIONVIEW": + $this->info->tempmod->completionview = $this->getContents(); + break; + case "COMPLETIONEXPECTED": + $this->info->tempmod->completionexpected = $this->getContents(); + break; default: break; } @@ -5672,6 +5739,36 @@ } } /// ends role_overrides + if (isset($this->tree[7]) && $this->tree[7] == "COMPLETIONDATA") { + if($this->level == 8) { + switch($tagName) { + case 'COMPLETION': + // Got all data to make completion entry... + $this->info->tempcompletion->coursemoduleid=$this->info->tempmod->id; + $this->info->completiondata[]=$this->info->tempcompletion; + unset($this->info->tempcompletion); + $this->info->tempcompletion=new stdClass; + break; + } + } + + if($this->level == 9) { + switch($tagName) { + case 'USERID' : + $this->info->tempcompletion->userid=$this->getContents(); + break; + case 'COMPLETIONSTATE' : + $this->info->tempcompletion->completionstate=$this->getContents(); + break; + case 'VIEWED' : + $this->info->tempcompletion->viewed=$this->getContents(); + break; + case 'TIMEMODIFIED' : + $this->info->tempcompletion->timemodified=$this->getContents(); + break; + } + } + } } //Stop parsing if todo = SECTIONS and tagName = SECTIONS (en of the tag, of course) @@ -8338,22 +8435,22 @@ } /// Now, restore role nameincourse (only if the role had nameincourse in backup) if (!empty($roledata->nameincourse)) { - $newrole = backup_getid($restore->backup_unique_code, 'role', $oldroleid); /// Look for target role - $coursecontext = get_context_instance(CONTEXT_COURSE, $restore->course_id); /// Look for target context - if (!empty($newrole->new_id) && !empty($coursecontext) && !empty($roledata->nameincourse)) { - /// Check the role hasn't any custom name in context - if (!$DB->record_exists('role_names', array('roleid'=>$newrole->new_id, 'contextid'=>$coursecontext->id))) { - $rolename = new object(); - $rolename->roleid = $newrole->new_id; - $rolename->contextid = $coursecontext->id; - $rolename->name = $roledata->nameincourse; + $newrole = backup_getid($restore->backup_unique_code, 'role', $oldroleid); /// Look for target role + $coursecontext = get_context_instance(CONTEXT_COURSE, $restore->course_id); /// Look for target context + if (!empty($newrole->new_id) && !empty($coursecontext) && !empty($roledata->nameincourse)) { + /// Check the role hasn't any custom name in context + if (!$DB->record_exists('role_names', array('roleid'=>$newrole->new_id, 'contextid'=>$coursecontext->id))) { + $rolename = new object(); + $rolename->roleid = $newrole->new_id; + $rolename->contextid = $coursecontext->id; + $rolename->name = $roledata->nameincourse; - $DB->insert_record('role_names', $rolename); - } + $DB->insert_record('role_names', $rolename); } } } } + } return true; } Index: backup/backuplib.php =================================================================== RCS file: /cvsroot/moodle/moodle/backup/backuplib.php,v retrieving revision 1.215 diff -u -r1.215 backuplib.php --- backup/backuplib.php 2 Jul 2008 22:47:57 -0000 1.215 +++ backup/backuplib.php 28 Jul 2008 12:05:01 -0000 @@ -716,7 +716,8 @@ fwrite ($bf,full_tag("ENROLSTARTDATE",3,false,$course->enrolstartdate)); fwrite ($bf,full_tag("ENROLENDDATE",3,false,$course->enrolenddate)); fwrite ($bf,full_tag("ENROLPERIOD",3,false,$course->enrolperiod)); - + fwrite ($bf,full_tag("ENABLECOMPLETION",3,false,$course->enablecompletion)); + /// write local course overrides here? write_role_overrides_xml($bf, $context, 3); /// write role_assign code here @@ -1162,6 +1163,11 @@ $status = true; $first_record = true; + + $course=$DB->get_record('course',array('id'=>$preferences->backup_course)); + if(!$course) { + return false; + } //Now print the mods in section //Extracts mod id from sequence @@ -1221,11 +1227,36 @@ fwrite ($bf,full_tag("GROUPINGID",6,false,$course_module->groupingid)); fwrite ($bf,full_tag("GROUPMEMBERSONLY",6,false,$course_module->groupmembersonly)); fwrite ($bf,full_tag("IDNUMBER",6,false,$course_module->idnumber)); + fwrite ($bf,full_tag("COMPLETION",6,false,$course_module->completion)); + fwrite ($bf,full_tag("COMPLETIONGRADEITEMNUMBER",6,false,$course_module->completiongradeitemnumber)); + fwrite ($bf,full_tag("COMPLETIONVIEW",6,false,$course_module->completionview)); + fwrite ($bf,full_tag("COMPLETIONEXPECTED",6,false,$course_module->completionexpected)); // get all the role_capabilities overrides in this mod write_role_overrides_xml($bf, $context, 6); /// write role_assign code here - write_role_assignments_xml($bf, $preferences, $context, 6); - /// write role_assign code here + write_role_assignments_xml($bf, $preferences, $context, 6); + // write completion data if enabled and user data enabled + require_once($CFG->libdir.'/completionlib.php'); + $completion=new completion_info($course); + if($completion->is_enabled($course_module) && + backup_userdata_selected($preferences,$moduletype,$course_module->instance)) { + fwrite ($bf,start_tag("COMPLETIONDATA",6,true)); + + // Get all completion records for this module and loop + $data=$DB->get_records('course_modules_completion',array('coursemoduleid'=>$course_module->id)); + $data=$data ? $data : array(); + foreach($data as $completion) { + // Write completion record + fwrite ($bf,start_tag("COMPLETION",7,true)); + fwrite ($bf,full_tag("USERID",8,false,$completion->userid)); + fwrite ($bf,full_tag("COMPLETIONSTATE",8,false,$completion->completionstate)); + fwrite ($bf,full_tag("VIEWED",8,false,$completion->viewed)); + fwrite ($bf,full_tag("TIMEMODIFIED",8,false,$completion->timemodified)); + fwrite ($bf,end_tag("COMPLETION",7,true)); + } + + fwrite ($bf,end_tag("COMPLETIONDATA",6,true)); + } fwrite ($bf,end_tag("MOD",5,true)); } @@ -1240,7 +1271,7 @@ return $status; } - + //Print users to xml //Only users previously calculated in backup_ids will output // Index: lang/en_utf8/forum.php =================================================================== RCS file: /cvsroot/moodle/moodle/lang/en_utf8/forum.php,v retrieving revision 1.39 diff -u -r1.39 forum.php --- lang/en_utf8/forum.php 25 Jul 2008 12:30:08 -0000 1.39 +++ lang/en_utf8/forum.php 28 Jul 2008 12:05:04 -0000 @@ -40,6 +40,15 @@ $string['cannotinsertrate'] = 'Could not insert a new rating ($a[0] = $a[1])'; $string['cannottrack'] = 'Could not stop tracking that forum'; $string['cleanreadtime'] = 'Mark old posts as read hour'; +$string['completiondiscussions'] = 'User must create discussions:'; +$string['completiondiscussionshelp'] = 'requiring discussions to complete'; +$string['completiondiscussionsgroup'] = 'Require discussions'; +$string['completionposts'] = 'User must post discussions or replies:'; +$string['completionpostshelp'] = 'requiring discussions or replies to complete'; +$string['completionpostsgroup'] = 'Require posts'; +$string['completionreplies'] = 'User must post replies:'; +$string['completionreplieshelp'] = 'requiring replies to complete'; +$string['completionrepliesgroup'] = 'Require replies'; $string['configcleanreadtime'] = 'The hour of the day to clean old posts from the \'read\' table.'; $string['configdisplaymode'] = 'The default display mode for discussions if one isn\'t set.'; $string['configenablerssfeeds'] = 'This switch will enable the possibility of RSS feeds for all forums. You will still need to turn feeds on manually in the settings for each forum.'; @@ -87,11 +96,11 @@ $string['editing'] = 'Editing'; $string['emptymessage'] = 'Something was wrong with your post. Perhaps you left it blank, or the attachment was too big. Your changes have NOT been saved.'; $string['everyonecanchoose'] = 'Everyone can choose to be subscribed'; -$string['everyonecannowchoose'] = 'Everyone can now choose to be subscribed'; +$string['everyonecannowchoose'] = 'Everyone can now choose to be subscribed'; $string['everyoneisnowsubscribed'] = 'Everyone is now subscribed to this forum'; $string['everyoneissubscribed'] = 'Everyone is subscribed to this forum'; $string['existingsubscribers'] = 'Existing subscribers'; -$string['forcessubscribe'] = 'This forum forces everyone to be subscribed'; +$string['forcessubscribe'] = 'This forum forces everyone to be subscribed'; $string['forcesubscribe'] = 'Force everyone to be subscribed'; $string['forcesubscribeq'] = 'Force everyone to be subscribed?'; $string['forum'] = 'Forum'; Index: version.php =================================================================== RCS file: /cvsroot/moodle/moodle/version.php,v retrieving revision 1.737 diff -u -r1.737 version.php --- version.php 28 Jul 2008 01:37:25 -0000 1.737 +++ version.php 28 Jul 2008 12:05:00 -0000 @@ -6,7 +6,7 @@ // This is compared against the values stored in the database to determine // whether upgrades should be performed (see lib/db/*.php) - $version = 2008072600; // YYYYMMDD = date of the last version bump + $version = 2008072800; // YYYYMMDD = date of the last version bump // XX = daily increments $release = '2.0 dev (Build: 20080728)'; // Human-friendly version name Index: admin/settings/misc.php =================================================================== RCS file: /cvsroot/moodle/moodle/admin/settings/misc.php,v retrieving revision 1.19 diff -u -r1.19 misc.php --- admin/settings/misc.php 25 May 2008 11:37:45 -0000 1.19 +++ admin/settings/misc.php 28 Jul 2008 12:05:00 -0000 @@ -13,6 +13,11 @@ $temp->add($item); $temp->add(new admin_setting_configcheckbox('enablegroupings', get_string('enablegroupings', 'admin'), get_string('configenablegroupings', 'admin'), 0)); + // Completion system + require_once($CFG->libdir.'/completionlib.php'); + $temp->add(new admin_setting_configcheckbox('enablecompletion',get_string('enablecompletion','completion'),get_string('configenablecompletion','completion'),COMPLETION_ENABLED)); + $temp->add(new admin_setting_pickroles('progresstrackedroles',get_string('progresstrackedroles','completion'),get_string('configprogresstrackedroles','completion'))); + $ADMIN->add('misc', $temp); // XMLDB editor Index: lib/simpletest/testcompletionlib.php =================================================================== RCS file: lib/simpletest/testcompletionlib.php diff -N lib/simpletest/testcompletionlib.php --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ lib/simpletest/testcompletionlib.php 1 Jan 1970 00:00:00 -0000 @@ -0,0 +1,676 @@ +libdir.'/completionlib.php'); + +global $DB; +Mock::generate(get_class($DB), 'mock_database'); + +Mock::generatePartial('completion_info','completion_cutdown', + array('delete_all_state','internal_get_tracked_users','update_state', + 'internal_get_grade_state','is_enabled','get_data','internal_get_state','internal_set_data')); +Mock::generatePartial('completion_info','completion_cutdown2', + array('is_enabled','get_data','internal_get_state','internal_set_data')); +Mock::generatePartial('completion_info','completion_cutdown3', + array('internal_get_grade_state')); + +class fake_recordset implements Iterator { + var $closed; + var $values,$index; + + function fake_recordset($values) { + $this->values=$values; + $this->index=0; + } + + function current() { + return $this->values[$this->index]; + } + + function key() { + return $this->values[$this->index]; + } + + function next() { + $this->index++; + } + + function rewind() { + $this->index=0; + } + + function valid() { + return count($this->values) > $this->index; + } + + function close() { + $closed=true; + } + + function was_closed() { + return $closed; + } +} + +/** + * Expectation that checks an object for given values (normal equality test) + * plus a 'timemodified' field that is current (last second or two). + */ +class TimeModifiedExpectation extends SimpleExpectation { + private $otherfields; + + /** + * @param array $otherfields Array key=>value of required object fields + */ + function TimeModifiedExpectation($otherfields) { + $this->otherfields=$otherfields; + } + + function test($thing) { + $thingfields=(array)$thing; + foreach($this->otherfields as $key=>$value) { + if(!array_key_exists($key,$thingfields)) { + return false; + } + if($thingfields[$key]!=$value) { + return false; + } + } + + $timedifference=time()-$thing->timemodified; + return ($timedifference < 2 && $timedifference>=0); + } + + function testMessage($thing) { + return "Object does not match fields/time requirement"; + } +} + +class completionlib_test extends UnitTestCase { + var $realdb,$realcfg,$realsession,$realuser; + + function setUp() { + global $DB,$CFG,$SESSION,$USER; + $this->realdb=$DB; + $this->realcfg=$CFG; + $this->realuser=$USER; + $DB=new mock_database(); + $CFG=clone($this->realcfg); + $CFG->prefix='test_'; + $CFG->enablecompletion=COMPLETION_ENABLED; + $SESSION=new stdClass(); + $USER=(object)array('id'=>314159); + } + + function tearDown() { + global $DB,$CFG,$SESSION,$USER; + $DB=$this->realdb; + $CFG=$this->realcfg; + $SESSION=$this->realsession; + $USER=$this->realuser; + } + + function test_is_enabled() { + global $CFG; + + // Config alone + $CFG->enablecompletion=COMPLETION_DISABLED; + $this->assertEqual(COMPLETION_DISABLED,completion_info::is_enabled_for_site()); + $CFG->enablecompletion=COMPLETION_ENABLED; + $this->assertEqual(COMPLETION_ENABLED,completion_info::is_enabled_for_site()); + + // Course + $course=new stdClass; + $c=new completion_info($course); + $course->enablecompletion=COMPLETION_DISABLED; + $this->assertEqual(COMPLETION_DISABLED,$c->is_enabled()); + $course->enablecompletion=COMPLETION_ENABLED; + $this->assertEqual(COMPLETION_ENABLED,$c->is_enabled()); + $CFG->enablecompletion=COMPLETION_DISABLED; + $this->assertEqual(COMPLETION_DISABLED,$c->is_enabled()); + + // Course and CM + $cm=new stdClass; + $cm->completion=COMPLETION_TRACKING_MANUAL; + $this->assertEqual(COMPLETION_DISABLED,$c->is_enabled($cm)); + $CFG->enablecompletion=COMPLETION_ENABLED; + $course->enablecompletion=COMPLETION_DISABLED; + $this->assertEqual(COMPLETION_DISABLED,$c->is_enabled($cm)); + $course->enablecompletion=COMPLETION_ENABLED; + $this->assertEqual(COMPLETION_TRACKING_MANUAL,$c->is_enabled($cm)); + $cm->completion=COMPLETION_TRACKING_NONE; + $this->assertEqual(COMPLETION_TRACKING_NONE,$c->is_enabled($cm)); + $cm->completion=COMPLETION_TRACKING_AUTOMATIC; + $this->assertEqual(COMPLETION_TRACKING_AUTOMATIC,$c->is_enabled($cm)); + } + + function test_update_state() { + $c=new completion_cutdown2(); + $c->completion_info((object)array('id'=>42)); + $cm=(object)array('id'=>13,'course'=>42); + + // Not enabled, should do nothing + $c->expectAt(0,'is_enabled',array($cm)); + $c->setReturnValueAt(0,'is_enabled',false); + $c->update_state($cm); + + // Enabled, but current state is same as possible result, do nothing + $current=(object)array('completionstate'=>COMPLETION_COMPLETE); + $c->expectAt(1,'is_enabled',array($cm)); + $c->setReturnValueAt(1,'is_enabled',true); + + $c->expectAt(0,'get_data',array($cm,false,0)); + $c->setReturnValueAt(0,'get_data',$current); + $c->update_state($cm,COMPLETION_COMPLETE); + + // Enabled, but current state is a specific one and new state is just + // omplete, so do nothing + $current->completionstate=COMPLETION_COMPLETE_PASS; + $c->expectAt(2,'is_enabled',array($cm)); + $c->setReturnValueAt(2,'is_enabled',true); + $c->expectAt(1,'get_data',array($cm,false,0)); + $c->setReturnValueAt(1,'get_data',$current); + $c->update_state($cm,COMPLETION_COMPLETE); + + // Manual, change state (no change) + $cm->completion=COMPLETION_TRACKING_MANUAL; + $current->completionstate=COMPLETION_COMPLETE; + $c->expectAt(3,'is_enabled',array($cm)); + $c->setReturnValueAt(3,'is_enabled',true); + $c->expectAt(2,'get_data',array($cm,false,0)); + $c->setReturnValueAt(2,'get_data',$current); + $c->update_state($cm,COMPLETION_COMPLETE); + + // Manual, change state (change) + $c->expectAt(4,'is_enabled',array($cm)); + $c->setReturnValueAt(4,'is_enabled',true); + $c->expectAt(3,'get_data',array($cm,false,0)); + $c->setReturnValueAt(3,'get_data',$current); + $c->expectAt(0,'internal_set_data',array($cm, + new TimeModifiedExpectation(array('completionstate'=>COMPLETION_INCOMPLETE)))); + $c->update_state($cm,COMPLETION_INCOMPLETE); + + // Auto, change state + $cm->completion=COMPLETION_TRACKING_AUTOMATIC; + $c->expectAt(5,'is_enabled',array($cm)); + $c->setReturnValueAt(5,'is_enabled',true); + $c->expectAt(4,'get_data',array($cm,false,0)); + $c->setReturnValueAt(4,'get_data',$current); + $c->expectAt(0,'internal_get_state',array($cm,0,$current)); + $c->setReturnValueAt(0,'internal_get_state',COMPLETION_COMPLETE_PASS); + $c->expectAt(1,'internal_set_data',array($cm, + new TimeModifiedExpectation(array('completionstate'=>COMPLETION_COMPLETE_PASS)))); + $c->update_state($cm,COMPLETION_COMPLETE_PASS); + + $c->tally(); + } + + function test_internal_get_state() { + global $DB; + + $c=new completion_cutdown3(); + $c->completion_info((object)array('id'=>42)); + $cm=(object)array('id'=>13,'course'=>42,'completiongradeitemnumber'=>null); + + // If view is required, but they haven't viewed it yet + $cm->completionview=COMPLETION_VIEW_REQUIRED; + $current=(object)array('viewed'=>COMPLETION_NOT_VIEWED); + $this->assertEqual(COMPLETION_INCOMPLETE,$c->internal_get_state($cm,123,$current)); + + // OK set view not required + $cm->completionview=COMPLETION_VIEW_NOT_REQUIRED; + + // Test not getting module name + $cm->modname='label'; + $this->assertEqual(COMPLETION_COMPLETE,$c->internal_get_state($cm,123,$current)); + + // Test getting module name + $cm->module=13; + unset($cm->modname); + $DB->expectOnce('get_field',array('modules','name',array('id'=>13))); + $DB->setReturnValue('get_field','label'); + $this->assertEqual(COMPLETION_COMPLETE,$c->internal_get_state($cm,123,$current)); + + // Note: This function is not fully tested (including kind of the main + // part) because: + // * the grade_item/grade_grade calls are static and can't be mocked + // * the plugin_supports call is static and can't be mocked + + $DB->tally(); + $c->tally(); + } + + function test_set_module_viewed() { + $c=new completion_cutdown(); + $c->completion_info((object)array('id'=>42)); + $cm=(object)array('id'=>13,'course'=>42); + + // Not tracking completion, should do nothing + $cm->completionview=COMPLETION_VIEW_NOT_REQUIRED; + $c->set_module_viewed($cm); + + // Tracking completion but completion is disabled, should do nothing + $cm->completionview=COMPLETION_VIEW_REQUIRED; + $c->expectAt(0,'is_enabled',array($cm)); + $c->setReturnValueAt(0,'is_enabled',false); + $c->set_module_viewed($cm); + + // Now it's enabled, we expect it to get data. If data already has + // viewed, still do nothing + $c->expectAt(1,'is_enabled',array($cm)); + $c->setReturnValueAt(1,'is_enabled',true); + $c->expectAt(0,'get_data',array($cm,0)); + $hasviewed=(object)array('viewed'=>COMPLETION_VIEWED); + $c->setReturnValueAt(0,'get_data',$hasviewed); + $c->set_module_viewed($cm); + + // OK finally one that hasn't been viewed, now it should set it viewed + // and update state + $c->expectAt(2,'is_enabled',array($cm)); + $c->setReturnValueAt(2,'is_enabled',true); + $notviewed=(object)array('viewed'=>COMPLETION_NOT_VIEWED); + $c->expectAt(1,'get_data',array($cm,1337)); + $c->setReturnValueAt(1,'get_data',$notviewed); + $c->expectOnce('internal_set_data',array($cm,$hasviewed)); + $c->expectOnce('update_state',array($cm,COMPLETION_COMPLETE,1337)); + $c->set_module_viewed($cm,1337); + + $c->tally(); + } + + function test_count_user_data() { + global $DB; + $cm=(object)array('id'=>42); + $DB->setReturnValue('get_field_sql',666); + $DB->expectOnce('get_field_sql',array(new IgnoreWhitespaceExpectation("SELECT + COUNT(1) +FROM + test_course_modules_completion +WHERE + coursemoduleid=? AND completionstate<>0"),array(42))); + $c=new completion_info(null); + $this->assertEqual(666,$c->count_user_data($cm)); + + $DB->tally(); + } + + function test_delete_all_state() { + global $DB,$SESSION; + $course=(object)array('id'=>13); + $cm=(object)array('id'=>42,'course'=>13); + $c=new completion_info($course); + // Check it works ok without data in session + $DB->expectAt(0,'delete_records', + array('course_modules_completion',array('coursemoduleid'=>42))); + $c->delete_all_state($cm); + + // Build up a session to check it deletes the right bits from it + // (and not other bits) + $SESSION->completioncache=array(); + $SESSION->completioncache[13]=array(); + $SESSION->completioncache[13][42]='foo'; + $SESSION->completioncache[13][43]='foo'; + $SESSION->completioncache[14]=array(); + $SESSION->completioncache[14][42]='foo'; + $DB->expectAt(1,'delete_records', + array('course_modules_completion',array('coursemoduleid'=>42))); + $c->delete_all_state($cm); + $this->assertEqual(array(13=>array(43=>'foo'),14=>array(42=>'foo')), + $SESSION->completioncache); + + $DB->tally(); + } + + function test_reset_all_state() { + global $DB; + $c=new completion_cutdown(); + $c->completion_info((object)array('id'=>42)); + + $cm=(object)array('id'=>13,'course'=>42); + + $DB->setReturnValue('get_recordset',new fake_recordset(array( + (object)array('id'=>1,'userid'=>100), + (object)array('id'=>2,'userid'=>101), + ))); + $DB->expectOnce('get_recordset',array('course_modules_completion', + array('coursemoduleid'=>13),'','userid')); + $c->expectOnce('delete_all_state',array($cm)); + $c->expectOnce('internal_get_tracked_users',array(false)); + $c->setReturnValue('internal_get_tracked_users',array( + (object)array('id'=>100,'firstname'=>'Woot','lastname'=>'Plugh'), + (object)array('id'=>201,'firstname'=>'Vroom','lastname'=>'Xyzzy'), + )); + + $c->expectAt(0,'update_state',array($cm,COMPLETION_UNKNOWN,100)); + $c->expectAt(1,'update_state',array($cm,COMPLETION_UNKNOWN,101)); + $c->expectAt(2,'update_state',array($cm,COMPLETION_UNKNOWN,201)); + + $c->reset_all_state($cm); + + $DB->tally(); + $c->tally(); + } + + function test_get_data() { + global $DB,$SESSION; + + $c=new completion_info((object)array('id'=>42)); + $cm=(object)array('id'=>13,'course'=>42); + + // 1. Not current user, record exists + $sillyrecord=(object)array('frog'=>'kermit'); + $DB->expectAt(0,'get_record',array('course_modules_completion', + array('coursemoduleid'=>13,'userid'=>123))); + $DB->setReturnValueAt(0,'get_record',$sillyrecord); + $result=$c->get_data($cm,false,123); + $this->assertEqual($sillyrecord,$result); + $this->assertTrue(empty($SESSION->completioncache)); + + // 2. Not current user, default record, wholecourse (ignored) + $DB->expectAt(1,'get_record',array('course_modules_completion', + array('coursemoduleid'=>13,'userid'=>123))); + $DB->setReturnValueAt(1,'get_record',false); + $result=$c->get_data($cm,true,123); + $this->assertEqual((object)array( + 'id'=>'0','coursemoduleid'=>13,'userid'=>123,'completionstate'=>0, + 'viewed'=>0,'timemodified'=>0),$result); + $this->assertTrue(empty($SESSION->completioncache)); + + // 3. Current user, single record, not from cache + $DB->expectAt(2,'get_record',array('course_modules_completion', + array('coursemoduleid'=>13,'userid'=>314159))); + $DB->setReturnValueAt(2,'get_record',$sillyrecord); + $result=$c->get_data($cm); + $this->assertEqual($sillyrecord,$result); + $this->assertEqual($sillyrecord,$SESSION->completioncache[42][13]); + // When checking time(), allow for second overlaps + $this->assertTrue(time()-$SESSION->completioncache[42]['updated']<2); + + // 4. Current user, 'whole course', but from cache + $result=$c->get_data($cm,true); + $this->assertEqual($sillyrecord,$result); + + // 5. Current user, single record, cache expired + $SESSION->completioncache[42]['updated']=37; // Quite a long time ago + $now=time(); + $SESSION->completioncache[17]['updated']=$now; + $SESSION->completioncache[39]['updated']=72; // Also a long time ago + $DB->expectAt(3,'get_record',array('course_modules_completion', + array('coursemoduleid'=>13,'userid'=>314159))); + $DB->setReturnValueAt(3,'get_record',$sillyrecord); + $result=$c->get_data($cm,false); + $this->assertEqual($sillyrecord,$result); + // Check that updated value is right, then fudge it to make next compare + // work + $this->assertTrue(time()-$SESSION->completioncache[42]['updated']<2); + $SESSION->completioncache[42]['updated']=$now; + // Check things got expired from cache + $this->assertEqual(array(42=>array(13=>$sillyrecord,'updated'=>$now), + 17=>array('updated'=>$now)),$SESSION->completioncache); + + // 6. Current user, 'whole course' and record not in cache + unset($SESSION->completioncache); + + // Scenario: Completion data exists for one CMid + $basicrecord=(object)array('coursemoduleid'=>13); + $DB->setReturnValueAt(0,'get_records_sql',array( + 1=>$basicrecord + )); + $DB->expectAt(0,'get_records_sql',array(new IgnoreWhitespaceExpectation(" +SELECT + cmc.* +FROM + test_course_modules cm + INNER JOIN test_course_modules_completion cmc ON cmc.coursemoduleid=cm.id +WHERE + cm.course=? AND cmc.userid=?"),array(42,314159))); + + // There are two CMids in total, the one we had data for and another one + $modinfo->cms=array((object)array('id'=>13),(object)array('id'=>14)); + $result=$c->get_data($cm,true,0,$modinfo); + + // Check result + $this->assertEqual($basicrecord,$result); + + // Check the cache contents + $this->assertTrue(time()-$SESSION->completioncache[42]['updated']<2); + $SESSION->completioncache[42]['updated']=$now; + $this->assertEqual(array(42=>array(13=>$basicrecord,14=>(object)array( + 'id'=>'0','coursemoduleid'=>14,'userid'=>314159,'completionstate'=>0, + 'viewed'=>0,'timemodified'=>0),'updated'=>$now)),$SESSION->completioncache); + + $DB->tally(); + } + + function test_internal_set_data() { + global $DB,$SESSION; + + $cm=(object)array('course'=>42,'id'=>13); + $c=new completion_info((object)array('id'=>42)); + + // 1) Test with new data + $data=(object)array('id'=>0,'userid'=>314159); + $DB->setReturnValueAt(0,'insert_record',4); + $DB->expectAt(0,'insert_record',array('course_modules_completion',$data)); + $c->internal_set_data($cm,$data); + $this->assertEqual(4,$data->id); + $this->assertEqual(array(42=>array(13=>$data)),$SESSION->completioncache); + + // 2) Test with existing data and for different user (not cached) + unset($SESSION->completioncache); + $d2=(object)array('id'=>7,'userid'=>17); + $DB->expectAt(0,'update_record',array('course_modules_completion',$d2)); + $c->internal_set_data($cm,$d2); + $this->assertFalse(isset($SESSION->completioncache)); + + $DB->tally(); + } + + function test_get_activities() { + global $DB; + + $c=new completion_info((object)array('id'=>42)); + + // Try with no activities + $DB->expectAt(0,'get_records_select',array('course_modules', + 'course=42 AND completion<>'.COMPLETION_TRACKING_NONE)); + $DB->setReturnValueAt(0,'get_records_select',array()); + $result=$c->get_activities(); + $this->assertEqual(array(),$result); + + // Try with an activity (need to fake up modinfo for it as well) + $DB->expectAt(1,'get_records_select',array('course_modules', + 'course=42 AND completion<>'.COMPLETION_TRACKING_NONE)); + $DB->setReturnValueAt(1,'get_records_select',array( + 13=>(object)array('id'=>13) + )); + $modinfo=new stdClass; + $modinfo->sections=array(array(1,2,3),array(12,13,14)); + $modinfo->cms[13]=(object)array('modname'=>'frog','name'=>'kermit'); + $result=$c->get_activities($modinfo); + $this->assertEqual(array(13=>(object)array('id'=>13,'modname'=>'frog','name'=>'kermit')),$result); + + $DB->tally(); + } + + // internal_get_tracked_users() cannot easily be tested because it uses + // get_role_users, so skipping that + + function test_get_progress_all() { + global $DB; + + $c=new completion_cutdown(); + $c->completion_info((object)array('id'=>42)); + + // 1) Basic usage + $c->expectAt(0,'internal_get_tracked_users',array(false,0)); + $c->setReturnValueAt(0,'internal_get_tracked_users',array( + (object)array('id'=>100,'firstname'=>'Woot','lastname'=>'Plugh'), + (object)array('id'=>201,'firstname'=>'Vroom','lastname'=>'Xyzzy'), + )); + $DB->expectAt(0,'get_in_or_equal',array(array(100,201))); + $DB->setReturnValueAt(0,'get_in_or_equal',array(' IN (100,201)',array())); + $DB->expectAt(0,'get_recordset_sql',array(new IgnoreWhitespaceExpectation(" +SELECT + cmc.* +FROM + test_course_modules cm + INNER JOIN test_course_modules_completion cmc ON cm.id=cmc.coursemoduleid +WHERE + cm.course=? AND cmc.userid IN (100,201)"),array(42))); + $progress1=(object)array('userid'=>100,'coursemoduleid'=>13); + $progress2=(object)array('userid'=>201,'coursemoduleid'=>14); + $DB->setReturnValueAt(0,'get_recordset_sql',new fake_recordset(array( + $progress1,$progress2 + ))); + + $this->assertEqual(array( + 100 => (object)array('id'=>100,'firstname'=>'Woot','lastname'=>'Plugh', + 'progress'=>array(13=>$progress1)), + 201 => (object)array('id'=>201,'firstname'=>'Vroom','lastname'=>'Xyzzy', + 'progress'=>array(14=>$progress2)), + ),$c->get_progress_all(false)); + + // 2) With more than 1,000 results + $c->expectAt(1,'internal_get_tracked_users',array(true,3)); + + $tracked=array(); + $ids=array(); + $progress=array(); + for($i=100;$i<2000;$i++) { + $tracked[]=(object)array('id'=>$i,'firstname'=>'frog','lastname'=>$i); + $ids[]=$i; + $progress[]=(object)array('userid'=>$i,'coursemoduleid'=>13); + $progress[]=(object)array('userid'=>$i,'coursemoduleid'=>14); + } + $c->setReturnValueAt(1,'internal_get_tracked_users',$tracked); + + $DB->expectAt(1,'get_in_or_equal',array(array_slice($ids,0,1000))); + $DB->setReturnValueAt(1,'get_in_or_equal',array(' IN whatever',array())); + $DB->expectAt(1,'get_recordset_sql',array(new IgnoreWhitespaceExpectation(" +SELECT + cmc.* +FROM + test_course_modules cm + INNER JOIN test_course_modules_completion cmc ON cm.id=cmc.coursemoduleid +WHERE + cm.course=? AND cmc.userid IN whatever"),array(42))); + $DB->setReturnValueAt(1,'get_recordset_sql',new fake_recordset(array_slice($progress,0,1000))); + $DB->expectAt(2,'get_in_or_equal',array(array_slice($ids,1000))); + $DB->setReturnValueAt(2,'get_in_or_equal',array(' IN whatever2',array())); + $DB->expectAt(2,'get_recordset_sql',array(new IgnoreWhitespaceExpectation(" +SELECT + cmc.* +FROM + test_course_modules cm + INNER JOIN test_course_modules_completion cmc ON cm.id=cmc.coursemoduleid +WHERE + cm.course=? AND cmc.userid IN whatever2"),array(42))); + $DB->setReturnValueAt(2,'get_recordset_sql',new fake_recordset(array_slice($progress,1000))); + + $result=$c->get_progress_all(true,3); + + $resultok=true; + $resultok = $resultok && ($ids==array_keys($result)); + foreach($result as $userid => $data) { + $resultok = $resultok && $data->firstname=='frog'; + $resultok = $resultok && $data->lastname==$userid; + $resultok = $resultok && $data->id==$userid; + $cms=$data->progress; + $resultok= $resultok && (array(13,14)==array_keys($cms)); + $resultok= $resultok && ((object)array('userid'=>$userid,'coursemoduleid'=>13)==$cms[13]); + $resultok= $resultok && ((object)array('userid'=>$userid,'coursemoduleid'=>14)==$cms[14]); + } + $this->assertTrue($resultok); + + $DB->tally(); + $c->tally(); + } + + function test_inform_grade_changed() { + $c=new completion_cutdown(); + $c->completion_info((object)array('id'=>42)); + + $cm=(object)array('course'=>42,'id'=>13,'completiongradeitemnumber'=>null); + $item=(object)array('itemnumber'=>3); + $grade=(object)array('userid'=>31337); + + // Not enabled (should do nothing) + $c->setReturnValueAt(0,'is_enabled',false); + $c->expectAt(0,'is_enabled',array($cm)); + $c->inform_grade_changed($cm,$item,$grade,false); + + // Enabled but still no grade completion required, should still do nothing + $c->setReturnValueAt(1,'is_enabled',true); + $c->expectAt(1,'is_enabled',array($cm)); + $c->inform_grade_changed($cm,$item,$grade,false); + + // Enabled and completion required but item number is wrong, does nothing + $cm->completiongradeitemnumber=7; + $c->setReturnValueAt(2,'is_enabled',true); + $c->expectAt(2,'is_enabled',array($cm)); + $c->inform_grade_changed($cm,$item,$grade,false); + + // Enabled and completion required and item number right. It is supposed + // to call update_state with the new potential state being obtained from + // internal_get_grade_state. + $cm->completiongradeitemnumber=3; + $c->setReturnValueAt(3,'is_enabled',true); + $c->expectAt(3,'is_enabled',array($cm)); + $c->expectAt(0,'internal_get_grade_state',array($item,$grade)); + $c->setReturnValueAt(0,'internal_get_grade_state',COMPLETION_COMPLETE_PASS); + $c->expectAt(0,'update_state',array($cm,COMPLETION_COMPLETE_PASS,31337)); + $c->inform_grade_changed($cm,$item,$grade,false); + + // Same as above but marked deleted. It is supposed to call update_state + // with new potential state being COMPLETION_INCOMPLETE + $c->setReturnValueAt(4,'is_enabled',false); + $c->expectAt(4,'is_enabled',array($cm)); + $c->expectAt(1,'update_state',array($cm,COMPLETION_INCOMPLETE,31337)); + $c->inform_grade_changed($cm,$item,$grade,false); + + $c->tally(); + } + + function test_internal_get_grade_state() { + $item=new stdClass; + $grade=new stdClass; + + $item->gradepass=4; + $item->hidden=0; + $grade->rawgrade=4.0; + $grade->finalgrade=null; + + // Grade has pass mark and is not hidden, user passes + $this->assertEqual( + COMPLETION_COMPLETE_PASS, + completion_info::internal_get_grade_state($item,$grade)); + + // Same but user fails + $grade->rawgrade=3.9; + $this->assertEqual( + COMPLETION_COMPLETE_FAIL, + completion_info::internal_get_grade_state($item,$grade)); + + // User fails on raw grade but passes on final +