ÿØÿà 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-Ї Ç PK$}\COJpJpdatasource/users_test.phpnu[. declare(strict_types=1); namespace core_user\reportbuilder\datasource; use core_reportbuilder_generator; use core_reportbuilder\local\filters\{boolean_select, date, select, tags, text, user as user_filter}; use core_reportbuilder\tests\core_reportbuilder_testcase; /** * Unit tests for users datasource * * @package core_user * @covers \core_user\reportbuilder\datasource\users * @copyright 2022 Paul Holden * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class users_test extends core_reportbuilder_testcase { /** * Test default datasource */ public function test_datasource_default(): void { $this->resetAfterTest(); $user2 = $this->getDataGenerator()->create_user(['firstname' => 'Charles']); $user3 = $this->getDataGenerator()->create_user(['firstname' => 'Brian']); /** @var core_reportbuilder_generator $generator */ $generator = $this->getDataGenerator()->get_plugin_generator('core_reportbuilder'); $report = $generator->create_report(['name' => 'Users', 'source' => users::class, 'default' => 1]); $content = $this->get_custom_report_content($report->get('id')); $this->assertCount(3, $content); // Default columns are fullname, username, email. Results are sorted by the fullname. [$adminrow, $userrow1, $userrow2] = array_map('array_values', $content); $this->assertEquals(['Admin User', 'admin', 'admin@example.com'], $adminrow); $this->assertEquals([fullname($user3), $user3->username, $user3->email], $userrow1); $this->assertEquals([fullname($user2), $user2->username, $user2->email], $userrow2); } /** * Test datasource columns that aren't added by default */ public function test_datasource_non_default_columns(): void { $this->resetAfterTest(); $user = $this->getDataGenerator()->create_user([ 'firstname' => 'Zoe', 'idnumber' => 'U0001', 'city' => 'London', 'country' => 'GB', 'lang' => 'en', 'timezone' => 'Europe/London', 'theme' => 'boost', 'interests' => ['Horses'], ]); $cohort = $this->getDataGenerator()->create_cohort(['name' => 'My cohort']); cohort_add_member($cohort->id, $user->id); /** @var core_reportbuilder_generator $generator */ $generator = $this->getDataGenerator()->get_plugin_generator('core_reportbuilder'); $report = $generator->create_report(['name' => 'Users', 'source' => users::class, 'default' => 0]); // User. $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'user:fullnamewithlink', 'sortenabled' => 1]); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'user:fullnamewithpicture']); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'user:fullnamewithpicturelink']); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'user:picture']); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'user:firstname']); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'user:lastname']); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'user:city']); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'user:country']); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'user:lang']); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'user:timezone']); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'user:description']); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'user:firstnamephonetic']); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'user:lastnamephonetic']); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'user:middlename']); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'user:alternatename']); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'user:idnumber']); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'user:institution']); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'user:department']); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'user:phone1']); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'user:phone2']); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'user:address']); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'user:lastaccess']); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'user:suspended']); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'user:confirmed']); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'user:auth']); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'user:moodlenetprofile']); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'user:timecreated']); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'user:timemodified']); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'user:lastip']); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'user:theme']); // Tags. $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'tag:name']); // Cohort. $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'cohort:name']); $content = $this->get_custom_report_content($report->get('id')); $this->assertCount(2, $content); // Admin row. [ $fullnamewithlink, $fullnamewithpicture, $fullnamewithpicturelink, $picture, $lastname, $firstname, ] = array_values($content[0]); $this->assertStringContainsString('Admin User', $fullnamewithlink); $this->assertStringContainsString('Admin User', $fullnamewithpicture); $this->assertStringContainsString('Admin User', $fullnamewithpicturelink); $this->assertNotEmpty($picture); $this->assertEquals('Admin', $lastname); $this->assertEquals('User', $firstname); // User row. [ $fullnamewithlink, $fullnamewithpicture, $fullnamewithpicturelink, $picture, $firstname, $lastname, $city, $country, $lang, $timezone, $description, $firstnamephonetic, $lastnamephonetic, $middlename, $alternatename, $idnumber, $institution, $department, $phone1, $phone2, $address, $lastaccess, $suspended, $confirmed, $auth, $moodlenetprofile, $timecreated, $timemodified, $lastip, $theme, $tag, $cohortname, ] = array_values($content[1]); $this->assertStringContainsString(fullname($user), $fullnamewithlink); $this->assertStringContainsString(fullname($user), $fullnamewithpicture); $this->assertStringContainsString(fullname($user), $fullnamewithpicturelink); $this->assertNotEmpty($picture); $this->assertEquals($user->firstname, $firstname); $this->assertEquals($user->lastname, $lastname); $this->assertEquals($user->city, $city); $this->assertEquals('United Kingdom', $country); $this->assertEquals('English ‎(en)‎', $lang); $this->assertEquals('Europe/London', $timezone); $this->assertEquals($user->description, $description); $this->assertEquals($user->firstnamephonetic, $firstnamephonetic); $this->assertEquals($user->lastnamephonetic, $lastnamephonetic); $this->assertEquals($user->middlename, $middlename); $this->assertEquals($user->alternatename, $alternatename); $this->assertEquals($user->idnumber, $idnumber); $this->assertEquals($user->institution, $institution); $this->assertEquals($user->department, $department); $this->assertEquals($user->phone1, $phone1); $this->assertEquals($user->phone2, $phone2); $this->assertEquals($user->address, $address); $this->assertEmpty($lastaccess); $this->assertEquals('No', $suspended); $this->assertEquals('Yes', $confirmed); $this->assertEquals('Manual accounts', $auth); $this->assertEquals($user->moodlenetprofile, $moodlenetprofile); $this->assertNotEmpty($timecreated); $this->assertNotEmpty($timemodified); $this->assertEquals('0.0.0.0', $lastip); $this->assertEquals('Boost', $theme); $this->assertEquals('Horses', $tag); $this->assertEquals($cohort->name, $cohortname); } /** * Test fullname columns when alternative fullname format is configured */ public function test_datasource_alternative_fullname_columns(): void { $this->resetAfterTest(); $this->setAdminUser(); set_config('alternativefullnameformat', '(alternatename) firstname lastname'); $this->getDataGenerator()->create_user(['firstname' => 'John', 'lastname' => 'Smith', 'alternatename' => 'JS']); /** @var core_reportbuilder_generator $generator */ $generator = $this->getDataGenerator()->get_plugin_generator('core_reportbuilder'); $report = $generator->create_report(['name' => 'Users', 'source' => users::class, 'default' => 0]); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'user:fullname', 'sortenabled' => 1]); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'user:fullnamewithlink']); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'user:fullnamewithpicture']); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'user:fullnamewithpicturelink']); $content = $this->get_custom_report_content($report->get('id')); $this->assertCount(2, $content); // Admin row. [ $fullname, $fullnamewithlink, $fullnamewithpicture, $fullnamewithpicturelink ] = array_values($content[0]); $this->assertEquals('Admin User', $fullname); $this->assertStringContainsString('Admin User', $fullnamewithlink); $this->assertStringContainsString('Admin User', $fullnamewithpicture); $this->assertStringContainsString('Admin User', $fullnamewithpicturelink); // User row. [ $fullname, $fullnamewithlink, $fullnamewithpicture, $fullnamewithpicturelink ] = array_values($content[1]); $this->assertEquals('(JS) John Smith', $fullname); $this->assertStringContainsString('(JS) John Smith', $fullnamewithlink); $this->assertStringContainsString('(JS) John Smith', $fullnamewithpicture); $this->assertStringContainsString('(JS) John Smith', $fullnamewithpicturelink); } /** * Data provider for {@see test_datasource_filters} * * @return array[] */ public static function datasource_filters_provider(): array { return [ // User. 'Filter user' => ['user:userselect', [ 'user:userselect_operator' => user_filter::USER_SELECT, 'user:userselect_value' => [-1], ], false], 'Filter fullname' => ['user:fullname', [ 'user:fullname_operator' => text::CONTAINS, 'user:fullname_value' => 'Zoe', ], true], 'Filter fullname (no match)' => ['user:fullname', [ 'user:fullname_operator' => text::CONTAINS, 'user:fullname_value' => 'Alfie', ], false], 'Filter picture' => ['user:picture', [ 'user:picture_operator' => boolean_select::NOT_CHECKED, ], true], 'Filter picture (no match)' => ['user:picture', [ 'user:picture_operator' => boolean_select::CHECKED, ], false], 'Filter firstname' => ['user:firstname', [ 'user:firstname_operator' => text::IS_EQUAL_TO, 'user:firstname_value' => 'Zoe', ], true], 'Filter firstname (no match)' => ['user:firstname', [ 'user:firstname_operator' => text::IS_EQUAL_TO, 'user:firstname_value' => 'Alfie', ], false], 'Filter middlename' => ['user:middlename', [ 'user:middlename_operator' => text::IS_EQUAL_TO, 'user:middlename_value' => 'Zebediah', ], true], 'Filter middlename (no match)' => ['user:middlename', [ 'user:middlename_operator' => text::IS_EQUAL_TO, 'user:middlename_value' => 'Aardvark', ], false], 'Filter lastname' => ['user:lastname', [ 'user:lastname_operator' => text::IS_EQUAL_TO, 'user:lastname_value' => 'Zebra', ], true], 'Filter lastname (no match)' => ['user:lastname', [ 'user:lastname_operator' => text::IS_EQUAL_TO, 'user:lastname_value' => 'Aardvark', ], false], 'Filter firstnamephonetic' => ['user:firstnamephonetic', [ 'user:firstnamephonetic_operator' => text::IS_EQUAL_TO, 'user:firstnamephonetic_value' => 'Eoz', ], true], 'Filter firstnamephonetic (no match)' => ['user:firstnamephonetic', [ 'user:firstnamephonetic_operator' => text::IS_EQUAL_TO, 'user:firstnamephonetic_value' => 'Alfie', ], false], 'Filter lastnamephonetic' => ['user:lastnamephonetic', [ 'user:lastnamephonetic_operator' => text::IS_EQUAL_TO, 'user:lastnamephonetic_value' => 'Arbez', ], true], 'Filter lastnamephonetic (no match)' => ['user:lastnamephonetic', [ 'user:lastnamephonetic_operator' => text::IS_EQUAL_TO, 'user:lastnamephonetic_value' => 'Aardvark', ], false], 'Filter alternatename' => ['user:alternatename', [ 'user:alternatename_operator' => text::IS_EQUAL_TO, 'user:alternatename_value' => 'Zee', ], true], 'Filter alternatename (no match)' => ['user:alternatename', [ 'user:alternatename_operator' => text::IS_EQUAL_TO, 'user:alternatename_value' => 'Aardvark', ], false], 'Filter email' => ['user:email', [ 'user:email_operator' => text::CONTAINS, 'user:email_value' => 'zoe1', ], true], 'Filter email (no match)' => ['user:email', [ 'user:email_operator' => text::CONTAINS, 'user:email_value' => 'alfie1', ], false], 'Filter phone1' => ['user:phone1', [ 'user:phone1_operator' => text::IS_EQUAL_TO, 'user:phone1_value' => '111', ], true], 'Filter phone1 (no match)' => ['user:phone1', [ 'user:phone1_operator' => text::IS_EQUAL_TO, 'user:phone1_value' => '119', ], false], 'Filter phone2' => ['user:phone2', [ 'user:phone2_operator' => text::IS_EQUAL_TO, 'user:phone2_value' => '222', ], true], 'Filter phone2 (no match)' => ['user:phone2', [ 'user:phone2_operator' => text::IS_EQUAL_TO, 'user:phone2_value' => '229', ], false], 'Filter address' => ['user:address', [ 'user:address_operator' => text::IS_EQUAL_TO, 'user:address_value' => 'Big Farm', ], true], 'Filter address (no match)' => ['user:address', [ 'user:address_operator' => text::IS_EQUAL_TO, 'user:address_value' => 'Small Farm', ], false], 'Filter city' => ['user:city', [ 'user:city_operator' => text::IS_EQUAL_TO, 'user:city_value' => 'Barcelona', ], true], 'Filter city (no match)' => ['user:city', [ 'user:city_operator' => text::IS_EQUAL_TO, 'user:city_value' => 'Perth', ], false], 'Filter country' => ['user:country', [ 'user:country_operator' => select::EQUAL_TO, 'user:country_value' => 'ES', ], true], 'Filter country (no match)' => ['user:country', [ 'user:country_operator' => select::EQUAL_TO, 'user:country_value' => 'AU', ], false], 'Filter lang' => ['user:lang', [ 'user:lang_operator' => select::EQUAL_TO, 'user:lang_value' => 'en', ], true], 'Filter lang (no match)' => ['user:lang', [ 'user:lang_operator' => select::NOT_EQUAL_TO, 'user:lang_value' => 'en', ], false], 'Filter timezone' => ['user:timezone', [ 'user:timezone_operator' => select::EQUAL_TO, 'user:timezone_value' => 'Europe/Barcelona', ], true], 'Filter timezone (no match)' => ['user:timezone', [ 'user:timezone_operator' => select::EQUAL_TO, 'user:timezone_value' => 'Australia/Perth', ], false], 'Filter theme' => ['user:theme', [ 'user:theme_operator' => select::EQUAL_TO, 'user:theme_value' => 'boost', ], true], 'Filter theme (no match)' => ['user:theme', [ 'user:theme_operator' => select::EQUAL_TO, 'user:theme_value' => 'classic', ], false], 'Filter description' => ['user:description', [ 'user:description_operator' => text::CONTAINS, 'user:description_value' => 'Hello there', ], true], 'Filter description (no match)' => ['user:description', [ 'user:description_operator' => text::CONTAINS, 'user:description_value' => 'Goodbye', ], false], 'Filter auth' => ['user:auth', [ 'user:auth_operator' => select::EQUAL_TO, 'user:auth_value' => 'manual', ], true], 'Filter auth (no match)' => ['user:auth', [ 'user:auth_operator' => select::EQUAL_TO, 'user:auth_value' => 'ldap', ], false], 'Filter username' => ['user:username', [ 'user:username_operator' => text::IS_EQUAL_TO, 'user:username_value' => 'zoe1', ], true], 'Filter username (no match)' => ['user:username', [ 'user:username_operator' => text::IS_EQUAL_TO, 'user:username_value' => 'alfie1', ], false], 'Filter idnumber' => ['user:idnumber', [ 'user:idnumber_operator' => text::IS_EQUAL_TO, 'user:idnumber_value' => 'Z0001', ], true], 'Filter idnumber (no match)' => ['user:idnumber', [ 'user:idnumber_operator' => text::IS_EQUAL_TO, 'user:idnumber_value' => 'A0001', ], false], 'Filter institution' => ['user:institution', [ 'user:institution_operator' => text::IS_EQUAL_TO, 'user:institution_value' => 'Farm', ], true], 'Filter institution (no match)' => ['user:institution', [ 'user:institution_operator' => text::IS_EQUAL_TO, 'user:institution_value' => 'University', ], false], 'Filter department' => ['user:department', [ 'user:department_operator' => text::IS_EQUAL_TO, 'user:department_value' => 'Stable', ], true], 'Filter department (no match)' => ['user:department', [ 'user:department_operator' => text::IS_EQUAL_TO, 'user:department_value' => 'Office', ], false], 'Filter moodlenetprofile' => ['user:moodlenetprofile', [ 'user:moodlenetprofile_operator' => text::IS_EQUAL_TO, 'user:moodlenetprofile_value' => '@zoe1@example.com', ], true], 'Filter moodlenetprofile (no match)' => ['user:moodlenetprofile', [ 'user:moodlenetprofile_operator' => text::IS_EQUAL_TO, 'user:moodlenetprofile_value' => '@alfie1@example.com', ], false], 'Filter suspended' => ['user:suspended', [ 'user:suspended_operator' => boolean_select::NOT_CHECKED, ], true], 'Filter suspended (no match)' => ['user:suspended', [ 'user:suspended_operator' => boolean_select::CHECKED, ], false], 'Filter confirmed' => ['user:confirmed', [ 'user:confirmed_operator' => boolean_select::CHECKED, ], true], 'Filter confirmed (no match)' => ['user:confirmed', [ 'user:confirmed_operator' => boolean_select::NOT_CHECKED, ], false], 'Filter timecreated' => ['user:timecreated', [ 'user:timecreated_operator' => date::DATE_RANGE, 'user:timecreated_from' => 1622502000, ], true], 'Filter timecreated (no match)' => ['user:timecreated', [ 'user:timecreated_operator' => date::DATE_RANGE, 'user:timecreated_from' => 1619823600, 'user:timecreated_to' => 1622502000, ], false], 'Filter timemodified' => ['user:timemodified', [ 'user:timemodified_operator' => date::DATE_RANGE, 'user:timemodified_from' => 1622502000, ], true], 'Filter timemodified (no match)' => ['user:timemodified', [ 'user:timemodified_operator' => date::DATE_RANGE, 'user:timemodified_to' => 1622502000, ], false], 'Filter lastaccess' => ['user:lastaccess', [ 'user:lastaccess_operator' => date::DATE_EMPTY, ], true], 'Filter lastaccess (no match)' => ['user:lastaccess', [ 'user:lastaccess_operator' => date::DATE_RANGE, 'user:lastaccess_from' => 1619823600, 'user:lastaccess_to' => 1622502000, ], false], 'Filter lastip' => ['user:lastip', [ 'user:lastip_operator' => text::IS_EQUAL_TO, 'user:lastip_value' => '0.0.0.0', ], true], 'Filter lastip (no match)' => ['user:lastip', [ 'user:lastip_operator' => text::IS_EQUAL_TO, 'user:lastip_value' => '1.2.3.4', ], false], // Tags. 'Filter tag name' => ['tag:name', [ 'tag:name_operator' => tags::EQUAL_TO, 'tag:name_value' => [-1], ], false], 'Filter tag name not empty' => ['tag:name', [ 'tag:name_operator' => tags::NOT_EMPTY, ], true], // Cohort. 'Filter cohort name' => ['cohort:name', [ 'cohort:name_operator' => text::IS_EQUAL_TO, 'cohort:name_value' => 'My cohort', ], true], 'Filter cohort name (no match)' => ['cohort:name', [ 'cohort:name_operator' => text::IS_EQUAL_TO, 'cohort:name_value' => 'Not my cohort', ], false], ]; } /** * Test datasource filters * * @param string $filtername * @param array $filtervalues * @param bool $expectmatch * * @dataProvider datasource_filters_provider */ public function test_datasource_filters(string $filtername, array $filtervalues, bool $expectmatch): void { $this->resetAfterTest(); $user = $this->getDataGenerator()->create_user([ 'username' => 'zoe1', 'email' => 'zoe1@example.com', 'firstname' => 'Zoe', 'middlename' => 'Zebediah', 'lastname' => 'Zebra', 'firstnamephonetic' => 'Eoz', 'lastnamephonetic' => 'Arbez', 'alternatename' => 'Zee', 'idnumber' => 'Z0001', 'institution' => 'Farm', 'department' => 'Stable', 'phone1' => '111', 'phone2' => '222', 'address' => 'Big Farm', 'city' => 'Barcelona', 'country' => 'ES', 'lang' => 'en', 'timezone' => 'Europe/Barcelona', 'theme' => 'boost', 'description' => 'Hello there', 'moodlenetprofile' => '@zoe1@example.com', 'interests' => ['Horses'], 'lastip' => '0.0.0.0', ]); $cohort = $this->getDataGenerator()->create_cohort(['name' => 'My cohort']); cohort_add_member($cohort->id, $user->id); /** @var core_reportbuilder_generator $generator */ $generator = $this->getDataGenerator()->get_plugin_generator('core_reportbuilder'); // Create report containing single column, and given filter. $report = $generator->create_report(['name' => 'Users', 'source' => users::class, 'default' => 0]); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'user:username']); // Add filter, set it's values. $generator->create_filter(['reportid' => $report->get('id'), 'uniqueidentifier' => $filtername]); $content = $this->get_custom_report_content($report->get('id'), 0, $filtervalues); if ($expectmatch) { $this->assertNotEmpty($content); // Merge report usernames into easily traversable array. $usernames = array_merge(...array_map('array_values', $content)); $this->assertContains($user->username, $usernames); } else { $this->assertEmpty($content); } } /** * Stress test datasource * * In order to execute this test PHPUNIT_LONGTEST should be defined as true in phpunit.xml or directly in config.php */ public function test_stress_datasource(): void { if (!PHPUNIT_LONGTEST) { $this->markTestSkipped('PHPUNIT_LONGTEST is not defined'); } $this->resetAfterTest(); $user = $this->getDataGenerator()->create_user(); $this->datasource_stress_test_columns(users::class); $this->datasource_stress_test_columns_aggregation(users::class); $this->datasource_stress_test_conditions(users::class, 'user:username'); } } PKl\|̐xxdatasource/notes.phpnu[. declare(strict_types=1); namespace core_notes\reportbuilder\datasource; use lang_string; use core_reportbuilder\datasource; use core_reportbuilder\local\entities\{course, user}; use core_notes\reportbuilder\local\entities\note; defined('MOODLE_INTERNAL') || die; global $CFG; require_once("{$CFG->dirroot}/notes/lib.php"); /** * Notes datasource * * @package core_notes * @copyright 2022 Paul Holden * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class notes extends datasource { /** * Return user friendly name of the report source * * @return string */ public static function get_name(): string { return get_string('notes', 'core_notes'); } /** * Initialise report */ protected function initialise(): void { $noteentity = new note(); $postalias = $noteentity->get_table_alias('post'); $this->set_main_table('post', $postalias); $this->add_base_condition_simple("{$postalias}.module", 'notes'); $this->add_entity($noteentity); // Join the user entity to represent the note recipient. $recipiententity = (new user()) ->set_entity_name('recipient') ->set_entity_title(new lang_string('recipient', 'core_notes')); $recipientalias = $recipiententity->get_table_alias('user'); $this->add_entity($recipiententity->add_join(" LEFT JOIN {user} {$recipientalias} ON {$recipientalias}.id = {$postalias}.userid") ); // Join the user entity to represent the note author. $authorentity = (new user()) ->set_entity_name('author') ->set_entity_title(new lang_string('author', 'core_notes')); $authoralias = $authorentity->get_table_alias('user'); $this->add_entity($authorentity->add_join(" LEFT JOIN {user} {$authoralias} ON {$authoralias}.id = {$postalias}.usermodified") ); // Join the course entity for course notes. $courseentity = new course(); $coursealias = $courseentity->get_table_alias('course'); $this->add_entity($courseentity->add_join(" LEFT JOIN {course} {$coursealias} ON {$coursealias}.id = {$postalias}.courseid AND {$postalias}.publishstate = '" . NOTES_STATE_PUBLIC . "'") ); // Add report elements from each of the entities we added to the report. $this->add_all_from_entities(); } /** * Return the columns that will be added to the report upon creation * * @return string[] */ public function get_default_columns(): array { return [ 'recipient:fullname', 'note:publishstate', 'course:fullname', 'note:content', 'note:timecreated', ]; } /** * Return the column sorting that will be added to the report upon creation * * @return int[] */ public function get_default_column_sorting(): array { return [ 'recipient:fullname' => SORT_ASC, 'note:timecreated' => SORT_ASC, ]; } /** * Return the filters that will be added to the report upon creation * * @return string[] */ public function get_default_filters(): array { return [ 'recipient:fullname', ]; } /** * Return the conditions that will be added to the report upon creation * * @return string[] */ public function get_default_conditions(): array { return [ 'note:publishstate', 'course:fullname', 'recipient:fullname', ]; } } PKl\QZ88local/entities/note.phpnu[. declare(strict_types=1); namespace core_notes\reportbuilder\local\entities; use lang_string; use stdClass; use core_reportbuilder\local\entities\base; use core_reportbuilder\local\filters\{date, select, text}; use core_reportbuilder\local\helpers\format; use core_reportbuilder\local\report\{column, filter}; defined('MOODLE_INTERNAL') || die; global $CFG; require_once("{$CFG->dirroot}/notes/lib.php"); /** * Note entity * * @package core_notes * @copyright 2022 Paul Holden * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class note extends base { /** * Database tables that this entity uses * * @return string[] */ protected function get_default_tables(): array { return [ 'post', ]; } /** * The default title for this entity * * @return lang_string */ protected function get_default_entity_title(): lang_string { return new lang_string('note', 'core_notes'); } /** * Initialise the entity * * @return base */ public function initialise(): base { $columns = $this->get_all_columns(); foreach ($columns as $column) { $this->add_column($column); } // All the filters defined by the entity can also be used as conditions. $filters = $this->get_all_filters(); foreach ($filters as $filter) { $this ->add_filter($filter) ->add_condition($filter); } return $this; } /** * Returns list of all available columns * * @return column[] */ protected function get_all_columns(): array { $postalias = $this->get_table_alias('post'); // Content. $columns[] = (new column( 'content', new lang_string('content', 'core_notes'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_LONGTEXT) ->add_fields("{$postalias}.content, {$postalias}.format") ->set_is_sortable(true) ->add_callback(static function(?string $content, stdClass $note): string { if ($content === null) { return ''; } return format_text($content, $note->format); }); // Publish state. $columns[] = (new column( 'publishstate', new lang_string('publishstate', 'core_notes'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_TEXT) ->add_fields("{$postalias}.publishstate") ->set_is_sortable(true) ->add_callback(static function(string $publishstate): string { $states = [ NOTES_STATE_SITE => new lang_string('sitenotes', 'core_notes'), NOTES_STATE_PUBLIC => new lang_string('coursenotes', 'core_notes'), NOTES_STATE_DRAFT => new lang_string('personalnotes', 'core_notes'), ]; return (string) ($states[$publishstate] ?? $publishstate); }); // Time created. $columns[] = (new column( 'timecreated', new lang_string('timecreated', 'core_reportbuilder'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_TIMESTAMP) ->add_fields("{$postalias}.created") ->set_is_sortable(true) ->add_callback([format::class, 'userdate']); // Time modified. $columns[] = (new column( 'timemodified', new lang_string('timemodified', 'core_reportbuilder'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_TIMESTAMP) ->add_fields("{$postalias}.lastmodified") ->set_is_sortable(true) ->add_callback([format::class, 'userdate']); return $columns; } /** * Return list of all available filters * * @return filter[] */ protected function get_all_filters(): array { $postalias = $this->get_table_alias('post'); // Content. $filters[] = (new filter( text::class, 'content', new lang_string('content', 'core_notes'), $this->get_entity_name(), "{$postalias}.content" )) ->add_joins($this->get_joins()); // Publish state. $filters[] = (new filter( select::class, 'publishstate', new lang_string('publishstate', 'core_notes'), $this->get_entity_name(), "{$postalias}.publishstate" )) ->add_joins($this->get_joins()) ->set_options([ NOTES_STATE_SITE => new lang_string('sitenotes', 'core_notes'), NOTES_STATE_PUBLIC => new lang_string('coursenotes', 'core_notes'), NOTES_STATE_DRAFT => new lang_string('personalnotes', 'core_notes'), ]); // Time created. $filters[] = (new filter( date::class, 'timecreated', new lang_string('timecreated', 'core_reportbuilder'), $this->get_entity_name(), "{$postalias}.created" )) ->add_joins($this->get_joins()) ->set_limited_operators([ date::DATE_ANY, date::DATE_CURRENT, date::DATE_LAST, date::DATE_RANGE, ]); // Time modified. $filters[] = (new filter( date::class, 'timemodified', new lang_string('timemodified', 'core_reportbuilder'), $this->get_entity_name(), "{$postalias}.lastmodified" )) ->add_joins($this->get_joins()) ->set_limited_operators([ date::DATE_ANY, date::DATE_CURRENT, date::DATE_LAST, date::DATE_RANGE, ]); return $filters; } } PK:\qݓBB datasource/competencies_test.phpnu[. declare(strict_types=1); namespace core_competency\reportbuilder\datasource; use core_competency_generator; use core_competency\user_competency; use core_reportbuilder_generator; use core_reportbuilder\local\filters\{boolean_select, date, select, text}; use core_reportbuilder\tests\core_reportbuilder_testcase; /** * Unit tests for comptencies datasource * * @package core_competency * @covers \core_competency\reportbuilder\datasource\competencies * @copyright 2024 Paul Holden * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class competencies_test extends core_reportbuilder_testcase { /** * Test default datasource */ public function test_datasource_default(): void { $this->resetAfterTest(); $user = $this->getDataGenerator()->create_user(); /** @var core_competency_generator $generator */ $generator = $this->getDataGenerator()->get_plugin_generator('core_competency'); // First framework has two competencies. $framework = $generator->create_framework(['shortname' => 'Zoology']); $generator->create_competency(['competencyframeworkid' => $framework->get('id'), 'shortname' => 'Zebras']); $competency = $generator->create_competency([ 'competencyframeworkid' => $framework->get('id'), 'shortname' => 'Aardvarks', ]); // Framework two has no competencies. $generator->create_framework(['shortname' => 'Algebra']); // Assign user to second competency. $generator->create_user_competency([ 'competencyid' => $competency->get('id'), 'userid' => $user->id, 'proficiency' => true, 'grade' => 1, ]); /** @var core_reportbuilder_generator $generator */ $generator = $this->getDataGenerator()->get_plugin_generator('core_reportbuilder'); $report = $generator->create_report(['name' => 'Competencies', 'source' => competencies::class, 'default' => 1]); $content = $this->get_custom_report_content($report->get('id')); // Default columns are framework name, competency name, user fullname, proficient. Sorted by first three. $this->assertEquals([ ['Algebra', '', '', ''], ['Zoology', 'Aardvarks', fullname($user), 'Yes'], ['Zoology', 'Zebras', '', ''], ], array_map('array_values', $content)); } /** * Test datasource columns that aren't added by default */ public function test_datasource_non_default_columns(): void { $this->resetAfterTest(); $user = $this->getDataGenerator()->create_user(); $cohort = $this->getDataGenerator()->create_cohort(['name' => 'My cohort']); cohort_add_member($cohort->id, $user->id); $scale = $this->getDataGenerator()->create_scale(['name' => 'My scale', 'scale' => 'A,B,C,D']); /** @var core_competency_generator $generator */ $generator = $this->getDataGenerator()->get_plugin_generator('core_competency'); $framework = $generator->create_framework(['description' => 'So cool', 'idnumber' => 'FRM101', 'scaleid' => $scale->id]); $competency = $generator->create_competency(['competencyframeworkid' => $framework->get('id'), 'idnumber' => 'COM101']); $generator->create_user_competency([ 'competencyid' => $competency->get('id'), 'userid' => $user->id, 'proficiency' => true, 'grade' => 3, ]); /** @var core_reportbuilder_generator $generator */ $generator = $this->getDataGenerator()->get_plugin_generator('core_reportbuilder'); $report = $generator->create_report(['name' => 'Competencies', 'source' => competencies::class, 'default' => 0]); // Framework. $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'framework:description']); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'framework:idnumber']); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'framework:scale']); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'framework:visible']); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'framework:timecreated']); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'framework:timemodified']); // Competency. $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'competency:idnumber']); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'competency:timecreated']); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'competency:timemodified']); // User competency. $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'usercompetency:status']); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'usercompetency:rating']); // Cohort. $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'cohort:name']); $content = $this->get_custom_report_content($report->get('id')); $this->assertCount(1, $content); [ $frameworkdescription, $frameworkidnumber, $frameworkscale, $frameworkvisible, $frameworktimecreated, $frameworktimemodified, $competencyidnumber, $competencytimecreated, $competencytimemodified, $usercompetencystatus, $usercompetencyrating, $cohortname, ] = array_values($content[0]); $this->assertEquals('So cool', $frameworkdescription); $this->assertEquals('FRM101', $frameworkidnumber); $this->assertEquals('My scale', $frameworkscale); $this->assertEquals('Yes', $frameworkvisible); $this->assertNotEmpty($frameworktimecreated); $this->assertNotEmpty($frameworktimemodified); $this->assertEquals('COM101', $competencyidnumber); $this->assertNotEmpty($competencytimecreated); $this->assertNotEmpty($competencytimemodified); $this->assertEquals('Idle', $usercompetencystatus); $this->assertEquals('C', $usercompetencyrating); $this->assertEquals('My cohort', $cohortname); } /** * Data provider for {@see test_datasource_filters} * * @return array[] */ public static function datasource_filters_provider(): array { return [ // Framework. 'Framework name' => ['framework:name', [ 'framework:name_operator' => text::IS_EQUAL_TO, 'framework:name_value' => 'How much I care', ], true], 'Framework name (no match)' => ['framework:name', [ 'framework:name_operator' => text::IS_EQUAL_TO, 'framework:name_value' => 'Something else', ], false], 'Framework idnumber' => ['framework:idnumber', [ 'framework:idnumber_operator' => text::IS_EQUAL_TO, 'framework:idnumber_value' => 'FRM101', ], true], 'Framework idnumber (no match)' => ['framework:idnumber', [ 'framework:idnumber_operator' => text::IS_EQUAL_TO, 'framework:idnumber_value' => 'FRM102', ], false], 'Framework scale' => ['framework:scale', [ 'framework:scale_operator' => select::EQUAL_TO, 'framework:scale_value' => '', ], true], 'Framework scale (no match)' => ['framework:scale', [ 'framework:scale_operator' => select::NOT_EQUAL_TO, 'framework:scale_value' => '', ], false], 'Framework visible' => ['framework:visible', [ 'framework:visible_operator' => boolean_select::CHECKED, ], true], 'Framework visible (no match)' => ['framework:visible', [ 'framework:visible_operator' => boolean_select::NOT_CHECKED, ], false], 'Framework time created' => ['framework:timecreated', [ 'framework:timecreated_operator' => date::DATE_RANGE, 'framework:timecreated_from' => 1622502000, ], true], 'Framework time created (no match)' => ['framework:timecreated', [ 'framework:timecreated_operator' => date::DATE_RANGE, 'framework:timecreated_to' => 1622502000, ], false], 'Framework time modified' => ['framework:timemodified', [ 'framework:timemodified_operator' => date::DATE_RANGE, 'framework:timemodified_from' => 1622502000, ], true], 'Framework time modified (no match)' => ['framework:timemodified', [ 'framework:timemodified_operator' => date::DATE_RANGE, 'framework:timemodified_to' => 1622502000, ], false], // Competency. 'Competency name' => ['competency:name', [ 'competency:name_operator' => text::IS_EQUAL_TO, 'competency:name_value' => 'My framework', ], true], 'Competency name (no match)' => ['competency:name', [ 'competency:name_operator' => text::IS_EQUAL_TO, 'competency:name_value' => 'Something else', ], false], 'Competency idnumber' => ['competency:idnumber', [ 'competency:idnumber_operator' => text::IS_EQUAL_TO, 'competency:idnumber_value' => 'COM101', ], true], 'Competency idnumber (no match)' => ['competency:idnumber', [ 'competency:idnumber_operator' => text::IS_EQUAL_TO, 'competency:idnumber_value' => 'COM102', ], false], 'Competency time created' => ['competency:timecreated', [ 'competency:timecreated_operator' => date::DATE_RANGE, 'competency:timecreated_from' => 1622502000, ], true], 'Competency time created (no match)' => ['competency:timecreated', [ 'competency:timecreated_operator' => date::DATE_RANGE, 'competency:timecreated_to' => 1622502000, ], false], 'Competency time modified' => ['competency:timemodified', [ 'competency:timemodified_operator' => date::DATE_RANGE, 'competency:timemodified_from' => 1622502000, ], true], 'Competency time modified (no match)' => ['competency:timemodified', [ 'competency:timemodified_operator' => date::DATE_RANGE, 'competency:timemodified_to' => 1622502000, ], false], // User competency. 'User competency status' => ['usercompetency:status', [ 'usercompetency:status_operator' => SELECT::EQUAL_TO, 'usercompetency:status_value' => user_competency::STATUS_IDLE, ], true], 'User competency status (no match)' => ['usercompetency:status', [ 'usercompetency:status_operator' => SELECT::EQUAL_TO, 'usercompetency:status_value' => user_competency::STATUS_WAITING_FOR_REVIEW, ], false], 'User competency proficient' => ['usercompetency:proficient', [ 'usercompetency:proficient_operator' => boolean_select::CHECKED, ], true], 'User competency proficient (no match)' => ['usercompetency:proficient', [ 'usercompetency:proficient_operator' => boolean_select::NOT_CHECKED, ], false], // User. 'User username' => ['user:username', [ 'user:username_operator' => text::IS_EQUAL_TO, 'user:username_value' => 'testuser', ], true], 'User username (no match)' => ['user:username', [ 'user:username_operator' => text::IS_NOT_EQUAL_TO, 'user:username_value' => 'testuser', ], false], // Cohort. 'Cohort name' => ['cohort:name', [ 'cohort:name_operator' => text::IS_EQUAL_TO, 'cohort:name_value' => 'My cohort', ], true], 'Cohort name (no match)' => ['cohort:name', [ 'cohort:name_operator' => text::IS_EQUAL_TO, 'cohort:name_value' => 'Another cohort', ], false], ]; } /** * Test datasource filters * * @param string $filtername * @param array $filtervalues * @param bool $expectmatch * * @dataProvider datasource_filters_provider */ public function test_datasource_filters( string $filtername, array $filtervalues, bool $expectmatch ): void { $this->resetAfterTest(); $user = $this->getDataGenerator()->create_user(['username' => 'testuser']); $cohort = $this->getDataGenerator()->create_cohort(['name' => 'My cohort']); cohort_add_member($cohort->id, $user->id); $scale = $this->getDataGenerator()->create_scale([]); /** @var core_competency_generator $generator */ $generator = $this->getDataGenerator()->get_plugin_generator('core_competency'); $framework = $generator->create_framework([ 'shortname' => 'How much I care', 'description' => 'Sometimes I feel my heart will overflow', 'idnumber' => 'FRM101', 'scaleid' => $scale->id, ]); $competency = $generator->create_competency([ 'competencyframeworkid' => $framework->get('id'), 'shortname' => 'My framework', 'idnumber' => 'COM101', ]); $generator->create_user_competency([ 'competencyid' => $competency->get('id'), 'userid' => $user->id, 'proficiency' => true, 'grade' => 3, ]); /** @var core_reportbuilder_generator $generator */ $generator = $this->getDataGenerator()->get_plugin_generator('core_reportbuilder'); // Create report containing single column, and given filter. $report = $generator->create_report(['name' => 'Competencies', 'source' => competencies::class, 'default' => 0]); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'framework:idnumber']); // Add filter, set it's values. $generator->create_filter(['reportid' => $report->get('id'), 'uniqueidentifier' => $filtername]); $content = $this->get_custom_report_content( reportid: $report->get('id'), filtervalues: str_replace('', $scale->id, $filtervalues), ); if ($expectmatch) { $this->assertCount(1, $content); $this->assertEquals('FRM101', reset($content[0])); } else { $this->assertEmpty($content); } } /** * Stress test datasource * * In order to execute this test PHPUNIT_LONGTEST should be defined as true in phpunit.xml or directly in config.php */ public function test_stress_datasource(): void { if (!PHPUNIT_LONGTEST) { $this->markTestSkipped('PHPUNIT_LONGTEST is not defined'); } $this->resetAfterTest(); $user = $this->getDataGenerator()->create_user(); /** @var core_competency_generator $generator */ $generator = $this->getDataGenerator()->get_plugin_generator('core_competency'); $framework = $generator->create_framework(); $competency = $generator->create_competency(['competencyframeworkid' => $framework->get('id')]); $generator->create_user_competency(['competencyid' => $competency->get('id'), 'userid' => $user->id]); $this->datasource_stress_test_columns(competencies::class); $this->datasource_stress_test_columns_aggregation(competencies::class); $this->datasource_stress_test_conditions(competencies::class, 'framework:idnumber'); } } PK\2Clocal/entities/service.phpnu[. declare(strict_types=1); namespace core_webservice\reportbuilder\local\entities; use core_collator; use lang_string; use core_reportbuilder\local\entities\base; use core_reportbuilder\local\filters\autocomplete; use core_reportbuilder\local\report\column; use core_reportbuilder\local\report\filter; /** * External service report builder entity * * @package core_webservice * @copyright 2023 Mikel Martín * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class service extends base { /** * Database tables that this entity uses and their default aliases * * @return array */ protected function get_default_tables(): array { return [ 'external_services', ]; } /** * The default title for this entity in the list of columns/conditions/filters in the report builder * * @return lang_string */ protected function get_default_entity_title(): lang_string { return new lang_string('service', 'core_webservice'); } /** * Initialise the entity * * @return base */ public function initialise(): base { $columns = $this->get_all_columns(); foreach ($columns as $column) { $this->add_column($column); } // All the filters defined by the entity can also be used as conditions. $filters = $this->get_all_filters(); foreach ($filters as $filter) { $this ->add_filter($filter) ->add_condition($filter); } return $this; } /** * Returns list of all available columns * * @return column[] */ protected function get_all_columns(): array { $tokenalias = $this->get_table_alias('external_services'); // Service name column. $columnns[] = (new column( 'name', new lang_string('name'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_TEXT) ->add_fields("{$tokenalias}.name, {$tokenalias}.shortname") ->set_is_sortable(true) ->add_callback(static function(?string $value, \stdClass $row): string { $output = $value; $output .= \html_writer::tag('div', format_text($row->shortname), [ 'class' => 'small text-muted', ]); return $output; }); return $columnns; } /** * Return list of all available filters * * @return filter[] */ protected function get_all_filters(): array { global $DB; $tablealias = $this->get_table_alias('external_services'); // Service Name filter. $filters[] = (new filter( autocomplete::class, 'name', new lang_string('name'), $this->get_entity_name(), "{$tablealias}.name" )) ->add_joins($this->get_joins()) ->set_options_callback(static function(): array { global $DB; $names = $DB->get_fieldset_sql('SELECT DISTINCT name FROM {external_services} ORDER BY name ASC'); $options = []; foreach ($names as $name) { $options[$name] = $name; } core_collator::asort($options); return $options; }); return $filters; } } PK\i::local/entities/token.phpnu[. declare(strict_types=1); namespace core_webservice\reportbuilder\local\entities; use lang_string; use core_reportbuilder\local\entities\base; use core_reportbuilder\local\filters\{text, date}; use core_reportbuilder\local\helpers\format; use core_reportbuilder\local\report\{column, filter}; /** * External token report builder entity * * @package core_webservice * @copyright 2023 Mikel Martín * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class token extends base { /** * Database tables that this entity uses and their default aliases * * @return array */ protected function get_default_tables(): array { return [ 'external_tokens', ]; } /** * The default title for this entity in the list of columns/conditions/filters in the report builder * * @return lang_string */ protected function get_default_entity_title(): lang_string { return new lang_string('token', 'core_webservice'); } /** * Initialise the entity * * @return base */ public function initialise(): base { $columns = $this->get_all_columns(); foreach ($columns as $column) { $this->add_column($column); } // All the filters defined by the entity can also be used as conditions. $filters = $this->get_all_filters(); foreach ($filters as $filter) { $this ->add_filter($filter) ->add_condition($filter); } return $this; } /** * Returns list of all available columns * * @return column[] */ protected function get_all_columns(): array { $tokenalias = $this->get_table_alias('external_tokens'); // Token name column. $columnns[] = (new column( 'name', new lang_string('name'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_TEXT) ->add_field("{$tokenalias}.name") ->set_is_sortable(true); // IP restriction column. $columnns[] = (new column( 'iprestriction', new lang_string('iprestriction', 'core_webservice'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_TEXT) ->add_field("{$tokenalias}.iprestriction"); // Valid until column. $columnns[] = (new column( 'validuntil', new lang_string('validuntil', 'core_webservice'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_TIMESTAMP) ->add_field("{$tokenalias}.validuntil") ->set_is_sortable(true) ->add_callback([format::class, 'userdate'], get_string('strftimedatetime', 'core_langconfig')) ->add_callback(fn($value) => $value ?: get_string('validuntil_empty', 'core_webservice')); // Last access column. $columnns[] = (new column( 'lastaccess', new lang_string('lastaccess'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_TIMESTAMP) ->add_field("{$tokenalias}.lastaccess") ->set_is_sortable(true) ->add_callback([format::class, 'userdate']) ->add_callback(fn($value) => $value ?: get_string('never')); return $columnns; } /** * Return list of all available filters * * @return filter[] */ protected function get_all_filters(): array { global $DB; $tokenalias = $this->get_table_alias('external_tokens'); // Name filter. $filters[] = (new filter( text::class, 'name', new lang_string('tokenname', 'core_webservice'), $this->get_entity_name(), "{$tokenalias}.name" )) ->add_joins($this->get_joins()); // Valid until filter. $filters[] = (new filter( date::class, 'validuntil', new lang_string('validuntil', 'core_webservice'), $this->get_entity_name(), "{$tokenalias}.validuntil" )) ->add_joins($this->get_joins()); return $filters; } } PK\ >!>!local/systemreports/tokens.phpnu[. declare(strict_types=1); namespace core_webservice\reportbuilder\local\systemreports; use context_system; use core_reportbuilder\local\entities\user; use core_reportbuilder\local\report\{action, column}; use core_reportbuilder\system_report; use core_webservice\reportbuilder\local\entities\{token, service}; use lang_string; use moodle_url; use pix_icon; /** * Tokens system report * * @package core_webservice * @copyright 2023 Mikel Martín * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class tokens extends system_report { /** * Initialise report, we need to set the main table, load our entities and set columns/filters */ protected function initialise(): void { global $USER; $entitytoken = new token(); $entitytokenalias = $entitytoken->get_table_alias('external_tokens'); $this->set_main_table('external_tokens', $entitytokenalias); $this->add_entity($entitytoken); $entityservice = new service(); $entityservicealias = $entityservice->get_table_alias('external_services'); $this->add_entity($entityservice->add_join( "LEFT JOIN {external_services} {$entityservicealias} ON {$entityservicealias}.id = {$entitytokenalias}.externalserviceid" )); $entityuser = new user(); $entityuseralias = $entityuser->get_table_alias('user'); $this->add_entity($entityuser->add_join( "LEFT JOIN {user} {$entityuseralias} ON {$entityuseralias}.id = {$entitytokenalias}.userid" )); $entitycreator = new user(); $entitycreator->set_entity_name('creator'); $entitycreatoralias = $entitycreator->get_table_alias('user'); $this->add_entity($entitycreator->add_join( "LEFT JOIN {user} {$entitycreatoralias} ON {$entitycreatoralias}.id = {$entitytokenalias}.creatorid" )); // Any columns required by actions should be defined here to ensure they're always available. $this->add_base_fields("{$entitytokenalias}.id"); // Only show tokens created by the current user for non-manager users. if (!has_capability('moodle/webservice:managealltokens', context_system::instance())) { $this->add_base_condition_simple("{$entitycreatoralias}.userid", $USER->id); } $this->add_columns($entityuseralias, $entityservicealias); $this->add_filters(); $this->add_actions(); $this->set_initial_sort_column('token:validuntil', SORT_ASC); } /** * Validates access to view this report * * @return bool */ protected function can_view(): bool { return has_capability('moodle/site:config', context_system::instance()); } /** * Adds the columns we want to display in the report * * They are all provided by the entities we previously added in the {@see initialise} method, referencing each by their * unique identifier * * @param string $entityuseralias * @param string $entityservicealias */ public function add_columns(string $entityuseralias, string $entityservicealias): void { $this->add_columns_from_entities([ 'token:name', 'user:fullnamewithlink', ]); // Include all identity field columns. $identitycolumns = $this->get_entity('user')->get_identity_columns($this->get_context()); foreach ($identitycolumns as $identitycolumn) { $this->add_column($identitycolumn); } $this->add_columns_from_entities([ 'service:name', 'token:iprestriction', 'token:validuntil', 'token:lastaccess', 'creator:fullnamewithlink', ]); $this->get_column('user:fullnamewithlink') ->set_title(new lang_string('user')); $this->get_column('service:name') ->set_title(new lang_string('service', 'core_webservice')); $this->get_column('creator:fullnamewithlink') ->set_title(new lang_string('tokencreator', 'core_webservice')) ->set_is_available(has_capability('moodle/webservice:managealltokens', context_system::instance())); $this->add_column((new column( 'missingcapabilities', new lang_string('missingcaps', 'webservice'), 'user' )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_TEXT) ->add_field("{$entityuseralias}.id", 'userid') ->add_fields(implode(', ', [ "{$entityservicealias}.id", "{$entityservicealias}.shortname", ])) ->add_callback(static function($value, \stdClass $row): string { global $OUTPUT; $missingcapabilities = self::get_missing_capabilities((int) $row->userid, (int) $row->id, (string) $row->shortname); if (empty($missingcapabilities)) { return ''; } $missingcapabilities = array_map(function($missingcapability) { return (object)[ 'name' => $missingcapability, 'link' => get_capability_docs_link((object)['name' => $missingcapability]), ]; }, $missingcapabilities); return $OUTPUT->render_from_template('core_webservice/missing_capabilities', [ 'missingcapabilities' => $missingcapabilities, 'helpicon' => $OUTPUT->help_icon('missingcaps', 'webservice'), ]); }) ); } /** * Adds the filters we want to display in the report * * They are all provided by the entities we previously added in the {@see initialise} method, referencing each by their * unique identifier */ protected function add_filters(): void { $filters = [ 'token:name', 'user:fullname', 'service:name', 'token:validuntil', ]; $this->add_filters_from_entities($filters); $this->get_filter('user:fullname') ->set_header(new lang_string('user')); $this->get_filter('service:name') ->set_header(new lang_string('service', 'core_webservice')); } /** * Add the system report actions. An extra column will be appended to each row, containing all actions added here * * Note the use of ":id" placeholder which will be substituted according to actual values in the row */ protected function add_actions(): void { // Action to delete token. $this->add_action((new action( new moodle_url('/admin/webservice/tokens.php', [ 'action' => 'delete', 'tokenid' => ':id', ]), new pix_icon('t/delete', '', 'core'), ['class' => 'text-danger'], false, new lang_string('delete', 'core') ))); } /** * Get the missing capabilities for a user * * @param int $userid * @param int $serviceid * @param string $serviceshortname * @return array */ protected static function get_missing_capabilities(int $userid, int $serviceid, string $serviceshortname): array { global $CFG; require_once($CFG->dirroot . '/webservice/lib.php'); $webservicemanager = new \webservice(); $usermissingcaps = $webservicemanager->get_missing_capabilities_by_users([['id' => $userid]], $serviceid); if ($serviceshortname != MOODLE_OFFICIAL_MOBILE_SERVICE && !is_siteadmin($userid)) { return $usermissingcaps[$userid] ?? []; } return []; } } PK)\PN datasource/comments.phpnu[. declare(strict_types=1); namespace core_comment\reportbuilder\datasource; use core\reportbuilder\local\entities\context; use core_reportbuilder\datasource; use core_reportbuilder\local\entities\user; use core_comment\reportbuilder\local\entities\comment; /** * Comments datasource * * @package core_comment * @copyright 2022 Paul Holden * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class comments extends datasource { /** * Return user friendly name of the report source * * @return string */ public static function get_name(): string { return get_string('comments', 'core_comment'); } /** * Initialise report */ protected function initialise(): void { $commententity = new comment(); $commentalias = $commententity->get_table_alias('comments'); $this->set_main_table('comments', $commentalias); $this->add_entity($commententity); // Join the context entity. $contextentity = (new context()) ->set_table_alias('context', $commententity->get_table_alias('context')); $this->add_entity($contextentity ->add_join($commententity->get_context_join()) ); // Join the user entity to the comment userid (author). $userentity = new user(); $useralias = $userentity->get_table_alias('user'); $this->add_entity($userentity ->add_join("LEFT JOIN {user} {$useralias} ON {$useralias}.id = {$commentalias}.userid")); // Add report elements from each of the entities we added to the report. $this->add_all_from_entities(); } /** * Return the columns that will be added to the report upon creation * * @return string[] */ public function get_default_columns(): array { return [ 'user:fullname', 'context:name', 'comment:content', 'comment:timecreated', ]; } /** * Return the column sorting that will be added to the report upon creation * * @return int[] */ public function get_default_column_sorting(): array { return [ 'user:fullname' => SORT_ASC, 'comment:timecreated' => SORT_ASC, ]; } /** * Return the filters that will be added to the report upon creation * * @return string[] */ public function get_default_filters(): array { return [ 'comment:content', ]; } /** * Return the conditions that will be added to the report upon creation * * @return string[] */ public function get_default_conditions(): array { return [ 'user:fullname', ]; } } PK)\u뺤local/entities/comment.phpnu[. declare(strict_types=1); namespace core_comment\reportbuilder\local\entities; use context; use context_helper; use lang_string; use stdClass; use core_reportbuilder\local\entities\base; use core_reportbuilder\local\filters\{date, text}; use core_reportbuilder\local\helpers\format; use core_reportbuilder\local\report\{column, filter}; /** * Comment entity * * @package core_comment * @copyright 2022 Paul Holden * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class comment extends base { /** * Database tables that this entity uses * * @return string[] */ protected function get_default_tables(): array { return [ 'comments', 'context', ]; } /** * The default title for this entity * * @return lang_string */ protected function get_default_entity_title(): lang_string { return new lang_string('comment', 'core_comment'); } /** * Initialise the entity * * @return base */ public function initialise(): base { $columns = $this->get_all_columns(); foreach ($columns as $column) { $this->add_column($column); } // All the filters defined by the entity can also be used as conditions. $filters = $this->get_all_filters(); foreach ($filters as $filter) { $this ->add_filter($filter) ->add_condition($filter); } return $this; } /** * Returns list of all available columns * * @return column[] */ protected function get_all_columns(): array { $commentalias = $this->get_table_alias('comments'); $contextalias = $this->get_table_alias('context'); // Content. $columns[] = (new column( 'content', new lang_string('content'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_LONGTEXT) ->add_join($this->get_context_join()) ->add_fields("{$commentalias}.content, {$commentalias}.format, {$commentalias}.contextid") ->add_fields(context_helper::get_preload_record_columns_sql($contextalias)) ->set_is_sortable(true) ->add_callback(static function($content, stdClass $comment): string { if ($content === null) { return ''; } context_helper::preload_from_record($comment); $context = context::instance_by_id($comment->contextid); return format_text($content, $comment->format, ['context' => $context]); }); // Component. $columns[] = (new column( 'component', new lang_string('plugin'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_TEXT) ->add_fields("{$commentalias}.component") ->set_is_sortable(true); // Area. $columns[] = (new column( 'area', new lang_string('pluginarea'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_TEXT) ->add_fields("{$commentalias}.commentarea") ->set_is_sortable(true); // Item ID. $columns[] = (new column( 'itemid', new lang_string('pluginitemid'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->add_fields("{$commentalias}.itemid") ->set_is_sortable(true); // Time created. $columns[] = (new column( 'timecreated', new lang_string('timecreated', 'core_reportbuilder'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_TIMESTAMP) ->add_fields("{$commentalias}.timecreated") ->set_is_sortable(true) ->add_callback([format::class, 'userdate']); return $columns; } /** * Return list of all available filters * * @return filter[] */ protected function get_all_filters(): array { $commentalias = $this->get_table_alias('comments'); // Content. $filters[] = (new filter( text::class, 'content', new lang_string('content'), $this->get_entity_name(), "{$commentalias}.content" )) ->add_joins($this->get_joins()); // Time created. $filters[] = (new filter( date::class, 'timecreated', new lang_string('timecreated', 'core_reportbuilder'), $this->get_entity_name(), "{$commentalias}.timecreated" )) ->add_joins($this->get_joins()) ->set_limited_operators([ date::DATE_ANY, date::DATE_RANGE, date::DATE_LAST, date::DATE_CURRENT, ]); return $filters; } /** * Return syntax for joining on the context table * * @return string */ public function get_context_join(): string { $commentalias = $this->get_table_alias('comments'); $contextalias = $this->get_table_alias('context'); return "LEFT JOIN {context} {$contextalias} ON {$contextalias}.id = {$commentalias}.contextid"; } } PK)\_aa local/systemreports/comments.phpnu[. declare(strict_types=1); namespace core_comment\reportbuilder\local\systemreports; use context_system; use lang_string; use moodle_url; use pix_icon; use stdClass; use core\reportbuilder\local\entities\context; use core_reportbuilder\system_report; use core_reportbuilder\local\entities\user; use core_reportbuilder\local\report\action; use core_comment\reportbuilder\local\entities\comment; /** * Comments system report * * @package core_comment * @copyright 2022 Paul Holden * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class comments extends system_report { /** * Initialise report, we need to set the main table, load our entities and set columns/filters */ protected function initialise(): void { $commententity = new comment(); $commentalias = $commententity->get_table_alias('comments'); $this->set_main_table('comments', $commentalias); $this->add_entity($commententity); // Base fields required for action callbacks and checkbox toggle. $this->add_base_fields("{$commentalias}.id"); $this->set_checkbox_toggleall(static function(stdClass $row): array { return [$row->id, get_string('select')]; }); // Join the context entity. $contextentity = (new context()) ->set_table_alias('context', $commententity->get_table_alias('context')); $this->add_entity($contextentity ->add_join($commententity->get_context_join()) ); // Join the user entity to the comment userid (author). $userentity = new user(); $useralias = $userentity->get_table_alias('user'); $this->add_entity($userentity ->add_join("LEFT JOIN {user} {$useralias} ON {$useralias}.id = {$commentalias}.userid")); $this->add_columns(); $this->add_filters(); $this->add_actions(); $this->set_downloadable(true, get_string('comments')); } /** * Validates access to view this report * * @return bool */ protected function can_view(): bool { return has_capability('moodle/comment:delete', context_system::instance()); } /** * Add columns to the report */ protected function add_columns(): void { $this->add_columns_from_entities([ 'user:fullnamewithlink', 'comment:content', 'context:link', 'comment:timecreated', ]); // Default sorting. $this->set_initial_sort_column('comment:timecreated', SORT_DESC); } /** * Add filters to the report */ protected function add_filters(): void { $this->add_filters_from_entities([ 'user:fullname', 'comment:content', 'comment:timecreated', ]); } /** * Add actions to report */ protected function add_actions(): void { $this->add_action(new action( new moodle_url('#'), new pix_icon('t/delete', ''), ['data-action' => 'comment-delete', 'data-comment-id' => ':id', 'class' => 'text-danger'], false, new lang_string('delete') )); } } PK;\_`datasource/categories.phpnu[. declare(strict_types=1); namespace core_course\reportbuilder\datasource; use core_cohort\reportbuilder\local\entities\cohort; use core_course\reportbuilder\local\entities\course_category; use core_reportbuilder\datasource; use core_reportbuilder\local\entities\{course, user}; use core_role\reportbuilder\local\entities\role; /** * Course categories datasource * * @package core_course * @copyright 2023 Paul Holden * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class categories extends datasource { /** * Return user friendly name of the datasource * * @return string */ public static function get_name(): string { return get_string('coursecategories'); } /** * Initialise report */ protected function initialise(): void { $categoryentity = new course_category(); $categoryalias = $categoryentity->get_table_alias('course_categories'); $contextalias = $categoryentity->get_table_alias('context'); $this->set_main_table('course_categories', $categoryalias); $this->add_entity($categoryentity); // Join course entity. $courseentity = new course(); $coursealias = $courseentity->get_table_alias('course'); $this->add_entity($courseentity ->add_join("LEFT JOIN {course} {$coursealias} ON {$coursealias}.category = {$categoryalias}.id")); // Join cohort entity (indicate context table join alias). $cohortentity = (new cohort()) ->set_table_join_alias('context', $contextalias); $cohort = $cohortentity->get_table_alias('cohort'); $this->add_entity($cohortentity ->add_join($categoryentity->get_context_join()) ->add_join("LEFT JOIN {cohort} {$cohort} ON {$cohort}.contextid = {$contextalias}.id")); // Join role entity. $roleentity = (new role()) ->set_table_alias('context', $contextalias); $role = $roleentity->get_table_alias('role'); $this->add_entity($roleentity ->add_join($categoryentity->get_context_join()) ->add_join("LEFT JOIN {role_assignments} ras ON ras.contextid = {$contextalias}.id") ->add_join("LEFT JOIN {role} {$role} ON {$role}.id = ras.roleid")); // Join user entity. $userentity = new user(); $user = $userentity->get_table_alias('user'); $this->add_entity($userentity ->add_joins($roleentity->get_joins()) ->add_join("LEFT JOIN {user} {$user} ON {$user}.id = ras.userid")); // Add all elements from entities to be available in custom reports. $this->add_all_from_entities(); } /** * Return the columns that will be added to the report as part of default setup * * @return string[] */ public function get_default_columns(): array { return [ 'course_category:name', 'course_category:idnumber', 'course_category:coursecount', ]; } /** * Return the default sorting that will be added to the report as part of default setup * * @return int[] */ public function get_default_column_sorting(): array { return [ 'course_category:name' => SORT_ASC, ]; } /** * Return the filters that will be added to the report as part of default setup * * @return string[] */ public function get_default_filters(): array { return [ 'course_category:name', ]; } /** * Return the conditions that will be added to the report as part of default setup * * @return string[] */ public function get_default_conditions(): array { return []; } } PK;\T~xdatasource/courses.phpnu[. declare(strict_types=1); namespace core_course\reportbuilder\datasource; use core_course\reportbuilder\local\entities\course_category; use core_files\reportbuilder\local\entities\file; use core_reportbuilder\datasource; use core_reportbuilder\local\entities\course; use core_reportbuilder\local\helpers\database; use core_tag\reportbuilder\local\entities\tag; use lang_string; /** * Courses datasource * * @package core_course * @copyright 2021 Paul Holden * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class courses extends datasource { /** * Return user friendly name of the datasource * * @return string */ public static function get_name(): string { return get_string('courses'); } /** * Initialise report */ protected function initialise(): void { $courseentity = new course(); $coursetablealias = $courseentity->get_table_alias('course'); // Exclude site course. $paramsiteid = database::generate_param_name(); $this->set_main_table('course', $coursetablealias); $this->add_base_condition_sql("{$coursetablealias}.id != :{$paramsiteid}", [$paramsiteid => SITEID]); $this->add_entity($courseentity); // Join the course category entity. $coursecatentity = new course_category(); $coursecattablealias = $coursecatentity->get_table_alias('course_categories'); $this->add_entity($coursecatentity ->add_join("JOIN {course_categories} {$coursecattablealias} ON {$coursecattablealias}.id = {$coursetablealias}.category")); // Join the tag entity. $tagentity = (new tag()) ->set_table_alias('tag', $courseentity->get_table_alias('tag')); $this->add_entity($tagentity ->add_joins($courseentity->get_tag_joins())); // Join the files entity. $contextalias = $courseentity->get_table_alias('context'); $fileentity = (new file()) ->set_entity_title(new lang_string('courseoverviewfiles')); $filesalias = $fileentity->get_table_alias('files'); $this->add_entity($fileentity ->add_join($courseentity->get_context_join()) ->add_join("LEFT JOIN {files} {$filesalias} ON {$filesalias}.contextid = {$contextalias}.id AND {$filesalias}.component = 'course' AND {$filesalias}.filearea = 'overviewfiles' AND {$filesalias}.itemid = 0 AND {$filesalias}.filename != '.'")); // Add all columns/filters/conditions from entities to be available in custom reports. $this->add_all_from_entity($coursecatentity->get_entity_name()); $this->add_all_from_entity($courseentity->get_entity_name()); // Add specific tag entity elements. $this->add_columns_from_entity($tagentity->get_entity_name(), ['name', 'namewithlink']); $this->add_filter($tagentity->get_filter('name')); $this->add_condition($tagentity->get_condition('name')); // Add specific file entity elements. $this->add_columns_from_entity($fileentity->get_entity_name(), ['name', 'size', 'type', 'timecreated']); $this->add_filters_from_entity($fileentity->get_entity_name(), ['name', 'size', 'timecreated']); $this->add_conditions_from_entity($fileentity->get_entity_name(), ['name', 'size', 'timecreated']); } /** * Return the columns that will be added to the report as part of default setup * * @return string[] */ public function get_default_columns(): array { return [ 'course_category:name', 'course:shortname', 'course:fullname', 'course:idnumber', ]; } /** * Return the filters that will be added to the report once is created * * @return string[] */ public function get_default_filters(): array { return ['course_category:name', 'course:fullname', 'course:idnumber']; } /** * Return the conditions that will be added to the report once is created * * @return string[] */ public function get_default_conditions(): array { return ['course_category:name']; } /** * Return the default sorting that will be added to the report once it is created * * @return array|int[] */ public function get_default_column_sorting(): array { return [ 'course_category:name' => SORT_ASC, 'course:shortname' => SORT_ASC, 'course:fullname' => SORT_ASC, ]; } } PK;\! @##datasource/participants.phpnu[. declare(strict_types=1); namespace core_course\reportbuilder\datasource; use core_cohort\reportbuilder\local\entities\cohort; use core_course\reportbuilder\local\entities\course_category; use core_course\reportbuilder\local\entities\access; use core_course\reportbuilder\local\entities\completion; use core_course\reportbuilder\local\entities\enrolment; use core_enrol\reportbuilder\local\entities\enrol; use core_group\reportbuilder\local\entities\group; use core_reportbuilder\datasource; use core_reportbuilder\local\entities\course; use core_reportbuilder\local\entities\user; use core_reportbuilder\local\filters\select; use core_reportbuilder\local\helpers\database; use core_role\reportbuilder\local\entities\role; use core_user\output\status_field; /** * Course participants datasource * * @package core_course * @copyright 2022 David Matamoros * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class participants extends datasource { /** * Initialise report */ protected function initialise(): void { $courseentity = new course(); $this->add_entity($courseentity); $context = $courseentity->get_table_alias('context'); $course = $courseentity->get_table_alias('course'); $this->set_main_table('course', $course); // Exclude site course. $paramsiteid = database::generate_param_name(); $this->add_base_condition_sql("{$course}.id != :{$paramsiteid}", [$paramsiteid => SITEID]); // Join the course category entity. $coursecatentity = new course_category(); $categories = $coursecatentity->get_table_alias('course_categories'); $this->add_entity($coursecatentity ->add_join("JOIN {course_categories} {$categories} ON {$categories}.id = {$course}.category")); // Join the enrolment method entity. $enrolentity = new enrol(); $enrol = $enrolentity->get_table_alias('enrol'); $this->add_entity($enrolentity ->add_join("LEFT JOIN {enrol} {$enrol} ON {$enrol}.courseid = {$course}.id")); // Join the enrolments entity. $enrolmententity = (new enrolment()) ->set_table_alias('enrol', $enrol); $userenrolment = $enrolmententity->get_table_alias('user_enrolments'); $this->add_entity($enrolmententity ->add_joins($enrolentity->get_joins()) ->add_join("LEFT JOIN {user_enrolments} {$userenrolment} ON {$userenrolment}.enrolid = {$enrol}.id")); // Join user entity. $userentity = new user(); $user = $userentity->get_table_alias('user'); $this->add_entity($userentity ->add_joins($enrolmententity->get_joins()) ->add_join("LEFT JOIN {user} {$user} ON {$user}.id = {$userenrolment}.userid AND {$user}.deleted = 0")); // Join the role entity. $roleentity = (new role()) ->set_table_alias('context', $context); $role = $roleentity->get_table_alias('role'); $this->add_entity($roleentity ->add_joins($userentity->get_joins()) ->add_join($courseentity->get_context_join()) ->add_join("LEFT JOIN {role_assignments} ras ON ras.contextid = {$context}.id AND ras.userid = {$user}.id") ->add_join("LEFT JOIN {role} {$role} ON {$role}.id = ras.roleid") ); // Join group entity. $groupentity = (new group()) ->set_table_alias('context', $context); $groups = $groupentity->get_table_alias('groups'); // Sub-select for all course group members. $groupsinnerselect = " SELECT grs.*, grms.userid FROM {groups} grs JOIN {groups_members} grms ON grms.groupid = grs.id"; $this->add_entity($groupentity ->add_join($courseentity->get_context_join()) ->add_joins($userentity->get_joins()) ->add_join(" LEFT JOIN ({$groupsinnerselect}) {$groups} ON {$groups}.courseid = {$course}.id AND {$groups}.userid = {$user}.id") ); // Join cohort entity. $cohortentity = new cohort(); $cohortalias = $cohortentity->get_table_alias('cohort'); $cohortmemberalias = database::generate_alias(); $this->add_entity($cohortentity ->add_joins($userentity->get_joins()) ->add_joins([ "LEFT JOIN {cohort_members} {$cohortmemberalias} ON {$cohortmemberalias}.userid = {$user}.id", "LEFT JOIN {cohort} {$cohortalias} ON {$cohortalias}.id = {$cohortmemberalias}.cohortid", ]) ); // Join completion entity. $completionentity = (new completion()) ->set_table_aliases([ 'course' => $course, 'user' => $user, ]); $completion = $completionentity->get_table_alias('course_completions'); $this->add_entity($completionentity ->add_joins($userentity->get_joins()) ->add_join(" LEFT JOIN {course_completions} {$completion} ON {$completion}.course = {$course}.id AND {$completion}.userid = {$user}.id") ); // Join course access entity. $accessentity = (new access()) ->set_table_alias('user', $user); $lastaccess = $accessentity->get_table_alias('user_lastaccess'); $this->add_entity($accessentity ->add_joins($userentity->get_joins()) ->add_join(" LEFT JOIN {user_lastaccess} {$lastaccess} ON {$lastaccess}.userid = {$user}.id AND {$lastaccess}.courseid = {$course}.id")); // Add all entities columns/filters/conditions. $this->add_all_from_entities([ $courseentity->get_entity_name(), $coursecatentity->get_entity_name(), $enrolmententity->get_entity_name(), $enrolentity->get_entity_name(), $userentity->get_entity_name(), $roleentity->get_entity_name(), $groupentity->get_entity_name(), ]); $this->add_all_from_entity($cohortentity->get_entity_name(), ['name', 'idnumber', 'description', 'customfield*'], ['cohortselect', 'name', 'idnumber', 'customfield*'], ['cohortselect', 'name', 'idnumber', 'customfield*']); $this->add_all_from_entities([ $completionentity->get_entity_name(), $accessentity->get_entity_name(), ]); } /** * Return user friendly name of the datasource * * @return string */ public static function get_name(): string { return get_string('courseparticipants', 'course'); } /** * Return the columns that will be added to the report as part of default setup * * @return string[] */ public function get_default_columns(): array { return [ 'course:coursefullnamewithlink', 'user:fullnamewithlink', 'enrol:name', ]; } /** * Return the column sorting that will be added to the report upon creation * * @return int[] */ public function get_default_column_sorting(): array { return [ 'course:coursefullnamewithlink' => SORT_ASC, 'user:fullnamewithlink' => SORT_ASC, 'enrol:name' => SORT_ASC, ]; } /** * Return the filters that will be added to the report once is created * * @return string[] */ public function get_default_filters(): array { return [ 'user:suspended', 'user:confirmed', ]; } /** * Return the conditions that will be added to the report once is created * * @return string[] */ public function get_default_conditions(): array { return [ 'enrolment:status', 'user:suspended', 'user:confirmed', ]; } /** * Return the condition values that will be set for the report upon creation * * @return array */ public function get_default_condition_values(): array { return [ 'enrolment:status_operator' => select::EQUAL_TO, 'enrolment:status_value' => status_field::STATUS_ACTIVE, ]; } } PK;\ local/formatters/completion.phpnu[. declare(strict_types=1); namespace core_course\reportbuilder\local\formatters; use core_completion\progress; use core_reportbuilder\local\helpers\format; use stdClass; /** * Formatters for the course completion entity * * @package core_course * @copyright 2022 David Matamoros * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class completion { /** * Return completion progress as a percentage * * @param string|null $value * @param stdClass $row * @return string */ public static function completion_progress(?string $value, stdClass $row): string { global $CFG; require_once($CFG->libdir . '/completionlib.php'); // Do not show progress if there is no userid. if (!$row->userid) { return ''; } // Make sure courseid and userid have a value, specially userid because get_course_progress_percentage() defaults // to the current user if this is null and the result would be wrong. $courseid = (int) $row->courseid; $userid = (int) $row->userid; if ($courseid === 0 || $userid === 0) { return format::percent(0); } $course = get_course($courseid); $progress = (float) progress::get_course_progress_percentage($course, $userid); return format::percent($progress); } /** * Return number of days for methods daystakingcourse and daysuntilcompletion * * @param int|null $value * @param stdClass $row * @return int|null * * @deprecated since Moodle 4.5 - please do not use this function any more */ #[\core\attribute\deprecated(null, mdl: 'MDL-82467', since: '4.5')] public static function get_days(?int $value, stdClass $row): ?int { \core\deprecation::emit_deprecation([self::class, __FUNCTION__]); // Do not show anything if there is no userid. if (!$row->userid) { return null; } return $value; } } PK;\TLZ Z local/formatters/enrolment.phpnu[. declare(strict_types=1); namespace core_course\reportbuilder\local\formatters; use core_user\output\status_field; use lang_string; /** * Formatters for the course enrolment entity * * @package core_course * @copyright 2022 David Matamoros * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class enrolment { /** * @deprecated since Moodle 4.3 - please do not use this function any more (to remove in MDL-78118) */ #[\core\attribute\deprecated(null, reason: 'It is no longer used', since: '4.3', mdl: 'MDL-76900', final: true)] public static function enrolment_name(): void { \core\deprecation::emit_deprecation([self::class, __FUNCTION__]); } /** * Returns list of enrolment statuses * * @return lang_string[] */ public static function enrolment_values(): array { return [ status_field::STATUS_ACTIVE => new lang_string('participationactive', 'enrol'), status_field::STATUS_SUSPENDED => new lang_string('participationsuspended', 'enrol'), status_field::STATUS_NOT_CURRENT => new lang_string('participationnotcurrent', 'enrol'), ]; } /** * Return enrolment status for user * * @param string|null $value * @return string|null */ public static function enrolment_status(?string $value): ?string { if ($value === null) { return null; } $statusvalues = self::enrolment_values(); $value = (int) $value; if (!array_key_exists($value, $statusvalues)) { return null; } return (string) $statusvalues[$value]; } } PK;\·l5l5local/entities/completion.phpnu[. declare(strict_types=1); namespace core_course\reportbuilder\local\entities; use core_reportbuilder\local\entities\base; use core_course\reportbuilder\local\formatters\completion as completion_formatter; use core_reportbuilder\local\filters\boolean_select; use core_reportbuilder\local\filters\date; use core_reportbuilder\local\helpers\database; use core_reportbuilder\local\helpers\format; use core_reportbuilder\local\report\column; use core_reportbuilder\local\report\filter; use completion_criteria_completion; use completion_info; use html_writer; use lang_string; use stdClass; /** * Course completion entity implementation * * @package core_course * @copyright 2022 David Matamoros * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class completion extends base { /** * Database tables that this entity uses * * @return string[] */ protected function get_default_tables(): array { return [ 'course', 'course_completions', 'grade_grades' , 'grade_items', 'user', ]; } /** * Database tables that this entity no longer uses * * @return string[] */ protected function get_deprecated_tables(): array { return [ 'course_completions' => 'course_completion', ]; } /** * The default title for this entity in the list of columns/conditions/filters in the report builder * * @return lang_string */ protected function get_default_entity_title(): lang_string { return new lang_string('coursecompletion', 'completion'); } /** * Initialise the entity * * @return base */ public function initialise(): base { foreach ($this->get_all_columns() as $column) { $this->add_column($column); } // All the filters defined by the entity can also be used as conditions. foreach ($this->get_all_filters() as $filter) { $this ->add_filter($filter) ->add_condition($filter); } return $this; } /** * Returns list of all available columns * * @return column[] */ protected function get_all_columns(): array { [ 'course' => $course, 'course_completions' => $coursecompletion, 'grade_grades' => $grade, 'grade_items' => $gradeitem, 'user' => $user, ] = $this->get_table_aliases(); // Completed column. $columns[] = (new column( 'completed', new lang_string('completed', 'completion'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_BOOLEAN) ->add_field(" CASE WHEN {$coursecompletion}.id IS NULL THEN NULL WHEN {$coursecompletion}.timecompleted > 0 THEN 1 ELSE 0 END", 'completed') ->set_is_sortable(true) ->add_callback([format::class, 'boolean_as_text']); // Completion criteria column. $criterias = database::generate_alias(); $columns[] = (new column( 'criteria', new lang_string('criteria', 'core_completion'), $this->get_entity_name() )) ->add_joins($this->get_joins()) // Determine whether any criteria exist for the course. We also group per course, rather than report each separately. ->add_join("LEFT JOIN ( SELECT DISTINCT course FROM {course_completion_criteria} ) {$criterias} ON {$criterias}.course = {$course}.id") ->set_type(column::TYPE_TEXT) // Select enough fields to determine user criteria for the course. ->add_field("{$criterias}.course", 'courseid') ->add_field("{$course}.enablecompletion") ->add_field("{$user}.id", 'userid') ->set_disabled_aggregation_all() ->add_callback(static function($id, stdClass $record): string { if (!$record->courseid) { return ''; } $info = new completion_info((object) ['id' => $record->courseid, 'enablecompletion' => $record->enablecompletion]); if ($info->get_aggregation_method() == COMPLETION_AGGREGATION_ALL) { $title = get_string('criteriarequiredall', 'core_completion'); } else { $title = get_string('criteriarequiredany', 'core_completion'); } // Map all completion data to their criteria summaries. $items = array_map(static function(completion_criteria_completion $completion): string { $criteria = $completion->get_criteria(); return get_string('criteriasummary', 'core_completion', [ 'type' => $criteria->get_details($completion)['type'], 'summary' => $criteria->get_title_detailed(), ]); }, $info->get_completions((int) $record->userid)); return $title . html_writer::alist($items); }); // Progress percentage column. $columns[] = (new column( 'progresspercent', new lang_string('progress', 'completion'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_TEXT) ->add_field("{$course}.id", 'courseid') ->add_field("{$user}.id", 'userid') ->set_is_sortable(false) ->add_callback([completion_formatter::class, 'completion_progress']); // Time enrolled. $columns[] = (new column( 'timeenrolled', new lang_string('timeenrolled', 'enrol'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_TIMESTAMP) ->add_field("{$coursecompletion}.timeenrolled") ->set_is_sortable(true) ->add_callback([format::class, 'userdate']); // Time started. $columns[] = (new column( 'timestarted', new lang_string('timestarted', 'enrol'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_TIMESTAMP) ->add_field("{$coursecompletion}.timestarted") ->set_is_sortable(true) ->add_callback([format::class, 'userdate']); // Time completed. $columns[] = (new column( 'timecompleted', new lang_string('timecompleted', 'completion'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_TIMESTAMP) ->add_field("{$coursecompletion}.timecompleted") ->set_is_sortable(true) ->add_callback([format::class, 'userdate']); // Time reaggregated. $columns[] = (new column( 'reaggregate', new lang_string('timereaggregated', 'enrol'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_TIMESTAMP) ->add_field("{$coursecompletion}.reaggregate") ->set_is_sortable(true) ->add_callback([format::class, 'userdate']); // Days taking course (days since course start date until completion or until current date if not completed). $currenttime = time(); $columns[] = (new column( 'dayscourse', new lang_string('daystakingcourse', 'completion'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_INTEGER) ->add_field("( CASE WHEN {$coursecompletion}.id IS NULL THEN NULL ELSE (CASE WHEN {$coursecompletion}.timecompleted > 0 THEN {$coursecompletion}.timecompleted ELSE {$currenttime} END - {$course}.startdate) END)", 'dayscourse') ->set_is_sortable(true) ->set_callback([format::class, 'format_time']); // Days since last completion (days since last enrolment date until completion or until current date if not completed). $columns[] = (new column( 'daysuntilcompletion', new lang_string('daysuntilcompletion', 'completion'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_INTEGER) ->add_field("( CASE WHEN {$coursecompletion}.id IS NULL THEN NULL ELSE (CASE WHEN {$coursecompletion}.timecompleted > 0 THEN {$coursecompletion}.timecompleted ELSE {$currenttime} END - {$coursecompletion}.timeenrolled) END)", 'daysuntilcompletion') ->set_is_sortable(true) ->set_callback([format::class, 'format_time']); // Student course grade. $columns[] = (new column( 'grade', new lang_string('gradenoun'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->add_join(" LEFT JOIN {grade_items} {$gradeitem} ON ({$gradeitem}.itemtype = 'course' AND {$course}.id = {$gradeitem}.courseid) ") ->add_join(" LEFT JOIN {grade_grades} {$grade} ON ({$user}.id = {$grade}.userid AND {$gradeitem}.id = {$grade}.itemid) ") ->set_type(column::TYPE_FLOAT) ->add_fields("{$grade}.finalgrade") ->set_is_sortable(true) ->add_callback(function(?float $value): string { if ($value === null) { return ''; } return format_float($value, 2); }); return $columns; } /** * Return list of all available filters * * @return filter[] */ protected function get_all_filters(): array { $coursecompletion = $this->get_table_alias('course_completions'); // Completed status filter. $filters[] = (new filter( boolean_select::class, 'completed', new lang_string('completed', 'completion'), $this->get_entity_name(), "CASE WHEN {$coursecompletion}.timecompleted > 0 THEN 1 ELSE 0 END" )) ->add_joins($this->get_joins()); // Time completed filter. $filters[] = (new filter( date::class, 'timecompleted', new lang_string('timecompleted', 'completion'), $this->get_entity_name(), "{$coursecompletion}.timecompleted" )) ->add_joins($this->get_joins()) ->set_limited_operators([ date::DATE_ANY, date::DATE_NOT_EMPTY, date::DATE_EMPTY, date::DATE_RANGE, date::DATE_LAST, date::DATE_CURRENT, ]); // Time enrolled/started filter and condition. $fields = ['timeenrolled', 'timestarted']; foreach ($fields as $field) { $filters[] = (new filter( date::class, $field, new lang_string($field, 'enrol'), $this->get_entity_name(), "{$coursecompletion}.{$field}" )) ->add_joins($this->get_joins()) ->set_limited_operators([ date::DATE_ANY, date::DATE_NOT_EMPTY, date::DATE_EMPTY, date::DATE_RANGE, date::DATE_LAST, date::DATE_CURRENT, ]); } // Time reaggregated filter and condition. $filters[] = (new filter( date::class, 'reaggregate', new lang_string('timereaggregated', 'enrol'), $this->get_entity_name(), "{$coursecompletion}.reaggregate" )) ->add_joins($this->get_joins()) ->set_limited_operators([ date::DATE_ANY, date::DATE_NOT_EMPTY, date::DATE_EMPTY, date::DATE_RANGE, date::DATE_LAST, date::DATE_CURRENT, ]); return $filters; } } PK;\(("local/entities/course_category.phpnu[. declare(strict_types=1); namespace core_course\reportbuilder\local\entities; use context_coursecat; use context_helper; use html_writer; use lang_string; use moodle_url; use stdClass; use theme_config; use core_course_category; use core_reportbuilder\local\entities\base; use core_reportbuilder\local\filters\{category, select, text}; use core_reportbuilder\local\report\column; use core_reportbuilder\local\report\filter; /** * Course category entity * * @package core_course * @copyright 2021 Paul Holden * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class course_category extends base { /** * Database tables that this entity uses * * @return string[] */ protected function get_default_tables(): array { return [ 'context', 'course_categories', ]; } /** * The default title for this entity * * @return lang_string */ protected function get_default_entity_title(): lang_string { return new lang_string('coursecategory'); } /** * Initialise the entity * * @return base */ public function initialise(): base { $columns = $this->get_all_columns(); foreach ($columns as $column) { $this->add_column($column); } // All the filters defined by the entity can also be used as conditions. $filters = $this->get_all_filters(); foreach ($filters as $filter) { $this ->add_filter($filter) ->add_condition($filter); } return $this; } /** * Returns list of all available columns * * @return column[] */ protected function get_all_columns(): array { $tablealias = $this->get_table_alias('course_categories'); $tablealiascontext = $this->get_table_alias('context'); // Name column. $columns[] = (new column( 'name', new lang_string('categoryname'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->add_join($this->get_context_join()) ->set_type(column::TYPE_TEXT) ->add_fields("{$tablealias}.name, {$tablealias}.id") ->add_fields(context_helper::get_preload_record_columns_sql($tablealiascontext)) ->add_callback(static function(?string $name, stdClass $category): string { if (empty($category->id)) { return ''; } context_helper::preload_from_record($category); $context = context_coursecat::instance($category->id); return format_string($category->name, true, ['context' => $context]); }) ->set_is_sortable(true); // Category name with link column. $columns[] = (new column( 'namewithlink', new lang_string('namewithlink', 'core_course'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->add_join($this->get_context_join()) ->set_type(column::TYPE_TEXT) ->add_fields("{$tablealias}.name, {$tablealias}.id") ->add_fields(context_helper::get_preload_record_columns_sql($tablealiascontext)) ->add_callback(static function(?string $name, stdClass $category): string { if (empty($category->id)) { return ''; } context_helper::preload_from_record($category); $context = context_coursecat::instance($category->id); $url = new moodle_url('/course/management.php', ['categoryid' => $category->id]); return html_writer::link($url, format_string($category->name, true, ['context' => $context])); }) ->set_is_sortable(true); // Path column. $columns[] = (new column( 'path', new lang_string('categorypath'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_TEXT) ->add_fields("{$tablealias}.name, {$tablealias}.id") ->add_callback(static function(?string $name, stdClass $category): string { return empty($category->id) ? '' : core_course_category::get($category->id, MUST_EXIST, true)->get_nested_name(false); }) ->set_disabled_aggregation(['groupconcat', 'groupconcatdistinct']) ->set_is_sortable(true); // ID number column. $columns[] = (new column( 'idnumber', new lang_string('idnumbercoursecategory'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_TEXT) ->add_fields("{$tablealias}.idnumber") ->set_is_sortable(true); // Description column (note we need to join/select from the context table in order to format the column). $columns[] = (new column( 'description', new lang_string('description'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->add_join($this->get_context_join()) ->set_type(column::TYPE_LONGTEXT) ->add_fields("{$tablealias}.description, {$tablealias}.descriptionformat, {$tablealias}.id") ->add_fields(context_helper::get_preload_record_columns_sql($tablealiascontext)) ->set_is_sortable(true) ->add_callback(static function(?string $description, stdClass $category): string { global $CFG; require_once("{$CFG->libdir}/filelib.php"); if ($description === null) { return ''; } context_helper::preload_from_record($category); $context = context_coursecat::instance($category->id); $description = file_rewrite_pluginfile_urls($description, 'pluginfile.php', $context->id, 'coursecat', 'description', null); return format_text($description, $category->descriptionformat, ['context' => $context->id]); }); // Theme column. $columns[] = (new column( 'theme', new lang_string('theme'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_TEXT) ->add_fields("{$tablealias}.theme") ->set_is_sortable(true) ->add_callback(static function (?string $theme): string { if ((string) $theme === '') { return ''; } return get_string('pluginname', "theme_{$theme}"); }); // Course count column. $columns[] = (new column( 'coursecount', new lang_string('coursecount', 'core_course'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_INTEGER) ->add_fields("{$tablealias}.coursecount") ->set_is_sortable(true); return $columns; } /** * Return list of all available filters * * @return filter[] */ protected function get_all_filters(): array { $tablealias = $this->get_table_alias('course_categories'); // Select category filter. $filters[] = (new filter( category::class, 'name', new lang_string('categoryselect', 'core_reportbuilder'), $this->get_entity_name(), "{$tablealias}.id" )) ->add_joins($this->get_joins()) ->set_options([ 'requiredcapabilities' => 'moodle/category:viewcourselist', ]); // Name filter. $filters[] = (new filter( text::class, 'text', new lang_string('categoryname'), $this->get_entity_name(), "{$tablealias}.name" )) ->add_joins($this->get_joins()); // ID number filter. $filters[] = (new filter( text::class, 'idnumber', new lang_string('idnumbercoursecategory'), $this->get_entity_name(), "{$tablealias}.idnumber" )) ->add_joins($this->get_joins()); // Theme filter. $filters[] = (new filter( select::class, 'theme', new lang_string('theme'), $this->get_entity_name(), "{$tablealias}.theme", )) ->set_options_callback(static function(): array { return array_map( fn(theme_config $theme) => $theme->get_theme_name(), get_list_of_themes(), ); }) ->add_joins($this->get_joins()); return $filters; } /** * Return context join used by columns * * @return string */ public function get_context_join(): string { $coursecategories = $this->get_table_alias('course_categories'); $context = $this->get_table_alias('context'); return "LEFT JOIN {context} {$context} ON {$context}.instanceid = {$coursecategories}.id AND {$context}.contextlevel = " . CONTEXT_COURSECAT; } } PK;\-nnlocal/entities/access.phpnu[. declare(strict_types=1); namespace core_course\reportbuilder\local\entities; use core_reportbuilder\local\entities\base; use core_reportbuilder\local\filters\date; use core_reportbuilder\local\helpers\format; use core_reportbuilder\local\report\column; use core_reportbuilder\local\report\filter; use lang_string; use stdClass; /** * Course access entity implementation * * @package core_course * @copyright 2022 David Matamoros * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class access extends base { /** * Database tables that this entity uses * * @return string[] */ protected function get_default_tables(): array { return [ 'user_lastaccess', 'user', ]; } /** * The default title for this entity in the list of columns/conditions/filters in the report builder * * @return lang_string */ protected function get_default_entity_title(): lang_string { return new lang_string('courseaccess', 'course'); } /** * Initialise the entity * * @return base */ public function initialise(): base { foreach ($this->get_all_columns() as $column) { $this->add_column($column); } // All the filters defined by the entity can also be used as conditions. foreach ($this->get_all_filters() as $filter) { $this ->add_filter($filter) ->add_condition($filter); } return $this; } /** * Returns list of all available columns * * @return column[] */ protected function get_all_columns(): array { $tablealias = $this->get_table_alias('user_lastaccess'); $user = $this->get_table_alias('user'); // Last course access column. $columns[] = (new column( 'timeaccess', new lang_string('lastcourseaccess', 'moodle'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_TIMESTAMP) ->add_field("{$tablealias}.timeaccess") ->add_field("{$user}.id", 'userid') ->set_is_sortable(true) ->add_callback(static function(?int $value, stdClass $row, $arguments, ?string $aggregation): string { if ($row->userid === null && $aggregation === null) { return ''; } else if ($value === null) { return get_string('never'); } return format::userdate($value, $row); }); return $columns; } /** * Return list of all available filters * * @return filter[] */ protected function get_all_filters(): array { $tablealias = $this->get_table_alias('user_lastaccess'); // Last course access filter. $filters[] = (new filter( date::class, 'timeaccess', new lang_string('lastcourseaccess', 'moodle'), $this->get_entity_name(), "{$tablealias}.timeaccess" )) ->add_joins($this->get_joins()) ->set_limited_operators([ date::DATE_ANY, date::DATE_NOT_EMPTY, date::DATE_EMPTY, date::DATE_RANGE, date::DATE_LAST, date::DATE_CURRENT, ]); return $filters; } } PK;\X2[local/entities/enrolment.phpnu[. declare(strict_types=1); namespace core_course\reportbuilder\local\entities; use context_course; use core_course\reportbuilder\local\formatters\enrolment as enrolment_formatter; use core_reportbuilder\local\entities\base; use core_reportbuilder\local\filters\date; use core_reportbuilder\local\filters\select; use core_reportbuilder\local\helpers\database; use core_reportbuilder\local\helpers\format; use core_reportbuilder\local\report\column; use core_reportbuilder\local\report\filter; use core_user\output\status_field; use enrol_plugin; use lang_string; use stdClass; /** * Course enrolment entity implementation * * @package core_course * @copyright 2022 David Matamoros * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class enrolment extends base { /** * Database tables that this entity uses * * @return string[] */ protected function get_default_tables(): array { return [ 'user_enrolments', 'enrol', ]; } /** * The default title for this entity in the list of columns/conditions/filters in the report builder * * @return lang_string */ protected function get_default_entity_title(): lang_string { return new lang_string('enrolment', 'enrol'); } /** * Initialise the entity * * @return base */ public function initialise(): base { foreach ($this->get_all_columns() as $column) { $this->add_column($column); } // All the filters defined by the entity can also be used as conditions. foreach ($this->get_all_filters() as $filter) { $this ->add_filter($filter) ->add_condition($filter); } return $this; } /** * Returns list of all available columns * * @return column[] */ protected function get_all_columns(): array { $userenrolments = $this->get_table_alias('user_enrolments'); // Enrolment time created. $columns[] = (new column( 'timecreated', new lang_string('timecreated', 'moodle'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_TIMESTAMP) ->add_field("{$userenrolments}.timecreated") ->set_is_sortable(true) ->add_callback([format::class, 'userdate']); // Enrolment time started. $columns[] = (new column( 'timestarted', new lang_string('timestarted', 'enrol'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_TIMESTAMP) ->add_field(" CASE WHEN {$userenrolments}.timestart = 0 THEN {$userenrolments}.timecreated ELSE {$userenrolments}.timestart END", 'timestarted') ->set_is_sortable(true) ->add_callback([format::class, 'userdate']); // Enrolment time ended. $columns[] = (new column( 'timeended', new lang_string('timeended', 'enrol'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_TIMESTAMP) ->add_field("{$userenrolments}.timeend") ->set_is_sortable(true) ->add_callback([format::class, 'userdate']); // Enrolment status. $columns[] = (new column( 'status', new lang_string('status', 'moodle'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_TEXT) ->add_field($this->get_status_field_sql(), 'status') ->set_is_sortable(true) ->add_callback([enrolment_formatter::class, 'enrolment_status']); return $columns; } /** * Generate SQL snippet suitable for returning enrolment status field * * @return string */ private function get_status_field_sql(): string { $time = time(); $userenrolments = $this->get_table_alias('user_enrolments'); $enrol = $this->get_table_alias('enrol'); return " CASE WHEN {$userenrolments}.status = " . ENROL_USER_ACTIVE . " THEN CASE WHEN ({$userenrolments}.timestart > {$time}) OR ({$userenrolments}.timeend > 0 AND {$userenrolments}.timeend < {$time}) OR ({$enrol}.status = " . ENROL_INSTANCE_DISABLED . ") THEN " . status_field::STATUS_NOT_CURRENT . " ELSE " . status_field::STATUS_ACTIVE . " END ELSE {$userenrolments}.status END"; } /** * Return list of all available filters * * @return filter[] */ protected function get_all_filters(): array { $userenrolments = $this->get_table_alias('user_enrolments'); // Enrolment time created. $filters[] = (new filter( date::class, 'timecreated', new lang_string('timecreated', 'moodle'), $this->get_entity_name(), "{$userenrolments}.timecreated" )) ->add_joins($this->get_joins()) ->set_limited_operators([ date::DATE_ANY, date::DATE_NOT_EMPTY, date::DATE_EMPTY, date::DATE_RANGE, date::DATE_LAST, date::DATE_CURRENT, ]); // Enrolment time started. $filters[] = (new filter( date::class, 'timestarted', new lang_string('timestarted', 'enrol'), $this->get_entity_name(), "CASE WHEN {$userenrolments}.timestart = 0 THEN {$userenrolments}.timecreated ELSE {$userenrolments}.timestart END" )) ->add_joins($this->get_joins()) ->set_limited_operators([ date::DATE_ANY, date::DATE_NOT_EMPTY, date::DATE_EMPTY, date::DATE_RANGE, date::DATE_LAST, date::DATE_CURRENT, ]); // Enrolment time ended. $filters[] = (new filter( date::class, 'timeended', new lang_string('timeended', 'enrol'), $this->get_entity_name(), "{$userenrolments}.timeend" )) ->add_joins($this->get_joins()) ->set_limited_operators([ date::DATE_ANY, date::DATE_NOT_EMPTY, date::DATE_EMPTY, date::DATE_RANGE, date::DATE_LAST, date::DATE_CURRENT, ]); // Enrolment status. $filters[] = (new filter( select::class, 'status', new lang_string('status', 'moodle'), $this->get_entity_name(), $this->get_status_field_sql() )) ->add_joins($this->get_joins()) ->set_options(enrolment_formatter::enrolment_values()); return $filters; } } PKx\ӇQdatasource/files.phpnu[. declare(strict_types=1); namespace core_files\reportbuilder\datasource; use core\reportbuilder\local\entities\context; use core_files\reportbuilder\local\entities\file; use core_reportbuilder\datasource; use core_reportbuilder\local\entities\user; use core_reportbuilder\local\filters\boolean_select; /** * Files datasource * * @package core_files * @copyright 2022 Paul Holden * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class files extends datasource { /** * Return user friendly name of the report source * * @return string */ public static function get_name(): string { return get_string('files'); } /** * Initialise report */ protected function initialise(): void { $fileentity = new file(); $filesalias = $fileentity->get_table_alias('files'); $this->set_main_table('files', $filesalias); $this->add_entity($fileentity); // Join the context entity. $contextentity = new context(); $contextalias = $contextentity->get_table_alias('context'); $this->add_entity($contextentity ->add_join("LEFT JOIN {context} {$contextalias} ON {$contextalias}.id = {$filesalias}.contextid") ); // Join the user entity. $userentity = new user(); $useralias = $userentity->get_table_alias('user'); $this->add_entity($userentity ->add_join("LEFT JOIN {user} {$useralias} ON {$useralias}.id = {$filesalias}.userid") ); // Add report elements from each of the entities we added to the report. $this->add_all_from_entities(); } /** * Return the columns that will be added to the report upon creation * * @return string[] */ public function get_default_columns(): array { return [ 'context:name', 'user:fullname', 'file:name', 'file:type', 'file:size', 'file:timecreated', ]; } /** * Return the column sorting that will be added to the report upon creation * * @return int[] */ public function get_default_column_sorting(): array { return [ 'context:name' => SORT_ASC, 'file:timecreated' => SORT_ASC, ]; } /** * Return the filters that will be added to the report upon creation * * @return string[] */ public function get_default_filters(): array { return [ 'file:size', 'file:timecreated', ]; } /** * Return the conditions that will be added to the report upon creation * * @return string[] */ public function get_default_conditions(): array { return ['file:directory']; } /** * Return the condition values that will be set for the report upon creation * * @return array */ public function get_default_condition_values(): array { return ['file:directory_operator' => boolean_select::NOT_CHECKED]; } } PKx\/l66local/entities/file.phpnu[. declare(strict_types=1); namespace core_files\reportbuilder\local\entities; use core_collator; use core_filetypes; use lang_string; use license_manager; use stdClass; use core_reportbuilder\local\entities\base; use core_reportbuilder\local\helpers\format; use core_reportbuilder\local\filters\{boolean_select, date, filesize, select, text}; use core_reportbuilder\local\report\{column, filter}; /** * File entity * * @package core_files * @copyright 2022 Paul Holden * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class file extends base { /** * Database tables that this entity uses * * @return string[] */ protected function get_default_tables(): array { return [ 'files', ]; } /** * Database tables that this entity no longer uses * * @return string[] */ protected function get_deprecated_tables(): array { return [ 'context', ]; } /** * The default title for this entity * * @return lang_string */ protected function get_default_entity_title(): lang_string { return new lang_string('file'); } /** * Initialise the entity * * @return base */ public function initialise(): base { $columns = $this->get_all_columns(); foreach ($columns as $column) { $this->add_column($column); } // All the filters defined by the entity can also be used as conditions. $filters = $this->get_all_filters(); foreach ($filters as $filter) { $this ->add_filter($filter) ->add_condition($filter); } return $this; } /** * Returns list of all available columns * * @return column[] */ protected function get_all_columns(): array { $filesalias = $this->get_table_alias('files'); // Name. $columns[] = (new column( 'name', new lang_string('filename', 'core_repository'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_TEXT) ->add_field("{$filesalias}.filename") ->set_is_sortable(true); // Size. $columns[] = (new column( 'size', new lang_string('size'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_INTEGER) ->add_field("{$filesalias}.filesize") ->add_field("CASE WHEN {$filesalias}.filename = '.' THEN 1 ELSE 0 END", 'directory') ->set_is_sortable(true) ->add_callback(static function($filesize, stdClass $fileinfo): string { // Absent file size and/or directory should not return output. if ($fileinfo->filesize === null || $fileinfo->directory) { return ''; } return display_size($fileinfo->filesize); }); // Path. $columns[] = (new column( 'path', new lang_string('path'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_TEXT) ->add_field("{$filesalias}.filepath") ->set_is_sortable(true); // Type. $columns[] = (new column( 'type', new lang_string('type', 'core_repository'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_TEXT) ->add_field("{$filesalias}.mimetype") ->add_field("CASE WHEN {$filesalias}.filename = '.' THEN 1 ELSE 0 END", 'directory') ->set_is_sortable(true) ->add_callback(static function($mimetype, stdClass $fileinfo): string { global $CFG; require_once("{$CFG->libdir}/filelib.php"); // Absent mime type and/or directory has pre-determined output. if ($fileinfo->mimetype === null && !$fileinfo->directory) { return ''; } else if ($fileinfo->directory) { return get_string('directory'); } return get_mimetype_description($fileinfo->mimetype); }); // Icon. $columns[] = (new column( 'icon', new lang_string('icon'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_TEXT) ->add_field("{$filesalias}.mimetype") ->add_field("CASE WHEN {$filesalias}.filename = '.' THEN 1 ELSE 0 END", 'directory') ->set_disabled_aggregation_all() ->add_callback(static function($mimetype, stdClass $fileinfo): string { global $CFG, $OUTPUT; require_once("{$CFG->libdir}/filelib.php"); if ($fileinfo->mimetype === null && !$fileinfo->directory) { return ''; } if ($fileinfo->directory) { $icon = file_folder_icon(); $description = get_string('directory'); } else { $icon = file_file_icon($fileinfo); $description = get_mimetype_description($fileinfo->mimetype); } return $OUTPUT->pix_icon($icon, $description, 'moodle', ['class' => 'iconsize-medium']); }); // Author. $columns[] = (new column( 'author', new lang_string('author', 'core_repository'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_TEXT) ->add_field("{$filesalias}.author") ->set_is_sortable(true); // License. $columns[] = (new column( 'license', new lang_string('license', 'core_repository'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_TEXT) ->add_field("{$filesalias}.license") ->set_is_sortable(true) ->add_callback(static function(?string $license): string { global $CFG; require_once("{$CFG->libdir}/licenselib.php"); $licenses = license_manager::get_licenses(); if ($license === null || !array_key_exists($license, $licenses)) { return ''; } return $licenses[$license]->fullname; }); // Content hash. $columns[] = (new column( 'contenthash', new lang_string('contenthash', 'core_files'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_TEXT) ->add_field("{$filesalias}.contenthash") ->set_is_sortable(true); // Component. $columns[] = (new column( 'component', new lang_string('plugin'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_TEXT) ->add_fields("{$filesalias}.component") ->set_is_sortable(true); // Area. $columns[] = (new column( 'area', new lang_string('pluginarea'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_TEXT) ->add_fields("{$filesalias}.filearea") ->set_is_sortable(true); // Item ID. $columns[] = (new column( 'itemid', new lang_string('pluginitemid'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->add_fields("{$filesalias}.itemid") ->set_is_sortable(true); // Time created. $columns[] = (new column( 'timecreated', new lang_string('timecreated', 'core_reportbuilder'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_TIMESTAMP) ->add_field("{$filesalias}.timecreated") ->add_callback([format::class, 'userdate']) ->set_is_sortable(true); return $columns; } /** * Return list of all available filters * * @return filter[] */ protected function get_all_filters(): array { $filesalias = $this->get_table_alias('files'); // Directory. $filters[] = (new filter( boolean_select::class, 'directory', new lang_string('directory'), $this->get_entity_name(), "CASE WHEN {$filesalias}.filename = '.' THEN 1 ELSE 0 END" )) ->add_joins($this->get_joins()); // Draft. $filters[] = (new filter( boolean_select::class, 'draft', new lang_string('areauserdraft', 'core_repository'), $this->get_entity_name(), "CASE WHEN {$filesalias}.component = 'user' AND {$filesalias}.filearea = 'draft' THEN 1 ELSE 0 END" )) ->add_joins($this->get_joins()); // Name. $filters[] = (new filter( text::class, 'name', new lang_string('filename', 'core_repository'), $this->get_entity_name(), "{$filesalias}.filename" )) ->add_joins($this->get_joins()); // Size. $filters[] = (new filter( filesize::class, 'size', new lang_string('size'), $this->get_entity_name(), "{$filesalias}.filesize" )) ->add_joins($this->get_joins()); // Type. $filters[] = (new filter( select::class, 'type', new lang_string('type', 'core_repository'), $this->get_entity_name(), "{$filesalias}.mimetype" )) ->add_joins($this->get_joins()) ->set_options_callback(static function(): array { $mimetypenames = array_column(core_filetypes::get_types(), 'type'); // Convert the names into a map of name => description. $mimetypes = array_combine($mimetypenames, array_map(static function(string $mimetype): string { return get_mimetype_description($mimetype); }, $mimetypenames)); core_collator::asort($mimetypes); return $mimetypes; }); // Author. $filters[] = (new filter( text::class, 'author', new lang_string('author', 'core_repository'), $this->get_entity_name(), "{$filesalias}.author" )) ->add_joins($this->get_joins()); // License (consider null = 'unknown/license not specified' for filtering purposes). $filters[] = (new filter( select::class, 'license', new lang_string('license', 'core_repository'), $this->get_entity_name(), "COALESCE({$filesalias}.license, 'unknown')" )) ->add_joins($this->get_joins()) ->set_options_callback(static function(): array { global $CFG; require_once("{$CFG->libdir}/licenselib.php"); $licenses = license_manager::get_licenses(); return array_map(static function(stdClass $license): string { return $license->fullname; }, $licenses); }); // Content hash. $filters[] = (new filter( text::class, 'contenthash', new lang_string('contenthash', 'core_files'), $this->get_entity_name(), "{$filesalias}.contenthash" )) ->add_joins($this->get_joins()); // Component. $filters[] = (new filter( text::class, 'component', new lang_string('plugin'), $this->get_entity_name(), "{$filesalias}.component" )) ->add_joins($this->get_joins()); // Area. $filters[] = (new filter( text::class, 'area', new lang_string('pluginarea'), $this->get_entity_name(), "{$filesalias}.filearea" )) ->add_joins($this->get_joins()); // Time created. $filters[] = (new filter( date::class, 'timecreated', new lang_string('timecreated', 'core_reportbuilder'), $this->get_entity_name(), "{$filesalias}.timecreated" )) ->add_joins($this->get_joins()) ->set_limited_operators([ date::DATE_ANY, date::DATE_RANGE, date::DATE_LAST, date::DATE_CURRENT, ]); return $filters; } } PK͐\I) ) local/entities/enrol.phpnu[. namespace core_enrol\reportbuilder\local\entities; use enrol_plugin; use lang_string; use stdClass; use core_reportbuilder\local\entities\base; use core_reportbuilder\local\filters\{boolean_select, date, duration, select, text}; use core_reportbuilder\local\helpers\format; use core_reportbuilder\local\report\{column, filter}; /** * Enrolment method entity * * @package core_enrol * @copyright 2023 Paul Holden * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class enrol extends base { /** * Database tables that this entity uses * * @return string[] */ protected function get_default_tables(): array { return [ 'enrol', ]; } /** * The default title for this entity * * @return lang_string */ protected function get_default_entity_title(): lang_string { return new lang_string('enrolmentmethod', 'core_enrol'); } /** * Initialise the entity * * @return base */ public function initialise(): base { $columns = $this->get_all_columns(); foreach ($columns as $column) { $this->add_column($column); } // All the filters defined by the entity can also be used as conditions. $filters = $this->get_all_filters(); foreach ($filters as $filter) { $this ->add_filter($filter) ->add_condition($filter); } return $this; } /** * Returns list of all available columns * * @return column[] */ protected function get_all_columns(): array { global $DB; $enrolalias = $this->get_table_alias('enrol'); // Plugin column. $columns[] = (new column( 'plugin', new lang_string('plugin'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_TEXT) ->add_fields("{$enrolalias}.enrol") ->set_is_sortable(true) ->set_callback(static function(?string $enrol): string { if ($enrol === null || !$plugin = enrol_get_plugin($enrol)) { return ''; } return $plugin->get_instance_name(null); }); // Name column. $columns[] = (new column( 'name', new lang_string('name'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_TEXT) ->add_fields("{$enrolalias}.enrol, {$enrolalias}.name, {$enrolalias}.courseid, " . "{$enrolalias}.roleid, {$enrolalias}.customint1") ->set_is_sortable(true) ->set_callback(static function(?string $enrol, stdClass $instance): string { if ($enrol === null || !$plugin = enrol_get_plugin($enrol)) { return ''; } return $plugin->get_instance_name($instance); }); // Enabled column. $columns[] = (new column( 'enabled', new lang_string('enabled', 'core_admin'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_BOOLEAN) // For accurate aggregation, we need to return boolean enabled = true by xor'ing the field value. ->add_field($DB->sql_bitxor("{$enrolalias}.status", 1), 'status') ->set_is_sortable(true) ->set_callback([format::class, 'boolean_as_text']); // Period column. $columns[] = (new column( 'period', new lang_string('enrolperiod', 'core_enrol'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_TIMESTAMP) ->add_fields("{$enrolalias}.enrolperiod") ->set_is_sortable(true) ->set_callback(static function(?int $enrolperiod, stdClass $row): string { if ($enrolperiod === 0) { return ''; } return format::format_time($enrolperiod, $row); }); // Start date column. $columns[] = (new column( 'startdate', new lang_string('enroltimestart', 'core_enrol'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_TIMESTAMP) ->add_fields("{$enrolalias}.enrolstartdate") ->set_is_sortable(true) ->set_callback([format::class, 'userdate']); // End date column. $columns[] = (new column( 'enddate', new lang_string('enroltimeend', 'core_enrol'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_TIMESTAMP) ->add_fields("{$enrolalias}.enrolenddate") ->set_is_sortable(true) ->set_callback([format::class, 'userdate']); return $columns; } /** * Return list of all available filters * * @return filter[] */ protected function get_all_filters(): array { global $DB; $enrolalias = $this->get_table_alias('enrol'); // Plugin filter. $filters[] = (new filter( select::class, 'plugin', new lang_string('plugin'), $this->get_entity_name(), "{$enrolalias}.enrol" )) ->add_joins($this->get_joins()) ->set_options_callback(static function(): array { return array_map(static function(enrol_plugin $plugin): string { return $plugin->get_instance_name(null); }, enrol_get_plugins(true)); }); // Custom name filter. $filters[] = (new filter( text::class, 'customname', new lang_string('custominstancename', 'core_enrol'), $this->get_entity_name(), "{$enrolalias}.name" )) ->add_joins($this->get_joins()); // Enabled filter. $filters[] = (new filter( boolean_select::class, 'enabled', new lang_string('enabled', 'core_admin'), $this->get_entity_name(), $DB->sql_bitxor("{$enrolalias}.status", 1) )) ->add_joins($this->get_joins()); // Period filter. $filters[] = (new filter( duration::class, 'period', new lang_string('enrolperiod', 'core_enrol'), $this->get_entity_name(), "{$enrolalias}.enrolperiod" )) ->add_joins($this->get_joins()); // Start date filter. $filters[] = (new filter( date::class, 'startdate', new lang_string('enroltimestart', 'core_enrol'), $this->get_entity_name(), "{$enrolalias}.enrolstartdate" )) ->add_joins($this->get_joins()); // End date filter. $filters[] = (new filter( date::class, 'enddate', new lang_string('enroltimeend', 'core_enrol'), $this->get_entity_name(), "{$enrolalias}.enrolenddate" )) ->add_joins($this->get_joins()); return $filters; } } PK%\<edatasource/cohorts.phpnu[. declare(strict_types=1); namespace core_cohort\reportbuilder\datasource; use core_cohort\reportbuilder\local\entities\{cohort, cohort_member}; use core_reportbuilder\local\filters\boolean_select; use core_reportbuilder\datasource; use core_reportbuilder\local\entities\user; /** * Cohorts datasource * * @package core_cohort * @copyright 2021 Paul Holden * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class cohorts extends datasource { /** * Return user friendly name of the datasource * * @return string */ public static function get_name(): string { return get_string('cohorts', 'core_cohort'); } /** * Initialise report */ protected function initialise(): void { $cohortentity = new cohort(); $cohorttablealias = $cohortentity->get_table_alias('cohort'); $this->set_main_table('cohort', $cohorttablealias); $this->add_entity($cohortentity); // Join the cohort member entity to the cohort entity. $cohortmemberentity = new cohort_member(); $cohortmembertablealias = $cohortmemberentity->get_table_alias('cohort_members'); $this->add_entity($cohortmemberentity ->add_join("LEFT JOIN {cohort_members} {$cohortmembertablealias} ON {$cohortmembertablealias}.cohortid = {$cohorttablealias}.id") ); // Join the user entity to the cohort member entity. $userentity = new user(); $usertablealias = $userentity->get_table_alias('user'); $this->add_entity($userentity ->add_joins($cohortmemberentity->get_joins()) ->add_join("LEFT JOIN {user} {$usertablealias} ON {$usertablealias}.id = {$cohortmembertablealias}.userid") ); // Add all columns/filters/conditions from entities to be available in custom reports. $this->add_all_from_entities(); } /** * Return the columns that will be added to the report as part of default setup * * @return string[] */ public function get_default_columns(): array { return [ 'cohort:name', 'cohort:context', 'cohort:idnumber', 'cohort:description', ]; } /** * Return the filters that will be added to the report once is created * * @return string[] */ public function get_default_filters(): array { return ['cohort:context', 'cohort:name']; } /** * Return the conditions that will be added to the report once is created * * @return string[] */ public function get_default_conditions(): array { return [ 'cohort:visible', ]; } /** * Return the condition values that will be set for the report upon creation * * @return array */ public function get_default_condition_values(): array { return [ 'cohort:visible_operator' => boolean_select::CHECKED, ]; } /** * Return the default sorting that will be added to the report once it is created * * @return array|int[] */ public function get_default_column_sorting(): array { return [ 'cohort:name' => SORT_ASC, ]; } } PK%\(44local/entities/cohort.phpnu[. declare(strict_types=1); namespace core_cohort\reportbuilder\local\entities; use context; use context_helper; use lang_string; use stdClass; use theme_config; use core_reportbuilder\local\entities\base; use core_reportbuilder\local\filters\boolean_select; use core_reportbuilder\local\filters\cohort as cohort_filter; use core_reportbuilder\local\filters\date; use core_reportbuilder\local\filters\select; use core_reportbuilder\local\filters\text; use core_reportbuilder\local\helpers\custom_fields; use core_reportbuilder\local\helpers\format; use core_reportbuilder\local\report\column; use core_reportbuilder\local\report\filter; /** * Cohort entity * * @package core_cohort * @copyright 2021 Paul Holden * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class cohort extends base { /** * Database tables that this entity uses * * @return string[] */ protected function get_default_tables(): array { return [ 'cohort', 'context', ]; } /** * The default title for this entity * * @return lang_string */ protected function get_default_entity_title(): lang_string { return new lang_string('cohort', 'core_cohort'); } /** * Initialise the entity * * @return base */ public function initialise(): base { $tablealias = $this->get_table_alias('cohort'); $customfields = (new custom_fields( "{$tablealias}.id", $this->get_entity_name(), 'core_cohort', 'cohort', )) ->add_joins($this->get_joins()); $columns = array_merge($this->get_all_columns(), $customfields->get_columns()); foreach ($columns as $column) { $this->add_column($column); } // All the filters defined by the entity can also be used as conditions. $filters = array_merge($this->get_all_filters(), $customfields->get_filters()); foreach ($filters as $filter) { $this ->add_filter($filter) ->add_condition($filter); } return $this; } /** * Returns list of all available columns * * @return column[] */ protected function get_all_columns(): array { $tablealias = $this->get_table_alias('cohort'); $contextalias = $this->get_table_alias('context'); // Category/context column. $columns[] = (new column( 'context', new lang_string('category'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->add_join($this->get_context_join()) ->set_type(column::TYPE_TEXT) ->add_fields("{$tablealias}.contextid, " . context_helper::get_preload_record_columns_sql($contextalias)) ->set_is_sortable(true) ->add_callback(static function($contextid, stdClass $cohort): string { if ($contextid === null) { return ''; } context_helper::preload_from_record($cohort); return context::instance_by_id($cohort->contextid)->get_context_name(false); }); // Name column. $columns[] = (new column( 'name', new lang_string('name', 'core_cohort'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_TEXT) ->add_join($this->get_context_join()) ->add_field("{$tablealias}.name") ->add_fields(context_helper::get_preload_record_columns_sql($contextalias)) ->set_is_sortable(true) ->set_callback(static function (?string $name, stdClass $cohort): string { if ($name === null || $cohort->ctxid === null) { return ''; } context_helper::preload_from_record(clone $cohort); $context = context::instance_by_id($cohort->ctxid); return format_string($name, options: ['context' => $context]); }); // ID number column. $columns[] = (new column( 'idnumber', new lang_string('idnumber', 'core_cohort'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_TEXT) ->add_fields("{$tablealias}.idnumber") ->set_is_sortable(true); // Description column. $columns[] = (new column( 'description', new lang_string('description'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->add_join($this->get_context_join()) ->set_type(column::TYPE_LONGTEXT) ->add_fields("{$tablealias}.description, {$tablealias}.descriptionformat, {$tablealias}.id, {$tablealias}.contextid") ->add_fields(context_helper::get_preload_record_columns_sql($contextalias)) ->set_is_sortable(true) ->add_callback(static function(?string $description, stdClass $cohort): string { global $CFG; require_once("{$CFG->libdir}/filelib.php"); if ($description === null) { return ''; } context_helper::preload_from_record($cohort); $context = context::instance_by_id($cohort->contextid); $description = file_rewrite_pluginfile_urls($description, 'pluginfile.php', $context->id, 'cohort', 'description', $cohort->id); return format_text($description, $cohort->descriptionformat, ['context' => $context->id]); }); // Visible column. $columns[] = (new column( 'visible', new lang_string('visible', 'core_cohort'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_BOOLEAN) ->add_fields("{$tablealias}.visible") ->set_is_sortable(true) ->set_callback([format::class, 'boolean_as_text']); // Time created column. $columns[] = (new column( 'timecreated', new lang_string('timecreated', 'core_reportbuilder'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_TIMESTAMP) ->add_fields("{$tablealias}.timecreated") ->set_is_sortable(true) ->set_callback([format::class, 'userdate']); // Time modified column. $columns[] = (new column( 'timemodified', new lang_string('timemodified', 'core_reportbuilder'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_TIMESTAMP) ->add_fields("{$tablealias}.timemodified") ->set_is_sortable(true) ->set_callback([format::class, 'userdate']); // Component column. $columns[] = (new column( 'component', new lang_string('component', 'core_cohort'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_TEXT) ->add_fields("{$tablealias}.component") ->set_is_sortable(true) ->add_callback(static function(?string $component): string { if ($component === null) { return ''; } return $component === '' ? get_string('nocomponent', 'cohort') : get_string('pluginname', $component); }); // Theme column. $columns[] = (new column( 'theme', new lang_string('theme'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_TEXT) ->add_fields("{$tablealias}.theme") ->set_is_sortable(true) ->add_callback(static function (?string $theme): string { if ((string) $theme === '') { return ''; } return get_string('pluginname', "theme_{$theme}"); }); return $columns; } /** * Return list of all available filters * * @return filter[] */ protected function get_all_filters(): array { $tablealias = $this->get_table_alias('cohort'); // Cohort select filter. $filters[] = (new filter( cohort_filter::class, 'cohortselect', new lang_string('selectcohort', 'core_cohort'), $this->get_entity_name(), "{$tablealias}.id" )) ->add_joins($this->get_joins()); // Context filter. $filters[] = (new filter( select::class, 'context', new lang_string('category'), $this->get_entity_name(), "{$tablealias}.contextid" )) ->add_joins($this->get_joins()) ->set_options_callback(static function(): array { global $DB; // Load all contexts in which there are cohorts. $ctxfields = context_helper::get_preload_record_columns_sql('ctx'); $contexts = $DB->get_records_sql(" SELECT DISTINCT {$ctxfields}, c.contextid FROM {context} ctx JOIN {cohort} c ON c.contextid = ctx.id"); // Transform context record into it's name (used as the filter options). return array_map(static function(stdClass $contextrecord): string { context_helper::preload_from_record($contextrecord); return context::instance_by_id($contextrecord->contextid) ->get_context_name(false); }, $contexts); }); // Name filter. $filters[] = (new filter( text::class, 'name', new lang_string('name', 'core_cohort'), $this->get_entity_name(), "{$tablealias}.name" )) ->add_joins($this->get_joins()); // ID number filter. $filters[] = (new filter( text::class, 'idnumber', new lang_string('idnumber', 'core_cohort'), $this->get_entity_name(), "{$tablealias}.idnumber" )) ->add_joins($this->get_joins()); // Time created filter. $filters[] = (new filter( date::class, 'timecreated', new lang_string('timecreated', 'core_reportbuilder'), $this->get_entity_name(), "{$tablealias}.timecreated" )) ->add_joins($this->get_joins()); // Description filter. $filters[] = (new filter( text::class, 'description', new lang_string('description'), $this->get_entity_name(), "{$tablealias}.description" )) ->add_joins($this->get_joins()); // Theme filter. $filters[] = (new filter( select::class, 'theme', new lang_string('theme'), $this->get_entity_name(), "{$tablealias}.theme", )) ->set_options_callback(static function(): array { return array_map( fn(theme_config $theme) => $theme->get_theme_name(), get_list_of_themes(), ); }) ->add_joins($this->get_joins()); // Visible filter. $filters[] = (new filter( boolean_select::class, 'visible', new lang_string('visible', 'core_cohort'), $this->get_entity_name(), "{$tablealias}.visible" )) ->add_joins($this->get_joins()); return $filters; } /** * Return context join used by columns * * @return string */ private function get_context_join(): string { // If the context table is already joined, we don't need to do that again. if ($this->has_table_join_alias('context')) { return ''; } $tablealias = $this->get_table_alias('cohort'); $contextalias = $this->get_table_alias('context'); return "LEFT JOIN {context} {$contextalias} ON {$contextalias}.id = {$tablealias}.contextid"; } } PK%\sĸw w local/entities/cohort_member.phpnu[. declare(strict_types=1); namespace core_cohort\reportbuilder\local\entities; use lang_string; use core_reportbuilder\local\entities\base; use core_reportbuilder\local\filters\date; use core_reportbuilder\local\helpers\format; use core_reportbuilder\local\report\column; use core_reportbuilder\local\report\filter; /** * Cohort member entity * * @package core_cohort * @copyright 2021 Paul Holden * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class cohort_member extends base { /** * Database tables that this entity uses * * @return string[] */ protected function get_default_tables(): array { return [ 'cohort_members', ]; } /** * The default title for this entity * * @return lang_string */ protected function get_default_entity_title(): lang_string { return new lang_string('cohortmember', 'core_cohort'); } /** * Initialise the entity * * @return base */ public function initialise(): base { $columns = $this->get_all_columns(); foreach ($columns as $column) { $this->add_column($column); } // All the filters defined by the entity can also be used as conditions. $filters = $this->get_all_filters(); foreach ($filters as $filter) { $this ->add_filter($filter) ->add_condition($filter); } return $this; } /** * Returns list of all available columns * * @return column[] */ protected function get_all_columns(): array { $tablealias = $this->get_table_alias('cohort_members'); // Time added column. $columns[] = (new column( 'timeadded', new lang_string('timeadded', 'core_reportbuilder'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_TIMESTAMP) ->add_fields("{$tablealias}.timeadded") ->set_is_sortable(true) ->set_callback([format::class, 'userdate']); return $columns; } /** * Return list of all available filters * * @return filter[] */ protected function get_all_filters(): array { $tablealias = $this->get_table_alias('cohort_members'); // Time added filter. $filters[] = (new filter( date::class, 'timeadded', new lang_string('timeadded', 'core_reportbuilder'), $this->get_entity_name(), "{$tablealias}.timeadded" )) ->add_joins($this->get_joins()); return $filters; } } PK%\#Ȝ(*-*-local/systemreports/cohorts.phpnu[. namespace core_cohort\reportbuilder\local\systemreports; use context; use context_coursecat; use context_system; use core_cohort\reportbuilder\local\entities\{cohort, cohort_member}; use core_reportbuilder\local\helpers\database; use core_reportbuilder\local\report\action; use core_reportbuilder\local\report\column; use html_writer; use lang_string; use moodle_url; use pix_icon; use core_reportbuilder\system_report; use stdClass; /** * Cohorts system report class implementation * * @package core_cohort * @copyright 2021 David Matamoros * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class cohorts extends system_report { /** * Initialise report, we need to set the main table, load our entities and set columns/filters */ protected function initialise(): void { // Our main entity, it contains all of the column definitions that we need. $cohortentity = new cohort(); $entitymainalias = $cohortentity->get_table_alias('cohort'); $this->set_main_table('cohort', $entitymainalias); $this->add_entity($cohortentity); // Join cohort member entity. $cohortmemberentity = new cohort_member(); $cohortmemberalias = $cohortmemberentity->get_table_alias('cohort_members'); $this->add_entity($cohortmemberentity ->add_join("LEFT JOIN {cohort_members} {$cohortmemberalias} ON {$cohortmemberalias}.cohortid = {$entitymainalias}.id") ); // Any columns required by actions should be defined here to ensure they're always available. $this->add_base_fields("{$entitymainalias}.id, {$entitymainalias}.contextid, {$entitymainalias}.visible, " . "{$entitymainalias}.component, {$entitymainalias}.name"); $this->set_checkbox_toggleall(static function(stdClass $cohort): ?array { if (!empty($cohort->component) || !has_capability('moodle/cohort:manage', context::instance_by_id($cohort->contextid))) { return null; } return [ $cohort->id, get_string('selectitem', 'moodle', $cohort->name), ]; }); // Check if report needs to show a specific category. if (!$this->get_context() instanceof context_system || !$this->get_parameter('showall', false, PARAM_BOOL)) { $paramcontextid = database::generate_param_name(); $this->add_base_condition_sql("{$entitymainalias}.contextid = :{$paramcontextid}", [ $paramcontextid => $this->get_context()->id, ]); } // Now we can call our helper methods to add the content we want to include in the report. $this->add_columns(); $this->add_filters(); $this->add_actions(); // Set if report can be downloaded. $this->set_downloadable(false); } /** * Validates access to view this report * * @return bool */ protected function can_view(): bool { return has_any_capability(['moodle/cohort:manage', 'moodle/cohort:view'], $this->get_context()); } /** * Adds the columns we want to display in the report * * They are provided by the entities we previously added in the {@see initialise} method, referencing each by their * unique identifier. If custom columns are needed just for this report, they can be defined here. */ protected function add_columns(): void { $cohortentity = $this->get_entity('cohort'); $entitymainalias = $cohortentity->get_table_alias('cohort'); // Category column. An extra callback is appended in order to extend the current column formatting. if ($this->get_context() instanceof context_system && $this->get_parameter('showall', false, PARAM_BOOL)) { $this->add_column_from_entity('cohort:context') ->add_callback(static function(string $value, stdClass $cohort): string { $context = context::instance_by_id($cohort->contextid); if ($context instanceof context_coursecat) { return html_writer::link(new moodle_url('/cohort/index.php', ['contextid' => $cohort->contextid]), $value); } return $value; }); } // Name column using the inplace editable component. $this->add_column(new column( 'editablename', new lang_string('name', 'core_cohort'), $cohortentity->get_entity_name() )) ->set_type(column::TYPE_TEXT) ->set_is_sortable(true) ->add_fields("{$entitymainalias}.name, {$entitymainalias}.id, {$entitymainalias}.contextid") ->add_callback(static function(string $name, stdClass $cohort): string { global $OUTPUT, $PAGE; $renderer = $PAGE->get_renderer('core'); $template = new \core_cohort\output\cohortname($cohort); return $renderer->render_from_template('core/inplace_editable', $template->export_for_template($OUTPUT)); }); // ID Number column using the inplace editable component. $this->add_column(new column( 'editableidnumber', new lang_string('idnumber', 'core_cohort'), $cohortentity->get_entity_name() )) ->set_type(column::TYPE_TEXT) ->set_is_sortable(true) ->add_fields("{$entitymainalias}.idnumber, {$entitymainalias}.id, {$entitymainalias}.contextid") ->add_callback(static function(?string $idnumber, stdClass $cohort): string { global $OUTPUT, $PAGE; $renderer = $PAGE->get_renderer('core'); $template = new \core_cohort\output\cohortidnumber($cohort); return $renderer->render_from_template('core/inplace_editable', $template->export_for_template($OUTPUT)); }); // Description column. $this->add_column_from_entity('cohort:description'); // Member count. $this->add_column_from_entity('cohort_member:timeadded') ->set_title(new lang_string('memberscount', 'cohort')) ->set_aggregation('count'); // Component column. Override the display name of a column. $this->add_column_from_entity('cohort:component') ->set_title(new lang_string('source', 'core_plugin')); // It's possible to set a default initial sort direction for one column. $this->set_initial_sort_column('cohort:editablename', SORT_ASC); } /** * Adds the filters we want to display in the report * * They are all provided by the entities we previously added in the {@see initialise} method, referencing each by their * unique identifier */ protected function add_filters(): void { $this->add_filters_from_entity('cohort', ['name', 'idnumber', 'description', 'customfield*']); } /** * Add the system report actions. An extra column will be appended to each row, containing all actions added here * * Note the use of ":id" placeholder which will be substituted according to actual values in the row */ protected function add_actions(): void { $returnurl = (new moodle_url('/cohort/index.php', [ 'id' => ':id', 'contextid' => $this->get_context()->id, 'showall' => $this->get_parameter('showall', false, PARAM_BOOL), ]))->out(false); // Hide action. It will be only shown if the property 'visible' is true and user has 'moodle/cohort:manage' capabillity. $this->add_action((new action( new moodle_url('/cohort/edit.php', ['id' => ':id', 'sesskey' => sesskey(), 'hide' => 1, 'returnurl' => $returnurl]), new pix_icon('t/show', '', 'core'), [], false, new lang_string('hide') ))->add_callback(function(stdClass $row): bool { return empty($row->component) && $row->visible && has_capability('moodle/cohort:manage', context::instance_by_id($row->contextid)); })); // Show action. It will be only shown if the property 'visible' is false and user has 'moodle/cohort:manage' capabillity. $this->add_action((new action( new moodle_url('/cohort/edit.php', ['id' => ':id', 'sesskey' => sesskey(), 'show' => 1, 'returnurl' => $returnurl]), new pix_icon('t/hide', '', 'core'), [], false, new lang_string('show') ))->add_callback(function(stdClass $row): bool { return empty($row->component) && !$row->visible && has_capability('moodle/cohort:manage', context::instance_by_id($row->contextid)); })); // Edit action. It will be only shown if user has 'moodle/cohort:manage' capabillity. $this->add_action((new action( new moodle_url('/cohort/edit.php', ['id' => ':id', 'returnurl' => $returnurl]), new pix_icon('t/edit', '', 'core'), [], false, new lang_string('edit') ))->add_callback(function(stdClass $row): bool { return empty($row->component) && has_capability('moodle/cohort:manage', context::instance_by_id($row->contextid)); })); // Delete action. It will be only shown if user has 'moodle/cohort:manage' capabillity. $this->add_action((new action( new moodle_url('#'), new pix_icon('t/delete', '', 'core'), ['class' => 'text-danger', 'data-action' => 'cohort-delete', 'data-cohort-id' => ':id', 'data-cohort-name' => ':name'], false, new lang_string('delete') ))->add_callback(function(stdClass $row): bool { return empty($row->component) && has_capability('moodle/cohort:manage', context::instance_by_id($row->contextid)); })); // Assign members to cohort action. It will be only shown if user has 'moodle/cohort:assign' capabillity. $this->add_action((new action( new moodle_url('/cohort/assign.php', ['id' => ':id', 'returnurl' => $returnurl]), new pix_icon('i/users', '', 'core'), [], false, new lang_string('assign', 'core_cohort') ))->add_callback(function(stdClass $row): bool { return empty($row->component) && has_capability('moodle/cohort:assign', context::instance_by_id($row->contextid)); })); } /** * CSS class for the row * * @param stdClass $row * @return string */ public function get_row_class(stdClass $row): string { return (!$row->visible) ? 'text-muted' : ''; } } PK%\xxaudience/cohortmember.phpnu[. declare(strict_types=1); namespace core_cohort\reportbuilder\audience; use context; use context_system; use core_course_category; use stdClass; use core_reportbuilder\local\audiences\base; use core_reportbuilder\local\helpers\database; use MoodleQuickForm; /** * The backend class for Cohort member audience type * * @package core_reportbuilder * @copyright 2021 David Matamoros * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class cohortmember extends base { /** * Adds audience's elements to the given mform * * @param MoodleQuickForm $mform The form to add elements to */ public function get_config_form(MoodleQuickForm $mform): void { $cohorts = self::get_cohorts(); $mform->addElement('autocomplete', 'cohorts', get_string('selectfromcohort', 'cohort'), $cohorts, ['multiple' => true]); $mform->addRule('cohorts', null, 'required', null, 'client'); } /** * Helps to build SQL to retrieve users that matches the current report audience * * @param string $usertablealias * @return array array of three elements [$join, $where, $params] */ public function get_sql(string $usertablealias): array { global $DB; $cohorts = $this->get_configdata()['cohorts']; [$insql, $inparams] = $DB->get_in_or_equal($cohorts, SQL_PARAMS_NAMED, database::generate_param_name('_')); $cm = database::generate_alias(); $join = "JOIN {cohort_members} {$cm} ON ({$cm}.userid = {$usertablealias}.id)"; return [$join, "{$cm}.cohortid " . $insql, $inparams]; } /** * Return user friendly name of this audience type * * @return string */ public function get_name(): string { return get_string('memberofcohort', 'cohort'); } /** * Return the description for the audience. * * @return string */ public function get_description(): string { global $DB; $cohortlist = []; $cohortids = $this->get_configdata()['cohorts']; $cohorts = $DB->get_records_list('cohort', 'id', $cohortids, 'name'); foreach ($cohorts as $cohort) { $cohortlist[] = format_string($cohort->name, true, ['context' => $cohort->contextid, 'escape' => false]); } return $this->format_description_for_multiselect($cohortlist); } /** * If the current user is able to add this audience. * * @return bool */ public function user_can_add(): bool { // Check system context first. if (has_capability('moodle/cohort:view', context_system::instance())) { return true; } // If there is at least one category with given permissions, user can add. return !empty(core_course_category::make_categories_list('moodle/cohort:view')); } /** * Returns if this audience type is available for the user * * Check if there are available cohorts in the system for this user to use. * * @return bool */ public function is_available(): bool { return !empty(self::get_cohorts()); } /** * If the current user is able to edit this audience. * * @return bool */ public function user_can_edit(): bool { global $DB; $canedit = true; $cohortids = $this->get_configdata()['cohorts']; $cohorts = $DB->get_records_list('cohort', 'id', $cohortids); foreach ($cohorts as $cohort) { $context = context::instance_by_id($cohort->contextid, MUST_EXIST); $canedit = $canedit && has_capability('moodle/cohort:view', $context); if ($canedit === false) { break; } } return $canedit; } /** * Cohorts selector. * * @return array */ private static function get_cohorts(): array { global $CFG; require_once($CFG->dirroot.'/cohort/lib.php'); $cohortslist = []; // Search cohorts user can view. $usercohorts = cohort_get_all_cohorts(0, 0); // The previous method doesn't check cohorts on system context. $syscontext = context_system::instance(); $cohorts = array_filter($usercohorts['cohorts'], static function(stdClass $cohort) use ($syscontext): bool { return ($cohort->contextid != $syscontext->id) || has_capability('moodle/cohort:view', $syscontext); }); foreach ($cohorts as $cohort) { $cohortslist[$cohort->id] = format_string($cohort->name, true, [ 'context' => $cohort->contextid, 'escape' => false, ]); } return $cohortslist; } } PKO\|a)a)datasource/notes_test.phpnu[. declare(strict_types=1); namespace core_notes\reportbuilder\datasource; use core_notes_generator; use core_reportbuilder_generator; use core_reportbuilder\local\filters\{date, select, text}; use core_reportbuilder\tests\core_reportbuilder_testcase; /** * Unit tests for notes datasource * * @package core_notes * @covers \core_notes\reportbuilder\datasource\notes * @copyright 2022 Paul Holden * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class notes_test extends core_reportbuilder_testcase { /** * Load required test libraries */ public static function setUpBeforeClass(): void { global $CFG; require_once("{$CFG->dirroot}/notes/lib.php"); parent::setUpBeforeClass(); } /** * Test default datasource */ public function test_datasource_default(): void { $this->resetAfterTest(); /** @var core_notes_generator $notesgenerator */ $notesgenerator = $this->getDataGenerator()->get_plugin_generator('core_notes'); // Our first user will create a course note. $course = $this->getDataGenerator()->create_course(); $userone = $this->getDataGenerator()->create_and_enrol($course, 'student', ['firstname' => 'Zoe']); $coursenote = $notesgenerator->create_instance(['courseid' => $course->id, 'userid' => $userone->id, 'content' => 'Course', 'publishstate' => NOTES_STATE_PUBLIC]); // Our second user will create a personal and site note. $usertwo = $this->getDataGenerator()->create_user(['firstname' => 'Amy']); $personalnote = $notesgenerator->create_instance(['courseid' => SITEID, 'userid' => $usertwo->id, 'content' => 'Personal', 'publishstate' => NOTES_STATE_DRAFT]); $this->waitForSecond(); // For consistent ordering we need distinct time for second user notes. $sitenote = $notesgenerator->create_instance(['courseid' => SITEID, 'userid' => $usertwo->id, 'content' => 'Site', 'publishstate' => NOTES_STATE_SITE]); /** @var core_reportbuilder_generator $generator */ $generator = $this->getDataGenerator()->get_plugin_generator('core_reportbuilder'); $report = $generator->create_report(['name' => 'Notes', 'source' => notes::class, 'default' => 1]); $content = $this->get_custom_report_content($report->get('id')); // Default columns are recipient, publishstate, course, note, time created. Sorted by recipient and time created. $this->assertEquals([ [fullname($usertwo), 'Personal notes', '', 'Personal', userdate($personalnote->created)], [fullname($usertwo), 'Site notes', '', 'Site', userdate($sitenote->created)], [fullname($userone), 'Course notes', $course->fullname, 'Course', userdate($coursenote->created)], ], array_map('array_values', $content)); } /** * Test datasource columns that aren't added by default */ public function test_datasource_non_default_columns(): void { global $DB; $this->resetAfterTest(); $recipient = $this->getDataGenerator()->create_user(); $author = $this->getDataGenerator()->create_user(); $this->setUser($author); /** @var core_notes_generator $notesgenerator */ $notesgenerator = $this->getDataGenerator()->get_plugin_generator('core_notes'); $note = $notesgenerator->create_instance(['courseid' => SITEID, 'publishstate' => NOTES_STATE_SITE, 'content' => 'Cool', 'userid' => $recipient->id, ]); // Manually update the created/modified date of the note. $note->created = 1654038000; $note->lastmodified = $note->created + HOURSECS; $DB->update_record('post', $note); /** @var core_reportbuilder_generator $generator */ $generator = $this->getDataGenerator()->get_plugin_generator('core_reportbuilder'); $report = $generator->create_report(['name' => 'Notes', 'source' => notes::class, 'default' => 0]); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'note:content']); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'note:timecreated']); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'note:timemodified']); // Ensure we can add data from both user entities. $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'recipient:fullname']); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'author:fullname']); $content = $this->get_custom_report_content($report->get('id')); $this->assertCount(1, $content); $this->assertEquals([ 'Cool', userdate($note->created), userdate($note->lastmodified), fullname($recipient), fullname($author), ], array_values($content[0])); } /** * Data provider for {@see test_datasource_filters} * * @return array[] */ public static function datasource_filters_provider(): array { return [ 'Filter content' => ['content', 'Cool', 'note:content', [ 'note:content_operator' => text::IS_EQUAL_TO, 'note:content_value' => 'Cool', ], true], 'Filter content (no match)' => ['content', 'Cool', 'note:content', [ 'note:content_operator' => text::DOES_NOT_CONTAIN, 'note:content_value' => 'Cool', ], false], 'Filter publish state' => ['publishstate', 'site', 'note:publishstate', [ 'note:publishstate_operator' => select::EQUAL_TO, 'note:publishstate_value' => 'site', ], true], 'Filter publish state (no match)' => ['publishstate', 'site', 'note:publishstate', [ 'note:publishstate_operator' => select::EQUAL_TO, 'note:publishstate_value' => 'public', ], false], 'Filter time created' => ['created', 1654038000, 'note:timecreated', [ 'note:timecreated_operator' => date::DATE_RANGE, 'note:timecreated_from' => 1622502000, ], true], 'Filter time created (no match)' => ['created', 1654038000, 'note:timecreated', [ 'note:timecreated_operator' => date::DATE_RANGE, 'note:timecreated_to' => 1622502000, ], false], 'Filter time modified' => ['lastmodified', 1654038000, 'note:timemodified', [ 'note:timemodified_operator' => date::DATE_RANGE, 'note:timemodified_from' => 1622502000, ], true], 'Filter time modified (no match)' => ['lastmodified', 1654038000, 'note:timemodified', [ 'note:timemodified_operator' => date::DATE_RANGE, 'note:timemodified_to' => 1622502000, ], false], ]; } /** * Test datasource filters * * @param string $field * @param mixed $value * @param string $filtername * @param array $filtervalues * @param bool $expectmatch * * @dataProvider datasource_filters_provider */ public function test_datasource_filters( string $field, $value, string $filtername, array $filtervalues, bool $expectmatch ): void { global $DB; $this->resetAfterTest(); $recipient = $this->getDataGenerator()->create_user(); /** @var core_notes_generator $notesgenerator */ $notesgenerator = $this->getDataGenerator()->get_plugin_generator('core_notes'); // Create default note, then manually override one of it's properties to use for filtering. $note = $notesgenerator->create_instance(['courseid' => SITEID, 'userid' => $recipient->id]); $DB->set_field('post', $field, $value, ['id' => $note->id]); /** @var core_reportbuilder_generator $generator */ $generator = $this->getDataGenerator()->get_plugin_generator('core_reportbuilder'); // Create report containing single recipient column, and given filter. $report = $generator->create_report(['name' => 'Notes', 'source' => notes::class, 'default' => 0]); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'recipient:fullname']); // Add filter, set it's values. $generator->create_filter(['reportid' => $report->get('id'), 'uniqueidentifier' => $filtername]); $content = $this->get_custom_report_content($report->get('id'), 0, $filtervalues); if ($expectmatch) { $this->assertCount(1, $content); $this->assertEquals(fullname($recipient), reset($content[0])); } else { $this->assertEmpty($content); } } /** * Stress test datasource * * In order to execute this test PHPUNIT_LONGTEST should be defined as true in phpunit.xml or directly in config.php */ public function test_stress_datasource(): void { if (!PHPUNIT_LONGTEST) { $this->markTestSkipped('PHPUNIT_LONGTEST is not defined'); } $this->resetAfterTest(); $recipient = $this->getDataGenerator()->create_user(); /** @var core_notes_generator $notesgenerator */ $notesgenerator = $this->getDataGenerator()->get_plugin_generator('core_notes'); $notesgenerator->create_instance(['courseid' => SITEID, 'userid' => $recipient->id]); $this->datasource_stress_test_columns(notes::class); $this->datasource_stress_test_columns_aggregation(notes::class); $this->datasource_stress_test_conditions(notes::class, 'note:content'); } } PKe\YCYCdatasource/files_test.phpnu[. declare(strict_types=1); namespace core_files\reportbuilder\datasource; use core\context\{course, coursecat, user}; use core_reportbuilder_generator; use core_reportbuilder\local\filters\{boolean_select, date, filesize, select, text}; use core_reportbuilder\tests\core_reportbuilder_testcase; /** * Unit tests for files datasource * * @package core_files * @covers \core_files\reportbuilder\datasource\files * @copyright 2022 Paul Holden * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class files_test extends core_reportbuilder_testcase { /** * Test default datasource */ public function test_datasource_default(): void { $this->resetAfterTest(); $course = $this->getDataGenerator()->create_course(); $coursecontext = course::instance($course->id); $user = $this->getDataGenerator()->create_user(); $usercontext = user::instance($user->id); $this->setUser($user); $this->generate_test_files($coursecontext); /** @var core_reportbuilder_generator $generator */ $generator = $this->getDataGenerator()->get_plugin_generator('core_reportbuilder'); $report = $generator->create_report(['name' => 'Files', 'source' => files::class, 'default' => 1]); $content = $this->get_custom_report_content($report->get('id')); $content = $this->filter_custom_report_content($content, static function(array $row): bool { return $row['c0_ctxid'] !== 'System'; }); $this->assertCount(2, $content); // Default columns are context, user, name, type, size, time created. Sorted by context and time created. [$contextname, $userfullname, $filename, $mimetype, $filesize, $timecreated] = array_values($content[0]); $this->assertEquals($coursecontext->get_context_name(), $contextname); $this->assertEquals(fullname($user), $userfullname); $this->assertEquals('Hello.txt', $filename); $this->assertEquals('Text file', $mimetype); $this->assertEquals("5\xc2\xa0bytes", $filesize); $this->assertNotEmpty($timecreated); [$contextname, $userfullname, $filename, $mimetype, $filesize, $timecreated] = array_values($content[1]); $this->assertEquals($usercontext->get_context_name(), $contextname); $this->assertEquals(fullname($user), $userfullname); $this->assertEquals('Hello.txt', $filename); $this->assertEquals('Text file', $mimetype); $this->assertEquals("5\xc2\xa0bytes", $filesize); $this->assertNotEmpty($timecreated); } /** * Test datasource columns that aren't added by default */ public function test_datasource_non_default_columns(): void { global $OUTPUT; $this->resetAfterTest(); $category = $this->getDataGenerator()->create_category(); $categorycontext = coursecat::instance($category->id); $course = $this->getDataGenerator()->create_course(['category' => $category->id]); $coursecontext = course::instance($course->id); $user = $this->getDataGenerator()->create_user(); $usercontext = user::instance($user->id); $this->setUser($user); $draftitemid = $this->generate_test_files($coursecontext); /** @var core_reportbuilder_generator $generator */ $generator = $this->getDataGenerator()->get_plugin_generator('core_reportbuilder'); $report = $generator->create_report(['name' => 'Files', 'source' => files::class, 'default' => 0]); // Consistent order, sorted by context and content hash. $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'context:link', 'sortenabled' => 1, 'sortorder' => 1]); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'context:name']); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'context:level']); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'context:path']); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'context:parent']); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'file:icon']); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'file:path']); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'file:author']); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'file:license']); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'file:contenthash', 'sortenabled' => 1, 'sortorder' => 2]); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'file:component']); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'file:area']); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'file:itemid']); $content = $this->get_custom_report_content($report->get('id')); $content = $this->filter_custom_report_content($content, static function(array $row): bool { return stripos($row['c0_ctxid'], 'System') === false; }); // There should be two entries (directory & file) for each context. $this->assertEquals([ [ "get_url()}\">{$coursecontext->get_context_name()}", $coursecontext->get_context_name(), 'Course', $coursecontext->path, $categorycontext->get_context_name(), 'Directory', '/', null, '', 'da39a3ee5e6b4b0d3255bfef95601890afd80709', 'course', 'summary', 0, ], [ "get_url()}\">{$coursecontext->get_context_name()}", $coursecontext->get_context_name(), 'Course', $coursecontext->path, $categorycontext->get_context_name(), 'Text file', '/', null, '', 'f7ff9e8b7bb2e09b70935a5d785e0cc5d9d0abf0', 'course', 'summary', 0, ], [ "get_url()}\">{$usercontext->get_context_name()}", $usercontext->get_context_name(), 'User', $usercontext->path, 'System', 'Directory', '/', null, '', 'da39a3ee5e6b4b0d3255bfef95601890afd80709', 'user', 'draft', $draftitemid, ], [ "get_url()}\">{$usercontext->get_context_name()}", $usercontext->get_context_name(), 'User', $usercontext->path, 'System', 'Text file', '/', null, '', 'f7ff9e8b7bb2e09b70935a5d785e0cc5d9d0abf0', 'user', 'draft', $draftitemid, ], ], array_map('array_values', $content)); } /** * Data provider for {@see test_datasource_filters} * * @return array[] */ public static function datasource_filters_provider(): array { return [ // File. 'Filter directory' => ['file:directory', [ 'file:directory_operator' => boolean_select::CHECKED, ], 2], 'Filter draft' => ['file:draft', [ 'file:draft_operator' => boolean_select::CHECKED, ], 2], 'Filter name' => ['file:name', [ 'file:name_operator' => text::IS_EQUAL_TO, 'file:name_value' => 'Hello.txt', ], 2], 'Filter size' => ['file:size', [ 'file:size_operator' => filesize::GREATER_THAN, 'file:size_value1' => 2, 'file:size_unit' => filesize::SIZE_UNIT_BYTE, ], 2], 'Filter type' => ['file:type', [ 'file:type_operator' => select::EQUAL_TO, 'file:type_value' => 'text/plain', ], 2], 'Filter type (non match)' => ['file:type', [ 'file:type_operator' => select::EQUAL_TO, 'file:type_value' => 'image/png', ], 0], 'Filter author' => ['file:author', [ 'file:author_operator' => text::IS_EMPTY, ], 4], 'Filter author (non match)' => ['file:author', [ 'file:author_operator' => text::IS_NOT_EMPTY, ], 0], 'Filter license' => ['file:license', [ 'file:license_operator' => select::EQUAL_TO, 'file:license_value' => 'unknown', ], 4], 'Filter license (non match)' => ['file:license', [ 'file:license_operator' => select::EQUAL_TO, 'file:license_value' => 'public', ], 0], 'Filter content hash' => ['file:contenthash', [ 'file:contenthash_operator' => text::IS_EQUAL_TO, 'file:contenthash_value' => 'f7ff9e8b7bb2e09b70935a5d785e0cc5d9d0abf0', ], 2], 'Filter content hash (no match)' => ['file:contenthash', [ 'file:contenthash_operator' => text::IS_EQUAL_TO, 'file:contenthash_value' => 'f00f', ], 0], 'Filter component' => ['file:component', [ 'file:component_operator' => text::IS_EQUAL_TO, 'file:component_value' => 'course', ], 2], 'Filter component (no match)' => ['file:component', [ 'file:component_operator' => text::IS_EQUAL_TO, 'file:component_value' => 'reportbuilder', ], 0], 'Filter area' => ['file:area', [ 'file:area_operator' => text::IS_EQUAL_TO, 'file:area_value' => 'summary', ], 2], 'Filter area (no match)' => ['file:area', [ 'file:area_operator' => text::IS_EQUAL_TO, 'file:area_value' => 'report', ], 0], 'Filter time created' => ['file:timecreated', [ 'file:timecreated_operator' => date::DATE_RANGE, 'file:timecreated_from' => 1622502000, ], 4], 'Filter time created (non match)' => ['file:timecreated', [ 'file:timecreated_operator' => date::DATE_RANGE, 'file:timecreated_to' => 1622502000, ], 0], // Context. 'Context level' => ['context:level', [ 'context:level_operator' => select::EQUAL_TO, 'context:level_value' => CONTEXT_COURSE, ], 2], 'Context level (no match)' => ['context:level', [ 'context:level_operator' => select::EQUAL_TO, 'context:level_value' => CONTEXT_BLOCK, ], 0], 'Context path' => ['context:path', [ 'context:path_operator' => text::STARTS_WITH, 'context:path_value' => '/1/', ], 4], 'Context path (no match)' => ['context:path', [ 'context:path_operator' => text::STARTS_WITH, 'context:path_value' => '/1/2/3/', ], 0], // User. 'Filter user' => ['user:username', [ 'user:username_operator' => text::IS_EQUAL_TO, 'user:username_value' => 'alfie', ], 4], 'Filter user (no match)' => ['user:username', [ 'user:username_operator' => text::IS_EQUAL_TO, 'user:username_value' => 'lionel', ], 0], ]; } /** * Test datasource filters * * @param string $filtername * @param array $filtervalues * @param int $expectmatchcount * * @dataProvider datasource_filters_provider */ public function test_datasource_filters( string $filtername, array $filtervalues, int $expectmatchcount ): void { $this->resetAfterTest(); $this->setAdminUser(); $user = $this->getDataGenerator()->create_user(['username' => 'alfie']); $this->setUser($user); $course = $this->getDataGenerator()->create_course(); $coursecontext = course::instance($course->id); $this->generate_test_files($coursecontext); /** @var core_reportbuilder_generator $generator */ $generator = $this->getDataGenerator()->get_plugin_generator('core_reportbuilder'); // Create report containing single column, and given filter. $report = $generator->create_report(['name' => 'Files', 'source' => files::class, 'default' => 0]); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'context:name']); // Add filter, set it's values. $generator->create_filter(['reportid' => $report->get('id'), 'uniqueidentifier' => $filtername]); $content = $this->get_custom_report_content($report->get('id'), 0, $filtervalues); $content = $this->filter_custom_report_content($content, static function(array $row): bool { return stripos($row['c0_ctxid'], 'System') === false; }); $this->assertCount($expectmatchcount, $content); } /** * Stress test datasource * * In order to execute this test PHPUNIT_LONGTEST should be defined as true in phpunit.xml or directly in config.php */ public function test_stress_datasource(): void { if (!PHPUNIT_LONGTEST) { $this->markTestSkipped('PHPUNIT_LONGTEST is not defined'); } $this->resetAfterTest(); $this->setAdminUser(); $course = $this->getDataGenerator()->create_course(); $coursecontext = course::instance($course->id); $this->generate_test_files($coursecontext); $this->datasource_stress_test_columns(files::class); $this->datasource_stress_test_columns_aggregation(files::class); $this->datasource_stress_test_conditions(files::class, 'file:path'); } /** * Ensuring report content only includes files we have explicitly created within the test * * @param array $content * @param callable $callback * @return array */ protected function filter_custom_report_content(array $content, callable $callback): array { $content = array_filter($content, $callback); return array_values($content); } /** * Helper method to generate some test files (a user draft and course summary file) for reporting on * * @param course $context * @return int Draft item ID */ protected function generate_test_files(course $context): int { global $USER; $draftitemid = file_get_unused_draft_itemid(); // Populate user draft. get_file_storage()->create_file_from_string([ 'contextid' => user::instance($USER->id)->id, 'userid' => $USER->id, 'component' => 'user', 'filearea' => 'draft', 'itemid' => $draftitemid, 'filepath' => '/', 'filename' => 'Hello.txt', ], 'Hello'); // Save draft to course summary file area. file_save_draft_area_files($draftitemid, $context->id, 'course', 'summary', 0); return $draftitemid; } } PK\**00datasource/cohorts_test.phpnu[. declare(strict_types=1); namespace core_cohort\reportbuilder\datasource; use core\context\{coursecat, system}; use core_customfield_generator; use core_reportbuilder_generator; use core_reportbuilder\local\filters\{boolean_select, date, select, text}; use core_reportbuilder\tests\core_reportbuilder_testcase; /** * Unit tests for cohorts datasource * * @package core_cohort * @covers \core_cohort\reportbuilder\datasource\cohorts * @copyright 2021 Paul Holden * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class cohorts_test extends core_reportbuilder_testcase { /** * Test default datasource */ public function test_datasource_default(): void { $this->resetAfterTest(); // Test subject. $contextsystem = system::instance(); $cohortone = $this->getDataGenerator()->create_cohort([ 'contextid' => $contextsystem->id, 'name' => 'Legends', 'idnumber' => 'C101', 'description' => 'Cohort for the legends', ]); $category = $this->getDataGenerator()->create_category(); $contextcategory = coursecat::instance($category->id); $cohorttwo = $this->getDataGenerator()->create_cohort([ 'contextid' => $contextcategory->id, 'name' => 'Category cohort', 'description' => 'This is my category cohort', ]); // Non-visible cohort (excluded by default). $cohortnonvisible = $this->getDataGenerator()->create_cohort([ 'contextid' => $contextsystem->id, 'name' => 'Non-visible', 'visible' => false, ]); /** @var core_reportbuilder_generator $generator */ $generator = $this->getDataGenerator()->get_plugin_generator('core_reportbuilder'); $report = $generator->create_report(['name' => 'Cohorts', 'source' => cohorts::class, 'default' => 1]); $content = $this->get_custom_report_content($report->get('id')); // Default columns are name, context, idnumber, description. Sorted by name. $this->assertEquals([ [$cohorttwo->name, $contextcategory->get_context_name(false), $cohorttwo->idnumber, format_text($cohorttwo->description)], [$cohortone->name, $contextsystem->get_context_name(false), $cohortone->idnumber, format_text($cohortone->description)], ], array_map('array_values', $content)); } /** * Test datasource columns that aren't added by default */ public function test_datasource_non_default_columns(): void { $this->resetAfterTest(); $this->setAdminUser(); set_config('allowcohortthemes', true); /** @var core_customfield_generator $generator */ $generator = $this->getDataGenerator()->get_plugin_generator('core_customfield'); $fieldcategory = $generator->create_category(['component' => 'core_cohort', 'area' => 'cohort']); $field = $generator->create_field(['categoryid' => $fieldcategory->get('id'), 'shortname' => 'hi']); // Test subject. $cohort = $this->getDataGenerator()->create_cohort([ 'name' => 'Legends', 'idnumber' => 'C101', 'description' => 'Cohort for the legends', 'theme' => 'boost', 'customfield_hi' => 'Hello', ]); $user = $this->getDataGenerator()->create_user(['firstname' => 'Lionel', 'lastname' => 'Richards']); cohort_add_member($cohort->id, $user->id); /** @var core_reportbuilder_generator $generator */ $generator = $this->getDataGenerator()->get_plugin_generator('core_reportbuilder'); $report = $generator->create_report(['name' => 'Cohorts', 'source' => cohorts::class, 'default' => 0]); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'cohort:visible']); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'cohort:timecreated']); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'cohort:timemodified']); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'cohort:component']); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'cohort:theme']); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'cohort:customfield_hi']); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'cohort_member:timeadded']); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'user:fullname']); $content = $this->get_custom_report_content($report->get('id')); $this->assertCount(1, $content); [$visible, $timecreated, $timemodified, $component, $theme, $custom, $timeadded, $fullname] = array_values($content[0]); $this->assertEquals('Yes', $visible); $this->assertNotEmpty($timecreated); $this->assertNotEmpty($timemodified); $this->assertEquals('Created manually', $component); $this->assertEquals('Boost', $theme); $this->assertEquals('Hello', $custom); $this->assertNotEmpty($timeadded); $this->assertEquals(fullname($user), $fullname); } /** * Data provider for {@see test_datasource_filters} * * @return array[] */ public static function datasource_filters_provider(): array { return [ // Cohort. 'Filter cohort' => ['cohort:cohortselect', [ 'cohort:cohortselect_values' => [-1], ], false], 'Filter context' => ['cohort:context', [ 'cohort:context_operator' => select::EQUAL_TO, 'cohort:context_value' => system::instance()->id, ], true], 'Filter context (no match)' => ['cohort:context', [ 'cohort:context_operator' => select::NOT_EQUAL_TO, 'cohort:context_value' => system::instance()->id, ], false], 'Filter name' => ['cohort:name', [ 'cohort:name_operator' => text::IS_EQUAL_TO, 'cohort:name_value' => 'Legends', ], true], 'Filter name (no match)' => ['cohort:name', [ 'cohort:name_operator' => text::IS_EQUAL_TO, 'cohort:name_value' => 'Dancing', ], false], 'Filter idnumber' => ['cohort:idnumber', [ 'cohort:idnumber_operator' => text::IS_EQUAL_TO, 'cohort:idnumber_value' => 'C101', ], true], 'Filter idnumber (no match)' => ['cohort:idnumber', [ 'cohort:idnumber_operator' => text::IS_EQUAL_TO, 'cohort:idnumber_value' => 'C102', ], false], 'Filter time created' => ['cohort:timecreated', [ 'cohort:timecreated_operator' => date::DATE_RANGE, 'cohort:timecreated_from' => 1622502000, ], true], 'Filter time created (no match)' => ['cohort:timecreated', [ 'cohort:timecreated_operator' => date::DATE_RANGE, 'cohort:timecreated_to' => 1622502000, ], false], 'Filter description' => ['cohort:description', [ 'cohort:description_operator' => text::CONTAINS, 'cohort:description_value' => 'legends', ], true], 'Filter description (no match)' => ['cohort:description', [ 'cohort:description_operator' => text::IS_EMPTY, ], false], 'Filter theme' => ['cohort:theme', [ 'cohort:theme_operator' => select::EQUAL_TO, 'cohort:theme_value' => 'boost', ], true], 'Filter theme (no match)' => ['cohort:theme', [ 'cohort:theme_operator' => select::EQUAL_TO, 'cohort:theme_value' => 'classic', ], false], 'Filter visible' => ['cohort:visible', [ 'cohort:visible_operator' => boolean_select::CHECKED, ], true], 'Filter visible (no match)' => ['cohort:visible', [ 'cohort:visible_operator' => boolean_select::NOT_CHECKED, ], false], // Cohort member. 'Filter time added' => ['cohort_member:timeadded', [ 'cohort_member:timeadded_operator' => date::DATE_RANGE, 'cohort_member:timeadded_from' => 1622502000, ], true], 'Filter time added (no match)' => ['cohort_member:timeadded', [ 'cohort_member:timeadded_operator' => date::DATE_RANGE, 'cohort_member:timeadded_to' => 1622502000, ], false], // User. 'Filter user' => ['user:username', [ 'user:username_operator' => text::IS_EQUAL_TO, 'user:username_value' => 'lionel', ], true], 'Filter user (no match)' => ['user:username', [ 'user:username_operator' => text::IS_EQUAL_TO, 'user:username_value' => 'rick', ], false], ]; } /** * Test datasource filters * * @param string $filtername * @param array $filtervalues * @param bool $expectmatch * * @dataProvider datasource_filters_provider */ public function test_datasource_filters(string $filtername, array $filtervalues, bool $expectmatch): void { $this->resetAfterTest(); set_config('allowcohortthemes', true); // Test subject. $cohort = $this->getDataGenerator()->create_cohort([ 'name' => 'Legends', 'idnumber' => 'C101', 'description' => 'Cohort for the legends', 'theme' => 'boost', ]); $user = $this->getDataGenerator()->create_user(['username' => 'lionel']); cohort_add_member($cohort->id, $user->id); /** @var core_reportbuilder_generator $generator */ $generator = $this->getDataGenerator()->get_plugin_generator('core_reportbuilder'); // Create report containing single column, and given filter. $report = $generator->create_report(['name' => 'Cohorts', 'source' => cohorts::class, 'default' => 0]); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'cohort:name']); // Add filter, set it's values. $generator->create_filter(['reportid' => $report->get('id'), 'uniqueidentifier' => $filtername]); $content = $this->get_custom_report_content($report->get('id'), 0, $filtervalues); if ($expectmatch) { $this->assertCount(1, $content); $this->assertEquals('Legends', reset($content[0])); } else { $this->assertEmpty($content); } } /** * Stress test datasource * * In order to execute this test PHPUNIT_LONGTEST should be defined as true in phpunit.xml or directly in config.php */ public function test_stress_datasource(): void { if (!PHPUNIT_LONGTEST) { $this->markTestSkipped('PHPUNIT_LONGTEST is not defined'); } $this->resetAfterTest(); $cohort = $this->getDataGenerator()->create_cohort(); $user = $this->getDataGenerator()->create_user(); cohort_add_member($cohort->id, $user->id); $this->datasource_stress_test_columns(cohorts::class); $this->datasource_stress_test_columns_aggregation(cohorts::class); $this->datasource_stress_test_conditions(cohorts::class, 'cohort:name'); } } PK\9;;;audience/cohortmember_test.phpnu[. declare(strict_types=1); namespace core_cohort\reportbuilder\audience; use advanced_testcase; use context; use core_reportbuilder_generator; use core_user\reportbuilder\datasource\users; /** * Unit tests for cohort member report audience type * * @package core_reportbuilder * @covers \core_cohort\reportbuilder\audience\cohortmember * @copyright 2021 David Matamoros * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class cohortmember_test extends advanced_testcase { /** * Test that this audience type description is generated correctly */ public function test_get_description(): void { $this->resetAfterTest(); /** @var core_reportbuilder_generator $generator */ $generator = $this->getDataGenerator()->get_plugin_generator('core_reportbuilder'); $report = $generator->create_report([ 'name' => 'My report', 'source' => users::class, 'default' => false, ]); $cohort = self::getDataGenerator()->create_cohort(); $audience = cohortmember::create($report->get('id'), ['cohorts' => [$cohort->id]]); $this->assertEquals($cohort->name, $audience->get_description()); } /** * Test if user can add this audience type to the report */ public function test_user_can_add(): void { $this->resetAfterTest(); /** @var core_reportbuilder_generator $generator */ $generator = $this->getDataGenerator()->get_plugin_generator('core_reportbuilder'); $report = $generator->create_report([ 'name' => 'My report', 'source' => users::class, 'default' => false, ]); // Admin user. self::setAdminUser(); $cohort = self::getDataGenerator()->create_cohort(); $context = context::instance_by_id($cohort->contextid); $audience = cohortmember::create($report->get('id'), ['cohorts' => [$cohort->id]]); $this->assertTrue($audience->user_can_add()); // Non-priveleged user. $user = self::getDataGenerator()->create_user(); self::setUser($user); $this->assertFalse($audience->user_can_add()); // Grant priveleges to user (moodle/cohort:view). $roleid = create_role('Dummy role', 'dummyrole', 'dummy role description'); assign_capability('moodle/cohort:view', CAP_ALLOW, $roleid, $context->id); role_assign($roleid, $user->id, $context->id); $this->assertTrue($audience->user_can_add()); } /** * Test if user can edit this audience type */ public function test_user_can_edit(): void { $this->resetAfterTest(); /** @var core_reportbuilder_generator $generator */ $generator = $this->getDataGenerator()->get_plugin_generator('core_reportbuilder'); $report = $generator->create_report([ 'name' => 'My report', 'source' => users::class, 'default' => false, ]); $cohort = self::getDataGenerator()->create_cohort(); $context = context::instance_by_id($cohort->contextid); $audience = cohortmember::create($report->get('id'), ['cohorts' => [$cohort->id]]); // Admin user. self::setAdminUser(); $this->assertTrue($audience->user_can_edit()); // Non-priveleged user. $user = self::getDataGenerator()->create_user(); self::setUser($user); $this->assertFalse($audience->user_can_edit()); // Grant priveleges to user (moodle/cohort:view). $roleid = create_role('Dummy role', 'dummyrole', 'dummy role description'); assign_capability('moodle/cohort:view', CAP_ALLOW, $roleid, $context->id); role_assign($roleid, $user->id, $context->id); $this->assertTrue($audience->user_can_edit()); } /** * Test that sql generated is correct */ public function test_get_sql(): void { global $DB; $this->resetAfterTest(); /** @var core_reportbuilder_generator $generator */ $generator = $this->getDataGenerator()->get_plugin_generator('core_reportbuilder'); $report = $generator->create_report([ 'name' => 'My report', 'source' => users::class, 'default' => false, ]); $cohort = self::getDataGenerator()->create_cohort(); $user1 = $this->getDataGenerator()->create_user(); $user2 = $this->getDataGenerator()->create_user(); $user3 = $this->getDataGenerator()->create_user(); // Add user1 into cohort. cohort_add_member($cohort->id, $user1->id); // Add user3 into cohort. cohort_add_member($cohort->id, $user3->id); $audience = cohortmember::create($report->get('id'), ['cohorts' => [$cohort->id]]); [$join, $where, $params] = $audience->get_sql('u'); $query = 'SELECT u.* FROM {user} u ' . $join . ' WHERE ' . $where; $records = $DB->get_records_sql($query, $params); $this->assertEqualsCanonicalizing([$user1->id, $user3->id], array_column($records, 'id')); } /** * Test if this audience type is available to use */ public function test_is_available(): void { $this->resetAfterTest(); /** @var core_reportbuilder_generator $generator */ $generator = $this->getDataGenerator()->get_plugin_generator('core_reportbuilder'); self::setAdminUser(); // Check with no cohorts available in the system. $report = $generator->create_report([ 'name' => 'My report', 'source' => users::class, 'default' => false, ]); $audience = cohortmember::create($report->get('id'), ['cohorts' => []]); $this->assertFalse($audience->is_available()); // Check with cohorts available in the system. self::getDataGenerator()->create_cohort(); $report = $generator->create_report([ 'name' => 'My report', 'source' => users::class, 'default' => false, ]); $audience2 = cohortmember::create($report->get('id'), ['cohorts' => []]); $this->assertTrue($audience2->is_available()); } } PK-\]00datasource/categories_test.phpnu[. declare(strict_types=1); namespace core_course\reportbuilder\datasource; use core_course_category; use core_reportbuilder_generator; use core_reportbuilder\local\filters\{category, select, text}; use core_reportbuilder\tests\core_reportbuilder_testcase; /** * Unit tests for course categories datasource * * @package core_course * @covers \core_course\reportbuilder\datasource\categories * @copyright 2023 Paul Holden * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class categories_test extends core_reportbuilder_testcase { /** * Test default datasource */ public function test_datasource_default(): void { $this->resetAfterTest(); $category = $this->getDataGenerator()->create_category(['name' => 'Zoo', 'idnumber' => 'Z01']); $course = $this->getDataGenerator()->create_course(['category' => $category->id]); /** @var core_reportbuilder_generator $generator */ $generator = $this->getDataGenerator()->get_plugin_generator('core_reportbuilder'); $report = $generator->create_report(['name' => 'My report', 'source' => categories::class, 'default' => 1]); $content = $this->get_custom_report_content($report->get('id')); $this->assertCount(2, $content); // Default columns are name, idnumber, coursecount. Sorted by name ascending. $this->assertEquals([ [get_string('defaultcategoryname'), '', 0], [$category->get_formatted_name(), $category->idnumber, 1], ], array_map('array_values', $content)); } /** * Test datasource columns that aren't added by default */ public function test_datasource_non_default_columns(): void { global $DB; $this->resetAfterTest(); set_config('allowcategorythemes', true); $category = $this->getDataGenerator()->create_category([ 'name' => 'Zoo', 'idnumber' => 'Z01', 'description' => 'Animals', 'theme' => 'boost', ]); $course = $this->getDataGenerator()->create_course(['category' => $category->id, 'fullname' => 'Zebra']); // Add a cohort. $cohort = $this->getDataGenerator()->create_cohort(['contextid' => $category->get_context()->id, 'name' => 'My cohort']); // Assign a role. $managerrole = $DB->get_field('role', 'id', ['shortname' => 'manager']); $user = $this->getDataGenerator()->create_user(); role_assign($managerrole, $user->id, $category->get_context()->id); /** @var core_reportbuilder_generator $generator */ $generator = $this->getDataGenerator()->get_plugin_generator('core_reportbuilder'); $report = $generator->create_report(['name' => 'My report', 'source' => categories::class, 'default' => 0]); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'course_category:namewithlink', 'sortenabled' => 1]); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'course_category:path']); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'course_category:description']); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'course_category:theme']); // Add column from each of our entities. $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'course:fullname']); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'cohort:name']); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'role:name']); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'user:fullname']); $content = $this->get_custom_report_content($report->get('id')); $this->assertCount(2, $content); [$namewithlink, $path, $description, $theme, $coursename, $cohortname, $rolename, $userfullname] = array_values($content[0]); $this->assertStringContainsString(get_string('defaultcategoryname'), $namewithlink); $this->assertEquals(get_string('defaultcategoryname'), $path); $this->assertEmpty($description); $this->assertEmpty($theme); $this->assertEmpty($coursename); $this->assertEmpty($cohortname); $this->assertEmpty($rolename); $this->assertEmpty($userfullname); [$namewithlink, $path, $description, $theme, $coursename, $cohortname, $rolename, $userfullname] = array_values($content[1]); $this->assertStringContainsString($category->get_formatted_name(), $namewithlink); $this->assertEquals($category->get_nested_name(false), $path); $this->assertEquals(format_text($category->description, $category->descriptionformat), $description); $this->assertEquals('Boost', $theme); $this->assertEquals($course->fullname, $coursename); $this->assertEquals($cohort->name, $cohortname); $this->assertEquals('Manager', $rolename); $this->assertEquals(fullname($user), $userfullname); } /** * Data provider for {@see test_datasource_filters} * * @return array[] */ public static function datasource_filters_provider(): array { global $DB; return [ // Category. 'Filter category' => ['course_category:name', [ 'course_category:name_operator' => category::NOT_EQUAL_TO, 'course_category:name_value' => -1, ], true], 'Filter category (no match)' => ['course_category:name', [ 'course_category:name_operator' => category::EQUAL_TO, 'course_category:name_value' => -1, ], false], 'Filter category name' => ['course_category:text', [ 'course_category:text_operator' => text::IS_EQUAL_TO, 'course_category:text_value' => 'Zoo', ], true], 'Filter category name (no match)' => ['course_category:text', [ 'course_category:text_operator' => text::IS_EQUAL_TO, 'course_category:text_value' => 'Plants', ], false], 'Filter category idnumber' => ['course_category:idnumber', [ 'course_category:idnumber_operator' => text::IS_EQUAL_TO, 'course_category:idnumber_value' => 'Z01', ], true], 'Filter category idnumber (no match)' => ['course_category:idnumber', [ 'course_category:idnumber_operator' => text::IS_EQUAL_TO, 'course_category:idnumber_value' => 'P01', ], false], 'Filter category theme' => ['course_category:theme', [ 'course_category:theme_operator' => select::EQUAL_TO, 'course_category:theme_value' => 'boost', ], true], 'Filter category theme (no match)' => ['course_category:theme', [ 'course_category:theme_operator' => select::EQUAL_TO, 'course_category:theme_value' => 'classic', ], false], // Course. 'Filter course fullname' => ['course:fullname', [ 'course:fullname_operator' => text::IS_EQUAL_TO, 'course:fullname_value' => 'Zebra', ], true], 'Filter course fullname (no match)' => ['course:fullname', [ 'course:fullname_operator' => text::IS_EQUAL_TO, 'course:fullname_value' => 'Python', ], false], // Cohort. 'Filter cohort name' => ['cohort:name', [ 'cohort:name_operator' => text::IS_EQUAL_TO, 'cohort:name_value' => 'My cohort', ], true], 'Filter cohort name (no match)' => ['cohort:name', [ 'cohort:name_operator' => text::IS_EQUAL_TO, 'cohort:name_value' => 'Not my cohort', ], false], // Role. 'Filter role' => ['role:name', [ 'role:name_operator' => select::EQUAL_TO, 'role:name_value' => $DB->get_field('role', 'id', ['shortname' => 'manager']), ], true], 'Filter role (no match)' => ['role:name', [ 'role:name_operator' => select::EQUAL_TO, 'role:name_value' => $DB->get_field('role', 'id', ['shortname' => 'teacher']), ], false], // User. 'Filter user firstname' => ['user:firstname', [ 'user:firstname_operator' => text::IS_EQUAL_TO, 'user:firstname_value' => 'Zoe', ], true], 'Filter user firstname (no match)' => ['user:firstname', [ 'user:firstname_operator' => text::IS_EQUAL_TO, 'user:firstname_value' => 'Pedro', ], false], ]; } /** * Test datasource filters * * @param string $filtername * @param array $filtervalues * @param bool $expectmatch * * @dataProvider datasource_filters_provider */ public function test_datasource_filters(string $filtername, array $filtervalues, bool $expectmatch): void { global $DB; $this->resetAfterTest(); set_config('allowcategorythemes', true); // Get the default category, modify it so we can filter each value. ($category = core_course_category::get_default())->update(['name' => 'Zoo', 'idnumber' => 'Z01', 'theme' => 'boost']); $course = $this->getDataGenerator()->create_course(['category' => $category->id, 'fullname' => 'Zebra']); // Add a cohort. $cohort = $this->getDataGenerator()->create_cohort(['contextid' => $category->get_context()->id, 'name' => 'My cohort']); // Assign a role. $managerrole = $DB->get_field('role', 'id', ['shortname' => 'manager']); $user = $this->getDataGenerator()->create_user(['firstname' => 'Zoe']); role_assign($managerrole, $user->id, $category->get_context()->id); /** @var core_reportbuilder_generator $generator */ $generator = $this->getDataGenerator()->get_plugin_generator('core_reportbuilder'); // Create report containing single idnumber column, and given filter. $report = $generator->create_report(['name' => 'My report', 'source' => categories::class, 'default' => 0]); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'course_category:idnumber']); // Add filter, set it's values. $generator->create_filter(['reportid' => $report->get('id'), 'uniqueidentifier' => $filtername]); $content = $this->get_custom_report_content($report->get('id'), 0, $filtervalues); if ($expectmatch) { $this->assertCount(1, $content); $this->assertEquals($category->idnumber, reset($content[0])); } else { $this->assertEmpty($content); } } /** * Stress test datasource * * In order to execute this test PHPUNIT_LONGTEST should be defined as true in phpunit.xml or directly in config.php */ public function test_stress_datasource(): void { if (!PHPUNIT_LONGTEST) { $this->markTestSkipped('PHPUNIT_LONGTEST is not defined'); } $this->resetAfterTest(); $category = $this->getDataGenerator()->create_category(['name' => 'My category']); $this->datasource_stress_test_columns(categories::class); $this->datasource_stress_test_columns_aggregation(categories::class); $this->datasource_stress_test_conditions(categories::class, 'course_category:idnumber'); } } PK-\(=PPdatasource/courses_test.phpnu[. declare(strict_types=1); namespace core_course\reportbuilder\datasource; use core\context\course; use core_reportbuilder_generator; use core_reportbuilder\local\filters\{boolean_select, date, select, tags, text}; use core_reportbuilder\tests\core_reportbuilder_testcase; /** * Unit tests for courses datasources * * @package core_course * @covers \core_course\reportbuilder\datasource\courses * @copyright 2021 Paul Holden * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class courses_test extends core_reportbuilder_testcase { /** * Test default datasource */ public function test_datasource_default(): void { $this->resetAfterTest(); // Test subject. $category = $this->getDataGenerator()->create_category(['name' => 'My cats']); $courseone = $this->getDataGenerator()->create_course([ 'category' => $category->id, 'fullname' => 'Feline fine', 'shortname' => 'C102', 'idnumber' => 'CAT102' ]); $coursetwo = $this->getDataGenerator()->create_course([ 'category' => $category->id, 'fullname' => 'All about cats', 'shortname' => 'C101', 'idnumber' => 'CAT101' ]); /** @var core_reportbuilder_generator $generator */ $generator = $this->getDataGenerator()->get_plugin_generator('core_reportbuilder'); $report = $generator->create_report(['name' => 'Courses', 'source' => courses::class, 'default' => 1]); $content = $this->get_custom_report_content($report->get('id')); // Default columns are category, shortname, fullname, idnumber. Sorted by category, shortname, fullname. $this->assertEquals([ [$category->name, $coursetwo->shortname, $coursetwo->fullname, $coursetwo->idnumber], [$category->name, $courseone->shortname, $courseone->fullname, $courseone->idnumber], ], array_map('array_values', $content)); } /** * Test datasource columns that aren't added by default */ public function test_datasource_non_default_columns(): void { $this->resetAfterTest(); $category = $this->getDataGenerator()->create_category([ 'name' => 'Animals', 'idnumber' => 'CAT101', 'description' => 'Category description', ]); $course = $this->getDataGenerator()->create_course([ 'category' => $category->id, 'fullname' => 'Cats', 'summary' => 'Course description', 'lang' => 'en', 'theme' => 'boost', 'tags' => ['Horses'], ]); // Add a course image. get_file_storage()->create_file_from_string([ 'contextid' => course::instance($course->id)->id, 'component' => 'course', 'filearea' => 'overviewfiles', 'itemid' => 0, 'filepath' => '/', 'filename' => 'HelloWorld.jpg', ], 'HelloWorld'); /** @var core_reportbuilder_generator $generator */ $generator = $this->getDataGenerator()->get_plugin_generator('core_reportbuilder'); $report = $generator->create_report(['name' => 'Courses', 'source' => courses::class, 'default' => 0]); // Course. $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'course:coursefullnamewithlink']); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'course:courseshortnamewithlink']); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'course:courseidnumberewithlink']); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'course:summary']); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'course:format']); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'course:startdate']); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'course:enddate']); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'course:visible']); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'course:groupmode']); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'course:groupmodeforce']); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'course:lang']); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'course:calendartype']); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'course:theme']); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'course:enablecompletion']); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'course:downloadcontent']); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'course:timecreated']); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'course:timemodified']); // Tags. $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'tag:name']); // File entity. $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'file:name']); $content = $this->get_custom_report_content($report->get('id')); $this->assertCount(1, $content); [ $coursenamewithlink, $courseshortnamewithlink, $courseidnumberwithlink, $coursesummary, $courseformat, $coursestartdate, $courseenddate, $coursevisible, $coursegroupmode, $coursegroupmodeforce, $courselang, $coursecalendar, $coursetheme, $coursecompletion, $coursedownload, $coursetimecreated, $coursetimemodified, $tagname, $filename, ] = array_values($content[0]); // Course. $this->assertStringContainsString($course->fullname, $coursenamewithlink); $this->assertStringContainsString($course->shortname, $courseshortnamewithlink); $this->assertStringContainsString($course->idnumber, $courseidnumberwithlink); $this->assertEquals(format_text($course->summary, $course->summaryformat), $coursesummary); $this->assertEquals('Custom sections', $courseformat); $this->assertEquals(userdate($course->startdate), $coursestartdate); $this->assertEmpty($courseenddate); $this->assertEquals('Yes', $coursevisible); $this->assertEquals('No groups', $coursegroupmode); $this->assertEquals('No', $coursegroupmodeforce); $this->assertEquals('English ‎(en)‎', $courselang); $this->assertEquals('Do not force', $coursecalendar); $this->assertEquals('Boost', $coursetheme); $this->assertEquals('No', $coursecompletion); $this->assertEmpty($coursedownload); $this->assertEquals(userdate($course->timecreated), $coursetimecreated); $this->assertEquals(userdate($course->timemodified), $coursetimemodified); // Tags. $this->assertEquals('Horses', $tagname); // File. $this->assertEquals('HelloWorld.jpg', $filename); } /** * Tests courses datasource using multilang filters */ public function test_courses_datasource_multilang_filters(): void { $this->resetAfterTest(); filter_set_global_state('multilang', TEXTFILTER_ON); filter_set_applies_to_strings('multilang', true); // Test subject. $category = $this->getDataGenerator()->create_category([ 'name' => 'Cat (en)Cat (es)', ]); $course = $this->getDataGenerator()->create_course([ 'category' => $category->id, 'fullname' => 'Crs (en)Crs (es)', ]); /** @var core_reportbuilder_generator $generator */ $generator = $this->getDataGenerator()->get_plugin_generator('core_reportbuilder'); // Create a report containing columns that support multilang content. $report = $generator->create_report(['name' => 'Courses', 'source' => courses::class, 'default' => 0]); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'course_category:name']); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'course:fullname']); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'course:coursefullnamewithlink']); $content = $this->get_custom_report_content($report->get('id')); $this->assertCount(1, $content); $contentrow = array_values(reset($content)); $this->assertEquals([ 'Cat (en)', 'Crs (en)', 'Crs (en)', ], $contentrow); } /** * Data provider for {@see test_datasource_filters} * * @return array[] */ public static function datasource_filters_provider(): array { return [ // Category. 'Filter category name' => ['course_category:text', [ 'course_category:text_operator' => text::IS_EQUAL_TO, 'course_category:text_value' => 'Animals', ], true], 'Filter category name (no match)' => ['course_category:text', [ 'course_category:text_operator' => text::IS_EQUAL_TO, 'course_category:text_value' => 'Fruit', ], false], // Course. 'Filter course' => ['course:courseselector', [ 'course:courseselector_values' => [-1], ], false], 'Filter course fullname' => ['course:fullname', [ 'course:fullname_operator' => text::IS_EQUAL_TO, 'course:fullname_value' => 'Equine', ], true], 'Filter course fullname (no match)' => ['course:fullname', [ 'course:fullname_operator' => text::IS_EQUAL_TO, 'course:fullname_value' => 'Foxes', ], false], 'Filter course shortname' => ['course:shortname', [ 'course:shortname_operator' => text::IS_EQUAL_TO, 'course:shortname_value' => 'EQ101', ], true], 'Filter course shortname (no match)' => ['course:shortname', [ 'course:shortname_operator' => text::IS_EQUAL_TO, 'course:shortname_value' => 'FX101', ], false], 'Filter course idnumber' => ['course:idnumber', [ 'course:idnumber_operator' => text::IS_EQUAL_TO, 'course:idnumber_value' => 'E-101AB', ], true], 'Filter course idnumber (no match)' => ['course:idnumber', [ 'course:idnumber_operator' => text::IS_EQUAL_TO, 'course:idnumber_value' => 'F-101XT', ], false], 'Filter course summary' => ['course:summary', [ 'course:summary_operator' => text::CONTAINS, 'course:summary_value' => 'Lorem ipsum', ], true], 'Filter course summary (no match)' => ['course:summary', [ 'course:summary_operator' => text::IS_EQUAL_TO, 'course:summary_value' => 'Fiat', ], false], 'Filter course format' => ['course:format', [ 'course:format_operator' => select::EQUAL_TO, 'course:format_value' => 'topics', ], true], 'Filter course format (no match)' => ['course:format', [ 'course:format_operator' => select::EQUAL_TO, 'course:format_value' => 'weeks', ], false], 'Filter course startdate' => ['course:startdate', [ 'course:startdate_operator' => date::DATE_RANGE, 'course:startdate_from' => 1622502000, ], true], 'Filter course startdate (no match)' => ['course:startdate', [ 'course:startdate_operator' => date::DATE_RANGE, 'course:startdate_to' => 1622502000, ], false], 'Filter course enddate' => ['course:enddate', [ 'course:enddate_operator' => date::DATE_EMPTY, ], true], 'Filter course enddate (no match)' => ['course:enddate', [ 'course:enddate_operator' => date::DATE_NOT_EMPTY, ], false], 'Filter course visible' => ['course:visible', [ 'course:visible_operator' => boolean_select::CHECKED, ], true], 'Filter course visible (no match)' => ['course:visible', [ 'course:visible_operator' => boolean_select::NOT_CHECKED, ], false], 'Filter course groupmode' => ['course:groupmode', [ 'course:groupmode_operator' => select::EQUAL_TO, 'course:groupmode_value' => 0, // No groups. ], true], 'Filter course groupmode (no match)' => ['course:groupmode', [ 'course:groupmode_operator' => select::EQUAL_TO, 'course:groupmode_value' => 1, // Separate groups. ], false], 'Filter course groupmodeforce' => ['course:groupmodeforce', [ 'course:groupmodeforce_operator' => boolean_select::NOT_CHECKED, ], true], 'Filter course groupmodeforce (no match)' => ['course:groupmodeforce', [ 'course:groupmodeforce_operator' => boolean_select::CHECKED, ], false], 'Filter course lang' => ['course:lang', [ 'course:lang_operator' => select::EQUAL_TO, 'course:lang_value' => 'en', ], true], 'Filter course lang (no match)' => ['course:lang', [ 'course:lang_operator' => select::EQUAL_TO, 'course:lang_value' => '', ], false], 'Filter course calendartype' => ['course:calendartype', [ 'course:calendartype_operator' => select::EQUAL_TO, 'course:calendartype_value' => 'gregorian', ], true], 'Filter course calendartype (no match)' => ['course:calendartype', [ 'course:calendartype_operator' => select::EQUAL_TO, 'course:calendartype_value' => '', ], false], 'Filter course theme' => ['course:theme', [ 'course:theme_operator' => select::EQUAL_TO, 'course:theme_value' => 'boost', ], true], 'Filter course theme (no match)' => ['course:theme', [ 'course:theme_operator' => select::EQUAL_TO, 'course:theme_value' => 'classic', ], false], 'Filter course enablecompletion' => ['course:enablecompletion', [ 'course:enablecompletion_operator' => boolean_select::NOT_CHECKED, ], true], 'Filter course enablecompletion (no match)' => ['course:enablecompletion', [ 'course:enablecompletion_operator' => boolean_select::CHECKED, ], false], 'Filter course downloadcontent' => ['course:downloadcontent', [ 'course:downloadcontent_operator' => boolean_select::CHECKED, ], true], 'Filter course downloadcontent (no match)' => ['course:downloadcontent', [ 'course:downloadcontent_operator' => boolean_select::NOT_CHECKED, ], false], 'Filter course timecreated' => ['course:timecreated', [ 'course:timecreated_operator' => date::DATE_RANGE, 'course:timecreated_from' => 1622502000, ], true], 'Filter course timecreated (no match)' => ['course:timecreated', [ 'course:timecreated_operator' => date::DATE_RANGE, 'course:timecreated_to' => 1622502000, ], false], 'Filter course timemodified' => ['course:timemodified', [ 'course:timemodified_operator' => date::DATE_RANGE, 'course:timemodified_from' => 1622502000, ], true], 'Filter course timemodified (no match)' => ['course:timemodified', [ 'course:timemodified_operator' => date::DATE_RANGE, 'course:timemodified_to' => 1622502000, ], false], // Tags. 'Filter tag name' => ['tag:name', [ 'tag:name_operator' => tags::EQUAL_TO, 'tag:name_value' => [-1], ], false], 'Filter tag name not empty' => ['tag:name', [ 'tag:name_operator' => tags::NOT_EMPTY, ], true], // File. 'Filter file name empty' => ['file:name', [ 'file:name_operator' => text::IS_EMPTY, ], true], ]; } /** * Test datasource filters * * @param string $filtername * @param array $filtervalues * @param bool $expectmatch * * @dataProvider datasource_filters_provider */ public function test_datasource_filters(string $filtername, array $filtervalues, bool $expectmatch): void { $this->resetAfterTest(); $category = $this->getDataGenerator()->create_category(['name' => 'Animals']); $course = $this->getDataGenerator()->create_course([ 'category' => $category->id, 'fullname' => 'Equine', 'shortname' => 'EQ101', 'idnumber' => 'E-101AB', 'lang' => 'en', 'calendartype' => 'gregorian', 'theme' => 'boost', 'downloadcontent' => 1, 'tags' => ['Horses'], ]); /** @var core_reportbuilder_generator $generator */ $generator = $this->getDataGenerator()->get_plugin_generator('core_reportbuilder'); // Create report containing single column, and given filter. $report = $generator->create_report(['name' => 'Courses', 'source' => courses::class, 'default' => 0]); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'course:fullname']); // Add filter, set it's values. $generator->create_filter(['reportid' => $report->get('id'), 'uniqueidentifier' => $filtername]); $content = $this->get_custom_report_content($report->get('id'), 0, $filtervalues); if ($expectmatch) { $this->assertCount(1, $content); $this->assertEquals($course->fullname, reset($content[0])); } else { $this->assertEmpty($content); } } /** * Stress test datasource * * In order to execute this test PHPUNIT_LONGTEST should be defined as true in phpunit.xml or directly in config.php */ public function test_stress_datasource(): void { if (!PHPUNIT_LONGTEST) { $this->markTestSkipped('PHPUNIT_LONGTEST is not defined'); } $this->resetAfterTest(); $category = $this->getDataGenerator()->create_category(); $course = $this->getDataGenerator()->create_course(['category' => $category->id]); $this->datasource_stress_test_columns(courses::class); $this->datasource_stress_test_columns_aggregation(courses::class); $this->datasource_stress_test_conditions(courses::class, 'course:idnumber'); } } PK-\`"__ datasource/participants_test.phpnu[. declare(strict_types=1); namespace core_course\reportbuilder\datasource; use completion_completion; use completion_criteria_self; use core_reportbuilder_generator; use core_reportbuilder\local\filters\{boolean_select, date, duration, select, text}; use core_reportbuilder\tests\core_reportbuilder_testcase; use core\user; use grade_item; /** * Course participants datasource tests * * @package core_course * @covers \core_course\reportbuilder\datasource\participants * @copyright 2022 David Matamoros * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class participants_test extends core_reportbuilder_testcase { /** * Load required test libraries */ public static function setUpBeforeClass(): void { global $CFG; require_once("{$CFG->libdir}/gradelib.php"); require_once("{$CFG->dirroot}/completion/criteria/completion_criteria_self.php"); parent::setUpBeforeClass(); } /** * Test default datasource */ public function test_datasource_default(): void { global $DB; $this->resetAfterTest(); // Course one, two manually enrolled users. $courseone = $this->getDataGenerator()->create_course(['fullname' => 'Zebras']); $userone = $this->getDataGenerator()->create_and_enrol($courseone, 'student', ['firstname' => 'Zoe']); $usertwo = $this->getDataGenerator()->create_and_enrol($courseone, 'student', ['firstname' => 'Amy']); // Course two, two self enrolled users (one inactive). $coursetwo = $this->getDataGenerator()->create_course(['fullname' => 'Aardvarks']); $enrol = $DB->get_record('enrol', ['courseid' => $coursetwo->id, 'enrol' => 'self']); enrol_get_plugin($enrol->enrol)->update_status($enrol, ENROL_INSTANCE_ENABLED); $this->getDataGenerator()->enrol_user($userone->id, $coursetwo->id, null, 'self'); $this->getDataGenerator()->enrol_user($usertwo->id, $coursetwo->id, null, 'self', 0, 0, ENROL_USER_SUSPENDED); /** @var core_reportbuilder_generator $generator */ $generator = $this->getDataGenerator()->get_plugin_generator('core_reportbuilder'); $report = $generator->create_report(['name' => 'Participants', 'source' => participants::class, 'default' => 1]); $content = $this->get_custom_report_content($report->get('id')); // Default columns are course, user, method. Sorted by each. $courseoneurl = course_get_url($courseone); $coursetwourl = course_get_url($coursetwo); $useroneurl = user::get_profile_url($userone); $usertwourl = user::get_profile_url($usertwo); $this->assertEquals([ ["{$coursetwo->fullname}", "" . fullname($userone) . "", 'Self enrolment (Student)'], ["{$courseone->fullname}", "" . fullname($usertwo) . "", 'Manual enrolments'], ["{$courseone->fullname}", "" . fullname($userone) . "", 'Manual enrolments'], ], array_map('array_values', $content)); } /** * Test datasource columns that aren't added by default */ public function test_datasource_non_default_columns(): void { global $DB; $this->resetAfterTest(); $timestart = time() - DAYSECS; $timeend = $timestart + 3 * DAYSECS; $timecompleted = $timestart + 2 * DAYSECS; $timelastaccess = time() + 4 * DAYSECS; $category = $this->getDataGenerator()->create_category(['name' => 'Music']); $course = $this->getDataGenerator()->create_course([ 'category' => $category->id, 'fullname' => 'All about Lionel at the work place', 'enablecompletion' => true, 'startdate' => $timestart, 'enddate' => $timeend, ]); $user1 = self::getDataGenerator()->create_user(); $this->getDataGenerator()->enrol_user($user1->id, $course->id, 'student', 'manual', $timestart, $timeend, ENROL_USER_ACTIVE); // Add them to a group. $group = self::getDataGenerator()->create_group(['courseid' => $course->id]); self::getDataGenerator()->create_group_member(['groupid' => $group->id, 'userid' => $user1->id]); // Create self completion, mark as complete for the user. $criteriaconfig = (object) ['id' => $course->id, 'criteria_self' => true]; (new completion_criteria_self())->update_config($criteriaconfig); $ccompletion = new completion_completion(['course' => $course->id, 'userid' => $user1->id]); $ccompletion->mark_enrolled($timestart); $ccompletion->mark_complete($timecompleted); // Update final grade for the user. $courseitem = grade_item::fetch_course_item($course->id); $courseitem->update_final_grade($user1->id, 42.5); // Add some cohort data. $cohort = $this->getDataGenerator()->create_cohort(['name' => 'My cohort']); cohort_add_member($cohort->id, $user1->id); // Set some last access value for the user in the course. $DB->insert_record('user_lastaccess', ['userid' => $user1->id, 'courseid' => $course->id, 'timeaccess' => $timelastaccess]); /** @var core_reportbuilder_generator $generator */ $generator = $this->getDataGenerator()->get_plugin_generator('core_reportbuilder'); $report = $generator->create_report(['name' => 'Participants', 'source' => participants::class, 'default' => false]); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'course:fullname']); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'course_category:name']); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'user:fullname']); // Enrol entity (report ordering by enrolment name). $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'enrol:name', 'sortenabled' => 1]); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'enrol:plugin']); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'enrol:enabled']); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'enrol:period']); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'enrol:startdate']); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'enrol:enddate']); // Role entity. $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'role:name']); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'role:shortname']); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'role:description']); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'group:name']); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'cohort:name']); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'completion:criteria']); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'completion:completed']); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'access:timeaccess']); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'completion:progresspercent']); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'completion:timeenrolled']); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'completion:timestarted']); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'completion:timecompleted']); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'completion:reaggregate']); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'completion:dayscourse']); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'completion:daysuntilcompletion']); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'completion:grade']); // It should get 3 records (manual enrolment, self and guest). $content = $this->get_custom_report_content($report->get('id')); $this->assertCount(3, $content); // Filter by Manual enrolment method. $generator->create_filter(['reportid' => $report->get('id'), 'uniqueidentifier' => 'enrol:plugin']); $content = $this->get_custom_report_content($report->get('id'), 30, [ 'enrol:plugin_operator' => select::EQUAL_TO, 'enrol:plugin_value' => 'manual', ]); $this->assertCount(1, $content); $this->assertEquals([ 'All about Lionel at the work place', // Course name. 'Music', // Course category name. fullname($user1), // User fullname. 'Manual enrolments', // Enrolment method. 'Manual enrolments', // Enrolment plugin. 'Yes', // Enrolment enabled. '', // Enrolment period. '', // Enrolment start date. '', // Enrolment end date. 'Student', // Role name. 'student', // Role shortname. 'Students generally have fewer privileges within a course.', // Role description. $group->name, // Group name. $cohort->name, // Cohort name. "All criteria below are required
    \n
  • Self completion: Self completion
  • \n
", // Completion criteria. 'Yes', // Course completed. userdate($timelastaccess), // Time last access. '100.0%', // Progress percentage. userdate($timestart), // Time enrolled. '', // Time started. userdate($timecompleted), // Time completed. '', // Reagreggate. '2 days', // Days taking course. '2 days', // Days until completion. '42.50', // Grade. ], array_values($content[0])); } /** * Test creating participants report, with aggregated last access date (minimum and maximum) */ public function test_course_last_access_aggregation(): void { $this->resetAfterTest(); $course = $this->getDataGenerator()->create_course(); $userone = $this->getDataGenerator()->create_and_enrol($course); $useronelastaccess = $this->getDataGenerator()->create_user_course_lastaccess($userone, $course, 1622502000); $usertwo = $this->getDataGenerator()->create_and_enrol($course); $usertwolastaccess = $this->getDataGenerator()->create_user_course_lastaccess($usertwo, $course, 1622847600); /** @var core_reportbuilder_generator $generator */ $generator = $this->getDataGenerator()->get_plugin_generator('core_reportbuilder'); $report = $generator->create_report(['name' => 'Participants', 'source' => participants::class, 'default' => 0]); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'course:fullname']); $column = $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'access:timeaccess']); // Course aggregated with "Minimum" last access. $column->set('aggregation', 'min')->update(); $content = $this->get_custom_report_content($report->get('id')); $this->assertEquals([ [$course->fullname, userdate($useronelastaccess->timeaccess)], ], array_map('array_values', $content)); // Course aggregated with "Maximum" last access. $column->set('aggregation', 'max')->update(); $content = $this->get_custom_report_content($report->get('id')); $this->assertEquals([ [$course->fullname, userdate($usertwolastaccess->timeaccess)], ], array_map('array_values', $content)); } /** * Test creating participants report, with aggregated days taking course column */ public function test_completion_days_taking_course_aggregation(): void { $this->resetAfterTest(); $courseone = $this->getDataGenerator()->create_course(['fullname' => 'Course 1', 'startdate' => 1622502000]); $coursetwo = $this->getDataGenerator()->create_course(['fullname' => 'Course 2']); // User one completed the course in two days. $userone = $this->getDataGenerator()->create_and_enrol($courseone); $completion = new completion_completion(['course' => $courseone->id, 'userid' => $userone->id]); $completion->mark_complete(1622502000 + (2 * DAYSECS)); // User two completed the course in three days (lazy bum). $usertwo = $this->getDataGenerator()->create_and_enrol($courseone); $completion = new completion_completion(['course' => $courseone->id, 'userid' => $usertwo->id]); $completion->mark_complete(1622502000 + (3 * DAYSECS)); /** @var core_reportbuilder_generator $generator */ $generator = $this->getDataGenerator()->get_plugin_generator('core_reportbuilder'); $report = $generator->create_report(['name' => 'Participants', 'source' => participants::class, 'default' => 0]); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'course:fullname', 'sortenabled' => 1]); $generator->create_column([ 'reportid' => $report->get('id'), 'uniqueidentifier' => 'completion:dayscourse', 'aggregation' => 'avg', ]); $content = $this->get_custom_report_content($report->get('id')); $this->assertEquals([ [$courseone->fullname, '2 days 12 hours'], [$coursetwo->fullname, ''], ], array_map('array_values', $content)); } /** * Data provider for {@see test_datasource_filters} * * @return array */ public static function datasource_filters_provider(): array { global $DB; return [ [ 'enrolment:status', [ 'enrolment:status_operator' => select::EQUAL_TO, 'enrolment:status_value' => 1, ], ['Luna'], ], [ 'enrolment:timecreated', [ 'enrolment:timecreated_operator' => date::DATE_CURRENT, 'enrolment:timecreated_unit' => date::DATE_UNIT_DAY, ], ['Kira'], ], [ 'enrolment:timestarted', [ 'enrolment:timestarted_operator' => date::DATE_CURRENT, 'enrolment:timecreated_unit' => date::DATE_UNIT_DAY, ], ['Luna'], ], [ 'enrolment:timeended', [ 'enrolment:timeended_operator' => date::DATE_CURRENT, 'enrolment:timeended_unit' => date::DATE_UNIT_DAY, ], ['Luna'], ], [ 'enrol:enabled', [ 'completion:enabled_operator' => boolean_select::CHECKED, ], ['Lionel', 'Kira', 'Luna'], ], [ 'enrol:period', [ 'enrol:period_operator' => duration::DURATION_MAXIMUM, 'enrol:period_unit' => MINSECS, 'enrol:period_value' => 2, ], ['Lionel', 'Kira', 'Luna'], ], [ 'enrol:startdate', [ 'enrol:startdate_operator' => date::DATE_EMPTY, ], ['Lionel', 'Kira', 'Luna'], ], [ 'enrol:enddate', [ 'enrol:enddate_operator' => date::DATE_EMPTY, ], ['Lionel', 'Kira', 'Luna'], ], [ 'enrol:customname', [ 'enrol:customname_operator' => text::IS_EMPTY, ], ['Luna', 'Kira', 'Lionel'], ], [ 'enrol:customname', [ 'enrol:customname_operator' => text::IS_EQUAL_TO, 'enrol:customname_value' => 'All night long' ], [], ], [ 'role:name', [ 'role:name_operator' => select::EQUAL_TO, 'role:name_value' => $DB->get_field('role', 'id', ['shortname' => 'editingteacher']), ], ['Luna'], ], [ 'group:name', [ 'group:name_operator' => text::IS_EQUAL_TO, 'group:name_value' => 'My group', ], ['Lionel'], ], [ 'cohort:name', [ 'cohort:name_operator' => text::IS_EQUAL_TO, 'cohort:name_value' => 'My cohort', ], ['Kira'], ], [ 'completion:completed', [ 'completion:completed_operator' => boolean_select::CHECKED, ], ['Lionel'], ], [ 'completion:timecompleted', [ 'completion:timecompleted_operator' => date::DATE_NOT_EMPTY, ], ['Lionel'], ], [ 'completion:timeenrolled', [ 'completion:timeenrolled_operator' => date::DATE_NOT_EMPTY, ], ['Lionel'], ], [ 'completion:timestarted', [ 'completion:timestarted_operator' => date::DATE_NOT_EMPTY, ], ['Lionel'], ], [ 'completion:reaggregate', [ 'completion:reaggregate_operator' => date::DATE_NOT_EMPTY, ], ['Lionel'], ], ]; } /** * Test getting filter SQL * * @param string $filter * @param array $filtervalues * @param string[] $expected * * @dataProvider datasource_filters_provider */ public function test_datasource_filters(string $filter, array $filtervalues, array $expected): void { global $DB; $this->resetAfterTest(); $timestart = time() - DAYSECS; $timeend = $timestart + 3 * DAYSECS; $timecompleted = $timestart + 2 * DAYSECS; $timelastaccess = time() + 4 * DAYSECS; $category = $this->getDataGenerator()->create_category(['name' => 'Music']); $course = $this->getDataGenerator()->create_course([ 'category' => $category->id, 'fullname' => 'All about Lionel at the work place', 'enablecompletion' => true, 'startdate' => $timestart, 'enddate' => $timeend, ]); $user1 = self::getDataGenerator()->create_user(['firstname' => 'Lionel']); $user2 = self::getDataGenerator()->create_user(['firstname' => 'Kira']); $user3 = self::getDataGenerator()->create_user(['firstname' => 'Luna']); $this->getDataGenerator()->enrol_user($user1->id, $course->id, 'student', 'manual', $timestart - 8 * DAYSECS, $timeend, ENROL_USER_ACTIVE); $this->getDataGenerator()->enrol_user($user2->id, $course->id, 'student', 'manual', $timestart, $timeend, ENROL_USER_ACTIVE); $this->getDataGenerator()->enrol_user($user3->id, $course->id, 'editingteacher', 'manual', time(), time(), ENROL_USER_SUSPENDED); // Add user1 to a group. $group = $this->getDataGenerator()->create_group(['courseid' => $course->id, 'name' => 'My group']); $this->getDataGenerator()->create_group_member(['groupid' => $group->id, 'userid' => $user1->id]); // Add some cohort data. $cohort = $this->getDataGenerator()->create_cohort(['name' => 'My cohort']); cohort_add_member($cohort->id, $user2->id); // Mark course as completed for the user. $ccompletion = new completion_completion(array('course' => $course->id, 'userid' => $user1->id)); $ccompletion->mark_enrolled($timestart); $ccompletion->mark_inprogress($timestart); $ccompletion->mark_complete($timecompleted); // Set some last access value for the user in the course. $DB->insert_record('user_lastaccess', ['userid' => $user1->id, 'courseid' => $course->id, 'timeaccess' => $timelastaccess]); /** @var core_reportbuilder_generator $generator */ $generator = $this->getDataGenerator()->get_plugin_generator('core_reportbuilder'); $report = $generator->create_report(['name' => 'Participants', 'source' => participants::class, 'default' => false]); // Add user firstname column to the report. $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'user:firstname']); $DB->set_field('user_enrolments', 'timecreated', 0, ['userid' => $user1->id]); $DB->set_field('user_enrolments', 'timecreated', 0, ['userid' => $user3->id]); // Add filters to the report. $generator->create_filter(['reportid' => $report->get('id'), 'uniqueidentifier' => 'enrol:plugin']); $generator->create_filter(['reportid' => $report->get('id'), 'uniqueidentifier' => $filter]); // Apply filters. $filtermanual = ['enrol:plugin_operator' => select::EQUAL_TO, 'enrol:plugin_value' => 'manual']; $content = $this->get_custom_report_content($report->get('id'), 30, $filtermanual + $filtervalues); $this->assertEqualsCanonicalizing($expected, array_column($content, 'c0_firstname')); } /** * Stress test datasource * * In order to execute this test PHPUNIT_LONGTEST should be defined as true in phpunit.xml or directly in config.php */ public function test_stress_datasource(): void { if (!PHPUNIT_LONGTEST) { $this->markTestSkipped('PHPUNIT_LONGTEST is not defined'); } $this->resetAfterTest(); $course = $this->getDataGenerator()->create_course(); $this->getDataGenerator()->create_and_enrol($course); $this->datasource_stress_test_columns(participants::class); $this->datasource_stress_test_columns_aggregation(participants::class); $this->datasource_stress_test_conditions(participants::class, 'course:idnumber'); } } PK7\qK__datasource/users.phpnu[. declare(strict_types=1); namespace core_badges\reportbuilder\datasource; use lang_string; use core_reportbuilder\datasource; use core_reportbuilder\local\entities\{course, user}; use core_reportbuilder\local\helpers\database; use core_badges\reportbuilder\local\entities\{badge, badge_issued}; use core_cohort\reportbuilder\local\entities\cohort; use core_tag\reportbuilder\local\entities\tag; /** * User badges datasource * * @package core_badges * @copyright 2023 Paul Holden * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class users extends datasource { /** * Return user friendly name of the report source * * @return string */ public static function get_name(): string { return get_string('userbadges', 'core_badges'); } /** * Initialise report */ protected function initialise(): void { global $CFG; $userentity = new user(); $useralias = $userentity->get_table_alias('user'); $this->set_main_table('user', $useralias); $this->add_entity($userentity); $paramguest = database::generate_param_name(); $this->add_base_condition_sql("{$useralias}.id != :{$paramguest} AND {$useralias}.deleted = 0", [ $paramguest => $CFG->siteguest, ]); // Join the badge issued entity to the user entity. $badgeissuedentity = new badge_issued(); $badgeissuedalias = $badgeissuedentity->get_table_alias('badge_issued'); $this->add_entity($badgeissuedentity ->add_join("LEFT JOIN {badge_issued} {$badgeissuedalias} ON {$badgeissuedalias}.userid = {$useralias}.id")); $badgeentity = new badge(); $badgealias = $badgeentity->get_table_alias('badge'); $this->add_entity($badgeentity ->add_joins($badgeissuedentity->get_joins()) ->add_join("LEFT JOIN {badge} {$badgealias} ON {$badgealias}.id = {$badgeissuedalias}.badgeid")); // Join the tag entity. $tagentity = (new tag()) ->set_table_alias('tag', $badgeentity->get_table_alias('tag')) ->set_entity_title(new lang_string('badgetags', 'core_badges')); $this->add_entity($tagentity ->add_joins($badgeentity->get_joins()) ->add_joins($badgeentity->get_tag_joins())); // Join the course entity to the badge entity, coalescing courseid with the siteid for site badges. $courseentity = new course(); $coursealias = $courseentity->get_table_alias('course'); $this->add_entity($courseentity ->add_joins($badgeentity->get_joins()) ->add_join("LEFT JOIN {course} {$coursealias} ON {$coursealias}.id = CASE WHEN {$badgealias}.id IS NULL THEN 0 ELSE COALESCE({$badgealias}.courseid, 1) END")); // Join the cohort entity. $cohortentity = new cohort(); $cohortalias = $cohortentity->get_table_alias('cohort'); $cohortmemberalias = database::generate_alias(); $this->add_entity($cohortentity->add_joins([ "LEFT JOIN {cohort_members} {$cohortmemberalias} ON {$cohortmemberalias}.userid = {$useralias}.id", "LEFT JOIN {cohort} {$cohortalias} ON {$cohortalias}.id = {$cohortmemberalias}.cohortid", ])); // Add report elements from each of the entities we added to the report. $this->add_all_from_entity($userentity->get_entity_name()); $this->add_all_from_entity($badgeissuedentity->get_entity_name()); $this->add_all_from_entity($badgeentity->get_entity_name()); $this->add_all_from_entity($tagentity->get_entity_name(), ['name', 'namewithlink'], ['name'], ['name']); $this->add_all_from_entity($courseentity->get_entity_name()); $this->add_all_from_entity($cohortentity->get_entity_name(), ['name', 'idnumber', 'description', 'customfield*'], ['cohortselect', 'name', 'idnumber', 'customfield*'], ['cohortselect', 'name', 'idnumber', 'customfield*']); } /** * Return the columns that will be added to the report upon creation * * @return string[] */ public function get_default_columns(): array { return [ 'user:fullname', 'badge:name', 'badge:description', 'badge_issued:issued', ]; } /** * Return the column sorting that will be added to the report upon creation * * @return int[] */ public function get_default_column_sorting(): array { return [ 'user:fullname' => SORT_ASC, 'badge:name' => SORT_ASC, 'badge_issued:issued' => SORT_ASC, ]; } /** * Return the filters that will be added to the report upon creation * * @return string[] */ public function get_default_filters(): array { return [ 'user:fullname', 'badge:name', 'badge_issued:issued', ]; } /** * Return the conditions that will be added to the report upon creation * * @return string[] */ public function get_default_conditions(): array { return [ 'badge:type', 'badge:name', ]; } } PK`\datasource/badges.phpnu[. declare(strict_types=1); namespace core_badges\reportbuilder\datasource; use lang_string; use core_reportbuilder\datasource; use core_reportbuilder\local\entities\{course, user}; use core_badges\reportbuilder\local\entities\{badge, badge_issued}; use core_tag\reportbuilder\local\entities\tag; /** * Badges datasource * * @package core_badges * @copyright 2022 Paul Holden * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class badges extends datasource { /** * Return user friendly name of the report source * * @return string */ public static function get_name(): string { return get_string('badges', 'core_badges'); } /** * Initialise report */ protected function initialise(): void { $badgeentity = new badge(); $badgealias = $badgeentity->get_table_alias('badge'); $this->set_main_table('badge', $badgealias); $this->add_entity($badgeentity); // Join the tag entity. $tagentity = (new tag()) ->set_table_alias('tag', $badgeentity->get_table_alias('tag')) ->set_entity_title(new lang_string('badgetags', 'core_badges')); $this->add_entity($tagentity ->add_joins($badgeentity->get_tag_joins())); // Join the badge issued entity to the badge entity. $badgeissuedentity = new badge_issued(); $badgeissuedalias = $badgeissuedentity->get_table_alias('badge_issued'); $this->add_entity($badgeissuedentity ->add_join("LEFT JOIN {badge_issued} {$badgeissuedalias} ON {$badgeissuedalias}.badgeid = {$badgealias}.id") ); // Join the user entity to the badge issued entity. $userentity = new user(); $useralias = $userentity->get_table_alias('user'); $this->add_entity($userentity ->add_joins($badgeissuedentity->get_joins()) ->add_join("LEFT JOIN {user} {$useralias} ON {$useralias}.id = {$badgeissuedalias}.userid") ->set_entity_title(new lang_string('recipient', 'core_badges')) ); // Join the course entity to the badge entity, coalescing courseid with the siteid for site badges. $courseentity = new course(); $coursealias = $courseentity->get_table_alias('course'); $this->add_entity($courseentity ->add_join("LEFT JOIN {course} {$coursealias} ON {$coursealias}.id = COALESCE({$badgealias}.courseid, 1)") ); // Add report elements from each of the entities we added to the report. $this->add_all_from_entity($badgeentity->get_entity_name()); // Add specific tag entity elements. $this->add_columns_from_entity($tagentity->get_entity_name(), ['name', 'namewithlink']); $this->add_filter($tagentity->get_filter('name')); $this->add_condition($tagentity->get_condition('name')); $this->add_all_from_entity($badgeissuedentity->get_entity_name()); $this->add_all_from_entity($userentity->get_entity_name()); $this->add_all_from_entity($courseentity->get_entity_name()); } /** * Return the columns that will be added to the report upon creation * * @return string[] */ public function get_default_columns(): array { return [ 'badge:name', 'badge:description', 'user:fullname', 'badge_issued:issued', ]; } /** * Return the filters that will be added to the report upon creation * * @return string[] */ public function get_default_filters(): array { return [ 'badge:name', 'user:fullname', 'badge_issued:issued', ]; } /** * Return the conditions that will be added to the report upon creation * * @return string[] */ public function get_default_conditions(): array { return [ 'badge:type', 'badge:name', ]; } /** * Return the default sorting that will be added to the report once it is created * * @return array|int[] */ public function get_default_column_sorting(): array { return [ 'badge:name' => SORT_ASC, 'user:fullname' => SORT_ASC, 'badge_issued:issued' => SORT_ASC, ]; } } PK`\=I.55local/entities/badge.phpnu[. declare(strict_types=1); namespace core_badges\reportbuilder\local\entities; use context_course; use context_helper; use context_system; use html_writer; use lang_string; use moodle_url; use stdClass; use core_reportbuilder\local\entities\base; use core_reportbuilder\local\filters\{date, select, text}; use core_reportbuilder\local\helpers\database; use core_reportbuilder\local\report\{column, filter}; defined('MOODLE_INTERNAL') or die; global $CFG; require_once("{$CFG->libdir}/badgeslib.php"); /** * Badge entity * * @package core_badges * @copyright 2022 Paul Holden * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class badge extends base { /** * Database tables that this entity uses * * @return string[] */ protected function get_default_tables(): array { return [ 'badge', 'context', 'tag_instance', 'tag', ]; } /** * The default title for this entity * * @return lang_string */ protected function get_default_entity_title(): lang_string { return new lang_string('badgedetails', 'core_badges'); } /** * Initialise the entity * * @return base */ public function initialise(): base { $columns = $this->get_all_columns(); foreach ($columns as $column) { $this->add_column($column); } // All the filters defined by the entity can also be used as conditions. $filters = $this->get_all_filters(); foreach ($filters as $filter) { $this ->add_filter($filter) ->add_condition($filter); } return $this; } /** * Returns list of all available columns * * @return column[] */ protected function get_all_columns(): array { $badgealias = $this->get_table_alias('badge'); $contextalias = $this->get_table_alias('context'); // Name. $columns[] = (new column( 'name', new lang_string('name'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_TEXT) ->add_field("{$badgealias}.name") ->set_is_sortable(true); // Name with link. $columns[] = (new column( 'namewithlink', new lang_string('namewithlink', 'core_badges'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_TEXT) ->add_fields("{$badgealias}.name, {$badgealias}.id") ->set_is_sortable(true) ->add_callback(static function(?string $value, stdClass $row): string { if (!$row->id) { return ''; } $url = new moodle_url('/badges/overview.php', ['id' => $row->id]); return html_writer::link($url, $row->name); }); // Name with image and link. $columns[] = (new column( 'namewithimagelink', new lang_string('namewithimagelink', 'core_badges'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->add_join("LEFT JOIN {context} {$contextalias} ON {$contextalias}.contextlevel = " . CONTEXT_COURSE . " AND {$contextalias}.instanceid = {$badgealias}.courseid") ->add_fields( "{$badgealias}.name, {$badgealias}.id, {$badgealias}.type, {$badgealias}.courseid, {$badgealias}.imagecaption" ) ->add_fields(context_helper::get_preload_record_columns_sql($contextalias)) ->set_is_sortable(true) ->add_callback(static function ($value, stdClass $badge): string { if ($badge->id === null) { return ''; } if ($badge->type == BADGE_TYPE_SITE) { $context = context_system::instance(); } else { context_helper::preload_from_record($badge); $context = context_course::instance($badge->courseid); } $badgeimage = moodle_url::make_pluginfile_url($context->id, 'badges', 'badgeimage', $badge->id, '/', 'f2'); $url = new moodle_url('/badges/overview.php', ['id' => $badge->id]); return html_writer::img($badgeimage, $badge->imagecaption) . ' ' . html_writer::link($url, $badge->name); }); // Description (note, this column contains plaintext so requires no post-processing). $columns[] = (new column( 'description', new lang_string('description', 'core_badges'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_LONGTEXT) ->add_field("{$badgealias}.description") ->set_is_sortable(true); // Criteria. $columns[] = (new column( 'criteria', new lang_string('bcriteria', 'core_badges'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_TEXT) ->add_field("{$badgealias}.id") ->set_disabled_aggregation_all() ->add_callback(static function($badgeid): string { global $PAGE; if (!$badgeid) { return ''; } $badge = new \core_badges\badge($badgeid); if (empty($badge->criteria)) { return ''; } $renderer = $PAGE->get_renderer('core_badges'); return $renderer->print_badge_criteria($badge, 'short'); }); // Image. $columns[] = (new column( 'image', new lang_string('badgeimage', 'core_badges'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->add_join("LEFT JOIN {context} {$contextalias} ON {$contextalias}.contextlevel = " . CONTEXT_COURSE . " AND {$contextalias}.instanceid = {$badgealias}.courseid") ->add_fields("{$badgealias}.id, {$badgealias}.type, {$badgealias}.courseid, {$badgealias}.imagecaption") ->add_fields(context_helper::get_preload_record_columns_sql($contextalias)) ->add_callback(static function($value, stdClass $badge): string { if ($badge->id === null) { return ''; } if ($badge->type == BADGE_TYPE_SITE) { $context = context_system::instance(); } else { context_helper::preload_from_record($badge); $context = context_course::instance($badge->courseid); } $badgeimage = moodle_url::make_pluginfile_url($context->id, 'badges', 'badgeimage', $badge->id, '/', 'f2'); return html_writer::img($badgeimage, $badge->imagecaption); }); // Language. $columns[] = (new column( 'language', new lang_string('language'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_TEXT) ->add_field("{$badgealias}.language") ->set_is_sortable(true) ->add_callback(static function($language): string { $languages = get_string_manager()->get_list_of_languages(); return (string) ($languages[$language] ?? $language); }); // Version. $columns[] = (new column( 'version', new lang_string('version', 'core_badges'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_TEXT) ->add_field("{$badgealias}.version") ->set_is_sortable(true); // Status. $columns[] = (new column( 'status', new lang_string('status', 'core_badges'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_TEXT) ->add_field("{$badgealias}.status") ->set_is_sortable(true) ->add_callback(static function($status): string { if ($status === null) { return ''; } return get_string("badgestatus_{$status}", 'core_badges'); }); // Expiry date/period. $columns[] = (new column( 'expiry', new lang_string('expirydate', 'core_badges'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_TIMESTAMP) ->add_fields("{$badgealias}.expiredate, {$badgealias}.expireperiod, {$badgealias}.id") ->set_is_sortable(true, ["{$badgealias}.expiredate", "{$badgealias}.expireperiod"]) ->set_disabled_aggregation_all() ->add_callback(static function(?int $expiredate, stdClass $badge): string { if (!$badge->id) { return ''; } else if ($expiredate) { return userdate($expiredate); } else if ($badge->expireperiod) { return format_time($badge->expireperiod); } else { return get_string('never', 'core_badges'); } }); return $columns; } /** * Return list of all available filters * * @return filter[] */ protected function get_all_filters(): array { $badgealias = $this->get_table_alias('badge'); // Name. $filters[] = (new filter( text::class, 'name', new lang_string('name'), $this->get_entity_name(), "{$badgealias}.name" )) ->add_joins($this->get_joins()); // Version. $filters[] = (new filter( text::class, 'version', new lang_string('version', 'core_badges'), $this->get_entity_name(), "{$badgealias}.version" )) ->add_joins($this->get_joins()); // Status. $filters[] = (new filter( select::class, 'status', new lang_string('status', 'core_badges'), $this->get_entity_name(), "{$badgealias}.status" )) ->add_joins($this->get_joins()) ->set_options([ BADGE_STATUS_INACTIVE => new lang_string('badgestatus_0', 'core_badges'), BADGE_STATUS_ACTIVE => new lang_string('badgestatus_1', 'core_badges'), BADGE_STATUS_INACTIVE_LOCKED => new lang_string('badgestatus_2', 'core_badges'), BADGE_STATUS_ACTIVE_LOCKED => new lang_string('badgestatus_3', 'core_badges'), BADGE_STATUS_ARCHIVED => new lang_string('badgestatus_4', 'core_badges'), ]); // Expiry date/period. $paramtime = database::generate_param_name(); $filters[] = (new filter( date::class, 'expiry', new lang_string('expirydate', 'core_badges'), $this->get_entity_name(), "CASE WHEN {$badgealias}.expiredate IS NULL AND {$badgealias}.expireperiod IS NULL THEN " . SQL_INT_MAX . " ELSE COALESCE({$badgealias}.expiredate, {$badgealias}.expireperiod + :{$paramtime}) END", [$paramtime => time()] )) ->add_joins($this->get_joins()) ->set_limited_operators([ date::DATE_ANY, date::DATE_RANGE, date::DATE_LAST, date::DATE_CURRENT, date::DATE_NEXT, date::DATE_PAST, date::DATE_FUTURE, ]); // Type. $filters[] = (new filter( select::class, 'type', new lang_string('type', 'core_badges'), $this->get_entity_name(), "{$badgealias}.type" )) ->add_joins($this->get_joins()) ->set_options([ BADGE_TYPE_SITE => new lang_string('site'), BADGE_TYPE_COURSE => new lang_string('course'), ]); return $filters; } /** * Return joins necessary for retrieving tags * * @return string[] */ public function get_tag_joins(): array { return $this->get_tag_joins_for_entity('core_badges', 'badge', $this->get_table_alias('badge') . '.id'); } } PK`\(ilocal/entities/badge_issued.phpnu[. declare(strict_types=1); namespace core_badges\reportbuilder\local\entities; use lang_string; use core_reportbuilder\local\entities\base; use core_reportbuilder\local\filters\{boolean_select, date}; use core_reportbuilder\local\helpers\format; use core_reportbuilder\local\report\{column, filter}; /** * Badge issued entity * * @package core_badges * @copyright 2022 Paul Holden * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class badge_issued extends base { /** * Database tables that this entity uses * * @return string[] */ protected function get_default_tables(): array { return [ 'badge_issued', ]; } /** * The default title for this entity * * @return lang_string */ protected function get_default_entity_title(): lang_string { return new lang_string('badgeissued', 'core_badges'); } /** * Initialise the entity * * @return base */ public function initialise(): base { $columns = $this->get_all_columns(); foreach ($columns as $column) { $this->add_column($column); } // All the filters defined by the entity can also be used as conditions. $filters = $this->get_all_filters(); foreach ($filters as $filter) { $this ->add_filter($filter) ->add_condition($filter); } return $this; } /** * Returns list of all available columns * * @return column[] */ protected function get_all_columns(): array { $badgeissuedalias = $this->get_table_alias('badge_issued'); // Date issued. $columns[] = (new column( 'issued', new lang_string('dateawarded', 'core_badges'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_TIMESTAMP) ->add_field("{$badgeissuedalias}.dateissued") ->set_is_sortable(true) ->add_callback([format::class, 'userdate']); // Date expires. $columns[] = (new column( 'expire', new lang_string('expirydate', 'core_badges'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_TIMESTAMP) ->add_field("{$badgeissuedalias}.dateexpire") ->set_is_sortable(true) ->add_callback([format::class, 'userdate']); // Visible. $columns[] = (new column( 'visible', new lang_string('visible', 'core_badges'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_BOOLEAN) ->add_fields("{$badgeissuedalias}.visible") ->add_callback([format::class, 'boolean_as_text']); return $columns; } /** * Return list of all available filters * * @return filter[] */ protected function get_all_filters(): array { $badgealias = $this->get_table_alias('badge_issued'); // Date issued. $filters[] = (new filter( date::class, 'issued', new lang_string('dateawarded', 'core_badges'), $this->get_entity_name(), "{$badgealias}.dateissued" )) ->add_joins($this->get_joins()); // Date expires. $filters[] = (new filter( date::class, 'expires', new lang_string('expirydate', 'core_badges'), $this->get_entity_name(), "{$badgealias}.dateexpire" )) ->add_joins($this->get_joins()); // Visible. $filters[] = (new filter( boolean_select::class, 'visible', new lang_string('visible', 'core_badges'), $this->get_entity_name(), "{$badgealias}.visible" )) ->add_joins($this->get_joins()); return $filters; } } PK`\)"local/systemreports/recipients.phpnu[. declare(strict_types=1); namespace core_badges\reportbuilder\local\systemreports; use core_badges\reportbuilder\local\entities\badge_issued; use core_reportbuilder\local\report\action; use core_reportbuilder\system_report; use lang_string; use moodle_url; use pix_icon; /** * Badge recipients system report class implementation * * @package core_badges * @copyright 2023 David Carrillo * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class recipients extends system_report { /** * Initialise report, we need to set the main table, load our entities and set columns/filters */ protected function initialise(): void { // Our main entity, it contains all of the column definitions that we need. $badgeissuedentity = new badge_issued(); $entityalias = $badgeissuedentity->get_table_alias('badge_issued'); $this->set_main_table('badge_issued', $entityalias); $this->add_entity($badgeissuedentity); $userentity = new \core_reportbuilder\local\entities\user(); $entityuseralias = $userentity->get_table_alias('user'); $this->add_entity($userentity ->add_joins($userentity->get_joins()) ->add_join("JOIN {user} {$entityuseralias} ON {$entityuseralias}.id = {$entityalias}.userid") ); $this->add_base_condition_simple('badgeid', $this->get_parameter('badgeid', 0, PARAM_INT)); $this->add_base_fields("{$entityalias}.uniquehash"); // Now we can call our helper methods to add the content we want to include in the report. $this->add_columns(); $this->add_filters(); $this->add_actions(); $this->set_initial_sort_column('badge_issued:issued', SORT_DESC); $this->set_default_no_results_notice(new lang_string('nomatchingawards', 'core_badges')); // Set if report can be downloaded. $this->set_downloadable(false); } /** * Validates access to view this report * * @return bool */ protected function can_view(): bool { $badgeid = $this->get_parameter('badgeid', 0, PARAM_INT); $badge = new \core_badges\badge($badgeid); return has_capability('moodle/badges:viewawarded', $badge->get_context()); } /** * Adds the columns we want to display in the report * * They are provided by the entities we previously added in the {@see initialise} method, referencing each by their * unique identifier. If custom columns are needed just for this report, they can be defined here. */ protected function add_columns(): void { $this->add_columns_from_entities([ 'user:fullnamewithlink', 'badge_issued:issued', ]); } /** * Adds the filters we want to display in the report * * They are all provided by the entities we previously added in the {@see initialise} method, referencing each by their * unique identifier */ protected function add_filters(): void { $this->add_filters_from_entities([ 'user:fullname', 'badge_issued:issued', ]); } /** * Add the system report actions. An extra column will be appended to each row, containing all actions added here * * Note the use of ":uniquehash" placeholder which will be substituted according to actual values in the row */ protected function add_actions(): void { $this->add_action((new action( new moodle_url('/badges/badge.php', [ 'hash' => ':uniquehash', ]), new pix_icon('i/search', '', 'core'), [], false, new lang_string('viewbadge', 'badges') ))); } } PK`\%local/systemreports/course_badges.phpnu[. declare(strict_types=1); namespace core_badges\reportbuilder\local\systemreports; use core\context\system; use core_badges\reportbuilder\local\entities\badge; use core_badges\reportbuilder\local\entities\badge_issued; use core_reportbuilder\local\helpers\database; use core_reportbuilder\system_report; use lang_string; use moodle_url; use pix_icon; use stdClass; defined('MOODLE_INTERNAL') || die; global $CFG; require_once("{$CFG->libdir}/badgeslib.php"); /** * Course badges system report class implementation * * @package core_badges * @copyright 2023 David Carrillo * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @deprecated since 4.5. Use badges system_report instead. * @todo MDL-82383 This class will be deleted in Moodle 6.0. */ class course_badges extends system_report { /** * Initialise report, we need to set the main table, load our entities and set columns/filters * * @deprecated since 4.5. Use badges system_report instead. */ #[\core\attribute\deprecated('course_badges::initialise', since: '4.5', mdl: 'MDL-82503')] protected function initialise(): void { \core\deprecation::emit_deprecation([$this, __FUNCTION__]); global $USER; // Our main entity, it contains all of the column definitions that we need. $badgeentity = new badge(); $entityalias = $badgeentity->get_table_alias('badge'); $this->set_main_table('badge', $entityalias); $this->add_entity($badgeentity); $paramtype = database::generate_param_name(); $context = $this->get_context(); if ($context instanceof system) { $type = BADGE_TYPE_SITE; $this->add_base_condition_sql("{$entityalias}.type = :$paramtype", [$paramtype => $type]); } else { $type = BADGE_TYPE_COURSE; $paramcourseid = database::generate_param_name(); $this->add_base_condition_sql("{$entityalias}.type = :$paramtype AND {$entityalias}.courseid = :$paramcourseid", [$paramtype => $type, $paramcourseid => $context->instanceid]); } $this->add_base_condition_sql("({$entityalias}.status = " . BADGE_STATUS_ACTIVE . " OR {$entityalias}.status = " . BADGE_STATUS_ACTIVE_LOCKED . ")"); $badgeissuedentity = new badge_issued(); $badgeissuedalias = $badgeissuedentity->get_table_alias('badge_issued'); $this->add_entity($badgeissuedentity ->add_join("LEFT JOIN {badge_issued} {$badgeissuedalias} ON {$entityalias}.id = {$badgeissuedalias}.badgeid AND {$badgeissuedalias}.userid = ".$USER->id) ); $this->add_base_fields("{$badgeissuedalias}.uniquehash"); // Now we can call our helper methods to add the content we want to include in the report. $this->add_columns(); $this->add_filters(); $this->set_initial_sort_column('badge:name', SORT_ASC); $this->set_default_no_results_notice(new lang_string('nomatchingbadges', 'core_badges')); // Set if report can be downloaded. $this->set_downloadable(false); } /** * Validates access to view this report * * @return bool * @deprecated since 4.5. Use badges system_report instead. */ #[\core\attribute\deprecated('course_badges::can_view', since: '4.5', mdl: 'MDL-82503')] protected function can_view(): bool { \core\deprecation::emit_deprecation([$this, __FUNCTION__]); return has_capability('moodle/badges:viewbadges', $this->get_context()); } /** * Adds the columns we want to display in the report * * They are provided by the entities we previously added in the {@see initialise} method, referencing each by their * unique identifier. If custom columns are needed just for this report, they can be defined here. * * @param string $badgeissuedalias * @deprecated since 4.5. Use badges system_report instead. */ #[\core\attribute\deprecated('course_badges::add_columns', since: '4.5', mdl: 'MDL-82503')] protected function add_columns(): void { \core\deprecation::emit_deprecation([$this, __FUNCTION__]); $badgeissuedalias = $this->get_entity('badge_issued')->get_table_alias('badge_issued'); $this->add_columns_from_entities([ 'badge:image', 'badge:name', 'badge:description', 'badge:criteria', 'badge_issued:issued', ]); $this->get_column('badge_issued:issued') ->set_title(new lang_string('awardedtoyou', 'core_badges')) ->add_fields("{$badgeissuedalias}.uniquehash") ->set_callback(static function(?int $value, stdClass $row) { global $OUTPUT; if (!$value) { return ''; } $format = get_string('strftimedatefullshort', 'core_langconfig'); $date = $value ? userdate($value, $format) : ''; $badgeurl = new moodle_url('/badges/badge.php', ['hash' => $row->uniquehash]); $icon = new pix_icon('i/valid', get_string('dateearned', 'badges', $date)); return $OUTPUT->action_icon($badgeurl, $icon, null, null, true); }); } /** * Adds the filters we want to display in the report * * They are all provided by the entities we previously added in the {@see initialise} method, referencing each by their * unique identifier * * @deprecated since 4.5. Use badges system_report instead. */ #[\core\attribute\deprecated('course_badges::add_filters', since: '4.5', mdl: 'MDL-82503')] protected function add_filters(): void { \core\deprecation::emit_deprecation([$this, __FUNCTION__]); $this->add_filters_from_entities([ 'badge:name', 'badge_issued:issued', ]); } } PK`\U/88local/systemreports/badges.phpnu[. declare(strict_types=1); namespace core_badges\reportbuilder\local\systemreports; use core\context\{course, system}; use core_badges\reportbuilder\local\entities\badge; use core_badges\reportbuilder\local\entities\badge_issued; use core_reportbuilder\local\helpers\database; use core_reportbuilder\local\report\{action, column}; use core_reportbuilder\system_report; use html_writer; use lang_string; use moodle_url; use pix_icon; use stdClass; defined('MOODLE_INTERNAL') || die; global $CFG; require_once("{$CFG->libdir}/badgeslib.php"); /** * Badges system report class implementation * * @package core_badges * @copyright 2023 David Carrillo * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class badges extends system_report { /** @var int $badgeid The ID of the current badge row */ private int $badgeid; /** * Initialise report, we need to set the main table, load our entities and set columns/filters */ protected function initialise(): void { global $USER; // Our main entity, it contains all of the column definitions that we need. $badgeentity = new badge(); $entityalias = $badgeentity->get_table_alias('badge'); $this->set_main_table('badge', $entityalias); $this->add_entity($badgeentity); $paramtype = database::generate_param_name(); $context = $this->get_context(); if ($context instanceof system) { $type = BADGE_TYPE_SITE; $this->add_base_condition_sql("{$entityalias}.type = :$paramtype", [$paramtype => $type]); } else { $type = BADGE_TYPE_COURSE; $paramcourseid = database::generate_param_name(); $this->add_base_condition_sql("{$entityalias}.type = :$paramtype AND {$entityalias}.courseid = :$paramcourseid", [$paramtype => $type, $paramcourseid => $context->instanceid]); } if (!$this->can_view_draft_badges()) { $this->add_base_condition_sql("({$entityalias}.status = " . BADGE_STATUS_ACTIVE . " OR {$entityalias}.status = " . BADGE_STATUS_ACTIVE_LOCKED . ")"); } // Any columns required by actions should be defined here to ensure they're always available. $this->add_base_fields("{$entityalias}.id, {$entityalias}.type, {$entityalias}.courseid, {$entityalias}.status"); $badgeissuedentity = new badge_issued(); $badgeissuedalias = $badgeissuedentity->get_table_alias('badge_issued'); $this->add_entity($badgeissuedentity ->add_join("LEFT JOIN {badge_issued} {$badgeissuedalias} ON {$entityalias}.id = {$badgeissuedalias}.badgeid AND {$badgeissuedalias}.userid = ".$USER->id) ); $this->add_base_fields("{$badgeissuedalias}.uniquehash"); // Now we can call our helper methods to add the content we want to include in the report. $this->add_columns($badgeissuedalias); $this->add_filters(); $this->add_actions(); $this->set_default_no_results_notice(new lang_string('nomatchingbadges', 'core_badges')); // Set if report can be downloaded. $this->set_downloadable(false); } /** * Validates access to view this report * * @return bool */ protected function can_view(): bool { return has_any_capability([ 'moodle/badges:viewbadges', 'moodle/badges:viewawarded', 'moodle/badges:createbadge', 'moodle/badges:awardbadge', 'moodle/badges:configurecriteria', 'moodle/badges:configuremessages', 'moodle/badges:configuredetails', 'moodle/badges:deletebadge'], $this->get_context()); } /** * Adds the columns we want to display in the report * * They are provided by the entities we previously added in the {@see initialise} method, referencing each by their * unique identifier. If custom columns are needed just for this report, they can be defined here. * * @param string $badgeissuedalias */ public function add_columns(string $badgeissuedalias): void { $columns = [ 'badge:namewithimagelink', 'badge:status', 'badge:criteria', ]; $canviewdraftbadges = $this->can_view_draft_badges(); if (!$canviewdraftbadges) { // Remove status and recipients column. unset($columns[1]); } $this->add_columns_from_entities($columns); // Change title of the `namewithimagelink` column to 'Name'. $this->get_column('badge:namewithimagelink')->set_title(new lang_string('name')); // Recipients column. if ($canviewdraftbadges) { $badgeentity = $this->get_entity('badge'); $tempbadgealias = database::generate_alias(); $badgeentityalias = $badgeentity->get_table_alias('badge'); $this->add_column((new column( 'issued', new lang_string('awards', 'core_badges'), $badgeentity->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_INTEGER) ->add_field("(SELECT COUNT({$tempbadgealias}.userid) FROM {badge_issued} {$tempbadgealias} INNER JOIN {user} u ON {$tempbadgealias}.userid = u.id WHERE {$tempbadgealias}.badgeid = {$badgeentityalias}.id AND u.deleted = 0)", 'issued') ->set_is_sortable(true) ->set_callback(function(int $count): string { if (!has_capability('moodle/badges:viewawarded', $this->get_context())) { return (string) $count; } return html_writer::link(new moodle_url('/badges/recipients.php', ['id' => $this->badgeid]), $count); })); } // Add the date the badge was issued at the end of the report. $this->add_column_from_entity('badge_issued:issued'); $this->get_column('badge_issued:issued') ->set_title(new lang_string('awardedtoyou', 'core_badges')) ->add_fields("{$badgeissuedalias}.uniquehash") ->set_callback(static function(?int $value, stdClass $row) { global $OUTPUT; if (!$value) { return ''; } $format = get_string('strftimedatefullshort', 'core_langconfig'); $date = $value ? userdate($value, $format) : ''; $badgeurl = new moodle_url('/badges/badge.php', ['hash' => $row->uniquehash]); $icon = new pix_icon('i/valid', get_string('dateearned', 'badges', $date)); return $OUTPUT->action_icon($badgeurl, $icon, null, null, true); }); $this->set_initial_sort_column('badge:namewithimagelink', SORT_ASC); } /** * Adds the filters we want to display in the report * * They are all provided by the entities we previously added in the {@see initialise} method, referencing each by their * unique identifier */ protected function add_filters(): void { $filters = [ 'badge:name', 'badge:version', 'badge:status', 'badge:expiry', 'badge_issued:issued', ]; if (!$this->can_view_draft_badges()) { // Remove version and status filters. unset($filters[1]); unset($filters[2]); } $this->add_filters_from_entities($filters); } /** * Add the system report actions. An extra column will be appended to each row, containing all actions added here * * Note the use of ":id" placeholder which will be substituted according to actual values in the row */ protected function add_actions(): void { // Activate badge. $this->add_action((new action( new moodle_url('#'), new pix_icon('t/show', '', 'core'), [ 'data-action' => 'enablebadge', 'data-badgeid' => ':id', 'data-badgename' => ':badgename', 'data-courseid' => ':courseid', ], false, new lang_string('activate', 'badges') ))->add_callback(static function(stdclass $row): bool { $badge = new \core_badges\badge($row->id); $row->badgename = $badge->name; return has_capability('moodle/badges:configurecriteria', $badge->get_context()) && $badge->has_criteria() && ($row->status == BADGE_STATUS_INACTIVE || $row->status == BADGE_STATUS_INACTIVE_LOCKED); })); // Deactivate badge. $this->add_action((new action( new moodle_url('#'), new pix_icon('t/hide', '', 'core'), [ 'data-action' => 'disablebadge', 'data-badgeid' => ':id', 'data-badgename' => ':badgename', 'data-courseid' => ':courseid', ], false, new lang_string('deactivate', 'badges') ))->add_callback(static function(stdclass $row): bool { $badge = new \core_badges\badge($row->id); $row->badgename = $badge->name; return has_capability('moodle/badges:configurecriteria', $badge->get_context()) && $badge->has_criteria() && $row->status != BADGE_STATUS_INACTIVE && $row->status != BADGE_STATUS_INACTIVE_LOCKED; })); // Award badge manually. $this->add_action((new action( new moodle_url('/badges/award.php', [ 'id' => ':id', ]), new pix_icon('t/award', '', 'core'), [], false, new lang_string('award', 'badges') ))->add_callback(static function(stdclass $row): bool { $badge = new \core_badges\badge($row->id); return has_capability('moodle/badges:awardbadge', $badge->get_context()) && $badge->has_manual_award_criteria() && $badge->is_active(); })); // Edit action. $this->add_action((new action( new moodle_url('/badges/edit.php', [ 'id' => ':id', 'action' => 'badge', ]), new pix_icon('t/edit', '', 'core'), [], false, new lang_string('edit', 'core') ))->add_callback(static function(stdclass $row): bool { $context = self::get_badge_context((int)$row->type, (int)$row->courseid); return has_capability('moodle/badges:configuredetails', $context); })); // Duplicate action. $this->add_action((new action( new moodle_url('/badges/action.php', [ 'id' => ':id', 'copy' => 1, 'sesskey' => sesskey(), ]), new pix_icon('t/copy', '', 'core'), [], false, new lang_string('copy', 'badges') ))->add_callback(static function(stdclass $row): bool { $context = self::get_badge_context((int)$row->type, (int)$row->courseid); return has_capability('moodle/badges:createbadge', $context); })); // Delete action. $this->add_action((new action( new moodle_url('/badges/index.php', [ 'delete' => ':id', 'type' => ':type', 'id' => ':courseid', ]), new pix_icon('t/delete', '', 'core'), ['class' => 'text-danger'], false, new lang_string('delete', 'core') ))->add_callback(static function(stdclass $row): bool { $context = self::get_badge_context((int)$row->type, (int)$row->courseid); return has_capability('moodle/badges:deletebadge', $context); })); } /** * Return badge context based on type and courseid * * @param int $type * @param int $courseid * @return \core\context * @throws \coding_exception */ private static function get_badge_context(int $type, int $courseid): \core\context { switch ($type) { case BADGE_TYPE_SITE: return system::instance(); case BADGE_TYPE_COURSE: return course::instance($courseid); default: throw new \coding_exception('Wrong context'); } } /** * Check whether the user can view unpublished badges. * * @return bool True if the user can edit badges, false otherwise. */ private function can_view_draft_badges(): bool { return has_any_capability([ 'moodle/badges:viewawarded', 'moodle/badges:createbadge', 'moodle/badges:awardbadge', 'moodle/badges:configurecriteria', 'moodle/badges:configuremessages', 'moodle/badges:configuredetails', 'moodle/badges:deletebadge'], $this->get_context()); } /** * Store the ID of the badge within each row * * @param stdClass $row */ public function row_callback(stdClass $row): void { $this->badgeid = (int) $row->id; } /** * CSS classes to add to the row * * @param stdClass $row * @return string */ public function get_row_class(stdClass $row): string { return ($row->status == BADGE_STATUS_INACTIVE_LOCKED || $row->status == BADGE_STATUS_INACTIVE) ? 'text-muted' : ''; } } PK\A,,%local/entities/ai_action_register.phpnu[. namespace core_ai\reportbuilder\local\entities; use core\di; use core_ai\manager; use core\component; use core_reportbuilder\local\entities\base; use core_reportbuilder\local\filters\boolean_select; use core_reportbuilder\local\filters\date; use core_reportbuilder\local\filters\number; use core_reportbuilder\local\filters\select; use core_reportbuilder\local\helpers\format; use core_reportbuilder\local\report\column; use core_reportbuilder\local\report\filter; use core\output\help_icon; use lang_string; /** * AI action register entity. * * Defines all the columns and filters that can be added to reports that use this entity. * * @package core_ai * @copyright 2024 David Woloszyn * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class ai_action_register extends base { #[\Override] protected function get_default_tables(): array { return [ 'ai_action_register', ]; } #[\Override] protected function get_default_entity_title(): lang_string { return new lang_string('aiactionregister', 'core_ai'); } #[\Override] public function initialise(): base { $columns = $this->get_all_columns(); foreach ($columns as $column) { $this->add_column($column); } // All the filters defined by the entity can also be used as conditions. $filters = $this->get_all_filters(); foreach ($filters as $filter) { $this ->add_filter($filter) ->add_condition($filter); } return $this; } /** * Returns list of all available columns. * * @return column[] */ protected function get_all_columns(): array { $mainalias = $this->get_table_alias('ai_action_register'); $generatetextalias = 'aagt'; $summarisetextalias = 'aast'; $explaintextalias = 'aaet'; // Action name column. $columns[] = (new column( 'actionname', new lang_string('action', 'core_ai'), $this->get_entity_name(), )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_TEXT) ->add_field("{$mainalias}.actionname") ->set_is_sortable(true) ->add_callback(static function(string $actionname): string { return get_string("action_{$actionname}", 'core_ai'); }); // Provider column. $columns[] = (new column( 'provider', new lang_string('provider', 'core_ai'), $this->get_entity_name(), )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_TEXT) ->add_field("{$mainalias}.provider") ->set_is_sortable(true) ->add_callback(static function(string $provider): string { if (get_string_manager()->string_exists('pluginname', $provider)) { return get_string('pluginname', $provider); } else { // Return as is if the lang string does not exist. return $provider; } }); // Success column. $columns[] = (new column( 'success', new lang_string('success', 'moodle'), $this->get_entity_name(), )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_BOOLEAN) ->add_field("{$mainalias}.success") ->set_is_sortable(true) ->set_callback([format::class, 'boolean_as_text']); // Time created column. $columns[] = (new column( 'timecreated', new lang_string('timegenerated', 'core_ai'), $this->get_entity_name(), )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_TIMESTAMP) ->add_field("{$mainalias}.timecreated") ->set_is_sortable(true) ->add_callback([format::class, 'userdate']); // Prompt tokens column. // Only available for summarise_text, generate_text actions and explain_text actions. $columns[] = (new column( 'prompttokens', new lang_string('prompttokens', 'core_ai'), $this->get_entity_name(), )) ->add_joins($this->get_joins()) ->add_join(" LEFT JOIN {ai_action_generate_text} {$generatetextalias} ON {$mainalias}.actionid = {$generatetextalias}.id AND {$mainalias}.actionname = 'generate_text'") ->add_join(" LEFT JOIN {ai_action_summarise_text} {$summarisetextalias} ON {$mainalias}.actionid = {$summarisetextalias}.id AND {$mainalias}.actionname = 'summarise_text'") ->add_join(" LEFT JOIN {ai_action_explain_text} {$explaintextalias} ON {$mainalias}.actionid = {$explaintextalias}.id AND {$mainalias}.actionname = 'explain_text'") ->set_type(column::TYPE_INTEGER) ->add_field("COALESCE({$generatetextalias}.prompttokens, {$summarisetextalias}.prompttokens, {$explaintextalias}.prompttokens)", 'prompttokens') ->set_is_sortable(true) ->set_help_icon(new help_icon('prompttokens', 'core_ai')) ->add_callback(static function(?int $value): string { return $value ?? get_string('unknownvalue', 'core_ai'); }); // Completion tokens column. // Only available for summarise_text, generate_text actions and explain_text actions. $columns[] = (new column( 'completiontokens', new lang_string('completiontokens', 'core_ai'), $this->get_entity_name(), )) ->add_joins($this->get_joins()) ->add_join(" LEFT JOIN {ai_action_generate_text} {$generatetextalias} ON {$mainalias}.actionid = {$generatetextalias}.id AND {$mainalias}.actionname = 'generate_text'") ->add_join(" LEFT JOIN {ai_action_summarise_text} {$summarisetextalias} ON {$mainalias}.actionid = {$summarisetextalias}.id AND {$mainalias}.actionname = 'summarise_text'") ->add_join(" LEFT JOIN {ai_action_explain_text} {$explaintextalias} ON {$mainalias}.actionid = {$explaintextalias}.id AND {$mainalias}.actionname = 'explain_text'") ->set_type(column::TYPE_INTEGER) ->add_field("COALESCE({$generatetextalias}.completiontoken, {$summarisetextalias}.completiontoken, {$explaintextalias}.completiontoken)", 'completiontokens') ->set_is_sortable(true) ->set_help_icon(new help_icon('completiontokens', 'core_ai')) ->add_callback(static function(?int $value): string { return $value ?? get_string('unknownvalue', 'core_ai'); }); return $columns; } /** * Return list of all available filters. * * @return filter[] */ protected function get_all_filters(): array { $mainalias = $this->get_table_alias('ai_action_register'); $generatetextalias = 'aagt'; $summarisetextalias = 'aast'; $explaintextalias = 'aaet'; // Action name filter. $filters[] = (new filter( select::class, 'actionname', new lang_string('action', 'core_ai'), $this->get_entity_name(), "{$mainalias}.actionname", )) ->add_joins($this->get_joins()) ->set_options([ 'explain_text' => new lang_string('action_explain_text', 'core_ai'), 'generate_image' => new lang_string('action_generate_image', 'core_ai'), 'generate_text' => new lang_string('action_generate_text', 'core_ai'), 'summarise_text' => new lang_string('action_summarise_text', 'core_ai'), ]); // Provider filter. $filters[] = (new filter( select::class, 'provider', new lang_string('provider', 'core_ai'), $this->get_entity_name(), "{$mainalias}.provider", )) ->add_joins($this->get_joins()) ->set_options_callback(static function(): array { $providers = []; $records = di::get(manager::class)->get_provider_records(); foreach ($records as $record) { $component = component::get_component_from_classname($record->provider); $providers[$component] = get_string('pluginname', $component); } return $providers; }); // Time created filter. $filters[] = (new filter( date::class, 'timecreated', new lang_string('timegenerated', 'core_ai'), $this->get_entity_name(), "{$mainalias}.timecreated", )) ->add_joins($this->get_joins()) ->set_limited_operators([ date::DATE_ANY, date::DATE_RANGE, date::DATE_PREVIOUS, date::DATE_CURRENT, ]); // Prompt tokens filter. $filters[] = (new filter( number::class, 'prompttokens', new lang_string('prompttokens', 'core_ai'), $this->get_entity_name(), "COALESCE({$generatetextalias}.prompttokens, {$summarisetextalias}.prompttokens, {$explaintextalias}.prompttokens)", )) ->add_joins($this->get_joins()); // Completion tokens filter. $filters[] = (new filter( number::class, 'completiontokens', new lang_string('completiontokens', 'core_ai'), $this->get_entity_name(), "COALESCE({$generatetextalias}.completiontoken, {$summarisetextalias}.completiontoken, {$explaintextalias}.completiontoken)", )) ->add_joins($this->get_joins()); // Success filter. $filters[] = (new filter( boolean_select::class, 'success', new lang_string('success', 'moodle'), $this->get_entity_name(), "{$mainalias}.success", )) ->add_joins($this->get_joins()); return $filters; } } PK\ %local/entities/ai_policy_register.phpnu[. namespace core_ai\reportbuilder\local\entities; use core_reportbuilder\local\entities\base; use core_reportbuilder\local\filters\date; use core_reportbuilder\local\helpers\format; use core_reportbuilder\local\report\column; use core_reportbuilder\local\report\filter; use lang_string; /** * AI policy register entity. * * Defines all the columns and filters that can be added to reports that use this entity. * * @package core_ai * @copyright 2024 David Woloszyn * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class ai_policy_register extends base { #[\Override] protected function get_default_tables(): array { return [ 'ai_policy_register', ]; } #[\Override] protected function get_default_entity_title(): lang_string { return new lang_string('aipolicyregister', 'core_ai'); } #[\Override] public function initialise(): base { $columns = $this->get_all_columns(); foreach ($columns as $column) { $this->add_column($column); } // All the filters defined by the entity can also be used as conditions. $filters = $this->get_all_filters(); foreach ($filters as $filter) { $this ->add_filter($filter) ->add_condition($filter); } return $this; } /** * Returns list of all available columns. * * @return column[] */ protected function get_all_columns(): array { $tablealias = $this->get_table_alias('ai_policy_register'); // Time accepted column. $columns[] = (new column( 'timeaccepted', new lang_string('dateaccepted', 'core_ai'), $this->get_entity_name(), )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_TIMESTAMP) ->add_field("{$tablealias}.timeaccepted") ->set_is_sortable(true) ->add_callback([format::class, 'userdate']); return $columns; } /** * Return list of all available filters. * * @return filter[] */ protected function get_all_filters(): array { $tablealias = $this->get_table_alias('ai_policy_register'); // Time accepted filter. $filters[] = (new filter( date::class, 'timeaccepted', new lang_string('dateaccepted', 'core_ai'), $this->get_entity_name(), "{$tablealias}.timeaccepted", )) ->add_joins($this->get_joins()) ->set_limited_operators([ date::DATE_ANY, date::DATE_RANGE, date::DATE_PREVIOUS, date::DATE_CURRENT, ]); return $filters; } } PK\ljjlocal/systemreports/usage.phpnu[. namespace core_ai\reportbuilder\local\systemreports; use core_reportbuilder\system_report; use core_ai\reportbuilder\local\entities\ai_action_register; use core\reportbuilder\local\entities\context; use core_reportbuilder\local\entities\user; /** * AI usage system report. * * @package core_ai * @copyright 2024 David Woloszyn * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class usage extends system_report { #[\Override] protected function initialise(): void { $entitymain = new ai_action_register(); $entitymainalias = $entitymain->get_table_alias('ai_action_register'); $this->set_main_table('ai_action_register', $entitymainalias); $this->add_entity($entitymain); // Join the 'user' entity to our main entity. $entityuser = new user(); $entituseralias = $entityuser->get_table_alias('user'); $this->add_entity($entityuser->add_join( "LEFT JOIN {user} {$entituseralias} ON {$entituseralias}.id = {$entitymainalias}.userid" )); // Join the 'context' entity to our main entity. $entitycontext = new context(); $entitycontextalias = $entitycontext->get_table_alias('context'); $this->add_entity($entitycontext->add_join( "LEFT JOIN {context} {$entitycontextalias} ON {$entitycontextalias}.id = {$entitymainalias}.contextid" )); // Now we can call our helper methods to add the content we want to include in the report. $this->add_columns(); $this->add_filters(); // Set if report can be downloaded. $this->set_downloadable(true, get_string('aiusage', 'core_ai')); } #[\Override] protected function can_view(): bool { return has_capability('moodle/ai:viewaiusagereport', $this->get_context()); } #[\Override] public static function get_name(): string { return get_string('aiusage', 'core_ai'); } /** * Adds the columns we want to display in the report. * * They are all provided by the entities we previously added in the {@see initialise} method, referencing each by their * unique identifier. */ public function add_columns(): void { $this->add_columns_from_entities([ 'ai_action_register:provider', 'ai_action_register:actionname', 'ai_action_register:timecreated', 'ai_action_register:prompttokens', 'ai_action_register:completiontokens', 'ai_action_register:success', 'context:name', 'user:fullnamewithlink', ]); // It's possible to set a default initial sort direction for one column. $this->set_initial_sort_column('ai_action_register:timecreated', SORT_DESC); } /** * Adds the filters we want to display in the report. * * They are all provided by the entities we previously added in the {@see initialise} method, referencing each by their * unique identifier. */ protected function add_filters(): void { $this->add_filters_from_entities([ 'ai_action_register:actionname', 'ai_action_register:provider', 'ai_action_register:timecreated', 'ai_action_register:prompttokens', 'ai_action_register:completiontokens', 'ai_action_register:success', 'context:level', 'user:fullname', ]); } } PK\c)local/systemreports/policy_acceptance.phpnu[. namespace core_ai\reportbuilder\local\systemreports; use core_ai\reportbuilder\local\entities\ai_policy_register; use core_reportbuilder\system_report; use core_reportbuilder\local\entities\user; /** * AI policy acceptance system report. * * @package core_ai * @copyright 2024 David Woloszyn * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class policy_acceptance extends system_report { #[\Override] protected function initialise(): void { $entitymain = new ai_policy_register(); $entitymainalias = $entitymain->get_table_alias('ai_policy_register'); $this->set_main_table('ai_policy_register', $entitymainalias); $this->add_entity($entitymain); // Join the 'user' entity to our main entity. $entityuser = new user(); $entituseralias = $entityuser->get_table_alias('user'); $this->add_entity($entityuser->add_join( "LEFT JOIN {user} {$entituseralias} ON {$entituseralias}.id = {$entitymainalias}.userid" )); // Any columns required by actions should be defined here to ensure they're always available. $this->add_base_fields("{$entitymainalias}.id, {$entitymainalias}.userid"); // Now we can call our helper methods to add the content we want to include in the report. $this->add_columns(); $this->add_filters(); // Set if report can be downloaded. $this->set_downloadable(true, get_string('aipolicyacceptance', 'core_ai')); } #[\Override] protected function can_view(): bool { return has_capability('moodle/ai:viewaipolicyacceptancereport', $this->get_context()); } #[\Override] public static function get_name(): string { return get_string('aipolicyacceptance', 'core_ai'); } /** * Adds the columns we want to display in the report. * * They are all provided by the entities we previously added in the {@see initialise} method, referencing each by their * unique identifier. */ public function add_columns(): void { $columns = [ 'user:fullnamewithlink', 'ai_policy_register:timeaccepted', ]; $this->add_columns_from_entities($columns); // It's possible to set a default initial sort direction for one column. $this->set_initial_sort_column('ai_policy_register:timeaccepted', SORT_DESC); } /** * Adds the filters we want to display in the report. * * They are all provided by the entities we previously added in the {@see initialise} method, referencing each by their * unique identifier. */ protected function add_filters(): void { $filters = [ 'user:fullname', 'ai_policy_register:timeaccepted', ]; $this->add_filters_from_entities($filters); } } PK#\ZjXlocal/entities/context.phpnu[. declare(strict_types=1); namespace core\reportbuilder\local\entities; use core\context_helper; use core_reportbuilder\local\entities\base; use core_reportbuilder\local\filters\{select, text}; use core_reportbuilder\local\report\{column, filter}; use html_writer; use lang_string; use stdClass; /** * Context entity * * @package core * @copyright 2023 Paul Holden * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class context extends base { /** * Database tables that this entity uses * * @return string[] */ protected function get_default_tables(): array { return [ 'context', ]; } /** * The default title for this entity in the list of columns/conditions/filters in the report builder * * @return lang_string */ protected function get_default_entity_title(): lang_string { return new lang_string('context'); } /** * Initialise the entity * * @return base */ public function initialise(): base { $columns = $this->get_all_columns(); foreach ($columns as $column) { $this->add_column($column); } // All the filters defined by the entity can also be used as conditions. $filters = $this->get_all_filters(); foreach ($filters as $filter) { $this ->add_filter($filter) ->add_condition($filter); } return $this; } /** * Returns list of all available columns * * @return column[] */ protected function get_all_columns(): array { global $DB; $contextalias = $this->get_table_alias('context'); // Name. $columns[] = (new column( 'name', new lang_string('contextname'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_TEXT) ->add_fields(context_helper::get_preload_record_columns_sql($contextalias)) // Sorting may not order alphabetically, but will at least group contexts together. ->set_is_sortable(true) ->add_callback(static function($contextid, stdClass $context): string { if ($contextid === null) { return ''; } context_helper::preload_from_record($context); return context_helper::instance_by_id($contextid)->get_context_name(); }); // Link. $columns[] = (new column( 'link', new lang_string('contexturl'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_TEXT) ->add_fields(context_helper::get_preload_record_columns_sql($contextalias)) // Sorting may not order alphabetically, but will at least group contexts together. ->set_is_sortable(true) ->add_callback(static function($contextid, stdClass $context): string { if ($contextid === null) { return ''; } context_helper::preload_from_record($context); $context = context_helper::instance_by_id($contextid); return html_writer::link($context->get_url(), $context->get_context_name()); }); // Level. $columns[] = (new column( 'level', new lang_string('contextlevel'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->add_fields("{$contextalias}.contextlevel") ->set_is_sortable(true) ->add_callback(static function(?string $level): string { if ($level === null) { return ''; } return context_helper::get_level_name((int) $level); }); // Path. $columns[] = (new column( 'path', new lang_string('path'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_TEXT) ->add_field("{$contextalias}.path") ->set_is_sortable(true); // Parent (note we select the parent path in SQL, so that aggregation/grouping is on the parent data itself). $columns[] = (new column( 'parent', new lang_string('contextparent'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_TEXT) // The "path" column looks like "/1/2/3", for context ID 3. In order to select/group by the parent context, we // concatenate a trailing slash (to prevent partial matches, e.g. "/1/2/31"), then replace "/3/" with empty string. ->add_field(" REPLACE( " . $DB->sql_concat("{$contextalias}.path", "'/'") . ", " . $DB->sql_concat("'/'", $DB->sql_cast_to_char("{$contextalias}.id"), "'/'") . ", '' )", 'parent' ) // Sorting may not order alphabetically, but will at least group contexts together. ->set_is_sortable(true) ->add_callback(static function (?string $parent): string { // System level (no parent) or null. if ($parent === '' || $parent === null) { return ''; } $contextids = explode('/', $parent); $contextid = (int) array_pop($contextids); return context_helper::instance_by_id($contextid)->get_context_name(); }); return $columns; } /** * Return list of all available filters * * @return filter[] */ protected function get_all_filters(): array { $contextalias = $this->get_table_alias('context'); // Level. $filters[] = (new filter( select::class, 'level', new lang_string('contextlevel'), $this->get_entity_name(), "{$contextalias}.contextlevel" )) ->add_joins($this->get_joins()) ->set_options_callback(static function(): array { $levels = context_helper::get_all_levels(); return array_map(static function(string $levelclass): string { return $levelclass::get_level_name(); }, $levels); }); // Path. $filters[] = (new filter( text::class, 'path', new lang_string('path'), $this->get_entity_name(), "{$contextalias}.path" )) ->add_joins($this->get_joins()); return $filters; } } PKp\db`==datasource/groups.phpnu[. declare(strict_types=1); namespace core_group\reportbuilder\datasource; use core_group\reportbuilder\local\entities\{grouping, group, group_member}; use core_reportbuilder\datasource; use core_reportbuilder\local\entities\{course, user}; use core_reportbuilder\local\helpers\database; /** * Groups datasource * * @package core_group * @copyright 2022 Paul Holden * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class groups extends datasource { /** * Return user friendly name of the datasource * * @return string */ public static function get_name(): string { return get_string('groups', 'core_group'); } /** * Initialise report */ protected function initialise(): void { $courseentity = new course(); $coursealias = $courseentity->get_table_alias('course'); $this->set_main_table('course', $coursealias); $this->add_entity($courseentity); $paramsiteid = database::generate_param_name(); $this->add_base_condition_sql("{$coursealias}.id != :{$paramsiteid}", [$paramsiteid => SITEID]); // Re-use the context table alias/join from the course entity in subsequent entities. $contextalias = $courseentity->get_table_alias('context'); $this->add_join($courseentity->get_context_join()); // Group entity. $groupentity = (new group()) ->set_table_alias('context', $contextalias); $groupsalias = $groupentity->get_table_alias('groups'); $this->add_entity($groupentity ->add_join("LEFT JOIN {groups} {$groupsalias} ON {$groupsalias}.courseid = {$coursealias}.id")); // Grouping entity. $groupingentity = (new grouping()) ->set_table_alias('context', $contextalias); $groupingsalias = $groupingentity->get_table_alias('groupings'); // Sub-select for all groupings groups. $groupinginnerselect = " SELECT gr.*, grg.groupid FROM {groupings} gr JOIN {groupings_groups} grg ON grg.groupingid = gr.id"; $this->add_entity($groupingentity ->add_joins($groupentity->get_joins()) ->add_join("LEFT JOIN ({$groupinginnerselect}) {$groupingsalias} ON {$groupingsalias}.courseid = {$coursealias}.id AND {$groupingsalias}.groupid = {$groupsalias}.id")); // Group member entity. $groupmemberentity = new group_member(); $groupsmembersalias = $groupmemberentity->get_table_alias('groups_members'); $this->add_entity($groupmemberentity ->add_joins($groupentity->get_joins()) ->add_join("LEFT JOIN {groups_members} {$groupsmembersalias} ON {$groupsmembersalias}.groupid = {$groupsalias}.id")); // User entity. $userentity = new user(); $useralias = $userentity->get_table_alias('user'); $this->add_entity($userentity ->add_joins($groupmemberentity->get_joins()) ->add_join("LEFT JOIN {user} {$useralias} ON {$useralias}.id = {$groupsmembersalias}.userid")); // Add all elements from entities to be available in custom reports. $this->add_all_from_entities(); } /** * Return the columns that will be added to the report as part of default setup * * @return string[] */ public function get_default_columns(): array { return [ 'course:coursefullnamewithlink', 'group:name', 'user:fullname', ]; } /** * Return the column sorting that will be added to the report upon creation * * @return int[] */ public function get_default_column_sorting(): array { return [ 'course:coursefullnamewithlink' => SORT_ASC, 'group:name' => SORT_ASC, 'user:fullname' => SORT_ASC, ]; } /** * Return the filters that will be added to the report as part of default setup * * @return string[] */ public function get_default_filters(): array { return [ 'course:fullname', 'group:name', ]; } /** * Return the conditions that will be added to the report as part of default setup * * @return string[] */ public function get_default_conditions(): array { return [ 'course:fullname', 'group:name', ]; } } PKp\nnY&&local/entities/grouping.phpnu[. declare(strict_types=1); namespace core_group\reportbuilder\local\entities; use context_course; use context_helper; use lang_string; use stdClass; use core_reportbuilder\local\entities\base; use core_reportbuilder\local\filters\{date, text}; use core_reportbuilder\local\helpers\{custom_fields, format}; use core_reportbuilder\local\report\{column, filter}; /** * Grouping entity * * @package core_group * @copyright 2022 Paul Holden * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class grouping extends base { /** * Database tables that this entity uses * * @return string[] */ protected function get_default_tables(): array { return [ 'context', 'groupings', ]; } /** * The default title for this entity * * @return lang_string */ protected function get_default_entity_title(): lang_string { return new lang_string('grouping', 'core_group'); } /** * Initialise the entity * * @return base */ public function initialise(): base { $groupingsalias = $this->get_table_alias('groupings'); $customfields = (new custom_fields( "{$groupingsalias}.id", $this->get_entity_name(), 'core_group', 'grouping', )) ->add_joins($this->get_joins()); $columns = array_merge($this->get_all_columns(), $customfields->get_columns()); foreach ($columns as $column) { $this->add_column($column); } // All the filters defined by the entity can also be used as conditions. $filters = array_merge($this->get_all_filters(), $customfields->get_filters()); foreach ($filters as $filter) { $this ->add_filter($filter) ->add_condition($filter); } return $this; } /** * Returns list of all available columns * * @return column[] */ protected function get_all_columns(): array { $contextalias = $this->get_table_alias('context'); $groupingsalias = $this->get_table_alias('groupings'); // Name column. $columns[] = (new column( 'name', new lang_string('name'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_TEXT) ->add_fields("{$groupingsalias}.name, {$groupingsalias}.courseid") ->add_fields(context_helper::get_preload_record_columns_sql($contextalias)) ->set_is_sortable(true) ->set_callback(static function($name, stdClass $grouping): string { if ($name === null) { return ''; } context_helper::preload_from_record($grouping); $context = context_course::instance($grouping->courseid); return format_string($grouping->name, true, ['context' => $context]); }); // ID number column. $columns[] = (new column( 'idnumber', new lang_string('idnumber'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_TEXT) ->add_fields("{$groupingsalias}.idnumber") ->set_is_sortable(true); // Description column. $columns[] = (new column( 'description', new lang_string('description'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_LONGTEXT) ->add_field("{$groupingsalias}.description") ->add_fields("{$groupingsalias}.descriptionformat, {$groupingsalias}.id, {$groupingsalias}.courseid") ->add_fields(context_helper::get_preload_record_columns_sql($contextalias)) ->set_is_sortable(true) ->set_callback(static function(?string $description, stdClass $grouping): string { global $CFG; if ($description === null) { return ''; } require_once("{$CFG->libdir}/filelib.php"); context_helper::preload_from_record($grouping); $context = context_course::instance($grouping->courseid); $description = file_rewrite_pluginfile_urls($description, 'pluginfile.php', $context->id, 'grouping', 'description', $grouping->id); return format_text($description, $grouping->descriptionformat, ['context' => $context]); }); // Time created column. $columns[] = (new column( 'timecreated', new lang_string('timecreated', 'core_reportbuilder'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_TIMESTAMP) ->add_fields("{$groupingsalias}.timecreated") ->set_is_sortable(true) ->set_callback([format::class, 'userdate']); // Time modified column. $columns[] = (new column( 'timemodified', new lang_string('timemodified', 'core_reportbuilder'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_TIMESTAMP) ->add_fields("{$groupingsalias}.timemodified") ->set_is_sortable(true) ->set_callback([format::class, 'userdate']); return $columns; } /** * Return list of all available filters * * @return filter[] */ protected function get_all_filters(): array { $groupingsalias = $this->get_table_alias('groupings'); // Name filter. $filters[] = (new filter( text::class, 'name', new lang_string('name'), $this->get_entity_name(), "{$groupingsalias}.name" )) ->add_joins($this->get_joins()); // ID number filter. $filters[] = (new filter( text::class, 'idnumber', new lang_string('idnumber'), $this->get_entity_name(), "{$groupingsalias}.idnumber" )) ->add_joins($this->get_joins()); // Time created filter. $filters[] = (new filter( date::class, 'timecreated', new lang_string('timecreated', 'core_reportbuilder'), $this->get_entity_name(), "{$groupingsalias}.timecreated" )) ->add_joins($this->get_joins()); return $filters; } } PKp\*local/entities/group_member.phpnu[. declare(strict_types=1); namespace core_group\reportbuilder\local\entities; use core_reportbuilder\local\filters\date; use lang_string; use core_reportbuilder\local\entities\base; use core_reportbuilder\local\helpers\format; use core_reportbuilder\local\report\{column, filter}; /** * Group member entity * * @package core_group * @copyright 2022 Paul Holden * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class group_member extends base { /** * Database tables that this entity uses * * @return string[] */ protected function get_default_tables(): array { return [ 'groups_members', ]; } /** * The default title for this entity * * @return lang_string */ protected function get_default_entity_title(): lang_string { return new lang_string('groupmember', 'core_group'); } /** * Initialise the entity * * @return base */ public function initialise(): base { $columns = $this->get_all_columns(); foreach ($columns as $column) { $this->add_column($column); } // All the filters defined by the entity can also be used as conditions. $filters = $this->get_all_filters(); foreach ($filters as $filter) { $this ->add_filter($filter) ->add_condition($filter); } return $this; } /** * Returns list of all available columns * * @return column[] */ protected function get_all_columns(): array { $groupsmembersalias = $this->get_table_alias('groups_members'); // Time added column. $columns[] = (new column( 'timeadded', new lang_string('timeadded', 'core_reportbuilder'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_TIMESTAMP) ->add_fields("{$groupsmembersalias}.timeadded") ->set_is_sortable(true) ->set_callback([format::class, 'userdate']); // Component column. $columns[] = (new column( 'component', new lang_string('plugin', 'core'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_TEXT) ->add_fields("{$groupsmembersalias}.component") ->set_is_sortable(true); return $columns; } /** * Return list of all available filters * * @return filter[] */ protected function get_all_filters(): array { $groupsmembersalias = $this->get_table_alias('groups_members'); // Time added filter. $filters[] = (new filter( date::class, 'timeadded', new lang_string('timeadded', 'core_reportbuilder'), $this->get_entity_name(), "{$groupsmembersalias}.timeadded" )) ->add_joins($this->get_joins()); return $filters; } } PKp\Di++local/entities/group.phpnu[. declare(strict_types=1); namespace core_group\reportbuilder\local\entities; use context_course; use context_helper; use html_writer; use lang_string; use moodle_url; use stdClass; use core_reportbuilder\local\entities\base; use core_reportbuilder\local\filters\{boolean_select, date, select, text}; use core_reportbuilder\local\helpers\{custom_fields, format}; use core_reportbuilder\local\report\{column, filter}; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once("{$CFG->libdir}/grouplib.php"); /** * Group entity * * @package core_group * @copyright 2022 Paul Holden * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class group extends base { /** * Database tables that this entity uses * * @return string[] */ protected function get_default_tables(): array { return [ 'context', 'groups', ]; } /** * The default title for this entity * * @return lang_string */ protected function get_default_entity_title(): lang_string { return new lang_string('group', 'core_group'); } /** * Initialise the entity * * @return base */ public function initialise(): base { $groupsalias = $this->get_table_alias('groups'); $customfields = (new custom_fields( "{$groupsalias}.id", $this->get_entity_name(), 'core_group', 'group', )) ->add_joins($this->get_joins()); $columns = array_merge($this->get_all_columns(), $customfields->get_columns()); foreach ($columns as $column) { $this->add_column($column); } // All the filters defined by the entity can also be used as conditions. $filters = array_merge($this->get_all_filters(), $customfields->get_filters()); foreach ($filters as $filter) { $this ->add_filter($filter) ->add_condition($filter); } return $this; } /** * Returns list of all available columns * * @return column[] */ protected function get_all_columns(): array { $contextalias = $this->get_table_alias('context'); $groupsalias = $this->get_table_alias('groups'); // Name column. $columns[] = (new column( 'name', new lang_string('name'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_TEXT) ->add_fields("{$groupsalias}.name, {$groupsalias}.courseid") ->add_fields(context_helper::get_preload_record_columns_sql($contextalias)) ->set_is_sortable(true) ->set_callback(static function($name, stdClass $group): string { if ($name === null) { return ''; } context_helper::preload_from_record($group); $context = context_course::instance($group->courseid); return format_string($group->name, true, ['context' => $context]); }); // ID number column. $columns[] = (new column( 'idnumber', new lang_string('idnumber'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_TEXT) ->add_fields("{$groupsalias}.idnumber") ->set_is_sortable(true); // Description column. $columns[] = (new column( 'description', new lang_string('description'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_LONGTEXT) ->add_fields("{$groupsalias}.description, {$groupsalias}.descriptionformat, {$groupsalias}.id, {$groupsalias}.courseid") ->add_fields(context_helper::get_preload_record_columns_sql($contextalias)) ->set_is_sortable(true) ->set_callback(static function(?string $description, stdClass $group): string { global $CFG; if ($description === null) { return ''; } require_once("{$CFG->libdir}/filelib.php"); context_helper::preload_from_record($group); $context = context_course::instance($group->courseid); $description = file_rewrite_pluginfile_urls($description, 'pluginfile.php', $context->id, 'group', 'description', $group->id); return format_text($description, $group->descriptionformat, ['context' => $context]); }); // Enrolment key column. $columns[] = (new column( 'enrolmentkey', new lang_string('enrolmentkey', 'core_group'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_TEXT) ->add_fields("{$groupsalias}.enrolmentkey") ->set_is_sortable(true); // Visibility column. $columns[] = (new column( 'visibility', new lang_string('visibilityshort', 'core_group'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->add_fields("{$groupsalias}.visibility") ->set_is_sortable(true) ->set_callback(static function(?string $visibility): string { if ($visibility === null) { return ''; } $options = [ GROUPS_VISIBILITY_ALL => new lang_string('visibilityall', 'core_group'), GROUPS_VISIBILITY_MEMBERS => new lang_string('visibilitymembers', 'core_group'), GROUPS_VISIBILITY_OWN => new lang_string('visibilityown', 'core_group'), GROUPS_VISIBILITY_NONE => new lang_string('visibilitynone', 'core_group'), ]; return (string) ($options[(int) $visibility] ?? $visibility); }); // Participation column. $columns[] = (new column( 'participation', new lang_string('participationshort', 'core_group'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_BOOLEAN) ->add_fields("{$groupsalias}.participation") ->set_is_sortable(true) ->set_callback([format::class, 'boolean_as_text']); // Picture column. $columns[] = (new column( 'picture', new lang_string('picture'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->add_fields("{$groupsalias}.picture, {$groupsalias}.id, {$contextalias}.id AS contextid") ->set_is_sortable(false) ->set_callback(static function($value, stdClass $group): string { if (empty($group->picture)) { return ''; } $pictureurl = moodle_url::make_pluginfile_url($group->contextid, 'group', 'icon', $group->id, '/', 'f2'); $pictureurl->param('rev', $group->picture); return html_writer::img($pictureurl, ''); }); // Time created column. $columns[] = (new column( 'timecreated', new lang_string('timecreated', 'core_reportbuilder'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_TIMESTAMP) ->add_fields("{$groupsalias}.timecreated") ->set_is_sortable(true) ->set_callback([format::class, 'userdate']); // Time modified column. $columns[] = (new column( 'timemodified', new lang_string('timemodified', 'core_reportbuilder'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_TIMESTAMP) ->add_fields("{$groupsalias}.timemodified") ->set_is_sortable(true) ->set_callback([format::class, 'userdate']); return $columns; } /** * Return list of all available filters * * @return filter[] */ protected function get_all_filters(): array { $groupsalias = $this->get_table_alias('groups'); // Name filter. $filters[] = (new filter( text::class, 'name', new lang_string('name'), $this->get_entity_name(), "{$groupsalias}.name" )) ->add_joins($this->get_joins()); // ID number filter. $filters[] = (new filter( text::class, 'idnumber', new lang_string('idnumber'), $this->get_entity_name(), "{$groupsalias}.idnumber" )) ->add_joins($this->get_joins()); // Visibility filter. $filters[] = (new filter( select::class, 'visibility', new lang_string('visibilityshort', 'core_group'), $this->get_entity_name(), "{$groupsalias}.visibility" )) ->add_joins($this->get_joins()) ->set_options([ GROUPS_VISIBILITY_ALL => new lang_string('visibilityall', 'core_group'), GROUPS_VISIBILITY_MEMBERS => new lang_string('visibilitymembers', 'core_group'), GROUPS_VISIBILITY_OWN => new lang_string('visibilityown', 'core_group'), GROUPS_VISIBILITY_NONE => new lang_string('visibilitynone', 'core_group'), ]); // Participation filter. $filters[] = (new filter( boolean_select::class, 'participation', new lang_string('participationshort', 'core_group'), $this->get_entity_name(), "{$groupsalias}.participation" )) ->add_joins($this->get_joins()); // Time created filter. $filters[] = (new filter( date::class, 'timecreated', new lang_string('timecreated', 'core_reportbuilder'), $this->get_entity_name(), "{$groupsalias}.timecreated" )) ->add_joins($this->get_joins()); return $filters; } } PKp\y KKdatasource/groups_test.phpnu[. declare(strict_types=1); namespace core_group\reportbuilder\datasource; use core_customfield_generator; use core_reportbuilder_generator; use core_reportbuilder\local\filters\{boolean_select, date, select, text}; use core_reportbuilder\tests\core_reportbuilder_testcase; /** * Unit tests for groups datasource * * @package core_group * @covers \core_group\reportbuilder\datasource\groups * @copyright 2022 Paul Holden * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class groups_test extends core_reportbuilder_testcase { /** * Test default datasource */ public function test_datasource_default(): void { $this->resetAfterTest(); $course = $this->getDataGenerator()->create_course(); $userone = $this->getDataGenerator()->create_and_enrol($course, 'student', ['firstname' => 'Zoe']); $usertwo = $this->getDataGenerator()->create_and_enrol($course, 'student', ['firstname' => 'Amy']); $groupone = $this->getDataGenerator()->create_group(['courseid' => $course->id, 'name' => 'Zebras']); $grouptwo = $this->getDataGenerator()->create_group(['courseid' => $course->id, 'name' => 'Aardvarks']); $this->getDataGenerator()->create_group_member(['groupid' => $groupone->id, 'userid' => $userone->id]); $this->getDataGenerator()->create_group_member(['groupid' => $groupone->id, 'userid' => $usertwo->id]); /** @var core_reportbuilder_generator $generator */ $generator = $this->getDataGenerator()->get_plugin_generator('core_reportbuilder'); $report = $generator->create_report(['name' => 'Groups', 'source' => groups::class, 'default' => 1]); $content = $this->get_custom_report_content($report->get('id')); // Default columns are course, group, user. Sorted by each. $courseurl = course_get_url($course); $this->assertEquals([ ["{$course->fullname}", $grouptwo->name, ''], ["{$course->fullname}", $groupone->name, fullname($usertwo)], ["{$course->fullname}", $groupone->name, fullname($userone)], ], array_map('array_values', $content)); } /** * Test datasource groupings reports */ public function test_datasource_groupings(): void { $this->resetAfterTest(); $course = $this->getDataGenerator()->create_course(); // Create group, add to grouping. $groupone = $this->getDataGenerator()->create_group(['courseid' => $course->id, 'name' => 'Group A']); $grouping = $this->getDataGenerator()->create_grouping(['courseid' => $course->id]); $this->getDataGenerator()->create_grouping_group(['groupingid' => $grouping->id, 'groupid' => $groupone->id]); // Create second group, no grouping. $grouptwo = $this->getDataGenerator()->create_group(['courseid' => $course->id, 'name' => 'Group B']); /** @var core_reportbuilder_generator $generator */ $generator = $this->getDataGenerator()->get_plugin_generator('core_reportbuilder'); $report = $generator->create_report(['name' => 'Groups', 'source' => groups::class, 'default' => 0]); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'course:fullname']); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'grouping:name']); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'group:name', 'sortenabled' => 1]); $content = $this->get_custom_report_content($report->get('id')); $this->assertCount(2, $content); $this->assertEquals([ [ $course->fullname, $grouping->name, $groupone->name, ], [ $course->fullname, null, $grouptwo->name, ], ], array_map('array_values', $content)); } /** * Test datasource columns that aren't added by default */ public function test_datasource_non_default_columns(): void { $this->resetAfterTest(); $this->setAdminUser(); $course = $this->getDataGenerator()->create_course(); $user = $this->getDataGenerator()->create_and_enrol($course, 'student'); /** @var core_customfield_generator $generator */ $generator = $this->getDataGenerator()->get_plugin_generator('core_customfield'); // Create group with custom field, and single group member. $groupfieldcategory = $generator->create_category(['component' => 'core_group', 'area' => 'group']); $generator->create_field(['categoryid' => $groupfieldcategory->get('id'), 'shortname' => 'hi']); $group = $this->getDataGenerator()->create_group([ 'courseid' => $course->id, 'idnumber' => 'G101', 'enrolmentkey' => 'S', 'description' => 'My group', 'customfield_hi' => 'Hello', ]); $this->getDataGenerator()->create_group_member(['userid' => $user->id, 'groupid' => $group->id]); // Create grouping with custom field, and single group. $groupingfieldcategory = $generator->create_category(['component' => 'core_group', 'area' => 'grouping']); $generator->create_field(['categoryid' => $groupingfieldcategory->get('id'), 'shortname' => 'bye']); $grouping = $this->getDataGenerator()->create_grouping([ 'courseid' => $course->id, 'idnumber' => 'GR101', 'description' => 'My grouping', 'customfield_bye' => 'Goodbye', ]); $this->getDataGenerator()->create_grouping_group(['groupingid' => $grouping->id, 'groupid' => $group->id]); /** @var core_reportbuilder_generator $generator */ $generator = $this->getDataGenerator()->get_plugin_generator('core_reportbuilder'); $report = $generator->create_report(['name' => 'Groups', 'source' => groups::class, 'default' => 0]); // Course (just to test join). $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'course:shortname']); // Group. $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'group:idnumber']); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'group:description']); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'group:enrolmentkey']); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'group:visibility']); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'group:participation']); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'group:picture']); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'group:timecreated']); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'group:timemodified']); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'group:customfield_hi']); // Grouping. $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'grouping:name']); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'grouping:idnumber']); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'grouping:description']); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'grouping:timecreated']); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'grouping:timemodified']); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'grouping:customfield_bye']); // Group member. $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'group_member:timeadded']); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'group_member:component']); // User (just to test join). $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'user:username']); $content = $this->get_custom_report_content($report->get('id')); $this->assertCount(1, $content); [ $courseshortname, $groupidnumber, $groupdescription, $groupenrolmentkey, $groupvisibility, $groupparticipation, $grouppicture, $grouptimecreated, $grouptimemodified, $groupcustomfield, $groupingname, $groupingidnumber, $groupingdescription, $groupingtimecreated, $groupingtimemodified, $groupingcustomfield, $groupmembertimeadded, $groupmemebercomponent, $userusername, ] = array_values($content[0]); $this->assertEquals($course->shortname, $courseshortname); $this->assertEquals('G101', $groupidnumber); $this->assertEquals(format_text($group->description), $groupdescription); $this->assertEquals('S', $groupenrolmentkey); $this->assertEquals('Visible', $groupvisibility); $this->assertEquals('Yes', $groupparticipation); $this->assertEmpty($grouppicture); $this->assertNotEmpty($grouptimecreated); $this->assertNotEmpty($grouptimemodified); $this->assertEquals('Hello', $groupcustomfield); $this->assertEquals($grouping->name, $groupingname); $this->assertEquals('GR101', $groupingidnumber); $this->assertEquals(format_text($grouping->description), $groupingdescription); $this->assertNotEmpty($groupingtimecreated); $this->assertNotEmpty($groupingtimemodified); $this->assertEquals('Goodbye', $groupingcustomfield); $this->assertNotEmpty($groupmembertimeadded); $this->assertEmpty($groupmemebercomponent); $this->assertEquals($user->username, $userusername); } /** * Data provider for {@see test_datasource_filters} * * @return array[] */ public static function datasource_filters_provider(): array { return [ // Course (just to test join). 'Filter course name' => ['course:fullname', [ 'course:fullname_operator' => text::IS_EQUAL_TO, 'course:fullname_value' => 'Test course', ], true], 'Filter course name (no match)' => ['course:fullname', [ 'course:fullname_operator' => text::IS_NOT_EQUAL_TO, 'course:fullname_value' => 'Test course', ], false], // Group. 'Filter group name' => ['group:name', [ 'group:name_operator' => text::IS_EQUAL_TO, 'group:name_value' => 'Test group', ], true], 'Filter group name (no match)' => ['group:name', [ 'group:name_operator' => text::IS_NOT_EQUAL_TO, 'group:name_value' => 'Test group', ], false], 'Filter group idnumber' => ['group:idnumber', [ 'group:idnumber_operator' => text::IS_EQUAL_TO, 'group:idnumber_value' => 'G101', ], true], 'Filter group idnumber (no match)' => ['group:idnumber', [ 'group:idnumber_operator' => text::IS_NOT_EQUAL_TO, 'group:idnumber_value' => 'G101', ], false], 'Filter group visibility' => ['group:visibility', [ 'group:visibility_operator' => select::EQUAL_TO, 'group:visibility_value' => 0, // Visible to everyone. ], true], 'Filter group visibility (no match)' => ['group:visibility', [ 'group:visibility_operator' => select::EQUAL_TO, 'group:visibility_value' => 1, // Visible to members only. ], false], 'Filter group participation' => ['group:participation', [ 'group:participation_operator' => boolean_select::CHECKED, ], true], 'Filter group participation (no match)' => ['group:participation', [ 'group:participation_operator' => boolean_select::NOT_CHECKED, ], false], 'Filter group time created' => ['group:timecreated', [ 'group:timecreated_operator' => date::DATE_RANGE, 'group:timecreated_from' => 1622502000, ], true], 'Filter group time created (no match)' => ['group:timecreated', [ 'group:timecreated_operator' => date::DATE_RANGE, 'group:timecreated_to' => 1622502000, ], false], // Grouping. 'Filter grouping name' => ['grouping:name', [ 'grouping:name_operator' => text::IS_EQUAL_TO, 'grouping:name_value' => 'Test grouping', ], true], 'Filter grouping name (no match)' => ['grouping:name', [ 'grouping:name_operator' => text::IS_NOT_EQUAL_TO, 'grouping:name_value' => 'Test grouping', ], false], 'Filter grouping idnumber' => ['grouping:idnumber', [ 'grouping:idnumber_operator' => text::IS_EQUAL_TO, 'grouping:idnumber_value' => 'GR101', ], true], 'Filter grouping idnumber (no match)' => ['grouping:idnumber', [ 'grouping:idnumber_operator' => text::IS_NOT_EQUAL_TO, 'grouping:idnumber_value' => 'GR101', ], false], 'Filter grouping time created' => ['grouping:timecreated', [ 'grouping:timecreated_operator' => date::DATE_RANGE, 'grouping:timecreated_from' => 1622502000, ], true], 'Filter grouping time created (no match)' => ['grouping:timecreated', [ 'grouping:timecreated_operator' => date::DATE_RANGE, 'grouping:timecreated_to' => 1622502000, ], false], // Group member. 'Filter group member time added' => ['group_member:timeadded', [ 'group_member:timeadded_operator' => date::DATE_RANGE, 'group_member:timeadded_from' => 1622502000, ], true], 'Filter group member time added (no match)' => ['group_member:timeadded', [ 'group_member:timeadded_operator' => date::DATE_RANGE, 'group_member:timeadded_to' => 1622502000, ], false], // User (just to test join). 'Filter user username' => ['user:username', [ 'user:username_operator' => text::IS_EQUAL_TO, 'user:username_value' => 'testuser', ], true], 'Filter user username (no match)' => ['user:username', [ 'user:username_operator' => text::IS_NOT_EQUAL_TO, 'user:username_value' => 'testuser', ], false], ]; } /** * Test datasource filters * * @param string $filtername * @param array $filtervalues * @param bool $expectmatch * * @dataProvider datasource_filters_provider */ public function test_datasource_filters( string $filtername, array $filtervalues, bool $expectmatch ): void { $this->resetAfterTest(); $course = $this->getDataGenerator()->create_course(['fullname' => 'Test course']); $user = $this->getDataGenerator()->create_and_enrol($course, 'student', ['username' => 'testuser']); $group = $this->getDataGenerator()->create_group(['courseid' => $course->id, 'idnumber' => 'G101', 'name' => 'Test group']); $this->getDataGenerator()->create_group_member(['userid' => $user->id, 'groupid' => $group->id]); $grouping = $this->getDataGenerator()->create_grouping(['courseid' => $course->id, 'idnumber' => 'GR101', 'name' => 'Test grouping']); $this->getDataGenerator()->create_grouping_group(['groupingid' => $grouping->id, 'groupid' => $group->id]); /** @var core_reportbuilder_generator $generator */ $generator = $this->getDataGenerator()->get_plugin_generator('core_reportbuilder'); // Create report containing single column, and given filter. $report = $generator->create_report(['name' => 'Groups', 'source' => groups::class, 'default' => 0]); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'group:idnumber']); // Add filter, set it's values. $generator->create_filter(['reportid' => $report->get('id'), 'uniqueidentifier' => $filtername]); $content = $this->get_custom_report_content($report->get('id'), 0, $filtervalues); if ($expectmatch) { $this->assertCount(1, $content); $this->assertEquals('G101', reset($content[0])); } else { $this->assertEmpty($content); } } /** * Stress test datasource * * In order to execute this test PHPUNIT_LONGTEST should be defined as true in phpunit.xml or directly in config.php */ public function test_stress_datasource(): void { if (!PHPUNIT_LONGTEST) { $this->markTestSkipped('PHPUNIT_LONGTEST is not defined'); } $this->resetAfterTest(); $course = $this->getDataGenerator()->create_course(['fullname' => 'Test course']); $user = $this->getDataGenerator()->create_and_enrol($course, 'student', ['username' => 'testuser']); $group = $this->getDataGenerator()->create_group(['courseid' => $course->id, 'idnumber' => 'G101', 'name' => 'Test group']); $this->getDataGenerator()->create_group_member(['userid' => $user->id, 'groupid' => $group->id]); $grouping = $this->getDataGenerator()->create_grouping(['courseid' => $course->id, 'idnumber' => 'GR101', 'name' => 'Test grouping']); $this->getDataGenerator()->create_grouping_group(['groupingid' => $grouping->id, 'groupid' => $group->id]); $this->datasource_stress_test_columns(groups::class); $this->datasource_stress_test_columns_aggregation(groups::class); $this->datasource_stress_test_conditions(groups::class, 'group:idnumber'); } } PK$}\COJpJpdatasource/users_test.phpnu[PKl\|̐xxpdatasource/notes.phpnu[PKl\QZ88Olocal/entities/note.phpnu[PK:\qݓBB Νdatasource/competencies_test.phpnu[PK\2Clocal/entities/service.phpnu[PK\i::local/entities/token.phpnu[PK\ >!>!Glocal/systemreports/tokens.phpnu[PK)\PN 'datasource/comments.phpnu[PK)\u뺤5local/entities/comment.phpnu[PK)\_aa Nlocal/systemreports/comments.phpnu[PK;\_`^^datasource/categories.phpnu[PK;\T~xCpdatasource/courses.phpnu[PK;\! @##datasource/participants.phpnu[PK;\ Qlocal/formatters/completion.phpnu[PK;\TLZ Z Tlocal/formatters/enrolment.phpnu[PK;\·l5l5local/entities/completion.phpnu[PK;\(("local/entities/course_category.phpnu[PK;\-nnlocal/entities/access.phpnu[PK;\X2[,local/entities/enrolment.phpnu[PKx\ӇQLdatasource/files.phpnu[PKx\/l66[local/entities/file.phpnu[PK͐\I) ) local/entities/enrol.phpnu[PK%\<eWdatasource/cohorts.phpnu[PK%\(44Xlocal/entities/cohort.phpnu[PK%\sĸw w 8local/entities/cohort_member.phpnu[PK%\#Ȝ(*-*-local/systemreports/cohorts.phpnu[PK%\xxx3audience/cohortmember.phpnu[PKO\|a)a)9Idatasource/notes_test.phpnu[PKe\YCYCrdatasource/files_test.phpnu[PK\**00datasource/cohorts_test.phpnu[PK\9;;;audience/cohortmember_test.phpnu[PK-\]00 datasource/categories_test.phpnu[PK-\(=PP>4datasource/courses_test.phpnu[PK-\`"__ ndatasource/participants_test.phpnu[PK7\qK__datasource/users.phpnu[PK`\tdatasource/badges.phpnu[PK`\=I.55local/entities/badge.phpnu[PK`\(iFlocal/entities/badge_issued.phpnu[PK`\)"Ylocal/systemreports/recipients.phpnu[PK`\%klocal/systemreports/course_badges.phpnu[PK`\U/88 local/systemreports/badges.phpnu[PK\A,,% local/entities/ai_action_register.phpnu[PK\ %local/entities/ai_policy_register.phpnu[PK\ljj local/systemreports/usage.phpnu[PK\c) local/systemreports/policy_acceptance.phpnu[PK#\ZjX)local/entities/context.phpnu[PKp\db`==67datasource/groups.phpnu[PKp\nnY&&Klocal/entities/grouping.phpnu[PKp\*)ilocal/entities/group_member.phpnu[PKp\Di++Vxlocal/entities/group.phpnu[PKp\y KK]datasource/groups_test.phpnu[PK33>N