ÿØÿà JFIF    ÿÛ „  ( %!1"%)+...383-7(-.+  -%%--------+------------/----------------------------ÿÀ  ¶" ÿÄ     ÿÄ @    !1AQaq‘¡"±ÁÑð2’BRráS‚ñ3b¢CT²Ò #cÿÄ    ÿÄ '   1!AQ2a"‘±#ÿÚ   ? ò(/p‘—–¢·,M|¯G»R£üešT. ôŸEoCFñ‡ 4ÔkåóxR|µÁ¨ÓDà!Ç´µ˜‡p ÍQ‹§!þž^õWsŒy  åÁ§$á«%ºq˜™JyyË0y«^Ϗ -3ÃÏËÈ£Æ\P’ä„s&•‹Üs üô'àWt¹pñCp$bÚF`·7.^)¾!)ÒÙ˜–ÅÏš^h¾ø³·ìe±ŒaeÖ—Í,e=B}Jéߦ$$½—šÙÜCC¯¶è8β–Åkèwx LÉngT±æ¹£Ócœ¿Í/©Éÿ ‰ÚCÒ­mq;Tt¶–}0"zÖPcd1š­¤tˆ„ÐÓâqú[™öUCŠ2޿د­É(¸ßúçšmW,Ò ºf‹Ån™ÄLêÁ8üÁkÁ€L¥ÍuJ1œôrÂSŒÓöt!7EUíïb)AËi¬â-ÙHk8òÁiXYwÅ)O^"ÇØriÛô{»wRUKÙÏÚlÓ¬€‚’שT ] ªb—ÊîKC^×M“xùäñz¬zJ× {4[¹M'IÆeH1LCWÕ]œûº WDæà°OÅ‚¹c²ÞuA–rL`HšpCez‡V’— Ž‚Ë„–ñ­'µR0ÕÖ=À:±G»C\nÆÉ5*¢ †3šhQ4ýÒi$Õ1"Ü]¢p-q3zcQ$ªö¨¥æf›‘»´ÝÚHá„e²JÇ–iÊ:·à¬Øc5Åk»Laª*wi [0Ówk ¦a¦0Õ²ÅÅ‚T¸’³q:Æ<¹ú Ò˜Æ}2VE•Ѐp¡Ô3Úº …h³^LJºçì¥Ánó|˜ÖˆÎˆ+« ¦dø‘Mº–d\ªDðÛ„Ì×LÝ——Ð9Ç|•FèçVm™¨€™3õS–9Äå¢<̉Ìm¢±`·º˜&Z²ž Ë®D«¶»6™‰©­HÈ Àá«h­i³04æ’hxœi3ï¨(ÓL·†‹¶ËpŒ ƒÜÒ? :õdsÀ –3æÓ·Üjâ¬9—]8n¼&]I F£¼…ì–—Œ0v°LǐÅfÛ2TÖ’aµ˜N@ÖBSÏ^à´4-±°b×nÈÈœÆD=KdŒø…ÄÈK,›ÖR§fjü$^ÔÎ!„6^ s:Üun\ôkCÞò÷<—Lúl Í›G¹ÀPá¼Nc¡šÒ…ÙWͳœ±vÊáЄ×9‹P‰cEØ‘™›µŸP<×g1 º©Ø¬wiÁ^l5× I/';Ë+´Z‡ÓÄFÕp[Èð’ HÄ ó 9¬Dl4“é±Ï”V=^Hð;žLädÉa‚°Ë,#/Ž~²BÔÛ QãñQtIeórVB,ÏÂf‹4#Μc‹É3ZIÖ Œ¢¥·üxa‡Ãçè„jŠ[5 Ä!U?,3žÎ×…ðdÓ÷háŠA‰ìÄ4÷ÃÜZÃE{‰®+W\ZÍE[‰»µk»MÝ­`¢©†˜ÃV‹-f¢¡†¢a«eŠ%«Y¨©q%jâd,Ôq`) ¤¦…šˆ ¦E>0Ùb²t–Ÿ  ÷jsςҒKÈcø(é›;k(’#ôŒQ ™C> õ³M9óºÆµ¤çâ;uÀ)5>RÔ¸fÓ~Ȧ—’-$fe«"FâUˆÊwµPÏu w+"åÉó¨6s™Û9(À¨ù©hE°½†n`m~“@©›p!Vkhµ Á1šbEqqÊ@ºèl›2 –Yã%‘‚hÛ{a¶‡ÅÞM²¬çL34®Çl]®‹ŽØŒm'RÜe35æöˆ­Á­ cP9šeÁköo´L³Ì=p8]”‡B¶,šº|ɏdzXˆØk*ÇÚ{#ñ‰pã(€¶‡9ý'šÜ³½¯kƒ†Â’ëSOƒ•Å®H6+a¢µˆ‹Y¨b˜b+XˆÖ!°u!¢"µˆˆn2ˆ Å0Ä`Å Ô®c(ÓÜGº•Ä»Œ âWî%q.áÐÄ÷î%umÍ£+ÜQ,Vn¦-GphU¸¢X­&,Gt ·K¢ÅÅ·¥RÄÊÉbKn N­R2g$VµÕó Qr#–ÓZUÏð0ÎtÙÀœz ëöo“Îk¯n„T°"SÞ+³/…€½"ižãO%Í%&Δâ‘ËØ´IˆÓ°Ë‰ÞM3.™yçÁzit›`4C`k¢¼á!álÄï sÂ[×!¤l¾æo"ƒIÔ'@ªu)3›ô®“@Z à¾] üœ'‡4¸PÞkÁ-qÄH‰ÓÍBd´ÿ —¹°ÄA&ÊUC„öz‚‚RæŠtf¬>˜ÑidšEçÑL÷áý¡V±h§­c¿¥¨ƒ@g¼…Ýö^ÞËc.ž„æ“I[ ª+·zÐÒÚ,½Ð»¶€C›7jk$๖€Ž¾ÅÙpp¶ÍþéΤ ½ØÎŒq ñpæžÅ¥#Øâ49ÃÂK^MYV“,¶Éz_òæ:…;Õ«¦I3âJ£¤û5$7Èxün\â]Êe¯(Ÿ&Ž‹¶2<6Ćf× æ¢ƱqWı[(ÙÂŽç‚T7¨@ÈÉÍß‚ïÙ"&3TYlGށ†)†#5ŠA©^T2ÆÁ†)©†©†¤yGXÁ†©"†)©¼ÅV WS†#©]R–r‹q5ÕbâWþBh¯u+ªÅÄ×ü„nÑ^êkªÁj‰jeœWˆÕÕ`µDµ2Ì+Ä µDµµ5Ô{¤Þ0RF’H÷AÛ<ÊÁ§ ƈæ0Ñ£ê4 öÚµ[¬/1²€â<-¬šö²r×0o×w ´xÐÒË„ g01¬„åEM蛇Ã\6µE›‘fÙO k÷CºnÓ1`iîÒ² áÂ!ÑLÄÿ ³¬\õ*:M=Ç»c‹2[‰ØsÕLò\ŒK L^™üÄN\¶#» Ç\……ßïIœÉ$¸ÎñÍ®ÔHÇ Ûí/´>Li”„†g^$ 7NÍÑÑ2M2ªNg=k½Ð}Ÿl6‡ —JWˆ”ÞéT·+µ¦üÂFè¢Vs6ýÆ4»2+2IÝ9ô9®ÇGÙÇ1+Ä8;*Õ§ž½D*îÑWÚçá3Ma¤·ˆ¥t}š²Ü…Q+Æüµ8HíS”üxGéÁˆ¦Ël„éÈ 5 µÄƒ6äÜ(kIš¯S†fÖ&¸®×v|Äyˆ*禌Éù@7-®Ä[]ÎÖ¿ê`º…=:…žJVnݳvðÖKE¼‚ »mà ÷f®:Ë ™–4rð¡4` ܹçž~‹C=œ…¾Ïj|VDtK.ƒ"9 Ï ­–éXá³6WÒïIL-Ї Ç db/access.php000064400000002443152266461520007117 0ustar00. /** * Completion status block caps. * * @package block_completionstatus * @copyright Mark Nelson * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); $capabilities = array( 'block/completionstatus:addinstance' => array( 'riskbitmask' => RISK_SPAM | RISK_XSS, 'captype' => 'write', 'contextlevel' => CONTEXT_BLOCK, 'archetypes' => array( 'editingteacher' => CAP_ALLOW, 'manager' => CAP_ALLOW ), 'clonepermissionsfrom' => 'moodle/site:manageblocks' ), ); db/upgrade.php000064400000004371152266461520007307 0ustar00. /** * This file keeps track of upgrades to the completion status block * * Sometimes, changes between versions involve alterations to database structures * and other major things that may break installations. * * The upgrade function in this file will attempt to perform all the necessary * actions to upgrade your older installation to the current version. * * If there's something it cannot do itself, it will tell you what you need to do. * * The commands in here will all be database-neutral, using the methods of * database_manager class * * Please do not forget to use upgrade_set_timeout() * before any action that may take longer time to finish. * * @since Moodle 2.0 * @package block_completionstatus * @copyright 2012 Mark Nelson * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ /** * Handles upgrading instances of this block. * * @param int $oldversion * @param object $block */ function xmldb_block_completionstatus_upgrade($oldversion, $block) { // Automatically generated Moodle v4.2.0 release upgrade line. // Put any upgrade step following this. // Automatically generated Moodle v4.3.0 release upgrade line. // Put any upgrade step following this. // Automatically generated Moodle v4.4.0 release upgrade line. // Put any upgrade step following this. // Automatically generated Moodle v4.5.0 release upgrade line. // Put any upgrade step following this. // Automatically generated Moodle v5.0.0 release upgrade line. // Put any upgrade step following this. return true; } lang/en/block_completionstatus.php000064400000002767152266461520013414 0ustar00. /** * Strings for component 'block_completionstatus', language 'en', branch 'MOODLE_20_STABLE' * * @package block_completionstatus * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ $string['completionprogressdetails'] = 'Completion progress details'; $string['completionstatus:addinstance'] = 'Add a new course completion status block'; $string['criteriagroup'] = 'Criteria group'; $string['firstofsecond'] = '{$a->first} of {$a->second}'; $string['pluginname'] = 'Course completion status'; $string['requirement'] = 'Requirement'; $string['returntocourse'] = 'Return to course'; $string['privacy:metadata'] = 'The Course completion status block only shows information about course completion and does not store any data of its own.'; version.php000064400000002305152266461520006753 0ustar00. /** * Version info * * @package block_completionstatus * @copyright 2009 Catalyst IT Ltd * @author Aaron Barnes * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); $plugin->version = 2025041400; // The current plugin version (Date: YYYYMMDDXX). $plugin->requires = 2025040800; // Requires this Moodle version. $plugin->component = 'block_completionstatus'; $plugin->dependencies = ['report_completion' => 2025040800]; classes/privacy/provider.php000064400000003010152266461520012224 0ustar00. /** * Privacy Subsystem implementation for block_completionstatus. * * @package block_completionstatus * @copyright 2018 Zig Tan * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace block_completionstatus\privacy; defined('MOODLE_INTERNAL') || die(); /** * Privacy Subsystem for block_completionstatus implementing null_provider. * * @copyright 2018 Zig Tan * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class provider implements \core_privacy\local\metadata\null_provider { /** * Get the language string identifier with the component's language * file to explain why this plugin stores no data. * * @return string */ public static function get_reason(): string { return 'privacy:metadata'; } } tests/completionstatus_test.php000064400000004440152266461520013106 0ustar00. namespace block_completionstatus; use advanced_testcase; use block_completionstatus; use context_course; /** * PHPUnit block_completionstatus tests * * @package block_completionstatus * @category test * @copyright 2021 Sara Arjona (sara@moodle.com) * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @coversDefaultClass \block_completionstatus */ final class completionstatus_test extends advanced_testcase { public static function setUpBeforeClass(): void { require_once(__DIR__ . '/../../moodleblock.class.php'); require_once(__DIR__ . '/../block_completionstatus.php'); parent::setUpBeforeClass(); } /** * Test the behaviour of can_block_be_added() method. * * @covers ::can_block_be_added */ public function test_can_block_be_added(): void { $this->resetAfterTest(); $this->setAdminUser(); // Create a course and prepare the page where the block will be added. $course = $this->getDataGenerator()->create_course(); $page = new \moodle_page(); $page->set_context(context_course::instance($course->id)); $page->set_pagelayout('course'); $block = new block_completionstatus(); // If blogs advanced feature is enabled, the method should return true. set_config('enablecompletion', true); $this->assertTrue($block->can_block_be_added($page)); // However, if the blogs advanced feature is disabled, the method should return false. set_config('enablecompletion', false); $this->assertFalse($block->can_block_be_added($page)); } } tests/behat/block_completionstatus_manual_other.feature000064400000011577152266461520017717 0ustar00@block @block_completionstatus Feature: Enable Block Completion in a course using manual completion by others In order to view the completion block in a course As a teacher I can add completion block to a course and set up manual completion by others Background: Given the following "users" exist: | username | firstname | lastname | email | idnumber | | teacher1 | Teacher | 1 | teacher1@example.com | T1 | | teacher2 | Teacher | 2 | teacher1@example.com | T2 | | student1 | Student | 1 | student1@example.com | S1 | And the following "courses" exist: | fullname | shortname | category | enablecompletion | | Course 1 | C1 | 0 | 1 | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | | teacher2 | C1 | teacher | | student1 | C1 | student | And the following "blocks" exist: | blockname | contextlevel | reference | pagetypepattern | defaultregion | | completionstatus | Course | C1 | course-view-* | side-pre | Scenario: Add the block to a the course and mark a student complete. Given I am on the "Course 1" course page logged in as teacher1 And I navigate to "Course completion" in current page administration And I expand all fieldsets And I set the following fields to these values: | Teacher | 1 | And I press "Save changes" When I am on the "Course 1" course page logged in as student1 And I should see "Status: Not yet started" in the "Course completion status" "block" And I should see "No" in the "Teacher" "table_row" And I am on the "Course 1" course page logged in as teacher1 And I navigate to "Reports" in current page administration And I click on "Course completion" "link" in the "region-main" "region" And I follow "Click to mark user complete" # Running completion task just after clicking sometimes fail, as record # should be created before the task runs. And I wait "1" seconds And I run the scheduled task "core\task\completion_regular_task" And I am on site homepage And I am on the "Course 1" course page logged in as student1 Then I should see "Status: Complete" in the "Course completion status" "block" And I should see "Yes" in the "Teacher" "table_row" And I follow "More details" And I should see "Yes" in the "Marked complete by Teacher" "table_row" Scenario: Add the block to a the course and require multiple roles to mark a student complete. Given I am on the "Course 1" course page logged in as teacher1 And I navigate to "Course completion" in current page administration And I expand all fieldsets And I set the following fields to these values: | Teacher | 1 | | Non-editing teacher | 1 | | id_role_aggregation | ALL selected roles to mark when the condition is met | And I press "Save changes" When I am on the "Course 1" course page logged in as student1 And I should see "Status: Not yet started" in the "Course completion status" "block" And I should see "No" in the "Teacher" "table_row" And I should see "No" in the "Non-editing teacher" "table_row" And I am on the "Course 1" course page logged in as teacher1 And I navigate to "Reports" in current page administration And I click on "Course completion" "link" in the "region-main" "region" And I follow "Click to mark user complete" And I am on the "Course 1" course page logged in as student1 And I should see "Status: In progress" in the "Course completion status" "block" And I should see "Yes" in the "Teacher" "table_row" And I should see "No" in the "Non-editing teacher" "table_row" And I follow "More details" And I should see "Yes" in the "Marked complete by Teacher" "table_row" And I should see "No" in the "Marked complete by Non-editing teacher" "table_row" And I am on the "Course 1" course page logged in as teacher2 And I navigate to "Reports" in current page administration And I click on "Course completion" "link" in the "region-main" "region" And I follow "Click to mark user complete" # Running completion task just after clicking sometimes fail, as record # should be created before the task runs. And I wait "1" seconds And I run the scheduled task "core\task\completion_regular_task" And I am on site homepage And I am on the "Course 1" course page logged in as student1 Then I should see "Status: Complete" in the "Course completion status" "block" And I should see "Yes" in the "Teacher" "table_row" And I should see "Yes" in the "Non-editing teacher" "table_row" And I follow "More details" And I should see "Yes" in the "Marked complete by Teacher" "table_row" And I should see "Yes" in the "Marked complete by Non-editing teacher" "table_row" tests/behat/block_completionstatus_manual_self.feature000064400000004631152266461520017520 0ustar00@block @block_completionstatus @block_selfcompletion Feature: Enable Block Completion in a course using manual self completion In order to view the completion block in a course As a teacher I can add completion block to a course and set up manual self completion Scenario: Add the block to a the course and manually complete the course Given the following "users" exist: | username | firstname | lastname | email | idnumber | | teacher1 | Teacher | 1 | teacher1@example.com | T1 | | student1 | Student | 1 | student1@example.com | S1 | And the following "courses" exist: | fullname | shortname | category | enablecompletion | | Course 1 | C1 | 0 | 1 | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | | student1 | C1 | student | And I enable "selfcompletion" "block" plugin And the following "blocks" exist: | blockname | contextlevel | reference | pagetypepattern | defaultregion | | completionstatus | Course | C1 | course-view-* | side-pre | | selfcompletion | Course | C1 | course-view-* | side-pre | And I am on the "Course 1" course page logged in as teacher1 And I navigate to "Course completion" in current page administration And I expand all fieldsets And I set the following fields to these values: | id_criteria_self | 1 | And I press "Save changes" When I am on the "Course 1" course page logged in as student1 And I should see "Status: Not yet started" in the "Course completion status" "block" And I should see "No" in the "Self completion" "table_row" And I follow "Complete course" And I should see "Confirm self completion" And I press "Yes" And I should see "Status: In progress" in the "Course completion status" "block" # Running completion task just after clicking sometimes fail, as record # should be created before the task runs. And I wait "1" seconds And I run the scheduled task "core\task\completion_regular_task" And I am on "Course 1" course homepage Then I should see "Status: Complete" in the "Course completion status" "block" And I should see "Yes" in the "Self completion" "table_row" And I follow "More details" And I should see "Yes" in the "Self completion" "table_row" tests/behat/block_completionstatus_activity_completion.feature000064400000046216152266461520021324 0ustar00@block @block_completionstatus @core_completion Feature: Enable Block Completion in a course using activity completion In order to view the completion block in a course As a teacher I can add completion block to a course and set up activity completion Background: Given the following "users" exist: | username | firstname | lastname | email | idnumber | | teacher1 | Teacher | 1 | teacher1@example.com | T1 | | student1 | Student | 1 | student1@example.com | S1 | | student2 | Student | 2 | student2@example.com | S2 | And the following "courses" exist: | fullname | shortname | category | enablecompletion | | Course 1 | C1 | 0 | 1 | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | | student1 | C1 | student | | student2 | C1 | student | And the following "activities" exist: | activity | course | idnumber | name | gradepass | completion | completionview | completionusegrade | completionpassgrade | | page | C1 | page1 | Test page name | | 2 | 1 | 0 | 0 | | assign | C1 | assign1 | Test assign name | 50 | 2 | 0 | 1 | 1 | And the following "blocks" exist: | blockname | contextlevel | reference | pagetypepattern | defaultregion | | completionstatus | Course | C1 | course-view-* | side-pre | And I am on the "Course 1" course page logged in as teacher1 And I change window size to "large" And I turn editing mode on Scenario: Completion status block when student has not started any activities Given I navigate to "Course completion" in current page administration And I expand all fieldsets And I set the following fields to these values: | Test page name | 1 | And I press "Save changes" When I am on the "Course 1" course page logged in as student1 Then I should see "Status: Not yet started" in the "Course completion status" "block" And I should see "0 of 1" in the "Activity completion" "table_row" Scenario: Completion status block when student has completed a page Given I navigate to "Course completion" in current page administration And I expand all fieldsets And I set the following fields to these values: | Test page name | 1 | And I press "Save changes" When I am on the "Test page name" "page activity" page logged in as student1 And I am on "Course 1" course homepage Then I should see "Status: Complete" in the "Course completion status" "block" And I should see "1 of 1" in the "Activity completion" "table_row" And I follow "More details" And I should see "Yes" in the "Activity completion" "table_row" Scenario: Completion status block with items with passing grade Given I navigate to "Course completion" in current page administration And I expand all fieldsets And I set the following fields to these values: | Test assign name | 1 | And I press "Save changes" And the following "grade grades" exist: | gradeitem | user | grade | | Test assign name | student1 | 53 | When I am on the "Course 1" course page logged in as student1 Then I should see "Status: Complete" in the "Course completion status" "block" And I should see "1 of 1" in the "Activity completion" "table_row" And I trigger cron And I am on "Course 1" course homepage And I follow "More details" And I should see "Achieving grade, Achieving passing grade" in the "Activity completion" "table_row" And I should see "Yes" in the "Activity completion" "table_row" Scenario: Completion status block with items with failing grade Given the following "grade grades" exist: | gradeitem | user | grade | | Test assign name | student1 | 49 | And I navigate to "Course completion" in current page administration And I expand all fieldsets And I set the following fields to these values: | Test assign name | 1 | And I press "Save changes" When I am on the "Course 1" course page logged in as student1 Then I should see "Status: Not yet started" in the "Course completion status" "block" And I should see "0 of 1" in the "Activity completion" "table_row" And I trigger cron And I am on "Course 1" course homepage And I follow "More details" And I should see "Achieving grade, Achieving passing grade" in the "Activity completion" "table_row" And I should see "No" in the "Activity completion" "table_row" @javascript Scenario: Student visibility respects combined activity and section restrictions with progressive completion Given the following "activities" exist: | activity | name | intro | course | idnumber | section | visible | completion | completionview | | page | task A | page description | C1 | page1 | 0 | 1 | 2 | 1 | | page | task B | page description | C1 | page2 | 1 | 1 | 2 | 1 | | assign | task C | assignment description | C1 | assign1 | 1 | 1 | 2 | 1 | And I navigate to "Course completion" in current page administration And I expand all fieldsets And I set the following fields to these values: | Page - task A | 1 | | Page - task B | 1 | | Assignment - task C | 1 | And I press "Save changes" # Add conditionally visible restriction (open eye) to section 1 requiring task A completion. And I turn editing mode on And I edit the section "1" And I expand all fieldsets And I click on "Add restriction..." "button" And I click on "Activity completion" "button" in the "Add restriction..." "dialogue" And I set the field "Activity or resource" to "task A" And I press "Save changes" # Add conditionally hidden restriction (closed eye) to task C requiring task A completion. And I am on the "task C" "assign activity editing" page And I expand all fieldsets And I click on "Add restriction..." "button" And I click on "Activity completion" "button" in the "Add restriction..." "dialogue" And I click on "Item name displayed with access restriction information if student doesn't meet this condition • Click to hide" "link" And I set the field "Activity or resource" to "task A" And I press "Save and return to course" And I log out # Initial state: Only unrestricted visible activities appear. When I am on the "Course 1" course page logged in as student1 Then I should see "Status: Not yet started" And I should see "0 of 1" in the "Activity completion" "table_row" And I follow "More details" And I should see "task A" in the "criteriastatus" "table" And I should not see "task B" in the "criteriastatus" "table" And I should not see "task C" in the "criteriastatus" "table" # After completing task A: Section 2 activities become visible. And I click on "task A" "link" And I am on the "Course 1" course page logged in as student1 And I should see "Status: In progress" And I should see "1 of 3" in the "Activity completion" "table_row" And I follow "More details" And I should see "task A" in the "criteriastatus" "table" And I should see "task B" in the "criteriastatus" "table" And I should see "task C" in the "criteriastatus" "table" @javascript Scenario: Student completion view shows only accessible activities considering all activity restrictions Given the following "activities" exist: | activity | name | intro | course | idnumber | section | visible | completion | completionview | | page | task A | page description | C1 | page1 | 0 | 1 | 2 | 1 | | page | task B | page description | C1 | assign1 | 1 | 1 | 2 | 1 | | assign | task C | assignment description | C1 | assign2 | 1 | 1 | 2 | 1 | And I navigate to "Course completion" in current page administration And I expand all fieldsets # Set completion of the activities. And I set the following fields to these values: | Page - task A | 1 | | Page - task B | 1 | | Assignment - task C | 1 | And I press "Save changes" # Add conditionally visible restriction (open eye) to "task B". And I am on the "task B" "page activity editing" page And I expand all fieldsets And I click on "Add restriction..." "button" And I click on "Activity completion" "button" in the "Add restriction..." "dialogue" And I set the field "Activity or resource" to "task A" And I press "Save and return to course" # Add conditionally hidden restriction (closed eye) to "task C". And I am on the "task C" "assign activity editing" page And I expand all fieldsets And I click on "Add restriction..." "button" And I click on "Activity completion" "button" in the "Add restriction..." "dialogue" And I click on "Item name displayed with access restriction information if student doesn't meet this condition • Click to hide" "link" And I set the field "Activity or resource" to "task A" And I press "Save and return to course" When I am on the "Course 1" course page logged in as student1 And I should see "Status: Not yet started" And I should see "0 of 2" in the "Activity completion" "table_row" And I follow "More details" Then I should see "task A" in the "criteriastatus" "table" And I should see "task B" in the "criteriastatus" "table" And I should not see "task C" in the "criteriastatus" "table" And I click on "task A" "link" # Complete task A to make task C visible. And I am on the "Course 1" course page logged in as student1 And I should see "Status: In progress" And I should see "1 of 3" in the "Activity completion" "table_row" And I follow "More details" And I should see "task A" in the "criteriastatus" "table" And I should see "task B" in the "criteriastatus" "table" And I should see "task C" in the "criteriastatus" "table" @javascript Scenario: Hidden activities do not appear in the completion status block Given the following "activities" exist: | activity | name | intro | course | idnumber | section | visible | completion | completionview | | page | task A | page description | C1 | page1 | 0 | 1 | 2 | 1 | | page | task B | page description | C1 | page2 | 0 | 0 | 2 | 1 | And I navigate to "Course completion" in current page administration And I expand all fieldsets # Set completion of the activities. And I set the following fields to these values: | Page - task A | 1 | | Page - task B | 1 | And I press "Save changes" When I am on the "Course 1" course page logged in as student1 And I should see "Status: Not yet started" And I should see "0 of 1" in the "Activity completion" "table_row" And I follow "More details" And I should see "task A" in the "criteriastatus" "table" Then I should not see "task B" in the "criteriastatus" "table" @javascript Scenario: Activities in the hidden section do not appear in the completion status block Given the following "activities" exist: | activity | name | intro | course | idnumber | section | visible | completion | completionview | | page | task A | page description | C1 | page1 | 0 | 1 | 2 | 1 | | page | task B | page description | C1 | page2 | 1 | 1 | 2 | 1 | | assign | task C | page description | C1 | page3 | 2 | 1 | 2 | 1 | | assign | task D | page description | C1 | page4 | 3 | 1 | 2 | 1 | And I navigate to "Course completion" in current page administration And I expand all fieldsets # Set completion of the activities. And I set the following fields to these values: | Page - task A | 1 | | Page - task B | 1 | | Assignment - task C | 1 | | Assignment - task D | 1 | And I press "Save changes" # Hide section 1 to make sure book activities are not visible to the student. And I hide section "1" # Add conditionally visible restriction to section 2. And I edit the section "2" And I expand all fieldsets And I click on "Add restriction..." "button" And I click on "Activity completion" "button" in the "Add restriction..." "dialogue" And I set the field "Activity or resource" to "task A" And I press "Save changes" # Add conditionally hidden restriction to section 3. And I am on the "Course 1" course page And I edit the section "3" And I expand all fieldsets And I click on "Add restriction..." "button" And I click on "Activity completion" "button" in the "Add restriction..." "dialogue" And I set the field "Activity or resource" to "task A" And I click on "Item name displayed with access restriction information if student doesn't meet this condition • Click to hide" "link" And I press "Save changes" And I log out When I am on the "Course 1" course page logged in as student1 And I should see "Status: Not yet started" And I should see "0 of 1" in the "Activity completion" "table_row" And I follow "More details" Then I should see "task A" in the "criteriastatus" "table" And I should not see "task B" in the "criteriastatus" "table" And I should not see "task C" in the "criteriastatus" "table" And I should not see "task D" in the "criteriastatus" "table" # Complete task A to make other activities visible. And I click on "task A" "link" And I am on the "Course 1" course page logged in as student1 And I should see "Status: In progress" And I should see "1 of 3" in the "Activity completion" "table_row" And I follow "More details" And I should see "task A" in the "criteriastatus" "table" And I should not see "task B" in the "criteriastatus" "table" And I should see "task C" in the "criteriastatus" "table" And I should see "task D" in the "criteriastatus" "table" @javascript Scenario: Activities with disabled completion tracking are omitted from the completion view Given the following "activities" exist: | activity | name | intro | course | idnumber | section | visible | completion | completionview | | page | task A | page description | C1 | page1 | 0 | 1 | 2 | 1 | | page | task B | page description | C1 | page2 | 1 | 1 | 2 | 1 | And I navigate to "Course completion" in current page administration And I expand all fieldsets # Set completion of the activities. And I set the following fields to these values: | Page - task A | 1 | | Page - task B | 1 | And I press "Save changes" # Disable completion tracking for "task A". And I am on the "task A" "page activity editing" page And I expand all fieldsets And I set the following fields to these values: | completion | 0 | And I press "Save and return to course" When I am on the "Course 1" course page logged in as student1 And I should see "Status: Not yet started" And I should see "0 of 1" in the "Activity completion" "table_row" And I follow "More details" Then I should not see "task A" in the "criteriastatus" "table" And I should see "task B" in the "criteriastatus" "table" @javascript Scenario: Activities with group or grouping restrictions are omitted from the completion view Given the following "activities" exist: | activity | name | intro | course | idnumber | section | visible | completion | completionview | | page | task A | page description | C1 | page1 | 0 | 1 | 2 | 1 | | page | task B | page description | C1 | page2 | 1 | 1 | 2 | 1 | | page | task C | page description | C1 | page3 | 2 | 1 | 2 | 1 | And I navigate to "Course completion" in current page administration And I expand all fieldsets # Set completion of the activities. And I set the following fields to these values: | Page - task A | 1 | | Page - task B | 1 | | Page - task C | 1 | And I press "Save changes" # Add groups and groupings. And the following "groups" exist: | name | course | idnumber | | G1 | C1 | GI1 | | G2 | C1 | GI2 | And the following "groupings" exist: | name | course | idnumber | | Grouping 1 | C1 | GG1 | | Grouping 2 | C1 | GG2 | And the following "grouping groups" exist: | grouping | group | | GG1 | GI1 | | GG2 | GI2 | # Add students to groups. And the following "group members" exist: | user | group | | student1 | GI1 | | student2 | GI2 | # Add group restriction to "task B". And I am on the "task B" "page activity editing" page And I expand all fieldsets And I click on "Add restriction..." "button" And I click on "Group" "button" in the "Add restriction..." "dialogue" And I set the field "Group" to "G1" And I click on "Save and return to course" "button" # Add Grouping and 'Activity or resource' restriction to "task C". And I am on the "task C" "page activity editing" page And I expand all fieldsets And I click on "Add restriction..." "button" And I click on "Grouping" "button" in the "Add restriction..." "dialogue" And I set the field "Grouping" to "Grouping 2" And I click on "Add restriction..." "button" And I click on "Activity completion" "button" in the "Add restriction..." "dialogue" And I set the field "Activity or resource" to "task A" And I click on "Save and return to course" "button" When I am on the "Course 1" course page logged in as student1 And I should see "Status: Not yet started" And I should see "0 of 2" in the "Activity completion" "table_row" And I follow "More details" Then I should see "task A" in the "criteriastatus" "table" And I should see "task B" in the "criteriastatus" "table" And I should not see "task C" in the "criteriastatus" "table" And I am on the "Course 1" course page logged in as student2 And I should see "Status: Not yet started" And I should see "0 of 2" in the "Activity completion" "table_row" And I follow "More details" And I should see "task A" in the "criteriastatus" "table" And I should not see "task B" in the "criteriastatus" "table" And I should see "task C" in the "criteriastatus" "table" tests/behat/block_completionstatus.feature000064400000005171152266461520015152 0ustar00@block @block_completionstatus Feature: Enable Block Completion in a course In order to view the completion block in a course As a teacher I can add completion block to a course Background: Given the following "users" exist: | username | firstname | lastname | email | idnumber | | teacher1 | Teacher | 1 | teacher1@example.com | T1 | | student1 | Student | 1 | student1@example.com | S1 | And the following "courses" exist: | fullname | shortname | category | enablecompletion | | Course 1 | C1 | 0 | 1 | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | | student1 | C1 | student | Scenario: Add the block to a the course where completion is disabled Given I log in as "teacher1" And I am on "Course 1" course homepage with editing mode on And I navigate to "Settings" in current page administration And I set the following fields to these values: | Enable completion tracking | No | And I press "Save and display" When I add the "Course completion status" block Then I should see "Completion is not enabled for this course" in the "Course completion status" "block" Scenario: Add the block to a the course where completion is not set Given I log in as "teacher1" And I am on "Course 1" course homepage with editing mode on When I add the "Course completion status" block Then I should see "No completion criteria set for this course" in the "Course completion status" "block" Scenario: Add the block to a course with criteria and view as an untracked role. Given the following "activities" exist: | activity | course | idnumber | name | intro | | page | C1 | page1 | Test page name | Test page description | And I log in as "teacher1" And I am on "Course 1" course homepage with editing mode on And I follow "Test page name" And I navigate to "Settings" in current page administration And I set the following fields to these values: | Add requirements | 1 | | View the activity | 1 | And I press "Save and return to course" When I add the "Course completion status" block And I navigate to "Course completion" in current page administration And I expand all fieldsets And I set the following fields to these values: | Test page name | 1 | And I press "Save changes" Then I should see "You are currently not being tracked by completion in this course" in the "Course completion status" "block" block_completionstatus.php000064400000024217152266461520012063 0ustar00. /** * Block for displayed logged in user's course completion status. * Displays overall, and individual criteria status for logged in user. * * @package block_completionstatus * @copyright 2009-2012 Catalyst IT Ltd * @author Aaron Barnes * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class block_completionstatus extends block_base { public function init() { global $CFG; require_once("{$CFG->libdir}/completionlib.php"); $this->title = get_string('pluginname', 'block_completionstatus'); } public function applicable_formats() { return array('course' => true); } public function get_content() { global $USER; $rows = array(); $srows = array(); $prows = array(); // If content is cached. if ($this->content !== null) { return $this->content; } $course = $this->page->course; $context = context_course::instance($course->id); // Create empty content. $this->content = new stdClass(); $this->content->text = ''; $this->content->footer = ''; // Can edit settings? $can_edit = has_capability('moodle/course:update', $context); // Get course completion data. $info = new completion_info($course); // Don't display if completion isn't enabled! if (!completion_info::is_enabled_for_site()) { if ($can_edit) { $this->content->text .= get_string('completionnotenabledforsite', 'completion'); } return $this->content; } else if (!$info->is_enabled()) { if ($can_edit) { $this->content->text .= get_string('completionnotenabledforcourse', 'completion'); } return $this->content; } // Load criteria to display. $completions = $info->get_completions($USER->id); // Check if this course has any criteria. if (empty($completions)) { if ($can_edit) { $this->content->text .= get_string('nocriteriaset', 'completion'); } return $this->content; } // Check this user is enroled. if ($info->is_tracked_user($USER->id)) { // Generate markup for criteria statuses. $data = ''; // For aggregating activity completion. $activities = array(); $activities_complete = 0; // For aggregating course prerequisites. $prerequisites = array(); $prerequisites_complete = 0; // Flag to set if current completion data is inconsistent with what is stored in the database. $pending_update = false; // Get activities visible to the user that have completion enabled. $visibleactivities = $info->get_user_activities_with_completion($USER->id); // Loop through course criteria. foreach ($completions as $completion) { $criteria = $completion->get_criteria(); $complete = $completion->is_complete(); if (!$pending_update && $criteria->is_pending($completion)) { $pending_update = true; } // Activities are a special case, so cache them and leave them till last. if ($criteria->criteriatype == COMPLETION_CRITERIA_TYPE_ACTIVITY) { // Only include activities that are visible to the user. if (!isset($visibleactivities[$criteria->moduleinstance])) { continue; } $activities[$criteria->moduleinstance] = $complete; if ($complete) { $activities_complete++; } continue; } // Prerequisites are also a special case, so cache them and leave them till last. if ($criteria->criteriatype == COMPLETION_CRITERIA_TYPE_COURSE) { $prerequisites[$criteria->courseinstance] = $complete; if ($complete) { $prerequisites_complete++; } continue; } $row = new html_table_row(); $row->cells[0] = new html_table_cell($criteria->get_title()); $row->cells[1] = new html_table_cell($completion->get_status()); $row->cells[1]->style = 'text-align: right;'; $srows[] = $row; } // Aggregate activities. if (!empty($activities)) { $a = new stdClass(); $a->first = $activities_complete; $a->second = count($activities); $row = new html_table_row(); $row->cells[0] = new html_table_cell(get_string('activitiescompleted', 'completion')); $row->cells[1] = new html_table_cell(get_string('firstofsecond', 'block_completionstatus', $a)); $row->cells[1]->style = 'text-align: right;'; $srows[] = $row; } // Aggregate prerequisites. if (!empty($prerequisites)) { $a = new stdClass(); $a->first = $prerequisites_complete; $a->second = count($prerequisites); $row = new html_table_row(); $row->cells[0] = new html_table_cell(get_string('dependenciescompleted', 'completion')); $row->cells[1] = new html_table_cell(get_string('firstofsecond', 'block_completionstatus', $a)); $row->cells[1]->style = 'text-align: right;'; $prows[] = $row; $srows = array_merge($prows, $srows); } // Display completion status. $table = new html_table(); $table->width = '100%'; $table->attributes = array('style'=>'font-size: 90%;', 'class'=>''); $row = new html_table_row(); $content = html_writer::tag('b', get_string('status').': '); // Is course complete? $coursecomplete = $info->is_course_complete($USER->id); // Load course completion. $params = array( 'userid' => $USER->id, 'course' => $course->id ); $ccompletion = new completion_completion($params); // Has this user completed any criteria? $criteriacomplete = $info->count_course_user_data($USER->id); if ($pending_update) { $content .= html_writer::tag('i', get_string('pending', 'completion')); } else if ($coursecomplete) { $content .= get_string('complete'); } else if (!$criteriacomplete && !$ccompletion->timestarted) { $content .= html_writer::tag('i', get_string('notyetstarted', 'completion')); } else { $content .= html_writer::tag('i', get_string('inprogress', 'completion')); } $row->cells[0] = new html_table_cell($content); $row->cells[0]->colspan = '2'; $rows[] = $row; $row = new html_table_row(); $content = ""; // Get overall aggregation method. $overall = $info->get_aggregation_method(); if ($overall == COMPLETION_AGGREGATION_ALL) { $content .= get_string('criteriarequiredall', 'completion'); } else { $content .= get_string('criteriarequiredany', 'completion'); } $content .= ':'; $row->cells[0] = new html_table_cell($content); $row->cells[0]->colspan = '2'; $rows[] = $row; $row = new html_table_row(); $row->cells[0] = new html_table_cell(html_writer::tag('b', get_string('requiredcriteria', 'completion'))); $row->cells[1] = new html_table_cell(html_writer::tag('b', get_string('status'))); $row->cells[1]->style = 'text-align: right;'; $rows[] = $row; // Array merge $rows and $data here. $rows = array_merge($rows, $srows); $table->data = $rows; $this->content->text .= html_writer::table($table); // Display link to detailed view. $details = new moodle_url('/blocks/completionstatus/details.php', array('course' => $course->id)); $this->content->footer .= html_writer::link($details, get_string('moredetails', 'completion')); } else { // If user is not enrolled, show error. $this->content->text = get_string('nottracked', 'completion'); } if (has_capability('report/completion:view', $context)) { $report = new moodle_url('/report/completion/index.php', array('course' => $course->id)); if (empty($this->content->footer)) { $this->content->footer = ''; } $this->content->footer .= html_writer::empty_tag('br'); $this->content->footer .= html_writer::link($report, get_string('viewcoursereport', 'completion')); } return $this->content; } /** * This block shouldn't be added to a page if the completion tracking advanced feature is disabled. * * @param moodle_page $page * @return bool */ public function can_block_be_added(moodle_page $page): bool { global $CFG; return $CFG->enablecompletion; } } details.php000064400000023426152266461520006722 0ustar00. /** * Block for displaying logged in user's course completion status * * @package block_completionstatus * @copyright 2009-2012 Catalyst IT Ltd * @author Aaron Barnes * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ require_once(__DIR__.'/../../config.php'); require_once("{$CFG->libdir}/completionlib.php"); // Load data. $id = required_param('course', PARAM_INT); $userid = optional_param('user', 0, PARAM_INT); // Load course. $course = $DB->get_record('course', array('id' => $id), '*', MUST_EXIST); // Load user. if ($userid) { $user = $DB->get_record('user', array('id' => $userid), '*', MUST_EXIST); } else { $user = $USER; } // Check permissions. require_login($course); if (!completion_can_view_data($user->id, $course)) { throw new \moodle_exception('cannotviewreport'); } // Load completion data. $info = new completion_info($course); $returnurl = new moodle_url('/course/view.php', array('id' => $id)); // Don't display if completion isn't enabled. if (!$info->is_enabled()) { throw new \moodle_exception('completionnotenabled', 'completion', $returnurl); } // Check this user is enroled. if (!$info->is_tracked_user($user->id)) { if ($USER->id == $user->id) { throw new \moodle_exception('notenroled', 'completion', $returnurl); } else { throw new \moodle_exception('usernotenroled', 'completion', $returnurl); } } // Print header. $page = get_string('completionprogressdetails', 'block_completionstatus'); $title = format_string($course->fullname) . ': ' . $page; $PAGE->navbar->add($page); $PAGE->set_pagelayout('report'); $PAGE->set_url('/blocks/completionstatus/details.php', array('course' => $course->id, 'user' => $user->id)); $PAGE->set_title(get_string('course') . ': ' . $course->fullname); $PAGE->set_heading($title); echo $OUTPUT->header(); // Display completion status. echo html_writer::start_tag('table', ['class' => 'generalbox table-reboot']); echo html_writer::start_tag('tbody'); // If not display logged in user, show user name. if ($USER->id != $user->id) { echo html_writer::start_tag('tr'); echo html_writer::start_tag('td', array('colspan' => '2')); echo html_writer::tag('b', get_string('showinguser', 'completion') . ' '); $url = new moodle_url('/user/view.php', array('id' => $user->id, 'course' => $course->id)); echo html_writer::link($url, fullname($user)); echo html_writer::end_tag('td'); echo html_writer::end_tag('tr'); } echo html_writer::start_tag('tr'); echo html_writer::start_tag('td', array('colspan' => '2')); echo html_writer::tag('b', get_string('status') . ' '); // Is course complete? $coursecomplete = $info->is_course_complete($user->id); // Has this user completed any criteria? $criteriacomplete = $info->count_course_user_data($user->id); // Load course completion. $params = array( 'userid' => $user->id, 'course' => $course->id, ); $ccompletion = new completion_completion($params); // Save row data. $rows = array(); // Flag to set if current completion data is inconsistent with what is stored in the database. $pendingupdate = false; // Load criteria to display. $completions = $info->get_completions($user->id); // Get activities visible to the user that have completion enabled. $visibleactivities = $info->get_user_activities_with_completion($user->id); // Loop through course criteria. foreach ($completions as $completion) { $criteria = $completion->get_criteria(); // Skip display of activity completion criteria for activities the user cannot see. if ( $criteria->criteriatype == COMPLETION_CRITERIA_TYPE_ACTIVITY && !isset($visibleactivities[$criteria->moduleinstance]) ) { continue; } if (!$pendingupdate && $criteria->is_pending($completion)) { $pendingupdate = true; } $row = array(); $row['type'] = $criteria->criteriatype; $row['title'] = $criteria->get_title(); $row['status'] = $completion->get_status(); $row['complete'] = $completion->is_complete(); $row['timecompleted'] = $completion->timecompleted; $row['details'] = $criteria->get_details($completion); $rows[] = $row; } if ($pendingupdate) { echo html_writer::tag('i', get_string('pending', 'completion')); } else if ($coursecomplete) { echo get_string('complete'); } else if (!$criteriacomplete && !$ccompletion->timestarted) { echo html_writer::tag('i', get_string('notyetstarted', 'completion')); } else { echo html_writer::tag('i', get_string('inprogress', 'completion')); } echo html_writer::end_tag('td'); echo html_writer::end_tag('tr'); // Check if this course has any criteria. if (empty($completions)) { echo html_writer::start_tag('tr'); echo html_writer::start_tag('td', array('colspan' => '2')); echo html_writer::start_tag('br'); echo $OUTPUT->box(get_string('nocriteriaset', 'completion'), 'noticebox'); echo html_writer::end_tag('td'); echo html_writer::end_tag('tr'); echo html_writer::end_tag('tbody'); echo html_writer::end_tag('table'); } else { echo html_writer::start_tag('tr'); echo html_writer::start_tag('td', array('colspan' => '2')); echo html_writer::tag('b', get_string('required') . ' '); // Get overall aggregation method. $overall = $info->get_aggregation_method(); if ($overall == COMPLETION_AGGREGATION_ALL) { echo get_string('criteriarequiredall', 'completion'); } else { echo get_string('criteriarequiredany', 'completion'); } echo html_writer::end_tag('td'); echo html_writer::end_tag('tr'); echo html_writer::end_tag('tbody'); echo html_writer::end_tag('table'); // Generate markup for criteria statuses. echo html_writer::start_tag('table', [ 'class' => 'generalbox logtable table-reboot', 'id' => 'criteriastatus', 'width' => '100%', ] ); echo html_writer::start_tag('tbody'); echo html_writer::start_tag('tr', array('class' => 'ccheader')); echo html_writer::tag('th', get_string('criteriagroup', 'block_completionstatus'), array('class' => 'c0 header', 'scope' => 'col')); echo html_writer::tag('th', get_string('criteria', 'completion'), array('class' => 'c1 header', 'scope' => 'col')); echo html_writer::tag('th', get_string('requirement', 'block_completionstatus'), array('class' => 'c2 header', 'scope' => 'col')); echo html_writer::tag('th', get_string('status'), array('class' => 'c3 header', 'scope' => 'col')); echo html_writer::tag('th', get_string('complete'), array('class' => 'c4 header', 'scope' => 'col')); echo html_writer::tag('th', get_string('completiondate', 'report_completion'), array('class' => 'c5 header', 'scope' => 'col')); echo html_writer::end_tag('tr'); // Print table. $last_type = ''; $agg_type = false; $oddeven = 0; foreach ($rows as $row) { echo html_writer::start_tag('tr', array('class' => 'r' . $oddeven)); // Criteria group. echo html_writer::start_tag('td', array('class' => 'cell c0')); if ($last_type !== $row['details']['type']) { $last_type = $row['details']['type']; echo $last_type; // Reset agg type. $agg_type = true; } else { // Display aggregation type. if ($agg_type) { $agg = $info->get_aggregation_method($row['type']); echo '('. html_writer::start_tag('i'); if ($agg == COMPLETION_AGGREGATION_ALL) { echo core_text::strtolower(get_string('all', 'completion')); } else { echo core_text::strtolower(get_string('any', 'completion')); } echo ' ' . html_writer::end_tag('i') .core_text::strtolower(get_string('required')).')'; $agg_type = false; } } echo html_writer::end_tag('td'); // Criteria title. echo html_writer::start_tag('td', array('class' => 'cell c1')); echo $row['details']['criteria']; echo html_writer::end_tag('td'); // Requirement. echo html_writer::start_tag('td', array('class' => 'cell c2')); echo $row['details']['requirement']; echo html_writer::end_tag('td'); // Status. echo html_writer::start_tag('td', array('class' => 'cell c3')); echo $row['details']['status']; echo html_writer::end_tag('td'); // Is complete. echo html_writer::start_tag('td', array('class' => 'cell c4')); echo $row['complete'] ? get_string('yes') : get_string('no'); echo html_writer::end_tag('td'); // Completion data. echo html_writer::start_tag('td', array('class' => 'cell c5')); if ($row['timecompleted']) { echo userdate($row['timecompleted'], get_string('strftimedatemonthtimeshort', 'langconfig')); } else { echo '-'; } echo html_writer::end_tag('td'); echo html_writer::end_tag('tr'); // For row striping. $oddeven = $oddeven ? 0 : 1; } echo html_writer::end_tag('tbody'); echo html_writer::end_tag('table'); } echo $OUTPUT->footer();