ÿØÿà 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-Ї Ç externallib.php000064400000410074152265517350007607 0ustar00. /** * External assign API * * @package mod_assign * @since Moodle 2.4 * @copyright 2012 Paul Charsley * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ use core_external\external_files; use core_external\external_format_value; use core_external\external_function_parameters; use core_external\external_multiple_structure; use core_external\external_single_structure; use core_external\external_value; use core_external\external_warnings; use core_external\util as external_util; defined('MOODLE_INTERNAL') || die; require_once("$CFG->dirroot/user/externallib.php"); require_once("$CFG->dirroot/mod/assign/locallib.php"); /** * Assign functions * @copyright 2012 Paul Charsley * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class mod_assign_external extends \mod_assign\external\external_api { /** * Describes the parameters for get_grades * @return external_function_parameters * @since Moodle 2.4 */ public static function get_grades_parameters() { return new external_function_parameters( array( 'assignmentids' => new external_multiple_structure( new external_value(PARAM_INT, 'assignment id'), '1 or more assignment ids', VALUE_REQUIRED), 'since' => new external_value(PARAM_INT, 'timestamp, only return records where timemodified >= since', VALUE_DEFAULT, 0) ) ); } /** * Returns grade information from assign_grades for the requested assignment ids * @param int[] $assignmentids * @param int $since only return records with timemodified >= since * @return array of grade records for each requested assignment * @since Moodle 2.4 */ public static function get_grades($assignmentids, $since = 0) { global $DB; $params = self::validate_parameters(self::get_grades_parameters(), array('assignmentids' => $assignmentids, 'since' => $since)); $assignments = array(); $warnings = array(); $requestedassignmentids = $params['assignmentids']; // Check the user is allowed to get the grades for the assignments requested. $placeholders = array(); list($sqlassignmentids, $placeholders) = $DB->get_in_or_equal($requestedassignmentids, SQL_PARAMS_NAMED); $sql = "SELECT cm.id, cm.instance FROM {course_modules} cm JOIN {modules} md ON md.id = cm.module ". "WHERE md.name = :modname AND cm.instance ".$sqlassignmentids; $placeholders['modname'] = 'assign'; $cms = $DB->get_records_sql($sql, $placeholders); foreach ($cms as $cm) { try { $context = context_module::instance($cm->id); self::validate_context($context); $assign = new assign($context, null, null); $assign->require_view_grades(); } catch (Exception $e) { $requestedassignmentids = array_diff($requestedassignmentids, array($cm->instance)); $warning = array(); $warning['item'] = 'assignment'; $warning['itemid'] = $cm->instance; $warning['warningcode'] = '1'; $warning['message'] = 'No access rights in module context'; $warnings[] = $warning; } } // Create the query and populate an array of grade records from the recordset results. if (count ($requestedassignmentids) > 0) { $placeholders = array(); list($inorequalsql, $placeholders) = $DB->get_in_or_equal($requestedassignmentids, SQL_PARAMS_NAMED); $sql = "SELECT ag.id, ag.assignment, ag.userid, ag.timecreated, ag.timemodified, ag.grader, ag.grade, ag.attemptnumber FROM {assign_grades} ag, {assign_submission} s WHERE s.assignment $inorequalsql AND s.userid = ag.userid AND s.latest = 1 AND s.attemptnumber = ag.attemptnumber AND ag.timemodified >= :since AND ag.assignment = s.assignment ORDER BY ag.assignment, ag.id"; $placeholders['since'] = $params['since']; $rs = $DB->get_recordset_sql($sql, $placeholders); $currentassignmentid = null; $assignment = null; foreach ($rs as $rd) { $grade = array(); $grade['id'] = $rd->id; $grade['userid'] = $rd->userid; $grade['timecreated'] = $rd->timecreated; $grade['timemodified'] = $rd->timemodified; $grade['grader'] = $rd->grader; $grade['attemptnumber'] = $rd->attemptnumber; $grade['grade'] = (string)$rd->grade; if (is_null($currentassignmentid) || ($rd->assignment != $currentassignmentid )) { if (!is_null($assignment)) { $assignments[] = $assignment; } $assignment = array(); $assignment['assignmentid'] = $rd->assignment; $assignment['grades'] = array(); $requestedassignmentids = array_diff($requestedassignmentids, array($rd->assignment)); } $assignment['grades'][] = $grade; $currentassignmentid = $rd->assignment; } if (!is_null($assignment)) { $assignments[] = $assignment; } $rs->close(); } foreach ($requestedassignmentids as $assignmentid) { $warning = array(); $warning['item'] = 'assignment'; $warning['itemid'] = $assignmentid; $warning['warningcode'] = '3'; $warning['message'] = 'No grades found'; $warnings[] = $warning; } $result = array(); $result['assignments'] = $assignments; $result['warnings'] = $warnings; return $result; } /** * Creates a grade single structure. * * @return external_single_structure a grade single structure. * @since Moodle 3.1 */ private static function get_grade_structure($required = VALUE_REQUIRED) { return new external_single_structure( array( 'id' => new external_value(PARAM_INT, 'grade id'), 'assignment' => new external_value(PARAM_INT, 'assignment id', VALUE_OPTIONAL), 'userid' => new external_value(PARAM_INT, 'student id'), 'attemptnumber' => new external_value(PARAM_INT, 'attempt number'), 'timecreated' => new external_value(PARAM_INT, 'grade creation time'), 'timemodified' => new external_value(PARAM_INT, 'grade last modified time'), 'grader' => new external_value(PARAM_INT, 'grader, -1 if grader is hidden'), 'grade' => new external_value(PARAM_TEXT, 'grade'), 'gradefordisplay' => new external_value(PARAM_RAW, 'grade rendered into a format suitable for display', VALUE_OPTIONAL), ), 'grade information', $required ); } /** * Creates an assign_grades external_single_structure * @return external_single_structure * @since Moodle 2.4 */ private static function assign_grades() { return new external_single_structure( array ( 'assignmentid' => new external_value(PARAM_INT, 'assignment id'), 'grades' => new external_multiple_structure(self::get_grade_structure()) ) ); } /** * Describes the get_grades return value * @return external_single_structure * @since Moodle 2.4 */ public static function get_grades_returns() { return new external_single_structure( array( 'assignments' => new external_multiple_structure(self::assign_grades(), 'list of assignment grade information'), 'warnings' => new external_warnings('item is always \'assignment\'', 'when errorcode is 3 then itemid is an assignment id. When errorcode is 1, itemid is a course module id', 'errorcode can be 3 (no grades found) or 1 (no permission to get grades)') ) ); } /** * Returns description of method parameters * * @return external_function_parameters * @since Moodle 2.4 */ public static function get_assignments_parameters() { return new external_function_parameters( array( 'courseids' => new external_multiple_structure( new external_value(PARAM_INT, 'course id, empty for retrieving all the courses where the user is enroled in'), '0 or more course ids', VALUE_DEFAULT, array() ), 'capabilities' => new external_multiple_structure( new external_value(PARAM_CAPABILITY, 'capability'), 'list of capabilities used to filter courses', VALUE_DEFAULT, array() ), 'includenotenrolledcourses' => new external_value(PARAM_BOOL, 'whether to return courses that the user can see even if is not enroled in. This requires the parameter courseids to not be empty.', VALUE_DEFAULT, false) ) ); } /** * Returns an array of courses the user is enrolled, and for each course all of the assignments that the user can * view within that course. * * @param array $courseids An optional array of course ids. If provided only assignments within the given course * will be returned. If the user is not enrolled in or can't view a given course a warning will be generated and returned. * @param array $capabilities An array of additional capability checks you wish to be made on the course context. * @param bool $includenotenrolledcourses Wheter to return courses that the user can see even if is not enroled in. * This requires the parameter $courseids to not be empty. * @return An array of courses and warnings. * @since Moodle 2.4 */ public static function get_assignments($courseids = array(), $capabilities = array(), $includenotenrolledcourses = false) { global $USER, $DB, $CFG; $params = self::validate_parameters( self::get_assignments_parameters(), array( 'courseids' => $courseids, 'capabilities' => $capabilities, 'includenotenrolledcourses' => $includenotenrolledcourses ) ); $warnings = array(); $courses = array(); $fields = 'sortorder,shortname,fullname,timemodified'; // If the courseids list is empty, we return only the courses where the user is enrolled in. if (empty($params['courseids'])) { $courses = enrol_get_users_courses($USER->id, true, $fields); $courseids = array_keys($courses); } else if ($includenotenrolledcourses) { // In this case, we don't have to check here for enrolmnents. Maybe the user can see the course even if is not enrolled. $courseids = $params['courseids']; } else { // We need to check for enrolments. $mycourses = enrol_get_users_courses($USER->id, true, $fields); $mycourseids = array_keys($mycourses); foreach ($params['courseids'] as $courseid) { if (!in_array($courseid, $mycourseids)) { unset($courses[$courseid]); $warnings[] = array( 'item' => 'course', 'itemid' => $courseid, 'warningcode' => '2', 'message' => 'User is not enrolled or does not have requested capability' ); } else { $courses[$courseid] = $mycourses[$courseid]; } } $courseids = array_keys($courses); } foreach ($courseids as $cid) { try { $context = context_course::instance($cid); self::validate_context($context); // Check if this course was already loaded (by enrol_get_users_courses). if (!isset($courses[$cid])) { $courses[$cid] = get_course($cid); } $courses[$cid]->contextid = $context->id; } catch (Exception $e) { unset($courses[$cid]); $warnings[] = array( 'item' => 'course', 'itemid' => $cid, 'warningcode' => '1', 'message' => 'No access rights in course context '.$e->getMessage() ); continue; } if (count($params['capabilities']) > 0 && !has_all_capabilities($params['capabilities'], $context)) { unset($courses[$cid]); } } $extrafields='m.id as assignmentid, ' . 'm.course, ' . 'm.nosubmissions, ' . 'm.submissiondrafts, ' . 'm.sendnotifications, '. 'm.sendlatenotifications, ' . 'm.sendstudentnotifications, ' . 'm.duedate, ' . 'm.allowsubmissionsfromdate, '. 'm.grade, ' . 'm.gradepenalty, ' . 'm.timemodified, '. 'm.completionsubmit, ' . 'm.cutoffdate, ' . 'm.gradingduedate, ' . 'm.teamsubmission, ' . 'm.requireallteammemberssubmit, '. 'm.teamsubmissiongroupingid, ' . 'm.blindmarking, ' . 'm.hidegrader, ' . 'm.revealidentities, ' . 'm.attemptreopenmethod, '. 'm.maxattempts, ' . 'm.markingworkflow, ' . 'm.markingallocation, ' . 'm.markinganonymous, ' . 'm.requiresubmissionstatement, '. 'm.preventsubmissionnotingroup, '. 'm.intro, '. 'm.introformat,' . 'm.activity,' . 'm.activityformat,' . 'm.timelimit,' . 'm.submissionattachments'; $coursearray = array(); foreach ($courses as $id => $course) { $assignmentarray = array(); // Get a list of assignments for the course. if ($modules = get_coursemodules_in_course('assign', $courses[$id]->id, $extrafields)) { foreach ($modules as $module) { $context = context_module::instance($module->id); try { self::validate_context($context); require_capability('mod/assign:view', $context); } catch (Exception $e) { $warnings[] = array( 'item' => 'module', 'itemid' => $module->id, 'warningcode' => '1', 'message' => 'No access rights in module context' ); continue; } $assign = new assign($context, null, null); // Update assign with override information. $assign->update_effective_access($USER->id); // Get configurations for only enabled plugins. $plugins = $assign->get_submission_plugins(); $plugins = array_merge($plugins, $assign->get_feedback_plugins()); $configarray = array(); foreach ($plugins as $plugin) { if ($plugin->is_enabled() && $plugin->is_visible()) { $configrecords = $plugin->get_config_for_external(); foreach ($configrecords as $name => $value) { $configarray[] = array( 'plugin' => $plugin->get_type(), 'subtype' => $plugin->get_subtype(), 'name' => $name, 'value' => $value ); } } } $assignment = array( 'id' => $module->assignmentid, 'cmid' => $module->id, 'course' => $module->course, 'name' => \core_external\util::format_string($module->name, $context), 'nosubmissions' => $module->nosubmissions, 'submissiondrafts' => $module->submissiondrafts, 'sendnotifications' => $module->sendnotifications, 'sendlatenotifications' => $module->sendlatenotifications, 'sendstudentnotifications' => $module->sendstudentnotifications, 'duedate' => $assign->get_instance()->duedate, 'allowsubmissionsfromdate' => $assign->get_instance()->allowsubmissionsfromdate, 'grade' => $module->grade, 'gradepenalty' => $module->gradepenalty, 'timemodified' => $module->timemodified, 'completionsubmit' => $module->completionsubmit, 'cutoffdate' => $assign->get_instance()->cutoffdate, 'gradingduedate' => $assign->get_instance()->gradingduedate, 'teamsubmission' => $module->teamsubmission, 'requireallteammemberssubmit' => $module->requireallteammemberssubmit, 'teamsubmissiongroupingid' => $module->teamsubmissiongroupingid, 'blindmarking' => $module->blindmarking, 'hidegrader' => $module->hidegrader, 'revealidentities' => $module->revealidentities, 'attemptreopenmethod' => $module->attemptreopenmethod, 'maxattempts' => $module->maxattempts, 'markingworkflow' => $module->markingworkflow, 'markingallocation' => $module->markingallocation, 'markinganonymous' => $module->markinganonymous, 'requiresubmissionstatement' => $module->requiresubmissionstatement, 'preventsubmissionnotingroup' => $module->preventsubmissionnotingroup, 'timelimit' => $module->timelimit, 'submissionattachments' => $module->submissionattachments, 'configs' => $configarray ); // Return or not intro and file attachments depending on the plugin settings. if ($assign->show_intro()) { $options = array('noclean' => true); [$assignment['intro'], $assignment['introformat']] = \core_external\util::format_text( $module->intro, $module->introformat, $context, 'mod_assign', 'intro', null, $options ); $assignment['introfiles'] = external_util::get_area_files($context->id, 'mod_assign', 'intro', false, false); if ($assign->should_provide_intro_attachments($USER->id)) { $assignment['introattachments'] = external_util::get_area_files($context->id, 'mod_assign', ASSIGN_INTROATTACHMENT_FILEAREA, 0); } } if ($module->requiresubmissionstatement) { // Submission statement is required, return the submission statement value. $adminconfig = get_config('assign'); // Single submission. if (!$module->teamsubmission) { list($assignment['submissionstatement'], $assignment['submissionstatementformat']) = \core_external\util::format_text($adminconfig->submissionstatement, FORMAT_MOODLE, $context->id, 'mod_assign', '', 0); } else { // Team submission. // One user can submit for the whole team. if (!empty($adminconfig->submissionstatementteamsubmission) && !$module->requireallteammemberssubmit) { list($assignment['submissionstatement'], $assignment['submissionstatementformat']) = \core_external\util::format_text($adminconfig->submissionstatementteamsubmission, FORMAT_MOODLE, $context->id, 'mod_assign', '', 0); } else if (!empty($adminconfig->submissionstatementteamsubmissionallsubmit) && $module->requireallteammemberssubmit) { // All team members must submit. list($assignment['submissionstatement'], $assignment['submissionstatementformat']) = \core_external\util::format_text($adminconfig->submissionstatementteamsubmissionallsubmit, FORMAT_MOODLE, $context->id, 'mod_assign', '', 0); } } } if ($module->activity && $assign->submissions_open($USER->id, true)) { list($assignment['activity'], $assignment['activityformat']) = \core_external\util::format_text( $module->activity, $module->activityformat, $context, 'mod_assign', ASSIGN_ACTIVITYATTACHMENT_FILEAREA, 0 ); $assignment['activityattachments'] = external_util::get_area_files($context->id, 'mod_assign', ASSIGN_ACTIVITYATTACHMENT_FILEAREA, 0); } $assignmentarray[] = $assignment; } } $coursearray[]= array( 'id' => $courses[$id]->id, 'fullname' => \core_external\util::format_string($courses[$id]->fullname, $course->contextid), 'shortname' => \core_external\util::format_string($courses[$id]->shortname, $course->contextid), 'timemodified' => $courses[$id]->timemodified, 'assignments' => $assignmentarray ); } $result = array( 'courses' => $coursearray, 'warnings' => $warnings ); return $result; } /** * Creates an assignment external_single_structure * * @return external_single_structure * @since Moodle 2.4 */ private static function get_assignments_assignment_structure() { return new external_single_structure( array( 'id' => new external_value(PARAM_INT, 'assignment id'), 'cmid' => new external_value(PARAM_INT, 'course module id'), 'course' => new external_value(PARAM_INT, 'course id'), 'name' => new external_value(PARAM_RAW, 'assignment name'), 'nosubmissions' => new external_value(PARAM_INT, 'no submissions'), 'submissiondrafts' => new external_value(PARAM_INT, 'submissions drafts'), 'sendnotifications' => new external_value(PARAM_INT, 'send notifications'), 'sendlatenotifications' => new external_value(PARAM_INT, 'send notifications'), 'sendstudentnotifications' => new external_value(PARAM_INT, 'send student notifications (default)'), 'duedate' => new external_value(PARAM_INT, 'assignment due date'), 'allowsubmissionsfromdate' => new external_value(PARAM_INT, 'allow submissions from date'), 'grade' => new external_value(PARAM_INT, 'grade type'), 'gradepenalty' => new external_value(PARAM_INT, 'if enabled, penalty will be applied to late submissions'), 'timemodified' => new external_value(PARAM_INT, 'last time assignment was modified'), 'completionsubmit' => new external_value(PARAM_INT, 'if enabled, set activity as complete following submission'), 'cutoffdate' => new external_value(PARAM_INT, 'date after which submission is not accepted without an extension'), 'gradingduedate' => new external_value(PARAM_INT, 'the expected date for marking the submissions'), 'teamsubmission' => new external_value(PARAM_INT, 'if enabled, students submit as a team'), 'requireallteammemberssubmit' => new external_value(PARAM_INT, 'if enabled, all team members must submit'), 'teamsubmissiongroupingid' => new external_value(PARAM_INT, 'the grouping id for the team submission groups'), 'blindmarking' => new external_value(PARAM_INT, 'if enabled, hide identities until reveal identities actioned'), 'hidegrader' => new external_value(PARAM_INT, 'If enabled, hide grader to student'), 'revealidentities' => new external_value(PARAM_INT, 'show identities for a blind marking assignment'), 'attemptreopenmethod' => new external_value(PARAM_TEXT, 'method used to control opening new attempts'), 'maxattempts' => new external_value(PARAM_INT, 'maximum number of attempts allowed'), 'markingworkflow' => new external_value(PARAM_INT, 'enable marking workflow'), 'markingallocation' => new external_value(PARAM_INT, 'enable marking allocation'), 'markinganonymous' => new external_value(PARAM_INT, 'enable marking anonymous'), 'requiresubmissionstatement' => new external_value(PARAM_INT, 'student must accept submission statement'), 'preventsubmissionnotingroup' => new external_value(PARAM_INT, 'Prevent submission not in group', VALUE_OPTIONAL), 'submissionstatement' => new external_value(PARAM_RAW, 'Submission statement formatted.', VALUE_OPTIONAL), 'submissionstatementformat' => new external_format_value('submissionstatement', VALUE_OPTIONAL), 'configs' => new external_multiple_structure(self::get_assignments_config_structure(), 'configuration settings'), 'intro' => new external_value(PARAM_RAW, 'assignment intro, not allways returned because it deppends on the activity configuration', VALUE_OPTIONAL), 'introformat' => new external_format_value('intro', VALUE_OPTIONAL), 'introfiles' => new external_files('Files in the introduction text', VALUE_OPTIONAL), 'introattachments' => new external_files('intro attachments files', VALUE_OPTIONAL), 'activity' => new external_value(PARAM_RAW, 'Description of activity', VALUE_OPTIONAL), 'activityformat' => new external_format_value('activity', VALUE_OPTIONAL), 'activityattachments' => new external_files('Files from activity field', VALUE_OPTIONAL), 'timelimit' => new external_value(PARAM_INT, 'Time limit to complete assigment', VALUE_OPTIONAL), 'submissionattachments' => new external_value(PARAM_INT, 'Flag to only show files during submission', VALUE_OPTIONAL), ), 'assignment information object'); } /** * Creates an assign_plugin_config external_single_structure * * @return external_single_structure * @since Moodle 2.4 */ private static function get_assignments_config_structure() { return new external_single_structure( array( 'id' => new external_value(PARAM_INT, 'assign_plugin_config id', VALUE_OPTIONAL), 'assignment' => new external_value(PARAM_INT, 'assignment id', VALUE_OPTIONAL), 'plugin' => new external_value(PARAM_TEXT, 'plugin'), 'subtype' => new external_value(PARAM_TEXT, 'subtype'), 'name' => new external_value(PARAM_TEXT, 'name'), 'value' => new external_value(PARAM_TEXT, 'value') ), 'assignment configuration object' ); } /** * Creates a course external_single_structure * * @return external_single_structure * @since Moodle 2.4 */ private static function get_assignments_course_structure() { return new external_single_structure( array( 'id' => new external_value(PARAM_INT, 'course id'), 'fullname' => new external_value(PARAM_RAW, 'course full name'), 'shortname' => new external_value(PARAM_RAW, 'course short name'), 'timemodified' => new external_value(PARAM_INT, 'last time modified'), 'assignments' => new external_multiple_structure(self::get_assignments_assignment_structure(), 'assignment info') ), 'course information object' ); } /** * Describes the return value for get_assignments * * @return external_single_structure * @since Moodle 2.4 */ public static function get_assignments_returns() { return new external_single_structure( array( 'courses' => new external_multiple_structure(self::get_assignments_course_structure(), 'list of courses'), 'warnings' => new external_warnings('item can be \'course\' (errorcode 1 or 2) or \'module\' (errorcode 1)', 'When item is a course then itemid is a course id. When the item is a module then itemid is a module id', 'errorcode can be 1 (no access rights) or 2 (not enrolled or no permissions)') ) ); } /** * Return information (files and text fields) for the given plugins in the assignment. * * @param assign $assign the assignment object * @param array $assignplugins array of assignment plugins (submission or feedback) * @param stdClass $item the item object (submission or grade) * @return array an array containing the plugins returned information */ private static function get_plugins_data($assign, $assignplugins, $item) { global $CFG; $plugins = array(); $fs = get_file_storage(); foreach ($assignplugins as $assignplugin) { if (!$assignplugin->is_enabled() or !$assignplugin->is_visible()) { continue; } $plugin = array( 'name' => $assignplugin->get_name(), 'type' => $assignplugin->get_type() ); // Subtype is 'assignsubmission', type is currently 'file' or 'onlinetext'. $component = $assignplugin->get_subtype().'_'.$assignplugin->get_type(); $fileareas = $assignplugin->get_file_areas(); foreach ($fileareas as $filearea => $name) { $fileareainfo = array('area' => $filearea); $fileareainfo['files'] = external_util::get_area_files( $assign->get_context()->id, $component, $filearea, $item->id ); $plugin['fileareas'][] = $fileareainfo; } $editorfields = $assignplugin->get_editor_fields(); foreach ($editorfields as $name => $description) { $editorfieldinfo = array( 'name' => $name, 'description' => $description, 'text' => $assignplugin->get_editor_text($name, $item->id), 'format' => $assignplugin->get_editor_format($name, $item->id) ); // Now format the text. foreach ($fileareas as $filearea => $name) { list($editorfieldinfo['text'], $editorfieldinfo['format']) = \core_external\util::format_text( $editorfieldinfo['text'], $editorfieldinfo['format'], $assign->get_context(), $component, $filearea, $item->id); } $plugin['editorfields'][] = $editorfieldinfo; } $plugins[] = $plugin; } return $plugins; } /** * Describes the parameters for get_submissions * * @return external_function_parameters * @since Moodle 2.5 */ public static function get_submissions_parameters() { return new external_function_parameters( array( 'assignmentids' => new external_multiple_structure( new external_value(PARAM_INT, 'assignment id'), '1 or more assignment ids', VALUE_REQUIRED), 'status' => new external_value(PARAM_ALPHA, 'status', VALUE_DEFAULT, ''), 'since' => new external_value(PARAM_INT, 'submitted since', VALUE_DEFAULT, 0), 'before' => new external_value(PARAM_INT, 'submitted before', VALUE_DEFAULT, 0) ) ); } /** * Returns submissions for the requested assignment ids * * @param int[] $assignmentids * @param string $status only return submissions with this status * @param int $since only return submissions with timemodified >= since * @param int $before only return submissions with timemodified <= before * @return array of submissions for each requested assignment * @since Moodle 2.5 */ public static function get_submissions($assignmentids, $status = '', $since = 0, $before = 0) { global $DB, $CFG; $params = self::validate_parameters(self::get_submissions_parameters(), array('assignmentids' => $assignmentids, 'status' => $status, 'since' => $since, 'before' => $before)); $warnings = array(); $assignments = array(); // Check the user is allowed to get the submissions for the assignments requested. $placeholders = array(); list($inorequalsql, $placeholders) = $DB->get_in_or_equal($params['assignmentids'], SQL_PARAMS_NAMED); $sql = "SELECT cm.id, cm.instance FROM {course_modules} cm JOIN {modules} md ON md.id = cm.module ". "WHERE md.name = :modname AND cm.instance ".$inorequalsql; $placeholders['modname'] = 'assign'; $cms = $DB->get_records_sql($sql, $placeholders); $assigns = array(); foreach ($cms as $cm) { try { $context = context_module::instance($cm->id); self::validate_context($context); $assign = new assign($context, null, null); $assign->require_view_grades(); $assigns[] = $assign; } catch (Exception $e) { $warnings[] = array( 'item' => 'assignment', 'itemid' => $cm->instance, 'warningcode' => '1', 'message' => 'No access rights in module context' ); } } foreach ($assigns as $assign) { $submissions = array(); $placeholders = array('assignid1' => $assign->get_instance()->id, 'assignid2' => $assign->get_instance()->id); $submissionmaxattempt = 'SELECT mxs.userid, mxs.groupid, MAX(mxs.attemptnumber) AS maxattempt FROM {assign_submission} mxs WHERE mxs.assignment = :assignid1 GROUP BY mxs.userid, mxs.groupid'; $sql = "SELECT mas.id, mas.assignment,mas.userid,". "mas.timecreated,mas.timemodified,mas.timestarted,mas.status,mas.groupid,mas.attemptnumber ". "FROM {assign_submission} mas ". "JOIN ( " . $submissionmaxattempt . " ) smx ON mas.userid = smx.userid ". "AND mas.groupid = smx.groupid ". "WHERE mas.assignment = :assignid2 AND mas.attemptnumber = smx.maxattempt"; if (!empty($params['status'])) { $placeholders['status'] = $params['status']; $sql = $sql." AND mas.status = :status"; } if (!empty($params['before'])) { $placeholders['since'] = $params['since']; $placeholders['before'] = $params['before']; $sql = $sql." AND mas.timemodified BETWEEN :since AND :before"; } else { $placeholders['since'] = $params['since']; $sql = $sql." AND mas.timemodified >= :since"; } $submissionrecords = $DB->get_records_sql($sql, $placeholders); if (!empty($submissionrecords)) { $submissionplugins = $assign->get_submission_plugins(); foreach ($submissionrecords as $submissionrecord) { $submission = array( 'id' => $submissionrecord->id, 'userid' => $submissionrecord->userid, 'timecreated' => $submissionrecord->timecreated, 'timemodified' => $submissionrecord->timemodified, 'timestarted' => $submissionrecord->timestarted, 'status' => $submissionrecord->status, 'attemptnumber' => $submissionrecord->attemptnumber, 'groupid' => $submissionrecord->groupid, 'plugins' => self::get_plugins_data($assign, $submissionplugins, $submissionrecord), 'gradingstatus' => $assign->get_grading_status($submissionrecord->userid) ); if (($assign->get_instance()->teamsubmission && $assign->can_view_group_submission($submissionrecord->groupid)) || (!$assign->get_instance()->teamsubmission && $assign->can_view_submission($submissionrecord->userid)) ) { $submissions[] = $submission; } } } else { $warnings[] = array( 'item' => 'module', 'itemid' => $assign->get_instance()->id, 'warningcode' => '3', 'message' => 'No submissions found' ); } $assignments[] = array( 'assignmentid' => $assign->get_instance()->id, 'submissions' => $submissions ); } $result = array( 'assignments' => $assignments, 'warnings' => $warnings ); return $result; } /** * Creates an assignment plugin structure. * * @return external_single_structure the plugin structure */ private static function get_plugin_structure() { return new external_single_structure( array( 'type' => new external_value(PARAM_TEXT, 'submission plugin type'), 'name' => new external_value(PARAM_TEXT, 'submission plugin name'), 'fileareas' => new external_multiple_structure( new external_single_structure( array ( 'area' => new external_value (PARAM_TEXT, 'file area'), 'files' => new external_files('files', VALUE_OPTIONAL), ) ), 'fileareas', VALUE_OPTIONAL ), 'editorfields' => new external_multiple_structure( new external_single_structure( array( 'name' => new external_value(PARAM_TEXT, 'field name'), 'description' => new external_value(PARAM_RAW, 'field description'), 'text' => new external_value (PARAM_RAW, 'field value'), 'format' => new external_format_value ('text') ) ) , 'editorfields', VALUE_OPTIONAL ) ) ); } /** * Creates a submission structure. * * @return external_single_structure the submission structure */ private static function get_submission_structure($required = VALUE_REQUIRED) { return new external_single_structure( array( 'id' => new external_value(PARAM_INT, 'submission id'), 'userid' => new external_value(PARAM_INT, 'student id'), 'attemptnumber' => new external_value(PARAM_INT, 'attempt number'), 'timecreated' => new external_value(PARAM_INT, 'submission creation time'), 'timemodified' => new external_value(PARAM_INT, 'submission last modified time'), 'timestarted' => new external_value(PARAM_INT, 'submission start time', VALUE_OPTIONAL), 'status' => new external_value(PARAM_TEXT, 'submission status'), 'groupid' => new external_value(PARAM_INT, 'group id'), 'assignment' => new external_value(PARAM_INT, 'assignment id', VALUE_OPTIONAL), 'latest' => new external_value(PARAM_INT, 'latest attempt', VALUE_OPTIONAL), 'plugins' => new external_multiple_structure(self::get_plugin_structure(), 'plugins', VALUE_OPTIONAL), 'gradingstatus' => new external_value(PARAM_ALPHANUMEXT, 'Grading status.', VALUE_OPTIONAL), ), 'submission info', $required ); } /** * Creates an assign_submissions external_single_structure * * @return external_single_structure * @since Moodle 2.5 */ private static function get_submissions_structure() { return new external_single_structure( array ( 'assignmentid' => new external_value(PARAM_INT, 'assignment id'), 'submissions' => new external_multiple_structure(self::get_submission_structure()) ) ); } /** * Describes the get_submissions return value * * @return external_single_structure * @since Moodle 2.5 */ public static function get_submissions_returns() { return new external_single_structure( array( 'assignments' => new external_multiple_structure(self::get_submissions_structure(), 'assignment submissions'), 'warnings' => new external_warnings() ) ); } /** * Describes the parameters for set_user_flags * @return external_function_parameters * @since Moodle 2.6 */ public static function set_user_flags_parameters() { return new external_function_parameters( array( 'assignmentid' => new external_value(PARAM_INT, 'assignment id'), 'userflags' => new external_multiple_structure( new external_single_structure( array( 'userid' => new external_value(PARAM_INT, 'student id'), 'locked' => new external_value(PARAM_INT, 'locked', VALUE_OPTIONAL), 'mailed' => new external_value(PARAM_INT, 'mailed', VALUE_OPTIONAL), 'extensionduedate' => new external_value(PARAM_INT, 'extension due date', VALUE_OPTIONAL), 'workflowstate' => new external_value(PARAM_ALPHA, 'marking workflow state', VALUE_OPTIONAL), 'allocatedmarker' => new external_value(PARAM_INT, 'allocated marker', VALUE_OPTIONAL) ) ) ) ) ); } /** * Create or update user_flags records * * @param int $assignmentid the assignment for which the userflags are created or updated * @param array $userflags An array of userflags to create or update * @return array containing success or failure information for each record * @since Moodle 2.6 */ public static function set_user_flags($assignmentid, $userflags = array()) { global $CFG, $DB; $params = self::validate_parameters(self::set_user_flags_parameters(), array('assignmentid' => $assignmentid, 'userflags' => $userflags)); // Load assignment if it exists and if the user has the capability. list($assign, $course, $cm, $context) = self::validate_assign($params['assignmentid']); require_capability('mod/assign:grade', $context); $results = array(); foreach ($params['userflags'] as $userflag) { $success = true; $result = array(); $record = $assign->get_user_flags($userflag['userid'], false); if ($record) { if (isset($userflag['locked'])) { $record->locked = $userflag['locked']; } if (isset($userflag['mailed'])) { $record->mailed = $userflag['mailed']; } if (isset($userflag['extensionduedate'])) { $record->extensionduedate = $userflag['extensionduedate']; } if (isset($userflag['workflowstate'])) { $record->workflowstate = $userflag['workflowstate']; } if (isset($userflag['allocatedmarker'])) { $record->allocatedmarker = $userflag['allocatedmarker']; } if ($assign->update_user_flags($record)) { $result['id'] = $record->id; $result['userid'] = $userflag['userid']; } else { $result['id'] = $record->id; $result['userid'] = $userflag['userid']; $result['errormessage'] = 'Record created but values could not be set'; } } else { $record = $assign->get_user_flags($userflag['userid'], true); $setfields = isset($userflag['locked']) || isset($userflag['mailed']) || isset($userflag['extensionduedate']) || isset($userflag['workflowstate']) || isset($userflag['allocatedmarker']); if ($record) { if ($setfields) { if (isset($userflag['locked'])) { $record->locked = $userflag['locked']; } if (isset($userflag['mailed'])) { $record->mailed = $userflag['mailed']; } if (isset($userflag['extensionduedate'])) { $record->extensionduedate = $userflag['extensionduedate']; } if (isset($userflag['workflowstate'])) { $record->workflowstate = $userflag['workflowstate']; } if (isset($userflag['allocatedmarker'])) { $record->allocatedmarker = $userflag['allocatedmarker']; } if ($assign->update_user_flags($record)) { $result['id'] = $record->id; $result['userid'] = $userflag['userid']; } else { $result['id'] = $record->id; $result['userid'] = $userflag['userid']; $result['errormessage'] = 'Record created but values could not be set'; } } else { $result['id'] = $record->id; $result['userid'] = $userflag['userid']; } } else { $result['id'] = -1; $result['userid'] = $userflag['userid']; $result['errormessage'] = 'Record could not be created'; } } $results[] = $result; } return $results; } /** * Describes the set_user_flags return value * @return external_multiple_structure * @since Moodle 2.6 */ public static function set_user_flags_returns() { return new external_multiple_structure( new external_single_structure( array( 'id' => new external_value(PARAM_INT, 'id of record if successful, -1 for failure'), 'userid' => new external_value(PARAM_INT, 'userid of record'), 'errormessage' => new external_value(PARAM_TEXT, 'Failure error message', VALUE_OPTIONAL) ) ) ); } /** * Describes the parameters for get_user_flags * @return external_function_parameters * @since Moodle 2.6 */ public static function get_user_flags_parameters() { return new external_function_parameters( array( 'assignmentids' => new external_multiple_structure( new external_value(PARAM_INT, 'assignment id'), '1 or more assignment ids', VALUE_REQUIRED) ) ); } /** * Returns user flag information from assign_user_flags for the requested assignment ids * @param int[] $assignmentids * @return array of user flag records for each requested assignment * @since Moodle 2.6 */ public static function get_user_flags($assignmentids) { global $DB; $params = self::validate_parameters(self::get_user_flags_parameters(), array('assignmentids' => $assignmentids)); $assignments = array(); $warnings = array(); $requestedassignmentids = $params['assignmentids']; // Check the user is allowed to get the user flags for the assignments requested. $placeholders = array(); list($sqlassignmentids, $placeholders) = $DB->get_in_or_equal($requestedassignmentids, SQL_PARAMS_NAMED); $sql = "SELECT cm.id, cm.instance FROM {course_modules} cm JOIN {modules} md ON md.id = cm.module ". "WHERE md.name = :modname AND cm.instance ".$sqlassignmentids; $placeholders['modname'] = 'assign'; $cms = $DB->get_records_sql($sql, $placeholders); foreach ($cms as $cm) { try { $context = context_module::instance($cm->id); self::validate_context($context); require_capability('mod/assign:grade', $context); } catch (Exception $e) { $requestedassignmentids = array_diff($requestedassignmentids, array($cm->instance)); $warning = array(); $warning['item'] = 'assignment'; $warning['itemid'] = $cm->instance; $warning['warningcode'] = '1'; $warning['message'] = 'No access rights in module context'; $warnings[] = $warning; } } // Create the query and populate an array of assign_user_flags records from the recordset results. if (count ($requestedassignmentids) > 0) { $placeholders = array(); list($inorequalsql, $placeholders) = $DB->get_in_or_equal($requestedassignmentids, SQL_PARAMS_NAMED); $sql = "SELECT auf.id,auf.assignment,auf.userid,auf.locked,auf.mailed,". "auf.extensionduedate,auf.workflowstate,auf.allocatedmarker ". "FROM {assign_user_flags} auf ". "WHERE auf.assignment ".$inorequalsql. " ORDER BY auf.assignment, auf.id"; $rs = $DB->get_recordset_sql($sql, $placeholders); $currentassignmentid = null; $assignment = null; foreach ($rs as $rd) { $userflag = array(); $userflag['id'] = $rd->id; $userflag['userid'] = $rd->userid; $userflag['locked'] = $rd->locked; $userflag['mailed'] = $rd->mailed; $userflag['extensionduedate'] = $rd->extensionduedate; $userflag['workflowstate'] = $rd->workflowstate; $userflag['allocatedmarker'] = $rd->allocatedmarker; if (is_null($currentassignmentid) || ($rd->assignment != $currentassignmentid )) { if (!is_null($assignment)) { $assignments[] = $assignment; } $assignment = array(); $assignment['assignmentid'] = $rd->assignment; $assignment['userflags'] = array(); $requestedassignmentids = array_diff($requestedassignmentids, array($rd->assignment)); } $assignment['userflags'][] = $userflag; $currentassignmentid = $rd->assignment; } if (!is_null($assignment)) { $assignments[] = $assignment; } $rs->close(); } foreach ($requestedassignmentids as $assignmentid) { $warning = array(); $warning['item'] = 'assignment'; $warning['itemid'] = $assignmentid; $warning['warningcode'] = '3'; $warning['message'] = 'No user flags found'; $warnings[] = $warning; } $result = array(); $result['assignments'] = $assignments; $result['warnings'] = $warnings; return $result; } /** * Creates an assign_user_flags external_single_structure * @return external_single_structure * @since Moodle 2.6 */ private static function assign_user_flags() { return new external_single_structure( array ( 'assignmentid' => new external_value(PARAM_INT, 'assignment id'), 'userflags' => new external_multiple_structure(new external_single_structure( array( 'id' => new external_value(PARAM_INT, 'user flag id'), 'userid' => new external_value(PARAM_INT, 'student id'), 'locked' => new external_value(PARAM_INT, 'locked'), 'mailed' => new external_value(PARAM_INT, 'mailed'), 'extensionduedate' => new external_value(PARAM_INT, 'extension due date'), 'workflowstate' => new external_value(PARAM_ALPHA, 'marking workflow state', VALUE_OPTIONAL), 'allocatedmarker' => new external_value(PARAM_INT, 'allocated marker') ) ) ) ) ); } /** * Describes the get_user_flags return value * @return external_single_structure * @since Moodle 2.6 */ public static function get_user_flags_returns() { return new external_single_structure( array( 'assignments' => new external_multiple_structure(self::assign_user_flags(), 'list of assign user flag information'), 'warnings' => new external_warnings('item is always \'assignment\'', 'when errorcode is 3 then itemid is an assignment id. When errorcode is 1, itemid is a course module id', 'errorcode can be 3 (no user flags found) or 1 (no permission to get user flags)') ) ); } /** * Describes the parameters for get_user_mappings * @return external_function_parameters * @since Moodle 2.6 */ public static function get_user_mappings_parameters() { return new external_function_parameters( array( 'assignmentids' => new external_multiple_structure( new external_value(PARAM_INT, 'assignment id'), '1 or more assignment ids', VALUE_REQUIRED) ) ); } /** * Returns user mapping information from assign_user_mapping for the requested assignment ids * @param int[] $assignmentids * @return array of user mapping records for each requested assignment * @since Moodle 2.6 */ public static function get_user_mappings($assignmentids) { global $DB; $params = self::validate_parameters(self::get_user_mappings_parameters(), array('assignmentids' => $assignmentids)); $assignments = array(); $warnings = array(); $requestedassignmentids = $params['assignmentids']; // Check the user is allowed to get the mappings for the assignments requested. $placeholders = array(); list($sqlassignmentids, $placeholders) = $DB->get_in_or_equal($requestedassignmentids, SQL_PARAMS_NAMED); $sql = "SELECT cm.id, cm.instance FROM {course_modules} cm JOIN {modules} md ON md.id = cm.module ". "WHERE md.name = :modname AND cm.instance ".$sqlassignmentids; $placeholders['modname'] = 'assign'; $cms = $DB->get_records_sql($sql, $placeholders); foreach ($cms as $cm) { try { $context = context_module::instance($cm->id); self::validate_context($context); require_capability('mod/assign:revealidentities', $context); } catch (Exception $e) { $requestedassignmentids = array_diff($requestedassignmentids, array($cm->instance)); $warning = array(); $warning['item'] = 'assignment'; $warning['itemid'] = $cm->instance; $warning['warningcode'] = '1'; $warning['message'] = 'No access rights in module context'; $warnings[] = $warning; } } // Create the query and populate an array of assign_user_mapping records from the recordset results. if (count ($requestedassignmentids) > 0) { $placeholders = array(); list($inorequalsql, $placeholders) = $DB->get_in_or_equal($requestedassignmentids, SQL_PARAMS_NAMED); $sql = "SELECT aum.id,aum.assignment,aum.userid ". "FROM {assign_user_mapping} aum ". "WHERE aum.assignment ".$inorequalsql. " ORDER BY aum.assignment, aum.id"; $rs = $DB->get_recordset_sql($sql, $placeholders); $currentassignmentid = null; $assignment = null; foreach ($rs as $rd) { $mapping = array(); $mapping['id'] = $rd->id; $mapping['userid'] = $rd->userid; if (is_null($currentassignmentid) || ($rd->assignment != $currentassignmentid )) { if (!is_null($assignment)) { $assignments[] = $assignment; } $assignment = array(); $assignment['assignmentid'] = $rd->assignment; $assignment['mappings'] = array(); $requestedassignmentids = array_diff($requestedassignmentids, array($rd->assignment)); } $assignment['mappings'][] = $mapping; $currentassignmentid = $rd->assignment; } if (!is_null($assignment)) { $assignments[] = $assignment; } $rs->close(); } foreach ($requestedassignmentids as $assignmentid) { $warning = array(); $warning['item'] = 'assignment'; $warning['itemid'] = $assignmentid; $warning['warningcode'] = '3'; $warning['message'] = 'No mappings found'; $warnings[] = $warning; } $result = array(); $result['assignments'] = $assignments; $result['warnings'] = $warnings; return $result; } /** * Creates an assign_user_mappings external_single_structure * @return external_single_structure * @since Moodle 2.6 */ private static function assign_user_mappings() { return new external_single_structure( array ( 'assignmentid' => new external_value(PARAM_INT, 'assignment id'), 'mappings' => new external_multiple_structure(new external_single_structure( array( 'id' => new external_value(PARAM_INT, 'user mapping id'), 'userid' => new external_value(PARAM_INT, 'student id') ) ) ) ) ); } /** * Describes the get_user_mappings return value * @return external_single_structure * @since Moodle 2.6 */ public static function get_user_mappings_returns() { return new external_single_structure( array( 'assignments' => new external_multiple_structure(self::assign_user_mappings(), 'list of assign user mapping data'), 'warnings' => new external_warnings('item is always \'assignment\'', 'when errorcode is 3 then itemid is an assignment id. When errorcode is 1, itemid is a course module id', 'errorcode can be 3 (no user mappings found) or 1 (no permission to get user mappings)') ) ); } /** * Describes the parameters for lock_submissions * @return external_function_parameters * @since Moodle 2.6 */ public static function lock_submissions_parameters() { return new external_function_parameters( array( 'assignmentid' => new external_value(PARAM_INT, 'The assignment id to operate on'), 'userids' => new external_multiple_structure( new external_value(PARAM_INT, 'user id'), '1 or more user ids', VALUE_REQUIRED), ) ); } /** * Locks (prevent updates to) submissions in this assignment. * * @param int $assignmentid The id of the assignment * @param array $userids Array of user ids to lock * @return array of warnings for each submission that could not be locked. * @since Moodle 2.6 */ public static function lock_submissions($assignmentid, $userids) { global $CFG; $params = self::validate_parameters(self::lock_submissions_parameters(), array('assignmentid' => $assignmentid, 'userids' => $userids)); list($assignment, $course, $cm, $context) = self::validate_assign($params['assignmentid']); $warnings = array(); foreach ($params['userids'] as $userid) { if (!$assignment->lock_submission($userid)) { $detail = 'User id: ' . $userid . ', Assignment id: ' . $params['assignmentid']; $warnings[] = self::generate_warning($params['assignmentid'], 'couldnotlock', $detail); } } return $warnings; } /** * Describes the return value for lock_submissions * * @return external_single_structure * @since Moodle 2.6 */ public static function lock_submissions_returns() { return new external_warnings(); } /** * Describes the parameters for revert_submissions_to_draft * @return external_function_parameters * @since Moodle 2.6 */ public static function revert_submissions_to_draft_parameters() { return new external_function_parameters( array( 'assignmentid' => new external_value(PARAM_INT, 'The assignment id to operate on'), 'userids' => new external_multiple_structure( new external_value(PARAM_INT, 'user id'), '1 or more user ids', VALUE_REQUIRED), ) ); } /** * Reverts a list of user submissions to draft for a single assignment. * * @param int $assignmentid The id of the assignment * @param array $userids Array of user ids to revert * @return array of warnings for each submission that could not be reverted. * @since Moodle 2.6 */ public static function revert_submissions_to_draft($assignmentid, $userids) { global $CFG; $params = self::validate_parameters(self::revert_submissions_to_draft_parameters(), array('assignmentid' => $assignmentid, 'userids' => $userids)); list($assignment, $course, $cm, $context) = self::validate_assign($params['assignmentid']); $warnings = array(); foreach ($params['userids'] as $userid) { if (!$assignment->revert_to_draft($userid)) { $detail = 'User id: ' . $userid . ', Assignment id: ' . $params['assignmentid']; $warnings[] = self::generate_warning($params['assignmentid'], 'couldnotrevert', $detail); } } return $warnings; } /** * Describes the return value for revert_submissions_to_draft * * @return external_single_structure * @since Moodle 2.6 */ public static function revert_submissions_to_draft_returns() { return new external_warnings(); } /** * Describes the parameters for unlock_submissions * @return external_function_parameters * @since Moodle 2.6 */ public static function unlock_submissions_parameters() { return new external_function_parameters( array( 'assignmentid' => new external_value(PARAM_INT, 'The assignment id to operate on'), 'userids' => new external_multiple_structure( new external_value(PARAM_INT, 'user id'), '1 or more user ids', VALUE_REQUIRED), ) ); } /** * Locks (prevent updates to) submissions in this assignment. * * @param int $assignmentid The id of the assignment * @param array $userids Array of user ids to lock * @return array of warnings for each submission that could not be locked. * @since Moodle 2.6 */ public static function unlock_submissions($assignmentid, $userids) { global $CFG; $params = self::validate_parameters(self::unlock_submissions_parameters(), array('assignmentid' => $assignmentid, 'userids' => $userids)); list($assignment, $course, $cm, $context) = self::validate_assign($params['assignmentid']); $warnings = array(); foreach ($params['userids'] as $userid) { if (!$assignment->unlock_submission($userid)) { $detail = 'User id: ' . $userid . ', Assignment id: ' . $params['assignmentid']; $warnings[] = self::generate_warning($params['assignmentid'], 'couldnotunlock', $detail); } } return $warnings; } /** * Describes the return value for unlock_submissions * * @return external_single_structure * @since Moodle 2.6 */ public static function unlock_submissions_returns() { return new external_warnings(); } /** * Describes the parameters for submit_grading_form webservice. * @return external_function_parameters * @since Moodle 3.1 */ public static function submit_grading_form_parameters() { return new external_function_parameters( array( 'assignmentid' => new external_value(PARAM_INT, 'The assignment id to operate on'), 'userid' => new external_value(PARAM_INT, 'The user id the submission belongs to'), 'jsonformdata' => new external_value(PARAM_RAW, 'The data from the grading form, encoded as a json array') ) ); } /** * Submit the logged in users assignment for grading. * * @param int $assignmentid The id of the assignment * @param int $userid The id of the user the submission belongs to. * @param string $jsonformdata The data from the form, encoded as a json array. * @return array of warnings to indicate any errors. * @since Moodle 3.1 */ public static function submit_grading_form($assignmentid, $userid, $jsonformdata) { global $CFG, $USER; require_once($CFG->dirroot . '/mod/assign/locallib.php'); require_once($CFG->dirroot . '/mod/assign/gradeform.php'); $params = self::validate_parameters(self::submit_grading_form_parameters(), array( 'assignmentid' => $assignmentid, 'userid' => $userid, 'jsonformdata' => $jsonformdata )); list($assignment, $course, $cm, $context) = self::validate_assign($params['assignmentid']); $serialiseddata = json_decode($params['jsonformdata']); $data = array(); parse_str($serialiseddata, $data); $warnings = array(); $options = array( 'userid' => $params['userid'], 'attemptnumber' => $data['attemptnumber'], 'rownum' => 0, 'gradingpanel' => true ); if (WS_SERVER) { // Assume form submission if coming from WS. $USER->ignoresesskey = true; $data['_qf__mod_assign_grade_form_'.$params['userid']] = 1; } $customdata = (object) $data; $formparams = array($assignment, $customdata, $options); // Data is injected into the form by the last param for the constructor. $mform = new mod_assign_grade_form(null, $formparams, 'post', '', null, true, $data); $validateddata = $mform->get_data(); if ($validateddata) { $assignment->save_grade($params['userid'], $validateddata); } else { $warnings[] = self::generate_warning($params['assignmentid'], 'couldnotsavegrade', 'Form validation failed.'); } return $warnings; } /** * Describes the return for submit_grading_form * @return external_function_parameters * @since Moodle 3.1 */ public static function submit_grading_form_returns() { return new external_warnings(); } /** * Describes the parameters for submit_for_grading * @return external_function_parameters * @since Moodle 2.6 */ public static function submit_for_grading_parameters() { return new external_function_parameters( array( 'assignmentid' => new external_value(PARAM_INT, 'The assignment id to operate on'), 'acceptsubmissionstatement' => new external_value(PARAM_BOOL, 'Accept the assignment submission statement') ) ); } /** * Submit the logged in users assignment for grading. * * @param int $assignmentid The id of the assignment * @return array of warnings to indicate any errors. * @since Moodle 2.6 */ public static function submit_for_grading($assignmentid, $acceptsubmissionstatement) { global $CFG, $USER; $params = self::validate_parameters(self::submit_for_grading_parameters(), array('assignmentid' => $assignmentid, 'acceptsubmissionstatement' => $acceptsubmissionstatement)); list($assignment, $course, $cm, $context) = self::validate_assign($params['assignmentid']); $warnings = array(); $data = new stdClass(); $data->submissionstatement = $params['acceptsubmissionstatement']; $notices = array(); if (!$assignment->submit_for_grading($data, $notices)) { $detail = 'User id: ' . $USER->id . ', Assignment id: ' . $params['assignmentid'] . ' Notices:' . implode(', ', $notices); $warnings[] = self::generate_warning($params['assignmentid'], 'couldnotsubmitforgrading', $detail); } return $warnings; } /** * Describes the return value for submit_for_grading * * @return external_single_structure * @since Moodle 2.6 */ public static function submit_for_grading_returns() { return new external_warnings(); } /** * Describes the parameters for save_user_extensions * @return external_function_parameters * @since Moodle 2.6 */ public static function save_user_extensions_parameters() { return new external_function_parameters( array( 'assignmentid' => new external_value(PARAM_INT, 'The assignment id to operate on'), 'userids' => new external_multiple_structure( new external_value(PARAM_INT, 'user id'), '1 or more user ids', VALUE_REQUIRED), 'dates' => new external_multiple_structure( new external_value(PARAM_INT, 'dates'), '1 or more extension dates (timestamp)', VALUE_REQUIRED), ) ); } /** * Grant extension dates to students for an assignment. * * @param int $assignmentid The id of the assignment * @param array $userids Array of user ids to grant extensions to * @param array $dates Array of extension dates * @return array of warnings for each extension date that could not be granted * @since Moodle 2.6 */ public static function save_user_extensions($assignmentid, $userids, $dates) { global $CFG; $params = self::validate_parameters(self::save_user_extensions_parameters(), array('assignmentid' => $assignmentid, 'userids' => $userids, 'dates' => $dates)); if (count($params['userids']) != count($params['dates'])) { $detail = 'Length of userids and dates parameters differ.'; $warnings[] = self::generate_warning($params['assignmentid'], 'invalidparameters', $detail); return $warnings; } list($assignment, $course, $cm, $context) = self::validate_assign($params['assignmentid']); $warnings = array(); foreach ($params['userids'] as $idx => $userid) { $duedate = $params['dates'][$idx]; if (!$assignment->save_user_extension($userid, $duedate)) { $detail = 'User id: ' . $userid . ', Assignment id: ' . $params['assignmentid'] . ', Extension date: ' . $duedate; $warnings[] = self::generate_warning($params['assignmentid'], 'couldnotgrantextensions', $detail); } } return $warnings; } /** * Describes the return value for save_user_extensions * * @return external_single_structure * @since Moodle 2.6 */ public static function save_user_extensions_returns() { return new external_warnings(); } /** * Describes the parameters for reveal_identities * @return external_function_parameters * @since Moodle 2.6 */ public static function reveal_identities_parameters() { return new external_function_parameters( array( 'assignmentid' => new external_value(PARAM_INT, 'The assignment id to operate on') ) ); } /** * Reveal the identities of anonymous students to markers for a single assignment. * * @param int $assignmentid The id of the assignment * @return array of warnings to indicate any errors. * @since Moodle 2.6 */ public static function reveal_identities($assignmentid) { global $CFG, $USER; $params = self::validate_parameters(self::reveal_identities_parameters(), array('assignmentid' => $assignmentid)); list($assignment, $course, $cm, $context) = self::validate_assign($params['assignmentid']); $warnings = array(); if (!$assignment->reveal_identities()) { $detail = 'User id: ' . $USER->id . ', Assignment id: ' . $params['assignmentid']; $warnings[] = self::generate_warning($params['assignmentid'], 'couldnotrevealidentities', $detail); } return $warnings; } /** * Describes the return value for reveal_identities * * @return external_single_structure * @since Moodle 2.6 */ public static function reveal_identities_returns() { return new external_warnings(); } /** * Describes the parameters for save_submission * @return external_function_parameters * @since Moodle 2.6 */ public static function save_submission_parameters() { global $CFG; $instance = new assign(null, null, null); $pluginsubmissionparams = array(); foreach ($instance->get_submission_plugins() as $plugin) { if ($plugin->is_visible()) { $pluginparams = $plugin->get_external_parameters(); if (!empty($pluginparams)) { $pluginsubmissionparams = array_merge($pluginsubmissionparams, $pluginparams); } } } return new external_function_parameters( array( 'assignmentid' => new external_value(PARAM_INT, 'The assignment id to operate on'), 'plugindata' => new external_single_structure( $pluginsubmissionparams ) ) ); } /** * Save a student submission for a single assignment * * @param int $assignmentid The id of the assignment * @param array $plugindata - The submitted data for plugins * @return array of warnings to indicate any errors * @since Moodle 2.6 */ public static function save_submission($assignmentid, $plugindata) { global $CFG, $USER; $params = self::validate_parameters(self::save_submission_parameters(), array('assignmentid' => $assignmentid, 'plugindata' => $plugindata)); list($assignment, $course, $cm, $context) = self::validate_assign($params['assignmentid']); $notices = array(); $assignment->update_effective_access($USER->id); if (!$assignment->submissions_open($USER->id)) { $notices[] = get_string('duedatereached', 'assign'); } else { $submissiondata = (object)$params['plugindata']; $assignment->save_submission($submissiondata, $notices); } $warnings = array(); foreach ($notices as $notice) { $warnings[] = self::generate_warning($params['assignmentid'], 'couldnotsavesubmission', $notice); } return $warnings; } /** * Describes the return value for save_submission * * @return external_single_structure * @since Moodle 2.6 */ public static function save_submission_returns() { return new external_warnings(); } /** * Describes the parameters for save_grade * @return external_function_parameters * @since Moodle 2.6 */ public static function save_grade_parameters() { global $CFG; require_once("$CFG->dirroot/grade/grading/lib.php"); $instance = new assign(null, null, null); $pluginfeedbackparams = array(); foreach ($instance->get_feedback_plugins() as $plugin) { if ($plugin->is_visible()) { $pluginparams = $plugin->get_external_parameters(); if (!empty($pluginparams)) { $pluginfeedbackparams = array_merge($pluginfeedbackparams, $pluginparams); } } } $advancedgradingdata = array(); $methods = array_keys(grading_manager::available_methods(false)); foreach ($methods as $method) { require_once($CFG->dirroot.'/grade/grading/form/'.$method.'/lib.php'); $details = call_user_func('gradingform_'.$method.'_controller::get_external_instance_filling_details'); if (!empty($details)) { $items = array(); foreach ($details as $key => $value) { $value->required = VALUE_OPTIONAL; unset($value->content->keys['id']); $items[$key] = new external_multiple_structure (new external_single_structure( array( 'criterionid' => new external_value(PARAM_INT, 'criterion id'), 'fillings' => $value ) )); } $advancedgradingdata[$method] = new external_single_structure($items, 'items', VALUE_OPTIONAL); } } return new external_function_parameters( array( 'assignmentid' => new external_value(PARAM_INT, 'The assignment id to operate on'), 'userid' => new external_value(PARAM_INT, 'The student id to operate on'), 'grade' => new external_value(PARAM_FLOAT, 'The new grade for this user. Ignored if advanced grading used'), 'attemptnumber' => new external_value(PARAM_INT, 'The attempt number (-1 means latest attempt)'), 'addattempt' => new external_value(PARAM_BOOL, 'Allow another attempt if the attempt reopen method is manual'), 'workflowstate' => new external_value(PARAM_ALPHA, 'The next marking workflow state'), 'applytoall' => new external_value(PARAM_BOOL, 'If true, this grade will be applied ' . 'to all members ' . 'of the group (for group assignments).'), 'plugindata' => new external_single_structure($pluginfeedbackparams, 'plugin data', VALUE_DEFAULT, array()), 'advancedgradingdata' => new external_single_structure($advancedgradingdata, 'advanced grading data', VALUE_DEFAULT, array()) ) ); } /** * Save a student grade for a single assignment. * * @param int $assignmentid The id of the assignment * @param int $userid The id of the user * @param float $grade The grade (ignored if the assignment uses advanced grading) * @param int $attemptnumber The attempt number * @param bool $addattempt Allow another attempt * @param string $workflowstate New workflow state * @param bool $applytoall Apply the grade to all members of the group * @param array $plugindata Custom data used by plugins * @param array $advancedgradingdata Advanced grading data * @return null * @since Moodle 2.6 */ public static function save_grade($assignmentid, $userid, $grade, $attemptnumber, $addattempt, $workflowstate, $applytoall, $plugindata = array(), $advancedgradingdata = array()) { global $CFG, $USER; $params = self::validate_parameters(self::save_grade_parameters(), array('assignmentid' => $assignmentid, 'userid' => $userid, 'grade' => $grade, 'attemptnumber' => $attemptnumber, 'workflowstate' => $workflowstate, 'addattempt' => $addattempt, 'applytoall' => $applytoall, 'plugindata' => $plugindata, 'advancedgradingdata' => $advancedgradingdata)); list($assignment, $course, $cm, $context) = self::validate_assign($params['assignmentid']); $gradedata = (object)$params['plugindata']; $gradedata->addattempt = $params['addattempt']; $gradedata->attemptnumber = $params['attemptnumber']; $gradedata->workflowstate = $params['workflowstate']; $gradedata->applytoall = $params['applytoall']; $gradedata->grade = $params['grade']; if (!empty($params['advancedgradingdata'])) { $advancedgrading = array(); $criteria = reset($params['advancedgradingdata']); foreach ($criteria as $key => $criterion) { $details = array(); foreach ($criterion as $value) { foreach ($value['fillings'] as $filling) { $details[$value['criterionid']] = $filling; } } $advancedgrading[$key] = $details; } $gradedata->advancedgrading = $advancedgrading; } $assignment->save_grade($params['userid'], $gradedata); return null; } /** * Describes the return value for save_grade * * @return external_single_structure * @since Moodle 2.6 */ public static function save_grade_returns() { return null; } /** * Describes the parameters for save_grades * @return external_function_parameters * @since Moodle 2.7 */ public static function save_grades_parameters() { global $CFG; require_once("$CFG->dirroot/grade/grading/lib.php"); $instance = new assign(null, null, null); $pluginfeedbackparams = array(); foreach ($instance->get_feedback_plugins() as $plugin) { if ($plugin->is_visible()) { $pluginparams = $plugin->get_external_parameters(); if (!empty($pluginparams)) { $pluginfeedbackparams = array_merge($pluginfeedbackparams, $pluginparams); } } } $advancedgradingdata = array(); $methods = array_keys(grading_manager::available_methods(false)); foreach ($methods as $method) { require_once($CFG->dirroot.'/grade/grading/form/'.$method.'/lib.php'); $details = call_user_func('gradingform_'.$method.'_controller::get_external_instance_filling_details'); if (!empty($details)) { $items = array(); foreach ($details as $key => $value) { $value->required = VALUE_OPTIONAL; unset($value->content->keys['id']); $items[$key] = new external_multiple_structure (new external_single_structure( array( 'criterionid' => new external_value(PARAM_INT, 'criterion id'), 'fillings' => $value ) )); } $advancedgradingdata[$method] = new external_single_structure($items, 'items', VALUE_OPTIONAL); } } return new external_function_parameters( array( 'assignmentid' => new external_value(PARAM_INT, 'The assignment id to operate on'), 'applytoall' => new external_value(PARAM_BOOL, 'If true, this grade will be applied ' . 'to all members ' . 'of the group (for group assignments).'), 'grades' => new external_multiple_structure( new external_single_structure( array ( 'userid' => new external_value(PARAM_INT, 'The student id to operate on'), 'grade' => new external_value(PARAM_FLOAT, 'The new grade for this user. '. 'Ignored if advanced grading used'), 'attemptnumber' => new external_value(PARAM_INT, 'The attempt number (-1 means latest attempt)'), 'addattempt' => new external_value(PARAM_BOOL, 'Allow another attempt if manual attempt reopen method'), 'workflowstate' => new external_value(PARAM_ALPHA, 'The next marking workflow state'), 'plugindata' => new external_single_structure($pluginfeedbackparams, 'plugin data', VALUE_DEFAULT, array()), 'advancedgradingdata' => new external_single_structure($advancedgradingdata, 'advanced grading data', VALUE_DEFAULT, array()) ) ) ) ) ); } /** * Save multiple student grades for a single assignment. * * @param int $assignmentid The id of the assignment * @param boolean $applytoall If set to true and this is a team assignment, * apply the grade to all members of the group * @param array $grades grade data for one or more students that includes * userid - The id of the student being graded * grade - The grade (ignored if the assignment uses advanced grading) * attemptnumber - The attempt number * addattempt - Allow another attempt * workflowstate - New workflow state * plugindata - Custom data used by plugins * advancedgradingdata - Optional Advanced grading data * @throws invalid_parameter_exception if multiple grades are supplied for * a team assignment that has $applytoall set to true * @return null * @since Moodle 2.7 */ public static function save_grades($assignmentid, $applytoall, $grades) { global $CFG, $USER; $params = self::validate_parameters(self::save_grades_parameters(), array('assignmentid' => $assignmentid, 'applytoall' => $applytoall, 'grades' => $grades)); list($assignment, $course, $cm, $context) = self::validate_assign($params['assignmentid']); if ($assignment->get_instance()->teamsubmission && $params['applytoall']) { // Check that only 1 user per submission group is provided. $groupids = array(); foreach ($params['grades'] as $gradeinfo) { $group = $assignment->get_submission_group($gradeinfo['userid']); if (in_array($group->id, $groupids)) { throw new invalid_parameter_exception('Multiple grades for the same team have been supplied ' .' this is not permitted when the applytoall flag is set'); } else { $groupids[] = $group->id; } } } foreach ($params['grades'] as $gradeinfo) { $gradedata = (object)$gradeinfo['plugindata']; $gradedata->addattempt = $gradeinfo['addattempt']; $gradedata->attemptnumber = $gradeinfo['attemptnumber']; $gradedata->workflowstate = $gradeinfo['workflowstate']; $gradedata->applytoall = $params['applytoall']; $gradedata->grade = $gradeinfo['grade']; if (!empty($gradeinfo['advancedgradingdata'])) { $advancedgrading = array(); $criteria = reset($gradeinfo['advancedgradingdata']); foreach ($criteria as $key => $criterion) { $details = array(); foreach ($criterion as $value) { foreach ($value['fillings'] as $filling) { $details[$value['criterionid']] = $filling; } } $advancedgrading[$key] = $details; } $gradedata->advancedgrading = $advancedgrading; } $assignment->save_grade($gradeinfo['userid'], $gradedata); } return null; } /** * Describes the return value for save_grades * * @return external_single_structure * @since Moodle 2.7 */ public static function save_grades_returns() { return null; } /** * Describes the parameters for copy_previous_attempt * @return external_function_parameters * @since Moodle 2.6 */ public static function copy_previous_attempt_parameters() { return new external_function_parameters( array( 'assignmentid' => new external_value(PARAM_INT, 'The assignment id to operate on'), ) ); } /** * Copy a students previous attempt to a new attempt. * * @param int $assignmentid * @return array of warnings to indicate any errors. * @since Moodle 2.6 */ public static function copy_previous_attempt($assignmentid) { $params = self::validate_parameters(self::copy_previous_attempt_parameters(), array('assignmentid' => $assignmentid)); list($assignment, $course, $cm, $context) = self::validate_assign($params['assignmentid']); $notices = array(); $assignment->copy_previous_attempt($notices); $warnings = array(); foreach ($notices as $notice) { $warnings[] = self::generate_warning($assignmentid, 'couldnotcopyprevioussubmission', $notice); } return $warnings; } /** * Describes the return value for save_submission * * @return external_single_structure * @since Moodle 2.6 */ public static function copy_previous_attempt_returns() { return new external_warnings(); } /** * Returns description of method parameters * * @return external_function_parameters * @since Moodle 3.0 */ public static function view_grading_table_parameters() { return new external_function_parameters( array( 'assignid' => new external_value(PARAM_INT, 'assign instance id') ) ); } /** * Trigger the grading_table_viewed event. * * @param int $assignid the assign instance id * @return array of warnings and status result * @since Moodle 3.0 * @throws moodle_exception */ public static function view_grading_table($assignid) { $params = self::validate_parameters(self::view_grading_table_parameters(), array( 'assignid' => $assignid )); $warnings = array(); list($assign, $course, $cm, $context) = self::validate_assign($params['assignid']); $assign->require_view_grades(); \mod_assign\event\grading_table_viewed::create_from_assign($assign)->trigger(); $result = array(); $result['status'] = true; $result['warnings'] = $warnings; return $result; } /** * Returns description of method result value * * @return \core_external\external_description * @since Moodle 3.0 */ public static function view_grading_table_returns() { return new external_single_structure([ 'status' => new external_value(PARAM_BOOL, 'status: true if success'), 'warnings' => new external_warnings() ]); } /** * Describes the parameters for view_submission_status. * * @return external_function_parameters * @since Moodle 3.1 */ public static function view_submission_status_parameters() { return new external_function_parameters ([ 'assignid' => new external_value(PARAM_INT, 'assign instance id'), ]); } /** * Trigger the submission status viewed event. * * @param int $assignid assign instance id * @return array of warnings and status result * @since Moodle 3.1 */ public static function view_submission_status($assignid) { $warnings = array(); $params = array( 'assignid' => $assignid, ); $params = self::validate_parameters(self::view_submission_status_parameters(), $params); list($assign, $course, $cm, $context) = self::validate_assign($params['assignid']); \mod_assign\event\submission_status_viewed::create_from_assign($assign)->trigger(); $result = array(); $result['status'] = true; $result['warnings'] = $warnings; return $result; } /** * Describes the view_submission_status return value. * * @return external_single_structure * @since Moodle 3.1 */ public static function view_submission_status_returns() { return new external_single_structure( array( 'status' => new external_value(PARAM_BOOL, 'status: true if success'), 'warnings' => new external_warnings(), ) ); } /** * Describes the parameters for get_submission_status. * * @return external_function_parameters * @since Moodle 3.1 */ public static function get_submission_status_parameters() { return new external_function_parameters ( array( 'assignid' => new external_value(PARAM_INT, 'assignment instance id'), 'userid' => new external_value(PARAM_INT, 'user id (empty for current user)', VALUE_DEFAULT, 0), 'groupid' => new external_value(PARAM_INT, 'filter by users in group (used for generating the grading summary). 0 for all groups information, any other empty value will calculate currrent group.', VALUE_DEFAULT, 0), ) ); } /** * Returns information about an assignment submission status for a given user. * * @param int $assignid assignment instance id * @param int $userid user id (empty for current user) * @param int $groupid filter by users in group id (used for generating the grading summary). Use 0 for all groups information. * @return array of warnings and grading, status, feedback and previous attempts information * @since Moodle 3.1 * @throws required_capability_exception */ public static function get_submission_status($assignid, $userid = 0, $groupid = 0) { global $USER; $warnings = array(); $params = array( 'assignid' => $assignid, 'userid' => $userid, 'groupid' => $groupid, ); $params = self::validate_parameters(self::get_submission_status_parameters(), $params); list($assign, $course, $cm, $context) = self::validate_assign($params['assignid']); // Default value for userid. if (empty($params['userid'])) { $params['userid'] = $USER->id; } $user = core_user::get_user($params['userid'], '*', MUST_EXIST); core_user::require_active_user($user); if (!$assign->can_view_submission($user->id)) { throw new required_capability_exception($context, 'mod/assign:viewgrades', 'nopermission', ''); } $assign->update_effective_access($user->id); $gradingsummary = $lastattempt = $feedback = $previousattempts = null; // Get the renderable since it contais all the info we need. if (!empty($params['groupid'])) { $groupid = $params['groupid']; // Determine is the group is visible to user. if (!groups_group_visible($groupid, $course, $cm)) { throw new moodle_exception('notingroup'); } } else { // A null group means that following functions will calculate the current group. // A groupid set to 0 means all groups. $groupid = ($params['groupid'] == 0) ? 0 : null; } if ($assign->can_view_grades($groupid)) { $gradingsummary = $assign->get_assign_grading_summary_renderable($groupid); } // Retrieve the rest of the renderable objects. if (has_capability('mod/assign:viewownsubmissionsummary', $context, $user, false)) { // The user can view the submission summary. $lastattempt = $assign->get_assign_submission_status_renderable($user, true); } $feedback = $assign->get_assign_feedback_status_renderable($user); $previousattempts = $assign->get_assign_attempt_history_renderable($user); // Now, build the result. $result = array(); // First of all, grading summary, this is suitable for teachers/managers. if ($gradingsummary) { $result['gradingsummary'] = $gradingsummary; } // Show the grader's identity if 'Hide Grader' is disabled or has the 'Show Hidden Grader' capability. $showgradername = (has_capability('mod/assign:showhiddengrader', $context) or !$assign->is_hidden_grader()); // Did we submit anything? if ($lastattempt) { $submissionplugins = $assign->get_submission_plugins(); if (empty($lastattempt->submission)) { unset($lastattempt->submission); } else { $lastattempt->submission->plugins = self::get_plugins_data($assign, $submissionplugins, $lastattempt->submission); } if (empty($lastattempt->teamsubmission)) { unset($lastattempt->teamsubmission); } else { $lastattempt->teamsubmission->plugins = self::get_plugins_data($assign, $submissionplugins, $lastattempt->teamsubmission); } // We need to change the type of some of the structures retrieved from the renderable. if (!empty($lastattempt->submissiongroup)) { $lastattempt->submissiongroup = $lastattempt->submissiongroup->id; } else { unset($lastattempt->submissiongroup); } if (!empty($lastattempt->usergroups)) { $lastattempt->usergroups = array_keys($lastattempt->usergroups); } // We cannot use array_keys here. if (!empty($lastattempt->submissiongroupmemberswhoneedtosubmit)) { $lastattempt->submissiongroupmemberswhoneedtosubmit = array_map( function($e){ return $e->id; }, $lastattempt->submissiongroupmemberswhoneedtosubmit); } // Can edit its own submission? $lastattempt->caneditowner = has_capability('mod/assign:submit', $context, $user, false) && $assign->submissions_open($user->id) && $assign->is_any_submission_plugin_enabled(); $result['lastattempt'] = $lastattempt; } // The feedback for our latest submission. if ($feedback) { if ($feedback->grade) { if (!$showgradername) { $feedback->grade->grader = -1; } $feedbackplugins = $assign->get_feedback_plugins(); $feedback->plugins = self::get_plugins_data($assign, $feedbackplugins, $feedback->grade); } else { unset($feedback->plugins); unset($feedback->grade); } $result['feedback'] = $feedback; } // Retrieve only previous attempts. if ($previousattempts and count($previousattempts->submissions) > 1) { // Don't show the last one because it is the current submission. array_pop($previousattempts->submissions); // Show newest to oldest. $previousattempts->submissions = array_reverse($previousattempts->submissions); foreach ($previousattempts->submissions as $i => $submission) { $attempt = array(); $grade = null; foreach ($previousattempts->grades as $onegrade) { if ($onegrade->attemptnumber == $submission->attemptnumber) { $grade = $onegrade; break; } } $attempt['attemptnumber'] = $submission->attemptnumber; if ($submission) { $submission->plugins = self::get_plugins_data($assign, $previousattempts->submissionplugins, $submission); $attempt['submission'] = $submission; } if ($grade) { // From object to id. if (!$showgradername) { $grade->grader = -1; } else { $grade->grader = $grade->grader->id; } $feedbackplugins = self::get_plugins_data($assign, $previousattempts->feedbackplugins, $grade); $attempt['grade'] = $grade; $attempt['feedbackplugins'] = $feedbackplugins; } $result['previousattempts'][] = $attempt; } } // Send back some assignment data as well. $instance = $assign->get_instance(); $assignmentdata = []; $attachments = []; if ($assign->should_provide_intro_attachments($user->id)) { $attachments['intro'] = external_util::get_area_files($context->id, 'mod_assign', ASSIGN_INTROATTACHMENT_FILEAREA, 0); } if ($instance->activity && ($lastattempt || $assign->submissions_open($user->id, true))) { [$assignmentdata['activity'], $assignmentdata['activityformat']] = \core_external\util::format_text( $instance->activity, $instance->activityformat, $context, 'mod_assign', ASSIGN_ACTIVITYATTACHMENT_FILEAREA, 0 ); $attachments['activity'] = external_util::get_area_files($context->id, 'mod_assign', ASSIGN_ACTIVITYATTACHMENT_FILEAREA, 0); } if (!empty($attachments)) { $assignmentdata['attachments'] = $attachments; } $result['assignmentdata'] = $assignmentdata; $result['warnings'] = $warnings; return $result; } /** * Describes the get_submission_status return value. * * @return external_single_structure * @since Moodle 3.1 */ public static function get_submission_status_returns() { return new external_single_structure( array( 'gradingsummary' => new external_single_structure( array( 'participantcount' => new external_value(PARAM_INT, 'Number of users who can submit.'), 'submissiondraftscount' => new external_value(PARAM_INT, 'Number of submissions in draft status.'), 'submissionsenabled' => new external_value(PARAM_BOOL, 'Whether submissions are enabled or not.'), 'submissionssubmittedcount' => new external_value(PARAM_INT, 'Number of submissions in submitted status.'), 'submissionsneedgradingcount' => new external_value(PARAM_INT, 'Number of submissions that need grading.'), 'warnofungroupedusers' => new external_value(PARAM_ALPHA, 'Whether we need to warn people that there are users without groups (\'warningrequired\'), warn people there are users who will submit in the default group (\'warningoptional\') or no warning (\'\').'), ), 'Grading information.', VALUE_OPTIONAL ), 'lastattempt' => new external_single_structure( array( 'submission' => self::get_submission_structure(VALUE_OPTIONAL), 'teamsubmission' => self::get_submission_structure(VALUE_OPTIONAL), 'submissiongroup' => new external_value(PARAM_INT, 'The submission group id (for group submissions only).', VALUE_OPTIONAL), 'submissiongroupmemberswhoneedtosubmit' => new external_multiple_structure( new external_value(PARAM_INT, 'USER id.'), 'List of users who still need to submit (for group submissions only).', VALUE_OPTIONAL ), 'submissionsenabled' => new external_value(PARAM_BOOL, 'Whether submissions are enabled or not.'), 'locked' => new external_value(PARAM_BOOL, 'Whether new submissions are locked.'), 'graded' => new external_value(PARAM_BOOL, 'Whether the submission is graded.'), 'canedit' => new external_value(PARAM_BOOL, 'Whether the user can edit the current submission.'), 'caneditowner' => new external_value(PARAM_BOOL, 'Whether the owner of the submission can edit it.'), 'cansubmit' => new external_value(PARAM_BOOL, 'Whether the user can submit.'), 'extensionduedate' => new external_value(PARAM_INT, 'Extension due date.'), 'timelimit' => new external_value(PARAM_INT, 'Time limit for submission.', VALUE_OPTIONAL), 'blindmarking' => new external_value(PARAM_BOOL, 'Whether blind marking is enabled.'), 'gradingstatus' => new external_value(PARAM_ALPHANUMEXT, 'Grading status.'), 'usergroups' => new external_multiple_structure( new external_value(PARAM_INT, 'Group id.'), 'User groups in the course.' ), ), 'Last attempt information.', VALUE_OPTIONAL ), 'feedback' => new external_single_structure( array( 'grade' => self::get_grade_structure(VALUE_OPTIONAL), 'gradefordisplay' => new external_value(PARAM_RAW, 'Grade rendered into a format suitable for display.'), 'gradeddate' => new external_value(PARAM_INT, 'The date the user was graded.'), 'plugins' => new external_multiple_structure(self::get_plugin_structure(), 'Plugins info.', VALUE_OPTIONAL), ), 'Feedback for the last attempt.', VALUE_OPTIONAL ), 'previousattempts' => new external_multiple_structure( new external_single_structure( array( 'attemptnumber' => new external_value(PARAM_INT, 'Attempt number.'), 'submission' => self::get_submission_structure(VALUE_OPTIONAL), 'grade' => self::get_grade_structure(VALUE_OPTIONAL), 'feedbackplugins' => new external_multiple_structure(self::get_plugin_structure(), 'Feedback info.', VALUE_OPTIONAL), ) ), 'List all the previous attempts did by the user.', VALUE_OPTIONAL ), 'assignmentdata' => new external_single_structure([ 'attachments' => new external_single_structure([ 'intro' => new external_files('Intro attachments files', VALUE_OPTIONAL), 'activity' => new external_files('Activity attachments files', VALUE_OPTIONAL), ], 'Intro and activity attachments', VALUE_OPTIONAL), 'activity' => new external_value(PARAM_RAW, 'Text of activity', VALUE_OPTIONAL), 'activityformat' => new external_format_value('activity', VALUE_OPTIONAL), ], 'Extra information about assignment', VALUE_OPTIONAL), 'warnings' => new external_warnings(), ) ); } /** * Returns description of method parameters * * @return external_function_parameters * @since Moodle 3.1 */ public static function list_participants_parameters() { return new external_function_parameters( array( 'assignid' => new external_value(PARAM_INT, 'assign instance id'), 'groupid' => new external_value(PARAM_INT, 'group id'), 'filter' => new external_value(PARAM_RAW, 'search string to filter the results'), 'skip' => new external_value(PARAM_INT, 'number of records to skip', VALUE_DEFAULT, 0), 'limit' => new external_value(PARAM_INT, 'maximum number of records to return', VALUE_DEFAULT, 0), 'onlyids' => new external_value(PARAM_BOOL, 'Do not return all user fields', VALUE_DEFAULT, false), 'includeenrolments' => new external_value(PARAM_BOOL, 'Do return courses where the user is enrolled', VALUE_DEFAULT, true), 'tablesort' => new external_value(PARAM_BOOL, 'Apply current user table sorting preferences.', VALUE_DEFAULT, false) ) ); } /** * Retrieves the list of students to be graded for the assignment. * * @param int $assignid the assign instance id * @param int $groupid the current group id * @param string $filter search string to filter the results. * @param int $skip Number of records to skip * @param int $limit Maximum number of records to return * @param bool $onlyids Only return user ids. * @param bool $includeenrolments Return courses where the user is enrolled. * @param bool $tablesort Apply current user table sorting params from the grading table. * @return array of warnings and status result * @since Moodle 3.1 * @throws moodle_exception */ public static function list_participants($assignid, $groupid, $filter, $skip, $limit, $onlyids, $includeenrolments, $tablesort) { global $DB, $CFG; require_once($CFG->dirroot . "/mod/assign/locallib.php"); require_once($CFG->dirroot . "/user/lib.php"); require_once($CFG->libdir . '/grouplib.php'); $params = self::validate_parameters(self::list_participants_parameters(), array( 'assignid' => $assignid, 'groupid' => $groupid, 'filter' => $filter, 'skip' => $skip, 'limit' => $limit, 'onlyids' => $onlyids, 'includeenrolments' => $includeenrolments, 'tablesort' => $tablesort )); $warnings = array(); list($assign, $course, $cm, $context) = self::validate_assign($params['assignid']); require_capability('mod/assign:view', $context); $assign->require_view_grades(); $participants = array(); $coursegroups = []; if (groups_group_visible($params['groupid'], $course, $cm)) { $participants = $assign->list_participants_with_filter_status_and_group($params['groupid'], $params['tablesort']); $coursegroups = groups_get_all_groups($course->id); } $userfields = user_get_default_fields(); if (!$params['includeenrolments']) { // Remove enrolled courses from users fields to be returned. $key = array_search('enrolledcourses', $userfields); if ($key !== false) { unset($userfields[$key]); } else { throw new moodle_exception('invaliduserfield', 'error', '', 'enrolledcourses'); } } $result = array(); $index = 0; foreach ($participants as $record) { // Preserve the fullname set by the assignment. $fullname = $record->fullname; $searchable = $fullname; $match = false; if (empty($filter)) { $match = true; } else { $filter = core_text::strtolower($filter); $value = core_text::strtolower($searchable); if (is_string($value) && (core_text::strpos($value, $filter) !== false)) { $match = true; } } if ($match) { $index++; if ($index <= $params['skip']) { continue; } if (($params['limit'] > 0) && (($index - $params['skip']) > $params['limit'])) { break; } // Now we do the expensive lookup of user details because we completed the filtering. if (!$assign->is_blind_marking() && !$params['onlyids']) { $userdetails = user_get_user_details($record, $course, $userfields); } else { $userdetails = array('id' => $record->id); } $userdetails['fullname'] = $fullname; $userdetails['submitted'] = $record->submitted; $userdetails['requiregrading'] = $record->requiregrading; $userdetails['grantedextension'] = $record->grantedextension; $userdetails['submissionstatus'] = $record->submissionstatus; if (!empty($record->groupid)) { $userdetails['groupid'] = $record->groupid; if (!empty($coursegroups[$record->groupid])) { // Format properly the group name. $group = $coursegroups[$record->groupid]; $userdetails['groupname'] = \core_external\util::format_string($group->name, $context); } } // Unique id is required for blind marking. $userdetails['recordid'] = -1; if (!empty($record->recordid)) { $userdetails['recordid'] = $record->recordid; } $result[] = $userdetails; } } return $result; } /** * Returns the description of the results of the mod_assign_external::list_participants() method. * * @return \core_external\external_description * @since Moodle 3.1 */ public static function list_participants_returns() { // Get user description. $userdesc = core_user_external::user_description(); // List unneeded properties. $unneededproperties = [ 'auth', 'confirmed', 'lang', 'calendartype', 'theme', 'timezone', 'mailformat' ]; // Remove unneeded properties for consistency with the previous version. foreach ($unneededproperties as $prop) { unset($userdesc->keys[$prop]); } // Override property attributes for consistency with the previous version. $userdesc->keys['fullname']->type = PARAM_NOTAGS; $userdesc->keys['profileimageurlsmall']->required = VALUE_OPTIONAL; $userdesc->keys['profileimageurl']->required = VALUE_OPTIONAL; $userdesc->keys['email']->desc = 'Email address'; $userdesc->keys['idnumber']->desc = 'The idnumber of the user'; $userdesc->keys['recordid'] = new external_value(PARAM_INT, 'record id'); // Define other keys. $otherkeys = [ 'groups' => new external_multiple_structure( new external_single_structure( [ 'id' => new external_value(PARAM_INT, 'group id'), 'name' => new external_value(PARAM_RAW, 'group name'), 'description' => new external_value(PARAM_RAW, 'group description'), ] ), 'user groups', VALUE_OPTIONAL ), 'roles' => new external_multiple_structure( new external_single_structure( [ 'roleid' => new external_value(PARAM_INT, 'role id'), 'name' => new external_value(PARAM_RAW, 'role name'), 'shortname' => new external_value(PARAM_ALPHANUMEXT, 'role shortname'), 'sortorder' => new external_value(PARAM_INT, 'role sortorder') ] ), 'user roles', VALUE_OPTIONAL ), 'enrolledcourses' => new external_multiple_structure( new external_single_structure( [ 'id' => new external_value(PARAM_INT, 'Id of the course'), 'fullname' => new external_value(PARAM_RAW, 'Fullname of the course'), 'shortname' => new external_value(PARAM_RAW, 'Shortname of the course') ] ), 'Courses where the user is enrolled - limited by which courses the user is able to see', VALUE_OPTIONAL ), 'submitted' => new external_value(PARAM_BOOL, 'have they submitted their assignment'), 'requiregrading' => new external_value(PARAM_BOOL, 'is their submission waiting for grading'), 'grantedextension' => new external_value(PARAM_BOOL, 'have they been granted an extension'), 'submissionstatus' => new external_value(PARAM_ALPHA, 'The submission status (new, draft, reopened or submitted). Empty when not submitted.', VALUE_OPTIONAL), 'groupid' => new external_value(PARAM_INT, 'for group assignments this is the group id', VALUE_OPTIONAL), 'groupname' => new external_value(PARAM_TEXT, 'for group assignments this is the group name', VALUE_OPTIONAL), ]; // Merge keys. $userdesc->keys = array_merge($userdesc->keys, $otherkeys); return new external_multiple_structure($userdesc); } /** * Returns description of method parameters * * @return external_function_parameters * @since Moodle 3.1 */ public static function get_participant_parameters() { return new external_function_parameters( array( 'assignid' => new external_value(PARAM_INT, 'assign instance id'), 'userid' => new external_value(PARAM_INT, 'user id'), 'embeduser' => new external_value(PARAM_BOOL, 'user id', VALUE_DEFAULT, false), ) ); } /** * Get the user participating in the given assignment. An error with code 'usernotincourse' * is thrown is the user isn't a participant of the given assignment. * * @param int $assignid the assign instance id * @param int $userid the user id * @param bool $embeduser return user details (only applicable if not blind marking) * @return array of warnings and status result * @since Moodle 3.1 * @throws moodle_exception */ public static function get_participant($assignid, $userid, $embeduser) { global $DB, $CFG; require_once($CFG->dirroot . "/mod/assign/locallib.php"); require_once($CFG->dirroot . "/user/lib.php"); require_once($CFG->libdir . '/grouplib.php'); $params = self::validate_parameters(self::get_participant_parameters(), array( 'assignid' => $assignid, 'userid' => $userid, 'embeduser' => $embeduser )); list($assign, $course, $cm, $context) = self::validate_assign($params['assignid']); $assign->require_view_grades(); $participant = $assign->get_participant($params['userid']); // Update assign with override information. $assign->update_effective_access($params['userid']); if (!$participant) { // No participant found so we can return early. throw new moodle_exception('usernotincourse'); } $filtered = $assign->is_userid_filtered($userid); if (!$filtered) { // User is filtered out by user filters or table preferences. throw new moodle_exception('userisfilteredout'); } $return = array( 'id' => $participant->id, 'fullname' => $participant->fullname, 'submitted' => $participant->submitted, 'requiregrading' => $participant->requiregrading, 'grantedextension' => $participant->grantedextension, 'submissionstatus' => $participant->submissionstatus, 'blindmarking' => $assign->is_blind_marking(), 'allowsubmissionsfromdate' => $assign->get_instance($userid)->allowsubmissionsfromdate, 'duedate' => $assign->get_instance($userid)->duedate, 'cutoffdate' => $assign->get_instance($userid)->cutoffdate, 'duedatestr' => userdate($assign->get_instance($userid)->duedate, get_string('strftimedatetime', 'langconfig')), ); if (!empty($participant->groupid)) { $return['groupid'] = $participant->groupid; if ($group = groups_get_group($participant->groupid)) { // Format properly the group name. $return['groupname'] = \core_external\util::format_string($group->name, $context); } } // Skip the expensive lookup of user detail if we're blind marking or the caller // hasn't asked for user details to be embedded. if (!$assign->is_blind_marking() && $embeduser) { if ($userdetails = user_get_user_details($participant, $course)) { $return['user'] = $userdetails; } } return $return; } /** * Returns description of method result value * * @return \core_external\external_description * @since Moodle 3.1 */ public static function get_participant_returns() { $userdescription = core_user_external::user_description(); $userdescription->default = []; $userdescription->required = VALUE_OPTIONAL; return new external_single_structure(array( 'id' => new external_value(PARAM_INT, 'ID of the user'), 'fullname' => new external_value(PARAM_NOTAGS, 'The fullname of the user'), 'submitted' => new external_value(PARAM_BOOL, 'have they submitted their assignment'), 'requiregrading' => new external_value(PARAM_BOOL, 'is their submission waiting for grading'), 'grantedextension' => new external_value(PARAM_BOOL, 'have they been granted an extension'), 'blindmarking' => new external_value(PARAM_BOOL, 'is blind marking enabled for this assignment'), 'allowsubmissionsfromdate' => new external_value(PARAM_INT, 'allowsubmissionsfromdate for the user'), 'duedate' => new external_value(PARAM_INT, 'duedate for the user'), 'cutoffdate' => new external_value(PARAM_INT, 'cutoffdate for the user'), 'duedatestr' => new external_value(PARAM_TEXT, 'duedate for the user'), 'groupid' => new external_value(PARAM_INT, 'for group assignments this is the group id', VALUE_OPTIONAL), 'groupname' => new external_value(PARAM_TEXT, 'for group assignments this is the group name', VALUE_OPTIONAL), 'submissionstatus' => new external_value(PARAM_ALPHA, 'The submission status (new, draft, reopened or submitted). Empty when not submitted.', VALUE_OPTIONAL), 'user' => $userdescription, )); } /** * Describes the parameters for view_assign. * * @return external_function_parameters * @since Moodle 3.2 */ public static function view_assign_parameters() { return new external_function_parameters ( array( 'assignid' => new external_value(PARAM_INT, 'assign instance id'), ) ); } /** * Update the module completion status. * * @param int $assignid assign instance id * @return array of warnings and status result * @since Moodle 3.2 */ public static function view_assign($assignid) { $warnings = array(); $params = array( 'assignid' => $assignid, ); $params = self::validate_parameters(self::view_assign_parameters(), $params); list($assign, $course, $cm, $context) = self::validate_assign($params['assignid']); $assign->set_module_viewed(); $result = array(); $result['status'] = true; $result['warnings'] = $warnings; return $result; } /** * Describes the view_assign return value. * * @return external_single_structure * @since Moodle 3.2 */ public static function view_assign_returns() { return new external_single_structure( array( 'status' => new external_value(PARAM_BOOL, 'status: true if success'), 'warnings' => new external_warnings(), ) ); } } overridedelete.php000064400000010713152265517350010274 0ustar00. /** * This page handles deleting assigment overrides * * @package mod_assign * @copyright 2016 Ilya Tregubov * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ require_once(dirname(__FILE__) . '/../../config.php'); require_once($CFG->dirroot.'/mod/assign/lib.php'); require_once($CFG->dirroot.'/mod/assign/locallib.php'); require_once($CFG->dirroot.'/mod/assign/override_form.php'); $overrideid = required_param('id', PARAM_INT); $confirm = optional_param('confirm', false, PARAM_BOOL); $recalculate = optional_param('recalculate', false, PARAM_BOOL); if (! $override = $DB->get_record('assign_overrides', array('id' => $overrideid))) { throw new \moodle_exception('invalidoverrideid', 'assign'); } list($course, $cm) = get_course_and_cm_from_instance($override->assignid, 'assign'); $context = context_module::instance($cm->id); $assign = new assign($context, null, null); require_login($course, false, $cm); // Check the user has the required capabilities to modify an override. require_capability('mod/assign:manageoverrides', $context); if ($override->groupid) { if (!groups_group_visible($override->groupid, $course, $cm)) { throw new \moodle_exception('invalidoverrideid', 'assign'); } } else { if (!groups_user_groups_visible($course, $override->userid, $cm)) { throw new \moodle_exception('invalidoverrideid', 'assign'); } } $url = new moodle_url('/mod/assign/overridedelete.php', array('id' => $override->id)); $confirmurl = new moodle_url($url, array('id' => $override->id, 'confirm' => 1)); $cancelurl = new moodle_url('/mod/assign/overrides.php', array('cmid' => $cm->id)); if (!empty($override->userid)) { $cancelurl->param('mode', 'user'); } // If confirm is set (PARAM_BOOL) then we have confirmation of intention to delete. if ($confirm) { require_sesskey(); $assign->delete_override($override->id); reorder_group_overrides($assign->get_instance()->id); // Recalculate grades after the override is deleted. if ($recalculate) { $assignintance = clone $assign->get_instance(); $assignintance->cmidnumber = $assign->get_course_module()->idnumber; if (!$override->groupid) { assign_update_grades($assignintance, $override->userid); } else { // If it is group mode. $groupmembers = groups_get_members($override->groupid); foreach ($groupmembers as $groupmember) { assign_update_grades($assignintance, $groupmember->id); } } } redirect($cancelurl); } // Prepare the page to show the confirmation form. $stroverride = get_string('override', 'assign'); $title = get_string('deletecheck', null, $stroverride); $PAGE->set_url($url); $PAGE->set_pagelayout('admin'); $PAGE->add_body_class('limitedwidth'); $PAGE->navbar->add($title); $PAGE->set_title($title); $PAGE->set_heading($course->fullname); $PAGE->activityheader->set_attrs([ "title" => format_string($assign->get_instance()->name, true, ['context' => $context]), "description" => "", "hidecompletion" => true ]); $PAGE->set_secondary_active_tab('mod_assign_useroverrides'); echo $OUTPUT->header(); if ($override->groupid) { $group = $DB->get_record('groups', array('id' => $override->groupid), 'id, name'); $confirmstr = get_string("overridedeletegroupsure", "assign", format_string($group->name, true, ['context' => $context])); } else { $userfieldsapi = \core_user\fields::for_name(); $namefields = $userfieldsapi->get_sql('', false, '', '', false)->selects; $user = $DB->get_record('user', array('id' => $override->userid), 'id, ' . $namefields); $confirmstr = get_string("overridedeleteusersure", "assign", fullname($user)); } echo $OUTPUT->confirm($confirmstr, $confirmurl, $cancelurl); echo $OUTPUT->footer(); styles.css000064400000103052152265517350006615 0ustar00/* stylelint-disable max-line-length */ /* FIXME: MDL-55370: remove this stylelint disable */ .path-mod-assign div.gradingnavigation div { float: left; margin-left: 2em; } .path-mod-assign div.submissionstatustable, .path-mod-assign div.submissionfull, .path-mod-assign div.submissionlinks, .path-mod-assign div.usersummary, .path-mod-assign div.feedback, .path-mod-assign div.gradingsummary { margin-bottom: 1.5em; } .path-mod-assign div.submissionstatus .generaltable, .path-mod-assign div.submissionlinks .generaltable, .path-mod-assign div.feedback .generaltable, .path-mod-assign div.submissionsummarytable .generaltable, .path-mod-assign div.attempthistory table, .path-mod-assign div.gradingsummary .generaltable { width: 100%; } .path-mod-assign table.generaltable table td { border: 0 none; } .path-mod-assign .gradingsummarytable, .path-mod-assign .feedbacktable, .path-mod-assign .lockedsubmission, .path-mod-assign .submissionsummarytable { margin-top: 1em; } .path-mod-assign div.submissionsummarytable table tbody tr td.c0 { width: 30%; } .path-mod-assign .submittedlate { color: red; font-weight: 900; } .path-mod-assign.jsenabled .gradingtable .c1 select { display: none; } .path-mod-assign .quickgradingform .mform fieldset { margin: 0; padding: 0; } .path-mod-assign textarea.quickgrade { resize: both; } .path-mod-assign td.submissionstatus, .path-mod-assign div.submissionstatus, .path-mod-assign a:link.submissionstatus { color: black; background-color: #efefef; } .path-mod-assign td.submissionstatusdraft, .path-mod-assign div.submissionstatusdraft, .path-mod-assign a:link.submissionstatusdraft { color: black; background-color: #efefcf; } .path-mod-assign td.submissionstatussubmitted, .path-mod-assign div.submissionstatussubmitted, .path-mod-assign a:link.submissionstatussubmitted { color: black; background-color: #cfefcf; } .path-mod-assign td.submissionlocked, .path-mod-assign div.submissionlocked { color: black; background-color: #efefcf; } .path-mod-assign td.submissionreopened, .path-mod-assign div.submissionreopened { color: black; background-color: #efefef; } .path-mod-assign td.submissiongraded, .path-mod-assign div.submissiongraded { color: black; background-color: #cfefcf; } .path-mod-assign td.submissionnotgraded, .path-mod-assign div.submissionnotgraded { color: black; } .path-mod-assign td.latesubmission, .path-mod-assign a:link.latesubmission, .path-mod-assign div.latesubmission { color: black; background-color: #efcfcf; } .path-mod-assign td.earlysubmission, .path-mod-assign div.earlysubmission { color: black; background-color: #cfefcf; } .path-mod-assign td.gradingreminder, .path-mod-assign div.gradingreminder { color: black; background-color: #efcfcf; } .path-mod-assign div.dropdown.extrafilters .dropdown-menu { width: 300px; } .path-mod-assign .gradingtable .c0 { display: none; } .path-mod-assign.jsenabled .gradingtable .c0 { display: table-cell; } .path-mod-assign .gradingtable table { border-collapse: separate; border-spacing: 0; } .path-mod-assign .gradingtable thead th { background-color: white; } .path-mod-assign .gradingtable tr.selectedrow td { background-color: #fec; } .path-mod-assign .gradingtable tr.unselectedrow td { background-color: white; } .path-mod-assign .gradingtable .yui3-menu ul { margin: 0; } .path-mod-assign .gradingtable .yui3-menu-label { padding-left: 0; line-height: 12px; } .path-mod-assign .gradingtable .yui3-menu-label img { padding: 0 3px; } .path-mod-assign .gradingtable .yui3-menu li { list-style-type: none; } .path-mod-assign.jsenabled .gradingtable .yui3-loading { display: none; } .path-mod-assign .gradingtable .yui3-menu .yui3-menu-content { border: 0; padding-top: 0; } .path-mod-assign div.gradingtable tr .quickgrademodified { background-color: #fc9; } .path-mod-assign .gradingtable .commentscontainer .comment-ctrl .comment-area textarea { resize: both; } .path-mod-assign td.submissioneditable { color: red; } .path-mod-assign .expandsummaryicon { display: none; } .path-mod-assign.jsenabled .expandsummaryicon { display: inline-block; } .path-mod-assign .hidefull { display: none; } .path-mod-assign .quickgradingform form .commentscontainer input, .path-mod-assign .quickgradingform form .commentscontainer textarea { display: none; } .path-mod-assign.jsenabled .quickgradingform form .commentscontainer input, .path-mod-assign.jsenabled .quickgradingform form .commentscontainer textarea { display: inline; } .path-mod-assign .previousfeedbackwarning { font-size: 140%; font-weight: bold; text-align: center; color: #500; } .path-mod-assign .submissionhistory { background-color: #b0b0b0; } .path-mod-assign .submissionhistory .cell.historytitle { background-color: #808080; } .path-mod-assign .submissionhistory .cell { background-color: #d0d0d0; } .path-mod-assign.jsenabled .mod-assign-history-link { background: none 2px 4px / 16px 16px no-repeat; display: block; cursor: pointer; margin-bottom: 7px; padding-left: 20px; } .path-mod-assign.jsenabled .mod-assign-history-link h4 { display: inline; } .path-mod-assign.jsenabled .attempthistory h4 { margin-bottom: 7px; text-align: left; } .path-mod-assign.jsenabled.dir_rtl .attempthistory h4 { text-align: right; } .path-mod-assign.jsenabled .mod-assign-history-link-open { background-image: url([[pix:t/expanded]]); } .path-mod-assign.jsenabled .mod-assign-history-link-closed { background-image: url([[pix:t/collapsed]]); } /*rtl:raw: .path-mod-assign.jsenabled .mod-assign-history-link-closed { background-image: url([[pix:t/collapsed_rtl]]); } */ .path-mod-assign .submithelp { padding: 1em; } .path-mod-assign .feedbacktitle { font-weight: bold; } .path-mod-assign .submitconfirm, .path-mod-assign .submissionlinks, .path-mod-assign .submissionaction { text-align: center; } .path-mod-assign .submissionsummarytable .c0, .path-mod-assign .gradingsummarytable .c0, .path-mod-assign .mod-assign-history-panel .c0 { width: 15em; } .path-mod-assign .gradingtable .moodle-actionmenu { white-space: nowrap; } .path-mod-assign .gradingtable .username .d-inline-block { white-space: nowrap; } .path-mod-assign .gradingtable .moodle-actionmenu[data-enhanced].show .menu a { padding-left: 12px; padding-right: 12px; } .path-mod-assign .gradingtable .menu-action img { display: none; } .path-mod-assign .editsubmissionform input[name="submissionstatement"] { vertical-align: top; } .path-mod-assign .editsubmissionform label[for="id_submissionstatement"] { display: inline-block; } .path-mod-assign.layout-option-nonavbar { padding-top: 0; } .path-mod-assign [data-region="user-selector"] select { margin-bottom: 0; } .path-mod-assign [data-region="user-selector"] .alignment { float: right; width: 320px; text-align: center; margin-top: 7px; } .path-mod-assign [data-region="user-selector"] [data-action="previous-user"], .path-mod-assign [data-region="user-selector"] [data-action="next-user"] { font-size: 26px; } .path-mod-assign [data-region="user-selector"] [data-action="next-user"] { margin-left: -10px; } .path-mod-assign [data-region="user-selector"] .alignment input { margin-bottom: 5px; } .path-mod-assign [data-region="user-selector"] .alignment .form-autocomplete-downarrow { top: 0; } .path-mod-assign [data-region="user-selector"] .form-autocomplete-selection { display: none; } .path-mod-assign [data-region="user-selector"] .form-autocomplete-suggestions { text-align: left; } .path-mod-assign [data-region="user-selector"] .form-autocomplete-suggestions { margin-left: 48px; } .path-mod-assign [data-region="user-filters"] { font-size: small; } .path-mod-assign [data-region="configure-filters"] { display: none; text-align: left; width: 480px; background-color: #fff; background-clip: padding-box; box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); border-radius: 6px; position: absolute; margin-top: 28px; margin-left: -452px; padding: 10px 0; z-index: 1; } .path-mod-assign [data-region="configure-filters"]::before, .path-mod-assign [data-region="configure-filters"]::after { position: absolute; left: auto; display: inline-block; content: ''; border-style: solid; border-color: transparent; border-top: none; } .path-mod-assign [data-region="configure-filters"]::before { top: -7px; right: 12px; border-width: 7px; border-bottom-color: rgba(0, 0, 0, 0.2); } .path-mod-assign [data-region="configure-filters"]::after { top: -6px; right: 13px; border-width: 6px; border-bottom-color: #fff; } .path-mod-assign .alignment [data-region="configure-filters"] input { margin-bottom: 0; } .path-mod-assign [data-region="grading-navigation-panel"] { position: absolute; top: 0; left: 0; width: 100%; height: 6em; margin: 0; border-bottom: 1px solid #ddd; } .path-mod-assign [data-region="grading-navigation"] { padding: 1em; } .path-mod-assign [data-region="assignment-info"] { white-space: nowrap; overflow-x: hidden; text-overflow: ellipsis; } .path-mod-assign [data-region="assignment-info"] small[data-region="assignment-tooltip"] { margin-left: 0.5em; } .path-mod-assign [data-region="user-info"] { height: 60px; } .path-mod-assign [data-region="user-info"] a { text-decoration: none; } .path-mod-assign [data-region="user-info"] .img-rounded { display: block; float: left; margin-top: -3px; margin-right: 10px; } .path-mod-assign [data-region="user-info"] em { display: block; font-style: normal; } .path-mod-assign [data-region="user-info"] em.nowrap { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .path-mod-assign [data-region="grading-actions-form"] label { display: inline-block; } .path-mod-assign.pagelayout-embedded { overflow: hidden; } .path-mod-assign [data-region="review-panel"] { position: absolute; top: 85px; bottom: 60px; left: 0; right: 30%; width: auto; box-sizing: border-box; -webkit-transition: right 0.5s, left 0.5s; -moz-transition: right 0.5s, left 0.5s; transition: right 0.5s, left 0.5s; } .path-mod-assign [data-region="review-panel"].grade-panel-collapsed { right: 30px; } .path-mod-assign [data-region="review-panel"] [data-region="review-panel-toggle"] { display: none; height: 30px; width: 30px; position: absolute; top: 0; right: 0; left: auto; box-sizing: border-box; border-radius: 0 0 0 4px; border: 1px solid #ccc; border-top: none; background-color: #fff; } .path-mod-assign [data-region="review-panel"] [data-region="review-panel-toggle"] .expand-icon, .path-mod-assign [data-region="review-panel"] [data-region="review-panel-toggle"] .collapse-icon { width: 100%; height: 100%; text-align: center; line-height: 30px; } .path-mod-assign [data-region="review-panel"] [data-region="review-panel-toggle"] .expand-icon .toggle-text, .path-mod-assign [data-region="review-panel"] [data-region="review-panel-toggle"] .collapse-icon .toggle-text { visibility: hidden; opacity: 0; height: 0; overflow: hidden; } .path-mod-assign [data-region="review-panel"] [data-region="review-panel-toggle"] img { height: 100%; float: right; } .path-mod-assign [data-region="review-panel"] [data-region="review-panel-toggle"] .expand-icon { display: none; } .path-mod-assign [data-region="review-panel"] [data-region="review-panel-toggle"] .collapse-icon { display: block; } .path-mod-assign [data-region="review-panel"] [data-region="review-panel-toggle"] .collapse-icon.full-width { display: none; } .path-mod-assign [data-region="review-panel"] .pageheader { border-right: 1px solid #ddd; } .path-mod-assign [data-region="review-panel"] + [data-region="grade-panel"] [data-region="grade"] { margin-left: auto; margin-right: auto; max-width: 100%; -webkit-transition: max-width 0.5s; -moz-transition: max-width 0.5s; transition: max-width 0.5s; } .path-mod-assign [data-region="review-panel"] .drawingregion { left: 0; right: 0; border-color: #ddd; } .path-mod-assign [data-region="review-panel"].collapsed { left: calc(30px - 70%); right: calc(100% - 30px); -webkit-transition: right 0.5s, left 0.5s; -moz-transition: right 0.5s, left 0.5s; transition: right 0.5s, left 0.5s; } .path-mod-assign [data-region="review-panel"].collapsed [data-region="review-panel-content"] { visibility: hidden; -webkit-transition: visibility 0s 0.5s; -moz-transition: visibility 0s 0.5s; transition: visibility 0s 0.5s; } .path-mod-assign [data-region="review-panel"].collapsed [data-region="review-panel-toggle"] .expand-icon { display: block; } .path-mod-assign [data-region="review-panel"].collapsed [data-region="review-panel-toggle"] .collapse-icon { display: none; } .path-mod-assign [data-region="review-panel"].collapsed + [data-region="grade-panel"] { position: absolute; left: 30px; right: 0; width: calc(100% - 30px); overflow: auto; -webkit-transition: width 0.5s, right 0.5s, left 0.5s; -moz-transition: width 0.5s, right 0.5s, left 0.5s; transition: width 0.5s, right 0.5s, left 0.5s; } .path-mod-assign [data-region="review-panel"].collapsed + [data-region="grade-panel"] [data-region="grade"], .path-mod-assign [data-region="grade-panel"].fullwidth [data-region="grade"] { max-width: 800px; margin-left: auto; margin-right: auto; } .path-mod-assign [data-region="grade-panel"] { position: absolute; top: 85px; bottom: 60px; right: 0; left: 70%; width: 30%; overflow: auto; box-sizing: border-box; background-color: #f5f5f5; padding: 15px; padding-top: 0; -webkit-transition: width 0.5s, right 0.5s, left 0.5s; -moz-transition: width 0.5s, right 0.5s, left 0.5s; transition: width 0.5s, right 0.5s, left 0.5s; } .path-mod-assign [data-region="grade-panel"].collapsed { left: calc(100% - 30px); right: calc(30px - 100%); visibility: hidden; -webkit-transition: right 0.5s, left 0.5s, visibility 0s 0.5s; -moz-transition: right 0.5s, left 0.5s, visibility 0s 0.5s; transition: right 0.5s, left 0.5s, visibility 0s 0.5s; } .path-mod-assign [data-region="grade-panel"].fullwidth { left: 0; width: 100%; overflow: auto; } .path-mod-assign [data-region="grade-panel"] h3 { font-size: 18px; font-weight: 500; } /***** Start submission status *****/ .path-mod-assign [data-region="grade-panel"] div.submissionstatustable { margin-bottom: 2em; } .path-mod-assign [data-region="grade-panel"] .submissionsummarytable { margin-left: 5px; margin-right: 5px; } .path-mod-assign [data-region="grade-panel"] .submissionsummarytable table.generaltable td { padding: 8px 0; background-color: transparent; } .path-mod-assign [data-region="grade-panel"] .submissionsummarytable .generaltable tbody > tr:nth-child(2n+1) > td, .path-mod-assign [data-region="grade-panel"] .submissionsummarytable .generaltable tbody tr:hover > td { background-color: transparent; } .path-mod-assign [data-region="grade-panel"] div.submissionsummarytable table tbody tr td.c0 { width: auto; } .path-mod-assign [data-region="grade-panel"] div.submissionsummarytable table tbody tr.lastrow td.c0, .path-mod-assign [data-region="grade-panel"] div.submissionsummarytable table tbody tr.lastrow td.c1 { border-bottom: 1px solid #ddd; } .path-mod-assign .overdue, .path-mod-assign .overduesubmission, .path-mod-assign [data-region="grade-panel"] td.submissionnotgraded, .path-mod-assign [data-region="grade-panel"] div.submissionnotgraded { color: #ca3120; background-color: transparent; } .path-mod-assign div.fileuploadsubmission { float: left; min-width: 300px; } .path-mod-assign div.fileuploadsubmissiontime { float: right; } /***** End submission status *****/ .path-mod-assign [data-region="grade-panel"] #id_gradeheader { display: table-cell; min-width: 0; } .path-mod-assign [data-region="grade-panel"] #id_gradeheader > legend { visibility: hidden; height: 0; margin-bottom: 0; } .path-mod-assign [data-region="grade-panel"] .comment-area textarea[cols] { width: 100%; box-sizing: border-box; } .path-mod-assign [data-region="grade-panel"] .mform .fitem.fitem_ftext, .path-mod-assign [data-region="grade-panel"] .mform .fitem.fitem_f, .path-mod-assign [data-region="grade-panel"] .mform .fitem.fitem_feditor, .path-mod-assign [data-region="grade-panel"] .mform .fitem.fitem_fgrading, .path-mod-assign [data-region="grade-panel"] .mform .fitem.fitem_ffilemanager { background-color: #fff; border: 1px solid #ddd; margin-bottom: 20px; } .path-mod-assign [data-region="grade-panel"] .mform .fitem.fitem_ftext .fitemtitle, .path-mod-assign [data-region="grade-panel"] .mform .fitem.fitem_f .fitemtitle, .path-mod-assign [data-region="grade-panel"] .mform .fitem.fitem_feditor .fitemtitle, .path-mod-assign [data-region="grade-panel"] .mform .fitem.fitem_fgrading .fitemtitle, .path-mod-assign [data-region="grade-panel"] .mform .fitem.fitem_ffilemanager .fitemtitle { padding-left: 5px; padding-right: 5px; } .path-mod-assign #page-content [data-region="grade-panel"] .mform:not(.unresponsive) .fcontainer .fitem.fitem_ftext .felement, .path-mod-assign #page-content [data-region="grade-panel"] .mform:not(.unresponsive) .fcontainer .fitem.fitem_f .felement, .path-mod-assign #page-content [data-region="grade-panel"] .mform:not(.unresponsive) .fcontainer .fitem.fitem_feditor .felement, .path-mod-assign #page-content [data-region="grade-panel"] .mform:not(.unresponsive) .fcontainer .fitem.fitem_fgrading .felement, .path-mod-assign #page-content [data-region="grade-panel"] .mform:not(.unresponsive) .fcontainer .fitem.fitem_ffilemanager .felement { padding: 6px 10px 10px; box-sizing: border-box; } .path-mod-assign #page-content [data-region="grade-panel"] .mform:not(.unresponsive) .fitem.fitem_ftext .fitemtitle, .path-mod-assign #page-content [data-region="grade-panel"] .mform:not(.unresponsive) .fitem.fitem_f .fitemtitle, .path-mod-assign #page-content [data-region="grade-panel"] .mform:not(.unresponsive) .fitem.fitem_feditor .fitemtitle, .path-mod-assign #page-content [data-region="grade-panel"] .mform:not(.unresponsive) .fitem.fitem_fgrading .fitemtitle, .path-mod-assign #page-content [data-region="grade-panel"] .mform:not(.unresponsive) .fitem.fitem_ffilemanager .fitemtitle { border-bottom: 1px solid #ddd; box-shadow: 0 1px 1px rgba(0, 0, 0, 0.05); padding: 6px 10px 3px; box-sizing: border-box; } .path-mod-assign #page-content [data-region="grade-panel"] [data-region="popout-button"] img { margin-left: 2px; margin-right: 2px; margin-top: -2px; } .path-mod-assign #page-content [data-region="grade-panel"] .popout [data-region="popout-button"] img { margin-left: -6px; margin-right: -6px; margin-top: 4px; } .path-mod-assign [data-region="grade-panel"] .fitem .fstaticlabel, .path-mod-assign #page-content [data-region="grade-panel"] .mform:not(.unresponsive) .fitem .fitemtitle label { font-weight: 500; } /***** Start grade *****/ .path-mod-assign [data-region="grade-panel"] .mform #fitem_id_grade.fitem { padding-top: 5px; } .path-mod-assign #page-content [data-region="grade-panel"] .mform:not(.unresponsive) #fitem_id_grade.fitem .fitemtitle { display: inline-block; width: auto; border-bottom: none; box-shadow: none; } .path-mod-assign #page-content [data-region="grade-panel"] .mform:not(.unresponsive) #fitem_id_grade.fitem .felement { width: auto; float: right; } .path-mod-assign #page-content .mform:not(.unresponsive) #fitem_id_grade.fitem .felement input { width: 80px; margin-bottom: 0; } /***** End grade *****/ /***** Start rubric *****/ .path-mod-assign [data-region="grade-panel"] .gradingform_rubric { max-width: 100%; padding-bottom: 0; } .path-mod-assign .gradingform_rubric { max-width: calc(100vw - 200px); } .path-mod-assign [data-region="grade-panel"] .gradingform_rubric .criterion .description { font-weight: 500; min-width: 150px; } .path-mod-assign [data-region="grade-panel"] .gradingform_rubric .criterion .levels { background-color: #fff; } .path-mod-assign [data-region="grade-panel"] .gradingform_rubric .criterion, .path-mod-assign [data-region="grade-panel"] .gradingform_rubric .criterion.even { background-color: transparent; } .path-mod-assign [data-region="grade-panel"] .gradingform_rubric .criterion .remark textarea { margin-bottom: 0; } /***** End rubric *****/ /***** Start marking guide *****/ .path-mod-assign [data-region="grade-panel"] .gradingform_guide { margin-bottom: 10px; } .path-mod-assign [data-region="grade-panel"] .gradingform_guide .descriptionreadonly, .path-mod-assign [data-region="grade-panel"] .gradingform_guide .remark, .path-mod-assign [data-region="grade-panel"] .gradingform_guide .score { display: block; } .path-mod-assign [data-region="grade-panel"] .gradingform_guide .descriptionreadonly { padding-top: 10px; } .path-mod-assign [data-region="grade-panel"] .gradingform_guide .criteriondescription { margin-top: 5px; } .path-mod-assign [data-region="grade-panel"] .gradingform_guide .criteriondescriptionmarkers { width: auto; margin-top: 5px; } .path-mod-assign [data-region="grade-panel"] .gradingform_guide .markingguideremark { margin-bottom: 10px; } .path-mod-assign [data-region="grade-panel"] .gradingform_guide .remark .commentchooser { float: right; margin-top: 2px; margin-left: 0; } .path-mod-assign [data-region="grade-panel"] .gradingform_guide .score { float: left; padding-bottom: 8px; } .path-mod-assign [data-region="grade-panel"] .gradingform_guide .score input, .path-mod-assign [data-region="grade-panel"] .gradingform_guide .score div { display: inline-block; } .path-mod-assign [data-region="grade-panel"] .gradingform_guide .criterion, .path-mod-assign [data-region="grade-panel"] .gradingform_guide .criterion.even { background-color: transparent; border-width: 0 0 1px 0; padding: 8px 0; } .path-mod-assign [data-region="grade-panel"] .showmarkerdesc, .path-mod-assign [data-region="grade-panel"] .showstudentdesc { background-color: #f5f5f5; padding: 10px; } /***** End marking guide *****/ .path-mod-assign [data-region="grade-panel"] .fitem.fitem_ffilemanager { margin-bottom: 0; } /***** Start popout dialogue *****/ .path-mod-assign [data-region="grade-panel"] .fitem.popout { position: fixed; left: 20%; right: 20%; top: 20%; bottom: 20%; z-index: 1000; border: 1px solid rgba(0, 0, 0, 0.3); border-radius: 6px; box-shadow: 0 3px 7px rgba(0, 0, 0, 0.3); } .path-mod-assign #page-content [data-region="grade-panel"] .mform:not(.unresponsive) .fcontainer .fitem.popout .fitemtitle { text-align: center; padding-left: 15px; padding-right: 15px; height: 45px; } .path-mod-assign #page-content [data-region="grade-panel"] .mform:not(.unresponsive) .fcontainer .fitem.popout .fitemtitle label { font-size: 16px; line-height: 30px; } .path-mod-assign #page-content [data-region="grade-panel"] [data-region="popout-button"] { float: right; } .path-mod-assign #page-content [data-region="grade-panel"] .mform:not(.unresponsive) .fitem.popout .fitemtitle [data-region="popout-button"] img { margin-top: -10px; margin-right: -7px; } .path-mod-assign #page-content [data-region="grade-panel"] .mform:not(.unresponsive) .fcontainer .fitem.popout .felement { padding: 10px 15px 15px; height: calc(100% - 54px); overflow: auto; } .path-mod-assign #page-content [data-region="grade-panel"] .mform:not(.unresponsive) .fcontainer .fitem.popout .felement .gradingform_rubric { overflow: visible; } /***** End popout dialogue *****/ /***** Start attempt settings *****/ .path-mod-assign [data-region="grade-panel"] #id_attemptsettings > legend { font-size: 18px; font-weight: 500; line-height: 40px; border-bottom: 0; margin-bottom: 10px; } .path-mod-assign [data-region="grade-panel"] #id_attemptsettings .fcontainer { display: table; width: 100%; padding-left: 5px; padding-right: 5px; margin-bottom: 10px; box-sizing: border-box; } .path-mod-assign [data-region="grade-panel"] .mform #id_attemptsettings .fitem { display: table-row; } .path-mod-assign #page-content [data-region="grade-panel"] .mform:not(.unresponsive) #id_attemptsettings .fitem:last-of-type .fitemtitle, .path-mod-assign #page-content [data-region="grade-panel"] .mform:not(.unresponsive) #id_attemptsettings .fitem:last-of-type .felement { border-bottom: 1px solid #ddd; } .path-mod-assign [data-region="grade-panel"] #id_attemptsettings .fitem .fstaticlabel, .path-mod-assign [data-region="grade-panel"] .mform:not(.unresponsive) #id_attemptsettings .fitem .fitemtitle label { font-weight: 400; } .path-mod-assign [data-region="grade-panel"] .mform:not(.unresponsive) #id_attemptsettings .fitem .felement select { margin-bottom: 0; } .path-mod-assign [data-region="grade-panel"] [data-region="attempt-chooser"] { margin-bottom: 10px; vertical-align: text-bottom; } /***** End attempt settings *****/ .path-mod-assign [data-region="grade-actions-panel"] { border-top: 1px solid #ddd; position: absolute; bottom: 0; left: 0; width: 100%; height: 60px; } .path-mod-assign [data-region="grade-actions"] { padding: 1em; text-align: center; } .path-mod-assign [data-region="submissions-list"] { text-align: inherit; } .path-mod-assign [data-region="submissions-list"] label.radio input { margin-top: 4px; min-width: inherit; } .path-mod-assign [data-region="overlay"] { display: none; z-index: 100; position: absolute; top: 0; left: 0; width: 100%; overflow: auto; bottom: 0; background-color: #ddd; opacity: 0.4; padding-top: 4em; text-align: center; } @media (max-width: 767px) { .path-mod-assign.pagelayout-embedded { overflow: auto; } .path-mod-assign [data-region="assignment-info"] { border-bottom: 1px solid #ddd; padding-bottom: 5px; } .path-mod-assign .page-context-header .page-header-headings { margin-top: 13px; } .path-mod-assign [data-region="grade-actions-panel"] [data-region="grade-actions"] .collapse-buttons { display: none; } .path-mod-assign [data-region="grading-navigation-panel"], .path-mod-assign [data-region="review-panel"], .path-mod-assign [data-region="grade-panel"], .path-mod-assign [data-region="review-panel"].collapsed + [data-region="grade-panel"], .path-mod-assign [data-region="grade-actions-panel"] { position: inherit; width: 100%; top: 0; left: 0; right: auto; overflow: auto; height: auto; margin-bottom: 1em; } .path-mod-assign [data-region="grade-panel"].collapsed { visibility: visible; } .path-mod-assign [data-region="grading-navigation"] { padding: 0; text-align: center; } .path-mod-assign [data-region="grade-panel"] { margin-bottom: 2em; } .path-mod-assign [data-region="grade-panel"] [data-region="popout-button"] { display: none; } .path-mod-assign [data-region="review-panel"] { position: relative; max-height: 2000px; -webkit-transition: max-height 0.25s linear; -moz-transition: max-height 0.25s linear; transition: max-height 0.25s linear; } .path-mod-assign [data-region="review-panel"] .pageheader { border-right: none; padding-right: 20px; padding-left: 40px; } .path-mod-assign [data-region="review-panel"] [data-region="review-panel-toggle"] { display: block; height: 20px; width: 20px; position: absolute; top: 0; left: 0; right: auto; box-sizing: border-box; border: 1px solid #ccc; border-radius: 0 0 4px 0; background-color: #fff; } .path-mod-assign [data-region="review-panel"] [data-region="review-panel-toggle"] .expand-icon, .path-mod-assign [data-region="review-panel"] [data-region="review-panel-toggle"] .collapse-icon { width: 100%; height: 100%; text-align: center; line-height: 30px; } .path-mod-assign [data-region="review-panel"] [data-region="review-panel-toggle"] .ltr-icon, .path-mod-assign [data-region="review-panel"] [data-region="review-panel-toggle"] .rtl-icon { height: 100%; } .path-mod-assign [data-region="review-panel"] [data-region="review-panel-toggle"] .ltr-icon { float: left; } .path-mod-assign [data-region="review-panel"] [data-region="review-panel-toggle"] .rtl-icon { float: right; } .path-mod-assign [data-region="review-panel"] [data-region="review-panel-toggle"] .expand-icon .toggle-text, .path-mod-assign [data-region="review-panel"] [data-region="review-panel-toggle"] .collapse-icon .toggle-text { line-height: 20px; visibility: visible; opacity: 1; height: auto; -webkit-transition: visibility 0s 0.25s, opacity 0s 0.25s; -moz-transition: visibility 0s 0.25s, opacity 0s 0.25s; transition: visibility 0s 0.25s, opacity 0s 0.25s; } .path-mod-assign [data-region="review-panel"] [data-region="review-panel-toggle"] .expand-icon .rtl-icon { display: none; } .path-mod-assign [data-region="review-panel"].collapsed { max-height: 20px; top: 0; left: 0; overflow: hidden; -webkit-transition: max-height 0.25s linear; -moz-transition: max-height 0.25s linear; transition: max-height 0.25s linear; } .path-mod-assign [data-region="review-panel"].collapsed [data-region="review-panel-toggle"] { width: 100%; border-radius: 0 0 0 0; -webkit-transition: all 0s 0.25s; -moz-transition: all 0s 0.25s; transition: all 0s 0.25s; } .path-mod-assign [data-region="review-panel"].collapsed [data-region="review-panel-toggle"] img { height: 100%; } .path-mod-assign [data-region="review-panel"] [data-region="review-panel-toggle"] .collapse-icon { display: block; } .path-mod-assign [data-region="review-panel"].collapsed [data-region="review-panel-toggle"] .collapse-icon { display: none; } .path-mod-assign.pagelayout-popup { overflow: inherit; } .path-mod-assign [data-region="grading-navigation"] [data-region="user-info"] { text-align: left; width: auto; display: inline-block; margin: 0 auto; height: auto; } .path-mod-assign [data-region="user-selector"] .alignment { float: none; margin: 0 auto 10px; } /* Bug fix for TinyMCE menu when in fullscreen mode (small screens). */ body.tox-fullscreen.path-mod-assign [data-region="grading-navigation"] { display: none; } } /** Start of CSS to make forms vertical in the grading panel (taken from theme/bootstrapbase/less/moodle/forms.less). */ .path-mod-assign [data-region="grade-panel"] .mform .fitem .fitemtitle { display: block; margin-top: 4px; margin-bottom: 4px; text-align: left; width: 100%; } .path-mod-assign [data-region="grade-panel"] .mform .fitem .felement { margin-left: 0; width: 100%; float: left; padding-left: 0; padding-right: 0; } .path-mod-assign [data-region="grade-panel"] .mform .fitem .fstatic:empty { display: none; } .path-mod-assign [data-region="grade-panel"] .mform .fitem .fcheckbox > span, .path-mod-assign [data-region="grade-panel"] .mform .fitem .fradio > span, .path-mod-assign [data-region="grade-panel"] .mform .fitem .fgroup > span { margin-top: 4px; } .path-mod-assign [data-region="grade-panel"] .mform .femptylabel .fitemtitle { display: inline-block; width: auto; margin-right: 8px; } .path-mod-assign [data-region="grade-panel"] .mform .femptylabel .felement { display: inline-block; margin-top: 4px; padding-top: 5px; width: auto; } .path-mod-assign [data-region="grade-panel"] .mform .fitem_fcheckbox .fitemtitle, .path-mod-assign [data-region="grade-panel"] .mform .fitem_fcheckbox .felement { display: inline-block; width: auto; } .path-mod-assign [data-region="grade-panel"] .mform .fitem_fcheckbox .felement { padding: 6px; } /** End of CSS to make forms vertical in the grading panel (taken from theme/bootstrapbase/less/moodle/forms.less). */ /** Styles to fix base theme **/ .path-mod-assign #page, .path-mod-assign #page-content { position: inherit; } /** End of base fixes **/ /** Fix to YUI tree (which is a table) when displayed within grading table. **/ .path-mod-assign table.generaltable table td.ygtvcell { border: 0; padding: 0; } /** End of YUI tree fix **/ /* Countdown timer. */ div[id*='mod_assign-timer-'] { display: block; font-weight: 600; font-size: 1.4em; } /* Bug fix for TinyMCE menu when in fullscreen mode. */ body.tox-fullscreen.path-mod-assign [data-region="grade-panel"] { position: unset; } /* Bug fix for TinyMCE menu when in fullscreen mode and when editor is nested in a popout.*/ body.path-mod-assign [data-region="grade-panel"] .mform .fitem.has-popout .felement[data-fieldtype="editor"] { overflow: unset; } .path-mod-assign .initialsdropdown { width: 460px; } .path-mod-assign .initialsdropdown::after { content: ''; } renderer.php000064400000002161152265517350007076 0ustar00. /** * Legacy file to avoid exceptions when scripts require it. * Please use classes from mod/assign/classes/output instead. * * @deprecated since Moodle 4.0 - please do not include this file anymore. * @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(); // TODO - MDL-72749 add debugging message. db/services.php000064400000027424152265517350007511 0ustar00. /** * Web service for mod assign * @package mod_assign * @subpackage db * @since Moodle 2.4 * @copyright 2012 Paul Charsley * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ $functions = array( 'mod_assign_copy_previous_attempt' => array( 'classname' => 'mod_assign_external', 'methodname' => 'copy_previous_attempt', 'classpath' => 'mod/assign/externallib.php', 'description' => 'Copy a students previous attempt to a new attempt.', 'type' => 'write', 'capabilities' => 'mod/assign:view, mod/assign:submit' ), 'mod_assign_get_grades' => array( 'classname' => 'mod_assign_external', 'methodname' => 'get_grades', 'classpath' => 'mod/assign/externallib.php', 'description' => 'Returns grades from the assignment', 'type' => 'read', 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE) ), 'mod_assign_get_assignments' => array( 'classname' => 'mod_assign_external', 'methodname' => 'get_assignments', 'classpath' => 'mod/assign/externallib.php', 'description' => 'Returns the courses and assignments for the users capability', 'type' => 'read', 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE) ), 'mod_assign_get_submissions' => array( 'classname' => 'mod_assign_external', 'methodname' => 'get_submissions', 'classpath' => 'mod/assign/externallib.php', 'description' => 'Returns the submissions for assignments', 'type' => 'read', 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE) ), 'mod_assign_get_user_flags' => array( 'classname' => 'mod_assign_external', 'methodname' => 'get_user_flags', 'classpath' => 'mod/assign/externallib.php', 'description' => 'Returns the user flags for assignments', 'type' => 'read', 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE) ), 'mod_assign_set_user_flags' => array( 'classname' => 'mod_assign_external', 'methodname' => 'set_user_flags', 'classpath' => 'mod/assign/externallib.php', 'description' => 'Creates or updates user flags', 'type' => 'write', 'capabilities'=> 'mod/assign:grade', 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE) ), 'mod_assign_get_user_mappings' => array( 'classname' => 'mod_assign_external', 'methodname' => 'get_user_mappings', 'classpath' => 'mod/assign/externallib.php', 'description' => 'Returns the blind marking mappings for assignments', 'type' => 'read', 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE) ), 'mod_assign_revert_submissions_to_draft' => array( 'classname' => 'mod_assign_external', 'methodname' => 'revert_submissions_to_draft', 'classpath' => 'mod/assign/externallib.php', 'description' => 'Reverts the list of submissions to draft status', 'type' => 'write', 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE) ), 'mod_assign_lock_submissions' => array( 'classname' => 'mod_assign_external', 'methodname' => 'lock_submissions', 'classpath' => 'mod/assign/externallib.php', 'description' => 'Prevent students from making changes to a list of submissions', 'type' => 'write', 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE) ), 'mod_assign_unlock_submissions' => array( 'classname' => 'mod_assign_external', 'methodname' => 'unlock_submissions', 'classpath' => 'mod/assign/externallib.php', 'description' => 'Allow students to make changes to a list of submissions', 'type' => 'write', 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE) ), 'mod_assign_save_submission' => array( 'classname' => 'mod_assign_external', 'methodname' => 'save_submission', 'classpath' => 'mod/assign/externallib.php', 'description' => 'Update the current students submission', 'type' => 'write', 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE) ), 'mod_assign_submit_for_grading' => array( 'classname' => 'mod_assign_external', 'methodname' => 'submit_for_grading', 'classpath' => 'mod/assign/externallib.php', 'description' => 'Submit the current students assignment for grading', 'type' => 'write', 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE) ), 'mod_assign_save_grade' => array( 'classname' => 'mod_assign_external', 'methodname' => 'save_grade', 'classpath' => 'mod/assign/externallib.php', 'description' => 'Save a grade update for a single student.', 'type' => 'write', 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE) ), 'mod_assign_save_grades' => array( 'classname' => 'mod_assign_external', 'methodname' => 'save_grades', 'classpath' => 'mod/assign/externallib.php', 'description' => 'Save multiple grade updates for an assignment.', 'type' => 'write', 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE) ), 'mod_assign_save_user_extensions' => array( 'classname' => 'mod_assign_external', 'methodname' => 'save_user_extensions', 'classpath' => 'mod/assign/externallib.php', 'description' => 'Save a list of assignment extensions', 'type' => 'write', 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE) ), 'mod_assign_reveal_identities' => array( 'classname' => 'mod_assign_external', 'methodname' => 'reveal_identities', 'classpath' => 'mod/assign/externallib.php', 'description' => 'Reveal the identities for a blind marking assignment', 'type' => 'write', 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE) ), 'mod_assign_view_grading_table' => array( 'classname' => 'mod_assign_external', 'methodname' => 'view_grading_table', 'classpath' => 'mod/assign/externallib.php', 'description' => 'Trigger the grading_table_viewed event.', 'type' => 'write', 'capabilities' => 'mod/assign:view, mod/assign:viewgrades', 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE) ), 'mod_assign_view_submission_status' => array( 'classname' => 'mod_assign_external', 'methodname' => 'view_submission_status', 'classpath' => 'mod/assign/externallib.php', 'description' => 'Trigger the submission status viewed event.', 'type' => 'write', 'capabilities' => 'mod/assign:view', 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE) ), 'mod_assign_get_submission_status' => array( 'classname' => 'mod_assign_external', 'methodname' => 'get_submission_status', 'classpath' => 'mod/assign/externallib.php', 'description' => 'Returns information about an assignment submission status for a given user.', 'type' => 'read', 'capabilities' => 'mod/assign:view', 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE) ), 'mod_assign_list_participants' => array( 'classname' => 'mod_assign_external', 'methodname' => 'list_participants', 'classpath' => 'mod/assign/externallib.php', 'description' => 'List the participants for a single assignment, with some summary info about their submissions.', 'type' => 'read', 'ajax' => true, 'capabilities' => 'mod/assign:view, mod/assign:viewgrades', 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE) ), 'mod_assign_submit_grading_form' => array( 'classname' => 'mod_assign_external', 'methodname' => 'submit_grading_form', 'classpath' => 'mod/assign/externallib.php', 'description' => 'Submit the grading form data via ajax', 'type' => 'write', 'ajax' => true, 'capabilities' => 'mod/assign:grade', 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE) ), 'mod_assign_get_participant' => array( 'classname' => 'mod_assign_external', 'methodname' => 'get_participant', 'classpath' => 'mod/assign/externallib.php', 'description' => 'Get a participant for an assignment, with some summary info about their submissions.', 'type' => 'read', 'ajax' => true, 'capabilities' => 'mod/assign:view, mod/assign:viewgrades', 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE) ), 'mod_assign_view_assign' => array( 'classname' => 'mod_assign_external', 'methodname' => 'view_assign', 'classpath' => 'mod/assign/externallib.php', 'description' => 'Update the module completion status.', 'type' => 'write', 'capabilities' => 'mod/assign:view', 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE) ), 'mod_assign_start_submission' => [ 'classname' => 'mod_assign\external\start_submission', 'methodname' => 'execute', 'description' => 'Start a submission for user if assignment has a time limit.', 'type' => 'write', 'capabilities' => 'mod/assign:view', 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE) ], 'mod_assign_remove_submission' => [ 'classname' => 'mod_assign\external\remove_submission', 'description' => 'Remove submission.', 'type' => 'write', 'capabilities' => 'mod/assign:submit', 'services' => [MOODLE_OFFICIAL_MOBILE_SERVICE], ], ); db/subplugins.json000064400000000362152265517350010233 0ustar00{ "subplugintypes": { "assignsubmission": "submission", "assignfeedback": "feedback" }, "plugintypes": { "assignsubmission": "mod/assign/submission", "assignfeedback": "mod/assign/feedback" } } db/log.php000064400000006246152265517350006446 0ustar00. /** * Definition of log events * * @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(); $logs = array( array('module'=>'assign', 'action'=>'add', 'mtable'=>'assign', 'field'=>'name'), array('module'=>'assign', 'action'=>'delete mod', 'mtable'=>'assign', 'field'=>'name'), array('module'=>'assign', 'action'=>'download all submissions', 'mtable'=>'assign', 'field'=>'name'), array('module'=>'assign', 'action'=>'grade submission', 'mtable'=>'assign', 'field'=>'name'), array('module'=>'assign', 'action'=>'lock submission', 'mtable'=>'assign', 'field'=>'name'), array('module'=>'assign', 'action'=>'reveal identities', 'mtable'=>'assign', 'field'=>'name'), array('module'=>'assign', 'action'=>'revert submission to draft', 'mtable'=>'assign', 'field'=>'name'), array('module'=>'assign', 'action'=>'set marking workflow state', 'mtable'=>'assign', 'field'=>'name'), array('module'=>'assign', 'action'=>'submission statement accepted', 'mtable'=>'assign', 'field'=>'name'), array('module'=>'assign', 'action'=>'submit', 'mtable'=>'assign', 'field'=>'name'), array('module'=>'assign', 'action'=>'submit for grading', 'mtable'=>'assign', 'field'=>'name'), array('module'=>'assign', 'action'=>'unlock submission', 'mtable'=>'assign', 'field'=>'name'), array('module'=>'assign', 'action'=>'update', 'mtable'=>'assign', 'field'=>'name'), array('module'=>'assign', 'action'=>'upload', 'mtable'=>'assign', 'field'=>'name'), array('module'=>'assign', 'action'=>'view', 'mtable'=>'assign', 'field'=>'name'), array('module'=>'assign', 'action'=>'view all', 'mtable'=>'course', 'field'=>'fullname'), array('module'=>'assign', 'action'=>'view confirm submit assignment form', 'mtable'=>'assign', 'field'=>'name'), array('module'=>'assign', 'action'=>'view grading form', 'mtable'=>'assign', 'field'=>'name'), array('module'=>'assign', 'action'=>'view submission', 'mtable'=>'assign', 'field'=>'name'), array('module'=>'assign', 'action'=>'view submission grading table', 'mtable'=>'assign', 'field'=>'name'), array('module'=>'assign', 'action'=>'view submit assignment form', 'mtable'=>'assign', 'field'=>'name'), array('module'=>'assign', 'action'=>'view feedback', 'mtable'=>'assign', 'field'=>'name'), array('module'=>'assign', 'action'=>'view batch set marking workflow state', 'mtable'=>'assign', 'field'=>'name'), ); db/tasks.php000064400000003606152265517350007007 0ustar00. /** * Definition of assignment scheduled tasks. * * @package mod_assign * @copyright 2019 Simey Lameze * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); $tasks = array( array( 'classname' => '\mod_assign\task\cron_task', 'blocking' => 0, 'minute' => '*', 'hour' => '*', 'day' => '*', 'month' => '*', 'dayofweek' => '*' ), [ 'classname' => '\mod_assign\task\queue_all_assignment_due_soon_notification_tasks', 'blocking' => 0, 'minute' => 'R', 'hour' => '*/2', 'day' => '*', 'month' => '*', 'dayofweek' => '*', ], [ 'classname' => '\mod_assign\task\queue_all_assignment_overdue_notification_tasks', 'blocking' => 0, 'minute' => 'R', 'hour' => '*/1', 'day' => '*', 'month' => '*', 'dayofweek' => '*', ], [ 'classname' => '\mod_assign\task\queue_all_assignment_due_digest_notification_tasks', 'blocking' => 0, 'minute' => 'R', 'hour' => '1', 'day' => '*', 'month' => '*', 'dayofweek' => '*', ], ); db/access.php000064400000013501152265517350007116 0ustar00. /** * Capability definitions for this 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 */ $capabilities = array( 'mod/assign:view' => array( 'captype' => 'read', 'contextlevel' => CONTEXT_MODULE, 'archetypes' => array( 'guest' => CAP_ALLOW, 'student' => CAP_ALLOW, 'teacher' => CAP_ALLOW, 'editingteacher' => CAP_ALLOW, 'manager' => CAP_ALLOW ) ), 'mod/assign:submit' => array( 'captype' => 'write', 'contextlevel' => CONTEXT_MODULE, 'archetypes' => array( 'student' => CAP_ALLOW ) ), 'mod/assign:grade' => array( 'riskbitmask' => RISK_XSS, 'captype' => 'write', 'contextlevel' => CONTEXT_MODULE, 'archetypes' => array( 'teacher' => CAP_ALLOW, 'editingteacher' => CAP_ALLOW, 'manager' => CAP_ALLOW ) ), 'mod/assign:exportownsubmission' => array( 'captype' => 'read', 'contextlevel' => CONTEXT_MODULE, 'archetypes' => array( 'teacher' => CAP_ALLOW, 'editingteacher' => CAP_ALLOW, 'manager' => CAP_ALLOW, 'student' => CAP_ALLOW, ) ), 'mod/assign:addinstance' => array( 'riskbitmask' => RISK_XSS, 'captype' => 'write', 'contextlevel' => CONTEXT_COURSE, 'archetypes' => array( 'editingteacher' => CAP_ALLOW, 'manager' => CAP_ALLOW ), 'clonepermissionsfrom' => 'moodle/course:manageactivities' ), 'mod/assign:editothersubmission' => array( 'riskbitmask' => RISK_MANAGETRUST|RISK_DATALOSS|RISK_PERSONAL, 'captype' => 'write', 'contextlevel' => CONTEXT_MODULE ), 'mod/assign:grantextension' => array( 'captype' => 'write', 'contextlevel' => CONTEXT_MODULE, 'archetypes' => array( 'teacher' => CAP_ALLOW, 'editingteacher' => CAP_ALLOW, 'manager' => CAP_ALLOW ), 'clonepermissionsfrom' => 'gradereport/grader:view' ), 'mod/assign:revealidentities' => array( 'captype' => 'write', 'contextlevel' => CONTEXT_MODULE, 'archetypes' => array( 'editingteacher' => CAP_ALLOW, 'manager' => CAP_ALLOW ) ), 'mod/assign:reviewgrades' => array( 'captype' => 'write', 'contextlevel' => CONTEXT_MODULE, 'archetypes' => array( 'editingteacher' => CAP_ALLOW, 'manager' => CAP_ALLOW ), 'clonepermissionsfrom' => 'moodle/grade:manage' ), 'mod/assign:releasegrades' => array( 'captype' => 'write', 'contextlevel' => CONTEXT_MODULE, 'archetypes' => array( 'editingteacher' => CAP_ALLOW, 'manager' => CAP_ALLOW ), 'clonepermissionsfrom' => 'moodle/grade:manage' ), 'mod/assign:managegrades' => array( 'captype' => 'write', 'contextlevel' => CONTEXT_MODULE, 'archetypes' => array( 'editingteacher' => CAP_ALLOW, 'manager' => CAP_ALLOW ), 'clonepermissionsfrom' => 'moodle/grade:manage' ), 'mod/assign:manageallocations' => array( 'captype' => 'write', 'contextlevel' => CONTEXT_MODULE, 'archetypes' => array( 'editingteacher' => CAP_ALLOW, 'manager' => CAP_ALLOW ), 'clonepermissionsfrom' => 'moodle/grade:manage' ), 'mod/assign:viewgrades' => array( 'captype' => 'read', 'contextlevel' => CONTEXT_MODULE, 'archetypes' => array( 'editingteacher' => CAP_ALLOW, 'manager' => CAP_ALLOW, 'teacher' => CAP_ALLOW ) ), 'mod/assign:viewblinddetails' => array( 'riskbitmask' => RISK_PERSONAL, 'captype' => 'write', 'contextlevel' => CONTEXT_MODULE, 'archetypes' => array( 'manager' => CAP_ALLOW ) ), 'mod/assign:receivegradernotifications' => array( 'captype' => 'read', 'contextlevel' => CONTEXT_MODULE, 'archetypes' => array( 'teacher' => CAP_ALLOW, 'editingteacher' => CAP_ALLOW, 'manager' => CAP_ALLOW ) ), // Edit the assign overrides. 'mod/assign:manageoverrides' => array( 'captype' => 'write', 'contextlevel' => CONTEXT_MODULE, 'archetypes' => array( 'editingteacher' => CAP_ALLOW, 'manager' => CAP_ALLOW ) ), 'mod/assign:showhiddengrader' => array( 'captype' => 'read', 'contextlevel' => CONTEXT_MODULE, 'archetypes' => array( 'teacher' => CAP_ALLOW, 'editingteacher' => CAP_ALLOW, 'manager' => CAP_ALLOW ) ), 'mod/assign:viewownsubmissionsummary' => array( 'captype' => 'read', 'contextlevel' => CONTEXT_MODULE, 'archetypes' => array( 'student' => CAP_ALLOW ) ), ); db/upgrade.php000064400000013726152265517350007315 0ustar00. /** * Upgrade code for install * * @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 */ /** * upgrade this assignment instance - this function could be skipped but it will be needed later * @param int $oldversion The old version of the assign module * @return bool */ function xmldb_assign_upgrade($oldversion) { global $DB; // Automatically generated Moodle v4.2.0 release upgrade line. // Put any upgrade step following this. // Automatically generated Moodle v4.3.0 release upgrade line. // Put any upgrade step following this. $dbman = $DB->get_manager(); // Loads ddl manager and xmldb classes. if ($oldversion < 2023103000) { // Define field activity to be added to assign. $table = new xmldb_table('assign'); $field = new xmldb_field( 'markinganonymous', XMLDB_TYPE_INTEGER, '2', null, XMLDB_NOTNULL, null, '0', 'markingallocation' ); // Conditionally launch add field activity. if (!$dbman->field_exists($table, $field)) { $dbman->add_field($table, $field); } // Assign savepoint reached. upgrade_mod_savepoint(true, 2023103000, 'assign'); } // Automatically generated Moodle v4.4.0 release upgrade line. // Put any upgrade step following this. if ($oldversion < 2024042201) { // The 'Never' ('none') option for the additional attempts (attemptreopenmethod) setting is no longer supported // and needs to be updated in all relevant instances. // The default value for the 'attemptreopenmethod' field in the 'assign' database table is currently set to 'none', // This needs to be updated to 'untilpass' to ensure the system functions correctly. Additionally, the default // value for the 'maxattempts' field needs to be changed to '1' to prevent multiple attempts and maintain the // original behavior. $table = new xmldb_table('assign'); $attemptreopenmethodfield = new xmldb_field('attemptreopenmethod', XMLDB_TYPE_CHAR, '10', null, XMLDB_NOTNULL, null, 'untilpass'); $maxattemptsfield = new xmldb_field('maxattempts', XMLDB_TYPE_INTEGER, '6', null, XMLDB_NOTNULL, null, '1'); $dbman->change_field_default($table, $attemptreopenmethodfield); $dbman->change_field_default($table, $maxattemptsfield); // If the current value for the 'attemptreopenmethod' global configuration in the assignment is set to 'none'. if (get_config('assign', 'attemptreopenmethod') == 'none') { // Reset the value to 'untilpass'. set_config('attemptreopenmethod', 'untilpass', 'assign'); // Also, setting the value for the 'maxattempts' global config in the assignment to '1' ensures that the // original behaviour is preserved by disallowing any additional attempts by default. set_config('maxattempts', 1, 'assign'); } // Update all the current assignment instances that have their 'attemptreopenmethod' set to 'none'. // By setting 'maxattempts' to 1, additional attempts are disallowed, preserving the original behavior. $DB->execute( 'UPDATE {assign} SET attemptreopenmethod = :newattemptreopenmethod, maxattempts = :maxattempts WHERE attemptreopenmethod = :oldattemptreopenmethod', [ 'newattemptreopenmethod' => 'untilpass', 'maxattempts' => 1, 'oldattemptreopenmethod' => 'none', ] ); // Assign savepoint reached. upgrade_mod_savepoint(true, 2024042201, 'assign'); } // Automatically generated Moodle v4.5.0 release upgrade line. // Put any upgrade step following this. if ($oldversion < 2024121801) { // Define field gradepenalty to be added to assign. $table = new xmldb_table('assign'); $field = new xmldb_field('gradepenalty', XMLDB_TYPE_INTEGER, '2', null, XMLDB_NOTNULL, null, '0', 'submissionattachments'); // Conditionally launch add field gradepenalty. if (!$dbman->field_exists($table, $field)) { $dbman->add_field($table, $field); } // Define index gradepenalty (not unique) to be added to assign. $index = new xmldb_index('gradepenalty', XMLDB_INDEX_NOTUNIQUE, ['gradepenalty']); // Conditionally launch add index gradepenalty. if (!$dbman->index_exists($table, $index)) { $dbman->add_index($table, $index); } // Define field penalty to be added to assign_grades. $table = new xmldb_table('assign_grades'); $field = new xmldb_field('penalty', XMLDB_TYPE_NUMBER, '10, 5', null, XMLDB_NOTNULL, null, '0', 'grade'); // Conditionally launch add field penalty. if (!$dbman->field_exists($table, $field)) { $dbman->add_field($table, $field); } // Assign savepoint reached. upgrade_mod_savepoint(true, 2024121801, 'assign'); } // Automatically generated Moodle v5.0.0 release upgrade line. // Put any upgrade step following this. return true; } db/install.xml000064400000043571152265517350007346 0ustar00
db/events.php000064400000002560152265517350007164 0ustar00. /** * Add event handlers for the assign * * @package mod_assign * @category event * @copyright 2016 Ilya Tregubov * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); $observers = array( array( 'eventname' => '\core\event\course_reset_started', 'callback' => '\mod_assign\group_observers::course_reset_started', ), array( 'eventname' => '\core\event\course_reset_ended', 'callback' => '\mod_assign\group_observers::course_reset_ended', ), array( 'eventname' => '\core\event\group_deleted', 'callback' => '\mod_assign\group_observers::group_deleted' ), ); db/messages.php000064400000004155152265517350007471 0ustar00. /** * Defines message providers (types of messages being sent) * * @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 */ $messageproviders = array ( // Ordinary assignment submissions. 'assign_notification' => [ 'defaults' => [ 'popup' => MESSAGE_PERMITTED + MESSAGE_DEFAULT_ENABLED, 'email' => MESSAGE_PERMITTED + MESSAGE_DEFAULT_ENABLED, ], ], // Assignments with a due date soon. 'assign_due_soon' => [ 'defaults' => [ 'popup' => MESSAGE_PERMITTED + MESSAGE_DEFAULT_ENABLED, 'email' => MESSAGE_PERMITTED + MESSAGE_DEFAULT_ENABLED, 'airnotifier' => MESSAGE_PERMITTED + MESSAGE_DEFAULT_ENABLED, ], ], // Assignments that are overdue. 'assign_overdue' => [ 'defaults' => [ 'popup' => MESSAGE_PERMITTED + MESSAGE_DEFAULT_ENABLED, 'email' => MESSAGE_PERMITTED + MESSAGE_DEFAULT_ENABLED, 'airnotifier' => MESSAGE_PERMITTED + MESSAGE_DEFAULT_ENABLED, ], ], // Assignments that are due in 7 days. 'assign_due_digest' => [ 'defaults' => [ 'popup' => MESSAGE_PERMITTED + MESSAGE_DEFAULT_ENABLED, 'email' => MESSAGE_PERMITTED + MESSAGE_DEFAULT_ENABLED, 'airnotifier' => MESSAGE_PERMITTED + MESSAGE_DEFAULT_ENABLED, ], ], ); db/caches.php000064400000002172152265517350007105 0ustar00. /** * Defined caches used internally by the plugin. * * @package mod_assign * @copyright 2021 Shamim Rezaie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ declare(strict_types=1); defined('MOODLE_INTERNAL') || die(); $definitions = [ 'overrides' => [ 'mode' => cache_store::MODE_APPLICATION, 'simplekeys' => true, 'datasource' => '\mod_assign\cache\overrides', ], ]; renderable.php000064400000074457152265517350007414 0ustar00. /** * This file contains the definition for the renderable classes for the assignment * * @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 */ use \mod_assign\output\assign_submission_status; defined('MOODLE_INTERNAL') || die(); /** * This class wraps the submit for grading confirmation page * @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_submit_for_grading_page implements renderable { /** @var array $notifications is a list of notification messages returned from the plugins */ public $notifications = array(); /** @var int $coursemoduleid */ public $coursemoduleid = 0; /** @var moodleform $confirmform */ public $confirmform = null; /** * Constructor * @param string $notifications - Any mesages to display * @param int $coursemoduleid * @param moodleform $confirmform */ public function __construct($notifications, $coursemoduleid, $confirmform) { $this->notifications = $notifications; $this->coursemoduleid = $coursemoduleid; $this->confirmform = $confirmform; } } /** * Implements a renderable message notification * @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_gradingmessage implements renderable { /** @var string $heading is the heading to display to the user */ public $heading = ''; /** @var string $message is the message to display to the user */ public $message = ''; /** @var int $coursemoduleid */ public $coursemoduleid = 0; /** @var int $gradingerror should be set true if there was a problem grading */ public $gradingerror = null; /** @var int the grading page. */ public $page; /** * Constructor * @param string $heading This is the heading to display * @param string $message This is the message to display * @param bool $gradingerror Set to true to display the message as an error. * @param int $coursemoduleid * @param int $page This is the current quick grading page */ public function __construct($heading, $message, $coursemoduleid, $gradingerror = false, $page = null) { $this->heading = $heading; $this->message = $message; $this->coursemoduleid = $coursemoduleid; $this->gradingerror = $gradingerror; $this->page = $page; } } /** * Implements a renderable grading options form * @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_form implements renderable { /** @var moodleform $form is the edit submission form */ public $form = null; /** @var string $classname is the name of the class to assign to the container */ public $classname = ''; /** @var string $jsinitfunction is an optional js function to add to the page requires */ public $jsinitfunction = ''; /** * Constructor * @param string $classname This is the class name for the container div * @param moodleform $form This is the moodleform * @param string $jsinitfunction This is an optional js function to add to the page requires */ public function __construct($classname, moodleform $form, $jsinitfunction = '') { $this->classname = $classname; $this->form = $form; $this->jsinitfunction = $jsinitfunction; } } /** * Implements a renderable user summary * @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_user_summary implements renderable { /** @var stdClass $user suitable for rendering with user_picture and fullname(). */ public $user = null; /** @var int $courseid */ public $courseid; /** @var bool $viewfullnames */ public $viewfullnames = false; /** @var bool $blindmarking */ public $blindmarking = false; /** @var int $uniqueidforuser */ public $uniqueidforuser; /** @var array $extrauserfields */ public $extrauserfields; /** @var bool $suspendeduser */ public $suspendeduser; /** * Constructor * @param stdClass $user * @param int $courseid * @param bool $viewfullnames * @param bool $blindmarking * @param int $uniqueidforuser * @param array $extrauserfields * @param bool $suspendeduser */ public function __construct(stdClass $user, $courseid, $viewfullnames, $blindmarking, $uniqueidforuser, $extrauserfields, $suspendeduser = false) { $this->user = $user; $this->courseid = $courseid; $this->viewfullnames = $viewfullnames; $this->blindmarking = $blindmarking; $this->uniqueidforuser = $uniqueidforuser; $this->extrauserfields = $extrauserfields; $this->suspendeduser = $suspendeduser; } } /** * Implements a renderable feedback plugin feedback * @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_feedback_plugin_feedback implements renderable { /** @var int SUMMARY */ const SUMMARY = 10; /** @var int FULL */ const FULL = 20; /** @var assign_submission_plugin $plugin */ public $plugin = null; /** @var stdClass $grade */ public $grade = null; /** @var string $view */ public $view = self::SUMMARY; /** @var int $coursemoduleid */ public $coursemoduleid = 0; /** @var string returnaction The action to take you back to the current page */ public $returnaction = ''; /** @var array returnparams The params to take you back to the current page */ public $returnparams = array(); /** * Feedback for a single plugin * * @param assign_feedback_plugin $plugin * @param stdClass $grade * @param string $view one of feedback_plugin::SUMMARY or feedback_plugin::FULL * @param int $coursemoduleid * @param string $returnaction The action required to return to this page * @param array $returnparams The params required to return to this page */ public function __construct(assign_feedback_plugin $plugin, stdClass $grade, $view, $coursemoduleid, $returnaction, $returnparams) { $this->plugin = $plugin; $this->grade = $grade; $this->view = $view; $this->coursemoduleid = $coursemoduleid; $this->returnaction = $returnaction; $this->returnparams = $returnparams; } } /** * Implements a renderable submission plugin submission * @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_submission_plugin_submission implements renderable { /** @var int SUMMARY */ const SUMMARY = 10; /** @var int FULL */ const FULL = 20; /** @var assign_submission_plugin $plugin */ public $plugin = null; /** @var stdClass $submission */ public $submission = null; /** @var string $view */ public $view = self::SUMMARY; /** @var int $coursemoduleid */ public $coursemoduleid = 0; /** @var string returnaction The action to take you back to the current page */ public $returnaction = ''; /** @var array returnparams The params to take you back to the current page */ public $returnparams = array(); /** * Constructor * @param assign_submission_plugin $plugin * @param stdClass $submission * @param string $view one of submission_plugin::SUMMARY, submission_plugin::FULL * @param int $coursemoduleid - the course module id * @param string $returnaction The action to return to the current page * @param array $returnparams The params to return to the current page */ public function __construct(assign_submission_plugin $plugin, stdClass $submission, $view, $coursemoduleid, $returnaction, $returnparams) { $this->plugin = $plugin; $this->submission = $submission; $this->view = $view; $this->coursemoduleid = $coursemoduleid; $this->returnaction = $returnaction; $this->returnparams = $returnparams; } } /** * Renderable feedback status * @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_feedback_status implements renderable { /** @var string $gradefordisplay the student grade rendered into a format suitable for display */ public $gradefordisplay = ''; /** @var mixed the graded date (may be null) */ public $gradeddate = 0; /** @var mixed the grader (may be null) */ public $grader = null; /** @var array feedbackplugins - array of feedback plugins */ public $feedbackplugins = array(); /** @var stdClass assign_grade record */ public $grade = null; /** @var int coursemoduleid */ public $coursemoduleid = 0; /** @var string returnaction */ public $returnaction = ''; /** @var array returnparams */ public $returnparams = array(); /** @var bool canviewfullnames */ public $canviewfullnames = false; /** @var string gradingcontrollergrade The grade information rendered by a grade controller */ public $gradingcontrollergrade; /** @var array information for the given plugins. */ public $plugins = []; /** * Constructor * @param string $gradefordisplay * @param mixed $gradeddate * @param mixed $grader * @param array $feedbackplugins * @param mixed $grade * @param int $coursemoduleid * @param string $returnaction The action required to return to this page * @param array $returnparams The list of params required to return to this page * @param bool $canviewfullnames * @param string $gradingcontrollergrade The grade information rendered by a grade controller */ public function __construct($gradefordisplay, $gradeddate, $grader, $feedbackplugins, $grade, $coursemoduleid, $returnaction, $returnparams, $canviewfullnames, $gradingcontrollergrade = '') { $this->gradefordisplay = $gradefordisplay; $this->gradeddate = $gradeddate; $this->grader = $grader; $this->feedbackplugins = $feedbackplugins; $this->grade = $grade; $this->coursemoduleid = $coursemoduleid; $this->returnaction = $returnaction; $this->returnparams = $returnparams; $this->canviewfullnames = $canviewfullnames; $this->gradingcontrollergrade = $gradingcontrollergrade; } } /** * Renderable submission status * @package mod_assign * @copyright 2016 Damyon Wiese * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class assign_submission_status_compact extends assign_submission_status implements renderable { // Compact view of the submission status. Not in a table etc. } /** * Used to output the attempt history for a particular assignment. * * @package mod_assign * @copyright 2012 Davo Smith, Synergy Learning * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class assign_attempt_history implements renderable { /** @var array submissions - The list of previous attempts */ public $submissions = array(); /** @var array grades - The grades for the previous attempts */ public $grades = array(); /** @var array submissionplugins - The list of submission plugins to render the previous attempts */ public $submissionplugins = array(); /** @var array feedbackplugins - The list of feedback plugins to render the previous attempts */ public $feedbackplugins = array(); /** @var int coursemoduleid - The cmid for the assignment */ public $coursemoduleid = 0; /** @var string returnaction - The action for the next page. */ public $returnaction = ''; /** @var string returnparams - The params for the next page. */ public $returnparams = array(); /** @var bool cangrade - Does this user have grade capability? */ public $cangrade = false; /** @var string useridlistid - Id of the useridlist stored in cache, this plus rownum determines the userid */ public $useridlistid = 0; /** @var int rownum - The rownum of the user in the useridlistid - this plus useridlistid determines the userid */ public $rownum = 0; /** * Constructor * * @param array $submissions * @param array $grades * @param array $submissionplugins * @param array $feedbackplugins * @param int $coursemoduleid * @param string $returnaction * @param array $returnparams * @param bool $cangrade * @param int $useridlistid * @param int $rownum */ public function __construct($submissions, $grades, $submissionplugins, $feedbackplugins, $coursemoduleid, $returnaction, $returnparams, $cangrade, $useridlistid, $rownum) { $this->submissions = $submissions; $this->grades = $grades; $this->submissionplugins = $submissionplugins; $this->feedbackplugins = $feedbackplugins; $this->coursemoduleid = $coursemoduleid; $this->returnaction = $returnaction; $this->returnparams = $returnparams; $this->cangrade = $cangrade; $this->useridlistid = $useridlistid; $this->rownum = $rownum; } } /** * Used to output the attempt history chooser for a particular assignment. * * @package mod_assign * @copyright 2016 Damyon Wiese * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class assign_attempt_history_chooser implements renderable, templatable { /** @var array submissions - The list of previous attempts */ public $submissions = array(); /** @var array grades - The grades for the previous attempts */ public $grades = array(); /** @var int coursemoduleid - The cmid for the assignment */ public $coursemoduleid = 0; /** @var int userid - The current userid */ public $userid = 0; /** @var int submission count */ public $submissioncount; /** * Constructor * * @param array $submissions * @param array $grades * @param int $coursemoduleid * @param int $userid */ public function __construct($submissions, $grades, $coursemoduleid, $userid) { $this->submissions = $submissions; $this->grades = $grades; $this->coursemoduleid = $coursemoduleid; $this->userid = $userid; } /** * Function to export the renderer data in a format that is suitable for a * mustache template. * * @param renderer_base $output Used to do a final render of any components that need to be rendered for export. * @return stdClass|array */ public function export_for_template(renderer_base $output) { // Show newest to oldest. $export = (object) $this; $export->submissions = array_reverse($export->submissions); $export->submissioncount = count($export->submissions); foreach ($export->submissions as $i => $submission) { $grade = null; foreach ($export->grades as $onegrade) { if ($onegrade->attemptnumber == $submission->attemptnumber) { $submission->grade = $onegrade; break; } } if (!$submission) { $submission = new stdClass(); } $editbtn = ''; if ($submission->timemodified) { $submissionsummary = userdate($submission->timemodified); } else { $submissionsummary = get_string('nosubmission', 'assign'); } $attemptsummaryparams = array('attemptnumber' => $submission->attemptnumber + 1, 'submissionsummary' => $submissionsummary); $submission->attemptsummary = get_string('attemptheading', 'assign', $attemptsummaryparams); $submission->statussummary = get_string('submissionstatus_' . $submission->status, 'assign'); } return $export; } } /** * Renderable header related to an individual subplugin * @package mod_assign * @copyright 2014 Henning Bostelmann * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class assign_plugin_header implements renderable { /** @var assign_plugin $plugin */ public $plugin = null; /** * Header for a single plugin * * @param assign_plugin $plugin */ public function __construct(assign_plugin $plugin) { $this->plugin = $plugin; } } /** * Renderable grading summary * @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_grading_summary implements renderable { /** @var int participantcount - The number of users who can submit to this assignment */ public $participantcount = 0; /** @var bool submissiondraftsenabled - Allow submission drafts */ public $submissiondraftsenabled = false; /** @var int submissiondraftscount - The number of submissions in draft status */ public $submissiondraftscount = 0; /** @var bool submissionsenabled - Allow submissions */ public $submissionsenabled = false; /** @var int submissionssubmittedcount - The number of submissions in submitted status */ public $submissionssubmittedcount = 0; /** @var int submissionsneedgradingcount - The number of submissions that need grading */ public $submissionsneedgradingcount = 0; /** @var int duedate - The assignment due date (if one is set) */ public $duedate = 0; /** @var int cutoffdate - The assignment cut off date (if one is set) */ public $cutoffdate = 0; /** @var int timelimit - The assignment time limit (if one is set) */ public $timelimit = 0; /** @var int coursemoduleid - The assignment course module id */ public $coursemoduleid = 0; /** @var boolean teamsubmission - Are team submissions enabled for this assignment */ public $teamsubmission = false; /** @var boolean warnofungroupedusers - Do we need to warn people that there are users without groups */ public $warnofungroupedusers = false; /** @var boolean relativedatesmode - Is the course a relative dates mode course or not */ public $courserelativedatesmode = false; /** @var int coursestartdate - start date of the course as a unix timestamp*/ public $coursestartdate; /** @var boolean cangrade - Can the current user grade students? */ public $cangrade = false; /** @var boolean isvisible - Is the assignment's context module visible to students? */ public $isvisible = true; /** @var cm_info $cm - The course module object. */ public $cm = null; /** @var string no warning needed about group submissions */ const WARN_GROUPS_NO = false; /** @var string warn about group submissions, as groups are required */ const WARN_GROUPS_REQUIRED = 'warnrequired'; /** @var string warn about group submissions, as some will submit as 'Default group' */ const WARN_GROUPS_OPTIONAL = 'warnoptional'; /** * constructor * * @param int $participantcount * @param bool $submissiondraftsenabled * @param int $submissiondraftscount * @param bool $submissionsenabled * @param int $submissionssubmittedcount * @param int $cutoffdate * @param int $duedate * @param int $timelimit * @param int $coursemoduleid * @param int $submissionsneedgradingcount * @param bool $teamsubmission * @param string $warnofungroupedusers * @param bool $courserelativedatesmode true if the course is using relative dates, false otherwise. * @param int $coursestartdate unix timestamp representation of the course start date. * @param bool $cangrade * @param bool $isvisible * @param cm_info|null $cm The course module object. */ public function __construct($participantcount, $submissiondraftsenabled, $submissiondraftscount, $submissionsenabled, $submissionssubmittedcount, $cutoffdate, $duedate, $timelimit, $coursemoduleid, $submissionsneedgradingcount, $teamsubmission, $warnofungroupedusers, $courserelativedatesmode, $coursestartdate, $cangrade = true, $isvisible = true, ?cm_info $cm = null) { $this->participantcount = $participantcount; $this->submissiondraftsenabled = $submissiondraftsenabled; $this->submissiondraftscount = $submissiondraftscount; $this->submissionsenabled = $submissionsenabled; $this->submissionssubmittedcount = $submissionssubmittedcount; $this->duedate = $duedate; $this->cutoffdate = $cutoffdate; $this->timelimit = $timelimit; $this->coursemoduleid = $coursemoduleid; $this->submissionsneedgradingcount = $submissionsneedgradingcount; $this->teamsubmission = $teamsubmission; $this->warnofungroupedusers = $warnofungroupedusers; $this->courserelativedatesmode = $courserelativedatesmode; $this->coursestartdate = $coursestartdate; $this->cangrade = $cangrade; $this->isvisible = $isvisible; $this->cm = $cm; } } /** * Renderable course index summary * * @deprecated since Moodle 5.0 (MDL-83888). * @todo MDL-84429 Final deprecation in Moodle 6.0. * * @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_course_index_summary implements renderable { /** @var array assignments - A list of course module info and submission counts or statuses */ public $assignments = array(); /** @var boolean usesections - Does this course format support sections? */ public $usesections = false; /** @var string courseformat - The current course format name */ public $courseformatname = ''; /** * constructor * * @deprecated since Moodle 5.0 (MDL-83888). * @todo MDL-84429 Final deprecation in Moodle 6.0. * @param boolean $usesections - True if this course format uses sections * @param string $courseformatname - The id of this course format */ #[\core\attribute\deprecated( since: '5.0', mdl: 'MDL-83888', reason: 'The assign_course_index_summary class is not used anymore.', )] public function __construct($usesections, $courseformatname) { \core\deprecation::emit_deprecation([$this, __FUNCTION__]); $this->usesections = $usesections; $this->courseformatname = $courseformatname; } /** * Add a row of data to display on the course index page * * @deprecated since Moodle 5.0 (MDL-83888). * @todo MDL-84429 Final deprecation in Moodle 6.0. * @param int $cmid - The course module id for generating a link * @param string $cmname - The course module name for generating a link * @param string $sectionname - The name of the course section (only if $usesections is true) * @param int $timedue - The due date for the assignment - may be 0 if no duedate * @param string $submissioninfo - A string with either the number of submitted assignments, or the * status of the current users submission depending on capabilities. * @param string $gradeinfo - The current users grade if they have been graded and it is not hidden. * @param bool cangrade - Does this user have grade capability? */ #[\core\attribute\deprecated( since: '5.0', mdl: 'MDL-83888', reason: 'The assign_course_index_summary class is not used anymore.', )] public function add_assign_info($cmid, $cmname, $sectionname, $timedue, $submissioninfo, $gradeinfo, $cangrade = false) { \core\deprecation::emit_deprecation([$this, __FUNCTION__]); $this->assignments[] = ['cmid' => $cmid, 'cmname' => $cmname, 'sectionname' => $sectionname, 'timedue' => $timedue, 'submissioninfo' => $submissioninfo, 'gradeinfo' => $gradeinfo, 'cangrade' => $cangrade]; } } /** * An assign file class that extends rendererable class and is used by the 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 */ class assign_files implements renderable { /** @var context $context */ public $context; /** @var array $dir as returned by {@see file_storage::get_area_tree()}. */ public $dir; /** @var MoodleQuickForm $portfolioform */ public $portfolioform; /** @var stdClass $cm course module */ public $cm; /** @var stdClass $course */ public $course; /** * The constructor * * @param context $context * @param int $sid * @param string $filearea * @param string $component * @param stdClass $course * @param stdClass $cm */ public function __construct(context $context, $sid, $filearea, $component, $course = null, $cm = null) { global $CFG; if (empty($course) || empty($cm)) { list($context, $course, $cm) = get_context_info_array($context->id); } $this->context = $context; $this->cm = $cm; $this->course = $course; $fs = get_file_storage(); $this->dir = $fs->get_area_tree($this->context->id, $component, $filearea, $sid); $files = $fs->get_area_files($this->context->id, $component, $filearea, $sid, 'timemodified', false); if (!empty($CFG->enableportfolios)) { require_once($CFG->libdir . '/portfoliolib.php'); if (count($files) >= 1 && !empty($sid) && has_capability('mod/assign:exportownsubmission', $this->context)) { $button = new portfolio_add_button(); $callbackparams = array('cmid' => $this->cm->id, 'sid' => $sid, 'area' => $filearea, 'component' => $component); $button->set_callback_options('assign_portfolio_caller', $callbackparams, 'mod_assign'); $button->reset_formats(); $this->portfolioform = $button->to_html(PORTFOLIO_ADD_TEXT_LINK); } } } /** * Preprocessing the file list to add the portfolio links if required. * * @param array $dir * @param string $filearea * @param string $component * @deprecated since Moodle 4.3 */ public function preprocess($dir, $filearea, $component) { // Nothing to do here any more. debugging('The preprocess method has been deprecated since Moodle 4.3.', DEBUG_DEVELOPER); } /** * Get the modified time of the specified file. * @param stored_file $file * @return string */ public function get_modified_time(stored_file $file): string { return userdate( $file->get_timemodified(), get_string('strftimedatetime', 'langconfig'), ); } /** * Get the URL used to view the file. * * @param stored_file * @return moodle_url */ public function get_file_url(stored_file $file): moodle_url { return \moodle_url::make_pluginfile_url( $this->context->id, $file->get_component(), $file->get_filearea(), $file->get_itemid(), $file->get_filepath(), $file->get_filename(), true, ); } } feedbackplugin.php000064400000020000152265517350010223 0ustar00. /** * This file contains the function for feedback_plugin abstract class * * @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(); require_once($CFG->dirroot.'/mod/assign/assignmentplugin.php'); /** * Abstract class for feedback_plugin inherited from assign_plugin abstract class. * * @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 */ abstract class assign_feedback_plugin extends assign_plugin { /** * Return subtype name of the plugin. * * @return string */ public function get_subtype() { return 'assignfeedback'; } /** * If this plugin adds to the gradebook comments field, it must specify the format * of the comment. * * (From weblib.php) * define('FORMAT_MOODLE', '0'); // Does all sorts of transformations and filtering * define('FORMAT_HTML', '1'); // Plain HTML (with some tags stripped) * define('FORMAT_PLAIN', '2'); // Plain text (even tags are printed in full) * define('FORMAT_WIKI', '3'); // Wiki-formatted text * define('FORMAT_MARKDOWN', '4'); // Markdown-formatted * * Only one feedback plugin can push comments to the gradebook and that is chosen by the assignment * settings page. * * @param stdClass $grade The grade * @return int */ public function format_for_gradebook(stdClass $grade) { return FORMAT_MOODLE; } /** * If this plugin adds to the gradebook comments field, it must format the text * of the comment. * * Only one feedback plugin can push comments to the gradebook and that is chosen by the assignment * settings page. * * @param stdClass $grade The grade * @return string */ public function text_for_gradebook(stdClass $grade) { return ''; } /** * Return any files this plugin wishes to save to the gradebook. * * The array being returned should contain the necessary information to * identify and copy the files. * * eg. * * [ * 'contextid' => $modulecontext->id, * 'component' => ASSIGNFEEDBACK_XYZ_COMPONENT, * 'filearea' => ASSIGNFEEDBACK_XYZ_FILEAREA, * 'itemid' => $grade->id * ] * * @param stdClass $grade The assign_grades object from the db * @return array */ public function files_for_gradebook(stdClass $grade): array { return []; } /** * Override to indicate a plugin supports quickgrading. * * @return boolean - True if the plugin supports quickgrading */ public function supports_quickgrading() { return false; } /** * Get quickgrading form elements as html. * * @param int $userid The user id in the table this quickgrading element relates to * @param mixed $grade grade or null - The grade data. * May be null if there are no grades for this user (yet) * @return mixed - A html string containing the html form elements required for * quickgrading or false to indicate this plugin does not support quickgrading */ public function get_quickgrading_html($userid, $grade) { return false; } /** * Has the plugin quickgrading form element been modified in the current form submission? * * @param int $userid The user id in the table this quickgrading element relates to * @param stdClass $grade The grade * @return boolean - true if the quickgrading form element has been modified */ public function is_quickgrading_modified($userid, $grade) { return false; } /** * Has the plugin form element been modified in the current submission? * * @param stdClass $grade The grade. * @param stdClass $data Form data from the feedback form. * @return boolean - True if the form element has been modified. */ public function is_feedback_modified(stdClass $grade, stdClass $data) { debugging('This plugin (' . $this->get_name() . ') has not overwritten the is_feedback_modified() method. Please add this method to your plugin', DEBUG_DEVELOPER); return true; } /** * Save quickgrading changes. * * @param int $userid The user id in the table this quickgrading element relates to * @param stdClass $grade The grade * @return boolean - true if the grade changes were saved correctly */ public function save_quickgrading_changes($userid, $grade) { return false; } /** * Return a list of detailed batch grading operations supported by this plugin. * * @return array - An array of objects containing batch operation details. Each object should contain: * - 'key': the action identifier (string) * - 'label': the button label (string) * - 'icon': the button icon (string) * - 'confirmationtitle': the title for the confirmation modal (string) * - 'confirmationquestion': the question for the confirmation modal (string) */ public function get_grading_batch_operation_details() { return []; } /** * Return a list of the batch grading operations supported by this plugin. * * @return array - An array of action and description strings. * The action will be passed to grading_batch_operation. * @deprecated since 4.5, use get_grading_batch_operation_details() instead. * @todo Final deprecation in Moodle 6.0. See MDL-82856. */ #[\core\attribute\deprecated('get_grading_batch_operation_details', since: '4.5', mdl: 'MDL-80750')] public function get_grading_batch_operations() { \core\deprecation::emit_deprecation([$this, __FUNCTION__]); return array_column(static::get_grading_batch_operation_details(), 'confirmationtitle', 'key'); } /** * Return a list of the grading actions supported by this plugin. * * A grading action is a page that is not specific to a user but to the whole assignment. * @return array - An array of action and description strings. * The action will be passed to grading_action. */ public function get_grading_actions() { return array(); } /** * Show a grading action form * * @param string $gradingaction The action chosen from the grading actions menu * @return string The page containing the form */ public function grading_action($gradingaction) { return ''; } /** * Supports injecting content into the review panel of the grading app. * * @return bool True if this plugin will add content to the review panel of the grading app. */ public function supports_review_panel() { return false; } /** * Show a batch operations form * * @param string $action The action chosen from the batch operations menu * @param array $users The list of selected userids * @return string The page containing the form */ public function grading_batch_operation($action, $users) { return ''; } } lang/en/deprecated.txt000064400000000451152265517350010743 0ustar00attemptreopenmethod_none,mod_assign groupoverridesdeleted,mod_assign useroverridesdeleted,mod_assign choosegradingaction,mod_assign updategrade,mod_assign viewgrader,mod_assign downloadasfolders_help,mod_assign editaction,mod_assign saveallquickgradingchanges,mod_assign gradingoptions,mod_assign lang/en/assign.php000064400000155371152265517350010113 0ustar00. /** * Strings for component 'assign', language 'en' * * @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 */ $string['activityattachments'] = 'Assignment activity attachments'; $string['activitydate:submissionsdue'] = 'Due:'; $string['activitydate:submissionsopen'] = 'Opens:'; $string['activitydate:submissionsopened'] = 'Opened:'; $string['activityeditor'] = 'Activity instructions'; $string['activityeditor_help'] = 'The actions you would like the student to complete for this assignment. This is only shown on the submission page where a student edits and submits their assignment.'; $string['activityoverview'] = 'You have assignments that need attention'; $string['addattempt'] = 'Allow another attempt'; $string['addnewattempt'] = 'Add a new attempt'; $string['addnewattempt_help'] = 'This will create a new blank submission for you to work on.'; $string['addnewattemptfromprevious'] = 'Add a new attempt based on previous submission'; $string['addnewattemptfromprevious_help'] = 'This will copy the contents of your previous submission to a new submission for you to work on.'; $string['addnewgroupoverride'] = 'Add group override'; $string['addnewuseroverride'] = 'Add user override'; $string['addsubmission'] = 'Add submission'; $string['addsubmission_help'] = 'You have not made a submission yet.'; $string['allocatedmarker'] = 'Allocated marker'; $string['allocatedmarker_help'] = 'Marker allocated to this submission.'; $string['allowsubmissions'] = 'Allow the user to continue making submissions to this assignment.'; $string['allowsubmissionsshort'] = 'Allow submission changes'; $string['allowsubmissionsfromdate'] = 'Allow submissions from'; $string['allowsubmissionsfromdate_help'] = 'If enabled, students will not be able to submit before this date. If disabled, students will be able to start submitting right away.'; $string['alwaysshowdescription'] = 'Always show description'; $string['alwaysshowdescription_help'] = 'If disabled, the assignment description above will only become visible to students on the "Allow submissions from" date.'; $string['applytoteam'] = 'Apply grades and feedback to entire group'; $string['assign:addinstance'] = 'Add a new assignment'; $string['assign:exportownsubmission'] = 'Export own submission'; $string['assign:editothersubmission'] = 'Edit another student\'s submission'; $string['assign:grade'] = 'Grade assignment'; $string['assign:grantextension'] = 'Grant extension'; $string['assign:manageallocations'] = 'Manage markers allocated to submissions'; $string['assign:managegrades'] = 'Review and release grades'; $string['assign:manageoverrides'] = 'Manage assignment overrides'; $string['assign:receivegradernotifications'] = 'Receive grader submission notifications'; $string['assign:releasegrades'] = 'Release grades'; $string['assign:revealidentities'] = 'Reveal student identities'; $string['assign:reviewgrades'] = 'Review grades'; $string['assign:viewblinddetails'] = 'View student identities when anonymous submissions are enabled'; $string['assign:viewgrades'] = 'View grades'; $string['assign:showhiddengrader'] = 'See the identity of a hidden grader'; $string['assign:submit'] = 'Submit assignment'; $string['assign:view'] = 'View assignment'; $string['assign:viewownsubmissionsummary'] = 'View own submission summary'; $string['assignfeedback'] = 'Feedback plugin'; $string['assignfeedbackpluginname'] = 'Feedback plugin'; $string['assignmentduedigesthtml'] = '

Hi {$a->firstname},

The following assignments are due on {$a->duedate}.

{$a->digest}'; $string['assignmentduedigestitem'] = '{$a->assignmentname} in course {$a->coursename}
Due: {$a->duetime}
Go to activity'; $string['assignmentduedigestsms'] = 'One or more assignments are due in 7 days. Log in to view upcoming assignments.'; $string['assignmentduedigestsubject'] = 'You have assignments due in 7 days'; $string['assignmentduesoonhtml'] = '

Hi {$a->firstname},

The assignment {$a->assignmentname} in course {$a->coursename} is due soon.

Due: {$a->duedate}

Go to activity

'; $string['assignmentduesoonofflinehtml'] = '

Hi {$a->firstname},

The assignment {$a->assignmentname} in course {$a->coursename} is due soon.

If you have already submitted your work, no further action is required.

Due: {$a->duedate}

Go to activity

'; $string['assignmentduesoonsms'] = 'Your assignment {$a->assignmentname} is due on {$a->duedate}: {$a->url}'; $string['assignmentoverduehtml'] = '

Hi {$a->firstname},

{$a->assignmentname} in course {$a->coursename} was due on {$a->duedate}.

You might still be able to submit your assignment{$a->cutoffsnippet}, but your submission will be marked as late.

Go to activity

'; $string['assignmentoverduesms'] = 'Your assignment {$a->assignmentname} is overdue on {$a->duedate}: {$a->url}'; $string['assignmentoverduehtmlcutoffsnippet'] = ' by {$a->cutoffdate}'; $string['assignmentduesoonsubject'] = 'Due on {$a->duedate}: {$a->assignmentname}'; $string['assignmentoverduesubject'] = 'Overdue: {$a->assignmentname}'; $string['assignmentisdue'] = 'Assignment is due'; $string['assignmentmail'] = '{$a->grader} has posted some feedback on your assignment submission for \'{$a->assignment}\' You can see it appended to your assignment submission: {$a->url}'; $string['assignmentmailhtml'] = '

{$a->grader} has posted some feedback on your assignment submission for \'{$a->assignment}\'.

You can see it appended to your assignment submission.

'; $string['assignmentmailsmall'] = '{$a->grader} has posted some feedback on your assignment submission for \'{$a->assignment}\' You can see it appended to your submission'; $string['assignmentname'] = 'Assignment name'; $string['assignmentplugins'] = 'Assignment plugins'; $string['assignmentsperpage'] = 'Assignments per page'; $string['assignsubmission'] = 'Submission plugin'; $string['assignsubmissionpluginname'] = 'Submission plugin'; $string['assigntimeleft'] = 'Time left'; $string['attemptheading'] = 'Attempt {$a->attemptnumber}: {$a->submissionsummary}'; $string['attempthistory'] = 'Previous attempts'; $string['attemptnumber'] = 'Attempt number'; $string['attemptsettings'] = 'Attempt settings'; $string['attemptreopenmethod'] = 'Grant attempts'; $string['attemptreopenmethod_automatic'] = 'Automatically'; $string['attemptreopenmethod_automatic_help'] = 'After each attempt, the next is granted automatically after grading.'; $string['attemptreopenmethod_help'] = 'This setting controls how students are granted attempts for this assignment. For each attempt, the grade and feedback are saved, and can be viewed by the teacher and the student. Options are: * Manually - After each attempt, you can grant the next one through the Submissions page or the Grader page. * Automatically - After each attempt, the next is granted automatically after grading. * Automatically until pass - After each attempt, the next will be granted automatically after grading, until the student achieves the passing grade.'; $string['attemptreopenmethod_manual'] = 'Manually'; $string['attemptreopenmethod_manual_help'] = 'After each attempt, you can grant the next one through the Submissions page or the Grader page.'; $string['attemptreopenmethod_untilpass'] = 'Automatically until pass'; $string['attemptreopenmethod_untilpass_help'] = 'After each attempt, the next will be granted automatically after grading, until the student achieves the passing grade.'; $string['availability'] = 'Availability'; $string['back'] = 'Back'; $string['backtoassignment'] = 'Back to assignment'; $string['batchoperationaddattempt'] = 'Grant attempt'; $string['batchoperationsdescription'] = 'With selected...'; $string['batchoperationconfirmlock'] = 'Lock all selected submissions?'; $string['batchoperationconfirmgrantextension'] = 'Grant an extension to all selected submissions?'; $string['batchoperationconfirmunlock'] = 'Unlock all selected submissions?'; $string['batchoperationconfirmremovesubmission'] = 'Remove selected submissions?'; $string['batchoperationconfirmreverttodraft'] = 'Revert selected submissions to draft?'; $string['batchoperationconfirmaddattempt'] = 'Allow another attempt for selected submissions?'; $string['batchoperationconfirmsetmarkingworkflowstate'] = 'Set marking workflow state for all selected submissions?'; $string['batchoperationconfirmsetmarkingallocation'] = 'Set marking allocation for all selected submissions?'; $string['batchoperationconfirmdownloadselected'] = 'Download selected submissions?'; $string['batchoperationdownloadselected'] = 'Download'; $string['batchoperationgrantextension'] = 'Extend'; $string['batchoperationlock'] = 'Lock'; $string['batchoperationremovesubmission'] = 'Delete'; $string['batchoperationunlock'] = 'Unlock'; $string['batchoperationreverttodraft'] = 'Revert to draft'; $string['batchoperationsetmarkingallocation'] = 'Allocate marker'; $string['batchoperationsetmarkingworkflowstate'] = 'Change marking state'; $string['batchsetallocatedmarker'] = 'Set allocated marker for {$a} selected user(s).'; $string['batchsetmarkingworkflowstateforusers'] = 'Set marking workflow state for {$a} selected user(s).'; $string['beginassignment'] = 'Begin assignment'; $string['blindmarking'] = 'Anonymous submissions'; $string['blindmarkingenabledwarning'] = 'Anonymous submissions are enabled for this activity.'; $string['blindmarkingnogradewarning'] = 'Anonymous submissions are enabled for this activity. Grades will not be added to the gradebook until student identities are revealed via the "Actions" menu.'; $string['blindmarking_help'] = 'Anonymous submissions hide the identity of students from markers. Anonymous submission settings will be locked once a submission or grade has been made in relation to this assignment.'; $string['bulkactionsselection'] = 'Bulk actions selection'; $string['cachedef_overrides'] = 'User and group override information'; $string['calendardue'] = '{$a} is due'; $string['calendarextension'] = '{$a} is due (extension)'; $string['calendargradingdue'] = '{$a} is due to be graded'; $string['caneditsubmission'] = 'You can edit your submission and submit it after the time limit has expired, but it will be marked as late.'; $string['changeuser'] = 'Change user'; $string['changefilters'] = 'Change filters'; $string['choosemarker'] = 'Choose...'; $string['chooseoperation'] = 'Choose operation'; $string['clickexpandreviewpanel'] = 'Click to expand review panel'; $string['collapsegradepanel'] = 'Collapse grade panel'; $string['collapsereviewpanel'] = 'Collapse review panel'; $string['comment'] = 'Comment'; $string['completiondetail:submit'] = 'Make a submission'; $string['completionsubmit'] = 'Make a submission'; $string['conversionexception'] = 'Could not convert assignment. Exception was: {$a}.'; $string['configshowrecentsubmissions'] = 'Everyone can see notifications of submissions in recent activity reports.'; $string['confirmsubmission'] = 'Are you sure you want to submit your work for grading? You will not be able to make any more changes.'; $string['confirmsubmissionheading'] = 'Confirm submission'; $string['confirmbatchgradingoperation'] = 'Are you sure you want to {$a->operation} for {$a->count} students?'; $string['couldnotconvertgrade'] = 'Could not convert assignment grade for user {$a}.'; $string['couldnotconvertsubmission'] = 'Could not convert assignment submission for user {$a}.'; $string['couldnotcreatecoursemodule'] = 'Could not create course module.'; $string['couldnotcreatenewassignmentinstance'] = 'Could not create new assignment instance.'; $string['couldnotfindassignmenttoupgrade'] = 'Could not find old assignment instance to upgrade.'; $string['crontask'] = 'Background processing for assignment module'; $string['currentassigngrade'] = 'Current grade in assignment'; $string['currentgrade'] = 'Current grade in gradebook'; $string['currentattempt'] = 'This is attempt {$a}.'; $string['currentattemptof'] = 'This is attempt {$a->attemptnumber} ( {$a->maxattempts} attempts allowed ).'; $string['cutoffdate'] = 'Cut-off date'; $string['cutoffdatecolon'] = 'Cut-off date: {$a}'; $string['cutoffdate_help'] = 'If set, submissions will not be accepted after this date without an extension. If not set, submissions will always be accepted.'; $string['cutoffdatevalidation'] = 'Cut-off date cannot be earlier than the due date.'; $string['cutoffdatefromdatevalidation'] = 'Cut-off date cannot be earlier than the allow submissions from date.'; $string['defaultgradescale'] = 'Grading scale'; $string['defaultgradescale_help'] = 'The default grading scale for new assignments. Only standard scales are available.'; $string['defaultgradetype'] = 'Grade type'; $string['defaultgradetype_help'] = 'The default grade type for new assignments.'; $string['defaultlayout'] = 'Restore default layout'; $string['defaultsettings'] = 'Default assignment settings'; $string['defaultsettings_help'] = 'These settings define the defaults for all new assignments.'; $string['defaultteam'] = 'Default group'; $string['deleteallsubmissions'] = 'All submissions'; $string['description'] = 'Description'; $string['disabled'] = 'Disabled'; $string['downloadall'] = 'Download all submissions'; $string['download all submissions'] = 'Download all submissions in a zip file.'; $string['downloadasfolders'] = 'Download submissions in folders'; $string['downloadselectedsubmissions'] = 'Download selected submissions'; $string['duedate'] = 'Due date'; $string['duedatecolon'] = 'Due date: {$a}'; $string['duedate_help'] = 'This is when the assignment is due. Submissions will still be allowed after this date, but any assignments submitted after this date will be marked as late. Set an assignment cut-off date to prevent submissions after a certain date.'; $string['duplicateoverride'] = 'Duplicate override'; $string['submissionempty'] = 'Nothing was submitted'; $string['submissionmodified'] = 'You have existing submission data. Please leave this page and try again.'; $string['submissionmodifiedgroup'] = 'The submission has been modified by somebody else. Please leave this page and try again.'; $string['duedatereached'] = 'The due date for this assignment has now passed'; $string['duedateaftersubmissionvalidation'] = 'Due date must be after the allow submissions from date.'; $string['duedatevalidation'] = 'Due date cannot be earlier than the allow submissions from date.'; $string['editattemptfeedback'] = 'Edit the grade and feedback for attempt number {$a}.'; $string['editonline'] = 'Edit online'; $string['editingpreviousfeedbackwarning'] = 'You are editing the feedback for a previous attempt. This is attempt {$a->attemptnumber} out of {$a->totalattempts}.'; $string['editoverride'] = 'Edit override'; $string['editsubmission'] = 'Edit submission'; $string['editsubmissionother'] = 'Edit submission for {$a}'; $string['editsubmission_help'] = 'You can still make changes to your submission.'; $string['editingstatus'] = 'Editing status'; $string['enabled'] = 'Enabled'; $string['enabletimelimit'] = 'Enable timed assignments'; $string['enabletimelimit_help'] = 'If enabled, you can set a time limit on assignment settings page.'; $string['errorgradechangessaveddetail'] = 'The changes to the grade and feedback were not saved.'; $string['eventallsubmissionsdownloaded'] = 'All the submissions are being downloaded.'; $string['eventassessablesubmitted'] = 'A submission has been submitted.'; $string['eventbatchsetmarkerallocationviewed'] = 'Batch set marker allocation viewed'; $string['eventbatchsetworkflowstateviewed'] = 'Batch set workflow state viewed.'; $string['eventextensiongranted'] = 'An extension has been granted.'; $string['eventfeedbackupdated'] = 'Feedback updated'; $string['eventfeedbackviewed'] = 'Feedback viewed'; $string['eventgradingformviewed'] = 'Grading form viewed'; $string['eventgradingtableviewed'] = 'Grading table viewed'; $string['eventidentitiesrevealed'] = 'The identities have been revealed.'; $string['eventmarkerupdated'] = 'The allocated marker has been updated.'; $string['eventoverridecreated'] = 'Assignment override created'; $string['eventoverridedeleted'] = 'Assignment override deleted'; $string['eventoverrideupdated'] = 'Assignment override updated'; $string['eventremovesubmissionformviewed'] = 'Remove submission confirmation viewed.'; $string['eventrevealidentitiesconfirmationpageviewed'] = 'Reveal identities confirmation page viewed.'; $string['eventstatementaccepted'] = 'The user has accepted the statement of the submission.'; $string['eventsubmissionconfirmationformviewed'] = 'Submission confirmation form viewed.'; $string['eventsubmissioncreated'] = 'Submission created.'; $string['eventsubmissionduplicated'] = 'The user duplicated their submission.'; $string['eventsubmissionformviewed'] = 'Submission form viewed.'; $string['eventsubmissiongraded'] = 'The submission has been graded.'; $string['eventsubmissionlocked'] = 'The submissions have been locked for a user.'; $string['eventsubmissionremoved'] = 'Submission removed.'; $string['eventsubmissionstatusupdated'] = 'The status of the submission has been updated.'; $string['eventsubmissionstatusviewed'] = 'The status of the submission has been viewed.'; $string['eventsubmissionunlocked'] = 'The submissions have been unlocked for a user.'; $string['eventsubmissionupdated'] = 'Submission updated.'; $string['eventsubmissionviewed'] = 'Submission viewed.'; $string['eventworkflowstateupdated'] = 'The state of the workflow has been updated.'; $string['expandreviewpanel'] = 'Expand review panel'; $string['extensionduedate'] = 'Extension due date'; $string['extensionduedatecurrent'] = 'Current extension due date'; $string['extensionduedatenone'] = 'None'; $string['extensionduedaterange'] = 'Various between {$a->earliest} and {$a->latest}'; $string['extensionduedatewithout'] = 'Users with no current extension: {$a}'; $string['extensionnotafterduedate'] = 'Extension date must be after the due date'; $string['extensionnotafterfromdate'] = 'Extension date must be after the allow submissions from date'; $string['fixrescalednullgrades'] = 'This assignment contains some erroneous grades. You can automatically fix these grades. This may affect course totals.'; $string['fixrescalednullgradesconfirm'] = 'Are you sure you want to fix erroneous grades? All affected grades will be removed. This may affect course totals.'; $string['fixrescalednullgradesdone'] = 'Grades fixed.'; $string['gradeactions'] = 'Grade actions'; $string['gradecanbechanged'] = 'Grade can be changed'; $string['gradeitem:submissions'] = 'Submissions'; $string['gradersubmissionupdatedtext'] = '{$a->username} has updated their assignment submission for \'{$a->assignment}\' at {$a->timeupdated} It is available here: {$a->url}'; $string['gradersubmissionupdatedhtml'] = '{$a->username} has updated their assignment submission for \'{$a->assignment}\' at {$a->timeupdated}. It is available in assignment submissions.'; $string['gradersubmissionupdatedsmall'] = '{$a->username} has updated their submission for assignment {$a->assignment}.'; $string['gradersubmissionupdatedsms'] = '{$a->username} updated their submission for assignment \'{$a->assignment}\' in \'{$a->coursefullname}\'. Log in to view'; $string['gradeuser'] = 'Grade {$a}'; $string['grantextension'] = 'Grant extension'; $string['grantextensionforusers'] = 'Grant extension for {$a} students'; $string['groupsubmissionsettings'] = 'Group submission settings'; $string['errornosubmissions'] = 'There are no submissions to download'; $string['errorquickgradingvsadvancedgrading'] = 'The grades were not saved because this assignment is currently using advanced grading'; $string['errorrecordmodified'] = 'The grades were not saved because someone has modified one or more records more recently than when you loaded the page.'; $string['feedback'] = 'Feedback'; $string['feedbackavailabletext'] = '{$a->username} has posted some feedback on your assignment submission for \'{$a->assignment}\' You can see it appended to your assignment submission: {$a->url}'; $string['feedbackavailablehtml'] = '{$a->username} has posted some feedback on your assignment submission for \'{$a->assignment}\'. You can see it appended to your assignment submission.'; $string['feedbackavailablesmall'] = '{$a->username} has given feedback for assignment {$a->assignment}'; $string['feedbackavailableanontext'] = 'You have new feedback on your assignment submission for \'{$a->assignment}\' You can see it appended to your assignment submission: {$a->url}'; $string['feedbackavailableanonhtml'] = 'You have new feedback on your assignment submission for \'{$a->assignment}\'. You can see it appended to your assignment submission.'; $string['feedbackavailableanonsmall'] = 'New feedback for assignment {$a->assignment}'; $string['feedbackavailableanonsms'] = 'New feedback on \'{$a->assignment}\' in \'{$a->coursefullname}\'. Log in to view'; $string['feedbackavailablesms'] = 'New feedback on \'{$a->assignment}\' in \'{$a->coursefullname}\'. Log in to view'; $string['feedbackplugins'] = 'Feedback plugins'; $string['feedbackpluginforgradebook'] = 'Feedback plugin that will push comments to the gradebook'; $string['feedbackpluginforgradebook_help'] = 'Only one assignment feedback plugin can push feedback into the gradebook.'; $string['feedbackplugin'] = 'Feedback plugin'; $string['feedbacksettings'] = 'Feedback settings'; $string['feedbacktypes'] = 'Feedback types'; $string['filesubmissions'] = 'File submissions'; $string['filter'] = 'Filter'; $string['filterall'] = 'All'; $string['filterdraft'] = 'Draft'; $string['filtergraded'] = 'Graded'; $string['filtergrantedextension'] = 'Granted extension'; $string['filternone'] = 'No filter'; $string['filternotsubmitted'] = 'Not submitted'; $string['filterrequiregrading'] = 'Requires grading'; $string['filtersubmitted'] = 'Submitted'; $string['graded'] = 'Graded'; $string['gradedby'] = 'Graded by'; $string['gradedfollowupsubmit'] = 'Graded - resubmitted'; $string['gradedon'] = 'Graded on'; $string['gradebelowzero'] = 'Grade must be greater than or equal to zero.'; $string['gradebreakdown'] = 'Grade breakdown'; $string['gradeabovemaximum'] = 'Grade must be less than or equal to {$a}.'; $string['gradelocked'] = 'This grade is locked or overridden in the gradebook.'; $string['gradeoutof'] = 'Grade out of {$a}'; $string['gradeoutofhelp'] = 'Grade'; $string['gradeoutofhelp_help'] = 'Enter the grade for the student\'s submission here. You may include decimals.'; $string['gradepenalty'] = 'Grade penalties'; $string['gradepenalty_help'] = 'If enabled, penalties will be applied to assignments submitted after the due date.'; $string['gradestudent'] = 'Grade student: (id={$a->id}, fullname={$a->fullname}). '; $string['grading'] = 'Grading'; $string['gradingchangessaved'] = 'The grade changes were saved'; $string['gradingduedate'] = 'Remind me to grade by'; $string['gradingduedate_help'] = 'The expected date that marking of the submissions should be completed by. This date is used to prioritise dashboard notifications for teachers.'; $string['gradingdueduedatevalidation'] = 'Remind me to grade by date cannot be earlier than the due date.'; $string['gradingduefromdatevalidation'] = 'Remind me to grade by date cannot be earlier than the allow submissions from date.'; $string['gradechangessaveddetail'] = 'The changes to the grade and feedback were saved'; $string['gradingmethodpreview'] = 'Grading criteria'; $string['gradingstatus'] = 'Grading status'; $string['gradingstudent'] = 'Grading student'; $string['gradingsummary'] = 'Grading summary'; $string['groupoverrides'] = 'Group overrides'; $string['groupsnone'] = 'There are no groups in this course.'; $string['hidegrader'] = 'Hide grader identity from students'; $string['hidegrader_help'] = 'If enabled, the identity of any user who grades an assignment submission is not shown, so students can\'t see who marked their work. Note that this setting has no effect on the comments box on the grading page.'; $string['hideshow'] = 'Hide/Show'; $string['hiddenuser'] = 'Participant '; $string['inactiveoverridehelp'] = '* This override is inactive because the user\'s access to the activity is restricted. This can be due to group or role assignments, other access restrictions, or the activity being hidden.'; $string['includesuspendedparticipants'] = 'Include suspended participants'; $string['indicator:cognitivedepth'] = 'Assignment cognitive'; $string['indicator:cognitivedepth_help'] = 'This indicator is based on the cognitive depth reached by the student in an assignment activity.'; $string['indicator:cognitivedepthdef'] = 'Assignment cognitive'; $string['indicator:cognitivedepthdef_help'] = 'The participant has reached this percentage of the cognitive engagement offered by the Assignment activities during this analysis interval (Levels = No view, View, Submit, View feedback, Comment on feedback, Resubmit after viewing feedback)'; $string['indicator:cognitivedepthdef_link'] = 'Learning_analytics_indicators#Cognitive_depth'; $string['indicator:socialbreadth'] = 'Assignment social'; $string['indicator:socialbreadth_help'] = 'This indicator is based on the social breadth reached by the student in an assignment activity.'; $string['indicator:socialbreadthdef'] = 'Assignment social'; $string['indicator:socialbreadthdef_help'] = 'The participant has reached this percentage of the social engagement offered by the Assignment activities during this analysis interval (Levels = No participation, Participant alone, Participant with others)'; $string['indicator:socialbreadthdef_link'] = 'Learning_analytics_indicators#Social_breadth'; $string['instructionfiles'] = 'Instruction files'; $string['introattachments'] = 'Additional files'; $string['introattachments_help'] = 'Additional files for use in the assignment, such as answer templates, may be added.'; $string['invalidgradeforscale'] = 'The grade supplied was not valid for the current scale'; $string['invalidfloatforgrade'] = 'The grade provided could not be understood: {$a}'; $string['invalidoverrideid'] = 'Invalid override id'; $string['lastmodifiedsubmission'] = 'Last modified (submission)'; $string['lastmodifiedgrade'] = 'Last modified (grade)'; $string['latesubmissions'] = 'Late submissions'; $string['latesubmissionsaccepted'] = 'Allowed until {$a}'; $string['loading'] = 'Loading...'; $string['locksubmissionforstudent'] = 'Prevent any more submissions for student: (id={$a->id}, fullname={$a->fullname}).'; $string['locksubmissions'] = 'Lock submissions'; $string['manageassignfeedbackplugins'] = 'Manage assignment feedback plugins'; $string['manageassignsubmissionplugins'] = 'Manage assignment submission plugins'; $string['marker'] = 'Marker'; $string['markerfilter'] = 'Marker filter'; $string['markerfilternomarker'] = 'No marker'; $string['markingallocation'] = 'Use marking allocation'; $string['markingallocation_help'] = 'If enabled together with marking workflow, markers can be allocated to particular students.'; $string['markinganonymous'] = 'Allow partial release of grades while marking anonymously'; $string['markinganonymous_help'] = 'If enabled together with anonymous submissions and marking workflow, it allows partial release of grades while marking anonymously.'; $string['markingstate'] = 'Marking state'; $string['markingworkflow'] = 'Use marking workflow'; $string['markingworkflow_help'] = 'If enabled, marks will go through a series of workflow stages before being released to students. This allows for multiple rounds of marking and allows marks to be released to all students at the same time.'; $string['markingworkflowstate'] = 'Marking workflow state'; $string['markingworkflowstate_help'] = 'Possible workflow states may include (depending on your permissions): * Not marked - the marker has not yet started * In marking - the marker has started but not yet finished * Marking completed - the marker has finished but might need to go back for checking/corrections * In review - the marking is now with the teacher in charge for quality checking * Ready for release - the teacher in charge is satisfied with the marking but may wait before giving students access to the marking * Released - the student can access the grades/feedback'; $string['markingworkflowstateinmarking'] = 'In marking'; $string['markingworkflowstateinreview'] = 'In review'; $string['markingworkflowstatenotmarked'] = 'Not marked'; $string['markingworkflowstatereadyforreview'] = 'Marking completed'; $string['markingworkflowstatereadyforrelease'] = 'Ready for release'; $string['markingworkflowstatereleased'] = 'Released'; $string['maxattempts'] = 'Allowed attempts'; $string['maxattempts_help'] = 'The maximum number of submission attempts that can be made by a student. After this number has been reached, the submission can no longer be reopened.'; $string['maxgrade'] = 'Maximum grade'; $string['maxperpage'] = 'Maximum assignments per page'; $string['maxperpage_help'] = 'The maximum number of assignments a grader can show in the assignment grading page. This setting is useful in preventing timeouts for courses with a large number of participants.'; $string['messageprovider:assign_due_digest'] = 'Assignments due in 7 days notification'; $string['messageprovider:assign_due_soon'] = 'Assignment due soon notification'; $string['messageprovider:assign_overdue'] = 'Assignment overdue notification'; $string['messageprovider:assign_notification'] = 'Assignment notifications'; $string['modulename'] = 'Assignment'; $string['modulename_help'] = 'The assignment activity module enables a teacher to communicate tasks, collect work and provide grades and feedback. Students can submit any digital content (files), such as word-processed documents, spreadsheets, images, or audio and video clips. Alternatively, or in addition, the assignment may require students to type text directly into the text editor. An assignment can also be used to remind students of \'real-world\' assignments they need to complete offline, such as art work, and thus not require any digital content. Students can submit work individually or as a member of a group. When reviewing assignments, teachers can leave feedback comments and upload files, such as marked-up student submissions, documents with comments or spoken audio feedback. Assignments can be graded using a numerical or custom scale or an advanced grading method such as a rubric. Final grades are recorded in the gradebook.'; $string['modulename_link'] = 'mod/assignment/view'; $string['modulenameplural'] = 'Assignments'; $string['moreusers'] = '{$a} more...'; $string['multipleteams'] = 'Member of more than one group'; $string['multipleteams_desc'] = 'The assignment requires submission in groups. You are a member of more than one group. To be able to submit you must be a member of only one group. Please contact your teacher to change your group membership.'; $string['multipleteamsgrader'] = 'Member of more than one group, so unable to make submissions.'; $string['nextuser'] = "Next user"; $string['newsubmissions'] = 'Assignments submitted'; $string['noattempt'] = 'No attempt'; $string['noclose'] = 'No close date'; $string['nofilters'] = 'No filters'; $string['nofiles'] = 'No files. '; $string['nograde'] = 'No grade. '; $string['nomoresubmissionsaccepted'] = 'Only allowed for participants who have been granted an extension'; $string['none'] = 'None'; $string['noonlinesubmissions'] = 'This assignment does not require you to submit anything online'; $string['noopen'] = 'No open date'; $string['nooverridedata'] = 'You must override at least one of the assignment settings.'; $string['nogroupoverrides'] = 'There are currently no group overrides.'; $string['nouseroverrides'] = 'There are currently no user overrides.'; $string['nosavebutnext'] = 'Next'; $string['nosubmission'] = 'Nothing has been submitted for this assignment'; $string['nosubmissionyet'] = 'No submissions have been made yet'; $string['noteam'] = 'Not a member of any group'; $string['noteam_desc'] = 'This assignment requires submission in groups. You are not a member of any group, so you cannot create a submission. Please contact your teacher to be added to a group.'; $string['noteamgrader'] = 'Not a member of any group, so unable to make submissions.'; $string['notgraded'] = 'Not graded'; $string['notgradedyet'] = 'Not graded yet'; $string['notifications'] = 'Notifications'; $string['notifystudents'] = 'Notify students'; $string['nousersselected'] = 'No users selected'; $string['nousers'] = 'No users'; $string['numberofdraftsubmissions'] = 'Drafts'; $string['numberofparticipants'] = 'Participants'; $string['numberofsubmittedassignments'] = 'Submitted'; $string['numberofsubmissionsneedgrading'] = 'Needs grading'; $string['numberofsubmissionsneedgradinglabel'] = 'Needs grading: {$a}'; $string['numberofteams'] = 'Groups'; $string['offline'] = 'No online submissions required'; $string['open'] = 'Open'; $string['opensubmissionexists'] = 'Open assignment submission already exists.'; $string['outof'] = '{$a->current} out of {$a->total}'; $string['overdue'] = 'Assignment is overdue by: {$a}'; $string['override'] = 'Override'; $string['overridedeletegroupsure'] = 'Are you sure you want to delete the override for group {$a}?'; $string['overridedeleteusersure'] = 'Are you sure you want to delete the override for user {$a}?'; $string['overridegroup'] = 'Override group'; $string['overridegroupeventname'] = '{$a->assign} - {$a->group}'; $string['overriderecalculatepenalty'] = 'Recalculate penalty for user(s) in the override'; $string['overrides'] = 'Overrides'; $string['overrideuser'] = 'Override user'; $string['overrideusereventname'] = '{$a->assign} - Override'; $string['outlinegrade'] = 'Grade: {$a}'; $string['page-mod-assign-x'] = 'Any assignment module page'; $string['page-mod-assign-view'] = 'Assignment module main and submission page'; $string['paramtimeremaining'] = '{$a} remaining'; $string['participant'] = 'Participant'; $string['penaltyduedatechangemessage'] = 'Some grades have already been awarded. In order to change the due date, disable/enable penalty, you must first choose whether or not to recalculate existing grades.'; $string['pluginadministration'] = 'Assignment administration'; $string['pluginname'] = 'Assignment'; $string['preventsubmissionnotingroup'] = 'Require group to make submission'; $string['preventsubmissionnotingroup_help'] = 'If enabled, users who are not members of a group will be unable to make submissions.'; $string['preventsubmissions'] = 'Prevent the user from making any more submissions to this assignment.'; $string['preventsubmissionsshort'] = 'Prevent submission changes'; $string['previous'] = 'Previous'; $string['previoususer'] = 'Previous user'; $string['privacy:attemptpath'] = 'attempt {$a}'; $string['privacy:blindmarkingidentifier'] = 'The identifier used for anonymous submissions'; $string['privacy:gradepath'] = 'grade'; $string['privacy:metadata:assigndownloadasfolders'] = 'A user preference for whether multiple file submissions should be downloaded into folders'; $string['privacy:metadata:assignfeedbackpluginsummary'] = 'Feedback data for the assignment.'; $string['privacy:metadata:assignfilter'] = 'Filter options such as \'Submitted\', \'Not submitted\', \'Requires grading\', and \'Granted extension\''; $string['privacy:metadata:assigngrades'] = 'Stores user grades for the assignment'; $string['privacy:metadata:assignmarkerfilter'] = 'Filter the assign summary by the assigned marker.'; $string['privacy:metadata:assignmentid'] = 'Assignment ID'; $string['privacy:metadata:assignmessageexplanation'] = 'Messages are sent to students through the messaging system.'; $string['privacy:metadata:assignoverrides'] = 'Stores override information for the assignment'; $string['privacy:metadata:assignperpage'] = 'Number of assignments shown per page.'; $string['privacy:metadata:assignquickgrading'] = 'A preference as to whether quick grading is used or not.'; $string['privacy:metadata:assignsubmissiondetail'] = 'Stores user submission information'; $string['privacy:metadata:assignsubmissionpluginsummary'] = 'Submission data for the assignment.'; $string['privacy:metadata:assignuserflags'] = 'Stores user meta data such as extension dates'; $string['privacy:metadata:assignusermapping'] = 'The mapping for anonymous submissions'; $string['privacy:metadata:assignworkflowfilter'] = 'Filter by the different workflow stages.'; $string['privacy:metadata:grade'] = 'The numerical grade for this assignment submission. Can be determined by scales/advancedgradingforms etc but will always be converted back to a floating point number.'; $string['privacy:metadata:grader'] = 'The user ID of the person grading.'; $string['privacy:metadata:groupid'] = 'Group ID that the user is a member of.'; $string['privacy:metadata:latest'] = 'Greatly simplifies queries wanting to know information about only the latest attempt.'; $string['privacy:metadata:mailed'] = 'Has this user been mailed yet?'; $string['privacy:metadata:timecreated'] = 'Time created'; $string['privacy:metadata:timestarted'] = 'Time started'; $string['privacy:metadata:userid'] = 'ID of the user'; $string['privacy:studentpath'] = 'studentsubmissions'; $string['quickgrading'] = 'Quick grading'; $string['quickgradingresult'] = 'Quick grading'; $string['quickgradingchangessaved'] = 'The grade changes were saved'; $string['quickgrading_help'] = 'Quick grading allows you to assign grades (and outcomes) directly in the submissions table. Quick grading is not compatible with advanced grading and is not recommended when there are multiple markers.'; $string['relativedatessubmissiontimeleft'] = 'Calculated for each student'; $string['removeallgroupoverrides'] = 'All group overrides'; $string['removealluseroverrides'] = 'All user overrides'; $string['reopenuntilpassincompatiblewithblindmarking'] = 'Reopen until pass option is incompatible with anonymous submissions, because the grades are not released to the gradebook until the student identities are revealed.'; $string['requiresubmissionstatement'] = 'Require that students accept the submission statement'; $string['requiresubmissionstatement_help'] = 'Require that students accept the submission statement for all submissions to this assignment.'; $string['requireallteammemberssubmit'] = 'Require all group members submit'; $string['requireallteammemberssubmit_help'] = 'This setting must be used together with the \'Require students to click the submit button\' in Submission settings. If enabled, all group members must click the submit button for the group submission to be considered as submitted. If disabled, any group member can click the submit button.'; $string['recordid'] = 'Identifier'; $string['removesubmission'] = 'Remove submission'; $string['removesubmissionforstudent'] = 'Remove submission for student: (id={$a->id}, fullname={$a->fullname}).'; $string['removesubmissionconfirm'] = 'Are you sure you want to remove your submission?'; $string['removesubmissionconfirmforstudent'] = 'Are you sure you want to remove the submission for {$a}?'; $string['removesubmissionconfirmforstudentwithtimelimit'] = 'Are you sure you want to remove the submission for {$a}? Please note that this will not reset the student\'s time limit. You can give more time by adding a time limit user override.'; $string['removesubmissionconfirmwithtimelimit'] = 'Are you sure you want to remove your submission? Please note that this will not reset your time limit.'; $string['revealidentities'] = 'Reveal student identities'; $string['revealidentitiesconfirm'] = 'Are you sure you want to reveal student identities for this assignment? This operation cannot be undone. Once the student identities have been revealed, the marks will be released to the gradebook.'; $string['reverttodefaults'] = 'Revert to assignment defaults'; $string['reverttodraftforgroup'] = 'Revert submission to draft for group {$a}.'; $string['reverttodraftforstudent'] = 'Revert submission to draft for student: (id={$a->id}, fullname={$a->fullname}).'; $string['reverttodraft'] = 'Revert the submission to draft status'; $string['reverttodraftshort'] = 'Revert the submission to draft'; $string['reviewed'] = 'Reviewed'; $string['save'] = 'Save'; $string['saveandcontinue'] = 'Save and continue'; $string['savechanges'] = 'Save changes'; $string['savegradingresult'] = 'Grade'; $string['savenext'] = 'Save and show next'; $string['savingchanges'] = 'Saving changes...'; $string['saveoverrideandstay'] = 'Save and enter another override'; $string['scale'] = 'Scale'; $string['search:activity'] = 'Assignment - activity information'; $string['sendstudentnotificationsdefault'] = 'Default for \'Notify student\''; $string['sendstudentnotificationsdefault_help'] = 'When grading each student, should \'Notify student\' be ticked by default?'; $string['sendstudentnotifications'] = 'Notify student'; $string['sendstudentnotifications_help'] = 'Tick this box to send a notification about the updated grade or feedback. If the assignment uses a marking workflow, or the grades are hidden in the grader report, then the notification will not be sent until the grade is released.'; $string['sendnotifications'] = 'Notify graders about submissions'; $string['sendnotifications_help'] = 'If enabled, graders (usually teachers) receive a message whenever a student submits an assignment, early, on time and late. Message methods are configurable.'; $string['selectlink'] = 'Select...'; $string['selectuser'] = 'Select {$a}'; $string['sendlatenotifications'] = 'Notify graders about late submissions'; $string['sendlatenotifications_help'] = 'If enabled, graders (usually teachers) receive a message whenever a student submits an assignment late. Message methods are configurable.'; $string['sendnotificationduedatesoon'] = 'Notify user of an approaching assignment due date'; $string['sendnotificationoverdue'] = 'Notify user of an assignment that is overdue'; $string['sendnotificationduedigest'] = 'Notify user of assignments due in 7 days'; $string['sendsubmissionreceipts'] = 'Send submission receipt to students'; $string['sendsubmissionreceipts_help'] = 'This switch enables submission receipts for students. Students will receive a notification every time they successfully submit an assignment.'; $string['setmarkingallocation'] = 'Set allocated marker'; $string['setmarkingworkflowstate'] = 'Set marking workflow state'; $string['selectedusers'] = 'Selected users'; $string['setmarkingworkflowstateforlog'] = 'Set marking workflow state : (id={$a->id}, fullname={$a->fullname}, state={$a->state}). '; $string['setmarkerallocationforlog'] = 'Set marking allocation : (id={$a->id}, fullname={$a->fullname}, marker={$a->marker}). '; $string['settings'] = 'Assignment settings'; $string['showrecentsubmissions'] = 'Show recent submissions'; $string['status'] = 'Status'; $string['studentnotificationworkflowstateerror'] = 'Marking workflow state must be \'Released\' to notify students.'; $string['submissionactions'] = 'Submission actions'; $string['submissionattachments'] = 'Only show files during submission'; $string['submissionattachments_help'] = 'Tick the box to only show files on the submission page. Otherwise, files will be shown on both the assignment and submission pages.'; $string['confirmstart'] = 'You have {$a} to complete this assignment. When you begin, the timer will start to count down and can\'t be paused.'; $string['submissioncontains'] = 'Your submission contains:'; $string['submissioncopiedtext'] = 'You have made a copy of your previous assignment submission for \'{$a->assignment}\' You can see the status of your assignment submission: {$a->url}'; $string['submissioncopiedhtml'] = '

You have made a copy of your previous assignment submission for \'{$a->assignment}\'.

You can see the status of your assignment submission.

'; $string['submissioncopiedsmall'] = 'You have copied your previous assignment submission for {$a->assignment}'; $string['submissioncopiedsms'] = 'You have made a copy of your previous submission for \'{$a->assignment}\' in \'{$a->coursefullname}\'. Log in to view'; $string['submissiondrafts'] = 'Require students to click the submit button'; $string['submissiondrafts_help'] = 'Should students have a submit button to indicate when they have finished making changes to their submission and it is ready for grading?'; $string['submissioneditable'] = 'Student can edit this submission'; $string['submissionlog'] = 'Student: {$a->fullname}, Status: {$a->status}'; $string['submissionnotcopiedinvalidstatus'] = 'The submission was not copied because it has been edited since it was reopened.'; $string['submissionnoteditable'] = 'Student cannot edit this submission'; $string['submissionnotopen'] = 'This assignment is not open for submissions'; $string['submissionnotready'] = 'This assignment is not ready to submit:'; $string['privacy:submissionpath'] = 'submission'; $string['submissionplugins'] = 'Submission plugins'; $string['submissionreceiptcontains'] = 'Submitted content ({$a->total} items):'; $string['submissionreceipthtml'] = '

Your assignment submission for \'{$a->assignment}\' has been submitted.

You can view your submission and check its status on the assignment page.

{$a->submissionsummaryhtml}'; $string['submissionreceiptothertext'] = 'Your assignment submission for \'{$a->assignment}\' has been submitted. You can see the status of your assignment submission: {$a->url}'; $string['submissionreceiptotherhtml'] = 'Your assignment submission for \'{$a->assignment}\' has been submitted. You can see the status of your assignment submission.'; $string['submissionreceiptothersmall'] = 'Your assignment submission for {$a->assignment} has been submitted.'; $string['submissionreceiptothersms'] = 'Submission made for \'{$a->assignment}\' in \'{$a->coursefullname}\'. Log in to view your submission status'; $string['submissionreceipts'] = 'Send submission receipts'; $string['submissionreceiptsmall'] = 'Assignment submission confirmation - {$a->assignment}'; $string['submissionreceiptsms'] = 'You have submitted \'{$a->assignment}\' in \'{$a->coursefullname}\'. Log in to view the submission status'; $string['submissionreceipttext'] = 'You have submitted an assignment submission for \'{$a->assignment}\'. You can see the status of your assignment submission: {$a->url} {$a->submissionsummarytext}'; $string['submissions'] = 'Submissions'; $string['submissionslocked'] = 'This assignment is not accepting submissions'; $string['submissionslockedshort'] = 'Submission changes not allowed'; $string['submissionsclosed'] = 'Submissions closed'; $string['submissionsettings'] = 'Submission settings'; $string['submissionstatement'] = 'Submission statement'; $string['submissionstatementdefault'] = 'This submission is my own work, except where I have acknowledged the use of the works of other people.'; $string['submissionstatement_help'] = 'Statement that each student must accept in order to submit their work.'; $string['submissionstatementrequired'] = 'You are required to agree to this statement before you can submit.'; $string['submissionstatementteamsubmission'] = 'Group submission statement'; $string['submissionstatementteamsubmissiondefault'] = 'This submission is the work of my group, except where we have acknowledged the use of the works of other people.'; $string['submissionstatementteamsubmission_help'] = 'Statement that each student must accept in order to submit the work of their group.'; $string['submissionstatementteamsubmissionallsubmit'] = 'Group submission statement where all group members submit'; $string['submissionstatementteamsubmissionallsubmitdefault'] = 'This submission is my own work as a group member, except where I have acknowledged the use of the works of other people.'; $string['submissionstatementteamsubmissionallsubmit_help'] = 'Statement that each student must accept in order to submit their work as a group member.'; $string['submissionstatementacceptedlog'] = 'Submission statement accepted by user {$a}'; $string['submissionstatus_draft'] = 'Draft (not submitted)'; $string['submissionstatusheading'] = 'Submission status'; $string['submissionstatus_marked'] = 'Graded'; $string['submissionstatus_new'] = 'No submission'; $string['submissionstatus_reopened'] = 'Reopened'; $string['submissionstatus_'] = 'No submission'; $string['submissionstatus'] = 'Submission status'; $string['submissionstatus_submitted'] = 'Submitted for grading'; $string['submissionsummary'] = '{$a->status}. Last modified on {$a->timemodified}'; $string['submissionteam'] = 'Group'; $string['submissiontypes'] = 'Submission types'; $string['submission'] = 'Submission'; $string['submitaction'] = 'Submit'; $string['submitforgrading'] = 'Submit for grading'; $string['submitassignment_help'] = 'Once this assignment is submitted you will not be able to make any more changes.'; $string['submitassignment'] = 'Submit assignment'; $string['submittedearly'] = 'Assignment was submitted {$a} early'; $string['submittedlate'] = 'Assignment was submitted {$a} late'; $string['submittedovertime'] = 'Assignment was submitted {$a} over the time limit'; $string['submittedundertime'] = 'Assignment was submitted {$a} under the time limit'; $string['submittedlateshort'] = '{$a} late'; $string['submitted'] = 'Submitted'; $string['subpagetitle'] = '{$a->contextname} - {$a->subpage}'; $string['subplugintype_assignsubmission'] = 'Submission plugin'; $string['subplugintype_assignsubmission_plural'] = 'Submission plugins'; $string['subplugintype_assignfeedback'] = 'Feedback plugin'; $string['subplugintype_assignfeedback_plural'] = 'Feedback plugins'; $string['teamname'] = 'Group: {$a}'; $string['teamsubmission'] = 'Students submit in groups'; $string['teamsubmission_help'] = 'If enabled, students will be divided into groups based on the default set of groups or a custom grouping. A group submission will be shared among group members and all members of the group will see each others\' changes to the submission. Note: You cannot change this setting if there are submissions already.'; $string['teamsubmissiongroupingid'] = 'Grouping for student groups'; $string['teamsubmissiongroupingid_help'] = 'This is the grouping that the assignment will use to find groups for student groups. If not set, the default set of groups will be used.'; $string['textinstructions'] = 'Assignment instructions'; $string['timelimit'] = 'Time limit'; $string['timelimit_help'] = 'If enabled, the time limit is stated on the assignment page and a countdown timer is displayed during the assignment.'; $string['timelimitnotenabled'] = 'Time limit is not enabled for assignment.'; $string['timelimitpassed'] = 'The time limit has expired'; $string['timemodified'] = 'Last modified'; $string['timeremaining'] = 'Time remaining'; $string['timeremainingcolon'] = 'Time remaining: {$a}'; $string['togglezoom'] = 'Zoom in/out of region'; $string['ungroupedusers'] = 'The setting \'Require group to make submission\' is enabled and some users are either not a member of any group, or are a member of more than one group, so are unable to make submissions.'; $string['ungroupedusersoptional'] = 'The setting \'Students submit in groups\' is enabled and some users are either not a member of any group, or are a member of more than one group. Please be aware that these students will submit as members of the \'Default group\'.'; $string['unlocksubmissionforstudent'] = 'Allow submissions for student: (id={$a->id}, fullname={$a->fullname}).'; $string['unlocksubmissions'] = 'Unlock submissions'; $string['unlimitedattempts'] = 'Unlimited'; $string['unlimitedattemptsallowed'] = 'Unlimited attempts allowed.'; $string['unlimitedpages'] = 'Unlimited'; $string['unsavedchanges'] = 'Unsaved changes'; $string['unsavedchangesquestion'] = 'There are unsaved changes to grades or feedback. Do you want to save the changes and continue?'; $string['updatetable'] = 'Save and update table'; $string['upgradenotimplemented'] = 'Upgrade not implemented in plugin ({$a->type} {$a->subtype})'; $string['userextensiondate'] = 'Extension granted until: {$a}'; $string['userassignmentdefaults'] = 'User assignment defaults'; $string['useridlistnotcached'] = 'The grade changes were NOT saved, as it was not possible to determine which submission they were for.'; $string['useroverrides'] = 'User overrides'; $string['usersnone'] = 'There are no users enrolled in this course or no users can access the assignment.'; $string['usersubmissioncannotberemoved'] = 'The submission of {$a} cannot be removed.'; $string['userswhoneedtosubmit'] = 'Users who need to submit: {$a}'; $string['usergrade'] = 'User grade'; $string['validmarkingworkflowstates'] = 'Valid marking workflow states'; $string['viewadifferentattempt'] = 'View a different attempt'; $string['viewbatchsetmarkingworkflowstate'] = 'View batch set marking workflow state page.'; $string['viewbatchmarkingallocation'] = 'View batch set marking allocation page.'; $string['viewfeedback'] = 'View feedback'; $string['viewfeedbackforuser'] = 'View feedback for user: {$a}'; $string['viewfullgradingpage'] = 'Open the full grading page to provide feedback'; $string['viewgradebook'] = 'View gradebook'; $string['viewgradingformforstudent'] = 'View grading page for student: (id={$a->id}, fullname={$a->fullname}).'; $string['viewgrading'] = 'View all submissions'; $string['viewownsubmissionform'] = 'View own submit assignment page.'; $string['viewownsubmissionstatus'] = 'View own submission status page.'; $string['viewsubmissionforuser'] = 'View submission for user: {$a}'; $string['viewsubmission'] = 'View submission'; $string['viewfull'] = 'View full'; $string['viewsummary'] = 'View summary'; $string['viewsubmissiongradingtable'] = 'View submission grading table.'; $string['viewrevealidentitiesconfirm'] = 'View reveal student identities confirmation page.'; $string['workflowfilter'] = 'Workflow filter'; $string['xofy'] = '{$a->x} of {$a->y}'; // Deprecated since Moodle 4.5. $string['attemptreopenmethod_none'] = 'Never'; $string['choosegradingaction'] = 'Grading action'; $string['downloadasfolders_help'] = 'Assignment submissions may be downloaded in folders. Each submission is then put in a separate folder, with the folder structure kept for any subfolders, and files are not renamed.'; $string['editaction'] = 'Actions...'; $string['gradingoptions'] = 'Options'; $string['groupoverridesdeleted'] = 'Group overrides deleted'; $string['saveallquickgradingchanges'] = 'Save all quick grading changes'; $string['updategrade'] = 'Update grade'; $string['useroverridesdeleted'] = 'User overrides deleted'; $string['viewgrader'] = 'View Grader'; backup/moodle2/backup_assign_stepslib.php000064400000024512152265517350014620 0ustar00. /** * Define all the backup steps that will be used by the backup_assign_activity_task * * @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(); require_once($CFG->dirroot . '/mod/assign/locallib.php'); /** * Define the complete choice structure for backup, with file and id annotations * * @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 backup_assign_activity_structure_step extends backup_activity_structure_step { /** * Annotate files from plugin configuration * @param backup_nested_element $assign the backup structure of the activity * @param string $subtype the plugin type to handle * @return void */ protected function annotate_plugin_config_files(backup_nested_element $assign, $subtype) { $dummyassign = new assign(null, null, null); $plugins = $dummyassign->load_plugins($subtype); foreach ($plugins as $plugin) { $component = $plugin->get_subtype() . '_' . $plugin->get_type(); $areas = $plugin->get_config_file_areas(); foreach ($areas as $area) { $assign->annotate_files($component, $area, null); } } } /** * Define the structure for the assign activity * @return backup_nested_element */ protected function define_structure() { // To know if we are including userinfo. $userinfo = $this->get_setting_value('userinfo'); $groupinfo = $this->get_setting_value('groups'); // Define each element separated. $assign = new backup_nested_element('assign', array('id'), array('name', 'intro', 'introformat', 'alwaysshowdescription', 'submissiondrafts', 'sendnotifications', 'sendlatenotifications', 'sendstudentnotifications', 'duedate', 'cutoffdate', 'gradingduedate', 'allowsubmissionsfromdate', 'grade', 'timemodified', 'completionsubmit', 'requiresubmissionstatement', 'teamsubmission', 'requireallteammemberssubmit', 'teamsubmissiongroupingid', 'blindmarking', 'hidegrader', 'revealidentities', 'attemptreopenmethod', 'maxattempts', 'markingworkflow', 'markingallocation', 'markinganonymous', 'preventsubmissionnotingroup', 'activity', 'activityformat', 'timelimit', 'submissionattachments', 'gradepenalty')); $userflags = new backup_nested_element('userflags'); $userflag = new backup_nested_element('userflag', array('id'), array('userid', 'assignment', 'mailed', 'locked', 'extensionduedate', 'workflowstate', 'allocatedmarker')); $submissions = new backup_nested_element('submissions'); $submission = new backup_nested_element('submission', array('id'), array('userid', 'timecreated', 'timemodified', 'timestarted', 'status', 'groupid', 'attemptnumber', 'latest')); $grades = new backup_nested_element('grades'); $grade = new backup_nested_element('grade', array('id'), array('userid', 'timecreated', 'timemodified', 'grader', 'grade', 'penalty', 'attemptnumber')); $pluginconfigs = new backup_nested_element('plugin_configs'); $pluginconfig = new backup_nested_element('plugin_config', array('id'), array('plugin', 'subtype', 'name', 'value')); $overrides = new backup_nested_element('overrides'); $override = new backup_nested_element('override', array('id'), array( 'groupid', 'userid', 'sortorder', 'allowsubmissionsfromdate', 'duedate', 'cutoffdate', 'timelimit')); // Build the tree. $assign->add_child($userflags); $userflags->add_child($userflag); $assign->add_child($submissions); $submissions->add_child($submission); $assign->add_child($grades); $grades->add_child($grade); $assign->add_child($pluginconfigs); $pluginconfigs->add_child($pluginconfig); $assign->add_child($overrides); $overrides->add_child($override); // Define sources. $assign->set_source_table('assign', array('id' => backup::VAR_ACTIVITYID)); $pluginconfig->set_source_table('assign_plugin_config', array('assignment' => backup::VAR_PARENTID)); // Assign overrides to backup are different depending of user info. $overrideparams = array('assignid' => backup::VAR_PARENTID); if ($userinfo) { $userflag->set_source_table('assign_user_flags', array('assignment' => backup::VAR_PARENTID)); $submissionparams = array('assignment' => backup::VAR_PARENTID); if (!$groupinfo) { // Without group info, skip group submissions. $submissionparams['groupid'] = backup_helper::is_sqlparam(0); } $submission->set_source_table('assign_submission', $submissionparams); $grade->set_source_table('assign_grades', array('assignment' => backup::VAR_PARENTID)); // Support 2 types of subplugins. $this->add_subplugin_structure('assignsubmission', $submission, true); $this->add_subplugin_structure('assignfeedback', $grade, true); } else { $overrideparams['userid'] = backup_helper::is_sqlparam(null); // Without userinfo, skip user overrides. } if (!$groupinfo) { // Without group info, skip group overrides. $overrideparams['groupid'] = backup_helper::is_sqlparam(0); } $override->set_source_table('assign_overrides', $overrideparams); // Define id annotations. $userflag->annotate_ids('user', 'userid'); $userflag->annotate_ids('user', 'allocatedmarker'); $submission->annotate_ids('user', 'userid'); $submission->annotate_ids('group', 'groupid'); $grade->annotate_ids('user', 'userid'); $grade->annotate_ids('user', 'grader'); $assign->annotate_ids('grouping', 'teamsubmissiongroupingid'); $override->annotate_ids('user', 'userid'); $override->annotate_ids('group', 'groupid'); // Define file annotations. // These file areas don't have an itemid. $assign->annotate_files('mod_assign', 'intro', null); $assign->annotate_files('mod_assign', 'introattachment', null); $assign->annotate_files('mod_assign', 'activityattachment', null); $this->annotate_plugin_config_files($assign, 'assignsubmission'); $this->annotate_plugin_config_files($assign, 'assignfeedback'); // Return the root element (choice), wrapped into standard activity structure. return $this->prepare_activity_structure($assign); } } backup/moodle2/restore_assign_activity_task.class.php000064400000011154152265517350017171 0ustar00. /** * Define all the backup steps that will be used by the backup_assign_activity_task * * @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(); require_once($CFG->dirroot . '/mod/assign/backup/moodle2/restore_assign_stepslib.php'); /** * assign restore task that provides all the settings and steps to perform one complete restore of the activity * * @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 restore_assign_activity_task extends restore_activity_task { /** * Define (add) particular settings this activity can have. */ protected function define_my_settings() { // No particular settings for this activity. } /** * Define (add) particular steps this activity can have. */ protected function define_my_steps() { // Assignment only has one structure step. $this->add_step(new restore_assign_activity_structure_step('assign_structure', 'assign.xml')); } /** * Define the contents in the activity that must be * processed by the link decoder. * * @return array */ public static function define_decode_contents() { $contents = array(); $contents[] = new restore_decode_content('assign', array('intro'), 'assign'); return $contents; } /** * Define the decoding rules for links belonging * to the activity to be executed by the link decoder. * * @return array of restore_decode_rule */ public static function define_decode_rules() { $rules = array(); $rules[] = new restore_decode_rule('ASSIGNVIEWBYID', '/mod/assign/view.php?id=$1', 'course_module'); $rules[] = new restore_decode_rule('ASSIGNINDEX', '/mod/assign/index.php?id=$1', 'course_module'); return $rules; } /** * Define the restore log rules that will be applied * by the {@link restore_logs_processor} when restoring * assign logs. It must return one array * of {@link restore_log_rule} objects. * * @return array of restore_log_rule */ public static function define_restore_log_rules() { $rules = array(); $rules[] = new restore_log_rule('assign', 'add', 'view.php?id={course_module}', '{assign}'); $rules[] = new restore_log_rule('assign', 'update', 'view.php?id={course_module}', '{assign}'); $rules[] = new restore_log_rule('assign', 'view', 'view.php?id={course_module}', '{assign}'); return $rules; } /** * Define the restore log rules that will be applied * by the {@link restore_logs_processor} when restoring * course logs. It must return one array * of {@link restore_log_rule} objects * * Note this rules are applied when restoring course logs * by the restore final task, but are defined here at * activity level. All them are rules not linked to any module instance (cmid = 0) * * @return array */ public static function define_restore_log_rules_for_course() { $rules = array(); return $rules; } /** * Given a comment area, return the itemname that contains the itemid mappings. * * @param string $commentarea * @return string */ public function get_comment_mapping_itemname($commentarea) { switch ($commentarea) { case 'submission_comments': $itemname = 'submission'; break; default: $itemname = parent::get_comment_mapping_itemname($commentarea); break; } return $itemname; } } backup/moodle2/restore_assign_stepslib.php000064400000040577152265517350015047 0ustar00. /** * Define all the restore steps that will be used by the restore_assign_activity_task * * @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(); require_once($CFG->dirroot . '/mod/assign/locallib.php'); /** * Define the complete assignment structure for restore, with file and id annotations * * @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 restore_assign_activity_structure_step extends restore_activity_structure_step { /** * Store whether submission details should be included. Details may not be included if the * this is a team submission, but groups/grouping information was not included in the backup. */ protected $includesubmission = true; /** * Define the structure of the restore workflow. * * @return restore_path_element $structure */ protected function define_structure() { $paths = array(); // To know if we are including userinfo. $userinfo = $this->get_setting_value('userinfo'); // Define each element separated. $paths[] = new restore_path_element('assign', '/activity/assign'); if ($userinfo) { $submission = new restore_path_element('assign_submission', '/activity/assign/submissions/submission'); $paths[] = $submission; $this->add_subplugin_structure('assignsubmission', $submission); $grade = new restore_path_element('assign_grade', '/activity/assign/grades/grade'); $paths[] = $grade; $this->add_subplugin_structure('assignfeedback', $grade); $userflag = new restore_path_element('assign_userflag', '/activity/assign/userflags/userflag'); $paths[] = $userflag; } $paths[] = new restore_path_element('assign_override', '/activity/assign/overrides/override'); $paths[] = new restore_path_element('assign_plugin_config', '/activity/assign/plugin_configs/plugin_config'); return $this->prepare_activity_structure($paths); } /** * Process an assign restore. * * @param object $data The data in object form * @return void */ protected function process_assign($data) { global $DB; $data = (object)$data; $oldid = $data->id; $data->course = $this->get_courseid(); // Any changes to the list of dates that needs to be rolled should be same during course restore and course reset. // See MDL-9367. $data->allowsubmissionsfromdate = $this->apply_date_offset($data->allowsubmissionsfromdate); $data->duedate = $this->apply_date_offset($data->duedate); // If this is a team submission, but there is no group info we need to flag that the submission // information should not be included. It should not be restored. $groupinfo = $this->task->get_setting_value('groups'); if ($data->teamsubmission && !$groupinfo) { $this->includesubmission = false; } // Reset revealidentities if blindmarking with no user data (MDL-43796). $userinfo = $this->get_setting_value('userinfo'); if (!$userinfo && $data->blindmarking) { $data->revealidentities = 0; } if (!empty($data->teamsubmissiongroupingid)) { $data->teamsubmissiongroupingid = $this->get_mappingid('grouping', $data->teamsubmissiongroupingid); } else { $data->teamsubmissiongroupingid = 0; } if (!isset($data->cutoffdate)) { $data->cutoffdate = 0; } if (!isset($data->gradingduedate)) { $data->gradingduedate = 0; } else { $data->gradingduedate = $this->apply_date_offset($data->gradingduedate); } if (!isset($data->markingworkflow)) { $data->markingworkflow = 0; } if (!isset($data->markingallocation)) { $data->markingallocation = 0; } if (!isset($data->markinganonymous)) { $data->markinganonymous = 0; } if (!isset($data->preventsubmissionnotingroup)) { $data->preventsubmissionnotingroup = 0; } if (!empty($data->preventlatesubmissions)) { $data->cutoffdate = $data->duedate; } else { $data->cutoffdate = $this->apply_date_offset($data->cutoffdate); } if ($data->grade < 0) { // Scale found, get mapping. $data->grade = -($this->get_mappingid('scale', abs($data->grade))); } // Grade penalty. if (!isset($data->gradepenalty)) { $data->gradepenalty = 0; } $newitemid = $DB->insert_record('assign', $data); $this->apply_activity_instance($newitemid); } /** * Process a submission restore * @param object $data The data in object form * @return void */ protected function process_assign_submission($data) { global $DB; if (!$this->includesubmission) { return; } $data = (object)$data; $oldid = $data->id; $data->assignment = $this->get_new_parentid('assign'); if ($data->userid > 0) { $data->userid = $this->get_mappingid('user', $data->userid); } if (!empty($data->groupid)) { $data->groupid = $this->get_mappingid('group', $data->groupid); if (!$data->groupid) { // If the group does not exist, then the submission cannot be viewed and restoring can // violate the unique index on the submission table. return; } } else { $data->groupid = 0; } // We will correct this in set_latest_submission_field() once all submissions are restored. $data->latest = 0; $newitemid = $DB->insert_record('assign_submission', $data); // Note - the old contextid is required in order to be able to restore files stored in // sub plugin file areas attached to the submissionid. $this->set_mapping('submission', $oldid, $newitemid, false, null, $this->task->get_old_contextid()); } /** * Process a user_flags restore * @param object $data The data in object form * @return void */ protected function process_assign_userflag($data) { global $DB; $data = (object)$data; $oldid = $data->id; $data->assignment = $this->get_new_parentid('assign'); $data->userid = $this->get_mappingid('user', $data->userid); if (!empty($data->allocatedmarker)) { $data->allocatedmarker = $this->get_mappingid('user', $data->allocatedmarker); } if (!empty($data->extensionduedate)) { $data->extensionduedate = $this->apply_date_offset($data->extensionduedate); } else { $data->extensionduedate = 0; } // Flags mailed and locked need no translation on restore. $newitemid = $DB->insert_record('assign_user_flags', $data); } /** * Process a grade restore * @param object $data The data in object form * @return void */ protected function process_assign_grade($data) { global $DB; $data = (object)$data; $oldid = $data->id; $data->assignment = $this->get_new_parentid('assign'); $data->userid = $this->get_mappingid('user', $data->userid); $data->grader = $this->get_mappingid('user', $data->grader); // Handle flags restore to a different table (for upgrade from old backups). if (!empty($data->extensionduedate) || !empty($data->mailed) || !empty($data->locked)) { $flags = new stdClass(); $flags->assignment = $this->get_new_parentid('assign'); if (!empty($data->extensionduedate)) { $flags->extensionduedate = $this->apply_date_offset($data->extensionduedate); } if (!empty($data->mailed)) { $flags->mailed = $data->mailed; } if (!empty($data->locked)) { $flags->locked = $data->locked; } $flags->userid = $this->get_mappingid('user', $data->userid); $DB->insert_record('assign_user_flags', $flags); } // Fix null grades that were rescaled. if ($data->grade < 0 && $data->grade != ASSIGN_GRADE_NOT_SET) { $data->grade = ASSIGN_GRADE_NOT_SET; } $newitemid = $DB->insert_record('assign_grades', $data); // Note - the old contextid is required in order to be able to restore files stored in // sub plugin file areas attached to the gradeid. $this->set_mapping('grade', $oldid, $newitemid, false, null, $this->task->get_old_contextid()); $this->set_mapping(restore_gradingform_plugin::itemid_mapping('submissions'), $oldid, $newitemid); } /** * Process a plugin-config restore * @param object $data The data in object form * @return void */ protected function process_assign_plugin_config($data) { global $DB; $data = (object)$data; $oldid = $data->id; $data->assignment = $this->get_new_parentid('assign'); $newitemid = $DB->insert_record('assign_plugin_config', $data); } /** * For all submissions in this assignment, either set the * submission->latest field to 1 for the latest attempts * or create a new submission record for grades with no submission. * * @return void */ protected function set_latest_submission_field() { global $DB, $CFG; // Required for constants. require_once($CFG->dirroot . '/mod/assign/locallib.php'); $assignmentid = $this->get_new_parentid('assign'); // First check for records with a grade, but no submission record. // This happens when a teacher marks a student before they have submitted anything. $records = $DB->get_recordset_sql('SELECT g.id, g.userid, g.attemptnumber FROM {assign_grades} g LEFT JOIN {assign_submission} s ON s.assignment = g.assignment AND s.userid = g.userid WHERE s.id IS NULL AND g.assignment = ?', array($assignmentid)); $submissions = array(); foreach ($records as $record) { $submission = new stdClass(); $submission->assignment = $assignmentid; $submission->userid = $record->userid; $submission->attemptnumber = $record->attemptnumber; $submission->status = ASSIGN_SUBMISSION_STATUS_NEW; $submission->groupid = 0; $submission->latest = 0; $submission->timecreated = time(); $submission->timemodified = time(); array_push($submissions, $submission); } $records->close(); $DB->insert_records('assign_submission', $submissions); // This code could be rewritten as a monster SQL - but the point of adding this "latest" field // to the submissions table in the first place was to get away from those hard to maintain SQL queries. // First user submissions. $sql = 'SELECT DISTINCT userid FROM {assign_submission} WHERE assignment = ? AND groupid = ?'; $params = array($assignmentid, 0); $users = $DB->get_records_sql($sql, $params); foreach ($users as $userid => $unused) { $params = array('assignment'=>$assignmentid, 'groupid'=>0, 'userid'=>$userid); // 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); $submission->latest = 1; $DB->update_record('assign_submission', $submission); } } // Then group submissions (if any). $sql = 'SELECT DISTINCT groupid FROM {assign_submission} WHERE assignment = ? AND userid = ?'; $params = array($assignmentid, 0); $groups = $DB->get_records_sql($sql, $params); foreach ($groups as $groupid => $unused) { $params = array('assignment'=>$assignmentid, 'userid'=>0, 'groupid'=>$groupid); // 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); $submission->latest = 1; $DB->update_record('assign_submission', $submission); } } } /** * Restore files from plugin configuration * @param string $subtype the plugin type to handle * @return void */ protected function add_plugin_config_files($subtype) { $dummyassign = new assign(null, null, null); $plugins = $dummyassign->load_plugins($subtype); foreach ($plugins as $plugin) { $component = $plugin->get_subtype() . '_' . $plugin->get_type(); $areas = $plugin->get_config_file_areas(); foreach ($areas as $area) { $this->add_related_files($component, $area, null); } } } /** * Process a assign override restore * @param object $data The data in object form * @return void */ protected function process_assign_override($data) { global $DB; $data = (object)$data; $oldid = $data->id; // Based on userinfo, we'll restore user overides or no. $userinfo = $this->get_setting_value('userinfo'); // Skip user overrides if we are not restoring userinfo. if (!$userinfo && !is_null($data->userid)) { return; } // Skip group overrides if we are not restoring groupinfo. $groupinfo = $this->get_setting_value('groups'); if (!$groupinfo && !is_null($data->groupid)) { return; } $data->assignid = $this->get_new_parentid('assign'); if (!is_null($data->userid)) { $data->userid = $this->get_mappingid('user', $data->userid); } if (!is_null($data->groupid)) { $data->groupid = $this->get_mappingid('group', $data->groupid); } $data->allowsubmissionsfromdate = $this->apply_date_offset($data->allowsubmissionsfromdate); $data->duedate = $this->apply_date_offset($data->duedate); $data->cutoffdate = $this->apply_date_offset($data->cutoffdate); $newitemid = $DB->insert_record('assign_overrides', $data); // Add mapping, restore of logs needs it. $this->set_mapping('assign_override', $oldid, $newitemid); } /** * Once the database tables have been fully restored, restore the files */ protected function after_execute() { $this->add_related_files('mod_assign', 'intro', null); $this->add_related_files('mod_assign', 'introattachment', null); $this->add_related_files('mod_assign', 'activityattachment', null); $this->add_plugin_config_files('assignsubmission'); $this->add_plugin_config_files('assignfeedback'); $this->set_latest_submission_field(); } } backup/moodle2/backup_assign_activity_task.class.php000064400000004614152265517350016756 0ustar00. /** * This file contains the backup activity for the 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(); require_once($CFG->dirroot . '/mod/assign/backup/moodle2/backup_assign_stepslib.php'); /** * assign backup task that provides all the settings and steps to perform one complete backup of the activity * * @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 backup_assign_activity_task extends backup_activity_task { /** * Define (add) particular settings this activity can have */ protected function define_my_settings() { // No particular settings for this activity. } /** * Define (add) particular steps this activity can have */ protected function define_my_steps() { $this->add_step(new backup_assign_activity_structure_step('assign_structure', 'assign.xml')); } /** * Code the transformations to perform in the activity in * order to get transportable (encoded) links * @param string $content * @return string */ public static function encode_content_links($content) { global $CFG; $base = preg_quote($CFG->wwwroot, "/"); $search="/(".$base."\/mod\/assign\/index.php\?id\=)([0-9]+)/"; $content= preg_replace($search, '$@ASSIGNINDEX*$2@$', $content); $search="/(".$base."\/mod\/assign\/view.php\?id\=)([0-9]+)/"; $content= preg_replace($search, '$@ASSIGNVIEWBYID*$2@$', $content); return $content; } } quickgradingform.php000064400000004173152265517350010631 0ustar00. /** * This file contains the forms to create and edit an instance of this 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('Direct access to this script is forbidden.'); require_once($CFG->libdir.'/formslib.php'); require_once($CFG->dirroot . '/mod/assign/locallib.php'); /** * Assignment quick grading form * * @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 mod_assign_quick_grading_form extends moodleform { /** * Define this form - called from the parent constructor */ public function definition() { $mform = $this->_form; $instance = $this->_customdata; // Visible elements. $mform->addElement('html', $instance['gradingtable']); // Hidden params. $mform->addElement('hidden', 'id', $instance['cm']); $mform->setType('id', PARAM_INT); $mform->addElement('hidden', 'action', 'quickgrade'); $mform->setType('action', PARAM_ALPHA); $mform->addElement('hidden', 'lastpage', $instance['page']); $mform->setType('lastpage', PARAM_INT); // No need to add a submit button. // The submit button is in the footer. $mform->addElement('html', $instance['footer']); } } submission_form.php000064400000004305152265517350010510 0ustar00. /** * This file contains the submission form used by the 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('Direct access to this script is forbidden.'); require_once($CFG->libdir . '/formslib.php'); require_once($CFG->dirroot . '/mod/assign/locallib.php'); /** * Assign submission form * * @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 mod_assign_submission_form extends moodleform { /** * Define this form - called by the parent constructor */ public function definition() { global $USER; $mform = $this->_form; list($assign, $data) = $this->_customdata; $instance = $assign->get_instance(); if ($instance->teamsubmission) { $submission = $assign->get_group_submission($data->userid, 0, true); } else { $submission = $assign->get_user_submission($data->userid, true); } if ($submission) { $mform->addElement('hidden', 'lastmodified', $submission->timemodified); $mform->setType('lastmodified', PARAM_INT); } $assign->add_submission_form_elements($mform, $data); $this->add_action_buttons(true, get_string('savechanges', 'assign')); if ($data) { $this->set_data($data); } } } view.php000064400000003631152265517350006245 0ustar00. /** * This file is the entry point to the assign module. All pages are rendered from here * * @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 */ require_once('../../config.php'); require_once($CFG->dirroot . '/mod/assign/locallib.php'); $id = required_param('id', PARAM_INT); list ($course, $cm) = get_course_and_cm_from_cmid($id, 'assign'); require_login($course, true, $cm); $context = context_module::instance($cm->id); require_capability('mod/assign:view', $context); $assign = new assign($context, $cm, $course); $urlparams = array('id' => $id, 'action' => optional_param('action', '', PARAM_ALPHA), 'rownum' => optional_param('rownum', 0, PARAM_INT), 'useridlistid' => optional_param('useridlistid', $assign->get_useridlist_key_id(), PARAM_ALPHANUM)); $url = new moodle_url('/mod/assign/view.php', $urlparams); $PAGE->set_url($url); // Update module completion status. $assign->set_module_viewed(); // Apply overrides. $assign->update_effective_access($USER->id); // Get the assign class to // render the page. echo $assign->view(optional_param('action', '', PARAM_ALPHA)); pix/layout-expand-right.png000064400000000203152265517350011765 0ustar00PNG  IHDRabKGD8IDAT8c @`D3 }JF`|\b0j0qs# :wIENDB`pix/layout-expand-right.svg000064400000001321152265517350012002 0ustar00 ]> pix/layout-expand-left.png000064400000000200152265517350011577 0ustar00PNG  IHDRabKGD5IDAT8c @`D3 }JF| a.)v <7F ${ IENDB`pix/gradefeedback.svg000064400000001030152265517350010621 0ustar00 ]>pix/layout-default.svg000064400000001322152265517350011035 0ustar00 ]> pix/gradefeedback.gif000064400000000546152265517350010602 0ustar00GIF89a6 "#7'W+_0i"9}):*J5M9Q=`HbHiIjJ`P! ?,pH,l:˚tJ56BXF *F'[ȈɠyL-"+0?b')z0mBb+/  4Q.1 W5123 _%(,mRU5 SOLCA;pix/gradefeedback.png000064400000000464152265517350010620 0ustar00PNG  IHDR Vu\bKGDIDAT(1N@JHA8>驨Mq=<4vLvq$EQ\IZIj{ !ld3[VU8" ov!8O%-̬΀LxsHJϮx^("w2or6]#1~-ipiܟOƅ=,fo޿L Ce6H$m eA0@K_qi/Ü IENDB`pix/layout-expand-left.svg000064400000001322152265517350011620 0ustar00 ]> pix/monologo.svg000064400000003271152265517350007734 0ustar00 pix/layout-default.png000064400000000200152265517350011014 0ustar00PNG  IHDRabKGD5IDAT8c @`D3 }JFl `.DSQF 0xnf bIENDB`lib.php000064400000205036152265517350006044 0ustar00. /** * This file contains the moodle hooks for the assign module. * * It delegates most functions to the assignment class. * * @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(); /** * Adds an assignment instance * * This is done by calling the add_instance() method of the assignment type class * @param stdClass $data * @param mod_assign_mod_form $form * @return int The instance id of the new assignment */ function assign_add_instance(stdClass $data, ?mod_assign_mod_form $form = null) { global $CFG; require_once($CFG->dirroot . '/mod/assign/locallib.php'); $assignment = new assign(context_module::instance($data->coursemodule), null, null); return $assignment->add_instance($data, true); } /** * delete an assignment instance * @param int $id * @return bool */ function assign_delete_instance($id) { global $CFG; require_once($CFG->dirroot . '/mod/assign/locallib.php'); $cm = get_coursemodule_from_instance('assign', $id, 0, false, MUST_EXIST); $context = context_module::instance($cm->id); $assignment = new assign($context, null, null); return $assignment->delete_instance(); } /** * This function is used by the reset_course_userdata function in moodlelib. * This function will remove all assignment submissions and feedbacks in the database * and clean up any related data. * * @param stdClass $data the data submitted from the reset course. * @return array */ function assign_reset_userdata($data) { global $CFG, $DB; require_once($CFG->dirroot . '/mod/assign/locallib.php'); $status = []; $params = ['courseid' => $data->courseid]; $sql = "SELECT a.id FROM {assign} a WHERE a.course=:courseid"; $course = $DB->get_record('course', ['id' => $data->courseid], '*', MUST_EXIST); if ($assigns = $DB->get_records_sql($sql, $params)) { foreach ($assigns as $assign) { $cm = get_coursemodule_from_instance( 'assign', $assign->id, $data->courseid, false, MUST_EXIST, ); $context = context_module::instance($cm->id); $assignment = new assign($context, $cm, $course); $status = array_merge($status, $assignment->reset_userdata($data)); } } return $status; } /** * This standard function will check all instances of this module * and make sure there are up-to-date events created for each of them. * If courseid = 0, then every assignment event in the site is checked, else * only assignment events belonging to the course specified are checked. * * @param int $courseid * @param int|stdClass $instance Assign module instance or ID. * @param int|stdClass $cm Course module object or ID (not used in this module). * @return bool */ function assign_refresh_events($courseid = 0, $instance = null, $cm = null) { global $CFG, $DB; require_once($CFG->dirroot . '/mod/assign/locallib.php'); // If we have instance information then we can just update the one event instead of updating all events. if (isset($instance)) { if (!is_object($instance)) { $instance = $DB->get_record('assign', array('id' => $instance), '*', MUST_EXIST); } if (isset($cm)) { if (!is_object($cm)) { assign_prepare_update_events($instance); return true; } else { $course = get_course($instance->course); assign_prepare_update_events($instance, $course, $cm); return true; } } } if ($courseid) { // Make sure that the course id is numeric. if (!is_numeric($courseid)) { return false; } if (!$assigns = $DB->get_records('assign', array('course' => $courseid))) { return false; } // Get course from courseid parameter. if (!$course = $DB->get_record('course', array('id' => $courseid), '*')) { return false; } } else { if (!$assigns = $DB->get_records('assign')) { return false; } } foreach ($assigns as $assign) { assign_prepare_update_events($assign); } return true; } /** * This actually updates the normal and completion calendar events. * * @param stdClass $assign Assignment object (from DB). * @param stdClass $course Course object. * @param stdClass $cm Course module object. */ function assign_prepare_update_events($assign, $course = null, $cm = null) { global $DB; if (!isset($course)) { // Get course and course module for the assignment. list($course, $cm) = get_course_and_cm_from_instance($assign->id, 'assign', $assign->course); } // Refresh the assignment's calendar events. $context = context_module::instance($cm->id); $assignment = new assign($context, $cm, $course); $assignment->update_calendar($cm->id); // Refresh the calendar events also for the assignment overrides. $overrides = $DB->get_records('assign_overrides', ['assignid' => $assign->id], '', 'id, groupid, userid, duedate, sortorder, timelimit'); foreach ($overrides as $override) { if (empty($override->userid)) { unset($override->userid); } if (empty($override->groupid)) { unset($override->groupid); } assign_update_events($assignment, $override); } } /** * Removes all grades from gradebook * * @param int $courseid The ID of the course to reset * @param string $type Optional type of assignment to limit the reset to a particular assignment type */ function assign_reset_gradebook($courseid, $type='') { global $CFG, $DB; $params = array('moduletype'=>'assign', 'courseid'=>$courseid); $sql = 'SELECT a.*, cm.idnumber as cmidnumber, a.course as courseid FROM {assign} a, {course_modules} cm, {modules} m WHERE m.name=:moduletype AND m.id=cm.module AND cm.instance=a.id AND a.course=:courseid'; if ($assignments = $DB->get_records_sql($sql, $params)) { foreach ($assignments as $assignment) { assign_grade_item_update($assignment, 'reset'); } } } /** * Implementation of the function for printing the form elements that control * whether the course reset functionality affects the assignment. * @param MoodleQuickForm $mform form passed by reference */ function assign_reset_course_form_definition(&$mform) { $mform->addElement('header', 'assignheader', get_string('modulenameplural', 'assign')); $mform->addElement('static', 'assigndelete', get_string('delete')); $name = get_string('deleteallsubmissions', 'assign'); $mform->addElement('advcheckbox', 'reset_assign_submissions', $name); $mform->addElement('advcheckbox', 'reset_assign_user_overrides', get_string('removealluseroverrides', 'assign')); $mform->addElement('advcheckbox', 'reset_assign_group_overrides', get_string('removeallgroupoverrides', 'assign')); } /** * Course reset form defaults. * @param object $course * @return array */ function assign_reset_course_form_defaults($course) { return array('reset_assign_submissions' => 1, 'reset_assign_group_overrides' => 1, 'reset_assign_user_overrides' => 1); } /** * Update an assignment instance * * This is done by calling the update_instance() method of the assignment type class * @param stdClass $data * @param stdClass $form - unused * @return object */ function assign_update_instance(stdClass $data, $form) { global $CFG; require_once($CFG->dirroot . '/mod/assign/locallib.php'); $context = context_module::instance($data->coursemodule); $assignment = new assign($context, null, null); return $assignment->update_instance($data); } /** * This function updates the events associated to the assign. * If $override is non-zero, then it updates only the events * associated with the specified override. * * @param assign $assign the assign object. * @param object $override (optional) limit to a specific override */ function assign_update_events($assign, $override = null) { global $CFG, $DB; require_once($CFG->dirroot . '/calendar/lib.php'); $assigninstance = $assign->get_instance(); // Load the old events relating to this assign. $conds = array('modulename' => 'assign', 'instance' => $assigninstance->id); if (!empty($override)) { // Only load events for this override. if (isset($override->userid)) { $conds['userid'] = $override->userid; } else if (isset($override->groupid)) { $conds['groupid'] = $override->groupid; } else { // This is not a valid override, it may have been left from a bad import or restore. $conds['groupid'] = $conds['userid'] = 0; } } $oldevents = $DB->get_records('event', $conds, 'id ASC'); // Now make a to-do list of all that needs to be updated. if (empty($override)) { // We are updating the primary settings for the assignment, so we need to add all the overrides. $overrides = $DB->get_records('assign_overrides', array('assignid' => $assigninstance->id), 'id ASC'); // It is necessary to add an empty stdClass to the beginning of the array as the $oldevents // list contains the original (non-override) event for the module. If this is not included // the logic below will end up updating the wrong row when we try to reconcile this $overrides // list against the $oldevents list. array_unshift($overrides, new stdClass()); } else { // Just do the one override. $overrides = array($override); } if (!empty($assign->get_course_module())) { $cmid = $assign->get_course_module()->id; } else { $cmid = get_coursemodule_from_instance('assign', $assigninstance->id, $assigninstance->course)->id; } foreach ($overrides as $current) { $groupid = isset($current->groupid) ? $current->groupid : 0; $userid = isset($current->userid) ? $current->userid : 0; $duedate = isset($current->duedate) ? $current->duedate : $assigninstance->duedate; $timelimit = isset($current->timelimit) ? $current->timelimit : 0; // Only add 'due' events for an override if they differ from the assign default. $addclose = empty($current->id) || !empty($current->duedate); $event = new stdClass(); $event->type = CALENDAR_EVENT_TYPE_ACTION; $event->description = format_module_intro('assign', $assigninstance, $cmid, false); $event->format = FORMAT_HTML; // Events module won't show user events when the courseid is nonzero. $event->courseid = ($userid) ? 0 : $assigninstance->course; $event->groupid = $groupid; $event->userid = $userid; $event->modulename = 'assign'; $event->instance = $assigninstance->id; $event->timestart = $duedate; $event->timeduration = $timelimit; $event->timesort = $event->timestart + $event->timeduration; $event->visible = instance_is_visible('assign', $assigninstance); $event->eventtype = ASSIGN_EVENT_TYPE_DUE; $event->priority = null; // Determine the event name and priority. if ($groupid) { // Group override event. $params = new stdClass(); $params->assign = $assigninstance->name; $params->group = groups_get_group_name($groupid); if ($params->group === false) { // Group doesn't exist, just skip it. continue; } $eventname = get_string('overridegroupeventname', 'assign', $params); // Set group override priority. if (isset($current->sortorder)) { $event->priority = $current->sortorder; } } else if ($userid) { // User override event. $params = new stdClass(); $params->assign = $assigninstance->name; $eventname = get_string('overrideusereventname', 'assign', $params); // Set user override priority. $event->priority = CALENDAR_EVENT_USER_OVERRIDE_PRIORITY; } else { // The parent event. $eventname = $assigninstance->name; } if ($duedate && $addclose) { if ($oldevent = array_shift($oldevents)) { $event->id = $oldevent->id; } else { unset($event->id); } $event->name = $eventname.' ('.get_string('duedate', 'assign').')'; calendar_event::create($event, false); } } // Delete any leftover events. foreach ($oldevents as $badevent) { $badevent = calendar_event::load($badevent); $badevent->delete(); } } /** * Return the list if Moodle features this module supports * * @param string $feature FEATURE_xx constant for requested feature * @return mixed True if module supports feature, false if not, null if doesn't know or string for the module purpose. */ function assign_supports($feature) { switch($feature) { case FEATURE_GROUPS: return true; case FEATURE_GROUPINGS: return true; case FEATURE_MOD_INTRO: return true; case FEATURE_COMPLETION_TRACKS_VIEWS: return true; case FEATURE_COMPLETION_HAS_RULES: return true; case FEATURE_GRADE_HAS_GRADE: return true; case FEATURE_GRADE_HAS_PENALTY: return true; case FEATURE_GRADE_OUTCOMES: return true; case FEATURE_BACKUP_MOODLE2: return true; case FEATURE_SHOW_DESCRIPTION: return true; case FEATURE_ADVANCED_GRADING: return true; case FEATURE_PLAGIARISM: return true; case FEATURE_COMMENT: return true; case FEATURE_MOD_PURPOSE: return MOD_PURPOSE_ASSESSMENT; default: return null; } } /** * extend an assigment navigation settings * * @param settings_navigation $settings * @param navigation_node $navref * @return void */ function assign_extend_settings_navigation(settings_navigation $settings, navigation_node $navref) { global $DB, $CFG; require_once($CFG->dirroot . '/mod/assign/locallib.php'); // We want to add these new nodes after the Edit settings node, and before the // Locally assigned roles node. Of course, both of those are controlled by capabilities. $keys = $navref->get_children_key_list(); $beforekey = null; $i = array_search('modedit', $keys); if ($i === false and array_key_exists(0, $keys)) { $beforekey = $keys[0]; } else if (array_key_exists($i + 1, $keys)) { $beforekey = $keys[$i + 1]; } $cm = $settings->get_page()->cm; if (!$cm) { return; } $context = $cm->context; $course = $settings->get_page()->course; if (!$course) { return; } if (has_capability('mod/assign:manageoverrides', $settings->get_page()->cm->context)) { $url = new moodle_url('/mod/assign/overrides.php', ['cmid' => $settings->get_page()->cm->id, 'mode' => 'user']); $node = navigation_node::create(get_string('overrides', 'assign'), $url, navigation_node::TYPE_SETTING, null, 'mod_assign_useroverrides'); $navref->add_node($node, $beforekey); } if (has_capability('mod/assign:revealidentities', $context)) { $dbparams = array('id'=>$cm->instance); $assignment = $DB->get_record('assign', $dbparams, 'blindmarking, revealidentities'); if ($assignment && $assignment->blindmarking && !$assignment->revealidentities) { $urlparams = array('id' => $cm->id, 'action'=>'revealidentities'); $url = new moodle_url('/mod/assign/view.php', $urlparams); $linkname = get_string('revealidentities', 'assign'); $node = $navref->add($linkname, $url, navigation_node::TYPE_SETTING); } } $assign = new assign($context, null, null); // If the current user can view grades, include the 'Submissions' navigation node. if ($assign->can_view_grades()) { $url = new moodle_url('/mod/assign/view.php', ['id' => $settings->get_page()->cm->id, 'action' => 'grading']); $navref->add( text: get_string('gradeitem:submissions', 'assign'), action: $url, type: navigation_node::TYPE_SETTING, key: 'mod_assign_submissions' ); } // Allow changing grade penalty settings at course module level, on assignment module. // Other modules can choose to allow this change or not. if (\mod_assign\penalty\helper::is_penalty_enabled($cm->instance)) { \core_grades\penalty_manager::extend_navigation_module($settings, $navref); } } /** * Add a get_coursemodule_info function in case any assignment type wants to add 'extra' information * for the course (see resource). * * Given a course_module object, this function returns any "extra" information that may be needed * when printing this activity in a course listing. See get_array_of_activities() in course/lib.php. * * @param stdClass $coursemodule The coursemodule object (record). * @return cached_cm_info An object on information that the courses * will know about (most noticeably, an icon). */ function assign_get_coursemodule_info($coursemodule) { global $DB; $dbparams = array('id'=>$coursemodule->instance); $fields = 'id, name, alwaysshowdescription, allowsubmissionsfromdate, intro, introformat, completionsubmit, duedate, cutoffdate, allowsubmissionsfromdate'; if (! $assignment = $DB->get_record('assign', $dbparams, $fields)) { return false; } $result = new cached_cm_info(); $result->name = $assignment->name; if ($coursemodule->showdescription) { if ($assignment->alwaysshowdescription || time() > $assignment->allowsubmissionsfromdate) { // Convert intro to html. Do not filter cached version, filters run at display time. $result->content = format_module_intro('assign', $assignment, $coursemodule->id, false); } } // Populate the custom completion rules as key => value pairs, but only if the completion mode is 'automatic'. if ($coursemodule->completion == COMPLETION_TRACKING_AUTOMATIC) { $result->customdata['customcompletionrules']['completionsubmit'] = $assignment->completionsubmit; } // Populate some other values that can be used in calendar or on dashboard. if ($assignment->duedate) { $result->customdata['duedate'] = $assignment->duedate; } if ($assignment->cutoffdate) { $result->customdata['cutoffdate'] = $assignment->cutoffdate; } if ($assignment->allowsubmissionsfromdate) { $result->customdata['allowsubmissionsfromdate'] = $assignment->allowsubmissionsfromdate; } return $result; } /** * Sets dynamic information about a course module * * This function is called from cm_info when displaying the module * * @param cm_info $cm */ function mod_assign_cm_info_dynamic(cm_info $cm) { global $USER; $cache = cache::make('mod_assign', 'overrides'); $override = $cache->get("{$cm->instance}_u_{$USER->id}"); if (!$override) { $override = (object) [ 'allowsubmissionsfromdate' => null, 'duedate' => null, 'cutoffdate' => null, ]; } // No need to look for group overrides if there are user overrides for all allowsubmissionsfromdate, duedate and cutoffdate. if (is_null($override->allowsubmissionsfromdate) || is_null($override->duedate) || is_null($override->cutoffdate)) { $selectedgroupoverride = (object) [ 'allowsubmissionsfromdate' => null, 'duedate' => null, 'cutoffdate' => null, 'sortorder' => PHP_INT_MAX, // So that every sortorder read from DB is less than this. ]; $groupings = groups_get_user_groups($cm->course, $USER->id); foreach ($groupings[0] as $groupid) { $groupoverride = $cache->get("{$cm->instance}_g_{$groupid}"); if ($groupoverride) { if ($groupoverride->sortorder < $selectedgroupoverride->sortorder) { $selectedgroupoverride = $groupoverride; } } } // If there is a user override for a setting, ignore the group override. if (is_null($override->allowsubmissionsfromdate)) { $override->allowsubmissionsfromdate = $selectedgroupoverride->allowsubmissionsfromdate; } if (is_null($override->duedate)) { $override->duedate = $selectedgroupoverride->duedate; } if (is_null($override->cutoffdate)) { $override->cutoffdate = $selectedgroupoverride->cutoffdate; } } // Calculate relative dates. The assignment module calculates relative date only for duedate. // A user or group override always has higher priority over any relative date calculation. if (empty($override->duedate) && !empty($cm->customdata['duedate'])) { $course = get_course($cm->course); $usercoursedates = course_get_course_dates_for_user_id($course, $USER->id); if ($usercoursedates['start']) { $override->duedate = $cm->customdata['duedate'] + $usercoursedates['startoffset']; } } // Populate some other values that can be used in calendar or on dashboard. if (!is_null($override->allowsubmissionsfromdate)) { $cm->override_customdata('allowsubmissionsfromdate', $override->allowsubmissionsfromdate); } if (!is_null($override->duedate)) { $cm->override_customdata('duedate', $override->duedate); } if (!is_null($override->cutoffdate)) { $cm->override_customdata('cutoffdate', $override->cutoffdate); } } /** * Callback which returns human-readable strings describing the active completion custom rules for the module instance. * * @param cm_info|stdClass $cm object with fields ->completion and ->customdata['customcompletionrules'] * @return array $descriptions the array of descriptions for the custom rules. */ function mod_assign_get_completion_active_rule_descriptions($cm) { // Values will be present in cm_info, and we assume these are up to date. if (empty($cm->customdata['customcompletionrules']) || $cm->completion != COMPLETION_TRACKING_AUTOMATIC) { return []; } $descriptions = []; foreach ($cm->customdata['customcompletionrules'] as $key => $val) { switch ($key) { case 'completionsubmit': if (!empty($val)) { $descriptions[] = get_string('completionsubmit', 'assign'); } break; default: break; } } return $descriptions; } /** * Return a list of page types * @param string $pagetype current page type * @param stdClass $parentcontext Block's parent context * @param stdClass $currentcontext Current context of block */ function assign_page_type_list($pagetype, $parentcontext, $currentcontext) { $modulepagetype = array( 'mod-assign-*' => get_string('page-mod-assign-x', 'assign'), 'mod-assign-view' => get_string('page-mod-assign-view', 'assign'), ); return $modulepagetype; } /** * Print recent activity from all assignments in a given course * * This is used by the recent activity block * @param mixed $course the course to print activity for * @param bool $viewfullnames boolean to determine whether to show full names or not * @param int $timestart the time the rendering started * @return bool true if activity was printed, false otherwise. */ function assign_print_recent_activity($course, $viewfullnames, $timestart) { global $CFG, $USER, $DB, $OUTPUT; require_once($CFG->dirroot . '/mod/assign/locallib.php'); // Do not use log table if possible, it may be huge. $dbparams = array($timestart, $course->id, 'assign', ASSIGN_SUBMISSION_STATUS_SUBMITTED); $userfieldsapi = \core_user\fields::for_userpic(); $namefields = $userfieldsapi->get_sql('u', false, '', 'userid', false)->selects;; if (!$submissions = $DB->get_records_sql("SELECT asb.id, asb.timemodified, cm.id AS cmid, um.id as recordid, $namefields FROM {assign_submission} asb JOIN {assign} a ON a.id = asb.assignment JOIN {course_modules} cm ON cm.instance = a.id JOIN {modules} md ON md.id = cm.module JOIN {user} u ON u.id = asb.userid LEFT JOIN {assign_user_mapping} um ON um.userid = u.id AND um.assignment = a.id WHERE asb.timemodified > ? AND asb.latest = 1 AND a.course = ? AND md.name = ? AND asb.status = ? ORDER BY asb.timemodified ASC", $dbparams)) { return false; } $modinfo = get_fast_modinfo($course); $show = array(); $grader = array(); $showrecentsubmissions = get_config('assign', 'showrecentsubmissions'); foreach ($submissions as $submission) { if (!array_key_exists($submission->cmid, $modinfo->get_cms())) { continue; } $cm = $modinfo->get_cm($submission->cmid); if (!$cm->uservisible) { continue; } if ($submission->userid == $USER->id) { $show[] = $submission; continue; } $context = context_module::instance($submission->cmid); // The act of submitting of assignment may be considered private - // only graders will see it if specified. if (empty($showrecentsubmissions)) { if (!array_key_exists($cm->id, $grader)) { $grader[$cm->id] = has_capability('moodle/grade:viewall', $context); } if (!$grader[$cm->id]) { continue; } } $groupmode = groups_get_activity_groupmode($cm, $course); if ($groupmode == SEPARATEGROUPS && !has_capability('moodle/site:accessallgroups', $context)) { if (isguestuser()) { // Shortcut - guest user does not belong into any group. continue; } // This will be slow - show only users that share group with me in this cm. if (!$modinfo->get_groups($cm->groupingid)) { continue; } $usersgroups = groups_get_all_groups($course->id, $submission->userid, $cm->groupingid); if (is_array($usersgroups)) { $usersgroups = array_keys($usersgroups); $intersect = array_intersect($usersgroups, $modinfo->get_groups($cm->groupingid)); if (empty($intersect)) { continue; } } } $show[] = $submission; } if (empty($show)) { return false; } echo $OUTPUT->heading(get_string('newsubmissions', 'assign') . ':', 6); foreach ($show as $submission) { $cm = $modinfo->get_cm($submission->cmid); $context = context_module::instance($submission->cmid); $assign = new assign($context, $cm, $cm->course); $link = $CFG->wwwroot.'/mod/assign/view.php?id='.$cm->id; // Obscure first and last name if blind marking enabled. if ($assign->is_blind_marking()) { $submission->firstname = get_string('participant', 'mod_assign'); if (empty($submission->recordid)) { $submission->recordid = $assign->get_uniqueid_for_user($submission->userid); } $submission->lastname = $submission->recordid; } print_recent_activity_note($submission->timemodified, $submission, $cm->name, $link, false, $viewfullnames); } return true; } /** * Returns all assignments since a given time. * * @param array $activities The activity information is returned in this array * @param int $index The current index in the activities array * @param int $timestart The earliest activity to show * @param int $courseid Limit the search to this course * @param int $cmid The course module id * @param int $userid Optional user id * @param int $groupid Optional group id * @return void */ function assign_get_recent_mod_activity(&$activities, &$index, $timestart, $courseid, $cmid, $userid=0, $groupid=0) { global $CFG, $COURSE, $USER, $DB; require_once($CFG->dirroot . '/mod/assign/locallib.php'); if ($COURSE->id == $courseid) { $course = $COURSE; } else { $course = $DB->get_record('course', array('id'=>$courseid)); } $modinfo = get_fast_modinfo($course); $cm = $modinfo->get_cm($cmid); $params = array(); if ($userid) { $userselect = 'AND u.id = :userid'; $params['userid'] = $userid; } else { $userselect = ''; } if ($groupid) { $groupselect = 'AND gm.groupid = :groupid'; $groupjoin = 'JOIN {groups_members} gm ON gm.userid=u.id'; $params['groupid'] = $groupid; } else { $groupselect = ''; $groupjoin = ''; } $params['cminstance'] = $cm->instance; $params['timestart'] = $timestart; $params['submitted'] = ASSIGN_SUBMISSION_STATUS_SUBMITTED; $userfieldsapi = \core_user\fields::for_userpic(); $userfields = $userfieldsapi->get_sql('u', false, '', 'userid', false)->selects; if (!$submissions = $DB->get_records_sql('SELECT asb.id, asb.timemodified, ' . $userfields . ' FROM {assign_submission} asb JOIN {assign} a ON a.id = asb.assignment JOIN {user} u ON u.id = asb.userid ' . $groupjoin . ' WHERE asb.timemodified > :timestart AND asb.status = :submitted AND a.id = :cminstance ' . $userselect . ' ' . $groupselect . ' ORDER BY asb.timemodified ASC', $params)) { return; } $groupmode = groups_get_activity_groupmode($cm, $course); $cmcontext = context_module::instance($cm->id); $grader = has_capability('moodle/grade:viewall', $cmcontext); $accessallgroups = has_capability('moodle/site:accessallgroups', $cmcontext); $viewfullnames = has_capability('moodle/site:viewfullnames', $cmcontext); $showrecentsubmissions = get_config('assign', 'showrecentsubmissions'); $show = array(); foreach ($submissions as $submission) { if ($submission->userid == $USER->id) { $show[] = $submission; continue; } // The act of submitting of assignment may be considered private - // only graders will see it if specified. if (empty($showrecentsubmissions)) { if (!$grader) { continue; } } if ($groupmode == SEPARATEGROUPS and !$accessallgroups) { if (isguestuser()) { // Shortcut - guest user does not belong into any group. continue; } // This will be slow - show only users that share group with me in this cm. if (!$modinfo->get_groups($cm->groupingid)) { continue; } $usersgroups = groups_get_all_groups($course->id, $submission->userid, $cm->groupingid); if (is_array($usersgroups)) { $usersgroups = array_keys($usersgroups); $intersect = array_intersect($usersgroups, $modinfo->get_groups($cm->groupingid)); if (empty($intersect)) { continue; } } } $show[] = $submission; } if (empty($show)) { return; } if ($grader) { require_once($CFG->libdir.'/gradelib.php'); $userids = array(); foreach ($show as $id => $submission) { $userids[] = $submission->userid; } $grades = grade_get_grades($courseid, 'mod', 'assign', $cm->instance, $userids); } $aname = format_string($cm->name, true); foreach ($show as $submission) { $activity = new stdClass(); $activity->type = 'assign'; $activity->cmid = $cm->id; $activity->name = $aname; $activity->sectionnum = $cm->sectionnum; $activity->timestamp = $submission->timemodified; $activity->user = new stdClass(); if ($grader) { $activity->grade = $grades->items[0]->grades[$submission->userid]->str_long_grade; } $userfields = explode(',', implode(',', \core_user\fields::get_picture_fields())); foreach ($userfields as $userfield) { if ($userfield == 'id') { // Aliased in SQL above. $activity->user->{$userfield} = $submission->userid; } else { $activity->user->{$userfield} = $submission->{$userfield}; } } $activity->user->fullname = fullname($submission, $viewfullnames); $activities[$index++] = $activity; } return; } /** * Print recent activity from all assignments in a given course * * This is used by course/recent.php * @param stdClass $activity * @param int $courseid * @param bool $detail * @param array $modnames */ function assign_print_recent_mod_activity($activity, $courseid, $detail, $modnames) { global $CFG, $OUTPUT; echo ''; echo '
'; echo $OUTPUT->user_picture($activity->user); echo ''; if ($detail) { $modname = $modnames[$activity->type]; echo '
'; echo $OUTPUT->image_icon('monologo', $modname, 'assign'); echo ''; echo $activity->name; echo ''; echo '
'; } if (isset($activity->grade)) { echo '
'; echo get_string('gradenoun') . ': '; echo $activity->grade; echo '
'; } echo ''; echo '
'; } /** * Checks if scale is being used by any instance of assignment or is the default scale used for assignments. * * This is used to find out if scale used anywhere * @param int $scaleid * @return boolean True if the scale is used by any assignment or is the default scale used for assignments. */ function assign_scale_used_anywhere($scaleid) { global $DB; return $scaleid && ( $DB->record_exists('assign', ['grade' => -(int)$scaleid]) || (int)get_config('mod_assign', 'defaultgradescale') === (int)$scaleid ); } /** * List the actions that correspond to a view of this module. * This is used by the participation report. * * Note: This is not used by new logging system. Event with * crud = 'r' and edulevel = LEVEL_PARTICIPATING will * be considered as view action. * * @return array */ function assign_get_view_actions() { return array('view submission', 'view feedback'); } /** * List the actions that correspond to a post of this module. * This is used by the participation report. * * Note: This is not used by new logging system. Event with * crud = ('c' || 'u' || 'd') and edulevel = LEVEL_PARTICIPATING * will be considered as post action. * * @return array */ function assign_get_post_actions() { return array('upload', 'submit', 'submit for grading'); } /** * Returns all other capabilities used by this module. * @return array Array of capability strings */ function assign_get_extra_capabilities() { return ['gradereport/grader:view', 'moodle/grade:viewall']; } /** * Create grade item for given assignment. * * @param stdClass $assign record with extra cmidnumber * @param array $grades optional array/object of grade(s); 'reset' means reset grades in gradebook * @return int 0 if ok, error code otherwise */ function assign_grade_item_update($assign, $grades=null) { global $CFG; require_once($CFG->libdir.'/gradelib.php'); if (!isset($assign->courseid)) { $assign->courseid = $assign->course; } $params = array('itemname'=>$assign->name, 'idnumber'=>$assign->cmidnumber); // Check if feedback plugin for gradebook is enabled, if yes then // gradetype = GRADE_TYPE_TEXT else GRADE_TYPE_NONE. $gradefeedbackenabled = false; if (isset($assign->gradefeedbackenabled)) { $gradefeedbackenabled = $assign->gradefeedbackenabled; } else if ($assign->grade == 0) { // Grade feedback is needed only when grade == 0. require_once($CFG->dirroot . '/mod/assign/locallib.php'); $mod = get_coursemodule_from_instance('assign', $assign->id, $assign->courseid); $cm = context_module::instance($mod->id); $assignment = new assign($cm, null, null); $gradefeedbackenabled = $assignment->is_gradebook_feedback_enabled(); } if ($assign->grade > 0) { $params['gradetype'] = GRADE_TYPE_VALUE; $params['grademax'] = $assign->grade; $params['grademin'] = 0; } else if ($assign->grade < 0) { $params['gradetype'] = GRADE_TYPE_SCALE; $params['scaleid'] = -$assign->grade; } else if ($gradefeedbackenabled) { // $assign->grade == 0 and feedback enabled. $params['gradetype'] = GRADE_TYPE_TEXT; } else { // $assign->grade == 0 and no feedback enabled. $params['gradetype'] = GRADE_TYPE_NONE; } if ($grades === 'reset') { $params['reset'] = true; $grades = null; } $result = grade_update('mod/assign', $assign->courseid, 'mod', 'assign', $assign->id, 0, $grades, $params); // Get lists of users whose grades are updated. $userids = []; if (is_array($grades)) { // The $grades is array/object of grade(s). // We are checking if it is single user (array with simple values such as userid and rawgrade). // Or it is array of grade objects, for multiple users. if (isset($grades['userid']) && isset($grades['rawgrade'])) { // Single user grade update. $userids = [$grades['userid']]; } else { // Multiple user grade update. foreach ($grades as $grade) { if (is_object($grade) && isset($grade->userid) && isset($grade->rawgrade)) { $userids[] = $grade->userid; } } } } // Apply penalty to each user. foreach ($userids as $userid) { \mod_assign\penalty\helper::apply_penalty_to_user($assign->id, $userid); } return $result; } /** * Return grade for given user or all users. * * @param stdClass $assign record of assign with an additional cmidnumber * @param int $userid optional user id, 0 means all users * @return array array of grades, false if none */ function assign_get_user_grades($assign, $userid=0) { global $CFG; require_once($CFG->dirroot . '/mod/assign/locallib.php'); $cm = get_coursemodule_from_instance('assign', $assign->id, 0, false, MUST_EXIST); $context = context_module::instance($cm->id); $assignment = new assign($context, null, null); $assignment->set_instance($assign); return $assignment->get_user_grades_for_gradebook($userid); } /** * Update activity grades. * * @param stdClass $assign database record * @param int $userid specific user only, 0 means all * @param bool $nullifnone - not used */ function assign_update_grades($assign, $userid=0, $nullifnone=true) { global $CFG; require_once($CFG->libdir.'/gradelib.php'); if ($assign->grade == 0) { assign_grade_item_update($assign); } else if ($grades = assign_get_user_grades($assign, $userid)) { foreach ($grades as $k => $v) { if ($v->rawgrade == -1) { $grades[$k]->rawgrade = null; } } assign_grade_item_update($assign, $grades); } else { assign_grade_item_update($assign); } } /** * List the file areas that can be browsed. * * @param stdClass $course * @param stdClass $cm * @param stdClass $context * @return array */ function assign_get_file_areas($course, $cm, $context) { global $CFG; require_once($CFG->dirroot . '/mod/assign/locallib.php'); $areas = array( ASSIGN_INTROATTACHMENT_FILEAREA => get_string('introattachments', 'mod_assign'), ASSIGN_ACTIVITYATTACHMENT_FILEAREA => get_string('activityattachments', 'mod_assign'), ); $assignment = new assign($context, $cm, $course); foreach ($assignment->get_submission_plugins() as $plugin) { if ($plugin->is_visible()) { $pluginareas = $plugin->get_file_areas(); if ($pluginareas) { $areas = array_merge($areas, $pluginareas); } } } foreach ($assignment->get_feedback_plugins() as $plugin) { if ($plugin->is_visible()) { $pluginareas = $plugin->get_file_areas(); if ($pluginareas) { $areas = array_merge($areas, $pluginareas); } } } return $areas; } /** * File browsing support for assign module. * * @param file_browser $browser * @param object $areas * @param object $course * @param object $cm * @param object $context * @param string $filearea * @param int $itemid * @param string $filepath * @param string $filename * @return object file_info instance or null if not found */ function assign_get_file_info($browser, $areas, $course, $cm, $context, $filearea, $itemid, $filepath, $filename) { global $CFG; require_once($CFG->dirroot . '/mod/assign/locallib.php'); if ($context->contextlevel != CONTEXT_MODULE) { return null; } $urlbase = $CFG->wwwroot.'/pluginfile.php'; $fs = get_file_storage(); $filepath = is_null($filepath) ? '/' : $filepath; $filename = is_null($filename) ? '.' : $filename; // Need to find where this belongs to. $assignment = new assign($context, $cm, $course); if ($filearea === ASSIGN_INTROATTACHMENT_FILEAREA || $filearea === ASSIGN_ACTIVITYATTACHMENT_FILEAREA) { if (!has_capability('moodle/course:managefiles', $context)) { // Students can not peak here! return null; } if (!($storedfile = $fs->get_file($assignment->get_context()->id, 'mod_assign', $filearea, 0, $filepath, $filename))) { return null; } return new file_info_stored($browser, $assignment->get_context(), $storedfile, $urlbase, $filearea, $itemid, true, true, false); } $pluginowner = null; foreach ($assignment->get_submission_plugins() as $plugin) { if ($plugin->is_visible()) { $pluginareas = $plugin->get_file_areas(); if (array_key_exists($filearea, $pluginareas)) { $pluginowner = $plugin; break; } } } if (!$pluginowner) { foreach ($assignment->get_feedback_plugins() as $plugin) { if ($plugin->is_visible()) { $pluginareas = $plugin->get_file_areas(); if (array_key_exists($filearea, $pluginareas)) { $pluginowner = $plugin; break; } } } } if (!$pluginowner) { return null; } $result = $pluginowner->get_file_info($browser, $filearea, $itemid, $filepath, $filename); return $result; } /** * Prints the complete info about a user's interaction with an assignment. * * @param stdClass $course * @param stdClass $user * @param stdClass $coursemodule * @param stdClass $assign the database assign record * * This prints the submission summary and feedback summary for this student. */ function assign_user_complete($course, $user, $coursemodule, $assign) { global $CFG; require_once($CFG->dirroot . '/mod/assign/locallib.php'); $context = context_module::instance($coursemodule->id); $assignment = new assign($context, $coursemodule, $course); echo $assignment->view_student_summary($user, false); } /** * Rescale all grades for this activity and push the new grades to the gradebook. * * @param stdClass $course Course db record * @param stdClass $cm Course module db record * @param float $oldmin * @param float $oldmax * @param float $newmin * @param float $newmax */ function assign_rescale_activity_grades($course, $cm, $oldmin, $oldmax, $newmin, $newmax) { global $DB; if ($oldmax <= $oldmin) { // Grades cannot be scaled. return false; } $scale = ($newmax - $newmin) / ($oldmax - $oldmin); if (($newmax - $newmin) <= 1) { // We would lose too much precision, lets bail. return false; } $params = array( 'p1' => $oldmin, 'p2' => $scale, 'p3' => $newmin, 'a' => $cm->instance ); // Only rescale grades that are greater than or equal to 0. Anything else is a special value. $sql = 'UPDATE {assign_grades} set grade = (((grade - :p1) * :p2) + :p3) where assignment = :a and grade >= 0'; $dbupdate = $DB->execute($sql, $params); if (!$dbupdate) { return false; } // Now re-push all grades to the gradebook. $dbparams = array('id' => $cm->instance); $assign = $DB->get_record('assign', $dbparams); $assign->cmidnumber = $cm->idnumber; assign_update_grades($assign); return true; } /** * Print the grade information for the assignment for this user. * * @param stdClass $course * @param stdClass $user * @param stdClass $coursemodule * @param stdClass $assignment */ function assign_user_outline($course, $user, $coursemodule, $assignment) { global $CFG; require_once($CFG->libdir.'/gradelib.php'); require_once($CFG->dirroot.'/grade/grading/lib.php'); $gradinginfo = grade_get_grades($course->id, 'mod', 'assign', $assignment->id, $user->id); $gradingitem = $gradinginfo->items[0]; $gradebookgrade = $gradingitem->grades[$user->id]; if (empty($gradebookgrade->str_long_grade)) { return null; } $result = new stdClass(); if (!$gradingitem->hidden || has_capability('moodle/grade:viewhidden', context_course::instance($course->id))) { $result->info = get_string('outlinegrade', 'assign', $gradebookgrade->str_long_grade); } else { $result->info = get_string('gradenoun') . ': ' . get_string('hidden', 'grades'); } $result->time = $gradebookgrade->dategraded; return $result; } /** * Serves intro attachment files. * * @param mixed $course course or id of the course * @param mixed $cm course module or id of the course module * @param context $context * @param string $filearea * @param array $args * @param bool $forcedownload * @param array $options additional options affecting the file serving * @return bool false if file not found, does not return if found - just send the file */ function assign_pluginfile($course, $cm, context $context, $filearea, $args, $forcedownload, array $options=array()) { global $CFG; if ($context->contextlevel != CONTEXT_MODULE) { return false; } require_login($course, false, $cm); if (!has_capability('mod/assign:view', $context)) { return false; } require_once($CFG->dirroot . '/mod/assign/locallib.php'); $assign = new assign($context, $cm, $course); if ($filearea !== ASSIGN_INTROATTACHMENT_FILEAREA && $filearea !== ASSIGN_ACTIVITYATTACHMENT_FILEAREA) { return false; } if (!$assign->show_intro()) { return false; } $itemid = (int)array_shift($args); if ($itemid != 0) { return false; } $relativepath = implode('/', $args); $fullpath = "/{$context->id}/mod_assign/$filearea/$itemid/$relativepath"; $fs = get_file_storage(); if (!$file = $fs->get_file_by_hash(sha1($fullpath)) or $file->is_directory()) { return false; } send_stored_file($file, 0, 0, $forcedownload, $options); } /** * Serve the grading panel as a fragment. * * @param array $args List of named arguments for the fragment loader. * @return string */ function mod_assign_output_fragment_gradingpanel($args) { global $CFG; \core\session\manager::write_close(); // No changes to session in this function. $context = $args['context']; if ($context->contextlevel != CONTEXT_MODULE) { return null; } require_once($CFG->dirroot . '/mod/assign/locallib.php'); $assign = new assign($context, null, null); $userid = clean_param($args['userid'], PARAM_INT); $participant = $assign->get_participant($userid); $isfiltered = $assign->is_userid_filtered($userid); if (!$participant || !$isfiltered) { // User is not enrolled or filtered out by filters and table preferences. return ''; } $attemptnumber = clean_param($args['attemptnumber'], PARAM_INT); $formdata = array(); if (!empty($args['jsonformdata'])) { $serialiseddata = json_decode($args['jsonformdata']); parse_str($serialiseddata, $formdata); } $viewargs = array( 'userid' => $userid, 'attemptnumber' => $attemptnumber, 'formdata' => $formdata ); return $assign->view('gradingpanel', $viewargs); } /** * Check if the module has any update that affects the current user since a given time. * * @param cm_info $cm course module data * @param int $from the time to check updates from * @param array $filter if we need to check only specific updates * @return stdClass an object with the different type of areas indicating if they were updated or not * @since Moodle 3.2 */ function assign_check_updates_since(cm_info $cm, $from, $filter = array()) { global $DB, $USER, $CFG; require_once($CFG->dirroot . '/mod/assign/locallib.php'); $updates = new stdClass(); $updates = course_check_module_updates_since($cm, $from, array(ASSIGN_INTROATTACHMENT_FILEAREA), $filter); // Check if there is a new submission by the user or new grades. $select = 'assignment = :id AND userid = :userid AND (timecreated > :since1 OR timemodified > :since2)'; $params = array('id' => $cm->instance, 'userid' => $USER->id, 'since1' => $from, 'since2' => $from); $updates->submissions = (object) array('updated' => false); $submissions = $DB->get_records_select('assign_submission', $select, $params, '', 'id'); if (!empty($submissions)) { $updates->submissions->updated = true; $updates->submissions->itemids = array_keys($submissions); } $updates->grades = (object) array('updated' => false); $grades = $DB->get_records_select('assign_grades', $select, $params, '', 'id'); if (!empty($grades)) { $updates->grades->updated = true; $updates->grades->itemids = array_keys($grades); } // Now, teachers should see other students updates. if (has_capability('mod/assign:viewgrades', $cm->context)) { $params = array('id' => $cm->instance, 'since1' => $from, 'since2' => $from); $select = 'assignment = :id AND (timecreated > :since1 OR timemodified > :since2)'; if (groups_get_activity_groupmode($cm) == SEPARATEGROUPS) { $groupusers = array_keys(groups_get_activity_shared_group_members($cm)); if (empty($groupusers)) { return $updates; } list($insql, $inparams) = $DB->get_in_or_equal($groupusers, SQL_PARAMS_NAMED); $select .= ' AND userid ' . $insql; $params = array_merge($params, $inparams); } $updates->usersubmissions = (object) array('updated' => false); $submissions = $DB->get_records_select('assign_submission', $select, $params, '', 'id'); if (!empty($submissions)) { $updates->usersubmissions->updated = true; $updates->usersubmissions->itemids = array_keys($submissions); } $updates->usergrades = (object) array('updated' => false); $grades = $DB->get_records_select('assign_grades', $select, $params, '', 'id'); if (!empty($grades)) { $updates->usergrades->updated = true; $updates->usergrades->itemids = array_keys($grades); } } return $updates; } /** * Is the event visible? * * This is used to determine global visibility of an event in all places throughout Moodle. For example, * the ASSIGN_EVENT_TYPE_GRADINGDUE event will not be shown to students on their calendar. * * @param calendar_event $event * @param int $userid User id to use for all capability checks, etc. Set to 0 for current user (default). * @return bool Returns true if the event is visible to the current user, false otherwise. */ function mod_assign_core_calendar_is_event_visible(calendar_event $event, $userid = 0) { global $CFG, $USER; require_once($CFG->dirroot . '/mod/assign/locallib.php'); if (empty($userid)) { $userid = $USER->id; } $cm = get_fast_modinfo($event->courseid, $userid)->instances['assign'][$event->instance]; $context = context_module::instance($cm->id); $assign = new assign($context, $cm, null); if ($event->eventtype == ASSIGN_EVENT_TYPE_GRADINGDUE) { return $assign->can_grade($userid); } else { return true; } } /** * This function receives a calendar event and returns the action associated with it, or null if there is none. * * This is used by block_myoverview in order to display the event appropriately. If null is returned then the event * is not displayed on the block. * * @param calendar_event $event * @param \core_calendar\action_factory $factory * @param int $userid User id to use for all capability checks, etc. Set to 0 for current user (default). * @return \core_calendar\local\event\entities\action_interface|null */ function mod_assign_core_calendar_provide_event_action(calendar_event $event, \core_calendar\action_factory $factory, $userid = 0) { global $CFG, $USER; require_once($CFG->dirroot . '/mod/assign/locallib.php'); if (empty($userid)) { $userid = $USER->id; } $cm = get_fast_modinfo($event->courseid, $userid)->instances['assign'][$event->instance]; $context = context_module::instance($cm->id); $completion = new \completion_info($cm->get_course()); $completiondata = $completion->get_data($cm, false, $userid); if ($completiondata->completionstate != COMPLETION_INCOMPLETE) { return null; } $assign = new assign($context, $cm, null); // Apply overrides. $assign->update_effective_access($userid); if ($event->eventtype == ASSIGN_EVENT_TYPE_GRADINGDUE) { $name = get_string('gradeverb'); $url = new \moodle_url('/mod/assign/view.php', [ 'id' => $cm->id, 'action' => 'grader' ]); $actionable = $assign->can_grade($userid) && (time() >= $assign->get_instance()->allowsubmissionsfromdate); $itemcount = $actionable ? $assign->count_submissions_need_grading() : 0; } else { $usersubmission = $assign->get_user_submission($userid, false); if ($usersubmission && $usersubmission->status === ASSIGN_SUBMISSION_STATUS_SUBMITTED) { // The user has already submitted. // We do not want to change the text to edit the submission, we want to remove the event from the Dashboard entirely. return null; } $instance = $assign->get_instance(); if ($instance->teamsubmission && !$instance->requireallteammemberssubmit) { $groupsubmission = $assign->get_group_submission($userid, 0, false); if ($groupsubmission && $groupsubmission->status === ASSIGN_SUBMISSION_STATUS_SUBMITTED) { return null; } } $participant = $assign->get_participant($userid); if (!$participant) { // If the user is not a participant in the assignment then they have // no action to take. This will filter out the events for teachers. return null; } // The user has not yet submitted anything. Show the addsubmission link. $name = get_string('addsubmission', 'assign'); $url = new \moodle_url('/mod/assign/view.php', [ 'id' => $cm->id, 'action' => 'editsubmission' ]); $itemcount = 1; $actionable = $assign->is_any_submission_plugin_enabled() && $assign->can_edit_submission($userid, $userid); } return $factory->create_instance( $name, $url, $itemcount, $actionable ); } /** * Callback function that determines whether an action event should be showing its item count * based on the event type and the item count. * * @param calendar_event $event The calendar event. * @param int $itemcount The item count associated with the action event. * @return bool */ function mod_assign_core_calendar_event_action_shows_item_count(calendar_event $event, $itemcount = 0) { // List of event types where the action event's item count should be shown. $eventtypesshowingitemcount = [ ASSIGN_EVENT_TYPE_GRADINGDUE ]; // For mod_assign, item count should be shown if the event type is 'gradingdue' and there is one or more item count. return in_array($event->eventtype, $eventtypesshowingitemcount) && $itemcount > 0; } /** * 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 * @param stdClass $instance The module instance to get the range from * @return array */ function mod_assign_core_calendar_get_valid_event_timestart_range(\calendar_event $event, \stdClass $instance) { global $CFG; require_once($CFG->dirroot . '/mod/assign/locallib.php'); $courseid = $event->courseid; $modulename = $event->modulename; $instanceid = $event->instance; $coursemodule = get_fast_modinfo($courseid)->instances[$modulename][$instanceid]; $context = context_module::instance($coursemodule->id); $assign = new assign($context, null, null); $assign->set_instance($instance); return $assign->get_valid_calendar_event_timestart_range($event); } /** * This function will update the assign module according to the * event that has been modified. * * @throws \moodle_exception * @param \calendar_event $event * @param stdClass $instance The module instance to get the range from */ function mod_assign_core_calendar_event_timestart_updated(\calendar_event $event, \stdClass $instance) { global $CFG, $DB; require_once($CFG->dirroot . '/mod/assign/locallib.php'); if (empty($event->instance) || $event->modulename != 'assign') { return; } if ($instance->id != $event->instance) { return; } if (!in_array($event->eventtype, [ASSIGN_EVENT_TYPE_DUE, ASSIGN_EVENT_TYPE_GRADINGDUE])) { return; } $courseid = $event->courseid; $modulename = $event->modulename; $instanceid = $event->instance; $modified = false; $coursemodule = get_fast_modinfo($courseid)->instances[$modulename][$instanceid]; $context = context_module::instance($coursemodule->id); // The user does not have the capability to modify this activity. if (!has_capability('moodle/course:manageactivities', $context)) { return; } $assign = new assign($context, $coursemodule, null); $assign->set_instance($instance); 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 ($assign->is_override_calendar_event($event)) { // This is an override event so we should ignore it. return; } $newduedate = $event->timestart; if ($newduedate != $instance->duedate) { $instance->duedate = $newduedate; $modified = true; } } else if ($event->eventtype == ASSIGN_EVENT_TYPE_GRADINGDUE) { $newduedate = $event->timestart; if ($newduedate != $instance->gradingduedate) { $instance->gradingduedate = $newduedate; $modified = true; } } if ($modified) { $instance->timemodified = time(); // Persist the assign instance changes. $DB->update_record('assign', $instance); $assign->update_calendar($coursemodule->id); $event = \core\event\course_module_updated::create_from_cm($coursemodule, $context); $event->trigger(); } } /** * Return a list of all the user preferences used by mod_assign. * * @uses core_user::is_current_user * * @return array[] */ function mod_assign_user_preferences(): array { $preferences = array(); $preferences['assign_filter'] = array( 'type' => PARAM_ALPHA, 'null' => NULL_NOT_ALLOWED, 'default' => '', 'permissioncallback' => [core_user::class, 'is_current_user'], ); $preferences['assign_workflowfilter'] = array( 'type' => PARAM_ALPHA, 'null' => NULL_NOT_ALLOWED, 'default' => '', 'permissioncallback' => [core_user::class, 'is_current_user'], ); $preferences['assign_markerfilter'] = array( 'type' => PARAM_ALPHANUMEXT, 'null' => NULL_NOT_ALLOWED, 'default' => '', 'permissioncallback' => [core_user::class, 'is_current_user'], ); return $preferences; } /** * Given an array with a file path, it returns the itemid and the filepath for the defined filearea. * * @param string $filearea The filearea. * @param array $args The path (the part after the filearea and before the filename). * @return array The itemid and the filepath inside the $args path, for the defined filearea. */ function mod_assign_get_path_from_pluginfile(string $filearea, array $args): array { // Assign never has an itemid (the number represents the revision but it's not stored in database). array_shift($args); // Get the filepath. if (empty($args)) { $filepath = '/'; } else { $filepath = '/' . implode('/', $args) . '/'; } return [ 'itemid' => 0, 'filepath' => $filepath, ]; } /** * Callback to fetch the activity event type lang string. * * @param string $eventtype The event type. * @return lang_string The event type lang string. */ function mod_assign_core_calendar_get_event_action_string(string $eventtype): string { $modulename = get_string('modulename', 'assign'); switch ($eventtype) { case ASSIGN_EVENT_TYPE_DUE: $identifier = 'calendardue'; break; case ASSIGN_EVENT_TYPE_GRADINGDUE: $identifier = 'calendargradingdue'; break; default: return get_string('requiresaction', 'calendar', $modulename); } return get_string($identifier, 'assign', $modulename); } /** * Allow the support for SMS in assignment. * * @return bool */ function mod_assign_supports_sms_notifications(): bool { // At the moment, assignment supports sms notifications. return true; } version.php000064400000002211152265517350006751 0ustar00. /** * Version information * * @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(); $plugin->component = 'mod_assign'; // Full name of the plugin (used for diagnostics). $plugin->version = 2025041400; // The current module version (Date: YYYYMMDDXX). $plugin->requires = 2025040800; // Requires this Moodle version. overrideedit.php000064400000023523152265517350007762 0ustar00. /** * This page handles editing and creation of assign overrides * * @package mod_assign * @copyright 2016 Ilya Tregubov * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ require_once(dirname(__FILE__) . '/../../config.php'); require_once($CFG->dirroot.'/mod/assign/lib.php'); require_once($CFG->dirroot.'/mod/assign/locallib.php'); require_once($CFG->dirroot.'/mod/assign/override_form.php'); $cmid = optional_param('cmid', 0, PARAM_INT); $overrideid = optional_param('id', 0, PARAM_INT); $action = optional_param('action', null, PARAM_ALPHA); $reset = optional_param('reset', false, PARAM_BOOL); $userid = optional_param('userid', null, PARAM_INT); $userchange = optional_param('userchange', false, PARAM_BOOL); $pagetitle = get_string('editoverride', 'assign'); $override = null; if ($overrideid) { if (! $override = $DB->get_record('assign_overrides', array('id' => $overrideid))) { throw new \moodle_exception('invalidoverrideid', 'assign'); } list($course, $cm) = get_course_and_cm_from_instance($override->assignid, 'assign'); } else if ($cmid) { list($course, $cm) = get_course_and_cm_from_cmid($cmid, 'assign'); } else { throw new \moodle_exception('invalidcoursemodule'); } $url = new moodle_url('/mod/assign/overrideedit.php'); if ($action) { $url->param('action', $action); } if ($overrideid) { $url->param('id', $overrideid); } else { $url->param('cmid', $cmid); } $PAGE->set_url($url); require_login($course, false, $cm); $context = context_module::instance($cm->id); $assign = new assign($context, $cm, $course); $assigninstance = $assign->get_instance($userid); $shouldadduserid = $userid && !empty($course->relativedatesmode); $shouldresetform = optional_param('resetbutton', 0, PARAM_ALPHA) || ($userchange && $action !== 'duplicate'); // Add or edit an override. require_capability('mod/assign:manageoverrides', $context); if ($overrideid) { // Editing an override. $data = clone $override; if ($override->groupid) { if (!groups_group_visible($override->groupid, $course, $cm)) { throw new \moodle_exception('invalidoverrideid', 'assign'); } } else { if (!groups_user_groups_visible($course, $override->userid, $cm)) { throw new \moodle_exception('invalidoverrideid', 'assign'); } } } else { // Creating a new override. $data = new stdClass(); } // Merge assign defaults with data. $keys = array('duedate', 'cutoffdate', 'allowsubmissionsfromdate', 'timelimit'); foreach ($keys as $key) { if (!isset($data->{$key}) || $reset) { $data->{$key} = $assigninstance->{$key}; } } // True if group-based override. $groupmode = !empty($data->groupid) || ($action === 'addgroup' && empty($overrideid)); // If we are duplicating an override, then clear the user/group and override id // since they will change. if ($action === 'duplicate') { $override->id = $data->id = null; $override->userid = $data->userid = null; $override->groupid = $data->groupid = null; $pagetitle = get_string('duplicateoverride', 'assign'); } if ($shouldadduserid) { $data->userid = $userid; } $overridelisturl = new moodle_url('/mod/assign/overrides.php', array('cmid' => $cm->id)); if (!$groupmode) { $overridelisturl->param('mode', 'user'); } // Setup the form. $mform = new assign_override_form($url, $cm, $assign, $context, $groupmode, $override, $userid); $mform->set_data($data); if ($mform->is_cancelled()) { redirect($overridelisturl); } else if ($shouldresetform) { $url->param('reset', true); if ($shouldadduserid) { $url->param('userid', $userid); } redirect($url); } else if (!$userchange && $fromform = $mform->get_data()) { // Process the data. $fromform->assignid = $assigninstance->id; // Replace unchanged values with null. foreach ($keys as $key) { if (!isset($fromform->{$key}) || $fromform->{$key} == $assigninstance->{$key}) { $fromform->{$key} = null; } } // See if we are replacing an existing override. $userorgroupchanged = false; if (empty($override->id)) { $userorgroupchanged = true; } else if (!empty($fromform->userid)) { $userorgroupchanged = $fromform->userid !== $override->userid; } else { $userorgroupchanged = $fromform->groupid !== $override->groupid; } if ($userorgroupchanged) { $conditions = array( 'assignid' => $assigninstance->id, 'userid' => empty($fromform->userid) ? null : $fromform->userid, 'groupid' => empty($fromform->groupid) ? null : $fromform->groupid); if ($oldoverride = $DB->get_record('assign_overrides', $conditions)) { // There is an old override, so we merge any new settings on top of // the older override. foreach ($keys as $key) { if (is_null($fromform->{$key})) { $fromform->{$key} = $oldoverride->{$key}; } } $assign->delete_override($oldoverride->id); } } // Set the common parameters for one of the events we may be triggering. $params = array( 'context' => $context, 'other' => array( 'assignid' => $assigninstance->id ) ); if (!empty($override->id)) { $fromform->id = $override->id; $DB->update_record('assign_overrides', $fromform); $cachekey = $groupmode ? "{$fromform->assignid}_g_{$fromform->groupid}" : "{$fromform->assignid}_u_{$fromform->userid}"; cache::make('mod_assign', 'overrides')->delete($cachekey); // Determine which override updated event to fire. $params['objectid'] = $override->id; if (!$groupmode) { $params['relateduserid'] = $fromform->userid; $event = \mod_assign\event\user_override_updated::create($params); } else { $params['other']['groupid'] = $fromform->groupid; $event = \mod_assign\event\group_override_updated::create($params); } // Trigger the override updated event. $event->trigger(); } else { unset($fromform->id); $fromform->id = $DB->insert_record('assign_overrides', $fromform); if ($groupmode) { $fromform->sortorder = 1; $overridecountgroup = $DB->count_records('assign_overrides', array('userid' => null, 'assignid' => $assigninstance->id)); $overridecountall = $DB->count_records('assign_overrides', array('assignid' => $assigninstance->id)); if ((!$overridecountgroup) && ($overridecountall)) { // No group overrides and there are user overrides. $fromform->sortorder = 1; } else { $fromform->sortorder = $overridecountgroup; } $DB->update_record('assign_overrides', $fromform); reorder_group_overrides($assigninstance->id); } $cachekey = $groupmode ? "{$fromform->assignid}_g_{$fromform->groupid}" : "{$fromform->assignid}_u_{$fromform->userid}"; cache::make('mod_assign', 'overrides')->delete($cachekey); // Determine which override created event to fire. $params['objectid'] = $fromform->id; if (!$groupmode) { $params['relateduserid'] = $fromform->userid; $event = \mod_assign\event\user_override_created::create($params); } else { $params['other']['groupid'] = $fromform->groupid; $event = \mod_assign\event\group_override_created::create($params); } // Trigger the override created event. $event->trigger(); } // Check if we need to recalculate penalty for existing grades. if (!empty($fromform->recalculatepenalty) && $fromform->recalculatepenalty === 'yes') { $assignintance = clone $assign->get_instance(); $assignintance->cmidnumber = $assign->get_course_module()->idnumber; // If it is user mode. if (!$groupmode) { assign_update_grades($assignintance, $fromform->userid); } else { // If it is group mode. $groupmembers = groups_get_members($fromform->groupid); foreach ($groupmembers as $groupmember) { assign_update_grades($assignintance, $groupmember->id); } } } assign_update_events($assign, $fromform); if (!empty($fromform->submitbutton)) { redirect($overridelisturl); } // The user pressed the 'again' button, so redirect back to this page. $url->remove_params('cmid'); $url->param('action', 'duplicate'); $url->param('id', $fromform->id); redirect($url); } // Print the form. $PAGE->navbar->add($pagetitle); $PAGE->set_pagelayout('admin'); $PAGE->add_body_class('limitedwidth'); $PAGE->set_title($pagetitle); $PAGE->set_heading($course->fullname); $PAGE->set_secondary_active_tab('mod_assign_useroverrides'); $activityheader = $PAGE->activityheader; $activityheader->set_attrs([ 'description' => '', 'hidecompletion' => true, 'title' => $activityheader->is_title_allowed() ? format_string($assigninstance->name, true, ['context' => $context]) : "" ]); echo $OUTPUT->header(); $mform->display(); echo $OUTPUT->footer(); submission/onlinetext/db/install.php000064400000002762152265517350013716 0ustar00. /** * Post-install code for the submission_onlinetext module. * * @package assignsubmission_onlinetext * @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(); /** * Code run after the assignsubmission_onlinetext module database tables have been created. * Moves the plugin to the top of the list (of 3) * @return bool */ function xmldb_assignsubmission_onlinetext_install() { global $CFG; // Set the correct initial order for the plugins. require_once($CFG->dirroot . '/mod/assign/adminlib.php'); $pluginmanager = new assign_plugin_manager('assignsubmission'); $pluginmanager->move_plugin('onlinetext', 'up'); $pluginmanager->move_plugin('onlinetext', 'up'); return true; } submission/onlinetext/db/access.php000064400000001664152265517350013511 0ustar00. /** * Capability definitions for this module. * * @package assignsubmission_onlinetext * @copyright 2012 NetSpot {@link http://www.netspot.com.au} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ $capabilities = array( ); submission/onlinetext/db/upgrade.php000064400000003131152265517350013666 0ustar00. /** * Upgrade code for install * * @package assignsubmission_onlinetext * @copyright 2012 NetSpot {@link http://www.netspot.com.au} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ /** * Stub for upgrade code * @param int $oldversion * @return bool */ function xmldb_assignsubmission_onlinetext_upgrade($oldversion) { // Automatically generated Moodle v4.2.0 release upgrade line. // Put any upgrade step following this. // Automatically generated Moodle v4.3.0 release upgrade line. // Put any upgrade step following this. // Automatically generated Moodle v4.4.0 release upgrade line. // Put any upgrade step following this. // Automatically generated Moodle v4.5.0 release upgrade line. // Put any upgrade step following this. // Automatically generated Moodle v5.0.0 release upgrade line. // Put any upgrade step following this. return true; } submission/onlinetext/db/install.xml000064400000003001152265517350013712 0ustar00
submission/onlinetext/lang/en/assignsubmission_onlinetext.php000064400000005041152265517350021050 0ustar00. /** * Strings for component 'assignsubmission_onlinetext', language 'en' * * @package assignsubmission_onlinetext * @copyright 2012 NetSpot {@link http://www.netspot.com.au} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ $string['allowonlinetextsubmissions'] = 'Enabled'; $string['default'] = 'Enabled by default'; $string['default_help'] = 'If set, this submission method will be enabled by default for all new assignments.'; $string['enabled'] = 'Online text'; $string['enabled_help'] = 'If enabled, students are able to type rich text directly into an editor field for their submission.'; $string['eventassessableuploaded'] = 'An online text has been uploaded.'; $string['nosubmission'] = 'Nothing has been submitted for this assignment'; $string['onlinetext'] = 'Online text'; $string['onlinetextfilename'] = 'onlinetext.html'; $string['onlinetextsubmission'] = 'Allow online text submission'; $string['numwords'] = '({$a} words)'; $string['pluginname'] = 'Online text submissions'; $string['privacy:metadata:assignmentid'] = 'Assignment ID'; $string['privacy:metadata:filepurpose'] = 'Files that are embedded in the text submission.'; $string['privacy:metadata:submissionpurpose'] = 'The submission ID that links to submissions for the user.'; $string['privacy:metadata:tablepurpose'] = 'Stores the text submission for each attempt.'; $string['privacy:metadata:textpurpose'] = 'The actual text submitted for this attempt of the assignment.'; $string['privacy:path'] = 'Submission Text'; $string['wordlimit'] = 'Word limit'; $string['wordlimit_help'] = 'If online text submissions are enabled, this is the maximum number of words that each student will be allowed to submit.'; $string['wordlimitexceeded'] = 'The word limit for this assignment is {$a->limit} words and you are attempting to submit {$a->count} words. Please review your submission and try again.'; submission/onlinetext/backup/moodle2/backup_assignsubmission_onlinetext_subplugin.class.php000064400000005067152265517350027064 0ustar00. /** * This file contains the class for backup of this submission plugin * * @package assignsubmission_onlinetext * @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(); /** * Provides the information to backup onlinetext submissions * * This just adds its filearea to the annotations and records the submissiontext and format * * @package assignsubmission_onlinetext * @copyright 2012 NetSpot {@link http://www.netspot.com.au} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class backup_assignsubmission_onlinetext_subplugin extends backup_subplugin { /** * Returns the subplugin information to attach to submission element * * @return backup_subplugin_element */ protected function define_submission_subplugin_structure() { // Create XML elements. $subplugin = $this->get_subplugin_element(); $subpluginwrapper = new backup_nested_element($this->get_recommended_name()); $subpluginelement = new backup_nested_element('submission_onlinetext', null, array('onlinetext', 'onlineformat', 'submission')); // Connect XML elements into the tree. $subplugin->add_child($subpluginwrapper); $subpluginwrapper->add_child($subpluginelement); // Set source to populate the data. $subpluginelement->set_source_table('assignsubmission_onlinetext', array('submission' => backup::VAR_PARENTID)); $subpluginelement->annotate_files('assignsubmission_onlinetext', 'submissions_onlinetext', 'submission'); return $subplugin; } } submission/onlinetext/backup/moodle2/restore_assignsubmission_onlinetext_subplugin.class.php000064400000005011152265517350027267 0ustar00. /** * This file contains the class for restore of this submission plugin * * @package assignsubmission_onlinetext * @copyright 2012 NetSpot {@link http://www.netspot.com.au} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ /** * Restore subplugin class. * * Provides the necessary information needed to restore * one assign_submission subplugin. * * @package assignsubmission_onlinetext * @copyright 2012 NetSpot {@link http://www.netspot.com.au} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class restore_assignsubmission_onlinetext_subplugin extends restore_subplugin { /** * Returns array the paths to be handled by the subplugin at assignment level * @return array */ protected function define_submission_subplugin_structure() { $paths = array(); $elename = $this->get_namefor('submission'); // We used get_recommended_name() so this works. $elepath = $this->get_pathfor('/submission_onlinetext'); $paths[] = new restore_path_element($elename, $elepath); return $paths; } /** * Processes one assignsubmission_onlinetext element * * @param mixed $data */ public function process_assignsubmission_onlinetext_submission($data) { global $DB; $data = (object)$data; $data->assignment = $this->get_new_parentid('assign'); $oldsubmissionid = $data->submission; // The mapping is set in the restore for the core assign activity // when a submission node is processed. $data->submission = $this->get_mappingid('submission', $data->submission); $DB->insert_record('assignsubmission_onlinetext', $data); $this->add_related_files('assignsubmission_onlinetext', 'submissions_onlinetext', 'submission', null, $oldsubmissionid); } } submission/onlinetext/lib.php000064400000006125152265517350012426 0ustar00. /** * This file contains the moodle hooks for the submission onlinetext plugin * * @package assignsubmission_onlinetext * @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(); /** * Serves assignment submissions and other files. * * @param mixed $course course or id of the course * @param mixed $cm course module or id of the course module * @param context $context * @param string $filearea * @param array $args * @param bool $forcedownload * @param array $options - List of options affecting file serving. * @return bool false if file not found, does not return if found - just send the file */ function assignsubmission_onlinetext_pluginfile($course, $cm, context $context, $filearea, $args, $forcedownload, array $options=array()) { global $DB, $CFG; if ($context->contextlevel != CONTEXT_MODULE) { return false; } require_login($course, false, $cm); $itemid = (int)array_shift($args); $record = $DB->get_record('assign_submission', array('id'=>$itemid), 'userid, assignment, groupid', MUST_EXIST); $userid = $record->userid; $groupid = $record->groupid; require_once($CFG->dirroot . '/mod/assign/locallib.php'); $assign = new assign($context, $cm, $course); if ($assign->get_instance()->id != $record->assignment) { return false; } if ($assign->get_instance()->teamsubmission && !$assign->can_view_group_submission($groupid)) { return false; } if (!$assign->get_instance()->teamsubmission && !$assign->can_view_submission($userid)) { return false; } $relativepath = implode('/', $args); $fullpath = "/{$context->id}/assignsubmission_onlinetext/$filearea/$itemid/$relativepath"; $fs = get_file_storage(); if (!($file = $fs->get_file_by_hash(sha1($fullpath))) || $file->is_directory()) { return false; } // Download MUST be forced - security! send_stored_file($file, 0, 0, true, $options); } submission/onlinetext/version.php000064400000002135152265517350013342 0ustar00. /** * This file contains the version information for the onlinetext submission plugin * * @package assignsubmission_onlinetext * @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(); $plugin->version = 2025041400; $plugin->requires = 2025040800; $plugin->component = 'assignsubmission_onlinetext'; submission/onlinetext/classes/privacy/provider.php000064400000021043152265517350016620 0ustar00. /** * Privacy class for requesting user data. * * @package assignsubmission_onlinetext * @copyright 2018 Adrian Greeve * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace assignsubmission_onlinetext\privacy; defined('MOODLE_INTERNAL') || die(); require_once($CFG->dirroot . '/mod/assign/locallib.php'); use \core_privacy\local\metadata\collection; use \core_privacy\local\request\writer; use \core_privacy\local\request\contextlist; use \mod_assign\privacy\assign_plugin_request_data; /** * Privacy class for requesting user data. * * @package assignsubmission_onlinetext * @copyright 2018 Adrian Greeve * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class provider implements \core_privacy\local\metadata\provider, \mod_assign\privacy\assignsubmission_provider, \mod_assign\privacy\assignsubmission_user_provider { /** * Return meta data about this plugin. * * @param collection $collection A list of information to add to. * @return collection Return the collection after adding to it. */ public static function get_metadata(collection $collection): collection { $detail = [ 'assignment' => 'privacy:metadata:assignmentid', 'submission' => 'privacy:metadata:submissionpurpose', 'onlinetext' => 'privacy:metadata:textpurpose' ]; $collection->add_database_table('assignsubmission_onlinetext', $detail, 'privacy:metadata:tablepurpose'); $collection->link_subsystem('core_files', 'privacy:metadata:filepurpose'); return $collection; } /** * This is covered by mod_assign provider and the query on assign_submissions. * * @param int $userid The user ID that we are finding contexts for. * @param contextlist $contextlist A context list to add sql and params to for contexts. */ public static function get_context_for_userid_within_submission(int $userid, contextlist $contextlist) { // This is already fetched from mod_assign. } /** * This is also covered by the mod_assign provider and it's queries. * * @param \mod_assign\privacy\useridlist $useridlist An object for obtaining user IDs of students. */ public static function get_student_user_ids(\mod_assign\privacy\useridlist $useridlist) { // No need. } /** * If you have tables that contain userids and you can generate entries in your tables without creating an * entry in the assign_submission table then please fill in this method. * * @param \core_privacy\local\request\userlist $userlist The userlist object */ public static function get_userids_from_context(\core_privacy\local\request\userlist $userlist) { // Not required. } /** * Export all user data for this plugin. * * @param assign_plugin_request_data $exportdata Data used to determine which context and user to export and other useful * information to help with exporting. */ public static function export_submission_user_data(assign_plugin_request_data $exportdata) { // We currently don't show submissions to teachers when exporting their data. if ($exportdata->get_user() != null) { return null; } // Retrieve text for this submission. $assign = $exportdata->get_assign(); $plugin = $assign->get_plugin_by_type('assignsubmission', 'onlinetext'); $submission = $exportdata->get_pluginobject(); $editortext = $plugin->get_editor_text('onlinetext', $submission->id); $context = $exportdata->get_context(); if (!empty($editortext)) { $submissiontext = new \stdClass(); $currentpath = $exportdata->get_subcontext(); $currentpath[] = get_string('privacy:path', 'assignsubmission_onlinetext'); $submissiontext->text = writer::with_context($context)->rewrite_pluginfile_urls($currentpath, 'assignsubmission_onlinetext', 'submissions_onlinetext', $submission->id, $editortext); writer::with_context($context) ->export_area_files($currentpath, 'assignsubmission_onlinetext', 'submissions_onlinetext', $submission->id) // Add the text to the exporter. ->export_data($currentpath, $submissiontext); // Handle plagiarism data. $coursecontext = $context->get_course_context(); $userid = $submission->userid; \core_plagiarism\privacy\provider::export_plagiarism_user_data($userid, $context, $currentpath, [ 'cmid' => $context->instanceid, 'course' => $coursecontext->instanceid, 'userid' => $userid, 'content' => $editortext, 'assignment' => $submission->assignment ]); } } /** * Any call to this method should delete all user data for the context defined in the deletion_criteria. * * @param assign_plugin_request_data $requestdata Data useful for deleting user data from this sub-plugin. */ public static function delete_submission_for_context(assign_plugin_request_data $requestdata) { global $DB; \core_plagiarism\privacy\provider::delete_plagiarism_for_context($requestdata->get_context()); // Delete related files. $fs = get_file_storage(); $fs->delete_area_files($requestdata->get_context()->id, 'assignsubmission_onlinetext', ASSIGNSUBMISSION_ONLINETEXT_FILEAREA); // Delete the records in the table. $DB->delete_records('assignsubmission_onlinetext', ['assignment' => $requestdata->get_assignid()]); } /** * A call to this method should delete user data (where practicle) from the userid and context. * * @param assign_plugin_request_data $deletedata Details about the user and context to focus the deletion. */ public static function delete_submission_for_userid(assign_plugin_request_data $deletedata) { global $DB; \core_plagiarism\privacy\provider::delete_plagiarism_for_user($deletedata->get_user()->id, $deletedata->get_context()); $submissionid = $deletedata->get_pluginobject()->id; // Delete related files. $fs = get_file_storage(); $fs->delete_area_files($deletedata->get_context()->id, 'assignsubmission_onlinetext', ASSIGNSUBMISSION_ONLINETEXT_FILEAREA, $submissionid); // Delete the records in the table. $DB->delete_records('assignsubmission_onlinetext', ['assignment' => $deletedata->get_assignid(), 'submission' => $submissionid]); } /** * Deletes all submissions for the submission ids / userids provided in a context. * assign_plugin_request_data contains: * - context * - assign object * - submission ids (pluginids) * - user ids * @param assign_plugin_request_data $deletedata A class that contains the relevant information required for deletion. */ public static function delete_submissions(assign_plugin_request_data $deletedata) { global $DB; \core_plagiarism\privacy\provider::delete_plagiarism_for_users($deletedata->get_userids(), $deletedata->get_context()); if (empty($deletedata->get_submissionids())) { return; } $fs = get_file_storage(); list($sql, $params) = $DB->get_in_or_equal($deletedata->get_submissionids(), SQL_PARAMS_NAMED); $fs->delete_area_files_select($deletedata->get_context()->id, 'assignsubmission_onlinetext', ASSIGNSUBMISSION_ONLINETEXT_FILEAREA, $sql, $params); $params['assignid'] = $deletedata->get_assignid(); $DB->delete_records_select('assignsubmission_onlinetext', "assignment = :assignid AND submission $sql", $params); } } submission/onlinetext/classes/event/submission_created.php000064400000005544152265517350020324 0ustar00. /** * The assignsubmission_onlinetext submission_created event. * * @package assignsubmission_onlinetext * @copyright 2014 Adrian Greeve * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace assignsubmission_onlinetext\event; defined('MOODLE_INTERNAL') || die(); /** * The assignsubmission_onlinetext submission_created event class. * * @property-read array $other { * Extra information about the event. * * - int onlinetextwordcount: Word count of the online text submission. * } * * @package assignsubmission_onlinetext * @since Moodle 2.7 * @copyright 2014 Adrian Greeve * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class submission_created extends \mod_assign\event\submission_created { /** * Init method. */ protected function init() { parent::init(); $this->data['objecttable'] = 'assignsubmission_onlinetext'; } /** * Returns non-localised description of what happened. * * @return string */ public function get_description() { $descriptionstring = "The user with id '$this->userid' created an online text submission with " . "'{$this->other['onlinetextwordcount']}' words in the assignment with course module id " . "'$this->contextinstanceid'"; if (!empty($this->other['groupid'])) { $descriptionstring .= " for the group with id '{$this->other['groupid']}'."; } else { $descriptionstring .= "."; } return $descriptionstring; } /** * Custom validation. * * @throws \coding_exception * @return void */ protected function validate_data() { parent::validate_data(); if (!isset($this->other['onlinetextwordcount'])) { throw new \coding_exception('The \'onlinetextwordcount\' value must be set in other.'); } } public static function get_objectid_mapping() { // No mapping available for 'assignsubmission_onlinetext'. return array('db' => 'assignsubmission_onlinetext', 'restore' => \core\event\base::NOT_MAPPED); } } submission/onlinetext/classes/event/submission_updated.php000064400000005544152265517350020343 0ustar00. /** * The assignsubmission_onlinetext submission_updated event. * * @package assignsubmission_onlinetext * @copyright 2014 Adrian Greeve * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace assignsubmission_onlinetext\event; defined('MOODLE_INTERNAL') || die(); /** * The assignsubmission_onlinetext submission_updated event class. * * @property-read array $other { * Extra information about the event. * * - int onlinetextwordcount: Word count of the online text submission. * } * * @package assignsubmission_onlinetext * @since Moodle 2.7 * @copyright 2014 Adrian Greeve * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class submission_updated extends \mod_assign\event\submission_updated { /** * Init method. */ protected function init() { parent::init(); $this->data['objecttable'] = 'assignsubmission_onlinetext'; } /** * Returns non-localised description of what happened. * * @return string */ public function get_description() { $descriptionstring = "The user with id '$this->userid' updated an online text submission with " . "'{$this->other['onlinetextwordcount']}' words in the assignment with course module id " . "'$this->contextinstanceid'"; if (!empty($this->other['groupid'])) { $descriptionstring .= " for the group with id '{$this->other['groupid']}'."; } else { $descriptionstring .= "."; } return $descriptionstring; } /** * Custom validation. * * @throws \coding_exception * @return void */ protected function validate_data() { parent::validate_data(); if (!isset($this->other['onlinetextwordcount'])) { throw new \coding_exception('The \'onlinetextwordcount\' value must be set in other.'); } } public static function get_objectid_mapping() { // No mapping available for 'assignsubmission_onlinetext'. return array('db' => 'assignsubmission_onlinetext', 'restore' => \core\event\base::NOT_MAPPED); } } submission/onlinetext/classes/event/assessable_uploaded.php000064400000005010152265517350020430 0ustar00. /** * The assignsubmission_onlinetext assessable uploaded event. * * @package assignsubmission_onlinetext * @copyright 2013 Frédéric Massart * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace assignsubmission_onlinetext\event; defined('MOODLE_INTERNAL') || die(); /** * The assignsubmission_onlinetext assessable uploaded event class. * * @property-read array $other { * Extra information about event. * * - string format: (optional) content format. * } * * @package assignsubmission_onlinetext * @since Moodle 2.6 * @copyright 2013 Frédéric Massart * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class assessable_uploaded extends \core\event\assessable_uploaded { /** * Returns description of what happened. * * @return string */ public function get_description() { return "The user with id '$this->userid' has saved an online text submission with id '$this->objectid' " . "in the assignment activity with course module id '$this->contextinstanceid'."; } /** * Return localised event name. * * @return string */ public static function get_name() { return get_string('eventassessableuploaded', 'assignsubmission_onlinetext'); } /** * Get URL related to the action. * * @return \moodle_url */ public function get_url() { return new \moodle_url('/mod/assign/view.php', array('id' => $this->contextinstanceid)); } /** * Init method. * * @return void */ protected function init() { parent::init(); $this->data['objecttable'] = 'assign_submission'; } public static function get_objectid_mapping() { return array('db' => 'assign_submission', 'restore' => 'submission'); } } submission/onlinetext/templates/email_summary_text.mustache000064400000002306152265517350020605 0ustar00{{! This file is part of Moodle - http://moodle.org/ Moodle is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Moodle is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Moodle. If not, see . }} {{! @template assignsubmission_onlinetext/email_summary_text This template is used render the plain text version of the summary of online text submissions to go into emails. Classes required for JS: * none Context variables required for this template: * wordcount - Word count of the student's submission. Example context (json): { "wordcount": 123 } }} {{#str}}onlinetext, assignsubmission_onlinetext{{/str}} {{#str}}numwords, assignsubmission_onlinetext, {{wordcount}}{{/str}} submission/onlinetext/templates/email_summary_html.mustache000064400000002320152265517350020561 0ustar00{{! This file is part of Moodle - http://moodle.org/ Moodle is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Moodle is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Moodle. If not, see . }} {{! @template assignsubmission_onlinetext/email_summary_html This template is used render the HTML version of the summary of online text submissions to go into emails. Classes required for JS: * none Context variables required for this template: * wordcount - Word count of the student's submission. Example context (json): { "wordcount": 123 } }}

{{#str}}onlinetext, assignsubmission_onlinetext{{/str}}

{{#str}}numwords, assignsubmission_onlinetext, {{wordcount}}{{/str}}

submission/onlinetext/locallib.php000064400000071750152265517350013447 0ustar00. /** * This file contains the definition for the library class for onlinetext submission plugin * * This class provides all the functionality for the new assign module. * * @package assignsubmission_onlinetext * @copyright 2012 NetSpot {@link http://www.netspot.com.au} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ use core_external\external_single_structure; use core_external\external_value; defined('MOODLE_INTERNAL') || die(); // File area for online text submission assignment. define('ASSIGNSUBMISSION_ONLINETEXT_FILEAREA', 'submissions_onlinetext'); /** * library class for onlinetext submission plugin extending submission plugin base class * * @package assignsubmission_onlinetext * @copyright 2012 NetSpot {@link http://www.netspot.com.au} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class assign_submission_onlinetext extends assign_submission_plugin { /** * Get the name of the online text submission plugin * @return string */ public function get_name() { return get_string('onlinetext', 'assignsubmission_onlinetext'); } /** * Get onlinetext submission information from the database * * @param int $submissionid * @return mixed */ private function get_onlinetext_submission($submissionid) { global $DB; return $DB->get_record('assignsubmission_onlinetext', array('submission'=>$submissionid)); } /** * Remove a submission. * * @param stdClass $submission The submission * @return boolean */ public function remove(stdClass $submission) { global $DB; $submissionid = $submission ? $submission->id : 0; if ($submissionid) { $DB->delete_records('assignsubmission_onlinetext', array('submission' => $submissionid)); } return true; } /** * Get the settings for onlinetext submission plugin * * @param MoodleQuickForm $mform The form to add elements to * @return void */ public function get_settings(MoodleQuickForm $mform) { global $CFG, $COURSE; $defaultwordlimit = $this->get_config('wordlimit') == 0 ? '' : $this->get_config('wordlimit'); $defaultwordlimitenabled = $this->get_config('wordlimitenabled'); $options = array('size' => '6', 'maxlength' => '6'); $name = get_string('wordlimit', 'assignsubmission_onlinetext'); // Create a text box that can be enabled/disabled for onlinetext word limit. $wordlimitgrp = array(); $wordlimitgrp[] = $mform->createElement('text', 'assignsubmission_onlinetext_wordlimit', '', $options); $wordlimitgrp[] = $mform->createElement('checkbox', 'assignsubmission_onlinetext_wordlimit_enabled', '', get_string('enable')); $mform->addGroup($wordlimitgrp, 'assignsubmission_onlinetext_wordlimit_group', $name, ' ', false); $mform->addHelpButton('assignsubmission_onlinetext_wordlimit_group', 'wordlimit', 'assignsubmission_onlinetext'); $mform->disabledIf('assignsubmission_onlinetext_wordlimit', 'assignsubmission_onlinetext_wordlimit_enabled', 'notchecked'); $mform->hideIf('assignsubmission_onlinetext_wordlimit', 'assignsubmission_onlinetext_enabled', 'notchecked'); // Add numeric rule to text field. $wordlimitgrprules = array(); $wordlimitgrprules['assignsubmission_onlinetext_wordlimit'][] = array(null, 'numeric', null, 'client'); $mform->addGroupRule('assignsubmission_onlinetext_wordlimit_group', $wordlimitgrprules); // Rest of group setup. $mform->setDefault('assignsubmission_onlinetext_wordlimit', $defaultwordlimit); $mform->setDefault('assignsubmission_onlinetext_wordlimit_enabled', $defaultwordlimitenabled); $mform->setType('assignsubmission_onlinetext_wordlimit', PARAM_INT); $mform->hideIf('assignsubmission_onlinetext_wordlimit_group', 'assignsubmission_onlinetext_enabled', 'notchecked'); } /** * Save the settings for onlinetext submission plugin * * @param stdClass $data * @return bool */ public function save_settings(stdClass $data) { if (empty($data->assignsubmission_onlinetext_wordlimit) || empty($data->assignsubmission_onlinetext_wordlimit_enabled)) { $wordlimit = 0; $wordlimitenabled = 0; } else { $wordlimit = $data->assignsubmission_onlinetext_wordlimit; $wordlimitenabled = 1; } $this->set_config('wordlimit', $wordlimit); $this->set_config('wordlimitenabled', $wordlimitenabled); return true; } /** * Add form elements for settings * * @param mixed $submission can be null * @param MoodleQuickForm $mform * @param stdClass $data * @return true if elements were added to the form */ public function get_form_elements($submission, MoodleQuickForm $mform, stdClass $data) { $elements = array(); $editoroptions = $this->get_edit_options(); $submissionid = $submission ? $submission->id : 0; if (!isset($data->onlinetext)) { $data->onlinetext = ''; } if (!isset($data->onlinetextformat)) { $data->onlinetextformat = editors_get_preferred_format(); } if ($submission) { $onlinetextsubmission = $this->get_onlinetext_submission($submission->id); if ($onlinetextsubmission) { $data->onlinetext = $onlinetextsubmission->onlinetext; $data->onlinetextformat = $onlinetextsubmission->onlineformat; } } $data = file_prepare_standard_editor($data, 'onlinetext', $editoroptions, $this->assignment->get_context(), 'assignsubmission_onlinetext', ASSIGNSUBMISSION_ONLINETEXT_FILEAREA, $submissionid); $mform->addElement('editor', 'onlinetext_editor', $this->get_name(), null, $editoroptions); return true; } /** * Editor format options * * @return array */ private function get_edit_options() { $editoroptions = array( 'noclean' => false, 'maxfiles' => EDITOR_UNLIMITED_FILES, 'maxbytes' => $this->assignment->get_course()->maxbytes, 'context' => $this->assignment->get_context(), 'return_types' => (FILE_INTERNAL | FILE_EXTERNAL | FILE_CONTROLLED_LINK), 'removeorphaneddrafts' => true // Whether or not to remove any draft files which aren't referenced in the text. ); return $editoroptions; } /** * Save data to the database and trigger plagiarism plugin, * if enabled, to scan the uploaded content via events trigger * * @param stdClass $submission * @param stdClass $data * @return bool */ public function save(stdClass $submission, stdClass $data) { global $USER, $DB; $editoroptions = $this->get_edit_options(); $data = file_postupdate_standard_editor($data, 'onlinetext', $editoroptions, $this->assignment->get_context(), 'assignsubmission_onlinetext', ASSIGNSUBMISSION_ONLINETEXT_FILEAREA, $submission->id); $onlinetextsubmission = $this->get_onlinetext_submission($submission->id); $fs = get_file_storage(); $files = $fs->get_area_files($this->assignment->get_context()->id, 'assignsubmission_onlinetext', ASSIGNSUBMISSION_ONLINETEXT_FILEAREA, $submission->id, 'id', false); // Check word count before submitting anything. $exceeded = $this->check_word_count(trim($data->onlinetext)); if ($exceeded) { $this->set_error($exceeded); return false; } $params = array( 'context' => context_module::instance($this->assignment->get_course_module()->id), 'courseid' => $this->assignment->get_course()->id, 'objectid' => $submission->id, 'other' => array( 'pathnamehashes' => array_keys($files), 'content' => trim($data->onlinetext), 'format' => $data->onlinetext_editor['format'] ) ); if (!empty($submission->userid) && ($submission->userid != $USER->id)) { $params['relateduserid'] = $submission->userid; } if ($this->assignment->is_blind_marking()) { $params['anonymous'] = 1; } $event = \assignsubmission_onlinetext\event\assessable_uploaded::create($params); $event->trigger(); $groupname = null; $groupid = 0; // Get the group name as other fields are not transcribed in the logs and this information is important. if (empty($submission->userid) && !empty($submission->groupid)) { $groupname = $DB->get_field('groups', 'name', array('id' => $submission->groupid), MUST_EXIST); $groupid = $submission->groupid; } else { $params['relateduserid'] = $submission->userid; } $count = count_words($data->onlinetext); // Unset the objectid and other field from params for use in submission events. unset($params['objectid']); unset($params['other']); $params['other'] = array( 'submissionid' => $submission->id, 'submissionattempt' => $submission->attemptnumber, 'submissionstatus' => $submission->status, 'onlinetextwordcount' => $count, 'groupid' => $groupid, 'groupname' => $groupname ); if ($onlinetextsubmission) { $onlinetextsubmission->onlinetext = $data->onlinetext; $onlinetextsubmission->onlineformat = $data->onlinetext_editor['format']; $params['objectid'] = $onlinetextsubmission->id; $updatestatus = $DB->update_record('assignsubmission_onlinetext', $onlinetextsubmission); $event = \assignsubmission_onlinetext\event\submission_updated::create($params); $event->set_assign($this->assignment); $event->trigger(); return $updatestatus; } else { $onlinetextsubmission = new stdClass(); $onlinetextsubmission->onlinetext = $data->onlinetext; $onlinetextsubmission->onlineformat = $data->onlinetext_editor['format']; $onlinetextsubmission->submission = $submission->id; $onlinetextsubmission->assignment = $this->assignment->get_instance()->id; $onlinetextsubmission->id = $DB->insert_record('assignsubmission_onlinetext', $onlinetextsubmission); $params['objectid'] = $onlinetextsubmission->id; $event = \assignsubmission_onlinetext\event\submission_created::create($params); $event->set_assign($this->assignment); $event->trigger(); return $onlinetextsubmission->id > 0; } } /** * Return a list of the text fields that can be imported/exported by this plugin * * @return array An array of field names and descriptions. (name=>description, ...) */ public function get_editor_fields() { return array('onlinetext' => get_string('pluginname', 'assignsubmission_onlinetext')); } /** * Get the saved text content from the editor * * @param string $name * @param int $submissionid * @return string */ public function get_editor_text($name, $submissionid) { if ($name == 'onlinetext') { $onlinetextsubmission = $this->get_onlinetext_submission($submissionid); if ($onlinetextsubmission) { return $onlinetextsubmission->onlinetext; } } return ''; } /** * Get the content format for the editor * * @param string $name * @param int $submissionid * @return int */ public function get_editor_format($name, $submissionid) { if ($name == 'onlinetext') { $onlinetextsubmission = $this->get_onlinetext_submission($submissionid); if ($onlinetextsubmission) { return $onlinetextsubmission->onlineformat; } } return 0; } /** * Display onlinetext word count in the submission status table * * @param stdClass $submission * @param bool $showviewlink - If the summary has been truncated set this to true * @return string */ public function view_summary(stdClass $submission, & $showviewlink) { global $CFG; $onlinetextsubmission = $this->get_onlinetext_submission($submission->id); // Always show the view link. $showviewlink = true; if ($onlinetextsubmission) { // This contains the shortened version of the text plus an optional 'Export to portfolio' button. $text = $this->assignment->render_editor_content(ASSIGNSUBMISSION_ONLINETEXT_FILEAREA, $onlinetextsubmission->submission, $this->get_type(), 'onlinetext', 'assignsubmission_onlinetext', true); // The actual submission text. $onlinetext = trim($onlinetextsubmission->onlinetext); // The shortened version of the submission text. $shorttext = shorten_text($onlinetext, 140); $plagiarismlinks = ''; if (!empty($CFG->enableplagiarism)) { require_once($CFG->libdir . '/plagiarismlib.php'); $plagiarismlinks .= plagiarism_get_links(array('userid' => $submission->userid, 'content' => $onlinetext, 'cmid' => $this->assignment->get_course_module()->id, 'course' => $this->assignment->get_course()->id, 'assignment' => $submission->assignment)); } // We compare the actual text submission and the shortened version. If they are not equal, we show the word count. if ($onlinetext != $shorttext) { $wordcount = get_string('numwords', 'assignsubmission_onlinetext', count_words($onlinetext)); return $plagiarismlinks . $wordcount . $text; } else { return $plagiarismlinks . $text; } } return ''; } #[\Override] public function submission_summary_for_messages(stdClass $submission): array { global $PAGE; $onlinetextsubmission = $this->get_onlinetext_submission($submission->id); if (!$onlinetextsubmission || !$onlinetextsubmission->onlinetext) { return ['', '']; } $renderer = $PAGE->get_renderer('mod_assign'); $templatecontext = ['wordcount' => count_words($onlinetextsubmission->onlinetext)]; return [ // Mustache strips off all trailing whitespace, but we want a newline at the end. $renderer->render_from_template( 'assignsubmission_onlinetext/email_summary_text', $templatecontext) . "\n", $renderer->render_from_template( 'assignsubmission_onlinetext/email_summary_html', $templatecontext), ]; } /** * Produce a list of files suitable for export that represent this submission. * * @param stdClass $submission - For this is the submission data * @param stdClass $user - This is the user record for this submission * @return array - return an array of files indexed by filename */ public function get_files(stdClass $submission, stdClass $user) { global $DB; $files = array(); $onlinetextsubmission = $this->get_onlinetext_submission($submission->id); // Note that this check is the same logic as the result from the is_empty function but we do // not call it directly because we already have the submission record. if ($onlinetextsubmission && !html_is_blank($onlinetextsubmission->onlinetext)) { // Do not pass the text through format_text. The result may not be displayed in Moodle and // may be passed to external services such as document conversion or portfolios. $formattedtext = $this->assignment->download_rewrite_pluginfile_urls($onlinetextsubmission->onlinetext, $user, $this); $head = ''; $submissioncontent = '' . $head . ''. $formattedtext . ''; $filename = get_string('onlinetextfilename', 'assignsubmission_onlinetext'); $files[$filename] = array($submissioncontent); $fs = get_file_storage(); $fsfiles = $fs->get_area_files($this->assignment->get_context()->id, 'assignsubmission_onlinetext', ASSIGNSUBMISSION_ONLINETEXT_FILEAREA, $submission->id, 'timemodified', false); foreach ($fsfiles as $file) { $files[$file->get_filename()] = $file; } } return $files; } /** * Display the saved text content from the editor in the view table * * @param stdClass $submission * @return string */ public function view(stdClass $submission) { global $CFG; $result = ''; $plagiarismlinks = ''; $onlinetextsubmission = $this->get_onlinetext_submission($submission->id); if ($onlinetextsubmission) { // Render for portfolio API. $result .= $this->assignment->render_editor_content(ASSIGNSUBMISSION_ONLINETEXT_FILEAREA, $onlinetextsubmission->submission, $this->get_type(), 'onlinetext', 'assignsubmission_onlinetext'); if (!empty($CFG->enableplagiarism)) { require_once($CFG->libdir . '/plagiarismlib.php'); $plagiarismlinks .= plagiarism_get_links(array('userid' => $submission->userid, 'content' => trim($onlinetextsubmission->onlinetext), 'cmid' => $this->assignment->get_course_module()->id, 'course' => $this->assignment->get_course()->id, 'assignment' => $submission->assignment)); } } return $plagiarismlinks . $result; } /** * Return true if this plugin can upgrade an old Moodle 2.2 assignment of this type and version. * * @param string $type old assignment subtype * @param int $version old assignment version * @return bool True if upgrade is possible */ public function can_upgrade($type, $version) { if ($type == 'online' && $version >= 2011112900) { return true; } return false; } /** * Upgrade the settings from the old assignment to the new plugin based one * * @param context $oldcontext - the database for the old assignment context * @param stdClass $oldassignment - the database for the old assignment instance * @param string $log record log events here * @return bool Was it a success? */ public function upgrade_settings(context $oldcontext, stdClass $oldassignment, & $log) { // No settings to upgrade. return true; } /** * Upgrade the submission from the old assignment to the new one * * @param context $oldcontext - the database for the old assignment context * @param stdClass $oldassignment The data record for the old assignment * @param stdClass $oldsubmission The data record for the old submission * @param stdClass $submission The data record for the new submission * @param string $log Record upgrade messages in the log * @return bool true or false - false will trigger a rollback */ public function upgrade(context $oldcontext, stdClass $oldassignment, stdClass $oldsubmission, stdClass $submission, & $log) { global $DB; $onlinetextsubmission = new stdClass(); $onlinetextsubmission->onlinetext = $oldsubmission->data1; $onlinetextsubmission->onlineformat = $oldsubmission->data2; $onlinetextsubmission->submission = $submission->id; $onlinetextsubmission->assignment = $this->assignment->get_instance()->id; if ($onlinetextsubmission->onlinetext === null) { $onlinetextsubmission->onlinetext = ''; } if ($onlinetextsubmission->onlineformat === null) { $onlinetextsubmission->onlineformat = editors_get_preferred_format(); } if (!$DB->insert_record('assignsubmission_onlinetext', $onlinetextsubmission) > 0) { $log .= get_string('couldnotconvertsubmission', 'mod_assign', $submission->userid); return false; } // Now copy the area files. $this->assignment->copy_area_files_for_upgrade($oldcontext->id, 'mod_assignment', 'submission', $oldsubmission->id, $this->assignment->get_context()->id, 'assignsubmission_onlinetext', ASSIGNSUBMISSION_ONLINETEXT_FILEAREA, $submission->id); return true; } /** * The assignment has been deleted - cleanup * * @return bool */ public function delete_instance() { global $DB; $DB->delete_records('assignsubmission_onlinetext', array('assignment'=>$this->assignment->get_instance()->id)); return true; } /** * No text is set for this plugin * * @param stdClass $submission * @return bool */ public function is_empty(stdClass $submission) { $onlinetextsubmission = $this->get_onlinetext_submission($submission->id); $wordcount = 0; $hasinsertedresources = false; if (isset($onlinetextsubmission->onlinetext)) { $wordcount = count_words(trim($onlinetextsubmission->onlinetext)); // Check if the online text submission contains video, audio or image elements // that can be ignored and stripped by count_words(). $hasinsertedresources = preg_match('/<\s*((video|audio)[^>]*>(.*?)<\s*\/\s*(video|audio)>)|(img[^>]*>(.*?))/', trim($onlinetextsubmission->onlinetext)); } return $wordcount == 0 && !$hasinsertedresources; } /** * Determine if a submission is empty * * This is distinct from is_empty in that it is intended to be used to * determine if a submission made before saving is empty. * * @param stdClass $data The submission data * @return bool */ public function submission_is_empty(stdClass $data) { if (!isset($data->onlinetext_editor)) { return true; } $wordcount = 0; $hasinsertedresources = false; if (isset($data->onlinetext_editor['text'])) { $wordcount = count_words(trim((string)$data->onlinetext_editor['text'])); // Check if the online text submission contains video, audio or image elements // that can be ignored and stripped by count_words(). $hasinsertedresources = preg_match('/<\s*((video|audio)[^>]*>(.*?)<\s*\/\s*(video|audio)>)|(img[^>]*>(.*?))/', trim((string)$data->onlinetext_editor['text'])); } return $wordcount == 0 && !$hasinsertedresources; } /** * Get file areas returns a list of areas this plugin stores files * @return array - An array of fileareas (keys) and descriptions (values) */ public function get_file_areas() { return array(ASSIGNSUBMISSION_ONLINETEXT_FILEAREA=>$this->get_name()); } /** * Copy the student's submission from a previous submission. Used when a student opts to base their resubmission * on the last submission. * @param stdClass $sourcesubmission * @param stdClass $destsubmission */ public function copy_submission(stdClass $sourcesubmission, stdClass $destsubmission) { global $DB; // Copy the files across (attached via the text editor). $contextid = $this->assignment->get_context()->id; $fs = get_file_storage(); $files = $fs->get_area_files($contextid, 'assignsubmission_onlinetext', ASSIGNSUBMISSION_ONLINETEXT_FILEAREA, $sourcesubmission->id, 'id', false); foreach ($files as $file) { $fieldupdates = array('itemid' => $destsubmission->id); $fs->create_file_from_storedfile($fieldupdates, $file); } // Copy the assignsubmission_onlinetext record. $onlinetextsubmission = $this->get_onlinetext_submission($sourcesubmission->id); if ($onlinetextsubmission) { unset($onlinetextsubmission->id); $onlinetextsubmission->submission = $destsubmission->id; $DB->insert_record('assignsubmission_onlinetext', $onlinetextsubmission); } return true; } /** * Return a description of external params suitable for uploading an onlinetext submission from a webservice. * * @return \core_external\external_description|null */ public function get_external_parameters() { $editorparams = array('text' => new external_value(PARAM_RAW, 'The text for this submission.'), 'format' => new external_value(PARAM_INT, 'The format for this submission'), 'itemid' => new external_value(PARAM_INT, 'The draft area id for files attached to the submission')); $editorstructure = new external_single_structure($editorparams, 'Editor structure', VALUE_OPTIONAL); return array('onlinetext_editor' => $editorstructure); } /** * Compare word count of onlinetext submission to word limit, and return result. * * @param string $submissiontext Onlinetext submission text from editor * @return string Error message if limit is enabled and exceeded, otherwise null */ public function check_word_count($submissiontext) { global $OUTPUT; $wordlimitenabled = $this->get_config('wordlimitenabled'); $wordlimit = $this->get_config('wordlimit'); if ($wordlimitenabled == 0) { return null; } // Count words and compare to limit. $wordcount = count_words($submissiontext); if ($wordcount <= $wordlimit) { return null; } else { $errormsg = get_string('wordlimitexceeded', 'assignsubmission_onlinetext', array('limit' => $wordlimit, 'count' => $wordcount)); return $OUTPUT->error_text($errormsg); } } /** * Return the plugin configs for external functions. * * @return array the list of settings * @since Moodle 3.2 */ public function get_config_for_external() { return (array) $this->get_config(); } } submission/onlinetext/tests/generator/lib.php000064400000003660152265517350015557 0ustar00. require_once("{$CFG->dirroot}/mod/assign/tests/generator/assignsubmission_subplugin_generator.php"); /** * Online Text assignment submission subplugin data generator. * * @package assignsubmission_onlinetext * @category test * @copyright 2021 Andrew Lyons * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class assignsubmission_onlinetext_generator extends assignsubmission_subplugin_generator { /** * Add submission data in the correct format for a call to `assign::save_submission()` from a table containing * submission data for a single activity. * * Data should be added to the $submission object passed into the function. * * @param stdClass $submission The submission record to be modified * @param assign $assign The assignment being submitted to * @param array $data The data received */ public function add_submission_data(stdClass $submission, assign $assign, array $data): void { if (array_key_exists('onlinetext', $data)) { $submission->onlinetext_editor = [ 'itemid' => file_get_unused_draft_itemid(), 'text' => $data['onlinetext'], 'format' => FORMAT_HTML, ]; } } } submission/onlinetext/tests/privacy/provider_test.php000064400000022705152265517350017372 0ustar00. namespace assignsubmission_onlinetext\privacy; /** * Unit tests for mod/assign/submission/onlinetext/classes/privacy/provider. * * @copyright 2018 Adrian Greeve * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @package assignsubmission_onlinetext * @covers \assignsubmission_onlinetext\privacy\provider */ final class provider_test extends \mod_assign\tests\provider_testcase { /** * Convenience function for creating feedback data. * * @param object $assign assign object * @param stdClass $student user object * @param string $text Submission text. * @return array Submission plugin object and the submission object. */ protected function create_online_submission($assign, $student, $text) { global $CFG; $this->setUser($student->id); $submission = $assign->get_user_submission($student->id, true); $data = new \stdClass(); $data->onlinetext_editor = array( 'itemid' => file_get_unused_draft_itemid(), 'text' => $text, 'format' => FORMAT_PLAIN ); $submission = $assign->get_user_submission($student->id, true); $plugin = $assign->get_submission_plugin_by_type('onlinetext'); $plugin->save($submission, $data); return [$plugin, $submission]; } /** * Quick test to make sure that get_metadata returns something. */ public function test_get_metadata(): void { $collection = new \core_privacy\local\metadata\collection('assignsubmission_onlinetext'); $collection = \assignsubmission_onlinetext\privacy\provider::get_metadata($collection); $this->assertNotEmpty($collection); } /** * Test that submission files and text are exported for a user. */ public function test_export_submission_user_data(): void { $this->resetAfterTest(); // Create course, assignment, submission, and then a feedback comment. $course = $this->getDataGenerator()->create_course(); // Student. $user1 = $this->getDataGenerator()->create_user(); $this->getDataGenerator()->enrol_user($user1->id, $course->id, 'student'); $assign = $this->create_instance(['course' => $course]); $context = $assign->get_context(); $submissiontext = 'Just some text'; list($plugin, $submission) = $this->create_online_submission($assign, $user1, $submissiontext); $writer = \core_privacy\local\request\writer::with_context($context); $this->assertFalse($writer->has_any_data()); // The student should have some text submitted. $exportdata = new \mod_assign\privacy\assign_plugin_request_data($context, $assign, $submission, ['Attempt 1']); \assignsubmission_onlinetext\privacy\provider::export_submission_user_data($exportdata); $this->assertEquals($submissiontext, $writer->get_data(['Attempt 1', get_string('privacy:path', 'assignsubmission_onlinetext')])->text); } /** * Test that all submission files are deleted for this context. */ public function test_delete_submission_for_context(): void { $this->resetAfterTest(); // Create course, assignment, submission, and then a feedback comment. $course = $this->getDataGenerator()->create_course(); // Student. $user1 = $this->getDataGenerator()->create_user(); $user2 = $this->getDataGenerator()->create_user(); $this->getDataGenerator()->enrol_user($user1->id, $course->id, 'student'); $this->getDataGenerator()->enrol_user($user2->id, $course->id, 'student'); $assign = $this->create_instance(['course' => $course]); $context = $assign->get_context(); $studenttext = 'Student one\'s text.'; list($plugin, $submission) = $this->create_online_submission($assign, $user1, $studenttext); $studenttext2 = 'Student two\'s text.'; list($plugin2, $submission2) = $this->create_online_submission($assign, $user2, $studenttext2); // Only need the context and assign object in this plugin for this operation. $requestdata = new \mod_assign\privacy\assign_plugin_request_data($context, $assign); \assignsubmission_onlinetext\privacy\provider::delete_submission_for_context($requestdata); // This checks that there is no content for these submissions. $this->assertTrue($plugin->is_empty($submission)); $this->assertTrue($plugin2->is_empty($submission2)); } /** * Test that the comments for a user are deleted. */ public function test_delete_submission_for_userid(): void { $this->resetAfterTest(); // Create course, assignment, submission, and then a feedback comment. $course = $this->getDataGenerator()->create_course(); // Student. $user1 = $this->getDataGenerator()->create_user(); $user2 = $this->getDataGenerator()->create_user(); $this->getDataGenerator()->enrol_user($user1->id, $course->id, 'student'); $this->getDataGenerator()->enrol_user($user2->id, $course->id, 'student'); $assign = $this->create_instance(['course' => $course]); $context = $assign->get_context(); $studenttext = 'Student one\'s text.'; list($plugin, $submission) = $this->create_online_submission($assign, $user1, $studenttext); $studenttext2 = 'Student two\'s text.'; list($plugin2, $submission2) = $this->create_online_submission($assign, $user2, $studenttext2); // Need more data for this operation. $requestdata = new \mod_assign\privacy\assign_plugin_request_data($context, $assign, $submission, [], $user1); \assignsubmission_onlinetext\privacy\provider::delete_submission_for_userid($requestdata); // This checks that there is no content for the first submission. $this->assertTrue($plugin->is_empty($submission)); // But there is for the second submission. $this->assertFalse($plugin2->is_empty($submission2)); } public function test_delete_submissions(): void { global $DB; $this->resetAfterTest(); $course = $this->getDataGenerator()->create_course(); $user1 = $this->getDataGenerator()->create_user(); $user2 = $this->getDataGenerator()->create_user(); $user3 = $this->getDataGenerator()->create_user(); // Only makes submissions in the second assignment. $user4 = $this->getDataGenerator()->create_user(); $this->getDataGenerator()->enrol_user($user1->id, $course->id, 'student'); $this->getDataGenerator()->enrol_user($user2->id, $course->id, 'student'); $this->getDataGenerator()->enrol_user($user3->id, $course->id, 'student'); $this->getDataGenerator()->enrol_user($user4->id, $course->id, 'student'); $assign1 = $this->create_instance(['course' => $course]); $assign2 = $this->create_instance(['course' => $course]); $context1 = $assign1->get_context(); $context2 = $assign2->get_context(); $student1text = 'Student one\'s text.'; list($plugin1, $submission1) = $this->create_online_submission($assign1, $user1, $student1text); $student2text = 'Student two\'s text.'; list($plugin2, $submission2) = $this->create_online_submission($assign1, $user2, $student2text); $student3text = 'Student two\'s text.'; list($plugin3, $submission3) = $this->create_online_submission($assign1, $user3, $student3text); // Now for submissions in assignment two. $student3text2 = 'Student two\'s text for the second assignment.'; list($plugin4, $submission4) = $this->create_online_submission($assign2, $user3, $student3text2); $student4text = 'Student four\'s text.'; list($plugin5, $submission5) = $this->create_online_submission($assign2, $user4, $student4text); $data = $DB->get_records('assignsubmission_onlinetext', ['assignment' => $assign1->get_instance()->id]); $this->assertCount(3, $data); // Delete the submissions for user 1 and 3. $requestdata = new \mod_assign\privacy\assign_plugin_request_data($context1, $assign1); $requestdata->set_userids([$user1->id, $user2->id]); $requestdata->populate_submissions_and_grades(); \assignsubmission_onlinetext\privacy\provider::delete_submissions($requestdata); // There should only be one record left for assignment one. $data = $DB->get_records('assignsubmission_onlinetext', ['assignment' => $assign1->get_instance()->id]); $this->assertCount(1, $data); // Check that the second assignment has not been touched. $data = $DB->get_records('assignsubmission_onlinetext', ['assignment' => $assign2->get_instance()->id]); $this->assertCount(2, $data); } } submission/onlinetext/tests/locallib_test.php000064400000007661152265517350015650 0ustar00. /** * Tests for mod/assign/submission/onlinetext/locallib.php * * @package assignsubmission_onlinetext * @copyright 2016 Cameron Ball * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace assignsubmission_onlinetext; use mod_assign_test_generator; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->dirroot . '/mod/assign/tests/generator.php'); /** * Unit tests for mod/assign/submission/onlinetext/locallib.php * * @copyright 2016 Cameron Ball * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class locallib_test extends \advanced_testcase { // Use the generator helper. use mod_assign_test_generator; /** * Test submission_is_empty * * @dataProvider submission_is_empty_testcases * @param string $submissiontext The online text submission text * @param bool $expected The expected return value */ public function test_submission_is_empty($submissiontext, $expected): void { $this->resetAfterTest(); $course = $this->getDataGenerator()->create_course(); $student = $this->getDataGenerator()->create_and_enrol($course, 'student'); $assign = $this->create_instance($course, [ 'assignsubmission_onlinetext_enabled' => true, ]); $this->setUser($student->id); $plugin = $assign->get_submission_plugin_by_type('onlinetext'); $result = $plugin->submission_is_empty((object) [ 'onlinetext_editor' => [ 'text' => $submissiontext, ], ]); $this->assertTrue($result === $expected); } /** * Test new_submission_empty * * @dataProvider submission_is_empty_testcases * @param string $submissiontext The file submission data * @param bool $expected The expected return value */ public function test_new_submission_empty($submissiontext, $expected): void { $this->resetAfterTest(); $course = $this->getDataGenerator()->create_course(); $student = $this->getDataGenerator()->create_and_enrol($course, 'student'); $assign = $this->create_instance($course, [ 'assignsubmission_onlinetext_enabled' => true, ]); $this->setUser($student->id); $result = $assign->new_submission_empty((object) [ 'onlinetext_editor' => [ 'text' => $submissiontext, ], ]); $this->assertTrue($result === $expected); } /** * Dataprovider for the test_submission_is_empty testcase * * @return array of testcases */ public static function submission_is_empty_testcases(): array { return [ 'Empty submission string' => ['', true], 'Empty submission null' => [null, true], 'Value 0' => [0, false], 'String 0' => ['0', false], 'Text' => ['Ai! laurië lantar lassi súrinen, yéni únótimë ve rámar aldaron!', false], 'Image' => ['', false], 'Video' => ['', false], 'Audio' => ['', false], ]; } } submission/onlinetext/tests/event/events_test.php000064400000014513152265517350016506 0ustar00. /** * Contains the event tests for the plugin. * * @package assignsubmission_onlinetext * @copyright 2013 Frédéric Massart * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace assignsubmission_onlinetext\event; use mod_assign_test_generator; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->dirroot . '/mod/assign/tests/generator.php'); final class events_test extends \advanced_testcase { // Use the generator helper. use mod_assign_test_generator; /** * Test that the assessable_uploaded event is fired when an online text submission is saved. */ public function test_assessable_uploaded(): void { $this->resetAfterTest(); $course = $this->getDataGenerator()->create_course(); $student = $this->getDataGenerator()->create_and_enrol($course, 'student'); $assign = $this->create_instance($course); $context = $assign->get_context(); $cm = $assign->get_course_module(); $this->setUser($student->id); $submission = $assign->get_user_submission($student->id, true); $data = (object) [ 'onlinetext_editor' => [ 'itemid' => file_get_unused_draft_itemid(), 'text' => 'Submission text', 'format' => FORMAT_PLAIN, ], ]; $sink = $this->redirectEvents(); $plugin = $assign->get_submission_plugin_by_type('onlinetext'); $plugin->save($submission, $data); $events = $sink->get_events(); $this->assertCount(2, $events); $event = reset($events); $this->assertInstanceOf('\assignsubmission_onlinetext\event\assessable_uploaded', $event); $this->assertEquals($context->id, $event->contextid); $this->assertEquals($submission->id, $event->objectid); $this->assertEquals(array(), $event->other['pathnamehashes']); $this->assertEquals(FORMAT_PLAIN, $event->other['format']); $this->assertEquals('Submission text', $event->other['content']); $expected = new \stdClass(); $expected->modulename = 'assign'; $expected->cmid = $cm->id; $expected->itemid = $submission->id; $expected->courseid = $course->id; $expected->userid = $student->id; $expected->content = 'Submission text'; $this->assertEventContextNotUsed($event); } /** * Test that the submission_created event is fired when an onlinetext submission is saved. */ public function test_submission_created(): void { $this->resetAfterTest(); $course = $this->getDataGenerator()->create_course(); $student = $this->getDataGenerator()->create_and_enrol($course, 'student'); $assign = $this->create_instance($course); $context = $assign->get_context(); $this->setUser($student->id); $submission = $assign->get_user_submission($student->id, true); $data = (object) [ 'onlinetext_editor' => [ 'itemid' => file_get_unused_draft_itemid(), 'text' => 'Submission text', 'format' => FORMAT_PLAIN, ], ]; $sink = $this->redirectEvents(); $plugin = $assign->get_submission_plugin_by_type('onlinetext'); $plugin->save($submission, $data); $events = $sink->get_events(); $this->assertCount(2, $events); $event = $events[1]; $this->assertInstanceOf('\assignsubmission_onlinetext\event\submission_created', $event); $this->assertEquals($context->id, $event->contextid); $this->assertEquals($course->id, $event->courseid); $this->assertEquals($submission->id, $event->other['submissionid']); $this->assertEquals($submission->attemptnumber, $event->other['submissionattempt']); $this->assertEquals($submission->status, $event->other['submissionstatus']); $this->assertEquals($submission->userid, $event->relateduserid); } /** * Test that the submission_updated event is fired when an onlinetext * submission is saved and an existing submission already exists. */ public function test_submission_updated(): void { $this->resetAfterTest(); $course = $this->getDataGenerator()->create_course(); $student = $this->getDataGenerator()->create_and_enrol($course, 'student'); $assign = $this->create_instance($course); $context = $assign->get_context(); $this->setUser($student->id); $submission = $assign->get_user_submission($student->id, true); $data = (object) [ 'onlinetext_editor' => [ 'itemid' => file_get_unused_draft_itemid(), 'text' => 'Submission text', 'format' => FORMAT_PLAIN, ], ]; $sink = $this->redirectEvents(); $plugin = $assign->get_submission_plugin_by_type('onlinetext'); $plugin->save($submission, $data); $sink->clear(); // Update a submission. $plugin->save($submission, $data); $events = $sink->get_events(); $this->assertCount(2, $events); $event = $events[1]; $this->assertInstanceOf('\assignsubmission_onlinetext\event\submission_updated', $event); $this->assertEquals($context->id, $event->contextid); $this->assertEquals($course->id, $event->courseid); $this->assertEquals($submission->id, $event->other['submissionid']); $this->assertEquals($submission->attemptnumber, $event->other['submissionattempt']); $this->assertEquals($submission->status, $event->other['submissionstatus']); $this->assertEquals($submission->userid, $event->relateduserid); } } submission/onlinetext/settings.php000064400000002242152265517350013514 0ustar00. /** * This file defines the admin settings for this plugin * * @package assignsubmission_onlinetext * @copyright 2012 NetSpot {@link http://www.netspot.com.au} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ $settings->add(new admin_setting_configcheckbox('assignsubmission_onlinetext/default', new lang_string('default', 'assignsubmission_onlinetext'), new lang_string('default_help', 'assignsubmission_onlinetext'), 0)); submission/comments/db/install.php000064400000002713152265517350013346 0ustar00. /** * Post-install code for the submission_comments module. * * @package assignsubmission_comments * @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(); /** * Code run after the module database tables have been created. * Moves the comments plugin to the bottom * @return bool */ function xmldb_assignsubmission_comments_install() { global $CFG; require_once($CFG->dirroot . '/mod/assign/adminlib.php'); // Set the correct initial order for the plugins. $pluginmanager = new assign_plugin_manager('assignsubmission'); $pluginmanager->move_plugin('comments', 'down'); $pluginmanager->move_plugin('comments', 'down'); return true; } submission/comments/db/access.php000064400000001663152265517350013144 0ustar00. /** * Capability definitions for this module. * * @package assignsubmission_comments * @copyright 2012 NetSpot {@link http://www.netspot.com.au} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ $capabilities = array( ); submission/comments/db/upgrade.php000064400000003125152265517350013325 0ustar00. /** * Upgrade code for install * * @package assignsubmission_comments * @copyright 2012 NetSpot {@link http://www.netspot.com.au} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ /** * Stub for upgrade code * @param int $oldversion * @return bool */ function xmldb_assignsubmission_comments_upgrade($oldversion) { // Automatically generated Moodle v4.2.0 release upgrade line. // Put any upgrade step following this. // Automatically generated Moodle v4.3.0 release upgrade line. // Put any upgrade step following this. // Automatically generated Moodle v4.4.0 release upgrade line. // Put any upgrade step following this. // Automatically generated Moodle v4.5.0 release upgrade line. // Put any upgrade step following this. // Automatically generated Moodle v5.0.0 release upgrade line. // Put any upgrade step following this. return true; } submission/comments/lang/en/assignsubmission_comments.php000064400000003167152265517350020147 0ustar00. /** * Strings for component 'submission_comments', language 'en' * * @package assignsubmission_comments * @copyright 2012 NetSpot {@link http://www.netspot.com.au} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ $string['blindmarkingname'] = 'Participant {$a}'; $string['blindmarkingviewfullname'] = 'Participant {$a->participantnumber} ({$a->participantfullname})'; $string['privacy:metadata:commentpurpose'] = 'Comments between the student and teacher about a submission.'; $string['default'] = 'Enabled by default'; $string['default_help'] = 'If set, this submission method will be enabled by default for all new assignments.'; $string['enabled'] = 'Submission comments'; $string['enabled_help'] = 'If enabled, students can leave comments on their own submission. For example, this can be used for students to specify which is the master file when submitting inter-linked files.'; $string['pluginname'] = 'Submission comments'; submission/comments/lib.php000064400000020122152265517350012053 0ustar00. /** * This file contains the moodle hooks for the submission comments plugin * * @package assignsubmission_comments * @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(); /** * * Callback method for data validation---- required method for AJAXmoodle based comment API * * @param stdClass $options * @return bool */ function assignsubmission_comments_comment_validate(stdClass $options) { global $USER, $CFG, $DB; if ($options->commentarea != 'submission_comments' && $options->commentarea != 'submission_comments_upgrade') { throw new comment_exception('invalidcommentarea'); } if (!$submission = $DB->get_record('assign_submission', array('id'=>$options->itemid))) { throw new comment_exception('invalidcommentitemid'); } $context = $options->context; require_once($CFG->dirroot . '/mod/assign/locallib.php'); static $assignment = null; if (is_null($assignment) || $assignment->get_context() != $context) { $assignment = new assign($context, null, null); } if ($assignment->get_instance()->id != $submission->assignment) { throw new comment_exception('invalidcontext'); } return true; } /** * Permission control method for submission plugin ---- required method for AJAXmoodle based comment API * * @param stdClass $options * @return array */ function assignsubmission_comments_comment_permissions(stdClass $options) { global $USER, $CFG, $DB; if ($options->commentarea != 'submission_comments' && $options->commentarea != 'submission_comments_upgrade') { throw new comment_exception('invalidcommentarea'); } if (!$submission = $DB->get_record('assign_submission', array('id'=>$options->itemid))) { throw new comment_exception('invalidcommentitemid'); } $context = $options->context; require_once($CFG->dirroot . '/mod/assign/locallib.php'); static $assignment = null; if (is_null($assignment) || $assignment->get_context() != $context) { $assignment = new assign($context, null, null); } if ($assignment->get_instance()->id != $submission->assignment) { throw new comment_exception('invalidcontext'); } if ($assignment->get_instance()->teamsubmission && !$assignment->can_view_group_submission($submission->groupid)) { return array('post' => false, 'view' => false); } if (!$assignment->get_instance()->teamsubmission && !$assignment->can_view_submission($submission->userid)) { return array('post' => false, 'view' => false); } return array('post' => true, 'view' => true); } /** * Callback called by comment::get_comments() and comment::add(). Gives an opportunity to enforce blind-marking. * * @param array $comments * @param stdClass $options * @return array * @throws comment_exception */ function assignsubmission_comments_comment_display($comments, $options) { global $CFG, $DB, $USER; if ($options->commentarea != 'submission_comments' && $options->commentarea != 'submission_comments_upgrade') { throw new comment_exception('invalidcommentarea'); } if (!$submission = $DB->get_record('assign_submission', array('id'=>$options->itemid))) { throw new comment_exception('invalidcommentitemid'); } $context = $options->context; $cm = $options->cm; $course = $options->courseid; require_once($CFG->dirroot . '/mod/assign/locallib.php'); $assignment = new assign($context, $cm, $course); if ($assignment->get_instance()->id != $submission->assignment) { throw new comment_exception('invalidcontext'); } if ($assignment->is_blind_marking() && !empty($comments)) { // Blind marking is being used, may need to map unique anonymous ids to the comments. $usermappings = array(); $guestuser = guest_user(); // Check group users first. $userinteam = false; if ($assignment->get_instance()->teamsubmission && has_capability('mod/assign:submit', $context)) { $assignment->set_course(get_course($course)); $userinteam = $assignment->can_edit_group_submission($submission->groupid); } foreach ($comments as $comment) { if (has_capability('mod/assign:viewblinddetails', $context) && $USER->id != $comment->userid) { $anonid = $assignment->get_uniqueid_for_user($comment->userid); // Show participant information and the user's full name to users with the view blind details capability. $a = new stdClass(); $a->participantnumber = $anonid; $a->participantfullname = $comment->fullname; $comment->fullname = get_string('blindmarkingviewfullname', 'assignsubmission_comments', $a); } else if ($USER->id == $comment->userid || $submission->userid == $USER->id || $userinteam) { // phpcs:ignore // Do not anonymize the user details for this comment. } else { // Anonymize the comments. if (empty($usermappings[$comment->userid])) { $anonid = $assignment->get_uniqueid_for_user($comment->userid); // The blind-marking information for this commenter has not been generated; do so now. $commenter = new stdClass(); $commenter->firstname = get_string('blindmarkingname', 'assignsubmission_comments', $anonid); $commenter->lastname = ''; $commenter->firstnamephonetic = ''; $commenter->lastnamephonetic = ''; $commenter->middlename = ''; $commenter->alternatename = ''; $commenter->picture = 0; $commenter->id = $guestuser->id; $commenter->email = $guestuser->email; $commenter->imagealt = $guestuser->imagealt; // Temporarily store blind-marking information for use in later comments if necessary. $usermappings[$comment->userid] = new stdClass(); $usermappings[$comment->userid]->fullname = fullname($commenter); $usermappings[$comment->userid]->avatar = $assignment->get_renderer()->user_picture($commenter, array('size' => 18, 'link' => false)); } // Set blind-marking information for this comment. $comment->fullname = $usermappings[$comment->userid]->fullname; $comment->avatar = $usermappings[$comment->userid]->avatar; $comment->profileurl = null; } } } return $comments; } /** * Callback to force the userid for all comments to be the userid of the submission and NOT the global $USER->id. This * is required by the upgrade code. Note the comment area is used to identify upgrades. * * @param stdClass $comment * @param stdClass $param */ function assignsubmission_comments_comment_add(stdClass $comment, stdClass $param) { global $DB; if ($comment->commentarea == 'submission_comments_upgrade') { $submissionid = $comment->itemid; $submission = $DB->get_record('assign_submission', array('id' => $submissionid)); $comment->userid = $submission->userid; $comment->commentarea = 'submission_comments'; } } submission/comments/version.php000064400000002127152265517350012777 0ustar00. /** * This file contains the version information for the comments submission plugin * * @package assignsubmission_comments * @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(); $plugin->version = 2025041400; $plugin->requires = 2025040800; $plugin->component = 'assignsubmission_comments'; submission/comments/classes/privacy/provider.php000064400000016227152265517350016264 0ustar00. /** * Privacy class for requesting user data. * * @package assignsubmission_comments * @copyright 2018 Adrian Greeve * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace assignsubmission_comments\privacy; defined('MOODLE_INTERNAL') || die(); require_once($CFG->dirroot . '/mod/assign/locallib.php'); use \core_privacy\local\metadata\collection; use \core_privacy\local\metadata\provider as metadataprovider; use \core_comment\privacy\provider as comments_provider; use \core_privacy\local\request\contextlist; use \mod_assign\privacy\assign_plugin_request_data; /** * Privacy class for requesting user data. * * @package assignsubmission_comments * @copyright 2018 Adrian Greeve * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class provider implements metadataprovider, \mod_assign\privacy\assignsubmission_provider, \mod_assign\privacy\assignsubmission_user_provider { /** * Return meta data about this plugin. * * @param collection $collection A list of information to add to. * @return collection Return the collection after adding to it. */ public static function get_metadata(collection $collection): collection { $collection->link_subsystem('core_comment', 'privacy:metadata:commentpurpose'); return $collection; } /** * It is possible to make a comment as a teacher without creating an entry in the submission table, so this is required * to find those entries. * * @param int $userid The user ID that we are finding contexts for. * @param contextlist $contextlist A context list to add sql and params to for contexts. */ public static function get_context_for_userid_within_submission(int $userid, contextlist $contextlist) { $sql = "SELECT contextid FROM {comments} WHERE component = :component AND commentarea = :commentarea AND userid = :userid"; $params = ['userid' => $userid, 'component' => 'assignsubmission_comments', 'commentarea' => 'submission_comments']; $contextlist->add_from_sql($sql, $params); } /** * Due to the fact that we can't rely on the queries in the mod_assign provider we have to add some additional sql. * * @param \mod_assign\privacy\useridlist $useridlist An object for obtaining user IDs of students. */ public static function get_student_user_ids(\mod_assign\privacy\useridlist $useridlist) { $params = ['assignid' => $useridlist->get_assignid(), 'commentuserid' => $useridlist->get_teacherid(), 'commentuserid2' => $useridlist->get_teacherid()]; $sql = "SELECT DISTINCT c.userid AS id FROM {comments} c JOIN (SELECT c.itemid FROM {comments} c JOIN {assign_submission} s ON s.id = c.itemid AND s.assignment = :assignid WHERE c.userid = :commentuserid) aa ON aa.itemid = c.itemid WHERE c.userid NOT IN (:commentuserid2)"; $useridlist->add_from_sql($sql, $params); } /** * If you have tables that contain userids and you can generate entries in your tables without creating an * entry in the assign_submission table then please fill in this method. * * @param \core_privacy\local\request\userlist $userlist The userlist object */ public static function get_userids_from_context(\core_privacy\local\request\userlist $userlist) { $context = $userlist->get_context(); if ($context->contextlevel != CONTEXT_MODULE) { return; } comments_provider::get_users_in_context_from_sql($userlist, 'c', 'assignsubmission_comments', 'submission_comments', $context->id); } /** * Export all user data for this plugin. * * @param assign_plugin_request_data $exportdata Data used to determine which context and user to export and other useful * information to help with exporting. */ public static function export_submission_user_data(assign_plugin_request_data $exportdata) { $component = 'assignsubmission_comments'; $commentarea = 'submission_comments'; $userid = ($exportdata->get_user() != null); $submission = $exportdata->get_pluginobject(); // For the moment we are only showing the comments made by this user. comments_provider::export_comments($exportdata->get_context(), $component, $commentarea, $submission->id, $exportdata->get_subcontext(), $userid); } /** * Delete all the comments made for this context. * * @param assign_plugin_request_data $requestdata Data to fulfill the deletion request. */ public static function delete_submission_for_context(assign_plugin_request_data $requestdata) { comments_provider::delete_comments_for_all_users($requestdata->get_context(), 'assignsubmission_comments', 'submission_comments'); } /** * A call to this method should delete user data (where practical) using the userid and submission. * * @param assign_plugin_request_data $exportdata Details about the user and context to focus the deletion. */ public static function delete_submission_for_userid(assign_plugin_request_data $exportdata) { // Create an approved context list to delete the comments. $contextlist = new \core_privacy\local\request\approved_contextlist($exportdata->get_user(), 'assignsubmission_comments', [$exportdata->get_context()->id]); comments_provider::delete_comments_for_user($contextlist, 'assignsubmission_comments', 'submission_comments'); } /** * Deletes all submissions for the submission ids / userids provided in a context. * assign_plugin_request_data contains: * - context * - assign object * - submission ids (pluginids) * - user ids * @param assign_plugin_request_data $deletedata A class that contains the relevant information required for deletion. */ public static function delete_submissions(assign_plugin_request_data $deletedata) { $userlist = new \core_privacy\local\request\approved_userlist($deletedata->get_context(), 'assignsubmission_comments', $deletedata->get_userids()); comments_provider::delete_comments_for_users($userlist, 'assignsubmission_comments', 'submission_comments'); } } submission/comments/classes/event/comment_deleted.php000064400000003606152265517350017223 0ustar00. /** * The assignsubmission_comments comment deleted event. * * @package assignsubmission_comments * @copyright 2013 Rajesh Taneja * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace assignsubmission_comments\event; defined('MOODLE_INTERNAL') || die(); /** * The assignsubmission_comments comment deleted event. * * @package assignsubmission_comments * @since Moodle 2.7 * @copyright 2013 Rajesh Taneja * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class comment_deleted extends \core\event\comment_deleted { /** * Get URL related to the action. * * @return \moodle_url */ public function get_url() { return new \moodle_url('/mod/assign/view.php', array('id' => $this->contextinstanceid)); } /** * Returns description of what happened. * * @return string */ public function get_description() { return "The user with id '$this->userid' deleted the comment with id '$this->objectid' from the submission " . "with id '{$this->other['itemid']}' for the assignment with course module id '$this->contextinstanceid'."; } } submission/comments/classes/event/comment_created.php000064400000003610152265517350017217 0ustar00. /** * The assignsubmission_comments comment created event. * * @package assignsubmission_comments * @copyright 2013 Rajesh Taneja * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace assignsubmission_comments\event; defined('MOODLE_INTERNAL') || die(); /** * The assignsubmission_comments comment created event class. * * @package assignsubmission_comments * @since Moodle 2.7 * @copyright 2013 Rajesh Taneja * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class comment_created extends \core\event\comment_created { /** * Get URL related to the action. * * @return \moodle_url */ public function get_url() { return new \moodle_url('/mod/assign/view.php', array('id' => $this->contextinstanceid)); } /** * Returns description of what happened. * * @return string */ public function get_description() { return "The user with id '$this->userid' added the comment with id '$this->objectid' to the submission " . "with id '{$this->other['itemid']}' for the assignment with course module id '$this->contextinstanceid'."; } } submission/comments/locallib.php000064400000015101152265517350013067 0ustar00. /** * This file contains the definition for the library class for online comment submission plugin * * @package assignsubmission_comments * @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(); require_once($CFG->dirroot . '/comment/lib.php'); require_once($CFG->dirroot . '/mod/assign/submissionplugin.php'); /** * Library class for comment submission plugin extending submission plugin base class * * @package assignsubmission_comments * @copyright 2012 NetSpot {@link http://www.netspot.com.au} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class assign_submission_comments extends assign_submission_plugin { /** * Get the name of the online comment submission plugin * @return string */ public function get_name() { return get_string('pluginname', 'assignsubmission_comments'); } /** * Display AJAX based comment in the submission status table * * @param stdClass $submission * @param bool $showviewlink - If the comments are long this is * set to true so they can be shown in a separate page * @return string */ public function view_summary(stdClass $submission, & $showviewlink) { // Never show a link to view full submission. $showviewlink = false; // Need to used this init() otherwise it does not have the javascript includes. comment::init(); $options = new stdClass(); $options->area = 'submission_comments'; $options->course = $this->assignment->get_course(); $options->context = $this->assignment->get_context(); $options->itemid = $submission->id; $options->component = 'assignsubmission_comments'; $options->showcount = true; $options->displaycancel = true; $comment = new comment($options); $o = $this->assignment->get_renderer()->container($comment->output(true), 'commentscontainer'); return $o; } /** * Always return true because the submission comments are not part of the submission form. * * @param stdClass $submission * @return bool */ public function is_empty(stdClass $submission) { return true; } /** * Return true if this plugin can upgrade an old Moodle 2.2 assignment of this type * and version. * * @param string $type old assignment subtype * @param int $version old assignment version * @return bool True if upgrade is possible */ public function can_upgrade($type, $version) { if ($type == 'upload' && $version >= 2011112900) { return true; } return false; } /** * Upgrade the settings from the old assignment to the new plugin based one * * @param context $oldcontext - the context for the old assignment * @param stdClass $oldassignment - the data for the old assignment * @param string $log - can be appended to by the upgrade * @return bool was it a success? (false will trigger a rollback) */ public function upgrade_settings(context $oldcontext, stdClass $oldassignment, & $log) { if ($oldassignment->assignmenttype == 'upload') { // Disable if allow notes was not enabled. if (!$oldassignment->var2) { $this->disable(); } } return true; } /** * Upgrade the submission from the old assignment to the new one * * @param context $oldcontext The context for the old assignment * @param stdClass $oldassignment The data record for the old assignment * @param stdClass $oldsubmission The data record for the old submission * @param stdClass $submission The new submission record * @param string $log Record upgrade messages in the log * @return bool true or false - false will trigger a rollback */ public function upgrade(context $oldcontext, stdClass $oldassignment, stdClass $oldsubmission, stdClass $submission, & $log) { if ($oldsubmission->data1 != '') { // Need to used this init() otherwise it does not have the javascript includes. comment::init(); $options = new stdClass(); $options->area = 'submission_comments_upgrade'; $options->course = $this->assignment->get_course(); $options->context = $this->assignment->get_context(); $options->itemid = $submission->id; $options->component = 'assignsubmission_comments'; $options->showcount = true; $options->displaycancel = true; $comment = new comment($options); $comment->add($oldsubmission->data1); $comment->set_view_permission(true); return $comment->output(true); } return true; } /** * The submission comments plugin has no submission component so should not be counted * when determining whether to show the edit submission link. * @return boolean */ public function allow_submissions() { return false; } /** * Automatically enable or disable this plugin based on "$CFG->commentsenabled" * * @return bool */ public function is_enabled() { global $CFG; return (!empty($CFG->usecomments)); } /** * Automatically hide the setting for the submission plugin. * * @return bool */ public function is_configurable() { return false; } /** * Return the plugin configs for external functions. * * @return array the list of settings * @since Moodle 3.2 */ public function get_config_for_external() { return (array) $this->get_config(); } } submission/comments/tests/privacy/provider_test.php000064400000037473152265517350017036 0ustar00. namespace assignsubmission_comments\privacy; use mod_assign\privacy\useridlist; /** * Unit tests for mod/assign/submission/comments/classes/privacy/ * * @package assignsubmission_comments * @copyright 2018 Adrian Greeve * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @covers \assignsubmission_comments\privacy\provider */ final class provider_test extends \mod_assign\tests\provider_testcase { /** * Convenience function for creating feedback data. * * @param object $assign assign object * @param stdClass $student user object * @param string $submissiontext Submission text * @return array Submission plugin object and the submission object and the comment object. */ protected function create_comment_submission($assign, $student, $submissiontext) { $submission = $assign->get_user_submission($student->id, true); $plugin = $assign->get_submission_plugin_by_type('comments'); $context = $assign->get_context(); $options = new \stdClass(); $options->area = 'submission_comments'; $options->course = $assign->get_course(); $options->context = $context; $options->itemid = $submission->id; $options->component = 'assignsubmission_comments'; $options->showcount = true; $options->displaycancel = true; $comment = new \comment($options); $comment->set_post_permission(true); $this->setUser($student); $comment->add($submissiontext); return [$plugin, $submission, $comment]; } /** * Quick test to make sure that get_metadata returns something. */ public function test_get_metadata(): void { $collection = new \core_privacy\local\metadata\collection('assignsubmission_comments'); $collection = \assignsubmission_comments\privacy\provider::get_metadata($collection); $this->assertNotEmpty($collection); } /** * Test returning the context for a user who has made a comment in an assignment. */ public function test_get_context_for_userid_within_submission(): void { $this->resetAfterTest(); // Create course, assignment, submission, and then a feedback comment. $course = $this->getDataGenerator()->create_course(); // Student. $user1 = $this->getDataGenerator()->create_user(); // Teacher. $user2 = $this->getDataGenerator()->create_user(); $this->getDataGenerator()->enrol_user($user1->id, $course->id, 'student'); $this->getDataGenerator()->enrol_user($user2->id, $course->id, 'editingteacher'); $assign = $this->create_instance(['course' => $course]); $context = $assign->get_context(); $studentcomment = 'Comment from user 1'; list($plugin, $submission, $comment) = $this->create_comment_submission($assign, $user1, $studentcomment); $teachercomment = 'From the teacher'; $this->setUser($user2); $comment->add($teachercomment); $contextlist = new \core_privacy\local\request\contextlist(); \assignsubmission_comments\privacy\provider::get_context_for_userid_within_submission($user2->id, $contextlist); $this->assertEquals($context->id, $contextlist->get_contextids()[0]); } /** * Test returning student ids given a user ID. */ public function test_get_student_user_ids(): void { $this->resetAfterTest(); // Create course, assignment, submission, and then a feedback comment. $course = $this->getDataGenerator()->create_course(); // Student. $user1 = $this->getDataGenerator()->create_user(); // Teacher. $user2 = $this->getDataGenerator()->create_user(); $this->getDataGenerator()->enrol_user($user1->id, $course->id, 'student'); $this->getDataGenerator()->enrol_user($user2->id, $course->id, 'editingteacher'); $assign = $this->create_instance(['course' => $course]); $context = $assign->get_context(); $studentcomment = 'Comment from user 1'; list($plugin, $submission, $comment) = $this->create_comment_submission($assign, $user1, $studentcomment); $teachercomment = 'From the teacher'; $this->setUser($user2); $comment->add($teachercomment); $useridlist = new useridlist($user2->id, $assign->get_instance()->id); \assignsubmission_comments\privacy\provider::get_student_user_ids($useridlist); $this->assertEquals($user1->id, $useridlist->get_userids()[0]->id); } /** * Test returning users related to a given context. */ public function test_get_userids_from_context(): void { // Get a bunch of users making comments. // Some in one context some in another. $this->resetAfterTest(); $course = $this->getDataGenerator()->create_course(); // Only in first context. $user1 = $this->getDataGenerator()->create_user(); $user2 = $this->getDataGenerator()->create_user(); // First and second context. $user3 = $this->getDataGenerator()->create_user(); // Second context only. $user4 = $this->getDataGenerator()->create_user(); $this->getDataGenerator()->enrol_user($user1->id, $course->id, 'student'); $assign1 = $this->create_instance(['course' => $course]); $assign2 = $this->create_instance(['course' => $course]); $assigncontext1 = $assign1->get_context(); $assigncontext2 = $assign2->get_context(); $user1comment = 'Comment from user 1'; list($plugin, $submission, $comment) = $this->create_comment_submission($assign1, $user1, $user1comment); $user2comment = 'From user 2'; $this->setUser($user2); $comment->add($user2comment); $user3comment = 'User 3 comment'; $this->setUser($user3); $comment->add($user3comment); $user4comment = 'Comment from user 4'; list($plugin, $submission, $comment) = $this->create_comment_submission($assign2, $user4, $user4comment); $user3secondcomment = 'Comment on user 4 post.'; $this->setUser($user3); $comment->add($user3comment); $userlist = new \core_privacy\local\request\userlist($assigncontext1, 'assignsubmission_comments'); \assignsubmission_comments\privacy\provider::get_userids_from_context($userlist); $userids = $userlist->get_userids(); $this->assertCount(3, $userids); // User 1,2 and 3 are the expected ones in the array. User 4 isn't. $this->assertContainsEquals($user1->id, $userids); $this->assertContainsEquals($user2->id, $userids); $this->assertContainsEquals($user3->id, $userids); $this->assertNotContainsEquals($user4->id, $userids); } /** * Test that comments are exported for a user. */ public function test_export_submission_user_data(): void { $this->resetAfterTest(); // Create course, assignment, submission, and then a feedback comment. $course = $this->getDataGenerator()->create_course(); // Student. $user1 = $this->getDataGenerator()->create_user(); // Teacher. $user2 = $this->getDataGenerator()->create_user(); $this->getDataGenerator()->enrol_user($user1->id, $course->id, 'student'); $this->getDataGenerator()->enrol_user($user2->id, $course->id, 'editingteacher'); $assign = $this->create_instance(['course' => $course]); $context = $assign->get_context(); $studentcomment = 'Comment from user 1'; list($plugin, $submission, $comment) = $this->create_comment_submission($assign, $user1, $studentcomment); $teachercomment = 'From the teacher'; $this->setUser($user2); $comment->add($teachercomment); $writer = \core_privacy\local\request\writer::with_context($context); $this->assertFalse($writer->has_any_data()); // The student should be able to see the teachers feedback. $exportdata = new \mod_assign\privacy\assign_plugin_request_data($context, $assign, $submission); \assignsubmission_comments\privacy\provider::export_submission_user_data($exportdata); $exportedcomments = $writer->get_data(['Comments']); // Can't rely on these comments coming out in order. if ($exportedcomments->comments[0]->userid == $user1->id) { $exportedstudentcomment = $exportedcomments->comments[0]->content; $exportedteachercomment = $exportedcomments->comments[1]->content; } else { $exportedstudentcomment = $exportedcomments->comments[1]->content; $exportedteachercomment = $exportedcomments->comments[0]->content; } $this->assertCount(2, $exportedcomments->comments); $this->assertStringContainsString($studentcomment, $exportedstudentcomment); $this->assertStringContainsString($teachercomment, $exportedteachercomment); } /** * Test that all comments are deleted for this context. */ public function test_delete_submission_for_context(): void { global $DB; $this->resetAfterTest(); // Create course, assignment, submission, and then a feedback comment. $course = $this->getDataGenerator()->create_course(); // Student. $user1 = $this->getDataGenerator()->create_user(); $user2 = $this->getDataGenerator()->create_user(); // Teacher. $user3 = $this->getDataGenerator()->create_user(); $this->getDataGenerator()->enrol_user($user1->id, $course->id, 'student'); $this->getDataGenerator()->enrol_user($user2->id, $course->id, 'student'); $this->getDataGenerator()->enrol_user($user3->id, $course->id, 'editingteacher'); $assign = $this->create_instance(['course' => $course]); $context = $assign->get_context(); $studentcomment = 'Comment from user 1'; list($plugin, $submission, $comment) = $this->create_comment_submission($assign, $user1, $studentcomment); $studentcomment = 'Comment from user 2'; list($plugin2, $submission2, $comment2) = $this->create_comment_submission($assign, $user2, $studentcomment); $teachercomment1 = 'From the teacher'; $teachercomment2 = 'From the teacher for second student.'; $this->setUser($user3); $comment->add($teachercomment1); $comment2->add($teachercomment2); // Only need the context in this plugin for this operation. $requestdata = new \mod_assign\privacy\assign_plugin_request_data($context, $assign); \assignsubmission_comments\privacy\provider::delete_submission_for_context($requestdata); $results = $DB->get_records('comments', ['contextid' => $context->id]); $this->assertEmpty($results); } /** * Test that the comments for a user are deleted. */ public function test_delete_submission_for_userid(): void { global $DB; $this->resetAfterTest(); // Create course, assignment, submission, and then a feedback comment. $course = $this->getDataGenerator()->create_course(); // Student. $user1 = $this->getDataGenerator()->create_user(); $user2 = $this->getDataGenerator()->create_user(); // Teacher. $user3 = $this->getDataGenerator()->create_user(); $this->getDataGenerator()->enrol_user($user1->id, $course->id, 'student'); $this->getDataGenerator()->enrol_user($user2->id, $course->id, 'student'); $this->getDataGenerator()->enrol_user($user3->id, $course->id, 'editingteacher'); $assign = $this->create_instance(['course' => $course]); $context = $assign->get_context(); $studentcomment = 'Comment from user 1'; list($plugin, $submission, $comment) = $this->create_comment_submission($assign, $user1, $studentcomment); $studentcomment = 'Comment from user 2'; list($plugin2, $submission2, $comment2) = $this->create_comment_submission($assign, $user2, $studentcomment); $teachercomment1 = 'From the teacher'; $teachercomment2 = 'From the teacher for second student.'; $this->setUser($user3); $comment->add($teachercomment1); $comment2->add($teachercomment2); // Provide full details to delete the comments. $requestdata = new \mod_assign\privacy\assign_plugin_request_data($context, $assign, null, [], $user1); \assignsubmission_comments\privacy\provider::delete_submission_for_userid($requestdata); $results = $DB->get_records('comments', ['contextid' => $context->id]); // We are only deleting the comments for user1 (one comment) so we should have three left. $this->assertCount(3, $results); foreach ($results as $result) { // Check that none of the comments are from user1. $this->assertNotEquals($user1->id, $result->userid); } } /** * Test deletion of all submissions for a context works. */ public function test_delete_submissions(): void { global $DB; // Get a bunch of users making comments. // Some in one context some in another. $this->resetAfterTest(); $course = $this->getDataGenerator()->create_course(); // Only in first context. $user1 = $this->getDataGenerator()->create_user(); $user2 = $this->getDataGenerator()->create_user(); // First and second context. $user3 = $this->getDataGenerator()->create_user(); // Second context only. $user4 = $this->getDataGenerator()->create_user(); $this->getDataGenerator()->enrol_user($user1->id, $course->id, 'student'); $assign1 = $this->create_instance(['course' => $course]); $assign2 = $this->create_instance(['course' => $course]); $assigncontext1 = $assign1->get_context(); $assigncontext2 = $assign2->get_context(); $user1comment = 'Comment from user 1'; list($plugin, $submission, $comment) = $this->create_comment_submission($assign1, $user1, $user1comment); $user2comment = 'From user 2'; $this->setUser($user2); $comment->add($user2comment); $user3comment = 'User 3 comment'; $this->setUser($user3); $comment->add($user3comment); $user4comment = 'Comment from user 4'; list($plugin, $submission, $comment) = $this->create_comment_submission($assign2, $user4, $user4comment); $user3secondcomment = 'Comment on user 4 post.'; $this->setUser($user3); $comment->add($user3comment); // There should be three entries. One for the first three users. $results = $DB->get_records('comments', ['contextid' => $assigncontext1->id]); $this->assertCount(3, $results); $deletedata = new \mod_assign\privacy\assign_plugin_request_data($assigncontext1, $assign1); $deletedata->set_userids([$user1->id, $user3->id]); \assignsubmission_comments\privacy\provider::delete_submissions($deletedata); // We should be left with just a comment from user 2. $results = $DB->get_records('comments', ['contextid' => $assigncontext1->id]); $this->assertCount(1, $results); $this->assertEquals($user2comment, current($results)->content); } } submission/comments/tests/event/events_test.php000064400000011553152265517350016143 0ustar00. /** * Events tests. * * @package assignsubmission_comments * @category test * @copyright 2013 Rajesh Taneja * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace assignsubmission_comments\event; use mod_assign_test_generator; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->dirroot . '/mod/assign/lib.php'); require_once($CFG->dirroot . '/mod/assign/locallib.php'); require_once($CFG->dirroot . '/mod/assign/tests/generator.php'); require_once($CFG->dirroot . '/comment/lib.php'); /** * Events tests class. * * @package assignsubmission_comments * @category test * @copyright 2013 Rajesh Taneja * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class events_test extends \advanced_testcase { // Use the generator helper. use mod_assign_test_generator; /** * Test comment_created event. */ public function test_comment_created(): void { $this->resetAfterTest(); $course = $this->getDataGenerator()->create_course(); $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher'); $student = $this->getDataGenerator()->create_and_enrol($course, 'student'); $assign = $this->create_instance($course); $this->setUser($teacher); $submission = $assign->get_user_submission($student->id, true); $context = $assign->get_context(); $options = new \stdClass(); $options->area = 'submission_comments'; $options->course = $assign->get_course(); $options->context = $context; $options->itemid = $submission->id; $options->component = 'assignsubmission_comments'; $options->showcount = true; $options->displaycancel = true; $comment = new \comment($options); // Triggering and capturing the event. $sink = $this->redirectEvents(); $comment->add('New comment'); $events = $sink->get_events(); $this->assertCount(1, $events); $event = reset($events); $sink->close(); // Checking that the event contains the expected values. $this->assertInstanceOf('\assignsubmission_comments\event\comment_created', $event); $this->assertEquals($context, $event->get_context()); $url = new \moodle_url('/mod/assign/view.php', array('id' => $assign->get_course_module()->id)); $this->assertEquals($url, $event->get_url()); $this->assertEventContextNotUsed($event); } /** * Test comment_deleted event. */ public function test_comment_deleted(): void { $this->resetAfterTest(); $course = $this->getDataGenerator()->create_course(); $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher'); $student = $this->getDataGenerator()->create_and_enrol($course, 'student'); $assign = $this->create_instance($course); $this->setUser($teacher); $submission = $assign->get_user_submission($student->id, true); $context = $assign->get_context(); $options = new \stdClass(); $options->area = 'submission_comments'; $options->course = $assign->get_course(); $options->context = $context; $options->itemid = $submission->id; $options->component = 'assignsubmission_comments'; $options->showcount = true; $options->displaycancel = true; $comment = new \comment($options); $newcomment = $comment->add('New comment 1'); // Triggering and capturing the event. $sink = $this->redirectEvents(); $comment->delete($newcomment->id); $events = $sink->get_events(); $this->assertCount(1, $events); $event = reset($events); // Checking that the event contains the expected values. $this->assertInstanceOf('\assignsubmission_comments\event\comment_deleted', $event); $this->assertEquals($context, $event->get_context()); $url = new \moodle_url('/mod/assign/view.php', array('id' => $assign->get_course_module()->id)); $this->assertEquals($url, $event->get_url()); $this->assertEventContextNotUsed($event); } } submission/file/db/access.php000064400000001656152265517350012240 0ustar00. /** * Capability definitions for this module. * * @package assignsubmission_file * @copyright 2012 NetSpot {@link http://www.netspot.com.au} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ $capabilities = array( ); submission/file/db/upgrade.php000064400000003115152265517350012416 0ustar00. /** * Upgrade code for install * * @package assignsubmission_file * @copyright 2012 NetSpot {@link http://www.netspot.com.au} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ /** * Stub for upgrade code * @param int $oldversion * @return bool */ function xmldb_assignsubmission_file_upgrade($oldversion) { // Automatically generated Moodle v4.2.0 release upgrade line. // Put any upgrade step following this. // Automatically generated Moodle v4.3.0 release upgrade line. // Put any upgrade step following this. // Automatically generated Moodle v4.4.0 release upgrade line. // Put any upgrade step following this. // Automatically generated Moodle v4.5.0 release upgrade line. // Put any upgrade step following this. // Automatically generated Moodle v5.0.0 release upgrade line. // Put any upgrade step following this. return true; } submission/file/db/install.xml000064400000002530152265517350012446 0ustar00
submission/file/lang/en/assignsubmission_file.php000064400000005147152265517350016333 0ustar00. /** * Strings for component 'assignsubmission_file', language 'en' * * @package assignsubmission_file * @copyright 2012 NetSpot {@link http://www.netspot.com.au} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ $string['acceptedfiletypes'] = 'Accepted file types'; $string['acceptedfiletypes_help'] = 'Accepted file types can be restricted by entering a list of file extensions. If the field is left empty, then all file types are allowed.'; $string['configmaxbytes'] = 'Maximum file size'; $string['countfiles'] = '{$a} files'; $string['default'] = 'Enabled by default'; $string['default_help'] = 'If set, this submission method will be enabled by default for all new assignments.'; $string['defaultacceptedfiletypes'] = 'Default accepted file types'; $string['enabled'] = 'File submissions'; $string['enabled_help'] = 'If enabled, students are able to upload one or more files as their submission.'; $string['eventassessableuploaded'] = 'A file has been uploaded.'; $string['file'] = 'File submissions'; $string['filewithsize'] = '{$a->filename} ({$a->size})'; $string['maxbytes'] = 'Maximum file size'; $string['maxfiles'] = 'Maximum files per submission'; $string['maxfiles_help'] = 'If file submissions are enabled, each assignment can be set to accept up to this number of files for their submission.'; $string['maxfilessubmission'] = 'Maximum number of uploaded files'; $string['maxfilessubmission_help'] = 'If file submissions are enabled, each student will be able to upload up to this number of files for their submission.'; $string['maximumsubmissionsize'] = 'Maximum submission size'; $string['maximumsubmissionsize_help'] = 'Files uploaded by students may be up to this size.'; $string['pluginname'] = 'File submissions'; $string['privacy:metadata:filepurpose'] = 'The files loaded for this assignment submission'; $string['siteuploadlimit'] = 'Site upload limit'; $string['submissionfilearea'] = 'Uploaded submission files'; submission/file/backup/moodle2/restore_assignsubmission_file_subplugin.class.php000064400000005125152265517350024551 0ustar00. /** * This file contains the class for restore of this submission plugin * * @package assignsubmission_file * @copyright 2012 NetSpot {@link http://www.netspot.com.au} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ /** * Restore subplugin class. * * Provides the necessary information * needed to restore one assign_submission subplugin. * * @package assignsubmission_file * @copyright 2012 NetSpot {@link http://www.netspot.com.au} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class restore_assignsubmission_file_subplugin extends restore_subplugin { /** * Returns the paths to be handled by the subplugin at workshop level * @return array */ protected function define_submission_subplugin_structure() { $paths = array(); $elename = $this->get_namefor('submission'); $elepath = $this->get_pathfor('/submission_file'); // We used get_recommended_name() so this works. $paths[] = new restore_path_element($elename, $elepath); return $paths; } /** * Processes one submission_file element * @param mixed $data * @return void */ public function process_assignsubmission_file_submission($data) { global $DB; $data = (object)$data; $data->assignment = $this->get_new_parentid('assign'); $oldsubmissionid = $data->submission; // The mapping is set in the restore for the core assign activity // when a submission node is processed. $data->submission = $this->get_mappingid('submission', $data->submission); $DB->insert_record('assignsubmission_file', $data); $this->add_related_files('assignsubmission_file', 'submission_files', 'submission', null, $oldsubmissionid); } } submission/file/backup/moodle2/backup_assignsubmission_file_subplugin.class.php000064400000005017152265517350024333 0ustar00. /** * This file contains the class for backup of this submission plugin * * @package assignsubmission_file * @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(); /** * Provides the information to backup submission files * * This just adds its filearea to the annotations and records the number of files * * @package assignsubmission_file * @copyright 2012 NetSpot {@link http://www.netspot.com.au} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class backup_assignsubmission_file_subplugin extends backup_subplugin { /** * Returns the subplugin information to attach to submission element * @return backup_subplugin_element */ protected function define_submission_subplugin_structure() { // Create XML elements. $subplugin = $this->get_subplugin_element(); $subpluginwrapper = new backup_nested_element($this->get_recommended_name()); $subpluginelement = new backup_nested_element('submission_file', null, array('numfiles', 'submission')); // Connect XML elements into the tree. $subplugin->add_child($subpluginwrapper); $subpluginwrapper->add_child($subpluginelement); // Set source to populate the data. $subpluginelement->set_source_table('assignsubmission_file', array('submission' => backup::VAR_PARENTID)); // The parent is the submission. $subpluginelement->annotate_files('assignsubmission_file', 'submission_files', 'submission'); return $subplugin; } } submission/file/lib.php000064400000006031152265517350011150 0ustar00. /** * This file contains the moodle hooks for the submission file plugin * * @package assignsubmission_file * @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(); /** * Serves assignment submissions and other files. * * @param mixed $course course or id of the course * @param mixed $cm course module or id of the course module * @param context $context * @param string $filearea * @param array $args * @param bool $forcedownload * @param array $options - List of options affecting file serving. * @return bool false if file not found, does not return if found - just send the file */ function assignsubmission_file_pluginfile($course, $cm, context $context, $filearea, $args, $forcedownload, array $options=array()) { global $DB, $CFG; if ($context->contextlevel != CONTEXT_MODULE) { return false; } require_login($course, false, $cm); $itemid = (int)array_shift($args); $record = $DB->get_record('assign_submission', array('id'=>$itemid), 'userid, assignment, groupid', MUST_EXIST); $userid = $record->userid; $groupid = $record->groupid; require_once($CFG->dirroot . '/mod/assign/locallib.php'); $assign = new assign($context, $cm, $course); if ($assign->get_instance()->id != $record->assignment) { return false; } if ($assign->get_instance()->teamsubmission && !$assign->can_view_group_submission($groupid)) { return false; } if (!$assign->get_instance()->teamsubmission && !$assign->can_view_submission($userid)) { return false; } $relativepath = implode('/', $args); $fullpath = "/{$context->id}/assignsubmission_file/$filearea/$itemid/$relativepath"; $fs = get_file_storage(); if (!($file = $fs->get_file_by_hash(sha1($fullpath))) || $file->is_directory()) { return false; } // Download MUST be forced - security! send_stored_file($file, 0, 0, true, $options); } submission/file/version.php000064400000002113152265517350012064 0ustar00. /** * This file contains the version information for the file submission plugin * * @package assignsubmission_file * @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(); $plugin->version = 2025041400; $plugin->requires = 2025040800; $plugin->component = 'assignsubmission_file'; submission/file/classes/privacy/provider.php000064400000016445152265517350015360 0ustar00. /** * Privacy class for requesting user data. * * @package assignsubmission_file * @copyright 2018 Adrian Greeve * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace assignsubmission_file\privacy; defined('MOODLE_INTERNAL') || die(); require_once($CFG->dirroot . '/mod/assign/locallib.php'); use \core_privacy\local\metadata\collection; use \core_privacy\local\request\writer; use \core_privacy\local\request\contextlist; use \mod_assign\privacy\assign_plugin_request_data; /** * Privacy class for requesting user data. * * @package assignsubmission_file * @copyright 2018 Adrian Greeve * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class provider implements \core_privacy\local\metadata\provider, \mod_assign\privacy\assignsubmission_provider, \mod_assign\privacy\assignsubmission_user_provider { /** * Return meta data about this plugin. * * @param collection $collection A list of information to add to. * @return collection Return the collection after adding to it. */ public static function get_metadata(collection $collection): collection { $collection->link_subsystem('core_files', 'privacy:metadata:filepurpose'); return $collection; } /** * This is covered by mod_assign provider and the query on assign_submissions. * * @param int $userid The user ID that we are finding contexts for. * @param contextlist $contextlist A context list to add sql and params to for contexts. */ public static function get_context_for_userid_within_submission(int $userid, contextlist $contextlist) { // This is already fetched from mod_assign. } /** * This is also covered by the mod_assign provider and it's queries. * * @param \mod_assign\privacy\useridlist $useridlist An object for obtaining user IDs of students. */ public static function get_student_user_ids(\mod_assign\privacy\useridlist $useridlist) { // No need. } /** * If you have tables that contain userids and you can generate entries in your tables without creating an * entry in the assign_submission table then please fill in this method. * * @param userlist $userlist The userlist object */ public static function get_userids_from_context(\core_privacy\local\request\userlist $userlist) { // Not required. } /** * Export all user data for this plugin. * * @param assign_plugin_request_data $exportdata Data used to determine which context and user to export and other useful * information to help with exporting. */ public static function export_submission_user_data(assign_plugin_request_data $exportdata) { // We currently don't show submissions to teachers when exporting their data. $context = $exportdata->get_context(); if ($exportdata->get_user() != null) { return null; } $user = new \stdClass(); $assign = $exportdata->get_assign(); $plugin = $assign->get_plugin_by_type('assignsubmission', 'file'); $files = $plugin->get_files($exportdata->get_pluginobject(), $user); foreach ($files as $file) { $userid = $exportdata->get_pluginobject()->userid; writer::with_context($exportdata->get_context())->export_file($exportdata->get_subcontext(), $file); // Plagiarism data. $coursecontext = $context->get_course_context(); \core_plagiarism\privacy\provider::export_plagiarism_user_data($userid, $context, $exportdata->get_subcontext(), [ 'cmid' => $context->instanceid, 'course' => $coursecontext->instanceid, 'userid' => $userid, 'file' => $file ]); } } /** * Any call to this method should delete all user data for the context defined in the deletion_criteria. * * @param assign_plugin_request_data $requestdata Information useful for deleting user data. */ public static function delete_submission_for_context(assign_plugin_request_data $requestdata) { global $DB; \core_plagiarism\privacy\provider::delete_plagiarism_for_context($requestdata->get_context()); $fs = get_file_storage(); $fs->delete_area_files($requestdata->get_context()->id, 'assignsubmission_file', ASSIGNSUBMISSION_FILE_FILEAREA); // Delete records from assignsubmission_file table. $DB->delete_records('assignsubmission_file', ['assignment' => $requestdata->get_assign()->get_instance()->id]); } /** * A call to this method should delete user data (where practical) using the userid and submission. * * @param assign_plugin_request_data $deletedata Details about the user and context to focus the deletion. */ public static function delete_submission_for_userid(assign_plugin_request_data $deletedata) { global $DB; \core_plagiarism\privacy\provider::delete_plagiarism_for_user($deletedata->get_user()->id, $deletedata->get_context()); $submissionid = $deletedata->get_pluginobject()->id; $fs = get_file_storage(); $fs->delete_area_files($deletedata->get_context()->id, 'assignsubmission_file', ASSIGNSUBMISSION_FILE_FILEAREA, $submissionid); $DB->delete_records('assignsubmission_file', ['assignment' => $deletedata->get_assignid(), 'submission' => $submissionid]); } /** * Deletes all submissions for the submission ids / userids provided in a context. * assign_plugin_request_data contains: * - context * - assign object * - submission ids (pluginids) * - user ids * @param assign_plugin_request_data $deletedata A class that contains the relevant information required for deletion. */ public static function delete_submissions(assign_plugin_request_data $deletedata) { global $DB; \core_plagiarism\privacy\provider::delete_plagiarism_for_users($deletedata->get_userids(), $deletedata->get_context()); if (empty($deletedata->get_submissionids())) { return; } $fs = get_file_storage(); list($sql, $params) = $DB->get_in_or_equal($deletedata->get_submissionids(), SQL_PARAMS_NAMED); $fs->delete_area_files_select($deletedata->get_context()->id, 'assignsubmission_file', ASSIGNSUBMISSION_FILE_FILEAREA, $sql, $params); $params['assignid'] = $deletedata->get_assignid(); $DB->delete_records_select('assignsubmission_file', "assignment = :assignid AND submission $sql", $params); } } submission/file/classes/event/submission_created.php000064400000005451152265517350017047 0ustar00. /** * The assignsubmission_file submission_created event. * * @package assignsubmission_file * @copyright 2014 Adrian Greeve * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace assignsubmission_file\event; defined('MOODLE_INTERNAL') || die(); /** * The assignsubmission_file submission_created event class. * * @property-read array $other { * Extra information about the event. * * - int filesubmissioncount: The number of files uploaded. * } * * @package assignsubmission_file * @since Moodle 2.7 * @copyright 2014 Adrian Greeve * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class submission_created extends \mod_assign\event\submission_created { /** * Init method. */ protected function init() { parent::init(); $this->data['objecttable'] = 'assignsubmission_file'; } /** * Returns non-localised description of what happened. * * @return string */ public function get_description() { $descriptionstring = "The user with id '$this->userid' created a file submission and uploaded " . "'{$this->other['filesubmissioncount']}' file/s in the assignment with course module id " . "'$this->contextinstanceid'"; if (!empty($this->other['groupid'])) { $descriptionstring .= " for the group with id '{$this->other['groupid']}'."; } else { $descriptionstring .= "."; } return $descriptionstring; } /** * Custom validation. * * @throws \coding_exception * @return void */ protected function validate_data() { parent::validate_data(); if (!isset($this->other['filesubmissioncount'])) { throw new \coding_exception('The \'filesubmissioncount\' value must be set in other.'); } } public static function get_objectid_mapping() { // No mapping available for 'assignsubmission_file'. return array('db' => 'assignsubmission_file', 'restore' => \core\event\base::NOT_MAPPED); } } submission/file/classes/event/submission_updated.php000064400000005451152265517350017066 0ustar00. /** * The assignsubmission_file submission_updated event. * * @package assignsubmission_file * @copyright 2014 Adrian Greeve * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace assignsubmission_file\event; defined('MOODLE_INTERNAL') || die(); /** * The assignsubmission_file submission_updated event class. * * @property-read array $other { * Extra information about the event. * * - int filesubmissioncount: The number of files uploaded. * } * * @package assignsubmission_file * @since Moodle 2.7 * @copyright 2014 Adrian Greeve * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class submission_updated extends \mod_assign\event\submission_updated { /** * Init method. */ protected function init() { parent::init(); $this->data['objecttable'] = 'assignsubmission_file'; } /** * Returns non-localised description of what happened. * * @return string */ public function get_description() { $descriptionstring = "The user with id '$this->userid' updated a file submission and uploaded " . "'{$this->other['filesubmissioncount']}' file/s in the assignment with course module id " . "'$this->contextinstanceid'"; if (!empty($this->other['groupid'])) { $descriptionstring .= " for the group with id '{$this->other['groupid']}'."; } else { $descriptionstring .= "."; } return $descriptionstring; } /** * Custom validation. * * @throws \coding_exception * @return void */ protected function validate_data() { parent::validate_data(); if (!isset($this->other['filesubmissioncount'])) { throw new \coding_exception('The \'filesubmissioncount\' value must be set in other.'); } } public static function get_objectid_mapping() { // No mapping available for 'assignsubmission_file'. return array('db' => 'assignsubmission_file', 'restore' => \core\event\base::NOT_MAPPED); } } submission/file/classes/event/assessable_uploaded.php000064400000005267152265517350017174 0ustar00. /** * The assignsubmission_file assessable uploaded event. * * @package assignsubmission_file * @copyright 2013 Frédéric Massart * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace assignsubmission_file\event; defined('MOODLE_INTERNAL') || die(); /** * The assignsubmission_file assessable uploaded event class. * * @package assignsubmission_file * @since Moodle 2.6 * @copyright 2013 Frédéric Massart * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class assessable_uploaded extends \core\event\assessable_uploaded { /** * Legacy event files. * * @var array */ protected $legacyfiles = array(); /** * Returns description of what happened. * * @return string */ public function get_description() { return "The user with id '$this->userid' has uploaded a file to the submission with id '$this->objectid' " . "in the assignment activity with course module id '$this->contextinstanceid'."; } /** * Return localised event name. * * @return string */ public static function get_name() { return get_string('eventassessableuploaded', 'assignsubmission_file'); } /** * Get URL related to the action. * * @return \moodle_url */ public function get_url() { return new \moodle_url('/mod/assign/view.php', array('id' => $this->contextinstanceid)); } /** * Sets the legacy event data. * * @param \stdClass $legacyfiles legacy event data. * @return void */ public function set_legacy_files($legacyfiles) { $this->legacyfiles = $legacyfiles; } /** * Init method. * * @return void */ protected function init() { parent::init(); $this->data['objecttable'] = 'assign_submission'; } public static function get_objectid_mapping() { return array('db' => 'assign_submission', 'restore' => 'submission'); } } submission/file/templates/email_summary_text.mustache000064400000003102152265517350017326 0ustar00{{! This file is part of Moodle - http://moodle.org/ Moodle is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Moodle is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Moodle. If not, see . }} {{! @template assignsubmission_file/email_summary_text This template is used render the plain text version of the summary of files submissions to go into emails. Classes required for JS: * none Context variables required for this template: * files - array of sumbitted files, each with properties filepath and filesize. Example context (json): { "files": [ { "filepath": "File 1.docx", "filesize": "12.3KB" }, { "filepath": "subdir/extra data.txt", "filesize": "456B" } ] } }} {{#str}}file, assignsubmission_file{{/str}} {{#files}} * {{#str}}filewithsize, assignsubmission_file, {"filename": {{#quote}}{{filepath}}{{/quote}}, "size": {{#quote}}{{filesize}}{{/quote}}, "coursename": {{#quote}}{{coursename}}{{/quote}} } {{/str}} {{/files}} submission/file/templates/email_summary_html.mustache000064400000003134152265517350017313 0ustar00{{! This file is part of Moodle - http://moodle.org/ Moodle is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Moodle is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Moodle. If not, see . }} {{! @template assignsubmission_file/email_summary_html This template is used render the HTML version of the summary of files submissions to go into emails. Classes required for JS: * none Context variables required for this template: * files - array of sumbitted files, each with properties filepath and filesize. Example context (json): { "files": [ { "filepath": "File 1.docx", "filesize": "12.3KB" }, { "filepath": "subdir/extra data.txt", "filesize": "456B" } ] } }}

{{#str}}file, assignsubmission_file{{/str}}

    {{#files}}
  • {{#str}}filewithsize, assignsubmission_file, {"filename": {{#quote}}{{filepath}}{{/quote}}, "size": {{#quote}}{{filesize}}{{/quote}}, "coursename": {{#quote}}{{coursename}}{{/quote}} } {{/str}}
  • {{/files}}
submission/file/locallib.php000064400000065113152265517350012171 0ustar00. /** * This file contains the definition for the library class for file submission plugin * * This class provides all the functionality for the new assign module. * * @package assignsubmission_file * @copyright 2012 NetSpot {@link http://www.netspot.com.au} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ use core_external\external_value; // File areas for file submission assignment. define('ASSIGNSUBMISSION_FILE_MAXSUMMARYFILES', 5); define('ASSIGNSUBMISSION_FILE_FILEAREA', 'submission_files'); /** * Library class for file submission plugin extending submission plugin base class * * @package assignsubmission_file * @copyright 2012 NetSpot {@link http://www.netspot.com.au} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class assign_submission_file extends assign_submission_plugin { /** * Get the name of the file submission plugin * @return string */ public function get_name() { return get_string('file', 'assignsubmission_file'); } /** * Get file submission information from the database * * @param int $submissionid * @return mixed */ private function get_file_submission($submissionid) { global $DB; return $DB->get_record('assignsubmission_file', array('submission'=>$submissionid)); } /** * Get the default setting for file submission plugin * * @param MoodleQuickForm $mform The form to add elements to * @return void */ public function get_settings(MoodleQuickForm $mform) { global $CFG, $COURSE; if ($this->assignment->has_instance()) { $defaultmaxfilesubmissions = $this->get_config('maxfilesubmissions'); $defaultmaxsubmissionsizebytes = $this->get_config('maxsubmissionsizebytes'); $defaultfiletypes = $this->get_config('filetypeslist'); } else { $defaultmaxfilesubmissions = get_config('assignsubmission_file', 'maxfiles'); $defaultmaxsubmissionsizebytes = get_config('assignsubmission_file', 'maxbytes'); $defaultfiletypes = get_config('assignsubmission_file', 'filetypes'); } $defaultfiletypes = (string)$defaultfiletypes; $settings = array(); $options = array(); for ($i = 1; $i <= get_config('assignsubmission_file', 'maxfiles'); $i++) { $options[$i] = $i; } $name = get_string('maxfilessubmission', 'assignsubmission_file'); $mform->addElement('select', 'assignsubmission_file_maxfiles', $name, $options); $mform->addHelpButton('assignsubmission_file_maxfiles', 'maxfilessubmission', 'assignsubmission_file'); $mform->setDefault('assignsubmission_file_maxfiles', $defaultmaxfilesubmissions); $mform->hideIf('assignsubmission_file_maxfiles', 'assignsubmission_file_enabled', 'notchecked'); $choices = get_max_upload_sizes($CFG->maxbytes, $COURSE->maxbytes, get_config('assignsubmission_file', 'maxbytes')); $settings[] = array('type' => 'select', 'name' => 'maxsubmissionsizebytes', 'description' => get_string('maximumsubmissionsize', 'assignsubmission_file'), 'options'=> $choices, 'default'=> $defaultmaxsubmissionsizebytes); $name = get_string('maximumsubmissionsize', 'assignsubmission_file'); $mform->addElement('select', 'assignsubmission_file_maxsizebytes', $name, $choices); $mform->addHelpButton('assignsubmission_file_maxsizebytes', 'maximumsubmissionsize', 'assignsubmission_file'); $mform->setDefault('assignsubmission_file_maxsizebytes', $defaultmaxsubmissionsizebytes); $mform->hideIf('assignsubmission_file_maxsizebytes', 'assignsubmission_file_enabled', 'notchecked'); $name = get_string('acceptedfiletypes', 'assignsubmission_file'); $mform->addElement('filetypes', 'assignsubmission_file_filetypes', $name); $mform->addHelpButton('assignsubmission_file_filetypes', 'acceptedfiletypes', 'assignsubmission_file'); $mform->setDefault('assignsubmission_file_filetypes', $defaultfiletypes); $mform->hideIf('assignsubmission_file_filetypes', 'assignsubmission_file_enabled', 'notchecked'); } /** * Save the settings for file submission plugin * * @param stdClass $data * @return bool */ public function save_settings(stdClass $data) { $this->set_config('maxfilesubmissions', $data->assignsubmission_file_maxfiles); $this->set_config('maxsubmissionsizebytes', $data->assignsubmission_file_maxsizebytes); if (!empty($data->assignsubmission_file_filetypes)) { $this->set_config('filetypeslist', $data->assignsubmission_file_filetypes); } else { $this->set_config('filetypeslist', ''); } return true; } /** * File format options * * @return array */ private function get_file_options() { $fileoptions = array('subdirs' => 1, 'maxbytes' => $this->get_config('maxsubmissionsizebytes'), 'maxfiles' => $this->get_config('maxfilesubmissions'), 'accepted_types' => $this->get_configured_typesets(), 'return_types' => (FILE_INTERNAL | FILE_CONTROLLED_LINK)); if ($fileoptions['maxbytes'] == 0) { // Use module default. $fileoptions['maxbytes'] = get_config('assignsubmission_file', 'maxbytes'); } return $fileoptions; } /** * Add elements to submission form * * @param mixed $submission stdClass|null * @param MoodleQuickForm $mform * @param stdClass $data * @return bool */ public function get_form_elements($submission, MoodleQuickForm $mform, stdClass $data) { global $OUTPUT; if ($this->get_config('maxfilesubmissions') <= 0) { return false; } $fileoptions = $this->get_file_options(); $submissionid = $submission ? $submission->id : 0; $data = file_prepare_standard_filemanager($data, 'files', $fileoptions, $this->assignment->get_context(), 'assignsubmission_file', ASSIGNSUBMISSION_FILE_FILEAREA, $submissionid); $mform->addElement('filemanager', 'files_filemanager', $this->get_name(), null, $fileoptions); return true; } /** * Count the number of files * * @param int $submissionid * @param string $area * @return int */ private function count_files($submissionid, $area) { $fs = get_file_storage(); $files = $fs->get_area_files($this->assignment->get_context()->id, 'assignsubmission_file', $area, $submissionid, 'id', false); return count($files); } /** * Save the files and trigger plagiarism plugin, if enabled, * to scan the uploaded files via events trigger * * @param stdClass $submission * @param stdClass $data * @return bool */ public function save(stdClass $submission, stdClass $data) { global $USER, $DB; $fileoptions = $this->get_file_options(); $data = file_postupdate_standard_filemanager($data, 'files', $fileoptions, $this->assignment->get_context(), 'assignsubmission_file', ASSIGNSUBMISSION_FILE_FILEAREA, $submission->id); $filesubmission = $this->get_file_submission($submission->id); // Plagiarism code event trigger when files are uploaded. $fs = get_file_storage(); $files = $fs->get_area_files($this->assignment->get_context()->id, 'assignsubmission_file', ASSIGNSUBMISSION_FILE_FILEAREA, $submission->id, 'id', false); $count = $this->count_files($submission->id, ASSIGNSUBMISSION_FILE_FILEAREA); $params = array( 'context' => context_module::instance($this->assignment->get_course_module()->id), 'courseid' => $this->assignment->get_course()->id, 'objectid' => $submission->id, 'other' => array( 'content' => '', 'pathnamehashes' => array_keys($files) ) ); if (!empty($submission->userid) && ($submission->userid != $USER->id)) { $params['relateduserid'] = $submission->userid; } if ($this->assignment->is_blind_marking()) { $params['anonymous'] = 1; } $event = \assignsubmission_file\event\assessable_uploaded::create($params); $event->set_legacy_files($files); $event->trigger(); $groupname = null; $groupid = 0; // Get the group name as other fields are not transcribed in the logs and this information is important. if (empty($submission->userid) && !empty($submission->groupid)) { $groupname = $DB->get_field('groups', 'name', array('id' => $submission->groupid), MUST_EXIST); $groupid = $submission->groupid; } else { $params['relateduserid'] = $submission->userid; } // Unset the objectid and other field from params for use in submission events. unset($params['objectid']); unset($params['other']); $params['other'] = array( 'submissionid' => $submission->id, 'submissionattempt' => $submission->attemptnumber, 'submissionstatus' => $submission->status, 'filesubmissioncount' => $count, 'groupid' => $groupid, 'groupname' => $groupname ); if ($filesubmission) { $filesubmission->numfiles = $this->count_files($submission->id, ASSIGNSUBMISSION_FILE_FILEAREA); $updatestatus = $DB->update_record('assignsubmission_file', $filesubmission); $params['objectid'] = $filesubmission->id; $event = \assignsubmission_file\event\submission_updated::create($params); $event->set_assign($this->assignment); $event->trigger(); return $updatestatus; } else { $filesubmission = new stdClass(); $filesubmission->numfiles = $this->count_files($submission->id, ASSIGNSUBMISSION_FILE_FILEAREA); $filesubmission->submission = $submission->id; $filesubmission->assignment = $this->assignment->get_instance()->id; $filesubmission->id = $DB->insert_record('assignsubmission_file', $filesubmission); $params['objectid'] = $filesubmission->id; $event = \assignsubmission_file\event\submission_created::create($params); $event->set_assign($this->assignment); $event->trigger(); return $filesubmission->id > 0; } } /** * Remove files from this submission. * * @param stdClass $submission The submission * @return boolean */ public function remove(stdClass $submission) { global $DB; $fs = get_file_storage(); $fs->delete_area_files($this->assignment->get_context()->id, 'assignsubmission_file', ASSIGNSUBMISSION_FILE_FILEAREA, $submission->id); $currentsubmission = $this->get_file_submission($submission->id); if ($currentsubmission) { $currentsubmission->numfiles = 0; $DB->update_record('assignsubmission_file', $currentsubmission); } return true; } /** * Produce a list of files suitable for export that represent this feedback or submission * * @param stdClass $submission The submission * @param stdClass $user The user record - unused * @return array - return an array of files indexed by filename */ public function get_files(stdClass $submission, stdClass $user) { $result = array(); $fs = get_file_storage(); $files = $fs->get_area_files($this->assignment->get_context()->id, 'assignsubmission_file', ASSIGNSUBMISSION_FILE_FILEAREA, $submission->id, 'timemodified, id', false); foreach ($files as $file) { // Do we return the full folder path or just the file name? if (isset($submission->exportfullpath) && $submission->exportfullpath == false) { $result[$file->get_filename()] = $file; } else { $result[$file->get_filepath().$file->get_filename()] = $file; } } return $result; } /** * Display the list of files in the submission status table * * @param stdClass $submission * @param bool $showviewlink Set this to true if the list of files is long * @return string */ public function view_summary(stdClass $submission, & $showviewlink) { $count = $this->count_files($submission->id, ASSIGNSUBMISSION_FILE_FILEAREA); // Show we show a link to view all files for this plugin? $showviewlink = $count > ASSIGNSUBMISSION_FILE_MAXSUMMARYFILES; if ($count <= ASSIGNSUBMISSION_FILE_MAXSUMMARYFILES) { return $this->assignment->render_area_files('assignsubmission_file', ASSIGNSUBMISSION_FILE_FILEAREA, $submission->id); } else { return get_string('countfiles', 'assignsubmission_file', $count); } } #[\Override] public function submission_summary_for_messages(stdClass $submission): array { global $PAGE; $files = new assign_files($this->assignment->get_context(), $submission->id, ASSIGNSUBMISSION_FILE_FILEAREA, 'assignsubmission_file', $this->assignment->get_course(), $this->assignment->get_course_module()); $filelist = $this->flatten_list_of_files($files->dir); if (!$filelist) { return ['', '']; } $renderer = $PAGE->get_renderer('mod_assign'); $templatecontext = ['files' => $filelist]; return [ // Mustache strips off all trailing whitespace, but we want a newline at the end. $renderer->render_from_template( 'assignsubmission_file/email_summary_text', $templatecontext) . "\n", $renderer->render_from_template( 'assignsubmission_file/email_summary_html', $templatecontext), ]; } /** * Turn the hierarchical list of files into a flat array. * * The returned array looks like * [ * ['filepath' => 'File 1.docx', 'filesize' => '12.3KB'], * ['filepath' => 'subdir/extra data.txt, 'filesize' => '456B'], * ]. * * @param array $dir as returned by {@see file_storage::get_area_tree()}. * @return array as above. */ private function flatten_list_of_files(array $dir): array { if (empty($dir['subdirs']) && empty($dir['files'])) { return []; } $filessummary = []; /** @var stored_file $file */ foreach ($dir['files'] as $file) { $filessummary[] = [ 'filepath' => ltrim($file->get_filepath(), '/') . $file->get_filename(), 'filesize' => display_size($file->get_filesize()), ]; } foreach ($dir['subdirs'] as $subdir) { $files = $this->flatten_list_of_files($subdir); $filessummary = array_merge($filessummary, $files); } return $filessummary; } /** * No full submission view - the summary contains the list of files and that is the whole submission * * @param stdClass $submission * @return string */ public function view(stdClass $submission) { return $this->assignment->render_area_files('assignsubmission_file', ASSIGNSUBMISSION_FILE_FILEAREA, $submission->id); } /** * Return true if this plugin can upgrade an old Moodle 2.2 assignment of this type * and version. * * @param string $type * @param int $version * @return bool True if upgrade is possible */ public function can_upgrade($type, $version) { $uploadsingletype ='uploadsingle'; $uploadtype ='upload'; if (($type == $uploadsingletype || $type == $uploadtype) && $version >= 2011112900) { return true; } return false; } /** * Upgrade the settings from the old assignment * to the new plugin based one * * @param context $oldcontext - the old assignment context * @param stdClass $oldassignment - the old assignment data record * @param string $log record log events here * @return bool Was it a success? (false will trigger rollback) */ public function upgrade_settings(context $oldcontext, stdClass $oldassignment, & $log) { global $DB; if ($oldassignment->assignmenttype == 'uploadsingle') { $this->set_config('maxfilesubmissions', 1); $this->set_config('maxsubmissionsizebytes', $oldassignment->maxbytes); return true; } else if ($oldassignment->assignmenttype == 'upload') { $this->set_config('maxfilesubmissions', $oldassignment->var1); $this->set_config('maxsubmissionsizebytes', $oldassignment->maxbytes); // Advanced file upload uses a different setting to do the same thing. $DB->set_field('assign', 'submissiondrafts', $oldassignment->var4, array('id'=>$this->assignment->get_instance()->id)); // Convert advanced file upload "hide description before due date" setting. $alwaysshow = 0; if (!$oldassignment->var3) { $alwaysshow = 1; } $DB->set_field('assign', 'alwaysshowdescription', $alwaysshow, array('id'=>$this->assignment->get_instance()->id)); return true; } } /** * Upgrade the submission from the old assignment to the new one * * @param context $oldcontext The context of the old assignment * @param stdClass $oldassignment The data record for the old oldassignment * @param stdClass $oldsubmission The data record for the old submission * @param stdClass $submission The data record for the new submission * @param string $log Record upgrade messages in the log * @return bool true or false - false will trigger a rollback */ public function upgrade(context $oldcontext, stdClass $oldassignment, stdClass $oldsubmission, stdClass $submission, & $log) { global $DB; $filesubmission = new stdClass(); $filesubmission->numfiles = $oldsubmission->numfiles; $filesubmission->submission = $submission->id; $filesubmission->assignment = $this->assignment->get_instance()->id; if (!$DB->insert_record('assignsubmission_file', $filesubmission) > 0) { $log .= get_string('couldnotconvertsubmission', 'mod_assign', $submission->userid); return false; } // Now copy the area files. $this->assignment->copy_area_files_for_upgrade($oldcontext->id, 'mod_assignment', 'submission', $oldsubmission->id, $this->assignment->get_context()->id, 'assignsubmission_file', ASSIGNSUBMISSION_FILE_FILEAREA, $submission->id); return true; } /** * The assignment has been deleted - cleanup * * @return bool */ public function delete_instance() { global $DB; // Will throw exception on failure. $DB->delete_records('assignsubmission_file', array('assignment'=>$this->assignment->get_instance()->id)); return true; } /** * Return true if there are no submission files * @param stdClass $submission */ public function is_empty(stdClass $submission) { return $this->count_files($submission->id, ASSIGNSUBMISSION_FILE_FILEAREA) == 0; } /** * Determine if a submission is empty * * This is distinct from is_empty in that it is intended to be used to * determine if a submission made before saving is empty. * * @param stdClass $data The submission data * @return bool */ public function submission_is_empty(stdClass $data) { global $USER; $fs = get_file_storage(); // Get a count of all the draft files, excluding any directories. $files = $fs->get_area_files(context_user::instance($USER->id)->id, 'user', 'draft', $data->files_filemanager, 'id', false); return count($files) == 0; } /** * Get file areas returns a list of areas this plugin stores files * @return array - An array of fileareas (keys) and descriptions (values) */ public function get_file_areas() { return array(ASSIGNSUBMISSION_FILE_FILEAREA=>$this->get_name()); } /** * Copy the student's submission from a previous submission. Used when a student opts to base their resubmission * on the last submission. * @param stdClass $sourcesubmission * @param stdClass $destsubmission */ public function copy_submission(stdClass $sourcesubmission, stdClass $destsubmission) { global $DB; // Copy the files across. $contextid = $this->assignment->get_context()->id; $fs = get_file_storage(); $files = $fs->get_area_files($contextid, 'assignsubmission_file', ASSIGNSUBMISSION_FILE_FILEAREA, $sourcesubmission->id, 'id', false); foreach ($files as $file) { $fieldupdates = array('itemid' => $destsubmission->id); $fs->create_file_from_storedfile($fieldupdates, $file); } // Copy the assignsubmission_file record. if ($filesubmission = $this->get_file_submission($sourcesubmission->id)) { unset($filesubmission->id); $filesubmission->submission = $destsubmission->id; $DB->insert_record('assignsubmission_file', $filesubmission); } return true; } /** * Return a description of external params suitable for uploading a file submission from a webservice. * * @return \core_external\external_description|null */ public function get_external_parameters() { return array( 'files_filemanager' => new external_value( PARAM_INT, 'The id of a draft area containing files for this submission.', VALUE_OPTIONAL ) ); } /** * Return the plugin configs for external functions. * * @return array the list of settings * @since Moodle 3.2 */ public function get_config_for_external() { global $CFG; $configs = $this->get_config(); // Get a size in bytes. if ($configs->maxsubmissionsizebytes == 0) { $configs->maxsubmissionsizebytes = get_max_upload_file_size($CFG->maxbytes, $this->assignment->get_course()->maxbytes, get_config('assignsubmission_file', 'maxbytes')); } return (array) $configs; } /** * Get the type sets configured for this assignment. * * @return array('groupname', 'mime/type', ...) */ private function get_configured_typesets() { $typeslist = (string)$this->get_config('filetypeslist'); $util = new \core_form\filetypes_util(); $sets = $util->normalize_file_types($typeslist); return $sets; } /** * Determine if the plugin allows image file conversion * @return bool */ public function allow_image_conversion() { return true; } } submission/file/tests/generator/lib.php000064400000005226152265517350014305 0ustar00. require_once("{$CFG->dirroot}/mod/assign/tests/generator/assignsubmission_subplugin_generator.php"); /** * Online Text assignment submission subplugin data generator. * * @package assignsubmission_file * @category test * @copyright 2021 Andrew Lyons * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class assignsubmission_file_generator extends assignsubmission_subplugin_generator { /** * Add submission data in the correct format for a call to `assign::save_submission()` from a table containing * submission data for a single activity. * * Data should be added to the $submission object passed into the function. * * @param stdClass $submission The submission record to be modified * @param assign $assign The assignment being submitted to * @param array $data The data received */ public function add_submission_data(stdClass $submission, assign $assign, array $data): void { global $CFG; if (array_key_exists('file', $data)) { $files = explode(',', $data['file']); $itemid = file_get_unused_draft_itemid(); $fs = get_file_storage(); foreach ($files as $filepath) { // All paths are relative to $CFG->dirroot. $filepath = trim($filepath); $filepath = "{$CFG->dirroot}/{$filepath}"; $filename = basename($filepath); $fs->create_file_from_pathname((object) [ 'itemid' => $itemid, 'contextid' => context_user::instance($submission->userid)->id, 'component' => 'user', 'filearea' => 'draft', 'filepath' => '/', 'filename' => $filename, ], $filepath); } $submission->files_filemanager = $itemid; $submission->file_editor = [ 'itemid' => $itemid, ]; } } } submission/file/tests/privacy/provider_test.php000064400000025110152265517350016111 0ustar00. namespace assignsubmission_file\privacy; /** * Unit tests for mod/assign/submission/file/classes/privacy/ * * @package assignsubmission_file * @copyright 2018 Adrian Greeve * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @covers \assignsubmission_file\privacy\provider */ final class provider_test extends \mod_assign\tests\provider_testcase { /** * Convenience function for creating feedback data. * * @param object $assign assign object * @param stdClass $student user object * @param string $filename filename for the file submission * @return array Submission plugin object and the submission object. */ protected function create_file_submission($assign, $student, $filename) { global $CFG; // Create a file submission with the test pdf. $submission = $assign->get_user_submission($student->id, true); $this->setUser($student->id); $fs = get_file_storage(); $pdfsubmission = (object) array( 'contextid' => $assign->get_context()->id, 'component' => 'assignsubmission_file', 'filearea' => ASSIGNSUBMISSION_FILE_FILEAREA, 'itemid' => $submission->id, 'filepath' => '/', 'filename' => $filename ); $sourcefile = $CFG->dirroot.'/mod/assign/feedback/editpdf/tests/fixtures/submission.pdf'; $fi = $fs->create_file_from_pathname($pdfsubmission, $sourcefile); $data = new \stdClass(); $plugin = $assign->get_submission_plugin_by_type('file'); $plugin->save($submission, $data); return [$plugin, $submission]; } /** * Quick test to make sure that get_metadata returns something. */ public function test_get_metadata(): void { $collection = new \core_privacy\local\metadata\collection('assignsubmission_file'); $collection = \assignsubmission_file\privacy\provider::get_metadata($collection); $this->assertNotEmpty($collection); } /** * Test that submission files are exported for a user. */ public function test_export_submission_user_data(): void { $this->resetAfterTest(); // Create course, assignment, submission, and then a feedback comment. $course = $this->getDataGenerator()->create_course(); // Student. $user1 = $this->getDataGenerator()->create_user(); // Teacher. $user2 = $this->getDataGenerator()->create_user(); $this->getDataGenerator()->enrol_user($user1->id, $course->id, 'student'); $this->getDataGenerator()->enrol_user($user2->id, $course->id, 'editingteacher'); $assign = $this->create_instance(['course' => $course]); $context = $assign->get_context(); $studentfilename = 'user1file.pdf'; list($plugin, $submission) = $this->create_file_submission($assign, $user1, $studentfilename); $writer = \core_privacy\local\request\writer::with_context($context); $this->assertFalse($writer->has_any_data()); // The student should have a file submission. $exportdata = new \mod_assign\privacy\assign_plugin_request_data($context, $assign, $submission, ['Attempt 1']); \assignsubmission_file\privacy\provider::export_submission_user_data($exportdata); // print_object($writer); $storedfile = $writer->get_files(['Attempt 1'])['user1file.pdf']; $this->assertInstanceOf('stored_file', $storedfile); $this->assertEquals($studentfilename, $storedfile->get_filename()); } /** * Test that all submission files are deleted for this context. */ public function test_delete_submission_for_context(): void { $this->resetAfterTest(); // Create course, assignment, submission, and then a feedback comment. $course = $this->getDataGenerator()->create_course(); // Student. $user1 = $this->getDataGenerator()->create_user(); $user2 = $this->getDataGenerator()->create_user(); $this->getDataGenerator()->enrol_user($user1->id, $course->id, 'student'); $this->getDataGenerator()->enrol_user($user2->id, $course->id, 'student'); $assign = $this->create_instance(['course' => $course]); $context = $assign->get_context(); $studentfilename = 'user1file.pdf'; list($plugin, $submission) = $this->create_file_submission($assign, $user1, $studentfilename); $student2filename = 'user2file.pdf'; list($plugin2, $submission2) = $this->create_file_submission($assign, $user2, $studentfilename); // Only need the context and assign object in this plugin for this operation. $requestdata = new \mod_assign\privacy\assign_plugin_request_data($context, $assign); \assignsubmission_file\privacy\provider::delete_submission_for_context($requestdata); // This checks that there are no files in this submission. $this->assertTrue($plugin->is_empty($submission)); $this->assertTrue($plugin2->is_empty($submission2)); } /** * Test that the comments for a user are deleted. */ public function test_delete_submission_for_userid(): void { $this->resetAfterTest(); // Create course, assignment, submission, and then a feedback comment. $course = $this->getDataGenerator()->create_course(); // Student. $user1 = $this->getDataGenerator()->create_user(); $user2 = $this->getDataGenerator()->create_user(); $this->getDataGenerator()->enrol_user($user1->id, $course->id, 'student'); $this->getDataGenerator()->enrol_user($user2->id, $course->id, 'student'); $assign = $this->create_instance(['course' => $course]); $context = $assign->get_context(); $studentfilename = 'user1file.pdf'; list($plugin, $submission) = $this->create_file_submission($assign, $user1, $studentfilename); $student2filename = 'user2file.pdf'; list($plugin2, $submission2) = $this->create_file_submission($assign, $user2, $studentfilename); // Only need the context and assign object in this plugin for this operation. $requestdata = new \mod_assign\privacy\assign_plugin_request_data($context, $assign, $submission, [], $user1); \assignsubmission_file\privacy\provider::delete_submission_for_userid($requestdata); // This checks that there are no files in this submission. $this->assertTrue($plugin->is_empty($submission)); // There should be files here. $this->assertFalse($plugin2->is_empty($submission2)); } /** * Test deletion of bulk submissions for a context. */ public function test_delete_submissions(): void { global $DB; $this->resetAfterTest(); // Create course, assignment, submission, and then a feedback comment. $course = $this->getDataGenerator()->create_course(); // Student. $user1 = $this->getDataGenerator()->create_user(); $user2 = $this->getDataGenerator()->create_user(); $user3 = $this->getDataGenerator()->create_user(); $user4 = $this->getDataGenerator()->create_user(); $this->getDataGenerator()->enrol_user($user1->id, $course->id, 'student'); $this->getDataGenerator()->enrol_user($user2->id, $course->id, 'student'); $this->getDataGenerator()->enrol_user($user3->id, $course->id, 'student'); $this->getDataGenerator()->enrol_user($user4->id, $course->id, 'student'); $assign1 = $this->create_instance(['course' => $course]); $assign2 = $this->create_instance(['course' => $course]); $context1 = $assign1->get_context(); $context2 = $assign2->get_context(); $student1filename = 'user1file.pdf'; list($plugin1, $submission1) = $this->create_file_submission($assign1, $user1, $student1filename); $student2filename = 'user2file.pdf'; list($plugin2, $submission2) = $this->create_file_submission($assign1, $user2, $student2filename); $student3filename = 'user3file.pdf'; list($plugin3, $submission3) = $this->create_file_submission($assign1, $user3, $student3filename); $student4filename = 'user4file.pdf'; list($plugin4, $submission4) = $this->create_file_submission($assign2, $user4, $student4filename); $student5filename = 'user5file.pdf'; list($plugin5, $submission5) = $this->create_file_submission($assign2, $user3, $student5filename); $submissionids = [ $submission1->id, $submission3->id ]; $userids = [ $user1->id, $user3->id ]; $data = $DB->get_records('files', ['contextid' => $context1->id, 'component' => 'assignsubmission_file']); $this->assertCount(6, $data); $data = $DB->get_records('assignsubmission_file', ['assignment' => $assign1->get_instance()->id]); $this->assertCount(3, $data); // Records in the second assignment (not being touched). $data = $DB->get_records('assignsubmission_file', ['assignment' => $assign2->get_instance()->id]); $this->assertCount(2, $data); $deletedata = new \mod_assign\privacy\assign_plugin_request_data($context1, $assign1); $deletedata->set_userids($userids); $deletedata->populate_submissions_and_grades(); \assignsubmission_file\privacy\provider::delete_submissions($deletedata); $data = $DB->get_records('files', ['contextid' => $context1->id, 'component' => 'assignsubmission_file']); $this->assertCount(2, $data); // Submission 1 and 3 have been removed. We should be left with submission2. $data = $DB->get_records('assignsubmission_file', ['assignment' => $assign1->get_instance()->id]); $this->assertCount(1, $data); // This should be untouched. $data = $DB->get_records('assignsubmission_file', ['assignment' => $assign2->get_instance()->id]); $this->assertCount(2, $data); } } submission/file/tests/locallib_test.php000064400000020622152265517350014366 0ustar00. namespace assignsubmission_file; use mod_assign_test_generator; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->dirroot . '/mod/assign/tests/generator.php'); /** * Unit tests for mod/assign/submission/file/locallib.php * * @package assignsubmission_file * @copyright 2016 Cameron Ball * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class locallib_test extends \advanced_testcase { // Use the generator helper. use mod_assign_test_generator; /** * Test submission_is_empty * * @dataProvider submission_is_empty_testcases * @param string $data The file submission data * @param bool $expected The expected return value */ public function test_submission_is_empty($data, $expected): void { $this->resetAfterTest(); $course = $this->getDataGenerator()->create_course(); $student = $this->getDataGenerator()->create_and_enrol($course, 'student'); $assign = $this->create_instance($course, [ 'assignsubmission_file_enabled' => 1, 'assignsubmission_file_maxfiles' => 12, 'assignsubmission_file_maxsizebytes' => 10, ]); $this->setUser($student->id); $itemid = file_get_unused_draft_itemid(); $submission = (object)['files_filemanager' => $itemid]; $plugin = $assign->get_submission_plugin_by_type('file'); if ($data) { $data += ['contextid' => \context_user::instance($student->id)->id, 'itemid' => $itemid]; $fs = get_file_storage(); $fs->create_file_from_string((object)$data, 'Content of ' . $data['filename']); } $result = $plugin->submission_is_empty($submission); $this->assertTrue($result === $expected); } /** * Test that an empty directory is is not detected as a valid submission by submission_is_empty. */ public function test_submission_is_empty_directory_only(): void { $this->resetAfterTest(); $course = $this->getDataGenerator()->create_course(); $student = $this->getDataGenerator()->create_and_enrol($course, 'student'); $assign = $this->create_instance($course, [ 'assignsubmission_file_enabled' => 1, 'assignsubmission_file_maxfiles' => 12, 'assignsubmission_file_maxsizebytes' => 10, ]); $this->setUser($student->id); $itemid = file_get_unused_draft_itemid(); $submission = (object)['files_filemanager' => $itemid]; $plugin = $assign->get_submission_plugin_by_type('file'); $fs = get_file_storage(); $fs->create_directory( \context_user::instance($student->id)->id, 'user', 'draft', $itemid, '/subdirectory/' ); $this->assertTrue($plugin->submission_is_empty($submission)); } /** * Test new_submission_empty * * @dataProvider submission_is_empty_testcases * @param string $data The file submission data * @param bool $expected The expected return value */ public function test_new_submission_empty($data, $expected): void { $this->resetAfterTest(); $course = $this->getDataGenerator()->create_course(); $student = $this->getDataGenerator()->create_and_enrol($course, 'student'); $assign = $this->create_instance($course, [ 'assignsubmission_file_enabled' => 1, 'assignsubmission_file_maxfiles' => 12, 'assignsubmission_file_maxsizebytes' => 10, ]); $this->setUser($student); $itemid = file_get_unused_draft_itemid(); $submission = (object) ['files_filemanager' => $itemid]; if ($data) { $data += ['contextid' => \context_user::instance($student->id)->id, 'itemid' => $itemid]; $fs = get_file_storage(); $fs->create_file_from_string((object)$data, 'Content of ' . $data['filename']); } $result = $assign->new_submission_empty($submission); $this->assertTrue($result === $expected); } /** * Test that an empty directory is is not detected as a valid submission by new_submission_is_empty. */ public function test_new_submission_empty_directory_only(): void { $this->resetAfterTest(); $course = $this->getDataGenerator()->create_course(); $student = $this->getDataGenerator()->create_and_enrol($course, 'student'); $assign = $this->create_instance($course, [ 'assignsubmission_file_enabled' => 1, 'assignsubmission_file_maxfiles' => 12, 'assignsubmission_file_maxsizebytes' => 10, ]); $this->setUser($student->id); $itemid = file_get_unused_draft_itemid(); $submission = (object)['files_filemanager' => $itemid]; $plugin = $assign->get_submission_plugin_by_type('file'); $fs = get_file_storage(); $fs->create_directory( \context_user::instance($student->id)->id, 'user', 'draft', $itemid, '/subdirectory/' ); $this->assertTrue($assign->new_submission_empty($submission)); } /** * Dataprovider for the test_submission_is_empty testcase * * @return array of testcases */ public static function submission_is_empty_testcases(): array { return [ 'With file' => [ [ 'component' => 'user', 'filearea' => 'draft', 'filepath' => '/', 'filename' => 'not_a_virus.exe' ], false ], 'With file in directory' => [ [ 'component' => 'user', 'filearea' => 'draft', 'filepath' => '/subdir/', 'filename' => 'not_a_virus.exe' ], false ], 'Without file' => [null, true] ]; } /** * Test getting files from plugin submission */ public function test_get_files(): void { $this->resetAfterTest(); $course = $this->getDataGenerator()->create_course(); $student = $this->getDataGenerator()->create_and_enrol($course, 'student'); $assign = $this->create_instance($course, [ 'assignsubmission_file_enabled' => 1, 'assignsubmission_file_maxfiles' => 2, 'assignsubmission_file_maxsizebytes' => 512, ]); // Switch to student, create some dummy files, and submit data to plugin. $this->setUser($student); $submission = $assign->get_user_submission($student->id, true); $filerecord = [ 'contextid' => $assign->get_context()->id, 'component' => 'assignsubmission_file', 'filearea' => ASSIGNSUBMISSION_FILE_FILEAREA, 'itemid' => $submission->id, 'filepath' => '/', ]; get_file_storage()->create_file_from_string($filerecord + ['filename' => 'File 1.txt'], 'File One'); get_file_storage()->create_file_from_string($filerecord + ['filename' => 'File 2.txt'], 'File Two'); /** @var \assign_submission_file $plugin */ $plugin = $assign->get_submission_plugin_by_type('file'); $plugin->save($submission, (object) []); // Ensure we retrieve back list of file submissions, deterministically ordered. $files = $plugin->get_files($submission, $student); $this->assertSame([ '/File 1.txt' => 'File 1.txt', '/File 2.txt' => 'File 2.txt', ], array_map(fn(\stored_file $f) => $f->get_filename(), $files)); } } submission/file/tests/event/events_test.php000064400000021445152265517350015236 0ustar00. /** * Contains the event tests for the plugin. * * @package assignsubmission_file * @copyright 2013 Frédéric Massart * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace assignsubmission_file\event; use mod_assign_test_generator; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->dirroot . '/mod/assign/tests/generator.php'); final class events_test extends \advanced_testcase { // Use the generator helper. use mod_assign_test_generator; /** * Test that the assessable_uploaded event is fired when a file submission has been made. */ public function test_assessable_uploaded(): void { $this->resetAfterTest(); $course = $this->getDataGenerator()->create_course(); $student = $this->getDataGenerator()->create_and_enrol($course, 'student'); $assign = $this->create_instance($course); $context = $assign->get_context(); $cm = $assign->get_course_module(); $this->setUser($student->id); $submission = $assign->get_user_submission($student->id, true); $fs = get_file_storage(); $dummy = (object) array( 'contextid' => $context->id, 'component' => 'assignsubmission_file', 'filearea' => ASSIGNSUBMISSION_FILE_FILEAREA, 'itemid' => $submission->id, 'filepath' => '/', 'filename' => 'myassignmnent.pdf' ); $fi = $fs->create_file_from_string($dummy, 'Content of ' . $dummy->filename); $dummy = (object) array( 'contextid' => $context->id, 'component' => 'assignsubmission_file', 'filearea' => ASSIGNSUBMISSION_FILE_FILEAREA, 'itemid' => $submission->id, 'filepath' => '/', 'filename' => 'myassignmnent.png' ); $fi2 = $fs->create_file_from_string($dummy, 'Content of ' . $dummy->filename); $files = $fs->get_area_files($context->id, 'assignsubmission_file', ASSIGNSUBMISSION_FILE_FILEAREA, $submission->id, 'id', false); $data = new \stdClass(); $plugin = $assign->get_submission_plugin_by_type('file'); $sink = $this->redirectEvents(); $plugin->save($submission, $data); $events = $sink->get_events(); $this->assertCount(2, $events); $event = reset($events); $this->assertInstanceOf('\assignsubmission_file\event\assessable_uploaded', $event); $this->assertEquals($context->id, $event->contextid); $this->assertEquals($submission->id, $event->objectid); $this->assertCount(2, $event->other['pathnamehashes']); $this->assertEquals($fi->get_pathnamehash(), $event->other['pathnamehashes'][0]); $this->assertEquals($fi2->get_pathnamehash(), $event->other['pathnamehashes'][1]); $expected = new \stdClass(); $expected->modulename = 'assign'; $expected->cmid = $cm->id; $expected->itemid = $submission->id; $expected->courseid = $course->id; $expected->userid = $student->id; $expected->file = $files; $expected->files = $files; $expected->pathnamehashes = array($fi->get_pathnamehash(), $fi2->get_pathnamehash()); $this->assertEventContextNotUsed($event); } /** * Test that the submission_created event is fired when a file submission is saved. */ public function test_submission_created(): void { $this->resetAfterTest(); $course = $this->getDataGenerator()->create_course(); $student = $this->getDataGenerator()->create_and_enrol($course, 'student'); $assign = $this->create_instance($course); $context = $assign->get_context(); $this->setUser($student->id); $submission = $assign->get_user_submission($student->id, true); $fs = get_file_storage(); $dummy = (object) array( 'contextid' => $context->id, 'component' => 'assignsubmission_file', 'filearea' => ASSIGNSUBMISSION_FILE_FILEAREA, 'itemid' => $submission->id, 'filepath' => '/', 'filename' => 'myassignmnent.pdf' ); $fi = $fs->create_file_from_string($dummy, 'Content of ' . $dummy->filename); $dummy = (object) array( 'contextid' => $context->id, 'component' => 'assignsubmission_file', 'filearea' => ASSIGNSUBMISSION_FILE_FILEAREA, 'itemid' => $submission->id, 'filepath' => '/', 'filename' => 'myassignmnent.png' ); $fi2 = $fs->create_file_from_string($dummy, 'Content of ' . $dummy->filename); $files = $fs->get_area_files($context->id, 'assignsubmission_file', ASSIGNSUBMISSION_FILE_FILEAREA, $submission->id, 'id', false); $data = new \stdClass(); $plugin = $assign->get_submission_plugin_by_type('file'); $sink = $this->redirectEvents(); $plugin->save($submission, $data); $events = $sink->get_events(); $this->assertCount(2, $events); // We want to test the last event fired. $event = $events[1]; $this->assertInstanceOf('\assignsubmission_file\event\submission_created', $event); $this->assertEquals($context->id, $event->contextid); $this->assertEquals($course->id, $event->courseid); $this->assertEquals($submission->id, $event->other['submissionid']); $this->assertEquals($submission->attemptnumber, $event->other['submissionattempt']); $this->assertEquals($submission->status, $event->other['submissionstatus']); $this->assertEquals($submission->userid, $event->relateduserid); } /** * Test that the submission_updated event is fired when a file submission is saved when an existing submission already exists. */ public function test_submission_updated(): void { $this->resetAfterTest(); $course = $this->getDataGenerator()->create_course(); $student = $this->getDataGenerator()->create_and_enrol($course, 'student'); $assign = $this->create_instance($course); $context = $assign->get_context(); $this->setUser($student->id); $submission = $assign->get_user_submission($student->id, true); $fs = get_file_storage(); $dummy = (object) array( 'contextid' => $context->id, 'component' => 'assignsubmission_file', 'filearea' => ASSIGNSUBMISSION_FILE_FILEAREA, 'itemid' => $submission->id, 'filepath' => '/', 'filename' => 'myassignmnent.pdf' ); $fi = $fs->create_file_from_string($dummy, 'Content of ' . $dummy->filename); $dummy = (object) array( 'contextid' => $context->id, 'component' => 'assignsubmission_file', 'filearea' => ASSIGNSUBMISSION_FILE_FILEAREA, 'itemid' => $submission->id, 'filepath' => '/', 'filename' => 'myassignmnent.png' ); $fi2 = $fs->create_file_from_string($dummy, 'Content of ' . $dummy->filename); $files = $fs->get_area_files($context->id, 'assignsubmission_file', ASSIGNSUBMISSION_FILE_FILEAREA, $submission->id, 'id', false); $data = new \stdClass(); $plugin = $assign->get_submission_plugin_by_type('file'); $sink = $this->redirectEvents(); // Create a submission. $plugin->save($submission, $data); // Update a submission. $plugin->save($submission, $data); $events = $sink->get_events(); $this->assertCount(4, $events); // We want to test the last event fired. $event = $events[3]; $this->assertInstanceOf('\assignsubmission_file\event\submission_updated', $event); $this->assertEquals($context->id, $event->contextid); $this->assertEquals($course->id, $event->courseid); $this->assertEquals($submission->id, $event->other['submissionid']); $this->assertEquals($submission->attemptnumber, $event->other['submissionattempt']); $this->assertEquals($submission->status, $event->other['submissionstatus']); $this->assertEquals($submission->userid, $event->relateduserid); } } submission/file/tests/behat/file_type_restriction.feature000064400000011022152265517350020074 0ustar00@mod @mod_assign @assignsubmission_file Feature: In an assignment, limit submittable file types In order to constrain student submissions for marking As a teacher I need to limit the submittable file types Background: Given the following "courses" exist: | fullname | shortname | category | groupmode | | Course 1 | C1 | 0 | 1 | And the following "users" exist: | username | firstname | lastname | email | | teacher1 | Teacher | 1 | teacher1@example.com | | student1 | Student | 1 | student1@example.com | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | | student1 | C1 | student | And the following config values are set as admin: | filetypes | image/png;spreadsheet | assignsubmission_file | @javascript Scenario: File types validation for an assignment Given the following "activities" exist: | activity | course | name | duedate | assignsubmission_onlinetext_enabled | assignsubmission_file_enabled | assignsubmission_file_maxfiles | assignsubmission_file_maxsizebytes | | assign | C1 | Test assignment name | 1388534400 | 0 | 1 | 1 | 0 | And I am on the "Test assignment name" Activity page logged in as teacher1 And I navigate to "Settings" in current page administration When I set the field "Accepted file types" to "image/png;doesntexist;.anything;unreal/mimetype;nodot" And I press "Save and display" And I should see "Unknown file types: .doesntexist, .anything, unreal/mimetype, .nodot" And I set the field "Accepted file types" to "image/png;spreadsheet" And I press "Save and display" And I navigate to "Settings" in current page administration And the field "Accepted file types" matches value "image/png,spreadsheet" And I set the field "Accepted file types" to "" And I press "Choose" And I set the field "Image files" to "1" And I press "Save changes" And I press "Save and display" And I navigate to "Settings" in current page administration Then the field "Accepted file types" matches value "image" @javascript @_file_upload Scenario: Uploading permitted file types for an assignment Given the following "activities" exist: | activity | course | name | duedate | assignsubmission_onlinetext_enabled | assignsubmission_file_enabled | assignsubmission_file_maxfiles | assignsubmission_file_maxsizebytes | assignsubmission_file_filetypes | | assign | C1 | Test assignment name | 1388534400 | 0 | 1 | 3 | 0 | image/png,spreadsheet,.xml,.txt | And I am on the "Test assignment name" Activity page logged in as student1 When I press "Add submission" And I should see "Accepted file types" And I should see "Image (PNG)" And I should see "Spreadsheet files" And I should see "Text file" And I upload "lib/tests/fixtures/gd-logo.png" file to "File submissions" filemanager And I upload "lib/tests/fixtures/tabfile.csv" file to "File submissions" filemanager And I upload "lib/tests/fixtures/empty.txt" file to "File submissions" filemanager And I press "Save changes" Then "gd-logo.png" "link" should exist And "tabfile.csv" "link" should exist And "empty.txt" "link" should exist @javascript @_file_upload Scenario: No filetypes allows all Given the following "activities" exist: | activity | course | name | duedate | assignsubmission_onlinetext_enabled | assignsubmission_file_enabled | assignsubmission_file_maxfiles | assignsubmission_file_maxsizebytes | assignsubmission_file_filetypes | | assign | C1 | Test assignment name | 1388534400 | 0 | 1 | 2 | 0 | | And I am on the "Test assignment name" Activity page logged in as student1 When I press "Add submission" And I should not see "Accepted file types" And I upload "lib/tests/fixtures/gd-logo.png" file to "File submissions" filemanager And I upload "lib/tests/fixtures/tabfile.csv" file to "File submissions" filemanager And I press "Save changes" Then "gd-logo.png" "link" should exist And "tabfile.csv" "link" should exist submission/file/settings.php000064400000004452152265517350012247 0ustar00. /** * This file defines the admin settings for this plugin * * @package assignsubmission_file * @copyright 2012 NetSpot {@link http://www.netspot.com.au} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ // Note: This is on by default. $settings->add(new admin_setting_configcheckbox('assignsubmission_file/default', new lang_string('default', 'assignsubmission_file'), new lang_string('default_help', 'assignsubmission_file'), 1)); $settings->add(new admin_setting_configtext('assignsubmission_file/maxfiles', new lang_string('maxfiles', 'assignsubmission_file'), new lang_string('maxfiles_help', 'assignsubmission_file'), 20, PARAM_INT)); $settings->add(new admin_setting_filetypes('assignsubmission_file/filetypes', new lang_string('defaultacceptedfiletypes', 'assignsubmission_file'), new lang_string('acceptedfiletypes_help', 'assignsubmission_file'), '')); if (isset($CFG->maxbytes)) { $name = new lang_string('maximumsubmissionsize', 'assignsubmission_file'); $description = new lang_string('configmaxbytes', 'assignsubmission_file'); $maxbytes = get_config('assignsubmission_file', 'maxbytes'); $element = new admin_setting_configselect('assignsubmission_file/maxbytes', $name, $description, $CFG->maxbytes, get_max_upload_sizes($CFG->maxbytes, 0, 0, $maxbytes)); $settings->add($element); } extensionform.php000064400000020010152265517350010161 0ustar00. /** * This file contains the forms to create and edit an instance of this 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('Direct access to this script is forbidden.'); require_once($CFG->libdir.'/formslib.php'); require_once($CFG->dirroot . '/mod/assign/locallib.php'); /** * Assignment extension dates form * * @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 mod_assign_extension_form extends moodleform { /** @var array $instance - The data passed to this form */ private $instance; /** * Define the form - called by parent constructor */ public function definition() { global $DB; $mform = $this->_form; $params = $this->_customdata; // Instance variable is used by the form validation function. $instance = $params['instance']; $this->instance = $instance; // Get the assignment class. /** @var assign $assign */ $assign = $params['assign']; $userlist = $params['userlist']; // Load current extensions for all selected users. [$usercondition, $params] = $DB->get_in_or_equal($userlist); array_unshift($params, $assign->get_instance()->id); $userinfo = $DB->get_records_sql(" SELECT u.*, auf.extensionduedate FROM {user} u LEFT JOIN {assign_user_flags} auf ON auf.userid = u.id AND auf.assignment = ? WHERE u.id $usercondition ", $params); // Prepare display of up to 5 users. // TODO Does not support custom user profile fields (MDL-70456). $extrauserfields = \core_user\fields::get_identity_fields($assign->get_context(), false); $displayedusercount = 0; $usershtml = ''; foreach ($userlist as $userid) { $displayedusercount += 1; if ($displayedusercount > 5) { $usershtml .= get_string('moreusers', 'assign', count($userlist) - 5); break; } $user = $userinfo[$userid]; $usershtml .= $assign->get_renderer()->render( new assign_user_summary($user, $assign->get_course()->id, has_capability('moodle/site:viewfullnames', $assign->get_course_context()), $assign->is_blind_marking(), $assign->get_uniqueid_for_user($user->id), $extrauserfields, !$assign->is_active_user($userid) ), ); } // Find the range of extensions that currently exist for these users. $earliestextension = null; $lateststextension = 0; $userswithoutanextension = 0; foreach ($userlist as $userid) { $user = $userinfo[$userid]; if ($user->extensionduedate) { $lateststextension = max($lateststextension, $user->extensionduedate); if ($earliestextension !== null) { $earliestextension = min($earliestextension, $user->extensionduedate); } else { $earliestextension = $user->extensionduedate; } } else { $userswithoutanextension += 1; } } $listusersmessage = get_string('grantextensionforusers', 'assign', count($userlist)); $mform->addElement('header', 'general', $listusersmessage); $mform->addElement('static', 'userslist', get_string('selectedusers', 'assign'), $usershtml); if ($instance->allowsubmissionsfromdate) { $mform->addElement('static', 'allowsubmissionsfromdate', get_string('allowsubmissionsfromdate', 'assign'), userdate($instance->allowsubmissionsfromdate)); } $finaldate = 0; if ($instance->duedate) { $mform->addElement('static', 'duedate', get_string('duedate', 'assign'), userdate($instance->duedate)); $finaldate = $instance->duedate; } if ($instance->cutoffdate) { $mform->addElement('static', 'cutoffdate', get_string('cutoffdate', 'assign'), userdate($instance->cutoffdate)); $finaldate = $instance->cutoffdate; } if ($userswithoutanextension == count($userlist)) { // All users don't have an extension yet. $currentdatesdisplay = get_string('extensionduedatenone', 'assign'); } else { if ($earliestextension == $lateststextension) { $currentdatesdisplay = userdate($lateststextension); } else { $currentdatesdisplay = get_string('extensionduedaterange', 'assign', (object) [ 'earliest' => userdate($earliestextension), 'latest' => userdate($lateststextension), ]); } if ($userswithoutanextension) { $currentdatesdisplay .= '
' . get_string('extensionduedatewithout', 'assign', $userswithoutanextension); } } $mform->addElement('static', 'currentextension', get_string('extensionduedatecurrent', 'assign'), $currentdatesdisplay); if ($lateststextension) { // If there are existing extensions, edit based on the current (latest) value. $defaultdate = $lateststextension; } else { // Otherwise take the later of the deadline and one minute before midnight tonight (server time). $endoftoday = new DateTimeImmutable('today 23:59', core_date::get_server_timezone_object()); $defaultdate = max($finaldate, $endoftoday->getTimestamp()); } $mform->addElement('date_time_selector', 'extensionduedate', get_string('extensionduedate', 'assign'), array('optional'=>true)); $mform->setDefault('extensionduedate', $defaultdate); $mform->addElement('hidden', 'id'); $mform->setType('id', PARAM_INT); $mform->addElement('hidden', 'userid'); $mform->setType('userid', PARAM_INT); $mform->addElement('hidden', 'selectedusers'); $mform->setType('selectedusers', PARAM_SEQUENCE); $mform->addElement('hidden', 'action', 'saveextension'); $mform->setType('action', PARAM_ALPHA); $this->add_action_buttons(true, get_string('savechanges', 'assign')); } /** * Perform validation on the extension form * @param array $data * @param array $files */ public function validation($data, $files) { $errors = parent::validation($data, $files); if ($this->instance->duedate && $data['extensionduedate']) { if ($this->instance->duedate > $data['extensionduedate']) { $errors['extensionduedate'] = get_string('extensionnotafterduedate', 'assign'); } } if ($this->instance->allowsubmissionsfromdate && $data['extensionduedate']) { if ($this->instance->allowsubmissionsfromdate > $data['extensionduedate']) { $errors['extensionduedate'] = get_string('extensionnotafterfromdate', 'assign'); } } return $errors; } } adminmanageplugins.php000064400000002641152265517350011136 0ustar00. /** * Allows the admin to manage assignment plugins * * @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 */ require_once(__DIR__ . '/../../config.php'); require_once($CFG->dirroot.'/mod/assign/adminlib.php'); $subtype = required_param('subtype', PARAM_PLUGIN); $action = optional_param('action', null, PARAM_PLUGIN); $plugin = optional_param('plugin', null, PARAM_PLUGIN); if (!empty($plugin)) { require_sesskey(); } // Create the class for this controller. $pluginmanager = new assign_plugin_manager($subtype); $PAGE->set_context(context_system::instance()); // Execute the controller. $pluginmanager->execute($action, $plugin); classes/grades/gradeitems.php000064400000003440152265517350012317 0ustar00. /** * Grade item mappings for the activity. * * @package mod_assign * @copyright Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ declare(strict_types = 1); namespace mod_assign\grades; use \core_grades\local\gradeitem\itemnumber_mapping; use \core_grades\local\gradeitem\advancedgrading_mapping; /** * Grade item mappings for the activity. * * @package mod_assign * @copyright Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class gradeitems implements itemnumber_mapping, advancedgrading_mapping { /** * Return the list of grade item mappings for the assign. * * @return array */ public static function get_itemname_mapping_for_component(): array { return [ 0 => 'submissions', ]; } /** * Get the list of advanced grading item names for this component. * * @return array */ public static function get_advancedgrading_itemnames(): array { return [ 'submissions', ]; } } classes/external/external_api.php000064400000011606152265517350013226 0ustar00. namespace mod_assign\external; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once("$CFG->dirroot/mod/assign/locallib.php"); /** * Extend the base external_api class with mod_assign utility methods. * * @package mod_assign * @author Andrew Madden * @copyright 2021 Catalyst IT * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class external_api extends \core_external\external_api { /** * Generate a warning in a standard structure for a known failure. * * @param int $assignmentid - The assignment * @param string $warningcode - The key for the warning message * @param string $detail - A description of the error * @return array - Warning structure containing item, itemid, warningcode, message */ protected static function generate_warning(int $assignmentid, string $warningcode, string $detail): array { $warningmessages = [ 'couldnotlock' => 'Could not lock the submission for this user.', 'couldnotunlock' => 'Could not unlock the submission for this user.', 'couldnotsubmitforgrading' => 'Could not submit assignment for grading.', 'couldnotrevealidentities' => 'Could not reveal identities.', 'couldnotgrantextensions' => 'Could not grant submission date extensions.', 'couldnotrevert' => 'Could not revert submission to draft.', 'invalidparameters' => 'Invalid parameters.', 'couldnotsavesubmission' => 'Could not save submission.', 'couldnotsavegrade' => 'Could not save grade.', 'couldnotstartsubmission' => 'Could not start submission with time limit.', 'submissionnotopen' => 'This assignment is not open for submissions', 'timelimitnotenabled' => 'Time limit is not enabled for assignment.', 'opensubmissionexists' => 'Open assignment submission already exists.', 'couldnotremovesubmission' => 'Could not remove the submission for this user', 'submissionnotfoundtoremove' => 'There is no submission to remove', ]; $message = $warningmessages[$warningcode]; if (empty($message)) { $message = 'Unknown warning type.'; } return [ 'item' => s($detail), 'itemid' => $assignmentid, 'warningcode' => $warningcode, 'message' => $message, ]; } /** * Utility function for validating an assign. * * @param int $assignid assign instance id * @return array array containing the assign, course, context and course module objects * @since Moodle 3.2 */ protected static function validate_assign(int $assignid): array { global $DB; // Request and permission validation. $assign = $DB->get_record('assign', ['id' => $assignid], 'id', MUST_EXIST); list($course, $cm) = get_course_and_cm_from_instance($assign, 'assign'); $context = \context_module::instance($cm->id); // Please, note that is not required to check mod/assign:view because is done by validate_context->require_login. self::validate_context($context); $assign = new \assign($context, $cm, $course); return [$assign, $course, $cm, $context]; } /** * Get a submission from an assignment for a user. Encapsulates checking whether it's a solo or team submission. * * @param \assign $assignment Assignment object. * @param int|null $userid User id. * @param int $groupid Group id. * @param bool $create Whether a new submission should be created. * @param int $attemptnumber Attempt number. Use -1 for last attempt. * @return bool|\stdClass */ protected static function get_user_or_group_submission(\assign $assignment, ?int $userid = null, int $groupid = 0, bool $create = false, int $attemptnumber = -1) { if ($assignment->get_instance($userid)->teamsubmission) { $submission = $assignment->get_group_submission($userid, $groupid, $create, $attemptnumber); } else { $submission = $assignment->get_user_submission($userid, $create, $attemptnumber); } return $submission; } } classes/external/start_submission.php000064400000010263152265517350014161 0ustar00. namespace mod_assign\external; use core_external\external_function_parameters; use core_external\external_single_structure; use core_external\external_value; use core_external\external_warnings; /** * External function to notify Moodle that an assignment submission is starting. * * @package mod_assign * @author Andrew Madden * @copyright 2021 Catalyst IT * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class start_submission extends external_api { /** * Describes the parameters for submission_start. * * @return external_function_parameters * @since Moodle 4.0 */ public static function execute_parameters(): external_function_parameters { return new external_function_parameters ([ 'assignid' => new external_value(PARAM_INT, 'Assignment instance id'), ] ); } /** * Call to start an assignment submission. * * @param int $assignid Assignment ID. * @return array * @since Moodle 4.0 */ public static function execute(int $assignid): array { global $DB, $USER; $result = $warnings = []; $submission = null; [ 'assignid' => $assignid, ] = self::validate_parameters(self::execute_parameters(), [ 'assignid' => $assignid, ]); list($assignment, $course, $cm, $context) = self::validate_assign($assignid); $assignment->update_effective_access($USER->id); $latestsubmission = external_api::get_user_or_group_submission($assignment, $USER->id); if (!$assignment->submissions_open($USER->id)) { $warnings[] = self::generate_warning($assignid, 'submissionnotopen', get_string('submissionnotopen', 'assign')); } if (!$assignment->is_time_limit_enabled()) { $warnings[] = self::generate_warning($assignid, 'timelimitnotenabled', get_string('timelimitnotenabled', 'assign')); } else if ($assignment->is_attempt_in_progress()) { $warnings[] = self::generate_warning($assignid, 'opensubmissionexists', get_string('opensubmissionexists', 'assign')); } if (empty($warnings)) { // If there is an open submission with no start time, use latest submission, otherwise create a new submission. if (!empty($latestsubmission) && $latestsubmission->status !== ASSIGN_SUBMISSION_STATUS_SUBMITTED && empty($latestsubmission->timestarted)) { $submission = $latestsubmission; } else { $submission = external_api::get_user_or_group_submission($assignment, $USER->id, 0, true); } // Set the start time of the submission. $submission->timestarted = time(); $DB->update_record('assign_submission', $submission); } $result['submissionid'] = $submission ? $submission->id : 0; $result['warnings'] = $warnings; return $result; } /** * Describes the submission_start return value. * * @return external_single_structure * @since Moodle 4.0 */ public static function execute_returns(): external_single_structure { return new external_single_structure([ 'submissionid' => new external_value(PARAM_INT, 'New submission ID.'), 'warnings' => new external_warnings(), ]); } } classes/external/remove_submission.php000064400000007261152265517350014325 0ustar00. namespace mod_assign\external; use core_external\external_function_parameters; use core_external\external_single_structure; use core_external\external_value; use core_external\external_warnings; /** * External function to remove an assignment submission. * * @package mod_assign * @category external * * @copyright 2024 Daniel Ureña * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @since Moodle 4.5 */ class remove_submission extends external_api { /** * Describes the parameters for remove submission. * * @return external_function_parameters */ public static function execute_parameters(): external_function_parameters { return new external_function_parameters([ 'userid' => new external_value(PARAM_INT, 'User id'), 'assignid' => new external_value(PARAM_INT, 'Assignment instance id'), ]); } /** * Call to remove submission. * * @param int $userid User id to remove submission * @param int $assignid The id of the assignment * @return array */ public static function execute(int $userid, int $assignid): array { // Initialize return variables. $warnings = []; $result = []; $status = false; [ 'userid' => $userid, 'assignid' => $assignid ] = self::validate_parameters(self::execute_parameters(), [ 'userid' => $userid, 'assignid' => $assignid, ]); // Validate and get the assign. [$assign, $course, $cm, $context] = self::validate_assign($assignid); // Get submission. $submission = $assign->get_user_submission($userid, false); if ( !$submission || $submission->status == ASSIGN_SUBMISSION_STATUS_NEW || $submission->status == ASSIGN_SUBMISSION_STATUS_REOPENED ) { // No submission to remove. $warnings[] = self::generate_warning($assignid, 'submissionnotfoundtoremove', 'assign'); return [ 'status' => $status, 'warnings' => $warnings, ]; } if (!$status = $assign->remove_submission($userid)) { $errors = $assign->get_error_messages(); foreach ($errors as $errormsg) { $warnings[] = self::generate_warning($assignid, 'couldnotremovesubmission', $errormsg); } } $result['status'] = $status; $result['warnings'] = $warnings; return $result; } /** * Describes the remove submissions return value. * * @return external_single_structure */ public static function execute_returns(): external_single_structure { return new external_single_structure([ 'status' => new external_value(PARAM_BOOL, 'True if the submission was successfully removed and false if was not.'), 'warnings' => new external_warnings(), ]); } } classes/privacy/assignfeedback_provider.php000064400000006715152265517350015256 0ustar00. /** * This file contains the assignfeedback_provider interface. * * Assignment Sub plugins should implement this if they store personal information. * * @package mod_assign * @copyright 2018 Adrian Greeve * * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_assign\privacy; use core_privacy\local\request\contextlist; defined('MOODLE_INTERNAL') || die(); interface assignfeedback_provider extends \core_privacy\local\request\plugin\subplugin_provider { /** * Retrieves the contextids associated with the provided userid for this subplugin. * NOTE if your subplugin must have an entry in the assign_grade table to work, then this * method can be empty. * * @param int $userid The user ID to get context IDs for. * @param \core_privacy\local\request\contextlist $contextlist Use add_from_sql with this object to add your context IDs. */ public static function get_context_for_userid_within_feedback(int $userid, contextlist $contextlist); /** * Returns student user ids related to the provided teacher ID. If an entry must be present in the assign_grade table for * your plugin to work then there is no need to fill in this method. If you filled in get_context_for_userid_within_feedback() * then you probably have to fill this in as well. * * @param useridlist $useridlist A list of user IDs of students graded by this user. */ public static function get_student_user_ids(useridlist $useridlist); /** * Export feedback data with the available grade and userid information provided. * assign_plugin_request_data contains: * - context * - grade object * - current path (subcontext) * - user object * * @param assign_plugin_request_data $exportdata Contains data to help export the user information. */ public static function export_feedback_user_data(assign_plugin_request_data $exportdata); /** * Any call to this method should delete all user data for the context defined in the deletion_criteria. * assign_plugin_request_data contains: * - context * - assign object * * @param assign_plugin_request_data $requestdata Data useful for deleting user data from this sub-plugin. */ public static function delete_feedback_for_context(assign_plugin_request_data $requestdata); /** * Calling this function should delete all user data associated with this grade. * assign_plugin_request_data contains: * - context * - grade object * - user object * - assign object * * @param assign_plugin_request_data $requestdata Data useful for deleting user data. */ public static function delete_feedback_for_grade(assign_plugin_request_data $requestdata); }classes/privacy/submission_legacy_polyfill.php000064400000010147152265517350016036 0ustar00. /** * This file contains the polyfill to allow a plugin to operate with Moodle 3.3 up. * * @package mod_assign * @copyright 2018 Adrian Greeve * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_assign\privacy; use core_privacy\local\request\contextlist; defined('MOODLE_INTERNAL') || die(); /** * The trait used to provide backwards compatability for third-party plugins. * * @copyright 2018 Adrian Greeve * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ trait submission_legacy_polyfill { /** * Retrieves the contextids associated with the provided userid for this subplugin. * NOTE if your subplugin must have an entry in the assign_submission table to work, then this * method can be empty. * * @param int $userid The user ID to get context IDs for. * @param \core_privacy\local\request\contextlist $contextlist Use add_from_sql with this object to add your context IDs. */ public static function get_context_for_userid_within_submission(int $userid, contextlist $contextlist) { return static::_get_context_for_userid_within_submission($userid, $contextlist); } /** * Returns student user ids related to the provided teacher ID. If it is possible that a student ID will not be returned by * the sql query in \mod_assign\privacy\provider::find_grader_info() Then you need to provide some sql to retrive those * student IDs. This is highly likely if you had to fill in get_context_for_userid_within_submission above. * * @param useridlist $useridlist A user ID list object that you can append your user IDs to. */ public static function get_student_user_ids(useridlist $useridlist) { return static::_get_student_user_ids($useridlist); } /** * This method is used to export any user data this sub-plugin has using the assign_plugin_request_data object to get the * context and userid. * assign_plugin_request_data contains: * - context * - submission object * - current path (subcontext) * - user object * * @param assign_plugin_request_data $exportdata Information to use to export user data for this sub-plugin. */ public static function export_submission_user_data(assign_plugin_request_data $exportdata) { return static::_export_submission_user_data($exportdata); } /** * Any call to this method should delete all user data for the context defined in the deletion_criteria. * assign_plugin_request_data contains: * - context * - assign object * * @param assign_plugin_request_data $requestdata Information to use to delete user data for this submission. */ public static function delete_submission_for_context(assign_plugin_request_data $requestdata) { return static::_delete_submission_for_context($requestdata); } /** * A call to this method should delete user data (where practicle) from the userid and context. * assign_plugin_request_data contains: * - context * - submission object * - user object * - assign object * * @param assign_plugin_request_data $exportdata Details about the user and context to focus the deletion. */ public static function delete_submission_for_userid(assign_plugin_request_data $exportdata) { return static::_delete_submission_for_userid($exportdata); } } classes/privacy/useridlist.php000064400000005161152265517350012574 0ustar00. /** * This file contains the mod_assign useridlist * * This is for collecting a list of user IDs * * @package mod_assign * @copyright 2018 Adrian Greeve * * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_assign\privacy; defined('MOODLE_INTERNAL') || die(); /** * An object for collecting user IDs related to a teacher. * * @copyright 2018 Adrian Greeve * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class useridlist { /** @var int The ID of the teacher. */ protected $teacherid; /** @var int The ID of the assignment object. */ protected $assignid; /** @var array A collection of user IDs (students). */ protected $userids = []; /** * Create this object. * * @param int $teacherid The teacher ID. * @param int $assignid The assignment ID. */ public function __construct($teacherid, $assignid) { $this->teacherid = $teacherid; $this->assignid = $assignid; } /** * Returns the teacher ID. * * @return int The teacher ID. */ public function get_teacherid() { return $this->teacherid; } /** * Returns the assign ID. * * @return int The assign ID. */ public function get_assignid() { return $this->assignid; } /** * Returns the user IDs. * * @return array User IDs. */ public function get_userids() { return $this->userids; } /** * Add sql and params to return user IDs. * * @param string $sql The sql string to return user IDs. * @param array $params Parameters for the sql statement. */ public function add_from_sql($sql, $params) { global $DB; $userids = $DB->get_records_sql($sql, $params); if (!empty($userids)) { $this->userids = array_merge($this->userids, $userids); } } } classes/privacy/feedback_legacy_polyfill.php000064400000007630152265517350015372 0ustar00. /** * This file contains the polyfill to allow a plugin to operate with Moodle 3.3 up. * * @package mod_assign * @copyright 2018 Adrian Greeve * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_assign\privacy; use core_privacy\local\request\contextlist; defined('MOODLE_INTERNAL') || die(); /** * The trait used to provide backwards compatability for third-party plugins. * * @copyright 2018 Adrian Greeve * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ trait feedback_legacy_polyfill { /** * Retrieves the contextids associated with the provided userid for this subplugin. * NOTE if your subplugin must have an entry in the assign_grade table to work, then this * method can be empty. * * @param int $userid The user ID to get context IDs for. * @param \core_privacy\local\request\contextlist $contextlist Use add_from_sql with this object to add your context IDs. */ public static function get_context_for_userid_within_feedback(int $userid, contextlist $contextlist) { return static::_get_context_for_userid_within_feedback($userid, $contextlist); } /** * Returns student user ids related to the provided teacher ID. If an entry must be present in the assign_grade table for * your plugin to work then there is no need to fill in this method. If you filled in get_context_for_userid_within_feedback() * then you probably have to fill this in as well. * * @param useridlist $useridlist A list of user IDs of students graded by this user. */ public static function get_student_user_ids(useridlist $useridlist) { return static::_get_student_user_ids($useridlist); } /** * Export feedback data with the available grade and userid information provided. * assign_plugin_request_data contains: * - context * - grade object * - current path (subcontext) * - user object * * @param assign_plugin_request_data $exportdata Contains data to help export the user information. */ public static function export_feedback_user_data(assign_plugin_request_data $exportdata) { return static::_export_feedback_user_data($exportdata); } /** * Any call to this method should delete all user data for the context defined in the deletion_criteria. * assign_plugin_request_data contains: * - context * - assign object * * @param assign_plugin_request_data $requestdata Data useful for deleting user data from this sub-plugin. */ public static function delete_feedback_for_context(assign_plugin_request_data $requestdata) { return static::_delete_feedback_for_context($requestdata); } /** * Calling this function should delete all user data associated with this grade. * assign_plugin_request_data contains: * - context * - grade object * - user object * - assign object * * @param assign_plugin_request_data $requestdata Data useful for deleting user data. */ public static function delete_feedback_for_grade(assign_plugin_request_data $requestdata) { return static::_delete_feedback_for_grade($requestdata); } } classes/privacy/assign_plugin_request_data.php000064400000013514152265517350016011 0ustar00. /** * This file contains the mod_assign assign_plugin_request_data class * * For assign plugin privacy data to fulfill requests. * * @package mod_assign * @copyright 2018 Adrian Greeve * * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_assign\privacy; defined('MOODLE_INTERNAL') || die(); /** * An object for fulfilling an assign plugin data request. * * @copyright 2018 Adrian Greeve * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class assign_plugin_request_data { /** @var \context The context that we are dealing with. */ protected $context; /** @var object For submisisons the submission object, for feedback the grade object. */ protected $pluginobject; /** @var array The path or location that we are exporting data to. */ protected $subcontext; /** @var object If set then only export data related directly to this user. */ protected $user; /** @var array The user IDs of the users that will be affected. */ protected $userids; /** @var array The submissions related to the users added. */ protected $submissions = []; /** @var array The grades related to the users added. */ protected $grades = []; /** @var \assign The assign object */ protected $assign; /** * Object creator for assign plugin request data. * * @param \context $context Context object. * @param \stdClass $pluginobject The grade object. * @param array $subcontext Directory / file location. * @param \stdClass $user The user object. * @param \assign $assign The assign object. */ public function __construct(\context $context, \assign $assign, ?\stdClass $pluginobject = null, array $subcontext = [], ?\stdClass $user = null) { $this->context = $context; $this->pluginobject = $pluginobject; $this->subcontext = $subcontext; $this->user = $user; $this->assign = $assign; } /** * Method for adding an array of user IDs. This will do a query to populate the submissions and grades * for these users. * * @param array $userids User IDs to do something with. */ public function set_userids(array $userids) { $this->userids = $userids; } /** * Getter for this attribute. * * @return \context Context */ public function get_context() { return $this->context; } /** * Getter for this attribute. * * @return object The assign plugin object */ public function get_pluginobject() { return $this->pluginobject; } /** * Getter for this attribute. * * @return array The location (path) that this data is being writter to. */ public function get_subcontext() { return $this->subcontext; } /** * Getter for this attribute. * * @return object The user id. If set then only information directly related to this user ID will be returned. */ public function get_user() { return $this->user; } /** * Getter for this attribute. * * @return \assign The assign object. */ public function get_assign() { return $this->assign; } /** * A method to conveniently fetch the assign id. * * @return int The assign id. */ public function get_assignid() { return $this->assign->get_instance()->id; } /** * Get all of the user IDs * * @return array User IDs */ public function get_userids() { return $this->userids; } /** * Returns all of the submission IDs * * @return array submission IDs */ public function get_submissionids() { return array_keys($this->submissions); } /** * Returns the submissions related to the user IDs * * @return array User submissions. */ public function get_submissions() { return $this->submissions; } /** * Returns the grade IDs related to the user IDs * * @return array User grade IDs. */ public function get_gradeids() { return array_keys($this->grades); } /** * Returns the grades related to the user IDs * * @return array User grades. */ public function get_grades() { return $this->grades; } /** * Fetches all of the submissions and grades related to the User IDs provided. Use get_grades, get_submissions etc to * retrieve this information. */ public function populate_submissions_and_grades() { global $DB; if (empty($this->get_userids())) { throw new \coding_exception('Please use set_userids() before calling this method.'); } list($sql, $params) = $DB->get_in_or_equal($this->get_userids(), SQL_PARAMS_NAMED); $params['assign'] = $this->get_assign()->get_instance()->id; $this->submissions = $DB->get_records_select('assign_submission', "assignment = :assign AND userid $sql", $params); $this->grades = $DB->get_records_select('assign_grades', "assignment = :assign AND userid $sql", $params); } } classes/privacy/assignfeedback_user_provider.php000064400000004104152265517350016302 0ustar00. /** * This file contains the assignfeedback_user_provider interface. * * Assignment Sub plugins should implement this if they store personal information and can retrieve a userid. * * @package mod_assign * @copyright 2018 Adrian Greeve * * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_assign\privacy; defined('MOODLE_INTERNAL') || die(); interface assignfeedback_user_provider extends \core_privacy\local\request\plugin\subplugin_provider, \core_privacy\local\request\shared_userlist_provider { /** * If you have tables that contain userids and you can generate entries in your tables without creating an * entry in the assign_grades table then please fill in this method. * * @param \core_privacy\local\request\userlist $userlist The userlist object */ public static function get_userids_from_context(\core_privacy\local\request\userlist $userlist); /** * Deletes all feedback for the grade ids / userids provided in a context. * assign_plugin_request_data contains: * - context * - assign object * - grade ids (pluginids) * - user ids * @param assign_plugin_request_data $deletedata A class that contains the relevant information required for deletion. */ public static function delete_feedback_for_grades(assign_plugin_request_data $deletedata); } classes/privacy/provider.php000064400000101330152265517350012232 0ustar00. /** * Privacy class for requesting user data. * * @package mod_assign * @copyright 2018 Adrian Greeve * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_assign\privacy; defined('MOODLE_INTERNAL') || die(); require_once($CFG->dirroot . '/mod/assign/locallib.php'); use \core_privacy\local\metadata\collection; use \core_privacy\local\request\contextlist; use \core_privacy\local\request\writer; use \core_privacy\local\request\approved_contextlist; use \core_privacy\local\request\transform; use \core_privacy\local\request\helper; use \core_privacy\local\request\userlist; use \core_privacy\local\request\approved_userlist; use \core_privacy\manager; /** * Privacy class for requesting user data. * * @package mod_assign * @copyright 2018 Adrian Greeve * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class provider implements \core_privacy\local\metadata\provider, \core_privacy\local\request\plugin\provider, \core_privacy\local\request\user_preference_provider, \core_privacy\local\request\core_userlist_provider { /** Interface for all assign submission sub-plugins. */ const ASSIGNSUBMISSION_INTERFACE = 'mod_assign\privacy\assignsubmission_provider'; /** Interface for all assign submission sub-plugins. This allows for deletion of users with a context. */ const ASSIGNSUBMISSION_USER_INTERFACE = 'mod_assign\privacy\assignsubmission_user_provider'; /** Interface for all assign feedback sub-plugins. This allows for deletion of users with a context. */ const ASSIGNFEEDBACK_USER_INTERFACE = 'mod_assign\privacy\assignfeedback_user_provider'; /** Interface for all assign feedback sub-plugins. */ const ASSIGNFEEDBACK_INTERFACE = 'mod_assign\privacy\assignfeedback_provider'; /** * Provides meta data that is stored about a user with mod_assign * * @param collection $collection A collection of meta data items to be added to. * @return collection Returns the collection of metadata. */ public static function get_metadata(collection $collection): collection { $assigngrades = [ 'userid' => 'privacy:metadata:userid', 'timecreated' => 'privacy:metadata:timecreated', 'timemodified' => 'timemodified', 'grader' => 'privacy:metadata:grader', 'grade' => 'privacy:metadata:grade', 'attemptnumber' => 'attemptnumber' ]; $assignoverrides = [ 'groupid' => 'privacy:metadata:groupid', 'userid' => 'privacy:metadata:userid', 'allowsubmissionsfromdate' => 'allowsubmissionsfromdate', 'duedate' => 'duedate', 'cutoffdate' => 'cutoffdate' ]; $assignsubmission = [ 'userid' => 'privacy:metadata:userid', 'timecreated' => 'privacy:metadata:timecreated', 'timemodified' => 'timemodified', 'timestarted' => 'privacy:metadata:timestarted', 'status' => 'gradingstatus', 'groupid' => 'privacy:metadata:groupid', 'attemptnumber' => 'attemptnumber', 'latest' => 'privacy:metadata:latest' ]; $assignuserflags = [ 'userid' => 'privacy:metadata:userid', 'assignment' => 'privacy:metadata:assignmentid', 'locked' => 'locksubmissions', 'mailed' => 'privacy:metadata:mailed', 'extensionduedate' => 'extensionduedate', 'workflowstate' => 'markingworkflowstate', 'allocatedmarker' => 'allocatedmarker' ]; $assignusermapping = [ 'assignment' => 'privacy:metadata:assignmentid', 'userid' => 'privacy:metadata:userid' ]; $collection->add_database_table('assign_grades', $assigngrades, 'privacy:metadata:assigngrades'); $collection->add_database_table('assign_overrides', $assignoverrides, 'privacy:metadata:assignoverrides'); $collection->add_database_table('assign_submission', $assignsubmission, 'privacy:metadata:assignsubmissiondetail'); $collection->add_database_table('assign_user_flags', $assignuserflags, 'privacy:metadata:assignuserflags'); $collection->add_database_table('assign_user_mapping', $assignusermapping, 'privacy:metadata:assignusermapping'); $collection->add_user_preference('assign_perpage', 'privacy:metadata:assignperpage'); $collection->add_user_preference('assign_filter', 'privacy:metadata:assignfilter'); $collection->add_user_preference('assign_markerfilter', 'privacy:metadata:assignmarkerfilter'); $collection->add_user_preference('assign_workflowfilter', 'privacy:metadata:assignworkflowfilter'); $collection->add_user_preference('assign_quickgrading', 'privacy:metadata:assignquickgrading'); $collection->add_user_preference('assign_downloadasfolders', 'privacy:metadata:assigndownloadasfolders'); // Link to subplugins. $collection->add_plugintype_link('assignsubmission', [],'privacy:metadata:assignsubmissionpluginsummary'); $collection->add_plugintype_link('assignfeedback', [], 'privacy:metadata:assignfeedbackpluginsummary'); $collection->add_subsystem_link('core_message', [], 'privacy:metadata:assignmessageexplanation'); return $collection; } /** * Returns all of the contexts that has information relating to the userid. * * @param int $userid The user ID. * @return contextlist an object with the contexts related to a userid. */ public static function get_contexts_for_userid(int $userid): contextlist { $params = ['modulename' => 'assign', 'contextlevel' => CONTEXT_MODULE, 'userid' => $userid, 'graderid' => $userid, 'aouserid' => $userid, 'asnuserid' => $userid, 'aufuserid' => $userid, 'aumuserid' => $userid]; $sql = "SELECT ctx.id FROM {course_modules} cm JOIN {modules} m ON cm.module = m.id AND m.name = :modulename JOIN {assign} a ON cm.instance = a.id JOIN {context} ctx ON cm.id = ctx.instanceid AND ctx.contextlevel = :contextlevel JOIN {assign_grades} ag ON a.id = ag.assignment AND (ag.userid = :userid OR ag.grader = :graderid)"; $contextlist = new contextlist(); $contextlist->add_from_sql($sql, $params); $sql = "SELECT ctx.id FROM {course_modules} cm JOIN {modules} m ON cm.module = m.id AND m.name = :modulename JOIN {assign} a ON cm.instance = a.id JOIN {context} ctx ON cm.id = ctx.instanceid AND ctx.contextlevel = :contextlevel JOIN {assign_overrides} ao ON a.id = ao.assignid WHERE ao.userid = :aouserid"; $contextlist->add_from_sql($sql, $params); $sql = "SELECT ctx.id FROM {course_modules} cm JOIN {modules} m ON cm.module = m.id AND m.name = :modulename JOIN {assign} a ON cm.instance = a.id JOIN {context} ctx ON cm.id = ctx.instanceid AND ctx.contextlevel = :contextlevel JOIN {assign_submission} asn ON a.id = asn.assignment WHERE asn.userid = :asnuserid"; $contextlist->add_from_sql($sql, $params); $sql = "SELECT ctx.id FROM {course_modules} cm JOIN {modules} m ON cm.module = m.id AND m.name = :modulename JOIN {assign} a ON cm.instance = a.id JOIN {context} ctx ON cm.id = ctx.instanceid AND ctx.contextlevel = :contextlevel JOIN {assign_user_flags} auf ON a.id = auf.assignment WHERE auf.userid = :aufuserid"; $contextlist->add_from_sql($sql, $params); $sql = "SELECT ctx.id FROM {course_modules} cm JOIN {modules} m ON cm.module = m.id AND m.name = :modulename JOIN {assign} a ON cm.instance = a.id JOIN {context} ctx ON cm.id = ctx.instanceid AND ctx.contextlevel = :contextlevel JOIN {assign_user_mapping} aum ON a.id = aum.assignment WHERE aum.userid = :aumuserid"; $contextlist->add_from_sql($sql, $params); manager::plugintype_class_callback('assignfeedback', self::ASSIGNFEEDBACK_INTERFACE, 'get_context_for_userid_within_feedback', [$userid, $contextlist]); manager::plugintype_class_callback('assignsubmission', self::ASSIGNSUBMISSION_INTERFACE, 'get_context_for_userid_within_submission', [$userid, $contextlist]); return $contextlist; } /** * Get the list of users who have data within a context. * * @param userlist $userlist The userlist containing the list of users who have data in this context/plugin combination. */ public static function get_users_in_context(userlist $userlist) { $context = $userlist->get_context(); if ($context->contextlevel != CONTEXT_MODULE) { return; } $params = [ 'modulename' => 'assign', 'contextid' => $context->id, 'contextlevel' => CONTEXT_MODULE ]; $sql = "SELECT g.userid, g.grader FROM {context} ctx JOIN {course_modules} cm ON cm.id = ctx.instanceid JOIN {modules} m ON m.id = cm.module AND m.name = :modulename JOIN {assign} a ON a.id = cm.instance JOIN {assign_grades} g ON a.id = g.assignment WHERE ctx.id = :contextid AND ctx.contextlevel = :contextlevel"; $userlist->add_from_sql('userid', $sql, $params); $userlist->add_from_sql('grader', $sql, $params); $sql = "SELECT o.userid FROM {context} ctx JOIN {course_modules} cm ON cm.id = ctx.instanceid JOIN {modules} m ON m.id = cm.module AND m.name = :modulename JOIN {assign} a ON a.id = cm.instance JOIN {assign_overrides} o ON a.id = o.assignid WHERE ctx.id = :contextid AND ctx.contextlevel = :contextlevel"; $userlist->add_from_sql('userid', $sql, $params); $sql = "SELECT s.userid FROM {context} ctx JOIN {course_modules} cm ON cm.id = ctx.instanceid JOIN {modules} m ON m.id = cm.module AND m.name = :modulename JOIN {assign} a ON a.id = cm.instance JOIN {assign_submission} s ON a.id = s.assignment WHERE ctx.id = :contextid AND ctx.contextlevel = :contextlevel"; $userlist->add_from_sql('userid', $sql, $params); $sql = "SELECT uf.userid FROM {context} ctx JOIN {course_modules} cm ON cm.id = ctx.instanceid JOIN {modules} m ON m.id = cm.module AND m.name = :modulename JOIN {assign} a ON a.id = cm.instance JOIN {assign_user_flags} uf ON a.id = uf.assignment WHERE ctx.id = :contextid AND ctx.contextlevel = :contextlevel"; $userlist->add_from_sql('userid', $sql, $params); $sql = "SELECT um.userid FROM {context} ctx JOIN {course_modules} cm ON cm.id = ctx.instanceid JOIN {modules} m ON m.id = cm.module AND m.name = :modulename JOIN {assign} a ON a.id = cm.instance JOIN {assign_user_mapping} um ON a.id = um.assignment WHERE ctx.id = :contextid AND ctx.contextlevel = :contextlevel"; $userlist->add_from_sql('userid', $sql, $params); manager::plugintype_class_callback('assignsubmission', self::ASSIGNSUBMISSION_USER_INTERFACE, 'get_userids_from_context', [$userlist]); manager::plugintype_class_callback('assignfeedback', self::ASSIGNFEEDBACK_USER_INTERFACE, 'get_userids_from_context', [$userlist]); } /** * Write out the user data filtered by contexts. * * @param approved_contextlist $contextlist contexts that we are writing data out from. */ public static function export_user_data(approved_contextlist $contextlist) { foreach ($contextlist->get_contexts() as $context) { // Check that the context is a module context. if ($context->contextlevel != CONTEXT_MODULE) { continue; } $user = $contextlist->get_user(); $assigndata = helper::get_context_data($context, $user); helper::export_context_files($context, $user); writer::with_context($context)->export_data([], $assigndata); $assign = new \assign($context, null, null); // I need to find out if I'm a student or a teacher. if ($userids = self::get_graded_users($user->id, $assign)) { // Return teacher info. $currentpath = [get_string('privacy:studentpath', 'mod_assign')]; foreach ($userids as $studentuserid) { $studentpath = array_merge($currentpath, [$studentuserid->id]); static::export_submission($assign, $studentuserid, $context, $studentpath, true); } } static::export_overrides($context, $assign, $user); static::export_submission($assign, $user, $context, []); // Meta data. self::store_assign_user_flags($context, $assign, $user->id); if ($assign->is_blind_marking()) { $uniqueid = $assign->get_uniqueid_for_user_static($assign->get_instance()->id, $contextlist->get_user()->id); if ($uniqueid) { writer::with_context($context) ->export_metadata([get_string('blindmarking', 'mod_assign')], 'blindmarkingid', $uniqueid, get_string('privacy:blindmarkingidentifier', 'mod_assign')); } } } } /** * Delete all use data which matches the specified context. * * @param \context $context The module context. */ public static function delete_data_for_all_users_in_context(\context $context) { global $DB; if ($context->contextlevel == CONTEXT_MODULE) { $cm = get_coursemodule_from_id('assign', $context->instanceid); if ($cm) { // Get the assignment related to this context. $assign = new \assign($context, null, null); // What to do first... Get sub plugins to delete their stuff. $requestdata = new assign_plugin_request_data($context, $assign); manager::plugintype_class_callback('assignsubmission', self::ASSIGNSUBMISSION_INTERFACE, 'delete_submission_for_context', [$requestdata]); $requestdata = new assign_plugin_request_data($context, $assign); manager::plugintype_class_callback('assignfeedback', self::ASSIGNFEEDBACK_INTERFACE, 'delete_feedback_for_context', [$requestdata]); $DB->delete_records('assign_grades', ['assignment' => $assign->get_instance()->id]); // Delete advanced grading information. $gradingmanager = get_grading_manager($context, 'mod_assign', 'submissions'); $controller = $gradingmanager->get_active_controller(); if (isset($controller)) { \core_grading\privacy\provider::delete_instance_data($context); } // Time to roll my own method for deleting overrides. static::delete_overrides_for_users($assign); $DB->delete_records('assign_submission', ['assignment' => $assign->get_instance()->id]); $DB->delete_records('assign_user_flags', ['assignment' => $assign->get_instance()->id]); $DB->delete_records('assign_user_mapping', ['assignment' => $assign->get_instance()->id]); } } } /** * Delete all user data for the specified user, in the specified contexts. * * @param approved_contextlist $contextlist The approved contexts and user information to delete information for. */ public static function delete_data_for_user(approved_contextlist $contextlist) { global $DB; $user = $contextlist->get_user(); foreach ($contextlist as $context) { if ($context->contextlevel != CONTEXT_MODULE) { continue; } // Get the assign object. $assign = new \assign($context, null, null); $assignid = $assign->get_instance()->id; $submissions = $DB->get_records('assign_submission', ['assignment' => $assignid, 'userid' => $user->id]); foreach ($submissions as $submission) { $requestdata = new assign_plugin_request_data($context, $assign, $submission, [], $user); manager::plugintype_class_callback('assignsubmission', self::ASSIGNSUBMISSION_INTERFACE, 'delete_submission_for_userid', [$requestdata]); } $grades = $DB->get_records('assign_grades', ['assignment' => $assignid, 'userid' => $user->id]); $gradingmanager = get_grading_manager($context, 'mod_assign', 'submissions'); $controller = $gradingmanager->get_active_controller(); foreach ($grades as $grade) { $requestdata = new assign_plugin_request_data($context, $assign, $grade, [], $user); manager::plugintype_class_callback('assignfeedback', self::ASSIGNFEEDBACK_INTERFACE, 'delete_feedback_for_grade', [$requestdata]); // Delete advanced grading information. if (isset($controller)) { \core_grading\privacy\provider::delete_instance_data($context, $grade->id); } } static::delete_overrides_for_users($assign, [$user->id]); $DB->delete_records('assign_user_flags', ['assignment' => $assignid, 'userid' => $user->id]); $DB->delete_records('assign_user_mapping', ['assignment' => $assignid, 'userid' => $user->id]); $DB->delete_records('assign_grades', ['assignment' => $assignid, 'userid' => $user->id]); $DB->delete_records('assign_submission', ['assignment' => $assignid, 'userid' => $user->id]); } } /** * Delete multiple users within a single context. * * @param approved_userlist $userlist The approved context and user information to delete information for. */ public static function delete_data_for_users(approved_userlist $userlist) { global $DB; $context = $userlist->get_context(); if ($context->contextlevel != CONTEXT_MODULE) { return; } $userids = $userlist->get_userids(); $assign = new \assign($context, null, null); $assignid = $assign->get_instance()->id; $requestdata = new assign_plugin_request_data($context, $assign); $requestdata->set_userids($userids); $requestdata->populate_submissions_and_grades(); manager::plugintype_class_callback('assignsubmission', self::ASSIGNSUBMISSION_USER_INTERFACE, 'delete_submissions', [$requestdata]); manager::plugintype_class_callback('assignfeedback', self::ASSIGNFEEDBACK_USER_INTERFACE, 'delete_feedback_for_grades', [$requestdata]); // Update this function to delete advanced grading information. $gradingmanager = get_grading_manager($context, 'mod_assign', 'submissions'); $controller = $gradingmanager->get_active_controller(); if (isset($controller)) { $gradeids = $requestdata->get_gradeids(); // Careful here, if no gradeids are provided then all data is deleted for the context. if (!empty($gradeids)) { \core_grading\privacy\provider::delete_data_for_instances($context, $gradeids); } } static::delete_overrides_for_users($assign, $userids); list($sql, $params) = $DB->get_in_or_equal($userids, SQL_PARAMS_NAMED); $params['assignment'] = $assignid; $DB->delete_records_select('assign_user_flags', "assignment = :assignment AND userid $sql", $params); $DB->delete_records_select('assign_user_mapping', "assignment = :assignment AND userid $sql", $params); $DB->delete_records_select('assign_grades', "assignment = :assignment AND userid $sql", $params); $DB->delete_records_select('assign_submission', "assignment = :assignment AND userid $sql", $params); } /** * Deletes assignment overrides in bulk * * @param \assign $assign The assignment object * @param array $userids An array of user IDs */ protected static function delete_overrides_for_users(\assign $assign, array $userids = []) { global $DB; $assignid = $assign->get_instance()->id; $usersql = ''; $params = ['assignid' => $assignid]; if (!empty($userids)) { list($usersql, $userparams) = $DB->get_in_or_equal($userids, SQL_PARAMS_NAMED); $params = array_merge($params, $userparams); $overrides = $DB->get_records_select('assign_overrides', "assignid = :assignid AND userid $usersql", $params); } else { $overrides = $DB->get_records('assign_overrides', $params); } if (!empty($overrides)) { $params = ['modulename' => 'assign', 'instance' => $assignid]; if (!empty($userids)) { $params = array_merge($params, $userparams); $DB->delete_records_select('event', "modulename = :modulename AND instance = :instance AND userid $usersql", $params); // Setting up for the next query. $params = $userparams; $usersql = "AND userid $usersql"; } else { $DB->delete_records('event', $params); // Setting up for the next query. $params = []; } list($overridesql, $overrideparams) = $DB->get_in_or_equal(array_keys($overrides), SQL_PARAMS_NAMED); $params = array_merge($params, $overrideparams); $DB->delete_records_select('assign_overrides', "id $overridesql $usersql", $params); } } /** * Find out if this user has graded any users. * * @param int $userid The user ID (potential teacher). * @param \assign $assign The assignment object. * @return array If successful an array of objects with userids that this user graded, otherwise false. */ protected static function get_graded_users(int $userid, \assign $assign) { $params = ['grader' => $userid, 'assignid' => $assign->get_instance()->id]; $sql = "SELECT DISTINCT userid AS id FROM {assign_grades} WHERE grader = :grader AND assignment = :assignid"; $useridlist = new useridlist($userid, $assign->get_instance()->id); $useridlist->add_from_sql($sql, $params); // Call sub-plugins to see if they have information not already collected. manager::plugintype_class_callback('assignsubmission', self::ASSIGNSUBMISSION_INTERFACE, 'get_student_user_ids', [$useridlist]); manager::plugintype_class_callback('assignfeedback', self::ASSIGNFEEDBACK_INTERFACE, 'get_student_user_ids', [$useridlist]); $userids = $useridlist->get_userids(); return ($userids) ? $userids : false; } /** * Writes out various user meta data about the assignment. * * @param \context $context The context of this assignment. * @param \assign $assign The assignment object. * @param int $userid The user ID */ protected static function store_assign_user_flags(\context $context, \assign $assign, int $userid) { $datatypes = ['locked' => get_string('locksubmissions', 'mod_assign'), 'mailed' => get_string('privacy:metadata:mailed', 'mod_assign'), 'extensionduedate' => get_string('extensionduedate', 'mod_assign'), 'workflowstate' => get_string('markingworkflowstate', 'mod_assign'), 'allocatedmarker' => get_string('allocatedmarker_help', 'mod_assign')]; $userflags = (array)$assign->get_user_flags($userid, false); foreach ($datatypes as $key => $description) { if (isset($userflags[$key]) && !empty($userflags[$key])) { $value = $userflags[$key]; if ($key == 'locked' || $key == 'mailed') { $value = transform::yesno($value); } else if ($key == 'extensionduedate') { $value = transform::datetime($value); } writer::with_context($context)->export_metadata([], $key, $value, $description); } } } /** * Formats and then exports the user's grade data. * * @param \stdClass $grade The assign grade object * @param \context $context The context object * @param array $currentpath Current directory path that we are exporting to. */ protected static function export_grade_data(\stdClass $grade, \context $context, array $currentpath) { $gradedata = (object)[ 'timecreated' => transform::datetime($grade->timecreated), 'timemodified' => transform::datetime($grade->timemodified), 'grader' => transform::user($grade->grader), 'grade' => $grade->grade, 'attemptnumber' => ($grade->attemptnumber + 1) ]; writer::with_context($context) ->export_data(array_merge($currentpath, [get_string('privacy:gradepath', 'mod_assign')]), $gradedata); } /** * Formats and then exports the user's submission data. * * @param \stdClass $submission The assign submission object * @param \context $context The context object * @param array $currentpath Current directory path that we are exporting to. */ protected static function export_submission_data(\stdClass $submission, \context $context, array $currentpath) { $submissiondata = (object)[ 'timecreated' => transform::datetime($submission->timecreated), 'timemodified' => transform::datetime($submission->timemodified), 'timestarted' => transform::datetime($submission->timestarted), 'status' => get_string('submissionstatus_' . $submission->status, 'mod_assign'), 'groupid' => $submission->groupid, 'attemptnumber' => ($submission->attemptnumber + 1), 'latest' => transform::yesno($submission->latest) ]; writer::with_context($context) ->export_data(array_merge($currentpath, [get_string('privacy:submissionpath', 'mod_assign')]), $submissiondata); } /** * Stores the user preferences related to mod_assign. * * @param int $userid The user ID that we want the preferences for. */ public static function export_user_preferences(int $userid) { $context = \context_system::instance(); $assignpreferences = [ 'assign_perpage' => ['string' => get_string('privacy:metadata:assignperpage', 'mod_assign'), 'bool' => false], 'assign_filter' => ['string' => get_string('privacy:metadata:assignfilter', 'mod_assign'), 'bool' => false], 'assign_markerfilter' => ['string' => get_string('privacy:metadata:assignmarkerfilter', 'mod_assign'), 'bool' => true], 'assign_workflowfilter' => ['string' => get_string('privacy:metadata:assignworkflowfilter', 'mod_assign'), 'bool' => true], 'assign_quickgrading' => ['string' => get_string('privacy:metadata:assignquickgrading', 'mod_assign'), 'bool' => true], 'assign_downloadasfolders' => ['string' => get_string('privacy:metadata:assigndownloadasfolders', 'mod_assign'), 'bool' => true] ]; foreach ($assignpreferences as $key => $preference) { $value = get_user_preferences($key, null, $userid); if ($preference['bool']) { $value = transform::yesno($value); } if (isset($value)) { writer::with_context($context)->export_user_preference('mod_assign', $key, $value, $preference['string']); } } } /** * Export overrides for this assignment. * * @param \context $context Context * @param \assign $assign The assign object. * @param \stdClass $user The user object. */ public static function export_overrides(\context $context, \assign $assign, \stdClass $user) { $overrides = $assign->override_exists($user->id); // Overrides returns an array with data in it, but an override with actual data will have the assign ID set. if (isset($overrides->assignid)) { $data = new \stdClass(); if (!empty($overrides->duedate)) { $data->duedate = transform::datetime($overrides->duedate); } if (!empty($overrides->cutoffdate)) { $data->cutoffdate = transform::datetime($overrides->cutoffdate); } if (!empty($overrides->allowsubmissionsfromdate)) { $data->allowsubmissionsfromdate = transform::datetime($overrides->allowsubmissionsfromdate); } if (!empty($data)) { writer::with_context($context)->export_data([get_string('overrides', 'mod_assign')], $data); } } } /** * Exports assignment submission data for a user. * * @param \assign $assign The assignment object * @param \stdClass $user The user object * @param \context_module $context The context * @param array $path The path for exporting data * @param bool|boolean $exportforteacher A flag for if this is exporting data as a teacher. */ protected static function export_submission(\assign $assign, \stdClass $user, \context_module $context, array $path, bool $exportforteacher = false) { $submissions = $assign->get_all_submissions($user->id); $teacher = ($exportforteacher) ? $user : null; $gradingmanager = get_grading_manager($context, 'mod_assign', 'submissions'); $controller = $gradingmanager->get_active_controller(); foreach ($submissions as $submission) { // Attempt numbers start at zero, which is fine for programming, but doesn't make as much sense // for users. $submissionpath = array_merge($path, [get_string('privacy:attemptpath', 'mod_assign', ($submission->attemptnumber + 1))]); $params = new assign_plugin_request_data($context, $assign, $submission, $submissionpath ,$teacher); manager::plugintype_class_callback('assignsubmission', self::ASSIGNSUBMISSION_INTERFACE, 'export_submission_user_data', [$params]); if (!isset($teacher)) { self::export_submission_data($submission, $context, $submissionpath); } $grade = $assign->get_user_grade($user->id, false, $submission->attemptnumber); if ($grade) { $params = new assign_plugin_request_data($context, $assign, $grade, $submissionpath, $teacher); manager::plugintype_class_callback('assignfeedback', self::ASSIGNFEEDBACK_INTERFACE, 'export_feedback_user_data', [$params]); self::export_grade_data($grade, $context, $submissionpath); // Check for advanced grading and retrieve that information. if (isset($controller)) { \core_grading\privacy\provider::export_item_data($context, $grade->id, $submissionpath); } } } } } classes/privacy/assignsubmission_user_provider.php000064400000004103152265517350016750 0ustar00. /** * This file contains the assignsubmission_user_provider interface. * * Assignment Sub plugins should implement this if they store personal information and can retrieve a userid. * * @package mod_assign * @copyright 2018 Adrian Greeve * * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_assign\privacy; use core_privacy\local\request\userlist; defined('MOODLE_INTERNAL') || die(); interface assignsubmission_user_provider extends \core_privacy\local\request\plugin\subplugin_provider, \core_privacy\local\request\shared_userlist_provider { /** * If you have tables that contain userids and you can generate entries in your tables without creating an * entry in the assign_submission table then please fill in this method. * * @param userlist $userlist The userlist object */ public static function get_userids_from_context(userlist $userlist); /** * Deletes all submissions for the submission ids / userids provided in a context. * assign_plugin_request_data contains: * - context * - assign object * - submission ids (pluginids) * - user ids * @param assign_plugin_request_data $deletedata A class that contains the relevant information required for deletion. */ public static function delete_submissions(assign_plugin_request_data $deletedata); } classes/privacy/assignsubmission_provider.php000064400000007227152265517350015724 0ustar00. /** * This file contains the assignsubmission_provider interface. * * Assignment Sub plugins should implement this if they store personal information. * * @package mod_assign * @copyright 2018 Adrian Greeve * * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_assign\privacy; use core_privacy\local\request\contextlist; defined('MOODLE_INTERNAL') || die(); interface assignsubmission_provider extends \core_privacy\local\request\plugin\subplugin_provider { /** * Retrieves the contextids associated with the provided userid for this subplugin. * NOTE if your subplugin must have an entry in the assign_submission table to work, then this * method can be empty. * * @param int $userid The user ID to get context IDs for. * @param \core_privacy\local\request\contextlist $contextlist Use add_from_sql with this object to add your context IDs. */ public static function get_context_for_userid_within_submission(int $userid, contextlist $contextlist); /** * Returns student user ids related to the provided teacher ID. If it is possible that a student ID will not be returned by * the sql query in \mod_assign\privacy\provider::find_grader_info() Then you need to provide some sql to retrive those * student IDs. This is highly likely if you had to fill in get_context_for_userid_within_submission above. * * @param useridlist $useridlist A user ID list object that you can append your user IDs to. */ public static function get_student_user_ids(useridlist $useridlist); /** * This method is used to export any user data this sub-plugin has using the assign_plugin_request_data object to get the * context and userid. * assign_plugin_request_data contains: * - context * - submission object * - current path (subcontext) * - user object * * @param assign_plugin_request_data $exportdata Information to use to export user data for this sub-plugin. */ public static function export_submission_user_data(assign_plugin_request_data $exportdata); /** * Any call to this method should delete all user data for the context defined in the deletion_criteria. * assign_plugin_request_data contains: * - context * - assign object * * @param assign_plugin_request_data $requestdata Information to use to delete user data for this submission. */ public static function delete_submission_for_context(assign_plugin_request_data $requestdata); /** * A call to this method should delete user data (where practicle) from the userid and context. * assign_plugin_request_data contains: * - context * - submission object * - user object * - assign object * * @param assign_plugin_request_data $exportdata Details about the user and context to focus the deletion. */ public static function delete_submission_for_userid(assign_plugin_request_data $exportdata); } classes/dates.php000064400000007512152265517350010032 0ustar00. /** * Contains the class for fetching the important dates in mod_assign for a given module instance and a user. * * @package mod_assign * @copyright 2021 Shamim Rezaie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ declare(strict_types=1); namespace mod_assign; use core\activity_dates; /** * Class for fetching the important dates in mod_assign for a given module instance and a user. * * @copyright 2021 Shamim Rezaie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class dates extends activity_dates { /** @var int|null $timedue the activity due date */ private ?int $timedue; /** * Returns a list of important dates in mod_assign * * @return array */ protected function get_dates(): array { global $CFG; require_once($CFG->dirroot . '/mod/assign/locallib.php'); $this->timedue = null; $course = get_course($this->cm->course); $context = \context_module::instance($this->cm->id); $assign = new \assign($context, $this->cm, $course); $timeopen = $this->cm->customdata['allowsubmissionsfromdate'] ?? null; $timedue = $this->cm->customdata['duedate'] ?? null; $activitygroup = groups_get_activity_group($this->cm, true); if ($activitygroup) { if ($assign->can_view_grades()) { $groupoverride = \cache::make('mod_assign', 'overrides')->get("{$this->cm->instance}_g_{$activitygroup}"); if (!empty($groupoverride->allowsubmissionsfromdate)) { $timeopen = $groupoverride->allowsubmissionsfromdate; } if (!empty($groupoverride->duedate)) { $timedue = $groupoverride->duedate; } } } $now = time(); $dates = []; if ($timeopen) { $openlabelid = $timeopen > $now ? 'activitydate:submissionsopen' : 'activitydate:submissionsopened'; $date = [ 'dataid' => 'allowsubmissionsfromdate', 'label' => get_string($openlabelid, 'mod_assign'), 'timestamp' => (int) $timeopen, ]; if ($course->relativedatesmode && $assign->can_view_grades()) { $date['relativeto'] = $course->startdate; } $dates[] = $date; } if ($timedue) { $this->timedue = (int) $timedue; $date = [ 'dataid' => 'duedate', 'label' => get_string('activitydate:submissionsdue', 'mod_assign'), 'timestamp' => $this->timedue, ]; if ($course->relativedatesmode && $assign->can_view_grades()) { $date['relativeto'] = $course->startdate; } $dates[] = $date; } return $dates; } /** * Returns the dues date data, if any. * @return int|null the due date timestamp or null if not set. */ public function get_due_date(): ?int { if (!isset($this->timedue)) { $this->get_dates(); } return $this->timedue; } } classes/navigation/views/secondary.php000064400000003325152265517350014213 0ustar00. namespace mod_assign\navigation\views; use core\navigation\views\secondary as core_secondary; /** * Class secondary_navigation_view. * * Custom implementation for a plugin. * * @package mod_assign * @category navigation * @copyright 2021 onwards Peter Dias * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class secondary extends core_secondary { protected function get_default_module_mapping(): array { $defaultmaping = parent::get_default_module_mapping(); $defaultmaping[self::TYPE_SETTING] = array_merge($defaultmaping[self::TYPE_SETTING], [ 'modedit' => 1, "mod_{$this->page->activityname}_useroverrides" => 2, // Overrides are module specific. "mod_{$this->page->activityname}_groupoverrides" => 3, "mod_{$this->page->activityname}_submissions" => 4, ]); $defaultmaping[self::TYPE_CUSTOM] = array_merge($defaultmaping[self::TYPE_CUSTOM], [ 'advgrading' => 5, ]); return $defaultmaping; } } classes/group_observers.php000064400000004577152265517350012170 0ustar00. /** * Group observers. * * @package mod_assign * @copyright 2016 Ilya Tregubov * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_assign; defined('MOODLE_INTERNAL') || die(); require_once($CFG->dirroot . '/mod/assign/locallib.php'); /** * Group observers class. * * @package mod_assign * @copyright 2016 Ilya Tregubov * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class group_observers { /** * Flag whether a course reset is in progress or not. * * @var int The course ID. */ protected static $resetinprogress = false; /** * A course reset has started. * * @param \core\event\base $event The event. * @return void */ public static function course_reset_started($event) { self::$resetinprogress = $event->courseid; } /** * A course reset has ended. * * @param \core\event\base $event The event. * @return void */ public static function course_reset_ended($event) { if (!empty(self::$resetinprogress)) { if (!empty($event->other['reset_options']['reset_groups_remove'])) { assign_process_group_deleted_in_course($event->courseid); } } self::$resetinprogress = null; } /** * A group was deleted. * * @param \core\event\base $event The event. * @return void */ public static function group_deleted($event) { if (!empty(self::$resetinprogress)) { // We will take care of that once the course reset ends. return; } assign_process_group_deleted_in_course($event->courseid, $event->objectid); } } classes/penalty/helper.php000064400000012673152265517350011671 0ustar00. namespace mod_assign\penalty; use assign; use core\context\module as context_module; use core_grades\penalty_manager; use grade_item; /** * Helper class for penalty in assignment module. * * @package mod_assign * @copyright 2024 Catalyst IT Australia Pty Ltd * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class helper { /** * Check if penalty is enabled for an assignment. * * @param int $assignid The assignment id. */ public static function is_penalty_enabled(int $assignid): bool { // Get the assignment course module. $cm = get_coursemodule_from_instance('assign', $assignid); $context = context_module::instance($cm->id); // Get the assignment instance. $assign = new assign($context, $cm, $cm->course); // Check if due date is set. if (!$assign->get_instance()->duedate) { return false; } // Check if the grade type is set to GRADE_TYPE_VALUE (grade 1 to 100). if ($assign->get_instance()->grade < GRADE_TYPE_VALUE) { return false; } // Check if the assignment is set to use penalty. if (!$assign->get_instance()->gradepenalty) { return false; } return true; } /** * Apply penalty to a user. * * @param int $assignid The assignment id. * @param int $userid The user id. */ public static function apply_penalty_to_user(int $assignid, int $userid): void { global $CFG, $DB; require_once($CFG->dirroot . '/mod/assign/locallib.php'); // Check if penalty is enabled for this assignment. if (!self::is_penalty_enabled($assignid)) { return; } // Get the assignment course module. $cm = get_coursemodule_from_instance('assign', $assignid); $context = context_module::instance($cm->id); // Get the assignment instance. $assign = new assign($context, $cm, $cm->course); // Find the graded attempt. $sql = "SELECT MAX(attemptnumber) as attemptnumber FROM {assign_grades} WHERE assignment = :assignid AND userid = :userid AND grade >= 0"; $assigngrade = $DB->get_record_sql($sql, ['assignid' => $assignid, 'userid' => $userid]); // Get the submission. if ($assign->get_instance()->teamsubmission) { $submission = $assign->get_group_submission($userid, 0, false, $assigngrade->attemptnumber); } else { $submission = $assign->get_user_submission($userid, false, $assigngrade->attemptnumber); } // Check if the submission is null. if ($submission === null) { debugging( "Submission not found for user {$userid} in assignment {$assignid} attempt {$assigngrade->attemptnumber}", ); return; } // Get submission date. $submissiondate = $submission->timemodified; // Check if we have valid submission date. if (empty($submissiondate)) { debugging( "Invalid submission date for user {$userid} in assignment {$assignid} attempt {$assigngrade->attemptnumber}", ); return; } // Get the due date from the override if it exists. Otherwise, retrieve the date from the assignment settings. $duedate = $assign->override_exists($userid)->duedate ?? $assign->get_instance()->duedate; // Get extension. $userflags = $assign->get_user_flags($userid, false); if (!empty($userflags)) { $duedate = max($userflags->extensionduedate, $duedate); } // Get grade item. $gradeitem = grade_item::fetch([ 'courseid' => $assign->get_course()->id, 'itemtype' => 'mod', 'itemmodule' => 'assign', 'iteminstance' => $assign->get_instance()->id, 'itemnumber' => 0, ]); // Apply penalty. $container = penalty_manager::apply_grade_penalty_to_user($userid, $gradeitem, $submissiondate, $duedate); if ($container->get_grade_before_penalties() == 0) { // There is no deduction applied to grade 0. $deductedpercentage = 0; } else { $deductedpercentage = $container->get_penalty() / $container->get_grade_before_penalties() * 100; } // Store the assign grade penalty. $DB->set_field_select( 'assign_grades', 'penalty', $deductedpercentage, 'assignment = :assignid AND userid = :userid AND attemptnumber = :attemptnumber', ['assignid' => $assignid, 'userid' => $userid, 'attemptnumber' => $assigngrade->attemptnumber], ); } } classes/notification_helper.php000064400000067555152265517350012774 0ustar00. namespace mod_assign; use DateTime; use core\output\html_writer; defined('MOODLE_INTERNAL') || die(); require_once($CFG->dirroot . '/mod/assign/locallib.php'); /** * Helper for sending assignment related notifications. * * @package mod_assign * @copyright 2024 David Woloszyn * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class notification_helper { /** * @var int Due soon time interval of 48 hours. */ private const INTERVAL_DUE_SOON = (DAYSECS * 2); /** * @var int Overdue time interval of 2 hours. */ private const INTERVAL_OVERDUE = (HOURSECS * 2); /** * @var int Due digest time interval of 7 days. */ private const INTERVAL_DUE_DIGEST = WEEKSECS; /** * @var string Due soon notification type. */ public const TYPE_DUE_SOON = 'assign_due_soon'; /** * @var string Overdue notification type. */ public const TYPE_OVERDUE = 'assign_overdue'; /** * @var string Due digest notification type. */ public const TYPE_DUE_DIGEST = 'assign_due_digest'; /** * Get all assignments that have an approaching due date (includes users and groups with due date overrides). * * @return \moodle_recordset Returns the matching assignment records. */ public static function get_due_soon_assignments(): \moodle_recordset { global $DB; $timenow = self::get_time_now(); $futuretime = self::get_future_time(self::INTERVAL_DUE_SOON); $sql = "SELECT DISTINCT a.id FROM {assign} a JOIN {course_modules} cm ON a.id = cm.instance JOIN {course} c ON a.course = c.id JOIN {modules} m ON cm.module = m.id AND m.name = :modulename LEFT JOIN {assign_overrides} ao ON a.id = ao.assignid WHERE (a.duedate < :futuretime OR ao.duedate < :ao_futuretime) AND (a.duedate > :timenow OR ao.duedate > :ao_timenow) AND cm.visible = 1 AND c.visible = 1"; $params = [ 'timenow' => $timenow, 'futuretime' => $futuretime, 'ao_timenow' => $timenow, 'ao_futuretime' => $futuretime, 'modulename' => 'assign', ]; return $DB->get_recordset_sql($sql, $params); } /** * Get all assignments that are overdue, but not exceeding the cut-off date (includes users and groups with due date overrides). * * We don't want to get every single overdue assignment ever. * We just want the ones within the specified window. * * @return \moodle_recordset Returns the matching assignment records. */ public static function get_overdue_assignments(): \moodle_recordset { global $DB; $timenow = self::get_time_now(); $timewindow = self::get_time_now() - self::INTERVAL_OVERDUE; // Get all assignments that: // - Are overdue. // - Do not exceed the window of time in the past. // - Are still within the cut-off (if it is set). $sql = "SELECT DISTINCT a.id FROM {assign} a JOIN {course_modules} cm ON a.id = cm.instance JOIN {course} c ON a.course = c.id JOIN {modules} m ON cm.module = m.id AND m.name = :modulename LEFT JOIN {assign_overrides} ao ON a.id = ao.assignid WHERE (a.duedate < :dd_timenow OR ao.duedate < :dd_ao_timenow) AND (a.duedate > :dd_timewindow OR ao.duedate > :dd_ao_timewindow) AND ((a.cutoffdate > :co_timenow OR a.cutoffdate = 0) OR (ao.cutoffdate > :co_ao_timenow OR ao.cutoffdate = 0)) AND cm.visible = 1 AND c.visible = 1"; $params = [ 'dd_timenow' => $timenow, 'dd_ao_timenow' => $timenow, 'dd_timewindow' => $timewindow, 'dd_ao_timewindow' => $timewindow, 'co_timenow' => $timenow, 'co_ao_timenow' => $timenow, 'modulename' => 'assign', ]; return $DB->get_recordset_sql($sql, $params); } /** * Get all assignments that are due in 7 days (includes users and groups with due date overrides). * * @return \moodle_recordset Returns the matching assignment records. */ public static function get_due_digest_assignments(): \moodle_recordset { global $DB; $futuretime = self::get_future_time(self::INTERVAL_DUE_DIGEST); $day = self::get_day_start_and_end($futuretime); $sql = "SELECT DISTINCT a.id FROM {assign} a JOIN {course_modules} cm ON a.id = cm.instance JOIN {course} c ON a.course = c.id JOIN {modules} m ON cm.module = m.id AND m.name = :modulename LEFT JOIN {assign_overrides} ao ON a.id = ao.assignid WHERE (a.duedate <= :endofday OR ao.duedate <= :ao_endofday) AND (a.duedate >= :startofday OR ao.duedate >= :ao_startofday) AND cm.visible = 1 AND c.visible = 1"; $params = [ 'startofday' => $day['start'], 'endofday' => $day['end'], 'ao_startofday' => $day['start'], 'ao_endofday' => $day['end'], 'modulename' => 'assign', ]; return $DB->get_recordset_sql($sql, $params); } /** * Get all assignments for a user that are due in 7 days (includes users and groups with due date overrides). * * @param int $userid The user id. * @return \moodle_recordset Returns the matching assignment records. */ public static function get_due_digest_assignments_for_user(int $userid): \moodle_recordset { global $DB; $futuretime = self::get_future_time(self::INTERVAL_DUE_DIGEST); $day = self::get_day_start_and_end($futuretime); $sql = "SELECT DISTINCT a.id, a.duedate, a.name AS assignmentname, c.fullname AS coursename, cm.id AS cmid FROM {assign} a JOIN {course} c ON a.course = c.id JOIN {course_modules} cm ON a.id = cm.instance JOIN {modules} m ON cm.module = m.id AND m.name = :modulename JOIN {enrol} e ON c.id = e.courseid JOIN {user_enrolments} ue ON e.id = ue.enrolid LEFT JOIN {assign_overrides} ao ON a.id = ao.assignid WHERE (a.duedate <= :endofday OR ao.duedate <= :ao_endofday) AND (a.duedate >= :startofday OR ao.duedate >= :ao_startofday) AND ue.userid = :userid AND cm.visible = 1 AND c.visible = 1 ORDER BY a.duedate ASC"; $params = [ 'startofday' => $day['start'], 'endofday' => $day['end'], 'ao_startofday' => $day['start'], 'ao_endofday' => $day['end'], 'modulename' => 'assign', 'userid' => $userid, ]; return $DB->get_recordset_sql($sql, $params); } /** * Get all assignment users that we should send the notification to. * * @param int $assignmentid The assignment id. * @param string $type The notification type. * @return array The users after all filtering has been applied. */ public static function get_users_within_assignment(int $assignmentid, string $type): array { // Get assignment data. $assignmentobj = self::get_assignment_data($assignmentid); // Get our assignment users. $users = $assignmentobj->list_participants(0, true, false, true); // If it's an overdue type and there are no submission plugins, skip the assignment. if ($type == self::TYPE_OVERDUE) { if (!$assignmentobj->is_any_submission_plugin_enabled()) { // No enabled submission plugins. Removing users. return []; } } // Get completion info for this module. $cm = $assignmentobj->get_course_module(); $course = $assignmentobj->get_course(); $completion = new \completion_info($course); foreach ($users as $key => $user) { // Check if the user has submitted already. $submission = $assignmentobj->get_user_submission($user->id, false); if ($submission && $submission->status === ASSIGN_SUBMISSION_STATUS_SUBMITTED) { unset($users[$key]); continue; } // Determine key dates with respect to any overrides. $duedate = $assignmentobj->override_exists($user->id)->duedate ?? $assignmentobj->get_instance()->duedate; $cutoffdate = $assignmentobj->override_exists($user->id)->cutoffdate ?? $assignmentobj->get_instance()->cutoffdate; // If the due date has no value, unset this user. if (empty($duedate)) { unset($users[$key]); continue; } // Don't send if the user has a grade for this assignment already. if ($assignmentobj->get_grading_status($user->id) === ASSIGN_GRADING_STATUS_GRADED) { unset($users[$key]); continue; } // Don't send if the user has met completion conditions. $completiondata = $completion->get_data($cm, false, $user->id); if ($completiondata && $completiondata->completionstate != COMPLETION_INCOMPLETE) { unset($users[$key]); continue; } // Perform some checks depending on the notification type. $match = []; $checksent = true; switch ($type) { case self::TYPE_DUE_SOON: $range = [ 'lower' => self::get_time_now(), 'upper' => self::get_future_time(self::INTERVAL_DUE_SOON), ]; if (!self::is_time_within_range($duedate, $range)) { unset($users[$key]); break; } $match = [ 'assignmentid' => $assignmentid, 'duedate' => $duedate, ]; break; case self::TYPE_OVERDUE: if ($duedate > self::get_time_now()) { unset($users[$key]); break; } // Check if the cut-off date is set and passed already. if (!empty($cutoffdate) && self::get_time_now() > $cutoffdate) { unset($users[$key]); break; } $match = [ 'assignmentid' => $assignmentid, 'duedate' => $duedate, 'cutoffdate' => $cutoffdate, ]; break; case self::TYPE_DUE_DIGEST: $checksent = false; $futuretime = self::get_future_time(self::INTERVAL_DUE_DIGEST); $day = self::get_day_start_and_end($futuretime); $range = [ 'lower' => $day['start'], 'upper' => $day['end'], ]; if (!self::is_time_within_range($duedate, $range)) { unset($users[$key]); break; } break; default: break; } // Check if the user has already received this notification. if ($checksent && self::has_user_been_sent_a_notification_already($user->id, json_encode($match), $type)) { unset($users[$key]); } } return $users; } /** * Send the due soon notification to the user. * * @param int $assignmentid The assignment id. * @param int $userid The user id. */ public static function send_due_soon_notification_to_user(int $assignmentid, int $userid): void { try { // Get assignment data. $assignmentobj = self::get_assignment_data($assignmentid); } catch (\dml_missing_record_exception) { // The assignment has vanished, nothing to do. mtrace("No notification send as the assignment $assignmentid can no longer be found in the database."); return; } // Check if the module is visible to the user. $cm = $assignmentobj->get_course_module(); if (!\core_availability\info_module::is_user_visible($cm, $userid)) { return; } // Check if the due date still within range. $assignmentobj->update_effective_access($userid); $duedate = $assignmentobj->get_instance($userid)->duedate; $range = [ 'lower' => self::get_time_now(), 'upper' => self::get_future_time(self::INTERVAL_DUE_SOON), ]; if (!self::is_time_within_range($duedate, $range)) { return; } // Check if the user has submitted already. $submission = $assignmentobj->get_user_submission($userid, false); if ($submission && $submission->status === ASSIGN_SUBMISSION_STATUS_SUBMITTED) { return; } // Build the user's notification message. $user = $assignmentobj->get_participant($userid); $urlparams = [ 'id' => $assignmentobj->get_course_module()->id, 'action' => 'view', ]; $url = new \moodle_url('/mod/assign/view.php', $urlparams); $stringparams = [ 'firstname' => $user->firstname, 'assignmentname' => format_string( $assignmentobj->get_instance()->name, options: ['context' => $assignmentobj->get_context()], ), 'coursename' => format_string( $assignmentobj->get_course()->fullname, options: ['context' => $assignmentobj->get_course_context()], ), 'duedate' => userdate($duedate), 'url' => $url, ]; $messagedata = [ 'user' => \core_user::get_user($user->id), 'url' => $url->out(false), 'subject' => get_string('assignmentduesoonsubject', 'mod_assign', $stringparams), 'assignmentname' => $stringparams['assignmentname'], 'sms' => get_string('assignmentduesoonsms', 'mod_assign', $stringparams), ]; // Offline assignments will have a slightly different message. $messagedata['html'] = !$assignmentobj->is_any_submission_plugin_enabled() ? get_string('assignmentduesoonofflinehtml', 'mod_assign', $stringparams) : get_string('assignmentduesoonhtml', 'mod_assign', $stringparams); $message = new \core\message\message(); $message->component = 'mod_assign'; $message->name = self::TYPE_DUE_SOON; $message->userfrom = \core_user::get_noreply_user(); $message->userto = $messagedata['user']; $message->subject = $messagedata['subject']; $message->fullmessageformat = FORMAT_HTML; $message->fullmessage = html_to_text($messagedata['html']); $message->fullmessagehtml = $messagedata['html']; $message->fullmessagesms = $messagedata['sms']; $message->smallmessage = $messagedata['subject']; $message->notification = 1; $message->contexturl = $messagedata['url']; $message->contexturlname = $messagedata['assignmentname']; // Use custom data to avoid future notifications being sent again. $message->customdata = [ 'assignmentid' => $assignmentid, 'duedate' => $duedate, ]; message_send($message); } /** * Send the overdue notification to the user. * * @param int $assignmentid The assignment id. * @param int $userid The user id. */ public static function send_overdue_notification_to_user(int $assignmentid, int $userid): void { try { // Get assignment data. $assignmentobj = self::get_assignment_data($assignmentid); } catch (\dml_missing_record_exception) { // The assignment has vanished, nothing to do. mtrace("No notification send as the assignment $assignmentid can no longer be found in the database."); return; } // Get the user and check they are a still a valid participant. $user = $assignmentobj->get_participant($userid); if (empty($user)) { return; } // Check if the module is visible to the user. $cm = $assignmentobj->get_course_module(); if (!\core_availability\info_module::is_user_visible($cm, $userid)) { return; } // Check if the due date still considered overdue. $assignmentobj->update_effective_access($userid); $duedate = $assignmentobj->get_instance($userid)->duedate; if ($duedate > self::get_time_now()) { return; } // Check if the cut-off date is set and passed already. $cutoffdate = $assignmentobj->get_instance($userid)->cutoffdate; if (!empty($cutoffdate) && self::get_time_now() > $cutoffdate) { return; } // Check if the user has submitted already. $submission = $assignmentobj->get_user_submission($userid, false); if ($submission && $submission->status === ASSIGN_SUBMISSION_STATUS_SUBMITTED) { return; } // Build the user's notification message. $urlparams = [ 'id' => $assignmentobj->get_course_module()->id, 'action' => 'view', ]; $url = new \moodle_url('/mod/assign/view.php', $urlparams); // Prepare the cut-off date html string. $snippet = ''; if (!empty($cutoffdate)) { $snippet = get_string('assignmentoverduehtmlcutoffsnippet', 'mod_assign', ['cutoffdate' => userdate($cutoffdate)]); } $stringparams = [ 'firstname' => $user->firstname, 'assignmentname' => format_string( $assignmentobj->get_instance()->name, options: ['context' => $assignmentobj->get_context()], ), 'coursename' => format_string( $assignmentobj->get_course()->fullname, options: ['context' => $assignmentobj->get_course_context()], ), 'duedate' => userdate($duedate), 'url' => $url, 'cutoffsnippet' => $snippet, ]; $messagedata = [ 'user' => \core_user::get_user($user->id), 'url' => $url->out(false), 'subject' => get_string('assignmentoverduesubject', 'mod_assign', $stringparams), 'assignmentname' => $stringparams['assignmentname'], 'html' => get_string('assignmentoverduehtml', 'mod_assign', $stringparams), 'sms' => get_string('assignmentoverduesms', 'mod_assign', $stringparams), ]; $message = new \core\message\message(); $message->component = 'mod_assign'; $message->name = self::TYPE_OVERDUE; $message->userfrom = \core_user::get_noreply_user(); $message->userto = $messagedata['user']; $message->subject = $messagedata['subject']; $message->fullmessageformat = FORMAT_HTML; $message->fullmessage = html_to_text($messagedata['html']); $message->fullmessagehtml = $messagedata['html']; $message->fullmessagesms = $messagedata['sms']; $message->smallmessage = $messagedata['subject']; $message->notification = 1; $message->contexturl = $messagedata['url']; $message->contexturlname = $messagedata['assignmentname']; // Use custom data to avoid future notifications being sent again. $message->customdata = [ 'assignmentid' => $assignmentid, 'duedate' => $duedate, 'cutoffdate' => $cutoffdate, ]; message_send($message); } /** * Get all the assignments and send the due digest notification to the user. * * @param int $userid The user id. */ public static function send_due_digest_notification_to_user(int $userid): void { // Get all the user's assignments due in 7 days. $assignments = self::get_due_digest_assignments_for_user($userid); $assignmentsfordigest = []; foreach ($assignments as $assignment) { $assignmentobj = self::get_assignment_data($assignment->id); // Check if the user has submitted already. $submission = $assignmentobj->get_user_submission($userid, false); if ($submission && $submission->status === ASSIGN_SUBMISSION_STATUS_SUBMITTED) { continue; } // Check if the module is visible to the user. $cm = $assignmentobj->get_course_module(); if (!\core_availability\info_module::is_user_visible($cm, $userid)) { continue; } // Check if the due date is still within range. $assignmentobj->update_effective_access($userid); $duedate = $assignmentobj->get_instance($userid)->duedate; $futuretime = self::get_future_time(self::INTERVAL_DUE_DIGEST); $day = self::get_day_start_and_end($futuretime); $range = [ 'lower' => $day['start'], 'upper' => $day['end'], ]; if (!self::is_time_within_range($duedate, $range)) { continue; } // Record the assignment data to help us build the digest. $urlparams = [ 'id' => $assignmentobj->get_course_module()->id, 'action' => 'view', ]; $assignmentsfordigest[$assignment->id] = [ 'assignmentname' => format_string( $assignmentobj->get_instance()->name, options: ['context' => $assignmentobj->get_context()], ), 'coursename' => format_string( $assignmentobj->get_course()->fullname, options: ['context' => $assignmentobj->get_course_context()], ), 'duetime' => userdate($duedate, get_string('strftimetime12', 'langconfig')), 'url' => new \moodle_url('/mod/assign/view.php', $urlparams), ]; } $assignments->close(); // If there are no assignments in the digest, don't send anything. if (empty($assignmentsfordigest)) { return; } // Build the digest. $digestarray = []; foreach ($assignmentsfordigest as $digestitem) { $digestarray[] = get_string('assignmentduedigestitem', 'mod_assign', $digestitem); } // Put the digest into list. $digest = html_writer::alist($digestarray); // Get user's object. $userobject = \core_user::get_user($userid); $stringparams = [ 'firstname' => $userobject->firstname, 'duedate' => userdate(self::get_future_time(self::INTERVAL_DUE_DIGEST), get_string('strftimedaydate', 'langconfig')), 'digest' => $digest, ]; $messagedata = [ 'user' => $userobject, 'subject' => get_string('assignmentduedigestsubject', 'mod_assign'), 'html' => get_string('assignmentduedigesthtml', 'mod_assign', $stringparams), 'sms' => get_string('assignmentduedigestsms', 'mod_assign', $stringparams), ]; $message = new \core\message\message(); $message->component = 'mod_assign'; $message->name = self::TYPE_DUE_DIGEST; $message->userfrom = \core_user::get_noreply_user(); $message->userto = $messagedata['user']; $message->subject = $messagedata['subject']; $message->fullmessageformat = FORMAT_HTML; $message->fullmessage = html_to_text($messagedata['html']); $message->fullmessagehtml = $messagedata['html']; $message->fullmessagesms = $messagedata['sms']; $message->smallmessage = $messagedata['subject']; $message->notification = 1; message_send($message); } /** * Get the time now. * * @return int The time now as a timestamp. */ protected static function get_time_now(): int { return \core\di::get(\core\clock::class)->time(); } /** * Get a future time. * * @param int $interval Amount of seconds added to the now time. * @return int The time now value plus the interval. */ protected static function get_future_time(int $interval): int { return self::get_time_now() + $interval; } /** * Get the timestamps for the start (00:00:00) and end (23:59:59) of the provided day. * * @param int $timestamp The timestamp to base the calculation on. * @return array Day start and end timestamps. */ protected static function get_day_start_and_end(int $timestamp): array { $day = []; $date = new DateTime(); $date->setTimestamp($timestamp); $date->setTime(0, 0, 0); $day['start'] = $date->getTimestamp(); $date->setTime(23, 59, 59); $day['end'] = $date->getTimestamp(); return $day; } /** * Check if a time is within the current time now and the future time values (inclusive). * * @param int $time The timestamp to check. * @param array $range Lower and upper times to check. * @return boolean */ protected static function is_time_within_range(int $time, array $range): bool { return ($time >= $range['lower'] && $time <= $range['upper']); } /** * Check if a user has been sent a notification already. * * @param int $userid The user id. * @param string $match The custom data string to match on. * @param string $type The notification/event type to match. * @return bool Returns true if already sent. */ protected static function has_user_been_sent_a_notification_already(int $userid, string $match, string $type): bool { global $DB; $sql = $DB->sql_compare_text('customdata', 255) . " = " . $DB->sql_compare_text(':match', 255) . " AND useridto = :userid AND component = :component AND eventtype = :eventtype"; return $DB->record_exists_select('notifications', $sql, [ 'userid' => $userid, 'match' => $match, 'component' => 'mod_assign', 'eventtype' => $type, ]); } /** * Get the assignment object, including the course and course module. * * @param int $assignmentid The assignment id. * @return \assign Returns the assign object. */ protected static function get_assignment_data(int $assignmentid): \assign { [$course, $assigncm] = get_course_and_cm_from_instance($assignmentid, 'assign'); $cmcontext = \context_module::instance($assigncm->id); return new \assign($cmcontext, $assigncm, $course); } } classes/plugininfo/assignsubmission.php000064400000011736152265517350014507 0ustar00. namespace mod_assign\plugininfo; use core\plugininfo\base; use core_plugin_manager; use moodle_url; /** * Assign submission subplugin info class. * * @package mod_assign * @copyright 2013 Petr Skoda {@link http://skodak.org} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class assignsubmission extends base { public static function plugintype_supports_disabling(): bool { return true; } /** * Finds all enabled plugins, the result may include missing plugins. * @return array|null of enabled plugins $pluginname=>$pluginname, null means unknown */ public static function get_enabled_plugins() { global $DB; $plugins = core_plugin_manager::instance()->get_installed_plugins('assignsubmission'); if (!$plugins) { return array(); } $installed = array(); foreach ($plugins as $plugin => $version) { $installed[] = 'assignsubmission_'.$plugin; } list($installed, $params) = $DB->get_in_or_equal($installed, SQL_PARAMS_NAMED); $disabled = $DB->get_records_select('config_plugins', "plugin $installed AND name = 'disabled'", $params, 'plugin ASC'); foreach ($disabled as $conf) { if (empty($conf->value)) { continue; } list($type, $name) = explode('_', $conf->plugin, 2); unset($plugins[$name]); } $enabled = array(); foreach ($plugins as $plugin => $version) { $enabled[$plugin] = $plugin; } return $enabled; } public static function enable_plugin(string $pluginname, int $enabled): bool { $haschanged = false; $plugin = 'assignsubmission_' . $pluginname; $oldvalue = get_config($plugin, 'disabled'); $disabled = !$enabled; // Only set value if there is no config setting or if the value is different from the previous one. if ($oldvalue === false || ((bool) $oldvalue != $disabled)) { set_config('disabled', $disabled, $plugin); $haschanged = true; add_to_config_log('disabled', $oldvalue, $disabled, $plugin); \core_plugin_manager::reset_caches(); } return $haschanged; } public function is_uninstall_allowed() { return true; } /** * Return URL used for management of plugins of this type. * @return moodle_url */ public static function get_manage_url() { return new moodle_url('/mod/assign/adminmanageplugins.php', array('subtype'=>'assignsubmission')); } /** * Pre-uninstall hook. * @private */ public function uninstall_cleanup() { global $DB; $DB->delete_records('assign_plugin_config', array('plugin'=>$this->name, 'subtype'=>'assignsubmission')); parent::uninstall_cleanup(); } public function get_settings_section_name() { return $this->type . '_' . $this->name; } /** * Loads plugin settings to the settings tree * * This function usually includes settings.php file in plugins folder. * Alternatively it can create a link to some settings page (instance of admin_externalpage) * * @param \part_of_admin_tree $adminroot * @param string $parentnodename * @param bool $hassiteconfig whether the current user has moodle/site:config capability */ public function load_settings(\part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) { global $CFG, $USER, $DB, $OUTPUT, $PAGE; // In case settings.php wants to refer to them. $ADMIN = $adminroot; // May be used in settings.php. $plugininfo = $this; // Also can be used inside settings.php. if (!$this->is_installed_and_upgraded()) { return; } if (!$hassiteconfig or !file_exists($this->full_path('settings.php'))) { return; } $section = $this->get_settings_section_name(); $settings = new \admin_settingpage($section, $this->displayname, 'moodle/site:config', $this->is_enabled() === false); if ($adminroot->fulltree) { $shortsubtype = substr($this->type, strlen('assign')); include($this->full_path('settings.php')); } $adminroot->add($this->type . 'plugins', $settings); } } classes/plugininfo/assignfeedback.php000064400000011720152265517350014031 0ustar00. namespace mod_assign\plugininfo; use core\plugininfo\base; use core_plugin_manager; use moodle_url; /** * Assign feedback subplugin info class. * * @package mod_assign * @copyright 2013 Petr Skoda {@link http://skodak.org} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class assignfeedback extends base { public static function plugintype_supports_disabling(): bool { return true; } /** * Finds all enabled plugins, the result may include missing plugins. * @return array|null of enabled plugins $pluginname=>$pluginname, null means unknown */ public static function get_enabled_plugins() { global $DB; $plugins = core_plugin_manager::instance()->get_installed_plugins('assignfeedback'); if (!$plugins) { return array(); } $installed = array(); foreach ($plugins as $plugin => $version) { $installed[] = 'assignfeedback_'.$plugin; } list($installed, $params) = $DB->get_in_or_equal($installed, SQL_PARAMS_NAMED); $disabled = $DB->get_records_select('config_plugins', "plugin $installed AND name = 'disabled'", $params, 'plugin ASC'); foreach ($disabled as $conf) { if (empty($conf->value)) { continue; } list($type, $name) = explode('_', $conf->plugin, 2); unset($plugins[$name]); } $enabled = array(); foreach ($plugins as $plugin => $version) { $enabled[$plugin] = $plugin; } return $enabled; } public static function enable_plugin(string $pluginname, int $enabled): bool { $haschanged = false; $plugin = 'assignfeedback_' . $pluginname; $oldvalue = get_config($plugin, 'disabled'); $disabled = !$enabled; // Only set value if there is no config setting or if the value is different from the previous one. if ($oldvalue === false || ((bool) $oldvalue != $disabled)) { set_config('disabled', $disabled, $plugin); $haschanged = true; add_to_config_log('disabled', $oldvalue, $disabled, $plugin); \core_plugin_manager::reset_caches(); } return $haschanged; } public function is_uninstall_allowed() { return true; } /** * Return URL used for management of plugins of this type. * @return moodle_url */ public static function get_manage_url() { return new moodle_url('/mod/assign/adminmanageplugins.php', array('subtype'=>'assignfeedback')); } /** * Pre-uninstall hook. * @private */ public function uninstall_cleanup() { global $DB; $DB->delete_records('assign_plugin_config', array('plugin'=>$this->name, 'subtype'=>'assignfeedback')); parent::uninstall_cleanup(); } public function get_settings_section_name() { return $this->type . '_' . $this->name; } /** * Loads plugin settings to the settings tree * * This function usually includes settings.php file in plugins folder. * Alternatively it can create a link to some settings page (instance of admin_externalpage) * * @param \part_of_admin_tree $adminroot * @param string $parentnodename * @param bool $hassiteconfig whether the current user has moodle/site:config capability */ public function load_settings(\part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) { global $CFG, $USER, $DB, $OUTPUT, $PAGE; // In case settings.php wants to refer to them. $ADMIN = $adminroot; // May be used in settings.php. $plugininfo = $this; // Also can be used inside settings.php. if (!$this->is_installed_and_upgraded()) { return; } if (!$hassiteconfig or !file_exists($this->full_path('settings.php'))) { return; } $section = $this->get_settings_section_name(); $settings = new \admin_settingpage($section, $this->displayname, 'moodle/site:config', $this->is_enabled() === false); if ($adminroot->fulltree) { $shortsubtype = substr($this->type, strlen('assign')); include($this->full_path('settings.php')); } $adminroot->add($this->type . 'plugins', $settings); } } classes/downloader.php000064400000024212152265517350011064 0ustar00. namespace mod_assign; use assign; use core_php_time_limit; use mod_assign\event\all_submissions_downloaded; use core\session\manager as sessionmanager; use core_files\archive_writer; use stdClass; use assign_plugin; use stored_file; /** * Class to download user submissions. * * @package mod_assign * @copyright 2022 Ferran Recio * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class downloader { /** @var assign the module manager instance. */ private $manager; /** @var stdClass the assign instance record. */ private $instance; /** @var array|null the selected user ids, if any. */ private $userids = null; /** @var int $groupmode the activity group mode. */ private $groupmode = ''; /** @var int $groupid the exported groupid. */ private $groupid = 0; /** @var array $filesforzipping the files to zipo (path => file) */ protected $filesforzipping; /** @var array $prefixes all loaded the student prefixes. * * A prefix will be converted into a file prefix or a folder name (depends on downloadasfolders). */ private $prefixes; /** @var int $downloadasfolders the files to zipo (path => file) */ private $downloadasfolders; /** * Class constructor. * * @param assign $manager the instance manager * @param int[]|null $userids the user ids to download. */ public function __construct(assign $manager, ?array $userids = null) { $this->manager = $manager; $this->userids = $userids; $this->instance = $manager->get_instance(); $this->downloadasfolders = get_user_preferences('assign_downloadasfolders', 1); $cm = $manager->get_course_module(); $this->groupmode = groups_get_activity_groupmode($cm); if ($this->groupmode) { $this->groupid = groups_get_activity_group($cm, true); } } /** * Load the filelist. * * @return bool true if there are some files to zip. */ public function load_filelist(): bool { $manager = $this->manager; $groupid = $this->groupid; // Increase the server timeout to handle the creation and sending of large zip files. core_php_time_limit::raise(); $manager->require_view_grades(); $students = get_enrolled_users( $manager->get_context(), '', 0, 'u.*', null, 0, 0, $manager->show_only_active_users() ); // Build a list of files to zip. $this->filesforzipping = []; // Get all the files for each student. foreach ($students as $student) { // Download all assigments submission or only selected users. if ($this->userids && !in_array($student->id, $this->userids)) { continue; } if (!groups_is_member($groupid, $student->id) && $this->groupmode && $groupid) { continue; } $this->load_student_filelist($student); } return !empty($this->filesforzipping); } /** * Load an individual student filelist. * * @param stdClass $student the user record */ private function load_student_filelist(stdClass $student) { $submission = $this->get_student_submission($student); if (!$submission) { return; } $prefix = $this->get_student_prefix($student); if (isset($this->prefixes[$prefix])) { // We already send that file (in group mode). return; } $this->prefixes[$prefix] = $student->id; foreach ($this->manager->get_submission_plugins() as $plugin) { if (!$plugin->is_enabled() || !$plugin->is_visible()) { continue; } $this->load_submissionplugin_filelist($student, $plugin, $submission, $prefix); } } /** * Return the student submission if any. * * @param stdClass $student the user record * @return stdClass|null the user submission or null if none */ private function get_student_submission(stdClass $student): ?stdClass { if ($this->instance->teamsubmission) { $submission = $this->manager->get_group_submission($student->id, 0, false); } else { $submission = $this->manager->get_user_submission($student->id, false); } return $submission ?: null; } /** * Return the file prefix used to generate the each submission folder or file. * * @param stdClass $student the user record * @return string the submission prefix */ private function get_student_prefix(stdClass $student): string { $manager = $this->manager; // Team submissions are by group, not by student. if ($this->instance->teamsubmission) { $submissiongroup = $manager->get_submission_group($student->id); if ($submissiongroup) { $groupname = format_string($submissiongroup->name, true, ['context' => $manager->get_context()]); $groupinfo = '_' . $submissiongroup->id; } else { $groupname = get_string('defaultteam', 'mod_assign'); $groupinfo = ''; } $prefix = str_replace('_', ' ', $groupname); return clean_filename($prefix . $groupinfo); } // Individual submissions are by user. if ($manager->is_blind_marking()) { $fullname = get_string('participant', 'mod_assign'); } else { $fullname = fullname($student, has_capability('moodle/site:viewfullnames', $manager->get_context())); } $prefix = str_replace('_', ' ', $fullname); $prefix = clean_filename($prefix . '_' . $manager->get_uniqueid_for_user($student->id)); return $prefix; } /** * Load a submission plugin filelist for a specific user. * * @param stdClass $student the user record * @param assign_plugin $plugin the submission plugin instance * @param stdClass $submission the submission object * @param string $prefix the files prefix */ private function load_submissionplugin_filelist( stdClass $student, assign_plugin $plugin, stdClass $submission, string $prefix ) { $subtype = $plugin->get_subtype(); $type = $plugin->get_type(); if ($this->downloadasfolders) { // Create a folder for each user for each assignment plugin. // This is the default behavior for version of Moodle >= 3.1. $submission->exportfullpath = true; $pluginfiles = $plugin->get_files($submission, $student); foreach ($pluginfiles as $zipfilepath => $file) { $zipfilename = basename($zipfilepath); $prefixedfilename = clean_filename($prefix . '_' . $subtype . '_' . $type); if ($type == 'file') { $pathfilename = $prefixedfilename . $file->get_filepath() . $zipfilename; } else { $pathfilename = $prefixedfilename . '/' . $zipfilename; } $pathfilename = clean_param($pathfilename, PARAM_PATH); $this->filesforzipping[$pathfilename] = $file; } } else { // Create a single folder for all users of all assignment plugins. // This was the default behavior for version of Moodle < 3.1. $submission->exportfullpath = false; $pluginfiles = $plugin->get_files($submission, $student); foreach ($pluginfiles as $zipfilename => $file) { $prefixedfilename = clean_filename($prefix . '_' . $subtype . '_' . $type . '_' . $zipfilename); $this->filesforzipping[$prefixedfilename] = $file; } } } /** * Download the exported zip. * * This method will terminate the current script when the file is send. */ public function download_zip() { $filename = $this->get_zip_filename(); all_submissions_downloaded::create_from_assign($this->manager)->trigger(); sessionmanager::write_close(); $zipwriter = archive_writer::get_stream_writer($filename, archive_writer::ZIP_WRITER); // Stream the files into the zip. foreach ($this->filesforzipping as $pathinzip => $file) { if ($file instanceof stored_file) { // Most of cases are stored_file. $zipwriter->add_file_from_stored_file($pathinzip, $file); } else if (is_array($file)) { // Save $file as contents, from onlinetext subplugin. $content = reset($file); $zipwriter->add_file_from_string($pathinzip, $content); } } // Finish the archive. $zipwriter->finish(); exit(); } /** * Generate the zip filename. * * @return string the zip filename */ private function get_zip_filename(): string { $manager = $this->manager; $filenameparts = [ $manager->get_course()->shortname, $this->instance->name, ]; if (!empty($this->groupid)) { $filenameparts[] = format_string(groups_get_group_name($this->groupid), true, ['context' => $manager->get_context()]); } $filenameparts[] = $manager->get_course_module()->id; return clean_filename(implode('-', $filenameparts). '.zip'); } } classes/search/activity.php000064400000003355152265517350012034 0ustar00. /** * Search area for mod_assign activities. * * @package mod_assign * @copyright 2015 David Monllao {@link http://www.davidmonllao.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_assign\search; defined('MOODLE_INTERNAL') || die(); require_once($CFG->dirroot . '/mod/assign/locallib.php'); /** * Search area for mod_assign activities. * * @package mod_assign * @copyright 2015 David Monllao {@link http://www.davidmonllao.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class activity extends \core_search\base_activity { /** * Returns true if this area uses file indexing. * * @return bool */ public function uses_file_indexing() { return true; } /** * Return the context info required to index files for * this search area. * * @return array */ public function get_search_fileareas() { $fileareas = array('intro', ASSIGN_INTROATTACHMENT_FILEAREA); // Fileareas. return $fileareas; } } classes/output/override_actionmenu.php000064400000013555152265517350014337 0ustar00. /** * Output the override actionbar for this activity. * * @package mod_assign * @copyright 2021 Adrian Greeve * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_assign\output; use core_availability\info_module; use moodle_url; use templatable; use renderable; use url_select; use single_button; /** * Output the override actionbar for this activity. * * @package mod_assign * @copyright 2021 Adrian Greeve * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class override_actionmenu implements templatable, renderable { /** @var moodle_url The current url for this page. */ protected $currenturl; /** @var \cm_info course module information */ protected $cm; /** @var bool Can all groups be accessed */ protected $canaccessallgroups; /** @var array Groups related to this activity */ protected $groups; /** * Constructor for this action menu. * * @param moodle_url $currenturl The current url for this page. * @param \cm_info $cm course module information. */ public function __construct(moodle_url $currenturl, \cm_info $cm) { $this->currenturl = $currenturl; $this->cm = $cm; $groupmode = groups_get_activity_groupmode($this->cm); $this->canaccessallgroups = ($groupmode === NOGROUPS) || has_capability('moodle/site:accessallgroups', $this->cm->context); $this->groups = $this->canaccessallgroups ? groups_get_all_groups($this->cm->course) : groups_get_activity_allowed_groups($this->cm); } /** * Create a select menu for overrides. * * @return url_select A url select object. */ protected function get_select_menu(): url_select { $userlink = new moodle_url('/mod/assign/overrides.php', ['cmid' => $this->cm->id, 'mode' => 'user']); $grouplink = new moodle_url('/mod/assign/overrides.php', ['cmid' => $this->cm->id, 'mode' => 'group']); $menu = [ $userlink->out(false) => get_string('useroverrides', 'mod_assign'), $grouplink->out(false) => get_string('groupoverrides', 'mod_assign'), ]; return new url_select($menu, $this->currenturl->out(false), null, 'mod_assign_override_select'); } /** * Whether to show groups or not. Assignments can be have group overrides if there are groups available in the course. * There is no restriction related to the assignment group setting. * * @return bool */ protected function show_groups(): bool { if ($this->canaccessallgroups) { $groups = groups_get_all_groups($this->cm->course); } else { $groups = groups_get_activity_allowed_groups($this->cm); } return !(empty($groups)); } /** * Whether to enable/disable user override button or not. * * @return bool */ protected function show_useroverride(): bool { global $DB; $users = []; $context = $this->cm->context; if ($this->canaccessallgroups) { $users = get_enrolled_users($context, '', 0, 'u.id'); } else if ($this->groups) { $enrolledjoin = get_enrolled_join($context, 'u.id'); list($ingroupsql, $ingroupparams) = $DB->get_in_or_equal(array_keys($this->groups), SQL_PARAMS_NAMED); $params = $enrolledjoin->params + $ingroupparams; $sql = "SELECT u.id FROM {user} u JOIN {groups_members} gm ON gm.userid = u.id {$enrolledjoin->joins} WHERE gm.groupid $ingroupsql AND {$enrolledjoin->wheres}"; $users = $DB->get_records_sql($sql, $params); } $info = new info_module($this->cm); $users = $info->filter_user_list($users); return !empty($users); } /** * Data to be used in a template. * * @param \renderer_base $output renderer base output. * @return array The data to be used in a template. */ public function export_for_template(\renderer_base $output): array { $type = $this->currenturl->get_param('mode'); if ($type == 'user') { $text = get_string('addnewuseroverride', 'mod_assign'); } else { $text = get_string('addnewgroupoverride', 'mod_assign'); } $action = ($type == 'user') ? 'adduser' : 'addgroup'; $params = ['cmid' => $this->currenturl->get_param('cmid'), 'action' => $action]; $url = new moodle_url('/mod/assign/overrideedit.php', $params); $options = []; if ($action == 'addgroup' && !$this->show_groups()) { $options = ['disabled' => 'true']; } else if ($action === 'adduser' && !$this->show_useroverride()) { $options = ['disabled' => 'true']; } $overridebutton = new single_button($url, $text, 'post', single_button::BUTTON_PRIMARY, $options); $urlselect = $this->get_select_menu(); return [ 'addoverride' => $overridebutton->export_for_template($output), 'urlselect' => $urlselect->export_for_template($output) ]; } } classes/output/renderer.php000064400000204235152265517350012101 0ustar00. /** * This file contains a renderer for the assignment class * * @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 */ namespace mod_assign\output; use assign_files; use html_writer; use mod_assign\output\grading_app; use portfolio_add_button; use stored_file; defined('MOODLE_INTERNAL') || die(); require_once($CFG->dirroot . '/mod/assign/locallib.php'); /** * A custom renderer class that extends the plugin_renderer_base and is used by the 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 */ class renderer extends \plugin_renderer_base { /** @var string a unique ID. */ public $htmlid; /** * Rendering assignment files * * @param \context $context * @param int $userid * @param string $filearea * @param string $component * @param \stdClass $course * @param \stdClass $coursemodule * @return string */ public function assign_files(\context $context, $userid, $filearea, $component, $course = null, $coursemodule = null) { return $this->render(new \assign_files($context, $userid, $filearea, $component, $course, $coursemodule)); } /** * Rendering assignment files * * @param \assign_files $tree * @return string */ public function render_assign_files(\assign_files $tree) { $this->htmlid = \html_writer::random_id('assign_files_tree'); $this->page->requires->js_init_call('M.mod_assign.init_tree', array(true, $this->htmlid)); $html = '
'; $html .= $this->htmllize_tree($tree, $tree->dir); $html .= '
'; if ($tree->portfolioform) { $html .= $tree->portfolioform; } return $html; } /** * Utility function to add a row of data to a table with 2 columns where the first column is the table's header. * Modified the table param and does not return a value. * * @param \html_table $table The table to append the row of data to * @param string $first The first column text * @param string $second The second column text * @param array $firstattributes The first column attributes (optional) * @param array $secondattributes The second column attributes (optional) * @return void */ private function add_table_row_tuple(\html_table $table, $first, $second, $firstattributes = [], $secondattributes = []) { $row = new \html_table_row(); $cell1 = new \html_table_cell($first); $cell1->header = true; if (!empty($firstattributes)) { $cell1->attributes = $firstattributes; } $cell2 = new \html_table_cell($second); if (!empty($secondattributes)) { $cell2->attributes = $secondattributes; } $row->cells = array($cell1, $cell2); $table->data[] = $row; } /** * Render a grading message notification * @param \assign_gradingmessage $result The result to render * @return string */ public function render_assign_gradingmessage(\assign_gradingmessage $result) { $urlparams = array('id' => $result->coursemoduleid, 'action'=>'grading'); if (!empty($result->page)) { $urlparams['page'] = $result->page; } $url = new \moodle_url('/mod/assign/view.php', $urlparams); $classes = $result->gradingerror ? 'notifyproblem' : 'notifysuccess'; $o = ''; $o .= $this->output->heading($result->heading, 4); $o .= $this->output->notification($result->message, $classes); $o .= $this->output->continue_button($url); return $o; } /** * Render the generic form * @param \assign_form $form The form to render * @return string */ public function render_assign_form(\assign_form $form) { $o = ''; if ($form->jsinitfunction) { $this->page->requires->js_init_call($form->jsinitfunction, array()); } $o .= $this->output->box_start('boxaligncenter ' . $form->classname); $o .= $this->moodleform($form->form); $o .= $this->output->box_end(); return $o; } /** * Render the user summary * * @param \assign_user_summary $summary The user summary to render * @return string */ public function render_assign_user_summary(\assign_user_summary $summary) { $o = ''; $supendedclass = ''; $suspendedicon = ''; if (!$summary->user) { return; } if ($summary->suspendeduser) { $supendedclass = ' usersuspended'; $suspendedstring = get_string('userenrolmentsuspended', 'grades'); $suspendedicon = ' ' . $this->pix_icon('i/enrolmentsuspended', $suspendedstring); } $o .= $this->output->container_start('usersummary'); $o .= $this->output->box_start('boxaligncenter usersummarysection'.$supendedclass); if ($summary->blindmarking) { $o .= get_string('hiddenuser', 'assign') . $summary->uniqueidforuser.$suspendedicon; } else { $o .= $this->output->user_picture($summary->user); $o .= $this->output->spacer(array('width'=>30)); $urlparams = array('id' => $summary->user->id, 'course'=>$summary->courseid); $url = new \moodle_url('/user/view.php', $urlparams); $fullname = fullname($summary->user, $summary->viewfullnames); $extrainfo = array(); foreach ($summary->extrauserfields as $extrafield) { $extrainfo[] = s($summary->user->$extrafield); } if (count($extrainfo)) { $fullname .= ' (' . implode(', ', $extrainfo) . ')'; } $fullname .= $suspendedicon; $o .= $this->output->action_link($url, $fullname); } $o .= $this->output->box_end(); $o .= $this->output->container_end(); return $o; } /** * Render the submit for grading page * * @param \assign_submit_for_grading_page $page * @return string */ public function render_assign_submit_for_grading_page($page) { $o = ''; $o .= $this->output->container_start('submitforgrading'); $o .= $this->output->heading(get_string('confirmsubmissionheading', 'assign'), 3); $cancelurl = new \moodle_url('/mod/assign/view.php', array('id' => $page->coursemoduleid)); if (count($page->notifications)) { // At least one of the submission plugins is not ready for submission. $o .= $this->output->heading(get_string('submissionnotready', 'assign'), 4); foreach ($page->notifications as $notification) { $o .= $this->output->notification($notification); } $o .= $this->output->continue_button($cancelurl); } else { // All submission plugins ready - show the confirmation form. $o .= $this->moodleform($page->confirmform); } $o .= $this->output->container_end(); return $o; } /** * Page is done - render the footer. * * @return void */ public function render_footer() { return $this->output->footer(); } /** * Render the header. * * @param assign_header $header * @return string */ public function render_assign_header(assign_header $header) { if ($header->subpage) { $this->page->navbar->add($header->subpage, $header->subpageurl); $args = ['contextname' => $header->context->get_context_name(false, true), 'subpage' => $header->subpage]; $title = get_string('subpagetitle', 'assign', $args); } else { $title = $header->context->get_context_name(false, true); } $courseshortname = $header->context->get_course_context()->get_context_name(false, true); $title = $courseshortname . ': ' . $title; $heading = format_string($header->assign->name, false, array('context' => $header->context)); $this->page->set_title($title); $this->page->set_heading($this->page->course->fullname); $description = $header->preface; if ($header->showintro || $header->activity) { $description = $this->output->box_start('generalbox boxaligncenter'); if ($header->showintro) { $description .= format_module_intro('assign', $header->assign, $header->coursemoduleid); } if ($header->activity) { $description .= $this->format_activity_text($header->assign, $header->coursemoduleid); } $description .= $header->postfix; $description .= $this->output->box_end(); } $activityheader = $this->page->activityheader; $activityheader->set_attrs([ 'title' => $activityheader->is_title_allowed() ? $heading : '', 'description' => $description ]); return $this->output->header(); } /** * Render the header for an individual plugin. * * @param \assign_plugin_header $header * @return string */ public function render_assign_plugin_header(\assign_plugin_header $header) { $o = $header->plugin->view_header(); return $o; } /** * Render a table containing the current status of the grading process. * * @param \assign_grading_summary $summary * @return string */ public function render_assign_grading_summary(\assign_grading_summary $summary) { // Create a table for the data. $o = ''; $o .= $this->output->container_start('gradingsummary'); $o .= $this->output->heading(get_string('gradingsummary', 'assign'), 3); if (isset($summary->cm)) { $currenturl = new \moodle_url('/mod/assign/view.php', array('id' => $summary->cm->id)); $o .= groups_print_activity_menu($summary->cm, $currenturl->out(), true, participationonly: false); } $o .= $this->output->box_start('boxaligncenter gradingsummarytable'); $t = new \html_table(); $t->attributes['class'] = 'generaltable table table-striped table-bordered table-hover'; $t->caption = get_string('gradingsummary', 'assign'); $t->captionhide = true; // Hidden because it matches the title above. // Visibility Status. $cell1content = get_string('hiddenfromstudents'); $cell2content = (!$summary->isvisible) ? get_string('yes') : get_string('no'); $this->add_table_row_tuple($t, $cell1content, $cell2content); // Status. if ($summary->teamsubmission) { if ($summary->warnofungroupedusers === \assign_grading_summary::WARN_GROUPS_REQUIRED) { $o .= $this->output->notification(get_string('ungroupedusers', 'assign')); } else if ($summary->warnofungroupedusers === \assign_grading_summary::WARN_GROUPS_OPTIONAL) { $o .= $this->output->notification(get_string('ungroupedusersoptional', 'assign')); } $cell1content = get_string('numberofteams', 'assign'); } else { $cell1content = get_string('numberofparticipants', 'assign'); } $cell2content = $summary->participantcount; $this->add_table_row_tuple($t, $cell1content, $cell2content); // Drafts count and dont show drafts count when using offline assignment. if ($summary->submissiondraftsenabled && $summary->submissionsenabled) { $cell1content = get_string('numberofdraftsubmissions', 'assign'); $cell2content = $summary->submissiondraftscount; $this->add_table_row_tuple($t, $cell1content, $cell2content); } // Submitted for grading. if ($summary->submissionsenabled) { $cell1content = get_string('numberofsubmittedassignments', 'assign'); $cell2content = $summary->submissionssubmittedcount; $this->add_table_row_tuple($t, $cell1content, $cell2content); if (!$summary->teamsubmission) { $cell1content = get_string('numberofsubmissionsneedgrading', 'assign'); $cell2content = $summary->submissionsneedgradingcount; $this->add_table_row_tuple($t, $cell1content, $cell2content); } } $time = time(); if ($summary->duedate) { // Time remaining. $duedate = $summary->duedate; $cell1content = get_string('timeremaining', 'assign'); if ($summary->courserelativedatesmode) { $cell2content = get_string('relativedatessubmissiontimeleft', 'mod_assign'); } else { if ($duedate - $time <= 0) { $cell2content = get_string('assignmentisdue', 'assign'); } else { $cell2content = format_time($duedate - $time); } } $this->add_table_row_tuple($t, $cell1content, $cell2content); if ($duedate < $time) { $cell1content = get_string('latesubmissions', 'assign'); $cutoffdate = $summary->cutoffdate; if ($cutoffdate) { if ($cutoffdate > $time) { $cell2content = get_string('latesubmissionsaccepted', 'assign', userdate($summary->cutoffdate)); } else { $cell2content = get_string('nomoresubmissionsaccepted', 'assign'); } $this->add_table_row_tuple($t, $cell1content, $cell2content); } } } // Add time limit info if there is one. $timelimitenabled = get_config('assign', 'enabletimelimit'); if ($timelimitenabled && $summary->timelimit > 0) { $cell1content = get_string('timelimit', 'assign'); $cell2content = format_time($summary->timelimit); $this->add_table_row_tuple($t, $cell1content, $cell2content, [], []); } // All done - write the table. $o .= \html_writer::table($t); $o .= $this->output->box_end(); // Close the container and insert a spacer. $o .= $this->output->container_end(); $o .= \html_writer::end_tag('center'); return $o; } /** * Render a table containing all the current grades and feedback. * * @param \assign_feedback_status $status * @return string */ public function render_assign_feedback_status(\assign_feedback_status $status) { $o = ''; $o .= $this->output->container_start('feedback'); $o .= $this->output->heading(get_string('feedback', 'assign'), 3); $o .= $this->output->box_start('boxaligncenter feedbacktable'); $t = new \html_table(); $t->caption = get_string('feedback', 'assign'); $t->captionhide = true; // Hidden because it matches the title above. // Grade. if (isset($status->gradefordisplay)) { $cell1content = get_string('gradenoun'); $cell2content = $status->gradefordisplay; $this->add_table_row_tuple($t, $cell1content, $cell2content); // Grade date. $cell1content = get_string('gradedon', 'assign'); $cell2content = userdate($status->gradeddate); $this->add_table_row_tuple($t, $cell1content, $cell2content); } if ($status->grader) { // Grader. $cell1content = get_string('gradedby', 'assign'); $cell2content = $this->output->user_picture($status->grader) . $this->output->spacer(array('width' => 30)) . fullname($status->grader, $status->canviewfullnames); $this->add_table_row_tuple($t, $cell1content, $cell2content); } foreach ($status->feedbackplugins as $plugin) { if ($plugin->is_enabled() && $plugin->is_visible() && $plugin->has_user_summary() && !empty($status->grade) && !$plugin->is_empty($status->grade)) { $displaymode = \assign_feedback_plugin_feedback::SUMMARY; $pluginfeedback = new \assign_feedback_plugin_feedback($plugin, $status->grade, $displaymode, $status->coursemoduleid, $status->returnaction, $status->returnparams); $cell1content = $plugin->get_name(); $cell2content = $this->render($pluginfeedback); $this->add_table_row_tuple($t, $cell1content, $cell2content); } } $o .= \html_writer::table($t); $o .= $this->output->box_end(); if (!empty($status->gradingcontrollergrade)) { $o .= $this->output->heading(get_string('gradebreakdown', 'assign'), 4); $o .= $status->gradingcontrollergrade; } $o .= $this->output->container_end(); return $o; } /** * Render a compact view of the current status of the submission. * * @param \assign_submission_status_compact $status * @return string */ public function render_assign_submission_status_compact(\assign_submission_status_compact $status) { $o = ''; $o .= $this->output->container_start('submissionstatustable'); $o .= $this->output->heading(get_string('submission', 'assign'), 3); if ($status->teamsubmissionenabled) { $group = $status->submissiongroup; if ($group) { $team = format_string($group->name, false, ['context' => $status->context]); } else if ($status->preventsubmissionnotingroup) { if (count($status->usergroups) == 0) { $team = '' . get_string('noteam', 'assign') . ''; } else if (count($status->usergroups) > 1) { $team = '' . get_string('multipleteams', 'assign') . ''; } } else { $team = get_string('defaultteam', 'assign'); } $o .= $this->output->container(get_string('teamname', 'assign', $team), 'teamname'); } if (!$status->teamsubmissionenabled) { if ($status->submission && $status->submission->status != ASSIGN_SUBMISSION_STATUS_NEW) { $statusstr = get_string('submissionstatus_' . $status->submission->status, 'assign'); $o .= $this->output->container($statusstr, 'submissionstatus' . $status->submission->status); } else { if (!$status->submissionsenabled) { $o .= $this->output->container(get_string('noonlinesubmissions', 'assign'), 'submissionstatus'); } else { $o .= $this->output->container(get_string('noattempt', 'assign'), 'submissionstatus'); } } } else { $group = $status->submissiongroup; if (!$group && $status->preventsubmissionnotingroup) { $o .= $this->output->container(get_string('nosubmission', 'assign'), 'submissionstatus'); } else if ($status->teamsubmission && $status->teamsubmission->status != ASSIGN_SUBMISSION_STATUS_NEW) { $teamstatus = $status->teamsubmission->status; $submissionsummary = get_string('submissionstatus_' . $teamstatus, 'assign'); $groupid = 0; if ($status->submissiongroup) { $groupid = $status->submissiongroup->id; } $members = $status->submissiongroupmemberswhoneedtosubmit; $userslist = array(); foreach ($members as $member) { $urlparams = array('id' => $member->id, 'course' => $status->courseid); $url = new \moodle_url('/user/view.php', $urlparams); if ($status->view == assign_submission_status::GRADER_VIEW && $status->blindmarking) { $userslist[] = $member->alias; } else { $fullname = fullname($member, $status->canviewfullnames); $userslist[] = $this->output->action_link($url, $fullname); } } if (count($userslist) > 0) { $userstr = join(', ', $userslist); $formatteduserstr = get_string('userswhoneedtosubmit', 'assign', $userstr); $submissionsummary .= $this->output->container($formatteduserstr); } $o .= $this->output->container($submissionsummary, 'submissionstatus' . $status->teamsubmission->status); } else { if (!$status->submissionsenabled) { $o .= $this->output->container(get_string('noonlinesubmissions', 'assign'), 'submissionstatus'); } else { $o .= $this->output->container(get_string('nosubmission', 'assign'), 'submissionstatus'); } } } // Is locked? if ($status->locked) { $o .= $this->output->container(get_string('submissionslocked', 'assign'), 'submissionlocked'); } // Grading status. $statusstr = ''; $classname = 'gradingstatus'; if ($status->gradingstatus == ASSIGN_GRADING_STATUS_GRADED || $status->gradingstatus == ASSIGN_GRADING_STATUS_NOT_GRADED) { $statusstr = get_string($status->gradingstatus, 'assign'); } else { $gradingstatus = 'markingworkflowstate' . $status->gradingstatus; $statusstr = get_string($gradingstatus, 'assign'); } if ($status->gradingstatus == ASSIGN_GRADING_STATUS_GRADED || $status->gradingstatus == ASSIGN_MARKING_WORKFLOW_STATE_RELEASED) { $classname = 'submissiongraded'; } else { $classname = 'submissionnotgraded'; } $o .= $this->output->container($statusstr, $classname); $submission = $status->teamsubmission ? $status->teamsubmission : $status->submission; $duedate = $status->duedate; if ($duedate > 0) { if ($status->extensionduedate) { // Extension date. $duedate = $status->extensionduedate; } } // Time remaining. // Only add the row if there is a due date, or a countdown. if ($status->duedate > 0 || !empty($submission->timestarted)) { [$remaining, $classname] = $this->get_time_remaining($status); // If the assignment is not submitted, and there is a submission in progress, // Add a heading for the time limit. if (!empty($submission) && $submission->status != ASSIGN_SUBMISSION_STATUS_SUBMITTED && !empty($submission->timestarted) ) { $o .= $this->output->container(get_string('timeremaining', 'assign')); } $o .= $this->output->container($remaining, $classname); } // Show graders whether this submission is editable by students. if ($status->view == assign_submission_status::GRADER_VIEW) { if ($status->canedit) { $o .= $this->output->container(get_string('submissioneditable', 'assign'), 'submissioneditable'); } else { $o .= $this->output->container(get_string('submissionnoteditable', 'assign'), 'submissionnoteditable'); } } // Grading criteria preview. if (!empty($status->gradingcontrollerpreview)) { $o .= $this->output->container($status->gradingcontrollerpreview, 'gradingmethodpreview'); } if ($submission) { if (!$status->teamsubmission || $status->submissiongroup != false || !$status->preventsubmissionnotingroup) { foreach ($status->submissionplugins as $plugin) { $pluginshowsummary = !$plugin->is_empty($submission) || !$plugin->allow_submissions(); if ($plugin->is_enabled() && $plugin->is_visible() && $plugin->has_user_summary() && $pluginshowsummary ) { $displaymode = \assign_submission_plugin_submission::SUMMARY; $pluginsubmission = new \assign_submission_plugin_submission($plugin, $submission, $displaymode, $status->coursemoduleid, $status->returnaction, $status->returnparams); $plugincomponent = $plugin->get_subtype() . '_' . $plugin->get_type(); $o .= $this->output->container($this->render($pluginsubmission), 'assignsubmission ' . $plugincomponent); } } } } $o .= $this->output->container_end(); return $o; } /** * Render a table containing the current status of the submission. * * @param assign_submission_status $status * @return string */ public function render_assign_submission_status(assign_submission_status $status) { $o = ''; $o .= $this->output->container_start('submissionstatustable'); $o .= $this->output->heading(get_string('submissionstatusheading', 'assign'), 3); $time = time(); $o .= $this->output->box_start('boxaligncenter submissionsummarytable'); $t = new \html_table(); $t->attributes['class'] = 'generaltable table table-striped table-bordered table-hover'; $t->caption = get_string('submissionstatusheading', 'assign'); $t->captionhide = true; // Hidden because it matches the title above. $warningmsg = ''; if ($status->teamsubmissionenabled) { $cell1content = get_string('submissionteam', 'assign'); $group = $status->submissiongroup; if ($group) { $cell2content = format_string($group->name, false, ['context' => $status->context]); } else if ($status->preventsubmissionnotingroup) { if (count($status->usergroups) == 0) { $notification = new \core\output\notification(get_string('noteam', 'assign'), 'error'); $notification->set_show_closebutton(false); $warningmsg = $this->output->notification(get_string('noteam_desc', 'assign'), 'error'); } else if (count($status->usergroups) > 1) { $notification = new \core\output\notification(get_string('multipleteams', 'assign'), 'error'); $notification->set_show_closebutton(false); $warningmsg = $this->output->notification(get_string('multipleteams_desc', 'assign'), 'error'); } $cell2content = $this->output->render($notification); } else { $cell2content = get_string('defaultteam', 'assign'); } $this->add_table_row_tuple($t, $cell1content, $cell2content); } // If multiple attempts are allowed. if ($status->maxattempts > 1 || $status->maxattempts == ASSIGN_UNLIMITED_ATTEMPTS) { $currentattempt = 1; if (!$status->teamsubmissionenabled) { if ($status->submission) { $currentattempt = $status->submission->attemptnumber + 1; } } else { if ($status->teamsubmission) { $currentattempt = $status->teamsubmission->attemptnumber + 1; } } $cell1content = get_string('attemptnumber', 'assign'); $maxattempts = $status->maxattempts; if ($maxattempts == ASSIGN_UNLIMITED_ATTEMPTS) { $cell2content = get_string('currentattempt', 'assign', $currentattempt); } else { $cell2content = get_string('currentattemptof', 'assign', array('attemptnumber' => $currentattempt, 'maxattempts' => $maxattempts)); } $this->add_table_row_tuple($t, $cell1content, $cell2content); } $cell1content = get_string('submissionstatus', 'assign'); $cell2attributes = []; if (!$status->teamsubmissionenabled) { if ($status->submission && $status->submission->status != ASSIGN_SUBMISSION_STATUS_NEW) { $cell2content = get_string('submissionstatus_' . $status->submission->status, 'assign'); $cell2attributes = array('class' => 'submissionstatus' . $status->submission->status); } else { if (!$status->submissionsenabled) { $cell2content = get_string('noonlinesubmissions', 'assign'); } else { $cell2content = get_string('nosubmissionyet', 'assign'); } } } else { $group = $status->submissiongroup; if (!$group && $status->preventsubmissionnotingroup) { $cell2content = get_string('nosubmission', 'assign'); } else if ($status->teamsubmission && $status->teamsubmission->status != ASSIGN_SUBMISSION_STATUS_NEW) { $teamstatus = $status->teamsubmission->status; $cell2content = get_string('submissionstatus_' . $teamstatus, 'assign'); $members = $status->submissiongroupmemberswhoneedtosubmit; $userslist = array(); foreach ($members as $member) { $urlparams = array('id' => $member->id, 'course'=>$status->courseid); $url = new \moodle_url('/user/view.php', $urlparams); if ($status->view == assign_submission_status::GRADER_VIEW && $status->blindmarking) { $userslist[] = $member->alias; } else { $fullname = fullname($member, $status->canviewfullnames); $userslist[] = $this->output->action_link($url, $fullname); } } if (count($userslist) > 0) { $userstr = join(', ', $userslist); $formatteduserstr = get_string('userswhoneedtosubmit', 'assign', $userstr); $cell2content .= $this->output->container($formatteduserstr); } $cell2attributes = array('class' => 'submissionstatus' . $status->teamsubmission->status); } else { if (!$status->submissionsenabled) { $cell2content = get_string('noonlinesubmissions', 'assign'); } else { $cell2content = get_string('nosubmission', 'assign'); } } } $this->add_table_row_tuple($t, $cell1content, $cell2content, [], $cell2attributes); // Is locked? if ($status->locked) { $cell1content = ''; $cell2content = get_string('submissionslocked', 'assign'); $cell2attributes = array('class' => 'submissionlocked'); $this->add_table_row_tuple($t, $cell1content, $cell2content, [], $cell2attributes); } // Grading status. $cell1content = get_string('gradingstatus', 'assign'); if ($status->gradingstatus == ASSIGN_GRADING_STATUS_GRADED || $status->gradingstatus == ASSIGN_GRADING_STATUS_NOT_GRADED) { $cell2content = get_string($status->gradingstatus, 'assign'); } else { $gradingstatus = 'markingworkflowstate' . $status->gradingstatus; $cell2content = get_string($gradingstatus, 'assign'); } if ($status->gradingstatus == ASSIGN_GRADING_STATUS_GRADED || $status->gradingstatus == ASSIGN_MARKING_WORKFLOW_STATE_RELEASED) { $cell2attributes = array('class' => 'submissiongraded'); } else { $cell2attributes = array('class' => 'submissionnotgraded'); } $this->add_table_row_tuple($t, $cell1content, $cell2content, [], $cell2attributes); $submission = $status->teamsubmission ? $status->teamsubmission : $status->submission; $duedate = $status->duedate; if ($duedate > 0) { if ($status->view == assign_submission_status::GRADER_VIEW) { if ($status->cutoffdate) { // Cut off date. $cell1content = get_string('cutoffdate', 'assign'); $cell2content = userdate($status->cutoffdate); $this->add_table_row_tuple($t, $cell1content, $cell2content); } } if ($status->extensionduedate) { // Extension date. $cell1content = get_string('extensionduedate', 'assign'); $cell2content = userdate($status->extensionduedate); $this->add_table_row_tuple($t, $cell1content, $cell2content); $duedate = $status->extensionduedate; } } // Time remaining. // Only add the row if there is a due date, or a countdown. if ($status->duedate > 0 || !empty($submission->timestarted)) { $cell1content = get_string('timeremaining', 'assign'); [$cell2content, $cell2attributes] = $this->get_time_remaining($status); $this->add_table_row_tuple($t, $cell1content, $cell2content, [], ['class' => $cell2attributes]); } // Add time limit info if there is one. $timelimitenabled = get_config('assign', 'enabletimelimit') && $status->timelimit > 0; if ($timelimitenabled && $status->timelimit > 0) { $cell1content = get_string('timelimit', 'assign'); $cell2content = format_time($status->timelimit); $this->add_table_row_tuple($t, $cell1content, $cell2content, [], []); } // Show graders whether this submission is editable by students. if ($status->view == assign_submission_status::GRADER_VIEW) { $cell1content = get_string('editingstatus', 'assign'); if ($status->canedit) { $cell2content = get_string('submissioneditable', 'assign'); $cell2attributes = array('class' => 'submissioneditable'); } else { $cell2content = get_string('submissionnoteditable', 'assign'); $cell2attributes = array('class' => 'submissionnoteditable'); } $this->add_table_row_tuple($t, $cell1content, $cell2content, [], $cell2attributes); } // Last modified. if ($submission) { $cell1content = get_string('timemodified', 'assign'); if ($submission->status != ASSIGN_SUBMISSION_STATUS_NEW) { $cell2content = userdate($submission->timemodified); } else { $cell2content = "-"; } $this->add_table_row_tuple($t, $cell1content, $cell2content); if (!$status->teamsubmission || $status->submissiongroup != false || !$status->preventsubmissionnotingroup) { foreach ($status->submissionplugins as $plugin) { $pluginshowsummary = !$plugin->is_empty($submission) || !$plugin->allow_submissions(); if ($plugin->is_enabled() && $plugin->is_visible() && $plugin->has_user_summary() && $pluginshowsummary ) { $cell1content = $plugin->get_name(); $displaymode = \assign_submission_plugin_submission::SUMMARY; $pluginsubmission = new \assign_submission_plugin_submission($plugin, $submission, $displaymode, $status->coursemoduleid, $status->returnaction, $status->returnparams); $cell2content = $this->render($pluginsubmission); $this->add_table_row_tuple($t, $cell1content, $cell2content); } } } } $o .= $warningmsg; $o .= \html_writer::table($t); $o .= $this->output->box_end(); // Grading criteria preview. if (!empty($status->gradingcontrollerpreview)) { $o .= $this->output->heading(get_string('gradingmethodpreview', 'assign'), 4); $o .= $status->gradingcontrollerpreview; } $o .= $this->output->container_end(); return $o; } /** * Output the attempt history chooser for this assignment * * @param \assign_attempt_history_chooser $history * @return string */ public function render_assign_attempt_history_chooser(\assign_attempt_history_chooser $history) { $o = ''; $context = $history->export_for_template($this); $o .= $this->render_from_template('mod_assign/attempt_history_chooser', $context); return $o; } /** * Output the attempt history for this assignment * * @param \assign_attempt_history $history * @return string */ public function render_assign_attempt_history(\assign_attempt_history $history) { $o = ''; // Don't show the last one because it is the current submission. array_pop($history->submissions); // Show newest to oldest. $history->submissions = array_reverse($history->submissions); if (empty($history->submissions)) { return ''; } $containerid = 'attempthistory' . uniqid(); $o .= $this->output->heading(get_string('attempthistory', 'assign'), 3); $o .= $this->box_start('attempthistory', $containerid); foreach ($history->submissions as $i => $submission) { $grade = null; foreach ($history->grades as $onegrade) { if ($onegrade->attemptnumber == $submission->attemptnumber) { if ($onegrade->grade != ASSIGN_GRADE_NOT_SET) { $grade = $onegrade; } break; } } if ($submission) { $submissionsummary = userdate($submission->timemodified); } else { $submissionsummary = get_string('nosubmission', 'assign'); } $attemptsummaryparams = array('attemptnumber'=>$submission->attemptnumber+1, 'submissionsummary'=>$submissionsummary); $o .= $this->heading(get_string('attemptheading', 'assign', $attemptsummaryparams), 4); $t = new \html_table(); $t->caption = get_string('attemptheading', 'assign', $attemptsummaryparams); $t->captionhide = true; // Hidden because it matches the title above. if ($submission) { $cell1content = get_string('submissionstatus', 'assign'); $cell2content = get_string('submissionstatus_' . $submission->status, 'assign'); $this->add_table_row_tuple($t, $cell1content, $cell2content); foreach ($history->submissionplugins as $plugin) { $pluginshowsummary = !$plugin->is_empty($submission) || !$plugin->allow_submissions(); if ($plugin->is_enabled() && $plugin->is_visible() && $plugin->has_user_summary() && $pluginshowsummary) { $cell1content = $plugin->get_name(); $pluginsubmission = new \assign_submission_plugin_submission($plugin, $submission, \assign_submission_plugin_submission::SUMMARY, $history->coursemoduleid, $history->returnaction, $history->returnparams); $cell2content = $this->render($pluginsubmission); $this->add_table_row_tuple($t, $cell1content, $cell2content); } } } if ($grade) { // Heading 'feedback'. $title = get_string('feedback', 'assign', $i); $title .= $this->output->spacer(array('width'=>10)); if ($history->cangrade) { // Edit previous feedback. $returnparams = http_build_query($history->returnparams); $urlparams = array('id' => $history->coursemoduleid, 'rownum'=>$history->rownum, 'useridlistid'=>$history->useridlistid, 'attemptnumber'=>$grade->attemptnumber, 'action'=>'grade', 'returnaction'=>$history->returnaction, 'returnparams'=>$returnparams); $url = new \moodle_url('/mod/assign/view.php', $urlparams); $icon = new \pix_icon('gradefeedback', get_string('editattemptfeedback', 'assign', $grade->attemptnumber+1), 'mod_assign'); $title .= $this->output->action_icon($url, $icon); } $cell = new \html_table_cell($title); $cell->attributes['class'] = 'feedbacktitle'; $cell->colspan = 2; $t->data[] = new \html_table_row(array($cell)); // Grade. $cell1content = get_string('gradenoun'); $cell2content = $grade->gradefordisplay; $this->add_table_row_tuple($t, $cell1content, $cell2content); // Graded on. $cell1content = get_string('gradedon', 'assign'); $cell2content = userdate($grade->timemodified); $this->add_table_row_tuple($t, $cell1content, $cell2content); // Graded by set to a real user. Not set can be empty or -1. if (!empty($grade->grader) && is_object($grade->grader)) { $cell1content = get_string('gradedby', 'assign'); $cell2content = $this->output->user_picture($grade->grader) . $this->output->spacer(array('width' => 30)) . fullname($grade->grader); $this->add_table_row_tuple($t, $cell1content, $cell2content); } // Feedback from plugins. foreach ($history->feedbackplugins as $plugin) { if ($plugin->is_enabled() && $plugin->is_visible() && $plugin->has_user_summary() && !$plugin->is_empty($grade)) { $pluginfeedback = new \assign_feedback_plugin_feedback( $plugin, $grade, \assign_feedback_plugin_feedback::SUMMARY, $history->coursemoduleid, $history->returnaction, $history->returnparams ); $cell1content = $plugin->get_name(); $cell2content = $this->render($pluginfeedback); $this->add_table_row_tuple($t, $cell1content, $cell2content); } } } $o .= \html_writer::table($t); } $o .= $this->box_end(); $this->page->requires->yui_module('moodle-mod_assign-history', 'Y.one("#' . $containerid . '").history'); return $o; } /** * Render a submission plugin submission * * @param \assign_submission_plugin_submission $submissionplugin * @return string */ public function render_assign_submission_plugin_submission(\assign_submission_plugin_submission $submissionplugin) { $o = ''; if ($submissionplugin->view == \assign_submission_plugin_submission::SUMMARY) { $showviewlink = false; $summary = $submissionplugin->plugin->view_summary($submissionplugin->submission, $showviewlink); $classsuffix = $submissionplugin->plugin->get_subtype() . '_' . $submissionplugin->plugin->get_type() . '_' . $submissionplugin->submission->id; $o .= $this->output->box_start('boxaligncenter plugincontentsummary summary_' . $classsuffix); $link = ''; if ($showviewlink) { $previewstr = get_string('viewsubmission', 'assign'); $icon = $this->output->pix_icon('t/viewdetails', $previewstr); $expandstr = get_string('viewfull', 'assign'); $expandicon = $this->output->pix_icon('t/switch_plus', $expandstr); $options = array( 'class' => 'expandsummaryicon expand_' . $classsuffix, 'aria-label' => $expandstr, 'role' => 'button', 'aria-expanded' => 'false' ); $o .= \html_writer::link('', $expandicon, $options); $jsparams = array($submissionplugin->plugin->get_subtype(), $submissionplugin->plugin->get_type(), $submissionplugin->submission->id); $this->page->requires->js_init_call('M.mod_assign.init_plugin_summary', $jsparams); $action = 'viewplugin' . $submissionplugin->plugin->get_subtype(); $returnparams = http_build_query($submissionplugin->returnparams); $link .= ''; $link .= $this->output->spacer(array('width'=>15)); } $o .= $link . $summary; $o .= $this->output->box_end(); if ($showviewlink) { $o .= $this->output->box_start('boxaligncenter hidefull full_' . $classsuffix); $collapsestr = get_string('viewsummary', 'assign'); $options = array( 'class' => 'expandsummaryicon contract_' . $classsuffix, 'aria-label' => $collapsestr, 'role' => 'button', 'aria-expanded' => 'true' ); $collapseicon = $this->output->pix_icon('t/switch_minus', $collapsestr); $o .= \html_writer::link('', $collapseicon, $options); $o .= $submissionplugin->plugin->view($submissionplugin->submission); $o .= $this->output->box_end(); } } else if ($submissionplugin->view == \assign_submission_plugin_submission::FULL) { $o .= $this->output->box_start('boxaligncenter submissionfull'); $o .= $submissionplugin->plugin->view($submissionplugin->submission); $o .= $this->output->box_end(); } return $o; } /** * Render the grading table. * * @param \assign_grading_table $table * @return string */ public function render_assign_grading_table(\assign_grading_table $table) { $o = ''; $o .= $this->output->box_start('boxaligncenter gradingtable position-relative'); $this->page->requires->js_init_call('M.mod_assign.init_grading_table', array()); $o .= $this->flexible_table($table, $table->get_rows_per_page(), false); $o .= $this->output->box_end(); return $o; } /** * Render a feedback plugin feedback * * @param \assign_feedback_plugin_feedback $feedbackplugin * @return string */ public function render_assign_feedback_plugin_feedback(\assign_feedback_plugin_feedback $feedbackplugin) { $o = ''; if ($feedbackplugin->view == \assign_feedback_plugin_feedback::SUMMARY) { $showviewlink = false; $summary = $feedbackplugin->plugin->view_summary($feedbackplugin->grade, $showviewlink); $classsuffix = $feedbackplugin->plugin->get_subtype() . '_' . $feedbackplugin->plugin->get_type() . '_' . $feedbackplugin->grade->id; $o .= $this->output->box_start('boxaligncenter plugincontentsummary summary_' . $classsuffix); $link = ''; if ($showviewlink) { $previewstr = get_string('viewfeedback', 'assign'); $icon = $this->output->pix_icon('t/viewdetails', $previewstr); $expandstr = get_string('viewfull', 'assign'); $expandicon = $this->output->pix_icon('t/switch_plus', $expandstr); $options = array( 'class' => 'expandsummaryicon expand_' . $classsuffix, 'aria-label' => $expandstr, 'role' => 'button', 'aria-expanded' => 'false' ); $o .= \html_writer::link('', $expandicon, $options); $jsparams = array($feedbackplugin->plugin->get_subtype(), $feedbackplugin->plugin->get_type(), $feedbackplugin->grade->id); $this->page->requires->js_init_call('M.mod_assign.init_plugin_summary', $jsparams); $urlparams = array('id' => $feedbackplugin->coursemoduleid, 'gid'=>$feedbackplugin->grade->id, 'plugin'=>$feedbackplugin->plugin->get_type(), 'action'=>'viewplugin' . $feedbackplugin->plugin->get_subtype(), 'returnaction'=>$feedbackplugin->returnaction, 'returnparams'=>http_build_query($feedbackplugin->returnparams)); $url = new \moodle_url('/mod/assign/view.php', $urlparams); $link .= ''; $link .= $this->output->spacer(array('width'=>15)); } $o .= $link . $summary; $o .= $this->output->box_end(); if ($showviewlink) { $o .= $this->output->box_start('boxaligncenter hidefull full_' . $classsuffix); $collapsestr = get_string('viewsummary', 'assign'); $options = array( 'class' => 'expandsummaryicon contract_' . $classsuffix, 'aria-label' => $collapsestr, 'role' => 'button', 'aria-expanded' => 'true' ); $collapseicon = $this->output->pix_icon('t/switch_minus', $collapsestr); $o .= \html_writer::link('', $collapseicon, $options); $o .= $feedbackplugin->plugin->view($feedbackplugin->grade); $o .= $this->output->box_end(); } } else if ($feedbackplugin->view == \assign_feedback_plugin_feedback::FULL) { $o .= $this->output->box_start('boxaligncenter feedbackfull'); $o .= $feedbackplugin->plugin->view($feedbackplugin->grade); $o .= $this->output->box_end(); } return $o; } /** * Render a course index summary * * @deprecated since Moodle 5.0 (MDL-83888). * @todo MDL-84429 Final deprecation in Moodle 6.0. * @param \assign_course_index_summary $indexsummary * @return string */ #[\core\attribute\deprecated( since: '5.0', mdl: 'MDL-83888', reason: 'The assign_course_index_summary class is not used anymore.', )] public function render_assign_course_index_summary(\assign_course_index_summary $indexsummary) { \core\deprecation::emit_deprecation([$this, __FUNCTION__]); $o = ''; $strplural = get_string('modulenameplural', 'assign'); $strsectionname = $indexsummary->courseformatname; $strduedate = get_string('duedate', 'assign'); $strsubmission = get_string('submission', 'assign'); $strgrade = get_string('gradenoun'); $table = new \html_table(); if ($indexsummary->usesections) { $table->head = array ($strsectionname, $strplural, $strduedate, $strsubmission, $strgrade); $table->align = array ('left', 'left', 'center', 'right', 'right'); } else { $table->head = array ($strplural, $strduedate, $strsubmission, $strgrade); $table->align = array ('left', 'left', 'center', 'right'); } $table->data = array(); $currentsection = ''; foreach ($indexsummary->assignments as $info) { $params = array('id' => $info['cmid']); $link = \html_writer::link(new \moodle_url('/mod/assign/view.php', $params), $info['cmname']); $due = $info['timedue'] ? userdate($info['timedue']) : '-'; if ($info['cangrade']) { $params['action'] = 'grading'; $gradeinfo = \html_writer::link(new \moodle_url('/mod/assign/view.php', $params), get_string('numberofsubmissionsneedgradinglabel', 'assign', $info['gradeinfo'])); } else { $gradeinfo = $info['gradeinfo']; } $printsection = ''; if ($indexsummary->usesections) { if ($info['sectionname'] !== $currentsection) { if ($info['sectionname']) { $printsection = $info['sectionname']; } if ($currentsection !== '') { $table->data[] = 'hr'; } $currentsection = $info['sectionname']; } } if ($indexsummary->usesections) { $row = [$printsection, $link, $due, $info['submissioninfo'], $gradeinfo]; } else { $row = [$link, $due, $info['submissioninfo'], $gradeinfo]; } $table->data[] = $row; } $o .= \html_writer::table($table); return $o; } /** * Get the time remaining for a submission. * * @param \mod_assign\output\assign_submission_status $status * @return array The first element is the time remaining as a human readable * string and the second is a CSS class. */ protected function get_time_remaining(\mod_assign\output\assign_submission_status $status): array { $time = time(); $submission = $status->teamsubmission ? $status->teamsubmission : $status->submission; $submissionstarted = $submission && property_exists($submission, 'timestarted') && $submission->timestarted; $timelimitenabled = get_config('assign', 'enabletimelimit') && $status->timelimit > 0 && $submissionstarted; // Define $duedate as latest between due date and extension - which is a possibility... $extensionduedate = intval($status->extensionduedate); $duedate = !empty($extensionduedate) ? max($status->duedate, $extensionduedate) : $status->duedate; $duedatereached = $duedate > 0 && $duedate - $time <= 0; $timelimitenabledbeforeduedate = $timelimitenabled && !$duedatereached; // There is a submission, display the relevant early/late message. if ($submission && $submission->status == ASSIGN_SUBMISSION_STATUS_SUBMITTED) { $latecalculation = $submission->timemodified - ($timelimitenabledbeforeduedate ? $submission->timestarted : 0); $latethreshold = $timelimitenabledbeforeduedate ? $status->timelimit : $duedate; $earlystring = $timelimitenabledbeforeduedate ? 'submittedundertime' : 'submittedearly'; $latestring = $timelimitenabledbeforeduedate ? 'submittedovertime' : 'submittedlate'; $ontime = $latecalculation <= $latethreshold; return [ get_string( $ontime ? $earlystring : $latestring, 'assign', format_time($latecalculation - $latethreshold) ), $ontime ? 'earlysubmission' : 'latesubmission' ]; } // There is no submission, due date has passed, show assignment is overdue. if ($duedatereached) { return [ get_string( $status->submissionsenabled ? 'overdue' : 'duedatereached', 'assign', format_time($time - $duedate) ), 'overdue' ]; } // An attempt has started and there is a time limit, display the time limit. if ($timelimitenabled && !empty($submission->timestarted)) { return [ (new \assign($status->context, null, null))->get_timelimit_panel($submission), 'timeremaining' ]; } // Assignment is not overdue, and no submission has been made. Just display the due date. return [get_string('paramtimeremaining', 'assign', format_time($duedate - $time)), 'timeremaining']; } /** * Internal function - creates htmls structure suitable for YUI tree. * * @param \assign_files $tree * @param array $dir * @return string */ protected function htmllize_tree(\assign_files $tree, $dir) { global $CFG; $yuiconfig = array(); $yuiconfig['type'] = 'html'; if (empty($dir['subdirs']) and empty($dir['files'])) { return ''; } $result = '
    '; foreach ($dir['subdirs'] as $subdir) { $image = $this->output->pix_icon(file_folder_icon(), $subdir['dirname'], 'moodle', array('class'=>'icon')); $result .= '
  • ' . '
    ' . $image . ' ' . s($subdir['dirname']) . '
    ' . $this->htmllize_tree($tree, $subdir) . '
  • '; } foreach ($dir['files'] as $file) { $filename = $file->get_filename(); if ($CFG->enableplagiarism) { require_once($CFG->libdir.'/plagiarismlib.php'); $plagiarismlinks = plagiarism_get_links(array('userid'=>$file->get_userid(), 'file'=>$file, 'cmid'=>$tree->cm->id, 'course'=>$tree->course)); } else { $plagiarismlinks = ''; } $image = $this->output->pix_icon(file_file_icon($file), $filename, 'moodle', array('class'=>'icon')); $result .= '
  • ' . '
    ' . '
    ' . $image . ' ' . html_writer::link($tree->get_file_url($file), $file->get_filename(), [ 'target' => '_blank', ]) . ' ' . $plagiarismlinks . ' ' . $this->get_portfolio_button($tree, $file) . ' ' . '
    ' . '
    ' . $tree->get_modified_time($file) . '
    ' . '
    ' . '
  • '; } $result .= '
'; return $result; } /** * Get the portfolio button content for the specified file. * * @param assign_files $tree * @param stored_file $file * @return string */ protected function get_portfolio_button(assign_files $tree, stored_file $file): string { global $CFG; if (empty($CFG->enableportfolios)) { return ''; } if (!has_capability('mod/assign:exportownsubmission', $tree->context)) { return ''; } require_once($CFG->libdir . '/portfoliolib.php'); $button = new portfolio_add_button(); $portfolioparams = [ 'cmid' => $tree->cm->id, 'fileid' => $file->get_id(), ]; $button->set_callback_options('assign_portfolio_caller', $portfolioparams, 'mod_assign'); $button->set_format_by_file($file); return (string) $button->to_html(PORTFOLIO_ADD_ICON_LINK); } /** * Helper method dealing with the fact we can not just fetch the output of flexible_table * * @param \flexible_table $table The table to render * @param int $rowsperpage How many assignments to render in a page * @param bool $displaylinks - Whether to render links in the table * (e.g. downloads would not enable this) * @return string HTML */ protected function flexible_table(\flexible_table $table, $rowsperpage, $displaylinks) { $o = ''; ob_start(); $table->out($rowsperpage, $displaylinks); $o = ob_get_contents(); ob_end_clean(); return $o; } /** * Helper method dealing with the fact we can not just fetch the output of moodleforms * * @param \moodleform $mform * @return string HTML */ protected function moodleform(\moodleform $mform) { $o = ''; ob_start(); $mform->display(); $o = ob_get_contents(); ob_end_clean(); return $o; } /** * Defer to template. * * @param grading_app $app - All the data to render the grading app. */ public function render_grading_app(grading_app $app) { $context = $app->export_for_template($this); return $this->render_from_template('mod_assign/grading_app', $context); } /** * Renders the submission action menu. * * @param \mod_assign\output\actionmenu $actionmenu The actionmenu * @return string Rendered action menu. */ public function submission_actionmenu(\mod_assign\output\actionmenu $actionmenu): string { $context = $actionmenu->export_for_template($this); return $this->render_from_template('mod_assign/submission_actionmenu', $context); } /** * Renders the user submission action menu. * * @param \mod_assign\output\user_submission_actionmenu $actionmenu The actionmenu * @return string The rendered action menu. */ public function render_user_submission_actionmenu(\mod_assign\output\user_submission_actionmenu $actionmenu): string { $context = $actionmenu->export_for_template($this); return $this->render_from_template('mod_assign/user_submission_actionmenu', $context); } /** * Renders the override action menu. * * @param \mod_assign\output\override_actionmenu $actionmenu The actionmenu * @return string The rendered override action menu. */ public function render_override_actionmenu(\mod_assign\output\override_actionmenu $actionmenu): string { $context = $actionmenu->export_for_template($this); return $this->render_from_template('mod_assign/override_actionmenu', $context); } /** * Renders the grading action menu. * * @param \mod_assign\output\grading_actionmenu $actionmenu The actionmenu * @return string The rendered grading action menu. */ public function render_grading_actionmenu(\mod_assign\output\grading_actionmenu $actionmenu): string { $context = $actionmenu->export_for_template($this); return $this->render_from_template('mod_assign/grading_actionmenu', $context); } /** * Formats activity intro text. * * @param object $assign Instance of assign. * @param int $cmid Course module ID. * @return string */ public function format_activity_text($assign, $cmid) { global $CFG; require_once("$CFG->libdir/filelib.php"); $context = \context_module::instance($cmid); $options = array('noclean' => true, 'para' => false, 'filter' => true, 'context' => $context, 'overflowdiv' => true); $activity = file_rewrite_pluginfile_urls( $assign->activity, 'pluginfile.php', $context->id, 'mod_assign', ASSIGN_ACTIVITYATTACHMENT_FILEAREA, 0); return trim(format_text($activity, $assign->activityformat, $options, null)); } } classes/output/assign_submission_status.php000064400000017322152265517350015434 0ustar00. /** * This file contains the definition for the renderable assign submission status. * * @package mod_assign * @copyright 2020 Matt Porritt * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_assign\output; /** * This file contains the definition for the renderable assign submission status. * * @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_submission_status implements \renderable { /** @var int STUDENT_VIEW */ const STUDENT_VIEW = 10; /** @var int GRADER_VIEW */ const GRADER_VIEW = 20; /** @var int allowsubmissionsfromdate */ public $allowsubmissionsfromdate = 0; /** @var bool alwaysshowdescription */ public $alwaysshowdescription = false; /** @var mixed the submission info (may be null or an integer) */ public $submission = null; /** @var boolean teamsubmissionenabled - true or false */ public $teamsubmissionenabled = false; /** @var \stdClass teamsubmission the team submission info (may be null) */ public $teamsubmission = null; /** @var mixed submissiongroup the submission group info (may be null) */ public $submissiongroup = null; /** @var array submissiongroupmemberswhoneedtosubmit list of users who still need to submit */ public $submissiongroupmemberswhoneedtosubmit = array(); /** @var bool submissionsenabled */ public $submissionsenabled = false; /** @var bool locked */ public $locked = false; /** @var bool graded */ public $graded = false; /** @var int duedate */ public $duedate = 0; /** @var int cutoffdate */ public $cutoffdate = 0; /** @var array submissionplugins - the list of submission plugins */ public $submissionplugins = array(); /** @var string returnaction */ public $returnaction = ''; /** @var string returnparams */ public $returnparams = array(); /** @var int courseid */ public $courseid = 0; /** @var int coursemoduleid */ public $coursemoduleid = 0; /** @var int the view (STUDENT_VIEW OR GRADER_VIEW) */ public $view = self::STUDENT_VIEW; /** @var bool canviewfullnames */ public $canviewfullnames = false; /** @var bool canedit */ public $canedit = false; /** @var bool cansubmit */ public $cansubmit = false; /** @var int extensionduedate */ public $extensionduedate = 0; /** @var \context context */ public $context = 0; /** @var bool blindmarking - Should we hide student identities from graders? */ public $blindmarking = false; /** @var string gradingcontrollerpreview */ public $gradingcontrollerpreview = ''; /** @var string attemptreopenmethod */ public $attemptreopenmethod; /** @var int maxattempts */ public $maxattempts; /** @var string gradingstatus */ public $gradingstatus = ''; /** @var bool preventsubmissionnotingroup */ public $preventsubmissionnotingroup = 0; /** @var array usergroups */ public $usergroups = array(); /** @var int The time limit for the assignment */ public $timelimit = 0; /** @var bool */ public $caneditowner; /** * Constructor * * @param int $allowsubmissionsfromdate * @param bool $alwaysshowdescription * @param mixed $submission * @param bool $teamsubmissionenabled * @param \stdClass $teamsubmission * @param mixed $submissiongroup * @param array $submissiongroupmemberswhoneedtosubmit * @param bool $submissionsenabled * @param bool $locked * @param bool $graded * @param int $duedate * @param int $cutoffdate * @param array $submissionplugins * @param string $returnaction * @param array $returnparams * @param int $coursemoduleid * @param int $courseid * @param string $view * @param bool $canedit * @param bool $cansubmit * @param bool $canviewfullnames * @param int $extensionduedate Any extension to the due date granted for this user. * @param \context $context Any extension to the due date granted for this user. * @param bool $blindmarking Should we hide student identities from graders? * @param string $gradingcontrollerpreview * @param string $attemptreopenmethod The method of reopening student attempts. * @param int $maxattempts How many attempts can a student make? * @param string $gradingstatus The submission status (ie. Graded, Not Released etc). * @param bool $preventsubmissionnotingroup Prevent submission if user is not in a group. * @param array $usergroups Array containing all groups the user is assigned to. * @param int $timelimit The time limit for the assignment. */ public function __construct( $allowsubmissionsfromdate, $alwaysshowdescription, $submission, $teamsubmissionenabled, $teamsubmission, $submissiongroup, $submissiongroupmemberswhoneedtosubmit, $submissionsenabled, $locked, $graded, $duedate, $cutoffdate, $submissionplugins, $returnaction, $returnparams, $coursemoduleid, $courseid, $view, $canedit, $cansubmit, $canviewfullnames, $extensionduedate, $context, $blindmarking, $gradingcontrollerpreview, $attemptreopenmethod, $maxattempts, $gradingstatus, $preventsubmissionnotingroup, $usergroups, $timelimit ) { $this->allowsubmissionsfromdate = $allowsubmissionsfromdate; $this->alwaysshowdescription = $alwaysshowdescription; $this->submission = $submission; $this->teamsubmissionenabled = $teamsubmissionenabled; $this->teamsubmission = $teamsubmission; $this->submissiongroup = $submissiongroup; $this->submissiongroupmemberswhoneedtosubmit = $submissiongroupmemberswhoneedtosubmit; $this->submissionsenabled = $submissionsenabled; $this->locked = $locked; $this->graded = $graded; $this->duedate = $duedate; $this->cutoffdate = $cutoffdate; $this->submissionplugins = $submissionplugins; $this->returnaction = $returnaction; $this->returnparams = $returnparams; $this->coursemoduleid = $coursemoduleid; $this->courseid = $courseid; $this->view = $view; $this->canedit = $canedit; $this->cansubmit = $cansubmit; $this->canviewfullnames = $canviewfullnames; $this->extensionduedate = $extensionduedate; $this->context = $context; $this->blindmarking = $blindmarking; $this->gradingcontrollerpreview = $gradingcontrollerpreview; $this->attemptreopenmethod = $attemptreopenmethod; $this->maxattempts = $maxattempts; $this->gradingstatus = $gradingstatus; $this->preventsubmissionnotingroup = $preventsubmissionnotingroup; $this->usergroups = $usergroups; $this->timelimit = $timelimit; } } classes/output/actionmenu.php000064400000003471152265517350012434 0ustar00. namespace mod_assign\output; use templatable; use renderable; use moodle_url; /** * Output the actionbar for this activity. * * @package mod_assign * @copyright 2021 Adrian Greeve * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class actionmenu implements templatable, renderable { /** @var int The course module ID. */ private $cmid; /** * Constructor for this object. * * @param int $cmid The course module ID. */ public function __construct(int $cmid) { $this->cmid = $cmid; } /** * Data to be used for a template. * * @param \renderer_base $output renderer base output. * @return array Data to be used for a template. */ public function export_for_template(\renderer_base $output): array { $return = []; if (has_capability('mod/assign:grade', \context_module::instance($this->cmid))) { $gradelink = new moodle_url('/mod/assign/view.php', ['id' => $this->cmid, 'action' => 'grader']); $return['gradelink'] = $gradelink->out(false); } return $return; } } classes/output/assign_header.php000064400000006562152265517350013072 0ustar00. /** * This file contains the definition for the renderable assign header. * * @package mod_assign * @copyright 2020 Matt Porritt * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_assign\output; /** * This file contains the definition for the renderable assign header. * * @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_header implements \renderable { /** @var \stdClass The assign record. */ public $assign; /** @var mixed \context|null the context record. */ public $context; /** @var bool $showintro Show or hide the intro. */ public $showintro; /** @var int coursemoduleid The course module id. */ public $coursemoduleid; /** @var string $subpage Optional subpage (extra level in the breadcrumbs). */ public $subpage; /** @var string $preface Optional preface (text to show before the heading). */ public $preface; /** @var string $postfix Optional postfix (text to show after the intro). */ public $postfix; /** @var \moodle_url|null $subpageurl link for the sub page */ public $subpageurl; /** @var bool $activity optional show activity text. */ public $activity; /** * Constructor * * @param \stdClass $assign The assign database record. * @param \context|null $context The course module context. * @param bool $showintro Show or hide the intro. * @param int $coursemoduleid The course module id. * @param string $subpage An optional sub page in the navigation. * @param string $preface An optional preface to show before the heading. * @param string $postfix An optional postfix to show after the intro. * @param \moodle_url|null $subpageurl An optional sub page URL link for the subpage. * @param bool $activity Optional show activity text if true. */ public function __construct( \stdClass $assign, $context, $showintro, $coursemoduleid, $subpage = '', $preface = '', $postfix = '', ?\moodle_url $subpageurl = null, bool $activity = false ) { $this->assign = $assign; $this->context = $context; $this->showintro = $showintro; $this->coursemoduleid = $coursemoduleid; $this->subpage = $subpage; $this->preface = $preface; $this->postfix = $postfix; $this->subpageurl = $subpageurl; $this->activity = $activity; } } classes/output/timelimit_panel.php000064400000004717152265517350013452 0ustar00. /** * Represents the timer panel. * * @package mod_assign * @copyright 2020 Ilya Tregubov * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_assign\output; use renderable; use renderer_base; use stdClass; use templatable; /** * Represents the timer panel. * * @package mod_assign * @copyright 2020 Ilya Tregubov * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class timelimit_panel implements templatable, renderable { /** @var \stdClass assign submission attempt.*/ protected $submission; /** @var object assign object.*/ protected $assign; /** * Constructor. * * @param \stdClass $submission assign submission. * @param \stdClass $assign assign object. */ public function __construct(\stdClass $submission, \stdClass $assign) { $this->submission = $submission; $this->assign = $assign; } /** * Render timer. * * @param renderer_base $output The current page renderer. * @return stdClass - Flat list of exported data. */ public function export_for_template(renderer_base $output): stdClass { return (object)['timerstartvalue' => $this->end_time() - time()]; } /** * Compute end time for this assign attempt. * * @return int the time when assign attempt is due. */ private function end_time(): int { $timedue = $this->submission->timestarted + $this->assign->timelimit; if ($this->assign->duedate) { return min($timedue, $this->assign->duedate); } if ($this->assign->cutoffdate) { return min($timedue, $this->assign->cutoffdate); } return $timedue; } } classes/output/grading_actionmenu.php000064400000041612152265517350014126 0ustar00. /** * Output the grading actionbar for this activity. * * @package mod_assign * @copyright 2021 Adrian Greeve * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_assign\output; use assign; use context_module; use core_course\output\actionbar\group_selector; use core_course\output\actionbar\user_selector; use templatable; use renderable; use moodle_url; use core\output\local\dropdown\dialog; /** * Output the grading actionbar for this activity. * * @package mod_assign * @copyright 2021 Adrian Greeve * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class grading_actionmenu implements templatable, renderable { /** @var int Course module ID. */ protected int $cmid; /** @var bool If any submission plugins are enabled. */ protected bool $submissionpluginenabled; /** @var int The number of submissions made. */ protected int $submissioncount; /** @var assign The assign instance. */ protected assign $assign; /** @var bool If there are submissions to download. */ protected bool $showdownload; /** @var array Applied user initials filters, containing 'firstname' and 'lastname'. **/ protected array $userinitials; /** @var bool true if the user has this capability. Otherwise false. */ private bool $hasviewblind; /** * Constructor for this object. * * @param int $cmid Course module ID. * @param null|bool $submissionpluginenabled This parameter has been deprecated since 4.5 and should not be used anymore. * @param null|int $submissioncount This parameter has been deprecated since 4.5 and should not be used anymore. * @param assign|null $assign The assign instance. If not provided, it will be loaded based on the cmid. * @param array $userinitials The user initials to filter the table by. */ public function __construct( int $cmid, ?bool $submissionpluginenabled = null, ?int $submissioncount = null, ?assign $assign = null, array $userinitials = [] ) { $this->cmid = $cmid; if (!$assign) { $context = context_module::instance($cmid); $assign = new assign($context, null, null); } $this->assign = $assign; $this->showdownload = $this->assign->is_any_submission_plugin_enabled() && $this->assign->count_submissions(); $this->userinitials = $userinitials; // Check if we have the elevated view capablities to see the blind details. $this->hasviewblind = has_capability('mod/assign:viewblinddetails', $this->assign->get_context()); } /** * Data to render in a template. * * @param \renderer_base $output renderer base output. * @return array Data to render. */ public function export_for_template(\renderer_base $output): array { global $PAGE; $course = $this->assign->get_course(); $cm = get_coursemodule_from_id('assign', $this->cmid); $data = []; $userid = optional_param('userid', null, PARAM_INT); // If the user ID is set, it indicates that a user has been selected. In this case, override the user search // string with the full name of the selected user. $usersearch = $userid ? fullname(\core_user::get_user($userid)) : optional_param('search', '', PARAM_NOTAGS); $isblind = $this->assign->is_blind_marking() && !$this->hasviewblind; if ($isblind) { $usersearch = $userid ? get_string('hiddenuser', 'assign') . $this->assign->get_uniqueid_for_user($userid) : $usersearch; } $resetlink = new moodle_url('/mod/assign/view.php', ['id' => $this->cmid, 'action' => 'grading']); $groupid = groups_get_course_group($course, true); $userselector = new user_selector( course: $course, resetlink: $resetlink, userid: !$isblind ? $userid : null, groupid: $groupid, usersearch: $usersearch, instanceid: $this->assign->get_instance()->id ); $data['userselector'] = $userselector->export_for_template($output); $hasinitials = !empty($this->userinitials['firstname']) || !empty($this->userinitials['lastname']); $additionalparams = ['action' => 'grading', 'id' => $this->cmid]; if (!empty($userid)) { $additionalparams['userid'] = $userid; } else if (!empty($usersearch)) { $additionalparams['search'] = $usersearch; } $initialselector = new \core_course\output\actionbar\initials_selector( course: $course, targeturl: 'mod/assign/view.php', firstinitial: $this->userinitials['firstname'] ?? '', lastinitial: $this->userinitials['lastname'] ?? '', firstinitialparam: 'tifirst', lastinitialparam: 'tilast', additionalparams: $additionalparams ); $data['initialselector'] = $initialselector->export_for_template($output); if (groups_get_activity_groupmode($cm, $course)) { $gs = new group_selector($PAGE->context, false); $data['groupselector'] = $gs->export_for_template($output); } if ($extrafiltersdropdown = $this->get_extra_filters_dropdown()) { $PAGE->requires->js_call_amd('mod_assign/actionbar/grading/extra_filters_dropdown', 'init', []); $data['extrafiltersdropdown'] = $extrafiltersdropdown->export_for_template($output); } $activitygroup = groups_get_activity_group($cm); $hasuserfilter = get_user_preferences('assign_filter'); $hasextrafilters = $this->get_applied_extra_filters_count() > 0; if ($activitygroup || $hasuserfilter || $hasextrafilters || $hasinitials) { $url = new moodle_url('/mod/assign/view.php', [ 'id' => $this->cmid, 'action' => 'grading', 'group' => 0, 'status' => '', 'workflowfilter' => '', 'markingallocationfilter' => '', 'suspendedparticipantsfilter' => 0, 'tifirst' => '', 'tilast' => '', ]); $data['pagereset'] = $url->out(false); } if ($this->assign->is_any_submission_plugin_enabled()) { ['statusmenu' => $statusmenu, 'currentvalue' => $currentvalue] = $this->get_status_menu(); $statusselect = new \core\output\select_menu('status', $statusmenu, $currentvalue); $statusselect->set_label(get_string('status', 'mod_assign'), [], true); $data['statusselector'] = $statusselect->export_for_template($output); } if ($this->assign->can_grade()) { $url = new moodle_url('/mod/assign/view.php', [ 'id' => $this->assign->get_course_module()->id, 'action' => 'grader', ]); $data['graderurl'] = $url->out(false); } $gradingmanager = get_grading_manager($this->assign->get_context(), 'mod_assign', 'submissions'); $controller = $gradingmanager->get_active_controller(); $showquickgrading = empty($controller) && $this->assign->can_grade(); if ($showquickgrading) { $quickgradingbaseurl = new moodle_url('/mod/assign/view.php', [ 'id' => $this->assign->get_course_module()->id, 'action' => 'grading', ]); if ($userid) { $quickgradingbaseurl->param('userid', $userid); } else if ($usersearch) { $quickgradingbaseurl->param('search', $usersearch); } $data['quickgrading'] = [ 'baseurl' => $quickgradingbaseurl->out(false), 'enabled' => get_user_preferences('assign_quickgrading', false), ]; } if ($this->showdownload) { $downloadasfoldersbaseurl = new moodle_url('/mod/assign/view.php', [ 'id' => $this->assign->get_course_module()->id, 'action' => 'grading', ]); if ($userid) { $downloadasfoldersbaseurl->param('userid', $userid); } else if ($usersearch) { $downloadasfoldersbaseurl->param('search', $usersearch); } $downloadasfolders = get_user_preferences('assign_downloadasfolders', 1); $data['downloadasfolders'] = [ 'baseurl' => $downloadasfoldersbaseurl->out(false), 'enabled' => $downloadasfolders, ]; } $actions = $this->get_actions(); if ($actions) { $menu = new \action_menu(); $menu->set_menu_trigger(get_string('actions'), 'btn btn-outline-primary'); foreach ($actions as $groupkey => $actiongroup) { foreach ($actiongroup as $label => $url) { $menu->add(new \action_menu_link_secondary(new \moodle_url($url), null, $label)); } if ($groupkey !== array_key_last($actions)) { $divider = new \action_menu_filler(); $divider->primary = false; $menu->add($divider); } } $renderer = $PAGE->get_renderer('core'); $data['actions'] = $renderer->render($menu); } return $data; } /** * Get the actions for the grading action menu. * * @return array A 2D array of actions grouped by a key in the form of key => label => URL. */ private function get_actions() { $actions = []; if ( has_capability('gradereport/grader:view', $this->assign->get_course_context()) && has_capability('moodle/grade:viewall', $this->assign->get_course_context()) ) { $url = new moodle_url('/grade/report/grader/index.php', ['id' => $this->assign->get_course()->id]); $actions['gradebook'][get_string('viewgradebook', 'assign')] = $url->out(false); } if ($this->assign->is_blind_marking() && has_capability('mod/assign:revealidentities', $this->assign->get_context())) { $url = new moodle_url('/mod/assign/view.php', [ 'id' => $this->assign->get_course_module()->id, 'action' => 'revealidentities', ]); $actions['blindmarking'][get_string('revealidentities', 'assign')] = $url->out(false); } foreach ($this->assign->get_feedback_plugins() as $plugin) { if ($plugin->is_enabled() && $plugin->is_visible()) { foreach ($plugin->get_grading_actions() as $action => $description) { $url = new moodle_url('/mod/assign/view.php', [ 'id' => $this->assign->get_course_module()->id, 'plugin' => $plugin->get_type(), 'pluginsubtype' => 'assignfeedback', 'action' => 'viewpluginpage', 'pluginaction' => $action, ]); $actions['assignfeedback_' . $plugin->get_type()][$description] = $url->out(false); } } } if ($this->showdownload) { $url = new moodle_url('/mod/assign/view.php', [ 'id' => $this->assign->get_course_module()->id, 'action' => 'downloadall', ]); $actions['downloadall'][get_string('downloadall', 'mod_assign')] = $url->out(false); } return $actions; } /** * Get the status menu for the grading action menu. * * @return array An array containing the status menu and the current value. */ private function get_status_menu(): array { $statusmenu = []; $currentvalue = ''; $groupedfilters = $this->assign->get_filters(true); foreach ($groupedfilters as $group => $filters) { foreach ($filters as $filter) { if ($filter['key'] === ASSIGN_FILTER_NONE) { // The 'none' filter is not a real filter. $filter['key'] = ''; } $url = new moodle_url('/mod/assign/view.php', [ 'id' => $this->assign->get_course_module()->id, 'action' => 'grading', 'status' => $filter['key'], ]); $statusmenu[$url->out(false)] = $filter['name']; if ($filter['active']) { $currentvalue = $url->out(false); } } if ($group !== array_key_last($groupedfilters)) { $statusmenu[] = ''; } } return [ 'statusmenu' => $statusmenu, 'currentvalue' => $currentvalue, ]; } /** * The renderable for the extra filters dropdown, if available. * * @return dialog|null The renderable for the extra filters dropdown, if available. */ private function get_extra_filters_dropdown(): ?dialog { global $OUTPUT; $dropdowncontentdata = [ 'actionurl' => (new moodle_url('/mod/assign/view.php'))->out(false), 'id' => $this->assign->get_course_module()->id, 'action' => 'grading', 'filters' => [], ]; // If marking workflow is enabled. if ($this->assign->get_instance()->markingworkflow) { $dropdowncontentdata['filters']['markingworkflow'] = [ 'workflowfilteroptions' => $this->assign->get_marking_workflow_filters(true), ]; // If marking allocation is enabled and the user has the capability to manage marker allocations. if ($this->assign->get_instance()->markingallocation && has_capability('mod/assign:manageallocations', $this->assign->get_context())) { $dropdowncontentdata['filters']['markingallocation'] = [ 'markingallocationoptions' => $this->assign->get_marking_allocation_filters(true), ]; } } if (has_capability('moodle/course:viewsuspendedusers', $this->assign->get_context())) { $dropdowncontentdata['filters']['suspendedparticipants'] = [ 'active' => !$this->assign->show_only_active_users(), ]; } // If there are no available filters, return null. if (empty($dropdowncontentdata['filters'])) { return null; } // Define the content output for the extra filters dropdown menu. $dropdowncontent = $OUTPUT->render_from_template( 'mod_assign/actionbar/grading/extra_filters_dropdown_body', $dropdowncontentdata ); // Define the output for the extra filters dropdown trigger. $buttoncontent = $OUTPUT->render_from_template( 'mod_assign/actionbar/grading/extra_filters_dropdown_trigger', ['appliedfilterscount' => $this->get_applied_extra_filters_count()] ); return new dialog( $buttoncontent, $dropdowncontent, [ 'classes' => 'extrafilters d-flex', 'buttonclasses' => 'btn d-flex border-none align-items-center dropdown-toggle p-0', 'autoclose' => 'outside', ] ); } /** * Returns the number of applied extra filters. * * @return int The number of applied extra filters. */ private function get_applied_extra_filters_count(): int { $appliedextrafilterscount = 0; // If marking workflow is enabled. if ($this->assign->get_instance()->markingworkflow) { if (get_user_preferences('assign_workflowfilter')) { $appliedextrafilterscount++; } $canallocatemarker = $this->assign->get_instance()->markingallocation && has_capability('mod/assign:manageallocations', $this->assign->get_context()); if ($canallocatemarker && get_user_preferences('assign_markerfilter')) { $appliedextrafilterscount++; } } // If suspended participants are included. if (!$this->assign->show_only_active_users()) { $appliedextrafilterscount++; } return $appliedextrafilterscount; } } classes/output/grading_app.php000064400000015356152265517350012552 0ustar00. /** * Renderable that initialises the grading "app". * * @package mod_assign * @copyright 2016 Damyon Wiese * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_assign\output; defined('MOODLE_INTERNAL') || die(); use renderer_base; use renderable; use templatable; use stdClass; /** * Grading app renderable. * * @package mod_assign * @since Moodle 3.1 * @copyright 2016 Damyon Wiese * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class grading_app implements templatable, renderable { /** * @var $userid - The initial user id. */ public $userid = 0; /** * @var $groupid - The initial group id. */ public $groupid = 0; /** * @var $assignment - The assignment instance. */ public $assignment = null; /** * @var array - List of user records with extra fields. */ public $participants = []; /** * Constructor for this renderable. * * @param int $userid The user we will open the grading app too. * @param int $groupid If groups are enabled this is the current course group. * @param \assign $assignment The assignment class */ public function __construct($userid, $groupid, $assignment) { $this->userid = $userid; $this->groupid = $groupid; $this->assignment = $assignment; $this->participants = $assignment->list_participants_with_filter_status_and_group($groupid); if (!$this->userid && count($this->participants)) { $this->userid = reset($this->participants)->id; } } /** * Export this class data as a flat list for rendering in a template. * * @param renderer_base $output The current page renderer. * @return stdClass - Flat list of exported data. */ public function export_for_template(renderer_base $output) { global $CFG, $USER; $export = new stdClass(); $export->userid = $this->userid; $export->assignmentid = $this->assignment->get_instance()->id; $export->cmid = $this->assignment->get_course_module()->id; $export->contextid = $this->assignment->get_context()->id; $export->groupid = $this->groupid; $export->name = $this->assignment->get_context()->get_context_name(true, false, false); $export->courseid = $this->assignment->get_course()->id; $export->participants = array(); $export->filters = $this->assignment->get_filters(); $export->markingworkflowfilters = $this->assignment->get_marking_workflow_filters(true); $export->hasmarkingworkflow = count($export->markingworkflowfilters) > 0; $export->markingallocationfilters = $this->assignment->get_marking_allocation_filters(true); $export->hasmarkingallocation = count($export->markingallocationfilters) > 0; $num = 1; foreach ($this->participants as $idx => $record) { $user = new stdClass(); $user->id = $record->id; $user->fullname = fullname($record); $user->requiregrading = $record->requiregrading; $user->grantedextension = $record->grantedextension; $user->submitted = $record->submitted; if (!empty($record->groupid)) { $user->groupid = $record->groupid; $user->groupname = $record->groupname; } if ($record->id == $this->userid) { $export->index = $num; $user->current = true; } $export->participants[] = $user; $num++; } $feedbackplugins = $this->assignment->get_feedback_plugins(); $showreview = false; foreach ($feedbackplugins as $plugin) { if ($plugin->is_enabled() && $plugin->is_visible()) { if ($plugin->supports_review_panel()) { $showreview = true; } } } $export->actiongrading = 'grading'; $export->viewgrading = get_string('viewgrading', 'mod_assign'); $export->showreview = $showreview; $time = time(); $export->count = count($export->participants); $export->coursename = $this->assignment->get_course_context()->get_context_name(true, false, false); $export->caneditsettings = has_capability('mod/assign:addinstance', $this->assignment->get_context()); $export->duedate = $this->assignment->get_instance()->duedate; $export->duedatestr = userdate($this->assignment->get_instance()->duedate); // Time remaining. $due = ''; if ($export->duedate - $time <= 0) { $due = get_string('assignmentisdue', 'assign'); } else { $due = get_string('timeremainingcolon', 'assign', format_time($export->duedate - $time)); } $export->timeremainingstr = $due; if ($export->duedate < $time) { $export->cutoffdate = $this->assignment->get_instance()->cutoffdate; $cutoffdate = $export->cutoffdate; if ($cutoffdate) { if ($cutoffdate > $time) { $late = get_string('latesubmissionsaccepted', 'assign', userdate($export->cutoffdate)); } else { $late = get_string('nomoresubmissionsaccepted', 'assign'); } $export->cutoffdatestr = $late; } } $export->defaultsendnotifications = $this->assignment->get_instance()->sendstudentnotifications; $export->rarrow = $output->rarrow(); $export->larrow = $output->larrow(); // List of identity fields to display (the user info will not contain any fields the user cannot view anyway). // TODO Does not support custom user profile fields (MDL-70456). $export->showuseridentity = implode(',', \core_user\fields::get_identity_fields(null, false)); $export->currentuserid = $USER->id; $helpicon = new \help_icon('sendstudentnotifications', 'assign'); $export->helpicon = $helpicon->export_for_template($output); return $export; } } classes/output/user_submission_actionmenu.php000064400000020127152265517350015742 0ustar00. /** * Output the user submission actionbar for this activity. * * @package mod_assign * @copyright 2021 Adrian Greeve * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_assign\output; use templatable; use renderable; use moodle_url; use help_icon; use single_button; use stdClass; /** * Output the user submission actionbar for this activity. * * @package mod_assign * @copyright 2021 Adrian Greeve * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class user_submission_actionmenu implements templatable, renderable { /** @var int The course module ID. */ protected $cmid; /** @var bool Whether to show the submit button. */ protected $showsubmit; /** @var bool Whether to show the edit button. */ protected $showedit; /** @var stdClass A submission for this activity. */ protected $submission; /** @var stdClass A team submission for this activity. */ protected $teamsubmission; /** @var int The time limit for the submission. 0 = no time limit. */ protected $timelimit; /** * Constructor for this object. * * @param int $cmid The course module ID. * @param bool $showsubmit Whether to show the submit button. * @param bool $showedit Whether to show the edit button. * @param stdClass|null $submission A submission for this activity. * @param stdClass|null $teamsubmission A team submission for this activity. * @param int $timelimit The time limit for completing this activity. */ public function __construct(int $cmid, bool $showsubmit, bool $showedit, ?stdClass $submission = null, ?stdClass $teamsubmission = null, int $timelimit = 0) { $this->cmid = $cmid; $this->showsubmit = $showsubmit; $this->showedit = $showedit; $this->submission = $submission; $this->teamsubmission = $teamsubmission; $this->timelimit = $timelimit; } /** * Get the submission status. * * @return string The status of the submission. */ protected function get_current_status(): string { if (!is_null($this->teamsubmission) && isset($this->teamsubmission->status)) { return $this->teamsubmission->status; } else if (!empty((array)$this->submission)) { return $this->submission->status; } else { return ASSIGN_SUBMISSION_STATUS_NEW; } } /** * Has the submission started. * * @return bool The status of the submission; true if started, otherwise false. */ protected function is_submission_started(): bool { return (is_object($this->teamsubmission) && isset($this->teamsubmission->timestarted)) || (is_object($this->submission) && isset($this->submission->timestarted)); } /** * Export the submission buttons for the page. * * @param \renderer_base $output renderer base output. * @return array The data to be rendered. */ public function export_for_template(\renderer_base $output): array { $data = ['edit' => false, 'submit' => false, 'remove' => false, 'previoussubmission' => false]; if ($this->showedit) { $url = new moodle_url('/mod/assign/view.php', ['id' => $this->cmid, 'action' => 'editsubmission']); $button = new single_button($url, get_string('editsubmission', 'mod_assign'), 'get'); $data['edit'] = [ 'button' => $button->export_for_template($output), ]; $status = $this->get_current_status(); if ($status !== ASSIGN_SUBMISSION_STATUS_NEW && $status !== ASSIGN_SUBMISSION_STATUS_REOPENED) { $url = new moodle_url('/mod/assign/view.php', ['id' => $this->cmid, 'action' => 'removesubmissionconfirm']); $button = new single_button($url, get_string('removesubmission', 'mod_assign'), 'get'); $data['remove'] = ['button' => $button->export_for_template($output)]; } if ($status === ASSIGN_SUBMISSION_STATUS_REOPENED) { $params = ['id' => $this->cmid, 'action' => 'editprevioussubmission', 'sesskey' => sesskey()]; $url = new moodle_url('/mod/assign/view.php', $params); $button = new single_button($url, get_string('addnewattemptfromprevious', 'mod_assign'), 'get'); $help = new help_icon('addnewattemptfromprevious', 'mod_assign'); $data['previoussubmission'] = [ 'button' => $button->export_for_template($output), 'help' => $help->export_for_template($output) ]; $url = new moodle_url('/mod/assign/view.php', ['id' => $this->cmid, 'action' => 'editsubmission']); $newattemptbutton = new single_button( $url, get_string('addnewattempt', 'mod_assign'), 'get', single_button::BUTTON_PRIMARY ); $newattempthelp = new help_icon('addnewattempt', 'mod_assign'); $data['edit']['button'] = $newattemptbutton->export_for_template($output); $data['edit']['help'] = $newattempthelp->export_for_template($output); } if ($status === ASSIGN_SUBMISSION_STATUS_NEW) { $timelimitenabled = get_config('assign', 'enabletimelimit'); if ($timelimitenabled && $this->timelimit && !$this->is_submission_started()) { $confirmation = new \confirm_action( get_string('confirmstart', 'assign', format_time($this->timelimit)), null, get_string('beginassignment', 'assign') ); // The 'begin' flag indicates that the user is starting a timed assignment. $urlparams = ['id' => $this->cmid, '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'] ); $data['edit']['button'] = $beginbutton->export_for_template($output); $data['edit']['begin'] = true; $data['edit']['help'] = ''; } else { $newattemptbutton = new single_button( $url, get_string('addsubmission', 'mod_assign'), 'get', single_button::BUTTON_PRIMARY ); $data['edit']['button'] = $newattemptbutton->export_for_template($output); $data['edit']['help'] = ''; } } } if ($this->showsubmit) { $url = new moodle_url('/mod/assign/view.php', ['id' => $this->cmid, 'action' => 'submit']); $button = new single_button($url, get_string('submitassignment', 'mod_assign'), 'get', single_button::BUTTON_PRIMARY); $help = new help_icon('submitassignment', 'mod_assign'); $data['submit'] = [ 'button' => $button->export_for_template($output), 'help' => $help->export_for_template($output) ]; } return $data; } } classes/penalty_recalculator.php000064400000003561152265517350013146 0ustar00. namespace mod_assign; use core\context; use mod_assign\task\recalculate_penalties; /** * Recalculate penalties for the assignment. * * @package mod_assign * @copyright 2025 Catalyst IT Australia Pty Ltd * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class penalty_recalculator extends \core_grades\penalty_recalculator { #[\Override] public static function recalculate_penalty(context $context, int $usermodified): void { global $CFG, $DB; require_once($CFG->dirroot . '/mod/assign/locallib.php'); switch ($context->contextlevel) { case CONTEXT_MODULE: $cmid = $context->instanceid; $cm = get_coursemodule_from_id('assign', $cmid, 0, false, MUST_EXIST); recalculate_penalties::queue($cm->instance, $usermodified); break; case CONTEXT_COURSE: $courseid = $context->instanceid; $assigns = $DB->get_records('assign', ['course' => $courseid]); foreach ($assigns as $assign) { recalculate_penalties::queue($assign->id, $usermodified); } break; } } } classes/event/submission_created.php000064400000005770152265517350013741 0ustar00. /** * The mod_assign submission_created abstract event. * * @package mod_assign * @copyright 2014 Adrian Greeve * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_assign\event; defined('MOODLE_INTERNAL') || die(); /** * The mod_assign submission_created abstract event class. * * @property-read array $other { * Extra information about the event. * * - int submissionid: ID number of this submission. * - int submissionattempt: Number of attempts made on this submission. * - string submissionstatus: Status of the submission. * - int groupid: (optional) The group ID if this is a teamsubmission. * - string groupname: (optional) The name of the group if this is a teamsubmission. * } * * @package mod_assign * @since Moodle 2.7 * @copyright 2014 Adrian Greeve * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ abstract class submission_created extends base { /** * Init method. */ protected function init() { $this->data['crud'] = 'c'; $this->data['edulevel'] = self::LEVEL_PARTICIPATING; } /** * Returns localised general event name. * * @return string */ public static function get_name() { return get_string('eventsubmissioncreated', 'mod_assign'); } /** * Custom validation. * * @throws \coding_exception * @return void */ protected function validate_data() { parent::validate_data(); if (!isset($this->other['submissionid'])) { throw new \coding_exception('The \'submissionid\' value must be set in other.'); } if (!isset($this->other['submissionattempt'])) { throw new \coding_exception('The \'submissionattempt\' value must be set in other.'); } if (!isset($this->other['submissionstatus'])) { throw new \coding_exception('The \'submissionstatus\' value must be set in other.'); } } public static function get_other_mapping() { $othermapped = array(); $othermapped['submissionid'] = array('db' => 'assign_submission', 'restore' => 'submission'); $othermapped['groupid'] = array('db' => 'groups', 'restore' => 'group'); return $othermapped; } } classes/event/extension_granted.php000064400000006536152265517350013600 0ustar00. /** * The mod_assign extension granted event. * * @package mod_assign * @copyright 2013 Frédéric Massart * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_assign\event; defined('MOODLE_INTERNAL') || die(); /** * The mod_assign extension granted event class. * * @package mod_assign * @since Moodle 2.6 * @copyright 2013 Frédéric Massart * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class extension_granted extends base { /** * Flag for prevention of direct create() call. * @var bool */ protected static $preventcreatecall = true; /** * Create instance of event. * * @since Moodle 2.7 * * @param \assign $assign * @param int $userid * @return extension_granted */ public static function create_from_assign(\assign $assign, $userid) { $data = array( 'context' => $assign->get_context(), 'objectid' => $assign->get_instance()->id, 'relateduserid' => $userid, ); self::$preventcreatecall = false; /** @var extension_granted $event */ $event = self::create($data); self::$preventcreatecall = true; $event->set_assign($assign); return $event; } /** * Returns description of what happened. * * @return string */ public function get_description() { return "The user with id '$this->userid' has granted an extension for the user with id '$this->relateduserid' " . "for the assignment with course module id '$this->contextinstanceid'."; } /** * Return localised event name. * * @return string */ public static function get_name() { return get_string('eventextensiongranted', 'mod_assign'); } /** * Init method. * * @return void */ protected function init() { $this->data['crud'] = 'u'; $this->data['edulevel'] = self::LEVEL_TEACHING; $this->data['objecttable'] = 'assign'; } /** * Custom validation. * * @throws \coding_exception * @return void */ protected function validate_data() { if (self::$preventcreatecall) { throw new \coding_exception('cannot call extension_granted::create() directly, use extension_granted::create_from_assign() instead.'); } parent::validate_data(); if (!isset($this->relateduserid)) { throw new \coding_exception('The \'relateduserid\' must be set.'); } } public static function get_objectid_mapping() { return array('db' => 'assign', 'restore' => 'assign'); } } classes/event/user_override_updated.php000064400000006262152265517350014437 0ustar00. /** * The mod_assign user override updated event. * * @package mod_assign * @copyright 2016 Ilya Tregubov * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_assign\event; defined('MOODLE_INTERNAL') || die(); /** * The mod_assign user override updated event class. * * @property-read array $other { * Extra information about event. * * - int assignid: the id of the assign. * } * * @package mod_assign * @since Moodle 3.2 * @copyright 2016 Ilya Tregubov * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class user_override_updated extends \core\event\base { /** * Init method. */ protected function init() { $this->data['objecttable'] = 'assign_overrides'; $this->data['crud'] = 'u'; $this->data['edulevel'] = self::LEVEL_TEACHING; } /** * Returns localised general event name. * * @return string */ public static function get_name() { return get_string('eventoverrideupdated', 'mod_assign'); } /** * Returns description of what happened. * * @return string */ public function get_description() { return "The user with id '$this->userid' updated the override with id '$this->objectid' for the assign with " . "course module id '$this->contextinstanceid' for the user with id '{$this->relateduserid}'."; } /** * Returns relevant URL. * * @return \moodle_url */ public function get_url() { return new \moodle_url('/mod/assign/overrideedit.php', array('id' => $this->objectid)); } /** * Custom validation. * * @throws \coding_exception * @return void */ protected function validate_data() { parent::validate_data(); if (!isset($this->relateduserid)) { throw new \coding_exception('The \'relateduserid\' must be set.'); } if (!isset($this->other['assignid'])) { throw new \coding_exception('The \'assignid\' value must be set in other.'); } } /** * Get objectid mapping */ public static function get_objectid_mapping() { return array('db' => 'assign_overrides', 'restore' => 'assign_override'); } /** * Get other mapping */ public static function get_other_mapping() { $othermapped = array(); $othermapped['assignid'] = array('db' => 'assign', 'restore' => 'assign'); return $othermapped; } } classes/event/batch_set_marker_allocation_viewed.php000064400000007104152265517350017115 0ustar00. /** * The mod_assign batch set marker allocation viewed event. * * @package mod_assign * @copyright 2014 Mark Nelson * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_assign\event; defined('MOODLE_INTERNAL') || die(); /** * The mod_assign batch set marker allocation viewed event class. * * @property-read array $other { * Extra information about event. * * - int assignid: the id of the assignment. * } * * @package mod_assign * @since Moodle 2.7 * @copyright 2014 Mark Nelson * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class batch_set_marker_allocation_viewed extends base { /** * Flag for prevention of direct create() call. * @var bool */ protected static $preventcreatecall = true; /** * Create instance of event. * * @param \assign $assign * @return batch_set_marker_allocation_viewed */ public static function create_from_assign(\assign $assign) { $data = array( 'context' => $assign->get_context(), 'other' => array( 'assignid' => $assign->get_instance()->id, ), ); self::$preventcreatecall = false; /** @var batch_set_marker_allocation_viewed $event */ $event = self::create($data); self::$preventcreatecall = true; $event->set_assign($assign); return $event; } /** * Init method. */ protected function init() { $this->data['crud'] = 'r'; $this->data['edulevel'] = self::LEVEL_TEACHING; } /** * Returns localised general event name. * * @return string */ public static function get_name() { return get_string('eventbatchsetmarkerallocationviewed', 'mod_assign'); } /** * Returns description of what happened. * * @return string */ public function get_description() { return "The user with id '$this->userid' viewed the batch set marker allocation for the assignment with course " . "module id '$this->contextinstanceid'."; } /** * Custom validation. * * @throws \coding_exception * @return void */ protected function validate_data() { if (self::$preventcreatecall) { throw new \coding_exception('cannot call batch_set_marker_allocation_viewed::create() directly, use batch_set_marker_allocation_viewed::create_from_assign() instead.'); } parent::validate_data(); if (!isset($this->other['assignid'])) { throw new \coding_exception('The \'assignid\' value must be set in other.'); } } public static function get_other_mapping() { $othermapped = array(); $othermapped['assignid'] = array('db' => 'assign', 'restore' => 'assign'); return $othermapped; } } classes/event/submission_status_updated.php000064400000007021152265517350015352 0ustar00. /** * The mod_assign submission status updated event. * * @package mod_assign * @copyright 2013 Frédéric Massart * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_assign\event; defined('MOODLE_INTERNAL') || die(); /** * The mod_assign submission status updated event class. * * @property-read array $other { * Extra information about event. * * - string newstatus: status of submission. * } * * @package mod_assign * @since Moodle 2.6 * @copyright 2013 Frédéric Massart * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class submission_status_updated extends base { /** * Create instance of event. * * @since Moodle 2.7 * * @param \assign $assign * @param \stdClass $submission * @return submission_status_updated */ public static function create_from_submission(\assign $assign, \stdClass $submission) { $data = array( 'context' => $assign->get_context(), 'objectid' => $submission->id, 'relateduserid' => ($assign->get_instance()->teamsubmission) ? null : $submission->userid, 'other' => array( 'newstatus' => $submission->status ) ); /** @var submission_status_updated $event */ $event = self::create($data); $event->set_assign($assign); $event->add_record_snapshot('assign_submission', $submission); return $event; } /** * Returns description of what happened. * * @return string */ public function get_description() { return "The user with id '$this->userid' has updated the status of the submission with id '$this->objectid' for " . "the assignment with course module id '$this->contextinstanceid' to the status '{$this->other['newstatus']}'."; } /** * Return localised event name. * * @return string */ public static function get_name() { return get_string('eventsubmissionstatusupdated', 'mod_assign'); } /** * Init method. * * @return void */ protected function init() { $this->data['crud'] = 'u'; $this->data['edulevel'] = self::LEVEL_TEACHING; $this->data['objecttable'] = 'assign_submission'; } /** * Custom validation. * * @throws \coding_exception */ protected function validate_data() { parent::validate_data(); if (!isset($this->other['newstatus'])) { throw new \coding_exception('The \'newstatus\' value must be set in other.'); } } public static function get_objectid_mapping() { return array('db' => 'assign_submission', 'restore' => 'submission'); } public static function get_other_mapping() { // Nothing to map. return false; } } classes/event/statement_accepted.php000064400000006453152265517350013712 0ustar00. /** * The mod_assign statement accepted event. * * @package mod_assign * @copyright 2013 Frédéric Massart * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_assign\event; defined('MOODLE_INTERNAL') || die(); /** * The mod_assign statement accepted event class. * * @package mod_assign * @since Moodle 2.6 * @copyright 2013 Frédéric Massart * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class statement_accepted extends base { /** * Flag for prevention of direct create() call. * @var bool */ protected static $preventcreatecall = true; /** * Create instance of event. * * @since Moodle 2.7 * * @param \assign $assign * @param \stdClass $submission * @return statement_accepted */ public static function create_from_submission(\assign $assign, \stdClass $submission) { $data = array( 'context' => $assign->get_context(), 'objectid' => $submission->id ); self::$preventcreatecall = false; /** @var statement_accepted $event */ $event = self::create($data); self::$preventcreatecall = true; $event->set_assign($assign); $event->add_record_snapshot('assign_submission', $submission); return $event; } /** * Returns description of what happened. * * @return string */ public function get_description() { return "The user with id '$this->userid' has accepted the statement of the submission with id '$this->objectid' " . "for the assignment with course module id '$this->contextinstanceid'."; } /** * Return localised event name. * * @return string */ public static function get_name() { return get_string('eventstatementaccepted', 'mod_assign'); } /** * Init method. * * @return void */ protected function init() { $this->data['crud'] = 'r'; $this->data['edulevel'] = self::LEVEL_OTHER; $this->data['objecttable'] = 'assign_submission'; } /** * Custom validation. * * @throws \coding_exception * @return void */ protected function validate_data() { if (self::$preventcreatecall) { throw new \coding_exception('cannot call statement_accepted::create() directly, use statement_accepted::create_from_submission() instead.'); } parent::validate_data(); } public static function get_objectid_mapping() { return array('db' => 'assign_submission', 'restore' => 'submission'); } } classes/event/group_override_deleted.php000064400000006511152265517350014572 0ustar00. /** * The mod_assign group override deleted event. * * @package mod_assign * @copyright 2016 Ilya Tregubov * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_assign\event; defined('MOODLE_INTERNAL') || die(); /** * The mod_assign group override deleted event class. * * @property-read array $other { * Extra information about event. * * - int assignid: the id of the assign. * - int groupid: the id of the group. * } * * @package mod_assign * @since Moodle 3.2 * @copyright 2016 Ilya Tregubov * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class group_override_deleted extends \core\event\base { /** * Init method. */ protected function init() { $this->data['objecttable'] = 'assign_overrides'; $this->data['crud'] = 'd'; $this->data['edulevel'] = self::LEVEL_TEACHING; } /** * Returns localised general event name. * * @return string */ public static function get_name() { return get_string('eventoverridedeleted', 'mod_assign'); } /** * Returns description of what happened. * * @return string */ public function get_description() { return "The user with id '$this->userid' deleted the override with id '$this->objectid' for the assign with " . "course module id '$this->contextinstanceid' for the group with id '{$this->other['groupid']}'."; } /** * Returns relevant URL. * * @return \moodle_url */ public function get_url() { return new \moodle_url('/mod/assign/overrides.php', array('cmid' => $this->contextinstanceid)); } /** * Custom validation. * * @throws \coding_exception * @return void */ protected function validate_data() { parent::validate_data(); if (!isset($this->other['assignid'])) { throw new \coding_exception('The \'assignid\' value must be set in other.'); } if (!isset($this->other['groupid'])) { throw new \coding_exception('The \'groupid\' value must be set in other.'); } } /** * Get objectid mapping */ public static function get_objectid_mapping() { return array('db' => 'assign_overrides', 'restore' => 'assign_override'); } /** * Get other mapping */ public static function get_other_mapping() { $othermapped = array(); $othermapped['assignid'] = array('db' => 'assign', 'restore' => 'assign'); $othermapped['groupid'] = array('db' => 'groups', 'restore' => 'group'); return $othermapped; } } classes/event/submission_removed.php000064400000012226152265517350013765 0ustar00. namespace mod_assign\event; use assign; use coding_exception; use stdClass; /** * The mod_assign submission removed event class. * * @property-read array $other { * Extra information about the event. * * - int submissionid: ID number of this submission. * - int submissionattempt: Number of attempts made on this submission. * - string submissionstatus: Status of the submission. * - int groupid: (optional) The group ID if this is a teamsubmission. * - string groupname: (optional) The name of the group if this is a teamsubmission. * } * * @package mod_assign * @since Moodle 4.0 * @copyright 2022 TU Berlin * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class submission_removed extends base { /** * Create instance of event. * * @param assign $assign * @param stdClass $submission * @return submission_removed * @throws coding_exception */ public static function create_from_submission(assign $assign, stdClass $submission): submission_removed { $groupname = null; $groupid = 0; if (empty($submission->userid) && !empty($submission->groupid)) { $groupname = groups_get_group_name($submission->groupid); $groupid = $submission->groupid; } $data = [ 'context' => $assign->get_context(), 'objectid' => $submission->id, 'relateduserid' => $assign->get_instance()->teamsubmission ? null : $submission->userid, 'anonymous' => $assign->is_blind_marking() ? 1 : 0, 'other' => [ 'submissionid' => $submission->id, 'submissionattempt' => $submission->attemptnumber, 'submissionstatus' => $submission->status, 'groupid' => $groupid, 'groupname' => $groupname ] ]; /** @var submission_removed $event */ $event = self::create($data); $event->set_assign($assign); $event->add_record_snapshot('assign_submission', $submission); return $event; } /** * Init method. */ protected function init() { $this->data['crud'] = 'd'; $this->data['edulevel'] = self::LEVEL_PARTICIPATING; $this->data['objecttable'] = 'assign_submission'; } /** * Returns localised general event name. * * @return string * @throws coding_exception */ public static function get_name(): string { return get_string('eventsubmissionremoved', 'mod_assign'); } /** * Returns non-localised description of what happened. * * @return string */ public function get_description(): string { $descriptionstring = "The user with id '$this->userid' removed the submission with id '$this->objectid' in " . "the assignment with course module id '$this->contextinstanceid' submitted by "; if (!empty($this->other['groupid'])) { $groupname = $this->other['groupname']; $groupid = $this->other['groupid']; $descriptionstring .= "the group '$groupname' with id '$groupid'."; } else { $descriptionstring .= "the user with id '$this->relateduserid'."; } return $descriptionstring; } /** * Custom validation. * * @return void * @throws coding_exception */ protected function validate_data() { parent::validate_data(); if (!isset($this->other['submissionid'])) { throw new coding_exception('The \'submissionid\' value must be set in other.'); } if (!isset($this->other['submissionattempt'])) { throw new coding_exception('The \'submissionattempt\' value must be set in other.'); } if (!isset($this->other['submissionstatus'])) { throw new coding_exception('The \'submissionstatus\' value must be set in other.'); } } /** * Get objectid mapping. * * @return array */ public static function get_objectid_mapping(): array { return ['db' => 'assign_submission', 'restore' => 'submission']; } /** * Get other mapping. * * @return array */ public static function get_other_mapping(): array { $othermapped = []; $othermapped['submissionid'] = ['db' => 'assign_submission', 'restore' => 'submission']; $othermapped['groupid'] = ['db' => 'groups', 'restore' => 'group']; return $othermapped; } } classes/event/feedback_viewed.php000064400000007035152265517350013142 0ustar00. /** * The mod_assign feedback viewed event. * * @package mod_assign * @copyright 2014 Mark Nelson * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_assign\event; defined('MOODLE_INTERNAL') || die(); /** * The mod_assign feedback viewed event class. * * @property-read array $other { * Extra information about event. * * - int assignid: the id of the assignment. * } * * @package mod_assign * @since Moodle 2.7 * @copyright 2014 Mark Nelson * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class feedback_viewed extends base { /** * Create instance of event. * * @param \assign $assign * @param \stdClass $grade * @return feedback_viewed */ public static function create_from_grade(\assign $assign, \stdClass $grade) { $data = array( 'objectid' => $grade->id, 'relateduserid' => $grade->userid, 'context' => $assign->get_context(), 'other' => array( 'assignid' => $assign->get_instance()->id, ), ); /** @var feedback_viewed $event */ $event = self::create($data); $event->set_assign($assign); $event->add_record_snapshot('assign_grades', $grade); return $event; } /** * Init method. */ protected function init() { $this->data['objecttable'] = 'assign_grades'; $this->data['crud'] = 'r'; $this->data['edulevel'] = self::LEVEL_PARTICIPATING; } /** * Returns localised general event name. * * @return string */ public static function get_name() { return get_string('eventfeedbackviewed', 'mod_assign'); } /** * Returns description of what happened. * * @return string */ public function get_description() { return "The user with id '$this->userid' viewed the feedback for the user with id '$this->relateduserid' " . "for the assignment with course module id '$this->contextinstanceid'."; } /** * Custom validation. * * @throws \coding_exception */ protected function validate_data() { parent::validate_data(); if (!isset($this->relateduserid)) { throw new \coding_exception('The \'relateduserid\' must be set.'); } if (!isset($this->other['assignid'])) { throw new \coding_exception('The \'assignid\' value must be set in other.'); } } public static function get_objectid_mapping() { return array('db' => 'assign_grades', 'restore' => 'grade'); } public static function get_other_mapping() { $othermapped = array(); $othermapped['assignid'] = array('db' => 'assign', 'restore' => 'assign'); return $othermapped; } } classes/event/grading_form_viewed.php000064400000007346152265517350014061 0ustar00. /** * The mod_assign grading form viewed event. * * @package mod_assign * @copyright 2014 Mark Nelson * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_assign\event; defined('MOODLE_INTERNAL') || die(); /** * The mod_assign grading form viewed event class. * * @property-read array $other { * Extra information about event. * * - int assignid: the id of the assignment. * } * * @package mod_assign * @since Moodle 2.7 * @copyright 2014 Mark Nelson * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class grading_form_viewed extends base { /** * Flag for prevention of direct create() call. * @var bool */ protected static $preventcreatecall = true; /** * Create instance of event. * * @param \assign $assign * @param \stdClass $user * @return grading_form_viewed */ public static function create_from_user(\assign $assign, \stdClass $user) { $data = array( 'relateduserid' => $user->id, 'context' => $assign->get_context(), 'other' => array( 'assignid' => $assign->get_instance()->id, ), ); self::$preventcreatecall = false; /** @var grading_form_viewed $event */ $event = self::create($data); self::$preventcreatecall = true; $event->set_assign($assign); $event->add_record_snapshot('user', $user); return $event; } /** * Init method. */ protected function init() { $this->data['crud'] = 'r'; $this->data['edulevel'] = self::LEVEL_TEACHING; } /** * Returns localised general event name. * * @return string */ public static function get_name() { return get_string('eventgradingformviewed', 'mod_assign'); } /** * Returns description of what happened. * * @return string */ public function get_description() { return "The user with id '$this->userid' viewed the grading form for the user with id '$this->relateduserid' " . "for the assignment with course module id '$this->contextinstanceid'."; } /** * Custom validation. * * @throws \coding_exception */ protected function validate_data() { if (self::$preventcreatecall) { throw new \coding_exception('cannot call grading_form_viewed::create() directly, use grading_form_viewed::create_from_user() instead.'); } parent::validate_data(); if (!isset($this->relateduserid)) { throw new \coding_exception('The \'relateduserid\' must be set.'); } if (!isset($this->other['assignid'])) { throw new \coding_exception('The \'assignid\' value must be set in other.'); } } public static function get_other_mapping() { $othermapped = array(); $othermapped['assignid'] = array('db' => 'assign', 'restore' => 'assign'); return $othermapped; } } classes/event/submission_graded.php000064400000006675152265517350013565 0ustar00. /** * The mod_assign submission graded event. * * @package mod_assign * @copyright 2013 Frédéric Massart * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_assign\event; defined('MOODLE_INTERNAL') || die(); /** * The mod_assign submission graded event class. * * @package mod_assign * @since Moodle 2.6 * @copyright 2013 Frédéric Massart * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class submission_graded extends base { /** * Flag for prevention of direct create() call. * @var bool */ protected static $preventcreatecall = true; /** * Create instance of event. * * @since Moodle 2.7 * * @param \assign $assign * @param \stdClass $grade * @return submission_graded */ public static function create_from_grade(\assign $assign, \stdClass $grade) { $data = array( 'context' => $assign->get_context(), 'objectid' => $grade->id, 'relateduserid' => $grade->userid ); self::$preventcreatecall = false; /** @var submission_graded $event */ $event = self::create($data); self::$preventcreatecall = true; $event->set_assign($assign); $event->add_record_snapshot('assign_grades', $grade); return $event; } /** * Returns description of what happened. * * @return string */ public function get_description() { return "The user with id '$this->userid' has graded the submission '$this->objectid' for the user with " . "id '$this->relateduserid' for the assignment with course module id '$this->contextinstanceid'."; } /** * Return localised event name. * * @return string */ public static function get_name() { return get_string('eventsubmissiongraded', 'mod_assign'); } /** * Init method. * * @return void */ protected function init() { $this->data['crud'] = 'u'; $this->data['edulevel'] = self::LEVEL_TEACHING; $this->data['objecttable'] = 'assign_grades'; } /** * Custom validation. * * @throws \coding_exception * @return void */ protected function validate_data() { if (self::$preventcreatecall) { throw new \coding_exception('cannot call submission_graded::create() directly, use submission_graded::create_from_grade() instead.'); } parent::validate_data(); if (!isset($this->relateduserid)) { throw new \coding_exception('The \'relateduserid\' must be set.'); } } public static function get_objectid_mapping() { return array('db' => 'assign_grades', 'restore' => 'grade'); } } classes/event/group_override_updated.php000064400000006501152265517350014611 0ustar00. /** * The mod_assign group override updated event. * * @package mod_assign * @copyright 2016 Ilya Tregubov * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_assign\event; defined('MOODLE_INTERNAL') || die(); /** * The mod_assign group override updated event class. * * @property-read array $other { * Extra information about event. * * - int assignid: the id of the assign. * - int groupid: the id of the group. * } * * @package mod_assign * @since Moodle 3.2 * @copyright 2016 Ilya Tregubov * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class group_override_updated extends \core\event\base { /** * Init method. */ protected function init() { $this->data['objecttable'] = 'assign_overrides'; $this->data['crud'] = 'u'; $this->data['edulevel'] = self::LEVEL_TEACHING; } /** * Returns localised general event name. * * @return string */ public static function get_name() { return get_string('eventoverrideupdated', 'mod_assign'); } /** * Returns description of what happened. * * @return string */ public function get_description() { return "The user with id '$this->userid' updated the override with id '$this->objectid' for the assign with " . "course module id '$this->contextinstanceid' for the group with id '{$this->other['groupid']}'."; } /** * Returns relevant URL. * * @return \moodle_url */ public function get_url() { return new \moodle_url('/mod/assign/overrideedit.php', array('id' => $this->objectid)); } /** * Custom validation. * * @throws \coding_exception * @return void */ protected function validate_data() { parent::validate_data(); if (!isset($this->other['assignid'])) { throw new \coding_exception('The \'assignid\' value must be set in other.'); } if (!isset($this->other['groupid'])) { throw new \coding_exception('The \'groupid\' value must be set in other.'); } } /** * Get objectid mapping */ public static function get_objectid_mapping() { return array('db' => 'assign_overrides', 'restore' => 'assign_override'); } /** * Get other mapping */ public static function get_other_mapping() { $othermapped = array(); $othermapped['assignid'] = array('db' => 'assign', 'restore' => 'assign'); $othermapped['groupid'] = array('db' => 'groups', 'restore' => 'group'); return $othermapped; } } classes/event/submission_unlocked.php000064400000006627152265517350014140 0ustar00. /** * The mod_assign submission unlocked event. * * @package mod_assign * @copyright 2013 Frédéric Massart * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_assign\event; defined('MOODLE_INTERNAL') || die(); /** * The mod_assign submission unlocked event class. * * @package mod_assign * @since Moodle 2.6 * @copyright 2013 Frédéric Massart * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class submission_unlocked extends base { /** * Flag for prevention of direct create() call. * @var bool */ protected static $preventcreatecall = true; /** * Create instance of event. * * @since Moodle 2.7 * * @param \assign $assign * @param \stdClass $user * @return submission_unlocked */ public static function create_from_user(\assign $assign, \stdClass $user) { $data = array( 'context' => $assign->get_context(), 'objectid' => $assign->get_instance()->id, 'relateduserid' => $user->id, ); self::$preventcreatecall = false; /** @var submission_unlocked $event */ $event = self::create($data); self::$preventcreatecall = true; $event->set_assign($assign); $event->add_record_snapshot('user', $user); return $event; } /** * Returns description of what happened. * * @return string */ public function get_description() { return "The user with id '$this->userid' unlocked the submission for the user with id '$this->relateduserid' " . "for the assignment with course module id '$this->contextinstanceid'."; } /** * Return localised event name. * * @return string */ public static function get_name() { return get_string('eventsubmissionunlocked', 'mod_assign'); } /** * Init method. * * @return void */ protected function init() { $this->data['crud'] = 'u'; $this->data['edulevel'] = self::LEVEL_TEACHING; $this->data['objecttable'] = 'assign'; } /** * Custom validation. * * @throws \coding_exception */ protected function validate_data() { if (self::$preventcreatecall) { throw new \coding_exception('cannot call submission_unlocked::create() directly, use submission_unlocked::create_from_user() instead.'); } parent::validate_data(); if (!isset($this->relateduserid)) { throw new \coding_exception('The \'relateduserid\' must be set.'); } } public static function get_objectid_mapping() { return array('db' => 'assign', 'restore' => 'assign'); } } classes/event/remove_submission_form_viewed.php000064400000010105152265517350016201 0ustar00. /** * The mod_assign remove submission form viewed event. * * @package mod_assign * @copyright 2019 Damyon Wiese * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_assign\event; defined('MOODLE_INTERNAL') || die(); /** * The mod_assign remove submission form viewed event class. * * @package mod_assign * @copyright 2019 Damyon Wiese * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class remove_submission_form_viewed extends base { /** * Flag for prevention of direct create() call. * @var bool */ protected static $preventcreatecall = true; /** * Create instance of event. * * @param \assign $assign * @param \stdClass $user * @return remove_submission_form_viewed */ public static function create_from_user(\assign $assign, \stdClass $user) { $data = array( 'relateduserid' => $user->id, 'context' => $assign->get_context(), 'other' => array( 'assignid' => $assign->get_instance()->id, ), ); self::$preventcreatecall = false; /** @var remove_submission_form_viewed $event */ $event = self::create($data); self::$preventcreatecall = true; $event->set_assign($assign); $event->add_record_snapshot('user', $user); return $event; } /** * Init method. */ protected function init() { $this->data['crud'] = 'r'; $this->data['edulevel'] = self::LEVEL_OTHER; } /** * Returns localised general event name. * * @return string */ public static function get_name() { return get_string('eventremovesubmissionformviewed', 'mod_assign'); } /** * Returns description of what happened. * * @return string */ public function get_description() { if ($this->userid != $this->relateduserid) { return "The user with id '$this->userid' viewed the " . "remove submission form for the user with id '$this->relateduserid' " . "for the assignment with course module id '$this->contextinstanceid'."; } return "The user with id '$this->userid' viewed their remove submission form for the assignment with course module id " . "'$this->contextinstanceid'."; } /** * Custom validation. * * @throws \coding_exception */ protected function validate_data() { if (self::$preventcreatecall) { throw new \coding_exception('cannot call remove_submission_form_viewed::create() directly, use ' . 'remove_submission_form_viewed::create_from_user() instead.'); } parent::validate_data(); if (!isset($this->relateduserid)) { throw new \coding_exception('The \'relateduserid\' must be set.'); } if (!isset($this->other['assignid'])) { throw new \coding_exception('The \'assignid\' value must be set in other.'); } } /** * This is documented in the parent class. * * @return array an array of other values and their corresponding mapping */ public static function get_other_mapping() { $othermapped = array(); $othermapped['assignid'] = array('db' => 'assign', 'restore' => 'assign'); return $othermapped; } } classes/event/submission_updated.php000064400000005710152265517350013752 0ustar00. /** * The mod_assign submission updated event. * * @package mod_assign * @copyright 2013 Frédéric Massart * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_assign\event; defined('MOODLE_INTERNAL') || die(); /** * The mod_assign submission updated event class. * * @property-read array $other { * Extra information about the event. * * - int submissionid: ID number of this submission. * - int submissionattempt: Number of attempts made on this submission. * - string submissionstatus: Status of the submission. * - int groupid: (optional) The group ID if this is a teamsubmission. * - string groupname: (optional) The name of the group if this is a teamsubmission. * } * * @package mod_assign * @since Moodle 2.6 * @copyright 2013 Frédéric Massart * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ abstract class submission_updated extends base { /** * Init method. */ protected function init() { $this->data['crud'] = 'u'; $this->data['edulevel'] = self::LEVEL_PARTICIPATING; } /** * Returns localised general event name. * * @return string */ public static function get_name() { return get_string('eventsubmissionupdated', 'mod_assign'); } /** * Custom validation. * * @throws \coding_exception * @return void */ protected function validate_data() { parent::validate_data(); if (!isset($this->other['submissionid'])) { throw new \coding_exception('The \'submissionid\' value must be set in other.'); } if (!isset($this->other['submissionattempt'])) { throw new \coding_exception('The \'submissionattempt\' value must be set in other.'); } if (!isset($this->other['submissionstatus'])) { throw new \coding_exception('The \'submissionstatus\' value must be set in other.'); } } public static function get_other_mapping() { $othermapped = array(); $othermapped['submissionid'] = array('db' => 'assign_submission', 'restore' => 'submission'); $othermapped['groupid'] = array('db' => 'groups', 'restore' => 'group'); return $othermapped; } } classes/event/group_override_created.php000064400000006502152265517350014573 0ustar00. /** * The mod_assign group override created event. * * @package mod_assign * @copyright 2016 Ilya Tregubov * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_assign\event; defined('MOODLE_INTERNAL') || die(); /** * The mod_assign group override created event class. * * @property-read array $other { * Extra information about event. * * - int assignid: the id of the assign. * - int groupid: the id of the group. * } * * @package mod_assign * @since Moodle 3.2 * @copyright 2016 Ilya Tregubov * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class group_override_created extends \core\event\base { /** * Init method. */ protected function init() { $this->data['objecttable'] = 'assign_overrides'; $this->data['crud'] = 'c'; $this->data['edulevel'] = self::LEVEL_TEACHING; } /** * Returns localised general event name. * * @return string */ public static function get_name() { return get_string('eventoverridecreated', 'mod_assign'); } /** * Returns description of what happened. * * @return string */ public function get_description() { return "The user with id '$this->userid' created the override with id '$this->objectid' for the assign with " . "course module id '$this->contextinstanceid' for the group with id '{$this->other['groupid']}'."; } /** * Returns relevant URL. * * @return \moodle_url */ public function get_url() { return new \moodle_url('/mod/assign/overrideedit.php', array('id' => $this->objectid)); } /** * Custom validation. * * @throws \coding_exception * @return void */ protected function validate_data() { parent::validate_data(); if (!isset($this->other['assignid'])) { throw new \coding_exception('The \'assignid\' value must be set in other.'); } if (!isset($this->other['groupid'])) { throw new \coding_exception('The \'groupid\' value must be set in other.'); } } /** * Get objectid mapping */ public static function get_objectid_mapping() { return array('db' => 'assign_overrides', 'restore' => 'assign_override'); } /** * Get other mapping */ public static function get_other_mapping() { $othermapped = array(); $othermapped['assignid'] = array('db' => 'assign', 'restore' => 'assign'); $othermapped['groupid'] = array('db' => 'groups', 'restore' => 'group'); return $othermapped; } } classes/event/course_module_viewed.php000064400000003213152265517350014255 0ustar00. /** * The mod_assign course module viewed event. * * @package mod_assign * @copyright 2018 Victor Deniz * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_assign\event; defined('MOODLE_INTERNAL') || die(); global $CFG; /** * The mod_assign course module viewed event class. * * @package mod_assign * @since Moodle 3.6 * @copyright 2018 Victor Deniz * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class course_module_viewed extends \core\event\course_module_viewed { /** * Init method. */ protected function init() { $this->data['crud'] = 'r'; $this->data['edulevel'] = self::LEVEL_PARTICIPATING; $this->data['objecttable'] = 'assign'; } /** * Get objectid mapping */ public static function get_objectid_mapping() { return array('db' => 'assign', 'restore' => 'assign'); } }classes/event/grading_table_viewed.php000064400000006710152265517350014177 0ustar00. /** * The mod_assign grading table viewed event. * * @package mod_assign * @copyright 2014 Mark Nelson * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_assign\event; defined('MOODLE_INTERNAL') || die(); /** * The mod_assign grading table viewed event class. * * @property-read array $other { * Extra information about event. * * - int assignid: the id of the assignment. * } * * @package mod_assign * @since Moodle 2.7 * @copyright 2014 Mark Nelson * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class grading_table_viewed extends base { /** * Flag for prevention of direct create() call. * @var bool */ protected static $preventcreatecall = true; /** * Create instance of event. * * @param \assign $assign * @return grading_table_viewed */ public static function create_from_assign(\assign $assign) { $data = array( 'context' => $assign->get_context(), 'other' => array( 'assignid' => $assign->get_instance()->id, ), ); self::$preventcreatecall = false; /** @var grading_table_viewed $event */ $event = self::create($data); self::$preventcreatecall = true; $event->set_assign($assign); return $event; } /** * Init method. */ protected function init() { $this->data['crud'] = 'r'; $this->data['edulevel'] = self::LEVEL_TEACHING; } /** * Returns localised general event name. * * @return string */ public static function get_name() { return get_string('eventgradingtableviewed', 'mod_assign'); } /** * Returns description of what happened. * * @return string */ public function get_description() { return "The user with id '$this->userid' viewed the grading table for the assignment with course module " . "id '$this->contextinstanceid'."; } /** * Custom validation. * * @throws \coding_exception * @return void */ protected function validate_data() { if (self::$preventcreatecall) { throw new \coding_exception('cannot call grading_table_viewed::create() directly, use grading_table_viewed::create_from_assign() instead.'); } parent::validate_data(); if (!isset($this->other['assignid'])) { throw new \coding_exception('The \'assignid\' value must be set in other.'); } } public static function get_other_mapping() { $othermapped = array(); $othermapped['assignid'] = array('db' => 'assign', 'restore' => 'assign'); return $othermapped; } } classes/event/user_override_created.php000064400000006261152265517350014417 0ustar00. /** * The mod_assign user override created event. * * @package mod_assign * @copyright 2016 Ilya Tregubov * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_assign\event; defined('MOODLE_INTERNAL') || die(); /** * The mod_assign user override created event class. * * @property-read array $other { * Extra information about event. * * - int assignid: the id of the assign. * } * * @package mod_assign * @since Moodle 3.2 * @copyright 2016 Ilya Tregubov * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class user_override_created extends \core\event\base { /** * Init method. */ protected function init() { $this->data['objecttable'] = 'assign_overrides'; $this->data['crud'] = 'c'; $this->data['edulevel'] = self::LEVEL_TEACHING; } /** * Returns localised general event name. * * @return string */ public static function get_name() { return get_string('eventoverridecreated', 'mod_assign'); } /** * Returns description of what happened. * * @return string */ public function get_description() { return "The user with id '$this->userid' created the override with id '$this->objectid' for the assign with " . "course module id '$this->contextinstanceid' for the user with id '{$this->relateduserid}'."; } /** * Returns relevant URL. * * @return \moodle_url */ public function get_url() { return new \moodle_url('/mod/assign/overrideedit.php', array('id' => $this->objectid)); } /** * Custom validation. * * @throws \coding_exception * @return void */ protected function validate_data() { parent::validate_data(); if (!isset($this->relateduserid)) { throw new \coding_exception('The \'relateduserid\' must be set.'); } if (!isset($this->other['assignid'])) { throw new \coding_exception('The \'assignid\' value must be set in other.'); } } /** * Get objectid mapping */ public static function get_objectid_mapping() { return array('db' => 'assign_overrides', 'restore' => 'assign_override'); } /** * Get other mapping */ public static function get_other_mapping() { $othermapped = array(); $othermapped['assignid'] = array('db' => 'assign', 'restore' => 'assign'); return $othermapped; } } classes/event/assessable_submitted.php000064400000007142152265517350014257 0ustar00. /** * The mod_assign assessable submitted event. * * @package mod_assign * @copyright 2013 Frédéric Massart * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_assign\event; defined('MOODLE_INTERNAL') || die(); /** * The mod_assign assessable submitted event class. * * @property-read array $other { * Extra information about event. * * - bool submission_editable: is submission editable. * } * * @package mod_assign * @since Moodle 2.6 * @copyright 2013 Frédéric Massart * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class assessable_submitted extends base { /** * Create instance of event. * * @since Moodle 2.7 * * @param \assign $assign * @param \stdClass $submission * @param bool $editable * @return assessable_submitted */ public static function create_from_submission(\assign $assign, \stdClass $submission, $editable) { global $USER; $data = array( 'context' => $assign->get_context(), 'objectid' => $submission->id, 'other' => array( 'submission_editable' => $editable, ), ); if (!empty($submission->userid) && ($submission->userid != $USER->id)) { $data['relateduserid'] = $submission->userid; } /** @var assessable_submitted $event */ $event = self::create($data); $event->set_assign($assign); $event->add_record_snapshot('assign_submission', $submission); return $event; } /** * Returns description of what happened. * * @return string */ public function get_description() { return "The user with id '$this->userid' has submitted the submission with id '$this->objectid' " . "for the assignment with course module id '$this->contextinstanceid'."; } /** * Return localised event name. * * @return string */ public static function get_name() { return get_string('eventassessablesubmitted', 'mod_assign'); } /** * Init method. * * @return void */ protected function init() { $this->data['objecttable'] = 'assign_submission'; $this->data['crud'] = 'u'; $this->data['edulevel'] = self::LEVEL_PARTICIPATING; } /** * Custom validation. * * @throws \coding_exception * @return void */ protected function validate_data() { parent::validate_data(); if (!isset($this->other['submission_editable'])) { throw new \coding_exception('The \'submission_editable\' value must be set in other.'); } } public static function get_objectid_mapping() { return array('db' => 'assign_submission', 'restore' => 'submission'); } public static function get_other_mapping() { // Nothing to map. return false; } } classes/event/reveal_identities_confirmation_page_viewed.php000064400000007176152265517350020667 0ustar00. /** * The mod_assign reveal identities confirmation page viewed event. * * @package mod_assign * @copyright 2014 Mark Nelson * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_assign\event; defined('MOODLE_INTERNAL') || die(); /** * The mod_assign reveal identities confirmation page viewed event class. * * @property-read array $other { * Extra information about event. * * - int assignid: the id of the assignment. * } * * @package mod_assign * @since Moodle 2.7 * @copyright 2014 Mark Nelson * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class reveal_identities_confirmation_page_viewed extends base { /** * Flag for prevention of direct create() call. * @var bool */ protected static $preventcreatecall = true; /** * Create instance of event. * * @param \assign $assign * @return reveal_identities_confirmation_page_viewed */ public static function create_from_assign(\assign $assign) { $data = array( 'context' => $assign->get_context(), 'other' => array( 'assignid' => $assign->get_instance()->id, ), ); self::$preventcreatecall = false; /** @var reveal_identities_confirmation_page_viewed $event */ $event = self::create($data); self::$preventcreatecall = true; $event->set_assign($assign); return $event; } /** * Init method. */ protected function init() { $this->data['crud'] = 'r'; $this->data['edulevel'] = self::LEVEL_TEACHING; } /** * Returns localised general event name. * * @return string */ public static function get_name() { return get_string('eventrevealidentitiesconfirmationpageviewed', 'mod_assign'); } /** * Returns description of what happened. * * @return string */ public function get_description() { return "The user with id '$this->userid' viewed the confirmation page for revealing identities for the " . "assignment with course module id '$this->contextinstanceid'."; } /** * Custom validation. * * @throws \coding_exception */ protected function validate_data() { if (self::$preventcreatecall) { throw new \coding_exception('cannot call reveal_identities_confirmation_page_viewed::create() directly, use reveal_identities_confirmation_page_viewed::create_from_grade() instead.'); } parent::validate_data(); if (!isset($this->other['assignid'])) { throw new \coding_exception('The \'assignid\' value must be set in other.'); } } public static function get_other_mapping() { $othermapped = array(); $othermapped['assignid'] = array('db' => 'assign', 'restore' => 'assign'); return $othermapped; } } classes/event/submission_viewed.php000064400000007136152265517350013613 0ustar00. /** * The mod_assign submission viewed event. * * @package mod_assign * @copyright 2014 Mark Nelson * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_assign\event; defined('MOODLE_INTERNAL') || die(); /** * The mod_assign submission viewed event class. * * @property-read array $other { * Extra information about the event. * * - int assignid: the id of the assignment. * } * * @package mod_assign * @since Moodle 2.7 * @copyright 2014 Mark Nelson * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class submission_viewed extends base { /** * Create instance of event. * * @param \assign $assign * @param \stdClass $submission * @return submission_viewed */ public static function create_from_submission(\assign $assign, \stdClass $submission) { $data = array( 'objectid' => $submission->id, 'relateduserid' => $submission->userid, 'context' => $assign->get_context(), 'other' => array( 'assignid' => $assign->get_instance()->id, ), ); /** @var submission_viewed $event */ $event = self::create($data); $event->set_assign($assign); $event->add_record_snapshot('assign_submission', $submission); return $event; } /** * Init method. */ protected function init() { $this->data['objecttable'] = 'assign_submission'; $this->data['crud'] = 'r'; $this->data['edulevel'] = self::LEVEL_PARTICIPATING; } /** * Returns localised general event name. * * @return string */ public static function get_name() { return get_string('eventsubmissionviewed', 'mod_assign'); } /** * Returns description of what happened. * * @return string */ public function get_description() { return "The user with id '$this->userid' viewed the submission for the user with id '$this->relateduserid' for the " . "assignment with course module id '$this->contextinstanceid'."; } /** * Custom validation. * * @throws \coding_exception */ protected function validate_data() { parent::validate_data(); if (!isset($this->relateduserid)) { throw new \coding_exception('The \'relateduserid\' must be set.'); } if (!isset($this->other['assignid'])) { throw new \coding_exception('The \'assignid\' value must be set in other.'); } } public static function get_objectid_mapping() { return array('db' => 'assign_submission', 'restore' => 'submission'); } public static function get_other_mapping() { $othermapped = array(); $othermapped['assignid'] = array('db' => 'assign', 'restore' => 'assign'); return $othermapped; } } classes/event/user_override_deleted.php000064400000006271152265517350014417 0ustar00. /** * The mod_assign user override deleted event. * * @package mod_assign * @copyright 2016 Ilya Tregubov * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_assign\event; defined('MOODLE_INTERNAL') || die(); /** * The mod_assign user override deleted event class. * * @property-read array $other { * Extra information about event. * * - int assignid: the id of the assign. * } * * @package mod_assign * @since Moodle 3.2 * @copyright 2016 Ilya Tregubov * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class user_override_deleted extends \core\event\base { /** * Init method. */ protected function init() { $this->data['objecttable'] = 'assign_overrides'; $this->data['crud'] = 'd'; $this->data['edulevel'] = self::LEVEL_TEACHING; } /** * Returns localised general event name. * * @return string */ public static function get_name() { return get_string('eventoverridedeleted', 'mod_assign'); } /** * Returns description of what happened. * * @return string */ public function get_description() { return "The user with id '$this->userid' deleted the override with id '$this->objectid' for the assign with " . "course module id '$this->contextinstanceid' for the user with id '{$this->relateduserid}'."; } /** * Returns relevant URL. * * @return \moodle_url */ public function get_url() { return new \moodle_url('/mod/assign/overrides.php', array('cmid' => $this->contextinstanceid)); } /** * Custom validation. * * @throws \coding_exception * @return void */ protected function validate_data() { parent::validate_data(); if (!isset($this->relateduserid)) { throw new \coding_exception('The \'relateduserid\' must be set.'); } if (!isset($this->other['assignid'])) { throw new \coding_exception('The \'assignid\' value must be set in other.'); } } /** * Get objectid mapping */ public static function get_objectid_mapping() { return array('db' => 'assign_overrides', 'restore' => 'assign_override'); } /** * Get other mapping */ public static function get_other_mapping() { $othermapped = array(); $othermapped['assignid'] = array('db' => 'assign', 'restore' => 'assign'); return $othermapped; } } classes/event/submission_locked.php000064400000006605152265517350013571 0ustar00. /** * The mod_assign submission locked event. * * @package mod_assign * @copyright 2013 Frédéric Massart * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_assign\event; defined('MOODLE_INTERNAL') || die(); /** * The mod_assign submission locked event class. * * @package mod_assign * @since Moodle 2.6 * @copyright 2013 Frédéric Massart * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class submission_locked extends base { /** * Flag for prevention of direct create() call. * @var bool */ protected static $preventcreatecall = true; /** * Create instance of event. * * @since Moodle 2.7 * * @param \assign $assign * @param \stdClass $user * @return submission_locked */ public static function create_from_user(\assign $assign, \stdClass $user) { $data = array( 'context' => $assign->get_context(), 'objectid' => $assign->get_instance()->id, 'relateduserid' => $user->id, ); self::$preventcreatecall = false; /** @var submission_locked $event */ $event = self::create($data); self::$preventcreatecall = true; $event->set_assign($assign); $event->add_record_snapshot('user', $user); return $event; } /** * Returns description of what happened. * * @return string */ public function get_description() { return "The user with id '$this->userid' locked the submission for the user with id '$this->relateduserid' for " . "the assignment with course module id '$this->contextinstanceid'."; } /** * Return localised event name. * * @return string */ public static function get_name() { return get_string('eventsubmissionlocked', 'mod_assign'); } /** * Init method. * * @return void */ protected function init() { $this->data['crud'] = 'u'; $this->data['edulevel'] = self::LEVEL_TEACHING; $this->data['objecttable'] = 'assign'; } /** * Custom validation. * * @throws \coding_exception */ protected function validate_data() { if (self::$preventcreatecall) { throw new \coding_exception('cannot call submission_locked::create() directly, use submission_locked::create_from_user() instead.'); } parent::validate_data(); if (!isset($this->relateduserid)) { throw new \coding_exception('The \'relateduserid\' must be set.'); } } public static function get_objectid_mapping() { return array('db' => 'assign', 'restore' => 'assign'); } } classes/event/submission_duplicated.php000064400000006447152265517350014452 0ustar00. /** * The mod_assign submission duplicated event. * * @package mod_assign * @copyright 2013 Frédéric Massart * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_assign\event; defined('MOODLE_INTERNAL') || die(); /** * The mod_assign submission duplicated event class. * * @package mod_assign * @since Moodle 2.6 * @copyright 2013 Frédéric Massart * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class submission_duplicated extends base { /** * Flag for prevention of direct create() call. * @var bool */ protected static $preventcreatecall = true; /** * Create instance of event. * * @since Moodle 2.7 * * @param \assign $assign * @param \stdClass $submission * @return submission_duplicated */ public static function create_from_submission(\assign $assign, \stdClass $submission) { $data = array( 'objectid' => $submission->id, 'context' => $assign->get_context(), ); self::$preventcreatecall = false; /** @var submission_duplicated $event */ $event = self::create($data); self::$preventcreatecall = true; $event->set_assign($assign); $event->add_record_snapshot('assign_submission', $submission); return $event; } /** * Returns description of what happened. * * @return string */ public function get_description() { return "The user with id '$this->userid' duplicated their submission with id '$this->objectid' for the " . "assignment with course module id '$this->contextinstanceid'."; } /** * Return localised event name. * * @return string */ public static function get_name() { return get_string('eventsubmissionduplicated', 'mod_assign'); } /** * Init method. * * @return void */ protected function init() { $this->data['crud'] = 'c'; $this->data['edulevel'] = self::LEVEL_PARTICIPATING; $this->data['objecttable'] = 'assign_submission'; } /** * Custom validation. * * @throws \coding_exception */ protected function validate_data() { if (self::$preventcreatecall) { throw new \coding_exception('cannot call submission_duplicated::create() directly, use submission_duplicated::create_from_submission() instead.'); } parent::validate_data(); } public static function get_objectid_mapping() { return array('db' => 'assign_submission', 'restore' => 'submission'); } } classes/event/all_submissions_downloaded.php000064400000006250152265517350015457 0ustar00. /** * The mod_assign all submissions downloaded event. * * @package mod_assign * @copyright 2013 Frédéric Massart * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_assign\event; defined('MOODLE_INTERNAL') || die(); /** * The mod_assign all submissions downloaded event class. * * @package mod_assign * @since Moodle 2.6 * @copyright 2013 Frédéric Massart * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class all_submissions_downloaded extends base { /** * Flag for prevention of direct create() call. * @var bool */ protected static $preventcreatecall = true; /** * Create instance of event. * * @since Moodle 2.7 * * @param \assign $assign * @return all_submissions_downloaded */ public static function create_from_assign(\assign $assign) { $data = array( 'context' => $assign->get_context(), 'objectid' => $assign->get_instance()->id ); self::$preventcreatecall = false; /** @var submission_graded $event */ $event = self::create($data); self::$preventcreatecall = true; $event->set_assign($assign); return $event; } /** * Returns description of what happened. * * @return string */ public function get_description() { return "The user with id '$this->userid' has downloaded all the submissions for the assignment " . "with course module id '$this->contextinstanceid'."; } /** * Return localised event name. * * @return string */ public static function get_name() { return get_string('eventallsubmissionsdownloaded', 'mod_assign'); } /** * Init method. * * @return void */ protected function init() { $this->data['crud'] = 'r'; $this->data['edulevel'] = self::LEVEL_TEACHING; $this->data['objecttable'] = 'assign'; } /** * Custom validation. * * @throws \coding_exception * @return void */ protected function validate_data() { if (self::$preventcreatecall) { throw new \coding_exception('cannot call all_submissions_downloaded::create() directly, use all_submissions_downloaded::create_from_assign() instead.'); } parent::validate_data(); } public static function get_objectid_mapping() { return array('db' => 'assign', 'restore' => 'assign'); } } classes/event/submission_confirmation_form_viewed.php000064400000007061152265517350017403 0ustar00. /** * The mod_assign submission form viewed event. * * @package mod_assign * @copyright 2014 Mark Nelson * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_assign\event; defined('MOODLE_INTERNAL') || die(); /** * The mod_assign submission form viewed event class. * * @property-read array $other { * Extra information about event. * * - int assignid: the id of the assignment. * } * * @package mod_assign * @since Moodle 2.7 * @copyright 2014 Mark Nelson * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class submission_confirmation_form_viewed extends base { /** * Flag for prevention of direct create() call. * @var bool */ protected static $preventcreatecall = true; /** * Create instance of event. * * @param \assign $assign * @return submission_confirmation_form_viewed */ public static function create_from_assign(\assign $assign) { $data = array( 'context' => $assign->get_context(), 'other' => array( 'assignid' => $assign->get_instance()->id, ), ); self::$preventcreatecall = false; /** @var submission_confirmation_form_viewed $event */ $event = self::create($data); self::$preventcreatecall = true; $event->set_assign($assign); return $event; } /** * Init method. */ protected function init() { $this->data['crud'] = 'r'; $this->data['edulevel'] = self::LEVEL_OTHER; } /** * Returns localised general event name. * * @return string */ public static function get_name() { return get_string('eventsubmissionconfirmationformviewed', 'mod_assign'); } /** * Returns description of what happened. * * @return string */ public function get_description() { return "The user with id '$this->userid' viewed the submission confirmation form for the assignment with " . "course module id '$this->contextinstanceid'."; } /** * Custom validation. * * @throws \coding_exception * @return void */ protected function validate_data() { if (self::$preventcreatecall) { throw new \coding_exception('cannot call submission_confirmation_form_viewed::create() directly, use submission_confirmation_form_viewed::create_from_assign() instead.'); } parent::validate_data(); if (!isset($this->other['assignid'])) { throw new \coding_exception('The \'assignid\' value must be set in other.'); } } public static function get_other_mapping() { $othermapped = array(); $othermapped['assignid'] = array('db' => 'assign', 'restore' => 'assign'); return $othermapped; } } classes/event/marker_updated.php000064400000010046152265517350013036 0ustar00. /** * The mod_assign marker updated event. * * @package mod_assign * @copyright 2013 Frédéric Massart * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_assign\event; defined('MOODLE_INTERNAL') || die(); /** * The mod_assign marker updated event class. * * @property-read array $other { * Extra information about event. * * - int markerid: user id of marker. * } * * @package mod_assign * @since Moodle 2.6 * @copyright 2013 Frédéric Massart * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class marker_updated extends base { /** * Flag for prevention of direct create() call. * @var bool */ protected static $preventcreatecall = true; /** * Create instance of event. * * @since Moodle 2.7 * * @param \assign $assign * @param \stdClass $user * @param \stdClass $marker * @return marker_updated */ public static function create_from_marker(\assign $assign, \stdClass $user, \stdClass $marker) { $data = array( 'context' => $assign->get_context(), 'objectid' => $assign->get_instance()->id, 'relateduserid' => $user->id, 'other' => array( 'markerid' => $marker->id, ), ); self::$preventcreatecall = false; /** @var marker_updated $event */ $event = self::create($data); self::$preventcreatecall = true; $event->set_assign($assign); $event->add_record_snapshot('user', $user); $event->add_record_snapshot('user', $marker); return $event; } /** * Returns description of what happened. * * @return string */ public function get_description() { return "The user with id '$this->userid' has set the marker for the user with id '$this->relateduserid' to " . "'{$this->other['markerid']}' for the assignment with course module id '$this->contextinstanceid'."; } /** * Return localised event name. * * @return string */ public static function get_name() { return get_string('eventmarkerupdated', 'mod_assign'); } /** * Init method. * * @return void */ protected function init() { $this->data['crud'] = 'u'; $this->data['edulevel'] = self::LEVEL_TEACHING; $this->data['objecttable'] = 'assign'; } /** * Custom validation. * * @throws \coding_exception */ protected function validate_data() { if (self::$preventcreatecall) { throw new \coding_exception('cannot call marker_updated::create() directly, use marker_updated::create_from_marker() instead.'); } parent::validate_data(); if (!isset($this->relateduserid)) { throw new \coding_exception('The \'relateduserid\' must be set.'); } if (!isset($this->other['markerid'])) { throw new \coding_exception('The \'markerid\' value must be set in other.'); } } public static function get_objectid_mapping() { return array('db' => 'assign', 'restore' => 'assign'); } public static function get_other_mapping() { $othermapped = array(); $othermapped['markerid'] = array('db' => 'user', 'restore' => 'user'); return $othermapped; } } classes/event/batch_set_workflow_state_viewed.php000064400000007065152265517350016507 0ustar00. /** * The mod_assign assignment batch set workflow stated viewed event. * * @package mod_assign * @copyright 2014 Mark Nelson * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_assign\event; defined('MOODLE_INTERNAL') || die(); /** * The mod_assign assignment batch set workflow stated viewed event. * * @property-read array $other { * Extra information about event. * * - int assignid: the id of the assignment. * } * * @package mod_assign * @since Moodle 2.7 * @copyright 2014 Mark Nelson * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class batch_set_workflow_state_viewed extends base { /** * Flag for prevention of direct create() call. * @var bool */ protected static $preventcreatecall = true; /** * Create instance of event. * * @param \assign $assign * @return batch_set_workflow_state_viewed */ public static function create_from_assign(\assign $assign) { $data = array( 'context' => $assign->get_context(), 'other' => array( 'assignid' => $assign->get_instance()->id, ), ); self::$preventcreatecall = false; /** @var batch_set_workflow_state_viewed $event */ $event = self::create($data); self::$preventcreatecall = true; $event->set_assign($assign); return $event; } /** * Init method. */ protected function init() { $this->data['crud'] = 'r'; $this->data['edulevel'] = self::LEVEL_TEACHING; } /** * Returns localised general event name. * * @return string */ public static function get_name() { return get_string('eventbatchsetworkflowstateviewed', 'mod_assign'); } /** * Returns description of what happened. * * @return string */ public function get_description() { return "The user with id '$this->userid' viewed the batch set workflow for the assignment with course " . "module id '$this->contextinstanceid'."; } /** * Custom validation. * * @throws \coding_exception * @return void */ protected function validate_data() { if (self::$preventcreatecall) { throw new \coding_exception('cannot call batch_set_workflow_state_viewed::create() directly, use batch_set_workflow_state_viewed::create_from_assign() instead.'); } parent::validate_data(); if (!isset($this->other['assignid'])) { throw new \coding_exception('The \'assignid\' value must be set in other.'); } } public static function get_other_mapping() { $othermapped = array(); $othermapped['assignid'] = array('db' => 'assign', 'restore' => 'assign'); return $othermapped; } } classes/event/submission_form_viewed.php000064400000007747152265517350014646 0ustar00. /** * The mod_assign submission form viewed event. * * @package mod_assign * @copyright 2014 Mark Nelson * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_assign\event; defined('MOODLE_INTERNAL') || die(); /** * The mod_assign submission form viewed event class. * * @property-read array $other { * Extra information about event. * * - int assignid: the id of the assignment. * } * * @package mod_assign * @since Moodle 2.7 * @copyright 2014 Mark Nelson * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class submission_form_viewed extends base { /** * Flag for prevention of direct create() call. * @var bool */ protected static $preventcreatecall = true; /** * Create instance of event. * * @param \assign $assign * @param \stdClass $user * @return submission_form_viewed */ public static function create_from_user(\assign $assign, \stdClass $user) { $data = array( 'relateduserid' => $user->id, 'context' => $assign->get_context(), 'other' => array( 'assignid' => $assign->get_instance()->id, ), ); self::$preventcreatecall = false; /** @var submission_form_viewed $event */ $event = self::create($data); self::$preventcreatecall = true; $event->set_assign($assign); $event->add_record_snapshot('user', $user); return $event; } /** * Init method. */ protected function init() { $this->data['crud'] = 'r'; $this->data['edulevel'] = self::LEVEL_OTHER; } /** * Returns localised general event name. * * @return string */ public static function get_name() { return get_string('eventsubmissionformviewed', 'mod_assign'); } /** * Returns description of what happened. * * @return string */ public function get_description() { if ($this->userid != $this->relateduserid) { return "The user with id '$this->userid' viewed the submission form for the user with id '$this->relateduserid' " . "for the assignment with course module id '$this->contextinstanceid'."; } return "The user with id '$this->userid' viewed their submission for the assignment with course module id " . "'$this->contextinstanceid'."; } /** * Custom validation. * * @throws \coding_exception */ protected function validate_data() { if (self::$preventcreatecall) { throw new \coding_exception('cannot call submission_form_viewed::create() directly, use submission_form_viewed::create_from_user() instead.'); } parent::validate_data(); if (!isset($this->relateduserid)) { throw new \coding_exception('The \'relateduserid\' must be set.'); } if (!isset($this->other['assignid'])) { throw new \coding_exception('The \'assignid\' value must be set in other.'); } } public static function get_other_mapping() { $othermapped = array(); $othermapped['assignid'] = array('db' => 'assign', 'restore' => 'assign'); return $othermapped; } } classes/event/identities_revealed.php000064400000006156152265517350014066 0ustar00. /** * The mod_assign identities revealed event. * * @package mod_assign * @copyright 2013 Frédéric Massart * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_assign\event; defined('MOODLE_INTERNAL') || die(); /** * The mod_assign identities revealed event class. * * @package mod_assign * @since Moodle 2.6 * @copyright 2013 Frédéric Massart * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class identities_revealed extends base { /** * Flag for prevention of direct create() call. * @var bool */ protected static $preventcreatecall = true; /** * Create instance of event. * * @since Moodle 2.7 * * @param \assign $assign * @return identities_revealed */ public static function create_from_assign(\assign $assign) { $data = array( 'context' => $assign->get_context(), 'objectid' => $assign->get_instance()->id ); self::$preventcreatecall = false; /** @var identities_revealed $event */ $event = self::create($data); self::$preventcreatecall = true; $event->set_assign($assign); return $event; } /** * Returns description of what happened. * * @return string */ public function get_description() { return "The user with id '$this->userid' has revealed identities in the assignment with course module " . "id '$this->contextinstanceid'."; } /** * Return localised event name. * * @return string */ public static function get_name() { return get_string('eventidentitiesrevealed', 'mod_assign'); } /** * Init method. * * @return void */ protected function init() { $this->data['crud'] = 'u'; $this->data['edulevel'] = self::LEVEL_TEACHING; $this->data['objecttable'] = 'assign'; } /** * Custom validation. * * @throws \coding_exception * @return void */ protected function validate_data() { if (self::$preventcreatecall) { throw new \coding_exception('cannot call identities_revealed::create() directly, use identities_revealed::create_from_assign() instead.'); } parent::validate_data(); } public static function get_objectid_mapping() { return array('db' => 'assign', 'restore' => 'assign'); } } classes/event/submission_status_viewed.php000064400000007004152265517350015210 0ustar00. /** * The mod_assign submission status viewed event. * * @package mod_assign * @copyright 2014 Mark Nelson * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_assign\event; defined('MOODLE_INTERNAL') || die(); /** * The mod_assign submission status viewed event class. * * @property-read array $other { * Extra information about event. * * - int assignid: the id of the assignment. * } * * @package mod_assign * @since Moodle 2.7 * @copyright 2014 Mark Nelson * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class submission_status_viewed extends base { /** * Flag for prevention of direct create() call. * @var bool */ protected static $preventcreatecall = true; /** * Create instance of event. * * @param \assign $assign * @return submission_status_viewed */ public static function create_from_assign(\assign $assign) { $data = array( 'context' => $assign->get_context(), 'other' => array( 'assignid' => $assign->get_instance()->id, ), ); self::$preventcreatecall = false; /** @var submission_status_viewed $event */ $event = self::create($data); self::$preventcreatecall = true; $event->set_assign($assign); return $event; } /** * Init method. * * @return void */ protected function init() { $this->data['crud'] = 'r'; $this->data['edulevel'] = self::LEVEL_OTHER; } /** * Return localised event name. * * @return string */ public static function get_name() { return get_string('eventsubmissionstatusviewed', 'mod_assign'); } /** * Returns description of what happened. * * @return string */ public function get_description() { return "The user with id '$this->userid' has viewed the submission status page for the assignment with " . "course module id '$this->contextinstanceid'."; } /** * Custom validation. * * @throws \coding_exception * @return void */ protected function validate_data() { if (self::$preventcreatecall) { throw new \coding_exception('cannot call submission_status_viewed::create() directly, use submission_status_viewed::create_from_assign() instead.'); } parent::validate_data(); if (!isset($this->other['assignid'])) { throw new \coding_exception('The \'assignid\' value must be set in other.'); } } public static function get_other_mapping() { $othermapped = array(); $othermapped['assignid'] = array('db' => 'assign', 'restore' => 'assign'); return $othermapped; } } classes/event/workflow_state_updated.php000064400000007711152265517350014634 0ustar00. /** * mod_assign workflow state updated event. * * @package mod_assign * @copyright 2013 Frédéric Massart * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_assign\event; defined('MOODLE_INTERNAL') || die(); /** * mod_assign workflow state updated event class. * * @property-read array $other { * Extra information about event. * * - string newstate: state of submission. * } * * @package mod_assign * @since Moodle 2.6 * @copyright 2013 Frédéric Massart * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class workflow_state_updated extends base { /** * Flag for prevention of direct create() call. * @var bool */ protected static $preventcreatecall = true; /** * Create instance of event. * * @since Moodle 2.7 * * @param \assign $assign * @param \stdClass $user * @param string $state * @return workflow_state_updated */ public static function create_from_user(\assign $assign, \stdClass $user, $state) { $data = array( 'context' => $assign->get_context(), 'objectid' => $assign->get_instance()->id, 'relateduserid' => $user->id, 'other' => array( 'newstate' => $state, ), ); self::$preventcreatecall = false; /** @var workflow_state_updated $event */ $event = self::create($data); self::$preventcreatecall = true; $event->set_assign($assign); $event->add_record_snapshot('user', $user); return $event; } /** * Returns description of what happened. * * @return string */ public function get_description() { return "The user with id '$this->userid' has set the workflow state of the user with id '$this->relateduserid' " . "to the state '{$this->other['newstate']}' for the assignment with course module id '$this->contextinstanceid'."; } /** * Return localised event name. * * @return string */ public static function get_name() { return get_string('eventworkflowstateupdated', 'mod_assign'); } /** * Init method. * * @return void */ protected function init() { $this->data['crud'] = 'u'; $this->data['edulevel'] = self::LEVEL_TEACHING; $this->data['objecttable'] = 'assign'; } /** * Custom validation. * * @throws \coding_exception */ protected function validate_data() { if (self::$preventcreatecall) { throw new \coding_exception('cannot call workflow_state_updated::create() directly, use workflow_state_updated::create_from_user() instead.'); } parent::validate_data(); if (!isset($this->relateduserid)) { throw new \coding_exception('The \'relateduserid\' must be set.'); } if (!isset($this->other['newstate'])) { throw new \coding_exception('The \'newstate\' value must be set in other.'); } } public static function get_objectid_mapping() { return array('db' => 'assign', 'restore' => 'assign'); } public static function get_other_mapping() { // Nothing to map. return false; } } classes/event/base.php000064400000006444152265517350010770 0ustar00. /** * The mod_assign abstract base event. * * @package mod_assign * @copyright 2014 Mark Nelson * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_assign\event; defined('MOODLE_INTERNAL') || die(); /** * The mod_assign abstract base event class. * * Most mod_assign events can extend this class. * * @package mod_assign * @since Moodle 2.7 * @copyright 2014 Mark Nelson * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ abstract class base extends \core\event\base { /** @var \assign */ protected $assign; /** * Legacy log data. * * @var array */ protected $legacylogdata; /** * Set assign instance for this event. * @param \assign $assign * @throws \coding_exception */ public function set_assign(\assign $assign) { if ($this->is_triggered()) { throw new \coding_exception('set_assign() must be done before triggerring of event'); } if ($assign->get_context()->id != $this->get_context()->id) { throw new \coding_exception('Invalid assign isntance supplied!'); } if ($assign->is_blind_marking()) { $this->data['anonymous'] = 1; } $this->assign = $assign; } /** * Get assign instance. * * NOTE: to be used from observers only. * * @throws \coding_exception * @return \assign */ public function get_assign() { if ($this->is_restored()) { throw new \coding_exception('get_assign() is intended for event observers only'); } if (!isset($this->assign)) { debugging('assign property should be initialised in each event', DEBUG_DEVELOPER); global $CFG; require_once($CFG->dirroot . '/mod/assign/locallib.php'); $cm = get_coursemodule_from_id('assign', $this->contextinstanceid, 0, false, MUST_EXIST); $course = get_course($cm->course); $this->assign = new \assign($this->get_context(), $cm, $course); } return $this->assign; } /** * Returns relevant URL. * * @return \moodle_url */ public function get_url() { return new \moodle_url('/mod/assign/view.php', array('id' => $this->contextinstanceid)); } /** * Custom validation. * * @throws \coding_exception */ protected function validate_data() { parent::validate_data(); if ($this->contextlevel != CONTEXT_MODULE) { throw new \coding_exception('Context level must be CONTEXT_MODULE.'); } } } classes/event/course_module_instance_list_viewed.php000064400000003377152265517350017207 0ustar00. /** * The mod_assign instance list viewed event. * * @package mod_assign * @copyright 2014 Mark Nelson * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_assign\event; defined('MOODLE_INTERNAL') || die(); /** * The mod_assign instance list viewed event class. * * @package mod_assign * @since Moodle 2.7 * @copyright 2014 Mark Nelson * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class course_module_instance_list_viewed extends \core\event\course_module_instance_list_viewed { /** * Create the event from course record. * * @param \stdClass $course * @return course_module_instance_list_viewed */ public static function create_from_course(\stdClass $course) { $params = array( 'context' => \context_course::instance($course->id) ); $event = \mod_assign\event\course_module_instance_list_viewed::create($params); $event->add_record_snapshot('course', $course); return $event; } } classes/task/cron_task.php000064400000004372152265517350011660 0ustar00. namespace mod_assign\task; defined('MOODLE_INTERNAL') || die(); /** * A schedule task for assignment cron. * * @package mod_assign * @copyright 2019 Simey Lameze * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class cron_task extends \core\task\scheduled_task { /** * Get a descriptive name for this task (shown to admins). * * @return string */ public function get_name() { return get_string('crontask', 'mod_assign'); } /** * Run assignment cron. */ public function execute() { global $CFG; require_once($CFG->dirroot . '/mod/assign/locallib.php'); \assign::cron(); $plugins = \core_component::get_plugin_list('assignsubmission'); foreach ($plugins as $name => $plugin) { $disabled = get_config('assignsubmission_' . $name, 'disabled'); if (!$disabled) { $class = 'assign_submission_' . $name; require_once($CFG->dirroot . '/mod/assign/submission/' . $name . '/locallib.php'); $class::cron(); } } $plugins = \core_component::get_plugin_list('assignfeedback'); foreach ($plugins as $name => $plugin) { $disabled = get_config('assignfeedback_' . $name, 'disabled'); if (!$disabled) { $class = 'assign_feedback_' . $name; require_once($CFG->dirroot . '/mod/assign/feedback/' . $name . '/locallib.php'); $class::cron(); } } return true; } } classes/task/queue_assignment_due_soon_notification_tasks_for_users.php000064400000003354152265517350023205 0ustar00. namespace mod_assign\task; use core\task\adhoc_task; use mod_assign\notification_helper; /** * Ad-hoc task to queue another task for notifying a user about an approaching due date. * * @package mod_assign * @copyright 2024 David Woloszyn * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class queue_assignment_due_soon_notification_tasks_for_users extends adhoc_task { /** * Execute the task. */ public function execute(): void { $assignmentid = $this->get_custom_data()->id; $type = notification_helper::TYPE_DUE_SOON; $users = notification_helper::get_users_within_assignment($assignmentid, $type); foreach ($users as $user) { $task = new send_assignment_due_soon_notification_to_user(); $task->set_custom_data([ 'assignmentid' => $assignmentid, 'userid' => $user->id, ]); $task->set_userid($user->id); \core\task\manager::queue_adhoc_task($task, true); } } } classes/task/queue_assignment_overdue_notification_tasks_for_users.php000064400000003347152265517350023045 0ustar00. namespace mod_assign\task; use core\task\adhoc_task; use mod_assign\notification_helper; /** * Ad-hoc task to queue another task for notifying a user about an overdue assignment. * * @package mod_assign * @copyright 2024 David Woloszyn * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class queue_assignment_overdue_notification_tasks_for_users extends adhoc_task { /** * Execute the task. */ public function execute(): void { $assignmentid = $this->get_custom_data()->id; $type = notification_helper::TYPE_OVERDUE; $users = notification_helper::get_users_within_assignment($assignmentid, $type); foreach ($users as $user) { $task = new send_assignment_overdue_notification_to_user(); $task->set_custom_data([ 'assignmentid' => $assignmentid, 'userid' => $user->id, ]); $task->set_userid($user->id); \core\task\manager::queue_adhoc_task($task, true); } } } classes/task/queue_all_assignment_due_soon_notification_tasks.php000064400000003377152265517350021753 0ustar00. namespace mod_assign\task; use core\task\scheduled_task; use mod_assign\notification_helper; /** * Scheduled task to queue tasks for notifying about assignments with an approaching due date. * * @package mod_assign * @copyright 2024 David Woloszyn * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class queue_all_assignment_due_soon_notification_tasks extends scheduled_task { /** * Return the task name. * * @return string The name of the task. */ public function get_name(): string { return get_string('sendnotificationduedatesoon', 'mod_assign'); } /** * Execute the task. */ public function execute(): void { $assignments = notification_helper::get_due_soon_assignments(); foreach ($assignments as $assignment) { $task = new queue_assignment_due_soon_notification_tasks_for_users(); $task->set_custom_data($assignment); \core\task\manager::queue_adhoc_task($task, true); } $assignments->close(); } } classes/task/recalculate_penalties.php000064400000003733152265517350014225 0ustar00. namespace mod_assign\task; use core\task\adhoc_task; /** * Ad-hoc task to recalculate penalties for users in an assignment. * * @package mod_assign * @copyright 2024 David Woloszyn * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class recalculate_penalties extends adhoc_task { #[\Override] public function execute(): void { global $CFG, $DB; require_once($CFG->dirroot . '/mod/assign/lib.php'); require_once($CFG->dirroot . '/course/lib.php'); $assignid = $this->get_custom_data()->assignid; $assign = $DB->get_record('assign', ['id' => $assignid], '*', MUST_EXIST); $cm = get_coursemodule_from_instance('assign', $assignid, 0, false, MUST_EXIST); $assign->cmidnumber = $cm->idnumber; assign_update_grades($assign); } /** * Queue the task. * * @param int $assignid assignment id * @param int $usermodified user who triggered the recalculation */ public static function queue(int $assignid, int $usermodified): void { $task = new self(); $task->set_custom_data((object) [ 'assignid' => $assignid, 'usermodified' => $usermodified, ]); \core\task\manager::queue_adhoc_task($task); } } classes/task/send_assignment_overdue_notification_to_user.php000064400000002562152265517350021114 0ustar00. namespace mod_assign\task; use core\task\adhoc_task; use mod_assign\notification_helper; /** * Ad-hoc task to send a notification to a user about an overdue assignment. * * @package mod_assign * @copyright 2024 David Woloszyn * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class send_assignment_overdue_notification_to_user extends adhoc_task { /** * Execute the task. */ public function execute(): void { $assignmentid = $this->get_custom_data()->assignmentid; $userid = $this->get_custom_data()->userid; notification_helper::send_overdue_notification_to_user($assignmentid, $userid); } } classes/task/send_assignment_due_soon_notification_to_user.php000064400000002566152265517350021262 0ustar00. namespace mod_assign\task; use core\task\adhoc_task; use mod_assign\notification_helper; /** * Ad-hoc task to send a notification to a user about an approaching due date. * * @package mod_assign * @copyright 2024 David Woloszyn * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class send_assignment_due_soon_notification_to_user extends adhoc_task { /** * Execute the task. */ public function execute(): void { $assignmentid = $this->get_custom_data()->assignmentid; $userid = $this->get_custom_data()->userid; notification_helper::send_due_soon_notification_to_user($assignmentid, $userid); } } classes/task/queue_all_assignment_due_digest_notification_tasks.php000064400000004145152265517350022246 0ustar00. namespace mod_assign\task; use core\task\scheduled_task; use mod_assign\notification_helper; /** * Scheduled task to queue tasks for notifying about assignments that are due in 7 days. * * @package mod_assign * @copyright 2024 David Woloszyn * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class queue_all_assignment_due_digest_notification_tasks extends scheduled_task { /** * Return the task name. * * @return string The name of the task. */ public function get_name(): string { return get_string('sendnotificationduedigest', 'mod_assign'); } /** * Execute the task. */ public function execute(): void { // Get all our assignments and the users within them. $assignments = notification_helper::get_due_digest_assignments(); $type = notification_helper::TYPE_DUE_DIGEST; $users = []; foreach ($assignments as $assignment) { $newusers = notification_helper::get_users_within_assignment($assignment->id, $type); $users = array_replace($users, $newusers); } $assignments->close(); // Queue a task for each user. foreach ($users as $user) { $task = new send_assignment_due_digest_notification_to_user(); $task->set_userid($user->id); \core\task\manager::queue_adhoc_task($task, true); } } } classes/task/send_assignment_due_digest_notification_to_user.php000064400000002416152265517350021555 0ustar00. namespace mod_assign\task; use core\task\adhoc_task; use mod_assign\notification_helper; /** * Ad-hoc task to send a notification to a user about assignments that are due in 7 days. * * @package mod_assign * @copyright 2024 David Woloszyn * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class send_assignment_due_digest_notification_to_user extends adhoc_task { /** * Execute the task. */ public function execute(): void { notification_helper::send_due_digest_notification_to_user($this->get_userid()); } } classes/task/queue_all_assignment_overdue_notification_tasks.php000064400000003360152265517350021601 0ustar00. namespace mod_assign\task; use core\task\scheduled_task; use mod_assign\notification_helper; /** * Scheduled task to queue tasks for notifying about assignments that are now overdue. * * @package mod_assign * @copyright 2024 David Woloszyn * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class queue_all_assignment_overdue_notification_tasks extends scheduled_task { /** * Return the task name. * * @return string The name of the task. */ public function get_name(): string { return get_string('sendnotificationoverdue', 'mod_assign'); } /** * Execute the task. */ public function execute(): void { $assignments = notification_helper::get_overdue_assignments(); foreach ($assignments as $assignment) { $task = new queue_assignment_overdue_notification_tasks_for_users(); $task->set_custom_data($assignment); \core\task\manager::queue_adhoc_task($task, true); } $assignments->close(); } } classes/completion/custom_completion.php000064400000005701152265517350014644 0ustar00. declare(strict_types=1); namespace mod_assign\completion; use core_completion\activity_custom_completion; /** * Activity custom completion subclass for the assign activity. * * Class for defining mod_assign's custom completion rules and fetching the completion statuses * of the custom completion rules for a given assign instance and a user. * * @package mod_assign * @copyright Simey Lameze * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class custom_completion extends activity_custom_completion { /** * Fetches the completion state for a given completion rule. * * @param string $rule The completion rule. * @return int The completion state. */ public function get_state(string $rule): int { global $CFG; $this->validate_rule($rule); $userid = $this->userid; $cm = $this->cm; require_once($CFG->dirroot . '/mod/assign/locallib.php'); $assign = new \assign(null, $cm, $cm->get_course()); if ($assign->get_instance()->teamsubmission) { $submission = $assign->get_group_submission($userid, 0, false); } else { $submission = $assign->get_user_submission($userid, false); } $status = $submission && $submission->status == ASSIGN_SUBMISSION_STATUS_SUBMITTED; return $status ? COMPLETION_COMPLETE : COMPLETION_INCOMPLETE; } /** * Fetch the list of custom completion rules that this module defines. * * @return array */ public static function get_defined_custom_rules(): array { return ['completionsubmit']; } /** * Returns an associative array of the descriptions of custom completion rules. * * @return array */ public function get_custom_rule_descriptions(): array { return [ 'completionsubmit' => get_string('completiondetail:submit', 'assign') ]; } /** * Returns an array of all completion rules, in the order they should be displayed to users. * * @return array */ public function get_sort_order(): array { return [ 'completionview', 'completionsubmit', 'completionusegrade', 'completionpassgrade', ]; } } classes/analytics/indicator/cognitive_depth.php000064400000004340152265517350016044 0ustar00. /** * Cognitive depth indicator - assign. * * @package mod_assign * @copyright 2017 David Monllao {@link http://www.davidmonllao.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_assign\analytics\indicator; defined('MOODLE_INTERNAL') || die(); /** * Cognitive depth indicator - assign. * * @package mod_assign * @copyright 2017 David Monllao {@link http://www.davidmonllao.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class cognitive_depth extends activity_base { /** * Returns the name. * * If there is a corresponding '_help' string this will be shown as well. * * @return \lang_string */ public static function get_name(): \lang_string { return new \lang_string('indicator:cognitivedepth', 'mod_assign'); } public function get_indicator_type() { return self::INDICATOR_COGNITIVE; } public function get_cognitive_depth_level(\cm_info $cm) { return self::COGNITIVE_LEVEL_5; } /** * feedback_submitted_events * * @return string[] */ protected function feedback_submitted_events() { return array('\mod_assign\event\assessable_submitted'); } /** * feedback_replied * * @param \cm_info $cm * @param int $contextid * @param int $userid * @param int $after * @return bool */ protected function feedback_replied(\cm_info $cm, $contextid, $userid, $after = false) { // No level 4. return false; } } classes/analytics/indicator/activity_base.php000064400000003574152265517350015527 0ustar00. /** * Activity base class. * * @package mod_assign * @copyright 2017 onwards Ankit Agarwal * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_assign\analytics\indicator; defined('MOODLE_INTERNAL') || die(); /** * Activity base class. * * @package mod_assign * @copyright 2017 onwards Ankit Agarwal * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ abstract class activity_base extends \core_analytics\local\indicator\community_of_inquiry_activity { /** * feedback_viewed_events * * @return string[] */ protected function feedback_viewed_events() { return array('\mod_assign\event\feedback_viewed'); } /** * feedback_check_grades * * @return bool */ protected function feedback_check_grades() { // We need the grade to be released to the student to consider that feedback has been provided. return true; } /** * Returns the name of the field that controls activity availability. * * @return null|string */ protected function get_timeclose_field() { return 'duedate'; } } classes/analytics/indicator/social_breadth.php000064400000003311152265517350015631 0ustar00. /** * Social breadth indicator - assign. * * @package mod_assign * @copyright 2017 David Monllao {@link http://www.davidmonllao.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_assign\analytics\indicator; defined('MOODLE_INTERNAL') || die(); /** * Social breadth indicator - assign. * * @package mod_assign * @copyright 2017 David Monllao {@link http://www.davidmonllao.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class social_breadth extends activity_base { /** * Returns the name. * * If there is a corresponding '_help' string this will be shown as well. * * @return \lang_string */ public static function get_name(): \lang_string { return new \lang_string('indicator:socialbreadth', 'mod_assign'); } public function get_indicator_type() { return self::INDICATOR_SOCIAL; } public function get_social_breadth_level(\cm_info $cm) { return self::SOCIAL_LEVEL_2; } } classes/cache/overrides.php000064400000006726152265517350012005 0ustar00. /** * Cache data source for the assign overrides. * * @package mod_assign * @copyright 2021 Shamim Rezaie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ declare(strict_types=1); namespace mod_assign\cache; use core_cache\data_source_interface; use core_cache\definition; /** * Class assign_overrides * * @package mod_assign * @copyright 2021 Shamim Rezaie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class overrides implements data_source_interface { /** @var overrides the singleton instance of this class. */ protected static $instance = null; /** * Returns an instance of the data source class that the cache can use for loading data using the other methods * specified by this interface. * * @param definition $definition * @return object */ public static function get_instance_for_cache(definition $definition): overrides { if (is_null(self::$instance)) { self::$instance = new overrides(); } return self::$instance; } /** * Loads the data for the key provided ready formatted for caching. * * @param string|int $key The key to load. * @return mixed What ever data should be returned, or false if it can't be loaded. * @throws \coding_exception */ public function load_for_cache($key) { global $DB; [$assignid, $ug, $ugid] = explode('_', $key); $assignid = (int) $assignid; switch ($ug) { case 'u': $userid = (int) $ugid; $override = $DB->get_record( 'assign_overrides', ['assignid' => $assignid, 'userid' => $userid], 'duedate, cutoffdate, allowsubmissionsfromdate' ); break; case 'g': $groupid = (int) $ugid; $override = $DB->get_record( 'assign_overrides', ['assignid' => $assignid, 'groupid' => $groupid], 'sortorder, duedate, cutoffdate, allowsubmissionsfromdate' ); break; default: throw new \coding_exception('Invalid cache key'); } // Return null instead of false, because false will not be cached. return $override ?: null; } /** * Loads several keys for the cache. * * @param array $keys An array of keys each of which will be string|int. * @return array An array of matching data items. */ public function load_many_for_cache(array $keys) { $results = []; foreach ($keys as $key) { $results[] = $this->load_for_cache($key); } return $results; } } classes/courseformat/overview.php000064400000014217152265517350013311 0ustar00. namespace mod_assign\courseformat; use assign; use cm_info; use core_courseformat\local\overview\overviewitem; use core\output\action_link; use core\output\local\properties\text_align; use core\output\local\properties\button; use core\url; use mod_assign\dates; /** * Assignment overview integration. * * @package mod_assign * @copyright 2025 Ferran Recio * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class overview extends \core_courseformat\activityoverviewbase { /** @var assign $assign the assign instance. */ private assign $assign; /** * Constructor. * * @param cm_info $cm the course module instance. * @param \core\output\renderer_helper $rendererhelper the renderer helper. */ public function __construct( cm_info $cm, /** @var \core\output\renderer_helper $rendererhelper the renderer helper */ protected readonly \core\output\renderer_helper $rendererhelper, ) { global $CFG; require_once($CFG->dirroot . '/mod/assign/locallib.php'); parent::__construct($cm); $this->assign = new assign($this->context, $this->cm, $this->cm->get_course()); } #[\Override] public function get_due_date_overview(): ?overviewitem { global $USER; $dates = new dates($this->cm, $USER->id); $duedate = $dates->get_due_date(); if (empty($duedate)) { return new overviewitem( name: get_string('duedate', 'assign'), value: null, content: '-', ); } $content = userdate($duedate); return new overviewitem( name: get_string('duedate', 'assign'), value: $duedate, content: $content, ); } #[\Override] public function get_actions_overview(): ?overviewitem { if (!has_capability('mod/assign:grade', $this->context)) { return null; } $needgrading = $this->assign->count_submissions_need_grading(); $alertlabel = get_string('numberofsubmissionsneedgrading', 'assign'); $name = get_string('gradeverb'); $badge = ''; if ($needgrading > 0) { $renderer = $this->rendererhelper->get_core_renderer(); $badge = $renderer->notice_badge( contents: $needgrading, title: $alertlabel, ); } $content = new action_link( url: new url('/mod/assign/view.php', ['id' => $this->cm->id, 'action' => 'grading']), text: $name . $badge, attributes: ['class' => button::SECONDARY_OUTLINE->classes()], ); return new overviewitem( name: get_string('actions'), value: $name, content: $content, textalign: text_align::CENTER, alertcount: $needgrading, alertlabel: $alertlabel, ); } #[\Override] public function get_extra_overview_items(): array { return [ 'submissions' => $this->get_extra_submissions_overview(), 'submissionstatus' => $this->get_extra_submission_status_overview(), ]; } /** * Retrieves an overview of submissions for the assignment. * * @return overviewitem|null An overview item c, or null if the user lacks the required capability. */ private function get_extra_submissions_overview(): ?overviewitem { global $USER; if (!has_capability('mod/assign:grade', $this->cm->context)) { return null; } $activitygroup = groups_get_activity_group($this->cm); $submissions = $this->assign->count_submissions_with_status(ASSIGN_SUBMISSION_STATUS_SUBMITTED); $total = $this->assign->count_participants($activitygroup); $content = new action_link( url: new url('/mod/assign/view.php', ['id' => $this->cm->id, 'action' => 'grading']), text: get_string( 'count_of_total', 'core', ['count' => $submissions, 'total' => $total] ), attributes: ['class' => button::SECONDARY_OUTLINE->classes()], ); return new overviewitem( name: get_string('submissions', 'assign'), value: $submissions, content: $content, textalign: text_align::CENTER, ); } /** * Retrieves the submission status overview for the current user. * * @return overviewitem|null The overview item, or null if the user does not have the required capabilities. */ private function get_extra_submission_status_overview(): ?overviewitem { global $USER; if (!has_capability('mod/assign:submit', $this->context, $USER, false)) { return null; } if ($this->assign->get_instance()->teamsubmission) { $usersubmission = $this->assign->get_group_submission($USER->id, 0, false); } else { $usersubmission = $this->assign->get_user_submission($USER->id, false); } if (!empty($usersubmission->status)) { $submittedstatus = get_string('submissionstatus_' . $usersubmission->status, 'assign'); } else { $submittedstatus = get_string('submissionstatus_', 'assign'); } return new overviewitem( name: get_string('submissionstatus', 'assign'), value: $submittedstatus, content: $submittedstatus, ); } } templates/grading_navigation_user_info.mustache000064400000002312152265517350016211 0ustar00{{! This file is part of Moodle - http://moodle.org/ Moodle is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Moodle is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Moodle. If not, see . }} {{! @template mod_assign/grading_navigation_user_info Initial template for the user info panel - until a user is selected a loading spinner will be displayed. Classes required for JS: * none Data attributes required for JS: * None Context variables required for this template: * None Example context (json): { } }} {{> mod_assign/loading }} {{#js}} require(['mod_assign/grading_navigation_user_info'], function(UserInfo) { new UserInfo('[data-region="user-info"]'); }); {{/js}} templates/actionbar/grading/extra_filters_dropdown_trigger.mustache000064400000002322152265517350022176 0ustar00{{! This file is part of Moodle - http://moodle.org/ Moodle is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Moodle is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Moodle. If not, see . }} {{! @template mod_assign/actionbar/grading/extra_filters_dropdown_trigger Template for the trigger of the extra filters dropdown menu. Context variables required for this template: * appliedfilterscount - The number of applied extra filters Example context (json): { "appliedfilterscount": 2 } }}
{{#str}} advanced, core {{/str}} {{#appliedfilterscount}} +{{.}} {{/appliedfilterscount}}
templates/actionbar/grading/extra_filters_dropdown_body.mustache000064400000012241152265517350021471 0ustar00{{! This file is part of Moodle - http://moodle.org/ Moodle is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Moodle is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Moodle. If not, see . }} {{! @template mod_assign/actionbar/grading/extra_filters_dropdown_body Template for the body of the extra filters dropdown menu. Context variables required for this template: * actionurl - Form action URL * id - Course module id * filters - Array of filter options * markingworkflow * workflowfilteroptions - Array of workflow filter options * key - The key of the workflow filter option * name - The name of the workflow filter option * active - Whether the workflow filter option is currently active * markingallocation * markingallocationoptions - Array of marking allocation filter options * key - The key of the marking allocation filter option * name - The name of the marking allocation filter option * active - Whether the marking allocation filter option is currently active * suspendedparticipants * active - Whether the suspended participants filter is currently active Example context (json): { "actionurl": "http://moodlesite/mod/assign/view.php", "id": 3, "action": "grading", "filters": [ { "markingworkflow": { "workflowfilteroptions": [ { "key": "notmarked", "name": "Not marked", "active": true } ] }, "markingallocation": { "markingallocationoptions": [ { "key": "-1", "name": "No marker", "active": true } ] }, "suspendedparticipants": { "active": true } } ] } }}
{{#filters}} {{#markingworkflow}}
{{/markingworkflow}} {{#markingallocation}}
{{/markingallocation}} {{#suspendedparticipants}}
{{/suspendedparticipants}} {{/filters}}
templates/grading_paging_selector.mustache000064400000003301152265517350015145 0ustar00{{! This file is part of Moodle - http://moodle.org/ Moodle is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Moodle is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Moodle. If not, see . }} {{! @template mod_assign/grading_paging_selector Template for pagination dropdown on the assignment grading page. Example context (json): { "baseurl": "http://example.com/grading.php?id=1", "options": [ {"name": "10", "value": "10", "selected": false}, {"name": "20", "value": "20", "selected": false}, {"name": "50", "value": "50", "selected": true}, {"name": "All", "value": "-1", "selected": false} ] } }} {{#js}} document.getElementById('{{uniqid}}-perpage').addEventListener('change', function(e) { var url = new URL('{{{baseurl}}}'); url.searchParams.set('perpage', e.target.value); window.location.href = url; }); {{/js}} templates/override_form_user_defaults.mustache000064400000003300152265517350016073 0ustar00{{! This file is part of Moodle - http://moodle.org/ Moodle is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Moodle is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Moodle. If not, see . }} {{! @template mod_assign/override_form_user_defaults HTML for showing user default dates in override form. }}
{{#str}} allowsubmissionsfromdate, mod_assign {{/str}}
{{#allowsubmissionsfromdate}} {{#userdate}} {{.}}, {{#str}} strftimedatetime, core_langconfig {{/str}} {{/userdate}} {{/allowsubmissionsfromdate}} {{^allowsubmissionsfromdate}} {{#str}} disabled, mod_assign {{/str}} {{/allowsubmissionsfromdate}}
{{#str}} duedate, mod_assign {{/str}}
{{#duedate}} {{#userdate}} {{.}}, {{#str}} strftimedatetime, core_langconfig {{/str}} {{/userdate}} {{/duedate}} {{^duedate}} {{#str}} disabled, mod_assign {{/str}} {{/duedate}}
{{#str}} cutoffdate, mod_assign {{/str}}
{{#cutoffdate}} {{#userdate}} {{cutoffdate}}, {{#str}} strftimedatetime, core_langconfig {{/str}} {{/userdate}} {{/cutoffdate}} {{^cutoffdate}} {{#str}} disabled, mod_assign {{/str}} {{/cutoffdate}}
templates/loading.mustache000064400000002035152265517350011725 0ustar00{{! This file is part of Moodle - http://moodle.org/ Moodle is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Moodle is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Moodle. If not, see . }} {{! @template mod_assign/loading Loading spinner shown while waiting for an ajax request Classes required for JS: * None Data attibutes required for JS: * None Context variables required for this template: * None Example context (json): { } }} {{#pix}}y/loading, core, {{#str}}loading, mod_assign{{/str}}{{/pix}} templates/user_submission_actionmenu.mustache000064400000013510152265517350015763 0ustar00{{! This file is part of Moodle - http://moodle.org/ Moodle is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Moodle is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Moodle. If not, see . }} {{! @template mod_assign/user_submission_actionmenu Actions for editing and submitting assignments. Classes required for JS: * none Context variables required for this template: * see mod/assign/classes/output/user_submission_actionmenu.php Example context (json): { "submit": { "button": { "method" : "get", "url" : "#", "formid": "submit_button_1", "primary" : true, "tooltip" : "Submit tooltip", "label" : "Submit", "attributes": [ { "name": "data-attribute", "value": "yeah" } ] }, "help": { "title": "Help with something", "url": "http://example.org/help", "linktext": "", "icon":{ "attributes": [ {"name": "class", "value": ""}, {"name": "src", "value": "../../../pix/help.svg"}, {"name": "alt", "value": "Help icon"} ] } } }, "previoussubmission": { "button": { "method" : "get", "url" : "#", "formid": "previous_button_2", "primary" : true, "tooltip" : "Submit tooltip", "label" : "Previous submission", "attributes": [ { "name": "data-attribute", "value": "yeah" } ] }, "help": { "title": "Help with something", "url": "http://example.org/help", "linktext": "", "icon":{ "attributes": [ {"name": "class", "value": ""}, {"name": "src", "value": "../../../pix/help.svg"}, {"name": "alt", "value": "Help icon"} ] } } }, "edit": { "button": { "method" : "get", "url" : "#", "formid": "edit_button_3", "primary" : true, "tooltip" : "Submit tooltip", "label" : "Edit", "attributes": [ { "name": "data-attribute", "value": "yeah" } ] }, "help": { "title": "Help with something", "url": "http://example.org/help", "linktext": "", "icon":{ "attributes": [ {"name": "class", "value": ""}, {"name": "src", "value": "../../../pix/help.svg"}, {"name": "alt", "value": "Help icon"} ] } } }, "remove": { "button": { "method" : "get", "url" : "#", "formid": "remove_button_4", "primary" : true, "tooltip" : "Submit tooltip", "label" : "Remove", "attributes": [ { "name": "data-attribute", "value": "yeah" } ] }, "help": { "title": "Help with something", "url": "http://example.org/help", "linktext": "", "icon":{ "attributes": [ {"name": "class", "value": ""}, {"name": "src", "value": "../../../pix/help.svg"}, {"name": "alt", "value": "Help icon"} ] } } } } }}
{{#submit}}
{{#button}} {{>core/single_button}} {{/button}} {{#help}} {{>core/help_icon}} {{/help}}
{{/submit}} {{#previoussubmission}}
{{#button}} {{>core/single_button}} {{/button}} {{#help}} {{>core/help_icon}} {{/help}}
{{/previoussubmission}} {{#edit}}
{{#begin}} {{#button}} {{>core/action_link}} {{/button}} {{/begin}} {{^begin}} {{#button}} {{>core/single_button}} {{/button}} {{#help}} {{>core/help_icon}} {{/help}} {{/begin}}
{{/edit}} {{#remove}}
{{#button}} {{>core/single_button}} {{/button}}
{{/remove}}
templates/grading_navigation_no_users.mustache000064400000002060152265517350016055 0ustar00{{! This file is part of Moodle - http://moodle.org/ Moodle is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Moodle is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Moodle. If not, see . }} {{! @template mod_assign/grading_navigation_no_users Notification when no users are available (given the current filters). Classes required for JS: * none Data attributes required for JS: * None Context variables required for this template: * None Example context (json): { } }}

{{#str}}nousersselected, mod_assign{{/str}}

templates/messages/notification_text.mustache000064400000005463152265517350015661 0ustar00{{! This file is part of Moodle - http://moodle.org/ Moodle is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Moodle is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Moodle. If not, see . }} {{! @template mod_assign/messages/notification_text This template is used for the plain text version of messages sent from the assignment module. Note: much more is passed to this template than might be needed, but this is to give flexibility to themes. Classes required for JS: * none Context variables required for this template: * messagetext - the main content of the message. Normally comes from a lang string. * username - full name of the user who performed the action leading to the message, but will be changed to 'Participant 123' if blink marking is in use. * recipentname - full name of the user the email is being sent to. (Never anonymised.) * assignment - The name of this assignment. * url - The URL of this assignment activity. * assigncmid - Course-module id of this assignment. * courseshortname - short name of the course this assignment is in. * coursefullname - full name of the course this assignment is in. * courseurl - URL of the course page. * courseassignsurl - URL of the paeg listing all assignments in the course. * timeupdated - Time the action that triggered this message was performed, formatted for display. Example context (json): { "messagehtml": "Your assignment for Assignment 1 has been submitted. You can check its status at https://qa.moodledemo.net/mod/assign/view.php?id=5.", "username": "Trusty Teacher", "recipientname": "Smart Student", "assignment": "Assignment 1", "url": "https://qa.moodledemo.net/mod/assign/view.php?id=5", "assigncmid": 5, "courseshortname": "A100", "coursefullname": "Assessment for beginners", "courseurl": "https://qa.moodledemo.net/course/view.php?id=2", "courseassignsurl": "https://qa.moodledemo.net/mod/assign/index.php?id=2", "timeupdated": "Fri, 14 Mar 2025, 11:08 PM" } }} {{{courseshortname}}} -> {{#str}}modulename, mod_assign{{/str}} -> {{{assignment}}} --------------------------------------------------------------------- {{{messagetext}}} --------------------------------------------------------------------- templates/messages/notification_html.mustache000064400000005623152265517350015637 0ustar00{{! This file is part of Moodle - http://moodle.org/ Moodle is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Moodle is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Moodle. If not, see . }} {{! @template mod_assign/messages/notification_html This template is used for the HTML version of messages sent from the assignment module. Note: much more is passed to this template than might be needed, but this is to give flexibility to themes. Classes required for JS: * none Context variables required for this template: * messagehtml - the main content of the message. Normally comes from a lang string. * username - full name of the user who performed the action leading to the message, but will be changed to 'Participant 123' if blink marking is in use. * recipentname - full name of the user the email is being sent to. (Never anonymised.) * assignment - The name of this assignment. * url - The URL of this assignment activity. * assigncmid - Course-module id of this assignment. * courseshortname - short name of the course this assignment is in. * coursefullname - full name of the course this assignment is in. * courseurl - URL of the course page. * courseassignsurl - URL of the paeg listing all assignments in the course. * timeupdated - Time the action that triggered this message was performed, formatted for display. Example context (json): { "messagehtml": "

Your assignment for Assignment 1 has been submitted. You can check its status on the assignment page.

", "username": "Trusty Teacher", "recipientname": "Smart Student", "assignment": "Assignment 1", "url": "https://qa.moodledemo.net/mod/assign/view.php?id=5", "assigncmid": 5, "courseshortname": "A100", "coursefullname": "Assessment for beginners", "courseurl": "https://qa.moodledemo.net/course/view.php?id=2", "courseassignsurl": "https://qa.moodledemo.net/mod/assign/index.php?id=2", "timeupdated": "Fri, 14 Mar 2025, 11:08 PM" } }}

{{{courseshortname}}} -> {{#str}}modulename, mod_assign{{/str}} -> {{{assignment}}}


{{{messagehtml}}}


templates/grading_actionmenu.mustache000064400000016024152265517350014150 0ustar00{{! This file is part of Moodle - http://moodle.org/ Moodle is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Moodle is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Moodle. If not, see . }} {{! @template mod_assign/grading_actionmenu Actions panel for the assignment grading UI. Classes required for JS: * none Context variables required for this template: * userselector - HTML that outputs the user selector * groupselector - (optional) HTML that outputs the group selector * statusselector - (optional) The data object containing the required properties to render the status selector * extrafiltersdropdown - HTML that outputs the extra filters dropdown * pagereset - (optional) URL to reset the page * graderurl - (optional) URL to the grader page * quickgrading - (optional) Includes the baseurl and enabled properties for the quick grading checkbox * downloadasfolders - (optional) Includes the baseurl and enabled properties for the download as folders checkbox * actions - (optional) HTML that outputs the bulk action menu Example context (json): { "userselector": {"user selector": "context here"}, "groupselector": {"group selector": "context here"}, "initialselector": {"initial selector": "context here"}, "statusselector": { "name": "filter", "value": "submitted", "baseid": "select-menu56789", "label": "Status", "inlinelabel": true, "selectedoption": "Not submitted", "options": [ {"name": "All", "value": "", "selected": false, "id": "select-menu-option56789a"}, {"name": "Not submitted", "value": "notsubmitted", "selected": true, "id": "select-menu-option56789b"}, {"isdivider": true}, {"name": "granted extention", "value": "grantedextension", "selected": false, "id": "select-menu-option56789d"} ] }, "extrafiltersdropdown": "", "pagereset": "http://moodle.local/mod/assign/view.php?id=2&action=grading&group=0", "graderurl": "http://moodle.local/mod/assign/view.php?id=2&action=grader", "quickgrading": { "baseurl": "http://moodle.local/mod/assign/view.php?id=2&action=grading", "enabled": true }, "downloadasfolders": { "baseurl": "http://moodle.local/mod/assign/view.php?id=2&action=grading", "enabled": true }, "actions": "
" } }}
{{#userselector}} {{/userselector}} {{#groupselector}} {{/groupselector}} {{#initialselector}} {{/initialselector}} {{#statusselector}} {{#js}} document.querySelector('#{{baseid}}').addEventListener('change', function(e) { window.location.href = e.target.value; }); {{/js}} {{/statusselector}} {{#extrafiltersdropdown}} {{/extrafiltersdropdown}} {{#pagereset}} {{/pagereset}} {{#graderurl}} {{/graderurl}}
{{#quickgrading}} {{#js}} document.querySelector('#quickgrading-{{uniqid}}').addEventListener('change', function(e) { var url = new URL('{{{baseurl}}}'); url.searchParams.set('quickgrading', e.target.checked ? 1 : 0); window.location.href = url; }); {{/js}} {{/quickgrading}} {{#downloadasfolders}} {{#js}} document.querySelector('#downloadasfolders-{{uniqid}}').addEventListener('change', function(e) { var url = new URL('{{{baseurl}}}'); url.searchParams.set('downloadasfolders', e.target.checked ? 1 : 0); window.location.href = url; }); {{/js}} {{/downloadasfolders}} {{#actions}} {{/actions}}
templates/override_delete_modal.mustache000064400000003643152265517350014633 0ustar00{{! This file is part of Moodle - http://moodle.org/ Moodle is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Moodle is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Moodle. If not, see . }} {{! @template mod_assign/override_delete_modal Example context (json): { } }} {{< core/modal }} {{$title}}{{#str}} confirm {{/str}}{{/title}} {{$body}}

{{confirmmessage}}

{{#showpenaltyrecalculation}}
{{/showpenaltyrecalculation}}
{{/body}} {{$footer}} {{/footer}} {{/ core/modal }}templates/popout_button.mustache000064400000000267152265517350013236 0ustar00 {{#pix}}e/fullscreen,core{{/pix}} templates/grading_actions.mustache000064400000006063152265517350013450 0ustar00{{! This file is part of Moodle - http://moodle.org/ Moodle is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Moodle is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Moodle. If not, see . }} {{! @template mod_assign/grading_actions Actions panel at the bottom of the assignment grading UI. Classes required for JS: * none Data attributes required for JS: * data-region Context variables required for this template: * see mod/assign/classes/output/grading_app.php This template is initially hidden, and is only displayed after the current user info has been loaded. }}
{{#helpicon}} {{>core/help_icon}} {{/helpicon}}
{{#showreview}}
{{/showreview}}
{{#js}} require(['mod_assign/grading_actions'], function(GradingActions) { new GradingActions('[data-region="grade-actions"]'); }); {{/js}} templates/grading_app.mustache000064400000005253152265517350012570 0ustar00{{! This file is part of Moodle - http://moodle.org/ Moodle is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Moodle is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Moodle. If not, see . }} {{! @template mod_assign/grading_app Actions panel at the bottom of the assignment grading UI. Classes required for JS: * Uses some bootstrap classes, but for visual appeal only. Data attributes required for JS: * data-region, data-first-userid, data-courseid, data-contextid, data-assignmentid Context variables required for this template: * see mod/assign/classes/output/grading_app.php This template includes ajax functionality, so it cannot be shown in the template library. }}
{{> mod_assign/grading_navigation }}
{{#showreview}} {{/showreview}}
{{> mod_assign/grading_panel }}
{{> mod_assign/grading_actions }}
{{> mod_assign/grading_save_in_progress }}
templates/bulkactions/grading/bulk_message_trigger.mustache000064400000002562152265517350020432 0ustar00{{! This file is part of Moodle - http://moodle.org/ Moodle is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Moodle is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Moodle. If not, see . }} {{! @template mod_assign/bulkactions/grading/bulk_message_trigger Renders the bulk message trigger element in assignment grading page. Context variables required for this template: * showindropdown - Whether the action is displayed under a 'More' dropdown or as a separate button. * isfirst - Whether the action is the first one in the list. Example context (json): { "showindropdown": false, "isfirst": true } }} {{< core/bulkactions/bulk_action_trigger }} {{$action}}message{{/action}} {{$title}}{{#str}} message, core {{/str}}{{/title}} {{$icon}}{{#pix}} t/message, core {{/pix}}{{/icon}} {{/ core/bulkactions/bulk_action_trigger }} templates/bulkactions/grading/bulk_delete_trigger.mustache000064400000002625152265517350020250 0ustar00{{! This file is part of Moodle - http://moodle.org/ Moodle is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Moodle is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Moodle. If not, see . }} {{! @template mod_assign/bulkactions/grading/bulk_delete_trigger Renders the bulk delete trigger element in assignment grading page. Context variables required for this template: * showindropdown - Whether the action is displayed under a 'More' dropdown or as a separate button. * isfirst - Whether the action is the first one in the list. Example context (json): { "showindropdown": false, "isfirst": true } }} {{< core/bulkactions/bulk_action_trigger }} {{$action}}removesubmission{{/action}} {{$title}}{{#str}} batchoperationremovesubmission, mod_assign {{/str}}{{/title}} {{$icon}}{{#pix}} i/delete, core {{/pix}}{{/icon}} {{/ core/bulkactions/bulk_action_trigger }} templates/bulkactions/grading/bulk_action_modal_body.mustache000064400000003353152265517350020730 0ustar00{{! This file is part of Moodle - http://moodle.org/ Moodle is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Moodle is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Moodle. If not, see . }} {{! @template mod_assign/bulkactions/grading/bulk_action_modal_body Renders the body of the bulk action confirmation modal. Context variables required for this template: * text - The bulk action confirmation text. * operation - The name of the bulk operation. * cmid - The course module ID. * selectedusers - The string containing the IDs of the selected users. * sesskey - The session key. Example context (json): { "text": "Lock all selected submissions?", "operation": "lock", "cmid": 2, "selectedusers": "3,5", "sesskey": "abc123" } }}

{{text}}

templates/bulkactions/grading/bulk_general_action_trigger.mustache000064400000003106152265517350021753 0ustar00{{! This file is part of Moodle - http://moodle.org/ Moodle is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Moodle is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Moodle. If not, see . }} {{! @template mod_assign/bulkactions/grading/bulk_general_action_trigger Renders the general bulk action trigger element in assignment grading page. Context variables required for this template: * action: The action to be performed. * title: The title of the action button. * icon: The icon of the action button. * showindropdown - Whether the action is displayed under a 'More' dropdown or as a separate button. * isfirst - Whether the action is the first one in the list. Example context (json): { "action": "lock", "title": "Lock", "icon": "", "showindropdown": false, "isfirst": true } }} {{< core/bulkactions/bulk_action_trigger }} {{$action}}{{action}}{{/action}} {{$title}}{{title}}{{/title}} {{$icon}}{{{icon}}}{{/icon}} {{/ core/bulkactions/bulk_action_trigger }} templates/bulkactions/grading/bulk_extend_trigger.mustache000064400000002623152265517350020273 0ustar00{{! This file is part of Moodle - http://moodle.org/ Moodle is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Moodle is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Moodle. If not, see . }} {{! @template mod_assign/bulkactions/grading/bulk_extend_trigger Renders the bulk extend trigger element in assignment grading page. Context variables required for this template: * showindropdown - Whether the action is displayed under a 'More' dropdown or as a separate button. * isfirst - Whether the action is the first one in the list. Example context (json): { "showindropdown": false, "isfirst": true } }} {{< core/bulkactions/bulk_action_trigger }} {{$action}}grantextension{{/action}} {{$title}}{{#str}} batchoperationgrantextension, mod_assign {{/str}}{{/title}} {{$icon}}{{#pix}} i/duration, core {{/pix}}{{/icon}} {{/ core/bulkactions/bulk_action_trigger }} templates/bulkactions/grading/bulk_setmarkingallocation_trigger.mustache000064400000002707152265517350023221 0ustar00{{! This file is part of Moodle - http://moodle.org/ Moodle is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Moodle is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Moodle. If not, see . }} {{! @template mod_assign/bulkactions/grading/bulk_setmarkingallocation_trigger Renders the bulk set allocated marker trigger element in assignment grading page. Context variables required for this template: * showindropdown - Whether the action is displayed under a 'More' dropdown or as a separate button. * isfirst - Whether the action is the first one in the list. Example context (json): { "showindropdown": false, "isfirst": true } }} {{< core/bulkactions/bulk_action_trigger }} {{$action}}setmarkingallocation{{/action}} {{$title}}{{#str}} batchoperationsetmarkingallocation, mod_assign {{/str}}{{/title}} {{$icon}}{{/icon}} {{/ core/bulkactions/bulk_action_trigger }} templates/bulkactions/grading/bulk_setmarkingworkflowstate_trigger.mustache000064400000002730152265517350024003 0ustar00{{! This file is part of Moodle - http://moodle.org/ Moodle is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Moodle is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Moodle. If not, see . }} {{! @template mod_assign/bulkactions/grading/bulk_setmarkingworkflowstate_trigger Renders the bulk set marking workflow state trigger element in assignment grading page. Context variables required for this template: * showindropdown - Whether the action is displayed under a 'More' dropdown or as a separate button. * isfirst - Whether the action is the first one in the list. Example context (json): { "showindropdown": false, "isfirst": true } }} {{< core/bulkactions/bulk_action_trigger }} {{$action}}setmarkingworkflowstate{{/action}} {{$title}}{{#str}} batchoperationsetmarkingworkflowstate, mod_assign {{/str}}{{/title}} {{$icon}}{{/icon}} {{/ core/bulkactions/bulk_action_trigger }} templates/grading_save_in_progress.mustache000064400000002156152265517350015357 0ustar00{{! This file is part of Moodle - http://moodle.org/ Moodle is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Moodle is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Moodle. If not, see . }} {{! @template mod_assign/grading_save_in_progress Shown in an overlay when the grading form has been submitted, and we are waiting for a response. Classes required for JS: * none Data attributes required for JS: * None Context variables required for this template: * None Example context (json): { "id": "4" } }}

{{#str}}savingchanges, mod_assign{{/str}} {{> mod_assign/loading }}

templates/submission_actionmenu.mustache000064400000002440152265517350014725 0ustar00{{! This file is part of Moodle - http://moodle.org/ Moodle is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Moodle is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Moodle. If not, see . }} {{! @template mod_assign/submission_actionmenu Actions for grading submissions in the assignment. Classes required for JS: * none Context variables required for this template: * see mod/assign/classes/output/actionmenu.php Example context (json): { "gradelink": "https://moodle.org" } }}
{{#gradelink}} {{/gradelink}}
templates/review_panel.mustache000064400000002014152265517350012765 0ustar00{{! This file is part of Moodle - http://moodle.org/ Moodle is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Moodle is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Moodle. If not, see . }} {{! @template mod_assign/review_panel Show a placeholder in the review panel until something injects some content. Classes required for JS: * None Data attibutes required for JS: * None Context variables required for this template: * None Example context (json): { } }} {{> mod_assign/loading }} templates/grading_sticky_footer.mustache000064400000004242152265517350014671 0ustar00{{! This file is part of Moodle - http://moodle.org/ Moodle is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Moodle is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Moodle. If not, see . }} {{! @template mod_assign/grading_sticky_footer Sticky footer template for the assignment grading page. Example context (json): { "perpage": "", "pagingbar": "", "hassubmit": true, "sendstudentnotifications": true } }}
{{#perpage}} {{{.}}} {{/perpage}}
{{#pagingbar}} {{{.}}} {{/pagingbar}}
{{#hassubmit}}
{{/hassubmit}}
templates/list_participant_user_summary.mustache000064400000002662152265517350016502 0ustar00{{! This file is part of Moodle - http://moodle.org/ Moodle is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Moodle is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Moodle. If not, see . }} {{! @template mod_assign/list_participant_user_summary Shown as the options for the list of users in the user picker on the grading page. Classes required for JS: * none Data attributes required for JS: * None Context variables required for this template: * fullname * requiregrading * identity - list of only the visible identity fields * list of fields from mod_assign_list_participants webservice Example context (json): { "id": "4", "fullname": "Mr T", "requiregrading": "true", "identity": "email, phone, etc" } }} {{fullname}} {{identity}} {{#requiregrading}}*{{/requiregrading}} templates/grading_panel.mustache000064400000002276152265517350013111 0ustar00{{! This file is part of Moodle - http://moodle.org/ Moodle is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Moodle is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Moodle. If not, see . }} {{! @template mod_assign/grading_panel Shown a place holder until the real content is fetched via ajax. Classes required for JS: * none Data attributes required for JS: * None Context variables required for this template: * None This template loads it's content via ajax, so it cannot be shown in the template library. }} {{> mod_assign/loading }} {{#js}} require(['mod_assign/grading_panel'], function(GradingPanel) { new GradingPanel('[data-region="grade"]'); }); {{/js}} templates/grading_navigation.mustache000064400000005743152265517350014153 0ustar00{{! This file is part of Moodle - http://moodle.org/ Moodle is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Moodle is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Moodle. If not, see . }} {{! @template mod_assign/grading_navigation Actions panel at the bottom of the assignment grading UI. Classes required for JS: * none Data attributes required for JS: * data-region, data-assignmentid, data-groupid Context variables required for this template: * see mod/assign/classes/output/grading_app.php This template includes ajax functionality, so it cannot be shown in the template library. }}
{{! There are three chunks, which appear side-by-side at large screen sizes. A) Assignment info, which has 3 rows: }}
{{! Row 1) course name & link. }} {{coursename}}
{{! Row 2) Assignment name & link, & edit settings. }} {{name}} {{#caneditsettings}} {{#pix}}t/edit, core{{/pix}} {{/caneditsettings}}
{{! Row 3) Assignment grading & link, due date, with tool-tip thing. }} {{viewgrading}}
{{! B) Current user name and info. }}
{{> mod_assign/grading_navigation_user_info }}
{{! C) User selector widget. }}
{{> mod_assign/grading_navigation_user_selector }}
{{#js}} require(['mod_assign/grading_navigation', 'core/tooltip'], function(GradingNavigation, ToolTip) { new GradingNavigation('[data-region="user-selector"]'); new ToolTip('[data-region="assignment-tooltip"]'); }); {{/js}} templates/override_actionmenu.mustache000064400000006053152265517350014355 0ustar00{{! This file is part of Moodle - http://moodle.org/ Moodle is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Moodle is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Moodle. If not, see . }} {{! @template mod_assign/override_actionmenu Override select menu for users and groups. Classes required for JS: * none Context variables required for this template: * see mod/lesson/classes/output/override_actionmenu.php Example context (json): { "urlselect": { "id": "url_select_test", "action": "https://example.com/post", "formid": "url_select_form", "sesskey": "sesskey", "label": "core/url_select", "helpicon": { "title": "Help with something", "text": "Help with something", "url": "http://example.org/help", "linktext": "", "icon": { "extraclasses": "", "attributes": [ {"name": "src", "value": "../../../pix/help.svg"}, {"name": "alt", "value": "Help icon"} ] } }, "showbutton": "Go", "options": [{ "name": "Group 1", "isgroup": true, "options": [ {"name": "Item 1", "isgroup": false, "value": "1"}, {"name": "Item 2", "isgroup": false, "value": "2"} ]}, {"name": "Group 2", "isgroup": true, "options": [ {"name": "Item 3", "isgroup": false, "value": "3"}, {"name": "Item 4", "isgroup": false, "value": "4"} ]}], "disabled": false, "title": "Some cool title" }, "addoverride": { "method" : "get", "url" : "#", "formid": "1", "primary" : true, "tooltip" : "This is a tooltip", "label" : "This is a the button text", "attributes": [ { "name": "data-attribute", "value": "yeah" } ] } } }}
{{#urlselect}} {{/urlselect}} {{#addoverride}} {{/addoverride}}
templates/grading_navigation_user_selector.mustache000064400000010436152265517350017104 0ustar00{{! This file is part of Moodle - http://moodle.org/ Moodle is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Moodle is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Moodle. If not, see . }} {{! @template mod_assign/grading_navigation_user_selector The template HTML for the user selector in the top right corner. Classes required for JS: * none Data attributes required for JS: * data-action, data-assignmentid, data-groupid, data-region Context variables required for this template: * see mod/assign/classes/output/grading_app.php This template uses ajax functionality, so it cannot be shown in the template library. Example context (json): { } }} {{{larrow}}} {{{rarrow}}}
{{#str}}xofy, mod_assign, { "x": "{{index}}", "y": "{{count}}" }{{/str}} {{#str}}resettable{{/str}}
{{#hasmarkingallocation}} {{/hasmarkingallocation}} {{#hasmarkingworkflow}} {{/hasmarkingworkflow}}
templates/attempt_history_chooser.mustache000064400000001172152265517350015272 0ustar00{{#submissioncount}}
{{#str}}viewadifferentattempt, mod_assign{{/str}}
{{#submissions}} {{/submissions}}
{{/submissioncount}} templates/timelimit_panel.mustache000064400000002315152265517350013465 0ustar00{{! This file is part of Moodle - http://moodle.org/ Moodle is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Moodle is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Moodle. If not, see . }} {{! @template mod_assign/timelimit_panel Show a timer that counts down to 0. Classes required for JS: * None Data attibutes required for JS: * None Example context (json): { "timerstartvalue": 1729000 } }} {{#js}} require(['mod_assign/timer'], function(Timer) { Timer.init("mod_assign-timer-{{uniqid}}"); }); {{/js}} templates/grading_navigation_user_summary.mustache000064400000004016152265517350016756 0ustar00{{! This file is part of Moodle - http://moodle.org/ Moodle is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Moodle is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Moodle. If not, see . }} {{! @template mod_assign/grading_navigation_user_summary Shown when the real data for a user has been loaded. Display in the top left corner. Classes required for JS: * none Data attributes required for JS: * data-region, data-assignmentid, data-groupid Context variables required for this template: * id, courseid, all fields from mod_assign_list_participants Example context (json): { "id": "5", "fullname": "Mr T", "hasidentity": true, "identity": "t@example.org, T", "profileimageurl": "https://moodle.org/pix/u/f3.png" } }}

{{^blindmarking}} {{/blindmarking}} {{#profileimageurl}} {{/profileimageurl}} {{fullname}} {{#hasidentity}} {{identity}} {{/hasidentity}} {{#duedate}} {{#str}}duedatecolon, mod_assign, {{duedatestr}}{{/str}} {{/duedate}} {{^blindmarking}} {{/blindmarking}}

upgrade.txt000064400000027714152265517350006762 0ustar00=== 4.5 Onwards === This file has been replaced by UPGRADING.md. See MDL-81125 for further information. === This files describes API changes in the assign code. === 4.3 === * The following deprecated methods have been removed and should not be used any more: - `assign::add_to_log` - `assign::format_grade_for_log` - `assign::format_submission_for_log` - `assign_plugin::format_for_log` * The assign_files renderable no longer abuses the dynamic nature of PHP and puts random properties onto stored_file instances. If you were previously using these properties, please update to use the new method on the tree: * $file->portfoliobutton $renderer->get_portfolio_button($file) * $file->timemodified $tree->get_modified_time($file) * $file->fileurl $tree->get_file_url($file) === 4.2 === * The upgradelib.php file has been removed from mod_assign as it was only being used by mod_assignment and mod_assignment has been removed from core. === 4.1 === * New method \assign::is_userid_filtered() has been implemented. It returns false if user id is filtered out by either user preferences for grading table or submission status filter. Otherwise, returns true. * A new method was added to feedback/editpdf/locallib.php - get_user_data_file_areas() This just returns the necessary file areas for retrieving a complete feedback pdf. === 4.0 === * The method \assign::grading_disabled() now has optional $gradinginfo parameter to improve performance * Renderer (renderer.php) has been moved from mod root to classes/output/ to be more PSR compliant. * Class assign_header has been moved from renderable.php to classes/ouput/assign_header.php * Class assign_submion_status has been moved from renderable.php to classes/ouput/assign_submion_status.php * The external functions mod_assign_external::list_participants and mod_assign_external::get_participant now return a new field "submissionstatus" indicating the submission status (new, draft, reopened or submitted). Empty when not submitted. === 3.9 === * The following functions, previously used (exclusively) by upgrade steps are not available anymore because of the upgrade cleanup performed for this version. See MDL-65809 for more info: - get_assignments_with_rescaled_null_grades() === 3.8 === * The allow_image_conversion method has been added to the submissionplugins. It determines whether the submission plugin allows image conversion or not. By default conversion is not allowed (except when overwritten in the submission plugin) * Webservice function mod_assign_get_submission_status, return value 'warnofungroupedusers', changed from PARAM_BOOL to PARAM_ALPHA. See the description for possible values. * The following functions have been finally deprecated and can not be used anymore: * assign_scale_used() === 3.7 === * Submissions plugins should implement the "remove" function to remove data when "Remove submission" is used. === 3.6 === * The mod_assign_base_testcase unit test base class has been deprecated. It encouraged poor unit test design and led to significant performance issues with unit tests. See MDL-55609 for further information. * The function can_grade() now has optional $user parameter. * Feedback plugins can now specify whether or not they want to attach files to the feedback that is stored in the gradebook via the new method files_for_gradebook(). An example of what this method would return is - [ 'contextid' => $modulecontext->id, 'component' => ASSIGNFEEDBACK_XYZ_COMPONENT, 'filearea' => ASSIGNFEEDBACK_XYZ_FILEAREA, 'itemid' => $grade->id ] === 3.5 === * Functions assign:get_assign_grading_summary_renderable, assign:can_view_submission, assign:count_submissions_with_status, assign:count_submissions_need_grading and mod_assign_external::get_submission_status now admit an additional group parameter. This parameter can be used to force those functions to retrieve data only for the given group. === 3.4 === * assign::add_attempt requires that set_most_recent_team_submission() be called if attempting to use this function with a team submission. === 3.3.2 === * assign_refresh_events() Now takes two additional parameters to refine the update to a specific instance. This function now optionally takes the module instance object or ID, and the course module object or ID. Please try to send the full objects instead of the ids to save DB calls. === 3.3 === * All pluginfile file serving functions now pass through the options to send_stored_file() (all assignment plugins should do the same). * Fixed calendar event types for overridden due dates from 'close' to 'due'. * Removed calendar event type of 'open', since mod_assign only has the 'due' event type. No point in creating an override event for an event type that does not exist. === 3.2 === * External function mod_assign_external::get_assignments now returns additional optional fields: - preventsubmissionnotingroup: Prevent submission not in group. - submissionstatement and submissionstatementformat: When there is a submission statement defined. * Proper checking for empty submissions * Submission modification time checking - this will help students working in groups not clobber each others' submissions * External functions that were returning file information now return the following file fields: filename, filepath, mimetype, filesize, timemodified and fileurl. Those fields are now marked as VALUE_OPTIONAL for backwards compatibility. Please, note that previously the filename was part of the filepath field, now they are separated. * Submission and feedback plugins can now specify file areas related to their configuration data, which will then be included in backup and restore; see assign_plugin::get_config_file_areas(). * Submission and feedback plugins must now return the specific list of configs available for external functions, this can be done implementing the new assign plugin method get_config_for_external() * Webservice function mod_assign_get_submissions returns a new field 'gradingstatus' from each submission. === 3.1 === * The feedback plugins now need to implement the is_feedback_modified() method. The default is to return true for backwards compatibiltiy. * When downloading all submissions as a zip each students' files are in a separate folder, are no longer renamed and the folder structure is kept intact. === 3.0 === * assign_submission_status renderable now requires $usergroups in its constructor * External function mod_assign_external::get_assignments now have a new optional parameter: includenotenrolledcourses. Its default value is false, setting it to true will return assignments for courses a user can see even if it's not enroled. === 2.9 === * External parameter now returns PARAM_RAW instead of PARAM_TEXT for webservices using feedback comments. * External function mod_assign_external::get_assignments now returns additional optional fields: - intro: Assignment intro. - introformat: Intro format. - introattachments: Intro attachments files. === 2.8 === * Some DB changes were made to simplify the SQL required to query the latest attempt. - The assign_submission table now has a column "latest" which is set to 1 for the latest submission attempt. - There will always be a submission row if there is a grade (so the latest grade can be found by joining with the submission) - There is a new submission status "new" for a submission that has never been attempted by a student (but the record exists purely to mark the latest attempt number as 0). The function get_user_submission will create a record with the status set to "new" by default (the previous default was "draft"). === 2.7 === * Added setting sendstudentnotifications to assign DB table with admin defaults. This sets the default value for the "Notify students" option on the grading forms. This setting can be retrieved via webservices. * Web service function mod_assign_save_grade has an additional optional parameter $advancedgradingdata which allows advanced grading data to be used. * A new web service function mod_assign_save_grades has been added which allows multiple grades to be processed. * The event submission_updated() [mod/assign/classes/event/submission_updated.php] has been altered to now be an abstract class for submission events in the submission plug-ins. === 2.6.1 === * format_text() is no longer used for formating assignment content to be used in events (assign_submission_onlinetext::save()) or the word count (assign_submission_onlinetext::format_for_log()) in mod/assign/submission/onlinetext/locallib.php. format_text() should only be used when displaying information to the screen. It was being used incorrectly before in these areas. Plugins using the event assessable_uploaded() should use file_rewrite_pluginfile_urls() to translate the text back to the desired output. === 2.6 === * To see submission/grades of inactive users, user should have moodle/course:viewsuspendedusers capability. * count_* functions will return only active participants. * assign_submission_plugin->lock and unlock methods have an additional parameter for user flags. A user will not always have a submission record when the submission is locked/unlocked. * Submission and feedback plugins can now participate in webservices. The plugin must implement get_external_parameters() to describe the parameters it is expecting from the mod_assign_save_grade or mod_assign_save_submission functions. The plugin will then handle the webservice via it's normal save() method with the extra data supplied in the $data argument. === 2.5 === * New feature - Attempt History This adds settings so that a student can have build up a history of separate submission attempts and grades for the same assignment. Extra settings on the assign table are: attemptreopenmethod - The way attempts can be reopened. One of: ASSIGN_ATTEMPT_REOPEN_METHOD_NONE - Attempts cannot be reopened (default) ASSIGN_ATTEMPT_REOPEN_METHOD_UNTIL_PASS - Attempts are reopened on grading until the gradebook indicates this student has passed. ASSIGN_ATTEMPT_REOPEN_METHOD_MANUAL - Attempts are reopened on manually by the grader. maxattempts - The maximum allowed number of attempts per student for this assign instance. Extra settings on the submission and grade records: attemptnumber - Starting from 0 until the latest attempt. New table assign_user_flags holds user specific flags that were stored in the assign_grade table. (contains assignment, userid, extensionduedate, mailed and locked) assign changes: "get_user_grade" function has an extra optional parameter to retrieve a specific attempt number. "get_user_submission" function has an extra optional parameter to retrieve a specific attempt number. "get_group_submission" function has an extra optional parameter to retrieve a specific attempt number. new "get_user_flags" function can retrieve the user flags for a specific user (extensionduedate, mailed and locked). new "update_user_flags" function can update the user flags for a specific user (extensionduedate, mailed and locked). assign_submission_plugin changes: "precheck_submission" function now takes a submission record so you can determine which submission is being checked. "submit_for_grading" function now takes a submission record so you can determine which submission is being submitted. new function "copy_submission" can be implemented to copy submission data from one submission to a new one. New renderable object "assign_attempt_history" for rendering the list of previous submissions. New renderable object "assign_gradingmessage" for rendering a generic grading message. assign_grading_table changes: get_group_and_submission is now protected and should not be called from outside this class. === Earlier changes === * Were not documented in this way. Sorry. gradingtable.php000064400000237613152265517350007727 0ustar00. /** * This file contains the definition for the grading table which subclassses easy_table * * @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(); require_once($CFG->libdir.'/tablelib.php'); require_once($CFG->libdir.'/gradelib.php'); require_once($CFG->dirroot.'/mod/assign/locallib.php'); /** * Extends table_sql to provide a table of assignment submissions * * @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_grading_table extends table_sql implements renderable { /** @var assign $assignment */ private $assignment = null; /** @var int $perpage */ private $perpage = 10; /** @var int[] $pagingoptions Available pagination options */ private $pagingoptions = [10, 20, 50, 100]; /** @var int $rownum (global index of current row in table) */ private $rownum = -1; /** @var renderer_base for getting output */ private $output = null; /** @var stdClass gradinginfo */ private $gradinginfo = null; /** @var int $tablemaxrows */ private $tablemaxrows = 10000; /** @var boolean $quickgrading */ private $quickgrading = false; /** @var boolean $hasgrantextension - Only do the capability check once for the entire table */ private $hasgrantextension = false; /** @var boolean $hasgrade - Only do the capability check once for the entire table */ private $hasgrade = false; /** @var array $groupsubmissions - A static cache of group submissions */ private $groupsubmissions = array(); /** @var array $submissiongroups - A static cache of submission groups */ private $submissiongroups = array(); /** @var array $plugincache - A cache of plugin lookups to match a column name to a plugin efficiently */ private $plugincache = array(); /** @var array $scale - A list of the keys and descriptions for the custom scale */ private $scale = null; /** @var bool true if the user has this capability. Otherwise false. */ private $hasviewblind; /** * overridden constructor keeps a reference to the assignment class that is displaying this table * * @param assign $assignment The assignment class * @param int $perpage how many per page * @param string $filter The current filter * @param int $rowoffset For showing a subsequent page of results * @param bool $quickgrading Is this table wrapped in a quickgrading form? * @param string $downloadfilename */ public function __construct(assign $assignment, $perpage, $filter, $rowoffset, $quickgrading, $downloadfilename = null) { global $CFG, $PAGE, $DB, $USER; parent::__construct('mod_assign_grading-' . $assignment->get_context()->id); $this->is_persistent(true); $this->set_attribute('id', 'submissions'); $this->assignment = $assignment; // Check permissions up front. $this->hasgrantextension = has_capability('mod/assign:grantextension', $this->assignment->get_context()); $this->hasgrade = $this->assignment->can_grade(); // Check if we have the elevated view capablities to see the blind details. $this->hasviewblind = has_capability('mod/assign:viewblinddetails', $this->assignment->get_context()); $this->perpage = $perpage; $this->quickgrading = $quickgrading && $this->hasgrade; $this->output = $PAGE->get_renderer('mod_assign'); $urlparams = array('action' => 'grading', 'id' => $assignment->get_course_module()->id); $url = new moodle_url($CFG->wwwroot . '/mod/assign/view.php', $urlparams); $this->define_baseurl($url); // Do some business - then set the sql. $currentgroup = groups_get_activity_group($assignment->get_course_module(), true, participationonly: false); if ($rowoffset) { $this->rownum = $rowoffset - 1; } $userid = optional_param('userid', null, PARAM_INT); $groupid = groups_get_course_group($assignment->get_course(), true); // If the user ID is set, it indicates that a user has been selected. In this case, override the user search // string with the full name of the selected user. $usersearch = $userid ? fullname(\core_user::get_user($userid)) : optional_param('search', '', PARAM_NOTAGS); $assignment->set_usersearch($userid, $groupid, $usersearch); $users = array_keys( $assignment->list_participants($currentgroup, true)); if (count($users) == 0) { // Insert a record that will never match to the sql is still valid. $users[] = -1; } $params = array(); $params['assignmentid1'] = (int)$this->assignment->get_instance()->id; $params['assignmentid2'] = (int)$this->assignment->get_instance()->id; $params['assignmentid3'] = (int)$this->assignment->get_instance()->id; $params['newstatus'] = ASSIGN_SUBMISSION_STATUS_NEW; // TODO Does not support custom user profile fields (MDL-70456). $userfieldsapi = \core_user\fields::for_identity($this->assignment->get_context(), false)->with_userpic(); $userfields = $userfieldsapi->get_sql('u', false, '', '', false)->selects; $extrauserfields = $userfieldsapi->get_required_fields([\core_user\fields::PURPOSE_IDENTITY]); $fields = $userfields . ', '; $fields .= 'u.id as userid, '; $fields .= 's.status as status, '; $fields .= 's.id as submissionid, '; $fields .= 's.timecreated as firstsubmission, '; $fields .= "CASE WHEN status <> :newstatus THEN s.timemodified ELSE NULL END as timesubmitted, "; $fields .= 's.attemptnumber as attemptnumber, '; $fields .= 'g.id as gradeid, '; $fields .= 'g.grade as grade, '; $fields .= 'g.timemodified as timemarked, '; $fields .= 'g.timecreated as firstmarked, '; $fields .= 'uf.mailed as mailed, '; $fields .= 'uf.locked as locked, '; $fields .= 'uf.extensionduedate as extensionduedate, '; $fields .= 'uf.workflowstate as workflowstate, '; $fields .= 'uf.allocatedmarker as allocatedmarker'; $from = '{user} u LEFT JOIN {assign_submission} s ON u.id = s.userid AND s.assignment = :assignmentid1 AND s.latest = 1 '; // For group assignments, there can be a grade with no submission. $from .= ' LEFT JOIN {assign_grades} g ON g.assignment = :assignmentid2 AND u.id = g.userid AND (g.attemptnumber = s.attemptnumber OR s.attemptnumber IS NULL) '; $from .= 'LEFT JOIN {assign_user_flags} uf ON u.id = uf.userid AND uf.assignment = :assignmentid3 '; if ($this->assignment->get_course()->relativedatesmode) { $params['courseid1'] = $this->assignment->get_course()->id; $from .= ' LEFT JOIN ( SELECT ue1.userid as enroluserid, CASE WHEN MIN(ue1.timestart - c2.startdate) < 0 THEN 0 ELSE MIN(ue1.timestart - c2.startdate) END as enrolstartoffset FROM {enrol} e1 JOIN {user_enrolments} ue1 ON (ue1.enrolid = e1.id AND ue1.status = 0) JOIN {course} c2 ON c2.id = e1.courseid WHERE e1.courseid = :courseid1 AND e1.status = 0 GROUP BY ue1.userid ) enroloffset ON (enroloffset.enroluserid = u.id) '; } $hasoverrides = $this->assignment->has_overrides(); $inrelativedatesmode = $this->assignment->get_course()->relativedatesmode; if ($hasoverrides) { $params['assignmentid5'] = (int)$this->assignment->get_instance()->id; $params['assignmentid6'] = (int)$this->assignment->get_instance()->id; $params['assignmentid7'] = (int)$this->assignment->get_instance()->id; $params['assignmentid8'] = (int)$this->assignment->get_instance()->id; $params['assignmentid9'] = (int)$this->assignment->get_instance()->id; list($userwhere1, $userparams1) = $DB->get_in_or_equal($users, SQL_PARAMS_NAMED, 'priorityuser'); list($userwhere2, $userparams2) = $DB->get_in_or_equal($users, SQL_PARAMS_NAMED, 'effectiveuser'); $userwhere1 = "WHERE u.id {$userwhere1}"; $userwhere2 = "WHERE u.id {$userwhere2}"; $params = array_merge($params, $userparams1); $params = array_merge($params, $userparams2); $fields .= ', priority.priority, '; $fields .= 'effective.allowsubmissionsfromdate, '; if ($inrelativedatesmode) { // If the priority is less than the 9999999 constant value it means it's an override // and we should use that value directly. Otherwise we need to apply the uesr's course // start date offset. $fields .= 'CASE WHEN priority.priority < 9999999 THEN effective.duedate ELSE' . ' effective.duedate + enroloffset.enrolstartoffset END as duedate, '; } else { $fields .= 'effective.duedate, '; } $fields .= 'effective.cutoffdate '; $from .= ' LEFT JOIN ( SELECT merged.userid, min(merged.priority) priority FROM ( ( SELECT u.id as userid, 9999999 AS priority FROM {user} u '.$userwhere1.' ) UNION ( SELECT uo.userid, 0 AS priority FROM {assign_overrides} uo WHERE uo.assignid = :assignmentid5 ) UNION ( SELECT gm.userid, go.sortorder AS priority FROM {assign_overrides} go JOIN {groups} g ON g.id = go.groupid JOIN {groups_members} gm ON gm.groupid = g.id WHERE go.assignid = :assignmentid6 ) ) merged GROUP BY merged.userid ) priority ON priority.userid = u.id JOIN ( (SELECT 9999999 AS priority, u.id AS userid, a.allowsubmissionsfromdate, a.duedate, a.cutoffdate FROM {user} u JOIN {assign} a ON a.id = :assignmentid7 '.$userwhere2.' ) UNION (SELECT 0 AS priority, uo.userid, uo.allowsubmissionsfromdate, uo.duedate, uo.cutoffdate FROM {assign_overrides} uo WHERE uo.assignid = :assignmentid8 ) UNION (SELECT go.sortorder AS priority, gm.userid, go.allowsubmissionsfromdate, go.duedate, go.cutoffdate FROM {assign_overrides} go JOIN {groups} g ON g.id = go.groupid JOIN {groups_members} gm ON gm.groupid = g.id WHERE go.assignid = :assignmentid9 ) ) effective ON effective.priority = priority.priority AND effective.userid = priority.userid '; } else if ($inrelativedatesmode) { // In relative dates mode and when we don't have overrides, include the // duedate, cutoffdate and allowsubmissionsfrom date anyway as this information is useful and can vary. $params['assignmentid5'] = (int)$this->assignment->get_instance()->id; $fields .= ', a.duedate + enroloffset.enrolstartoffset as duedate, '; $fields .= 'a.allowsubmissionsfromdate, '; $fields .= 'a.cutoffdate '; $from .= 'JOIN {assign} a ON a.id = :assignmentid5 '; } if (!empty($this->assignment->get_instance()->blindmarking)) { $from .= 'LEFT JOIN {assign_user_mapping} um ON u.id = um.userid AND um.assignment = :assignmentidblind '; $params['assignmentidblind'] = (int)$this->assignment->get_instance()->id; $fields .= ', um.id as recordid '; } $userparams3 = array(); $userindex = 0; list($userwhere3, $userparams3) = $DB->get_in_or_equal($users, SQL_PARAMS_NAMED, 'user'); $where = 'u.id ' . $userwhere3; $params = array_merge($params, $userparams3); // The filters do not make sense when there are no submissions, so do not apply them. if ($this->assignment->is_any_submission_plugin_enabled()) { if ($filter == ASSIGN_FILTER_SUBMITTED) { $where .= ' AND (s.timemodified IS NOT NULL AND s.status = :submitted) '; $params['submitted'] = ASSIGN_SUBMISSION_STATUS_SUBMITTED; } else if ($filter == ASSIGN_FILTER_NOT_SUBMITTED) { $where .= ' AND (s.timemodified IS NULL OR s.status <> :submitted) '; $params['submitted'] = ASSIGN_SUBMISSION_STATUS_SUBMITTED; } else if ($filter == ASSIGN_FILTER_REQUIRE_GRADING) { $where .= ' 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'; // Assignment grade is set to the negative grade scale id when scales are used. if ($this->assignment->get_instance()->grade < 0) { // Scale grades are set to -1 when not graded. $where .= ' OR g.grade = -1'; } $where .= '))'; $params['submitted'] = ASSIGN_SUBMISSION_STATUS_SUBMITTED; } else if ($filter == ASSIGN_FILTER_GRADED) { $where .= ' AND (s.timemodified IS NOT NULL AND s.timemodified < g.timemodified AND g.grade IS NOT NULL)'; $params['submitted'] = ASSIGN_SUBMISSION_STATUS_SUBMITTED; } else if ($filter == ASSIGN_FILTER_GRANTED_EXTENSION) { $where .= ' AND uf.extensionduedate > 0 '; } else if (strpos($filter, ASSIGN_FILTER_SINGLE_USER) === 0) { $userfilter = (int) array_pop(explode('=', $filter)); $where .= ' AND (u.id = :userid)'; $params['userid'] = $userfilter; } else if ($filter == ASSIGN_FILTER_DRAFT) { $where .= ' AND (s.timemodified IS NOT NULL AND s.status = :draft) '; $params['draft'] = ASSIGN_SUBMISSION_STATUS_DRAFT; } } if ($this->assignment->get_instance()->markingworkflow && $this->assignment->get_instance()->markingallocation) { if (has_capability('mod/assign:manageallocations', $this->assignment->get_context())) { // Check to see if marker filter is set. $markerfilter = (int)get_user_preferences('assign_markerfilter', ''); if (!empty($markerfilter)) { if ($markerfilter == ASSIGN_MARKER_FILTER_NO_MARKER) { $where .= ' AND (uf.allocatedmarker IS NULL OR uf.allocatedmarker = 0)'; } else { $where .= ' AND uf.allocatedmarker = :markerid'; $params['markerid'] = $markerfilter; } } } } if ($this->assignment->get_instance()->markingworkflow) { $workflowstates = $this->assignment->get_marking_workflow_states_for_current_user(); if (!empty($workflowstates)) { $workflowfilter = get_user_preferences('assign_workflowfilter', ''); if ($workflowfilter == ASSIGN_MARKING_WORKFLOW_STATE_NOTMARKED) { $where .= ' AND (uf.workflowstate = :workflowstate OR uf.workflowstate IS NULL OR '. $DB->sql_isempty('assign_user_flags', 'workflowstate', true, true).')'; $params['workflowstate'] = $workflowfilter; } else if (array_key_exists($workflowfilter, $workflowstates)) { $where .= ' AND uf.workflowstate = :workflowstate'; $params['workflowstate'] = $workflowfilter; } } } $this->set_sql($fields, $from, $where, $params); if ($downloadfilename) { $this->is_downloading('csv', $downloadfilename); } $columns = array(); $headers = array(); // Select. if (!$this->is_downloading() && $this->hasgrade) { $columns[] = 'select'; // The displayed text for the column header. Hidden to assistive technologies. $visibleheader = html_writer::span(get_string('select'), '', ['aria-hidden' => 'true']); // The actual accessible name for the column header which provides more context about the column's purpose to // screen reader users. $bulkactionsselection = html_writer::span(get_string('bulkactionsselection', 'assign'), 'visually-hidden'); // The select all checkbox. $selectalllabel = html_writer::label(get_string('selectall'), 'selectall', false, ['class' => 'visually-hidden']); $selectallcheckbox = html_writer::empty_tag( 'input', [ 'type' => 'checkbox', 'id' => 'selectall', 'name' => 'selectall', ], ); $headers[] = $visibleheader . $bulkactionsselection . html_writer::div($selectalllabel . $selectallcheckbox, 'selectall'); } if ($this->hasviewblind || !$this->assignment->is_blind_marking()) { if ($this->is_downloading()) { $columns[] = 'recordid'; $headers[] = get_string('recordid', 'assign'); } // Fullname. $columns[] = 'fullname'; $headers[] = get_string('fullname'); // Participant # details if can view real identities. if ($this->assignment->is_blind_marking()) { if (!$this->is_downloading()) { $columns[] = 'recordid'; $headers[] = get_string('recordid', 'assign'); } } foreach ($extrauserfields as $extrafield) { $columns[] = $extrafield; $headers[] = \core_user\fields::get_display_name($extrafield); } } else { // Record ID. $columns[] = 'recordid'; $headers[] = get_string('recordid', 'assign'); } // Submission status. $columns[] = 'status'; $headers[] = get_string('status', 'assign'); if ($hasoverrides || $inrelativedatesmode) { // Allowsubmissionsfromdate. $columns[] = 'allowsubmissionsfromdate'; $headers[] = get_string('allowsubmissionsfromdate', 'assign'); // Duedate. $columns[] = 'duedate'; $headers[] = get_string('duedate', 'assign'); // Cutoffdate. $columns[] = 'cutoffdate'; $headers[] = get_string('cutoffdate', 'assign'); } // Team submission columns. if ($assignment->get_instance()->teamsubmission) { $columns[] = 'team'; $headers[] = get_string('submissionteam', 'assign'); } // Allocated marker. if ($this->assignment->get_instance()->markingworkflow && $this->assignment->get_instance()->markingallocation && has_capability('mod/assign:manageallocations', $this->assignment->get_context())) { // Add a column for the allocated marker. $columns[] = 'allocatedmarker'; $headers[] = get_string('marker', 'assign'); } // Grade. $columns[] = 'grade'; $headers[] = get_string('gradenoun'); if ($this->is_downloading()) { $gradetype = $this->assignment->get_instance()->grade; if ($gradetype > 0) { $columns[] = 'grademax'; $headers[] = get_string('maxgrade', 'assign'); } else if ($gradetype < 0) { // This is a custom scale. $columns[] = 'scale'; $headers[] = get_string('scale', 'assign'); } if ($this->assignment->get_instance()->markingworkflow) { // Add a column for the marking workflow state. $columns[] = 'workflowstate'; $headers[] = get_string('markingworkflowstate', 'assign'); } // Add a column to show if this grade can be changed. $columns[] = 'gradecanbechanged'; $headers[] = get_string('gradecanbechanged', 'assign'); } // Submission plugins. if ($assignment->is_any_submission_plugin_enabled()) { $columns[] = 'timesubmitted'; $headers[] = get_string('lastmodifiedsubmission', 'assign'); foreach ($this->assignment->get_submission_plugins() as $plugin) { if ($this->is_downloading()) { if ($plugin->is_visible() && $plugin->is_enabled()) { foreach ($plugin->get_editor_fields() as $field => $description) { $index = 'plugin' . count($this->plugincache); $this->plugincache[$index] = array($plugin, $field); $columns[] = $index; $headers[] = $plugin->get_name(); } } } else { if ($plugin->is_visible() && $plugin->is_enabled() && $plugin->has_user_summary()) { $index = 'plugin' . count($this->plugincache); $this->plugincache[$index] = array($plugin); $columns[] = $index; $headers[] = $plugin->get_name(); } } } } // Time marked. $columns[] = 'timemarked'; $headers[] = get_string('lastmodifiedgrade', 'assign'); // Feedback plugins. foreach ($this->assignment->get_feedback_plugins() as $plugin) { if ($this->is_downloading()) { if ($plugin->is_visible() && $plugin->is_enabled()) { foreach ($plugin->get_editor_fields() as $field => $description) { $index = 'plugin' . count($this->plugincache); $this->plugincache[$index] = array($plugin, $field); $columns[] = $index; $headers[] = $description; } } } else if ($plugin->is_visible() && $plugin->is_enabled() && $plugin->has_user_summary()) { $index = 'plugin' . count($this->plugincache); $this->plugincache[$index] = array($plugin); $columns[] = $index; $headers[] = $plugin->get_name(); } } // Exclude 'Final grade' column in downloaded grading worksheets. if (!$this->is_downloading()) { // Final grade. $columns[] = 'finalgrade'; $headers[] = get_string('finalgrade', 'grades'); } // Load the grading info for all users. $this->gradinginfo = grade_get_grades($this->assignment->get_course()->id, 'mod', 'assign', $this->assignment->get_instance()->id, $users); if (!empty($CFG->enableoutcomes) && !empty($this->gradinginfo->outcomes)) { $columns[] = 'outcomes'; $headers[] = get_string('outcomes', 'grades'); } // Set the columns. $this->define_columns($columns); $this->define_headers($headers); $this->column_class('fullname', 'username'); $this->column_class('status', 'status'); $this->column_class('grade', 'grade'); foreach ($extrauserfields as $extrafield) { $this->column_class($extrafield, $extrafield); } $this->no_sorting('recordid'); $this->no_sorting('finalgrade'); $this->no_sorting('userid'); $this->no_sorting('select'); $this->no_sorting('outcomes'); if ($assignment->get_instance()->teamsubmission) { $this->no_sorting('team'); } $plugincolumnindex = 0; foreach ($this->assignment->get_submission_plugins() as $plugin) { if ($plugin->is_visible() && $plugin->is_enabled() && $plugin->has_user_summary()) { $submissionpluginindex = 'plugin' . $plugincolumnindex++; $this->no_sorting($submissionpluginindex); } } foreach ($this->assignment->get_feedback_plugins() as $plugin) { if ($plugin->is_visible() && $plugin->is_enabled() && $plugin->has_user_summary()) { $feedbackpluginindex = 'plugin' . $plugincolumnindex++; $this->no_sorting($feedbackpluginindex); } } // When there is no data we still want the column headers printed in the csv file. if ($this->is_downloading()) { $this->start_output(); } } /** * Before adding each row to the table make sure rownum is incremented. * * @param array $row row of data from db used to make one row of the table. * @return array one row for the table */ public function format_row($row) { if ($this->rownum < 0) { $this->rownum = $this->currpage * $this->pagesize; } else { $this->rownum += 1; } return parent::format_row($row); } /** * Add a column with an ID that uniquely identifies this user in this assignment. * * @param stdClass $row * @return string */ public function col_recordid(stdClass $row) { if (empty($row->recordid)) { $row->recordid = $this->assignment->get_uniqueid_for_user($row->userid); } return get_string('hiddenuser', 'assign') . $row->recordid; } /** * Add the userid to the row class so it can be updated via ajax. * * @param stdClass $row The row of data * @return string The row class */ public function get_row_class($row) { return 'user' . $row->userid; } /** * Return the number of rows to display on a single page. * * @return int The number of rows per page */ public function get_rows_per_page() { return $this->perpage; } /** * list current marking workflow state * * @param stdClass $row * @return string */ public function col_workflowstatus(stdClass $row) { $o = ''; $gradingdisabled = $this->assignment->grading_disabled($row->id, true, $this->gradinginfo); // The function in the assignment keeps a static cache of this list of states. $workflowstates = $this->assignment->get_marking_workflow_states_for_current_user(); $workflowstate = $row->workflowstate; if (empty($workflowstate)) { $workflowstate = ASSIGN_MARKING_WORKFLOW_STATE_NOTMARKED; } if ($this->quickgrading && !$gradingdisabled) { $notmarked = get_string('markingworkflowstatenotmarked', 'assign'); $name = 'quickgrade_' . $row->id . '_workflowstate'; if ($workflowstate !== ASSIGN_MARKING_WORKFLOW_STATE_NOTMARKED && !array_key_exists($workflowstate, $workflowstates)) { $allworkflowstates = $this->assignment->get_all_marking_workflow_states(); $o .= html_writer::div($allworkflowstates[$workflowstate]); } else { $o .= html_writer::select($workflowstates, $name, $workflowstate, ['' => $notmarked]); // Check if this user is a marker that can't manage allocations and doesn't have the marker column added. if ($this->assignment->get_instance()->markingworkflow && $this->assignment->get_instance()->markingallocation && !has_capability('mod/assign:manageallocations', $this->assignment->get_context())) { $name = 'quickgrade_' . $row->id . '_allocatedmarker'; $o .= html_writer::empty_tag('input', ['type' => 'hidden', 'name' => $name, 'value' => $row->allocatedmarker]); } } } else { $o .= $this->output->container(get_string('markingworkflowstate' . $workflowstate, 'assign'), $workflowstate); } return $o; } /** * For download only - list current marking workflow state * * @param stdClass $row - The row of data * @return string The current marking workflow state */ public function col_workflowstate($row) { $state = $row->workflowstate; if (empty($state)) { $state = ASSIGN_MARKING_WORKFLOW_STATE_NOTMARKED; } return get_string('markingworkflowstate' . $state, 'assign'); } /** * list current marker * * @param stdClass $row - The row of data * @return id the user->id of the marker. */ public function col_allocatedmarker(stdClass $row) { static $markers = null; static $markerlist = array(); if ($markers === null) { list($sort, $params) = users_order_by_sql('u'); // Only enrolled users could be assigned as potential markers. $markers = get_enrolled_users($this->assignment->get_context(), 'mod/assign:grade', 0, 'u.*', $sort); $markerlist[0] = get_string('choosemarker', 'assign'); $viewfullnames = has_capability('moodle/site:viewfullnames', $this->assignment->get_context()); foreach ($markers as $marker) { $markerlist[$marker->id] = fullname($marker, $viewfullnames); } } if (empty($markerlist)) { // TODO: add some form of notification here that no markers are available. return ''; } if ($this->is_downloading()) { if (isset($markers[$row->allocatedmarker])) { return fullname($markers[$row->allocatedmarker], has_capability('moodle/site:viewfullnames', $this->assignment->get_context())); } else { return ''; } } if ($this->quickgrading && has_capability('mod/assign:manageallocations', $this->assignment->get_context()) && (empty($row->workflowstate) || $row->workflowstate == ASSIGN_MARKING_WORKFLOW_STATE_INMARKING || $row->workflowstate == ASSIGN_MARKING_WORKFLOW_STATE_NOTMARKED)) { $name = 'quickgrade_' . $row->id . '_allocatedmarker'; return html_writer::select($markerlist, $name, $row->allocatedmarker, false); } else if (!empty($row->allocatedmarker)) { $output = ''; if ($this->quickgrading) { // Add hidden field for quickgrading page. $name = 'quickgrade_' . $row->id . '_allocatedmarker'; $attributes = ['type' => 'hidden', 'name' => $name, 'value' => $row->allocatedmarker]; $output .= html_writer::empty_tag('input', $attributes); } $output .= $markerlist[$row->allocatedmarker]; return $output; } } /** * For download only - list all the valid options for this custom scale. * * @param stdClass $row - The row of data * @return string A list of valid options for the current scale */ public function col_scale($row) { global $DB; if (empty($this->scale)) { $dbparams = array('id' => -($this->assignment->get_instance()->grade)); $this->scale = $DB->get_record('scale', $dbparams); } if (!empty($this->scale->scale)) { return implode("\n", explode(',', $this->scale->scale)); } return ''; } /** * Display a grade with scales etc. * * @param string $grade * @param boolean $editable * @param int $userid The user id of the user this grade belongs to * @param int $modified Timestamp showing when the grade was last modified * @param float $deductedmark The deducted mark if penalty is applied * @return string The formatted grade */ public function display_grade($grade, $editable, $userid, $modified, float $deductedmark = 0) { if ($this->is_downloading()) { if ($this->assignment->get_instance()->grade >= 0) { if ($grade == -1 || $grade === null) { return ''; } $gradeitem = $this->assignment->get_grade_item(); return format_float($grade, $gradeitem->get_decimals()); } else { // This is a custom scale. $scale = $this->assignment->display_grade($grade, false); if ($scale == '-') { $scale = ''; } return $scale; } } return $this->assignment->display_grade($grade, $editable, $userid, $modified, $deductedmark); } /** * Get the team info for this user. * * @param stdClass $row * @return string The team name */ public function col_team(stdClass $row) { $submission = false; $group = false; $this->get_group_and_submission($row->id, $group, $submission, -1); if ($group) { return format_string($group->name, true, ['context' => $this->assignment->get_context()]); } else if ($this->assignment->get_instance()->preventsubmissionnotingroup) { $usergroups = $this->assignment->get_all_groups($row->id); if (count($usergroups) > 1) { return get_string('multipleteamsgrader', 'assign'); } else { return get_string('noteamgrader', 'assign'); } } return get_string('defaultteam', 'assign'); } /** * Use a static cache to try and reduce DB calls. * * @param int $userid The user id for this submission * @param int $group The groupid (returned) * @param stdClass|false $submission The stdClass submission or false (returned) * @param int $attemptnumber Return a specific attempt number (-1 for latest) */ protected function get_group_and_submission($userid, &$group, &$submission, $attemptnumber) { $group = false; if (isset($this->submissiongroups[$userid])) { $group = $this->submissiongroups[$userid]; } else { $group = $this->assignment->get_submission_group($userid, false); $this->submissiongroups[$userid] = $group; } $groupid = 0; if ($group) { $groupid = $group->id; } // Static cache is keyed by groupid and attemptnumber. // We may need both the latest and previous attempt in the same page. if (isset($this->groupsubmissions[$groupid . ':' . $attemptnumber])) { $submission = $this->groupsubmissions[$groupid . ':' . $attemptnumber]; } else { $submission = $this->assignment->get_group_submission($userid, $groupid, false, $attemptnumber); $this->groupsubmissions[$groupid . ':' . $attemptnumber] = $submission; } } /** * Format a list of outcomes. * * @param stdClass $row * @return string */ public function col_outcomes(stdClass $row) { $outcomes = ''; foreach ($this->gradinginfo->outcomes as $index => $outcome) { $options = make_grades_menu(-$outcome->scaleid); $options[0] = get_string('nooutcome', 'grades'); if ($this->quickgrading && !($outcome->grades[$row->userid]->locked)) { $select = ''; $outcomes .= $this->output->container($outcome->name . ': ' . $select, 'outcome'); } else { $name = $outcome->name . ': ' . $options[$outcome->grades[$row->userid]->grade]; if ($this->is_downloading()) { $outcomes .= $name; } else { $outcomes .= $this->output->container($name, 'outcome'); } } } return $outcomes; } /** * Format a user picture for display. * * @param stdClass $row * @return string * @deprecated since Moodle 4.5 * @todo Final deprecation in Moodle 6.0. See MDL-82336. */ #[\core\attribute\deprecated( replacement: null, since: '4.5', reason: 'Picture column is merged with fullname column' )] public function col_picture(stdClass $row) { \core\deprecation::emit_deprecation([$this, __FUNCTION__]); return $this->output->user_picture($row); } /** * Format a user record for display (link to profile). * * @param stdClass $row * @return string */ public function col_fullname($row) { if (!$this->is_downloading()) { $courseid = $this->assignment->get_course()->id; $fullname = $this->output->render(\core_user::get_profile_picture($row, null, ['courseid' => $courseid, 'includefullname' => true])); } else { $fullname = $this->assignment->fullname($row); } if (!$this->assignment->is_active_user($row->id)) { $suspendedstring = get_string('userenrolmentsuspended', 'grades'); $fullname .= ' ' . $this->output->pix_icon('i/enrolmentsuspended', $suspendedstring); $fullname = html_writer::tag('span', $fullname, array('class' => 'usersuspended')); } return $fullname; } /** * Insert a checkbox for selecting the current row for batch operations. * * @param stdClass $row * @return string */ public function col_select(stdClass $row) { $selectcol = ''; $selectcol .= ''; $selectcol .= ''; $selectcol .= ''; return $selectcol; } /** * Return a users grades from the listing of all grade data for this assignment. * * @param int $userid * @return mixed stdClass or false */ private function get_gradebook_data_for_user($userid) { if (isset($this->gradinginfo->items[0]) && $this->gradinginfo->items[0]->grades[$userid]) { return $this->gradinginfo->items[0]->grades[$userid]; } return false; } /** * Format a column of data for display. * * @param stdClass $row * @return string */ public function col_gradecanbechanged(stdClass $row) { $gradingdisabled = $this->assignment->grading_disabled($row->id, true, $this->gradinginfo); if ($gradingdisabled) { return get_string('no'); } else { return get_string('yes'); } } /** * Format a column of data for display * * @param stdClass $row * @return string */ public function col_grademax(stdClass $row) { if ($this->assignment->get_instance()->grade > 0) { $gradeitem = $this->assignment->get_grade_item(); return format_float($this->assignment->get_instance()->grade, $gradeitem->get_decimals()); } else { return ''; } } /** * Format a column of data for display. * * @param stdClass $row * @return string */ public function col_grade(stdClass $row): string { $gradingdisabled = $this->assignment->grading_disabled($row->id, true, $this->gradinginfo); $displaygrade = $this->display_grade($row->grade, $this->quickgrading && !$gradingdisabled, $row->userid, $row->timemarked); if (!$this->is_downloading() && $this->hasgrade) { $urlparams = [ 'id' => $this->assignment->get_course_module()->id, 'rownum' => 0, 'action' => 'grader', ]; if ($this->assignment->is_blind_marking()) { if (empty($row->recordid)) { $row->recordid = $this->assignment->get_uniqueid_for_user($row->userid); } $urlparams['blindid'] = $row->recordid; } else { $urlparams['userid'] = $row->userid; } $url = new moodle_url('/mod/assign/view.php', $urlparams); // The container with the grade information. $gradecontainer = $this->output->container($displaygrade, 'w-100'); $menu = new action_menu(); $menu->set_owner_selector('.gradingtable-actionmenu'); $menu->set_boundary('window'); $menu->set_kebab_trigger(get_string('gradeactions', 'assign')); $menu->set_additional_classes('ps-2 ms-auto'); // Prioritise the menu ahead of all other actions. $menu->prioritise = true; // Add the 'Grade' action item to the contextual menu. $menu->add(new action_menu_link_secondary($url, null, get_string('gradeverb'))); // The contextual menu container. $contextualmenucontainer = $this->output->container($this->output->render($menu), 'd-flex'); return $this->output->container($gradecontainer . $contextualmenucontainer, ['class' => 'd-flex']); } // The table data is being downloaded, or the user cannot grade; therefore, only the formatted grade for display // is returned. return $displaygrade; } /** * Format a column of data for display. * * @param stdClass $row * @return string */ public function col_finalgrade(stdClass $row) { $o = ''; $grade = $this->get_gradebook_data_for_user($row->userid); if ($grade) { $o = $this->display_grade($grade->grade, false, $row->userid, $row->timemarked, $grade->deductedmark); } return $o; } /** * Format a column of data for display. * * @param stdClass $row * @return string */ public function col_timemarked(stdClass $row) { $o = '-'; if ($row->timemarked && $row->grade !== null && $row->grade >= 0) { $o = userdate($row->timemarked); } if ($row->timemarked && $this->is_downloading()) { // Force it for downloads as it affects import. $o = userdate($row->timemarked); } return $o; } /** * Format a column of data for display. * * @param stdClass $row * @return string */ public function col_timesubmitted(stdClass $row) { $o = '-'; $group = false; $submission = false; $this->get_group_and_submission($row->id, $group, $submission, -1); if ($submission && $submission->timemodified && $submission->status != ASSIGN_SUBMISSION_STATUS_NEW) { $o = userdate($submission->timemodified); } else if ($row->timesubmitted && $row->status != ASSIGN_SUBMISSION_STATUS_NEW) { $o = userdate($row->timesubmitted); } return $o; } /** * Format a column of data for display * * @param stdClass $row * @return string */ public function col_status(stdClass $row) { global $USER; $o = ''; $instance = $this->assignment->get_instance($row->userid); $timelimitenabled = get_config('assign', 'enabletimelimit'); $due = $instance->duedate; if ($row->extensionduedate) { $due = $row->extensionduedate; } else if (!empty($row->duedate)) { // The override due date. $due = $row->duedate; } $group = false; $submission = false; if ($instance->teamsubmission) { $this->get_group_and_submission($row->id, $group, $submission, -1); } if ($instance->teamsubmission && !$group && !$instance->preventsubmissionnotingroup) { $group = true; } if ($group && $submission) { $timesubmitted = $submission->timemodified; $status = $submission->status; } else { $timesubmitted = $row->timesubmitted; $status = $row->status; } $displaystatus = $status; if ($displaystatus == 'new') { $displaystatus = ''; } // Generate the output for the submission contextual (action) menu. $actionmenu = ''; if (!$this->is_downloading() && $this->hasgrade) { $submissionsopen = $this->assignment->submissions_open( userid: $row->id, skipenrolled: true, submission: $submission ? $submission : $row, flags: $row, gradinginfo: $this->gradinginfo ); $caneditsubmission = $this->assignment->can_edit_submission($row->id, $USER->id); $urlparams = [ 'id' => $this->assignment->get_course_module()->id, 'sesskey' => sesskey(), 'page' => $this->currpage, ]; if ($this->assignment->is_blind_marking()) { if (empty($row->recordid)) { $row->recordid = $this->assignment->get_uniqueid_for_user($row->userid); } $urlparams['blindid'] = $row->recordid; } else { $urlparams['userid'] = $row->userid; } $baseactionurl = new moodle_url('/mod/assign/view.php', $urlparams); $menu = new action_menu(); $menu->set_owner_selector('.gradingtable-actionmenu'); $menu->set_boundary('window'); $menu->set_kebab_trigger(get_string('submissionactions', 'assign')); $menu->set_additional_classes('ps-2 ms-auto'); // Prioritise the menu ahead of all other actions. $menu->prioritise = true; // Hide for offline assignments. if ($this->assignment->is_any_submission_plugin_enabled()) { if ($submissionsopen && $USER->id != $row->id && $caneditsubmission) { // Edit submission action link. $baseactionurl->param('action', 'editsubmission'); $description = get_string('editsubmission', 'assign'); $menu->add(new action_menu_link_secondary($baseactionurl, null, $description)); } if (!$row->status || $row->status == ASSIGN_SUBMISSION_STATUS_DRAFT || !$this->assignment->get_instance()->submissiondrafts) { // Allow/prevent submission changes action link. $baseactionurl->param('action', $row->locked ? 'unlock' : 'lock'); $description = $row->locked ? get_string('allowsubmissionsshort', 'assign') : get_string('preventsubmissionsshort', 'assign'); $menu->add(new action_menu_link_secondary($baseactionurl, null, $description)); } } if (($this->assignment->get_instance()->duedate || $this->assignment->get_instance()->cutoffdate) && $this->hasgrantextension) { // Grant extension action link. $baseactionurl->param('action', 'grantextension'); $description = get_string('grantextension', 'assign'); $menu->add(new action_menu_link_secondary($baseactionurl, null, $description)); } if ($row->status == ASSIGN_SUBMISSION_STATUS_SUBMITTED && $this->assignment->get_instance()->submissiondrafts) { // Revert submission to draft action link. $baseactionurl->param('action', 'reverttodraft'); $description = get_string('reverttodraftshort', 'assign'); $menu->add(new action_menu_link_secondary($baseactionurl, null, $description)); } if ($row->status == ASSIGN_SUBMISSION_STATUS_DRAFT && $this->assignment->get_instance()->submissiondrafts && $caneditsubmission && $submissionsopen && $row->id != $USER->id) { // Submit for grading action link. $baseactionurl->param('action', 'submitotherforgrading'); $description = get_string('submitforgrading', 'assign'); $menu->add(new action_menu_link_secondary($baseactionurl, null, $description)); } $ismanual = $this->assignment->get_instance()->attemptreopenmethod == ASSIGN_ATTEMPT_REOPEN_METHOD_MANUAL; $hassubmission = !empty($row->status); $notreopened = $hassubmission && $row->status != ASSIGN_SUBMISSION_STATUS_REOPENED; $isunlimited = $this->assignment->get_instance()->maxattempts == ASSIGN_UNLIMITED_ATTEMPTS; $hasattempts = $isunlimited || $row->attemptnumber < $this->assignment->get_instance()->maxattempts - 1; if ($ismanual && $hassubmission && $notreopened && $hasattempts) { // Allow another attempt action link. $baseactionurl->param('action', 'addattempt'); $description = get_string('addattempt', 'assign'); $menu->add(new action_menu_link_secondary($baseactionurl, null, $description)); } if ($this->assignment->is_any_submission_plugin_enabled()) { if ($USER->id != $row->id && $caneditsubmission && !empty($row->status)) { // Remove submission action link. This action link should be always placed as the last item // within the contextual menu. $baseactionurl->param('action', 'removesubmissionconfirm'); $description = get_string('removesubmission', 'assign'); $menu->add(new action_menu_link_secondary($baseactionurl, null, $description, ['class' => 'text-danger'])); } } $actionmenu = $this->output->render($menu); } $actionmenucontainer = $this->output->container($actionmenu, 'd-flex'); // Generate the output for the submission information. $submissioninfo = ''; if ($this->assignment->is_any_submission_plugin_enabled()) { $submissioninfo .= $this->output->container(get_string('submissionstatus_' . $displaystatus, 'assign'), ['class' => 'submissionstatus' . $displaystatus]); if ($due && $timesubmitted > $due && $status != ASSIGN_SUBMISSION_STATUS_NEW) { $usertime = format_time($timesubmitted - $due); $latemessage = get_string('submittedlateshort', 'assign', $usertime); $submissioninfo .= $this->output->container($latemessage, 'latesubmission'); } else if ($timelimitenabled && $instance->timelimit && !empty($submission->timestarted) && ($timesubmitted - $submission->timestarted > $instance->timelimit) && $status != ASSIGN_SUBMISSION_STATUS_NEW) { $usertime = format_time($timesubmitted - $submission->timestarted - $instance->timelimit); $latemessage = get_string('submittedlateshort', 'assign', $usertime); $submissioninfo .= $this->output->container($latemessage, 'latesubmission'); } if ($row->locked) { $lockedstr = get_string('submissionslockedshort', 'assign'); $submissioninfo .= $this->output->container($lockedstr, 'lockedsubmission'); } // Add status of "grading" if markflow is not enabled. if (!$instance->markingworkflow) { if ($row->grade !== null && $row->grade >= 0) { if ($row->timemarked < $row->timesubmitted) { $submissioninfo .= $this->output->container(get_string('gradedfollowupsubmit', 'assign'), 'gradingreminder'); } else { $submissioninfo .= $this->output->container(get_string('graded', 'assign'), 'submissiongraded'); } } else if (!$timesubmitted || $status == ASSIGN_SUBMISSION_STATUS_NEW) { $now = time(); if ($due && ($now > $due)) { $overduestr = get_string('overdue', 'assign', format_time($now - $due)); $submissioninfo .= $this->output->container($overduestr, 'overduesubmission'); } } } } if ($instance->markingworkflow) { $submissioninfo .= $this->col_workflowstatus($row); } if ($row->extensionduedate) { $userdate = userdate($row->extensionduedate); $extensionstr = get_string('userextensiondate', 'assign', $userdate); $submissioninfo .= $this->output->container($extensionstr, 'extensiondate'); } // The container with the submission information. $submissoninfocontainer = $this->output->container($submissioninfo, 'submissioninfo w-100'); $o .= $this->output->container($submissoninfocontainer . $actionmenucontainer, 'd-flex'); if ($this->is_downloading()) { $o = strip_tags(rtrim(str_replace('', ' - ', $o), '- ')); } return $o; } /** * Format a column of data for display. * * @param stdClass $row * @return string */ public function col_allowsubmissionsfromdate(stdClass $row) { $o = ''; if ($row->allowsubmissionsfromdate) { $userdate = userdate($row->allowsubmissionsfromdate); $o = ($this->is_downloading()) ? $userdate : $this->output->container($userdate, 'allowsubmissionsfromdate'); } return $o; } /** * Format a column of data for display. * * @param stdClass $row * @return string */ public function col_duedate(stdClass $row) { $o = ''; if ($row->duedate) { $userdate = userdate($row->duedate); $o = ($this->is_downloading()) ? $userdate : $this->output->container($userdate, 'duedate'); } return $o; } /** * Format a column of data for display. * * @param stdClass $row * @return string */ public function col_cutoffdate(stdClass $row) { $o = ''; if ($row->cutoffdate) { $userdate = userdate($row->cutoffdate); $o = ($this->is_downloading()) ? $userdate : $this->output->container($userdate, 'cutoffdate'); } return $o; } /** * Format a column of data for display. * * @param stdClass $row * @return string * @deprecated since Moodle 4.5 * @todo Final deprecation in Moodle 6.0. See MDL-82336. */ #[\core\attribute\deprecated( replacement: null, since: '4.5', reason: 'Userid column is merged with status and grade columns' )] public function col_userid(stdClass $row) { global $USER; \core\deprecation::emit_deprecation([$this, __FUNCTION__]); $edit = ''; $actions = array(); $urlparams = array('id' => $this->assignment->get_course_module()->id, 'rownum' => 0, 'action' => 'grader'); if ($this->assignment->is_blind_marking()) { if (empty($row->recordid)) { $row->recordid = $this->assignment->get_uniqueid_for_user($row->userid); } $urlparams['blindid'] = $row->recordid; } else { $urlparams['userid'] = $row->userid; } $url = new moodle_url('/mod/assign/view.php', $urlparams); $noimage = null; if (!$row->grade) { $description = get_string('gradeverb'); } else { $description = get_string('updategrade', 'assign'); } $actions['grade'] = new action_menu_link_secondary( $url, $noimage, $description ); // Everything we need is in the row. $submission = $row; $flags = $row; if ($this->assignment->get_instance()->teamsubmission) { // Use the cache for this. $submission = false; $group = false; $this->get_group_and_submission($row->id, $group, $submission, -1); } $submissionsopen = $this->assignment->submissions_open($row->id, true, $submission, $flags, $this->gradinginfo); $caneditsubmission = $this->assignment->can_edit_submission($row->id, $USER->id); // Hide for offline assignments. if ($this->assignment->is_any_submission_plugin_enabled()) { if (!$row->status || $row->status == ASSIGN_SUBMISSION_STATUS_DRAFT || !$this->assignment->get_instance()->submissiondrafts) { if (!$row->locked) { $urlparams = array('id' => $this->assignment->get_course_module()->id, 'userid' => $row->id, 'action' => 'lock', 'sesskey' => sesskey(), 'page' => $this->currpage); $url = new moodle_url('/mod/assign/view.php', $urlparams); $description = get_string('preventsubmissionsshort', 'assign'); $actions['lock'] = new action_menu_link_secondary( $url, $noimage, $description ); } else { $urlparams = array('id' => $this->assignment->get_course_module()->id, 'userid' => $row->id, 'action' => 'unlock', 'sesskey' => sesskey(), 'page' => $this->currpage); $url = new moodle_url('/mod/assign/view.php', $urlparams); $description = get_string('allowsubmissionsshort', 'assign'); $actions['unlock'] = new action_menu_link_secondary( $url, $noimage, $description ); } } if ($submissionsopen && $USER->id != $row->id && $caneditsubmission) { $urlparams = array('id' => $this->assignment->get_course_module()->id, 'userid' => $row->id, 'action' => 'editsubmission', 'sesskey' => sesskey(), 'page' => $this->currpage); $url = new moodle_url('/mod/assign/view.php', $urlparams); $description = get_string('editsubmission', 'assign'); $actions['editsubmission'] = new action_menu_link_secondary( $url, $noimage, $description ); } if ($USER->id != $row->id && $caneditsubmission && !empty($row->status)) { $urlparams = array('id' => $this->assignment->get_course_module()->id, 'userid' => $row->id, 'action' => 'removesubmissionconfirm', 'sesskey' => sesskey(), 'page' => $this->currpage); $url = new moodle_url('/mod/assign/view.php', $urlparams); $description = get_string('removesubmission', 'assign'); $actions['removesubmission'] = new action_menu_link_secondary( $url, $noimage, $description ); } } if (($this->assignment->get_instance()->duedate || $this->assignment->get_instance()->cutoffdate) && $this->hasgrantextension) { $urlparams = array('id' => $this->assignment->get_course_module()->id, 'userid' => $row->id, 'action' => 'grantextension', 'sesskey' => sesskey(), 'page' => $this->currpage); $url = new moodle_url('/mod/assign/view.php', $urlparams); $description = get_string('grantextension', 'assign'); $actions['grantextension'] = new action_menu_link_secondary( $url, $noimage, $description ); } if ($row->status == ASSIGN_SUBMISSION_STATUS_SUBMITTED && $this->assignment->get_instance()->submissiondrafts) { $urlparams = array('id' => $this->assignment->get_course_module()->id, 'userid' => $row->id, 'action' => 'reverttodraft', 'sesskey' => sesskey(), 'page' => $this->currpage); $url = new moodle_url('/mod/assign/view.php', $urlparams); $description = get_string('reverttodraftshort', 'assign'); $actions['reverttodraft'] = new action_menu_link_secondary( $url, $noimage, $description ); } if ($row->status == ASSIGN_SUBMISSION_STATUS_DRAFT && $this->assignment->get_instance()->submissiondrafts && $caneditsubmission && $submissionsopen && $row->id != $USER->id) { $urlparams = array('id' => $this->assignment->get_course_module()->id, 'userid' => $row->id, 'action' => 'submitotherforgrading', 'sesskey' => sesskey(), 'page' => $this->currpage); $url = new moodle_url('/mod/assign/view.php', $urlparams); $description = get_string('submitforgrading', 'assign'); $actions['submitforgrading'] = new action_menu_link_secondary( $url, $noimage, $description ); } $ismanual = $this->assignment->get_instance()->attemptreopenmethod == ASSIGN_ATTEMPT_REOPEN_METHOD_MANUAL; $hassubmission = !empty($row->status); $notreopened = $hassubmission && $row->status != ASSIGN_SUBMISSION_STATUS_REOPENED; $isunlimited = $this->assignment->get_instance()->maxattempts == ASSIGN_UNLIMITED_ATTEMPTS; $hasattempts = $isunlimited || $row->attemptnumber < $this->assignment->get_instance()->maxattempts - 1; if ($ismanual && $hassubmission && $notreopened && $hasattempts) { $urlparams = array('id' => $this->assignment->get_course_module()->id, 'userid' => $row->id, 'action' => 'addattempt', 'sesskey' => sesskey(), 'page' => $this->currpage); $url = new moodle_url('/mod/assign/view.php', $urlparams); $description = get_string('addattempt', 'assign'); $actions['addattempt'] = new action_menu_link_secondary( $url, $noimage, $description ); } $menu = new action_menu(); $menu->set_owner_selector('.gradingtable-actionmenu'); $menu->set_boundary('window'); $menu->set_menu_trigger(get_string('edit')); foreach ($actions as $action) { $menu->add($action); } // Prioritise the menu ahead of all other actions. $menu->prioritise = true; $edit .= $this->output->render($menu); return $edit; } /** * Write the plugin summary with an optional link to view the full feedback/submission. * * @param assign_plugin $plugin Submission plugin or feedback plugin * @param stdClass $item Submission or grade * @param string $returnaction The return action to pass to the * view_submission page (the current page) * @param string $returnparams The return params to pass to the view_submission * page (the current page) * @return string The summary with an optional link */ private function format_plugin_summary_with_link(assign_plugin $plugin, stdClass $item, $returnaction, $returnparams) { $link = ''; $showviewlink = false; $summary = $plugin->view_summary($item, $showviewlink); $separator = ''; if ($showviewlink) { $viewstr = get_string('view' . substr($plugin->get_subtype(), strlen('assign')), 'assign'); $icon = $this->output->pix_icon('t/viewdetails', $viewstr); $urlparams = array('id' => $this->assignment->get_course_module()->id, 'sid' => $item->id, 'gid' => $item->id, 'plugin' => $plugin->get_type(), 'action' => 'viewplugin' . $plugin->get_subtype(), 'returnaction' => $returnaction, 'returnparams' => http_build_query($returnparams)); $url = new moodle_url('/mod/assign/view.php', $urlparams); $link = $this->output->action_link($url, $icon); $separator = $this->output->spacer(array(), true); } return $link . $separator . $summary; } /** * Format the submission and feedback columns. * * @param string $colname The column name * @param stdClass $row The submission row * @return mixed string or NULL */ public function other_cols($colname, $row) { // For extra user fields the result is already in $row. if (empty($this->plugincache[$colname])) { return parent::other_cols($colname, $row); } // This must be a plugin field. $plugincache = $this->plugincache[$colname]; $plugin = $plugincache[0]; $field = null; if (isset($plugincache[1])) { $field = $plugincache[1]; } if ($plugin->is_visible() && $plugin->is_enabled()) { if ($plugin->get_subtype() == 'assignsubmission') { if ($this->assignment->get_instance()->teamsubmission) { $group = false; $submission = false; $this->get_group_and_submission($row->id, $group, $submission, -1); if ($submission) { if ($submission->status == ASSIGN_SUBMISSION_STATUS_REOPENED) { // For a newly reopened submission - we want to show the previous submission in the table. $this->get_group_and_submission($row->id, $group, $submission, $submission->attemptnumber-1); } if (isset($field)) { return $plugin->get_editor_text($field, $submission->id); } return $this->format_plugin_summary_with_link($plugin, $submission, 'grading', array()); } } else if ($row->submissionid) { if ($row->status == ASSIGN_SUBMISSION_STATUS_REOPENED) { // For a newly reopened submission - we want to show the previous submission in the table. $submission = $this->assignment->get_user_submission($row->userid, false, $row->attemptnumber - 1); } else { $submission = new stdClass(); $submission->id = $row->submissionid; $submission->timecreated = $row->firstsubmission; $submission->timemodified = $row->timesubmitted; $submission->assignment = $this->assignment->get_instance()->id; $submission->userid = $row->userid; $submission->attemptnumber = $row->attemptnumber; } // Field is used for only for import/export and refers the the fieldname for the text editor. if (isset($field)) { return $plugin->get_editor_text($field, $submission->id); } return $this->format_plugin_summary_with_link($plugin, $submission, 'grading', array()); } } else { $grade = null; if (isset($field)) { return $plugin->get_editor_text($field, $row->gradeid); } if ($row->gradeid) { $grade = new stdClass(); $grade->id = $row->gradeid; $grade->timecreated = $row->firstmarked; $grade->timemodified = $row->timemarked; $grade->assignment = $this->assignment->get_instance()->id; $grade->userid = $row->userid; $grade->grade = $row->grade; $grade->mailed = $row->mailed; $grade->attemptnumber = $row->attemptnumber; } if ($this->quickgrading && $plugin->supports_quickgrading()) { return $plugin->get_quickgrading_html($row->userid, $grade); } else if ($grade) { return $this->format_plugin_summary_with_link($plugin, $grade, 'grading', array()); } } } return ''; } /** * Using the current filtering and sorting - load all rows and return a single column from them. * * @param string $columnname The name of the raw column data * @return array of data */ public function get_column_data($columnname) { $this->setup(); $this->currpage = 0; $this->query_db($this->tablemaxrows); $result = array(); foreach ($this->rawdata as $row) { $result[] = $row->$columnname; } return $result; } /** * Return things to the renderer. * * @return string the assignment name */ public function get_assignment_name() { return $this->assignment->get_instance()->name; } /** * Return things to the renderer. * * @return int the course module id */ public function get_course_module_id() { return $this->assignment->get_course_module()->id; } /** * Return things to the renderer. * * @return int the course id */ public function get_course_id() { return $this->assignment->get_course()->id; } /** * Return things to the renderer. * * @return stdClass The course context */ public function get_course_context() { return $this->assignment->get_course_context(); } /** * Return things to the renderer. * * @return bool Does this assignment accept submissions */ public function submissions_enabled() { return $this->assignment->is_any_submission_plugin_enabled(); } /** * Return things to the renderer. * * @return bool Can this user view all grades (the gradebook) */ public function can_view_all_grades() { $context = $this->assignment->get_course_context(); return has_capability('gradereport/grader:view', $context) && has_capability('moodle/grade:viewall', $context); } /** * Always return a valid sort - even if the userid column is missing. * @return array column name => SORT_... constant. */ public function get_sort_columns() { $result = parent::get_sort_columns(); $assignment = $this->assignment->get_instance(); if (empty($assignment->blindmarking)) { $result = array_merge($result, array('userid' => SORT_ASC)); } else { $result = array_merge($result, [ 'COALESCE(s.timecreated, ' . time() . ')' => SORT_ASC, 'COALESCE(s.id, ' . PHP_INT_MAX . ')' => SORT_ASC, 'um.id' => SORT_ASC, ]); } return $result; } /** * Override the table show_hide_link to not show for select column. * * @param string $column the column name, index into various names. * @param int $index numerical index of the column. * @return string HTML fragment. */ protected function show_hide_link($column, $index) { if ($index > 0 || !$this->hasgrade) { return parent::show_hide_link($column, $index); } return ''; } /** * Overides setup to ensure it will only run a single time. */ public function setup() { // Check if the setup function has been called before, we should not run it twice. // If we do the sortorder of the table will be broken. if (!empty($this->setup)) { return; } parent::setup(); } /** * Returns the html for the paging bar. * * @return string */ public function get_paging_bar(): string { global $OUTPUT; if ($this->use_pages) { $pagingbar = new paging_bar($this->totalrows, $this->currpage, $this->pagesize, $this->baseurl); $pagingbar->pagevar = $this->request[TABLE_VAR_PAGE]; return $OUTPUT->render($pagingbar); } return ''; } /** * Returns the html for the paging selector. * * @return string */ public function get_paging_selector(): string { global $OUTPUT; if ($this->use_pages) { $pagingoptions = [...$this->pagingoptions, $this->perpage]; // To make sure the actual page size is within the options. $pagingoptions = array_unique($pagingoptions); sort($pagingoptions); $pagingoptions = array_combine($pagingoptions, $pagingoptions); $maxperpage = get_config('assign', 'maxperpage'); if (isset($maxperpage) && $maxperpage != -1) { // Remove any options that are greater than the maxperpage. $pagingoptions = array_filter($pagingoptions, fn($value) => $value <= $maxperpage); } else { $pagingoptions[-1] = get_string('all'); } $data = [ 'baseurl' => $this->baseurl->out(false), 'options' => array_map(fn($key, $name): array => [ 'name' => $name, 'value' => $key, 'selected' => $key == $this->perpage, ], array_keys($pagingoptions), $pagingoptions), ]; return $OUTPUT->render_from_template('mod_assign/grading_paging_selector', $data); } return ''; } /** * Finish the HTML output. * This function is essentially a copy of the parent function except the paging bar not being rendered. * * @return void */ public function finish_html(): void { if (!$this->started_output) { // No data has been added to the table. $this->print_nothing_to_display(); } else { // Print empty rows to fill the table to the current pagesize. // This is done so the header aria-controls attributes do not point to // non-existent elements. $emptyrow = array_fill(0, count($this->columns), ''); while ($this->currentrow < $this->pagesize) { $this->print_row($emptyrow, 'emptyrow'); } echo html_writer::end_tag('tbody'); echo html_writer::end_tag('table'); if ($this->responsive) { echo html_writer::end_tag('div'); } $this->wrap_html_finish(); if (in_array(TABLE_P_BOTTOM, $this->showdownloadbuttonsat)) { echo $this->download_buttons(); } // Render the dynamic table footer. echo $this->get_dynamic_table_html_end(); } } /** * Start the HTML output. * This function is essentially a copy of the parent function except the paging bar not being rendered. * * @return void */ public function start_html(): void { // Render the dynamic table header. echo $this->get_dynamic_table_html_start(); // Render button to allow user to reset table preferences. echo $this->render_reset_button(); // Do we need to print initial bars? $this->print_initials_bar(); if (in_array(TABLE_P_TOP, $this->showdownloadbuttonsat)) { echo $this->download_buttons(); } $this->wrap_html_start(); // Start of main data table. if ($this->responsive) { echo html_writer::start_tag('div', ['class' => 'table-responsive']); } echo html_writer::start_tag('table', $this->attributes) . $this->render_caption(); } } adminlib.php000064400000030066152265517350007054 0ustar00. /** * This file contains the classes for the admin settings of the 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(); require_once($CFG->libdir . '/adminlib.php'); /** * Admin external page that displays a list of the installed submission plugins. * * @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_admin_page_manage_assign_plugins extends admin_externalpage { /** @var string the name of plugin subtype */ private $subtype = ''; /** * The constructor - calls parent constructor * * @param string $subtype */ public function __construct($subtype) { $this->subtype = $subtype; $url = new moodle_url('/mod/assign/adminmanageplugins.php', array('subtype'=>$subtype)); parent::__construct('manage' . $subtype . 'plugins', get_string('manage' . $subtype . 'plugins', 'assign'), $url); } /** * Search plugins for the specified string * * @param string $query The string to search for * @return array */ public function search($query) { if ($result = parent::search($query)) { return $result; } $found = false; foreach (core_component::get_plugin_list($this->subtype) as $name => $notused) { if (strpos(core_text::strtolower(get_string('pluginname', $this->subtype . '_' . $name)), $query) !== false) { $found = true; break; } } if ($found) { $result = new stdClass(); $result->page = $this; $result->settings = array(); return array($this->name => $result); } else { return array(); } } } /** * Class that handles the display and configuration of the list of submission plugins. * * @package mod_assign * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class assign_plugin_manager { /** @var object the url of the manage submission plugin page */ private $pageurl; /** @var string any error from the current action */ private $error = ''; /** @var string either submission or feedback */ private $subtype = ''; /** * Constructor for this assignment plugin manager * @param string $subtype - either assignsubmission or assignfeedback */ public function __construct($subtype) { $this->pageurl = new moodle_url('/mod/assign/adminmanageplugins.php', array('subtype'=>$subtype)); $this->subtype = $subtype; } /** * Return a list of plugins sorted by the order defined in the admin interface * * @return array The list of plugins */ public function get_sorted_plugins_list() { $names = core_component::get_plugin_list($this->subtype); $result = array(); foreach ($names as $name => $path) { $idx = get_config($this->subtype . '_' . $name, 'sortorder'); if (!$idx) { $idx = 0; } while (array_key_exists($idx, $result)) { $idx +=1; } $result[$idx] = $name; } ksort($result); return $result; } /** * Util function for writing an action icon link * * @param string $action URL parameter to include in the link * @param string $plugin URL parameter to include in the link * @param string $icon The key to the icon to use (e.g. 't/up') * @param string $alt The string description of the link used as the title and alt text * @return string The icon/link */ private function format_icon_link($action, $plugin, $icon, $alt) { global $OUTPUT; $url = $this->pageurl; if ($action === 'delete') { $url = core_plugin_manager::instance()->get_uninstall_url($this->subtype.'_'.$plugin, 'manage'); if (!$url) { return ' '; } return html_writer::link($url, get_string('uninstallplugin', 'core_admin')); } return $OUTPUT->action_icon(new moodle_url($url, array('action' => $action, 'plugin'=> $plugin, 'sesskey' => sesskey())), new pix_icon($icon, $alt, 'moodle', array('title' => $alt)), null, array('title' => $alt)) . ' '; } /** * Write the HTML for the submission plugins table. * * @return None */ private function view_plugins_table() { global $OUTPUT, $CFG; require_once($CFG->libdir . '/tablelib.php'); // Set up the table. $this->view_header(); $table = new flexible_table($this->subtype . 'pluginsadminttable'); $table->define_baseurl($this->pageurl); $table->define_columns(array('pluginname', 'version', 'hideshow', 'order', 'settings', 'uninstall')); $table->define_headers(array(get_string($this->subtype . 'pluginname', 'assign'), get_string('version'), get_string('hideshow', 'assign'), get_string('order'), get_string('settings'), get_string('uninstallplugin', 'core_admin'))); $table->set_attribute('id', $this->subtype . 'plugins'); $table->set_attribute('class', 'admintable generaltable'); $table->setup(); $plugins = $this->get_sorted_plugins_list(); $shortsubtype = substr($this->subtype, strlen('assign')); foreach ($plugins as $idx => $plugin) { $row = array(); $class = ''; $row[] = get_string('pluginname', $this->subtype . '_' . $plugin); $row[] = get_config($this->subtype . '_' . $plugin, 'version'); $visible = !get_config($this->subtype . '_' . $plugin, 'disabled'); if ($visible) { $row[] = $this->format_icon_link('hide', $plugin, 't/hide', get_string('disable')); } else { $row[] = $this->format_icon_link('show', $plugin, 't/show', get_string('enable')); $class = 'dimmed_text'; } $movelinks = ''; if (!$idx == 0) { $movelinks .= $this->format_icon_link('moveup', $plugin, 't/up', get_string('up')); } else { $movelinks .= $OUTPUT->spacer(array('width'=>16)); } if ($idx != count($plugins) - 1) { $movelinks .= $this->format_icon_link('movedown', $plugin, 't/down', get_string('down')); } $row[] = $movelinks; $exists = file_exists($CFG->dirroot . '/mod/assign/' . $shortsubtype . '/' . $plugin . '/settings.php'); if ($row[1] != '' && $exists) { $row[] = html_writer::link(new moodle_url('/admin/settings.php', array('section' => $this->subtype . '_' . $plugin)), get_string('settings')); } else { $row[] = ' '; } $row[] = $this->format_icon_link('delete', $plugin, 't/delete', get_string('uninstallplugin', 'core_admin')); $table->add_data($row, $class); } $table->finish_output(); $this->view_footer(); } /** * Write the page header * * @return None */ private function view_header() { global $OUTPUT; admin_externalpage_setup('manage' . $this->subtype . 'plugins'); // Print the page heading. echo $OUTPUT->header(); echo $OUTPUT->heading(get_string('manage' . $this->subtype . 'plugins', 'assign')); } /** * Write the page footer * * @return None */ private function view_footer() { global $OUTPUT; echo $OUTPUT->footer(); } /** * Check this user has permission to edit the list of installed plugins * * @return None */ private function check_permissions() { // Check permissions. require_login(); $systemcontext = context_system::instance(); require_capability('moodle/site:config', $systemcontext); } /** * Hide this plugin. * * @param string $plugin - The plugin to hide * @return string The next page to display */ public function hide_plugin($plugin) { $class = \core_plugin_manager::resolve_plugininfo_class($this->subtype); $class::enable_plugin($plugin, false); return 'view'; } /** * Change the order of this plugin. * * @param string $plugintomove - The plugin to move * @param string $dir - up or down * @return string The next page to display */ public function move_plugin($plugintomove, $dir) { // Get a list of the current plugins. $plugins = $this->get_sorted_plugins_list(); $currentindex = 0; // Throw away the keys. $plugins = array_values($plugins); // Find this plugin in the list. foreach ($plugins as $key => $plugin) { if ($plugin == $plugintomove) { $currentindex = $key; break; } } // Make the switch. if ($dir == 'up') { if ($currentindex > 0) { $tempplugin = $plugins[$currentindex - 1]; $plugins[$currentindex - 1] = $plugins[$currentindex]; $plugins[$currentindex] = $tempplugin; } } else if ($dir == 'down') { if ($currentindex < (count($plugins) - 1)) { $tempplugin = $plugins[$currentindex + 1]; $plugins[$currentindex + 1] = $plugins[$currentindex]; $plugins[$currentindex] = $tempplugin; } } // Save the new normal order. foreach ($plugins as $key => $plugin) { set_config('sortorder', $key, $this->subtype . '_' . $plugin); } return 'view'; } /** * Show this plugin. * * @param string $plugin - The plugin to show * @return string The next page to display */ public function show_plugin($plugin) { $class = \core_plugin_manager::resolve_plugininfo_class($this->subtype); $class::enable_plugin($plugin, true); return 'view'; } /** * This is the entry point for this controller class. * * @param string $action - The action to perform * @param string $plugin - Optional name of a plugin type to perform the action on * @return None */ public function execute($action, $plugin) { if ($action == null) { $action = 'view'; } $this->check_permissions(); // Process. if ($action == 'hide' && $plugin != null) { $action = $this->hide_plugin($plugin); } else if ($action == 'show' && $plugin != null) { $action = $this->show_plugin($plugin); } else if ($action == 'moveup' && $plugin != null) { $action = $this->move_plugin($plugin, 'up'); } else if ($action == 'movedown' && $plugin != null) { $action = $this->move_plugin($plugin, 'down'); } // View. if ($action == 'view') { $this->view_plugins_table(); } } } locallib.php000064400001475216152265517350007071 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', ); } gradingoptionsform.php000064400000002131152265517350011200 0ustar00. /** * File only retained to prevent fatal errors in code that tries to require/include this. * * @deprecated since 4.5. This file is no longer required. * @todo This file should be removed in Moodle 6.0. See MDL-82876. */ defined('MOODLE_INTERNAL') || die('Direct access to this script is forbidden.'); debugging('This file is no longer required in Moodle 4.5+. Please do not include/require it.', DEBUG_DEVELOPER); submissionplugin.php000064400000013104152265517350010701 0ustar00. /** * This file contains the definition for the abstract class for submission_plugin * * This class provides all the functionality for submission plugins. * * @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(); require_once($CFG->dirroot . '/mod/assign/assignmentplugin.php'); /** * Abstract base class for submission plugin 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 */ abstract class assign_submission_plugin extends assign_plugin { /** * return subtype name of the plugin * * @return string */ final public function get_subtype() { return 'assignsubmission'; } /** * This plugin accepts submissions from a student * The comments plugin has no submission component so should not be counted * when determining whether to show the edit submission link. * @return boolean */ public function allow_submissions() { return true; } /** * Check if the submission plugin has all the required data to allow the work * to be submitted for grading * @param stdClass $submission the assign_submission record being submitted. * @return bool|string 'true' if OK to proceed with submission, otherwise a * a message to display to the user */ public function precheck_submission($submission) { return true; } /** * Carry out any extra processing required when the work is submitted for grading * @param stdClass $submission the assign_submission record being submitted. * @return void */ public function submit_for_grading($submission) { } /** * Copy the plugin specific submission data to a new submission record. * * @param stdClass $oldsubmission - Old submission record * @param stdClass $submission - New submission record * @return bool */ public function copy_submission(stdClass $oldsubmission, stdClass $submission) { return true; } /** * Carry out any extra processing required when the work is locked. * * @param stdClass|false $submission - assign_submission data if any * @param stdClass $flags - User flags record * @return void */ public function lock($submission, stdClass $flags) { } /** * Carry out any extra processing required when the work is unlocked. * * @param stdClass|false $submission - assign_submission data if any * @param stdClass $flags - User flags record * @return void */ public function unlock($submission, stdClass $flags) { } /** * Carry out any extra processing required when the work reverted to draft. * * @param stdClass $submission - assign_submission data * @return void */ public function revert_to_draft(stdClass $submission) { } /** * Remove any saved data from this submission. * * @param stdClass $submission - assign_submission data * @return void */ public function remove(stdClass $submission) { } /** * Carry out any extra processing required when a student is given a new attempt * (i.e. when the submission is "reopened" * @param stdClass $oldsubmission The previous attempt * @param stdClass $newsubmission The new attempt */ public function add_attempt(stdClass $oldsubmission, stdClass $newsubmission) { } /** * Determine if a submission is empty * * This is distinct from is_empty in that it is intended to be used to * determine if a submission made before saving is empty. * * @param stdClass $data The submission data * @return bool */ public function submission_is_empty(stdClass $data) { return false; } /** * Determine if the plugin allows image file conversion * @return bool */ public function allow_image_conversion() { return false; } /** * Summarise a submission for inclusion in messages. * * Moodle messages can be sent as either HTML or plain text, so you need to * produce two versions of the summary. * * If there is nothing in the submission from your plugin return an array of two empty strings. * * The plain text version should finish in a newline character. * The HTML version should have block-level elements like headings or

s as the outer elements. * * @param stdClass $submission the assign_submission record for the submission the message is about. * @return string[] with two elements, a plain text summary and an HTML summary. */ public function submission_summary_for_messages(stdClass $submission): array { return ['', '']; } } module.js000064400000012174152265517350006407 0ustar00M.mod_assign = {}; M.mod_assign.init_tree = function(Y, expand_all, htmlid) { var treeElement = Y.one('#'+htmlid); if (treeElement) { Y.use('yui2-treeview', 'node-event-simulate', function(Y) { var tree = new Y.YUI2.widget.TreeView(htmlid); tree.subscribe("clickEvent", function(node, event) { // We want normal clicking which redirects to url. return false; }); tree.subscribe("enterKeyPressed", function(node) { // We want keyboard activation to trigger a click on the first link. Y.one(node.getContentEl()).one('a').simulate('click'); return false; }); if (expand_all) { tree.expandAll(); } tree.render(); }); } }; M.mod_assign.init_grading_table = function(Y) { Y.use('node', function(Y) { const checkboxes = Y.all('td.c0 input'); let rowelement; checkboxes.each(function(node) { node.on('change', function(e) { rowelement = e.currentTarget.get('parentNode').get('parentNode'); if (e.currentTarget.get('checked')) { rowelement.removeClass('unselectedrow'); rowelement.addClass('selectedrow'); } else { rowelement.removeClass('selectedrow'); rowelement.addClass('unselectedrow'); } }); rowelement = node.get('parentNode').get('parentNode'); if (node.get('checked')) { rowelement.removeClass('unselectedrow'); rowelement.addClass('selectedrow'); } else { rowelement.removeClass('selectedrow'); rowelement.addClass('unselectedrow'); } }); const selectall = Y.one('th.c0 input'); if (selectall) { selectall.on('change', function(e) { Y.all('td.c0 input[type="checkbox"]').each(function(node) { rowelement = node.get('parentNode').get('parentNode'); if (e.currentTarget.get('checked')) { node.set('checked', true); rowelement.removeClass('unselectedrow'); rowelement.addClass('selectedrow'); } else { node.set('checked', false); rowelement.removeClass('selectedrow'); rowelement.addClass('unselectedrow'); } }); }); } var quickgrade = Y.all('.gradingtable .quickgrade'); quickgrade.each(function(quick) { quick.on('change', function(e) { this.get('parentNode').addClass('quickgrademodified'); }); }); }); }; M.mod_assign.init_plugin_summary = function(Y, subtype, type, submissionid) { var suffix = subtype + '_' + type + '_' + submissionid; var classname = 'contract_' + suffix; var contract = Y.one('.' + classname); if (contract) { contract.on('click', function(e) { e.preventDefault(); var link = e.currentTarget || e.target; var linkclasses = link.getAttribute('class').split(' '); var thissuffix = ''; for (var i = 0; i < linkclasses.length; i++) { classname = linkclasses[i]; if (classname.indexOf('contract_') == 0) { thissuffix = classname.substr(9); } } var fullclassname = 'full_' + thissuffix; var full = Y.one('.' + fullclassname); if (full) { full.hide(false); } var summaryclassname = 'summary_' + thissuffix; var summary = Y.one('.' + summaryclassname); if (summary) { summary.show(false); summary.one('a.expand_' + thissuffix).focus(); } }); } classname = 'expand_' + suffix; var expand = Y.one('.' + classname); var full = Y.one('.full_' + suffix); if (full) { full.hide(false); full.toggleClass('hidefull'); } if (expand) { expand.on('click', function(e) { e.preventDefault(); var link = e.currentTarget || e.target; var linkclasses = link.getAttribute('class').split(' '); var thissuffix = ''; for (var i = 0; i < linkclasses.length; i++) { classname = linkclasses[i]; if (classname.indexOf('expand_') == 0) { thissuffix = classname.substr(7); } } var summaryclassname = 'summary_' + thissuffix; var summary = Y.one('.' + summaryclassname); if (summary) { summary.hide(false); } var fullclassname = 'full_' + thissuffix; full = Y.one('.' + fullclassname); if (full) { full.show(false); full.one('a.contract_' + thissuffix).focus(); } }); } }; submissionconfirmform.php000064400000004570152265517350011733 0ustar00. /** * This file contains the submission confirmation form * * @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('Direct access to this script is forbidden.'); require_once($CFG->libdir.'/formslib.php'); require_once($CFG->dirroot . '/mod/assign/locallib.php'); /** * Assignment submission confirmation form * * @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 mod_assign_confirm_submission_form extends moodleform { /** * Define the form - called by parent constructor */ public function definition() { $mform = $this->_form; list($requiresubmissionstatement, $submissionstatement, $coursemoduleid, $data) = $this->_customdata; if ($requiresubmissionstatement) { $mform->addElement('checkbox', 'submissionstatement', '', $submissionstatement); $mform->addRule('submissionstatement', get_string('submissionstatementrequired', 'mod_assign'), 'required', null, 'client'); } $mform->addElement('static', 'confirmmessage', '', get_string('confirmsubmission', 'mod_assign')); $mform->addElement('hidden', 'id', $coursemoduleid); $mform->setType('id', PARAM_INT); $mform->addElement('hidden', 'action', 'confirmsubmit'); $mform->setType('action', PARAM_ALPHA); $this->add_action_buttons(true, get_string('continue')); if ($data) { $this->set_data($data); } } } overrides.php000064400000032226152265517350007277 0ustar00. /** * This page handles listing of assign overrides * * @package mod_assign * @copyright 2016 Ilya Tregubov * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ require_once(dirname(__FILE__) . '/../../config.php'); require_once($CFG->dirroot.'/mod/assign/lib.php'); require_once($CFG->dirroot.'/mod/assign/locallib.php'); require_once($CFG->dirroot.'/mod/assign/override_form.php'); $cmid = required_param('cmid', PARAM_INT); $mode = optional_param('mode', '', PARAM_ALPHA); // One of 'user' or 'group', default is 'group'. $action = optional_param('action', '', PARAM_ALPHA); $redirect = $CFG->wwwroot.'/mod/assign/overrides.php?cmid=' . $cmid . '&mode=group'; list($course, $cm) = get_course_and_cm_from_cmid($cmid, 'assign'); $assign = $DB->get_record('assign', array('id' => $cm->instance), '*', MUST_EXIST); require_login($course, false, $cm); $context = context_module::instance($cm->id); // Check the user has the required capabilities to list overrides. require_capability('mod/assign:manageoverrides', $context); $assigngroupmode = groups_get_activity_groupmode($cm); $accessallgroups = ($assigngroupmode == NOGROUPS) || has_capability('moodle/site:accessallgroups', $context); $overridecountgroup = $DB->count_records('assign_overrides', array('userid' => null, 'assignid' => $assign->id)); // Get the course groups that the current user can access. $groups = $accessallgroups ? groups_get_all_groups($cm->course) : groups_get_activity_allowed_groups($cm); // Default mode is "group", unless there are no groups. if ($mode != "user" and $mode != "group") { if (!empty($groups)) { $mode = "group"; } else { $mode = "user"; } } $groupmode = ($mode == "group"); $url = new moodle_url('/mod/assign/overrides.php', array('cmid' => $cm->id, 'mode' => $mode)); $PAGE->set_url($url); navigation_node::override_active_url(new moodle_url('/mod/assign/overrides.php', ['cmid' => $cmid])); if ($action == 'movegroupoverride') { $id = required_param('id', PARAM_INT); $dir = required_param('dir', PARAM_ALPHA); if (confirm_sesskey()) { move_group_override($id, $dir, $assign->id); } redirect($redirect); } // Display a list of overrides. $PAGE->set_pagelayout('admin'); $PAGE->add_body_class('limitedwidth'); $PAGE->set_title(get_string('overrides', 'assign')); $PAGE->set_heading($course->fullname); $activityheader = $PAGE->activityheader; $activityheader->set_attrs([ 'description' => '', 'hidecompletion' => true, 'title' => $activityheader->is_title_allowed() ? format_string($assign->name, true, ['context' => $context]) : "" ]); echo $OUTPUT->header(); echo $OUTPUT->heading(get_string('overrides', 'mod_assign'), 2); $overridemenu = new \mod_assign\output\override_actionmenu($url, $cm); $renderer = $PAGE->get_renderer('mod_assign'); echo $renderer->render($overridemenu); // Delete orphaned group overrides. $sql = 'SELECT o.id FROM {assign_overrides} o LEFT JOIN {groups} g ON o.groupid = g.id WHERE o.groupid IS NOT NULL AND g.id IS NULL AND o.assignid = ?'; $params = array($assign->id); $orphaned = $DB->get_records_sql($sql, $params); if (!empty($orphaned)) { $DB->delete_records_list('assign_overrides', 'id', array_keys($orphaned)); } $overrides = []; // Fetch all overrides. if ($groupmode) { $colname = get_string('group'); // To filter the result by the list of groups that the current user has access to. if ($groups) { $params = ['assignid' => $assign->id]; list($insql, $inparams) = $DB->get_in_or_equal(array_keys($groups), SQL_PARAMS_NAMED); $params += $inparams; $sql = "SELECT o.*, g.name FROM {assign_overrides} o JOIN {groups} g ON o.groupid = g.id WHERE o.assignid = :assignid AND g.id $insql ORDER BY o.sortorder"; $overrides = $DB->get_records_sql($sql, $params); } } else { $colname = get_string('user'); list($sort, $params) = users_order_by_sql('u'); $params['assignid'] = $assign->id; $userfieldsapi = \core_user\fields::for_name(); if ($accessallgroups) { $sql = 'SELECT o.*, ' . $userfieldsapi->get_sql('u', false, '', '', false)->selects . ' FROM {assign_overrides} o JOIN {user} u ON o.userid = u.id WHERE o.assignid = :assignid ORDER BY ' . $sort; $overrides = $DB->get_records_sql($sql, $params); } else if ($groups) { list($insql, $inparams) = $DB->get_in_or_equal(array_keys($groups), SQL_PARAMS_NAMED); $params += $inparams; $sql = 'SELECT o.*, ' . $userfieldsapi->get_sql('u', false, '', '', false)->selects . ' FROM {assign_overrides} o JOIN {user} u ON o.userid = u.id JOIN {groups_members} gm ON u.id = gm.userid WHERE o.assignid = :assignid AND gm.groupid ' . $insql . ' ORDER BY ' . $sort; $overrides = $DB->get_records_sql($sql, $params); } } // Initialise table. $table = new html_table(); $table->headspan = array(1, 2, 1); $table->colclasses = array('colname', 'colsetting', 'colvalue', 'colaction'); $table->head = array( $colname, get_string('overrides', 'assign'), get_string('action'), ); $userurl = new moodle_url('/user/view.php', array()); $groupurl = new moodle_url('/group/overview.php', array('id' => $cm->course)); $overridedeleteurl = new moodle_url('/mod/assign/overridedelete.php'); $overrideediturl = new moodle_url('/mod/assign/overrideedit.php'); $hasinactive = false; // Whether there are any inactive overrides. $PAGE->requires->js_call_amd('mod_assign/override_modal_manager', 'init', [$mode, !empty($assign->gradepenalty)]); foreach ($overrides as $override) { $fields = array(); $values = array(); $active = true; // Check for inactive overrides. if (!$groupmode) { if (!is_enrolled($context, $override->userid)) { // User not enrolled. $active = false; } else if (!\core_availability\info_module::is_user_visible($cm, $override->userid)) { // User cannot access the module. $active = false; } } // Format allowsubmissionsfromdate. if (isset($override->allowsubmissionsfromdate)) { $fields[] = get_string('open', 'assign'); $values[] = $override->allowsubmissionsfromdate > 0 ? userdate($override->allowsubmissionsfromdate) : get_string('noopen', 'assign'); } // Format duedate. if (isset($override->duedate)) { $fields[] = get_string('duedate', 'assign'); $values[] = $override->duedate > 0 ? userdate($override->duedate) : get_string('noclose', 'assign'); } // Format cutoffdate. if (isset($override->cutoffdate)) { $fields[] = get_string('cutoffdate', 'assign'); $values[] = $override->cutoffdate > 0 ? userdate($override->cutoffdate) : get_string('noclose', 'assign'); } // Format timelimit. $timelimitenabled = get_config('assign', 'enabletimelimit'); if ($timelimitenabled && isset($override->timelimit)) { $fields[] = get_string('timelimit', 'assign'); $values[] = $override->timelimit > 0 ? format_time($override->timelimit) : get_string('none', 'assign'); } // Icons. $iconstr = ''; // Edit. $editurlstr = $overrideediturl->out(true, array('id' => $override->id)); $iconstr = '' . $OUTPUT->pix_icon('t/edit', get_string('edit')) . ' '; // Duplicate. $copyurlstr = $overrideediturl->out(true, array('id' => $override->id, 'action' => 'duplicate')); $iconstr .= '' . $OUTPUT->pix_icon('t/copy', get_string('copy')) . ' '; // Delete. $deletelink = html_writer::link( "#", $OUTPUT->pix_icon('t/delete', get_string('delete')), [ 'class' => 'delete-override', 'data-overrideid' => $override->id, 'data-sesskey' => sesskey(), ], ); $iconstr .= $deletelink; if ($groupmode) { $usergroupstr = html_writer::link( $groupurl->out(true, ['group' => $override->groupid]), format_string($override->name, true, ['context' => $context]), ['class' => 'usergroupname'], ); // Move up. if ($override->sortorder > 1) { $iconstr .= '' . $OUTPUT->pix_icon('t/up', get_string('moveup')) . ' '; } else { $iconstr .= $OUTPUT->spacer() . ' '; } // Move down. if ($override->sortorder < $overridecountgroup) { $iconstr .= '' . $OUTPUT->pix_icon('t/down', get_string('movedown')) . ' '; } else { $iconstr .= $OUTPUT->spacer() . ' '; } } else { $usergroupstr = html_writer::link( $userurl->out(false, ['id' => $override->userid, 'course' => $course->id]), fullname($override), ['class' => 'usergroupname'], ); } $class = ''; if (!$active) { $class = "dimmed_text"; $usergroupstr .= '*'; $hasinactive = true; } $usergroupcell = new html_table_cell(); $usergroupcell->rowspan = count($fields); $usergroupcell->text = $usergroupstr; $actioncell = new html_table_cell(); $actioncell->rowspan = count($fields); $actioncell->text = $iconstr; for ($i = 0; $i < count($fields); ++$i) { $row = new html_table_row(); $row->attributes['class'] = $class; if ($i == 0) { $row->cells[] = $usergroupcell; } $cell1 = new html_table_cell(); $cell1->text = $fields[$i]; $row->cells[] = $cell1; $cell2 = new html_table_cell(); $cell2->text = $values[$i]; $row->cells[] = $cell2; if ($i == 0) { $row->cells[] = $actioncell; } $table->data[] = $row; } } // Output the table and button. echo html_writer::start_tag('div', array('id' => 'assignoverrides')); if (count($table->data)) { echo html_writer::table($table); } else { if ($groupmode) { echo $OUTPUT->notification(get_string('nogroupoverrides', 'mod_assign'), 'info'); } else { echo $OUTPUT->notification(get_string('nouseroverrides', 'mod_assign'), 'info'); } } if ($hasinactive) { echo $OUTPUT->notification(get_string('inactiveoverridehelp', 'assign'), 'dimmed_text'); } echo html_writer::start_tag('div', array('class' => 'buttons')); $options = array(); if ($groupmode) { if (empty($groups)) { // There are no groups. echo $OUTPUT->notification(get_string('groupsnone', 'assign'), 'error'); $options['disabled'] = true; } } else { $users = array(); // See if there are any users in the assign. if ($accessallgroups) { $users = get_enrolled_users($context, '', 0, 'u.id'); $nousermessage = get_string('usersnone', 'assign'); } else if ($groups) { $enrolledjoin = get_enrolled_join($context, 'u.id'); list($ingroupsql, $ingroupparams) = $DB->get_in_or_equal(array_keys($groups), SQL_PARAMS_NAMED); $params = $enrolledjoin->params + $ingroupparams; $sql = "SELECT u.id FROM {user} u JOIN {groups_members} gm ON gm.userid = u.id {$enrolledjoin->joins} WHERE gm.groupid $ingroupsql AND {$enrolledjoin->wheres} ORDER BY $sort"; $users = $DB->get_records_sql($sql, $params); $nousermessage = get_string('usersnone', 'assign'); } else { $nousermessage = get_string('groupsnone', 'assign'); } $info = new \core_availability\info_module($cm); $users = $info->filter_user_list($users); if (empty($users)) { // There are no users. echo $OUTPUT->notification($nousermessage, 'error'); $options['disabled'] = true; } } echo html_writer::end_tag('div'); echo html_writer::end_tag('div'); // Finish the page. echo $OUTPUT->footer(); yui/src/history/build.json000064400000000270152265517350011646 0ustar00{ "name": "moodle-mod_assign-history", "builds": { "moodle-mod_assign-history": { "jsfiles": [ "history.js" ] } } } yui/src/history/meta/history.json000064400000000172152265517350013177 0ustar00{ "moodle-mod_assign-history": { "requires": [ "node", "transition" ] } } yui/src/history/js/history.js000064400000004732152265517350012336 0ustar00// Define a function that will run in the context of a // Node instance: var CSS = { LINK: 'mod-assign-history-link', OPEN: 'mod-assign-history-link-open', CLOSED: 'mod-assign-history-link-closed', PANEL: 'mod-assign-history-panel' }, COUNT = 0, TOGGLE = function() { var id = this.get('for'), panel = Y.one('#' + id); if (this.hasClass(CSS.OPEN)) { this.removeClass(CSS.OPEN); this.addClass(CSS.CLOSED); this.setStyle('overflow', 'hidden'); panel.hide(); } else { this.removeClass(CSS.CLOSED); this.addClass(CSS.OPEN); panel.show(); } }, HISTORY = function() { var link = null, panel = null, wrapper = null, container = this; // Loop through all the children of this container and turn // every odd node to a link to open/close the following panel. this.get('children').each(function() { if (link) { COUNT++; // First convert the link to an anchor. wrapper = Y.Node.create(''); panel = this; container.insertBefore(wrapper, link); link.remove(false); wrapper.appendChild(link); // Add a for attribute to the link to link to the id of the panel. if (!panel.get('id')) { panel.set('id', CSS.PANEL + COUNT); } wrapper.set('for', panel.get('id')); // Add an aria attribute for the live region. panel.set('aria-live', 'polite'); wrapper.addClass(CSS.LINK); if (COUNT == 1) { wrapper.addClass(CSS.OPEN); } else { wrapper.addClass(CSS.CLOSED); } panel.addClass(CSS.PANEL); if (COUNT == 1) { panel.show(); } else { panel.hide(); } link = null; } else { link = this; } }); // Setup event listeners. this.delegate('click', TOGGLE, '.' + CSS.LINK); }; // Use addMethod to add history to the Node prototype: Y.Node.addMethod("history", HISTORY); // Extend this functionality to NodeLists. Y.NodeList.importMethod(Y.Node.prototype, "history"); yui/build/moodle-mod_assign-history/moodle-mod_assign-history-debug.js000064400000005116152265517350022307 0ustar00YUI.add('moodle-mod_assign-history', function (Y, NAME) { // Define a function that will run in the context of a // Node instance: var CSS = { LINK: 'mod-assign-history-link', OPEN: 'mod-assign-history-link-open', CLOSED: 'mod-assign-history-link-closed', PANEL: 'mod-assign-history-panel' }, COUNT = 0, TOGGLE = function() { var id = this.get('for'), panel = Y.one('#' + id); if (this.hasClass(CSS.OPEN)) { this.removeClass(CSS.OPEN); this.addClass(CSS.CLOSED); this.setStyle('overflow', 'hidden'); panel.hide(); } else { this.removeClass(CSS.CLOSED); this.addClass(CSS.OPEN); panel.show(); } }, HISTORY = function() { var link = null, panel = null, wrapper = null, container = this; // Loop through all the children of this container and turn // every odd node to a link to open/close the following panel. this.get('children').each(function() { if (link) { COUNT++; // First convert the link to an anchor. wrapper = Y.Node.create(''); panel = this; container.insertBefore(wrapper, link); link.remove(false); wrapper.appendChild(link); // Add a for attribute to the link to link to the id of the panel. if (!panel.get('id')) { panel.set('id', CSS.PANEL + COUNT); } wrapper.set('for', panel.get('id')); // Add an aria attribute for the live region. panel.set('aria-live', 'polite'); wrapper.addClass(CSS.LINK); if (COUNT == 1) { wrapper.addClass(CSS.OPEN); } else { wrapper.addClass(CSS.CLOSED); } panel.addClass(CSS.PANEL); if (COUNT == 1) { panel.show(); } else { panel.hide(); } link = null; } else { link = this; } }); // Setup event listeners. this.delegate('click', TOGGLE, '.' + CSS.LINK); }; // Use addMethod to add history to the Node prototype: Y.Node.addMethod("history", HISTORY); // Extend this functionality to NodeLists. Y.NodeList.importMethod(Y.Node.prototype, "history"); }, '@VERSION@', {"requires": ["node", "transition"]}); yui/build/moodle-mod_assign-history/moodle-mod_assign-history-min.js000064400000001707152265517350022006 0ustar00YUI.add("moodle-mod_assign-history",function(o,s){var d={LINK:"mod-assign-history-link",OPEN:"mod-assign-history-link-open",CLOSED:"mod-assign-history-link-closed",PANEL:"mod-assign-history-panel"},a=0,h=function(){var s=this.get("for"),s=o.one("#"+s);this.hasClass(d.OPEN)?(this.removeClass(d.OPEN),this.addClass(d.CLOSED),this.setStyle("overflow","hidden"),s.hide()):(this.removeClass(d.CLOSED),this.addClass(d.OPEN),s.show())};o.Node.addMethod("history",function(){var s,i=null,e=null,t=this;this.get("children").each(function(){i=i?(a++,e=o.Node.create(""),s=this,t.insertBefore(e,i),i.remove(!1),e.appendChild(i),s.get("id")||s.set("id",d.PANEL+a),e.set("for",s.get("id")),s.set("aria-live","polite"),e.addClass(d.LINK),1==a?e.addClass(d.OPEN):e.addClass(d.CLOSED),s.addClass(d.PANEL),1==a?s.show():s.hide(),null):this}),this.delegate("click",h,"."+d.LINK)}),o.NodeList.importMethod(o.Node.prototype,"history")},"@VERSION@",{requires:["node","transition"]});yui/build/moodle-mod_assign-history/moodle-mod_assign-history.js000064400000005116152265517350021223 0ustar00YUI.add('moodle-mod_assign-history', function (Y, NAME) { // Define a function that will run in the context of a // Node instance: var CSS = { LINK: 'mod-assign-history-link', OPEN: 'mod-assign-history-link-open', CLOSED: 'mod-assign-history-link-closed', PANEL: 'mod-assign-history-panel' }, COUNT = 0, TOGGLE = function() { var id = this.get('for'), panel = Y.one('#' + id); if (this.hasClass(CSS.OPEN)) { this.removeClass(CSS.OPEN); this.addClass(CSS.CLOSED); this.setStyle('overflow', 'hidden'); panel.hide(); } else { this.removeClass(CSS.CLOSED); this.addClass(CSS.OPEN); panel.show(); } }, HISTORY = function() { var link = null, panel = null, wrapper = null, container = this; // Loop through all the children of this container and turn // every odd node to a link to open/close the following panel. this.get('children').each(function() { if (link) { COUNT++; // First convert the link to an anchor. wrapper = Y.Node.create(''); panel = this; container.insertBefore(wrapper, link); link.remove(false); wrapper.appendChild(link); // Add a for attribute to the link to link to the id of the panel. if (!panel.get('id')) { panel.set('id', CSS.PANEL + COUNT); } wrapper.set('for', panel.get('id')); // Add an aria attribute for the live region. panel.set('aria-live', 'polite'); wrapper.addClass(CSS.LINK); if (COUNT == 1) { wrapper.addClass(CSS.OPEN); } else { wrapper.addClass(CSS.CLOSED); } panel.addClass(CSS.PANEL); if (COUNT == 1) { panel.show(); } else { panel.hide(); } link = null; } else { link = this; } }); // Setup event listeners. this.delegate('click', TOGGLE, '.' + CSS.LINK); }; // Use addMethod to add history to the Node prototype: Y.Node.addMethod("history", HISTORY); // Extend this functionality to NodeLists. Y.NodeList.importMethod(Y.Node.prototype, "history"); }, '@VERSION@', {"requires": ["node", "transition"]}); batchsetmarkingworkflowstateform.php000064400000007046152265517350014165 0ustar00. /** * This file contains the forms to set the marking workflow for selected submissions. * * @package mod_assign * @copyright 2013 Catalyst IT {@link http://www.catalyst.net.nz} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ /** * Set marking workflow form. * * @package mod_assign * @copyright 2013 Catalyst IT {@link http://www.catalyst.net.nz} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class mod_assign_batch_set_marking_workflow_state_form extends moodleform { /** * Define this form - called by the parent constructor */ public function definition() { $mform = $this->_form; $params = $this->_customdata; $formheader = get_string('batchsetmarkingworkflowstateforusers', 'assign', $params['userscount']); $mform->addElement('header', 'general', $formheader); $mform->addElement('static', 'userslist', get_string('selectedusers', 'assign'), $params['usershtml']); $options = $params['markingworkflowstates']; $mform->addElement('select', 'markingworkflowstate', get_string('markingworkflowstate', 'assign'), $options); // Don't allow notification to be sent until in "Released" state. $mform->addElement('selectyesno', 'sendstudentnotifications', get_string('sendstudentnotifications', 'assign')); $mform->setDefault('sendstudentnotifications', 0); $mform->disabledIf('sendstudentnotifications', 'markingworkflowstate', 'neq', ASSIGN_MARKING_WORKFLOW_STATE_RELEASED); $mform->addElement('hidden', 'id'); $mform->setType('id', PARAM_INT); $mform->addElement('hidden', 'action', 'setbatchmarkingworkflowstate'); $mform->setType('action', PARAM_ALPHA); $mform->addElement('hidden', 'selectedusers'); $mform->setType('selectedusers', PARAM_SEQUENCE); $this->add_action_buttons(true, get_string('savechanges')); } /** * Validate the submitted form data. * * @param array $data array of ("fieldname"=>value) of submitted data * @param array $files array of uploaded files "element_name"=>tmp_file_path * @return array of "element_name"=>"error_description" if there are errors */ public function validation($data, $files) { $errors = parent::validation($data, $files); // As the implementation of this feature exists currently, no user will see a validation // failure from this form, but this check ensures the form won't validate if someone // manipulates the 'sendstudentnotifications' field's disabled attribute client-side. if (!empty($data['sendstudentnotifications']) && $data['markingworkflowstate'] != ASSIGN_MARKING_WORKFLOW_STATE_RELEASED) { $errors['sendstudentnotifications'] = get_string('studentnotificationworkflowstateerror', 'assign'); } return $errors; } } gradingbatchoperationsform.php000064400000011721152265517350012677 0ustar00. /** * This file contains the forms to create and edit an instance of this 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('Direct access to this script is forbidden.'); require_once($CFG->libdir.'/formslib.php'); require_once($CFG->dirroot . '/mod/assign/locallib.php'); /** * Assignment grading options form * * @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 * @deprecated since 4.5 * @todo Final deprecation in Moodle 6.0. See MDL-82869. */ #[\core\attribute\deprecated( replacement: null, since: '4.5', reason: 'It is no longer used.', mdl: 'MDL-80750', )] class mod_assign_grading_batch_operations_form extends moodleform { /** * Define this form - called by the parent constructor. * * @deprecated since 4.5 */ #[\core\attribute\deprecated( replacement: null, since: '4.5', reason: 'It is no longer used.', mdl: 'MDL-80750' )] public function definition() { \core\deprecation::emit_deprecation([$this, __FUNCTION__]); global $CFG; $mform = $this->_form; $instance = $this->_customdata; // Visible elements. $options = array(); $options['lock'] = get_string('locksubmissions', 'assign'); $options['unlock'] = get_string('unlocksubmissions', 'assign'); if (!empty($CFG->messaging) && has_all_capabilities(['moodle/site:sendmessage', 'moodle/course:bulkmessaging'], $instance['context']) ) { $options['message'] = get_string('messageselectadd'); } $options['downloadselected'] = get_string('downloadselectedsubmissions', 'assign'); if ($instance['submissiondrafts']) { $options['reverttodraft'] = get_string('reverttodraft', 'assign'); } if (has_capability('mod/assign:editothersubmission', $instance['context'])) { $options['removesubmission'] = get_string('removesubmission', 'assign'); } if ($instance['duedate'] && has_capability('mod/assign:grantextension', $instance['context'])) { $options['grantextension'] = get_string('grantextension', 'assign'); } $multipleattemptsallowed = $instance['maxattempts'] > 1 || $instance['maxattempts'] == ASSIGN_UNLIMITED_ATTEMPTS; if ($multipleattemptsallowed && $instance['attemptreopenmethod'] == ASSIGN_ATTEMPT_REOPEN_METHOD_MANUAL) { $options['addattempt'] = get_string('addattempt', 'assign'); } foreach ($instance['feedbackplugins'] as $plugin) { if ($plugin->is_visible() && $plugin->is_enabled()) { foreach ($plugin->get_grading_batch_operations() as $action => $description) { $operationkey = 'plugingradingbatchoperation_' . $plugin->get_type() . '_' . $action; $options[$operationkey] = $description; } } } if ($instance['markingworkflow']) { $options['setmarkingworkflowstate'] = get_string('setmarkingworkflowstate', 'assign'); } if ($instance['markingallocation']) { $options['setmarkingallocation'] = get_string('setmarkingallocation', 'assign'); } $mform->addElement('hidden', 'action', 'gradingbatchoperation'); $mform->setType('action', PARAM_ALPHA); $mform->addElement('hidden', 'id', $instance['cm']); $mform->setType('id', PARAM_INT); $mform->addElement('hidden', 'selectedusers', '', array('class'=>'selectedusers')); $mform->setType('selectedusers', PARAM_SEQUENCE); $mform->addElement('hidden', 'returnaction', 'grading'); $mform->setType('returnaction', PARAM_ALPHA); $objs = array(); $objs[] =& $mform->createElement('select', 'operation', get_string('chooseoperation', 'assign'), $options); $objs[] =& $mform->createElement('submit', 'submit', get_string('go')); $batchdescription = get_string('batchoperationsdescription', 'assign'); $mform->addElement('group', 'actionsgrp', $batchdescription, $objs, ' ', false); } } feedback/editpdf/thirdpartylibs.xml000064400000001012152265517350013502 0ustar00 fpdi FPDI Collection of PHP classes facilitating developers to read pages from existing PDF documents and use them as templates in FPDF. 2.6.3 MIT https://github.com/Setasign/FPDI 2020 Setasign GmbH & Co. KG, https://www.setasign.com feedback/editpdf/fixtures/blank.pdf000064400000002132152265517350013353 0ustar00%PDF-1.4 %äüöß 2 0 obj <> stream x33T(*T0P0L-M,L ,Rµr endstream endobj 3 0 obj 42 endobj 5 0 obj << >> endobj 6 0 obj <> endobj 1 0 obj <>/Contents 2 0 R>> endobj 4 0 obj <> endobj 7 0 obj <> endobj 8 0 obj < /Producer /CreationDate(D:20170213125530+05'30')>> endobj xref 0 9 0000000000 65535 f 0000000226 00000 n 0000000019 00000 n 0000000132 00000 n 0000000368 00000 n 0000000151 00000 n 0000000173 00000 n 0000000466 00000 n 0000000562 00000 n trailer < ] /DocChecksum /808042869C3B747976AF1CE26AA7C9FD >> startxref 736 %%EOF feedback/editpdf/styles.css000064400000022776152265517350011775 0ustar00.assignfeedback_editpdf_widget .toolbar ul { display: none; } .assignfeedback_editpdf_widget .toolbar li { list-style-type: none; } .assignfeedback_editpdf_widget .drawingcanvas { position: relative; min-width: 817px; min-height: 400px; background-repeat: no-repeat; background-color: #ccc; margin-left: auto; margin-right: auto; box-shadow: inset 0 1px 0 rgba(255, 255, 255, .2), 0 1px 20px rgba(0, 0, 0, .2); } .assignfeedback_editpdf_widget .moodle-dialogue-bd .drawingregion { position: inherit; } .assignfeedback_editpdf_widget .drawingregion[data-currenttool=select] .commentdrawable textarea, .assignfeedback_editpdf_widget .drawingregion[data-currenttool=select] .commentdrawable svg { cursor: move; } .assignfeedback_editpdf_widget .infoicon { display: none; } .assignfeedback_editpdf_widget .warningmessages { position: absolute; margin-left: 20px; margin-right: 20px; bottom: 20px; } .assignfeedback_editpdf_widget .drawingregion { border: 1px solid #ccc; left: 1em; right: 1em; top: 52px; bottom: 0; position: absolute; overflow: auto; background-color: #ccc; touch-action: none; } .assignfeedback_editpdf_widget { user-select: none; -moz-user-select: none; -webkit-user-select: none; } .assignfeedback_editpdf_widget .pageheader { background-color: #ebebeb; border-bottom: 1px solid #ccc; padding: 0; padding-left: 20px; padding-right: 20px; min-height: 50px; height: 52px; overflow: auto; } .moodle-dialogue-base .moodle-dialogue.assignfeedback_editpdf_widget .moodle-dialogue-bd { padding: 0; } .yui3-colourpicker-hidden, .yui3-commentsearch-hidden, .yui3-commentmenu-hidden { display: none; } .assignfeedback_editpdf_widget .pageheader button img { padding-top: 3px; vertical-align: top; } .assignfeedback_editpdf_widget .pageheader button:active { background-color: #ccc; } .assignfeedback_editpdf_widget .pageheader select, .assignfeedback_editpdf_widget .pageheader button { background: none; padding: 4px 7px; border: 0; border-radius: 0; margin: 0; height: 30px; line-height: 30px; vertical-align: top; cursor: pointer; } .assignfeedback_editpdf_widget .pageheader select { vertical-align: top; -webkit-appearance: none; -moz-appearance: menulist-text; background-color: #fff; padding: 0 10px; } .assignfeedback_editpdf_widget .pageheader select::-ms-expand { display: none; } .assignfeedback_editpdf_widget .pageheader .navigation button + button, .assignfeedback_editpdf_widget .pageheader .toolbar button + button, .assignfeedback_editpdf_widget .pageheader .navigation select + button, .assignfeedback_editpdf_widget .pageheader .toolbar select + button { border-left: 1px solid #ccc; border-right: 0; } .assignfeedback_editpdf_widget .pageheader .navigation button { border-right: 1px solid #ccc; } .assignfeedback_editpdf_widget .pageheader .toolbar, .assignfeedback_editpdf_widget .pageheader .navigation-search, .assignfeedback_editpdf_widget .pageheader .navigation-expcol, .assignfeedback_editpdf_widget .pageheader .navigation { border: 1px solid #ccc; border-bottom-color: #b3b3b3; border-radius: 4px; margin: 10px 4px; background-color: white; height: 30px; line-height: 30px; padding: 0; } .assignfeedback_editpdf_commentsearch ul { max-height: 400px; overflow-y: auto; padding: 1em; } .assignfeedback_editpdf_commentsearch ul li pre { background-color: #efefef; white-space: pre-wrap; word-break: break-word; } .assignfeedback_editpdf_commentsearch ul li pre:hover { background-color: #ddd; } .assignfeedback_editpdf_commentsearch ul li { margin: 2px; } .assignfeedback_editpdf_commentsearch a pre { font-family: helvetica; margin: 0; padding: 4px; } .assignfeedback_editpdf_widget .navigation-search, .assignfeedback_editpdf_widget .navigation-expcol, .assignfeedback_editpdf_widget .navigation { float: left; } .assignfeedback_editpdf_widget .toolbar_group { float: right; } .assignfeedback_editpdf_widget .toolbar button { box-shadow: none; -moz-box-shadow: none; -webkit-box-shadow: none; } .assignfeedback_editpdf_widget .toolbar > button.rotateleftbutton { /*rtl:raw: border-right: 1px solid #ccc; border-left: 0; */ /*rtl:ignore*/ float: left; } .assignfeedback_editpdf_widget .toolbar > button.rotaterightbutton { /*rtl:ignore*/ float: right; } .assignfeedback_editpdf_widget .toolbar { float: left; } .assignfeedback_editpdf_widget .navigation, .assignfeedback_editpdf_widget .navigation-search, .assignfeedback_editpdf_widget .navigation-expcol, .assignfeedback_editpdf_widget .toolbar { display: inline-block; } .assignfeedback_editpdf_colourpicker ul { margin: 0; } .assignfeedback_editpdf_dropdown li.quicklist_comment { width: 200px; } .assignfeedback_editpdf_dropdown li.quicklist_comment a { white-space: nowrap; display: inline-block; max-width: 140px; overflow: hidden; text-overflow: ellipsis; } .assignfeedback_editpdf_dropdown a.delete_quicklist_comment { float: right; width: 20px; } .assignfeedback_editpdf_dropdown button { border: 0; background: none; padding: 6px 7px; border-radius: 0; border-top: 1px solid #ccc; } .assignfeedback_editpdf_dropdown li:first-child button { border-top: 0; } .moodle-dialogue-base .moodle-dialogue.assignfeedback_editpdf_dropdown .moodle-dialogue-wrap { box-shadow: none; -moz-box-shadow: none; -webkit-box-shadow: none; margin-left: 0; margin-right: 0; margin-top: 0; border-radius: 4px; } .moodle-dialogue-base .moodle-dialogue.assignfeedback_editpdf_dropdown .moodle-dialogue-bd { padding: 0; } .moodle-dialogue-base .assignfeedback_editpdf_dropdown .moodle-dialogue-wrap .moodle-dialogue-hd, .moodle-dialogue-base .assignfeedback_editpdf_dropdown .moodle-dialogue-wrap .moodle-dialogue-ft { display: none; } .assignfeedback_editpdf_menu li hr { margin: 0; } .assignfeedback_editpdf_menu li a { text-decoration: none; color: #555; margin-left: 10px; margin-right: 10px; margin-top: 4px; margin-bottom: 4px; } ul.assignfeedback_editpdf_search { margin: 0; } .assignfeedback_editpdf_search li { list-style-type: none; } .assignfeedback_editpdf_search li a { text-decoration: none; color: #555; } .assignfeedback_editpdf_menu li:hover { background-color: #ebebeb; } ul.assignfeedback_editpdf_menu { margin: 0; max-height: 40vh; overflow-y: auto; overflow-x: hidden; } .assignfeedback_editpdf_menu li { list-style-type: none; margin: 0; border-radius: 4px; } .assignfeedback_editpdf_menu li button { margin: 0; background: none; } .assignfeedback_editpdf_widget .pageheader button:hover { background-color: #ebebeb; background-image: radial-gradient(ellipse at center, #fff 60%, #dfdfdf 100%); } .assignfeedback_editpdf_widget .pageheader button.assignfeedback_editpdf_selectedbutton:hover, .assignfeedback_editpdf_widget .pageheader button.assignfeedback_editpdf_selectedbutton { background-color: #dfdfdf; background-image: radial-gradient(ellipse at center, #fff 40%, #dfdfdf 100%); } .assignfeedback_editpdf_widget .commentdrawable img { margin: 0 5px; } .assignfeedback_editpdf_widget .commentdrawable a { float: right; position: relative; left: -17px; top: 2px; height: 16px; width: 16px; } .assignfeedback_editpdf_widget .commentdrawable textarea { padding: 4px; padding-right: 20px; resize: none; overflow: hidden; color: black; border: 2px solid #ccc; border-radius: 4px; font-size: 16px; font-family: helvetica; min-height: 1.2em; } .assignfeedback_editpdf_widget .commentdrawable textarea:focus { outline: thin dotted #333; outline-color: -webkit-focus-ring-color; outline-width: 5px; outline-style: auto; outline-offset: -2px; } .assignfeedback_editpdf_widget .commentdrawable { display: flex; z-index: 52; height: 0; } .assignfeedback_editpdf_widget .annotation { z-index: 51; } .assignfeedback_editpdf_widget .deleteannotationbutton { z-index: 51; } .assignfeedback_editpdf_widget .commentdrawable label { display: inline-block; } .assignfeedback_editpdf_widget .commentdrawable svg { display: none; } .assignfeedback_editpdf_widget .commentdrawable.commentcollapsed { z-index: auto; width: 24px; } .assignfeedback_editpdf_widget .commentdrawable.commentcollapsed textarea, .assignfeedback_editpdf_widget .commentdrawable.commentcollapsed a { display: none; } .assignfeedback_editpdf_widget .commentdrawable.commentcollapsed svg { display: inline-block; width: 24px; height: 24px; } .assignfeedback_editpdf_widget .drawingcanvas .loading .progressbarlabel { text-align: center; } .hideoverflow { overflow: hidden; position: relative; } @media (max-width: 960px) { .assignfeedback_editpdf_widget .pageheader { height: 104px; } .assignfeedback_editpdf_widget .drawingregion { top: 104px; } } @media (max-width: 767px) { .assignfeedback_editpdf_widget .drawingregion { position: relative; margin-bottom: 1em; top: 0; max-height: 312px; } .assignfeedback_editpdf_widget .pageheader { height: auto; } } @media (max-width: 480px) { .assignfeedback_editpdf_widget .pageheader { padding-left: 5px; padding-right: 5px; } } feedback/editpdf/db/install.php000064400000003342152265517350012470 0ustar00. /** * Install code for the feedback_editpdf module. * * @package assignfeedback_editpdf * @copyright 2013 Jerome Mouneyrac * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); /** * EditPDF install code */ function xmldb_assignfeedback_editpdf_install() { global $CFG; // List of default stamps. $defaultstamps = array('smile.png', 'sad.png', 'tick.png', 'cross.png'); // Stamp file object. $filerecord = new stdClass; $filerecord->component = 'assignfeedback_editpdf'; $filerecord->contextid = context_system::instance()->id; $filerecord->userid = get_admin()->id; $filerecord->filearea = 'stamps'; $filerecord->filepath = '/'; $filerecord->itemid = 0; $fs = get_file_storage(); // Load all default stamps. foreach ($defaultstamps as $stamp) { $filerecord->filename = $stamp; $fs->create_file_from_pathname($filerecord, $CFG->dirroot . '/mod/assign/feedback/editpdf/pix/' . $filerecord->filename); } } feedback/editpdf/db/upgrade.php000064400000003112152265517350012444 0ustar00. /** * Upgrade code for the feedback_editpdf module. * * @package assignfeedback_editpdf * @copyright 2013 Jerome Mouneyrac * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ /** * EditPDF upgrade code * @param int $oldversion * @return bool */ function xmldb_assignfeedback_editpdf_upgrade($oldversion) { // Automatically generated Moodle v4.2.0 release upgrade line. // Put any upgrade step following this. // Automatically generated Moodle v4.3.0 release upgrade line. // Put any upgrade step following this. // Automatically generated Moodle v4.4.0 release upgrade line. // Put any upgrade step following this. // Automatically generated Moodle v4.5.0 release upgrade line. // Put any upgrade step following this. // Automatically generated Moodle v5.0.0 release upgrade line. // Put any upgrade step following this. return true; } feedback/editpdf/db/install.xml000064400000013622152265517350012503 0ustar00
feedback/editpdf/db/events.php000064400000002377152265517350012335 0ustar00. /** * EditPDF event handler definition. * * @package assignfeedback_editpdf * @category event * @copyright 2016 Damyon Wiese * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ // List of observers. $observers = array( array( 'eventname' => '\mod_assign\event\submission_created', 'callback' => '\assignfeedback_editpdf\event\observer::submission_created', ), array( 'eventname' => '\mod_assign\event\submission_updated', 'callback' => '\assignfeedback_editpdf\event\observer::submission_updated', ), ); feedback/editpdf/lang/en/assignfeedback_editpdf.php000064400000013670152265517350016415 0ustar00. /** * Strings for component 'assignfeedback_editpdf', language 'en' * * @package assignfeedback_editpdf * @copyright 2012 Davo Smith * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); $string['addtoquicklist'] = 'Add to quicklist'; $string['annotationcolour'] = 'Annotation colour'; $string['black'] = 'Black'; $string['blue'] = 'Blue'; $string['cannotopenpdf'] = 'Cannot open the PDF. The file may be corrupt, or in an unsupported format.'; $string['clear'] = 'Clear'; $string['colourpicker'] = 'Colour picker'; $string['commentcolour'] = 'Comment colour'; $string['comment'] = 'Comments'; $string['commentindex'] = 'Index of comments'; $string['commentlabel'] = '{$a->pnum}.{$a->cnum}'; $string['command'] = 'Command:'; $string['commentcontextmenu'] = 'Comment context menu'; $string['couldnotsavepage'] = 'Could not save page {$a}'; $string['currentstamp'] = 'Stamp'; $string['default'] = 'Enabled by default'; $string['default_help'] = 'If set, this feedback method will be enabled by default for all new assignments.'; $string['deleteannotation'] = 'Delete annotation'; $string['deletecomment'] = 'Delete comment'; $string['deletefeedback'] = 'Delete feedback PDF'; $string['downloadablefilename'] = 'feedback.pdf'; $string['downloadfeedback'] = 'Download feedback PDF'; $string['drag'] = 'Drag'; $string['errorgenerateimage'] = 'Error generating image with ghostscript, debugging info: {$a}'; $string['errorpdfpage'] = 'There was an error while generating this page.'; $string['editpdf'] = 'Annotate PDF'; $string['editpdf_help'] = 'Annotate student submissions directly in the browser and produce an edited downloadable PDF.'; $string['enabled'] = 'Annotate PDF'; $string['enabled_help'] = 'If enabled, the teacher will be able to create annotated PDF files when marking assignment submissions. This allows the teacher to add comments, drawing and stamps directly on top of the student\'s work. The annotating is done in the browser and no extra software is required.'; $string['expcolcomments'] = 'Expand/collapse all comments'; $string['filter'] = 'Filter comments...'; $string['generatefeedback'] = 'Generate feedback PDF'; $string['gotopage'] = 'Go to page'; $string['green'] = 'Green'; $string['gsimage'] = 'Ghostscript test image'; $string['pathtogserror'] = 'The configured path to ghostscript is not correctly set: {$a}'; $string['pathtogspathdesc'] = 'Please note that annotate PDF requires the path to ghostscript to be set in {$a}.'; $string['highlight'] = 'Highlight'; $string['jsrequired'] = 'JavaScript is required to annotate a PDF. Please enable JavaScript in your browser to use this feature.'; $string['launcheditor'] = 'Launch PDF editor...'; $string['line'] = 'Line'; $string['loadingeditor'] = 'Loading PDF editor'; $string['navigatenext'] = 'Next page (Alt/Shift-Alt/Ctrl-Option + {$a})'; $string['navigateprevious'] = 'Previous page (Alt/Shift-Alt/Ctrl-Option + {$a})'; $string['oval'] = 'Oval'; $string['output'] = 'Output:'; $string['pagenumber'] = 'Page {$a}'; $string['pagexofy'] = 'Page {$a->page} of {$a->total}'; $string['pen'] = 'Pen'; $string['partialwarning'] = 'Some of the files in this submission can only be accessed by direct download.'; $string['pluginname'] = 'Annotate PDF'; $string['privacy:metadata:colourpurpose'] = 'Colour of the comment or annotation'; $string['privacy:metadata:conversionpurpose'] = 'Files are converted to PDFs to allow for annotations.'; $string['privacy:metadata:filepurpose'] = 'Stores an annotated PDF with feedback for the user.'; $string['privacy:metadata:rawtextpurpose'] = 'Stores raw text for the quick data.'; $string['privacy:metadata:tablepurpose'] = 'Stores teacher specified quicklist comments'; $string['privacy:metadata:userid'] = 'The user ID'; $string['privacy:path'] = 'PDF Feedback'; $string['generatingpdf'] = 'Generating the PDF...'; $string['rectangle'] = 'Rectangle'; $string['red'] = 'Red'; $string['result'] = 'Result:'; $string['searchcomments'] = 'Search comments'; $string['select'] = 'Select'; $string['stamppicker'] = 'Stamp picker'; $string['stampsdesc'] = 'Stamps must be image files (recommended size: 40x40). These images can be used with the stamp tool to annotate the PDF.'; $string['stamps'] = 'Stamps'; $string['stamp'] = 'Stamp'; $string['test_doesnotexist'] = 'The ghostscript path points to a non-existent file'; $string['test_empty'] = 'The ghostscript path is empty - please enter the correct path'; $string['testgs'] = 'Test ghostscript path'; $string['test_isdir'] = 'The ghostscript path points to a folder, please include the ghostscript program in the path you specify'; $string['test_notestfile'] = 'The test PDF is missing'; $string['test_notexecutable'] = 'The ghostscript points to a file that is not executable'; $string['test_ok'] = 'The ghostscript path appears to be OK - please check you can see the message in the image below'; $string['toolbarbutton'] = '{$a->tool} {$a->shortcut}'; $string['tool'] = 'Tool'; $string['viewfeedbackonline'] = 'View annotated PDF...'; $string['white'] = 'White'; $string['yellow'] = 'Yellow'; $string['draftchangessaved'] = 'Draft annotations saved'; $string['preparesubmissionsforannotation'] = 'Prepare submissions for annotation'; $string['rotateleft'] = 'Rotate 90 degrees to the left'; $string['rotateright'] = 'Rotate 90 degrees to the right'; feedback/editpdf/backup/moodle2/restore_assignfeedback_editpdf_subplugin.class.php000064400000011236152265517350024633 0ustar00. /** * This file contains the restore code for the feedback_editpdf plugin. * * @package assignfeedback_editpdf * @copyright 2013 Damyon Wiese * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); /** * Restore subplugin class. * * Provides the necessary information needed * to restore one assign_feedback subplugin. * * @package assignfeedback_editpdf * @copyright 2013 Damyon Wiese * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class restore_assignfeedback_editpdf_subplugin extends restore_subplugin { /** * Returns the paths to be handled by the subplugin at assignment level * @return array */ protected function define_grade_subplugin_structure() { $paths = array(); // We used get_recommended_name() so this works. // The files node is a placeholder just containing gradeid so we can restore files once per grade. $elename = $this->get_namefor('files'); $elepath = $this->get_pathfor('/feedback_editpdf_files'); $paths[] = new restore_path_element($elename, $elepath); // Now we have the list of comments and annotations per grade. $elename = $this->get_namefor('comment'); $elepath = $this->get_pathfor('/feedback_editpdf_comments/comment'); $paths[] = new restore_path_element($elename, $elepath); $elename = $this->get_namefor('annotation'); $elepath = $this->get_pathfor('/feedback_editpdf_annotations/annotation'); $paths[] = new restore_path_element($elename, $elepath); // Rotation details. $elename = $this->get_namefor('pagerotation'); $elepath = $this->get_pathfor('/feedback_editpdf_rotation/pagerotation'); $paths[] = new restore_path_element($elename, $elepath); return $paths; } /** * Processes one feedback_editpdf_files element * @param mixed $data */ public function process_assignfeedback_editpdf_files($data) { $data = (object)$data; // In this case the id is the old gradeid which will be mapped. $this->add_related_files('assignfeedback_editpdf', \assignfeedback_editpdf\document_services::FINAL_PDF_FILEAREA, 'grade', null, $data->gradeid); $this->add_related_files('assignfeedback_editpdf', \assignfeedback_editpdf\document_services::PAGE_IMAGE_READONLY_FILEAREA, 'grade', null, $data->gradeid); $this->add_related_files('assignfeedback_editpdf', 'stamps', 'grade', null, $data->gradeid); } /** * Processes one feedback_editpdf_annotations/annotation element * @param mixed $data */ public function process_assignfeedback_editpdf_annotation($data) { global $DB; $data = (object)$data; $oldgradeid = $data->gradeid; // The mapping is set in the restore for the core assign activity // when a grade node is processed. $data->gradeid = $this->get_mappingid('grade', $data->gradeid); $DB->insert_record('assignfeedback_editpdf_annot', $data); } /** * Processes one feedback_editpdf_comments/comment element * @param mixed $data */ public function process_assignfeedback_editpdf_comment($data) { global $DB; $data = (object)$data; $oldgradeid = $data->gradeid; // The mapping is set in the restore for the core assign activity // when a grade node is processed. $data->gradeid = $this->get_mappingid('grade', $data->gradeid); $DB->insert_record('assignfeedback_editpdf_cmnt', $data); } /** * Processes one /feedback_editpdf_rotation/pagerotation element * @param mixed $data */ public function process_assignfeedback_editpdf_pagerotation($data) { global $DB; $data = (object)$data; $oldgradeid = $data->gradeid; $data->gradeid = $this->get_mappingid('grade', $oldgradeid); $DB->insert_record('assignfeedback_editpdf_rot', $data); } } feedback/editpdf/backup/moodle2/backup_assignfeedback_editpdf_subplugin.class.php000064400000010364152265517350024416 0ustar00. /** * This file contains the backup code for the feedback_editpdf plugin. * * @package assignfeedback_editpdf * @copyright 2013 Damyon Wiese * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); /** * Provides the information to backup feedback pdf annotations. * * This just adds its fileareas to the annotations and the comments and annotation data. * * @package assignfeedback_editpdf * @copyright 2013 Damyon Wiese * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class backup_assignfeedback_editpdf_subplugin extends backup_subplugin { /** * Returns the subplugin information to attach to feedback element * @return backup_subplugin_element */ protected function define_grade_subplugin_structure() { // Create XML elements. $subplugin = $this->get_subplugin_element(); $subpluginwrapper = new backup_nested_element($this->get_recommended_name()); $subpluginelementfiles = new backup_nested_element('feedback_editpdf_files', null, array('gradeid')); $subpluginelementannotations = new backup_nested_element('feedback_editpdf_annotations'); $subpluginelementannotation = new backup_nested_element('annotation', null, array('gradeid', 'pageno', 'type', 'x', 'y', 'endx', 'endy', 'colour', 'path', 'draft')); $subpluginelementcomments = new backup_nested_element('feedback_editpdf_comments'); $subpluginelementcomment = new backup_nested_element('comment', null, array('gradeid', 'pageno', 'x', 'y', 'width', 'rawtext', 'colour', 'draft')); $subpluginelementrotation = new backup_nested_element('feedback_editpdf_rotation'); $subpluginelementpagerotation = new backup_nested_element('pagerotation', null, array('gradeid', 'pageno', 'pathnamehash', 'isrotated', 'degree')); // Connect XML elements into the tree. $subplugin->add_child($subpluginwrapper); $subpluginelementannotations->add_child($subpluginelementannotation); $subpluginelementcomments->add_child($subpluginelementcomment); $subpluginelementrotation->add_child($subpluginelementpagerotation); $subpluginwrapper->add_child($subpluginelementfiles); $subpluginwrapper->add_child($subpluginelementannotations); $subpluginwrapper->add_child($subpluginelementcomments); $subpluginwrapper->add_child($subpluginelementrotation); // Set source to populate the data. $subpluginelementfiles->set_source_sql('SELECT id AS gradeid from {assign_grades} where id = :gradeid', array('gradeid' => backup::VAR_PARENTID)); $subpluginelementannotation->set_source_table('assignfeedback_editpdf_annot', array('gradeid' => backup::VAR_PARENTID)); $subpluginelementcomment->set_source_table('assignfeedback_editpdf_cmnt', array('gradeid' => backup::VAR_PARENTID)); $subpluginelementpagerotation->set_source_table('assignfeedback_editpdf_rot', array('gradeid' => backup::VAR_PARENTID)); // We only need to backup the files in the final pdf area, and the readonly page images - the others can be regenerated. $subpluginelementfiles->annotate_files('assignfeedback_editpdf', \assignfeedback_editpdf\document_services::FINAL_PDF_FILEAREA, 'gradeid'); $subpluginelementfiles->annotate_files('assignfeedback_editpdf', \assignfeedback_editpdf\document_services::PAGE_IMAGE_READONLY_FILEAREA, 'gradeid'); $subpluginelementfiles->annotate_files('assignfeedback_editpdf', 'stamps', 'gradeid'); return $subplugin; } } feedback/editpdf/pix/line.png000064400000000351152265517350012156 0ustar00PNG  IHDRabKGDIDAT8 @!` J,"Mh%{vaaFAlA]O&٧s1x 9O'>ИrJVBhHHn%uιG5q" XIjE` j` 1"8r,^ ]exIENDB`feedback/editpdf/pix/rotate_left.svg000064400000003602152265517350013554 0ustar00 image/svg+xml feedback/editpdf/pix/nav_prev.png000064400000000453152265517350013052 0ustar00PNG  IHDRabKGDIDAT8J@E ]W,ېB If  m*\w370XMEQUl'ɷ\.wEl6=;xEsB!$)1cY,K׵Z9a<_۶|>7!ş5YQ*8u'4_VQ+ NA`X|(yx!? t\ S=b2m3oEA,l[bd2aJe2v;躎p ,Rz < l6t]< @q@)E<qH$ʔR8Qo øk4Q4:0 Fqwع\uLSA 8t](W3EQtn?fѯۦR)]Ӵw}?{IENDB`feedback/editpdf/pix/background_colour_red.svg000064400000000364152265517350015602 0ustar00feedback/editpdf/pix/background_colour_clear.svg000064400000000676152265517350016124 0ustar00 ]>feedback/editpdf/pix/rotate_right.svg000064400000003576152265517350013751 0ustar00 image/svg+xml feedback/editpdf/pix/drag.png000064400000001074152265517350012147 0ustar00PNG  IHDRabKGDIDAT8kQ渽C"bP &EW dwp/kq`#/h%J$&3w{c.lKk$|ߏai: 'aF)&_UUUm9&pZU4=ji D$pV$%y_4k`u3 ܹYT恳vy N1f3n=I?CW4vrk6'eKDnWYU32ɲ`0gpeHߟΈHqhQk{"2#@^eUuJUkq;)adUUjs@ "/1\t^ cLsiipf8 ADQxZr\rb(Ju8~= @%"Ggmg ,ʷ"gIX\\|* ܋?<<IENDB`feedback/editpdf/pix/colour_yellow.png000064400000000516152265517350014130 0ustar00PNG  IHDR(-SlPLTEU.1378876444544655556565555555555 ^C#tRNS  )8KNOYfpqwv1nIDATxUA 0?33߱%95FqxkD7AKeo|lJC%DB(4N&/ NK:xKeR%JRIENDB`feedback/editpdf/pix/smile.png000064400000002075152265517350012345 0ustar00PNG  IHDR(( H_qPLTEU$93.;730-96311531863627532435535245445444343353343543353544543544435344544435354444544444444444444444444544444444444444_[ztRNS  !#&)*+-367:>ACGIMNX\]^cdginorsuwy{|}~IDATxSE1Dj۶m۶m>ogX9h<}~>ݜiɔW =88L'ag3#ۗ/QrSv dȒӱUE'lI>0,p[!WA)i8HYjQ6A=k_.5O&|܌}YG3]9L`Hֿ|X䛵*k\C°Cv仢82E>: K͐!Z mL1.&k-2#f93RDmHly6;Y+LW4CW/FNbG+memo EIgYu˒*e@uFI-e] Bkp.i35K8ehHۀtI0pqi(x.k\\SnvFFvn"\W 6oErks_?z| image/svg+xml feedback/editpdf/pix/trash.png000064400000000367152265517350012357 0ustar00PNG  IHDRabKGDIDAT8P 0X]aL2&ahb"y!*߻ϳMH)sDCsV<-@p?fr9`'nŬ@U^Tjhm> sI3  ) F^De@\Fr( ,D$feedback/editpdf/pix/colour_white.svg000064400000001221152265517350013742 0ustar00 ]>feedback/editpdf/pix/highlight.png000064400000001032152265517350013173 0ustar00PNG  IHDRabKGDIDAT8A7FS g#Y 9 ٤q3vsB zwhZԘ:$$f{7o~SZ8laxc Tt"ϯg3 [NE q]T*YBp8tkkkG1/Vl2YK&mWAfq_Npm|_α$Ilau-@+ιnEn$IN@x`7Xy0 qL~6}O86q(D$NU7v ^AUu޹EdxjwB"" d/d8^O AЙ4bKQp"Th4zl5˜HӴ\R'Ƙe5`s! E!F ]>feedback/editpdf/pix/pen.png000064400000001147152265517350012015 0ustar00PNG  IHDRabKGDIDAT8KTQ?羁&23D("Z$AQ(h!Q"6OAmz(\?c.B b弉6gdg|s.WW*@Ƚ8IbmmmlTf:BYFݽہ^x8ǀE%˲ gDD"ҥI}'H;0]V&I+g6;8_j]inռBp 8S{Vj8"U[6;nZ;a9Ȳ) t{ﯫꀁ7~Es$I(Y(e٘8nZ Р#R鋤i=CzH+wU ܮ~:zB39'U}U` p#CٜLj,r\%"-!UܢN@0<IENDB`feedback/editpdf/pix/comment.svg000064400000000724152265517350012710 0ustar00 ]>feedback/editpdf/pix/rotate_left.png000064400000000773152265517350013547 0ustar00PNG  IHDRabKGDIDAT8=kQsYq[h)lDӉª~fwVN!('R,ZT Bvv;eӜ}Ϲ‚y~*I;"b8X`UG*EQl[P p(^K(" @ 䟪D^e+{k0|9m8LkDd#"n t:kTvUuZ@DީZHuHk:󼓦iv peYcu:,J+|v}R~?8YUXD XpέBCDv%4^kQȓ_1汪9~L&l_U>\ l`ZФ)YDh?@eSQ\%EUxﷇA 1Ʊ<IENDB`feedback/editpdf/pix/nav_prev.svg000064400000000641152265517350013064 0ustar00 ]>feedback/editpdf/pix/oval.svg000064400000000655152265517350012212 0ustar00 ]>feedback/editpdf/pix/colour_green.svg000064400000000515152265517350013727 0ustar00feedback/editpdf/pix/rectangle.svg000064400000000545152265517350013213 0ustar00 ]>feedback/editpdf/pix/cross.png000064400000001535152265517350012365 0ustar00PNG  IHDR(( H_8PLTE@@33II@@99M3FF@@;;I7G9==F:C7E;@@B:@@>>@9A;@9C=A;@:?:A<@;?:B>B>A=?;A=@BCEGHUX]^_dikmpu~mIDATxS(1l۶m۶Y_xF/TtP'AN)N$tup+#`'Cv/IK`KYS2kXne`JX{21,g~S!-ׄWˡ|93IX].jk&*N3Fb`\)XST2+B6++&ɭ T)u~9^IRm\%}`V gТ]`3CF&&`vuϒO>l* PpuJ!U,ȧQUvȩ*~PY49&#o>I-[1+S◃P}sIENDB`feedback/editpdf/pix/background_colour_white.svg000064400000001065152265517350016147 0ustar00 ]>feedback/editpdf/pix/comment.png000064400000000227152265517350012673 0ustar00PNG  IHDRabKGDLIDAT8c@DfG& 430000 s***fD挺`ػELS?&n5KIENDB`feedback/editpdf/pix/background_colour_clear.png000064400000000241152265517350016075 0ustar00PNG  IHDRabKGDVIDAT8 0 %e*{{d4R .EOO#qtjdGDqQ Ph*w_ls>&nO IENDB`feedback/editpdf/pix/background_colour_green.png000064400000000152152265517350016110 0ustar00PNG  IHDR%=m"PLTE겁ftRNSkIDATxcg@$* CIENDB`feedback/editpdf/pix/select.png000064400000000532152265517350012507 0ustar00PNG  IHDRabKGDIDAT8бK@$8:k Cu(I dHAprPW'AQ:u҆Fw{sQA$+|,uWਮ뇪6\D䰮4Mw&{↑+ <+ (_5(<4Ed鑵x>`WUu`a*˔eYݍ܈_0 ð\&Yu Ƙ"Rm״KvVECABIENDB`feedback/editpdf/pix/colour_green.png000064400000000516152265517350013715 0ustar00PNG  IHDR(-SlPLTEUF==@@>@=>=??==>=?>>>>=>>>>>>>>>>6M#tRNS  )8KNOYfpqwv1nIDATxUA 0?33߱%95FqxkD7AKeo|lJC%DB(4N&/ NK:xKeR%JRIENDB`feedback/editpdf/pix/line.svg000064400000000636152265517350012177 0ustar00 ]>feedback/editpdf/pix/comment_search.svg000064400000001561152265517350014235 0ustar00 ]>feedback/editpdf/pix/colour_blue.svg000064400000000515152265517350013556 0ustar00feedback/editpdf/pix/sad.png000064400000001706152265517350012003 0ustar00PNG  IHDR(( H_DPLTEmۀqƀv؀ۀxҀՀyzӀ؀{|ӀՀЀ|Ӏ}Հ}{}{{{~~~~~~|~}}}~}}||}|}||||||}}}}~}}|}|}|}}}}}}}}}}}~~~|}||}}}}}}}}}}}}}}}}}}}}}NktRNS #$&()*+.567:<>ACGIKMNWXZ\]^`girstuwy{}HIDATxC`3m۶m EUȯxpiriTqNq1SX^vWoަ38*Q ة_FN>ML ]>feedback/editpdf/pix/background_colour_yellow.png000064400000000152152265517350016323 0ustar00PNG  IHDR%=m"PLTE|P tRNSkIDATxcg@$* CIENDB`feedback/editpdf/pix/comment_search.png000064400000000761152265517350014223 0ustar00PNG  IHDRabKGDIDAT8kTQ D1""DŴ+ΔPHE%Fc)E4gUD+e}w,093 91>f; Lg; P*Pl9R1Fc$}4˲/zJ颤y`|;re+K67Fa׶ʲ<"i |SRJ%-:t}@~f| PeI˶oJ:BUTzyn!ip.Q#&BeANGAc}c|EN`nfcjlUU?YI=ۻ% W@UIENDB`feedback/editpdf/pix/background_colour_red.png000064400000000152152265517350015562 0ustar00PNG  IHDR%=m"PLTE_#tRNSkIDATxcg@$* CIENDB`feedback/editpdf/pix/drag.svg000064400000002024152265517350012156 0ustar00 ]> feedback/editpdf/pix/select.svg000064400000000706152265517350012525 0ustar00 ]>feedback/editpdf/pix/background_colour_yellow.svg000064400000000364152265517350016343 0ustar00feedback/editpdf/pix/background_colour_blue.png000064400000000152152265517350015737 0ustar00PNG  IHDR%=m"PLTEtRNSkIDATxcg@$* CIENDB`feedback/editpdf/pix/rotate_right.png000064400000001010152265517350013713 0ustar00PNG  IHDRabKGDIDAT8kTA39kh?! VʡډeDի}BlBS Z hE|EU7}|;+ 2-c3ۮiQ?".M! YOI,zN恇eYE[43D0ZJӴ yCPc9&V;!U}/3;fv%"?n.t@Uo)p&Wvf֎[ 92zW,)2옚E4`NEdg5͓ SQ"2h@]ע ,"{[xfjU]n,"+!+"~?wUuebfq{@@#m'Ir6˲z~'{`/{/j`-IY y4yX?KIoߚh\|#IENDB`feedback/editpdf/pix/background_colour_white.png000064400000000241152265517350016127 0ustar00PNG  IHDRabKGDVIDAT8ӡ0 CQ7̌3^BF E9 3]qglTw1F Dߦg'p? ,eMw~T@uCIENDB`feedback/editpdf/pix/tick.png000064400000002017152265517350012162 0ustar00PNG  IHDR((mIDATx+IV<۶ڶm۶m۶mclF[Q=B*;)~9AQc2*xWՍdUoG#kX?r/ a96n_|p kO?{4pFC,n [>N繰mJ982zx;ʧ@@;N^:-+Wm[ Whz+N dU8 >{n%#>tE`'PLUKYC`[*Syᴼ‰>k~#JCj܊Xk9TI4pњ!z=5-twHȁ #D;i>Z4')h&pЉfP#$qP'l͵{s`ND#z牛$@7rCs-UJ]шC=&{$jkf(*S:3vhB=w-nVx؊J S,J ?: x؂Nm`w1sSxA \@l=HS]̱Tr"m@+bxŋI^G2`-icmI<\: ۠r9[akzt N-:4H#Gt`D-% JNv2cm*Hl+q|V?'TN*FSE8 ^'O 5T&pp0ɀ!w.C7RI'B,>l$=(O I:~xSXUXሡk 3DU7Xq^DtO`OE?prn>Pc7CQ`5?rfeedback/editpdf/pix/nav_next.png000064400000000524152265517350013053 0ustar00PNG  IHDRabKGD IDAT8=K`)AC\:/PpwoP,$nj'_BT(jLܳ\>=d2i@$0|3FIKs~ u*FYy齿5kzcI>;EQIee٥snhf/W)& ~I<ɡ$I. zYqQ8Iן$H]L qEhɩpq|_ڼ7 xn^IENDB`feedback/editpdf/pix/nav_next.svg000064400000000640152265517350013065 0ustar00 ]>feedback/editpdf/pix/background_colour_green.svg000064400000000364152265517350016130 0ustar00feedback/editpdf/pix/colour_red.png000064400000000516152265517350013367 0ustar00PNG  IHDR(-SlPLTEUUFFI=G=I@H@D>D@DAEADAE?FAD@F?E@FAE@D@EAF@E?E@E@E?E@E?E@E?E?E@E@E@:#tRNS  )8KNOYfpqwv1nIDATxUA 0?33߱%95FqxkD7AKeo|lJC%DB(4N&/ NK:xKeR%JRIENDB`feedback/editpdf/pix/comment_expcol.png000064400000000623152265517350014245 0ustar00PNG  IHDRabKGDHIDAT8J\Aw4.[m2!sXA2R3vV!Bovvi =г?+F}aogacܑL)Lf#<I `o9n)PJ%ZpR,i~fp-]Nlݢcۥzᶤ7eY~- 8 3 >Fpt~qEW[4ciQC߷=3Q|T>>l9l4n/z_OX Z9fTUUc}:PJi>68,i x; )+zGѡIENDB`feedback/editpdf/pix/colour_blue.png000064400000000516152265517350013544 0ustar00PNG  IHDR(-SlPLTEUtyzրр|{~|~~}}|||}|}}}~|}}}}}}}}}744#tRNS  )8KNOYfpqwv1nIDATxUA 0?33߱%95FqxkD7AKeo|lJC%DB(4N&/ NK:xKeR%JRIENDB`feedback/editpdf/pix/trash.svg000064400000001156152265517350012367 0ustar00 ]>feedback/editpdf/pix/colour_black.svg000064400000000747152265517350013712 0ustar00 ]>feedback/editpdf/pix/colour_red.svg000064400000000515152265517350013401 0ustar00feedback/editpdf/pix/stamp.png000064400000000440152265517350012352 0ustar00PNG  IHDRabKGDIDAT8j0E2 ;'8^tֱS ?ԩCcy`d# sߪO!AUއ ڛy 15Н|z Iԫ~Ts P& 97&fU ,˺9ME3eY5Aov2Sۙxoo1UUEi,EQ;E0500IENDB`feedback/editpdf/pix/colour_black.png000064400000000347152265517350013673 0ustar00PNG  IHDRR*PLTE333333333333333333333333333333333333333$ tRNS @P`p1_IDATxc38ޝf޽ {mz ncD DQ-qȹ{q"{7 f2Pd; ]3WIENDB`feedback/editpdf/pix/rectangle.png000064400000000174152265517350013176 0ustar00PNG  IHDRabKGD1IDAT8c@`D3 QQQHF)v <7 OKIENDB`feedback/editpdf/pix/pen.svg000064400000001647152265517350012035 0ustar00 ]>feedback/editpdf/lib.php000064400000010713152265517350011203 0ustar00. /** * This file contains the version information for the comments feedback plugin * * @package assignfeedback_editpdf * @copyright 2012 Davo Smith * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->dirroot . '/mod/assign/locallib.php'); /** * Serves assignment feedback and other files. * * @param mixed $course course or id of the course * @param mixed $cm course module or id of the course module * @param context $context * @param string $filearea * @param array $args * @param bool $forcedownload * @param array $options - List of options affecting file serving. * @return bool false if file not found, does not return if found - just send the file */ function assignfeedback_editpdf_pluginfile( $course, $cm, context $context, $filearea, $args, $forcedownload, array $options = array() ) { global $DB; if ($filearea === 'systemstamps') { if ($context->contextlevel !== CONTEXT_SYSTEM) { return false; } $filename = array_pop($args); $filepath = '/' . implode('/', $args) . '/'; $fs = get_file_storage(); $file = $fs->get_file($context->id, 'assignfeedback_editpdf', $filearea, 0, $filepath, $filename); if (!$file) { return false; } $options['cacheability'] = 'public'; $options['immutable'] = true; send_stored_file($file, null, 0, false, $options); } if ($context->contextlevel == CONTEXT_MODULE) { require_login($course, false, $cm); $itemid = (int)array_shift($args); $assign = new assign($context, $cm, $course); $record = $DB->get_record('assign_grades', array('id' => $itemid), 'userid,assignment', MUST_EXIST); $userid = $record->userid; if ($assign->get_instance()->id != $record->assignment) { return false; } // Rely on mod_assign checking permissions. if (!$assign->can_view_submission($userid)) { return false; } $relativepath = implode('/', $args); $fullpath = "/{$context->id}/assignfeedback_editpdf/$filearea/$itemid/$relativepath"; $fs = get_file_storage(); if (!$file = $fs->get_file_by_hash(sha1($fullpath)) or $file->is_directory()) { return false; } // Download MUST be forced - security! send_stored_file($file, 0, 0, true, $options);// Check if we want to retrieve the stamps. } } /** * Files API hook to remove stale conversion records. * * When a file is update, its contenthash will change, but its ID * remains the same. The document converter API records source file * IDs and destination file IDs. When a file is updated, the document * converter API has no way of knowing that the content of the file * has changed, so it just serves the previously stored destination * file. * * In this hook we check if the contenthash has changed, and if it has * we delete the existing conversion so that a new one will be created. * * @param stdClass $file The updated file record. * @param stdClass $filepreupdate The file record pre-update. */ function assignfeedback_editpdf_after_file_updated(stdClass $file, stdClass $filepreupdate) { $contenthashchanged = $file->contenthash !== $filepreupdate->contenthash; if ($contenthashchanged && $file->component == 'assignsubmission_file' && $file->filearea == 'submission_files') { $fs = get_file_storage(); $file = $fs->get_file_by_id($file->id); $conversions = \core_files\conversion::get_conversions_for_file($file, 'pdf'); foreach ($conversions as $conversion) { if ($conversion->get('id')) { $conversion->delete(); } } } } feedback/editpdf/version.php000064400000002055152265517350012122 0ustar00. /** * This file contains the version information for the comments feedback plugin * * @package assignfeedback_editpdf * @copyright 2012 Davo Smith * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); $plugin->version = 2025041400; $plugin->requires = 2025040800; $plugin->component = 'assignfeedback_editpdf'; feedback/editpdf/ajax_progress.php000064400000005453152265517350013311 0ustar00. /** * Process concurrent ajax request. * ALL RETURNED INFO IS PUBLIC. * * @package assignfeedback_editpdf * @copyright 2013 Jerome Mouneyrac * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ define('AJAX_SCRIPT', true); // To be able to process concurrent ajax request with the generate pdf ajax request we can not use cookie. define('NO_MOODLE_COOKIES', true); use \assignfeedback_editpdf\document_services; require_once('../../../../config.php'); try { $assignmentid = required_param('assignmentid', PARAM_INT); $userid = required_param('userid', PARAM_INT); $attemptnumber = required_param('attemptnumber', PARAM_INT); // Retrieve the assignments. require_once($CFG->dirroot . '/mod/assign/locallib.php'); $cm = get_coursemodule_from_instance('assign', $assignmentid, 0, false, MUST_EXIST); $context = context_module::instance($cm->id); $assignment = new assign($context, null, null); // Get the generated images from file API call. $grade = $assignment->get_user_grade($userid, false, $attemptnumber); // Check we found a grade. if (empty($grade)) { throw new coding_exception('grade not found'); } // No need to handle the readonly files here, the should be already generated. $component = 'assignfeedback_editpdf'; $filearea = document_services::PAGE_IMAGE_FILEAREA; $filepath = '/'; $fs = get_file_storage(); $files = $fs->get_directory_files($context->id, $component, $filearea, $grade->id, $filepath); // The important security part: we ONLY RETURN the total NUMBER of generated images. echo $OUTPUT->header(); echo json_encode(count($files)); echo $OUTPUT->footer(); } catch (Exception $e) { // This should never happened! // Return a 500 HTTP header so Y.io gets it as a failure. if (substr(php_sapi_name(), 0, 3) == 'cgi') { header("Status: 500 Internal Server Error"); } else { header('HTTP/1.0 500 Internal Server Error'); } throw new moodle_exception('An exception was caught but can not be returned for security purpose. To easily debug, comment the try catch.'); } feedback/editpdf/classes/comments_quick_list.php000064400000005544152265517350016154 0ustar00. /** * This file contains the functions for managing a users comments quicklist. * * @package assignfeedback_editpdf * @copyright 2012 Davo Smith * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace assignfeedback_editpdf; /** * This class performs crud operations on a users quicklist comments. * * No capability checks are done - they should be done by the calling class. * @package assignfeedback_editpdf * @copyright 2012 Davo Smith * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class comments_quick_list { /** * Get all comments for the current user. * @return array(comment) */ public static function get_comments() { global $DB, $USER; $comments = array(); $records = $DB->get_records('assignfeedback_editpdf_quick', array('userid'=>$USER->id)); return $records; } /** * Add a comment to the quick list. * @param string $commenttext * @param int $width * @param string $colour * @return \stdClass - the comment record (with new id set) */ public static function add_comment($commenttext, $width, $colour) { global $DB, $USER; $comment = new \stdClass(); $comment->userid = $USER->id; $comment->rawtext = $commenttext; $comment->width = $width; $comment->colour = $colour; $comment->id = $DB->insert_record('assignfeedback_editpdf_quick', $comment); return $comment; } /** * Get a single comment by id. * @param int $commentid * @return comment or false */ public static function get_comment($commentid) { global $DB; $record = $DB->get_record('assignfeedback_editpdf_quick', array('id'=>$commentid), '*', IGNORE_MISSING); if ($record) { return $record; } return false; } /** * Remove a comment from the quick list. * @param int $commentid * @return bool */ public static function remove_comment($commentid) { global $DB, $USER; return $DB->delete_records('assignfeedback_editpdf_quick', array('id'=>$commentid, 'userid'=>$USER->id)); } } feedback/editpdf/classes/renderer.php000064400000030016152265517350013676 0ustar00. /** * This file contains the definition for the library class for edit PDF renderer. * * @package assignfeedback_editpdf * @copyright 2012 Davo Smith * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); /** * A custom renderer class that extends the plugin_renderer_base and is used by the editpdf feedback plugin. * * @package assignfeedback_editpdf * @copyright 2013 Davo Smith * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class assignfeedback_editpdf_renderer extends plugin_renderer_base { /** * Return the PDF button shortcut. * * @param string $name the name of a specific button. * @return string the specific shortcut. */ private function get_shortcut($name) { $shortcuts = array('navigate-previous-button' => 'j', 'rotateleft' => 'q', 'rotateright' => 'w', 'navigate-page-select' => 'k', 'navigate-next-button' => 'l', 'searchcomments' => 'h', 'expcolcomments' => 'g', 'comment' => 'z', 'commentcolour' => 'x', 'select' => 'c', 'drag' => 'd', 'pen' => 'y', 'line' => 'u', 'rectangle' => 'i', 'oval' => 'o', 'highlight' => 'p', 'annotationcolour' => 'r', 'stamp' => 'n', 'currentstamp' => 'm'); // Return the shortcut. return $shortcuts[$name]; } /** * Render a single colour button. * * @param string $icon - The key for the icon * @param string $tool - The key for the lang string. * @param string $accesskey Optional - The access key for the button. * @param bool $disabled Optional - Is this button disabled. * @return string */ private function render_toolbar_button($icon, $tool, $accesskey = null, $disabled=false) { // Build button alt text. $alttext = new stdClass(); $alttext->tool = get_string($tool, 'assignfeedback_editpdf'); if (!empty($accesskey)) { $alttext->shortcut = '(Alt/Shift-Alt/Ctrl-Option + ' . $accesskey . ')'; } else { $alttext->shortcut = ''; } $iconalt = get_string('toolbarbutton', 'assignfeedback_editpdf', $alttext); $iconhtml = $this->image_icon($icon, $iconalt, 'assignfeedback_editpdf'); $iconparams = array('data-tool'=>$tool, 'class'=>$tool . 'button'); if ($disabled) { $iconparams['disabled'] = 'true'; } if (!empty($accesskey)) { $iconparams['accesskey'] = $accesskey; } return html_writer::tag('button', $iconhtml, $iconparams); } /** * Render the editpdf widget in the grading form. * * @param assignfeedback_editpdf_widget $widget - Renderable widget containing assignment, user and attempt number. * @return string */ public function render_assignfeedback_editpdf_widget(assignfeedback_editpdf_widget $widget) { global $CFG; $html = ''; $html .= html_writer::div(get_string('jsrequired', 'assignfeedback_editpdf'), 'hiddenifjs'); $linkid = html_writer::random_id(); $launcheditorstring = $widget->readonly ? get_string('viewfeedbackonline', 'assignfeedback_editpdf') : get_string('launcheditor', 'assignfeedback_editpdf'); $links = html_writer::link('#', $launcheditorstring, ['id' => $linkid, 'class' => 'd-block mt-2']); $html .= ''; $html .= html_writer::div($links, 'visibleifjs'); $header = get_string('pluginname', 'assignfeedback_editpdf'); $body = ''; // Create the page navigation. $navigation1 = ''; $navigation2 = ''; $navigation3 = ''; // Pick the correct arrow icons for right to left mode. if (right_to_left()) { $nav_prev = 'nav_next'; $nav_next = 'nav_prev'; } else { $nav_prev = 'nav_prev'; $nav_next = 'nav_next'; } $iconshortcut = $this->get_shortcut('navigate-previous-button'); $iconalt = get_string('navigateprevious', 'assignfeedback_editpdf', $iconshortcut); $iconhtml = $this->image_icon($nav_prev, $iconalt, 'assignfeedback_editpdf'); $navigation1 .= html_writer::tag('button', $iconhtml, array('disabled'=>'true', 'class'=>'navigate-previous-button', 'accesskey' => $this->get_shortcut('navigate-previous-button'))); $navigation1 .= html_writer::tag('select', null, array('disabled'=>'true', 'aria-label' => get_string('gotopage', 'assignfeedback_editpdf'), 'class'=>'navigate-page-select', 'accesskey' => $this->get_shortcut('navigate-page-select'))); $iconshortcut = $this->get_shortcut('navigate-next-button'); $iconalt = get_string('navigatenext', 'assignfeedback_editpdf', $iconshortcut); $iconhtml = $this->image_icon($nav_next, $iconalt, 'assignfeedback_editpdf'); $navigation1 .= html_writer::tag('button', $iconhtml, array('disabled'=>'true', 'class'=>'navigate-next-button', 'accesskey' => $this->get_shortcut('navigate-next-button'))); $navigation1 = html_writer::div($navigation1, 'navigation', array('role'=>'navigation')); $navigation2 .= $this->render_toolbar_button('comment_search', 'searchcomments', $this->get_shortcut('searchcomments')); $navigation2 = html_writer::div($navigation2, 'navigation-search', array('role'=>'navigation')); $navigation3 .= $this->render_toolbar_button('comment_expcol', 'expcolcomments', $this->get_shortcut('expcolcomments')); $navigation3 = html_writer::div($navigation3, 'navigation-expcol', array('role' => 'navigation')); $rotationtools = ''; if (!$widget->readonly) { $rotationtools .= $this->render_toolbar_button('rotate_left', 'rotateleft', $this->get_shortcut('rotateleft')); $rotationtools .= $this->render_toolbar_button('rotate_right', 'rotateright', $this->get_shortcut('rotateright')); $rotationtools = html_writer::div($rotationtools, 'toolbar', array('role' => 'toolbar')); } $toolbargroup = ''; $clearfix = html_writer::div('', 'clearfix'); if (!$widget->readonly) { // Comments. $toolbar1 = ''; $toolbar1 .= $this->render_toolbar_button('comment', 'comment', $this->get_shortcut('comment')); $toolbar1 .= $this->render_toolbar_button('background_colour_clear', 'commentcolour', $this->get_shortcut('commentcolour')); $toolbar1 = html_writer::div($toolbar1, 'toolbar', array('role' => 'toolbar')); // Select Tool. $toolbar2 = ''; $toolbar2 .= $this->render_toolbar_button('drag', 'drag', $this->get_shortcut('drag')); $toolbar2 .= $this->render_toolbar_button('select', 'select', $this->get_shortcut('select')); $toolbar2 = html_writer::div($toolbar2, 'toolbar', array('role' => 'toolbar')); // Other Tools. $toolbar3 = ''; $toolbar3 .= $this->render_toolbar_button('pen', 'pen', $this->get_shortcut('pen')); $toolbar3 .= $this->render_toolbar_button('line', 'line', $this->get_shortcut('line')); $toolbar3 .= $this->render_toolbar_button('rectangle', 'rectangle', $this->get_shortcut('rectangle')); $toolbar3 .= $this->render_toolbar_button('oval', 'oval', $this->get_shortcut('oval')); $toolbar3 .= $this->render_toolbar_button('highlight', 'highlight', $this->get_shortcut('highlight')); $toolbar3 .= $this->render_toolbar_button('background_colour_clear', 'annotationcolour', $this->get_shortcut('annotationcolour')); $toolbar3 = html_writer::div($toolbar3, 'toolbar', array('role' => 'toolbar')); // Stamps. $toolbar4 = ''; $toolbar4 .= $this->render_toolbar_button('stamp', 'stamp', $this->get_shortcut('stamp')); $toolbar4 .= $this->render_toolbar_button('background_colour_clear', 'currentstamp', $this->get_shortcut('currentstamp')); $toolbar4 = html_writer::div($toolbar4, 'toolbar', array('role'=>'toolbar')); // Add toolbars to toolbar_group in order of display, and float the toolbar_group right. $toolbars = $rotationtools . $toolbar1 . $toolbar2 . $toolbar3 . $toolbar4; $toolbargroup = html_writer::div($toolbars, 'toolbar_group', ['role' => 'toolbar']); } $pageheader = html_writer::div($navigation1 . $navigation2 . $navigation3 . $toolbargroup . $clearfix, 'pageheader'); $body = $pageheader; // Loading progress bar. $progressbar = html_writer::div('', 'bar', array('style' => 'width: 0%')); $progressbar = html_writer::div($progressbar, 'progress progress-info progress-striped active', array('title' => get_string('loadingeditor', 'assignfeedback_editpdf'), 'role'=> 'progressbar', 'aria-valuenow' => 0, 'aria-valuemin' => 0, 'aria-valuemax' => 100)); $progressbarlabel = html_writer::div(get_string('generatingpdf', 'assignfeedback_editpdf'), 'progressbarlabel'); $loading = html_writer::div($progressbar . $progressbarlabel, 'loading'); $canvas = html_writer::div($loading, 'drawingcanvas'); $canvas = html_writer::div($canvas, 'drawingregion'); // Place for messages, but no warnings displayed yet. $changesmessage = html_writer::div('', 'warningmessages'); $canvas .= $changesmessage; $infoicon = $this->image_icon('i/info', ''); $infomessage = html_writer::div($infoicon, 'infoicon'); $canvas .= $infomessage; $body .= $canvas; $footer = ''; $editorparams = array( array( 'header' => $header, 'body' => $body, 'footer' => $footer, 'linkid' => $linkid, 'assignmentid' => $widget->assignment, 'userid' => $widget->userid, 'attemptnumber' => $widget->attemptnumber, 'stampfiles' => $widget->stampfiles, 'readonly' => $widget->readonly, ) ); $this->page->requires->yui_module('moodle-assignfeedback_editpdf-editor', 'M.assignfeedback_editpdf.editor.init', $editorparams); $this->page->requires->strings_for_js(array( 'yellow', 'white', 'red', 'blue', 'green', 'black', 'clear', 'colourpicker', 'loadingeditor', 'pagexofy', 'deletecomment', 'addtoquicklist', 'filter', 'searchcomments', 'commentcontextmenu', 'deleteannotation', 'stamp', 'stamppicker', 'cannotopenpdf', 'pagenumber', 'partialwarning', 'draftchangessaved' ), 'assignfeedback_editpdf'); return $html; } } feedback/editpdf/classes/privacy/provider.php000064400000016247152265517350015411 0ustar00. /** * Privacy class for requesting user data. * * @package assignfeedback_editpdf * @copyright 2018 Adrian Greeve * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace assignfeedback_editpdf\privacy; defined('MOODLE_INTERNAL') || die(); require_once($CFG->dirroot . '/mod/assign/locallib.php'); use \core_privacy\local\metadata\collection; use \mod_assign\privacy\assignfeedback_provider; use \core_privacy\local\request\writer; use \core_privacy\local\request\contextlist; use \mod_assign\privacy\assign_plugin_request_data; use \mod_assign\privacy\useridlist; /** * Privacy class for requesting user data. * * @package assignfeedback_editpdf * @copyright 2018 Adrian Greeve * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class provider implements \core_privacy\local\metadata\provider, \mod_assign\privacy\assignfeedback_provider, \mod_assign\privacy\assignfeedback_user_provider { /** * Return meta data about this plugin. * * @param collection $collection A list of information to add to. * @return collection Return the collection after adding to it. */ public static function get_metadata(collection $collection): collection { $quickdata = [ 'userid' => 'privacy:metadata:userid', 'rawtext' => 'privacy:metadata:rawtextpurpose', 'colour' => 'privacy:metadata:colourpurpose' ]; $collection->add_database_table('assignfeedback_editpdf_quick', $quickdata, 'privacy:metadata:tablepurpose'); $collection->add_subsystem_link('core_files', [], 'privacy:metadata:filepurpose'); $collection->add_subsystem_link('core_fileconverter', [], 'privacy:metadata:conversionpurpose'); return $collection; } /** * No need to fill in this method as all information can be acquired from the assign_grades table in the mod assign * provider. * * @param int $userid The user ID. * @param contextlist $contextlist The context list. */ public static function get_context_for_userid_within_feedback(int $userid, contextlist $contextlist) { // This uses the assign_grade table. } /** * This also does not need to be filled in as this is already collected in the mod assign provider. * * @param useridlist $useridlist A list of user IDs */ public static function get_student_user_ids(useridlist $useridlist) { // Not required. } /** * If you have tables that contain userids and you can generate entries in your tables without creating an * entry in the assign_grades table then please fill in this method. * * @param \core_privacy\local\request\userlist $userlist The userlist object */ public static function get_userids_from_context(\core_privacy\local\request\userlist $userlist) { // Not required. } /** * Export all user data for this plugin. * * @param assign_plugin_request_data $exportdata Data used to determine which context and user to export and other useful * information to help with exporting. */ public static function export_feedback_user_data(assign_plugin_request_data $exportdata) { $currentpath = $exportdata->get_subcontext(); $currentpath[] = get_string('privacy:path', 'assignfeedback_editpdf'); $assign = $exportdata->get_assign(); $plugin = $assign->get_plugin_by_type('assignfeedback', 'editpdf'); $fileareas = $plugin->get_user_data_file_areas(); $grade = $exportdata->get_pluginobject(); foreach ($fileareas as $filearea => $notused) { writer::with_context($exportdata->get_context()) ->export_area_files($currentpath, 'assignfeedback_editpdf', $filearea, $grade->id); } } /** * Any call to this method should delete all user data for the context defined in the deletion_criteria. * * @param assign_plugin_request_data $requestdata Data useful for deleting user data from this sub-plugin. */ public static function delete_feedback_for_context(assign_plugin_request_data $requestdata) { $assign = $requestdata->get_assign(); $plugin = $assign->get_plugin_by_type('assignfeedback', 'editpdf'); $fileareas = $plugin->get_file_areas(); $fs = get_file_storage(); foreach ($fileareas as $filearea => $notused) { // Delete pdf files. $fs->delete_area_files($requestdata->get_context()->id, 'assignfeedback_editpdf', $filearea); } // Delete entries from the tables. $plugin->delete_instance(); } /** * Calling this function should delete all user data associated with this grade. * * @param assign_plugin_request_data $requestdata Data useful for deleting user data. */ public static function delete_feedback_for_grade(assign_plugin_request_data $requestdata) { $requestdata->set_userids([$requestdata->get_user()->id]); $requestdata->populate_submissions_and_grades(); self::delete_feedback_for_grades($requestdata); } /** * Deletes all feedback for the grade ids / userids provided in a context. * assign_plugin_request_data contains: * - context * - assign object * - grade ids (pluginids) * - user ids * @param assign_plugin_request_data $deletedata A class that contains the relevant information required for deletion. */ public static function delete_feedback_for_grades(assign_plugin_request_data $deletedata) { global $DB; if (empty($deletedata->get_gradeids())) { return; } $assign = $deletedata->get_assign(); $plugin = $assign->get_plugin_by_type('assignfeedback', 'editpdf'); $fileareas = $plugin->get_file_areas(); $fs = get_file_storage(); list($sql, $params) = $DB->get_in_or_equal($deletedata->get_gradeids(), SQL_PARAMS_NAMED); foreach ($fileareas as $filearea => $notused) { // Delete pdf files. $fs->delete_area_files_select($deletedata->get_context()->id, 'assignfeedback_editpdf', $filearea, $sql, $params); } // Remove table entries. $DB->delete_records_select('assignfeedback_editpdf_annot', "gradeid $sql", $params); $DB->delete_records_select('assignfeedback_editpdf_cmnt', "gradeid $sql", $params); $DB->delete_records_select('assignfeedback_editpdf_rot', "gradeid $sql", $params); } } feedback/editpdf/classes/pdf.php000064400000103041152265517350012640 0ustar00. /** * Library code for manipulating PDFs * * @package assignfeedback_editpdf * @copyright 2012 Davo Smith * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace assignfeedback_editpdf; use setasign\Fpdi\Tcpdf\Fpdi; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->libdir.'/pdflib.php'); require_once($CFG->dirroot.'/mod/assign/feedback/editpdf/fpdi/src/autoload.php'); /** * Library code for manipulating PDFs * * @package assignfeedback_editpdf * @copyright 2012 Davo Smith * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class pdf extends Fpdi { /** @var int the number of the current page in the PDF being processed */ protected $currentpage = 0; /** @var int the total number of pages in the PDF being processed */ protected $pagecount = 0; /** @var float used to scale the pixel position of annotations (in the database) to the position in the final PDF */ protected $scale = 0.0; /** @var string the path in which to store generated page images */ protected $imagefolder = null; /** @var string the path to the PDF currently being processed */ protected $filename = null; /** @var string the fontname used when the PDF being processed */ protected $fontname = null; /** No errors */ const GSPATH_OK = 'ok'; /** Not set */ const GSPATH_EMPTY = 'empty'; /** Does not exist */ const GSPATH_DOESNOTEXIST = 'doesnotexist'; /** Is a dir */ const GSPATH_ISDIR = 'isdir'; /** Not executable */ const GSPATH_NOTEXECUTABLE = 'notexecutable'; /** Test file missing */ const GSPATH_NOTESTFILE = 'notestfile'; /** Any other error */ const GSPATH_ERROR = 'error'; /** Min. width an annotation should have */ const MIN_ANNOTATION_WIDTH = 5; /** Min. height an annotation should have */ const MIN_ANNOTATION_HEIGHT = 5; /** Blank PDF file used during error. */ const BLANK_PDF = '/mod/assign/feedback/editpdf/fixtures/blank.pdf'; /** Page image file name prefix*/ const IMAGE_PAGE = 'image_page'; /** * Get the name of the font to use in generated PDF files. * If $CFG->pdfexportfont is set - use it, otherwise use "freesans" as this * open licensed font has wide support for different language charsets. * * @return string */ private function get_export_font_name() { $fontname = 'freesans'; if (!empty($this->fontname)) { $fontname = $this->fontname; } return $fontname; } /** * Set font name. * * @param string $fontname Font name which is * @return void */ public function set_export_font_name($fontname): void { $this->fontname = $fontname; } /** * Combine the given PDF files into a single PDF. Optionally add a coversheet and coversheet fields. * @param string[] $pdflist the filenames of the files to combine * @param string $outfilename the filename to write to * @return int the number of pages in the combined PDF */ public function combine_pdfs($pdflist, $outfilename) { raise_memory_limit(MEMORY_EXTRA); $olddebug = error_reporting(0); $this->setPageUnit('pt'); $this->setPrintHeader(false); $this->setPrintFooter(false); $this->scale = 72.0 / 100.0; // Use font supporting the widest range of characters. $this->SetFont($this->get_export_font_name(), '', 16.0 * $this->scale, '', true); $this->SetTextColor(0, 0, 0); $totalpagecount = 0; foreach ($pdflist as $file) { $pagecount = $this->setSourceFile($file); $totalpagecount += $pagecount; for ($i = 1; $i<=$pagecount; $i++) { $this->create_page_from_source($i); } } $this->save_pdf($outfilename); error_reporting($olddebug); return $totalpagecount; } /** * The number of the current page in the PDF being processed * @return int */ public function current_page() { return $this->currentpage; } /** * The total number of pages in the PDF being processed * @return int */ public function page_count() { return $this->pagecount; } /** * Load the specified PDF and set the initial output configuration * Used when processing comments and outputting a new PDF * @param string $filename the path to the PDF to load * @return int the number of pages in the PDF */ public function load_pdf($filename) { raise_memory_limit(MEMORY_EXTRA); $olddebug = error_reporting(0); $this->setPageUnit('pt'); $this->scale = 72.0 / 100.0; $this->SetFont($this->get_export_font_name(), '', 16.0 * $this->scale, '', true); $this->SetFillColor(255, 255, 176); $this->SetDrawColor(0, 0, 0); $this->SetLineWidth(1.0 * $this->scale); $this->SetTextColor(0, 0, 0); $this->setPrintHeader(false); $this->setPrintFooter(false); $this->pagecount = $this->setSourceFile($filename); $this->filename = $filename; error_reporting($olddebug); return $this->pagecount; } /** * Sets the name of the PDF to process, but only loads the file if the * pagecount is zero (in order to count the number of pages) * Used when generating page images (but not a new PDF) * @param string $filename the path to the PDF to process * @param int $pagecount optional the number of pages in the PDF, if known * @return int the number of pages in the PDF */ public function set_pdf($filename, $pagecount = 0) { if ($pagecount == 0) { return $this->load_pdf($filename); } else { $this->filename = $filename; $this->pagecount = $pagecount; return $pagecount; } } /** * Copy the next page from the source file and set it as the current page * @return bool true if successful */ public function copy_page() { if (!$this->filename) { return false; } if ($this->currentpage>=$this->pagecount) { return false; } $this->currentpage++; $this->create_page_from_source($this->currentpage); return true; } /** * Create a page from a source PDF. * * @param int $pageno */ protected function create_page_from_source($pageno) { // Get the size (and deduce the orientation) of the next page. $template = $this->importPage($pageno); $size = $this->getTemplateSize($template); // Create a page of the required size / orientation. $this->AddPage($size['orientation'], array($size['width'], $size['height'])); // Prevent new page creation when comments are at the bottom of a page. $this->setPageOrientation($size['orientation'], false, 0); // Fill in the page with the original contents from the student. $this->useTemplate($template); } /** * Copy all the remaining pages in the file */ public function copy_remaining_pages() { $morepages = true; while ($morepages) { $morepages = $this->copy_page(); } } /** * Append all comments to the end of the document. * * @param array $allcomments All comments, indexed by page number (starting at 0). * @return array|bool An array of links to comments, or false. */ public function append_comments($allcomments) { if (!$this->filename) { return false; } $this->SetFontSize(12 * $this->scale); $this->SetMargins(100 * $this->scale, 120 * $this->scale, -1, true); $this->SetAutoPageBreak(true, 100 * $this->scale); $this->setHeaderFont(array($this->get_export_font_name(), '', 24 * $this->scale, '', true)); $this->setHeaderMargin(24 * $this->scale); $this->setHeaderData('', 0, '', get_string('commentindex', 'assignfeedback_editpdf')); // Add a new page to the document with an appropriate header. $this->setPrintHeader(true); $this->AddPage(); // Add the comments. $commentlinks = array(); foreach ($allcomments as $pageno => $comments) { foreach ($comments as $index => $comment) { // Create a link to the current location, which will be added to the marker. $commentlink = $this->AddLink(); $this->SetLink($commentlink, -1); $commentlinks[$pageno][$index] = $commentlink; // Also create a link back to the marker, which will be added here. $markerlink = $this->AddLink(); $this->SetLink($markerlink, $comment->y * $this->scale, $pageno + 1); $label = get_string('commentlabel', 'assignfeedback_editpdf', array('pnum' => $pageno + 1, 'cnum' => $index + 1)); $this->Cell(50 * $this->scale, 0, $label, 0, 0, '', false, $markerlink); $this->MultiCell(0, 0, $comment->rawtext, 0, 'L'); $this->Ln(12 * $this->scale); } // Add an extra line break between pages. $this->Ln(12 * $this->scale); } return $commentlinks; } /** * Add a comment marker to the specified page. * * @param int $pageno The page number to add markers to (starting at 0). * @param int $index The comment index. * @param int $x The x-coordinate of the marker (in pixels). * @param int $y The y-coordinate of the marker (in pixels). * @param int $link The link identifier pointing to the full comment text. * @param string $colour The fill colour of the marker (red, yellow, green, blue, white, clear). * @return bool Success status. */ public function add_comment_marker($pageno, $index, $x, $y, $link, $colour = 'yellow') { if (!$this->filename) { return false; } $fill = ''; $fillopacity = 0.9; switch ($colour) { case 'red': $fill = 'rgb(249, 181, 179)'; break; case 'green': $fill = 'rgb(214, 234, 178)'; break; case 'blue': $fill = 'rgb(203, 217, 237)'; break; case 'white': $fill = 'rgb(255, 255, 255)'; break; case 'clear': $fillopacity = 0; break; default: /* Yellow */ $fill = 'rgb(255, 236, 174)'; } $marker = '@' . ''; $label = get_string('commentlabel', 'assignfeedback_editpdf', array('pnum' => $pageno + 1, 'cnum' => $index + 1)); $x *= $this->scale; $y *= $this->scale; $size = 24 * $this->scale; $this->SetDrawColor(51, 51, 51); $this->SetFontSize(10 * $this->scale); $this->setPage($pageno + 1); // Add the marker image. $this->ImageSVG($marker, $x - 0.5, $y - 0.5, $size, $size, $link); // Add the label. $this->MultiCell($size * 0.95, 0, $label, 0, 'C', false, 1, $x, $y, true, 0, false, true, $size * 0.60, 'M', true); return true; } /** * Add a comment to the current page * @param string $text the text of the comment * @param int $x the x-coordinate of the comment (in pixels) * @param int $y the y-coordinate of the comment (in pixels) * @param int $width the width of the comment (in pixels) * @param string $colour optional the background colour of the comment (red, yellow, green, blue, white, clear) * @return bool true if successful (always) */ public function add_comment($text, $x, $y, $width, $colour = 'yellow') { if (!$this->filename) { return false; } $this->SetDrawColor(51, 51, 51); switch ($colour) { case 'red': $this->SetFillColor(249, 181, 179); break; case 'green': $this->SetFillColor(214, 234, 178); break; case 'blue': $this->SetFillColor(203, 217, 237); break; case 'white': $this->SetFillColor(255, 255, 255); break; default: /* Yellow */ $this->SetFillColor(255, 236, 174); break; } $x *= $this->scale; $y *= $this->scale; $width *= $this->scale; $text = str_replace('<', '<', $text); $text = str_replace('>', '>', $text); // Draw the text with a border, but no background colour (using a background colour would cause the fill to // appear behind any existing content on the page, hence the extra filled rectangle drawn below). $this->MultiCell($width, 1.0, $text, 0, 'L', 0, 4, $x, $y); /* width, height, text, border, justify, fill, ln, x, y */ if ($colour != 'clear') { $newy = $this->GetY(); // Now we know the final size of the comment, draw a rectangle with the background colour. $this->Rect($x, $y, $width, $newy - $y, 'DF'); // Re-draw the text over the top of the background rectangle. $this->MultiCell($width, 1.0, $text, 0, 'L', 0, 4, $x, $y); /* width, height, text, border, justify, fill, ln, x, y */ } return true; } /** * Add an annotation to the current page * @param int $sx starting x-coordinate (in pixels) * @param int $sy starting y-coordinate (in pixels) * @param int $ex ending x-coordinate (in pixels) * @param int $ey ending y-coordinate (in pixels) * @param string $colour optional the colour of the annotation (red, yellow, green, blue, white, black) * @param string $type optional the type of annotation (line, oval, rectangle, highlight, pen, stamp) * @param int[]|string $path optional for 'pen' annotations this is an array of x and y coordinates for * the line, for 'stamp' annotations it is the name of the stamp file (without the path) * @param string $imagefolder - Folder containing stamp images. * @return bool true if successful (always) */ public function add_annotation($sx, $sy, $ex, $ey, $colour, $type, $path, $imagefolder) { global $CFG; if (!$this->filename) { return false; } switch ($colour) { case 'yellow': $colourarray = array(255, 207, 53); break; case 'green': $colourarray = array(153, 202, 62); break; case 'blue': $colourarray = array(125, 159, 211); break; case 'white': $colourarray = array(255, 255, 255); break; case 'black': $colourarray = array(51, 51, 51); break; default: /* Red */ $colour = 'red'; $colourarray = array(239, 69, 64); break; } $this->SetDrawColorArray($colourarray); $sx *= $this->scale; $sy *= $this->scale; $ex *= $this->scale; $ey *= $this->scale; $this->SetLineWidth(3.0 * $this->scale); switch ($type) { case 'oval': $rx = abs($sx - $ex) / 2; $ry = abs($sy - $ey) / 2; $sx = min($sx, $ex) + $rx; $sy = min($sy, $ey) + $ry; // $rx and $ry should be >= min width and height if ($rx < self::MIN_ANNOTATION_WIDTH) { $rx = self::MIN_ANNOTATION_WIDTH; } if ($ry < self::MIN_ANNOTATION_HEIGHT) { $ry = self::MIN_ANNOTATION_HEIGHT; } $this->Ellipse($sx, $sy, $rx, $ry); break; case 'rectangle': $w = abs($sx - $ex); $h = abs($sy - $ey); $sx = min($sx, $ex); $sy = min($sy, $ey); // Width or height should be >= min width and height if ($w < self::MIN_ANNOTATION_WIDTH) { $w = self::MIN_ANNOTATION_WIDTH; } if ($h < self::MIN_ANNOTATION_HEIGHT) { $h = self::MIN_ANNOTATION_HEIGHT; } $this->Rect($sx, $sy, $w, $h); break; case 'highlight': $w = abs($sx - $ex); $h = 8.0 * $this->scale; $sx = min($sx, $ex); $sy = min($sy, $ey) + ($h * 0.5); $this->SetAlpha(0.5, 'Normal', 0.5, 'Normal'); $this->SetLineWidth(8.0 * $this->scale); // width should be >= min width if ($w < self::MIN_ANNOTATION_WIDTH) { $w = self::MIN_ANNOTATION_WIDTH; } $this->Rect($sx, $sy, $w, $h); $this->SetAlpha(1.0, 'Normal', 1.0, 'Normal'); break; case 'pen': if ($path) { $scalepath = array(); $points = preg_split('/[,:]/', $path); foreach ($points as $point) { $scalepath[] = intval($point) * $this->scale; } if (!empty($scalepath)) { $this->PolyLine($scalepath, 'S'); } } break; case 'stamp': $imgfile = $imagefolder . '/' . clean_filename($path); $w = abs($sx - $ex); $h = abs($sy - $ey); $sx = min($sx, $ex); $sy = min($sy, $ey); // Stamp is always more than 40px, so no need to check width/height. $this->Image($imgfile, $sx, $sy, $w, $h); break; default: // Line. $this->Line($sx, $sy, $ex, $ey); break; } $this->SetDrawColor(0, 0, 0); $this->SetLineWidth(1.0 * $this->scale); return true; } /** * Save the completed PDF to the given file * @param string $filename the filename for the PDF (including the full path) */ public function save_pdf($filename) { $olddebug = error_reporting(0); $this->Output($filename, 'F'); error_reporting($olddebug); } /** * Set the path to the folder in which to generate page image files * @param string $folder */ public function set_image_folder($folder) { $this->imagefolder = $folder; } /** * Generate images from the PDF * @return array Array of filename of the generated images */ public function get_images(): array { $this->precheck_generate_image(); $imagefile = $this->imagefolder . '/' . self::IMAGE_PAGE; $command = $this->get_command_for_image(-1, $imagefile); exec($command); $images = array(); for ($i = 0; $i < $this->pagecount; $i++) { // Image file is created from 1, so need to change to 0. $file = $imagefile . ($i + 1) . '.png'; $newfile = $imagefile . $i . '.png'; if (file_exists($file)) { rename($file, $newfile); } else { // Converter added '-' and zerofill for the pagenumber. $length = strlen($this->pagecount); $file = $imagefile . '-' . str_pad(($i + 1), $length, '0', STR_PAD_LEFT) . '.png'; if (file_exists($file)) { rename($file, $newfile); } else { $newfile = self::get_error_image($this->imagefolder, $i); } } $images[$i] = basename($newfile); } return $images; } /** * Generate an image of the specified page in the PDF * @param int $pageno the page to generate the image of * @throws \moodle_exception * @throws \coding_exception * @return string the filename of the generated image */ public function get_image($pageno) { $this->precheck_generate_image(); $imagefile = $this->imagefolder . '/' . self::IMAGE_PAGE . $pageno . '.png'; $generate = true; if (file_exists($imagefile)) { if (filemtime($imagefile) > filemtime($this->filename)) { // Make sure the image is newer than the PDF file. $generate = false; } } if ($generate) { $command = $this->get_command_for_image($pageno, $imagefile); $output = null; $result = exec($command, $output); if (!file_exists($imagefile)) { $fullerror = '

'.get_string('command', 'assignfeedback_editpdf')."\n";
                $fullerror .= $command . "\n\n";
                $fullerror .= get_string('result', 'assignfeedback_editpdf')."\n";
                $fullerror .= htmlspecialchars($result, ENT_COMPAT) . "\n\n";
                $fullerror .= get_string('output', 'assignfeedback_editpdf')."\n";
                $fullerror .= htmlspecialchars(implode("\n", $output), ENT_COMPAT) . '
'; throw new \moodle_exception('errorgenerateimage', 'assignfeedback_editpdf', '', $fullerror); } } return self::IMAGE_PAGE . $pageno . '.png'; } /** * Make sure the file name and image folder are ready before generate image. * @return bool */ protected function precheck_generate_image() { if (!$this->filename) { throw new \coding_exception('Attempting to generate a page image without first setting the PDF filename'); } if (!$this->imagefolder) { throw new \coding_exception('Attempting to generate a page image without first specifying the image output folder'); } if (!is_dir($this->imagefolder)) { throw new \coding_exception('The specified image output folder is not a valid folder'); } return true; } /** * Gets the command to use to extract as image the given $pageno page number * from a PDF document into the $imagefile file. * @param int $pageno Page number to extract from document. -1 means for all pages. * @param string $imagefile Target filename for the PNG image as absolute path. * @return string The command to use to extract a page as PNG image. */ private function get_command_for_image(int $pageno, string $imagefile): string { global $CFG; // First, quickest convertion option. if (!empty($CFG->pathtopdftoppm) && is_executable($CFG->pathtopdftoppm)) { return $this->get_pdftoppm_command_for_image($pageno, $imagefile); } // Otherwise, rely on default behaviour. return $this->get_gs_command_for_image($pageno, $imagefile); } /** * Gets the pdftoppm command to use to extract as image the given $pageno page number * from a PDF document into the $imagefile file. * @param int $pageno Page number to extract from document. -1 means for all pages. * @param string $imagefile Target filename for the PNG image as absolute path. * @return string The pdftoppm command to use to extract a page as PNG image. */ private function get_pdftoppm_command_for_image(int $pageno, string $imagefile): string { global $CFG; $pdftoppmexec = \escapeshellarg($CFG->pathtopdftoppm); $imageres = \escapeshellarg(100); $filename = \escapeshellarg($this->filename); $pagenoinc = \escapeshellarg($pageno + 1); if ($pageno >= 0) { // Convert 1 page. $imagefile = substr($imagefile, 0, -4); // Pdftoppm tool automatically adds extension file. $frompageno = $pagenoinc; $topageno = $pagenoinc; $singlefile = '-singlefile'; } else { // Convert all pages at once. $frompageno = 1; $topageno = $this->pagecount; $singlefile = ''; } $imagefilearg = \escapeshellarg($imagefile); return "$pdftoppmexec -q -r $imageres -f $frompageno -l $topageno -png $singlefile $filename $imagefilearg"; } /** * Gets the ghostscript (gs) command to use to extract as image the given $pageno page number * from a PDF document into the $imagefile file. * @param int $pageno Page number to extract from document. -1 means for all pages. * @param string $imagefile Target filename for the PNG image as absolute path. * @return string The ghostscript (gs) command to use to extract a page as PNG image. */ private function get_gs_command_for_image(int $pageno, string $imagefile): string { global $CFG; $gsexec = \escapeshellarg($CFG->pathtogs); $imageres = \escapeshellarg(100); $imagefilearg = \escapeshellarg($imagefile); $filename = \escapeshellarg($this->filename); $pagenoinc = \escapeshellarg($pageno + 1); if ($pageno >= 0) { // Convert 1 page. $firstpage = $pagenoinc; $lastpage = $pagenoinc; } else { // Convert all pages at once. $imagefilearg = \escapeshellarg($imagefile . '%d.png'); $firstpage = 1; $lastpage = $this->pagecount; } return "$gsexec -q -sDEVICE=png16m -dSAFER -dBATCH -dNOPAUSE -r$imageres -dFirstPage=$firstpage -dLastPage=$lastpage ". "-dDOINTERPOLATE -dGraphicsAlphaBits=4 -dTextAlphaBits=4 -sOutputFile=$imagefilearg $filename"; } /** * Check to see if PDF is version 1.4 (or below); if not: use ghostscript to convert it * * @param \stored_file $file * @return string path to copy or converted pdf (false == fail) */ public static function ensure_pdf_compatible(\stored_file $file) { global $CFG; // Copy the stored_file to local disk for checking. $temparea = make_request_directory(); $tempsrc = $temparea . "/source.pdf"; $file->copy_content_to($tempsrc); return self::ensure_pdf_file_compatible($tempsrc); } /** * Flatten and convert file using ghostscript then load pdf. * * @param string $tempsrc The path to the file on disk. * @return string path to copy or converted pdf (false == fail) */ public static function ensure_pdf_file_compatible($tempsrc) { global $CFG; $temparea = make_request_directory(); $tempdst = $temparea . "/target.pdf"; $gsexec = \escapeshellarg($CFG->pathtogs); $tempdstarg = \escapeshellarg($tempdst); $tempsrcarg = \escapeshellarg($tempsrc); $command = "$gsexec -q -sDEVICE=pdfwrite -dPreserveAnnots=false -dSAFER -dBATCH -dNOPAUSE -dCompatibilityLevel=1.4 " . "-sOutputFile=$tempdstarg $tempsrcarg"; exec($command); if (!file_exists($tempdst)) { // Something has gone wrong in the conversion. return false; } $pdf = new pdf(); $pagecount = 0; try { $pagecount = $pdf->load_pdf($tempdst); } catch (\Exception $e) { // PDF was not valid - try running it through ghostscript to clean it up. $pagecount = 0; } $pdf->Close(); // PDF loaded and never saved/outputted needs to be closed. if ($pagecount <= 0) { // Could not parse the converted pdf. return false; } return $tempdst; } /** * Generate an localised error image for the given pagenumber. * * @param string $errorimagefolder path of the folder where error image needs to be created. * @param int $pageno page number for which error image needs to be created. * * @return string File name * @throws \coding_exception */ public static function get_error_image($errorimagefolder, $pageno) { global $CFG; $errorfile = $CFG->dirroot . self::BLANK_PDF; if (!file_exists($errorfile)) { throw new \coding_exception("Blank PDF not found", "File path" . $errorfile); } $tmperrorimagefolder = make_request_directory(); $pdf = new pdf(); $pdf->set_pdf($errorfile); $pdf->copy_page(); $pdf->add_comment(get_string('errorpdfpage', 'assignfeedback_editpdf'), 250, 300, 200, "red"); $generatedpdf = $tmperrorimagefolder . '/' . 'error.pdf'; $pdf->save_pdf($generatedpdf); $pdf = new pdf(); $pdf->set_pdf($generatedpdf); $pdf->set_image_folder($tmperrorimagefolder); $image = $pdf->get_image(0); $pdf->Close(); // PDF loaded and never saved/outputted needs to be closed. $newimg = self::IMAGE_PAGE . $pageno . '.png'; copy($tmperrorimagefolder . '/' . $image, $errorimagefolder . '/' . $newimg); return $newimg; } /** * Test that the configured path to ghostscript is correct and working. * @param bool $generateimage - If true - a test image will be generated to verify the install. * @return \stdClass */ public static function test_gs_path($generateimage = true) { global $CFG; $ret = (object)array( 'status' => self::GSPATH_OK, 'message' => null, ); $gspath = $CFG->pathtogs; if (empty($gspath)) { $ret->status = self::GSPATH_EMPTY; return $ret; } if (!file_exists($gspath)) { $ret->status = self::GSPATH_DOESNOTEXIST; return $ret; } if (is_dir($gspath)) { $ret->status = self::GSPATH_ISDIR; return $ret; } if (!is_executable($gspath)) { $ret->status = self::GSPATH_NOTEXECUTABLE; return $ret; } if (!$generateimage) { return $ret; } $testfile = $CFG->dirroot.'/mod/assign/feedback/editpdf/tests/fixtures/testgs.pdf'; if (!file_exists($testfile)) { $ret->status = self::GSPATH_NOTESTFILE; return $ret; } $testimagefolder = \make_temp_directory('assignfeedback_editpdf_test'); $filepath = $testimagefolder . '/' . self::IMAGE_PAGE . '0.png'; // Delete any previous test images, if they exist. if (file_exists($filepath)) { unlink($filepath); } $pdf = new pdf(); $pdf->set_pdf($testfile); $pdf->set_image_folder($testimagefolder); try { $pdf->get_image(0); } catch (\moodle_exception $e) { $ret->status = self::GSPATH_ERROR; $ret->message = $e->getMessage(); } $pdf->Close(); // PDF loaded and never saved/outputted needs to be closed. return $ret; } /** * If the test image has been generated correctly - send it direct to the browser. */ public static function send_test_image() { global $CFG; header('Content-type: image/png'); require_once($CFG->libdir.'/filelib.php'); $testimagefolder = \make_temp_directory('assignfeedback_editpdf_test'); $testimage = $testimagefolder . '/' . self::IMAGE_PAGE . '0.png'; send_file($testimage, basename($testimage), 0); die(); } /** * This function add an image file to PDF page. * @param \stored_file $imagestoredfile Image file to be added */ public function add_image_page($imagestoredfile) { $imageinfo = $imagestoredfile->get_imageinfo(); $imagecontent = $imagestoredfile->get_content(); $this->currentpage++; $template = $this->importPage($this->currentpage); $size = $this->getTemplateSize($template); $orientation = 'P'; if ($imageinfo["width"] > $imageinfo["height"]) { if ($size['width'] < $size['height']) { $temp = $size['width']; $size['width'] = $size['height']; $size['height'] = $temp; } $orientation = 'L'; } else if ($imageinfo["width"] < $imageinfo["height"]) { if ($size['width'] > $size['height']) { $temp = $size['width']; $size['width'] = $size['height']; $size['height'] = $temp; } } $this->SetHeaderMargin(0); $this->SetFooterMargin(0); $this->SetMargins(0, 0, 0, true); $this->setPrintFooter(false); $this->setPrintHeader(false); $this->AddPage($orientation, $size); $this->SetAutoPageBreak(false, 0); $this->Image('@' . $imagecontent, 0, 0, $size['width'], $size['height'], '', '', '', false, null, '', false, false, 0); } } feedback/editpdf/classes/combined_document.php000064400000033147152265517350015556 0ustar00. /** * This file contains the combined document class for the assignfeedback_editpdf plugin. * * @package assignfeedback_editpdf * @copyright 2017 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace assignfeedback_editpdf; defined('MOODLE_INTERNAL') || die(); /** * The combined_document class for the assignfeedback_editpdf plugin. * * @copyright 2017 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class combined_document { /** * Status value representing a conversion waiting to start. */ const STATUS_PENDING_INPUT = 0; /** * Status value representing all documents ready to be combined. */ const STATUS_READY = 1; /** * Status value representing all documents are ready to be combined as are supported. */ const STATUS_READY_PARTIAL = 3; /** * Status value representing a successful conversion. */ const STATUS_COMPLETE = 2; /** * Status value representing a permanent error. */ const STATUS_FAILED = -1; /** * The list of files which make this document. */ protected $sourcefiles = []; /** * The resultant combined file. */ protected $combinedfile; /** * The combination status. */ protected $combinationstatus = null; /** * The number of pages in the combined PDF. */ protected $pagecount = 0; /** * Check the current status of the document combination. * Note that the combined document may not contain all the source files if some of the * source files were not able to be converted. An example is an audio file with a pdf cover sheet. Only * the cover sheet will be included in the combined document. * * @return int */ public function get_status() { if ($this->combinedfile) { // The combined file exists. Report success. return self::STATUS_COMPLETE; } if (empty($this->sourcefiles)) { // There are no source files to combine. return self::STATUS_FAILED; } if (!empty($this->combinationstatus)) { // The combination is in progress and has set a status. // Return it instead. return $this->combinationstatus; } $pending = false; $partial = false; foreach ($this->sourcefiles as $file) { // The combined file has not yet been generated. // Check the status of each source file. if (is_a($file, \core_files\conversion::class)) { $status = $file->get('status'); switch ($status) { case \core_files\conversion::STATUS_IN_PROGRESS: case \core_files\conversion::STATUS_PENDING: $pending = true; break; // There are 4 status flags, so the only remaining one is complete which is fine. case \core_files\conversion::STATUS_FAILED: $partial = true; break; } } } if ($pending) { return self::STATUS_PENDING_INPUT; } else { if ($partial) { return self::STATUS_READY_PARTIAL; } return self::STATUS_READY; } } /** * Set the completed combined file. * * @param \stored_file $file The completed document for all files to be combined. * @return $this */ public function set_combined_file($file) { $this->combinedfile = $file; return $this; } /** * Return true of the combined file contained only some of the submission files. * * @return boolean */ public function is_partial_conversion() { $combinedfile = $this->get_combined_file(); if (empty($combinedfile)) { return false; } $filearea = $combinedfile->get_filearea(); return $filearea == document_services::PARTIAL_PDF_FILEAREA; } /** * Retrieve the completed combined file. * * @return stored_file */ public function get_combined_file() { return $this->combinedfile; } /** * Set all source files which are to be combined. * * @param \stored_file|conversion[] $files The complete list of all source files to be combined. * @return $this */ public function set_source_files($files) { $this->sourcefiles = $files; return $this; } /** * Add an additional source file to the end of the existing list. * * @param \stored_file|conversion $file The file to add to the end of the list. * @return $this */ public function add_source_file($file) { $this->sourcefiles[] = $file; return $this; } /** * Retrieve the complete list of source files. * * @return stored_file|conversion[] */ public function get_source_files() { return $this->sourcefiles; } /** * Refresh the files. * * This includes polling any pending conversions to see if they are complete. * * @return $this */ public function refresh_files() { $converter = new \core_files\converter(); foreach ($this->sourcefiles as $file) { if (is_a($file, \core_files\conversion::class)) { $status = $file->get('status'); switch ($status) { case \core_files\conversion::STATUS_COMPLETE: continue 2; break; default: $converter->poll_conversion($conversion); } } } return $this; } /** * Combine all source files into a single PDF and store it in the * file_storage API using the supplied contextid and itemid. * * @param int $contextid The contextid for the file to be stored under * @param int $itemid The itemid for the file to be stored under * @return $this */ public function combine_files($contextid, $itemid) { global $CFG; $currentstatus = $this->get_status(); $readystatuslist = [self::STATUS_READY, self::STATUS_READY_PARTIAL]; if ($currentstatus === self::STATUS_FAILED) { $this->store_empty_document($contextid, $itemid); return $this; } else if (!in_array($currentstatus, $readystatuslist)) { // The document is either: // * already combined; or // * pending input being fully converted; or // * unable to continue due to an issue with the input documents. // // Exit early as we cannot continue. return $this; } require_once($CFG->libdir . '/pdflib.php'); $pdf = new pdf(); $files = $this->get_source_files(); $compatiblepdfs = []; foreach ($files as $file) { // Check that each file is compatible and add it to the list. // Note: We drop non-compatible files. $compatiblepdf = false; if (is_a($file, \core_files\conversion::class)) { $status = $file->get('status'); if ($status == \core_files\conversion::STATUS_COMPLETE) { $compatiblepdf = pdf::ensure_pdf_compatible($file->get_destfile()); } } else { $compatiblepdf = pdf::ensure_pdf_compatible($file); } if ($compatiblepdf) { $compatiblepdfs[] = $compatiblepdf; } } $tmpdir = make_request_directory(); $tmpfile = $tmpdir . '/' . document_services::COMBINED_PDF_FILENAME; try { $pagecount = $pdf->combine_pdfs($compatiblepdfs, $tmpfile); $pdf->Close(); } catch (\Exception $e) { // Unable to combine the PDF. debugging('TCPDF could not process the pdf files:' . $e->getMessage(), DEBUG_DEVELOPER); $pdf->Close(); return $this->mark_combination_failed(); } // Verify the PDF. $verifypdf = new pdf(); $verifypagecount = $verifypdf->load_pdf($tmpfile); $verifypdf->Close(); if ($verifypagecount <= 0) { // No pages were found in the combined PDF. return $this->mark_combination_failed(); } // Store the newly created file as a stored_file. $this->store_combined_file($tmpfile, $contextid, $itemid, ($currentstatus == self::STATUS_READY_PARTIAL)); // Note the verified page count. $this->pagecount = $verifypagecount; return $this; } /** * Mark the combination attempt as having encountered a permanent failure. * * @return $this */ protected function mark_combination_failed() { $this->combinationstatus = self::STATUS_FAILED; return $this; } /** * Store the combined file in the file_storage API. * * @param string $tmpfile The path to the file on disk to be stored. * @param int $contextid The contextid for the file to be stored under * @param int $itemid The itemid for the file to be stored under * @param boolean $partial The combined pdf contains only some of the source files. * @return $this */ protected function store_combined_file($tmpfile, $contextid, $itemid, $partial = false) { // Store the file. $record = $this->get_stored_file_record($contextid, $itemid, $partial); $fs = get_file_storage(); // Delete existing files first. $fs->delete_area_files($record->contextid, $record->component, $record->filearea, $record->itemid); // This was a combined pdf. $file = $fs->create_file_from_pathname($record, $tmpfile); $this->set_combined_file($file); return $this; } /** * Store the empty document file in the file_storage API. * * @param int $contextid The contextid for the file to be stored under * @param int $itemid The itemid for the file to be stored under * @return $this */ protected function store_empty_document($contextid, $itemid) { // Store the file. $record = $this->get_stored_file_record($contextid, $itemid); $fs = get_file_storage(); // Delete existing files first. $fs->delete_area_files($record->contextid, $record->component, $record->filearea, $record->itemid); $file = $fs->create_file_from_string($record, base64_decode(document_services::BLANK_PDF_BASE64)); $this->pagecount = 1; $this->set_combined_file($file); return $this; } /** * Get the total number of pages in the combined document. * * If there are no pages, or it is not yet possible to count them a * value of 0 is returned. * * @return int */ public function get_page_count() { if ($this->pagecount) { return $this->pagecount; } $status = $this->get_status(); if ($status === self::STATUS_FAILED) { // The empty document will be returned. return 1; } if ($status !== self::STATUS_COMPLETE) { // No pages yet. return 0; } // Load the PDF to determine the page count. $temparea = make_request_directory(); $tempsrc = $temparea . "/source.pdf"; $this->get_combined_file()->copy_content_to($tempsrc); $pdf = new pdf(); $pagecount = $pdf->load_pdf($tempsrc); $pdf->Close(); if ($pagecount <= 0) { // Something went wrong. Return an empty page count again. return 0; } $this->pagecount = $pagecount; return $this->pagecount; } /** * Get the total number of documents to be combined. * * @return int */ public function get_document_count() { return count($this->sourcefiles); } /** * Helper to fetch the stored_file record. * * @param int $contextid The contextid for the file to be stored under * @param int $itemid The itemid for the file to be stored under * @param boolean $partial The combined file contains only some of the source files. * @return stdClass */ protected function get_stored_file_record($contextid, $itemid, $partial = false) { $filearea = document_services::COMBINED_PDF_FILEAREA; if ($partial) { $filearea = document_services::PARTIAL_PDF_FILEAREA; } return (object) [ 'contextid' => $contextid, 'component' => 'assignfeedback_editpdf', 'filearea' => $filearea, 'itemid' => $itemid, 'filepath' => '/', 'filename' => document_services::COMBINED_PDF_FILENAME, ]; } } feedback/editpdf/classes/annotation.php000064400000005414152265517350014246 0ustar00. /** * This file contains the annotation class for the assignfeedback_editpdf plugin * * @package assignfeedback_editpdf * @copyright 2012 Davo Smith * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace assignfeedback_editpdf; /** * This class adds and removes annotations from a page of a response. * * @package assignfeedback_editpdf * @copyright 2012 Davo Smith * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class annotation { /** @var int unique id for this annotation */ public $id = 0; /** @var int gradeid for this annotation */ public $gradeid = 0; /** @var int page number for this annotation */ public $pageno = 0; /** @var int starting location in pixels. Image resolution is 100 pixels per inch */ public $x = 0; /** @var int ending location in pixels. Image resolution is 100 pixels per inch */ public $endx = 0; /** @var int starting location in pixels. Image resolution is 100 pixels per inch */ public $y = 0; /** @var int ending location in pixels. Image resolution is 100 pixels per inch */ public $endy = 0; /** @var string path information for drawing the annotation. */ public $path = ''; /** @var string colour - One of red, yellow, green, blue, white */ public $colour = 'yellow'; /** @var string type - One of line, oval, rect, etc */ public $type = 'line'; /** @var int draft status, default 1 = true */ public $draft = 1; /** * Convert a compatible stdClass into an instance of this class. * @param \stdClass $record */ public function __construct(?\stdClass $record = null) { if ($record) { $intcols = array('endx', 'endy', 'x', 'y'); foreach ($this as $key => $value) { if (isset($record->$key)) { if (in_array($key, $intcols)) { $this->$key = intval($record->$key); } else { $this->$key = $record->$key; } } } } } } feedback/editpdf/classes/comment.php000064400000004712152265517350013536 0ustar00. /** * This file contains the comment class for the assignfeedback_editpdf plugin * * @package assignfeedback_editpdf * @copyright 2012 Davo Smith * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace assignfeedback_editpdf; /** * This class represents a comment box on a page of feedback. * @copyright 2012 Davo Smith * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class comment { /** @var int unique id for this annotation */ public $id = 0; /** @var int gradeid for this annotation */ public $gradeid = 0; /** @var int page number for this annotation */ public $pageno = 0; /** @var int starting location in pixels. Image resolution is 100 pixels per inch */ public $x = 0; /** @var int starting location in pixels. Image resolution is 100 pixels per inch */ public $y = 0; /** @var int width of the comment box */ public $width = 120; /** @var string The comment text. */ public $rawtext = ''; /** @var string colour - One of red, yellow, green, blue, white */ public $colour = 'yellow'; /** @var int draft status, default 1 = true */ public $draft = 1; /** * Convert a compatible stdClass into an instance of a comment. * @param \stdClass $record */ public function __construct(?\stdClass $record = null) { if ($record) { $intcols = array('width', 'x', 'y'); foreach ($this as $key => $value) { if (isset($record->$key)) { if (in_array($key, $intcols)) { $this->$key = intval($record->$key); } else { $this->$key = $record->$key; } } } } } } feedback/editpdf/classes/event/observer.php000064400000004627152265517350015051 0ustar00. /** * An event observer. * * @package assignfeedback_editpdf * @copyright 2016 Damyon Wiese * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace assignfeedback_editpdf\event; /** * An event observer. * @copyright 2016 Damyon Wiese * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class observer { /** * Listen to events and queue the submission for processing. * @param \mod_assign\event\submission_created $event */ public static function submission_created(\mod_assign\event\submission_created $event) { self::queue_conversion($event); } /** * Listen to events and queue the submission for processing. * @param \mod_assign\event\submission_updated $event */ public static function submission_updated(\mod_assign\event\submission_updated $event) { self::queue_conversion($event); } /** * Queue the submission for processing. * @param \mod_assign\event\base $event The submission created/updated event. */ protected static function queue_conversion($event) { $assign = $event->get_assign(); $plugin = $assign->get_feedback_plugin_by_type('editpdf'); if (!$plugin->is_visible() || !$plugin->is_enabled()) { // The plugin is not enabled on this assignment instance, so nothing should be queued. return; } $data = [ 'submissionid' => $event->other['submissionid'], 'submissionattempt' => $event->other['submissionattempt'], ]; $task = new \assignfeedback_editpdf\task\convert_submission; $task->set_custom_data($data); \core\task\manager::queue_adhoc_task($task, true); } } feedback/editpdf/classes/document_services.php000064400000134630152265517350015620 0ustar00. /** * This file contains the ingest manager for the assignfeedback_editpdf plugin * * @package assignfeedback_editpdf * @copyright 2012 Davo Smith * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace assignfeedback_editpdf; use DOMDocument; /** * Functions for generating the annotated pdf. * * This class controls the ingest of student submission files to a normalised * PDF 1.4 document with all submission files concatinated together. It also * provides the functions to generate a downloadable pdf with all comments and * annotations embedded. * @copyright 2012 Davo Smith * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class document_services { /** Compoment name */ const COMPONENT = "assignfeedback_editpdf"; /** File area for generated pdf */ const FINAL_PDF_FILEAREA = 'download'; /** File area for combined pdf */ const COMBINED_PDF_FILEAREA = 'combined'; /** File area for partial combined pdf */ const PARTIAL_PDF_FILEAREA = 'partial'; /** File area for importing html */ const IMPORT_HTML_FILEAREA = 'importhtml'; /** File area for page images */ const PAGE_IMAGE_FILEAREA = 'pages'; /** File area for readonly page images */ const PAGE_IMAGE_READONLY_FILEAREA = 'readonlypages'; /** File area for the stamps */ const STAMPS_FILEAREA = 'stamps'; /** Filename for combined pdf */ const COMBINED_PDF_FILENAME = 'combined.pdf'; /** Temporary place to save JPG Image to PDF file */ const TMP_JPG_TO_PDF_FILEAREA = 'tmp_jpg_to_pdf'; /** Temporary place to save (Automatically) Rotated JPG FILE */ const TMP_ROTATED_JPG_FILEAREA = 'tmp_rotated_jpg'; /** Hash of blank pdf */ const BLANK_PDF_HASH = '4c803c92c71f21b423d13de570c8a09e0a31c718'; /** Base64 encoded blank pdf. This is the most reliable/fastest way to generate a blank pdf. */ const BLANK_PDF_BASE64 = <<dirroot . '/mod/assign/locallib.php'); if (!is_object($assignment)) { $cm = get_coursemodule_from_instance('assign', $assignment, 0, false, MUST_EXIST); $context = \context_module::instance($cm->id); $assignment = new \assign($context, null, null); } return $assignment; } /** * Get a hash that will be unique and can be used in a path name. * @param int|\assign $assignment * @param int $userid * @param int $attemptnumber (-1 means latest attempt) */ private static function hash($assignment, $userid, $attemptnumber) { if (is_object($assignment)) { $assignmentid = $assignment->get_instance()->id; } else { $assignmentid = $assignment; } return sha1($assignmentid . '_' . $userid . '_' . $attemptnumber); } /** * Use a DOM parser to accurately replace images with their alt text. * @param string $html * @return string New html with no image tags. */ protected static function strip_images($html) { // Load HTML and suppress any parsing errors (DOMDocument->loadHTML() does not current support HTML5 tags). $dom = new DOMDocument(); libxml_use_internal_errors(true); $dom->loadHTML('' . $html); libxml_clear_errors(); // Find all img tags. if ($imgnodes = $dom->getElementsByTagName('img')) { // Replace img nodes with the img alt text without overriding DOM elements. for ($i = ($imgnodes->length - 1); $i >= 0; $i--) { $imgnode = $imgnodes->item($i); $alt = ($imgnode->hasAttribute('alt')) ? ' [ ' . $imgnode->getAttribute('alt') . ' ] ' : ' '; $textnode = $dom->createTextNode($alt); $imgnode->parentNode->replaceChild($textnode, $imgnode); } } $count = 1; return str_replace("", "", $dom->saveHTML(), $count); } /** * This function will search for all files that can be converted * and concatinated into a PDF (1.4) - for any submission plugin * for this students attempt. * * @param int|\assign $assignment * @param int $userid * @param int $attemptnumber (-1 means latest attempt) * @return combined_document */ protected static function list_compatible_submission_files_for_attempt($assignment, $userid, $attemptnumber) { global $USER, $DB; $assignment = self::get_assignment_from_param($assignment); // Capability checks. if (!$assignment->can_view_submission($userid)) { throw new \moodle_exception('nopermission'); } $files = array(); if ($assignment->get_instance()->teamsubmission) { $submission = $assignment->get_group_submission($userid, 0, false, $attemptnumber); } else { $submission = $assignment->get_user_submission($userid, false, $attemptnumber); } $user = $DB->get_record('user', array('id' => $userid)); // User has not submitted anything yet. if (!$submission) { return new combined_document(); } $fs = get_file_storage(); $converter = new \core_files\converter(); // Ask each plugin for it's list of files. foreach ($assignment->get_submission_plugins() as $plugin) { if ($plugin->is_enabled() && $plugin->is_visible()) { $pluginfiles = $plugin->get_files($submission, $user); foreach ($pluginfiles as $filename => $file) { if ($file instanceof \stored_file) { $mimetype = $file->get_mimetype(); // PDF File, no conversion required. if ($mimetype === 'application/pdf') { $files[$filename] = $file; } else if ($plugin->allow_image_conversion() && $mimetype === "image/jpeg") { // Rotates image based on the EXIF value. list ($rotateddata, $size) = $file->rotate_image(); if ($rotateddata) { $file = self::save_rotated_image_file($assignment, $userid, $attemptnumber, $rotateddata, $filename); } // Save as PDF file if there is no available converter. if (!$converter->can_convert_format_to('jpg', 'pdf')) { $pdffile = self::save_jpg_to_pdf($assignment, $userid, $attemptnumber, $file, $size); if ($pdffile) { $files[$filename] = $pdffile; } } } // The file has not been converted to PDF, try to convert it to PDF. if (!isset($files[$filename]) && $convertedfile = $converter->start_conversion($file, 'pdf')) { $files[$filename] = $convertedfile; } } else if ($converter->can_convert_format_to('html', 'pdf')) { // Create a tmp stored_file from this html string. $file = reset($file); // Strip image tags, because they will not be resolvable. $file = self::strip_images($file); $record = new \stdClass(); $record->contextid = $assignment->get_context()->id; $record->component = 'assignfeedback_editpdf'; $record->filearea = self::IMPORT_HTML_FILEAREA; $record->itemid = $submission->id; $record->filepath = '/'; $record->filename = $plugin->get_type() . '-' . $filename; $htmlfile = $fs->get_file($record->contextid, $record->component, $record->filearea, $record->itemid, $record->filepath, $record->filename); $newhash = sha1($file); // If the file exists, and the content hash doesn't match, remove it. if ($htmlfile && $newhash !== $htmlfile->get_contenthash()) { $htmlfile->delete(); $htmlfile = false; } // If the file doesn't exist, or if it was removed above, create a new one. if (!$htmlfile) { $htmlfile = $fs->create_file_from_string($record, $file); } $convertedfile = $converter->start_conversion($htmlfile, 'pdf'); if ($convertedfile) { $files[$filename] = $convertedfile; } } } } } $combineddocument = new combined_document(); $combineddocument->set_source_files($files); return $combineddocument; } /** * Fetch the current combined document ready for state checking. * * @param int|\assign $assignment * @param int $userid * @param int $attemptnumber (-1 means latest attempt) * @return combined_document */ public static function get_combined_document_for_attempt($assignment, $userid, $attemptnumber) { global $USER, $DB; $assignment = self::get_assignment_from_param($assignment); // Capability checks. if (!$assignment->can_view_submission($userid)) { throw new \moodle_exception('nopermissiontoaccesspage', 'error'); } $grade = $assignment->get_user_grade($userid, true, $attemptnumber); if ($assignment->get_instance()->teamsubmission) { $submission = $assignment->get_group_submission($userid, 0, false, $attemptnumber); } else { $submission = $assignment->get_user_submission($userid, false, $attemptnumber); } $contextid = $assignment->get_context()->id; $component = 'assignfeedback_editpdf'; $filearea = self::COMBINED_PDF_FILEAREA; $partialfilearea = self::PARTIAL_PDF_FILEAREA; $itemid = $grade->id; $filepath = '/'; $filename = self::COMBINED_PDF_FILENAME; $fs = get_file_storage(); $partialpdf = $fs->get_file($contextid, $component, $partialfilearea, $itemid, $filepath, $filename); if (!empty($partialpdf)) { $combinedpdf = $partialpdf; } else { $combinedpdf = $fs->get_file($contextid, $component, $filearea, $itemid, $filepath, $filename); } if ($combinedpdf && $submission) { if ($combinedpdf->get_timemodified() < $submission->timemodified) { // The submission has been updated since the PDF was generated. $combinedpdf = false; } else if ($combinedpdf->get_contenthash() == self::BLANK_PDF_HASH) { // The PDF is for a blank page. $combinedpdf = false; } } if (empty($combinedpdf)) { // The combined PDF does not exist yet. Return the list of files to be combined. return self::list_compatible_submission_files_for_attempt($assignment, $userid, $attemptnumber); } else { // The combined PDF aleady exists. Return it in a new combined_document object. $combineddocument = new combined_document(); return $combineddocument->set_combined_file($combinedpdf); } } /** * This function return the combined pdf for all valid submission files. * * @param int|\assign $assignment * @param int $userid * @param int $attemptnumber (-1 means latest attempt) * @return combined_document */ public static function get_combined_pdf_for_attempt($assignment, $userid, $attemptnumber) { $document = self::get_combined_document_for_attempt($assignment, $userid, $attemptnumber); if ($document->get_status() === combined_document::STATUS_COMPLETE) { // The combined document is already ready. return $document; } else { // Attempt to combined the files in the document. $grade = $assignment->get_user_grade($userid, true, $attemptnumber); $document->combine_files($assignment->get_context()->id, $grade->id); return $document; } } /** * This function will return the number of pages of a pdf. * * @param int|\assign $assignment * @param int $userid * @param int $attemptnumber (-1 means latest attempt) * @param bool $readonly When true we get the number of pages for the readonly version. * @return int number of pages */ public static function page_number_for_attempt($assignment, $userid, $attemptnumber, $readonly = false) { global $CFG; require_once($CFG->libdir . '/pdflib.php'); $assignment = self::get_assignment_from_param($assignment); if (!$assignment->can_view_submission($userid)) { throw new \moodle_exception('nopermission'); } // When in readonly we can return the number of images in the DB because they should already exist, // if for some reason they do not, then we proceed as for the normal version. if ($readonly) { $grade = $assignment->get_user_grade($userid, true, $attemptnumber); $fs = get_file_storage(); $files = $fs->get_directory_files($assignment->get_context()->id, 'assignfeedback_editpdf', self::PAGE_IMAGE_READONLY_FILEAREA, $grade->id, '/'); $pagecount = count($files); if ($pagecount > 0) { return $pagecount; } } // Get a combined pdf file from all submitted pdf files. $document = self::get_combined_pdf_for_attempt($assignment, $userid, $attemptnumber); return $document->get_page_count(); } /** * This function will generate and return a list of the page images from a pdf. * @param int|\assign $assignment * @param int $userid * @param int $attemptnumber (-1 means latest attempt) * @param bool $resetrotation check if need to reset page rotation information * @return array(stored_file) */ protected static function generate_page_images_for_attempt($assignment, $userid, $attemptnumber, $resetrotation = true) { global $CFG; require_once($CFG->libdir . '/pdflib.php'); $assignment = self::get_assignment_from_param($assignment); if (!$assignment->can_view_submission($userid)) { throw new \moodle_exception('nopermission'); } // Need to generate the page images - first get a combined pdf. $document = self::get_combined_pdf_for_attempt($assignment, $userid, $attemptnumber); $status = $document->get_status(); if ($status === combined_document::STATUS_FAILED) { throw new \moodle_exception('Could not generate combined pdf.'); } else if ($status === combined_document::STATUS_PENDING_INPUT) { // The conversion is still in progress. return []; } $tmpdir = \make_request_directory(); $combined = $tmpdir . '/' . self::COMBINED_PDF_FILENAME; $document->get_combined_file()->copy_content_to($combined); // Copy the file. $pdf = new pdf(); $pdf->set_image_folder($tmpdir); $pagecount = $pdf->set_pdf($combined); $grade = $assignment->get_user_grade($userid, true, $attemptnumber); $record = new \stdClass(); $record->contextid = $assignment->get_context()->id; $record->component = 'assignfeedback_editpdf'; $record->filearea = self::PAGE_IMAGE_FILEAREA; $record->itemid = $grade->id; $record->filepath = '/'; $fs = get_file_storage(); // Remove the existing content of the filearea. $fs->delete_area_files($record->contextid, $record->component, $record->filearea, $record->itemid); $files = array(); $images = $pdf->get_images(); for ($i = 0; $i < $pagecount; $i++) { try { if (empty($images[$i])) { throw new \moodle_exception('error image'); } $image = $images[$i]; if (!$resetrotation) { $pagerotation = page_editor::get_page_rotation($grade->id, $i); $degree = !empty($pagerotation) ? $pagerotation->degree : 0; if ($degree != 0) { $filepath = $tmpdir . '/' . $image; $imageresource = imagecreatefrompng($filepath); $content = imagerotate($imageresource, $degree, 0); imagepng($content, $filepath); } } } catch (\moodle_exception $e) { // We catch only moodle_exception here as other exceptions indicate issue with setup not the pdf. $image = pdf::get_error_image($tmpdir, $i); } $record->filename = basename($image); $files[$i] = $fs->create_file_from_pathname($record, $tmpdir . '/' . $image); @unlink($tmpdir . '/' . $image); // Set page rotation default value. if (!empty($files[$i])) { if ($resetrotation) { page_editor::set_page_rotation($grade->id, $i, false, $files[$i]->get_pathnamehash()); } } } $pdf->Close(); // PDF loaded and never saved/outputted needs to be closed. @unlink($combined); @rmdir($tmpdir); return $files; } /** * This function returns a list of the page images from a pdf. * * The readonly version is different than the normal one. The readonly version contains a copy * of the pages in the state they were when the PDF was annotated, by doing so we prevent the * the pages that are displayed to change as soon as the submission changes. * * Though there is an edge case, if the PDF was annotated before MDL-45580, then it is possible * that we do not find any readonly version of the pages. In that case, we will get the normal * pages and copy them to the readonly area. This ensures that the pages will remain in that * state until the submission is updated. When the normal files do not exist, we throw an exception * because the readonly pages should only ever be displayed after a teacher has annotated the PDF, * they would not exist until they do. * * @param int|\assign $assignment * @param int $userid * @param int $attemptnumber (-1 means latest attempt) * @param bool $readonly If true, then we are requesting the readonly version. * @return array(stored_file) */ public static function get_page_images_for_attempt($assignment, $userid, $attemptnumber, $readonly = false) { global $DB; $assignment = self::get_assignment_from_param($assignment); if (!$assignment->can_view_submission($userid)) { throw new \moodle_exception('nopermission'); } if ($assignment->get_instance()->teamsubmission) { $submission = $assignment->get_group_submission($userid, 0, false, $attemptnumber); } else { $submission = $assignment->get_user_submission($userid, false, $attemptnumber); } $grade = $assignment->get_user_grade($userid, true, $attemptnumber); $contextid = $assignment->get_context()->id; $component = 'assignfeedback_editpdf'; $itemid = $grade->id; $filepath = '/'; $filearea = self::PAGE_IMAGE_FILEAREA; $fs = get_file_storage(); // If we are after the readonly pages... if ($readonly) { $filearea = self::PAGE_IMAGE_READONLY_FILEAREA; if ($fs->is_area_empty($contextid, $component, $filearea, $itemid)) { // We have a problem here, we were supposed to find the files. // Attempt to re-generate the pages from the combined images. self::generate_page_images_for_attempt($assignment, $userid, $attemptnumber); self::copy_pages_to_readonly_area($assignment, $grade); } } $files = $fs->get_directory_files($contextid, $component, $filearea, $itemid, $filepath); $pages = array(); $resetrotation = false; if (!empty($files)) { $first = reset($files); $pagemodified = $first->get_timemodified(); // Check that we don't just have a single blank page. The hash of a blank page image can vary with // the version of ghostscript used, so we need to examine the combined pdf it was generated from. $blankpage = false; if (!$readonly && count($files) == 1) { $pdfarea = self::COMBINED_PDF_FILEAREA; $pdfname = self::COMBINED_PDF_FILENAME; if ($pdf = $fs->get_file($contextid, $component, $pdfarea, $itemid, $filepath, $pdfname)) { // The combined pdf may have a different hash if it has been regenerated since the page // image was created. However if this is the case the page image will be stale anyway. if ($pdf->get_contenthash() == self::BLANK_PDF_HASH || $pagemodified < $pdf->get_timemodified()) { $blankpage = true; } } } if (!$readonly && ($pagemodified < $submission->timemodified || $blankpage)) { // Image files are stale, we need to regenerate them, except in readonly mode. // We also need to remove the draft annotations and comments associated with this attempt. $fs->delete_area_files($contextid, $component, $filearea, $itemid); page_editor::delete_draft_content($itemid); $files = array(); $resetrotation = true; } else { // Need to reorder the files following their name. // because get_directory_files() return a different order than generate_page_images_for_attempt(). foreach ($files as $file) { // Extract the page number from the file name image_pageXXXX.png. preg_match('/page([\d]+)\./', $file->get_filename(), $matches); if (empty($matches) or !is_numeric($matches[1])) { throw new \coding_exception("'" . $file->get_filename() . "' file hasn't the expected format filename: image_pageXXXX.png."); } $pagenumber = (int)$matches[1]; // Save the page in the ordered array. $pages[$pagenumber] = $file; } ksort($pages); } } // This should never happen, there should be a version of the pages available // whenever we are requesting the readonly version. if (empty($pages) && $readonly) { throw new \moodle_exception('Could not find readonly pages for grade ' . $grade->id); } // There are two situations where the number of page images generated does not // match the number of pages in the PDF: // // 1. The document conversion adhoc task was interrupted somehow (node died, solar flare, etc) // 2. The submission has been updated by the student // // In the case of 1. we need to regenerate the pages, see MDL-66626. // In the case of 2. we should do nothing, see MDL-45580. // // To differentiate between 1. and 2. we can check if the submission has been modified since the // pages were generated. If it has, then we're in situation 2. $totalpagesforattempt = self::page_number_for_attempt($assignment, $userid, $attemptnumber, false); $submissionmodified = isset($pagemodified) && $submission->timemodified > $pagemodified; if (empty($pages) || (count($pages) != $totalpagesforattempt && !$submissionmodified)) { $pages = self::generate_page_images_for_attempt($assignment, $userid, $attemptnumber, $resetrotation); } return $pages; } /** * This function returns sensible filename for a feedback file. * @param int|\assign $assignment * @param int $userid * @param int $attemptnumber (-1 means latest attempt) * @return string */ protected static function get_downloadable_feedback_filename($assignment, $userid, $attemptnumber) { global $DB; $assignment = self::get_assignment_from_param($assignment); $groupmode = groups_get_activity_groupmode($assignment->get_course_module()); $groupname = ''; if ($groupmode) { $groupid = groups_get_activity_group($assignment->get_course_module(), true); $groupname = groups_get_group_name($groupid).'-'; } if ($groupname == '-') { $groupname = ''; } $grade = $assignment->get_user_grade($userid, true, $attemptnumber); $user = $DB->get_record('user', array('id'=>$userid), '*', MUST_EXIST); if ($assignment->is_blind_marking()) { $prefix = $groupname . get_string('participant', 'assign'); $prefix = str_replace('_', ' ', $prefix); $prefix = clean_filename($prefix . '_' . $assignment->get_uniqueid_for_user($userid) . '_'); } else { $prefix = $groupname . fullname($user); $prefix = str_replace('_', ' ', $prefix); $prefix = clean_filename($prefix . '_' . $assignment->get_uniqueid_for_user($userid) . '_'); } $prefix .= $grade->attemptnumber; return $prefix . '.pdf'; } /** * This function takes the combined pdf and embeds all the comments and annotations. * * This also moves the annotations and comments from drafts to not drafts. And it will * copy all the images stored to the readonly area, so that they can be viewed online, and * not be overwritten when a new submission is sent. * * @param int|\assign $assignment * @param int $userid * @param int $attemptnumber (-1 means latest attempt) * @return \stored_file */ public static function generate_feedback_document($assignment, $userid, $attemptnumber) { global $CFG; $assignment = self::get_assignment_from_param($assignment); if (!$assignment->can_view_submission($userid)) { throw new \moodle_exception('nopermission'); } if (!$assignment->can_grade()) { throw new \moodle_exception('nopermission'); } // Need to generate the page images - first get a combined pdf. $document = self::get_combined_pdf_for_attempt($assignment, $userid, $attemptnumber); $status = $document->get_status(); if ($status === combined_document::STATUS_FAILED) { throw new \moodle_exception('Could not generate combined pdf.'); } else if ($status === combined_document::STATUS_PENDING_INPUT) { // The conversion is still in progress. return false; } $file = $document->get_combined_file(); $tmpdir = make_request_directory(); $combined = $tmpdir . '/' . self::COMBINED_PDF_FILENAME; $file->copy_content_to($combined); // Copy the file. $pdf = new pdf(); // Set fontname from course setting if it's enabled. if (!empty($CFG->enablepdfexportfont)) { $fontlist = $pdf->get_export_fontlist(); // Load font from course if it's more than 1. if (count($fontlist) > 1) { $course = $assignment->get_course(); if (!empty($course->pdfexportfont)) { $pdf->set_export_font_name($course->pdfexportfont); } } else { $pdf->set_export_font_name(current($fontlist)); } } $fs = get_file_storage(); $stamptmpdir = make_request_directory(); $grade = $assignment->get_user_grade($userid, true, $attemptnumber); // Copy any new stamps to this instance. if ($files = $fs->get_area_files($assignment->get_context()->id, 'assignfeedback_editpdf', 'stamps', $grade->id, "filename", false)) { foreach ($files as $file) { $filename = $stamptmpdir . '/' . $file->get_filename(); $file->copy_content_to($filename); // Copy the file. } } $pagecount = $pdf->set_pdf($combined); $grade = $assignment->get_user_grade($userid, true, $attemptnumber); page_editor::release_drafts($grade->id); $allcomments = array(); for ($i = 0; $i < $pagecount; $i++) { $pagerotation = page_editor::get_page_rotation($grade->id, $i); $pagemargin = $pdf->getBreakMargin(); $autopagebreak = $pdf->getAutoPageBreak(); if (empty($pagerotation) || !$pagerotation->isrotated) { $pdf->copy_page(); } else { $rotatedimagefile = $fs->get_file_by_hash($pagerotation->pathnamehash); if (empty($rotatedimagefile)) { $pdf->copy_page(); } else { $pdf->add_image_page($rotatedimagefile); } } $comments = page_editor::get_comments($grade->id, $i, false); $annotations = page_editor::get_annotations($grade->id, $i, false); if (!empty($comments)) { $allcomments[$i] = $comments; } foreach ($annotations as $annotation) { $pdf->add_annotation($annotation->x, $annotation->y, $annotation->endx, $annotation->endy, $annotation->colour, $annotation->type, $annotation->path, $stamptmpdir); } $pdf->SetAutoPageBreak($autopagebreak, $pagemargin); $pdf->setPageMark(); } if (!empty($allcomments)) { // Append all comments to the end of the document. $links = $pdf->append_comments($allcomments); // Add the comment markers with links. foreach ($allcomments as $pageno => $comments) { foreach ($comments as $index => $comment) { $pdf->add_comment_marker($comment->pageno, $index, $comment->x, $comment->y, $links[$pageno][$index], $comment->colour); } } } fulldelete($stamptmpdir); $filename = self::get_downloadable_feedback_filename($assignment, $userid, $attemptnumber); $filename = clean_param($filename, PARAM_FILE); $generatedpdf = $tmpdir . '/' . $filename; $pdf->save_pdf($generatedpdf); $record = new \stdClass(); $record->contextid = $assignment->get_context()->id; $record->component = 'assignfeedback_editpdf'; $record->filearea = self::FINAL_PDF_FILEAREA; $record->itemid = $grade->id; $record->filepath = '/'; $record->filename = $filename; // Only keep one current version of the generated pdf. $fs->delete_area_files($record->contextid, $record->component, $record->filearea, $record->itemid); $file = $fs->create_file_from_pathname($record, $generatedpdf); // Cleanup. @unlink($generatedpdf); @unlink($combined); @rmdir($tmpdir); self::copy_pages_to_readonly_area($assignment, $grade); return $file; } /** * Copy the pages image to the readonly area. * * @param int|\assign $assignment The assignment. * @param \stdClass $grade The grade record. * @return void */ public static function copy_pages_to_readonly_area($assignment, $grade) { $fs = get_file_storage(); $assignment = self::get_assignment_from_param($assignment); $contextid = $assignment->get_context()->id; $component = 'assignfeedback_editpdf'; $itemid = $grade->id; // Get all the pages. $originalfiles = $fs->get_area_files($contextid, $component, self::PAGE_IMAGE_FILEAREA, $itemid); if (empty($originalfiles)) { // Nothing to do here... return; } // Delete the old readonly files. $fs->delete_area_files($contextid, $component, self::PAGE_IMAGE_READONLY_FILEAREA, $itemid); // Do the copying. foreach ($originalfiles as $originalfile) { $fs->create_file_from_storedfile(array('filearea' => self::PAGE_IMAGE_READONLY_FILEAREA), $originalfile); } } /** * This function returns the generated pdf (if it exists). * @param int|\assign $assignment * @param int $userid * @param int $attemptnumber (-1 means latest attempt) * @return \stored_file */ public static function get_feedback_document($assignment, $userid, $attemptnumber) { $assignment = self::get_assignment_from_param($assignment); if (!$assignment->can_view_submission($userid)) { throw new \moodle_exception('nopermission'); } $grade = $assignment->get_user_grade($userid, true, $attemptnumber); $contextid = $assignment->get_context()->id; $component = 'assignfeedback_editpdf'; $filearea = self::FINAL_PDF_FILEAREA; $itemid = $grade->id; $filepath = '/'; $fs = get_file_storage(); $files = $fs->get_area_files($contextid, $component, $filearea, $itemid, "itemid, filepath, filename", false); if ($files) { return reset($files); } return false; } /** * This function deletes the generated pdf for a student. * @param int|\assign $assignment * @param int $userid * @param int $attemptnumber (-1 means latest attempt) * @return bool */ public static function delete_feedback_document($assignment, $userid, $attemptnumber) { $assignment = self::get_assignment_from_param($assignment); if (!$assignment->can_view_submission($userid)) { throw new \moodle_exception('nopermission'); } if (!$assignment->can_grade()) { throw new \moodle_exception('nopermission'); } $grade = $assignment->get_user_grade($userid, true, $attemptnumber); $contextid = $assignment->get_context()->id; $component = 'assignfeedback_editpdf'; $filearea = self::FINAL_PDF_FILEAREA; $itemid = $grade->id; $fs = get_file_storage(); return $fs->delete_area_files($contextid, $component, $filearea, $itemid); } /** * Get All files in a File area * @param int|\assign $assignment Assignment * @param int $userid User ID * @param int $attemptnumber Attempt Number * @param string $filearea File Area * @param string $filepath File Path * @return array */ private static function get_files($assignment, $userid, $attemptnumber, $filearea, $filepath = '/') { $grade = $assignment->get_user_grade($userid, true, $attemptnumber); $itemid = $grade->id; $contextid = $assignment->get_context()->id; $component = self::COMPONENT; $fs = get_file_storage(); $files = $fs->get_directory_files($contextid, $component, $filearea, $itemid, $filepath); return $files; } /** * Save file. * @param int|\assign $assignment Assignment * @param int $userid User ID * @param int $attemptnumber Attempt Number * @param string $filearea File Area * @param string $newfilepath File Path * @param string $storedfilepath stored file path * @return \stored_file * @throws \file_exception * @throws \stored_file_creation_exception */ private static function save_file($assignment, $userid, $attemptnumber, $filearea, $newfilepath, $storedfilepath = '/') { $grade = $assignment->get_user_grade($userid, true, $attemptnumber); $itemid = $grade->id; $contextid = $assignment->get_context()->id; $record = new \stdClass(); $record->contextid = $contextid; $record->component = self::COMPONENT; $record->filearea = $filearea; $record->itemid = $itemid; $record->filepath = $storedfilepath; $record->filename = basename($newfilepath); $fs = get_file_storage(); $oldfile = $fs->get_file($record->contextid, $record->component, $record->filearea, $record->itemid, $record->filepath, $record->filename); if ($oldfile) { $newhash = \file_storage::hash_from_path($newfilepath); if ($newhash === $oldfile->get_contenthash()) { // Use existing file if contenthash match. return $oldfile; } // Delete existing file. $oldfile->delete(); } return $fs->create_file_from_pathname($record, $newfilepath); } /** * This function rotate a page, and mark the page as rotated. * @param int|\assign $assignment Assignment * @param int $userid User ID * @param int $attemptnumber Attempt Number * @param int $index Index of Current Page * @param bool $rotateleft To determine whether the page is rotated left or right. * @return null|\stored_file return rotated File * @throws \coding_exception * @throws \file_exception * @throws \moodle_exception * @throws \stored_file_creation_exception */ public static function rotate_page($assignment, $userid, $attemptnumber, $index, $rotateleft) { $assignment = self::get_assignment_from_param($assignment); $grade = $assignment->get_user_grade($userid, true, $attemptnumber); // Check permission. if (!$assignment->can_view_submission($userid)) { throw new \moodle_exception('nopermission'); } $filearea = self::PAGE_IMAGE_FILEAREA; $files = self::get_files($assignment, $userid, $attemptnumber, $filearea); if (!empty($files)) { foreach ($files as $file) { preg_match('/' . pdf::IMAGE_PAGE . '([\d]+)\./', $file->get_filename(), $matches); if (empty($matches) or !is_numeric($matches[1])) { throw new \coding_exception("'" . $file->get_filename() . "' file hasn't the expected format filename: image_pageXXXX.png."); } $pagenumber = (int)$matches[1]; if ($pagenumber == $index) { $source = imagecreatefromstring($file->get_content()); $pagerotation = page_editor::get_page_rotation($grade->id, $index); $degree = empty($pagerotation) ? 0 : $pagerotation->degree; if ($rotateleft) { $content = imagerotate($source, 90, 0); $degree = ($degree + 90) % 360; } else { $content = imagerotate($source, -90, 0); $degree = ($degree - 90) % 360; } $filename = $matches[0].'png'; $tmpdir = make_request_directory(); $tempfile = $tmpdir . '/' . time() . '_' . $filename; imagepng($content, $tempfile); $filearea = self::PAGE_IMAGE_FILEAREA; $newfile = self::save_file($assignment, $userid, $attemptnumber, $filearea, $tempfile); unlink($tempfile); rmdir($tmpdir); imagedestroy($source); imagedestroy($content); $file->delete(); if (!empty($newfile)) { page_editor::set_page_rotation($grade->id, $pagenumber, true, $newfile->get_pathnamehash(), $degree); } return $newfile; } } } return null; } /** * Convert jpg file to pdf file * @param int|\assign $assignment Assignment * @param int $userid User ID * @param int $attemptnumber Attempt Number * @param \stored_file $file file to save * @param null|array $size size of image * @return null|\stored_file * @throws \file_exception * @throws \stored_file_creation_exception */ private static function save_jpg_to_pdf($assignment, $userid, $attemptnumber, $file, $size=null) { // Temporary file. $filename = $file->get_filename(); $tmpdir = make_request_directory(); $tempfile = $tmpdir . DIRECTORY_SEPARATOR . $filename . ".pdf"; // Determine orientation. $orientation = 'P'; if (!empty($size['width']) && !empty($size['height'])) { if ($size['width'] > $size['height']) { $orientation = 'L'; } } // Save JPG image to PDF file. $pdf = new pdf(); $pdf->SetHeaderMargin(0); $pdf->SetFooterMargin(0); $pdf->SetMargins(0, 0, 0, true); $pdf->setPrintFooter(false); $pdf->setPrintHeader(false); $pdf->setImageScale(PDF_IMAGE_SCALE_RATIO); $pdf->AddPage($orientation); $pdf->SetAutoPageBreak(false); try { // Width has to be defined here to fit into an A4 page, otherwise the image will be inserted with original size. if ($orientation == 'P') { $pdf->Image('@' . $file->get_content(), 0, 0, 210); } else { $pdf->Image('@' . $file->get_content(), 0, 0, 297); } $pdf->setPageMark(); $pdf->save_pdf($tempfile); } catch (\Exception $e) { // Trim off the binary image data in the exception message for debugging output. $exceptionmsg = strstr($e->getMessage(), '@', true) ?: $e->getMessage(); debugging("Could not convert {$file->get_contenthash()} jpg to pdf: {$exceptionmsg}", DEBUG_ALL); return null; } $filearea = self::TMP_JPG_TO_PDF_FILEAREA; $pdffile = self::save_file($assignment, $userid, $attemptnumber, $filearea, $tempfile); if (file_exists($tempfile)) { unlink($tempfile); rmdir($tmpdir); } return $pdffile; } /** * Save rotated image data to file. * @param int|\assign $assignment Assignment * @param int $userid User ID * @param int $attemptnumber Attempt Number * @param resource $rotateddata image data to save * @param string $filename name of the image file * @return \stored_file * @throws \file_exception * @throws \stored_file_creation_exception */ private static function save_rotated_image_file($assignment, $userid, $attemptnumber, $rotateddata, $filename) { $filearea = self::TMP_ROTATED_JPG_FILEAREA; $tmpdir = make_request_directory(); $tempfile = $tmpdir . DIRECTORY_SEPARATOR . basename($filename); imagejpeg($rotateddata, $tempfile); $newfile = self::save_file($assignment, $userid, $attemptnumber, $filearea, $tempfile); if (file_exists($tempfile)) { unlink($tempfile); rmdir($tmpdir); } return $newfile; } } feedback/editpdf/classes/task/remove_orphaned_editpdf_files.php000064400000005477152265517350021105 0ustar00. /** * Cleans up orphaned feedback pdf files and table entries. * * @package assignfeedback_editpdf * @copyright 2022 Adrian Greeve * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace assignfeedback_editpdf\task; use core\task\adhoc_task; /** * Cleans up orphaned feedback pdf files and table entries. * * @package assignfeedback_editpdf * @copyright 2022 Adrian Greeve * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class remove_orphaned_editpdf_files extends adhoc_task { /** * Run the task. */ public function execute() { $this->remove_files_and_entries(); $this->remove_rotated_table_entries(); } /** * Removes edit pdf feedback files and table entries that have been orphaned. */ private function remove_files_and_entries(): void { global $DB; // Patiently remove all orphaned temporary pdf files. $sql = "SELECT DISTINCT f.contextid, f.component, f.filearea, f.itemid FROM {files} f LEFT JOIN {assign_grades} g ON g.id = f.itemid WHERE f.component = :assigneditpdf AND NOT (filearea = :stamps AND f.itemid = 0) AND g.id IS NULL"; $params = ['assigneditpdf' => 'assignfeedback_editpdf', 'stamps' => 'stamps']; $results = $DB->get_recordset_sql($sql, $params); foreach ($results as $record) { $fs = get_file_storage(); $fs->delete_area_files($record->contextid, $record->component, $record->filearea, $record->itemid); } $results->close(); } /** * Removes orphaned entries in the feedback edit pdf rotation table. */ private function remove_rotated_table_entries(): void { global $DB; $rotatesql = "SELECT er.id AS erid FROM {assignfeedback_editpdf_rot} er LEFT JOIN {assign_grades} g ON g.id = er.gradeid WHERE g.id IS NULL"; $DB->delete_records_subquery('assignfeedback_editpdf_rot', 'id', 'erid', $rotatesql); } } feedback/editpdf/classes/task/convert_submission.php000064400000014314152265517350016770 0ustar00. namespace assignfeedback_editpdf\task; use core\task\adhoc_task; use core\task\manager; use assignfeedback_editpdf\document_services; use assignfeedback_editpdf\combined_document; use assignfeedback_editpdf\pdf; use context_module; use moodle_exception; use assign; /** * An adhoc task to convert submissions to pdf in the background. * * @copyright 2022 Mikhail Golenkov * @package assignfeedback_editpdf * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class convert_submission extends adhoc_task { /** * Run the task. */ public function execute() { global $CFG, $DB; require_once($CFG->dirroot . '/mod/assign/locallib.php'); $data = $this->get_custom_data(); $submission = $DB->get_record('assign_submission', ['id' => $data->submissionid], '*', IGNORE_MISSING); if (!$submission) { mtrace('Submission no longer exists'); return; } // Early exit if ghostscript isn't correctly configured. $result = pdf::test_gs_path(false); if ($result->status !== pdf::GSPATH_OK) { $statusstring = get_string('test_' . $result->status, 'assignfeedback_editpdf'); throw new moodle_exception('pathtogserror', 'assignfeedback_editpdf', '', $statusstring, $result->status); } $cm = get_coursemodule_from_instance('assign', $submission->assignment, 0, false, MUST_EXIST); $context = context_module::instance($cm->id); $assign = new assign($context, null, null); if ($submission->userid) { $users = [$submission->userid]; } else { $users = []; $members = $assign->get_submission_group_members($submission->groupid, true); foreach ($members as $member) { $users[] = $member->id; } } $conversionrequirespolling = false; foreach ($users as $userid) { mtrace('Converting submission for user id ' . $userid); // If the assignment is not vieweable, we should not try to convert the documents // for this submission, as it will cause the adhoc task to fail with a permission // error. // // Comments on MDL-56810 indicate that submission conversion should not be attempted // if the submission is not viewable due to the user not being enrolled. if (!$assign->can_view_submission($userid)) { continue; } // Note: Before MDL-71468, the scheduled task version of this // task would stop attempting to poll the conversion after a // configured number of attempts were made to poll it, see: // // mod/assign/feedback/editpdf/classes/task/convert_submissions.php@MOODLE_400_STABLE // // This means that currently this adhoc task, if it fails, will retry forever. But // the fail-delay mechanism will ensure that it eventually only tries once per day. // // See MDL-75457 for details on re-implementing the conversionattemptlimit. // // Also note: This code must not be in a try/catch - an exception needs to be thrown to // allow the task API to mark the task as failed and update its faildelay. Using // manager::adhoc_task_failed in the catch block will not work, as the task API // will later assume the task completed successfully (as no exception was thrown) and // complete it (removing it from the adhoc task queue). $combineddocument = document_services::get_combined_pdf_for_attempt($assign, $userid, $data->submissionattempt); switch ($combineddocument->get_status()) { case combined_document::STATUS_READY: case combined_document::STATUS_READY_PARTIAL: case combined_document::STATUS_PENDING_INPUT: // The document has not been converted yet or is somehow still ready. $conversionrequirespolling = true; continue 2; case combined_document::STATUS_FAILED: // Although STATUS_FAILED indicates a "permanent error" it should be possible // in some cases to fix the underlying problem, allowing the conversion to // complete. So we throw an exception here, allowing the adhoc task to retry. // // Currently this can result in the task trying indefinitely (although it will // settle on trying once per day due to the faildelay exponential backoff) // however once the conversionattepmtlimit is re-implemented in MDL-75457 the // task will eventually get dropped. throw new \moodle_exception('documentcombinationfailed'); } document_services::get_page_images_for_attempt($assign, $userid, $data->submissionattempt, false); document_services::get_page_images_for_attempt($assign, $userid, $data->submissionattempt, true); } if ($conversionrequirespolling) { mtrace('Conversion still in progress. Requeueing self to check again.'); $task = new self; $task->set_custom_data($data); $task->set_next_run_time(time() + MINSECS); manager::queue_adhoc_task($task); } else { mtrace('The document has been successfully converted'); } } } feedback/editpdf/classes/task/bump_submission_for_stale_conversions.php000064400000011451152265517350022740 0ustar00. /** * Bump submission timemodified for conversions that are stale. * * @package assignfeedback_editpdf * @copyright 2022 Catalyst IT Australia Pty Ltd * @author Cameron Ball * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace assignfeedback_editpdf\task; use core\task\adhoc_task; /** * Adhoc task to bump the submission timemodified associated with a stale conversion. * * @package assignfeedback_editpdf * @copyright 2022 Catalyst IT Australia Pty Ltd * @author Cameron Ball * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class bump_submission_for_stale_conversions extends adhoc_task { /** * Run the task. */ public function execute() { global $DB; // Used to only get records after whenever document conversion was enabled for this site. $earliestconversion = $DB->get_record_sql("SELECT MIN(timecreated) AS min FROM {files} WHERE filearea = 'documentconversion'"); if (isset($earliestconversion->min)) { ['sql' => $extensionsql, 'params' => $extensionparams] = array_reduce( ['doc', 'docx', 'rtf', 'xls', 'xlsx', 'ppt', 'pptx', 'html', 'odt', 'ods', 'png', 'jpg', 'txt', 'gif'], function(array $c, string $ext) use ($DB): array { return [ 'sql' => $c['sql'] . ($c['sql'] ? ' OR ' : '') . $DB->sql_like('f1.filename', ':' . $ext), 'params' => $c['params'] + [$ext => '%.' . $ext] ]; }, ['sql' => '', 'params' => []] ); // A converted file has its filename set to the contenthash of the file it converted. // Find all files in the relevant file areas for which there is no corresponding // file with the contenthash as the file name. // // Also check if the file has a greater modified time than the submission, if it does // that means it is both stale (as per the above) and will never be reconverted. $sql = "SELECT f3.id, f3.timemodified as fmodified, asu.id as submissionid FROM {files} f1 LEFT JOIN {files} f2 ON f1.contenthash = f2.filename AND f2.component = 'core' AND f2.filearea = 'documentconversion' JOIN {assign_submission} asu ON asu.id = f1.itemid JOIN {assign_grades} asg ON asg.userid = asu.userid AND asg.assignment = asu.assignment JOIN {files} f3 ON f3.itemid = asg.id WHERE f1.filearea = 'submission_files' AND f3.timecreated >= :earliest AND ($extensionsql) AND f2.filename IS NULL AND f3.component = 'assignfeedback_editpdf' AND f3.filearea = 'combined' AND f3.filename = 'combined.pdf' AND f3.timemodified >= asu.timemodified"; $submissionstobump = $DB->get_records_sql($sql, ['earliest' => $earliestconversion->min] + $extensionparams); foreach ($submissionstobump as $submission) { // Set the submission modified time to one second later than the // converted files modified time, this will cause assign to reconvert // everything and delete the old files when the assignment grader is // viewed. See get_page_images_for_attempt in document_services.php. $newmodified = $submission->fmodified + 1; $record = (object)[ 'id' => $submission->submissionid, 'timemodified' => $newmodified ]; mtrace('Set submission ' . $submission->submissionid . ' timemodified to ' . $newmodified); $DB->update_record('assign_submission', $record); } } } } feedback/editpdf/classes/widget.php000064400000005356152265517350013364 0ustar00. /** * This file contains the definition for the library class for edit PDF renderer. * * @package assignfeedback_editpdf * @copyright 2012 Davo Smith * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); /** * A custom renderer class that extends the plugin_renderer_base and is used by the editpdf feedback plugin. * * @package assignfeedback_editpdf * @copyright 2013 Davo Smith * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class assignfeedback_editpdf_widget implements renderable { /** @var int $assignment - Assignment instance id */ public $assignment = 0; /** @var int $userid - The user id we are grading */ public $userid = 0; /** @var mixed $attemptnumber - The attempt number we are grading */ public $attemptnumber = 0; /** @var moodle_url $downloadurl */ public $downloadurl = null; /** @var string $downloadfilename */ public $downloadfilename = null; /** @var string[] $stampfiles */ public $stampfiles = array(); /** @var bool $readonly */ public $readonly = true; /** * Constructor * @param int $assignment - Assignment instance id * @param int $userid - The user id we are grading * @param int $attemptnumber - The attempt number we are grading * @param moodle_url $downloadurl - A url to download the current generated pdf. * @param string $downloadfilename - Name of the generated pdf. * @param string[] $stampfiles - The file names of the stamps. * @param bool $readonly - Show the readonly interface (no tools). */ public function __construct($assignment, $userid, $attemptnumber, $downloadurl, $downloadfilename, $stampfiles, $readonly) { $this->assignment = $assignment; $this->userid = $userid; $this->attemptnumber = $attemptnumber; $this->downloadurl = $downloadurl; $this->downloadfilename = $downloadfilename; $this->stampfiles = $stampfiles; $this->readonly = $readonly; } } feedback/editpdf/classes/page_editor.php000064400000036254152265517350014364 0ustar00. /** * This file contains the editor class for the assignfeedback_editpdf plugin * * @package assignfeedback_editpdf * @copyright 2012 Davo Smith * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace assignfeedback_editpdf; /** * This class performs crud operations on comments and annotations from a page of a response. * * No capability checks are done - they should be done by the calling class. * * @package assignfeedback_editpdf * @copyright 2012 Davo Smith * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class page_editor { /** * Get all comments for a page. * @param int $gradeid * @param int $pageno * @param bool $draft * @return comment[] */ public static function get_comments($gradeid, $pageno, $draft) { global $DB; $comments = array(); $params = array('gradeid'=>$gradeid, 'pageno'=>$pageno, 'draft'=>1); if (!$draft) { $params['draft'] = 0; } // Fetch comments ordered by position on the page. $records = $DB->get_records('assignfeedback_editpdf_cmnt', $params, 'y, x'); foreach ($records as $record) { array_push($comments, new comment($record)); } return $comments; } /** * Set all comments for a page. * @param int $gradeid * @param int $pageno * @param comment[] $comments * @return int - the number of comments. */ public static function set_comments($gradeid, $pageno, $comments) { global $DB; $DB->delete_records('assignfeedback_editpdf_cmnt', array('gradeid'=>$gradeid, 'pageno'=>$pageno, 'draft'=>1)); $added = 0; foreach ($comments as $record) { // Force these. if (!($record instanceof comment)) { $comment = new comment($record); } else { $comment = $record; } if (trim($comment->rawtext) === '') { continue; } $comment->gradeid = $gradeid; $comment->pageno = $pageno; $comment->draft = 1; if (self::add_comment($comment)) { $added++; } } return $added; } /** * Get a single comment by id. * @param int $commentid * @return comment or false */ public static function get_comment($commentid) { $record = $DB->get_record('assignfeedback_editpdf_cmnt', array('id'=>$commentid), '*', IGNORE_MISSING); if ($record) { return new comment($record); } return false; } /** * Add a comment to a page. * @param comment $comment * @return bool */ public static function add_comment(comment $comment) { global $DB; $comment->id = null; return $DB->insert_record('assignfeedback_editpdf_cmnt', $comment); } /** * Remove a comment from a page. * @param int $commentid * @return bool */ public static function remove_comment($commentid) { global $DB; return $DB->delete_records('assignfeedback_editpdf_cmnt', array('id'=>$commentid)); } /** * Get all annotations for a page. * @param int $gradeid * @param int $pageno * @param bool $draft * @return annotation[] */ public static function get_annotations($gradeid, $pageno, $draft) { global $DB; $params = array('gradeid'=>$gradeid, 'pageno'=>$pageno, 'draft'=>1); if (!$draft) { $params['draft'] = 0; } $annotations = array(); $records = $DB->get_records('assignfeedback_editpdf_annot', $params); foreach ($records as $record) { array_push($annotations, new annotation($record)); } return $annotations; } /** * Set all annotations for a page. * @param int $gradeid * @param int $pageno * @param annotation[] $annotations * @return int - the number of annotations. */ public static function set_annotations($gradeid, $pageno, $annotations) { global $DB; $DB->delete_records('assignfeedback_editpdf_annot', array('gradeid' => $gradeid, 'pageno' => $pageno, 'draft' => 1)); $added = 0; foreach ($annotations as $record) { // Force these. if (!($record instanceof annotation)) { $annotation = new annotation($record); } else { $annotation = $record; } $annotation->gradeid = $gradeid; $annotation->pageno = $pageno; $annotation->draft = 1; if (self::add_annotation($annotation)) { $added++; } } return $added; } /** * Get a single annotation by id. * @param int $annotationid * @return annotation or false */ public static function get_annotation($annotationid) { global $DB; $record = $DB->get_record('assignfeedback_editpdf_annot', array('id'=>$annotationid), '*', IGNORE_MISSING); if ($record) { return new annotation($record); } return false; } /** * Unrelease drafts * @param int $gradeid * @return bool */ public static function unrelease_drafts($gradeid) { global $DB; // Delete the non-draft annotations and comments. $result = $DB->delete_records('assignfeedback_editpdf_cmnt', array('gradeid'=>$gradeid, 'draft'=>0)); $result = $DB->delete_records('assignfeedback_editpdf_annot', array('gradeid'=>$gradeid, 'draft'=>0)) && $result; return $result; } /** * Release the draft comments and annotations to students. * @param int $gradeid * @return bool */ public static function release_drafts($gradeid) { global $DB; // Delete the previous non-draft annotations and comments. $DB->delete_records('assignfeedback_editpdf_cmnt', array('gradeid'=>$gradeid, 'draft'=>0)); $DB->delete_records('assignfeedback_editpdf_annot', array('gradeid'=>$gradeid, 'draft'=>0)); // Copy all the draft annotations and comments to non-drafts. $records = $DB->get_records('assignfeedback_editpdf_annot', array('gradeid'=>$gradeid, 'draft'=>1)); foreach ($records as $record) { unset($record->id); $record->draft = 0; $DB->insert_record('assignfeedback_editpdf_annot', $record); } $records = $DB->get_records('assignfeedback_editpdf_cmnt', array('gradeid'=>$gradeid, 'draft'=>1)); foreach ($records as $record) { unset($record->id); $record->draft = 0; $DB->insert_record('assignfeedback_editpdf_cmnt', $record); } return true; } /** * Has annotations or comments. * @param int $gradeid * @return bool */ public static function has_annotations_or_comments($gradeid, $includedraft) { global $DB; $params = array('gradeid'=>$gradeid); if (!$includedraft) { $params['draft'] = 0; } if ($DB->count_records('assignfeedback_editpdf_cmnt', $params)) { return true; } if ($DB->count_records('assignfeedback_editpdf_annot', $params)) { return true; } return false; } /** * Aborts all draft annotations and reverts to the last version released to students. * @param int $gradeid * @return bool */ public static function revert_drafts($gradeid) { global $DB; // Delete the previous non-draft annotations and comments. $DB->delete_records('assignfeedback_editpdf_cmnt', array('gradeid'=>$gradeid, 'draft'=>1)); $DB->delete_records('assignfeedback_editpdf_annot', array('gradeid'=>$gradeid, 'draft'=>1)); // Copy all the draft annotations and comments to non-drafts. $records = $DB->get_records('assignfeedback_editpdf_annot', array('gradeid'=>$gradeid, 'draft'=>0)); foreach ($records as $record) { unset($record->id); $record->draft = 0; $DB->insert_record('assignfeedback_editpdf_annot', $record); } $records = $DB->get_records('assignfeedback_editpdf_cmnt', array('gradeid'=>$gradeid, 'draft'=>0)); foreach ($records as $record) { unset($record->id); $record->draft = 0; $DB->insert_record('assignfeedback_editpdf_annot', $record); } return true; } /** * Add a annotation to a page. * @param annotation $annotation * @return bool */ public static function add_annotation(annotation $annotation) { global $DB; $annotation->id = null; return $DB->insert_record('assignfeedback_editpdf_annot', $annotation); } /** * Remove a annotation from a page. * @param int $annotationid * @return bool */ public static function remove_annotation($annotationid) { global $DB; return $DB->delete_records('assignfeedback_editpdf_annot', array('id'=>$annotationid)); } /** * Copy annotations, comments, pages, and other required content from the source user to the current group member * being procssed when using applytoall. * * @param int|\assign $assignment * @param \stdClass $grade * @param int $sourceuserid * @return bool */ public static function copy_drafts_from_to($assignment, $grade, $sourceuserid) { global $DB; // Delete any existing annotations and comments from current user. $DB->delete_records('assignfeedback_editpdf_annot', array('gradeid' => $grade->id)); $DB->delete_records('assignfeedback_editpdf_cmnt', array('gradeid' => $grade->id)); // Get gradeid, annotations and comments from sourceuserid. $sourceusergrade = $assignment->get_user_grade($sourceuserid, true, $grade->attemptnumber); $annotations = $DB->get_records('assignfeedback_editpdf_annot', array('gradeid' => $sourceusergrade->id, 'draft' => 1)); $comments = $DB->get_records('assignfeedback_editpdf_cmnt', array('gradeid' => $sourceusergrade->id, 'draft' => 1)); $contextid = $assignment->get_context()->id; $sourceitemid = $sourceusergrade->id; // Add annotations and comments to current user to generate feedback file. foreach ($annotations as $annotation) { $annotation->gradeid = $grade->id; $DB->insert_record('assignfeedback_editpdf_annot', $annotation); } foreach ($comments as $comment) { $comment->gradeid = $grade->id; $DB->insert_record('assignfeedback_editpdf_cmnt', $comment); } $fs = get_file_storage(); // Copy the stamp files. self::replace_files_from_to($fs, $contextid, $sourceitemid, $grade->id, document_services::STAMPS_FILEAREA, true); // Copy the PAGE_IMAGE_FILEAREA files. self::replace_files_from_to($fs, $contextid, $sourceitemid, $grade->id, document_services::PAGE_IMAGE_FILEAREA); return true; } /** * Replace the area files in the specified area with those in the source item id. * * @param \file_storage $fs The file storage class * @param int $contextid The ID of the context for the assignment. * @param int $sourceitemid The itemid to copy from - typically the source grade id. * @param int $itemid The itemid to copy to - typically the target grade id. * @param string $area The file storage area. * @param bool $includesubdirs Whether to copy the content of sub-directories too. */ public static function replace_files_from_to($fs, $contextid, $sourceitemid, $itemid, $area, $includesubdirs = false) { $component = 'assignfeedback_editpdf'; // Remove the existing files within this area. $fs->delete_area_files($contextid, $component, $area, $itemid); // Copy the files from the source area. if ($files = $fs->get_area_files($contextid, $component, $area, $sourceitemid, "filename", $includesubdirs)) { foreach ($files as $file) { $newrecord = new \stdClass(); $newrecord->contextid = $contextid; $newrecord->itemid = $itemid; $fs->create_file_from_storedfile($newrecord, $file); } } } /** * Delete the draft annotations and comments. * * This is intended to be used when the version of the PDF has changed and the annotations * might not be relevant any more, therefore we should delete them. * * @param int $gradeid The grade ID. * @return bool */ public static function delete_draft_content($gradeid) { global $DB; $conditions = array('gradeid' => $gradeid, 'draft' => 1); $result = $DB->delete_records('assignfeedback_editpdf_annot', $conditions); $result = $result && $DB->delete_records('assignfeedback_editpdf_cmnt', $conditions); return $result; } /** * Set page rotation value. * @param int $gradeid grade id. * @param int $pageno page number. * @param bool $isrotated whether the page is rotated or not. * @param string $pathnamehash path name hash * @param int $degree rotation degree. * @throws \dml_exception */ public static function set_page_rotation($gradeid, $pageno, $isrotated, $pathnamehash, $degree = 0) { global $DB; $oldrecord = self::get_page_rotation($gradeid, $pageno); if ($oldrecord == null) { $record = new \stdClass(); $record->gradeid = $gradeid; $record->pageno = $pageno; $record->isrotated = $isrotated; $record->pathnamehash = $pathnamehash; $record->degree = $degree; $DB->insert_record('assignfeedback_editpdf_rot', $record, false); } else { $oldrecord->isrotated = $isrotated; $oldrecord->pathnamehash = $pathnamehash; $oldrecord->degree = $degree; $DB->update_record('assignfeedback_editpdf_rot', $oldrecord, false); } } /** * Get Page Rotation Value. * @param int $gradeid grade id. * @param int $pageno page number. * @return mixed * @throws \dml_exception */ public static function get_page_rotation($gradeid, $pageno) { global $DB; $result = $DB->get_record('assignfeedback_editpdf_rot', array('gradeid' => $gradeid, 'pageno' => $pageno)); return $result; } } feedback/editpdf/locallib.php000064400000047000152265517350012215 0ustar00. /** * This file contains the definition for the library class for PDF feedback plugin * * * @package assignfeedback_editpdf * @copyright 2012 Davo Smith * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); use \assignfeedback_editpdf\document_services; use \assignfeedback_editpdf\page_editor; /** * library class for editpdf feedback plugin extending feedback plugin base class * * @package assignfeedback_editpdf * @copyright 2012 Davo Smith * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class assign_feedback_editpdf extends assign_feedback_plugin { /** @var boolean|null $enabledcache Cached lookup of the is_enabled function */ private $enabledcache = null; /** * Get the name of the file feedback plugin * @return string */ public function get_name() { return get_string('pluginname', 'assignfeedback_editpdf'); } /** * Create a widget for rendering the editor. * * @param int $userid * @param stdClass $grade * @param bool $readonly * @return assignfeedback_editpdf_widget */ public function get_widget($userid, $grade, $readonly) { $attempt = -1; if ($grade && isset($grade->attemptnumber)) { $attempt = $grade->attemptnumber; } else { $grade = $this->assignment->get_user_grade($userid, true); } $feedbackfile = document_services::get_feedback_document( $this->assignment->get_instance()->id, $userid, $attempt ); $stampfiles = array(); $fs = get_file_storage(); $syscontext = context_system::instance(); $asscontext = $this->assignment->get_context(); // Three file areas are used for stamps. // Current stamps are those configured as a site administration setting to be available for new uses. // When a stamp is removed from this filearea it is no longer available for new grade items. $currentstamps = $fs->get_area_files($syscontext->id, 'assignfeedback_editpdf', 'stamps', 0, 'filename', false); // Grade stamps are those which have been assigned for a specific grade item. // The stamps associated with a grade item are always used for that grade item, even if the stamp is removed // from the list of current stamps. $gradestamps = $fs->get_area_files($asscontext->id, 'assignfeedback_editpdf', 'stamps', $grade->id, 'filename', false); // The system stamps are perpetual and always exist. // They allow Moodle to serve a common URL for all users for any possible combination of stamps. // Files in the perpetual stamp filearea are within the system context, in itemid 0, and use the original stamps // contenthash as a folder name. This ensures that the combination of stamp filename, and stamp file content is // unique. $systemstamps = $fs->get_area_files($syscontext->id, 'assignfeedback_editpdf', 'systemstamps', 0, 'filename', false); // First check that all current stamps are listed in the grade stamps. foreach ($currentstamps as $stamp) { // Ensure that the current stamp is in the list of perpetual stamps. $systempathnamehash = $this->get_system_stamp_path($stamp); if (!array_key_exists($systempathnamehash, $systemstamps)) { $filerecord = (object) [ 'filearea' => 'systemstamps', 'filepath' => '/' . $stamp->get_contenthash() . '/', ]; $newstamp = $fs->create_file_from_storedfile($filerecord, $stamp); $systemstamps[$newstamp->get_pathnamehash()] = $newstamp; } // Ensure that the current stamp is in the list of stamps for the current grade item. $gradeitempathhash = $this->get_assignment_stamp_path($stamp, $grade->id); if (!array_key_exists($gradeitempathhash, $gradestamps)) { $filerecord = (object) [ 'contextid' => $asscontext->id, 'filearea' => 'stamps', 'itemid' => $grade->id, ]; $newstamp = $fs->create_file_from_storedfile($filerecord, $stamp); $gradestamps[$newstamp->get_pathnamehash()] = $newstamp; } } foreach ($gradestamps as $stamp) { // All gradestamps should be available in the systemstamps filearea, but some legacy stamps may not be. // These need to be copied over. // Note: This should ideally be performed as an upgrade step, but there may be other cases that these do not // exist, for example restored backups. // In any case this is a cheap operation as it is solely performing an array lookup. $systempathnamehash = $this->get_system_stamp_path($stamp); if (!array_key_exists($systempathnamehash, $systemstamps)) { $filerecord = (object) [ 'contextid' => $syscontext->id, 'itemid' => 0, 'filearea' => 'systemstamps', 'filepath' => '/' . $stamp->get_contenthash() . '/', ]; $systemstamp = $fs->create_file_from_storedfile($filerecord, $stamp); $systemstamps[$systemstamp->get_pathnamehash()] = $systemstamp; } // Always serve the perpetual system stamp. // This ensures that the stamp is highly cached and reduces the hit on the application server. $gradestamp = $systemstamps[$systempathnamehash]; $url = moodle_url::make_pluginfile_url( $gradestamp->get_contextid(), $gradestamp->get_component(), $gradestamp->get_filearea(), null, $gradestamp->get_filepath(), $gradestamp->get_filename(), false ); array_push($stampfiles, $url->out()); } $url = false; $filename = ''; if ($feedbackfile) { $url = moodle_url::make_pluginfile_url( $this->assignment->get_context()->id, 'assignfeedback_editpdf', document_services::FINAL_PDF_FILEAREA, $grade->id, '/', $feedbackfile->get_filename(), false ); $filename = $feedbackfile->get_filename(); } $widget = new assignfeedback_editpdf_widget( $this->assignment->get_instance()->id, $userid, $attempt, $url, $filename, $stampfiles, $readonly ); return $widget; } /** * Get the pathnamehash for the specified stamp if in the system stamps. * * @param stored_file $file * @return string */ protected function get_system_stamp_path(stored_file $stamp): string { $systemcontext = context_system::instance(); return file_storage::get_pathname_hash( $systemcontext->id, 'assignfeedback_editpdf', 'systemstamps', 0, '/' . $stamp->get_contenthash() . '/', $stamp->get_filename() ); } /** * Get the pathnamehash for the specified stamp if in the current assignment stamps. * * @param stored_file $file * @param int $gradeid * @return string */ protected function get_assignment_stamp_path(stored_file $stamp, int $gradeid): string { return file_storage::get_pathname_hash( $this->assignment->get_context()->id, 'assignfeedback_editpdf', 'stamps', $gradeid, $stamp->get_filepath(), $stamp->get_filename() ); } /** * Get form elements for grading form * * @param stdClass $grade * @param MoodleQuickForm $mform * @param stdClass $data * @param int $userid * @return bool true if elements were added to the form */ public function get_form_elements_for_user($grade, MoodleQuickForm $mform, stdClass $data, $userid) { global $PAGE; $attempt = -1; if ($grade) { $attempt = $grade->attemptnumber; } $renderer = $PAGE->get_renderer('assignfeedback_editpdf'); // Links to download the generated pdf... if ($attempt > -1 && page_editor::has_annotations_or_comments($grade->id, false)) { $html = $this->assignment->render_area_files('assignfeedback_editpdf', document_services::FINAL_PDF_FILEAREA, $grade->id); $mform->addElement('static', 'editpdf_files', get_string('downloadfeedback', 'assignfeedback_editpdf'), $html); } $widget = $this->get_widget($userid, $grade, false); $html = $renderer->render($widget); $mform->addElement('static', 'editpdf', get_string('editpdf', 'assignfeedback_editpdf'), $html); $mform->addHelpButton('editpdf', 'editpdf', 'assignfeedback_editpdf'); $mform->addElement('hidden', 'editpdf_source_userid', $userid); $mform->setType('editpdf_source_userid', PARAM_INT); $mform->setConstant('editpdf_source_userid', $userid); } /** * Check to see if the grade feedback for the pdf has been modified. * * @param stdClass $grade Grade object. * @param stdClass $data Data from the form submission (not used). * @return boolean True if the pdf has been modified, else false. */ public function is_feedback_modified(stdClass $grade, stdClass $data) { // We only need to know if the source user's PDF has changed. If so then all // following users will have the same status. If it's only an individual annotation // then only one user will come through this method. // Source user id is only added to the form if there was a pdf. if (!empty($data->editpdf_source_userid)) { $sourceuserid = $data->editpdf_source_userid; // Retrieve the grade information for the source user. $sourcegrade = $this->assignment->get_user_grade($sourceuserid, true, $grade->attemptnumber); $pagenumbercount = document_services::page_number_for_attempt($this->assignment, $sourceuserid, $sourcegrade->attemptnumber); for ($i = 0; $i < $pagenumbercount; $i++) { // Select all annotations. $draftannotations = page_editor::get_annotations($sourcegrade->id, $i, true); $nondraftannotations = page_editor::get_annotations($grade->id, $i, false); // Check to see if the count is the same. if (count($draftannotations) != count($nondraftannotations)) { // The count is different so we have a modification. return true; } else { $matches = 0; // Have a closer look and see if the draft files match all the non draft files. foreach ($nondraftannotations as $ndannotation) { foreach ($draftannotations as $dannotation) { foreach ($ndannotation as $key => $value) { // As the $draft was included in the class annotation, // it is necessary to omit it in the condition below as well, // otherwise, an error would be raised. if ($key != 'id' && $key != 'draft' && $value != $dannotation->{$key}) { continue 2; } } $matches++; } } if ($matches !== count($nondraftannotations)) { return true; } } // Select all comments. $draftcomments = page_editor::get_comments($sourcegrade->id, $i, true); $nondraftcomments = page_editor::get_comments($grade->id, $i, false); if (count($draftcomments) != count($nondraftcomments)) { return true; } else { // Go for a closer inspection. $matches = 0; foreach ($nondraftcomments as $ndcomment) { foreach ($draftcomments as $dcomment) { foreach ($ndcomment as $key => $value) { // As the $draft was included in the class comment, // it is necessary to omit it in the condition below as well, // otherwise, an error would be raised. if ($key != 'id' && $key != 'draft' && $value != $dcomment->{$key}) { continue 2; } } $matches++; } } if ($matches !== count($nondraftcomments)) { return true; } } } } return false; } /** * Generate the pdf. * * @param stdClass $grade * @param stdClass $data * @return bool */ public function save(stdClass $grade, stdClass $data) { // Source user id is only added to the form if there was a pdf. if (!empty($data->editpdf_source_userid)) { $sourceuserid = $data->editpdf_source_userid; // Copy drafts annotations and comments if current user is different to sourceuserid. if ($sourceuserid != $grade->userid) { page_editor::copy_drafts_from_to($this->assignment, $grade, $sourceuserid); } } if (page_editor::has_annotations_or_comments($grade->id, true)) { document_services::generate_feedback_document($this->assignment, $grade->userid, $grade->attemptnumber); } return true; } /** * Display the list of files in the feedback status table. * * @param stdClass $grade * @param bool $showviewlink (Always set to false). * @return string */ public function view_summary(stdClass $grade, & $showviewlink) { $showviewlink = false; return $this->view($grade); } /** * Display the list of files in the feedback status table. * * @param stdClass $grade * @return string */ public function view(stdClass $grade) { global $PAGE; $html = ''; // Show a link to download the pdf. if (page_editor::has_annotations_or_comments($grade->id, false)) { $html = $this->assignment->render_area_files('assignfeedback_editpdf', document_services::FINAL_PDF_FILEAREA, $grade->id); // Also show the link to the read-only interface. $renderer = $PAGE->get_renderer('assignfeedback_editpdf'); $widget = $this->get_widget($grade->userid, $grade, true); $html .= $renderer->render($widget); } return $html; } /** * Return true if there are no released comments/annotations. * * @param stdClass $grade */ public function is_empty(stdClass $grade) { global $DB; $comments = $DB->count_records('assignfeedback_editpdf_cmnt', array('gradeid'=>$grade->id, 'draft'=>0)); $annotations = $DB->count_records('assignfeedback_editpdf_annot', array('gradeid'=>$grade->id, 'draft'=>0)); return $comments == 0 && $annotations == 0; } /** * The assignment has been deleted - remove the plugin specific data * * @return bool */ public function delete_instance() { global $DB; $grades = $DB->get_records('assign_grades', array('assignment'=>$this->assignment->get_instance()->id), '', 'id'); if ($grades) { list($gradeids, $params) = $DB->get_in_or_equal(array_keys($grades), SQL_PARAMS_NAMED); $DB->delete_records_select('assignfeedback_editpdf_annot', 'gradeid ' . $gradeids, $params); $DB->delete_records_select('assignfeedback_editpdf_cmnt', 'gradeid ' . $gradeids, $params); $DB->delete_records_select('assignfeedback_editpdf_rot', 'gradeid ' . $gradeids, $params); } return true; } /** * Determine if ghostscript is available and working. * * @return bool */ public function is_available() { if ($this->enabledcache === null) { $testpath = assignfeedback_editpdf\pdf::test_gs_path(false); $this->enabledcache = ($testpath->status == assignfeedback_editpdf\pdf::GSPATH_OK); } return $this->enabledcache; } /** * Prevent enabling this plugin if ghostscript is not available. * * @return bool false */ public function is_configurable() { return $this->is_available(); } /** * Get file areas returns a list of areas this plugin stores files. * * @return array - An array of fileareas (keys) and descriptions (values) */ public function get_file_areas() { return [ document_services::FINAL_PDF_FILEAREA => $this->get_name(), document_services::COMBINED_PDF_FILEAREA => $this->get_name(), document_services::PARTIAL_PDF_FILEAREA => $this->get_name(), document_services::IMPORT_HTML_FILEAREA => $this->get_name(), document_services::PAGE_IMAGE_FILEAREA => $this->get_name(), document_services::PAGE_IMAGE_READONLY_FILEAREA => $this->get_name(), document_services::STAMPS_FILEAREA => $this->get_name(), document_services::TMP_JPG_TO_PDF_FILEAREA => $this->get_name(), document_services::TMP_ROTATED_JPG_FILEAREA => $this->get_name() ]; } /** * Get all file areas for user data related to this plugin. * * @return array - An array of user data fileareas (keys) and descriptions (values) */ public function get_user_data_file_areas(): array { return [ document_services::FINAL_PDF_FILEAREA => $this->get_name(), ]; } /** * This plugin will inject content into the review panel with javascript. * @return bool true */ public function supports_review_panel() { return true; } /** * Return the plugin configs for external functions. * * @return array the list of settings * @since Moodle 3.2 */ public function get_config_for_external() { return (array) $this->get_config(); } } feedback/editpdf/fpdi/README.md000064400000010707152265517350012130 0ustar00FPDI - Free PDF Document Importer ================================= [![Latest Stable Version](https://poser.pugx.org/setasign/fpdi/v/stable.svg)](https://packagist.org/packages/setasign/fpdi) [![Total Downloads](https://poser.pugx.org/setasign/fpdi/downloads.svg)](https://packagist.org/packages/setasign/fpdi) [![License](https://poser.pugx.org/setasign/fpdi/license.svg)](https://packagist.org/packages/setasign/fpdi) :heavy_exclamation_mark: This document refers to FPDI 2. Version 1 is deprecated and development is discontinued. :heavy_exclamation_mark: FPDI is a collection of PHP classes facilitating developers to read pages from existing PDF documents and use them as templates in [FPDF](http://www.fpdf.org), which was developed by Olivier Plathey. Apart from a copy of [FPDF](http://www.fpdf.org), FPDI does not require any special PHP extensions. FPDI can also be used as an extension for [TCPDF](https://github.com/tecnickcom/TCPDF) or [tFPDF](http://fpdf.org/en/script/script92.php), too. ## Installation with [Composer](https://packagist.org/packages/setasign/fpdi) Because FPDI can be used with FPDF, TCPDF or tFPDF we haven't added a fixed dependency in the main composer.json file. You need to add the dependency to the PDF generation library of your choice yourself. To use FPDI with FPDF include following in your composer.json file: ```json { "require": { "setasign/fpdf": "1.8.*", "setasign/fpdi": "^2.5" } } ``` If you want to use TCPDF, you have to update your composer.json to: ```json { "require": { "tecnickcom/tcpdf": "6.6.*", "setasign/fpdi": "^2.5" } } ``` If you want to use tFPDF, you have to update your composer.json to: ```json { "require": { "setasign/tfpdf": "1.33.*", "setasign/fpdi": "^2.3" } } ``` ## Manual Installation If you do not use composer, just require the autoload.php in the /src folder: ```php require_once('src/autoload.php'); ``` If you have a PSR-4 autoloader implemented, just register the src path as follows: ```php $loader = new \Example\Psr4AutoloaderClass; $loader->register(); $loader->addNamespace('setasign\Fpdi', 'path/to/src/'); ``` ## Changes to Version 1 Version 2 is a complete rewrite from scratch of FPDI which comes with: - Namespaced code - Clean and up-to-date code base and style - PSR-4 compatible autoloading - Performance improvements by up to 100% - Less memory consumption - Native support for reading PDFs from strings or stream-resources - Support for documents with "invalid" data before their file-header - Optimized page tree resolving - Usage of individual exceptions - Several test types (unit, functional and visual tests) We tried to keep the main methods and logical workflow the same as in version 1 but please notice that there were incompatible changes which you should consider when updating to version 2: - You need to load the code using the `src/autoload.php` file instead of `classes/FPDI.php`. - The classes and traits are namespaced now: `setasign\Fpdi` - Page boundaries beginning with a slash, such as `/MediaBox`, are not supported anymore. Remove the slash or use a constant of `PdfReader\PageBoundaries`. - The parameters $x, $y, $width and $height of the `useTemplate()` or `getTemplateSize()` method have more logical correct default values now. Passing `0` as width or height will result in an `InvalidArgumentException` now. - The return value of `getTemplateSize()` had changed to an array with more speaking keys and reusability: Use `width` instead of `w` and `height` instead of `h`. - If you want to use **FPDI with TCPDF** you need to refactor your code to use the class `Tcpdf\Fpdi` (since 2.1; before it was `TcpdfFpdi`) instead of `FPDI`. ## Example and Documentation A simple example, that imports a single page and places this onto a new created page: ```php AddPage(); // set the source file $pdf->setSourceFile("Fantastic-Speaker.pdf"); // import page 1 $tplId = $pdf->importPage(1); // use the imported page and place it at point 10,10 with a width of 100 mm $pdf->useTemplate($tplId, 10, 10, 100); $pdf->Output(); ``` A full end-user documentation and API reference is available [here](https://manuals.setasign.com/fpdi-manual/). feedback/editpdf/fpdi/SECURITY.md000064400000000301152265517350012427 0ustar00## Security contact information To report a security vulnerability, please use the [Tidelift security contact](https://tidelift.com/security). Tidelift will coordinate the fix and disclosure. feedback/editpdf/fpdi/readme_moodle.txt000064400000001332152265517350014200 0ustar00FPDI ================================== No changes from the upstream version have been made. Both FPDI and FPDF_TPL have been downloaded and unzipped to this directory. Information ----------- URL: http://www.setasign.de/products/pdf-php-solutions/fpdi/ Download from: http://www.setasign.de/products/pdf-php-solutions/fpdi/downloads Documentation: http://www.setasign.de/products/pdf-php-solutions/fpdi/manuals/ License: The MIT License (MIT) Installation ------------ 1) Download the latest version of fpdi from the url above. 2) Unzip the src directory files into this directory. 3) Update mod/assign/feedback/editpdf/fpdi/src/Tcpdf/Fpdi.php(or whichever file it has been replaced with) to extend 'pdf' instead of 'TCPDF'. feedback/editpdf/fpdi/composer.json000064400000003106152265517350013366 0ustar00{ "name": "setasign/fpdi", "homepage": "https://www.setasign.com/fpdi", "description": "FPDI is a collection of PHP classes facilitating developers to read pages from existing PDF documents and use them as templates in FPDF. Because it is also possible to use FPDI with TCPDF, there are no fixed dependencies defined. Please see suggestions for packages which evaluates the dependencies automatically.", "type": "library", "keywords": [ "pdf", "fpdi", "fpdf" ], "license": "MIT", "autoload": { "psr-4": { "setasign\\Fpdi\\": "src/" } }, "require": { "php": "^7.1 || ^8.0", "ext-zlib": "*" }, "conflict": { "setasign/tfpdf": "<1.31" }, "authors": [ { "name": "Jan Slabon", "email": "jan.slabon@setasign.com", "homepage": "https://www.setasign.com" }, { "name": "Maximilian Kresse", "email": "maximilian.kresse@setasign.com", "homepage": "https://www.setasign.com" } ], "suggest": { "setasign/fpdf": "FPDI will extend this class but as it is also possible to use TCPDF or tFPDF as an alternative. There's no fixed dependency configured." }, "require-dev": { "phpunit/phpunit": "^7", "setasign/fpdf": "~1.8.6", "tecnickcom/tcpdf": "^6.2", "setasign/tfpdf": "~1.33", "squizlabs/php_codesniffer": "^3.5" }, "autoload-dev": { "psr-4": { "setasign\\Fpdi\\": "tests/" } } } feedback/editpdf/fpdi/src/TcpdfFpdi.php000064400000001057152265517350014012 0ustar00currentTemplateId !== null) { throw new \BadMethodCallException('The page format cannot be changed when writing to a template.'); } if (!\in_array($orientation, ['P', 'L'], true)) { throw new \InvalidArgumentException(\sprintf( 'Invalid page orientation "%s"! Only "P" and "L" are allowed!', $orientation )); } $size = $this->_getpagesize($size); if ( $orientation != $this->CurOrientation || $size[0] != $this->CurPageSize[0] || $size[1] != $this->CurPageSize[1] ) { // New size or orientation if ($orientation === 'P') { $this->w = $size[0]; $this->h = $size[1]; } else { $this->w = $size[1]; $this->h = $size[0]; } $this->wPt = $this->w * $this->k; $this->hPt = $this->h * $this->k; $this->PageBreakTrigger = $this->h - $this->bMargin; $this->CurOrientation = $orientation; $this->CurPageSize = $size; $this->PageInfo[$this->page]['size'] = array($this->wPt, $this->hPt); } } /** * Draws a template onto the page or another template. * * Give only one of the size parameters (width, height) to calculate the other one automatically in view to the * aspect ratio. * * @param mixed $tpl The template id * @param array|float|int $x The abscissa of upper-left corner. Alternatively you could use an assoc array * with the keys "x", "y", "width", "height", "adjustPageSize". * @param float|int $y The ordinate of upper-left corner. * @param float|int|null $width The width. * @param float|int|null $height The height. * @param bool $adjustPageSize * @return array The size * @see FpdfTplTrait::getTemplateSize() */ public function useTemplate($tpl, $x = 0, $y = 0, $width = null, $height = null, $adjustPageSize = false) { if (!isset($this->templates[$tpl])) { throw new \InvalidArgumentException('Template does not exist!'); } if (\is_array($x)) { unset($x['tpl']); \extract($x, EXTR_IF_EXISTS); /** @noinspection NotOptimalIfConditionsInspection */ /** @phpstan-ignore function.alreadyNarrowedType */ if (\is_array($x)) { $x = 0; } } $template = $this->templates[$tpl]; $originalSize = $this->getTemplateSize($tpl); $newSize = $this->getTemplateSize($tpl, $width, $height); if ($adjustPageSize) { $this->setPageFormat($newSize, $newSize['orientation']); } $this->_out( // reset standard values, translate and scale \sprintf( 'q 0 J 1 w 0 j 0 G 0 g %.4F 0 0 %.4F %.4F %.4F cm /%s Do Q', ($newSize['width'] / $originalSize['width']), ($newSize['height'] / $originalSize['height']), $x * $this->k, ($this->h - $y - $newSize['height']) * $this->k, $template['id'] ) ); return $newSize; } /** * Get the size of a template. * * Give only one of the size parameters (width, height) to calculate the other one automatically in view to the * aspect ratio. * * @param mixed $tpl The template id * @param float|int|null $width The width. * @param float|int|null $height The height. * @return array|bool An array with following keys: width, height, 0 (=width), 1 (=height), orientation (L or P) */ public function getTemplateSize($tpl, $width = null, $height = null) { if (!isset($this->templates[$tpl])) { return false; } if ($width === null && $height === null) { $width = $this->templates[$tpl]['width']; $height = $this->templates[$tpl]['height']; } elseif ($width === null) { $width = $height * $this->templates[$tpl]['width'] / $this->templates[$tpl]['height']; } if ($height === null) { $height = $width * $this->templates[$tpl]['height'] / $this->templates[$tpl]['width']; } if ($height <= 0. || $width <= 0.) { throw new \InvalidArgumentException('Width or height parameter needs to be larger than zero.'); } return [ 'width' => $width, 'height' => $height, 0 => $width, 1 => $height, 'orientation' => $width > $height ? 'L' : 'P' ]; } /** * Begins a new template. * * @param float|int|null $width The width of the template. If null, the current page width is used. * @param float|int|null $height The height of the template. If null, the current page height is used. * @param bool $groupXObject Define the form XObject as a group XObject to support transparency (if used). * @return int A template identifier. */ public function beginTemplate($width = null, $height = null, $groupXObject = false) { if ($width === null) { $width = $this->w; } if ($height === null) { $height = $this->h; } $templateId = $this->getNextTemplateId(); // initiate buffer with current state of FPDF $buffer = "2 J\n" . \sprintf('%.2F w', $this->LineWidth * $this->k) . "\n"; if ($this->FontFamily) { $buffer .= \sprintf("BT /F%d %.2F Tf ET\n", $this->CurrentFont['i'], $this->FontSizePt); } if ($this->DrawColor !== '0 G') { $buffer .= $this->DrawColor . "\n"; } if ($this->FillColor !== '0 g') { $buffer .= $this->FillColor . "\n"; } if ($groupXObject && \version_compare('1.4', $this->PDFVersion, '>')) { $this->PDFVersion = '1.4'; } $this->templates[$templateId] = [ 'objectNumber' => null, 'id' => 'TPL' . $templateId, 'buffer' => $buffer, 'width' => $width, 'height' => $height, 'groupXObject' => $groupXObject, 'state' => [ 'x' => $this->x, 'y' => $this->y, 'AutoPageBreak' => $this->AutoPageBreak, 'bMargin' => $this->bMargin, 'tMargin' => $this->tMargin, 'lMargin' => $this->lMargin, 'rMargin' => $this->rMargin, 'h' => $this->h, 'hPt' => $this->hPt, 'w' => $this->w, 'wPt' => $this->wPt, 'FontFamily' => $this->FontFamily, 'FontStyle' => $this->FontStyle, 'FontSizePt' => $this->FontSizePt, 'FontSize' => $this->FontSize, 'underline' => $this->underline, 'TextColor' => $this->TextColor, 'DrawColor' => $this->DrawColor, 'FillColor' => $this->FillColor, 'ColorFlag' => $this->ColorFlag ] ]; $this->SetAutoPageBreak(false); $this->currentTemplateId = $templateId; $this->h = $height; $this->hPt = $height * $this->k; $this->w = $width; $this->wPt = $width * $this->k; $this->SetXY($this->lMargin, $this->tMargin); $this->SetRightMargin($this->w - $width + $this->rMargin); return $templateId; } /** * Ends a template. * * @return bool|int|null A template identifier. */ public function endTemplate() { if ($this->currentTemplateId === null) { return false; } $templateId = $this->currentTemplateId; $template = $this->templates[$templateId]; $state = $template['state']; $this->SetXY($state['x'], $state['y']); $this->tMargin = $state['tMargin']; $this->lMargin = $state['lMargin']; $this->rMargin = $state['rMargin']; $this->h = $state['h']; $this->hPt = $state['hPt']; $this->w = $state['w']; $this->wPt = $state['wPt']; $this->SetAutoPageBreak($state['AutoPageBreak'], $state['bMargin']); $this->FontFamily = $state['FontFamily']; $this->FontStyle = $state['FontStyle']; $this->FontSizePt = $state['FontSizePt']; $this->FontSize = $state['FontSize']; $this->TextColor = $state['TextColor']; $this->DrawColor = $state['DrawColor']; $this->FillColor = $state['FillColor']; $this->ColorFlag = $state['ColorFlag']; $this->underline = $state['underline']; $fontKey = $this->FontFamily . $this->FontStyle; if ($fontKey) { $this->CurrentFont =& $this->fonts[$fontKey]; } else { unset($this->CurrentFont); } $this->currentTemplateId = null; return $templateId; } /** * Get the next template id. * * @return int */ protected function getNextTemplateId() { return $this->templateId++; } /* overwritten FPDF methods: */ /** * @inheritdoc */ public function AddPage($orientation = '', $size = '', $rotation = 0) { if ($this->currentTemplateId !== null) { throw new \BadMethodCallException('Pages cannot be added when writing to a template.'); } parent::AddPage($orientation, $size, $rotation); } /** * @inheritdoc */ public function Link($x, $y, $w, $h, $link) { if ($this->currentTemplateId !== null) { throw new \BadMethodCallException('Links cannot be set when writing to a template.'); } parent::Link($x, $y, $w, $h, $link); } /** * @inheritdoc */ public function SetLink($link, $y = 0, $page = -1) { if ($this->currentTemplateId !== null) { throw new \BadMethodCallException('Links cannot be set when writing to a template.'); } return parent::SetLink($link, $y, $page); } /** * @inheritdoc */ public function SetDrawColor($r, $g = null, $b = null) { parent::SetDrawColor($r, $g, $b); if ($this->page === 0 && $this->currentTemplateId !== null) { $this->_out($this->DrawColor); } } /** * @inheritdoc */ public function SetFillColor($r, $g = null, $b = null) { parent::SetFillColor($r, $g, $b); if ($this->page === 0 && $this->currentTemplateId !== null) { $this->_out($this->FillColor); } } /** * @inheritdoc */ public function SetLineWidth($width) { parent::SetLineWidth($width); if ($this->page === 0 && $this->currentTemplateId !== null) { $this->_out(\sprintf('%.2F w', $width * $this->k)); } } /** * @inheritdoc */ public function SetFont($family, $style = '', $size = 0) { parent::SetFont($family, $style, $size); if ($this->page === 0 && $this->currentTemplateId !== null) { $this->_out(\sprintf('BT /F%d %.2F Tf ET', $this->CurrentFont['i'], $this->FontSizePt)); } } /** * @inheritdoc */ public function SetFontSize($size) { parent::SetFontSize($size); if ($this->page === 0 && $this->currentTemplateId !== null) { $this->_out(sprintf('BT /F%d %.2F Tf ET', $this->CurrentFont['i'], $this->FontSizePt)); } } protected function _putimages() { parent::_putimages(); foreach ($this->templates as $key => $template) { $this->_newobj(); $this->templates[$key]['objectNumber'] = $this->n; $this->_put('<_put(\sprintf( '/BBox[0 0 %.2F %.2F]', $template['width'] * $this->k, $template['height'] * $this->k )); $this->_put('/Resources 2 0 R'); // default resources dictionary of FPDF if ($this->compress) { $buffer = \gzcompress($template['buffer']); $this->_put('/Filter/FlateDecode'); } else { $buffer = $template['buffer']; } $this->_put('/Length ' . \strlen($buffer)); if ($template['groupXObject']) { $this->_put('/Group <>'); } $this->_put('>>'); $this->_putstream($buffer); $this->_put('endobj'); } } /** * @inheritdoc */ protected function _putxobjectdict() { foreach ($this->templates as $key => $template) { $this->_put('/' . $template['id'] . ' ' . $template['objectNumber'] . ' 0 R'); } parent::_putxobjectdict(); } /** * @inheritdoc */ public function _out($s) { if ($this->currentTemplateId !== null) { $this->templates[$this->currentTemplateId]['buffer'] .= $s . "\n"; } else { parent::_out($s); } } } feedback/editpdf/fpdi/src/Tcpdf/Fpdi.php000064400000032577152265517350014104 0ustar00cleanUp(); } /** * Get the next template id. * * @return int */ protected function getNextTemplateId() { return $this->templateId++; } /** * Draws an imported page onto the page or another template. * * Give only one of the size parameters (width, height) to calculate the other one automatically in view to the * aspect ratio. * * @param mixed $tpl The template id * @param float|int|array $x The abscissa of upper-left corner. Alternatively you could use an assoc array * with the keys "x", "y", "width", "height", "adjustPageSize". * @param float|int $y The ordinate of upper-left corner. * @param float|int|null $width The width. * @param float|int|null $height The height. * @param bool $adjustPageSize * @return array The size * @see FpdiTrait::getTemplateSize() */ public function useTemplate($tpl, $x = 0, $y = 0, $width = null, $height = null, $adjustPageSize = false) { return $this->useImportedPage($tpl, $x, $y, $width, $height, $adjustPageSize); } /** * Draws an imported page onto the page. * * Give only one of the size parameters (width, height) to calculate the other one automatically in view to the * aspect ratio. * * @param mixed $pageId The page id * @param float|int|array $x The abscissa of upper-left corner. Alternatively you could use an assoc array * with the keys "x", "y", "width", "height", "adjustPageSize". * @param float|int $y The ordinate of upper-left corner. * @param float|int|null $width The width. * @param float|int|null $height The height. * @param bool $adjustPageSize * @return array The size. * @see Fpdi::getTemplateSize() */ public function useImportedPage($pageId, $x = 0, $y = 0, $width = null, $height = null, $adjustPageSize = false) { $size = $this->fpdiUseImportedPage($pageId, $x, $y, $width, $height, $adjustPageSize); if ($this->inxobj) { $importedPage = $this->importedPages[$pageId]; $this->xobjects[$this->xobjid]['importedPages'][$importedPage['id']] = $pageId; } return $size; } /** * Get the size of an imported page. * * Give only one of the size parameters (width, height) to calculate the other one automatically in view to the * aspect ratio. * * @param mixed $tpl The template id * @param float|int|null $width The width. * @param float|int|null $height The height. * @return array|bool An array with following keys: width, height, 0 (=width), 1 (=height), orientation (L or P) */ public function getTemplateSize($tpl, $width = null, $height = null) { return $this->getImportedPageSize($tpl, $width, $height); } /** * @inheritdoc * @return string */ protected function _getxobjectdict() { $out = parent::_getxobjectdict(); foreach ($this->importedPages as $pageData) { $out .= '/' . $pageData['id'] . ' ' . $pageData['objectNumber'] . ' 0 R '; } return $out; } /** * @inheritdoc * @throws CrossReferenceException * @throws PdfParserException */ protected function _putxobjects() { foreach ($this->importedPages as $key => $pageData) { $this->currentObjectNumber = $this->_newobj(); $this->importedPages[$key]['objectNumber'] = $this->currentObjectNumber; $this->currentReaderId = $pageData['readerId']; $this->writePdfType($pageData['stream']); $this->_put('endobj'); } foreach (\array_keys($this->readers) as $readerId) { $parser = $this->getPdfReader($readerId)->getParser(); $this->currentReaderId = $readerId; while (($objectNumber = \array_pop($this->objectsToCopy[$readerId])) !== null) { try { $object = $parser->getIndirectObject($objectNumber); } catch (CrossReferenceException $e) { if ($e->getCode() === CrossReferenceException::OBJECT_NOT_FOUND) { $object = PdfIndirectObject::create($objectNumber, 0, new PdfNull()); } else { throw $e; } } $this->writePdfType($object); } } // let's prepare resources for imported pages in templates foreach ($this->xobjects as $xObjectId => $data) { if (!isset($data['importedPages'])) { continue; } foreach ($data['importedPages'] as $id => $pageKey) { $page = $this->importedPages[$pageKey]; $this->xobjects[$xObjectId]['xobjects'][$id] = ['n' => $page['objectNumber']]; } } parent::_putxobjects(); $this->currentObjectNumber = null; } /** * Append content to the buffer of TCPDF. * * @param string $s * @param bool $newLine */ protected function _put($s, $newLine = true) { if ($newLine) { $this->setBuffer($s . "\n"); } else { $this->setBuffer($s); } } /** * Begin a new object and return the object number. * * @param int|string $objid Object ID (leave empty to get a new ID). * @return int object number */ protected function _newobj($objid = '') { $this->_out($this->_getobj($objid)); return $this->n; } /** * Writes a PdfType object to the resulting buffer. * * @param PdfType $value * @throws PdfTypeException */ protected function writePdfType(PdfType $value) { if (!$this->encrypted) { $this->fpdiWritePdfType($value); return; } if ($value instanceof PdfString) { $string = PdfString::unescape($value->value); $string = $this->_encrypt_data($this->currentObjectNumber, $string); $value->value = PdfString::escape($string); } elseif ($value instanceof PdfHexString) { $filter = new AsciiHex(); $string = $filter->decode($value->value); $string = $this->_encrypt_data($this->currentObjectNumber, $string); $value->value = $filter->encode($string, true); } elseif ($value instanceof PdfStream) { $stream = $value->getStream(); $stream = $this->_encrypt_data($this->currentObjectNumber, $stream); $dictionary = $value->value; $dictionary->value['Length'] = PdfNumeric::create(\strlen($stream)); $value = PdfStream::create($dictionary, $stream); } elseif ($value instanceof PdfIndirectObject) { /** * @var PdfIndirectObject $value */ $this->currentObjectNumber = $this->objectMap[$this->currentReaderId][$value->objectNumber]; } $this->fpdiWritePdfType($value); } /** * This method will add additional data to the last created link/annotation. * * It will copy styling properties (supported by TCPDF) of the imported link. * * @param array $externalLink * @param float|int $xPt * @param float|int $scaleX * @param float|int $yPt * @param float|int $newHeightPt * @param float|int $scaleY * @param array $importedPage * @return void */ protected function adjustLastLink($externalLink, $xPt, $scaleX, $yPt, $newHeightPt, $scaleY, $importedPage) { $parser = $this->getPdfReader($importedPage['readerId'])->getParser(); if ($this->inxobj) { // store parameters for later use on template $lastAnnotationKey = count($this->xobjects[$this->xobjid]['annotations']) - 1; $lastAnnotationOpt = &$this->xobjects[$this->xobjid]['annotations'][$lastAnnotationKey]['opt']; } else { $lastAnnotationKey = count($this->PageAnnots[$this->page]) - 1; $lastAnnotationOpt = &$this->PageAnnots[$this->page][$lastAnnotationKey]['opt']; } // ensure we have a default value - otherwise TCPDF will set it to 4 throughout $lastAnnotationOpt['f'] = 0; // values in this dictonary are all direct objects and we don't need to resolve them here again. $values = $externalLink['pdfObject']->value; foreach ($values as $key => $value) { try { switch ($key) { case 'BS': $value = PdfDictionary::ensure($value); $bs = []; if (isset($value->value['W'])) { $bs['w'] = PdfNumeric::ensure($value->value['W'])->value; } if (isset($value->value['S'])) { $bs['s'] = PdfName::ensure($value->value['S'])->value; } if (isset($value->value['D'])) { $d = []; foreach (PdfArray::ensure($value->value['D'])->value as $item) { $d[] = PdfNumeric::ensure($item)->value; } $bs['d'] = $d; } $lastAnnotationOpt['bs'] = $bs; break; case 'Border': $borderArray = PdfArray::ensure($value)->value; if (count($borderArray) < 3) { continue 2; } $border = [ PdfNumeric::ensure($borderArray[0])->value, PdfNumeric::ensure($borderArray[1])->value, PdfNumeric::ensure($borderArray[2])->value, ]; if (isset($borderArray[3])) { $dashArray = []; foreach (PdfArray::ensure($borderArray[3])->value as $item) { $dashArray[] = PdfNumeric::ensure($item)->value; } $border[] = $dashArray; } $lastAnnotationOpt['border'] = $border; break; case 'C': $c = []; $colors = PdfArray::ensure(PdfType::resolve($value, $parser))->value; $m = count($colors) === 4 ? 100 : 255; foreach ($colors as $item) { $c[] = PdfNumeric::ensure($item)->value * $m; } $lastAnnotationOpt['c'] = $c; break; case 'F': $lastAnnotationOpt['f'] = $value->value; break; case 'BE': // is broken in current TCPDF version: "bc" key is checked but "bs" is used. break; } // let's silence invalid/not supported values } catch (FpdiException $e) { continue; } } // QuadPoints are not supported by TCPDF // if (count($externalLink['quadPoints']) > 0) { // $quadPoints = []; // for ($i = 0, $n = count($externalLink['quadPoints']); $i < $n; $i += 2) { // $quadPoints[] = $xPt + $externalLink['quadPoints'][$i] * $scaleX; // $quadPoints[] = $this->hPt - $yPt - $newHeightPt + $externalLink['quadPoints'][$i + 1] * $scaleY; // } // // ????? = $quadPoints; // } } } feedback/editpdf/fpdi/src/Tfpdf/Fpdi.php000064400000001133152265517350014067 0ustar00value; $ax = PdfNumeric::ensure(PdfType::resolve($array[0], $parser))->value; $ay = PdfNumeric::ensure(PdfType::resolve($array[1], $parser))->value; $bx = PdfNumeric::ensure(PdfType::resolve($array[2], $parser))->value; $by = PdfNumeric::ensure(PdfType::resolve($array[3], $parser))->value; return new self($ax, $ay, $bx, $by); } public static function byVectors(Vector $ll, Vector $ur) { return new self($ll->getX(), $ll->getY(), $ur->getX(), $ur->getY()); } /** * Rectangle constructor. * * @param float|int $ax * @param float|int $ay * @param float|int $bx * @param float|int $by */ public function __construct($ax, $ay, $bx, $by) { $this->llx = \min($ax, $bx); $this->lly = \min($ay, $by); $this->urx = \max($ax, $bx); $this->ury = \max($ay, $by); } /** * Get the width of the rectangle. * * @return float|int */ public function getWidth() { return $this->urx - $this->llx; } /** * Get the height of the rectangle. * * @return float|int */ public function getHeight() { return $this->ury - $this->lly; } /** * Get the lower left abscissa. * * @return float|int */ public function getLlx() { return $this->llx; } /** * Get the lower left ordinate. * * @return float|int */ public function getLly() { return $this->lly; } /** * Get the upper right abscissa. * * @return float|int */ public function getUrx() { return $this->urx; } /** * Get the upper right ordinate. * * @return float|int */ public function getUry() { return $this->ury; } /** * Get the rectangle as an array. * * @return array */ public function toArray() { return [ $this->llx, $this->lly, $this->urx, $this->ury ]; } /** * Get the rectangle as a PdfArray. * * @return PdfArray */ public function toPdfArray() { $array = new PdfArray(); $array->value[] = PdfNumeric::create($this->llx); $array->value[] = PdfNumeric::create($this->lly); $array->value[] = PdfNumeric::create($this->urx); $array->value[] = PdfNumeric::create($this->ury); return $array; } } feedback/editpdf/fpdi/src/PdfReader/PdfReaderException.php000064400000001132152265517350017510 0ustar00parser = $parser; } /** * PdfReader destructor. */ public function __destruct() { if ($this->parser !== null) { $this->parser->cleanUp(); } } /** * Get the pdf parser instance. * * @return PdfParser */ public function getParser() { return $this->parser; } /** * Get the PDF version. * * @return string * @throws PdfParserException */ public function getPdfVersion() { return \implode('.', $this->parser->getPdfVersion()); } /** * Get the page count. * * @return int * @throws PdfTypeException * @throws CrossReferenceException * @throws PdfParserException */ public function getPageCount() { if ($this->pageCount === null) { $catalog = $this->parser->getCatalog(); $pages = PdfType::resolve(PdfDictionary::get($catalog, 'Pages'), $this->parser); $count = PdfType::resolve(PdfDictionary::get($pages, 'Count'), $this->parser); $this->pageCount = PdfNumeric::ensure($count)->value; } return $this->pageCount; } /** * Get a page instance. * * @param int|numeric-string $pageNumber * @return Page * @throws PdfTypeException * @throws CrossReferenceException * @throws PdfParserException * @throws \InvalidArgumentException */ public function getPage($pageNumber) { if (!\is_numeric($pageNumber)) { throw new \InvalidArgumentException( 'Page number needs to be a number.' ); } if ($pageNumber < 1 || $pageNumber > $this->getPageCount()) { throw new \InvalidArgumentException( \sprintf( 'Page number "%s" out of available page range (1 - %s)', $pageNumber, $this->getPageCount() ) ); } $this->readPages(); $page = $this->pages[$pageNumber - 1]; if ($page instanceof PdfIndirectObjectReference) { $alreadyReadKids = []; $readPages = function ($kids) use (&$readPages, &$alreadyReadKids) { $kids = PdfArray::ensure($kids); /** @noinspection LoopWhichDoesNotLoopInspection */ foreach ($kids->value as $reference) { $reference = PdfIndirectObjectReference::ensure($reference); if (\in_array($reference->value, $alreadyReadKids, true)) { throw new PdfReaderException('Recursive pages dictionary detected.'); } $alreadyReadKids[] = $reference->value; $object = $this->parser->getIndirectObject($reference->value); $type = PdfDictionary::get($object->value, 'Type'); if ($type->value === 'Pages') { return $readPages(PdfDictionary::get($object->value, 'Kids')); } return $object; } throw new PdfReaderException( 'Kids array cannot be empty.', PdfReaderException::KIDS_EMPTY ); }; $page = $this->parser->getIndirectObject($page->value); $dict = PdfType::resolve($page, $this->parser); $type = PdfDictionary::get($dict, 'Type'); if ($type->value === 'Pages') { $kids = PdfType::resolve(PdfDictionary::get($dict, 'Kids'), $this->parser); try { $alreadyReadKids[] = $page->objectNumber; $page = $this->pages[$pageNumber - 1] = $readPages($kids); } catch (PdfReaderException $e) { if ($e->getCode() !== PdfReaderException::KIDS_EMPTY) { throw $e; } // let's reset the pages array and read all page objects $this->pages = []; $this->readPages(true); // @phpstan-ignore-next-line $page = $this->pages[$pageNumber - 1]; } } else { $this->pages[$pageNumber - 1] = $page; } } return new Page($page, $this->parser); } /** * Walk the page tree and resolve all indirect objects of all pages. * * @param bool $readAll * @throws CrossReferenceException * @throws PdfParserException * @throws PdfTypeException */ protected function readPages($readAll = false) { if (\count($this->pages) > 0) { return; } $expectedPageCount = $this->getPageCount(); $alreadyReadKids = []; $readPages = function ($kids, $count) use (&$readPages, &$alreadyReadKids, $readAll, $expectedPageCount) { $kids = PdfArray::ensure($kids); $isLeaf = ($count->value === \count($kids->value)); foreach ($kids->value as $reference) { $reference = PdfIndirectObjectReference::ensure($reference); if (!$readAll && $isLeaf) { $this->pages[] = $reference; continue; } if (\in_array($reference->value, $alreadyReadKids, true)) { throw new PdfReaderException('Recursive pages dictionary detected.'); } $alreadyReadKids[] = $reference->value; $object = $this->parser->getIndirectObject($reference->value); $type = PdfDictionary::get($object->value, 'Type'); if ($type->value === 'Pages') { $readPages( PdfType::resolve(PdfDictionary::get($object->value, 'Kids'), $this->parser), PdfType::resolve(PdfDictionary::get($object->value, 'Count'), $this->parser) ); } else { $this->pages[] = $object; } // stop if all pages are read - faulty documents exists with additional entries with invalid data. if (count($this->pages) === $expectedPageCount) { break; } } }; $catalog = $this->parser->getCatalog(); $pages = PdfType::resolve(PdfDictionary::get($catalog, 'Pages'), $this->parser); $count = PdfType::resolve(PdfDictionary::get($pages, 'Count'), $this->parser); $kids = PdfType::resolve(PdfDictionary::get($pages, 'Kids'), $this->parser); $readPages($kids, $count); } } feedback/editpdf/fpdi/src/PdfReader/Page.php000064400000033033152265517350014656 0ustar00pageObject = $page; $this->parser = $parser; } /** * Get the indirect object of this page. * * @return PdfIndirectObject */ public function getPageObject() { return $this->pageObject; } /** * Get the dictionary of this page. * * @return PdfDictionary * @throws PdfParserException * @throws PdfTypeException * @throws CrossReferenceException */ public function getPageDictionary() { if ($this->pageDictionary === null) { $this->pageDictionary = PdfDictionary::ensure(PdfType::resolve($this->getPageObject(), $this->parser)); } return $this->pageDictionary; } /** * Get a page attribute. * * @param string $name * @param bool $inherited * @return PdfType|null * @throws PdfParserException * @throws PdfTypeException * @throws CrossReferenceException */ public function getAttribute($name, $inherited = true) { $dict = $this->getPageDictionary(); if (isset($dict->value[$name])) { return $dict->value[$name]; } $inheritedKeys = ['Resources', 'MediaBox', 'CropBox', 'Rotate']; if ($inherited && \in_array($name, $inheritedKeys, true)) { if ($this->inheritedAttributes === null) { $this->inheritedAttributes = []; $inheritedKeys = \array_filter($inheritedKeys, function ($key) use ($dict) { return !isset($dict->value[$key]); }); if (\count($inheritedKeys) > 0) { $parentDict = PdfType::resolve(PdfDictionary::get($dict, 'Parent'), $this->parser); while ($parentDict instanceof PdfDictionary) { foreach ($inheritedKeys as $index => $key) { if (isset($parentDict->value[$key])) { $this->inheritedAttributes[$key] = $parentDict->value[$key]; unset($inheritedKeys[$index]); } } /** @noinspection NotOptimalIfConditionsInspection */ if (isset($parentDict->value['Parent']) && \count($inheritedKeys) > 0) { $parentDict = PdfType::resolve(PdfDictionary::get($parentDict, 'Parent'), $this->parser); } else { break; } } } } if (isset($this->inheritedAttributes[$name])) { return $this->inheritedAttributes[$name]; } } return null; } /** * Get the rotation value. * * @return int * @throws PdfParserException * @throws PdfTypeException * @throws CrossReferenceException */ public function getRotation() { $rotation = $this->getAttribute('Rotate'); if ($rotation === null) { return 0; } $rotation = PdfNumeric::ensure(PdfType::resolve($rotation, $this->parser))->value % 360; if ($rotation < 0) { $rotation += 360; } return $rotation; } /** * Get a boundary of this page. * * @param string $box * @param bool $fallback * @return bool|Rectangle * @throws PdfParserException * @throws PdfTypeException * @throws CrossReferenceException * @see PageBoundaries */ public function getBoundary($box = PageBoundaries::CROP_BOX, $fallback = true) { $value = $this->getAttribute($box); if ($value !== null) { return Rectangle::byPdfArray($value, $this->parser); } if ($fallback === false) { return false; } switch ($box) { case PageBoundaries::BLEED_BOX: case PageBoundaries::TRIM_BOX: case PageBoundaries::ART_BOX: return $this->getBoundary(PageBoundaries::CROP_BOX, true); case PageBoundaries::CROP_BOX: return $this->getBoundary(PageBoundaries::MEDIA_BOX, true); } return false; } /** * Get the width and height of this page. * * @param string $box * @param bool $fallback * @return array|bool * @throws PdfParserException * @throws PdfTypeException * @throws CrossReferenceException */ public function getWidthAndHeight($box = PageBoundaries::CROP_BOX, $fallback = true) { $boundary = $this->getBoundary($box, $fallback); if ($boundary === false) { return false; } $rotation = $this->getRotation(); $interchange = ($rotation / 90) % 2; return [ $interchange ? $boundary->getHeight() : $boundary->getWidth(), $interchange ? $boundary->getWidth() : $boundary->getHeight() ]; } /** * Get the raw content stream. * * @return string * @throws PdfReaderException * @throws PdfTypeException * @throws FilterException * @throws PdfParserException */ public function getContentStream() { $dict = $this->getPageDictionary(); $contents = PdfType::resolve(PdfDictionary::get($dict, 'Contents'), $this->parser); if ($contents instanceof PdfNull) { return ''; } if ($contents instanceof PdfArray) { $result = []; foreach ($contents->value as $content) { $content = PdfType::resolve($content, $this->parser); if (!($content instanceof PdfStream)) { continue; } $result[] = $content->getUnfilteredStream(); } return \implode("\n", $result); } if ($contents instanceof PdfStream) { return $contents->getUnfilteredStream(); } throw new PdfReaderException( 'Array or stream expected.', PdfReaderException::UNEXPECTED_DATA_TYPE ); } /** * Get information of all external links on this page. * * All coordinates are normalized in view to rotation and translation of the boundary-box, so that their * origin is lower-left. * * The URI is the binary value of the PDF string object. It can be in PdfDocEncoding or in UTF-16BE encoding. * * @return array */ public function getExternalLinks($box = PageBoundaries::CROP_BOX) { try { $dict = $this->getPageDictionary(); $annotations = PdfType::resolve(PdfDictionary::get($dict, 'Annots'), $this->parser); } catch (FpdiException $e) { return []; } if (!$annotations instanceof PdfArray) { return []; } $links = []; foreach ($annotations->value as $entry) { try { $annotation = PdfType::resolve($entry, $this->parser); $value = PdfType::resolve(PdfDictionary::get($annotation, 'Subtype'), $this->parser); if (!$value instanceof PdfName || $value->value !== 'Link') { continue; } $dest = PdfType::resolve(PdfDictionary::get($annotation, 'Dest'), $this->parser); if (!$dest instanceof PdfNull) { continue; } $action = PdfType::resolve(PdfDictionary::get($annotation, 'A'), $this->parser); if (!$action instanceof PdfDictionary) { continue; } $actionType = PdfType::resolve(PdfDictionary::get($action, 'S'), $this->parser); if (!$actionType instanceof PdfName || $actionType->value !== 'URI') { continue; } $uri = PdfType::resolve(PdfDictionary::get($action, 'URI'), $this->parser); if ($uri instanceof PdfString) { $uriValue = PdfString::unescape($uri->value); } elseif ($uri instanceof PdfHexString) { $uriValue = \hex2bin($uri->value); } else { continue; } $rect = PdfType::resolve(PdfDictionary::get($annotation, 'Rect'), $this->parser); if (!$rect instanceof PdfArray || count($rect->value) !== 4) { continue; } $rect = Rectangle::byPdfArray($rect, $this->parser); if ($rect->getWidth() === 0 || $rect->getHeight() === 0) { continue; } $bbox = $this->getBoundary($box); $rotation = $this->getRotation(); $gs = new GraphicsState(); $gs->translate(-$bbox->getLlx(), -$bbox->getLly()); $gs->rotate($bbox->getLlx(), $bbox->getLly(), -$rotation); switch ($rotation) { case 90: $gs->translate(-$bbox->getWidth(), 0); break; case 180: $gs->translate(-$bbox->getWidth(), -$bbox->getHeight()); break; case 270: $gs->translate(0, -$bbox->getHeight()); break; } $normalizedRect = Rectangle::byVectors( $gs->toUserSpace(new Vector($rect->getLlx(), $rect->getLly())), $gs->toUserSpace(new Vector($rect->getUrx(), $rect->getUry())) ); $quadPoints = PdfType::resolve(PdfDictionary::get($annotation, 'QuadPoints'), $this->parser); $normalizedQuadPoints = []; if ($quadPoints instanceof PdfArray) { $quadPointsCount = count($quadPoints->value); if ($quadPointsCount % 8 === 0) { for ($i = 0; ($i + 1) < $quadPointsCount; $i += 2) { $x = PdfNumeric::ensure(PdfType::resolve($quadPoints->value[$i], $this->parser)); $y = PdfNumeric::ensure(PdfType::resolve($quadPoints->value[$i + 1], $this->parser)); $v = $gs->toUserSpace(new Vector($x->value, $y->value)); $normalizedQuadPoints[] = $v->getX(); $normalizedQuadPoints[] = $v->getY(); } } } // we remove unsupported/unneeded values here unset( $annotation->value['P'], $annotation->value['NM'], $annotation->value['AP'], $annotation->value['AS'], $annotation->value['Type'], $annotation->value['Subtype'], $annotation->value['Rect'], $annotation->value['A'], $annotation->value['QuadPoints'], $annotation->value['Rotate'], $annotation->value['M'], $annotation->value['StructParent'], $annotation->value['OC'] ); // ...and flatten the PDF object to eliminate any indirect references. // Indirect references are a problem when writing the output in FPDF // because FPDF uses pre-calculated object numbers while FPDI creates // them at runtime. $annotation = PdfType::flatten($annotation, $this->parser); $links[] = [ 'rect' => $normalizedRect, 'quadPoints' => $normalizedQuadPoints, 'uri' => $uriValue, 'pdfObject' => $annotation ]; } catch (FpdiException $e) { continue; } } return $links; } } feedback/editpdf/fpdi/src/FpdfTrait.php000064400000015146152265517350014036 0ustar00cleanUp(); } /** * Draws an imported page or a template onto the page or another template. * * Give only one of the size parameters (width, height) to calculate the other one automatically in view to the * aspect ratio. * * @param mixed $tpl The template id * @param float|int|array $x The abscissa of upper-left corner. Alternatively you could use an assoc array * with the keys "x", "y", "width", "height", "adjustPageSize". * @param float|int $y The ordinate of upper-left corner. * @param float|int|null $width The width. * @param float|int|null $height The height. * @param bool $adjustPageSize * @return array The size * @see Fpdi::getTemplateSize() */ public function useTemplate($tpl, $x = 0, $y = 0, $width = null, $height = null, $adjustPageSize = false) { if (isset($this->importedPages[$tpl])) { $size = $this->useImportedPage($tpl, $x, $y, $width, $height, $adjustPageSize); if ($this->currentTemplateId !== null) { $this->templates[$this->currentTemplateId]['resources']['templates']['importedPages'][$tpl] = $tpl; } return $size; } return parent::useTemplate($tpl, $x, $y, $width, $height, $adjustPageSize); } /** * Get the size of an imported page or template. * * Give only one of the size parameters (width, height) to calculate the other one automatically in view to the * aspect ratio. * * @param mixed $tpl The template id * @param float|int|null $width The width. * @param float|int|null $height The height. * @return array|bool An array with following keys: width, height, 0 (=width), 1 (=height), orientation (L or P) */ public function getTemplateSize($tpl, $width = null, $height = null) { $size = parent::getTemplateSize($tpl, $width, $height); if ($size === false) { return $this->getImportedPageSize($tpl, $width, $height); } return $size; } /** * @throws CrossReferenceException * @throws PdfParserException */ protected function _putimages() { $this->currentReaderId = null; parent::_putimages(); foreach ($this->importedPages as $key => $pageData) { $this->_newobj(); $this->importedPages[$key]['objectNumber'] = $this->n; $this->currentReaderId = $pageData['readerId']; $this->writePdfType($pageData['stream']); $this->_put('endobj'); } foreach (\array_keys($this->readers) as $readerId) { $parser = $this->getPdfReader($readerId)->getParser(); $this->currentReaderId = $readerId; while (($objectNumber = \array_pop($this->objectsToCopy[$readerId])) !== null) { try { $object = $parser->getIndirectObject($objectNumber); } catch (CrossReferenceException $e) { if ($e->getCode() === CrossReferenceException::OBJECT_NOT_FOUND) { $object = PdfIndirectObject::create($objectNumber, 0, new PdfNull()); } else { throw $e; } } $this->writePdfType($object); } } $this->currentReaderId = null; } /** * @inheritdoc */ protected function _putxobjectdict() { foreach ($this->importedPages as $pageData) { $this->_put('/' . $pageData['id'] . ' ' . $pageData['objectNumber'] . ' 0 R'); } parent::_putxobjectdict(); } /** * @param int $n * @return void * @throws PdfParser\Type\PdfTypeException */ protected function _putlinks($n) { foreach ($this->PageLinks[$n] as $pl) { $this->_newobj(); $rect = sprintf('%.2F %.2F %.2F %.2F', $pl[0], $pl[1], $pl[0] + $pl[2], $pl[1] - $pl[3]); $this->_put('<_put('/A <_escape($pl[4]) . ')>>'); $values = $pl['importedLink']['pdfObject']->value; foreach ($values as $name => $entry) { $this->_put('/' . $name . ' ', false); $this->writePdfType($entry); } if (isset($pl['quadPoints'])) { $s = '/QuadPoints['; foreach ($pl['quadPoints'] as $value) { $s .= sprintf('%.2F ', $value); } $s .= ']'; $this->_put($s); } } else { $this->_put('/A <_textstring($pl[4]) . '>>'); $this->_put('/Border [0 0 0]', false); } $this->_put('>>'); } else { $this->_put('/Border [0 0 0] ', false); $l = $this->links[$pl[4]]; if (isset($this->PageInfo[$l[0]]['size'])) { $h = $this->PageInfo[$l[0]]['size'][1]; } else { $h = ($this->DefOrientation === 'P') ? $this->DefPageSize[1] * $this->k : $this->DefPageSize[0] * $this->k; } $this->_put(sprintf( '/Dest [%d 0 R /XYZ 0 %.2F null]>>', $this->PageInfo[$l[0]]['n'], $h - $l[1] * $this->k )); } $this->_put('endobj'); } } protected function _put($s, $newLine = true) { if ($newLine) { $this->buffer .= $s . "\n"; } else { $this->buffer .= $s; } } } feedback/editpdf/fpdi/src/Math/Vector.php000064400000002172152265517350014301 0ustar00x = (float)$x; $this->y = (float)$y; } /** * @return float */ public function getX() { return $this->x; } /** * @return float */ public function getY() { return $this->y; } /** * @param Matrix $matrix * @return Vector */ public function multiplyWithMatrix(Matrix $matrix) { list($a, $b, $c, $d, $e, $f) = $matrix->getValues(); $x = $a * $this->x + $c * $this->y + $e; $y = $b * $this->x + $d * $this->y + $f; return new self($x, $y); } } feedback/editpdf/fpdi/src/Math/Matrix.php000064400000004031152265517350014277 0ustar00a = (float)$a; $this->b = (float)$b; $this->c = (float)$c; $this->d = (float)$d; $this->e = (float)$e; $this->f = (float)$f; } /** * @return float[] */ public function getValues() { return [$this->a, $this->b, $this->c, $this->d, $this->e, $this->f]; } /** * @param Matrix $by * @return Matrix */ public function multiply(self $by) { $a = $this->a * $by->a + $this->b * $by->c //+ 0 * $by->e ; $b = $this->a * $by->b + $this->b * $by->d //+ 0 * $by->f ; $c = $this->c * $by->a + $this->d * $by->c //+ 0 * $by->e ; $d = $this->c * $by->b + $this->d * $by->d //+ 0 * $by->f ; $e = $this->e * $by->a + $this->f * $by->c + /*1 * */$by->e; $f = $this->e * $by->b + $this->f * $by->d + /*1 * */$by->f; return new self($a, $b, $c, $d, $e, $f); } } feedback/editpdf/fpdi/src/FpdiTrait.php000064400000056035152265517350014043 0ustar00readers) : $this->createdReaders; foreach ($readers as $id) { $this->readers[$id]->getParser()->getStreamReader()->cleanUp(); unset($this->readers[$id]); } $this->createdReaders = []; } /** * Set the minimal PDF version. * * @param string $pdfVersion */ protected function setMinPdfVersion($pdfVersion) { if (\version_compare($pdfVersion, $this->PDFVersion, '>')) { $this->PDFVersion = $pdfVersion; } } /** @noinspection PhpUndefinedClassInspection */ /** * Get a new pdf parser instance. * * @param StreamReader $streamReader * @param array $parserParams Individual parameters passed to the parser instance. * @return PdfParser|FpdiPdfParser */ protected function getPdfParserInstance(StreamReader $streamReader, array $parserParams = []) { // note: if you get an exception here - turn off errors/warnings on not found classes for your autoloader. // psr-4 (https://www.php-fig.org/psr/psr-4/) says: Autoloader implementations MUST NOT throw // exceptions, MUST NOT raise errors of any level, and SHOULD NOT return a value. /** @noinspection PhpUndefinedClassInspection */ if (\class_exists(FpdiPdfParser::class)) { /** @noinspection PhpUndefinedClassInspection */ return new FpdiPdfParser($streamReader, $parserParams); } return new PdfParser($streamReader); } /** * Get an unique reader id by the $file parameter. * * @param string|resource|PdfReader|StreamReader $file An open file descriptor, a path to a file, a PdfReader * instance or a StreamReader instance. * @param array $parserParams Individual parameters passed to the parser instance. * @return string */ protected function getPdfReaderId($file, array $parserParams = []) { if (\is_resource($file)) { $id = (string) $file; } elseif (\is_string($file)) { $id = \realpath($file); if ($id === false) { $id = $file; } } elseif (\is_object($file)) { $id = \spl_object_hash($file); } else { throw new \InvalidArgumentException( \sprintf('Invalid type in $file parameter (%s)', \gettype($file)) ); } /** @noinspection OffsetOperationsInspection */ if (isset($this->readers[$id])) { return $id; } if (\is_resource($file)) { $streamReader = new StreamReader($file); } elseif (\is_string($file)) { $streamReader = StreamReader::createByFile($file); $this->createdReaders[] = $id; } else { $streamReader = $file; } $reader = new PdfReader($this->getPdfParserInstance($streamReader, $parserParams)); /** @noinspection OffsetOperationsInspection */ $this->readers[$id] = $reader; return $id; } /** * Get a pdf reader instance by its id. * * @param string $id * @return PdfReader */ protected function getPdfReader($id) { if (isset($this->readers[$id])) { return $this->readers[$id]; } throw new \InvalidArgumentException( \sprintf('No pdf reader with the given id (%s) exists.', $id) ); } /** * Set the source PDF file. * * @param string|resource|StreamReader $file Path to the file or a stream resource or a StreamReader instance. * @return int The page count of the PDF document. * @throws PdfParserException */ public function setSourceFile($file) { return $this->setSourceFileWithParserParams($file); } /** * Set the source PDF file with parameters which are passed to the parser instance. * * This method allows us to pass e.g. authentication information to the parser instance. * * @param string|resource|StreamReader $file Path to the file or a stream resource or a StreamReader instance. * @param array $parserParams Individual parameters passed to the parser instance. * @return int The page count of the PDF document. * @throws CrossReferenceException * @throws PdfParserException * @throws PdfTypeException */ public function setSourceFileWithParserParams($file, array $parserParams = []) { $this->currentReaderId = $this->getPdfReaderId($file, $parserParams); $this->objectsToCopy[$this->currentReaderId] = []; $reader = $this->getPdfReader($this->currentReaderId); $this->setMinPdfVersion($reader->getPdfVersion()); return $reader->getPageCount(); } /** * Imports a page. * * @param int $pageNumber The page number. * @param string $box The page boundary to import. Default set to PageBoundaries::CROP_BOX. * @param bool $groupXObject Define the form XObject as a group XObject to support transparency (if used). * @param bool $importExternalLinks Define whether external links are imported or not. * @return string A unique string identifying the imported page. * @throws CrossReferenceException * @throws FilterException * @throws PdfParserException * @throws PdfTypeException * @throws PdfReaderException * @see PageBoundaries */ public function importPage( $pageNumber, $box = PageBoundaries::CROP_BOX, $groupXObject = true, $importExternalLinks = false ) { if ($this->currentReaderId === null) { throw new \BadMethodCallException('No reader initiated. Call setSourceFile() first.'); } $pageId = $this->currentReaderId; $pageNumber = (int)$pageNumber; $pageId .= '|' . $pageNumber . '|' . ($groupXObject ? '1' : '0') . '|' . ($importExternalLinks ? '1' : '0'); // for backwards compatibility with FPDI 1 $box = \ltrim($box, '/'); if (!PageBoundaries::isValidName($box)) { throw new \InvalidArgumentException( \sprintf('Box name is invalid: "%s"', $box) ); } $pageId .= '|' . $box; if (isset($this->importedPages[$pageId])) { return $pageId; } $reader = $this->getPdfReader($this->currentReaderId); $page = $reader->getPage($pageNumber); $bbox = $page->getBoundary($box); if ($bbox === false) { throw new PdfReaderException( \sprintf("Page doesn't have a boundary box (%s).", $box), PdfReaderException::MISSING_DATA ); } $dict = new PdfDictionary(); $dict->value['Type'] = PdfName::create('XObject'); $dict->value['Subtype'] = PdfName::create('Form'); $dict->value['FormType'] = PdfNumeric::create(1); $dict->value['BBox'] = $bbox->toPdfArray(); if ($groupXObject) { $this->setMinPdfVersion('1.4'); $dict->value['Group'] = PdfDictionary::create([ 'Type' => PdfName::create('Group'), 'S' => PdfName::create('Transparency') ]); } $resources = $page->getAttribute('Resources'); if ($resources !== null) { $dict->value['Resources'] = $resources; } list($width, $height) = $page->getWidthAndHeight($box); $a = 1; $b = 0; $c = 0; $d = 1; $e = -$bbox->getLlx(); $f = -$bbox->getLly(); $rotation = $page->getRotation(); if ($rotation !== 0) { $rotation *= -1; $angle = $rotation * M_PI / 180; $a = \cos($angle); $b = \sin($angle); $c = -$b; $d = $a; switch ($rotation) { case -90: $e = -$bbox->getLly(); $f = $bbox->getUrx(); break; case -180: $e = $bbox->getUrx(); $f = $bbox->getUry(); break; case -270: $e = $bbox->getUry(); $f = -$bbox->getLlx(); break; } } // we need to rotate/translate if ($a != 1 || $b != 0 || $c != 0 || $d != 1 || $e != 0 || $f != 0) { $dict->value['Matrix'] = PdfArray::create([ PdfNumeric::create($a), PdfNumeric::create($b), PdfNumeric::create($c), PdfNumeric::create($d), PdfNumeric::create($e), PdfNumeric::create($f) ]); } // try to use the existing content stream $pageDict = $page->getPageDictionary(); try { $contentsObject = PdfType::resolve(PdfDictionary::get($pageDict, 'Contents'), $reader->getParser(), true); $contents = PdfType::resolve($contentsObject, $reader->getParser()); // just copy the stream reference if it is only a single stream if ( ($contentsIsStream = ($contents instanceof PdfStream)) || ($contents instanceof PdfArray && \count($contents->value) === 1) ) { if ($contentsIsStream) { /** * @var PdfIndirectObject $contentsObject */ $stream = $contents; } else { $stream = PdfType::resolve($contents->value[0], $reader->getParser()); } $filter = PdfDictionary::get($stream->value, 'Filter'); if (!$filter instanceof PdfNull) { $dict->value['Filter'] = $filter; } $length = PdfType::resolve(PdfDictionary::get($stream->value, 'Length'), $reader->getParser()); $dict->value['Length'] = $length; $stream->value = $dict; // otherwise extract it from the array and re-compress the whole stream } else { $streamContent = $this->compress ? \gzcompress($page->getContentStream()) : $page->getContentStream(); $dict->value['Length'] = PdfNumeric::create(\strlen($streamContent)); if ($this->compress) { $dict->value['Filter'] = PdfName::create('FlateDecode'); } $stream = PdfStream::create($dict, $streamContent); } // Catch faulty pages and use an empty content stream } catch (FpdiException $e) { $dict->value['Length'] = PdfNumeric::create(0); $stream = PdfStream::create($dict, ''); } $externalLinks = []; if ($importExternalLinks) { $externalLinks = $page->getExternalLinks($box); } $this->importedPages[$pageId] = [ 'objectNumber' => null, 'readerId' => $this->currentReaderId, 'id' => 'TPL' . $this->getNextTemplateId(), 'width' => $width / $this->k, 'height' => $height / $this->k, 'stream' => $stream, 'externalLinks' => $externalLinks ]; return $pageId; } /** * Draws an imported page onto the page. * * Give only one of the size parameters (width, height) to calculate the other one automatically in view to the * aspect ratio. * * @param mixed $pageId The page id * @param float|int|array $x The abscissa of upper-left corner. Alternatively you could use an assoc array * with the keys "x", "y", "width", "height", "adjustPageSize". * @param float|int $y The ordinate of upper-left corner. * @param float|int|null $width The width. * @param float|int|null $height The height. * @param bool $adjustPageSize * @return array The size. * @see Fpdi::getTemplateSize() */ public function useImportedPage($pageId, $x = 0, $y = 0, $width = null, $height = null, $adjustPageSize = false) { if (\is_array($x)) { /** @noinspection OffsetOperationsInspection */ unset($x['pageId']); \extract($x, EXTR_IF_EXISTS); /** @noinspection NotOptimalIfConditionsInspection */ /** @phpstan-ignore function.alreadyNarrowedType */ if (\is_array($x)) { $x = 0; } } if (!isset($this->importedPages[$pageId])) { throw new \InvalidArgumentException('Imported page does not exist!'); } $importedPage = $this->importedPages[$pageId]; $originalSize = $this->getTemplateSize($pageId); $newSize = $this->getTemplateSize($pageId, $width, $height); if ($adjustPageSize) { $this->setPageFormat($newSize, $newSize['orientation']); } $scaleX = ($newSize['width'] / $originalSize['width']); $scaleY = ($newSize['height'] / $originalSize['height']); $xPt = $x * $this->k; $yPt = $y * $this->k; $newHeightPt = $newSize['height'] * $this->k; $this->_out( // reset standard values, translate and scale \sprintf( 'q 0 J 1 w 0 j 0 G 0 g %.4F 0 0 %.4F %.4F %.4F cm /%s Do Q', $scaleX, $scaleY, $xPt, $this->hPt - $yPt - $newHeightPt, $importedPage['id'] ) ); if (count($importedPage['externalLinks']) > 0) { foreach ($importedPage['externalLinks'] as $externalLink) { // mPDF uses also 'externalLinks' but doesn't come with a rect-value if (!isset($externalLink['rect'])) { continue; } /** @var Rectangle $rect */ $rect = $externalLink['rect']; $this->Link( $x + $rect->getLlx() / $this->k * $scaleX, $y + $newSize['height'] - ($rect->getLly() + $rect->getHeight()) / $this->k * $scaleY, $rect->getWidth() / $this->k * $scaleX, $rect->getHeight() / $this->k * $scaleY, $externalLink['uri'] ); $this->adjustLastLink($externalLink, $xPt, $scaleX, $yPt, $newHeightPt, $scaleY, $importedPage); } } return $newSize; } /** * This method will add additional data to the last created link/annotation. * * It is separated because TCPDF uses its own logic to handle link annotations. * This method is overwritten in the TCPDF implementation. * * @param array $externalLink * @param float|int $xPt * @param float|int $scaleX * @param float|int $yPt * @param float|int $newHeightPt * @param float|int $scaleY * @param array $importedPage * @return void */ protected function adjustLastLink($externalLink, $xPt, $scaleX, $yPt, $newHeightPt, $scaleY, $importedPage) { // let's create a relation of the newly created link to the data of the external link $lastLink = count($this->PageLinks[$this->page]); $this->PageLinks[$this->page][$lastLink - 1]['importedLink'] = $externalLink; if (count($externalLink['quadPoints']) > 0) { $quadPoints = []; for ($i = 0, $n = count($externalLink['quadPoints']); $i < $n; $i += 2) { $quadPoints[] = $xPt + $externalLink['quadPoints'][$i] * $scaleX; $quadPoints[] = $this->hPt - $yPt - $newHeightPt + $externalLink['quadPoints'][$i + 1] * $scaleY; } $this->PageLinks[$this->page][$lastLink - 1]['quadPoints'] = $quadPoints; } } /** * Get the size of an imported page. * * Give only one of the size parameters (width, height) to calculate the other one automatically in view to the * aspect ratio. * * @param mixed $tpl The template id * @param float|int|null $width The width. * @param float|int|null $height The height. * @return array|bool An array with following keys: width, height, 0 (=width), 1 (=height), orientation (L or P) */ public function getImportedPageSize($tpl, $width = null, $height = null) { if (isset($this->importedPages[$tpl])) { $importedPage = $this->importedPages[$tpl]; if ($width === null && $height === null) { $width = $importedPage['width']; $height = $importedPage['height']; } elseif ($width === null) { $width = $height * $importedPage['width'] / $importedPage['height']; } if ($height === null) { $height = $width * $importedPage['height'] / $importedPage['width']; } if ($height <= 0. || $width <= 0.) { throw new \InvalidArgumentException('Width or height parameter needs to be larger than zero.'); } return [ 'width' => $width, 'height' => $height, 0 => $width, 1 => $height, 'orientation' => $width > $height ? 'L' : 'P' ]; } return false; } /** * Writes a PdfType object to the resulting buffer. * * @param PdfType $value * @throws PdfTypeException */ protected function writePdfType(PdfType $value) { if ($value instanceof PdfNumeric) { if (\is_int($value->value)) { $this->_put($value->value . ' ', false); } else { $this->_put(\rtrim(\rtrim(\sprintf('%.5F', $value->value), '0'), '.') . ' ', false); } } elseif ($value instanceof PdfName) { $this->_put('/' . $value->value . ' ', false); } elseif ($value instanceof PdfString) { $this->_put('(' . $value->value . ')', false); } elseif ($value instanceof PdfHexString) { $this->_put('<' . $value->value . '>', false); } elseif ($value instanceof PdfBoolean) { $this->_put($value->value ? 'true ' : 'false ', false); } elseif ($value instanceof PdfArray) { $this->_put('[', false); foreach ($value->value as $entry) { $this->writePdfType($entry); } $this->_put(']'); } elseif ($value instanceof PdfDictionary) { $this->_put('<<', false); foreach ($value->value as $name => $entry) { $this->_put('/' . $name . ' ', false); $this->writePdfType($entry); } $this->_put('>>'); } elseif ($value instanceof PdfToken) { $this->_put($value->value); } elseif ($value instanceof PdfNull) { $this->_put('null ', false); } elseif ($value instanceof PdfStream) { $this->writePdfType($value->value); $this->_put('stream'); $this->_put($value->getStream()); $this->_put('endstream'); } elseif ($value instanceof PdfIndirectObjectReference) { if (!isset($this->objectMap[$this->currentReaderId])) { $this->objectMap[$this->currentReaderId] = []; } if (!isset($this->objectMap[$this->currentReaderId][$value->value])) { $this->objectMap[$this->currentReaderId][$value->value] = ++$this->n; $this->objectsToCopy[$this->currentReaderId][] = $value->value; } $this->_put($this->objectMap[$this->currentReaderId][$value->value] . ' 0 R ', false); } elseif ($value instanceof PdfIndirectObject) { $n = $this->objectMap[$this->currentReaderId][$value->objectNumber]; $this->_newobj($n); $this->writePdfType($value->value); // add newline before "endobj" for all objects in view to PDF/A conformance if ( !( ($value->value instanceof PdfArray) || ($value->value instanceof PdfDictionary) || ($value->value instanceof PdfToken) || ($value->value instanceof PdfStream) ) ) { $this->_put("\n", false); } $this->_put('endobj'); } } } feedback/editpdf/fpdi/src/PdfParser/Filter/Ascii85.php000064400000005620152265517350016467 0ustar00 if ($ch === 126 && isset($data[$k + 1]) && (\ord($data[$k + 1]) & 0xFF) === 62) { break; } if ($ch === 122 /* z */ && $state === 0) { $out .= \chr(0) . \chr(0) . \chr(0) . \chr(0); continue; } if ($ch < 33 /* ! */ || $ch > 117 /* u */) { throw new Ascii85Exception( 'Illegal character found while ASCII85 decode.', Ascii85Exception::ILLEGAL_CHAR_FOUND ); } $chn[$state] = $ch - 33;/* ! */ $state++; if ($state === 5) { $state = 0; $r = 0; for ($j = 0; $j < 5; ++$j) { /** @noinspection UnnecessaryCastingInspection */ $r = (int)($r * 85 + $chn[$j]); } $out .= \chr($r >> 24) . \chr($r >> 16) . \chr($r >> 8) . \chr($r); } } if ($state === 1) { throw new Ascii85Exception( 'Illegal length while ASCII85 decode.', Ascii85Exception::ILLEGAL_LENGTH ); } if ($state === 2) { $r = $chn[0] * 85 * 85 * 85 * 85 + ($chn[1] + 1) * 85 * 85 * 85; $out .= \chr($r >> 24); } elseif ($state === 3) { $r = $chn[0] * 85 * 85 * 85 * 85 + $chn[1] * 85 * 85 * 85 + ($chn[2] + 1) * 85 * 85; $out .= \chr($r >> 24); $out .= \chr($r >> 16); } elseif ($state === 4) { $r = $chn[0] * 85 * 85 * 85 * 85 + $chn[1] * 85 * 85 * 85 + $chn[2] * 85 * 85 + ($chn[3] + 1) * 85; $out .= \chr($r >> 24); $out .= \chr($r >> 16); $out .= \chr($r >> 8); } return $out; } } feedback/editpdf/fpdi/src/PdfParser/Filter/FlateException.php000064400000000772152265517350020177 0ustar00initsTable(); $this->data = $data; $this->dataLength = \strlen($data); // Initialize pointers $this->bytePointer = 0; $this->nextData = 0; $this->nextBits = 0; $prevCode = 0; $uncompData = ''; while (($code = $this->getNextCode()) !== 257) { if ($code === 256) { $this->initsTable(); } elseif ($prevCode === 256) { $uncompData .= $this->sTable[$code]; } elseif ($code < $this->tIdx) { $string = $this->sTable[$code]; $uncompData .= $string; $this->addStringToTable($this->sTable[$prevCode], $string[0]); } else { $string = $this->sTable[$prevCode]; $string .= $string[0]; $uncompData .= $string; $this->addStringToTable($string); } $prevCode = $code; } return $uncompData; } /** * Initialize the string table. */ protected function initsTable() { $this->sTable = []; for ($i = 0; $i < 256; $i++) { $this->sTable[$i] = \chr($i); } $this->tIdx = 258; $this->bitsToGet = 9; } /** * Add a new string to the string table. * * @param string $oldString * @param string $newString */ protected function addStringToTable($oldString, $newString = '') { $string = $oldString . $newString; // Add this new String to the table $this->sTable[$this->tIdx++] = $string; if ($this->tIdx === 511) { $this->bitsToGet = 10; } elseif ($this->tIdx === 1023) { $this->bitsToGet = 11; } elseif ($this->tIdx === 2047) { $this->bitsToGet = 12; } } /** * Returns the next 9, 10, 11 or 12 bits. * * @return int */ protected function getNextCode() { if ($this->bytePointer === $this->dataLength) { return 257; } $this->nextData = ($this->nextData << 8) | (\ord($this->data[$this->bytePointer++]) & 0xff); $this->nextBits += 8; if ($this->nextBits < $this->bitsToGet) { $this->nextData = ($this->nextData << 8) | (\ord($this->data[$this->bytePointer++]) & 0xff); $this->nextBits += 8; } $code = ($this->nextData >> ($this->nextBits - $this->bitsToGet)) & $this->andTable[$this->bitsToGet - 9]; $this->nextBits -= $this->bitsToGet; return $code; } } feedback/editpdf/fpdi/src/PdfParser/Filter/LzwException.php000064400000000676152265517350017723 0ustar00')); if ((\strlen($data) % 2) === 1) { $data .= '0'; } return \pack('H*', $data); } /** * Converts a string into ASCII hexadecimal representation. * * @param string $data The input string * @param boolean $leaveEOD * @return string */ public function encode($data, $leaveEOD = false) { $t = \unpack('H*', $data); return \current($t) . ($leaveEOD ? '' : '>'); } } feedback/editpdf/fpdi/src/PdfParser/Filter/Flate.php000064400000004366152265517350016323 0ustar00extensionLoaded()) { $oData = $data; $data = (($data !== '') ? @\gzuncompress($data) : ''); if ($data === false) { // let's try if the checksum is CRC32 $fh = fopen('php://temp', 'w+b'); fwrite($fh, "\x1f\x8b\x08\x00\x00\x00\x00\x00" . $oData); // "window" == 31 -> 16 + (8 to 15): Uses the low 4 bits of the value as the window size logarithm. // The input must include a gzip header and trailer (via 16). stream_filter_append($fh, 'zlib.inflate', STREAM_FILTER_READ, ['window' => 31]); fseek($fh, 0); $data = @stream_get_contents($fh); fclose($fh); if ($data) { return $data; } // Try this fallback (remove the zlib stream header) $data = @(gzinflate(substr($oData, 2))); if ($data === false) { throw new FlateException( 'Error while decompressing stream.', FlateException::DECOMPRESS_ERROR ); } } } else { throw new FlateException( 'To handle FlateDecode filter, enable zlib support in PHP.', FlateException::NO_ZLIB ); } return $data; } } feedback/editpdf/fpdi/src/PdfParser/Filter/Ascii85Exception.php000064400000001007152265517350020341 0ustar00parser = $parser; $this->readTrailer(); } /** * Get the trailer dictionary. * * @return PdfDictionary */ public function getTrailer() { return $this->trailer; } /** * Read the trailer dictionary. * * @throws CrossReferenceException * @throws PdfTypeException */ protected function readTrailer() { try { $trailerKeyword = $this->parser->readValue(null, PdfToken::class); if ($trailerKeyword->value !== 'trailer') { throw new CrossReferenceException( \sprintf( 'Unexpected end of cross reference. "trailer"-keyword expected, got: %s.', $trailerKeyword->value ), CrossReferenceException::UNEXPECTED_END ); } } catch (PdfTypeException $e) { throw new CrossReferenceException( 'Unexpected end of cross reference. "trailer"-keyword expected, got an invalid object type.', CrossReferenceException::UNEXPECTED_END, $e ); } try { $trailer = $this->parser->readValue(null, PdfDictionary::class); } catch (PdfTypeException $e) { throw new CrossReferenceException( 'Unexpected end of cross reference. Trailer not found.', CrossReferenceException::UNEXPECTED_END, $e ); } $this->trailer = $trailer; } } feedback/editpdf/fpdi/src/PdfParser/CrossReference/ReaderInterface.php000064400000001376152265517350021774 0ustar00 20 bytes). */ class LineReader extends AbstractReader implements ReaderInterface { /** * The object offsets. * * @var array */ protected $offsets; /** * LineReader constructor. * * @param PdfParser $parser * @throws CrossReferenceException */ public function __construct(PdfParser $parser) { $this->read($this->extract($parser->getStreamReader())); parent::__construct($parser); } /** * @inheritdoc * @return int|false */ public function getOffsetFor($objectNumber) { if (isset($this->offsets[$objectNumber])) { return $this->offsets[$objectNumber][0]; } return false; } /** * Get all found offsets. * * @return array */ public function getOffsets() { return $this->offsets; } /** * Extracts the cross reference data from the stream reader. * * @param StreamReader $reader * @return string * @throws CrossReferenceException */ protected function extract(StreamReader $reader) { $bytesPerCycle = 100; $reader->reset(null, $bytesPerCycle); $cycles = 0; do { // 6 = length of "trailer" - 1 $pos = \max(($bytesPerCycle * $cycles) - 6, 0); $trailerPos = \strpos($reader->getBuffer(false), 'trailer', $pos); $cycles++; } while ($trailerPos === false && $reader->increaseLength($bytesPerCycle) !== false); if ($trailerPos === false) { throw new CrossReferenceException( 'Unexpected end of cross reference. "trailer"-keyword not found.', CrossReferenceException::NO_TRAILER_FOUND ); } $xrefContent = \substr($reader->getBuffer(false), 0, $trailerPos); $reader->reset($reader->getPosition() + $trailerPos); return $xrefContent; } /** * Read the cross-reference entries. * * @param string $xrefContent * @throws CrossReferenceException */ protected function read($xrefContent) { // get eol markers in the first 100 bytes \preg_match_all("/(\r\n|\n|\r)/", \substr($xrefContent, 0, 100), $m); if (\count($m[0]) === 0) { throw new CrossReferenceException( 'No data found in cross-reference.', CrossReferenceException::INVALID_DATA ); } // count(array_count_values()) is faster then count(array_unique()) // @see https://github.com/symfony/symfony/pull/23731 // can be reverted for php7.2 $differentLineEndings = \count(\array_count_values($m[0])); if ($differentLineEndings > 1) { $lines = \preg_split("/(\r\n|\n|\r)/", $xrefContent, -1, PREG_SPLIT_NO_EMPTY); } else { $lines = \explode($m[0][0], $xrefContent); } unset($differentLineEndings, $m); if (!\is_array($lines)) { $this->offsets = []; return; } $start = 0; $offsets = []; // trim all lines and remove empty lines $lines = \array_filter(\array_map('\trim', $lines)); foreach ($lines as $line) { $pieces = \explode(' ', $line); switch (\count($pieces)) { case 2: $start = (int) $pieces[0]; break; case 3: switch ($pieces[2]) { case 'n': $offsets[$start] = [(int) $pieces[0], (int) $pieces[1]]; $start++; break 2; case 'f': $start++; break 2; } // fall through if pieces doesn't match default: throw new CrossReferenceException( \sprintf('Unexpected data in xref table (%s)', \implode(' ', $pieces)), CrossReferenceException::INVALID_DATA ); } } $this->offsets = $offsets; } } feedback/editpdf/fpdi/src/PdfParser/CrossReference/CrossReference.php000064400000023335152265517350021660 0ustar00parser = $parser; $this->fileHeaderOffset = $fileHeaderOffset; $offset = $this->findStartXref(); $reader = null; /** @noinspection TypeUnsafeComparisonInspection */ while ($offset != false) { // By doing an unsafe comparsion we ignore faulty references to byte offset 0 try { $reader = $this->readXref($offset + $this->fileHeaderOffset); } catch (CrossReferenceException $e) { // sometimes the file header offset is part of the byte offsets, so let's retry by resetting it to zero. if ($e->getCode() === CrossReferenceException::INVALID_DATA && $this->fileHeaderOffset !== 0) { $this->fileHeaderOffset = 0; $reader = $this->readXref($offset); } else { throw $e; } } $trailer = $reader->getTrailer(); $this->checkForEncryption($trailer); $this->readers[] = $reader; if (isset($trailer->value['Prev'])) { $offset = $trailer->value['Prev']->value; } else { $offset = false; } } // fix faulty sub-section header if ($reader instanceof FixedReader) { /** * @var FixedReader $reader */ $reader->fixFaultySubSectionShift(); } if ($reader === null) { throw new CrossReferenceException('No cross-reference found.', CrossReferenceException::NO_XREF_FOUND); } } /** * Get the size of the cross reference. * * @return integer */ public function getSize() { return $this->getTrailer()->value['Size']->value; } /** * Get the trailer dictionary. * * @return PdfDictionary */ public function getTrailer() { return $this->readers[0]->getTrailer(); } /** * Get the cross reference readser instances. * * @return ReaderInterface[] */ public function getReaders() { return $this->readers; } /** * Get the offset by an object number. * * @param int $objectNumber * @return integer|bool */ public function getOffsetFor($objectNumber) { foreach ($this->getReaders() as $reader) { $offset = $reader->getOffsetFor($objectNumber); if ($offset !== false) { return $offset; } } return false; } /** * Get an indirect object by its object number. * * @param int $objectNumber * @return PdfIndirectObject * @throws CrossReferenceException */ public function getIndirectObject($objectNumber) { $offset = $this->getOffsetFor($objectNumber); if ($offset === false) { throw new CrossReferenceException( \sprintf('Object (id:%s) not found.', $objectNumber), CrossReferenceException::OBJECT_NOT_FOUND ); } $parser = $this->parser; $parser->getTokenizer()->clearStack(); $parser->getStreamReader()->reset($offset + $this->fileHeaderOffset); try { /** @var PdfIndirectObject $object */ $object = $parser->readValue(null, PdfIndirectObject::class); } catch (PdfTypeException $e) { throw new CrossReferenceException( \sprintf('Object (id:%s) not found at location (%s).', $objectNumber, $offset), CrossReferenceException::OBJECT_NOT_FOUND, $e ); } if ($object->objectNumber !== $objectNumber) { throw new CrossReferenceException( \sprintf('Wrong object found, got %s while %s was expected.', $object->objectNumber, $objectNumber), CrossReferenceException::OBJECT_NOT_FOUND ); } return $object; } /** * Read the cross-reference table at a given offset. * * Internally the method will try to evaluate the best reader for this cross-reference. * * @param int $offset * @return ReaderInterface * @throws CrossReferenceException * @throws PdfTypeException */ protected function readXref($offset) { $this->parser->getStreamReader()->reset($offset); $this->parser->getTokenizer()->clearStack(); $initValue = $this->parser->readValue(); return $this->initReaderInstance($initValue); } /** * Get a cross-reference reader instance. * * @param PdfToken|PdfIndirectObject $initValue * @return ReaderInterface|bool * @throws CrossReferenceException * @throws PdfTypeException */ protected function initReaderInstance($initValue) { $position = $this->parser->getStreamReader()->getPosition() + $this->parser->getStreamReader()->getOffset() + $this->fileHeaderOffset; if ($initValue instanceof PdfToken && $initValue->value === 'xref') { try { return new FixedReader($this->parser); } catch (CrossReferenceException $e) { $this->parser->getStreamReader()->reset($position); $this->parser->getTokenizer()->clearStack(); return new LineReader($this->parser); } } if ($initValue instanceof PdfIndirectObject) { try { $stream = PdfStream::ensure($initValue->value); } catch (PdfTypeException $e) { throw new CrossReferenceException( 'Invalid object type at xref reference offset.', CrossReferenceException::INVALID_DATA, $e ); } $type = PdfDictionary::get($stream->value, 'Type'); if ($type->value !== 'XRef') { throw new CrossReferenceException( 'The xref position points to an incorrect object type.', CrossReferenceException::INVALID_DATA ); } $this->checkForEncryption($stream->value); throw new CrossReferenceException( 'This PDF document probably uses a compression technique which is not supported by the ' . 'free parser shipped with FPDI. (See https://www.setasign.com/fpdi-pdf-parser for more details)', CrossReferenceException::COMPRESSED_XREF ); } throw new CrossReferenceException( 'The xref position points to an incorrect object type.', CrossReferenceException::INVALID_DATA ); } /** * Check for encryption. * * @param PdfDictionary $dictionary * @throws CrossReferenceException */ protected function checkForEncryption(PdfDictionary $dictionary) { if (isset($dictionary->value['Encrypt'])) { throw new CrossReferenceException( 'This PDF document is encrypted and cannot be processed with FPDI.', CrossReferenceException::ENCRYPTED ); } } /** * Find the start position for the first cross-reference. * * @return int The byte-offset position of the first cross-reference. * @throws CrossReferenceException */ protected function findStartXref() { $reader = $this->parser->getStreamReader(); $reader->reset(-self::$trailerSearchLength, self::$trailerSearchLength); $buffer = $reader->getBuffer(false); $pos = \strrpos($buffer, 'startxref'); $addOffset = 9; if ($pos === false) { // Some corrupted documents uses startref, instead of startxref $pos = \strrpos($buffer, 'startref'); if ($pos === false) { throw new CrossReferenceException( 'Unable to find pointer to xref table', CrossReferenceException::NO_STARTXREF_FOUND ); } $addOffset = 8; } $reader->setOffset($pos + $addOffset); try { $value = $this->parser->readValue(null, PdfNumeric::class); } catch (PdfTypeException $e) { throw new CrossReferenceException( 'Invalid data after startxref keyword.', CrossReferenceException::INVALID_DATA, $e ); } return $value->value; } } feedback/editpdf/fpdi/src/PdfParser/CrossReference/FixedReader.php000064400000013402152265517350021124 0ustar00reader = $parser->getStreamReader(); $this->read(); parent::__construct($parser); } /** * Get all subsection data. * * @return array */ public function getSubSections() { return $this->subSections; } /** * @inheritdoc * @return int|false */ public function getOffsetFor($objectNumber) { foreach ($this->subSections as $offset => list($startObject, $objectCount)) { /** * @var int $startObject * @var int $objectCount */ if ($objectNumber >= $startObject && $objectNumber < ($startObject + $objectCount)) { $position = $offset + 20 * ($objectNumber - $startObject); $this->reader->ensure($position, 20); $line = $this->reader->readBytes(20); if ($line[17] === 'f') { return false; } return (int) \substr($line, 0, 10); } } return false; } /** * Read the cross-reference. * * This reader will only read the subsections in this method. The offsets were resolved individually by this * information. * * @throws CrossReferenceException */ protected function read() { $subSections = []; $startObject = $entryCount = $lastLineStart = null; $validityChecked = false; while (($line = $this->reader->readLine(20)) !== false) { if (\strpos($line, 'trailer') !== false) { $this->reader->reset($lastLineStart); break; } // jump over if line content doesn't match the expected string if (\sscanf($line, '%d %d', $startObject, $entryCount) !== 2) { continue; } $oldPosition = $this->reader->getPosition(); $position = $oldPosition + $this->reader->getOffset(); if (!$validityChecked && $entryCount > 0) { $nextLine = $this->reader->readBytes(21); /* Check the next line for maximum of 20 bytes and not longer * By catching 21 bytes and trimming the length should be still 21. */ if (\strlen(\trim($nextLine)) !== 21) { throw new CrossReferenceException( 'Cross-reference entries are larger than 20 bytes.', CrossReferenceException::ENTRIES_TOO_LARGE ); } /* Check for less than 20 bytes: cut the line to 20 bytes and trim; have to result in exactly 18 bytes. * If it would have less bytes the substring would get the first bytes of the next line which would * evaluate to a 20 bytes long string after trimming. */ if (\strlen(\trim(\substr($nextLine, 0, 20))) !== 18) { throw new CrossReferenceException( 'Cross-reference entries are less than 20 bytes.', CrossReferenceException::ENTRIES_TOO_SHORT ); } $validityChecked = true; } $subSections[$position] = [$startObject, $entryCount]; $lastLineStart = $position + $entryCount * 20; $this->reader->reset($lastLineStart); } // reset after the last correct parsed line $this->reader->reset($lastLineStart); if (\count($subSections) === 0) { throw new CrossReferenceException( 'No entries found in cross-reference.', CrossReferenceException::NO_ENTRIES ); } $this->subSections = $subSections; } /** * Fixes an invalid object number shift. * * This method can be used to repair documents with an invalid subsection header: * * * xref * 1 7 * 0000000000 65535 f * 0000000009 00000 n * 0000412075 00000 n * 0000412172 00000 n * 0000412359 00000 n * 0000412417 00000 n * 0000412468 00000 n * * * It shall only be called on the first table. * * @return bool */ public function fixFaultySubSectionShift() { $subSections = $this->getSubSections(); if (\count($subSections) > 1) { return false; } $subSection = \current($subSections); if ($subSection[0] != 1) { return false; } if ($this->getOffsetFor(1) === false) { foreach ($subSections as $offset => list($startObject, $objectCount)) { $this->subSections[$offset] = [$startObject - 1, $objectCount]; } return true; } return false; } } feedback/editpdf/fpdi/src/PdfParser/StreamReader.php000064400000027656152265517350016430 0ustar00stream = $stream; $this->closeStream = $closeStream; $this->reset(); } /** * The destructor. */ public function __destruct() { $this->cleanUp(); } /** * Closes the file handle. */ public function cleanUp() { if ($this->closeStream && is_resource($this->stream)) { \fclose($this->stream); } } /** * Returns the byte length of the buffer. * * @param bool $atOffset * @return int */ public function getBufferLength($atOffset = false) { if ($atOffset === false) { return $this->bufferLength; } return $this->bufferLength - $this->offset; } /** * Get the current position in the stream. * * @return int */ public function getPosition() { return $this->position; } /** * Returns the current buffer. * * @param bool $atOffset * @return string */ public function getBuffer($atOffset = true) { if ($atOffset === false) { return $this->buffer; } $string = \substr($this->buffer, $this->offset); return (string) $string; } /** * Gets a byte at a specific position in the buffer. * * If the position is invalid the method will return false. * * If the $position parameter is set to null the value of $this->offset will be used. * * @param int|null $position * @return string|bool */ public function getByte($position = null) { $position = (int) ($position !== null ? $position : $this->offset); if ( $position >= $this->bufferLength && (!$this->increaseLength() || $position >= $this->bufferLength) ) { return false; } return $this->buffer[$position]; } /** * Returns a byte at a specific position, and set the offset to the next byte position. * * If the position is invalid the method will return false. * * If the $position parameter is set to null the value of $this->offset will be used. * * @param int|null $position * @return string|bool */ public function readByte($position = null) { if ($position !== null) { $position = (int) $position; // check if needed bytes are available in the current buffer if (!($position >= $this->position && $position < $this->position + $this->bufferLength)) { $this->reset($position); $offset = $this->offset; } else { $offset = $position - $this->position; } } else { $offset = $this->offset; } if ( $offset >= $this->bufferLength && ((!$this->increaseLength()) || $offset >= $this->bufferLength) ) { return false; } $this->offset = $offset + 1; return $this->buffer[$offset]; } /** * Read bytes from the current or a specific offset position and set the internal pointer to the next byte. * * If the position is invalid the method will return false. * * If the $position parameter is set to null the value of $this->offset will be used. * * @param int $length * @param int|null $position * @return string|false */ public function readBytes($length, $position = null) { $length = (int) $length; if ($position !== null) { // check if needed bytes are available in the current buffer if (!($position >= $this->position && $position < $this->position + $this->bufferLength)) { $this->reset($position, $length); $offset = $this->offset; } else { $offset = $position - $this->position; } } else { $offset = $this->offset; } if ( ($offset + $length) > $this->bufferLength && ((!$this->increaseLength($length)) || ($offset + $length) > $this->bufferLength) ) { return false; } $bytes = \substr($this->buffer, $offset, $length); $this->offset = $offset + $length; return $bytes; } /** * Read a line from the current position. * * @param int $length * @return string|bool */ public function readLine($length = 1024) { if ($this->ensureContent() === false) { return false; } $line = ''; while ($this->ensureContent()) { $char = $this->readByte(); if ($char === "\n") { break; } if ($char === "\r") { if ($this->getByte() === "\n") { $this->addOffset(1); } break; } $line .= $char; if (\strlen($line) >= $length) { break; } } return $line; } /** * Set the offset position in the current buffer. * * @param int $offset */ public function setOffset($offset) { if ($offset > $this->bufferLength || $offset < 0) { throw new \OutOfRangeException( \sprintf('Offset (%s) out of range (length: %s)', $offset, $this->bufferLength) ); } $this->offset = (int) $offset; } /** * Returns the current offset in the current buffer. * * @return int */ public function getOffset() { return $this->offset; } /** * Add an offset to the current offset. * * @param int $offset */ public function addOffset($offset) { $this->setOffset($this->offset + $offset); } /** * Make sure that there is at least one character beyond the current offset in the buffer. * * @return bool */ public function ensureContent() { while ($this->offset >= $this->bufferLength) { if (!$this->increaseLength()) { return false; } } return true; } /** * Returns the stream. * * @return resource */ public function getStream() { return $this->stream; } /** * Gets the total available length. * * @return int */ public function getTotalLength() { if ($this->totalLength === null) { $stat = \fstat($this->stream); $this->totalLength = $stat['size']; } return $this->totalLength; } /** * Resets the buffer to a position and re-read the buffer with the given length. * * If the $pos parameter is negative the start buffer position will be the $pos'th position from * the end of the file. * * If the $pos parameter is negative and the absolute value is bigger then the totalLength of * the file $pos will set to zero. * * @param int|null $pos Start position of the new buffer * @param int $length Length of the new buffer. Mustn't be negative */ public function reset($pos = 0, $length = 200) { if ($pos === null) { $pos = $this->position + $this->offset; } elseif ($pos < 0) { $pos = \max(0, $this->getTotalLength() + $pos); } \fseek($this->stream, $pos); $this->position = $pos; $this->offset = 0; if ($length > 0) { $this->buffer = (string) \fread($this->stream, $length); } else { $this->buffer = ''; } $this->bufferLength = \strlen($this->buffer); // If a stream wrapper is in use it is possible that // length values > 8096 will be ignored, so use the // increaseLength()-method to correct that behavior if ($this->bufferLength < $length && $this->increaseLength($length - $this->bufferLength)) { // increaseLength parameter is $minLength, so cut to have only the required bytes in the buffer $this->buffer = (string) \substr($this->buffer, 0, $length); $this->bufferLength = \strlen($this->buffer); } } /** * Ensures bytes in the buffer with a specific length and location in the file. * * @param int $pos * @param int $length * @see reset() */ public function ensure($pos, $length) { if ( $pos >= $this->position && $pos < ($this->position + $this->bufferLength) && ($this->position + $this->bufferLength) >= ($pos + $length) ) { $this->offset = $pos - $this->position; } else { $this->reset($pos, $length); } } /** * Forcefully read more data into the buffer. * * @param int $minLength * @return bool Returns false if the stream reaches the end */ public function increaseLength($minLength = 100) { $length = \max($minLength, 100); if (\feof($this->stream) || $this->getTotalLength() === $this->position + $this->bufferLength) { return false; } $newLength = $this->bufferLength + $length; do { $this->buffer .= \fread($this->stream, $newLength - $this->bufferLength); $this->bufferLength = \strlen($this->buffer); } while (($this->bufferLength !== $newLength) && !\feof($this->stream)); return true; } } feedback/editpdf/fpdi/src/PdfParser/Tokenizer.php000064400000006744152265517350016017 0ustar00streamReader = $streamReader; } /** * Get the stream reader instance. * * @return StreamReader */ public function getStreamReader() { return $this->streamReader; } /** * Clear the token stack. */ public function clearStack() { $this->stack = []; } /** * Push a token onto the stack. * * @param string $token */ public function pushStack($token) { $this->stack[] = $token; } /** * Get next token. * * @return false|string */ public function getNextToken() { $token = \array_pop($this->stack); if ($token !== null) { return $token; } if (($byte = $this->streamReader->readByte()) === false) { return false; } if (\in_array($byte, ["\x20", "\x0A", "\x0D", "\x0C", "\x09", "\x00"], true)) { if ($this->leapWhiteSpaces() === false) { return false; } $byte = $this->streamReader->readByte(); } switch ($byte) { case '/': case '[': case ']': case '(': case ')': case '{': case '}': case '<': case '>': return $byte; case '%': $this->streamReader->readLine(); return $this->getNextToken(); } /* This way is faster than checking single bytes. */ $bufferOffset = $this->streamReader->getOffset(); do { $lastBuffer = $this->streamReader->getBuffer(false); $pos = \strcspn( $lastBuffer, "\x00\x09\x0A\x0C\x0D\x20()<>[]{}/%", $bufferOffset ); } while ( // Break the loop if a delimiter or white space char is matched // in the current buffer or increase the buffers length $bufferOffset + $pos === \strlen($lastBuffer) && $this->streamReader->increaseLength() ); $result = \substr($lastBuffer, $bufferOffset - 1, $pos + 1); $this->streamReader->setOffset($bufferOffset + $pos); return $result; } /** * Leap white spaces. * * @return boolean */ public function leapWhiteSpaces() { do { if (!$this->streamReader->ensureContent()) { return false; } $buffer = $this->streamReader->getBuffer(false); $matches = \strspn($buffer, "\x20\x0A\x0C\x0D\x09\x00", $this->streamReader->getOffset()); if ($matches > 0) { $this->streamReader->addOffset($matches); } } while ($this->streamReader->getOffset() >= $this->streamReader->getBufferLength()); return true; } } feedback/editpdf/fpdi/src/PdfParser/PdfParser.php000064400000030605152265517350015724 0ustar00streamReader = $streamReader; $this->tokenizer = new Tokenizer($streamReader); } /** * Removes cycled references. * * @internal */ public function cleanUp() { $this->xref = null; } /** * Get the stream reader instance. * * @return StreamReader */ public function getStreamReader() { return $this->streamReader; } /** * Get the tokenizer instance. * * @return Tokenizer */ public function getTokenizer() { return $this->tokenizer; } /** * Resolves the file header. * * @throws PdfParserException * @return int */ protected function resolveFileHeader() { if ($this->fileHeader) { return $this->fileHeaderOffset; } $this->streamReader->reset(0); $maxIterations = 1000; while (true) { $buffer = $this->streamReader->getBuffer(false); $offset = \strpos($buffer, '%PDF-'); if ($offset === false) { if (!$this->streamReader->increaseLength(100) || (--$maxIterations === 0)) { throw new PdfParserException( 'Unable to find PDF file header.', PdfParserException::FILE_HEADER_NOT_FOUND ); } continue; } break; } $this->fileHeaderOffset = $offset; $this->streamReader->setOffset($offset); $this->fileHeader = \trim($this->streamReader->readLine()); return $this->fileHeaderOffset; } /** * Get the cross-reference instance. * * @return CrossReference * @throws CrossReferenceException * @throws PdfParserException */ public function getCrossReference() { if ($this->xref === null) { $this->xref = new CrossReference($this, $this->resolveFileHeader()); } return $this->xref; } /** * Get the PDF version. * * @return int[] An array of major and minor version. * @throws PdfParserException */ public function getPdfVersion() { $this->resolveFileHeader(); if (\preg_match('/%PDF-(\d)\.(\d)/', $this->fileHeader, $result) === 0) { throw new PdfParserException( 'Unable to extract PDF version from file header.', PdfParserException::PDF_VERSION_NOT_FOUND ); } list(, $major, $minor) = $result; $catalog = $this->getCatalog(); if (isset($catalog->value['Version'])) { $versionParts = \explode( '.', PdfName::unescape(PdfType::resolve($catalog->value['Version'], $this)->value) ); if (count($versionParts) === 2) { list($major, $minor) = $versionParts; } } return [(int) $major, (int) $minor]; } /** * Get the catalog dictionary. * * @return PdfDictionary * @throws Type\PdfTypeException * @throws CrossReferenceException * @throws PdfParserException */ public function getCatalog() { $trailer = $this->getCrossReference()->getTrailer(); $catalog = PdfType::resolve(PdfDictionary::get($trailer, 'Root'), $this); return PdfDictionary::ensure($catalog); } /** * Get an indirect object by its object number. * * @param int $objectNumber * @param bool $cache * @return PdfIndirectObject * @throws CrossReferenceException * @throws PdfParserException */ public function getIndirectObject($objectNumber, $cache = false) { $objectNumber = (int) $objectNumber; if (isset($this->objects[$objectNumber])) { return $this->objects[$objectNumber]; } $object = $this->getCrossReference()->getIndirectObject($objectNumber); if ($cache) { $this->objects[$objectNumber] = $object; } return $object; } /** * Read a PDF value. * * @param null|bool|string $token * @param null|string $expectedType * @return false|PdfArray|PdfBoolean|PdfDictionary|PdfHexString|PdfIndirectObject|PdfIndirectObjectReference|PdfName|PdfNull|PdfNumeric|PdfStream|PdfString|PdfToken * @throws Type\PdfTypeException */ public function readValue($token = null, $expectedType = null) { if ($token === null) { $token = $this->tokenizer->getNextToken(); } if ($token === false) { if ($expectedType !== null) { throw new Type\PdfTypeException('Got unexpected token type.', Type\PdfTypeException::INVALID_DATA_TYPE); } return false; } switch ($token) { case '(': $this->ensureExpectedType($token, $expectedType); return $this->parsePdfString(); case '<': if ($this->streamReader->getByte() === '<') { $this->ensureExpectedType('<<', $expectedType); $this->streamReader->addOffset(1); return $this->parsePdfDictionary(); } $this->ensureExpectedType($token, $expectedType); return $this->parsePdfHexString(); case '/': $this->ensureExpectedType($token, $expectedType); return $this->parsePdfName(); case '[': $this->ensureExpectedType($token, $expectedType); return $this->parsePdfArray(); default: if (\is_numeric($token)) { $token2 = $this->tokenizer->getNextToken(); if ($token2 !== false) { if (\is_numeric($token2)) { $token3 = $this->tokenizer->getNextToken(); if ($token3 === 'obj') { if ($expectedType !== null && $expectedType !== PdfIndirectObject::class) { throw new Type\PdfTypeException( 'Got unexpected token type.', Type\PdfTypeException::INVALID_DATA_TYPE ); } return $this->parsePdfIndirectObject((int) $token, (int) $token2); } elseif ($token3 === 'R') { if ( $expectedType !== null && $expectedType !== PdfIndirectObjectReference::class ) { throw new Type\PdfTypeException( 'Got unexpected token type.', Type\PdfTypeException::INVALID_DATA_TYPE ); } return PdfIndirectObjectReference::create((int) $token, (int) $token2); } elseif ($token3 !== false) { $this->tokenizer->pushStack($token3); } } $this->tokenizer->pushStack($token2); } if ($expectedType !== null && $expectedType !== PdfNumeric::class) { throw new Type\PdfTypeException( 'Got unexpected token type.', Type\PdfTypeException::INVALID_DATA_TYPE ); } return PdfNumeric::create($token + 0); } if ($token === 'true' || $token === 'false') { $this->ensureExpectedType($token, $expectedType); return PdfBoolean::create($token === 'true'); } if ($token === 'null') { $this->ensureExpectedType($token, $expectedType); return new PdfNull(); } if ($expectedType !== null && $expectedType !== PdfToken::class) { throw new Type\PdfTypeException( 'Got unexpected token type.', Type\PdfTypeException::INVALID_DATA_TYPE ); } $v = new PdfToken(); $v->value = $token; return $v; } } /** * @return PdfString */ protected function parsePdfString() { return PdfString::parse($this->streamReader); } /** * @return false|PdfHexString */ protected function parsePdfHexString() { return PdfHexString::parse($this->streamReader); } /** * @return bool|PdfDictionary * @throws PdfTypeException */ protected function parsePdfDictionary() { return PdfDictionary::parse($this->tokenizer, $this->streamReader, $this); } /** * @return PdfName */ protected function parsePdfName() { return PdfName::parse($this->tokenizer, $this->streamReader); } /** * @return false|PdfArray * @throws PdfTypeException */ protected function parsePdfArray() { return PdfArray::parse($this->tokenizer, $this); } /** * @param int $objectNumber * @param int $generationNumber * @return false|PdfIndirectObject * @throws Type\PdfTypeException */ protected function parsePdfIndirectObject($objectNumber, $generationNumber) { return PdfIndirectObject::parse( $objectNumber, $generationNumber, $this, $this->tokenizer, $this->streamReader ); } /** * Ensures that the token will evaluate to an expected object type (or not). * * @param string $token * @param string|null $expectedType * @return bool * @throws Type\PdfTypeException */ protected function ensureExpectedType($token, $expectedType) { static $mapping = [ '(' => PdfString::class, '<' => PdfHexString::class, '<<' => PdfDictionary::class, '/' => PdfName::class, '[' => PdfArray::class, 'true' => PdfBoolean::class, 'false' => PdfBoolean::class, 'null' => PdfNull::class ]; if ($expectedType === null || $mapping[$token] === $expectedType) { return true; } throw new Type\PdfTypeException('Got unexpected token type.', Type\PdfTypeException::INVALID_DATA_TYPE); } } feedback/editpdf/fpdi/src/PdfParser/Type/PdfDictionary.php000064400000006645152265517350017525 0ustar00getNextToken(); if ($token === '>' && $streamReader->getByte() === '>') { $streamReader->addOffset(1); break; } $key = $parser->readValue($token); if ($key === false) { return false; } // ensure the first value to be a Name object if (!($key instanceof PdfName)) { $lastToken = null; // ignore all other entries and search for the closing brackets while (($token = $tokenizer->getNextToken()) !== '>' || $lastToken !== '>') { if ($token === false) { return false; } $lastToken = $token; } break; } $value = $parser->readValue(); if ($value === false) { return false; } if ($value instanceof PdfNull) { continue; } // catch missing value if ($value instanceof PdfToken && $value->value === '>' && $streamReader->getByte() === '>') { $streamReader->addOffset(1); break; } $entries[$key->value] = $value; } $v = new self(); $v->value = $entries; return $v; } /** * Helper method to create an instance. * * @param PdfType[] $entries The keys are the name entries of the dictionary. * @return self */ public static function create(array $entries = []) { $v = new self(); $v->value = $entries; return $v; } /** * Get a value by its key from a dictionary or a default value. * * @param mixed $dictionary * @param string $key * @param PdfType|null $default * @return PdfNull|PdfType * @throws PdfTypeException */ public static function get($dictionary, $key, ?PdfType $default = null) { $dictionary = self::ensure($dictionary); if (isset($dictionary->value[$key])) { return $dictionary->value[$key]; } return $default ?? new PdfNull(); } /** * Ensures that the passed value is a PdfDictionary instance. * * @param mixed $dictionary * @return self * @throws PdfTypeException */ public static function ensure($dictionary) { return PdfType::ensureType(self::class, $dictionary, 'Dictionary value expected.'); } } feedback/editpdf/fpdi/src/PdfParser/Type/PdfType.php000064400000006520152265517350016331 0ustar00getIndirectObject($value->value); } if ($value instanceof PdfIndirectObject) { if ($stopAtIndirectObject === true) { return $value; } if (\in_array($value->objectNumber, $ensuredObjectsList, true)) { throw new PdfParserException( \sprintf('Indirect reference recursion detected (%s).', $value->objectNumber) ); } $ensuredObjectsList[] = $value->objectNumber; return self::resolve($value->value, $parser, $stopAtIndirectObject, $ensuredObjectsList); } return $value; } /** * Ensure that a value is an instance of a specific PDF type. * * @param string $type * @param PdfType $value * @param string $errorMessage * @return mixed * @throws PdfTypeException */ protected static function ensureType($type, $value, $errorMessage) { if (!($value instanceof $type)) { throw new PdfTypeException( $errorMessage, PdfTypeException::INVALID_DATA_TYPE ); } return $value; } /** * Flatten indirect object references to direct objects. * * @param PdfType $value * @param PdfParser $parser * @return PdfType * @throws CrossReferenceException * @throws PdfParserException */ public static function flatten(PdfType $value, PdfParser $parser) { if ($value instanceof PdfIndirectObjectReference) { return self::flatten(self::resolve($value, $parser), $parser); } if ($value instanceof PdfDictionary || $value instanceof PdfArray) { foreach ($value->value as $key => $_value) { $value->value[$key] = self::flatten($_value, $parser); } } if ($value instanceof PdfStream) { throw new PdfTypeException('There is a stream object found which cannot be flattened to a direct object.'); } return $value; } /** * The value of the PDF type. * * @var mixed */ public $value; } feedback/editpdf/fpdi/src/PdfParser/Type/PdfIndirectObject.php000064400000004761152265517350020305 0ustar00readValue(); if ($value === false) { return false; } $nextToken = $tokenizer->getNextToken(); if ($nextToken === 'stream') { $value = PdfStream::parse($value, $reader, $parser); } elseif ($nextToken !== false) { $tokenizer->pushStack($nextToken); } $v = new self(); $v->objectNumber = (int) $objectNumber; $v->generationNumber = (int) $objectGenerationNumber; $v->value = $value; return $v; } /** * Helper method to create an instance. * * @param int $objectNumber * @param int $generationNumber * @param PdfType $value * @return self */ public static function create($objectNumber, $generationNumber, PdfType $value) { $v = new self(); $v->objectNumber = (int) $objectNumber; $v->generationNumber = (int) $generationNumber; $v->value = $value; return $v; } /** * Ensures that the passed value is a PdfIndirectObject instance. * * @param mixed $indirectObject * @return self * @throws PdfTypeException */ public static function ensure($indirectObject) { return PdfType::ensureType(self::class, $indirectObject, 'Indirect object expected.'); } /** * The object number. * * @var int */ public $objectNumber; /** * The generation number. * * @var int */ public $generationNumber; } feedback/editpdf/fpdi/src/PdfParser/Type/PdfArray.php000064400000004076152265517350016472 0ustar00getNextToken()) !== ']') { if ($token === false || ($value = $parser->readValue($token)) === false) { return false; } $result[] = $value; } $v = new self(); $v->value = $result; return $v; } /** * Helper method to create an instance. * * @param PdfType[] $values * @return self */ public static function create(array $values = []) { $v = new self(); $v->value = $values; return $v; } /** * Ensures that the passed array is a PdfArray instance with a (optional) specific size. * * @param mixed $array * @param null|int $size * @return self * @throws PdfTypeException */ public static function ensure($array, $size = null) { $result = PdfType::ensureType(self::class, $array, 'Array value expected.'); if ($size !== null && \count($array->value) !== $size) { throw new PdfTypeException( \sprintf('Array with %s entries expected.', $size), PdfTypeException::INVALID_DATA_SIZE ); } return $result; } } feedback/editpdf/fpdi/src/PdfParser/Type/PdfIndirectObjectReference.php000064400000002240152265517350022112 0ustar00value = (int) $objectNumber; $v->generationNumber = (int) $generationNumber; return $v; } /** * Ensures that the passed value is a PdfIndirectObject instance. * * @param mixed $value * @return self * @throws PdfTypeException */ public static function ensure($value) { return PdfType::ensureType(self::class, $value, 'Indirect reference value expected.'); } /** * The generation number. * * @var int */ public $generationNumber; } feedback/editpdf/fpdi/src/PdfParser/Type/PdfStream.php000064400000025761152265517350016653 0ustar00value = $dictionary; $v->reader = $reader; $v->parser = $parser; $offset = $reader->getOffset(); // Find the first "newline" while (($firstByte = $reader->getByte($offset)) !== false) { $offset++; if ($firstByte === "\n" || $firstByte === "\r") { break; } } if ($firstByte === false) { throw new PdfTypeException( 'Unable to parse stream data. No newline after the stream keyword found.', PdfTypeException::NO_NEWLINE_AFTER_STREAM_KEYWORD ); } $sndByte = $reader->getByte($offset); if ($sndByte === "\n" && $firstByte !== "\n") { $offset++; } $reader->setOffset($offset); // let's only save the byte-offset and read the stream only when needed $v->stream = $reader->getPosition() + $reader->getOffset(); return $v; } /** * Helper method to create an instance. * * @param PdfDictionary $dictionary * @param string $stream * @return self */ public static function create(PdfDictionary $dictionary, $stream) { $v = new self(); $v->value = $dictionary; $v->stream = (string) $stream; return $v; } /** * Ensures that the passed value is a PdfStream instance. * * @param mixed $stream * @return self * @throws PdfTypeException */ public static function ensure($stream) { return PdfType::ensureType(self::class, $stream, 'Stream value expected.'); } /** * The stream or its byte-offset position. * * @var int|string */ protected $stream; /** * The stream reader instance. * * @var StreamReader|null */ protected $reader; /** * The PDF parser instance. * * @var PdfParser */ protected $parser; /** * Get the stream data. * * @param bool $cache Whether cache the stream data or not. * @return bool|string * @throws PdfTypeException * @throws CrossReferenceException * @throws PdfParserException */ public function getStream($cache = false) { if (\is_int($this->stream)) { $length = PdfDictionary::get($this->value, 'Length'); if ($this->parser !== null) { $length = PdfType::resolve($length, $this->parser); } if (!($length instanceof PdfNumeric) || $length->value === 0) { $this->reader->reset($this->stream, 100000); $buffer = $this->extractStream(); } else { $this->reader->reset($this->stream, $length->value); $buffer = $this->reader->getBuffer(false); if ($this->parser !== null) { $this->reader->reset($this->stream + strlen($buffer)); $this->parser->getTokenizer()->clearStack(); $token = $this->parser->readValue(); if ($token === false || !($token instanceof PdfToken) || $token->value !== 'endstream') { $this->reader->reset($this->stream, 100000); $buffer = $this->extractStream(); $this->reader->reset($this->stream + strlen($buffer)); } } } if ($cache === false) { return $buffer; } $this->stream = $buffer; $this->reader = null; } return $this->stream; } /** * Extract the stream "manually". * * @return string * @throws PdfTypeException */ protected function extractStream() { while (true) { $buffer = $this->reader->getBuffer(false); $length = \strpos($buffer, 'endstream'); if ($length === false) { if (!$this->reader->increaseLength(100000)) { throw new PdfTypeException('Cannot extract stream.'); } continue; } break; } $buffer = \substr($buffer, 0, $length); $lastByte = \substr($buffer, -1); /* Check for EOL marker = * CARRIAGE RETURN (\r) and a LINE FEED (\n) or just a LINE FEED (\n}, * and not by a CARRIAGE RETURN (\r) alone */ if ($lastByte === "\n") { $buffer = \substr($buffer, 0, -1); $lastByte = \substr($buffer, -1); if ($lastByte === "\r") { $buffer = \substr($buffer, 0, -1); } } // There are streams in the wild, which have only white signs in them but need to be parsed manually due // to a problem encountered before (e.g. Length === 0). We should set them to empty streams to avoid problems // in further processing (e.g. applying of filters). if (trim($buffer) === '') { $buffer = ''; } return $buffer; } /** * Get all filters defined for this stream. * * @return PdfType[] * @throws PdfTypeException */ public function getFilters() { $filters = PdfDictionary::get($this->value, 'Filter'); if ($filters instanceof PdfNull) { return []; } if ($filters instanceof PdfArray) { $filters = $filters->value; } else { $filters = [$filters]; } return $filters; } /** * Get the unfiltered stream data. * * @return string * @throws FilterException * @throws PdfParserException */ public function getUnfilteredStream() { $stream = $this->getStream(); $filters = $this->getFilters(); if ($filters === []) { return $stream; } $decodeParams = PdfDictionary::get($this->value, 'DecodeParms'); if ($decodeParams instanceof PdfArray) { $decodeParams = $decodeParams->value; } else { $decodeParams = [$decodeParams]; } foreach ($filters as $key => $filter) { if (!($filter instanceof PdfName)) { continue; } $decodeParam = null; if (isset($decodeParams[$key])) { $decodeParam = ($decodeParams[$key] instanceof PdfDictionary ? $decodeParams[$key] : null); } switch ($filter->value) { case 'FlateDecode': case 'Fl': case 'LZWDecode': case 'LZW': if (\strpos($filter->value, 'LZW') === 0) { $filterObject = new Lzw(); } else { $filterObject = new Flate(); } $stream = $filterObject->decode($stream); if ($decodeParam instanceof PdfDictionary) { $predictor = PdfDictionary::get($decodeParam, 'Predictor', PdfNumeric::create(1)); if ($predictor->value !== 1) { if (!\class_exists(Predictor::class)) { throw new PdfParserException( 'This PDF document makes use of features which are only implemented in the ' . 'commercial "FPDI PDF-Parser" add-on (see https://www.setasign.com/fpdi-pdf-' . 'parser).', PdfParserException::IMPLEMENTED_IN_FPDI_PDF_PARSER ); } $colors = PdfDictionary::get($decodeParam, 'Colors', PdfNumeric::create(1)); $bitsPerComponent = PdfDictionary::get( $decodeParam, 'BitsPerComponent', PdfNumeric::create(8) ); $columns = PdfDictionary::get($decodeParam, 'Columns', PdfNumeric::create(1)); $filterObject = new Predictor( $predictor->value, $colors->value, $bitsPerComponent->value, $columns->value ); $stream = $filterObject->decode($stream); } } break; case 'ASCII85Decode': case 'A85': $filterObject = new Ascii85(); $stream = $filterObject->decode($stream); break; case 'ASCIIHexDecode': case 'AHx': $filterObject = new AsciiHex(); $stream = $filterObject->decode($stream); break; case 'Crypt': if (!$decodeParam instanceof PdfDictionary) { break; } // Filter is "Identity" $name = PdfDictionary::get($decodeParam, 'Name'); if (!$name instanceof PdfName || $name->value !== 'Identity') { break; } throw new FilterException( 'Support for Crypt filters other than "Identity" is not implemented.', FilterException::UNSUPPORTED_FILTER ); default: throw new FilterException( \sprintf('Unsupported filter "%s".', $filter->value), FilterException::UNSUPPORTED_FILTER ); } } return $stream; } } feedback/editpdf/fpdi/src/PdfParser/Type/PdfBoolean.php000064400000001617152265517350016771 0ustar00value = (bool) $value; return $v; } /** * Ensures that the passed value is a PdfBoolean instance. * * @param mixed $value * @return self * @throws PdfTypeException */ public static function ensure($value) { return PdfType::ensureType(self::class, $value, 'Boolean value expected.'); } } feedback/editpdf/fpdi/src/PdfParser/Type/PdfHexString.php000064400000003456152265517350017330 0ustar00getOffset(); while (true) { $buffer = $streamReader->getBuffer(false); $pos = \strpos($buffer, '>', $bufferOffset); if ($pos === false) { if (!$streamReader->increaseLength()) { return false; } continue; } break; } $result = \substr($buffer, $bufferOffset, $pos - $bufferOffset); $streamReader->setOffset($pos + 1); $v = new self(); $v->value = $result; return $v; } /** * Helper method to create an instance. * * @param string $string The hex encoded string. * @return self */ public static function create($string) { $v = new self(); $v->value = $string; return $v; } /** * Ensures that the passed value is a PdfHexString instance. * * @param mixed $hexString * @return self * @throws PdfTypeException */ public static function ensure($hexString) { return PdfType::ensureType(self::class, $hexString, 'Hex string value expected.'); } } feedback/editpdf/fpdi/src/PdfParser/Type/PdfTypeException.php000064400000000774152265517350020215 0ustar00getByte(), "\x00\x09\x0A\x0C\x0D\x20()<>[]{}/%") === 0) { $v->value = (string) $tokenizer->getNextToken(); return $v; } $v->value = ''; return $v; } /** * Unescapes a name string. * * @param string $value * @return string */ public static function unescape($value) { if (strpos($value, '#') === false) { return $value; } return preg_replace_callback('/#([a-fA-F\d]{2})/', function ($matches) { return chr(hexdec($matches[1])); }, $value); } /** * Helper method to create an instance. * * @param string $string * @return self */ public static function create($string) { $v = new self(); $v->value = $string; return $v; } /** * Ensures that the passed value is a PdfName instance. * * @param mixed $name * @return self * @throws PdfTypeException */ public static function ensure($name) { return PdfType::ensureType(self::class, $name, 'Name value expected.'); } } feedback/editpdf/fpdi/src/PdfParser/Type/PdfToken.php000064400000001601152265517350016463 0ustar00value = $token; return $v; } /** * Ensures that the passed value is a PdfToken instance. * * @param mixed $token * @return self * @throws PdfTypeException */ public static function ensure($token) { return PdfType::ensureType(self::class, $token, 'Token value expected.'); } } feedback/editpdf/fpdi/src/PdfParser/Type/PdfString.php000064400000013702152265517350016656 0ustar00getOffset(); $openBrackets = 1; do { $buffer = $streamReader->getBuffer(false); for ($length = \strlen($buffer); $openBrackets !== 0 && $pos < $length; $pos++) { switch ($buffer[$pos]) { case '(': $openBrackets++; break; case ')': $openBrackets--; break; case '\\': $pos++; } } } while ($openBrackets !== 0 && $streamReader->increaseLength()); $result = \substr($buffer, $startPos, $openBrackets + $pos - $startPos - 1); $streamReader->setOffset($pos); $v = new self(); $v->value = $result; return $v; } /** * Helper method to create an instance. * * @param string $value The string needs to be escaped accordingly. * @return self */ public static function create($value) { $v = new self(); $v->value = $value; return $v; } /** * Ensures that the passed value is a PdfString instance. * * @param mixed $string * @return self * @throws PdfTypeException */ public static function ensure($string) { return PdfType::ensureType(self::class, $string, 'String value expected.'); } /** * Escapes sequences in a string according to the PDF specification. * * @param string $s * @return string */ public static function escape($s) { // Still a bit faster, than direct replacing if ( \strpos($s, '\\') !== false || \strpos($s, ')') !== false || \strpos($s, '(') !== false || \strpos($s, "\x0D") !== false || \strpos($s, "\x0A") !== false || \strpos($s, "\x09") !== false || \strpos($s, "\x08") !== false || \strpos($s, "\x0C") !== false ) { // is faster than strtr(...) return \str_replace( ['\\', ')', '(', "\x0D", "\x0A", "\x09", "\x08", "\x0C"], ['\\\\', '\\)', '\\(', '\r', '\n', '\t', '\b', '\f'], $s ); } return $s; } /** * Unescapes escaped sequences in a PDF string according to the PDF specification. * * @param string $s * @return string */ public static function unescape($s) { $out = ''; /** @noinspection ForeachInvariantsInspection */ for ($count = 0, $n = \strlen($s); $count < $n; $count++) { if ($s[$count] !== '\\') { $out .= $s[$count]; } else { // A backslash at the end of the string - ignore it if ($count === ($n - 1)) { break; } switch ($s[++$count]) { case ')': case '(': case '\\': $out .= $s[$count]; break; case 'f': $out .= "\x0C"; break; case 'b': $out .= "\x08"; break; case 't': $out .= "\x09"; break; case 'r': $out .= "\x0D"; break; case 'n': $out .= "\x0A"; break; case "\r": if ($count !== $n - 1 && $s[$count + 1] === "\n") { $count++; } break; case "\n": break; default: $actualChar = \ord($s[$count]); // ascii 48 = number 0 // ascii 57 = number 9 if ($actualChar >= 48 && $actualChar <= 57) { $oct = '' . $s[$count]; /** @noinspection NotOptimalIfConditionsInspection */ if ( $count + 1 < $n && \ord($s[$count + 1]) >= 48 && \ord($s[$count + 1]) <= 57 ) { $count++; $oct .= $s[$count]; /** @noinspection NotOptimalIfConditionsInspection */ if ( $count + 1 < $n && \ord($s[$count + 1]) >= 48 && \ord($s[$count + 1]) <= 57 ) { $oct .= $s[++$count]; } } $out .= \chr(\octdec($oct)); } else { // If the character is not one of those defined, the backslash is ignored $out .= $s[$count]; } } } } return $out; } } feedback/editpdf/fpdi/src/PdfParser/Type/PdfNumeric.php000064400000001630152265517350017007 0ustar00value = $value + 0; return $v; } /** * Ensures that the passed value is a PdfNumeric instance. * * @param mixed $value * @return self * @throws PdfTypeException */ public static function ensure($value) { return PdfType::ensureType(self::class, $value, 'Numeric value expected.'); } } feedback/editpdf/fpdi/src/PdfParser/PdfParserException.php000064400000001513152265517350017577 0ustar00ctm = $ctm; } /** * @param Matrix $matrix * @return $this */ public function add(Matrix $matrix) { $this->ctm = $matrix->multiply($this->ctm); return $this; } /** * @param int|float $x * @param int|float $y * @param int|float $angle * @return $this */ public function rotate($x, $y, $angle) { if (abs($angle) < 1e-5) { return $this; } $angle = deg2rad($angle); $c = cos($angle); $s = sin($angle); $this->add(new Matrix($c, $s, -$s, $c, $x, $y)); return $this->translate(-$x, -$y); } /** * @param int|float $shiftX * @param int|float $shiftY * @return $this */ public function translate($shiftX, $shiftY) { return $this->add(new Matrix(1, 0, 0, 1, $shiftX, $shiftY)); } /** * @param int|float $scaleX * @param int|float $scaleY * @return $this */ public function scale($scaleX, $scaleY) { return $this->add(new Matrix($scaleX, 0, 0, $scaleY, 0, 0)); } /** * @param Vector $vector * @return Vector */ public function toUserSpace(Vector $vector) { return $vector->multiplyWithMatrix($this->ctm); } } feedback/editpdf/fpdi/src/Fpdi.php000064400000001354152265517350013031 0ustar00. /** * Provides an in browser PDF editor. * * @module moodle-assignfeedback_editpdf-editor */ /** * EDIT * * @namespace M.assignfeedback_editpdf * @class edit */ var EDIT = function() { /** * Starting point for the edit. * @property start * @type M.assignfeedback_editpdf.point|false * @public */ this.start = false; /** * Finishing point for the edit. * @property end * @type M.assignfeedback_editpdf.point|false * @public */ this.end = false; /** * Starting time for the edit. * @property starttime * @type int * @public */ this.starttime = 0; /** * Starting point for the currently selected annotation. * @property annotationstart * @type M.assignfeedback_editpdf.point|false * @public */ this.annotationstart = false; /** * The currently selected tool * @property tool * @type String * @public */ this.tool = "drag"; /** * The currently comment colour * @property commentcolour * @type String * @public */ this.commentcolour = 'yellow'; /** * The currently annotation colour * @property annotationcolour * @type String * @public */ this.annotationcolour = 'red'; /** * The current stamp image. * @property stamp * @type String * @public */ this.stamp = ''; /** * List of points the the current drawing path. * @property path * @type M.assignfeedback_editpdf.point[] * @public */ this.path = []; }; M.assignfeedback_editpdf = M.assignfeedback_editpdf || {}; M.assignfeedback_editpdf.edit = EDIT; feedback/editpdf/yui/src/editor/js/annotationline.js000064400000006560152265517350016610 0ustar00// This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . /** * Provides an in browser PDF editor. * * @module moodle-assignfeedback_editpdf-editor */ /** * Class representing a line. * * @namespace M.assignfeedback_editpdf * @class annotationline * @extends M.assignfeedback_editpdf.annotation */ var ANNOTATIONLINE = function(config) { ANNOTATIONLINE.superclass.constructor.apply(this, [config]); }; ANNOTATIONLINE.NAME = "annotationline"; ANNOTATIONLINE.ATTRS = {}; Y.extend(ANNOTATIONLINE, M.assignfeedback_editpdf.annotation, { /** * Draw a line annotation * @protected * @method draw * @return M.assignfeedback_editpdf.drawable */ draw: function() { var drawable, shape; drawable = new M.assignfeedback_editpdf.drawable(this.editor); shape = this.editor.graphic.addShape({ type: Y.Path, fill: false, stroke: { weight: STROKEWEIGHT, color: ANNOTATIONCOLOUR[this.colour] } }); shape.moveTo(this.x, this.y); shape.lineTo(this.endx, this.endy); shape.end(); drawable.shapes.push(shape); this.drawable = drawable; return ANNOTATIONLINE.superclass.draw.apply(this); }, /** * Draw the in progress edit. * * @public * @method draw_current_edit * @param M.assignfeedback_editpdf.edit edit */ draw_current_edit: function(edit) { var drawable = new M.assignfeedback_editpdf.drawable(this.editor), shape; shape = this.editor.graphic.addShape({ type: Y.Path, fill: false, stroke: { weight: STROKEWEIGHT, color: ANNOTATIONCOLOUR[edit.annotationcolour] } }); shape.moveTo(edit.start.x, edit.start.y); shape.lineTo(edit.end.x, edit.end.y); shape.end(); drawable.shapes.push(shape); return drawable; }, /** * Promote the current edit to a real annotation. * * @public * @method init_from_edit * @param M.assignfeedback_editpdf.edit edit * @return bool true if line bound is more than min width/height, else false. */ init_from_edit: function(edit) { this.gradeid = this.editor.get('gradeid'); this.pageno = this.editor.currentpage; this.x = edit.start.x; this.y = edit.start.y; this.endx = edit.end.x; this.endy = edit.end.y; this.colour = edit.annotationcolour; this.path = ''; return !(((this.endx - this.x) === 0) && ((this.endy - this.y) === 0)); } }); M.assignfeedback_editpdf = M.assignfeedback_editpdf || {}; M.assignfeedback_editpdf.annotationline = ANNOTATIONLINE; feedback/editpdf/yui/src/editor/js/point.js000064400000003520152265517350014710 0ustar00// This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . /** * Provides an in browser PDF editor. * * @module moodle-assignfeedback_editpdf-editor */ /** * Class representing a 2d point. * * @namespace M.assignfeedback_editpdf * @param Number x * @param Number y * @class point */ var POINT = function(x, y) { /** * X coordinate. * @property x * @type int * @public */ this.x = parseInt(x, 10); /** * Y coordinate. * @property y * @type int * @public */ this.y = parseInt(y, 10); /** * Clip this point to the rect * @method clip * @param M.assignfeedback_editpdf.point * @public */ this.clip = function(bounds) { if (this.x < bounds.x) { this.x = bounds.x; } if (this.x > (bounds.x + bounds.width)) { this.x = bounds.x + bounds.width; } if (this.y < bounds.y) { this.y = bounds.y; } if (this.y > (bounds.y + bounds.height)) { this.y = bounds.y + bounds.height; } // For chaining. return this; }; }; M.assignfeedback_editpdf = M.assignfeedback_editpdf || {}; M.assignfeedback_editpdf.point = POINT; feedback/editpdf/yui/src/editor/js/editor.js000064400000162644152265517350015062 0ustar00// This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . /* eslint-disable no-unused-vars */ /** * Provides an in browser PDF editor. * * @module moodle-assignfeedback_editpdf-editor */ /** * EDITOR * This is an in browser PDF editor. * * @namespace M.assignfeedback_editpdf * @class editor * @constructor * @extends Y.Base */ var EDITOR = function() { EDITOR.superclass.constructor.apply(this, arguments); }; EDITOR.prototype = { /** * Store old coordinates of the annotations before rotation happens. */ oldannotationcoordinates: null, /** * The dialogue used for all action menu displays. * * @property type * @type M.core.dialogue * @protected */ dialogue: null, /** * The panel used for all action menu displays. * * @property type * @type Y.Node * @protected */ panel: null, /** * The number of pages in the pdf. * * @property pagecount * @type Number * @protected */ pagecount: 0, /** * The active page in the editor. * * @property currentpage * @type Number * @protected */ currentpage: 0, /** * A list of page objects. Each page has a list of comments and annotations. * * @property pages * @type array * @protected */ pages: [], /** * The reported status of the document. * * @property documentstatus * @type int * @protected */ documentstatus: 0, /** * The yui node for the loading icon. * * @property loadingicon * @type Node * @protected */ loadingicon: null, /** * Image object of the current page image. * * @property pageimage * @type Image * @protected */ pageimage: null, /** * YUI Graphic class for drawing shapes. * * @property graphic * @type Graphic * @protected */ graphic: null, /** * Info about the current edit operation. * * @property currentedit * @type M.assignfeedback_editpdf.edit * @protected */ currentedit: new M.assignfeedback_editpdf.edit(), /** * Current drawable. * * @property currentdrawable * @type M.assignfeedback_editpdf.drawable|false * @protected */ currentdrawable: false, /** * Current drawables. * * @property drawables * @type array(M.assignfeedback_editpdf.drawable) * @protected */ drawables: [], /** * Current comment when the comment menu is open. * @property currentcomment * @type M.assignfeedback_editpdf.comment * @protected */ currentcomment: null, /** * Current annotation when the select tool is used. * @property currentannotation * @type M.assignfeedback_editpdf.annotation * @protected */ currentannotation: null, /** * Track the previous annotation so we can remove selection highlights. * @property lastannotation * @type M.assignfeedback_editpdf.annotation * @protected */ lastannotation: null, /** * Last selected annotation tool * @property lastannotationtool * @type String * @protected */ lastannotationtool: "pen", /** * The users comments quick list * @property quicklist * @type M.assignfeedback_editpdf.quickcommentlist * @protected */ quicklist: null, /** * The search comments window. * @property searchcommentswindow * @type M.core.dialogue * @protected */ searchcommentswindow: null, /** * The selected stamp picture. * @property currentstamp * @type String * @protected */ currentstamp: null, /** * The stamps. * @property stamps * @type Array * @protected */ stamps: [], /** * Prevent new comments from appearing * immediately after clicking off a current * comment * @property editingcomment * @type Boolean * @public */ editingcomment: false, /** * Should inactive comments be collapsed? * * @property collapsecomments * @type Boolean * @public */ collapsecomments: true, /** * Called during the initialisation process of the object. * @method initializer */ initializer: function() { var link; link = Y.one('#' + this.get('linkid')); if (link) { link.on('click', this.link_handler, this); link.on('key', this.link_handler, 'down:13', this); // We call the amd module to see if we can take control of the review panel. require(['mod_assign/grading_review_panel'], function(ReviewPanelManager) { var panelManager = new ReviewPanelManager(); var panel = panelManager.getReviewPanel('assignfeedback_editpdf'); if (panel) { panel = Y.one(panel); panel.empty(); link.ancestor('.fitem').hide(); this.open_in_panel(panel); } this.currentedit.start = false; this.currentedit.end = false; if (!this.get('readonly')) { this.quicklist = new M.assignfeedback_editpdf.quickcommentlist(this); } }.bind(this)); } }, /** * Called to show/hide buttons and set the current colours/stamps. * @method refresh_button_state */ refresh_button_state: function() { var button, currenttoolnode, imgurl, drawingregion, stampimgurl, drawingcanvas; // Initalise the colour buttons. button = this.get_dialogue_element(SELECTOR.COMMENTCOLOURBUTTON); imgurl = M.util.image_url('background_colour_' + this.currentedit.commentcolour, 'assignfeedback_editpdf'); button.one('img').setAttribute('src', imgurl); if (this.currentedit.commentcolour === 'clear') { button.one('img').setStyle('borderStyle', 'dashed'); } else { button.one('img').setStyle('borderStyle', 'solid'); } button = this.get_dialogue_element(SELECTOR.ANNOTATIONCOLOURBUTTON); imgurl = M.util.image_url('colour_' + this.currentedit.annotationcolour, 'assignfeedback_editpdf'); button.one('img').setAttribute('src', imgurl); currenttoolnode = this.get_dialogue_element(TOOLSELECTOR[this.currentedit.tool]); currenttoolnode.addClass('assignfeedback_editpdf_selectedbutton'); currenttoolnode.setAttribute('aria-pressed', 'true'); drawingregion = this.get_dialogue_element(SELECTOR.DRAWINGREGION); drawingregion.setAttribute('data-currenttool', this.currentedit.tool); button = this.get_dialogue_element(SELECTOR.STAMPSBUTTON); stampimgurl = this.get_stamp_image_url(this.currentedit.stamp); button.one('img').setAttrs({'src': stampimgurl, 'height': '16', 'width': '16'}); drawingcanvas = this.get_dialogue_element(SELECTOR.DRAWINGCANVAS); switch (this.currentedit.tool) { case 'drag': drawingcanvas.setStyle('cursor', 'move'); break; case 'highlight': drawingcanvas.setStyle('cursor', 'text'); break; case 'select': drawingcanvas.setStyle('cursor', 'default'); break; case 'stamp': drawingcanvas.setStyle('cursor', 'url(' + stampimgurl + '), crosshair'); break; default: drawingcanvas.setStyle('cursor', 'crosshair'); } }, /** * Called to get the bounds of the drawing region. * @method get_canvas_bounds */ get_canvas_bounds: function() { var canvas = this.get_dialogue_element(SELECTOR.DRAWINGCANVAS), offsetcanvas = canvas.getXY(), offsetleft = offsetcanvas[0], offsettop = offsetcanvas[1], width = parseInt(canvas.getStyle('width'), 10), height = parseInt(canvas.getStyle('height'), 10); return new M.assignfeedback_editpdf.rect(offsetleft, offsettop, width, height); }, /** * Called to translate from window coordinates to canvas coordinates. * @method get_canvas_coordinates * @param M.assignfeedback_editpdf.point point in window coordinats. */ get_canvas_coordinates: function(point) { var bounds = this.get_canvas_bounds(), newpoint = new M.assignfeedback_editpdf.point(point.x - bounds.x, point.y - bounds.y); bounds.x = bounds.y = 0; newpoint.clip(bounds); return newpoint; }, /** * Called to translate from canvas coordinates to window coordinates. * @method get_window_coordinates * @param M.assignfeedback_editpdf.point point in window coordinats. */ get_window_coordinates: function(point) { var bounds = this.get_canvas_bounds(), newpoint = new M.assignfeedback_editpdf.point(point.x + bounds.x, point.y + bounds.y); return newpoint; }, /** * Open the edit-pdf editor in the panel in the page instead of a popup. * @method open_in_panel */ open_in_panel: function(panel) { var drawingcanvas; this.panel = panel; panel.append(this.get('body')); panel.addClass(CSS.DIALOGUE); this.loadingicon = this.get_dialogue_element(SELECTOR.LOADINGICON); drawingcanvas = this.get_dialogue_element(SELECTOR.DRAWINGCANVAS); this.graphic = new Y.Graphic({render: drawingcanvas}); if (!this.get('readonly')) { drawingcanvas.on('gesturemovestart', this.edit_start, null, this); drawingcanvas.on('gesturemove', this.edit_move, null, this); drawingcanvas.on('gesturemoveend', this.edit_end, null, this); this.refresh_button_state(); } this.start_generation(); }, /** * Called to open the pdf editing dialogue. * @method link_handler */ link_handler: function(e) { var drawingcanvas; var resize = true; e.preventDefault(); if (!this.dialogue) { this.dialogue = new M.core.dialogue({ headerContent: this.get('header'), bodyContent: this.get('body'), footerContent: this.get('footer'), modal: true, width: '840px', visible: false, draggable: true }); // Add custom class for styling. this.dialogue.get('boundingBox').addClass(CSS.DIALOGUE); this.loadingicon = this.get_dialogue_element(SELECTOR.LOADINGICON); drawingcanvas = this.get_dialogue_element(SELECTOR.DRAWINGCANVAS); this.graphic = new Y.Graphic({render: drawingcanvas}); if (!this.get('readonly')) { drawingcanvas.on('gesturemovestart', this.edit_start, null, this); drawingcanvas.on('gesturemove', this.edit_move, null, this); drawingcanvas.on('gesturemoveend', this.edit_end, null, this); this.refresh_button_state(); } this.start_generation(); drawingcanvas.on('windowresize', this.resize, this); resize = false; } this.dialogue.centerDialogue(); this.dialogue.show(); // Redraw when the dialogue is moved, to ensure the absolute elements are all positioned correctly. this.dialogue.dd.on('drag:end', this.redraw, this); if (resize) { this.resize(); // When re-opening the dialog call redraw, to make sure the size + layout is correct. } }, /** * Called to load the information and annotations for all pages. * * @method start_generation */ start_generation: function() { this.poll_document_conversion_status(); }, /** * Poll the current document conversion status and start the next step * in the process. * * @method poll_document_conversion_status */ poll_document_conversion_status: function() { var requestUserId = this.get('userid'); Y.io(AJAXBASE, { method: 'get', context: this, sync: false, data: { sesskey: M.cfg.sesskey, action: 'pollconversions', userid: this.get('userid'), attemptnumber: this.get('attemptnumber'), assignmentid: this.get('assignmentid'), readonly: this.get('readonly') ? 1 : 0 }, on: { success: function(tid, response) { var currentUserRegion = Y.one(SELECTOR.USERINFOREGION); if (currentUserRegion) { var currentUserId = currentUserRegion.getAttribute('data-userid'); if (currentUserId && (currentUserId != requestUserId)) { // Polling conversion status needs to abort because // the current user changed. return; } } var data = this.handle_response_data(response), poll = false; if (data) { // When we are requesting the readonly version of the pages, they should // always be available (see document_services::get_page_images_for_attempt) // so we can just serve them immediately without triggering any document // conversion or polling. // // This is necessary to prevent situations where the student has updated // their submission and the teacher has annotated a previous version of // the submission in the assignment grader. In this situation if a student // views the online version of the annotated PDF ("View annotated PDF" link) // the readonly pages here and the updated pages (awaiting conversion) will // never match, and the code endlessly polls. // // See also: MDL-45580, MDL-66626, MDL-75898. if (this.get('readonly') === true) { this.prepare_pages_for_display(data); return; } this.documentstatus = data.status; if (data.status === 0) { // The combined document is still waiting for input to be ready. poll = true; } else if (data.status === 1 || data.status === 3) { // The combine document is ready for conversion into a single PDF. poll = true; } else if (data.status === 2 || data.status === -1) { // The combined PDF is ready. // We now know the page count and can convert it to a set of images. if (data.pageready == data.pagecount) { this.prepare_pages_for_display(data); } else { // Some pages are not ready yet. // Note: We use a different polling process here which does not block. this.update_page_load_progress(); // Fetch the images for the combined document. this.start_document_to_image_conversion(); } } if (poll) { // Check again in 1 second. Y.later(1000, this, this.poll_document_conversion_status); } } }, failure: function(tid, response) { return new M.core.exception(response.responseText); } } }); }, /** * Spwan the PDF to Image conversion on the server. * * @method get_images_for_documents */ start_document_to_image_conversion: function() { Y.io(AJAXBASE, { method: 'get', context: this, sync: false, data: { sesskey: M.cfg.sesskey, action: 'pollconversions', userid: this.get('userid'), attemptnumber: this.get('attemptnumber'), assignmentid: this.get('assignmentid'), readonly: this.get('readonly') ? 1 : 0 }, on: { success: function(tid, response) { var data = this.handle_response_data(response); if (data) { this.documentstatus = data.status; if (data.status === 2) { // The pages are ready. Add all of the annotations to them. this.prepare_pages_for_display(data); } } }, failure: function(tid, response) { return new M.core.exception(response.responseText); } } }); }, /** * Display an error in a small part of the page (don't block everything). * * @param string The error text. * @param boolean dismissable Not critical messages can be removed after a short display. * @protected * @method warning */ warning: function(message, dismissable) { var icontemplate = this.get_dialogue_element(SELECTOR.ICONMESSAGECONTAINER); var warningregion = this.get_dialogue_element(SELECTOR.WARNINGMESSAGECONTAINER); var delay = 15, duration = 1; var messageclasses = 'assignfeedback_editpdf_warningmessages alert alert-warning'; if (dismissable) { delay = 4; messageclasses = 'assignfeedback_editpdf_warningmessages alert alert-info'; } var warningelement = Y.Node.create(''); // Copy info icon template. warningelement.append(icontemplate.one('*').cloneNode()); // Append the message. warningelement.append(message); // Add the entire warning to the container. warningregion.prepend(warningelement); // Remove the message after a short delay. warningelement.transition( { duration: duration, delay: delay, opacity: 0 }, function() { warningelement.remove(); } ); }, /** * The info about all pages in the pdf has been returned. * * @param string The ajax response as text. * @protected * @method prepare_pages_for_display */ prepare_pages_for_display: function(data) { var i, j, comment, error, annotation, readonly; if (!data.pagecount) { if (this.dialogue) { this.dialogue.hide(); } // Display alert dialogue. error = new M.core.alert({message: M.util.get_string('cannotopenpdf', 'assignfeedback_editpdf')}); error.show(); return; } this.pagecount = data.pagecount; this.pages = data.pages; for (i = 0; i < this.pages.length; i++) { for (j = 0; j < this.pages[i].comments.length; j++) { comment = this.pages[i].comments[j]; this.pages[i].comments[j] = new M.assignfeedback_editpdf.comment(this, comment.gradeid, comment.pageno, comment.x, comment.y, comment.width, comment.colour, comment.rawtext); } for (j = 0; j < this.pages[i].annotations.length; j++) { annotation = this.pages[i].annotations[j]; this.pages[i].annotations[j] = this.create_annotation(annotation.type, annotation); } } readonly = this.get('readonly'); if (!readonly && data.partial) { // Warn about non converted files, but only for teachers. this.warning(M.util.get_string('partialwarning', 'assignfeedback_editpdf'), false); } // Update the ui. if (this.quicklist) { this.quicklist.load(); } this.setup_navigation(); this.setup_toolbar(); this.change_page(); }, /** * Fetch the page images. * * @method update_page_load_progress */ update_page_load_progress: function() { var checkconversionstatus, ajax_error_total = 0, progressbar = this.get_dialogue_element(SELECTOR.PROGRESSBARCONTAINER + ' .bar'); if (!progressbar) { return; } // If pages are not loaded, check PDF conversion status for the progress bar. checkconversionstatus = { method: 'get', context: this, sync: false, data: { sesskey: M.cfg.sesskey, action: 'conversionstatus', userid: this.get('userid'), attemptnumber: this.get('attemptnumber'), assignmentid: this.get('assignmentid') }, on: { success: function(tid, response) { ajax_error_total = 0; var progress = 0; var progressbar = this.get_dialogue_element(SELECTOR.PROGRESSBARCONTAINER + ' .bar'); if (progressbar) { // Calculate progress. progress = (response.response / this.pagecount) * 100; progressbar.setStyle('width', progress + '%'); progressbar.ancestor(SELECTOR.PROGRESSBARCONTAINER).setAttribute('aria-valuenow', progress); if (progress < 100) { // Keep polling until all pages are generated. M.util.js_pending('checkconversionstatus'); Y.later(1000, this, function() { M.util.js_complete('checkconversionstatus'); Y.io(AJAXBASEPROGRESS, checkconversionstatus); }); } } }, failure: function(tid, response) { ajax_error_total = ajax_error_total + 1; // We only continue on error if the all pages were not generated, // and if the ajax call did not produce 5 errors in the row. if (this.pagecount === 0 && ajax_error_total < 5) { M.util.js_pending('checkconversionstatus'); Y.later(1000, this, function() { M.util.js_complete('checkconversionstatus'); Y.io(AJAXBASEPROGRESS, checkconversionstatus); }); } return new M.core.exception(response.responseText); } } }; // We start the AJAX "generated page total number" call a second later to give a chance to // the AJAX "combined pdf generation" call to clean the previous submission images. M.util.js_pending('checkconversionstatus'); Y.later(1000, this, function() { ajax_error_total = 0; M.util.js_complete('checkconversionstatus'); Y.io(AJAXBASEPROGRESS, checkconversionstatus); }); }, /** * Handle response data. * * @method handle_response_data * @param {object} response * @return {object} */ handle_response_data: function(response) { var data; try { data = Y.JSON.parse(response.responseText); if (data.error) { if (this.dialogue) { this.dialogue.hide(); } new M.core.alert({ message: M.util.get_string('cannotopenpdf', 'assignfeedback_editpdf'), visible: true }); } else { return data; } } catch (e) { if (this.dialogue) { this.dialogue.hide(); } new M.core.alert({ title: M.util.get_string('cannotopenpdf', 'assignfeedback_editpdf'), visible: true }); } return; }, /** * Get the full pluginfile url for an image file - just given the filename. * * @public * @method get_stamp_image_url * @param string filename */ get_stamp_image_url: function(filename) { var urls = this.get('stampfiles'), fullurl = ''; Y.Array.each(urls, function(url) { if (url.indexOf(filename) > 0) { fullurl = url; } }, this); return fullurl; }, /** * Attach listeners and enable the color picker buttons. * @protected * @method setup_toolbar */ setup_toolbar: function() { var toolnode, commentcolourbutton, annotationcolourbutton, searchcommentsbutton, expcolcommentsbutton, rotateleftbutton, rotaterightbutton, currentstampbutton, stampfiles, picker, filename; searchcommentsbutton = this.get_dialogue_element(SELECTOR.SEARCHCOMMENTSBUTTON); searchcommentsbutton.on('click', this.open_search_comments, this); searchcommentsbutton.on('key', this.open_search_comments, 'down:13', this); expcolcommentsbutton = this.get_dialogue_element(SELECTOR.EXPCOLCOMMENTSBUTTON); expcolcommentsbutton.on('click', this.expandCollapseComments, this); expcolcommentsbutton.on('key', this.expandCollapseComments, 'down:13', this); if (this.get('readonly')) { return; } // Rotate Left. rotateleftbutton = this.get_dialogue_element(SELECTOR.ROTATELEFTBUTTON); rotateleftbutton.on('click', this.rotatePDF, this, true); rotateleftbutton.on('key', this.rotatePDF, 'down:13', this, true); // Rotate Right. rotaterightbutton = this.get_dialogue_element(SELECTOR.ROTATERIGHTBUTTON); rotaterightbutton.on('click', this.rotatePDF, this, false); rotaterightbutton.on('key', this.rotatePDF, 'down:13', this, false); this.disable_touch_scroll(); // Setup the tool buttons. Y.each(TOOLSELECTOR, function(selector, tool) { toolnode = this.get_dialogue_element(selector); toolnode.on('click', this.handle_tool_button, this, tool); toolnode.on('key', this.handle_tool_button, 'down:13', this, tool); toolnode.setAttribute('aria-pressed', 'false'); }, this); // Set the default tool. commentcolourbutton = this.get_dialogue_element(SELECTOR.COMMENTCOLOURBUTTON); picker = new M.assignfeedback_editpdf.colourpicker({ buttonNode: commentcolourbutton, colours: COMMENTCOLOUR, iconprefix: 'background_colour_', callback: function(e) { var colour = e.target.getAttribute('data-colour'); if (!colour) { colour = e.target.ancestor().getAttribute('data-colour'); } this.currentedit.commentcolour = colour; this.handle_tool_button(e, "comment"); }, context: this }); annotationcolourbutton = this.get_dialogue_element(SELECTOR.ANNOTATIONCOLOURBUTTON); picker = new M.assignfeedback_editpdf.colourpicker({ buttonNode: annotationcolourbutton, iconprefix: 'colour_', colours: ANNOTATIONCOLOUR, callback: function(e) { var colour = e.target.getAttribute('data-colour'); if (!colour) { colour = e.target.ancestor().getAttribute('data-colour'); } this.currentedit.annotationcolour = colour; if (this.lastannotationtool) { this.handle_tool_button(e, this.lastannotationtool); } else { this.handle_tool_button(e, "pen"); } }, context: this }); stampfiles = this.get('stampfiles'); if (stampfiles.length <= 0) { this.get_dialogue_element(TOOLSELECTOR.stamp).ancestor().hide(); } else { filename = stampfiles[0].substr(stampfiles[0].lastIndexOf('/') + 1); this.currentedit.stamp = filename; currentstampbutton = this.get_dialogue_element(SELECTOR.STAMPSBUTTON); picker = new M.assignfeedback_editpdf.stamppicker({ buttonNode: currentstampbutton, stamps: stampfiles, callback: function(e) { var stamp = e.target.getAttribute('data-stamp'), filename; if (!stamp) { stamp = e.target.ancestor().getAttribute('data-stamp'); } filename = stamp.substr(stamp.lastIndexOf('/')); this.currentedit.stamp = filename; this.handle_tool_button(e, "stamp"); }, context: this }); this.refresh_button_state(); } }, /** * Change the current tool. * @protected * @method handle_tool_button */ handle_tool_button: function(e, tool) { var currenttoolnode; e.preventDefault(); // Change style of the pressed button. currenttoolnode = this.get_dialogue_element(TOOLSELECTOR[this.currentedit.tool]); currenttoolnode.removeClass('assignfeedback_editpdf_selectedbutton'); currenttoolnode.setAttribute('aria-pressed', 'false'); this.currentedit.tool = tool; if (tool !== "comment" && tool !== "select" && tool !== "drag" && tool !== "stamp") { this.lastannotationtool = tool; } this.refresh_button_state(); }, /** * JSON encode the current page data - stripping out drawable references which cannot be encoded. * @protected * @method stringify_current_page * @return string */ stringify_current_page: function() { var comments = [], annotations = [], page, i = 0; for (i = 0; i < this.pages[this.currentpage].comments.length; i++) { comments[i] = this.pages[this.currentpage].comments[i].clean(); } for (i = 0; i < this.pages[this.currentpage].annotations.length; i++) { annotations[i] = this.pages[this.currentpage].annotations[i].clean(); } page = {comments: comments, annotations: annotations}; return Y.JSON.stringify(page); }, /** * Generate a drawable from the current in progress edit. * @protected * @method get_current_drawable */ get_current_drawable: function() { var comment, annotation, drawable = false; if (!this.currentedit.start || !this.currentedit.end) { return false; } if (this.currentedit.tool === 'comment') { comment = new M.assignfeedback_editpdf.comment(this); drawable = comment.draw_current_edit(this.currentedit); } else { annotation = this.create_annotation(this.currentedit.tool, {}); if (annotation) { drawable = annotation.draw_current_edit(this.currentedit); } } return drawable; }, /** * Find an element within the dialogue. * @protected * @method get_dialogue_element */ get_dialogue_element: function(selector) { if (this.panel) { return this.panel.one(selector); } else { return this.dialogue.get('boundingBox').one(selector); } }, /** * Redraw the active edit. * @protected * @method redraw_active_edit */ redraw_current_edit: function() { if (this.currentdrawable) { this.currentdrawable.erase(); } this.currentdrawable = this.get_current_drawable(); }, /** * Event handler for mousedown or touchstart. * @protected * @param Event * @method edit_start */ edit_start: function(e) { var canvas = this.get_dialogue_element(SELECTOR.DRAWINGCANVAS), offset = canvas.getXY(), scrolltop = canvas.get('docScrollY'), scrollleft = canvas.get('docScrollX'), point = {x: e.clientX - offset[0] + scrollleft, y: e.clientY - offset[1] + scrolltop}, selected = false; // Ignore right mouse click. if (e.button === 3) { return; } if (this.currentedit.starttime) { return; } if (this.editingcomment) { return; } this.currentedit.starttime = new Date().getTime(); this.currentedit.start = point; this.currentedit.end = {x: point.x, y: point.y}; if (this.currentedit.tool === 'select') { var x = this.currentedit.end.x, y = this.currentedit.end.y, annotations = this.pages[this.currentpage].annotations; // Find the first annotation whose bounds encompass the click. Y.each(annotations, function(annotation) { if (((x - annotation.x) * (x - annotation.endx)) <= 0 && ((y - annotation.y) * (y - annotation.endy)) <= 0) { selected = annotation; } }); if (selected) { this.lastannotation = this.currentannotation; this.currentannotation = selected; if (this.lastannotation && this.lastannotation !== selected) { // Redraw the last selected annotation to remove the highlight. if (this.lastannotation.drawable) { this.lastannotation.drawable.erase(); this.drawables.push(this.lastannotation.draw()); } } // Redraw the newly selected annotation to show the highlight. if (this.currentannotation.drawable) { this.currentannotation.drawable.erase(); } this.drawables.push(this.currentannotation.draw()); } else { this.lastannotation = this.currentannotation; this.currentannotation = null; // Redraw the last selected annotation to remove the highlight. if (this.lastannotation && this.lastannotation.drawable) { this.lastannotation.drawable.erase(); this.drawables.push(this.lastannotation.draw()); } } } if (this.currentannotation) { // Used to calculate drag offset. this.currentedit.annotationstart = {x: this.currentannotation.x, y: this.currentannotation.y}; } }, /** * Event handler for mousemove. * @protected * @param Event * @method edit_move */ edit_move: function(e) { var bounds = this.get_canvas_bounds(), canvas = this.get_dialogue_element(SELECTOR.DRAWINGCANVAS), drawingregion = this.get_dialogue_element(SELECTOR.DRAWINGREGION), clientpoint = new M.assignfeedback_editpdf.point(e.clientX + canvas.get('docScrollX'), e.clientY + canvas.get('docScrollY')), point = this.get_canvas_coordinates(clientpoint), activeelement = document.activeElement, diffX, diffY; if (activeelement.type === 'textarea') { return; } e.preventDefault(); // Ignore events out of the canvas area. if (point.x < 0 || point.x > bounds.width || point.y < 0 || point.y > bounds.height) { return; } if (this.currentedit.tool === 'pen') { this.currentedit.path.push(point); } if (this.currentedit.tool === 'select') { if (this.currentannotation && this.currentedit) { this.currentannotation.move(this.currentedit.annotationstart.x + point.x - this.currentedit.start.x, this.currentedit.annotationstart.y + point.y - this.currentedit.start.y); } } else if (this.currentedit.tool === 'drag') { diffX = point.x - this.currentedit.start.x; diffY = point.y - this.currentedit.start.y; drawingregion.getDOMNode().scrollLeft -= diffX; drawingregion.getDOMNode().scrollTop -= diffY; } else { if (this.currentedit.start) { this.currentedit.end = point; this.redraw_current_edit(); } } }, /** * Event handler for mouseup or touchend. * @protected * @param Event * @method edit_end */ edit_end: function() { var duration, comment, annotation; duration = new Date().getTime() - this.currentedit.start; if (duration < CLICKTIMEOUT || this.currentedit.start === false) { return; } if (this.currentedit.tool === 'comment') { if (this.currentdrawable) { this.currentdrawable.erase(); } this.currentdrawable = false; comment = new M.assignfeedback_editpdf.comment(this); if (comment.init_from_edit(this.currentedit)) { this.pages[this.currentpage].comments.push(comment); this.drawables.push(comment.draw(true)); this.editingcomment = true; } } else { annotation = this.create_annotation(this.currentedit.tool, {}); if (annotation) { if (this.currentdrawable) { this.currentdrawable.erase(); } this.currentdrawable = false; if (annotation.init_from_edit(this.currentedit)) { this.pages[this.currentpage].annotations.push(annotation); this.drawables.push(annotation.draw()); } } } // Save the changes. this.save_current_page(); // Reset the current edit. this.currentedit.starttime = 0; this.currentedit.start = false; this.currentedit.end = false; this.currentedit.path = []; }, /** * Resize the dialogue window when the browser is resized. * @public * @method resize */ resize: function() { var drawingregion, drawregionheight; if (this.dialogue) { if (!this.dialogue.get('visible')) { return; } this.dialogue.centerDialogue(); } // Make sure the dialogue box is not bigger than the max height of the viewport. drawregionheight = Y.one('body').get('winHeight') - 120; // Space for toolbar + titlebar. if (drawregionheight < 100) { drawregionheight = 100; } drawingregion = this.get_dialogue_element(SELECTOR.DRAWINGREGION); if (this.dialogue) { drawingregion.setStyle('maxHeight', drawregionheight + 'px'); } this.redraw(); return true; }, /** * Factory method for creating annotations of the correct subclass. * @public * @method create_annotation */ create_annotation: function(type, data) { data.type = type; data.editor = this; if (type === "line") { return new M.assignfeedback_editpdf.annotationline(data); } else if (type === "rectangle") { return new M.assignfeedback_editpdf.annotationrectangle(data); } else if (type === "oval") { return new M.assignfeedback_editpdf.annotationoval(data); } else if (type === "pen") { return new M.assignfeedback_editpdf.annotationpen(data); } else if (type === "highlight") { return new M.assignfeedback_editpdf.annotationhighlight(data); } else if (type === "stamp") { return new M.assignfeedback_editpdf.annotationstamp(data); } return false; }, /** * Save all the annotations and comments for the current page. * @protected * @method save_current_page */ save_current_page: function() { this.clear_warnings(false); var ajaxurl = AJAXBASE, config; config = { method: 'post', context: this, sync: false, data: { 'sesskey': M.cfg.sesskey, 'action': 'savepage', 'index': this.currentpage, 'userid': this.get('userid'), 'attemptnumber': this.get('attemptnumber'), 'assignmentid': this.get('assignmentid'), 'page': this.stringify_current_page() }, on: { success: function(tid, response) { var jsondata; try { jsondata = Y.JSON.parse(response.responseText); if (jsondata.error) { return new M.core.ajaxException(jsondata); } // Show warning that we have not saved the feedback. Y.one(SELECTOR.UNSAVEDCHANGESINPUT).set('value', 'true'); this.warning(M.util.get_string('draftchangessaved', 'assignfeedback_editpdf'), true); } catch (e) { return new M.core.exception(e); } }, failure: function(tid, response) { return new M.core.exception(response.responseText); } } }; Y.io(ajaxurl, config); }, /** * Event handler to open the comment search interface. * * @param Event e * @protected * @method open_search_comments */ open_search_comments: function(e) { if (!this.searchcommentswindow) { this.searchcommentswindow = new M.assignfeedback_editpdf.commentsearch({ editor: this }); } this.searchcommentswindow.show(); e.preventDefault(); }, /** * Toggle function to expand/collapse all comments on page. * * @protected * @method expandCollapseComments */ expandCollapseComments: function() { var comments = Y.all('.commentdrawable'); if (this.collapsecomments) { this.collapsecomments = false; comments.removeClass('commentcollapsed'); } else { this.collapsecomments = true; comments.addClass('commentcollapsed'); } }, /** * Redraw all the comments and annotations. * @protected * @method redraw */ redraw: function() { var i, page; page = this.pages[this.currentpage]; if (page === undefined) { return; // Can happen if a redraw is triggered by an event, before the page has been selected. } while (this.drawables.length > 0) { this.drawables.pop().erase(); } for (i = 0; i < page.annotations.length; i++) { this.drawables.push(page.annotations[i].draw()); } for (i = 0; i < page.comments.length; i++) { this.drawables.push(page.comments[i].draw(false)); } }, /** * Clear all current warning messages from display. * @protected * @method clear_warnings * @param {Boolean} allwarnings If true, all previous warnings are removed. */ clear_warnings: function(allwarnings) { // Remove all warning messages, they may not relate to the current document or page anymore. var warningregion = this.get_dialogue_element(SELECTOR.WARNINGMESSAGECONTAINER); if (allwarnings) { warningregion.empty(); } else { warningregion.all('.alert-info').remove(true); } }, /** * Load the image for this pdf page and remove the loading icon (if there). * @protected * @method change_page */ change_page: function() { var drawingcanvas = this.get_dialogue_element(SELECTOR.DRAWINGCANVAS), page, previousbutton, nextbutton; previousbutton = this.get_dialogue_element(SELECTOR.PREVIOUSBUTTON); nextbutton = this.get_dialogue_element(SELECTOR.NEXTBUTTON); if (this.currentpage > 0) { previousbutton.removeAttribute('disabled'); } else { previousbutton.setAttribute('disabled', 'true'); } if (this.currentpage < (this.pagecount - 1)) { nextbutton.removeAttribute('disabled'); } else { nextbutton.setAttribute('disabled', 'true'); } page = this.pages[this.currentpage]; if (this.loadingicon) { this.loadingicon.hide(); } drawingcanvas.setStyle('backgroundImage', 'url("' + page.url + '")'); drawingcanvas.setStyle('width', page.width + 'px'); drawingcanvas.setStyle('height', page.height + 'px'); drawingcanvas.scrollIntoView(); // Update page select. this.get_dialogue_element(SELECTOR.PAGESELECT).set('selectedIndex', this.currentpage); this.resize(); // Internally will call 'redraw', after checking the dialogue size. }, /** * Now we know how many pages there are, * we can enable the navigation controls. * @protected * @method setup_navigation */ setup_navigation: function() { var pageselect, i, strinfo, option, previousbutton, nextbutton; pageselect = this.get_dialogue_element(SELECTOR.PAGESELECT); var options = pageselect.all('option'); if (options.size() <= 1) { for (i = 0; i < this.pages.length; i++) { option = Y.Node.create('