ÿØÿà 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-Ї Ç home/smkb8269/public_html/lms.smk-almanshuriyah.sch.id/report/courseoverview/locallib.php000064400000005751152266400120025563 0ustar00. /** * This file contains functions used by the course overview report. * * @package report_courseoverview * @copyright 2016 Simey Lameze * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die; require_once('../../config.php'); require_once($CFG->dirroot . '/lib/statslib.php'); /** * Gather course overview data and print the chart. * * @param int $report represents the report type field on the course overview report filter. * @param int $time represents the time period field on the course overview report filter. * @param int $numcourses represents the number of courses field on the course overview report filter. * @return void */ function report_courseoverview_print_chart($report, $time, $numcourses) { global $DB, $OUTPUT, $PAGE; $param = stats_get_parameters($time, $report, SITEID, STATS_MODE_RANKED); if (!empty($param->sql)) { $sql = $param->sql; } else { $sql = "SELECT courseid, $param->fields FROM {" . 'stats_' . $param->table . "} WHERE timeend >= $param->timeafter AND stattype = 'activity' AND roleid = 0 GROUP BY courseid $param->extras ORDER BY $param->orderby"; } $courses = $DB->get_records_sql($sql, $param->params, 0, $numcourses); if (empty($courses)) { $PAGE->set_url('/report/courseoverview/index.php'); throw new \moodle_exception('statsnodata', 'error', $PAGE->url->out()); } $data = []; $i = 0; foreach ($courses as $c) { $data['labels'][$i] = $DB->get_field('course', 'shortname', array('id' => $c->courseid)); // Line3 represents the third column of the report table except for the most active users report. // It is a float number and can be participation radio or activity per user. if (isset($c->line3)) { $data['series'][$i] = round($c->line3, 2); } else { $data['series'][$i] = $c->{$param->graphline}; } $i++; } $chart = new \core\chart_bar(); $series = new \core\chart_series($param->{$param->graphline}, $data['series']); $chart->add_series($series); $chart->set_labels($data['labels']); echo $OUTPUT->render($chart); } home/smkb8269/public_html/lms.smk-almanshuriyah.sch.id/repository/url/locallib.php000064400000061727152266417740024227 0ustar00 '!%3A!ui', "/" => '!%2F!ui', "?" => '!%3F!ui', "#" => '!%23!ui', "[" => '!%5B!ui', "]" => '!%5D!ui', "@" => '!%40!ui', "!" => '!%21!ui', "$" => '!%24!ui', "&" => '!%26!ui', "'" => '!%27!ui', "(" => '!%28!ui', ")" => '!%29!ui', "*" => '!%2A!ui', "+" => '!%2B!ui', "," => '!%2C!ui', ";" => '!%3B!ui', "=" => '!%3D!ui', "%" => '!%25!ui', ); $url = rawurlencode($url); $url = preg_replace(array_values($reserved), array_keys($reserved), $url); return $url; } /** * Extract URLs from a web page. * * URLs are extracted from a long list of tags and attributes as defined * by the HTML 2.0, HTML 3.2, HTML 4.01, and draft HTML 5.0 specifications. * URLs are also extracted from tags and attributes that are common * extensions of HTML, from the draft Forms 2.0 specification, from XHTML, * and from WML 1.3 and 2.0. * * The function returns an associative array of associative arrays of * arrays of URLs. The outermost array's keys are the tag (element) name, * such as "a" for or "img" for . The values for these entries * are associative arrays where the keys are attribute names for those * tags, such as "href" for . Finally, the values for * those arrays are URLs found in those tags and attributes throughout * the text. * * Parameters: * text the UTF-8 text to scan * * Return values: * an associative array where keys are tags and values are an * associative array where keys are attributes and values are * an array of URLs. * * See: * http://nadeausoftware.com/articles/2008/01/php_tip_how_extract_urls_web_page */ function extract_html_urls( $text ) { $match_elements = array( // HTML array('element'=>'a', 'attribute'=>'href'), // 2.0 array('element'=>'a', 'attribute'=>'urn'), // 2.0 array('element'=>'base', 'attribute'=>'href'), // 2.0 array('element'=>'form', 'attribute'=>'action'), // 2.0 array('element'=>'img', 'attribute'=>'src'), // 2.0 array('element'=>'link', 'attribute'=>'href'), // 2.0 array('element'=>'applet', 'attribute'=>'code'), // 3.2 array('element'=>'applet', 'attribute'=>'codebase'), // 3.2 array('element'=>'area', 'attribute'=>'href'), // 3.2 array('element'=>'body', 'attribute'=>'background'), // 3.2 array('element'=>'img', 'attribute'=>'usemap'), // 3.2 array('element'=>'input', 'attribute'=>'src'), // 3.2 array('element'=>'applet', 'attribute'=>'archive'), // 4.01 array('element'=>'applet', 'attribute'=>'object'), // 4.01 array('element'=>'blockquote', 'attribute'=>'cite'), // 4.01 array('element'=>'del', 'attribute'=>'cite'), // 4.01 array('element'=>'frame', 'attribute'=>'longdesc'), // 4.01 array('element'=>'frame', 'attribute'=>'src'), // 4.01 array('element'=>'head', 'attribute'=>'profile'), // 4.01 array('element'=>'iframe', 'attribute'=>'longdesc'), // 4.01 array('element'=>'iframe', 'attribute'=>'src'), // 4.01 array('element'=>'img', 'attribute'=>'longdesc'), // 4.01 array('element'=>'input', 'attribute'=>'usemap'), // 4.01 array('element'=>'ins', 'attribute'=>'cite'), // 4.01 array('element'=>'object', 'attribute'=>'archive'), // 4.01 array('element'=>'object', 'attribute'=>'classid'), // 4.01 array('element'=>'object', 'attribute'=>'codebase'), // 4.01 array('element'=>'object', 'attribute'=>'data'), // 4.01 array('element'=>'object', 'attribute'=>'usemap'), // 4.01 array('element'=>'q', 'attribute'=>'cite'), // 4.01 array('element'=>'script', 'attribute'=>'src'), // 4.01 array('element'=>'audio', 'attribute'=>'src'), // 5.0 array('element'=>'command', 'attribute'=>'icon'), // 5.0 array('element'=>'embed', 'attribute'=>'src'), // 5.0 array('element'=>'event-source','attribute'=>'src'), // 5.0 array('element'=>'html', 'attribute'=>'manifest'), // 5.0 array('element'=>'source', 'attribute'=>'src'), // 5.0 array('element'=>'video', 'attribute'=>'src'), // 5.0 array('element'=>'video', 'attribute'=>'poster'), // 5.0 array('element'=>'bgsound', 'attribute'=>'src'), // Extension array('element'=>'body', 'attribute'=>'credits'), // Extension array('element'=>'body', 'attribute'=>'instructions'), // Extension array('element'=>'body', 'attribute'=>'logo'), // Extension array('element'=>'div', 'attribute'=>'href'), // Extension array('element'=>'div', 'attribute'=>'src'), // Extension array('element'=>'embed', 'attribute'=>'code'), // Extension array('element'=>'embed', 'attribute'=>'pluginspage'), // Extension array('element'=>'html', 'attribute'=>'background'), // Extension array('element'=>'ilayer', 'attribute'=>'src'), // Extension array('element'=>'img', 'attribute'=>'dynsrc'), // Extension array('element'=>'img', 'attribute'=>'lowsrc'), // Extension array('element'=>'input', 'attribute'=>'dynsrc'), // Extension array('element'=>'input', 'attribute'=>'lowsrc'), // Extension array('element'=>'table', 'attribute'=>'background'), // Extension array('element'=>'td', 'attribute'=>'background'), // Extension array('element'=>'th', 'attribute'=>'background'), // Extension array('element'=>'layer', 'attribute'=>'src'), // Extension array('element'=>'xml', 'attribute'=>'src'), // Extension array('element'=>'button', 'attribute'=>'action'), // Forms 2.0 array('element'=>'datalist', 'attribute'=>'data'), // Forms 2.0 array('element'=>'form', 'attribute'=>'data'), // Forms 2.0 array('element'=>'input', 'attribute'=>'action'), // Forms 2.0 array('element'=>'select', 'attribute'=>'data'), // Forms 2.0 // XHTML array('element'=>'html', 'attribute'=>'xmlns'), // WML array('element'=>'access', 'attribute'=>'path'), // 1.3 array('element'=>'card', 'attribute'=>'onenterforward'), // 1.3 array('element'=>'card', 'attribute'=>'onenterbackward'),// 1.3 array('element'=>'card', 'attribute'=>'ontimer'), // 1.3 array('element'=>'go', 'attribute'=>'href'), // 1.3 array('element'=>'option', 'attribute'=>'onpick'), // 1.3 array('element'=>'template', 'attribute'=>'onenterforward'), // 1.3 array('element'=>'template', 'attribute'=>'onenterbackward'),// 1.3 array('element'=>'template', 'attribute'=>'ontimer'), // 1.3 array('element'=>'wml', 'attribute'=>'xmlns'), // 2.0 ); $match_metas = array( 'content-base', 'content-location', 'referer', 'location', 'refresh', ); // Extract all elements if ( !preg_match_all( '/<([a-z][^>]*)>/iu', $text, $matches ) ) return array( ); $elements = $matches[1]; $value_pattern = '=(("([^"]*)")|([^\s]*))'; // Match elements and attributes foreach ( $match_elements as $match_element ) { $name = $match_element['element']; $attr = $match_element['attribute']; $pattern = '/^' . $name . '\s.*' . $attr . $value_pattern . '/iu'; if ( $name == 'object' ) $split_pattern = '/\s*/u'; // Space-separated URL list else if ( $name == 'archive' ) $split_pattern = '/,\s*/u'; // Comma-separated URL list else unset( $split_pattern ); // Single URL foreach ( $elements as $element ) { if ( !preg_match( $pattern, $element, $match ) ) continue; $m = empty($match[3]) ? (!empty($match[4])?$match[4]:'') : $match[3]; if ( !isset( $split_pattern ) ) $urls[$name][$attr][] = $m; else { $msplit = preg_split( $split_pattern, $m ); foreach ( $msplit as $ms ) $urls[$name][$attr][] = $ms; } } } // Match meta http-equiv elements foreach ( $match_metas as $match_meta ) { $attr_pattern = '/http-equiv="?' . $match_meta . '"?/iu'; $content_pattern = '/content' . $value_pattern . '/iu'; $refresh_pattern = '/\d*;\s*(url=)?(.*)$/iu'; foreach ( $elements as $element ) { if ( !preg_match( '/^meta/iu', $element ) || !preg_match( $attr_pattern, $element ) || !preg_match( $content_pattern, $element, $match ) ) continue; $m = empty($match[3]) ? $match[4] : $match[3]; if ( $match_meta != 'refresh' ) $urls['meta']['http-equiv'][] = $m; else if ( preg_match( $refresh_pattern, $m, $match ) ) $urls['meta']['http-equiv'][] = $match[2]; } } // Match style attributes $urls['style'] = array( ); $style_pattern = '/style' . $value_pattern . '/iu'; foreach ( $elements as $element ) { if ( !preg_match( $style_pattern, $element, $match ) ) continue; $m = empty($match[3]) ? $match[4] : $match[3]; $style_urls = extract_css_urls( $m ); if ( !empty( $style_urls ) ) $urls['style'] = array_merge_recursive( $urls['style'], $style_urls ); } // Match style bodies if ( preg_match_all( '/]*>(.*?)<\/style>/siu', $text, $style_bodies ) ) { foreach ( $style_bodies[1] as $style_body ) { $style_urls = extract_css_urls( $style_body ); if ( !empty( $style_urls ) ) $urls['style'] = array_merge_recursive( $urls['style'], $style_urls ); } } if ( empty($urls['style']) ) unset( $urls['style'] ); return $urls; } /** * Extract URLs from UTF-8 CSS text. * * URLs within @import statements and url() property functions are extracted * and returned in an associative array of arrays. Array keys indicate * the use context for the URL, including: * * "import" * "property" * * Each value in the associative array is an array of URLs. * * Parameters: * text the UTF-8 text to scan * * Return values: * an associative array of arrays of URLs. * * See: * http://nadeausoftware.com/articles/2008/01/php_tip_how_extract_urls_css_file */ function extract_css_urls( $text ) { $urls = array( ); $url_pattern = '(([^\\\\\'", \(\)]*(\\\\.)?)+)'; $urlfunc_pattern = 'url\(\s*[\'"]?' . $url_pattern . '[\'"]?\s*\)'; $pattern = '/(' . '(@import\s*[\'"]' . $url_pattern . '[\'"])' . '|(@import\s*' . $urlfunc_pattern . ')' . '|(' . $urlfunc_pattern . ')' . ')/iu'; if ( !preg_match_all( $pattern, $text, $matches ) ) return $urls; // @import '...' // @import "..." foreach ( $matches[3] as $match ) if ( !empty($match) ) $urls['import'][] = preg_replace( '/\\\\(.)/u', '\\1', $match ); // @import url(...) // @import url('...') // @import url("...") foreach ( $matches[7] as $match ) if ( !empty($match) ) $urls['import'][] = preg_replace( '/\\\\(.)/u', '\\1', $match ); // url(...) // url('...') // url("...") foreach ( $matches[11] as $match ) if ( !empty($match) ) $urls['property'][] = preg_replace( '/\\\\(.)/u', '\\1', $match ); return $urls; } home/smkb8269/public_html/lms.smk-almanshuriyah.sch.id/enrol/category/locallib.php000064400000025041152266451540024123 0ustar00. /** * Local stuff for category enrolment plugin. * * @package enrol_category * @copyright 2010 Petr Skoda {@link http://skodak.org} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); /** * Sync all category enrolments in one course * @param stdClass $course * @return void */ function enrol_category_sync_course($course) { global $DB; if (!enrol_is_enabled('category')) { return; } $plugin = enrol_get_plugin('category'); $syscontext = context_system::instance(); $roles = get_roles_with_capability('enrol/category:synchronised', CAP_ALLOW, $syscontext); if (!$roles) { // Nothing to sync, so remove the instance completely if exists. if ($instances = $DB->get_records('enrol', array('courseid'=>$course->id, 'enrol'=>'category'))) { foreach ($instances as $instance) { $plugin->delete_instance($instance); } } return; } // First find out if any parent category context contains interesting role assignments. $coursecontext = context_course::instance($course->id); $contextids = $coursecontext->get_parent_context_ids(); array_pop($contextids); // Remove system context, we are interested in categories only. list($roleids, $params) = $DB->get_in_or_equal(array_keys($roles), SQL_PARAMS_NAMED, 'r'); list($contextids, $contextparams) = $DB->get_in_or_equal($contextids, SQL_PARAMS_NAMED, 'c'); $params = array_merge($params, $contextparams); $params['courseid'] = $course->id; $sql = "SELECT 'x' FROM {role_assignments} WHERE roleid $roleids AND contextid $contextids"; if (!$DB->record_exists_sql($sql, $params)) { if ($instances = $DB->get_records('enrol', array('courseid'=>$course->id, 'enrol'=>'category'))) { // Should be max one instance, but anyway. foreach ($instances as $instance) { $plugin->delete_instance($instance); } } return; } // Make sure the enrol instance exists - there should be always only one instance. $delinstances = array(); if ($instances = $DB->get_records('enrol', array('courseid'=>$course->id, 'enrol'=>'category'))) { $instance = array_shift($instances); $delinstances = $instances; } else { $i = $plugin->add_instance($course); $instance = $DB->get_record('enrol', array('id'=>$i)); } // Add new enrolments. $sql = "SELECT ra.userid, ra.estart FROM (SELECT xra.userid, MIN(xra.timemodified) AS estart FROM {role_assignments} xra JOIN {user} xu ON (xu.id = xra.userid AND xu.deleted = 0) WHERE xra.roleid $roleids AND xra.contextid $contextids GROUP BY xra.userid ) ra LEFT JOIN {user_enrolments} ue ON (ue.enrolid = :instanceid AND ue.userid = ra.userid) WHERE ue.id IS NULL"; $params['instanceid'] = $instance->id; $rs = $DB->get_recordset_sql($sql, $params); foreach ($rs as $ra) { $plugin->enrol_user($instance, $ra->userid, null, $ra->estart); } $rs->close(); // Remove unwanted enrolments. $sql = "SELECT DISTINCT ue.userid FROM {user_enrolments} ue LEFT JOIN {role_assignments} ra ON (ra.roleid $roleids AND ra.contextid $contextids AND ra.userid = ue.userid) WHERE ue.enrolid = :instanceid AND ra.id IS NULL"; $rs = $DB->get_recordset_sql($sql, $params); foreach ($rs as $ra) { $plugin->unenrol_user($instance, $ra->userid); } $rs->close(); if ($delinstances) { // We have to do this as the last step in order to prevent temporary unenrolment. foreach ($delinstances as $delinstance) { $plugin->delete_instance($delinstance); } } } /** * Synchronise courses in all categories. * * It gets out-of-sync if: * - you move course to different category * - reorder categories * - disable enrol_category and enable it again * * @param progress_trace $trace * @return int exit code - 0 is ok, 1 means error, 2 if plugin disabled */ function enrol_category_sync_full(progress_trace $trace) { global $DB; if (!enrol_is_enabled('category')) { $trace->finished(); return 2; } // We may need a lot of time here. core_php_time_limit::raise(); $plugin = enrol_get_plugin('category'); $syscontext = context_system::instance(); // Any interesting roles worth synchronising? if (!$roles = get_roles_with_capability('enrol/category:synchronised', CAP_ALLOW, $syscontext)) { // yay, nothing to do, so let's remove all leftovers $trace->output("No roles with 'enrol/category:synchronised' capability found."); if ($instances = $DB->get_records('enrol', array('enrol'=>'category'))) { $trace->output("Deleting all category enrol instances..."); foreach ($instances as $instance) { $trace->output("deleting category enrol instance from course {$instance->courseid}", 1); $plugin->delete_instance($instance); } $trace->output("...all instances deleted."); } $trace->finished(); return 0; } $rolenames = role_fix_names($roles, null, ROLENAME_SHORT, true); $trace->output('Synchronising category enrolments for roles: '.implode(', ', $rolenames).'...'); list($roleids, $params) = $DB->get_in_or_equal(array_keys($roles), SQL_PARAMS_NAMED, 'r'); $params['courselevel'] = CONTEXT_COURSE; $params['catlevel'] = CONTEXT_COURSECAT; // First of all add necessary enrol instances to all courses. $parentcat = $DB->sql_concat("cat.path", "'/%'"); $parentcctx = $DB->sql_concat("cctx.path", "'/%'"); // Need whole course records to be used by add_instance(), use inner view (ci) to // get distinct records only. // TODO: Moodle 2.1. Improve enrol API to accept courseid / courserec $sql = "SELECT c.* FROM {course} c JOIN ( SELECT DISTINCT c.id FROM {course} c JOIN {context} ctx ON (ctx.instanceid = c.id AND ctx.contextlevel = :courselevel) JOIN (SELECT DISTINCT cctx.path FROM {course_categories} cc JOIN {context} cctx ON (cctx.instanceid = cc.id AND cctx.contextlevel = :catlevel) JOIN {role_assignments} ra ON (ra.contextid = cctx.id AND ra.roleid $roleids) ) cat ON (ctx.path LIKE $parentcat) LEFT JOIN {enrol} e ON (e.courseid = c.id AND e.enrol = 'category') WHERE e.id IS NULL) ci ON (c.id = ci.id)"; $rs = $DB->get_recordset_sql($sql, $params); foreach($rs as $course) { $plugin->add_instance($course); } $rs->close(); // Now look for courses that do not have any interesting roles in parent contexts, // but still have the instance and delete them. $sql = "SELECT e.* FROM {enrol} e JOIN {context} ctx ON (ctx.instanceid = e.courseid AND ctx.contextlevel = :courselevel) LEFT JOIN ({course_categories} cc JOIN {context} cctx ON (cctx.instanceid = cc.id AND cctx.contextlevel = :catlevel) JOIN {role_assignments} ra ON (ra.contextid = cctx.id AND ra.roleid $roleids) ) ON (ctx.path LIKE $parentcctx) WHERE e.enrol = 'category' AND cc.id IS NULL"; $rs = $DB->get_recordset_sql($sql, $params); foreach($rs as $instance) { $plugin->delete_instance($instance); } $rs->close(); // Add missing enrolments. $sql = "SELECT e.*, cat.userid, cat.estart FROM {enrol} e JOIN {context} ctx ON (ctx.instanceid = e.courseid AND ctx.contextlevel = :courselevel) JOIN (SELECT cctx.path, ra.userid, MIN(ra.timemodified) AS estart FROM {course_categories} cc JOIN {context} cctx ON (cctx.instanceid = cc.id AND cctx.contextlevel = :catlevel) JOIN {role_assignments} ra ON (ra.contextid = cctx.id AND ra.roleid $roleids) GROUP BY cctx.path, ra.userid ) cat ON (ctx.path LIKE $parentcat) LEFT JOIN {user_enrolments} ue ON (ue.enrolid = e.id AND ue.userid = cat.userid) WHERE e.enrol = 'category' AND ue.id IS NULL"; $rs = $DB->get_recordset_sql($sql, $params); foreach($rs as $instance) { $userid = $instance->userid; $estart = $instance->estart; unset($instance->userid); unset($instance->estart); $plugin->enrol_user($instance, $userid, null, $estart); $trace->output("enrolling: user $userid ==> course $instance->courseid", 1); } $rs->close(); // Remove stale enrolments. $sql = "SELECT e.*, ue.userid FROM {enrol} e JOIN {context} ctx ON (ctx.instanceid = e.courseid AND ctx.contextlevel = :courselevel) JOIN {user_enrolments} ue ON (ue.enrolid = e.id) LEFT JOIN ({course_categories} cc JOIN {context} cctx ON (cctx.instanceid = cc.id AND cctx.contextlevel = :catlevel) JOIN {role_assignments} ra ON (ra.contextid = cctx.id AND ra.roleid $roleids) ) ON (ctx.path LIKE $parentcctx AND ra.userid = ue.userid) WHERE e.enrol = 'category' AND cc.id IS NULL"; $rs = $DB->get_recordset_sql($sql, $params); foreach($rs as $instance) { $userid = $instance->userid; unset($instance->userid); $plugin->unenrol_user($instance, $userid); $trace->output("unenrolling: user $userid ==> course $instance->courseid", 1); } $rs->close(); $trace->output('...user enrolment synchronisation finished.'); $trace->finished(); return 0; } home/smkb8269/public_html/lms.smk-almanshuriyah.sch.id/comment/locallib.php000064400000024100152266540560022625 0ustar00. /** * comment_manager is helper class to manage moodle comments in admin page (Reports->Comments) * * @package core_comment * @copyright 2010 Dongsheng Cai {@link http://dongsheng.org} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class comment_manager { /** @var int The number of comments to display per page */ private $perpage; /** @var stdClass Course data. */ protected $course; /** @var context|bool To store the context object or false if not found. */ protected $context; /** @var stdClass Course module. */ protected $cm; /** @var course_modinfo Module information for course, or null if resetting. */ protected $modinfo; /** @var string plugin type. */ protected $plugintype; /** @var string plugin name. */ protected $pluginname; /** * Constructs the comment_manage object */ public function __construct() { global $CFG; $this->perpage = $CFG->commentsperpage; } /** * Return comments by pages * * @global moodle_database $DB * @param int $page * @return array An array of comments */ function get_comments($page) { global $DB; if ($page == 0) { $start = 0; } else { $start = $page * $this->perpage; } $comments = array(); $userfieldsapi = \core_user\fields::for_name(); $usernamefields = $userfieldsapi->get_sql('u', false, '', '', false)->selects; $sql = "SELECT c.id, c.contextid, c.itemid, c.component, c.commentarea, c.userid, c.content, $usernamefields, c.timecreated FROM {comments} c JOIN {user} u ON u.id=c.userid ORDER BY c.timecreated ASC"; $rs = $DB->get_recordset_sql($sql, null, $start, $this->perpage); $formatoptions = array('overflowdiv' => true, 'blanktarget' => true); foreach ($rs as $item) { // Set calculated fields $item->fullname = fullname($item); $item->time = userdate($item->timecreated); $item->content = format_text($item->content, FORMAT_MOODLE, $formatoptions); // Unset fields not related to the comment foreach (\core_user\fields::get_name_fields() as $namefield) { unset($item->$namefield); } unset($item->timecreated); // Record the comment $comments[] = $item; } $rs->close(); return $comments; } /** * Records the course object * * @global moodle_page $PAGE * @global moodle_database $DB * @param int $courseid */ private function setup_course($courseid) { global $PAGE, $DB; if (!empty($this->course) && $this->course->id == $courseid) { // already set, stop return; } if ($courseid == $PAGE->course->id) { $this->course = $PAGE->course; } else if (!$this->course = $DB->get_record('course', array('id' => $courseid))) { $this->course = null; } } /** * Sets up the module or block information for a comment * * @global moodle_database $DB * @param stdClass $comment * @return bool */ private function setup_plugin($comment) { global $DB; $this->context = context::instance_by_id($comment->contextid, IGNORE_MISSING); if (!$this->context) { return false; } switch ($this->context->contextlevel) { case CONTEXT_BLOCK: if ($block = $DB->get_record('block_instances', array('id' => $this->context->instanceid))) { $this->plugintype = 'block'; $this->pluginname = $block->blockname; } else { return false; } break; case CONTEXT_MODULE: $this->plugintype = 'mod'; $this->cm = get_coursemodule_from_id('', $this->context->instanceid); $this->setup_course($this->cm->course); $this->modinfo = get_fast_modinfo($this->course); $this->pluginname = $this->modinfo->cms[$this->cm->id]->modname; break; } return true; } /** * Print comments * @param int $page * @return bool return false if no comments available * * @deprecated since Moodle 4.2 - please do not use this function any more */ public function print_comments($page = 0) { global $OUTPUT, $CFG, $OUTPUT, $DB; debugging('The function ' . __FUNCTION__ . '() is deprecated, please do not use it any more. ' . 'See \'comments\' system report class for replacement', DEBUG_DEVELOPER); $count = $DB->count_records('comments'); $comments = $this->get_comments($page); if (count($comments) == 0) { echo $OUTPUT->notification(get_string('nocomments', 'moodle')); return false; } $table = new html_table(); $table->head = array ( html_writer::checkbox('selectall', '', false, get_string('selectall'), array('id' => 'comment_select_all', 'class' => 'me-1')), get_string('author', 'search'), get_string('content'), get_string('action') ); $table->colclasses = array ('leftalign', 'leftalign', 'leftalign', 'leftalign'); $table->attributes = array('class'=>'admintable generaltable'); $table->id = 'commentstable'; $table->data = array(); $link = new moodle_url('/comment/index.php', array('action' => 'delete', 'sesskey' => sesskey())); foreach ($comments as $c) { $userdata = html_writer::link(new moodle_url('/user/profile.php', ['id' => $c->userid]), $c->fullname); $this->setup_plugin($c); if (!empty($this->plugintype)) { $context_url = plugin_callback($this->plugintype, $this->pluginname, 'comment', 'url', array($c)); } $checkbox = html_writer::checkbox('comments', $c->id, false); $action = html_writer::link(new moodle_url($link, array('commentid' => $c->id)), get_string('delete')); if (!empty($context_url)) { $action .= html_writer::empty_tag('br'); $action .= html_writer::link($context_url, get_string('commentincontext'), array('target'=>'_blank')); } $table->data[] = array($checkbox, $userdata, $c->content, $action); } echo html_writer::table($table); echo $OUTPUT->paging_bar($count, $page, $this->perpage, $CFG->wwwroot.'/comment/index.php'); return true; } /** * Delete a comment * * @param int $commentid * @return bool */ public function delete_comment($commentid) { global $DB; if ($DB->record_exists('comments', array('id' => $commentid))) { $DB->delete_records('comments', array('id' => $commentid)); return true; } return false; } /** * Delete comments * * @param string $list A list of comment ids separated by hyphens * @return bool */ public function delete_comments($list) { global $DB; $ids = explode('-', $list); foreach ($ids as $id) { $id = (int)$id; if ($DB->record_exists('comments', array('id' => $id))) { $DB->delete_records('comments', array('id' => $id)); } } return true; } /** * Get comments created since a given time. * * @param stdClass $course course object * @param stdClass $context context object * @param string $component component name * @param int $since the time to check * @param stdClass|\cm_info|null $cm course module object * @return array list of comments db records since the given timelimit * @since Moodle 3.2 */ public function get_component_comments_since($course, $context, $component, $since, $cm = null) { global $DB; $commentssince = array(); $where = 'contextid = ? AND component = ? AND timecreated > ?'; $comments = $DB->get_records_select('comments', $where, array($context->id, $component, $since)); // Check item by item if we have permissions. $managersviewstatus = array(); foreach ($comments as $comment) { // Check if the manager for the item is cached. if (!isset($managersviewstatus[$comment->commentarea]) or !isset($managersviewstatus[$comment->commentarea][$comment->itemid])) { $args = new stdClass; $args->area = $comment->commentarea; $args->itemid = $comment->itemid; $args->context = $context; $args->course = $course; $args->client_id = 0; $args->component = $component; if (!empty($cm)) { $args->cm = $cm; } $manager = new comment($args); $managersviewstatus[$comment->commentarea][$comment->itemid] = $manager->can_view(); } if ($managersviewstatus[$comment->commentarea][$comment->itemid]) { $commentssince[$comment->id] = $comment; } } return $commentssince; } } home/smkb8269/public_html/lms.smk-almanshuriyah.sch.id/enrol/meta/locallib.php000064400000074763152266563360023257 0ustar00. /** * Local stuff for meta course enrolment plugin. * * @package enrol_meta * @copyright 2010 Petr Skoda {@link http://skodak.org} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); /** * Event handler for meta enrolment plugin. * * We try to keep everything in sync via listening to events, * it may fail sometimes, so we always do a full sync in cron too. */ class enrol_meta_handler { /** * Synchronise meta enrolments of this user in this course * @static * @param int $courseid * @param int $userid * @return void */ protected static function sync_course_instances($courseid, $userid) { global $DB; static $preventrecursion = false; // does anything want to sync with this parent? if (!$enrols = $DB->get_records('enrol', array('customint1'=>$courseid, 'enrol'=>'meta'), 'id ASC')) { return; } if ($preventrecursion) { return; } $preventrecursion = true; try { foreach ($enrols as $enrol) { self::sync_with_parent_course($enrol, $userid); } } catch (Exception $e) { $preventrecursion = false; throw $e; } $preventrecursion = false; } /** * Synchronise user enrolments in given instance as fast as possible. * * All roles are removed if the meta plugin disabled. * * @static * @param stdClass $instance * @param int $userid * @return void */ protected static function sync_with_parent_course(stdClass $instance, $userid) { global $DB, $CFG; require_once($CFG->dirroot . '/group/lib.php'); $plugin = enrol_get_plugin('meta'); if ($instance->customint1 == $instance->courseid) { // can not sync with self!!! return; } $context = context_course::instance($instance->courseid); // list of enrolments in parent course (we ignore meta enrols in parents completely) list($enabled, $params) = $DB->get_in_or_equal(explode(',', $CFG->enrol_plugins_enabled), SQL_PARAMS_NAMED, 'e'); $params['userid'] = $userid; $params['parentcourse'] = $instance->customint1; $sql = "SELECT ue.*, e.status AS enrolstatus FROM {user_enrolments} ue JOIN {enrol} e ON (e.id = ue.enrolid AND e.enrol <> 'meta' AND e.courseid = :parentcourse AND e.enrol $enabled) WHERE ue.userid = :userid"; $parentues = $DB->get_records_sql($sql, $params); // current enrolments for this instance $ue = $DB->get_record('user_enrolments', array('enrolid'=>$instance->id, 'userid'=>$userid)); // first deal with users that are not enrolled in parent if (empty($parentues)) { self::user_not_supposed_to_be_here($instance, $ue, $context, $plugin); return; } if (!$parentcontext = context_course::instance($instance->customint1, IGNORE_MISSING)) { // Weird, we should not get here. return; } $skiproles = $plugin->get_config('nosyncroleids', ''); $skiproles = empty($skiproles) ? array() : explode(',', $skiproles); $syncall = $plugin->get_config('syncall', 1); // roles in parent course (meta enrols must be ignored!) $parentroles = array(); list($ignoreroles, $params) = $DB->get_in_or_equal($skiproles, SQL_PARAMS_NAMED, 'ri', false, -1); $params['contextid'] = $parentcontext->id; $params['userid'] = $userid; $select = "contextid = :contextid AND userid = :userid AND component <> 'enrol_meta' AND roleid $ignoreroles"; foreach($DB->get_records_select('role_assignments', $select, $params) as $ra) { $parentroles[$ra->roleid] = $ra->roleid; } // roles from this instance $roles = array(); $ras = $DB->get_records('role_assignments', array('contextid'=>$context->id, 'userid'=>$userid, 'component'=>'enrol_meta', 'itemid'=>$instance->id)); foreach($ras as $ra) { $roles[$ra->roleid] = $ra->roleid; } unset($ras); // do we want users without roles? if (!$syncall and empty($parentroles)) { self::user_not_supposed_to_be_here($instance, $ue, $context, $plugin); return; } // Is parent enrol active? Find minimum timestart and maximum timeend of all active enrolments. $parentstatus = ENROL_USER_SUSPENDED; $parenttimeend = null; $parenttimestart = null; foreach ($parentues as $pue) { if ($pue->status == ENROL_USER_ACTIVE && $pue->enrolstatus == ENROL_INSTANCE_ENABLED) { $parentstatus = ENROL_USER_ACTIVE; if ($parenttimeend === null || $pue->timeend == 0 || ($parenttimeend && $parenttimeend < $pue->timeend)) { $parenttimeend = $pue->timeend; } if ($parenttimestart === null || $parenttimestart > $pue->timestart) { $parenttimestart = $pue->timestart; } } } // Enrol user if not enrolled yet or fix status/timestart/timeend. Use the minimum timestart and maximum timeend found above. if ($ue) { if ($parentstatus != $ue->status || ($parentstatus == ENROL_USER_ACTIVE && ($parenttimestart != $ue->timestart || $parenttimeend != $ue->timeend))) { $plugin->update_user_enrol($instance, $userid, $parentstatus, $parenttimestart, $parenttimeend); $ue->status = $parentstatus; $ue->timestart = $parenttimestart; $ue->timeend = $parenttimeend; } } else { $plugin->enrol_user($instance, $userid, NULL, (int)$parenttimestart, (int)$parenttimeend, $parentstatus); $ue = new stdClass(); $ue->userid = $userid; $ue->enrolid = $instance->id; $ue->status = $parentstatus; if ($instance->customint2 && $group = $DB->get_record('groups', ['id' => $instance->customint2])) { groups_add_member($group, $userid, 'enrol_meta', $instance->id); } } $unenrolaction = $plugin->get_config('unenrolaction', ENROL_EXT_REMOVED_SUSPENDNOROLES); // Only active users in enabled instances are supposed to have roles (we can reassign the roles any time later). if ($ue->status != ENROL_USER_ACTIVE or $instance->status != ENROL_INSTANCE_ENABLED or ($parenttimeend and $parenttimeend < time()) or ($parenttimestart > time())) { if ($unenrolaction == ENROL_EXT_REMOVED_SUSPEND) { // Always keep the roles. } else if ($roles) { // This will only unassign roles that were assigned in this enrolment method, leaving all manual role assignments intact. role_unassign_all(array('userid'=>$userid, 'contextid'=>$context->id, 'component'=>'enrol_meta', 'itemid'=>$instance->id)); } return; } // add new roles foreach ($parentroles as $rid) { if (!isset($roles[$rid])) { role_assign($rid, $userid, $context->id, 'enrol_meta', $instance->id); } } if ($unenrolaction == ENROL_EXT_REMOVED_SUSPEND) { // Always keep the roles. return; } // remove roles foreach ($roles as $rid) { if (!isset($parentroles[$rid])) { role_unassign($rid, $userid, $context->id, 'enrol_meta', $instance->id); } } } /** * Deal with users that are not supposed to be enrolled via this instance * @static * @param stdClass $instance * @param stdClass $ue * @param context_course $context * @param enrol_meta $plugin * @return void */ protected static function user_not_supposed_to_be_here($instance, $ue, context_course $context, $plugin) { if (!$ue) { // Not enrolled yet - simple! return; } $userid = $ue->userid; $unenrolaction = $plugin->get_config('unenrolaction', ENROL_EXT_REMOVED_SUSPENDNOROLES); if ($unenrolaction == ENROL_EXT_REMOVED_UNENROL) { // Purges grades, group membership, preferences, etc. - admins were warned! $plugin->unenrol_user($instance, $userid); } else if ($unenrolaction == ENROL_EXT_REMOVED_SUSPEND) { if ($ue->status != ENROL_USER_SUSPENDED) { $plugin->update_user_enrol($instance, $userid, ENROL_USER_SUSPENDED); } } else if ($unenrolaction == ENROL_EXT_REMOVED_SUSPENDNOROLES) { if ($ue->status != ENROL_USER_SUSPENDED) { $plugin->update_user_enrol($instance, $userid, ENROL_USER_SUSPENDED); } role_unassign_all(array('userid'=>$userid, 'contextid'=>$context->id, 'component'=>'enrol_meta', 'itemid'=>$instance->id)); } else { debugging('Unknown unenrol action '.$unenrolaction); } } } /** * Sync all meta course links. * * @param int $courseid one course, empty mean all * @param bool $verbose verbose CLI output * @return int 0 means ok, 1 means error, 2 means plugin disabled */ function enrol_meta_sync($courseid = NULL, $verbose = false) { global $CFG, $DB; require_once("{$CFG->dirroot}/group/lib.php"); // purge all roles if meta sync disabled, those can be recreated later here in cron if (!enrol_is_enabled('meta')) { if ($verbose) { mtrace('Meta sync plugin is disabled, unassigning all plugin roles and stopping.'); } role_unassign_all(array('component'=>'enrol_meta')); return 2; } // unfortunately this may take a long time, execution can be interrupted safely core_php_time_limit::raise(); raise_memory_limit(MEMORY_HUGE); if ($verbose) { mtrace('Starting user enrolment synchronisation...'); } $instances = array(); // cache instances $meta = enrol_get_plugin('meta'); $unenrolaction = $meta->get_config('unenrolaction', ENROL_EXT_REMOVED_SUSPENDNOROLES); $skiproles = $meta->get_config('nosyncroleids', ''); $skiproles = empty($skiproles) ? array() : explode(',', $skiproles); $syncall = $meta->get_config('syncall', 1); $allroles = get_all_roles(); // Iterate through all not enrolled yet users. For each active enrolment of each user find the minimum // enrolment startdate and maximum enrolment enddate. // This SQL relies on the fact that ENROL_USER_ACTIVE < ENROL_USER_SUSPENDED // and ENROL_INSTANCE_ENABLED < ENROL_INSTANCE_DISABLED. Condition "pue.status + pe.status = 0" means // that enrolment is active. When MIN(pue.status + pe.status)=0 it means there exists an active // enrolment. $onecourse = $courseid ? "AND e.courseid = :courseid" : ""; list($enabled, $params) = $DB->get_in_or_equal(explode(',', $CFG->enrol_plugins_enabled), SQL_PARAMS_NAMED, 'e'); $params['courseid'] = $courseid; $sql = "SELECT pue.userid, e.id AS enrolid, MIN(pue.status + pe.status) AS status, MIN(CASE WHEN (pue.status + pe.status = 0) THEN pue.timestart ELSE " . SQL_INT_MAX . " END) AS timestart, MAX(CASE WHEN (pue.status + pe.status = 0) THEN (CASE WHEN pue.timeend = 0 THEN " . SQL_INT_MAX . " ELSE pue.timeend END) ELSE 0 END) AS timeend FROM {user_enrolments} pue JOIN {enrol} pe ON (pe.id = pue.enrolid AND pe.enrol <> 'meta' AND pe.enrol $enabled) JOIN {enrol} e ON (e.customint1 = pe.courseid AND e.enrol = 'meta' AND e.status = :enrolstatus $onecourse) JOIN {user} u ON (u.id = pue.userid AND u.deleted = 0) LEFT JOIN {user_enrolments} ue ON (ue.enrolid = e.id AND ue.userid = pue.userid) WHERE ue.id IS NULL GROUP BY pue.userid, e.id"; $params['enrolstatus'] = ENROL_INSTANCE_ENABLED; $rs = $DB->get_recordset_sql($sql, $params); foreach($rs as $ue) { if (!isset($instances[$ue->enrolid])) { $instances[$ue->enrolid] = $DB->get_record('enrol', array('id'=>$ue->enrolid)); } $instance = $instances[$ue->enrolid]; if (!$syncall) { // this may be slow if very many users are ignored in sync $parentcontext = context_course::instance($instance->customint1); list($ignoreroles, $params) = $DB->get_in_or_equal($skiproles, SQL_PARAMS_NAMED, 'ri', false, -1); $params['contextid'] = $parentcontext->id; $params['userid'] = $ue->userid; $select = "contextid = :contextid AND userid = :userid AND component <> 'enrol_meta' AND roleid $ignoreroles"; if (!$DB->record_exists_select('role_assignments', $select, $params)) { // bad luck, this user does not have any role we want in parent course if ($verbose) { mtrace(" skipping enrolling: $ue->userid ==> $instance->courseid (user without role)"); } continue; } } // So now we have aggregated values that we will use for the meta enrolment status, timeend and timestart. // Again, we use the fact that active=0 and disabled/suspended=1. Only when MIN(pue.status + pe.status)=0 the enrolment is active: $ue->status = ($ue->status == ENROL_USER_ACTIVE + ENROL_INSTANCE_ENABLED) ? ENROL_USER_ACTIVE : ENROL_USER_SUSPENDED; // Timeend 9999999999 was used instead of 0 in the "MAX()" function: $ue->timeend = ($ue->timeend == SQL_INT_MAX) ? 0 : (int)$ue->timeend; // Timestart 9999999999 is only possible when there are no active enrolments: $ue->timestart = ($ue->timestart == SQL_INT_MAX) ? 0 : (int)$ue->timestart; $meta->enrol_user($instance, $ue->userid, null, $ue->timestart, $ue->timeend, $ue->status); if ($instance->customint2 && $group = $DB->get_record('groups', ['id' => $instance->customint2])) { groups_add_member($group, $ue->userid, 'enrol_meta', $instance->id); } if ($verbose) { mtrace(" enrolling: $ue->userid ==> $instance->courseid"); } } $rs->close(); // unenrol as necessary - ignore enabled flag, we want to get rid of existing enrols in any case $onecourse = $courseid ? "AND e.courseid = :courseid" : ""; list($enabled, $params) = $DB->get_in_or_equal(explode(',', $CFG->enrol_plugins_enabled), SQL_PARAMS_NAMED, 'e'); $params['courseid'] = $courseid; $sql = "SELECT ue.* FROM {user_enrolments} ue JOIN {enrol} e ON (e.id = ue.enrolid AND e.enrol = 'meta' $onecourse) LEFT JOIN ({user_enrolments} xpue JOIN {enrol} xpe ON (xpe.id = xpue.enrolid AND xpe.enrol <> 'meta' AND xpe.enrol $enabled) ) ON (xpe.courseid = e.customint1 AND xpue.userid = ue.userid) WHERE xpue.userid IS NULL"; $rs = $DB->get_recordset_sql($sql, $params); foreach($rs as $ue) { if (!isset($instances[$ue->enrolid])) { $instances[$ue->enrolid] = $DB->get_record('enrol', array('id'=>$ue->enrolid)); } $instance = $instances[$ue->enrolid]; if ($unenrolaction == ENROL_EXT_REMOVED_UNENROL) { $meta->unenrol_user($instance, $ue->userid); if ($verbose) { mtrace(" unenrolling: $ue->userid ==> $instance->courseid"); } } else if ($unenrolaction == ENROL_EXT_REMOVED_SUSPEND) { if ($ue->status != ENROL_USER_SUSPENDED) { $meta->update_user_enrol($instance, $ue->userid, ENROL_USER_SUSPENDED); if ($verbose) { mtrace(" suspending: $ue->userid ==> $instance->courseid"); } } } else if ($unenrolaction == ENROL_EXT_REMOVED_SUSPENDNOROLES) { if ($ue->status != ENROL_USER_SUSPENDED) { $meta->update_user_enrol($instance, $ue->userid, ENROL_USER_SUSPENDED); $context = context_course::instance($instance->courseid); role_unassign_all(array('userid'=>$ue->userid, 'contextid'=>$context->id, 'component'=>'enrol_meta', 'itemid'=>$instance->id)); if ($verbose) { mtrace(" suspending and removing all roles: $ue->userid ==> $instance->courseid"); } } } } $rs->close(); // Update status - meta enrols are ignored to avoid recursion. // Note the trick here is that the active enrolment and instance constants have value 0. $onecourse = $courseid ? "AND e.courseid = :courseid" : ""; list($enabled, $params) = $DB->get_in_or_equal(explode(',', $CFG->enrol_plugins_enabled), SQL_PARAMS_NAMED, 'e'); $params['courseid'] = $courseid; // The query builds a a list of all the non-meta enrolments that are on courses (the children) that are linked to by a meta // enrolment, it then groups them by the course that linked to them (the parents). // // It will only return results where the there is a difference between the status of the parent and the lowest status // of the children (remember that 0 is active, any other status is some form of inactive), or the time the earliest non-zero // start time of a child is different to the parent, or the longest effective end date has changed. // // The last two case statements in the HAVING clause are designed to ignore any inactive child records when calculating // the start and end time. $sql = "SELECT ue.userid, ue.enrolid, MIN(xpue.status + xpe.status) AS pstatus, MIN(CASE WHEN (xpue.status + xpe.status = 0) THEN xpue.timestart ELSE " . SQL_INT_MAX . " END) AS ptimestart, MAX(CASE WHEN (xpue.status + xpe.status = 0) THEN (CASE WHEN xpue.timeend = 0 THEN " . SQL_INT_MAX . " ELSE xpue.timeend END) ELSE 0 END) AS ptimeend FROM {user_enrolments} ue JOIN {enrol} e ON (e.id = ue.enrolid AND e.enrol = 'meta' $onecourse) JOIN {user_enrolments} xpue ON (xpue.userid = ue.userid) JOIN {enrol} xpe ON (xpe.id = xpue.enrolid AND xpe.enrol <> 'meta' AND xpe.enrol $enabled AND xpe.courseid = e.customint1) GROUP BY ue.userid, ue.enrolid HAVING (MIN(xpue.status + xpe.status) = 0 AND MIN(ue.status) > 0) OR (MIN(xpue.status + xpe.status) > 0 AND MIN(ue.status) = 0) OR ((CASE WHEN MIN(CASE WHEN (xpue.status + xpe.status = 0) THEN xpue.timestart ELSE " . SQL_INT_MAX . " END) = " . SQL_INT_MAX . " THEN 0 ELSE MIN(CASE WHEN (xpue.status + xpe.status = 0) THEN xpue.timestart ELSE " . SQL_INT_MAX . " END) END) <> MIN(ue.timestart)) OR ((CASE WHEN MAX(CASE WHEN (xpue.status + xpe.status = 0) THEN (CASE WHEN xpue.timeend = 0 THEN " . SQL_INT_MAX . " ELSE xpue.timeend END) ELSE 0 END) = " . SQL_INT_MAX . " THEN 0 ELSE MAX(CASE WHEN (xpue.status + xpe.status = 0) THEN (CASE WHEN xpue.timeend = 0 THEN " . SQL_INT_MAX . " ELSE xpue.timeend END) ELSE 0 END) END) <> MAX(ue.timeend))"; $rs = $DB->get_recordset_sql($sql, $params); foreach($rs as $ue) { if (!isset($instances[$ue->enrolid])) { $instances[$ue->enrolid] = $DB->get_record('enrol', array('id'=>$ue->enrolid)); } $instance = $instances[$ue->enrolid]; $ue->pstatus = ($ue->pstatus == ENROL_USER_ACTIVE + ENROL_INSTANCE_ENABLED) ? ENROL_USER_ACTIVE : ENROL_USER_SUSPENDED; $ue->ptimeend = ($ue->ptimeend == SQL_INT_MAX) ? 0 : (int)$ue->ptimeend; $ue->ptimestart = ($ue->ptimestart == SQL_INT_MAX) ? 0 : (int)$ue->ptimestart; if ($ue->pstatus == ENROL_USER_ACTIVE and (!$ue->ptimeend || $ue->ptimeend > time()) and !$syncall and $unenrolaction != ENROL_EXT_REMOVED_UNENROL) { // this may be slow if very many users are ignored in sync $parentcontext = context_course::instance($instance->customint1); list($ignoreroles, $params) = $DB->get_in_or_equal($skiproles, SQL_PARAMS_NAMED, 'ri', false, -1); $params['contextid'] = $parentcontext->id; $params['userid'] = $ue->userid; $select = "contextid = :contextid AND userid = :userid AND component <> 'enrol_meta' AND roleid $ignoreroles"; if (!$DB->record_exists_select('role_assignments', $select, $params)) { // bad luck, this user does not have any role we want in parent course if ($verbose) { mtrace(" skipping unsuspending: $ue->userid ==> $instance->courseid (user without role)"); } continue; } } $meta->update_user_enrol($instance, $ue->userid, $ue->pstatus, $ue->ptimestart, $ue->ptimeend); if ($verbose) { if ($ue->pstatus == ENROL_USER_ACTIVE) { mtrace(" unsuspending: $ue->userid ==> $instance->courseid"); } else { mtrace(" suspending: $ue->userid ==> $instance->courseid"); } } } $rs->close(); // now assign all necessary roles $enabled = explode(',', $CFG->enrol_plugins_enabled); foreach($enabled as $k=>$v) { if ($v === 'meta') { continue; // no meta sync of meta roles } $enabled[$k] = 'enrol_'.$v; } $enabled[] = ''; // manual assignments are replicated too $onecourse = $courseid ? "AND e.courseid = :courseid" : ""; list($enabled, $params) = $DB->get_in_or_equal($enabled, SQL_PARAMS_NAMED, 'e'); $params['coursecontext'] = CONTEXT_COURSE; $params['courseid'] = $courseid; $params['activeuser'] = ENROL_USER_ACTIVE; $params['enabledinstance'] = ENROL_INSTANCE_ENABLED; $sql = "SELECT DISTINCT pra.roleid, pra.userid, c.id AS contextid, e.id AS enrolid, e.courseid FROM {role_assignments} pra JOIN {user} u ON (u.id = pra.userid AND u.deleted = 0) JOIN {context} pc ON (pc.id = pra.contextid AND pc.contextlevel = :coursecontext AND pra.component $enabled) JOIN {enrol} e ON (e.customint1 = pc.instanceid AND e.enrol = 'meta' $onecourse AND e.status = :enabledinstance) JOIN {user_enrolments} ue ON (ue.enrolid = e.id AND ue.userid = u.id AND ue.status = :activeuser) JOIN {context} c ON (c.contextlevel = pc.contextlevel AND c.instanceid = e.courseid) LEFT JOIN {role_assignments} ra ON (ra.contextid = c.id AND ra.userid = pra.userid AND ra.roleid = pra.roleid AND ra.itemid = e.id AND ra.component = 'enrol_meta') WHERE ra.id IS NULL"; if ($ignored = $meta->get_config('nosyncroleids')) { list($notignored, $xparams) = $DB->get_in_or_equal(explode(',', $ignored), SQL_PARAMS_NAMED, 'ig', false); $params = array_merge($params, $xparams); $sql = "$sql AND pra.roleid $notignored"; } $rs = $DB->get_recordset_sql($sql, $params); foreach($rs as $ra) { role_assign($ra->roleid, $ra->userid, $ra->contextid, 'enrol_meta', $ra->enrolid); if ($verbose) { mtrace(" assigning role: $ra->userid ==> $ra->courseid as ".$allroles[$ra->roleid]->shortname); } } $rs->close(); // remove unwanted roles - include ignored roles and disabled plugins too $onecourse = $courseid ? "AND e.courseid = :courseid" : ""; $params = array(); $params['coursecontext'] = CONTEXT_COURSE; $params['courseid'] = $courseid; $params['activeuser'] = ENROL_USER_ACTIVE; $params['enabledinstance'] = ENROL_INSTANCE_ENABLED; if ($ignored = $meta->get_config('nosyncroleids')) { list($notignored, $xparams) = $DB->get_in_or_equal(explode(',', $ignored), SQL_PARAMS_NAMED, 'ig', false); $params = array_merge($params, $xparams); $notignored = "AND pra.roleid $notignored"; } else { $notignored = ""; } $sql = "SELECT ra.roleid, ra.userid, ra.contextid, ra.itemid, e.courseid FROM {role_assignments} ra JOIN {enrol} e ON (e.id = ra.itemid AND ra.component = 'enrol_meta' AND e.enrol = 'meta' $onecourse) JOIN {context} pc ON (pc.instanceid = e.customint1 AND pc.contextlevel = :coursecontext) LEFT JOIN {role_assignments} pra ON (pra.contextid = pc.id AND pra.userid = ra.userid AND pra.roleid = ra.roleid AND pra.component <> 'enrol_meta' $notignored) LEFT JOIN {user_enrolments} ue ON (ue.enrolid = e.id AND ue.userid = ra.userid AND ue.status = :activeuser) WHERE pra.id IS NULL OR ue.id IS NULL OR e.status <> :enabledinstance"; if ($unenrolaction != ENROL_EXT_REMOVED_SUSPEND) { $rs = $DB->get_recordset_sql($sql, $params); foreach($rs as $ra) { role_unassign($ra->roleid, $ra->userid, $ra->contextid, 'enrol_meta', $ra->itemid); if ($verbose) { mtrace(" unassigning role: $ra->userid ==> $ra->courseid as ".$allroles[$ra->roleid]->shortname); } } $rs->close(); } // kick out or suspend users without synced roles if syncall disabled if (!$syncall) { if ($unenrolaction == ENROL_EXT_REMOVED_UNENROL) { $onecourse = $courseid ? "AND e.courseid = :courseid" : ""; $params = array(); $params['coursecontext'] = CONTEXT_COURSE; $params['courseid'] = $courseid; $sql = "SELECT ue.userid, ue.enrolid FROM {user_enrolments} ue JOIN {enrol} e ON (e.id = ue.enrolid AND e.enrol = 'meta' $onecourse) JOIN {context} c ON (e.courseid = c.instanceid AND c.contextlevel = :coursecontext) LEFT JOIN {role_assignments} ra ON (ra.contextid = c.id AND ra.itemid = e.id AND ra.userid = ue.userid) WHERE ra.id IS NULL"; $ues = $DB->get_recordset_sql($sql, $params); foreach($ues as $ue) { if (!isset($instances[$ue->enrolid])) { $instances[$ue->enrolid] = $DB->get_record('enrol', array('id'=>$ue->enrolid)); } $instance = $instances[$ue->enrolid]; $meta->unenrol_user($instance, $ue->userid); if ($verbose) { mtrace(" unenrolling: $ue->userid ==> $instance->courseid (user without role)"); } } $ues->close(); } else { // just suspend the users $onecourse = $courseid ? "AND e.courseid = :courseid" : ""; $params = array(); $params['coursecontext'] = CONTEXT_COURSE; $params['courseid'] = $courseid; $params['active'] = ENROL_USER_ACTIVE; $sql = "SELECT ue.userid, ue.enrolid FROM {user_enrolments} ue JOIN {enrol} e ON (e.id = ue.enrolid AND e.enrol = 'meta' $onecourse) JOIN {context} c ON (e.courseid = c.instanceid AND c.contextlevel = :coursecontext) LEFT JOIN {role_assignments} ra ON (ra.contextid = c.id AND ra.itemid = e.id AND ra.userid = ue.userid) WHERE ra.id IS NULL AND ue.status = :active"; $ues = $DB->get_recordset_sql($sql, $params); foreach($ues as $ue) { if (!isset($instances[$ue->enrolid])) { $instances[$ue->enrolid] = $DB->get_record('enrol', array('id'=>$ue->enrolid)); } $instance = $instances[$ue->enrolid]; $meta->update_user_enrol($instance, $ue->userid, ENROL_USER_SUSPENDED); if ($verbose) { mtrace(" suspending: $ue->userid ==> $instance->courseid (user without role)"); } } $ues->close(); } } // Finally sync groups. $affectedusers = groups_sync_with_enrolment('meta', $courseid); if ($verbose) { foreach ($affectedusers['removed'] as $gm) { mtrace("removing user from group: $gm->userid ==> $gm->courseid - $gm->groupname"); } foreach ($affectedusers['added'] as $ue) { mtrace("adding user to group: $ue->userid ==> $ue->courseid - $ue->groupname"); } } if ($verbose) { mtrace('...user enrolment synchronisation finished.'); } return 0; } /** * Create a new group with the course's name. * * @param int $courseid * @param int $linkedcourseid * @return int $groupid Group ID for this cohort. */ function enrol_meta_create_new_group($courseid, $linkedcourseid) { global $DB, $CFG; require_once($CFG->dirroot.'/group/lib.php'); $coursename = $DB->get_field('course', 'fullname', array('id' => $linkedcourseid), MUST_EXIST); $a = new stdClass(); $a->name = $coursename; $a->increment = ''; $inc = 1; $groupname = trim(get_string('defaultgroupnametext', 'enrol_meta', $a)); // Check to see if the group name already exists in this course. Add an incremented number if it does. while ($DB->record_exists('groups', array('name' => $groupname, 'courseid' => $courseid))) { $a->increment = '(' . (++$inc) . ')'; $groupname = trim(get_string('defaultgroupnametext', 'enrol_meta', $a)); } // Create a new group for the course meta sync. $groupdata = new stdClass(); $groupdata->courseid = $courseid; $groupdata->name = $groupname; $groupid = groups_create_group($groupdata); return $groupid; } home/smkb8269/public_html/lms.smk-almanshuriyah.sch.id/enrol/self/locallib.php000064400000015504152266564030023241 0ustar00. /** * Self enrol plugin implementation. * * @package enrol_self * @copyright 2010 Petr Skoda {@link http://skodak.org} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); require_once("$CFG->libdir/formslib.php"); require_once($CFG->dirroot . '/enrol/locallib.php'); /** * Check if the given password match a group enrolment key in the specified course. * * @param int $courseid course id * @param string $enrolpassword enrolment password * @return bool True if match * @since Moodle 3.0 */ function enrol_self_check_group_enrolment_key($courseid, $enrolpassword) { global $DB; $found = false; $groups = $DB->get_records('groups', array('courseid' => $courseid), 'id ASC', 'id, enrolmentkey'); foreach ($groups as $group) { if (empty($group->enrolmentkey)) { continue; } if ($group->enrolmentkey === $enrolpassword) { $found = true; break; } } return $found; } /** * Old class for displaying the self enrolment form * * @deprecated since Moodle 5.0 - please use {@see enrol_self\form\enrol_form} */ #[\core\attribute\deprecated(replacement: enrol_self\form\enrol_form::class, since: '5.0', reason: 'Now a dynamic form is used')] class enrol_self_enrol_form extends moodleform { protected $instance; protected $toomany = false; /** * Constructor * * @param mixed $action * @param mixed $customdata * @param string $method * @param string $target * @param mixed $attributes * @param bool $editable * @param array $ajaxformdata */ public function __construct($action=null, $customdata=null, $method='post', $target='', $attributes=null, $editable=true, $ajaxformdata=null) { \core\deprecation::emit_deprecation([$this, __FUNCTION__]); parent::__construct($action, $customdata, $method, $target, $attributes, $editable, $ajaxformdata); } /** * Overriding this function to get unique form id for multiple self enrolments. * * @return string form identifier */ protected function get_form_identifier() { $formid = $this->_customdata->id.'_'.get_class($this); return $formid; } public function definition() { global $USER, $OUTPUT, $CFG; $mform = $this->_form; $instance = $this->_customdata; $this->instance = $instance; $plugin = enrol_get_plugin('self'); $heading = $plugin->get_instance_name($instance); $mform->addElement('header', 'selfheader', $heading); if ($instance->password) { // Change the id of self enrolment key input as there can be multiple self enrolment methods. $mform->addElement('password', 'enrolpassword', get_string('password', 'enrol_self'), array('id' => 'enrolpassword_'.$instance->id)); $context = context_course::instance($this->instance->courseid); $userfieldsapi = \core_user\fields::for_userpic(); $ufields = $userfieldsapi->get_sql('u', false, '', '', false)->selects; $keyholders = get_users_by_capability($context, 'enrol/self:holdkey', $ufields); $keyholdercount = 0; foreach ($keyholders as $keyholder) { $keyholdercount++; if ($keyholdercount === 1) { $mform->addElement('static', 'keyholder', '', get_string('keyholder', 'enrol_self')); } $keyholdercontext = context_user::instance($keyholder->id); if ($USER->id == $keyholder->id || has_capability('moodle/user:viewdetails', context_system::instance()) || has_coursecontact_role($keyholder->id)) { $profilelink = '' . fullname($keyholder) . ''; } else { $profilelink = fullname($keyholder); } $profilepic = $OUTPUT->user_picture($keyholder, array('size' => 35, 'courseid' => $this->instance->courseid)); $mform->addElement('static', 'keyholder'.$keyholdercount, '', $profilepic . $profilelink); } } else { $mform->addElement('static', 'nokey', '', get_string('nopassword', 'enrol_self')); } $this->add_action_buttons(false, get_string('enrolme', 'enrol_self')); $mform->addElement('hidden', 'id'); $mform->setType('id', PARAM_INT); $mform->setDefault('id', $instance->courseid); $mform->addElement('hidden', 'instance'); $mform->setType('instance', PARAM_INT); $mform->setDefault('instance', $instance->id); } public function validation($data, $files) { global $DB, $CFG; $errors = parent::validation($data, $files); $instance = $this->instance; if ($this->toomany) { $errors['notice'] = get_string('error'); return $errors; } if ($instance->password) { if ($data['enrolpassword'] !== $instance->password) { if ($instance->customint1) { // Check group enrolment key. if (!enrol_self_check_group_enrolment_key($instance->courseid, $data['enrolpassword'])) { // We can not hint because there are probably multiple passwords. $errors['enrolpassword'] = get_string('passwordinvalid', 'enrol_self'); } } else { $plugin = enrol_get_plugin('self'); if ($plugin->get_config('showhint')) { $hint = core_text::substr($instance->password, 0, 1); $errors['enrolpassword'] = get_string('passwordinvalidhint', 'enrol_self', $hint); } else { $errors['enrolpassword'] = get_string('passwordinvalid', 'enrol_self'); } } } } return $errors; } } home/smkb8269/public_html/lms.smk-almanshuriyah.sch.id/webservice/soap/locallib.php000064400000026671152266564340024304 0ustar00. /** * SOAP web service implementation classes and methods. * * @package webservice_soap * @copyright 2009 Petr Skodak * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ global $CFG; require_once($CFG->dirroot . '/webservice/lib.php'); use webservice_soap\wsdl; /** * SOAP service server implementation. * * @package webservice_soap * @copyright 2009 Petr Skodak * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @since Moodle 2.0 */ class webservice_soap_server extends webservice_base_server { /** @var moodle_url The server URL. */ protected $serverurl; /** @var SoapServer The Soap */ protected $soapserver; /** @var string The response. */ protected $response; /** @var string The class name of the virtual class generated for this web service. */ protected $serviceclass; /** @var bool WSDL mode flag. */ protected $wsdlmode; /** @var \webservice_soap\wsdl The object for WSDL generation. */ protected $wsdl; /** * Contructor. * * @param string $authmethod authentication method of the web service (WEBSERVICE_AUTHMETHOD_PERMANENT_TOKEN, ...) */ public function __construct($authmethod) { parent::__construct($authmethod); // Must not cache wsdl - the list of functions is created on the fly. ini_set('soap.wsdl_cache_enabled', '0'); $this->wsname = 'soap'; $this->wsdlmode = false; } /** * This method parses the $_POST and $_GET superglobals and looks for the following information: * - User authentication parameters: * - Username + password (wsusername and wspassword), or * - Token (wstoken) */ protected function parse_request() { // Retrieve and clean the POST/GET parameters from the parameters specific to the server. parent::set_web_service_call_settings(); if ($this->authmethod == WEBSERVICE_AUTHMETHOD_USERNAME) { $this->username = optional_param('wsusername', null, PARAM_RAW); $this->password = optional_param('wspassword', null, PARAM_RAW); if (!$this->username or !$this->password) { // Workaround for the trouble with & in soap urls. $authdata = get_file_argument(); $authdata = explode('/', trim($authdata, '/')); if (count($authdata) == 2) { list($this->username, $this->password) = $authdata; } } $this->serverurl = new moodle_url('/webservice/soap/simpleserver.php/' . $this->username . '/' . $this->password); } else { $this->token = optional_param('wstoken', null, PARAM_RAW); $this->serverurl = new moodle_url('/webservice/soap/server.php'); $this->serverurl->param('wstoken', $this->token); } if ($wsdl = optional_param('wsdl', 0, PARAM_INT)) { $this->wsdlmode = true; } } /** * Runs the SOAP web service. * * @throws coding_exception * @throws moodle_exception * @throws webservice_access_exception */ public function run() { // We will probably need a lot of memory in some functions. raise_memory_limit(MEMORY_EXTRA); // Set some longer timeout since operations may need longer time to finish. \core_external\external_api::set_timeout(); // Set up exception handler. set_exception_handler(array($this, 'exception_handler')); // Init all properties from the request data. $this->parse_request(); // Authenticate user, this has to be done after the request parsing. This also sets up $USER and $SESSION. $this->authenticate_user(); // Make a list of all functions user is allowed to execute. $this->init_service_class(); if ($this->wsdlmode) { // Generate the WSDL. $this->generate_wsdl(); } // Log the web service request. $params = array( 'other' => array( 'function' => 'unknown' ) ); /** @var \core\event\webservice_function_called $event */ $event = \core\event\webservice_function_called::create($params); $event->trigger(); // Handle the SOAP request. $this->handle(); // Session cleanup. $this->session_cleanup(); die; } /** * Generates the WSDL. */ protected function generate_wsdl() { // Initialise WSDL. $this->wsdl = new wsdl($this->serviceclass, $this->serverurl); // Register service struct classes as complex types. foreach ($this->servicestructs as $structinfo) { $this->wsdl->add_complex_type($structinfo->classname, $structinfo->properties); } // Register the method for the WSDL generation. foreach ($this->servicemethods as $methodinfo) { $this->wsdl->register($methodinfo->name, $methodinfo->inputparams, $methodinfo->outputparams, $methodinfo->description); } } /** * Handles the web service function call. */ protected function handle() { if ($this->wsdlmode) { // Prepare the response. $this->response = $this->wsdl->to_xml(); // Send the results back in correct format. $this->send_response(); } else { $wsdlurl = clone($this->serverurl); $wsdlurl->param('wsdl', 1); $options = array( 'uri' => $this->serverurl->out(false) ); // Initialise the SOAP server. $this->soapserver = new SoapServer($wsdlurl->out(false), $options); if (!empty($this->serviceclass)) { $this->soapserver->setClass($this->serviceclass); // Get all the methods for the generated service class then register to the SOAP server. $functions = get_class_methods($this->serviceclass); $this->soapserver->addFunction($functions); } // Get soap request from raw POST data. $soaprequest = file_get_contents('php://input'); // Handle the request. try { $this->soapserver->handle($soaprequest); } catch (Exception $e) { $this->fault($e); } } } /** * Send the error information to the WS client formatted as an XML document. * * @param Exception $ex the exception to send back */ protected function send_error($ex = null) { if ($ex) { $info = $ex->getMessage(); if (debugging() and isset($ex->debuginfo)) { $info .= ' - '.$ex->debuginfo; } } else { $info = 'Unknown error'; } // Initialise new DOM document object. $dom = new DOMDocument('1.0', 'UTF-8'); // Fault node. $fault = $dom->createElement('SOAP-ENV:Fault'); // Faultcode node. $fault->appendChild($dom->createElement('faultcode', 'MOODLE:error')); // Faultstring node. $fault->appendChild($dom->createElement('faultstring', $info)); // Body node. $body = $dom->createElement('SOAP-ENV:Body'); $body->appendChild($fault); // Envelope node. $envelope = $dom->createElement('SOAP-ENV:Envelope'); $envelope->setAttribute('xmlns:SOAP-ENV', 'http://schemas.xmlsoap.org/soap/envelope/'); $envelope->appendChild($body); $dom->appendChild($envelope); $this->response = $dom->saveXML(); $this->send_response(); } /** * Send the result of function call to the WS client. */ protected function send_response() { $this->send_headers(); echo $this->response; } /** * Internal implementation - sending of page headers. */ protected function send_headers() { header('Cache-Control: private, must-revalidate, pre-check=0, post-check=0, max-age=0'); header('Expires: ' . gmdate('D, d M Y H:i:s', 0) . ' GMT'); header('Pragma: no-cache'); header('Accept-Ranges: none'); header('Content-Length: ' . strlen($this->response)); header('Content-Type: application/xml; charset=utf-8'); header('Content-Disposition: inline; filename="response.xml"'); } /** * Generate a server fault. * * Note that the parameter order is the reverse of SoapFault's constructor parameters. * * Moodle note: basically we return the faultactor (errorcode) and faultdetails (debuginfo). * * If an exception is passed as the first argument, its message and code * will be used to create the fault object. * * @link http://www.w3.org/TR/soap12-part1/#faultcodes * @param string|Exception $fault * @param string $code SOAP Fault Codes */ public function fault($fault = null, $code = 'Receiver') { $allowedfaultmodes = array( 'VersionMismatch', 'MustUnderstand', 'DataEncodingUnknown', 'Sender', 'Receiver', 'Server' ); if (!in_array($code, $allowedfaultmodes)) { $code = 'Receiver'; } // Intercept any exceptions and add the errorcode and debuginfo (optional). $actor = null; $details = null; $errorcode = 'unknownerror'; $message = get_string($errorcode); if ($fault instanceof Exception) { // Add the debuginfo to the exception message if debuginfo must be returned. $actor = isset($fault->errorcode) ? $fault->errorcode : null; $errorcode = $actor; if (debugging()) { $message = $fault->getMessage(); $details = isset($fault->debuginfo) ? $fault->debuginfo : null; } } else if (is_string($fault)) { $message = $fault; } $this->soapserver->fault($code, $message . ' | ERRORCODE: ' . $errorcode, $actor, $details); } } /** * SOAP test client class * * @package webservice_soap * @copyright 2009 Petr Skodak * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @since Moodle 2.0 */ class webservice_soap_test_client implements webservice_test_client_interface { /** * Execute test client WS request * * @param string $serverurl server url (including token parameter or username/password parameters) * @param string $function function name * @param array $params parameters of the called function * @return mixed */ public function simpletest($serverurl, $function, $params) { global $CFG; require_once($CFG->dirroot . '/webservice/soap/lib.php'); $client = new webservice_soap_client($serverurl); return $client->call($function, $params); } } home/smkb8269/public_html/lms.smk-almanshuriyah.sch.id/enrol/cohort/locallib.php000064400000032505152266571660023615 0ustar00. /** * Local stuff for cohort enrolment plugin. * * @package enrol_cohort * @copyright 2010 Petr Skoda {@link http://skodak.org} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); require_once($CFG->dirroot . '/enrol/locallib.php'); require_once($CFG->dirroot . '/cohort/lib.php'); /** * Event handler for cohort enrolment plugin. * * We try to keep everything in sync via listening to events, * it may fail sometimes, so we always do a full sync in cron too. */ class enrol_cohort_handler { /** * Event processor - cohort member added. * @param \core\event\cohort_member_added $event * @return bool */ public static function member_added(\core\event\cohort_member_added $event) { global $DB, $CFG; require_once("$CFG->dirroot/group/lib.php"); if (!enrol_is_enabled('cohort')) { return true; } // Does any enabled cohort instance want to sync with this cohort? $sql = "SELECT e.*, r.id as roleexists FROM {enrol} e LEFT JOIN {role} r ON (r.id = e.roleid) WHERE e.customint1 = :cohortid AND e.enrol = 'cohort' AND e.status = :enrolstatus ORDER BY e.id ASC"; $params['cohortid'] = $event->objectid; $params['enrolstatus'] = ENROL_INSTANCE_ENABLED; if (!$instances = $DB->get_records_sql($sql, $params)) { return true; } $plugin = enrol_get_plugin('cohort'); foreach ($instances as $instance) { if ($instance->status != ENROL_INSTANCE_ENABLED ) { // No roles for disabled instances. $instance->roleid = 0; } else if ($instance->roleid and !$instance->roleexists) { // Invalid role - let's just enrol, they will have to create new sync and delete this one. $instance->roleid = 0; } unset($instance->roleexists); // No problem if already enrolled. $plugin->enrol_user($instance, $event->relateduserid, $instance->roleid, 0, 0, ENROL_USER_ACTIVE); // Sync groups. if ($instance->customint2) { if (!groups_is_member($instance->customint2, $event->relateduserid)) { if ($group = $DB->get_record('groups', array('id'=>$instance->customint2, 'courseid'=>$instance->courseid))) { groups_add_member($group->id, $event->relateduserid, 'enrol_cohort', $instance->id); } } } } return true; } /** * Event processor - cohort member removed. * @param \core\event\cohort_member_removed $event * @return bool */ public static function member_removed(\core\event\cohort_member_removed $event) { global $DB; // Does anything want to sync with this cohort? if (!$instances = $DB->get_records('enrol', array('customint1'=>$event->objectid, 'enrol'=>'cohort'), 'id ASC')) { return true; } $plugin = enrol_get_plugin('cohort'); $unenrolaction = $plugin->get_config('unenrolaction', ENROL_EXT_REMOVED_UNENROL); foreach ($instances as $instance) { if (!$ue = $DB->get_record('user_enrolments', array('enrolid'=>$instance->id, 'userid'=>$event->relateduserid))) { continue; } if ($unenrolaction == ENROL_EXT_REMOVED_UNENROL) { $plugin->unenrol_user($instance, $event->relateduserid); } else { if ($ue->status != ENROL_USER_SUSPENDED) { $plugin->update_user_enrol($instance, $ue->userid, ENROL_USER_SUSPENDED); $context = context_course::instance($instance->courseid); if ($unenrolaction != ENROL_EXT_REMOVED_SUSPEND) { role_unassign_all(array('userid' => $ue->userid, 'contextid' => $context->id, 'component' => 'enrol_cohort', 'itemid' => $instance->id)); } } } } return true; } /** * Event processor - cohort deleted. * @param \core\event\cohort_deleted $event * @return bool */ public static function deleted(\core\event\cohort_deleted $event) { global $DB; // Does anything want to sync with this cohort? if (!$instances = $DB->get_records('enrol', array('customint1'=>$event->objectid, 'enrol'=>'cohort'), 'id ASC')) { return true; } $plugin = enrol_get_plugin('cohort'); $unenrolaction = $plugin->get_config('unenrolaction', ENROL_EXT_REMOVED_UNENROL); foreach ($instances as $instance) { if ($unenrolaction != ENROL_EXT_REMOVED_UNENROL) { $context = context_course::instance($instance->courseid); if ($unenrolaction != ENROL_EXT_REMOVED_SUSPEND) { role_unassign_all(array('contextid' => $context->id, 'component' => 'enrol_cohort', 'itemid' => $instance->id)); } $plugin->update_status($instance, ENROL_INSTANCE_DISABLED); } else { $plugin->delete_instance($instance); } } return true; } } /** * Sync all cohort course links. * @param progress_trace $trace * @param int $courseid one course, empty mean all * @return int 0 means ok, 1 means error, 2 means plugin disabled */ function enrol_cohort_sync(progress_trace $trace, $courseid = NULL) { global $CFG, $DB; require_once("$CFG->dirroot/group/lib.php"); // Purge all roles if cohort sync disabled, those can be recreated later here by cron or CLI. if (!enrol_is_enabled('cohort')) { $trace->output('Cohort sync plugin is disabled, unassigning all plugin roles and stopping.'); role_unassign_all(array('component'=>'enrol_cohort')); return 2; } // Unfortunately this may take a long time, this script can be interrupted without problems. core_php_time_limit::raise(); raise_memory_limit(MEMORY_HUGE); $trace->output('Starting user enrolment synchronisation...'); $allroles = get_all_roles(); $instances = array(); //cache $plugin = enrol_get_plugin('cohort'); $unenrolaction = $plugin->get_config('unenrolaction', ENROL_EXT_REMOVED_UNENROL); // Iterate through all not enrolled yet users. $onecourse = $courseid ? "AND e.courseid = :courseid" : ""; $sql = "SELECT cm.userid, e.id AS enrolid, ue.status FROM {cohort_members} cm JOIN {enrol} e ON (e.customint1 = cm.cohortid AND e.enrol = 'cohort' AND e.status = :enrolstatus $onecourse) JOIN {user} u ON (u.id = cm.userid AND u.deleted = 0) LEFT JOIN {user_enrolments} ue ON (ue.enrolid = e.id AND ue.userid = cm.userid) WHERE ue.id IS NULL OR ue.status = :suspended"; $params = array(); $params['courseid'] = $courseid; $params['suspended'] = ENROL_USER_SUSPENDED; $params['enrolstatus'] = ENROL_INSTANCE_ENABLED; $rs = $DB->get_recordset_sql($sql, $params); foreach($rs as $ue) { if (!isset($instances[$ue->enrolid])) { $instances[$ue->enrolid] = $DB->get_record('enrol', array('id'=>$ue->enrolid)); } $instance = $instances[$ue->enrolid]; if ($ue->status == ENROL_USER_SUSPENDED) { $plugin->update_user_enrol($instance, $ue->userid, ENROL_USER_ACTIVE); $trace->output("unsuspending: $ue->userid ==> $instance->courseid via cohort $instance->customint1", 1); } else { $plugin->enrol_user($instance, $ue->userid); $trace->output("enrolling: $ue->userid ==> $instance->courseid via cohort $instance->customint1", 1); } } $rs->close(); // Unenrol as necessary. $sql = "SELECT ue.*, e.courseid FROM {user_enrolments} ue JOIN {enrol} e ON (e.id = ue.enrolid AND e.enrol = 'cohort' $onecourse) LEFT JOIN {cohort_members} cm ON (cm.cohortid = e.customint1 AND cm.userid = ue.userid) WHERE cm.id IS NULL"; $rs = $DB->get_recordset_sql($sql, array('courseid'=>$courseid)); foreach($rs as $ue) { if (!isset($instances[$ue->enrolid])) { $instances[$ue->enrolid] = $DB->get_record('enrol', array('id'=>$ue->enrolid)); } $instance = $instances[$ue->enrolid]; if ($unenrolaction == ENROL_EXT_REMOVED_UNENROL) { // Remove enrolment together with group membership, grades, preferences, etc. $plugin->unenrol_user($instance, $ue->userid); $trace->output("unenrolling: $ue->userid ==> $instance->courseid via cohort $instance->customint1", 1); } else { // ENROL_EXT_REMOVED_SUSPENDNOROLES // Just disable and ignore any changes. if ($ue->status != ENROL_USER_SUSPENDED) { $plugin->update_user_enrol($instance, $ue->userid, ENROL_USER_SUSPENDED); $context = context_course::instance($instance->courseid); if ($unenrolaction != ENROL_EXT_REMOVED_SUSPEND) { role_unassign_all(array('userid' => $ue->userid, 'contextid' => $context->id, 'component' => 'enrol_cohort', 'itemid' => $instance->id)); $trace->output("unsassigning all roles: $ue->userid ==> $instance->courseid", 1); } $trace->output("suspending: $ue->userid ==> $instance->courseid", 1); } } } $rs->close(); unset($instances); // Now assign all necessary roles to enrolled users - skip suspended instances and users. $onecourse = $courseid ? "AND e.courseid = :courseid" : ""; $sql = "SELECT e.roleid, ue.userid, c.id AS contextid, e.id AS itemid, e.courseid FROM {user_enrolments} ue JOIN {enrol} e ON (e.id = ue.enrolid AND e.enrol = 'cohort' AND e.status = :statusenabled $onecourse) JOIN {role} r ON (r.id = e.roleid) JOIN {context} c ON (c.instanceid = e.courseid AND c.contextlevel = :coursecontext) JOIN {user} u ON (u.id = ue.userid AND u.deleted = 0) LEFT JOIN {role_assignments} ra ON (ra.contextid = c.id AND ra.userid = ue.userid AND ra.itemid = e.id AND ra.component = 'enrol_cohort' AND e.roleid = ra.roleid) WHERE ue.status = :useractive AND ra.id IS NULL"; $params = array(); $params['statusenabled'] = ENROL_INSTANCE_ENABLED; $params['useractive'] = ENROL_USER_ACTIVE; $params['coursecontext'] = CONTEXT_COURSE; $params['courseid'] = $courseid; $rs = $DB->get_recordset_sql($sql, $params); foreach($rs as $ra) { role_assign($ra->roleid, $ra->userid, $ra->contextid, 'enrol_cohort', $ra->itemid); $trace->output("assigning role: $ra->userid ==> $ra->courseid as ".$allroles[$ra->roleid]->shortname, 1); } $rs->close(); if ($unenrolaction != ENROL_EXT_REMOVED_SUSPEND) { // Remove unwanted roles - sync role can not be changed, we only remove role when unenrolled. $onecourse = $courseid ? "AND e.courseid = :courseid" : ""; $sql = "SELECT ra.roleid, ra.userid, ra.contextid, ra.itemid, e.courseid FROM {role_assignments} ra JOIN {context} c ON (c.id = ra.contextid AND c.contextlevel = :coursecontext) JOIN {enrol} e ON (e.id = ra.itemid AND e.enrol = 'cohort' $onecourse) LEFT JOIN {user_enrolments} ue ON (ue.enrolid = e.id AND ue.userid = ra.userid AND ue.status = :useractive) WHERE ra.component = 'enrol_cohort' AND (ue.id IS NULL OR e.status <> :statusenabled)"; $params = array(); $params['statusenabled'] = ENROL_INSTANCE_ENABLED; $params['useractive'] = ENROL_USER_ACTIVE; $params['coursecontext'] = CONTEXT_COURSE; $params['courseid'] = $courseid; $rs = $DB->get_recordset_sql($sql, $params); foreach ($rs as $ra) { role_unassign($ra->roleid, $ra->userid, $ra->contextid, 'enrol_cohort', $ra->itemid); $trace->output("unassigning role: $ra->userid ==> $ra->courseid as ".$allroles[$ra->roleid]->shortname, 1); } $rs->close(); } // Finally sync groups. $affectedusers = groups_sync_with_enrolment('cohort', $courseid); foreach ($affectedusers['removed'] as $gm) { $trace->output("removing user from group: $gm->userid ==> $gm->courseid - $gm->groupname", 1); } foreach ($affectedusers['added'] as $ue) { $trace->output("adding user to group: $ue->userid ==> $ue->courseid - $ue->groupname", 1); } $trace->output('...user enrolment synchronisation finished.'); return 0; } home/smkb8269/public_html/lms.smk-almanshuriyah.sch.id/enrol/guest/locallib.php000064400000006530152266574240023442 0ustar00. defined('MOODLE_INTERNAL') || die(); require_once("$CFG->libdir/formslib.php"); /** * Guest access plugin implementation. * * @deprecated since Moodle 5.0 - please use {@see enrol_guest\form\enrol_form} * * @package enrol_guest * @copyright 2010 Petr Skoda {@link http://skodak.org} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ #[\core\attribute\deprecated(replacement: enrol_guest\form\enrol_form::class, since: '5.0', reason: 'Now a dynamic form is used')] class enrol_guest_enrol_form extends moodleform { protected $instance; /** * Constructor * * @param mixed $action * @param mixed $customdata * @param string $method * @param string $target * @param mixed $attributes * @param bool $editable * @param array $ajaxformdata */ public function __construct($action=null, $customdata=null, $method='post', $target='', $attributes=null, $editable=true, $ajaxformdata=null) { \core\deprecation::emit_deprecation([$this, __FUNCTION__]); parent::__construct($action, $customdata, $method, $target, $attributes, $editable, $ajaxformdata); } public function definition() { $mform = $this->_form; $instance = $this->_customdata; $this->instance = $instance; $plugin = enrol_get_plugin('guest'); $heading = $plugin->get_instance_name($instance); $mform->addElement('header', 'guestheader', $heading); $mform->addElement('password', 'guestpassword', get_string('password', 'enrol_guest')); $this->add_action_buttons(false, get_string('submit')); $mform->addElement('hidden', 'id'); $mform->setType('id', PARAM_INT); $mform->setDefault('id', $instance->courseid); $mform->addElement('hidden', 'instance'); $mform->setType('instance', PARAM_INT); $mform->setDefault('instance', $instance->id); } public function validation($data, $files) { global $DB, $CFG; $errors = parent::validation($data, $files); $instance = $this->instance; if ($instance->password !== '') { if ($data['guestpassword'] !== $instance->password) { $plugin = enrol_get_plugin('guest'); if ($plugin->get_config('showhint')) { $hint = core_text::substr($instance->password, 0, 1); $errors['guestpassword'] = get_string('passwordinvalidhint', 'enrol_guest', $hint); } else { $errors['guestpassword'] = get_string('passwordinvalid', 'enrol_guest'); } } } return $errors; } } home/smkb8269/public_html/lms.smk-almanshuriyah.sch.id/webservice/rest/locallib.php000064400000025356152266574510024316 0ustar00. /** * REST web service implementation classes and methods. * * @package webservice_rest * @copyright 2009 Jerome Mouneyrac * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ use core_external\external_multiple_structure; use core_external\external_single_structure; use core_external\external_value; require_once("$CFG->dirroot/webservice/lib.php"); /** * REST service server implementation. * * @package webservice_rest * @copyright 2009 Petr Skoda (skodak) * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class webservice_rest_server extends webservice_base_server { /** @var string return method ('xml' or 'json') */ protected $restformat; /** * Contructor * * @param string $authmethod authentication method of the web service (WEBSERVICE_AUTHMETHOD_PERMANENT_TOKEN, ...) * @param string $restformat Format of the return values: 'xml' or 'json' */ public function __construct($authmethod) { parent::__construct($authmethod); $this->wsname = 'rest'; } /** * Set the request format to. */ public function set_rest_format(): void { // Get GET and POST parameters. $methodvariables = array_merge($_GET, $_POST); // Retrieve REST format parameter - 'xml' (default) or 'json'. $restformatisset = isset($methodvariables['moodlewsrestformat']) && (($methodvariables['moodlewsrestformat'] == 'xml' || $methodvariables['moodlewsrestformat'] == 'json')); $this->restformat = $restformatisset ? $methodvariables['moodlewsrestformat'] : 'xml'; } /** * This method parses the $_POST and $_GET superglobals and looks for * the following information: * 1/ user authentication - username+password or token (wsusername, wspassword and wstoken parameters) * 2/ function name (wsfunction parameter) * 3/ function parameters (all other parameters except those above) * 4/ text format parameters * 5/ return rest format xml/json */ protected function parse_request() { // Retrieve and clean the POST/GET parameters from the parameters specific to the server. parent::set_web_service_call_settings(); // Get GET and POST parameters. $methodvariables = array_merge($_GET, $_POST); $this->set_rest_format(); unset($methodvariables['moodlewsrestformat']); if ($this->authmethod == WEBSERVICE_AUTHMETHOD_USERNAME) { $this->username = isset($methodvariables['wsusername']) ? $methodvariables['wsusername'] : null; unset($methodvariables['wsusername']); $this->password = isset($methodvariables['wspassword']) ? $methodvariables['wspassword'] : null; unset($methodvariables['wspassword']); $this->functionname = isset($methodvariables['wsfunction']) ? $methodvariables['wsfunction'] : null; unset($methodvariables['wsfunction']); $this->parameters = $methodvariables; } else { $this->token = isset($methodvariables['wstoken']) ? $methodvariables['wstoken'] : null; unset($methodvariables['wstoken']); $this->functionname = isset($methodvariables['wsfunction']) ? $methodvariables['wsfunction'] : null; unset($methodvariables['wsfunction']); $this->parameters = $methodvariables; } } /** * Send the result of function call to the WS client * formatted as XML document. */ protected function send_response() { //Check that the returned values are valid try { if ($this->function->returns_desc != null) { $validatedvalues = \core_external\external_api::clean_returnvalue( $this->function->returns_desc, $this->returns ); } else { $validatedvalues = null; } } catch (Exception $ex) { $exception = $ex; } if (!empty($exception)) { $response = $this->generate_error($exception); } else { //We can now convert the response to the requested REST format if ($this->restformat == 'json') { $response = json_encode($validatedvalues); } else { $response = ''."\n"; $response .= ''."\n"; $response .= self::xmlize_result($validatedvalues, $this->function->returns_desc); $response .= ''."\n"; } } $this->send_headers(); echo $response; } /** * Send the error information to the WS client * formatted as XML document. * Note: the exception is never passed as null, * it only matches the abstract function declaration. * @param exception $ex the exception that we are sending */ protected function send_error($ex=null) { // Unless debugging is completely off, log the error to server error log. if (debugging('', DEBUG_MINIMAL)) { $info = get_exception_info($ex); // This format is the same as default_exception_handler() in setuplib.php but with the // word 'REST' instead of 'Default', to make it easy to reuse any existing processing. error_log('REST exception handler: ' . $info->message . ' Debug: ' . $info->debuginfo . "\n" . format_backtrace($info->backtrace, true)); } $this->send_headers(); echo $this->generate_error($ex); } /** * Build the error information matching the REST returned value format (JSON or XML) * @param exception $ex the exception we are converting in the server rest format * @return string the error in the requested REST format */ protected function generate_error($ex) { if ($this->restformat == 'json') { $errorobject = new stdClass; $errorobject->exception = get_class($ex); if (isset($ex->errorcode)) { $errorobject->errorcode = $ex->errorcode; } $errorobject->message = $ex->getMessage(); if (debugging() and isset($ex->debuginfo)) { $errorobject->debuginfo = $ex->debuginfo; } $error = json_encode($errorobject); } else { $error = ''."\n"; $error .= ''."\n"; if (isset($ex->errorcode)) { $error .= '' . htmlspecialchars($ex->errorcode, ENT_COMPAT, 'UTF-8') . '' . "\n"; } $error .= ''.htmlspecialchars($ex->getMessage(), ENT_COMPAT, 'UTF-8').''."\n"; if (debugging() and isset($ex->debuginfo)) { $error .= ''.htmlspecialchars($ex->debuginfo, ENT_COMPAT, 'UTF-8').''."\n"; } $error .= ''."\n"; } return $error; } /** * Internal implementation - sending of page headers. */ protected function send_headers() { if ($this->restformat == 'json') { header('Content-type: application/json'); } else { header('Content-Type: application/xml; charset=utf-8'); header('Content-Disposition: inline; filename="response.xml"'); } header('Cache-Control: private, must-revalidate, pre-check=0, post-check=0, max-age=0'); header('Expires: '. gmdate('D, d M Y H:i:s', 0) .' GMT'); header('Pragma: no-cache'); header('Accept-Ranges: none'); // Allow cross-origin requests only for Web Services. // This allow to receive requests done by Web Workers or webapps in different domains. header('Access-Control-Allow-Origin: *'); } /** * Internal implementation - recursive function producing XML markup. * * @param mixed $returns the returned values * @param external_description $desc * @return string */ protected static function xmlize_result($returns, $desc) { if ($desc === null) { return ''; } else if ($desc instanceof external_value) { if (is_bool($returns)) { // we want 1/0 instead of true/false here $returns = (int)$returns; } if (is_null($returns)) { return ''."\n"; } else { return ''.htmlspecialchars($returns, ENT_COMPAT, 'UTF-8').''."\n"; } } else if ($desc instanceof external_multiple_structure) { $mult = ''."\n"; if (!empty($returns)) { foreach ($returns as $val) { $mult .= self::xmlize_result($val, $desc->content); } } $mult .= ''."\n"; return $mult; } else if ($desc instanceof external_single_structure) { $single = ''."\n"; foreach ($desc->keys as $key=>$subdesc) { $value = isset($returns[$key]) ? $returns[$key] : null; $single .= ''.self::xmlize_result($value, $subdesc).''."\n"; } $single .= ''."\n"; return $single; } } } /** * REST test client class * * @package webservice_rest * @copyright 2009 Petr Skoda (skodak) * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class webservice_rest_test_client implements webservice_test_client_interface { /** * Execute test client WS request * @param string $serverurl server url (including token parameter or username/password parameters) * @param string $function function name * @param array $params parameters of the called function * @return mixed */ public function simpletest($serverurl, $function, $params) { return download_file_content($serverurl.'&wsfunction='.$function, null, $params); } } home/smkb8269/public_html/lms.smk-almanshuriyah.sch.id/enrol/locallib.php000064400000173201152266574560022320 0ustar00. /** * This file contains the course_enrolment_manager class which is used to interface * with the functions that exist in enrollib.php in relation to a single course. * * @package core_enrol * @copyright 2010 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ use core_user\fields; defined('MOODLE_INTERNAL') || die(); /** * This class provides a targeted tied together means of interfacing the enrolment * tasks together with a course. * * It is provided as a convenience more than anything else. * * @copyright 2010 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class course_enrolment_manager { /** * The course context * @var context */ protected $context; /** * The course we are managing enrolments for * @var stdClass */ protected $course = null; /** * Limits the focus of the manager to one enrolment plugin instance * @var string */ protected $instancefilter = null; /** * Limits the focus of the manager to users with specified role * @var int */ protected $rolefilter = 0; /** * Limits the focus of the manager to users who match search string * @var string */ protected $searchfilter = ''; /** * Limits the focus of the manager to users in specified group * @var int */ protected $groupfilter = 0; /** * Limits the focus of the manager to users who match status active/inactive * @var int */ protected $statusfilter = -1; /** * The total number of users enrolled in the course * Populated by course_enrolment_manager::get_total_users * @var int */ protected $totalusers = null; /** * An array of users currently enrolled in the course * Populated by course_enrolment_manager::get_users * @var array */ protected $users = array(); /** * An array of users who have roles within this course but who have not * been enrolled in the course * @var array */ protected $otherusers = array(); /** * The total number of users who hold a role within the course but who * arn't enrolled. * @var int */ protected $totalotherusers = null; /** * The current moodle_page object * @var moodle_page */ protected $moodlepage = null; /**#@+ * These variables are used to cache the information this class uses * please never use these directly instead use their get_ counterparts. * @access private * @var array */ private $_instancessql = null; private $_instances = null; private $_inames = null; private $_plugins = null; private $_allplugins = null; private $_roles = null; private $_visibleroles = null; private $_assignableroles = null; private $_assignablerolesothers = null; private $_groups = null; /**#@-*/ /** * Constructs the course enrolment manager * * @param moodle_page $moodlepage * @param stdClass $course * @param string $instancefilter * @param int $rolefilter If non-zero, filters to users with specified role * @param string $searchfilter If non-blank, filters to users with search text * @param int $groupfilter if non-zero, filter users with specified group * @param int $statusfilter if not -1, filter users with active/inactive enrollment. */ public function __construct(moodle_page $moodlepage, $course, $instancefilter = null, $rolefilter = 0, $searchfilter = '', $groupfilter = 0, $statusfilter = -1) { $this->moodlepage = $moodlepage; $this->context = context_course::instance($course->id); $this->course = $course; $this->instancefilter = $instancefilter; $this->rolefilter = $rolefilter; $this->searchfilter = $searchfilter; $this->groupfilter = $groupfilter; $this->statusfilter = $statusfilter; } /** * Returns the current moodle page * @return moodle_page */ public function get_moodlepage() { return $this->moodlepage; } /** * Returns the total number of enrolled users in the course. * * If a filter was specificed this will be the total number of users enrolled * in this course by means of that instance. * * @global moodle_database $DB * @return int */ public function get_total_users() { global $DB; if ($this->totalusers === null) { list($instancessql, $params, $filter) = $this->get_instance_sql(); list($filtersql, $moreparams) = $this->get_filter_sql(); $params += $moreparams; $sqltotal = "SELECT COUNT(DISTINCT u.id) FROM {user} u JOIN {user_enrolments} ue ON (ue.userid = u.id AND ue.enrolid $instancessql) JOIN {enrol} e ON (e.id = ue.enrolid)"; if ($this->groupfilter) { $sqltotal .= " LEFT JOIN ({groups_members} gm JOIN {groups} g ON (g.id = gm.groupid)) ON (u.id = gm.userid AND g.courseid = e.courseid)"; } $sqltotal .= "WHERE $filtersql"; $this->totalusers = (int)$DB->count_records_sql($sqltotal, $params); } return $this->totalusers; } /** * Returns the total number of enrolled users in the course. * * If a filter was specificed this will be the total number of users enrolled * in this course by means of that instance. * * @global moodle_database $DB * @return int */ public function get_total_other_users() { global $DB; if ($this->totalotherusers === null) { list($ctxcondition, $params) = $DB->get_in_or_equal($this->context->get_parent_context_ids(true), SQL_PARAMS_NAMED, 'ctx'); $params['courseid'] = $this->course->id; $sql = "SELECT COUNT(DISTINCT u.id) FROM {role_assignments} ra JOIN {user} u ON u.id = ra.userid JOIN {context} ctx ON ra.contextid = ctx.id LEFT JOIN ( SELECT ue.id, ue.userid FROM {user_enrolments} ue LEFT JOIN {enrol} e ON e.id=ue.enrolid WHERE e.courseid = :courseid ) ue ON ue.userid=u.id WHERE ctx.id $ctxcondition AND ue.id IS NULL"; $this->totalotherusers = (int)$DB->count_records_sql($sql, $params); } return $this->totalotherusers; } /** * Gets all of the users enrolled in this course. * * If a filter was specified this will be the users who were enrolled * in this course by means of that instance. If role or search filters were * specified then these will also be applied. * * @global moodle_database $DB * @param string $sort * @param string $direction ASC or DESC * @param int $page First page should be 0 * @param int $perpage Defaults to 25 * @return array */ public function get_users($sort, $direction='ASC', $page=0, $perpage=25) { global $DB; if ($direction !== 'ASC') { $direction = 'DESC'; } $key = md5("$sort-$direction-$page-$perpage"); if (!array_key_exists($key, $this->users)) { list($instancessql, $params, $filter) = $this->get_instance_sql(); list($filtersql, $moreparams) = $this->get_filter_sql(); $params += $moreparams; $userfields = fields::for_identity($this->get_context())->with_userpic()->excluding('lastaccess'); ['selects' => $fieldselect, 'joins' => $fieldjoin, 'params' => $fieldjoinparams] = (array)$userfields->get_sql('u', true, '', '', false); $params += $fieldjoinparams; $sql = "SELECT DISTINCT $fieldselect, COALESCE(ul.timeaccess, 0) AS lastcourseaccess FROM {user} u JOIN {user_enrolments} ue ON (ue.userid = u.id AND ue.enrolid $instancessql) JOIN {enrol} e ON (e.id = ue.enrolid) $fieldjoin LEFT JOIN {user_lastaccess} ul ON (ul.courseid = e.courseid AND ul.userid = u.id)"; if ($this->groupfilter) { $sql .= " LEFT JOIN ({groups_members} gm JOIN {groups} g ON (g.id = gm.groupid)) ON (u.id = gm.userid AND g.courseid = e.courseid)"; } $sql .= "WHERE $filtersql ORDER BY $sort $direction"; $this->users[$key] = $DB->get_records_sql($sql, $params, $page*$perpage, $perpage); } return $this->users[$key]; } /** * Obtains WHERE clause to filter results by defined search and role filter * (instance filter is handled separately in JOIN clause, see * get_instance_sql). * * @return array Two-element array with SQL and params for WHERE clause */ protected function get_filter_sql() { global $DB; // Search condition. // TODO Does not support custom user profile fields (MDL-70456). $extrafields = fields::get_identity_fields($this->get_context(), false); list($sql, $params) = users_search_sql($this->searchfilter, 'u', USER_SEARCH_CONTAINS, $extrafields); // Role condition. if ($this->rolefilter) { // Get context SQL. $contextids = $this->context->get_parent_context_ids(); $contextids[] = $this->context->id; list($contextsql, $contextparams) = $DB->get_in_or_equal($contextids, SQL_PARAMS_NAMED); $params += $contextparams; // Role check condition. $sql .= " AND (SELECT COUNT(1) FROM {role_assignments} ra WHERE ra.userid = u.id " . "AND ra.roleid = :roleid AND ra.contextid $contextsql) > 0"; $params['roleid'] = $this->rolefilter; } // Group condition. if ($this->groupfilter) { if ($this->groupfilter < 0) { // Show users who are not in any group. $sql .= " AND gm.groupid IS NULL"; } else { $sql .= " AND gm.groupid = :groupid"; $params['groupid'] = $this->groupfilter; } } // Status condition. if ($this->statusfilter === ENROL_USER_ACTIVE) { $sql .= " AND ue.status = :active AND e.status = :enabled AND ue.timestart < :now1 AND (ue.timeend = 0 OR ue.timeend > :now2)"; $now = round(time(), -2); // rounding helps caching in DB $params += array('enabled' => ENROL_INSTANCE_ENABLED, 'active' => ENROL_USER_ACTIVE, 'now1' => $now, 'now2' => $now); } else if ($this->statusfilter === ENROL_USER_SUSPENDED) { $sql .= " AND (ue.status = :inactive OR e.status = :disabled OR ue.timestart > :now1 OR (ue.timeend <> 0 AND ue.timeend < :now2))"; $now = round(time(), -2); // rounding helps caching in DB $params += array('disabled' => ENROL_INSTANCE_DISABLED, 'inactive' => ENROL_USER_SUSPENDED, 'now1' => $now, 'now2' => $now); } return array($sql, $params); } /** * Gets and array of other users. * * Other users are users who have been assigned roles or inherited roles * within this course but who have not been enrolled in the course * * @global moodle_database $DB * @param string $sort * @param string $direction * @param int $page * @param int $perpage * @return array */ public function get_other_users($sort, $direction='ASC', $page=0, $perpage=25) { global $DB; if ($direction !== 'ASC') { $direction = 'DESC'; } $key = md5("$sort-$direction-$page-$perpage"); if (!array_key_exists($key, $this->otherusers)) { list($ctxcondition, $params) = $DB->get_in_or_equal($this->context->get_parent_context_ids(true), SQL_PARAMS_NAMED, 'ctx'); $params['courseid'] = $this->course->id; $params['cid'] = $this->course->id; $userfields = fields::for_identity($this->get_context())->with_userpic(); ['selects' => $fieldselect, 'joins' => $fieldjoin, 'params' => $fieldjoinparams] = (array)$userfields->get_sql('u', true); $params += $fieldjoinparams; $sql = "SELECT ra.id as raid, ra.contextid, ra.component, ctx.contextlevel, ra.roleid, coalesce(u.lastaccess,0) AS lastaccess $fieldselect FROM {role_assignments} ra JOIN {user} u ON u.id = ra.userid JOIN {context} ctx ON ra.contextid = ctx.id $fieldjoin LEFT JOIN ( SELECT ue.id, ue.userid FROM {user_enrolments} ue JOIN {enrol} e ON e.id = ue.enrolid WHERE e.courseid = :courseid ) ue ON ue.userid=u.id WHERE ctx.id $ctxcondition AND ue.id IS NULL ORDER BY $sort $direction, ctx.depth DESC"; $this->otherusers[$key] = $DB->get_records_sql($sql, $params, $page*$perpage, $perpage); } return $this->otherusers[$key]; } /** * Helper method used by {@link get_potential_users()} and {@link search_other_users()}. * * @param string $search the search term, if any. * @param bool $searchanywhere Can the search term be anywhere, or must it be at the start. * @return array with three elements: * string list of fields to SELECT, * string possible database joins for user fields * string contents of SQL WHERE clause, * array query params. Note that the SQL snippets use named parameters. */ protected function get_basic_search_conditions($search, $searchanywhere) { global $DB, $CFG; // Get custom user field SQL used for querying all the fields we need (identity, name, and // user picture). $userfields = fields::for_identity($this->context)->with_name()->with_userpic() ->excluding('username', 'lastaccess', 'maildisplay'); ['selects' => $fieldselects, 'joins' => $fieldjoins, 'params' => $params, 'mappings' => $mappings] = (array)$userfields->get_sql('u', true, '', '', false); // Searchable fields are only the identity and name ones (not userpic, and without exclusions). $searchablefields = fields::for_identity($this->context)->with_name(); $searchable = array_fill_keys($searchablefields->get_required_fields(), true); if (array_key_exists('username', $searchable)) { // Add the username into the mappings list from the other query, because it was excluded. $mappings['username'] = 'u.username'; } // Add some additional sensible conditions $tests = array("u.id <> :guestid", 'u.deleted = 0', 'u.confirmed = 1'); $params['guestid'] = $CFG->siteguest; if (!empty($search)) { // Include identity and name fields as conditions. foreach ($mappings as $fieldname => $fieldsql) { if (array_key_exists($fieldname, $searchable)) { $conditions[] = $fieldsql; } } $conditions[] = $DB->sql_fullname('u.firstname', 'u.lastname'); if ($searchanywhere) { $searchparam = '%' . $search . '%'; } else { $searchparam = $search . '%'; } $i = 0; foreach ($conditions as $key => $condition) { $conditions[$key] = $DB->sql_like($condition, ":con{$i}00", false); $params["con{$i}00"] = $searchparam; $i++; } $tests[] = '(' . implode(' OR ', $conditions) . ')'; } $wherecondition = implode(' AND ', $tests); $selects = $fieldselects . ', u.username, u.lastaccess, u.maildisplay'; return [$selects, $fieldjoins, $params, $wherecondition]; } /** * Helper method used by {@link get_potential_users()} and {@link search_other_users()}. * * @param string $search the search string, if any. * @param string $fields the first bit of the SQL when returning some users. * @param string $countfields fhe first bit of the SQL when counting the users. * @param string $sql the bulk of the SQL statement. * @param array $params query parameters. * @param int $page which page number of the results to show. * @param int $perpage number of users per page. * @param int $addedenrollment number of users added to enrollment. * @param bool $returnexactcount Return the exact total users using count_record or not. * @return array with two or three elements: * int totalusers Number users matching the search. (This element only exist if $returnexactcount was set to true) * array users List of user objects returned by the query. * boolean moreusers True if there are still more users, otherwise is False. * @throws dml_exception */ protected function execute_search_queries($search, $fields, $countfields, $sql, array $params, $page, $perpage, $addedenrollment = 0, $returnexactcount = false) { global $DB, $CFG; list($sort, $sortparams) = users_order_by_sql('u', $search, $this->get_context()); $order = ' ORDER BY ' . $sort; $totalusers = 0; $moreusers = false; $results = []; $availableusers = $DB->get_records_sql($fields . $sql . $order, array_merge($params, $sortparams), ($page * $perpage) - $addedenrollment, $perpage + 1); if ($availableusers) { $totalusers = count($availableusers); $moreusers = $totalusers > $perpage; if ($moreusers) { // We need to discard the last record. array_pop($availableusers); } if ($returnexactcount && $moreusers) { // There is more data. We need to do the exact count. $totalusers = $DB->count_records_sql($countfields . $sql, $params); } } $results['users'] = $availableusers; $results['moreusers'] = $moreusers; if ($returnexactcount) { // Include totalusers in result if $returnexactcount flag is true. $results['totalusers'] = $totalusers; } return $results; } /** * Gets an array of the users that can be enrolled in this course. * * @global moodle_database $DB * @param int $enrolid * @param string $search * @param bool $searchanywhere * @param int $page Defaults to 0 * @param int $perpage Defaults to 25 * @param int $addedenrollment Defaults to 0 * @param bool $returnexactcount Return the exact total users using count_record or not. * @return array with two or three elements: * int totalusers Number users matching the search. (This element only exist if $returnexactcount was set to true) * array users List of user objects returned by the query. * boolean moreusers True if there are still more users, otherwise is False. * @throws dml_exception */ public function get_potential_users($enrolid, $search = '', $searchanywhere = false, $page = 0, $perpage = 25, $addedenrollment = 0, $returnexactcount = false) { global $DB; [$ufields, $joins, $params, $wherecondition] = $this->get_basic_search_conditions($search, $searchanywhere); $fields = 'SELECT '.$ufields; $countfields = 'SELECT COUNT(1)'; $sql = " FROM {user} u $joins LEFT JOIN {user_enrolments} ue ON (ue.userid = u.id AND ue.enrolid = :enrolid) WHERE $wherecondition AND ue.id IS NULL"; $params['enrolid'] = $enrolid; return $this->execute_search_queries($search, $fields, $countfields, $sql, $params, $page, $perpage, $addedenrollment, $returnexactcount); } /** * Searches other users and returns paginated results * * @global moodle_database $DB * @param string $search * @param bool $searchanywhere * @param int $page Starting at 0 * @param int $perpage * @param bool $returnexactcount Return the exact total users using count_record or not. * @return array with two or three elements: * int totalusers Number users matching the search. (This element only exist if $returnexactcount was set to true) * array users List of user objects returned by the query. * boolean moreusers True if there are still more users, otherwise is False. * @throws dml_exception */ public function search_other_users($search = '', $searchanywhere = false, $page = 0, $perpage = 25, $returnexactcount = false) { global $DB, $CFG; [$ufields, $joins, $params, $wherecondition] = $this->get_basic_search_conditions($search, $searchanywhere); $fields = 'SELECT ' . $ufields; $countfields = 'SELECT COUNT(u.id)'; $sql = " FROM {user} u $joins LEFT JOIN {role_assignments} ra ON (ra.userid = u.id AND ra.contextid = :contextid) WHERE $wherecondition AND ra.id IS NULL"; $params['contextid'] = $this->context->id; return $this->execute_search_queries($search, $fields, $countfields, $sql, $params, $page, $perpage, 0, $returnexactcount); } /** * Searches through the enrolled users in this course. * * @param string $search The search term. * @param bool $searchanywhere Can the search term be anywhere, or must it be at the start. * @param int $page Starting at 0. * @param int $perpage Number of users returned per page. * @param bool $returnexactcount Return the exact total users using count_record or not. * @param ?int $contextid Context ID we are in - we might use search on activity level and its group mode can be different from course group mode. * @return array with two or three elements: * int totalusers Number users matching the search. (This element only exist if $returnexactcount was set to true) * array users List of user objects returned by the query. * boolean moreusers True if there are still more users, otherwise is False. */ public function search_users(string $search = '', bool $searchanywhere = false, int $page = 0, int $perpage = 25, bool $returnexactcount = false, ?int $contextid = null) { global $USER; [$ufields, $joins, $params, $wherecondition] = $this->get_basic_search_conditions($search, $searchanywhere); if (isset($contextid)) { // If contextid is set, we need to determine the group mode that should be used (module or course). [$context, $course, $cm] = get_context_info_array($contextid); // If cm instance is returned, then use the group mode from the module, otherwise get the course group mode. $groupmode = $cm ? groups_get_activity_groupmode($cm, $course) : groups_get_course_groupmode($this->course); } else { // Otherwise, default to the group mode of the course. $context = $this->context; $groupmode = groups_get_course_groupmode($this->course); } if ($groupmode == SEPARATEGROUPS && !has_capability('moodle/site:accessallgroups', $context)) { $groups = groups_get_all_groups($this->course->id, $USER->id, 0, 'g.id'); $groupids = array_column($groups, 'id'); if (!$groupids) { return ['totalusers' => 0, 'users' => [], 'moreusers' => false]; } } else { $groupids = []; } [$enrolledsql, $enrolledparams] = get_enrolled_sql($context, '', $groupids); $fields = 'SELECT ' . $ufields; $countfields = 'SELECT COUNT(u.id)'; $sql = " FROM {user} u $joins JOIN ($enrolledsql) je ON je.id = u.id WHERE $wherecondition"; $params = array_merge($params, $enrolledparams); return $this->execute_search_queries($search, $fields, $countfields, $sql, $params, $page, $perpage, 0, $returnexactcount); } /** * Gets an array containing some SQL to user for when selecting, params for * that SQL, and the filter that was used in constructing the sql. * * @global moodle_database $DB * @return array */ protected function get_instance_sql() { global $DB; if ($this->_instancessql === null) { $instances = $this->get_enrolment_instances(); $filter = $this->get_enrolment_filter(); if ($filter && array_key_exists($filter, $instances)) { $sql = " = :ifilter"; $params = array('ifilter'=>$filter); } else { $filter = 0; if ($instances) { list($sql, $params) = $DB->get_in_or_equal(array_keys($this->get_enrolment_instances()), SQL_PARAMS_NAMED); } else { // no enabled instances, oops, we should probably say something $sql = "= :never"; $params = array('never'=>-1); } } $this->instancefilter = $filter; $this->_instancessql = array($sql, $params, $filter); } return $this->_instancessql; } /** * Returns all of the enrolment instances for this course. * * @param bool $onlyenabled Whether to return data from enabled enrolment instance names only. * @return array */ public function get_enrolment_instances($onlyenabled = false) { if ($this->_instances === null) { $this->_instances = enrol_get_instances($this->course->id, $onlyenabled); } return $this->_instances; } /** * Returns the names for all of the enrolment instances for this course. * * @param bool $onlyenabled Whether to return data from enabled enrolment instance names only. * @return array */ public function get_enrolment_instance_names($onlyenabled = false) { if ($this->_inames === null) { $instances = $this->get_enrolment_instances($onlyenabled); $plugins = $this->get_enrolment_plugins(false); foreach ($instances as $key=>$instance) { if (!isset($plugins[$instance->enrol])) { // weird, some broken stuff in plugin unset($instances[$key]); continue; } $this->_inames[$key] = $plugins[$instance->enrol]->get_instance_name($instance); } } return $this->_inames; } /** * Gets all of the enrolment plugins that are available for this course. * * @param bool $onlyenabled return only enabled enrol plugins * @return array */ public function get_enrolment_plugins($onlyenabled = true) { if ($this->_plugins === null) { $this->_plugins = enrol_get_plugins(true); } if ($onlyenabled) { return $this->_plugins; } if ($this->_allplugins === null) { // Make sure we have the same objects in _allplugins and _plugins. $this->_allplugins = $this->_plugins; foreach (enrol_get_plugins(false) as $name=>$plugin) { if (!isset($this->_allplugins[$name])) { $this->_allplugins[$name] = $plugin; } } } return $this->_allplugins; } /** * Gets all of the roles this course can contain. * * @return array */ public function get_all_roles() { if ($this->_roles === null) { $this->_roles = role_fix_names(get_all_roles($this->context), $this->context); } return $this->_roles; } /** * Gets all of the roles this course can contain. * * @return array */ public function get_viewable_roles() { if ($this->_visibleroles === null) { $this->_visibleroles = get_viewable_roles($this->context); } return $this->_visibleroles; } /** * Gets all of the assignable roles for this course. * * @return array */ public function get_assignable_roles($otherusers = false) { if ($this->_assignableroles === null) { $this->_assignableroles = get_assignable_roles($this->context, ROLENAME_ALIAS, false); // verifies unassign access control too } if ($otherusers) { if (!is_array($this->_assignablerolesothers)) { $this->_assignablerolesothers = array(); list($courseviewroles, $ignored) = get_roles_with_cap_in_context($this->context, 'moodle/course:view'); foreach ($this->_assignableroles as $roleid=>$role) { if (isset($courseviewroles[$roleid])) { $this->_assignablerolesothers[$roleid] = $role; } } } return $this->_assignablerolesothers; } else { return $this->_assignableroles; } } /** * Gets all of the assignable roles for this course, wrapped in an array to ensure * role sort order is not lost during json deserialisation. * * @param boolean $otherusers whether to include the assignable roles for other users * @return array */ public function get_assignable_roles_for_json($otherusers = false) { $rolesarray = array(); $assignable = $this->get_assignable_roles($otherusers); foreach ($assignable as $id => $role) { $rolesarray[] = array('id' => $id, 'name' => $role); } return $rolesarray; } /** * Gets all of the groups for this course. * * @return array */ public function get_all_groups() { if ($this->_groups === null) { $this->_groups = groups_get_all_groups($this->course->id); foreach ($this->_groups as $gid=>$group) { $this->_groups[$gid]->name = format_string($group->name); } } return $this->_groups; } /** * Unenrols a user from the course given the users ue entry * * @global moodle_database $DB * @param stdClass $ue * @return bool */ public function unenrol_user($ue) { global $DB; list ($instance, $plugin) = $this->get_user_enrolment_components($ue); if ($instance && $plugin && $plugin->allow_unenrol_user($instance, $ue) && has_capability("enrol/$instance->enrol:unenrol", $this->context)) { $plugin->unenrol_user($instance, $ue->userid); return true; } return false; } /** * Given a user enrolment record this method returns the plugin and enrolment * instance that relate to it. * * @param stdClass|int $userenrolment * @return array array($instance, $plugin) */ public function get_user_enrolment_components($userenrolment) { global $DB; if (is_numeric($userenrolment)) { $userenrolment = $DB->get_record('user_enrolments', array('id'=>(int)$userenrolment)); } $instances = $this->get_enrolment_instances(); $plugins = $this->get_enrolment_plugins(false); if (!$userenrolment || !isset($instances[$userenrolment->enrolid])) { return array(false, false); } $instance = $instances[$userenrolment->enrolid]; $plugin = $plugins[$instance->enrol]; return array($instance, $plugin); } /** * Removes an assigned role from a user. * * @global moodle_database $DB * @param int $userid * @param int $roleid * @return bool */ public function unassign_role_from_user($userid, $roleid) { global $DB; // Admins may unassign any role, others only those they could assign. if (!is_siteadmin() and !array_key_exists($roleid, $this->get_assignable_roles())) { if (defined('AJAX_SCRIPT')) { throw new moodle_exception('invalidrole'); } return false; } $user = $DB->get_record('user', array('id'=>$userid), '*', MUST_EXIST); $ras = $DB->get_records('role_assignments', array('contextid'=>$this->context->id, 'userid'=>$user->id, 'roleid'=>$roleid)); foreach ($ras as $ra) { if ($ra->component) { if (strpos($ra->component, 'enrol_') !== 0) { continue; } if (!$plugin = enrol_get_plugin(substr($ra->component, 6))) { continue; } if ($plugin->roles_protected()) { continue; } } role_unassign($ra->roleid, $ra->userid, $ra->contextid, $ra->component, $ra->itemid); } return true; } /** * Assigns a role to a user. * * @param int $roleid * @param int $userid * @return int|false */ public function assign_role_to_user($roleid, $userid) { require_capability('moodle/role:assign', $this->context); if (!array_key_exists($roleid, $this->get_assignable_roles())) { if (defined('AJAX_SCRIPT')) { throw new moodle_exception('invalidrole'); } return false; } return role_assign($roleid, $userid, $this->context->id, '', NULL); } /** * Adds a user to a group * * @param stdClass $user * @param int $groupid * @return bool */ public function add_user_to_group($user, $groupid) { require_capability('moodle/course:managegroups', $this->context); $group = $this->get_group($groupid); if (!$group) { return false; } return groups_add_member($group->id, $user->id); } /** * Removes a user from a group * * @global moodle_database $DB * @param StdClass $user * @param int $groupid * @return bool */ public function remove_user_from_group($user, $groupid) { global $DB; require_capability('moodle/course:managegroups', $this->context); $group = $this->get_group($groupid); if (!groups_remove_member_allowed($group, $user)) { return false; } if (!$group) { return false; } return groups_remove_member($group, $user); } /** * Gets the requested group * * @param int $groupid * @return stdClass|int */ public function get_group($groupid) { $groups = $this->get_all_groups(); if (!array_key_exists($groupid, $groups)) { return false; } return $groups[$groupid]; } /** * Edits an enrolment * * @param stdClass $userenrolment * @param stdClass $data * @return bool */ public function edit_enrolment($userenrolment, $data) { //Only allow editing if the user has the appropriate capability //Already checked in /user/index.php but checking again in case this function is called from elsewhere list($instance, $plugin) = $this->get_user_enrolment_components($userenrolment); if ($instance && $plugin && $plugin->allow_manage($instance) && has_capability("enrol/$instance->enrol:manage", $this->context)) { if (!isset($data->status)) { $data->status = $userenrolment->status; } $plugin->update_user_enrol($instance, $userenrolment->userid, $data->status, $data->timestart, $data->timeend); return true; } return false; } /** * Returns the current enrolment filter that is being applied by this class * @return string */ public function get_enrolment_filter() { return $this->instancefilter; } /** * Gets the roles assigned to this user that are applicable for this course. * * @param int $userid * @return array */ public function get_user_roles($userid) { $roles = array(); $ras = get_user_roles($this->context, $userid, true, 'c.contextlevel DESC, r.sortorder ASC'); $plugins = $this->get_enrolment_plugins(false); foreach ($ras as $ra) { if ($ra->contextid != $this->context->id) { if (!array_key_exists($ra->roleid, $roles)) { $roles[$ra->roleid] = null; } // higher ras, course always takes precedence continue; } if (array_key_exists($ra->roleid, $roles) && $roles[$ra->roleid] === false) { continue; } $changeable = true; if ($ra->component) { $changeable = false; if (strpos($ra->component, 'enrol_') === 0) { $plugin = substr($ra->component, 6); if (isset($plugins[$plugin])) { $changeable = !$plugins[$plugin]->roles_protected(); } } } $roles[$ra->roleid] = $changeable; } return $roles; } /** * Gets the enrolments this user has in the course - including all suspended plugins and instances. * * @global moodle_database $DB * @param int $userid * @return array */ public function get_user_enrolments($userid) { global $DB; list($instancessql, $params, $filter) = $this->get_instance_sql(); $params['userid'] = $userid; $userenrolments = $DB->get_records_select('user_enrolments', "enrolid $instancessql AND userid = :userid", $params); $instances = $this->get_enrolment_instances(); $plugins = $this->get_enrolment_plugins(false); $inames = $this->get_enrolment_instance_names(); foreach ($userenrolments as &$ue) { $ue->enrolmentinstance = $instances[$ue->enrolid]; $ue->enrolmentplugin = $plugins[$ue->enrolmentinstance->enrol]; $ue->enrolmentinstancename = $inames[$ue->enrolmentinstance->id]; } return $userenrolments; } /** * Gets the groups this user belongs to * * @param int $userid * @return array */ public function get_user_groups($userid) { return groups_get_all_groups($this->course->id, $userid, 0, 'g.id'); } /** * Retursn an array of params that would go into the URL to return to this * exact page. * * @return array */ public function get_url_params() { $args = array( 'id' => $this->course->id ); if (!empty($this->instancefilter)) { $args['ifilter'] = $this->instancefilter; } if (!empty($this->rolefilter)) { $args['role'] = $this->rolefilter; } if ($this->searchfilter !== '') { $args['search'] = $this->searchfilter; } if (!empty($this->groupfilter)) { $args['filtergroup'] = $this->groupfilter; } if ($this->statusfilter !== -1) { $args['status'] = $this->statusfilter; } return $args; } /** * Returns the course this object is managing enrolments for * * @return stdClass */ public function get_course() { return $this->course; } /** * Returns the course context * * @return context */ public function get_context() { return $this->context; } /** * Gets an array of other users in this course ready for display. * * Other users are users who have been assigned or inherited roles within this * course but have not been enrolled. * * @param core_enrol_renderer $renderer * @param moodle_url $pageurl * @param string $sort * @param string $direction ASC | DESC * @param int $page Starting from 0 * @param int $perpage * @return array */ public function get_other_users_for_display(core_enrol_renderer $renderer, moodle_url $pageurl, $sort, $direction, $page, $perpage) { $userroles = $this->get_other_users($sort, $direction, $page, $perpage); $roles = $this->get_all_roles(); $plugins = $this->get_enrolment_plugins(false); $context = $this->get_context(); $now = time(); // TODO Does not support custom user profile fields (MDL-70456). $extrafields = fields::get_identity_fields($context, false); $users = array(); foreach ($userroles as $userrole) { $contextid = $userrole->contextid; unset($userrole->contextid); // This would collide with user avatar. if (!array_key_exists($userrole->id, $users)) { $users[$userrole->id] = $this->prepare_user_for_display($userrole, $extrafields, $now); } $a = new stdClass; $a->role = $roles[$userrole->roleid]->localname; if ($contextid == $this->context->id) { $changeable = true; if ($userrole->component) { $changeable = false; if (strpos($userrole->component, 'enrol_') === 0) { $plugin = substr($userrole->component, 6); if (isset($plugins[$plugin])) { $changeable = !$plugins[$plugin]->roles_protected(); } } } $roletext = get_string('rolefromthiscourse', 'enrol', $a); } else { $changeable = false; switch ($userrole->contextlevel) { case CONTEXT_COURSE : // Meta course $roletext = get_string('rolefrommetacourse', 'enrol', $a); break; case CONTEXT_COURSECAT : $roletext = get_string('rolefromcategory', 'enrol', $a); break; case CONTEXT_SYSTEM: default: $roletext = get_string('rolefromsystem', 'enrol', $a); break; } } if (!isset($users[$userrole->id]['roles'])) { $users[$userrole->id]['roles'] = array(); } $users[$userrole->id]['roles'][$userrole->roleid] = array( 'text' => $roletext, 'unchangeable' => !$changeable ); } return $users; } /** * Gets an array of users for display, this includes minimal user information * as well as minimal information on the users roles, groups, and enrolments. * * @param core_enrol_renderer $renderer * @param moodle_url $pageurl * @param int $sort * @param string $direction ASC or DESC * @param int $page * @param int $perpage * @return array */ public function get_users_for_display(course_enrolment_manager $manager, $sort, $direction, $page, $perpage) { $pageurl = $manager->get_moodlepage()->url; $users = $this->get_users($sort, $direction, $page, $perpage); $now = time(); $straddgroup = get_string('addgroup', 'group'); $strunenrol = get_string('unenrol', 'enrol'); $stredit = get_string('edit'); $visibleroles = $this->get_viewable_roles(); $assignable = $this->get_assignable_roles(); $allgroups = $this->get_all_groups(); $context = $this->get_context(); $canmanagegroups = has_capability('moodle/course:managegroups', $context); $url = new moodle_url($pageurl, $this->get_url_params()); // TODO Does not support custom user profile fields (MDL-70456). $extrafields = fields::get_identity_fields($context, false); $enabledplugins = $this->get_enrolment_plugins(true); $userdetails = array(); foreach ($users as $user) { $details = $this->prepare_user_for_display($user, $extrafields, $now); // Roles $details['roles'] = array(); foreach ($this->get_user_roles($user->id) as $rid=>$rassignable) { $unchangeable = !$rassignable; if (!is_siteadmin() and !isset($assignable[$rid])) { $unchangeable = true; } if (isset($visibleroles[$rid])) { $label = $visibleroles[$rid]; } else { $label = get_string('novisibleroles', 'role'); $unchangeable = true; } $details['roles'][$rid] = array('text' => $label, 'unchangeable' => $unchangeable); } // Users $usergroups = $this->get_user_groups($user->id); $details['groups'] = array(); foreach($usergroups as $gid=>$unused) { $details['groups'][$gid] = $allgroups[$gid]->name; } // Enrolments $details['enrolments'] = array(); foreach ($this->get_user_enrolments($user->id) as $ue) { if (!isset($enabledplugins[$ue->enrolmentinstance->enrol])) { $details['enrolments'][$ue->id] = array( 'text' => $ue->enrolmentinstancename, 'period' => null, 'dimmed' => true, 'actions' => array() ); continue; } else if ($ue->timestart and $ue->timeend) { $period = get_string('periodstartend', 'enrol', array('start'=>userdate($ue->timestart), 'end'=>userdate($ue->timeend))); $periodoutside = ($ue->timestart && $ue->timeend && ($now < $ue->timestart || $now > $ue->timeend)); } else if ($ue->timestart) { $period = get_string('periodstart', 'enrol', userdate($ue->timestart)); $periodoutside = ($ue->timestart && $now < $ue->timestart); } else if ($ue->timeend) { $period = get_string('periodend', 'enrol', userdate($ue->timeend)); $periodoutside = ($ue->timeend && $now > $ue->timeend); } else { // If there is no start or end show when user was enrolled. $period = get_string('periodnone', 'enrol', userdate($ue->timecreated)); $periodoutside = false; } $details['enrolments'][$ue->id] = array( 'text' => $ue->enrolmentinstancename, 'period' => $period, 'dimmed' => ($periodoutside or $ue->status != ENROL_USER_ACTIVE or $ue->enrolmentinstance->status != ENROL_INSTANCE_ENABLED), 'actions' => $ue->enrolmentplugin->get_user_enrolment_actions($manager, $ue) ); } $userdetails[$user->id] = $details; } return $userdetails; } /** * Prepare a user record for display * * This function is called by both {@link get_users_for_display} and {@link get_other_users_for_display} to correctly * prepare user fields for display * * Please note that this function does not check capability for moodle/coures:viewhiddenuserfields * * @param object $user The user record * @param array $extrafields The list of fields as returned from \core_user\fields::get_identity_fields used to determine which * additional fields may be displayed * @param int $now The time used for lastaccess calculation * @return array The fields to be displayed including userid, courseid, picture, firstname, lastcourseaccess, lastaccess and any * additional fields from $extrafields */ private function prepare_user_for_display($user, $extrafields, $now) { $details = array( 'userid' => $user->id, 'courseid' => $this->get_course()->id, 'picture' => new user_picture($user), 'userfullnamedisplay' => fullname($user, has_capability('moodle/site:viewfullnames', $this->get_context())), 'lastaccess' => get_string('never'), 'lastcourseaccess' => get_string('never'), ); foreach ($extrafields as $field) { $details[$field] = s($user->{$field}); } // Last time user has accessed the site. if (!empty($user->lastaccess)) { $details['lastaccess'] = format_time($now - $user->lastaccess); } // Last time user has accessed the course. if (!empty($user->lastcourseaccess)) { $details['lastcourseaccess'] = format_time($now - $user->lastcourseaccess); } return $details; } public function get_manual_enrol_buttons() { $plugins = $this->get_enrolment_plugins(true); // Skip disabled plugins. $buttons = array(); foreach ($plugins as $plugin) { $newbutton = $plugin->get_manual_enrol_button($this); if (is_array($newbutton)) { $buttons += $newbutton; } else if ($newbutton instanceof enrol_user_button) { $buttons[] = $newbutton; } } return $buttons; } public function has_instance($enrolpluginname) { // Make sure manual enrolments instance exists foreach ($this->get_enrolment_instances() as $instance) { if ($instance->enrol == $enrolpluginname) { return true; } } return false; } /** * Returns the enrolment plugin that the course manager was being filtered to. * * If no filter was being applied then this function returns false. * * @return enrol_plugin */ public function get_filtered_enrolment_plugin() { $instances = $this->get_enrolment_instances(); $plugins = $this->get_enrolment_plugins(false); if (empty($this->instancefilter) || !array_key_exists($this->instancefilter, $instances)) { return false; } $instance = $instances[$this->instancefilter]; return $plugins[$instance->enrol]; } /** * Returns and array of users + enrolment details. * * Given an array of user id's this function returns and array of user enrolments for those users * as well as enough user information to display the users name and picture for each enrolment. * * @global moodle_database $DB * @param array $userids * @return array */ public function get_users_enrolments(array $userids) { global $DB; $instances = $this->get_enrolment_instances(); $plugins = $this->get_enrolment_plugins(false); if (!empty($this->instancefilter)) { $instancesql = ' = :instanceid'; $instanceparams = array('instanceid' => $this->instancefilter); } else { list($instancesql, $instanceparams) = $DB->get_in_or_equal(array_keys($instances), SQL_PARAMS_NAMED, 'instanceid0000'); } $userfieldsapi = \core_user\fields::for_userpic(); $userfields = $userfieldsapi->get_sql('u', false, '', '', false)->selects; list($idsql, $idparams) = $DB->get_in_or_equal($userids, SQL_PARAMS_NAMED, 'userid0000'); list($sort, $sortparams) = users_order_by_sql('u'); $sql = "SELECT ue.id AS ueid, ue.status, ue.enrolid, ue.userid, ue.timestart, ue.timeend, ue.modifierid, ue.timecreated, ue.timemodified, $userfields FROM {user_enrolments} ue LEFT JOIN {user} u ON u.id = ue.userid WHERE ue.enrolid $instancesql AND u.id $idsql ORDER BY $sort"; $rs = $DB->get_recordset_sql($sql, $idparams + $instanceparams + $sortparams); $users = array(); foreach ($rs as $ue) { $user = user_picture::unalias($ue); $ue->id = $ue->ueid; unset($ue->ueid); if (!array_key_exists($user->id, $users)) { $user->enrolments = array(); $users[$user->id] = $user; } $ue->enrolmentinstance = $instances[$ue->enrolid]; $ue->enrolmentplugin = $plugins[$ue->enrolmentinstance->enrol]; $users[$user->id]->enrolments[$ue->id] = $ue; } $rs->close(); return $users; } } /** * A button that is used to enrol users in a course * * @copyright 2010 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class enrol_user_button extends single_button { /** * An array containing JS YUI modules required by this button * @var array */ protected $jsyuimodules = array(); /** * An array containing JS initialisation calls required by this button * @var array */ protected $jsinitcalls = array(); /** * An array strings required by JS for this button * @var array */ protected $jsstrings = array(); /** * Initialises the new enrol_user_button * * @staticvar int $count The number of enrol user buttons already created * @param moodle_url $url * @param string $label The text to display in the button * @param string $method Either post or get */ public function __construct(moodle_url $url, $label, $method = 'post') { static $count = 0; $count ++; parent::__construct($url, $label, $method); $this->class = 'singlebutton enrolusersbutton'; $this->formid = 'enrolusersbutton-'.$count; } /** * Adds a YUI module call that will be added to the page when the button is used. * * @param string|array $modules One or more modules to require * @param string $function The JS function to call * @param array $arguments An array of arguments to pass to the function * @param string $galleryversion Deprecated: The gallery version to use * @param bool $ondomready If true the call is postponed until the DOM is finished loading */ public function require_yui_module($modules, $function, ?array $arguments = null, $galleryversion = null, $ondomready = false) { if ($galleryversion != null) { debugging('The galleryversion parameter to yui_module has been deprecated since Moodle 2.3.', DEBUG_DEVELOPER); } $js = new stdClass; $js->modules = (array)$modules; $js->function = $function; $js->arguments = $arguments; $js->ondomready = $ondomready; $this->jsyuimodules[] = $js; } /** * Adds a JS initialisation call to the page when the button is used. * * @param string $function The function to call * @param array $extraarguments An array of arguments to pass to the function * @param bool $ondomready If true the call is postponed until the DOM is finished loading * @param array $module A module definition */ public function require_js_init_call($function, ?array $extraarguments = null, $ondomready = false, ?array $module = null) { $js = new stdClass; $js->function = $function; $js->extraarguments = $extraarguments; $js->ondomready = $ondomready; $js->module = $module; $this->jsinitcalls[] = $js; } /** * Requires strings for JS that will be loaded when the button is used. * * @param type $identifiers * @param string $component * @param mixed $a */ public function strings_for_js($identifiers, $component = 'moodle', $a = null) { $string = new stdClass; $string->identifiers = (array)$identifiers; $string->component = $component; $string->a = $a; $this->jsstrings[] = $string; } /** * Initialises the JS that is required by this button * * @param moodle_page $page */ public function initialise_js(moodle_page $page) { foreach ($this->jsyuimodules as $js) { $page->requires->yui_module($js->modules, $js->function, $js->arguments, null, $js->ondomready); } foreach ($this->jsinitcalls as $js) { $page->requires->js_init_call($js->function, $js->extraarguments, $js->ondomready, $js->module); } foreach ($this->jsstrings as $string) { $page->requires->strings_for_js($string->identifiers, $string->component, $string->a); } } } /** * User enrolment action * * This class is used to manage a renderable ue action such as editing an user enrolment or deleting * a user enrolment. * * @copyright 2011 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class user_enrolment_action implements renderable { /** * The icon to display for the action * @var pix_icon */ protected $icon; /** * The title for the action * @var string */ protected $title; /** * The URL to the action * @var moodle_url */ protected $url; /** * An array of HTML attributes * @var array */ protected $attributes = array(); /** * Constructor * @param pix_icon $icon * @param string $title * @param moodle_url $url * @param array $attributes */ public function __construct(pix_icon $icon, $title, $url, ?array $attributes = null) { $this->icon = $icon; $this->title = $title; $this->url = new moodle_url($url); if (!empty($attributes)) { $this->attributes = $attributes; } $this->attributes['title'] = $title; } /** * Returns the icon for this action * @return pix_icon */ public function get_icon() { return $this->icon; } /** * Returns the title for this action * @return string */ public function get_title() { return $this->title; } /** * Returns the URL for this action * @return moodle_url */ public function get_url() { return $this->url; } /** * Returns the attributes to use for this action * @return array */ public function get_attributes() { return $this->attributes; } } class enrol_ajax_exception extends moodle_exception { /** * Constructor * @param string $errorcode The name of the string from error.php to print * @param string $module name of module * @param string $link The url where the user will be prompted to continue. If no url is provided the user will be directed to the site index page. * @param object $a Extra words and phrases that might be required in the error string * @param string $debuginfo optional debugging information */ public function __construct($errorcode, $link = '', $a = NULL, $debuginfo = null) { parent::__construct($errorcode, 'enrol', $link, $a, $debuginfo); } } /** * This class is used to manage a bulk operations for enrolment plugins. * * @copyright 2011 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ abstract class enrol_bulk_enrolment_operation { /** * The course enrolment manager * @var course_enrolment_manager */ protected $manager; /** * The enrolment plugin to which this operation belongs * @var enrol_plugin */ protected $plugin; /** * Contructor * @param course_enrolment_manager $manager * @param stdClass $plugin */ public function __construct(course_enrolment_manager $manager, ?enrol_plugin $plugin = null) { $this->manager = $manager; $this->plugin = $plugin; } /** * Returns a moodleform used for this operation, or false if no form is required and the action * should be immediatly processed. * * @param moodle_url|string $defaultaction * @param mixed $defaultcustomdata * @return enrol_bulk_enrolment_change_form|moodleform|false */ public function get_form($defaultaction = null, $defaultcustomdata = null) { return false; } /** * Returns the title to use for this bulk operation * * @return string */ abstract public function get_title(); /** * Returns the identifier for this bulk operation. * This should be the same identifier used by the plugins function when returning * all of its bulk operations. * * @return string */ abstract public function get_identifier(); /** * Processes the bulk operation on the given users * * @param course_enrolment_manager $manager * @param array $users * @param stdClass $properties */ abstract public function process(course_enrolment_manager $manager, array $users, stdClass $properties); } home/smkb8269/public_html/lms.smk-almanshuriyah.sch.id/report/log/locallib.php000064400000057156152266577460023311 0ustar00. /** * This file contains functions used by the log reports * * This files lists the functions that are used during the log report generation. * * @package report_log * @copyright 1999 onwards Martin Dougiamas (http://dougiamas.com) * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die; if (!defined('REPORT_LOG_MAX_DISPLAY')) { define('REPORT_LOG_MAX_DISPLAY', 150); // days } require_once(__DIR__.'/lib.php'); /** * This function is used to generate and display the log activity graph * * @global stdClass $CFG * @param stdClass $course course instance * @param int|stdClass $user id/object of the user whose logs are needed * @param string $typeormode type of logs graph needed (usercourse.png/userday.png) or the mode (today, all). * @param int $date timestamp in GMT (seconds since epoch) * @param string $logreader Log reader. * @param int $sitecoursefilter use a course filter in site context. * @return void */ function report_log_print_graph($course, $user, $typeormode, $date=0, $logreader='', $sitecoursefilter = 0) { global $CFG, $OUTPUT; if (!is_object($user)) { $user = core_user::get_user($user); } $logmanager = get_log_manager(); $readers = $logmanager->get_readers(); if (empty($logreader)) { $reader = reset($readers); } else { $reader = $readers[$logreader]; } // If reader is not a sql_internal_table_reader and not legacy store then don't show graph. if (!($reader instanceof \core\log\sql_internal_table_reader)) { return array(); } $coursecontext = context_course::instance($course->id); $a = new stdClass(); $a->coursename = format_string($course->shortname, true, array('context' => $coursecontext)); $a->username = fullname($user, true); if ($typeormode == 'today' || $typeormode == 'userday.png') { $logs = report_log_usertoday_data($course, $user, $date, $logreader, $sitecoursefilter); $title = get_string("hitsoncoursetoday", "", $a); } else if ($typeormode == 'all' || $typeormode == 'usercourse.png') { $logs = report_log_userall_data($course, $user, $logreader, $sitecoursefilter); $title = get_string("hitsoncourse", "", $a); } if (!empty($CFG->preferlinegraphs)) { $chart = new \core\chart_line(); } else { $chart = new \core\chart_bar(); } $series = new \core\chart_series(get_string("hits"), $logs['series']); $chart->add_series($series); $chart->set_title($title); $chart->set_labels($logs['labels']); $yaxis = $chart->get_yaxis(0, true); $yaxis->set_label(get_string("hits")); $yaxis->set_stepsize(max(1, round(max($logs['series']) / 10))); echo $OUTPUT->render($chart); } /** * Select all log records for a given course and user * * @param int $userid The id of the user as found in the 'user' table. * @param int $courseid The id of the course as found in the 'course' table. * @param string $coursestart unix timestamp representing course start date and time. * @param string $logreader log reader to use. * @return array */ function report_log_usercourse($userid, $courseid, $coursestart, $logreader = '') { global $DB; $logmanager = get_log_manager(); $readers = $logmanager->get_readers(); if (empty($logreader)) { $reader = reset($readers); } else { $reader = $readers[$logreader]; } // If reader is not a sql_internal_table_reader and not legacy store then return. if (!($reader instanceof \core\log\sql_internal_table_reader)) { return array(); } $coursestart = (int)$coursestart; // Note: unfortunately pg complains if you use name parameter or column alias in GROUP BY. $logtable = $reader->get_internal_log_table_name(); $timefield = 'timecreated'; $coursefield = 'courseid'; $nonanonymous = 'AND anonymous = 0'; $params = array(); $courseselect = ''; if ($courseid) { $courseselect = "AND $coursefield = :courseid"; $params['courseid'] = $courseid; } $params['userid'] = $userid; return $DB->get_records_sql("SELECT FLOOR(($timefield - $coursestart)/" . DAYSECS . ") AS day, COUNT(*) AS num FROM {" . $logtable . "} WHERE userid = :userid AND $timefield > $coursestart $courseselect $nonanonymous GROUP BY FLOOR(($timefield - $coursestart)/" . DAYSECS .")", $params); } /** * Select all log records for a given course, user, and day * * @param int $userid The id of the user as found in the 'user' table. * @param int $courseid The id of the course as found in the 'course' table. * @param string $daystart unix timestamp of the start of the day for which the logs needs to be retrived * @param string $logreader log reader to use. * @return array */ function report_log_userday($userid, $courseid, $daystart, $logreader = '') { global $DB; $logmanager = get_log_manager(); $readers = $logmanager->get_readers(); if (empty($logreader)) { $reader = reset($readers); } else { $reader = $readers[$logreader]; } // If reader is not a sql_internal_table_reader and not legacy store then return. if (!($reader instanceof \core\log\sql_internal_table_reader)) { return array(); } $daystart = (int)$daystart; // Note: unfortunately pg complains if you use name parameter or column alias in GROUP BY. $logtable = $reader->get_internal_log_table_name(); $timefield = 'timecreated'; $coursefield = 'courseid'; $nonanonymous = 'AND anonymous = 0'; $params = array('userid' => $userid); $courseselect = ''; if ($courseid) { $courseselect = "AND $coursefield = :courseid"; $params['courseid'] = $courseid; } return $DB->get_records_sql("SELECT FLOOR(($timefield - $daystart)/" . HOURSECS . ") AS hour, COUNT(*) AS num FROM {" . $logtable . "} WHERE userid = :userid AND $timefield > $daystart $courseselect $nonanonymous GROUP BY FLOOR(($timefield - $daystart)/" . HOURSECS . ") ", $params); } /** * This function is used to generate and display Mnet selector form * * @global stdClass $USER * @global stdClass $CFG * @global stdClass $SITE * @global moodle_database $DB * @global core_renderer $OUTPUT * @global stdClass $SESSION * @uses CONTEXT_SYSTEM * @uses COURSE_MAX_COURSES_PER_DROPDOWN * @uses CONTEXT_COURSE * @uses SEPARATEGROUPS * @param int $hostid host id * @param stdClass $course course instance * @param int $selecteduser id of the selected user * @param string $selecteddate Date selected * @param string $modname course_module->id * @param string $modid number or 'site_errors' * @param string $modaction an action as recorded in the logs * @param int $selectedgroup Group to display * @param int $showcourses whether to show courses if we're over our limit. * @param int $showusers whether to show users if we're over our limit. * @param string $logformat Format of the logs (downloadascsv, showashtml, downloadasods, downloadasexcel) * @return void */ function report_log_print_mnet_selector_form($hostid, $course, $selecteduser=0, $selecteddate='today', $modname="", $modid=0, $modaction='', $selectedgroup=-1, $showcourses=0, $showusers=0, $logformat='showashtml') { global $USER, $CFG, $SITE, $DB, $OUTPUT, $SESSION; require_once $CFG->dirroot.'/mnet/peer.php'; $mnet_peer = new mnet_peer(); $mnet_peer->set_id($hostid); $sql = "SELECT DISTINCT course, hostid, coursename FROM {mnet_log}"; $courses = $DB->get_records_sql($sql); $remotecoursecount = count($courses); // first check to see if we can override showcourses and showusers $numcourses = $remotecoursecount + $DB->count_records('course'); if ($numcourses < COURSE_MAX_COURSES_PER_DROPDOWN && !$showcourses) { $showcourses = 1; } $sitecontext = context_system::instance(); // Context for remote data is always SITE // Groups for remote data are always OFF if ($hostid == $CFG->mnet_localhost_id) { $context = context_course::instance($course->id); /// Setup for group handling. if ($course->groupmode == SEPARATEGROUPS and !has_capability('moodle/site:accessallgroups', $context)) { $selectedgroup = -1; $showgroups = false; } else if ($course->groupmode) { $showgroups = true; } else { $selectedgroup = 0; $showgroups = false; } if ($selectedgroup === -1) { if (isset($SESSION->currentgroup[$course->id])) { $selectedgroup = $SESSION->currentgroup[$course->id]; } else { $selectedgroup = groups_get_all_groups($course->id, $USER->id); if (is_array($selectedgroup)) { $selectedgroup = array_shift(array_keys($selectedgroup)); $SESSION->currentgroup[$course->id] = $selectedgroup; } else { $selectedgroup = 0; } } } } else { $context = $sitecontext; } // Get all the possible users $users = array(); // Define limitfrom and limitnum for queries below // If $showusers is enabled... don't apply limitfrom and limitnum $limitfrom = empty($showusers) ? 0 : ''; $limitnum = empty($showusers) ? COURSE_MAX_USERS_PER_DROPDOWN + 1 : ''; // If looking at a different host, we're interested in all our site users if ($hostid == $CFG->mnet_localhost_id && $course->id != SITEID) { $userfieldsapi = \core_user\fields::for_name(); $courseusers = get_enrolled_users($context, '', $selectedgroup, 'u.id, ' . $userfieldsapi->get_sql('u', false, '', '', false)->selects, null, $limitfrom, $limitnum); } else { // this may be a lot of users :-( $userfieldsapi = \core_user\fields::for_name(); $courseusers = $DB->get_records('user', array('deleted' => 0), 'lastaccess DESC', 'id, ' . $userfieldsapi->get_sql('', false, '', '', false)->selects, $limitfrom, $limitnum); } if (count($courseusers) < COURSE_MAX_USERS_PER_DROPDOWN && !$showusers) { $showusers = 1; } if ($showusers) { if ($courseusers) { foreach ($courseusers as $courseuser) { $users[$courseuser->id] = fullname($courseuser, has_capability('moodle/site:viewfullnames', $context)); } } $users[$CFG->siteguest] = get_string('guestuser'); } // Get all the hosts that have log records $sql = "select distinct h.id, h.name from {mnet_host} h, {mnet_log} l where h.id = l.hostid order by h.name"; if ($hosts = $DB->get_records_sql($sql)) { foreach($hosts as $host) { $hostarray[$host->id] = $host->name; } } $hostarray[$CFG->mnet_localhost_id] = $SITE->fullname; asort($hostarray); $dropdown = array(); foreach($hostarray as $hostid => $name) { $courses = array(); $sites = array(); if ($CFG->mnet_localhost_id == $hostid) { if (has_capability('report/log:view', $sitecontext) && $showcourses) { if ($ccc = $DB->get_records("course", null, "fullname","id,shortname,fullname,category")) { foreach ($ccc as $cc) { if ($cc->id == SITEID) { $sites["$hostid/$cc->id"] = format_string($cc->fullname).' ('.get_string('site').')'; } else { $courses["$hostid/$cc->id"] = format_string(get_course_display_name_for_list($cc)); } } } } } else { if (has_capability('report/log:view', $sitecontext) && $showcourses) { $sql = "SELECT DISTINCT course, coursename FROM {mnet_log} where hostid = ?"; if ($ccc = $DB->get_records_sql($sql, array($hostid))) { foreach ($ccc as $cc) { if (1 == $cc->course) { // TODO: this might be wrong - site course may have another id $sites["$hostid/$cc->course"] = $cc->coursename.' ('.get_string('site').')'; } else { $courses["$hostid/$cc->course"] = $cc->coursename; } } } } } asort($courses); $dropdown[] = array($name=>($sites + $courses)); } $activities = array(); $selectedactivity = ""; $modinfo = get_fast_modinfo($course); if (!empty($modinfo->cms)) { $section = 0; $thissection = array(); foreach ($modinfo->cms as $cm) { // Exclude activities that aren't visible or have no view link (e.g. label). Account for folder being displayed inline. if (!$cm->uservisible || (!$cm->has_view() && strcmp($cm->modname, 'folder') !== 0)) { continue; } if ($cm->sectionnum > 0 and $section <> $cm->sectionnum) { $activities[] = $thissection; $thissection = array(); } $section = $cm->sectionnum; $modname = strip_tags($cm->get_formatted_name()); if (core_text::strlen($modname) > 55) { $modname = core_text::substr($modname, 0, 50)."..."; } if (!$cm->visible) { $modname = "(".$modname.")"; } $key = get_section_name($course, $cm->sectionnum); if (!isset($thissection[$key])) { $thissection[$key] = array(); } $thissection[$key][$cm->id] = $modname; if ($cm->id == $modid) { $selectedactivity = "$cm->id"; } } if (!empty($thissection)) { $activities[] = $thissection; } } if (has_capability('report/log:view', $sitecontext) && !$course->category) { $activities["site_errors"] = get_string("siteerrors"); if ($modid === "site_errors") { $selectedactivity = "site_errors"; } } $strftimedate = get_string("strftimedate"); $strftimedaydate = get_string("strftimedaydate"); asort($users); // Prepare the list of action options. $actions = array( 'view' => get_string('view'), 'add' => get_string('add'), 'update' => get_string('update'), 'delete' => get_string('delete'), '-view' => get_string('allchanges') ); // Get all the possible dates // Note that we are keeping track of real (GMT) time and user time // User time is only used in displays - all calcs and passing is GMT $timenow = time(); // GMT // What day is it now for the user, and when is midnight that day (in GMT). $timemidnight = $today = usergetmidnight($timenow); // Put today up the top of the list $dates = array( "0" => get_string('alldays'), "$timemidnight" => get_string("today").", ".userdate($timenow, $strftimedate) ); if (!$course->startdate or ($course->startdate > $timenow)) { $course->startdate = $course->timecreated; } $numdates = 1; while ($timemidnight > $course->startdate and $numdates < 365) { $timemidnight = $timemidnight - 86400; $timenow = $timenow - 86400; $dates["$timemidnight"] = userdate($timenow, $strftimedaydate); $numdates++; } if ($selecteddate === "today") { $selecteddate = $today; } echo "
wwwroot/report/log/index.php\" method=\"get\">\n"; echo "
\n";//invisible fieldset here breaks wrapping echo "\n"; echo "\n"; echo "\n"; if (has_capability('report/log:view', $sitecontext) && $showcourses) { $cid = empty($course->id)? '1' : $course->id; echo html_writer::label(get_string('selectacoursesite'), 'menuhost_course', false, array('class' => 'accesshide')); echo html_writer::select($dropdown, "host_course", $hostid.'/'.$cid); } else { $courses = array(); $courses[$course->id] = get_course_display_name_for_list($course) . ((empty($course->category)) ? ' ('.get_string('site').') ' : ''); echo html_writer::label(get_string('selectacourse'), 'menuid', false, array('class' => 'accesshide')); echo html_writer::select($courses,"id",$course->id, false); if (has_capability('report/log:view', $sitecontext)) { $a = new stdClass(); $a->url = "$CFG->wwwroot/report/log/index.php?chooselog=0&group=$selectedgroup&user=$selecteduser" ."&id=$course->id&date=$selecteddate&modid=$selectedactivity&showcourses=1&showusers=$showusers"; print_string('logtoomanycourses','moodle',$a); } } if ($showgroups) { if ($cgroups = groups_get_all_groups($course->id)) { foreach ($cgroups as $cgroup) { $groups[$cgroup->id] = $cgroup->name; } } else { $groups = array(); } echo html_writer::label(get_string('selectagroup'), 'menugroup', false, array('class' => 'accesshide')); echo html_writer::select($groups, "group", $selectedgroup, get_string("allgroups")); } if ($showusers) { echo html_writer::label(get_string('participantslist'), 'menuuser', false, array('class' => 'accesshide')); echo html_writer::select($users, "user", $selecteduser, get_string("allparticipants")); } else { $users = array(); if (!empty($selecteduser)) { $user = $DB->get_record('user', array('id'=>$selecteduser)); $users[$selecteduser] = fullname($user); } else { $users[0] = get_string('allparticipants'); } echo html_writer::label(get_string('participantslist'), 'menuuser', false, array('class' => 'accesshide')); echo html_writer::select($users, "user", $selecteduser, false); $a = new stdClass(); $a->url = "$CFG->wwwroot/report/log/index.php?chooselog=0&group=$selectedgroup&user=$selecteduser" ."&id=$course->id&date=$selecteddate&modid=$selectedactivity&showusers=1&showcourses=$showcourses"; print_string('logtoomanyusers','moodle',$a); } echo html_writer::label(get_string('date'), 'menudate', false, array('class' => 'accesshide')); echo html_writer::select($dates, "date", $selecteddate, false); echo html_writer::label(get_string('showreports'), 'menumodid', false, array('class' => 'accesshide')); echo html_writer::select($activities, "modid", $selectedactivity, get_string("allactivities")); echo html_writer::label(get_string('actions'), 'menumodaction', false, array('class' => 'accesshide')); echo html_writer::select($actions, 'modaction', $modaction, get_string("allactions")); $logformats = array('showashtml' => get_string('displayonpage'), 'downloadascsv' => get_string('downloadtext'), 'downloadasods' => get_string('downloadods'), 'downloadasexcel' => get_string('downloadexcel')); echo html_writer::label(get_string('logsformat', 'report_log'), 'menulogformat', false, array('class' => 'accesshide')); echo html_writer::select($logformats, 'logformat', $logformat, false); echo ''; echo '
'; echo '
'; } /** * Fetch logs since the start of the courses and structure in series and labels to be sent to Chart API. * * @param stdClass $course the course object * @param stdClass $user user object * @param string $logreader the log reader where the logs are. * @param int $sitecoursefilter use a course filter in site context. * @return array structured array to be sent to chart API, split in two indexes (series and labels). */ function report_log_userall_data($course, $user, $logreader, $sitecoursefilter = 0) { global $CFG; $site = get_site(); $timenow = time(); $logs = []; if ($course->id == $site->id) { $courseselect = $sitecoursefilter; } else { $courseselect = $course->id; } $maxseconds = REPORT_LOG_MAX_DISPLAY * 3600 * 24; // Seconds. if ($timenow - $course->startdate > $maxseconds) { $course->startdate = $timenow - $maxseconds; } if (!empty($CFG->loglifetime)) { $maxseconds = $CFG->loglifetime * 3600 * 24; // Seconds. if ($timenow - $course->startdate > $maxseconds) { $course->startdate = $timenow - $maxseconds; } } $timestart = $coursestart = usergetmidnight($course->startdate); $i = 0; $logs['series'][$i] = 0; $logs['labels'][$i] = 0; while ($timestart < $timenow) { $timefinish = $timestart + 86400; $logs['labels'][$i] = userdate($timestart, "%a %d %b"); $logs['series'][$i] = 0; $i++; $timestart = $timefinish; } $rawlogs = report_log_usercourse($user->id, $courseselect, $coursestart, $logreader); foreach ($rawlogs as $rawlog) { if (isset($logs['labels'][$rawlog->day])) { $logs['series'][$rawlog->day] = $rawlog->num; } } return $logs; } /** * Fetch logs of the current day and structure in series and labels to be sent to Chart API. * * @param stdClass $course the course object * @param stdClass $user user object * @param int $date A time of a day (in GMT). * @param string $logreader the log reader where the logs are. * @param int $sitecoursefilter use a course filter in site context. * @return array $logs structured array to be sent to chart API, split in two indexes (series and labels). */ function report_log_usertoday_data($course, $user, $date, $logreader, $sitecoursefilter = 0) { $site = get_site(); $logs = []; if ($course->id == $site->id) { $courseselect = $sitecoursefilter; } else { $courseselect = $course->id; } if ($date) { $daystart = usergetmidnight($date); } else { $daystart = usergetmidnight(time()); } for ($i = 0; $i <= 23; $i++) { $hour = $daystart + $i * 3600; $logs['series'][$i] = 0; $logs['labels'][$i] = userdate($hour, "%H:00"); } $rawlogs = report_log_userday($user->id, $courseselect, $daystart, $logreader); foreach ($rawlogs as $rawlog) { if (isset($logs['labels'][$rawlog->hour])) { $logs['series'][$rawlog->hour] = $rawlog->num; } } return $logs; } home/smkb8269/public_html/lms.smk-almanshuriyah.sch.id/enrol/manual/locallib.php000064400000044466152266661310023575 0ustar00. /** * Auxiliary manual user enrolment lib, the main purpose is to lower memory requirements... * * @package enrol_manual * @copyright 2010 Petr Skoda {@link http://skodak.org} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); require_once($CFG->dirroot . '/user/selector/lib.php'); require_once($CFG->dirroot . '/enrol/locallib.php'); /** * Enrol candidates. */ class enrol_manual_potential_participant extends user_selector_base { protected $enrolid; public function __construct($name, $options) { $this->enrolid = $options['enrolid']; $options['includecustomfields'] = true; parent::__construct($name, $options); } /** * Candidate users * @param string $search * @return array */ public function find_users($search) { global $DB; // By default wherecondition retrieves all users except the deleted, not confirmed and guest. list($wherecondition, $params) = $this->search_sql($search, 'u'); $params = array_merge($params, $this->userfieldsparams); $params['enrolid'] = $this->enrolid; $fields = 'SELECT u.id, ' . $this->userfieldsselects; $countfields = 'SELECT COUNT(1)'; $sql = " FROM {user} u LEFT JOIN {user_enrolments} ue ON (ue.userid = u.id AND ue.enrolid = :enrolid) $this->userfieldsjoin WHERE $wherecondition AND ue.id IS NULL"; list($sort, $sortparams) = users_order_by_sql('u', $search, $this->accesscontext, $this->userfieldsmappings); $order = ' ORDER BY ' . $sort; if (!$this->is_validating()) { $potentialmemberscount = $DB->count_records_sql($countfields . $sql, $params); if ($potentialmemberscount > $this->maxusersperpage) { return $this->too_many_results($search, $potentialmemberscount); } } $availableusers = $DB->get_records_sql($fields . $sql . $order, array_merge($params, $sortparams)); if (empty($availableusers)) { return array(); } if ($search) { $groupname = get_string('enrolcandidatesmatching', 'enrol', $search); } else { $groupname = get_string('enrolcandidates', 'enrol'); } return array($groupname => $availableusers); } protected function get_options() { $options = parent::get_options(); $options['enrolid'] = $this->enrolid; $options['file'] = 'enrol/manual/locallib.php'; return $options; } } /** * Enrolled users. */ class enrol_manual_current_participant extends user_selector_base { protected $courseid; protected $enrolid; public function __construct($name, $options) { $this->enrolid = $options['enrolid']; $options['includecustomfields'] = true; parent::__construct($name, $options); } /** * Candidate users * @param string $search * @return array */ public function find_users($search) { global $DB; // By default wherecondition retrieves all users except the deleted, not confirmed and guest. list($wherecondition, $params) = $this->search_sql($search, 'u'); $params = array_merge($params, $this->userfieldsparams); $params['enrolid'] = $this->enrolid; $fields = 'SELECT u.id, ' . $this->userfieldsselects; $countfields = 'SELECT COUNT(1)'; $sql = " FROM {user} u JOIN {user_enrolments} ue ON (ue.userid = u.id AND ue.enrolid = :enrolid) $this->userfieldsjoin WHERE $wherecondition"; list($sort, $sortparams) = users_order_by_sql('u', $search, $this->accesscontext, $this->userfieldsmappings); $order = ' ORDER BY ' . $sort; if (!$this->is_validating()) { $potentialmemberscount = $DB->count_records_sql($countfields . $sql, $params); if ($potentialmemberscount > $this->maxusersperpage) { return $this->too_many_results($search, $potentialmemberscount); } } $availableusers = $DB->get_records_sql($fields . $sql . $order, array_merge($params, $sortparams)); if (empty($availableusers)) { return array(); } if ($search) { $groupname = get_string('enrolledusersmatching', 'enrol', $search); } else { $groupname = get_string('enrolledusers', 'enrol'); } return array($groupname => $availableusers); } protected function get_options() { $options = parent::get_options(); $options['enrolid'] = $this->enrolid; $options['file'] = 'enrol/manual/locallib.php'; return $options; } } /** * A bulk operation for the manual enrolment plugin to edit selected users. * * @copyright 2011 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class enrol_manual_editselectedusers_operation extends enrol_bulk_enrolment_operation { /** * Returns the title to display for this bulk operation. * * @return string */ public function get_title() { return get_string('editselectedusers', 'enrol_manual'); } /** * Returns the identifier for this bulk operation. This is the key used when the plugin * returns an array containing all of the bulk operations it supports. */ public function get_identifier() { return 'editselectedusers'; } /** * Processes the bulk operation request for the given userids with the provided properties. * * @param course_enrolment_manager $manager * @param array $userids * @param stdClass $properties The data returned by the form. */ public function process(course_enrolment_manager $manager, array $users, stdClass $properties) { global $DB, $USER; if (!has_capability("enrol/manual:manage", $manager->get_context())) { return false; } // Get all of the user enrolment id's. $ueids = array(); $instances = array(); foreach ($users as $user) { foreach ($user->enrolments as $enrolment) { $ueids[] = $enrolment->id; if (!array_key_exists($enrolment->id, $instances)) { $instances[$enrolment->id] = $enrolment; } } } // Check that each instance is manageable by the current user. foreach ($instances as $instance) { if (!$this->plugin->allow_manage($instance)) { return false; } } // Collect the known properties. $status = $properties->status; $timestart = $properties->timestart; $timeend = $properties->timeend; list($ueidsql, $params) = $DB->get_in_or_equal($ueids, SQL_PARAMS_NAMED); $updatesql = array(); if ($status == ENROL_USER_ACTIVE || $status == ENROL_USER_SUSPENDED) { $updatesql[] = 'status = :status'; $params['status'] = (int)$status; } if (!empty($timestart)) { $updatesql[] = 'timestart = :timestart'; $params['timestart'] = (int)$timestart; } if (!empty($timeend)) { $updatesql[] = 'timeend = :timeend'; $params['timeend'] = (int)$timeend; } if (empty($updatesql)) { return true; } // Update the modifierid. $updatesql[] = 'modifierid = :modifierid'; $params['modifierid'] = (int)$USER->id; // Update the time modified. $updatesql[] = 'timemodified = :timemodified'; $params['timemodified'] = time(); // Build the SQL statement. $updatesql = join(', ', $updatesql); $sql = "UPDATE {user_enrolments} SET $updatesql WHERE id $ueidsql"; if ($DB->execute($sql, $params)) { foreach ($users as $user) { foreach ($user->enrolments as $enrolment) { $enrolment->courseid = $enrolment->enrolmentinstance->courseid; $enrolment->enrol = 'manual'; // Trigger event. $event = \core\event\user_enrolment_updated::create( array( 'objectid' => $enrolment->id, 'courseid' => $enrolment->courseid, 'context' => context_course::instance($enrolment->courseid), 'relateduserid' => $user->id, 'other' => array('enrol' => 'manual') ) ); $event->trigger(); } } // Delete cached course contacts for this course because they may be affected. cache::make('core', 'coursecontacts')->delete($manager->get_context()->instanceid); return true; } return false; } /** * Returns a enrol_bulk_enrolment_operation extension form to be used * in collecting required information for this operation to be processed. * * @param string|moodle_url|null $defaultaction * @param mixed $defaultcustomdata * @return enrol_manual_editselectedusers_form */ public function get_form($defaultaction = null, $defaultcustomdata = null) { global $CFG; require_once($CFG->dirroot.'/enrol/manual/bulkchangeforms.php'); return new enrol_manual_editselectedusers_form($defaultaction, $defaultcustomdata); } } /** * A bulk operation for the manual enrolment plugin to delete selected users enrolments. * * @copyright 2011 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class enrol_manual_deleteselectedusers_operation extends enrol_bulk_enrolment_operation { /** * Returns the title to display for this bulk operation. * * @return string */ public function get_identifier() { return 'deleteselectedusers'; } /** * Returns the identifier for this bulk operation. This is the key used when the plugin * returns an array containing all of the bulk operations it supports. * * @return string */ public function get_title() { return get_string('deleteselectedusers', 'enrol_manual'); } /** * Returns a enrol_bulk_enrolment_operation extension form to be used * in collecting required information for this operation to be processed. * * @param string|moodle_url|null $defaultaction * @param mixed $defaultcustomdata * @return enrol_manual_editselectedusers_form */ public function get_form($defaultaction = null, $defaultcustomdata = null) { global $CFG; require_once($CFG->dirroot.'/enrol/manual/bulkchangeforms.php'); if (!array($defaultcustomdata)) { $defaultcustomdata = array(); } $defaultcustomdata['title'] = $this->get_title(); $defaultcustomdata['message'] = get_string('confirmbulkdeleteenrolment', 'enrol_manual'); $defaultcustomdata['button'] = get_string('unenrolusers', 'enrol_manual'); return new enrol_manual_deleteselectedusers_form($defaultaction, $defaultcustomdata); } /** * Processes the bulk operation request for the given userids with the provided properties. * * @global moodle_database $DB * @param course_enrolment_manager $manager * @param array $userids * @param stdClass $properties The data returned by the form. */ public function process(course_enrolment_manager $manager, array $users, stdClass $properties) { if (!has_capability("enrol/manual:unenrol", $manager->get_context())) { return false; } $counter = 0; foreach ($users as $user) { foreach ($user->enrolments as $enrolment) { $plugin = $enrolment->enrolmentplugin; $instance = $enrolment->enrolmentinstance; if ($plugin->allow_unenrol_user($instance, $enrolment)) { $plugin->unenrol_user($instance, $user->id); $counter++; } } } // Display a notification message after the bulk user unenrollment. if ($counter > 0) { \core\notification::info(get_string('totalunenrolledusers', 'enrol', $counter)); } return true; } } /** * Migrates all enrolments of the given plugin to enrol_manual plugin, * this is used for example during plugin uninstallation. * * NOTE: this function does not trigger role and enrolment related events. * * @param string $enrol The enrolment method. */ function enrol_manual_migrate_plugin_enrolments($enrol) { global $DB; if ($enrol === 'manual') { // We can not migrate to self. return; } $manualplugin = enrol_get_plugin('manual'); $params = array('enrol'=>$enrol); $sql = "SELECT e.id, e.courseid, e.status, MIN(me.id) AS mid, COUNT(ue.id) AS cu FROM {enrol} e JOIN {user_enrolments} ue ON (ue.enrolid = e.id) JOIN {course} c ON (c.id = e.courseid) LEFT JOIN {enrol} me ON (me.courseid = e.courseid AND me.enrol='manual') WHERE e.enrol = :enrol GROUP BY e.id, e.courseid, e.status ORDER BY e.id"; $rs = $DB->get_recordset_sql($sql, $params); foreach($rs as $e) { $minstance = false; if (!$e->mid) { // Manual instance does not exist yet, add a new one. $course = $DB->get_record('course', array('id'=>$e->courseid), '*', MUST_EXIST); if ($minstance = $DB->get_record('enrol', array('courseid'=>$course->id, 'enrol'=>'manual'))) { // Already created by previous iteration. $e->mid = $minstance->id; } else if ($e->mid = $manualplugin->add_default_instance($course)) { $minstance = $DB->get_record('enrol', array('id'=>$e->mid)); if ($e->status != ENROL_INSTANCE_ENABLED) { $DB->set_field('enrol', 'status', ENROL_INSTANCE_DISABLED, array('id'=>$e->mid)); $minstance->status = ENROL_INSTANCE_DISABLED; } } } else { $minstance = $DB->get_record('enrol', array('id'=>$e->mid)); } if (!$minstance) { // This should never happen unless adding of default instance fails unexpectedly. debugging('Failed to find manual enrolment instance', DEBUG_DEVELOPER); continue; } // First delete potential role duplicates. $params = array('id'=>$e->id, 'component'=>'enrol_'.$enrol, 'empty'=>''); $sql = "SELECT ra.id FROM {role_assignments} ra JOIN {role_assignments} mra ON (mra.contextid = ra.contextid AND mra.userid = ra.userid AND mra.roleid = ra.roleid AND mra.component = :empty AND mra.itemid = 0) WHERE ra.component = :component AND ra.itemid = :id"; $ras = $DB->get_records_sql($sql, $params); $ras = array_keys($ras); $DB->delete_records_list('role_assignments', 'id', $ras); unset($ras); // Migrate roles. $sql = "UPDATE {role_assignments} SET itemid = 0, component = :empty WHERE itemid = :id AND component = :component"; $params = array('empty'=>'', 'id'=>$e->id, 'component'=>'enrol_'.$enrol); $DB->execute($sql, $params); // Delete potential enrol duplicates. $params = array('id'=>$e->id, 'mid'=>$e->mid); $sql = "SELECT ue.id FROM {user_enrolments} ue JOIN {user_enrolments} mue ON (mue.userid = ue.userid AND mue.enrolid = :mid) WHERE ue.enrolid = :id"; $ues = $DB->get_records_sql($sql, $params); $ues = array_keys($ues); $DB->delete_records_list('user_enrolments', 'id', $ues); unset($ues); // Migrate to manual enrol instance. $params = array('id'=>$e->id, 'mid'=>$e->mid); if ($e->status != ENROL_INSTANCE_ENABLED and $minstance->status == ENROL_INSTANCE_ENABLED) { $status = ", status = :disabled"; $params['disabled'] = ENROL_USER_SUSPENDED; } else { $status = ""; } $sql = "UPDATE {user_enrolments} SET enrolid = :mid $status WHERE enrolid = :id"; $DB->execute($sql, $params); } $rs->close(); } /** * Gets an array of the cohorts that can be enrolled in this course. * * @param int $enrolid * @param string $search * @param int $page Defaults to 0 * @param int $perpage Defaults to 25 * @param int $addedenrollment * @return array Array(totalcohorts => int, cohorts => array) */ function enrol_manual_get_potential_cohorts($context, $enrolid, $search = '', $page = 0, $perpage = 25, $addedenrollment = 0) { global $CFG; require_once($CFG->dirroot . '/cohort/lib.php'); $allcohorts = cohort_get_available_cohorts($context, COHORT_WITH_NOTENROLLED_MEMBERS_ONLY, 0, 0, $search); $totalcohorts = count($allcohorts); $cohorts = array(); $cnt = 0; foreach ($allcohorts as $c) { if ($cnt >= $page * $perpage && (!$perpage || $cnt < ($page+1)*$perpage)) { $cohorts[] = (object)array( 'id' => $c->id, 'name' => format_string($c->name, true, array('context' => $c->contextid)), 'cnt' => $c->memberscnt - $c->enrolledcnt ); } $cnt++; } return array('totalcohorts' => $totalcohorts, 'cohorts' => $cohorts); } home/smkb8269/public_html/lms.smk-almanshuriyah.sch.id/mod/data/locallib.php000064400000161242152267007750022664 0ustar00. /** * @package mod_data * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); require_once($CFG->dirroot . '/mod/data/lib.php'); require_once($CFG->libdir . '/portfolio/caller.php'); require_once($CFG->libdir . '/filelib.php'); /** * The class to handle entry exports of a database module */ class data_portfolio_caller extends portfolio_module_caller_base { /** @var int the single record to export */ protected $recordid; /** @var object the record from the data table */ private $data; /**#@+ @var array the fields used and their fieldtypes */ private $fields; private $fieldtypes; /** @var object the records to export */ private $records; /** @var int how many records are 'mine' */ private $minecount; /** * the required callback arguments for a single-record export * * @return array */ public static function expected_callbackargs() { return array( 'id' => true, 'recordid' => false, ); } /** * @param array $callbackargs the arguments passed through */ public function __construct($callbackargs) { parent::__construct($callbackargs); } /** * load up the data needed for the export * * @global object $DB */ public function load_data() { global $DB, $USER; if (!$this->cm = get_coursemodule_from_id('data', $this->id)) { throw new portfolio_caller_exception('invalidid', 'data'); } if (!$this->data = $DB->get_record('data', array('id' => $this->cm->instance))) { throw new portfolio_caller_exception('invalidid', 'data'); } $fieldrecords = $DB->get_records('data_fields', array('dataid' => $this->cm->instance), 'id'); // populate objets for this databases fields $this->fields = array(); foreach ($fieldrecords as $fieldrecord) { $tmp = data_get_field($fieldrecord, $this->data); $this->fields[] = $tmp; $this->fieldtypes[] = $tmp->type; } $this->records = array(); if ($this->recordid) { $tmp = $DB->get_record('data_records', array('id' => $this->recordid)); $tmp->content = $DB->get_records('data_content', array('recordid' => $this->recordid)); $this->records[] = $tmp; } else { $where = array('dataid' => $this->data->id); if (!has_capability('mod/data:exportallentries', context_module::instance($this->cm->id))) { $where['userid'] = $USER->id; // get them all in case, we'll unset ones that aren't ours later if necessary } $tmp = $DB->get_records('data_records', $where); foreach ($tmp as $t) { $t->content = $DB->get_records('data_content', array('recordid' => $t->id)); $this->records[] = $t; } $this->minecount = $DB->count_records('data_records', array('dataid' => $this->data->id, 'userid' => $USER->id)); } if ($this->recordid) { list($formats, $files) = self::formats($this->fields, $this->records[0]); $this->set_file_and_format_data($files); } } /** * How long we think the export will take * Single entry is probably not too long. * But we check for filesizes * Else base it on the number of records * * @return one of PORTFOLIO_TIME_XX constants */ public function expected_time() { if ($this->recordid) { return $this->expected_time_file(); } else { return portfolio_expected_time_db(count($this->records)); } } /** * Calculate the shal1 of this export * Dependent on the export format. * @return string */ public function get_sha1() { // in the case that we're exporting a subclass of 'file' and we have a singlefile, // then we're not exporting any metadata, just the file by itself by mimetype. if ($this->exporter->get('format') instanceof portfolio_format_file && $this->singlefile) { return $this->get_sha1_file(); } // otherwise we're exporting some sort of multipart content so use the data $str = ''; foreach ($this->records as $record) { foreach ($record as $data) { if (is_array($data) || is_object($data)) { $keys = array_keys($data); $testkey = array_pop($keys); if (is_array($data[$testkey]) || is_object($data[$testkey])) { foreach ($data as $d) { $str .= implode(',', (array)$d); } } else { $str .= implode(',', (array)$data); } } else { $str .= $data; } } } return sha1($str . ',' . $this->exporter->get('formatclass')); } /** * Prepare the package for export * * @return stored_file object */ public function prepare_package() { global $DB; $leapwriter = null; $content = ''; $filename = ''; $uid = $this->exporter->get('user')->id; $users = array(); //cache $onlymine = $this->get_export_config('mineonly'); if ($this->exporter->get('formatclass') == PORTFOLIO_FORMAT_LEAP2A) { $leapwriter = $this->exporter->get('format')->leap2a_writer(); $ids = array(); } if ($this->exporter->get('format') instanceof portfolio_format_file && $this->singlefile) { return $this->get('exporter')->copy_existing_file($this->singlefile); } foreach ($this->records as $key => $record) { if ($onlymine && $record->userid != $uid) { unset($this->records[$key]); // sha1 continue; } list($tmpcontent, $files) = $this->exportentry($record); $content .= $tmpcontent; if ($leapwriter) { $entry = new portfolio_format_leap2a_entry('dataentry' . $record->id, $this->data->name, 'resource', $tmpcontent); $entry->published = $record->timecreated; $entry->updated = $record->timemodified; if ($record->userid != $uid) { if (!array_key_exists($record->userid, $users)) { $users[$record->userid] = $DB->get_record('user', array('id' => $record->userid), 'id,firstname,lastname'); } $entry->author = $users[$record->userid]; } $ids[] = $entry->id; $leapwriter->link_files($entry, $files, 'dataentry' . $record->id . 'file'); $leapwriter->add_entry($entry); } } if ($leapwriter) { if (count($this->records) > 1) { // make a selection element to tie them all together $selection = new portfolio_format_leap2a_entry('datadb' . $this->data->id, get_string('entries', 'data') . ': ' . $this->data->name, 'selection'); $leapwriter->add_entry($selection); $leapwriter->make_selection($selection, $ids, 'Grouping'); } $filename = $this->exporter->get('format')->manifest_name(); $content = $leapwriter->to_xml(); } else { if (count($this->records) == 1) { $filename = clean_filename($this->cm->name . '-entry.html'); } else { $filename = clean_filename($this->cm->name . '-full.html'); } } return $this->exporter->write_new_file( $content, $filename, ($this->exporter->get('format') instanceof PORTFOLIO_FORMAT_RICH) // if we have associate files, this is a 'manifest' ); } /** * Verify the user can still export this entry * * @return bool */ public function check_permissions() { if ($this->recordid) { if (data_isowner($this->recordid)) { return has_capability('mod/data:exportownentry', context_module::instance($this->cm->id)); } return has_capability('mod/data:exportentry', context_module::instance($this->cm->id)); } if ($this->has_export_config() && !$this->get_export_config('mineonly')) { return has_capability('mod/data:exportallentries', context_module::instance($this->cm->id)); } return has_capability('mod/data:exportownentry', context_module::instance($this->cm->id)); } /** * @return string */ public static function display_name() { return get_string('modulename', 'data'); } /** * @global object * @return bool|void */ public function __wakeup() { global $CFG; if (empty($CFG)) { return true; // too early yet } foreach ($this->fieldtypes as $key => $field) { $filepath = $CFG->dirroot . '/mod/data/field/' . $field .'/field.class.php'; if (!file_exists($filepath)) { continue; } require_once($filepath); $this->fields[$key] = unserialize(serialize($this->fields[$key])); } } /** * Prepare a single entry for export, replacing all the content etc * * @param stdclass $record the entry to export * * @return array with key 0 = the html content, key 1 = array of attachments */ private function exportentry($record) { // Replacing tags $patterns = array(); $replacement = array(); $files = array(); // Then we generate strings to replace for normal tags $format = $this->get('exporter')->get('format'); foreach ($this->fields as $field) { $patterns[]='[['.$field->field->name.']]'; if (is_callable(array($field, 'get_file'))) { if (!$file = $field->get_file($record->id)) { $replacement[] = ''; continue; // probably left empty } $replacement[] = $format->file_output($file); $this->get('exporter')->copy_existing_file($file); $files[] = $file; } else { $replacement[] = $field->display_browse_field($record->id, 'singletemplate'); } } // Replacing special tags (##Edit##, ##Delete##, ##More##) $patterns[]='##edit##'; $patterns[]='##delete##'; $patterns[]='##export##'; $patterns[]='##more##'; $patterns[]='##moreurl##'; $patterns[]='##user##'; $patterns[]='##approve##'; $patterns[]='##disapprove##'; $patterns[]='##comments##'; $patterns[] = '##timeadded##'; $patterns[] = '##timemodified##'; $replacement[] = ''; $replacement[] = ''; $replacement[] = ''; $replacement[] = ''; $replacement[] = ''; $replacement[] = ''; $replacement[] = ''; $replacement[] = ''; $replacement[] = ''; $replacement[] = userdate($record->timecreated); $replacement[] = userdate($record->timemodified); if (empty($this->data->singletemplate)) { // Use default template if the template is not created. $this->data->singletemplate = data_generate_default_template($this->data, 'singletemplate', 0, false, false); } // actual replacement of the tags return array(str_ireplace($patterns, $replacement, $this->data->singletemplate), $files); } /** * Given the fields being exported, and the single record, * work out which export format(s) we can use * * @param array $fields array of field objects * @param object $record The data record object * * @uses PORTFOLIO_FORMAT_PLAINHTML * @uses PORTFOLIO_FORMAT_RICHHTML * * @return array of PORTFOLIO_XX constants */ public static function formats($fields, $record) { $formats = array(PORTFOLIO_FORMAT_PLAINHTML); $includedfiles = array(); foreach ($fields as $singlefield) { if (is_callable(array($singlefield, 'get_file'))) { if ($file = $singlefield->get_file($record->id)) { $includedfiles[] = $file; } } } if (count($includedfiles) == 1 && count($fields) == 1) { $formats = array(portfolio_format_from_mimetype($includedfiles[0]->get_mimetype())); } else if (count($includedfiles) > 0) { $formats = array(PORTFOLIO_FORMAT_RICHHTML); } return array($formats, $includedfiles); } public static function has_files($data) { global $DB; $fieldrecords = $DB->get_records('data_fields', array('dataid' => $data->id), 'id'); // populate objets for this databases fields foreach ($fieldrecords as $fieldrecord) { $field = data_get_field($fieldrecord, $data); if (is_callable(array($field, 'get_file'))) { return true; } } return false; } /** * base supported formats before we know anything about the export */ public static function base_supported_formats() { return array(PORTFOLIO_FORMAT_RICHHTML, PORTFOLIO_FORMAT_PLAINHTML, PORTFOLIO_FORMAT_LEAP2A); } public function has_export_config() { // if we're exporting more than just a single entry, // and we have the capability to export all entries, // then ask whether we want just our own, or all of them return (empty($this->recordid) // multi-entry export && $this->minecount > 0 // some of them are mine && $this->minecount != count($this->records) // not all of them are mine && has_capability('mod/data:exportallentries', context_module::instance($this->cm->id))); // they actually have a choice in the matter } public function export_config_form(&$mform, $instance) { if (!$this->has_export_config()) { return; } $mform->addElement('selectyesno', 'mineonly', get_string('exportownentries', 'data', (object)array('mine' => $this->minecount, 'all' => count($this->records)))); $mform->setDefault('mineonly', 1); } public function get_allowed_export_config() { return array('mineonly'); } } /** * Class representing the virtual node with all itemids in the file browser * * @category files * @copyright 2012 David Mudrak * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class data_file_info_container extends file_info { /** @var file_browser */ protected $browser; /** @var stdClass */ protected $course; /** @var stdClass */ protected $cm; /** @var string */ protected $component; /** @var stdClass */ protected $context; /** @var array */ protected $areas; /** @var string */ protected $filearea; /** * Constructor (in case you did not realize it ;-) * * @param file_browser $browser * @param stdClass $course * @param stdClass $cm * @param stdClass $context * @param array $areas * @param string $filearea */ public function __construct($browser, $course, $cm, $context, $areas, $filearea) { parent::__construct($browser, $context); $this->browser = $browser; $this->course = $course; $this->cm = $cm; $this->component = 'mod_data'; $this->context = $context; $this->areas = $areas; $this->filearea = $filearea; } /** * @return array with keys contextid, filearea, itemid, filepath and filename */ public function get_params() { return array( 'contextid' => $this->context->id, 'component' => $this->component, 'filearea' => $this->filearea, 'itemid' => null, 'filepath' => null, 'filename' => null, ); } /** * Can new files or directories be added via the file browser * * @return bool */ public function is_writable() { return false; } /** * Should this node be considered as a folder in the file browser * * @return bool */ public function is_directory() { return true; } /** * Returns localised visible name of this node * * @return string */ public function get_visible_name() { return $this->areas[$this->filearea]; } /** * Returns list of children nodes * * @return array of file_info instances */ public function get_children() { return $this->get_filtered_children('*', false, true); } /** * Help function to return files matching extensions or their count * * @param string|array $extensions, either '*' or array of lowercase extensions, i.e. array('.gif','.jpg') * @param bool|int $countonly if false returns the children, if an int returns just the * count of children but stops counting when $countonly number of children is reached * @param bool $returnemptyfolders if true returns items that don't have matching files inside * @return array|int array of file_info instances or the count */ private function get_filtered_children($extensions = '*', $countonly = false, $returnemptyfolders = false) { global $DB; $params = array('contextid' => $this->context->id, 'component' => $this->component, 'filearea' => $this->filearea); $sql = 'SELECT DISTINCT itemid FROM {files} WHERE contextid = :contextid AND component = :component AND filearea = :filearea'; if (!$returnemptyfolders) { $sql .= ' AND filename <> :emptyfilename'; $params['emptyfilename'] = '.'; } list($sql2, $params2) = $this->build_search_files_sql($extensions); $sql .= ' '.$sql2; $params = array_merge($params, $params2); if ($countonly === false) { $sql .= ' ORDER BY itemid DESC'; } $rs = $DB->get_recordset_sql($sql, $params); $children = array(); foreach ($rs as $record) { if ($child = $this->browser->get_file_info($this->context, 'mod_data', $this->filearea, $record->itemid)) { $children[] = $child; } if ($countonly !== false && count($children) >= $countonly) { break; } } $rs->close(); if ($countonly !== false) { return count($children); } return $children; } /** * Returns list of children which are either files matching the specified extensions * or folders that contain at least one such file. * * @param string|array $extensions, either '*' or array of lowercase extensions, i.e. array('.gif','.jpg') * @return array of file_info instances */ public function get_non_empty_children($extensions = '*') { return $this->get_filtered_children($extensions, false); } /** * Returns the number of children which are either files matching the specified extensions * or folders containing at least one such file. * * @param string|array $extensions, for example '*' or array('.gif','.jpg') * @param int $limit stop counting after at least $limit non-empty children are found * @return int */ public function count_non_empty_children($extensions = '*', $limit = 1) { return $this->get_filtered_children($extensions, $limit); } /** * Returns parent file_info instance * * @return file_info or null for root */ public function get_parent() { return $this->browser->get_file_info($this->context); } } /** * This creates new calendar events given as timeavailablefrom and timeclose by $data. * * @param stdClass $data * @return void */ function data_set_events($data) { global $DB, $CFG; require_once($CFG->dirroot.'/calendar/lib.php'); // Get CMID if not sent as part of $data. if (!isset($data->coursemodule)) { $cm = get_coursemodule_from_instance('data', $data->id, $data->course); $data->coursemodule = $cm->id; } // Data start calendar events. $event = new stdClass(); $event->eventtype = DATA_EVENT_TYPE_OPEN; // The DATA_EVENT_TYPE_OPEN event should only be an action event if no close time was specified. $event->type = empty($data->timeavailableto) ? CALENDAR_EVENT_TYPE_ACTION : CALENDAR_EVENT_TYPE_STANDARD; if ($event->id = $DB->get_field('event', 'id', array('modulename' => 'data', 'instance' => $data->id, 'eventtype' => $event->eventtype))) { if ($data->timeavailablefrom > 0) { // Calendar event exists so update it. $event->name = get_string('calendarstart', 'data', $data->name); $event->description = format_module_intro('data', $data, $data->coursemodule, false); $event->format = FORMAT_HTML; $event->timestart = $data->timeavailablefrom; $event->timesort = $data->timeavailablefrom; $event->visible = instance_is_visible('data', $data); $event->timeduration = 0; $calendarevent = calendar_event::load($event->id); $calendarevent->update($event, false); } else { // Calendar event is on longer needed. $calendarevent = calendar_event::load($event->id); $calendarevent->delete(); } } else { // Event doesn't exist so create one. if (isset($data->timeavailablefrom) && $data->timeavailablefrom > 0) { $event->name = get_string('calendarstart', 'data', $data->name); $event->description = format_module_intro('data', $data, $data->coursemodule, false); $event->format = FORMAT_HTML; $event->courseid = $data->course; $event->groupid = 0; $event->userid = 0; $event->modulename = 'data'; $event->instance = $data->id; $event->timestart = $data->timeavailablefrom; $event->timesort = $data->timeavailablefrom; $event->visible = instance_is_visible('data', $data); $event->timeduration = 0; calendar_event::create($event, false); } } // Data end calendar events. $event = new stdClass(); $event->type = CALENDAR_EVENT_TYPE_ACTION; $event->eventtype = DATA_EVENT_TYPE_CLOSE; if ($event->id = $DB->get_field('event', 'id', array('modulename' => 'data', 'instance' => $data->id, 'eventtype' => $event->eventtype))) { if ($data->timeavailableto > 0) { // Calendar event exists so update it. $event->name = get_string('calendarend', 'data', $data->name); $event->description = format_module_intro('data', $data, $data->coursemodule, false); $event->format = FORMAT_HTML; $event->timestart = $data->timeavailableto; $event->timesort = $data->timeavailableto; $event->visible = instance_is_visible('data', $data); $event->timeduration = 0; $calendarevent = calendar_event::load($event->id); $calendarevent->update($event, false); } else { // Calendar event is on longer needed. $calendarevent = calendar_event::load($event->id); $calendarevent->delete(); } } else { // Event doesn't exist so create one. if (isset($data->timeavailableto) && $data->timeavailableto > 0) { $event->name = get_string('calendarend', 'data', $data->name); $event->description = format_module_intro('data', $data, $data->coursemodule, false); $event->format = FORMAT_HTML; $event->courseid = $data->course; $event->groupid = 0; $event->userid = 0; $event->modulename = 'data'; $event->instance = $data->id; $event->timestart = $data->timeavailableto; $event->timesort = $data->timeavailableto; $event->visible = instance_is_visible('data', $data); $event->timeduration = 0; calendar_event::create($event, false); } } } /** * Check if a database is available for the current user. * * @param stdClass $data database record * @param boolean $canmanageentries optional, if the user can manage entries * @param stdClass $context Module context, required if $canmanageentries is not set * @return array status (available or not and possible warnings) * @since Moodle 3.3 */ function data_get_time_availability_status($data, $canmanageentries = null, $context = null) { $open = true; $closed = false; $warnings = array(); if ($canmanageentries === null) { $canmanageentries = has_capability('mod/data:manageentries', $context); } if (!$canmanageentries) { $timenow = time(); if (!empty($data->timeavailablefrom) and $data->timeavailablefrom > $timenow) { $open = false; } if (!empty($data->timeavailableto) and $timenow > $data->timeavailableto) { $closed = true; } if (!$open or $closed) { if (!$open) { $warnings['notopenyet'] = userdate($data->timeavailablefrom); } if ($closed) { $warnings['expired'] = userdate($data->timeavailableto); } return array(false, $warnings); } } // Database is available. return array(true, $warnings); } /** * Requires a database to be available for the current user. * * @param stdClass $data database record * @param boolean $canmanageentries optional, if the user can manage entries * @param stdClass $context Module context, required if $canmanageentries is not set * @throws moodle_exception * @since Moodle 3.3 */ function data_require_time_available($data, $canmanageentries = null, $context = null) { list($available, $warnings) = data_get_time_availability_status($data, $canmanageentries, $context); if (!$available) { $reason = current(array_keys($warnings)); throw new moodle_exception($reason, 'data', '', $warnings[$reason]); } } /** * Return the number of entries left to add to complete the activity. * * @param stdClass $data database object * @param int $numentries the number of entries the current user has created * @param bool $canmanageentries whether the user can manage entries (teachers, managers) * @return int the number of entries left, 0 if no entries left or if is not required * @since Moodle 3.3 */ function data_get_entries_left_to_add($data, $numentries, $canmanageentries) { if ($data->requiredentries > 0 && $numentries < $data->requiredentries && !$canmanageentries) { return $data->requiredentries - $numentries; } return 0; } /** * Return the number of entires left to add to view other users entries.. * * @param stdClass $data database object * @param int $numentries the number of entries the current user has created * @param bool $canmanageentries whether the user can manage entries (teachers, managers) * @return int the number of entries left, 0 if no entries left or if is not required * @since Moodle 3.3 */ function data_get_entries_left_to_view($data, $numentries, $canmanageentries) { if ($data->requiredentriestoview > 0 && $numentries < $data->requiredentriestoview && !$canmanageentries) { return $data->requiredentriestoview - $numentries; } return 0; } /** * Returns data records tagged with a specified tag. * * This is a callback used by the tag area mod_data/data_records to search for data records * tagged with a specific tag. * * @param core_tag_tag $tag * @param bool $exclusivemode if set to true it means that no other entities tagged with this tag * are displayed on the page and the per-page limit may be bigger * @param int $fromctx context id where the link was displayed, may be used by callbacks * to display items in the same context first * @param int $ctx context id where to search for records * @param bool $rec search in subcontexts as well * @param int $page 0-based number of page being displayed * @return \core_tag\output\tagindex */ function mod_data_get_tagged_records($tag, $exclusivemode = false, $fromctx = 0, $ctx = 0, $rec = true, $page = 0) { global $DB, $OUTPUT, $USER; $perpage = $exclusivemode ? 20 : 5; // Build the SQL query. $ctxselect = context_helper::get_preload_record_columns_sql('ctx'); $query = "SELECT dr.id, dr.dataid, dr.approved, d.timeviewfrom, d.timeviewto, dr.groupid, d.approval, dr.userid, d.requiredentriestoview, cm.id AS cmid, c.id AS courseid, c.shortname, c.fullname, $ctxselect FROM {data_records} dr JOIN {data} d ON d.id = dr.dataid JOIN {modules} m ON m.name = 'data' JOIN {course_modules} cm ON cm.module = m.id AND cm.instance = d.id JOIN {tag_instance} tt ON dr.id = tt.itemid JOIN {course} c ON cm.course = c.id JOIN {context} ctx ON ctx.instanceid = cm.id AND ctx.contextlevel = :coursemodulecontextlevel WHERE tt.itemtype = :itemtype AND tt.tagid = :tagid AND tt.component = :component AND cm.deletioninprogress = 0 AND dr.id %ITEMFILTER% AND c.id %COURSEFILTER%"; $params = array( 'itemtype' => 'data_records', 'tagid' => $tag->id, 'component' => 'mod_data', 'coursemodulecontextlevel' => CONTEXT_MODULE ); if ($ctx) { $context = $ctx ? context::instance_by_id($ctx) : context_system::instance(); $query .= $rec ? ' AND (ctx.id = :contextid OR ctx.path LIKE :path)' : ' AND ctx.id = :contextid'; $params['contextid'] = $context->id; $params['path'] = $context->path . '/%'; } $query .= " ORDER BY "; if ($fromctx) { // In order-clause specify that modules from inside "fromctx" context should be returned first. $fromcontext = context::instance_by_id($fromctx); $query .= ' (CASE WHEN ctx.id = :fromcontextid OR ctx.path LIKE :frompath THEN 0 ELSE 1 END),'; $params['fromcontextid'] = $fromcontext->id; $params['frompath'] = $fromcontext->path . '/%'; } $query .= ' c.sortorder, cm.id, dr.id'; $totalpages = $page + 1; // Use core_tag_index_builder to build and filter the list of items. $builder = new core_tag_index_builder('mod_data', 'data_records', $query, $params, $page * $perpage, $perpage + 1); $now = time(); $entrycount = []; $activitygroupmode = []; $usergroups = []; $titlefields = []; while ($item = $builder->has_item_that_needs_access_check()) { context_helper::preload_from_record($item); $modinfo = get_fast_modinfo($item->courseid); $cm = $modinfo->get_cm($item->cmid); $context = \context_module::instance($cm->id); $courseid = $item->courseid; if (!$builder->can_access_course($courseid)) { $builder->set_accessible($item, false); continue; } if (!$cm->uservisible) { $builder->set_accessible($item, false); continue; } if (!has_capability('mod/data:viewentry', $context)) { $builder->set_accessible($item, false); continue; } if ($USER->id != $item->userid && (($item->timeviewfrom && $now < $item->timeviewfrom) || ($item->timeviewto && $now > $item->timeviewto))) { $builder->set_accessible($item, false); continue; } if ($USER->id != $item->userid && $item->approval && !$item->approved) { $builder->set_accessible($item, false); continue; } if ($item->requiredentriestoview) { if (!isset($entrycount[$item->dataid])) { $entrycount[$item->dataid] = $DB->count_records('data_records', array('dataid' => $item->dataid)); } $sufficiententries = $item->requiredentriestoview > $entrycount[$item->dataid]; $builder->set_accessible($item, $sufficiententries); } if (!isset($activitygroupmode[$cm->id])) { $activitygroupmode[$cm->id] = groups_get_activity_groupmode($cm); } if (!isset($usergroups[$item->groupid])) { $usergroups[$item->groupid] = groups_is_member($item->groupid, $USER->id); } if ($activitygroupmode[$cm->id] == SEPARATEGROUPS && !$usergroups[$item->groupid]) { $builder->set_accessible($item, false); continue; } $builder->set_accessible($item, true); } $items = $builder->get_items(); if (count($items) > $perpage) { $totalpages = $page + 2; // We don't need exact page count, just indicate that the next page exists. array_pop($items); } // Build the display contents. if ($items) { $tagfeed = new core_tag\output\tagfeed(); foreach ($items as $item) { context_helper::preload_from_record($item); $modinfo = get_fast_modinfo($item->courseid); $cm = $modinfo->get_cm($item->cmid); $pageurl = new moodle_url('/mod/data/view.php', array( 'rid' => $item->id, 'd' => $item->dataid )); if (!isset($titlefields[$item->dataid])) { $titlefields[$item->dataid] = data_get_tag_title_field($item->dataid); } $pagename = data_get_tag_title_for_entry($titlefields[$item->dataid], $item); $pagename = html_writer::link($pageurl, $pagename); $courseurl = course_get_url($item->courseid, $cm->sectionnum); $cmname = html_writer::link($cm->url, $cm->get_formatted_name()); $coursename = format_string($item->fullname, true, array('context' => context_course::instance($item->courseid))); $coursename = html_writer::link($courseurl, $coursename); $icon = html_writer::link($pageurl, html_writer::empty_tag('img', array('src' => $cm->get_icon_url()))); $tagfeed->add($icon, $pagename, $cmname . '
' . $coursename); } $content = $OUTPUT->render_from_template('core_tag/tagfeed', $tagfeed->export_for_template($OUTPUT)); return new core_tag\output\tagindex($tag, 'mod_data', 'data_records', $content, $exclusivemode, $fromctx, $ctx, $rec, $page, $totalpages); } } /** * Get the title of a field to show when displaying tag results. * * @param int $dataid The id of the data field * @return stdClass The field data from the 'data_fields' table as well as it's priority */ function data_get_tag_title_field($dataid) { global $DB, $CFG; $validfieldtypes = array('text', 'textarea', 'menu', 'radiobutton', 'checkbox', 'multimenu', 'url'); $fields = $DB->get_records('data_fields', ['dataid' => $dataid]); $template = $DB->get_field('data', 'addtemplate', ['id' => $dataid]); if (empty($template)) { $data = $DB->get_record('data', ['id' => $dataid]); $template = data_generate_default_template($data, 'addtemplate', 0, false, false); } $filteredfields = []; foreach ($fields as $field) { if (!in_array($field->type, $validfieldtypes)) { continue; } $field->addtemplateposition = strpos($template, '[['.$field->name.']]'); if ($field->addtemplateposition === false) { continue; } $field->type = clean_param($field->type, PARAM_ALPHA); $filepath = $CFG->dirroot . '/mod/data/field/' . $field->type . '/field.class.php'; if (!file_exists($filepath)) { continue; } require_once($filepath); $classname = 'data_field_' . $field->type; $field->priority = $classname::get_priority(); $filteredfields[] = $field; } $sort = function($record1, $record2) { // If a content's fieldtype is compulsory in the database than it would have priority than any other non-compulsory content. if (($record1->required && $record2->required) || (!$record1->required && !$record2->required)) { if ($record1->priority === $record2->priority) { return $record1->id < $record2->id ? 1 : -1; } return $record1->priority < $record2->priority ? -1 : 1; } else if ($record1->required && !$record2->required) { return 1; } else { return -1; } }; usort($filteredfields, $sort); return array_shift($filteredfields); } /** * Get the title of an entry to show when displaying tag results. * * @param stdClass $field The field from the 'data_fields' table * @param stdClass $entry The entry from the 'data_records' table * @return string|null It will return the title of the entry or null if the field type is not available. */ function data_get_tag_title_for_entry($field, $entry) { global $CFG, $DB; if (!isset($field->type)) { return null; } $field->type = clean_param($field->type, PARAM_ALPHA); $filepath = $CFG->dirroot . '/mod/data/field/' . $field->type . '/field.class.php'; if (!file_exists($filepath)) { return null; } require_once($filepath); $classname = 'data_field_' . $field->type; $sql = "SELECT dc.* FROM {data_content} dc INNER JOIN {data_fields} df ON dc.fieldid = df.id WHERE df.id = :fieldid AND dc.recordid = :recordid"; $fieldcontents = $DB->get_record_sql($sql, array('recordid' => $entry->id, 'fieldid' => $field->id)); return $classname::get_content_value($fieldcontents); } /** * Search entries in a database. * * @param stdClass $data database object * @param stdClass $cm course module object * @param stdClass $context context object * @param stdClass $mode in which mode we are viewing the database (list, single) * @param int $currentgroup the current group being used * @param str $search search for this text in the entry data * @param str $sort the field to sort by * @param str $order the order to use when sorting * @param int $page for pagination, the current page * @param int $perpage entries per page * @param bool $advanced whether we are using or not advanced search * @param array $searcharray when using advanced search, the advanced data to use * @param stdClass $record if we jsut want this record after doing all the access checks * @return array the entries found among other data related to the search * @since Moodle 3.3 */ function data_search_entries($data, $cm, $context, $mode, $currentgroup, $search = '', $sort = null, $order = null, $page = 0, $perpage = 0, $advanced = null, $searcharray = null, $record = null) { global $DB, $USER; if ($sort === null) { $sort = $data->defaultsort; } if ($order === null) { $order = ($data->defaultsortdir == 0) ? 'ASC' : 'DESC'; } if ($searcharray === null) { $searcharray = array(); } if (core_text::strlen($search) < 2) { $search = ''; } $approvecap = has_capability('mod/data:approve', $context); $canmanageentries = has_capability('mod/data:manageentries', $context); // If a student is not part of a group and seperate groups is enabled, we don't // want them seeing all records. $groupmode = groups_get_activity_groupmode($cm); $canviewallrecords = $groupmode != SEPARATEGROUPS || has_capability('moodle/site:accessallgroups', $context); $numentries = data_numentries($data); $requiredentriesallowed = true; if (data_get_entries_left_to_view($data, $numentries, $canmanageentries)) { $requiredentriesallowed = false; } // Initialise the first group of params for advanced searches. $initialparams = array(); $params = array(); // Named params array. // Setup group and approve restrictions. if (!$approvecap && $data->approval) { if (isloggedin()) { $approveselect = ' AND (r.approved=1 OR r.userid=:myid1) '; $params['myid1'] = $USER->id; $initialparams['myid1'] = $params['myid1']; } else { $approveselect = ' AND r.approved=1 '; } } else { $approveselect = ' '; } if ($currentgroup) { $groupselect = " AND (r.groupid = :currentgroup OR r.groupid = 0)"; $params['currentgroup'] = $currentgroup; $initialparams['currentgroup'] = $params['currentgroup']; } else { if ($canviewallrecords) { $groupselect = ' '; } else { // If separate groups are enabled and the user isn't in a group or // a teacher, manager, admin etc, then just show them entries for 'All participants'. $groupselect = " AND r.groupid = 0"; } } // Init some variables to be used by advanced search. $advsearchselect = ''; $advwhere = ''; $advtables = ''; $advparams = array(); // This is used for the initial reduction of advanced search results with required entries. $entrysql = ''; $userfieldsapi = \core_user\fields::for_userpic()->excluding('id'); $namefields = $userfieldsapi->get_sql('u', false, '', '', false)->selects; // Find the field we are sorting on. if ($sort <= 0 || !($sortfield = data_get_field_from_id($sort, $data))) { switch ($sort) { case DATA_LASTNAME: $ordering = "u.lastname $order, u.firstname $order"; break; case DATA_FIRSTNAME: $ordering = "u.firstname $order, u.lastname $order"; break; case DATA_APPROVED: $ordering = "r.approved $order, r.timecreated $order"; break; case DATA_TIMEMODIFIED: $ordering = "r.timemodified $order"; break; case DATA_TIMEADDED: default: $sort = 0; $ordering = "r.timecreated $order"; } $what = ' DISTINCT r.id, r.approved, r.timecreated, r.timemodified, r.userid, r.groupid, r.dataid, ' . $namefields; $count = ' COUNT(DISTINCT c.recordid) '; $tables = '{data_content} c,{data_records} r, {user} u '; $where = 'WHERE c.recordid = r.id AND r.dataid = :dataid AND r.userid = u.id '; $params['dataid'] = $data->id; $sortorder = " ORDER BY $ordering, r.id $order"; $searchselect = ''; // If requiredentries is not reached, only show current user's entries. if (!$requiredentriesallowed) { $where .= ' AND u.id = :myid2 '; $entrysql = ' AND r.userid = :myid3 '; $params['myid2'] = $USER->id; $initialparams['myid3'] = $params['myid2']; } if ($search) { $searchselect = " AND (".$DB->sql_like('c.content', ':search1', false)." OR ".$DB->sql_like('u.firstname', ':search2', false)." OR ".$DB->sql_like('u.lastname', ':search3', false)." ) "; $params['search1'] = "%$search%"; $params['search2'] = "%$search%"; $params['search3'] = "%$search%"; } else { $searchselect = ' '; } } else { $sortcontent = $DB->sql_compare_text('s.' . $sortfield->get_sort_field()); $sortcontentfull = $sortfield->get_sort_sql($sortcontent); $what = ' DISTINCT r.id, r.approved, r.timecreated, r.timemodified, r.userid, r.groupid, r.dataid, ' . $namefields . ', ' . $sortcontentfull . ' AS sortorder '; $count = ' COUNT(DISTINCT c.recordid) '; $tables = '{data_content} c, {data_records} r, {user} u '; $where = 'WHERE c.recordid = r.id AND r.dataid = :dataid AND r.userid = u.id '; if (!$advanced) { $where .= 'AND s.fieldid = :sort AND s.recordid = r.id'; $tables .= ',{data_content} s '; } $params['dataid'] = $data->id; $params['sort'] = $sort; $sortorder = ' ORDER BY sortorder '.$order.' , r.id ASC '; $searchselect = ''; // If requiredentries is not reached, only show current user's entries. if (!$requiredentriesallowed) { $where .= ' AND u.id = :myid2'; $entrysql = ' AND r.userid = :myid3'; $params['myid2'] = $USER->id; $initialparams['myid3'] = $params['myid2']; } if ($search) { $searchselect = " AND (".$DB->sql_like('c.content', ':search1', false)." OR ".$DB->sql_like('u.firstname', ':search2', false)." OR ".$DB->sql_like('u.lastname', ':search3', false)." ) "; $params['search1'] = "%$search%"; $params['search2'] = "%$search%"; $params['search3'] = "%$search%"; } else { $searchselect = ' '; } } // To actually fetch the records. $fromsql = "FROM $tables $advtables $where $advwhere $groupselect $approveselect $searchselect $advsearchselect"; $allparams = array_merge($params, $advparams); // Provide initial sql statements and parameters to reduce the number of total records. $initialselect = $groupselect . $approveselect . $entrysql; $recordids = data_get_all_recordids($data->id, $initialselect, $initialparams); $newrecordids = data_get_advance_search_ids($recordids, $searcharray, $data->id); $selectdata = $where . $groupselect . $approveselect; if (!empty($advanced)) { $advancedsearchsql = data_get_advanced_search_sql($sort, $data, $newrecordids, $selectdata, $sortorder); $sqlselect = $advancedsearchsql['sql']; $allparams = array_merge($allparams, $advancedsearchsql['params']); $totalcount = count($newrecordids); } else { $sqlselect = "SELECT $what $fromsql $sortorder"; $sqlcountselect = "SELECT $count $fromsql"; $totalcount = $DB->count_records_sql($sqlcountselect, $allparams); } // Work out the paging numbers and counts. if (empty($searchselect) && empty($advsearchselect)) { $maxcount = $totalcount; } else { $maxcount = count($recordids); } if ($record) { // We need to just show one, so where is it in context? $nowperpage = 1; $mode = 'single'; $page = 0; // TODO MDL-33797 - Reduce this or consider redesigning the paging system. if ($allrecordids = $DB->get_fieldset_sql($sqlselect, $allparams)) { $page = (int)array_search($record->id, $allrecordids); unset($allrecordids); } } else if ($mode == 'single') { // We rely on ambient $page settings $nowperpage = 1; } else { $nowperpage = $perpage; } // Get the actual records. if (!$records = $DB->get_records_sql($sqlselect, $allparams, $page * $nowperpage, $nowperpage)) { // Nothing to show! if ($record) { // Something was requested so try to show that at least (bug 5132) if (data_can_view_record($data, $record, $currentgroup, $canmanageentries)) { // OK, we can show this one $records = array($record->id => $record); $totalcount = 1; } } } return [$records, $maxcount, $totalcount, $page, $nowperpage, $sort, $mode]; } /** * Check if the current user can view the given record. * * @param stdClass $data database record * @param stdClass $record the record (entry) to check * @param int $currentgroup current group * @param bool $canmanageentries if the user can manage entries * @return bool true if the user can view the entry * @since Moodle 3.3 */ function data_can_view_record($data, $record, $currentgroup, $canmanageentries) { global $USER; if ($canmanageentries || empty($data->approval) || $record->approved || (isloggedin() && $record->userid == $USER->id)) { if (!$currentgroup || $record->groupid == $currentgroup || $record->groupid == 0) { return true; } } return false; } /** * Return all the field instances for a given database. * * @param stdClass $data database object * @return array field instances * @since Moodle 3.3 */ function data_get_field_instances($data) { global $DB; $instances = []; if ($fields = $DB->get_records('data_fields', array('dataid' => $data->id), 'id')) { foreach ($fields as $field) { $instances[] = data_get_field($field, $data); } } return $instances; } /** * Build the search array. * * @param stdClass $data the database object * @param bool $paging if paging is being used * @param array $searcharray the current search array (saved by session) * @param array $defaults default values for the searchable fields * @param str $fn the first name to search (optional) * @param str $ln the last name to search (optional) * @return array the search array and plain search build based on the different elements * @since Moodle 3.3 */ function data_build_search_array($data, $paging, $searcharray, $defaults = null, $fn = '', $ln = '') { global $DB; $search = ''; $vals = array(); $fields = $DB->get_records('data_fields', array('dataid' => $data->id)); if (!empty($fields)) { foreach ($fields as $field) { $searchfield = data_get_field_from_id($field->id, $data); // Get field data to build search sql with. If paging is false, get from user. // If paging is true, get data from $searcharray which is obtained from the $SESSION (see line 116). if (!$paging && $searchfield->type != 'unknown') { $val = $searchfield->parse_search_field($defaults); } else { // Set value from session if there is a value @ the required index. if (isset($searcharray[$field->id])) { $val = $searcharray[$field->id]->data; } else { // If there is not an entry @ the required index, set value to blank. $val = ''; } } if (!empty($val)) { $searcharray[$field->id] = new stdClass(); list($searcharray[$field->id]->sql, $searcharray[$field->id]->params) = $searchfield->generate_sql('c'.$field->id, $val); $searcharray[$field->id]->data = $val; $vals[] = $val; } else { // Clear it out. unset($searcharray[$field->id]); } } } $rawtagnames = optional_param_array('tags', false, PARAM_TAGLIST); if ($rawtagnames) { $searcharray[DATA_TAGS] = new stdClass(); $searcharray[DATA_TAGS]->params = []; $searcharray[DATA_TAGS]->rawtagnames = $rawtagnames; $searcharray[DATA_TAGS]->sql = ''; } else { unset($searcharray[DATA_TAGS]); } if (!$paging) { // Name searching. $fn = optional_param('u_fn', $fn, PARAM_NOTAGS); $ln = optional_param('u_ln', $ln, PARAM_NOTAGS); } else { $fn = isset($searcharray[DATA_FIRSTNAME]) ? $searcharray[DATA_FIRSTNAME]->data : ''; $ln = isset($searcharray[DATA_LASTNAME]) ? $searcharray[DATA_LASTNAME]->data : ''; } if (!empty($fn)) { $searcharray[DATA_FIRSTNAME] = new stdClass(); $searcharray[DATA_FIRSTNAME]->sql = ''; $searcharray[DATA_FIRSTNAME]->params = array(); $searcharray[DATA_FIRSTNAME]->field = 'u.firstname'; $searcharray[DATA_FIRSTNAME]->data = $fn; $vals[] = $fn; } else { unset($searcharray[DATA_FIRSTNAME]); } if (!empty($ln)) { $searcharray[DATA_LASTNAME] = new stdClass(); $searcharray[DATA_LASTNAME]->sql = ''; $searcharray[DATA_LASTNAME]->params = array(); $searcharray[DATA_LASTNAME]->field = 'u.lastname'; $searcharray[DATA_LASTNAME]->data = $ln; $vals[] = $ln; } else { unset($searcharray[DATA_LASTNAME]); } // In case we want to switch to simple search later - there might be multiple values there ;-). if ($vals) { $val = reset($vals); if (is_string($val)) { $search = $val; } } return [$searcharray, $search]; } /** * Approves or unapproves an entry. * * @param int $entryid the entry to approve or unapprove. * @param bool $approve Whether to approve or unapprove (true for approve false otherwise). * @since Moodle 3.3 */ function data_approve_entry($entryid, $approve) { global $DB; $newrecord = new stdClass(); $newrecord->id = $entryid; $newrecord->approved = $approve ? 1 : 0; $DB->update_record('data_records', $newrecord); } /** * Populate the field contents of a new record with the submitted data. * An event has been previously triggered upon the creation of the new record in data_add_record(). * * @param stdClass $data database object * @param stdClass $context context object * @param int $recordid the new record id * @param array $fields list of fields of the database * @param stdClass $datarecord the submitted data * @param stdClass $processeddata pre-processed submitted fields * @since Moodle 3.3 */ function data_add_fields_contents_to_new_record($data, $context, $recordid, $fields, $datarecord, $processeddata) { global $DB; // Insert a whole lot of empty records to make sure we have them. $records = array(); foreach ($fields as $field) { $content = new stdClass(); $content->recordid = $recordid; $content->fieldid = $field->id; $records[] = $content; } // Bulk insert the records now. Some records may have no data but all must exist. $DB->insert_records('data_content', $records); // Add all provided content. foreach ($processeddata->fields as $fieldname => $field) { $field->update_content($recordid, $datarecord->$fieldname, $fieldname); } } /** * Updates the fields contents of an existing record. * * @param stdClass $data database object * @param stdClass $record record to update object * @param stdClass $context context object * @param stdClass $datarecord the submitted data * @param stdClass $processeddata pre-processed submitted fields * @since Moodle 3.3 */ function data_update_record_fields_contents($data, $record, $context, $datarecord, $processeddata) { global $DB; // Reset the approved flag after edit if the user does not have permission to approve their own entries. if (!has_capability('mod/data:approve', $context)) { $record->approved = 0; } // Update the parent record. $record->timemodified = time(); $DB->update_record('data_records', $record); // Update all content. foreach ($processeddata->fields as $fieldname => $field) { $field->update_content($record->id, $datarecord->$fieldname, $fieldname); } // Trigger an event for updating this record. $event = \mod_data\event\record_updated::create(array( 'objectid' => $record->id, 'context' => $context, 'courseid' => $data->course, 'other' => array( 'dataid' => $data->id ) )); $event->add_record_snapshot('data', $data); $event->trigger(); } home/smkb8269/public_html/lms.smk-almanshuriyah.sch.id/mod/workshop/locallib.php000064400000572761152267042160023635 0ustar00. /** * Library of internal classes and functions for module workshop * * All the workshop specific functions, needed to implement the module * logic, should go to here. Instead of having bunch of function named * workshop_something() taking the workshop instance as the first * parameter, we use a class workshop that provides all methods. * * @package mod_workshop * @copyright 2009 David Mudrak * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); require_once(__DIR__.'/lib.php'); // we extend this library here require_once($CFG->libdir . '/gradelib.php'); // we use some rounding and comparing routines here require_once($CFG->libdir . '/filelib.php'); /** * Full-featured workshop API * * This wraps the workshop database record with a set of methods that are called * from the module itself. The class should be initialized right after you get * $workshop, $cm and $course records at the begining of the script. */ class workshop { /** error status of the {@link self::add_allocation()} */ const ALLOCATION_EXISTS = -9999; /** the internal code of the workshop phases as are stored in the database */ const PHASE_SETUP = 10; const PHASE_SUBMISSION = 20; const PHASE_ASSESSMENT = 30; const PHASE_EVALUATION = 40; const PHASE_CLOSED = 50; /** the internal code of the examples modes as are stored in the database */ const EXAMPLES_VOLUNTARY = 0; const EXAMPLES_BEFORE_SUBMISSION = 1; const EXAMPLES_BEFORE_ASSESSMENT = 2; /** @var stdclass workshop record from database */ public $dbrecord; /** @var cm_info course module record */ public $cm; /** @var stdclass course record */ public $course; /** @var stdclass context object */ public $context; /** @var int workshop instance identifier */ public $id; /** @var string workshop activity name */ public $name; /** @var string introduction or description of the activity */ public $intro; /** @var int format of the {@link $intro} */ public $introformat; /** @var string instructions for the submission phase */ public $instructauthors; /** @var int format of the {@link $instructauthors} */ public $instructauthorsformat; /** @var string instructions for the assessment phase */ public $instructreviewers; /** @var int format of the {@link $instructreviewers} */ public $instructreviewersformat; /** @var int timestamp of when the module was modified */ public $timemodified; /** @var int current phase of workshop, for example {@link workshop::PHASE_SETUP} */ public $phase; /** @var bool optional feature: students practise evaluating on example submissions from teacher */ public $useexamples; /** @var bool optional feature: students perform peer assessment of others' work (deprecated, consider always enabled) */ public $usepeerassessment; /** @var bool optional feature: students perform self assessment of their own work */ public $useselfassessment; /** @var float number (10, 5) unsigned, the maximum grade for submission */ public $grade; /** @var float number (10, 5) unsigned, the maximum grade for assessment */ public $gradinggrade; /** @var string type of the current grading strategy used in this workshop, for example 'accumulative' */ public $strategy; /** @var string the name of the evaluation plugin to use for grading grades calculation */ public $evaluation; /** @var int number of digits that should be shown after the decimal point when displaying grades */ public $gradedecimals; /** @var int number of allowed submission attachments and the files embedded into submission */ public $nattachments; /** @var string list of allowed file types that are allowed to be embedded into submission */ public $submissionfiletypes = null; /** @var bool allow submitting the work after the deadline */ public $latesubmissions; /** @var int maximum size of the one attached file in bytes */ public $maxbytes; /** @var int mode of example submissions support, for example {@link workshop::EXAMPLES_VOLUNTARY} */ public $examplesmode; /** @var int if greater than 0 then the submission is not allowed before this timestamp */ public $submissionstart; /** @var int if greater than 0 then the submission is not allowed after this timestamp */ public $submissionend; /** @var int if greater than 0 then the peer assessment is not allowed before this timestamp */ public $assessmentstart; /** @var int if greater than 0 then the peer assessment is not allowed after this timestamp */ public $assessmentend; /** @var bool automatically switch to the assessment phase after the submissions deadline */ public $phaseswitchassessment; /** @var string conclusion text to be displayed at the end of the activity */ public $conclusion; /** @var int format of the conclusion text */ public $conclusionformat; /** @var int the mode of the overall feedback */ public $overallfeedbackmode; /** @var int maximum number of overall feedback attachments */ public $overallfeedbackfiles; /** @var string list of allowed file types that can be attached to the overall feedback */ public $overallfeedbackfiletypes = null; /** @var int maximum size of one file attached to the overall feedback */ public $overallfeedbackmaxbytes; /** @var int Should the submission form show the text field? */ public $submissiontypetext; /** @var int Should the submission form show the file attachment field? */ public $submissiontypefile; /** * @var workshop_strategy grading strategy instance * Do not use directly, get the instance using {@link workshop::grading_strategy_instance()} */ protected $strategyinstance = null; /** * @var workshop_evaluation grading evaluation instance * Do not use directly, get the instance using {@link workshop::grading_evaluation_instance()} */ protected $evaluationinstance = null; /** * @var array It gets initialised in init_initial_bar, and may have keys 'i_first' and 'i_last' depending on what is selected. */ protected $initialbarprefs = []; /** * Initializes the workshop API instance using the data from DB * * Makes deep copy of all passed records properties. * * For unit testing only, $cm and $course may be set to null. This is so that * you can test without having any real database objects if you like. Not all * functions will work in this situation. * * @param stdClass $dbrecord Workshop instance data from {workshop} table * @param stdClass|cm_info $cm Course module record * @param stdClass $course Course record from {course} table * @param stdClass $context The context of the workshop instance */ public function __construct(stdclass $dbrecord, $cm, $course, ?stdclass $context=null) { $this->dbrecord = $dbrecord; foreach ($this->dbrecord as $field => $value) { if (property_exists('workshop', $field)) { $this->{$field} = $value; } } $this->strategy = clean_param($this->strategy, PARAM_PLUGIN); $this->evaluation = clean_param($this->evaluation, PARAM_PLUGIN); if (is_null($cm) || is_null($course)) { throw new coding_exception('Must specify $cm and $course'); } $this->course = $course; if ($cm instanceof cm_info) { $this->cm = $cm; } else { $modinfo = get_fast_modinfo($course); $this->cm = $modinfo->get_cm($cm->id); } if (is_null($context)) { $this->context = context_module::instance($this->cm->id); } else { $this->context = $context; } } //////////////////////////////////////////////////////////////////////////////// // Static methods // //////////////////////////////////////////////////////////////////////////////// /** * Return list of available allocation methods * * @return array Array ['string' => 'string'] of localized allocation method names */ public static function installed_allocators() { $installed = core_component::get_plugin_list('workshopallocation'); $forms = array(); foreach ($installed as $allocation => $allocationpath) { if (file_exists($allocationpath . '/lib.php')) { $forms[$allocation] = get_string('pluginname', 'workshopallocation_' . $allocation); } } // usability - make sure that manual allocation appears the first if (isset($forms['manual'])) { $m = array('manual' => $forms['manual']); unset($forms['manual']); $forms = array_merge($m, $forms); } return $forms; } /** * Returns an array of options for the editors that are used for submitting and assessing instructions * * @param stdClass $context * @uses EDITOR_UNLIMITED_FILES hard-coded value for the 'maxfiles' option * @return array */ public static function instruction_editors_options(stdclass $context) { return array('subdirs' => 1, 'maxbytes' => 0, 'maxfiles' => -1, 'changeformat' => 1, 'context' => $context, 'noclean' => 1, 'trusttext' => 0); } /** * Given the percent and the total, returns the number * * @param float $percent from 0 to 100 * @param float $total the 100% value * @return float */ public static function percent_to_value($percent, $total) { if ($percent < 0 or $percent > 100) { throw new coding_exception('The percent can not be less than 0 or higher than 100'); } return $total * $percent / 100; } /** * Returns an array of numeric values that can be used as maximum grades * * @return array Array of integers */ public static function available_maxgrades_list() { $grades = array(); for ($i=100; $i>=0; $i--) { $grades[$i] = $i; } return $grades; } /** * Returns the localized list of supported examples modes * * @return array */ public static function available_example_modes_list() { $options = array(); $options[self::EXAMPLES_VOLUNTARY] = get_string('examplesvoluntary', 'workshop'); $options[self::EXAMPLES_BEFORE_SUBMISSION] = get_string('examplesbeforesubmission', 'workshop'); $options[self::EXAMPLES_BEFORE_ASSESSMENT] = get_string('examplesbeforeassessment', 'workshop'); return $options; } /** * Returns the list of available grading strategy methods * * @return array ['string' => 'string'] */ public static function available_strategies_list() { $installed = core_component::get_plugin_list('workshopform'); $forms = array(); foreach ($installed as $strategy => $strategypath) { if (file_exists($strategypath . '/lib.php')) { $forms[$strategy] = get_string('pluginname', 'workshopform_' . $strategy); } } return $forms; } /** * Returns the list of available grading evaluation methods * * @return array of (string)name => (string)localized title */ public static function available_evaluators_list() { $evals = array(); foreach (core_component::get_plugin_list_with_file('workshopeval', 'lib.php', false) as $eval => $evalpath) { $evals[$eval] = get_string('pluginname', 'workshopeval_' . $eval); } return $evals; } /** * Return an array of possible values of assessment dimension weight * * @return array of integers 0, 1, 2, ..., 16 */ public static function available_dimension_weights_list() { $weights = array(); for ($i=16; $i>=0; $i--) { $weights[$i] = $i; } return $weights; } /** * Return an array of possible values of assessment weight * * Note there is no real reason why the maximum value here is 16. It used to be 10 in * workshop 1.x and I just decided to use the same number as in the maximum weight of * a single assessment dimension. * The value looks reasonable, though. Teachers who would want to assign themselves * higher weight probably do not want peer assessment really... * * @return array of integers 0, 1, 2, ..., 16 */ public static function available_assessment_weights_list() { $weights = array(); for ($i=16; $i>=0; $i--) { $weights[$i] = $i; } return $weights; } /** * Helper function returning the greatest common divisor * * @param int $a * @param int $b * @return int */ public static function gcd($a, $b) { return ($b == 0) ? ($a):(self::gcd($b, $a % $b)); } /** * Helper function returning the least common multiple * * @param int $a * @param int $b * @return int */ public static function lcm($a, $b) { return ($a / self::gcd($a,$b)) * $b; } /** * Returns an object suitable for strings containing dates/times * * The returned object contains properties date, datefullshort, datetime, ... containing the given * timestamp formatted using strftimedate, strftimedatefullshort, strftimedatetime, ... from the * current lang's langconfig.php * This allows translators and administrators customize the date/time format. * * @param int $timestamp the timestamp in UTC * @return stdclass */ public static function timestamp_formats($timestamp) { $formats = array('date', 'datefullshort', 'dateshort', 'datetime', 'datetimeshort', 'daydate', 'daydatetime', 'dayshort', 'daytime', 'monthyear', 'recent', 'recentfull', 'time'); $a = new stdclass(); foreach ($formats as $format) { $a->{$format} = userdate($timestamp, get_string('strftime'.$format, 'langconfig')); } $day = userdate($timestamp, '%Y%m%d', 99, false); $today = userdate(time(), '%Y%m%d', 99, false); $tomorrow = userdate(time() + DAYSECS, '%Y%m%d', 99, false); $yesterday = userdate(time() - DAYSECS, '%Y%m%d', 99, false); $distance = (int)round(abs(time() - $timestamp) / DAYSECS); if ($day == $today) { $a->distanceday = get_string('daystoday', 'workshop'); } elseif ($day == $yesterday) { $a->distanceday = get_string('daysyesterday', 'workshop'); } elseif ($day < $today) { $a->distanceday = get_string('daysago', 'workshop', $distance); } elseif ($day == $tomorrow) { $a->distanceday = get_string('daystomorrow', 'workshop'); } elseif ($day > $today) { $a->distanceday = get_string('daysleft', 'workshop', $distance); } return $a; } /** * Converts the argument into an array (list) of file extensions. * * The list can be separated by whitespace, end of lines, commas colons and semicolons. * Empty values are not returned. Values are converted to lowercase. * Duplicates are removed. Glob evaluation is not supported. * * @deprecated since Moodle 3.4 MDL-56486 - please use the {@link core_form\filetypes_util} * @param string|array $extensions list of file extensions * @return array of strings */ public static function normalize_file_extensions($extensions) { debugging('The method workshop::normalize_file_extensions() is deprecated. Please use the methods provided by the \core_form\filetypes_util class.', DEBUG_DEVELOPER); if ($extensions === '') { return array(); } if (!is_array($extensions)) { $extensions = preg_split('/[\s,;:"\']+/', $extensions, -1, PREG_SPLIT_NO_EMPTY); } foreach ($extensions as $i => $extension) { $extension = str_replace('*.', '', $extension); $extension = strtolower($extension); $extension = ltrim($extension, '.'); $extension = trim($extension); $extensions[$i] = $extension; } foreach ($extensions as $i => $extension) { if (strpos($extension, '*') !== false or strpos($extension, '?') !== false) { unset($extensions[$i]); } } $extensions = array_filter($extensions, 'strlen'); $extensions = array_keys(array_flip($extensions)); foreach ($extensions as $i => $extension) { $extensions[$i] = '.'.$extension; } return $extensions; } /** * Cleans the user provided list of file extensions. * * @deprecated since Moodle 3.4 MDL-56486 - please use the {@link core_form\filetypes_util} * @param string $extensions * @return string */ public static function clean_file_extensions($extensions) { debugging('The method workshop::clean_file_extensions() is deprecated. Please use the methods provided by the \core_form\filetypes_util class.', DEBUG_DEVELOPER); $extensions = self::normalize_file_extensions($extensions); foreach ($extensions as $i => $extension) { $extensions[$i] = ltrim($extension, '.'); } return implode(', ', $extensions); } /** * Check given file types and return invalid/unknown ones. * * Empty allowlist is interpretted as "any extension is valid". * * @deprecated since Moodle 3.4 MDL-56486 - please use the {@link core_form\filetypes_util} * @param string|array $extensions list of file extensions * @param string|array $allowlist list of valid extensions * @return array list of invalid extensions not found in the allowlist */ public static function invalid_file_extensions($extensions, $allowlist) { debugging('The method workshop::invalid_file_extensions() is deprecated. Please use the methods provided by the \core_form\filetypes_util class.', DEBUG_DEVELOPER); $extensions = self::normalize_file_extensions($extensions); $allowlist = self::normalize_file_extensions($allowlist); if (empty($extensions) or empty($allowlist)) { return array(); } // Return those items from $extensions that are not present in $allowlist. return array_keys(array_diff_key(array_flip($extensions), array_flip($allowlist))); } /** * Is the file have allowed to be uploaded to the workshop? * * Empty allowlist is interpretted as "any file type is allowed" rather * than "no file can be uploaded". * * @deprecated since Moodle 3.4 MDL-56486 - please use the {@link core_form\filetypes_util} * @param string $filename the file name * @param string|array $allowlist list of allowed file extensions * @return false */ public static function is_allowed_file_type($filename, $allowlist) { debugging('The method workshop::is_allowed_file_type() is deprecated. Please use the methods provided by the \core_form\filetypes_util class.', DEBUG_DEVELOPER); $allowlist = self::normalize_file_extensions($allowlist); if (empty($allowlist)) { return true; } $haystack = strrev(trim(strtolower($filename))); foreach ($allowlist as $extension) { if (strpos($haystack, strrev($extension)) === 0) { // The file name ends with the extension. return true; } } return false; } //////////////////////////////////////////////////////////////////////////////// // Workshop API // //////////////////////////////////////////////////////////////////////////////// /** * Fetches all enrolled users with the capability mod/workshop:submit in the current workshop * * The returned objects contain properties required by user_picture and are ordered by lastname, firstname. * Only users with the active enrolment are returned. * * @param bool $musthavesubmission if true, return only users who have already submitted * @param int $groupid 0 means ignore groups, any other value limits the result by group id * @param int $limitfrom return a subset of records, starting at this point (optional, required if $limitnum is set) * @param int $limitnum return a subset containing this number of records (optional, required if $limitfrom is set) * @return array array[userid] => stdClass */ public function get_potential_authors($musthavesubmission=true, $groupid=0, $limitfrom=0, $limitnum=0) { return $this->get_users_with_capability(['mod/workshop:submit'], $musthavesubmission, $groupid, $limitfrom, $limitnum); } /** * Returns the total number of users that would be fetched by {@link self::get_potential_authors()} * * @param bool $musthavesubmission if true, count only users who have already submitted * @param int $groupid 0 means ignore groups, any other value limits the result by group id * @return int */ public function count_potential_authors($musthavesubmission=true, $groupid=0) { return count($this->get_users_with_capability(['mod/workshop:submit'], $musthavesubmission, $groupid)); } /** * Fetches all enrolled users with the capability mod/workshop:peerassess in the current workshop * * The returned objects contain properties required by user_picture and are ordered by lastname, firstname. * Only users with the active enrolment are returned. * * @param bool $musthavesubmission if true, return only users who have already submitted * @param int $groupid 0 means ignore groups, any other value limits the result by group id * @param int $limitfrom return a subset of records, starting at this point (optional, required if $limitnum is set) * @param int $limitnum return a subset containing this number of records (optional, required if $limitfrom is set) * @return array array[userid] => stdClass */ public function get_potential_reviewers($musthavesubmission=false, $groupid=0, $limitfrom=0, $limitnum=0) { return $this->get_users_with_capability(['mod/workshop:peerassess'], $musthavesubmission, $groupid, $limitfrom, $limitnum); } /** * Returns the total number of users that would be fetched by {@link self::get_potential_reviewers()} * * @param bool $musthavesubmission if true, count only users who have already submitted * @param int $groupid 0 means ignore groups, any other value limits the result by group id * @return int */ public function count_potential_reviewers($musthavesubmission=false, $groupid=0) { return count($this->get_users_with_capability(['mod/workshop:peerassess'], $musthavesubmission, $groupid)); } /** * Fetches all enrolled users that are authors or reviewers (or both) in the current workshop * * The returned objects contain properties required by user_picture and are ordered by lastname, firstname. * Only users with the active enrolment are returned. * * @see self::get_potential_authors() * @see self::get_potential_reviewers() * @param bool $musthavesubmission if true, return only users who have already submitted * @param int $groupid 0 means ignore groups, any other value limits the result by group id * @param int $limitfrom return a subset of records, starting at this point (optional, required if $limitnum is set) * @param int $limitnum return a subset containing this number of records (optional, required if $limitfrom is set) * @return array array[userid] => stdClass */ public function get_participants($musthavesubmission=false, $groupid=0, $limitfrom=0, $limitnum=0) { // Get any users who have either of these 2 capabilities on the activity. return $this->get_users_with_capability(['mod/workshop:submit', 'mod/workshop:peerassess'], $musthavesubmission, $groupid, $limitfrom, $limitnum); } /** * Returns the total number of records that would be returned by {@link self::get_participants()} * * @param bool $musthavesubmission if true, return only users who have already submitted * @param int $groupid 0 means ignore groups, any other value limits the result by group id * @return int */ public function count_participants($musthavesubmission=false, $groupid=0) { return count($this->get_participants($musthavesubmission, $groupid)); } /** * Checks if the given user is an actively enrolled participant in the workshop * * @param int $userid, defaults to the current $USER * @return boolean */ public function is_participant($userid=null) { global $USER; if (is_null($userid)) { $userid = $USER->id; } // Get the participants on this activity and see if the user exists in that array. $participants = $this->get_participants(); return (array_key_exists($userid, $participants)); } /** * Groups the given users by the group membership * * This takes the module grouping settings into account. If a grouping is * set, returns only groups withing the course module grouping. Always * returns group [0] with all the given users. * * @param array $users array[userid] => stdclass{->id ->lastname ->firstname} * @return array array[groupid][userid] => stdclass{->id ->lastname ->firstname} */ public function get_grouped($users) { global $DB; global $CFG; $grouped = array(); // grouped users to be returned if (empty($users)) { return $grouped; } if ($this->cm->groupingid) { // Group workshop set to specified grouping - only consider groups // within this grouping, and leave out users who aren't members of // this grouping. $groupingid = $this->cm->groupingid; // All users that are members of at least one group will be // added into a virtual group id 0 $grouped[0] = array(); } else { $groupingid = 0; // there is no need to be member of a group so $grouped[0] will contain // all users $grouped[0] = $users; } $gmemberships = groups_get_all_groups($this->cm->course, array_keys($users), $groupingid, 'gm.id,gm.groupid,gm.userid'); foreach ($gmemberships as $gmembership) { if (!isset($grouped[$gmembership->groupid])) { $grouped[$gmembership->groupid] = array(); } $grouped[$gmembership->groupid][$gmembership->userid] = $users[$gmembership->userid]; $grouped[0][$gmembership->userid] = $users[$gmembership->userid]; } return $grouped; } /** * Returns the list of all allocations (i.e. assigned assessments) in the workshop * * Assessments of example submissions are ignored * * @return array */ public function get_allocations() { global $DB; $sql = 'SELECT a.id, a.submissionid, a.reviewerid, s.authorid FROM {workshop_assessments} a INNER JOIN {workshop_submissions} s ON (a.submissionid = s.id) WHERE s.example = 0 AND s.workshopid = :workshopid'; $params = array('workshopid' => $this->id); return $DB->get_records_sql($sql, $params); } /** * Returns the total number of records that would be returned by {@link self::get_submissions()} * * @param mixed $authorid int|array|'all' If set to [array of] integer, return submission[s] of the given user[s] only * @param int $groupid If non-zero, return only submissions by authors in the specified group * @return int number of records */ public function count_submissions($authorid='all', $groupid=0) { global $DB; $params = array('workshopid' => $this->id); $sql = "SELECT COUNT(s.id) FROM {workshop_submissions} s JOIN {user} u ON (s.authorid = u.id)"; if ($groupid) { $sql .= " JOIN {groups_members} gm ON (gm.userid = u.id AND gm.groupid = :groupid)"; $params['groupid'] = $groupid; } $sql .= " WHERE s.example = 0 AND s.workshopid = :workshopid"; if ('all' === $authorid) { // no additional conditions } elseif (!empty($authorid)) { list($usql, $uparams) = $DB->get_in_or_equal($authorid, SQL_PARAMS_NAMED); $sql .= " AND authorid $usql"; $params = array_merge($params, $uparams); } else { // $authorid is empty return 0; } return $DB->count_records_sql($sql, $params); } /** * Returns the total number of assessments in the workshop. * * @param bool $onlygraded If true, count only graded assessments * @param int|null $groupid If not null, return only assessments by reviewers in the specified group * @return int Number of assessments */ public function count_assessments(bool $onlygraded = false, ?int $groupid = null): int { global $DB; $params = ['workshopid' => $this->id]; $sql = "SELECT COUNT(s.id) FROM {workshop_assessments} s JOIN {workshop_submissions} ws ON (s.submissionid = ws.id) JOIN {user} u ON (ws.authorid = u.id) JOIN {workshop} w ON (ws.workshopid = w.id)"; if ($groupid) { $sql .= " JOIN {groups_members} gm ON (gm.userid = u.id AND gm.groupid = :groupid)"; $params['groupid'] = $groupid; } if ($onlygraded) { $sql .= " WHERE s.grade IS NOT NULL AND w.id = :workshopid"; } else { $sql .= " WHERE w.id = :workshopid"; } return $DB->count_records_sql($sql, $params); } /** * Returns submissions from this workshop * * Fetches data from {workshop_submissions} and adds some useful information from other * tables. Does not return textual fields to prevent possible memory lack issues. * * @see self::count_submissions() * @param mixed $authorid int|array|'all' If set to [array of] integer, return submission[s] of the given user[s] only * @param int $groupid If non-zero, return only submissions by authors in the specified group * @param int $limitfrom Return a subset of records, starting at this point (optional) * @param int $limitnum Return a subset containing this many records in total (optional, required if $limitfrom is set) * @return array of records or an empty array */ public function get_submissions($authorid='all', $groupid=0, $limitfrom=0, $limitnum=0) { global $DB; $userfieldsapi = \core_user\fields::for_userpic(); $authorfields = $userfieldsapi->get_sql('u', false, 'author', 'authoridx', false)->selects; $gradeoverbyfields = $userfieldsapi->get_sql('t', false, 'over', 'gradeoverbyx', false)->selects; $params = array('workshopid' => $this->id); $sql = "SELECT s.id, s.workshopid, s.example, s.authorid, s.timecreated, s.timemodified, s.title, s.grade, s.gradeover, s.gradeoverby, s.published, $authorfields, $gradeoverbyfields FROM {workshop_submissions} s JOIN {user} u ON (s.authorid = u.id)"; if ($groupid) { $sql .= " JOIN {groups_members} gm ON (gm.userid = u.id AND gm.groupid = :groupid)"; $params['groupid'] = $groupid; } $sql .= " LEFT JOIN {user} t ON (s.gradeoverby = t.id) WHERE s.example = 0 AND s.workshopid = :workshopid"; if ('all' === $authorid) { // no additional conditions } elseif (!empty($authorid)) { list($usql, $uparams) = $DB->get_in_or_equal($authorid, SQL_PARAMS_NAMED); $sql .= " AND authorid $usql"; $params = array_merge($params, $uparams); } else { // $authorid is empty return array(); } list($sort, $sortparams) = users_order_by_sql('u'); $sql .= " ORDER BY $sort"; return $DB->get_records_sql($sql, array_merge($params, $sortparams), $limitfrom, $limitnum); } /** * Returns submissions from this workshop that are viewable by the current user (except example submissions). * * @param mixed $authorid int|array If set to [array of] integer, return submission[s] of the given user[s] only * @param int $groupid If non-zero, return only submissions by authors in the specified group. 0 for all groups. * @param int $limitfrom Return a subset of records, starting at this point (optional) * @param int $limitnum Return a subset containing this many records in total (optional, required if $limitfrom is set) * @return array of records and the total submissions count * @since Moodle 3.4 */ public function get_visible_submissions($authorid = 0, $groupid = 0, $limitfrom = 0, $limitnum = 0) { global $DB, $USER; $submissions = array(); $select = "SELECT s.*"; $selectcount = "SELECT COUNT(s.id)"; $from = " FROM {workshop_submissions} s"; $params = array('workshopid' => $this->id); // Check if the passed group (or all groups when groupid is 0) is visible by the current user. if (!groups_group_visible($groupid, $this->course, $this->cm)) { return array($submissions, 0); } if ($groupid) { $from .= " JOIN {groups_members} gm ON (gm.userid = s.authorid AND gm.groupid = :groupid)"; $params['groupid'] = $groupid; } $where = " WHERE s.workshopid = :workshopid AND s.example = 0"; if (!has_capability('mod/workshop:viewallsubmissions', $this->context)) { // Check published submissions. $workshopclosed = $this->phase == self::PHASE_CLOSED; $canviewpublished = has_capability('mod/workshop:viewpublishedsubmissions', $this->context); if ($workshopclosed && $canviewpublished) { $published = " OR s.published = 1"; } else { $published = ''; } // Always get submissions I did or I provided feedback to. $where .= " AND (s.authorid = :authorid OR s.gradeoverby = :graderid $published)"; $params['authorid'] = $USER->id; $params['graderid'] = $USER->id; } // Now, user filtering. if (!empty($authorid)) { list($usql, $uparams) = $DB->get_in_or_equal($authorid, SQL_PARAMS_NAMED); $where .= " AND s.authorid $usql"; $params = array_merge($params, $uparams); } $order = " ORDER BY s.timecreated"; $totalcount = $DB->count_records_sql($selectcount.$from.$where, $params); if ($totalcount) { $submissions = $DB->get_records_sql($select.$from.$where.$order, $params, $limitfrom, $limitnum); } return array($submissions, $totalcount); } /** * Returns a submission record with the author's data * * @param int $id submission id * @return stdclass */ public function get_submission_by_id($id) { global $DB; // we intentionally check the workshopid here, too, so the workshop can't touch submissions // from other instances $userfieldsapi = \core_user\fields::for_userpic(); $authorfields = $userfieldsapi->get_sql('u', false, 'author', 'authoridx', false)->selects; $gradeoverbyfields = $userfieldsapi->get_sql('g', false, 'gradeoverby', 'gradeoverbyx', false)->selects; $sql = "SELECT s.*, $authorfields, $gradeoverbyfields FROM {workshop_submissions} s INNER JOIN {user} u ON (s.authorid = u.id) LEFT JOIN {user} g ON (s.gradeoverby = g.id) WHERE s.example = 0 AND s.workshopid = :workshopid AND s.id = :id"; $params = array('workshopid' => $this->id, 'id' => $id); return $DB->get_record_sql($sql, $params, MUST_EXIST); } /** * Returns a submission submitted by the given author * * @param int $id author id * @return stdclass|false */ public function get_submission_by_author($authorid) { global $DB; if (empty($authorid)) { return false; } $userfieldsapi = \core_user\fields::for_userpic(); $authorfields = $userfieldsapi->get_sql('u', false, 'author', 'authoridx', false)->selects; $gradeoverbyfields = $userfieldsapi->get_sql('g', false, 'gradeoverby', 'gradeoverbyx', false)->selects; $sql = "SELECT s.*, $authorfields, $gradeoverbyfields FROM {workshop_submissions} s INNER JOIN {user} u ON (s.authorid = u.id) LEFT JOIN {user} g ON (s.gradeoverby = g.id) WHERE s.example = 0 AND s.workshopid = :workshopid AND s.authorid = :authorid"; $params = array('workshopid' => $this->id, 'authorid' => $authorid); return $DB->get_record_sql($sql, $params); } /** * Returns published submissions with their authors data * * @return array of stdclass */ public function get_published_submissions($orderby='finalgrade DESC') { global $DB; $userfieldsapi = \core_user\fields::for_userpic(); $authorfields = $userfieldsapi->get_sql('u', false, 'author', 'authoridx', false)->selects; $sql = "SELECT s.id, s.authorid, s.timecreated, s.timemodified, s.title, s.grade, s.gradeover, COALESCE(s.gradeover,s.grade) AS finalgrade, $authorfields FROM {workshop_submissions} s INNER JOIN {user} u ON (s.authorid = u.id) WHERE s.example = 0 AND s.workshopid = :workshopid AND s.published = 1 ORDER BY $orderby"; $params = array('workshopid' => $this->id); return $DB->get_records_sql($sql, $params); } /** * Returns full record of the given example submission * * @param int $id example submission od * @return object */ public function get_example_by_id($id) { global $DB; return $DB->get_record('workshop_submissions', array('id' => $id, 'workshopid' => $this->id, 'example' => 1), '*', MUST_EXIST); } /** * Returns the list of example submissions in this workshop with reference assessments attached * * @return array of objects or an empty array * @see workshop::prepare_example_summary() */ public function get_examples_for_manager() { global $DB; $sql = 'SELECT s.id, s.title, a.id AS assessmentid, a.grade, a.gradinggrade FROM {workshop_submissions} s LEFT JOIN {workshop_assessments} a ON (a.submissionid = s.id AND a.weight = 1) WHERE s.example = 1 AND s.workshopid = :workshopid ORDER BY s.title'; return $DB->get_records_sql($sql, array('workshopid' => $this->id)); } /** * Returns the list of all example submissions in this workshop with the information of assessments done by the given user * * @param int $reviewerid user id * @return array of objects, indexed by example submission id * @see workshop::prepare_example_summary() */ public function get_examples_for_reviewer($reviewerid) { global $DB; if (empty($reviewerid)) { return false; } $sql = 'SELECT s.id, s.title, a.id AS assessmentid, a.grade, a.gradinggrade FROM {workshop_submissions} s LEFT JOIN {workshop_assessments} a ON (a.submissionid = s.id AND a.reviewerid = :reviewerid AND a.weight = 0) WHERE s.example = 1 AND s.workshopid = :workshopid ORDER BY s.title'; return $DB->get_records_sql($sql, array('workshopid' => $this->id, 'reviewerid' => $reviewerid)); } /** * Prepares renderable submission component * * @param stdClass $record required by {@see workshop_submission} * @param bool $showauthor show the author-related information * @return workshop_submission */ public function prepare_submission(stdClass $record, $showauthor = false) { $submission = new workshop_submission($this, $record, $showauthor); $submission->url = $this->submission_url($record->id); return $submission; } /** * Prepares renderable submission summary component * * @param stdClass $record required by {@see workshop_submission_summary} * @param bool $showauthor show the author-related information * @return workshop_submission_summary */ public function prepare_submission_summary(stdClass $record, $showauthor = false) { $summary = new workshop_submission_summary($this, $record, $showauthor); $summary->url = $this->submission_url($record->id); return $summary; } /** * Prepares renderable example submission component * * @param stdClass $record required by {@see workshop_example_submission} * @return workshop_example_submission */ public function prepare_example_submission(stdClass $record) { $example = new workshop_example_submission($this, $record); return $example; } /** * Prepares renderable example submission summary component * * If the example is editable, the caller must set the 'editable' flag explicitly. * * @param stdClass $example as returned by {@link workshop::get_examples_for_manager()} or {@link workshop::get_examples_for_reviewer()} * @return workshop_example_submission_summary to be rendered */ public function prepare_example_summary(stdClass $example) { $summary = new workshop_example_submission_summary($this, $example); if (is_null($example->grade)) { $summary->status = 'notgraded'; $summary->assesslabel = get_string('assess', 'workshop'); } else { $summary->status = 'graded'; $summary->assesslabel = get_string('reassess', 'workshop'); } $summary->gradeinfo = new stdclass(); $summary->gradeinfo->received = $this->real_grade($example->grade); $summary->gradeinfo->max = $this->real_grade(100); $summary->url = new moodle_url($this->exsubmission_url($example->id)); $summary->editurl = new moodle_url($this->exsubmission_url($example->id), array('edit' => 'on')); $summary->assessurl = new moodle_url($this->exsubmission_url($example->id), array('assess' => 'on', 'sesskey' => sesskey())); return $summary; } /** * Prepares renderable assessment component * * The $options array supports the following keys: * showauthor - should the author user info be available for the renderer * showreviewer - should the reviewer user info be available for the renderer * showform - show the assessment form if it is available * showweight - should the assessment weight be available for the renderer * * @param stdClass $record as returned by eg {@link self::get_assessment_by_id()} * @param workshop_assessment_form|null $form as returned by {@link workshop_strategy::get_assessment_form()} * @param array $options * @return workshop_assessment */ public function prepare_assessment(stdClass $record, $form, array $options = array()) { $assessment = new workshop_assessment($this, $record, $options); $assessment->url = $this->assess_url($record->id); $assessment->maxgrade = $this->real_grade(100); if (!empty($options['showform']) and !($form instanceof workshop_assessment_form)) { debugging('Not a valid instance of workshop_assessment_form supplied', DEBUG_DEVELOPER); } if (!empty($options['showform']) and ($form instanceof workshop_assessment_form)) { $assessment->form = $form; } if (empty($options['showweight'])) { $assessment->weight = null; } if (!is_null($record->grade)) { $assessment->realgrade = $this->real_grade($record->grade); } return $assessment; } /** * Prepares renderable example submission's assessment component * * The $options array supports the following keys: * showauthor - should the author user info be available for the renderer * showreviewer - should the reviewer user info be available for the renderer * showform - show the assessment form if it is available * * @param stdClass $record as returned by eg {@link self::get_assessment_by_id()} * @param workshop_assessment_form|null $form as returned by {@link workshop_strategy::get_assessment_form()} * @param array $options * @return workshop_example_assessment */ public function prepare_example_assessment(stdClass $record, $form = null, array $options = array()) { $assessment = new workshop_example_assessment($this, $record, $options); $assessment->url = $this->exassess_url($record->id); $assessment->maxgrade = $this->real_grade(100); if (!empty($options['showform']) and !($form instanceof workshop_assessment_form)) { debugging('Not a valid instance of workshop_assessment_form supplied', DEBUG_DEVELOPER); } if (!empty($options['showform']) and ($form instanceof workshop_assessment_form)) { $assessment->form = $form; } if (!is_null($record->grade)) { $assessment->realgrade = $this->real_grade($record->grade); } $assessment->weight = null; return $assessment; } /** * Prepares renderable example submission's reference assessment component * * The $options array supports the following keys: * showauthor - should the author user info be available for the renderer * showreviewer - should the reviewer user info be available for the renderer * showform - show the assessment form if it is available * * @param stdClass $record as returned by eg {@link self::get_assessment_by_id()} * @param workshop_assessment_form|null $form as returned by {@link workshop_strategy::get_assessment_form()} * @param array $options * @return workshop_example_reference_assessment */ public function prepare_example_reference_assessment(stdClass $record, $form = null, array $options = array()) { $assessment = new workshop_example_reference_assessment($this, $record, $options); $assessment->maxgrade = $this->real_grade(100); if (!empty($options['showform']) and !($form instanceof workshop_assessment_form)) { debugging('Not a valid instance of workshop_assessment_form supplied', DEBUG_DEVELOPER); } if (!empty($options['showform']) and ($form instanceof workshop_assessment_form)) { $assessment->form = $form; } if (!is_null($record->grade)) { $assessment->realgrade = $this->real_grade($record->grade); } $assessment->weight = null; return $assessment; } /** * Removes the submission and all relevant data * * @param stdClass $submission record to delete * @return void */ public function delete_submission(stdclass $submission) { global $DB; $assessments = $DB->get_records('workshop_assessments', array('submissionid' => $submission->id), '', 'id'); $this->delete_assessment(array_keys($assessments)); $fs = get_file_storage(); $fs->delete_area_files($this->context->id, 'mod_workshop', 'submission_content', $submission->id); $fs->delete_area_files($this->context->id, 'mod_workshop', 'submission_attachment', $submission->id); $DB->delete_records('workshop_submissions', array('id' => $submission->id)); // Event information. $params = array( 'context' => $this->context, 'courseid' => $this->course->id, 'relateduserid' => $submission->authorid, 'other' => array( 'submissiontitle' => $submission->title ) ); $params['objectid'] = $submission->id; $event = \mod_workshop\event\submission_deleted::create($params); $event->add_record_snapshot('workshop', $this->dbrecord); $event->trigger(); } /** * Returns the list of all assessments in the workshop with some data added * * Fetches data from {workshop_assessments} and adds some useful information from other * tables. The returned object does not contain textual fields (i.e. comments) to prevent memory * lack issues. * * @return array [assessmentid] => assessment stdclass */ public function get_all_assessments() { global $DB; $userfieldsapi = \core_user\fields::for_userpic(); $reviewerfields = $userfieldsapi->get_sql('reviewer', false, '', 'revieweridx', false)->selects; $authorfields = $userfieldsapi->get_sql('author', false, 'author', 'authorid', false)->selects; $overbyfields = $userfieldsapi->get_sql('overby', false, 'overby', 'gradinggradeoverbyx', false)->selects; list($sort, $params) = users_order_by_sql('reviewer'); $sql = "SELECT a.id, a.submissionid, a.reviewerid, a.timecreated, a.timemodified, a.grade, a.gradinggrade, a.gradinggradeover, a.gradinggradeoverby, $reviewerfields, $authorfields, $overbyfields, s.title FROM {workshop_assessments} a INNER JOIN {user} reviewer ON (a.reviewerid = reviewer.id) INNER JOIN {workshop_submissions} s ON (a.submissionid = s.id) INNER JOIN {user} author ON (s.authorid = author.id) LEFT JOIN {user} overby ON (a.gradinggradeoverby = overby.id) WHERE s.workshopid = :workshopid AND s.example = 0 ORDER BY $sort"; $params['workshopid'] = $this->id; return $DB->get_records_sql($sql, $params); } /** * Get the complete information about the given assessment * * @param int $id Assessment ID * @return stdclass */ public function get_assessment_by_id($id) { global $DB; $userfieldsapi = \core_user\fields::for_userpic(); $reviewerfields = $userfieldsapi->get_sql('reviewer', false, 'reviewer', 'revieweridx', false)->selects; $authorfields = $userfieldsapi->get_sql('author', false, 'author', 'authorid', false)->selects; $overbyfields = $userfieldsapi->get_sql('overby', false, 'overby', 'gradinggradeoverbyx', false)->selects; $sql = "SELECT a.*, s.title, $reviewerfields, $authorfields, $overbyfields FROM {workshop_assessments} a INNER JOIN {user} reviewer ON (a.reviewerid = reviewer.id) INNER JOIN {workshop_submissions} s ON (a.submissionid = s.id) INNER JOIN {user} author ON (s.authorid = author.id) LEFT JOIN {user} overby ON (a.gradinggradeoverby = overby.id) WHERE a.id = :id AND s.workshopid = :workshopid"; $params = array('id' => $id, 'workshopid' => $this->id); return $DB->get_record_sql($sql, $params, MUST_EXIST); } /** * Get the complete information about the user's assessment of the given submission * * @param int $sid submission ID * @param int $uid user ID of the reviewer * @return false|stdclass false if not found, stdclass otherwise */ public function get_assessment_of_submission_by_user($submissionid, $reviewerid) { global $DB; $userfieldsapi = \core_user\fields::for_userpic(); $reviewerfields = $userfieldsapi->get_sql('reviewer', false, 'reviewer', 'revieweridx', false)->selects; $authorfields = $userfieldsapi->get_sql('author', false, 'author', 'authorid', false)->selects; $overbyfields = $userfieldsapi->get_sql('overby', false, 'overby', 'gradinggradeoverbyx', false)->selects; $sql = "SELECT a.*, s.title, $reviewerfields, $authorfields, $overbyfields FROM {workshop_assessments} a INNER JOIN {user} reviewer ON (a.reviewerid = reviewer.id) INNER JOIN {workshop_submissions} s ON (a.submissionid = s.id AND s.example = 0) INNER JOIN {user} author ON (s.authorid = author.id) LEFT JOIN {user} overby ON (a.gradinggradeoverby = overby.id) WHERE s.id = :sid AND reviewer.id = :rid AND s.workshopid = :workshopid"; $params = array('sid' => $submissionid, 'rid' => $reviewerid, 'workshopid' => $this->id); return $DB->get_record_sql($sql, $params, IGNORE_MISSING); } /** * Get the complete information about all assessments of the given submission * * @param int $submissionid * @return array */ public function get_assessments_of_submission($submissionid) { global $DB; $userfieldsapi = \core_user\fields::for_userpic(); $reviewerfields = $userfieldsapi->get_sql('reviewer', false, 'reviewer', 'revieweridx', false)->selects; $overbyfields = $userfieldsapi->get_sql('overby', false, 'overby', 'gradinggradeoverbyx', false)->selects; list($sort, $params) = users_order_by_sql('reviewer'); $sql = "SELECT a.*, s.title, $reviewerfields, $overbyfields FROM {workshop_assessments} a INNER JOIN {user} reviewer ON (a.reviewerid = reviewer.id) INNER JOIN {workshop_submissions} s ON (a.submissionid = s.id) LEFT JOIN {user} overby ON (a.gradinggradeoverby = overby.id) WHERE s.example = 0 AND s.id = :submissionid AND s.workshopid = :workshopid ORDER BY $sort"; $params['submissionid'] = $submissionid; $params['workshopid'] = $this->id; return $DB->get_records_sql($sql, $params); } /** * Get the complete information about all assessments allocated to the given reviewer * * @param int $reviewerid * @return array */ public function get_assessments_by_reviewer($reviewerid) { global $DB; $userfieldsapi = \core_user\fields::for_userpic(); $reviewerfields = $userfieldsapi->get_sql('reviewer', false, 'reviewer', 'revieweridx', false)->selects; $authorfields = $userfieldsapi->get_sql('author', false, 'author', 'authorid', false)->selects; $overbyfields = $userfieldsapi->get_sql('overby', false, 'overby', 'gradinggradeoverbyx', false)->selects; $sql = "SELECT a.*, $reviewerfields, $authorfields, $overbyfields, s.id AS submissionid, s.title AS submissiontitle, s.timecreated AS submissioncreated, s.timemodified AS submissionmodified FROM {workshop_assessments} a INNER JOIN {user} reviewer ON (a.reviewerid = reviewer.id) INNER JOIN {workshop_submissions} s ON (a.submissionid = s.id) INNER JOIN {user} author ON (s.authorid = author.id) LEFT JOIN {user} overby ON (a.gradinggradeoverby = overby.id) WHERE s.example = 0 AND reviewer.id = :reviewerid AND s.workshopid = :workshopid"; $params = array('reviewerid' => $reviewerid, 'workshopid' => $this->id); return $DB->get_records_sql($sql, $params); } /** * Get allocated assessments not graded yet by the given reviewer * * @see self::get_assessments_by_reviewer() * @param int $reviewerid the reviewer id * @param null|int|array $exclude optional assessment id (or list of them) to be excluded * @return array */ public function get_pending_assessments_by_reviewer($reviewerid, $exclude = null) { $assessments = $this->get_assessments_by_reviewer($reviewerid); foreach ($assessments as $id => $assessment) { if (!is_null($assessment->grade)) { unset($assessments[$id]); continue; } if (!empty($exclude)) { if (is_array($exclude) and in_array($id, $exclude)) { unset($assessments[$id]); continue; } else if ($id == $exclude) { unset($assessments[$id]); continue; } } } return $assessments; } /** * Allocate a submission to a user for review * * @param stdClass $submission Submission object with at least id property * @param int $reviewerid User ID * @param int $weight of the new assessment, from 0 to 16 * @param bool $bulk repeated inserts into DB expected * @return int ID of the new assessment or an error code {@link self::ALLOCATION_EXISTS} if the allocation already exists */ public function add_allocation(stdclass $submission, $reviewerid, $weight=1, $bulk=false) { global $DB; if ($DB->record_exists('workshop_assessments', array('submissionid' => $submission->id, 'reviewerid' => $reviewerid))) { return self::ALLOCATION_EXISTS; } $weight = (int)$weight; if ($weight < 0) { $weight = 0; } if ($weight > 16) { $weight = 16; } $now = time(); $assessment = new stdclass(); $assessment->submissionid = $submission->id; $assessment->reviewerid = $reviewerid; $assessment->timecreated = $now; // do not set timemodified here $assessment->weight = $weight; $assessment->feedbackauthorformat = editors_get_preferred_format(); $assessment->feedbackreviewerformat = editors_get_preferred_format(); return $DB->insert_record('workshop_assessments', $assessment, true, $bulk); } /** * Delete assessment record or records. * * Removes associated records from the workshop_grades table, too. * * @param int|array $id assessment id or array of assessments ids * @todo Give grading strategy plugins a chance to clean up their data, too. * @return bool true */ public function delete_assessment($id) { global $DB; if (empty($id)) { return true; } $fs = get_file_storage(); if (is_array($id)) { $DB->delete_records_list('workshop_grades', 'assessmentid', $id); foreach ($id as $itemid) { $fs->delete_area_files($this->context->id, 'mod_workshop', 'overallfeedback_content', $itemid); $fs->delete_area_files($this->context->id, 'mod_workshop', 'overallfeedback_attachment', $itemid); } $DB->delete_records_list('workshop_assessments', 'id', $id); } else { $DB->delete_records('workshop_grades', array('assessmentid' => $id)); $fs->delete_area_files($this->context->id, 'mod_workshop', 'overallfeedback_content', $id); $fs->delete_area_files($this->context->id, 'mod_workshop', 'overallfeedback_attachment', $id); $DB->delete_records('workshop_assessments', array('id' => $id)); } return true; } /** * Returns instance of grading strategy class * * @return stdclass Instance of a grading strategy */ public function grading_strategy_instance() { global $CFG; // because we require other libs here if (is_null($this->strategyinstance)) { if (empty($this->strategy)) { throw new coding_exception('Unknown grading strategy'); } $strategylib = __DIR__ . '/form/' . $this->strategy . '/lib.php'; if (is_readable($strategylib)) { require_once($strategylib); } else { throw new coding_exception('the grading forms subplugin must contain library ' . $strategylib); } $classname = 'workshop_' . $this->strategy . '_strategy'; $this->strategyinstance = new $classname($this); if (!in_array('workshop_strategy', class_implements($this->strategyinstance))) { throw new coding_exception($classname . ' does not implement workshop_strategy interface'); } } return $this->strategyinstance; } /** * Sets the current evaluation method to the given plugin. * * @param string $method the name of the workshopeval subplugin * @return bool true if successfully set * @throws coding_exception if attempting to set a non-installed evaluation method */ public function set_grading_evaluation_method($method) { global $DB; $method = clean_param($method, PARAM_PLUGIN); $evaluationlib = __DIR__ . '/eval/' . $method . '/lib.php'; if (is_readable($evaluationlib)) { $this->evaluationinstance = null; $this->evaluation = $method; $DB->set_field('workshop', 'evaluation', $method, array('id' => $this->id)); return true; } throw new coding_exception('Attempt to set a non-existing evaluation method.'); } /** * Returns instance of grading evaluation class * * @return stdclass Instance of a grading evaluation */ public function grading_evaluation_instance() { global $CFG; // because we require other libs here if (is_null($this->evaluationinstance)) { if (empty($this->evaluation)) { $this->evaluation = 'best'; } $evaluationlib = __DIR__ . '/eval/' . $this->evaluation . '/lib.php'; if (is_readable($evaluationlib)) { require_once($evaluationlib); } else { // Fall back in case the subplugin is not available. $this->evaluation = 'best'; $evaluationlib = __DIR__ . '/eval/' . $this->evaluation . '/lib.php'; if (is_readable($evaluationlib)) { require_once($evaluationlib); } else { // Fall back in case the subplugin is not available any more. throw new coding_exception('Missing default grading evaluation library ' . $evaluationlib); } } $classname = 'workshop_' . $this->evaluation . '_evaluation'; $this->evaluationinstance = new $classname($this); if (!in_array('workshop_evaluation', class_parents($this->evaluationinstance))) { throw new coding_exception($classname . ' does not extend workshop_evaluation class'); } } return $this->evaluationinstance; } /** * Returns instance of submissions allocator * * @param string $method The name of the allocation method, must be PARAM_ALPHA * @return stdclass Instance of submissions allocator */ public function allocator_instance($method) { global $CFG; // because we require other libs here $allocationlib = __DIR__ . '/allocation/' . $method . '/lib.php'; if (is_readable($allocationlib)) { require_once($allocationlib); } else { throw new coding_exception('Unable to find the allocation library ' . $allocationlib); } $classname = 'workshop_' . $method . '_allocator'; return new $classname($this); } /** * @return moodle_url of this workshop's view page */ public function view_url() { global $CFG; return new moodle_url('/mod/workshop/view.php', array('id' => $this->cm->id)); } /** * @return moodle_url of the page for editing this workshop's grading form */ public function editform_url() { global $CFG; return new moodle_url('/mod/workshop/editform.php', array('cmid' => $this->cm->id)); } /** * @return moodle_url of the page for previewing this workshop's grading form */ public function previewform_url() { global $CFG; return new moodle_url('/mod/workshop/editformpreview.php', array('cmid' => $this->cm->id)); } /** * @param int $assessmentid The ID of assessment record * @return moodle_url of the assessment page */ public function assess_url($assessmentid) { global $CFG; $assessmentid = clean_param($assessmentid, PARAM_INT); return new moodle_url('/mod/workshop/assessment.php', array('asid' => $assessmentid)); } /** * @param int $assessmentid The ID of assessment record * @return moodle_url of the example assessment page */ public function exassess_url($assessmentid) { global $CFG; $assessmentid = clean_param($assessmentid, PARAM_INT); return new moodle_url('/mod/workshop/exassessment.php', array('asid' => $assessmentid)); } /** * @return moodle_url of the page to view a submission, defaults to the own one */ public function submission_url($id=null) { global $CFG; return new moodle_url('/mod/workshop/submission.php', array('cmid' => $this->cm->id, 'id' => $id)); } /** * @param int $id example submission id * @return moodle_url of the page to view an example submission */ public function exsubmission_url($id) { global $CFG; return new moodle_url('/mod/workshop/exsubmission.php', array('cmid' => $this->cm->id, 'id' => $id)); } /** * @param int $sid submission id * @param array $aid of int assessment ids * @return moodle_url of the page to compare assessments of the given submission */ public function compare_url($sid, array $aids) { global $CFG; $url = new moodle_url('/mod/workshop/compare.php', array('cmid' => $this->cm->id, 'sid' => $sid)); $i = 0; foreach ($aids as $aid) { $url->param("aid{$i}", $aid); $i++; } return $url; } /** * @param int $sid submission id * @param int $aid assessment id * @return moodle_url of the page to compare the reference assessments of the given example submission */ public function excompare_url($sid, $aid) { global $CFG; return new moodle_url('/mod/workshop/excompare.php', array('cmid' => $this->cm->id, 'sid' => $sid, 'aid' => $aid)); } /** * @return moodle_url of the mod_edit form */ public function updatemod_url() { global $CFG; return new moodle_url('/course/modedit.php', array('update' => $this->cm->id, 'return' => 1)); } /** * @param string $method allocation method * @return moodle_url to the allocation page */ public function allocation_url($method=null) { global $CFG; $params = array('cmid' => $this->cm->id); if (!empty($method)) { $params['method'] = $method; } return new moodle_url('/mod/workshop/allocation.php', $params); } /** * @param int $phasecode The internal phase code * @return moodle_url of the script to change the current phase to $phasecode */ public function switchphase_url($phasecode) { global $CFG; $phasecode = clean_param($phasecode, PARAM_INT); return new moodle_url('/mod/workshop/switchphase.php', array('cmid' => $this->cm->id, 'phase' => $phasecode)); } /** * @return moodle_url to the aggregation page */ public function aggregate_url() { global $CFG; return new moodle_url('/mod/workshop/aggregate.php', array('cmid' => $this->cm->id)); } /** * @return moodle_url of this workshop's toolbox page */ public function toolbox_url($tool) { global $CFG; return new moodle_url('/mod/workshop/toolbox.php', array('id' => $this->cm->id, 'tool' => $tool)); } /** * Workshop wrapper around {@see add_to_log()} * @deprecated since 2.7 Please use the provided event classes for logging actions. * * @param string $action to be logged * @param moodle_url $url absolute url as returned by {@see workshop::submission_url()} and friends * @param mixed $info additional info, usually id in a table * @param bool $return true to return the arguments for add_to_log. * @return void|array array of arguments for add_to_log if $return is true */ public function log($action, ?moodle_url $url = null, $info = null, $return = false) { debugging('The log method is now deprecated, please use event classes instead', DEBUG_DEVELOPER); if (is_null($url)) { $url = $this->view_url(); } if (is_null($info)) { $info = $this->id; } $logurl = $this->log_convert_url($url); $args = array($this->course->id, 'workshop', $action, $logurl, $info, $this->cm->id); if ($return) { return $args; } call_user_func_array('add_to_log', $args); } /** * Is the given user allowed to create their submission? * * @param int $userid * @return bool */ public function creating_submission_allowed($userid) { $now = time(); $ignoredeadlines = has_capability('mod/workshop:ignoredeadlines', $this->context, $userid); if ($this->latesubmissions) { if ($this->phase != self::PHASE_SUBMISSION and $this->phase != self::PHASE_ASSESSMENT) { // late submissions are allowed in the submission and assessment phase only return false; } if (!$ignoredeadlines and !empty($this->submissionstart) and $this->submissionstart > $now) { // late submissions are not allowed before the submission start return false; } return true; } else { if ($this->phase != self::PHASE_SUBMISSION) { // submissions are allowed during the submission phase only return false; } if (!$ignoredeadlines and !empty($this->submissionstart) and $this->submissionstart > $now) { // if enabled, submitting is not allowed before the date/time defined in the mod_form return false; } if (!$ignoredeadlines and !empty($this->submissionend) and $now > $this->submissionend ) { // if enabled, submitting is not allowed after the date/time defined in the mod_form unless late submission is allowed return false; } return true; } } /** * Is the given user allowed to modify their existing submission? * * @param int $userid * @return bool */ public function modifying_submission_allowed($userid) { $now = time(); $ignoredeadlines = has_capability('mod/workshop:ignoredeadlines', $this->context, $userid); if ($this->phase != self::PHASE_SUBMISSION) { // submissions can be edited during the submission phase only return false; } if (!$ignoredeadlines and !empty($this->submissionstart) and $this->submissionstart > $now) { // if enabled, re-submitting is not allowed before the date/time defined in the mod_form return false; } if (!$ignoredeadlines and !empty($this->submissionend) and $now > $this->submissionend) { // if enabled, re-submitting is not allowed after the date/time defined in the mod_form even if late submission is allowed return false; } return true; } /** * Is the given reviewer allowed to create/edit their assessments? * * @param int $userid * @return bool */ public function assessing_allowed($userid) { if ($this->phase != self::PHASE_ASSESSMENT) { // assessing is allowed in the assessment phase only, unless the user is a teacher // providing additional assessment during the evaluation phase if ($this->phase != self::PHASE_EVALUATION or !has_capability('mod/workshop:overridegrades', $this->context, $userid)) { return false; } } $now = time(); $ignoredeadlines = has_capability('mod/workshop:ignoredeadlines', $this->context, $userid); if (!$ignoredeadlines and !empty($this->assessmentstart) and $this->assessmentstart > $now) { // if enabled, assessing is not allowed before the date/time defined in the mod_form return false; } if (!$ignoredeadlines and !empty($this->assessmentend) and $now > $this->assessmentend) { // if enabled, assessing is not allowed after the date/time defined in the mod_form return false; } // here we go, assessing is allowed return true; } /** * Are reviewers allowed to create/edit their assessments of the example submissions? * * Returns null if example submissions are not enabled in this workshop. Otherwise returns * true or false. Note this does not check other conditions like the number of already * assessed examples, examples mode etc. * * @return null|bool */ public function assessing_examples_allowed() { if (empty($this->useexamples)) { return null; } if (self::EXAMPLES_VOLUNTARY == $this->examplesmode) { return true; } if (self::EXAMPLES_BEFORE_SUBMISSION == $this->examplesmode and self::PHASE_SUBMISSION == $this->phase) { return true; } if (self::EXAMPLES_BEFORE_ASSESSMENT == $this->examplesmode and self::PHASE_ASSESSMENT == $this->phase) { return true; } return false; } /** * Are the peer-reviews available to the authors? * * @return bool */ public function assessments_available() { return $this->phase == self::PHASE_CLOSED; } /** * Switch to a new workshop phase * * Modifies the underlying database record. You should terminate the script shortly after calling this. * * @param int $newphase new phase code * @return bool true if success, false otherwise */ public function switch_phase($newphase) { global $DB; $known = $this->available_phases_list(); if (!isset($known[$newphase])) { return false; } if (self::PHASE_CLOSED == $newphase) { // push the grades into the gradebook $workshop = new stdclass(); foreach ($this as $property => $value) { $workshop->{$property} = $value; } $workshop->course = $this->course->id; $workshop->cmidnumber = $this->cm->id; $workshop->modname = 'workshop'; workshop_update_grades($workshop); } $DB->set_field('workshop', 'phase', $newphase, array('id' => $this->id)); $this->phase = $newphase; $eventdata = array( 'objectid' => $this->id, 'context' => $this->context, 'other' => array( 'workshopphase' => $this->phase ) ); $event = \mod_workshop\event\phase_switched::create($eventdata); $event->trigger(); return true; } /** * Saves a raw grade for submission as calculated from the assessment form fields * * @param array $assessmentid assessment record id, must exists * @param mixed $grade raw percentual grade from 0.00000 to 100.00000 * @return false|float the saved grade */ public function set_peer_grade($assessmentid, $grade) { global $DB; if (is_null($grade)) { return false; } $data = new stdclass(); $data->id = $assessmentid; $data->grade = $grade; $data->timemodified = time(); $DB->update_record('workshop_assessments', $data); return $grade; } /** * Prepares data object with all workshop grades to be rendered * * @param int $userid the user we are preparing the report for * @param int $groupid if non-zero, prepare the report for the given group only * @param int $page the current page (for the pagination) * @param int $perpage participants per page (for the pagination) * @param string $sortby lastname|firstname|submissiontitle|submissiongrade|gradinggrade * @param string $sorthow ASC|DESC * @return stdclass data for the renderer */ public function prepare_grading_report_data($userid, $groupid, $page, $perpage, $sortby, $sorthow) { global $DB; $canviewall = has_capability('mod/workshop:viewallassessments', $this->context, $userid); $isparticipant = $this->is_participant($userid); if (!$canviewall and !$isparticipant) { // who the hell is this? return array(); } if (!in_array($sortby, array('lastname', 'firstname', 'submissiontitle', 'submissionmodified', 'submissiongrade', 'gradinggrade'))) { $sortby = 'lastname'; } if (!($sorthow === 'ASC' or $sorthow === 'DESC')) { $sorthow = 'ASC'; } // get the list of user ids to be displayed if ($canviewall) { $participants = $this->get_participants(false, $groupid); } else { // this is an ordinary workshop participant (aka student) - display the report just for him/her $participants = array($userid => (object)array('id' => $userid)); } // we will need to know the number of all records later for the pagination purposes $numofparticipants = count($participants); if ($numofparticipants > 0) { // load all fields which can be used for sorting and paginate the records list($participantids, $params) = $DB->get_in_or_equal(array_keys($participants), SQL_PARAMS_NAMED); $params['workshopid1'] = $this->id; $params['workshopid2'] = $this->id; $sqlsort = array(); $sqlsortfields = array($sortby => $sorthow) + array('lastname' => 'ASC', 'firstname' => 'ASC', 'u.id' => 'ASC'); foreach ($sqlsortfields as $sqlsortfieldname => $sqlsortfieldhow) { $sqlsort[] = $sqlsortfieldname . ' ' . $sqlsortfieldhow; } $sqlsort = implode(',', $sqlsort); $userfieldsapi = \core_user\fields::for_userpic(); $picturefields = $userfieldsapi->get_sql('u', false, '', 'userid', false)->selects; $sql = "SELECT $picturefields, s.title AS submissiontitle, s.timemodified AS submissionmodified, s.grade AS submissiongrade, ag.gradinggrade FROM {user} u LEFT JOIN {workshop_submissions} s ON (s.authorid = u.id AND s.workshopid = :workshopid1 AND s.example = 0) LEFT JOIN {workshop_aggregations} ag ON (ag.userid = u.id AND ag.workshopid = :workshopid2) WHERE u.id $participantids ORDER BY $sqlsort"; $participants = $DB->get_records_sql($sql, $params, $page * $perpage, $perpage); } else { $participants = array(); } // this will hold the information needed to display user names and pictures $userinfo = array(); // get the user details for all participants to display $additionalnames = \core_user\fields::get_name_fields(); foreach ($participants as $participant) { if (!isset($userinfo[$participant->userid])) { $userinfo[$participant->userid] = new stdclass(); $userinfo[$participant->userid]->id = $participant->userid; $userinfo[$participant->userid]->picture = $participant->picture; $userinfo[$participant->userid]->imagealt = $participant->imagealt; $userinfo[$participant->userid]->email = $participant->email; foreach ($additionalnames as $addname) { $userinfo[$participant->userid]->$addname = $participant->$addname; } } } // load the submissions details $submissions = $this->get_submissions(array_keys($participants)); // get the user details for all moderators (teachers) that have overridden a submission grade foreach ($submissions as $submission) { if (!isset($userinfo[$submission->gradeoverby])) { $userinfo[$submission->gradeoverby] = new stdclass(); $userinfo[$submission->gradeoverby]->id = $submission->gradeoverby; $userinfo[$submission->gradeoverby]->picture = $submission->overpicture; $userinfo[$submission->gradeoverby]->imagealt = $submission->overimagealt; $userinfo[$submission->gradeoverby]->email = $submission->overemail; foreach ($additionalnames as $addname) { $temp = 'over' . $addname; $userinfo[$submission->gradeoverby]->$addname = $submission->$temp; } } } // get the user details for all reviewers of the displayed participants $reviewers = array(); if ($submissions) { list($submissionids, $params) = $DB->get_in_or_equal(array_keys($submissions), SQL_PARAMS_NAMED); list($sort, $sortparams) = users_order_by_sql('r'); $userfieldsapi = \core_user\fields::for_userpic(); $picturefields = $userfieldsapi->get_sql('r', false, '', 'reviewerid', false)->selects; $sql = "SELECT a.id AS assessmentid, a.submissionid, a.grade, a.gradinggrade, a.gradinggradeover, a.weight, $picturefields, s.id AS submissionid, s.authorid FROM {workshop_assessments} a JOIN {user} r ON (a.reviewerid = r.id) JOIN {workshop_submissions} s ON (a.submissionid = s.id AND s.example = 0) WHERE a.submissionid $submissionids ORDER BY a.weight DESC, $sort"; $reviewers = $DB->get_records_sql($sql, array_merge($params, $sortparams)); foreach ($reviewers as $reviewer) { if (!isset($userinfo[$reviewer->reviewerid])) { $userinfo[$reviewer->reviewerid] = new stdclass(); $userinfo[$reviewer->reviewerid]->id = $reviewer->reviewerid; $userinfo[$reviewer->reviewerid]->picture = $reviewer->picture; $userinfo[$reviewer->reviewerid]->imagealt = $reviewer->imagealt; $userinfo[$reviewer->reviewerid]->email = $reviewer->email; foreach ($additionalnames as $addname) { $userinfo[$reviewer->reviewerid]->$addname = $reviewer->$addname; } } } } // get the user details for all reviewees of the displayed participants $reviewees = array(); if ($participants) { list($participantids, $params) = $DB->get_in_or_equal(array_keys($participants), SQL_PARAMS_NAMED); list($sort, $sortparams) = users_order_by_sql('e'); $params['workshopid'] = $this->id; $userfieldsapi = \core_user\fields::for_userpic(); $picturefields = $userfieldsapi->get_sql('e', false, '', 'authorid', false)->selects; $sql = "SELECT a.id AS assessmentid, a.submissionid, a.grade, a.gradinggrade, a.gradinggradeover, a.reviewerid, a.weight, s.id AS submissionid, $picturefields FROM {user} u JOIN {workshop_assessments} a ON (a.reviewerid = u.id) JOIN {workshop_submissions} s ON (a.submissionid = s.id AND s.example = 0) JOIN {user} e ON (s.authorid = e.id) WHERE u.id $participantids AND s.workshopid = :workshopid ORDER BY a.weight DESC, $sort"; $reviewees = $DB->get_records_sql($sql, array_merge($params, $sortparams)); foreach ($reviewees as $reviewee) { if (!isset($userinfo[$reviewee->authorid])) { $userinfo[$reviewee->authorid] = new stdclass(); $userinfo[$reviewee->authorid]->id = $reviewee->authorid; $userinfo[$reviewee->authorid]->picture = $reviewee->picture; $userinfo[$reviewee->authorid]->imagealt = $reviewee->imagealt; $userinfo[$reviewee->authorid]->email = $reviewee->email; foreach ($additionalnames as $addname) { $userinfo[$reviewee->authorid]->$addname = $reviewee->$addname; } } } } // finally populate the object to be rendered $grades = $participants; foreach ($participants as $participant) { // set up default (null) values $grades[$participant->userid]->submissionid = null; $grades[$participant->userid]->submissiontitle = null; $grades[$participant->userid]->submissiongrade = null; $grades[$participant->userid]->submissiongradeover = null; $grades[$participant->userid]->submissiongradeoverby = null; $grades[$participant->userid]->submissionpublished = null; $grades[$participant->userid]->reviewedby = array(); $grades[$participant->userid]->reviewerof = array(); } unset($participants); unset($participant); foreach ($submissions as $submission) { $grades[$submission->authorid]->submissionid = $submission->id; $grades[$submission->authorid]->submissiontitle = $submission->title; $grades[$submission->authorid]->submissiongrade = $this->real_grade($submission->grade); $grades[$submission->authorid]->submissiongradeover = $this->real_grade($submission->gradeover); $grades[$submission->authorid]->submissiongradeoverby = $submission->gradeoverby; $grades[$submission->authorid]->submissionpublished = $submission->published; } unset($submissions); unset($submission); foreach($reviewers as $reviewer) { $info = new stdclass(); $info->userid = $reviewer->reviewerid; $info->assessmentid = $reviewer->assessmentid; $info->submissionid = $reviewer->submissionid; $info->grade = $this->real_grade($reviewer->grade); $info->gradinggrade = $this->real_grading_grade($reviewer->gradinggrade); $info->gradinggradeover = $this->real_grading_grade($reviewer->gradinggradeover); $info->weight = $reviewer->weight; $grades[$reviewer->authorid]->reviewedby[$reviewer->reviewerid] = $info; } unset($reviewers); unset($reviewer); foreach($reviewees as $reviewee) { $info = new stdclass(); $info->userid = $reviewee->authorid; $info->assessmentid = $reviewee->assessmentid; $info->submissionid = $reviewee->submissionid; $info->grade = $this->real_grade($reviewee->grade); $info->gradinggrade = $this->real_grading_grade($reviewee->gradinggrade); $info->gradinggradeover = $this->real_grading_grade($reviewee->gradinggradeover); $info->weight = $reviewee->weight; $grades[$reviewee->reviewerid]->reviewerof[$reviewee->authorid] = $info; } unset($reviewees); unset($reviewee); foreach ($grades as $grade) { $grade->gradinggrade = $this->real_grading_grade($grade->gradinggrade); } $data = new stdclass(); $data->grades = $grades; $data->userinfo = $userinfo; $data->totalcount = $numofparticipants; $data->maxgrade = $this->real_grade(100); $data->maxgradinggrade = $this->real_grading_grade(100); return $data; } /** * Calculates the real value of a grade * * @param float $value percentual value from 0 to 100 * @param float $max the maximal grade * @return string */ public function real_grade_value($value, $max) { $localized = true; if (is_null($value) or $value === '') { return null; } elseif ($max == 0) { return 0; } else { return format_float($max * $value / 100, $this->gradedecimals, $localized); } } /** * Calculates the raw (percentual) value from a real grade * * This is used in cases when a user wants to give a grade such as 12 of 20 and we need to save * this value in a raw percentual form into DB * @param float $value given grade * @param float $max the maximal grade * @return float suitable to be stored as numeric(10,5) */ public function raw_grade_value($value, $max) { if (is_null($value) or $value === '') { return null; } if ($max == 0 or $value < 0) { return 0; } $p = $value / $max * 100; if ($p > 100) { return $max; } return grade_floatval($p); } /** * Calculates the real value of grade for submission * * @param float $value percentual value from 0 to 100 * @return string */ public function real_grade($value) { return $this->real_grade_value($value, $this->grade); } /** * Calculates the real value of grade for assessment * * @param float $value percentual value from 0 to 100 * @return string */ public function real_grading_grade($value) { return $this->real_grade_value($value, $this->gradinggrade); } /** * Sets the given grades and received grading grades to null * * This does not clear the information about how the peers filled the assessment forms, but * clears the calculated grades in workshop_assessments. Therefore reviewers have to re-assess * the allocated submissions. * * @return void */ public function clear_assessments() { global $DB; $submissions = $this->get_submissions(); if (empty($submissions)) { // no money, no love return; } $submissions = array_keys($submissions); list($sql, $params) = $DB->get_in_or_equal($submissions, SQL_PARAMS_NAMED); $sql = "submissionid $sql"; $DB->set_field_select('workshop_assessments', 'grade', null, $sql, $params); $DB->set_field_select('workshop_assessments', 'gradinggrade', null, $sql, $params); } /** * Sets the grades for submission to null * * @param null|int|array $restrict If null, update all authors, otherwise update just grades for the given author(s) * @return void */ public function clear_submission_grades($restrict=null) { global $DB; $sql = "workshopid = :workshopid AND example = 0"; $params = array('workshopid' => $this->id); if (is_null($restrict)) { // update all users - no more conditions } elseif (!empty($restrict)) { list($usql, $uparams) = $DB->get_in_or_equal($restrict, SQL_PARAMS_NAMED); $sql .= " AND authorid $usql"; $params = array_merge($params, $uparams); } else { throw new coding_exception('Empty value is not a valid parameter here'); } $DB->set_field_select('workshop_submissions', 'grade', null, $sql, $params); } /** * Calculates grades for submission for the given participant(s) and updates it in the database * * @param null|int|array $restrict If null, update all authors, otherwise update just grades for the given author(s) * @return void */ public function aggregate_submission_grades($restrict=null) { global $DB; // fetch a recordset with all assessments to process $sql = 'SELECT s.id AS submissionid, s.grade AS submissiongrade, a.weight, a.grade FROM {workshop_submissions} s LEFT JOIN {workshop_assessments} a ON (a.submissionid = s.id) WHERE s.example=0 AND s.workshopid=:workshopid'; // to be cont. $params = array('workshopid' => $this->id); if (is_null($restrict)) { // update all users - no more conditions } elseif (!empty($restrict)) { list($usql, $uparams) = $DB->get_in_or_equal($restrict, SQL_PARAMS_NAMED); $sql .= " AND s.authorid $usql"; $params = array_merge($params, $uparams); } else { throw new coding_exception('Empty value is not a valid parameter here'); } $sql .= ' ORDER BY s.id'; // this is important for bulk processing $rs = $DB->get_recordset_sql($sql, $params); $batch = array(); // will contain a set of all assessments of a single submission $previous = null; // a previous record in the recordset foreach ($rs as $current) { if (is_null($previous)) { // we are processing the very first record in the recordset $previous = $current; } if ($current->submissionid == $previous->submissionid) { // we are still processing the current submission $batch[] = $current; } else { // process all the assessments of a sigle submission $this->aggregate_submission_grades_process($batch); // and then start to process another submission $batch = array($current); $previous = $current; } } // do not forget to process the last batch! $this->aggregate_submission_grades_process($batch); $rs->close(); } /** * Sets the aggregated grades for assessment to null * * @param null|int|array $restrict If null, update all reviewers, otherwise update just grades for the given reviewer(s) * @return void */ public function clear_grading_grades($restrict=null) { global $DB; $sql = "workshopid = :workshopid"; $params = array('workshopid' => $this->id); if (is_null($restrict)) { // update all users - no more conditions } elseif (!empty($restrict)) { list($usql, $uparams) = $DB->get_in_or_equal($restrict, SQL_PARAMS_NAMED); $sql .= " AND userid $usql"; $params = array_merge($params, $uparams); } else { throw new coding_exception('Empty value is not a valid parameter here'); } $DB->set_field_select('workshop_aggregations', 'gradinggrade', null, $sql, $params); } /** * Calculates grades for assessment for the given participant(s) * * Grade for assessment is calculated as a simple mean of all grading grades calculated by the grading evaluator. * The assessment weight is not taken into account here. * * @param null|int|array $restrict If null, update all reviewers, otherwise update just grades for the given reviewer(s) * @return void */ public function aggregate_grading_grades($restrict=null) { global $DB; // fetch a recordset with all assessments to process $sql = 'SELECT a.reviewerid, a.gradinggrade, a.gradinggradeover, ag.id AS aggregationid, ag.gradinggrade AS aggregatedgrade FROM {workshop_assessments} a INNER JOIN {workshop_submissions} s ON (a.submissionid = s.id) LEFT JOIN {workshop_aggregations} ag ON (ag.userid = a.reviewerid AND ag.workshopid = s.workshopid) WHERE s.example=0 AND s.workshopid=:workshopid'; // to be cont. $params = array('workshopid' => $this->id); if (is_null($restrict)) { // update all users - no more conditions } elseif (!empty($restrict)) { list($usql, $uparams) = $DB->get_in_or_equal($restrict, SQL_PARAMS_NAMED); $sql .= " AND a.reviewerid $usql"; $params = array_merge($params, $uparams); } else { throw new coding_exception('Empty value is not a valid parameter here'); } $sql .= ' ORDER BY a.reviewerid'; // this is important for bulk processing $rs = $DB->get_recordset_sql($sql, $params); $batch = array(); // will contain a set of all assessments of a single submission $previous = null; // a previous record in the recordset foreach ($rs as $current) { if (is_null($previous)) { // we are processing the very first record in the recordset $previous = $current; } if ($current->reviewerid == $previous->reviewerid) { // we are still processing the current reviewer $batch[] = $current; } else { // process all the assessments of a sigle submission $this->aggregate_grading_grades_process($batch); // and then start to process another reviewer $batch = array($current); $previous = $current; } } // do not forget to process the last batch! $this->aggregate_grading_grades_process($batch); $rs->close(); } /** * Returns the mform the teachers use to put a feedback for the reviewer * * @param mixed moodle_url|null $actionurl * @param stdClass $assessment * @param array $options editable, editableweight, overridablegradinggrade * @return workshop_feedbackreviewer_form */ public function get_feedbackreviewer_form($actionurl, stdclass $assessment, $options=array()) { global $CFG; require_once(__DIR__ . '/feedbackreviewer_form.php'); $current = new stdclass(); $current->asid = $assessment->id; $current->weight = $assessment->weight; $current->gradinggrade = $this->real_grading_grade($assessment->gradinggrade); $current->gradinggradeover = $this->real_grading_grade($assessment->gradinggradeover); $current->feedbackreviewer = $assessment->feedbackreviewer; $current->feedbackreviewerformat = $assessment->feedbackreviewerformat; if (is_null($current->gradinggrade)) { $current->gradinggrade = get_string('nullgrade', 'workshop'); } if (!isset($options['editable'])) { $editable = true; // by default } else { $editable = (bool)$options['editable']; } // prepare wysiwyg editor $current = file_prepare_standard_editor($current, 'feedbackreviewer', array()); return new workshop_feedbackreviewer_form($actionurl, array('workshop' => $this, 'current' => $current, 'editoropts' => array(), 'options' => $options), 'post', '', null, $editable); } /** * Returns the mform the teachers use to put a feedback for the author on their submission * * @mixed moodle_url|null $actionurl * @param stdClass $submission * @param array $options editable * @return workshop_feedbackauthor_form */ public function get_feedbackauthor_form($actionurl, stdclass $submission, $options=array()) { global $CFG; require_once(__DIR__ . '/feedbackauthor_form.php'); $current = new stdclass(); $current->submissionid = $submission->id; $current->published = $submission->published; $current->grade = $this->real_grade($submission->grade); $current->gradeover = $this->real_grade($submission->gradeover); $current->feedbackauthor = $submission->feedbackauthor; $current->feedbackauthorformat = $submission->feedbackauthorformat; if (is_null($current->grade)) { $current->grade = get_string('nullgrade', 'workshop'); } if (!isset($options['editable'])) { $editable = true; // by default } else { $editable = (bool)$options['editable']; } // prepare wysiwyg editor $current = file_prepare_standard_editor($current, 'feedbackauthor', array()); return new workshop_feedbackauthor_form($actionurl, array('workshop' => $this, 'current' => $current, 'editoropts' => array(), 'options' => $options), 'post', '', null, $editable); } /** * Returns the information about the user's grades as they are stored in the gradebook * * The submission grade is returned for users with the capability mod/workshop:submit and the * assessment grade is returned for users with the capability mod/workshop:peerassess. Unless the * user has the capability to view hidden grades, grades must be visible to be returned. Null * grades are not returned. If none grade is to be returned, this method returns false. * * @param int $userid the user's id * @return workshop_final_grades|false */ public function get_gradebook_grades($userid) { global $CFG; require_once($CFG->libdir.'/gradelib.php'); if (empty($userid)) { throw new coding_exception('User id expected, empty value given.'); } // Read data via the Gradebook API $gradebook = grade_get_grades($this->course->id, 'mod', 'workshop', $this->id, $userid); $grades = new workshop_final_grades(); if (has_capability('mod/workshop:submit', $this->context, $userid)) { if (!empty($gradebook->items[0]->grades)) { $submissiongrade = reset($gradebook->items[0]->grades); if (!is_null($submissiongrade->grade)) { if (!$submissiongrade->hidden or has_capability('moodle/grade:viewhidden', $this->context, $userid)) { $grades->submissiongrade = $submissiongrade; } } } } if (has_capability('mod/workshop:peerassess', $this->context, $userid)) { if (!empty($gradebook->items[1]->grades)) { $assessmentgrade = reset($gradebook->items[1]->grades); if (!is_null($assessmentgrade->grade)) { if (!$assessmentgrade->hidden or has_capability('moodle/grade:viewhidden', $this->context, $userid)) { $grades->assessmentgrade = $assessmentgrade; } } } } if (!is_null($grades->submissiongrade) or !is_null($grades->assessmentgrade)) { return $grades; } return false; } /** * Return the editor options for the submission content field. * * @return array */ public function submission_content_options() { global $CFG; require_once($CFG->dirroot.'/repository/lib.php'); return array( 'trusttext' => true, 'subdirs' => false, 'maxfiles' => $this->nattachments, 'maxbytes' => $this->maxbytes, 'context' => $this->context, 'return_types' => FILE_INTERNAL | FILE_EXTERNAL, ); } /** * Return the filemanager options for the submission attachments field. * * @return array */ public function submission_attachment_options() { global $CFG; require_once($CFG->dirroot.'/repository/lib.php'); $options = array( 'subdirs' => true, 'maxfiles' => $this->nattachments, 'maxbytes' => $this->maxbytes, 'return_types' => FILE_INTERNAL | FILE_CONTROLLED_LINK, ); $filetypesutil = new \core_form\filetypes_util(); $options['accepted_types'] = $filetypesutil->normalize_file_types($this->submissionfiletypes); return $options; } /** * Return the editor options for the overall feedback for the author. * * @return array */ public function overall_feedback_content_options() { global $CFG; require_once($CFG->dirroot.'/repository/lib.php'); return array( 'subdirs' => 0, 'maxbytes' => $this->overallfeedbackmaxbytes, 'maxfiles' => $this->overallfeedbackfiles, 'changeformat' => 1, 'context' => $this->context, 'return_types' => FILE_INTERNAL, ); } /** * Return the filemanager options for the overall feedback for the author. * * @return array */ public function overall_feedback_attachment_options() { global $CFG; require_once($CFG->dirroot.'/repository/lib.php'); $options = array( 'subdirs' => 1, 'maxbytes' => $this->overallfeedbackmaxbytes, 'maxfiles' => $this->overallfeedbackfiles, 'return_types' => FILE_INTERNAL | FILE_CONTROLLED_LINK, ); $filetypesutil = new \core_form\filetypes_util(); $options['accepted_types'] = $filetypesutil->normalize_file_types($this->overallfeedbackfiletypes); return $options; } /** * Performs the reset of this workshop instance. * * @param stdClass $data The actual course reset settings. * @return array List of results, each being array[(string)component, (string)item, (string)error] */ public function reset_userdata(stdClass $data) { $componentstr = get_string('pluginname', 'workshop').': '.format_string($this->name); $status = array(); if (!empty($data->reset_workshop_assessments) or !empty($data->reset_workshop_submissions)) { // Reset all data related to assessments, including assessments of // example submissions. $result = $this->reset_userdata_assessments($data); if ($result === true) { $status[] = array( 'component' => $componentstr, 'item' => get_string('resetassessments', 'mod_workshop'), 'error' => false, ); } else { $status[] = array( 'component' => $componentstr, 'item' => get_string('resetassessments', 'mod_workshop'), 'error' => $result, ); } } if (!empty($data->reset_workshop_submissions)) { // Reset all remaining data related to submissions. $result = $this->reset_userdata_submissions($data); if ($result === true) { $status[] = array( 'component' => $componentstr, 'item' => get_string('resetsubmissions', 'mod_workshop'), 'error' => false, ); } else { $status[] = array( 'component' => $componentstr, 'item' => get_string('resetsubmissions', 'mod_workshop'), 'error' => $result, ); } } if (!empty($data->reset_workshop_phase)) { // Do not use the {@link workshop::switch_phase()} here, we do not // want to trigger events. $this->reset_phase(); $status[] = array( 'component' => $componentstr, 'item' => get_string('resetsubmissions', 'mod_workshop'), 'error' => false, ); } return $status; } /** * Check if the current user can access the other user's group. * * This is typically used for teacher roles that have permissions like * 'view all submissions'. Even with such a permission granted, we have to * check the workshop activity group mode. * * If the workshop is not in a group mode, or if it is in the visible group * mode, this method returns true. This is consistent with how the * {@link groups_get_activity_allowed_groups()} behaves. * * If the workshop is in a separate group mode, the current user has to * have the 'access all groups' permission, or share at least one * accessible group with the other user. * * @param int $otheruserid The ID of the other user, e.g. the author of a submission. * @return bool False if the current user cannot access the other user's group. */ public function check_group_membership($otheruserid) { global $USER; if (groups_get_activity_groupmode($this->cm) != SEPARATEGROUPS) { // The workshop is not in a group mode, or it is in a visible group mode. return true; } else if (has_capability('moodle/site:accessallgroups', $this->context)) { // The current user can access all groups. return true; } else { $thisusersgroups = groups_get_all_groups($this->course->id, $USER->id, $this->cm->groupingid, 'g.id'); $otherusersgroups = groups_get_all_groups($this->course->id, $otheruserid, $this->cm->groupingid, 'g.id'); $commongroups = array_intersect_key($thisusersgroups, $otherusersgroups); if (empty($commongroups)) { // The current user has no group common with the other user. return false; } else { // The current user has a group common with the other user. return true; } } } /** * Check whether the given user has assessed all his required examples before submission. * * @param int $userid the user to check * @return bool false if there are examples missing assessment, true otherwise. * @since Moodle 3.4 */ public function check_examples_assessed_before_submission($userid) { if ($this->useexamples and $this->examplesmode == self::EXAMPLES_BEFORE_SUBMISSION and !has_capability('mod/workshop:manageexamples', $this->context)) { // Check that all required examples have been assessed by the user. $examples = $this->get_examples_for_reviewer($userid); foreach ($examples as $exampleid => $example) { if (is_null($example->assessmentid)) { $examples[$exampleid]->assessmentid = $this->add_allocation($example, $userid, 0); } if (is_null($example->grade)) { return false; } } } return true; } /** * Check that all required examples have been assessed by the given user. * * @param stdClass $userid the user (reviewer) to check * @return mixed bool|state false and notice code if there are examples missing assessment, true otherwise. * @since Moodle 3.4 */ public function check_examples_assessed_before_assessment($userid) { if ($this->useexamples and $this->examplesmode == self::EXAMPLES_BEFORE_ASSESSMENT and !has_capability('mod/workshop:manageexamples', $this->context)) { // The reviewer must have submitted their own submission. $reviewersubmission = $this->get_submission_by_author($userid); if (!$reviewersubmission) { // No money, no love. return array(false, 'exampleneedsubmission'); } else { $examples = $this->get_examples_for_reviewer($userid); foreach ($examples as $exampleid => $example) { if (is_null($example->grade)) { return array(false, 'exampleneedassessed'); } } } } return array(true, null); } /** * Trigger module viewed event and set the module viewed for completion. * * @since Moodle 3.4 */ public function set_module_viewed() { global $CFG; require_once($CFG->libdir . '/completionlib.php'); // Mark viewed. $completion = new completion_info($this->course); $completion->set_module_viewed($this->cm); $eventdata = array(); $eventdata['objectid'] = $this->id; $eventdata['context'] = $this->context; // Trigger module viewed event. $event = \mod_workshop\event\course_module_viewed::create($eventdata); $event->add_record_snapshot('course', $this->course); $event->add_record_snapshot('workshop', $this->dbrecord); $event->add_record_snapshot('course_modules', $this->cm); $event->trigger(); } /** * Validates the submission form or WS data. * * @param array $data the data to be validated * @return array the validation errors (if any) * @since Moodle 3.4 */ public function validate_submission_data($data) { global $DB, $USER; $errors = array(); if (empty($data['id']) and empty($data['example'])) { // Make sure there is no submission saved meanwhile from another browser window. $sql = "SELECT COUNT(s.id) FROM {workshop_submissions} s JOIN {workshop} w ON (s.workshopid = w.id) JOIN {course_modules} cm ON (w.id = cm.instance) JOIN {modules} m ON (m.name = 'workshop' AND m.id = cm.module) WHERE cm.id = ? AND s.authorid = ? AND s.example = 0"; if ($DB->count_records_sql($sql, array($data['cmid'], $USER->id))) { $errors['title'] = get_string('err_multiplesubmissions', 'mod_workshop'); } } // Get the workshop record by id or cmid, depending on whether we're creating or editing a submission. if (empty($data['workshopid'])) { $workshop = $DB->get_record_select('workshop', 'id = (SELECT instance FROM {course_modules} WHERE id = ?)', [$data['cmid']]); } else { $workshop = $DB->get_record('workshop', ['id' => $data['workshopid']]); } if (isset($data['attachment_filemanager'])) { $getfiles = file_get_drafarea_files($data['attachment_filemanager']); $attachments = $getfiles->list; } else { $attachments = array(); } if ($workshop->submissiontypefile == WORKSHOP_SUBMISSION_TYPE_REQUIRED) { if (empty($attachments)) { $errors['attachment_filemanager'] = get_string('err_required', 'form'); } } else if ($workshop->submissiontypefile == WORKSHOP_SUBMISSION_TYPE_DISABLED && !empty($data['attachment_filemanager'])) { $errors['attachment_filemanager'] = get_string('submissiontypedisabled', 'mod_workshop'); } if ($workshop->submissiontypetext == WORKSHOP_SUBMISSION_TYPE_REQUIRED && html_is_blank($data['content_editor']['text'])) { $errors['content_editor'] = get_string('err_required', 'form'); } else if ($workshop->submissiontypetext == WORKSHOP_SUBMISSION_TYPE_DISABLED && !empty($data['content_editor']['text'])) { $errors['content_editor'] = get_string('submissiontypedisabled', 'mod_workshop'); } // If neither type is explicitly required, one or the other must be submitted. if ($workshop->submissiontypetext != WORKSHOP_SUBMISSION_TYPE_REQUIRED && $workshop->submissiontypefile != WORKSHOP_SUBMISSION_TYPE_REQUIRED && empty($attachments) && html_is_blank($data['content_editor']['text'])) { $errors['content_editor'] = get_string('submissionrequiredcontent', 'mod_workshop'); $errors['attachment_filemanager'] = get_string('submissionrequiredfile', 'mod_workshop'); } return $errors; } /** * Adds or updates a submission. * * @param stdClass $submission The submissin data (via form or via WS). * @return the new or updated submission id. * @since Moodle 3.4 */ public function edit_submission($submission) { global $USER, $DB; if ($submission->example == 0) { // This was used just for validation, it must be set to zero when dealing with normal submissions. unset($submission->example); } else { throw new coding_exception('Invalid submission form data value: example'); } $timenow = time(); if (is_null($submission->id)) { $submission->workshopid = $this->id; $submission->example = 0; $submission->authorid = $USER->id; $submission->timecreated = $timenow; $submission->feedbackauthorformat = editors_get_preferred_format(); } $submission->timemodified = $timenow; $submission->title = trim($submission->title); $submission->content = ''; // Updated later. $submission->contentformat = FORMAT_HTML; // Updated later. $submission->contenttrust = 0; // Updated later. $submission->late = 0x0; // Bit mask. if (!empty($this->submissionend) and ($this->submissionend < time())) { $submission->late = $submission->late | 0x1; } if ($this->phase == self::PHASE_ASSESSMENT) { $submission->late = $submission->late | 0x2; } // Event information. $params = array( 'context' => $this->context, 'courseid' => $this->course->id, 'other' => array( 'submissiontitle' => $submission->title ) ); $logdata = null; if (is_null($submission->id)) { $submission->id = $DB->insert_record('workshop_submissions', $submission); $params['objectid'] = $submission->id; $event = \mod_workshop\event\submission_created::create($params); $event->trigger(); } else { if (empty($submission->id) or empty($submission->id) or ($submission->id != $submission->id)) { throw new moodle_exception('err_submissionid', 'workshop'); } } $params['objectid'] = $submission->id; // Save and relink embedded images and save attachments. if ($this->submissiontypetext != WORKSHOP_SUBMISSION_TYPE_DISABLED) { $submission = file_postupdate_standard_editor($submission, 'content', $this->submission_content_options(), $this->context, 'mod_workshop', 'submission_content', $submission->id); } $submission = file_postupdate_standard_filemanager($submission, 'attachment', $this->submission_attachment_options(), $this->context, 'mod_workshop', 'submission_attachment', $submission->id); if (empty($submission->attachment)) { // Explicit cast to zero integer. $submission->attachment = 0; } // Store the updated values or re-save the new submission (re-saving needed because URLs are now rewritten). $DB->update_record('workshop_submissions', $submission); $event = \mod_workshop\event\submission_updated::create($params); $event->add_record_snapshot('workshop', $this->dbrecord); $event->trigger(); // Send submitted content for plagiarism detection. $fs = get_file_storage(); $files = $fs->get_area_files($this->context->id, 'mod_workshop', 'submission_attachment', $submission->id); $params['other']['content'] = $submission->content; $params['other']['pathnamehashes'] = array_keys($files); $event = \mod_workshop\event\assessable_uploaded::create($params); $event->trigger(); return $submission->id; } /** * Helper method for validating if the current user can view the given assessment. * * @param stdClass $assessment assessment object * @param stdClass $submission submission object * @return void * @throws moodle_exception * @since Moodle 3.4 */ public function check_view_assessment($assessment, $submission) { global $USER; $isauthor = $submission->authorid == $USER->id; $isreviewer = $assessment->reviewerid == $USER->id; $canviewallassessments = has_capability('mod/workshop:viewallassessments', $this->context); $canviewallsubmissions = has_capability('mod/workshop:viewallsubmissions', $this->context); $canviewallsubmissions = $canviewallsubmissions && $this->check_group_membership($submission->authorid); if (!$isreviewer and !$isauthor and !($canviewallassessments and $canviewallsubmissions)) { throw new \moodle_exception('nopermissions', 'error', $this->view_url(), 'view this assessment'); } if ($isauthor and !$isreviewer and !$canviewallassessments and $this->phase != self::PHASE_CLOSED) { // Authors can see assessments of their work at the end of workshop only. throw new \moodle_exception('nopermissions', 'error', $this->view_url(), 'view assessment of own work before workshop is closed'); } } /** * Helper method for validating if the current user can edit the given assessment. * * @param stdClass $assessment assessment object * @param stdClass $submission submission object * @return void * @throws moodle_exception * @since Moodle 3.4 */ public function check_edit_assessment($assessment, $submission) { global $USER; $this->check_view_assessment($assessment, $submission); // Further checks. $isreviewer = ($USER->id == $assessment->reviewerid); $assessmenteditable = $isreviewer && $this->assessing_allowed($USER->id); if (!$assessmenteditable) { throw new moodle_exception('nopermissions', 'error', '', 'edit assessments'); } list($assessed, $notice) = $this->check_examples_assessed_before_assessment($assessment->reviewerid); if (!$assessed) { throw new moodle_exception($notice, 'mod_workshop'); } } /** * Adds information to an allocated assessment (function used the first time a review is done or when updating an existing one). * * @param stdClass $assessment the assessment * @param stdClass $submission the submission * @param stdClass $data the assessment data to be added or Updated * @param stdClass $strategy the strategy instance * @return float|null Raw percentual grade (0.00000 to 100.00000) for submission * @since Moodle 3.4 */ public function edit_assessment($assessment, $submission, $data, $strategy) { global $DB; $cansetassessmentweight = has_capability('mod/workshop:allocate', $this->context); // Let the grading strategy subplugin save its data. $rawgrade = $strategy->save_assessment($assessment, $data); // Store the data managed by the workshop core. $coredata = (object)array('id' => $assessment->id); if (isset($data->feedbackauthor_editor)) { $coredata->feedbackauthor_editor = $data->feedbackauthor_editor; $coredata = file_postupdate_standard_editor($coredata, 'feedbackauthor', $this->overall_feedback_content_options(), $this->context, 'mod_workshop', 'overallfeedback_content', $assessment->id); unset($coredata->feedbackauthor_editor); } if (isset($data->feedbackauthorattachment_filemanager)) { $coredata->feedbackauthorattachment_filemanager = $data->feedbackauthorattachment_filemanager; $coredata = file_postupdate_standard_filemanager($coredata, 'feedbackauthorattachment', $this->overall_feedback_attachment_options(), $this->context, 'mod_workshop', 'overallfeedback_attachment', $assessment->id); unset($coredata->feedbackauthorattachment_filemanager); if (empty($coredata->feedbackauthorattachment)) { $coredata->feedbackauthorattachment = 0; } } if (isset($data->weight) and $cansetassessmentweight) { $coredata->weight = $data->weight; } // Update the assessment data if there is something other than just the 'id'. if (count((array)$coredata) > 1 ) { $DB->update_record('workshop_assessments', $coredata); $params = array( 'relateduserid' => $submission->authorid, 'objectid' => $assessment->id, 'context' => $this->context, 'other' => array( 'workshopid' => $this->id, 'submissionid' => $assessment->submissionid ) ); if (is_null($assessment->grade)) { // All workshop_assessments are created when allocations are made. The create event is of more use located here. $event = \mod_workshop\event\submission_assessed::create($params); $event->trigger(); } else { $params['other']['grade'] = $assessment->grade; $event = \mod_workshop\event\submission_reassessed::create($params); $event->trigger(); } } return $rawgrade; } /** * Evaluates an assessment. * * @param stdClass $assessment the assessment * @param stdClass $data the assessment data to be updated * @param bool $cansetassessmentweight whether the user can change the assessment weight * @param bool $canoverridegrades whether the user can override the assessment grades * @return void * @since Moodle 3.4 */ public function evaluate_assessment($assessment, $data, $cansetassessmentweight, $canoverridegrades) { global $DB, $USER; $data = file_postupdate_standard_editor($data, 'feedbackreviewer', array(), $this->context); $record = new stdclass(); $record->id = $assessment->id; if ($cansetassessmentweight) { $record->weight = $data->weight; } if ($canoverridegrades) { $record->gradinggradeover = $this->raw_grade_value($data->gradinggradeover, $this->gradinggrade); $record->gradinggradeoverby = $USER->id; $record->feedbackreviewer = $data->feedbackreviewer; $record->feedbackreviewerformat = $data->feedbackreviewerformat; } $DB->update_record('workshop_assessments', $record); } /** * Trigger submission viewed event. * * @param stdClass $submission submission object * @since Moodle 3.4 */ public function set_submission_viewed($submission) { $params = array( 'objectid' => $submission->id, 'context' => $this->context, 'courseid' => $this->course->id, 'relateduserid' => $submission->authorid, 'other' => array( 'workshopid' => $this->id ) ); $event = \mod_workshop\event\submission_viewed::create($params); $event->trigger(); } /** * Evaluates a submission. * * @param stdClass $submission the submission * @param stdClass $data the submission data to be updated * @param bool $canpublish whether the user can publish the submission * @param bool $canoverride whether the user can override the submission grade * @return void * @since Moodle 3.4 */ public function evaluate_submission($submission, $data, $canpublish, $canoverride) { global $DB, $USER; $data = file_postupdate_standard_editor($data, 'feedbackauthor', array(), $this->context); $record = new stdclass(); $record->id = $submission->id; if ($canoverride) { $record->gradeover = $this->raw_grade_value($data->gradeover, $this->grade); $record->gradeoverby = $USER->id; $record->feedbackauthor = $data->feedbackauthor; $record->feedbackauthorformat = $data->feedbackauthorformat; } if ($canpublish) { $record->published = !empty($data->published); } $DB->update_record('workshop_submissions', $record); } /** * Get the initial first name. * * @return string|null initial of first name we are currently filtering by. */ public function get_initial_first(): ?string { if (empty($this->initialbarprefs['i_first'])) { return null; } return $this->initialbarprefs['i_first']; } /** * Get the initial last name. * * @return string|null initial of last name we are currently filtering by. */ public function get_initial_last(): ?string { if (empty($this->initialbarprefs['i_last'])) { return null; } return $this->initialbarprefs['i_last']; } /** * Init method for initial bars. * @return void */ public function init_initial_bar(): void { global $SESSION; if ($this->phase === self::PHASE_SETUP) { return; } $ifirst = optional_param('ifirst', null, PARAM_NOTAGS); $ilast = optional_param('ilast', null, PARAM_NOTAGS); if (empty($SESSION->mod_workshop->initialbarprefs['id-'.$this->context->id])) { $SESSION->mod_workshop = new stdClass(); $SESSION->mod_workshop->initialbarprefs['id-'.$this->context->id] = []; } if (!empty($SESSION->mod_workshop->initialbarprefs['id-'.$this->context->id]['i_first'])) { $this->initialbarprefs['i_first'] = $SESSION->mod_workshop->initialbarprefs['id-'.$this->context->id]['i_first']; } if (!empty($SESSION->mod_workshop->initialbarprefs['id-'.$this->context->id]['i_last'])) { $this->initialbarprefs['i_last'] = $SESSION->mod_workshop->initialbarprefs['id-'.$this->context->id]['i_last']; } if (!is_null($ifirst)) { $this->initialbarprefs['i_first'] = $ifirst; $SESSION->mod_workshop->initialbarprefs['id-'.$this->context->id]['i_first'] = $ifirst; } if (!is_null($ilast)) { $this->initialbarprefs['i_last'] = $ilast; $SESSION->mod_workshop->initialbarprefs['id-'.$this->context->id]['i_last'] = $ilast; } } //////////////////////////////////////////////////////////////////////////////// // Internal methods (implementation details) // //////////////////////////////////////////////////////////////////////////////// /** * Given an array of all assessments of a single submission, calculates the final grade for this submission * * This calculates the weighted mean of the passed assessment grades. If, however, the submission grade * was overridden by a teacher, the gradeover value is returned and the rest of grades are ignored. * * @param array $assessments of stdclass(->submissionid ->submissiongrade ->gradeover ->weight ->grade) * @return void */ protected function aggregate_submission_grades_process(array $assessments) { global $DB; $submissionid = null; // the id of the submission being processed $current = null; // the grade currently saved in database $finalgrade = null; // the new grade to be calculated $sumgrades = 0; $sumweights = 0; foreach ($assessments as $assessment) { if (is_null($submissionid)) { // the id is the same in all records, fetch it during the first loop cycle $submissionid = $assessment->submissionid; } if (is_null($current)) { // the currently saved grade is the same in all records, fetch it during the first loop cycle $current = $assessment->submissiongrade; } if (is_null($assessment->grade)) { // this was not assessed yet continue; } if ($assessment->weight == 0) { // this does not influence the calculation continue; } $sumgrades += $assessment->grade * $assessment->weight; $sumweights += $assessment->weight; } if ($sumweights > 0 and is_null($finalgrade)) { $finalgrade = grade_floatval($sumgrades / $sumweights); } // check if the new final grade differs from the one stored in the database if (grade_floats_different($finalgrade, $current)) { // we need to save new calculation into the database $record = new stdclass(); $record->id = $submissionid; $record->grade = $finalgrade; $record->timegraded = time(); $DB->update_record('workshop_submissions', $record); } } /** * Given an array of all assessments done by a single reviewer, calculates the final grading grade * * This calculates the simple mean of the passed grading grades. If, however, the grading grade * was overridden by a teacher, the gradinggradeover value is returned and the rest of grades are ignored. * * @param array $assessments of stdclass(->reviewerid ->gradinggrade ->gradinggradeover ->aggregationid ->aggregatedgrade) * @param null|int $timegraded explicit timestamp of the aggregation, defaults to the current time * @return void */ protected function aggregate_grading_grades_process(array $assessments, $timegraded = null) { global $DB; $reviewerid = null; // the id of the reviewer being processed $current = null; // the gradinggrade currently saved in database $finalgrade = null; // the new grade to be calculated $agid = null; // aggregation id $sumgrades = 0; $count = 0; if (is_null($timegraded)) { $timegraded = time(); } foreach ($assessments as $assessment) { if (is_null($reviewerid)) { // the id is the same in all records, fetch it during the first loop cycle $reviewerid = $assessment->reviewerid; } if (is_null($agid)) { // the id is the same in all records, fetch it during the first loop cycle $agid = $assessment->aggregationid; } if (is_null($current)) { // the currently saved grade is the same in all records, fetch it during the first loop cycle $current = $assessment->aggregatedgrade; } if (!is_null($assessment->gradinggradeover)) { // the grading grade for this assessment is overridden by a teacher $sumgrades += $assessment->gradinggradeover; $count++; } else { if (!is_null($assessment->gradinggrade)) { $sumgrades += $assessment->gradinggrade; $count++; } } } if ($count > 0) { $finalgrade = grade_floatval($sumgrades / $count); } // Event information. $params = array( 'context' => $this->context, 'courseid' => $this->course->id, 'relateduserid' => $reviewerid ); // check if the new final grade differs from the one stored in the database if (grade_floats_different($finalgrade, $current)) { $params['other'] = array( 'currentgrade' => $current, 'finalgrade' => $finalgrade ); // we need to save new calculation into the database if (is_null($agid)) { // no aggregation record yet $record = new stdclass(); $record->workshopid = $this->id; $record->userid = $reviewerid; $record->gradinggrade = $finalgrade; $record->timegraded = $timegraded; $record->id = $DB->insert_record('workshop_aggregations', $record); $params['objectid'] = $record->id; $event = \mod_workshop\event\assessment_evaluated::create($params); $event->trigger(); } else { $record = new stdclass(); $record->id = $agid; $record->gradinggrade = $finalgrade; $record->timegraded = $timegraded; $DB->update_record('workshop_aggregations', $record); $params['objectid'] = $agid; $event = \mod_workshop\event\assessment_reevaluated::create($params); $event->trigger(); } } } /** * This is a replacement for the old get_users_with_capability_sql method. * The old method returned huge amounts of SQL for large courses, potentially 10s of thousands of lines * with hundreds of UNIONS, which caused some poor performance. This new method aims to streamline it all * into one query. * * Gets users with any of the specified capabilities on the workshop activity. * * The list is automatically restricted according to any availability restrictions * that apply to user lists (e.g. group, grouping restrictions). * * @param array $capabilities array of capability names (If the user has ANY of them, it returns true for that user) * @param bool $musthavesubmission if true, return only users who have already submitted * @param int $groupid 0 means ignore groups, any other value limits the result by group id * @param int $limitfrom return a subset of records, starting at this point (optional, required if $limitnum is set) * @param int $limitnum return a subset containing this number of records (optional, required if $limitfrom is set) * @return array of users */ protected function get_users_with_capability(array $capabilities, bool $musthavesubmission, int $groupid, int $limitfrom = 0, int $limitnum = 0): array { global $CFG, $DB; $params = []; // This is from enrollib.php:get_enrolled_join(). It says it's better for caching to use round. $now = round(time(), -2); $userfieldsapi = \core_user\fields::for_name()->with_userpic(); $sqlarray = []; $sqlarray['select'] = "SELECT DISTINCT u.id" . $userfieldsapi->get_sql('u')->selects; $sqlarray['from'] = "FROM {user} u"; $sqlarray['join'] = []; $sqlarray['join'][] = "JOIN {user_enrolments} ue ON ue.userid = u.id"; $sqlarray['join'][] = "JOIN {enrol} e ON e.id = ue.enrolid AND e.courseid = :courseid"; $sqlarray['where'] = []; $sqlarray['where'][] = "WHERE u.deleted = 0"; $sqlarray['where'][] = "AND u.id <> :guestid"; $sqlarray['where'][] = "AND ue.status = :activestatus"; $sqlarray['where'][] = "AND e.status = :enabledstatus"; $sqlarray['where'][] = "AND ue.timestart < :timestart"; $sqlarray['where'][] = "AND (ue.timeend > :timeend OR ue.timeend = 0)"; // Apply sorting. [$sortby, $sortbyparams] = users_order_by_sql('u'); $sqlarray['sort'] = "ORDER BY {$sortby}"; // Apply filtering. [$filtersql, $filterparams] = $this->get_users_with_initial_filtering_sql_where('u'); if ($filtersql) { $sqlarray['where'][] = "AND {$filtersql}"; $params = array_merge($params, $filterparams); } $params['courseid'] = $this->context->get_course_context()->instanceid; $params['timestart'] = $now; $params['timeend'] = $now; $params['guestid'] = $CFG->siteguest; $params['activestatus'] = ENROL_USER_ACTIVE; $params['enabledstatus'] = ENROL_INSTANCE_ENABLED; // If we are filtering by users who have submitted. if ($musthavesubmission) { $sqlarray['join'][] = "JOIN {workshop_submissions} ws ON (ws.authorid = u.id AND ws.example = 0 AND ws.workshopid = :workshopid) "; $params['workshopid'] = $this->id; } // Are we searching one specific group? if ($groupid > 0) { $sqlarray['join'][] = "JOIN {groups_members} gm ON (gm.userid = u.id AND gm.groupid = :groupid)"; $params['groupid'] = $groupid; } else if ($this->cm->groupingid) { // If not, is there a groupingid set on the activity? $sqlarray['join'][] = "JOIN {groupings_groups} gg ON gg.groupingid = :groupingid"; $sqlarray['join'][] = "JOIN {groups_members} gm ON (gm.userid = u.id AND gm.groupid = gg.groupid)"; $params['groupingid'] = $this->cm->groupingid; } // Is the activity restricted so only certain users can access it? $info = new \core_availability\info_module($this->cm); [$listsql, $listparams] = $info->get_user_list_sql(false); if ($listsql) { $sqlarray['join'][] = " JOIN ($listsql) restricted ON restricted.id = u.id"; $params = array_merge($params, $listparams); } // Join the role assignments to only return users with the right capabilities. $capjoin = get_with_capability_join($this->context, $capabilities, 'u.id'); $sqlarray['join'][] = $capjoin->joins; // Convert the sql array into a string. $sql = $this->build_sql($sqlarray); // We actually query the users here instead of returning SQL like it used to. return $DB->get_records_sql($sql, $params, $limitfrom, $limitnum); } /** * Build SQL string from array of clauses * * @param array $sqlarray ['select' => '', 'from' => '', 'join' => [], 'where' => [], 'sort' => ''] * @return string */ protected function build_sql(array $sqlarray): string { $sql = ""; foreach ($sqlarray as $el) { if (is_array($el)) { $sql .= implode(" ", $el) . " "; } else { $sql .= " {$el} "; } } return $sql; } /** * Returns SQL to fetch all enrolled users with the first name or last name. * * @param string $table Table alias for users table * @return array */ protected function get_users_with_initial_filtering_sql_where(string $table): array { global $DB; $conditions = []; $params = []; $ifirst = $this->get_initial_first(); $ilast = $this->get_initial_last(); if ($ifirst) { $conditions[] = $DB->sql_like('LOWER(' . $table . '.firstname)', ':i_first' , false, false); $params['i_first'] = $DB->sql_like_escape($ifirst) . '%'; } if ($ilast) { $conditions[] = $DB->sql_like('LOWER(' . $table . '.lastname)', ':i_last' , false, false); $params['i_last'] = $DB->sql_like_escape($ilast) . '%'; } return [implode(" AND ", $conditions), $params]; } /** * @return array of available workshop phases */ protected function available_phases_list() { return array( self::PHASE_SETUP => true, self::PHASE_SUBMISSION => true, self::PHASE_ASSESSMENT => true, self::PHASE_EVALUATION => true, self::PHASE_CLOSED => true, ); } /** * Converts absolute URL to relative URL needed by {@see add_to_log()} * * @param moodle_url $url absolute URL * @return string */ protected function log_convert_url(moodle_url $fullurl) { static $baseurl; if (!isset($baseurl)) { $baseurl = new moodle_url('/mod/workshop/'); $baseurl = $baseurl->out(); } return substr($fullurl->out(), strlen($baseurl)); } /** * Removes all user data related to assessments (including allocations). * * This includes assessments of example submissions as long as they are not * referential assessments. * * @param stdClass $data The actual course reset settings. * @return bool|string True on success, error message otherwise. */ protected function reset_userdata_assessments(stdClass $data) { global $DB; $sql = "SELECT a.id FROM {workshop_assessments} a JOIN {workshop_submissions} s ON (a.submissionid = s.id) WHERE s.workshopid = :workshopid AND (s.example = 0 OR (s.example = 1 AND a.weight = 0))"; $assessments = $DB->get_records_sql($sql, array('workshopid' => $this->id)); $this->delete_assessment(array_keys($assessments)); $DB->delete_records('workshop_aggregations', array('workshopid' => $this->id)); return true; } /** * Removes all user data related to participants' submissions. * * @param stdClass $data The actual course reset settings. * @return bool|string True on success, error message otherwise. */ protected function reset_userdata_submissions(stdClass $data) { global $DB; $submissions = $this->get_submissions(); foreach ($submissions as $submission) { $this->delete_submission($submission); } return true; } /** * Hard set the workshop phase to the setup one. */ protected function reset_phase() { global $DB; $DB->set_field('workshop', 'phase', self::PHASE_SETUP, array('id' => $this->id)); $this->phase = self::PHASE_SETUP; } } //////////////////////////////////////////////////////////////////////////////// // Renderable components //////////////////////////////////////////////////////////////////////////////// /** * Represents the user planner tool * * Planner contains list of phases. Each phase contains list of tasks. Task is a simple object with * title, link and completed (true/false/null logic). */ class workshop_user_plan implements renderable { /** @var int id of the user this plan is for */ public $userid; /** @var workshop */ public $workshop; /** @var array of (stdclass)tasks */ public $phases = array(); /** @var null|array of example submissions to be assessed by the planner owner */ protected $examples = null; /** * Prepare an individual workshop plan for the given user. * * @param workshop $workshop instance * @param int $userid whom the plan is prepared for */ public function __construct(workshop $workshop, $userid) { global $DB; $this->workshop = $workshop; $this->userid = $userid; //--------------------------------------------------------- // * SETUP | submission | assessment | evaluation | closed //--------------------------------------------------------- $phase = new stdclass(); $phase->title = get_string('phasesetup', 'workshop'); $phase->tasks = array(); if (has_capability('moodle/course:manageactivities', $workshop->context, $userid)) { $task = new stdclass(); $task->title = get_string('taskintro', 'workshop'); $task->link = $workshop->updatemod_url(); $task->completed = !(trim($workshop->intro) == ''); $phase->tasks['intro'] = $task; } if (has_capability('moodle/course:manageactivities', $workshop->context, $userid)) { $task = new stdclass(); $task->title = get_string('taskinstructauthors', 'workshop'); $task->link = $workshop->updatemod_url(); $task->completed = !(trim($workshop->instructauthors) == ''); $phase->tasks['instructauthors'] = $task; } if (has_capability('mod/workshop:editdimensions', $workshop->context, $userid)) { $task = new stdclass(); $task->title = get_string('editassessmentform', 'workshop'); $task->link = $workshop->editform_url(); if ($workshop->grading_strategy_instance()->form_ready()) { $task->completed = true; } elseif ($workshop->phase > workshop::PHASE_SETUP) { $task->completed = false; } $phase->tasks['editform'] = $task; } if ($workshop->useexamples and has_capability('mod/workshop:manageexamples', $workshop->context, $userid)) { $task = new stdclass(); $task->title = get_string('prepareexamples', 'workshop'); if ($DB->count_records('workshop_submissions', array('example' => 1, 'workshopid' => $workshop->id)) > 0) { $task->completed = true; } elseif ($workshop->phase > workshop::PHASE_SETUP) { $task->completed = false; } $phase->tasks['prepareexamples'] = $task; } if (empty($phase->tasks) and $workshop->phase == workshop::PHASE_SETUP) { // if we are in the setup phase and there is no task (typical for students), let us // display some explanation what is going on $task = new stdclass(); $task->title = get_string('undersetup', 'workshop'); $task->completed = 'info'; $phase->tasks['setupinfo'] = $task; } $this->phases[workshop::PHASE_SETUP] = $phase; //--------------------------------------------------------- // setup | * SUBMISSION | assessment | evaluation | closed //--------------------------------------------------------- $phase = new stdclass(); $phase->title = get_string('phasesubmission', 'workshop'); $phase->tasks = array(); if (has_capability('moodle/course:manageactivities', $workshop->context, $userid)) { $task = new stdclass(); $task->title = get_string('taskinstructreviewers', 'workshop'); $task->link = $workshop->updatemod_url(); if (trim($workshop->instructreviewers)) { $task->completed = true; } elseif ($workshop->phase >= workshop::PHASE_ASSESSMENT) { $task->completed = false; } $phase->tasks['instructreviewers'] = $task; } if ($workshop->useexamples and $workshop->examplesmode == workshop::EXAMPLES_BEFORE_SUBMISSION and has_capability('mod/workshop:submit', $workshop->context, $userid, false) and !has_capability('mod/workshop:manageexamples', $workshop->context, $userid)) { $task = new stdclass(); $task->title = get_string('exampleassesstask', 'workshop'); $examples = $this->get_examples(); $a = new stdclass(); $a->expected = count($examples); $a->assessed = 0; foreach ($examples as $exampleid => $example) { if (!is_null($example->grade)) { $a->assessed++; } } $task->details = get_string('exampleassesstaskdetails', 'workshop', $a); if ($a->assessed == $a->expected) { $task->completed = true; } elseif ($workshop->phase >= workshop::PHASE_ASSESSMENT) { $task->completed = false; } $phase->tasks['examples'] = $task; } if (has_capability('mod/workshop:submit', $workshop->context, $userid, false)) { $task = new stdclass(); $task->title = get_string('tasksubmit', 'workshop'); $task->link = $workshop->submission_url(); if ($DB->record_exists('workshop_submissions', array('workshopid'=>$workshop->id, 'example'=>0, 'authorid'=>$userid))) { $task->completed = true; } elseif ($workshop->phase >= workshop::PHASE_ASSESSMENT) { $task->completed = false; } else { $task->completed = null; // still has a chance to submit } $phase->tasks['submit'] = $task; } if (has_capability('mod/workshop:allocate', $workshop->context, $userid)) { if ($workshop->phaseswitchassessment) { $task = new stdClass(); $allocator = $DB->get_record('workshopallocation_scheduled', array('workshopid' => $workshop->id)); if (empty($allocator)) { $task->completed = false; } else if ($allocator->enabled and is_null($allocator->resultstatus)) { $task->completed = true; } else if ($workshop->submissionend > time()) { $task->completed = null; } else { $task->completed = false; } $task->title = get_string('setup', 'workshopallocation_scheduled'); $task->link = $workshop->allocation_url('scheduled'); $phase->tasks['allocatescheduled'] = $task; } $task = new stdclass(); $task->title = get_string('allocate', 'workshop'); $task->link = $workshop->allocation_url(); $numofauthors = $workshop->count_potential_authors(false); $numofsubmissions = $DB->count_records('workshop_submissions', array('workshopid'=>$workshop->id, 'example'=>0)); $sql = 'SELECT COUNT(s.id) AS nonallocated FROM {workshop_submissions} s LEFT JOIN {workshop_assessments} a ON (a.submissionid=s.id) WHERE s.workshopid = :workshopid AND s.example=0 AND a.submissionid IS NULL'; $params['workshopid'] = $workshop->id; $numnonallocated = $DB->count_records_sql($sql, $params); if ($numofsubmissions == 0) { $task->completed = null; } elseif ($numnonallocated == 0) { $task->completed = true; } elseif ($workshop->phase > workshop::PHASE_SUBMISSION) { $task->completed = false; } else { $task->completed = null; // still has a chance to allocate } $a = new stdclass(); $a->expected = $numofauthors; $a->submitted = $numofsubmissions; $a->allocate = $numnonallocated; $task->details = get_string('allocatedetails', 'workshop', $a); unset($a); $phase->tasks['allocate'] = $task; if ($numofsubmissions < $numofauthors and $workshop->phase >= workshop::PHASE_SUBMISSION) { $task = new stdclass(); $task->title = get_string('someuserswosubmission', 'workshop'); $task->completed = 'info'; $phase->tasks['allocateinfo'] = $task; } } if ($workshop->submissionstart) { $task = new stdclass(); $task->title = get_string('submissionstartdatetime', 'workshop', workshop::timestamp_formats($workshop->submissionstart)); $task->completed = 'info'; $phase->tasks['submissionstartdatetime'] = $task; } if ($workshop->submissionend) { $task = new stdclass(); $task->title = get_string('submissionenddatetime', 'workshop', workshop::timestamp_formats($workshop->submissionend)); $task->completed = 'info'; $phase->tasks['submissionenddatetime'] = $task; } if (($workshop->submissionstart < time()) and $workshop->latesubmissions) { // If submission deadline has passed and late submissions are allowed, only display 'latesubmissionsallowed' text to // users (students) who have not submitted and users(teachers, admins) who can switch pahase.. if (has_capability('mod/workshop:switchphase', $workshop->context, $userid) || (!$workshop->get_submission_by_author($userid) && $workshop->submissionend < time())) { $task = new stdclass(); $task->title = get_string('latesubmissionsallowed', 'workshop'); $task->completed = 'info'; $phase->tasks['latesubmissionsallowed'] = $task; } } if (isset($phase->tasks['submissionstartdatetime']) or isset($phase->tasks['submissionenddatetime'])) { if (has_capability('mod/workshop:ignoredeadlines', $workshop->context, $userid)) { $task = new stdclass(); $task->title = get_string('deadlinesignored', 'workshop'); $task->completed = 'info'; $phase->tasks['deadlinesignored'] = $task; } } $this->phases[workshop::PHASE_SUBMISSION] = $phase; //--------------------------------------------------------- // setup | submission | * ASSESSMENT | evaluation | closed //--------------------------------------------------------- $phase = new stdclass(); $phase->title = get_string('phaseassessment', 'workshop'); $phase->tasks = array(); $phase->isreviewer = has_capability('mod/workshop:peerassess', $workshop->context, $userid); if ($workshop->phase == workshop::PHASE_SUBMISSION and $workshop->phaseswitchassessment and has_capability('mod/workshop:switchphase', $workshop->context, $userid)) { $task = new stdClass(); $task->title = get_string('switchphase30auto', 'mod_workshop', workshop::timestamp_formats($workshop->submissionend)); $task->completed = 'info'; $phase->tasks['autoswitchinfo'] = $task; } if ($workshop->useexamples and $workshop->examplesmode == workshop::EXAMPLES_BEFORE_ASSESSMENT and $phase->isreviewer and !has_capability('mod/workshop:manageexamples', $workshop->context, $userid)) { $task = new stdclass(); $task->title = get_string('exampleassesstask', 'workshop'); $examples = $workshop->get_examples_for_reviewer($userid); $a = new stdclass(); $a->expected = count($examples); $a->assessed = 0; foreach ($examples as $exampleid => $example) { if (!is_null($example->grade)) { $a->assessed++; } } $task->details = get_string('exampleassesstaskdetails', 'workshop', $a); if ($a->assessed == $a->expected) { $task->completed = true; } elseif ($workshop->phase > workshop::PHASE_ASSESSMENT) { $task->completed = false; } $phase->tasks['examples'] = $task; } if (empty($phase->tasks['examples']) or !empty($phase->tasks['examples']->completed)) { $phase->assessments = $workshop->get_assessments_by_reviewer($userid); $numofpeers = 0; // number of allocated peer-assessments $numofpeerstodo = 0; // number of peer-assessments to do $numofself = 0; // number of allocated self-assessments - should be 0 or 1 $numofselftodo = 0; // number of self-assessments to do - should be 0 or 1 foreach ($phase->assessments as $a) { if ($a->authorid == $userid) { $numofself++; if (is_null($a->grade)) { $numofselftodo++; } } else { $numofpeers++; if (is_null($a->grade)) { $numofpeerstodo++; } } } unset($a); if ($numofpeers) { $task = new stdclass(); if ($numofpeerstodo == 0) { $task->completed = true; } elseif ($workshop->phase > workshop::PHASE_ASSESSMENT) { $task->completed = false; } $a = new stdclass(); $a->total = $numofpeers; $a->todo = $numofpeerstodo; $task->title = get_string('taskassesspeers', 'workshop'); $task->details = get_string('taskassesspeersdetails', 'workshop', $a); unset($a); $phase->tasks['assesspeers'] = $task; } if ($workshop->useselfassessment and $numofself) { $task = new stdclass(); if ($numofselftodo == 0) { $task->completed = true; } elseif ($workshop->phase > workshop::PHASE_ASSESSMENT) { $task->completed = false; } $task->title = get_string('taskassessself', 'workshop'); $phase->tasks['assessself'] = $task; } } if ($workshop->assessmentstart) { $task = new stdclass(); $task->title = get_string('assessmentstartdatetime', 'workshop', workshop::timestamp_formats($workshop->assessmentstart)); $task->completed = 'info'; $phase->tasks['assessmentstartdatetime'] = $task; } if ($workshop->assessmentend) { $task = new stdclass(); $task->title = get_string('assessmentenddatetime', 'workshop', workshop::timestamp_formats($workshop->assessmentend)); $task->completed = 'info'; $phase->tasks['assessmentenddatetime'] = $task; } if (isset($phase->tasks['assessmentstartdatetime']) or isset($phase->tasks['assessmentenddatetime'])) { if (has_capability('mod/workshop:ignoredeadlines', $workshop->context, $userid)) { $task = new stdclass(); $task->title = get_string('deadlinesignored', 'workshop'); $task->completed = 'info'; $phase->tasks['deadlinesignored'] = $task; } } $this->phases[workshop::PHASE_ASSESSMENT] = $phase; //--------------------------------------------------------- // setup | submission | assessment | * EVALUATION | closed //--------------------------------------------------------- $phase = new stdclass(); $phase->title = get_string('phaseevaluation', 'workshop'); $phase->tasks = array(); if (has_capability('mod/workshop:overridegrades', $workshop->context)) { $expected = $workshop->count_potential_authors(false); $calculated = $DB->count_records_select('workshop_submissions', 'workshopid = ? AND (grade IS NOT NULL OR gradeover IS NOT NULL)', array($workshop->id)); $task = new stdclass(); $task->title = get_string('calculatesubmissiongrades', 'workshop'); $a = new stdclass(); $a->expected = $expected; $a->calculated = $calculated; $task->details = get_string('calculatesubmissiongradesdetails', 'workshop', $a); if ($calculated >= $expected) { $task->completed = true; } elseif ($workshop->phase > workshop::PHASE_EVALUATION) { $task->completed = false; } $phase->tasks['calculatesubmissiongrade'] = $task; $expected = $workshop->count_potential_reviewers(false); $calculated = $DB->count_records_select('workshop_aggregations', 'workshopid = ? AND gradinggrade IS NOT NULL', array($workshop->id)); $task = new stdclass(); $task->title = get_string('calculategradinggrades', 'workshop'); $a = new stdclass(); $a->expected = $expected; $a->calculated = $calculated; $task->details = get_string('calculategradinggradesdetails', 'workshop', $a); if ($calculated >= $expected) { $task->completed = true; } elseif ($workshop->phase > workshop::PHASE_EVALUATION) { $task->completed = false; } $phase->tasks['calculategradinggrade'] = $task; } elseif ($workshop->phase == workshop::PHASE_EVALUATION) { $task = new stdclass(); $task->title = get_string('evaluategradeswait', 'workshop'); $task->completed = 'info'; $phase->tasks['evaluateinfo'] = $task; } if (has_capability('moodle/course:manageactivities', $workshop->context, $userid)) { $task = new stdclass(); $task->title = get_string('taskconclusion', 'workshop'); $task->link = $workshop->updatemod_url(); if (trim($workshop->conclusion)) { $task->completed = true; } elseif ($workshop->phase >= workshop::PHASE_EVALUATION) { $task->completed = false; } $phase->tasks['conclusion'] = $task; } $this->phases[workshop::PHASE_EVALUATION] = $phase; //--------------------------------------------------------- // setup | submission | assessment | evaluation | * CLOSED //--------------------------------------------------------- $phase = new stdclass(); $phase->title = get_string('phaseclosed', 'workshop'); $phase->tasks = array(); $this->phases[workshop::PHASE_CLOSED] = $phase; // Polish data, set default values if not done explicitly foreach ($this->phases as $phasecode => $phase) { $phase->title = isset($phase->title) ? $phase->title : ''; $phase->tasks = isset($phase->tasks) ? $phase->tasks : array(); if ($phasecode == $workshop->phase) { $phase->active = true; } else { $phase->active = false; } if (!isset($phase->actions)) { $phase->actions = array(); } foreach ($phase->tasks as $taskcode => $task) { $task->title = isset($task->title) ? $task->title : ''; $task->link = isset($task->link) ? $task->link : null; $task->details = isset($task->details) ? $task->details : ''; $task->completed = isset($task->completed) ? $task->completed : null; } } // Add phase switching actions. if (has_capability('mod/workshop:switchphase', $workshop->context, $userid)) { $nextphases = array( workshop::PHASE_SETUP => workshop::PHASE_SUBMISSION, workshop::PHASE_SUBMISSION => workshop::PHASE_ASSESSMENT, workshop::PHASE_ASSESSMENT => workshop::PHASE_EVALUATION, workshop::PHASE_EVALUATION => workshop::PHASE_CLOSED, ); foreach ($this->phases as $phasecode => $phase) { if ($phase->active) { if (isset($nextphases[$workshop->phase])) { $task = new stdClass(); $task->title = get_string('switchphasenext', 'mod_workshop'); $task->link = $workshop->switchphase_url($nextphases[$workshop->phase]); $task->details = ''; $task->completed = null; $phase->tasks['switchtonextphase'] = $task; } } else { $action = new stdclass(); $action->type = 'switchphase'; $action->url = $workshop->switchphase_url($phasecode); $phase->actions[] = $action; } } } } /** * Returns example submissions to be assessed by the owner of the planner * * This is here to cache the DB query because the same list is needed later in view.php * * @see workshop::get_examples_for_reviewer() for the format of returned value * @return array */ public function get_examples() { if (is_null($this->examples)) { $this->examples = $this->workshop->get_examples_for_reviewer($this->userid); } return $this->examples; } } /** * Common base class for submissions and example submissions rendering * * Subclasses of this class convert raw submission record from * workshop_submissions table (as returned by {@see workshop::get_submission_by_id()} * for example) into renderable objects. */ abstract class workshop_submission_base { /** @var bool is the submission anonymous (i.e. contains author information) */ protected $anonymous; /* @var array of columns from workshop_submissions that are assigned as properties */ protected $fields = array(); /** @var workshop */ protected $workshop; /** * Copies the properties of the given database record into properties of $this instance * * @param workshop $workshop * @param stdClass $submission full record * @param bool $showauthor show the author-related information * @param array $options additional properties */ public function __construct(workshop $workshop, stdClass $submission, $showauthor = false) { $this->workshop = $workshop; foreach ($this->fields as $field) { if (!property_exists($submission, $field)) { throw new coding_exception('Submission record must provide public property ' . $field); } if (!property_exists($this, $field)) { throw new coding_exception('Renderable component must accept public property ' . $field); } $this->{$field} = $submission->{$field}; } if ($showauthor) { $this->anonymous = false; } else { $this->anonymize(); } } /** * Unsets all author-related properties so that the renderer does not have access to them * * Usually this is called by the contructor but can be called explicitely, too. */ public function anonymize() { $authorfields = explode(',', implode(',', \core_user\fields::get_picture_fields())); foreach ($authorfields as $field) { $prefixedusernamefield = 'author' . $field; unset($this->{$prefixedusernamefield}); } $this->anonymous = true; } /** * Does the submission object contain author-related information? * * @return null|boolean */ public function is_anonymous() { return $this->anonymous; } } /** * Renderable object containing a basic set of information needed to display the submission summary * * @see workshop_renderer::render_workshop_submission_summary */ class workshop_submission_summary extends workshop_submission_base implements renderable { /** @var int */ public $id; /** @var string */ public $title; /** @var string graded|notgraded */ public $status; /** @var int */ public $timecreated; /** @var int */ public $timemodified; /** @var int */ public $authorid; /** @var string */ public $authorfirstname; /** @var string */ public $authorlastname; /** @var string */ public $authorfirstnamephonetic; /** @var string */ public $authorlastnamephonetic; /** @var string */ public $authormiddlename; /** @var string */ public $authoralternatename; /** @var int */ public $authorpicture; /** @var string */ public $authorimagealt; /** @var string */ public $authoremail; /** @var moodle_url to display submission */ public $url; /** * @var array of columns from workshop_submissions that are assigned as properties * of instances of this class */ protected $fields = array( 'id', 'title', 'timecreated', 'timemodified', 'authorid', 'authorfirstname', 'authorlastname', 'authorfirstnamephonetic', 'authorlastnamephonetic', 'authormiddlename', 'authoralternatename', 'authorpicture', 'authorimagealt', 'authoremail'); } /** * Renderable object containing all the information needed to display the submission * * @see workshop_renderer::render_workshop_submission() */ class workshop_submission extends workshop_submission_summary implements renderable { /** @var string */ public $content; /** @var int */ public $contentformat; /** @var bool */ public $contenttrust; /** @var array */ public $attachment; /** * @var array of columns from workshop_submissions that are assigned as properties * of instances of this class */ protected $fields = array( 'id', 'title', 'timecreated', 'timemodified', 'content', 'contentformat', 'contenttrust', 'attachment', 'authorid', 'authorfirstname', 'authorlastname', 'authorfirstnamephonetic', 'authorlastnamephonetic', 'authormiddlename', 'authoralternatename', 'authorpicture', 'authorimagealt', 'authoremail'); } /** * Renderable object containing a basic set of information needed to display the example submission summary * * @see workshop::prepare_example_summary() * @see workshop_renderer::render_workshop_example_submission_summary() */ class workshop_example_submission_summary extends workshop_submission_base implements renderable { /** @var int */ public $id; /** @var string */ public $title; /** @var string graded|notgraded */ public $status; /** @var stdClass */ public $gradeinfo; /** @var moodle_url */ public $url; /** @var moodle_url */ public $editurl; /** @var string */ public $assesslabel; /** @var moodle_url */ public $assessurl; /** @var bool must be set explicitly by the caller */ public $editable = false; /** * @var array of columns from workshop_submissions that are assigned as properties * of instances of this class */ protected $fields = array('id', 'title'); /** * Example submissions are always anonymous * * @return true */ public function is_anonymous() { return true; } } /** * Renderable object containing all the information needed to display the example submission * * @see workshop_renderer::render_workshop_example_submission() */ class workshop_example_submission extends workshop_example_submission_summary implements renderable { /** @var string */ public $content; /** @var int */ public $contentformat; /** @var bool */ public $contenttrust; /** @var array */ public $attachment; /** * @var array of columns from workshop_submissions that are assigned as properties * of instances of this class */ protected $fields = array('id', 'title', 'content', 'contentformat', 'contenttrust', 'attachment'); } /** * Common base class for assessments rendering * * Subclasses of this class convert raw assessment record from * workshop_assessments table (as returned by {@see workshop::get_assessment_by_id()} * for example) into renderable objects. */ abstract class workshop_assessment_base { /** @var string the optional title of the assessment */ public $title = ''; /** @var workshop_assessment_form $form as returned by {@link workshop_strategy::get_assessment_form()} */ public $form; /** @var moodle_url */ public $url; /** @var float|null the real received grade */ public $realgrade = null; /** @var float the real maximum grade */ public $maxgrade; /** @var stdClass|null reviewer user info */ public $reviewer = null; /** @var stdClass|null assessed submission's author user info */ public $author = null; /** @var array of actions */ public $actions = array(); /* @var array of columns that are assigned as properties */ protected $fields = array(); /** @var workshop */ public $workshop; /** * Copies the properties of the given database record into properties of $this instance * * The $options keys are: showreviewer, showauthor * @param workshop $workshop * @param stdClass $assessment full record * @param array $options additional properties */ public function __construct(workshop $workshop, stdClass $record, array $options = array()) { $this->workshop = $workshop; $this->validate_raw_record($record); foreach ($this->fields as $field) { if (!property_exists($record, $field)) { throw new coding_exception('Assessment record must provide public property ' . $field); } if (!property_exists($this, $field)) { throw new coding_exception('Renderable component must accept public property ' . $field); } $this->{$field} = $record->{$field}; } if (!empty($options['showreviewer'])) { $this->reviewer = user_picture::unalias($record, null, 'revieweridx', 'reviewer'); } if (!empty($options['showauthor'])) { $this->author = user_picture::unalias($record, null, 'authorid', 'author'); } } /** * Adds a new action * * @param moodle_url $url action URL * @param string $label action label * @param string $method get|post */ public function add_action(moodle_url $url, $label, $method = 'get') { $action = new stdClass(); $action->url = $url; $action->label = $label; $action->method = $method; $this->actions[] = $action; } /** * Makes sure that we can cook the renderable component from the passed raw database record * * @param stdClass $assessment full assessment record * @throws coding_exception if the caller passed unexpected data */ protected function validate_raw_record(stdClass $record) { // nothing to do here } } /** * Represents a rendarable full assessment */ class workshop_assessment extends workshop_assessment_base implements renderable { /** @var int */ public $id; /** @var int */ public $submissionid; /** @var int */ public $weight; /** @var int */ public $timecreated; /** @var int */ public $timemodified; /** @var float */ public $grade; /** @var float */ public $gradinggrade; /** @var float */ public $gradinggradeover; /** @var string */ public $feedbackauthor; /** @var int */ public $feedbackauthorformat; /** @var int */ public $feedbackauthorattachment; /** @var array */ protected $fields = array('id', 'submissionid', 'weight', 'timecreated', 'timemodified', 'grade', 'gradinggrade', 'gradinggradeover', 'feedbackauthor', 'feedbackauthorformat', 'feedbackauthorattachment'); /** * Format the overall feedback text content * * False is returned if the overall feedback feature is disabled. Null is returned * if the overall feedback content has not been found. Otherwise, string with * formatted feedback text is returned. * * @return string|bool|null */ public function get_overall_feedback_content() { if ($this->workshop->overallfeedbackmode == 0) { return false; } if (trim($this->feedbackauthor) === '') { return null; } $content = file_rewrite_pluginfile_urls($this->feedbackauthor, 'pluginfile.php', $this->workshop->context->id, 'mod_workshop', 'overallfeedback_content', $this->id); $content = format_text($content, $this->feedbackauthorformat, array('overflowdiv' => true, 'context' => $this->workshop->context)); return $content; } /** * Prepares the list of overall feedback attachments * * Returns false if overall feedback attachments are not allowed. Otherwise returns * list of attachments (may be empty). * * @return bool|array of stdClass */ public function get_overall_feedback_attachments() { if ($this->workshop->overallfeedbackmode == 0) { return false; } if ($this->workshop->overallfeedbackfiles == 0) { return false; } if (empty($this->feedbackauthorattachment)) { return array(); } $attachments = array(); $fs = get_file_storage(); $files = $fs->get_area_files($this->workshop->context->id, 'mod_workshop', 'overallfeedback_attachment', $this->id); foreach ($files as $file) { if ($file->is_directory()) { continue; } $filepath = $file->get_filepath(); $filename = $file->get_filename(); $fileurl = moodle_url::make_pluginfile_url($this->workshop->context->id, 'mod_workshop', 'overallfeedback_attachment', $this->id, $filepath, $filename, true); $previewurl = new moodle_url(moodle_url::make_pluginfile_url($this->workshop->context->id, 'mod_workshop', 'overallfeedback_attachment', $this->id, $filepath, $filename, false), array('preview' => 'bigthumb')); $attachments[] = (object)array( 'filepath' => $filepath, 'filename' => $filename, 'fileurl' => $fileurl, 'previewurl' => $previewurl, 'mimetype' => $file->get_mimetype(), ); } return $attachments; } } /** * Represents a renderable training assessment of an example submission */ class workshop_example_assessment extends workshop_assessment implements renderable { /** * @see parent::validate_raw_record() */ protected function validate_raw_record(stdClass $record) { if ($record->weight != 0) { throw new coding_exception('Invalid weight of example submission assessment'); } parent::validate_raw_record($record); } } /** * Represents a renderable reference assessment of an example submission */ class workshop_example_reference_assessment extends workshop_assessment implements renderable { /** * @see parent::validate_raw_record() */ protected function validate_raw_record(stdClass $record) { if ($record->weight != 1) { throw new coding_exception('Invalid weight of the reference example submission assessment'); } parent::validate_raw_record($record); } } /** * Renderable message to be displayed to the user * * Message can contain an optional action link with a label that is supposed to be rendered * as a button or a link. * * @see workshop::renderer::render_workshop_message() */ class workshop_message implements renderable { const TYPE_INFO = 10; const TYPE_OK = 20; const TYPE_ERROR = 30; /** @var string */ protected $text = ''; /** @var int */ protected $type = self::TYPE_INFO; /** @var moodle_url */ protected $actionurl = null; /** @var string */ protected $actionlabel = ''; /** * @param string $text short text to be displayed * @param string $type optional message type info|ok|error */ public function __construct($text = null, $type = self::TYPE_INFO) { $this->set_text($text); $this->set_type($type); } /** * Sets the message text * * @param string $text short text to be displayed */ public function set_text($text) { $this->text = $text; } /** * Sets the message type * * @param int $type */ public function set_type($type = self::TYPE_INFO) { if (in_array($type, array(self::TYPE_OK, self::TYPE_ERROR, self::TYPE_INFO))) { $this->type = $type; } else { throw new coding_exception('Unknown message type.'); } } /** * Sets the optional message action * * @param moodle_url $url to follow on action * @param string $label action label */ public function set_action(moodle_url $url, $label) { $this->actionurl = $url; $this->actionlabel = $label; } /** * Returns message text with HTML tags quoted * * @return string */ public function get_message() { return s($this->text); } /** * Returns message type * * @return int */ public function get_type() { return $this->type; } /** * Returns action URL * * @return moodle_url|null */ public function get_action_url() { return $this->actionurl; } /** * Returns action label * * @return string */ public function get_action_label() { return $this->actionlabel; } } /** * Renderable component containing all the data needed to display the grading report */ class workshop_grading_report implements renderable { /** @var stdClass returned by {@see workshop::prepare_grading_report_data()} */ protected $data; /** @var stdClass rendering options */ protected $options; /** * Grades in $data must be already rounded to the set number of decimals or must be null * (in which later case, the [mod_workshop,nullgrade] string shall be displayed) * * @param stdClass $data prepared by {@link workshop::prepare_grading_report_data()} * @param stdClass $options display options (showauthornames, showreviewernames, sortby, sorthow, showsubmissiongrade, showgradinggrade) */ public function __construct(stdClass $data, stdClass $options) { $this->data = $data; $this->options = $options; } /** * @return stdClass grading report data */ public function get_data() { return $this->data; } /** * @return stdClass rendering options */ public function get_options() { return $this->options; } /** * Prepare the data to be exported to a external system via Web Services. * * This function applies extra capabilities checks. * @return stdClass the data ready for external systems */ public function export_data_for_external() { $data = $this->get_data(); $options = $this->get_options(); foreach ($data->grades as $reportdata) { // If we are in submission phase ignore the following data. if ($options->workshopphase == workshop::PHASE_SUBMISSION) { unset($reportdata->submissiongrade); unset($reportdata->gradinggrade); unset($reportdata->submissiongradeover); unset($reportdata->submissiongradeoverby); unset($reportdata->submissionpublished); unset($reportdata->reviewedby); unset($reportdata->reviewerof); continue; } if (!$options->showsubmissiongrade) { unset($reportdata->submissiongrade); unset($reportdata->submissiongradeover); } if (!$options->showgradinggrade and $tr == 0) { unset($reportdata->gradinggrade); } if (!$options->showreviewernames) { foreach ($reportdata->reviewedby as $reviewedby) { $reviewedby->userid = 0; } } if (!$options->showauthornames) { foreach ($reportdata->reviewerof as $reviewerof) { $reviewerof->userid = 0; } } } return $data; } } /** * Base class for renderable feedback for author and feedback for reviewer */ abstract class workshop_feedback { /** @var stdClass the user info */ protected $provider = null; /** @var string the feedback text */ protected $content = null; /** @var int format of the feedback text */ protected $format = null; /** * @return stdClass the user info */ public function get_provider() { if (is_null($this->provider)) { throw new coding_exception('Feedback provider not set'); } return $this->provider; } /** * @return string the feedback text */ public function get_content() { if (is_null($this->content)) { throw new coding_exception('Feedback content not set'); } return $this->content; } /** * @return int format of the feedback text */ public function get_format() { if (is_null($this->format)) { throw new coding_exception('Feedback text format not set'); } return $this->format; } } /** * Renderable feedback for the author of submission */ class workshop_feedback_author extends workshop_feedback implements renderable { /** * Extracts feedback from the given submission record * * @param stdClass $submission record as returned by {@see self::get_submission_by_id()} */ public function __construct(stdClass $submission) { $this->provider = user_picture::unalias($submission, null, 'gradeoverbyx', 'gradeoverby'); $this->content = $submission->feedbackauthor; $this->format = $submission->feedbackauthorformat; } } /** * Renderable feedback for the reviewer */ class workshop_feedback_reviewer extends workshop_feedback implements renderable { /** * Extracts feedback from the given assessment record * * @param stdClass $assessment record as returned by eg {@see self::get_assessment_by_id()} */ public function __construct(stdClass $assessment) { $this->provider = user_picture::unalias($assessment, null, 'gradinggradeoverbyx', 'overby'); $this->content = $assessment->feedbackreviewer; $this->format = $assessment->feedbackreviewerformat; } } /** * Holds the final grades for the activity as are stored in the gradebook */ class workshop_final_grades implements renderable { /** @var object the info from the gradebook about the grade for submission */ public $submissiongrade = null; /** @var object the infor from the gradebook about the grade for assessment */ public $assessmentgrade = null; } home/smkb8269/public_html/lms.smk-almanshuriyah.sch.id/mod/wiki/locallib.php000064400000171601152267056210022711 0ustar00. /** * This contains functions and classes that will be used by scripts in wiki module * * @package mod_wiki * @copyright 2009 Marc Alier, Jordi Piguillem marc.alier@upc.edu * @copyright 2009 Universitat Politecnica de Catalunya http://www.upc.edu * * @author Jordi Piguillem * @author Marc Alier * @author David Jimenez * @author Josep Arus * @author Daniel Serrano * @author Kenneth Riba * * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); require_once($CFG->dirroot . '/mod/wiki/lib.php'); require_once($CFG->dirroot . '/mod/wiki/parser/parser.php'); require_once($CFG->libdir . '/filelib.php'); define('WIKI_REFRESH_CACHE_TIME', 30); // @TODO: To be deleted. define('FORMAT_CREOLE', '37'); define('FORMAT_NWIKI', '38'); define('NO_VALID_RATE', '-999'); define('IMPROVEMENT', '+'); define('EQUAL', '='); define('WORST', '-'); define('LOCK_TIMEOUT', 30); /** * Get a wiki instance * @param int $wikiid the instance id of wiki */ function wiki_get_wiki($wikiid) { global $DB; return $DB->get_record('wiki', array('id' => $wikiid)); } /** * Get sub wiki instances with same wiki id * @param int $wikiid */ function wiki_get_subwikis($wikiid) { global $DB; return $DB->get_records('wiki_subwikis', array('wikiid' => $wikiid)); } /** * Get a sub wiki instance by wiki id and group id * @param int $wikiid * @param int $groupid * @return object */ function wiki_get_subwiki_by_group($wikiid, $groupid, $userid = 0) { global $DB; return $DB->get_record('wiki_subwikis', array('wikiid' => $wikiid, 'groupid' => $groupid, 'userid' => $userid)); } /** * Get a sub wiki instace by instance id * @param int $subwikiid * @return object */ function wiki_get_subwiki($subwikiid) { global $DB; return $DB->get_record('wiki_subwikis', array('id' => $subwikiid)); } /** * Add a new sub wiki instance * @param int $wikiid * @param int $groupid * @return int $insertid */ function wiki_add_subwiki($wikiid, $groupid, $userid = 0) { global $DB; $record = new StdClass(); $record->wikiid = $wikiid; $record->groupid = $groupid; $record->userid = $userid; $insertid = $DB->insert_record('wiki_subwikis', $record); return $insertid; } /** * Get a wiki instance by pageid * @param int $pageid * @return object */ function wiki_get_wiki_from_pageid($pageid) { global $DB; $sql = "SELECT w.* FROM {wiki} w, {wiki_subwikis} s, {wiki_pages} p WHERE p.id = ? AND p.subwikiid = s.id AND s.wikiid = w.id"; return $DB->get_record_sql($sql, array($pageid)); } /** * Get a wiki page by pageid * @param int $pageid * @return object */ function wiki_get_page($pageid) { global $DB; return $DB->get_record('wiki_pages', array('id' => $pageid)); } /** * Get latest version of wiki page * @param int $pageid * @return object */ function wiki_get_current_version($pageid) { global $DB; // @TODO: Fix this query $sql = "SELECT * FROM {wiki_versions} WHERE pageid = ? ORDER BY version DESC"; $records = $DB->get_records_sql($sql, array($pageid), 0, 1); return array_pop($records); } /** * Alias of wiki_get_current_version * @TODO, does the exactly same thing as wiki_get_current_version, should be removed * @param int $pageid * @return object */ function wiki_get_last_version($pageid) { return wiki_get_current_version($pageid); } /** * Get page section * @param int $pageid * @param string $section */ function wiki_get_section_page($page, $section) { $version = wiki_get_current_version($page->id); return wiki_parser_proxy::get_section($version->content, $version->contentformat, $section); } /** * Get a wiki page by page title * @param int $swid, sub wiki id * @param string $title * @return object */ function wiki_get_page_by_title($swid, $title) { global $DB; return $DB->get_record('wiki_pages', array('subwikiid' => $swid, 'title' => $title)); } /** * Get a version record by record id * @param int $versionid, the version id * @return object */ function wiki_get_version($versionid) { global $DB; return $DB->get_record('wiki_versions', array('id' => $versionid)); } /** * Get first page of wiki instace * @param int $subwikiid * @param int $module, wiki instance object */ function wiki_get_first_page($subwikid, $module = null) { global $DB, $USER; $sql = "SELECT p.* FROM {wiki} w, {wiki_subwikis} s, {wiki_pages} p WHERE s.id = ? AND s.wikiid = w.id AND w.firstpagetitle = p.title AND p.subwikiid = s.id"; return $DB->get_record_sql($sql, array($subwikid)); } function wiki_save_section($wikipage, $sectiontitle, $sectioncontent, $userid) { $wiki = wiki_get_wiki_from_pageid($wikipage->id); $cm = get_coursemodule_from_instance('wiki', $wiki->id); $context = context_module::instance($cm->id); if (has_capability('mod/wiki:editpage', $context)) { $version = wiki_get_current_version($wikipage->id); $content = wiki_parser_proxy::get_section($version->content, $version->contentformat, $sectiontitle, true); $newcontent = $content[0] . $sectioncontent . $content[2]; return wiki_save_page($wikipage, $newcontent, $userid); } else { return false; } } /** * Save page content * @param object $wikipage * @param string $newcontent * @param int $userid */ function wiki_save_page($wikipage, $newcontent, $userid) { global $DB; $wiki = wiki_get_wiki_from_pageid($wikipage->id); $cm = get_coursemodule_from_instance('wiki', $wiki->id); $context = context_module::instance($cm->id); if (has_capability('mod/wiki:editpage', $context)) { $version = wiki_get_current_version($wikipage->id); $version->content = $newcontent; $version->userid = $userid; $version->version++; $version->timecreated = time(); $version->id = $DB->insert_record('wiki_versions', $version); $wikipage->timemodified = $version->timecreated; $wikipage->userid = $userid; $return = wiki_refresh_cachedcontent($wikipage, $newcontent); $event = \mod_wiki\event\page_updated::create( array( 'context' => $context, 'objectid' => $wikipage->id, 'relateduserid' => $userid, 'other' => array( 'newcontent' => $newcontent ) )); $event->add_record_snapshot('wiki', $wiki); $event->add_record_snapshot('wiki_pages', $wikipage); $event->add_record_snapshot('wiki_versions', $version); $event->trigger(); return $return; } else { return false; } } function wiki_refresh_cachedcontent($page, $newcontent = null) { global $DB; $version = wiki_get_current_version($page->id); if (empty($version)) { return null; } if (!isset($newcontent)) { $newcontent = $version->content; } $options = array('swid' => $page->subwikiid, 'pageid' => $page->id); $parseroutput = wiki_parse_content($version->contentformat, $newcontent, $options); $page->cachedcontent = $parseroutput['toc'] . $parseroutput['parsed_text']; $page->timerendered = time(); $DB->update_record('wiki_pages', $page); wiki_refresh_page_links($page, $parseroutput['link_count']); return array('page' => $page, 'sections' => $parseroutput['repeated_sections'], 'version' => $version->version); } /** * Restore a page with specified version. * * @param stdClass $wikipage wiki page record * @param stdClass $version wiki page version to restore * @param context_module $context context of wiki module * @return stdClass restored page */ function wiki_restore_page($wikipage, $version, $context) { $return = wiki_save_page($wikipage, $version->content, $version->userid); $event = \mod_wiki\event\page_version_restored::create( array( 'context' => $context, 'objectid' => $version->id, 'other' => array( 'pageid' => $wikipage->id ) )); $event->add_record_snapshot('wiki_versions', $version); $event->trigger(); return $return['page']; } function wiki_refresh_page_links($page, $links) { global $DB; $DB->delete_records('wiki_links', array('frompageid' => $page->id)); foreach ($links as $linkname => $linkinfo) { $newlink = new stdClass(); $newlink->subwikiid = $page->subwikiid; $newlink->frompageid = $page->id; if ($linkinfo['new']) { $newlink->tomissingpage = $linkname; } else { $newlink->topageid = $linkinfo['pageid']; } try { $DB->insert_record('wiki_links', $newlink); } catch (dml_exception $e) { debugging($e->getMessage()); } } } /** * Create a new wiki page, if the page exists, return existing pageid * @param int $swid * @param string $title * @param string $format * @param int $userid */ function wiki_create_page($swid, $title, $format, $userid) { global $DB; $subwiki = wiki_get_subwiki($swid); $cm = get_coursemodule_from_instance('wiki', $subwiki->wikiid); $context = context_module::instance($cm->id); require_capability('mod/wiki:editpage', $context); // if page exists if ($page = wiki_get_page_by_title($swid, $title)) { return $page->id; } // Creating a new empty version $version = new stdClass(); $version->content = ''; $version->contentformat = $format; $version->version = 0; $version->timecreated = time(); $version->userid = $userid; $versionid = null; $versionid = $DB->insert_record('wiki_versions', $version); // Createing a new empty page $page = new stdClass(); $page->subwikiid = $swid; $page->title = $title; $page->cachedcontent = ''; $page->timecreated = $version->timecreated; $page->timemodified = $version->timecreated; $page->timerendered = $version->timecreated; $page->userid = $userid; $page->pageviews = 0; $page->readonly = 0; $pageid = $DB->insert_record('wiki_pages', $page); // Setting the pageid $version->id = $versionid; $version->pageid = $pageid; $DB->update_record('wiki_versions', $version); $event = \mod_wiki\event\page_created::create( array( 'context' => $context, 'objectid' => $pageid ) ); $event->trigger(); wiki_make_cache_expire($page->title); return $pageid; } function wiki_make_cache_expire($pagename) { global $DB; $sql = "UPDATE {wiki_pages} SET timerendered = 0 WHERE id IN ( SELECT l.frompageid FROM {wiki_links} l WHERE l.tomissingpage = ? )"; $DB->execute ($sql, array($pagename)); } /** * Get a specific version of page * @param int $pageid * @param int $version */ function wiki_get_wiki_page_version($pageid, $version) { global $DB; return $DB->get_record('wiki_versions', array('pageid' => $pageid, 'version' => $version)); } /** * Get version list * @param int $pageid * @param int $limitfrom * @param int $limitnum */ function wiki_get_wiki_page_versions($pageid, $limitfrom, $limitnum) { global $DB; return $DB->get_records('wiki_versions', array('pageid' => $pageid), 'version DESC', '*', $limitfrom, $limitnum); } /** * Count the number of page version * @param int $pageid */ function wiki_count_wiki_page_versions($pageid) { global $DB; return $DB->count_records('wiki_versions', array('pageid' => $pageid)); } /** * Get linked from page * @param int $pageid */ function wiki_get_linked_to_pages($pageid) { global $DB; return $DB->get_records('wiki_links', array('frompageid' => $pageid)); } /** * Get linked from page * @param int $pageid */ function wiki_get_linked_from_pages($pageid) { global $DB; return $DB->get_records('wiki_links', array('topageid' => $pageid)); } /** * Get pages which user have been edited * @param int $swid * @param int $userid */ function wiki_get_contributions($swid, $userid) { global $DB; $sql = "SELECT v.* FROM {wiki_versions} v, {wiki_pages} p WHERE p.subwikiid = ? AND v.pageid = p.id AND v.userid = ?"; return $DB->get_records_sql($sql, array($swid, $userid)); } /** * Get missing or empty pages in wiki * @param int $swid sub wiki id */ function wiki_get_missing_or_empty_pages($swid) { global $DB; $sql = "SELECT DISTINCT p.title, p.id, p.subwikiid FROM {wiki} w, {wiki_subwikis} s, {wiki_pages} p WHERE s.wikiid = w.id and s.id = ? and w.firstpagetitle != p.title and p.subwikiid = ? and 1 = (SELECT count(*) FROM {wiki_versions} v WHERE v.pageid = p.id) UNION SELECT DISTINCT l.tomissingpage as title, 0 as id, l.subwikiid FROM {wiki_links} l WHERE l.subwikiid = ? and l.topageid = 0"; return $DB->get_records_sql($sql, array($swid, $swid, $swid)); } /** * Get pages list in wiki * @param int $swid sub wiki id * @param string $sort How to sort the pages. By default, title ASC. * @return stdClass[] */ function wiki_get_page_list($swid, $sort = 'title ASC') { global $DB; $records = $DB->get_records('wiki_pages', array('subwikiid' => $swid), $sort); return $records; } /** * Return a list of orphaned wikis for one specific subwiki * @global object * @param int $swid sub wiki id */ function wiki_get_orphaned_pages($swid) { global $DB; $sql = "SELECT p.id, p.title FROM {wiki_pages} p, {wiki} w , {wiki_subwikis} s WHERE p.subwikiid = ? AND s.id = ? AND w.id = s.wikiid AND p.title != w.firstpagetitle AND p.id NOT IN (SELECT topageid FROM {wiki_links} WHERE subwikiid = ?)"; return $DB->get_records_sql($sql, array($swid, $swid, $swid)); } /** * Search wiki title * @param int $swid sub wiki id * @param string $search */ function wiki_search_title($swid, $search) { global $DB; return $DB->get_records_select('wiki_pages', "subwikiid = ? AND title LIKE ?", array($swid, '%'.$search.'%')); } /** * Search wiki content * @param int $swid sub wiki id * @param string $search */ function wiki_search_content($swid, $search) { global $DB; return $DB->get_records_select('wiki_pages', "subwikiid = ? AND cachedcontent LIKE ?", array($swid, '%'.$search.'%')); } /** * Search wiki title and content * @param int $swid sub wiki id * @param string $search */ function wiki_search_all($swid, $search) { global $DB; return $DB->get_records_select('wiki_pages', "subwikiid = ? AND (cachedcontent LIKE ? OR title LIKE ?)", array($swid, '%'.$search.'%', '%'.$search.'%')); } /** * Get user data */ function wiki_get_user_info($userid) { global $DB; return $DB->get_record('user', array('id' => $userid)); } /** * Increase page view nubmer * @param int $page, database record */ function wiki_increment_pageviews($page) { global $DB; $page->pageviews++; $DB->update_record('wiki_pages', $page); } //---------------------------------------------------------- //---------------------------------------------------------- /** * Text format supported by wiki module */ function wiki_get_formats() { return array('html', 'creole', 'nwiki'); } /** * Parses a string with the wiki markup language in $markup. * * @return Array or false when something wrong has happened. * * Returned array contains the following fields: * 'parsed_text' => String. Contains the parsed wiki content. * 'unparsed_text' => String. Constains the original wiki content. * 'link_count' => Array of array('destination' => ..., 'new' => "is new?"). Contains the internal wiki links found in the wiki content. * 'deleted_sections' => the list of deleted sections. * '' => * * @author Josep Arús Pous **/ function wiki_parse_content($markup, $pagecontent, $options = array()) { global $PAGE; $subwiki = wiki_get_subwiki($options['swid']); $cm = get_coursemodule_from_instance("wiki", $subwiki->wikiid); $context = context_module::instance($cm->id); $parser_options = array( 'link_callback' => '/mod/wiki/locallib.php:wiki_parser_link', 'link_callback_args' => array('swid' => $options['swid']), 'table_callback' => '/mod/wiki/locallib.php:wiki_parser_table', 'real_path_callback' => '/mod/wiki/locallib.php:wiki_parser_real_path', 'real_path_callback_args' => array( 'context' => $context, 'component' => 'mod_wiki', 'filearea' => 'attachments', 'subwikiid'=> $subwiki->id, 'pageid' => $options['pageid'] ), 'pageid' => $options['pageid'], 'pretty_print' => (isset($options['pretty_print']) && $options['pretty_print']), 'printable' => (isset($options['printable']) && $options['printable']) ); return wiki_parser_proxy::parse($pagecontent, $markup, $parser_options); } /** * This function is the parser callback to parse wiki links. * * It returns the necesary information to print a link. * * NOTE: Empty pages and non-existent pages must be print in red color. * * !!!!!! IMPORTANT !!!!!! * It is critical that you call format_string on the content before it is used. * * @param string|stdClass $link name of a page or page object. * @param array $options * @return array Array('content' => string, 'url' => string, 'new' => bool, 'link_info' => array) * * @TODO Doc return and options */ function wiki_parser_link($link, $options = null) { global $CFG; if (is_object($link)) { $parsedlink = array('content' => $link->title, 'url' => $CFG->wwwroot . '/mod/wiki/view.php?pageid=' . $link->id, 'new' => false, 'link_info' => array('link' => $link->title, 'pageid' => $link->id, 'new' => false)); $version = wiki_get_current_version($link->id); if ($version->version == 0) { $parsedlink['new'] = true; } return $parsedlink; } else { $swid = $options['swid']; if ($page = wiki_get_page_by_title($swid, $link)) { $parsedlink = array('content' => $link, 'url' => $CFG->wwwroot . '/mod/wiki/view.php?pageid=' . $page->id, 'new' => false, 'link_info' => array('link' => $link, 'pageid' => $page->id, 'new' => false)); $version = wiki_get_current_version($page->id); if ($version->version == 0) { $parsedlink['new'] = true; } return $parsedlink; } else { return array('content' => $link, 'url' => $CFG->wwwroot . '/mod/wiki/create.php?swid=' . $swid . '&title=' . urlencode($link) . '&action=new', 'new' => true, 'link_info' => array('link' => $link, 'new' => true, 'pageid' => 0)); } } } /** * Returns the table fully parsed (HTML) * * @return HTML for the table $table * @author Josep Arús Pous * **/ function wiki_parser_table($table) { global $OUTPUT; $htmltable = new html_table(); $headers = $table[0]; $htmltable->head = array(); foreach ($headers as $h) { $htmltable->head[] = $h[1]; } array_shift($table); $htmltable->data = array(); foreach ($table as $row) { $row_data = array(); foreach ($row as $r) { $row_data[] = $r[1]; } $htmltable->data[] = $row_data; } return html_writer::table($htmltable); } /** * Returns an absolute path link, unless there is no such link. * * @param string $url Link's URL or filename * @param stdClass $context filearea params * @param string $component The component the file is associated with * @param string $filearea The filearea the file is stored in * @param int $swid Sub wiki id * * @return string URL for files full path */ function wiki_parser_real_path($url, $context, $component, $filearea, $swid) { global $CFG; if (preg_match("/^(?:http|ftp)s?\:\/\//", $url)) { return $url; } else { $file = 'pluginfile.php'; if (!$CFG->slasharguments) { $file = $file . '?file='; } $baseurl = "$CFG->wwwroot/$file/{$context->id}/$component/$filearea/$swid/"; // it is a file in current file area return $baseurl . $url; } } /** * Returns the token used by a wiki language to represent a given tag or "object" (bold -> **) * * @return A string when it has only one token at the beginning (f. ex. lists). An array composed by 2 strings when it has 2 tokens, one at the beginning and one at the end (f. ex. italics). Returns false otherwise. * @author Josep Arús Pous **/ function wiki_parser_get_token($markup, $name) { return wiki_parser_proxy::get_token($name, $markup); } /** * Checks if current user can view a subwiki * * @param stdClass $subwiki usually record from {wiki_subwikis}. Must contain fields 'wikiid', 'groupid', 'userid'. * If it also contains fields 'course' and 'groupmode' from table {wiki} it will save extra DB query. * @param stdClass $wiki optional wiki object if known * @return bool */ function wiki_user_can_view($subwiki, $wiki = null) { global $USER; if (empty($wiki) || $wiki->id != $subwiki->wikiid) { $wiki = wiki_get_wiki($subwiki->wikiid); } $modinfo = get_fast_modinfo($wiki->course); if (!isset($modinfo->instances['wiki'][$subwiki->wikiid])) { // Module does not exist. return false; } $cm = $modinfo->instances['wiki'][$subwiki->wikiid]; if (!$cm->uservisible) { // The whole module is not visible to the current user. return false; } $context = context_module::instance($cm->id); // Working depending on activity groupmode switch (groups_get_activity_groupmode($cm)) { case NOGROUPS: if ($wiki->wikimode == 'collaborative') { // Collaborative Mode: // There is one wiki for all the class. // // Only view capbility needed return has_capability('mod/wiki:viewpage', $context); } else if ($wiki->wikimode == 'individual') { // Individual Mode: // Each person owns a wiki. if ($subwiki->userid == $USER->id) { // Only the owner of the wiki can view it return has_capability('mod/wiki:viewpage', $context); } else { // User has special capabilities // User must have: // mod/wiki:viewpage capability // and // mod/wiki:managewiki capability $view = has_capability('mod/wiki:viewpage', $context); $manage = has_capability('mod/wiki:managewiki', $context); return $view && $manage; } } else { //Error return false; } case SEPARATEGROUPS: // Collaborative and Individual Mode // // Collaborative Mode: // There is one wiki per group. // Individual Mode: // Each person owns a wiki. if ($wiki->wikimode == 'collaborative' || $wiki->wikimode == 'individual') { // Only members of subwiki group could view that wiki if (in_array($subwiki->groupid, $modinfo->get_groups($cm->groupingid))) { // Only view capability needed return has_capability('mod/wiki:viewpage', $context); } else { // User is not part of that group // User must have: // mod/wiki:managewiki capability // or // moodle/site:accessallgroups capability // and // mod/wiki:viewpage capability $view = has_capability('mod/wiki:viewpage', $context); $manage = has_capability('mod/wiki:managewiki', $context); $access = has_capability('moodle/site:accessallgroups', $context); return ($manage || $access) && $view; } } else { //Error return false; } case VISIBLEGROUPS: // Collaborative and Individual Mode // // Collaborative Mode: // There is one wiki per group. // Individual Mode: // Each person owns a wiki. if ($wiki->wikimode == 'collaborative' || $wiki->wikimode == 'individual') { // Everybody can read all wikis // // Only view capability needed return has_capability('mod/wiki:viewpage', $context); } else { //Error return false; } default: // Error return false; } } /** * Checks if current user can edit a subwiki * * @param $subwiki */ function wiki_user_can_edit($subwiki) { global $USER; $wiki = wiki_get_wiki($subwiki->wikiid); $cm = get_coursemodule_from_instance('wiki', $wiki->id); $context = context_module::instance($cm->id); // Working depending on activity groupmode switch (groups_get_activity_groupmode($cm)) { case NOGROUPS: if ($wiki->wikimode == 'collaborative') { // Collaborative Mode: // There is a wiki for all the class. // // Only edit capbility needed return has_capability('mod/wiki:editpage', $context); } else if ($wiki->wikimode == 'individual') { // Individual Mode // There is a wiki per user // Only the owner of that wiki can edit it if ($subwiki->userid == $USER->id) { return has_capability('mod/wiki:editpage', $context); } else { // Current user is not the owner of that wiki. // User must have: // mod/wiki:editpage capability // and // mod/wiki:managewiki capability $edit = has_capability('mod/wiki:editpage', $context); $manage = has_capability('mod/wiki:managewiki', $context); return $edit && $manage; } } else { //Error return false; } case SEPARATEGROUPS: if ($wiki->wikimode == 'collaborative') { // Collaborative Mode: // There is one wiki per group. // // Only members of subwiki group could edit that wiki if (groups_is_member($subwiki->groupid)) { // Only edit capability needed return has_capability('mod/wiki:editpage', $context); } else { // User is not part of that group // User must have: // mod/wiki:managewiki capability // and // moodle/site:accessallgroups capability // and // mod/wiki:editpage capability $manage = has_capability('mod/wiki:managewiki', $context); $access = has_capability('moodle/site:accessallgroups', $context); $edit = has_capability('mod/wiki:editpage', $context); return $manage && $access && $edit; } } else if ($wiki->wikimode == 'individual') { // Individual Mode: // Each person owns a wiki. // // Only the owner of that wiki can edit it if ($subwiki->userid == $USER->id) { return has_capability('mod/wiki:editpage', $context); } else { // Current user is not the owner of that wiki. // User must have: // mod/wiki:managewiki capability // and // moodle/site:accessallgroups capability // and // mod/wiki:editpage capability $manage = has_capability('mod/wiki:managewiki', $context); $access = has_capability('moodle/site:accessallgroups', $context); $edit = has_capability('mod/wiki:editpage', $context); return $manage && $access && $edit; } } else { //Error return false; } case VISIBLEGROUPS: if ($wiki->wikimode == 'collaborative') { // Collaborative Mode: // There is one wiki per group. // // Only members of subwiki group could edit that wiki if (groups_is_member($subwiki->groupid)) { // Only edit capability needed return has_capability('mod/wiki:editpage', $context); } else { // User is not part of that group // User must have: // mod/wiki:managewiki capability // and // mod/wiki:editpage capability $manage = has_capability('mod/wiki:managewiki', $context); $edit = has_capability('mod/wiki:editpage', $context); return $manage && $edit; } } else if ($wiki->wikimode == 'individual') { // Individual Mode: // Each person owns a wiki. // // Only the owner of that wiki can edit it if ($subwiki->userid == $USER->id) { return has_capability('mod/wiki:editpage', $context); } else { // Current user is not the owner of that wiki. // User must have: // mod/wiki:managewiki capability // and // mod/wiki:editpage capability $manage = has_capability('mod/wiki:managewiki', $context); $edit = has_capability('mod/wiki:editpage', $context); return $manage && $edit; } } else { //Error return false; } default: // Error return false; } } //---------------- // Locks //---------------- /** * Checks if a page-section is locked. * * @return true if the combination of section and page is locked, FALSE otherwise. */ function wiki_is_page_section_locked($pageid, $userid, $section = null) { global $DB; $sql = "pageid = ? AND lockedat > ? AND userid != ?"; $params = array($pageid, time(), $userid); if (!empty($section)) { $sql .= " AND (sectionname = ? OR sectionname IS null)"; $params[] = $section; } return $DB->record_exists_select('wiki_locks', $sql, $params); } /** * Inserts or updates a wiki_locks record. */ function wiki_set_lock($pageid, $userid, $section = null, $insert = false) { global $DB; if (wiki_is_page_section_locked($pageid, $userid, $section)) { return false; } $params = array('pageid' => $pageid, 'userid' => $userid, 'sectionname' => $section); $lock = $DB->get_record('wiki_locks', $params); if (!empty($lock)) { $DB->update_record('wiki_locks', array('id' => $lock->id, 'lockedat' => time() + LOCK_TIMEOUT)); } else if ($insert) { $DB->insert_record('wiki_locks', array('pageid' => $pageid, 'sectionname' => $section, 'userid' => $userid, 'lockedat' => time() + LOCK_TIMEOUT)); } return true; } /** * Deletes wiki_locks that are not in use. (F.Ex. after submitting the changes). If no userid is present, it deletes ALL the wiki_locks of a specific page. * * @param int $pageid page id. * @param int $userid id of user for which lock is deleted. * @param string $section section to be deleted. * @param bool $delete_from_db deleted from db. * @param bool $delete_section_and_page delete section and page version. */ function wiki_delete_locks($pageid, $userid = null, $section = null, $delete_from_db = true, $delete_section_and_page = false) { global $DB; $wiki = wiki_get_wiki_from_pageid($pageid); $cm = get_coursemodule_from_instance('wiki', $wiki->id); $context = context_module::instance($cm->id); $params = array('pageid' => $pageid); if (!empty($userid)) { $params['userid'] = $userid; } if (!empty($section)) { $params['sectionname'] = $section; } if ($delete_from_db) { $DB->delete_records('wiki_locks', $params); if ($delete_section_and_page && !empty($section)) { $params['sectionname'] = null; $DB->delete_records('wiki_locks', $params); } $event = \mod_wiki\event\page_locks_deleted::create( array( 'context' => $context, 'objectid' => $pageid, 'relateduserid' => $userid, 'other' => array( 'section' => $section ) )); // No need to add snapshot, as important data is section, userid and pageid, which is part of event. $event->trigger(); } else { $DB->set_field('wiki_locks', 'lockedat', time(), $params); } } /** * Deletes wiki_locks that expired 1 hour ago. */ function wiki_delete_old_locks() { global $DB; $DB->delete_records_select('wiki_locks', "lockedat < ?", array(time() - 3600)); } /** * Deletes wiki_links. It can be sepecific link or links attached in subwiki * * @global mixed $DB database object * @param int $linkid id of the link to be deleted * @param int $topageid links to the specific page * @param int $frompageid links from specific page * @param int $subwikiid links to subwiki */ function wiki_delete_links($linkid = null, $topageid = null, $frompageid = null, $subwikiid = null) { global $DB; $params = array(); // if link id is givien then don't check for anything else if (!empty($linkid)) { $params['id'] = $linkid; } else { if (!empty($topageid)) { $params['topageid'] = $topageid; } if (!empty($frompageid)) { $params['frompageid'] = $frompageid; } if (!empty($subwikiid)) { $params['subwikiid'] = $subwikiid; } } //Delete links if any params are passed, else nothing to delete. if (!empty($params)) { $DB->delete_records('wiki_links', $params); } } /** * Delete wiki synonyms related to subwikiid or page * * @param int $subwikiid id of sunbwiki * @param int $pageid id of page */ function wiki_delete_synonym($subwikiid, $pageid = null) { global $DB; $params = array('subwikiid' => $subwikiid); if (!is_null($pageid)) { $params['pageid'] = $pageid; } $DB->delete_records('wiki_synonyms', $params, IGNORE_MISSING); } /** * Delete pages and all related data * * @param mixed $context context in which page needs to be deleted. * @param mixed $pageids id's of pages to be deleted * @param int $subwikiid id of the subwiki for which all pages should be deleted */ function wiki_delete_pages($context, $pageids = null, $subwikiid = null) { global $DB, $CFG; if (!empty($pageids) && is_int($pageids)) { $pageids = array($pageids); } else if (!empty($subwikiid)) { $pageids = wiki_get_page_list($subwikiid); } //If there is no pageid then return as we can't delete anything. if (empty($pageids)) { return; } /// Delete page and all it's relevent data foreach ($pageids as $pageid) { if (is_object($pageid)) { $pageid = $pageid->id; } //Delete page comments $comments = wiki_get_comments($context->id, $pageid); foreach ($comments as $commentid => $commentvalue) { wiki_delete_comment($commentid, $context, $pageid); } //Delete page tags core_tag_tag::remove_all_item_tags('mod_wiki', 'wiki_pages', $pageid); //Delete Synonym wiki_delete_synonym($subwikiid, $pageid); //Delete all page versions wiki_delete_page_versions(array($pageid=>array(0)), $context); //Delete all page locks wiki_delete_locks($pageid); //Delete all page links wiki_delete_links(null, $pageid); $params = array('id' => $pageid); // Get page before deleting. $page = $DB->get_record('wiki_pages', $params); //Delete page $DB->delete_records('wiki_pages', $params); // Trigger page_deleted event. $event = \mod_wiki\event\page_deleted::create( array( 'context' => $context, 'objectid' => $pageid, 'other' => array('subwikiid' => $subwikiid) )); $event->add_record_snapshot('wiki_pages', $page); $event->trigger(); } } /** * Delete specificed versions of a page or versions created by users * if version is 0 then it will remove all versions of the page * * @param array $deleteversions delete versions for a page * @param context_module $context module context */ function wiki_delete_page_versions($deleteversions, $context = null) { global $DB; /// delete page-versions foreach ($deleteversions as $id => $versions) { $params = array('pageid' => $id); if (is_null($context)) { $wiki = wiki_get_wiki_from_pageid($id); $cm = get_coursemodule_from_instance('wiki', $wiki->id); $context = context_module::instance($cm->id); } // Delete all versions, if version specified is 0. if (in_array(0, $versions)) { $oldversions = $DB->get_records('wiki_versions', $params); $DB->delete_records('wiki_versions', $params, IGNORE_MISSING); } else { list($insql, $param) = $DB->get_in_or_equal($versions); $insql .= ' AND pageid = ?'; array_push($param, $params['pageid']); $oldversions = $DB->get_records_select('wiki_versions', 'version ' . $insql, $param); $DB->delete_records_select('wiki_versions', 'version ' . $insql, $param); } foreach ($oldversions as $version) { // Trigger page version deleted event. $event = \mod_wiki\event\page_version_deleted::create( array( 'context' => $context, 'objectid' => $version->id, 'other' => array( 'pageid' => $id ) )); $event->add_record_snapshot('wiki_versions', $version); $event->trigger(); } } } function wiki_get_comment($commentid){ global $DB; return $DB->get_record('comments', array('id' => $commentid)); } /** * Returns all comments by context and pageid * * @param int $contextid Current context id * @param int $pageid Current pageid **/ function wiki_get_comments($contextid, $pageid) { global $DB; return $DB->get_records('comments', array('contextid' => $contextid, 'itemid' => $pageid, 'commentarea' => 'wiki_page'), 'timecreated ASC'); } /** * Add comments ro database * * @param object $context. Current context * @param int $pageid. Current pageid * @param string $content. Content of the comment * @param string editor. Version of editor we are using. **/ function wiki_add_comment($context, $pageid, $content, $editor) { global $CFG; require_once($CFG->dirroot . '/comment/lib.php'); list($context, $course, $cm) = get_context_info_array($context->id); $cmt = new stdclass(); $cmt->context = $context; $cmt->itemid = $pageid; $cmt->area = 'wiki_page'; $cmt->course = $course; $cmt->component = 'mod_wiki'; $manager = new comment($cmt); if ($editor == 'creole') { $manager->add($content, FORMAT_CREOLE); } else if ($editor == 'html') { $manager->add($content, FORMAT_HTML); } else if ($editor == 'nwiki') { $manager->add($content, FORMAT_NWIKI); } } /** * Delete comments from database * * @param $idcomment. Id of comment which will be deleted * @param $context. Current context * @param $pageid. Current pageid **/ function wiki_delete_comment($idcomment, $context, $pageid) { global $CFG; require_once($CFG->dirroot . '/comment/lib.php'); list($context, $course, $cm) = get_context_info_array($context->id); $cmt = new stdClass(); $cmt->context = $context; $cmt->itemid = $pageid; $cmt->area = 'wiki_page'; $cmt->course = $course; $cmt->component = 'mod_wiki'; $manager = new comment($cmt); $manager->delete($idcomment); } /** * Delete al comments from wiki * **/ function wiki_delete_comments_wiki() { global $PAGE, $DB; $cm = $PAGE->cm; $context = context_module::instance($cm->id); $table = 'comments'; $select = 'contextid = ?'; $DB->delete_records_select($table, $select, array($context->id)); } function wiki_add_progress($pageid, $oldversionid, $versionid, $progress) { global $DB; for ($v = $oldversionid + 1; $v <= $versionid; $v++) { $user = wiki_get_wiki_page_id($pageid, $v); $DB->insert_record('wiki_progress', array('userid' => $user->userid, 'pageid' => $pageid, 'versionid' => $v, 'progress' => $progress)); } } function wiki_get_wiki_page_id($pageid, $id) { global $DB; return $DB->get_record('wiki_versions', array('pageid' => $pageid, 'id' => $id)); } function wiki_print_page_content($page, $context, $subwikiid) { global $OUTPUT, $CFG; if ($page->timerendered + WIKI_REFRESH_CACHE_TIME < time()) { $content = wiki_refresh_cachedcontent($page); $page = $content['page']; } if (isset($content)) { $box = ''; foreach ($content['sections'] as $s) { $box .= '

' . get_string('repeatedsection', 'wiki', $s) . '

'; } if (!empty($box)) { echo $OUTPUT->box($box); } } $html = file_rewrite_pluginfile_urls($page->cachedcontent, 'pluginfile.php', $context->id, 'mod_wiki', 'attachments', $subwikiid); $html = format_text($html, FORMAT_HTML, array('overflowdiv' => true, 'allowid' => true)); echo $OUTPUT->box($html); echo $OUTPUT->tag_list(core_tag_tag::get_item_tags('mod_wiki', 'wiki_pages', $page->id), null, 'wiki-tags'); wiki_increment_pageviews($page); } /** * This function trims any given text and returns it with some dots at the end * * @param string $text * @param string $limit * * @return string */ function wiki_trim_string($text, $limit = 25) { if (core_text::strlen($text) > $limit) { $text = core_text::substr($text, 0, $limit) . '...'; } return $text; } /** * Prints default edit form fields and buttons * * @param string $format Edit form format (html, creole...) * @param integer $version Version number. A negative number means no versioning. */ function wiki_print_edit_form_default_fields($format, $pageid, $version = -1, $upload = false, $deleteuploads = array()) { global $CFG, $PAGE, $OUTPUT; echo ''; if ($version >= 0) { echo ''; } echo ''; //attachments require_once($CFG->dirroot . '/lib/form/filemanager.php'); $filemanager = new MoodleQuickForm_filemanager('attachments', get_string('wikiattachments', 'wiki'), array('id' => 'attachments'), array('subdirs' => false, 'maxfiles' => 99, 'maxbytes' => $CFG->maxbytes)); $value = file_get_submitted_draft_itemid('attachments'); if (!empty($value) && !$upload) { $filemanager->setValue($value); } echo "
" . get_string("uploadtitle", 'wiki') . ""; echo $OUTPUT->container_start('container'); print $filemanager->toHtml(); echo $OUTPUT->container_end(); $cm = $PAGE->cm; $context = context_module::instance($cm->id); echo $OUTPUT->container_start('container wiki-upload-table'); wiki_print_upload_table($context, 'wiki_upload', $pageid, $deleteuploads); echo $OUTPUT->container_end(); echo "
"; echo ''; echo ''; echo ''; echo ''; } /** * Prints a table with the files attached to a wiki page * @param object $context * @param string $filearea * @param int $fileitemid * @param array deleteuploads */ function wiki_print_upload_table($context, $filearea, $fileitemid, $deleteuploads = array()) { global $CFG, $OUTPUT; $htmltable = new html_table(); $htmltable->head = array(get_string('deleteupload', 'wiki'), get_string('uploadname', 'wiki'), get_string('uploadactions', 'wiki')); $fs = get_file_storage(); $files = $fs->get_area_files($context->id, 'mod_wiki', $filearea, $fileitemid); //TODO: this is weird (skodak) foreach ($files as $file) { if (!$file->is_directory()) { $checkbox = 'get_pathnamehash(), $deleteuploads)) { $checkbox .= ' checked="checked"'; } $checkbox .= " />"; $htmltable->data[] = array($checkbox, '' . $file->get_filename() . '', ""); } } print '

' . get_string('uploadfiletitle', 'wiki') . "

"; print html_writer::table($htmltable); } /** * Generate wiki's page tree * * @param page_wiki $page. A wiki page object * @param navigation_node $node. Starting navigation_node * @param array $keys. An array to store keys * @return an array with all tree nodes */ function wiki_build_tree($page, $node, &$keys) { $content = array(); static $icon = null; if ($icon === null) { // Substitute the default navigation icon with empty image. $icon = new pix_icon('spacer', ''); } $pages = wiki_get_linked_pages($page->id); foreach ($pages as $p) { $key = $page->id . ':' . $p->id; if (in_array($key, $keys)) { break; } array_push($keys, $key); $l = wiki_parser_link($p); $link = new moodle_url('/mod/wiki/view.php', array('pageid' => $p->id)); // navigation_node::get_content will format the title for us $nodeaux = $node->add($p->title, $link, null, null, null, $icon); if ($l['new']) { $nodeaux->add_class('wiki_newentry'); } wiki_build_tree($p, $nodeaux, $keys); } $content[] = $node; return $content; } /** * Get linked pages from page * @param int $pageid */ function wiki_get_linked_pages($pageid) { global $DB; $sql = "SELECT p.id, p.title FROM {wiki_pages} p JOIN {wiki_links} l ON l.topageid = p.id WHERE l.frompageid = ? ORDER BY p.title ASC"; return $DB->get_records_sql($sql, array($pageid)); } /** * Get updated pages from wiki * @param int $pageid */ function wiki_get_updated_pages_by_subwiki($swid) { global $DB, $USER; $sql = "SELECT * FROM {wiki_pages} WHERE subwikiid = ? AND timemodified > ? ORDER BY timemodified DESC"; return $DB->get_records_sql($sql, array($swid, $USER->lastlogin)); } /** * Check if the user can create pages in a certain wiki. * @param context $context Wiki's context. * @param integer|stdClass $user A user id or object. By default (null) checks the permissions of the current user. * @return bool True if user can create pages, false otherwise. * @since Moodle 3.1 */ function wiki_can_create_pages($context, $user = null) { return has_capability('mod/wiki:createpage', $context, $user); } /** * Get a sub wiki instance by wiki id, group id and user id. * If the wiki doesn't exist in DB it will return an isntance with id -1. * * @param int $wikiid Wiki ID. * @param int $groupid Group ID. * @param int $userid User ID. * @return object Subwiki instance. * @since Moodle 3.1 */ function wiki_get_possible_subwiki_by_group($wikiid, $groupid, $userid = 0) { if (!$subwiki = wiki_get_subwiki_by_group($wikiid, $groupid, $userid)) { $subwiki = new stdClass(); $subwiki->id = -1; $subwiki->wikiid = $wikiid; $subwiki->groupid = $groupid; $subwiki->userid = $userid; } return $subwiki; } /** * Get all the possible subwikis visible to the user in a wiki. * It will return all the subwikis that can be created in a wiki, even if they don't exist in DB yet. * * @param stdClass $wiki Wiki to get the subwikis from. * @param cm_info|stdClass $cm Optional. The course module object. * @param context_module $context Optional. Context of wiki module. * @return array List of subwikis. * @since Moodle 3.1 */ function wiki_get_visible_subwikis($wiki, $cm = null, $context = null) { global $USER; $subwikis = array(); if (empty($wiki) or !is_object($wiki)) { // Wiki not valid. return $subwikis; } if (empty($cm)) { $cm = get_coursemodule_from_instance('wiki', $wiki->id); } if (empty($context)) { $context = context_module::instance($cm->id); } if (!has_capability('mod/wiki:viewpage', $context)) { return $subwikis; } $manage = has_capability('mod/wiki:managewiki', $context); if (!$groupmode = groups_get_activity_groupmode($cm)) { // No groups. if ($wiki->wikimode == 'collaborative') { // Only 1 subwiki. $subwikis[] = wiki_get_possible_subwiki_by_group($wiki->id, 0, 0); } else if ($wiki->wikimode == 'individual') { // There's 1 subwiki per user. if ($manage) { // User can view all subwikis. $users = get_enrolled_users($context); foreach ($users as $user) { $subwikis[] = wiki_get_possible_subwiki_by_group($wiki->id, 0, $user->id); } } else { // User can only see his subwiki. $subwikis[] = wiki_get_possible_subwiki_by_group($wiki->id, 0, $USER->id); } } } else { if ($wiki->wikimode == 'collaborative') { // 1 subwiki per group. $aag = has_capability('moodle/site:accessallgroups', $context); if ($aag || $groupmode == VISIBLEGROUPS) { // User can see all groups. $allowedgroups = groups_get_all_groups($cm->course, 0, $cm->groupingid); $allparticipants = new stdClass(); $allparticipants->id = 0; array_unshift($allowedgroups, $allparticipants); // Add all participants. } else { // User can only see the groups he belongs to. $allowedgroups = groups_get_all_groups($cm->course, $USER->id, $cm->groupingid); } foreach ($allowedgroups as $group) { $subwikis[] = wiki_get_possible_subwiki_by_group($wiki->id, $group->id, 0); } } else if ($wiki->wikimode == 'individual') { // 1 subwiki per user and group. if ($manage || $groupmode == VISIBLEGROUPS) { // User can view all subwikis. $users = get_enrolled_users($context); foreach ($users as $user) { // Get all the groups this user belongs to. $groups = groups_get_all_groups($cm->course, $user->id); if (!empty($groups)) { foreach ($groups as $group) { $subwikis[] = wiki_get_possible_subwiki_by_group($wiki->id, $group->id, $user->id); } } else { // User doesn't belong to any group, add it to group 0. $subwikis[] = wiki_get_possible_subwiki_by_group($wiki->id, 0, $user->id); } } } else { // The user can only see the subwikis of the groups he belongs. $allowedgroups = groups_get_all_groups($cm->course, $USER->id, $cm->groupingid); foreach ($allowedgroups as $group) { $users = groups_get_members($group->id); foreach ($users as $user) { $subwikis[] = wiki_get_possible_subwiki_by_group($wiki->id, $group->id, $user->id); } } } } } return $subwikis; } /** * Utility function for getting a subwiki by group and user, validating that the user can view it. * If the subwiki doesn't exists in DB yet it'll have id -1. * * @param stdClass $wiki The wiki. * @param int $groupid Group ID. 0 means the subwiki doesn't use groups. * @param int $userid User ID. 0 means the subwiki doesn't use users. * @return stdClass Subwiki. If it doesn't exists in DB yet it'll have id -1. If the user can't view the * subwiki this function will return false. * @since Moodle 3.1 * @throws moodle_exception */ function wiki_get_subwiki_by_group_and_user_with_validation($wiki, $groupid, $userid) { global $USER, $DB; // Get subwiki based on group and user. if (!$subwiki = wiki_get_subwiki_by_group($wiki->id, $groupid, $userid)) { // The subwiki doesn't exist. // Validate if user is valid. if ($userid != 0) { $user = core_user::get_user($userid, '*', MUST_EXIST); core_user::require_active_user($user); } // Validate that groupid is valid. if ($groupid != 0 && !groups_group_exists($groupid)) { throw new moodle_exception('cannotfindgroup', 'error'); } // Valid data but subwiki not found. We'll simulate a subwiki object to check if the user would be able to see it // if it existed. If he's able to see it then we'll return an empty array because the subwiki has no pages. $subwiki = new stdClass(); $subwiki->id = -1; $subwiki->wikiid = $wiki->id; $subwiki->userid = $userid; $subwiki->groupid = $groupid; } // Check that the user can view the subwiki. This function checks capabilities. if (!wiki_user_can_view($subwiki, $wiki)) { return false; } return $subwiki; } /** * Returns wiki pages tagged with a specified tag. * * This is a callback used by the tag area mod_wiki/wiki_pages to search for wiki pages * tagged with a specific tag. * * @param core_tag_tag $tag * @param bool $exclusivemode if set to true it means that no other entities tagged with this tag * are displayed on the page and the per-page limit may be bigger * @param int $fromctx context id where the link was displayed, may be used by callbacks * to display items in the same context first * @param int $ctx context id where to search for records * @param bool $rec search in subcontexts as well * @param int $page 0-based number of page being displayed * @return \core_tag\output\tagindex */ function mod_wiki_get_tagged_pages($tag, $exclusivemode = false, $fromctx = 0, $ctx = 0, $rec = 1, $page = 0) { global $OUTPUT; $perpage = $exclusivemode ? 20 : 5; // Build the SQL query. $ctxselect = context_helper::get_preload_record_columns_sql('ctx'); $query = "SELECT wp.id, wp.title, ws.userid, ws.wikiid, ws.id AS subwikiid, ws.groupid, w.wikimode, cm.id AS cmid, c.id AS courseid, c.shortname, c.fullname, $ctxselect FROM {wiki_pages} wp JOIN {wiki_subwikis} ws ON wp.subwikiid = ws.id JOIN {wiki} w ON w.id = ws.wikiid JOIN {modules} m ON m.name='wiki' JOIN {course_modules} cm ON cm.module = m.id AND cm.instance = w.id JOIN {tag_instance} tt ON wp.id = tt.itemid JOIN {course} c ON cm.course = c.id JOIN {context} ctx ON ctx.instanceid = cm.id AND ctx.contextlevel = :coursemodulecontextlevel WHERE tt.itemtype = :itemtype AND tt.tagid = :tagid AND tt.component = :component AND cm.deletioninprogress = 0 AND wp.id %ITEMFILTER% AND c.id %COURSEFILTER%"; $params = array('itemtype' => 'wiki_pages', 'tagid' => $tag->id, 'component' => 'mod_wiki', 'coursemodulecontextlevel' => CONTEXT_MODULE); if ($ctx) { $context = $ctx ? context::instance_by_id($ctx) : context_system::instance(); $query .= $rec ? ' AND (ctx.id = :contextid OR ctx.path LIKE :path)' : ' AND ctx.id = :contextid'; $params['contextid'] = $context->id; $params['path'] = $context->path.'/%'; } $query .= " ORDER BY "; if ($fromctx) { // In order-clause specify that modules from inside "fromctx" context should be returned first. $fromcontext = context::instance_by_id($fromctx); $query .= ' (CASE WHEN ctx.id = :fromcontextid OR ctx.path LIKE :frompath THEN 0 ELSE 1 END),'; $params['fromcontextid'] = $fromcontext->id; $params['frompath'] = $fromcontext->path.'/%'; } $query .= ' c.sortorder, cm.id, wp.id'; $totalpages = $page + 1; // Use core_tag_index_builder to build and filter the list of items. $builder = new core_tag_index_builder('mod_wiki', 'wiki_pages', $query, $params, $page * $perpage, $perpage + 1); while ($item = $builder->has_item_that_needs_access_check()) { context_helper::preload_from_record($item); $courseid = $item->courseid; if (!$builder->can_access_course($courseid)) { $builder->set_accessible($item, false); continue; } $modinfo = get_fast_modinfo($builder->get_course($courseid)); // Set accessibility of this item and all other items in the same course. $builder->walk(function ($taggeditem) use ($courseid, $modinfo, $builder) { if ($taggeditem->courseid == $courseid) { $accessible = false; if (($cm = $modinfo->get_cm($taggeditem->cmid)) && $cm->uservisible) { $subwiki = (object)array('id' => $taggeditem->subwikiid, 'groupid' => $taggeditem->groupid, 'userid' => $taggeditem->userid, 'wikiid' => $taggeditem->wikiid); $wiki = (object)array('id' => $taggeditem->wikiid, 'wikimode' => $taggeditem->wikimode, 'course' => $cm->course); $accessible = wiki_user_can_view($subwiki, $wiki); } $builder->set_accessible($taggeditem, $accessible); } }); } $items = $builder->get_items(); if (count($items) > $perpage) { $totalpages = $page + 2; // We don't need exact page count, just indicate that the next page exists. array_pop($items); } // Build the display contents. if ($items) { $tagfeed = new core_tag\output\tagfeed(); foreach ($items as $item) { context_helper::preload_from_record($item); $modinfo = get_fast_modinfo($item->courseid); $cm = $modinfo->get_cm($item->cmid); $pageurl = new moodle_url('/mod/wiki/view.php', array('pageid' => $item->id)); $pagename = format_string($item->title, true, array('context' => context_module::instance($item->cmid))); $pagename = html_writer::link($pageurl, $pagename); $courseurl = course_get_url($item->courseid, $cm->sectionnum); $cmname = html_writer::link($cm->url, $cm->get_formatted_name()); $coursename = format_string($item->fullname, true, array('context' => context_course::instance($item->courseid))); $coursename = html_writer::link($courseurl, $coursename); $icon = html_writer::link($pageurl, html_writer::empty_tag('img', array('src' => $cm->get_icon_url()))); $tagfeed->add($icon, $pagename, $cmname.'
'.$coursename); } $content = $OUTPUT->render_from_template('core_tag/tagfeed', $tagfeed->export_for_template($OUTPUT)); return new core_tag\output\tagindex($tag, 'mod_wiki', 'wiki_pages', $content, $exclusivemode, $fromctx, $ctx, $rec, $page, $totalpages); } } home/smkb8269/public_html/lms.smk-almanshuriyah.sch.id/mod/assign/locallib.php000064400001475216152267106570023251 0ustar00. /** * This file contains the definition for the class assignment * * This class provides all the functionality for the new assign module. * * @package mod_assign * @copyright 2012 NetSpot {@link http://www.netspot.com.au} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); // Assignment submission statuses. define('ASSIGN_SUBMISSION_STATUS_NEW', 'new'); define('ASSIGN_SUBMISSION_STATUS_REOPENED', 'reopened'); define('ASSIGN_SUBMISSION_STATUS_DRAFT', 'draft'); define('ASSIGN_SUBMISSION_STATUS_SUBMITTED', 'submitted'); // Search filters for grading page. define('ASSIGN_FILTER_NONE', 'none'); define('ASSIGN_FILTER_SUBMITTED', 'submitted'); define('ASSIGN_FILTER_NOT_SUBMITTED', 'notsubmitted'); define('ASSIGN_FILTER_SINGLE_USER', 'singleuser'); define('ASSIGN_FILTER_REQUIRE_GRADING', 'requiregrading'); define('ASSIGN_FILTER_GRADED', 'graded'); define('ASSIGN_FILTER_GRANTED_EXTENSION', 'grantedextension'); define('ASSIGN_FILTER_DRAFT', 'draft'); // Marker filter for grading page. define('ASSIGN_MARKER_FILTER_NO_MARKER', -1); // Reopen attempt methods. /** * ASSIGN_ATTEMPT_REOPEN_METHOD_NONE - Reopening attempts is not allowed. * * @deprecated since Moodle 4.4 * @todo MDL-81977 This will be deleted in Moodle 4.8. */ define('ASSIGN_ATTEMPT_REOPEN_METHOD_NONE', 'none'); define('ASSIGN_ATTEMPT_REOPEN_METHOD_MANUAL', 'manual'); define('ASSIGN_ATTEMPT_REOPEN_METHOD_AUTOMATIC', 'automatic'); define('ASSIGN_ATTEMPT_REOPEN_METHOD_UNTILPASS', 'untilpass'); // Special value means allow unlimited attempts. define('ASSIGN_UNLIMITED_ATTEMPTS', -1); // Special value means no grade has been set. define('ASSIGN_GRADE_NOT_SET', -1); // Grading states. define('ASSIGN_GRADING_STATUS_GRADED', 'graded'); define('ASSIGN_GRADING_STATUS_NOT_GRADED', 'notgraded'); // Marking workflow states. define('ASSIGN_MARKING_WORKFLOW_STATE_NOTMARKED', 'notmarked'); define('ASSIGN_MARKING_WORKFLOW_STATE_INMARKING', 'inmarking'); define('ASSIGN_MARKING_WORKFLOW_STATE_READYFORREVIEW', 'readyforreview'); define('ASSIGN_MARKING_WORKFLOW_STATE_INREVIEW', 'inreview'); define('ASSIGN_MARKING_WORKFLOW_STATE_READYFORRELEASE', 'readyforrelease'); define('ASSIGN_MARKING_WORKFLOW_STATE_RELEASED', 'released'); /** ASSIGN_MAX_EVENT_LENGTH = 432000 ; 5 days maximum */ define("ASSIGN_MAX_EVENT_LENGTH", "432000"); // Name of file area for intro attachments. define('ASSIGN_INTROATTACHMENT_FILEAREA', 'introattachment'); // Name of file area for activity attachments. define('ASSIGN_ACTIVITYATTACHMENT_FILEAREA', 'activityattachment'); // Event types. define('ASSIGN_EVENT_TYPE_DUE', 'due'); define('ASSIGN_EVENT_TYPE_GRADINGDUE', 'gradingdue'); define('ASSIGN_EVENT_TYPE_OPEN', 'open'); define('ASSIGN_EVENT_TYPE_CLOSE', 'close'); define('ASSIGN_EVENT_TYPE_EXTENSION', 'extension'); require_once($CFG->libdir . '/accesslib.php'); require_once($CFG->libdir . '/formslib.php'); require_once($CFG->dirroot . '/repository/lib.php'); require_once($CFG->dirroot . '/mod/assign/mod_form.php'); require_once($CFG->libdir . '/gradelib.php'); require_once($CFG->dirroot . '/grade/grading/lib.php'); require_once($CFG->dirroot . '/mod/assign/feedbackplugin.php'); require_once($CFG->dirroot . '/mod/assign/submissionplugin.php'); require_once($CFG->dirroot . '/mod/assign/renderable.php'); require_once($CFG->dirroot . '/mod/assign/gradingtable.php'); require_once($CFG->libdir . '/portfolio/caller.php'); use mod_assign\event\submission_removed; use mod_assign\event\submission_status_updated; use \mod_assign\output\grading_app; use \mod_assign\output\assign_header; use \mod_assign\output\assign_submission_status; use mod_assign\output\timelimit_panel; use mod_assign\downloader; /** * Standard base class for mod_assign (assignment types). * * @package mod_assign * @copyright 2012 NetSpot {@link http://www.netspot.com.au} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class assign { /** @var stdClass the assignment record that contains the global settings for this assign instance */ private $instance; /** @var array $var array an array containing per-user assignment records, each having calculated properties (e.g. dates) */ private $userinstances = []; /** @var grade_item the grade_item record for this assign instance's primary grade item. */ private $gradeitem; /** @var context the context of the course module for this assign instance * (or just the course if we are creating a new one) */ private $context; /** @var stdClass the course this assign instance belongs to */ private $course; /** @var stdClass the admin config for all assign instances */ private $adminconfig; /** @var assign_renderer the custom renderer for this module */ private $output; /** @var cm_info the course module for this assign instance */ private $coursemodule; /** @var array cache for things like the coursemodule name or the scale menu - * only lives for a single request. */ private $cache; /** @var assign_submission_plugin[] list of the installed submission plugins */ private $submissionplugins; /** @var assign_feedback_plugin[] list of the installed feedback plugins */ private $feedbackplugins; /** @var string action to be used to return to this page * (without repeating any form submissions etc). */ private $returnaction = 'view'; /** @var array params to be used to return to this page */ private $returnparams = array(); /** @var string modulename prevents excessive calls to get_string */ private static $modulename = null; /** @var string modulenameplural prevents excessive calls to get_string */ private static $modulenameplural = null; /** @var array of marking workflow states for the current user */ private $markingworkflowstates = null; /** @var array of all marking workflow states */ private $allmarkingworkflowstates = null; /** @var bool whether to exclude users with inactive enrolment */ private $showonlyactiveenrol = null; /** @var string A key used to identify userlists created by this object. */ private $useridlistid = null; /** @var array cached list of participants for this assignment. The cache key will be group, showactive and the context id */ private $participants = array(); /** @var array cached list of user groups when team submissions are enabled. The cache key will be the user. */ private $usersubmissiongroups = array(); /** @var array cached list of user groups. The cache key will be the user. */ private $usergroups = array(); /** @var array cached list of IDs of users who share group membership with the user. The cache key will be the user. */ private $sharedgroupmembers = array(); /** * @var stdClass The most recent team submission. Used to determine additional attempt numbers and whether * to update the gradebook. */ private $mostrecentteamsubmission = null; /** @var array Array of error messages encountered during the execution of assignment related operations. */ private $errors = array(); /** @var mixed This var can vary between false for no overrides to a stdClass of the overrides for a group */ private $overridedata; /** @var float grade value. */ public $grade; /** @var array $usersearch The content that the current user is looking for. */ protected array $usersearch = []; /** * Constructor for the base assign class. * * Note: For $coursemodule you can supply a stdclass if you like, but it * will be more efficient to supply a cm_info object. * * @param mixed $coursemodulecontext context|null the course module context * (or the course context if the coursemodule has not been * created yet). * @param mixed $coursemodule the current course module if it was already loaded, * otherwise this class will load one from the context as required. * @param mixed $course the current course if it was already loaded, * otherwise this class will load one from the context as required. */ public function __construct($coursemodulecontext, $coursemodule, $course) { $this->context = $coursemodulecontext; $this->course = $course; // Ensure that $this->coursemodule is a cm_info object (or null). $this->coursemodule = cm_info::create($coursemodule); // Temporary cache only lives for a single request - used to reduce db lookups. $this->cache = array(); $this->submissionplugins = $this->load_plugins('assignsubmission'); $this->feedbackplugins = $this->load_plugins('assignfeedback'); // Extra entropy is required for uniqid() to work on cygwin. $this->useridlistid = clean_param(uniqid('', true), PARAM_ALPHANUM); } /** * Set the action and parameters that can be used to return to the current page. * * @param string $action The action for the current page * @param array $params An array of name value pairs which form the parameters * to return to the current page. * @return void */ public function register_return_link($action, $params) { global $PAGE; $params['action'] = $action; $cm = $this->get_course_module(); if ($cm) { $currenturl = new moodle_url('/mod/assign/view.php', array('id' => $cm->id)); } else { $currenturl = new moodle_url('/mod/assign/index.php', array('id' => $this->get_course()->id)); } $currenturl->params($params); $PAGE->set_url($currenturl); } /** * Return an action that can be used to get back to the current page. * * @return string action */ public function get_return_action() { global $PAGE; // Web services don't set a URL, we should avoid debugging when ussing the url object. if (!WS_SERVER) { $params = $PAGE->url->params(); } if (!empty($params['action'])) { return $params['action']; } return ''; } /** * Based on the current assignment settings should we display the intro. * * @return bool showintro */ public function show_intro() { if ($this->get_instance()->alwaysshowdescription || time() > $this->get_instance()->allowsubmissionsfromdate) { return true; } return false; } /** * Return a list of parameters that can be used to get back to the current page. * * @return array params */ public function get_return_params() { global $PAGE; $params = array(); if (!WS_SERVER) { $params = $PAGE->url->params(); } unset($params['id']); unset($params['action']); return $params; } /** * Set the submitted form data. * * @param stdClass $data The form data (instance) */ public function set_instance(stdClass $data) { $this->instance = $data; } /** * Set the context. * * @param context $context The new context */ public function set_context(context $context) { $this->context = $context; } /** * Set the course data. * * @param stdClass $course The course data */ public function set_course(stdClass $course) { $this->course = $course; } /** * Set usersearch to limit results when getting list of participants. * * @param int|null $userid User id to search for. * @param int|null $groupid Group id to limit resuts to specific group. * @param string $usersearch Search string to limit results. */ public function set_usersearch(?int $userid, ?int $groupid, string $usersearch = ''): void { $usersearcharray = []; $usersearcharray['userid'] = $userid; $usersearcharray['groupid'] = $groupid; $usersearcharray['usersearch'] = $usersearch; $this->usersearch = $usersearcharray; } /** * Set error message. * * @param string $message The error message */ protected function set_error_message(string $message) { $this->errors[] = $message; } /** * Get error messages. * * @return array The array of error messages */ public function get_error_messages(): array { return $this->errors; } /** * Get list of feedback plugins installed. * * @return array */ public function get_feedback_plugins() { return $this->feedbackplugins; } /** * Get list of submission plugins installed. * * @return array */ public function get_submission_plugins() { return $this->submissionplugins; } /** * Is blind marking enabled and reveal identities not set yet? * * @return bool */ public function is_blind_marking() { return $this->get_instance()->blindmarking && !$this->get_instance()->revealidentities; } /** * Is hidden grading enabled? * * This just checks the assignment settings. Remember to check * the user has the 'showhiddengrader' capability too * * @return bool */ public function is_hidden_grader() { return $this->get_instance()->hidegrader; } /** * Does an assignment have submission(s) or grade(s) already? * * @return bool */ public function has_submissions_or_grades() { $allgrades = $this->count_grades(); $allsubmissions = $this->count_submissions(); if (($allgrades == 0) && ($allsubmissions == 0)) { return false; } return true; } /** * Get a specific submission plugin by its type. * * @param string $subtype assignsubmission | assignfeedback * @param string $type * @return mixed assign_plugin|null */ public function get_plugin_by_type($subtype, $type) { $shortsubtype = substr($subtype, strlen('assign')); $name = $shortsubtype . 'plugins'; if ($name != 'feedbackplugins' && $name != 'submissionplugins') { return null; } $pluginlist = $this->$name; foreach ($pluginlist as $plugin) { if ($plugin->get_type() == $type) { return $plugin; } } return null; } /** * Get a feedback plugin by type. * * @param string $type - The type of plugin e.g comments * @return mixed assign_feedback_plugin|null */ public function get_feedback_plugin_by_type($type) { return $this->get_plugin_by_type('assignfeedback', $type); } /** * Get a submission plugin by type. * * @param string $type - The type of plugin e.g comments * @return mixed assign_submission_plugin|null */ public function get_submission_plugin_by_type($type) { return $this->get_plugin_by_type('assignsubmission', $type); } /** * Load the plugins from the sub folders under subtype. * * @param string $subtype - either submission or feedback * @return array - The sorted list of plugins */ public function load_plugins($subtype) { global $CFG; $result = array(); $names = core_component::get_plugin_list($subtype); foreach ($names as $name => $path) { if (file_exists($path . '/locallib.php')) { require_once($path . '/locallib.php'); $shortsubtype = substr($subtype, strlen('assign')); $pluginclass = 'assign_' . $shortsubtype . '_' . $name; $plugin = new $pluginclass($this, $name); if ($plugin instanceof assign_plugin) { $idx = $plugin->get_sort_order(); while (array_key_exists($idx, $result)) { $idx +=1; } $result[$idx] = $plugin; } } } ksort($result); return $result; } /** * Display the assignment, used by view.php * * The assignment is displayed differently depending on your role, * the settings for the assignment and the status of the assignment. * * @param string $action The current action if any. * @param array $args Optional arguments to pass to the view (instead of getting them from GET and POST). * @return string - The page output. */ public function view($action='', $args = array()) { global $PAGE; $o = ''; $mform = null; $notices = array(); $nextpageparams = array(); if (!empty($this->get_course_module()->id)) { $nextpageparams['id'] = $this->get_course_module()->id; } // Handle form submissions first. if ($action == 'savesubmission') { $action = 'editsubmission'; if ($this->process_save_submission($mform, $notices)) { $action = 'redirect'; if ($this->can_grade()) { $nextpageparams['action'] = 'grading'; } else { $nextpageparams['action'] = 'view'; } } } else if ($action == 'editprevioussubmission') { $action = 'editsubmission'; if ($this->process_copy_previous_attempt($notices)) { $action = 'redirect'; $nextpageparams['action'] = 'editsubmission'; } } else if ($action == 'lock') { $this->process_lock_submission(); $action = 'redirect'; $nextpageparams['action'] = 'grading'; } else if ($action == 'removesubmission') { $this->process_remove_submission(); $action = 'redirect'; if ($this->can_grade()) { $nextpageparams['action'] = 'grading'; } else { $nextpageparams['action'] = 'view'; } } else if ($action == 'addattempt') { $userid = false; if ($this->is_blind_marking()) { $blindid = optional_param('blindid', 0, PARAM_INT); $userid = $this->get_user_id_for_uniqueid($blindid); } // Userid is required, if not found by blindid. if (!$userid) { $userid = required_param('userid', PARAM_INT); } $this->process_add_attempt($userid); $action = 'redirect'; $nextpageparams['action'] = 'grading'; } else if ($action == 'reverttodraft') { $this->process_revert_to_draft(); $action = 'redirect'; $nextpageparams['action'] = 'grading'; } else if ($action == 'unlock') { $this->process_unlock_submission(); $action = 'redirect'; $nextpageparams['action'] = 'grading'; } else if ($action == 'setbatchmarkingworkflowstate') { $this->process_set_batch_marking_workflow_state(); $action = 'redirect'; $nextpageparams['action'] = 'grading'; } else if ($action == 'setbatchmarkingallocation') { $this->process_set_batch_marking_allocation(); $action = 'redirect'; $nextpageparams['action'] = 'grading'; } else if ($action == 'confirmsubmit') { $action = 'submit'; if ($this->process_submit_for_grading($mform, $notices)) { $action = 'redirect'; $nextpageparams['action'] = 'view'; } else if ($notices) { $action = 'viewsubmitforgradingerror'; } } else if ($action == 'submitotherforgrading') { if ($this->process_submit_other_for_grading($mform, $notices)) { $action = 'redirect'; $nextpageparams['action'] = 'grading'; } else { $action = 'viewsubmitforgradingerror'; } } else if ($action == 'gradingbatchoperation') { $action = $this->process_grading_batch_operation(); if ($action == 'grading') { $action = 'redirect'; $nextpageparams['action'] = 'grading'; } } else if ($action == 'submitgrade') { if (optional_param('saveandshownext', null, PARAM_RAW)) { // Save and show next. $action = 'grade'; if ($this->process_save_grade($mform)) { $action = 'redirect'; $nextpageparams['action'] = 'grade'; $nextpageparams['rownum'] = optional_param('rownum', 0, PARAM_INT) + 1; $nextpageparams['useridlistid'] = optional_param('useridlistid', $this->get_useridlist_key_id(), PARAM_ALPHANUM); } } else if (optional_param('nosaveandprevious', null, PARAM_RAW)) { $action = 'redirect'; $nextpageparams['action'] = 'grade'; $nextpageparams['rownum'] = optional_param('rownum', 0, PARAM_INT) - 1; $nextpageparams['useridlistid'] = optional_param('useridlistid', $this->get_useridlist_key_id(), PARAM_ALPHANUM); } else if (optional_param('nosaveandnext', null, PARAM_RAW)) { $action = 'redirect'; $nextpageparams['action'] = 'grade'; $nextpageparams['rownum'] = optional_param('rownum', 0, PARAM_INT) + 1; $nextpageparams['useridlistid'] = optional_param('useridlistid', $this->get_useridlist_key_id(), PARAM_ALPHANUM); } else if (optional_param('savegrade', null, PARAM_RAW)) { // Save changes button. $action = 'grade'; if ($this->process_save_grade($mform)) { $action = 'redirect'; $nextpageparams['action'] = 'savegradingresult'; } } else { // Cancel button. $action = 'redirect'; $nextpageparams['action'] = 'grading'; } } else if ($action == 'quickgrade') { $message = $this->process_save_quick_grades(); $action = 'quickgradingresult'; } else if ($action == 'saveextension') { $action = 'grantextension'; if ($this->process_save_extension($mform)) { $action = 'redirect'; $nextpageparams['action'] = 'grading'; } } else if ($action == 'revealidentitiesconfirm') { $this->process_reveal_identities(); $action = 'redirect'; $nextpageparams['action'] = 'grading'; } $returnparams = array('rownum'=>optional_param('rownum', 0, PARAM_INT), 'useridlistid' => optional_param('useridlistid', $this->get_useridlist_key_id(), PARAM_ALPHANUM)); $this->register_return_link($action, $returnparams); // Include any page action as part of the body tag CSS id. if (!empty($action)) { $PAGE->set_pagetype('mod-assign-' . $action); } // Now show the right view page. if ($action == 'redirect') { $nextpageurl = new moodle_url('/mod/assign/view.php', $nextpageparams); $messages = ''; $messagetype = \core\output\notification::NOTIFY_INFO; $errors = $this->get_error_messages(); if (!empty($errors)) { $messages = html_writer::alist($errors, ['class' => 'mb-1 mt-1']); $messagetype = \core\output\notification::NOTIFY_ERROR; } redirect($nextpageurl, $messages, null, $messagetype); return; } else if ($action == 'savegradingresult') { $message = get_string('gradingchangessaved', 'assign'); $o .= $this->view_savegrading_result($message); } else if ($action == 'quickgradingresult') { $mform = null; $o .= $this->view_quickgrading_result($message); } else if ($action == 'gradingpanel') { $o .= $this->view_single_grading_panel($args); } else if ($action == 'grade') { $o .= $this->view_single_grade_page($mform); } else if ($action == 'viewpluginassignfeedback') { $o .= $this->view_plugin_content('assignfeedback'); } else if ($action == 'viewpluginassignsubmission') { $o .= $this->view_plugin_content('assignsubmission'); } else if ($action == 'editsubmission') { $PAGE->add_body_class('limitedwidth'); $o .= $this->view_edit_submission_page($mform, $notices); } else if ($action == 'grader') { $o .= $this->view_grader(); } else if ($action == 'grading') { $o .= $this->view_grading_page(); } else if ($action == 'downloadall') { $o .= $this->download_submissions(); } else if ($action == 'submit') { $PAGE->add_body_class('limitedwidth'); $o .= $this->check_submit_for_grading($mform); } else if ($action == 'grantextension') { $o .= $this->view_grant_extension($mform); } else if ($action == 'revealidentities') { $o .= $this->view_reveal_identities_confirm(); } else if ($action == 'removesubmissionconfirm') { $PAGE->add_body_class('limitedwidth'); $o .= $this->view_remove_submission_confirm(); } else if ($action == 'plugingradingbatchoperation') { $o .= $this->view_plugin_grading_batch_operation(); } else if ($action == 'viewpluginpage') { $o .= $this->view_plugin_page(); } else if ($action == 'viewbatchsetmarkingworkflowstate') { $o .= $this->view_batch_set_workflow_state(); } else if ($action == 'viewbatchmarkingallocation') { $o .= $this->view_batch_markingallocation(); } else if ($action == 'viewsubmitforgradingerror') { $o .= $this->view_notices(get_string('submitforgrading', 'assign'), $notices); } else if ($action == 'fixrescalednullgrades') { $o .= $this->view_fix_rescaled_null_grades(); } else { $PAGE->add_body_class('limitedwidth'); $o .= $this->view_submission_page(); } return $o; } /** * Add this instance to the database. * * @param stdClass $formdata The data submitted from the form * @param bool $callplugins This is used to skip the plugin code * when upgrading an old assignment to a new one (the plugins get called manually) * @return mixed false if an error occurs or the int id of the new instance */ public function add_instance(stdClass $formdata, $callplugins) { global $DB; $adminconfig = $this->get_admin_config(); $err = ''; // Add the database record. $update = new stdClass(); $update->name = $formdata->name; $update->timemodified = time(); $update->timecreated = time(); $update->course = $formdata->course; $update->courseid = $formdata->course; $update->intro = $formdata->intro; $update->introformat = $formdata->introformat; $update->alwaysshowdescription = !empty($formdata->alwaysshowdescription); if (isset($formdata->activityeditor)) { $update->activity = $this->save_editor_draft_files($formdata); $update->activityformat = $formdata->activityeditor['format']; } if (isset($formdata->submissionattachments)) { $update->submissionattachments = $formdata->submissionattachments; } $update->submissiondrafts = $formdata->submissiondrafts; $update->requiresubmissionstatement = $formdata->requiresubmissionstatement; $update->sendnotifications = $formdata->sendnotifications; $update->sendlatenotifications = $formdata->sendlatenotifications; $update->sendstudentnotifications = $adminconfig->sendstudentnotifications; if (isset($formdata->sendstudentnotifications)) { $update->sendstudentnotifications = $formdata->sendstudentnotifications; } $update->duedate = $formdata->duedate; $update->cutoffdate = $formdata->cutoffdate; $update->gradingduedate = $formdata->gradingduedate; if (isset($formdata->timelimit)) { $update->timelimit = $formdata->timelimit; } $update->allowsubmissionsfromdate = $formdata->allowsubmissionsfromdate; $update->grade = $formdata->grade; $update->completionsubmit = !empty($formdata->completionsubmit); $update->teamsubmission = $formdata->teamsubmission; $update->requireallteammemberssubmit = $formdata->requireallteammemberssubmit; if (isset($formdata->teamsubmissiongroupingid)) { $update->teamsubmissiongroupingid = $formdata->teamsubmissiongroupingid; } $update->blindmarking = $formdata->blindmarking; if (isset($formdata->hidegrader)) { $update->hidegrader = $formdata->hidegrader; } $update->maxattempts = $formdata->maxattempts ?? 1; $update->attemptreopenmethod = $formdata->attemptreopenmethod ?? ASSIGN_ATTEMPT_REOPEN_METHOD_UNTILPASS; if (isset($formdata->preventsubmissionnotingroup)) { $update->preventsubmissionnotingroup = $formdata->preventsubmissionnotingroup; } $update->markingworkflow = $formdata->markingworkflow; $update->markingallocation = $formdata->markingallocation; if (empty($update->markingworkflow)) { // If marking workflow is disabled, make sure allocation is disabled. $update->markingallocation = 0; } if (isset($formdata->markinganonymous)) { // If marking workflow is disabled, or anonymous submissions is disabled then make sure marking anonymous is disabled. if (empty($update->markingworkflow) || empty($update->blindmarking)) { $update->markinganonymous = 0; } else { $update->markinganonymous = $formdata->markinganonymous; } } // Grade penalties. $update->gradepenalty = $formdata->gradepenalty ?? 0; $returnid = $DB->insert_record('assign', $update); $this->instance = $DB->get_record('assign', array('id'=>$returnid), '*', MUST_EXIST); // Cache the course record. $this->course = $DB->get_record('course', array('id'=>$formdata->course), '*', MUST_EXIST); $this->save_intro_draft_files($formdata); $this->save_editor_draft_files($formdata); if ($callplugins) { // Call save_settings hook for submission plugins. foreach ($this->submissionplugins as $plugin) { if (!$this->update_plugin_instance($plugin, $formdata)) { throw new \moodle_exception($plugin->get_error()); return false; } } foreach ($this->feedbackplugins as $plugin) { if (!$this->update_plugin_instance($plugin, $formdata)) { throw new \moodle_exception($plugin->get_error()); return false; } } // In the case of upgrades the coursemodule has not been set, // so we need to wait before calling these two. $this->update_calendar($formdata->coursemodule); if (!empty($formdata->completionexpected)) { \core_completion\api::update_completion_date_event($formdata->coursemodule, 'assign', $this->instance, $formdata->completionexpected); } $this->update_gradebook(false, $formdata->coursemodule); } $update = new stdClass(); $update->id = $this->get_instance()->id; $update->nosubmissions = (!$this->is_any_submission_plugin_enabled()) ? 1: 0; $DB->update_record('assign', $update); return $returnid; } /** * Delete all grades from the gradebook for this assignment. * * @return bool */ protected function delete_grades() { global $CFG; $result = grade_update('mod/assign', $this->get_course()->id, 'mod', 'assign', $this->get_instance()->id, 0, null, array('deleted'=>1)); return $result == GRADE_UPDATE_OK; } /** * Delete this instance from the database. * * @return bool false if an error occurs */ public function delete_instance() { global $DB; $result = true; foreach ($this->submissionplugins as $plugin) { if (!$plugin->delete_instance()) { throw new \moodle_exception($plugin->get_error()); $result = false; } } foreach ($this->feedbackplugins as $plugin) { if (!$plugin->delete_instance()) { throw new \moodle_exception($plugin->get_error()); $result = false; } } // Delete files associated with this assignment. $fs = get_file_storage(); if (! $fs->delete_area_files($this->context->id) ) { $result = false; } $this->delete_all_overrides(); // Delete_records will throw an exception if it fails - so no need for error checking here. $DB->delete_records('assign_submission', array('assignment' => $this->get_instance()->id)); $DB->delete_records('assign_grades', array('assignment' => $this->get_instance()->id)); $DB->delete_records('assign_plugin_config', array('assignment' => $this->get_instance()->id)); $DB->delete_records('assign_user_flags', array('assignment' => $this->get_instance()->id)); $DB->delete_records('assign_user_mapping', array('assignment' => $this->get_instance()->id)); // Delete items from the gradebook. if (! $this->delete_grades()) { $result = false; } // Delete the instance. // We must delete the module record after we delete the grade item. $DB->delete_records('assign', array('id'=>$this->get_instance()->id)); return $result; } /** * Deletes a assign override from the database and clears any corresponding calendar events * * @param int $overrideid The id of the override being deleted * @return bool true on success */ public function delete_override($overrideid) { global $CFG, $DB; require_once($CFG->dirroot . '/calendar/lib.php'); $cm = $this->get_course_module(); if (empty($cm)) { $instance = $this->get_instance(); $cm = get_coursemodule_from_instance('assign', $instance->id, $instance->course); } $override = $DB->get_record('assign_overrides', array('id' => $overrideid), '*', MUST_EXIST); // Delete the events. $conds = array('modulename' => 'assign', 'instance' => $this->get_instance()->id); if (isset($override->userid)) { $conds['userid'] = $override->userid; $cachekey = "{$cm->instance}_u_{$override->userid}"; } else { $conds['groupid'] = $override->groupid; $cachekey = "{$cm->instance}_g_{$override->groupid}"; } $events = $DB->get_records('event', $conds); foreach ($events as $event) { $eventold = calendar_event::load($event); $eventold->delete(); } $DB->delete_records('assign_overrides', array('id' => $overrideid)); cache::make('mod_assign', 'overrides')->delete($cachekey); // Set the common parameters for one of the events we will be triggering. $params = array( 'objectid' => $override->id, 'context' => context_module::instance($cm->id), 'other' => array( 'assignid' => $override->assignid ) ); // Determine which override deleted event to fire. if (!empty($override->userid)) { $params['relateduserid'] = $override->userid; $event = \mod_assign\event\user_override_deleted::create($params); } else { $params['other']['groupid'] = $override->groupid; $event = \mod_assign\event\group_override_deleted::create($params); } // Trigger the override deleted event. $event->add_record_snapshot('assign_overrides', $override); $event->trigger(); return true; } /** * Deletes all assign overrides from the database and clears any corresponding calendar events */ public function delete_all_overrides() { global $DB; $overrides = $DB->get_records('assign_overrides', array('assignid' => $this->get_instance()->id), 'id'); foreach ($overrides as $override) { $this->delete_override($override->id); } } /** * Updates the assign properties with override information for a user. * * Algorithm: For each assign setting, if there is a matching user-specific override, * then use that otherwise, if there are group-specific overrides, return the most * lenient combination of them. If neither applies, leave the assign setting unchanged. * * @param int $userid The userid. */ public function update_effective_access($userid) { $override = $this->override_exists($userid); // Merge with assign defaults. $keys = array('duedate', 'cutoffdate', 'allowsubmissionsfromdate', 'timelimit'); foreach ($keys as $key) { if (isset($override->{$key})) { $this->get_instance($userid)->{$key} = $override->{$key}; } } } /** * Returns whether an assign has any overrides. * * @return true if any, false if not */ public function has_overrides() { global $DB; $override = $DB->record_exists('assign_overrides', array('assignid' => $this->get_instance()->id)); if ($override) { return true; } return false; } /** * Returns user override * * Algorithm: For each assign setting, if there is a matching user-specific override, * then use that otherwise, if there are group-specific overrides, use the one with the * lowest sort order. If neither applies, leave the assign setting unchanged. * * @param int $userid The userid. * @return stdClass The override */ public function override_exists($userid) { global $DB; // Gets an assoc array containing the keys for defined user overrides only. $getuseroverride = function($userid) use ($DB) { $useroverride = $DB->get_record('assign_overrides', ['assignid' => $this->get_instance()->id, 'userid' => $userid]); return $useroverride ? get_object_vars($useroverride) : []; }; // Gets an assoc array containing the keys for defined group overrides only. $getgroupoverride = function($userid) use ($DB) { $groupings = groups_get_user_groups($this->get_instance()->course, $userid); if (empty($groupings[0])) { return []; } // Select all overrides that apply to the User's groups. list($extra, $params) = $DB->get_in_or_equal(array_values($groupings[0])); $sql = "SELECT * FROM {assign_overrides} WHERE groupid $extra AND assignid = ? ORDER BY sortorder ASC"; $params[] = $this->get_instance()->id; $groupoverride = $DB->get_record_sql($sql, $params, IGNORE_MULTIPLE); return $groupoverride ? get_object_vars($groupoverride) : []; }; // Later arguments clobber earlier ones with array_merge. The two helper functions // return arrays containing keys for only the defined overrides. So we get the // desired behaviour as per the algorithm. return (object)array_merge( ['timelimit' => null, 'duedate' => null, 'cutoffdate' => null, 'allowsubmissionsfromdate' => null], $getgroupoverride($userid), $getuseroverride($userid) ); } /** * Check if the given calendar_event is either a user or group override * event. * * @return bool */ public function is_override_calendar_event(\calendar_event $event) { global $DB; if (!isset($event->modulename)) { return false; } if ($event->modulename != 'assign') { return false; } if (!isset($event->instance)) { return false; } if (!isset($event->userid) && !isset($event->groupid)) { return false; } $overrideparams = [ 'assignid' => $event->instance ]; if (isset($event->groupid)) { $overrideparams['groupid'] = $event->groupid; } else if (isset($event->userid)) { $overrideparams['userid'] = $event->userid; } if ($DB->get_record('assign_overrides', $overrideparams)) { return true; } else { return false; } } /** * This function calculates the minimum and maximum cutoff values for the timestart of * the given event. * * It will return an array with two values, the first being the minimum cutoff value and * the second being the maximum cutoff value. Either or both values can be null, which * indicates there is no minimum or maximum, respectively. * * If a cutoff is required then the function must return an array containing the cutoff * timestamp and error string to display to the user if the cutoff value is violated. * * A minimum and maximum cutoff return value will look like: * [ * [1505704373, 'The due date must be after the sbumission start date'], * [1506741172, 'The due date must be before the cutoff date'] * ] * * If the event does not have a valid timestart range then [false, false] will * be returned. * * @param calendar_event $event The calendar event to get the time range for * @return array */ function get_valid_calendar_event_timestart_range(\calendar_event $event) { $instance = $this->get_instance(); $submissionsfromdate = $instance->allowsubmissionsfromdate; $cutoffdate = $instance->cutoffdate; $duedate = $instance->duedate; $gradingduedate = $instance->gradingduedate; $mindate = null; $maxdate = null; if ($event->eventtype == ASSIGN_EVENT_TYPE_DUE) { // This check is in here because due date events are currently // the only events that can be overridden, so we can save a DB // query if we don't bother checking other events. if ($this->is_override_calendar_event($event)) { // This is an override event so there is no valid timestart // range to set it to. return [false, false]; } if ($submissionsfromdate) { $mindate = [ $submissionsfromdate, get_string('duedatevalidation', 'assign'), ]; } if ($cutoffdate) { $maxdate = [ $cutoffdate, get_string('cutoffdatevalidation', 'assign'), ]; } if ($gradingduedate) { // If we don't have a cutoff date or we've got a grading due date // that is earlier than the cutoff then we should use that as the // upper limit for the due date. if (!$cutoffdate || $gradingduedate < $cutoffdate) { $maxdate = [ $gradingduedate, get_string('gradingdueduedatevalidation', 'assign'), ]; } } } else if ($event->eventtype == ASSIGN_EVENT_TYPE_GRADINGDUE) { if ($duedate) { $mindate = [ $duedate, get_string('gradingdueduedatevalidation', 'assign'), ]; } else if ($submissionsfromdate) { $mindate = [ $submissionsfromdate, get_string('gradingduefromdatevalidation', 'assign'), ]; } } return [$mindate, $maxdate]; } /** * Actual implementation of the reset course functionality, delete all the * assignment submissions for course $data->courseid. * * @param stdClass $data the data submitted from the reset course. * @return array status array */ public function reset_userdata($data) { global $CFG, $DB; $componentstr = get_string('modulenameplural', 'assign'); $status = []; $fs = get_file_storage(); if (!empty($data->reset_assign_submissions)) { // Delete files associated with this assignment. foreach ($this->submissionplugins as $plugin) { $fileareas = []; $plugincomponent = $plugin->get_subtype() . '_' . $plugin->get_type(); $fileareas = $plugin->get_file_areas(); foreach ($fileareas as $filearea => $notused) { $fs->delete_area_files($this->context->id, $plugincomponent, $filearea); } if (!$plugin->delete_instance()) { $status[] = [ 'component' => $componentstr, 'item' => get_string('deleteallsubmissions', 'assign'), 'error' => $plugin->get_error(), ]; } } foreach ($this->feedbackplugins as $plugin) { $fileareas = array(); $plugincomponent = $plugin->get_subtype() . '_' . $plugin->get_type(); $fileareas = $plugin->get_file_areas(); foreach ($fileareas as $filearea => $notused) { $fs->delete_area_files($this->context->id, $plugincomponent, $filearea); } if (!$plugin->delete_instance()) { $status[] = [ 'component' => $componentstr, 'item ' => get_string('deleteallsubmissions', 'assign'), 'error' => $plugin->get_error(), ]; } } $assignids = $DB->get_records('assign', ['course' => $data->courseid], '', 'id'); list($sql, $params) = $DB->get_in_or_equal(array_keys($assignids)); $DB->delete_records_select('assign_submission', "assignment $sql", $params); $DB->delete_records_select('assign_user_flags', "assignment $sql", $params); $status[] = [ 'component' => $componentstr, 'item' => get_string('deleteallsubmissions', 'assign'), 'error' => false, ]; if (!empty($data->reset_gradebook_grades)) { $DB->delete_records_select('assign_grades', "assignment $sql", $params); // Remove all grades from gradebook. require_once($CFG->dirroot . '/mod/assign/lib.php'); assign_reset_gradebook($data->courseid); } // Reset revealidentities for assign if blindmarking is enabled. if ($this->get_instance()->blindmarking) { $DB->set_field('assign', 'revealidentities', 0, ['id' => $this->get_instance()->id]); } } $purgeoverrides = false; // Remove user overrides. if (!empty($data->reset_assign_user_overrides)) { $DB->delete_records_select('assign_overrides', 'assignid IN (SELECT id FROM {assign} WHERE course = ?) AND userid IS NOT NULL', [$data->courseid]); $status[] = [ 'component' => $componentstr, 'item' => get_string('useroverrides', 'assign'), 'error' => false, ]; $purgeoverrides = true; } // Remove group overrides. if (!empty($data->reset_assign_group_overrides)) { $DB->delete_records_select('assign_overrides', 'assignid IN (SELECT id FROM {assign} WHERE course = ?) AND groupid IS NOT NULL', [$data->courseid]); $status[] = [ 'component' => $componentstr, 'item' => get_string('groupoverrides', 'assign'), 'error' => false, ]; $purgeoverrides = true; } // Updating dates - shift may be negative too. if ($data->timeshift) { $sql = "UPDATE {assign_overrides} SET allowsubmissionsfromdate = allowsubmissionsfromdate + ? WHERE assignid = ? AND allowsubmissionsfromdate <> 0"; $DB->execute( $sql, [$data->timeshift, $this->get_instance()->id], ); $sql = "UPDATE {assign_overrides} SET duedate = duedate + ? WHERE assignid = ? AND duedate <> 0"; $DB->execute( $sql, [$data->timeshift, $this->get_instance()->id], ); $sql = "UPDATE {assign_overrides} SET cutoffdate = cutoffdate + ? WHERE assignid =? AND cutoffdate <> 0"; $DB->execute( $sql, [$data->timeshift, $this->get_instance()->id], ); $purgeoverrides = true; // Any changes to the list of dates that needs to be rolled should be same during course restore and course reset. // See MDL-9367. shift_course_mod_dates('assign', [ 'allowsubmissionsfromdate', 'duedate', 'cutoffdate', 'gradingduedate', ], $data->timeshift, $data->courseid, $this->get_instance()->id); $status[] = [ 'component' => $componentstr, 'item' => get_string('date'), 'error' => false, ]; } if ($purgeoverrides) { cache::make('mod_assign', 'overrides')->purge(); } return $status; } /** * Update the settings for a single plugin. * * @param assign_plugin $plugin The plugin to update * @param stdClass $formdata The form data * @return bool false if an error occurs */ protected function update_plugin_instance(assign_plugin $plugin, stdClass $formdata) { if ($plugin->is_visible()) { $enabledname = $plugin->get_subtype() . '_' . $plugin->get_type() . '_enabled'; if (!empty($formdata->$enabledname)) { $plugin->enable(); if (!$plugin->save_settings($formdata)) { throw new \moodle_exception($plugin->get_error()); return false; } } else { $plugin->disable(); } } return true; } /** * Update the gradebook information for this assignment. * * @param bool $reset If true, will reset all grades in the gradbook for this assignment * @param int $coursemoduleid This is required because it might not exist in the database yet * @return bool */ public function update_gradebook($reset, $coursemoduleid) { global $CFG; require_once($CFG->dirroot.'/mod/assign/lib.php'); $assign = clone $this->get_instance(); $assign->cmidnumber = $coursemoduleid; // Set assign gradebook feedback plugin status (enabled and visible). $assign->gradefeedbackenabled = $this->is_gradebook_feedback_enabled(); $param = null; if ($reset) { $param = 'reset'; } return assign_grade_item_update($assign, $param); } /** * Get the marking table page size * * @return integer */ public function get_assign_perpage() { $perpage = (int) get_user_preferences('assign_perpage', 10); $adminconfig = $this->get_admin_config(); $maxperpage = -1; if (isset($adminconfig->maxperpage)) { $maxperpage = $adminconfig->maxperpage; } if (isset($maxperpage) && $maxperpage != -1 && ($perpage == -1 || $perpage > $maxperpage)) { $perpage = $maxperpage; } return $perpage; } /** * Load and cache the admin config for this module. * * @return stdClass the plugin config */ public function get_admin_config() { if ($this->adminconfig) { return $this->adminconfig; } $this->adminconfig = get_config('assign'); return $this->adminconfig; } /** * Update the calendar entries for this assignment. * * @param int $coursemoduleid - Required to pass this in because it might * not exist in the database yet. * @return bool */ public function update_calendar($coursemoduleid) { global $DB, $CFG; require_once($CFG->dirroot.'/calendar/lib.php'); // Special case for add_instance as the coursemodule has not been set yet. $instance = $this->get_instance(); // Start with creating the event. $event = new stdClass(); $event->modulename = 'assign'; $event->courseid = $instance->course; $event->groupid = 0; $event->userid = 0; $event->instance = $instance->id; $event->type = CALENDAR_EVENT_TYPE_ACTION; // Convert the links to pluginfile. It is a bit hacky but at this stage the files // might not have been saved in the module area yet. $intro = $instance->intro; if ($draftid = file_get_submitted_draft_itemid('introeditor')) { $intro = file_rewrite_urls_to_pluginfile($intro, $draftid); } // We need to remove the links to files as the calendar is not ready // to support module events with file areas. $intro = strip_pluginfile_content($intro); if ($this->show_intro()) { $event->description = array( 'text' => $intro, 'format' => $instance->introformat ); } else { $event->description = array( 'text' => '', 'format' => $instance->introformat ); } $eventtype = ASSIGN_EVENT_TYPE_DUE; if ($instance->duedate) { $event->name = get_string('calendardue', 'assign', $instance->name); $event->eventtype = $eventtype; $event->timestart = $instance->duedate; $event->timesort = $instance->duedate; $select = "modulename = :modulename AND instance = :instance AND eventtype = :eventtype AND groupid = 0 AND courseid <> 0"; $params = array('modulename' => 'assign', 'instance' => $instance->id, 'eventtype' => $eventtype); $event->id = $DB->get_field_select('event', 'id', $select, $params); // Now process the event. if ($event->id) { $calendarevent = calendar_event::load($event->id); $calendarevent->update($event, false); } else { calendar_event::create($event, false); } } else { $DB->delete_records('event', array('modulename' => 'assign', 'instance' => $instance->id, 'eventtype' => $eventtype)); } $eventtype = ASSIGN_EVENT_TYPE_GRADINGDUE; if ($instance->gradingduedate) { $event->name = get_string('calendargradingdue', 'assign', $instance->name); $event->eventtype = $eventtype; $event->timestart = $instance->gradingduedate; $event->timesort = $instance->gradingduedate; $event->id = $DB->get_field('event', 'id', array('modulename' => 'assign', 'instance' => $instance->id, 'eventtype' => $event->eventtype)); // Now process the event. if ($event->id) { $calendarevent = calendar_event::load($event->id); $calendarevent->update($event, false); } else { calendar_event::create($event, false); } } else { $DB->delete_records('event', array('modulename' => 'assign', 'instance' => $instance->id, 'eventtype' => $eventtype)); } return true; } /** * Update this instance in the database. * * @param stdClass $formdata - the data submitted from the form * @return bool false if an error occurs */ public function update_instance($formdata) { global $DB; $adminconfig = $this->get_admin_config(); $update = new stdClass(); $update->id = $formdata->instance; $update->name = $formdata->name; $update->timemodified = time(); $update->course = $formdata->course; $update->intro = $formdata->intro; $update->introformat = $formdata->introformat; $update->alwaysshowdescription = !empty($formdata->alwaysshowdescription); if (isset($formdata->activityeditor)) { $update->activity = $this->save_editor_draft_files($formdata); $update->activityformat = $formdata->activityeditor['format']; } if (isset($formdata->submissionattachments)) { $update->submissionattachments = $formdata->submissionattachments; } $update->submissiondrafts = $formdata->submissiondrafts; $update->requiresubmissionstatement = $formdata->requiresubmissionstatement; $update->sendnotifications = $formdata->sendnotifications; $update->sendlatenotifications = $formdata->sendlatenotifications; $update->sendstudentnotifications = $adminconfig->sendstudentnotifications; if (isset($formdata->sendstudentnotifications)) { $update->sendstudentnotifications = $formdata->sendstudentnotifications; } $update->duedate = $formdata->duedate; $update->cutoffdate = $formdata->cutoffdate; if (isset($formdata->timelimit)) { $update->timelimit = $formdata->timelimit; } $update->gradingduedate = $formdata->gradingduedate; $update->allowsubmissionsfromdate = $formdata->allowsubmissionsfromdate; $update->grade = $formdata->grade; if (!empty($formdata->completionunlocked)) { $update->completionsubmit = !empty($formdata->completionsubmit); } $update->teamsubmission = $formdata->teamsubmission; $update->requireallteammemberssubmit = $formdata->requireallteammemberssubmit; if (isset($formdata->teamsubmissiongroupingid)) { $update->teamsubmissiongroupingid = $formdata->teamsubmissiongroupingid; } if (isset($formdata->hidegrader)) { $update->hidegrader = $formdata->hidegrader; } $update->blindmarking = $formdata->blindmarking; $update->maxattempts = $formdata->maxattempts ?? 1; $update->attemptreopenmethod = $formdata->attemptreopenmethod ?? ASSIGN_ATTEMPT_REOPEN_METHOD_UNTILPASS; if (isset($formdata->preventsubmissionnotingroup)) { $update->preventsubmissionnotingroup = $formdata->preventsubmissionnotingroup; } $update->markingworkflow = $formdata->markingworkflow; $update->markingallocation = $formdata->markingallocation; if (empty($update->markingworkflow)) { // If marking workflow is disabled, make sure allocation is disabled. $update->markingallocation = 0; } $update->markinganonymous = $formdata->markinganonymous; // If marking workflow is disabled, or blindmarking is disabled then make sure marking anonymous is disabled. if (empty($update->markingworkflow) || empty($update->blindmarking)) { $update->markinganonymous = 0; } // Grade penalties. $update->gradepenalty = $formdata->gradepenalty ?? 0; $result = $DB->update_record('assign', $update); $this->instance = $DB->get_record('assign', array('id'=>$update->id), '*', MUST_EXIST); $this->save_intro_draft_files($formdata); // Load the assignment so the plugins have access to it. // Call save_settings hook for submission plugins. foreach ($this->submissionplugins as $plugin) { if (!$this->update_plugin_instance($plugin, $formdata)) { throw new \moodle_exception($plugin->get_error()); return false; } } foreach ($this->feedbackplugins as $plugin) { if (!$this->update_plugin_instance($plugin, $formdata)) { throw new \moodle_exception($plugin->get_error()); return false; } } $this->update_calendar($this->get_course_module()->id); $completionexpected = (!empty($formdata->completionexpected)) ? $formdata->completionexpected : null; \core_completion\api::update_completion_date_event($this->get_course_module()->id, 'assign', $this->instance, $completionexpected); $this->update_gradebook(false, $this->get_course_module()->id); $update = new stdClass(); $update->id = $this->get_instance()->id; $update->nosubmissions = (!$this->is_any_submission_plugin_enabled()) ? 1: 0; $DB->update_record('assign', $update); // Check if we need to recalculate penalty for existing grades. if (!empty($formdata->recalculatepenalty) && $formdata->recalculatepenalty === 'yes') { $assign = clone $this->get_instance(); $assign->cmidnumber = $this->get_course_module()->idnumber; assign_update_grades($assign); } return $result; } /** * Save the attachments in the intro description. * * @param stdClass $formdata */ protected function save_intro_draft_files($formdata) { if (isset($formdata->introattachments)) { file_save_draft_area_files($formdata->introattachments, $this->get_context()->id, 'mod_assign', ASSIGN_INTROATTACHMENT_FILEAREA, 0); } } /** * Save the attachments in the editor description. * * @param stdClass $formdata */ protected function save_editor_draft_files($formdata): string { $text = ''; if (isset($formdata->activityeditor)) { $text = $formdata->activityeditor['text']; if (isset($formdata->activityeditor['itemid'])) { $text = file_save_draft_area_files($formdata->activityeditor['itemid'], $this->get_context()->id, 'mod_assign', ASSIGN_ACTIVITYATTACHMENT_FILEAREA, 0, array('subdirs' => true), $formdata->activityeditor['text']); } } return $text; } /** * Add elements in grading plugin form. * * @param mixed $grade stdClass|null * @param MoodleQuickForm $mform * @param stdClass $data * @param int $userid - The userid we are grading * @return void */ protected function add_plugin_grade_elements($grade, MoodleQuickForm $mform, stdClass $data, $userid) { foreach ($this->feedbackplugins as $plugin) { if ($plugin->is_enabled() && $plugin->is_visible()) { $plugin->get_form_elements_for_user($grade, $mform, $data, $userid); } } } /** * Add one plugins settings to edit plugin form. * * @param assign_plugin $plugin The plugin to add the settings from * @param MoodleQuickForm $mform The form to add the configuration settings to. * This form is modified directly (not returned). * @param array $pluginsenabled A list of form elements to be added to a group. * The new element is added to this array by this function. * @return void */ protected function add_plugin_settings(assign_plugin $plugin, MoodleQuickForm $mform, & $pluginsenabled) { global $CFG; if ($plugin->is_visible() && !$plugin->is_configurable() && $plugin->is_enabled()) { $name = $plugin->get_subtype() . '_' . $plugin->get_type() . '_enabled'; $pluginsenabled[] = $mform->createElement('hidden', $name, 1); $mform->setType($name, PARAM_BOOL); $plugin->get_settings($mform); } else if ($plugin->is_visible() && $plugin->is_configurable()) { $name = $plugin->get_subtype() . '_' . $plugin->get_type() . '_enabled'; $label = $plugin->get_name(); $pluginsenabled[] = $mform->createElement('checkbox', $name, '', $label); $helpicon = $this->get_renderer()->help_icon('enabled', $plugin->get_subtype() . '_' . $plugin->get_type()); $pluginsenabled[] = $mform->createElement('static', '', '', $helpicon); $default = get_config($plugin->get_subtype() . '_' . $plugin->get_type(), 'default'); if ($plugin->get_config('enabled') !== false) { $default = $plugin->is_enabled(); } $mform->setDefault($plugin->get_subtype() . '_' . $plugin->get_type() . '_enabled', $default); $plugin->get_settings($mform); } } /** * Add settings to edit plugin form. * * @param MoodleQuickForm $mform The form to add the configuration settings to. * This form is modified directly (not returned). * @return void */ public function add_all_plugin_settings(MoodleQuickForm $mform) { $mform->addElement('header', 'submissiontypes', get_string('submissiontypes', 'assign')); $submissionpluginsenabled = array(); $group = $mform->addGroup(array(), 'submissionplugins', get_string('submissiontypes', 'assign'), array(' '), false); foreach ($this->submissionplugins as $plugin) { $this->add_plugin_settings($plugin, $mform, $submissionpluginsenabled); } $group->setElements($submissionpluginsenabled); $mform->addElement('header', 'feedbacktypes', get_string('feedbacktypes', 'assign')); $feedbackpluginsenabled = array(); $group = $mform->addGroup(array(), 'feedbackplugins', get_string('feedbacktypes', 'assign'), array(' '), false); foreach ($this->feedbackplugins as $plugin) { $this->add_plugin_settings($plugin, $mform, $feedbackpluginsenabled); } $group->setElements($feedbackpluginsenabled); $mform->setExpanded('submissiontypes'); } /** * Allow each plugin an opportunity to update the defaultvalues * passed in to the settings form (needed to set up draft areas for * editor and filemanager elements) * * @param array $defaultvalues */ public function plugin_data_preprocessing(&$defaultvalues) { foreach ($this->submissionplugins as $plugin) { if ($plugin->is_visible()) { $plugin->data_preprocessing($defaultvalues); } } foreach ($this->feedbackplugins as $plugin) { if ($plugin->is_visible()) { $plugin->data_preprocessing($defaultvalues); } } } /** * Allow each plugin to validiate the data from the assignments settings form. * * @param array $data as passed to mod_assign_mod_form::validation(). * @param array $files as passed to mod_assign_mod_form::validation(). * @return array and validation errors that should be displayed. */ public function plugin_settings_validation(array $data, array $files): array { $errors = []; foreach ($this->submissionplugins as $plugin) { if ($plugin->is_visible()) { $errors = array_merge($errors, $plugin->settings_validation($data, $files)); } } foreach ($this->feedbackplugins as $plugin) { if ($plugin->is_visible()) { $errors = array_merge($errors, $plugin->settings_validation($data, $files)); } } return $errors; } /** * Get the name of the current module. * * @return string the module name (Assignment) */ protected function get_module_name() { if (isset(self::$modulename)) { return self::$modulename; } self::$modulename = get_string('modulename', 'assign'); return self::$modulename; } /** * Get the plural name of the current module. * * @return string the module name plural (Assignments) */ protected function get_module_name_plural() { if (isset(self::$modulenameplural)) { return self::$modulenameplural; } self::$modulenameplural = get_string('modulenameplural', 'assign'); return self::$modulenameplural; } /** * Has this assignment been constructed from an instance? * * @return bool */ public function has_instance() { return $this->instance || $this->get_course_module(); } /** * Get the settings for the current instance of this assignment. * * @return stdClass The settings * @throws dml_exception */ public function get_default_instance() { global $DB; if (!$this->instance && $this->get_course_module()) { $params = array('id' => $this->get_course_module()->instance); $this->instance = $DB->get_record('assign', $params, '*', MUST_EXIST); $this->userinstances = []; } return $this->instance; } /** * Get the settings for the current instance of this assignment * @param int|null $userid the id of the user to load the assign instance for. * @return stdClass The settings */ public function get_instance(?int $userid = null): stdClass { global $USER; $userid = $userid ?? $USER->id; $this->instance = $this->get_default_instance(); // If we have the user instance already, just return it. if (isset($this->userinstances[$userid])) { return $this->userinstances[$userid]; } // Calculate properties which vary per user. $this->userinstances[$userid] = $this->calculate_properties($this->instance, $userid); return $this->userinstances[$userid]; } /** * Calculates and updates various properties based on the specified user. * * @param stdClass $record the raw assign record. * @param int $userid the id of the user to calculate the properties for. * @return stdClass a new record having calculated properties. */ private function calculate_properties(\stdClass $record, int $userid): \stdClass { $record = clone ($record); // Relative dates. if (!empty($record->duedate)) { $course = $this->get_course(); $usercoursedates = course_get_course_dates_for_user_id($course, $userid); if ($usercoursedates['start']) { $userprops = ['duedate' => $record->duedate + $usercoursedates['startoffset']]; $record = (object) array_merge((array) $record, (array) $userprops); } } return $record; } /** * Get the primary grade item for this assign instance. * * @return grade_item The grade_item record */ public function get_grade_item() { if ($this->gradeitem) { return $this->gradeitem; } $instance = $this->get_instance(); $params = array('itemtype' => 'mod', 'itemmodule' => 'assign', 'iteminstance' => $instance->id, 'courseid' => $instance->course, 'itemnumber' => 0); $this->gradeitem = grade_item::fetch($params); if (!$this->gradeitem) { throw new coding_exception('Improper use of the assignment class. ' . 'Cannot load the grade item.'); } return $this->gradeitem; } /** * Get the context of the current course. * * @return mixed context|null The course context */ public function get_course_context() { if (!$this->context && !$this->course) { throw new coding_exception('Improper use of the assignment class. ' . 'Cannot load the course context.'); } if ($this->context) { return $this->context->get_course_context(); } else { return context_course::instance($this->course->id); } } /** * Get the current course module. * * @return cm_info|null The course module or null if not known */ public function get_course_module() { if ($this->coursemodule) { return $this->coursemodule; } if (!$this->context) { return null; } if ($this->context->contextlevel == CONTEXT_MODULE) { $modinfo = get_fast_modinfo($this->get_course()); $this->coursemodule = $modinfo->get_cm($this->context->instanceid); return $this->coursemodule; } return null; } /** * Get context module. * * @return context */ public function get_context() { return $this->context; } /** * Get the current course. * * @return mixed stdClass|null The course */ public function get_course() { global $DB; if ($this->course && is_object($this->course)) { return $this->course; } if (!$this->context) { return null; } $params = array('id' => $this->get_course_context()->instanceid); $this->course = $DB->get_record('course', $params, '*', MUST_EXIST); return $this->course; } /** * Count the number of intro attachments. * * @return int */ protected function count_attachments() { $fs = get_file_storage(); $files = $fs->get_area_files($this->get_context()->id, 'mod_assign', ASSIGN_INTROATTACHMENT_FILEAREA, 0, 'id', false); return count($files); } /** * Are there any intro attachments to display? * * @return boolean */ protected function has_visible_attachments() { return ($this->count_attachments() > 0); } /** * Check if the intro attachments should be provided to the user. * * @param int $userid User id. * @return bool */ public function should_provide_intro_attachments(int $userid): bool { $instance = $this->get_instance($userid); // Check if user has permission to view attachments regardless of assignment settings. if (has_capability('moodle/course:manageactivities', $this->get_context())) { return true; } // If assignment does not show intro, we never provide intro attachments. if (!$this->show_intro()) { return false; } // If intro attachments should only be shown when submission is started, check if there is an open submission. if (!empty($instance->submissionattachments) && !$this->submissions_open($userid, true)) { return false; } return true; } /** * Return a grade in user-friendly form, whether it's a scale or not. * * @param mixed $grade int|null * @param boolean $editing Are we allowing changes to this grade? * @param int $userid The user id the grade belongs to * @param int $modified Timestamp from when the grade was last modified * @param float $deductedmark The deducted mark if penalty is applied * @return string User-friendly representation of grade */ public function display_grade($grade, $editing, $userid = 0, $modified = 0, float $deductedmark = 0) { global $DB, $PAGE; static $scalegrades = array(); $o = ''; if ($this->get_instance()->grade >= 0) { // Normal number. if ($editing && $this->get_instance()->grade > 0) { if ($grade < 0) { $displaygrade = ''; } else { $displaygrade = format_float($grade, $this->get_grade_item()->get_decimals()); } $o .= ''; $o .= ''; $o .= ' / ' . format_float($this->get_instance()->grade, $this->get_grade_item()->get_decimals()); } else { if ($grade == -1 || $grade === null) { $o .= '-'; } else { $item = $this->get_grade_item(); $o .= grade_format_gradevalue($grade, $item); if ($item->get_displaytype() == GRADE_DISPLAY_TYPE_REAL) { // If displaying the raw grade, also display the total value. $o .= ' / ' . format_float($this->get_instance()->grade, $item->get_decimals()); } } } // Add penalty indicator, icon only. $penaltyindicator = ''; if ($deductedmark > 0) { $gradegrade = new \grade_grade(); $gradegrade->deductedmark = $deductedmark; $gradegrade->overridden = $userid > 0 ? $this->get_grade_item()->get_grade($userid)->overridden : 0; $penaltyindicator = \core_grades\penalty_manager::show_penalty_indicator($gradegrade); } return $penaltyindicator . $o; } else { // Scale. if (empty($this->cache['scale'])) { if ($scale = $DB->get_record('scale', array('id'=>-($this->get_instance()->grade)))) { $this->cache['scale'] = make_menu_from_list($scale->scale); } else { $o .= '-'; return $o; } } if ($editing) { $o .= ''; $o .= ''; return $o; } else { $scaleid = (int)$grade; if (isset($this->cache['scale'][$scaleid])) { $o .= $this->cache['scale'][$scaleid]; return $o; } $o .= '-'; return $o; } } } /** * Get the submission status/grading status for all submissions in this assignment for the * given paticipants. * * These statuses match the available filters (requiregrading, submitted, notsubmitted, grantedextension). * If this is a group assignment, group info is also returned. * * @param array $participants an associative array where the key is the participant id and * the value is the participant record. * @return array an associative array where the key is the participant id and the value is * the participant record. */ private function get_submission_info_for_participants($participants) { global $DB; if (empty($participants)) { return $participants; } list($insql, $params) = $DB->get_in_or_equal(array_keys($participants), SQL_PARAMS_NAMED); $assignid = $this->get_instance()->id; $params['assignmentid1'] = $assignid; $params['assignmentid2'] = $assignid; $params['assignmentid3'] = $assignid; $fields = 'SELECT u.id, s.status, s.timemodified AS stime, g.timemodified AS gtime, g.grade, uf.extensionduedate'; $from = ' FROM {user} u LEFT JOIN {assign_submission} s ON u.id = s.userid AND s.assignment = :assignmentid1 AND s.latest = 1 LEFT JOIN {assign_grades} g ON u.id = g.userid AND g.assignment = :assignmentid2 AND g.attemptnumber = s.attemptnumber LEFT JOIN {assign_user_flags} uf ON u.id = uf.userid AND uf.assignment = :assignmentid3 '; $where = ' WHERE u.id ' . $insql; if (!empty($this->get_instance()->blindmarking)) { $from .= 'LEFT JOIN {assign_user_mapping} um ON u.id = um.userid AND um.assignment = :assignmentid4 '; $params['assignmentid4'] = $assignid; $fields .= ', um.id as recordid '; } $sql = "$fields $from $where"; $records = $DB->get_records_sql($sql, $params); if ($this->get_instance()->teamsubmission) { // Get all groups. $allgroups = groups_get_all_groups($this->get_course()->id, array_keys($participants), $this->get_instance()->teamsubmissiongroupingid, 'DISTINCT g.id, g.name'); } foreach ($participants as $userid => $participant) { $participants[$userid]->fullname = $this->fullname($participant); $participants[$userid]->submitted = false; $participants[$userid]->requiregrading = false; $participants[$userid]->grantedextension = false; $participants[$userid]->submissionstatus = ''; } foreach ($records as $userid => $submissioninfo) { // These filters are 100% the same as the ones in the grading table SQL. $submitted = false; $requiregrading = false; $grantedextension = false; $submissionstatus = !empty($submissioninfo->status) ? $submissioninfo->status : ''; if (!empty($submissioninfo->stime) && $submissioninfo->status == ASSIGN_SUBMISSION_STATUS_SUBMITTED) { $submitted = true; } if ($submitted && ($submissioninfo->stime >= $submissioninfo->gtime || empty($submissioninfo->gtime) || $submissioninfo->grade === null)) { $requiregrading = true; } if (!empty($submissioninfo->extensionduedate)) { $grantedextension = true; } $participants[$userid]->submitted = $submitted; $participants[$userid]->requiregrading = $requiregrading; $participants[$userid]->grantedextension = $grantedextension; $participants[$userid]->submissionstatus = $submissionstatus; if ($this->get_instance()->teamsubmission) { $group = $this->get_submission_group($userid); if ($group) { $participants[$userid]->groupid = $group->id; $participants[$userid]->groupname = $group->name; } } } return $participants; } /** * Get the submission status/grading status for all submissions in this assignment. * These statuses match the available filters (requiregrading, submitted, notsubmitted, grantedextension). * If this is a group assignment, group info is also returned. * * @param int $currentgroup * @param boolean $tablesort Apply current user table sorting preferences. * @return array List of user records with extra fields 'submitted', 'notsubmitted', 'requiregrading', 'grantedextension', * 'groupid', 'groupname' */ public function list_participants_with_filter_status_and_group($currentgroup, $tablesort = false) { $participants = $this->list_participants($currentgroup, false, $tablesort); if (empty($participants)) { return $participants; } else { return $this->get_submission_info_for_participants($participants); } } /** * Return a valid order by segment for list_participants that matches * the sorting of the current grading table. Not every field is supported, * we are only concerned with a list of users so we can't search on anything * that is not part of the user information (like grading statud or last modified stuff). * * @return string Order by clause for list_participants */ private function get_grading_sort_sql() { $usersort = flexible_table::get_sort_for_table('mod_assign_grading'); // TODO Does not support custom user profile fields (MDL-70456). $userfieldsapi = \core_user\fields::for_identity($this->context, false)->with_userpic(); $userfields = $userfieldsapi->get_required_fields(); $orderfields = explode(',', $usersort); $validlist = []; foreach ($orderfields as $orderfield) { $orderfield = trim($orderfield); foreach ($userfields as $field) { $parts = explode(' ', $orderfield); if ($parts[0] == $field) { // Prepend the user table prefix and count this as a valid order field. array_push($validlist, 'u.' . $orderfield); } } } // Produce a final list. $result = implode(',', $validlist); if (empty($result)) { // Fall back ordering when none has been set. $result = 'u.lastname, u.firstname, u.id'; } return $result; } /** * Returns array with sql code and parameters returning all ids of users who have submitted an assignment. * * @param int $group The group that the query is for. * @return array list($sql, $params) */ protected function get_submitted_sql($group = 0) { // We need to guarentee unique table names. static $i = 0; $i++; $prefix = 'sa' . $i . '_'; $params = [ "{$prefix}assignment" => (int) $this->get_instance()->id, "{$prefix}status" => ASSIGN_SUBMISSION_STATUS_NEW, ]; $capjoin = get_enrolled_with_capabilities_join($this->context, $prefix, '', $group, $this->show_only_active_users()); $params += $capjoin->params; $sql = "SELECT {$prefix}s.userid FROM {assign_submission} {$prefix}s JOIN {user} {$prefix}u ON {$prefix}u.id = {$prefix}s.userid $capjoin->joins WHERE {$prefix}s.assignment = :{$prefix}assignment AND {$prefix}s.status <> :{$prefix}status AND $capjoin->wheres"; return array($sql, $params); } /** * Load a list of users enrolled in the current course with the specified permission and group. * 0 for no group. * Apply any current sort filters from the grading table. * * @param int $currentgroup * @param bool $idsonly * @param bool $tablesort * @return array List of user records * @param bool|null $onlyactive Whether to show only active users. */ public function list_participants($currentgroup, $idsonly, $tablesort = false, ?bool $onlyactive = null) { global $DB, $USER; // Get the last known sort order for the grading table. if (empty($currentgroup)) { $currentgroup = 0; } if ($onlyactive === null) { $onlyactive = $this->show_only_active_users(); } $key = $this->context->id . '-' . $currentgroup . '-' . $onlyactive; if (!isset($this->participants[$key])) { list($esql, $params) = get_enrolled_sql($this->context, 'mod/assign:submit', $currentgroup, $onlyactive); list($ssql, $sparams) = $this->get_submitted_sql($currentgroup); $params += $sparams; $fields = 'u.*'; $orderby = 'u.lastname, u.firstname, u.id'; $additionaljoins = ''; $additionalfilters = ''; $instance = $this->get_instance(); if (!empty($instance->blindmarking)) { $additionaljoins .= " LEFT JOIN {assign_user_mapping} um ON u.id = um.userid AND um.assignment = :assignmentid1 LEFT JOIN {assign_submission} s ON u.id = s.userid AND s.assignment = :assignmentid2 AND s.latest = 1 "; $params['assignmentid1'] = (int) $instance->id; $params['assignmentid2'] = (int) $instance->id; $fields .= ', um.id as recordid '; // Sort by submission time first, then by um.id to sort reliably by the blind marking id. // Note, different DBs have different ordering of NULL values. // Therefore we coalesce the current time into the timecreated field, and the max possible integer into // the ID field. if (empty($tablesort)) { $orderby = "COALESCE(s.timecreated, " . time() . ") ASC, COALESCE(s.id, " . PHP_INT_MAX . ") ASC, um.id ASC"; } } if ($instance->markingworkflow && $instance->markingallocation && !has_capability('mod/assign:manageallocations', $this->get_context()) && has_capability('mod/assign:grade', $this->get_context())) { $additionaljoins .= ' LEFT JOIN {assign_user_flags} uf ON u.id = uf.userid AND uf.assignment = :assignmentid3'; $params['assignmentid3'] = (int) $instance->id; $additionalfilters .= ' AND uf.allocatedmarker = :markerid'; $params['markerid'] = $USER->id; } // A user wants to view a particular user rather than a set of users. if ($this->usersearch) { if (isset($this->usersearch['userid'])) { $additionalfilters .= " AND u.id = :uid"; $params['uid'] = $this->usersearch['userid']; } else if ($this->usersearch['usersearch'] !== '') { // A user wants to view a subset of learners that match the search criteria. [ 'where' => $keywordswhere, 'params' => $keywordsparams, ] = \core_user::get_users_search_sql($this->context, $this->usersearch['usersearch']); $additionalfilters .= " AND $keywordswhere"; $params = array_merge($params, $keywordsparams); } } // Exclude suspended users from the list of participants. $additionalfilters .= " AND u.suspended = 0 AND u.auth <> 'nologin'"; $sql = "SELECT $fields FROM {user} u JOIN ($esql UNION $ssql) je ON je.id = u.id $additionaljoins WHERE u.deleted = 0 $additionalfilters ORDER BY $orderby"; $users = $DB->get_records_sql($sql, $params); $cm = $this->get_course_module(); $info = new \core_availability\info_module($cm); $users = $info->filter_user_list($users); $this->participants[$key] = $users; } if ($tablesort) { // Resort the user list according to the grading table sort and filter settings. $sortedfiltereduserids = $this->get_grading_userid_list(true, ''); $sortedfilteredusers = []; foreach ($sortedfiltereduserids as $nextid) { $nextid = intval($nextid); if (isset($this->participants[$key][$nextid])) { $sortedfilteredusers[$nextid] = $this->participants[$key][$nextid]; } } $this->participants[$key] = $sortedfilteredusers; } if ($idsonly) { $idslist = array(); foreach ($this->participants[$key] as $id => $user) { $idslist[$id] = new stdClass(); $idslist[$id]->id = $id; } return $idslist; } return $this->participants[$key]; } /** * Load a user if they are enrolled in the current course. Populated with submission * status for this assignment. * * @param int $userid * @return null|stdClass user record */ public function get_participant($userid) { global $DB, $USER; if ($userid == $USER->id) { $participant = clone ($USER); } else { $participant = $DB->get_record('user', array('id' => $userid)); } if (!$participant) { return null; } if (!is_enrolled($this->context, $participant, '', $this->show_only_active_users())) { return null; } $result = $this->get_submission_info_for_participants(array($participant->id => $participant)); $submissioninfo = $result[$participant->id]; if (!$submissioninfo->submitted && !has_capability('mod/assign:submit', $this->context, $userid)) { return null; } return $submissioninfo; } /** * Load a count of valid teams for this assignment. * * @param int $activitygroup Activity active group * @return int number of valid teams */ public function count_teams($activitygroup = 0) { $count = 0; $participants = $this->list_participants($activitygroup, true); // If a team submission grouping id is provided all good as all returned groups // are the submission teams, but if no team submission grouping was specified // $groups will contain all participants groups. if ($this->get_instance()->teamsubmissiongroupingid) { // We restrict the users to the selected group ones. $groups = groups_get_all_groups($this->get_course()->id, array_keys($participants), $this->get_instance()->teamsubmissiongroupingid, 'DISTINCT g.id, g.name'); $count = count($groups); // When a specific group is selected we don't count the default group users. if ($activitygroup == 0) { if (empty($this->get_instance()->preventsubmissionnotingroup)) { // See if there are any users in the default group. $defaultusers = $this->get_submission_group_members(0, true); if (count($defaultusers) > 0) { $count += 1; } } } else if ($activitygroup != 0 && empty($groups)) { // Set count to 1 if $groups returns empty. // It means the group is not part of $this->get_instance()->teamsubmissiongroupingid. $count = 1; } } else { // It is faster to loop around participants if no grouping was specified. $groups = array(); foreach ($participants as $participant) { if ($group = $this->get_submission_group($participant->id)) { $groups[$group->id] = true; } else if (empty($this->get_instance()->preventsubmissionnotingroup)) { $groups[0] = true; } } $count = count($groups); } return $count; } /** * Load a count of active users enrolled in the current course with the specified permission and group. * 0 for no group. * * @param int $currentgroup * @return int number of matching users */ public function count_participants($currentgroup) { return count($this->list_participants($currentgroup, true)); } /** * Load a count of active users submissions in the current module that require grading * This means the submission modification time is more recent than the * grading modification time and the status is SUBMITTED. * * @param mixed $currentgroup int|null the group for counting (if null the function will determine it) * @return int number of matching submissions */ public function count_submissions_need_grading($currentgroup = null) { global $DB; if ($this->get_instance()->teamsubmission) { // This does not make sense for group assignment because the submission is shared. return 0; } if ($currentgroup === null) { $currentgroup = groups_get_activity_group($this->get_course_module(), true); } list($esql, $params) = get_enrolled_sql($this->get_context(), '', $currentgroup, true); $params['assignid'] = $this->get_instance()->id; $params['submitted'] = ASSIGN_SUBMISSION_STATUS_SUBMITTED; $sqlscalegrade = $this->get_instance()->grade < 0 ? ' OR g.grade = -1' : ''; $sql = 'SELECT COUNT(s.userid) FROM {assign_submission} s LEFT JOIN {assign_grades} g ON s.assignment = g.assignment AND s.userid = g.userid AND g.attemptnumber = s.attemptnumber JOIN(' . $esql . ') e ON e.id = s.userid WHERE s.latest = 1 AND s.assignment = :assignid AND s.timemodified IS NOT NULL AND s.status = :submitted AND (s.timemodified >= g.timemodified OR g.timemodified IS NULL OR g.grade IS NULL ' . $sqlscalegrade . ')'; return $DB->count_records_sql($sql, $params); } /** * Load a count of grades. * * @return int number of grades */ public function count_grades() { global $DB; if (!$this->has_instance()) { return 0; } $currentgroup = groups_get_activity_group($this->get_course_module(), true); list($esql, $params) = get_enrolled_sql($this->get_context(), 'mod/assign:submit', $currentgroup, true); $params['assignid'] = $this->get_instance()->id; $sql = 'SELECT COUNT(g.userid) FROM {assign_grades} g JOIN(' . $esql . ') e ON e.id = g.userid WHERE g.assignment = :assignid'; return $DB->count_records_sql($sql, $params); } /** * Load a count of submissions. * * @param bool $includenew When true, also counts the submissions with status 'new'. * @return int number of submissions */ public function count_submissions($includenew = false) { global $DB; if (!$this->has_instance()) { return 0; } $params = array(); $sqlnew = ''; if (!$includenew) { $sqlnew = ' AND s.status <> :status '; $params['status'] = ASSIGN_SUBMISSION_STATUS_NEW; } if ($this->get_instance()->teamsubmission) { // We cannot join on the enrolment tables for group submissions (no userid). $sql = 'SELECT COUNT(DISTINCT s.groupid) FROM {assign_submission} s WHERE s.assignment = :assignid AND s.timemodified IS NOT NULL AND s.userid = :groupuserid' . $sqlnew; $params['assignid'] = $this->get_instance()->id; $params['groupuserid'] = 0; } else { $currentgroup = groups_get_activity_group($this->get_course_module(), true); list($esql, $enrolparams) = get_enrolled_sql($this->get_context(), 'mod/assign:submit', $currentgroup, true); $params = array_merge($params, $enrolparams); $params['assignid'] = $this->get_instance()->id; $sql = 'SELECT COUNT(DISTINCT s.userid) FROM {assign_submission} s JOIN(' . $esql . ') e ON e.id = s.userid WHERE s.assignment = :assignid AND s.timemodified IS NOT NULL ' . $sqlnew; } return $DB->count_records_sql($sql, $params); } /** * Load a count of submissions with a specified status. * * @param string $status The submission status - should match one of the constants * @param mixed $currentgroup int|null the group for counting (if null the function will determine it) * @return int number of matching submissions */ public function count_submissions_with_status($status, $currentgroup = null) { global $DB; if ($currentgroup === null) { $currentgroup = groups_get_activity_group($this->get_course_module(), true); } list($esql, $params) = get_enrolled_sql($this->get_context(), '', $currentgroup, true); $params['assignid'] = $this->get_instance()->id; $params['assignid2'] = $this->get_instance()->id; $params['submissionstatus'] = $status; if ($this->get_instance()->teamsubmission) { $groupsstr = ''; if ($currentgroup != 0) { // If there is an active group we should only display the current group users groups. $participants = $this->list_participants($currentgroup, true); $groups = groups_get_all_groups($this->get_course()->id, array_keys($participants), $this->get_instance()->teamsubmissiongroupingid, 'DISTINCT g.id, g.name'); if (empty($groups)) { // If $groups is empty it means it is not part of $this->get_instance()->teamsubmissiongroupingid. // All submissions from students that do not belong to any of teamsubmissiongroupingid groups // count towards groupid = 0. Setting to true as only '0' key matters. $groups = [true]; } list($groupssql, $groupsparams) = $DB->get_in_or_equal(array_keys($groups), SQL_PARAMS_NAMED); $groupsstr = 's.groupid ' . $groupssql . ' AND'; $params = $params + $groupsparams; } $sql = 'SELECT COUNT(s.groupid) FROM {assign_submission} s WHERE s.latest = 1 AND s.assignment = :assignid AND s.timemodified IS NOT NULL AND s.userid = :groupuserid AND ' . $groupsstr . ' s.status = :submissionstatus'; $params['groupuserid'] = 0; } else { $sql = 'SELECT COUNT(s.userid) FROM {assign_submission} s JOIN(' . $esql . ') e ON e.id = s.userid WHERE s.latest = 1 AND s.assignment = :assignid AND s.timemodified IS NOT NULL AND s.status = :submissionstatus'; } return $DB->count_records_sql($sql, $params); } /** * Utility function to get the userid for every row in the grading table * so the order can be frozen while we iterate it. * * @param boolean $cached If true, the cached list from the session could be returned. * @param string $useridlistid String value used for caching the participant list. * @return array An array of userids */ protected function get_grading_userid_list($cached = false, $useridlistid = '') { global $SESSION; if ($cached) { if (empty($useridlistid)) { $useridlistid = $this->get_useridlist_key_id(); } $useridlistkey = $this->get_useridlist_key($useridlistid); if (empty($SESSION->mod_assign_useridlist[$useridlistkey])) { $SESSION->mod_assign_useridlist[$useridlistkey] = $this->get_grading_userid_list(false, ''); } return $SESSION->mod_assign_useridlist[$useridlistkey]; } $filter = get_user_preferences('assign_filter', ''); $table = new assign_grading_table($this, 0, $filter, 0, false); $useridlist = $table->get_column_data('userid'); return $useridlist; } /** * Is user id filtered by user filters and table preferences. * * @param int $userid User id that needs to be checked. * @return bool */ public function is_userid_filtered($userid) { $users = $this->get_grading_userid_list(); return in_array($userid, $users); } /** * Finds all assignment notifications that have yet to be mailed out, and mails them. * * Cron function to be run periodically according to the moodle cron. * * @return bool */ public static function cron() { global $DB; // Only ever send a max of one days worth of updates. $yesterday = time() - (24 * 3600); $timenow = time(); $task = \core\task\manager::get_scheduled_task(mod_assign\task\cron_task::class); $lastruntime = $task->get_last_run_time(); // Collect all submissions that require mailing. // Submissions are included if all are true: // - The assignment is visible in the gradebook. // - No previous notification has been sent. // - The grader was a real user, not an automated process. // - The grade was updated in the past 24 hours. // - If marking workflow is enabled, the workflow state is at 'released'. $sql = "SELECT g.id as gradeid, a.course, a.name, a.blindmarking, a.revealidentities, a.hidegrader, g.*, g.timemodified as lastmodified, cm.id as cmid, um.id as recordid FROM {assign} a JOIN {assign_grades} g ON g.assignment = a.id LEFT JOIN {assign_user_flags} uf ON uf.assignment = a.id AND uf.userid = g.userid JOIN {course_modules} cm ON cm.course = a.course AND cm.instance = a.id JOIN {modules} md ON md.id = cm.module AND md.name = 'assign' JOIN {grade_items} gri ON gri.iteminstance = a.id AND gri.courseid = a.course AND gri.itemmodule = md.name LEFT JOIN {assign_user_mapping} um ON g.id = um.userid AND um.assignment = a.id WHERE (a.markingworkflow = 0 OR (a.markingworkflow = 1 AND uf.workflowstate = :wfreleased)) AND g.grader > 0 AND uf.mailed = 0 AND gri.hidden = 0 AND g.timemodified >= :yesterday AND g.timemodified <= :today ORDER BY a.course, cm.id"; $params = array( 'yesterday' => $yesterday, 'today' => $timenow, 'wfreleased' => ASSIGN_MARKING_WORKFLOW_STATE_RELEASED, ); $submissions = $DB->get_records_sql($sql, $params); if (!empty($submissions)) { mtrace('Processing ' . count($submissions) . ' assignment submissions ...'); // Preload courses we are going to need those. $courseids = array(); foreach ($submissions as $submission) { $courseids[] = $submission->course; } // Filter out duplicates. $courseids = array_unique($courseids); $ctxselect = context_helper::get_preload_record_columns_sql('ctx'); list($courseidsql, $params) = $DB->get_in_or_equal($courseids, SQL_PARAMS_NAMED); $sql = 'SELECT c.*, ' . $ctxselect . ' FROM {course} c LEFT JOIN {context} ctx ON ctx.instanceid = c.id AND ctx.contextlevel = :contextlevel WHERE c.id ' . $courseidsql; $params['contextlevel'] = CONTEXT_COURSE; $courses = $DB->get_records_sql($sql, $params); // Clean up... this could go on for a while. unset($courseids); unset($ctxselect); unset($courseidsql); unset($params); // Message students about new feedback. foreach ($submissions as $submission) { mtrace("Processing assignment submission $submission->id ..."); // Do not cache user lookups - could be too many. if (!$user = $DB->get_record('user', array('id'=>$submission->userid))) { mtrace('Could not find user ' . $submission->userid); continue; } // Use a cache to prevent the same DB queries happening over and over. if (!array_key_exists($submission->course, $courses)) { mtrace('Could not find course ' . $submission->course); continue; } $course = $courses[$submission->course]; if (isset($course->ctxid)) { // Context has not yet been preloaded. Do so now. context_helper::preload_from_record($course); } // Override the language and timezone of the "current" user, so that // mail is customised for the receiver. \core\cron::setup_user($user, $course); // Context lookups are already cached. $coursecontext = context_course::instance($course->id); if (!is_enrolled($coursecontext, $user->id)) { $courseshortname = format_string($course->shortname, true, array('context' => $coursecontext)); mtrace(fullname($user) . ' not an active participant in ' . $courseshortname); continue; } if (!$grader = $DB->get_record('user', array('id'=>$submission->grader))) { mtrace('Could not find grader ' . $submission->grader); continue; } $modinfo = get_fast_modinfo($course, $user->id); $cm = $modinfo->get_cm($submission->cmid); // Context lookups are already cached. $contextmodule = context_module::instance($cm->id); if (!$cm->uservisible) { // Hold mail notification for assignments the user cannot access until later. continue; } // Notify the student. Default to the non-anon version. $messagetype = 'feedbackavailable'; // Message type needs 'anon' if "hidden grading" is enabled and the student // doesn't have permission to see the grader. if ($submission->hidegrader && !has_capability('mod/assign:showhiddengrader', $contextmodule, $user)) { $messagetype = 'feedbackavailableanon'; // There's no point in having an "anonymous grader" if the notification email // comes from them. Send the email from the noreply user instead. $grader = core_user::get_noreply_user(); } $eventtype = 'assign_notification'; $updatetime = $submission->lastmodified; $modulename = get_string('modulename', 'assign'); $uniqueid = 0; if ($submission->blindmarking && !$submission->revealidentities) { if (empty($submission->recordid)) { $uniqueid = self::get_uniqueid_for_user_static($submission->assignment, $grader->id); } else { $uniqueid = $submission->recordid; } } $showusers = $submission->blindmarking && !$submission->revealidentities; self::send_assignment_notification($grader, $user, $messagetype, $eventtype, $updatetime, $cm, $contextmodule, $course, $modulename, $submission->name, $showusers, $uniqueid); $flags = $DB->get_record('assign_user_flags', array('userid'=>$user->id, 'assignment'=>$submission->assignment)); if ($flags) { $flags->mailed = 1; $DB->update_record('assign_user_flags', $flags); } else { $flags = new stdClass(); $flags->userid = $user->id; $flags->assignment = $submission->assignment; $flags->mailed = 1; $DB->insert_record('assign_user_flags', $flags); } mtrace('Done'); } mtrace('Done processing ' . count($submissions) . ' assignment submissions'); \core\cron::setup_user(); // Free up memory just to be sure. unset($courses); } // Update calendar events to provide a description. $sql = 'SELECT id FROM {assign} WHERE allowsubmissionsfromdate >= :lastruntime AND allowsubmissionsfromdate <= :timenow AND alwaysshowdescription = 0'; $params = array('lastruntime' => $lastruntime, 'timenow' => $timenow); $newlyavailable = $DB->get_records_sql($sql, $params); foreach ($newlyavailable as $record) { $cm = get_coursemodule_from_instance('assign', $record->id, 0, false, MUST_EXIST); $context = context_module::instance($cm->id); $assignment = new assign($context, null, null); $assignment->update_calendar($cm->id); } return true; } /** * Mark in the database that this grade record should have an update notification sent by cron. * * @param stdClass $grade a grade record keyed on id * @param bool $mailedoverride when true, flag notification to be sent again. * @return bool true for success */ public function notify_grade_modified($grade, $mailedoverride = false) { global $DB; $flags = $this->get_user_flags($grade->userid, true); if ($flags->mailed != 1 || $mailedoverride) { $flags->mailed = 0; } return $this->update_user_flags($flags); } /** * Update user flags for this user in this assignment. * * @param stdClass $flags a flags record keyed on id * @return bool true for success */ public function update_user_flags($flags) { global $DB; if ($flags->userid <= 0 || $flags->assignment <= 0 || $flags->id <= 0) { return false; } $result = $DB->update_record('assign_user_flags', $flags); return $result; } /** * Update a grade in the grade table for the assignment and in the gradebook. * * @param stdClass $grade a grade record keyed on id * @param bool $reopenattempt If the attempt reopen method is manual, allow another attempt at this assignment. * @return bool true for success */ public function update_grade($grade, $reopenattempt = false) { global $DB; $grade->timemodified = time(); if (!empty($grade->workflowstate)) { $validstates = $this->get_marking_workflow_states_for_current_user(); if (!array_key_exists($grade->workflowstate, $validstates)) { return false; } } if ($grade->grade && $grade->grade != -1) { if ($this->get_instance()->grade > 0) { if (!is_numeric($grade->grade)) { return false; } else if ($grade->grade > $this->get_instance()->grade) { return false; } else if ($grade->grade < 0) { return false; } } else { // This is a scale. if ($scale = $DB->get_record('scale', array('id' => -($this->get_instance()->grade)))) { $scaleoptions = make_menu_from_list($scale->scale); if (!array_key_exists((int) $grade->grade, $scaleoptions)) { return false; } } } } if (empty($grade->attemptnumber)) { // Set it to the default. $grade->attemptnumber = 0; } $DB->update_record('assign_grades', $grade); $submission = null; if ($this->get_instance()->teamsubmission) { if (isset($this->mostrecentteamsubmission)) { $submission = $this->mostrecentteamsubmission; } else { $submission = $this->get_group_submission($grade->userid, 0, false); } } else { $submission = $this->get_user_submission($grade->userid, false); } // Only push to gradebook if the update is for the most recent attempt. if ($submission && $submission->attemptnumber != $grade->attemptnumber) { return true; } if ($this->gradebook_item_update(null, $grade)) { \mod_assign\event\submission_graded::create_from_grade($this, $grade)->trigger(); } // If the conditions are met, allow another attempt. if ($submission) { $isreopened = $this->reopen_submission_if_required($grade->userid, $submission, $reopenattempt); if ($isreopened) { $completion = new completion_info($this->get_course()); if ($completion->is_enabled($this->get_course_module()) && $this->get_instance()->completionsubmit) { $completion->update_state($this->get_course_module(), COMPLETION_INCOMPLETE, $grade->userid); } } } return true; } /** * View the grant extension date page. * * Uses url parameters 'userid' * or from parameter 'selectedusers' * * @param moodleform $mform - Used for validation of the submitted data * @return string */ protected function view_grant_extension($mform) { global $CFG; require_once($CFG->dirroot . '/mod/assign/extensionform.php'); $o = ''; $data = new stdClass(); $data->id = $this->get_course_module()->id; $formparams = array( 'instance' => $this->get_instance(), 'assign' => $this ); $users = optional_param('userid', 0, PARAM_INT); if (!$users) { if ($this->is_blind_marking()) { $blindid = optional_param('blindid', 0, PARAM_INT); $users = $this->get_user_id_for_uniqueid($blindid); } // We need users, if not found by blindid. if (!$users) { $users = required_param('selectedusers', PARAM_SEQUENCE); } } $userlist = explode(',', $users); $keys = array('duedate', 'cutoffdate', 'allowsubmissionsfromdate'); $maxoverride = array('allowsubmissionsfromdate' => 0, 'duedate' => 0, 'cutoffdate' => 0); foreach ($userlist as $userid) { // To validate extension date with users overrides. $override = $this->override_exists($userid); foreach ($keys as $key) { if ($override->{$key}) { if ($maxoverride[$key] < $override->{$key}) { $maxoverride[$key] = $override->{$key}; } } else if ($maxoverride[$key] < $this->get_instance()->{$key}) { $maxoverride[$key] = $this->get_instance()->{$key}; } } } foreach ($keys as $key) { if ($maxoverride[$key]) { $this->get_instance()->{$key} = $maxoverride[$key]; } } $formparams['userlist'] = $userlist; $data->selectedusers = $users; $data->userid = 0; if (empty($mform)) { $mform = new mod_assign_extension_form(null, $formparams); } $mform->set_data($data); $header = new assign_header($this->get_instance(), $this->get_context(), $this->show_intro(), $this->get_course_module()->id, get_string('grantextension', 'assign')); $o .= $this->get_renderer()->render($header); $o .= $this->get_renderer()->render(new assign_form('extensionform', $mform)); $o .= $this->view_footer(); return $o; } /** * Get a list of the users in the same group as this user. * * @param int $groupid The id of the group whose members we want or 0 for the default group * @param bool $onlyids Whether to retrieve only the user id's * @param bool $excludesuspended Whether to exclude suspended users * @return array The users (possibly id's only) */ public function get_submission_group_members($groupid, $onlyids, $excludesuspended = false) { $members = array(); if ($groupid != 0) { $allusers = $this->list_participants($groupid, $onlyids); foreach ($allusers as $user) { if ($this->get_submission_group($user->id)) { $members[] = $user; } } } else { $allusers = $this->list_participants(null, $onlyids); foreach ($allusers as $user) { if ($this->get_submission_group($user->id) == null) { $members[] = $user; } } } // Exclude suspended users, if user can't see them. if ($excludesuspended || !has_capability('moodle/course:viewsuspendedusers', $this->context)) { foreach ($members as $key => $member) { if (!$this->is_active_user($member->id)) { unset($members[$key]); } } } return $members; } /** * Get a list of the users in the same group as this user that have not submitted the assignment. * * @param int $groupid The id of the group whose members we want or 0 for the default group * @param bool $onlyids Whether to retrieve only the user id's * @return array The users (possibly id's only) */ public function get_submission_group_members_who_have_not_submitted($groupid, $onlyids) { $instance = $this->get_instance(); if (!$instance->teamsubmission || !$instance->requireallteammemberssubmit) { return array(); } $members = $this->get_submission_group_members($groupid, $onlyids); foreach ($members as $id => $member) { $submission = $this->get_user_submission($member->id, false); if ($submission && $submission->status == ASSIGN_SUBMISSION_STATUS_SUBMITTED) { unset($members[$id]); } else { if ($this->is_blind_marking()) { $members[$id]->alias = get_string('hiddenuser', 'assign') . $this->get_uniqueid_for_user($id); } } } return $members; } /** * Load the group submission object for a particular user, optionally creating it if required. * * @param int $userid The id of the user whose submission we want * @param int $groupid The id of the group for this user - may be 0 in which * case it is determined from the userid. * @param bool $create If set to true a new submission object will be created in the database * with the status set to "new". * @param int $attemptnumber - -1 means the latest attempt * @return stdClass|false The submission */ public function get_group_submission($userid, $groupid, $create, $attemptnumber=-1) { global $DB; if ($groupid == 0) { $group = $this->get_submission_group($userid); if ($group) { $groupid = $group->id; } } // Now get the group submission. $params = array('assignment'=>$this->get_instance()->id, 'groupid'=>$groupid, 'userid'=>0); if ($attemptnumber >= 0) { $params['attemptnumber'] = $attemptnumber; } // Only return the row with the highest attemptnumber. $submission = null; $submissions = $DB->get_records('assign_submission', $params, 'attemptnumber DESC', '*', 0, 1); if ($submissions) { $submission = reset($submissions); } if ($submission) { if ($create) { $action = optional_param('action', '', PARAM_TEXT); if ($action == 'editsubmission') { $starttimer = optional_param('begin', 0, PARAM_INT); // Only start the timer if the user has clicked the 'Begin assignment' button. if (empty($submission->timestarted) && $this->get_instance()->timelimit && $starttimer) { $submission->timestarted = time(); $DB->update_record('assign_submission', $submission); } } } return $submission; } if ($create) { $submission = new stdClass(); $submission->assignment = $this->get_instance()->id; $submission->userid = 0; $submission->groupid = $groupid; $submission->timecreated = time(); $submission->timemodified = $submission->timecreated; if ($attemptnumber >= 0) { $submission->attemptnumber = $attemptnumber; } else { $submission->attemptnumber = 0; } // Work out if this is the latest submission. $submission->latest = 0; $params = array('assignment'=>$this->get_instance()->id, 'groupid'=>$groupid, 'userid'=>0); if ($attemptnumber == -1) { // This is a new submission so it must be the latest. $submission->latest = 1; } else { // We need to work this out. $result = $DB->get_records('assign_submission', $params, 'attemptnumber DESC', 'attemptnumber', 0, 1); if ($result) { $latestsubmission = reset($result); } if (!$latestsubmission || ($attemptnumber == $latestsubmission->attemptnumber)) { $submission->latest = 1; } } $transaction = $DB->start_delegated_transaction(); if ($submission->latest) { // This is the case when we need to set latest to 0 for all the other attempts. $DB->set_field('assign_submission', 'latest', 0, $params); } $submission->status = ASSIGN_SUBMISSION_STATUS_NEW; $sid = $DB->insert_record('assign_submission', $submission); $transaction->allow_commit(); return $DB->get_record('assign_submission', array('id' => $sid)); } return false; } /** * View a page rendered by a plugin. * * Uses url parameters 'pluginaction', 'pluginsubtype', 'plugin', and 'id'. * * @return string */ protected function view_plugin_page() { global $USER; $o = ''; $pluginsubtype = required_param('pluginsubtype', PARAM_ALPHA); $plugintype = required_param('plugin', PARAM_PLUGIN); $pluginaction = required_param('pluginaction', PARAM_ALPHA); $plugin = $this->get_plugin_by_type($pluginsubtype, $plugintype); if (!$plugin) { throw new \moodle_exception('invalidformdata', ''); return; } $o .= $plugin->view_page($pluginaction); return $o; } /** * This is used for team assignments to get the group for the specified user. * If the user is a member of multiple or no groups this will return false * * @param int $userid The id of the user whose submission we want * @return mixed The group or false */ public function get_submission_group($userid) { if (isset($this->usersubmissiongroups[$userid])) { return $this->usersubmissiongroups[$userid]; } $groups = $this->get_all_groups($userid); if (count($groups) != 1) { $return = false; } else { $return = array_pop($groups); } // Cache the user submission group. $this->usersubmissiongroups[$userid] = $return; return $return; } /** * Gets all groups the user is a member of. * * @param int $userid Teh id of the user who's groups we are checking * @return array The group objects */ public function get_all_groups($userid) { if (isset($this->usergroups[$userid])) { return $this->usergroups[$userid]; } $grouping = $this->get_instance()->teamsubmissiongroupingid; $return = groups_get_all_groups($this->get_course()->id, $userid, $grouping, 'g.*', false, true); $this->usergroups[$userid] = $return; return $return; } /** * Display the submission that is used by a plugin. * * Uses url parameters 'sid', 'gid' and 'plugin'. * * @param string $pluginsubtype * @return string */ protected function view_plugin_content($pluginsubtype) { $o = ''; $submissionid = optional_param('sid', 0, PARAM_INT); $gradeid = optional_param('gid', 0, PARAM_INT); $plugintype = required_param('plugin', PARAM_PLUGIN); $item = null; if ($pluginsubtype == 'assignsubmission') { $plugin = $this->get_submission_plugin_by_type($plugintype); if ($submissionid <= 0) { throw new coding_exception('Submission id should not be 0'); } $item = $this->get_submission($submissionid); // Check permissions. if (empty($item->userid)) { // Group submission. $this->require_view_group_submission($item->groupid); } else { $this->require_view_submission($item->userid); } $o .= $this->get_renderer()->render(new assign_header($this->get_instance(), $this->get_context(), $this->show_intro(), $this->get_course_module()->id, $plugin->get_name())); $o .= $this->get_renderer()->render(new assign_submission_plugin_submission($plugin, $item, assign_submission_plugin_submission::FULL, $this->get_course_module()->id, $this->get_return_action(), $this->get_return_params())); // Trigger event for viewing a submission. \mod_assign\event\submission_viewed::create_from_submission($this, $item)->trigger(); } else { $plugin = $this->get_feedback_plugin_by_type($plugintype); if ($gradeid <= 0) { throw new coding_exception('Grade id should not be 0'); } $item = $this->get_grade($gradeid); // Check permissions. $this->require_view_submission($item->userid); $o .= $this->get_renderer()->render(new assign_header($this->get_instance(), $this->get_context(), $this->show_intro(), $this->get_course_module()->id, $plugin->get_name())); $o .= $this->get_renderer()->render(new assign_feedback_plugin_feedback($plugin, $item, assign_feedback_plugin_feedback::FULL, $this->get_course_module()->id, $this->get_return_action(), $this->get_return_params())); // Trigger event for viewing feedback. \mod_assign\event\feedback_viewed::create_from_grade($this, $item)->trigger(); } $o .= $this->view_return_links(); $o .= $this->view_footer(); return $o; } /** * Rewrite plugin file urls so they resolve correctly in an exported zip. * * @param string $text - The replacement text * @param stdClass $user - The user record * @param assign_plugin $plugin - The assignment plugin */ public function download_rewrite_pluginfile_urls($text, $user, $plugin) { // The groupname prefix for the urls doesn't depend on the group mode of the assignment instance. // Rather, it should be determined by checking the group submission settings of the instance, // which is what download_submission() does when generating the file name prefixes. $groupname = ''; if ($this->get_instance()->teamsubmission) { $submissiongroup = $this->get_submission_group($user->id); if ($submissiongroup) { $groupname = $submissiongroup->name . '-'; } else { $groupname = get_string('defaultteam', 'assign') . '-'; } } if ($this->is_blind_marking()) { $prefix = $groupname . get_string('participant', 'assign'); $prefix = str_replace('_', ' ', $prefix); $prefix = clean_filename($prefix . '_' . $this->get_uniqueid_for_user($user->id) . '_'); } else { $prefix = $groupname . fullname($user); $prefix = str_replace('_', ' ', $prefix); $prefix = clean_filename($prefix . '_' . $this->get_uniqueid_for_user($user->id) . '_'); } // Only prefix files if downloadasfolders user preference is NOT set. if (!get_user_preferences('assign_downloadasfolders', 1)) { $subtype = $plugin->get_subtype(); $type = $plugin->get_type(); $prefix = $prefix . $subtype . '_' . $type . '_'; } else { $prefix = ""; } $result = str_replace('@@PLUGINFILE@@/', $prefix, $text); return $result; } /** * Render the content in editor that is often used by plugin. * * @param string $filearea * @param int $submissionid * @param string $plugintype * @param string $editor * @param string $component * @param bool $shortentext Whether to shorten the text content. * @return string */ public function render_editor_content($filearea, $submissionid, $plugintype, $editor, $component, $shortentext = false) { global $CFG; $result = ''; $plugin = $this->get_submission_plugin_by_type($plugintype); $text = $plugin->get_editor_text($editor, $submissionid); if ($shortentext) { $text = shorten_text($text, 140); } $format = $plugin->get_editor_format($editor, $submissionid); $finaltext = file_rewrite_pluginfile_urls($text, 'pluginfile.php', $this->get_context()->id, $component, $filearea, $submissionid); $params = array('overflowdiv' => true, 'context' => $this->get_context()); $result .= format_text($finaltext, $format, $params); if ($CFG->enableportfolios && has_capability('mod/assign:exportownsubmission', $this->context)) { require_once($CFG->libdir . '/portfoliolib.php'); $button = new portfolio_add_button(); $portfolioparams = array('cmid' => $this->get_course_module()->id, 'sid' => $submissionid, 'plugin' => $plugintype, 'editor' => $editor, 'area'=>$filearea); $button->set_callback_options('assign_portfolio_caller', $portfolioparams, 'mod_assign'); $fs = get_file_storage(); if ($files = $fs->get_area_files($this->context->id, $component, $filearea, $submissionid, 'timemodified', false)) { $button->set_formats(PORTFOLIO_FORMAT_RICHHTML); } else { $button->set_formats(PORTFOLIO_FORMAT_PLAINHTML); } $result .= $button->to_html(PORTFOLIO_ADD_TEXT_LINK); } return $result; } /** * Display a continue page after grading. * * @param string $message - The message to display. * @return string */ protected function view_savegrading_result($message) { $o = ''; $o .= $this->get_renderer()->render(new assign_header($this->get_instance(), $this->get_context(), $this->show_intro(), $this->get_course_module()->id, get_string('savegradingresult', 'assign'))); $gradingresult = new assign_gradingmessage(get_string('savegradingresult', 'assign'), $message, $this->get_course_module()->id); $o .= $this->get_renderer()->render($gradingresult); $o .= $this->view_footer(); return $o; } /** * Display a continue page after quickgrading. * * @param string $message - The message to display. * @return string */ protected function view_quickgrading_result($message) { $o = ''; $o .= $this->get_renderer()->render(new assign_header($this->get_instance(), $this->get_context(), $this->show_intro(), $this->get_course_module()->id, get_string('quickgradingresult', 'assign'))); $gradingerror = in_array($message, $this->get_error_messages()); $lastpage = optional_param('lastpage', null, PARAM_INT); $gradingresult = new assign_gradingmessage(get_string('quickgradingresult', 'assign'), $message, $this->get_course_module()->id, $gradingerror, $lastpage); $o .= $this->get_renderer()->render($gradingresult); $o .= $this->view_footer(); return $o; } /** * Display the page footer. * * @return string */ protected function view_footer() { // When viewing the footer during PHPUNIT tests a set_state error is thrown. if (!PHPUNIT_TEST) { return $this->get_renderer()->render_footer(); } return ''; } /** * Throw an error if the permissions to view this users' group submission are missing. * * @param int $groupid Group id. * @throws required_capability_exception */ public function require_view_group_submission($groupid) { if (!$this->can_view_group_submission($groupid)) { throw new required_capability_exception($this->context, 'mod/assign:viewgrades', 'nopermission', ''); } } /** * Throw an error if the permissions to view this users submission are missing. * * @throws required_capability_exception * @return none */ public function require_view_submission($userid) { if (!$this->can_view_submission($userid)) { throw new required_capability_exception($this->context, 'mod/assign:viewgrades', 'nopermission', ''); } } /** * Throw an error if the permissions to view grades in this assignment are missing. * * @throws required_capability_exception * @return none */ public function require_view_grades() { if (!$this->can_view_grades()) { throw new required_capability_exception($this->context, 'mod/assign:viewgrades', 'nopermission', ''); } } /** * Does this user have view grade or grade permission for this assignment? * * @param mixed $groupid int|null when is set to a value, use this group instead calculating it * @return bool */ public function can_view_grades($groupid = null) { // Permissions check. if (!has_any_capability(array('mod/assign:viewgrades', 'mod/assign:grade'), $this->context)) { return false; } // Checks for the edge case when user belongs to no groups and groupmode is sep. if ($this->get_course_module()->effectivegroupmode == SEPARATEGROUPS) { if ($groupid === null) { $groupid = groups_get_activity_allowed_groups($this->get_course_module()); } $groupflag = has_capability('moodle/site:accessallgroups', $this->get_context()); $groupflag = $groupflag || !empty($groupid); return (bool)$groupflag; } return true; } /** * Does this user have grade permission for this assignment? * * @param int|stdClass $user The object or id of the user who will do the editing (default to current user). * @return bool */ public function can_grade($user = null) { // Permissions check. if (!has_capability('mod/assign:grade', $this->context, $user)) { return false; } return true; } /** * Download a zip file of all assignment submissions. * * @param int[]|null $userids Array of user ids to download assignment submissions in a zip file * @return string - If an error occurs, this will contain the error page. */ protected function download_submissions($userids = null) { $downloader = new downloader($this, $userids ?: null); if ($downloader->load_filelist()) { $downloader->download_zip(); } // Show some notification if we have nothing to download. $cm = $this->get_course_module(); $renderer = $this->get_renderer(); $header = new assign_header( $this->get_instance(), $this->get_context(), '', $cm->id, get_string('downloadall', 'mod_assign') ); $result = $renderer->render($header); $result .= $renderer->notification(get_string('nosubmission', 'mod_assign')); $url = new moodle_url('/mod/assign/view.php', ['id' => $cm->id, 'action' => 'grading']); $result .= $renderer->continue_button($url); $result .= $this->view_footer(); return $result; } /** * Lazy load the page renderer and expose the renderer to plugins. * * @return assign_renderer */ public function get_renderer() { global $PAGE; if ($this->output) { return $this->output; } $this->output = $PAGE->get_renderer('mod_assign', null, RENDERER_TARGET_GENERAL); return $this->output; } /** * Load the submission object for a particular user, optionally creating it if required. * * For team assignments there are 2 submissions - the student submission and the team submission * All files are associated with the team submission but the status of the students contribution is * recorded separately. * * @param int $userid The id of the user whose submission we want or 0 in which case USER->id is used * @param bool $create If set to true a new submission object will be created in the database with the status set to "new". * @param int $attemptnumber - -1 means the latest attempt * @return stdClass|false The submission */ public function get_user_submission($userid, $create, $attemptnumber=-1) { global $DB, $USER; if (!$userid) { $userid = $USER->id; } // If the userid is not null then use userid. $params = array('assignment'=>$this->get_instance()->id, 'userid'=>$userid, 'groupid'=>0); if ($attemptnumber >= 0) { $params['attemptnumber'] = $attemptnumber; } // Only return the row with the highest attemptnumber. $submission = null; $submissions = $DB->get_records('assign_submission', $params, 'attemptnumber DESC', '*', 0, 1); if ($submissions) { $submission = reset($submissions); } if ($submission) { if ($create) { $action = optional_param('action', '', PARAM_TEXT); if ($action == 'editsubmission') { $starttimer = optional_param('begin', 0, PARAM_INT); // Only start the timer if the user has clicked the 'Begin assignment' button. if (empty($submission->timestarted) && $this->get_instance()->timelimit && $starttimer) { $submission->timestarted = time(); $DB->update_record('assign_submission', $submission); } } } return $submission; } if ($create) { $submission = new stdClass(); $submission->assignment = $this->get_instance()->id; $submission->userid = $userid; $submission->timecreated = time(); $submission->timemodified = $submission->timecreated; $submission->status = ASSIGN_SUBMISSION_STATUS_NEW; if ($attemptnumber >= 0) { $submission->attemptnumber = $attemptnumber; } else { $submission->attemptnumber = 0; } // Work out if this is the latest submission. $submission->latest = 0; $params = array('assignment'=>$this->get_instance()->id, 'userid'=>$userid, 'groupid'=>0); if ($attemptnumber == -1) { // This is a new submission so it must be the latest. $submission->latest = 1; } else { // We need to work this out. $result = $DB->get_records('assign_submission', $params, 'attemptnumber DESC', 'attemptnumber', 0, 1); $latestsubmission = null; if ($result) { $latestsubmission = reset($result); } if (empty($latestsubmission) || ($attemptnumber > $latestsubmission->attemptnumber)) { $submission->latest = 1; } } $transaction = $DB->start_delegated_transaction(); if ($submission->latest) { // This is the case when we need to set latest to 0 for all the other attempts. $DB->set_field('assign_submission', 'latest', 0, $params); } $sid = $DB->insert_record('assign_submission', $submission); $transaction->allow_commit(); return $DB->get_record('assign_submission', array('id' => $sid)); } return false; } /** * Load the submission object from it's id. * * @param int $submissionid The id of the submission we want * @return stdClass The submission */ protected function get_submission($submissionid) { global $DB; $params = array('assignment'=>$this->get_instance()->id, 'id'=>$submissionid); return $DB->get_record('assign_submission', $params, '*', MUST_EXIST); } /** * This will retrieve a user flags object from the db optionally creating it if required. * The user flags was split from the user_grades table in 2.5. * * @param int $userid The user we are getting the flags for. * @param bool $create If true the flags record will be created if it does not exist * @return stdClass The flags record */ public function get_user_flags($userid, $create) { global $DB, $USER; // If the userid is not null then use userid. if (!$userid) { $userid = $USER->id; } $params = array('assignment'=>$this->get_instance()->id, 'userid'=>$userid); $flags = $DB->get_record('assign_user_flags', $params); if ($flags) { return $flags; } if ($create) { $flags = new stdClass(); $flags->assignment = $this->get_instance()->id; $flags->userid = $userid; $flags->locked = 0; $flags->extensionduedate = 0; $flags->workflowstate = ''; $flags->allocatedmarker = 0; // The mailed flag can be one of 3 values: 0 is unsent, 1 is sent and 2 is do not send yet. // This is because students only want to be notified about certain types of update (grades and feedback). $flags->mailed = 2; $fid = $DB->insert_record('assign_user_flags', $flags); $flags->id = $fid; return $flags; } return false; } /** * This will retrieve a grade object from the db, optionally creating it if required. * * @param int $userid The user we are grading * @param bool $create If true the grade will be created if it does not exist * @param int $attemptnumber The attempt number to retrieve the grade for. -1 means the latest submission. * @return stdClass The grade record */ public function get_user_grade($userid, $create, $attemptnumber=-1) { global $DB, $USER; // If the userid is not null then use userid. if (!$userid) { $userid = $USER->id; } $submission = null; $params = array('assignment'=>$this->get_instance()->id, 'userid'=>$userid); if ($attemptnumber < 0 || $create) { // Make sure this grade matches the latest submission attempt. if ($this->get_instance()->teamsubmission) { $submission = $this->get_group_submission($userid, 0, true, $attemptnumber); } else { $submission = $this->get_user_submission($userid, true, $attemptnumber); } if ($submission) { $attemptnumber = $submission->attemptnumber; } } if ($attemptnumber >= 0) { $params['attemptnumber'] = $attemptnumber; } $grades = $DB->get_records('assign_grades', $params, 'attemptnumber DESC', '*', 0, 1); if ($grades) { return reset($grades); } if ($create) { $grade = new stdClass(); $grade->assignment = $this->get_instance()->id; $grade->userid = $userid; $grade->timecreated = time(); // If we are "auto-creating" a grade - and there is a submission // the new grade should not have a more recent timemodified value // than the submission. if ($submission) { $grade->timemodified = $submission->timemodified; } else { $grade->timemodified = $grade->timecreated; } $grade->grade = -1; // Do not set the grader id here as it would be the admin users which is incorrect. $grade->grader = -1; if ($attemptnumber >= 0) { $grade->attemptnumber = $attemptnumber; } $grade->penalty = 0.0; $gid = $DB->insert_record('assign_grades', $grade); $grade->id = $gid; return $grade; } return false; } /** * This will retrieve a grade object from the db. * * @param int $gradeid The id of the grade * @return stdClass The grade record */ protected function get_grade($gradeid) { global $DB; $params = array('assignment'=>$this->get_instance()->id, 'id'=>$gradeid); return $DB->get_record('assign_grades', $params, '*', MUST_EXIST); } /** * Print the grading page for a single user submission. * * @param array $args Optional args array (better than pulling args from _GET and _POST) * @return string */ protected function view_single_grading_panel($args) { global $DB, $CFG; $o = ''; require_once($CFG->dirroot . '/mod/assign/gradeform.php'); // Need submit permission to submit an assignment. require_capability('mod/assign:grade', $this->context); // If userid is passed - we are only grading a single student. $userid = $args['userid']; $attemptnumber = $args['attemptnumber']; $instance = $this->get_instance($userid); // Apply overrides. $this->update_effective_access($userid); $rownum = 0; $useridlist = array($userid); $last = true; // This variation on the url will link direct to this student, with no next/previous links. // The benefit is the url will be the same every time for this student, so Atto autosave drafts can match up. $returnparams = array('userid' => $userid, 'rownum' => 0, 'useridlistid' => 0); $this->register_return_link('grade', $returnparams); $user = $DB->get_record('user', array('id' => $userid)); $submission = $this->get_user_submission($userid, false, $attemptnumber); $submissiongroup = null; $teamsubmission = null; $notsubmitted = array(); if ($instance->teamsubmission) { $teamsubmission = $this->get_group_submission($userid, 0, false, $attemptnumber); $submissiongroup = $this->get_submission_group($userid); $groupid = 0; if ($submissiongroup) { $groupid = $submissiongroup->id; } $notsubmitted = $this->get_submission_group_members_who_have_not_submitted($groupid, false); } // Get the requested grade. $grade = $this->get_user_grade($userid, false, $attemptnumber); $flags = $this->get_user_flags($userid, false); if ($this->can_view_submission($userid)) { $submissionlocked = ($flags && $flags->locked); $extensionduedate = null; if ($flags) { $extensionduedate = $flags->extensionduedate; } $showedit = $this->submissions_open($userid) && ($this->is_any_submission_plugin_enabled()); $viewfullnames = has_capability('moodle/site:viewfullnames', $this->get_context()); $usergroups = $this->get_all_groups($user->id); $submissionstatus = new assign_submission_status_compact($instance->allowsubmissionsfromdate, $instance->alwaysshowdescription, $submission, $instance->teamsubmission, $teamsubmission, $submissiongroup, $notsubmitted, $this->is_any_submission_plugin_enabled(), $submissionlocked, $this->is_graded($userid), $instance->duedate, $instance->cutoffdate, $this->get_submission_plugins(), $this->get_return_action(), $this->get_return_params(), $this->get_course_module()->id, $this->get_course()->id, assign_submission_status::GRADER_VIEW, $showedit, false, $viewfullnames, $extensionduedate, $this->get_context(), $this->is_blind_marking(), '', $instance->attemptreopenmethod, $instance->maxattempts, $this->get_grading_status($userid), $instance->preventsubmissionnotingroup, $usergroups, $instance->timelimit); $o .= $this->get_renderer()->render($submissionstatus); } if ($grade) { $data = new stdClass(); if ($grade->grade !== null && $grade->grade >= 0) { $data->grade = format_float($grade->grade, $this->get_grade_item()->get_decimals()); } } else { $data = new stdClass(); $data->grade = ''; } if (!empty($flags->workflowstate)) { $data->workflowstate = $flags->workflowstate; } if (!empty($flags->allocatedmarker)) { $data->allocatedmarker = $flags->allocatedmarker; } // Warning if required. $allsubmissions = $this->get_all_submissions($userid); if ($attemptnumber != -1 && ($attemptnumber + 1) != count($allsubmissions)) { $params = array('attemptnumber' => $attemptnumber + 1, 'totalattempts' => count($allsubmissions)); $message = get_string('editingpreviousfeedbackwarning', 'assign', $params); $o .= $this->get_renderer()->notification($message); } $pagination = array('rownum' => $rownum, 'useridlistid' => 0, 'last' => $last, 'userid' => $userid, 'attemptnumber' => $attemptnumber, 'gradingpanel' => true); if (!empty($args['formdata'])) { $data = (array) $data; $data = (object) array_merge($data, $args['formdata']); } $formparams = array($this, $data, $pagination); $mform = new mod_assign_grade_form(null, $formparams, 'post', '', array('class' => 'gradeform')); if (!empty($args['formdata'])) { // If we were passed form data - we want the form to check the data // and show errors. $mform->is_validated(); } $o .= $this->get_renderer()->render(new assign_form('gradingform', $mform)); if (count($allsubmissions) > 1) { $allgrades = $this->get_all_grades($userid); $history = new assign_attempt_history_chooser($allsubmissions, $allgrades, $this->get_course_module()->id, $userid); $o .= $this->get_renderer()->render($history); } \mod_assign\event\grading_form_viewed::create_from_user($this, $user)->trigger(); return $o; } /** * Print the grading page for a single user submission. * * @param moodleform $mform * @return string */ protected function view_single_grade_page($mform) { global $DB, $CFG, $SESSION; $o = ''; $instance = $this->get_instance(); require_once($CFG->dirroot . '/mod/assign/gradeform.php'); // Need submit permission to submit an assignment. require_capability('mod/assign:grade', $this->context); $header = new assign_header($instance, $this->get_context(), false, $this->get_course_module()->id, get_string('grading', 'assign')); $o .= $this->get_renderer()->render($header); // If userid is passed - we are only grading a single student. $rownum = optional_param('rownum', 0, PARAM_INT); $useridlistid = optional_param('useridlistid', $this->get_useridlist_key_id(), PARAM_ALPHANUM); $userid = optional_param('userid', 0, PARAM_INT); $attemptnumber = optional_param('attemptnumber', -1, PARAM_INT); if (!$userid) { $useridlist = $this->get_grading_userid_list(true, $useridlistid); } else { $rownum = 0; $useridlistid = 0; $useridlist = array($userid); } if ($rownum < 0 || $rownum > count($useridlist)) { throw new coding_exception('Row is out of bounds for the current grading table: ' . $rownum); } $last = false; $userid = $useridlist[$rownum]; if ($rownum == count($useridlist) - 1) { $last = true; } // This variation on the url will link direct to this student, with no next/previous links. // The benefit is the url will be the same every time for this student, so Atto autosave drafts can match up. $returnparams = array('userid' => $userid, 'rownum' => 0, 'useridlistid' => 0); $this->register_return_link('grade', $returnparams); $user = $DB->get_record('user', array('id' => $userid)); if ($user) { $this->update_effective_access($userid); $viewfullnames = has_capability('moodle/site:viewfullnames', $this->get_context()); $usersummary = new assign_user_summary($user, $this->get_course()->id, $viewfullnames, $this->is_blind_marking(), $this->get_uniqueid_for_user($user->id), // TODO Does not support custom user profile fields (MDL-70456). \core_user\fields::get_identity_fields($this->get_context(), false), !$this->is_active_user($userid)); $o .= $this->get_renderer()->render($usersummary); } $submission = $this->get_user_submission($userid, false, $attemptnumber); $submissiongroup = null; $teamsubmission = null; $notsubmitted = array(); if ($instance->teamsubmission) { $teamsubmission = $this->get_group_submission($userid, 0, false, $attemptnumber); $submissiongroup = $this->get_submission_group($userid); $groupid = 0; if ($submissiongroup) { $groupid = $submissiongroup->id; } $notsubmitted = $this->get_submission_group_members_who_have_not_submitted($groupid, false); } // Get the requested grade. $grade = $this->get_user_grade($userid, false, $attemptnumber); $flags = $this->get_user_flags($userid, false); if ($this->can_view_submission($userid)) { $submissionlocked = ($flags && $flags->locked); $extensionduedate = null; if ($flags) { $extensionduedate = $flags->extensionduedate; } $showedit = $this->submissions_open($userid) && ($this->is_any_submission_plugin_enabled()); $viewfullnames = has_capability('moodle/site:viewfullnames', $this->get_context()); $usergroups = $this->get_all_groups($user->id); $submissionstatus = new assign_submission_status($instance->allowsubmissionsfromdate, $instance->alwaysshowdescription, $submission, $instance->teamsubmission, $teamsubmission, $submissiongroup, $notsubmitted, $this->is_any_submission_plugin_enabled(), $submissionlocked, $this->is_graded($userid), $instance->duedate, $instance->cutoffdate, $this->get_submission_plugins(), $this->get_return_action(), $this->get_return_params(), $this->get_course_module()->id, $this->get_course()->id, assign_submission_status::GRADER_VIEW, $showedit, false, $viewfullnames, $extensionduedate, $this->get_context(), $this->is_blind_marking(), '', $instance->attemptreopenmethod, $instance->maxattempts, $this->get_grading_status($userid), $instance->preventsubmissionnotingroup, $usergroups, $instance->timelimit); $o .= $this->get_renderer()->render($submissionstatus); } if ($grade) { $data = new stdClass(); if ($grade->grade !== null && $grade->grade >= 0) { $data->grade = format_float($grade->grade, $this->get_grade_item()->get_decimals()); } } else { $data = new stdClass(); $data->grade = ''; } if (!empty($flags->workflowstate)) { $data->workflowstate = $flags->workflowstate; } if (!empty($flags->allocatedmarker)) { $data->allocatedmarker = $flags->allocatedmarker; } // Warning if required. $allsubmissions = $this->get_all_submissions($userid); if ($attemptnumber != -1 && ($attemptnumber + 1) != count($allsubmissions)) { $params = array('attemptnumber'=>$attemptnumber + 1, 'totalattempts'=>count($allsubmissions)); $message = get_string('editingpreviousfeedbackwarning', 'assign', $params); $o .= $this->get_renderer()->notification($message); } // Now show the grading form. if (!$mform) { $pagination = array('rownum' => $rownum, 'useridlistid' => $useridlistid, 'last' => $last, 'userid' => $userid, 'attemptnumber' => $attemptnumber); $formparams = array($this, $data, $pagination); $mform = new mod_assign_grade_form(null, $formparams, 'post', '', array('class'=>'gradeform')); } $o .= $this->get_renderer()->render(new assign_form('gradingform', $mform)); if (count($allsubmissions) > 1 && $attemptnumber == -1) { $allgrades = $this->get_all_grades($userid); $history = new assign_attempt_history($allsubmissions, $allgrades, $this->get_submission_plugins(), $this->get_feedback_plugins(), $this->get_course_module()->id, $this->get_return_action(), $this->get_return_params(), true, $useridlistid, $rownum); $o .= $this->get_renderer()->render($history); } \mod_assign\event\grading_form_viewed::create_from_user($this, $user)->trigger(); $o .= $this->view_footer(); return $o; } /** * Show a confirmation page to make sure they want to remove submission data. * * @return string */ protected function view_remove_submission_confirm() { global $USER; $userid = optional_param('userid', 0, PARAM_INT); $blindid = optional_param('blindid', 0, PARAM_INT); // Construct the base URL parameters for the confirm and cancel actions. $urlparams = [ 'id' => $this->get_course_module()->id, 'action' => 'removesubmission', 'sesskey' => sesskey(), ]; // Determine the correct user ID. if ($userid) { // Real user ID was explicitly provided. $urlparams['userid'] = $userid; } elseif ($this->is_blind_marking() && $blindid) { // Blind marking is in use. Resolve anonymized (blind) ID to the real user ID to obtain the user object later. $userid = $this->get_user_id_for_uniqueid($blindid); $urlparams['blindid'] = $blindid; } else { // Default to the currently logged-in user (e.g. user deleting their own submission scenario). $userid = $USER->id; $urlparams['userid'] = $userid; } if (!$this->can_edit_submission($userid, $USER->id)) { throw new \moodle_exception('nopermission'); } $user = core_user::get_user($userid, '*', MUST_EXIST); $o = ''; $header = new assign_header($this->get_instance(), $this->get_context(), false, $this->get_course_module()->id); $o .= $this->get_renderer()->render($header); $confirmurl = new moodle_url('/mod/assign/view.php', $urlparams); $urlparams = array('id' => $this->get_course_module()->id, 'action' => 'view'); $cancelurl = new moodle_url('/mod/assign/view.php', $urlparams); if ($userid == $USER->id) { if ($this->is_time_limit_enabled($userid)) { $confirmstr = get_string('removesubmissionconfirmwithtimelimit', 'assign'); } else { $confirmstr = get_string('removesubmissionconfirm', 'assign'); } } else { if ($this->is_time_limit_enabled($userid)) { $confirmstr = get_string('removesubmissionconfirmforstudentwithtimelimit', 'assign', $this->fullname($user)); } else { $confirmstr = get_string('removesubmissionconfirmforstudent', 'assign', $this->fullname($user)); } } $o .= $this->get_renderer()->confirm($confirmstr, $confirmurl, $cancelurl); $o .= $this->view_footer(); \mod_assign\event\remove_submission_form_viewed::create_from_user($this, $user)->trigger(); return $o; } /** * Show a confirmation page to make sure they want to release student identities. * * @return string */ protected function view_reveal_identities_confirm() { require_capability('mod/assign:revealidentities', $this->get_context()); $o = ''; $header = new assign_header($this->get_instance(), $this->get_context(), false, $this->get_course_module()->id); $o .= $this->get_renderer()->render($header); $urlparams = array('id'=>$this->get_course_module()->id, 'action'=>'revealidentitiesconfirm', 'sesskey'=>sesskey()); $confirmurl = new moodle_url('/mod/assign/view.php', $urlparams); $urlparams = array('id'=>$this->get_course_module()->id, 'action'=>'grading'); $cancelurl = new moodle_url('/mod/assign/view.php', $urlparams); $o .= $this->get_renderer()->confirm(get_string('revealidentitiesconfirm', 'assign'), $confirmurl, $cancelurl); $o .= $this->view_footer(); \mod_assign\event\reveal_identities_confirmation_page_viewed::create_from_assign($this)->trigger(); return $o; } /** * View a link to go back to the previous page. Uses url parameters returnaction and returnparams. * * @return string */ protected function view_return_links() { $returnaction = optional_param('returnaction', '', PARAM_ALPHA); $returnparams = optional_param('returnparams', '', PARAM_TEXT); $params = array(); $returnparams = str_replace('&', '&', $returnparams); parse_str($returnparams, $params); $newparams = array('id' => $this->get_course_module()->id, 'action' => $returnaction); $params = array_merge($newparams, $params); $url = new moodle_url('/mod/assign/view.php', $params); return $this->get_renderer()->single_button($url, get_string('back'), 'get'); } /** * View the grading table of all submissions for this assignment. * * @return string */ protected function view_grading_table() { global $USER, $CFG, $SESSION, $PAGE, $OUTPUT; require_once($CFG->dirroot . '/mod/assign/quickgradingform.php'); $submittedfilter = optional_param('status', null, PARAM_ALPHA); if (isset($submittedfilter)) { $validfilters = array_column($this->get_filters(), 'key'); $validfilters = array_diff($validfilters, [ASSIGN_FILTER_NONE]); // The 'none' filter is not a real filter. if ($submittedfilter === '' || in_array($submittedfilter, $validfilters)) { set_user_preference('assign_filter', $submittedfilter); } } $submittedquickgrading = optional_param('quickgrading', null, PARAM_BOOL); if (isset($submittedquickgrading)) { set_user_preference('assign_quickgrading', $submittedquickgrading); } $submitteddownloadasfolders = optional_param('downloadasfolders', null, PARAM_BOOL); if (isset($submitteddownloadasfolders)) { set_user_preference('assign_downloadasfolders', $submitteddownloadasfolders); } $submittedperpage = optional_param('perpage', null, PARAM_INT); if (isset($submittedperpage)) { set_user_preference('assign_perpage', $submittedperpage); } $o = ''; $cmid = $this->get_course_module()->id; $gradingmanager = get_grading_manager($this->get_context(), 'mod_assign', 'submissions'); $perpage = $this->get_assign_perpage(); $filter = get_user_preferences('assign_filter', ''); // Retrieve the 'workflowfilter' parameter, or set it to null if not provided. $workflowfilter = optional_param('workflowfilter', null, PARAM_ALPHA); // Check if the parameter is not null and if it exists in the list of valid workflow filters. if ($workflowfilter !== null && array_key_exists($workflowfilter, $this->get_marking_workflow_filters())) { // Save the valid 'workflowfilter' value as a user preference. set_user_preference('assign_workflowfilter', $workflowfilter); } // Retrieve the 'markingallocationfilter' parameter, or set it to null if not provided. $markingallocationfilter = optional_param('markingallocationfilter', null, PARAM_ALPHANUMEXT); // Check if the parameter is not null and if it exists in the list of valid marking allocation filters. if ($markingallocationfilter !== null && array_key_exists($markingallocationfilter, $this->get_marking_allocation_filters())) { // Save the valid 'markingallocationfilter' value as a user preference. set_user_preference('assign_markerfilter', $markingallocationfilter); } // Retrieve the 'suspendedparticipantsfilter' parameter, or set it to null if not provided. $suspendedparticipantsfilter = optional_param('suspendedparticipantsfilter', null, PARAM_BOOL); if ($suspendedparticipantsfilter !== null && has_capability('moodle/course:viewsuspendedusers', $this->get_context())) { // Save the 'suspendedparticipantsfilter' value as a user preference. set_user_preference('grade_report_showonlyactiveenrol', !$suspendedparticipantsfilter); } $controller = $gradingmanager->get_active_controller(); $showquickgrading = empty($controller) && $this->can_grade(); $quickgrading = get_user_preferences('assign_quickgrading', false); $markingallocation = $this->get_instance()->markingworkflow && $this->get_instance()->markingallocation && has_capability('mod/assign:manageallocations', $this->context); $markingworkflow = $this->get_instance()->markingworkflow; // Load the table of submissions, to be printed further down. $usequickgrading = $showquickgrading && $quickgrading; $gradingtable = new assign_grading_table($this, $perpage, $filter, 0, $usequickgrading); $gradingtable->responsive = false; $table = $this->get_renderer()->render($gradingtable); // This initialises the selected first/last initials for the action menu. $gradingtable->initialbars(true); $buttons = new \mod_assign\output\grading_actionmenu( cmid: $this->get_course_module()->id, assign: $this, userinitials: [ 'firstname' => $gradingtable->get_initial_first(), 'lastname' => $gradingtable->get_initial_last(), ] ); $actionformtext = $this->get_renderer()->render($buttons); $currenturl = new moodle_url('/mod/assign/view.php', ['id' => $this->get_course_module()->id, 'action' => 'grading']); $PAGE->activityheader->set_attrs(['hidecompletion' => true]); $PAGE->requires->js_call_amd('mod_assign/user', 'init', [$currenturl->out(false)]); // Conditionally add the group JS if we have groups enabled. if (groups_get_activity_groupmode($this->get_course_module(), $this->get_course())) { $PAGE->requires->js_call_amd('core_course/actionbar/group', 'init', [$currenturl->out(false), $cmid]); } $header = new assign_header($this->get_instance(), $this->get_context(), false, $this->get_course_module()->id, get_string('gradeitem:submissions', 'assign'), '', '', $currenturl); $o .= $this->get_renderer()->render($header); $o .= $actionformtext; if ($this->is_blind_marking() && has_capability('mod/assign:viewblinddetails', $this->get_context())) { if ($this->is_marking_anonymous()) { $o .= $this->get_renderer()->notification(get_string('blindmarkingenabledwarning', 'assign'), 'notifymessage'); } else { $o .= $this->get_renderer()->notification(get_string('blindmarkingnogradewarning', 'assign'), 'notifymessage'); } } // Print the table of submissions. $footerdata = [ 'perpage' => $gradingtable->get_paging_selector(), 'pagingbar' => $gradingtable->get_paging_bar(), 'hassubmit' => $usequickgrading, 'sendstudentnotifications' => $this->get_instance()->sendstudentnotifications, ]; $footer = new core\output\sticky_footer($OUTPUT->render_from_template('mod_assign/grading_sticky_footer', $footerdata)); if ($usequickgrading) { $page = optional_param('page', null, PARAM_INT); $PAGE->requires->js_call_amd('mod_assign/quick_grading', 'init', []); $quickformparams = [ 'cm' => $this->get_course_module()->id, 'gradingtable' => $table, 'page' => $page, 'footer' => $this->get_renderer()->render($footer), ]; $quickgradingform = new mod_assign_quick_grading_form(null, $quickformparams); $o .= $this->get_renderer()->render(new assign_form('quickgradingform', $quickgradingform)); } else { $o .= $table; $o .= $this->get_renderer()->render($footer); } if ($this->can_grade()) { // We need to store the order of uses in the table as the person may wish to grade them. // This is done based on the row number of the user. // Pagination has be added before this because calling get_column_data will reset the pagination. $useridlist = $gradingtable->get_column_data('userid'); $SESSION->mod_assign_useridlist[$this->get_useridlist_key()] = $useridlist; } $currentgroup = groups_get_activity_group($this->get_course_module(), true); $users = array_keys($this->list_participants($currentgroup, true)); if (count($users) != 0 && $this->can_grade()) { $jsparams = []; $jsparams['message'] = !empty($CFG->messaging) && has_all_capabilities(['moodle/site:sendmessage', 'moodle/course:bulkmessaging'], $this->context); $jsparams['submissiondrafts'] = !empty($this->get_instance()->submissiondrafts); $jsparams['removesubmission'] = has_capability('mod/assign:editothersubmission', $this->context); $jsparams['extend'] = $this->get_instance()->duedate && has_capability('mod/assign:grantextension', $this->context); $multipleattemptsallowed = $this->get_instance()->maxattempts > 1 || $this->get_instance()->maxattempts == ASSIGN_UNLIMITED_ATTEMPTS; $jsparams['grantattempt'] = $multipleattemptsallowed && $this->get_instance()->attemptreopenmethod == ASSIGN_ATTEMPT_REOPEN_METHOD_MANUAL; $jsparams['pluginoperations'] = []; foreach ($this->get_feedback_plugins() as $plugin) { if ($plugin->is_visible() && $plugin->is_enabled()) { foreach ($plugin->get_grading_batch_operation_details() as $operation) { $jsparams['pluginoperations'][] = [ 'key' => 'plugingradingbatchoperation_' . $plugin->get_type() . '_' . $operation->key, 'label' => $operation->label, 'icon' => $operation->icon, 'confirmationtitle' => $operation->confirmationtitle, 'confirmationquestion' => $operation->confirmationquestion, ]; } } } $jsparams['workflowstate'] = !empty($this->get_instance()->markingworkflow); $jsparams['markingallocation'] = !empty($this->get_instance()->markingallocation); $jsparams['cmid'] = $this->get_course_module()->id; $jsparams['sesskey'] = sesskey(); $PAGE->requires->js_call_amd('mod_assign/bulkactions/grading/bulk_actions', 'init', [$jsparams]); } return $o; } /** * View entire grader app. * * @return string */ protected function view_grader() { global $USER, $PAGE; $o = ''; // Need submit permission to submit an assignment. $this->require_view_grades(); $PAGE->set_pagelayout('embedded'); $PAGE->activityheader->disable(); $courseshortname = $this->get_context()->get_course_context()->get_context_name(false, true); $args = [ 'contextname' => $this->get_context()->get_context_name(false, true), 'subpage' => get_string('grading', 'assign') ]; $title = get_string('subpagetitle', 'assign', $args); $title = $courseshortname . ': ' . $title; $PAGE->set_title($title); $o .= $this->get_renderer()->header(); $userid = optional_param('userid', 0, PARAM_INT); $blindid = optional_param('blindid', 0, PARAM_INT); if ($this->is_blind_marking() && !$userid && $blindid) { $userid = $this->get_user_id_for_uniqueid($blindid); } // Instantiate table object to apply table preferences. $gradingtable = new assign_grading_table($this, 10, '', 0, false); $gradingtable->setup(); $currentgroup = groups_get_activity_group($this->get_course_module(), true); $framegrader = new grading_app($userid, $currentgroup, $this); $this->update_effective_access($userid); $o .= $this->get_renderer()->render($framegrader); $o .= $this->view_footer(); \mod_assign\event\grading_table_viewed::create_from_assign($this)->trigger(); return $o; } /** * View entire grading page. * * @return string */ protected function view_grading_page() { global $CFG, $PAGE; // Ensure that the 'Submissions' navigation node is highlighted as 'active' in the secondary navigation. $PAGE->set_secondary_active_tab('mod_assign_submissions'); $o = ''; // Need submit permission to submit an assignment. $this->require_view_grades(); require_once($CFG->dirroot . '/mod/assign/gradeform.php'); $this->add_grade_notices(); // Only load this if it is. $o .= $this->view_grading_table(); $o .= $this->view_footer(); \mod_assign\event\grading_table_viewed::create_from_assign($this)->trigger(); return $o; } /** * Capture the output of the plagiarism plugins disclosures and return it as a string. * * @return string */ protected function plagiarism_print_disclosure() { global $CFG; $o = ''; if (!empty($CFG->enableplagiarism)) { require_once($CFG->libdir . '/plagiarismlib.php'); $o .= plagiarism_print_disclosure($this->get_course_module()->id); } return $o; } /** * Message for students when assignment submissions have been closed. * * @param string $title The page title * @param array $notices The array of notices to show. * @return string */ protected function view_notices($title, $notices) { global $CFG; $o = ''; $header = new assign_header($this->get_instance(), $this->get_context(), $this->show_intro(), $this->get_course_module()->id, $title); $o .= $this->get_renderer()->render($header); foreach ($notices as $notice) { $o .= $this->get_renderer()->notification($notice); } $url = new moodle_url('/mod/assign/view.php', array('id'=>$this->get_course_module()->id, 'action'=>'view')); $o .= $this->get_renderer()->continue_button($url); $o .= $this->view_footer(); return $o; } /** * Get the name for a user - hiding their real name if blind marking is on. * * @param stdClass $user The user record as required by fullname() * @return string The name. */ public function fullname($user) { if ($this->is_blind_marking()) { $hasviewblind = has_capability('mod/assign:viewblinddetails', $this->get_context()); if (empty($user->recordid)) { $uniqueid = $this->get_uniqueid_for_user($user->id); } else { $uniqueid = $user->recordid; } if ($hasviewblind) { return get_string('participant', 'assign') . ' ' . $uniqueid . ' (' . fullname($user, has_capability('moodle/site:viewfullnames', $this->get_context())) . ')'; } else { return get_string('participant', 'assign') . ' ' . $uniqueid; } } else { return fullname($user, has_capability('moodle/site:viewfullnames', $this->get_context())); } } /** * View edit submissions page. * * @param moodleform $mform * @param array $notices A list of notices to display at the top of the * edit submission form (e.g. from plugins). * @return string The page output. */ protected function view_edit_submission_page($mform, $notices) { global $CFG, $USER, $DB, $PAGE, $OUTPUT; $o = ''; require_once($CFG->dirroot . '/mod/assign/submission_form.php'); // Need submit permission to submit an assignment. $userid = optional_param('userid', 0, PARAM_INT); $blindid = optional_param('blindid', 0, PARAM_INT); if ($this->is_blind_marking() && !$userid && $blindid) { $userid = $this->get_user_id_for_uniqueid($blindid); } // If no userid specified, default to current user. if (!$userid) { $userid = $USER->id; } $user = $DB->get_record('user', array('id'=>$userid), '*', MUST_EXIST); $timelimitenabled = get_config('assign', 'enabletimelimit'); // This variation on the url will link direct to this student. // The benefit is the url will be the same every time for this student, so Atto autosave drafts can match up. $returnparams = array('userid' => $userid, 'rownum' => 0, 'useridlistid' => 0); $this->register_return_link('editsubmission', $returnparams); if ($userid == $USER->id) { if (!$this->can_edit_submission($userid, $USER->id)) { throw new \moodle_exception('nopermission'); } // User is editing their own submission. require_capability('mod/assign:submit', $this->context); $title = get_string('editsubmission', 'assign'); } else { // User is editing another user's submission. if (!$this->can_edit_submission($userid, $USER->id)) { throw new \moodle_exception('nopermission'); } $name = $this->fullname($user); $title = get_string('editsubmissionother', 'assign', $name); } if (!$this->submissions_open($userid)) { $message = array(get_string('submissionsclosed', 'assign')); return $this->view_notices($title, $message); } $postfix = ''; if ($this->has_visible_attachments()) { $postfix = $this->render_area_files('mod_assign', ASSIGN_INTROATTACHMENT_FILEAREA, 0); } $data = new stdClass(); $data->userid = $userid; if (!$mform) { $mform = new mod_assign_submission_form(null, array($this, $data)); } if ($this->get_instance()->teamsubmission) { $submission = $this->get_group_submission($userid, 0, false); } else { $submission = $this->get_user_submission($userid, false); } if ($timelimitenabled && !empty($submission->timestarted) && $this->get_instance()->timelimit) { $navbc = $this->get_timelimit_panel($submission); $regions = $PAGE->blocks->get_regions(); $bc = new \block_contents(); $bc->attributes['id'] = 'mod_assign_timelimit_block'; $bc->attributes['role'] = 'navigation'; $bc->attributes['aria-labelledby'] = 'mod_assign_timelimit_block_title'; $bc->title = get_string('assigntimeleft', 'assign'); $bc->content = $navbc; $PAGE->blocks->add_fake_block($bc, reset($regions)); } $o .= $this->get_renderer()->render( new assign_header($this->get_instance(), $this->get_context(), $this->show_intro(), $this->get_course_module()->id, $title, '', $postfix, null, true ) ); // Show plagiarism disclosure for any user submitter. $o .= $this->plagiarism_print_disclosure(); foreach ($notices as $notice) { $o .= $this->get_renderer()->notification($notice); } if ( $submission->status == ASSIGN_SUBMISSION_STATUS_NEW && $this->get_instance()->timelimit && empty($submission->timestarted) ) { // Timed assignment should always get a confirmation that the user wants to start it. $confirmation = new \confirm_action( get_string('confirmstart', 'assign', format_time($this->get_instance()->timelimit)), null, get_string('beginassignment', 'assign') ); // The 'begin' flag indicates that the user is starting a timed assignment. $urlparams = ['id' => $this->get_course_module()->id, 'action' => 'editsubmission', 'begin' => 1]; $beginbutton = new \action_link( new moodle_url('/mod/assign/view.php', $urlparams), get_string('beginassignment', 'assign'), $confirmation, ['class' => 'btn btn-primary'] ); $o .= $OUTPUT->render($beginbutton); } else { $o .= $this->get_renderer()->render(new assign_form('editsubmissionform', $mform)); } $o .= $this->view_footer(); \mod_assign\event\submission_form_viewed::create_from_user($this, $user)->trigger(); return $o; } /** * Get the time limit panel object for this submission attempt. * * @param stdClass $submission assign submission. * @return string the panel output. */ public function get_timelimit_panel(stdClass $submission): string { global $USER; // Apply overrides. $this->update_effective_access($USER->id); $panel = new timelimit_panel($submission, $this->get_instance()); return $this->get_renderer()->render($panel); } /** * See if this assignment has a grade yet. * * @param int $userid * @return bool */ protected function is_graded($userid) { $grade = $this->get_user_grade($userid, false); if ($grade) { return ($grade->grade !== null && $grade->grade >= 0); } return false; } /** * Perform an access check to see if the current $USER can edit this group submission. * * @param int $groupid * @return bool */ public function can_edit_group_submission($groupid) { global $USER; $members = $this->get_submission_group_members($groupid, true); foreach ($members as $member) { // If we can edit any members submission, we can edit the submission for the group. if ($this->can_edit_submission($member->id)) { return true; } } return false; } /** * Perform an access check to see if the current $USER can view this group submission. * * @param int $groupid * @return bool */ public function can_view_group_submission($groupid) { global $USER; $members = $this->get_submission_group_members($groupid, true); foreach ($members as $member) { // If we can view any members submission, we can view the submission for the group. if ($this->can_view_submission($member->id)) { return true; } } return false; } /** * Perform an access check to see if the current $USER can view this users submission. * * @param int $userid * @return bool */ public function can_view_submission($userid) { global $USER; if (!$this->is_active_user($userid) && !has_capability('moodle/course:viewsuspendedusers', $this->context)) { return false; } if (!is_enrolled($this->get_course_context(), $userid)) { return false; } if (has_any_capability(array('mod/assign:viewgrades', 'mod/assign:grade'), $this->context)) { return true; } if ($userid == $USER->id) { return true; } return false; } /** * Allows the plugin to show a batch grading operation page. * You should confirm sesskey before calling this function. * * @return none */ protected function view_plugin_grading_batch_operation() { require_capability('mod/assign:grade', $this->context); $prefix = 'plugingradingbatchoperation_'; $operation = required_param('operation', PARAM_ALPHAEXT); $tail = substr($operation, strlen($prefix)); list($plugintype, $action) = explode('_', $tail, 2); $plugin = $this->get_feedback_plugin_by_type($plugintype); if ($plugin) { $users = required_param('selectedusers', PARAM_SEQUENCE); $userlist = explode(',', $users); echo $plugin->grading_batch_operation($action, $userlist); return; } throw new \moodle_exception('invalidformdata', ''); } /** * Ask the user to confirm they want to perform this batch operation * * @return string - the page to view after processing these actions */ protected function process_grading_batch_operation() { require_sesskey(); $operation = required_param('operation', PARAM_ALPHAEXT); $selectedusers = required_param('selectedusers', PARAM_SEQUENCE); // Get the list of users. $userlist = explode(',', $selectedusers); $prefix = 'plugingradingbatchoperation_'; if ($operation == 'grantextension') { return 'grantextension'; } else if ($operation == 'setmarkingworkflowstate') { return 'viewbatchsetmarkingworkflowstate'; } else if ($operation == 'setmarkingallocation') { return 'viewbatchmarkingallocation'; } else if (strpos($operation, $prefix) === 0) { $tail = substr($operation, strlen($prefix)); list($plugintype, $action) = explode('_', $tail, 2); $plugin = $this->get_feedback_plugin_by_type($plugintype); if ($plugin) { return 'plugingradingbatchoperation'; } } if ($operation == 'downloadselected') { $this->download_submissions($userlist); } else { foreach ($userlist as $userid) { if ($operation == 'lock') { $this->process_lock_submission($userid); } else if ($operation == 'unlock') { $this->process_unlock_submission($userid); } else if ($operation == 'reverttodraft') { $this->process_revert_to_draft($userid); } else if ($operation == 'removesubmission') { $this->process_remove_submission($userid); } else if ($operation == 'addattempt') { if (!$this->get_instance()->teamsubmission) { $this->process_add_attempt($userid); } } } } if ($this->get_instance()->teamsubmission && $operation == 'addattempt') { // This needs to be handled separately so that each team submission is only re-opened one time. $this->process_add_attempt_group($userlist); } return 'grading'; } /** * Shows a form that allows the workflow state for selected submissions to be changed. * You should confirm sesskey before calling this function. * * @return string - the page to view after processing these actions */ protected function view_batch_set_workflow_state() { global $CFG, $DB; require_once($CFG->dirroot . '/mod/assign/batchsetmarkingworkflowstateform.php'); $o = ''; $users = required_param('selectedusers', PARAM_SEQUENCE); $userlist = explode(',', $users); $formdata = array('id' => $this->get_course_module()->id, 'selectedusers' => $users); $usershtml = ''; $usercount = 0; // TODO Does not support custom user profile fields (MDL-70456). $extrauserfields = \core_user\fields::get_identity_fields($this->get_context(), false); $viewfullnames = has_capability('moodle/site:viewfullnames', $this->get_context()); foreach ($userlist as $userid) { if ($usercount >= 5) { $usershtml .= get_string('moreusers', 'assign', count($userlist) - 5); break; } $user = $DB->get_record('user', array('id'=>$userid), '*', MUST_EXIST); $usershtml .= $this->get_renderer()->render(new assign_user_summary($user, $this->get_course()->id, $viewfullnames, $this->is_blind_marking(), $this->get_uniqueid_for_user($user->id), $extrauserfields, !$this->is_active_user($userid))); $usercount += 1; } $formparams = array( 'userscount' => count($userlist), 'usershtml' => $usershtml, 'markingworkflowstates' => $this->get_marking_workflow_states_for_current_user() ); $mform = new mod_assign_batch_set_marking_workflow_state_form(null, $formparams); $mform->set_data($formdata); // Initialises the hidden elements. $header = new assign_header($this->get_instance(), $this->get_context(), $this->show_intro(), $this->get_course_module()->id, get_string('setmarkingworkflowstate', 'assign')); $o .= $this->get_renderer()->render($header); $o .= $this->get_renderer()->render(new assign_form('setworkflowstate', $mform)); $o .= $this->view_footer(); \mod_assign\event\batch_set_workflow_state_viewed::create_from_assign($this)->trigger(); return $o; } /** * Shows a form that allows the allocated marker for selected submissions to be changed. * You should confirm sesskey before calling this function. * * @return string - the page to view after processing these actions */ public function view_batch_markingallocation() { global $CFG, $DB; require_once($CFG->dirroot . '/mod/assign/batchsetallocatedmarkerform.php'); $o = ''; $users = required_param('selectedusers', PARAM_SEQUENCE); $userlist = explode(',', $users); $formdata = array('id' => $this->get_course_module()->id, 'selectedusers' => $users); $usershtml = ''; $usercount = 0; // TODO Does not support custom user profile fields (MDL-70456). $extrauserfields = \core_user\fields::get_identity_fields($this->get_context(), false); $viewfullnames = has_capability('moodle/site:viewfullnames', $this->get_context()); foreach ($userlist as $userid) { if ($usercount >= 5) { $usershtml .= get_string('moreusers', 'assign', count($userlist) - 5); break; } $user = $DB->get_record('user', array('id'=>$userid), '*', MUST_EXIST); $usershtml .= $this->get_renderer()->render(new assign_user_summary($user, $this->get_course()->id, $viewfullnames, $this->is_blind_marking(), $this->get_uniqueid_for_user($user->id), $extrauserfields, !$this->is_active_user($userid))); $usercount += 1; } $formparams = array( 'userscount' => count($userlist), 'usershtml' => $usershtml, ); list($sort, $params) = users_order_by_sql('u'); // Only enrolled users could be assigned as potential markers. $markers = get_enrolled_users($this->get_context(), 'mod/assign:grade', 0, 'u.*', $sort); $markerlist = array(); foreach ($markers as $marker) { $markerlist[$marker->id] = fullname($marker); } $formparams['markers'] = $markerlist; $mform = new mod_assign_batch_set_allocatedmarker_form(null, $formparams); $mform->set_data($formdata); // Initialises the hidden elements. $header = new assign_header($this->get_instance(), $this->get_context(), $this->show_intro(), $this->get_course_module()->id, get_string('setmarkingallocation', 'assign')); $o .= $this->get_renderer()->render($header); $o .= $this->get_renderer()->render(new assign_form('setworkflowstate', $mform)); $o .= $this->view_footer(); \mod_assign\event\batch_set_marker_allocation_viewed::create_from_assign($this)->trigger(); return $o; } /** * Ask the user to confirm they want to submit their work for grading. * * @param moodleform $mform - null unless form validation has failed * @return string */ protected function check_submit_for_grading($mform) { global $USER, $CFG; require_once($CFG->dirroot . '/mod/assign/submissionconfirmform.php'); // Check that all of the submission plugins are ready for this submission. // Also check whether there is something to be submitted as well against atleast one. $notifications = array(); $submission = $this->get_user_submission($USER->id, false); if ($this->get_instance()->teamsubmission) { $submission = $this->get_group_submission($USER->id, 0, false); } $plugins = $this->get_submission_plugins(); $hassubmission = false; foreach ($plugins as $plugin) { if ($plugin->is_enabled() && $plugin->is_visible()) { $check = $plugin->precheck_submission($submission); if ($check !== true) { $notifications[] = $check; } if (is_object($submission) && !$plugin->is_empty($submission)) { $hassubmission = true; } } } // If there are no submissions and no existing notifications to be displayed the stop. if (!$hassubmission && !$notifications) { $notifications[] = get_string('addsubmission_help', 'assign'); } $data = new stdClass(); $adminconfig = $this->get_admin_config(); $requiresubmissionstatement = $this->get_instance()->requiresubmissionstatement; $submissionstatement = ''; if ($requiresubmissionstatement) { $submissionstatement = $this->get_submissionstatement($adminconfig, $this->get_instance(), $this->get_context()); } // If we get back an empty submission statement, we have to set $requiredsubmisisonstatement to false to prevent // that the submission statement checkbox will be displayed. if (empty($submissionstatement)) { $requiresubmissionstatement = false; } if ($mform == null) { $mform = new mod_assign_confirm_submission_form(null, array($requiresubmissionstatement, $submissionstatement, $this->get_course_module()->id, $data)); } $o = ''; $o .= $this->get_renderer()->render(new assign_header($this->get_instance(), $this->get_context(), $this->show_intro(), $this->get_course_module()->id, get_string('confirmsubmissionheading', 'assign'))); $submitforgradingpage = new assign_submit_for_grading_page($notifications, $this->get_course_module()->id, $mform); $o .= $this->get_renderer()->render($submitforgradingpage); $o .= $this->view_footer(); \mod_assign\event\submission_confirmation_form_viewed::create_from_assign($this)->trigger(); return $o; } /** * Creates an assign_submission_status renderable. * * @param stdClass $user the user to get the report for * @param bool $showlinks return plain text or links to the profile * @return assign_submission_status renderable object */ public function get_assign_submission_status_renderable($user, $showlinks) { global $PAGE; $instance = $this->get_instance(); $flags = $this->get_user_flags($user->id, false); $submission = $this->get_user_submission($user->id, false); $teamsubmission = null; $submissiongroup = null; $notsubmitted = array(); if ($instance->teamsubmission) { $teamsubmission = $this->get_group_submission($user->id, 0, false); $submissiongroup = $this->get_submission_group($user->id); $groupid = 0; if ($submissiongroup) { $groupid = $submissiongroup->id; } $notsubmitted = $this->get_submission_group_members_who_have_not_submitted($groupid, false); } $showedit = $showlinks && ($this->is_any_submission_plugin_enabled()) && $this->can_edit_submission($user->id); $submissionlocked = ($flags && $flags->locked); // Grading criteria preview. $gradingmanager = get_grading_manager($this->context, 'mod_assign', 'submissions'); $gradingcontrollerpreview = ''; if ($gradingmethod = $gradingmanager->get_active_method()) { $controller = $gradingmanager->get_controller($gradingmethod); if ($controller->is_form_defined()) { $gradingcontrollerpreview = $controller->render_preview($PAGE); } } $showsubmit = ($showlinks && $this->submissions_open($user->id)); $showsubmit = ($showsubmit && $this->show_submit_button($submission, $teamsubmission, $user->id)); $extensionduedate = null; if ($flags) { $extensionduedate = $flags->extensionduedate; } $viewfullnames = has_capability('moodle/site:viewfullnames', $this->get_context()); $gradingstatus = $this->get_grading_status($user->id); $usergroups = $this->get_all_groups($user->id); $submissionstatus = new assign_submission_status($instance->allowsubmissionsfromdate, $instance->alwaysshowdescription, $submission, $instance->teamsubmission, $teamsubmission, $submissiongroup, $notsubmitted, $this->is_any_submission_plugin_enabled(), $submissionlocked, $this->is_graded($user->id), $instance->duedate, $instance->cutoffdate, $this->get_submission_plugins(), $this->get_return_action(), $this->get_return_params(), $this->get_course_module()->id, $this->get_course()->id, assign_submission_status::STUDENT_VIEW, $showedit, $showsubmit, $viewfullnames, $extensionduedate, $this->get_context(), $this->is_blind_marking(), $gradingcontrollerpreview, $instance->attemptreopenmethod, $instance->maxattempts, $gradingstatus, $instance->preventsubmissionnotingroup, $usergroups, $instance->timelimit); return $submissionstatus; } /** * Creates an assign_feedback_status renderable. * * @param stdClass $user the user to get the report for * @return assign_feedback_status renderable object */ public function get_assign_feedback_status_renderable($user) { global $CFG, $DB, $PAGE; require_once($CFG->libdir.'/gradelib.php'); require_once($CFG->dirroot.'/grade/grading/lib.php'); $instance = $this->get_instance(); $grade = $this->get_user_grade($user->id, false); $gradingstatus = $this->get_grading_status($user->id); $gradinginfo = grade_get_grades($this->get_course()->id, 'mod', 'assign', $instance->id, $user->id); $gradingitem = null; $gradebookgrade = null; if (isset($gradinginfo->items[0])) { $gradingitem = $gradinginfo->items[0]; $gradebookgrade = $gradingitem->grades[$user->id]; } // Check to see if all feedback plugins are empty. $emptyplugins = true; if ($grade) { foreach ($this->get_feedback_plugins() as $plugin) { if ($plugin->is_visible() && $plugin->is_enabled()) { if (!$plugin->is_empty($grade)) { $emptyplugins = false; } } } } if ($this->get_instance()->markingworkflow && $gradingstatus != ASSIGN_MARKING_WORKFLOW_STATE_RELEASED) { $emptyplugins = true; // Don't show feedback plugins until released either. } $cangrade = has_capability('mod/assign:grade', $this->get_context()); $hasgrade = $this->get_instance()->grade != GRADE_TYPE_NONE && !is_null($gradebookgrade) && !is_null($gradebookgrade->grade); $gradevisible = $cangrade || $this->get_instance()->grade == GRADE_TYPE_NONE || (!is_null($gradebookgrade) && !$gradebookgrade->hidden); // If there is a visible grade, show the summary. if (($hasgrade || !$emptyplugins) && $gradevisible) { $gradefordisplay = null; $gradeddate = null; $grader = null; $gradingmanager = get_grading_manager($this->get_context(), 'mod_assign', 'submissions'); $gradingcontrollergrade = ''; if ($hasgrade) { if ($controller = $gradingmanager->get_active_controller()) { $menu = make_grades_menu($this->get_instance()->grade); $controller->set_grade_range($menu, $this->get_instance()->grade > 0); $gradingcontrollergrade = $controller->render_grade( $PAGE, $grade->id, $gradingitem, '', $cangrade ); // Display the penalty indicator next to the penalized grade, if applicable. $penaltyindicator = \core_grades\penalty_manager::show_penalty_indicator( new \grade_grade([ 'deductedmark' => $gradebookgrade->deductedmark ?? 0, 'overridden' => $gradebookgrade->overridden ?? 0, ], false) ); $gradefordisplay = $penaltyindicator . $gradebookgrade->str_long_grade; } else { // This grade info is the grade from gradebook. // We need user id to determine if the grade is overridden or not. $gradefordisplay = $this->display_grade( $gradebookgrade->grade, false, $user->id, 0, $gradebookgrade->deductedmark ); } $gradeddate = $gradebookgrade->dategraded; // Show the grader's identity if 'Hide Grader' is disabled or has the 'Show Hidden Grader' capability. if (has_capability('mod/assign:showhiddengrader', $this->context) || !$this->is_hidden_grader()) { // Only display the grader if it is in the right state. if (in_array($gradingstatus, [ASSIGN_GRADING_STATUS_GRADED, ASSIGN_MARKING_WORKFLOW_STATE_RELEASED])) { if (isset($grade->grader) && $grade->grader > 0) { $grader = $DB->get_record('user', array('id' => $grade->grader)); } else if (isset($gradebookgrade->usermodified) && $gradebookgrade->usermodified > 0 && has_capability('mod/assign:grade', $this->get_context(), $gradebookgrade->usermodified)) { // Grader not provided. Check that usermodified is a user who can grade. // Case 1: When an assignment is reopened an empty assign_grade is created so the feedback // plugin can know which attempt it's referring to. In this case, usermodifed is a student. // Case 2: When an assignment's grade is overrided via the gradebook, usermodified is a grader. $grader = $DB->get_record('user', array('id' => $gradebookgrade->usermodified)); } } } } $viewfullnames = has_capability('moodle/site:viewfullnames', $this->get_context()); if ($grade) { \mod_assign\event\feedback_viewed::create_from_grade($this, $grade)->trigger(); } $feedbackstatus = new assign_feedback_status( $gradefordisplay, $gradeddate, $grader, $this->get_feedback_plugins(), $grade, $this->get_course_module()->id, $this->get_return_action(), $this->get_return_params(), $viewfullnames, $gradingcontrollergrade ); return $feedbackstatus; } return; } /** * Creates an assign_attempt_history renderable. * * @param stdClass $user the user to get the report for * @return assign_attempt_history renderable object */ public function get_assign_attempt_history_renderable($user) { $allsubmissions = $this->get_all_submissions($user->id); $allgrades = $this->get_all_grades($user->id); $history = new assign_attempt_history($allsubmissions, $allgrades, $this->get_submission_plugins(), $this->get_feedback_plugins(), $this->get_course_module()->id, $this->get_return_action(), $this->get_return_params(), false, 0, 0); return $history; } /** * Print 2 tables of information with no action links - * the submission summary and the grading summary. * * @param stdClass $user the user to print the report for * @param bool $showlinks - Return plain text or links to the profile * @return string - the html summary */ public function view_student_summary($user, $showlinks) { $o = ''; if ($this->can_view_submission($user->id)) { if (has_capability('mod/assign:viewownsubmissionsummary', $this->get_context(), $user, false)) { // The user can view the submission summary. $submissionstatus = $this->get_assign_submission_status_renderable($user, $showlinks); $o .= $this->get_renderer()->render($submissionstatus); } // If there is a visible grade, show the feedback. $feedbackstatus = $this->get_assign_feedback_status_renderable($user); if ($feedbackstatus) { $o .= $this->get_renderer()->render($feedbackstatus); } // If there is more than one submission, show the history. $history = $this->get_assign_attempt_history_renderable($user); if (count($history->submissions) > 1) { $o .= $this->get_renderer()->render($history); } } return $o; } /** * Returns true if the submit subsission button should be shown to the user. * * @param stdClass $submission The users own submission record. * @param stdClass $teamsubmission The users team submission record if there is one * @param int $userid The user * @return bool */ protected function show_submit_button($submission = null, $teamsubmission = null, $userid = null) { if (!has_capability('mod/assign:submit', $this->get_context(), $userid, false)) { // The user does not have the capability to submit. return false; } if ($teamsubmission) { if ($teamsubmission->status === ASSIGN_SUBMISSION_STATUS_SUBMITTED) { // The assignment submission has been completed. return false; } else if ($this->submission_empty($teamsubmission)) { // There is nothing to submit yet. return false; } else if ($submission && $submission->status === ASSIGN_SUBMISSION_STATUS_SUBMITTED) { // The user has already clicked the submit button on the team submission. return false; } else if ( !empty($this->get_instance()->preventsubmissionnotingroup) && $this->get_submission_group($userid) == false ) { return false; } } else if ($submission) { if ($submission->status === ASSIGN_SUBMISSION_STATUS_SUBMITTED) { // The assignment submission has been completed. return false; } else if ($this->submission_empty($submission)) { // There is nothing to submit. return false; } } else { // We've not got a valid submission or team submission. return false; } // Last check is that this instance allows drafts. return $this->get_instance()->submissiondrafts; } /** * Get the grades for all previous attempts. * For each grade - the grader is a full user record, * and gradefordisplay is added (rendered from grading manager). * * @param int $userid If not set, $USER->id will be used. * @return array $grades All grade records for this user. */ protected function get_all_grades($userid) { global $DB, $USER, $PAGE; // If the userid is not null then use userid. if (!$userid) { $userid = $USER->id; } $params = array('assignment'=>$this->get_instance()->id, 'userid'=>$userid); $grades = $DB->get_records('assign_grades', $params, 'attemptnumber ASC'); $gradercache = array(); $cangrade = has_capability('mod/assign:grade', $this->get_context()); // Show the grader's identity if 'Hide Grader' is disabled or has the 'Show Hidden Grader' capability. $showgradername = ( has_capability('mod/assign:showhiddengrader', $this->context, $userid) or !$this->is_hidden_grader() ); // Need gradingitem and gradingmanager. $gradingmanager = get_grading_manager($this->get_context(), 'mod_assign', 'submissions'); $controller = $gradingmanager->get_active_controller(); $gradinginfo = grade_get_grades($this->get_course()->id, 'mod', 'assign', $this->get_instance()->id, $userid); $gradingitem = null; if (isset($gradinginfo->items[0])) { $gradingitem = $gradinginfo->items[0]; } foreach ($grades as $grade) { // First lookup the grader info. if (!$showgradername) { $grade->grader = null; } else if (isset($gradercache[$grade->grader])) { $grade->grader = $gradercache[$grade->grader]; } else if ($grade->grader > 0) { // Not in cache - need to load the grader record. $grade->grader = $DB->get_record('user', array('id'=>$grade->grader)); if ($grade->grader) { $gradercache[$grade->grader->id] = $grade->grader; } } // The assign grade for each attempt is not stored in the gradebook. // We need to calculate them from assign_grade records. [$penalisedgrade, $deductedmark] = $this->calculate_penalised_grade($grade); // Now get the gradefordisplay. if ($controller) { $controller->set_grade_range(make_grades_menu($this->get_instance()->grade), $this->get_instance()->grade > 0); // Display the penalty indicator next to the penalized grade, if applicable. $penaltyindicator = \core_grades\penalty_manager::show_penalty_indicator( new \grade_grade([ 'deductedmark' => $deductedmark, 'overridden' => $userid > 0 ? $this->get_grade_item()->get_grade($userid)->overridden : 0, ], false) ); $gradeoutput = $penaltyindicator . format_float($penalisedgrade, $this->get_grade_item()->get_decimals()); $grade->gradefordisplay = $controller->render_grade($PAGE, $grade->id, $gradingitem, $gradeoutput, $cangrade); } else { // We do not need user id here as the overriden grade should not affect the previous attempts. $grade->gradefordisplay = $this->display_grade($penalisedgrade, false, 0, 0, $deductedmark); } } return $grades; } /** * Calculate penalised grade and deducted mark. * * @param stdClass $grade The grade object * @return array [$penalisedgrade, $deductedmark] the penalised grade and the deducted mark */ public function calculate_penalised_grade(stdClass $grade): array { $penalisedgrade = $grade->grade; $deductedmark = 0; // No calculation needed if the grade is null or negative. if (is_null($penalisedgrade) || $penalisedgrade < 0) { return [$penalisedgrade, $deductedmark]; } if ($grade->penalty > 0) { $deductedmark = $grade->grade * $grade->penalty / 100; $penalisedgrade = $grade->grade - $deductedmark; } return [$penalisedgrade, $deductedmark]; } /** * Get the submissions for all previous attempts. * * @param int $userid If not set, $USER->id will be used. * @return array $submissions All submission records for this user (or group). */ public function get_all_submissions($userid) { global $DB, $USER; // If the userid is not null then use userid. if (!$userid) { $userid = $USER->id; } $params = array(); if ($this->get_instance()->teamsubmission) { $groupid = 0; $group = $this->get_submission_group($userid); if ($group) { $groupid = $group->id; } // Params to get the group submissions. $params = array('assignment'=>$this->get_instance()->id, 'groupid'=>$groupid, 'userid'=>0); } else { // Params to get the user submissions. $params = array('assignment'=>$this->get_instance()->id, 'userid'=>$userid); } // Return the submissions ordered by attempt. $submissions = $DB->get_records('assign_submission', $params, 'attemptnumber ASC'); return $submissions; } /** * Creates an assign_grading_summary renderable. * * @param mixed $activitygroup int|null the group for calculating the grading summary (if null the function will determine it) * @return assign_grading_summary renderable object */ public function get_assign_grading_summary_renderable($activitygroup = null) { $instance = $this->get_default_instance(); // Grading summary requires the raw dates, regardless of relativedates mode. $cm = $this->get_course_module(); $course = $this->get_course(); $draft = ASSIGN_SUBMISSION_STATUS_DRAFT; $submitted = ASSIGN_SUBMISSION_STATUS_SUBMITTED; $isvisible = $cm->visible; if ($activitygroup === null) { $activitygroup = groups_get_activity_group($cm); } if ($instance->teamsubmission) { $warnofungroupedusers = assign_grading_summary::WARN_GROUPS_NO; $defaultteammembers = $this->get_submission_group_members(0, true); if (count($defaultteammembers) > 0) { if ($instance->preventsubmissionnotingroup) { $warnofungroupedusers = assign_grading_summary::WARN_GROUPS_REQUIRED; } else { $warnofungroupedusers = assign_grading_summary::WARN_GROUPS_OPTIONAL; } } $summary = new assign_grading_summary( $this->count_teams($activitygroup), $instance->submissiondrafts, $this->count_submissions_with_status($draft, $activitygroup), $this->is_any_submission_plugin_enabled(), $this->count_submissions_with_status($submitted, $activitygroup), $this->get_cutoffdate($activitygroup), $this->get_duedate($activitygroup), $this->get_timelimit($activitygroup), $this->get_course_module()->id, $this->count_submissions_need_grading($activitygroup), $instance->teamsubmission, $warnofungroupedusers, $course->relativedatesmode, $course->startdate, $this->can_grade(), $isvisible, $this->get_course_module() ); } else { // The active group has already been updated in groups_print_activity_menu(). $countparticipants = $this->count_participants($activitygroup); $summary = new assign_grading_summary( $countparticipants, $instance->submissiondrafts, $this->count_submissions_with_status($draft, $activitygroup), $this->is_any_submission_plugin_enabled(), $this->count_submissions_with_status($submitted, $activitygroup), $this->get_cutoffdate($activitygroup), $this->get_duedate($activitygroup), $this->get_timelimit($activitygroup), $this->get_course_module()->id, $this->count_submissions_need_grading($activitygroup), $instance->teamsubmission, assign_grading_summary::WARN_GROUPS_NO, $course->relativedatesmode, $course->startdate, $this->can_grade(), $isvisible, $this->get_course_module() ); } return $summary; } /** * Helper function to allow up to fetch the group overrides via one query as opposed to many calls. * * @param int $activitygroup The group we want to check the overrides of * @return mixed Can return either a fetched DB object, local object or false */ private function get_override_data(int $activitygroup) { global $DB; $instanceid = $this->get_instance()->id; $cachekey = "$instanceid-$activitygroup"; if (isset($this->overridedata[$cachekey])) { return $this->overridedata[$cachekey]; } $params = ['groupid' => $activitygroup, 'assignid' => $instanceid]; $this->overridedata[$cachekey] = $DB->get_record('assign_overrides', $params); return $this->overridedata[$cachekey]; } /** * Return group override duedate. * * @param int $activitygroup Activity active group * @return int $duedate */ private function get_duedate($activitygroup = null) { if ($activitygroup === null) { $activitygroup = groups_get_activity_group($this->get_course_module()); } if ($this->can_view_grades() && !empty($activitygroup)) { $groupoverride = $this->get_override_data($activitygroup); if (!empty($groupoverride->duedate)) { return $groupoverride->duedate; } } return $this->get_instance()->duedate; } /** * Return group override timelimit. * * @param null|int $activitygroup Activity active group * @return int $timelimit */ private function get_timelimit(?int $activitygroup = null): int { if ($activitygroup === null) { $activitygroup = groups_get_activity_group($this->get_course_module()); } if ($this->can_view_grades() && !empty($activitygroup)) { $groupoverride = $this->get_override_data($activitygroup); if (!empty($groupoverride->timelimit)) { return $groupoverride->timelimit; } } return $this->get_instance()->timelimit; } /** * Return group override cutoffdate. * * @param null|int $activitygroup Activity active group * @return int $cutoffdate */ private function get_cutoffdate(?int $activitygroup = null): int { if ($activitygroup === null) { $activitygroup = groups_get_activity_group($this->get_course_module()); } if ($this->can_view_grades() && !empty($activitygroup)) { $groupoverride = $this->get_override_data($activitygroup); if (!empty($groupoverride->cutoffdate)) { return $groupoverride->cutoffdate; } } return $this->get_instance()->cutoffdate; } /** * View submissions page (contains details of current submission). * * @return string */ protected function view_submission_page() { global $CFG, $DB, $USER, $PAGE; $instance = $this->get_instance(); $this->add_grade_notices(); $o = ''; $postfix = ''; if ($this->has_visible_attachments() && (!$this->get_instance($USER->id)->submissionattachments)) { $postfix = $this->render_area_files('mod_assign', ASSIGN_INTROATTACHMENT_FILEAREA, 0); } $o .= $this->get_renderer()->render(new assign_header($instance, $this->get_context(), $this->show_intro(), $this->get_course_module()->id, '', '', $postfix)); // Display plugin specific headers. $plugins = array_merge($this->get_submission_plugins(), $this->get_feedback_plugins()); foreach ($plugins as $plugin) { if ($plugin->is_enabled() && $plugin->is_visible()) { $o .= $this->get_renderer()->render(new assign_plugin_header($plugin)); } } if ($this->can_view_grades()) { $actionbuttons = new \mod_assign\output\actionmenu($this->get_course_module()->id); $o .= $this->get_renderer()->submission_actionmenu($actionbuttons); $summary = $this->get_assign_grading_summary_renderable(); $o .= $this->get_renderer()->render($summary); } if ($this->can_view_submission($USER->id)) { $o .= $this->view_submission_action_bar($instance, $USER); $o .= $this->view_student_summary($USER, true); } $o .= $this->view_footer(); \mod_assign\event\submission_status_viewed::create_from_assign($this)->trigger(); return $o; } /** * The action bar displayed in the submissions page. * * @param stdClass $instance The settings for the current instance of this assignment * @param stdClass $user The user to print the action bar for * @return string */ public function view_submission_action_bar(stdClass $instance, stdClass $user): string { $submission = $this->get_user_submission($user->id, false); // Figure out if we are team or solitary submission. $teamsubmission = null; if ($instance->teamsubmission) { $teamsubmission = $this->get_group_submission($user->id, 0, false); } $showsubmit = ($this->submissions_open($user->id) && $this->show_submit_button($submission, $teamsubmission, $user->id)); $showedit = ($this->is_any_submission_plugin_enabled()) && $this->can_edit_submission($user->id); // The method get_group_submission() says that it returns a stdClass, but it can return false >_>. if ($teamsubmission === false) { $teamsubmission = new stdClass(); } // Same goes for get_user_submission(). if ($submission === false) { $submission = new stdClass(); } $actionbuttons = new \mod_assign\output\user_submission_actionmenu( $this->get_course_module()->id, $showsubmit, $showedit, $submission, $teamsubmission, $instance->timelimit ); return $this->get_renderer()->render($actionbuttons); } /** * Convert the final raw grade(s) in the grading table for the gradebook. * * @param stdClass $grade * @return array */ protected function convert_grade_for_gradebook(stdClass $grade) { $gradebookgrade = array(); if ($grade->grade >= 0) { $gradebookgrade['rawgrade'] = $grade->grade; } // Allow "no grade" to be chosen. if ($grade->grade == -1) { $gradebookgrade['rawgrade'] = NULL; } $gradebookgrade['userid'] = $grade->userid; $gradebookgrade['usermodified'] = $grade->grader; $gradebookgrade['datesubmitted'] = null; $gradebookgrade['dategraded'] = $grade->timemodified; if (isset($grade->feedbackformat)) { $gradebookgrade['feedbackformat'] = $grade->feedbackformat; } if (isset($grade->feedbacktext)) { $gradebookgrade['feedback'] = $grade->feedbacktext; } if (isset($grade->feedbackfiles)) { $gradebookgrade['feedbackfiles'] = $grade->feedbackfiles; } return $gradebookgrade; } /** * Convert submission details for the gradebook. * * @param stdClass $submission * @return array */ protected function convert_submission_for_gradebook(stdClass $submission) { $gradebookgrade = array(); $gradebookgrade['userid'] = $submission->userid; $gradebookgrade['usermodified'] = $submission->userid; $gradebookgrade['datesubmitted'] = $submission->timemodified; return $gradebookgrade; } /** * Update grades in the gradebook. * * @param mixed $submission stdClass|null * @param mixed $grade stdClass|null * @return bool */ protected function gradebook_item_update($submission=null, $grade=null) { global $CFG; require_once($CFG->dirroot.'/mod/assign/lib.php'); // Do not push grade to gradebook if blind marking is active as // the gradebook would reveal the students. if ($this->is_blind_marking() && !$this->is_marking_anonymous()) { return false; } // If marking workflow is enabled and grade is not released then remove any grade that may exist in the gradebook. if ($this->get_instance()->markingworkflow && !empty($grade) && $this->get_grading_status($grade->userid) != ASSIGN_MARKING_WORKFLOW_STATE_RELEASED) { // Remove the grade (if it exists) from the gradebook as it is not 'final'. $grade->grade = -1; $grade->feedbacktext = ''; $grade->feebackfiles = []; } if ($submission != null) { if ($submission->userid == 0) { // This is a group submission update. $team = groups_get_members($submission->groupid, 'u.id'); foreach ($team as $member) { $membersubmission = clone $submission; $membersubmission->groupid = 0; $membersubmission->userid = $member->id; $this->gradebook_item_update($membersubmission, null); } return; } $gradebookgrade = $this->convert_submission_for_gradebook($submission); } else { $gradebookgrade = $this->convert_grade_for_gradebook($grade); } // Grading is disabled, return. if ($this->grading_disabled($gradebookgrade['userid'])) { return false; } $assign = clone $this->get_instance(); $assign->cmidnumber = $this->get_course_module()->idnumber; // Set assign gradebook feedback plugin status (enabled and visible). $assign->gradefeedbackenabled = $this->is_gradebook_feedback_enabled(); return assign_grade_item_update($assign, $gradebookgrade) == GRADE_UPDATE_OK; } /** * Update team submission. * * @param stdClass $submission * @param int $userid * @param bool $updatetime * @return bool */ protected function update_team_submission(stdClass $submission, $userid, $updatetime) { global $DB; if ($updatetime) { $submission->timemodified = time(); } // First update the submission for the current user. $mysubmission = $this->get_user_submission($userid, true, $submission->attemptnumber); $mysubmission->status = $submission->status; $this->update_submission($mysubmission, 0, $updatetime, false); // Now check the team settings to see if this assignment qualifies as submitted or draft. $team = $this->get_submission_group_members($submission->groupid, true); $allsubmitted = true; $anysubmitted = false; $result = true; if (!in_array($submission->status, [ASSIGN_SUBMISSION_STATUS_NEW, ASSIGN_SUBMISSION_STATUS_REOPENED])) { foreach ($team as $member) { $membersubmission = $this->get_user_submission($member->id, false, $submission->attemptnumber); // If no submission found for team member and member is active then everyone has not submitted. if (!$membersubmission || $membersubmission->status != ASSIGN_SUBMISSION_STATUS_SUBMITTED && ($this->is_active_user($member->id))) { $allsubmitted = false; if ($anysubmitted) { break; } } else { $anysubmitted = true; } } if ($this->get_instance()->requireallteammemberssubmit) { if ($allsubmitted) { $submission->status = ASSIGN_SUBMISSION_STATUS_SUBMITTED; } else { $submission->status = ASSIGN_SUBMISSION_STATUS_DRAFT; } $result = $DB->update_record('assign_submission', $submission); } else { if ($anysubmitted) { $submission->status = ASSIGN_SUBMISSION_STATUS_SUBMITTED; } else { $submission->status = ASSIGN_SUBMISSION_STATUS_DRAFT; } $result = $DB->update_record('assign_submission', $submission); } } else { // Set the group submission to reopened. foreach ($team as $member) { $membersubmission = $this->get_user_submission($member->id, true, $submission->attemptnumber); $membersubmission->status = $submission->status; $result = $DB->update_record('assign_submission', $membersubmission) && $result; } $result = $DB->update_record('assign_submission', $submission) && $result; } $this->gradebook_item_update($submission); return $result; } /** * Update grades in the gradebook based on submission time. * * @param stdClass $submission * @param int $userid * @param bool $updatetime * @param bool $teamsubmission * @return bool */ protected function update_submission(stdClass $submission, $userid, $updatetime, $teamsubmission) { global $DB; if ($teamsubmission) { return $this->update_team_submission($submission, $userid, $updatetime); } if ($updatetime) { $submission->timemodified = time(); } $result= $DB->update_record('assign_submission', $submission); if ($result) { $this->gradebook_item_update($submission); } return $result; } /** * Is this assignment open for submissions? * * Check the due date, * prevent late submissions, * has this person already submitted, * is the assignment locked? * * @param int $userid - Optional userid so we can see if a different user can submit * @param bool $skipenrolled - Skip enrollment checks (because they have been done already) * @param stdClass $submission - Pre-fetched submission record (or false to fetch it) * @param stdClass $flags - Pre-fetched user flags record (or false to fetch it) * @param stdClass $gradinginfo - Pre-fetched user gradinginfo record (or false to fetch it) * @return bool */ public function submissions_open($userid = 0, $skipenrolled = false, $submission = false, $flags = false, $gradinginfo = false) { global $USER; if (!$userid) { $userid = $USER->id; } $time = time(); $dateopen = true; $finaldate = false; if ($this->get_instance()->cutoffdate) { $finaldate = $this->get_instance()->cutoffdate; } if ($flags === false) { $flags = $this->get_user_flags($userid, false); } if ($flags && $flags->locked) { return false; } // User extensions. if ($finaldate) { if ($flags && $flags->extensionduedate) { // Extension can be before cut off date. if ($flags->extensionduedate > $finaldate) { $finaldate = $flags->extensionduedate; } } } if ($finaldate) { $dateopen = ($this->get_instance()->allowsubmissionsfromdate <= $time && $time <= $finaldate); } else { $dateopen = ($this->get_instance()->allowsubmissionsfromdate <= $time); } if (!$dateopen) { return false; } // Now check if this user has already submitted etc. if (!$skipenrolled && !is_enrolled($this->get_course_context(), $userid)) { return false; } // Note you can pass null for submission and it will not be fetched. if ($submission === false) { if ($this->get_instance()->teamsubmission) { $submission = $this->get_group_submission($userid, 0, false); } else { $submission = $this->get_user_submission($userid, false); } } if ($submission) { if ($this->get_instance()->submissiondrafts && $submission->status == ASSIGN_SUBMISSION_STATUS_SUBMITTED) { // Drafts are tracked and the student has submitted the assignment. return false; } } // See if this user grade is locked in the gradebook. if ($gradinginfo === false) { $gradinginfo = grade_get_grades($this->get_course()->id, 'mod', 'assign', $this->get_instance()->id, array($userid)); } if ($gradinginfo && isset($gradinginfo->items[0]->grades[$userid]) && $gradinginfo->items[0]->grades[$userid]->locked) { return false; } return true; } /** * Render the files in file area. * * @param string $component * @param string $area * @param int $submissionid * @return string */ public function render_area_files($component, $area, $submissionid) { global $USER; return $this->get_renderer()->assign_files($this->context, $submissionid, $area, $component, $this->course, $this->coursemodule); } /** * Capability check to make sure this grader can edit this submission. * * @param int $userid - The user whose submission is to be edited * @param int $graderid (optional) - The user who will do the editing (default to $USER->id). * @return bool */ public function can_edit_submission($userid, $graderid = 0) { global $USER; if (empty($graderid)) { $graderid = $USER->id; } $instance = $this->get_instance(); if ($userid == $graderid && $instance->teamsubmission && $instance->preventsubmissionnotingroup && $this->get_submission_group($userid) == false) { return false; } if ($userid == $graderid) { if ( $this->submissions_open($userid) && has_capability('mod/assign:submit', $this->context, $graderid, false) ) { // User can edit their own submission. return true; } else { // We need to return here because editothersubmission should never apply to a users own submission. return false; } } if (!has_capability('mod/assign:editothersubmission', $this->context, $graderid)) { return false; } $cm = $this->get_course_module(); if (groups_get_activity_groupmode($cm) == SEPARATEGROUPS && !has_capability('moodle/site:accessallgroups', $this->context, $graderid)) { $sharedgroupmembers = $this->get_shared_group_members($cm, $graderid); return in_array($userid, $sharedgroupmembers); } return true; } /** * Returns IDs of the users who share group membership with the specified user. * * @param stdClass|cm_info $cm Course-module * @param int $userid User ID * @return array An array of ID of users. */ public function get_shared_group_members($cm, $userid) { if (!isset($this->sharedgroupmembers[$userid])) { $this->sharedgroupmembers[$userid] = array(); if ($members = groups_get_activity_shared_group_members($cm, $userid)) { $this->sharedgroupmembers[$userid] = array_keys($members); } } return $this->sharedgroupmembers[$userid]; } /** * Returns a list of teachers that should be grading given submission. * * @param int $userid The submission to grade * @return array */ protected function get_graders($userid) { // Potential graders should be active users only. $potentialgraders = get_enrolled_users($this->context, "mod/assign:grade", null, 'u.*', null, null, null, true); $graders = array(); if (groups_get_activity_groupmode($this->get_course_module()) == SEPARATEGROUPS) { if ($groups = groups_get_all_groups($this->get_course()->id, $userid, $this->get_course_module()->groupingid)) { foreach ($groups as $group) { foreach ($potentialgraders as $grader) { if ($grader->id == $userid) { // Do not send self. continue; } if (groups_is_member($group->id, $grader->id)) { $graders[$grader->id] = $grader; } } } } else { // User not in group, try to find graders without group. foreach ($potentialgraders as $grader) { if ($grader->id == $userid) { // Do not send self. continue; } if (!groups_has_membership($this->get_course_module(), $grader->id)) { $graders[$grader->id] = $grader; } } } } else { foreach ($potentialgraders as $grader) { if ($grader->id == $userid) { // Do not send self. continue; } // Must be enrolled. if (is_enrolled($this->get_course_context(), $grader->id)) { $graders[$grader->id] = $grader; } } } return $graders; } /** * Returns a list of users that should receive notification about given submission. * * @param int $userid The submission to grade * @return array */ protected function get_notifiable_users($userid) { // Potential users should be active users only. $potentialusers = get_enrolled_users($this->context, "mod/assign:receivegradernotifications", null, 'u.*', null, null, null, true); $notifiableusers = array(); if (groups_get_activity_groupmode($this->get_course_module()) == SEPARATEGROUPS) { if ($groups = groups_get_all_groups($this->get_course()->id, $userid, $this->get_course_module()->groupingid)) { foreach ($groups as $group) { foreach ($potentialusers as $potentialuser) { if ($potentialuser->id == $userid) { // Do not send self. continue; } if (groups_is_member($group->id, $potentialuser->id)) { $notifiableusers[$potentialuser->id] = $potentialuser; } } } } else { // User not in group, try to find graders without group. foreach ($potentialusers as $potentialuser) { if ($potentialuser->id == $userid) { // Do not send self. continue; } if (!groups_has_membership($this->get_course_module(), $potentialuser->id)) { $notifiableusers[$potentialuser->id] = $potentialuser; } } } } else { foreach ($potentialusers as $potentialuser) { if ($potentialuser->id == $userid) { // Do not send self. continue; } // Must be enrolled. if (is_enrolled($this->get_course_context(), $potentialuser->id)) { $notifiableusers[$potentialuser->id] = $potentialuser; } } } return $notifiableusers; } /** * Message someone about something (static so it can be called from cron). * * @param stdClass $userfrom * @param stdClass $userto * @param string $messagetype * @param string $eventtype * @param int $updatetime * @param stdClass $coursemodule * @param context $context * @param stdClass $course * @param string $modulename - no longer used. * @param string $assignmentname * @param bool $blindmarking * @param int $uniqueidforuser * @param array $extrainfo extra values to pass to any language strings or templates used in preparing the message. * @return void */ public static function send_assignment_notification($userfrom, $userto, $messagetype, $eventtype, $updatetime, $coursemodule, $context, $course, $modulename, $assignmentname, $blindmarking, $uniqueidforuser, $extrainfo = []) { global $CFG, $PAGE; // We put more data into info that is used by the default language strings, // so that there is more flexibility for organisations that want to use // Language Customisation to customise the messages. // Information about the sender - normally the user who performed the action. $info = new stdClass(); if ($blindmarking) { $userfrom = clone($userfrom); $info->username = get_string('participant', 'assign') . ' ' . $uniqueidforuser; $userfrom->firstname = get_string('participant', 'assign'); $userfrom->lastname = $uniqueidforuser; $userfrom->email = $CFG->noreplyaddress; } else { $info->username = core_user::get_fullname($userfrom, $context, ['override' => true]); } // Information about the recipient (for greeting, etc.). $info->recipentname = core_user::get_fullname($userto, $context, ['override' => true]); // Information about the assignment. $info->assignment = format_string($assignmentname, true, ['context' => $context]); $info->url = $CFG->wwwroot . '/mod/assign/view.php?id=' . $coursemodule->id; // Note: URLs here avoid the & character to avoid escaping issues between text and HTML messages. $info->assignmentlink = '' . s($info->assignment) . ' report'; $info->assigncmid = $coursemodule->id; // Information about the course. $info->courseshortname = format_string($course->shortname, true, ['context' => $context]); $info->coursefullname = format_string($course->fullname, true, ['context' => $context]); // Note: URLs here avoid the & character to avoid escaping issues between text and HTML messages. $info->courseurl = $CFG->wwwroot . '/course/view.php?id=' . $course->id; $info->courseassignsurl = $CFG->wwwroot . '/mod/assign/index.php?id=' . $course->id; // Time of the action. $info->timeupdated = userdate($updatetime); // Other data passed in. $info = (object) array_merge((array) $info, $extrainfo); // Since format_string returns text with HTML characters escaped, // we need to un-escape before including in the plain text email and subject line. // (Test with a course or assignment called "Escaping & unescaping" to understand). $plaintextinfo = clone $info; $plaintextinfo->assignment = html_entity_decode($info->assignment); $plaintextinfo->courseshortname = html_entity_decode($info->courseshortname); $plaintextinfo->coursefullname = html_entity_decode($info->coursefullname); // Prepare the message subject and bodies. $postsubject = get_string($messagetype . 'small', 'assign', $plaintextinfo); $smsmessage = get_string($messagetype . 'sms', 'assign', $plaintextinfo); $renderer = $PAGE->get_renderer('mod_assign'); $context = clone $plaintextinfo; $context->messagetext = get_string($messagetype . 'text', 'assign', $plaintextinfo); // Mustache strips off all training whitespace, but we want a newline at the end. $posttext = $renderer->render_from_template( 'mod_assign/messages/notification_text', $context) . "\n"; $posthtml = ''; if ($userto->mailformat == 1) { $context = clone $info; $context->messagehtml = get_string($messagetype . 'html', 'assign', $info); $posthtml = $renderer->render_from_template( 'mod_assign/messages/notification_html', $context); } // Build the message object. $eventdata = new \core\message\message(); $eventdata->courseid = $course->id; $eventdata->modulename = 'assign'; $eventdata->userfrom = $userfrom; $eventdata->userto = $userto; $eventdata->subject = $postsubject; $eventdata->fullmessage = $posttext; $eventdata->fullmessageformat = FORMAT_PLAIN; $eventdata->fullmessagehtml = $posthtml; $eventdata->fullmessagesms = $smsmessage; $eventdata->smallmessage = $postsubject; $eventdata->name = $eventtype; $eventdata->component = 'mod_assign'; $eventdata->notification = 1; $eventdata->contexturl = $info->url; $eventdata->contexturlname = $info->assignment; $customdata = [ 'cmid' => $coursemodule->id, 'instance' => $coursemodule->instance, 'messagetype' => $messagetype, 'blindmarking' => $blindmarking, 'uniqueidforuser' => $uniqueidforuser, ]; // Check if the userfrom is real and visible. if (!empty($userfrom->id) && core_user::is_real_user($userfrom->id)) { $userpicture = new user_picture($userfrom); $userpicture->size = 1; // Use f1 size. $userpicture->includetoken = $userto->id; // Generate an out-of-session token for the user receiving the message. $customdata['notificationiconurl'] = $userpicture->get_url($PAGE)->out(false); } $eventdata->customdata = $customdata; message_send($eventdata); } /** * Message someone about something. * * @param stdClass $userfrom * @param stdClass $userto * @param string $messagetype * @param string $eventtype * @param int $updatetime * @param array $extrainfo extra values to pass to any language strings or templates used in preparing the message. * @return void */ public function send_notification($userfrom, $userto, $messagetype, $eventtype, $updatetime, $extrainfo = []) { global $USER; $userid = core_user::is_real_user($userfrom->id) ? $userfrom->id : $USER->id; $uniqueid = $this->get_uniqueid_for_user($userid); $oldforcelang = force_current_language($userto->lang); self::send_assignment_notification($userfrom, $userto, $messagetype, $eventtype, $updatetime, $this->get_course_module(), $this->get_context(), $this->get_course(), $this->get_module_name(), $this->get_instance()->name, $this->is_blind_marking(), $uniqueid, $extrainfo); force_current_language($oldforcelang); } /** * Notify student upon successful submission copy. * * @param stdClass $submission * @return void */ protected function notify_student_submission_copied(stdClass $submission) { global $DB, $USER; $adminconfig = $this->get_admin_config(); // Use the same setting for this - no need for another one. if (empty($adminconfig->submissionreceipts)) { // No need to do anything. return; } if ($submission->userid) { $user = $DB->get_record('user', array('id'=>$submission->userid), '*', MUST_EXIST); } else { $user = $USER; } $this->send_notification($user, $user, 'submissioncopied', 'assign_notification', $submission->timemodified); } /** * Notify student upon successful submission. * * @param stdClass $submission * @return void */ protected function notify_student_submission_receipt(stdClass $submission) { global $DB, $USER; $adminconfig = $this->get_admin_config(); if (empty($adminconfig->submissionreceipts)) { // No need to do anything. return; } if ($submission->userid) { $user = $DB->get_record('user', array('id'=>$submission->userid), '*', MUST_EXIST); } else { $user = $USER; } // Prepare extra data for submission receipt notification. $oldforcelang = force_current_language($user->lang); $extrainfo = $this->get_submission_summaries_for_messages($submission); force_current_language($oldforcelang); if ($submission->userid == $USER->id) { $this->send_notification(core_user::get_noreply_user(), $user, 'submissionreceipt', 'assign_notification', $submission->timemodified, $extrainfo); } else { $this->send_notification($USER, $user, 'submissionreceiptother', 'assign_notification', $submission->timemodified, $extrainfo); } } /** * Produce a summary of a submission that can be used in messages. * * This function iterates through all enabled submission plugins and calls their * `get_submission_summary` method (if implemented). It aggregates the results * into a formatted summary string. * * @param stdClass $submission the submission the message is about. Row from assign_submission table. * @return string[] with two elements: * 'submissionsummarytext' => a plain text summary, * 'submissionsummaryhtml' => an HTML summary. */ protected function get_submission_summaries_for_messages(stdClass $submission): array { $textsummaries = []; $htmlsummaries = []; foreach ($this->submissionplugins as $plugin) { if ($plugin->is_enabled() && $plugin->is_visible()) { [$textsummary, $htmlsummary] = $plugin->submission_summary_for_messages($submission); if ($textsummary) { $textsummaries[] = $textsummary; } if ($htmlsummary) { $htmlsummaries[] = $htmlsummary; } } } $textsummary = ''; if ($textsummaries) { $textsummary = get_string('submissioncontains', 'assign') . "\n\n" . implode("\n", $textsummaries); } $htmlsummary = ''; if ($htmlsummaries) { $htmlsummary = html_writer::tag('h2', get_string('submissioncontains', 'assign')) . implode('', $htmlsummaries); } return [ 'submissionsummarytext' => $textsummary, 'submissionsummaryhtml' => $htmlsummary, ]; } /** * Send notifications to graders upon student submissions. * * @param stdClass $submission * @return void */ protected function notify_graders(stdClass $submission) { global $DB, $USER; $instance = $this->get_instance(); $late = $instance->duedate && ($instance->duedate < time()); if (!$instance->sendnotifications && !($late && $instance->sendlatenotifications)) { // No need to do anything. return; } if ($submission->userid) { $user = $DB->get_record('user', array('id'=>$submission->userid), '*', MUST_EXIST); } else { $user = $USER; } if ($notifyusers = $this->get_notifiable_users($user->id)) { foreach ($notifyusers as $notifyuser) { $this->send_notification($user, $notifyuser, 'gradersubmissionupdated', 'assign_notification', $submission->timemodified); } } } /** * Submit a submission for grading. * * @param stdClass $data - The form data * @param array $notices - List of error messages to display on an error condition. * @return bool Return false if the submission was not submitted. */ public function submit_for_grading($data, $notices) { global $USER; $userid = $USER->id; if (!empty($data->userid)) { $userid = $data->userid; } // Need submit permission to submit an assignment. if ($userid == $USER->id) { require_capability('mod/assign:submit', $this->context); } else { if (!$this->can_edit_submission($userid, $USER->id)) { throw new \moodle_exception('nopermission'); } } $instance = $this->get_instance(); if ($instance->teamsubmission) { $submission = $this->get_group_submission($userid, 0, true); } else { $submission = $this->get_user_submission($userid, true); } if (!$this->submissions_open($userid)) { $notices[] = get_string('submissionsclosed', 'assign'); return false; } $adminconfig = $this->get_admin_config(); $submissionstatement = ''; if ($instance->requiresubmissionstatement) { $submissionstatement = $this->get_submissionstatement($adminconfig, $instance, $this->context); } if (!empty($submissionstatement) && $instance->requiresubmissionstatement && empty($data->submissionstatement) && $USER->id == $userid) { return false; } if ($submission->status != ASSIGN_SUBMISSION_STATUS_SUBMITTED) { // Give each submission plugin a chance to process the submission. $plugins = $this->get_submission_plugins(); foreach ($plugins as $plugin) { if ($plugin->is_enabled() && $plugin->is_visible()) { $plugin->submit_for_grading($submission); } } $submission->status = ASSIGN_SUBMISSION_STATUS_SUBMITTED; $this->update_submission($submission, $userid, true, $instance->teamsubmission); $completion = new completion_info($this->get_course()); if ($completion->is_enabled($this->get_course_module()) && $instance->completionsubmit) { $this->update_activity_completion_records($instance->teamsubmission, $instance->requireallteammemberssubmit, $submission, $userid, COMPLETION_COMPLETE, $completion); } if (!empty($data->submissionstatement) && $USER->id == $userid) { \mod_assign\event\statement_accepted::create_from_submission($this, $submission)->trigger(); } $this->notify_graders($submission); $this->notify_student_submission_receipt($submission); \mod_assign\event\assessable_submitted::create_from_submission($this, $submission, false)->trigger(); return true; } $notices[] = get_string('submissionsclosed', 'assign'); return false; } /** * A students submission is submitted for grading by a teacher. * * @param moodleform|null $mform If validation failed when submitting this form - this is the moodleform. * It can be null. * @param array $notices Receives error messages to display on an error condition. * @return bool */ protected function process_submit_other_for_grading($mform, &$notices) { global $USER, $CFG; require_sesskey(); $userid = optional_param('userid', 0, PARAM_INT); $blindid = optional_param('blindid', 0, PARAM_INT); if ($this->is_blind_marking() && !$userid && $blindid) { $userid = $this->get_user_id_for_uniqueid($blindid); } // If no userid specified, default to current user. if (!$userid) { $userid = $USER->id; } if (!$this->submissions_open($userid)) { $notices[] = get_string('submissionsclosed', 'assign'); return false; } $data = new stdClass(); $data->userid = $userid; return $this->submit_for_grading($data, $notices); } /** * Assignment submission is processed before grading. * * @param moodleform|null $mform If validation failed when submitting this form - this is the moodleform. * It can be null. * @param array $notices Receives error messages to display on an error condition. * @return bool Return false if the validation fails. This affects which page is displayed next. */ protected function process_submit_for_grading($mform, &$notices) { global $CFG; require_once($CFG->dirroot . '/mod/assign/submissionconfirmform.php'); require_sesskey(); if (!$this->submissions_open()) { $notices[] = get_string('submissionsclosed', 'assign'); return false; } $data = new stdClass(); $adminconfig = $this->get_admin_config(); $requiresubmissionstatement = $this->get_instance()->requiresubmissionstatement; $submissionstatement = ''; if ($requiresubmissionstatement) { $submissionstatement = $this->get_submissionstatement($adminconfig, $this->get_instance(), $this->get_context()); } // If we get back an empty submission statement, we have to set $requiredsubmisisonstatement to false to prevent // that the submission statement checkbox will be displayed. if (empty($submissionstatement)) { $requiresubmissionstatement = false; } if ($mform == null) { $mform = new mod_assign_confirm_submission_form(null, array($requiresubmissionstatement, $submissionstatement, $this->get_course_module()->id, $data)); } $data = $mform->get_data(); if (!$mform->is_cancelled()) { if ($mform->get_data() == false) { return false; } return $this->submit_for_grading($data, $notices); } return true; } /** * Save the extension date for a single user. * * @param int $userid The user id * @param mixed $extensionduedate Either an integer date or null * @return boolean */ public function save_user_extension($userid, $extensionduedate) { global $DB, $CFG; require_once($CFG->dirroot.'/calendar/lib.php'); // Need submit permission to submit an assignment. require_capability('mod/assign:grantextension', $this->context); if (!is_enrolled($this->get_course_context(), $userid)) { return false; } if (!has_capability('mod/assign:submit', $this->context, $userid)) { return false; } if ($this->get_instance()->duedate && $extensionduedate) { if ($this->get_instance()->duedate > $extensionduedate) { return false; } } if ($this->get_instance()->allowsubmissionsfromdate && $extensionduedate) { if ($this->get_instance()->allowsubmissionsfromdate > $extensionduedate) { return false; } } $flags = $this->get_user_flags($userid, true); $flags->extensionduedate = $extensionduedate; $result = $this->update_user_flags($flags); if ($result) { \mod_assign\event\extension_granted::create_from_assign($this, $userid)->trigger(); $cm = $this->get_course_module(); $instance = $this->get_instance(); if ($extensionduedate) { $event = $DB->get_record('event', [ 'userid' => $userid, 'eventtype' => ASSIGN_EVENT_TYPE_EXTENSION, 'modulename' => 'assign', 'instance' => $instance->id, ]); if ($event) { $event->timestart = $extensionduedate; $DB->update_record('event', $event); } else { $event = new stdClass(); $event->type = CALENDAR_EVENT_TYPE_ACTION; $event->name = get_string('calendarextension', 'assign', $instance->name); $event->description = format_module_intro('assign', $instance, $cm->id); $event->format = FORMAT_HTML; $event->courseid = 0; $event->groupid = 0; $event->userid = $userid; $event->modulename = 'assign'; $event->instance = $instance->id; $event->timestart = $extensionduedate; $event->timeduration = 0; $event->visible = instance_is_visible('assign', $instance); $event->eventtype = ASSIGN_EVENT_TYPE_EXTENSION; $event->priority = null; calendar_event::create($event, false); } } else { $DB->delete_records('event', [ 'userid' => $userid, 'eventtype' => ASSIGN_EVENT_TYPE_EXTENSION, 'modulename' => 'assign', 'instance' => $instance->id, ]); } } return $result; } /** * Save extension date. * * @param moodleform $mform The submitted form * @return boolean */ protected function process_save_extension(& $mform) { global $DB, $CFG; // Include extension form. require_once($CFG->dirroot . '/mod/assign/extensionform.php'); require_sesskey(); $users = optional_param('userid', 0, PARAM_INT); if (!$users) { $users = required_param('selectedusers', PARAM_SEQUENCE); } $userlist = explode(',', $users); $keys = array('duedate', 'cutoffdate', 'allowsubmissionsfromdate'); $maxoverride = array('allowsubmissionsfromdate' => 0, 'duedate' => 0, 'cutoffdate' => 0); foreach ($userlist as $userid) { // To validate extension date with users overrides. $override = $this->override_exists($userid); foreach ($keys as $key) { if ($override->{$key}) { if ($maxoverride[$key] < $override->{$key}) { $maxoverride[$key] = $override->{$key}; } } else if ($maxoverride[$key] < $this->get_instance()->{$key}) { $maxoverride[$key] = $this->get_instance()->{$key}; } } } foreach ($keys as $key) { if ($maxoverride[$key]) { $this->get_instance()->{$key} = $maxoverride[$key]; } } $formparams = array( 'instance' => $this->get_instance(), 'assign' => $this, 'userlist' => $userlist ); $mform = new mod_assign_extension_form(null, $formparams); if ($mform->is_cancelled()) { return true; } if ($formdata = $mform->get_data()) { if (!empty($formdata->selectedusers)) { $users = explode(',', $formdata->selectedusers); $result = true; foreach ($users as $userid) { $user = $DB->get_record('user', array('id' => $userid), '*', MUST_EXIST); $result = $this->save_user_extension($user->id, $formdata->extensionduedate) && $result; } return $result; } if (!empty($formdata->userid)) { $user = $DB->get_record('user', array('id' => $formdata->userid), '*', MUST_EXIST); return $this->save_user_extension($user->id, $formdata->extensionduedate); } } return false; } /** * Save quick grades. * * @return string The result of the save operation */ protected function process_save_quick_grades() { global $USER, $DB, $CFG; // Need grade permission. require_capability('mod/assign:grade', $this->context); require_sesskey(); // Make sure advanced grading is disabled. $gradingmanager = get_grading_manager($this->get_context(), 'mod_assign', 'submissions'); $controller = $gradingmanager->get_active_controller(); if (!empty($controller)) { $message = get_string('errorquickgradingvsadvancedgrading', 'assign'); $this->set_error_message($message); return $message; } $users = array(); // First check all the last modified values. $currentgroup = groups_get_activity_group($this->get_course_module(), true); $participants = $this->list_participants($currentgroup, true); // Gets a list of possible users and look for values based upon that. foreach ($participants as $userid => $unused) { $modified = optional_param('grademodified_' . $userid, -1, PARAM_INT); $attemptnumber = optional_param('gradeattempt_' . $userid, -1, PARAM_INT); // Gather the userid, updated grade and last modified value. $record = new stdClass(); $record->userid = $userid; if ($modified >= 0) { $record->grade = unformat_float(optional_param('quickgrade_' . $record->userid, -1, PARAM_TEXT)); $record->workflowstate = optional_param('quickgrade_' . $record->userid.'_workflowstate', false, PARAM_ALPHA); $record->allocatedmarker = optional_param('quickgrade_' . $record->userid.'_allocatedmarker', false, PARAM_INT); } else { // This user was not in the grading table. continue; } $record->attemptnumber = $attemptnumber; $record->lastmodified = $modified; $record->gradinginfo = grade_get_grades($this->get_course()->id, 'mod', 'assign', $this->get_instance()->id, array($userid)); $users[$userid] = $record; } if (empty($users)) { $message = get_string('nousersselected', 'assign'); $this->set_error_message($message); return $message; } list($userids, $params) = $DB->get_in_or_equal(array_keys($users), SQL_PARAMS_NAMED); $params['assignid1'] = $this->get_instance()->id; $params['assignid2'] = $this->get_instance()->id; // Check them all for currency. $grademaxattempt = 'SELECT s.userid, s.attemptnumber AS maxattempt FROM {assign_submission} s WHERE s.assignment = :assignid1 AND s.latest = 1'; $sql = 'SELECT u.id AS userid, g.grade AS grade, g.timemodified AS lastmodified, uf.workflowstate, uf.allocatedmarker, gmx.maxattempt AS attemptnumber FROM {user} u LEFT JOIN ( ' . $grademaxattempt . ' ) gmx ON u.id = gmx.userid LEFT JOIN {assign_grades} g ON u.id = g.userid AND g.assignment = :assignid2 AND g.attemptnumber = gmx.maxattempt LEFT JOIN {assign_user_flags} uf ON uf.assignment = g.assignment AND uf.userid = g.userid WHERE u.id ' . $userids; $currentgrades = $DB->get_recordset_sql($sql, $params); $modifiedusers = array(); foreach ($currentgrades as $current) { $modified = $users[(int)$current->userid]; $grade = $this->get_user_grade($modified->userid, false); // Check to see if the grade column was even visible. $gradecolpresent = optional_param('quickgrade_' . $modified->userid, false, PARAM_INT) !== false; // Check to see if the outcomes were modified. if ($CFG->enableoutcomes) { foreach ($modified->gradinginfo->outcomes as $outcomeid => $outcome) { $oldoutcome = $outcome->grades[$modified->userid]->grade; $paramname = 'outcome_' . $outcomeid . '_' . $modified->userid; $newoutcome = optional_param($paramname, -1, PARAM_FLOAT); // Check to see if the outcome column was even visible. $outcomecolpresent = optional_param($paramname, false, PARAM_FLOAT) !== false; if ($outcomecolpresent && ($oldoutcome != $newoutcome)) { // Can't check modified time for outcomes because it is not reported. $modifiedusers[$modified->userid] = $modified; continue; } } } // Let plugins participate. foreach ($this->feedbackplugins as $plugin) { if ($plugin->is_visible() && $plugin->is_enabled() && $plugin->supports_quickgrading()) { // The plugins must handle is_quickgrading_modified correctly - ie // handle hidden columns. if ($plugin->is_quickgrading_modified($modified->userid, $grade)) { if ((int)$current->lastmodified > (int)$modified->lastmodified) { $message = get_string('errorrecordmodified', 'assign'); $this->set_error_message($message); return $message; } else { $modifiedusers[$modified->userid] = $modified; continue; } } } } if (($current->grade < 0 || $current->grade === null) && ($modified->grade < 0 || $modified->grade === null)) { // Different ways to indicate no grade. $modified->grade = $current->grade; // Keep existing grade. } // Treat 0 and null as different values. if ($current->grade !== null) { $current->grade = floatval($current->grade); } $gradechanged = $gradecolpresent && grade_floats_different($current->grade, $modified->grade); $markingallocationchanged = $this->get_instance()->markingworkflow && $this->get_instance()->markingallocation && ($modified->allocatedmarker !== false) && ($current->allocatedmarker != $modified->allocatedmarker); $workflowstatechanged = $this->get_instance()->markingworkflow && ($modified->workflowstate !== false) && ($current->workflowstate != $modified->workflowstate); if ($gradechanged || $markingallocationchanged || $workflowstatechanged) { // Grade changed. if ($this->grading_disabled($modified->userid)) { continue; } $badmodified = (int)$current->lastmodified > (int)$modified->lastmodified; $badattempt = (int)$current->attemptnumber != (int)$modified->attemptnumber; if ($badmodified || $badattempt) { // Error - record has been modified since viewing the page. $message = get_string('errorrecordmodified', 'assign'); $this->set_error_message($message); return $message; } else { $modifiedusers[$modified->userid] = $modified; } } } $currentgrades->close(); $adminconfig = $this->get_admin_config(); $gradebookplugin = $adminconfig->feedback_plugin_for_gradebook; // Ok - ready to process the updates. foreach ($modifiedusers as $userid => $modified) { $grade = $this->get_user_grade($userid, true); $flags = $this->get_user_flags($userid, true); $grade->grade= grade_floatval(unformat_float($modified->grade)); $grade->grader= $USER->id; $gradecolpresent = optional_param('quickgrade_' . $userid, false, PARAM_INT) !== false; // Save plugins data. foreach ($this->feedbackplugins as $plugin) { if ($plugin->is_visible() && $plugin->is_enabled() && $plugin->supports_quickgrading()) { $plugin->save_quickgrading_changes($userid, $grade); if (('assignfeedback_' . $plugin->get_type()) == $gradebookplugin) { // This is the feedback plugin chose to push comments to the gradebook. $grade->feedbacktext = $plugin->text_for_gradebook($grade); $grade->feedbackformat = $plugin->format_for_gradebook($grade); $grade->feedbackfiles = $plugin->files_for_gradebook($grade); } } } // These will be set to false if they are not present in the quickgrading // form (e.g. column hidden). $workflowstatemodified = ($modified->workflowstate !== false) && ($flags->workflowstate != $modified->workflowstate); $allocatedmarkermodified = ($modified->allocatedmarker !== false) && ($flags->allocatedmarker != $modified->allocatedmarker); if ($workflowstatemodified) { $flags->workflowstate = $modified->workflowstate; } if ($allocatedmarkermodified) { $flags->allocatedmarker = $modified->allocatedmarker; } if ($workflowstatemodified || $allocatedmarkermodified) { if ($this->update_user_flags($flags) && $workflowstatemodified) { $user = $DB->get_record('user', array('id' => $userid), '*', MUST_EXIST); \mod_assign\event\workflow_state_updated::create_from_user($this, $user, $flags->workflowstate)->trigger(); } } $this->update_grade($grade); // Allow teachers to skip sending notifications. if (optional_param('sendstudentnotifications', true, PARAM_BOOL)) { $this->notify_grade_modified($grade, true); } // Save outcomes. if ($CFG->enableoutcomes) { $data = array(); foreach ($modified->gradinginfo->outcomes as $outcomeid => $outcome) { $oldoutcome = $outcome->grades[$modified->userid]->grade; $paramname = 'outcome_' . $outcomeid . '_' . $modified->userid; // This will be false if the input was not in the quickgrading // form (e.g. column hidden). $newoutcome = optional_param($paramname, false, PARAM_INT); if ($newoutcome !== false && ($oldoutcome != $newoutcome)) { $data[$outcomeid] = $newoutcome; } } if (count($data) > 0) { grade_update_outcomes('mod/assign', $this->course->id, 'mod', 'assign', $this->get_instance()->id, $userid, $data); } } } return get_string('quickgradingchangessaved', 'assign'); } /** * Reveal student identities to markers (and the gradebook). * * @return void */ public function reveal_identities() { global $DB; require_capability('mod/assign:revealidentities', $this->context); if ($this->get_instance()->revealidentities || empty($this->get_instance()->blindmarking)) { return false; } // Update the assignment record. $update = new stdClass(); $update->id = $this->get_instance()->id; $update->revealidentities = 1; $DB->update_record('assign', $update); // Refresh the instance data. $this->instance = null; // Release the grades to the gradebook. // First create the column in the gradebook. $this->update_gradebook(false, $this->get_course_module()->id); // Now release all grades. $adminconfig = $this->get_admin_config(); $gradebookplugin = $adminconfig->feedback_plugin_for_gradebook; $gradebookplugin = str_replace('assignfeedback_', '', $gradebookplugin); $grades = $DB->get_records('assign_grades', array('assignment'=>$this->get_instance()->id)); $plugin = $this->get_feedback_plugin_by_type($gradebookplugin); foreach ($grades as $grade) { // Fetch any comments for this student. if ($plugin && $plugin->is_enabled() && $plugin->is_visible()) { $grade->feedbacktext = $plugin->text_for_gradebook($grade); $grade->feedbackformat = $plugin->format_for_gradebook($grade); $grade->feedbackfiles = $plugin->files_for_gradebook($grade); } $this->gradebook_item_update(null, $grade); } \mod_assign\event\identities_revealed::create_from_assign($this)->trigger(); } /** * Reveal student identities to markers (and the gradebook). * * @return void */ protected function process_reveal_identities() { if (!confirm_sesskey()) { return false; } return $this->reveal_identities(); } /** * Save grading options. * * @deprecated since Moodle 4.5 * @todo Final deprecation in Moodle 6.0. See MDL-82876. * @return void */ #[\core\attribute\deprecated( 'null', since: '4.5', reason: 'It is no longer used.', mdl: 'MDL-82681', )] protected function process_save_grading_options() { \core\deprecation::emit_deprecation([self::class, __FUNCTION__]); } /** * Require a valid sess key and then call copy_previous_attempt. * * @param array $notices Any error messages that should be shown * to the user at the top of the edit submission form. * @return bool */ protected function process_copy_previous_attempt(&$notices) { require_sesskey(); return $this->copy_previous_attempt($notices); } /** * Copy the current assignment submission from the last submitted attempt. * * @param array $notices Any error messages that should be shown * to the user at the top of the edit submission form. * @return bool */ public function copy_previous_attempt(&$notices) { global $USER, $CFG; require_capability('mod/assign:submit', $this->context); $instance = $this->get_instance(); if ($instance->teamsubmission) { $submission = $this->get_group_submission($USER->id, 0, true); } else { $submission = $this->get_user_submission($USER->id, true); } if (!$submission || $submission->status != ASSIGN_SUBMISSION_STATUS_REOPENED) { $notices[] = get_string('submissionnotcopiedinvalidstatus', 'assign'); return false; } $flags = $this->get_user_flags($USER->id, false); // Get the flags to check if it is locked. if ($flags && $flags->locked) { $notices[] = get_string('submissionslocked', 'assign'); return false; } if ($instance->submissiondrafts) { $submission->status = ASSIGN_SUBMISSION_STATUS_DRAFT; } else { $submission->status = ASSIGN_SUBMISSION_STATUS_SUBMITTED; } $this->update_submission($submission, $USER->id, true, $instance->teamsubmission); // Find the previous submission. if ($instance->teamsubmission) { $previoussubmission = $this->get_group_submission($USER->id, 0, true, $submission->attemptnumber - 1); } else { $previoussubmission = $this->get_user_submission($USER->id, true, $submission->attemptnumber - 1); } if (!$previoussubmission) { // There was no previous submission so there is nothing else to do. return true; } $pluginerror = false; foreach ($this->get_submission_plugins() as $plugin) { if ($plugin->is_visible() && $plugin->is_enabled()) { if (!$plugin->copy_submission($previoussubmission, $submission)) { $notices[] = $plugin->get_error(); $pluginerror = true; } } } if ($pluginerror) { return false; } \mod_assign\event\submission_duplicated::create_from_submission($this, $submission)->trigger(); $complete = COMPLETION_INCOMPLETE; if ($submission->status == ASSIGN_SUBMISSION_STATUS_SUBMITTED) { $complete = COMPLETION_COMPLETE; } $completion = new completion_info($this->get_course()); if ($completion->is_enabled($this->get_course_module()) && $instance->completionsubmit) { $this->update_activity_completion_records($instance->teamsubmission, $instance->requireallteammemberssubmit, $submission, $USER->id, $complete, $completion); } if (!$instance->submissiondrafts) { // There is a case for not notifying the student about the submission copy, // but it provides a record of the event and if they then cancel editing it // is clear that the submission was copied. $this->notify_student_submission_copied($submission); $this->notify_graders($submission); // The same logic applies here - we could not notify teachers, // but then they would wonder why there are submitted assignments // and they haven't been notified. \mod_assign\event\assessable_submitted::create_from_submission($this, $submission, true)->trigger(); } return true; } /** * Determine if the current submission is empty or not. * * @param submission $submission the students submission record to check. * @return bool */ public function submission_empty($submission) { $allempty = true; foreach ($this->submissionplugins as $plugin) { if ($plugin->is_enabled() && $plugin->is_visible()) { if (!$allempty || !$plugin->is_empty($submission)) { $allempty = false; } } } return $allempty; } /** * Determine if a new submission is empty or not * * @param stdClass $data Submission data * @return bool */ public function new_submission_empty($data) { foreach ($this->submissionplugins as $plugin) { if ($plugin->is_enabled() && $plugin->is_visible() && $plugin->allow_submissions() && !$plugin->submission_is_empty($data)) { return false; } } return true; } /** * Save assignment submission for the current user. * * @param stdClass $data * @param array $notices Any error messages that should be shown * to the user. * @return bool */ public function save_submission(stdClass $data, & $notices) { global $CFG, $USER, $DB; $userid = $USER->id; if (!empty($data->userid)) { $userid = $data->userid; } $user = clone($USER); if ($userid == $USER->id) { require_capability('mod/assign:submit', $this->context); } else { $user = $DB->get_record('user', array('id'=>$userid), '*', MUST_EXIST); if (!$this->can_edit_submission($userid, $USER->id)) { throw new \moodle_exception('nopermission'); } } $instance = $this->get_instance(); if ($instance->teamsubmission) { $submission = $this->get_group_submission($userid, 0, true); } else { $submission = $this->get_user_submission($userid, true); } if ($this->new_submission_empty($data)) { $notices[] = get_string('submissionempty', 'mod_assign'); return false; } // Check that no one has modified the submission since we started looking at it. if (isset($data->lastmodified) && ($submission->timemodified > $data->lastmodified)) { // Another user has submitted something. Notify the current user. if ($submission->status !== ASSIGN_SUBMISSION_STATUS_NEW) { $notices[] = $instance->teamsubmission ? get_string('submissionmodifiedgroup', 'mod_assign') : get_string('submissionmodified', 'mod_assign'); return false; } } if ($instance->submissiondrafts) { $submission->status = ASSIGN_SUBMISSION_STATUS_DRAFT; } else { $submission->status = ASSIGN_SUBMISSION_STATUS_SUBMITTED; } $flags = $this->get_user_flags($userid, false); // Get the flags to check if it is locked. if ($flags && $flags->locked) { throw new \moodle_exception('submissionslocked', 'assign'); return true; } $pluginerror = false; foreach ($this->submissionplugins as $plugin) { if ($plugin->is_enabled() && $plugin->is_visible()) { if (!$plugin->save($submission, $data)) { $notices[] = $plugin->get_error(); $pluginerror = true; } } } $allempty = $this->submission_empty($submission); if ($pluginerror || $allempty) { if ($allempty) { $notices[] = get_string('submissionempty', 'mod_assign'); } return false; } $this->update_submission($submission, $userid, true, $instance->teamsubmission); $users = [$userid]; if ($instance->teamsubmission && !$instance->requireallteammemberssubmit) { $team = $this->get_submission_group_members($submission->groupid, true); foreach ($team as $member) { if ($member->id != $userid) { $membersubmission = clone($submission); $this->update_submission($membersubmission, $member->id, true, $instance->teamsubmission); $users[] = $member->id; } } } $complete = COMPLETION_INCOMPLETE; if ($submission->status == ASSIGN_SUBMISSION_STATUS_SUBMITTED) { $complete = COMPLETION_COMPLETE; } $completion = new completion_info($this->get_course()); if ($completion->is_enabled($this->get_course_module()) && $instance->completionsubmit) { foreach ($users as $id) { $completion->update_state($this->get_course_module(), $complete, $id); } } // Logging. if (isset($data->submissionstatement) && ($userid == $USER->id)) { \mod_assign\event\statement_accepted::create_from_submission($this, $submission)->trigger(); } if (!$instance->submissiondrafts) { $this->notify_student_submission_receipt($submission); $this->notify_graders($submission); \mod_assign\event\assessable_submitted::create_from_submission($this, $submission, true)->trigger(); } return true; } /** * Save assignment submission. * * @param moodleform $mform * @param array $notices Any error messages that should be shown * to the user at the top of the edit submission form. * @return bool */ protected function process_save_submission(&$mform, &$notices) { global $CFG, $USER; // Include submission form. require_once($CFG->dirroot . '/mod/assign/submission_form.php'); $userid = optional_param('userid', $USER->id, PARAM_INT); // Need submit permission to submit an assignment. require_sesskey(); if (!$this->submissions_open($userid)) { $notices[] = get_string('duedatereached', 'assign'); return false; } $instance = $this->get_instance(); $data = new stdClass(); $data->userid = $userid; $mform = new mod_assign_submission_form(null, array($this, $data)); if ($mform->is_cancelled()) { return true; } if ($data = $mform->get_data()) { return $this->save_submission($data, $notices); } return false; } /** * Determine if this users grade can be edited. * * @param int $userid - The student userid * @param bool $checkworkflow - whether to include a check for the workflow state. * @param stdClass $gradinginfo - optional, allow gradinginfo to be passed for performance. * @return bool $gradingdisabled */ public function grading_disabled($userid, $checkworkflow = true, $gradinginfo = null) { if ($checkworkflow && $this->get_instance()->markingworkflow) { $grade = $this->get_user_grade($userid, false); $validstates = $this->get_marking_workflow_states_for_current_user(); if (!empty($grade) && !empty($grade->workflowstate) && !array_key_exists($grade->workflowstate, $validstates)) { return true; } } if (is_null($gradinginfo)) { $gradinginfo = grade_get_grades($this->get_course()->id, 'mod', 'assign', $this->get_instance()->id, array($userid)); } if (!$gradinginfo) { return false; } if (!isset($gradinginfo->items[0]->grades[$userid])) { return false; } $gradingdisabled = $gradinginfo->items[0]->grades[$userid]->locked || $gradinginfo->items[0]->grades[$userid]->overridden; return $gradingdisabled; } /** * Get an instance of a grading form if advanced grading is enabled. * This is specific to the assignment, marker and student. * * @param int $userid - The student userid * @param stdClass|false $grade - The grade record * @param bool $gradingdisabled * @return mixed gradingform_instance|null $gradinginstance */ protected function get_grading_instance($userid, $grade, $gradingdisabled) { global $CFG, $USER; $grademenu = make_grades_menu($this->get_instance()->grade); $allowgradedecimals = $this->get_instance()->grade > 0; $advancedgradingwarning = false; $gradingmanager = get_grading_manager($this->context, 'mod_assign', 'submissions'); $gradinginstance = null; if ($gradingmethod = $gradingmanager->get_active_method()) { $controller = $gradingmanager->get_controller($gradingmethod); if ($controller->is_form_available()) { $itemid = null; if ($grade) { $itemid = $grade->id; } if ($gradingdisabled && $itemid) { $gradinginstance = $controller->get_current_instance($USER->id, $itemid); } else if (!$gradingdisabled) { $instanceid = optional_param('advancedgradinginstanceid', 0, PARAM_INT); $gradinginstance = $controller->get_or_create_instance($instanceid, $USER->id, $itemid); } } else { $advancedgradingwarning = $controller->form_unavailable_notification(); } } if ($gradinginstance) { $gradinginstance->get_controller()->set_grade_range($grademenu, $allowgradedecimals); } return $gradinginstance; } /** * Add elements to grade form. * * @param MoodleQuickForm $mform * @param stdClass $data * @param array $params * @return void */ public function add_grade_form_elements(MoodleQuickForm $mform, stdClass $data, $params) { global $USER, $CFG, $SESSION, $PAGE; $settings = $this->get_instance(); $rownum = isset($params['rownum']) ? $params['rownum'] : 0; $last = isset($params['last']) ? $params['last'] : true; $useridlistid = isset($params['useridlistid']) ? $params['useridlistid'] : 0; $userid = isset($params['userid']) ? $params['userid'] : 0; $attemptnumber = isset($params['attemptnumber']) ? $params['attemptnumber'] : 0; $gradingpanel = !empty($params['gradingpanel']); $bothids = ($userid && $useridlistid); if (!$userid || $bothids) { $useridlist = $this->get_grading_userid_list(true, $useridlistid); } else { $useridlist = array($userid); $rownum = 0; $useridlistid = ''; } $userid = $useridlist[$rownum]; // We need to create a grade record matching this attempt number // or the feedback plugin will have no way to know what is the correct attempt. $grade = $this->get_user_grade($userid, true, $attemptnumber); $submission = null; if ($this->get_instance()->teamsubmission) { $submission = $this->get_group_submission($userid, 0, false, $attemptnumber); } else { $submission = $this->get_user_submission($userid, false, $attemptnumber); } // Add advanced grading. $gradingdisabled = $this->grading_disabled($userid); $gradinginstance = $this->get_grading_instance($userid, $grade, $gradingdisabled); $mform->addElement('header', 'gradeheader', get_string('gradenoun')); if ($gradinginstance) { $gradingelement = $mform->addElement('grading', 'advancedgrading', get_string('gradenoun') . ':', array('gradinginstance' => $gradinginstance)); if ($gradingdisabled) { $gradingelement->freeze(); } else { $mform->addElement('hidden', 'advancedgradinginstanceid', $gradinginstance->get_id()); $mform->setType('advancedgradinginstanceid', PARAM_INT); } } else { // Use simple direct grading. if ($this->get_instance()->grade > 0) { $name = get_string('gradeoutof', 'assign', $this->get_instance()->grade); if (!$gradingdisabled) { $gradingelement = $mform->addElement('text', 'grade', $name); $mform->addHelpButton('grade', 'gradeoutofhelp', 'assign'); $mform->setType('grade', PARAM_RAW); } else { $strgradelocked = get_string('gradelocked', 'assign'); $mform->addElement('static', 'gradedisabled', $name, $strgradelocked); $mform->addHelpButton('gradedisabled', 'gradeoutofhelp', 'assign'); } } else { $grademenu = array(-1 => get_string("nograde")) + make_grades_menu($this->get_instance()->grade); if (count($grademenu) > 1) { $gradingelement = $mform->addElement('select', 'grade', get_string('gradenoun') . ':', $grademenu); // The grade is already formatted with format_float so it needs to be converted back to an integer. if (!empty($data->grade)) { $data->grade = (int)unformat_float($data->grade); } $mform->setType('grade', PARAM_INT); if ($gradingdisabled) { $gradingelement->freeze(); } } } } $gradinginfo = grade_get_grades($this->get_course()->id, 'mod', 'assign', $this->get_instance()->id, $userid); if (!empty($CFG->enableoutcomes)) { foreach ($gradinginfo->outcomes as $index => $outcome) { $options = make_grades_menu(-$outcome->scaleid); $options[0] = get_string('nooutcome', 'grades'); if ($outcome->grades[$userid]->locked) { $mform->addElement('static', 'outcome_' . $index . '[' . $userid . ']', $outcome->name . ':', $options[$outcome->grades[$userid]->grade]); } else { $attributes = array('id' => 'menuoutcome_' . $index ); $mform->addElement('select', 'outcome_' . $index . '[' . $userid . ']', $outcome->name.':', $options, $attributes); $mform->setType('outcome_' . $index . '[' . $userid . ']', PARAM_INT); $mform->setDefault('outcome_' . $index . '[' . $userid . ']', $outcome->grades[$userid]->grade); } } } $capabilitylist = array('gradereport/grader:view', 'moodle/grade:viewall'); $usergrade = get_string('notgraded', 'assign'); if (has_all_capabilities($capabilitylist, $this->get_course_context())) { $urlparams = array('id'=>$this->get_course()->id); $url = new moodle_url('/grade/report/grader/index.php', $urlparams); if (isset($gradinginfo->items[0]->grades[$userid]->grade)) { $usergrade = $gradinginfo->items[0]->grades[$userid]->str_grade; } $gradestring = $this->get_renderer()->action_link($url, $usergrade); } else { if (isset($gradinginfo->items[0]->grades[$userid]) && !$gradinginfo->items[0]->grades[$userid]->hidden) { $usergrade = $gradinginfo->items[0]->grades[$userid]->str_grade; } $gradestring = $usergrade; } // Penalty indicator. $userassigngrade = $gradinginfo->items[0]->grades[$userid]; if (isset($userassigngrade->grade)) { $gradegrade = new \grade_grade(); $gradegrade->deductedmark = $userassigngrade->deductedmark; $gradegrade->overridden = $userassigngrade->overridden; $penaltyindicator = \core_grades\penalty_manager::show_penalty_indicator($gradegrade); $gradestring = $penaltyindicator . $gradestring; } if ($this->get_instance()->markingworkflow) { $states = $this->get_marking_workflow_states_for_current_user(); $options = array('' => get_string('markingworkflowstatenotmarked', 'assign')) + $states; $select = $mform->addElement('select', 'workflowstate', get_string('markingworkflowstate', 'assign'), $options); $mform->addHelpButton('workflowstate', 'markingworkflowstate', 'assign'); if (!empty($data->workflowstate) && !array_key_exists($data->workflowstate, $states)) { // In a workflow state that user should not be able to change, so freeze workflow selector. // Have to add the state so it shows in the frozen selector. $allworkflowstates = $this->get_all_marking_workflow_states(); $select->addOption($allworkflowstates[$data->workflowstate], $data->workflowstate); $mform->freeze('workflowstate'); } $gradingstatus = $this->get_grading_status($userid); if ($gradingstatus != ASSIGN_MARKING_WORKFLOW_STATE_RELEASED) { if ($grade->grade && $grade->grade != -1) { if ($settings->grade > 0) { $assigngradestring = format_float($grade->grade, $this->get_grade_item()->get_decimals()); } else { $assigngradestring = make_grades_menu($settings->grade)[grade_floatval($grade->grade)]; } $assigngradestring = html_writer::span($assigngradestring, 'currentgrade'); $label = get_string('currentassigngrade', 'assign'); $mform->addElement('static', 'currentassigngrade', $label, $assigngradestring); } } } if ($this->get_instance()->markingworkflow && $this->get_instance()->markingallocation && has_capability('mod/assign:manageallocations', $this->context)) { list($sort, $params) = users_order_by_sql('u'); // Only enrolled users could be assigned as potential markers. $markers = get_enrolled_users($this->context, 'mod/assign:grade', 0, 'u.*', $sort); $markerlist = array('' => get_string('choosemarker', 'assign')); $viewfullnames = has_capability('moodle/site:viewfullnames', $this->context); foreach ($markers as $marker) { $markerlist[$marker->id] = fullname($marker, $viewfullnames); } $mform->addElement('select', 'allocatedmarker', get_string('allocatedmarker', 'assign'), $markerlist); $mform->addHelpButton('allocatedmarker', 'allocatedmarker', 'assign'); $mform->disabledIf('allocatedmarker', 'workflowstate', 'eq', ASSIGN_MARKING_WORKFLOW_STATE_READYFORREVIEW); $mform->disabledIf('allocatedmarker', 'workflowstate', 'eq', ASSIGN_MARKING_WORKFLOW_STATE_INREVIEW); $mform->disabledIf('allocatedmarker', 'workflowstate', 'eq', ASSIGN_MARKING_WORKFLOW_STATE_READYFORRELEASE); $mform->disabledIf('allocatedmarker', 'workflowstate', 'eq', ASSIGN_MARKING_WORKFLOW_STATE_RELEASED); } $gradestring = '' . $gradestring . ''; $mform->addElement('static', 'currentgrade', get_string('currentgrade', 'assign'), $gradestring); if (count($useridlist) > 1) { $strparams = array('current'=>$rownum+1, 'total'=>count($useridlist)); $name = get_string('outof', 'assign', $strparams); $mform->addElement('static', 'gradingstudent', get_string('gradingstudent', 'assign'), $name); } // Let feedback plugins add elements to the grading form. $this->add_plugin_grade_elements($grade, $mform, $data, $userid); // Hidden params. $mform->addElement('hidden', 'id', $this->get_course_module()->id); $mform->setType('id', PARAM_INT); $mform->addElement('hidden', 'rownum', $rownum); $mform->setType('rownum', PARAM_INT); $mform->setConstant('rownum', $rownum); $mform->addElement('hidden', 'useridlistid', $useridlistid); $mform->setType('useridlistid', PARAM_ALPHANUM); $mform->addElement('hidden', 'attemptnumber', $attemptnumber); $mform->setType('attemptnumber', PARAM_INT); $mform->addElement('hidden', 'ajax', optional_param('ajax', 0, PARAM_INT)); $mform->setType('ajax', PARAM_INT); $mform->addElement('hidden', 'userid', optional_param('userid', 0, PARAM_INT)); $mform->setType('userid', PARAM_INT); if ($this->get_instance()->teamsubmission) { $mform->addElement('header', 'groupsubmissionsettings', get_string('groupsubmissionsettings', 'assign')); $mform->addElement('selectyesno', 'applytoall', get_string('applytoteam', 'assign')); $mform->setDefault('applytoall', 1); } // Do not show if we are editing a previous attempt. if (($attemptnumber == -1 || ($attemptnumber + 1) == count($this->get_all_submissions($userid))) && ($this->get_instance()->maxattempts > 1 || $this->get_instance()->maxattempts == ASSIGN_UNLIMITED_ATTEMPTS)) { $mform->addElement('header', 'attemptsettings', get_string('attemptsettings', 'assign')); $attemptreopenmethod = get_string('attemptreopenmethod_' . $this->get_instance()->attemptreopenmethod, 'assign'); $mform->addElement('static', 'attemptreopenmethod', get_string('attemptreopenmethod', 'assign'), $attemptreopenmethod); $attemptnumber = 0; if ($submission) { $attemptnumber = $submission->attemptnumber; } $maxattempts = $this->get_instance()->maxattempts; if ($maxattempts == ASSIGN_UNLIMITED_ATTEMPTS) { $maxattempts = get_string('unlimitedattempts', 'assign'); } $mform->addelement('static', 'maxattemptslabel', get_string('maxattempts', 'assign'), $maxattempts); $mform->addelement('static', 'attemptnumberlabel', get_string('attemptnumber', 'assign'), $attemptnumber + 1); $ismanual = $this->get_instance()->attemptreopenmethod == ASSIGN_ATTEMPT_REOPEN_METHOD_MANUAL; $issubmission = !empty($submission); $isunlimited = $this->get_instance()->maxattempts == ASSIGN_UNLIMITED_ATTEMPTS; $islessthanmaxattempts = $issubmission && ($submission->attemptnumber < ($this->get_instance()->maxattempts-1)); if ($ismanual && (!$issubmission || $isunlimited || $islessthanmaxattempts)) { $mform->addElement('selectyesno', 'addattempt', get_string('addattempt', 'assign')); $mform->setDefault('addattempt', 0); } } if (!$gradingpanel) { $mform->addElement('selectyesno', 'sendstudentnotifications', get_string('sendstudentnotifications', 'assign')); } else { $mform->addElement('hidden', 'sendstudentnotifications', get_string('sendstudentnotifications', 'assign')); $mform->setType('sendstudentnotifications', PARAM_BOOL); } // Get assignment visibility information for student. $modinfo = get_fast_modinfo($settings->course, $userid); $cm = $modinfo->get_cm($this->get_course_module()->id); // Don't allow notification to be sent if the student can't access the assignment, // or until in "Released" state if using marking workflow. if (!$cm->uservisible) { $mform->setDefault('sendstudentnotifications', 0); $mform->freeze('sendstudentnotifications'); } else if ($this->get_instance()->markingworkflow) { $mform->setDefault('sendstudentnotifications', 0); if (!$gradingpanel) { $mform->disabledIf('sendstudentnotifications', 'workflowstate', 'neq', ASSIGN_MARKING_WORKFLOW_STATE_RELEASED); } } else { $mform->setDefault('sendstudentnotifications', $this->get_instance()->sendstudentnotifications); } $mform->addElement('hidden', 'action', 'submitgrade'); $mform->setType('action', PARAM_ALPHA); if (!$gradingpanel) { $buttonarray = array(); $name = get_string('savechanges', 'assign'); $buttonarray[] = $mform->createElement('submit', 'savegrade', $name); if (!$last) { $name = get_string('savenext', 'assign'); $buttonarray[] = $mform->createElement('submit', 'saveandshownext', $name); } $buttonarray[] = $mform->createElement('cancel', 'cancelbutton', get_string('cancel')); $mform->addGroup($buttonarray, 'buttonar', '', array(' '), false); $mform->closeHeaderBefore('buttonar'); $buttonarray = array(); if ($rownum > 0) { $name = get_string('previous', 'assign'); $buttonarray[] = $mform->createElement('submit', 'nosaveandprevious', $name); } if (!$last) { $name = get_string('nosavebutnext', 'assign'); $buttonarray[] = $mform->createElement('submit', 'nosaveandnext', $name); } if (!empty($buttonarray)) { $mform->addGroup($buttonarray, 'navar', '', array(' '), false); } } // The grading form does not work well with shortforms. $mform->setDisableShortforms(); } /** * Add elements in submission plugin form. * * @param mixed $submission stdClass|null * @param MoodleQuickForm $mform * @param stdClass $data * @param int $userid The current userid (same as $USER->id) * @return void */ protected function add_plugin_submission_elements($submission, MoodleQuickForm $mform, stdClass $data, $userid) { foreach ($this->submissionplugins as $plugin) { if ($plugin->is_enabled() && $plugin->is_visible() && $plugin->allow_submissions()) { $plugin->get_form_elements_for_user($submission, $mform, $data, $userid); } } } /** * Check if feedback plugins installed are enabled. * * @return bool */ public function is_any_feedback_plugin_enabled() { if (!isset($this->cache['any_feedback_plugin_enabled'])) { $this->cache['any_feedback_plugin_enabled'] = false; foreach ($this->feedbackplugins as $plugin) { if ($plugin->is_enabled() && $plugin->is_visible()) { $this->cache['any_feedback_plugin_enabled'] = true; break; } } } return $this->cache['any_feedback_plugin_enabled']; } /** * Check if submission plugins installed are enabled. * * @return bool */ public function is_any_submission_plugin_enabled() { if (!isset($this->cache['any_submission_plugin_enabled'])) { $this->cache['any_submission_plugin_enabled'] = false; foreach ($this->submissionplugins as $plugin) { if ($plugin->is_enabled() && $plugin->is_visible() && $plugin->allow_submissions()) { $this->cache['any_submission_plugin_enabled'] = true; break; } } } return $this->cache['any_submission_plugin_enabled']; } /** * Add elements to submission form. * @param MoodleQuickForm $mform * @param stdClass $data * @return void */ public function add_submission_form_elements(MoodleQuickForm $mform, stdClass $data) { global $USER; $userid = $data->userid; // Team submissions. if ($this->get_instance()->teamsubmission) { $submission = $this->get_group_submission($userid, 0, false); } else { $submission = $this->get_user_submission($userid, false); } // Submission statement. $adminconfig = $this->get_admin_config(); $requiresubmissionstatement = $this->get_instance()->requiresubmissionstatement; $draftsenabled = $this->get_instance()->submissiondrafts; $submissionstatement = ''; if ($requiresubmissionstatement) { $submissionstatement = $this->get_submissionstatement($adminconfig, $this->get_instance(), $this->get_context()); } // If we get back an empty submission statement, we have to set $requiredsubmisisonstatement to false to prevent // that the submission statement checkbox will be displayed. if (empty($submissionstatement)) { $requiresubmissionstatement = false; } $mform->addElement('header', 'submission header', get_string('addsubmission', 'mod_assign')); // Only show submission statement if we are editing our own submission. if ($requiresubmissionstatement && !$draftsenabled && $userid == $USER->id) { $mform->addElement('checkbox', 'submissionstatement', '', $submissionstatement); $mform->addRule('submissionstatement', get_string('submissionstatementrequired', 'mod_assign'), 'required', null, 'client'); } $this->add_plugin_submission_elements($submission, $mform, $data, $userid); // Hidden params. $mform->addElement('hidden', 'id', $this->get_course_module()->id); $mform->setType('id', PARAM_INT); $mform->addElement('hidden', 'userid', $userid); $mform->setType('userid', PARAM_INT); $mform->addElement('hidden', 'action', 'savesubmission'); $mform->setType('action', PARAM_ALPHA); } /** * Remove any data from the current submission. * * @param int $userid * @return boolean * @throws coding_exception */ public function remove_submission($userid) { global $USER; if (!$this->can_edit_submission($userid, $USER->id)) { $user = core_user::get_user($userid); $message = get_string('usersubmissioncannotberemoved', 'assign', fullname($user)); $this->set_error_message($message); return false; } if ($this->get_instance()->teamsubmission) { $submission = $this->get_group_submission($userid, 0, false); } else { $submission = $this->get_user_submission($userid, false); } if (!$submission) { return false; } $submission->status = $submission->attemptnumber ? ASSIGN_SUBMISSION_STATUS_REOPENED : ASSIGN_SUBMISSION_STATUS_NEW; $this->update_submission($submission, $userid, false, $this->get_instance()->teamsubmission); // Tell each submission plugin we were saved with no data. $plugins = $this->get_submission_plugins(); foreach ($plugins as $plugin) { if ($plugin->is_enabled() && $plugin->is_visible()) { $plugin->remove($submission); } } $completion = new completion_info($this->get_course()); if ($completion->is_enabled($this->get_course_module()) && $this->get_instance()->completionsubmit) { $completion->update_state($this->get_course_module(), COMPLETION_INCOMPLETE, $userid); } submission_removed::create_from_submission($this, $submission)->trigger(); submission_status_updated::create_from_submission($this, $submission)->trigger(); return true; } /** * Revert to draft. * * @param int $userid * @return boolean */ public function revert_to_draft($userid) { global $DB, $USER; // Need grade permission. require_capability('mod/assign:grade', $this->context); if ($this->get_instance()->teamsubmission) { $submission = $this->get_group_submission($userid, 0, false); } else { $submission = $this->get_user_submission($userid, false); } if (!$submission) { return false; } $submission->status = ASSIGN_SUBMISSION_STATUS_DRAFT; $this->update_submission($submission, $userid, false, $this->get_instance()->teamsubmission); // Give each submission plugin a chance to process the reverting to draft. $plugins = $this->get_submission_plugins(); foreach ($plugins as $plugin) { if ($plugin->is_enabled() && $plugin->is_visible()) { $plugin->revert_to_draft($submission); } } // Update the modified time on the grade (grader modified). $grade = $this->get_user_grade($userid, true); $grade->grader = $USER->id; $this->update_grade($grade); $completion = new completion_info($this->get_course()); if ($completion->is_enabled($this->get_course_module()) && $this->get_instance()->completionsubmit) { $completion->update_state($this->get_course_module(), COMPLETION_INCOMPLETE, $userid); } \mod_assign\event\submission_status_updated::create_from_submission($this, $submission)->trigger(); return true; } /** * Remove the current submission. * * @param int $userid * @return boolean */ protected function process_remove_submission($userid = 0) { require_sesskey(); if (!$userid) { if ($this->is_blind_marking()) { $blindid = optional_param('blindid', 0, PARAM_INT); $userid = $this->get_user_id_for_uniqueid($blindid); } // Userid is required, if not found by blindid. if (!$userid) { $userid = required_param('userid', PARAM_INT); } } return $this->remove_submission($userid); } /** * Revert to draft. * Uses url parameter userid if userid not supplied as a parameter. * * @param int $userid * @return boolean */ protected function process_revert_to_draft($userid = 0) { require_sesskey(); if (!$userid) { if ($this->is_blind_marking()) { $blindid = optional_param('blindid', 0, PARAM_INT); if ($blindid) { $userid = $this->get_user_id_for_uniqueid($blindid); } } // Userid is required, if not found by blindid. if (!$userid) { $userid = required_param('userid', PARAM_INT); } } return $this->revert_to_draft($userid); } /** * Prevent student updates to this submission * * @param int $userid * @return bool */ public function lock_submission($userid) { global $USER, $DB; // Need grade permission. require_capability('mod/assign:grade', $this->context); // Give each submission plugin a chance to process the locking. $plugins = $this->get_submission_plugins(); $submission = $this->get_user_submission($userid, false); $flags = $this->get_user_flags($userid, true); $flags->locked = 1; $this->update_user_flags($flags); foreach ($plugins as $plugin) { if ($plugin->is_enabled() && $plugin->is_visible()) { $plugin->lock($submission, $flags); } } $user = $DB->get_record('user', array('id' => $userid), '*', MUST_EXIST); \mod_assign\event\submission_locked::create_from_user($this, $user)->trigger(); return true; } /** * Set the workflow state for multiple users * * @return void */ protected function process_set_batch_marking_workflow_state() { global $CFG, $DB; // Include batch marking workflow form. require_once($CFG->dirroot . '/mod/assign/batchsetmarkingworkflowstateform.php'); $formparams = array( 'userscount' => 0, // This form is never re-displayed, so we don't need to 'usershtml' => '', // initialise these parameters with real information. 'markingworkflowstates' => $this->get_marking_workflow_states_for_current_user() ); $mform = new mod_assign_batch_set_marking_workflow_state_form(null, $formparams); if ($mform->is_cancelled()) { return true; } if ($formdata = $mform->get_data()) { $useridlist = explode(',', $formdata->selectedusers); $state = $formdata->markingworkflowstate; foreach ($useridlist as $userid) { $flags = $this->get_user_flags($userid, true); $flags->workflowstate = $state; // Clear the mailed flag if notification is requested, the student hasn't been // notified previously, the student can access the assignment, and the state // is "Released". $modinfo = get_fast_modinfo($this->course, $userid); $cm = $modinfo->get_cm($this->get_course_module()->id); if ($formdata->sendstudentnotifications && $cm->uservisible && $state == ASSIGN_MARKING_WORKFLOW_STATE_RELEASED) { $flags->mailed = 0; } $gradingdisabled = $this->grading_disabled($userid); // Will not apply update if user does not have permission to assign this workflow state. if (!$gradingdisabled && $this->update_user_flags($flags)) { // Update Gradebook. $grade = $this->get_user_grade($userid, true); // Fetch any feedback for this student. $gradebookplugin = $this->get_admin_config()->feedback_plugin_for_gradebook; $gradebookplugin = str_replace('assignfeedback_', '', $gradebookplugin); $plugin = $this->get_feedback_plugin_by_type($gradebookplugin); if ($plugin && $plugin->is_enabled() && $plugin->is_visible()) { $grade->feedbacktext = $plugin->text_for_gradebook($grade); $grade->feedbackformat = $plugin->format_for_gradebook($grade); $grade->feedbackfiles = $plugin->files_for_gradebook($grade); } $this->update_grade($grade); $user = $DB->get_record('user', array('id' => $userid), '*', MUST_EXIST); \mod_assign\event\workflow_state_updated::create_from_user($this, $user, $state)->trigger(); } } } } /** * Set the marking allocation for multiple users * * @return void */ protected function process_set_batch_marking_allocation() { global $CFG, $DB; // Include batch marking allocation form. require_once($CFG->dirroot . '/mod/assign/batchsetallocatedmarkerform.php'); $formparams = array( 'userscount' => 0, // This form is never re-displayed, so we don't need to 'usershtml' => '' // initialise these parameters with real information. ); list($sort, $params) = users_order_by_sql('u'); // Only enrolled users could be assigned as potential markers. $markers = get_enrolled_users($this->get_context(), 'mod/assign:grade', 0, 'u.*', $sort); $markerlist = array(); foreach ($markers as $marker) { $markerlist[$marker->id] = fullname($marker); } $formparams['markers'] = $markerlist; $mform = new mod_assign_batch_set_allocatedmarker_form(null, $formparams); if ($mform->is_cancelled()) { return true; } if ($formdata = $mform->get_data()) { $useridlist = explode(',', $formdata->selectedusers); $marker = $DB->get_record('user', array('id' => $formdata->allocatedmarker), '*', MUST_EXIST); foreach ($useridlist as $userid) { $flags = $this->get_user_flags($userid, true); if ($flags->workflowstate == ASSIGN_MARKING_WORKFLOW_STATE_READYFORREVIEW || $flags->workflowstate == ASSIGN_MARKING_WORKFLOW_STATE_INREVIEW || $flags->workflowstate == ASSIGN_MARKING_WORKFLOW_STATE_READYFORRELEASE || $flags->workflowstate == ASSIGN_MARKING_WORKFLOW_STATE_RELEASED) { continue; // Allocated marker can only be changed in certain workflow states. } $flags->allocatedmarker = $marker->id; if ($this->update_user_flags($flags)) { $user = $DB->get_record('user', array('id' => $userid), '*', MUST_EXIST); \mod_assign\event\marker_updated::create_from_marker($this, $user, $marker)->trigger(); } } } } /** * Prevent student updates to this submission. * Uses url parameter userid. * * @param int $userid * @return void */ protected function process_lock_submission($userid = 0) { require_sesskey(); if (!$userid) { if ($this->is_blind_marking()) { $blindid = optional_param('blindid', 0, PARAM_INT); if ($blindid) { $userid = $this->get_user_id_for_uniqueid($blindid); } } // Userid is required, if not found by blindid. if (!$userid) { $userid = required_param('userid', PARAM_INT); } } return $this->lock_submission($userid); } /** * Unlock the student submission. * * @param int $userid * @return bool */ public function unlock_submission($userid) { global $USER, $DB; // Need grade permission. require_capability('mod/assign:grade', $this->context); // Give each submission plugin a chance to process the unlocking. $plugins = $this->get_submission_plugins(); $submission = $this->get_user_submission($userid, false); $flags = $this->get_user_flags($userid, true); $flags->locked = 0; $this->update_user_flags($flags); foreach ($plugins as $plugin) { if ($plugin->is_enabled() && $plugin->is_visible()) { $plugin->unlock($submission, $flags); } } $user = $DB->get_record('user', array('id' => $userid), '*', MUST_EXIST); \mod_assign\event\submission_unlocked::create_from_user($this, $user)->trigger(); return true; } /** * Unlock the student submission. * Uses url parameter userid. * * @param int $userid * @return bool */ protected function process_unlock_submission($userid = 0) { require_sesskey(); if (!$userid) { if ($this->is_blind_marking()) { $blindid = optional_param('blindid', 0, PARAM_INT); if ($blindid) { $userid = $this->get_user_id_for_uniqueid($blindid); } } // Userid is required, if not found by blindid. if (!$userid) { $userid = required_param('userid', PARAM_INT); } } return $this->unlock_submission($userid); } /** * Apply a grade from a grading form to a user (may be called multiple times for a group submission). * * @param stdClass $formdata - the data from the form * @param int $userid - the user to apply the grade to * @param int $attemptnumber - The attempt number to apply the grade to. * @return void */ protected function apply_grade_to_user($formdata, $userid, $attemptnumber) { global $USER, $CFG, $DB; $grade = $this->get_user_grade($userid, true, $attemptnumber); $originalgrade = $grade->grade; $gradingdisabled = $this->grading_disabled($userid); $gradinginstance = $this->get_grading_instance($userid, $grade, $gradingdisabled); if (!$gradingdisabled) { if ($gradinginstance) { $grade->grade = $gradinginstance->submit_and_get_grade($formdata->advancedgrading, $grade->id); } else { // Handle the case when grade is set to No Grade. if (isset($formdata->grade)) { $grade->grade = grade_floatval(unformat_float($formdata->grade)); } } if (isset($formdata->workflowstate) || isset($formdata->allocatedmarker)) { $flags = $this->get_user_flags($userid, true); $oldworkflowstate = $flags->workflowstate; $flags->workflowstate = isset($formdata->workflowstate) ? $formdata->workflowstate : $flags->workflowstate; $flags->allocatedmarker = isset($formdata->allocatedmarker) ? $formdata->allocatedmarker : $flags->allocatedmarker; if ($this->update_user_flags($flags) && isset($formdata->workflowstate) && $formdata->workflowstate !== $oldworkflowstate) { $user = $DB->get_record('user', array('id' => $userid), '*', MUST_EXIST); \mod_assign\event\workflow_state_updated::create_from_user($this, $user, $formdata->workflowstate)->trigger(); } } } $grade->grader= $USER->id; $adminconfig = $this->get_admin_config(); $gradebookplugin = $adminconfig->feedback_plugin_for_gradebook; $feedbackmodified = false; // Call save in plugins. foreach ($this->feedbackplugins as $plugin) { if ($plugin->is_enabled() && $plugin->is_visible()) { $gradingmodified = $plugin->is_feedback_modified($grade, $formdata); if ($gradingmodified) { if (!$plugin->save($grade, $formdata)) { $result = false; throw new \moodle_exception($plugin->get_error()); } // If $feedbackmodified is true, keep it true. $feedbackmodified = $feedbackmodified || $gradingmodified; } if (('assignfeedback_' . $plugin->get_type()) == $gradebookplugin) { // This is the feedback plugin chose to push comments to the gradebook. $grade->feedbacktext = $plugin->text_for_gradebook($grade); $grade->feedbackformat = $plugin->format_for_gradebook($grade); $grade->feedbackfiles = $plugin->files_for_gradebook($grade); } } } // We do not want to update the timemodified if no grade was added. if (!empty($formdata->addattempt) || ($originalgrade !== null && $originalgrade != -1) || ($grade->grade !== null && $grade->grade != -1) || $feedbackmodified) { $this->update_grade($grade, !empty($formdata->addattempt)); } // We never send notifications if we have marking workflow and the grade is not released. if ($this->get_instance()->markingworkflow && isset($formdata->workflowstate) && $formdata->workflowstate != ASSIGN_MARKING_WORKFLOW_STATE_RELEASED) { $formdata->sendstudentnotifications = false; } // Note the default if not provided for this option is true (e.g. webservices). // This is for backwards compatibility. if (!isset($formdata->sendstudentnotifications) || $formdata->sendstudentnotifications) { $this->notify_grade_modified($grade, true); } } /** * Save outcomes submitted from grading form. * * @param int $userid * @param stdClass $formdata * @param int $sourceuserid The user ID under which the outcome data is accessible. This is relevant * for an outcome set to a user but applied to an entire group. */ protected function process_outcomes($userid, $formdata, $sourceuserid = null) { global $CFG, $USER; if (empty($CFG->enableoutcomes)) { return; } if ($this->grading_disabled($userid)) { return; } require_once($CFG->libdir.'/gradelib.php'); $data = array(); $gradinginfo = grade_get_grades($this->get_course()->id, 'mod', 'assign', $this->get_instance()->id, $userid); if (!empty($gradinginfo->outcomes)) { foreach ($gradinginfo->outcomes as $index => $oldoutcome) { $name = 'outcome_'.$index; $sourceuserid = $sourceuserid !== null ? $sourceuserid : $userid; if (isset($formdata->{$name}[$sourceuserid]) && $oldoutcome->grades[$userid]->grade != $formdata->{$name}[$sourceuserid]) { $data[$index] = $formdata->{$name}[$sourceuserid]; } } } if (count($data) > 0) { grade_update_outcomes('mod/assign', $this->course->id, 'mod', 'assign', $this->get_instance()->id, $userid, $data); } } /** * If the requirements are met - reopen the submission for another attempt. * Only call this function when grading the latest attempt. * * @param int $userid The userid. * @param stdClass $submission The submission (may be a group submission). * @param bool $addattempt - True if the "allow another attempt" checkbox was checked. * @return bool - true if another attempt was added. */ protected function reopen_submission_if_required($userid, $submission, $addattempt) { $instance = $this->get_instance(); $maxattemptsreached = !empty($submission) && $submission->attemptnumber >= ($instance->maxattempts - 1) && $instance->maxattempts != ASSIGN_UNLIMITED_ATTEMPTS; $shouldreopen = false; switch ($instance->attemptreopenmethod) { case ASSIGN_ATTEMPT_REOPEN_METHOD_AUTOMATIC: $shouldreopen = true; break; case ASSIGN_ATTEMPT_REOPEN_METHOD_UNTILPASS: if (!is_gradable($this->course->id, 'mod', 'assign', $this->get_instance()->id)) { return false; } // Check the gradetopass from the gradebook. $gradeitem = $this->get_grade_item(); if ($gradeitem) { $gradegrade = grade_grade::fetch(['userid' => $userid, 'itemid' => $gradeitem->id]); // Do not reopen if is_passed returns null, e.g. if there is no pass criterion set. if ($gradegrade && ($gradegrade->is_passed() === false)) { $shouldreopen = true; } } break; case ASSIGN_ATTEMPT_REOPEN_METHOD_MANUAL: if (!empty($addattempt)) { $shouldreopen = true; } break; } if ($shouldreopen && !$maxattemptsreached) { $this->add_attempt($userid); return true; } return false; } /** * Save grade update. * * @param int $userid * @param stdClass $data * @return bool - was the grade saved */ public function save_grade($userid, $data) { // Need grade permission. require_capability('mod/assign:grade', $this->context); $instance = $this->get_instance(); $submission = null; if ($instance->teamsubmission) { // We need to know what the most recent group submission is. // Specifically when determining if we are adding another attempt (we only want to add one attempt per team), // and when deciding if we need to update the gradebook with an edited grade. $mostrecentsubmission = $this->get_group_submission($userid, 0, false, -1); $this->set_most_recent_team_submission($mostrecentsubmission); // Get the submission that we are saving grades for. The data attempt number determines which submission attempt. $submission = $this->get_group_submission($userid, 0, false, $data->attemptnumber); } else { $submission = $this->get_user_submission($userid, false, $data->attemptnumber); } if ($instance->teamsubmission && !empty($data->applytoall)) { $groupid = 0; if ($this->get_submission_group($userid)) { $group = $this->get_submission_group($userid); if ($group) { $groupid = $group->id; } } $members = $this->get_submission_group_members($groupid, true, $this->show_only_active_users()); foreach ($members as $member) { // We only want to update the grade for this group submission attempt. The data attempt number could be // -1 which may end up in additional attempts being created for each group member instead of just one // additional attempt for the group. $this->apply_grade_to_user($data, $member->id, $submission->attemptnumber); $this->process_outcomes($member->id, $data, $userid); } } else { $this->apply_grade_to_user($data, $userid, $data->attemptnumber); $this->process_outcomes($userid, $data); } return true; } /** * Save grade. * * @param moodleform $mform * @return bool - was the grade saved */ protected function process_save_grade(&$mform) { global $CFG, $SESSION; // Include grade form. require_once($CFG->dirroot . '/mod/assign/gradeform.php'); require_sesskey(); $instance = $this->get_instance(); $rownum = required_param('rownum', PARAM_INT); $attemptnumber = optional_param('attemptnumber', -1, PARAM_INT); $useridlistid = optional_param('useridlistid', $this->get_useridlist_key_id(), PARAM_ALPHANUM); $userid = optional_param('userid', 0, PARAM_INT); if (!$userid) { if (empty($SESSION->mod_assign_useridlist[$this->get_useridlist_key($useridlistid)])) { // If the userid list is not stored we must not save, as it is possible that the user in a // given row position may not be the same now as when the grading page was generated. $url = new moodle_url('/mod/assign/view.php', array('id' => $this->get_course_module()->id)); throw new moodle_exception('useridlistnotcached', 'mod_assign', $url); } $useridlist = $SESSION->mod_assign_useridlist[$this->get_useridlist_key($useridlistid)]; } else { $useridlist = array($userid); $rownum = 0; } $last = false; $userid = $useridlist[$rownum]; if ($rownum == count($useridlist) - 1) { $last = true; } $data = new stdClass(); $gradeformparams = array('rownum' => $rownum, 'useridlistid' => $useridlistid, 'last' => $last, 'attemptnumber' => $attemptnumber, 'userid' => $userid); $mform = new mod_assign_grade_form(null, array($this, $data, $gradeformparams), 'post', '', array('class'=>'gradeform')); if ($formdata = $mform->get_data()) { return $this->save_grade($userid, $formdata); } else { return false; } } /** * This function is a static wrapper around can_upgrade. * * @param string $type The plugin type * @param int $version The plugin version * @return bool */ public static function can_upgrade_assignment($type, $version) { $assignment = new assign(null, null, null); return $assignment->can_upgrade($type, $version); } /** * This function returns true if it can upgrade an assignment from the 2.2 module. * * @param string $type The plugin type * @param int $version The plugin version * @return bool */ public function can_upgrade($type, $version) { if ($type == 'offline' && $version >= 2011112900) { return true; } foreach ($this->submissionplugins as $plugin) { if ($plugin->can_upgrade($type, $version)) { return true; } } foreach ($this->feedbackplugins as $plugin) { if ($plugin->can_upgrade($type, $version)) { return true; } } return false; } /** * Copy all the files from the old assignment files area to the new one. * This is used by the plugin upgrade code. * * @param int $oldcontextid The old assignment context id * @param int $oldcomponent The old assignment component ('assignment') * @param int $oldfilearea The old assignment filearea ('submissions') * @param int $olditemid The old submissionid (can be null e.g. intro) * @param int $newcontextid The new assignment context id * @param int $newcomponent The new assignment component ('assignment') * @param int $newfilearea The new assignment filearea ('submissions') * @param int $newitemid The new submissionid (can be null e.g. intro) * @return int The number of files copied */ public function copy_area_files_for_upgrade($oldcontextid, $oldcomponent, $oldfilearea, $olditemid, $newcontextid, $newcomponent, $newfilearea, $newitemid) { // Note, this code is based on some code in filestorage - but that code // deleted the old files (which we don't want). $count = 0; $fs = get_file_storage(); $oldfiles = $fs->get_area_files($oldcontextid, $oldcomponent, $oldfilearea, $olditemid, 'id', false); foreach ($oldfiles as $oldfile) { $filerecord = new stdClass(); $filerecord->contextid = $newcontextid; $filerecord->component = $newcomponent; $filerecord->filearea = $newfilearea; $filerecord->itemid = $newitemid; $fs->create_file_from_storedfile($filerecord, $oldfile); $count += 1; } return $count; } /** * Add a new attempt for each user in the list - but reopen each group assignment * at most 1 time. * * @param array $useridlist Array of userids to reopen. * @return bool */ protected function process_add_attempt_group($useridlist) { $groupsprocessed = array(); $result = true; foreach ($useridlist as $userid) { $groupid = 0; $group = $this->get_submission_group($userid); if ($group) { $groupid = $group->id; } if (empty($groupsprocessed[$groupid])) { // We need to know what the most recent group submission is. // Specifically when determining if we are adding another attempt (we only want to add one attempt per team), // and when deciding if we need to update the gradebook with an edited grade. $currentsubmission = $this->get_group_submission($userid, 0, false, -1); $this->set_most_recent_team_submission($currentsubmission); $result = $this->process_add_attempt($userid) && $result; $groupsprocessed[$groupid] = true; } } return $result; } /** * Check for a sess key and then call add_attempt. * * @param int $userid int The user to add the attempt for * @return bool - true if successful. */ protected function process_add_attempt($userid) { require_sesskey(); return $this->add_attempt($userid); } /** * Add a new attempt for a user. * * @param int $userid int The user to add the attempt for * @return bool - true if successful. */ protected function add_attempt($userid) { require_capability('mod/assign:grade', $this->context); // If additional attempts are disallowed. if ($this->get_instance()->maxattempts == 1) { return false; } if ($this->get_instance()->teamsubmission) { $oldsubmission = $this->get_group_submission($userid, 0, false); } else { $oldsubmission = $this->get_user_submission($userid, false); } if (!$oldsubmission) { return false; } // No more than max attempts allowed. if ($this->get_instance()->maxattempts != ASSIGN_UNLIMITED_ATTEMPTS && $oldsubmission->attemptnumber >= ($this->get_instance()->maxattempts - 1)) { return false; } // Create the new submission record for the group/user. if ($this->get_instance()->teamsubmission) { if (isset($this->mostrecentteamsubmission)) { // Team submissions can end up in this function for each user (via save_grade). We don't want to create // more than one attempt for the whole team. if ($this->mostrecentteamsubmission->attemptnumber == $oldsubmission->attemptnumber) { $newsubmission = $this->get_group_submission($userid, 0, true, $oldsubmission->attemptnumber + 1); } else { $newsubmission = $this->get_group_submission($userid, 0, false, $oldsubmission->attemptnumber); } } else { debugging('Please use set_most_recent_team_submission() before calling add_attempt', DEBUG_DEVELOPER); $newsubmission = $this->get_group_submission($userid, 0, true, $oldsubmission->attemptnumber + 1); } } else { $newsubmission = $this->get_user_submission($userid, true, $oldsubmission->attemptnumber + 1); } // Set the status of the new attempt to reopened. $newsubmission->status = ASSIGN_SUBMISSION_STATUS_REOPENED; // Give each submission plugin a chance to process the add_attempt. $plugins = $this->get_submission_plugins(); foreach ($plugins as $plugin) { if ($plugin->is_enabled() && $plugin->is_visible()) { $plugin->add_attempt($oldsubmission, $newsubmission); } } $this->update_submission($newsubmission, $userid, false, $this->get_instance()->teamsubmission); $flags = $this->get_user_flags($userid, false); if (isset($flags->locked) && $flags->locked) { // May not exist. $this->process_unlock_submission($userid); } return true; } /** * Get an upto date list of user grades and feedback for the gradebook. * * @param int $userid int or 0 for all users * @return array of grade data formated for the gradebook api * The data required by the gradebook api is userid, * rawgrade, * feedback, * feedbackformat, * usermodified, * dategraded, * datesubmitted */ public function get_user_grades_for_gradebook($userid) { global $DB, $CFG; $grades = array(); $assignmentid = $this->get_instance()->id; $adminconfig = $this->get_admin_config(); $gradebookpluginname = $adminconfig->feedback_plugin_for_gradebook; $gradebookplugin = null; // Find the gradebook plugin. foreach ($this->feedbackplugins as $plugin) { if ($plugin->is_enabled() && $plugin->is_visible()) { if (('assignfeedback_' . $plugin->get_type()) == $gradebookpluginname) { $gradebookplugin = $plugin; } } } if ($userid) { $where = ' WHERE u.id = :userid '; } else { $where = ' WHERE u.id != :userid '; } // When the gradebook asks us for grades - only return the last attempt for each user. $params = array('assignid1'=>$assignmentid, 'assignid2'=>$assignmentid, 'userid'=>$userid); $graderesults = $DB->get_recordset_sql('SELECT u.id as userid, s.timemodified as datesubmitted, g.grade as rawgrade, g.timemodified as dategraded, g.grader as usermodified FROM {user} u LEFT JOIN {assign_submission} s ON u.id = s.userid and s.assignment = :assignid1 AND s.latest = 1 JOIN {assign_grades} g ON u.id = g.userid and g.assignment = :assignid2 AND g.attemptnumber = s.attemptnumber' . $where, $params); foreach ($graderesults as $result) { $gradingstatus = $this->get_grading_status($result->userid); if (!$this->get_instance()->markingworkflow || $gradingstatus == ASSIGN_MARKING_WORKFLOW_STATE_RELEASED) { $gradebookgrade = clone $result; // Now get the feedback. if ($gradebookplugin) { $grade = $this->get_user_grade($result->userid, false); if ($grade) { $feedbacktext = $gradebookplugin->text_for_gradebook($grade); if (!empty($feedbacktext)) { $gradebookgrade->feedback = $feedbacktext; } $gradebookgrade->feedbackformat = $gradebookplugin->format_for_gradebook($grade); $gradebookgrade->feedbackfiles = $gradebookplugin->files_for_gradebook($grade); } } $grades[$gradebookgrade->userid] = $gradebookgrade; } } $graderesults->close(); return $grades; } /** * Call the static version of this function * * @param int $userid The userid to lookup * @return int The unique id */ public function get_uniqueid_for_user($userid) { return self::get_uniqueid_for_user_static($this->get_instance()->id, $userid); } /** * Foreach participant in the course - assign them a random id. * * @param int $assignid The assignid to lookup */ public static function allocate_unique_ids($assignid) { global $DB; $cm = get_coursemodule_from_instance('assign', $assignid, 0, false, MUST_EXIST); $context = context_module::instance($cm->id); $currentgroup = groups_get_activity_group($cm, true); $users = get_enrolled_users($context, "mod/assign:submit", $currentgroup, 'u.id'); // Shuffle the users. shuffle($users); foreach ($users as $user) { $record = $DB->get_record('assign_user_mapping', array('assignment'=>$assignid, 'userid'=>$user->id), 'id'); if (!$record) { $record = new stdClass(); $record->assignment = $assignid; $record->userid = $user->id; $DB->insert_record('assign_user_mapping', $record); } } } /** * Lookup this user id and return the unique id for this assignment. * * @param int $assignid The assignment id * @param int $userid The userid to lookup * @return int The unique id */ public static function get_uniqueid_for_user_static($assignid, $userid) { global $DB; // Search for a record. $params = array('assignment'=>$assignid, 'userid'=>$userid); if ($record = $DB->get_record('assign_user_mapping', $params, 'id')) { return $record->id; } // Be a little smart about this - there is no record for the current user. // We should ensure any unallocated ids for the current participant // list are distrubited randomly. self::allocate_unique_ids($assignid); // Retry the search for a record. if ($record = $DB->get_record('assign_user_mapping', $params, 'id')) { return $record->id; } // The requested user must not be a participant. Add a record anyway. $record = new stdClass(); $record->assignment = $assignid; $record->userid = $userid; return $DB->insert_record('assign_user_mapping', $record); } /** * Call the static version of this function. * * @param int $uniqueid The uniqueid to lookup * @return int The user id or false if they don't exist */ public function get_user_id_for_uniqueid($uniqueid) { return self::get_user_id_for_uniqueid_static($this->get_instance()->id, $uniqueid); } /** * Lookup this unique id and return the user id for this assignment. * * @param int $assignid The id of the assignment this user mapping is in * @param int $uniqueid The uniqueid to lookup * @return int The user id or false if they don't exist */ public static function get_user_id_for_uniqueid_static($assignid, $uniqueid) { global $DB; // Search for a record. if ($record = $DB->get_record('assign_user_mapping', array('assignment'=>$assignid, 'id'=>$uniqueid), 'userid', IGNORE_MISSING)) { return $record->userid; } return false; } /** * Get the list of marking_workflow states the current user has permission to transition a grade to. * * @return array of state => description */ public function get_marking_workflow_states_for_current_user() { if (!empty($this->markingworkflowstates)) { return $this->markingworkflowstates; } $states = array(); if (has_capability('mod/assign:grade', $this->context)) { $states[ASSIGN_MARKING_WORKFLOW_STATE_INMARKING] = get_string('markingworkflowstateinmarking', 'assign'); $states[ASSIGN_MARKING_WORKFLOW_STATE_READYFORREVIEW] = get_string('markingworkflowstatereadyforreview', 'assign'); } if (has_any_capability(array('mod/assign:reviewgrades', 'mod/assign:managegrades'), $this->context)) { $states[ASSIGN_MARKING_WORKFLOW_STATE_INREVIEW] = get_string('markingworkflowstateinreview', 'assign'); $states[ASSIGN_MARKING_WORKFLOW_STATE_READYFORRELEASE] = get_string('markingworkflowstatereadyforrelease', 'assign'); } if (has_any_capability(array('mod/assign:releasegrades', 'mod/assign:managegrades'), $this->context)) { $states[ASSIGN_MARKING_WORKFLOW_STATE_RELEASED] = get_string('markingworkflowstatereleased', 'assign'); } $this->markingworkflowstates = $states; return $this->markingworkflowstates; } /** * Get the list of marking_workflow states. * * @return array Array of multiple state => description. */ public function get_all_marking_workflow_states(): array { if (!empty($this->allmarkingworkflowstates)) { return $this->allmarkingworkflowstates; } $this->allmarkingworkflowstates = [ ASSIGN_MARKING_WORKFLOW_STATE_NOTMARKED => get_string('markingworkflowstatenotmarked', 'assign'), ASSIGN_MARKING_WORKFLOW_STATE_INMARKING => get_string('markingworkflowstateinmarking', 'assign'), ASSIGN_MARKING_WORKFLOW_STATE_READYFORREVIEW => get_string('markingworkflowstatereadyforreview', 'assign'), ASSIGN_MARKING_WORKFLOW_STATE_INREVIEW => get_string('markingworkflowstateinreview', 'assign'), ASSIGN_MARKING_WORKFLOW_STATE_READYFORRELEASE => get_string('markingworkflowstatereadyforrelease', 'assign'), ASSIGN_MARKING_WORKFLOW_STATE_RELEASED => get_string('markingworkflowstatereleased', 'assign'), ]; return $this->allmarkingworkflowstates; } /** * Check is only active users in course should be shown. * * @return bool true if only active users should be shown. */ public function show_only_active_users() { global $CFG; if (is_null($this->showonlyactiveenrol)) { $defaultgradeshowactiveenrol = !empty($CFG->grade_report_showonlyactiveenrol); $this->showonlyactiveenrol = get_user_preferences('grade_report_showonlyactiveenrol', $defaultgradeshowactiveenrol); if (!is_null($this->context)) { $this->showonlyactiveenrol = $this->showonlyactiveenrol || !has_capability('moodle/course:viewsuspendedusers', $this->context); } } return $this->showonlyactiveenrol; } /** * Return true is user is active user in course else false * * @param int $userid * @return bool true is user is active in course. */ public function is_active_user($userid) { return !in_array($userid, get_suspended_userids($this->context, true)); } /** * Returns true if gradebook feedback plugin is enabled * * @return bool true if gradebook feedback plugin is enabled and visible else false. */ public function is_gradebook_feedback_enabled() { // Get default grade book feedback plugin. $adminconfig = $this->get_admin_config(); $gradebookplugin = $adminconfig->feedback_plugin_for_gradebook; $gradebookplugin = str_replace('assignfeedback_', '', $gradebookplugin); // Check if default gradebook feedback is visible and enabled. $gradebookfeedbackplugin = $this->get_feedback_plugin_by_type($gradebookplugin); if (empty($gradebookfeedbackplugin)) { return false; } if ($gradebookfeedbackplugin->is_visible() && $gradebookfeedbackplugin->is_enabled()) { return true; } // Gradebook feedback plugin is either not visible/enabled. return false; } /** * Returns the grading status. * * @param int $userid the user id * @return string returns the grading status */ public function get_grading_status($userid) { if ($this->get_instance()->markingworkflow) { $flags = $this->get_user_flags($userid, false); if (!empty($flags->workflowstate)) { return $flags->workflowstate; } return ASSIGN_MARKING_WORKFLOW_STATE_NOTMARKED; } else { $attemptnumber = optional_param('attemptnumber', -1, PARAM_INT); $grade = $this->get_user_grade($userid, false, $attemptnumber); if (!empty($grade) && $grade->grade !== null && $grade->grade >= 0) { return ASSIGN_GRADING_STATUS_GRADED; } else { return ASSIGN_GRADING_STATUS_NOT_GRADED; } } } /** * The id used to uniquily identify the cache for this instance of the assign object. * * @return string */ public function get_useridlist_key_id() { return $this->useridlistid; } /** * Generates the key that should be used for an entry in the useridlist cache. * * @param string $id Generate a key for this instance (optional) * @return string The key for the id, or new entry if no $id is passed. */ public function get_useridlist_key($id = null) { global $SESSION; // Ensure the user id list cache is initialised. if (!isset($SESSION->mod_assign_useridlist)) { $SESSION->mod_assign_useridlist = []; } if ($id === null) { $id = $this->get_useridlist_key_id(); } return $this->get_course_module()->id . '_' . $id; } /** * Updates and creates the completion records in mdl_course_modules_completion. * * @param int $teamsubmission value of 0 or 1 to indicate whether this is a group activity * @param int $requireallteammemberssubmit value of 0 or 1 to indicate whether all group members must click Submit * @param obj $submission the submission * @param int $userid the user id * @param int $complete * @param obj $completion * * @return null */ protected function update_activity_completion_records($teamsubmission, $requireallteammemberssubmit, $submission, $userid, $complete, $completion) { if (($teamsubmission && $submission->groupid > 0 && !$requireallteammemberssubmit) || ($teamsubmission && $submission->groupid > 0 && $requireallteammemberssubmit && $submission->status == ASSIGN_SUBMISSION_STATUS_SUBMITTED)) { $members = groups_get_members($submission->groupid); foreach ($members as $member) { $completion->update_state($this->get_course_module(), $complete, $member->id); } } else { $completion->update_state($this->get_course_module(), $complete, $userid); } return; } /** * Update the module completion status (set it viewed) and trigger module viewed event. * * @since Moodle 3.2 */ public function set_module_viewed() { $completion = new completion_info($this->get_course()); $completion->set_module_viewed($this->get_course_module()); // Trigger the course module viewed event. $assigninstance = $this->get_instance(); $params = [ 'objectid' => $assigninstance->id, 'context' => $this->get_context() ]; if ($this->is_blind_marking()) { $params['anonymous'] = 1; } $event = \mod_assign\event\course_module_viewed::create($params); $event->add_record_snapshot('assign', $assigninstance); $event->trigger(); } /** * Checks for any grade notices, and adds notifications. Will display on assignment main page and grading table. * * @return void The notifications API will render the notifications at the appropriate part of the page. */ protected function add_grade_notices() { if (has_capability('mod/assign:grade', $this->get_context()) && get_config('assign', 'has_rescaled_null_grades_' . $this->get_instance()->id)) { $link = new \moodle_url('/mod/assign/view.php', array('id' => $this->get_course_module()->id, 'action' => 'fixrescalednullgrades')); \core\notification::warning(get_string('fixrescalednullgrades', 'mod_assign', ['link' => $link->out()])); } } /** * View fix rescaled null grades. * * @return bool True if null all grades are now fixed. */ protected function fix_null_grades() { global $DB; $result = $DB->set_field_select( 'assign_grades', 'grade', ASSIGN_GRADE_NOT_SET, 'grade <> ? AND grade < 0', [ASSIGN_GRADE_NOT_SET] ); $assign = clone $this->get_instance(); $assign->cmidnumber = $this->get_course_module()->idnumber; assign_update_grades($assign); return $result; } /** * View fix rescaled null grades. * * @return void The notifications API will render the notifications at the appropriate part of the page. */ protected function view_fix_rescaled_null_grades() { global $OUTPUT; $o = ''; require_capability('mod/assign:grade', $this->get_context()); $instance = $this->get_instance(); $o .= $this->get_renderer()->render( new assign_header( $instance, $this->get_context(), $this->show_intro(), $this->get_course_module()->id ) ); $confirm = optional_param('confirm', 0, PARAM_BOOL); if ($confirm) { if (confirm_sesskey()) { // Fix the grades. $this->fix_null_grades(); unset_config('has_rescaled_null_grades_' . $instance->id, 'assign'); // Display the success notice. $o .= $this->get_renderer()->notification(get_string('fixrescalednullgradesdone', 'assign'), 'notifysuccess'); } else { // If the sesskey is not valid, then display the error notice. $o .= $this->get_renderer()->notification(get_string('invalidsesskey', 'error'), 'notifyerror'); } $url = new moodle_url( url: '/mod/assign/view.php', params: [ 'id' => $this->get_course_module()->id, 'action' => 'grading', ], ); $o .= $this->get_renderer()->continue_button($url); } else { // Ask for confirmation. $continue = new \moodle_url('/mod/assign/view.php', array('id' => $this->get_course_module()->id, 'action' => 'fixrescalednullgrades', 'confirm' => true, 'sesskey' => sesskey())); $cancel = new \moodle_url('/mod/assign/view.php', array('id' => $this->get_course_module()->id)); $o .= $OUTPUT->confirm(get_string('fixrescalednullgradesconfirm', 'mod_assign'), $continue, $cancel); } $o .= $this->view_footer(); return $o; } /** * Set the most recent submission for the team. * The most recent team submission is used to determine if another attempt should be created when allowing another * attempt on a group assignment, and whether the gradebook should be updated. * * @since Moodle 3.4 * @param stdClass $submission The most recent submission of the group. */ public function set_most_recent_team_submission($submission) { $this->mostrecentteamsubmission = $submission; } /** * Return array of valid grading allocation filters for the grading interface. * * @param boolean $export Export the list of filters for a template. * @return array */ public function get_marking_allocation_filters($export = false) { $markingallocation = $this->get_instance()->markingworkflow && $this->get_instance()->markingallocation && has_capability('mod/assign:manageallocations', $this->context); // Get markers to use in drop lists. $markingallocationoptions = array(); if ($markingallocation) { list($sort, $params) = users_order_by_sql('u'); // Only enrolled users could be assigned as potential markers. $markers = get_enrolled_users($this->context, 'mod/assign:grade', 0, 'u.*', $sort); $markingallocationoptions[''] = get_string('filternone', 'assign'); $markingallocationoptions[ASSIGN_MARKER_FILTER_NO_MARKER] = get_string('markerfilternomarker', 'assign'); $viewfullnames = has_capability('moodle/site:viewfullnames', $this->context); foreach ($markers as $marker) { $markingallocationoptions[$marker->id] = fullname($marker, $viewfullnames); } } if ($export) { $allocationfilter = get_user_preferences('assign_markerfilter', ''); $result = []; foreach ($markingallocationoptions as $option => $label) { array_push($result, [ 'key' => $option, 'name' => $label, 'active' => ($allocationfilter == $option), ]); } return $result; } return $markingallocationoptions; } /** * Return array of valid grading workflow filters for the grading interface. * * @param boolean $export Export the list of filters for a template. * @return array */ public function get_marking_workflow_filters($export = false) { $markingworkflow = $this->get_instance()->markingworkflow; // Get marking states to show in form. $markingworkflowoptions = array(); if ($markingworkflow) { $notmarked = get_string('markingworkflowstatenotmarked', 'assign'); $markingworkflowoptions[''] = get_string('filternone', 'assign'); $markingworkflowoptions[ASSIGN_MARKING_WORKFLOW_STATE_NOTMARKED] = $notmarked; $markingworkflowoptions = array_merge($markingworkflowoptions, $this->get_marking_workflow_states_for_current_user()); } if ($export) { $workflowfilter = get_user_preferences('assign_workflowfilter', ''); $result = []; foreach ($markingworkflowoptions as $option => $label) { array_push($result, [ 'key' => $option, 'name' => $label, 'active' => ($workflowfilter == $option), ]); } return $result; } return $markingworkflowoptions; } /** * Return array of valid search filters for the grading interface. * * @param bool $grouped Whether to return the filters grouped or not. * @return array */ public function get_filters(bool $grouped = false): array { $groupedfilterkeys = [ [ ASSIGN_FILTER_NOT_SUBMITTED, ASSIGN_FILTER_DRAFT, ASSIGN_FILTER_SUBMITTED, ASSIGN_FILTER_REQUIRE_GRADING, ASSIGN_FILTER_GRADED, ], [ ASSIGN_FILTER_GRANTED_EXTENSION, ], ]; $current = get_user_preferences('assign_filter', ''); $filters = []; // First is always "no filter" option. $filters[0] = [ [ 'key' => ASSIGN_FILTER_NONE, 'name' => get_string('filterall', 'assign'), 'active' => ($current == ''), ], ]; foreach ($groupedfilterkeys as $group => $filterkeys) { foreach ($filterkeys as $key) { $filters[$group] = $filters[$group] ?? []; $filters[$group][] = [ 'key' => $key, 'name' => get_string('filter' . $key, 'assign'), 'active' => ($current == $key), ]; } } return $grouped ? $filters : array_merge(...$filters); } /** * Get the correct submission statement depending on single submisison, team submission or team submission * where all team memebers must submit. * * @param stdClass $adminconfig * @param stdClass $instance * @param context $context * * @return string */ protected function get_submissionstatement($adminconfig, $instance, $context) { $submissionstatement = ''; if (!($context instanceof context)) { return $submissionstatement; } // Single submission. if (!$instance->teamsubmission) { // Single submission statement is not empty. if (!empty($adminconfig->submissionstatement)) { // Format the submission statement before its sent. We turn off para because this is going within // a form element. $options = array( 'context' => $context, 'para' => false ); $submissionstatement = format_text($adminconfig->submissionstatement, FORMAT_MOODLE, $options); } } else { // Team submission. // One user can submit for the whole team. if (!empty($adminconfig->submissionstatementteamsubmission) && !$instance->requireallteammemberssubmit) { // Format the submission statement before its sent. We turn off para because this is going within // a form element. $options = array( 'context' => $context, 'para' => false ); $submissionstatement = format_text($adminconfig->submissionstatementteamsubmission, FORMAT_MOODLE, $options); } else if (!empty($adminconfig->submissionstatementteamsubmissionallsubmit) && $instance->requireallteammemberssubmit) { // All team members must submit. // Format the submission statement before its sent. We turn off para because this is going within // a form element. $options = array( 'context' => $context, 'para' => false ); $submissionstatement = format_text($adminconfig->submissionstatementteamsubmissionallsubmit, FORMAT_MOODLE, $options); } } return $submissionstatement; } /** * Check if time limit for assignment enabled and set up. * * @param int|null $userid User ID. If null, use global user. * @return bool */ public function is_time_limit_enabled(?int $userid = null): bool { $instance = $this->get_instance($userid); return get_config('assign', 'enabletimelimit') && !empty($instance->timelimit); } /** * Check if an assignment submission is already started and not yet submitted. * * @param int|null $userid User ID. If null, use global user. * @param int $groupid Group ID. If 0, use user id to determine group. * @param int $attemptnumber Attempt number. If -1, check latest submission. * @return bool */ public function is_attempt_in_progress(?int $userid = null, int $groupid = 0, int $attemptnumber = -1): bool { if ($this->get_instance($userid)->teamsubmission) { $submission = $this->get_group_submission($userid, $groupid, false, $attemptnumber); } else { $submission = $this->get_user_submission($userid, false, $attemptnumber); } // If time limit is enabled, we only assume it is in progress if there is a start time for submission. $timedattemptstarted = true; if ($this->is_time_limit_enabled($userid)) { $timedattemptstarted = !empty($submission) && !empty($submission->timestarted); } return !empty($submission) && $submission->status !== ASSIGN_SUBMISSION_STATUS_SUBMITTED && $timedattemptstarted; } /** * Is "Allow partial release of grades while marking anonymously" enabled? * * @return bool */ public function is_marking_anonymous(): bool { return isset($this->get_instance()->markinganonymous) && $this->get_instance()->markinganonymous; } } /** * Portfolio caller class for mod_assign. * * @package mod_assign * @copyright 2012 NetSpot {@link http://www.netspot.com.au} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class assign_portfolio_caller extends portfolio_module_caller_base { /** @var int callback arg - the id of submission we export */ protected $sid; /** @var string component of the submission files we export*/ protected $component; /** @var string callback arg - the area of submission files we export */ protected $area; /** @var int callback arg - the id of file we export */ protected $fileid; /** @var int callback arg - the cmid of the assignment we export */ protected $cmid; /** @var string callback arg - the plugintype of the editor we export */ protected $plugin; /** @var string callback arg - the name of the editor field we export */ protected $editor; /** * Callback arg for a single file export. */ public static function expected_callbackargs() { return array( 'cmid' => true, 'sid' => false, 'area' => false, 'component' => false, 'fileid' => false, 'plugin' => false, 'editor' => false, ); } /** * The constructor. * * @param array $callbackargs */ public function __construct($callbackargs) { parent::__construct($callbackargs); $this->cm = get_coursemodule_from_id('assign', $this->cmid, 0, false, MUST_EXIST); } /** * Load data needed for the portfolio export. * * If the assignment type implements portfolio_load_data(), the processing is delegated * to it. Otherwise, the caller must provide either fileid (to export single file) or * submissionid and filearea (to export all data attached to the given submission file area) * via callback arguments. * * @throws portfolio_caller_exception */ public function load_data() { global $DB; $context = context_module::instance($this->cmid); if (empty($this->fileid)) { if (empty($this->sid) || empty($this->area)) { throw new portfolio_caller_exception('invalidfileandsubmissionid', 'mod_assign'); } $submission = $DB->get_record('assign_submission', array('id' => $this->sid)); } else { $submissionid = $DB->get_field('files', 'itemid', array('id' => $this->fileid, 'contextid' => $context->id)); if ($submissionid) { $submission = $DB->get_record('assign_submission', array('id' => $submissionid)); } } if (empty($submission)) { throw new portfolio_caller_exception('filenotfound'); } else if ($submission->userid == 0) { // This must be a group submission. if (!groups_is_member($submission->groupid, $this->user->id)) { throw new portfolio_caller_exception('filenotfound'); } } else if ($this->user->id != $submission->userid) { throw new portfolio_caller_exception('filenotfound'); } // Export either an area of files or a single file (see function for more detail). // The first arg is an id or null. If it is an id, the rest of the args are ignored. // If it is null, the rest of the args are used to load a list of files from get_areafiles. $this->set_file_and_format_data($this->fileid, $context->id, $this->component, $this->area, $this->sid, 'timemodified', false); } /** * Prepares the package up before control is passed to the portfolio plugin. * * @throws portfolio_caller_exception * @return mixed */ public function prepare_package() { if ($this->plugin && $this->editor) { $options = portfolio_format_text_options(); $context = context_module::instance($this->cmid); $options->context = $context; $plugin = $this->get_submission_plugin(); $text = $plugin->get_editor_text($this->editor, $this->sid); $format = $plugin->get_editor_format($this->editor, $this->sid); $html = format_text($text, $format, $options); $html = portfolio_rewrite_pluginfile_urls($html, $context->id, 'mod_assign', $this->area, $this->sid, $this->exporter->get('format')); $exporterclass = $this->exporter->get('formatclass'); if (in_array($exporterclass, array(PORTFOLIO_FORMAT_PLAINHTML, PORTFOLIO_FORMAT_RICHHTML))) { if ($files = $this->exporter->get('caller')->get('multifiles')) { foreach ($files as $file) { $this->exporter->copy_existing_file($file); } } return $this->exporter->write_new_file($html, 'assignment.html', !empty($files)); } else if ($this->exporter->get('formatclass') == PORTFOLIO_FORMAT_LEAP2A) { $leapwriter = $this->exporter->get('format')->leap2a_writer(); $entry = new portfolio_format_leap2a_entry($this->area . $this->cmid, $context->get_context_name(), 'resource', $html); $entry->add_category('web', 'resource_type'); $entry->author = $this->user; $leapwriter->add_entry($entry); if ($files = $this->exporter->get('caller')->get('multifiles')) { $leapwriter->link_files($entry, $files, $this->area . $this->cmid . 'file'); foreach ($files as $file) { $this->exporter->copy_existing_file($file); } } return $this->exporter->write_new_file($leapwriter->to_xml(), $this->exporter->get('format')->manifest_name(), true); } else { debugging('invalid format class: ' . $this->exporter->get('formatclass')); } } if ($this->exporter->get('formatclass') == PORTFOLIO_FORMAT_LEAP2A) { $leapwriter = $this->exporter->get('format')->leap2a_writer(); $files = array(); if ($this->singlefile) { $files[] = $this->singlefile; } else if ($this->multifiles) { $files = $this->multifiles; } else { throw new portfolio_caller_exception('invalidpreparepackagefile', 'portfolio', $this->get_return_url()); } $entryids = array(); foreach ($files as $file) { $entry = new portfolio_format_leap2a_file($file->get_filename(), $file); $entry->author = $this->user; $leapwriter->add_entry($entry); $this->exporter->copy_existing_file($file); $entryids[] = $entry->id; } if (count($files) > 1) { $baseid = 'assign' . $this->cmid . $this->area; $context = context_module::instance($this->cmid); // If we have multiple files, they should be grouped together into a folder. $entry = new portfolio_format_leap2a_entry($baseid . 'group', $context->get_context_name(), 'selection'); $leapwriter->add_entry($entry); $leapwriter->make_selection($entry, $entryids, 'Folder'); } return $this->exporter->write_new_file($leapwriter->to_xml(), $this->exporter->get('format')->manifest_name(), true); } return $this->prepare_package_file(); } /** * Fetch the plugin by its type. * * @return assign_submission_plugin */ protected function get_submission_plugin() { global $CFG; if (!$this->plugin || !$this->cmid) { return null; } require_once($CFG->dirroot . '/mod/assign/locallib.php'); $context = context_module::instance($this->cmid); $assignment = new assign($context, null, null); return $assignment->get_submission_plugin_by_type($this->plugin); } /** * Calculate a sha1 has of either a single file or a list * of files based on the data set by load_data. * * @return string */ public function get_sha1() { if ($this->plugin && $this->editor) { $plugin = $this->get_submission_plugin(); $options = portfolio_format_text_options(); $options->context = context_module::instance($this->cmid); $text = format_text($plugin->get_editor_text($this->editor, $this->sid), $plugin->get_editor_format($this->editor, $this->sid), $options); $textsha1 = sha1($text); $filesha1 = ''; try { $filesha1 = $this->get_sha1_file(); } catch (portfolio_caller_exception $e) { // No files. } return sha1($textsha1 . $filesha1); } return $this->get_sha1_file(); } /** * Calculate the time to transfer either a single file or a list * of files based on the data set by load_data. * * @return int */ public function expected_time() { return $this->expected_time_file(); } /** * Checking the permissions. * * @return bool */ public function check_permissions() { $context = context_module::instance($this->cmid); return has_capability('mod/assign:exportownsubmission', $context); } /** * Display a module name. * * @return string */ public static function display_name() { return get_string('modulename', 'assign'); } /** * Return array of formats supported by this portfolio call back. * * @return array */ public static function base_supported_formats() { return array(PORTFOLIO_FORMAT_FILE, PORTFOLIO_FORMAT_LEAP2A); } } /** * Logic to happen when a/some group(s) has/have been deleted in a course. * * @param int $courseid The course ID. * @param int $groupid The group id if it is known * @return void */ function assign_process_group_deleted_in_course($courseid, $groupid = null) { global $DB; $params = array('courseid' => $courseid); if ($groupid) { $params['groupid'] = $groupid; // We just update the group that was deleted. $sql = "SELECT o.id, o.assignid, o.groupid FROM {assign_overrides} o JOIN {assign} assign ON assign.id = o.assignid WHERE assign.course = :courseid AND o.groupid = :groupid"; } else { // No groupid, we update all orphaned group overrides for all assign in course. $sql = "SELECT o.id, o.assignid, o.groupid FROM {assign_overrides} o JOIN {assign} assign ON assign.id = o.assignid LEFT JOIN {groups} grp ON grp.id = o.groupid WHERE assign.course = :courseid AND o.groupid IS NOT NULL AND grp.id IS NULL"; } $records = $DB->get_records_sql($sql, $params); if (!$records) { return; // Nothing to do. } $DB->delete_records_list('assign_overrides', 'id', array_keys($records)); $cache = cache::make('mod_assign', 'overrides'); foreach ($records as $record) { $cache->delete("{$record->assignid}_g_{$record->groupid}"); } } /** * Change the sort order of an override * * @param int $id of the override * @param string $move direction of move * @param int $assignid of the assignment * @return bool success of operation */ function move_group_override($id, $move, $assignid) { global $DB; // Get the override object. if (!$override = $DB->get_record('assign_overrides', ['id' => $id, 'assignid' => $assignid], 'id, sortorder, groupid')) { return false; } // Count the number of group overrides. $overridecountgroup = $DB->count_records('assign_overrides', array('userid' => null, 'assignid' => $assignid)); // Calculate the new sortorder. if ( ($move == 'up') and ($override->sortorder > 1)) { $neworder = $override->sortorder - 1; } else if (($move == 'down') and ($override->sortorder < $overridecountgroup)) { $neworder = $override->sortorder + 1; } else { return false; } // Retrieve the override object that is currently residing in the new position. $params = ['sortorder' => $neworder, 'assignid' => $assignid]; if ($swapoverride = $DB->get_record('assign_overrides', $params, 'id, sortorder, groupid')) { // Swap the sortorders. $swapoverride->sortorder = $override->sortorder; $override->sortorder = $neworder; // Update the override records. $DB->update_record('assign_overrides', $override); $DB->update_record('assign_overrides', $swapoverride); // Delete cache for the 2 records we updated above. $cache = cache::make('mod_assign', 'overrides'); $cache->delete("{$assignid}_g_{$override->groupid}"); $cache->delete("{$assignid}_g_{$swapoverride->groupid}"); } reorder_group_overrides($assignid); return true; } /** * Reorder the overrides starting at the override at the given startorder. * * @param int $assignid of the assigment */ function reorder_group_overrides($assignid) { global $DB; $i = 1; if ($overrides = $DB->get_records('assign_overrides', array('userid' => null, 'assignid' => $assignid), 'sortorder ASC')) { $cache = cache::make('mod_assign', 'overrides'); foreach ($overrides as $override) { $f = new stdClass(); $f->id = $override->id; $f->sortorder = $i++; $DB->update_record('assign_overrides', $f); $cache->delete("{$assignid}_g_{$override->groupid}"); // Update priorities of group overrides. $params = [ 'modulename' => 'assign', 'instance' => $override->assignid, 'groupid' => $override->groupid ]; $DB->set_field('event', 'priority', $f->sortorder, $params); } } } /** * Get the information about the standard assign JavaScript module. * @return array a standard jsmodule structure. */ function assign_get_js_module() { return array( 'name' => 'mod_assign', 'fullpath' => '/mod/assign/module.js', ); } home/smkb8269/public_html/lms.smk-almanshuriyah.sch.id/mod/lesson/locallib.php000064400000641171152267112340023252 0ustar00. /** * Local library file for Lesson. These are non-standard functions that are used * only by Lesson. * * @package mod_lesson * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or late **/ /** Make sure this isn't being directly accessed */ defined('MOODLE_INTERNAL') || die(); /** Include the files that are required by this module */ require_once($CFG->dirroot.'/course/moodleform_mod.php'); require_once($CFG->dirroot . '/mod/lesson/lib.php'); require_once($CFG->libdir . '/filelib.php'); /** This page */ define('LESSON_THISPAGE', 0); /** Next page -> any page not seen before */ define("LESSON_UNSEENPAGE", 1); /** Next page -> any page not answered correctly */ define("LESSON_UNANSWEREDPAGE", 2); /** Jump to Next Page */ define("LESSON_NEXTPAGE", -1); /** End of Lesson */ define("LESSON_EOL", -9); /** Jump to an unseen page within a branch and end of branch or end of lesson */ define("LESSON_UNSEENBRANCHPAGE", -50); /** Jump to Previous Page */ define("LESSON_PREVIOUSPAGE", -40); /** Jump to a random page within a branch and end of branch or end of lesson */ define("LESSON_RANDOMPAGE", -60); /** Jump to a random Branch */ define("LESSON_RANDOMBRANCH", -70); /** Cluster Jump */ define("LESSON_CLUSTERJUMP", -80); /** Undefined */ define("LESSON_UNDEFINED", -99); /** LESSON_MAX_EVENT_LENGTH = 432000 ; 5 days maximum */ define("LESSON_MAX_EVENT_LENGTH", "432000"); /** Answer format is HTML */ define("LESSON_ANSWER_HTML", "HTML"); /** Placeholder answer for all other answers. */ define("LESSON_OTHER_ANSWERS", "@#wronganswer#@"); ////////////////////////////////////////////////////////////////////////////////////// /// Any other lesson functions go here. Each of them must have a name that /// starts with lesson_ /** * Checks to see if a LESSON_CLUSTERJUMP or * a LESSON_UNSEENBRANCHPAGE is used in a lesson. * * This function is only executed when a teacher is * checking the navigation for a lesson. * * @param stdClass $lesson Id of the lesson that is to be checked. * @return boolean True or false. **/ function lesson_display_teacher_warning($lesson) { global $DB; // get all of the lesson answers $params = array ("lessonid" => $lesson->id); if (!$lessonanswers = $DB->get_records_select("lesson_answers", "lessonid = :lessonid", $params)) { // no answers, then not using cluster or unseen return false; } // just check for the first one that fulfills the requirements foreach ($lessonanswers as $lessonanswer) { if ($lessonanswer->jumpto == LESSON_CLUSTERJUMP || $lessonanswer->jumpto == LESSON_UNSEENBRANCHPAGE) { return true; } } // if no answers use either of the two jumps return false; } /** * Interprets the LESSON_UNSEENBRANCHPAGE jump. * * will return the pageid of a random unseen page that is within a branch * * @param lesson $lesson * @param int $userid Id of the user. * @param int $pageid Id of the page from which we are jumping. * @return int Id of the next page. **/ function lesson_unseen_question_jump($lesson, $user, $pageid) { global $DB; // get the number of retakes if (!$retakes = $DB->count_records("lesson_grades", array("lessonid"=>$lesson->id, "userid"=>$user))) { $retakes = 0; } // get all the lesson_attempts aka what the user has seen if ($viewedpages = $DB->get_records("lesson_attempts", array("lessonid"=>$lesson->id, "userid"=>$user, "retry"=>$retakes), "timeseen DESC")) { foreach($viewedpages as $viewed) { $seenpages[] = $viewed->pageid; } } else { $seenpages = array(); } // get the lesson pages $lessonpages = $lesson->load_all_pages(); if ($pageid == LESSON_UNSEENBRANCHPAGE) { // this only happens when a student leaves in the middle of an unseen question within a branch series $pageid = $seenpages[0]; // just change the pageid to the last page viewed inside the branch table } // go up the pages till branch table while ($pageid != 0) { // this condition should never be satisfied... only happens if there are no branch tables above this page if ($lessonpages[$pageid]->qtype == LESSON_PAGE_BRANCHTABLE) { break; } $pageid = $lessonpages[$pageid]->prevpageid; } $pagesinbranch = $lesson->get_sub_pages_of($pageid, array(LESSON_PAGE_BRANCHTABLE, LESSON_PAGE_ENDOFBRANCH)); // this foreach loop stores all the pages that are within the branch table but are not in the $seenpages array $unseen = array(); foreach($pagesinbranch as $page) { if (!in_array($page->id, $seenpages)) { $unseen[] = $page->id; } } if(count($unseen) == 0) { if(isset($pagesinbranch)) { $temp = end($pagesinbranch); $nextpage = $temp->nextpageid; // they have seen all the pages in the branch, so go to EOB/next branch table/EOL } else { // there are no pages inside the branch, so return the next page $nextpage = $lessonpages[$pageid]->nextpageid; } if ($nextpage == 0) { return LESSON_EOL; } else { return $nextpage; } } else { return $unseen[rand(0, count($unseen)-1)]; // returns a random page id for the next page } } /** * Handles the unseen branch table jump. * * @param lesson $lesson * @param int $userid User id. * @return int Will return the page id of a branch table or end of lesson **/ function lesson_unseen_branch_jump($lesson, $userid) { global $DB; if (!$retakes = $DB->count_records("lesson_grades", array("lessonid"=>$lesson->id, "userid"=>$userid))) { $retakes = 0; } if (!$seenbranches = $lesson->get_content_pages_viewed($retakes, $userid, 'timeseen DESC')) { throw new \moodle_exception('cannotfindrecords', 'lesson'); } // get the lesson pages $lessonpages = $lesson->load_all_pages(); // this loads all the viewed branch tables into $seen until it finds the branch table with the flag // which is the branch table that starts the unseenbranch function $seen = array(); foreach ($seenbranches as $seenbranch) { if (!$seenbranch->flag) { $seen[$seenbranch->pageid] = $seenbranch->pageid; } else { $start = $seenbranch->pageid; break; } } // this function searches through the lesson pages to find all the branch tables // that follow the flagged branch table $pageid = $lessonpages[$start]->nextpageid; // move down from the flagged branch table $branchtables = array(); while ($pageid != 0) { // grab all of the branch table till eol if ($lessonpages[$pageid]->qtype == LESSON_PAGE_BRANCHTABLE) { $branchtables[] = $lessonpages[$pageid]->id; } $pageid = $lessonpages[$pageid]->nextpageid; } $unseen = array(); foreach ($branchtables as $branchtable) { // load all of the unseen branch tables into unseen if (!array_key_exists($branchtable, $seen)) { $unseen[] = $branchtable; } } if (count($unseen) > 0) { return $unseen[rand(0, count($unseen)-1)]; // returns a random page id for the next page } else { return LESSON_EOL; // has viewed all of the branch tables } } /** * Handles the random jump between a branch table and end of branch or end of lesson (LESSON_RANDOMPAGE). * * @param lesson $lesson * @param int $pageid The id of the page that we are jumping from (?) * @return int The pageid of a random page that is within a branch table **/ function lesson_random_question_jump($lesson, $pageid) { global $DB; // get the lesson pages $params = array ("lessonid" => $lesson->id); if (!$lessonpages = $DB->get_records_select("lesson_pages", "lessonid = :lessonid", $params)) { throw new \moodle_exception('cannotfindpages', 'lesson'); } // go up the pages till branch table while ($pageid != 0) { // this condition should never be satisfied... only happens if there are no branch tables above this page if ($lessonpages[$pageid]->qtype == LESSON_PAGE_BRANCHTABLE) { break; } $pageid = $lessonpages[$pageid]->prevpageid; } // get the pages within the branch $pagesinbranch = $lesson->get_sub_pages_of($pageid, array(LESSON_PAGE_BRANCHTABLE, LESSON_PAGE_ENDOFBRANCH)); if(count($pagesinbranch) == 0) { // there are no pages inside the branch, so return the next page return $lessonpages[$pageid]->nextpageid; } else { return $pagesinbranch[rand(0, count($pagesinbranch)-1)]->id; // returns a random page id for the next page } } /** * Calculates a user's grade for a lesson. * * @param object $lesson The lesson that the user is taking. * @param int $retries The attempt number. * @param int $userid Id of the user (optional, default current user). * @return object { nquestions => number of questions answered attempts => number of question attempts total => max points possible earned => points earned by student grade => calculated percentage grade nmanual => number of manually graded questions manualpoints => point value for manually graded questions } */ function lesson_grade($lesson, $ntries, $userid = 0) { global $USER, $DB; if (empty($userid)) { $userid = $USER->id; } // Zero out everything $ncorrect = 0; $nviewed = 0; $score = 0; $nmanual = 0; $manualpoints = 0; $thegrade = 0; $nquestions = 0; $total = 0; $earned = 0; $params = array ("lessonid" => $lesson->id, "userid" => $userid, "retry" => $ntries); if ($useranswers = $DB->get_records_select("lesson_attempts", "lessonid = :lessonid AND userid = :userid AND retry = :retry", $params, "timeseen")) { // group each try with its page $attemptset = array(); foreach ($useranswers as $useranswer) { $attemptset[$useranswer->pageid][] = $useranswer; } if (!empty($lesson->maxattempts)) { // Drop all attempts that go beyond max attempts for the lesson. foreach ($attemptset as $key => $set) { $attemptset[$key] = array_slice($set, 0, $lesson->maxattempts); } } // get only the pages and their answers that the user answered list($usql, $parameters) = $DB->get_in_or_equal(array_keys($attemptset)); array_unshift($parameters, $lesson->id); $pages = $DB->get_records_select("lesson_pages", "lessonid = ? AND id $usql", $parameters); $answers = $DB->get_records_select("lesson_answers", "lessonid = ? AND pageid $usql", $parameters); // Number of pages answered $nquestions = count($pages); foreach ($attemptset as $attempts) { $page = lesson_page::load($pages[end($attempts)->pageid], $lesson); if ($lesson->custom) { $attempt = end($attempts); // If essay question, handle it, otherwise add to score if ($page->requires_manual_grading()) { $useranswerobj = unserialize_object($attempt->useranswer); if (isset($useranswerobj->score)) { $earned += $useranswerobj->score; } $nmanual++; $manualpoints += $answers[$attempt->answerid]->score; } else if (!empty($attempt->answerid)) { $earned += $page->earned_score($answers, $attempt); } } else { foreach ($attempts as $attempt) { $earned += $attempt->correct; } $attempt = end($attempts); // doesn't matter which one // If essay question, increase numbers if ($page->requires_manual_grading()) { $nmanual++; $manualpoints++; } } // Number of times answered $nviewed += count($attempts); } if ($lesson->custom) { $bestscores = array(); // Find the highest possible score per page to get our total foreach ($answers as $answer) { if(!isset($bestscores[$answer->pageid])) { $bestscores[$answer->pageid] = $answer->score; } else if ($bestscores[$answer->pageid] < $answer->score) { $bestscores[$answer->pageid] = $answer->score; } } $total = array_sum($bestscores); } else { // Check to make sure the student has answered the minimum questions if ($lesson->minquestions and $nquestions < $lesson->minquestions) { // Nope, increase number viewed by the amount of unanswered questions $total = $nviewed + ($lesson->minquestions - $nquestions); } else { $total = $nviewed; } } } if ($total) { // not zero $thegrade = round(100 * $earned / $total, 5); } // Build the grade information object $gradeinfo = new stdClass; $gradeinfo->nquestions = $nquestions; $gradeinfo->attempts = $nviewed; $gradeinfo->total = $total; $gradeinfo->earned = $earned; $gradeinfo->grade = $thegrade; $gradeinfo->nmanual = $nmanual; $gradeinfo->manualpoints = $manualpoints; return $gradeinfo; } /** * Determines if a user can view the left menu. The determining factor * is whether a user has a grade greater than or equal to the lesson setting * of displayleftif * * @param object $lesson Lesson object of the current lesson * @return boolean 0 if the user cannot see, or $lesson->displayleft to keep displayleft unchanged **/ function lesson_displayleftif($lesson) { global $CFG, $USER, $DB; if (!empty($lesson->displayleftif)) { // get the current user's max grade for this lesson $params = array ("userid" => $USER->id, "lessonid" => $lesson->id); if ($maxgrade = $DB->get_record_sql('SELECT userid, MAX(grade) AS maxgrade FROM {lesson_grades} WHERE userid = :userid AND lessonid = :lessonid GROUP BY userid', $params)) { if ($maxgrade->maxgrade < $lesson->displayleftif) { return 0; // turn off the displayleft } } else { return 0; // no grades } } // if we get to here, keep the original state of displayleft lesson setting return $lesson->displayleft; } /** * * @param $cm * @param $lesson * @param $page * @return unknown_type */ function lesson_add_fake_blocks($page, $cm, $lesson, $timer = null) { $bc = lesson_menu_block_contents($cm->id, $lesson); if (!empty($bc)) { $regions = $page->blocks->get_regions(); $firstregion = reset($regions); $page->blocks->add_fake_block($bc, $firstregion); } $bc = lesson_mediafile_block_contents($cm->id, $lesson); if (!empty($bc)) { $page->blocks->add_fake_block($bc, $page->blocks->get_default_region()); } if (!empty($timer)) { $bc = lesson_clock_block_contents($cm->id, $lesson, $timer, $page); if (!empty($bc)) { $page->blocks->add_fake_block($bc, $page->blocks->get_default_region()); } } } /** * If there is a media file associated with this * lesson, return a block_contents that displays it. * * @param int $cmid Course Module ID for this lesson * @param object $lesson Full lesson record object * @return block_contents **/ function lesson_mediafile_block_contents($cmid, $lesson) { global $OUTPUT; if (empty($lesson->mediafile)) { return null; } $options = array(); $options['menubar'] = 0; $options['location'] = 0; $options['left'] = 5; $options['top'] = 5; $options['scrollbars'] = 1; $options['resizable'] = 1; $options['width'] = $lesson->mediawidth; $options['height'] = $lesson->mediaheight; $link = new moodle_url('/mod/lesson/mediafile.php?id='.$cmid); $action = new popup_action('click', $link, 'lessonmediafile', $options); $content = $OUTPUT->action_link($link, get_string('mediafilepopup', 'lesson'), $action, array('title'=>get_string('mediafilepopup', 'lesson'))); $bc = new block_contents(); $bc->title = get_string('linkedmedia', 'lesson'); $bc->attributes['class'] = 'mediafile block'; $bc->content = $content; return $bc; } /** * If a timed lesson and not a teacher, then * return a block_contents containing the clock. * * @param int $cmid Course Module ID for this lesson * @param object $lesson Full lesson record object * @param object $timer Full timer record object * @return block_contents **/ function lesson_clock_block_contents($cmid, $lesson, $timer, $page) { // Display for timed lessons and for students only $context = context_module::instance($cmid); if ($lesson->timelimit == 0 || has_capability('mod/lesson:manage', $context)) { return null; } $content = '
'; $content .= $lesson->time_remaining($timer->starttime); $content .= '
'; $clocksettings = array('starttime' => $timer->starttime, 'servertime' => time(), 'testlength' => $lesson->timelimit); $page->requires->data_for_js('clocksettings', $clocksettings, true); $page->requires->strings_for_js(array('timeisup'), 'lesson'); $page->requires->js('/mod/lesson/timer.js'); $page->requires->js_init_call('show_clock'); $bc = new block_contents(); $bc->title = get_string('timeremaining', 'lesson'); $bc->attributes['class'] = 'clock block'; $bc->content = $content; return $bc; } /** * If left menu is turned on, then this will * print the menu in a block * * @param int $cmid Course Module ID for this lesson * @param lesson $lesson Full lesson record object * @return void **/ function lesson_menu_block_contents($cmid, $lesson) { global $CFG, $DB; if (!$lesson->displayleft) { return null; } $pages = $lesson->load_all_pages(); foreach ($pages as $page) { if ((int)$page->prevpageid === 0) { $pageid = $page->id; break; } } $currentpageid = optional_param('pageid', $pageid, PARAM_INT); if (!$pageid || !$pages) { return null; } $content = '' . get_string('skip', 'lesson') . "\n
\n\n
\n"; $bc = new block_contents(); $bc->title = get_string('lessonmenu', 'lesson'); $bc->attributes['class'] = 'menu block'; $bc->content = $content; return $bc; } /** * This is a function used to detect media types and generate html code. * * @global object $CFG * @global object $PAGE * @param object $lesson * @param object $context * @return string $code the html code of media */ function lesson_get_media_html($lesson, $context) { global $CFG, $PAGE, $OUTPUT; require_once("$CFG->libdir/resourcelib.php"); // get the media file link if (strpos($lesson->mediafile, '://') !== false) { $url = new moodle_url($lesson->mediafile); } else { // the timemodified is used to prevent caching problems, instead of '/' we should better read from files table and use sortorder $url = moodle_url::make_pluginfile_url($context->id, 'mod_lesson', 'mediafile', $lesson->timemodified, '/', ltrim($lesson->mediafile, '/')); } $title = $lesson->mediafile; $clicktoopen = html_writer::link($url, get_string('download')); $mimetype = resourcelib_guess_url_mimetype($url); $extension = resourcelib_get_extension($url->out(false)); $mediamanager = core_media_manager::instance($PAGE); $embedoptions = array( core_media_manager::OPTION_TRUSTED => true, core_media_manager::OPTION_BLOCK => true ); // find the correct type and print it out if (in_array($mimetype, array('image/gif','image/jpeg','image/png'))) { // It's an image $code = resourcelib_embed_image($url, $title); } else if ($mediamanager->can_embed_url($url, $embedoptions)) { // Media (audio/video) file. $code = $mediamanager->embed_url($url, $title, 0, 0, $embedoptions); } else { // anything else - just try object tag enlarged as much as possible $code = resourcelib_embed_general($url, $title, $clicktoopen, $mimetype); } return $code; } /** * Logic to happen when a/some group(s) has/have been deleted in a course. * * @param int $courseid The course ID. * @param int $groupid The group id if it is known * @return void */ function lesson_process_group_deleted_in_course($courseid, $groupid = null) { global $DB; $params = array('courseid' => $courseid); if ($groupid) { $params['groupid'] = $groupid; // We just update the group that was deleted. $sql = "SELECT o.id, o.lessonid, o.groupid FROM {lesson_overrides} o JOIN {lesson} lesson ON lesson.id = o.lessonid WHERE lesson.course = :courseid AND o.groupid = :groupid"; } else { // No groupid, we update all orphaned group overrides for all lessons in course. $sql = "SELECT o.id, o.lessonid, o.groupid FROM {lesson_overrides} o JOIN {lesson} lesson ON lesson.id = o.lessonid LEFT JOIN {groups} grp ON grp.id = o.groupid WHERE lesson.course = :courseid AND o.groupid IS NOT NULL AND grp.id IS NULL"; } $records = $DB->get_records_sql($sql, $params); if (!$records) { return; // Nothing to do. } $DB->delete_records_list('lesson_overrides', 'id', array_keys($records)); $cache = cache::make('mod_lesson', 'overrides'); foreach ($records as $record) { $cache->delete("{$record->lessonid}_g_{$record->groupid}"); } } /** * Return the overview report table and data. * * @param lesson $lesson lesson instance * @param mixed $currentgroup false if not group used, 0 for all groups, group id (int) to filter by that groups * @return mixed false if there is no information otherwise html_table and stdClass with the table and data * @since Moodle 3.3 */ function lesson_get_overview_report_table_and_data(lesson $lesson, $currentgroup) { global $DB, $CFG, $OUTPUT; require_once($CFG->dirroot . '/mod/lesson/pagetypes/branchtable.php'); $context = $lesson->context; $cm = $lesson->cm; // Count the number of branch and question pages in this lesson. $branchcount = $DB->count_records('lesson_pages', array('lessonid' => $lesson->id, 'qtype' => LESSON_PAGE_BRANCHTABLE)); $questioncount = ($DB->count_records('lesson_pages', array('lessonid' => $lesson->id)) - $branchcount); // Only load students if there attempts for this lesson. $attempts = $DB->record_exists('lesson_attempts', array('lessonid' => $lesson->id)); $branches = $DB->record_exists('lesson_branch', array('lessonid' => $lesson->id)); $timer = $DB->record_exists('lesson_timer', array('lessonid' => $lesson->id)); if ($attempts or $branches or $timer) { list($esql, $params) = get_enrolled_sql($context, '', $currentgroup, true); list($sort, $sortparams) = users_order_by_sql('u'); // TODO Does not support custom user profile fields (MDL-70456). $userfieldsapi = \core_user\fields::for_identity($context, false)->with_userpic(); $ufields = $userfieldsapi->get_sql('u', false, '', '', false)->selects; $extrafields = $userfieldsapi->get_required_fields([\core_user\fields::PURPOSE_IDENTITY]); $params['a1lessonid'] = $lesson->id; $params['b1lessonid'] = $lesson->id; $params['c1lessonid'] = $lesson->id; $sql = "SELECT DISTINCT $ufields FROM {user} u JOIN ( SELECT userid, lessonid FROM {lesson_attempts} a1 WHERE a1.lessonid = :a1lessonid UNION SELECT userid, lessonid FROM {lesson_branch} b1 WHERE b1.lessonid = :b1lessonid UNION SELECT userid, lessonid FROM {lesson_timer} c1 WHERE c1.lessonid = :c1lessonid ) a ON u.id = a.userid JOIN ($esql) ue ON ue.id = a.userid ORDER BY $sort"; $students = $DB->get_recordset_sql($sql, $params); if (!$students->valid()) { $students->close(); return array(false, false); } } else { return array(false, false); } if (! $grades = $DB->get_records('lesson_grades', array('lessonid' => $lesson->id), 'completed')) { $grades = array(); } if (! $times = $DB->get_records('lesson_timer', array('lessonid' => $lesson->id), 'starttime')) { $times = array(); } // Build an array for output. $studentdata = array(); $attempts = $DB->get_recordset('lesson_attempts', array('lessonid' => $lesson->id), 'timeseen'); foreach ($attempts as $attempt) { // if the user is not in the array or if the retry number is not in the sub array, add the data for that try. if (empty($studentdata[$attempt->userid]) || empty($studentdata[$attempt->userid][$attempt->retry])) { // restore/setup defaults $n = 0; $timestart = 0; $timeend = 0; $usergrade = null; $eol = 0; // search for the grade record for this try. if not there, the nulls defined above will be used. foreach($grades as $grade) { // check to see if the grade matches the correct user if ($grade->userid == $attempt->userid) { // see if n is = to the retry if ($n == $attempt->retry) { // get grade info $usergrade = round($grade->grade, 2); // round it here so we only have to do it once break; } $n++; // if not equal, then increment n } } $n = 0; // search for the time record for this try. if not there, the nulls defined above will be used. foreach($times as $time) { // check to see if the grade matches the correct user if ($time->userid == $attempt->userid) { // see if n is = to the retry if ($n == $attempt->retry) { // get grade info $timeend = $time->lessontime; $timestart = $time->starttime; $eol = $time->completed; break; } $n++; // if not equal, then increment n } } // build up the array. // this array represents each student and all of their tries at the lesson $studentdata[$attempt->userid][$attempt->retry] = array( "timestart" => $timestart, "timeend" => $timeend, "grade" => $usergrade, "end" => $eol, "try" => $attempt->retry, "userid" => $attempt->userid); } } $attempts->close(); $branches = $DB->get_recordset('lesson_branch', array('lessonid' => $lesson->id), 'timeseen'); foreach ($branches as $branch) { // If the user is not in the array or if the retry number is not in the sub array, add the data for that try. if (empty($studentdata[$branch->userid]) || empty($studentdata[$branch->userid][$branch->retry])) { // Restore/setup defaults. $n = 0; $timestart = 0; $timeend = 0; $usergrade = null; $eol = 0; // Search for the time record for this try. if not there, the nulls defined above will be used. foreach ($times as $time) { // Check to see if the grade matches the correct user. if ($time->userid == $branch->userid) { // See if n is = to the retry. if ($n == $branch->retry) { // Get grade info. $timeend = $time->lessontime; $timestart = $time->starttime; $eol = $time->completed; break; } $n++; // If not equal, then increment n. } } // Build up the array. // This array represents each student and all of their tries at the lesson. $studentdata[$branch->userid][$branch->retry] = array( "timestart" => $timestart, "timeend" => $timeend, "grade" => $usergrade, "end" => $eol, "try" => $branch->retry, "userid" => $branch->userid); } } $branches->close(); // Need the same thing for timed entries that were not completed. foreach ($times as $time) { $endoflesson = $time->completed; // If the time start is the same with another record then we shouldn't be adding another item to this array. if (isset($studentdata[$time->userid])) { $foundmatch = false; $n = 0; foreach ($studentdata[$time->userid] as $key => $value) { if ($value['timestart'] == $time->starttime) { // Don't add this to the array. $foundmatch = true; break; } } $n = count($studentdata[$time->userid]) + 1; if (!$foundmatch) { // Add a record. $studentdata[$time->userid][] = array( "timestart" => $time->starttime, "timeend" => $time->lessontime, "grade" => null, "end" => $endoflesson, "try" => $n, "userid" => $time->userid ); } } else { $studentdata[$time->userid][] = array( "timestart" => $time->starttime, "timeend" => $time->lessontime, "grade" => null, "end" => $endoflesson, "try" => 0, "userid" => $time->userid ); } } // To store all the data to be returned by the function. $data = new stdClass(); // Determine if lesson should have a score. if ($branchcount > 0 AND $questioncount == 0) { // This lesson only contains content pages and is not graded. $data->lessonscored = false; } else { // This lesson is graded. $data->lessonscored = true; } // set all the stats variables $data->numofattempts = 0; $data->avescore = 0; $data->avetime = 0; $data->highscore = null; $data->lowscore = null; $data->hightime = null; $data->lowtime = null; $data->students = array(); $table = new html_table(); $headers = [get_string('name')]; foreach ($extrafields as $field) { $headers[] = \core_user\fields::get_display_name($field); } $caneditlesson = has_capability('mod/lesson:edit', $context); $attemptsheader = get_string('attempts', 'lesson'); if ($caneditlesson) { $selectall = get_string('selectallattempts', 'lesson'); $deselectall = get_string('deselectallattempts', 'lesson'); // Build the select/deselect all control. $selectallid = 'selectall-attempts'; $mastercheckbox = new \core\output\checkbox_toggleall('lesson-attempts', true, [ 'id' => $selectallid, 'name' => $selectallid, 'value' => 1, 'label' => $selectall, 'selectall' => $selectall, 'deselectall' => $deselectall, 'labelclasses' => 'form-check-label' ]); $attemptsheader = $OUTPUT->render($mastercheckbox); } $headers [] = $attemptsheader; // Set up the table object. if ($data->lessonscored) { $headers [] = get_string('highscore', 'lesson'); } $colcount = count($headers); $table->head = $headers; $table->align = []; $table->align = array_pad($table->align, $colcount, 'center'); $table->align[$colcount - 1] = 'left'; if ($data->lessonscored) { $table->align[$colcount - 2] = 'left'; } $table->attributes['class'] = 'table table-striped'; // print out the $studentdata array // going through each student that has attempted the lesson, so, each student should have something to be displayed foreach ($students as $student) { // check to see if the student has attempts to print out if (array_key_exists($student->id, $studentdata)) { // set/reset some variables $attempts = array(); $dataforstudent = new stdClass; $dataforstudent->attempts = array(); // gather the data for each user attempt $bestgrade = 0; // $tries holds all the tries/retries a student has done $tries = $studentdata[$student->id]; $studentname = fullname($student, true); foreach ($tries as $try) { $dataforstudent->attempts[] = $try; // Start to build up the checkbox and link. $attempturlparams = [ 'id' => $cm->id, 'action' => 'reportdetail', 'userid' => $try['userid'], 'try' => $try['try'], ]; if ($try["grade"] !== null) { // if null then not done yet // this is what the link does when the user has completed the try $timetotake = $try["timeend"] - $try["timestart"]; if ($try["grade"] > $bestgrade) { $bestgrade = $try["grade"]; } $attemptdata = (object)[ 'grade' => $try["grade"], 'timestart' => userdate($try["timestart"]), 'duration' => format_time($timetotake), ]; $attemptlinkcontents = get_string('attemptinfowithgrade', 'lesson', $attemptdata); } else { if ($try["end"]) { // User finished the lesson but has no grade. (Happens when there are only content pages). $timetotake = $try["timeend"] - $try["timestart"]; $attemptdata = (object)[ 'timestart' => userdate($try["timestart"]), 'duration' => format_time($timetotake), ]; $attemptlinkcontents = get_string('attemptinfonograde', 'lesson', $attemptdata); } else { // This is what the link does/looks like when the user has not completed the attempt. if ($try['timestart'] !== 0) { // Teacher previews do not track time spent. $attemptlinkcontents = get_string("notcompletedwithdate", "lesson", userdate($try["timestart"])); } else { $attemptlinkcontents = get_string("notcompleted", "lesson"); } $timetotake = null; } } $attempturl = new moodle_url('/mod/lesson/report.php', $attempturlparams); $attemptlink = html_writer::link($attempturl, $attemptlinkcontents, ['class' => 'lesson-attempt-link']); if ($caneditlesson) { $attemptid = 'attempt-' . $try['userid'] . '-' . $try['try']; $attemptname = 'attempts[' . $try['userid'] . '][' . $try['try'] . ']'; $checkbox = new \core\output\checkbox_toggleall('lesson-attempts', false, [ 'id' => $attemptid, 'name' => $attemptname, 'label' => $attemptlink ]); $attemptlink = $OUTPUT->render($checkbox); } // build up the attempts array $attempts[] = $attemptlink; // Run these lines for the stats only if the user finnished the lesson. if ($try["end"]) { // User has completed the lesson. $data->numofattempts++; $data->avetime += $timetotake; if ($timetotake > $data->hightime || $data->hightime == null) { $data->hightime = $timetotake; } if ($timetotake < $data->lowtime || $data->lowtime == null) { $data->lowtime = $timetotake; } if ($try["grade"] !== null) { // The lesson was scored. $data->avescore += $try["grade"]; if ($try["grade"] > $data->highscore || $data->highscore === null) { $data->highscore = $try["grade"]; } if ($try["grade"] < $data->lowscore || $data->lowscore === null) { $data->lowscore = $try["grade"]; } } } } // get line breaks in after each attempt $attempts = implode("
\n", $attempts); $row = [$studentname]; foreach ($extrafields as $field) { $row[] = s($student->$field); } $row[] = $attempts; if ($data->lessonscored) { // Add the grade if the lesson is graded. $row[] = $bestgrade."%"; } $table->data[] = $row; // Add the student data. $dataforstudent->id = $student->id; $dataforstudent->fullname = $studentname; $dataforstudent->bestgrade = $bestgrade; $data->students[] = $dataforstudent; } } $students->close(); if ($data->numofattempts > 0) { $data->avescore = $data->avescore / $data->numofattempts; } return array($table, $data); } /** * Return information about one user attempt (including answers) * @param lesson $lesson lesson instance * @param int $userid the user id * @param int $attempt the attempt number * @return array the user answers (array) and user data stats (object) * @since Moodle 3.3 */ function lesson_get_user_detailed_report_data(lesson $lesson, $userid, $attempt) { global $DB; $context = $lesson->context; if (!empty($userid)) { // Apply overrides. $lesson->update_effective_access($userid); } $pageid = 0; $lessonpages = $lesson->load_all_pages(); foreach ($lessonpages as $lessonpage) { if ($lessonpage->prevpageid == 0) { $pageid = $lessonpage->id; } } // now gather the stats into an object $firstpageid = $pageid; $pagestats = array(); while ($pageid != 0) { // EOL $page = $lessonpages[$pageid]; $params = array ("lessonid" => $lesson->id, "pageid" => $page->id); if ($allanswers = $DB->get_records_select("lesson_attempts", "lessonid = :lessonid AND pageid = :pageid", $params, "timeseen")) { // get them ready for processing $orderedanswers = array(); foreach ($allanswers as $singleanswer) { // ordering them like this, will help to find the single attempt record that we want to keep. $orderedanswers[$singleanswer->userid][$singleanswer->retry][] = $singleanswer; } // this is foreach user and for each try for that user, keep one attempt record foreach ($orderedanswers as $orderedanswer) { foreach($orderedanswer as $tries) { $page->stats($pagestats, $tries); } } } else { // no one answered yet... } //unset($orderedanswers); initialized above now $pageid = $page->nextpageid; } $manager = lesson_page_type_manager::get($lesson); $qtypes = $manager->get_page_type_strings(); $answerpages = array(); $answerpage = ""; $pageid = $firstpageid; // cycle through all the pages // foreach page, add to the $answerpages[] array all the data that is needed // from the question, the users attempt, and the statistics // grayout pages that the user did not answer and Branch, end of branch, cluster // and end of cluster pages while ($pageid != 0) { // EOL $page = $lessonpages[$pageid]; $answerpage = new stdClass; // Keep the original page object. $answerpage->page = $page; $data =''; $answerdata = new stdClass; // Set some defaults for the answer data. $answerdata->score = null; $answerdata->response = null; $answerdata->responseformat = FORMAT_PLAIN; $answerpage->title = format_string($page->title); $options = new stdClass; $options->noclean = true; $options->overflowdiv = true; $options->context = $context; $answerpage->contents = format_text($page->contents, $page->contentsformat, $options); $answerpage->qtype = $qtypes[$page->qtype].$page->option_description_string(); $answerpage->grayout = $page->grayout; $answerpage->context = $context; if (empty($userid)) { // there is no userid, so set these vars and display stats. $answerpage->grayout = 0; $useranswer = null; } elseif ($useranswers = $DB->get_records("lesson_attempts",array("lessonid"=>$lesson->id, "userid"=>$userid, "retry"=>$attempt,"pageid"=>$page->id), "timeseen")) { // get the user's answer for this page // need to find the right one $i = 0; foreach ($useranswers as $userattempt) { $useranswer = $userattempt; $i++; if ($lesson->maxattempts == $i) { break; // reached maxattempts, break out } } } else { // user did not answer this page, gray it out and set some nulls $answerpage->grayout = 1; $useranswer = null; } $i = 0; $n = 0; $answerpages[] = $page->report_answers(clone($answerpage), clone($answerdata), $useranswer, $pagestats, $i, $n); $pageid = $page->nextpageid; } $userstats = new stdClass; if (!empty($userid)) { $params = array("lessonid"=>$lesson->id, "userid"=>$userid); $alreadycompleted = true; if (!$grades = $DB->get_records_select("lesson_grades", "lessonid = :lessonid and userid = :userid", $params, "completed", "*", $attempt, 1)) { $userstats->grade = -1; $userstats->completed = -1; $alreadycompleted = false; } else { $userstats->grade = current($grades); $userstats->completed = $userstats->grade->completed; $userstats->grade = round($userstats->grade->grade, 2); } if (!$times = $lesson->get_user_timers($userid, 'starttime', '*', $attempt, 1)) { $userstats->timetotake = -1; $alreadycompleted = false; } else { $userstats->timetotake = current($times); $userstats->timetotake = $userstats->timetotake->lessontime - $userstats->timetotake->starttime; } if ($alreadycompleted) { $userstats->gradeinfo = lesson_grade($lesson, $attempt, $userid); } } return array($answerpages, $userstats); } /** * Return user's deadline for all lessons in a course, hereby taking into account group and user overrides. * * @param int $courseid the course id. * @return object An object with of all lessonsids and close unixdates in this course, * taking into account the most lenient overrides, if existing and 0 if no close date is set. */ function lesson_get_user_deadline($courseid) { global $DB, $USER; // For teacher and manager/admins return lesson's deadline. if (has_capability('moodle/course:update', context_course::instance($courseid))) { $sql = "SELECT lesson.id, lesson.deadline AS userdeadline FROM {lesson} lesson WHERE lesson.course = :courseid"; $results = $DB->get_records_sql($sql, array('courseid' => $courseid)); return $results; } $sql = "SELECT a.id, COALESCE(v.userclose, v.groupclose, a.deadline, 0) AS userdeadline FROM ( SELECT lesson.id as lessonid, MAX(leo.deadline) AS userclose, MAX(qgo.deadline) AS groupclose FROM {lesson} lesson LEFT JOIN {lesson_overrides} leo on lesson.id = leo.lessonid AND leo.userid = :userid LEFT JOIN {groups_members} gm ON gm.userid = :useringroupid LEFT JOIN {lesson_overrides} qgo on lesson.id = qgo.lessonid AND qgo.groupid = gm.groupid WHERE lesson.course = :courseid GROUP BY lesson.id ) v JOIN {lesson} a ON a.id = v.lessonid"; $results = $DB->get_records_sql($sql, array('userid' => $USER->id, 'useringroupid' => $USER->id, 'courseid' => $courseid)); return $results; } /** * Abstract class that page type's MUST inherit from. * * This is the abstract class that ALL add page type forms must extend. * You will notice that all but two of the methods this class contains are final. * Essentially the only thing that extending classes can do is extend custom_definition. * OR if it has a special requirement on creation it can extend construction_override * * @abstract * @copyright 2009 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ abstract class lesson_add_page_form_base extends moodleform { /** * This is the classic define that is used to identify this pagetype. * Will be one of LESSON_* * @var int */ public $qtype; /** * The simple string that describes the page type e.g. truefalse, multichoice * @var string */ public $qtypestring; /** * An array of options used in the htmleditor * @var array */ protected $editoroptions = array(); /** * True if this is a standard page of false if it does something special. * Questions are standard pages, branch tables are not * @var bool */ protected $standard = true; /** * Answer format supported by question type. */ protected $answerformat = ''; /** * Response format supported by question type. */ protected $responseformat = ''; /** * Each page type can and should override this to add any custom elements to * the basic form that they want */ public function custom_definition() {} /** * Returns answer format used by question type. */ public function get_answer_format() { return $this->answerformat; } /** * Returns response format used by question type. */ public function get_response_format() { return $this->responseformat; } /** * Used to determine if this is a standard page or a special page * @return bool */ final public function is_standard() { return (bool)$this->standard; } /** * Add the required basic elements to the form. * * This method adds the basic elements to the form including title and contents * and then calls custom_definition(); */ final public function definition() { global $CFG; $mform = $this->_form; $editoroptions = $this->_customdata['editoroptions']; if ($this->qtypestring != 'selectaqtype') { if ($this->_customdata['edit']) { $mform->addElement('header', 'qtypeheading', get_string('edit'. $this->qtypestring, 'lesson')); } else { $mform->addElement('header', 'qtypeheading', get_string('add'. $this->qtypestring, 'lesson')); } } if (!empty($this->_customdata['returnto'])) { $mform->addElement('hidden', 'returnto', $this->_customdata['returnto']); $mform->setType('returnto', PARAM_LOCALURL); } $mform->addElement('hidden', 'id'); $mform->setType('id', PARAM_INT); $mform->addElement('hidden', 'pageid'); $mform->setType('pageid', PARAM_INT); if ($this->standard === true) { $mform->addElement('hidden', 'qtype'); $mform->setType('qtype', PARAM_INT); $mform->addElement('text', 'title', get_string('pagetitle', 'lesson'), ['size' => 70, 'maxlength' => 255]); $mform->addRule('title', get_string('required'), 'required', null, 'client'); $mform->addRule('title', get_string('maximumchars', '', 255), 'maxlength', 255, 'client'); if (!empty($CFG->formatstringstriptags)) { $mform->setType('title', PARAM_TEXT); } else { $mform->setType('title', PARAM_CLEANHTML); } $this->editoroptions = array('noclean'=>true, 'maxfiles'=>EDITOR_UNLIMITED_FILES, 'maxbytes'=>$this->_customdata['maxbytes']); $mform->addElement('editor', 'contents_editor', get_string('pagecontents', 'lesson'), null, $this->editoroptions); $mform->setType('contents_editor', PARAM_RAW); $mform->addRule('contents_editor', get_string('required'), 'required', null, 'client'); } $this->custom_definition(); if ($this->_customdata['edit'] === true) { $mform->addElement('hidden', 'edit', 1); $mform->setType('edit', PARAM_BOOL); $this->add_action_buttons(get_string('cancel'), get_string('savepage', 'lesson')); } else if ($this->qtype === 'questiontype') { $this->add_action_buttons(get_string('cancel'), get_string('addaquestionpage', 'lesson')); } else { $this->add_action_buttons(get_string('cancel'), get_string('savepage', 'lesson')); } } /** * Convenience function: Adds a jumpto select element * * @param string $name * @param string|null $label * @param int $selected The page to select by default */ final protected function add_jumpto($name, $label=null, $selected=LESSON_NEXTPAGE) { $title = get_string("jump", "lesson"); if ($label === null) { $label = $title; } if (is_int($name)) { $name = "jumpto[$name]"; } $this->_form->addElement('select', $name, $label, $this->_customdata['jumpto']); $this->_form->setDefault($name, $selected); $this->_form->addHelpButton($name, 'jumps', 'lesson'); } /** * Convenience function: Adds a score input element * * @param string $name * @param string|null $label * @param mixed $value The default value */ final protected function add_score($name, $label=null, $value=null) { if ($label === null) { $label = get_string("score", "lesson"); } if (is_int($name)) { $name = "score[$name]"; } $this->_form->addElement('text', $name, $label, array('size'=>5)); $this->_form->setType($name, PARAM_INT); if ($value !== null) { $this->_form->setDefault($name, $value); } $this->_form->addHelpButton($name, 'score', 'lesson'); // Score is only used for custom scoring. Disable the element when not in use to stop some confusion. if (!$this->_customdata['lesson']->custom) { $this->_form->freeze($name); } } /** * Convenience function: Adds an answer editor * * @param int $count The count of the element to add * @param string $label, null means default * @param bool $required * @param string $format * @param array $help Add help text via the addHelpButton. Must be an array which contains the string identifier and * component as it's elements * @return void */ final protected function add_answer($count, $label = null, $required = false, $format= '', array $help = []) { if ($label === null) { $label = get_string('answer', 'lesson'); } if ($format == LESSON_ANSWER_HTML) { $this->_form->addElement('editor', 'answer_editor['.$count.']', $label, array('rows' => '4', 'columns' => '80'), array('noclean' => true, 'maxfiles' => EDITOR_UNLIMITED_FILES, 'maxbytes' => $this->_customdata['maxbytes'])); $this->_form->setType('answer_editor['.$count.']', PARAM_RAW); $this->_form->setDefault('answer_editor['.$count.']', array('text' => '', 'format' => FORMAT_HTML)); } else { $this->_form->addElement('text', 'answer_editor['.$count.']', $label, array('size' => '50', 'maxlength' => '200')); $this->_form->setType('answer_editor['.$count.']', PARAM_TEXT); } if ($required) { $this->_form->addRule('answer_editor['.$count.']', get_string('required'), 'required', null, 'client'); } if ($help) { $this->_form->addHelpButton("answer_editor[$count]", $help['identifier'], $help['component']); } } /** * Convenience function: Adds an response editor * * @param int $count The count of the element to add * @param string $label, null means default * @param bool $required * @return void */ final protected function add_response($count, $label = null, $required = false) { if ($label === null) { $label = get_string('response', 'lesson'); } $this->_form->addElement('editor', 'response_editor['.$count.']', $label, array('rows' => '4', 'columns' => '80'), array('noclean' => true, 'maxfiles' => EDITOR_UNLIMITED_FILES, 'maxbytes' => $this->_customdata['maxbytes'])); $this->_form->setType('response_editor['.$count.']', PARAM_RAW); $this->_form->setDefault('response_editor['.$count.']', array('text' => '', 'format' => FORMAT_HTML)); if ($required) { $this->_form->addRule('response_editor['.$count.']', get_string('required'), 'required', null, 'client'); } } /** * A function that gets called upon init of this object by the calling script. * * This can be used to process an immediate action if required. Currently it * is only used in special cases by non-standard page types. * * @return bool */ public function construction_override($pageid, lesson $lesson) { return true; } } /** * Class representation of a lesson * * This class is used the interact with, and manage a lesson once instantiated. * If you need to fetch a lesson object you can do so by calling * * * lesson::load($lessonid); * // or * $lessonrecord = $DB->get_record('lesson', $lessonid); * $lesson = new lesson($lessonrecord); * * * The class itself extends lesson_base as all classes within the lesson module should * * These properties are from the database * @property int $id The id of this lesson * @property int $course The ID of the course this lesson belongs to * @property string $name The name of this lesson * @property int $practice Flag to toggle this as a practice lesson * @property int $modattempts Toggle to allow the user to go back and review answers * @property int $usepassword Toggle the use of a password for entry * @property string $password The password to require users to enter * @property int $dependency ID of another lesson this lesson is dependent on * @property string $conditions Conditions of the lesson dependency * @property int $grade The maximum grade a user can achieve (%) * @property int $custom Toggle custom scoring on or off * @property int $ongoing Toggle display of an ongoing score * @property int $usemaxgrade How retakes are handled (max=1, mean=0) * @property int $maxanswers The max number of answers or branches * @property int $maxattempts The maximum number of attempts a user can record * @property int $review Toggle use or wrong answer review button * @property int $nextpagedefault Override the default next page * @property int $feedback Toggles display of default feedback * @property int $minquestions Sets a minimum value of pages seen when calculating grades * @property int $maxpages Maximum number of pages this lesson can contain * @property int $retake Flag to allow users to retake a lesson * @property int $activitylink Relate this lesson to another lesson * @property string $mediafile File to pop up to or webpage to display * @property int $mediaheight Sets the height of the media file popup * @property int $mediawidth Sets the width of the media file popup * @property int $mediaclose Toggle display of a media close button * @property int $slideshow Flag for whether branch pages should be shown as slideshows * @property int $width Width of slideshow * @property int $height Height of slideshow * @property string $bgcolor Background colour of slideshow * @property int $displayleft Display a left menu * @property int $displayleftif Sets the condition on which the left menu is displayed * @property int $progressbar Flag to toggle display of a lesson progress bar * @property int $available Timestamp of when this lesson becomes available * @property int $deadline Timestamp of when this lesson is no longer available * @property int $timemodified Timestamp when lesson was last modified * @property int $allowofflineattempts Whether to allow the lesson to be attempted offline in the mobile app * * These properties are calculated * @property int $firstpageid Id of the first page of this lesson (prevpageid=0) * @property int $lastpageid Id of the last page of this lesson (nextpageid=0) * * @copyright 2009 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class lesson extends lesson_base { /** * The id of the first page (where prevpageid = 0) gets set and retrieved by * {@see get_firstpageid()} by directly calling $lesson->firstpageid; * @var int */ protected $firstpageid = null; /** * The id of the last page (where nextpageid = 0) gets set and retrieved by * {@see get_lastpageid()} by directly calling $lesson->lastpageid; * @var int */ protected $lastpageid = null; /** * An array used to cache the pages associated with this lesson after the first * time they have been loaded. * A note to developers: If you are going to be working with MORE than one or * two pages from a lesson you should probably call {@see $lesson->load_all_pages()} * in order to save excess database queries. * @var array An array of lesson_page objects */ protected $pages = array(); /** * Flag that gets set to true once all of the pages associated with the lesson * have been loaded. * @var bool */ protected $loadedallpages = false; /** * Course module object gets set and retrieved by directly calling $lesson->cm; * @see get_cm() * @var stdClass */ protected $cm = null; /** * Course object gets set and retrieved by directly calling $lesson->courserecord; * @see get_courserecord() * @var stdClass */ protected $courserecord = null; /** * Context object gets set and retrieved by directly calling $lesson->context; * @see get_context() * @var stdClass */ protected $context = null; /** * Constructor method * * @param object $properties * @param stdClass $cm course module object * @param stdClass $course course object * @since Moodle 3.3 */ public function __construct($properties, $cm = null, $course = null) { parent::__construct($properties); $this->cm = $cm; $this->courserecord = $course; } /** * Simply generates a lesson object given an array/object of properties * Overrides {@see lesson_base->create()} * @static * @param object|array $properties * @return lesson */ public static function create($properties) { return new lesson($properties); } /** * Generates a lesson object from the database given its id * @static * @param int $lessonid * @return lesson */ public static function load($lessonid) { global $DB; if (!$lesson = $DB->get_record('lesson', array('id' => $lessonid))) { throw new \moodle_exception('invalidcoursemodule'); } return new lesson($lesson); } /** * Deletes this lesson from the database */ public function delete() { global $CFG, $DB; require_once($CFG->libdir.'/gradelib.php'); require_once($CFG->dirroot.'/calendar/lib.php'); $cm = get_coursemodule_from_instance('lesson', $this->properties->id, $this->properties->course); $context = context_module::instance($cm->id); $this->delete_all_overrides(); grade_update('mod/lesson', $this->properties->course, 'mod', 'lesson', $this->properties->id, 0, null, array('deleted'=>1)); // We must delete the module record after we delete the grade item. $DB->delete_records("lesson", array("id"=>$this->properties->id)); $DB->delete_records("lesson_pages", array("lessonid"=>$this->properties->id)); $DB->delete_records("lesson_answers", array("lessonid"=>$this->properties->id)); $DB->delete_records("lesson_attempts", array("lessonid"=>$this->properties->id)); $DB->delete_records("lesson_grades", array("lessonid"=>$this->properties->id)); $DB->delete_records("lesson_timer", array("lessonid"=>$this->properties->id)); $DB->delete_records("lesson_branch", array("lessonid"=>$this->properties->id)); if ($events = $DB->get_records('event', array("modulename"=>'lesson', "instance"=>$this->properties->id))) { $coursecontext = context_course::instance($cm->course); foreach($events as $event) { $event->context = $coursecontext; $event = calendar_event::load($event); $event->delete(); } } // Delete files associated with this module. $fs = get_file_storage(); $fs->delete_area_files($context->id); return true; } /** * Deletes a lesson override from the database and clears any corresponding calendar events * * @param int $overrideid The id of the override being deleted * @return bool true on success */ public function delete_override($overrideid) { global $CFG, $DB; require_once($CFG->dirroot . '/calendar/lib.php'); $cm = get_coursemodule_from_instance('lesson', $this->properties->id, $this->properties->course); $override = $DB->get_record('lesson_overrides', array('id' => $overrideid), '*', MUST_EXIST); // Delete the events. $conds = array('modulename' => 'lesson', 'instance' => $this->properties->id); if (isset($override->userid)) { $conds['userid'] = $override->userid; $cachekey = "{$cm->instance}_u_{$override->userid}"; } else { $conds['groupid'] = $override->groupid; $cachekey = "{$cm->instance}_g_{$override->groupid}"; } $events = $DB->get_records('event', $conds); foreach ($events as $event) { $eventold = calendar_event::load($event); $eventold->delete(); } $DB->delete_records('lesson_overrides', array('id' => $overrideid)); cache::make('mod_lesson', 'overrides')->delete($cachekey); // Set the common parameters for one of the events we will be triggering. $params = array( 'objectid' => $override->id, 'context' => context_module::instance($cm->id), 'other' => array( 'lessonid' => $override->lessonid ) ); // Determine which override deleted event to fire. if (!empty($override->userid)) { $params['relateduserid'] = $override->userid; $event = \mod_lesson\event\user_override_deleted::create($params); } else { $params['other']['groupid'] = $override->groupid; $event = \mod_lesson\event\group_override_deleted::create($params); } // Trigger the override deleted event. $event->add_record_snapshot('lesson_overrides', $override); $event->trigger(); return true; } /** * Deletes all lesson overrides from the database and clears any corresponding calendar events */ public function delete_all_overrides() { global $DB; $overrides = $DB->get_records('lesson_overrides', array('lessonid' => $this->properties->id), 'id'); foreach ($overrides as $override) { $this->delete_override($override->id); } } /** * Checks user enrollment in the current course. * * @param int $userid * @return null|stdClass user record */ public function is_participant($userid) { return is_enrolled($this->get_context(), $userid, 'mod/lesson:view', $this->show_only_active_users()); } /** * Check is only active users in course should be shown. * * @return bool true if only active users should be shown. */ public function show_only_active_users() { return !has_capability('moodle/course:viewsuspendedusers', $this->get_context()); } /** * Updates the lesson properties with override information for a user. * * Algorithm: For each lesson setting, if there is a matching user-specific override, * then use that otherwise, if there are group-specific overrides, return the most * lenient combination of them. If neither applies, leave the quiz setting unchanged. * * Special case: if there is more than one password that applies to the user, then * lesson->extrapasswords will contain an array of strings giving the remaining * passwords. * * @param int $userid The userid. */ public function update_effective_access($userid) { global $DB; // Check for user override. $override = $DB->get_record('lesson_overrides', array('lessonid' => $this->properties->id, 'userid' => $userid)); if (!$override) { $override = new stdClass(); $override->available = null; $override->deadline = null; $override->timelimit = null; $override->review = null; $override->maxattempts = null; $override->retake = null; $override->password = null; } // Check for group overrides. $groupings = groups_get_user_groups($this->properties->course, $userid); if (!empty($groupings[0])) { // Select all overrides that apply to the User's groups. list($extra, $params) = $DB->get_in_or_equal(array_values($groupings[0])); $sql = "SELECT * FROM {lesson_overrides} WHERE groupid $extra AND lessonid = ?"; $params[] = $this->properties->id; $records = $DB->get_records_sql($sql, $params); // Combine the overrides. $availables = array(); $deadlines = array(); $timelimits = array(); $reviews = array(); $attempts = array(); $retakes = array(); $passwords = array(); foreach ($records as $gpoverride) { if (isset($gpoverride->available)) { $availables[] = $gpoverride->available; } if (isset($gpoverride->deadline)) { $deadlines[] = $gpoverride->deadline; } if (isset($gpoverride->timelimit)) { $timelimits[] = $gpoverride->timelimit; } if (isset($gpoverride->review)) { $reviews[] = $gpoverride->review; } if (isset($gpoverride->maxattempts)) { $attempts[] = $gpoverride->maxattempts; } if (isset($gpoverride->retake)) { $retakes[] = $gpoverride->retake; } if (isset($gpoverride->password)) { $passwords[] = $gpoverride->password; } } // If there is a user override for a setting, ignore the group override. if (is_null($override->available) && count($availables)) { $override->available = min($availables); } if (is_null($override->deadline) && count($deadlines)) { if (in_array(0, $deadlines)) { $override->deadline = 0; } else { $override->deadline = max($deadlines); } } if (is_null($override->timelimit) && count($timelimits)) { if (in_array(0, $timelimits)) { $override->timelimit = 0; } else { $override->timelimit = max($timelimits); } } if (is_null($override->review) && count($reviews)) { $override->review = max($reviews); } if (is_null($override->maxattempts) && count($attempts)) { $override->maxattempts = max($attempts); } if (is_null($override->retake) && count($retakes)) { $override->retake = max($retakes); } if (is_null($override->password) && count($passwords)) { $override->password = array_shift($passwords); if (count($passwords)) { $override->extrapasswords = $passwords; } } } // Merge with lesson defaults. $keys = array('available', 'deadline', 'timelimit', 'maxattempts', 'review', 'retake'); foreach ($keys as $key) { if (isset($override->{$key})) { $this->properties->{$key} = $override->{$key}; } } // Special handling of lesson usepassword and password. if (isset($override->password)) { if ($override->password == '') { $this->properties->usepassword = 0; } else { $this->properties->usepassword = 1; $this->properties->password = $override->password; if (isset($override->extrapasswords)) { $this->properties->extrapasswords = $override->extrapasswords; } } } } /** * Fetches messages from the session that may have been set in previous page * actions. * * * // Do not call this method directly instead use * $lesson->messages; * * * @return array */ protected function get_messages() { global $SESSION; $messages = array(); if (!empty($SESSION->lesson_messages) && is_array($SESSION->lesson_messages) && array_key_exists($this->properties->id, $SESSION->lesson_messages)) { $messages = $SESSION->lesson_messages[$this->properties->id]; unset($SESSION->lesson_messages[$this->properties->id]); } return $messages; } /** * Get all of the attempts for the current user. * * @param int $retries * @param bool $correct Optional: only fetch correct attempts * @param int $pageid Optional: only fetch attempts at the given page * @param int $userid Optional: defaults to the current user if not set * @return array|false */ public function get_attempts($retries, $correct=false, $pageid=null, $userid=null) { global $USER, $DB; $params = array("lessonid"=>$this->properties->id, "userid"=>$userid, "retry"=>$retries); if ($correct) { $params['correct'] = 1; } if ($pageid !== null) { $params['pageid'] = $pageid; } if ($userid === null) { $params['userid'] = $USER->id; } return $DB->get_records('lesson_attempts', $params, 'timeseen ASC'); } /** * Get a list of content pages (formerly known as branch tables) viewed in the lesson for the given user during an attempt. * * @param int $lessonattempt the lesson attempt number (also known as retries) * @param int $userid the user id to retrieve the data from * @param string $sort an order to sort the results in (a valid SQL ORDER BY parameter) * @param string $fields a comma separated list of fields to return * @return array of pages * @since Moodle 3.3 */ public function get_content_pages_viewed($lessonattempt, $userid = null, $sort = '', $fields = '*') { global $USER, $DB; if ($userid === null) { $userid = $USER->id; } $conditions = array("lessonid" => $this->properties->id, "userid" => $userid, "retry" => $lessonattempt); return $DB->get_records('lesson_branch', $conditions, $sort, $fields); } /** * Returns the first page for the lesson or false if there isn't one. * * This method should be called via the magic method __get(); * * $firstpage = $lesson->firstpage; * * * @return lesson_page|bool Returns the lesson_page specialised object or false */ protected function get_firstpage() { $pages = $this->load_all_pages(); if (count($pages) > 0) { foreach ($pages as $page) { if ((int)$page->prevpageid === 0) { return $page; } } } return false; } /** * Returns the last page for the lesson or false if there isn't one. * * This method should be called via the magic method __get(); * * $lastpage = $lesson->lastpage; * * * @return lesson_page|bool Returns the lesson_page specialised object or false */ protected function get_lastpage() { $pages = $this->load_all_pages(); if (count($pages) > 0) { foreach ($pages as $page) { if ((int)$page->nextpageid === 0) { return $page; } } } return false; } /** * Returns the id of the first page of this lesson. (prevpageid = 0) * @return int */ protected function get_firstpageid() { global $DB; if ($this->firstpageid == null) { if (!$this->loadedallpages) { $firstpageid = $DB->get_field('lesson_pages', 'id', array('lessonid'=>$this->properties->id, 'prevpageid'=>0)); if (!$firstpageid) { throw new \moodle_exception('cannotfindfirstpage', 'lesson'); } $this->firstpageid = $firstpageid; } else { $firstpage = $this->get_firstpage(); $this->firstpageid = $firstpage->id; } } return $this->firstpageid; } /** * Returns the id of the last page of this lesson. (nextpageid = 0) * @return int */ public function get_lastpageid() { global $DB; if ($this->lastpageid == null) { if (!$this->loadedallpages) { $lastpageid = $DB->get_field('lesson_pages', 'id', array('lessonid'=>$this->properties->id, 'nextpageid'=>0)); if (!$lastpageid) { throw new \moodle_exception('cannotfindlastpage', 'lesson'); } $this->lastpageid = $lastpageid; } else { $lastpageid = $this->get_lastpage(); $this->lastpageid = $lastpageid->id; } } return $this->lastpageid; } /** * Gets the next page id to display after the one that is provided. * @param int $nextpageid * @return bool */ public function get_next_page($nextpageid) { global $USER, $DB; $allpages = $this->load_all_pages(); if ($this->properties->nextpagedefault) { // in Flash Card mode...first get number of retakes $nretakes = $DB->count_records("lesson_grades", array("lessonid" => $this->properties->id, "userid" => $USER->id)); shuffle($allpages); $found = false; if ($this->properties->nextpagedefault == LESSON_UNSEENPAGE) { foreach ($allpages as $nextpage) { if (!$DB->count_records("lesson_attempts", array("pageid" => $nextpage->id, "userid" => $USER->id, "retry" => $nretakes))) { $found = true; break; } } } elseif ($this->properties->nextpagedefault == LESSON_UNANSWEREDPAGE) { foreach ($allpages as $nextpage) { if (!$DB->count_records("lesson_attempts", array('pageid' => $nextpage->id, 'userid' => $USER->id, 'correct' => 1, 'retry' => $nretakes))) { $found = true; break; } } } if ($found) { if ($this->properties->maxpages) { // check number of pages viewed (in the lesson) if ($DB->count_records("lesson_attempts", array("lessonid" => $this->properties->id, "userid" => $USER->id, "retry" => $nretakes)) >= $this->properties->maxpages) { return LESSON_EOL; } } return $nextpage->id; } } // In a normal lesson mode foreach ($allpages as $nextpage) { if ((int)$nextpage->id === (int)$nextpageid) { return $nextpage->id; } } return LESSON_EOL; } /** * Sets a message against the session for this lesson that will displayed next * time the lesson processes messages * * @param string $message * @param string $class * @param string $align * @return bool */ public function add_message($message, $class="notifyproblem", $align='center') { global $SESSION; if (empty($SESSION->lesson_messages) || !is_array($SESSION->lesson_messages)) { $SESSION->lesson_messages = array(); $SESSION->lesson_messages[$this->properties->id] = array(); } else if (!array_key_exists($this->properties->id, $SESSION->lesson_messages)) { $SESSION->lesson_messages[$this->properties->id] = array(); } $SESSION->lesson_messages[$this->properties->id][] = array($message, $class, $align); return true; } /** * Check if the lesson is accessible at the present time * @return bool True if the lesson is accessible, false otherwise */ public function is_accessible() { $available = $this->properties->available; $deadline = $this->properties->deadline; return (($available == 0 || time() >= $available) && ($deadline == 0 || time() < $deadline)); } /** * Starts the lesson time for the current user * @return bool Returns true */ public function start_timer() { global $USER, $DB; $cm = get_coursemodule_from_instance('lesson', $this->properties()->id, $this->properties()->course, false, MUST_EXIST); // Trigger lesson started event. $event = \mod_lesson\event\lesson_started::create(array( 'objectid' => $this->properties()->id, 'context' => context_module::instance($cm->id), 'courseid' => $this->properties()->course )); $event->trigger(); $USER->startlesson[$this->properties->id] = true; $timenow = time(); $startlesson = new stdClass; $startlesson->lessonid = $this->properties->id; $startlesson->userid = $USER->id; $startlesson->starttime = $timenow; $startlesson->lessontime = $timenow; if (WS_SERVER) { $startlesson->timemodifiedoffline = $timenow; } $DB->insert_record('lesson_timer', $startlesson); if ($this->properties->timelimit) { $this->add_message(get_string('timelimitwarning', 'lesson', format_time($this->properties->timelimit)), 'center'); } return true; } /** * Updates the timer to the current time and returns the new timer object * @param bool $restart If set to true the timer is restarted * @param bool $continue If set to true AND $restart=true then the timer * will continue from a previous attempt * @return stdClass The new timer */ public function update_timer($restart=false, $continue=false, $endreached =false) { global $USER, $DB; $cm = get_coursemodule_from_instance('lesson', $this->properties->id, $this->properties->course); // clock code // get time information for this user if (!$timer = $this->get_user_timers($USER->id, 'starttime DESC', '*', 0, 1)) { $this->start_timer(); $timer = $this->get_user_timers($USER->id, 'starttime DESC', '*', 0, 1); } $timer = current($timer); // This will get the latest start time record. if ($restart) { if ($continue) { // continue a previous test, need to update the clock (think this option is disabled atm) $timer->starttime = time() - ($timer->lessontime - $timer->starttime); // Trigger lesson resumed event. $event = \mod_lesson\event\lesson_resumed::create(array( 'objectid' => $this->properties->id, 'context' => context_module::instance($cm->id), 'courseid' => $this->properties->course )); $event->trigger(); } else { // starting over, so reset the clock $timer->starttime = time(); // Trigger lesson restarted event. $event = \mod_lesson\event\lesson_restarted::create(array( 'objectid' => $this->properties->id, 'context' => context_module::instance($cm->id), 'courseid' => $this->properties->course )); $event->trigger(); } } $timenow = time(); $timer->lessontime = $timenow; if (WS_SERVER) { $timer->timemodifiedoffline = $timenow; } $timer->completed = $endreached; $DB->update_record('lesson_timer', $timer); // Update completion state. $cm = get_coursemodule_from_instance('lesson', $this->properties()->id, $this->properties()->course, false, MUST_EXIST); $course = get_course($cm->course); $completion = new completion_info($course); if ($completion->is_enabled($cm) && $this->properties()->completiontimespent > 0) { $completion->update_state($cm, COMPLETION_COMPLETE); } return $timer; } /** * Updates the timer to the current time then stops it by unsetting the user var * @return bool Returns true */ public function stop_timer() { global $USER, $DB; unset($USER->startlesson[$this->properties->id]); $cm = get_coursemodule_from_instance('lesson', $this->properties()->id, $this->properties()->course, false, MUST_EXIST); // Trigger lesson ended event. $event = \mod_lesson\event\lesson_ended::create(array( 'objectid' => $this->properties()->id, 'context' => context_module::instance($cm->id), 'courseid' => $this->properties()->course )); $event->trigger(); return $this->update_timer(false, false, true); } /** * Checks to see if the lesson has pages */ public function has_pages() { global $DB; $pagecount = $DB->count_records('lesson_pages', array('lessonid'=>$this->properties->id)); return ($pagecount>0); } /** * Returns the link for the related activity * @return string */ public function link_for_activitylink() { global $DB; $module = $DB->get_record('course_modules', array('id' => $this->properties->activitylink)); if ($module) { $modname = $DB->get_field('modules', 'name', array('id' => $module->module)); if ($modname) { $instancename = $DB->get_field($modname, 'name', array('id' => $module->instance)); if ($instancename) { return html_writer::link( new moodle_url('/mod/'.$modname.'/view.php', [ 'id' => $this->properties->activitylink, ]), get_string( 'activitylinkname', 'lesson', format_string($instancename, true, ['context' => $this->get_context()]), ), ['class' => 'centerpadded lessonbutton standardbutton pe-3'], ); } } } return ''; } /** * Loads the requested page. * * This function will return the requested page id as either a specialised * lesson_page object OR as a generic lesson_page. * If the page has been loaded previously it will be returned from the pages * array, otherwise it will be loaded from the database first * * @param int $pageid * @return lesson_page A lesson_page object or an object that extends it */ public function load_page($pageid) { if (!array_key_exists($pageid, $this->pages)) { $manager = lesson_page_type_manager::get($this); $this->pages[$pageid] = $manager->load_page($pageid, $this); } return $this->pages[$pageid]; } /** * Loads ALL of the pages for this lesson * * @return array An array containing all pages from this lesson */ public function load_all_pages() { if (!$this->loadedallpages) { $manager = lesson_page_type_manager::get($this); $this->pages = $manager->load_all_pages($this); $this->loadedallpages = true; } return $this->pages; } /** * Duplicate the lesson page. * * @param int $pageid Page ID of the page to duplicate. * @return void. */ public function duplicate_page($pageid) { global $PAGE; $cm = get_coursemodule_from_instance('lesson', $this->properties->id, $this->properties->course); $context = context_module::instance($cm->id); // Load the page. $page = $this->load_page($pageid); $properties = $page->properties(); // The create method checks to see if these properties are set and if not sets them to zero, hence the unsetting here. if (!$properties->qoption) { unset($properties->qoption); } if (!$properties->layout) { unset($properties->layout); } if (!$properties->display) { unset($properties->display); } $properties->pageid = $pageid; // Add text and format into the format required to create a new page. $properties->contents_editor = array( 'text' => $properties->contents, 'format' => $properties->contentsformat ); $answers = $page->get_answers(); // Answers need to be added to $properties. $i = 0; $answerids = array(); foreach ($answers as $answer) { // Needs to be rearranged to work with the create function. $properties->answer_editor[$i] = array( 'text' => $answer->answer, 'format' => $answer->answerformat ); $properties->response_editor[$i] = array( 'text' => $answer->response, 'format' => $answer->responseformat ); $answerids[] = $answer->id; $properties->jumpto[$i] = $answer->jumpto; $properties->score[$i] = $answer->score; $i++; } // Create the duplicate page. $newlessonpage = lesson_page::create($properties, $this, $context, $PAGE->course->maxbytes); $newanswers = $newlessonpage->get_answers(); // Copy over the file areas as well. $this->copy_page_files('page_contents', $pageid, $newlessonpage->id, $context->id); $j = 0; foreach ($newanswers as $answer) { if (isset($answer->answer) && strpos($answer->answer, '@@PLUGINFILE@@') !== false) { $this->copy_page_files('page_answers', $answerids[$j], $answer->id, $context->id); } if (isset($answer->response) && !is_array($answer->response) && strpos($answer->response, '@@PLUGINFILE@@') !== false) { $this->copy_page_files('page_responses', $answerids[$j], $answer->id, $context->id); } $j++; } } /** * Copy the files from one page to another. * * @param string $filearea Area that the files are stored. * @param int $itemid Item ID. * @param int $newitemid The item ID for the new page. * @param int $contextid Context ID for this page. * @return void. */ protected function copy_page_files($filearea, $itemid, $newitemid, $contextid) { $fs = get_file_storage(); $files = $fs->get_area_files($contextid, 'mod_lesson', $filearea, $itemid); foreach ($files as $file) { $fieldupdates = array('itemid' => $newitemid); $fs->create_file_from_storedfile($fieldupdates, $file); } } /** * Determines if a jumpto value is correct or not. * * returns true if jumpto page is (logically) after the pageid page or * if the jumpto value is a special value. Returns false in all other cases. * * @param int $pageid Id of the page from which you are jumping from. * @param int $jumpto The jumpto number. * @return boolean True or false after a series of tests. **/ public function jumpto_is_correct($pageid, $jumpto) { global $DB; // first test the special values if (!$jumpto) { // same page return false; } elseif ($jumpto == LESSON_NEXTPAGE) { return true; } elseif ($jumpto == LESSON_UNSEENBRANCHPAGE) { return true; } elseif ($jumpto == LESSON_RANDOMPAGE) { return true; } elseif ($jumpto == LESSON_CLUSTERJUMP) { return true; } elseif ($jumpto == LESSON_EOL) { return true; } $pages = $this->load_all_pages(); $apageid = $pages[$pageid]->nextpageid; while ($apageid != 0) { if ($jumpto == $apageid) { return true; } $apageid = $pages[$apageid]->nextpageid; } return false; } /** * Returns the time a user has remaining on this lesson * @param int $starttime Starttime timestamp * @return string */ public function time_remaining($starttime) { $timeleft = $starttime + $this->properties->timelimit - time(); $hours = floor($timeleft/3600); $timeleft = $timeleft - ($hours * 3600); $minutes = floor($timeleft/60); $secs = $timeleft - ($minutes * 60); if ($minutes < 10) { $minutes = "0$minutes"; } if ($secs < 10) { $secs = "0$secs"; } $output = array(); $output[] = $hours; $output[] = $minutes; $output[] = $secs; $output = implode(':', $output); return $output; } /** * Interprets LESSON_CLUSTERJUMP jumpto value. * * This will select a page randomly * and the page selected will be inbetween a cluster page and end of clutter or end of lesson * and the page selected will be a page that has not been viewed already * and if any pages are within a branch table or end of branch then only 1 page within * the branch table or end of branch will be randomly selected (sub clustering). * * @param int $pageid Id of the current page from which we are jumping from. * @param int $userid Id of the user. * @return int The id of the next page. **/ public function cluster_jump($pageid, $userid=null) { global $DB, $USER; if ($userid===null) { $userid = $USER->id; } // get the number of retakes if (!$retakes = $DB->count_records("lesson_grades", array("lessonid"=>$this->properties->id, "userid"=>$userid))) { $retakes = 0; } // get all the lesson_attempts aka what the user has seen $seenpages = array(); if ($attempts = $this->get_attempts($retakes)) { foreach ($attempts as $attempt) { $seenpages[$attempt->pageid] = $attempt->pageid; } } // get the lesson pages $lessonpages = $this->load_all_pages(); // find the start of the cluster while ($pageid != 0) { // this condition should not be satisfied... should be a cluster page if ($lessonpages[$pageid]->qtype == LESSON_PAGE_CLUSTER) { break; } $pageid = $lessonpages[$pageid]->prevpageid; } $clusterpages = array(); $clusterpages = $this->get_sub_pages_of($pageid, array(LESSON_PAGE_ENDOFCLUSTER)); $unseen = array(); foreach ($clusterpages as $key=>$cluster) { // Remove the page if it is in a branch table or is an endofbranch. if ($this->is_sub_page_of_type($cluster->id, array(LESSON_PAGE_BRANCHTABLE), array(LESSON_PAGE_ENDOFBRANCH, LESSON_PAGE_CLUSTER)) || $cluster->qtype == LESSON_PAGE_ENDOFBRANCH) { unset($clusterpages[$key]); } else if ($cluster->qtype == LESSON_PAGE_BRANCHTABLE) { // If branchtable, check to see if any pages inside have been viewed. $branchpages = $this->get_sub_pages_of($cluster->id, array(LESSON_PAGE_BRANCHTABLE, LESSON_PAGE_ENDOFBRANCH)); $flag = true; foreach ($branchpages as $branchpage) { if (array_key_exists($branchpage->id, $seenpages)) { // Check if any of the pages have been viewed. $flag = false; } } if ($flag && count($branchpages) > 0) { // Add branch table. $unseen[] = $cluster; } } elseif ($cluster->is_unseen($seenpages)) { $unseen[] = $cluster; } } if (count($unseen) > 0) { // it does not contain elements, then use exitjump, otherwise find out next page/branch $nextpage = $unseen[rand(0, count($unseen)-1)]; if ($nextpage->qtype == LESSON_PAGE_BRANCHTABLE) { // if branch table, then pick a random page inside of it $branchpages = $this->get_sub_pages_of($nextpage->id, array(LESSON_PAGE_BRANCHTABLE, LESSON_PAGE_ENDOFBRANCH)); return $branchpages[rand(0, count($branchpages)-1)]->id; } else { // otherwise, return the page's id return $nextpage->id; } } else { // seen all there is to see, leave the cluster if (end($clusterpages)->nextpageid == 0) { return LESSON_EOL; } else { $clusterendid = $pageid; while ($clusterendid != 0) { // This condition should not be satisfied... should be an end of cluster page. if ($lessonpages[$clusterendid]->qtype == LESSON_PAGE_ENDOFCLUSTER) { break; } $clusterendid = $lessonpages[$clusterendid]->nextpageid; } $exitjump = $DB->get_field("lesson_answers", "jumpto", array("pageid" => $clusterendid, "lessonid" => $this->properties->id)); if ($exitjump == LESSON_NEXTPAGE) { $exitjump = $lessonpages[$clusterendid]->nextpageid; } if ($exitjump == 0) { return LESSON_EOL; } else if (in_array($exitjump, array(LESSON_EOL, LESSON_PREVIOUSPAGE))) { return $exitjump; } else { if (!array_key_exists($exitjump, $lessonpages)) { $found = false; foreach ($lessonpages as $page) { if ($page->id === $clusterendid) { $found = true; } else if ($page->qtype == LESSON_PAGE_ENDOFCLUSTER) { $exitjump = $DB->get_field("lesson_answers", "jumpto", array("pageid" => $page->id, "lessonid" => $this->properties->id)); if ($exitjump == LESSON_NEXTPAGE) { $exitjump = $lessonpages[$page->id]->nextpageid; } break; } } } if (!array_key_exists($exitjump, $lessonpages)) { return LESSON_EOL; } // Check to see that the return type is not a cluster. if ($lessonpages[$exitjump]->qtype == LESSON_PAGE_CLUSTER) { // If the exitjump is a cluster then go through this function again and try to find an unseen question. $exitjump = $this->cluster_jump($exitjump, $userid); } return $exitjump; } } } } /** * Finds all pages that appear to be a subtype of the provided pageid until * an end point specified within $ends is encountered or no more pages exist * * @param int $pageid * @param array $ends An array of LESSON_PAGE_* types that signify an end of * the subtype * @return array An array of specialised lesson_page objects */ public function get_sub_pages_of($pageid, array $ends) { $lessonpages = $this->load_all_pages(); $pageid = $lessonpages[$pageid]->nextpageid; // move to the first page after the branch table $pages = array(); while (true) { if ($pageid == 0 || in_array($lessonpages[$pageid]->qtype, $ends)) { break; } $pages[] = $lessonpages[$pageid]; $pageid = $lessonpages[$pageid]->nextpageid; } return $pages; } /** * Checks to see if the specified page[id] is a subpage of a type specified in * the $types array, until either there are no more pages of we find a type * corresponding to that of a type specified in $ends * * @param int $pageid The id of the page to check * @param array $types An array of types that would signify this page was a subpage * @param array $ends An array of types that mean this is not a subpage * @return bool */ public function is_sub_page_of_type($pageid, array $types, array $ends) { $pages = $this->load_all_pages(); $pageid = $pages[$pageid]->prevpageid; // move up one array_unshift($ends, 0); // go up the pages till branch table while (true) { if ($pageid==0 || in_array($pages[$pageid]->qtype, $ends)) { return false; } else if (in_array($pages[$pageid]->qtype, $types)) { return true; } $pageid = $pages[$pageid]->prevpageid; } } /** * Move a page resorting all other pages. * * @param int $pageid * @param int $after * @return void */ public function resort_pages($pageid, $after) { global $CFG; $cm = get_coursemodule_from_instance('lesson', $this->properties->id, $this->properties->course); $context = context_module::instance($cm->id); $pages = $this->load_all_pages(); if (!array_key_exists($pageid, $pages) || ($after!=0 && !array_key_exists($after, $pages))) { throw new \moodle_exception('cannotfindpages', 'lesson', "$CFG->wwwroot/mod/lesson/edit.php?id=$cm->id"); } $pagetomove = clone($pages[$pageid]); unset($pages[$pageid]); $pageids = array(); if ($after === 0) { $pageids['p0'] = $pageid; } foreach ($pages as $page) { $pageids[] = $page->id; if ($page->id == $after) { $pageids[] = $pageid; } } $pageidsref = $pageids; reset($pageidsref); $prev = 0; $next = next($pageidsref); foreach ($pageids as $pid) { if ($pid === $pageid) { $page = $pagetomove; } else { $page = $pages[$pid]; } if ($page->prevpageid != $prev || $page->nextpageid != $next) { $page->move($next, $prev); if ($pid === $pageid) { // We will trigger an event. $pageupdated = array('next' => $next, 'prev' => $prev); } } $prev = $page->id; $next = next($pageidsref); if (!$next) { $next = 0; } } // Trigger an event: page moved. if (!empty($pageupdated)) { $eventparams = array( 'context' => $context, 'objectid' => $pageid, 'other' => array( 'pagetype' => $page->get_typestring(), 'prevpageid' => $pageupdated['prev'], 'nextpageid' => $pageupdated['next'] ) ); $event = \mod_lesson\event\page_moved::create($eventparams); $event->trigger(); } } /** * Return the lesson context object. * * @return stdClass context * @since Moodle 3.3 */ public function get_context() { if ($this->context == null) { $this->context = context_module::instance($this->get_cm()->id); } return $this->context; } /** * Set the lesson course module object. * * @param stdClass $cm course module objct * @since Moodle 3.3 */ private function set_cm($cm) { $this->cm = $cm; } /** * Return the lesson course module object. * * @return stdClass course module * @since Moodle 3.3 */ public function get_cm() { if ($this->cm == null) { $this->cm = get_coursemodule_from_instance('lesson', $this->properties->id); } return $this->cm; } /** * Set the lesson course object. * * @param stdClass $course course objct * @since Moodle 3.3 */ private function set_courserecord($course) { $this->courserecord = $course; } /** * Return the lesson course object. * * @return stdClass course * @since Moodle 3.3 */ public function get_courserecord() { global $DB; if ($this->courserecord == null) { $this->courserecord = $DB->get_record('course', array('id' => $this->properties->course)); } return $this->courserecord; } /** * Check if the user can manage the lesson activity. * * @return bool true if the user can manage the lesson * @since Moodle 3.3 */ public function can_manage() { return has_capability('mod/lesson:manage', $this->get_context()); } /** * Check if time restriction is applied. * * @return mixed false if there aren't restrictions or an object with the restriction information * @since Moodle 3.3 */ public function get_time_restriction_status() { if ($this->can_manage()) { return false; } if (!$this->is_accessible()) { if ($this->properties->deadline != 0 && time() > $this->properties->deadline) { $status = ['reason' => 'lessonclosed', 'time' => $this->properties->deadline]; } else { $status = ['reason' => 'lessonopen', 'time' => $this->properties->available]; } return (object) $status; } return false; } /** * Check if password restriction is applied. * * @param string $userpassword the user password to check (if the restriction is set) * @return mixed false if there aren't restrictions or an object with the restriction information * @since Moodle 3.3 */ public function get_password_restriction_status($userpassword) { global $USER; if ($this->can_manage()) { return false; } if ($this->properties->usepassword && empty($USER->lessonloggedin[$this->id])) { $correctpass = false; $userpassword = trim((string) $userpassword); if ($userpassword !== '' && ($this->properties->password === md5($userpassword) || $this->properties->password === $userpassword)) { // With or without md5 for backward compatibility (MDL-11090). $correctpass = true; $USER->lessonloggedin[$this->id] = true; } else if (isset($this->properties->extrapasswords)) { // Group overrides may have additional passwords. foreach ($this->properties->extrapasswords as $password) { if ($password === md5($userpassword) || $password === $userpassword) { $correctpass = true; $USER->lessonloggedin[$this->id] = true; } } } return !$correctpass; } return false; } /** * Check if dependencies restrictions are applied. * * @return mixed false if there aren't restrictions or an object with the restriction information * @since Moodle 3.3 */ public function get_dependencies_restriction_status() { global $DB, $USER; if ($this->can_manage()) { return false; } if ($dependentlesson = $DB->get_record('lesson', array('id' => $this->properties->dependency))) { // Lesson exists, so we can proceed. $conditions = unserialize_object($this->properties->conditions); // Assume false for all. $errors = array(); // Check for the timespent condition. if (!empty($conditions->timespent)) { $timespent = false; if ($attempttimes = $DB->get_records('lesson_timer', array("userid" => $USER->id, "lessonid" => $dependentlesson->id))) { // Go through all the times and test to see if any of them satisfy the condition. foreach ($attempttimes as $attempttime) { $duration = $attempttime->lessontime - $attempttime->starttime; if ($conditions->timespent < $duration / 60) { $timespent = true; } } } if (!$timespent) { $errors[] = get_string('timespenterror', 'lesson', $conditions->timespent); } } // Check for the gradebetterthan condition. if (!empty($conditions->gradebetterthan)) { $gradebetterthan = false; if ($studentgrades = $DB->get_records('lesson_grades', array("userid" => $USER->id, "lessonid" => $dependentlesson->id))) { // Go through all the grades and test to see if any of them satisfy the condition. foreach ($studentgrades as $studentgrade) { if ($studentgrade->grade >= $conditions->gradebetterthan) { $gradebetterthan = true; } } } if (!$gradebetterthan) { $errors[] = get_string('gradebetterthanerror', 'lesson', $conditions->gradebetterthan); } } // Check for the completed condition. if (!empty($conditions->completed)) { if (!$DB->count_records('lesson_grades', array('userid' => $USER->id, 'lessonid' => $dependentlesson->id))) { $errors[] = get_string('completederror', 'lesson'); } } if (!empty($errors)) { return (object) ['errors' => $errors, 'dependentlesson' => $dependentlesson]; } } return false; } /** * Check if the lesson is in review mode. (The user already finished it and retakes are not allowed). * * @return bool true if is in review mode * @since Moodle 3.3 */ public function is_in_review_mode() { global $DB, $USER; $userhasgrade = $DB->count_records("lesson_grades", array("lessonid" => $this->properties->id, "userid" => $USER->id)); if ($userhasgrade && !$this->properties->retake) { return true; } return false; } /** * Return the last page the current user saw. * * @param int $retriescount the number of retries for the lesson (the last retry number). * @return mixed false if the user didn't see the lesson or the last page id */ public function get_last_page_seen($retriescount) { global $DB, $USER; $lastpageseen = false; $allattempts = $this->get_attempts($retriescount); if (!empty($allattempts)) { $attempt = end($allattempts); $attemptpage = $this->load_page($attempt->pageid); $jumpto = $DB->get_field('lesson_answers', 'jumpto', array('id' => $attempt->answerid)); // Convert the jumpto to a proper page id. if ($jumpto == 0) { // Check if a question has been incorrectly answered AND no more attempts at it are left. $nattempts = $this->get_attempts($attempt->retry, false, $attempt->pageid, $USER->id); if (count($nattempts) >= $this->properties->maxattempts) { $lastpageseen = $this->get_next_page($attemptpage->nextpageid); } else { $lastpageseen = $attempt->pageid; } } else if ($jumpto == LESSON_NEXTPAGE) { $lastpageseen = $this->get_next_page($attemptpage->nextpageid); } else if ($jumpto == LESSON_CLUSTERJUMP) { $lastpageseen = $this->cluster_jump($attempt->pageid); } else { $lastpageseen = $jumpto; } } if ($branchtables = $this->get_content_pages_viewed($retriescount, $USER->id, 'timeseen DESC')) { // In here, user has viewed a branch table. $lastbranchtable = current($branchtables); if (count($allattempts) > 0) { if ($lastbranchtable->timeseen > $attempt->timeseen) { // This branch table was viewed more recently than the question page. if (!empty($lastbranchtable->nextpageid)) { $lastpageseen = $lastbranchtable->nextpageid; } else { // Next page ID did not exist prior to MDL-34006. $lastpageseen = $lastbranchtable->pageid; } } } else { // Has not answered any questions but has viewed a branch table. if (!empty($lastbranchtable->nextpageid)) { $lastpageseen = $lastbranchtable->nextpageid; } else { // Next page ID did not exist prior to MDL-34006. $lastpageseen = $lastbranchtable->pageid; } } } return $lastpageseen; } /** * Return the number of retries in a lesson for a given user. * * @param int $userid the user id * @return int the retries count * @since Moodle 3.3 */ public function count_user_retries($userid) { global $DB; return $DB->count_records('lesson_grades', array("lessonid" => $this->properties->id, "userid" => $userid)); } /** * Check if a user left a timed session. * * @param int $retriescount the number of retries for the lesson (the last retry number). * @return true if the user left the timed session * @since Moodle 3.3 */ public function left_during_timed_session($retriescount) { global $DB, $USER; $conditions = array('lessonid' => $this->properties->id, 'userid' => $USER->id, 'retry' => $retriescount); return $DB->count_records('lesson_attempts', $conditions) > 0 || $DB->count_records('lesson_branch', $conditions) > 0; } /** * Trigger module viewed event and set the module viewed for completion. * * @since Moodle 3.3 */ public function set_module_viewed() { global $CFG; require_once($CFG->libdir . '/completionlib.php'); // Trigger module viewed event. $event = \mod_lesson\event\course_module_viewed::create(array( 'objectid' => $this->properties->id, 'context' => $this->get_context() )); $event->add_record_snapshot('course_modules', $this->get_cm()); $event->add_record_snapshot('course', $this->get_courserecord()); $event->trigger(); // Mark as viewed. $completion = new completion_info($this->get_courserecord()); $completion->set_module_viewed($this->get_cm()); } /** * Return the timers in the current lesson for the given user. * * @param int $userid the user id * @param string $sort an order to sort the results in (optional, a valid SQL ORDER BY parameter). * @param string $fields a comma separated list of fields to return * @param int $limitfrom return a subset of records, starting at this point (optional). * @param int $limitnum return a subset comprising this many records in total (optional, required if $limitfrom is set). * @return array list of timers for the given user in the lesson * @since Moodle 3.3 */ public function get_user_timers($userid = null, $sort = '', $fields = '*', $limitfrom = 0, $limitnum = 0) { global $DB, $USER; if ($userid === null) { $userid = $USER->id; } $params = array('lessonid' => $this->properties->id, 'userid' => $userid); return $DB->get_records('lesson_timer', $params, $sort, $fields, $limitfrom, $limitnum); } /** * Check if the user is out of time in a timed lesson. * * @param stdClass $timer timer object * @return bool True if the user is on time, false is the user ran out of time * @since Moodle 3.3 */ public function check_time($timer) { if ($this->properties->timelimit) { $timeleft = $timer->starttime + $this->properties->timelimit - time(); if ($timeleft <= 0) { // Out of time. $this->add_message(get_string('eolstudentoutoftime', 'lesson')); return false; } else if ($timeleft < 60) { // One minute warning. $this->add_message(get_string('studentoneminwarning', 'lesson')); } } return true; } /** * Add different informative messages to the given page. * * @param lesson_page $page page object * @param reviewmode $bool whether we are in review mode or not * @since Moodle 3.3 */ public function add_messages_on_page_view(lesson_page $page, $reviewmode) { global $DB, $USER; if (!$this->can_manage()) { if ($page->qtype == LESSON_PAGE_BRANCHTABLE && $this->properties->minquestions) { // Tell student how many questions they have seen, how many are required and their grade. $ntries = $DB->count_records("lesson_grades", array("lessonid" => $this->properties->id, "userid" => $USER->id)); $gradeinfo = lesson_grade($this, $ntries); if ($gradeinfo->attempts) { if ($gradeinfo->nquestions < $this->properties->minquestions) { $a = new stdClass; $a->nquestions = $gradeinfo->nquestions; $a->minquestions = $this->properties->minquestions; $this->add_message(get_string('numberofpagesviewednotice', 'lesson', $a)); } if (!$reviewmode && $this->properties->ongoing) { $this->add_message(get_string("numberofcorrectanswers", "lesson", $gradeinfo->earned), 'notify'); if ($this->properties->grade != GRADE_TYPE_NONE) { $a = new stdClass; $a->grade = format_float($gradeinfo->grade * $this->properties->grade / 100, 1); $a->total = $this->properties->grade; $this->add_message(get_string('yourcurrentgradeisoutof', 'lesson', $a), 'notify'); } } } } } else { if ($this->properties->timelimit) { $this->add_message(get_string('teachertimerwarning', 'lesson')); } if (lesson_display_teacher_warning($this)) { // This is the warning msg for teachers to inform them that cluster // and unseen does not work while logged in as a teacher. $warningvars = new stdClass(); $warningvars->cluster = get_string('clusterjump', 'lesson'); $warningvars->unseen = get_string('unseenpageinbranch', 'lesson'); $this->add_message(get_string('teacherjumpwarning', 'lesson', $warningvars)); } } } /** * Get the ongoing score message for the user (depending on the user permission and lesson settings). * * @return str the ongoing score message * @since Moodle 3.3 */ public function get_ongoing_score_message() { global $USER, $DB; $context = $this->get_context(); if (has_capability('mod/lesson:manage', $context)) { return get_string('teacherongoingwarning', 'lesson'); } else { $ntries = $DB->count_records("lesson_grades", array("lessonid" => $this->properties->id, "userid" => $USER->id)); if (isset($USER->modattempts[$this->properties->id])) { $ntries--; } $gradeinfo = lesson_grade($this, $ntries); $a = new stdClass; if ($this->properties->custom) { $a->score = $gradeinfo->earned; $a->currenthigh = $gradeinfo->total; return get_string("ongoingcustom", "lesson", $a); } else { $a->correct = $gradeinfo->earned; $a->viewed = $gradeinfo->attempts; return get_string("ongoingnormal", "lesson", $a); } } } /** * Calculate the progress of the current user in the lesson. * * @return int the progress (scale 0-100) * @since Moodle 3.3 */ public function calculate_progress() { global $USER, $DB; // Check if the user is reviewing the attempt. if (isset($USER->modattempts[$this->properties->id])) { return 100; } // All of the lesson pages. $pages = $this->load_all_pages(); foreach ($pages as $page) { if ($page->prevpageid == 0) { $pageid = $page->id; // Find the first page id. break; } } // Current attempt number. if (!$ntries = $DB->count_records("lesson_grades", array("lessonid" => $this->properties->id, "userid" => $USER->id))) { $ntries = 0; // May not be necessary. } $viewedpageids = array(); if ($attempts = $this->get_attempts($ntries, false)) { foreach ($attempts as $attempt) { $viewedpageids[$attempt->pageid] = $attempt; } } $viewedbranches = array(); // Collect all of the branch tables viewed. if ($branches = $this->get_content_pages_viewed($ntries, $USER->id, 'timeseen ASC', 'id, pageid')) { foreach ($branches as $branch) { $viewedbranches[$branch->pageid] = $branch; } $viewedpageids = array_merge($viewedpageids, $viewedbranches); } // Filter out the following pages: // - End of Cluster // - End of Branch // - Pages found inside of Clusters // Do not filter out Cluster Page(s) because we count a cluster as one. // By keeping the cluster page, we get our 1. $validpages = array(); while ($pageid != 0) { $pageid = $pages[$pageid]->valid_page_and_view($validpages, $viewedpageids); } // Progress calculation as a percent. $progress = round(count($viewedpageids) / count($validpages), 2) * 100; return (int) $progress; } /** * Calculate the correct page and prepare contents for a given page id (could be a page jump id). * * @param int $pageid the given page id * @param mod_lesson_renderer $lessonoutput the lesson output rendered * @param bool $reviewmode whether we are in review mode or not * @param bool $redirect Optional, default to true. Set to false to avoid redirection and return the page to redirect. * @return array the page object and contents * @throws moodle_exception * @since Moodle 3.3 */ public function prepare_page_and_contents($pageid, $lessonoutput, $reviewmode, $redirect = true) { global $USER, $CFG; $page = $this->load_page($pageid); // Check if the page is of a special type and if so take any nessecary action. $newpageid = $page->callback_on_view($this->can_manage(), $redirect); // Avoid redirections returning the jump to special page id. if (!$redirect && is_numeric($newpageid) && $newpageid < 0) { return array($newpageid, null, null); } if (is_numeric($newpageid)) { $page = $this->load_page($newpageid); } // Add different informative messages to the given page. $this->add_messages_on_page_view($page, $reviewmode); if (is_array($page->answers) && count($page->answers) > 0) { // This is for modattempts option. Find the users previous answer to this page, // and then display it below in answer processing. if (isset($USER->modattempts[$this->properties->id])) { $retries = $this->count_user_retries($USER->id); if (!$attempts = $this->get_attempts($retries - 1, false, $page->id)) { throw new moodle_exception('cannotfindpreattempt', 'lesson'); } $attempt = end($attempts); $USER->modattempts[$this->properties->id] = $attempt; } else { $attempt = false; } $lessoncontent = $lessonoutput->display_page($this, $page, $attempt); } else { require_once($CFG->dirroot . '/mod/lesson/view_form.php'); $data = new stdClass; $data->id = $this->get_cm()->id; $data->pageid = $page->id; $data->newpageid = $this->get_next_page($page->nextpageid); $customdata = array( 'title' => $page->title, 'contents' => $page->get_contents() ); $mform = new lesson_page_without_answers($CFG->wwwroot.'/mod/lesson/continue.php', $customdata); $mform->set_data($data); ob_start(); $mform->display(); $lessoncontent = ob_get_contents(); ob_end_clean(); } return array($page->id, $page, $lessoncontent); } /** * This returns a real page id to jump to (or LESSON_EOL) after processing page responses. * * @param lesson_page $page lesson page * @param int $newpageid the new page id * @return int the real page to jump to (or end of lesson) * @since Moodle 3.3 */ public function calculate_new_page_on_jump(lesson_page $page, $newpageid) { global $USER, $DB; $canmanage = $this->can_manage(); if (isset($USER->modattempts[$this->properties->id])) { // Make sure if the student is reviewing, that he/she sees the same pages/page path that he/she saw the first time. if ($USER->modattempts[$this->properties->id]->pageid == $page->id && $page->nextpageid == 0) { // Remember, this session variable holds the pageid of the last page that the user saw. $newpageid = LESSON_EOL; } else { $nretakes = $DB->count_records("lesson_grades", array("lessonid" => $this->properties->id, "userid" => $USER->id)); $nretakes--; // Make sure we are looking at the right try. $attempts = $DB->get_records("lesson_attempts", array("lessonid" => $this->properties->id, "userid" => $USER->id, "retry" => $nretakes), "timeseen", "id, pageid"); $found = false; $temppageid = 0; // Make sure that the newpageid always defaults to something valid. $newpageid = LESSON_EOL; foreach ($attempts as $attempt) { if ($found && $temppageid != $attempt->pageid) { // Now try to find the next page, make sure next few attempts do no belong to current page. $newpageid = $attempt->pageid; break; } if ($attempt->pageid == $page->id) { $found = true; // If found current page. $temppageid = $attempt->pageid; } } } } else if ($newpageid != LESSON_CLUSTERJUMP && $page->id != 0 && $newpageid > 0) { // Going to check to see if the page that the user is going to view next, is a cluster page. // If so, dont display, go into the cluster. // The $newpageid > 0 is used to filter out all of the negative code jumps. $newpage = $this->load_page($newpageid); if ($overridenewpageid = $newpage->override_next_page($newpageid)) { $newpageid = $overridenewpageid; } } else if ($newpageid == LESSON_UNSEENBRANCHPAGE) { if ($canmanage) { if ($page->nextpageid == 0) { $newpageid = LESSON_EOL; } else { $newpageid = $page->nextpageid; } } else { $newpageid = lesson_unseen_question_jump($this, $USER->id, $page->id); } } else if ($newpageid == LESSON_PREVIOUSPAGE) { $newpageid = $page->prevpageid; } else if ($newpageid == LESSON_RANDOMPAGE) { $newpageid = lesson_random_question_jump($this, $page->id); } else if ($newpageid == LESSON_CLUSTERJUMP) { if ($canmanage) { if ($page->nextpageid == 0) { // If teacher, go to next page. $newpageid = LESSON_EOL; } else { $newpageid = $page->nextpageid; } } else { $newpageid = $this->cluster_jump($page->id); } } else if ($newpageid == 0) { $newpageid = $page->id; } else if ($newpageid == LESSON_NEXTPAGE) { $newpageid = $this->get_next_page($page->nextpageid); } return $newpageid; } /** * Process page responses. * * @param lesson_page $page page object * @since Moodle 3.3 */ public function process_page_responses(lesson_page $page) { $context = $this->get_context(); // Check the page has answers [MDL-25632]. if (count($page->answers) > 0) { $result = $page->record_attempt($context); } else { // The page has no answers so we will just progress to the next page in the // sequence (as set by newpageid). $result = new stdClass; $result->newpageid = optional_param('newpageid', $page->nextpageid, PARAM_INT); $result->nodefaultresponse = true; $result->inmediatejump = false; } if ($result->inmediatejump) { return $result; } $result->newpageid = $this->calculate_new_page_on_jump($page, $result->newpageid); return $result; } /** * Add different informative messages to the given page. * * @param lesson_page $page page object * @param stdClass $result the page processing result object * @param bool $reviewmode whether we are in review mode or not * @since Moodle 3.3 */ public function add_messages_on_page_process(lesson_page $page, $result, $reviewmode) { if ($this->can_manage()) { // This is the warning msg for teachers to inform them that cluster and unseen does not work while logged in as a teacher. if (lesson_display_teacher_warning($this)) { $warningvars = new stdClass(); $warningvars->cluster = get_string("clusterjump", "lesson"); $warningvars->unseen = get_string("unseenpageinbranch", "lesson"); $this->add_message(get_string("teacherjumpwarning", "lesson", $warningvars)); } // Inform teacher that s/he will not see the timer. if ($this->properties->timelimit) { $this->add_message(get_string("teachertimerwarning", "lesson")); } } // Report attempts remaining. if ($result->attemptsremaining != 0 && $this->properties->review && !$reviewmode) { $this->add_message(get_string('attemptsremaining', 'lesson', $result->attemptsremaining)); } } /** * Process and return all the information for the end of lesson page. * * @param string $outoftime used to check to see if the student ran out of time * @return stdclass an object with all the page data ready for rendering * @since Moodle 3.3 */ public function process_eol_page($outoftime) { global $DB, $USER; $course = $this->get_courserecord(); $cm = $this->get_cm(); $canmanage = $this->can_manage(); // Init all the possible fields and values. $data = (object) array( 'gradelesson' => true, 'notenoughtimespent' => false, 'numberofpagesviewed' => false, 'youshouldview' => false, 'numberofcorrectanswers' => false, 'displayscorewithessays' => false, 'displayscorewithoutessays' => false, 'yourcurrentgradeisoutof' => false, 'yourcurrentgradeis' => false, 'eolstudentoutoftimenoanswers' => false, 'welldone' => false, 'progressbar' => false, 'displayofgrade' => false, 'reviewlesson' => false, 'modattemptsnoteacher' => false, 'activitylink' => false, 'progresscompleted' => false, ); $ntries = $DB->count_records("lesson_grades", array("lessonid" => $this->properties->id, "userid" => $USER->id)); if (isset($USER->modattempts[$this->properties->id])) { $ntries--; // Need to look at the old attempts :). } $gradeinfo = lesson_grade($this, $ntries); $data->gradeinfo = $gradeinfo; if ($this->properties->custom && !$canmanage) { // Before we calculate the custom score make sure they answered the minimum // number of questions. We only need to do this for custom scoring as we can // not get the miniumum score the user should achieve. If we are not using // custom scoring (so all questions are valued as 1) then we simply check if // they answered more than the minimum questions, if not, we mark it out of the // number specified in the minimum questions setting - which is done in lesson_grade(). // Get the number of answers given. if ($gradeinfo->nquestions < $this->properties->minquestions) { $data->gradelesson = false; $a = new stdClass; $a->nquestions = $gradeinfo->nquestions; $a->minquestions = $this->properties->minquestions; $this->add_message(get_string('numberofpagesviewednotice', 'lesson', $a)); } } if (!$canmanage) { if ($data->gradelesson) { // Store this now before any modifications to pages viewed. $progresscompleted = $this->calculate_progress(); // Update the clock / get time information for this user. $this->stop_timer(); // Update completion state. $completion = new completion_info($course); if ($completion->is_enabled($cm) && $this->properties->completionendreached) { $completion->update_state($cm, COMPLETION_COMPLETE); } if ($this->properties->completiontimespent > 0) { $duration = $DB->get_field_sql( "SELECT SUM(lessontime - starttime) FROM {lesson_timer} WHERE lessonid = :lessonid AND userid = :userid", array('userid' => $USER->id, 'lessonid' => $this->properties->id)); if (!$duration) { $duration = 0; } // If student has not spend enough time in the lesson, display a message. if ($duration < $this->properties->completiontimespent) { $a = new stdClass; $a->timespentraw = $duration; $a->timespent = format_time($duration); $a->timerequiredraw = $this->properties->completiontimespent; $a->timerequired = format_time($this->properties->completiontimespent); $data->notenoughtimespent = $a; } } if ($gradeinfo->attempts) { if (!$this->properties->custom) { $data->numberofpagesviewed = $gradeinfo->nquestions; if ($this->properties->minquestions) { if ($gradeinfo->nquestions < $this->properties->minquestions) { $data->youshouldview = $this->properties->minquestions; } } $data->numberofcorrectanswers = $gradeinfo->earned; } $a = new stdClass; $a->score = $gradeinfo->earned; $a->grade = $gradeinfo->total; if ($gradeinfo->nmanual) { $a->tempmaxgrade = $gradeinfo->total - $gradeinfo->manualpoints; $a->essayquestions = $gradeinfo->nmanual; $data->displayscorewithessays = $a; } else { $data->displayscorewithoutessays = $a; } $grade = new stdClass(); $grade->lessonid = $this->properties->id; $grade->userid = $USER->id; $grade->grade = $gradeinfo->grade; $grade->completed = time(); if (isset($USER->modattempts[$this->properties->id])) { // If reviewing, make sure update old grade record. if (!$grades = $DB->get_records("lesson_grades", array("lessonid" => $this->properties->id, "userid" => $USER->id), "completed DESC", '*', 0, 1)) { throw new moodle_exception('cannotfindgrade', 'lesson'); } $oldgrade = array_shift($grades); $grade->id = $oldgrade->id; $DB->update_record("lesson_grades", $grade); } else { $newgradeid = $DB->insert_record("lesson_grades", $grade); } // Update central gradebook. lesson_update_grades($this, $USER->id); // Print grade (grade type Point). if ($this->properties->grade > 0) { $a = new stdClass; $a->grade = format_float($gradeinfo->grade * $this->properties->grade / 100, 1); $a->total = $this->properties->grade; $data->yourcurrentgradeisoutof = $a; } // Print grade (grade type Scale). if ($this->properties->grade < 0) { // Grade type is Scale. $grades = grade_get_grades($course->id, 'mod', 'lesson', $cm->instance, $USER->id); $grade = reset($grades->items[0]->grades); $data->yourcurrentgradeis = $grade->str_grade; } } else { if ($this->properties->timelimit) { if ($outoftime == 'normal') { $grade = new stdClass(); $grade->lessonid = $this->properties->id; $grade->userid = $USER->id; $grade->grade = 0; $grade->completed = time(); $newgradeid = $DB->insert_record("lesson_grades", $grade); $data->eolstudentoutoftimenoanswers = true; // Update central gradebook. lesson_update_grades($this, $USER->id); } } else { $data->welldone = true; } } $data->progresscompleted = $progresscompleted; } } else { // Display for teacher. if ($this->properties->grade != GRADE_TYPE_NONE) { $data->displayofgrade = true; } } if ($this->properties->modattempts && !$canmanage) { // Make sure if the student is reviewing, that he/she sees the same pages/page path that he/she saw the first time // look at the attempt records to find the first QUESTION page that the user answered, then use that page id // to pass to view again. This is slick cause it wont call the empty($pageid) code // $ntries is decremented above. if (!$attempts = $this->get_attempts($ntries)) { $attempts = array(); $url = new moodle_url('/mod/lesson/view.php', array('id' => $cm->id)); } else { $firstattempt = current($attempts); $pageid = $firstattempt->pageid; // If the student wishes to review, need to know the last question page that the student answered. // This will help to make sure that the student can leave the lesson via pushing the continue button. $lastattempt = end($attempts); $USER->modattempts[$this->properties->id] = $lastattempt->pageid; $url = new moodle_url('/mod/lesson/view.php', array('id' => $cm->id, 'pageid' => $pageid)); } $data->reviewlesson = $url->out(false); } else if ($this->properties->modattempts && $canmanage) { $data->modattemptsnoteacher = true; } if ($this->properties->activitylink) { $data->activitylink = $this->link_for_activitylink(); } return $data; } /** * Returns the last "legal" attempt from the list of student attempts. * * @param array $attempts The list of student attempts. * @return stdClass The updated fom data. */ public function get_last_attempt(array $attempts): stdClass { // If there are more tries than the max that is allowed, grab the last "legal" attempt. if (!empty($this->maxattempts) && (count($attempts) > $this->maxattempts)) { $lastattempt = $attempts[$this->maxattempts - 1]; } else { // Grab the last attempt since there's no limit to the max attempts or the user has made fewer attempts than the max. $lastattempt = end($attempts); } return $lastattempt; } } /** * Abstract class to provide a core functions to the all lesson classes * * This class should be abstracted by ALL classes with the lesson module to ensure * that all classes within this module can be interacted with in the same way. * * This class provides the user with a basic properties array that can be fetched * or set via magic methods, or alternatively by defining methods get_blah() or * set_blah() within the extending object. * * @copyright 2009 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ abstract class lesson_base { /** * An object containing properties * @var stdClass */ protected $properties; /** * The constructor * @param stdClass $properties */ public function __construct($properties) { $this->properties = (object)$properties; } /** * Magic property method * * Attempts to call a set_$key method if one exists otherwise falls back * to simply set the property * * @param string $key * @param mixed $value */ public function __set($key, $value) { if (method_exists($this, 'set_'.$key)) { $this->{'set_'.$key}($value); } $this->properties->{$key} = $value; } /** * Magic get method * * Attempts to call a get_$key method to return the property and ralls over * to return the raw property * * @param str $key * @return mixed */ public function __get($key) { if (method_exists($this, 'get_'.$key)) { return $this->{'get_'.$key}(); } return $this->properties->{$key}; } /** * Stupid PHP needs an isset magic method if you use the get magic method and * still want empty calls to work.... blah ~! * * @param string $key * @return bool */ public function __isset($key) { if (method_exists($this, 'get_'.$key)) { $val = $this->{'get_'.$key}(); return !empty($val); } return !empty($this->properties->{$key}); } /** * If implemented should create a new instance, save it in the DB and return it */ //public static function create() {} /** * If implemented should load an instance from the DB and return it */ //public static function load() {} /** * Fetches all of the properties of the object * @return stdClass */ public function properties() { return $this->properties; } } /** * Abstract class representation of a page associated with a lesson. * * This class should MUST be extended by all specialised page types defined in * mod/lesson/pagetypes/. * There are a handful of abstract methods that need to be defined as well as * severl methods that can optionally be defined in order to make the page type * operate in the desired way * * Database properties * @property int $id The id of this lesson page * @property int $lessonid The id of the lesson this page belongs to * @property int $prevpageid The id of the page before this one * @property int $nextpageid The id of the next page in the page sequence * @property int $qtype Identifies the page type of this page * @property int $qoption Used to record page type specific options * @property int $layout Used to record page specific layout selections * @property int $display Used to record page specific display selections * @property int $timecreated Timestamp for when the page was created * @property int $timemodified Timestamp for when the page was last modified * @property string $title The title of this page * @property string $contents The rich content shown to describe the page * @property int $contentsformat The format of the contents field * * Calculated properties * @property-read array $answers An array of answers for this page * @property-read bool $displayinmenublock Toggles display in the left menu block * @property-read array $jumps An array containing all the jumps this page uses * @property-read lesson $lesson The lesson this page belongs to * @property-read int $type The type of the page [question | structure] * @property-read typeid The unique identifier for the page type * @property-read typestring The string that describes this page type * * @abstract * @copyright 2009 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ abstract class lesson_page extends lesson_base { /** * A reference to the lesson this page belongs to * @var lesson */ protected $lesson = null; /** * Contains the answers to this lesson_page once loaded * @var null|array */ protected $answers = null; /** * This sets the type of the page, can be one of the constants defined below * @var int */ protected $type = 0; /** * Constants used to identify the type of the page */ const TYPE_QUESTION = 0; const TYPE_STRUCTURE = 1; /** * Constant used as a delimiter when parsing multianswer questions */ const MULTIANSWER_DELIMITER = '@^#|'; /** * This method should return the integer used to identify the page type within * the database and throughout code. This maps back to the defines used in 1.x * @abstract * @return int */ abstract protected function get_typeid(); /** * This method should return the string that describes the pagetype * @abstract * @return string */ abstract protected function get_typestring(); /** * This method gets called to display the page to the user taking the lesson * @abstract * @param object $renderer * @param object $attempt * @return string */ abstract public function display($renderer, $attempt); /** * Creates a new lesson_page within the database and returns the correct pagetype * object to use to interact with the new lesson * * @final * @static * @param object $properties * @param lesson $lesson * @return lesson_page Specialised object that extends lesson_page */ final public static function create($properties, lesson $lesson, $context, $maxbytes) { global $DB; $newpage = new stdClass; $newpage->title = $properties->title; $newpage->contents = $properties->contents_editor['text']; $newpage->contentsformat = $properties->contents_editor['format']; $newpage->lessonid = $lesson->id; $newpage->timecreated = time(); $newpage->qtype = $properties->qtype; $newpage->qoption = (isset($properties->qoption))?1:0; $newpage->layout = (isset($properties->layout))?1:0; $newpage->display = (isset($properties->display))?1:0; $newpage->prevpageid = 0; // this is a first page $newpage->nextpageid = 0; // this is the only page if ($properties->pageid) { $prevpage = $DB->get_record("lesson_pages", array("id" => $properties->pageid), 'id, nextpageid'); if (!$prevpage) { throw new \moodle_exception('cannotfindpages', 'lesson'); } $newpage->prevpageid = $prevpage->id; $newpage->nextpageid = $prevpage->nextpageid; } else { $nextpage = $DB->get_record('lesson_pages', array('lessonid'=>$lesson->id, 'prevpageid'=>0), 'id'); if ($nextpage) { // This is the first page, there are existing pages put this at the start $newpage->nextpageid = $nextpage->id; } } $newpage->id = $DB->insert_record("lesson_pages", $newpage); $editor = new stdClass; $editor->id = $newpage->id; $editor->contents_editor = $properties->contents_editor; $editor = file_postupdate_standard_editor($editor, 'contents', array('noclean'=>true, 'maxfiles'=>EDITOR_UNLIMITED_FILES, 'maxbytes'=>$maxbytes), $context, 'mod_lesson', 'page_contents', $editor->id); $DB->update_record("lesson_pages", $editor); if ($newpage->prevpageid > 0) { $DB->set_field("lesson_pages", "nextpageid", $newpage->id, array("id" => $newpage->prevpageid)); } if ($newpage->nextpageid > 0) { $DB->set_field("lesson_pages", "prevpageid", $newpage->id, array("id" => $newpage->nextpageid)); } $page = lesson_page::load($newpage, $lesson); $page->create_answers($properties); // Trigger an event: page created. $eventparams = array( 'context' => $context, 'objectid' => $newpage->id, 'other' => array( 'pagetype' => $page->get_typestring() ) ); $event = \mod_lesson\event\page_created::create($eventparams); $snapshot = clone($newpage); $snapshot->timemodified = 0; $event->add_record_snapshot('lesson_pages', $snapshot); $event->trigger(); $lesson->add_message(get_string('insertedpage', 'lesson').': '.format_string($newpage->title, true), 'notifysuccess'); return $page; } /** * This method loads a page object from the database and returns it as a * specialised object that extends lesson_page * * @final * @static * @param int $id * @param lesson $lesson * @return lesson_page Specialised lesson_page object */ final public static function load($id, lesson $lesson) { global $DB; if (is_object($id) && !empty($id->qtype)) { $page = $id; } else { $page = $DB->get_record("lesson_pages", array("id" => $id)); if (!$page) { throw new \moodle_exception('cannotfindpages', 'lesson'); } } $manager = lesson_page_type_manager::get($lesson); $class = 'lesson_page_type_'.$manager->get_page_type_idstring($page->qtype); if (!class_exists($class)) { $class = 'lesson_page'; } return new $class($page, $lesson); } /** * Deletes a lesson_page from the database as well as any associated records. * @final * @return bool */ final public function delete() { global $DB; $cm = get_coursemodule_from_instance('lesson', $this->lesson->id, $this->lesson->course); $context = context_module::instance($cm->id); // Delete files associated with attempts. $fs = get_file_storage(); if ($attempts = $DB->get_records('lesson_attempts', array("pageid" => $this->properties->id))) { foreach ($attempts as $attempt) { $fs->delete_area_files($context->id, 'mod_lesson', 'essay_responses', $attempt->id); $fs->delete_area_files($context->id, 'mod_lesson', 'essay_answers', $attempt->id); } } // Then delete all the associated records... $DB->delete_records("lesson_attempts", array("pageid" => $this->properties->id)); $DB->delete_records("lesson_branch", array("pageid" => $this->properties->id)); // Delete files related to answers and responses. if ($answers = $DB->get_records("lesson_answers", array("pageid" => $this->properties->id))) { foreach ($answers as $answer) { $fs->delete_area_files($context->id, 'mod_lesson', 'page_answers', $answer->id); $fs->delete_area_files($context->id, 'mod_lesson', 'page_responses', $answer->id); } } // ...now delete the answers... $DB->delete_records("lesson_answers", array("pageid" => $this->properties->id)); // ..and the page itself $DB->delete_records("lesson_pages", array("id" => $this->properties->id)); // Trigger an event: page deleted. $eventparams = array( 'context' => $context, 'objectid' => $this->properties->id, 'other' => array( 'pagetype' => $this->get_typestring() ) ); $event = \mod_lesson\event\page_deleted::create($eventparams); $event->add_record_snapshot('lesson_pages', $this->properties); $event->trigger(); // Delete files associated with this page. $fs->delete_area_files($context->id, 'mod_lesson', 'page_contents', $this->properties->id); // repair the hole in the linkage if (!$this->properties->prevpageid && !$this->properties->nextpageid) { //This is the only page, no repair needed } elseif (!$this->properties->prevpageid) { // this is the first page... $page = $this->lesson->load_page($this->properties->nextpageid); $page->move(null, 0); } elseif (!$this->properties->nextpageid) { // this is the last page... $page = $this->lesson->load_page($this->properties->prevpageid); $page->move(0); } else { // page is in the middle... $prevpage = $this->lesson->load_page($this->properties->prevpageid); $nextpage = $this->lesson->load_page($this->properties->nextpageid); $prevpage->move($nextpage->id); $nextpage->move(null, $prevpage->id); } return true; } /** * Moves a page by updating its nextpageid and prevpageid values within * the database * * @final * @param int $nextpageid * @param int $prevpageid */ final public function move($nextpageid=null, $prevpageid=null) { global $DB; if ($nextpageid === null) { $nextpageid = $this->properties->nextpageid; } if ($prevpageid === null) { $prevpageid = $this->properties->prevpageid; } $obj = new stdClass; $obj->id = $this->properties->id; $obj->prevpageid = $prevpageid; $obj->nextpageid = $nextpageid; $DB->update_record('lesson_pages', $obj); } /** * Returns the answers that are associated with this page in the database * * @final * @return array */ final public function get_answers() { global $DB; if ($this->answers === null) { $this->answers = array(); $answers = $DB->get_records('lesson_answers', array('pageid'=>$this->properties->id, 'lessonid'=>$this->lesson->id), 'id'); if (!$answers) { // It is possible that a lesson upgraded from Moodle 1.9 still // contains questions without any answers [MDL-25632]. // debugging(get_string('cannotfindanswer', 'lesson')); return array(); } foreach ($answers as $answer) { $this->answers[count($this->answers)] = new lesson_page_answer($answer); } } return $this->answers; } /** * Returns the lesson this page is associated with * @final * @return lesson */ final protected function get_lesson() { return $this->lesson; } /** * Returns the type of page this is. Not to be confused with page type * @final * @return int */ final protected function get_type() { return $this->type; } /** * Records an attempt at this page * * @final * @global moodle_database $DB * @param stdClass $context * @return stdClass Returns the result of the attempt */ final public function record_attempt($context) { global $DB, $USER, $OUTPUT, $PAGE; /** * This should be overridden by each page type to actually check the response * against what ever custom criteria they have defined */ $result = $this->check_answer(); // Processes inmediate jumps. if ($result->inmediatejump) { return $result; } $result->attemptsremaining = 0; $result->maxattemptsreached = false; if ($result->noanswer) { $result->newpageid = $this->properties->id; // display same page again $result->feedback = get_string('noanswer', 'lesson'); } else { if (!has_capability('mod/lesson:manage', $context)) { $nretakes = $DB->count_records("lesson_grades", array("lessonid"=>$this->lesson->id, "userid"=>$USER->id)); // Get the number of attempts that have been made on this question for this student and retake, $nattempts = $DB->count_records('lesson_attempts', array('lessonid' => $this->lesson->id, 'userid' => $USER->id, 'pageid' => $this->properties->id, 'retry' => $nretakes)); // Check if they have reached (or exceeded) the maximum number of attempts allowed. if (!empty($this->lesson->maxattempts) && $nattempts >= $this->lesson->maxattempts) { $result->maxattemptsreached = true; $result->feedback = get_string('maximumnumberofattemptsreached', 'lesson'); $result->newpageid = $this->lesson->get_next_page($this->properties->nextpageid); return $result; } // record student's attempt $attempt = new stdClass; $attempt->lessonid = $this->lesson->id; $attempt->pageid = $this->properties->id; $attempt->userid = $USER->id; $attempt->answerid = $result->answerid; $attempt->retry = $nretakes; $attempt->correct = $result->correctanswer; if($result->userresponse !== null) { $attempt->useranswer = $result->userresponse; } $attempt->timeseen = time(); // if allow modattempts, then update the old attempt record, otherwise, insert new answer record $userisreviewing = false; if (isset($USER->modattempts[$this->lesson->id])) { $attempt->retry = $nretakes - 1; // they are going through on review, $nretakes will be too high $userisreviewing = true; } // Only insert a record if we are not reviewing the lesson. if (!$userisreviewing) { if ($this->lesson->retake || (!$this->lesson->retake && $nretakes == 0)) { $attempt->id = $DB->insert_record("lesson_attempts", $attempt); list($updatedattempt, $updatedresult) = $this->on_after_write_attempt($attempt, $result); if ($updatedattempt) { $attempt = $updatedattempt; $result = $updatedresult; $DB->update_record("lesson_attempts", $attempt); } // Trigger an event: question answered. $eventparams = array( 'context' => context_module::instance($PAGE->cm->id), 'objectid' => $this->properties->id, 'other' => array( 'pagetype' => $this->get_typestring() ) ); $event = \mod_lesson\event\question_answered::create($eventparams); $event->add_record_snapshot('lesson_attempts', $attempt); $event->trigger(); // Increase the number of attempts made. $nattempts++; } } else { // When reviewing the lesson, the existing attemptid is also needed for the filearea options. $params = [ 'lessonid' => $attempt->lessonid, 'pageid' => $attempt->pageid, 'userid' => $attempt->userid, 'answerid' => $attempt->answerid, 'retry' => $attempt->retry ]; $attempt->id = $DB->get_field('lesson_attempts', 'id', $params); } // "number of attempts remaining" message if $this->lesson->maxattempts > 1 // displaying of message(s) is at the end of page for more ergonomic display // If we are showing the number of remaining attempts, we need to show it regardless of what the next // jump to page is. if (!$result->correctanswer) { // Retrieve the number of attempts left counter for displaying at bottom of feedback page. if ($result->newpageid == 0 && !empty($this->lesson->maxattempts) && $nattempts >= $this->lesson->maxattempts) { if ($this->lesson->maxattempts > 1) { // don't bother with message if only one attempt $result->maxattemptsreached = true; } $result->newpageid = LESSON_NEXTPAGE; } else if ($this->lesson->maxattempts > 1) { // don't bother with message if only one attempt $result->attemptsremaining = $this->lesson->maxattempts - $nattempts; if ($result->attemptsremaining == 0) { $result->maxattemptsreached = true; } } } } // Determine default feedback if necessary if (empty($result->response)) { if (!$this->lesson->feedback && !$result->noanswer && !($this->lesson->review & !$result->correctanswer && !$result->isessayquestion)) { // These conditions have been met: // 1. The lesson manager has not supplied feedback to the student // 2. Not displaying default feedback // 3. The user did provide an answer // 4. We are not reviewing with an incorrect answer (and not reviewing an essay question) $result->nodefaultresponse = true; // This will cause a redirect below } else if ($result->isessayquestion) { $result->response = get_string('defaultessayresponse', 'lesson'); } else if ($result->correctanswer) { $result->response = get_string('thatsthecorrectanswer', 'lesson'); } else { $result->response = get_string('thatsthewronganswer', 'lesson'); } } if ($result->response) { if ($this->lesson->review && !$result->correctanswer && !$result->isessayquestion) { $nretakes = $DB->count_records("lesson_grades", array("lessonid"=>$this->lesson->id, "userid"=>$USER->id)); $qattempts = $DB->count_records("lesson_attempts", array("userid"=>$USER->id, "retry"=>$nretakes, "pageid"=>$this->properties->id)); if ($qattempts == 1) { $result->feedback = $OUTPUT->box(get_string("firstwrong", "lesson"), 'feedback'); } else { if (!$result->maxattemptsreached) { $result->feedback = $OUTPUT->box(get_string("secondpluswrong", "lesson"), 'feedback'); } else { $result->feedback = $OUTPUT->box(get_string("finalwrong", "lesson"), 'feedback'); } } } else { $result->feedback = ''; } $class = 'response'; if ($result->correctanswer) { $class .= ' correct'; // CSS over-ride this if they exist (!important). } else if (!$result->isessayquestion) { $class .= ' incorrect'; // CSS over-ride this if they exist (!important). } $options = [ 'noclean' => true, 'para' => true, 'overflowdiv' => true, 'context' => $context, ]; $answeroptions = (object) array_merge($options, [ 'attemptid' => $attempt->id ?? null, ]); $result->feedback .= $OUTPUT->box(format_text($this->get_contents(), $this->properties->contentsformat, $options), 'generalbox boxaligncenter py-3'); $result->feedback .= '
' . get_string("youranswer", "lesson").' :
'; // Create a table containing the answers and responses. $table = new html_table(); // Multianswer allowed. if ($this->properties->qoption) { $studentanswerarray = explode(self::MULTIANSWER_DELIMITER, $result->studentanswer); $responsearr = explode(self::MULTIANSWER_DELIMITER, $result->response); $studentanswerresponse = array_combine($studentanswerarray, $responsearr); foreach ($studentanswerresponse as $answer => $response) { // Add a table row containing the answer. $studentanswer = $this->format_answer($answer, $context, $result->studentanswerformat, $answeroptions); $table->data[] = array($studentanswer); // If the response exists, add a table row containing the response. If not, add en empty row. if (!empty(trim($response))) { $studentresponse = isset($result->responseformat) ? $this->format_response($response, $context, $result->responseformat, $options) : $response; $studentresponsecontent = html_writer::div('' . get_string("response", "lesson") . ':
' . $studentresponse, $class); $table->data[] = array($studentresponsecontent); } else { $table->data[] = array(''); } } } else { // Add a table row containing the answer. $studentanswer = $this->format_answer($result->studentanswer, $context, $result->studentanswerformat, $answeroptions); $table->data[] = array($studentanswer); // If the response exists, add a table row containing the response. If not, add en empty row. if (!empty(trim($result->response))) { $studentresponse = isset($result->responseformat) ? $this->format_response($result->response, $context, $result->responseformat, $result->answerid, $options) : $result->response; $studentresponsecontent = html_writer::div('' . get_string("response", "lesson") . ':
' . $studentresponse, $class); $table->data[] = array($studentresponsecontent); } else { $table->data[] = array(''); } } $result->feedback .= html_writer::table($table).'
'; } } return $result; } /** * Formats the answer. Override for custom formatting. * * @param string $answer * @param context $context * @param int $answerformat * @return string Returns formatted string */ public function format_answer($answer, $context, $answerformat, $options = []) { if (is_object($options)) { $options = (array) $options; } if (empty($options['context'])) { $options['context'] = $context; } if (empty($options['para'])) { $options['para'] = true; } // The attemptid is used by some plugins but is not a valid argument to format_text. unset($options['attemptid']); return format_text($answer, $answerformat, $options); } /** * Formats the response * * @param string $response * @param context $context * @param int $responseformat * @param int $answerid * @param stdClass $options * @return string Returns formatted string */ private function format_response($response, $context, $responseformat, $answerid, $options) { $convertstudentresponse = file_rewrite_pluginfile_urls($response, 'pluginfile.php', $context->id, 'mod_lesson', 'page_responses', $answerid); return format_text($convertstudentresponse, $responseformat, $options); } /** * Returns the string for a jump name * * @final * @param int $jumpto Jump code or page ID * @return string **/ final protected function get_jump_name($jumpto) { global $DB; static $jumpnames = array(); if (!array_key_exists($jumpto, $jumpnames)) { if ($jumpto == LESSON_THISPAGE) { $jumptitle = get_string('thispage', 'lesson'); } elseif ($jumpto == LESSON_NEXTPAGE) { $jumptitle = get_string('nextpage', 'lesson'); } elseif ($jumpto == LESSON_EOL) { $jumptitle = get_string('endoflesson', 'lesson'); } elseif ($jumpto == LESSON_UNSEENBRANCHPAGE) { $jumptitle = get_string('unseenpageinbranch', 'lesson'); } elseif ($jumpto == LESSON_PREVIOUSPAGE) { $jumptitle = get_string('previouspage', 'lesson'); } elseif ($jumpto == LESSON_RANDOMPAGE) { $jumptitle = get_string('randompageinbranch', 'lesson'); } elseif ($jumpto == LESSON_RANDOMBRANCH) { $jumptitle = get_string('randombranch', 'lesson'); } elseif ($jumpto == LESSON_CLUSTERJUMP) { $jumptitle = get_string('clusterjump', 'lesson'); } else { if (!$jumptitle = $DB->get_field('lesson_pages', 'title', array('id' => $jumpto))) { $jumptitle = ''.get_string('notdefined', 'lesson').''; } } $jumpnames[$jumpto] = format_string($jumptitle,true); } return $jumpnames[$jumpto]; } /** * Constructor method * @param object $properties * @param lesson $lesson */ public function __construct($properties, lesson $lesson) { parent::__construct($properties); $this->lesson = $lesson; } /** * Returns the score for the attempt * This may be overridden by page types that require manual grading * @param array $answers * @param object $attempt * @return int */ public function earned_score($answers, $attempt) { return $answers[$attempt->answerid]->score; } /** * This is a callback method that can be override and gets called when ever a page * is viewed * * @param bool $canmanage True if the user has the manage cap * @param bool $redirect Optional, default to true. Set to false to avoid redirection and return the page to redirect. * @return mixed */ public function callback_on_view($canmanage, $redirect = true) { return true; } /** * save editor answers files and update answer record * * @param object $context * @param int $maxbytes * @param object $answer * @param object $answereditor * @param object $responseeditor */ public function save_answers_files($context, $maxbytes, &$answer, $answereditor = '', $responseeditor = '') { global $DB; if (isset($answereditor['itemid'])) { $answer->answer = file_save_draft_area_files($answereditor['itemid'], $context->id, 'mod_lesson', 'page_answers', $answer->id, array('noclean' => true, 'maxfiles' => EDITOR_UNLIMITED_FILES, 'maxbytes' => $maxbytes), $answer->answer, null); $DB->set_field('lesson_answers', 'answer', $answer->answer, array('id' => $answer->id)); } if (isset($responseeditor['itemid'])) { $answer->response = file_save_draft_area_files($responseeditor['itemid'], $context->id, 'mod_lesson', 'page_responses', $answer->id, array('noclean' => true, 'maxfiles' => EDITOR_UNLIMITED_FILES, 'maxbytes' => $maxbytes), $answer->response, null); $DB->set_field('lesson_answers', 'response', $answer->response, array('id' => $answer->id)); } } /** * Rewrite urls in response and optionality answer of a question answer * * @param object $answer * @param bool $rewriteanswer must rewrite answer * @return object answer with rewritten urls */ public static function rewrite_answers_urls($answer, $rewriteanswer = true) { global $PAGE; $context = context_module::instance($PAGE->cm->id); if ($rewriteanswer) { $answer->answer = file_rewrite_pluginfile_urls($answer->answer, 'pluginfile.php', $context->id, 'mod_lesson', 'page_answers', $answer->id); } $answer->response = file_rewrite_pluginfile_urls($answer->response, 'pluginfile.php', $context->id, 'mod_lesson', 'page_responses', $answer->id); return $answer; } /** * Updates a lesson page and its answers within the database * * @param object $properties * @return bool */ public function update($properties, $context = null, $maxbytes = null) { global $DB, $PAGE; $answers = $this->get_answers(); $properties->id = $this->properties->id; $properties->lessonid = $this->lesson->id; if (empty($properties->qoption)) { $properties->qoption = '0'; } if (empty($context)) { $context = $PAGE->context; } if ($maxbytes === null) { $maxbytes = get_user_max_upload_file_size($context); } $properties->timemodified = time(); $properties = file_postupdate_standard_editor($properties, 'contents', array('noclean'=>true, 'maxfiles'=>EDITOR_UNLIMITED_FILES, 'maxbytes'=>$maxbytes), $context, 'mod_lesson', 'page_contents', $properties->id); $DB->update_record("lesson_pages", $properties); // Trigger an event: page updated. \mod_lesson\event\page_updated::create_from_lesson_page($this, $context)->trigger(); if ($this->type == self::TYPE_STRUCTURE && $this->get_typeid() != LESSON_PAGE_BRANCHTABLE) { // These page types have only one answer to save the jump and score. if (count($answers) > 1) { $answer = array_shift($answers); foreach ($answers as $a) { $DB->delete_records('lesson_answers', array('id' => $a->id)); } } else if (count($answers) == 1) { $answer = array_shift($answers); } else { $answer = new stdClass; $answer->lessonid = $properties->lessonid; $answer->pageid = $properties->id; $answer->timecreated = time(); } $answer->timemodified = time(); if (isset($properties->jumpto[0])) { $answer->jumpto = $properties->jumpto[0]; } if (isset($properties->score[0])) { $answer->score = $properties->score[0]; } if (!empty($answer->id)) { $DB->update_record("lesson_answers", $answer->properties()); } else { $DB->insert_record("lesson_answers", $answer); } } else { for ($i = 0; $i < count($properties->answer_editor); $i++) { if (!array_key_exists($i, $this->answers)) { $this->answers[$i] = new stdClass; $this->answers[$i]->lessonid = $this->lesson->id; $this->answers[$i]->pageid = $this->id; $this->answers[$i]->timecreated = $this->timecreated; $this->answers[$i]->answer = null; } if (isset($properties->answer_editor[$i])) { if (is_array($properties->answer_editor[$i])) { // Multichoice and true/false pages have an HTML editor. $this->answers[$i]->answer = $properties->answer_editor[$i]['text']; $this->answers[$i]->answerformat = $properties->answer_editor[$i]['format']; } else { // Branch tables, shortanswer and mumerical pages have only a text field. $this->answers[$i]->answer = $properties->answer_editor[$i]; $this->answers[$i]->answerformat = FORMAT_MOODLE; } } else { // If there is no data posted which means we want to reset the stored values. $this->answers[$i]->answer = null; } if (!empty($properties->response_editor[$i]) && is_array($properties->response_editor[$i])) { $this->answers[$i]->response = $properties->response_editor[$i]['text']; $this->answers[$i]->responseformat = $properties->response_editor[$i]['format']; } if ($this->answers[$i]->answer !== null && $this->answers[$i]->answer !== '') { if (isset($properties->jumpto[$i])) { $this->answers[$i]->jumpto = $properties->jumpto[$i]; } if ($this->lesson->custom && isset($properties->score[$i])) { $this->answers[$i]->score = $properties->score[$i]; } if (!isset($this->answers[$i]->id)) { $this->answers[$i]->id = $DB->insert_record("lesson_answers", $this->answers[$i]); } else { $DB->update_record("lesson_answers", $this->answers[$i]->properties()); } // Save files in answers and responses. if (isset($properties->response_editor[$i])) { $this->save_answers_files($context, $maxbytes, $this->answers[$i], $properties->answer_editor[$i], $properties->response_editor[$i]); } else { $this->save_answers_files($context, $maxbytes, $this->answers[$i], $properties->answer_editor[$i]); } } else if (isset($this->answers[$i]->id)) { $DB->delete_records('lesson_answers', array('id' => $this->answers[$i]->id)); unset($this->answers[$i]); } } } return true; } /** * Can be set to true if the page requires a static link to create a new instance * instead of simply being included in the dropdown * @param int $previd * @return bool */ public function add_page_link($previd) { return false; } /** * Returns true if a page has been viewed before * * @param array|int $param Either an array of pages that have been seen or the * number of retakes a user has had * @return bool */ public function is_unseen($param) { global $USER, $DB; if (is_array($param)) { $seenpages = $param; return (!array_key_exists($this->properties->id, $seenpages)); } else { $nretakes = $param; if (!$DB->count_records("lesson_attempts", array("pageid"=>$this->properties->id, "userid"=>$USER->id, "retry"=>$nretakes))) { return true; } } return false; } /** * Checks to see if a page has been answered previously * @param int $nretakes * @return bool */ public function is_unanswered($nretakes) { global $DB, $USER; if (!$DB->count_records("lesson_attempts", array('pageid'=>$this->properties->id, 'userid'=>$USER->id, 'correct'=>1, 'retry'=>$nretakes))) { return true; } return false; } /** * Creates answers within the database for this lesson_page. Usually only ever * called when creating a new page instance * @param object $properties * @return array */ public function create_answers($properties) { global $DB, $PAGE; // now add the answers $newanswer = new stdClass; $newanswer->lessonid = $this->lesson->id; $newanswer->pageid = $this->properties->id; $newanswer->timecreated = $this->properties->timecreated; $cm = get_coursemodule_from_instance('lesson', $this->lesson->id, $this->lesson->course); $context = context_module::instance($cm->id); $answers = array(); for ($i = 0; $i < ($this->lesson->maxanswers + 1); $i++) { $answer = clone($newanswer); if (isset($properties->answer_editor[$i])) { if (is_array($properties->answer_editor[$i])) { // Multichoice and true/false pages have an HTML editor. $answer->answer = $properties->answer_editor[$i]['text']; $answer->answerformat = $properties->answer_editor[$i]['format']; } else { // Branch tables, shortanswer and mumerical pages have only a text field. $answer->answer = $properties->answer_editor[$i]; $answer->answerformat = FORMAT_MOODLE; } } if (!empty($properties->response_editor[$i]) && is_array($properties->response_editor[$i])) { $answer->response = $properties->response_editor[$i]['text']; $answer->responseformat = $properties->response_editor[$i]['format']; } if (isset($answer->answer) && $answer->answer != '') { if (isset($properties->jumpto[$i])) { $answer->jumpto = $properties->jumpto[$i]; } if ($this->lesson->custom && isset($properties->score[$i])) { $answer->score = $properties->score[$i]; } $answer->id = $DB->insert_record("lesson_answers", $answer); if (isset($properties->response_editor[$i])) { $this->save_answers_files($context, $PAGE->course->maxbytes, $answer, $properties->answer_editor[$i], $properties->response_editor[$i]); } else { $this->save_answers_files($context, $PAGE->course->maxbytes, $answer, $properties->answer_editor[$i]); } $answers[$answer->id] = new lesson_page_answer($answer); } } $this->answers = $answers; return $answers; } /** * This method MUST be overridden by all question page types, or page types that * wish to score a page. * * The structure of result should always be the same so it is a good idea when * overriding this method on a page type to call * * $result = parent::check_answer(); * * before modifying it as required. * * @return stdClass */ public function check_answer() { $result = new stdClass; $result->answerid = 0; $result->noanswer = false; $result->correctanswer = false; $result->isessayquestion = false; // use this to turn off review button on essay questions $result->response = ''; $result->newpageid = 0; // stay on the page $result->studentanswer = ''; // use this to store student's answer(s) in order to display it on feedback page $result->studentanswerformat = FORMAT_MOODLE; $result->userresponse = null; $result->feedback = ''; // Store data that was POSTd by a form. This is currently used to perform any logic after the 1st write to the db // of the attempt. $result->postdata = false; $result->nodefaultresponse = false; // Flag for redirecting when default feedback is turned off $result->inmediatejump = false; // Flag to detect when we should do a jump from the page without further processing. return $result; } /** * Do any post persistence processing logic of an attempt. E.g. in cases where we need update file urls in an editor * and we need to have the id of the stored attempt. Should be overridden in each individual child * pagetype on a as required basis * * @param object $attempt The attempt corresponding to the db record * @param object $result The result from the 'check_answer' method * @return array False if nothing to be modified, updated $attempt and $result if update required. */ public function on_after_write_attempt($attempt, $result) { return [false, false]; } /** * True if the page uses a custom option * * Should be override and set to true if the page uses a custom option. * * @return bool */ public function has_option() { return false; } /** * Returns the maximum number of answers for this page given the maximum number * of answers permitted by the lesson. * * @param int $default * @return int */ public function max_answers($default) { return $default; } /** * Returns the properties of this lesson page as an object * @return stdClass; */ public function properties() { $properties = clone($this->properties); if ($this->answers === null) { $this->get_answers(); } if (count($this->answers)>0) { $count = 0; $qtype = $properties->qtype; foreach ($this->answers as $answer) { $properties->{'answer_editor['.$count.']'} = array('text' => $answer->answer, 'format' => $answer->answerformat); if ($qtype != LESSON_PAGE_MATCHING) { $properties->{'response_editor['.$count.']'} = array('text' => $answer->response, 'format' => $answer->responseformat); } else { $properties->{'response_editor['.$count.']'} = $answer->response; } $properties->{'jumpto['.$count.']'} = $answer->jumpto; $properties->{'score['.$count.']'} = $answer->score; $count++; } } return $properties; } /** * Returns an array of options to display when choosing the jumpto for a page/answer * @static * @param int $pageid * @param lesson $lesson * @return array */ public static function get_jumptooptions($pageid, lesson $lesson) { global $DB; $jump = array(); $jump[0] = get_string("thispage", "lesson"); $jump[LESSON_NEXTPAGE] = get_string("nextpage", "lesson"); $jump[LESSON_PREVIOUSPAGE] = get_string("previouspage", "lesson"); $jump[LESSON_EOL] = get_string("endoflesson", "lesson"); if ($pageid == 0) { return $jump; } $pages = $lesson->load_all_pages(); if ($pages[$pageid]->qtype == LESSON_PAGE_BRANCHTABLE || $lesson->is_sub_page_of_type($pageid, array(LESSON_PAGE_BRANCHTABLE), array(LESSON_PAGE_ENDOFBRANCH, LESSON_PAGE_CLUSTER))) { $jump[LESSON_UNSEENBRANCHPAGE] = get_string("unseenpageinbranch", "lesson"); $jump[LESSON_RANDOMPAGE] = get_string("randompageinbranch", "lesson"); } if($pages[$pageid]->qtype == LESSON_PAGE_CLUSTER || $lesson->is_sub_page_of_type($pageid, array(LESSON_PAGE_CLUSTER), array(LESSON_PAGE_ENDOFCLUSTER))) { $jump[LESSON_CLUSTERJUMP] = get_string("clusterjump", "lesson"); } if (!optional_param('firstpage', 0, PARAM_INT)) { $apageid = $DB->get_field("lesson_pages", "id", array("lessonid" => $lesson->id, "prevpageid" => 0)); while (true) { if ($apageid) { $title = $DB->get_field("lesson_pages", "title", array("id" => $apageid)); $jump[$apageid] = strip_tags(format_string($title,true)); $apageid = $DB->get_field("lesson_pages", "nextpageid", array("id" => $apageid)); } else { // last page reached break; } } } return $jump; } /** * Returns the contents field for the page properly formatted and with plugin * file url's converted * @return string */ public function get_contents() { global $PAGE; if (!empty($this->properties->contents)) { if (!isset($this->properties->contentsformat)) { $this->properties->contentsformat = FORMAT_HTML; } $context = context_module::instance($PAGE->cm->id); $contents = file_rewrite_pluginfile_urls($this->properties->contents, 'pluginfile.php', $context->id, 'mod_lesson', 'page_contents', $this->properties->id); // Must do this BEFORE format_text()! return format_text($contents, $this->properties->contentsformat, array('context' => $context, 'noclean' => true, 'overflowdiv' => true)); // Page edit is marked with XSS, we want all content here. } else { return ''; } } /** * Set to true if this page should display in the menu block * @return bool */ protected function get_displayinmenublock() { return false; } /** * Get the string that describes the options of this page type * @return string */ public function option_description_string() { return ''; } /** * Updates a table with the answers for this page * @param html_table $table * @return html_table */ public function display_answers(html_table $table) { $answers = $this->get_answers(); $i = 1; foreach ($answers as $answer) { $cells = array(); $cells[] = ':'; $cells[] = $this->get_jump_name($answer->jumpto); $table->data[] = new html_table_row($cells); if ($i === 1){ $table->data[count($table->data)-1]->cells[0]->style = 'width:20%;'; } $i++; } return $table; } /** * Determines if this page should be grayed out on the management/report screens * @return int 0 or 1 */ protected function get_grayout() { return 0; } /** * Adds stats for this page to the &pagestats object. This should be defined * for all page types that grade * @param array $pagestats * @param int $tries * @return bool */ public function stats(array &$pagestats, $tries) { return true; } /** * Formats the answers of this page for a report * * @param object $answerpage * @param object $answerdata * @param object $useranswer * @param array $pagestats * @param int $i Count of first level answers * @param int $n Count of second level answers * @return object The answer page for this */ public function report_answers($answerpage, $answerdata, $useranswer, $pagestats, &$i, &$n) { $answers = $this->get_answers(); $formattextdefoptions = new stdClass; $formattextdefoptions->para = false; //I'll use it widely in this page foreach ($answers as $answer) { $data = get_string('jumpsto', 'lesson', $this->get_jump_name($answer->jumpto)); $answerdata->answers[] = array($data, ""); $answerpage->answerdata = $answerdata; } return $answerpage; } /** * Gets an array of the jumps used by the answers of this page * * @return array */ public function get_jumps() { global $DB; $jumps = array(); $params = array ("lessonid" => $this->lesson->id, "pageid" => $this->properties->id); if ($answers = $this->get_answers()) { foreach ($answers as $answer) { $jumps[] = $this->get_jump_name($answer->jumpto); } } else { $jumps[] = $this->get_jump_name($this->properties->nextpageid); } return $jumps; } /** * Informs whether this page type require manual grading or not * @return bool */ public function requires_manual_grading() { return false; } /** * A callback method that allows a page to override the next page a user will * see during when this page is being completed. * @return false|int */ public function override_next_page() { return false; } /** * This method is used to determine if this page is a valid page * * @param array $validpages * @param array $pageviews * @return int The next page id to check */ public function valid_page_and_view(&$validpages, &$pageviews) { $validpages[$this->properties->id] = 1; return $this->properties->nextpageid; } /** * Get files from the page area file. * * @param bool $includedirs whether or not include directories * @param int $updatedsince return files updated since this time * @return array list of stored_file objects * @since Moodle 3.2 */ public function get_files($includedirs = true, $updatedsince = 0) { $fs = get_file_storage(); return $fs->get_area_files($this->lesson->context->id, 'mod_lesson', 'page_contents', $this->properties->id, 'itemid, filepath, filename', $includedirs, $updatedsince); } /** * Make updates to the form data if required. * * @since Moodle 3.7 * @param stdClass $data The form data to update. * @return stdClass The updated fom data. */ public function update_form_data(stdClass $data): stdClass { return $data; } } /** * Class used to represent an answer to a page * * @property int $id The ID of this answer in the database * @property int $lessonid The ID of the lesson this answer belongs to * @property int $pageid The ID of the page this answer belongs to * @property int $jumpto Identifies where the user goes upon completing a page with this answer * @property int $grade The grade this answer is worth * @property int $score The score this answer will give * @property int $flags Used to store options for the answer * @property int $timecreated A timestamp of when the answer was created * @property int $timemodified A timestamp of when the answer was modified * @property string $answer The answer itself * @property string $response The response the user sees if selecting this answer * * @copyright 2009 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class lesson_page_answer extends lesson_base { /** * Loads an page answer from the DB * * @param int $id * @return lesson_page_answer */ public static function load($id) { global $DB; $answer = $DB->get_record("lesson_answers", array("id" => $id)); return new lesson_page_answer($answer); } /** * Given an object of properties and a page created answer(s) and saves them * in the database. * * @param stdClass $properties * @param lesson_page $page * @return array */ public static function create($properties, lesson_page $page) { return $page->create_answers($properties); } /** * Get files from the answer area file. * * @param bool $includedirs whether or not include directories * @param int $updatedsince return files updated since this time * @return array list of stored_file objects * @since Moodle 3.2 */ public function get_files($includedirs = true, $updatedsince = 0) { $lesson = lesson::load($this->properties->lessonid); $fs = get_file_storage(); $answerfiles = $fs->get_area_files($lesson->context->id, 'mod_lesson', 'page_answers', $this->properties->id, 'itemid, filepath, filename', $includedirs, $updatedsince); $responsefiles = $fs->get_area_files($lesson->context->id, 'mod_lesson', 'page_responses', $this->properties->id, 'itemid, filepath, filename', $includedirs, $updatedsince); return array_merge($answerfiles, $responsefiles); } } /** * A management class for page types * * This class is responsible for managing the different pages. A manager object can * be retrieved by calling the following line of code: * * $manager = lesson_page_type_manager::get($lesson); * * The first time the page type manager is retrieved the it includes all of the * different page types located in mod/lesson/pagetypes. * * @copyright 2009 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class lesson_page_type_manager { /** * An array of different page type classes * @var array */ protected $types = array(); /** * Retrieves the lesson page type manager object * * If the object hasn't yet been created it is created here. * * @staticvar lesson_page_type_manager $pagetypemanager * @param lesson $lesson * @return lesson_page_type_manager */ public static function get(lesson $lesson) { static $pagetypemanager; if (!($pagetypemanager instanceof lesson_page_type_manager)) { $pagetypemanager = new lesson_page_type_manager(); $pagetypemanager->load_lesson_types($lesson); } return $pagetypemanager; } /** * Finds and loads all lesson page types in mod/lesson/pagetypes * * @param lesson $lesson */ public function load_lesson_types(lesson $lesson) { global $CFG; $basedir = $CFG->dirroot.'/mod/lesson/pagetypes/'; $dir = dir($basedir); while (false !== ($entry = $dir->read())) { if (strpos($entry, '.')===0 || !preg_match('#^[a-zA-Z]+\.php#i', $entry)) { continue; } require_once($basedir.$entry); $class = 'lesson_page_type_'.strtok($entry,'.'); if (class_exists($class)) { $pagetype = new $class(new stdClass, $lesson); $this->types[$pagetype->typeid] = $pagetype; } } } /** * Returns an array of strings to describe the loaded page types * * @param int $type Can be used to return JUST the string for the requested type * @return array */ public function get_page_type_strings($type=null, $special=true) { $types = array(); foreach ($this->types as $pagetype) { if (($type===null || $pagetype->type===$type) && ($special===true || $pagetype->is_standard())) { $types[$pagetype->typeid] = $pagetype->typestring; } } return $types; } /** * Returns the basic string used to identify a page type provided with an id * * This string can be used to instantiate or identify the page type class. * If the page type id is unknown then 'unknown' is returned * * @param int $id * @return string */ public function get_page_type_idstring($id) { foreach ($this->types as $pagetype) { if ((int)$pagetype->typeid === (int)$id) { return $pagetype->idstring; } } return 'unknown'; } /** * Loads a page for the provided lesson given it's id * * This function loads a page from the lesson when given both the lesson it belongs * to as well as the page's id. * If the page doesn't exist an error is thrown * * @param int $pageid The id of the page to load * @param lesson $lesson The lesson the page belongs to * @return lesson_page A class that extends lesson_page */ public function load_page($pageid, lesson $lesson) { global $DB; if (!($page =$DB->get_record('lesson_pages', array('id'=>$pageid, 'lessonid'=>$lesson->id)))) { throw new \moodle_exception('cannotfindpages', 'lesson'); } $pagetype = get_class($this->types[$page->qtype]); $page = new $pagetype($page, $lesson); return $page; } /** * This function detects errors in the ordering between 2 pages and updates the page records. * * @param stdClass $page1 Either the first of 2 pages or null if the $page2 param is the first in the list. * @param stdClass $page1 Either the second of 2 pages or null if the $page1 param is the last in the list. */ protected function check_page_order($page1, $page2) { global $DB; if (empty($page1)) { if ($page2->prevpageid != 0) { debugging("***prevpageid of page " . $page2->id . " set to 0***"); $page2->prevpageid = 0; $DB->set_field("lesson_pages", "prevpageid", 0, array("id" => $page2->id)); } } else if (empty($page2)) { if ($page1->nextpageid != 0) { debugging("***nextpageid of page " . $page1->id . " set to 0***"); $page1->nextpageid = 0; $DB->set_field("lesson_pages", "nextpageid", 0, array("id" => $page1->id)); } } else { if ($page1->nextpageid != $page2->id) { debugging("***nextpageid of page " . $page1->id . " set to " . $page2->id . "***"); $page1->nextpageid = $page2->id; $DB->set_field("lesson_pages", "nextpageid", $page2->id, array("id" => $page1->id)); } if ($page2->prevpageid != $page1->id) { debugging("***prevpageid of page " . $page2->id . " set to " . $page1->id . "***"); $page2->prevpageid = $page1->id; $DB->set_field("lesson_pages", "prevpageid", $page1->id, array("id" => $page2->id)); } } } /** * This function loads ALL pages that belong to the lesson. * * @param lesson $lesson * @return array An array of lesson_page_type_* */ public function load_all_pages(lesson $lesson) { global $DB; if (!($pages =$DB->get_records('lesson_pages', array('lessonid'=>$lesson->id)))) { return array(); // Records returned empty. } foreach ($pages as $key=>$page) { $pagetype = get_class($this->types[$page->qtype]); $pages[$key] = new $pagetype($page, $lesson); } $orderedpages = array(); $lastpageid = 0; $morepages = true; while ($morepages) { $morepages = false; foreach ($pages as $page) { if ((int)$page->prevpageid === (int)$lastpageid) { // Check for errors in page ordering and fix them on the fly. $prevpage = null; if ($lastpageid !== 0) { $prevpage = $orderedpages[$lastpageid]; } $this->check_page_order($prevpage, $page); $morepages = true; $orderedpages[$page->id] = $page; unset($pages[$page->id]); $lastpageid = $page->id; if ((int)$page->nextpageid===0) { break 2; } else { break 1; } } } } // Add remaining pages and fix the nextpageid links for each page. foreach ($pages as $page) { // Check for errors in page ordering and fix them on the fly. $prevpage = null; if ($lastpageid !== 0) { $prevpage = $orderedpages[$lastpageid]; } $this->check_page_order($prevpage, $page); $orderedpages[$page->id] = $page; unset($pages[$page->id]); $lastpageid = $page->id; } if ($lastpageid !== 0) { $this->check_page_order($orderedpages[$lastpageid], null); } return $orderedpages; } /** * Fetches an mform that can be used to create/edit an page * * @param int $type The id for the page type * @param array $arguments Any arguments to pass to the mform * @return lesson_add_page_form_base */ public function get_page_form($type, $arguments) { $class = 'lesson_add_page_form_'.$this->get_page_type_idstring($type); if (!class_exists($class) || get_parent_class($class)!=='lesson_add_page_form_base') { debugging('Lesson page type unknown class requested '.$class, DEBUG_DEVELOPER); $class = 'lesson_add_page_form_selection'; } else if ($class === 'lesson_add_page_form_unknown') { $class = 'lesson_add_page_form_selection'; } return new $class(null, $arguments); } /** * Returns an array of links to use as add page links * @param int $previd The id of the previous page * @return array */ public function get_add_page_type_links($previd) { global $OUTPUT; $links = array(); foreach ($this->types as $key=>$type) { if ($link = $type->add_page_link($previd)) { $links[$key] = $link; } } return $links; } } home/smkb8269/public_html/lms.smk-almanshuriyah.sch.id/mod/imscp/locallib.php000064400000032607152267113450023063 0ustar00. /** * Private imscp module utility functions * * @package mod_imscp * @copyright 2009 Petr Skoda {@link http://skodak.org} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); require_once("$CFG->dirroot/mod/imscp/lib.php"); require_once("$CFG->libdir/filelib.php"); require_once("$CFG->libdir/resourcelib.php"); /** * Print IMSCP content to page. * * @param stdClass $imscp module instance. * @param stdClass $cm course module. * @param stdClass $course record. */ function imscp_print_content($imscp, $cm, $course) { global $PAGE, $CFG; $items = array_filter((array) unserialize_array($imscp->structure)); echo '
'; echo '
'; echo '
    '; foreach ($items as $item) { echo imscp_htmllize_item($item, $imscp, $cm); } echo '
'; echo ''; echo '
'; echo '
'; $PAGE->requires->js_init_call('M.mod_imscp.init'); } /** * Internal function - creates htmls structure suitable for YUI tree. */ function imscp_htmllize_item($item, $imscp, $cm) { global $CFG; if ($item['href']) { if (preg_match('|^https?://|', $item['href'])) { $url = $item['href']; } else { $context = context_module::instance($cm->id); $urlbase = "$CFG->wwwroot/pluginfile.php"; $path = '/'.$context->id.'/mod_imscp/content/'.$imscp->revision.'/'.$item['href']; $url = file_encode_url($urlbase, $path, false); } $result = "
  • ".$item['title'].''; } else { $result = '
  • '.$item['title']; } if ($item['subitems']) { $result .= '
      '; foreach ($item['subitems'] as $subitem) { $result .= imscp_htmllize_item($subitem, $imscp, $cm); } $result .= '
    '; } $result .= '
  • '; return $result; } /** * Parse an IMS content package's manifest file to determine its structure * @param object $imscp * @param object $context * @return array */ function imscp_parse_structure($imscp, $context) { $fs = get_file_storage(); if (!$manifestfile = $fs->get_file($context->id, 'mod_imscp', 'content', $imscp->revision, '/', 'imsmanifest.xml')) { return null; } return imscp_parse_manifestfile($manifestfile->get_content(), $imscp, $context); } /** * Parse the contents of a IMS package's manifest file. * @param string $manifestfilecontents the contents of the manifest file * @return array */ function imscp_parse_manifestfile($manifestfilecontents, $imscp, $context) { $doc = new DOMDocument(); if (!$doc->loadXML($manifestfilecontents, LIBXML_NONET)) { return null; } // We put this fake URL as base in order to detect path changes caused by xml:base attributes. $doc->documentURI = 'http://grrr/'; $xmlorganizations = $doc->getElementsByTagName('organizations'); if (empty($xmlorganizations->length)) { return null; } $default = null; if ($xmlorganizations->item(0)->attributes->getNamedItem('default')) { $default = $xmlorganizations->item(0)->attributes->getNamedItem('default')->nodeValue; } $xmlorganization = $doc->getElementsByTagName('organization'); if (empty($xmlorganization->length)) { return null; } $organization = null; foreach ($xmlorganization as $org) { if (is_null($organization)) { // Use first if default nor found. $organization = $org; } if (!$org->attributes->getNamedItem('identifier')) { continue; } if ($default === $org->attributes->getNamedItem('identifier')->nodeValue) { // Found default - use it. $organization = $org; break; } } // Load all resources. $resources = array(); $xmlresources = $doc->getElementsByTagName('resource'); foreach ($xmlresources as $res) { if (!$identifier = $res->attributes->getNamedItem('identifier')) { continue; } $identifier = $identifier->nodeValue; if ($xmlbase = $res->baseURI) { // Undo the fake URL, we are interested in relative links only. $xmlbase = str_replace('http://grrr/', '/', $xmlbase); $xmlbase = rtrim($xmlbase, '/').'/'; } else { $xmlbase = ''; } if (!$href = $res->attributes->getNamedItem('href')) { // If href not found look for . $fileresources = $res->getElementsByTagName('file'); foreach ($fileresources as $file) { $href = $file->getAttribute('href'); } if (pathinfo($href, PATHINFO_EXTENSION) == 'xml') { $href = imscp_recursive_href($href, $imscp, $context); } if (empty($href)) { continue; } } else { $href = $href->nodeValue; } if (strpos($href, 'http://') !== 0) { $href = $xmlbase.$href; } // Item href cleanup - Some packages are poorly done and use \ in urls. $href = ltrim(strtr($href, "\\", '/'), '/'); $resources[$identifier] = $href; } $items = array(); foreach ($organization->childNodes as $child) { if ($child->nodeName === 'item') { if (!$item = imscp_recursive_item($child, 0, $resources)) { continue; } $items[] = $item; } } return $items; } function imscp_recursive_href($manifestfilename, $imscp, $context) { $fs = get_file_storage(); $dirname = dirname($manifestfilename); $filename = basename($manifestfilename); if ($dirname !== '/') { $dirname = "/$dirname/"; } if (!$manifestfile = $fs->get_file($context->id, 'mod_imscp', 'content', $imscp->revision, $dirname, $filename)) { return null; } $doc = new DOMDocument(); if (!$doc->loadXML($manifestfile->get_content(), LIBXML_NONET)) { return null; } $xmlresources = $doc->getElementsByTagName('resource'); foreach ($xmlresources as $res) { if (!$href = $res->attributes->getNamedItem('href')) { $fileresources = $res->getElementsByTagName('file'); foreach ($fileresources as $file) { $href = $file->getAttribute('href'); if (pathinfo($href, PATHINFO_EXTENSION) == 'xml') { $href = imscp_recursive_href($href, $imscp, $context); } if (pathinfo($href, PATHINFO_EXTENSION) == 'htm' || pathinfo($href, PATHINFO_EXTENSION) == 'html') { return $href; } } } } return $manifestfilename; } function imscp_recursive_item($xmlitem, $level, $resources) { $identifierref = ''; if ($identifierref = $xmlitem->attributes->getNamedItem('identifierref')) { $identifierref = $identifierref->nodeValue; } $title = '?'; $subitems = array(); foreach ($xmlitem->childNodes as $child) { if ($child->nodeName === 'title') { $title = $child->textContent; } else if ($child->nodeName === 'item') { if ($subitem = imscp_recursive_item($child, $level + 1, $resources)) { $subitems[] = $subitem; } } } return array('href' => isset($resources[$identifierref]) ? $resources[$identifierref] : '', 'title' => $title, 'level' => $level, 'subitems' => $subitems, ); } /** * File browsing support class * * @copyright 2009 Petr Skoda {@link http://skodak.org} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class imscp_file_info extends file_info { protected $course; protected $cm; protected $areas; protected $filearea; public function __construct($browser, $course, $cm, $context, $areas, $filearea) { parent::__construct($browser, $context); $this->course = $course; $this->cm = $cm; $this->areas = $areas; $this->filearea = $filearea; } /** * Returns list of standard virtual file/directory identification. * The difference from stored_file parameters is that null values * are allowed in all fields * @return array with keys contextid, filearea, itemid, filepath and filename */ public function get_params() { return array('contextid' => $this->context->id, 'component' => 'mod_imscp', 'filearea' => $this->filearea, 'itemid' => null, 'filepath' => null, 'filename' => null); } /** * Returns localised visible name. * @return string */ public function get_visible_name() { return $this->areas[$this->filearea]; } /** * Can I add new files or directories? * @return bool */ public function is_writable() { return false; } /** * Is directory? * @return bool */ public function is_directory() { return true; } /** * Returns list of children. * @return array of file_info instances */ public function get_children() { return $this->get_filtered_children('*', false, true); } /** * Help function to return files matching extensions or their count * * @param string|array $extensions, either '*' or array of lowercase extensions, i.e. array('.gif','.jpg') * @param bool|int $countonly if false returns the children, if an int returns just the * count of children but stops counting when $countonly number of children is reached * @param bool $returnemptyfolders if true returns items that don't have matching files inside * @return array|int array of file_info instances or the count */ private function get_filtered_children($extensions = '*', $countonly = false, $returnemptyfolders = false) { global $DB; $params = array('contextid' => $this->context->id, 'component' => 'mod_imscp', 'filearea' => $this->filearea); $sql = 'SELECT DISTINCT itemid FROM {files} WHERE contextid = :contextid AND component = :component AND filearea = :filearea'; if (!$returnemptyfolders) { $sql .= ' AND filename <> :emptyfilename'; $params['emptyfilename'] = '.'; } list($sql2, $params2) = $this->build_search_files_sql($extensions); $sql .= ' '.$sql2; $params = array_merge($params, $params2); if ($countonly !== false) { $sql .= ' ORDER BY itemid'; } $rs = $DB->get_recordset_sql($sql, $params); $children = array(); foreach ($rs as $record) { if ($child = $this->browser->get_file_info($this->context, 'mod_imscp', $this->filearea, $record->itemid)) { $children[] = $child; if ($countonly !== false && count($children) >= $countonly) { break; } } } $rs->close(); if ($countonly !== false) { return count($children); } return $children; } /** * Returns list of children which are either files matching the specified extensions * or folders that contain at least one such file. * * @param string|array $extensions, either '*' or array of lowercase extensions, i.e. array('.gif','.jpg') * @return array of file_info instances */ public function get_non_empty_children($extensions = '*') { return $this->get_filtered_children($extensions, false); } /** * Returns the number of children which are either files matching the specified extensions * or folders containing at least one such file. * * @param string|array $extensions, for example '*' or array('.gif','.jpg') * @param int $limit stop counting after at least $limit non-empty children are found * @return int */ public function count_non_empty_children($extensions = '*', $limit = 1) { return $this->get_filtered_children($extensions, $limit); } /** * Returns parent file_info instance * @return file_info or null for root */ public function get_parent() { return $this->browser->get_file_info($this->context); } } home/smkb8269/public_html/lms.smk-almanshuriyah.sch.id/mod/choice/locallib.php000064400000013341152267114530023174 0ustar00. /** * Internal library of functions for choice module. * * All the choice specific functions, needed to implement the module * logic, should go here. Never include this file from your lib.php! * * @package mod_choice * @copyright 2016 Stephen Bourget * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); /** * This creates new calendar events given as timeopen and timeclose by $choice. * * @param stdClass $choice * @return void */ function choice_set_events($choice) { global $DB, $CFG; require_once($CFG->dirroot.'/calendar/lib.php'); // Get CMID if not sent as part of $choice. if (!isset($choice->coursemodule)) { $cm = get_coursemodule_from_instance('choice', $choice->id, $choice->course); $choice->coursemodule = $cm->id; } // Choice start calendar events. $event = new stdClass(); $event->eventtype = CHOICE_EVENT_TYPE_OPEN; // The CHOICE_EVENT_TYPE_OPEN event should only be an action event if no close time is specified. $event->type = empty($choice->timeclose) ? CALENDAR_EVENT_TYPE_ACTION : CALENDAR_EVENT_TYPE_STANDARD; if ($event->id = $DB->get_field('event', 'id', array('modulename' => 'choice', 'instance' => $choice->id, 'eventtype' => $event->eventtype))) { if ((!empty($choice->timeopen)) && ($choice->timeopen > 0)) { // Calendar event exists so update it. $event->name = get_string('calendarstart', 'choice', $choice->name); $event->description = format_module_intro('choice', $choice, $choice->coursemodule, false); $event->format = FORMAT_HTML; $event->timestart = $choice->timeopen; $event->timesort = $choice->timeopen; $event->visible = instance_is_visible('choice', $choice); $event->timeduration = 0; $calendarevent = calendar_event::load($event->id); $calendarevent->update($event, false); } else { // Calendar event is on longer needed. $calendarevent = calendar_event::load($event->id); $calendarevent->delete(); } } else { // Event doesn't exist so create one. if ((!empty($choice->timeopen)) && ($choice->timeopen > 0)) { $event->name = get_string('calendarstart', 'choice', $choice->name); $event->description = format_module_intro('choice', $choice, $choice->coursemodule, false); $event->format = FORMAT_HTML; $event->courseid = $choice->course; $event->groupid = 0; $event->userid = 0; $event->modulename = 'choice'; $event->instance = $choice->id; $event->timestart = $choice->timeopen; $event->timesort = $choice->timeopen; $event->visible = instance_is_visible('choice', $choice); $event->timeduration = 0; calendar_event::create($event, false); } } // Choice end calendar events. $event = new stdClass(); $event->type = CALENDAR_EVENT_TYPE_ACTION; $event->eventtype = CHOICE_EVENT_TYPE_CLOSE; if ($event->id = $DB->get_field('event', 'id', array('modulename' => 'choice', 'instance' => $choice->id, 'eventtype' => $event->eventtype))) { if ((!empty($choice->timeclose)) && ($choice->timeclose > 0)) { // Calendar event exists so update it. $event->name = get_string('calendarend', 'choice', $choice->name); $event->description = format_module_intro('choice', $choice, $choice->coursemodule, false); $event->format = FORMAT_HTML; $event->timestart = $choice->timeclose; $event->timesort = $choice->timeclose; $event->visible = instance_is_visible('choice', $choice); $event->timeduration = 0; $calendarevent = calendar_event::load($event->id); $calendarevent->update($event, false); } else { // Calendar event is on longer needed. $calendarevent = calendar_event::load($event->id); $calendarevent->delete(); } } else { // Event doesn't exist so create one. if ((!empty($choice->timeclose)) && ($choice->timeclose > 0)) { $event->name = get_string('calendarend', 'choice', $choice->name); $event->description = format_module_intro('choice', $choice, $choice->coursemodule, false); $event->format = FORMAT_HTML; $event->courseid = $choice->course; $event->groupid = 0; $event->userid = 0; $event->modulename = 'choice'; $event->instance = $choice->id; $event->timestart = $choice->timeclose; $event->timesort = $choice->timeclose; $event->visible = instance_is_visible('choice', $choice); $event->timeduration = 0; calendar_event::create($event, false); } } } home/smkb8269/public_html/lms.smk-almanshuriyah.sch.id/mod/page/locallib.php000064400000003457152267114720022666 0ustar00. /** * Private page module utility functions * * @package mod_page * @copyright 2009 Petr Skoda (http://skodak.org) * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die; require_once("$CFG->libdir/filelib.php"); require_once("$CFG->libdir/resourcelib.php"); require_once("$CFG->dirroot/mod/page/lib.php"); /** * File browsing support class */ class page_content_file_info extends file_info_stored { public function get_parent() { if ($this->lf->get_filepath() === '/' and $this->lf->get_filename() === '.') { return $this->browser->get_file_info($this->context); } return parent::get_parent(); } public function get_visible_name() { if ($this->lf->get_filepath() === '/' and $this->lf->get_filename() === '.') { return $this->topvisiblename; } return parent::get_visible_name(); } } function page_get_editor_options($context) { global $CFG; return array('subdirs'=>1, 'maxbytes'=>$CFG->maxbytes, 'maxfiles'=>-1, 'changeformat'=>1, 'context'=>$context, 'noclean'=>1, 'trusttext'=>0); }home/smkb8269/public_html/lms.smk-almanshuriyah.sch.id/mod/resource/locallib.php000064400000047722152267115250023603 0ustar00. /** * Private resource module utility functions * * @package mod_resource * @copyright 2009 Petr Skoda {@link http://skodak.org} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die; require_once("$CFG->libdir/filelib.php"); require_once("$CFG->libdir/resourcelib.php"); require_once("$CFG->dirroot/mod/resource/lib.php"); /** * Redirected to migrated resource if needed, * return if incorrect parameters specified * @param int $oldid * @param int $cmid * @return void */ function resource_redirect_if_migrated($oldid, $cmid) { global $DB, $CFG; if ($oldid) { $old = $DB->get_record('resource_old', array('oldid'=>$oldid)); } else { $old = $DB->get_record('resource_old', array('cmid'=>$cmid)); } if (!$old) { return; } redirect("$CFG->wwwroot/mod/$old->newmodule/view.php?id=".$old->cmid); } /** * Display embedded resource file. * @param object $resource * @param object $cm * @param object $course * @param stored_file $file main file * @return does not return */ function resource_display_embed($resource, $cm, $course, $file) { global $PAGE, $OUTPUT; $clicktoopen = resource_get_clicktoopen($file, $resource->revision); $context = context_module::instance($cm->id); $moodleurl = moodle_url::make_pluginfile_url($context->id, 'mod_resource', 'content', $resource->revision, $file->get_filepath(), $file->get_filename()); $mimetype = $file->get_mimetype(); $title = $resource->name; $extension = resourcelib_get_extension($file->get_filename()); $mediamanager = core_media_manager::instance($PAGE); $embedoptions = array( core_media_manager::OPTION_TRUSTED => true, core_media_manager::OPTION_BLOCK => true, ); if (file_mimetype_in_typegroup($mimetype, 'web_image')) { // It's an image $code = resourcelib_embed_image($moodleurl->out(), $title); } else if ($mimetype === 'application/pdf') { // PDF document $code = resourcelib_embed_pdf($moodleurl->out(), $title, $clicktoopen); } else if ($mediamanager->can_embed_url($moodleurl, $embedoptions)) { // Media (audio/video) file. $code = $mediamanager->embed_url($moodleurl, $title, 0, 0, $embedoptions); } else { // We need a way to discover if we are loading remote docs inside an iframe. $moodleurl->param('embed', 1); // anything else - just try object tag enlarged as much as possible $code = resourcelib_embed_general($moodleurl, $title, $clicktoopen, $mimetype); } // Let the module handle the display. $PAGE->activityheader->set_description(resource_get_intro($resource, $cm)); resource_print_header($resource, $cm, $course); echo format_text($code, FORMAT_HTML, ['noclean' => true]); echo $OUTPUT->footer(); die; } /** * Display resource frames. * @param object $resource * @param object $cm * @param object $course * @param stored_file $file main file * @return does not return */ function resource_display_frame($resource, $cm, $course, $file) { global $PAGE, $OUTPUT, $CFG; $frame = optional_param('frameset', 'main', PARAM_ALPHA); if ($frame === 'top') { $PAGE->set_pagelayout('frametop'); $PAGE->activityheader->set_description(resource_get_intro($resource, $cm, true)); resource_print_header($resource, $cm, $course); echo $OUTPUT->footer(); die; } else { $config = get_config('resource'); $context = context_module::instance($cm->id); $path = '/'.$context->id.'/mod_resource/content/'.$resource->revision.$file->get_filepath().$file->get_filename(); $fileurl = file_encode_url($CFG->wwwroot.'/pluginfile.php', $path, false); $navurl = "$CFG->wwwroot/mod/resource/view.php?id=$cm->id&frameset=top"; $title = strip_tags(format_string($course->shortname.': '.$resource->name)); $framesize = $config->framesize; $contentframetitle = s(format_string($resource->name)); $modulename = s(get_string('modulename','resource')); $dir = get_string('thisdirection', 'langconfig'); $file = << $title EOF; @header('Content-Type: text/html; charset=utf-8'); echo $file; die; } } /** * Internal function - create click to open text with link. */ function resource_get_clicktoopen($file, $revision, $extra='') { global $CFG; $filename = $file->get_filename(); $path = '/'.$file->get_contextid().'/mod_resource/content/'.$revision.$file->get_filepath().$file->get_filename(); $fullurl = file_encode_url($CFG->wwwroot.'/pluginfile.php', $path, false); $string = get_string('clicktoopen2', 'resource', "$filename"); return $string; } /** * Internal function - create click to open text with link. */ function resource_get_clicktodownload($file, $revision) { global $CFG; $filename = $file->get_filename(); $path = '/'.$file->get_contextid().'/mod_resource/content/'.$revision.$file->get_filepath().$file->get_filename(); $fullurl = file_encode_url($CFG->wwwroot.'/pluginfile.php', $path, true); $string = get_string('clicktodownload', 'resource', "$filename"); return $string; } /** * Print resource info and workaround link when JS not available. * @param object $resource * @param object $cm * @param object $course * @param stored_file $file main file * @return does not return */ function resource_print_workaround($resource, $cm, $course, $file) { global $CFG, $OUTPUT, $PAGE; // Let the module handle the display. $PAGE->activityheader->set_description(resource_get_intro($resource, $cm, true)); resource_print_header($resource, $cm, $course); $resource->mainfile = $file->get_filename(); echo '
    '; switch (resource_get_final_display_type($resource)) { case RESOURCELIB_DISPLAY_POPUP: $path = '/'.$file->get_contextid().'/mod_resource/content/'.$resource->revision.$file->get_filepath().$file->get_filename(); $fullurl = file_encode_url($CFG->wwwroot.'/pluginfile.php', $path, false); $options = empty($resource->displayoptions) ? [] : (array) unserialize_array($resource->displayoptions); $width = empty($options['popupwidth']) ? 620 : $options['popupwidth']; $height = empty($options['popupheight']) ? 450 : $options['popupheight']; $wh = "width=$width,height=$height,toolbar=no,location=no,menubar=no,copyhistory=no,status=no,directories=no,scrollbars=yes,resizable=yes"; $extra = "onclick=\"window.open('$fullurl', '', '$wh'); return false;\""; echo resource_get_clicktoopen($file, $resource->revision, $extra); break; case RESOURCELIB_DISPLAY_NEW: $extra = 'onclick="this.target=\'_blank\'"'; echo resource_get_clicktoopen($file, $resource->revision, $extra); break; case RESOURCELIB_DISPLAY_DOWNLOAD: echo resource_get_clicktodownload($file, $resource->revision); break; case RESOURCELIB_DISPLAY_OPEN: default: echo resource_get_clicktoopen($file, $resource->revision); break; } echo '
    '; echo $OUTPUT->footer(); die; } /** * Print resource header. * @param object $resource * @param object $cm * @param object $course * @return void */ function resource_print_header($resource, $cm, $course) { global $PAGE, $OUTPUT; $PAGE->set_title($course->shortname.': '.$resource->name); $PAGE->set_heading($course->fullname); $PAGE->set_activity_record($resource); echo $OUTPUT->header(); } /** * Gets details of the file to cache in course cache to be displayed using {@link resource_get_optional_details()} * * @param object $resource Resource table row (only property 'displayoptions' is used here) * @param object $cm Course-module table row * @return string Size and type or empty string if show options are not enabled */ function resource_get_file_details($resource, $cm) { $options = empty($resource->displayoptions) ? [] : (array) unserialize_array($resource->displayoptions); $filedetails = array(); if (!empty($options['showsize']) || !empty($options['showtype']) || !empty($options['showdate'])) { $context = context_module::instance($cm->id); $fs = get_file_storage(); $files = $fs->get_area_files($context->id, 'mod_resource', 'content', 0, 'sortorder DESC, id ASC', false); // For a typical file resource, the sortorder is 1 for the main file // and 0 for all other files. This sort approach is used just in case // there are situations where the file has a different sort order. $mainfile = $files ? reset($files) : null; if (!empty($options['showsize'])) { $filedetails['size'] = 0; foreach ($files as $file) { // This will also synchronize the file size for external files if needed. $filedetails['size'] += $file->get_filesize(); if ($file->get_repository_id()) { // If file is a reference the 'size' attribute can not be cached. $filedetails['isref'] = true; } } } if (!empty($options['showtype'])) { if ($mainfile) { $filedetails['type'] = get_mimetype_description($mainfile); $filedetails['mimetype'] = $mainfile->get_mimetype(); $filedetails['extension'] = strtoupper(resourcelib_get_extension($mainfile->get_filename())); // Only show type if it is not unknown. if ($filedetails['type'] === get_mimetype_description('document/unknown')) { $filedetails['type'] = ''; } } else { $filedetails['type'] = ''; } } if (!empty($options['showdate'])) { if ($mainfile) { // Modified date may be up to several minutes later than uploaded date just because // teacher did not submit the form promptly. Give teacher up to 5 minutes to do it. if ($mainfile->get_timemodified() > $mainfile->get_timecreated() + 5 * MINSECS) { $filedetails['modifieddate'] = $mainfile->get_timemodified(); } else { $filedetails['uploadeddate'] = $mainfile->get_timecreated(); } if ($mainfile->get_repository_id()) { // If main file is a reference the 'date' attribute can not be cached. $filedetails['isref'] = true; } } else { $filedetails['uploadeddate'] = ''; } } } return $filedetails; } /** * Gets optional details for a resource, depending on resource settings. * * Result may include the file size and type if those settings are chosen, * or blank if none. * * @param object $resource Resource table row (only property 'displayoptions' is used here) * @param object $cm Course-module table row * @param bool $showtype Whether the file type should be displayed or not (regardless the display option is enabled). * @return string Size and type or empty string if show options are not enabled */ function resource_get_optional_details($resource, $cm, bool $showtype = true) { global $DB; $details = ''; $options = empty($resource->displayoptions) ? [] : (array) unserialize_array($resource->displayoptions); if (!empty($options['showsize']) || ($showtype && !empty($options['showtype'])) || !empty($options['showdate'])) { if (!array_key_exists('filedetails', $options)) { $filedetails = resource_get_file_details($resource, $cm); } else { $filedetails = $options['filedetails']; } $size = ''; $type = ''; $date = ''; $langstring = ''; $infodisplayed = 0; if (!empty($options['showsize'])) { if (!empty($filedetails['size'])) { $size = display_size($filedetails['size']); $langstring .= 'size'; $infodisplayed += 1; } } if ($showtype && !empty($options['showtype'])) { if (!empty($filedetails['type'])) { $type = $filedetails['extension']; $langstring .= 'type'; $infodisplayed += 1; } } if (!empty($options['showdate']) && (!empty($filedetails['modifieddate']) || !empty($filedetails['uploadeddate']))) { if (!empty($filedetails['modifieddate'])) { $date = get_string('modifieddate', 'mod_resource', userdate($filedetails['modifieddate'], get_string('strftimedatetimeshort', 'langconfig'))); } else if (!empty($filedetails['uploadeddate'])) { $date = get_string('uploadeddate', 'mod_resource', userdate($filedetails['uploadeddate'], get_string('strftimedatetimeshort', 'langconfig'))); } $langstring .= 'date'; $infodisplayed += 1; } if ($infodisplayed > 1) { $details = get_string("resourcedetails_{$langstring}", 'resource', (object)array('size' => $size, 'type' => $type, 'date' => $date)); } else { // Only one of size, type and date is set, so just append. $details = $size . $type . $date; } } return $details; } /** * Gets optional file type extension for a resource, depending on resource settings. * * @param object $resource Resource table row (only property 'displayoptions' is used here) * @param object $cm Course-module table row * @return string File extension or null if showtype option is not enabled */ function resource_get_optional_filetype($resource, $cm): ?string { $filetype = null; $options = empty($resource->displayoptions) ? [] : (array) unserialize_array($resource->displayoptions); if (empty($options['showtype'])) { // Show type option is disabled; early return null filetype. return $filetype; } if (!array_key_exists('filedetails', $options)) { $filedetails = resource_get_file_details($resource, $cm); } else { $filedetails = $options['filedetails']; } if (!empty($filedetails['type'])) { $filetype = $filedetails['extension']; } return $filetype; } /** * Get resource introduction. * * @param object $resource * @param object $cm * @param bool $ignoresettings print even if not specified in modedit * @return string */ function resource_get_intro(object $resource, object $cm, bool $ignoresettings = false): string { $options = empty($resource->displayoptions) ? [] : (array) unserialize_array($resource->displayoptions); $extraintro = resource_get_optional_details($resource, $cm); if ($extraintro) { // Put a paragaph tag around the details $extraintro = html_writer::tag('p', $extraintro, array('class' => 'resourcedetails')); } $content = ""; if ($ignoresettings || !empty($options['printintro']) || $extraintro) { $resourceintro = !empty($options['printintro']) && !html_is_blank($resource->intro); if ($resourceintro) { $content .= format_module_intro('resource', $resource, $cm->id); } if ($extraintro) { $content .= $extraintro; } } return $content; } /** * Print warning that instance not migrated yet. * @param object $resource * @param object $cm * @param object $course * @return void, does not return */ function resource_print_tobemigrated($resource, $cm, $course) { global $DB, $OUTPUT, $PAGE; $PAGE->activityheader->set_description(resource_get_intro($resource, $cm)); $resource_old = $DB->get_record('resource_old', array('oldid'=>$resource->id)); resource_print_header($resource, $cm, $course); echo $OUTPUT->notification(get_string('notmigrated', 'resource', $resource_old->type)); echo $OUTPUT->footer(); die; } /** * Print warning that file can not be found. * @param object $resource * @param object $cm * @param object $course * @return void, does not return */ function resource_print_filenotfound($resource, $cm, $course) { global $DB, $OUTPUT, $PAGE; $resource_old = $DB->get_record('resource_old', array('oldid'=>$resource->id)); $PAGE->activityheader->set_description(resource_get_intro($resource, $cm)); resource_print_header($resource, $cm, $course); if ($resource_old) { echo $OUTPUT->notification(get_string('notmigrated', 'resource', $resource_old->type)); } else { echo $OUTPUT->notification(get_string('filenotfound', 'resource')); } echo $OUTPUT->footer(); die; } /** * Decide the best display format. * @param object $resource * @return int display type constant */ function resource_get_final_display_type($resource) { global $CFG, $PAGE; if ($resource->display != RESOURCELIB_DISPLAY_AUTO) { return $resource->display; } if (empty($resource->mainfile)) { return RESOURCELIB_DISPLAY_DOWNLOAD; } else { $mimetype = mimeinfo('type', $resource->mainfile); } if (file_mimetype_in_typegroup($mimetype, 'archive')) { return RESOURCELIB_DISPLAY_DOWNLOAD; } if (file_mimetype_in_typegroup($mimetype, array('web_image', '.htm', 'web_video', 'web_audio'))) { return RESOURCELIB_DISPLAY_EMBED; } // let the browser deal with it somehow return RESOURCELIB_DISPLAY_OPEN; } /** * File browsing support class */ class resource_content_file_info extends file_info_stored { public function get_parent() { if ($this->lf->get_filepath() === '/' and $this->lf->get_filename() === '.') { return $this->browser->get_file_info($this->context); } return parent::get_parent(); } public function get_visible_name() { if ($this->lf->get_filepath() === '/' and $this->lf->get_filename() === '.') { return $this->topvisiblename; } return parent::get_visible_name(); } } function resource_set_mainfile($data) { global $DB; $fs = get_file_storage(); $cmid = $data->coursemodule; $draftitemid = $data->files; $context = context_module::instance($cmid); if ($draftitemid) { $options = array('subdirs' => true, 'embed' => false); if ($data->display == RESOURCELIB_DISPLAY_EMBED) { $options['embed'] = true; } file_save_draft_area_files($draftitemid, $context->id, 'mod_resource', 'content', 0, $options); } $files = $fs->get_area_files($context->id, 'mod_resource', 'content', 0, 'sortorder', false); if (count($files) == 1) { // only one file attached, set it as main file automatically $file = reset($files); file_set_sortorder($context->id, 'mod_resource', 'content', 0, $file->get_filepath(), $file->get_filename(), 1); } } home/smkb8269/public_html/lms.smk-almanshuriyah.sch.id/mod/glossary/locallib.php000064400000072452152267115530023616 0ustar00. /** * Library of functions and constants for module glossary * outside of what is required for the core moodle api * * @package mod_glossary * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ require_once($CFG->libdir . '/portfolio/caller.php'); require_once($CFG->libdir . '/filelib.php'); /** * class to handle exporting an entire glossary database */ class glossary_full_portfolio_caller extends portfolio_module_caller_base { private $glossary; private $exportdata; private $keyedfiles = array(); // keyed on entry /** * return array of expected call back arguments * and whether they are required or not * * @return array */ public static function expected_callbackargs() { return array( 'id' => true, ); } /** * load up all data required for this export. * * @return void */ public function load_data() { global $DB; if (!$this->cm = get_coursemodule_from_id('glossary', $this->id)) { throw new portfolio_caller_exception('invalidid', 'glossary'); } if (!$this->glossary = $DB->get_record('glossary', array('id' => $this->cm->instance))) { throw new portfolio_caller_exception('invalidid', 'glossary'); } $entries = $DB->get_records('glossary_entries', array('glossaryid' => $this->glossary->id)); list($where, $params) = $DB->get_in_or_equal(array_keys($entries)); $aliases = $DB->get_records_select('glossary_alias', 'entryid ' . $where, $params); $categoryentries = $DB->get_records_sql('SELECT ec.entryid, c.name FROM {glossary_entries_categories} ec JOIN {glossary_categories} c ON c.id = ec.categoryid WHERE ec.entryid ' . $where, $params); $this->exportdata = array('entries' => $entries, 'aliases' => $aliases, 'categoryentries' => $categoryentries); $fs = get_file_storage(); $context = context_module::instance($this->cm->id); $this->multifiles = array(); foreach (array_keys($entries) as $entry) { $this->keyedfiles[$entry] = array_merge( $fs->get_area_files($context->id, 'mod_glossary', 'attachment', $entry, "timemodified", false), $fs->get_area_files($context->id, 'mod_glossary', 'entry', $entry, "timemodified", false) ); $this->multifiles = array_merge($this->multifiles, $this->keyedfiles[$entry]); } } /** * how long might we expect this export to take * * @return constant one of PORTFOLIO_TIME_XX */ public function expected_time() { $filetime = portfolio_expected_time_file($this->multifiles); $dbtime = portfolio_expected_time_db(count($this->exportdata['entries'])); return ($filetime > $dbtime) ? $filetime : $dbtime; } /** * return the sha1 of this content * * @return string */ public function get_sha1() { $file = ''; if ($this->multifiles) { $file = $this->get_sha1_file(); } return sha1(serialize($this->exportdata) . $file); } /** * prepare the package ready to be passed off to the portfolio plugin * * @return void */ public function prepare_package() { $entries = $this->exportdata['entries']; $aliases = array(); $categories = array(); if (is_array($this->exportdata['aliases'])) { foreach ($this->exportdata['aliases'] as $alias) { if (!array_key_exists($alias->entryid, $aliases)) { $aliases[$alias->entryid] = array(); } $aliases[$alias->entryid][] = $alias->alias; } } if (is_array($this->exportdata['categoryentries'])) { foreach ($this->exportdata['categoryentries'] as $cat) { if (!array_key_exists($cat->entryid, $categories)) { $categories[$cat->entryid] = array(); } $categories[$cat->entryid][] = $cat->name; } } if ($this->get('exporter')->get('formatclass') == PORTFOLIO_FORMAT_SPREADSHEET) { $csv = glossary_generate_export_csv($entries, $aliases, $categories); $this->exporter->write_new_file($csv, clean_filename($this->cm->name) . '.csv', false); return; } else if ($this->get('exporter')->get('formatclass') == PORTFOLIO_FORMAT_LEAP2A) { $ids = array(); // keep track of these to make into a selection later global $USER, $DB; $writer = $this->get('exporter')->get('format')->leap2a_writer($USER); $format = $this->exporter->get('format'); $filename = $this->get('exporter')->get('format')->manifest_name(); foreach ($entries as $e) { $content = glossary_entry_portfolio_caller::entry_content( $this->course, $this->cm, $this->glossary, $e, (array_key_exists($e->id, $aliases) ? $aliases[$e->id] : array()), $format ); $entry = new portfolio_format_leap2a_entry('glossaryentry' . $e->id, $e->concept, 'entry', $content); $entry->author = $DB->get_record('user', array('id' => $e->userid), 'id,firstname,lastname,email'); $entry->published = $e->timecreated; $entry->updated = $e->timemodified; if (!empty($this->keyedfiles[$e->id])) { $writer->link_files($entry, $this->keyedfiles[$e->id], 'glossaryentry' . $e->id . 'file'); foreach ($this->keyedfiles[$e->id] as $file) { $this->exporter->copy_existing_file($file); } } if (!empty($categories[$e->id])) { foreach ($categories[$e->id] as $cat) { // this essentially treats them as plain tags // leap has the idea of category schemes // but I think this is overkill here $entry->add_category($cat); } } $writer->add_entry($entry); $ids[] = $entry->id; } $selection = new portfolio_format_leap2a_entry('wholeglossary' . $this->glossary->id, get_string('modulename', 'glossary'), 'selection'); $writer->add_entry($selection); $writer->make_selection($selection, $ids, 'Grouping'); $content = $writer->to_xml(); } $this->exporter->write_new_file($content, $filename, true); } /** * make sure that the current user is allowed to do this * * @return boolean */ public function check_permissions() { return has_capability('mod/glossary:export', context_module::instance($this->cm->id)); } /** * return a nice name to be displayed about this export location * * @return string */ public static function display_name() { return get_string('modulename', 'glossary'); } /** * what formats this function *generally* supports * * @return array */ public static function base_supported_formats() { return array(PORTFOLIO_FORMAT_SPREADSHEET, PORTFOLIO_FORMAT_LEAP2A); } } /** * class to export a single glossary entry * * @package mod_glossary * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class glossary_entry_portfolio_caller extends portfolio_module_caller_base { private $glossary; private $entry; protected $entryid; /** @var array Array that contains all aliases for the given glossary entry. */ private array $aliases = []; /** @var array categories. */ private array $categories = []; /* * @return array */ public static function expected_callbackargs() { return array( 'entryid' => true, 'id' => true, ); } /** * load up all data required for this export. * * @return void */ public function load_data() { global $DB; if (!$this->cm = get_coursemodule_from_id('glossary', $this->id)) { throw new portfolio_caller_exception('invalidid', 'glossary'); } if (!$this->glossary = $DB->get_record('glossary', array('id' => $this->cm->instance))) { throw new portfolio_caller_exception('invalidid', 'glossary'); } if ($this->entryid) { if (!$this->entry = $DB->get_record('glossary_entries', array('id' => $this->entryid))) { throw new portfolio_caller_exception('noentry', 'glossary'); } // in case we don't have USER this will make the entry be printed $this->entry->approved = true; } $this->categories = $DB->get_records_sql('SELECT ec.entryid, c.name FROM {glossary_entries_categories} ec JOIN {glossary_categories} c ON c.id = ec.categoryid WHERE ec.entryid = ?', array($this->entryid)); $context = context_module::instance($this->cm->id); if ($this->entry->sourceglossaryid == $this->cm->instance) { if ($maincm = get_coursemodule_from_instance('glossary', $this->entry->glossaryid)) { $context = context_module::instance($maincm->id); } } $this->aliases = $DB->get_records('glossary_alias', ['entryid' => $this->entryid]); $fs = get_file_storage(); $this->multifiles = array_merge( $fs->get_area_files($context->id, 'mod_glossary', 'attachment', $this->entry->id, "timemodified", false), $fs->get_area_files($context->id, 'mod_glossary', 'entry', $this->entry->id, "timemodified", false) ); if (!empty($this->multifiles)) { $this->add_format(PORTFOLIO_FORMAT_RICHHTML); } else { $this->add_format(PORTFOLIO_FORMAT_PLAINHTML); } } /** * how long might we expect this export to take * * @return constant one of PORTFOLIO_TIME_XX */ public function expected_time() { return PORTFOLIO_TIME_LOW; } /** * make sure that the current user is allowed to do this * * @return boolean */ public function check_permissions() { $context = context_module::instance($this->cm->id); return has_capability('mod/glossary:exportentry', $context) || ($this->entry->userid == $this->user->id && has_capability('mod/glossary:exportownentry', $context)); } /** * return a nice name to be displayed about this export location * * @return string */ public static function display_name() { return get_string('modulename', 'glossary'); } /** * prepare the package ready to be passed off to the portfolio plugin * * @return void */ public function prepare_package() { global $DB; $format = $this->exporter->get('format'); $content = self::entry_content($this->course, $this->cm, $this->glossary, $this->entry, $this->aliases, $format); if ($this->exporter->get('formatclass') === PORTFOLIO_FORMAT_PLAINHTML) { $filename = clean_filename($this->entry->concept) . '.html'; $this->exporter->write_new_file($content, $filename); } else if ($this->exporter->get('formatclass') === PORTFOLIO_FORMAT_RICHHTML) { if ($this->multifiles) { foreach ($this->multifiles as $file) { $this->exporter->copy_existing_file($file); } } $filename = clean_filename($this->entry->concept) . '.html'; $this->exporter->write_new_file($content, $filename); } else if ($this->exporter->get('formatclass') === PORTFOLIO_FORMAT_LEAP2A) { $writer = $this->get('exporter')->get('format')->leap2a_writer(); $entry = new portfolio_format_leap2a_entry('glossaryentry' . $this->entry->id, $this->entry->concept, 'entry', $content); $entry->author = $DB->get_record('user', array('id' => $this->entry->userid), 'id,firstname,lastname,email'); $entry->published = $this->entry->timecreated; $entry->updated = $this->entry->timemodified; if ($this->multifiles) { $writer->link_files($entry, $this->multifiles); foreach ($this->multifiles as $file) { $this->exporter->copy_existing_file($file); } } if ($this->categories) { foreach ($this->categories as $cat) { // this essentially treats them as plain tags // leap has the idea of category schemes // but I think this is overkill here $entry->add_category($cat->name); } } $writer->add_entry($entry); $content = $writer->to_xml(); $filename = $this->get('exporter')->get('format')->manifest_name(); $this->exporter->write_new_file($content, $filename); } else { throw new portfolio_caller_exception('unexpected_format_class', 'glossary'); } } /** * return the sha1 of this content * * @return string */ public function get_sha1() { if ($this->multifiles) { return sha1(serialize($this->entry) . $this->get_sha1_file()); } return sha1(serialize($this->entry)); } /** * what formats this function *generally* supports * * @return array */ public static function base_supported_formats() { return array(PORTFOLIO_FORMAT_RICHHTML, PORTFOLIO_FORMAT_PLAINHTML, PORTFOLIO_FORMAT_LEAP2A); } /** * helper function to get the html content of an entry * for both this class and the full glossary exporter * this is a very simplified version of the dictionary format output, * but with its 500 levels of indirection removed * and file rewriting handled by the portfolio export format. * * @param stdclass $course * @param stdclass $cm * @param stdclass $glossary * @param stdclass $entry * * @return string */ public static function entry_content($course, $cm, $glossary, $entry, $aliases, $format) { global $OUTPUT, $DB; $entry = clone $entry; $context = context_module::instance($cm->id); $options = portfolio_format_text_options(); $options->trusted = $entry->definitiontrust; $options->context = $context; $output = '' . "\n"; $output .= '' . "\n"; $output .= '' . "\n"; if (!empty($aliases)) { $output .= '' . "\n"; } if ($entry->sourceglossaryid == $cm->instance) { if (!$maincm = get_coursemodule_from_instance('glossary', $entry->glossaryid)) { return ''; } $filecontext = context_module::instance($maincm->id); } else { $filecontext = $context; } $fs = get_file_storage(); if ($files = $fs->get_area_files($filecontext->id, 'mod_glossary', 'attachment', $entry->id, "timemodified", false)) { $output .= '
    ' . "\n"; $output .= '
    '; $output .= format_text($OUTPUT->heading($entry->concept, 3), FORMAT_MOODLE, $options); $output .= '
    ' . "\n"; $entry->definition = format_text($entry->definition, $entry->definitionformat, $options); $output .= portfolio_rewrite_pluginfile_urls($entry->definition, $context->id, 'mod_glossary', 'entry', $entry->id, $format); if (isset($entry->footer)) { $output .= $entry->footer; } $output .= '
    '; $key = (count($aliases) == 1) ? 'alias' : 'aliases'; $output .= get_string($key, 'glossary') . ': '; foreach ($aliases as $alias) { $output .= s($alias->alias) . ','; } $output = substr($output, 0, -1); $output .= '
    ' . "\n"; foreach ($files as $file) { $output .= $format->file_output($file); } $output .= '
    ' . "\n"; } $output .= '' . "\n"; return $output; } } /** * Class representing the virtual node with all itemids in the file browser * * @category files * @copyright 2012 David Mudrak * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class glossary_file_info_container extends file_info { /** @var file_browser */ protected $browser; /** @var stdClass */ protected $course; /** @var stdClass */ protected $cm; /** @var string */ protected $component; /** @var stdClass */ protected $context; /** @var array */ protected $areas; /** @var string */ protected $filearea; /** * Constructor (in case you did not realize it ;-) * * @param file_browser $browser * @param stdClass $course * @param stdClass $cm * @param stdClass $context * @param array $areas * @param string $filearea */ public function __construct($browser, $course, $cm, $context, $areas, $filearea) { parent::__construct($browser, $context); $this->browser = $browser; $this->course = $course; $this->cm = $cm; $this->component = 'mod_glossary'; $this->context = $context; $this->areas = $areas; $this->filearea = $filearea; } /** * @return array with keys contextid, filearea, itemid, filepath and filename */ public function get_params() { return array( 'contextid' => $this->context->id, 'component' => $this->component, 'filearea' => $this->filearea, 'itemid' => null, 'filepath' => null, 'filename' => null, ); } /** * Can new files or directories be added via the file browser * * @return bool */ public function is_writable() { return false; } /** * Should this node be considered as a folder in the file browser * * @return bool */ public function is_directory() { return true; } /** * Returns localised visible name of this node * * @return string */ public function get_visible_name() { return $this->areas[$this->filearea]; } /** * Returns list of children nodes * * @return array of file_info instances */ public function get_children() { return $this->get_filtered_children('*', false, true); } /** * Help function to return files matching extensions or their count * * @param string|array $extensions, either '*' or array of lowercase extensions, i.e. array('.gif','.jpg') * @param bool|int $countonly if false returns the children, if an int returns just the * count of children but stops counting when $countonly number of children is reached * @param bool $returnemptyfolders if true returns items that don't have matching files inside * @return array|int array of file_info instances or the count */ private function get_filtered_children($extensions = '*', $countonly = false, $returnemptyfolders = false) { global $DB; $sql = 'SELECT DISTINCT f.itemid, ge.concept FROM {files} f JOIN {modules} m ON (m.name = :modulename AND m.visible = 1) JOIN {course_modules} cm ON (cm.module = m.id AND cm.id = :instanceid) JOIN {glossary} g ON g.id = cm.instance JOIN {glossary_entries} ge ON (ge.glossaryid = g.id AND ge.id = f.itemid) WHERE f.contextid = :contextid AND f.component = :component AND f.filearea = :filearea'; $params = array( 'modulename' => 'glossary', 'instanceid' => $this->context->instanceid, 'contextid' => $this->context->id, 'component' => $this->component, 'filearea' => $this->filearea); if (!$returnemptyfolders) { $sql .= ' AND f.filename <> :emptyfilename'; $params['emptyfilename'] = '.'; } list($sql2, $params2) = $this->build_search_files_sql($extensions, 'f'); $sql .= ' '.$sql2; $params = array_merge($params, $params2); if ($countonly !== false) { $sql .= ' ORDER BY ge.concept, f.itemid'; } $rs = $DB->get_recordset_sql($sql, $params); $children = array(); foreach ($rs as $record) { if ($child = $this->browser->get_file_info($this->context, 'mod_glossary', $this->filearea, $record->itemid)) { $children[] = $child; } if ($countonly !== false && count($children) >= $countonly) { break; } } $rs->close(); if ($countonly !== false) { return count($children); } return $children; } /** * Returns list of children which are either files matching the specified extensions * or folders that contain at least one such file. * * @param string|array $extensions, either '*' or array of lowercase extensions, i.e. array('.gif','.jpg') * @return array of file_info instances */ public function get_non_empty_children($extensions = '*') { return $this->get_filtered_children($extensions, false); } /** * Returns the number of children which are either files matching the specified extensions * or folders containing at least one such file. * * @param string|array $extensions, for example '*' or array('.gif','.jpg') * @param int $limit stop counting after at least $limit non-empty children are found * @return int */ public function count_non_empty_children($extensions = '*', $limit = 1) { return $this->get_filtered_children($extensions, $limit); } /** * Returns parent file_info instance * * @return file_info or null for root */ public function get_parent() { return $this->browser->get_file_info($this->context); } } /** * Returns glossary entries tagged with a specified tag. * * This is a callback used by the tag area mod_glossary/glossary_entries to search for glossary entries * tagged with a specific tag. * * @param core_tag_tag $tag * @param bool $exclusivemode if set to true it means that no other entities tagged with this tag * are displayed on the page and the per-page limit may be bigger * @param int $fromctx context id where the link was displayed, may be used by callbacks * to display items in the same context first * @param int $ctx context id where to search for records * @param bool $rec search in subcontexts as well * @param int $page 0-based number of page being displayed * @return \core_tag\output\tagindex */ function mod_glossary_get_tagged_entries($tag, $exclusivemode = false, $fromctx = 0, $ctx = 0, $rec = 1, $page = 0) { global $OUTPUT; $perpage = $exclusivemode ? 20 : 5; // Build the SQL query. $ctxselect = context_helper::get_preload_record_columns_sql('ctx'); $query = "SELECT ge.id, ge.concept, ge.glossaryid, ge.approved, ge.userid, cm.id AS cmid, c.id AS courseid, c.shortname, c.fullname, $ctxselect FROM {glossary_entries} ge JOIN {glossary} g ON g.id = ge.glossaryid JOIN {modules} m ON m.name='glossary' JOIN {course_modules} cm ON cm.module = m.id AND cm.instance = g.id JOIN {tag_instance} tt ON ge.id = tt.itemid JOIN {course} c ON cm.course = c.id JOIN {context} ctx ON ctx.instanceid = cm.id AND ctx.contextlevel = :coursemodulecontextlevel WHERE tt.itemtype = :itemtype AND tt.tagid = :tagid AND tt.component = :component AND cm.deletioninprogress = 0 AND ge.id %ITEMFILTER% AND c.id %COURSEFILTER%"; $params = array('itemtype' => 'glossary_entries', 'tagid' => $tag->id, 'component' => 'mod_glossary', 'coursemodulecontextlevel' => CONTEXT_MODULE); if ($ctx) { $context = $ctx ? context::instance_by_id($ctx) : context_system::instance(); $query .= $rec ? ' AND (ctx.id = :contextid OR ctx.path LIKE :path)' : ' AND ctx.id = :contextid'; $params['contextid'] = $context->id; $params['path'] = $context->path.'/%'; } $query .= " ORDER BY "; if ($fromctx) { // In order-clause specify that modules from inside "fromctx" context should be returned first. $fromcontext = context::instance_by_id($fromctx); $query .= ' (CASE WHEN ctx.id = :fromcontextid OR ctx.path LIKE :frompath THEN 0 ELSE 1 END),'; $params['fromcontextid'] = $fromcontext->id; $params['frompath'] = $fromcontext->path.'/%'; } $query .= ' c.sortorder, cm.id, ge.id'; $totalpages = $page + 1; // Use core_tag_index_builder to build and filter the list of items. $builder = new core_tag_index_builder('mod_glossary', 'glossary_entries', $query, $params, $page * $perpage, $perpage + 1); while ($item = $builder->has_item_that_needs_access_check()) { context_helper::preload_from_record($item); $courseid = $item->courseid; if (!$builder->can_access_course($courseid)) { $builder->set_accessible($item, false); continue; } $modinfo = get_fast_modinfo($builder->get_course($courseid)); // Set accessibility of this item and all other items in the same course. $builder->walk(function ($taggeditem) use ($courseid, $modinfo, $builder) { global $USER; if ($taggeditem->courseid == $courseid) { $accessible = false; if (($cm = $modinfo->get_cm($taggeditem->cmid)) && $cm->uservisible) { if ($taggeditem->approved) { $accessible = true; } else if ($taggeditem->userid == $USER->id) { $accessible = true; } else { $accessible = has_capability('mod/glossary:approve', context_module::instance($cm->id)); } } $builder->set_accessible($taggeditem, $accessible); } }); } $items = $builder->get_items(); if (count($items) > $perpage) { $totalpages = $page + 2; // We don't need exact page count, just indicate that the next page exists. array_pop($items); } // Build the display contents. if ($items) { $tagfeed = new core_tag\output\tagfeed(); foreach ($items as $item) { context_helper::preload_from_record($item); $modinfo = get_fast_modinfo($item->courseid); $cm = $modinfo->get_cm($item->cmid); $pageurl = new moodle_url('/mod/glossary/showentry.php', array('eid' => $item->id, 'displayformat' => 'dictionary')); $pagename = format_string($item->concept, true, array('context' => context_module::instance($item->cmid))); $pagename = html_writer::link($pageurl, $pagename); $courseurl = course_get_url($item->courseid, $cm->sectionnum); $cmname = html_writer::link($cm->url, $cm->get_formatted_name()); $coursename = format_string($item->fullname, true, array('context' => context_course::instance($item->courseid))); $coursename = html_writer::link($courseurl, $coursename); $icon = html_writer::link($pageurl, html_writer::empty_tag('img', array('src' => $cm->get_icon_url()))); $approved = ""; if (!$item->approved) { $approved = '
    '. html_writer::span(get_string('entrynotapproved', 'mod_glossary'), 'badge bg-warning text-dark'); } $tagfeed->add($icon, $pagename, $cmname.'
    '.$coursename.$approved); } $content = $OUTPUT->render_from_template('core_tag/tagfeed', $tagfeed->export_for_template($OUTPUT)); return new core_tag\output\tagindex($tag, 'mod_glossary', 'glossary_entries', $content, $exclusivemode, $fromctx, $ctx, $rec, $page, $totalpages); } } home/smkb8269/public_html/lms.smk-almanshuriyah.sch.id/mod/forum/locallib.php000064400000073021152267117300023071 0ustar00. /** * Library of functions for forum outside of the core api * * @package mod_forum * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ // Event types. define('FORUM_EVENT_TYPE_DUE', 'due'); require_once($CFG->dirroot . '/mod/forum/lib.php'); require_once($CFG->libdir . '/portfolio/caller.php'); /** * @package mod_forum * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class forum_portfolio_caller extends portfolio_module_caller_base { protected $postid; protected $discussionid; protected $attachment; private $post; private $forum; private $discussion; private $posts; private $keyedfiles; // just using multifiles isn't enough if we're exporting a full thread /** @var context_module context instance. */ private $modcontext; /** * @return array */ public static function expected_callbackargs() { return array( 'postid' => false, 'discussionid' => false, 'attachment' => false, ); } /** * @param array $callbackargs */ function __construct($callbackargs) { parent::__construct($callbackargs); if (!$this->postid && !$this->discussionid) { throw new portfolio_caller_exception('mustprovidediscussionorpost', 'forum'); } } /** * @global object */ public function load_data() { global $DB; if ($this->postid) { if (!$this->post = $DB->get_record('forum_posts', array('id' => $this->postid))) { throw new portfolio_caller_exception('invalidpostid', 'forum'); } } $dparams = array(); if ($this->discussionid) { $dbparams = array('id' => $this->discussionid); } else if ($this->post) { $dbparams = array('id' => $this->post->discussion); } else { throw new portfolio_caller_exception('mustprovidediscussionorpost', 'forum'); } if (!$this->discussion = $DB->get_record('forum_discussions', $dbparams)) { throw new portfolio_caller_exception('invaliddiscussionid', 'forum'); } if (!$this->forum = $DB->get_record('forum', array('id' => $this->discussion->forum))) { throw new portfolio_caller_exception('invalidforumid', 'forum'); } if (!$this->cm = get_coursemodule_from_instance('forum', $this->forum->id)) { throw new portfolio_caller_exception('invalidcoursemodule'); } $this->modcontext = context_module::instance($this->cm->id); $fs = get_file_storage(); if ($this->post) { if ($this->attachment) { // Make sure the requested file belongs to this post. $file = $fs->get_file_by_id($this->attachment); if ($file->get_contextid() != $this->modcontext->id || $file->get_itemid() != $this->post->id) { throw new portfolio_caller_exception('filenotfound'); } $this->set_file_and_format_data($this->attachment); } else { $attach = $fs->get_area_files($this->modcontext->id, 'mod_forum', 'attachment', $this->post->id, 'timemodified', false); $embed = $fs->get_area_files($this->modcontext->id, 'mod_forum', 'post', $this->post->id, 'timemodified', false); $files = array_merge($attach, $embed); $this->set_file_and_format_data($files); } if (!empty($this->multifiles)) { $this->keyedfiles[$this->post->id] = $this->multifiles; } else if (!empty($this->singlefile)) { $this->keyedfiles[$this->post->id] = array($this->singlefile); } } else { // whole thread $fs = get_file_storage(); $this->posts = forum_get_all_discussion_posts($this->discussion->id, 'p.created ASC'); $this->multifiles = array(); foreach ($this->posts as $post) { $attach = $fs->get_area_files($this->modcontext->id, 'mod_forum', 'attachment', $post->id, 'timemodified', false); $embed = $fs->get_area_files($this->modcontext->id, 'mod_forum', 'post', $post->id, 'timemodified', false); $files = array_merge($attach, $embed); if ($files) { $this->keyedfiles[$post->id] = $files; } else { continue; } $this->multifiles = array_merge($this->multifiles, array_values($this->keyedfiles[$post->id])); } } if (empty($this->multifiles) && !empty($this->singlefile)) { $this->multifiles = array($this->singlefile); // copy_files workaround } // depending on whether there are files or not, we might have to change richhtml/plainhtml if (empty($this->attachment)) { if (!empty($this->multifiles)) { $this->add_format(PORTFOLIO_FORMAT_RICHHTML); } else { $this->add_format(PORTFOLIO_FORMAT_PLAINHTML); } } } /** * @global object * @return string */ function get_return_url() { global $CFG; return $CFG->wwwroot . '/mod/forum/discuss.php?d=' . $this->discussion->id; } /** * @global object * @return array */ function get_navigation() { global $CFG; $navlinks = array(); $navlinks[] = array( 'name' => format_string($this->discussion->name), 'link' => $CFG->wwwroot . '/mod/forum/discuss.php?d=' . $this->discussion->id, 'type' => 'title' ); return array($navlinks, $this->cm); } /** * either a whole discussion * a single post, with or without attachment * or just an attachment with no post * * @global object * @global object * @uses PORTFOLIO_FORMAT_RICH * @return mixed */ function prepare_package() { global $CFG; // set up the leap2a writer if we need it $writingleap = false; if ($this->exporter->get('formatclass') == PORTFOLIO_FORMAT_LEAP2A) { $leapwriter = $this->exporter->get('format')->leap2a_writer(); $writingleap = true; } if ($this->attachment) { // simplest case first - single file attachment $this->copy_files(array($this->singlefile), $this->attachment); if ($writingleap) { // if we're writing leap, make the manifest to go along with the file $entry = new portfolio_format_leap2a_file($this->singlefile->get_filename(), $this->singlefile); $leapwriter->add_entry($entry); return $this->exporter->write_new_file($leapwriter->to_xml(), $this->exporter->get('format')->manifest_name(), true); } } else if (empty($this->post)) { // exporting whole discussion $content = ''; // if we're just writing HTML, start a string to add each post to $ids = array(); // if we're writing leap2a, keep track of all entryids so we can add a selection element foreach ($this->posts as $post) { $posthtml = $this->prepare_post($post); if ($writingleap) { $ids[] = $this->prepare_post_leap2a($leapwriter, $post, $posthtml); } else { $content .= $posthtml . '

    '; } } $this->copy_files($this->multifiles); $name = 'discussion.html'; $manifest = ($this->exporter->get('format') instanceof PORTFOLIO_FORMAT_RICH); if ($writingleap) { // add on an extra 'selection' entry $selection = new portfolio_format_leap2a_entry('forumdiscussion' . $this->discussionid, get_string('discussion', 'forum') . ': ' . $this->discussion->name, 'selection'); $leapwriter->add_entry($selection); $leapwriter->make_selection($selection, $ids, 'Grouping'); $content = $leapwriter->to_xml(); $name = $this->get('exporter')->get('format')->manifest_name(); } $this->get('exporter')->write_new_file($content, $name, $manifest); } else { // exporting a single post $posthtml = $this->prepare_post($this->post); $content = $posthtml; $name = 'post.html'; $manifest = ($this->exporter->get('format') instanceof PORTFOLIO_FORMAT_RICH); if ($writingleap) { $this->prepare_post_leap2a($leapwriter, $this->post, $posthtml); $content = $leapwriter->to_xml(); $name = $this->exporter->get('format')->manifest_name(); } $this->copy_files($this->multifiles); $this->get('exporter')->write_new_file($content, $name, $manifest); } } /** * helper function to add a leap2a entry element * that corresponds to a single forum post, * including any attachments * * the entry/ies are added directly to the leapwriter, which is passed by ref * * @param portfolio_format_leap2a_writer $leapwriter writer object to add entries to * @param object $post the stdclass object representing the database record * @param string $posthtml the content of the post (prepared by {@link prepare_post} * * @return int id of new entry */ private function prepare_post_leap2a(portfolio_format_leap2a_writer $leapwriter, $post, $posthtml) { $entry = new portfolio_format_leap2a_entry('forumpost' . $post->id, $post->subject, 'resource', $posthtml); $entry->published = $post->created; $entry->updated = $post->modified; $entry->author = $post->author; if (is_array($this->keyedfiles) && array_key_exists($post->id, $this->keyedfiles) && is_array($this->keyedfiles[$post->id])) { $leapwriter->link_files($entry, $this->keyedfiles[$post->id], 'forumpost' . $post->id . 'attachment'); } $entry->add_category('web', 'resource_type'); $leapwriter->add_entry($entry); return $entry->id; } /** * @param array $files * @param mixed $justone false of id of single file to copy * @return bool|void */ private function copy_files($files, $justone=false) { if (empty($files)) { return; } foreach ($files as $f) { if ($justone && $f->get_id() != $justone) { continue; } $this->get('exporter')->copy_existing_file($f); if ($justone && $f->get_id() == $justone) { return true; // all we need to do } } } /** * this is a very cut down version of what is in forum_make_mail_post * * @global object * @param int $post * @return string */ private function prepare_post($post, $fileoutputextras=null) { global $DB; static $users; if (empty($users)) { $users = array($this->user->id => $this->user); } if (!array_key_exists($post->userid, $users)) { $users[$post->userid] = $DB->get_record('user', array('id' => $post->userid)); } // add the user object on to the post so we can pass it to the leap writer if necessary $post->author = $users[$post->userid]; $viewfullnames = true; // format the post body $options = portfolio_format_text_options(); $options->context = $this->modcontext; $format = $this->get('exporter')->get('format'); $formattedtext = format_text($post->message, $post->messageformat, $options); $formattedtext = portfolio_rewrite_pluginfile_urls($formattedtext, $this->modcontext->id, 'mod_forum', 'post', $post->id, $format); $output = ''; $output .= ''; if ($post->parent) { $output .= ''; $output .= '
    ';// can't print picture. $output .= ''; } else { $output .= ''; } $output .= '
    '.format_string($post->subject).'
    '; $fullname = fullname($users[$post->userid], $viewfullnames); $by = new stdClass(); $by->name = $fullname; $by->date = userdate($post->modified, '', core_date::get_user_timezone($this->user)); $output .= '
    '.get_string('bynameondate', 'forum', $by).'
    '; $output .= '
    '; $output .= ''; $output .= $formattedtext; if (is_array($this->keyedfiles) && array_key_exists($post->id, $this->keyedfiles) && is_array($this->keyedfiles[$post->id]) && count($this->keyedfiles[$post->id]) > 0) { $output .= '
    '; $output .= '
    ' . get_string('attachments', 'forum') . ':

    '; foreach ($this->keyedfiles[$post->id] as $file) { $output .= $format->file_output($file) . '
    '; } $output .= "
    "; } $output .= '
    '."\n\n"; return $output; } /** * @return string */ function get_sha1() { $filesha = ''; try { $filesha = $this->get_sha1_file(); } catch (portfolio_caller_exception $e) { } // no files if ($this->post) { return sha1($filesha . ',' . $this->post->subject . ',' . $this->post->message); } else { $sha1s = array($filesha); foreach ($this->posts as $post) { $sha1s[] = sha1($post->subject . ',' . $post->message); } return sha1(implode(',', $sha1s)); } } function expected_time() { $filetime = $this->expected_time_file(); if ($this->posts) { $posttime = portfolio_expected_time_db(count($this->posts)); if ($filetime < $posttime) { return $posttime; } } return $filetime; } /** * @uses CONTEXT_MODULE * @return bool */ function check_permissions() { $context = context_module::instance($this->cm->id); if ($this->post) { return (has_capability('mod/forum:exportpost', $context) || ($this->post->userid == $this->user->id && has_capability('mod/forum:exportownpost', $context))); } return has_capability('mod/forum:exportdiscussion', $context); } /** * @return string */ public static function display_name() { return get_string('modulename', 'forum'); } public static function base_supported_formats() { return array(PORTFOLIO_FORMAT_FILE, PORTFOLIO_FORMAT_RICHHTML, PORTFOLIO_FORMAT_PLAINHTML, PORTFOLIO_FORMAT_LEAP2A); } } /** * Class representing the virtual node with all itemids in the file browser * * @category files * @copyright 2012 David Mudrak * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class forum_file_info_container extends file_info { /** @var file_browser */ protected $browser; /** @var stdClass */ protected $course; /** @var stdClass */ protected $cm; /** @var string */ protected $component; /** @var stdClass */ protected $context; /** @var array */ protected $areas; /** @var string */ protected $filearea; /** * Constructor (in case you did not realize it ;-) * * @param file_browser $browser * @param stdClass $course * @param stdClass $cm * @param stdClass $context * @param array $areas * @param string $filearea */ public function __construct($browser, $course, $cm, $context, $areas, $filearea) { parent::__construct($browser, $context); $this->browser = $browser; $this->course = $course; $this->cm = $cm; $this->component = 'mod_forum'; $this->context = $context; $this->areas = $areas; $this->filearea = $filearea; } /** * @return array with keys contextid, filearea, itemid, filepath and filename */ public function get_params() { return array( 'contextid' => $this->context->id, 'component' => $this->component, 'filearea' => $this->filearea, 'itemid' => null, 'filepath' => null, 'filename' => null, ); } /** * Can new files or directories be added via the file browser * * @return bool */ public function is_writable() { return false; } /** * Should this node be considered as a folder in the file browser * * @return bool */ public function is_directory() { return true; } /** * Returns localised visible name of this node * * @return string */ public function get_visible_name() { return $this->areas[$this->filearea]; } /** * Returns list of children nodes * * @return array of file_info instances */ public function get_children() { return $this->get_filtered_children('*', false, true); } /** * Help function to return files matching extensions or their count * * @param string|array $extensions, either '*' or array of lowercase extensions, i.e. array('.gif','.jpg') * @param bool|int $countonly if false returns the children, if an int returns just the * count of children but stops counting when $countonly number of children is reached * @param bool $returnemptyfolders if true returns items that don't have matching files inside * @return array|int array of file_info instances or the count */ private function get_filtered_children($extensions = '*', $countonly = false, $returnemptyfolders = false) { global $DB; $params = array('contextid' => $this->context->id, 'component' => $this->component, 'filearea' => $this->filearea); $sql = 'SELECT DISTINCT itemid FROM {files} WHERE contextid = :contextid AND component = :component AND filearea = :filearea'; if (!$returnemptyfolders) { $sql .= ' AND filename <> :emptyfilename'; $params['emptyfilename'] = '.'; } list($sql2, $params2) = $this->build_search_files_sql($extensions); $sql .= ' '.$sql2; $params = array_merge($params, $params2); if ($countonly !== false) { $sql .= ' ORDER BY itemid DESC'; } $rs = $DB->get_recordset_sql($sql, $params); $children = array(); foreach ($rs as $record) { if (($child = $this->browser->get_file_info($this->context, 'mod_forum', $this->filearea, $record->itemid)) && ($returnemptyfolders || $child->count_non_empty_children($extensions))) { $children[] = $child; } if ($countonly !== false && count($children) >= $countonly) { break; } } $rs->close(); if ($countonly !== false) { return count($children); } return $children; } /** * Returns list of children which are either files matching the specified extensions * or folders that contain at least one such file. * * @param string|array $extensions, either '*' or array of lowercase extensions, i.e. array('.gif','.jpg') * @return array of file_info instances */ public function get_non_empty_children($extensions = '*') { return $this->get_filtered_children($extensions, false); } /** * Returns the number of children which are either files matching the specified extensions * or folders containing at least one such file. * * @param string|array $extensions, for example '*' or array('.gif','.jpg') * @param int $limit stop counting after at least $limit non-empty children are found * @return int */ public function count_non_empty_children($extensions = '*', $limit = 1) { return $this->get_filtered_children($extensions, $limit); } /** * Returns parent file_info instance * * @return file_info or null for root */ public function get_parent() { return $this->browser->get_file_info($this->context); } } /** * Returns forum posts tagged with a specified tag. * * This is a callback used by the tag area mod_forum/forum_posts to search for forum posts * tagged with a specific tag. * * @param core_tag_tag $tag * @param bool $exclusivemode if set to true it means that no other entities tagged with this tag * are displayed on the page and the per-page limit may be bigger * @param int $fromctx context id where the link was displayed, may be used by callbacks * to display items in the same context first * @param int $ctx context id where to search for records * @param bool $rec search in subcontexts as well * @param int $page 0-based number of page being displayed * @return \core_tag\output\tagindex */ function mod_forum_get_tagged_posts($tag, $exclusivemode = false, $fromctx = 0, $ctx = 0, $rec = 1, $page = 0) { global $OUTPUT; $perpage = $exclusivemode ? 20 : 5; // Build the SQL query. $ctxselect = context_helper::get_preload_record_columns_sql('ctx'); $query = "SELECT fp.id, fp.subject, fd.forum, fp.discussion, f.type, fd.timestart, fd.timeend, fd.groupid, fd.firstpost, fp.parent, fp.userid, cm.id AS cmid, c.id AS courseid, c.shortname, c.fullname, $ctxselect FROM {forum_posts} fp JOIN {forum_discussions} fd ON fp.discussion = fd.id JOIN {forum} f ON f.id = fd.forum JOIN {modules} m ON m.name='forum' JOIN {course_modules} cm ON cm.module = m.id AND cm.instance = f.id JOIN {tag_instance} tt ON fp.id = tt.itemid JOIN {course} c ON cm.course = c.id JOIN {context} ctx ON ctx.instanceid = cm.id AND ctx.contextlevel = :coursemodulecontextlevel WHERE tt.itemtype = :itemtype AND tt.tagid = :tagid AND tt.component = :component AND cm.deletioninprogress = 0 AND fp.id %ITEMFILTER% AND c.id %COURSEFILTER%"; $params = array('itemtype' => 'forum_posts', 'tagid' => $tag->id, 'component' => 'mod_forum', 'coursemodulecontextlevel' => CONTEXT_MODULE); if ($ctx) { $context = $ctx ? context::instance_by_id($ctx) : context_system::instance(); $query .= $rec ? ' AND (ctx.id = :contextid OR ctx.path LIKE :path)' : ' AND ctx.id = :contextid'; $params['contextid'] = $context->id; $params['path'] = $context->path.'/%'; } $query .= " ORDER BY "; if ($fromctx) { // In order-clause specify that modules from inside "fromctx" context should be returned first. $fromcontext = context::instance_by_id($fromctx); $query .= ' (CASE WHEN ctx.id = :fromcontextid OR ctx.path LIKE :frompath THEN 0 ELSE 1 END),'; $params['fromcontextid'] = $fromcontext->id; $params['frompath'] = $fromcontext->path.'/%'; } $query .= ' c.sortorder, cm.id, fp.id'; $totalpages = $page + 1; // Use core_tag_index_builder to build and filter the list of items. $builder = new core_tag_index_builder('mod_forum', 'forum_posts', $query, $params, $page * $perpage, $perpage + 1); while ($item = $builder->has_item_that_needs_access_check()) { context_helper::preload_from_record($item); $courseid = $item->courseid; if (!$builder->can_access_course($courseid)) { $builder->set_accessible($item, false); continue; } $modinfo = get_fast_modinfo($builder->get_course($courseid)); // Set accessibility of this item and all other items in the same course. $builder->walk(function ($taggeditem) use ($courseid, $modinfo, $builder, $item) { // Checking permission for Q&A forums performs additional DB queries, do not do them in bulk. if ($taggeditem->courseid == $courseid && ($taggeditem->type != 'qanda' || $taggeditem->id == $item->id)) { $cm = $modinfo->get_cm($taggeditem->cmid); $forum = (object)['id' => $taggeditem->forum, 'course' => $taggeditem->courseid, 'type' => $taggeditem->type ]; $discussion = (object)['id' => $taggeditem->discussion, 'timestart' => $taggeditem->timestart, 'timeend' => $taggeditem->timeend, 'groupid' => $taggeditem->groupid, 'firstpost' => $taggeditem->firstpost ]; $post = (object)['id' => $taggeditem->id, 'parent' => $taggeditem->parent, 'userid' => $taggeditem->userid, 'groupid' => $taggeditem->groupid ]; $accessible = forum_user_can_see_post($forum, $discussion, $post, null, $cm); $builder->set_accessible($taggeditem, $accessible); } }); } $items = $builder->get_items(); if (count($items) > $perpage) { $totalpages = $page + 2; // We don't need exact page count, just indicate that the next page exists. array_pop($items); } // Build the display contents. if ($items) { $tagfeed = new core_tag\output\tagfeed(); foreach ($items as $item) { context_helper::preload_from_record($item); $modinfo = get_fast_modinfo($item->courseid); $cm = $modinfo->get_cm($item->cmid); $pageurl = new moodle_url('/mod/forum/discuss.php', array('d' => $item->discussion), 'p' . $item->id); $pagename = format_string($item->subject, true, array('context' => context_module::instance($item->cmid))); $pagename = html_writer::link($pageurl, $pagename); $courseurl = course_get_url($item->courseid, $cm->sectionnum); $cmname = html_writer::link($cm->url, $cm->get_formatted_name()); $coursename = format_string($item->fullname, true, array('context' => context_course::instance($item->courseid))); $coursename = html_writer::link($courseurl, $coursename); $icon = html_writer::link($pageurl, html_writer::empty_tag('img', array('src' => $cm->get_icon_url()))); $tagfeed->add($icon, $pagename, $cmname.'
    '.$coursename); } $content = $OUTPUT->render_from_template('core_tag/tagfeed', $tagfeed->export_for_template($OUTPUT)); return new core_tag\output\tagindex($tag, 'mod_forum', 'forum_posts', $content, $exclusivemode, $fromctx, $ctx, $rec, $page, $totalpages); } } /** * Update the calendar entries for this forum activity. * * @param stdClass $forum the row from the database table forum. * @param int $cmid The coursemodule id * @return bool */ function forum_update_calendar($forum, $cmid) { global $DB, $CFG; require_once($CFG->dirroot.'/calendar/lib.php'); $event = new stdClass(); if (!empty($forum->duedate)) { $event->name = get_string('calendardue', 'forum', $forum->name); $event->description = format_module_intro('forum', $forum, $cmid, false); $event->format = FORMAT_HTML; $event->courseid = $forum->course; $event->modulename = 'forum'; $event->instance = $forum->id; $event->type = CALENDAR_EVENT_TYPE_ACTION; $event->eventtype = FORUM_EVENT_TYPE_DUE; $event->timestart = $forum->duedate; $event->timesort = $forum->duedate; $event->visible = instance_is_visible('forum', $forum); } $event->id = $DB->get_field('event', 'id', array('modulename' => 'forum', 'instance' => $forum->id, 'eventtype' => FORUM_EVENT_TYPE_DUE)); if ($event->id) { $calendarevent = calendar_event::load($event->id); if (!empty($forum->duedate)) { // Calendar event exists so update it. $calendarevent->update($event); } else { // Calendar event is no longer needed. $calendarevent->delete(); } } else if (!empty($forum->duedate)) { // Event doesn't exist so create one. calendar_event::create($event); } return true; }home/smkb8269/public_html/lms.smk-almanshuriyah.sch.id/mod/scorm/locallib.php000064400000311252152267120010023055 0ustar00. /** * Library of internal classes and functions for module SCORM * * @package mod_scorm * @copyright 1999 onwards Roberto Pinna * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ require_once("$CFG->dirroot/mod/scorm/lib.php"); require_once("$CFG->libdir/filelib.php"); // Constants and settings for module scorm. define('SCORM_UPDATE_NEVER', '0'); define('SCORM_UPDATE_EVERYDAY', '2'); define('SCORM_UPDATE_EVERYTIME', '3'); define('SCORM_SKIPVIEW_NEVER', '0'); define('SCORM_SKIPVIEW_FIRST', '1'); define('SCORM_SKIPVIEW_ALWAYS', '2'); define('SCO_ALL', 0); define('SCO_DATA', 1); define('SCO_ONLY', 2); define('GRADESCOES', '0'); define('GRADEHIGHEST', '1'); define('GRADEAVERAGE', '2'); define('GRADESUM', '3'); define('HIGHESTATTEMPT', '0'); define('AVERAGEATTEMPT', '1'); define('FIRSTATTEMPT', '2'); define('LASTATTEMPT', '3'); define('TOCJSLINK', 1); define('TOCFULLURL', 2); define('SCORM_FORCEATTEMPT_NO', 0); define('SCORM_FORCEATTEMPT_ONCOMPLETE', 1); define('SCORM_FORCEATTEMPT_ALWAYS', 2); // Local Library of functions for module scorm. /** * @package mod_scorm * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class scorm_package_file_info extends file_info_stored { public function get_parent() { if ($this->lf->get_filepath() === '/' and $this->lf->get_filename() === '.') { return $this->browser->get_file_info($this->context); } return parent::get_parent(); } public function get_visible_name() { if ($this->lf->get_filepath() === '/' and $this->lf->get_filename() === '.') { return $this->topvisiblename; } return parent::get_visible_name(); } } /** * Returns an array of the popup options for SCORM and each options default value * * @return array an array of popup options as the key and their defaults as the value */ function scorm_get_popup_options_array() { $cfgscorm = get_config('scorm'); return array('scrollbars' => isset($cfgscorm->scrollbars) ? $cfgscorm->scrollbars : 0, 'directories' => isset($cfgscorm->directories) ? $cfgscorm->directories : 0, 'location' => isset($cfgscorm->location) ? $cfgscorm->location : 0, 'menubar' => isset($cfgscorm->menubar) ? $cfgscorm->menubar : 0, 'toolbar' => isset($cfgscorm->toolbar) ? $cfgscorm->toolbar : 0, 'status' => isset($cfgscorm->status) ? $cfgscorm->status : 0); } /** * Returns an array of the array of what grade options * * @return array an array of what grade options */ function scorm_get_grade_method_array() { return array (GRADESCOES => get_string('gradescoes', 'scorm'), GRADEHIGHEST => get_string('gradehighest', 'scorm'), GRADEAVERAGE => get_string('gradeaverage', 'scorm'), GRADESUM => get_string('gradesum', 'scorm')); } /** * Returns an array of the array of what grade options * * @return array an array of what grade options */ function scorm_get_what_grade_array() { return array (HIGHESTATTEMPT => get_string('highestattempt', 'scorm'), AVERAGEATTEMPT => get_string('averageattempt', 'scorm'), FIRSTATTEMPT => get_string('firstattempt', 'scorm'), LASTATTEMPT => get_string('lastattempt', 'scorm')); } /** * Returns an array of the array of skip view options * * @return array an array of skip view options */ function scorm_get_skip_view_array() { return array(SCORM_SKIPVIEW_NEVER => get_string('never'), SCORM_SKIPVIEW_FIRST => get_string('firstaccess', 'scorm'), SCORM_SKIPVIEW_ALWAYS => get_string('always')); } /** * Returns an array of the array of hide table of contents options * * @return array an array of hide table of contents options */ function scorm_get_hidetoc_array() { return array(SCORM_TOC_SIDE => get_string('sided', 'scorm'), SCORM_TOC_HIDDEN => get_string('hidden', 'scorm'), SCORM_TOC_POPUP => get_string('popupmenu', 'scorm'), SCORM_TOC_DISABLED => get_string('disabled', 'scorm')); } /** * Returns an array of the array of update frequency options * * @return array an array of update frequency options */ function scorm_get_updatefreq_array() { return array(SCORM_UPDATE_NEVER => get_string('never'), SCORM_UPDATE_EVERYDAY => get_string('everyday', 'scorm'), SCORM_UPDATE_EVERYTIME => get_string('everytime', 'scorm')); } /** * Returns an array of the array of popup display options * * @return array an array of popup display options */ function scorm_get_popup_display_array() { return array(0 => get_string('currentwindow', 'scorm'), 1 => get_string('popup', 'scorm')); } /** * Returns an array of the array of navigation buttons display options * * @return array an array of navigation buttons display options */ function scorm_get_navigation_display_array() { return array(SCORM_NAV_DISABLED => get_string('no'), SCORM_NAV_UNDER_CONTENT => get_string('undercontent', 'scorm'), SCORM_NAV_FLOATING => get_string('floating', 'scorm')); } /** * Returns an array of the array of attempt options * * @return array an array of attempt options */ function scorm_get_attempts_array() { $attempts = array(0 => get_string('nolimit', 'scorm'), 1 => get_string('attempt1', 'scorm')); for ($i = 2; $i <= 6; $i++) { $attempts[$i] = get_string('attemptsx', 'scorm', $i); } return $attempts; } /** * Returns an array of the attempt status options * * @return array an array of attempt status options */ function scorm_get_attemptstatus_array() { return array(SCORM_DISPLAY_ATTEMPTSTATUS_NO => get_string('no'), SCORM_DISPLAY_ATTEMPTSTATUS_ALL => get_string('attemptstatusall', 'scorm'), SCORM_DISPLAY_ATTEMPTSTATUS_MY => get_string('attemptstatusmy', 'scorm'), SCORM_DISPLAY_ATTEMPTSTATUS_ENTRY => get_string('attemptstatusentry', 'scorm')); } /** * Returns an array of the force attempt options * * @return array an array of attempt options */ function scorm_get_forceattempt_array() { return array(SCORM_FORCEATTEMPT_NO => get_string('no'), SCORM_FORCEATTEMPT_ONCOMPLETE => get_string('forceattemptoncomplete', 'scorm'), SCORM_FORCEATTEMPT_ALWAYS => get_string('forceattemptalways', 'scorm')); } /** * Extracts scrom package, sets up all variables. * Called whenever scorm changes * @param object $scorm instance - fields are updated and changes saved into database * @param bool $full force full update if true * @return void */ function scorm_parse($scorm, $full) { global $CFG, $DB; $cfgscorm = get_config('scorm'); if (!isset($scorm->cmid)) { $cm = get_coursemodule_from_instance('scorm', $scorm->id); $scorm->cmid = $cm->id; } $context = context_module::instance($scorm->cmid); $newhash = $scorm->sha1hash; if ($scorm->scormtype === SCORM_TYPE_LOCAL or $scorm->scormtype === SCORM_TYPE_LOCALSYNC) { $fs = get_file_storage(); $packagefile = false; $packagefileimsmanifest = false; if ($scorm->scormtype === SCORM_TYPE_LOCAL) { if ($packagefile = $fs->get_file($context->id, 'mod_scorm', 'package', 0, '/', $scorm->reference)) { if ($packagefile->is_external_file()) { // Get zip file so we can check it is correct. $packagefile->import_external_file_contents(); } $newhash = $packagefile->get_contenthash(); if (strtolower($packagefile->get_filename()) == 'imsmanifest.xml') { $packagefileimsmanifest = true; } } else { $newhash = null; } } else { if (!$cfgscorm->allowtypelocalsync) { // Sorry - localsync disabled. return; } if ($scorm->reference !== '') { $fs->delete_area_files($context->id, 'mod_scorm', 'package'); $filerecord = array('contextid' => $context->id, 'component' => 'mod_scorm', 'filearea' => 'package', 'itemid' => 0, 'filepath' => '/'); if ($packagefile = $fs->create_file_from_url($filerecord, $scorm->reference, array('calctimeout' => true), true)) { $newhash = $packagefile->get_contenthash(); } else { $newhash = null; } } } if ($packagefile) { if (!$full and $packagefile and $scorm->sha1hash === $newhash) { if (strpos($scorm->version, 'SCORM') !== false) { if ($packagefileimsmanifest || $fs->get_file($context->id, 'mod_scorm', 'content', 0, '/', 'imsmanifest.xml')) { // No need to update. return; } } else if (strpos($scorm->version, 'AICC') !== false) { // TODO: add more sanity checks - something really exists in scorm_content area. return; } } if (!$packagefileimsmanifest) { // Now extract files. $fs->delete_area_files($context->id, 'mod_scorm', 'content'); $packer = get_file_packer('application/zip'); $packagefile->extract_to_storage($packer, $context->id, 'mod_scorm', 'content', 0, '/'); } } else if (!$full) { return; } if ($packagefileimsmanifest) { require_once("$CFG->dirroot/mod/scorm/datamodels/scormlib.php"); // Direct link to imsmanifest.xml file. if (!scorm_parse_scorm($scorm, $packagefile)) { $scorm->version = 'ERROR'; } } else if ($manifest = $fs->get_file($context->id, 'mod_scorm', 'content', 0, '/', 'imsmanifest.xml')) { require_once("$CFG->dirroot/mod/scorm/datamodels/scormlib.php"); // SCORM. if (!scorm_parse_scorm($scorm, $manifest)) { $scorm->version = 'ERROR'; } } else { require_once("$CFG->dirroot/mod/scorm/datamodels/aicclib.php"); // AICC. $result = scorm_parse_aicc($scorm); if (!$result) { $scorm->version = 'ERROR'; } else { $scorm->version = 'AICC'; } } } else if ($scorm->scormtype === SCORM_TYPE_EXTERNAL and $cfgscorm->allowtypeexternal) { require_once("$CFG->dirroot/mod/scorm/datamodels/scormlib.php"); // SCORM only, AICC can not be external. if (!scorm_parse_scorm($scorm, $scorm->reference)) { $scorm->version = 'ERROR'; } $newhash = sha1($scorm->reference); } else if ($scorm->scormtype === SCORM_TYPE_AICCURL and $cfgscorm->allowtypeexternalaicc) { require_once("$CFG->dirroot/mod/scorm/datamodels/aicclib.php"); // AICC. $result = scorm_parse_aicc($scorm); if (!$result) { $scorm->version = 'ERROR'; } else { $scorm->version = 'AICC'; } } else { // Sorry, disabled type. return; } $scorm->revision++; $scorm->sha1hash = $newhash; $DB->update_record('scorm', $scorm); } function scorm_array_search($item, $needle, $haystacks, $strict=false) { if (!empty($haystacks)) { foreach ($haystacks as $key => $element) { if ($strict) { if ($element->{$item} === $needle) { return $key; } } else { if ($element->{$item} == $needle) { return $key; } } } } return false; } function scorm_repeater($what, $times) { if ($times <= 0) { return null; } $return = ''; for ($i = 0; $i < $times; $i++) { $return .= $what; } return $return; } function scorm_external_link($link) { // Check if a link is external. $result = false; $link = strtolower($link); if (substr($link, 0, 7) == 'http://') { $result = true; } else if (substr($link, 0, 8) == 'https://') { $result = true; } else if (substr($link, 0, 4) == 'www.') { $result = true; } return $result; } /** * Returns an object containing all datas relative to the given sco ID * * @param integer $id The sco ID * @return mixed (false if sco id does not exists) */ function scorm_get_sco($id, $what=SCO_ALL) { global $DB; if ($sco = $DB->get_record('scorm_scoes', array('id' => $id))) { $sco = ($what == SCO_DATA) ? new stdClass() : $sco; if (($what != SCO_ONLY) && ($scodatas = $DB->get_records('scorm_scoes_data', array('scoid' => $id)))) { foreach ($scodatas as $scodata) { $sco->{$scodata->name} = $scodata->value; } } else if (($what != SCO_ONLY) && (!($scodatas = $DB->get_records('scorm_scoes_data', array('scoid' => $id))))) { $sco->parameters = ''; } return $sco; } else { return false; } } /** * Returns an object (array) containing all the scoes data related to the given sco ID * * @param integer $id The sco ID * @param integer $organisation an organisation ID - defaults to false if not required * @return mixed (false if there are no scoes or an array) */ function scorm_get_scoes($id, $organisation=false) { global $DB; $queryarray = array('scorm' => $id); if (!empty($organisation)) { $queryarray['organization'] = $organisation; } if ($scoes = $DB->get_records('scorm_scoes', $queryarray, 'sortorder, id')) { // Drop keys so that it is a simple array as expected. $scoes = array_values($scoes); foreach ($scoes as $sco) { if ($scodatas = $DB->get_records('scorm_scoes_data', array('scoid' => $sco->id))) { foreach ($scodatas as $scodata) { $sco->{$scodata->name} = $scodata->value; } } } return $scoes; } else { return false; } } /** * Insert SCORM track into db. * * @param int $userid The userid * @param int $scormid The id from scorm table * @param int $scoid The scoid * @param int|stdClass $attemptornumber - number of attempt or attempt record from scorm_attempt table. * @param string $element The element being saved * @param string $value The value of the element * @param boolean $forcecompleted Force this sco as completed * @param stdclass $trackdata - existing tracking data * @return int - the id of the record being saved. */ function scorm_insert_track($userid, $scormid, $scoid, $attemptornumber, $element, $value, $forcecompleted=false, $trackdata = null) { global $DB, $CFG; if (is_object($attemptornumber)) { $attempt = $attemptornumber; } else { $attempt = scorm_get_attempt($userid, $scormid, $attemptornumber); } $id = null; if ($forcecompleted) { // TODO - this could be broadened to encompass SCORM 2004 in future. if (($element == 'cmi.core.lesson_status') && ($value == 'incomplete')) { $track = scorm_get_sco_value($scoid, $userid, 'cmi.core.score.raw', $attempt->attempt); if (!empty($track)) { $value = 'completed'; } } if ($element == 'cmi.core.score.raw') { $tracktest = scorm_get_sco_value($scoid, $userid, 'cmi.core.lesson_status', $attempt->attempt); if (!empty($tracktest)) { if ($tracktest->value == "incomplete") { $v = new stdClass(); $v->id = $tracktest->valueid; $v->value = "completed"; $DB->update_record('scorm_scoes_value', $v); } } } if (($element == 'cmi.success_status') && ($value == 'passed' || $value == 'failed')) { if ($DB->get_record('scorm_scoes_data', array('scoid' => $scoid, 'name' => 'objectivesetbycontent'))) { $objectiveprogressstatus = true; $objectivesatisfiedstatus = false; if ($value == 'passed') { $objectivesatisfiedstatus = true; } $track = scorm_get_sco_value($scoid, $userid, 'objectiveprogressstatus', $attempt->attempt); if (!empty($track)) { $v = new stdClass(); $v->id = $track->valueid; $v->value = $objectiveprogressstatus; $v->timemodified = time(); $DB->update_record('scorm_scoes_value', $v); $id = $track->valueid; } else { $track = new stdClass(); $track->scoid = $scoid; $track->attemptid = $attempt->id; $track->elementid = scorm_get_elementid('objectiveprogressstatus'); $track->value = $objectiveprogressstatus; $track->timemodified = time(); $id = $DB->insert_record('scorm_scoes_value', $track); } if ($objectivesatisfiedstatus) { $track = scorm_get_sco_value($scoid, $userid, 'objectivesatisfiedstatus', $attempt->attempt); if (!empty($track)) { $v = new stdClass(); $v->id = $track->valueid; $v->value = $objectivesatisfiedstatus; $v->timemodified = time(); $DB->update_record('scorm_scoes_value', $v); $id = $track->valueid; } else { $track = new stdClass(); $track->scoid = $scoid; $track->attemptid = $attempt->id; $track->elementid = scorm_get_elementid('objectivesatisfiedstatus'); $track->value = $objectivesatisfiedstatus; $track->timemodified = time(); $id = $DB->insert_record('scorm_scoes_value', $track); } } } } } $track = null; if ($trackdata !== null) { if (isset($trackdata[$element])) { $track = $trackdata[$element]; } } else { $track = scorm_get_sco_value($scoid, $userid, $element, $attempt->attempt); } if ($track) { if ($element != 'x.start.time' ) { // Don't update x.start.time - keep the original value. if ($track->value != $value) { $v = new stdClass(); $v->id = $track->valueid; $v->value = $value; $v->timemodified = time(); $DB->update_record('scorm_scoes_value', $v); } $id = $track->valueid; } } else { $track = new stdClass(); $track->scoid = $scoid; $track->attemptid = $attempt->id; $track->elementid = scorm_get_elementid($element); $track->value = $value; $track->timemodified = time(); $id = $DB->insert_record('scorm_scoes_value', $track); $track->id = $id; } // Trigger updating grades based on a given set of SCORM CMI elements. $scorm = false; if (in_array($element, ['cmi.core.score.raw', 'cmi.score.raw']) || (in_array($element, ['cmi.completion_status', 'cmi.core.lesson_status', 'cmi.success_status']) && in_array($value, ['completed', 'passed']))) { $scorm = $DB->get_record('scorm', array('id' => $scormid)); include_once($CFG->dirroot.'/mod/scorm/lib.php'); scorm_update_grades($scorm, $userid); } // Trigger CMI element events. if (in_array($element, ['cmi.core.score.raw', 'cmi.score.raw']) || (in_array($element, ['cmi.completion_status', 'cmi.core.lesson_status', 'cmi.success_status']) && in_array($value, ['completed', 'failed', 'passed']))) { if (!$scorm) { $scorm = $DB->get_record('scorm', array('id' => $scormid)); } $cm = get_coursemodule_from_instance('scorm', $scormid); $data = ['other' => ['attemptid' => $attempt->id, 'cmielement' => $element, 'cmivalue' => $value], 'objectid' => $scorm->id, 'context' => context_module::instance($cm->id), 'relateduserid' => $userid, ]; if (in_array($element, array('cmi.core.score.raw', 'cmi.score.raw'))) { // Create score submitted event. $event = \mod_scorm\event\scoreraw_submitted::create($data); } else { // Create status submitted event. $event = \mod_scorm\event\status_submitted::create($data); } // Fix the missing track keys when the SCORM track record already exists, see $trackdata in datamodel.php. // There, for performances reasons, columns are limited to: element, id, value, timemodified. // Missing fields are: scoid, attemptid, elementid. $track->scoid = $scoid; $track->attemptid = $attempt->id; $track->elementid = scorm_get_elementid($element); $track->id = $id; // Trigger submitted event. $event->add_record_snapshot('scorm_scoes_value', $track); $event->add_record_snapshot('course_modules', $cm); $event->add_record_snapshot('scorm', $scorm); $event->trigger(); } return $id; } /** * simple quick function to return true/false if this user has tracks in this scorm * * @param integer $scormid The scorm ID * @param integer $userid the users id * @return boolean (false if there are no tracks) */ function scorm_has_tracks($scormid, $userid) { global $DB; return $DB->record_exists('scorm_attempt', ['userid' => $userid, 'scormid' => $scormid]); } function scorm_get_tracks($scoid, $userid, $attempt='') { // Gets all tracks of specified sco and user. global $DB; if (empty($attempt)) { if ($scormid = $DB->get_field('scorm_scoes', 'scorm', ['id' => $scoid])) { $attempt = scorm_get_last_attempt($scormid, $userid); } else { $attempt = 1; } } $sql = "SELECT v.id, a.userid, a.scormid, v.scoid, a.attempt, v.value, v.timemodified, e.element FROM {scorm_attempt} a JOIN {scorm_scoes_value} v ON v.attemptid = a.id JOIN {scorm_element} e ON e.id = v.elementid WHERE a.userid = ? AND v.scoid = ? AND a.attempt = ? ORDER BY e.element ASC"; if ($tracks = $DB->get_records_sql($sql, [$userid, $scoid, $attempt])) { $usertrack = scorm_format_interactions($tracks); $usertrack->userid = $userid; $usertrack->scoid = $scoid; return $usertrack; } else { return false; } } /** * helper function to return a formatted list of interactions for reports. * * @param array $trackdata the user tracking records from the database * @return object formatted list of interactions */ function scorm_format_interactions($trackdata) { $usertrack = new stdClass(); // Defined in order to unify scorm1.2 and scorm2004. $usertrack->score_raw = ''; $usertrack->status = ''; $usertrack->total_time = '00:00:00'; $usertrack->session_time = '00:00:00'; $usertrack->timemodified = 0; foreach ($trackdata as $track) { $element = $track->element; $usertrack->{$element} = $track->value; switch ($element) { case 'cmi.core.lesson_status': case 'cmi.completion_status': if ($track->value == 'not attempted') { $track->value = 'notattempted'; } $usertrack->status = $track->value; break; case 'cmi.core.score.raw': case 'cmi.score.raw': $usertrack->score_raw = (float) sprintf('%2.2f', $track->value); break; case 'cmi.core.session_time': case 'cmi.session_time': $usertrack->session_time = $track->value; break; case 'cmi.core.total_time': case 'cmi.total_time': $usertrack->total_time = $track->value; break; } if (isset($track->timemodified) && ($track->timemodified > $usertrack->timemodified)) { $usertrack->timemodified = $track->timemodified; } } return $usertrack; } /* Find the start and finsh time for a a given SCO attempt * * @param int $scormid SCORM Id * @param int $scoid SCO Id * @param int $userid User Id * @param int $attemt Attempt Id * * @return object start and finsh time EPOC secods * */ function scorm_get_sco_runtime($scormid, $scoid, $userid, $attempt=1) { global $DB; $params = array('userid' => $userid, 'scormid' => $scormid, 'attempt' => $attempt); $sql = "SELECT MIN(timemodified) AS timemin, MAX(timemodified) AS timemax FROM {scorm_scoes_value} v JOIN {scorm_attempt} a on a.id = v.attemptid WHERE a.userid = :userid AND a.scormid = :scormid AND a.attempt = :attempt"; if (!empty($scoid)) { $params['scoid'] = $scoid; $sql .= " AND v.scoid = :scoid"; } if ($timedata = $DB->get_record_sql($sql, $params)) { return (object) [ 'start' => $timedata->timemin, 'finish' => $timedata->timemax, ]; } else { $timedata = new stdClass(); $timedata->start = false; return $timedata; } } function scorm_grade_user_attempt($scorm, $userid, $attempt=1) { global $DB; $attemptscore = new stdClass(); $attemptscore->scoes = 0; $attemptscore->values = 0; $attemptscore->max = 0; $attemptscore->sum = 0; $attemptscore->lastmodify = 0; if (!$scoes = $DB->get_records('scorm_scoes', array('scorm' => $scorm->id), 'sortorder, id')) { return null; } foreach ($scoes as $sco) { if ($userdata = scorm_get_tracks($sco->id, $userid, $attempt)) { if (($userdata->status == 'completed') || ($userdata->status == 'passed')) { $attemptscore->scoes++; } if (!empty($userdata->score_raw) || (isset($scorm->type) && $scorm->type == 'sco' && isset($userdata->score_raw))) { $attemptscore->values++; $attemptscore->sum += $userdata->score_raw; $attemptscore->max = ($userdata->score_raw > $attemptscore->max) ? $userdata->score_raw : $attemptscore->max; if (isset($userdata->timemodified) && ($userdata->timemodified > $attemptscore->lastmodify)) { $attemptscore->lastmodify = $userdata->timemodified; } else { $attemptscore->lastmodify = 0; } } } } switch ($scorm->grademethod) { case GRADEHIGHEST: $score = (float) $attemptscore->max; break; case GRADEAVERAGE: if ($attemptscore->values > 0) { $score = $attemptscore->sum / $attemptscore->values; } else { $score = 0; } break; case GRADESUM: $score = $attemptscore->sum; break; case GRADESCOES: $score = $attemptscore->scoes; break; default: $score = $attemptscore->max; // Remote Learner GRADEHIGHEST is default. } return $score; } function scorm_grade_user($scorm, $userid) { // Ensure we dont grade user beyond $scorm->maxattempt settings. $lastattempt = scorm_get_last_attempt($scorm->id, $userid); if ($scorm->maxattempt != 0 && $lastattempt >= $scorm->maxattempt) { $lastattempt = $scorm->maxattempt; } switch ($scorm->whatgrade) { case FIRSTATTEMPT: return scorm_grade_user_attempt($scorm, $userid, scorm_get_first_attempt($scorm->id, $userid)); break; case LASTATTEMPT: return scorm_grade_user_attempt($scorm, $userid, scorm_get_last_completed_attempt($scorm->id, $userid)); break; case HIGHESTATTEMPT: $maxscore = 0; for ($attempt = 1; $attempt <= $lastattempt; $attempt++) { $attemptscore = scorm_grade_user_attempt($scorm, $userid, $attempt); $maxscore = $attemptscore > $maxscore ? $attemptscore : $maxscore; } return $maxscore; break; case AVERAGEATTEMPT: $attemptcount = scorm_get_attempt_count($userid, $scorm, true, true); if (empty($attemptcount)) { return 0; } else { $attemptcount = count($attemptcount); } $lastattempt = scorm_get_last_attempt($scorm->id, $userid); $sumscore = 0; for ($attempt = 1; $attempt <= $lastattempt; $attempt++) { $attemptscore = scorm_grade_user_attempt($scorm, $userid, $attempt); $sumscore += $attemptscore; } return round($sumscore / $attemptcount); break; } } function scorm_count_launchable($scormid, $organization='') { global $DB; $sqlorganization = ''; $params = array($scormid); if (!empty($organization)) { $sqlorganization = " AND organization=?"; $params[] = $organization; } return $DB->count_records_select('scorm_scoes', "scorm = ? $sqlorganization AND ". $DB->sql_isnotempty('scorm_scoes', 'launch', false, true), $params); } /** * Returns the last attempt used - if no attempts yet, returns 1 for first attempt * * @param int $scormid the id of the scorm. * @param int $userid the id of the user. * * @return int The attempt number to use. */ function scorm_get_last_attempt($scormid, $userid) { global $DB; // Find the last attempt number for the given user id and scorm id. $sql = "SELECT MAX(attempt) FROM {scorm_attempt} WHERE userid = ? AND scormid = ?"; $lastattempt = $DB->get_field_sql($sql, array($userid, $scormid)); if (empty($lastattempt)) { return '1'; } else { return $lastattempt; } } /** * Returns the first attempt used - if no attempts yet, returns 1 for first attempt. * * @param int $scormid the id of the scorm. * @param int $userid the id of the user. * * @return int The first attempt number. */ function scorm_get_first_attempt($scormid, $userid) { global $DB; // Find the first attempt number for the given user id and scorm id. $sql = "SELECT MIN(attempt) FROM {scorm_attempt} WHERE userid = ? AND scormid = ?"; $lastattempt = $DB->get_field_sql($sql, array($userid, $scormid)); if (empty($lastattempt)) { return '1'; } else { return $lastattempt; } } /** * Returns the last completed attempt used - if no completed attempts yet, returns 1 for first attempt * * @param int $scormid the id of the scorm. * @param int $userid the id of the user. * * @return int The attempt number to use. */ function scorm_get_last_completed_attempt($scormid, $userid) { global $DB; // Find the last completed attempt number for the given user id and scorm id. $sql = "SELECT MAX(a.attempt) FROM {scorm_attempt} a JOIN {scorm_scoes_value} v ON v.attemptid = a.id JOIN {scorm_element} e ON e.id = v.elementid WHERE userid = ? AND scormid = ? AND (" . $DB->sql_compare_text('v.value') . " = " . $DB->sql_compare_text('?') . " OR ". $DB->sql_compare_text('v.value') . " = " . $DB->sql_compare_text('?') . ")"; $lastattempt = $DB->get_field_sql($sql, [$userid, $scormid, 'completed', 'passed']); if (empty($lastattempt)) { return '1'; } else { return $lastattempt; } } /** * Returns the full list of attempts a user has made. * * @param int $scormid the id of the scorm. * @param int $userid the id of the user. * * @return array array of attemptids */ function scorm_get_all_attempts($scormid, $userid) { global $DB; $attemptids = array(); $sql = "SELECT DISTINCT attempt FROM {scorm_attempt} WHERE userid = ? AND scormid = ? ORDER BY attempt"; $attempts = $DB->get_records_sql($sql, [$userid, $scormid]); foreach ($attempts as $attempt) { $attemptids[] = $attempt->attempt; } return $attemptids; } /** * Displays the entry form and toc if required. * * @param stdClass $user user object * @param stdClass $scorm scorm object * @param string $action base URL for the organizations select box * @param stdClass $cm course module object */ function scorm_print_launch($user, $scorm, $action, $cm) { global $CFG, $DB, $OUTPUT; if ($scorm->updatefreq == SCORM_UPDATE_EVERYTIME) { scorm_parse($scorm, false); } $organization = optional_param('organization', '', PARAM_INT); if ($scorm->displaycoursestructure == 1) { echo $OUTPUT->box_start('generalbox boxaligncenter toc', 'toc'); echo html_writer::div(get_string('contents', 'scorm'), 'structurehead'); } if (empty($organization)) { $organization = $scorm->launch; } if ($orgs = $DB->get_records_select_menu('scorm_scoes', 'scorm = ? AND '. $DB->sql_isempty('scorm_scoes', 'launch', false, true).' AND '. $DB->sql_isempty('scorm_scoes', 'organization', false, false), array($scorm->id), 'sortorder, id', 'id,title')) { if (count($orgs) > 1) { $select = new single_select(new moodle_url($action), 'organization', $orgs, $organization, null); $select->label = get_string('organizations', 'scorm'); $select->class = 'scorm-center'; echo $OUTPUT->render($select); } } $orgidentifier = ''; if ($sco = scorm_get_sco($organization, SCO_ONLY)) { if (($sco->organization == '') && ($sco->launch == '')) { $orgidentifier = $sco->identifier; } else { $orgidentifier = $sco->organization; } } $scorm->version = strtolower(clean_param($scorm->version, PARAM_SAFEDIR)); // Just to be safe. if (!file_exists($CFG->dirroot.'/mod/scorm/datamodels/'.$scorm->version.'lib.php')) { $scorm->version = 'scorm_12'; } require_once($CFG->dirroot.'/mod/scorm/datamodels/'.$scorm->version.'lib.php'); $result = scorm_get_toc($user, $scorm, $cm->id, TOCFULLURL, $orgidentifier); $incomplete = $result->incomplete; // Get latest incomplete sco to launch first if force new attempt isn't set to always. if (!empty($result->sco->id) && $scorm->forcenewattempt != SCORM_FORCEATTEMPT_ALWAYS) { $launchsco = $result->sco->id; } else { // Use launch defined by SCORM package. $launchsco = $scorm->launch; } // Do we want the TOC to be displayed? if ($scorm->displaycoursestructure == 1) { echo $result->toc; echo $OUTPUT->box_end(); } // Is this the first attempt ? $attemptcount = scorm_get_attempt_count($user->id, $scorm); // Do not give the player launch FORM if the SCORM object is locked after the final attempt. if ($scorm->lastattemptlock == 0 || $result->attemptleft > 0) { echo html_writer::start_div('scorm-center'); echo html_writer::start_tag('form', [ 'id' => 'scormviewform', 'method' => 'post', 'action' => $CFG->wwwroot . '/mod/scorm/player.php', ]); $haspopup = !empty($scorm->popup); $popuphtml = ''; if ($haspopup) { $popuphtml = html_writer::span(get_string('opensinnewwindowbracketed'), 'visually-hidden') . $OUTPUT->pix_icon('i/externallink', '', 'moodle', ['class' => 'ms-1 me-0']); } if ($scorm->hidebrowse == 0) { $previewbuttonparams = [ 'class' => 'btn btn-secondary me-1', 'name' => 'mode', 'type' => 'submit', 'id' => 'b', 'value' => 'browse', ]; echo html_writer::tag('button', get_string('browse', 'scorm') . $popuphtml, $previewbuttonparams); } else { echo html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'mode', 'value' => 'normal')); } $enterbuttonparams = [ 'class' => 'btn btn-primary mx-1', 'name' => 'mode', 'type' => 'submit', 'id' => 'n', 'value' => 'normal', ]; echo html_writer::tag('button', get_string('enter', 'scorm') . $popuphtml, $enterbuttonparams); if (!empty($scorm->forcenewattempt)) { if ($scorm->forcenewattempt == SCORM_FORCEATTEMPT_ALWAYS || ($scorm->forcenewattempt == SCORM_FORCEATTEMPT_ONCOMPLETE && $incomplete === false)) { echo html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'newattempt', 'value' => 'on')); } } else if (!empty($attemptcount) && ($incomplete === false) && (($result->attemptleft > 0)||($scorm->maxattempt == 0))) { echo html_writer::start_div('pt-1'); echo html_writer::checkbox('newattempt', 'on', false, '', array('id' => 'a')); echo html_writer::label(get_string('newattempt', 'scorm'), 'a', true, ['class' => 'ps-1']); echo html_writer::end_div(); } if ($haspopup) { echo html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'display', 'value' => 'popup')); } echo html_writer::empty_tag('br'); echo html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'scoid', 'value' => $launchsco)); echo html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'cm', 'value' => $cm->id)); echo html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'currentorg', 'value' => $orgidentifier)); echo html_writer::end_tag('form'); echo html_writer::end_div(); } } function scorm_simple_play($scorm, $user, $context, $cmid) { global $DB; $result = false; if (has_capability('mod/scorm:viewreport', $context)) { // If this user can view reports, don't skipview so they can see links to reports. return $result; } if ($scorm->updatefreq == SCORM_UPDATE_EVERYTIME) { scorm_parse($scorm, false); } $scoes = $DB->get_records_select('scorm_scoes', 'scorm = ? AND '. $DB->sql_isnotempty('scorm_scoes', 'launch', false, true), array($scorm->id), 'sortorder, id', 'id'); if ($scoes) { $orgidentifier = ''; if ($sco = scorm_get_sco($scorm->launch, SCO_ONLY)) { if (($sco->organization == '') && ($sco->launch == '')) { $orgidentifier = $sco->identifier; } else { $orgidentifier = $sco->organization; } } if ($scorm->skipview >= SCORM_SKIPVIEW_FIRST) { $sco = current($scoes); $result = scorm_get_toc($user, $scorm, $cmid, TOCFULLURL, $orgidentifier); $url = new moodle_url('/mod/scorm/player.php', array('a' => $scorm->id, 'currentorg' => $orgidentifier)); // Set last incomplete sco to launch first if forcenewattempt not set to always. if (!empty($result->sco->id) && $scorm->forcenewattempt != SCORM_FORCEATTEMPT_ALWAYS) { $url->param('scoid', $result->sco->id); } else { $url->param('scoid', $sco->id); } if ($scorm->skipview == SCORM_SKIPVIEW_ALWAYS || !scorm_has_tracks($scorm->id, $user->id)) { if ($scorm->forcenewattempt == SCORM_FORCEATTEMPT_ALWAYS || ($result->incomplete === false && $scorm->forcenewattempt == SCORM_FORCEATTEMPT_ONCOMPLETE)) { $url->param('newattempt', 'on'); } redirect($url); } } } return $result; } function scorm_get_count_users($scormid, $groupingid=null) { global $CFG, $DB; if (!empty($groupingid)) { $sql = "SELECT COUNT(DISTINCT st.userid) FROM {scorm_attempt} st INNER JOIN {groups_members} gm ON st.userid = gm.userid INNER JOIN {groupings_groups} gg ON gm.groupid = gg.groupid WHERE st.scormid = ? AND gg.groupingid = ? "; $params = array($scormid, $groupingid); } else { $sql = "SELECT COUNT(DISTINCT st.userid) FROM {scorm_attempt} st WHERE st.scormid = ? "; $params = array($scormid); } return ($DB->count_records_sql($sql, $params)); } /** * Build up the JavaScript representation of an array element * * @param string $sversion SCORM API version * @param array $userdata User track data * @param string $elementname Name of array element to get values for * @param array $children list of sub elements of this array element that also need instantiating * @return Javascript array elements */ function scorm_reconstitute_array_element($sversion, $userdata, $elementname, $children) { // Reconstitute comments_from_learner and comments_from_lms. $current = ''; $currentsubelement = ''; $currentsub = ''; $count = 0; $countsub = 0; $scormseperator = '_'; $return = ''; if (scorm_version_check($sversion, SCORM_13)) { // Scorm 1.3 elements use a . instead of an _ . $scormseperator = '.'; } // Filter out the ones we want. $elementlist = array(); foreach ($userdata as $element => $value) { if (substr($element, 0, strlen($elementname)) == $elementname) { $elementlist[$element] = $value; } } // Sort elements in .n array order. uksort($elementlist, "scorm_element_cmp"); // Generate JavaScript. foreach ($elementlist as $element => $value) { if (scorm_version_check($sversion, SCORM_13)) { $element = preg_replace('/\.(\d+)\./', ".N\$1.", $element); preg_match('/\.(N\d+)\./', $element, $matches); } else { $element = preg_replace('/\.(\d+)\./', "_\$1.", $element); preg_match('/\_(\d+)\./', $element, $matches); } if (count($matches) > 0 && $current != $matches[1]) { if ($countsub > 0) { $return .= ' '.$elementname.$scormseperator.$current.'.'.$currentsubelement.'._count = '.$countsub.";\n"; } $current = $matches[1]; $count++; $currentsubelement = ''; $currentsub = ''; $countsub = 0; $end = strpos($element, $matches[1]) + strlen($matches[1]); $subelement = substr($element, 0, $end); $return .= ' '.$subelement." = new Object();\n"; // Now add the children. foreach ($children as $child) { $return .= ' '.$subelement.".".$child." = new Object();\n"; $return .= ' '.$subelement.".".$child."._children = ".$child."_children;\n"; } } // Now - flesh out the second level elements if there are any. if (scorm_version_check($sversion, SCORM_13)) { $element = preg_replace('/(.*?\.N\d+\..*?)\.(\d+)\./', "\$1.N\$2.", $element); preg_match('/.*?\.N\d+\.(.*?)\.(N\d+)\./', $element, $matches); } else { $element = preg_replace('/(.*?\_\d+\..*?)\.(\d+)\./', "\$1_\$2.", $element); preg_match('/.*?\_\d+\.(.*?)\_(\d+)\./', $element, $matches); } // Check the sub element type. if (count($matches) > 0 && $currentsubelement != $matches[1]) { if ($countsub > 0) { $return .= ' '.$elementname.$scormseperator.$current.'.'.$currentsubelement.'._count = '.$countsub.";\n"; } $currentsubelement = $matches[1]; $currentsub = ''; $countsub = 0; $end = strpos($element, $matches[1]) + strlen($matches[1]); $subelement = substr($element, 0, $end); $return .= ' '.$subelement." = new Object();\n"; } // Now check the subelement subscript. if (count($matches) > 0 && $currentsub != $matches[2]) { $currentsub = $matches[2]; $countsub++; $end = strrpos($element, $matches[2]) + strlen($matches[2]); $subelement = substr($element, 0, $end); $return .= ' '.$subelement." = new Object();\n"; } $return .= ' '.$element.' = '.json_encode($value).";\n"; } if ($countsub > 0) { $return .= ' '.$elementname.$scormseperator.$current.'.'.$currentsubelement.'._count = '.$countsub.";\n"; } if ($count > 0) { $return .= ' '.$elementname.'._count = '.$count.";\n"; } return $return; } /** * Build up the JavaScript representation of an array element * * @param string $a left array element * @param string $b right array element * @return comparator - 0,1,-1 */ function scorm_element_cmp($a, $b) { preg_match('/.*?(\d+)\./', $a, $matches); $left = intval($matches[1]); preg_match('/.?(\d+)\./', $b, $matches); $right = intval($matches[1]); if ($left < $right) { return -1; // Smaller. } else if ($left > $right) { return 1; // Bigger. } else { // Look for a second level qualifier eg cmi.interactions_0.correct_responses_0.pattern. if (preg_match('/.*?(\d+)\.(.*?)\.(\d+)\./', $a, $matches)) { $leftterm = intval($matches[2]); $left = intval($matches[3]); if (preg_match('/.*?(\d+)\.(.*?)\.(\d+)\./', $b, $matches)) { $rightterm = intval($matches[2]); $right = intval($matches[3]); if ($leftterm < $rightterm) { return -1; // Smaller. } else if ($leftterm > $rightterm) { return 1; // Bigger. } else { if ($left < $right) { return -1; // Smaller. } else if ($left > $right) { return 1; // Bigger. } } } } // Fall back for no second level matches or second level matches are equal. return 0; // Equal to. } } /** * Generate the user attempt status string * * @param object $user Current context user * @param object $scorm a moodle scrom object - mdl_scorm * @return string - Attempt status string */ function scorm_get_attempt_status($user, $scorm, $cm='') { global $DB, $PAGE, $OUTPUT; $attempts = scorm_get_attempt_count($user->id, $scorm, true); if (empty($attempts)) { $attemptcount = 0; } else { $attemptcount = count($attempts); } $result = html_writer::start_tag('p').get_string('noattemptsallowed', 'scorm').': '; if ($scorm->maxattempt > 0) { $result .= $scorm->maxattempt . html_writer::empty_tag('br'); } else { $result .= get_string('unlimited').html_writer::empty_tag('br'); } $result .= get_string('noattemptsmade', 'scorm').': ' . $attemptcount . html_writer::empty_tag('br'); if ($scorm->maxattempt == 1) { switch ($scorm->grademethod) { case GRADEHIGHEST: $grademethod = get_string('gradehighest', 'scorm'); break; case GRADEAVERAGE: $grademethod = get_string('gradeaverage', 'scorm'); break; case GRADESUM: $grademethod = get_string('gradesum', 'scorm'); break; case GRADESCOES: $grademethod = get_string('gradescoes', 'scorm'); break; } } else { switch ($scorm->whatgrade) { case HIGHESTATTEMPT: $grademethod = get_string('highestattempt', 'scorm'); break; case AVERAGEATTEMPT: $grademethod = get_string('averageattempt', 'scorm'); break; case FIRSTATTEMPT: $grademethod = get_string('firstattempt', 'scorm'); break; case LASTATTEMPT: $grademethod = get_string('lastattempt', 'scorm'); break; } } if (!empty($attempts)) { $i = 1; foreach ($attempts as $attempt) { $gradereported = scorm_grade_user_attempt($scorm, $user->id, $attempt->attemptnumber); if ($scorm->grademethod !== GRADESCOES && !empty($scorm->maxgrade)) { $gradereported = $gradereported / $scorm->maxgrade; $gradereported = number_format($gradereported * 100, 0) .'%'; } $result .= get_string('gradeforattempt', 'scorm').' ' . $i . ': ' . $gradereported .html_writer::empty_tag('br'); $i++; } } $calculatedgrade = scorm_grade_user($scorm, $user->id); if ($scorm->grademethod !== GRADESCOES && !empty($scorm->maxgrade)) { $calculatedgrade = $calculatedgrade / $scorm->maxgrade; $calculatedgrade = number_format($calculatedgrade * 100, 0) .'%'; } $result .= get_string('grademethod', 'scorm'). ': ' . $grademethod; if (empty($attempts)) { $result .= html_writer::empty_tag('br').get_string('gradereported', 'scorm'). ': '.get_string('none').html_writer::empty_tag('br'); } else { $result .= html_writer::empty_tag('br').get_string('gradereported', 'scorm'). ': '.$calculatedgrade.html_writer::empty_tag('br'); } $result .= html_writer::end_tag('p'); if ($attemptcount >= $scorm->maxattempt and $scorm->maxattempt > 0) { $result .= html_writer::tag('p', get_string('exceededmaxattempts', 'scorm'), array('class' => 'exceededmaxattempts')); } if (!empty($cm)) { $context = context_module::instance($cm->id); if (has_capability('mod/scorm:deleteownresponses', $context) && $DB->record_exists('scorm_attempt', ['userid' => $user->id, 'scormid' => $scorm->id])) { // Check to see if any data is stored for this user. $deleteurl = new moodle_url($PAGE->url, array('action' => 'delete', 'sesskey' => sesskey())); $result .= $OUTPUT->single_button($deleteurl, get_string('deleteallattempts', 'scorm')); } } return $result; } /** * Get SCORM attempt count * * @param object $user Current context user * @param object $scorm a moodle scrom object - mdl_scorm * @param bool $returnobjects if true returns a object with attempts, if false returns count of attempts. * @param bool $ignoremissingcompletion - ignores attempts that haven't reported a grade/completion. * @return int - no. of attempts so far */ function scorm_get_attempt_count($userid, $scorm, $returnobjects = false, $ignoremissingcompletion = false) { global $DB; // Historically attempts that don't report these elements haven't been included in the average attempts grading method // we may want to change this in future, but to avoid unexpected grade decreases we're leaving this in. MDL-43222 . if (scorm_version_check($scorm->version, SCORM_13)) { $element = 'cmi.score.raw'; } else if ($scorm->grademethod == GRADESCOES) { $element = 'cmi.core.lesson_status'; } else { $element = 'cmi.core.score.raw'; } if ($returnobjects) { $params = array('userid' => $userid, 'scormid' => $scorm->id); if ($ignoremissingcompletion) { // Exclude attempts that don't have the completion element requested. $params['element'] = $element; $sql = "SELECT DISTINCT a.attempt AS attemptnumber FROM {scorm_attempt} a JOIN {scorm_scoes_value} v ON v.attemptid = a.id JOIN {scorm_element} e ON e.id = v.elementid WHERE a.userid = :userid AND a.scormid = :scormid AND e.element = :element ORDER BY a.attempt"; $attempts = $DB->get_records_sql($sql, $params); } else { $attempts = $DB->get_records('scorm_attempt', $params, 'attempt', 'DISTINCT attempt AS attemptnumber'); } return $attempts; } else { $params = ['userid' => $userid, 'scormid' => $scorm->id]; if ($ignoremissingcompletion) { // Exclude attempts that don't have the completion element requested. $params['element'] = $element; $sql = "SELECT COUNT(DISTINCT a.attempt) FROM {scorm_attempt} a JOIN {scorm_scoes_value} v ON v.attemptid = a.id JOIN {scorm_element} e ON e.id = v.elementid WHERE a.userid = :userid AND a.scormid = :scormid AND e.element = :element"; } else { $sql = "SELECT COUNT(DISTINCT attempt) FROM {scorm_attempt} WHERE userid = :userid AND scormid = :scormid"; } $attemptscount = $DB->count_records_sql($sql, $params); return $attemptscount; } } /** * Figure out with this is a debug situation * * @param object $scorm a moodle scrom object - mdl_scorm * @return boolean - debugging true/false */ function scorm_debugging($scorm) { global $USER; $cfgscorm = get_config('scorm'); if (!$cfgscorm->allowapidebug) { return false; } $identifier = $USER->username.':'.$scorm->name; $test = $cfgscorm->apidebugmask; // Check the regex is only a short list of safe characters. if (!preg_match('/^[\w\s\*\.\?\+\:\_\\\]+$/', $test)) { return false; } if (preg_match('/^'.$test.'/', $identifier)) { return true; } return false; } /** * Delete Scorm tracks for selected users * * @param array $attemptids list of attempts that need to be deleted * @param stdClass $scorm instance * * @return bool true deleted all responses, false failed deleting an attempt - stopped here */ function scorm_delete_responses($attemptids, $scorm) { if (!is_array($attemptids) || empty($attemptids)) { return false; } foreach ($attemptids as $num => $attemptid) { if (empty($attemptid)) { unset($attemptids[$num]); } } foreach ($attemptids as $attempt) { $keys = explode(':', $attempt); if (count($keys) == 2) { $userid = clean_param($keys[0], PARAM_INT); $attemptid = clean_param($keys[1], PARAM_INT); if (!$userid || !$attemptid || !scorm_delete_attempt($userid, $scorm, $attemptid)) { return false; } } else { return false; } } return true; } /** * Delete Scorm tracks for selected users * * @param int $userid ID of User * @param stdClass $scorm Scorm object * @param int|stdClass $attemptornumber user attempt that need to be deleted * * @return bool true suceeded */ function scorm_delete_attempt($userid, $scorm, $attemptornumber) { if (is_object($attemptornumber)) { $attempt = $attemptornumber; } else { $attempt = scorm_get_attempt($userid, $scorm->id, $attemptornumber, false); // The attempt doesn't exist. if ($attempt === null) { return true; } } scorm_delete_tracks($scorm->id, null, $userid, $attempt->id); $cm = get_coursemodule_from_instance('scorm', $scorm->id); // Trigger instances list viewed event. $event = \mod_scorm\event\attempt_deleted::create([ 'other' => ['attemptid' => $attempt->attempt], 'context' => context_module::instance($cm->id), 'relateduserid' => $userid ]); $event->add_record_snapshot('course_modules', $cm); $event->add_record_snapshot('scorm', $scorm); $event->trigger(); include_once('lib.php'); scorm_update_grades($scorm, $userid, true); return true; } /** * Converts SCORM duration notation to human-readable format * The function works with both SCORM 1.2 and SCORM 2004 time formats * @param $duration string SCORM duration * @return string human-readable date/time */ function scorm_format_duration($duration) { // Fetch date/time strings. $stryears = get_string('years'); $strmonths = get_string('nummonths'); $strdays = get_string('days'); $strhours = get_string('hours'); $strminutes = get_string('minutes'); $strseconds = get_string('seconds'); if ($duration[0] == 'P') { // If timestamp starts with 'P' - it's a SCORM 2004 format // this regexp discards empty sections, takes Month/Minute ambiguity into consideration, // and outputs filled sections, discarding leading zeroes and any format literals // also saves the only zero before seconds decimals (if there are any) and discards decimals if they are zero. $pattern = array( '#([A-Z])0+Y#', '#([A-Z])0+M#', '#([A-Z])0+D#', '#P(|\d+Y)0*(\d+)M#', '#0*(\d+)Y#', '#0*(\d+)D#', '#P#', '#([A-Z])0+H#', '#([A-Z])[0.]+S#', '#\.0+S#', '#T(|\d+H)0*(\d+)M#', '#0*(\d+)H#', '#0+\.(\d+)S#', '#0*([\d.]+)S#', '#T#' ); $replace = array( '$1', '$1', '$1', '$1$2 '.$strmonths.' ', '$1 '.$stryears.' ', '$1 '.$strdays.' ', '', '$1', '$1', 'S', '$1$2 '.$strminutes.' ', '$1 '.$strhours.' ', '0.$1 '.$strseconds, '$1 '.$strseconds, ''); } else { // Else we have SCORM 1.2 format there // first convert the timestamp to some SCORM 2004-like format for conveniency. $duration = preg_replace('#^(\d+):(\d+):([\d.]+)$#', 'T$1H$2M$3S', $duration); // Then convert in the same way as SCORM 2004. $pattern = array( '#T0+H#', '#([A-Z])0+M#', '#([A-Z])[0.]+S#', '#\.0+S#', '#0*(\d+)H#', '#0*(\d+)M#', '#0+\.(\d+)S#', '#0*([\d.]+)S#', '#T#' ); $replace = array( 'T', '$1', '$1', 'S', '$1 '.$strhours.' ', '$1 '.$strminutes.' ', '0.$1 '.$strseconds, '$1 '.$strseconds, '' ); } $result = preg_replace($pattern, $replace, $duration); return $result; } function scorm_get_toc_object($user, $scorm, $currentorg='', $scoid='', $mode='normal', $attempt='', $play=false, $organizationsco=null) { global $CFG, $DB, $PAGE, $OUTPUT; // Always pass the mode even if empty as that is what is done elsewhere and the urls have to match. $modestr = '&mode='; if ($mode != 'normal') { $modestr = '&mode='.$mode; } $result = array(); $incomplete = false; if (!empty($organizationsco)) { $result[0] = $organizationsco; $result[0]->isvisible = 'true'; $result[0]->statusicon = ''; $result[0]->url = ''; } if ($scoes = scorm_get_scoes($scorm->id, $currentorg)) { // Retrieve user tracking data for each learning object. $usertracks = array(); foreach ($scoes as $sco) { if (!empty($sco->launch)) { if ($usertrack = scorm_get_tracks($sco->id, $user->id, $attempt)) { if ($usertrack->status == '') { $usertrack->status = 'notattempted'; } $usertracks[$sco->identifier] = $usertrack; } } } foreach ($scoes as $sco) { if (!isset($sco->isvisible)) { $sco->isvisible = 'true'; } if (empty($sco->title)) { $sco->title = $sco->identifier; } if (scorm_version_check($scorm->version, SCORM_13)) { $sco->prereq = true; } else { $sco->prereq = empty($sco->prerequisites) || scorm_eval_prerequisites($sco->prerequisites, $usertracks); } if ($sco->isvisible === 'true') { if (!empty($sco->launch)) { // Set first sco to launch if in browse/review mode. if (empty($scoid) && ($mode != 'normal')) { $scoid = $sco->id; } if (isset($usertracks[$sco->identifier])) { $usertrack = $usertracks[$sco->identifier]; // Check we have a valid status string identifier. if ($statusstringexists = get_string_manager()->string_exists($usertrack->status, 'scorm')) { $strstatus = get_string($usertrack->status, 'scorm'); } else { $strstatus = get_string('invalidstatus', 'scorm'); } if ($sco->scormtype == 'sco') { // Assume if we didn't get a valid status string, we don't have an icon either. $statusicon = $OUTPUT->pix_icon($statusstringexists ? $usertrack->status : 'incomplete', $strstatus, 'scorm'); } else { $statusicon = $OUTPUT->pix_icon('asset', get_string('assetlaunched', 'scorm'), 'scorm'); } if (($usertrack->status == 'notattempted') || ($usertrack->status == 'incomplete') || ($usertrack->status == 'browsed')) { $incomplete = true; if (empty($scoid)) { $scoid = $sco->id; } } $strsuspended = get_string('suspended', 'scorm'); $exitvar = 'cmi.core.exit'; if (scorm_version_check($scorm->version, SCORM_13)) { $exitvar = 'cmi.exit'; } if ($incomplete && isset($usertrack->{$exitvar}) && ($usertrack->{$exitvar} == 'suspend')) { $statusicon = $OUTPUT->pix_icon('suspend', $strstatus.' - '.$strsuspended, 'scorm'); } } else { if (empty($scoid)) { $scoid = $sco->id; } $incomplete = true; if ($sco->scormtype == 'sco') { $statusicon = $OUTPUT->pix_icon('notattempted', get_string('notattempted', 'scorm'), 'scorm'); } else { $statusicon = $OUTPUT->pix_icon('asset', get_string('asset', 'scorm'), 'scorm'); } } } } if (empty($statusicon)) { $sco->statusicon = $OUTPUT->pix_icon('notattempted', get_string('notattempted', 'scorm'), 'scorm'); } else { $sco->statusicon = $statusicon; } $sco->url = 'a='.$scorm->id.'&scoid='.$sco->id.'¤torg='.$currentorg.$modestr.'&attempt='.$attempt; $sco->incomplete = $incomplete; if (!in_array($sco->id, array_keys($result))) { $result[$sco->id] = $sco; } } } // Get the parent scoes! $result = scorm_get_toc_get_parent_child($result, $currentorg); // Be safe, prevent warnings from showing up while returning array. if (!isset($scoid)) { $scoid = ''; } return array('scoes' => $result, 'usertracks' => $usertracks, 'scoid' => $scoid); } function scorm_get_toc_get_parent_child(&$result, $currentorg) { $final = array(); $level = 0; // Organization is always the root, prevparent. if (!empty($currentorg)) { $prevparent = $currentorg; } else { $prevparent = '/'; } foreach ($result as $sco) { if ($sco->parent == '/') { $final[$level][$sco->identifier] = $sco; $prevparent = $sco->identifier; unset($result[$sco->id]); } else { if ($sco->parent == $prevparent) { $final[$level][$sco->identifier] = $sco; $prevparent = $sco->identifier; unset($result[$sco->id]); } else { if (!empty($final[$level])) { $found = false; foreach ($final[$level] as $fin) { if ($sco->parent == $fin->identifier) { $found = true; } } if ($found) { $final[$level][$sco->identifier] = $sco; unset($result[$sco->id]); $found = false; } else { $level++; $final[$level][$sco->identifier] = $sco; unset($result[$sco->id]); } } } } } for ($i = 0; $i <= $level; $i++) { $prevparent = ''; foreach ($final[$i] as $ident => $sco) { if (empty($prevparent)) { $prevparent = $ident; } if (!isset($final[$i][$prevparent]->children)) { $final[$i][$prevparent]->children = array(); } if ($sco->parent == $prevparent) { $final[$i][$prevparent]->children[] = $sco; $prevparent = $ident; } else { $parent = false; foreach ($final[$i] as $identifier => $scoobj) { if ($identifier == $sco->parent) { $parent = $identifier; } } if ($parent !== false) { $final[$i][$parent]->children[] = $sco; } } } } $results = array(); for ($i = 0; $i <= $level; $i++) { $keys = array_keys($final[$i]); $results[] = $final[$i][$keys[0]]; } return $results; } function scorm_format_toc_for_treeview($user, $scorm, $scoes, $usertracks, $cmid, $toclink=TOCJSLINK, $currentorg='', $attempt='', $play=false, $organizationsco=null, $children=false) { global $CFG; $result = new stdClass(); $result->prerequisites = true; $result->incomplete = true; $result->toc = ''; if (!$children) { $attemptsmade = scorm_get_attempt_count($user->id, $scorm); $result->attemptleft = $scorm->maxattempt == 0 ? 1 : $scorm->maxattempt - $attemptsmade; } if (!$children) { $result->toc = html_writer::start_tag('ul'); if (!$play && !empty($organizationsco)) { $result->toc .= html_writer::start_tag('li').$organizationsco->title.html_writer::end_tag('li'); } } $prevsco = ''; if (!empty($scoes)) { foreach ($scoes as $sco) { if ($sco->isvisible === 'false') { continue; } $result->toc .= html_writer::start_tag('li'); $scoid = $sco->id; $score = ''; if (isset($usertracks[$sco->identifier])) { $viewscore = has_capability('mod/scorm:viewscores', context_module::instance($cmid)); if (isset($usertracks[$sco->identifier]->score_raw) && $viewscore) { if ($usertracks[$sco->identifier]->score_raw != '') { $score = '('.get_string('score', 'scorm').': '.$usertracks[$sco->identifier]->score_raw.')'; } } } if (!empty($sco->prereq)) { if ($sco->id == $scoid) { $result->prerequisites = true; } if (!empty($prevsco) && scorm_version_check($scorm->version, SCORM_13) && !empty($prevsco->hidecontinue)) { if ($sco->scormtype == 'sco') { $result->toc .= html_writer::span($sco->statusicon.' '.format_string($sco->title)); } else { $result->toc .= html_writer::span(' '.format_string($sco->title)); } } else if ($toclink == TOCFULLURL) { $url = $CFG->wwwroot.'/mod/scorm/player.php?'.$sco->url; if (!empty($sco->launch)) { if ($sco->scormtype == 'sco') { $result->toc .= $sco->statusicon.' '; $result->toc .= html_writer::link($url, format_string($sco->title)).$score; } else { $result->toc .= ' '.html_writer::link($url, format_string($sco->title), array('data-scoid' => $sco->id)).$score; } } else { if ($sco->scormtype == 'sco') { $result->toc .= $sco->statusicon.' '.format_string($sco->title).$score; } else { $result->toc .= ' '.format_string($sco->title).$score; } } } else { if (!empty($sco->launch)) { if ($sco->scormtype == 'sco') { $result->toc .= html_writer::tag('a', $sco->statusicon.' '. format_string($sco->title).' '.$score, array('data-scoid' => $sco->id, 'title' => $sco->url)); } else { $result->toc .= html_writer::tag('a', ' '.format_string($sco->title).' '.$score, array('data-scoid' => $sco->id, 'title' => $sco->url)); } } else { if ($sco->scormtype == 'sco') { $result->toc .= html_writer::span($sco->statusicon.' '.format_string($sco->title)); } else { $result->toc .= html_writer::span(' '.format_string($sco->title)); } } } } else { if ($play) { if ($sco->scormtype == 'sco') { $result->toc .= html_writer::span($sco->statusicon.' '.format_string($sco->title)); } else { $result->toc .= ' '.format_string($sco->title).html_writer::end_span(); } } else { if ($sco->scormtype == 'sco') { $result->toc .= $sco->statusicon.' '.format_string($sco->title); } else { $result->toc .= ' '.format_string($sco->title); } } } if (!empty($sco->children)) { $result->toc .= html_writer::start_tag('ul'); $childresult = scorm_format_toc_for_treeview($user, $scorm, $sco->children, $usertracks, $cmid, $toclink, $currentorg, $attempt, $play, $organizationsco, true); // Is any of the children incomplete? $sco->incomplete = $childresult->incomplete; $result->toc .= $childresult->toc; $result->toc .= html_writer::end_tag('ul'); $result->toc .= html_writer::end_tag('li'); } else { $result->toc .= html_writer::end_tag('li'); } $prevsco = $sco; } $result->incomplete = $sco->incomplete; } if (!$children) { $result->toc .= html_writer::end_tag('ul'); } return $result; } function scorm_format_toc_for_droplist($scorm, $scoes, $usertracks, $currentorg='', $organizationsco=null, $children=false, $level=0, $tocmenus=array()) { if (!empty($scoes)) { if (!empty($organizationsco) && !$children) { $tocmenus[$organizationsco->id] = $organizationsco->title; } $parents[$level] = '/'; foreach ($scoes as $sco) { if ($parents[$level] != $sco->parent) { if ($newlevel = array_search($sco->parent, $parents)) { $level = $newlevel; } else { $i = $level; while (($i > 0) && ($parents[$level] != $sco->parent)) { $i--; } if (($i == 0) && ($sco->parent != $currentorg)) { $level++; } else { $level = $i; } $parents[$level] = $sco->parent; } } if ($sco->scormtype == 'sco') { $tocmenus[$sco->id] = scorm_repeater('−', $level) . '>' . format_string($sco->title); } if (!empty($sco->children)) { $tocmenus = scorm_format_toc_for_droplist($scorm, $sco->children, $usertracks, $currentorg, $organizationsco, true, $level, $tocmenus); } } } return $tocmenus; } function scorm_get_toc($user, $scorm, $cmid, $toclink=TOCJSLINK, $currentorg='', $scoid='', $mode='normal', $attempt='', $play=false, $tocheader=false) { global $CFG, $DB, $OUTPUT; if (empty($attempt)) { $attempt = scorm_get_last_attempt($scorm->id, $user->id); } $result = new stdClass(); $organizationsco = null; if ($tocheader) { $result->toc = html_writer::start_div('yui3-g-r', array('id' => 'scorm_layout')); $result->toc .= html_writer::start_div('yui3-u-1-5 loading', array('id' => 'scorm_toc')); $result->toc .= html_writer::div('', '', array('id' => 'scorm_toc_title')); $result->toc .= html_writer::start_div('', array('id' => 'scorm_tree')); } if (!empty($currentorg)) { $organizationsco = $DB->get_record('scorm_scoes', array('scorm' => $scorm->id, 'identifier' => $currentorg)); if (!empty($organizationsco->title)) { if ($play) { $result->toctitle = $organizationsco->title; } } } $scoes = scorm_get_toc_object($user, $scorm, $currentorg, $scoid, $mode, $attempt, $play, $organizationsco); $treeview = scorm_format_toc_for_treeview($user, $scorm, $scoes['scoes'][0]->children, $scoes['usertracks'], $cmid, $toclink, $currentorg, $attempt, $play, $organizationsco, false); if ($tocheader) { $result->toc .= $treeview->toc; } else { $result->toc = $treeview->toc; } if (!empty($scoes['scoid'])) { $scoid = $scoes['scoid']; } if (empty($scoid)) { // If this is a normal package with an org sco and child scos get the first child. if (!empty($scoes['scoes'][0]->children)) { $result->sco = $scoes['scoes'][0]->children[0]; } else { // This package only has one sco - it may be a simple external AICC package. $result->sco = $scoes['scoes'][0]; } } else { $result->sco = scorm_get_sco($scoid); } if ($scorm->hidetoc == SCORM_TOC_POPUP) { $tocmenu = scorm_format_toc_for_droplist($scorm, $scoes['scoes'][0]->children, $scoes['usertracks'], $currentorg, $organizationsco); $modestr = ''; if ($mode != 'normal') { $modestr = '&mode='.$mode; } $url = new moodle_url('/mod/scorm/player.php?a='.$scorm->id.'¤torg='.$currentorg.$modestr); $result->tocmenu = $OUTPUT->single_select($url, 'scoid', $tocmenu, $result->sco->id, null, "tocmenu"); } $result->prerequisites = $treeview->prerequisites; $result->incomplete = $treeview->incomplete; $result->attemptleft = $treeview->attemptleft; if ($tocheader) { $result->toc .= html_writer::end_div().html_writer::end_div(); $result->toc .= html_writer::start_div('loading', array('id' => 'scorm_toc_toggle')); $toggletoclabel = get_string('toggletableofcontents', 'scorm'); $toggletocbutton = html_writer::tag('button', '', [ 'id' => 'scorm_toc_toggle_btn', 'type' => 'button', 'class' => 'btn btn-secondary btn-sm mb-1', 'title' => $toggletoclabel, 'aria-label' => $toggletoclabel, 'aria-controls' => 'scorm_toc', ]); $result->toc .= $toggletocbutton . html_writer::end_div(); $result->toc .= html_writer::start_div('', array('id' => 'scorm_content')); $result->toc .= html_writer::div('', '', array('id' => 'scorm_navpanel')); $result->toc .= html_writer::end_div().html_writer::end_div(); } return $result; } function scorm_get_adlnav_json ($scoes, &$adlnav = array(), $parentscoid = null) { if (is_object($scoes)) { $sco = $scoes; if (isset($sco->url)) { $adlnav[$sco->id]['identifier'] = $sco->identifier; $adlnav[$sco->id]['launch'] = $sco->launch; $adlnav[$sco->id]['title'] = $sco->title; $adlnav[$sco->id]['url'] = $sco->url; $adlnav[$sco->id]['parent'] = $sco->parent; if (isset($sco->choice)) { $adlnav[$sco->id]['choice'] = $sco->choice; } if (isset($sco->flow)) { $adlnav[$sco->id]['flow'] = $sco->flow; } else if (isset($parentscoid) && isset($adlnav[$parentscoid]['flow'])) { $adlnav[$sco->id]['flow'] = $adlnav[$parentscoid]['flow']; } if (isset($sco->isvisible)) { $adlnav[$sco->id]['isvisible'] = $sco->isvisible; } if (isset($sco->parameters)) { $adlnav[$sco->id]['parameters'] = $sco->parameters; } if (isset($sco->hidecontinue)) { $adlnav[$sco->id]['hidecontinue'] = $sco->hidecontinue; } if (isset($sco->hideprevious)) { $adlnav[$sco->id]['hideprevious'] = $sco->hideprevious; } if (isset($sco->hidesuspendall)) { $adlnav[$sco->id]['hidesuspendall'] = $sco->hidesuspendall; } if (!empty($parentscoid)) { $adlnav[$sco->id]['parentscoid'] = $parentscoid; } if (isset($adlnav['prevscoid'])) { $adlnav[$sco->id]['prevscoid'] = $adlnav['prevscoid']; $adlnav[$adlnav['prevscoid']]['nextscoid'] = $sco->id; if (isset($adlnav['prevparent']) && $adlnav['prevparent'] == $sco->parent) { $adlnav[$sco->id]['prevsibling'] = $adlnav['prevscoid']; $adlnav[$adlnav['prevscoid']]['nextsibling'] = $sco->id; } } $adlnav['prevscoid'] = $sco->id; $adlnav['prevparent'] = $sco->parent; } if (isset($sco->children)) { foreach ($sco->children as $children) { scorm_get_adlnav_json($children, $adlnav, $sco->id); } } } else { foreach ($scoes as $sco) { scorm_get_adlnav_json ($sco, $adlnav); } unset($adlnav['prevscoid']); unset($adlnav['prevparent']); } return json_encode($adlnav); } /** * Check for the availability of a resource by URL. * * Check is performed using an HTTP HEAD call. * * @param $url string A valid URL * @return bool|string True if no issue is found. The error string message, otherwise */ function scorm_check_url($url) { $curl = new curl; // Same options as in {@link download_file_content()}, used in {@link scorm_parse_scorm()}. $curl->setopt(array('CURLOPT_FOLLOWLOCATION' => true, 'CURLOPT_MAXREDIRS' => 5)); $cmsg = $curl->head($url); $info = $curl->get_info(); if (empty($info['http_code']) || $info['http_code'] != 200) { return get_string('invalidurlhttpcheck', 'scorm', array('cmsg' => $cmsg)); } return true; } /** * Check for a parameter in userdata and return it if it's set * or return the value from $ifempty if its empty * * @param stdClass $userdata Contains user's data * @param string $param parameter that should be checked * @param string $ifempty value to be replaced with if $param is not set * @return string value from $userdata->$param if its not empty, or $ifempty */ function scorm_isset($userdata, $param, $ifempty = '') { if (isset($userdata->$param)) { return $userdata->$param; } else { return $ifempty; } } /** * Check if the current sco is launchable * If not, find the next launchable sco * * @param stdClass $scorm Scorm object * @param integer $scoid id of scorm_scoes record. * @return integer scoid of correct sco to launch or empty if one cannot be found, which will trigger first sco. */ function scorm_check_launchable_sco($scorm, $scoid) { global $DB; if ($sco = scorm_get_sco($scoid, SCO_ONLY)) { if ($sco->launch == '') { // This scoid might be a top level org that can't be launched, find the first launchable sco after this sco. $scoes = $DB->get_records_select('scorm_scoes', 'scorm = ? AND '.$DB->sql_isnotempty('scorm_scoes', 'launch', false, true). ' AND id > ?', array($scorm->id, $sco->id), 'sortorder, id', 'id', 0, 1); if (!empty($scoes)) { $sco = reset($scoes); // Get first item from the list. return $sco->id; } } else { return $sco->id; } } // Returning 0 will cause default behaviour which will find the first launchable sco in the package. return 0; } /** * Check if a SCORM is available for the current user. * * @param stdClass $scorm SCORM record * @param boolean $checkviewreportcap Check the scorm:viewreport cap * @param stdClass $context Module context, required if $checkviewreportcap is set to true * @param int $userid User id override * @return array status (available or not and possible warnings) * @since Moodle 3.0 */ function scorm_get_availability_status($scorm, $checkviewreportcap = false, $context = null, $userid = null) { $open = true; $closed = false; $warnings = array(); $timenow = time(); if (!empty($scorm->timeopen) and $scorm->timeopen > $timenow) { $open = false; } if (!empty($scorm->timeclose) and $timenow > $scorm->timeclose) { $closed = true; } if (!$open or $closed) { if ($checkviewreportcap and !empty($context) and has_capability('mod/scorm:viewreport', $context, $userid)) { return array(true, $warnings); } if (!$open) { $warnings['notopenyet'] = userdate($scorm->timeopen); } if ($closed) { $warnings['expired'] = userdate($scorm->timeclose); } return array(false, $warnings); } // Scorm is available. return array(true, $warnings); } /** * Requires a SCORM package to be available for the current user. * * @param stdClass $scorm SCORM record * @param boolean $checkviewreportcap Check the scorm:viewreport cap * @param stdClass $context Module context, required if $checkviewreportcap is set to true * @throws moodle_exception * @since Moodle 3.0 */ function scorm_require_available($scorm, $checkviewreportcap = false, $context = null) { list($available, $warnings) = scorm_get_availability_status($scorm, $checkviewreportcap, $context); if (!$available) { $reason = current(array_keys($warnings)); throw new moodle_exception($reason, 'scorm', '', $warnings[$reason]); } } /** * Return a SCO object and the SCO launch URL * * @param stdClass $scorm SCORM object * @param int $scoid The SCO id in database * @param stdClass $context context object * @return array the SCO object and URL * @since Moodle 3.1 */ function scorm_get_sco_and_launch_url($scorm, $scoid, $context) { global $CFG, $DB; if (!empty($scoid)) { // Direct SCO request. if ($sco = scorm_get_sco($scoid)) { if ($sco->launch == '') { // Search for the next launchable sco. if ($scoes = $DB->get_records_select( 'scorm_scoes', 'scorm = ? AND '.$DB->sql_isnotempty('scorm_scoes', 'launch', false, true).' AND id > ?', array($scorm->id, $sco->id), 'sortorder, id')) { $sco = current($scoes); } } } } // If no sco was found get the first of SCORM package. if (!isset($sco)) { $scoes = $DB->get_records_select( 'scorm_scoes', 'scorm = ? AND '.$DB->sql_isnotempty('scorm_scoes', 'launch', false, true), array($scorm->id), 'sortorder, id' ); $sco = current($scoes); } $connector = ''; $version = substr($scorm->version, 0, 4); if ((isset($sco->parameters) && (!empty($sco->parameters))) || ($version == 'AICC')) { if (stripos($sco->launch, '?') !== false) { $connector = '&'; } else { $connector = '?'; } if ((isset($sco->parameters) && (!empty($sco->parameters))) && ($sco->parameters[0] == '?')) { $sco->parameters = substr($sco->parameters, 1); } } if ($version == 'AICC') { require_once("$CFG->dirroot/mod/scorm/datamodels/aicclib.php"); $aiccsid = scorm_aicc_get_hacp_session($scorm->id); if (empty($aiccsid)) { $aiccsid = sesskey(); } $scoparams = ''; if (isset($sco->parameters) && (!empty($sco->parameters))) { $scoparams = '&'. $sco->parameters; } $launcher = $sco->launch.$connector.'aicc_sid='.$aiccsid.'&aicc_url='.$CFG->wwwroot.'/mod/scorm/aicc.php'.$scoparams; } else { if (isset($sco->parameters) && (!empty($sco->parameters))) { $launcher = $sco->launch.$connector.$sco->parameters; } else { $launcher = $sco->launch; } } if (scorm_external_link($sco->launch)) { // TODO: does this happen? $scolaunchurl = $launcher; } else if ($scorm->scormtype === SCORM_TYPE_EXTERNAL) { // Remote learning activity. $scolaunchurl = dirname($scorm->reference).'/'.$launcher; } else if ($scorm->scormtype === SCORM_TYPE_LOCAL && strtolower($scorm->reference) == 'imsmanifest.xml') { // This SCORM content sits in a repository that allows relative links. $scolaunchurl = "$CFG->wwwroot/pluginfile.php/$context->id/mod_scorm/imsmanifest/$scorm->revision/$launcher"; } else if ($scorm->scormtype === SCORM_TYPE_LOCAL or $scorm->scormtype === SCORM_TYPE_LOCALSYNC) { // Note: do not convert this to use moodle_url(). // SCORM does not work without slasharguments and moodle_url() encodes querystring vars. $scolaunchurl = "$CFG->wwwroot/pluginfile.php/$context->id/mod_scorm/content/$scorm->revision/$launcher"; } return array($sco, $scolaunchurl); } /** * Trigger the scorm_launched event. * * @param stdClass $scorm scorm object * @param stdClass $sco sco object * @param stdClass $cm course module object * @param stdClass $context context object * @param string $scourl SCO URL * @since Moodle 3.1 */ function scorm_launch_sco($scorm, $sco, $cm, $context, $scourl) { $event = \mod_scorm\event\sco_launched::create(array( 'objectid' => $sco->id, 'context' => $context, 'other' => array('instanceid' => $scorm->id, 'loadedcontent' => $scourl) )); $event->add_record_snapshot('course_modules', $cm); $event->add_record_snapshot('scorm', $scorm); $event->add_record_snapshot('scorm_scoes', $sco); $event->trigger(); } /** * This is really a little language parser for AICC_SCRIPT * evaluates the expression and returns a boolean answer * see 2.3.2.5.1. Sequencing/Navigation Today - from the SCORM 1.2 spec (CAM). * Also used by AICC packages. * * @param string $prerequisites the aicc_script prerequisites expression * @param array $usertracks the tracked user data of each SCO visited * @return boolean */ function scorm_eval_prerequisites($prerequisites, $usertracks) { // This is really a little language parser - AICC_SCRIPT is the reference // see 2.3.2.5.1. Sequencing/Navigation Today - from the SCORM 1.2 spec. $element = ''; $stack = array(); $statuses = array( 'passed' => 'passed', 'completed' => 'completed', 'failed' => 'failed', 'incomplete' => 'incomplete', 'browsed' => 'browsed', 'not attempted' => 'notattempted', 'p' => 'passed', 'c' => 'completed', 'f' => 'failed', 'i' => 'incomplete', 'b' => 'browsed', 'n' => 'notattempted' ); $i = 0; // Expand the amp entities. $prerequisites = preg_replace('/&/', '&', $prerequisites); // Find all my parsable tokens. $prerequisites = preg_replace('/(&|\||\(|\)|\~)/', '\t$1\t', $prerequisites); // Expand operators. $prerequisites = preg_replace('/&/', '&&', $prerequisites); $prerequisites = preg_replace('/\|/', '||', $prerequisites); // Now - grab all the tokens. $elements = explode('\t', trim($prerequisites)); // Process each token to build an expression to be evaluated. $stack = array(); foreach ($elements as $element) { $element = trim($element); if (empty($element)) { continue; } if (!preg_match('/^(&&|\|\||\(|\))$/', $element)) { // Create each individual expression. // Search for ~ = <> X*{} . // Sets like 3*{S34, S36, S37, S39}. if (preg_match('/^(\d+)\*\{(.+)\}$/', $element, $matches)) { $repeat = $matches[1]; $set = explode(',', $matches[2]); $count = 0; foreach ($set as $setelement) { if (isset($usertracks[$setelement]) && ($usertracks[$setelement]->status == 'completed' || $usertracks[$setelement]->status == 'passed')) { $count++; } } if ($count >= $repeat) { $element = 'true'; } else { $element = 'false'; } } else if ($element == '~') { // Not maps ~. $element = '!'; } else if (preg_match('/^(.+)(\=|\<\>)(.+)$/', $element, $matches)) { // Other symbols = | <> . $element = trim($matches[1]); if (isset($usertracks[$element])) { $value = trim(preg_replace('/(\'|\")/', '', $matches[3])); if (isset($statuses[$value])) { $value = $statuses[$value]; } $elementprerequisitematch = (strcmp($usertracks[$element]->status, $value) == 0); if ($matches[2] == '<>') { $element = $elementprerequisitematch ? 'false' : 'true'; } else { $element = $elementprerequisitematch ? 'true' : 'false'; } } else { $element = 'false'; } } else { // Everything else must be an element defined like S45 ... if (isset($usertracks[$element]) && ($usertracks[$element]->status == 'completed' || $usertracks[$element]->status == 'passed')) { $element = 'true'; } else { $element = 'false'; } } } $stack[] = ' '.$element.' '; } return eval('return '.implode($stack).';'); } /** * Update the calendar entries for this scorm activity. * * @param stdClass $scorm the row from the database table scorm. * @param int $cmid The coursemodule id * @return bool */ function scorm_update_calendar(stdClass $scorm, $cmid) { global $DB, $CFG; require_once($CFG->dirroot.'/calendar/lib.php'); // Scorm start calendar events. $event = new stdClass(); $event->eventtype = SCORM_EVENT_TYPE_OPEN; // The SCORM_EVENT_TYPE_OPEN event should only be an action event if no close time is specified. $event->type = empty($scorm->timeclose) ? CALENDAR_EVENT_TYPE_ACTION : CALENDAR_EVENT_TYPE_STANDARD; if ($event->id = $DB->get_field('event', 'id', array('modulename' => 'scorm', 'instance' => $scorm->id, 'eventtype' => $event->eventtype))) { if ((!empty($scorm->timeopen)) && ($scorm->timeopen > 0)) { // Calendar event exists so update it. $event->name = get_string('calendarstart', 'scorm', $scorm->name); $event->description = format_module_intro('scorm', $scorm, $cmid, false); $event->format = FORMAT_HTML; $event->timestart = $scorm->timeopen; $event->timesort = $scorm->timeopen; $event->visible = instance_is_visible('scorm', $scorm); $event->timeduration = 0; $calendarevent = calendar_event::load($event->id); $calendarevent->update($event, false); } else { // Calendar event is on longer needed. $calendarevent = calendar_event::load($event->id); $calendarevent->delete(); } } else { // Event doesn't exist so create one. if ((!empty($scorm->timeopen)) && ($scorm->timeopen > 0)) { $event->name = get_string('calendarstart', 'scorm', $scorm->name); $event->description = format_module_intro('scorm', $scorm, $cmid, false); $event->format = FORMAT_HTML; $event->courseid = $scorm->course; $event->groupid = 0; $event->userid = 0; $event->modulename = 'scorm'; $event->instance = $scorm->id; $event->timestart = $scorm->timeopen; $event->timesort = $scorm->timeopen; $event->visible = instance_is_visible('scorm', $scorm); $event->timeduration = 0; calendar_event::create($event, false); } } // Scorm end calendar events. $event = new stdClass(); $event->type = CALENDAR_EVENT_TYPE_ACTION; $event->eventtype = SCORM_EVENT_TYPE_CLOSE; if ($event->id = $DB->get_field('event', 'id', array('modulename' => 'scorm', 'instance' => $scorm->id, 'eventtype' => $event->eventtype))) { if ((!empty($scorm->timeclose)) && ($scorm->timeclose > 0)) { // Calendar event exists so update it. $event->name = get_string('calendarend', 'scorm', $scorm->name); $event->description = format_module_intro('scorm', $scorm, $cmid, false); $event->format = FORMAT_HTML; $event->timestart = $scorm->timeclose; $event->timesort = $scorm->timeclose; $event->visible = instance_is_visible('scorm', $scorm); $event->timeduration = 0; $calendarevent = calendar_event::load($event->id); $calendarevent->update($event, false); } else { // Calendar event is on longer needed. $calendarevent = calendar_event::load($event->id); $calendarevent->delete(); } } else { // Event doesn't exist so create one. if ((!empty($scorm->timeclose)) && ($scorm->timeclose > 0)) { $event->name = get_string('calendarend', 'scorm', $scorm->name); $event->description = format_module_intro('scorm', $scorm, $cmid, false); $event->format = FORMAT_HTML; $event->courseid = $scorm->course; $event->groupid = 0; $event->userid = 0; $event->modulename = 'scorm'; $event->instance = $scorm->id; $event->timestart = $scorm->timeclose; $event->timesort = $scorm->timeclose; $event->visible = instance_is_visible('scorm', $scorm); $event->timeduration = 0; calendar_event::create($event, false); } } } /** * Function to delete user tracks from tables. * * @param int $scormid - id from scorm. * @param int $scoid - id of sco that needs to be deleted. * @param int $userid - userid that needs to be deleted. * @param int $attemptid - attemptid that should be deleted. * @since Moodle 4.3 */ function scorm_delete_tracks($scormid, $scoid = null, $userid = null, $attemptid = null) { global $DB; $usersql = ''; $params = ['scormid' => $scormid]; if (!empty($attemptid)) { $params['attemptid'] = $attemptid; $sql = "attemptid = :attemptid"; } else { if (!empty($userid)) { $usersql = ' AND userid = :userid'; $params['userid'] = $userid; } $sql = "attemptid in (SELECT id FROM {scorm_attempt} WHERE scormid = :scormid $usersql)"; } if (!empty($scoid)) { $params['scoid'] = $scoid; $sql .= " AND scoid = :scoid"; } $DB->delete_records_select('scorm_scoes_value', $sql, $params); if (empty($scoid)) { if (empty($attemptid)) { // Scoid is empty so we delete the attempt as well. $DB->delete_records('scorm_attempt', $params); } else { $DB->delete_records('scorm_attempt', ['id' => $attemptid]); } } } /** * Get specific scorm track data. * Note: the $attempt var is optional as SCORM 2004 code doesn't always use it, probably a bug, * but we do not want to change SCORM 2004 behaviour right now. * * @param int $scoid - scoid. * @param int $userid - user id of user. * @param string $element - name of element being requested. * @param int $attempt - attempt number (not id) * @since Moodle 4.3 * @return mixed */ function scorm_get_sco_value($scoid, $userid, $element, $attempt = null): ?stdClass { global $DB; $params = ['scoid' => $scoid, 'userid' => $userid, 'element' => $element]; $sql = "SELECT a.id, a.userid, a.scormid, a.attempt, v.id as valueid, v.scoid, v.value, v.timemodified, e.element FROM {scorm_attempt} a JOIN {scorm_scoes_value} v ON v.attemptid = a.id JOIN {scorm_element} e on e.id = v.elementid WHERE v.scoid = :scoid AND a.userid = :userid AND e.element = :element"; if ($attempt !== null) { $params['attempt'] = $attempt; $sql .= " AND a.attempt = :attempt"; } $value = $DB->get_record_sql($sql, $params); return $value ?: null; } /** * Get attempt record, allow one to be created if doesn't exist. * * @param int $userid - user id. * @param int $scormid - SCORM id. * @param int $attempt - attempt number. * @param boolean $create - should an attempt record be created if it does not exist. * @since Moodle 4.3 * @return stdclass */ function scorm_get_attempt($userid, $scormid, $attempt, $create = true): ?stdClass { global $DB; $params = ['scormid' => $scormid, 'userid' => $userid, 'attempt' => $attempt]; $attemptobject = $DB->get_record('scorm_attempt', $params); if (empty($attemptobject) && $create) { // Create new attempt. $attemptobject = new stdClass(); $attemptobject->userid = $userid; $attemptobject->attempt = $attempt; $attemptobject->scormid = $scormid; $attemptobject->id = $DB->insert_record('scorm_attempt', $attemptobject); } return $attemptobject ?: null; } /** * Get Scorm element id from cache, allow one to be created if doesn't exist. * * @param string $elementname - name of element that is being requested. * @since Moodle 4.3 * @return int - element id. */ function scorm_get_elementid($elementname): ?int { global $DB; $cache = cache::make('mod_scorm', 'elements'); $element = $cache->get($elementname); if (empty($element)) { // Create new attempt. $element = new stdClass(); $element->element = $elementname; $elementid = $DB->insert_record('scorm_element', $element); $cache->set($elementname, $elementid); return $elementid; } else { return $element; } } home/smkb8269/public_html/lms.smk-almanshuriyah.sch.id/mod/url/locallib.php000064400000047377152267120640022563 0ustar00. /** * Private url module utility functions * * @package mod_url * @copyright 2009 Petr Skoda {@link http://skodak.org} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die; require_once("$CFG->libdir/filelib.php"); require_once("$CFG->libdir/resourcelib.php"); require_once("$CFG->dirroot/mod/url/lib.php"); /** * This methods does weak url validation, we are looking for major problems only, * no strict RFE validation. * * @param $url * @return bool true is seems valid, false if definitely not valid URL */ function url_appears_valid_url($url) { if (preg_match('/^(\/|https?:|ftp:)/i', $url)) { // note: this is not exact validation, we look for severely malformed URLs only return (bool) preg_match('/^[a-z]+:\/\/([^:@\s]+:[^@\s]+@)?[^ @]+(:[0-9]+)?(\/[^#]*)?(#.*)?$/i', $url); } else { return (bool)preg_match('/^[a-z]+:\/\/...*$/i', $url); } } /** * Fix common URL problems that we want teachers to see fixed * the next time they edit the resource. * * This function does not include any XSS protection. * * @param string $url * @return string */ function url_fix_submitted_url($url) { // note: empty urls are prevented in form validation $url = trim($url); // remove encoded entities - we want the raw URI here $url = html_entity_decode($url, ENT_QUOTES, 'UTF-8'); if (!preg_match('|^[a-z]+:|i', $url) and !preg_match('|^/|', $url)) { // invalid URI, try to fix it by making it normal URL, // please note relative urls are not allowed, /xx/yy links are ok $url = 'http://'.$url; } return $url; } /** * Return full url with all extra parameters * * This function does not include any XSS protection. * * @param stdClass $url * @param object $cm * @param object $course * @param object $config * @return string url with & encoded as & */ function url_get_full_url($url, $cm, $course, $config=null) { $parameters = empty($url->parameters) ? [] : (array) unserialize_array($url->parameters); // make sure there are no encoded entities, it is ok to do this twice $fullurl = html_entity_decode($url->externalurl, ENT_QUOTES, 'UTF-8'); $letters = '\pL'; $latin = 'a-zA-Z'; $digits = '0-9'; $symbols = '\x{20E3}\x{00AE}\x{00A9}\x{203C}\x{2047}\x{2048}\x{2049}\x{3030}\x{303D}\x{2139}\x{2122}\x{3297}\x{3299}' . '\x{2300}-\x{23FF}\x{2600}-\x{27BF}\x{2B00}-\x{2BF0}'; $arabic = '\x{FE00}-\x{FEFF}'; $math = '\x{2190}-\x{21FF}\x{2900}-\x{297F}'; $othernumbers = '\x{2460}-\x{24FF}'; $geometric = '\x{25A0}-\x{25FF}'; $emojis = '\x{1F000}-\x{1F6FF}'; if (preg_match('/^(\/|https?:|ftp:)/i', $fullurl) or preg_match('|^/|', $fullurl)) { // encode extra chars in URLs - this does not make it always valid, but it helps with some UTF-8 problems // Thanks to 💩.la emojis count as valid, too. $allowed = "[" . $letters . $latin . $digits . $symbols . $arabic . $math . $othernumbers . $geometric . $emojis . "]" . preg_quote(';/?:@=&$_.+!*(),-#%', '/'); $fullurl = preg_replace_callback("/[^$allowed]/u", 'url_filter_callback', $fullurl); } else { // encode special chars only $fullurl = str_replace('"', '%22', $fullurl); $fullurl = str_replace('\'', '%27', $fullurl); $fullurl = str_replace(' ', '%20', $fullurl); $fullurl = str_replace('<', '%3C', $fullurl); $fullurl = str_replace('>', '%3E', $fullurl); } if (!$config) { $config = get_config('url'); } // add variable url parameters if ($config->allowvariables && !empty($parameters)) { $paramvalues = url_get_variable_values($url, $cm, $course, $config); foreach ($parameters as $parse=>$parameter) { if (isset($paramvalues[$parameter])) { $parameters[$parse] = rawurlencode($parse).'='.rawurlencode($paramvalues[$parameter]); } else { unset($parameters[$parse]); } } if (!empty($parameters)) { if (stripos($fullurl, 'teamspeak://') === 0) { $fullurl = $fullurl.'?'.implode('?', $parameters); } else { $join = (strpos($fullurl, '?') === false) ? '?' : '&'; $fullurl = $fullurl.$join.implode('&', $parameters); } } } // encode all & to & entity $fullurl = str_replace('&', '&', $fullurl); return $fullurl; } /** * Unicode encoding helper callback * @internal * @param array $matches * @return string */ function url_filter_callback($matches) { return rawurlencode($matches[0]); } /** * Print url header. * @param object $url * @param object $cm * @param object $course * @return void */ function url_print_header($url, $cm, $course) { global $PAGE, $OUTPUT; $PAGE->set_title($course->shortname.': '.$url->name); $PAGE->set_heading($course->fullname); $PAGE->set_activity_record($url); echo $OUTPUT->header(); } /** * Get url introduction. * * @param object $url * @param object $cm * @param bool $ignoresettings print even if not specified in modedit * @return string */ function url_get_intro(object $url, object $cm, bool $ignoresettings = false): string { $options = empty($url->displayoptions) ? [] : (array) unserialize_array($url->displayoptions); if ($ignoresettings || !empty($options['printintro'])) { if (!html_is_blank($url->intro)) { return format_module_intro('url', $url, $cm->id); } } return ''; } /** * Display url frames. * @param object $url * @param object $cm * @param object $course * @return does not return */ function url_display_frame($url, $cm, $course) { global $PAGE, $OUTPUT, $CFG; $frame = optional_param('frameset', 'main', PARAM_ALPHA); if ($frame === 'top') { $PAGE->set_pagelayout('frametop'); $PAGE->activityheader->set_attrs([ 'description' => url_get_intro($url, $cm), 'title' => format_string($url->name) ]); url_print_header($url, $cm, $course); echo $OUTPUT->footer(); die; } else { $config = get_config('url'); $context = context_module::instance($cm->id); $exteurl = url_get_full_url($url, $cm, $course, $config); $navurl = "$CFG->wwwroot/mod/url/view.php?id=$cm->id&frameset=top"; $coursecontext = context_course::instance($course->id); $courseshortname = format_string($course->shortname, true, array('context' => $coursecontext)); $title = strip_tags($courseshortname.': '.format_string($url->name)); $framesize = $config->framesize; $modulename = s(get_string('modulename','url')); $contentframetitle = s(format_string($url->name)); $dir = get_string('thisdirection', 'langconfig'); $extframe = << $title EOF; @header('Content-Type: text/html; charset=utf-8'); echo $extframe; die; } } /** * Print url info and link. * @param object $url * @param object $cm * @param object $course */ function url_print_workaround($url, $cm, $course) { global $OUTPUT, $PAGE, $USER; $PAGE->activityheader->set_description(url_get_intro($url, $cm)); url_print_header($url, $cm, $course); $fullurl = new moodle_url(url_get_full_url($url, $cm, $course)); $display = url_get_final_display_type($url); if ($display == RESOURCELIB_DISPLAY_POPUP) { $jsfullurl = addslashes_js($fullurl->out(false)); $options = empty($url->displayoptions) ? [] : (array) unserialize_array($url->displayoptions); $width = empty($options['popupwidth']) ? 620 : $options['popupwidth']; $height = empty($options['popupheight']) ? 450 : $options['popupheight']; $wh = "width=$width,height=$height,toolbar=no,location=no,menubar=no,copyhistory=no,status=no,directories=no,scrollbars=yes,resizable=yes"; $attributes = ['onclick' => "window.open('$jsfullurl', '', '$wh'); return false;"]; } else if ($display == RESOURCELIB_DISPLAY_NEW) { $attributes = ['onclick' => "this.target='_blank';"]; } else { $attributes = []; } echo '
    '; print_string('clicktoopen', 'url', html_writer::link($fullurl, format_string($cm->name), $attributes)); echo '
    '; echo $OUTPUT->footer(); die; } /** * Display embedded url file. * @param object $url * @param object $cm * @param object $course */ function url_display_embed($url, $cm, $course) { global $PAGE, $OUTPUT; $mimetype = resourcelib_guess_url_mimetype($url->externalurl); $fullurl = url_get_full_url($url, $cm, $course); $title = $url->name; $moodleurl = new moodle_url($fullurl); $link = html_writer::link($moodleurl, format_string($cm->name)); $clicktoopen = get_string('clicktoopen', 'url', $link); $extension = resourcelib_get_extension($url->externalurl); $mediamanager = core_media_manager::instance($PAGE); $embedoptions = array( core_media_manager::OPTION_TRUSTED => true, core_media_manager::OPTION_BLOCK => true ); if (in_array($mimetype, array('image/gif','image/jpeg','image/png'))) { // It's an image $code = resourcelib_embed_image($fullurl, $title); } else if ($mediamanager->can_embed_url($moodleurl, $embedoptions)) { // Media (audio/video) file. $code = $mediamanager->embed_url($moodleurl, $title, 0, 0, $embedoptions); } else { // anything else - just try object tag enlarged as much as possible $code = resourcelib_embed_general($fullurl, $title, $clicktoopen, $mimetype); } $PAGE->activityheader->set_description(url_get_intro($url, $cm)); url_print_header($url, $cm, $course); echo $code; echo $OUTPUT->footer(); die; } /** * Decide the best display format. * @param object $url * @return int display type constant */ function url_get_final_display_type($url) { global $CFG; if ($url->display != RESOURCELIB_DISPLAY_AUTO) { return $url->display; } // detect links to local moodle pages if (strpos($url->externalurl, $CFG->wwwroot) === 0) { if (strpos($url->externalurl, 'file.php') === false and strpos($url->externalurl, '.php') !== false ) { // most probably our moodle page with navigation return RESOURCELIB_DISPLAY_OPEN; } } // Binaries and other formats that are known to cause trouble for external links. static $download = ['application/zip', 'application/x-tar', 'application/g-zip', 'application/pdf', 'text/html', 'document/unknown']; static $embed = array('image/gif', 'image/jpeg', 'image/png', 'image/svg+xml', // images 'application/x-shockwave-flash', 'video/x-flv', 'video/x-ms-wm', // video formats 'video/quicktime', 'video/mpeg', 'video/mp4', 'audio/mp3', 'audio/x-realaudio-plugin', 'x-realaudio-plugin', // audio formats, ); $mimetype = resourcelib_guess_url_mimetype($url->externalurl); if (in_array($mimetype, $download)) { return RESOURCELIB_DISPLAY_DOWNLOAD; } if (in_array($mimetype, $embed)) { return RESOURCELIB_DISPLAY_EMBED; } // let the browser deal with it somehow return RESOURCELIB_DISPLAY_OPEN; } /** * Get the parameters that may be appended to URL * @param object $config url module config options * @return array array describing opt groups */ function url_get_variable_options($config) { global $CFG; $options = array(); $options[''] = array('' => get_string('chooseavariable', 'url')); $options[get_string('course')] = array( 'courseid' => 'id', 'coursefullname' => get_string('fullnamecourse'), 'courseshortname' => get_string('shortnamecourse'), 'courseidnumber' => get_string('idnumbercourse'), 'coursesummary' => get_string('summary'), 'courseformat' => get_string('format'), ); $options[get_string('modulename', 'url')] = array( 'urlinstance' => 'id', 'urlcmid' => 'cmid', 'urlname' => get_string('name'), 'urlidnumber' => get_string('idnumbermod'), ); $options[get_string('miscellaneous')] = array( 'sitename' => get_string('fullsitename'), 'serverurl' => get_string('serverurl', 'url'), 'currenttime' => get_string('time'), 'lang' => get_string('language'), ); if (!empty($config->secretphrase)) { $options[get_string('miscellaneous')]['encryptedcode'] = get_string('encryptedcode'); } $options[get_string('user')] = array( 'userid' => 'id', 'userusername' => get_string('username'), 'useridnumber' => get_string('idnumber'), 'userfirstname' => get_string('firstname'), 'userlastname' => get_string('lastname'), 'userfullname' => get_string('fullnameuser'), 'useremail' => get_string('email'), 'userphone1' => get_string('phone1'), 'userphone2' => get_string('phone2'), 'userinstitution' => get_string('institution'), 'userdepartment' => get_string('department'), 'useraddress' => get_string('address'), 'usercity' => get_string('city'), 'usertimezone' => get_string('timezone'), ); if ($config->rolesinparams) { $roles = role_fix_names(get_all_roles()); $roleoptions = array(); foreach ($roles as $role) { $roleoptions['course'.$role->shortname] = get_string('yourwordforx', '', $role->localname); } $options[get_string('roles')] = $roleoptions; } return $options; } /** * Get the parameter values that may be appended to URL * @param object $url module instance * @param object $cm * @param object $course * @param object $config module config options * @return array of parameter values */ function url_get_variable_values($url, $cm, $course, $config) { global $USER, $CFG; $site = get_site(); $coursecontext = context_course::instance($course->id); $values = array ( 'courseid' => $course->id, 'coursefullname' => format_string($course->fullname, true, array('context' => $coursecontext)), 'courseshortname' => format_string($course->shortname, true, array('context' => $coursecontext)), 'courseidnumber' => $course->idnumber, 'coursesummary' => $course->summary, 'courseformat' => $course->format, 'lang' => current_language(), 'sitename' => format_string($site->fullname, true, array('context' => $coursecontext)), 'serverurl' => $CFG->wwwroot, 'currenttime' => time(), 'urlinstance' => $url->id, 'urlcmid' => $cm->id, 'urlname' => format_string($url->name, true, array('context' => $coursecontext)), 'urlidnumber' => $cm->idnumber, ); if (isloggedin()) { $values['userid'] = $USER->id; $values['userusername'] = $USER->username; $values['useridnumber'] = $USER->idnumber; $values['userfirstname'] = $USER->firstname; $values['userlastname'] = $USER->lastname; $values['userfullname'] = fullname($USER); $values['useremail'] = $USER->email; $values['userphone1'] = $USER->phone1; $values['userphone2'] = $USER->phone2; $values['userinstitution'] = $USER->institution; $values['userdepartment'] = $USER->department; $values['useraddress'] = $USER->address; $values['usercity'] = $USER->city; $now = new DateTime('now', core_date::get_user_timezone_object()); $values['usertimezone'] = $now->getOffset() / 3600.0; // Value in hours for BC. } // weak imitation of Single-Sign-On, for backwards compatibility only // NOTE: login hack is not included in 2.0 any more, new contrib auth plugin // needs to be createed if somebody needs the old functionality! if (!empty($config->secretphrase)) { $values['encryptedcode'] = url_get_encrypted_parameter($url, $config); } //hmm, this is pretty fragile and slow, why do we need it here?? if ($config->rolesinparams) { $coursecontext = context_course::instance($course->id); $roles = role_fix_names(get_all_roles($coursecontext), $coursecontext, ROLENAME_ALIAS); foreach ($roles as $role) { $values['course'.$role->shortname] = $role->localname; } } return $values; } /** * BC internal function * @param object $url * @param object $config * @return string */ function url_get_encrypted_parameter($url, $config) { global $CFG; if (file_exists("$CFG->dirroot/local/externserverfile.php")) { require_once("$CFG->dirroot/local/externserverfile.php"); if (function_exists('extern_server_file')) { return extern_server_file($url, $config); } } return md5(getremoteaddr().$config->secretphrase); } /** * Optimised mimetype detection from general URL * @param $fullurl * @param null $unused This parameter has been deprecated since 4.3 and should not be used anymore. * @return string|null mimetype or null when the filetype is not relevant. */ function url_guess_icon($fullurl, $unused = null) { global $CFG; require_once("$CFG->libdir/filelib.php"); if ($unused !== null) { debugging('Deprecated argument passed to ' . __FUNCTION__, DEBUG_DEVELOPER); } if (substr_count($fullurl, '/') < 3 or substr($fullurl, -1) === '/') { // Most probably default directory - index.php, index.html, etc. Return null because // we want to use the default module icon instead of the HTML file icon. return null; } try { // There can be some cases where the url is invalid making parse_url() to return false. // That will make moodle_url class to throw an exception, so we need to catch the exception to prevent errors. $moodleurl = new moodle_url($fullurl); $fullurl = $moodleurl->out_omit_querystring(); } catch (\moodle_exception $e) { // If an exception is thrown, means the url is invalid. No need to log exception. return null; } $icon = file_extension_icon($fullurl); $htmlicon = file_extension_icon('.htm'); $unknownicon = file_extension_icon(''); $phpicon = file_extension_icon('.php'); // Exception for php files. // We do not want to return those icon types, the module icon is more appropriate. if ($icon === $unknownicon || $icon === $htmlicon || $icon === $phpicon) { return null; } return $icon; } home/smkb8269/public_html/lms.smk-almanshuriyah.sch.id/mod/quiz/locallib.php000064400000251207152267121620022735 0ustar00. /** * Library of functions used by the quiz module. * * This contains functions that are called from within the quiz module only * Functions that are also called by core Moodle are in {@link lib.php} * This script also loads the code in {@link questionlib.php} which holds * the module-indpendent code for handling questions and which in turn * initialises all the questiontype classes. * * @package mod_quiz * @copyright 1999 onwards Martin Dougiamas and others {@link http://moodle.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); require_once($CFG->dirroot . '/mod/quiz/lib.php'); require_once($CFG->libdir . '/completionlib.php'); require_once($CFG->libdir . '/filelib.php'); require_once($CFG->libdir . '/questionlib.php'); use core\di; use core\hook; use core_question\local\bank\condition; use mod_quiz\access_manager; use mod_quiz\event\attempt_submitted; use mod_quiz\grade_calculator; use mod_quiz\hook\attempt_state_changed; use mod_quiz\local\override_manager; use mod_quiz\question\bank\qbank_helper; use mod_quiz\question\display_options; use mod_quiz\quiz_attempt; use mod_quiz\quiz_settings; use mod_quiz\structure; use qbank_previewquestion\question_preview_options; /** * @var int We show the countdown timer if there is less than this amount of time left before the * the quiz close date. (1 hour) */ define('QUIZ_SHOW_TIME_BEFORE_DEADLINE', '3600'); /** * @var int If there are fewer than this many seconds left when the student submits * a page of the quiz, then do not take them to the next page of the quiz. Instead * close the quiz immediately. */ define('QUIZ_MIN_TIME_TO_CONTINUE', '2'); /** * @var int We show no image when user selects No image from dropdown menu in quiz settings. */ define('QUIZ_SHOWIMAGE_NONE', 0); /** * @var int We show small image when user selects small image from dropdown menu in quiz settings. */ define('QUIZ_SHOWIMAGE_SMALL', 1); /** * @var int We show Large image when user selects Large image from dropdown menu in quiz settings. */ define('QUIZ_SHOWIMAGE_LARGE', 2); // Functions related to attempts /////////////////////////////////////////////// /** * Creates an object to represent a new attempt at a quiz * * Creates an attempt object to represent an attempt at the quiz by the current * user starting at the current time. The ->id field is not set. The object is * NOT written to the database. * * @param quiz_settings $quizobj the quiz object to create an attempt for. * @param int $attemptnumber the sequence number for the attempt. * @param stdClass|false $lastattempt the previous attempt by this user, if any. Only needed * if $attemptnumber > 1 and $quiz->attemptonlast is true. * @param int $timenow the time the attempt was started at. * @param bool $ispreview whether this new attempt is a preview. * @param int|null $userid the id of the user attempting this quiz. * * @return stdClass the newly created attempt object. */ function quiz_create_attempt(quiz_settings $quizobj, $attemptnumber, $lastattempt, $timenow, $ispreview = false, $userid = null) { global $USER; if ($userid === null) { $userid = $USER->id; } $quiz = $quizobj->get_quiz(); if ($quiz->sumgrades < grade_calculator::ALMOST_ZERO && $quiz->grade > grade_calculator::ALMOST_ZERO) { throw new moodle_exception('cannotstartgradesmismatch', 'quiz', new moodle_url('/mod/quiz/view.php', ['q' => $quiz->id]), ['grade' => quiz_format_grade($quiz, $quiz->grade)]); } if ($attemptnumber == 1 || !$quiz->attemptonlast) { // We are not building on last attempt so create a new attempt. $attempt = new stdClass(); $attempt->quiz = $quiz->id; $attempt->userid = $userid; $attempt->preview = 0; $attempt->layout = ''; } else { // Build on last attempt. if (empty($lastattempt)) { throw new \moodle_exception('cannotfindprevattempt', 'quiz'); } $attempt = $lastattempt; } $attempt->attempt = $attemptnumber; $attempt->timefinish = 0; $attempt->timemodified = $timenow; $attempt->timemodifiedoffline = 0; $attempt->currentpage = 0; $attempt->sumgrades = null; $attempt->gradednotificationsenttime = null; $attempt->timecheckstate = null; // If this is a preview, mark it as such. if ($ispreview) { $attempt->preview = 1; } return $attempt; } /** * Start a normal, new, quiz attempt. * * @param quiz_settings $quizobj the quiz object to start an attempt for. * @param question_usage_by_activity $quba * @param stdClass $attempt * @param integer $attemptnumber starting from 1 * @param integer $timenow the attempt start time * @param array $questionids slot number => question id. Used for random questions, to force the choice * of a particular actual question. Intended for testing purposes only. * @param array $forcedvariantsbyslot slot number => variant. Used for questions with variants, * to force the choice of a particular variant. Intended for testing * purposes only. * @return stdClass modified attempt object */ function quiz_start_new_attempt($quizobj, $quba, $attempt, $attemptnumber, $timenow, $questionids = [], $forcedvariantsbyslot = []) { // Usages for this user's previous quiz attempts. $qubaids = new \mod_quiz\question\qubaids_for_users_attempts( $quizobj->get_quizid(), $attempt->userid); // Partially load all the questions in this quiz. $quizobj->preload_questions(); // First load all the non-random questions. $randomfound = false; $slot = 0; $questions = []; $maxmark = []; $page = []; foreach ($quizobj->get_questions(null, false) as $questiondata) { $slot += 1; $maxmark[$slot] = $questiondata->maxmark; $page[$slot] = $questiondata->page; if ($questiondata->status == \core_question\local\bank\question_version_status::QUESTION_STATUS_DRAFT) { throw new moodle_exception('questiondraftonly', 'mod_quiz', '', $questiondata->name); } if ($questiondata->qtype == 'random') { $randomfound = true; continue; } $questions[$slot] = question_bank::load_question($questiondata->questionid, $quizobj->get_quiz()->shuffleanswers); } // Then find a question to go in place of each random question. if ($randomfound) { $slot = 0; $usedquestionids = []; foreach ($questions as $question) { if ($question->id && isset($usedquestions[$question->id])) { $usedquestionids[$question->id] += 1; } else { $usedquestionids[$question->id] = 1; } } $randomloader = new \core_question\local\bank\random_question_loader($qubaids, $usedquestionids); foreach ($quizobj->get_questions(null, false) as $questiondata) { $slot += 1; if ($questiondata->qtype != 'random') { continue; } $tagids = qbank_helper::get_tag_ids_for_slot($questiondata); // Deal with fixed random choices for testing. if (isset($questionids[$quba->next_slot_number()])) { $filtercondition = $questiondata->filtercondition; $filters = $filtercondition['filter'] ?? []; if ($randomloader->is_filtered_question_available($filters, $questionids[$quba->next_slot_number()])) { $questions[$slot] = question_bank::load_question( $questionids[$quba->next_slot_number()], $quizobj->get_quiz()->shuffleanswers); continue; } else { throw new coding_exception('Forced question id not available.'); } } // Normal case, pick one at random. $filtercondition = $questiondata->filtercondition; $filters = $filtercondition['filter'] ?? []; $questionid = $randomloader->get_next_filtered_question_id($filters); if ($questionid === null) { throw new moodle_exception('notenoughrandomquestions', 'quiz', $quizobj->view_url(), $questiondata); } $questions[$slot] = question_bank::load_question($questionid, $quizobj->get_quiz()->shuffleanswers); } } // Finally add them all to the usage. ksort($questions); foreach ($questions as $slot => $question) { $newslot = $quba->add_question($question, $maxmark[$slot]); if ($newslot != $slot) { throw new coding_exception('Slot numbers have got confused.'); } } // Start all the questions. $variantstrategy = new core_question\engine\variants\least_used_strategy($quba, $qubaids); if (!empty($forcedvariantsbyslot)) { $forcedvariantsbyseed = question_variant_forced_choices_selection_strategy::prepare_forced_choices_array( $forcedvariantsbyslot, $quba); $variantstrategy = new question_variant_forced_choices_selection_strategy( $forcedvariantsbyseed, $variantstrategy); } $quba->start_all_questions($variantstrategy, question_attempt_step::TIMECREATED_ON_FIRST_RENDER, $attempt->userid); // Work out the attempt layout. $sections = $quizobj->get_sections(); foreach ($sections as $i => $section) { if (isset($sections[$i + 1])) { $sections[$i]->lastslot = $sections[$i + 1]->firstslot - 1; } else { $sections[$i]->lastslot = count($questions); } } $layout = []; foreach ($sections as $section) { if ($section->shufflequestions) { $questionsinthissection = []; for ($slot = $section->firstslot; $slot <= $section->lastslot; $slot += 1) { $questionsinthissection[] = $slot; } shuffle($questionsinthissection); $questionsonthispage = 0; foreach ($questionsinthissection as $slot) { if ($questionsonthispage && $questionsonthispage == $quizobj->get_quiz()->questionsperpage) { $layout[] = 0; $questionsonthispage = 0; } $layout[] = $slot; $questionsonthispage += 1; } } else { $currentpage = $page[$section->firstslot]; for ($slot = $section->firstslot; $slot <= $section->lastslot; $slot += 1) { if ($currentpage !== null && $page[$slot] != $currentpage) { $layout[] = 0; } $layout[] = $slot; $currentpage = $page[$slot]; } } // Each section ends with a page break. $layout[] = 0; } $attempt->layout = implode(',', $layout); return $attempt; } /** * Start a subsequent new attempt, in each attempt builds on last mode. * * @param question_usage_by_activity $quba this question usage * @param stdClass $attempt this attempt * @param stdClass $lastattempt last attempt * @return stdClass modified attempt object * */ function quiz_start_attempt_built_on_last($quba, $attempt, $lastattempt) { $oldquba = question_engine::load_questions_usage_by_activity($lastattempt->uniqueid); $oldnumberstonew = []; foreach ($oldquba->get_attempt_iterator() as $oldslot => $oldqa) { $question = $oldqa->get_question(false); if ($question->status == \core_question\local\bank\question_version_status::QUESTION_STATUS_DRAFT) { throw new moodle_exception('questiondraftonly', 'mod_quiz', '', $question->name); } $newslot = $quba->add_question($question, $oldqa->get_max_mark()); $quba->start_question_based_on($newslot, $oldqa); $oldnumberstonew[$oldslot] = $newslot; } // Update attempt layout. $newlayout = []; foreach (explode(',', $lastattempt->layout) as $oldslot) { if ($oldslot != 0) { $newlayout[] = $oldnumberstonew[$oldslot]; } else { $newlayout[] = 0; } } $attempt->layout = implode(',', $newlayout); return $attempt; } /** * Create or update the quiz attempt record, and the question usage. * * If the attempt already exists in the database with the NOT_STARTED state, it will be transitioned * to IN_PROGRESS and the timestart updated. If it does not already exist, a new record will be created * already in the IN_PROGRESS state. * * @param quiz_settings $quizobj * @param question_usage_by_activity $quba * @param stdClass $attempt * @param ?int $timenow The time to use for the attempt's timestart property. Defaults to time(). * @return stdClass attempt object with uniqueid and id set. */ function quiz_attempt_save_started( quiz_settings $quizobj, question_usage_by_activity $quba, \stdClass $attempt, ?int $timenow = null, ): stdClass { global $DB; $attempt->timestart = $timenow ?? time(); $timeclose = $quizobj->get_access_manager($attempt->timestart)->get_end_time($attempt); if ($timeclose === false || $attempt->preview) { $attempt->timecheckstate = null; } else { $attempt->timecheckstate = $timeclose; } $originalattempt = null; if (isset($attempt->id) && $attempt->state === quiz_attempt::NOT_STARTED) { $originalattempt = clone $attempt; // In case questions have been edited since attempts were pre-created, update questions now. quiz_attempt::create($attempt->id)->update_questions_to_new_version_if_changed(); // Update the attempt's state. $attempt->state = quiz_attempt::IN_PROGRESS; $DB->update_record('quiz_attempts', $attempt); } else { // Save the attempt in the database. question_engine::save_questions_usage_by_activity($quba); $attempt->uniqueid = $quba->get_id(); $attempt->state = quiz_attempt::IN_PROGRESS; $attempt->id = $DB->insert_record('quiz_attempts', $attempt); } // Params used by the events below. $params = [ 'objectid' => $attempt->id, 'relateduserid' => $attempt->userid, 'courseid' => $quizobj->get_courseid(), 'context' => $quizobj->get_context() ]; // Decide which event we are using. if ($attempt->preview) { $params['other'] = [ 'quizid' => $quizobj->get_quizid() ]; $event = \mod_quiz\event\attempt_preview_started::create($params); } else { $event = \mod_quiz\event\attempt_started::create($params); } // Trigger the event. $event->add_record_snapshot('quiz', $quizobj->get_quiz()); $event->add_record_snapshot('quiz_attempts', $attempt); $event->trigger(); di::get(hook\manager::class)->dispatch(new attempt_state_changed($originalattempt, $attempt)); return $attempt; } /** * Create the quiz attempt record, and the question usage. * * This saves an attempt in the NOT_STARTED state, and is designed for use when pre-creating attempts * ahead of the quiz start time to spread out the processing load. * * @param question_usage_by_activity $quba * @param stdClass $attempt * @return stdClass attempt object with uniqueid and id set. */ function quiz_attempt_save_not_started(question_usage_by_activity $quba, stdClass $attempt): stdClass { global $DB; // Save the attempt in the database. question_engine::save_questions_usage_by_activity($quba); $attempt->uniqueid = $quba->get_id(); $attempt->state = quiz_attempt::NOT_STARTED; $attempt->id = $DB->insert_record('quiz_attempts', $attempt); di::get(hook\manager::class)->dispatch(new attempt_state_changed(null, $attempt)); return $attempt; } /** * Returns an unfinished attempt (if there is one) for the given * user on the given quiz. This function does not return preview attempts. * * @param int $quizid the id of the quiz. * @param int $userid the id of the user. * * @return mixed the unfinished attempt if there is one, false if not. */ function quiz_get_user_attempt_unfinished($quizid, $userid) { $attempts = quiz_get_user_attempts($quizid, $userid, 'unfinished', true); if ($attempts) { return array_shift($attempts); } else { return false; } } /** * Delete a quiz attempt. * @param mixed $attempt an integer attempt id or an attempt object * (row of the quiz_attempts table). * @param stdClass $quiz the quiz object. */ function quiz_delete_attempt($attempt, $quiz) { global $DB; if (is_numeric($attempt)) { if (!$attempt = $DB->get_record('quiz_attempts', ['id' => $attempt])) { return; } } if ($attempt->quiz != $quiz->id) { debugging("Trying to delete attempt $attempt->id which belongs to quiz $attempt->quiz " . "but was passed quiz $quiz->id."); return; } if (!isset($quiz->cmid)) { $cm = get_coursemodule_from_instance('quiz', $quiz->id, $quiz->course); $quiz->cmid = $cm->id; } question_engine::delete_questions_usage_by_activity($attempt->uniqueid); $DB->delete_records('quiz_attempts', ['id' => $attempt->id]); // Log the deletion of the attempt if not a preview. if (!$attempt->preview) { $params = [ 'objectid' => $attempt->id, 'relateduserid' => $attempt->userid, 'context' => context_module::instance($quiz->cmid), 'other' => [ 'quizid' => $quiz->id ] ]; $event = \mod_quiz\event\attempt_deleted::create($params); $event->add_record_snapshot('quiz_attempts', $attempt); $event->trigger(); // This class callback is deprecated, and will be removed in Moodle 4.8 (MDL-80327). // Use the attempt_state_changed hook instead. $callbackclasses = \core_component::get_plugin_list_with_class('quiz', 'quiz_attempt_deleted'); foreach ($callbackclasses as $callbackclass) { component_class_callback($callbackclass, 'callback', [$quiz->id], null, true); } di::get(hook\manager::class)->dispatch(new attempt_state_changed($attempt, null)); } // Search quiz_attempts for other instances by this user. // If none, then delete record for this quiz, this user from quiz_grades // else recalculate best grade. $userid = $attempt->userid; $gradecalculator = quiz_settings::create($quiz->id)->get_grade_calculator(); if (!$DB->record_exists('quiz_attempts', ['userid' => $userid, 'quiz' => $quiz->id])) { $DB->delete_records('quiz_grades', ['userid' => $userid, 'quiz' => $quiz->id]); } else { $gradecalculator->recompute_final_grade($userid); } quiz_update_grades($quiz, $userid); } /** * Delete all the preview attempts at a quiz, or possibly all the attempts belonging * to one user. * @param stdClass $quiz the quiz object. * @param int $userid (optional) if given, only delete the previews belonging to this user. */ function quiz_delete_previews($quiz, $userid = null) { global $DB; $conditions = ['quiz' => $quiz->id, 'preview' => 1]; if (!empty($userid)) { $conditions['userid'] = $userid; } $previewattempts = $DB->get_records('quiz_attempts', $conditions); foreach ($previewattempts as $attempt) { quiz_delete_attempt($attempt, $quiz); } } /** * @param int $quizid The quiz id. * @return bool whether this quiz has any (non-preview) attempts. */ function quiz_has_attempts($quizid) { global $DB; return $DB->record_exists('quiz_attempts', ['quiz' => $quizid, 'preview' => 0]); } // Functions to do with quiz layout and pages ////////////////////////////////// /** * Repaginate the questions in a quiz * @param int $quizid the id of the quiz to repaginate. * @param int $slotsperpage number of items to put on each page. 0 means unlimited. */ function quiz_repaginate_questions($quizid, $slotsperpage) { global $DB; $trans = $DB->start_delegated_transaction(); $sections = $DB->get_records('quiz_sections', ['quizid' => $quizid], 'firstslot ASC'); $firstslots = []; foreach ($sections as $section) { if ((int)$section->firstslot === 1) { continue; } $firstslots[] = $section->firstslot; } $slots = $DB->get_records('quiz_slots', ['quizid' => $quizid], 'slot'); $currentpage = 1; $slotsonthispage = 0; foreach ($slots as $slot) { if (($firstslots && in_array($slot->slot, $firstslots)) || ($slotsonthispage && $slotsonthispage == $slotsperpage)) { $currentpage += 1; $slotsonthispage = 0; } if ($slot->page != $currentpage) { $DB->set_field('quiz_slots', 'page', $currentpage, ['id' => $slot->id]); } $slotsonthispage += 1; } $trans->allow_commit(); // Log quiz re-paginated event. $cm = get_coursemodule_from_instance('quiz', $quizid); $event = \mod_quiz\event\quiz_repaginated::create([ 'context' => \context_module::instance($cm->id), 'objectid' => $quizid, 'other' => [ 'slotsperpage' => $slotsperpage ] ]); $event->trigger(); } // Functions to do with quiz grades //////////////////////////////////////////// // Note a lot of logic related to this is now in the grade_calculator class. /** * Convert the raw grade stored in $attempt into a grade out of the maximum * grade for this quiz. * * @param float $rawgrade the unadjusted grade, fof example $attempt->sumgrades * @param stdClass $quiz the quiz object. Only the fields grade, sumgrades and decimalpoints are used. * @param bool|string $format whether to format the results for display * or 'question' to format a question grade (different number of decimal places. * @return float|string the rescaled grade, or null/the lang string 'notyetgraded' * if the $grade is null. */ function quiz_rescale_grade($rawgrade, $quiz, $format = true) { if (is_null($rawgrade)) { $grade = null; } else if ($quiz->sumgrades >= grade_calculator::ALMOST_ZERO) { $grade = $rawgrade * $quiz->grade / $quiz->sumgrades; } else { $grade = 0; } if ($format === 'question') { $grade = quiz_format_question_grade($quiz, $grade); } else if ($format) { $grade = quiz_format_grade($quiz, $grade); } return $grade; } /** * Get the feedback object for this grade on this quiz. * * @param float $grade a grade on this quiz. * @param stdClass $quiz the quiz settings. * @return false|stdClass the record object or false if there is not feedback for the given grade * @since Moodle 3.1 */ function quiz_feedback_record_for_grade($grade, $quiz) { global $DB; // With CBM etc, it is possible to get -ve grades, which would then not match // any feedback. Therefore, we replace -ve grades with 0. $grade = max($grade, 0); $feedback = $DB->get_record_select('quiz_feedback', 'quizid = ? AND mingrade <= ? AND ? < maxgrade', [$quiz->id, $grade, $grade]); return $feedback; } /** * Get the feedback text that should be show to a student who * got this grade on this quiz. The feedback is processed ready for diplay. * * @param float $grade a grade on this quiz. * @param stdClass $quiz the quiz settings. * @param context_module $context the quiz context. * @return string the comment that corresponds to this grade (empty string if there is not one. */ function quiz_feedback_for_grade($grade, $quiz, $context) { if (is_null($grade)) { return ''; } $feedback = quiz_feedback_record_for_grade($grade, $quiz); if (empty($feedback->feedbacktext)) { return ''; } // Clean the text, ready for display. $formatoptions = new stdClass(); $formatoptions->noclean = true; $feedbacktext = file_rewrite_pluginfile_urls($feedback->feedbacktext, 'pluginfile.php', $context->id, 'mod_quiz', 'feedback', $feedback->id); $feedbacktext = format_text($feedbacktext, $feedback->feedbacktextformat, $formatoptions); return $feedbacktext; } /** * @param stdClass $quiz the quiz database row. * @return bool Whether this quiz has any non-blank feedback text. */ function quiz_has_feedback($quiz) { global $DB; static $cache = []; if (!array_key_exists($quiz->id, $cache)) { $cache[$quiz->id] = quiz_has_grades($quiz) && $DB->record_exists_select('quiz_feedback', "quizid = ? AND " . $DB->sql_isnotempty('quiz_feedback', 'feedbacktext', false, true), [$quiz->id]); } return $cache[$quiz->id]; } /** * Return summary of the number of settings override that exist. * * To get a nice display of this, see the quiz_override_summary_links() * quiz renderer method. * * @param stdClass $quiz the quiz settings. Only $quiz->id is used at the moment. * @param cm_info|stdClass $cm the cm object. Only $cm->course, $cm->groupmode and * $cm->groupingid fields are used at the moment. * @param int $currentgroup if there is a concept of current group where this method is being called * (e.g. a report) pass it in here. Default 0 which means no current group. * @return array like 'group' => 3, 'user' => 12] where 3 is the number of group overrides, * and 12 is the number of user ones. */ function quiz_override_summary(stdClass $quiz, cm_info|stdClass $cm, int $currentgroup = 0): array { global $DB; if ($currentgroup) { // Currently only interested in one group. $groupcount = $DB->count_records('quiz_overrides', ['quiz' => $quiz->id, 'groupid' => $currentgroup]); $usercount = $DB->count_records_sql(" SELECT COUNT(1) FROM {quiz_overrides} o JOIN {groups_members} gm ON o.userid = gm.userid WHERE o.quiz = ? AND gm.groupid = ? ", [$quiz->id, $currentgroup]); return ['group' => $groupcount, 'user' => $usercount, 'mode' => 'onegroup']; } $quizgroupmode = groups_get_activity_groupmode($cm); $accessallgroups = ($quizgroupmode == NOGROUPS) || has_capability('moodle/site:accessallgroups', context_module::instance($cm->id)); if ($accessallgroups) { // User can see all groups. $groupcount = $DB->count_records_select('quiz_overrides', 'quiz = ? AND groupid IS NOT NULL', [$quiz->id]); $usercount = $DB->count_records_select('quiz_overrides', 'quiz = ? AND userid IS NOT NULL', [$quiz->id]); return ['group' => $groupcount, 'user' => $usercount, 'mode' => 'allgroups']; } else { // User can only see groups they are in. $groups = groups_get_activity_allowed_groups($cm); if (!$groups) { return ['group' => 0, 'user' => 0, 'mode' => 'somegroups']; } list($groupidtest, $params) = $DB->get_in_or_equal(array_keys($groups)); $params[] = $quiz->id; $groupcount = $DB->count_records_select('quiz_overrides', "groupid $groupidtest AND quiz = ?", $params); $usercount = $DB->count_records_sql(" SELECT COUNT(1) FROM {quiz_overrides} o JOIN {groups_members} gm ON o.userid = gm.userid WHERE gm.groupid $groupidtest AND o.quiz = ? ", $params); return ['group' => $groupcount, 'user' => $usercount, 'mode' => 'somegroups']; } } /** * Efficiently update check state time on all open attempts * * @param array $conditions optional restrictions on which attempts to update * Allowed conditions: * courseid => (array|int) attempts in given course(s) * userid => (array|int) attempts for given user(s) * quizid => (array|int) attempts in given quiz(s) * groupid => (array|int) quizzes with some override for given group(s) * */ function quiz_update_open_attempts(array $conditions) { global $DB; foreach ($conditions as &$value) { if (!is_array($value)) { $value = [$value]; } } $params = []; $wheres = ["quiza.state IN ('inprogress', 'overdue')"]; $iwheres = ["iquiza.state IN ('inprogress', 'overdue')"]; if (isset($conditions['courseid'])) { list ($incond, $inparams) = $DB->get_in_or_equal($conditions['courseid'], SQL_PARAMS_NAMED, 'cid'); $params = array_merge($params, $inparams); $wheres[] = "quiza.quiz IN (SELECT q.id FROM {quiz} q WHERE q.course $incond)"; list ($incond, $inparams) = $DB->get_in_or_equal($conditions['courseid'], SQL_PARAMS_NAMED, 'icid'); $params = array_merge($params, $inparams); $iwheres[] = "iquiza.quiz IN (SELECT q.id FROM {quiz} q WHERE q.course $incond)"; } if (isset($conditions['userid'])) { list ($incond, $inparams) = $DB->get_in_or_equal($conditions['userid'], SQL_PARAMS_NAMED, 'uid'); $params = array_merge($params, $inparams); $wheres[] = "quiza.userid $incond"; list ($incond, $inparams) = $DB->get_in_or_equal($conditions['userid'], SQL_PARAMS_NAMED, 'iuid'); $params = array_merge($params, $inparams); $iwheres[] = "iquiza.userid $incond"; } if (isset($conditions['quizid'])) { list ($incond, $inparams) = $DB->get_in_or_equal($conditions['quizid'], SQL_PARAMS_NAMED, 'qid'); $params = array_merge($params, $inparams); $wheres[] = "quiza.quiz $incond"; list ($incond, $inparams) = $DB->get_in_or_equal($conditions['quizid'], SQL_PARAMS_NAMED, 'iqid'); $params = array_merge($params, $inparams); $iwheres[] = "iquiza.quiz $incond"; } if (isset($conditions['groupid'])) { list ($incond, $inparams) = $DB->get_in_or_equal($conditions['groupid'], SQL_PARAMS_NAMED, 'gid'); $params = array_merge($params, $inparams); $wheres[] = "quiza.quiz IN (SELECT qo.quiz FROM {quiz_overrides} qo WHERE qo.groupid $incond)"; list ($incond, $inparams) = $DB->get_in_or_equal($conditions['groupid'], SQL_PARAMS_NAMED, 'igid'); $params = array_merge($params, $inparams); $iwheres[] = "iquiza.quiz IN (SELECT qo.quiz FROM {quiz_overrides} qo WHERE qo.groupid $incond)"; } // SQL to compute timeclose and timelimit for each attempt: $quizausersql = quiz_get_attempt_usertime_sql( implode("\n AND ", $iwheres)); // SQL to compute the new timecheckstate $timecheckstatesql = " CASE WHEN quizauser.usertimelimit = 0 AND quizauser.usertimeclose = 0 THEN NULL WHEN quizauser.usertimelimit = 0 THEN quizauser.usertimeclose WHEN quizauser.usertimeclose = 0 THEN quiza.timestart + quizauser.usertimelimit WHEN quiza.timestart + quizauser.usertimelimit < quizauser.usertimeclose THEN quiza.timestart + quizauser.usertimelimit ELSE quizauser.usertimeclose END + CASE WHEN quiza.state = 'overdue' THEN quiz.graceperiod ELSE 0 END"; // SQL to select which attempts to process $attemptselect = implode("\n AND ", $wheres); /* * Each database handles updates with inner joins differently: * - mysql does not allow a FROM clause * - postgres and mssql allow FROM but handle table aliases differently * * Different code for each database. */ $dbfamily = $DB->get_dbfamily(); if ($dbfamily == 'mysql') { $updatesql = "UPDATE {quiz_attempts} quiza JOIN {quiz} quiz ON quiz.id = quiza.quiz JOIN ( $quizausersql ) quizauser ON quizauser.id = quiza.id SET quiza.timecheckstate = $timecheckstatesql WHERE $attemptselect"; } else if ($dbfamily == 'postgres') { $updatesql = "UPDATE {quiz_attempts} quiza SET timecheckstate = $timecheckstatesql FROM {quiz} quiz, ( $quizausersql ) quizauser WHERE quiz.id = quiza.quiz AND quizauser.id = quiza.id AND $attemptselect"; } else if ($dbfamily == 'mssql') { $updatesql = "UPDATE quiza SET timecheckstate = $timecheckstatesql FROM {quiz_attempts} quiza JOIN {quiz} quiz ON quiz.id = quiza.quiz JOIN ( $quizausersql ) quizauser ON quizauser.id = quiza.id WHERE $attemptselect"; } else { throw new \core\exception\coding_exception("Unsupported database family: {$dbfamily}"); } $DB->execute($updatesql, $params); } /** * Returns SQL to compute timeclose and timelimit for every attempt, taking into account user and group overrides. * The query used herein is very similar to the one in function quiz_get_user_timeclose, so, in case you * would change either one of them, make sure to apply your changes to both. * * @param string $redundantwhereclauses extra where clauses to add to the subquery * for performance. These can use the table alias iquiza for the quiz attempts table. * @return string SQL select with columns attempt.id, usertimeclose, usertimelimit. */ function quiz_get_attempt_usertime_sql($redundantwhereclauses = '') { if ($redundantwhereclauses) { $redundantwhereclauses = 'WHERE ' . $redundantwhereclauses; } // The multiple qgo JOINS are necessary because we want timeclose/timelimit = 0 (unlimited) to supercede // any other group override $quizausersql = " SELECT iquiza.id, COALESCE(MAX(quo.timeclose), MAX(qgo1.timeclose), MAX(qgo2.timeclose), iquiz.timeclose) AS usertimeclose, COALESCE(MAX(quo.timelimit), MAX(qgo3.timelimit), MAX(qgo4.timelimit), iquiz.timelimit) AS usertimelimit FROM {quiz_attempts} iquiza JOIN {quiz} iquiz ON iquiz.id = iquiza.quiz LEFT JOIN {quiz_overrides} quo ON quo.quiz = iquiza.quiz AND quo.userid = iquiza.userid LEFT JOIN {groups_members} gm ON gm.userid = iquiza.userid LEFT JOIN {quiz_overrides} qgo1 ON qgo1.quiz = iquiza.quiz AND qgo1.groupid = gm.groupid AND qgo1.timeclose = 0 LEFT JOIN {quiz_overrides} qgo2 ON qgo2.quiz = iquiza.quiz AND qgo2.groupid = gm.groupid AND qgo2.timeclose > 0 LEFT JOIN {quiz_overrides} qgo3 ON qgo3.quiz = iquiza.quiz AND qgo3.groupid = gm.groupid AND qgo3.timelimit = 0 LEFT JOIN {quiz_overrides} qgo4 ON qgo4.quiz = iquiza.quiz AND qgo4.groupid = gm.groupid AND qgo4.timelimit > 0 $redundantwhereclauses GROUP BY iquiza.id, iquiz.id, iquiz.timeclose, iquiz.timelimit"; return $quizausersql; } /** * @return array int => lang string the options for calculating the quiz grade * from the individual attempt grades. */ function quiz_get_grading_options() { return [ QUIZ_GRADEHIGHEST => get_string('gradehighest', 'quiz'), QUIZ_GRADEAVERAGE => get_string('gradeaverage', 'quiz'), QUIZ_ATTEMPTFIRST => get_string('attemptfirst', 'quiz'), QUIZ_ATTEMPTLAST => get_string('attemptlast', 'quiz') ]; } /** * @param int $option one of the values QUIZ_GRADEHIGHEST, QUIZ_GRADEAVERAGE, * QUIZ_ATTEMPTFIRST or QUIZ_ATTEMPTLAST. * @return the lang string for that option. */ function quiz_get_grading_option_name($option) { $strings = quiz_get_grading_options(); return $strings[$option]; } /** * @return array string => lang string the options for handling overdue quiz * attempts. */ function quiz_get_overdue_handling_options() { return [ 'autosubmit' => get_string('overduehandlingautosubmit', 'quiz'), 'graceperiod' => get_string('overduehandlinggraceperiod', 'quiz'), 'autoabandon' => get_string('overduehandlingautoabandon', 'quiz'), ]; } /** * Get the choices for what size user picture to show. * @return array string => lang string the options for whether to display the user's picture. */ function quiz_get_user_image_options() { return [ QUIZ_SHOWIMAGE_NONE => get_string('shownoimage', 'quiz'), QUIZ_SHOWIMAGE_SMALL => get_string('showsmallimage', 'quiz'), QUIZ_SHOWIMAGE_LARGE => get_string('showlargeimage', 'quiz'), ]; } /** * Return an user's timeclose for all quizzes in a course, hereby taking into account group and user overrides. * * @param int $courseid the course id. * @return stdClass An object with of all quizids and close unixdates in this course, taking into account the most lenient * overrides, if existing and 0 if no close date is set. */ function quiz_get_user_timeclose($courseid) { global $DB, $USER; // For teacher and manager/admins return timeclose. if (has_capability('moodle/course:update', context_course::instance($courseid))) { $sql = "SELECT quiz.id, quiz.timeclose AS usertimeclose FROM {quiz} quiz WHERE quiz.course = :courseid"; $results = $DB->get_records_sql($sql, ['courseid' => $courseid]); return $results; } $sql = "SELECT q.id, COALESCE(v.userclose, v.groupclose, q.timeclose, 0) AS usertimeclose FROM ( SELECT quiz.id as quizid, MAX(quo.timeclose) AS userclose, MAX(qgo.timeclose) AS groupclose FROM {quiz} quiz LEFT JOIN {quiz_overrides} quo on quiz.id = quo.quiz AND quo.userid = :userid LEFT JOIN {groups_members} gm ON gm.userid = :useringroupid LEFT JOIN {quiz_overrides} qgo on quiz.id = qgo.quiz AND qgo.groupid = gm.groupid WHERE quiz.course = :courseid GROUP BY quiz.id) v JOIN {quiz} q ON q.id = v.quizid"; $results = $DB->get_records_sql($sql, ['userid' => $USER->id, 'useringroupid' => $USER->id, 'courseid' => $courseid]); return $results; } /** * Get the choices to offer for the 'Questions per page' option. * @return array int => string. */ function quiz_questions_per_page_options() { $pageoptions = []; $pageoptions[0] = get_string('neverallononepage', 'quiz'); $pageoptions[1] = get_string('everyquestion', 'quiz'); for ($i = 2; $i <= QUIZ_MAX_QPP_OPTION; ++$i) { $pageoptions[$i] = get_string('everynquestions', 'quiz', $i); } return $pageoptions; } /** * Get the human-readable name for a quiz attempt state. * @param string $state one of the state constants like {@see quiz_attempt::IN_PROGRESS}. * @return string The lang string to describe that state. */ function quiz_attempt_state_name($state) { switch ($state) { case quiz_attempt::NOT_STARTED: return get_string('statenotstarted', 'quiz'); case quiz_attempt::IN_PROGRESS: return get_string('stateinprogress', 'quiz'); case quiz_attempt::OVERDUE: return get_string('stateoverdue', 'quiz'); case quiz_attempt::SUBMITTED: return get_string('statesubmitted', 'quiz'); case quiz_attempt::FINISHED: return get_string('statefinished', 'quiz'); case quiz_attempt::ABANDONED: return get_string('stateabandoned', 'quiz'); default: throw new coding_exception('Unknown quiz attempt state.'); } } // Other quiz functions //////////////////////////////////////////////////////// /** * @param stdClass $quiz the quiz. * @param int $cmid the course_module object for this quiz. * @param stdClass $question the question. * @param string $returnurl url to return to after action is done. * @param int $variant which question variant to preview (optional). * @return string html for a number of icons linked to action pages for a * question - preview and edit / view icons depending on user capabilities. */ function quiz_question_action_icons($quiz, $cmid, $question, $returnurl, $variant = null) { $html = ''; if ($question->qtype !== 'random') { $html = quiz_question_preview_button($quiz, $question, false, $variant); } $html .= quiz_question_edit_button($cmid, $question, $returnurl); return $html; } /** * @param int $cmid the course_module.id for this quiz. * @param stdClass $question the question. * @param string $returnurl url to return to after action is done. * @param string $contentbeforeicon some HTML content to be added inside the link, before the icon. * @return the HTML for an edit icon, view icon, or nothing for a question * (depending on permissions). */ function quiz_question_edit_button($cmid, $question, $returnurl, $contentaftericon = '') { global $CFG, $OUTPUT; // Minor efficiency saving. Only get strings once, even if there are a lot of icons on one page. static $stredit = null; static $strview = null; if ($stredit === null) { $stredit = get_string('edit'); $strview = get_string('view'); } // What sort of icon should we show? $action = ''; if (!empty($question->id) && (question_has_capability_on($question, 'edit') || question_has_capability_on($question, 'move'))) { $action = $stredit; $icon = 't/edit'; } else if (!empty($question->id) && question_has_capability_on($question, 'view')) { $action = $strview; $icon = 'i/info'; } // Build the icon. if ($action) { if ($returnurl instanceof moodle_url) { $returnurl = $returnurl->out_as_local_url(false); } $questionparams = ['returnurl' => $returnurl, 'cmid' => $cmid, 'id' => $question->id]; $questionurl = new moodle_url("$CFG->wwwroot/question/bank/editquestion/question.php", $questionparams); return '' . $OUTPUT->pix_icon($icon, $action) . $contentaftericon . ''; } else if ($contentaftericon) { return '' . $contentaftericon . ''; } else { return ''; } } /** * @param stdClass $quiz the quiz settings * @param stdClass $question the question * @param int $variant which question variant to preview (optional). * @param int $restartversion version of the question to use when restarting the preview. * @return moodle_url to preview this question with the options from this quiz. */ function quiz_question_preview_url($quiz, $question, $variant = null, $restartversion = null) { // Get the appropriate display options. $displayoptions = display_options::make_from_quiz($quiz, display_options::DURING); $maxmark = null; if (isset($question->maxmark)) { $maxmark = $question->maxmark; } // Work out the correcte preview URL. return \qbank_previewquestion\helper::question_preview_url($question->id, $quiz->preferredbehaviour, $maxmark, $displayoptions, $variant, null, null, $restartversion); } /** * @param stdClass $quiz the quiz settings * @param stdClass $question the question * @param bool $label if true, show the preview question label after the icon * @param int $variant which question variant to preview (optional). * @param bool $random if question is random, true. * @return string the HTML for a preview question icon. */ function quiz_question_preview_button($quiz, $question, $label = false, $variant = null, $random = null) { global $PAGE; if (!question_has_capability_on($question, 'use')) { return ''; } $structure = quiz_settings::create($quiz->id)->get_structure(); if (!empty($question->slot)) { $requestedversion = $structure->get_slot_by_number($question->slot)->requestedversion ?? question_preview_options::ALWAYS_LATEST; } else { $requestedversion = question_preview_options::ALWAYS_LATEST; } return $PAGE->get_renderer('mod_quiz', 'edit')->question_preview_icon( $quiz, $question, $label, $variant, $requestedversion); } /** * @param stdClass $attempt the attempt. * @param stdClass $context the quiz context. * @return int whether flags should be shown/editable to the current user for this attempt. */ function quiz_get_flag_option($attempt, $context) { global $USER; if (!has_capability('moodle/question:flag', $context)) { return question_display_options::HIDDEN; } else if ($attempt->userid == $USER->id) { return question_display_options::EDITABLE; } else { return question_display_options::VISIBLE; } } /** * Work out what state this quiz attempt is in - in the sense used by * quiz_get_review_options, not in the sense of $attempt->state. * @param stdClass $quiz the quiz settings * @param stdClass $attempt the quiz_attempt database row. * @return int one of the display_options::DURING, * IMMEDIATELY_AFTER, LATER_WHILE_OPEN or AFTER_CLOSE constants. */ function quiz_attempt_state($quiz, $attempt) { if ($attempt->state == quiz_attempt::IN_PROGRESS) { return display_options::DURING; } else if ($quiz->timeclose && time() >= $quiz->timeclose) { return display_options::AFTER_CLOSE; } else if (time() < $attempt->timefinish + quiz_attempt::IMMEDIATELY_AFTER_PERIOD) { return display_options::IMMEDIATELY_AFTER; } else { return display_options::LATER_WHILE_OPEN; } } /** * The appropriate display_options object for this attempt at this quiz right now. * * @param stdClass $quiz the quiz instance. * @param stdClass $attempt the attempt in question. * @param context $context the quiz context. * * @return display_options */ function quiz_get_review_options($quiz, $attempt, $context) { $options = display_options::make_from_quiz($quiz, quiz_attempt_state($quiz, $attempt)); $options->readonly = true; $options->flags = quiz_get_flag_option($attempt, $context); if (!empty($attempt->id)) { $options->questionreviewlink = new moodle_url('/mod/quiz/reviewquestion.php', ['attempt' => $attempt->id]); } // Show a link to the comment box only for closed attempts. if (!empty($attempt->id) && $attempt->state == quiz_attempt::FINISHED && !$attempt->preview && !is_null($context) && has_capability('mod/quiz:grade', $context)) { $options->manualcomment = question_display_options::VISIBLE; $options->manualcommentlink = new moodle_url('/mod/quiz/comment.php', ['attempt' => $attempt->id]); } if (!is_null($context) && !$attempt->preview && has_capability('mod/quiz:viewreports', $context) && has_capability('moodle/grade:viewhidden', $context)) { // People who can see reports and hidden grades should be shown everything, // except during preview when teachers want to see what students see. $options->attempt = question_display_options::VISIBLE; $options->correctness = question_display_options::VISIBLE; $options->marks = question_display_options::MARK_AND_MAX; $options->feedback = question_display_options::VISIBLE; $options->numpartscorrect = question_display_options::VISIBLE; $options->manualcomment = question_display_options::VISIBLE; $options->generalfeedback = question_display_options::VISIBLE; $options->rightanswer = question_display_options::VISIBLE; $options->overallfeedback = question_display_options::VISIBLE; $options->history = question_display_options::VISIBLE; $options->userinfoinhistory = $attempt->userid; } return $options; } /** * Combines the review options from a number of different quiz attempts. * Returns an array of two ojects, so the suggested way of calling this * funciton is: * list($someoptions, $alloptions) = quiz_get_combined_reviewoptions(...) * * @param stdClass $quiz the quiz instance. * @param array $attempts an array of attempt objects. * * @return array of two options objects, one showing which options are true for * at least one of the attempts, the other showing which options are true * for all attempts. */ function quiz_get_combined_reviewoptions($quiz, $attempts) { $fields = ['feedback', 'generalfeedback', 'rightanswer', 'overallfeedback']; $someoptions = new stdClass(); $alloptions = new stdClass(); foreach ($fields as $field) { $someoptions->$field = false; $alloptions->$field = true; } $someoptions->marks = question_display_options::HIDDEN; $alloptions->marks = question_display_options::MARK_AND_MAX; // This shouldn't happen, but we need to prevent reveal information. if (empty($attempts)) { return [$someoptions, $someoptions]; } foreach ($attempts as $attempt) { $attemptoptions = display_options::make_from_quiz($quiz, quiz_attempt_state($quiz, $attempt)); foreach ($fields as $field) { $someoptions->$field = $someoptions->$field || $attemptoptions->$field; $alloptions->$field = $alloptions->$field && $attemptoptions->$field; } $someoptions->marks = max($someoptions->marks, $attemptoptions->marks); $alloptions->marks = min($alloptions->marks, $attemptoptions->marks); } return [$someoptions, $alloptions]; } // Functions for sending notification messages ///////////////////////////////// /** * Sends a confirmation message to the student confirming that the attempt was processed. * * @param stdClass $recipient user object for the recipient. * @param stdClass $a lots of useful information that can be used in the message * subject and body. * @param bool $studentisonline is the student currently interacting with Moodle? * * @return int|false as for {@link message_send()}. */ function quiz_send_confirmation($recipient, $a, $studentisonline) { // Add information about the recipient to $a. // Don't do idnumber. we want idnumber to be the submitter's idnumber. $a->username = fullname($recipient); $a->userusername = $recipient->username; // Prepare the message. $eventdata = new \core\message\message(); $eventdata->courseid = $a->courseid; $eventdata->component = 'mod_quiz'; $eventdata->name = 'confirmation'; $eventdata->notification = 1; $eventdata->userfrom = core_user::get_noreply_user(); $eventdata->userto = $recipient; $eventdata->subject = get_string('emailconfirmsubject', 'quiz', $a); if ($studentisonline) { $eventdata->fullmessage = get_string('emailconfirmbody', 'quiz', $a); } else { $eventdata->fullmessage = get_string('emailconfirmbodyautosubmit', 'quiz', $a); } $eventdata->fullmessageformat = FORMAT_PLAIN; $eventdata->fullmessagehtml = ''; $eventdata->smallmessage = get_string('emailconfirmsmall', 'quiz', $a); $eventdata->contexturl = $a->quizurl; $eventdata->contexturlname = $a->quizname; $eventdata->customdata = [ 'cmid' => $a->quizcmid, 'instance' => $a->quizid, 'attemptid' => $a->attemptid, ]; // ... and send it. return message_send($eventdata); } /** * Sends notification messages to the interested parties that assign the role capability * * @param stdClass $recipient user object of the intended recipient * @param stdClass $submitter user object for the user who submitted the attempt. * @param stdClass $a associative array of replaceable fields for the templates * * @return int|false as for {@link message_send()}. */ function quiz_send_notification($recipient, $submitter, $a) { global $PAGE; // Recipient info for template. $a->useridnumber = $recipient->idnumber; $a->username = fullname($recipient); $a->userusername = $recipient->username; // Prepare the message. $eventdata = new \core\message\message(); $eventdata->courseid = $a->courseid; $eventdata->component = 'mod_quiz'; $eventdata->name = 'submission'; $eventdata->notification = 1; $eventdata->userfrom = $submitter; $eventdata->userto = $recipient; $eventdata->subject = get_string('emailnotifysubject', 'quiz', $a); $eventdata->fullmessage = get_string('emailnotifybody', 'quiz', $a); $eventdata->fullmessageformat = FORMAT_PLAIN; $eventdata->fullmessagehtml = ''; $eventdata->smallmessage = get_string('emailnotifysmall', 'quiz', $a); $eventdata->contexturl = $a->quizreviewurl; $eventdata->contexturlname = $a->quizname; $userpicture = new user_picture($submitter); $userpicture->size = 1; // Use f1 size. $userpicture->includetoken = $recipient->id; // Generate an out-of-session token for the user receiving the message. $eventdata->customdata = [ 'cmid' => $a->quizcmid, 'instance' => $a->quizid, 'attemptid' => $a->attemptid, 'notificationiconurl' => $userpicture->get_url($PAGE)->out(false), ]; // ... and send it. return message_send($eventdata); } /** * Send all the requried messages when a quiz attempt is submitted. * * @param stdClass $course the course * @param stdClass $quiz the quiz * @param stdClass $attempt this attempt just finished * @param stdClass $context the quiz context * @param stdClass $cm the coursemodule for this quiz * @param bool $studentisonline is the student currently interacting with Moodle? * * @return bool true if all necessary messages were sent successfully, else false. */ function quiz_send_notification_messages($course, $quiz, $attempt, $context, $cm, $studentisonline) { global $CFG, $DB; // Do nothing if required objects not present. if (empty($course) or empty($quiz) or empty($attempt) or empty($context)) { throw new coding_exception('$course, $quiz, $attempt, $context and $cm must all be set.'); } $submitter = $DB->get_record('user', ['id' => $attempt->userid], '*', MUST_EXIST); // Check for confirmation required. $sendconfirm = false; $notifyexcludeusers = ''; if (has_capability('mod/quiz:emailconfirmsubmission', $context, $submitter, false)) { $notifyexcludeusers = $submitter->id; $sendconfirm = true; } // Check for notifications required. $notifyfields = 'u.id, u.username, u.idnumber, u.email, u.emailstop, u.lang, u.timezone, u.mailformat, u.maildisplay, u.auth, u.suspended, u.deleted, '; $userfieldsapi = \core_user\fields::for_name(); $notifyfields .= $userfieldsapi->get_sql('u', false, '', '', false)->selects; $groups = groups_get_all_groups($course->id, $submitter->id, $cm->groupingid); if (is_array($groups) && count($groups) > 0) { $groups = array_keys($groups); } else if (groups_get_activity_groupmode($cm, $course) != NOGROUPS) { // If the user is not in a group, and the quiz is set to group mode, // then set $groups to a non-existant id so that only users with // 'moodle/site:accessallgroups' get notified. $groups = -1; } else { $groups = ''; } $userstonotify = get_users_by_capability($context, 'mod/quiz:emailnotifysubmission', $notifyfields, '', '', '', $groups, $notifyexcludeusers, false, false, true); if (empty($userstonotify) && !$sendconfirm) { return true; // Nothing to do. } $a = new stdClass(); // Course info. $a->courseid = $course->id; $a->coursename = format_string($course->fullname, true, ['context' => $context]); $a->courseshortname = format_string($course->shortname, true, ['context' => $context]); // Quiz info. $a->quizname = format_string($quiz->name, true, ['context' => $context]); $a->quizreporturl = $CFG->wwwroot . '/mod/quiz/report.php?id=' . $cm->id; $a->quizreportlink = '' . $a->quizname . ' report'; $a->quizurl = $CFG->wwwroot . '/mod/quiz/view.php?id=' . $cm->id; $a->quizlink = '' . $a->quizname . ''; $a->quizid = $quiz->id; $a->quizcmid = $cm->id; // Attempt info. $a->submissiontime = userdate($attempt->timefinish); $a->timetaken = format_time($attempt->timefinish - $attempt->timestart); $a->quizreviewurl = $CFG->wwwroot . '/mod/quiz/review.php?attempt=' . $attempt->id; $a->quizreviewlink = '' . $a->quizname . ' review'; $a->attemptid = $attempt->id; // Student who sat the quiz info. $a->studentidnumber = $submitter->idnumber; $a->studentname = fullname($submitter); $a->studentusername = $submitter->username; $allok = true; // Send notifications if required. if (!empty($userstonotify)) { foreach ($userstonotify as $recipient) { $allok = $allok && quiz_send_notification($recipient, $submitter, $a); } } // Send confirmation if required. We send the student confirmation last, so // that if message sending is being intermittently buggy, which means we send // some but not all messages, and then try again later, then teachers may get // duplicate messages, but the student will always get exactly one. if ($sendconfirm) { $allok = $allok && quiz_send_confirmation($submitter, $a, $studentisonline); } return $allok; } /** * Send the notification message when a quiz attempt becomes overdue. * * @param quiz_attempt $attemptobj all the data about the quiz attempt. */ function quiz_send_overdue_message($attemptobj) { global $CFG, $DB; $submitter = $DB->get_record('user', ['id' => $attemptobj->get_userid()], '*', MUST_EXIST); if (!$attemptobj->has_capability('mod/quiz:emailwarnoverdue', $submitter->id, false)) { return; // Message not required. } if (!$attemptobj->has_response_to_at_least_one_graded_question()) { return; // Message not required. } // Prepare lots of useful information that admins might want to include in // the email message. $quizname = format_string($attemptobj->get_quiz_name()); $deadlines = []; if ($attemptobj->get_quiz()->timelimit) { $deadlines[] = $attemptobj->get_attempt()->timestart + $attemptobj->get_quiz()->timelimit; } if ($attemptobj->get_quiz()->timeclose) { $deadlines[] = $attemptobj->get_quiz()->timeclose; } $duedate = min($deadlines); $graceend = $duedate + $attemptobj->get_quiz()->graceperiod; $a = new stdClass(); // Course info. $a->courseid = $attemptobj->get_course()->id; $a->coursename = format_string($attemptobj->get_course()->fullname); $a->courseshortname = format_string($attemptobj->get_course()->shortname); // Quiz info. $a->quizname = $quizname; $a->quizurl = $attemptobj->view_url()->out(false); $a->quizlink = '' . $quizname . ''; // Attempt info. $a->attemptduedate = userdate($duedate); $a->attemptgraceend = userdate($graceend); $a->attemptsummaryurl = $attemptobj->summary_url()->out(false); $a->attemptsummarylink = '' . $quizname . ' review'; // Student's info. $a->studentidnumber = $submitter->idnumber; $a->studentname = fullname($submitter); $a->studentusername = $submitter->username; // Prepare the message. $eventdata = new \core\message\message(); $eventdata->courseid = $a->courseid; $eventdata->component = 'mod_quiz'; $eventdata->name = 'attempt_overdue'; $eventdata->notification = 1; $eventdata->userfrom = core_user::get_noreply_user(); $eventdata->userto = $submitter; $eventdata->subject = get_string('emailoverduesubject', 'quiz', $a); $eventdata->fullmessage = get_string('emailoverduebody', 'quiz', $a); $eventdata->fullmessageformat = FORMAT_PLAIN; $eventdata->fullmessagehtml = ''; $eventdata->smallmessage = get_string('emailoverduesmall', 'quiz', $a); $eventdata->contexturl = $a->quizurl; $eventdata->contexturlname = $a->quizname; $eventdata->customdata = [ 'cmid' => $attemptobj->get_cmid(), 'instance' => $attemptobj->get_quizid(), 'attemptid' => $attemptobj->get_attemptid(), ]; // Send the message. return message_send($eventdata); } /** * Handle the quiz_attempt_submitted event. * * This sends the confirmation and notification messages, if required. * * @param attempt_submitted $event the event object. */ function quiz_attempt_submitted_handler($event) { $course = get_course($event->courseid); $attempt = $event->get_record_snapshot('quiz_attempts', $event->objectid); $quiz = $event->get_record_snapshot('quiz', $attempt->quiz); $cm = get_coursemodule_from_id('quiz', $event->get_context()->instanceid, $event->courseid); $eventdata = $event->get_data(); if (!($course && $quiz && $cm && $attempt)) { // Something has been deleted since the event was raised. Therefore, the // event is no longer relevant. return true; } // Update completion state. $completion = new completion_info($course); if ($completion->is_enabled($cm) && ($quiz->completionattemptsexhausted || $quiz->completionminattempts)) { $completion->update_state($cm, COMPLETION_COMPLETE, $event->userid); } return quiz_send_notification_messages($course, $quiz, $attempt, context_module::instance($cm->id), $cm, $eventdata['other']['studentisonline']); } /** * Send the notification message when a quiz attempt has been manual graded. * * @param quiz_attempt $attemptobj Some data about the quiz attempt. * @param stdClass $userto * @return int|false As for message_send. */ function quiz_send_notify_manual_graded_message(quiz_attempt $attemptobj, object $userto): ?int { global $CFG; $quizname = format_string($attemptobj->get_quiz_name()); $a = new stdClass(); // Course info. $a->courseid = $attemptobj->get_courseid(); $a->coursename = format_string($attemptobj->get_course()->fullname); // Quiz info. $a->quizname = $quizname; $a->quizurl = $CFG->wwwroot . '/mod/quiz/view.php?id=' . $attemptobj->get_cmid(); // Attempt info. $a->attempttimefinish = userdate($attemptobj->get_attempt()->timefinish); // Student's info. $a->studentidnumber = $userto->idnumber; $a->studentname = fullname($userto); $eventdata = new \core\message\message(); $eventdata->component = 'mod_quiz'; $eventdata->name = 'attempt_grading_complete'; $eventdata->userfrom = core_user::get_noreply_user(); $eventdata->userto = $userto; $eventdata->subject = get_string('emailmanualgradedsubject', 'quiz', $a); $eventdata->fullmessage = get_string('emailmanualgradedbody', 'quiz', $a); $eventdata->fullmessageformat = FORMAT_PLAIN; $eventdata->fullmessagehtml = ''; $eventdata->notification = 1; $eventdata->contexturl = $a->quizurl; $eventdata->contexturlname = $a->quizname; // Send the message. return message_send($eventdata); } /** * Logic to happen when a/some group(s) has/have been deleted in a course. * * @param int $courseid The course ID. * @return void */ function quiz_process_group_deleted_in_course($courseid) { $affectedquizzes = override_manager::delete_orphaned_group_overrides_in_course($courseid); if (!empty($affectedquizzes)) { quiz_update_open_attempts(['quizid' => $affectedquizzes]); } } /** * Get the information about the standard quiz JavaScript module. * @return array a standard jsmodule structure. */ function quiz_get_js_module() { global $PAGE; return [ 'name' => 'mod_quiz', 'fullpath' => '/mod/quiz/module.js', 'requires' => ['base', 'dom', 'event-delegate', 'event-key', 'core_question_engine'], 'strings' => [ ['cancel', 'moodle'], ['flagged', 'question'], ['functiondisabledbysecuremode', 'quiz'], ['startattempt', 'quiz'], ['timesup', 'quiz'], ['show', 'moodle'], ['hide', 'moodle'], ], ]; } /** * Creates a textual representation of a question for display. * * @param stdClass $question A question object from the database questions table * @param bool $showicon If true, show the question's icon with the question. False by default. * @param bool $showquestiontext If true (default), show question text after question name. * If false, show only question name. * @param bool $showidnumber If true, show the question's idnumber, if any. False by default. * @param core_tag_tag[]|bool $showtags if array passed, show those tags. Else, if true, get and show tags, * else, don't show tags (which is the default). * @param bool $displaytaglink Indicates whether the tag should be displayed as a link. * @return string HTML fragment. */ function quiz_question_tostring($question, $showicon = false, $showquestiontext = true, $showidnumber = false, $showtags = false, $displaytaglink = true) { global $OUTPUT; $result = ''; // Question name. $name = shorten_text(format_string($question->name), 200); if ($showicon) { $name .= print_question_icon($question) . ' ' . $name; } $result .= html_writer::span($name, 'questionname'); // Question idnumber. if ($showidnumber && $question->idnumber !== null && $question->idnumber !== '') { $result .= ' ' . html_writer::span( html_writer::span(get_string('idnumber', 'question'), 'accesshide') . ' ' . s($question->idnumber), 'badge bg-primary text-white'); } // Question tags. if (is_array($showtags)) { $tags = $showtags; } else if ($showtags) { $tags = core_tag_tag::get_item_tags('core_question', 'question', $question->id); } else { $tags = []; } if ($tags) { $result .= $OUTPUT->tag_list($tags, null, 'd-inline', 0, null, true, $displaytaglink); } // Question text. if ($showquestiontext) { $questiontext = question_utils::to_plain_text($question->questiontext, $question->questiontextformat, ['noclean' => true, 'para' => false, 'filter' => false]); $questiontext = shorten_text($questiontext, 50); if ($questiontext) { $result .= ' ' . html_writer::span(s($questiontext), 'questiontext'); } } return $result; } /** * Verify that the question exists, and the user has permission to use it. * Does not return. Throws an exception if the question cannot be used. * @param int $questionid The id of the question. */ function quiz_require_question_use($questionid) { global $DB; $question = $DB->get_record('question', ['id' => $questionid], '*', MUST_EXIST); question_require_capability_on($question, 'use'); } /** * Add a question to a quiz * * Adds a question to a quiz by updating $quiz as well as the * quiz and quiz_slots tables. It also adds a page break if required. * @param int $questionid The id of the question to be added * @param stdClass $quiz The extended quiz object as used by edit.php * This is updated by this function * @param int $page Which page in quiz to add the question on. If 0 (default), * add at the end * @param float $maxmark The maximum mark to set for this question. (Optional, * defaults to question.defaultmark. * @return bool false if the question was already in the quiz */ function quiz_add_quiz_question($questionid, $quiz, $page = 0, $maxmark = null) { global $DB; if (!isset($quiz->cmid)) { $cm = get_coursemodule_from_instance('quiz', $quiz->id, $quiz->course); $quiz->cmid = $cm->id; } // Make sue the question is not of the "random" type. $questiontype = $DB->get_field('question', 'qtype', ['id' => $questionid]); if ($questiontype == 'random') { throw new coding_exception( 'Adding "random" questions via quiz_add_quiz_question() is deprecated. Please use quiz_add_random_questions().' ); } // If the question type is invalid, we cannot add it to the quiz. It shouldn't be possible to get to this // point without fiddling with the DOM so we can just throw an exception. if (!\question_bank::is_qtype_installed($questiontype)) { throw new coding_exception('Invalid question type: ' . $questiontype); } $trans = $DB->start_delegated_transaction(); $sql = "SELECT qbe.id FROM {quiz_slots} slot JOIN {question_references} qr ON qr.itemid = slot.id JOIN {question_bank_entries} qbe ON qbe.id = qr.questionbankentryid WHERE slot.quizid = ? AND qr.component = ? AND qr.questionarea = ? AND qr.usingcontextid = ?"; $questionslots = $DB->get_records_sql($sql, [$quiz->id, 'mod_quiz', 'slot', context_module::instance($quiz->cmid)->id]); $currententry = get_question_bank_entry($questionid); if (array_key_exists($currententry->id, $questionslots)) { $trans->allow_commit(); return false; } $sql = "SELECT slot.slot, slot.page, slot.id FROM {quiz_slots} slot WHERE slot.quizid = ? ORDER BY slot.slot"; $slots = $DB->get_records_sql($sql, [$quiz->id]); $maxpage = 1; $numonlastpage = 0; foreach ($slots as $slot) { if ($slot->page > $maxpage) { $maxpage = $slot->page; $numonlastpage = 1; } else { $numonlastpage += 1; } } // Add the new instance. $slot = new stdClass(); $slot->quizid = $quiz->id; if ($maxmark !== null) { $slot->maxmark = $maxmark; } else { $slot->maxmark = $DB->get_field('question', 'defaultmark', ['id' => $questionid]); } if (is_int($page) && $page >= 1) { // Adding on a given page. $lastslotbefore = 0; foreach (array_reverse($slots) as $otherslot) { if ($otherslot->page > $page) { $DB->set_field('quiz_slots', 'slot', $otherslot->slot + 1, ['id' => $otherslot->id]); } else { $lastslotbefore = $otherslot->slot; break; } } $slot->slot = $lastslotbefore + 1; $slot->page = min($page, $maxpage + 1); quiz_update_section_firstslots($quiz->id, 1, max($lastslotbefore, 1)); } else { $lastslot = end($slots); if ($lastslot) { $slot->slot = $lastslot->slot + 1; } else { $slot->slot = 1; } if ($quiz->questionsperpage && $numonlastpage >= $quiz->questionsperpage) { $slot->page = $maxpage + 1; } else { $slot->page = $maxpage; } } $slotid = $DB->insert_record('quiz_slots', $slot); // Update or insert record in question_reference table. $sql = "SELECT DISTINCT qr.id, qr.itemid FROM {question} q JOIN {question_versions} qv ON q.id = qv.questionid JOIN {question_bank_entries} qbe ON qbe.id = qv.questionbankentryid JOIN {question_references} qr ON qbe.id = qr.questionbankentryid AND qr.version = qv.version JOIN {quiz_slots} qs ON qs.id = qr.itemid WHERE q.id = ? AND qs.id = ? AND qr.component = ? AND qr.questionarea = ?"; $qreferenceitem = $DB->get_record_sql($sql, [$questionid, $slotid, 'mod_quiz', 'slot']); if (!$qreferenceitem) { // Create a new reference record for questions created already. $questionreferences = new stdClass(); $questionreferences->usingcontextid = context_module::instance($quiz->cmid)->id; $questionreferences->component = 'mod_quiz'; $questionreferences->questionarea = 'slot'; $questionreferences->itemid = $slotid; $questionreferences->questionbankentryid = get_question_bank_entry($questionid)->id; $questionreferences->version = null; // Always latest. $DB->insert_record('question_references', $questionreferences); } else if ($qreferenceitem->itemid === 0 || $qreferenceitem->itemid === null) { $questionreferences = new stdClass(); $questionreferences->id = $qreferenceitem->id; $questionreferences->itemid = $slotid; $DB->update_record('question_references', $questionreferences); } else { // If the reference record exits for another quiz. $questionreferences = new stdClass(); $questionreferences->usingcontextid = context_module::instance($quiz->cmid)->id; $questionreferences->component = 'mod_quiz'; $questionreferences->questionarea = 'slot'; $questionreferences->itemid = $slotid; $questionreferences->questionbankentryid = get_question_bank_entry($questionid)->id; $questionreferences->version = null; // Always latest. $DB->insert_record('question_references', $questionreferences); } $trans->allow_commit(); // Log slot created event. $cm = get_coursemodule_from_instance('quiz', $quiz->id); $event = \mod_quiz\event\slot_created::create([ 'context' => context_module::instance($cm->id), 'objectid' => $slotid, 'other' => [ 'quizid' => $quiz->id, 'slotnumber' => $slot->slot, 'page' => $slot->page ] ]); $event->trigger(); } /** * Move all the section headings in a certain slot range by a certain offset. * * @param int $quizid the id of a quiz * @param int $direction amount to adjust section heading positions. Normally +1 or -1. * @param int $afterslot adjust headings that start after this slot. * @param int|null $beforeslot optionally, only adjust headings before this slot. */ function quiz_update_section_firstslots($quizid, $direction, $afterslot, $beforeslot = null) { global $DB; $where = 'quizid = ? AND firstslot > ?'; $params = [$direction, $quizid, $afterslot]; if ($beforeslot) { $where .= ' AND firstslot < ?'; $params[] = $beforeslot; } $firstslotschanges = $DB->get_records_select_menu('quiz_sections', $where, $params, '', 'firstslot, firstslot + ?'); update_field_with_unique_index('quiz_sections', 'firstslot', $firstslotschanges, ['quizid' => $quizid]); } /** * Add a random question to the quiz at a given point. * @param stdClass $quiz the quiz settings. * @param int $addonpage the page on which to add the question. * @param int $categoryid the question category to add the question from. * @param int $number the number of random questions to add. * @deprecated Since Moodle 4.3 MDL-72321 * @todo Final deprecation in Moodle 4.7 MDL-78091 */ function quiz_add_random_questions(stdClass $quiz, int $addonpage, int $categoryid, int $number): void { debugging( 'quiz_add_random_questions is deprecated. Please use mod_quiz\structure::add_random_questions() instead.', DEBUG_DEVELOPER ); $settings = quiz_settings::create($quiz->id); $structure = structure::create_for_quiz($settings); $structure->add_random_questions($addonpage, $number, [ 'filter' => [ 'category' => [ 'jointype' => condition::JOINTYPE_DEFAULT, 'values' => [$categoryid], 'filteroptions' => ['includesubcategories' => false], ], ], ]); } /** * Mark the activity completed (if required) and trigger the course_module_viewed event. * * @param stdClass $quiz quiz object * @param stdClass $course course object * @param stdClass $cm course module object * @param stdClass $context context object * @since Moodle 3.1 */ function quiz_view($quiz, $course, $cm, $context) { $params = [ 'objectid' => $quiz->id, 'context' => $context ]; $event = \mod_quiz\event\course_module_viewed::create($params); $event->add_record_snapshot('quiz', $quiz); $event->trigger(); // Completion. $completion = new completion_info($course); $completion->set_module_viewed($cm); } /** * Validate permissions for creating a new attempt and start a new preview attempt if required. * * @param quiz_settings $quizobj quiz object * @param access_manager $accessmanager quiz access manager * @param bool $forcenew whether was required to start a new preview attempt * @param int $page page to jump to in the attempt * @param bool $redirect whether to redirect or throw exceptions (for web or ws usage) * @return array an array containing the attempt information, access error messages and the page to jump to in the attempt * @since Moodle 3.1 */ function quiz_validate_new_attempt(quiz_settings $quizobj, access_manager $accessmanager, $forcenew, $page, $redirect) { global $DB, $USER; $timenow = time(); if ($quizobj->is_preview_user() && $forcenew) { $accessmanager->current_attempt_finished(); } // Check capabilities. if (!$quizobj->is_preview_user()) { $quizobj->require_capability('mod/quiz:attempt'); } // Check to see if a new preview was requested. if ($quizobj->is_preview_user() && $forcenew) { // To force the creation of a new preview, we mark the current attempt (if any) // as abandoned. It will then automatically be deleted below. $DB->set_field('quiz_attempts', 'state', quiz_attempt::ABANDONED, ['quiz' => $quizobj->get_quizid(), 'userid' => $USER->id]); } // Look for an existing attempt. $attempts = quiz_get_user_attempts($quizobj->get_quizid(), $USER->id, 'all', true); $lastattempt = end($attempts); $attemptnumber = null; if ( $lastattempt && in_array($lastattempt->state, [quiz_attempt::NOT_STARTED, quiz_attempt::IN_PROGRESS, quiz_attempt::OVERDUE]) ) { // If an in-progress or not-started attempt exists, check password then redirect to it. $currentattemptid = $lastattempt->id; $messages = $accessmanager->prevent_access(); // If the attempt is now overdue, deal with that. $quizobj->create_attempt_object($lastattempt)->handle_if_time_expired($timenow, true); // And, if the attempt is now no longer in progress, redirect to the appropriate place. if ($lastattempt->state == quiz_attempt::ABANDONED || $lastattempt->state == quiz_attempt::FINISHED) { if ($redirect) { redirect($quizobj->review_url($lastattempt->id)); } else { throw new moodle_exception('attemptalreadyclosed', 'quiz', $quizobj->view_url()); } } // If the page number was not explicitly in the URL, go to the current page. if ($page == -1) { $page = $lastattempt->currentpage; } } else { while ($lastattempt && $lastattempt->preview) { $lastattempt = array_pop($attempts); } // Get number for the next or unfinished attempt. if ($lastattempt) { $attemptnumber = $lastattempt->attempt + 1; } else { $lastattempt = false; $attemptnumber = 1; } $currentattemptid = null; $messages = $accessmanager->prevent_access() + $accessmanager->prevent_new_attempt(count($attempts), $lastattempt); if ($page == -1) { $page = 0; } } return [$currentattemptid, $attemptnumber, $lastattempt, $messages, $page]; } /** * Prepare and start a new attempt deleting the previous preview attempts. * * @param quiz_settings $quizobj quiz object * @param int $attemptnumber the attempt number * @param stdClass $lastattempt last attempt object * @param bool $offlineattempt whether is an offline attempt or not * @param array $forcedrandomquestions slot number => question id. Used for random questions, * to force the choice of a particular actual question. Intended for testing purposes only. * @param array $forcedvariants slot number => variant. Used for questions with variants, * to force the choice of a particular variant. Intended for testing purposes only. * @param int $userid Specific user id to create an attempt for that user, null for current logged in user * @return stdClass the new attempt * @since Moodle 3.1 */ function quiz_prepare_and_start_new_attempt(quiz_settings $quizobj, $attemptnumber, $lastattempt, $offlineattempt = false, $forcedrandomquestions = [], $forcedvariants = [], $userid = null) { global $DB, $USER; if ($userid === null) { $userid = $USER->id; $ispreviewuser = $quizobj->is_preview_user(); } else { $ispreviewuser = has_capability('mod/quiz:preview', $quizobj->get_context(), $userid); } // Delete any previous preview attempts belonging to this user. quiz_delete_previews($quizobj->get_quiz(), $userid); $quba = question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context()); $quba->set_preferred_behaviour($quizobj->get_quiz()->preferredbehaviour); $attempt = $DB->get_record( 'quiz_attempts', [ 'quiz' => $quizobj->get_quizid(), 'userid' => $userid, 'preview' => 0, 'state' => quiz_attempt::NOT_STARTED, ], ); if (!$attempt) { // Create the new attempt and initialize the question sessions. $timenow = time(); // Update time now, in case the server is running really slowly. $attempt = quiz_create_attempt($quizobj, $attemptnumber, $lastattempt, $timenow, $ispreviewuser, $userid); if (!($quizobj->get_quiz()->attemptonlast && $lastattempt)) { $attempt = quiz_start_new_attempt( $quizobj, $quba, $attempt, $attemptnumber, $timenow, $forcedrandomquestions, $forcedvariants, ); } else { $attempt = quiz_start_attempt_built_on_last($quba, $attempt, $lastattempt); } } $transaction = $DB->start_delegated_transaction(); // Init the timemodifiedoffline for offline attempts. if ($offlineattempt) { $attempt->timemodifiedoffline = $attempt->timemodified; } $attempt = quiz_attempt_save_started($quizobj, $quba, $attempt); $transaction->allow_commit(); return $attempt; } /** * Check if the given calendar_event is either a user or group override * event for quiz. * * @param calendar_event $event The calendar event to check * @return bool */ function quiz_is_overriden_calendar_event(\calendar_event $event) { global $DB; if (!isset($event->modulename)) { return false; } if ($event->modulename != 'quiz') { return false; } if (!isset($event->instance)) { return false; } if (!isset($event->userid) && !isset($event->groupid)) { return false; } $overrideparams = [ 'quiz' => $event->instance ]; if (isset($event->groupid)) { $overrideparams['groupid'] = $event->groupid; } else if (isset($event->userid)) { $overrideparams['userid'] = $event->userid; } return $DB->record_exists('quiz_overrides', $overrideparams); } /** * Get quiz attempt and handling error. * * @param int $attemptid the id of the current attempt. * @param int|null $cmid the course_module id for this quiz. * @return quiz_attempt all the data about the quiz attempt. */ function quiz_create_attempt_handling_errors($attemptid, $cmid = null) { try { $attempobj = quiz_attempt::create($attemptid); } catch (moodle_exception $e) { if (!empty($cmid)) { list($course, $cm) = get_course_and_cm_from_cmid($cmid, 'quiz'); $continuelink = new moodle_url('/mod/quiz/view.php', ['id' => $cmid]); $context = context_module::instance($cm->id); if (has_capability('mod/quiz:preview', $context)) { throw new moodle_exception('attempterrorcontentchange', 'quiz', $continuelink); } else { throw new moodle_exception('attempterrorcontentchangeforuser', 'quiz', $continuelink); } } else { throw new moodle_exception('attempterrorinvalid', 'quiz'); } } if (!empty($cmid) && $attempobj->get_cmid() != $cmid) { throw new moodle_exception('invalidcoursemodule'); } else { return $attempobj; } } home/smkb8269/public_html/lms.smk-almanshuriyah.sch.id/mod/lti/locallib.php000064400000476500152267123330022542 0ustar00. // // This file is part of BasicLTI4Moodle // // BasicLTI4Moodle is an IMS BasicLTI (Basic Learning Tools for Interoperability) // consumer for Moodle 1.9 and Moodle 2.0. BasicLTI is a IMS Standard that allows web // based learning tools to be easily integrated in LMS as native ones. The IMS BasicLTI // specification is part of the IMS standard Common Cartridge 1.1 Sakai and other main LMS // are already supporting or going to support BasicLTI. This project Implements the consumer // for Moodle. Moodle is a Free Open source Learning Management System by Martin Dougiamas. // BasicLTI4Moodle is a project iniciated and leaded by Ludo(Marc Alier) and Jordi Piguillem // at the GESSI research group at UPC. // SimpleLTI consumer for Moodle is an implementation of the early specification of LTI // by Charles Severance (Dr Chuck) htp://dr-chuck.com , developed by Jordi Piguillem in a // Google Summer of Code 2008 project co-mentored by Charles Severance and Marc Alier. // // BasicLTI4Moodle is copyright 2009 by Marc Alier Forment, Jordi Piguillem and Nikolas Galanis // of the Universitat Politecnica de Catalunya http://www.upc.edu // Contact info: Marc Alier Forment granludo @ gmail.com or marc.alier @ upc.edu. /** * This file contains the library of functions and constants for the lti module * * @package mod_lti * @copyright 2009 Marc Alier, Jordi Piguillem, Nikolas Galanis * marc.alier@upc.edu * @copyright 2009 Universitat Politecnica de Catalunya http://www.upc.edu * @author Marc Alier * @author Jordi Piguillem * @author Nikolas Galanis * @author Chris Scribner * @copyright 2015 Vital Source Technologies http://vitalsource.com * @author Stephen Vickers * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die; // TODO: Switch to core oauthlib once implemented - MDL-30149. use mod_lti\helper; use moodle\mod\lti as lti; use Firebase\JWT\JWT; use Firebase\JWT\JWK; use Firebase\JWT\Key; use mod_lti\local\ltiopenid\jwks_helper; use mod_lti\local\ltiopenid\registration_helper; global $CFG; require_once($CFG->dirroot.'/mod/lti/OAuth.php'); require_once($CFG->libdir.'/weblib.php'); require_once($CFG->dirroot . '/course/modlib.php'); require_once($CFG->dirroot . '/mod/lti/TrivialStore.php'); define('LTI_URL_DOMAIN_REGEX', '/(?:https?:\/\/)?(?:www\.)?([^\/]+)(?:\/|$)/i'); define('LTI_LAUNCH_CONTAINER_DEFAULT', 1); define('LTI_LAUNCH_CONTAINER_EMBED', 2); define('LTI_LAUNCH_CONTAINER_EMBED_NO_BLOCKS', 3); define('LTI_LAUNCH_CONTAINER_WINDOW', 4); define('LTI_LAUNCH_CONTAINER_REPLACE_MOODLE_WINDOW', 5); define('LTI_TOOL_STATE_ANY', 0); define('LTI_TOOL_STATE_CONFIGURED', 1); define('LTI_TOOL_STATE_PENDING', 2); define('LTI_TOOL_STATE_REJECTED', 3); define('LTI_TOOL_PROXY_TAB', 4); define('LTI_TOOL_PROXY_STATE_CONFIGURED', 1); define('LTI_TOOL_PROXY_STATE_PENDING', 2); define('LTI_TOOL_PROXY_STATE_ACCEPTED', 3); define('LTI_TOOL_PROXY_STATE_REJECTED', 4); define('LTI_SETTING_NEVER', 0); define('LTI_SETTING_ALWAYS', 1); define('LTI_SETTING_DELEGATE', 2); define('LTI_COURSEVISIBLE_NO', 0); define('LTI_COURSEVISIBLE_PRECONFIGURED', 1); define('LTI_COURSEVISIBLE_ACTIVITYCHOOSER', 2); define('LTI_VERSION_1', 'LTI-1p0'); define('LTI_VERSION_2', 'LTI-2p0'); define('LTI_VERSION_1P3', '1.3.0'); define('LTI_RSA_KEY', 'RSA_KEY'); define('LTI_JWK_KEYSET', 'JWK_KEYSET'); define('LTI_DEFAULT_ORGID_SITEID', 'SITEID'); define('LTI_DEFAULT_ORGID_SITEHOST', 'SITEHOST'); define('LTI_ACCESS_TOKEN_LIFE', 3600); // Standard prefix for JWT claims. define('LTI_JWT_CLAIM_PREFIX', 'https://purl.imsglobal.org/spec/lti'); /** * Return the mapping for standard message types to JWT message_type claim. * * @return array */ function lti_get_jwt_message_type_mapping() { return array( 'basic-lti-launch-request' => 'LtiResourceLinkRequest', 'ContentItemSelectionRequest' => 'LtiDeepLinkingRequest', 'LtiDeepLinkingResponse' => 'ContentItemSelection', 'LtiSubmissionReviewRequest' => 'LtiSubmissionReviewRequest', ); } /** * Return the mapping for standard message parameters to JWT claim. * * @return array */ function lti_get_jwt_claim_mapping() { $mapping = []; $services = lti_get_services(); foreach ($services as $service) { $mapping = array_merge($mapping, $service->get_jwt_claim_mappings()); } $mapping = array_merge($mapping, [ 'accept_copy_advice' => [ 'suffix' => 'dl', 'group' => 'deep_linking_settings', 'claim' => 'accept_copy_advice', 'isarray' => false, 'type' => 'boolean' ], 'accept_media_types' => [ 'suffix' => 'dl', 'group' => 'deep_linking_settings', 'claim' => 'accept_media_types', 'isarray' => true ], 'accept_multiple' => [ 'suffix' => 'dl', 'group' => 'deep_linking_settings', 'claim' => 'accept_multiple', 'isarray' => false, 'type' => 'boolean' ], 'accept_presentation_document_targets' => [ 'suffix' => 'dl', 'group' => 'deep_linking_settings', 'claim' => 'accept_presentation_document_targets', 'isarray' => true ], 'accept_types' => [ 'suffix' => 'dl', 'group' => 'deep_linking_settings', 'claim' => 'accept_types', 'isarray' => true ], 'accept_unsigned' => [ 'suffix' => 'dl', 'group' => 'deep_linking_settings', 'claim' => 'accept_unsigned', 'isarray' => false, 'type' => 'boolean' ], 'auto_create' => [ 'suffix' => 'dl', 'group' => 'deep_linking_settings', 'claim' => 'auto_create', 'isarray' => false, 'type' => 'boolean' ], 'can_confirm' => [ 'suffix' => 'dl', 'group' => 'deep_linking_settings', 'claim' => 'can_confirm', 'isarray' => false, 'type' => 'boolean' ], 'content_item_return_url' => [ 'suffix' => 'dl', 'group' => 'deep_linking_settings', 'claim' => 'deep_link_return_url', 'isarray' => false ], 'content_items' => [ 'suffix' => 'dl', 'group' => '', 'claim' => 'content_items', 'isarray' => true ], 'data' => [ 'suffix' => 'dl', 'group' => 'deep_linking_settings', 'claim' => 'data', 'isarray' => false ], 'text' => [ 'suffix' => 'dl', 'group' => 'deep_linking_settings', 'claim' => 'text', 'isarray' => false ], 'title' => [ 'suffix' => 'dl', 'group' => 'deep_linking_settings', 'claim' => 'title', 'isarray' => false ], 'lti_msg' => [ 'suffix' => 'dl', 'group' => '', 'claim' => 'msg', 'isarray' => false ], 'lti_log' => [ 'suffix' => 'dl', 'group' => '', 'claim' => 'log', 'isarray' => false ], 'lti_errormsg' => [ 'suffix' => 'dl', 'group' => '', 'claim' => 'errormsg', 'isarray' => false ], 'lti_errorlog' => [ 'suffix' => 'dl', 'group' => '', 'claim' => 'errorlog', 'isarray' => false ], 'context_id' => [ 'suffix' => '', 'group' => 'context', 'claim' => 'id', 'isarray' => false ], 'context_label' => [ 'suffix' => '', 'group' => 'context', 'claim' => 'label', 'isarray' => false ], 'context_title' => [ 'suffix' => '', 'group' => 'context', 'claim' => 'title', 'isarray' => false ], 'context_type' => [ 'suffix' => '', 'group' => 'context', 'claim' => 'type', 'isarray' => true ], 'for_user_id' => [ 'suffix' => '', 'group' => 'for_user', 'claim' => 'user_id', 'isarray' => false ], 'lis_course_offering_sourcedid' => [ 'suffix' => '', 'group' => 'lis', 'claim' => 'course_offering_sourcedid', 'isarray' => false ], 'lis_course_section_sourcedid' => [ 'suffix' => '', 'group' => 'lis', 'claim' => 'course_section_sourcedid', 'isarray' => false ], 'launch_presentation_css_url' => [ 'suffix' => '', 'group' => 'launch_presentation', 'claim' => 'css_url', 'isarray' => false ], 'launch_presentation_document_target' => [ 'suffix' => '', 'group' => 'launch_presentation', 'claim' => 'document_target', 'isarray' => false ], 'launch_presentation_height' => [ 'suffix' => '', 'group' => 'launch_presentation', 'claim' => 'height', 'isarray' => false ], 'launch_presentation_locale' => [ 'suffix' => '', 'group' => 'launch_presentation', 'claim' => 'locale', 'isarray' => false ], 'launch_presentation_return_url' => [ 'suffix' => '', 'group' => 'launch_presentation', 'claim' => 'return_url', 'isarray' => false ], 'launch_presentation_width' => [ 'suffix' => '', 'group' => 'launch_presentation', 'claim' => 'width', 'isarray' => false ], 'lis_person_contact_email_primary' => [ 'suffix' => '', 'group' => null, 'claim' => 'email', 'isarray' => false ], 'lis_person_name_family' => [ 'suffix' => '', 'group' => null, 'claim' => 'family_name', 'isarray' => false ], 'lis_person_name_full' => [ 'suffix' => '', 'group' => null, 'claim' => 'name', 'isarray' => false ], 'lis_person_name_given' => [ 'suffix' => '', 'group' => null, 'claim' => 'given_name', 'isarray' => false ], 'lis_person_sourcedid' => [ 'suffix' => '', 'group' => 'lis', 'claim' => 'person_sourcedid', 'isarray' => false ], 'user_id' => [ 'suffix' => '', 'group' => null, 'claim' => 'sub', 'isarray' => false ], 'user_image' => [ 'suffix' => '', 'group' => null, 'claim' => 'picture', 'isarray' => false ], 'roles' => [ 'suffix' => '', 'group' => '', 'claim' => 'roles', 'isarray' => true ], 'role_scope_mentor' => [ 'suffix' => '', 'group' => '', 'claim' => 'role_scope_mentor', 'isarray' => false ], 'deployment_id' => [ 'suffix' => '', 'group' => '', 'claim' => 'deployment_id', 'isarray' => false ], 'lti_message_type' => [ 'suffix' => '', 'group' => '', 'claim' => 'message_type', 'isarray' => false ], 'lti_version' => [ 'suffix' => '', 'group' => '', 'claim' => 'version', 'isarray' => false ], 'resource_link_description' => [ 'suffix' => '', 'group' => 'resource_link', 'claim' => 'description', 'isarray' => false ], 'resource_link_id' => [ 'suffix' => '', 'group' => 'resource_link', 'claim' => 'id', 'isarray' => false ], 'resource_link_title' => [ 'suffix' => '', 'group' => 'resource_link', 'claim' => 'title', 'isarray' => false ], 'tool_consumer_info_product_family_code' => [ 'suffix' => '', 'group' => 'tool_platform', 'claim' => 'product_family_code', 'isarray' => false ], 'tool_consumer_info_version' => [ 'suffix' => '', 'group' => 'tool_platform', 'claim' => 'version', 'isarray' => false ], 'tool_consumer_instance_contact_email' => [ 'suffix' => '', 'group' => 'tool_platform', 'claim' => 'contact_email', 'isarray' => false ], 'tool_consumer_instance_description' => [ 'suffix' => '', 'group' => 'tool_platform', 'claim' => 'description', 'isarray' => false ], 'tool_consumer_instance_guid' => [ 'suffix' => '', 'group' => 'tool_platform', 'claim' => 'guid', 'isarray' => false ], 'tool_consumer_instance_name' => [ 'suffix' => '', 'group' => 'tool_platform', 'claim' => 'name', 'isarray' => false ], 'tool_consumer_instance_url' => [ 'suffix' => '', 'group' => 'tool_platform', 'claim' => 'url', 'isarray' => false ] ]); return $mapping; } /** * Return the type of the instance, using domain matching if no explicit type is set. * * @param object $instance the external tool activity settings * @return object|null * @since Moodle 3.9 */ function lti_get_instance_type(object $instance): ?object { if (empty($instance->typeid)) { if (!$tool = lti_get_tool_by_url_match($instance->toolurl, $instance->course)) { $tool = lti_get_tool_by_url_match($instance->securetoolurl, $instance->course); } return $tool; } return lti_get_type($instance->typeid); } /** * Return the launch data required for opening the external tool. * * @param stdClass $instance the external tool activity settings * @param string $nonce the nonce value to use (applies to LTI 1.3 only) * @return array the endpoint URL and parameters (including the signature) * @since Moodle 3.0 */ function lti_get_launch_data($instance, $nonce = '', $messagetype = 'basic-lti-launch-request', $foruserid = 0) { global $PAGE, $USER; $messagetype = $messagetype ? $messagetype : 'basic-lti-launch-request'; $tool = lti_get_instance_type($instance); if ($tool) { $typeid = $tool->id; $ltiversion = $tool->ltiversion; } else { $typeid = null; $ltiversion = LTI_VERSION_1; } if ($typeid) { $typeconfig = lti_get_type_config($typeid); } else { // There is no admin configuration for this tool. Use configuration in the lti instance record plus some defaults. $typeconfig = (array)$instance; $typeconfig['sendname'] = $instance->instructorchoicesendname; $typeconfig['sendemailaddr'] = $instance->instructorchoicesendemailaddr; $typeconfig['customparameters'] = $instance->instructorcustomparameters; $typeconfig['acceptgrades'] = $instance->instructorchoiceacceptgrades; $typeconfig['allowroster'] = $instance->instructorchoiceallowroster; $typeconfig['forcessl'] = '0'; } if (isset($tool->toolproxyid)) { $toolproxy = lti_get_tool_proxy($tool->toolproxyid); $key = $toolproxy->guid; $secret = $toolproxy->secret; } else { $toolproxy = null; if (!empty($instance->resourcekey)) { $key = $instance->resourcekey; } else if ($ltiversion === LTI_VERSION_1P3) { $key = $tool->clientid; } else if (!empty($typeconfig['resourcekey'])) { $key = $typeconfig['resourcekey']; } else { $key = ''; } if (!empty($instance->password)) { $secret = $instance->password; } else if (!empty($typeconfig['password'])) { $secret = $typeconfig['password']; } else { $secret = ''; } } $endpoint = !empty($instance->toolurl) ? $instance->toolurl : $typeconfig['toolurl']; $endpoint = trim($endpoint); // If the current request is using SSL and a secure tool URL is specified, use it. if (lti_request_is_using_ssl() && !empty($instance->securetoolurl)) { $endpoint = trim($instance->securetoolurl); } // If SSL is forced, use the secure tool url if specified. Otherwise, make sure https is on the normal launch URL. if (isset($typeconfig['forcessl']) && ($typeconfig['forcessl'] == '1')) { if (!empty($instance->securetoolurl)) { $endpoint = trim($instance->securetoolurl); } if ($endpoint !== '') { $endpoint = lti_ensure_url_is_https($endpoint); } } else if ($endpoint !== '' && !strstr($endpoint, '://')) { $endpoint = 'http://' . $endpoint; } $orgid = lti_get_organizationid($typeconfig); $course = $PAGE->course; $islti2 = isset($tool->toolproxyid); $allparams = lti_build_request($instance, $typeconfig, $course, $typeid, $islti2, $messagetype, $foruserid); if ($islti2) { $requestparams = lti_build_request_lti2($tool, $allparams); } else { $requestparams = $allparams; } $requestparams = array_merge($requestparams, lti_build_standard_message($instance, $orgid, $ltiversion, $messagetype)); $customstr = ''; if (isset($typeconfig['customparameters'])) { $customstr = $typeconfig['customparameters']; } $services = lti_get_services(); foreach ($services as $service) { [$endpoint, $customstr] = $service->override_endpoint($messagetype, $endpoint, $customstr, $instance->course, $instance); } $requestparams = array_merge($requestparams, lti_build_custom_parameters($toolproxy, $tool, $instance, $allparams, $customstr, $instance->instructorcustomparameters, $islti2)); $launchcontainer = lti_get_launch_container($instance, $typeconfig); $returnurlparams = array('course' => $course->id, 'launch_container' => $launchcontainer, 'instanceid' => $instance->id, 'sesskey' => sesskey()); // Add the return URL. We send the launch container along to help us avoid frames-within-frames when the user returns. $url = new \moodle_url('/mod/lti/return.php', $returnurlparams); $returnurl = $url->out(false); if (isset($typeconfig['forcessl']) && ($typeconfig['forcessl'] == '1')) { $returnurl = lti_ensure_url_is_https($returnurl); } $target = ''; switch($launchcontainer) { case LTI_LAUNCH_CONTAINER_EMBED: case LTI_LAUNCH_CONTAINER_EMBED_NO_BLOCKS: $target = 'iframe'; break; case LTI_LAUNCH_CONTAINER_REPLACE_MOODLE_WINDOW: $target = 'frame'; break; case LTI_LAUNCH_CONTAINER_WINDOW: $target = 'window'; break; } if (!empty($target)) { $requestparams['launch_presentation_document_target'] = $target; } $requestparams['launch_presentation_return_url'] = $returnurl; // Add the parameters configured by the LTI services. if ($typeid && !$islti2) { $services = lti_get_services(); foreach ($services as $service) { $serviceparameters = $service->get_launch_parameters('basic-lti-launch-request', $course->id, $USER->id , $typeid, $instance->id); foreach ($serviceparameters as $paramkey => $paramvalue) { $requestparams['custom_' . $paramkey] = lti_parse_custom_parameter($toolproxy, $tool, $requestparams, $paramvalue, $islti2); } } } // Allow request params to be updated by sub-plugins. $plugins = core_component::get_plugin_list('ltisource'); foreach (array_keys($plugins) as $plugin) { $pluginparams = component_callback('ltisource_'.$plugin, 'before_launch', array($instance, $endpoint, $requestparams), array()); if (!empty($pluginparams) && is_array($pluginparams)) { $requestparams = array_merge($requestparams, $pluginparams); } } if ((!empty($key) && !empty($secret)) || ($ltiversion === LTI_VERSION_1P3)) { if ($ltiversion !== LTI_VERSION_1P3) { $parms = lti_sign_parameters($requestparams, $endpoint, 'POST', $key, $secret); } else { $parms = lti_sign_jwt($requestparams, $endpoint, $key, $typeid, $nonce); } $endpointurl = new \moodle_url($endpoint); $endpointparams = $endpointurl->params(); // Strip querystring params in endpoint url from $parms to avoid duplication. if (!empty($endpointparams) && !empty($parms)) { foreach (array_keys($endpointparams) as $paramname) { if (isset($parms[$paramname])) { unset($parms[$paramname]); } } } } else { // If no key and secret, do the launch unsigned. $returnurlparams['unsigned'] = '1'; $parms = $requestparams; } return array($endpoint, $parms); } /** * Launch an external tool activity. * * @param stdClass $instance the external tool activity settings * @param int $foruserid for user param, optional * @return string The HTML code containing the javascript code for the launch */ function lti_launch_tool($instance, $foruserid=0) { list($endpoint, $parms) = lti_get_launch_data($instance, '', '', $foruserid); $debuglaunch = ( $instance->debuglaunch == 1 ); $content = lti_post_launch_html($parms, $endpoint, $debuglaunch); echo $content; } /** * Prepares an LTI registration request message * * @param object $toolproxy Tool Proxy instance object */ function lti_register($toolproxy) { $endpoint = $toolproxy->regurl; // Change the status to pending. $toolproxy->state = LTI_TOOL_PROXY_STATE_PENDING; lti_update_tool_proxy($toolproxy); $requestparams = lti_build_registration_request($toolproxy); $content = lti_post_launch_html($requestparams, $endpoint, false); echo $content; } /** * Gets the parameters for the regirstration request * * @param object $toolproxy Tool Proxy instance object * @return array Registration request parameters */ function lti_build_registration_request($toolproxy) { $key = $toolproxy->guid; $secret = $toolproxy->secret; $requestparams = array(); $requestparams['lti_message_type'] = 'ToolProxyRegistrationRequest'; $requestparams['lti_version'] = 'LTI-2p0'; $requestparams['reg_key'] = $key; $requestparams['reg_password'] = $secret; $requestparams['reg_url'] = $toolproxy->regurl; // Add the profile URL. $profileservice = lti_get_service_by_name('profile'); $profileservice->set_tool_proxy($toolproxy); $requestparams['tc_profile_url'] = $profileservice->parse_value('$ToolConsumerProfile.url'); // Add the return URL. $returnurlparams = array('id' => $toolproxy->id, 'sesskey' => sesskey()); $url = new \moodle_url('/mod/lti/externalregistrationreturn.php', $returnurlparams); $returnurl = $url->out(false); $requestparams['launch_presentation_return_url'] = $returnurl; return $requestparams; } /** get Organization ID using default if no value provided * @param object $typeconfig * @return string */ function lti_get_organizationid($typeconfig) { global $CFG; // Default the organizationid if not specified. if (empty($typeconfig['organizationid'])) { if (($typeconfig['organizationid_default'] ?? LTI_DEFAULT_ORGID_SITEHOST) == LTI_DEFAULT_ORGID_SITEHOST) { $urlparts = parse_url($CFG->wwwroot); return $urlparts['host']; } else { return md5(get_site_identifier()); } } return $typeconfig['organizationid']; } /** * Build source ID * * @param int $instanceid * @param int $userid * @param string $servicesalt * @param null|int $typeid * @param null|int $launchid * @return stdClass */ function lti_build_sourcedid($instanceid, $userid, $servicesalt, $typeid = null, $launchid = null) { $data = new \stdClass(); $data->instanceid = $instanceid; $data->userid = $userid; $data->typeid = $typeid; if (!empty($launchid)) { $data->launchid = $launchid; } else { $data->launchid = mt_rand(); } $json = json_encode($data); $hash = hash('sha256', $json . $servicesalt, false); $container = new \stdClass(); $container->data = $data; $container->hash = $hash; return $container; } /** * This function builds the request that must be sent to the tool producer * * @param object $instance Basic LTI instance object * @param array $typeconfig Basic LTI tool configuration * @param object $course Course object * @param int|null $typeid Basic LTI tool ID * @param boolean $islti2 True if an LTI 2 tool is being launched * @param string $messagetype LTI Message Type for this launch * @param int $foruserid User targeted by this launch * * @return array Request details */ function lti_build_request($instance, $typeconfig, $course, $typeid = null, $islti2 = false, $messagetype = 'basic-lti-launch-request', $foruserid = 0) { global $USER, $CFG; if (empty($instance->cmid)) { $instance->cmid = 0; } $role = lti_get_ims_role($USER, $instance->cmid, $instance->course, $islti2); $requestparams = array( 'user_id' => $USER->id, 'lis_person_sourcedid' => $USER->idnumber, 'roles' => $role, 'context_id' => $course->id, 'context_label' => trim(html_to_text($course->shortname, 0)), 'context_title' => trim(html_to_text($course->fullname, 0)), ); if ($foruserid) { $requestparams['for_user_id'] = $foruserid; } if ($messagetype) { $requestparams['lti_message_type'] = $messagetype; } if (!empty($instance->name)) { $requestparams['resource_link_title'] = trim(html_to_text($instance->name, 0)); } if (!empty($instance->cmid)) { $intro = format_module_intro('lti', $instance, $instance->cmid); $intro = trim(html_to_text($intro, 0, false)); // This may look weird, but this is required for new lines // so we generate the same OAuth signature as the tool provider. $intro = str_replace("\n", "\r\n", $intro); $requestparams['resource_link_description'] = $intro; } if (!empty($instance->id)) { $requestparams['resource_link_id'] = $instance->id; } if (!empty($instance->resource_link_id)) { $requestparams['resource_link_id'] = $instance->resource_link_id; } if ($course->format == 'site') { $requestparams['context_type'] = 'Group'; } else { $requestparams['context_type'] = 'CourseSection'; $requestparams['lis_course_section_sourcedid'] = $course->idnumber; } if (!empty($instance->id) && !empty($instance->servicesalt) && ($islti2 || $typeconfig['acceptgrades'] == LTI_SETTING_ALWAYS || ($typeconfig['acceptgrades'] == LTI_SETTING_DELEGATE && $instance->instructorchoiceacceptgrades == LTI_SETTING_ALWAYS)) ) { $placementsecret = $instance->servicesalt; $sourcedid = json_encode(lti_build_sourcedid($instance->id, $USER->id, $placementsecret, $typeid)); $requestparams['lis_result_sourcedid'] = $sourcedid; // Add outcome service URL. $serviceurl = new \moodle_url('/mod/lti/service.php'); $serviceurl = $serviceurl->out(); $forcessl = false; if (!empty($CFG->mod_lti_forcessl)) { $forcessl = true; } if ((isset($typeconfig['forcessl']) && ($typeconfig['forcessl'] == '1')) or $forcessl) { $serviceurl = lti_ensure_url_is_https($serviceurl); } $requestparams['lis_outcome_service_url'] = $serviceurl; } // Send user's name and email data if appropriate. if ($islti2 || $typeconfig['sendname'] == LTI_SETTING_ALWAYS || ($typeconfig['sendname'] == LTI_SETTING_DELEGATE && isset($instance->instructorchoicesendname) && $instance->instructorchoicesendname == LTI_SETTING_ALWAYS) ) { $requestparams['lis_person_name_given'] = $USER->firstname; $requestparams['lis_person_name_family'] = $USER->lastname; $requestparams['lis_person_name_full'] = fullname($USER); $requestparams['ext_user_username'] = $USER->username; } if ($islti2 || $typeconfig['sendemailaddr'] == LTI_SETTING_ALWAYS || ($typeconfig['sendemailaddr'] == LTI_SETTING_DELEGATE && isset($instance->instructorchoicesendemailaddr) && $instance->instructorchoicesendemailaddr == LTI_SETTING_ALWAYS) ) { $requestparams['lis_person_contact_email_primary'] = $USER->email; } return $requestparams; } /** * This function builds the request that must be sent to an LTI 2 tool provider * * @param object $tool Basic LTI tool object * @param array $params Custom launch parameters * * @return array Request details */ function lti_build_request_lti2($tool, $params) { $requestparams = array(); $capabilities = lti_get_capabilities(); $enabledcapabilities = explode("\n", $tool->enabledcapability); foreach ($enabledcapabilities as $capability) { if (array_key_exists($capability, $capabilities)) { $val = $capabilities[$capability]; if ($val && (substr($val, 0, 1) != '$')) { if (isset($params[$val])) { $requestparams[$capabilities[$capability]] = $params[$capabilities[$capability]]; } } } } return $requestparams; } /** * This function builds the standard parameters for an LTI 1 or 2 request that must be sent to the tool producer * * @param stdClass $instance Basic LTI instance object * @param string $orgid Organisation ID * @param boolean $islti2 True if an LTI 2 tool is being launched * @param string $messagetype The request message type. Defaults to basic-lti-launch-request if empty. * * @return array Request details * @deprecated since Moodle 3.7 MDL-62599 - please do not use this function any more. * @see lti_build_standard_message() */ function lti_build_standard_request($instance, $orgid, $islti2, $messagetype = 'basic-lti-launch-request') { if (!$islti2) { $ltiversion = LTI_VERSION_1; } else { $ltiversion = LTI_VERSION_2; } return lti_build_standard_message($instance, $orgid, $ltiversion, $messagetype); } /** * This function builds the standard parameters for an LTI message that must be sent to the tool producer * * @param stdClass $instance Basic LTI instance object * @param string $orgid Organisation ID * @param boolean $ltiversion LTI version to be used for tool messages * @param string $messagetype The request message type. Defaults to basic-lti-launch-request if empty. * * @return array Message parameters */ function lti_build_standard_message($instance, $orgid, $ltiversion, $messagetype = 'basic-lti-launch-request') { global $CFG; $requestparams = array(); if ($instance) { $requestparams['resource_link_id'] = $instance->id; if (property_exists($instance, 'resource_link_id') and !empty($instance->resource_link_id)) { $requestparams['resource_link_id'] = $instance->resource_link_id; } } $requestparams['launch_presentation_locale'] = current_language(); // Make sure we let the tool know what LMS they are being called from. $requestparams['ext_lms'] = 'moodle-2'; $requestparams['tool_consumer_info_product_family_code'] = 'moodle'; $requestparams['tool_consumer_info_version'] = strval($CFG->version); // Add oauth_callback to be compliant with the 1.0A spec. $requestparams['oauth_callback'] = 'about:blank'; $requestparams['lti_version'] = $ltiversion; $requestparams['lti_message_type'] = $messagetype; if ($orgid) { $requestparams["tool_consumer_instance_guid"] = $orgid; } if (!empty($CFG->mod_lti_institution_name)) { $requestparams['tool_consumer_instance_name'] = trim(html_to_text($CFG->mod_lti_institution_name, 0)); } else { $requestparams['tool_consumer_instance_name'] = get_site()->shortname; } $requestparams['tool_consumer_instance_description'] = trim(html_to_text(get_site()->fullname, 0)); return $requestparams; } /** * This function builds the custom parameters * * @param object $toolproxy Tool proxy instance object * @param object $tool Tool instance object * @param object $instance Tool placement instance object * @param array $params LTI launch parameters * @param string $customstr Custom parameters defined for tool * @param string $instructorcustomstr Custom parameters defined for this placement * @param boolean $islti2 True if an LTI 2 tool is being launched * * @return array Custom parameters */ function lti_build_custom_parameters($toolproxy, $tool, $instance, $params, $customstr, $instructorcustomstr, $islti2) { // Concatenate the custom parameters from the administrator and the instructor // Instructor parameters are only taken into consideration if the administrator // has given permission. $custom = array(); if ($customstr) { $custom = lti_split_custom_parameters($toolproxy, $tool, $params, $customstr, $islti2); } if ($instructorcustomstr) { $custom = array_merge(lti_split_custom_parameters($toolproxy, $tool, $params, $instructorcustomstr, $islti2), $custom); } if ($islti2) { $custom = array_merge(lti_split_custom_parameters($toolproxy, $tool, $params, $tool->parameter, true), $custom); $settings = lti_get_tool_settings($tool->toolproxyid); $custom = array_merge($custom, lti_get_custom_parameters($toolproxy, $tool, $params, $settings)); if (!empty($instance->course)) { $settings = lti_get_tool_settings($tool->toolproxyid, $instance->course); $custom = array_merge($custom, lti_get_custom_parameters($toolproxy, $tool, $params, $settings)); if (!empty($instance->id)) { $settings = lti_get_tool_settings($tool->toolproxyid, $instance->course, $instance->id); $custom = array_merge($custom, lti_get_custom_parameters($toolproxy, $tool, $params, $settings)); } } } return $custom; } /** * Builds a standard LTI Content-Item selection request. * * @param int $id The tool type ID. * @param stdClass $course The course object. * @param moodle_url $returnurl The return URL in the tool consumer (TC) that the tool provider (TP) * will use to return the Content-Item message. * @param string $title The tool's title, if available. * @param string $text The text to display to represent the content item. This value may be a long description of the content item. * @param array $mediatypes Array of MIME types types supported by the TC. If empty, the TC will support ltilink by default. * @param array $presentationtargets Array of ways in which the selected content item(s) can be requested to be opened * (via the presentationDocumentTarget element for a returned content item). * If empty, "frame", "iframe", and "window" will be supported by default. * @param bool $autocreate Indicates whether any content items returned by the TP would be automatically persisted without * @param bool $multiple Indicates whether the user should be permitted to select more than one item. False by default. * any option for the user to cancel the operation. False by default. * @param bool $unsigned Indicates whether the TC is willing to accept an unsigned return message, or not. * A signed message should always be required when the content item is being created automatically in the * TC without further interaction from the user. False by default. * @param bool $canconfirm Flag for can_confirm parameter. False by default. * @param bool $copyadvice Indicates whether the TC is able and willing to make a local copy of a content item. False by default. * @param string $nonce * @return stdClass The object containing the signed request parameters and the URL to the TP's Content-Item selection interface. * @throws moodle_exception When the LTI tool type does not exist.` * @throws coding_exception For invalid media type and presentation target parameters. */ function lti_build_content_item_selection_request($id, $course, moodle_url $returnurl, $title = '', $text = '', $mediatypes = [], $presentationtargets = [], $autocreate = false, $multiple = true, $unsigned = false, $canconfirm = false, $copyadvice = false, $nonce = '') { global $USER; $tool = lti_get_type($id); // Validate parameters. if (!$tool) { throw new moodle_exception('errortooltypenotfound', 'mod_lti'); } if (!is_array($mediatypes)) { throw new coding_exception('The list of accepted media types should be in an array'); } if (!is_array($presentationtargets)) { throw new coding_exception('The list of accepted presentation targets should be in an array'); } // Check title. If empty, use the tool's name. if (empty($title)) { $title = $tool->name; } $typeconfig = lti_get_type_config($id); $key = ''; $secret = ''; $islti2 = false; $islti13 = false; if (isset($tool->toolproxyid)) { $islti2 = true; $toolproxy = lti_get_tool_proxy($tool->toolproxyid); $key = $toolproxy->guid; $secret = $toolproxy->secret; } else { $islti13 = $tool->ltiversion === LTI_VERSION_1P3; $toolproxy = null; if ($islti13 && !empty($tool->clientid)) { $key = $tool->clientid; } else if (!$islti13 && !empty($typeconfig['resourcekey'])) { $key = $typeconfig['resourcekey']; } if (!empty($typeconfig['password'])) { $secret = $typeconfig['password']; } } $tool->enabledcapability = ''; if (!empty($typeconfig['enabledcapability_ContentItemSelectionRequest'])) { $tool->enabledcapability = $typeconfig['enabledcapability_ContentItemSelectionRequest']; } $tool->parameter = ''; if (!empty($typeconfig['parameter_ContentItemSelectionRequest'])) { $tool->parameter = $typeconfig['parameter_ContentItemSelectionRequest']; } // Set the tool URL. if (!empty($typeconfig['toolurl_ContentItemSelectionRequest'])) { $toolurl = new moodle_url($typeconfig['toolurl_ContentItemSelectionRequest']); } else { $toolurl = new moodle_url($typeconfig['toolurl']); } // Check if SSL is forced. if (!empty($typeconfig['forcessl'])) { // Make sure the tool URL is set to https. if (strtolower($toolurl->get_scheme()) === 'http') { $toolurl->set_scheme('https'); } // Make sure the return URL is set to https. if (strtolower($returnurl->get_scheme()) === 'http') { $returnurl->set_scheme('https'); } } $toolurlout = $toolurl->out(false); // Get base request parameters. $instance = new stdClass(); $instance->course = $course->id; $requestparams = lti_build_request($instance, $typeconfig, $course, $id, $islti2); // Get LTI2-specific request parameters and merge to the request parameters if applicable. if ($islti2) { $lti2params = lti_build_request_lti2($tool, $requestparams); $requestparams = array_merge($requestparams, $lti2params); } // Get standard request parameters and merge to the request parameters. $orgid = lti_get_organizationid($typeconfig); $standardparams = lti_build_standard_message(null, $orgid, $tool->ltiversion, 'ContentItemSelectionRequest'); $requestparams = array_merge($requestparams, $standardparams); // Get custom request parameters and merge to the request parameters. $customstr = ''; if (!empty($typeconfig['customparameters'])) { $customstr = $typeconfig['customparameters']; } $customparams = lti_build_custom_parameters($toolproxy, $tool, $instance, $requestparams, $customstr, '', $islti2); $requestparams = array_merge($requestparams, $customparams); // Add the parameters configured by the LTI services. if ($id && !$islti2) { $services = lti_get_services(); foreach ($services as $service) { $serviceparameters = $service->get_launch_parameters('ContentItemSelectionRequest', $course->id, $USER->id , $id); foreach ($serviceparameters as $paramkey => $paramvalue) { $requestparams['custom_' . $paramkey] = lti_parse_custom_parameter($toolproxy, $tool, $requestparams, $paramvalue, $islti2); } } } // Allow request params to be updated by sub-plugins. $plugins = core_component::get_plugin_list('ltisource'); foreach (array_keys($plugins) as $plugin) { $pluginparams = component_callback('ltisource_' . $plugin, 'before_launch', [$instance, $toolurlout, $requestparams], []); if (!empty($pluginparams) && is_array($pluginparams)) { $requestparams = array_merge($requestparams, $pluginparams); } } if (!$islti13) { // Media types. Set to ltilink by default if empty. if (empty($mediatypes)) { $mediatypes = [ 'application/vnd.ims.lti.v1.ltilink', ]; } $requestparams['accept_media_types'] = implode(',', $mediatypes); } else { // Only LTI links are currently supported. $requestparams['accept_types'] = 'ltiResourceLink'; } // Presentation targets. Supports frame, iframe, window by default if empty. if (empty($presentationtargets)) { $presentationtargets = [ 'frame', 'iframe', 'window', ]; } $requestparams['accept_presentation_document_targets'] = implode(',', $presentationtargets); // Other request parameters. $requestparams['accept_copy_advice'] = $copyadvice === true ? 'true' : 'false'; $requestparams['accept_multiple'] = $multiple === true ? 'true' : 'false'; $requestparams['accept_unsigned'] = $unsigned === true ? 'true' : 'false'; $requestparams['auto_create'] = $autocreate === true ? 'true' : 'false'; $requestparams['can_confirm'] = $canconfirm === true ? 'true' : 'false'; $requestparams['content_item_return_url'] = $returnurl->out(false); $requestparams['title'] = $title; $requestparams['text'] = $text; if (!$islti13) { $signedparams = lti_sign_parameters($requestparams, $toolurlout, 'POST', $key, $secret); } else { $signedparams = lti_sign_jwt($requestparams, $toolurlout, $key, $id, $nonce); } $toolurlparams = $toolurl->params(); // Strip querystring params in endpoint url from $signedparams to avoid duplication. if (!empty($toolurlparams) && !empty($signedparams)) { foreach (array_keys($toolurlparams) as $paramname) { if (isset($signedparams[$paramname])) { unset($signedparams[$paramname]); } } } // Check for params that should not be passed. Unset if they are set. $unwantedparams = [ 'resource_link_id', 'resource_link_title', 'resource_link_description', 'launch_presentation_return_url', 'lis_result_sourcedid', ]; foreach ($unwantedparams as $param) { if (isset($signedparams[$param])) { unset($signedparams[$param]); } } // Prepare result object. $result = new stdClass(); $result->params = $signedparams; $result->url = $toolurlout; return $result; } /** * Verifies the OAuth signature of an incoming message. * * @param int $typeid The tool type ID. * @param string $consumerkey The consumer key. * @return stdClass Tool type * @throws moodle_exception * @throws lti\OAuthException */ function lti_verify_oauth_signature($typeid, $consumerkey) { $tool = lti_get_type($typeid); // Validate parameters. if (!$tool) { throw new moodle_exception('errortooltypenotfound', 'mod_lti'); } $typeconfig = lti_get_type_config($typeid); if (isset($tool->toolproxyid)) { $toolproxy = lti_get_tool_proxy($tool->toolproxyid); $key = $toolproxy->guid; $secret = $toolproxy->secret; } else { $toolproxy = null; if (!empty($typeconfig['resourcekey'])) { $key = $typeconfig['resourcekey']; } else { $key = ''; } if (!empty($typeconfig['password'])) { $secret = $typeconfig['password']; } else { $secret = ''; } } if ($consumerkey !== $key) { throw new moodle_exception('errorincorrectconsumerkey', 'mod_lti'); } $store = new lti\TrivialOAuthDataStore(); $store->add_consumer($key, $secret); $server = new lti\OAuthServer($store); $method = new lti\OAuthSignatureMethod_HMAC_SHA1(); $server->add_signature_method($method); $request = lti\OAuthRequest::from_request(); try { $server->verify_request($request); } catch (lti\OAuthException $e) { throw new lti\OAuthException("OAuth signature failed: " . $e->getMessage()); } return $tool; } /** * Verifies the JWT signature using a JWK keyset. * * @param string $jwtparam JWT parameter value. * @param string $keyseturl The tool keyseturl. * @param string $clientid The tool client id. * * @return object The JWT's payload as a PHP object * @throws moodle_exception * @throws UnexpectedValueException Provided JWT was invalid * @throws SignatureInvalidException Provided JWT was invalid because the signature verification failed * @throws BeforeValidException Provided JWT is trying to be used before it's eligible as defined by 'nbf' * @throws BeforeValidException Provided JWT is trying to be used before it's been created as defined by 'iat' * @throws ExpiredException Provided JWT has since expired, as defined by the 'exp' claim */ function lti_verify_with_keyset($jwtparam, $keyseturl, $clientid) { // Attempts to retrieve cached keyset. $cache = cache::make('mod_lti', 'keyset'); $keyset = $cache->get($clientid); try { if (empty($keyset)) { throw new moodle_exception('errornocachedkeysetfound', 'mod_lti'); } $keysetarr = json_decode($keyset, true); $keys = JWK::parseKeySet($keysetarr); $jwt = JWT::decode($jwtparam, $keys); } catch (Exception $e) { // Something went wrong, so attempt to update cached keyset and then try again. $keyset = download_file_content($keyseturl); $keysetarr = json_decode($keyset, true); // Fix for firebase/php-jwt's dependency on the optional 'alg' property in the JWK. // The fix_jwks_alg() call only fixes a single, matched key and will leave others present (which may be missing alg too), // Remaining keys missing alg are excluded since they cannot be used for decoding anyway (no match to JWT kid). $keysetarr = jwks_helper::fix_jwks_alg($keysetarr, $jwtparam); $keysetarr['keys'] = array_filter($keysetarr['keys'], fn($key) => isset($key['alg'])); // JWK::parseKeySet uses RS256 algorithm by default. $keys = JWK::parseKeySet($keysetarr); $jwt = JWT::decode($jwtparam, $keys); // If sucessful, updates the cached keyset. $cache->set($clientid, $keyset); } return $jwt; } /** * Verifies the JWT signature of an incoming message. * * @param int $typeid The tool type ID. * @param string $consumerkey The consumer key. * @param string $jwtparam JWT parameter value * * @return stdClass Tool type * @throws moodle_exception * @throws UnexpectedValueException Provided JWT was invalid * @throws SignatureInvalidException Provided JWT was invalid because the signature verification failed * @throws BeforeValidException Provided JWT is trying to be used before it's eligible as defined by 'nbf' * @throws BeforeValidException Provided JWT is trying to be used before it's been created as defined by 'iat' * @throws ExpiredException Provided JWT has since expired, as defined by the 'exp' claim */ function lti_verify_jwt_signature($typeid, $consumerkey, $jwtparam) { $tool = lti_get_type($typeid); // Validate parameters. if (!$tool) { throw new moodle_exception('errortooltypenotfound', 'mod_lti'); } if (isset($tool->toolproxyid)) { throw new moodle_exception('JWT security not supported with LTI 2'); } $typeconfig = lti_get_type_config($typeid); $key = $tool->clientid ?? ''; if ($consumerkey !== $key) { throw new moodle_exception('errorincorrectconsumerkey', 'mod_lti'); } if (empty($typeconfig['keytype']) || $typeconfig['keytype'] === LTI_RSA_KEY) { $publickey = $typeconfig['publickey'] ?? ''; if (empty($publickey)) { throw new moodle_exception('No public key configured'); } // Attemps to verify jwt with RSA key. JWT::decode($jwtparam, new Key($publickey, 'RS256')); } else if ($typeconfig['keytype'] === LTI_JWK_KEYSET) { $keyseturl = $typeconfig['publickeyset'] ?? ''; if (empty($keyseturl)) { throw new moodle_exception('No public keyset configured'); } // Attempts to verify jwt with jwk keyset. lti_verify_with_keyset($jwtparam, $keyseturl, $tool->clientid); } else { throw new moodle_exception('Invalid public key type'); } return $tool; } /** * Converts an array of custom parameters to a new line separated string. * * @param object $params list of params to concatenate * * @return string */ function params_to_string(object $params) { $customparameters = []; foreach ($params as $key => $value) { $customparameters[] = "{$key}={$value}"; } return implode("\n", $customparameters); } /** * Converts LTI 1.1 Content Item for LTI Link to Form data. * * @param object $tool Tool for which the item is created for. * @param object $typeconfig The tool configuration. * @param object $item Item populated from JSON to be converted to Form form * * @return stdClass Form config for the item */ function content_item_to_form(object $tool, object $typeconfig, object $item): stdClass { global $OUTPUT; $config = new stdClass(); $config->name = ''; if (isset($item->title)) { $config->name = $item->title; } if (empty($config->name)) { $config->name = $tool->name; } if (isset($item->text)) { $config->introeditor = [ 'text' => $item->text, 'format' => FORMAT_PLAIN ]; } else { $config->introeditor = [ 'text' => '', 'format' => FORMAT_PLAIN ]; } if (isset($item->icon->{'@id'})) { $iconurl = new moodle_url($item->icon->{'@id'}); // Assign item's icon URL to secureicon or icon depending on its scheme. if (strtolower($iconurl->get_scheme()) === 'https') { $config->secureicon = $iconurl->out(false); } else { $config->icon = $iconurl->out(false); } } if (isset($item->url)) { $url = new moodle_url($item->url); $config->toolurl = $url->out(false); $config->typeid = 0; } else { $config->typeid = $tool->id; } $config->instructorchoiceacceptgrades = LTI_SETTING_NEVER; $islti2 = $tool->ltiversion === LTI_VERSION_2; if (!$islti2 && isset($typeconfig->lti_acceptgrades)) { $acceptgrades = $typeconfig->lti_acceptgrades; if ($acceptgrades == LTI_SETTING_ALWAYS) { // We create a line item regardless if the definition contains one or not. $config->instructorchoiceacceptgrades = LTI_SETTING_ALWAYS; $config->grade_modgrade_point = 100; } if ($acceptgrades == LTI_SETTING_DELEGATE || $acceptgrades == LTI_SETTING_ALWAYS) { if (isset($item->lineItem)) { $lineitem = $item->lineItem; $config->instructorchoiceacceptgrades = LTI_SETTING_ALWAYS; $maxscore = 100; if (isset($lineitem->scoreConstraints)) { $sc = $lineitem->scoreConstraints; if (isset($sc->totalMaximum)) { $maxscore = $sc->totalMaximum; } else if (isset($sc->normalMaximum)) { $maxscore = $sc->normalMaximum; } } $config->grade_modgrade_point = $maxscore; $config->lineitemresourceid = ''; $config->lineitemtag = ''; $config->lineitemsubreviewurl = ''; $config->lineitemsubreviewparams = ''; if (isset($lineitem->assignedActivity) && isset($lineitem->assignedActivity->activityId)) { $config->lineitemresourceid = $lineitem->assignedActivity->activityId?:''; } if (isset($lineitem->tag)) { $config->lineitemtag = $lineitem->tag?:''; } if (isset($lineitem->submissionReview)) { $subreview = $lineitem->submissionReview; $config->lineitemsubreviewurl = 'DEFAULT'; if (!empty($subreview->url)) { $config->lineitemsubreviewurl = $subreview->url; } if (isset($subreview->custom)) { $config->lineitemsubreviewparams = params_to_string($subreview->custom); } } } } } $config->instructorchoicesendname = LTI_SETTING_NEVER; $config->instructorchoicesendemailaddr = LTI_SETTING_NEVER; // Since 4.3, the launch container is dictated by the value set in tool configuration and isn't controllable by content items. $config->launchcontainer = LTI_LAUNCH_CONTAINER_DEFAULT; if (isset($item->custom)) { $config->instructorcustomparameters = params_to_string($item->custom); } // Pass an indicator to the relevant form field. $config->selectcontentindicator = $OUTPUT->pix_icon('i/valid', get_string('yes')) . get_string('contentselected', 'mod_lti'); return $config; } /** * Processes the tool provider's response to the ContentItemSelectionRequest and builds the configuration data from the * selected content item. This configuration data can be then used when adding a tool into the course. * * @param int $typeid The tool type ID. * @param string $messagetype The value for the lti_message_type parameter. * @param string $ltiversion The value for the lti_version parameter. * @param string $consumerkey The consumer key. * @param string $contentitemsjson The JSON string for the content_items parameter. * @return stdClass The array of module information objects. * @throws moodle_exception * @throws lti\OAuthException */ function lti_tool_configuration_from_content_item($typeid, $messagetype, $ltiversion, $consumerkey, $contentitemsjson) { $tool = lti_get_type($typeid); // Validate parameters. if (!$tool) { throw new moodle_exception('errortooltypenotfound', 'mod_lti'); } // Check lti_message_type. Show debugging if it's not set to ContentItemSelection. // No need to throw exceptions for now since lti_message_type does not seem to be used in this processing at the moment. if ($messagetype !== 'ContentItemSelection') { debugging("lti_message_type is invalid: {$messagetype}. It should be set to 'ContentItemSelection'.", DEBUG_DEVELOPER); } // Check LTI versions from our side and the response's side. Show debugging if they don't match. // No need to throw exceptions for now since LTI version does not seem to be used in this processing at the moment. $expectedversion = $tool->ltiversion; $islti2 = ($expectedversion === LTI_VERSION_2); if ($ltiversion !== $expectedversion) { debugging("lti_version from response does not match the tool's configuration. Tool: {$expectedversion}," . " Response: {$ltiversion}", DEBUG_DEVELOPER); } $items = json_decode($contentitemsjson); if (empty($items)) { throw new moodle_exception('errorinvaliddata', 'mod_lti', '', $contentitemsjson); } if (!isset($items->{'@graph'}) || !is_array($items->{'@graph'})) { throw new moodle_exception('errorinvalidresponseformat', 'mod_lti'); } $config = null; $items = $items->{'@graph'}; if (!empty($items)) { $typeconfig = lti_get_type_type_config($tool->id); if (count($items) == 1) { $config = content_item_to_form($tool, $typeconfig, $items[0]); } else { $multiple = []; foreach ($items as $item) { $multiple[] = content_item_to_form($tool, $typeconfig, $item); } $config = new stdClass(); $config->multiple = $multiple; } } return $config; } /** * Converts the new Deep-Linking format for Content-Items to the old format. * * @param string $param JSON string representing new Deep-Linking format * @return string JSON representation of content-items */ function lti_convert_content_items($param) { $items = array(); $json = json_decode($param); if (!empty($json) && is_array($json)) { foreach ($json as $item) { if (isset($item->type)) { $newitem = clone $item; switch ($item->type) { case 'ltiResourceLink': $newitem->{'@type'} = 'LtiLinkItem'; $newitem->mediaType = 'application\/vnd.ims.lti.v1.ltilink'; break; case 'link': case 'rich': $newitem->{'@type'} = 'ContentItem'; $newitem->mediaType = 'text/html'; break; case 'file': $newitem->{'@type'} = 'FileItem'; break; } unset($newitem->type); if (isset($item->html)) { $newitem->text = $item->html; unset($newitem->html); } if (isset($item->iframe)) { // DeepLinking allows multiple options to be declared as supported. // We favor iframe over new window if both are specified. $newitem->placementAdvice = new stdClass(); $newitem->placementAdvice->presentationDocumentTarget = 'iframe'; if (isset($item->iframe->width)) { $newitem->placementAdvice->displayWidth = $item->iframe->width; } if (isset($item->iframe->height)) { $newitem->placementAdvice->displayHeight = $item->iframe->height; } unset($newitem->iframe); unset($newitem->window); } else if (isset($item->window)) { $newitem->placementAdvice = new stdClass(); $newitem->placementAdvice->presentationDocumentTarget = 'window'; if (isset($item->window->targetName)) { $newitem->placementAdvice->windowTarget = $item->window->targetName; } if (isset($item->window->width)) { $newitem->placementAdvice->displayWidth = $item->window->width; } if (isset($item->window->height)) { $newitem->placementAdvice->displayHeight = $item->window->height; } unset($newitem->window); } else if (isset($item->presentation)) { // This may have been part of an early draft but is not in the final spec // so keeping it around for now in case it's actually been used. $newitem->placementAdvice = new stdClass(); if (isset($item->presentation->documentTarget)) { $newitem->placementAdvice->presentationDocumentTarget = $item->presentation->documentTarget; } if (isset($item->presentation->windowTarget)) { $newitem->placementAdvice->windowTarget = $item->presentation->windowTarget; } if (isset($item->presentation->width)) { $newitem->placementAdvice->dislayWidth = $item->presentation->width; } if (isset($item->presentation->height)) { $newitem->placementAdvice->dislayHeight = $item->presentation->height; } unset($newitem->presentation); } if (isset($item->icon) && isset($item->icon->url)) { $newitem->icon->{'@id'} = $item->icon->url; unset($newitem->icon->url); } if (isset($item->thumbnail) && isset($item->thumbnail->url)) { $newitem->thumbnail->{'@id'} = $item->thumbnail->url; unset($newitem->thumbnail->url); } if (isset($item->lineItem)) { unset($newitem->lineItem); $newitem->lineItem = new stdClass(); $newitem->lineItem->{'@type'} = 'LineItem'; $newitem->lineItem->reportingMethod = 'http://purl.imsglobal.org/ctx/lis/v2p1/Result#totalScore'; if (isset($item->lineItem->label)) { $newitem->lineItem->label = $item->lineItem->label; } if (isset($item->lineItem->resourceId)) { $newitem->lineItem->assignedActivity = new stdClass(); $newitem->lineItem->assignedActivity->activityId = $item->lineItem->resourceId; } if (isset($item->lineItem->tag)) { $newitem->lineItem->tag = $item->lineItem->tag; } if (isset($item->lineItem->scoreMaximum)) { $newitem->lineItem->scoreConstraints = new stdClass(); $newitem->lineItem->scoreConstraints->{'@type'} = 'NumericLimits'; $newitem->lineItem->scoreConstraints->totalMaximum = $item->lineItem->scoreMaximum; } if (isset($item->lineItem->submissionReview)) { $newitem->lineItem->submissionReview = $item->lineItem->submissionReview; } } $items[] = $newitem; } } } $newitems = new stdClass(); $newitems->{'@context'} = 'http://purl.imsglobal.org/ctx/lti/v1/ContentItem'; $newitems->{'@graph'} = $items; return json_encode($newitems); } function lti_get_tool_table($tools, $id) { global $OUTPUT; $html = ''; $typename = get_string('typename', 'lti'); $baseurl = get_string('baseurl', 'lti'); $action = get_string('action', 'lti'); $createdon = get_string('createdon', 'lti'); if (!empty($tools)) { $html .= "
    "; foreach ($tools as $type) { $date = userdate($type->timecreated, get_string('strftimedatefullshort', 'core_langconfig')); $accept = get_string('accept', 'lti'); $update = get_string('update', 'lti'); $delete = get_string('delete', 'lti'); if (empty($type->toolproxyid)) { $baseurl = new \moodle_url('/mod/lti/typessettings.php', array( 'action' => 'accept', 'id' => $type->id, 'sesskey' => sesskey(), 'tab' => $id )); $ref = $type->baseurl; } else { $baseurl = new \moodle_url('/mod/lti/toolssettings.php', array( 'action' => 'accept', 'id' => $type->id, 'sesskey' => sesskey(), 'tab' => $id )); $ref = $type->tpname; } $accepthtml = $OUTPUT->action_icon($baseurl, new \pix_icon('t/check', $accept, '', array('class' => 'iconsmall')), null, array('title' => $accept, 'class' => 'editing_accept')); $deleteaction = 'delete'; if ($type->state == LTI_TOOL_STATE_CONFIGURED) { $accepthtml = ''; } if ($type->state != LTI_TOOL_STATE_REJECTED) { $deleteaction = 'reject'; $delete = get_string('reject', 'lti'); } $updateurl = clone($baseurl); $updateurl->param('action', 'update'); $updatehtml = $OUTPUT->action_icon($updateurl, new \pix_icon('t/edit', $update, '', array('class' => 'iconsmall')), null, array('title' => $update, 'class' => 'editing_update')); if (($type->state != LTI_TOOL_STATE_REJECTED) || empty($type->toolproxyid)) { $deleteurl = clone($baseurl); $deleteurl->param('action', $deleteaction); $deletehtml = $OUTPUT->action_icon($deleteurl, new \pix_icon('t/delete', $delete, '', array('class' => 'iconsmall')), null, array('title' => $delete, 'class' => 'editing_delete')); } else { $deletehtml = ''; } $html .= " "; } $html .= '
    $typename $baseurl $createdon $action
    {$type->name} {$ref} {$date} {$accepthtml}{$updatehtml}{$deletehtml}
    '; } else { $html .= get_string('no_' . $id, 'lti'); } return $html; } /** * This function builds the tab for a category of tool proxies * * @param object $toolproxies Tool proxy instance objects * @param string $id Category ID * * @return string HTML for tab */ function lti_get_tool_proxy_table($toolproxies, $id) { global $OUTPUT; if (!empty($toolproxies)) { $typename = get_string('typename', 'lti'); $url = get_string('registrationurl', 'lti'); $action = get_string('action', 'lti'); $createdon = get_string('createdon', 'lti'); $html = <<< EOD
    EOD; foreach ($toolproxies as $toolproxy) { $date = userdate($toolproxy->timecreated, get_string('strftimedatefullshort', 'core_langconfig')); $accept = get_string('register', 'lti'); $update = get_string('update', 'lti'); $delete = get_string('delete', 'lti'); $baseurl = new \moodle_url('/mod/lti/registersettings.php', array( 'action' => 'accept', 'id' => $toolproxy->id, 'sesskey' => sesskey(), 'tab' => $id )); $registerurl = new \moodle_url('/mod/lti/register.php', array( 'id' => $toolproxy->id, 'sesskey' => sesskey(), 'tab' => 'tool_proxy' )); $accepthtml = $OUTPUT->action_icon($registerurl, new \pix_icon('t/check', $accept, '', array('class' => 'iconsmall')), null, array('title' => $accept, 'class' => 'editing_accept')); $deleteaction = 'delete'; if ($toolproxy->state != LTI_TOOL_PROXY_STATE_CONFIGURED) { $accepthtml = ''; } if (($toolproxy->state == LTI_TOOL_PROXY_STATE_CONFIGURED) || ($toolproxy->state == LTI_TOOL_PROXY_STATE_PENDING)) { $delete = get_string('cancel', 'lti'); } $updateurl = clone($baseurl); $updateurl->param('action', 'update'); $updatehtml = $OUTPUT->action_icon($updateurl, new \pix_icon('t/edit', $update, '', array('class' => 'iconsmall')), null, array('title' => $update, 'class' => 'editing_update')); $deleteurl = clone($baseurl); $deleteurl->param('action', $deleteaction); $deletehtml = $OUTPUT->action_icon($deleteurl, new \pix_icon('t/delete', $delete, '', array('class' => 'iconsmall')), null, array('title' => $delete, 'class' => 'editing_delete')); $html .= <<< EOD EOD; } $html .= '
    {$typename} {$url} {$createdon} {$action}
    {$toolproxy->name} {$toolproxy->regurl} {$date} {$accepthtml}{$updatehtml}{$deletehtml}
    '; } else { $html = get_string('no_' . $id, 'lti'); } return $html; } /** * Extracts the enabled capabilities into an array, including those implicitly declared in a parameter * * @param object $tool Tool instance object * * @return array List of enabled capabilities */ function lti_get_enabled_capabilities($tool) { if (!isset($tool)) { return array(); } if (!empty($tool->enabledcapability)) { $enabledcapabilities = explode("\n", $tool->enabledcapability); } else { $enabledcapabilities = array(); } if (!empty($tool->parameter)) { $paramstr = str_replace("\r\n", "\n", $tool->parameter); $paramstr = str_replace("\n\r", "\n", $paramstr); $paramstr = str_replace("\r", "\n", $paramstr); $params = explode("\n", $paramstr); foreach ($params as $param) { $pos = strpos($param, '='); if (($pos === false) || ($pos < 1)) { continue; } $value = trim(core_text::substr($param, $pos + 1, strlen($param))); if (substr($value, 0, 1) == '$') { $value = substr($value, 1); if (!in_array($value, $enabledcapabilities)) { $enabledcapabilities[] = $value; } } } } return $enabledcapabilities; } /** * Splits the custom parameters * * @param string $customstr String containing the parameters * * @return array of custom parameters */ function lti_split_parameters($customstr) { $customstr = str_replace("\r\n", "\n", $customstr); $customstr = str_replace("\n\r", "\n", $customstr); $customstr = str_replace("\r", "\n", $customstr); $lines = explode("\n", $customstr); // Or should this split on "/[\n;]/"? $retval = array(); foreach ($lines as $line) { $pos = strpos($line, '='); if ( $pos === false || $pos < 1 ) { continue; } $key = trim(core_text::substr($line, 0, $pos)); $val = trim(core_text::substr($line, $pos + 1, strlen($line))); $retval[$key] = $val; } return $retval; } /** * Splits the custom parameters field to the various parameters * * @param object $toolproxy Tool proxy instance object * @param object $tool Tool instance object * @param array $params LTI launch parameters * @param string $customstr String containing the parameters * @param boolean $islti2 True if an LTI 2 tool is being launched * * @return array of custom parameters */ function lti_split_custom_parameters($toolproxy, $tool, $params, $customstr, $islti2 = false) { $splitted = lti_split_parameters($customstr); $retval = array(); foreach ($splitted as $key => $val) { $val = lti_parse_custom_parameter($toolproxy, $tool, $params, $val, $islti2); $key2 = lti_map_keyname($key); $retval['custom_'.$key2] = $val; if (($islti2 || ($tool->ltiversion === LTI_VERSION_1P3)) && ($key != $key2)) { $retval['custom_'.$key] = $val; } } return $retval; } /** * Adds the custom parameters to an array * * @param object $toolproxy Tool proxy instance object * @param object $tool Tool instance object * @param array $params LTI launch parameters * @param array $parameters Array containing the parameters * * @return array Array of custom parameters */ function lti_get_custom_parameters($toolproxy, $tool, $params, $parameters) { $retval = array(); foreach ($parameters as $key => $val) { $key2 = lti_map_keyname($key); $val = lti_parse_custom_parameter($toolproxy, $tool, $params, $val, true); $retval['custom_'.$key2] = $val; if ($key != $key2) { $retval['custom_'.$key] = $val; } } return $retval; } /** * Parse a custom parameter to replace any substitution variables * * @param object $toolproxy Tool proxy instance object * @param object $tool Tool instance object * @param array $params LTI launch parameters * @param string $value Custom parameter value * @param boolean $islti2 True if an LTI 2 tool is being launched * * @return string Parsed value of custom parameter */ function lti_parse_custom_parameter($toolproxy, $tool, $params, $value, $islti2) { // This is required as {${$valarr[0]}->{$valarr[1]}}" may be using the USER or COURSE var. global $USER, $COURSE; if ($value) { if (substr($value, 0, 1) == '\\') { $value = substr($value, 1); } else if (substr($value, 0, 1) == '$') { $value1 = substr($value, 1); $enabledcapabilities = lti_get_enabled_capabilities($tool); if (!$islti2 || in_array($value1, $enabledcapabilities)) { $capabilities = lti_get_capabilities(); if (array_key_exists($value1, $capabilities)) { $val = $capabilities[$value1]; if ($val) { if (substr($val, 0, 1) != '$') { $value = $params[$val]; } else { $valarr = explode('->', substr($val, 1), 2); $value = "{${$valarr[0]}->{$valarr[1]}}"; $value = str_replace('
    ' , ' ', $value); $value = str_replace('
    ' , ' ', $value); $value = format_string($value); } } else { $value = lti_calculate_custom_parameter($value1); } } else { $val = $value; $services = lti_get_services(); foreach ($services as $service) { $service->set_tool_proxy($toolproxy); $service->set_type($tool); $value = $service->parse_value($val); if ($val != $value) { break; } } } } } } return $value; } /** * Calculates the value of a custom parameter that has not been specified earlier * * @param string $value Custom parameter value * * @return string Calculated value of custom parameter */ function lti_calculate_custom_parameter($value) { global $USER, $COURSE; switch ($value) { case 'Moodle.Person.userGroupIds': return implode(",", groups_get_user_groups($COURSE->id, $USER->id)[0]); case 'Context.id.history': return implode(",", get_course_history($COURSE)); case 'CourseSection.timeFrame.begin': if (empty($COURSE->startdate)) { return ""; } $dt = new DateTime("@$COURSE->startdate", new DateTimeZone('UTC')); return $dt->format(DateTime::ATOM); case 'CourseSection.timeFrame.end': if (empty($COURSE->enddate)) { return ""; } $dt = new DateTime("@$COURSE->enddate", new DateTimeZone('UTC')); return $dt->format(DateTime::ATOM); } return null; } /** * Build the history chain for this course using the course originalcourseid. * * @param object $course course for which the history is returned. * * @return array ids of the source course in ancestry order, immediate parent 1st. */ function get_course_history($course) { global $DB; $history = []; $parentid = $course->originalcourseid; while (!empty($parentid) && !in_array($parentid, $history)) { $history[] = $parentid; $parentid = $DB->get_field('course', 'originalcourseid', array('id' => $parentid)); } return $history; } /** * Used for building the names of the different custom parameters * * @param string $key Parameter name * @param bool $tolower Do we want to convert the key into lower case? * @return string Processed name */ function lti_map_keyname($key, $tolower = true) { if ($tolower) { $newkey = ''; $key = core_text::strtolower(trim($key)); foreach (str_split($key) as $ch) { if ( ($ch >= 'a' && $ch <= 'z') || ($ch >= '0' && $ch <= '9') ) { $newkey .= $ch; } else { $newkey .= '_'; } } } else { $newkey = $key; } return $newkey; } /** * Gets the IMS role string for the specified user and LTI course module. * * @param mixed $user User object or user id * @param int $cmid The course module id of the LTI activity * @param int $courseid The course id of the LTI activity * @param boolean $islti2 True if an LTI 2 tool is being launched * * @return string A role string suitable for passing with an LTI launch */ function lti_get_ims_role($user, $cmid, $courseid, $islti2) { $roles = array(); if (empty($cmid)) { // If no cmid is passed, check if the user is a teacher in the course // This allows other modules to programmatically "fake" a launch without // a real LTI instance. $context = context_course::instance($courseid); if (has_capability('moodle/course:manageactivities', $context, $user)) { array_push($roles, 'Instructor'); } else { array_push($roles, 'Learner'); } } else { $context = context_module::instance($cmid); if (has_capability('mod/lti:manage', $context)) { array_push($roles, 'Instructor'); } else { array_push($roles, 'Learner'); } } if (!is_role_switched($courseid) && (is_siteadmin($user)) || has_capability('mod/lti:admin', $context)) { // Make sure admins do not have the Learner role, then set admin role. $roles = array_diff($roles, array('Learner')); if (!$islti2) { array_push($roles, 'urn:lti:sysrole:ims/lis/Administrator', 'urn:lti:instrole:ims/lis/Administrator'); } else { array_push($roles, 'http://purl.imsglobal.org/vocab/lis/v2/person#Administrator'); } } return join(',', $roles); } /** * Returns configuration details for the tool * * @param int $typeid Basic LTI tool typeid * * @return array Tool Configuration */ function lti_get_type_config($typeid) { global $DB; $query = "SELECT name, value FROM {lti_types_config} WHERE typeid = :typeid1 UNION ALL SELECT 'toolurl' AS name, baseurl AS value FROM {lti_types} WHERE id = :typeid2 UNION ALL SELECT 'icon' AS name, icon AS value FROM {lti_types} WHERE id = :typeid3 UNION ALL SELECT 'secureicon' AS name, secureicon AS value FROM {lti_types} WHERE id = :typeid4"; $typeconfig = array(); $configs = $DB->get_records_sql($query, array('typeid1' => $typeid, 'typeid2' => $typeid, 'typeid3' => $typeid, 'typeid4' => $typeid)); if (!empty($configs)) { foreach ($configs as $config) { $typeconfig[$config->name] = $config->value; } } return $typeconfig; } function lti_get_tools_by_url($url, $state, $courseid = null) { $domain = lti_get_domain_from_url($url); return lti_get_tools_by_domain($domain, $state, $courseid); } function lti_get_tools_by_domain($domain, $state = null, $courseid = null) { global $DB, $SITE; $statefilter = ''; $coursefilter = ''; if ($state) { $statefilter = 'AND t.state = :state'; } if ($courseid && $courseid != $SITE->id) { $coursefilter = 'OR t.course = :courseid'; } $coursecategory = $DB->get_field('course', 'category', ['id' => $courseid]); $query = "SELECT t.* FROM {lti_types} t LEFT JOIN {lti_types_categories} tc on t.id = tc.typeid WHERE t.tooldomain = :tooldomain AND (t.course = :siteid $coursefilter) $statefilter AND (tc.id IS NULL OR tc.categoryid = :categoryid)"; return $DB->get_records_sql($query, [ 'courseid' => $courseid, 'siteid' => $SITE->id, 'tooldomain' => $domain, 'state' => $state, 'categoryid' => $coursecategory ]); } /** * Returns all basicLTI tools configured by the administrator * * @param int $course * * @return array */ function lti_filter_get_types($course) { global $DB; if (!empty($course)) { $where = "WHERE t.course = :course"; $params = array('course' => $course); } else { $where = ''; $params = array(); } $query = "SELECT t.id, t.name, t.baseurl, t.state, t.toolproxyid, t.timecreated, tp.name tpname FROM {lti_types} t LEFT OUTER JOIN {lti_tool_proxies} tp ON t.toolproxyid = tp.id {$where}"; return $DB->get_records_sql($query, $params); } /** * Given an array of tools, filter them based on their state * * @param array $tools An array of lti_types records * @param int $state One of the LTI_TOOL_STATE_* constants * @return array */ function lti_filter_tool_types(array $tools, $state) { $return = array(); foreach ($tools as $key => $tool) { if ($tool->state == $state) { $return[$key] = $tool; } } return $return; } /** * Returns all lti types visible in this course * * @deprecated since Moodle 4.3 * @param int $courseid The id of the course to retieve types for * @param array $coursevisible options for 'coursevisible' field, * default [LTI_COURSEVISIBLE_PRECONFIGURED, LTI_COURSEVISIBLE_ACTIVITYCHOOSER] * @return stdClass[] All the lti types visible in the given course */ function lti_get_lti_types_by_course($courseid, $coursevisible = null) { debugging(__FUNCTION__ . '() is deprecated. Please use \mod_lti\local\types_helper::get_lti_types_by_course() instead.', DEBUG_DEVELOPER); global $USER; return \mod_lti\local\types_helper::get_lti_types_by_course($courseid, $USER->id, $coursevisible ?? []); } /** * Returns tool types for lti add instance and edit page * * @return array Array of lti types */ function lti_get_types_for_add_instance() { global $COURSE, $USER; // Always return the 'manual' type option, despite manual config being deprecated, so that we have it for legacy instances. $types = [(object) ['name' => get_string('automatic', 'lti'), 'course' => 0, 'toolproxyid' => null]]; $preconfiguredtypes = \mod_lti\local\types_helper::get_lti_types_by_course($COURSE->id, $USER->id); foreach ($preconfiguredtypes as $type) { $types[$type->id] = $type; } return $types; } /** * Returns a list of configured types in the given course * * @param int $courseid The id of the course to retieve types for * @param int $sectionreturn section to return to for forming the URLs * @return array Array of lti types. Each element is object with properties: name, title, icon, help, helplink, link */ function lti_get_configured_types($courseid, $sectionreturn = 0) { global $OUTPUT, $USER; $types = []; $preconfiguredtypes = \mod_lti\local\types_helper::get_lti_types_by_course($courseid, $USER->id, [LTI_COURSEVISIBLE_ACTIVITYCHOOSER]); foreach ($preconfiguredtypes as $ltitype) { $type = new stdClass(); $type->id = $ltitype->id; $type->modclass = MOD_CLASS_ACTIVITY; $type->name = 'lti_type_' . $ltitype->id; // Clean the name. We don't want tags here. $type->title = clean_param($ltitype->name, PARAM_NOTAGS); $trimmeddescription = trim($ltitype->description ?? ''); if ($trimmeddescription != '') { // Clean the description. We don't want tags here. $type->help = clean_param($trimmeddescription, PARAM_NOTAGS); $type->helplink = get_string('modulename_shortcut_link', 'lti'); } $iconurl = get_tool_type_icon_url($ltitype); $iconclass = ''; if ($iconurl !== $OUTPUT->image_url('monologo', 'lti')->out()) { // Do not filter the icon if it is not the default LTI activity icon. $iconclass = 'nofilter'; } $type->icon = html_writer::empty_tag('img', ['src' => $iconurl, 'alt' => '', 'class' => "icon $iconclass"]); $params = [ 'add' => 'lti', 'return' => 0, 'course' => $courseid, 'typeid' => $ltitype->id, ]; if (!is_null($sectionreturn)) { $params['sr'] = $sectionreturn; } $type->link = new moodle_url('/course/modedit.php', $params); $types[] = $type; } return $types; } function lti_get_domain_from_url($url) { $matches = array(); if (preg_match(LTI_URL_DOMAIN_REGEX, $url ?? '', $matches)) { return $matches[1]; } } function lti_get_tool_by_url_match($url, $courseid = null, $state = LTI_TOOL_STATE_CONFIGURED) { $possibletools = lti_get_tools_by_url($url, $state, $courseid); return lti_get_best_tool_by_url($url, $possibletools, $courseid); } function lti_get_url_thumbprint($url) { // Parse URL requires a schema otherwise everything goes into 'path'. Fixed 5.4.7 or later. if (preg_match('/https?:\/\//', $url) !== 1) { $url = 'http://'.$url; } $urlparts = parse_url(strtolower($url)); if (!isset($urlparts['path'])) { $urlparts['path'] = ''; } if (!isset($urlparts['query'])) { $urlparts['query'] = ''; } if (!isset($urlparts['host'])) { $urlparts['host'] = ''; } if (substr($urlparts['host'], 0, 4) === 'www.') { $urlparts['host'] = substr($urlparts['host'], 4); } $urllower = $urlparts['host'] . '/' . $urlparts['path']; if ($urlparts['query'] != '') { $urllower .= '?' . $urlparts['query']; } return $urllower; } function lti_get_best_tool_by_url($url, $tools, $courseid = null) { if (count($tools) === 0) { return null; } $urllower = lti_get_url_thumbprint($url); foreach ($tools as $tool) { $tool->_matchscore = 0; $toolbaseurllower = lti_get_url_thumbprint($tool->baseurl); if ($urllower === $toolbaseurllower) { // 100 points for exact thumbprint match. $tool->_matchscore += 100; } else if (substr($urllower, 0, strlen($toolbaseurllower)) === $toolbaseurllower) { // 50 points if tool thumbprint starts with the base URL thumbprint. $tool->_matchscore += 50; } // Prefer course tools over site tools. if (!empty($courseid)) { // Minus 10 points for not matching the course id (global tools). if ($tool->course != $courseid) { $tool->_matchscore -= 10; } } } $bestmatch = array_reduce($tools, function($value, $tool) { if ($tool->_matchscore > $value->_matchscore) { return $tool; } else { return $value; } }, (object)array('_matchscore' => -1)); // None of the tools are suitable for this URL. if ($bestmatch->_matchscore <= 0) { return null; } return $bestmatch; } function lti_get_shared_secrets_by_key($key) { global $DB; // Look up the shared secret for the specified key in both the types_config table (for configured tools) // And in the lti resource table for ad-hoc tools. $lti13 = LTI_VERSION_1P3; $query = "SELECT " . $DB->sql_compare_text('t2.value', 256) . " AS value FROM {lti_types_config} t1 JOIN {lti_types_config} t2 ON t1.typeid = t2.typeid JOIN {lti_types} type ON t2.typeid = type.id WHERE t1.name = 'resourcekey' AND " . $DB->sql_compare_text('t1.value', 256) . " = :key1 AND t2.name = 'password' AND type.state = :configured1 AND type.ltiversion <> :ltiversion UNION SELECT tp.secret AS value FROM {lti_tool_proxies} tp JOIN {lti_types} t ON tp.id = t.toolproxyid WHERE tp.guid = :key2 AND t.state = :configured2 UNION SELECT password AS value FROM {lti} WHERE resourcekey = :key3"; $sharedsecrets = $DB->get_records_sql($query, array('configured1' => LTI_TOOL_STATE_CONFIGURED, 'ltiversion' => $lti13, 'configured2' => LTI_TOOL_STATE_CONFIGURED, 'key1' => $key, 'key2' => $key, 'key3' => $key)); $values = array_map(function($item) { return $item->value; }, $sharedsecrets); // There should really only be one shared secret per key. But, we can't prevent // more than one getting entered. For instance, if the same key is used for two tool providers. return $values; } /** * Delete a Basic LTI configuration * * @param int $id Configuration id */ function lti_delete_type($id) { global $DB; // We should probably just copy the launch URL to the tool instances in this case... using a single query. /* $instances = $DB->get_records('lti', array('typeid' => $id)); foreach ($instances as $instance) { $instance->typeid = 0; $DB->update_record('lti', $instance); }*/ $DB->delete_records('lti_types', array('id' => $id)); $DB->delete_records('lti_types_config', array('typeid' => $id)); $DB->delete_records('lti_types_categories', array('typeid' => $id)); } function lti_set_state_for_type($id, $state) { global $DB; $DB->update_record('lti_types', (object)array('id' => $id, 'state' => $state)); } /** * Transforms a basic LTI object to an array * * @param object $ltiobject Basic LTI object * * @return array Basic LTI configuration details */ function lti_get_config($ltiobject) { $typeconfig = (array)$ltiobject; $additionalconfig = lti_get_type_config($ltiobject->typeid); $typeconfig = array_merge($typeconfig, $additionalconfig); return $typeconfig; } /** * * Generates some of the tool configuration based on the instance details * * @param int $id * * @return object configuration * */ function lti_get_type_config_from_instance($id) { global $DB; $instance = $DB->get_record('lti', array('id' => $id)); $config = lti_get_config($instance); $type = new \stdClass(); $type->lti_fix = $id; if (isset($config['toolurl'])) { $type->lti_toolurl = $config['toolurl']; } if (isset($config['instructorchoicesendname'])) { $type->lti_sendname = $config['instructorchoicesendname']; } if (isset($config['instructorchoicesendemailaddr'])) { $type->lti_sendemailaddr = $config['instructorchoicesendemailaddr']; } if (isset($config['instructorchoiceacceptgrades'])) { $type->lti_acceptgrades = $config['instructorchoiceacceptgrades']; } if (isset($config['instructorchoiceallowroster'])) { $type->lti_allowroster = $config['instructorchoiceallowroster']; } if (isset($config['instructorcustomparameters'])) { $type->lti_allowsetting = $config['instructorcustomparameters']; } return $type; } /** * Generates some of the tool configuration based on the admin configuration details * * @param int $id * * @return stdClass Configuration details */ function lti_get_type_type_config($id) { global $DB; $basicltitype = $DB->get_record('lti_types', array('id' => $id)); $config = lti_get_type_config($id); $type = new \stdClass(); $type->lti_typename = $basicltitype->name; $type->typeid = $basicltitype->id; $type->course = $basicltitype->course; $type->toolproxyid = $basicltitype->toolproxyid; $type->lti_toolurl = $basicltitype->baseurl; $type->lti_ltiversion = $basicltitype->ltiversion; $type->lti_clientid = $basicltitype->clientid; $type->lti_clientid_disabled = $type->lti_clientid; $type->lti_description = $basicltitype->description; $type->lti_parameters = $basicltitype->parameter; $type->lti_icon = $basicltitype->icon; $type->lti_secureicon = $basicltitype->secureicon; if (isset($config['resourcekey'])) { $type->lti_resourcekey = $config['resourcekey']; } if (isset($config['password'])) { $type->lti_password = $config['password']; } if (isset($config['publickey'])) { $type->lti_publickey = $config['publickey']; } if (isset($config['publickeyset'])) { $type->lti_publickeyset = $config['publickeyset']; } if (isset($config['keytype'])) { $type->lti_keytype = $config['keytype']; } if (isset($config['initiatelogin'])) { $type->lti_initiatelogin = $config['initiatelogin']; } if (isset($config['redirectionuris'])) { $type->lti_redirectionuris = $config['redirectionuris']; } if (isset($config['sendname'])) { $type->lti_sendname = $config['sendname']; } if (isset($config['instructorchoicesendname'])) { $type->lti_instructorchoicesendname = $config['instructorchoicesendname']; } if (isset($config['sendemailaddr'])) { $type->lti_sendemailaddr = $config['sendemailaddr']; } if (isset($config['instructorchoicesendemailaddr'])) { $type->lti_instructorchoicesendemailaddr = $config['instructorchoicesendemailaddr']; } if (isset($config['acceptgrades'])) { $type->lti_acceptgrades = $config['acceptgrades']; } if (isset($config['instructorchoiceacceptgrades'])) { $type->lti_instructorchoiceacceptgrades = $config['instructorchoiceacceptgrades']; } if (isset($config['allowroster'])) { $type->lti_allowroster = $config['allowroster']; } if (isset($config['instructorchoiceallowroster'])) { $type->lti_instructorchoiceallowroster = $config['instructorchoiceallowroster']; } if (isset($config['customparameters'])) { $type->lti_customparameters = $config['customparameters']; } if (isset($config['forcessl'])) { $type->lti_forcessl = $config['forcessl']; } if (isset($config['organizationid_default'])) { $type->lti_organizationid_default = $config['organizationid_default']; } else { // Tool was configured before this option was available and the default then was host. $type->lti_organizationid_default = LTI_DEFAULT_ORGID_SITEHOST; } if (isset($config['organizationid'])) { $type->lti_organizationid = $config['organizationid']; } if (isset($config['organizationurl'])) { $type->lti_organizationurl = $config['organizationurl']; } if (isset($config['organizationdescr'])) { $type->lti_organizationdescr = $config['organizationdescr']; } if (isset($config['launchcontainer'])) { $type->lti_launchcontainer = $config['launchcontainer']; } if (isset($config['coursevisible'])) { $type->lti_coursevisible = $config['coursevisible']; } if (isset($config['contentitem'])) { $type->lti_contentitem = $config['contentitem']; } if (isset($config['toolurl_ContentItemSelectionRequest'])) { $type->lti_toolurl_ContentItemSelectionRequest = $config['toolurl_ContentItemSelectionRequest']; } if (isset($config['debuglaunch'])) { $type->lti_debuglaunch = $config['debuglaunch']; } if (isset($config['module_class_type'])) { $type->lti_module_class_type = $config['module_class_type']; } // Get the parameters from the LTI services. foreach ($config as $name => $value) { if (strpos($name, 'ltiservice_') === 0) { $type->{$name} = $config[$name]; } } return $type; } function lti_prepare_type_for_save($type, $config) { if (isset($config->lti_toolurl)) { $type->baseurl = $config->lti_toolurl; if (isset($config->lti_tooldomain)) { $type->tooldomain = $config->lti_tooldomain; } else { $type->tooldomain = lti_get_domain_from_url($config->lti_toolurl); } } if (isset($config->lti_description)) { $type->description = $config->lti_description; } if (isset($config->lti_typename)) { $type->name = $config->lti_typename; } if (isset($config->lti_ltiversion)) { $type->ltiversion = $config->lti_ltiversion; } if (isset($config->lti_clientid)) { $type->clientid = $config->lti_clientid; } if ((!empty($type->ltiversion) && $type->ltiversion === LTI_VERSION_1P3) && empty($type->clientid)) { $type->clientid = registration_helper::get()->new_clientid(); } else if (empty($type->clientid)) { $type->clientid = null; } if (isset($config->lti_coursevisible)) { $type->coursevisible = $config->lti_coursevisible; } if (isset($config->lti_icon)) { $type->icon = $config->lti_icon; } if (isset($config->lti_secureicon)) { $type->secureicon = $config->lti_secureicon; } $type->forcessl = !empty($config->lti_forcessl) ? $config->lti_forcessl : 0; $config->lti_forcessl = $type->forcessl; if (isset($config->lti_contentitem)) { $type->contentitem = !empty($config->lti_contentitem) ? $config->lti_contentitem : 0; $config->lti_contentitem = $type->contentitem; } if (isset($config->lti_toolurl_ContentItemSelectionRequest)) { if (!empty($config->lti_toolurl_ContentItemSelectionRequest)) { $type->toolurl_ContentItemSelectionRequest = $config->lti_toolurl_ContentItemSelectionRequest; } else { $type->toolurl_ContentItemSelectionRequest = ''; } $config->lti_toolurl_ContentItemSelectionRequest = $type->toolurl_ContentItemSelectionRequest; } $type->timemodified = time(); unset ($config->lti_typename); unset ($config->lti_toolurl); unset ($config->lti_description); unset ($config->lti_ltiversion); unset ($config->lti_clientid); unset ($config->lti_icon); unset ($config->lti_secureicon); } function lti_update_type($type, $config) { global $DB, $CFG; lti_prepare_type_for_save($type, $config); if (lti_request_is_using_ssl() && !empty($type->secureicon)) { $clearcache = !isset($config->oldicon) || ($config->oldicon !== $type->secureicon); } else { $clearcache = isset($type->icon) && (!isset($config->oldicon) || ($config->oldicon !== $type->icon)); } unset($config->oldicon); if ($DB->update_record('lti_types', $type)) { foreach ($config as $key => $value) { if (substr($key, 0, 4) == 'lti_' && !is_null($value)) { $record = new \StdClass(); $record->typeid = $type->id; $record->name = substr($key, 4); $record->value = $value; lti_update_config($record); } if (substr($key, 0, 11) == 'ltiservice_' && !is_null($value)) { $record = new \StdClass(); $record->typeid = $type->id; $record->name = $key; $record->value = $value; lti_update_config($record); } } if (isset($type->toolproxyid) && $type->ltiversion === LTI_VERSION_1P3) { // We need to remove the tool proxy for this tool to function under 1.3. $toolproxyid = $type->toolproxyid; $DB->delete_records('lti_tool_settings', array('toolproxyid' => $toolproxyid)); $DB->delete_records('lti_tool_proxies', array('id' => $toolproxyid)); $type->toolproxyid = null; $DB->update_record('lti_types', $type); } $DB->delete_records('lti_types_categories', ['typeid' => $type->id]); if (isset($config->lti_coursecategories) && !empty($config->lti_coursecategories)) { lti_type_add_categories($type->id, $config->lti_coursecategories); } require_once($CFG->libdir.'/modinfolib.php'); if ($clearcache) { $sql = "SELECT cm.id, cm.course FROM {course_modules} cm JOIN {modules} m ON cm.module = m.id JOIN {lti} l ON l.course = cm.course WHERE m.name = :name AND l.typeid = :typeid"; $rs = $DB->get_recordset_sql($sql, ['name' => 'lti', 'typeid' => $type->id]); $courseids = []; foreach ($rs as $record) { $courseids[] = $record->course; \course_modinfo::purge_course_module_cache($record->course, $record->id); } $rs->close(); $courseids = array_unique($courseids); foreach ($courseids as $courseid) { rebuild_course_cache($courseid, false, true); } } } } /** * Add LTI Type course category. * * @param int $typeid * @param string $lticoursecategories Comma separated list of course categories. * @return void */ function lti_type_add_categories(int $typeid, string $lticoursecategories = ''): void { global $DB; $coursecategories = explode(',', $lticoursecategories); foreach ($coursecategories as $coursecategory) { $DB->insert_record('lti_types_categories', ['typeid' => $typeid, 'categoryid' => $coursecategory]); } } function lti_add_type($type, $config) { global $USER, $SITE, $DB; lti_prepare_type_for_save($type, $config); if (!isset($type->state)) { $type->state = LTI_TOOL_STATE_PENDING; } if (!isset($type->ltiversion)) { $type->ltiversion = LTI_VERSION_1; } if (!isset($type->timecreated)) { $type->timecreated = time(); } if (!isset($type->createdby)) { $type->createdby = $USER->id; } if (!isset($type->course)) { $type->course = $SITE->id; } // Create a salt value to be used for signing passed data to extension services // The outcome service uses the service salt on the instance. This can be used // for communication with services not related to a specific LTI instance. $config->lti_servicesalt = uniqid('', true); $id = $DB->insert_record('lti_types', $type); if ($id) { foreach ($config as $key => $value) { if (!is_null($value)) { if (substr($key, 0, 4) === 'lti_') { $fieldname = substr($key, 4); } else if (substr($key, 0, 11) !== 'ltiservice_') { continue; } else { $fieldname = $key; } $record = new \StdClass(); $record->typeid = $id; $record->name = $fieldname; $record->value = $value; lti_add_config($record); } } if (isset($config->lti_coursecategories) && !empty($config->lti_coursecategories)) { lti_type_add_categories($id, $config->lti_coursecategories); } } return $id; } /** * Given an array of tool proxies, filter them based on their state * * @param array $toolproxies An array of lti_tool_proxies records * @param int $state One of the LTI_TOOL_PROXY_STATE_* constants * * @return array */ function lti_filter_tool_proxy_types(array $toolproxies, $state) { $return = array(); foreach ($toolproxies as $key => $toolproxy) { if ($toolproxy->state == $state) { $return[$key] = $toolproxy; } } return $return; } /** * Get the tool proxy instance given its GUID * * @param string $toolproxyguid Tool proxy GUID value * * @return object */ function lti_get_tool_proxy_from_guid($toolproxyguid) { global $DB; $toolproxy = $DB->get_record('lti_tool_proxies', array('guid' => $toolproxyguid)); return $toolproxy; } /** * Get the tool proxy instance given its registration URL * * @param string $regurl Tool proxy registration URL * * @return array The record of the tool proxy with this url */ function lti_get_tool_proxies_from_registration_url($regurl) { global $DB; return $DB->get_records_sql( 'SELECT * FROM {lti_tool_proxies} WHERE '.$DB->sql_compare_text('regurl', 256).' = :regurl', array('regurl' => $regurl) ); } /** * Generates some of the tool proxy configuration based on the admin configuration details * * @param int $id * * @return mixed Tool Proxy details */ function lti_get_tool_proxy($id) { global $DB; $toolproxy = $DB->get_record('lti_tool_proxies', array('id' => $id)); return $toolproxy; } /** * Returns lti tool proxies. * * @param bool $orphanedonly Only retrieves tool proxies that have no type associated with them * @return array of basicLTI types */ function lti_get_tool_proxies($orphanedonly) { global $DB; if ($orphanedonly) { $usedproxyids = array_values($DB->get_fieldset_select('lti_types', 'toolproxyid', 'toolproxyid IS NOT NULL')); $proxies = $DB->get_records('lti_tool_proxies', null, 'state DESC, timemodified DESC'); foreach ($proxies as $key => $value) { if (in_array($value->id, $usedproxyids)) { unset($proxies[$key]); } } return $proxies; } else { return $DB->get_records('lti_tool_proxies', null, 'state DESC, timemodified DESC'); } } /** * Generates some of the tool proxy configuration based on the admin configuration details * * @param int $id * * @return mixed Tool Proxy details */ function lti_get_tool_proxy_config($id) { $toolproxy = lti_get_tool_proxy($id); $tp = new \stdClass(); $tp->lti_registrationname = $toolproxy->name; $tp->toolproxyid = $toolproxy->id; $tp->state = $toolproxy->state; $tp->lti_registrationurl = $toolproxy->regurl; $tp->lti_capabilities = explode("\n", $toolproxy->capabilityoffered); $tp->lti_services = explode("\n", $toolproxy->serviceoffered); return $tp; } /** * Update the database with a tool proxy instance * * @param object $config Tool proxy definition * * @return int Record id number */ function lti_add_tool_proxy($config) { global $USER, $DB; $toolproxy = new \stdClass(); if (isset($config->lti_registrationname)) { $toolproxy->name = trim($config->lti_registrationname); } if (isset($config->lti_registrationurl)) { $toolproxy->regurl = trim($config->lti_registrationurl); } if (isset($config->lti_capabilities)) { $toolproxy->capabilityoffered = implode("\n", $config->lti_capabilities); } else { $toolproxy->capabilityoffered = implode("\n", array_keys(lti_get_capabilities())); } if (isset($config->lti_services)) { $toolproxy->serviceoffered = implode("\n", $config->lti_services); } else { $func = function($s) { return $s->get_id(); }; $servicenames = array_map($func, lti_get_services()); $toolproxy->serviceoffered = implode("\n", $servicenames); } if (isset($config->toolproxyid) && !empty($config->toolproxyid)) { $toolproxy->id = $config->toolproxyid; if (!isset($toolproxy->state) || ($toolproxy->state != LTI_TOOL_PROXY_STATE_ACCEPTED)) { $toolproxy->state = LTI_TOOL_PROXY_STATE_CONFIGURED; $toolproxy->guid = random_string(); $toolproxy->secret = random_string(); } $id = lti_update_tool_proxy($toolproxy); } else { $toolproxy->state = LTI_TOOL_PROXY_STATE_CONFIGURED; $toolproxy->timemodified = time(); $toolproxy->timecreated = $toolproxy->timemodified; if (!isset($toolproxy->createdby)) { $toolproxy->createdby = $USER->id; } $toolproxy->guid = random_string(); $toolproxy->secret = random_string(); $id = $DB->insert_record('lti_tool_proxies', $toolproxy); } return $id; } /** * Updates a tool proxy in the database * * @param object $toolproxy Tool proxy * * @return int Record id number */ function lti_update_tool_proxy($toolproxy) { global $DB; $toolproxy->timemodified = time(); $id = $DB->update_record('lti_tool_proxies', $toolproxy); return $id; } /** * Delete a Tool Proxy * * @param int $id Tool Proxy id */ function lti_delete_tool_proxy($id) { global $DB; $DB->delete_records('lti_tool_settings', array('toolproxyid' => $id)); $tools = $DB->get_records('lti_types', array('toolproxyid' => $id)); foreach ($tools as $tool) { lti_delete_type($tool->id); } $DB->delete_records('lti_tool_proxies', array('id' => $id)); } /** * Get both LTI tool proxies and tool types. * * If limit and offset are not zero, a subset of the tools will be returned. Tool proxies will be counted before tool * types. * For example: If 10 tool proxies and 10 tool types exist, and the limit is set to 15, then 10 proxies and 5 types * will be returned. * * @param int $limit Maximum number of tools returned. * @param int $offset Do not return tools before offset index. * @param bool $orphanedonly If true, only return orphaned proxies. * @param int $toolproxyid If not 0, only return tool types that have this tool proxy id. * @return array list(proxies[], types[]) List containing array of tool proxies and array of tool types. */ function lti_get_lti_types_and_proxies(int $limit = 0, int $offset = 0, bool $orphanedonly = false, int $toolproxyid = 0): array { global $DB; if ($orphanedonly) { $orphanedproxiessql = helper::get_tool_proxy_sql($orphanedonly, false); $countsql = helper::get_tool_proxy_sql($orphanedonly, true); $proxies = $DB->get_records_sql($orphanedproxiessql, null, $offset, $limit); $totalproxiescount = $DB->count_records_sql($countsql); } else { $proxies = $DB->get_records('lti_tool_proxies', null, 'name ASC, state DESC, timemodified DESC', '*', $offset, $limit); $totalproxiescount = $DB->count_records('lti_tool_proxies'); } // Find new offset and limit for tool types after getting proxies and set up query. $typesoffset = max($offset - $totalproxiescount, 0); // Set to 0 if negative. $typeslimit = max($limit - count($proxies), 0); // Set to 0 if negative. $typesparams = []; if (!empty($toolproxyid)) { $typesparams['toolproxyid'] = $toolproxyid; } $types = $DB->get_records('lti_types', $typesparams, 'name ASC, state DESC, timemodified DESC', '*', $typesoffset, $typeslimit); return [$proxies, array_map('serialise_tool_type', $types)]; } /** * Get the total number of LTI tool types and tool proxies. * * @param bool $orphanedonly If true, only count orphaned proxies. * @param int $toolproxyid If not 0, only count tool types that have this tool proxy id. * @return int Count of tools. */ function lti_get_lti_types_and_proxies_count(bool $orphanedonly = false, int $toolproxyid = 0): int { global $DB; $typessql = "SELECT count(*) FROM {lti_types}"; $typesparams = []; if (!empty($toolproxyid)) { $typessql .= " WHERE toolproxyid = :toolproxyid"; $typesparams['toolproxyid'] = $toolproxyid; } $proxiessql = helper::get_tool_proxy_sql($orphanedonly, true); $countsql = "SELECT ($typessql) + ($proxiessql) as total" . $DB->sql_null_from_clause(); return $DB->count_records_sql($countsql, $typesparams); } /** * Add a tool configuration in the database * * @param object $config Tool configuration * * @return int Record id number */ function lti_add_config($config) { global $DB; return $DB->insert_record('lti_types_config', $config); } /** * Updates a tool configuration in the database * * @param object $config Tool configuration * * @return mixed Record id number */ function lti_update_config($config) { global $DB; $old = $DB->get_record('lti_types_config', array('typeid' => $config->typeid, 'name' => $config->name)); if ($old) { $config->id = $old->id; $return = $DB->update_record('lti_types_config', $config); } else { $return = $DB->insert_record('lti_types_config', $config); } return $return; } /** * Gets the tool settings * * @param int $toolproxyid Id of tool proxy record (or tool ID if negative) * @param int $courseid Id of course (null if system settings) * @param int $instanceid Id of course module (null if system or context settings) * * @return array Array settings */ function lti_get_tool_settings($toolproxyid, $courseid = null, $instanceid = null) { global $DB; $settings = array(); if ($toolproxyid > 0) { $settingsstr = $DB->get_field('lti_tool_settings', 'settings', array('toolproxyid' => $toolproxyid, 'course' => $courseid, 'coursemoduleid' => $instanceid)); } else { $settingsstr = $DB->get_field('lti_tool_settings', 'settings', array('typeid' => -$toolproxyid, 'course' => $courseid, 'coursemoduleid' => $instanceid)); } if ($settingsstr !== false) { $settings = json_decode($settingsstr, true); } return $settings; } /** * Sets the tool settings ( * * @param array $settings Array of settings * @param int $toolproxyid Id of tool proxy record (or tool ID if negative) * @param int $courseid Id of course (null if system settings) * @param int $instanceid Id of course module (null if system or context settings) */ function lti_set_tool_settings($settings, $toolproxyid, $courseid = null, $instanceid = null) { global $DB; $json = json_encode($settings); if ($toolproxyid >= 0) { $record = $DB->get_record('lti_tool_settings', array('toolproxyid' => $toolproxyid, 'course' => $courseid, 'coursemoduleid' => $instanceid)); } else { $record = $DB->get_record('lti_tool_settings', array('typeid' => -$toolproxyid, 'course' => $courseid, 'coursemoduleid' => $instanceid)); } if ($record !== false) { $DB->update_record('lti_tool_settings', (object)array('id' => $record->id, 'settings' => $json, 'timemodified' => time())); } else { $record = new \stdClass(); if ($toolproxyid > 0) { $record->toolproxyid = $toolproxyid; } else { $record->typeid = -$toolproxyid; } $record->course = $courseid; $record->coursemoduleid = $instanceid; $record->settings = $json; $record->timecreated = time(); $record->timemodified = $record->timecreated; $DB->insert_record('lti_tool_settings', $record); } } /** * Signs the petition to launch the external tool using OAuth * * @param array $oldparms Parameters to be passed for signing * @param string $endpoint url of the external tool * @param string $method Method for sending the parameters (e.g. POST) * @param string $oauthconsumerkey * @param string $oauthconsumersecret * @return array|null */ function lti_sign_parameters($oldparms, $endpoint, $method, $oauthconsumerkey, $oauthconsumersecret) { $parms = $oldparms; $testtoken = ''; // TODO: Switch to core oauthlib once implemented - MDL-30149. $hmacmethod = new lti\OAuthSignatureMethod_HMAC_SHA1(); $testconsumer = new lti\OAuthConsumer($oauthconsumerkey, $oauthconsumersecret, null); $accreq = lti\OAuthRequest::from_consumer_and_token($testconsumer, $testtoken, $method, $endpoint, $parms); $accreq->sign_request($hmacmethod, $testconsumer, $testtoken); $newparms = $accreq->get_parameters(); return $newparms; } /** * Converts the message paramters to their equivalent JWT claim and signs the payload to launch the external tool using JWT * * @param array $parms Parameters to be passed for signing * @param string $endpoint url of the external tool * @param string $oauthconsumerkey * @param string $typeid ID of LTI tool type * @param string $nonce Nonce value to use * @return array|null */ function lti_sign_jwt($parms, $endpoint, $oauthconsumerkey, $typeid = 0, $nonce = '') { global $CFG; if (empty($typeid)) { $typeid = 0; } $messagetypemapping = lti_get_jwt_message_type_mapping(); if (isset($parms['lti_message_type']) && array_key_exists($parms['lti_message_type'], $messagetypemapping)) { $parms['lti_message_type'] = $messagetypemapping[$parms['lti_message_type']]; } if (isset($parms['roles'])) { $roles = explode(',', $parms['roles']); $newroles = array(); foreach ($roles as $role) { if (strpos($role, 'urn:lti:role:ims/lis/') === 0) { $role = 'http://purl.imsglobal.org/vocab/lis/v2/membership#' . substr($role, 21); } else if (strpos($role, 'urn:lti:instrole:ims/lis/') === 0) { $role = 'http://purl.imsglobal.org/vocab/lis/v2/institution/person#' . substr($role, 25); } else if (strpos($role, 'urn:lti:sysrole:ims/lis/') === 0) { $role = 'http://purl.imsglobal.org/vocab/lis/v2/system/person#' . substr($role, 24); } else if ((strpos($role, '://') === false) && (strpos($role, 'urn:') !== 0)) { $role = "http://purl.imsglobal.org/vocab/lis/v2/membership#{$role}"; } $newroles[] = $role; } $parms['roles'] = implode(',', $newroles); } $now = time(); if (empty($nonce)) { $nonce = bin2hex(openssl_random_pseudo_bytes(10)); } $claimmapping = lti_get_jwt_claim_mapping(); $payload = array( 'nonce' => $nonce, 'iat' => $now, 'exp' => $now + 60, ); $payload['iss'] = $CFG->wwwroot; $payload['aud'] = $oauthconsumerkey; $payload[LTI_JWT_CLAIM_PREFIX . '/claim/deployment_id'] = strval($typeid); $payload[LTI_JWT_CLAIM_PREFIX . '/claim/target_link_uri'] = $endpoint; foreach ($parms as $key => $value) { $claim = LTI_JWT_CLAIM_PREFIX; if (array_key_exists($key, $claimmapping)) { $mapping = $claimmapping[$key]; $type = $mapping["type"] ?? "string"; if ($mapping['isarray']) { $value = explode(',', $value); sort($value); } else if ($type == 'boolean') { $value = isset($value) && ($value == 'true'); } if (!empty($mapping['suffix'])) { $claim .= "-{$mapping['suffix']}"; } $claim .= '/claim/'; if (is_null($mapping['group'])) { $payload[$mapping['claim']] = $value; } else if (empty($mapping['group'])) { $payload["{$claim}{$mapping['claim']}"] = $value; } else { $claim .= $mapping['group']; $payload[$claim][$mapping['claim']] = $value; } } else if (strpos($key, 'custom_') === 0) { $payload["{$claim}/claim/custom"][substr($key, 7)] = $value; } else if (strpos($key, 'ext_') === 0) { $payload["{$claim}/claim/ext"][substr($key, 4)] = $value; } } $privatekey = jwks_helper::get_private_key(); $jwt = JWT::encode($payload, $privatekey['key'], 'RS256', $privatekey['kid']); $newparms = array(); $newparms['id_token'] = $jwt; return $newparms; } /** * Verfies the JWT and converts its claims to their equivalent message parameter. * * @param int $typeid * @param string $jwtparam JWT parameter * * @return array message parameters * @throws moodle_exception */ function lti_convert_from_jwt($typeid, $jwtparam) { $params = array(); $parts = explode('.', $jwtparam); $ok = (count($parts) === 3); if ($ok) { $payload = JWT::urlsafeB64Decode($parts[1]); $claims = json_decode($payload, true); $ok = !is_null($claims) && !empty($claims['iss']); } if ($ok) { lti_verify_jwt_signature($typeid, $claims['iss'], $jwtparam); $params['oauth_consumer_key'] = $claims['iss']; foreach (lti_get_jwt_claim_mapping() as $key => $mapping) { $claim = LTI_JWT_CLAIM_PREFIX; if (!empty($mapping['suffix'])) { $claim .= "-{$mapping['suffix']}"; } $claim .= '/claim/'; if (is_null($mapping['group'])) { $claim = $mapping['claim']; } else if (empty($mapping['group'])) { $claim .= $mapping['claim']; } else { $claim .= $mapping['group']; } if (isset($claims[$claim])) { $value = null; if (empty($mapping['group'])) { $value = $claims[$claim]; } else { $group = $claims[$claim]; if (is_array($group) && array_key_exists($mapping['claim'], $group)) { $value = $group[$mapping['claim']]; } } if (!empty($value) && $mapping['isarray']) { if (is_array($value)) { if (is_array($value[0])) { $value = json_encode($value); } else { $value = implode(',', $value); } } } if (!is_null($value) && is_string($value) && (strlen($value) > 0)) { $params[$key] = $value; } } $claim = LTI_JWT_CLAIM_PREFIX . '/claim/custom'; if (isset($claims[$claim])) { $custom = $claims[$claim]; if (is_array($custom)) { foreach ($custom as $key => $value) { $params["custom_{$key}"] = $value; } } } $claim = LTI_JWT_CLAIM_PREFIX . '/claim/ext'; if (isset($claims[$claim])) { $ext = $claims[$claim]; if (is_array($ext)) { foreach ($ext as $key => $value) { $params["ext_{$key}"] = $value; } } } } } if (isset($params['content_items'])) { $params['content_items'] = lti_convert_content_items($params['content_items']); } $messagetypemapping = lti_get_jwt_message_type_mapping(); if (isset($params['lti_message_type']) && array_key_exists($params['lti_message_type'], $messagetypemapping)) { $params['lti_message_type'] = $messagetypemapping[$params['lti_message_type']]; } return $params; } /** * Posts the launch petition HTML * * @param array $newparms Signed parameters * @param string $endpoint URL of the external tool * @param bool $debug Debug (true/false) * @return string */ function lti_post_launch_html($newparms, $endpoint, $debug=false) { $r = "
    \n"; // Contruct html for the launch parameters. foreach ($newparms as $key => $value) { $key = htmlspecialchars($key, ENT_COMPAT); $value = htmlspecialchars($value, ENT_COMPAT); if ( $key == "ext_submit" ) { $r .= "\n"; } if ( $debug ) { $r .= "\n"; $r .= ""; $r .= get_string("toggle_debug_data", "lti")."\n"; $r .= "
    \n"; $r .= "".get_string("basiclti_endpoint", "lti")."
    \n"; $r .= $endpoint . "
    \n 
    \n"; $r .= "".get_string("basiclti_parameters", "lti")."
    \n"; foreach ($newparms as $key => $value) { $key = htmlspecialchars($key, ENT_COMPAT); $value = htmlspecialchars($value, ENT_COMPAT); $r .= "$key = $value
    \n"; } $r .= " 
    \n"; $r .= "
    \n"; } $r .= "
    \n"; // Auto-submit the form if endpoint is set. if ($endpoint !== '' && !$debug) { $r .= " \n"; } return $r; } /** * Generate the form for initiating a login request for an LTI 1.3 message * * @param int $courseid Course ID * @param int $cmid LTI instance ID * @param stdClass|null $instance LTI instance * @param stdClass $config Tool type configuration * @param string $messagetype LTI message type * @param string $title Title of content item * @param string $text Description of content item * @param int $foruserid Id of the user targeted by the launch * @return string */ function lti_initiate_login($courseid, $cmid, $instance, $config, $messagetype = 'basic-lti-launch-request', $title = '', $text = '', $foruserid = 0) { global $SESSION; $params = lti_build_login_request($courseid, $cmid, $instance, $config, $messagetype, $foruserid, $title, $text); $r = "
    lti_initiatelogin . "\" name=\"ltiInitiateLoginForm\" id=\"ltiInitiateLoginForm\" method=\"post\" " . "encType=\"application/x-www-form-urlencoded\">\n"; foreach ($params as $key => $value) { $key = htmlspecialchars($key, ENT_COMPAT); $value = htmlspecialchars($value, ENT_COMPAT); $r .= " \n"; } $r .= "
    \n"; $r .= "\n"; return $r; } /** * Prepares an LTI 1.3 login request * * @param int $courseid Course ID * @param int $cmid Course Module instance ID * @param stdClass|null $instance LTI instance * @param stdClass $config Tool type configuration * @param string $messagetype LTI message type * @param int $foruserid Id of the user targeted by the launch * @param string $title Title of content item * @param string $text Description of content item * @return array Login request parameters */ function lti_build_login_request($courseid, $cmid, $instance, $config, $messagetype, $foruserid=0, $title = '', $text = '') { global $USER, $CFG, $SESSION; $ltihint = []; if (!empty($instance)) { $endpoint = !empty($instance->toolurl) ? $instance->toolurl : $config->lti_toolurl; $launchid = 'ltilaunch'.$instance->id.'_'.rand(); $ltihint['cmid'] = $cmid; $SESSION->$launchid = "{$courseid},{$config->typeid},{$cmid},{$messagetype},{$foruserid},,"; } else { $endpoint = $config->lti_toolurl; if (($messagetype === 'ContentItemSelectionRequest') && !empty($config->lti_toolurl_ContentItemSelectionRequest)) { $endpoint = $config->lti_toolurl_ContentItemSelectionRequest; } $launchid = "ltilaunch_$messagetype".rand(); $SESSION->$launchid = "{$courseid},{$config->typeid},,{$messagetype},{$foruserid}," . base64_encode($title) . ',' . base64_encode($text); } $endpoint = trim($endpoint); $services = lti_get_services(); foreach ($services as $service) { [$endpoint] = $service->override_endpoint($messagetype ?? 'basic-lti-launch-request', $endpoint, '', $courseid, $instance); } $ltihint['launchid'] = $launchid; // If SSL is forced make sure https is on the normal launch URL. if (isset($config->lti_forcessl) && ($config->lti_forcessl == '1')) { $endpoint = lti_ensure_url_is_https($endpoint); } else if (!strstr($endpoint, '://')) { $endpoint = 'http://' . $endpoint; } $params = array(); $params['iss'] = $CFG->wwwroot; $params['target_link_uri'] = $endpoint; $params['login_hint'] = $USER->id; $params['lti_message_hint'] = json_encode($ltihint); $params['client_id'] = $config->lti_clientid; $params['lti_deployment_id'] = $config->typeid; return $params; } function lti_get_type($typeid) { global $DB; return $DB->get_record('lti_types', array('id' => $typeid)); } function lti_get_launch_container($lti, $toolconfig) { if (empty($lti->launchcontainer)) { $lti->launchcontainer = LTI_LAUNCH_CONTAINER_DEFAULT; } if ($lti->launchcontainer == LTI_LAUNCH_CONTAINER_DEFAULT) { if (isset($toolconfig['launchcontainer'])) { $launchcontainer = $toolconfig['launchcontainer']; } } else { $launchcontainer = $lti->launchcontainer; } if (empty($launchcontainer) || $launchcontainer == LTI_LAUNCH_CONTAINER_DEFAULT) { $launchcontainer = LTI_LAUNCH_CONTAINER_EMBED_NO_BLOCKS; } $devicetype = core_useragent::get_device_type(); // Scrolling within the object element doesn't work on iOS or Android // Opening the popup window also had some issues in testing // For mobile devices, always take up the entire screen to ensure the best experience. if ($devicetype === core_useragent::DEVICETYPE_MOBILE || $devicetype === core_useragent::DEVICETYPE_TABLET ) { $launchcontainer = LTI_LAUNCH_CONTAINER_REPLACE_MOODLE_WINDOW; } return $launchcontainer; } function lti_request_is_using_ssl() { global $CFG; return (stripos($CFG->wwwroot, 'https://') === 0); } function lti_ensure_url_is_https($url) { if (!strstr($url, '://')) { $url = 'https://' . $url; } else { // If the URL starts with http, replace with https. if (stripos($url, 'http://') === 0) { $url = 'https://' . substr($url, 7); } } return $url; } /** * Determines if we should try to log the request * * @param string $rawbody * @return bool */ function lti_should_log_request($rawbody) { global $CFG; if (empty($CFG->mod_lti_log_users)) { return false; } $logusers = explode(',', $CFG->mod_lti_log_users); if (empty($logusers)) { return false; } try { $xml = new \SimpleXMLElement($rawbody); $ns = $xml->getNamespaces(); $ns = array_shift($ns); $xml->registerXPathNamespace('lti', $ns); $requestuserid = ''; if ($node = $xml->xpath('//lti:userId')) { $node = $node[0]; $requestuserid = clean_param((string) $node, PARAM_INT); } else if ($node = $xml->xpath('//lti:sourcedId')) { $node = $node[0]; $resultjson = json_decode((string) $node); $requestuserid = clean_param($resultjson->data->userid, PARAM_INT); } } catch (Exception $e) { return false; } if (empty($requestuserid) or !in_array($requestuserid, $logusers)) { return false; } return true; } /** * Logs the request to a file in temp dir. * * @param string $rawbody */ function lti_log_request($rawbody) { if ($tempdir = make_temp_directory('mod_lti', false)) { if ($tempfile = tempnam($tempdir, 'mod_lti_request'.date('YmdHis'))) { $content = "Request Headers:\n"; foreach (moodle\mod\lti\OAuthUtil::get_headers() as $header => $value) { $content .= "$header: $value\n"; } $content .= "Request Body:\n"; $content .= $rawbody; file_put_contents($tempfile, $content); chmod($tempfile, 0644); } } } /** * Log an LTI response. * * @param string $responsexml The response XML * @param Exception $e If there was an exception, pass that too */ function lti_log_response($responsexml, $e = null) { if ($tempdir = make_temp_directory('mod_lti', false)) { if ($tempfile = tempnam($tempdir, 'mod_lti_response'.date('YmdHis'))) { $content = ''; if ($e instanceof Exception) { $info = get_exception_info($e); $content .= "Exception:\n"; $content .= "Message: $info->message\n"; $content .= "Debug info: $info->debuginfo\n"; $content .= "Backtrace:\n"; $content .= format_backtrace($info->backtrace, true); $content .= "\n"; } $content .= "Response XML:\n"; $content .= $responsexml; file_put_contents($tempfile, $content); chmod($tempfile, 0644); } } } /** * Fetches LTI type configuration for an LTI instance * * @param stdClass $instance * @return array Can be empty if no type is found */ function lti_get_type_config_by_instance($instance) { $typeid = null; if (empty($instance->typeid)) { $tool = lti_get_tool_by_url_match($instance->toolurl, $instance->course); if ($tool) { $typeid = $tool->id; } } else { $typeid = $instance->typeid; } if (!empty($typeid)) { return lti_get_type_config($typeid); } return array(); } /** * Enforce type config settings onto the LTI instance * * @param stdClass $instance * @param array $typeconfig */ function lti_force_type_config_settings($instance, array $typeconfig) { $forced = array( 'instructorchoicesendname' => 'sendname', 'instructorchoicesendemailaddr' => 'sendemailaddr', 'instructorchoiceacceptgrades' => 'acceptgrades', ); foreach ($forced as $instanceparam => $typeconfigparam) { if (array_key_exists($typeconfigparam, $typeconfig) && $typeconfig[$typeconfigparam] != LTI_SETTING_DELEGATE) { $instance->$instanceparam = $typeconfig[$typeconfigparam]; } } } /** * Initializes an array with the capabilities supported by the LTI module * * @return array List of capability names (without a dollar sign prefix) */ function lti_get_capabilities() { $capabilities = array( 'basic-lti-launch-request' => '', 'ContentItemSelectionRequest' => '', 'ToolProxyRegistrationRequest' => '', 'Context.id' => 'context_id', 'Context.title' => 'context_title', 'Context.label' => 'context_label', 'Context.id.history' => null, 'Context.sourcedId' => 'lis_course_section_sourcedid', 'Context.longDescription' => '$COURSE->summary', 'Context.timeFrame.begin' => '$COURSE->startdate', 'CourseSection.title' => 'context_title', 'CourseSection.label' => 'context_label', 'CourseSection.sourcedId' => 'lis_course_section_sourcedid', 'CourseSection.longDescription' => '$COURSE->summary', 'CourseSection.timeFrame.begin' => null, 'CourseSection.timeFrame.end' => null, 'ResourceLink.id' => 'resource_link_id', 'ResourceLink.title' => 'resource_link_title', 'ResourceLink.description' => 'resource_link_description', 'User.id' => 'user_id', 'User.username' => '$USER->username', 'Person.name.full' => 'lis_person_name_full', 'Person.name.given' => 'lis_person_name_given', 'Person.name.family' => 'lis_person_name_family', 'Person.email.primary' => 'lis_person_contact_email_primary', 'Person.sourcedId' => 'lis_person_sourcedid', 'Person.name.middle' => '$USER->middlename', 'Person.address.street1' => '$USER->address', 'Person.address.locality' => '$USER->city', 'Person.address.country' => '$USER->country', 'Person.address.timezone' => '$USER->timezone', 'Person.phone.primary' => '$USER->phone1', 'Person.phone.mobile' => '$USER->phone2', 'Person.webaddress' => '$USER->url', 'Membership.role' => 'roles', 'Result.sourcedId' => 'lis_result_sourcedid', 'Result.autocreate' => 'lis_outcome_service_url', 'BasicOutcome.sourcedId' => 'lis_result_sourcedid', 'BasicOutcome.url' => 'lis_outcome_service_url', 'Moodle.Person.userGroupIds' => null); return $capabilities; } /** * Initializes an array with the services supported by the LTI module * * @return array List of services */ function lti_get_services() { $services = array(); $definedservices = core_component::get_plugin_list('ltiservice'); foreach ($definedservices as $name => $location) { $classname = "\\ltiservice_{$name}\\local\\service\\{$name}"; $services[] = new $classname(); } return $services; } /** * Initializes an instance of the named service * * @param string $servicename Name of service * * @return bool|\mod_lti\local\ltiservice\service_base Service */ function lti_get_service_by_name($servicename) { $service = false; $classname = "\\ltiservice_{$servicename}\\local\\service\\{$servicename}"; if (class_exists($classname)) { $service = new $classname(); } return $service; } /** * Finds a service by id * * @param \mod_lti\local\ltiservice\service_base[] $services Array of services * @param string $resourceid ID of resource * * @return mod_lti\local\ltiservice\service_base Service */ function lti_get_service_by_resource_id($services, $resourceid) { $service = false; foreach ($services as $aservice) { foreach ($aservice->get_resources() as $resource) { if ($resource->get_id() === $resourceid) { $service = $aservice; break 2; } } } return $service; } /** * Initializes an array with the scopes for services supported by the LTI module * and authorized for this particular tool instance. * * @param object $type LTI tool type * @param array $typeconfig LTI tool type configuration * * @return array List of scopes */ function lti_get_permitted_service_scopes($type, $typeconfig) { $services = lti_get_services(); $scopes = array(); foreach ($services as $service) { $service->set_type($type); $service->set_typeconfig($typeconfig); $servicescopes = $service->get_permitted_scopes(); if (!empty($servicescopes)) { $scopes = array_merge($scopes, $servicescopes); } } return $scopes; } /** * Extracts the named contexts from a tool proxy * * @param object $json * * @return array Contexts */ function lti_get_contexts($json) { $contexts = array(); if (isset($json->{'@context'})) { foreach ($json->{'@context'} as $context) { if (is_object($context)) { $contexts = array_merge(get_object_vars($context), $contexts); } } } return $contexts; } /** * Converts an ID to a fully-qualified ID * * @param array $contexts * @param string $id * * @return string Fully-qualified ID */ function lti_get_fqid($contexts, $id) { $parts = explode(':', $id, 2); if (count($parts) > 1) { if (array_key_exists($parts[0], $contexts)) { $id = $contexts[$parts[0]] . $parts[1]; } } return $id; } /** * Returns the icon for the given tool type * * @param stdClass $type The tool type * * @return string The url to the tool type's corresponding icon */ function get_tool_type_icon_url(stdClass $type) { global $OUTPUT; $iconurl = $type->secureicon; if (empty($iconurl)) { $iconurl = $type->icon; } if (empty($iconurl)) { $iconurl = $OUTPUT->image_url('monologo', 'lti')->out(); } return $iconurl; } /** * Returns the edit url for the given tool type * * @param stdClass $type The tool type * * @return string The url to edit the tool type */ function get_tool_type_edit_url(stdClass $type) { $url = new moodle_url('/mod/lti/typessettings.php', array('action' => 'update', 'id' => $type->id, 'sesskey' => sesskey(), 'returnto' => 'toolconfigure')); return $url->out(); } /** * Returns the edit url for the given tool proxy. * * @param stdClass $proxy The tool proxy * * @return string The url to edit the tool type */ function get_tool_proxy_edit_url(stdClass $proxy) { $url = new moodle_url('/mod/lti/registersettings.php', array('action' => 'update', 'id' => $proxy->id, 'sesskey' => sesskey(), 'returnto' => 'toolconfigure')); return $url->out(); } /** * Returns the course url for the given tool type * * @param stdClass $type The tool type * * @return string The url to the course of the tool type, void if it is a site wide type */ function get_tool_type_course_url(stdClass $type) { if ($type->course != 1) { $url = new moodle_url('/course/view.php', array('id' => $type->course)); return $url->out(); } return null; } /** * Returns the icon and edit urls for the tool type and the course url if it is a course type. * * @param stdClass $type The tool type * * @return array The urls of the tool type */ function get_tool_type_urls(stdClass $type) { $courseurl = get_tool_type_course_url($type); $urls = array( 'icon' => get_tool_type_icon_url($type), 'edit' => get_tool_type_edit_url($type), ); if ($courseurl) { $urls['course'] = $courseurl; } $url = new moodle_url('/mod/lti/certs.php'); $urls['publickeyset'] = $url->out(); $url = new moodle_url('/mod/lti/token.php'); $urls['accesstoken'] = $url->out(); $url = new moodle_url('/mod/lti/auth.php'); $urls['authrequest'] = $url->out(); return $urls; } /** * Returns the icon and edit urls for the tool proxy. * * @param stdClass $proxy The tool proxy * * @return array The urls of the tool proxy */ function get_tool_proxy_urls(stdClass $proxy) { global $OUTPUT; $urls = array( 'icon' => $OUTPUT->image_url('monologo', 'lti')->out(), 'edit' => get_tool_proxy_edit_url($proxy), ); return $urls; } /** * Returns information on the current state of the tool type * * @param stdClass $type The tool type * * @return array An array with a text description of the state, and boolean for whether it is in each state: * pending, configured, rejected, unknown */ function get_tool_type_state_info(stdClass $type) { $isconfigured = false; $ispending = false; $isrejected = false; $isunknown = false; switch ($type->state) { case LTI_TOOL_STATE_CONFIGURED: $state = get_string('active', 'mod_lti'); $isconfigured = true; break; case LTI_TOOL_STATE_PENDING: $state = get_string('pending', 'mod_lti'); $ispending = true; break; case LTI_TOOL_STATE_REJECTED: $state = get_string('rejected', 'mod_lti'); $isrejected = true; break; default: $state = get_string('unknownstate', 'mod_lti'); $isunknown = true; break; } return array( 'text' => $state, 'pending' => $ispending, 'configured' => $isconfigured, 'rejected' => $isrejected, 'unknown' => $isunknown ); } /** * Returns information on the configuration of the tool type * * @param stdClass $type The tool type * * @return array An array with configuration details */ function get_tool_type_config($type) { global $CFG; $platformid = $CFG->wwwroot; $clientid = $type->clientid; $deploymentid = $type->id; $publickeyseturl = new moodle_url('/mod/lti/certs.php'); $publickeyseturl = $publickeyseturl->out(); $accesstokenurl = new moodle_url('/mod/lti/token.php'); $accesstokenurl = $accesstokenurl->out(); $authrequesturl = new moodle_url('/mod/lti/auth.php'); $authrequesturl = $authrequesturl->out(); return array( 'platformid' => $platformid, 'clientid' => $clientid, 'deploymentid' => $deploymentid, 'publickeyseturl' => $publickeyseturl, 'accesstokenurl' => $accesstokenurl, 'authrequesturl' => $authrequesturl ); } /** * Returns a summary of each LTI capability this tool type requires in plain language * * @param stdClass $type The tool type * * @return array An array of text descriptions of each of the capabilities this tool type requires */ function get_tool_type_capability_groups($type) { $capabilities = lti_get_enabled_capabilities($type); $groups = array(); $hascourse = false; $hasactivities = false; $hasuseraccount = false; $hasuserpersonal = false; foreach ($capabilities as $capability) { // Bail out early if we've already found all groups. if (count($groups) >= 4) { continue; } if (!$hascourse && preg_match('/^CourseSection/', $capability)) { $hascourse = true; $groups[] = get_string('courseinformation', 'mod_lti'); } else if (!$hasactivities && preg_match('/^ResourceLink/', $capability)) { $hasactivities = true; $groups[] = get_string('courseactivitiesorresources', 'mod_lti'); } else if (!$hasuseraccount && preg_match('/^User/', $capability) || preg_match('/^Membership/', $capability)) { $hasuseraccount = true; $groups[] = get_string('useraccountinformation', 'mod_lti'); } else if (!$hasuserpersonal && preg_match('/^Person/', $capability)) { $hasuserpersonal = true; $groups[] = get_string('userpersonalinformation', 'mod_lti'); } } return $groups; } /** * Returns the ids of each instance of this tool type * * @param stdClass $type The tool type * * @return array An array of ids of the instances of this tool type */ function get_tool_type_instance_ids($type) { global $DB; return array_keys($DB->get_fieldset_select('lti', 'id', 'typeid = ?', array($type->id))); } /** * Serialises this tool type * * @param stdClass $type The tool type * * @return array An array of values representing this type */ function serialise_tool_type(stdClass $type) { global $CFG; $capabilitygroups = get_tool_type_capability_groups($type); $instanceids = get_tool_type_instance_ids($type); // Clean the name. We don't want tags here. $name = clean_param($type->name, PARAM_NOTAGS); if (!empty($type->description)) { // Clean the description. We don't want tags here. $description = clean_param($type->description, PARAM_NOTAGS); } else { $description = get_string('editdescription', 'mod_lti'); } return array( 'id' => $type->id, 'name' => $name, 'description' => $description, 'urls' => get_tool_type_urls($type), 'state' => get_tool_type_state_info($type), 'platformid' => $CFG->wwwroot, 'clientid' => $type->clientid, 'deploymentid' => $type->id, 'hascapabilitygroups' => !empty($capabilitygroups), 'capabilitygroups' => $capabilitygroups, // Course ID of 1 means it's not linked to a course. 'courseid' => $type->course == 1 ? 0 : $type->course, 'instanceids' => $instanceids, 'instancecount' => count($instanceids) ); } /** * Loads the cartridge information into the tool type, if the launch url is for a cartridge file * * @param stdClass $type The tool type object to be filled in * @since Moodle 3.1 */ function lti_load_type_if_cartridge($type) { if (!empty($type->lti_toolurl) && lti_is_cartridge($type->lti_toolurl)) { lti_load_type_from_cartridge($type->lti_toolurl, $type); } } /** * Loads the cartridge information into the new tool, if the launch url is for a cartridge file * * @param stdClass $lti The tools config * @since Moodle 3.1 */ function lti_load_tool_if_cartridge($lti) { if (!empty($lti->toolurl) && lti_is_cartridge($lti->toolurl)) { lti_load_tool_from_cartridge($lti->toolurl, $lti); } } /** * Determines if the given url is for a IMS basic cartridge * * @param string $url The url to be checked * @return True if the url is for a cartridge * @since Moodle 3.1 */ function lti_is_cartridge($url) { // If it is empty, it's not a cartridge. if (empty($url)) { return false; } // If it has xml at the end of the url, it's a cartridge. if (preg_match('/\.xml$/', $url)) { return true; } // Skip slow cartridge checks during tests. // During tests, working .xml cartridge URLs are used when testing cartridge support. These will match the '.xml' check // above (which is fast). Don't try to check whether other tool URLs are cartridges because most URLs used in tests will be // example URLs and won't be resolvable, resulting in network hangs within load_cartridge() - which is called every time a // tool is edited and will result in slow tests or seemingly random test failures. if (!defined('BEHAT_SITE_RUNNING') && !defined('PHPUNIT_TEST')) { // Even if it doesn't have .xml, load the url to check if it's a cartridge.. try { $toolinfo = lti_load_cartridge($url, array( "launch_url" => "launchurl" ) ); if (!empty($toolinfo['launchurl'])) { return true; } } catch (moodle_exception $e) { return false; // Error loading the xml, so it's not a cartridge. } } return false; } /** * Allows you to load settings for an external tool type from an IMS cartridge. * * @param string $url The URL to the cartridge * @param stdClass $type The tool type object to be filled in * @throws moodle_exception if the cartridge could not be loaded correctly * @since Moodle 3.1 */ function lti_load_type_from_cartridge($url, $type) { $toolinfo = lti_load_cartridge($url, array( "title" => "lti_typename", "launch_url" => "lti_toolurl", "description" => "lti_description", "icon" => "lti_icon", "secure_icon" => "lti_secureicon" ), array( "icon_url" => "lti_extension_icon", "secure_icon_url" => "lti_extension_secureicon" ) ); // If an activity name exists, unset the cartridge name so we don't override it. if (isset($type->lti_typename)) { unset($toolinfo['lti_typename']); } // Always prefer cartridge core icons first, then, if none are found, look at the extension icons. if (empty($toolinfo['lti_icon']) && !empty($toolinfo['lti_extension_icon'])) { $toolinfo['lti_icon'] = $toolinfo['lti_extension_icon']; } unset($toolinfo['lti_extension_icon']); if (empty($toolinfo['lti_secureicon']) && !empty($toolinfo['lti_extension_secureicon'])) { $toolinfo['lti_secureicon'] = $toolinfo['lti_extension_secureicon']; } unset($toolinfo['lti_extension_secureicon']); // Ensure Custom icons aren't overridden by cartridge params. if (!empty($type->lti_icon)) { unset($toolinfo['lti_icon']); } if (!empty($type->lti_secureicon)) { unset($toolinfo['lti_secureicon']); } foreach ($toolinfo as $property => $value) { $type->$property = $value; } } /** * Allows you to load in the configuration for an external tool from an IMS cartridge. * * @param string $url The URL to the cartridge * @param stdClass $lti LTI object * @throws moodle_exception if the cartridge could not be loaded correctly * @since Moodle 3.1 */ function lti_load_tool_from_cartridge($url, $lti) { $toolinfo = lti_load_cartridge($url, array( "title" => "name", "launch_url" => "toolurl", "secure_launch_url" => "securetoolurl", "description" => "intro", "icon" => "icon", "secure_icon" => "secureicon" ), array( "icon_url" => "extension_icon", "secure_icon_url" => "extension_secureicon" ) ); // If an activity name exists, unset the cartridge name so we don't override it. if (isset($lti->name)) { unset($toolinfo['name']); } // Always prefer cartridge core icons first, then, if none are found, look at the extension icons. if (empty($toolinfo['icon']) && !empty($toolinfo['extension_icon'])) { $toolinfo['icon'] = $toolinfo['extension_icon']; } unset($toolinfo['extension_icon']); if (empty($toolinfo['secureicon']) && !empty($toolinfo['extension_secureicon'])) { $toolinfo['secureicon'] = $toolinfo['extension_secureicon']; } unset($toolinfo['extension_secureicon']); foreach ($toolinfo as $property => $value) { $lti->$property = $value; } } /** * Search for a tag within an XML DOMDocument * * @param string $url The url of the cartridge to be loaded * @param array $map The map of tags to keys in the return array * @param array $propertiesmap The map of properties to keys in the return array * @return array An associative array with the given keys and their values from the cartridge * @throws moodle_exception if the cartridge could not be loaded correctly * @since Moodle 3.1 */ function lti_load_cartridge($url, $map, $propertiesmap = array()) { global $CFG; require_once($CFG->libdir. "/filelib.php"); $curl = new curl(); $response = $curl->get($url); // Got a completely empty response (real or error), cannot process this with // DOMDocument::loadXML() because it errors with ValueError. So let's throw // the moodle_exception before waiting to examine the errors later. if (trim($response) === '') { throw new moodle_exception('errorreadingfile', '', '', $url); } // TODO MDL-46023 Replace this code with a call to the new library. $origerrors = libxml_use_internal_errors(true); libxml_clear_errors(); $document = new DOMDocument(); @$document->loadXML($response, LIBXML_NONET); $cartridge = new DomXpath($document); $errors = libxml_get_errors(); libxml_clear_errors(); libxml_use_internal_errors($origerrors); if (count($errors) > 0) { $message = 'Failed to load cartridge.'; foreach ($errors as $error) { $message .= "\n" . trim($error->message, "\n\r\t .") . " at line " . $error->line; } throw new moodle_exception('errorreadingfile', '', '', $url, $message); } $toolinfo = array(); foreach ($map as $tag => $key) { $value = get_tag($tag, $cartridge); if ($value) { $toolinfo[$key] = $value; } } if (!empty($propertiesmap)) { foreach ($propertiesmap as $property => $key) { $value = get_tag("property", $cartridge, $property); if ($value) { $toolinfo[$key] = $value; } } } return $toolinfo; } /** * Search for a tag within an XML DOMDocument * * @param stdClass $tagname The name of the tag to search for * @param XPath $xpath The XML to find the tag in * @param XPath $attribute The attribute to search for (if we should search for a child node with the given * value for the name attribute * @since Moodle 3.1 */ function get_tag($tagname, $xpath, $attribute = null) { if ($attribute) { $result = $xpath->query('//*[local-name() = \'' . $tagname . '\'][@name="' . $attribute . '"]'); } else { $result = $xpath->query('//*[local-name() = \'' . $tagname . '\']'); } if ($result->length > 0) { return $result->item(0)->nodeValue; } return null; } /** * Create a new access token. * * @param int $typeid Tool type ID * @param string[] $scopes Scopes permitted for new token * * @return stdClass Access token */ function lti_new_access_token($typeid, $scopes) { global $DB; // Make sure the token doesn't exist (even if it should be almost impossible with the random generation). $numtries = 0; do { $numtries ++; $generatedtoken = md5(uniqid(rand(), 1)); if ($numtries > 5) { throw new moodle_exception('Failed to generate LTI access token'); } } while ($DB->record_exists('lti_access_tokens', array('token' => $generatedtoken))); $newtoken = new stdClass(); $newtoken->typeid = $typeid; $newtoken->scope = json_encode(array_values($scopes)); $newtoken->token = $generatedtoken; $newtoken->timecreated = time(); $newtoken->validuntil = $newtoken->timecreated + LTI_ACCESS_TOKEN_LIFE; $newtoken->lastaccess = null; $DB->insert_record('lti_access_tokens', $newtoken); return $newtoken; } home/smkb8269/public_html/lms.smk-almanshuriyah.sch.id/mod/book/locallib.php000064400000067637152267157720022725 0ustar00. /** * Book module local lib functions * * @package mod_book * @copyright 2010-2011 Petr Skoda {@link http://skodak.org} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die; require_once(__DIR__.'/lib.php'); require_once($CFG->libdir.'/filelib.php'); /** * The following defines are used to define how the chapters and subchapters of a book should be displayed in that table of contents. * BOOK_NUM_NONE No special styling will applied and the editor will be able to do what ever thay want in the title * BOOK_NUM_NUMBERS Chapters and subchapters are numbered (1, 1.1, 1.2, 2, ...) * BOOK_NUM_BULLETS Subchapters are indented and displayed with bullets * BOOK_NUM_INDENTED Subchapters are indented */ define('BOOK_NUM_NONE', '0'); define('BOOK_NUM_NUMBERS', '1'); define('BOOK_NUM_BULLETS', '2'); define('BOOK_NUM_INDENTED', '3'); /** * Preload book chapters and fix toc structure if necessary. * * Returns array of chapters with standard 'pagenum', 'id, pagenum, subchapter, title, content, contentformat, hidden' * and extra 'parent, number, subchapters, prev, next'. * Please note the content/text of chapters is not included. * * @param stdClass $book * @return array of id=>chapter */ function book_preload_chapters($book) { global $DB; $chapters = $DB->get_records('book_chapters', array('bookid' => $book->id), 'pagenum', 'id, pagenum, subchapter, title, content, contentformat, hidden'); if (!$chapters) { return array(); } $prev = null; $prevsub = null; $first = true; $hidesub = true; $parent = null; $pagenum = 0; // chapter sort $i = 0; // main chapter num $j = 0; // subchapter num foreach ($chapters as $id => $ch) { $oldch = clone($ch); $pagenum++; $ch->pagenum = $pagenum; if ($first) { // book can not start with a subchapter $ch->subchapter = 0; $first = false; } if (!$ch->subchapter) { if ($ch->hidden) { if ($book->numbering == BOOK_NUM_NUMBERS) { $ch->number = 'x'; } else { $ch->number = null; } } else { $i++; $ch->number = $i; } $j = 0; $prevsub = null; $hidesub = $ch->hidden; $parent = $ch->id; $ch->parent = null; $ch->subchapters = array(); } else { $ch->parent = $parent; $ch->subchapters = null; $chapters[$parent]->subchapters[$ch->id] = $ch->id; if ($hidesub) { // all subchapters in hidden chapter must be hidden too $ch->hidden = 1; } if ($ch->hidden) { if ($book->numbering == BOOK_NUM_NUMBERS) { $ch->number = 'x'; } else { $ch->number = null; } } else { $j++; $ch->number = $j; } } if ($oldch->subchapter != $ch->subchapter or $oldch->pagenum != $ch->pagenum or $oldch->hidden != $ch->hidden) { // update only if something changed $DB->update_record('book_chapters', $ch); } $chapters[$id] = $ch; } return $chapters; } /** * Returns the title for a given chapter * * @param int $chid * @param array $chapters * @param stdClass $book * @param context_module $context * @return string */ function book_get_chapter_title($chid, $chapters, $book, $context) { $ch = $chapters[$chid]; $title = trim(format_string($ch->title, true, array('context'=>$context))); $numbers = array(); if ($book->numbering == BOOK_NUM_NUMBERS) { if ($ch->parent and $chapters[$ch->parent]->number) { $numbers[] = $chapters[$ch->parent]->number; } if ($ch->number) { $numbers[] = $ch->number; } } if ($numbers) { $title = implode('.', $numbers) . '. ' . $title; } return $title; } /** * Add the book TOC sticky block to the default region. * * @param array $chapters The Chapters in the book * @param stdClass $chapter The current chapter * @param stdClass $book The book * @param stdClass $cm The course module * @param bool|null $edit Whether the user is editing */ function book_add_fake_block($chapters, $chapter, $book, $cm, $edit = null) { global $PAGE, $USER; if ($edit === null) { if (has_capability('mod/book:edit', context_module::instance($cm->id))) { if (isset($USER->editing)) { $edit = $USER->editing; } else { $edit = 0; } } else { $edit = 0; } } $toc = book_get_toc($chapters, $chapter, $book, $cm, $edit); $bc = new block_contents(); $bc->title = get_string('toc', 'mod_book'); $bc->attributes['class'] = 'block block_book_toc'; $bc->content = $toc; $defaultregion = $PAGE->blocks->get_default_region(); $PAGE->blocks->add_fake_block($bc, $defaultregion); } /** * Generate toc structure * * @param array $chapters * @param stdClass $chapter * @param stdClass $book * @param stdClass $cm * @param bool $edit * @return string */ function book_get_toc($chapters, $chapter, $book, $cm, $edit) { global $USER, $OUTPUT; $toc = ''; $nch = 0; // Chapter number $ns = 0; // Subchapter number $first = 1; $context = context_module::instance($cm->id); $viewhidden = has_capability('mod/book:viewhiddenchapters', $context); switch ($book->numbering) { case BOOK_NUM_NONE: $toc .= html_writer::start_tag('div', array('class' => 'book_toc book_toc_none clearfix')); break; case BOOK_NUM_NUMBERS: $toc .= html_writer::start_tag('div', array('class' => 'book_toc book_toc_numbered clearfix')); break; case BOOK_NUM_BULLETS: $toc .= html_writer::start_tag('div', array('class' => 'book_toc book_toc_bullets clearfix')); break; case BOOK_NUM_INDENTED: $toc .= html_writer::start_tag('div', array('class' => 'book_toc book_toc_indented clearfix')); break; } if ($edit) { // Editing on (Teacher's TOC). $toc .= html_writer::start_tag('ul'); $i = 0; foreach ($chapters as $ch) { $i++; $title = trim(format_string($ch->title, true, array('context' => $context))); $titleunescaped = trim(format_string($ch->title, true, array('context' => $context, 'escape' => false))); $titleout = $title; if (!$ch->subchapter) { if ($first) { $toc .= html_writer::start_tag('li'); } else { $toc .= html_writer::end_tag('ul'); $toc .= html_writer::end_tag('li'); $toc .= html_writer::start_tag('li'); } if (!$ch->hidden) { $nch++; $ns = 0; if ($book->numbering == BOOK_NUM_NUMBERS) { $title = "$nch. $title"; $titleout = $title; } } else { if ($book->numbering == BOOK_NUM_NUMBERS) { $title = "x. $title"; } $titleout = html_writer::tag('span', $title, array('class' => 'dimmed_text')); } } else { if ($first) { $toc .= html_writer::start_tag('li'); $toc .= html_writer::start_tag('ul'); $toc .= html_writer::start_tag('li'); } else { $toc .= html_writer::start_tag('li'); } if (!$ch->hidden) { $ns++; if ($book->numbering == BOOK_NUM_NUMBERS) { $title = "$nch.$ns. $title"; $titleout = $title; } } else { if ($book->numbering == BOOK_NUM_NUMBERS) { if (empty($chapters[$ch->parent]->hidden)) { $title = "$nch.x. $title"; } else { $title = "x.x. $title"; } } $titleout = html_writer::tag('span', $title, array('class' => 'dimmed_text')); } } $toc .= html_writer::start_tag('div', array('class' => 'd-flex')); if ($ch->id == $chapter->id) { $toc .= html_writer::tag('strong', $titleout, array('class' => 'text-truncate')); } else { $toc .= html_writer::link(new moodle_url('view.php', array('id' => $cm->id, 'chapterid' => $ch->id)), $titleout, array('title' => $titleunescaped, 'class' => 'text-truncate')); } $toc .= html_writer::start_tag('div', array('class' => 'action-list d-flex ms-auto')); if ($i != 1) { $toc .= html_writer::link(new moodle_url('move.php', array('id' => $cm->id, 'chapterid' => $ch->id, 'up' => '1', 'sesskey' => $USER->sesskey)), $OUTPUT->pix_icon('t/up', get_string('movechapterup', 'mod_book', $title)), array('title' => get_string('movechapterup', 'mod_book', $titleunescaped))); } if ($i != count($chapters)) { $toc .= html_writer::link(new moodle_url('move.php', array('id' => $cm->id, 'chapterid' => $ch->id, 'up' => '0', 'sesskey' => $USER->sesskey)), $OUTPUT->pix_icon('t/down', get_string('movechapterdown', 'mod_book', $title)), array('title' => get_string('movechapterdown', 'mod_book', $titleunescaped))); } $toc .= html_writer::link(new moodle_url('edit.php', array('cmid' => $cm->id, 'id' => $ch->id)), $OUTPUT->pix_icon('t/edit', get_string('editchapter', 'mod_book', $title)), array('title' => get_string('editchapter', 'mod_book', $titleunescaped))); $deleteaction = new confirm_action(get_string('deletechapter', 'mod_book', $titleunescaped)); $toc .= $OUTPUT->action_link( new moodle_url('delete.php', [ 'id' => $cm->id, 'chapterid' => $ch->id, 'sesskey' => sesskey(), 'confirm' => 1, ]), $OUTPUT->pix_icon('t/delete', get_string('deletechapter', 'mod_book', $title)), $deleteaction, ['title' => get_string('deletechapter', 'mod_book', $titleunescaped)] ); if ($ch->hidden) { $toc .= html_writer::link(new moodle_url('show.php', array('id' => $cm->id, 'chapterid' => $ch->id, 'sesskey' => $USER->sesskey)), $OUTPUT->pix_icon('t/show', get_string('showchapter', 'mod_book', $title)), array('title' => get_string('showchapter', 'mod_book', $titleunescaped))); } else { $toc .= html_writer::link(new moodle_url('show.php', array('id' => $cm->id, 'chapterid' => $ch->id, 'sesskey' => $USER->sesskey)), $OUTPUT->pix_icon('t/hide', get_string('hidechapter', 'mod_book', $title)), array('title' => get_string('hidechapter', 'mod_book', $titleunescaped))); } $buttontitle = get_string('addafterchapter', 'mod_book', ['title' => $ch->title]); $toc .= html_writer::link(new moodle_url('edit.php', array('cmid' => $cm->id, 'pagenum' => $ch->pagenum, 'subchapter' => $ch->subchapter)), $OUTPUT->pix_icon('add', $buttontitle, 'mod_book'), array('title' => $buttontitle)); $toc .= html_writer::end_tag('div'); $toc .= html_writer::end_tag('div'); if (!$ch->subchapter) { $toc .= html_writer::start_tag('ul'); } else { $toc .= html_writer::end_tag('li'); } $first = 0; } $toc .= html_writer::end_tag('ul'); $toc .= html_writer::end_tag('li'); $toc .= html_writer::end_tag('ul'); } else { // Editing off. Normal students, teachers view. $toc .= html_writer::start_tag('ul'); foreach ($chapters as $ch) { $title = trim(format_string($ch->title, true, array('context'=>$context))); $titleunescaped = trim(format_string($ch->title, true, array('context' => $context, 'escape' => false))); if (!$ch->hidden || ($ch->hidden && $viewhidden)) { if (!$ch->subchapter) { if ($first) { $toc .= html_writer::start_tag('li'); } else { $toc .= html_writer::end_tag('ul'); $toc .= html_writer::end_tag('li'); $toc .= html_writer::start_tag('li'); } // Don't show numbering for hidden chapters, so that numbering is consistent with what students see // and the edit mode. if (!$ch->hidden) { $nch++; $ns = 0; if ($book->numbering == BOOK_NUM_NUMBERS) { $title = "$nch. $title"; } } else { if ($book->numbering == BOOK_NUM_NUMBERS) { $title = "x. $title"; } } } else { if ($first) { $toc .= html_writer::start_tag('li'); $toc .= html_writer::start_tag('ul'); $toc .= html_writer::start_tag('li'); } else { $toc .= html_writer::start_tag('li'); } // Don't show numbering for hidden subchapters, so that numbering is consistent with what students see // and the edit mode. if (!$ch->hidden) { $ns++; if ($book->numbering == BOOK_NUM_NUMBERS) { $title = "$nch.$ns. $title"; } } else { if ($book->numbering == BOOK_NUM_NUMBERS) { if (empty($chapters[$ch->parent]->hidden)) { $title = "$nch.x. $title"; } else { $title = "x.x. $title"; } } } } $cssclass = ($ch->hidden && $viewhidden) ? 'dimmed_text' : ''; if ($ch->id == $chapter->id) { $toc .= html_writer::tag('strong', $title, array('class' => $cssclass)); } else { $toc .= html_writer::link(new moodle_url('view.php', array('id' => $cm->id, 'chapterid' => $ch->id)), $title, array('title' => s($titleunescaped), 'class' => $cssclass)); } if (!$ch->subchapter) { $toc .= html_writer::start_tag('ul'); } else { $toc .= html_writer::end_tag('li'); } $first = 0; } } $toc .= html_writer::end_tag('ul'); $toc .= html_writer::end_tag('li'); $toc .= html_writer::end_tag('ul'); } $toc .= html_writer::end_tag('div'); $toc = str_replace('
      ', '', $toc); // Cleanup of invalid structures. return $toc; } /** * Returns book chapters tagged with a specified tag. * * This is a callback used by the tag area mod_book/book_chapters to search for book chapters * tagged with a specific tag. * * @param core_tag_tag $tag * @param bool $exclusivemode if set to true it means that no other entities tagged with this tag * are displayed on the page and the per-page limit may be bigger * @param int $fromctx context id where the link was displayed, may be used by callbacks * to display items in the same context first * @param int $ctx context id where to search for records * @param bool $rec search in subcontexts as well * @param int $page 0-based number of page being displayed * @return \core_tag\output\tagindex */ function mod_book_get_tagged_chapters($tag, $exclusivemode = false, $fromctx = 0, $ctx = 0, $rec = true, $page = 0) { global $OUTPUT; $perpage = $exclusivemode ? 20 : 5; // Build the SQL query. $ctxselect = context_helper::get_preload_record_columns_sql('ctx'); $query = "SELECT bc.id, bc.title, bc.bookid, bc.hidden, cm.id AS cmid, c.id AS courseid, c.shortname, c.fullname, $ctxselect FROM {book_chapters} bc JOIN {book} b ON b.id = bc.bookid JOIN {modules} m ON m.name='book' JOIN {course_modules} cm ON cm.module = m.id AND cm.instance = b.id JOIN {tag_instance} tt ON bc.id = tt.itemid JOIN {course} c ON cm.course = c.id JOIN {context} ctx ON ctx.instanceid = cm.id AND ctx.contextlevel = :coursemodulecontextlevel WHERE tt.itemtype = :itemtype AND tt.tagid = :tagid AND tt.component = :component AND cm.deletioninprogress = 0 AND bc.id %ITEMFILTER% AND c.id %COURSEFILTER%"; $params = array('itemtype' => 'book_chapters', 'tagid' => $tag->id, 'component' => 'mod_book', 'coursemodulecontextlevel' => CONTEXT_MODULE); if ($ctx) { $context = $ctx ? context::instance_by_id($ctx) : context_system::instance(); $query .= $rec ? ' AND (ctx.id = :contextid OR ctx.path LIKE :path)' : ' AND ctx.id = :contextid'; $params['contextid'] = $context->id; $params['path'] = $context->path.'/%'; } $query .= " ORDER BY "; if ($fromctx) { // In order-clause specify that modules from inside "fromctx" context should be returned first. $fromcontext = context::instance_by_id($fromctx); $query .= ' (CASE WHEN ctx.id = :fromcontextid OR ctx.path LIKE :frompath THEN 0 ELSE 1 END),'; $params['fromcontextid'] = $fromcontext->id; $params['frompath'] = $fromcontext->path.'/%'; } $query .= ' c.sortorder, cm.id, bc.id'; $totalpages = $page + 1; // Use core_tag_index_builder to build and filter the list of items. $builder = new core_tag_index_builder('mod_book', 'book_chapters', $query, $params, $page * $perpage, $perpage + 1); while ($item = $builder->has_item_that_needs_access_check()) { context_helper::preload_from_record($item); $courseid = $item->courseid; if (!$builder->can_access_course($courseid)) { $builder->set_accessible($item, false); continue; } $modinfo = get_fast_modinfo($builder->get_course($courseid)); // Set accessibility of this item and all other items in the same course. $builder->walk(function ($taggeditem) use ($courseid, $modinfo, $builder) { if ($taggeditem->courseid == $courseid) { $accessible = false; if (($cm = $modinfo->get_cm($taggeditem->cmid)) && $cm->uservisible) { if (empty($taggeditem->hidden)) { $accessible = true; } else { $accessible = has_capability('mod/book:viewhiddenchapters', context_module::instance($cm->id)); } } $builder->set_accessible($taggeditem, $accessible); } }); } $items = $builder->get_items(); if (count($items) > $perpage) { $totalpages = $page + 2; // We don't need exact page count, just indicate that the next page exists. array_pop($items); } // Build the display contents. if ($items) { $tagfeed = new core_tag\output\tagfeed(); foreach ($items as $item) { context_helper::preload_from_record($item); $modinfo = get_fast_modinfo($item->courseid); $cm = $modinfo->get_cm($item->cmid); $pageurl = new moodle_url('/mod/book/view.php', array('chapterid' => $item->id, 'b' => $item->bookid)); $pagename = format_string($item->title, true, array('context' => context_module::instance($item->cmid))); $pagename = html_writer::link($pageurl, $pagename); $courseurl = course_get_url($item->courseid, $cm->sectionnum); $cmname = html_writer::link($cm->url, $cm->get_formatted_name()); $coursename = format_string($item->fullname, true, array('context' => context_course::instance($item->courseid))); $coursename = html_writer::link($courseurl, $coursename); $icon = html_writer::link($pageurl, html_writer::empty_tag('img', array('src' => $cm->get_icon_url()))); $tagfeed->add($icon, $pagename, $cmname.'
      '.$coursename); } $content = $OUTPUT->render_from_template('core_tag/tagfeed', $tagfeed->export_for_template($OUTPUT)); return new core_tag\output\tagindex($tag, 'mod_book', 'book_chapters', $content, $exclusivemode, $fromctx, $ctx, $rec, $page, $totalpages); } } /** * File browsing support class * * @copyright 2010-2011 Petr Skoda {@link http://skodak.org} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class book_file_info extends file_info { /** @var stdClass Course object */ protected $course; /** @var stdClass Course module object */ protected $cm; /** @var array Available file areas */ protected $areas; /** @var string File area to browse */ protected $filearea; /** * Constructor * * @param file_browser $browser file_browser instance * @param stdClass $course course object * @param stdClass $cm course module object * @param stdClass $context module context * @param array $areas available file areas * @param string $filearea file area to browse */ public function __construct($browser, $course, $cm, $context, $areas, $filearea) { parent::__construct($browser, $context); $this->course = $course; $this->cm = $cm; $this->areas = $areas; $this->filearea = $filearea; } /** * Returns list of standard virtual file/directory identification. * The difference from stored_file parameters is that null values * are allowed in all fields * @return array with keys contextid, filearea, itemid, filepath and filename */ public function get_params() { return array('contextid'=>$this->context->id, 'component'=>'mod_book', 'filearea' =>$this->filearea, 'itemid' =>null, 'filepath' =>null, 'filename' =>null); } /** * Returns localised visible name. * @return string */ public function get_visible_name() { return $this->areas[$this->filearea]; } /** * Can I add new files or directories? * @return bool */ public function is_writable() { return false; } /** * Is directory? * @return bool */ public function is_directory() { return true; } /** * Returns list of children. * @return array of file_info instances */ public function get_children() { return $this->get_filtered_children('*', false, true); } /** * Help function to return files matching extensions or their count * * @param string|array $extensions, either '*' or array of lowercase extensions, i.e. array('.gif','.jpg') * @param bool|int $countonly if false returns the children, if an int returns just the * count of children but stops counting when $countonly number of children is reached * @param bool $returnemptyfolders if true returns items that don't have matching files inside * @return array|int array of file_info instances or the count */ private function get_filtered_children($extensions = '*', $countonly = false, $returnemptyfolders = false) { global $DB; $params = array('contextid' => $this->context->id, 'component' => 'mod_book', 'filearea' => $this->filearea, 'bookid' => $this->cm->instance); $sql = 'SELECT DISTINCT bc.id, bc.pagenum FROM {files} f, {book_chapters} bc WHERE f.contextid = :contextid AND f.component = :component AND f.filearea = :filearea AND bc.bookid = :bookid AND bc.id = f.itemid'; if (!$returnemptyfolders) { $sql .= ' AND filename <> :emptyfilename'; $params['emptyfilename'] = '.'; } list($sql2, $params2) = $this->build_search_files_sql($extensions, 'f'); $sql .= ' '.$sql2; $params = array_merge($params, $params2); if ($countonly === false) { $sql .= ' ORDER BY bc.pagenum'; } $rs = $DB->get_recordset_sql($sql, $params); $children = array(); foreach ($rs as $record) { if ($child = $this->browser->get_file_info($this->context, 'mod_book', $this->filearea, $record->id)) { if ($returnemptyfolders || $child->count_non_empty_children($extensions)) { $children[] = $child; } } if ($countonly !== false && count($children) >= $countonly) { break; } } $rs->close(); if ($countonly !== false) { return count($children); } return $children; } /** * Returns list of children which are either files matching the specified extensions * or folders that contain at least one such file. * * @param string|array $extensions, either '*' or array of lowercase extensions, i.e. array('.gif','.jpg') * @return array of file_info instances */ public function get_non_empty_children($extensions = '*') { return $this->get_filtered_children($extensions, false); } /** * Returns the number of children which are either files matching the specified extensions * or folders containing at least one such file. * * @param string|array $extensions, for example '*' or array('.gif','.jpg') * @param int $limit stop counting after at least $limit non-empty children are found * @return int */ public function count_non_empty_children($extensions = '*', $limit = 1) { return $this->get_filtered_children($extensions, $limit); } /** * Returns parent file_info instance * @return file_info or null for root */ public function get_parent() { return $this->browser->get_file_info($this->context); } }